Table of Contents

Control-flow expressions and statements

Raven lets control flow such as if produce a value. Familiar-looking forms can also be used as statements when the code is performing work rather than calculating a value.

This is a contextual surface resemblance, not the absence of a syntax distinction. Raven's syntax tree still contains separate expression and statement nodes. In an expression position, a value-producing form contributes a value to its surrounding expression. In a statement position, the corresponding statement form controls execution and any expression result is discarded. Raven is expression-oriented because it makes value-producing composition broadly available, not because every construct is an expression.

A block is an expression; its value is the value of its last expression (or () if none). Each block introduces a new scope for local declarations.

{
    val x = 10
    x + 1
}

When the same brace form appears in statement position (for example, as the body of an if statement or loop, or as a standalone scoped { ... } statement), it is bound as a block statement and its expression results are discarded.

if expression

if expressions evaluate the condition and execute exactly one branch. The value of the overall expression is the value produced by the executed branch. If both branches produce values, the result participates in type inference as described in Type inference.

Branches may be written either as block expressions ({ ... }) or as a single expression without braces:

val a = if cond { 10 } else { 20 }
val b = if cond 10 else 20
val res =
    if cond {
        10
    } else {
        20
    }

When an if expression is used purely for its effects, omitting the else branch is permitted and the value is ignored. In value contexts, provide an else branch so that both outcomes yield a value.

Pattern-binding if expression

An if expression may use the same pattern-binding header as an if let statement:

let option: Option<int> = Some(42)
let value = if let Some(x) = option {
    x
} else {
    0
}

The right-hand expression is evaluated once and matched against the pattern. If the match succeeds, the first branch runs with the pattern bindings in scope. Otherwise, the else branch runs; pattern bindings are not in scope in that branch. The value and type of the overall expression are determined from the two branches in the same way as an ordinary if expression.

The pattern and its leading let / val / var have exactly the same meaning as in statement-form if let: the leading keyword supplies the binding mode for otherwise bare captures and for an optional whole-pattern designation. An else branch is required when the expression's value is used.

while statement

while repeatedly executes its body while the condition evaluates to true.

var i = 0
while i < list.Length {
    val item = list[i]
    Console.WriteLine(item)
    i = i + 1
}

while also supports a pattern-binding condition:

while let Ok(value) = Next() {
    Console.WriteLine(value)
}

The right-hand expression is evaluated before each iteration. If the pattern matches, the body executes and pattern bindings are in scope for that iteration. If the pattern does not match, the loop exits. The leading let / val / var binding keyword is required and supplies the binding mode for otherwise bare captures inside the pattern and for an optional whole-pattern designation:

while let Person(1, name, _) person = NextPerson() {
    Console.WriteLine(person.Name)
    Console.WriteLine(name)
}

loop statement

loop repeatedly executes its body until control leaves through break, return, throw, or another abrupt exit.

var attempts = 0

loop {
    attempts += 1

    if attempts == 3 {
        break
    }
}

for statement

Iterates over each element of a collection. The iteration target may bind a fresh local, discard the element, omit the target entirely, or match a pattern against each iterated value.

for item in items {
    Console.WriteLine(item)
}

Pattern targets use the same pattern syntax as is and match. Matching elements execute the body; non-matching elements are skipped.

for (let x, 0) in points {
    Console.WriteLine(x)
}

An optional outer binding keyword may appear before the iteration target:

for let item in items {
    Console.WriteLine(item)
}

for item: int in items {
    Console.WriteLine(item)
}

for let item: int in items {
    Console.WriteLine(item)
}

for let _ in items {
    log("processing")
}

for let Person(1, name, _) in persons {
    Console.WriteLine(name)
}

for let Person(1, name, _) person in persons {
    Console.WriteLine(person.Name)
}

For pattern targets, the outer binding keyword uses the same shorthand rule as pattern/deconstruction assignment: it supplies the binding mode for otherwise bare captures inside the pattern. The same ambient binding mode also applies to an optional trailing whole-pattern designation, so person in the second example above is introduced as an immutable local. Mixing an outer binding keyword with inline pattern binding keywords in the same target is an error.

