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.