

# Blocks and Statement Composition

Blocks group statements, establish lexical scopes, and provide the “body” form
for structured control-flow constructs like `if`, [`while`](/silk/wiki/language/flow-while/), and the [`match`](/silk/wiki/language/flow-match/)
statement used for typed errors.

## Surface Syntax

A block is a sequence of zero or more statements delimited by braces:

```silk
{
  stmt0;
  stmt1;
  ...
}
```

The empty block `{}` is permitted.

## Statements

Silk currently supports these statement forms (see
[grammar](/silk/docs/language/grammar/) for exact syntax):

- Local bindings:
 - `const` (compile-time constant binding; initializer must be const-evaluable),
 - `let` and `let mut` (and `var` as an alias for `let mut`),
 - `let move` and `var move` for initialization-time ownership transfer,
 including combined modifier forms such as `let mut move` and
 `var mut move`, plus direct destructuring forms such as
 `let move (a, b) = pair;` and `let move Some(value) = maybe;`.
- Specification-only declarations: `#const` (Formal Silk; not usable in runtime expressions).
- Structured blocks: `async { ... }` / `task { ... }` (see [concurrency](/silk/docs/language/concurrency/)).
- Expression statements: limited to calls, assignments, and increment/decrement
 in the Supported forms ([flow expression statements](/silk/docs/language/flow-expression-statements/)).
- Flow control:
 - `if` / `else` statements (including `if let` pattern destructuring),
 - [`while`](/silk/wiki/language/flow-while/) loops,
 - `break`, `continue`,
 - `return`,
 - `assert`,
 - `panic` (typed errors),
 - [`match`](/silk/wiki/language/flow-match/) statement (typed errors; see [typed errors](/silk/docs/language/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`/`var` are 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;` and `let _: 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)` and `Promise(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 with `yield *`, `await`, or `await *`.

Destructuring `let` bindings (Supported forms) bind multiple locals from a
single struct value:

- Positional (field order):

  ```silk
  struct User { id: u64, name: string }
  let (id, name) = User{ id: 123, name: "alice" };
  ```

- Named (by field name, order-independent), with aliasing:

  ```silk
  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:

```silk
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 `struct` value 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) = ...;` or `let { 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]` requires `T[2]`),
 - slices trap at runtime if too short (as if indexing each element).

Enum destructuring binds payload elements from a single enum variant:

```silk
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), or
 - `E::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:

```silk
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 `let` binding).
- `let mut <pattern> = ... else { ... };` introduces mutable pattern binders.
- `let move <pattern> = ...;` and `let move <pattern> = ... else { ... };`
 consume the scrutinee for
 ownership-tracked values before either branch continues. The `else` block
 cannot use the moved source binding, and the continuation receives the
 payload binders as moved values.
- If the pattern does not match, the `else` block executes.
- The `else` block 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 `else` block.

Examples:

```silk
fn main () -> int {
  let maybe: int? = Some(7);
  let Some(v) = maybe else { return 0; };
  return v;
}
```

```silk
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 `const` binding 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 `struct` types whose fields are compile-time scalar value types and that do not require `Drop`, and
- compile-time evaluable expressions composed of:
 - literals,
 - other `const` bindings,
 - calls to `const fn` functions 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
 - `as` casts between supported scalar types, and
 - a small operator subset (notably `+`, `-`, `*`, bitwise ops, shifts; `/` and `%` are currently rejected for `const`).

- `string` bindings whose initializer is either:
 - a string literal (`"..."` or `` `...` ``), or
 - another `const` string binding.

Example:

```silk
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](/silk/docs/language/formal-verification/) and
[diagnostics](/silk/docs/compiler/diagnostics/), `E2014`).

### Blocks as Expressions

The broader language design includes expression-oriented flow constructs (for
example [`match`](/silk/wiki/language/flow-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

```silk
fn main () -> int {
  let x: int = 1;
  {
    let y: int = 2;
    if x < y {
      return 0;
    }
  }
  return 1;
}
```

### Formal Silk declarations for loop specifications

```silk
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`/`var` bindings and nested blocks.
- Formal Silk `#const` declarations (parsed, type-checked, and rejected if used at runtime).

examples:
