Skip to main content
v0.13.0

Expressions

Pattern Matching

match performs exhaustive pattern matching. All cases must be covered.

fun main() -> i64 {
let value := 1;
match (value) {
1 => 10,
_ => 0,
}
}

Each arm body can be any expression, or a blockL1. return/break/continue are themselves expressions of type ! (see §Break continue and return below), so a bare arm body like 1 => return 10 needs no special grammar case — it's just an ordinary expression arm, like any other:

// Match arm body forms start here.
fun classify(value: i64) -> i64 {
loop {
break match (value) {
0 => 0,
1 => return 10,
_ => { 20 },
};
}
}

fun main() -> i64 {
return classify(0);
}

match is an expression — all arms must produce the same type:

fun main() -> i64 {
let x := 1;
let label := match (x) {
0 => "zero",
1 => "one",
_ => "other",
};
return label.len();
}

Arms with blocks follow the same rules as function bodies: the block's tail expression (if present) is the arm's value; a block with no tail produces UnitD1.

enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}

fun main() -> i64 {
let shape := Shape::Circle { radius = 3.0 };
let desc: String := match (shape) {
Shape::Circle { radius } => {
let area := radius * radius;
(area as i64).to_string()
},
Shape::Rectangle { width, height } => "rectangle",
};
return desc.len();
}
Formal rules
Legality Rule №1

A match arm body may be either a single expression or a block, and both forms may appear in the same match expression.

Referenced by: rfc-0018

Dynamic Semantics №1

A block arm evaluates its statements and then its tail expression, if any; that tail is the arm's result, while a block with no tail produces ().

Referenced by: rfc-0018

Legality Rule №2

Bindings introduced by an arm's pattern are in scope throughout that arm's block body.

Referenced by: rfc-0018

Legality Rule №3

A match expression's scrutinee must be enclosed in parentheses — match (x) { … }. The bare form match x { … } is a parse error. A tuple scrutinee's own parentheses satisfy this (match (a, b) { … }), as does the unit literal (match () { … }).

Referenced by: rfc-0156

Tested by (2)

Pattern Kinds

PatternExampleMatches
Wildcard_anything, binds nothing
Bindingnanything, binds to n
Literal0, "hi", trueexact value
Enum variantDirection::North, Northunit variant (qualified or, since v0.11.0, bare)
Enum with fieldsShape::Circle { radius }, Circle { radius }variant, binds fields
StructPoint { x, y }, Token { kind, .. }struct, binds named fields
Tuple(a, b)tuple, binds elements
Guardn if n < 0binding + boolean condition

Examples

// Pattern examples start here.
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}

fun main() -> i64 {
let shape := Shape::Rectangle { width = 4.0, height = 2.0 };
let x := -3;
let point: (i64, i64) := (0, 7);

let a := match (shape) {
Shape::Circle { radius } => radius as i64,
Shape::Rectangle { width, height } => width as i64,
};

let b := match (x) {
0 => 0,
n if n < 0 => 1,
_ => 2,
};

let c := match (point) {
(0, 0) => 0,
(x, 0) => x,
(0, y) => y,
(x, y) => x + y,
};

return a + b + c;
}

Unqualified variant constructors

Sincev0.11.0RFC-0111

A bare variant name may be used where the expected type determines which enum is meant — the expression-position counterpart of "Unqualified variant patterns" below. Both no-field and fieldful variants participate, and per RFC-0106 the empty-brace spelling Red {} is equally valid:

enum Colour { Red, Green, Blue }

fun paint(c: Colour) { }
fun favourite() -> Colour { Green } // return type supplies the expected type

fun main() {
let c: Colour := Red; // annotation supplies it
paint(Blue); // parameter type supplies it
let p: Perhaps<i64> := Some { value = 5 };
let q: Perhaps<i64> := None; // `None` is an ordinary variant, not a literal
}

Resolution is type-directed against the expected type only — never a lexical import of variant names — so two enums may both declare Red with no ambiguity.

A bare variant is a last resort, never a shadowing mechanism. It resolves only when the name means nothing else in scope — not a binding, and not a unit struct (struct Red {} and enum C { Red } may coexist, and Red then means the struct even where a C is expected; write C::Red).

An in-scope binding wins over a variant of the same name. This is the opposite of pattern position, and deliberately so: a pattern introduces names, so a bare identifier there is always the variant, while an expression uses names and must resolve to the nearest binding or lexical scoping breaks.