for evaluates the collection once, then executes the body for every element. Simple identifier targets resolve their element type from arrays, IEnumerable<T>, and enumerator-pattern Current members; non-generic fallbacks use object. For simple identifier targets, val and let are allowed and optional; var is rejected. A simple identifier target may include an explicit type annotation, written after the target name. The annotation is part of the iteration target, so for item: int in items declares item as int, and the collection element type must be implicitly assignable to the annotated type. This form is distinct from type-pattern syntax with a designation, which keeps the pattern shape Type designation. If the element value is unused, the iteration target may be written as _ or omitted entirely:

for _ in items {
    log("processing")
}

for in items {
    log("processing")
}

Both forms still enumerate the collection but do not introduce a new binding. Pattern targets are lowered as a per-element is check around the loop body.

Async enumeration uses await for:

async func Process(values: IAsyncEnumerable<int>) -> Task {
    await for value in values {
        Console.WriteLine(value)
    }
}

await for requires an async context and an async-enumerator pattern (GetAsyncEnumerator, MoveNextAsync, and Current). The loop is lowered before async state-machine rewriting so it works in both classic async lowering and runtime-async mode. When the collection is a range with explicit, from-start bounds, the loop iterates over integral, floating-point, char, or decimal values beginning at the range's lower bound and continuing through the upper bound. .. uses an inclusive upper bound, while ..< uses an exclusive upper bound. Omitting the start defaults it to 0, while omitting the end or using from-end bounds in a for statement reports a diagnostic.

Range-based for statements may include an optional by clause:

for x in 0..10 by 2 { }
for x in 0..<10 by 2 { }
for x in 10..0 by -3 { }
for x in 0..10.0 by 0.1 { }

The step must be non-zero. With .., if the step is positive iteration continues while the current value is less than or equal to the end bound; if the step is negative, iteration continues while the current value is greater than or equal to the end bound. With ..<, the comparisons are strict (< and > respectively). The by clause is only valid for range-based for loops.

break and continue

break immediately exits the innermost loop (loop, while, or for). continue skips the remainder of the current iteration and jumps to the next loop pass. break label and continue label target the enclosing labeled loop with that name, allowing nested loops to exit or continue an outer loop directly. Both keywords are only valid inside loops; using them elsewhere produces diagnostics (RAV2600 for break, RAV2601 for continue). They are statements, not expressions, so placing them in an expression context also triggers an error.

while cond {
    if shouldStop {
        break
    }

    if shouldSkip {
        continue
    }

    work()
}

A labeled break or continue must name an enclosing labeled loop. A label on an ordinary statement is still a valid goto target, but it is not a loop target; using it with break or continue reports RAV2606.

outer: loop {
    for value in values {
        if shouldStop(value) {
            break outer
        }
    }
}

Unreachable statements

Flow analysis tracks whether each statement in a block can execute. When a previous statement always transfers control—such as return, throw, goto, yield break, or a loop control transfer like break/continue—any following statement in the same block becomes unreachable. The compiler reports RAV0162 (warning severity) on each such statement to highlight dead code paths.

func demo(flag: bool) {
    if flag {
        return
    }

    throw System.InvalidOperationException()
    var value = 1      // RAV0162: Unreachable code detected
}

Labeled statements and goto

A label is written as identifier: ahead of a statement. Labels share a single scope within the surrounding function body: redeclaring the same label name, or using a reserved keyword as the label, produces a diagnostic. Escaped identifiers such as @loop: use the identifier's logical name for lookup, so goto @loop and @loop: refer to the same target. 【F:src/Raven.CodeAnalysis/Binder/BlockBinder.Statements.cs†L560-L609】【F:src/Raven.CodeAnalysis/Binder/BlockBinder.Statements.cs†L702-L733】

goto name transfers control to the labeled statement with the given name. Targets must be labels declared in the same function; unresolved names, missing identifiers, or reserved words report diagnostics (RAV2500RAV2502). goto statements are likewise disallowed in expression contexts. 【F:src/Raven.CodeAnalysis/Binder/BlockBinder.Statements.cs†L572-L609】【F:src/Raven.CodeAnalysis/DiagnosticDescriptors.xml†L337-L345】

