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
Tested by
1// Stage 7: match arms with block bodies (RFC-0018).
2// Arms can use either `=> expr` or `=> { stmts* expr? }`.
3
4enum Shape {
5 Circle { radius: f64 },
6 Rectangle { width: f64, height: f64 },
7}
8
9let s: Shape := Shape::Circle { radius = 3.0 };
10
11// Block arm with a local binding and computation.
12let area: f64 := match (s) {
13 Shape::Circle { radius } => {
14 let r := radius;
15 r * r
16 },
17 Shape::Rectangle { width, height } => width * height,
18};
19
20// Block arm producing unit (no tail expression).
21let msg: Perhaps<i64> := Perhaps::Some { value = 1 };
22match (msg) {
23 Perhaps::Some { value } => {
24 let v := value;
25 },
26 None => {},
27};
28
29// Mixed: one arm is a bare expr, one is a block.
30let ok: Result<i64, String> := Result::Ok { value = 5 };
31let n: i64 := match (ok) {
32 Result::Ok { value } => value,
33 Result::Err { error } => {
34 let fallback := 0;
35 fallback
36 },
37};
passes
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
Tested by
1// Stage 7: match arms with block bodies (RFC-0018).
2// Arms can use either `=> expr` or `=> { stmts* expr? }`.
3
4enum Shape {
5 Circle { radius: f64 },
6 Rectangle { width: f64, height: f64 },
7}
8
9let s: Shape := Shape::Circle { radius = 3.0 };
10
11// Block arm with a local binding and computation.
12let area: f64 := match (s) {
13 Shape::Circle { radius } => {
14 let r := radius;
15 r * r
16 },
17 Shape::Rectangle { width, height } => width * height,
18};
19
20// Block arm producing unit (no tail expression).
21let msg: Perhaps<i64> := Perhaps::Some { value = 1 };
22match (msg) {
23 Perhaps::Some { value } => {
24 let v := value;
25 },
26 None => {},
27};
28
29// Mixed: one arm is a bare expr, one is a block.
30let ok: Result<i64, String> := Result::Ok { value = 5 };
31let n: i64 := match (ok) {
32 Result::Ok { value } => value,
33 Result::Err { error } => {
34 let fallback := 0;
35 fallback
36 },
37};
passes
Legality Rule №2
Bindings introduced by an arm's pattern are in scope throughout that arm's block body.
Referenced by: rfc-0018
Tested by
1// Stage 7: match arms with block bodies (RFC-0018).
2// Arms can use either `=> expr` or `=> { stmts* expr? }`.
3
4enum Shape {
5 Circle { radius: f64 },
6 Rectangle { width: f64, height: f64 },
7}
8
9let s: Shape := Shape::Circle { radius = 3.0 };
10
11// Block arm with a local binding and computation.
12let area: f64 := match (s) {
13 Shape::Circle { radius } => {
14 let r := radius;
15 r * r
16 },
17 Shape::Rectangle { width, height } => width * height,
18};
19
20// Block arm producing unit (no tail expression).
21let msg: Perhaps<i64> := Perhaps::Some { value = 1 };
22match (msg) {
23 Perhaps::Some { value } => {
24 let v := value;
25 },
26 None => {},
27};
28
29// Mixed: one arm is a bare expr, one is a block.
30let ok: Result<i64, String> := Result::Ok { value = 5 };
31let n: i64 := match (ok) {
32 Result::Ok { value } => value,
33 Result::Err { error } => {
34 let fallback := 0;
35 fallback
36 },
37};
passes
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)
1// RFC-0156: the parenthesized `match` scrutinee forms — a plain parenthesized
2// expression, a tuple (its own parens satisfy the requirement), and the unit
3// literal.
4fun classify(n: i64) -> i64 {
5 match (n) {
6 0 => 0,
7 _ => 1,
8 }
9}
10
11fun main() -> i64 {
12 let pair: (i64, i64) := (1, 2);
13 let a := match (pair) {
14 (1, 2) => 10,
15 _ => 0,
16 };
17 let b := match (1, 2) {
18 (1, 2) => 20,
19 _ => 0,
20 };
21 let c := match () {
22 _ => 30,
23 };
24 return classify(0) + a + b + c;
25}
passes
1// RFC-0156: a `match` scrutinee must be parenthesized, like `if`/`while`/`for`.
2// The bare `match x { ... }` form is a hard parse error — no alias.
3fun main() -> i64 {
4 match 1 {
5 1 => 10,
6 _ => 0,
7 }
8}
parse errorP0001
Pattern Kinds
| Pattern | Example | Matches |
|---|---|---|
| Wildcard | _ | anything, binds nothing |
| Binding | n | anything, binds to n |
| Literal | 0, "hi", true | exact value |
| Enum variant | Direction::North, North | unit variant (qualified or, since v0.11.0, bare) |
| Enum with fields | Shape::Circle { radius }, Circle { radius } | variant, binds fields |
| Struct | Point { x, y }, Token { kind, .. } | struct, binds named fields |
| Tuple | (a, b) | tuple, binds elements |
| Guard | n if n < 0 | binding + 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
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)
1enum Colour { Red, Green, Blue }
2
3struct Holder {
4 colour: Colour,
5 maybe: Perhaps<i64>,
6 nothing: Perhaps<i64>,
7 ok: Result<i64, String>,
8 err: Result<i64, String>,
9}
10
11fun paint(c: Colour) -> i64 {
12 match (c) {
13 Red => 1,
14 Green => 2,
15 Blue => 3,
16 }
17}
18
19fun favourite() -> Colour {
20 Green
21}
22
23fun shadow(Red: i64) -> i64 {
24 return Red;
25}
26
27fun unwrap_result(r: Result<i64, String>) -> i64 {
28 match (r) {
29 Ok { value } => value,
30 Err { error } => -1,
31 }
32}
33
34fun main() {
35 let c: Colour := Red;
36 let c2: Colour := Red {};
37 assert(paint(c) == 1);
38 assert(paint(c2) == 1);
39 assert(paint(Blue) == 3);
40 assert(paint(favourite()) == 2);
41
42 let holder := Holder {
43 colour = Blue,
44 maybe = Some { value = 5 },
45 nothing = None,
46 ok = Ok { value = 9 },
47 err = Err { error = "bad" },
48 };
49
50 assert(paint(holder.colour) == 3);
51 assert(shadow(7) == 7);
52
53 match (holder.maybe) {
54 Some { value } => assert(value == 5),
55 None => assert(false),
56 }
57
58 match (holder.nothing) {
59 Some { value } => assert(false),
60 None => assert(true),
61 }
62
63 assert(unwrap_result(holder.ok) == 9);
64 assert(unwrap_result(holder.err) == -1);
65}
passes
1// metel-core#285's check must not fire on a deferral that *does* resolve, at any of the
2// positions RFC-0111 supports, and must leave genuinely polymorphic deferrals alone.
3enum Colour { Red, Green, Blue }
4
5fun paint(c: Colour) -> i64 { return 1; }
6fun favourite() -> Colour { Green }
7
8// A closure with a declared return type gives its body an expected type, so a bare
9// variant inside it resolves normally.
10fun annotated_closure() -> Colour {
11 let f := || -> Colour { Red };
12 return f();
13}
14
15fun main() {
16 let c: Colour := Red;
17 assert(paint(c) == 1);
18 assert(paint(Blue) == 1);
19
20 let g: Colour := favourite();
21 assert(paint(g) == 1);
22
23 let p: Perhaps<i64> := Some { value = 5 };
24 let q: Perhaps<i64> := None;
25 assert(paint(annotated_closure()) == 1);
26
27 // An empty array literal is deferred too, and is *genuinely* polymorphic -- the
28 // #285 check is scoped to bare variants precisely so this keeps working.
29 let mk := || { [] };
30 let ints: i64[] := mk();
31 let strs: String[] := mk();
32 assert(ints.len() == 0);
33 assert(strs.len() == 0);
34}
passes
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
Tested by
1enum Colour { Red, Green, Blue }
2
3struct Holder {
4 colour: Colour,
5 maybe: Perhaps<i64>,
6 nothing: Perhaps<i64>,
7 ok: Result<i64, String>,
8 err: Result<i64, String>,
9}
10
11fun paint(c: Colour) -> i64 {
12 match (c) {
13 Red => 1,
14 Green => 2,
15 Blue => 3,
16 }
17}
18
19fun favourite() -> Colour {
20 Green
21}
22
23fun shadow(Red: i64) -> i64 {
24 return Red;
25}
26
27fun unwrap_result(r: Result<i64, String>) -> i64 {
28 match (r) {
29 Ok { value } => value,
30 Err { error } => -1,
31 }
32}
33
34fun main() {
35 let c: Colour := Red;
36 let c2: Colour := Red {};
37 assert(paint(c) == 1);
38 assert(paint(c2) == 1);
39 assert(paint(Blue) == 3);
40 assert(paint(favourite()) == 2);
41
42 let holder := Holder {
43 colour = Blue,
44 maybe = Some { value = 5 },
45 nothing = None,
46 ok = Ok { value = 9 },
47 err = Err { error = "bad" },
48 };
49
50 assert(paint(holder.colour) == 3);
51 assert(shadow(7) == 7);
52
53 match (holder.maybe) {
54 Some { value } => assert(value == 5),
55 None => assert(false),
56 }
57
58 match (holder.nothing) {
59 Some { value } => assert(false),
60 None => assert(true),
61 }
62
63 assert(unwrap_result(holder.ok) == 9);
64 assert(unwrap_result(holder.err) == -1);
65}
passes
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)
1enum Colour { Red, Green, Blue }
2
3struct Holder {
4 colour: Colour,
5 maybe: Perhaps<i64>,
6 nothing: Perhaps<i64>,
7 ok: Result<i64, String>,
8 err: Result<i64, String>,
9}
10
11fun paint(c: Colour) -> i64 {
12 match (c) {
13 Red => 1,
14 Green => 2,
15 Blue => 3,
16 }
17}
18
19fun favourite() -> Colour {
20 Green
21}
22
23fun shadow(Red: i64) -> i64 {
24 return Red;
25}
26
27fun unwrap_result(r: Result<i64, String>) -> i64 {
28 match (r) {
29 Ok { value } => value,
30 Err { error } => -1,
31 }
32}
33
34fun main() {
35 let c: Colour := Red;
36 let c2: Colour := Red {};
37 assert(paint(c) == 1);
38 assert(paint(c2) == 1);
39 assert(paint(Blue) == 3);
40 assert(paint(favourite()) == 2);
41
42 let holder := Holder {
43 colour = Blue,
44 maybe = Some { value = 5 },
45 nothing = None,
46 ok = Ok { value = 9 },
47 err = Err { error = "bad" },
48 };
49
50 assert(paint(holder.colour) == 3);
51 assert(shadow(7) == 7);
52
53 match (holder.maybe) {
54 Some { value } => assert(value == 5),
55 None => assert(false),
56 }
57
58 match (holder.nothing) {
59 Some { value } => assert(false),
60 None => assert(true),
61 }
62
63 assert(unwrap_result(holder.ok) == 9);
64 assert(unwrap_result(holder.err) == -1);
65}
passes
1// metel-core#285's check must not fire on a deferral that *does* resolve, at any of the
2// positions RFC-0111 supports, and must leave genuinely polymorphic deferrals alone.
3enum Colour { Red, Green, Blue }
4
5fun paint(c: Colour) -> i64 { return 1; }
6fun favourite() -> Colour { Green }
7
8// A closure with a declared return type gives its body an expected type, so a bare
9// variant inside it resolves normally.
10fun annotated_closure() -> Colour {
11 let f := || -> Colour { Red };
12 return f();
13}
14
15fun main() {
16 let c: Colour := Red;
17 assert(paint(c) == 1);
18 assert(paint(Blue) == 1);
19
20 let g: Colour := favourite();
21 assert(paint(g) == 1);
22
23 let p: Perhaps<i64> := Some { value = 5 };
24 let q: Perhaps<i64> := None;
25 assert(paint(annotated_closure()) == 1);
26
27 // An empty array literal is deferred too, and is *genuinely* polymorphic -- the
28 // #285 check is scoped to bare variants precisely so this keeps working.
29 let mk := || { [] };
30 let ints: i64[] := mk();
31 let strs: String[] := mk();
32 assert(ints.len() == 0);
33 assert(strs.len() == 0);
34}
passes
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
Tested by
1// metel-core#285: a bare variant that never resolves must be reported, not silently
2// accepted. Pass 1 defers it (RFC-0111 §3.1) and only pass 2 resolves it against an
3// expected type -- but an uncalled closure's body is never constructed, so no expected
4// type ever arrives and nothing used to notice. Checked after the final solve instead.
5enum Colour { Red, Green }
6
7fun main() {
8 let f := || { Red }; // ERROR[T0002]
9}
typecheck errorT0002at 8
Unqualified variant patterns
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
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)
1struct Point { x: i64, y: i64 }
2
3fun main() -> i64 {
4 let p := Point { x = 3, y = 4 };
5 match (p) {
6 Point { x, y } => x + y,
7 }
8}
passes
1struct Token { kind: i64, span: i64, offset: i64 }
2
3fun main() {
4 let t := Token { kind = 1, span = 2, offset = 3 };
5 match (t) {
6 Token { kind, span } => println(kind + span),
7 }
8}
typecheck errorT0001at 6
Matching through a reference
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)
1// RFC-0108: a `&T`/`&var T` scrutinee matches against the referent's own patterns —
2// reference layers are peeled before pattern resolution, the same way field access and
3// method dispatch already auto-dereference. Also covers composition with RFC-0107:
4// peeling happens first, so a bare variant resolves against the referent's enum.
5
6enum Colour { Red, Green, Blue }
7
8struct Point { x: i64, y: i64 }
9
10fun name_qualified(c: &Colour) -> String {
11 match (c) {
12 Colour::Red => "red",
13 Colour::Green => "green",
14 Colour::Blue => "blue",
15 }
16}
17
18fun name_bare(c: &Colour) -> String {
19 match (c) {
20 Red => "red",
21 Green => "green",
22 Blue => "blue",
23 }
24}
25
26fun name_mut(c: &var Colour) -> String {
27 match (c) {
28 Red => "red",
29 Green => "green",
30 Blue => "blue",
31 }
32}
33
34// Bindings introduced through a reference-matched pattern copy the referent.
35fun payload(v: &Perhaps<i64>) -> i64 {
36 match (v) {
37 Some { value } => value,
38 None => -1,
39 }
40}
41
42fun main() {
43 let c := Colour::Green;
44 assert(name_qualified(&c) == "green");
45 assert(name_bare(&c) == "green");
46
47 var d := Colour::Blue;
48 assert(name_mut(&var d) == "blue");
49
50 let some: Perhaps<i64> := Perhaps::Some { value = 7 };
51 let none: Perhaps<i64> := Perhaps::None;
52 assert(payload(&some) == 7);
53 assert(payload(&none) == -1);
54
55 // A non-reference scrutinee is unaffected by the peel.
56 let red := Colour::Red;
57 assert(name_qualified(&red) == "red");
58
59 // Matching a reference to a struct still binds by value through the peel.
60 let p := Point { x = 1, y = 2 };
61 let r: &Point := &p;
62 assert(r.x == 1);
63}
passes
1// RFC-0107§1, RFC-0107§1.1, RFC-0107§1.2, RFC-0107§1.3, RFC-0107§3, and RFC-0107§4:
2// a bare variant name in a match arm resolves type-directed against the
3// scrutinee's own enum. Covers no-field variants (parsed as a binding, rewritten to
4// an EnumVariant), fieldful variants (the new bare `Variant { fields }` grammar), the
5// still-valid qualified form, and bare `None` — which no longer has a dedicated
6// Pattern::None node and instead goes through this same general mechanism.
7
8enum Colour { Red, Green, Blue }
9
10fun name(c: Colour) -> String {
11 match (c) {
12 Red => "red",
13 Green => "green",
14 Blue => "blue",
15 }
16}
17
18fun unwrap_or(v: Perhaps<i64>, d: i64) -> i64 {
19 match (v) {
20 Some { value } => value,
21 None => d,
22 }
23}
24
25fun qualified_still_works(v: Perhaps<i64>) -> i64 {
26 match (v) {
27 Perhaps::Some { value } => value,
28 Perhaps::None => -1,
29 }
30}
31
32// A bare identifier that names no variant of the scrutinee's enum stays an ordinary
33// binding, exactly as before.
34fun binding_fallback(c: Colour) -> String {
35 match (c) {
36 Red => "red",
37 other => name(other),
38 }
39}
40
41fun main() {
42 assert(name(Colour::Red) == "red");
43 assert(name(Colour::Green) == "green");
44 assert(name(Colour::Blue) == "blue");
45
46 assert(unwrap_or(Perhaps::Some { value = 5 }, 0) == 5);
47 assert(unwrap_or(Perhaps::None, 9) == 9);
48
49 assert(qualified_still_works(Perhaps::Some { value = 3 }) == 3);
50 assert(qualified_still_works(Perhaps::None) == -1);
51
52 assert(binding_fallback(Colour::Red) == "red");
53 assert(binding_fallback(Colour::Blue) == "blue");
54}
passes
Legality Rule №2
A fieldful enum variant may likewise omit its enum prefix in a match pattern.
Referenced by: rfc-0107
Tested by
1// RFC-0107§1, RFC-0107§1.1, RFC-0107§1.2, RFC-0107§1.3, RFC-0107§3, and RFC-0107§4:
2// a bare variant name in a match arm resolves type-directed against the
3// scrutinee's own enum. Covers no-field variants (parsed as a binding, rewritten to
4// an EnumVariant), fieldful variants (the new bare `Variant { fields }` grammar), the
5// still-valid qualified form, and bare `None` — which no longer has a dedicated
6// Pattern::None node and instead goes through this same general mechanism.
7
8enum Colour { Red, Green, Blue }
9
10fun name(c: Colour) -> String {
11 match (c) {
12 Red => "red",
13 Green => "green",
14 Blue => "blue",
15 }
16}
17
18fun unwrap_or(v: Perhaps<i64>, d: i64) -> i64 {
19 match (v) {
20 Some { value } => value,
21 None => d,
22 }
23}
24
25fun qualified_still_works(v: Perhaps<i64>) -> i64 {
26 match (v) {
27 Perhaps::Some { value } => value,
28 Perhaps::None => -1,
29 }
30}
31
32// A bare identifier that names no variant of the scrutinee's enum stays an ordinary
33// binding, exactly as before.
34fun binding_fallback(c: Colour) -> String {
35 match (c) {
36 Red => "red",
37 other => name(other),
38 }
39}
40
41fun main() {
42 assert(name(Colour::Red) == "red");
43 assert(name(Colour::Green) == "green");
44 assert(name(Colour::Blue) == "blue");
45
46 assert(unwrap_or(Perhaps::Some { value = 5 }, 0) == 5);
47 assert(unwrap_or(Perhaps::None, 9) == 9);
48
49 assert(qualified_still_works(Perhaps::Some { value = 3 }) == 3);
50 assert(qualified_still_works(Perhaps::None) == -1);
51
52 assert(binding_fallback(Colour::Red) == "red");
53 assert(binding_fallback(Colour::Blue) == "blue");
54}
passes
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
Tested by
1// RFC-0107§1, RFC-0107§1.1, RFC-0107§1.2, RFC-0107§1.3, RFC-0107§3, and RFC-0107§4:
2// a bare variant name in a match arm resolves type-directed against the
3// scrutinee's own enum. Covers no-field variants (parsed as a binding, rewritten to
4// an EnumVariant), fieldful variants (the new bare `Variant { fields }` grammar), the
5// still-valid qualified form, and bare `None` — which no longer has a dedicated
6// Pattern::None node and instead goes through this same general mechanism.
7
8enum Colour { Red, Green, Blue }
9
10fun name(c: Colour) -> String {
11 match (c) {
12 Red => "red",
13 Green => "green",
14 Blue => "blue",
15 }
16}
17
18fun unwrap_or(v: Perhaps<i64>, d: i64) -> i64 {
19 match (v) {
20 Some { value } => value,
21 None => d,
22 }
23}
24
25fun qualified_still_works(v: Perhaps<i64>) -> i64 {
26 match (v) {
27 Perhaps::Some { value } => value,
28 Perhaps::None => -1,
29 }
30}
31
32// A bare identifier that names no variant of the scrutinee's enum stays an ordinary
33// binding, exactly as before.
34fun binding_fallback(c: Colour) -> String {
35 match (c) {
36 Red => "red",
37 other => name(other),
38 }
39}
40
41fun main() {
42 assert(name(Colour::Red) == "red");
43 assert(name(Colour::Green) == "green");
44 assert(name(Colour::Blue) == "blue");
45
46 assert(unwrap_or(Perhaps::Some { value = 5 }, 0) == 5);
47 assert(unwrap_or(Perhaps::None, 9) == 9);
48
49 assert(qualified_still_works(Perhaps::Some { value = 3 }) == 3);
50 assert(qualified_still_works(Perhaps::None) == -1);
51
52 assert(binding_fallback(Colour::Red) == "red");
53 assert(binding_fallback(Colour::Blue) == "blue");
54}
passes
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
Tested by
1// RFC-0107 §2: a bare variant tag is rewritten before exhaustiveness checking;
2// it is not a catch-all binding.
3enum Colour { Red, Blue }
4
5fun name(c: Colour) -> String {
6 match (c) {
7 Red => "red",
8 }
9}
10
11fun main() {}
typecheck errorT0008at 6:5“non-exhaustive match”
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
Tested by
1// RFC-0107§1, RFC-0107§1.1, RFC-0107§1.2, RFC-0107§1.3, RFC-0107§3, and RFC-0107§4:
2// a bare variant name in a match arm resolves type-directed against the
3// scrutinee's own enum. Covers no-field variants (parsed as a binding, rewritten to
4// an EnumVariant), fieldful variants (the new bare `Variant { fields }` grammar), the
5// still-valid qualified form, and bare `None` — which no longer has a dedicated
6// Pattern::None node and instead goes through this same general mechanism.
7
8enum Colour { Red, Green, Blue }
9
10fun name(c: Colour) -> String {
11 match (c) {
12 Red => "red",
13 Green => "green",
14 Blue => "blue",
15 }
16}
17
18fun unwrap_or(v: Perhaps<i64>, d: i64) -> i64 {
19 match (v) {
20 Some { value } => value,
21 None => d,
22 }
23}
24
25fun qualified_still_works(v: Perhaps<i64>) -> i64 {
26 match (v) {
27 Perhaps::Some { value } => value,
28 Perhaps::None => -1,
29 }
30}
31
32// A bare identifier that names no variant of the scrutinee's enum stays an ordinary
33// binding, exactly as before.
34fun binding_fallback(c: Colour) -> String {
35 match (c) {
36 Red => "red",
37 other => name(other),
38 }
39}
40
41fun main() {
42 assert(name(Colour::Red) == "red");
43 assert(name(Colour::Green) == "green");
44 assert(name(Colour::Blue) == "blue");
45
46 assert(unwrap_or(Perhaps::Some { value = 5 }, 0) == 5);
47 assert(unwrap_or(Perhaps::None, 9) == 9);
48
49 assert(qualified_still_works(Perhaps::Some { value = 3 }) == 3);
50 assert(qualified_still_works(Perhaps::None) == -1);
51
52 assert(binding_fallback(Colour::Red) == "red");
53 assert(binding_fallback(Colour::Blue) == "blue");
54}
passes
Legality Rule №6
The fully qualified enum-variant pattern remains valid wherever its bare spelling is valid.
Tested by
1// RFC-0107§1, RFC-0107§1.1, RFC-0107§1.2, RFC-0107§1.3, RFC-0107§3, and RFC-0107§4:
2// a bare variant name in a match arm resolves type-directed against the
3// scrutinee's own enum. Covers no-field variants (parsed as a binding, rewritten to
4// an EnumVariant), fieldful variants (the new bare `Variant { fields }` grammar), the
5// still-valid qualified form, and bare `None` — which no longer has a dedicated
6// Pattern::None node and instead goes through this same general mechanism.
7
8enum Colour { Red, Green, Blue }
9
10fun name(c: Colour) -> String {
11 match (c) {
12 Red => "red",
13 Green => "green",
14 Blue => "blue",
15 }
16}
17
18fun unwrap_or(v: Perhaps<i64>, d: i64) -> i64 {
19 match (v) {
20 Some { value } => value,
21 None => d,
22 }
23}
24
25fun qualified_still_works(v: Perhaps<i64>) -> i64 {
26 match (v) {
27 Perhaps::Some { value } => value,
28 Perhaps::None => -1,
29 }
30}
31
32// A bare identifier that names no variant of the scrutinee's enum stays an ordinary
33// binding, exactly as before.
34fun binding_fallback(c: Colour) -> String {
35 match (c) {
36 Red => "red",
37 other => name(other),
38 }
39}
40
41fun main() {
42 assert(name(Colour::Red) == "red");
43 assert(name(Colour::Green) == "green");
44 assert(name(Colour::Blue) == "blue");
45
46 assert(unwrap_or(Perhaps::Some { value = 5 }, 0) == 5);
47 assert(unwrap_or(Perhaps::None, 9) == 9);
48
49 assert(qualified_still_works(Perhaps::Some { value = 3 }) == 3);
50 assert(qualified_still_works(Perhaps::None) == -1);
51
52 assert(binding_fallback(Colour::Red) == "red");
53 assert(binding_fallback(Colour::Blue) == "blue");
54}
passes
Legality Rule №7
None in pattern position is resolved by the ordinary unqualified-variant rule for a
Perhaps<T> scrutinee.
Referenced by: rfc-0107
Tested by
1// RFC-0107§1, RFC-0107§1.1, RFC-0107§1.2, RFC-0107§1.3, RFC-0107§3, and RFC-0107§4:
2// a bare variant name in a match arm resolves type-directed against the
3// scrutinee's own enum. Covers no-field variants (parsed as a binding, rewritten to
4// an EnumVariant), fieldful variants (the new bare `Variant { fields }` grammar), the
5// still-valid qualified form, and bare `None` — which no longer has a dedicated
6// Pattern::None node and instead goes through this same general mechanism.
7
8enum Colour { Red, Green, Blue }
9
10fun name(c: Colour) -> String {
11 match (c) {
12 Red => "red",
13 Green => "green",
14 Blue => "blue",
15 }
16}
17
18fun unwrap_or(v: Perhaps<i64>, d: i64) -> i64 {
19 match (v) {
20 Some { value } => value,
21 None => d,
22 }
23}
24
25fun qualified_still_works(v: Perhaps<i64>) -> i64 {
26 match (v) {
27 Perhaps::Some { value } => value,
28 Perhaps::None => -1,
29 }
30}
31
32// A bare identifier that names no variant of the scrutinee's enum stays an ordinary
33// binding, exactly as before.
34fun binding_fallback(c: Colour) -> String {
35 match (c) {
36 Red => "red",
37 other => name(other),
38 }
39}
40
41fun main() {
42 assert(name(Colour::Red) == "red");
43 assert(name(Colour::Green) == "green");
44 assert(name(Colour::Blue) == "blue");
45
46 assert(unwrap_or(Perhaps::Some { value = 5 }, 0) == 5);
47 assert(unwrap_or(Perhaps::None, 9) == 9);
48
49 assert(qualified_still_works(Perhaps::Some { value = 3 }) == 3);
50 assert(qualified_still_works(Perhaps::None) == -1);
51
52 assert(binding_fallback(Colour::Red) == "red");
53 assert(binding_fallback(Colour::Blue) == "blue");
54}
passes
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
Tested by
1// RFC-0108: a `&T`/`&var T` scrutinee matches against the referent's own patterns —
2// reference layers are peeled before pattern resolution, the same way field access and
3// method dispatch already auto-dereference. Also covers composition with RFC-0107:
4// peeling happens first, so a bare variant resolves against the referent's enum.
5
6enum Colour { Red, Green, Blue }
7
8struct Point { x: i64, y: i64 }
9
10fun name_qualified(c: &Colour) -> String {
11 match (c) {
12 Colour::Red => "red",
13 Colour::Green => "green",
14 Colour::Blue => "blue",
15 }
16}
17
18fun name_bare(c: &Colour) -> String {
19 match (c) {
20 Red => "red",
21 Green => "green",
22 Blue => "blue",
23 }
24}
25
26fun name_mut(c: &var Colour) -> String {
27 match (c) {
28 Red => "red",
29 Green => "green",
30 Blue => "blue",
31 }
32}
33
34// Bindings introduced through a reference-matched pattern copy the referent.
35fun payload(v: &Perhaps<i64>) -> i64 {
36 match (v) {
37 Some { value } => value,
38 None => -1,
39 }
40}
41
42fun main() {
43 let c := Colour::Green;
44 assert(name_qualified(&c) == "green");
45 assert(name_bare(&c) == "green");
46
47 var d := Colour::Blue;
48 assert(name_mut(&var d) == "blue");
49
50 let some: Perhaps<i64> := Perhaps::Some { value = 7 };
51 let none: Perhaps<i64> := Perhaps::None;
52 assert(payload(&some) == 7);
53 assert(payload(&none) == -1);
54
55 // A non-reference scrutinee is unaffected by the peel.
56 let red := Colour::Red;
57 assert(name_qualified(&red) == "red");
58
59 // Matching a reference to a struct still binds by value through the peel.
60 let p := Point { x = 1, y = 2 };
61 let r: &Point := &p;
62 assert(r.x == 1);
63}
passes
Legality Rule №2
Type checking a match uses the reference-peeled scrutinee type when checking its patterns.
Referenced by: rfc-0108
Tested by
1// RFC-0108: a `&T`/`&var T` scrutinee matches against the referent's own patterns —
2// reference layers are peeled before pattern resolution, the same way field access and
3// method dispatch already auto-dereference. Also covers composition with RFC-0107:
4// peeling happens first, so a bare variant resolves against the referent's enum.
5
6enum Colour { Red, Green, Blue }
7
8struct Point { x: i64, y: i64 }
9
10fun name_qualified(c: &Colour) -> String {
11 match (c) {
12 Colour::Red => "red",
13 Colour::Green => "green",
14 Colour::Blue => "blue",
15 }
16}
17
18fun name_bare(c: &Colour) -> String {
19 match (c) {
20 Red => "red",
21 Green => "green",
22 Blue => "blue",
23 }
24}
25
26fun name_mut(c: &var Colour) -> String {
27 match (c) {
28 Red => "red",
29 Green => "green",
30 Blue => "blue",
31 }
32}
33
34// Bindings introduced through a reference-matched pattern copy the referent.
35fun payload(v: &Perhaps<i64>) -> i64 {
36 match (v) {
37 Some { value } => value,
38 None => -1,
39 }
40}
41
42fun main() {
43 let c := Colour::Green;
44 assert(name_qualified(&c) == "green");
45 assert(name_bare(&c) == "green");
46
47 var d := Colour::Blue;
48 assert(name_mut(&var d) == "blue");
49
50 let some: Perhaps<i64> := Perhaps::Some { value = 7 };
51 let none: Perhaps<i64> := Perhaps::None;
52 assert(payload(&some) == 7);
53 assert(payload(&none) == -1);
54
55 // A non-reference scrutinee is unaffected by the peel.
56 let red := Colour::Red;
57 assert(name_qualified(&red) == "red");
58
59 // Matching a reference to a struct still binds by value through the peel.
60 let p := Point { x = 1, y = 2 };
61 let r: &Point := &p;
62 assert(r.x == 1);
63}
passes
Legality Rule №3
Exhaustiveness checking a match uses the reference-peeled scrutinee type.
Referenced by: rfc-0108
Tested by
1// RFC-0108: a `&T`/`&var T` scrutinee matches against the referent's own patterns —
2// reference layers are peeled before pattern resolution, the same way field access and
3// method dispatch already auto-dereference. Also covers composition with RFC-0107:
4// peeling happens first, so a bare variant resolves against the referent's enum.
5
6enum Colour { Red, Green, Blue }
7
8struct Point { x: i64, y: i64 }
9
10fun name_qualified(c: &Colour) -> String {
11 match (c) {
12 Colour::Red => "red",
13 Colour::Green => "green",
14 Colour::Blue => "blue",
15 }
16}
17
18fun name_bare(c: &Colour) -> String {
19 match (c) {
20 Red => "red",
21 Green => "green",
22 Blue => "blue",
23 }
24}
25
26fun name_mut(c: &var Colour) -> String {
27 match (c) {
28 Red => "red",
29 Green => "green",
30 Blue => "blue",
31 }
32}
33
34// Bindings introduced through a reference-matched pattern copy the referent.
35fun payload(v: &Perhaps<i64>) -> i64 {
36 match (v) {
37 Some { value } => value,
38 None => -1,
39 }
40}
41
42fun main() {
43 let c := Colour::Green;
44 assert(name_qualified(&c) == "green");
45 assert(name_bare(&c) == "green");
46
47 var d := Colour::Blue;
48 assert(name_mut(&var d) == "blue");
49
50 let some: Perhaps<i64> := Perhaps::Some { value = 7 };
51 let none: Perhaps<i64> := Perhaps::None;
52 assert(payload(&some) == 7);
53 assert(payload(&none) == -1);
54
55 // A non-reference scrutinee is unaffected by the peel.
56 let red := Colour::Red;
57 assert(name_qualified(&red) == "red");
58
59 // Matching a reference to a struct still binds by value through the peel.
60 let p := Point { x = 1, y = 2 };
61 let r: &Point := &p;
62 assert(r.x == 1);
63}
passes
Dynamic Semantics №1
At runtime, matching through a reference compares the patterns with the fully dereferenced scrutinee value.
Referenced by: rfc-0108
Tested by
1// RFC-0108: a `&T`/`&var T` scrutinee matches against the referent's own patterns —
2// reference layers are peeled before pattern resolution, the same way field access and
3// method dispatch already auto-dereference. Also covers composition with RFC-0107:
4// peeling happens first, so a bare variant resolves against the referent's enum.
5
6enum Colour { Red, Green, Blue }
7
8struct Point { x: i64, y: i64 }
9
10fun name_qualified(c: &Colour) -> String {
11 match (c) {
12 Colour::Red => "red",
13 Colour::Green => "green",
14 Colour::Blue => "blue",
15 }
16}
17
18fun name_bare(c: &Colour) -> String {
19 match (c) {
20 Red => "red",
21 Green => "green",
22 Blue => "blue",
23 }
24}
25
26fun name_mut(c: &var Colour) -> String {
27 match (c) {
28 Red => "red",
29 Green => "green",
30 Blue => "blue",
31 }
32}
33
34// Bindings introduced through a reference-matched pattern copy the referent.
35fun payload(v: &Perhaps<i64>) -> i64 {
36 match (v) {
37 Some { value } => value,
38 None => -1,
39 }
40}
41
42fun main() {
43 let c := Colour::Green;
44 assert(name_qualified(&c) == "green");
45 assert(name_bare(&c) == "green");
46
47 var d := Colour::Blue;
48 assert(name_mut(&var d) == "blue");
49
50 let some: Perhaps<i64> := Perhaps::Some { value = 7 };
51 let none: Perhaps<i64> := Perhaps::None;
52 assert(payload(&some) == 7);
53 assert(payload(&none) == -1);
54
55 // A non-reference scrutinee is unaffected by the peel.
56 let red := Colour::Red;
57 assert(name_qualified(&red) == "red");
58
59 // Matching a reference to a struct still binds by value through the peel.
60 let p := Point { x = 1, y = 2 };
61 let r: &Point := &p;
62 assert(r.x == 1);
63}
passes
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
Tested by
1// RFC-0108: a `&T`/`&var T` scrutinee matches against the referent's own patterns —
2// reference layers are peeled before pattern resolution, the same way field access and
3// method dispatch already auto-dereference. Also covers composition with RFC-0107:
4// peeling happens first, so a bare variant resolves against the referent's enum.
5
6enum Colour { Red, Green, Blue }
7
8struct Point { x: i64, y: i64 }
9
10fun name_qualified(c: &Colour) -> String {
11 match (c) {
12 Colour::Red => "red",
13 Colour::Green => "green",
14 Colour::Blue => "blue",
15 }
16}
17
18fun name_bare(c: &Colour) -> String {
19 match (c) {
20 Red => "red",
21 Green => "green",
22 Blue => "blue",
23 }
24}
25
26fun name_mut(c: &var Colour) -> String {
27 match (c) {
28 Red => "red",
29 Green => "green",
30 Blue => "blue",
31 }
32}
33
34// Bindings introduced through a reference-matched pattern copy the referent.
35fun payload(v: &Perhaps<i64>) -> i64 {
36 match (v) {
37 Some { value } => value,
38 None => -1,
39 }
40}
41
42fun main() {
43 let c := Colour::Green;
44 assert(name_qualified(&c) == "green");
45 assert(name_bare(&c) == "green");
46
47 var d := Colour::Blue;
48 assert(name_mut(&var d) == "blue");
49
50 let some: Perhaps<i64> := Perhaps::Some { value = 7 };
51 let none: Perhaps<i64> := Perhaps::None;
52 assert(payload(&some) == 7);
53 assert(payload(&none) == -1);
54
55 // A non-reference scrutinee is unaffected by the peel.
56 let red := Colour::Red;
57 assert(name_qualified(&red) == "red");
58
59 // Matching a reference to a struct still binds by value through the peel.
60 let p := Point { x = 1, y = 2 };
61 let r: &Point := &p;
62 assert(r.x == 1);
63}
passes
Dynamic Semantics №3
For a reference scrutinee, match reference and match *reference compare patterns
against the same referent value.
Referenced by: rfc-0110
Tested by
1// RFC-0110: explicit `*` for reads and for writing through, auto-deref at selectors
2// only, and bare assignment to a reference-typed binding rebinding rather than writing
3// through — which is what makes repointing expressible.
4
5struct Point { x: i64, y: i64 }
6
7fun add(x: i64, y: i64) -> i64 { return x + y; }
8
9// `*` is the spelling in the positions auto-deref deliberately does not cover:
10// call arguments and binary operands.
11fun explicit_reads() {
12 let a := 3;
13 let b := 4;
14 let p: &i64 := &a;
15 let q: &i64 := &b;
16
17 assert(*p == 3);
18 assert(add(*p, *q) == 7);
19 assert(*p + *q == 7);
20 assert(*p * *q == 12); // unary `*` and binary `*` in one expression
21}
22
23// Bare assignment rebinds; `*p = v` writes through.
24fun repoint_and_write_through() {
25 var a := 1;
26 var b := 2;
27 var p: &var i64 := &var a;
28
29 *p := 5;
30 assert(a == 5);
31
32 p := &var b; // repoint — impossible before RFC-0110
33 *p := 9;
34 assert(a == 5); // a is untouched by the write through the repointed p
35 assert(b == 9);
36
37 *p += 1;
38 assert(b == 10);
39}
40
41// Selectors stay implicit: no `*` needed for field, index, or method access.
42fun selectors_stay_implicit() {
43 var q := Point { x = 5, y = 7 };
44 let qp: &var Point := &var q;
45 qp.y := 99;
46 assert(qp.x == 5);
47 assert(q.y == 99);
48
49 var xs := [1, 2, 3];
50 let xp: &var [i64; 3] := &var xs;
51 xp[0] := 9;
52 xp[1] += 10;
53 assert(xs[0] == 9);
54 assert(xs[1] == 12);
55}
56
57// `*(obj.field) = v` and `obj.field = v` are synonyms.
58fun redundant_star_on_a_field_path() {
59 var q := Point { x = 1, y = 2 };
60 let qp: &var Point := &var q;
61 qp.x := 3;
62 assert(q.x == 3);
63}
64
65enum Choice { Left, Right }
66
67fun explicit_and_transparent_match_are_equivalent() {
68 let choice := Choice::Right;
69 let reference: &Choice := &choice;
70 let transparent := match (reference) { Choice::Left => 1, Choice::Right => 2 };
71 let explicit := match (*reference) { Choice::Left => 1, Choice::Right => 2 };
72 assert(transparent == explicit);
73}
74
75fun main() {
76 explicit_reads();
77 repoint_and_write_through();
78 selectors_stay_implicit();
79 redundant_star_on_a_field_path();
80 explicit_and_transparent_match_are_equivalent();
81}
passes
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
Tested by
1// RFC-0108: a `&T`/`&var T` scrutinee matches against the referent's own patterns —
2// reference layers are peeled before pattern resolution, the same way field access and
3// method dispatch already auto-dereference. Also covers composition with RFC-0107:
4// peeling happens first, so a bare variant resolves against the referent's enum.
5
6enum Colour { Red, Green, Blue }
7
8struct Point { x: i64, y: i64 }
9
10fun name_qualified(c: &Colour) -> String {
11 match (c) {
12 Colour::Red => "red",
13 Colour::Green => "green",
14 Colour::Blue => "blue",
15 }
16}
17
18fun name_bare(c: &Colour) -> String {
19 match (c) {
20 Red => "red",
21 Green => "green",
22 Blue => "blue",
23 }
24}
25
26fun name_mut(c: &var Colour) -> String {
27 match (c) {
28 Red => "red",
29 Green => "green",
30 Blue => "blue",
31 }
32}
33
34// Bindings introduced through a reference-matched pattern copy the referent.
35fun payload(v: &Perhaps<i64>) -> i64 {
36 match (v) {
37 Some { value } => value,
38 None => -1,
39 }
40}
41
42fun main() {
43 let c := Colour::Green;
44 assert(name_qualified(&c) == "green");
45 assert(name_bare(&c) == "green");
46
47 var d := Colour::Blue;
48 assert(name_mut(&var d) == "blue");
49
50 let some: Perhaps<i64> := Perhaps::Some { value = 7 };
51 let none: Perhaps<i64> := Perhaps::None;
52 assert(payload(&some) == 7);
53 assert(payload(&none) == -1);
54
55 // A non-reference scrutinee is unaffected by the peel.
56 let red := Colour::Red;
57 assert(name_qualified(&red) == "red");
58
59 // Matching a reference to a struct still binds by value through the peel.
60 let p := Point { x = 1, y = 2 };
61 let r: &Point := &p;
62 assert(r.x == 1);
63}
passes
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
Tested by
1// RFC-0108 §3: reference-transparency is scoped to match scrutinees only. A `&Colour`
2// value that matches fine as a scrutinee does not silently widen to `Colour` in a
3// call-argument position -- that is an ordinary type mismatch, not a peel.
4
5enum Colour { Red, Green, Blue }
6
7fun takes_by_value(c: Colour) -> String {
8 match (c) {
9 Colour::Red => "red",
10 Colour::Green => "green",
11 Colour::Blue => "blue",
12 }
13}
14
15fun main() {
16 let c := Colour::Green;
17 let r: &Colour := &c;
18 println(takes_by_value(r));
19}
typecheck errorT0001at 18
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:
- Arm style must be consistent. Both the
thenandelsearms must use the same style — either both braced or both braceless. Mixing is a parse error. - 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; }} - No semicolon between braceless arms. Write
if (c) a else b;, notif (c) a; else b;— the;terminates the statement before theelse.
Formal rules
Legality Rule №1
An if branch may be a single braceless expression.
Referenced by: rfc-0022
Tested by
1// Braceless if body syntax (RFC-0022).
2
3fun main() {
4 // Braceless if in statement position (no else).
5 var x := 0;
6 if (true) x := 1;
7 assert(x == 1);
8
9 // Braceless if — condition false, body not executed.
10 if (false) x := 99;
11 assert(x == 1);
12
13 // Braceless if–else in expression position.
14 let a := if (true) 10 else 20;
15 assert(a == 10);
16
17 let b := if (false) 10 else 20;
18 assert(b == 20);
19
20 // Nested braceless: inner if has no else — ok.
21 var flag := false;
22 if (true) if (true) flag := true;
23 assert(flag);
24
25 // Braceless if–else used as a function argument.
26 assert((if (true) 7 else 8) == 7);
27
28 // Braceless if in a loop.
29 var sum := 0;
30 var i := 0;
31 while (i < 5) {
32 if (i % 2 == 0) sum := sum + i;
33 i := i + 1;
34 }
35 assert(sum == 6); // 0 + 2 + 4
36
37 // Braceless else-if chain.
38 let v := 2;
39 let label := if (v == 1) 100 else if (v == 2) 200 else 300;
40 assert(label == 200);
41}
passes
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
Tested by
1// RFC-0022 §2 (corrected metel-core#750): a braceless `if` with no `else` has
2// type Unit, and is not restricted to statement position -- it's usable
3// anywhere a Unit-typed expression is, including a `let` binding's
4// initializer.
5fun main() {
6 var calls := 0;
7 let calls_ref: &var i64 := &var calls;
8 let value := if (true) *calls_ref += 1;
9 assert(calls == 1);
10}
passes
Legality Rule №3
A braceless if-else is an expression when its two branches have the same type.
Referenced by: rfc-0022
Tested by
1// Braceless if body syntax (RFC-0022).
2
3fun main() {
4 // Braceless if in statement position (no else).
5 var x := 0;
6 if (true) x := 1;
7 assert(x == 1);
8
9 // Braceless if — condition false, body not executed.
10 if (false) x := 99;
11 assert(x == 1);
12
13 // Braceless if–else in expression position.
14 let a := if (true) 10 else 20;
15 assert(a == 10);
16
17 let b := if (false) 10 else 20;
18 assert(b == 20);
19
20 // Nested braceless: inner if has no else — ok.
21 var flag := false;
22 if (true) if (true) flag := true;
23 assert(flag);
24
25 // Braceless if–else used as a function argument.
26 assert((if (true) 7 else 8) == 7);
27
28 // Braceless if in a loop.
29 var sum := 0;
30 var i := 0;
31 while (i < 5) {
32 if (i % 2 == 0) sum := sum + i;
33 i := i + 1;
34 }
35 assert(sum == 6); // 0 + 2 + 4
36
37 // Braceless else-if chain.
38 let v := 2;
39 let label := if (v == 1) 100 else if (v == 2) 200 else 300;
40 assert(label == 200);
41}
passes
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
Tested by
1// PARSE_ERROR[braceless if body may not contain an if–else expression]
2// Outer body is braceless and contains an inner if–else — dangling-else ambiguity.
3fun main() {
4 if (true) if (false) 1 else 2;
5}
parse error“braceless if body may not contain an if–else expression”
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
Tested by
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
Tested by
1fun main() {
2 // Basic counting.
3 var sum := 0;
4 for (var i := 0; i < 5; i += 1) { sum += i; }
5 assert(sum == 10);
6 // Break exits the loop.
7 var count := 0;
8 for (var i := 0; i < 100; i += 1) {
9 if (i == 5) { break; }
10 count += 1;
11 }
12 assert(count == 5);
13 // Continue still executes the step expression.
14 var c2 := 0;
15 for (var i := 0; i < 5; i += 1) {
16 if (i == 2) { continue; }
17 c2 += 1;
18 }
19 assert(c2 == 4);
20}
passes
For-In
Iterable<T> implementationsv0.4.0for-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
Tested by
1fun main() {
2 // Array iteration.
3 let arr := [10, 20, 30];
4 var sum := 0;
5 for (x in arr) { sum += x; }
6 assert(sum == 60);
7 // Exclusive range.
8 var sum2 := 0;
9 for (i in 0..5) { sum2 += i; }
10 assert(sum2 == 10);
11 // Inclusive range.
12 var sum3 := 0;
13 for (i in 1..=4) { sum3 += i; }
14 assert(sum3 == 10);
15 // Mutable for-in binding may be rebound locally.
16 var bumped := 0;
17 for (var x in [1, 2, 3]) {
18 x += 1;
19 bumped += x;
20 }
21 assert(bumped == 9);
22 // Break stops early.
23 var sum4 := 0;
24 for (x in [1, 2, 3, 4, 5]) {
25 if (x == 3) { break; }
26 sum4 += x;
27 }
28 assert(sum4 == 3);
29 // Continue skips the current element.
30 var sum5 := 0;
31 for (x in [1, 2, 3, 4, 5]) {
32 if (x == 3) { continue; }
33 sum5 += x;
34 }
35 assert(sum5 == 12);
36 // Binding does not leak into the outer scope.
37 let x := 42;
38 var inner := 0;
39 for (x in [1, 2, 3]) { inner += x; }
40 assert(inner == 6);
41 assert(x == 42);
42}
passes
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
Tested by
1fun main() {
2 // Array iteration.
3 let arr := [10, 20, 30];
4 var sum := 0;
5 for (x in arr) { sum += x; }
6 assert(sum == 60);
7 // Exclusive range.
8 var sum2 := 0;
9 for (i in 0..5) { sum2 += i; }
10 assert(sum2 == 10);
11 // Inclusive range.
12 var sum3 := 0;
13 for (i in 1..=4) { sum3 += i; }
14 assert(sum3 == 10);
15 // Mutable for-in binding may be rebound locally.
16 var bumped := 0;
17 for (var x in [1, 2, 3]) {
18 x += 1;
19 bumped += x;
20 }
21 assert(bumped == 9);
22 // Break stops early.
23 var sum4 := 0;
24 for (x in [1, 2, 3, 4, 5]) {
25 if (x == 3) { break; }
26 sum4 += x;
27 }
28 assert(sum4 == 3);
29 // Continue skips the current element.
30 var sum5 := 0;
31 for (x in [1, 2, 3, 4, 5]) {
32 if (x == 3) { continue; }
33 sum5 += x;
34 }
35 assert(sum5 == 12);
36 // Binding does not leak into the outer scope.
37 let x := 42;
38 var inner := 0;
39 for (x in [1, 2, 3]) { inner += x; }
40 assert(inner == 6);
41 assert(x == 42);
42}
passes
References
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:
&exprcreates a shared reference&Twhereexpris an addressable lvalue&var xcreates an exclusive reference&var Twherexis avaraddressable lvalue*pdereferences 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 = vis 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).
& and &var may borrow a temporary expressionNeither &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
Tested by
1// RFC-0045: Mutable address-of lvalue paths — fat pointer (MutFieldPointer) tests.
2// RFC-0067a: *T/*mut T renamed to &T/&var T; explicit *p deref replaced by
3// write-through assignment and type-directed read-copy.
4
5struct Point { x: i64, y: i64 }
6struct Rect { top_left: Point, bottom_right: Point }
7
8fun main() {
9 // &var struct.field — write through fat pointer.
10 var p := Point { x = 1, y = 2 };
11 let px: &var i64 := &var p.x;
12 *px := 10;
13 assert(p.x == 10);
14 assert(p.y == 2);
15
16 // &var struct.field — compound assign through fat pointer.
17 *px += 5;
18 assert(p.x == 15);
19
20 // Read through fat pointer — type-directed copy yields current field value.
21 let read_x: i64 := px;
22 assert(read_x == 15);
23
24 // &var tuple element.
25 var t := (100, 200);
26 let t1: &var i64 := &var t.1;
27 *t1 := 999;
28 assert(t.1 == 999);
29 assert(t.0 == 100);
30
31 // &var array element.
32 var arr: [i64; 3] := [1, 2, 3];
33 let a1: &var i64 := &var arr[1];
34 *a1 := 42;
35 assert(arr[0] == 1);
36 assert(arr[1] == 42);
37 assert(arr[2] == 3);
38
39 // Compound assign through array element fat pointer.
40 *a1 += 8;
41 assert(arr[1] == 50);
42
43 // Chained path: &var outer.inner.field (nested struct).
44 var r := Rect {
45 top_left = Point { x = 0, y = 0 },
46 bottom_right = Point { x = 10, y = 10 },
47 };
48 let brx: &var i64 := &var r.bottom_right.x;
49 *brx := 20;
50 assert(r.bottom_right.x == 20);
51 assert(r.top_left.x == 0);
52
53 // Auto-deref field access through MutFieldReference — FieldAccess auto-deref.
54 var q := Point { x = 5, y = 7 };
55 let qptr: &var Point := &var q;
56 assert(qptr.x == 5);
57 qptr.y := 99;
58 assert(q.y == 99);
59}
passes
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
Tested by
1// RFC-0045: Mutable address-of lvalue paths — fat pointer (MutFieldPointer) tests.
2// RFC-0067a: *T/*mut T renamed to &T/&var T; explicit *p deref replaced by
3// write-through assignment and type-directed read-copy.
4
5struct Point { x: i64, y: i64 }
6struct Rect { top_left: Point, bottom_right: Point }
7
8fun main() {
9 // &var struct.field — write through fat pointer.
10 var p := Point { x = 1, y = 2 };
11 let px: &var i64 := &var p.x;
12 *px := 10;
13 assert(p.x == 10);
14 assert(p.y == 2);
15
16 // &var struct.field — compound assign through fat pointer.
17 *px += 5;
18 assert(p.x == 15);
19
20 // Read through fat pointer — type-directed copy yields current field value.
21 let read_x: i64 := px;
22 assert(read_x == 15);
23
24 // &var tuple element.
25 var t := (100, 200);
26 let t1: &var i64 := &var t.1;
27 *t1 := 999;
28 assert(t.1 == 999);
29 assert(t.0 == 100);
30
31 // &var array element.
32 var arr: [i64; 3] := [1, 2, 3];
33 let a1: &var i64 := &var arr[1];
34 *a1 := 42;
35 assert(arr[0] == 1);
36 assert(arr[1] == 42);
37 assert(arr[2] == 3);
38
39 // Compound assign through array element fat pointer.
40 *a1 += 8;
41 assert(arr[1] == 50);
42
43 // Chained path: &var outer.inner.field (nested struct).
44 var r := Rect {
45 top_left = Point { x = 0, y = 0 },
46 bottom_right = Point { x = 10, y = 10 },
47 };
48 let brx: &var i64 := &var r.bottom_right.x;
49 *brx := 20;
50 assert(r.bottom_right.x == 20);
51 assert(r.top_left.x == 0);
52
53 // Auto-deref field access through MutFieldReference — FieldAccess auto-deref.
54 var q := Point { x = 5, y = 7 };
55 let qptr: &var Point := &var q;
56 assert(qptr.x == 5);
57 qptr.y := 99;
58 assert(q.y == 99);
59}
passes
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
Tested by
1// RFC-0045: Mutable address-of lvalue paths — fat pointer (MutFieldPointer) tests.
2// RFC-0067a: *T/*mut T renamed to &T/&var T; explicit *p deref replaced by
3// write-through assignment and type-directed read-copy.
4
5struct Point { x: i64, y: i64 }
6struct Rect { top_left: Point, bottom_right: Point }
7
8fun main() {
9 // &var struct.field — write through fat pointer.
10 var p := Point { x = 1, y = 2 };
11 let px: &var i64 := &var p.x;
12 *px := 10;
13 assert(p.x == 10);
14 assert(p.y == 2);
15
16 // &var struct.field — compound assign through fat pointer.
17 *px += 5;
18 assert(p.x == 15);
19
20 // Read through fat pointer — type-directed copy yields current field value.
21 let read_x: i64 := px;
22 assert(read_x == 15);
23
24 // &var tuple element.
25 var t := (100, 200);
26 let t1: &var i64 := &var t.1;
27 *t1 := 999;
28 assert(t.1 == 999);
29 assert(t.0 == 100);
30
31 // &var array element.
32 var arr: [i64; 3] := [1, 2, 3];
33 let a1: &var i64 := &var arr[1];
34 *a1 := 42;
35 assert(arr[0] == 1);
36 assert(arr[1] == 42);
37 assert(arr[2] == 3);
38
39 // Compound assign through array element fat pointer.
40 *a1 += 8;
41 assert(arr[1] == 50);
42
43 // Chained path: &var outer.inner.field (nested struct).
44 var r := Rect {
45 top_left = Point { x = 0, y = 0 },
46 bottom_right = Point { x = 10, y = 10 },
47 };
48 let brx: &var i64 := &var r.bottom_right.x;
49 *brx := 20;
50 assert(r.bottom_right.x == 20);
51 assert(r.top_left.x == 0);
52
53 // Auto-deref field access through MutFieldReference — FieldAccess auto-deref.
54 var q := Point { x = 5, y = 7 };
55 let qptr: &var Point := &var q;
56 assert(qptr.x == 5);
57 qptr.y := 99;
58 assert(q.y == 99);
59}
passes
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
Tested by
1// RFC-0045: Mutable address-of lvalue paths — fat pointer (MutFieldPointer) tests.
2// RFC-0067a: *T/*mut T renamed to &T/&var T; explicit *p deref replaced by
3// write-through assignment and type-directed read-copy.
4
5struct Point { x: i64, y: i64 }
6struct Rect { top_left: Point, bottom_right: Point }
7
8fun main() {
9 // &var struct.field — write through fat pointer.
10 var p := Point { x = 1, y = 2 };
11 let px: &var i64 := &var p.x;
12 *px := 10;
13 assert(p.x == 10);
14 assert(p.y == 2);
15
16 // &var struct.field — compound assign through fat pointer.
17 *px += 5;
18 assert(p.x == 15);
19
20 // Read through fat pointer — type-directed copy yields current field value.
21 let read_x: i64 := px;
22 assert(read_x == 15);
23
24 // &var tuple element.
25 var t := (100, 200);
26 let t1: &var i64 := &var t.1;
27 *t1 := 999;
28 assert(t.1 == 999);
29 assert(t.0 == 100);
30
31 // &var array element.
32 var arr: [i64; 3] := [1, 2, 3];
33 let a1: &var i64 := &var arr[1];
34 *a1 := 42;
35 assert(arr[0] == 1);
36 assert(arr[1] == 42);
37 assert(arr[2] == 3);
38
39 // Compound assign through array element fat pointer.
40 *a1 += 8;
41 assert(arr[1] == 50);
42
43 // Chained path: &var outer.inner.field (nested struct).
44 var r := Rect {
45 top_left = Point { x = 0, y = 0 },
46 bottom_right = Point { x = 10, y = 10 },
47 };
48 let brx: &var i64 := &var r.bottom_right.x;
49 *brx := 20;
50 assert(r.bottom_right.x == 20);
51 assert(r.top_left.x == 0);
52
53 // Auto-deref field access through MutFieldReference — FieldAccess auto-deref.
54 var q := Point { x = 5, y = 7 };
55 let qptr: &var Point := &var q;
56 assert(qptr.x == 5);
57 qptr.y := 99;
58 assert(q.y == 99);
59}
passes
Dereference
*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)
1// RFC-0067a §1/§3: write-through for a thin reference (Value::MutReference, not a
2// fat MutFieldReference — every existing fixture's write-through case is a
3// field/tuple/array-element reference; this covers a plain scalar binding directly).
4fun main() {
5 var n := 1;
6 let p: &var i64 := &var n;
7 *p := 4;
8 assert(n == 4);
9
10 *p += 6;
11 assert(n == 10);
12}
passes
1// RFC-0045: Mutable address-of lvalue paths — fat pointer (MutFieldPointer) tests.
2// RFC-0067a: *T/*mut T renamed to &T/&var T; explicit *p deref replaced by
3// write-through assignment and type-directed read-copy.
4
5struct Point { x: i64, y: i64 }
6struct Rect { top_left: Point, bottom_right: Point }
7
8fun main() {
9 // &var struct.field — write through fat pointer.
10 var p := Point { x = 1, y = 2 };
11 let px: &var i64 := &var p.x;
12 *px := 10;
13 assert(p.x == 10);
14 assert(p.y == 2);
15
16 // &var struct.field — compound assign through fat pointer.
17 *px += 5;
18 assert(p.x == 15);
19
20 // Read through fat pointer — type-directed copy yields current field value.
21 let read_x: i64 := px;
22 assert(read_x == 15);
23
24 // &var tuple element.
25 var t := (100, 200);
26 let t1: &var i64 := &var t.1;
27 *t1 := 999;
28 assert(t.1 == 999);
29 assert(t.0 == 100);
30
31 // &var array element.
32 var arr: [i64; 3] := [1, 2, 3];
33 let a1: &var i64 := &var arr[1];
34 *a1 := 42;
35 assert(arr[0] == 1);
36 assert(arr[1] == 42);
37 assert(arr[2] == 3);
38
39 // Compound assign through array element fat pointer.
40 *a1 += 8;
41 assert(arr[1] == 50);
42
43 // Chained path: &var outer.inner.field (nested struct).
44 var r := Rect {
45 top_left = Point { x = 0, y = 0 },
46 bottom_right = Point { x = 10, y = 10 },
47 };
48 let brx: &var i64 := &var r.bottom_right.x;
49 *brx := 20;
50 assert(r.bottom_right.x == 20);
51 assert(r.top_left.x == 0);
52
53 // Auto-deref field access through MutFieldReference — FieldAccess auto-deref.
54 var q := Point { x = 5, y = 7 };
55 let qptr: &var Point := &var q;
56 assert(qptr.x == 5);
57 qptr.y := 99;
58 assert(q.y == 99);
59}
passes
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)
1// RFC-0067a §3: auto-deref for field access/method dispatch already chains through
2// arbitrary depth ("&&T derefs through both levels") independent of read-copy —
3// confirms this pre-existing guarantee explicitly under the new &T/&var T syntax.
4struct Counter { value: i64 }
5
6extend Counter {
7 fun get(&self) -> i64 {
8 self.value
9 }
10 fun bump(&var self) {
11 self.value += 1;
12 }
13}
14
15fun main() {
16 var c := Counter { value = 1 };
17 let r: &var Counter := &var c;
18 let rr: &&var Counter := &r;
19
20 assert(rr.value == 1);
21 assert(rr.get() == 1);
22
23 rr.bump();
24 assert(c.value == 2);
25}
passes
1// RFC-0045: Mutable address-of lvalue paths — fat pointer (MutFieldPointer) tests.
2// RFC-0067a: *T/*mut T renamed to &T/&var T; explicit *p deref replaced by
3// write-through assignment and type-directed read-copy.
4
5struct Point { x: i64, y: i64 }
6struct Rect { top_left: Point, bottom_right: Point }
7
8fun main() {
9 // &var struct.field — write through fat pointer.
10 var p := Point { x = 1, y = 2 };
11 let px: &var i64 := &var p.x;
12 *px := 10;
13 assert(p.x == 10);
14 assert(p.y == 2);
15
16 // &var struct.field — compound assign through fat pointer.
17 *px += 5;
18 assert(p.x == 15);
19
20 // Read through fat pointer — type-directed copy yields current field value.
21 let read_x: i64 := px;
22 assert(read_x == 15);
23
24 // &var tuple element.
25 var t := (100, 200);
26 let t1: &var i64 := &var t.1;
27 *t1 := 999;
28 assert(t.1 == 999);
29 assert(t.0 == 100);
30
31 // &var array element.
32 var arr: [i64; 3] := [1, 2, 3];
33 let a1: &var i64 := &var arr[1];
34 *a1 := 42;
35 assert(arr[0] == 1);
36 assert(arr[1] == 42);
37 assert(arr[2] == 3);
38
39 // Compound assign through array element fat pointer.
40 *a1 += 8;
41 assert(arr[1] == 50);
42
43 // Chained path: &var outer.inner.field (nested struct).
44 var r := Rect {
45 top_left = Point { x = 0, y = 0 },
46 bottom_right = Point { x = 10, y = 10 },
47 };
48 let brx: &var i64 := &var r.bottom_right.x;
49 *brx := 20;
50 assert(r.bottom_right.x == 20);
51 assert(r.top_left.x == 0);
52
53 // Auto-deref field access through MutFieldReference — FieldAccess auto-deref.
54 var q := Point { x = 5, y = 7 };
55 let qptr: &var Point := &var q;
56 assert(qptr.x == 5);
57 qptr.y := 99;
58 assert(q.y == 99);
59}
passes
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
Tested by
1// TYPECHECK_ERROR[T0002]
2// RFC-0110 §3: applying `*` to a non-reference is a type error. Adding parser support
3// for `*` is what makes `UnaryOp::Deref`'s existing type rule reachable from surface
4// syntax for the first time.
5fun main() {
6 let a: i64 := 1;
7 let b := *a;
8}
typecheck error“T0002”
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)
1// RFC-0110 §4.2/§5: write-through is spelled explicitly, one `*` per reference layer.
2// This replaces RFC-0067a's implicit rule, under which a bare `pp = 5` peeled *every*
3// `&var` layer at once — convenient, but it made the number of layers invisible at the
4// write site and left no way to repoint any of them.
5fun main() {
6 var n := 1;
7 var p: &var i64 := &var n;
8 let pp: &var &var i64 := &var p;
9
10 **pp := 5; // two layers, two stars
11 assert(n == 5);
12
13 **pp += 10;
14 assert(n == 15);
15
16 // One star reaches the inner reference itself, not the i64 — which is what makes
17 // repointing through a chain expressible at all.
18 var m := 100;
19 *pp := &var m; // p now refers to m; n keeps its value
20 assert(n == 15);
21 **pp := 7;
22 assert(m == 7);
23}
passes
1// RFC-0110: explicit `*` for reads and for writing through, auto-deref at selectors
2// only, and bare assignment to a reference-typed binding rebinding rather than writing
3// through — which is what makes repointing expressible.
4
5struct Point { x: i64, y: i64 }
6
7fun add(x: i64, y: i64) -> i64 { return x + y; }
8
9// `*` is the spelling in the positions auto-deref deliberately does not cover:
10// call arguments and binary operands.
11fun explicit_reads() {
12 let a := 3;
13 let b := 4;
14 let p: &i64 := &a;
15 let q: &i64 := &b;
16
17 assert(*p == 3);
18 assert(add(*p, *q) == 7);
19 assert(*p + *q == 7);
20 assert(*p * *q == 12); // unary `*` and binary `*` in one expression
21}
22
23// Bare assignment rebinds; `*p = v` writes through.
24fun repoint_and_write_through() {
25 var a := 1;
26 var b := 2;
27 var p: &var i64 := &var a;
28
29 *p := 5;
30 assert(a == 5);
31
32 p := &var b; // repoint — impossible before RFC-0110
33 *p := 9;
34 assert(a == 5); // a is untouched by the write through the repointed p
35 assert(b == 9);
36
37 *p += 1;
38 assert(b == 10);
39}
40
41// Selectors stay implicit: no `*` needed for field, index, or method access.
42fun selectors_stay_implicit() {
43 var q := Point { x = 5, y = 7 };
44 let qp: &var Point := &var q;
45 qp.y := 99;
46 assert(qp.x == 5);
47 assert(q.y == 99);
48
49 var xs := [1, 2, 3];
50 let xp: &var [i64; 3] := &var xs;
51 xp[0] := 9;
52 xp[1] += 10;
53 assert(xs[0] == 9);
54 assert(xs[1] == 12);
55}
56
57// `*(obj.field) = v` and `obj.field = v` are synonyms.
58fun redundant_star_on_a_field_path() {
59 var q := Point { x = 1, y = 2 };
60 let qp: &var Point := &var q;
61 qp.x := 3;
62 assert(q.x == 3);
63}
64
65enum Choice { Left, Right }
66
67fun explicit_and_transparent_match_are_equivalent() {
68 let choice := Choice::Right;
69 let reference: &Choice := &choice;
70 let transparent := match (reference) { Choice::Left => 1, Choice::Right => 2 };
71 let explicit := match (*reference) { Choice::Left => 1, Choice::Right => 2 };
72 assert(transparent == explicit);
73}
74
75fun main() {
76 explicit_reads();
77 repoint_and_write_through();
78 selectors_stay_implicit();
79 redundant_star_on_a_field_path();
80 explicit_and_transparent_match_are_equivalent();
81}
passes
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)
1// RFC-0110: explicit `*` for reads and for writing through, auto-deref at selectors
2// only, and bare assignment to a reference-typed binding rebinding rather than writing
3// through — which is what makes repointing expressible.
4
5struct Point { x: i64, y: i64 }
6
7fun add(x: i64, y: i64) -> i64 { return x + y; }
8
9// `*` is the spelling in the positions auto-deref deliberately does not cover:
10// call arguments and binary operands.
11fun explicit_reads() {
12 let a := 3;
13 let b := 4;
14 let p: &i64 := &a;
15 let q: &i64 := &b;
16
17 assert(*p == 3);
18 assert(add(*p, *q) == 7);
19 assert(*p + *q == 7);
20 assert(*p * *q == 12); // unary `*` and binary `*` in one expression
21}
22
23// Bare assignment rebinds; `*p = v` writes through.
24fun repoint_and_write_through() {
25 var a := 1;
26 var b := 2;
27 var p: &var i64 := &var a;
28
29 *p := 5;
30 assert(a == 5);
31
32 p := &var b; // repoint — impossible before RFC-0110
33 *p := 9;
34 assert(a == 5); // a is untouched by the write through the repointed p
35 assert(b == 9);
36
37 *p += 1;
38 assert(b == 10);
39}
40
41// Selectors stay implicit: no `*` needed for field, index, or method access.
42fun selectors_stay_implicit() {
43 var q := Point { x = 5, y = 7 };
44 let qp: &var Point := &var q;
45 qp.y := 99;
46 assert(qp.x == 5);
47 assert(q.y == 99);
48
49 var xs := [1, 2, 3];
50 let xp: &var [i64; 3] := &var xs;
51 xp[0] := 9;
52 xp[1] += 10;
53 assert(xs[0] == 9);
54 assert(xs[1] == 12);
55}
56
57// `*(obj.field) = v` and `obj.field = v` are synonyms.
58fun redundant_star_on_a_field_path() {
59 var q := Point { x = 1, y = 2 };
60 let qp: &var Point := &var q;
61 qp.x := 3;
62 assert(q.x == 3);
63}
64
65enum Choice { Left, Right }
66
67fun explicit_and_transparent_match_are_equivalent() {
68 let choice := Choice::Right;
69 let reference: &Choice := &choice;
70 let transparent := match (reference) { Choice::Left => 1, Choice::Right => 2 };
71 let explicit := match (*reference) { Choice::Left => 1, Choice::Right => 2 };
72 assert(transparent == explicit);
73}
74
75fun main() {
76 explicit_reads();
77 repoint_and_write_through();
78 selectors_stay_implicit();
79 redundant_star_on_a_field_path();
80 explicit_and_transparent_match_are_equivalent();
81}
passes
1// TYPECHECK_ERROR[T0001]
2// RFC-0110: under the Go model, reading through a reference is implicit only at
3// selectors (field, index, method). A call argument is not a selector, so passing a
4// reference where the parameter expects the referent type is a hard mismatch — write
5// `takes_i64(*r)`.
6//
7// Note the reason RFC-0067a §3a originally gave for this — "there is no declared type
8// for the argument itself to compare against" — is factually wrong: `param_hints`
9// already threads the parameter's declared type here for monomorphic callees. The
10// behavior is right; the justification was not. See RFC-0112 §4.1, which re-examined
11// closing this gap and declined it deliberately rather than by accident.
12fun takes_i64(x: i64) {}
13
14fun main() {
15 let n := 5;
16 let r: &i64 := &n;
17 takes_i64(r);
18}
typecheck error“T0001”
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)
1// RFC-0110 §4.2/§5: write-through is spelled explicitly, one `*` per reference layer.
2// This replaces RFC-0067a's implicit rule, under which a bare `pp = 5` peeled *every*
3// `&var` layer at once — convenient, but it made the number of layers invisible at the
4// write site and left no way to repoint any of them.
5fun main() {
6 var n := 1;
7 var p: &var i64 := &var n;
8 let pp: &var &var i64 := &var p;
9
10 **pp := 5; // two layers, two stars
11 assert(n == 5);
12
13 **pp += 10;
14 assert(n == 15);
15
16 // One star reaches the inner reference itself, not the i64 — which is what makes
17 // repointing through a chain expressible at all.
18 var m := 100;
19 *pp := &var m; // p now refers to m; n keeps its value
20 assert(n == 15);
21 **pp := 7;
22 assert(m == 7);
23}
passes
1// RFC-0110: explicit `*` for reads and for writing through, auto-deref at selectors
2// only, and bare assignment to a reference-typed binding rebinding rather than writing
3// through — which is what makes repointing expressible.
4
5struct Point { x: i64, y: i64 }
6
7fun add(x: i64, y: i64) -> i64 { return x + y; }
8
9// `*` is the spelling in the positions auto-deref deliberately does not cover:
10// call arguments and binary operands.
11fun explicit_reads() {
12 let a := 3;
13 let b := 4;
14 let p: &i64 := &a;
15 let q: &i64 := &b;
16
17 assert(*p == 3);
18 assert(add(*p, *q) == 7);
19 assert(*p + *q == 7);
20 assert(*p * *q == 12); // unary `*` and binary `*` in one expression
21}
22
23// Bare assignment rebinds; `*p = v` writes through.
24fun repoint_and_write_through() {
25 var a := 1;
26 var b := 2;
27 var p: &var i64 := &var a;
28
29 *p := 5;
30 assert(a == 5);
31
32 p := &var b; // repoint — impossible before RFC-0110
33 *p := 9;
34 assert(a == 5); // a is untouched by the write through the repointed p
35 assert(b == 9);
36
37 *p += 1;
38 assert(b == 10);
39}
40
41// Selectors stay implicit: no `*` needed for field, index, or method access.
42fun selectors_stay_implicit() {
43 var q := Point { x = 5, y = 7 };
44 let qp: &var Point := &var q;
45 qp.y := 99;
46 assert(qp.x == 5);
47 assert(q.y == 99);
48
49 var xs := [1, 2, 3];
50 let xp: &var [i64; 3] := &var xs;
51 xp[0] := 9;
52 xp[1] += 10;
53 assert(xs[0] == 9);
54 assert(xs[1] == 12);
55}
56
57// `*(obj.field) = v` and `obj.field = v` are synonyms.
58fun redundant_star_on_a_field_path() {
59 var q := Point { x = 1, y = 2 };
60 let qp: &var Point := &var q;
61 qp.x := 3;
62 assert(q.x == 3);
63}
64
65enum Choice { Left, Right }
66
67fun explicit_and_transparent_match_are_equivalent() {
68 let choice := Choice::Right;
69 let reference: &Choice := &choice;
70 let transparent := match (reference) { Choice::Left => 1, Choice::Right => 2 };
71 let explicit := match (*reference) { Choice::Left => 1, Choice::Right => 2 };
72 assert(transparent == explicit);
73}
74
75fun main() {
76 explicit_reads();
77 repoint_and_write_through();
78 selectors_stay_implicit();
79 redundant_star_on_a_field_path();
80 explicit_and_transparent_match_are_equivalent();
81}
passes
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)
1// RFC-0110 §4.2/§5: write-through is spelled explicitly, one `*` per reference layer.
2// This replaces RFC-0067a's implicit rule, under which a bare `pp = 5` peeled *every*
3// `&var` layer at once — convenient, but it made the number of layers invisible at the
4// write site and left no way to repoint any of them.
5fun main() {
6 var n := 1;
7 var p: &var i64 := &var n;
8 let pp: &var &var i64 := &var p;
9
10 **pp := 5; // two layers, two stars
11 assert(n == 5);
12
13 **pp += 10;
14 assert(n == 15);
15
16 // One star reaches the inner reference itself, not the i64 — which is what makes
17 // repointing through a chain expressible at all.
18 var m := 100;
19 *pp := &var m; // p now refers to m; n keeps its value
20 assert(n == 15);
21 **pp := 7;
22 assert(m == 7);
23}
passes
1// RFC-0110: explicit `*` for reads and for writing through, auto-deref at selectors
2// only, and bare assignment to a reference-typed binding rebinding rather than writing
3// through — which is what makes repointing expressible.
4
5struct Point { x: i64, y: i64 }
6
7fun add(x: i64, y: i64) -> i64 { return x + y; }
8
9// `*` is the spelling in the positions auto-deref deliberately does not cover:
10// call arguments and binary operands.
11fun explicit_reads() {
12 let a := 3;
13 let b := 4;
14 let p: &i64 := &a;
15 let q: &i64 := &b;
16
17 assert(*p == 3);
18 assert(add(*p, *q) == 7);
19 assert(*p + *q == 7);
20 assert(*p * *q == 12); // unary `*` and binary `*` in one expression
21}
22
23// Bare assignment rebinds; `*p = v` writes through.
24fun repoint_and_write_through() {
25 var a := 1;
26 var b := 2;
27 var p: &var i64 := &var a;
28
29 *p := 5;
30 assert(a == 5);
31
32 p := &var b; // repoint — impossible before RFC-0110
33 *p := 9;
34 assert(a == 5); // a is untouched by the write through the repointed p
35 assert(b == 9);
36
37 *p += 1;
38 assert(b == 10);
39}
40
41// Selectors stay implicit: no `*` needed for field, index, or method access.
42fun selectors_stay_implicit() {
43 var q := Point { x = 5, y = 7 };
44 let qp: &var Point := &var q;
45 qp.y := 99;
46 assert(qp.x == 5);
47 assert(q.y == 99);
48
49 var xs := [1, 2, 3];
50 let xp: &var [i64; 3] := &var xs;
51 xp[0] := 9;
52 xp[1] += 10;
53 assert(xs[0] == 9);
54 assert(xs[1] == 12);
55}
56
57// `*(obj.field) = v` and `obj.field = v` are synonyms.
58fun redundant_star_on_a_field_path() {
59 var q := Point { x = 1, y = 2 };
60 let qp: &var Point := &var q;
61 qp.x := 3;
62 assert(q.x == 3);
63}
64
65enum Choice { Left, Right }
66
67fun explicit_and_transparent_match_are_equivalent() {
68 let choice := Choice::Right;
69 let reference: &Choice := &choice;
70 let transparent := match (reference) { Choice::Left => 1, Choice::Right => 2 };
71 let explicit := match (*reference) { Choice::Left => 1, Choice::Right => 2 };
72 assert(transparent == explicit);
73}
74
75fun main() {
76 explicit_reads();
77 repoint_and_write_through();
78 selectors_stay_implicit();
79 redundant_star_on_a_field_path();
80 explicit_and_transparent_match_are_equivalent();
81}
passes
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)
1// RFC-0110: explicit `*` for reads and for writing through, auto-deref at selectors
2// only, and bare assignment to a reference-typed binding rebinding rather than writing
3// through — which is what makes repointing expressible.
4
5struct Point { x: i64, y: i64 }
6
7fun add(x: i64, y: i64) -> i64 { return x + y; }
8
9// `*` is the spelling in the positions auto-deref deliberately does not cover:
10// call arguments and binary operands.
11fun explicit_reads() {
12 let a := 3;
13 let b := 4;
14 let p: &i64 := &a;
15 let q: &i64 := &b;
16
17 assert(*p == 3);
18 assert(add(*p, *q) == 7);
19 assert(*p + *q == 7);
20 assert(*p * *q == 12); // unary `*` and binary `*` in one expression
21}
22
23// Bare assignment rebinds; `*p = v` writes through.
24fun repoint_and_write_through() {
25 var a := 1;
26 var b := 2;
27 var p: &var i64 := &var a;
28
29 *p := 5;
30 assert(a == 5);
31
32 p := &var b; // repoint — impossible before RFC-0110
33 *p := 9;
34 assert(a == 5); // a is untouched by the write through the repointed p
35 assert(b == 9);
36
37 *p += 1;
38 assert(b == 10);
39}
40
41// Selectors stay implicit: no `*` needed for field, index, or method access.
42fun selectors_stay_implicit() {
43 var q := Point { x = 5, y = 7 };
44 let qp: &var Point := &var q;
45 qp.y := 99;
46 assert(qp.x == 5);
47 assert(q.y == 99);
48
49 var xs := [1, 2, 3];
50 let xp: &var [i64; 3] := &var xs;
51 xp[0] := 9;
52 xp[1] += 10;
53 assert(xs[0] == 9);
54 assert(xs[1] == 12);
55}
56
57// `*(obj.field) = v` and `obj.field = v` are synonyms.
58fun redundant_star_on_a_field_path() {
59 var q := Point { x = 1, y = 2 };
60 let qp: &var Point := &var q;
61 qp.x := 3;
62 assert(q.x == 3);
63}
64
65enum Choice { Left, Right }
66
67fun explicit_and_transparent_match_are_equivalent() {
68 let choice := Choice::Right;
69 let reference: &Choice := &choice;
70 let transparent := match (reference) { Choice::Left => 1, Choice::Right => 2 };
71 let explicit := match (*reference) { Choice::Left => 1, Choice::Right => 2 };
72 assert(transparent == explicit);
73}
74
75fun main() {
76 explicit_reads();
77 repoint_and_write_through();
78 selectors_stay_implicit();
79 redundant_star_on_a_field_path();
80 explicit_and_transparent_match_are_equivalent();
81}
passes
1struct Holder { t: (i64, (i64, i64)) }
2
3fun main() {
4 var t := (1, 2);
5 t.0 := 5;
6 assert(t.0 == 5);
7 t.0 += 4;
8 assert(t.0 == 9);
9
10 var nested := (10, (20, 30));
11 nested.1.0 := 99;
12 assert(nested.1.0 == 99);
13
14 var s := Holder { t = (7, (8, 9)) };
15 s.t.0 := 11;
16 assert(s.t.0 == 11);
17
18 tuple_assign_through_references();
19}
20
21// Tuple-element assignment must reach through a reference at any step of the path, the
22// way field- and index-path assignment already do. Neither pass peeled for tuple targets
23// when the variant was first added, so `t.0 = v` worked on a plain binding but not on a
24// `&var` receiver -- a seam between metel-core#283 and the RFC-0110 reference work.
25struct RefHolder { pair: (i64, i64) }
26
27fun set_direct(t: &var (i64, i64)) {
28 t.0 := 9;
29 t.1 += 5;
30}
31
32fun set_nested(h: &var RefHolder) {
33 h.pair.0 := 42;
34 h.pair.1 += 3;
35}
36
37fun tuple_assign_through_references() {
38 var t := (1, 2);
39 set_direct(&var t);
40 assert(t.0 == 9);
41 assert(t.1 == 7);
42
43 var rh := RefHolder { pair = (1, 2) };
44 set_nested(&var rh);
45 assert(rh.pair.0 == 42);
46 assert(rh.pair.1 == 5);
47
48 // A shared reference still grants no write access.
49 let r: &i64 := &rh.pair.0;
50 rh.pair.0 := 7;
51 assert(*r == 7); // and it aliases (metel-core#282)
52}
passes
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
Tested by
1// metel-core#280: every form the addressability rule admits keeps working, including
2// RFC-0110 §6's reborrow `&*p`, which previously hit the same internal error despite the
3// RFC specifying it as legal.
4struct Point { x: i64, y: i64 }
5struct Pair { a: Point }
6
7fun main() {
8 let n := 1;
9 let arr := [1, 2, 3];
10 let t := (10, 20);
11 let pair := Pair { a = Point { x = 1, y = 2 } };
12
13 let p1 := &n; // binding
14 let p2 := &pair.a; // field
15 let p3 := &pair.a.x; // chained field
16 let p4 := &t.0; // tuple element
17 let p5 := &arr[1]; // array element
18 assert(*p1 == 1);
19 assert(*p3 == 1);
20 assert(*p4 == 10);
21 assert(*p5 == 2);
22 assert(p2.y == 2);
23
24 // Reborrow: `&*p` shares the referent's storage rather than snapshotting it.
25 var m := 5;
26 let mp: &var i64 := &var m;
27 let rb := &*mp;
28 assert(*rb == 5);
29
30 let rbm := &var *mp;
31 *rbm := 9;
32 assert(m == 9);
33}
passes
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 typeTwhereexpr: T. Allbreakarms must produce the same type; abreakexpression is typechecked against its enclosing loop value typeType inference L2.loop { }— a loop with no reachablebreak— has type!(Never). See §Never type.
Break, Continue, and Return
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)
1fun main() {
2 // Break with a value.
3 let x := loop { break 42; };
4 assert(x == 42);
5 // Continue skips the current iteration.
6 var count := 0;
7 var i := 0;
8 loop {
9 if (i >= 5) { break; }
10 i := i + 1;
11 if (i == 3) { continue; }
12 count := count + 1;
13 }
14 assert(count == 4);
15 // Assignments inside the loop are visible in subsequent iterations.
16 var acc := 0;
17 var j := 1;
18 loop {
19 if (j > 5) { break; }
20 acc += j;
21 j += 1;
22 }
23 assert(acc == 15);
24}
passes
1// Issue #229: a `break` written as a nested `if`-tail or match-arm-tail must
2// still correctly propagate to the enclosing loop's own inferred type --
3// `find_loop_break_type` previously only checked `block.stmts`, never
4// `block.tail` and never recursed into `Match`, both pre-existing gaps only
5// reachable in practice once `break` could be a tail expression at all.
6fun via_if(c: boolean) -> i64 {
7 loop {
8 if (c) { break 7 }
9 }
10}
11
12fun via_match(c: boolean) -> i64 {
13 loop {
14 match (c) {
15 true => break 8,
16 false => break 8,
17 }
18 }
19}
20
21fun main() {
22 assert(via_if(true) == 7);
23 assert(via_match(true) == 8);
24}
passes
1// Stage 6: break nested inside if branches is visible to find_loop_break_type.
2
3// break inside if-then branch only
4let _a: i64 := loop {
5 if (true) { break 42; }
6};
7
8// break in both then and else branches
9let _b: i64 := loop {
10 if (true) { break 42; } else { break 0; }
11};
12
13// inner loop break does not escape to outer loop; outer diverges (Never).
14// Both trailing-semicolon and no-semicolon forms are valid in statement position.
15fun diverge_outer_with_semi() -> i64 {
16 loop {
17 loop { break "inner"; };
18 loop { break "inner"; };
19 }
20}
21
22fun diverge_outer_no_semi() -> i64 {
23 loop {
24 loop { break "inner"; }
25 loop { break "inner"; }
26 }
27}
passes
Dynamic Semantics №2
continue abandons the current iteration of the innermost enclosing loop and begins its
next iteration.
Tested by (2)
1fun main() {
2 // Break with a value.
3 let x := loop { break 42; };
4 assert(x == 42);
5 // Continue skips the current iteration.
6 var count := 0;
7 var i := 0;
8 loop {
9 if (i >= 5) { break; }
10 i := i + 1;
11 if (i == 3) { continue; }
12 count := count + 1;
13 }
14 assert(count == 4);
15 // Assignments inside the loop are visible in subsequent iterations.
16 var acc := 0;
17 var j := 1;
18 loop {
19 if (j > 5) { break; }
20 acc += j;
21 j += 1;
22 }
23 assert(acc == 15);
24}
passes
1// `break` and `continue` remain valid in every loop form.
2
3fun while_controls() {
4 var i := 0;
5 while (i < 3) {
6 i += 1;
7 if (i < 2) {
8 continue;
9 }
10 break;
11 }
12}
13
14fun for_controls() {
15 for (var i := 0; i < 3; i += 1) {
16 if (i < 2) {
17 continue;
18 }
19 break;
20 }
21}
22
23fun for_in_controls() {
24 for (item in [1, 2, 3]) {
25 if (item < 2) {
26 continue;
27 }
28 break;
29 }
30}
passes
Dynamic Semantics №3
return expr transfers control out of the enclosing function with expr as its result;
bare return returns ().
Tested by