Table of Contents

Functions

Raven uses a unified function model for callable declarations and callable values. A named func declaration can appear as a top-level function at namespace scope, as a member of a type where it is a method, or as a statement inside a block where it is a local function. Top-level functions are implicitly static; methods follow the usual member rules for instance and static receivers. Local functions are scoped to the containing body and may capture context from enclosing scopes.

Function expressions are the expression form of the same concept. They produce function values that can be assigned, passed, returned, or converted to compatible delegate types. Lambda syntax is the shorthand function-expression form and is described in the function-expression section below.

The rules in this section cover shared callable syntax and behavior. See Top-level functions and constants for namespace placement and import rules, Classes and members for method-specific member rules, and Function expressions for the lambda and delegate-conversion form.

Function declarations

func Foo(a: int, b: int) -> int {
    a + b
}

Arrow bodies are allowed:

func add(a: int, b: int) -> int => a + b

Function declarations (including local func statements) may carry declaration attributes with the same bracket syntax as methods:

[Trace]
func compute(x: int) -> int => x * 2

Return-targeted attribute lists are also supported on functions:

[return: MaybeNull]
func find(name: string) -> string { /* ... */ }

Attribute target prefixes are validated by declaration context:

  • [assembly: ...] is only valid as a compilation-unit attribute list (before top-level functions and constants).
  • [module: ...] is only valid as a compilation-unit attribute list and applies to module metadata.
  • [type: ...] is valid on type declarations and applies to the declared type metadata target (class, struct, interface, enum, or delegate).
  • [method: ...] is valid on function and method declarations. On a class, struct, or record declaration with a primary constructor, it applies to the synthesized primary constructor.
  • [return: ...] is only valid on callable return positions (function/method return metadata).
  • [param: ...] and [parameter: ...] are valid on parameter declarations.
  • [property: ...] is valid on property declarations.
  • [field: ...] is valid on field declarations. On a property or event declaration with a synthesized backing field, it applies to that backing field rather than to the property or event metadata.
  • [event: ...] is valid on event declarations.

If a target prefix is syntactically recognized but not valid in that position, the compiler reports an attribute-target diagnostic.

Parameters

Function, method, and accessor parameters use the name: Type syntax. Parameter names are required and participate in overload resolution alongside their types and any ref/out modifiers.

val/var binding keywords are not valid on ordinary function, method, operator, indexer, or accessor parameters. The only parameter context where val/var is valid is primary-constructor parameter promotion.

Lambda parameter lists use the same parameter form and default-value rules. Parameters in function-like declarations may include attribute lists. This applies to functions, methods, constructors, delegates, accessors, and lambdas.

Parameters may provide a default value using = expression after the type. A parameter with a default value is optional when invoking the function: callers can omit that argument and the compiler supplies the stored constant instead. Only trailing parameters may be optional; omitting an argument fixes the default for that position and all following parameters must also declare defaults.

func greet(name: string, punctuation: string = "!") {
    Console.WriteLine("Hello, ${name}${punctuation}")
}

greet("Raven")          // prints "Hello, Raven!"
greet("Raven", "!!!")    // caller-provided punctuation wins

Default expressions must be compile-time constants: literals (including null), parenthesized literals, and unary +/- applied to numeric literals. The value must convert to the parameter type using an implicit conversion. Nullable value types accept null defaults; other value types require a literal of the underlying type. Reference-type parameters accept null defaults.

When importing methods from other assemblies, Raven also recognizes optional parameters surfaced through metadata. Parameters marked optional with a stored default constant or with System.Runtime.InteropServices.DefaultParameterValueAttribute and System.Runtime.InteropServices.OptionalAttribute participate in overload resolution just like source-declared defaults. Omitted positional and named arguments are synthesized from those metadata-provided constants, after applying the same constant-conversion rules as source defaults. If only System.Runtime.InteropServices.OptionalAttribute is present, Raven will use the parameter type’s CLR default value when materializing an omitted argument, emitting default(T) for value types that lack a literal representation.