fun demo(Red: i64) -> i64 {
return Red; // the parameter, not Colour::Red
}

Where no expected type exists, the bare form does not resolve and the name is reported as undefined (T0003) — there is deliberately no search for "some enum, somewhere, declaring Red". Qualify (Colour::Red) or ascribe (Red: Colour). This affects an unannotated let x = Red;, an argument to a generic callee (whose parameter types are not known until the arguments are), and the body of a closure with no declared return type. None without a determinable type keeps its existing T0002 "add a type annotation" diagnostic rather than degrading to T0003.

Formal rules
Legality Rule №1

A bare no-field or fieldful enum variant is valid in expression position when the expected type determines its enum and no binding or declaration of that name is in scope.

Referenced by: rfc-0111

Tested by (2)
Legality Rule №2

An in-scope binding of the same name takes precedence over a bare enum variant in expression position.

Referenced by: rfc-0111

Legality Rule №3

Expected types from an annotation, return type, monomorphic call parameter, or struct-literal field may direct bare-variant resolution.

Referenced by: rfc-0111

Tested by (2)
Legality Rule №4

Without an expected enum type, a bare variant does not resolve by searching other enums; the program must qualify or ascribe it.

Referenced by: rfc-0111

Unqualified variant patterns

Sincev0.11.0RFC-0107

A match arm may name an enum variant without its Enum:: prefix when the variant resolves unambiguously against the scrutinee's known enum type. The candidate enum is only the scrutinee's own type — this is type-directed resolution, not a lexical import of variant names — so there is no cross-enum collision to resolve:

enum Colour { Red, Green, Blue }

fun name(c: Colour) -> String {
match (c) {
Red => "red",
Green => "green",
Blue => "blue",
}
}

Fieldful variants may also be written bare:

fun unwrap_or_zero(v: Perhaps<i64>) -> i64 {
match (v) {
Some { value } => value,
None => 0,
}
}

Resolution happens during type-checking, against the scrutinee's concrete type. If that type is not a known enum at the point of matching (for example an abstract, aspect-bounded type parameter inside a generic function), a bare identifier is an ordinary binding, as before. A bare identifier that exactly names a no-field variant of the scrutinee's enum is always the variant, never a fresh binding — use _ or a differently-named binding for a catch-all. The fully-qualified form (Colour::Red) remains valid everywhere; qualification is optional, not removed.

Struct patterns

Sincev0.13.0

A named struct's fields may be destructured directly in a match arm, the same bare-field syntax a struct literal uses:

struct Point { x: i64, y: i64 }

fun magnitude_squared(p: Point) -> i64 {
match (p) {
Point { x, y } => x * x + y * y,
}
}

Naming every field is required unless the pattern ends in ..L1, which matches the struct against any value of that type regardless of the fields it doesn't name:

struct Token { kind: i64, span: i64, offset: i64 }

fun kind_and_span(t: Token) -> i64 {
match (t) {
Token { kind, span, .. } => kind + span,
}
}

A field's own visibility applies the same way it does to ordinary field access — see §Visibility. An external pattern (outside the struct's declaring module) that names a private field is a T0009 visibility error; the field must be omitted, which requires ...

A struct pattern's sub-patterns are always plain field bindings — there is no field: subpattern form for matching a field's own value against something other than a bare name. An unguarded struct-pattern arm is exhaustive for its struct type on its own: a struct has exactly one shape, so naming every field (or every field plus ..) always covers it.

Formal rules
Legality Rule №1

A struct pattern with no trailing .. must name every field of the struct; one that ends in .. may name any subset, including none.

Referenced by: rfc-0032

Tested by (3)

Matching through a reference

Sincev0.11.0RFC-0108

A scrutinee of reference type (&T, &var T, and chains thereof) matches against T's own patterns — reference layers are peeled before pattern resolution, the same way field access and method dispatch already auto-dereference:

enum Colour { Red, Green, Blue }

fun name(c: &Colour) -> String {
match (c) {
Colour::Red => "red",
Colour::Green => "green",
Colour::Blue => "blue",
}
}

Bindings introduced by a pattern matched through a reference copy the referent, following the ordinary type-directed copy rule (see Types§Reading a value out of a refe…).

Reference-transparency and unqualified variants compose — peeling happens first, so a bare variant resolves against the referent's enum:

