Skip to content

Blocks and Statements

Accepted

Accepted for V1 block values, divergence, and statement separation.

Blocks

Blocks can appear anywhere an expression can appear.

In expression position, a block's value comes from its last statement:

var x = {
  compute()
}

For expression statements, that means the block value is the expression value. A block with no statements evaluates to void:

var x = {}

If a block should produce a local binding's value, write that binding as the final expression:

var x = {
  const computed = compute()
  computed
}

In V1, a block ending in a local declaration completes as void:

var x = {
  const computed = compute()
  void
}

void is the empty value/result. return is syntactic sugar for return void.

More precisely, statements have completion types:

  • expression statements complete with the expression's type;
  • local declaration statements complete as void in V1;
  • return expr completes as never;
  • a non-empty block completes as its last statement;
  • an empty block completes as void.

Source syntax still distinguishes statements from expressions. Declarations are statements, not expressions.

Discarding Values

Non-final statement values may be discarded by sequencing. This is legal, but producing a meaningful value only to discard it is lintable.

Function calls are the common case:

fn run() void {
  checksum(bytes)
  void
}

Use _ = expr when discarding a produced value is intentional. This is an explicit discard expression: it evaluates the right-hand side, discards the produced value, and completes as void.

fn run() void {
  _ = checksum(bytes)
}

Function body completion is not sequencing discard. The final statement of a function body is checked against the function's return type:

fn run() void {
  checksum(bytes) // error: final completion is not void
}

Use an explicit discard expression when a value-returning call is the final function body statement and is used only for its effects:

fn run() void {
  _ = checksum(bytes)
}

The compiler lint model owns exact severities and suppression behavior for discarded meaningful values in non-final statement positions.

Divergence

return expr is a diverging expression. It has type never and is compatible with any expected branch type because control does not continue locally.

return is equivalent to:

return void

break and continue are diverging expressions inside loops. break exits the nearest enclosing loop. continue starts the next iteration of the nearest enclosing loop. Both trigger scope-exit cleanup, including pending defer statements and compiler-owned loop temporaries.

V1 does not have labeled break or continue, break values, or value-producing loops.

Statement Separation

The lexer preserves newlines, but it does not classify braces or synthesize statement separators. Newline significance is parser-contextual.

In a statement-list context, newlines and ; are statement separators. A newline separates statements only at grammar points where the parser can accept the end of the current statement. Otherwise, newline is ordinary whitespace inside the construct being parsed.

var a = first_value
var b = second_value

; is only for separating two statements on one line:

do_a(); do_b()

Lists that use commas still require commas. Newlines are not comma substitutes in parameter lists, argument lists, array literals, aggregate literals, enum cases, contract operation lists, or similar list-like constructs.

fn f(
  x: i32,
  y: i32,
) void {
}
fn f(
  x: i32
  y: i32
) void {
}

Statements may continue across a newline only when the grammar is incomplete at that point:

const x = first +
  second

If a newline appears where the parser can accept the end of the statement, the statement ends:

const x = first
  + second

V1 uses a same-line rule for trailing continuation keywords such as else and catch when the preceding expression or block could otherwise end at the newline:

const x = if ready {
  1
} else {
  2
}
const x = if ready {
  1
}
else {
  2
}

Likewise, catch must remain on the same logical statement as the expression it handles:

var bytes = read(path) catch {
  empty_bytes
}
var bytes = read(path)
catch {
  empty_bytes
}

Allowing trailing continuation keywords across a statement-separating newline is deferred to CEP-0060: Newline-Permissive Trailing Continuations.

Function and operation bodies attach only when the body opener stays on the same logical line as the signature. A newline that can end the signature also ends the declaration header, so a following { ... } or => expr does not attach as the body. Wrapped signatures may break inside incomplete syntax such as a parameter list or return type, but the body opener itself belongs with the completed signature line.

contract {
  fn len(self: *const Self) usize        // bodyless required operation

  fn is_empty(self: *const Self) bool {
    return self.len() == 0
  }
}

A signature is bodyless when no body opener is attached on the same logical line. Bodyless fn is valid only for contract required operations and deferred @extern; a bodyless fn where a body is required is an error suggesting a { ... } or => expr body. This declaration-position rule matches the same-line rule for statement-position else and catch.

Trailing empty statements are trimmed. This means:

var x = {
  compute();
}

still produces the value of compute(), but should produce a lint warning because the semicolon is unnecessary or misleading.

To explicitly produce void, end with void or use an empty block:

var x = {
  compute();
  void
}