Method overloads may also be influenced by System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute. When two or more applicable candidates belong to the same overload set, Raven keeps the highest-priority candidates before applying its normal overload-comparison rules. This matches the intended .NET/C# interop behavior for both Raven source methods and imported metadata methods.

Generic functions and methods

Functions, methods, types, and local functions may declare one or more type parameters using a type parameter list written after the declaration name:

func identity<T>(value: T) -> T { value }
class Box<T> { value: T }

Each type parameter introduces a distinct generic placeholder type. Type parameter names must be unique within the same type parameter list. A type parameter is in scope within:

  • the remainder of the type parameter list
  • parameter types
  • return types
  • constraint clauses
  • the declaration body

Type parameter lists follow the same rule: commas may be written explicitly, but a newline is also a valid separator between adjacent type parameters.

Variance

Where permitted by the enclosing declaration, a type parameter may be annotated with variance:

  • out T — covariant
  • in T — contravariant

Variance annotations affect assignability of constructed generic types and are validated by the compiler according to the rules of the enclosing declaration kind.

Inline constraints

A type parameter may declare inline constraints using a colon immediately following its name:

func inner<T: struct>(value: T) -> T { value }
func map<T: class, U>(value: T) -> U { /* ... */ }

Inline constraints are syntactic sugar for an equivalent where clause on the same type parameter:

func inner<T>(value: T) -> T where T: struct { value }

The compiler normalizes inline constraints and where clauses into the same internal constraint representation.

Constraint source rule

For a given type parameter, constraints must be specified using exactly one of:

  • inline constraints (T: ...)
  • one or more where clauses targeting that parameter

Specifying constraints for the same type parameter using both forms is a compile-time error.

Constraint forms

Each constraint in a constraint list must be one of the following:

  • class — reference type constraint
  • struct — non-nullable value type constraint
  • notnull — non-null constraint
  • unmanaged — unmanaged value type constraint
  • a base class type
  • an interface type
  • new() — public parameterless constructor constraint

Constraints are conjunctive: all listed constraints must be satisfied.

The following restrictions apply:

  • At most one of class or struct may appear.
  • At most one base class constraint may appear.
  • Any number of interface constraints may appear.
  • new() may appear at most once.
  • Duplicate constraints are not permitted.

Violating any of these rules produces a compile-time diagnostic.

Constraint ordering

When written, constraints should appear in the following order:

  1. class or struct
  2. base class
  3. interfaces
  4. new()

The compiler may diagnose violations of this ordering for consistency.

Functions—including methods declared inside types—may introduce type parameters by placing <...> after the function name. Each type parameter can be used in the parameter list, return type, and body just like any other type annotation.

func identity<T>(value: T) -> T { value }

val number = identity(42)         // inferred T = int
val text = identity<string>("hi")

Call sites may omit explicit type arguments when inference can determine a unique solution from the arguments and expected return type. When inference fails—for example, because multiple type choices satisfy the call—the type arguments must be provided explicitly.

Method declarations use the same syntax, and local functions follow the exact rules when they introduce type parameters inside another body:

class Cache {
    static store<T: class>(value: T) { /* ... */ }
}

Cache.store(System.Text.StringBuilder())   // inference picks T = System.Text.StringBuilder
Cache.store<string>(null)                  // explicit type argument when passing null

Type parameter constraints mirror those on generic types. After the colon, list class, struct, or specific base/interface types that each argument must implement. Constraints are conjunctive: every listed requirement must be satisfied. The struct constraint excludes nullable value types, while class admits reference types (including nullable references). Violating a constraint produces a diagnostic identifying the failing type argument and the unmet requirement.

Local functions

Functions may be declared as statements inside other functions, methods, and block bodies. Such a function is scoped to its containing body and can capture local variables, parameters, and self from enclosing scopes. Local functions support the same generic syntax and constraints as top-level functions. Place an optional type parameter list after the function name and declare constraints using the : syntax when needed. When no instance receiver is available (for example inside static members or static func local functions), using self reports RAV2801.

func outer() {
    func inner<T: struct>(value: T) -> T { value }

    val y = inner(2)
    val point = inner((x: 1, y: 2))
}