fun name(c: &Colour) -> String {
match (c) {
Red => "red", // c is peeled &Colour -> Colour, then Red resolves against Colour
Green => "green",
Blue => "blue",
}
}
Formal rules
Legality Rule №1

A no-field enum variant may be written as a bare match pattern when it is a variant of the scrutinee's known enum type.

Referenced by: rfc-0107

Tested by (2)
Legality Rule №2

A fieldful enum variant may likewise omit its enum prefix in a match pattern.

Referenced by: rfc-0107

Legality Rule №3

Bare-variant pattern resolution is directed only by the scrutinee's concrete enum type; when that type is not a known enum, the identifier remains an ordinary binding.

Referenced by: rfc-0107

Legality Rule №4

A bare variant tag is not a catch-all binding and therefore does not satisfy match exhaustiveness for the enum's other variants.

Referenced by: rfc-0107

Legality Rule №5

When a bare identifier exactly names a no-field variant of the scrutinee enum, it is the variant rather than a fresh binding; _ or another name is required for a catch-all.

Referenced by: rfc-0107

Legality Rule №6

The fully qualified enum-variant pattern remains valid wherever its bare spelling is valid.

Legality Rule №7

None in pattern position is resolved by the ordinary unqualified-variant rule for a Perhaps<T> scrutinee.

Referenced by: rfc-0107

Legality Rule №1

A &T, &var T, or nested-reference scrutinee is accepted against the ordinary patterns of its referent type T.

Referenced by: rfc-0108

Legality Rule №2

Type checking a match uses the reference-peeled scrutinee type when checking its patterns.

Referenced by: rfc-0108

Legality Rule №3

Exhaustiveness checking a match uses the reference-peeled scrutinee type.

Referenced by: rfc-0108

Dynamic Semantics №1

At runtime, matching through a reference compares the patterns with the fully dereferenced scrutinee value.

Referenced by: rfc-0108

Dynamic Semantics №2

Bindings introduced while matching through a reference copy values from the peeled referent under the ordinary type-directed copy rule.

Referenced by: rfc-0108

Dynamic Semantics №3

For a reference scrutinee, match reference and match *reference compare patterns against the same referent value.

Referenced by: rfc-0110

Legality Rule №4

Reference peeling happens before unqualified-variant resolution, so a bare variant is resolved against the referent's enum type.

Referenced by: rfc-0108

Legality Rule №5

Reference transparency is limited to the match-scrutinee position and does not change the types required in call arguments or other non-match contexts.

Referenced by: rfc-0108

Control Flow

If / Else

fun main() -> i64 {
let condition := false;
let other := true;
if (condition) {
return 1;
} else if (other) {
return 2;
} else {
return 3;
}
}

if is also an expression (both branches must produce the same type):

fun main() -> i64 {
let x := 1;
let label := if (x > 0) { "positive" } else { "non-positive" };
return label.len();
}

Braceless bodies. A single expression may be used as the branch body without braces:

fun print_state() { }

fun main() -> i64 {
let debug := true;
let flag := false;
let value_a := 10;
let value_b := 20;
if (debug) print_state();
let x := if (flag) value_a else value_b;
return x;
}

The braceless form desugars to a single-expression block. Three restrictions apply:

  1. Arm style must be consistent. Both the then and else arms must use the same style — either both braced or both braceless. Mixing is a parse error.
  2. Dangling-else is forbidden. If the outer body is braceless, the body expression must not itself be an if–else. Use braces on the outer body to resolve the ambiguity.
    fun main() -> i64 {
    let a := true;
    let b := false;
    if (a) if (b) { return 1; }
    if (a) { if (b) { return 2; } else { return 3; } }
    return 4;
    }
    fun main() {
    let a := true;
    let b := false;
    if (a) if (b) { return; } else { return; }
    }
  3. No semicolon between braceless arms. Write if (c) a else b;, not if (c) a; else b; — the ; terminates the statement before the else.
Formal rules
Legality Rule №1

An if branch may be a single braceless expression.

Referenced by: rfc-0022

Legality Rule №2

A braceless if without else has type Unit and may occur wherever a Unit-typed expression is accepted.

Referenced by: rfc-0022

Legality Rule №3

A braceless if-else is an expression when its two branches have the same type.

Referenced by: rfc-0022

Legality Rule №4

