Skip to content

Blocks

Statement order, tail expressions, lexical scope, divergence, and block lifetimes.

Updated View as Markdown

A block is a brace-delimited sequence of statements with an optional final expression.

value := {
    first := read_first()
    second := read_second()
    first + second
}

Statements run in source order. The final expression, when present, is the block’s value.

A block establishes three related boundaries:

  • a lexical name scope;
  • a lifetime and cleanup scope for local values;
  • a type boundary whose tail expression determines the value.

Tail values

A tail expression is not followed by a semicolon.

fn square(value: int): int {
    value * value
}

An explicit return is equivalent at the function boundary but exits immediately rather than merely supplying the enclosing block’s value.

fn absolute(value: int): int {
    if value < 0 {
        return -value
    }
    value
}

An empty block or a block ending in a statement has type void/unit when used for effects.

if ready {
    launch()
}

Discarding a value as an expression statement does not change its computation: calls still run, errors still need handling, and effects still propagate. It only means the successful value is not used.

Statements

A block may contain bindings, local constants, expression statements, deferred work, and nested control flow.

{
    const LIMIT = 10
    mut count := 0
    defer report(count)

    for count < LIMIT {
        count += 1
    }
}

Declarations such as local constants are processed according to their own rules. An expression statement evaluates its expression and discards the value.

Scope

A local name is visible from its declaration to the end of the enclosing block.

{
    message := "inside"
    println(message)
}

// `message` is not visible here.

A nested block can read an outer binding and may shadow its name.

name := "outer"
{
    name := "inner"
    println(name)
}
println(name)

Shadowing introduces a new binding. It does not assign to the outer binding.

The initializer of a shadowing binding is resolved before the new name becomes available, so it can deliberately derive from the previous value:

name := read_name()
name := name.trim()

These are two immutable bindings with different lexical identities, not mutation of one slot.

Bindings created by a conditional pattern have the narrower scope defined by that construct. Match-arm bindings are local to their arm, while a successful refutable binding with else remains in scope after the statement.

Nested blocks

A standalone nested block is useful for limiting a name or resource lifetime.

result := {
    temporary := prepare()
    transform(temporary)
}

The value moves out through the tail expression, while other locals leave scope at the closing brace.

Returning a managed value from the block keeps that value alive through its new owner. Leaving scope releases bindings that do not escape; it does not invalidate data retained by the block result.

Destruction

The compiler inserts releases and user-defined Drop behavior at the appropriate exit points. Inner-scope resources are not intended to remain alive merely because the containing function continues.

{
    file := File.open(path)?
    process(file)
}
// The inner `file` binding is no longer usable.

Use defer for an explicit paired action whose timing should be visible in source. Ordinary reference-count maintenance does not need a manual defer.

Scope cleanup applies to every edge leaving the block:

Exit Block cleanup
tail or fall-through runs before control continues outside
return or error runs while unwinding toward the function boundary
break or continue runs for scopes left on the way to the loop target
propagated ? runs before the unsuccessful value leaves the function

Divergence

An expression that transfers control has no normal continuation. return, error, break, and continue can therefore appear in a block whose other path produces a concrete value.

value := if available {
    load()
} else {
    return fallback()
}

The returning branch does not need to manufacture a value for value because execution never reaches the binding from that branch.

Special blocks

Some keywords introduce blocks with additional semantics:

  • unsafe { ... } permits unsafe calls and operations;
  • spawn { ... } creates a child task;
  • transaction { ... } wraps integrated SQL operations atomically.

These are not interchangeable with an ordinary lexical block. Each adds checking and lowering rules documented in its own chapter.

A match arm, conditional branch, closure body, and loop body also contains blocks, so their locals follow the same lexical rules even though the surrounding construct adds selection, capture, or iteration semantics.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close