Bodies may likewise declare local helper class, struct, record, and enum types when the type should remain encapsulated to that body. Local type declarations participate in body-local name lookup for the entire containing body, but the declared type is not exposed as a surrounding namespace or outer type member in Raven source.

Async declarations and their task-return rules are documented in Async functions.

Function expressions

Unnamed function expressions support explicit and shorthand forms: func (x: int) => x + 1, func (x: int) { x + 1 }, async func (x: int) => x + 1, static func (x: int) => x + 1, and (x: int) => x + 1 (or x => x + 1). The shorthand form is valid anywhere the equivalent explicit function expression is valid.

Function expressions start with either a parenthesized parameter list or a single identifier, optionally followed by a return-type arrow. Expression bodies use =>; func-introduced block bodies may omit => and use func (...) { ... }. Function expressions may also use modifier forms async func, static func, and static async func. Function expressions may appear wherever a function value is expected.

val addA = func (x: int) => x + 42
val addB = func (x: int) {
    x + 42
}
val addC = x => x + 42

Function expressions may optionally declare a local identifier: func Fib(n: int) => Fib(n - 1). This identifier is visible only inside the function-expression body. It does not declare a surrounding local/member name; the emitted backing method name remains compiler-generated.

Captured variables

When a function expression or local function statement (func) references a local defined in an outer scope, the compiler lifts that symbol into shared closure storage so both scopes observe the same value. Each capturing lambda or local function materializes a synthesized closure class that stores the body as an instance method and exposes fields for every captured symbol. Reads and writes in any scope access those fields directly, so mutating a var binding after creating a lambda immediately affects all delegates that captured it. Capturing self produces a reference to the enclosing instance, and capturing parameters preserves the argument value from the invoking scope. Nested lambdas reuse the closure instances produced by their enclosing scopes so that captures shared across multiple lambda layers continue to reference the same storage locations. Non-capturing lambdas are emitted using the same closure-carrier convention (with zero capture fields) so lambda emission remains uniform across contexts. Synthesized lambda method names follow C#-style metadata naming (<Method>b__...). static func declarations do not capture enclosing state, and self is not available in static contexts (RAV2801). Static function expressions are also non-capturing; attempting to capture an outer local or parameter reports RAV2204.

base is available only in instance members of classes with a base class. It produces a receiver typed as the direct base class. Invoking a member through base emits non-virtual dispatch to the selected base member, so an override on the current class is bypassed. base is not valid in interfaces, static members, or contexts without a class base receiver (RAV2802). Explicit interface implementation member access is a separate qualified-member form, for example IBase.Member(), and is not modeled as a base expression.

Parenthesized function expressions may place attribute lists immediately before the parameter list as shorthand. Leading lists are applied contextually: non-targeted attributes are applied to the first parameter, while [return: ...] lists are applied to the function expression return type.

val parse = [FromBody](content: string) => content

Function-expression parameters may also declare default values using the same trailing optional-parameter rules as functions and methods.

val format = (name: string, age: int = 1) => "$name:$age"

Function-expression parameter types are optional when the expression is converted to a known delegate type. The compiler infers the parameter types (and any ref/out modifiers) from the delegate's Invoke signature and converts the body to the delegate's return type. If no delegate context is available, diagnostic RAV2200 is reported and explicit parameter annotations are required.

Parenthesized function-expression parameter lists also support destructuring patterns as parameter entries:

  • positional deconstruction (tuple/Deconstruct style), for example ((a, b))
  • sequence deconstruction (collection style), for example ([head, ..tail])

This is primarily target-typed and inference-driven: the underlying parameter type still comes from the delegate context, and destructuring is then applied inside the lambda body.

val pickSecond: ((int, string)) -> string = ((a, b)) => b
val sumTail: (int[]) -> int = ([head, ..tail]) => head + tail[0]

Nested deconstruction is recursive in parameter patterns. Positional and sequence forms may be freely nested as long as each nested segment is compatible with its inferred input type:

val project: (((int, string), int[])) -> string =
    (((id, name), [head, ..tail])) => "$id:$name:$head:${tail.Length}"