A braceless outer branch cannot contain an inner if-else; braces are required to avoid dangling-else ambiguity.

Referenced by: rfc-0022

Legality Rule №5

The then and else branches of an if-else must use the same body style: both braced or both braceless.

Referenced by: rfc-0022

While

fun main() -> i64 {
var n := 3;
var total := 0;
while (n > 0) {
total += n;
n -= 1;
}
return total;
}

For

fun main() -> i64 {
var total := 0;
for (var i := 0; i < 4; i += 1) {
total += i;
}
return total;
}
Formal rules
Legality Rule №1

A C-style for initializer may declare a mutable loop-local binding with var; that binding may be reassigned by the loop body or step expression.

Referenced by: rfc-0042, rfc-0098

For-In

SinceArray and range iterationv0.1.0User-defined Iterable<T> implementationsv0.4.0

for-in works on any type implementing the Iterable<T> aspectBuilt in aspects L1. The loop variable receives type T. T[], [T; N] (array and fixed-size array), and Range (produced by .. and ..=) implement Iterable<T> by default. A T[] loop binding denotes an element of an immutable borrowed view: with move checking enabled, a non-Copy binding may be read or borrowed but not consumed. User-defined types can be made iterable by implementing Iterable<T>. The loop binding is immutable by default and may be made loop-locally mutable with varL1:

aspect Iterable<T> {
fun next(&var self) -> Perhaps<T>;
}

fun main() -> i64 {
return 0;
}
fun main() -> i64 {
let collection := [1, 2, 3];
var total := 0;
for (let item in collection) { total += item; }
for (var item in collection) {
item += 1;
total += item;
}
for (let i in 0..10) { total += i; }
for (let i in 0..=10) { total += i; }
return total;
}
Formal rules
Legality Rule №1

A for-in binding may be declared with var, making that iteration's loop-local binding mutable.

Referenced by: rfc-0042, rfc-0098

Dynamic Semantics №1

Reassigning a var for-in binding changes only that iteration's loop-local binding and does not write the replacement value back into the iterated source.

Referenced by: rfc-0042, rfc-0098

References

Sincev0.10.0

References provide explicit aliasing.

fun main() -> i64 {
var n := 1;
let p: &var i64 := &var n;
*p := 4; // write-through: mutate the referent via explicit deref
return p; // type-directed copy: reads the value out at `return`
}

Rules:

  • &expr creates a shared reference &T where expr is an addressable lvalue
  • &var x creates an exclusive reference &var T where x is a var addressable lvalue
  • *p dereferences a reference — reading the referent, or, as an assignment target (*p = v), writing through to it (see "Dereference" below)
  • reading a plain value out of a reference with no field/method involved can also go through type-directed copy (see §Reading a value out of a refe…), and field access, index, and method dispatch go through auto-deref (below)
  • assigning to a reference-typed binding (p = v) rebinds it, like any other type; *p = v is the spelling that writes through

