Blocks and Statement Composition
Blocks group statements, establish lexical scopes, and provide the “body” form
for structured control-flow constructs like if, while, and the match
statement used for typed errors.
Surface Syntax#
A block is a sequence of zero or more statements delimited by braces:
{
stmt0;
stmt1;
...
}
The empty block {} is permitted.
Statements#
Silk currently supports these statement forms (see grammar for exact syntax):
- Local bindings:
const(compile-time constant binding; initializer must be const-evaluable),letandlet mut(andvaras an alias forlet mut),let moveandvar movefor initialization-time ownership transfer, including combined modifier forms such aslet mut moveandvar mut move, plus direct destructuring forms such aslet move (a, b) = pair;andlet move Some(value) = maybe;.- Specification-only declarations:
#const(Formal Silk; not usable in runtime expressions). - Structured blocks:
async { ... }/task { ... }(see concurrency). - Expression statements: limited to calls, assignments, and increment/decrement in the Supported forms (flow expression statements).
- Flow control:
if/elsestatements (includingif letpattern destructuring),whileloops,break,continue,return,assert,panic(typed errors),matchstatement (typed errors; see typed errors).
Semantics#
Sequencing#
Statements in a block execute in source order. If a statement transfers control
out of the current block (return, panic, break/continue inside loops),
the remainder of the block is not executed on that path.
Scope#
A block introduces a lexical scope:
- Names declared by
const/let/varare visible only after their declaration within the same block, and within any nested blocks. - Inner blocks may shadow outer bindings by reusing a name (this is a normal lexical-shadowing rule; the checker should reject only when a specific feature imposes stricter rules).
- The special name
_is a discard binding: let _ = expr;andlet _: T = expr;evaluate the initializer but do not introduce a binding into scope._may be used repeatedly in the same scope without conflicts.- Any produced runtime value is cleaned up at end-of-statement (not at scope exit).
Task(T)andPromise(T)handles are rejected in discard bindings: bind the handle to a real name if you want structured scope-exit cleanup, or consume it explicitly withyield *,await, orawait *.
Destructuring let bindings (Supported forms) bind multiple locals from a
single struct value:
-
Positional (field order):
struct User { id: u64, name: string } let (id, name) = User{ id: 123, name: "alice" }; -
Named (by field name, order-independent), with aliasing:
struct Record { id: u64, data: string } let { data, id } = Record{ id: 123, data: "a record" }; let { data as d, id as i } = Record{ id: 456, data: "other record" };
Array destructuring binds multiple locals from a single array/slice value:
struct Record { id: u64, data: string }
let records: Record[] = [{ id: 123, data: "a" }, { id: 456, data: "b" }];
let [a, b] = records;
Rules (Supported forms):
- Only flat patterns are supported (no nested destructuring).
- The initializer is required.
- The initializer must have a non-opaque
structvalue type. - The pattern must account for every field exactly once:
- positional patterns must have exactly one binder per declared field (in field order),
- named patterns must list each field exactly once (in any order),
- use
_to discard a field (let (_, name) = ...;orlet { data as _ } = ...;).
For array/slice destructuring:
- The initializer must have an array type (
T[N]) or slice type (T[]). - Each binder is positional (index order).
- The pattern binds exactly the number of listed binders:
- fixed arrays require an exact arity match (
[a, b]requiresT[2]), - slices trap at runtime if too short (as if indexing each element).
Enum destructuring binds payload elements from a single enum variant:
import std::result;
error Oops { code: int }
fn foo (oops: bool) -> std::result::Result(int, Oops) {
if (oops) {
return Err(Oops{ code: 123 });
}
return Ok(7);
}
fn main () -> int {
// Destructure `Ok(...)` and bind its payload.
// If the value is `Err(...)`, the program traps.
let Ok(value) = foo(false);
return value;
}
Rules (Supported forms):
- The initializer is required.
- The initializer must have an enum type
E(including a monomorphized generic enum). - The initializer value is consumed (moved); the original binding may not be used after destructuring.
- The pattern must be an enum variant pattern:
Variant(...)(shorthand), orE::Variant(...)/pkg::E::Variant(...)/::pkg::E::Variant(...).- Binder arity must match the variant payload arity (use
_to discard payload elements). - If the runtime value is not the matched variant, execution traps.
Refutable bindings: let ... else { ... };#
For refutable patterns where you want explicit control-flow on mismatch (instead
of trapping), Silk provides a let ... else statement form:
let <pattern> = <expr> else {
// must end with a terminal statement
};
Semantics (Supported forms):
- The initializer expression is evaluated exactly once.
- If the pattern matches, the pattern binders are introduced into the current
scope for the remainder of the block (like a normal
letbinding). let mut <pattern> = ... else { ... };introduces mutable pattern binders.let move <pattern> = ...;andlet move <pattern> = ... else { ... };consume the scrutinee for ownership-tracked values before either branch continues. Theelseblock cannot use the moved source binding, and the continuation receives the payload binders as moved values.- If the pattern does not match, the
elseblock executes. - The
elseblock must be terminal (it must not fall through), so the binders are always available after the statement on any path that continues. - The binders are not in scope inside the
elseblock.
Examples:
fn main () -> int {
let maybe: int? = Some(7);
let Some(v) = maybe else { return 0; };
return v;
}
import std::result;
error Oops { code: int }
fn foo (ok: bool) -> std::result::Result(int, Oops) {
if ok { return Ok(7); }
return Err(Oops{ code: 123 });
}
fn main () -> int {
let Ok(v) = foo(true) else { return 1; };
return v;
}
const bindings are compile-time constants:
- their initializer expression must be compile-time evaluable (otherwise the compiler reports an error),
- the binding is immutable (there is no
const mut), - a
constbinding is a normal runtime value (unlike#const), but its value is computed by the compiler at compile time and does not incur runtime computation cost in Silk currently.
In Silk currently, compile-time evaluation for runtime const
bindings is restricted to:
-
scalar primitive types (
bool, integer/float scalars,char,Instant,Duration), -
compile-time POD
structtypes whose fields are compile-time scalar value types and that do not requireDrop, and -
compile-time evaluable expressions composed of:
-
literals,
-
other
constbindings, -
calls to
const fnfunctions where all arguments are themselves compile-time evaluable, and -
struct literals and field access when the struct type is a supported compile-time POD
struct, and -
ascasts between supported scalar types, and -
a small operator subset (notably
+,-,*, bitwise ops, shifts;/and%are currently rejected forconst). -
stringbindings whose initializer is either: -
a string literal (
"..."or`...`), or -
another
conststring binding.
Example:
struct Point { x: int, y: int }
const origin: Point = Point{ x: 0, y: 0 };
const ox: int = origin.x;
Formal Silk declarations (#const) are compile-time-only names intended for specifications
(#require, #assure, #assert, #invariant, #variant, #monovariant). They must not be referenced
in runtime expressions (see formal verification and
diagnostics, E2014).
Blocks as Expressions#
The broader language design includes expression-oriented flow constructs (for
example match expressions today and if expressions).
In Silk currently:
- a block is not an expression and does not produce a value; it is purely a statement list used as the body of constructs.
The if expression form is a special-case expression-oriented construct; it
does not make { ... } a general expression form.
If/when general block expressions are introduced, the spec will define:
- which contexts accept them (and how ambiguity with
{ ... }struct literals is resolved), and - how their result values are computed.
Examples#
Nested scope#
fn main () -> int {
let x: int = 1;
{
let y: int = 2;
if x < y {
return 0;
}
}
return 1;
}
Formal Silk declarations for loop specifications#
fn main () -> int {
let limit: int = 3;
#const original_limit = limit;
let mut i: int = 0;
#invariant i >= 0;
#invariant i <= original_limit;
#variant original_limit - i;
while i < limit {
i = i + 1;
}
return 0;
}
Notes#
Supported forms:
- Block scoping for runtime
let/varbindings and nested blocks. - Formal Silk
#constdeclarations (parsed, type-checked, and rejected if used at runtime).
examples:
Source repository · Edit this page · View Markdown