For sequence deconstruction in lambda parameter lists, ..name and JavaScript- style ...name are both accepted as rest syntax.

When a destructuring parameter omits per-element binding keywords, elements are bound as immutable (val) by default. Compatibility is still validated against the inferred parameter type; non-deconstructable inputs produce the same deconstruction diagnostics as other pattern-based bindings (for example RAV0132).

Parameters themselves do not carry binding keywords in symbol display/tooling representations. Hover/signature text renders them as name: type (or params name: elementType for collector parameters). In primary-constructor contexts, val/var continues to mean promotion to a property rather than a mutable parameter binding.

Function expressions are target-typed: the same function expression may be assigned to, passed to, or returned as any compatible delegate type. Compatibility is determined solely by the delegate's Invoke signature (parameter types, ref/out modifiers, and return type). Delegate types themselves are not implicitly convertible between one another even when their signatures match; preserving delegate identity requires an explicit cast when converting from one delegate type to another.

Function values and method references

Functions and methods are first-class values. Referencing a function or method name without invoking it produces a delegate that can be stored, passed around, or invoked later. The compiler picks an appropriate delegate type using the same target-typing rules that guide overload resolution. In a binding such as val or var, the initializer (or an explicit type annotation) supplies that context so the delegate type—and corresponding overload—can be determined. When no delegate context is available, diagnostic RAV2201 is reported and the method must either be invoked directly or annotated with a delegate type.

val writeLine: (string) -> () = Console.WriteLine
writeLine("Hello from Raven!")

If the referenced member has no overloads, the compiler may omit the annotation and still infer the delegate type from that unique signature.

When the referenced method defines multiple overloads, Raven does not allow an unannotated binding to rely solely on type inference; such declarations are ambiguous and produce diagnostic RAV2202. To disambiguate, provide the delegate type explicitly or use another context with a well-defined target type.

val writeLine = Console.WriteLine             // error: overloaded method group
val writeLine: (string) -> () = Console.WriteLine // ok

Passing Console.WriteLine as an argument to a parameter of function type (string) -> () (equivalent to System.Action<string>) likewise selects the string overload without requiring an explicit annotation at the call site. If no overload matches the target delegate's signature, diagnostic RAV2203 is produced.

When the selected method is compatible with the target delegate only through implicit parameter/return conversion (for example, delegate parameter KeyValuePair<string, int> forwarded to method parameter object), Raven synthesizes an internal compiler-generated bridge method and binds the delegate to that bridge. The bridge performs the required implicit conversions (including boxing for value types) before invoking the selected method.

If no compatible delegate type exists in the current context, the compiler generates one whose signature matches the referenced function or method. The generated delegate observes the same parameter list (including ref/out modifiers) and return type as the source symbol so the resulting value behaves identically to directly invoking that member. Subsequent uses of the same signature within the compilation reuse the synthesized delegate.

Instance method references capture their receiver automatically. Evaluating self.Member as a value stores the current instance alongside the referenced method so later invocations execute against the same object:

class Counter {
    value: int = 3

    func Increment(delta: int) -> int { self.value + delta }

    func Run() -> int {
        val increment = self.Increment
        increment(7) // returns 10
    }
}

Method references may be passed directly to parameters of delegate type. The overload chosen for the receiving method is the one whose delegate parameter matches the referenced method's signature:

func Run(action: System.Action<string>) { action("ready") }
func Run(value: string) { Console.WriteLine(value) }

Run(Console.WriteLine) // selects the Action<string> overload

When a referenced method's signature requires a delegate that doesn't already exist—such as one with ref/out parameters—Raven synthesizes an internal delegate type and uses it as the expression's type. These delegates behave like framework-provided types and faithfully propagate modifiers:

class Accumulator {
    static func TryAccumulate(ref state: int, out doubled: int) -> bool {
        state = state + 1
        doubled = state * 2
        true
    }

    static func Execute(value: int) -> int {
        val callback = Accumulator.TryAccumulate
        var current = value
        var doubled = 0

        callback(&current, &doubled)
        current + doubled // evaluates to 12 when value is 3
    }
}