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 (2)
1record Message { text: String }
2aspect Render { fun render(&self) -> String; }
3extend Message: Render {
4 fun render(&self) -> String { self.text.clone() }
5}
6
7type Renderable := dyn Render;
8enum Display { Item { renderable: Renderable }, Empty }
9
10fun render(display: Display) -> String {
11 match (display) {
12 Display::Item { renderable } => renderable.render(),
13 Display::Empty => "empty",
14 }
15}
16
17fun main() {
18 let display := Display::Item { renderable = Message { text = "matched" } };
19 assert(render(display) == "matched");
20}
passes
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 (3)
1fun choose<T>(condition: boolean, first: T, second: T) -> T {
2 match (condition) { true => first, false => second }
3}
4
5fun restored<T>(condition: boolean, original: T, replacement: T) -> T {
6 var value := original;
7 let consumed := value;
8 match (condition) { true => { value := replacement; }, _ => { value := replacement; } }
9 value
10}
11
12fun main() {
13 assert(choose(true, 7, 9) == 7);
14 assert(choose(false, 7, 9) == 9);
15 assert(restored(true, 1, 2) == 2);
16 assert(restored(false, 1, 3) == 3);
17}
passes
1enum Flag { On, Off }
2enum Payload<T> { Present { value: T }, Missing }
3struct Envelope<T> { value: T }
4struct Ticket { count: i64 }
5extend Ticket: Copy;
6
7fun copy_witness<T>(unused: T) -> i64 {
8 let witness := Ticket { count = 2 };
9 let first := witness;
10 let again := witness;
11 first.count + again.count
12}
13
14fun extract_saved<T>(value: Payload<T>) -> T {
15 match (value) { Present { value } => value, Missing => { loop {} } }
16}
17
18fun unwrap<T>(value: Envelope<T>) -> T { value.value }
19
20fun flag<T>(tag: Flag, unused: T) -> boolean {
21 match (tag) { On => true, Off => false }
22}
23
24fun choose<T>(tag: i64, first: T, second: T) -> T {
25 match (tag) { 1 => first, _ => second }
26}
27
28fun whole<T>(value: T) -> T {
29 match (value) { bound => bound }
30}
31
32fun make<T>(value: T, mark: &var i64) -> Perhaps<T> {
33 *mark := *mark + 1;
34 Perhaps::Some { value = value }
35}
36
37fun extract<T>(value: T, mark: &var i64) -> T {
38 match (make(value, mark)) { Some { value } => value, None => { loop {} } }
39}
40
41fun main() {
42 assert(flag(Flag::On, "unused"));
43 assert(!flag(Flag::Off, 0));
44 assert(choose(1, "first", "second") == "first");
45 assert(choose(2, "first", "second") == "second");
46 assert(whole("owned") == "owned");
47 var mark := 0;
48 assert(extract("payload", &var mark) == "payload");
49 assert(mark == 1);
50 assert(extract_saved(Payload::Present { value = "saved" }) == "saved");
51 assert(unwrap(Envelope { value = "enclosed" }) == "enclosed");
52 assert(copy_witness("unused") == 4);
53}
passes
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
GAP-EXPRESSIONS-001RFC-0178RFC-0122No form binds the remainder of a record through a reference: a borrowed view of "everything but these fields" has no spelling.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 (4)
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
1struct Token { kind: i64, span: i64, offset: i64 }
2
3fun main() -> i64 {
4 let t := Token { kind = 1, span = 2, offset = 3 };
5 match (t) {
6 Token { kind, span, .. } => kind + span,
7 }
8}
passes
1// Cross-feature (v0.13.0): a `match (h)` (RFC-0156 parenthesized scrutinee) binds
2// a non-`Copy` field out of a struct value through a struct pattern
3// (metel-core#753). That partial move narrows `h` (RFC-0137 slice 2): the moved
4// field is gone, every other field stays readable, and reassigning the moved
5// field widens `h` back to the whole struct.
6
7struct Handle { id: i64, name: String }
8
9fun main() {
10 var h := Handle { id = 7, name = "fd".to_string() };
11
12 // bind and consume `name` via a struct pattern; `h` is now `Handle.{ id }`
13 let taken := match (h) { Handle { name, .. } => name };
14 assert(taken == "fd");
15
16 // a still-present field is readable on the narrowed value
17 assert(h.id == 7);
18
19 // reassigning the moved field widens `h` back to the whole `Handle`
20 h.name := "fd2".to_string();
21 assert(h.name == "fd2");
22 assert(h.id == 7);
23
24 println("ok");
25}
passes
Record rest patterns
An anonymous-record or nominal record pattern may end in ..name. The binding receives
the owned anonymous record made from all fields not named by the pattern. Bare .. remains
discard-only. The named fields must be entailed present; the row facts must also determine
the remainder type. A presence-only bound without a decomposition is insufficient. Removing
all fields from a closed row produces {}. Structs, Drop types, and borrowed scrutinees are
not accepted by this form. Every named field is bound by the pattern and follows the
ordinary ownership rules for pattern bindings; every unmentioned field moves into the
remainder binding. Matching consumes the scrutinee as a whole. A nominal record is
eligible only when all of its fields are public, and the remainder loses the nominal brand.
match (value) { // value : { token: Token, extra: i64, ..Rest }
{ token, ..rest } => rest // rest : { extra: i64, ..Rest }
}
Legality Rule №1
A record rest pattern's named fields must be entailed present, and its row facts must determine the remainder type after removing those labels; otherwise it is rejected.
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
Referenced by: rfc-0178
Tested by (25)
1fun strip<row R, row Rest>(d: { ..R }) -> { ..Rest }
2where R = { token: String, ..Rest } {
3 let { token: _, ..rest } := d;
4 rest
5}
6
7fun strip_extra<row R, row Rest>(d: { ..R }) -> { user: i64, ..Rest }
8where R = { token: String, user: i64, ..Rest } {
9 let var { token: _, ..rest } := d;
10 rest.user := rest.user + 1;
11 rest
12}
13
14fun strip_two<row R, row Mid, row Rest>(d: { ..R }) -> { ..Rest }
15where R = { token: String, ..Mid }, Mid = { user: i64, ..Rest } {
16 let { token: _, user: _, ..rest } := d;
17 rest
18}
19
20fun take<row R>(a: { x: i64, ..R }, b: { y: i64, ..R }) -> { ..R } {
21 let { x: _, ..rest } := a;
22 rest
23}
24
25record Box<row R> { data: { ..R } }
26record Credentials { token: String, name: String }
27extend<row R> Box<..R> {
28 fun strip<row Rest>(self) -> { ..Rest }
29 where R = { token: String, ..Rest } {
30 let { token: _, ..rest } := self.data;
31 rest
32 }
33 fun from_arg<row Q, row Rest>(self, d: { id: i64, ..Q }) -> { ..Rest }
34 where Q = { token: String, ..Rest } {
35 let { id: _, token: _, ..rest } := d;
36 rest
37 }
38 fun add<row Q: !{token}>(self, d: { ..Q }) -> { token: String, ..Q } {
39 { token = "new", ..d }
40 }
41}
42
43fun main() {
44 assert(strip({ token = "t", user = 3 }).user == 3);
45 assert(strip({ token = "t", owned = "kept" }).owned == "kept");
46 assert(strip(Credentials { token = "t", name = "Ada" }).name == "Ada");
47 let empty: {} := strip({ token = "only" });
48 assert(strip_extra({ token = "t", user = 3, active = true }).active);
49 assert(strip_extra({ token = "t", user = 3 }).user == 4);
50 assert(strip_two({ token = "t", user = 3, active = true }).active);
51 assert(take({ x = 1, z = true }, { y = 2, z = false }).z);
52 let b := Box { data = { token = "t", x = true } };
53 assert(b.strip().x);
54 let from_arg_box := Box { data = { token = "t", x = true } };
55 assert(from_arg_box.from_arg({ id = 1, token = "t", y = true }).y);
56 let add_box := Box { data = { token = "t", x = true } };
57 assert(add_box.add({ z = 5 }).z == 5);
58 println("ok");
59}
passes
1fun strip_two<row R, row Mid, row Rest>(d: { ..R }) -> { ..Rest }
2where R = { token: String, ..Mid }, Mid = { user: i64, ..Rest } {
3 let { token: _, user: _, ..rest } := d;
4 rest
5}
6fun main() {
7 strip_two({ token = "t", active = true }); // ERROR[T0012]
8}
typecheck errorT0012at 7
1type Callback := once || -> i64;
2fun invoke(f: Callback) -> i64 { f() }
3
4fun transition<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
5 { ..r, token = "secret" }
6}
7
8fun copied_tail<row R: !{token}>(r: { id: i64, ..R }) -> i64 where all R: Copy {
9 let { id, ..rest } := r;
10 let copy := rest;
11 let callback := [copy, id] once || {
12 let extended := transition(copy);
13 let { token, ..again } := extended;
14 id + token.len() as i64
15 };
16 invoke(callback)
17}
18
19fun main() {
20 assert(copied_tail({ id = 3, count = 7 }) == 9);
21 assert(copied_tail({ id = 5 }) == 11);
22}
passes
1type Pair<T> := { left: T, right: T };
2type Grid := [i64; 3];
3record Cell { value: i64, grid: Grid }
4type Residual<row R> := { ..R };
5fun swap(p: Pair<i64>) -> Pair<i64> { { left = p.right, right = p.left } }
6fun strip(c: Cell) -> Residual<{ grid: Grid }> {
7 let { value: _, ..rest } := c;
8 rest
9}
10fun main() {
11 let s := swap({ left = 1, right = 2 });
12 assert(s.left == 2);
13 let r := strip(Cell { value = 9, grid = [1, 2, 3] });
14 assert(r.grid[1] == 2);
15 extend_ok();
16}
17fun extend_ok() {
18 let g: Grid := [4, 5, 6];
19 var total := 0;
20 for (x in g) { total := total + x; }
21 assert(total == 15);
22}
passes
1record Wrapper<T> { inner: T, tag: String }
2fun inner_of<T: Copy>(w: Wrapper<T>) -> T {
3 let { tag: _, ..rest } := w;
4 rest.inner
5}
6fun sum_all<row R>(v: { a: i64, b: i64, ..R }) -> i64 where all R: Copy { v.a + v.b }
7aspect Source { type Item; fun take(self) -> Item; }
8struct Holder { value: { a: i64, b: i64, c: boolean } }
9extend Holder: Source { type Item := { a: i64, b: i64, c: boolean }; fun take(self) -> { a: i64, b: i64, c: boolean } { self.value } }
10fun use_source<S: Source<Item = { a: i64, b: i64, c: boolean }>>(s: S) -> i64 { sum_all(s.take()) }
11fun main() {
12 assert(inner_of(Wrapper { inner = 7, tag = "t" }) == 7);
13 assert(use_source(Holder { value = { a = 1, b = 2, c = true } }) == 3);
14}
passes
1record Session<row R> { id: i64, data: { ..R } }
2extend<row R: { token: String, .. }> Session<..R> { fun kind(&self) -> i64 { 1 } }
3extend<row R: !{ token }> Session<..R> { fun kind(&self) -> i64 { 2 } }
4fun after_rest() {
5 let s := Session { id = 1, data = { token = "t", n = 5 } };
6 assert(s.kind() == 1);
7 let { id, data } := s;
8 let { token: _, ..rest } := data;
9 let s2 := Session { id = id, data = rest };
10 assert(s2.kind() == 2);
11}
12
13fun empty_and_stripped() {
14 let s := Session { id = 1, data = { token = "t" } };
15 let { id, data } := s;
16 let { token: _, ..rest } := data;
17 let s2 := Session { id = id, data = rest };
18 assert(s2.kind() == 2);
19 let e := Session { id = 2, data = {} };
20 assert(e.kind() == 2);
21}
22
23fun main() {
24 after_rest();
25 empty_and_stripped();
26}
passes
1enum E { A, B, C }
2fun main() { let e := E::A; match (e) { E::A => 1, E::B => 2 }; } // ERROR[T0008]
typecheck errorT0008at 2“non-exhaustive match”
1record P { x: i64, y: i64 }
2fun main() { let p := P { x = 1, y = 2 }; let { x: _, ..r1 } := p; let { y: _, ..r2 } := r1; let { y: _, ..r3 } := r2; } // ERROR[T0012]
typecheck errorT0012at 2“not known to be present”
1record P { x: i64 }
2fun main() {
3 let p := P { x = 1 };
4 let { y, ..rest } := p; // ERROR[T0012]
5}
typecheck errorT0012at 4“not known to be present”
1record Token { value: String }
2fun take(t: Token.{}) -> i64 { 1 }
3fun main() {
4 let t := Token { value = "x".to_string() };
5 let { value: _, ..rest } := t;
6 take(rest); // ERROR[T0001]
7}
typecheck errorT0001at 6“a record rest pattern (`..rest`) drops the nominal brand”
1record Tok { value: String, id: i64, n: i64 }
2fun take(t: Tok.{ id, n }) -> i64 { t.id + t.n }
3fun main() {
4 let t := Tok { value = "x".to_string(), id = 1, n = 2 };
5 let { value: _, ..rest } := t;
6 assert(take(rest) == 3); // ERROR[T0001]
7}
typecheck errorT0001at 6“an anonymous record is not `Tok.{ id, n }`”
1record Token { value: String, id: i64 }
2fun count(xs: [{}]) -> i64 { xs.len() as i64 }
3fun main() {
4 let a := Token { value = "a".to_string(), id = 1 };
5 let { value: _, id: _, ..e1 } := a;
6 let b := Token { value = "b".to_string(), id = 2 };
7 let { value: _, id: _, ..e2 } := b;
8 let es: [{}] := [e1, e2];
9 assert(count(es) == 2);
10 let c := Token { value = "c".to_string(), id = 3 };
11 let { value: _, id: _, ..e3 } := c;
12 match (e3) { whole => { let _w: {} := whole; } }
13}
passes
1// A record rest pattern drops the nominal brand (spec.expressions "Record rest patterns"),
2// so aspect dispatch on the remainder follows its anonymous static type, not the hidden
3// brand of the record it came from (metel-core#1446). A whole value (here `u`, after a
4// `Copy` field read that does not narrow it) still dispatches to its nominal impl.
5aspect Describe { fun describe(&self) -> String; }
6record Point { x: i64, y: i64 }
7extend Point: Describe { fun describe(&self) -> String { "point".to_string() } }
8extend<row R> { ..R }: Describe where all R: Copy { fun describe(&self) -> String { "copy-row".to_string() } }
9
10record Tok { value: String, id: i64, n: i64 }
11aspect Keyed { fun key(&self) -> String; }
12extend Tok: Keyed { fun key(&self) -> String { "tok".to_string() } }
13extend<row R: { id: i64, .. }> { ..R }: Keyed where all R: Copy { fun key(&self) -> String { "row".to_string() } }
14
15fun erase(p: Point) -> {} {
16 let { x: _, y: _, ..rest } := p;
17 rest
18}
19
20fun show(d: &dyn Describe) -> String { d.describe() }
21
22fun main() {
23 let e: {} := erase(Point { x = 1, y = 2 });
24 assert(e.describe() == "copy-row");
25 assert(show(&e) == "copy-row");
26
27 let t := Tok { value = "x".to_string(), id = 1, n = 2 };
28 let { value: _, ..rest } := t;
29 assert(rest.key() == "row");
30
31 let u := Point { x = 1, y = 2 };
32 let moved := u.x;
33 assert(u.describe() == "point");
34 assert(moved == 1);
35}
passes
1enum Shape { Circle { r: i64, tag: String }, Square { s: i64, tag: String } }
2record Item { id: i64, shape: [i64; 2] }
3fun area(s: Shape) -> i64 {
4 match (s) {
5 Shape::Circle { r, tag } => r * 3 + tag.len(),
6 Shape::Square { s, tag } => s * s + tag.len(),
7 }
8}
9fun total<row R>(v: { n: i64, ..R }) -> i64 where all R: Copy {
10 match (v) { { n, ..rest } => n }
11}
12fun main() {
13 assert(area(Shape::Circle { r = 2, tag = "ab" }) == 8);
14 assert(area(Shape::Square { s = 3, tag = "" }) == 9);
15 assert(total({ n = 5, flag = true }) == 5);
16 let it := Item { id = 1, shape = [2, 3] };
17 match (it.shape) { [a, b] => assert(a + b == 5) }
18 let r: Perhaps<{ n: i64 }> := Perhaps::Some { value = { n = 4 } };
19 match (r) { Perhaps::Some { value } => assert(value.n == 4), None => assert(false) }
20}
passes
1record Counter { n: i64, label: String }
2fun main() {
3 let c := Counter { n = 1, label = "c" };
4 let { label, ..rest } := c;
5 let f := [rest] once || { rest.n + 1 };
6 assert(f() == 2);
7 assert(label == "c");
8 let arr: [i64; 3] := [1, 2, 3];
9 let g := [arr] || { arr[0] + arr[2] };
10 assert(g() == 4);
11 assert(g() == 4);
12}
passes
1record Pair { left: String, right: String }
2type PairAlias := Pair;
3type Restore := once var || -> PairAlias;
4
5aspect State { fun state(&self) -> i64; }
6extend<row R: { right: String, .. }> { ..R }: State {
7 fun state(&self) -> i64 { 1 }
8}
9extend<row R: !{ right }> { ..R }: State {
10 fun state(&self) -> i64 { 0 }
11}
12
13fun main() {
14 var pair: PairAlias := Pair { left = "left", right = "right" };
15 assert(pair.state() == 1);
16 let left := pair.left;
17 let partial := pair.clone();
18 assert(partial.right == "right");
19 assert(pair.state() == 1);
20 let right := pair.right;
21 assert(pair.state() == 0);
22 let empty: Pair.{} := pair.clone();
23 assert(empty.state() == 0);
24 let restore: Restore := [pair] once var || -> PairAlias {
25 pair.left := "restored";
26 pair.right := "again";
27 assert(pair.state() == 1);
28 pair
29 };
30 match (restore()) {
31 { left, ..rest } => {
32 assert(left == "restored");
33 assert(rest.right == "again");
34 assert(rest.state() == 1);
35 },
36 }
37}
passes
1record Frame { rows: [i64; 3], label: String }
2record Grid { cells: [i64; 3] }
3fun consume(g: Grid) -> i64 { g.cells[0] }
4fun rest_rows() {
5 let f := Frame { rows = [1, 2, 3], label = "f" };
6 let { label, ..rest } := f;
7 assert(label == "f");
8 var total := 0;
9 for (r in rest.rows) { total := total + r; }
10 assert(total == 6);
11}
12fun empty_rest() {
13 let g := Grid { cells = [7, 8, 9] };
14 let { cells, ..rest } := g;
15 assert(cells[2] == 9);
16 let again := Grid { cells = cells };
17 assert(consume(again) == 7);
18}
19fun main() {
20 rest_rows();
21 empty_rest();
22}
passes
1record Item { id: i64, name: String, tags: [String; 2] }
2record Token { value: String }
3fun clone_rest() {
4 let it := Item { id = 1, name = "n", tags = ["a", "b"] };
5 let { id, ..rest } := it;
6 assert(id == 1);
7 let c := rest.clone();
8 assert("${c}" == "{ name: n, tags: [a, b] }");
9 assert(c.name == rest.name);
10}
11
12fun clone_empty() {
13 let t := Token { value = "x" };
14 let { value: _, ..rest } := t;
15 let c := rest.clone();
16 assert("${c}" == "{}");
17 assert("${rest}" == "{}");
18}
19
20fun main() {
21 clone_rest();
22 clone_empty();
23}
passes
1// RP x DY, RP x CO, ER x PM, ER x CO, AR x OW, AR x GA
2aspect Name { fun name(&self) -> String; }
3record User { id: i64, name: String, tags: [String; 2] }
4extend<row R: { name: String, .. }> { ..R }: Name { fun name(&self) -> String { self.name.clone() } }
5fun first<T: Copy>(xs: [T; 2]) -> T { xs[0] }
6fun main() {
7 let u := User { id = 1, name = "ann", tags = ["a", "b"] };
8 let { id, ..rest } := u;
9 let d: &dyn Name := &rest;
10 assert(d.name() == "ann");
11 assert(id == 1);
12 let users := [User { id = 2, name = "b", tags = ["x", "y"] }, User { id = 3, name = "c", tags = ["p", "q"] }];
13 var ids := 0;
14 for (w in users) { ids := ids + w.id; }
15 assert(ids == 5);
16 let t := User { id = 4, name = "d", tags = ["m", "n"] };
17 let tags := t.tags;
18 let name := t.name;
19 assert(tags[0] == "m");
20 assert(name == "d");
21 assert(first([5, 6]) == 5);
22 let { id: _, name: _, tags: _, ..empty } := User { id = 5, name = "e", tags = ["u", "v"] };
23 let _e: {} := empty;
24}
passes
1type Callback := once || -> i64;
2fun invoke(f: Callback) -> i64 { f() }
3
4fun transition<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
5 { ..r, token = "secret" }
6}
7
8fun copied_tail<row R: !{token}>(r: { id: i64, ..R }) -> i64 where all R: Copy {
9 let { id, ..rest } := r;
10 let copy := rest;
11 let callback := [copy] once || { // ERROR[T0026]
12 let extended := transition(copy);
13 let { token, ..again } := extended;
14 id + token.len() as i64
15 };
16 invoke(callback)
17}
18
19fun main() {
20 copied_tail({ id = 3, count = 7 });
21}
typecheck errorT0026at 11“`id` is captured but not listed”
1fun take_id<row R, row Rest>(r: { id: i64, ..R }) -> i64
2where R = { extra: i64, ..Rest }
3{
4 let var { id, ..rest } := r;
5 id := id + 1;
6 rest.extra := rest.extra + 1;
7 id + rest.extra
8}
9
10fun take_extra(r: { id: i64, extra: i64 }) -> i64 {
11 let { id: _, extra } := r;
12 extra
13}
14
15fun add_auth<row R: !{ auth }>(r: { ..R }, auth: i64) -> { ..R, auth: i64 } {
16 { ..r, auth = auth }
17}
18
19fun copy_row(r: &{ x: i64, y: i64 }) -> { x: i64, y: i64 } {
20 { ..r }
21}
22
23record PublicPair {
24 id: i64,
25 extra: i64,
26}
27
28fun nominal_remainder(pair: PublicPair) -> i64 {
29 match (pair) {
30 { id, ..tail } => id + tail.extra
31 }
32}
33
34fun main() -> i64 {
35 assert(take_id({ id = 7, extra = 5 }) == 14);
36 assert(take_extra({ id = 8, extra = 9 }) == 9);
37 let r := add_auth({ id = 7 }, 5);
38 let total := take_id({ id = 7, extra = 5 });
39 let anonymous := match ({ id = total, extra = r.auth }) {
40 { id, ..rest } => id + rest.extra
41 };
42 let original := { x = 4, y = 6 };
43 let copied := copy_row(&original);
44 anonymous + nominal_remainder(PublicPair { id = 2, extra = 3 }) + copied.x + original.y
45}
passes
1fun bad<row R>(r: { ..R }) -> i64 {
2 let { id: _, ..rest } := r; // ERROR[T0012]
3 0
4}
5
6fun main() -> i64 {
7 bad({ other = 1 })
8}
typecheck errorT0012at 2:9
1fun take<row R>(a: { x: i64, ..R }, b: { y: i64, ..R }) -> { ..R } {
2 let { x: _, ..rest } := a;
3 rest
4}
5fun main() {
6 println(take({ x = 1, z = true }, { y = 2, z = 8 }).z); // ERROR[T0001]
7}
typecheck errorT0001at 6
1fun consume<row R>(value: { ..R }) {}
2fun main() {
3 let source := { token = "gone", owned = "kept" };
4 let { token: _, ..rest } := source;
5 assert(rest.owned == "kept");
6 consume(source); // ERROR[T0019]
7}
typecheck errorT0019at 6
1import shapes::{Frame, Weigh, strip_label, total, empty_after};
2fun main() {
3 let f := Frame { public_rows = [1, 2, 3], label = "x" };
4 let stripped := strip_label(f);
5 assert(stripped.public_rows[2] == 3);
6 assert(total(stripped) == 6);
7 assert(total({ public_rows = [4, 5, 6], extra = true }) == 15);
8 let e := empty_after({ label = "gone".to_string() });
9 assert(e.weigh() == 1);
10 assert(stripped.weigh() == 1);
11}
passes
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 (4)
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
1fun extracted<T>(value: T) -> T {
2 loop { break value; }
3}
4
5fun nested<T>(value: T) -> T {
6 loop {
7 let result := loop { break value; };
8 break result;
9 }
10}
11
12fun returning<T>(value: T, mark: &var i64) {
13 *mark := 1;
14 let result: () := loop { break if (true) { return; } else { return; }; };
15 *mark := 2;
16}
17
18fun main() {
19 assert(extracted(7) == 7);
20 assert(nested(9) == 9);
21 var mark := 0;
22 returning(1, &var mark);
23 assert(mark == 1);
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
Known gaps
GAP-EXPRESSIONS-001 — A record rest pattern cannot borrow the remainder through a reference. No form binds the remainder of a record through a reference: a borrowed view of "everything but these fields" has no spelling. Open record ↗