func retryingWork() {
start:
    val ok = tryOnce()
    if not ok {
        goto start
    }
}

throw statement

throw expression aborts the current control path by raising an exception. The expression must be implicitly convertible to System.Exception; otherwise the compiler reports RAV1020. Statement throw is allowed inside block expressions used as scoped early-exit regions, but it remains disallowed in inline expression arms (for example, if/match expression arms), where it reports RAV1907. Use throw expression form (?? throw ...) when you need inline expression-level control flow. 【F:src/Raven.CodeAnalysis/Binder/BlockBinder.Statements.cs†L529-L557】【F:src/Raven.CodeAnalysis/DiagnosticDescriptors.xml†L231-L240】

func parse(text: string) -> int {
    if text.Length == 0 {
        throw System.ArgumentException("Value is required")
    }

    return Convert.ToInt32(text)
}

Iterator statements (yield, yield return, yield break)

Iterator methods and function expressions produce lazily-evaluated sequences by using yield statements. yield expression and yield return expression publish the next element of the sequence; the expression is converted to the iterator's element type before emission. yield break terminates the sequence early. Both forms may only appear in methods or function expressions whose return type implements System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IEnumerator<T>, System.Collections.Generic.IAsyncEnumerable<T>, System.Collections.Generic.IAsyncEnumerator<T>, or their non-generic counterparts. When such a method or function expression contains yield, the compiler rewrites it into a state machine that implements the appropriate enumerator pattern. 【F:src/Raven.CodeAnalysis/Binder/BlockBinder.Statements.cs†L489-L527】【F:src/Raven.CodeAnalysis/BoundTree/Lowering/IteratorLowerer.cs†L25-L58】【F:src/Raven.CodeAnalysis/BoundTree/Lowering/IteratorLowerer.cs†L302-L340】

For function expressions, the iterator return type may be written explicitly or inferred from the presence of yield. Unannotated synchronous iterator lambdas infer IEnumerable<T>; unannotated async iterator lambdas infer IAsyncEnumerable<T>.

import System.Collections.Generic.*

class Counter {
    func Numbers(max: int) -> IEnumerable<int> {
        var current = 0
        while current < max {
            yield current
            current = current + 1
        }

        yield break
    }
}
import System.*
import System.Collections.Generic.*

class Counter {
    func Numbers() -> Func<IEnumerable<int>> {
        val factory: Func<IEnumerable<int>> = () => {
            yield 1
            yield 2
        }

        factory
    }
}

The generated state machine preserves captured locals and surfaces the expected metadata shape for the declared iterator kind. Synchronous iterators expose Current, MoveNext, Dispose, and GetEnumerator; async iterators expose Current, MoveNextAsync, DisposeAsync, and GetAsyncEnumerator. Each yield / yield return resumes exactly where it left off on the next move call. For async iterators, a CancellationToken parameter only receives the token passed to GetAsyncEnumerator(...) when that parameter is marked with [EnumeratorCancellation]. If an async iterator has CancellationToken parameters but none is marked, Raven warns that the enumerator token will be ignored. If both a direct method argument token and a GetAsyncEnumerator(...) token are present for the marked parameter, Raven combines them so cancellation from either source is observed inside the iterator body. 【F:src/Raven.CodeAnalysis/BoundTree/Lowering/IteratorLowerer.cs†L46-L128】【F:src/Raven.CodeAnalysis/Binder/MethodBodyBinder.cs†L255-L280】【F:src/Raven.CodeAnalysis/Binder/BlockBinder.FunctionExpressions.cs†L2140-L2165】

import System.Collections.Generic.*
import System.Runtime.CompilerServices.*
import System.Threading.*
import System.Threading.Tasks.*

class Streams {
    async func People([EnumeratorCancellation] cancellationToken: CancellationToken) -> IAsyncEnumerable<Person> {
        yield Person("Bob", 30)

        await Task.Delay(1000, cancellationToken)
        yield Person("Alice", 20)
    }
}