Addressable places for both & and &var include named bindings (x), struct field access (s.field), tuple element access (t.0), array indexing (arr[i]), a dereference (*p — so &*p is a reborrow that shares the referent's storage), and chains thereof (nested.outer.field, t.1.0).

Sincev0.12.0& and &var may borrow a temporary expression

Neither &expr nor &var expr requires expr to be an addressable place. A literal, call result, struct or enum construction, or other non-addressable expression is materialized into a fresh, independent cell and referenced directly — as in foo(&Vec::new()) or foo(&var Vec::new()), with no intermediate binding. This is sound for both forms because nothing outside the expression can alias that cell.

> fun takes_ref(l: &List<i64>) -> i64 { l.len() }
> fun bump(x: &var i64) -> i64 { *x := *x + 1; *x }
> fun main() -> i64 {
> let a := takes_ref(&List::from([1, 2, 3])); // no `let` needed for the argument
> let b := bump(&var 41); // &var works on a temporary too
> return a + b;
> }

&var requires the operand to be a var binding — applying it to a plain let is a type error (T0006). &var on a lvalue path (a struct fieldD1, tuple elementD2, array elementD3, or chain of projectionsD4) produces a true exclusive reference with write-back semantics, matching &var on a named binding exactly — writes through it propagate to the original storage location (RFC-0045, already implemented; this section previously described &var struct.field as a non-propagating snapshot, which was the pre-RFC-0045 behavior and had never been updated to match). & on a field or element also aliases the original storage through the same path machinery, so later writes to the binding remain visible through the shared reference; it is still read-only, so writing through &T remains rejected. Reborrowing preserves this: &*r shares whatever storage r names, and reborrowing a &var T as &T downgrades to shared. The reverse is rejected — &var *r where r: &T is a type error (T0006), since a shared reference never grants write access.

Tuple elements are assignable like struct fields and array elements — t.0 = v, t.0 += v, and nested or chained forms (s.pair.0, t.1.0), including through a &var reference. An out-of-range index is a type error (T0003), and a shared & grants no write access (T0006).

Formal rules
Dynamic Semantics №1

Evaluating &var value.field creates an exclusive reference to that field. A write through the reference updates the corresponding field in value.

Referenced by: rfc-0045

Dynamic Semantics №2

Evaluating &var value.n creates an exclusive reference to tuple element n. A write through the reference updates that element and leaves the other tuple elements unchanged.

Referenced by: rfc-0045

Dynamic Semantics №3

Evaluating &var values[index] creates an exclusive reference to the selected array element. A write through the reference is observable through subsequent indexing.

Referenced by: rfc-0045

Dynamic Semantics №4

Evaluating &var over a chain of addressable projections creates an exclusive reference to the chain's leaf storage. A write through the reference updates that original leaf.

Referenced by: rfc-0045

Dereference

Changed inv0.11.0RFC-0110*p added; assignment to a reference-typed binding now rebinds it, use *p = v to write through

*expr dereferences a &T/&var T. As an expression it reads the referent; as an assignment target, *p = v writes through a &var T. Applying * to a non-reference is a type error (T0002).

Auto-deref covers selectors only — field access, indexing, and method dispatch, where the target of the operation is unambiguous. Everywhere else, reading through a reference is spelled explicitly:

fun add(x: i64, y: i64) -> i64 { x + y }

fun main() -> i64 {
let a := 3;
let b := 4;
let p: &i64 := &a;
let q: &i64 := &b;
return add(*p, *q) + (*p + *q); // explicit: call arguments and operands
}

Bare assignment to a reference-typed binding rebinds it rather than writing through, so a &var T can be repointed:

fun main() -> i64 {
var a := 1;
var b := 2;
var p: &var i64 := &var a;
p := &var b; // repoint: p now refers to b (p is `var`) — a stays 1
*p := 5; // write-through: b becomes 5
return a + b; // 1 + 5
}

Field- and index-path targets keep writing through with no * needed — s.field = v and arr[i] = v have no competing "rebind" reading, so they are unambiguous as they stand:

struct Point { x: i64, y: i64 }

fun main() -> i64 {
var q := Point { x = 5, y = 7 };
let qp: &var Point := &var q;
qp.y := 99; // field write-through — no `*` needed
var xs := [1, 2, 3];
let xp: &var [i64; 3] := &var xs;
xp[0] := 9; // index write-through — no `*` needed
return q.y + xs[0];
}

*(obj.field) = v and obj.field = v are synonyms; for a bare identifier target, *p = v is the only spelling that writes through.

Field access, field assignment, indexing, and method dispatch auto-dereference through a reference:

struct Counter {
value: i64,
}

extend Counter {
fun increment(&var self) {
self.value += 1;
}
}

fun main() -> i64 {
var counter := Counter { value = 0 };
let p: &var Counter := &var counter;
p.increment(); // auto-deref: equivalent to accessing through the reference directly
p.value := 1; // auto-deref field assign; the reference binding need not be var
return p.value; // auto-deref field read
}

Function references (&|| -> T and &var || -> T) are callable directly, the same way:

fun main() -> i64 {
let f := || { return 42; };
let r: &|| -> i64 := &f;
return r(); // auto-deref: calls through the reference directly
}

This applies uniformly: a closure or named function stored behind a reference can be called as if it were the function value itself. A common use is passing arrays of function references:

fun apply_all(fns: Array<&|| -> ()>) {
for (let f in fns) {
f(); // auto-deref each element
}
}

Field access, method dispatch, and calling through a reference all chain through multiple reference layers, not just one — rr: &&var Counter auto-derefs through both levels to reach the Counter for a field read, a field write, or a method call (&var self included: a shared outer layer doesn't remove the write access the inner &var layer carries, it just adds a read-only step to reach it):

struct Counter { value: i64 }

extend Counter {
fun increment(&var self) { self.value += 1; }
}

fun main() -> i64 {
var c := Counter { value = 0 };
let p: &var Counter := &var c;
let rr: &&var Counter := &p;
rr.increment(); // auto-deref through both layers
return rr.value; // likewise for a field read
}

Indexing, argument passing, and assignment remain ordinary reference operations — none of them are the value-extraction case (see types.md), so none require type-directed copy.

Formal rules
Dynamic Semantics №5

Evaluating &place or &var place produces, respectively, a shared or exclusive reference to the addressed storage; an exclusive reference can write through to that same storage.

Referenced by: rfc-0067a

Tested by (2)
Dynamic Semantics №6

Field access, field assignment, method dispatch, and calls through a reference auto-dereference through every reference layer necessary to reach their receiver.

Referenced by: rfc-0067a

Tested by (2)
Legality Rule №1

The unary * operator requires a shared or exclusive reference operand. Applying it to a non-reference is a T0002 type error.

Referenced by: rfc-0110

Legality Rule №2

Writing through *place requires an &var T reference; a shared &T never grants write access.

Referenced by: rfc-0110

Tested by (3)
Dynamic Semantics №7

Evaluating *reference reads its referent. Explicit dereference is available in every expression position, while selector operations retain their ordinary auto-dereference.

Referenced by: rfc-0110

Tested by (2)
Dynamic Semantics №8

Each leading * reads or writes through exactly one reference layer. A bare assignment to a reference-typed binding instead rebinds that binding when it is mutable.

Referenced by: rfc-0110

Tested by (2)
Dynamic Semantics №9

An assignment through a dereference writes the referenced storage; after a mutable reference binding is rebound, a later dereference writes the new referent.

Referenced by: rfc-0110

Tested by (2)
Dynamic Semantics №10

Field and index assignment through a reference remains implicit because those targets are unambiguous selectors; *(object.field) = value and object.field = value have the same write effect.

Referenced by: rfc-0110

Tested by (2)
Dynamic Semantics №11

Taking &*reference or &var *reference reborrows the storage named by the dereference; an exclusive reborrow may write that same storage.

Referenced by: rfc-0110

Loop

loop creates an infinite loopNever type D1. It is the only loop form that can produce a value:

fun main() -> i64 {
let result := loop {
break 42;
};
return result;
}

Typing rules:

  • loop { break expr; } has type T where expr: T. All break arms must produce the same type; a break expression is typechecked against its enclosing loop value typeType inference L2.
  • loop { } — a loop with no reachable break — has type ! (Never). See §Never type.

Break, Continue, and Return

Sincev0.10.0

return, break, and continue are expressions of type !Never type D1 (Never — see §Never type), not statements. Since ! is a subtype of every type, they're valid anywhere an expression is valid — a block tail with no trailing ;, a braceless if-arm, a match-arm body, or nested inside another expression — not just as a semicolon-terminated statement on its own line:

fun pick(ok: boolean) -> i64 {
if (ok) return 42; // braceless if-arm, no braces needed
0
}

fun compute() -> i64 {
var i := 0;
loop {
i := i + 1;
if (i == 5) {
break i * 10 // loop-body tail, no trailing `;`
}
}
}

fun classify(value: i64) -> i64 {
match (value) {
0 => 0,
1 => return 10, // match-arm body, same as any other expression arm
_ => 20,
}
}

fun nested(c: boolean) -> i64 {
let x := if (c) return 99 else 0; // nested expression position
x
}

break exits the innermost loopD1; break expr exits a loop and produces expr as the loop's value (break with no value produces Unit). continue skips to the next iteration of the innermost loopD2. return/ return expr returns from the enclosing functionD3, using the function's declared return type (or Unit, if omitted):

fun returns_unit() {
return;
}

fun returns_value() -> i64 {
return 42;
}

fun main() -> i64 {
returns_unit();
return returns_value();
}
Formal rules
Dynamic Semantics №1

break transfers control out of the innermost enclosing loop. In a value-producing loop, break expr supplies that loop's result and bare break supplies ().

Tested by (3)
Dynamic Semantics №2

continue abandons the current iteration of the innermost enclosing loop and begins its next iteration.

Tested by (2)
Dynamic Semantics №3

return expr transfers control out of the enclosing function with expr as its result; bare return returns ().