Type System
Metel is statically and strongly typed. Types are checked at compile time. There are no implicit conversions.
Primitive Types
| Type | Description | Example |
|---|---|---|
i64 | 64-bit signed integer | 42 |
f64 | 64-bit floating point | 3.14 |
boolean | Boolean | true |
String | UTF-8 string | "hello" |
Char | Unicode scalar value | 'a' |
() | Unit — represents no value | () |
The unit type () is only written explicitly when needed as a type parameter (e.g. Result<(), Error>). Functions that return nothing omit the -> annotation entirely.
Sized Numeric Types
Metel provides exact-width numeric types for low-level and systems programming. i64 and f64 are the default integer and floating-point types in ordinary code.
Signed integers:
| Type | Width |
|---|---|
i8 | 8-bit |
i16 | 16-bit |
i32 | 32-bit |
i64 | 64-bit |
Unsigned integers:
| Type | Width |
|---|---|
u8 | 8-bit |
u16 | 16-bit |
u32 | 32-bit |
u64 | 64-bit |
Floats:
| Type | Width |
|---|---|
f32 | 32-bit IEEE 754 |
f64 | 64-bit IEEE 754 |
Sized literals use a suffix: 42i32, 3.14f32, 255u8. All casts between sized numeric types are explicit (as). Array indices must be u64; indexing with an i64 requires an explicit as u64 cast.
Unsuffixed literals are polymorphic. When the expected type is known from context (annotation, function parameter, struct field, return type, or the other operand in arithmetic/comparison), an unsuffixed numeric literal adopts that type automaticallyL3. When no context is available, the literal defaults to i64 (integer) or f64 (float).
let a: i32 := 10; // 10 is i32
let b: u8 := 255; // 255 is u8
let c: f32 := 1.5; // 1.5 is f32
fun scale(x: f32, factor: f32) -> f32 { x * factor }
let r := scale(2.0, 3.0); // both literals are f32
let x: i32 := 10i32;
let y := x + 5; // 5 adopts i32 from x; y is i32
This also applies to var reassignment — the right-hand side of m := expr adopts m's declared type:
var count: i32 := 0;
count := 99; // 99 is i32
Formal rules
Legality Rule №1
The exact-width numeric primitive types are i8, i16, i32, i64, u8, u16,
u32, u64, f32, and f64.
Referenced by: rfc-0007
Tested by
1fun main() {
2 // ── Sized integer literals and equality ───────────────────────────────────
3 let a: i8 := 127i8;
4 let b: i16 := 32767i16;
5 let c: i32 := 2147483647i32;
6 let d: u8 := 255u8;
7 let e: u16 := 65535u16;
8 let f: u32 := 4294967295u32;
9 let g: u64 := 18446744073709551615u64;
10 let h: f32 := 1.5f32;
11
12 assert(a == 127i8);
13 assert(b == 32767i16);
14 assert(c == 2147483647i32);
15 assert(d == 255u8);
16 assert(e == 65535u16);
17 assert(f == 4294967295u32);
18 assert(g == 18446744073709551615u64);
19 assert(h == 1.5f32);
20
21 // ── Arithmetic preserves type ─────────────────────────────────────────────
22 let x: i32 := 10i32 + 5i32;
23 assert(x == 15i32);
24 let y: u8 := 200u8 - 100u8;
25 assert(y == 100u8);
26 let z: f32 := 2.0f32 * 3.0f32;
27 assert(z == 6.0f32);
28
29 // ── Comparison operators ──────────────────────────────────────────────────
30 assert(10i32 < 20i32);
31 assert(255u8 == 255u8);
32 assert(1.0f32 < 2.0f32);
33 assert(100u16 <= 100u16);
34 assert(5i8 != 6i8);
35
36 // ── From casts: sized → i64 / f64 ────────────────────────────────────────
37 let from_i8: i64 := 42i8 as i64;
38 let from_i16: i64 := 1000i16 as i64;
39 let from_i32: i64 := 100000i32 as i64;
40 let from_u8: i64 := 200u8 as i64;
41 let from_u16: i64 := 50000u16 as i64;
42 let from_u32: i64 := 123456u32 as i64;
43 let from_u64: i64 := 99u64 as i64;
44 let from_f32_to_i64: i64 := 7i8 as i64;
45
46 assert(from_i8 == 42);
47 assert(from_i16 == 1000);
48 assert(from_i32 == 100000);
49 assert(from_u8 == 200);
50 assert(from_u16 == 50000);
51 assert(from_u32 == 123456);
52 assert(from_u64 == 99);
53 assert(from_f32_to_i64 == 7);
54
55 let from_i8_f: f64 := 42i8 as f64;
56 let from_f32_f: f64 := 1.5f32 as f64;
57 assert(from_i8_f == 42.0);
58 assert(from_f32_f == 1.5);
59
60 // ── From casts: i64 / f64 → sized ────────────────────────────────────────
61 let to_u8: u8 := 200 as u8;
62 let to_i32: i32 := 42 as i32;
63 let to_u32: u32 := 999 as u32;
64 let to_u64: u64 := 0 as u64;
65
66 assert(to_u8 == 200u8);
67 assert(to_i32 == 42i32);
68 assert(to_u32 == 999u32);
69 assert(to_u64 == 0u64);
70
71 // ── Negation on signed types ──────────────────────────────────────────────
72 let neg_i8: i8 := -42i8;
73 let neg_i32: i32 := -1000i32;
74 assert(neg_i8 == -42i8);
75 assert(neg_i32 == -1000i32);
76}
passes
Legality Rule №2
Conversion between numeric types is written with an explicit as cast.
Referenced by: rfc-0007
Tested by
1fun main() {
2 // ── Sized integer literals and equality ───────────────────────────────────
3 let a: i8 := 127i8;
4 let b: i16 := 32767i16;
5 let c: i32 := 2147483647i32;
6 let d: u8 := 255u8;
7 let e: u16 := 65535u16;
8 let f: u32 := 4294967295u32;
9 let g: u64 := 18446744073709551615u64;
10 let h: f32 := 1.5f32;
11
12 assert(a == 127i8);
13 assert(b == 32767i16);
14 assert(c == 2147483647i32);
15 assert(d == 255u8);
16 assert(e == 65535u16);
17 assert(f == 4294967295u32);
18 assert(g == 18446744073709551615u64);
19 assert(h == 1.5f32);
20
21 // ── Arithmetic preserves type ─────────────────────────────────────────────
22 let x: i32 := 10i32 + 5i32;
23 assert(x == 15i32);
24 let y: u8 := 200u8 - 100u8;
25 assert(y == 100u8);
26 let z: f32 := 2.0f32 * 3.0f32;
27 assert(z == 6.0f32);
28
29 // ── Comparison operators ──────────────────────────────────────────────────
30 assert(10i32 < 20i32);
31 assert(255u8 == 255u8);
32 assert(1.0f32 < 2.0f32);
33 assert(100u16 <= 100u16);
34 assert(5i8 != 6i8);
35
36 // ── From casts: sized → i64 / f64 ────────────────────────────────────────
37 let from_i8: i64 := 42i8 as i64;
38 let from_i16: i64 := 1000i16 as i64;
39 let from_i32: i64 := 100000i32 as i64;
40 let from_u8: i64 := 200u8 as i64;
41 let from_u16: i64 := 50000u16 as i64;
42 let from_u32: i64 := 123456u32 as i64;
43 let from_u64: i64 := 99u64 as i64;
44 let from_f32_to_i64: i64 := 7i8 as i64;
45
46 assert(from_i8 == 42);
47 assert(from_i16 == 1000);
48 assert(from_i32 == 100000);
49 assert(from_u8 == 200);
50 assert(from_u16 == 50000);
51 assert(from_u32 == 123456);
52 assert(from_u64 == 99);
53 assert(from_f32_to_i64 == 7);
54
55 let from_i8_f: f64 := 42i8 as f64;
56 let from_f32_f: f64 := 1.5f32 as f64;
57 assert(from_i8_f == 42.0);
58 assert(from_f32_f == 1.5);
59
60 // ── From casts: i64 / f64 → sized ────────────────────────────────────────
61 let to_u8: u8 := 200 as u8;
62 let to_i32: i32 := 42 as i32;
63 let to_u32: u32 := 999 as u32;
64 let to_u64: u64 := 0 as u64;
65
66 assert(to_u8 == 200u8);
67 assert(to_i32 == 42i32);
68 assert(to_u32 == 999u32);
69 assert(to_u64 == 0u64);
70
71 // ── Negation on signed types ──────────────────────────────────────────────
72 let neg_i8: i8 := -42i8;
73 let neg_i32: i32 := -1000i32;
74 assert(neg_i8 == -42i8);
75 assert(neg_i32 == -1000i32);
76}
passes
Legality Rule №3
An unsuffixed numeric literal adopts the numeric type supplied by context; without
context, integer literals default to i64 and floating-point literals to f64.
Referenced by: rfc-0007
Tested by
1// Polymorphic integer and float literals: unsuffixed numeric literals unify with
2// whatever type the context demands, defaulting to i64 / f64 when unconstrained.
3
4struct Pixel {
5 r: u8,
6 g: u8,
7 b: u8,
8}
9
10fun add_i32(x: i32, y: i32) -> i32 { x + y }
11fun scale_f32(x: f32, factor: f32) -> f32 { x * factor }
12fun identity_u8(x: u8) -> u8 { x }
13fun returns_i16() -> i16 { 1000 }
14
15fun main() {
16 // ── Default: unconstrained literals become i64 / f64 ─────────────────────
17 let a := 42;
18 let b := 3.14;
19 assert(a == 42);
20 assert(b == 3.14);
21
22 // ── Type annotation coerces the literal ───────────────────────────────────
23 let c: i8 := 100;
24 let d: i16 := 1000;
25 let e: i32 := 50000;
26 let f: u8 := 200;
27 let g: u16 := 60000;
28 let h: u32 := 100000;
29 let i: u64 := 999999;
30 assert(c == 100i8);
31 assert(d == 1000i16);
32 assert(e == 50000i32);
33 assert(f == 200u8);
34 assert(g == 60000u16);
35 assert(h == 100000u32);
36 assert(i == 999999u64);
37
38 let p: f32 := 1.5;
39 assert(p == 1.5f32);
40
41 // ── Arithmetic propagates the constrained type ────────────────────────────
42 let x: i32 := 10;
43 let y := x + 5; // 5 picks up i32 from x
44 assert(y == 15i32);
45
46 let fx: f32 := 2.0f32;
47 let fy := fx + 1.0; // 1.0 picks up f32 from fx
48 assert(fy == 3.0f32);
49
50 // ── Function argument coercion ────────────────────────────────────────────
51 let r1 := add_i32(3, 4);
52 assert(r1 == 7i32);
53
54 let r2 := scale_f32(2.0, 3.0);
55 assert(r2 == 6.0f32);
56
57 let r3 := identity_u8(255);
58 assert(r3 == 255u8);
59
60 // ── Return type coercion ──────────────────────────────────────────────────
61 let r4: i16 := returns_i16();
62 assert(r4 == 1000i16);
63
64 // ── Struct field coercion ─────────────────────────────────────────────────
65 let px := Pixel { r = 255, g = 128, b = 0 };
66 assert(px.r == 255u8);
67 assert(px.g == 128u8);
68 assert(px.b == 0u8);
69
70 // ── Array element type propagation ───────────────────────────────────────
71 let arr: i32[] := [1, 2, 3];
72 assert(arr[0] == 1i32);
73 assert(arr[1] == 2i32);
74 assert(arr[2] == 3i32);
75
76 // ── Cast coercion ─────────────────────────────────────────────────────────
77 let cast_u8: u8 := 42 as u8;
78 assert(cast_u8 == 42u8);
79
80 let cast_f32: f32 := 7 as f32;
81 assert(cast_f32 == 7.0f32);
82
83 // ── Method dispatch defaults literal to i64 ───────────────────────────────
84 let s1 := 42.to_string();
85 assert(s1 == "42");
86
87 let s2 := 0.to_string();
88 assert(s2 == "0");
89
90 let s3 := 3.14.to_string();
91 assert(s3 == "3.14");
92
93 // ── Comparison between literal and sized variable ─────────────────────────
94 let vi32: i32 := 5i32;
95 assert(vi32 == 5); // 5 coerces to i32
96
97 let vf32: f32 := 1.0f32;
98 assert(vf32 == 1.0); // 1.0 coerces to f32
99
100 // ── Negative literals coerce correctly ────────────────────────────────────
101 let neg_i8: i8 := -100;
102 let neg_i32: i32 := -50000;
103 let neg_f32: f32 := -2.5;
104 assert(neg_i8 == -100i8);
105 assert(neg_i32 == -50000i32);
106 assert(neg_f32 == -2.5f32);
107}
passes
Dynamic Semantics №1
Integer overflow panics, unconditionally — Metel has no debug/release build-mode distinction of its own (the interpreter takes no such flag), so this applies the same way regardless of how the interpreter binary happens to have been compiled. Floating-point overflow follows IEEE 754 behavior.
Referenced by: rfc-0007
Tested by
1// RUNTIME_ERROR[overflow]
2fun main() {
3 let a: i8 := 127i8 + 1i8;
4 let _ := a;
5}
runtime errorR0007“overflow”
Char
Char represents a single Unicode scalar value. Character literals use single quotes: 'a', '\n', '\u{1F600}'.
fun main() {
let c: Char := 'a';
let code: u32 := u32::from(c);
let back: Char := Char::from(code);
}
Char is not u32 and not a string — no implicit coercions exist. Use
u32::from(c)Char methods D1 to get the Unicode
scalar value and Char::from(n)Char methods D1 to
construct from a code point; Char::from raises a runtime error if n is not a valid
Unicode scalar value.
Formal rules
Legality Rule №1
Char is a distinct Unicode-scalar type, not an alias for u32 or u8.
Referenced by: rfc-0007
Tested by
1fun main() {
2 // Literals and basic equality
3 let a: Char := 'A';
4 let z: Char := 'z';
5 let zero: Char := '0';
6 assert(a == 'A');
7 assert(z == 'z');
8 assert(zero == '0');
9 assert(a != z);
10
11 // Escape sequences
12 let newline: Char := '\n';
13 let tab: Char := '\t';
14 let backslash: Char := '\\';
15 let single_quote: Char := '\'';
16 assert(newline != tab);
17 assert(backslash == '\\');
18 assert(single_quote == '\'');
19
20 // Unicode escape
21 let smiley: Char := '\u{1F600}';
22 assert(smiley == '\u{1F600}');
23
24 // to_string
25 assert(a.to_string() == "A");
26 assert(zero.to_string() == "0");
27 assert(single_quote.to_string() == "'");
28
29 // Comparison operators (Unicode scalar order)
30 assert('A' < 'B');
31 assert('z' > 'a');
32 assert('0' < '9');
33 assert('A' <= 'A');
34 assert('B' >= 'A');
35
36 // Conversion to u32 (Unicode code point)
37 let code: u32 := a as u32;
38 assert(code == 65u32);
39
40 // Conversion from u32 back to Char
41 let back: Char := 65u32 as Char;
42 assert(back == 'A');
43
44 // Round-trip
45 let orig: Char := 'M';
46 let round: Char := (orig as u32) as Char;
47 assert(round == orig);
48
49 // Pattern matching
50 let greeting: String := match (a) {
51 'A' => "alpha",
52 'B' => "beta",
53 _ => "other",
54 };
55 assert(greeting == "alpha");
56
57 let category: String := match (zero) {
58 '0' => "digit",
59 'a' => "lower",
60 'A' => "upper",
61 _ => "other",
62 };
63 assert(category == "digit");
64}
passes
Type Inference
Types are inferred using the Hindley-Milner algorithm with let-polymorphism. Annotations are optional for all bindings, including function parameters and return types. They may be written explicitly for documentation or to restrict a binding to a less general type.
Annotations are required only where there is no expression to infer from:
- Struct and enum field types
- Aspect method signatures
Every named type in an annotation must resolve in the annotation's declaring scope,
including names nested inside arrays, tuples, function types, and record fields. This is
checked when the declaration is type-checked, even if no value ever reaches the
annotation. A generic parameter in scope and Self where it is permitted resolve as
types; every other unknown name is error T0003.
fun add_annotated(a: i64, b: i64) -> i64 { a + b }
fun add_inferred(a, b) { a + b }
fun main() -> i64 {
let x := 42; // inferred: i64
let name := "Vlad"; // inferred: String
let y: f64 := 3.14; // explicit annotation (optional here)
let total := add_annotated(x, 1) + add_inferred(2, 3);
if (name == "Vlad") { total + (y as i64) } else { 0 }
}
Formal rules
Legality Rule №1
An expression in return position is typechecked against the enclosing function or
method's declared return type, which supplies its expected type.
Referenced by: rfc-0019
Tested by
1// Stage 7: return type propagation from function context.
2// Pass 2 must propagate the declared return type into return/break expressions
3// so that None and bare enum variants can be typed without a local annotation.
4
5// Return None in a Perhaps<i64>-returning function.
6fun find(arr: i64[], target: i64) -> Perhaps<i64> {
7 return None;
8}
9
10// Return Result::Err where T is not present in Err's fields.
11fun divide(a: f64, b: f64) -> Result<f64, String> {
12 if (b == 0.0) {
13 return Result::Err { error = "division by zero" };
14 }
15 return Result::Ok { value = a / b };
16}
17
18// Break None in a loop whose result is annotated as Perhaps<i64>.
19fun first_positive(arr: i64[]) -> Perhaps<i64> {
20 let result: Perhaps<i64> := loop {
21 break None;
22 };
23 result
24}
passes
Legality Rule №2
An expression in break position is typechecked against its enclosing loop's value
type, independently of the enclosing function's return type.
Referenced by: rfc-0019
Tested by
1// Stage 7: return type propagation from function context.
2// Pass 2 must propagate the declared return type into return/break expressions
3// so that None and bare enum variants can be typed without a local annotation.
4
5// Return None in a Perhaps<i64>-returning function.
6fun find(arr: i64[], target: i64) -> Perhaps<i64> {
7 return None;
8}
9
10// Return Result::Err where T is not present in Err's fields.
11fun divide(a: f64, b: f64) -> Result<f64, String> {
12 if (b == 0.0) {
13 return Result::Err { error = "division by zero" };
14 }
15 return Result::Ok { value = a / b };
16}
17
18// Break None in a loop whose result is annotated as Perhaps<i64>.
19fun first_positive(arr: i64[]) -> Perhaps<i64> {
20 let result: Perhaps<i64> := loop {
21 break None;
22 };
23 result
24}
passes
Tuples
Tuples are lightweight anonymous product types.
fun main() -> i64 {
let coord: (i64, i64) := (10, 20);
let triple: (String, i64, boolean) := ("yes", 42, true);
return coord.0 + triple.1;
}
Positional field access uses zero-based selectors .0, .1, etc.L1:
fun main() -> i64 {
let coord: (i64, i64) := (10, 20);
let x := coord.0;
let y := coord.1;
return x + y;
}
() is the zero-element tuple (unit type).
Tuples can be destructured in match:
fun main() -> i64 {
let coord: (i64, i64) := (10, 0);
match (coord) {
(0, y) => y,
(x, 0) => x,
(x, y) => x + y,
}
}
Formal rules
Legality Rule №1
A tuple's elements are addressed by zero-based positional selectors. A selector is valid only for an element in the tuple's declared arity.
Tested by (2)
1fun main() {
2 // Construction and element access.
3 let t := (10, 20);
4 assert(t.0 == 10);
5 assert(t.1 == 20);
6 // Mixed types.
7 let pair := (1, true);
8 assert(pair.0 == 1);
9 assert(pair.1);
10 // Nested tuple.
11 let nested := (1, (2, 3));
12 let inner := nested.1;
13 assert(inner.0 == 2);
14 assert(inner.1 == 3);
15}
passes
1// TYPECHECK_ERROR[out of bounds]
2fun main() {
3 let t := (1, 2);
4 let _x := t.5;
5}
typecheck errorT0003“out of bounds”
Anonymous Records
A record is a product type whose components are labelled, where a tuple's are positional. It is written in bare braces, with no keyword:
{ x: f64, y: f64 } // the type
{ x = 1.0, y = 2.0 } // a value of it
Field declarations classify and take :; field initializers define and take = — the same
distinction let x: i64 = 1 already draws.
A record type is exact. { x: f64 } is inhabited only by records with that row and
nothing else; a value of { x: f64, y: f64 } is not a value of { x: f64 }. Records are
not implicitly widened or narrowed.
Records are structurally typed. Two records with the same labels and field types are the
same type, wherever they were written. A record has no declaration site and no name.
Field order does not matter: { x: i64, y: i64 } and { y: i64, x: i64 } are the same
type, and { x = 1, y = 2 } and { y = 2, x = 1 } are indistinguishable — each is usable
wherever the other is. A record is a set of labelled fields, not an ordered one. Repeating a
label in one record ({ x: i64, x: f64 }) is an error.
(Indistinguishable is a statement about the type, not about ==, which no compound type —
record, struct, tuple, or array — supports.)
When a local variable has the same name as a field, the = value part may be omitted, as in
a struct literal:
fun main() {
let x := 1.0;
let y := 2.0;
let p := { x, y }; // { x: f64, y: f64 }
println("${p.x}");
}
Punning, and single-field record literals generally, are read as records only in positions
that expect an expression — a let/var or field initializer, a call argument, an array
element. In a position that also admits a block — an if/else or match arm, a
function, closure, or loop body — a bare { x } is a block whose result is x, and
{ x = 1 } is a block whose result is the assignment. Write the record in parentheses to
force it: ({ x }). A multi-field literal needs no parentheses, as { x = 1, y = 2 }
cannot be a block.
Where records may be used
Records are ordinary values: they may appear as parameters, returns, let bindings, and
struct or enum fields; they may be pattern-matched, used as generic arguments, and borrowed
(&{ x: f64 }) exactly as a struct is. Send and Sync extend
to them by the same field-composition rule used for structs.
Three things a record cannot do, all for the same underlying reason — it has no nominal owner:
- No inherent methods. Two unrelated modules could otherwise write conflicting methods for the same shape with no principled way to choose between them.
- No implementations of a non-local aspect, by the other direction of that rule. An aspect local to the current module may be implemented for a record — but see the note below: that is not available yet.
- No custom
Drop.Dropis a standard-library aspect and never local to ordinary user code, so teardown logic belongs to nominal types only.
extend { w: i64 }: MyAspect { … } does not work. Arrays are the exception: extend<T> T[]: MyAspect { … } is supported, per the orphan-rule carve-out for structural type constructors — see Declarations — Structural Aspect Bounds. Until a record or tuple target is supported, a record satisfies no aspect that requires an implementation, so a record cannot be printed, compared, or passed where any such bound is required. Auto-derived aspects are unaffectedProjection
A nominal type's row may be projected to a named subset, written with a dot to distinguish it from a struct literal:
Handle.{ fd } // the type: Handle's row, narrowed to `fd`
A bare identifier inside projection braces is always a field label, never a type or a
row variable. Chained projection (S.{ a }.{ b }) and projection in pattern position are not
accepted.
Inside an extend block, Self.{ fd } projects Self's own row exactly as
Handle.{ fd } would project Handle's — Self resolves to the enclosing block's
target type here the same way it does everywhere else the target's name can stand in
for it.
Formal rules
Dynamic Semantics №1
Record identity is structural: records with the same labelled fields and field types are the same type regardless of declaration-free spelling order.
Referenced by: rfc-0116
Tested by
1// Extra anonymous-record coverage (RFC-0116): nesting, records as struct
2// fields, whole-value and field mutation, multi-field projection, the
3// order-insensitive identity across a function boundary, and pattern binding.
4
5struct Wrap { inner: { x: i64, y: i64 } }
6
7struct Handle { fd: i64, tag: i64, mode: i64 }
8
9// The declared return row is written `{ b, a }`; the call site below annotates
10// `{ a, b }`. They are the same type — field order is not part of identity.
11fun swap_names(a: i64, b: i64) -> { b: i64, a: i64 } {
12 ({ a = a, b = b })
13}
14
15// A block whose tail is a record-typed identifier is a perfectly ordinary
16// record return — no parentheses needed. (Regression guard: an earlier
17// block-vs-record heuristic wrongly rejected this valid form.)
18fun via_ident() -> { a: i64 } {
19 let r := ({ a = 1 });
20 r
21}
22
23fun main() {
24 // Nested records, and access through two levels.
25 let nested := { outer = { inner = 7 } };
26 assert(nested.outer.inner == 7);
27
28 // A record as a struct field.
29 let w := Wrap { inner = { x = 1, y = 2 } };
30 assert(w.inner.x == 1);
31 assert(w.inner.y == 2);
32
33 // Order-insensitive identity across a call: `{ b, a }` result used where
34 // `{ a, b }` is annotated.
35 let ab: { a: i64, b: i64 } := swap_names(10, 20);
36 assert(ab.a == 10);
37 assert(ab.b == 20);
38
39 // Record-typed identifier as a block tail (regression guard).
40 assert(via_ident().a == 1);
41
42 // Whole-value mutation through a `var` binding.
43 var p := { x = 1, y = 1 };
44 p := { x = 5, y = 6 };
45 assert(p.x == 5);
46 assert(p.y == 6);
47
48 // Field mutation.
49 p.y := 9;
50 assert(p.y == 9);
51
52 // Multi-field projection of a nominal type's row.
53 let h := Handle { fd = 3, tag = 4, mode = 5 };
54 let picked := h.{ fd, mode };
55 assert(picked.fd == 3);
56 assert(picked.mode == 5);
57
58 // Pattern binding, both fields used.
59 let total := match (p) { { x, y } => x + y, };
60 assert(total == 14);
61}
passes
Legality Rule №1
An anonymous record cannot satisfy an impl-based aspect bound, because no implementation for a record target is available.
Referenced by: rfc-0116
Tested by
1// Negative (RFC-0116 §3): an anonymous record has no nominal owner, so it satisfies no
2// impl-based aspect. It must be rejected at the call site, like a tuple or a struct
3// without the impl — not accepted and then blown up at run time.
4fun show<T: Display>(x: T) -> String { x.to_string() }
5
6fun main() {
7 let r := { x = 1 };
8 let s := show(r);
9}
typecheck errorT0012“anything impl-based needs a nominal type”
Dynamic Semantics №2
Projection Handle.{ fd, mode } yields the record made from precisely the named fields of
the nominal receiver type.
Referenced by: rfc-0116
Tested by (3)
1// #774: `Self` resolves inside a record projection (`Self.{ field }`) exactly as
2// it does as a plain type in the same position -- confirmed by writing the same
3// method both ways and getting the same result from the same shaped argument.
4// Before the fix, only the `Handle.{ fd }` (concrete-name) spelling resolved;
5// `Self.{ fd }` failed with "unknown type `Self`" during the eager
6// projections-validity pass, and -- once that pass no longer masked it -- a
7// second, independent gap in the real signature resolution (no registry access
8// at the one place `Self` alone already had a target name to resolve against)
9// surfaced right behind it.
10//
11// Called with `h.{ fd }` -- RFC-0116's expression-position projection, which
12// evaluates the real `Handle` value and extracts a genuine `{ fd: i64 }` record
13// from it -- rather than a bare `Handle` value. A parameter typed `Self.{ fd }`/
14// `Handle.{ fd }` intentionally does not accept a bare struct value: no implicit
15// struct-to-record coercion exists anywhere (RFC-0118's already-shipped row-bound
16// rule is the same principle: "only a record satisfies a row bound; a nominal
17// struct is rejected even when it has matching fields"). `.{ fd }` is the
18// explicit, already-implemented way to produce the narrower value the parameter
19// asks for -- not a workaround, the intended pairing.
20
21struct Handle { fd: i64, name: String }
22
23extend Handle {
24 fun describe_self(h: Self.{ fd }) -> i64 {
25 return h.fd;
26 }
27 fun describe_named(h: Handle.{ fd }) -> i64 {
28 return h.fd;
29 }
30}
31
32fun main() {
33 let h := Handle { fd = 3, name = "stdin" };
34 assert(Handle::describe_self(h.{ fd }) == 3);
35 assert(Handle::describe_named(h.{ fd }) == 3);
36}
passes
1// #774 architectural revision: a body-internal `let x: Self.{ field } = ...;`
2// annotation resolves the same way `Self.{ field }` already does in param/return
3// position, now that `Self` is bound as an ordinary type parameter for the whole
4// method rather than only its own signature. The value side uses `self.{ fd }`
5// (RFC-0116's expression-position projection on the receiver itself), the
6// idiomatic way to produce it -- not an anonymous record literal built by hand.
7
8struct Handle { fd: i64, name: String }
9
10extend Handle {
11 fun narrow_let(self) -> i64 {
12 let x: Self.{ fd } := self.{ fd };
13 return x.fd;
14 }
15}
16
17fun main() {
18 let h := Handle { fd = 3, name = "stdin" };
19 assert(h.narrow_let() == 3);
20}
passes
1// Extra anonymous-record coverage (RFC-0116): nesting, records as struct
2// fields, whole-value and field mutation, multi-field projection, the
3// order-insensitive identity across a function boundary, and pattern binding.
4
5struct Wrap { inner: { x: i64, y: i64 } }
6
7struct Handle { fd: i64, tag: i64, mode: i64 }
8
9// The declared return row is written `{ b, a }`; the call site below annotates
10// `{ a, b }`. They are the same type — field order is not part of identity.
11fun swap_names(a: i64, b: i64) -> { b: i64, a: i64 } {
12 ({ a = a, b = b })
13}
14
15// A block whose tail is a record-typed identifier is a perfectly ordinary
16// record return — no parentheses needed. (Regression guard: an earlier
17// block-vs-record heuristic wrongly rejected this valid form.)
18fun via_ident() -> { a: i64 } {
19 let r := ({ a = 1 });
20 r
21}
22
23fun main() {
24 // Nested records, and access through two levels.
25 let nested := { outer = { inner = 7 } };
26 assert(nested.outer.inner == 7);
27
28 // A record as a struct field.
29 let w := Wrap { inner = { x = 1, y = 2 } };
30 assert(w.inner.x == 1);
31 assert(w.inner.y == 2);
32
33 // Order-insensitive identity across a call: `{ b, a }` result used where
34 // `{ a, b }` is annotated.
35 let ab: { a: i64, b: i64 } := swap_names(10, 20);
36 assert(ab.a == 10);
37 assert(ab.b == 20);
38
39 // Record-typed identifier as a block tail (regression guard).
40 assert(via_ident().a == 1);
41
42 // Whole-value mutation through a `var` binding.
43 var p := { x = 1, y = 1 };
44 p := { x = 5, y = 6 };
45 assert(p.x == 5);
46 assert(p.y == 6);
47
48 // Field mutation.
49 p.y := 9;
50 assert(p.y == 9);
51
52 // Multi-field projection of a nominal type's row.
53 let h := Handle { fd = 3, tag = 4, mode = 5 };
54 let picked := h.{ fd, mode };
55 assert(picked.fd == 3);
56 assert(picked.mode == 5);
57
58 // Pattern binding, both fields used.
59 let total := match (p) { { x, y } => x + y, };
60 assert(total == 14);
61}
passes
Legality Rule №3
An anonymous record is rejected as an inherent-implementation target, as the target of a
non-local aspect implementation, and as the target of a custom Drop implementation.
Referenced by: rfc-0116
Tested by (3)
1// Negative: anonymous records have no nominal owner, so no inherent methods.
2extend { x: i64 } {
3 fun get(&self) -> i64 { self.x }
4}
typecheck errorT0001“cannot have inherent methods”
1// Negative: anonymous records cannot carry custom teardown logic.
2extend { x: i64 }: Drop {
3 fun drop(&self) {}
4}
typecheck errorT0001“cannot implement `Drop`”
1// Negative: `Display` is a standard-library aspect (not local to this module),
2// so it cannot be implemented for an anonymous record.
3extend { x: i64 }: Display {
4 fun to_string(&self) -> String { "x" }
5}
typecheck errorT0014“orphan implementation”
Arrays
Array<T> is the built-in ordered sequence type. The shorthand T[] is preferred.
fun main() -> i64 {
let nums: i64[] := [1, 2, 3];
let names: Array<String> := ["alice", "bob"];
if (names.len() == 2) { nums[0] } else { 0 }
}
Index access uses [] with a u64 index. Out-of-bounds access causes a panic.
fun main() -> i64 {
let nums: i64[] := [1, 2, 3];
let first := nums[0];
return first;
}
Arrays are usable in for-in loops.
T[] is no longer an owning, mutable bufferT[] is a non-owning, immutable, unconditionally-Copy view over a contiguous run — a
pointer and a length — produced only by borrowing a List<T>, a [T; N], or another slice.
Assignment through a slice, such as a[0] = 9, does not compile; mutation belongs to
List<T> or [T; N]. Array literals produce [T; N] (below), not T[]; let nums: i64[] = [1, 2, 3]; continues to work through [T; N]'s implicit coercion to T[]
(RFC-0053), not because the literal itself is a T[].
The three-way split between T[], [T; N], and List<T> below reflects the current
design. The exact boundary between them — in particular, how a growable list's storage is
allocated and grown — is not yet fully specified and may change in a future release.
Formal rules
Legality Rule №1
T[] is an unconditionally-Copy, non-owning borrowed view. It has no Drop; using a
view does not move the underlying elements out of the view.
Referenced by: rfc-0061, rfc-0071, rfc-0126
Tested by (4)
1fun first<T>(items: T[]) -> T {
2 for (item in items) {
3 return item;
4 }
5 panic("empty")
6}
7
8fun main() { }
typecheck errorT0019“it is borrowed from a `T[]` view”
1fun first<T: Copy>(items: T[]) -> T {
2 for (item in items) {
3 return item;
4 }
5 panic("empty")
6}
7
8fun main() {
9 let values: i64[] := [1, 2, 3];
10 assert(first(values) == 1);
11}
passes
1fun main() {
2 let xs := [1, 2, 3];
3 assert(xs.to_string() == "[1, 2, 3]");
4 assert([4, 5].to_string() == "[4, 5]");
5}
passes
1struct Box {
2 value: i64,
3}
4
5extend Box: Display {
6 fun to_string(&self) -> String {
7 return self.value.to_string();
8 }
9}
10
11extend Box: Clone {
12 fun clone(&self) -> Self {
13 return Box { value = self.value };
14 }
15}
16
17fun main() {
18 let xs := [Box { value = 1 }, Box { value = 2 }, Box { value = 3 }];
19 let ys := xs.clone();
20 assert(ys.to_string() == "[1, 2, 3]");
21}
passes
Legality Rule №2
An array index expression must have type u64.
Referenced by: rfc-0007
Tested by
Fixed-size arrays
[T; N] is an array type whose length N is a non-negative integer literal known at compile time.
[T; N] coerces to T[] (not the reverse). N must be a non-negative integer literal; variables are not permitted.
fun main() {
// Repeat construction: every element is the same value.
let zeros: [i64; 3] := [0; 3];
// Literal construction with an explicit sized type.
let ones: [i64; 3] := [1, 2, 3];
// Coerces to T[] when a T[] is expected.
fun first(xs: i64[]) -> i64 { xs[0] }
let v := first(ones); // [i64; 3] → i64[]
}
Indexing and for-in work identically to T[]. Array patterns match sized arrays:
fun sum(xs: [i64; 3]) -> i64 {
match (xs) {
[a, b, c] => a + b + c, // exact-count pattern on [T; 3]
}
}
[T; N], not T[]An unannotated literal such as [1, 2, 3] has type [i64; 3]: its length is statically
known and it owns its elements. Slices arise only from borrowing, never from a literal. The
[T; N] → T[] coercion above applies wherever T[] is expected — a let/var target, a
function argument, or a generic instantiation — so existing call sites need not change when
they already accept a [T; N]-typed or explicitly T[]-annotated value. Only an
unannotated literal's own type changed.
See the note under "Arrays" above — this split is not considered final.
Formal rules
Legality Rule №1
An array literal has fixed-size-array type [T; N], not T[], where N is its literal
element count.
Referenced by: rfc-0126
Tested by
1// Stage 3: fixed-size array type [T; N]
2//
3// [expr; N] constructs a SizedArray.
4// [T; N] is the type annotation syntax.
5// [T; N] coerces to T[] (one-directional).
6// Indexing [T; N] yields T.
7// for-in over [T; N] yields T.
8// Pattern [a, b, c] matches [T; 3] exactly.
9// Pattern [head, ..rest] matches any array.
10
11fun sum3(xs: [i64; 3]) -> i64 {
12 xs[0] + xs[1] + xs[2]
13}
14
15let zeros: [i64; 3] := [0; 3];
16let ones: [i64; 3] := [1, 2, 3];
17let result: i64 := sum3(ones);
18
19// Coercion: [T; N] coerces to T[] when passed as T[] argument.
20fun first(xs: i64[]) -> i64 { xs[0] }
21let coerced: i64 := first(zeros);
22
23// For-in over sized array.
24fun total(xs: [i64; 3]) -> i64 {
25 var acc := 0;
26 for (x in xs) {
27 acc += x;
28 }
29 acc
30}
31
32// len() is available on sized arrays.
33fun sized_len(xs: [i64; 3]) -> i64 { xs.len() }
34let _n: i64 := sized_len(ones);
passes
Legality Rule №2
[T; N] implicitly coerces to T[] wherever T[] is expected. The reverse coercion
is not permitted.
Referenced by: rfc-0053, rfc-0126
Tested by (3)
1fun first<T: Copy>(items: T[]) -> T {
2 for (item in items) {
3 return item;
4 }
5 panic("empty")
6}
7
8fun main() {
9 let values: i64[] := [1, 2, 3];
10 assert(first(values) == 1);
11}
passes
1// Stage 3: fixed-size array type [T; N]
2//
3// [expr; N] constructs a SizedArray.
4// [T; N] is the type annotation syntax.
5// [T; N] coerces to T[] (one-directional).
6// Indexing [T; N] yields T.
7// for-in over [T; N] yields T.
8// Pattern [a, b, c] matches [T; 3] exactly.
9// Pattern [head, ..rest] matches any array.
10
11fun sum3(xs: [i64; 3]) -> i64 {
12 xs[0] + xs[1] + xs[2]
13}
14
15let zeros: [i64; 3] := [0; 3];
16let ones: [i64; 3] := [1, 2, 3];
17let result: i64 := sum3(ones);
18
19// Coercion: [T; N] coerces to T[] when passed as T[] argument.
20fun first(xs: i64[]) -> i64 { xs[0] }
21let coerced: i64 := first(zeros);
22
23// For-in over sized array.
24fun total(xs: [i64; 3]) -> i64 {
25 var acc := 0;
26 for (x in xs) {
27 acc += x;
28 }
29 acc
30}
31
32// len() is available on sized arrays.
33fun sized_len(xs: [i64; 3]) -> i64 { xs.len() }
34let _n: i64 := sized_len(ones);
passes
1// A dynamic array has no statically known length and cannot satisfy [T; N].
2fun main() {
3 let dynamic: i64[] := [1, 2, 3];
4 let fixed: [i64; 3] := dynamic; // ERROR[T0001]
5 println(fixed[0].to_string());
6}
typecheck errorT0001at 4
Legality Rule №3
[T; N] is a fixed-size-array type only when N is a non-negative integer literal;
the element type and literal length both participate in type identity, including for
[T; 0].
Referenced by: rfc-0053
Tested by (4)
1fun main() {
2 // Empty sized array [T; 0] — len() returns 0.
3 let empty: [i64; 0] := [0; 0];
4 assert(empty.len() == 0);
5
6 // Single-element sized array.
7 let single: [i64; 1] := [42];
8 assert(single[0] == 42);
9
10 // Repeat with a non-trivial expression.
11 let computed: [i64; 3] := [2 + 3; 3];
12 assert(computed[0] == 5);
13 assert(computed[1] == 5);
14 assert(computed[2] == 5);
15
16 // Mutation of a sized array element.
17 var arr: [i64; 3] := [1, 2, 3];
18 arr[1] := 99;
19 assert(arr[0] == 1);
20 assert(arr[1] == 99);
21 assert(arr[2] == 3);
22
23 // Coercion: [T; N] iterates via for-in (same as T[]).
24 let sized: [i64; 4] := [10, 20, 30, 40];
25 var dyn_sum := 0;
26 for (x in sized) {
27 dyn_sum += x;
28 }
29 assert(dyn_sum == 100);
30
31 // Pattern: ..rest is empty when only one element in the sized array.
32 let arr1: [i64; 1] := [42];
33 let rest_empty := match (arr1) {
34 [head, ..rest] => {
35 var cnt := 0;
36 for (_ in rest) { cnt += 1; }
37 head + cnt
38 },
39 };
40 assert(rest_empty == 42);
41
42 // Pattern: ..rest collects remaining elements.
43 let arr2: [i64; 4] := [1, 2, 3, 4];
44 let rest_sum := match (arr2) {
45 [_a, _b, ..rest] => rest[0] + rest[1],
46 };
47 assert(rest_sum == 7);
48
49 // Exact-count pattern: element bindings are correct.
50 let coords: [i64; 3] := [3, 4, 0];
51 let dist_sq := match (coords) {
52 [x, y, _z] => x * x + y * y,
53 };
54 assert(dist_sq == 25);
55
56 // for-in over a repeat-constructed sized array.
57 var total := 0;
58 for (v in [7; 5]) {
59 total += v;
60 }
61 assert(total == 35);
62}
passes
1// Stage 3: fixed-size array type [T; N]
2//
3// [expr; N] constructs a SizedArray.
4// [T; N] is the type annotation syntax.
5// [T; N] coerces to T[] (one-directional).
6// Indexing [T; N] yields T.
7// for-in over [T; N] yields T.
8// Pattern [a, b, c] matches [T; 3] exactly.
9// Pattern [head, ..rest] matches any array.
10
11fun sum3(xs: [i64; 3]) -> i64 {
12 xs[0] + xs[1] + xs[2]
13}
14
15let zeros: [i64; 3] := [0; 3];
16let ones: [i64; 3] := [1, 2, 3];
17let result: i64 := sum3(ones);
18
19// Coercion: [T; N] coerces to T[] when passed as T[] argument.
20fun first(xs: i64[]) -> i64 { xs[0] }
21let coerced: i64 := first(zeros);
22
23// For-in over sized array.
24fun total(xs: [i64; 3]) -> i64 {
25 var acc := 0;
26 for (x in xs) {
27 acc += x;
28 }
29 acc
30}
31
32// len() is available on sized arrays.
33fun sized_len(xs: [i64; 3]) -> i64 { xs.len() }
34let _n: i64 := sized_len(ones);
passes
1// [expr; N] with annotation [T; M] where N ≠ M must be rejected.
2let x: [i64; 3] := [0; 4]; // ERROR[T0001]
typecheck errorT0001at 2
1// Element type mismatch in repeat construction: [boolean; 3] cannot satisfy [i64; 3].
2let x: [i64; 3] := [true; 3]; // ERROR[T0001]
typecheck errorT0001at 2
Dynamic Semantics №1
A repeat array expression [expr; N] evaluates expr once, then clones that result to
produce all N elements.
Referenced by: rfc-0053
Tested by
1fun main() {
2 var calls := 0;
3 var calls_ref: &var i64 := &var calls;
4 var next := [&var calls_ref] var || -> i64 {
5 *calls_ref += 1;
6 *calls_ref
7 };
8 let values: [i64; 3] := [next(); 3];
9 assert(calls == 1);
10 assert(values[0] == 1);
11 assert(values[1] == 1);
12 assert(values[2] == 1);
13}
passes
Legality Rule №4
Where [T; N] is expected, an array literal is accepted only when it contains exactly
N elements of type T.
Referenced by: rfc-0053
Tested by (3)
1// Stage 3: fixed-size array type [T; N]
2//
3// [expr; N] constructs a SizedArray.
4// [T; N] is the type annotation syntax.
5// [T; N] coerces to T[] (one-directional).
6// Indexing [T; N] yields T.
7// for-in over [T; N] yields T.
8// Pattern [a, b, c] matches [T; 3] exactly.
9// Pattern [head, ..rest] matches any array.
10
11fun sum3(xs: [i64; 3]) -> i64 {
12 xs[0] + xs[1] + xs[2]
13}
14
15let zeros: [i64; 3] := [0; 3];
16let ones: [i64; 3] := [1, 2, 3];
17let result: i64 := sum3(ones);
18
19// Coercion: [T; N] coerces to T[] when passed as T[] argument.
20fun first(xs: i64[]) -> i64 { xs[0] }
21let coerced: i64 := first(zeros);
22
23// For-in over sized array.
24fun total(xs: [i64; 3]) -> i64 {
25 var acc := 0;
26 for (x in xs) {
27 acc += x;
28 }
29 acc
30}
31
32// len() is available on sized arrays.
33fun sized_len(xs: [i64; 3]) -> i64 { xs.len() }
34let _n: i64 := sized_len(ones);
passes
1// [expr; N] with annotation [T; M] where N ≠ M must be rejected.
2let x: [i64; 3] := [0; 4]; // ERROR[T0001]
typecheck errorT0001at 2
1// Element type mismatch in repeat construction: [boolean; 3] cannot satisfy [i64; 3].
2let x: [i64; 3] := [true; 3]; // ERROR[T0001]
typecheck errorT0001at 2
Legality Rule №5
A fixed-size array type [T; N] is valid as a struct field type.
Referenced by: rfc-0053
Tested by
1// Regression: field and index assignment must work with chained field access,
2// not just bare identifiers (#110).
3
4fun main() {
5 // Struct declarations inside function bodies (#125).
6 struct Point { x: i64, y: i64 }
7 struct Line { start: Point, end: Point }
8 struct Container { items: [i64; 3] }
9
10 // Chained field assignment: a.b.c = val (#110)
11 var ln := Line { start = Point { x = 0, y = 0 }, end = Point { x = 10, y = 10 } };
12 ln.start.x := 5;
13 ln.start.y := 7;
14 ln.end.x := 20;
15 assert(ln.start.x == 5);
16 assert(ln.start.y == 7);
17 assert(ln.end.x == 20);
18 assert(ln.end.y == 10); // unchanged
19
20 // Index assignment via a field-access receiver: a.field[i] = val (#110)
21 var c := Container { items = [1, 2, 3] };
22 c.items[1] := 99;
23 assert(c.items[1] == 99);
24 assert(c.items[0] == 1); // unchanged
25}
passes
Legality Rule №6
A fixed-size array may have another fixed-size array as its element type, such as
[[i64; 2]; 2].
Referenced by: rfc-0053
Tested by
1fun main() {
2 let matrix: [[i64; 2]; 2] := [[1, 2], [3, 4]];
3 assert(matrix[0][1] == 2);
4 assert(matrix[1][0] == 3);
5}
passes
Legality Rule №7
An exact array pattern for a [T; N] value must have a compatible element count; a
different exact count is rejected.
Referenced by: rfc-0053
Tested by (3)
1fun main() {
2 // Repeat construction: [expr; N].
3 let zeros := [0; 3];
4 assert(zeros[0] == 0);
5 assert(zeros[1] == 0);
6 assert(zeros[2] == 0);
7
8 // Literal construction typed as [T; N].
9 let ones: [i64; 3] := [1, 2, 3];
10 assert(ones[0] == 1);
11 assert(ones[1] == 2);
12 assert(ones[2] == 3);
13
14 // Indexing.
15 let arr: [i64; 4] := [10, 20, 30, 40];
16 assert(arr[0] == 10);
17 assert(arr[3] == 40);
18
19 // For-in iteration.
20 var sum := 0;
21 for (x in [1; 4]) {
22 sum += x;
23 }
24 assert(sum == 4);
25
26 // Array pattern — exact match (sized array scrutinee).
27 let sized: [i64; 3] := [1, 2, 3];
28 let got := match (sized) {
29 [a, b, c] => a + b + c,
30 };
31 assert(got == 6);
32
33 // Array pattern — rest binding (works on any array).
34 let arr2: [i64; 4] := [10, 20, 30, 40];
35 let tail_sum := match (arr2) {
36 [_head, ..rest] => rest[0] + rest[1] + rest[2],
37 };
38 assert(tail_sum == 90);
39}
passes
1fun main() {
2 // Empty sized array [T; 0] — len() returns 0.
3 let empty: [i64; 0] := [0; 0];
4 assert(empty.len() == 0);
5
6 // Single-element sized array.
7 let single: [i64; 1] := [42];
8 assert(single[0] == 42);
9
10 // Repeat with a non-trivial expression.
11 let computed: [i64; 3] := [2 + 3; 3];
12 assert(computed[0] == 5);
13 assert(computed[1] == 5);
14 assert(computed[2] == 5);
15
16 // Mutation of a sized array element.
17 var arr: [i64; 3] := [1, 2, 3];
18 arr[1] := 99;
19 assert(arr[0] == 1);
20 assert(arr[1] == 99);
21 assert(arr[2] == 3);
22
23 // Coercion: [T; N] iterates via for-in (same as T[]).
24 let sized: [i64; 4] := [10, 20, 30, 40];
25 var dyn_sum := 0;
26 for (x in sized) {
27 dyn_sum += x;
28 }
29 assert(dyn_sum == 100);
30
31 // Pattern: ..rest is empty when only one element in the sized array.
32 let arr1: [i64; 1] := [42];
33 let rest_empty := match (arr1) {
34 [head, ..rest] => {
35 var cnt := 0;
36 for (_ in rest) { cnt += 1; }
37 head + cnt
38 },
39 };
40 assert(rest_empty == 42);
41
42 // Pattern: ..rest collects remaining elements.
43 let arr2: [i64; 4] := [1, 2, 3, 4];
44 let rest_sum := match (arr2) {
45 [_a, _b, ..rest] => rest[0] + rest[1],
46 };
47 assert(rest_sum == 7);
48
49 // Exact-count pattern: element bindings are correct.
50 let coords: [i64; 3] := [3, 4, 0];
51 let dist_sq := match (coords) {
52 [x, y, _z] => x * x + y * y,
53 };
54 assert(dist_sq == 25);
55
56 // for-in over a repeat-constructed sized array.
57 var total := 0;
58 for (v in [7; 5]) {
59 total += v;
60 }
61 assert(total == 35);
62}
passes
1// Exact-count pattern [a, b] on a [i64; 3] scrutinee has the wrong element count.
2// The constraint [i64; 3] ~ [i64; 2] fails, leaving the match non-exhaustive.
3fun main() {
4 let sized: [i64; 3] := [1, 2, 3];
5 let _ := match (sized) { [a, b] => a + b, }; // ERROR[T0008]
6}
typecheck errorT0008at 5
Legality Rule №8
The length in [T; N] is an integer literal, not a named generic type parameter or an
arbitrary runtime expression.
Referenced by: rfc-0053
Tested by
Legality Rule №9
Every literal index into [T; 0] is statically rejected because it is out of bounds.
Referenced by: rfc-0053
Tested by
1// Every literal index is out of bounds for an empty fixed-size array.
2fun main() {
3 let empty: [i64; 0] := [];
4 let x := empty[0]; // ERROR[T0001]
5 println(x.to_string());
6}
typecheck errorT0001at 4
References
Reference types provide explicit aliasing.
fun main() -> i64 {
var value := 1;
let p: &i64 := &value;
let q: &var i64 := &var value;
*q := *p + 1;
return q;
}
Metel has two reference types:
&T— shared immutable reference toT&var T— exclusive mutable reference toT
&T borrows, or exactly one &var T, never both"Exclusive" means exactly that rule. It is not yet enforced: the current interpreter has
no borrow checker, so a program may hold two &var T to the same place and will not be
rejected.
&var T coerces to &T. The reverse coercion does not exist. Both are non-owning
aliases — a reference never owns the value it points to.
&T is Copy; &var T is not, so an exclusive reference is moved on use rather than
duplicated. Passing one as an argument reborrows instead of moving — see
§References and moves.
References are first-class values, but they are distinct from the referent type. Ordinary
access — field reads/writes, indexing, method dispatch, reading a plain value out — goes
through auto-deref and type-directed copy; an explicit dereference operator *p is also
available (v0.11.0) for reading through a reference and for writing through a
&var T (*p = v). See §References.
&var accepts arbitrary addressable lvalue paths — struct fields, tuple elements, array elements, and chains thereof. Writes through the resulting &var T propagate back to the original storage location:
struct Counter { value: i64 }
fun main() -> i64 {
var c := Counter { value = 0 };
let p: &var i64 := &var c.value;
*p := 42;
return c.value; // 42
}
&var for lvalue pathsv0.10.0Formal rules
Legality Rule №1
An &var T reference may be used where &T is expected; an &T reference may not be
used where &var T is expected.
Referenced by: rfc-0067a
Tested by
1// RFC-0067a §1: &var T coerces to &T (the reverse does not exist).
2fun read_shared(r: &i64) -> i64 {
3 return r;
4}
5
6fun main() {
7 var n := 7;
8 let m: &var i64 := &var n;
9 let m2: &var i64 := &var n;
10 let s: &i64 := m2;
11 assert(read_shared(s) == 7);
12 assert(read_shared(m) == 7);
13}
passes
Reading a value out of a reference
No field, no method, no operator — just the plain value a reference points to. This cannot be a move (references never own their referent), only a copy, and only when the referent's type permits copying:
fun main() -> i64 {
let x := 42;
let r: &i64 := &x;
let y: i64 := r; // type-directed copy: y's declared type differs from r's
return y;
}
The copy fires at every position where a declared or expected type is already
known — not only let/var bindings and explicit ascription, but also a return
value against the enclosing function's declared return type, a break value against
the enclosing loop's inferred type, and any tail expression of a function/method/
closure body, an if/else branch, or a match arm (each of those resolves its
result against a declared or expected type the same way a let binding does):
fun bump(p: &var i64) -> i64 {
*p += 1;
p // tail expression, no explicit `return` — copies out of p
}
It never fires silently at a plain call site; fun f(v: i64) called as f(r) where
r: &i64 is a type error, not an implicit copy. Argument position has no declared type
of its own for the rule to compare against, the same reason type-directed extraction of
an allocated value never fires implicitly at a plain-parameter call site either
(public/rfcs/2-accepted/rfc-0066-allocated-value-extraction.md §3a — not yet
integrated, cited here only for the parallel).
Chains through multiple reference layers the same way auto-deref does — reaching the declared type may require copying out of more than one layer:
fun main() -> i64 {
let x := 42;
let r: &i64 := &x;
let rr: &&i64 := &r;
let y: i64 := rr; // copies through both layers of the chain
return y;
}
Until affine ownership (Copy/Drop, not yet integrated) lands, this applies to
every type — the interpreter has no move semantics today (everything is deep-cloned on
bind), so there is no non-Copy type yet to exclude. Once ownership is integrated, a
non-Copy T cannot be produced this way.
Formal rules
Legality Rule №1
Where a declared or expected non-reference type is known, a reference expression may
copy out its referent through every reference layer only when the referent is Copy.
This applies to bindings, ascriptions, returns, breaks, and tail expressions, but not
to an un-ascribed call argument.
Referenced by: rfc-0067a
Tested by (8)
1// RFC-0067a §3a: reading a plain value out of a reference via an explicit `return`
2// with a declared return type. This is the RFC's own worked example.
3fun f() -> i64 {
4 var n := 1;
5 let p: &var i64 := &var n;
6 *p := 4;
7 return p;
8}
9
10fun main() {
11 assert(f() == 4);
12}
passes
1// RFC-0067a §3a: read-copy at an implicit tail expression (no `return` keyword),
2// including through an `if`/`else` whose branches are both reference-typed while the
3// function's own declared return type is the plain referent type.
4fun pick(cond: boolean, a: &i64, b: &i64) -> i64 {
5 if (cond) { a } else { b }
6}
7
8fun main() {
9 let x := 10;
10 let y := 20;
11 assert(pick(true, &x, &y) == 10);
12 assert(pick(false, &x, &y) == 20);
13}
passes
1// RFC-0067a §3a: read-copy at a `loop { break value; }` where the break value is a
2// reference, made concrete via an explicit ascription on the break value itself.
3fun main() {
4 var n := 0;
5 let p: &var i64 := &var n;
6 let result: i64 := loop {
7 break (p: i64);
8 };
9 assert(result == 0);
10}
passes
1// RFC-0067a §3: auto-deref chains through arbitrary depth ("a &&T will deref
2// through both levels if needed") applies to read-copy the same as ordinary
3// auto-deref — reading a plain value out of a chain of references must peel
4// every layer, not just one.
5fun main() {
6 let n := 7;
7 let r: &i64 := &n;
8 let rr: &&i64 := &r;
9 let rrr: &&&i64 := &rr;
10
11 let x: i64 := rr;
12 assert(x == 7);
13
14 let y: i64 := rrr;
15 assert(y == 7);
16
17 // Mixed shared/mut chain.
18 var m := 9;
19 let mr: &var i64 := &var m;
20 let rmr: &&var i64 := &mr;
21 let z: i64 := rmr;
22 assert(z == 9);
23
24 // Read-copy through a chain at `return`, not just `let`.
25 assert(read_through_chain(&rr) == 7);
26}
27
28fun read_through_chain(rrp: &&&i64) -> i64 {
29 return rrp;
30}
passes
1// Read-copy (RFC-0067a §3a) must decide whether to peel against the *substituted* type,
2// not the raw one. A call returning `&T` yields a fresh inference variable at the point
3// `constrain_with_read_copy` runs; matching that raw variable against a reference pattern
4// fails, so the peel was silently skipped and the later unification reported T0001.
5//
6// The effect was that `let n: i64 = g();` failed where `let n: i64 = r;` succeeded --
7// same position, same types, different value shape. RFC-0112 §1.0.
8
9fun shared() -> &i64 {
10 let a := 42;
11 return &a;
12}
13
14fun chained() -> &&i64 {
15 let a := 42;
16 let r: &i64 := &a;
17 return &r;
18}
19
20fun through_return() -> i64 {
21 return shared();
22}
23
24fun main() {
25 // let annotation
26 let n: i64 := shared();
27 assert(n == 42);
28
29 // binding the call result first, then copying out at the annotation
30 let r := shared();
31 let m: i64 := r;
32 assert(m == 42);
33
34 // ascription
35 let a := shared(): i64;
36 assert(a == 42);
37
38 // return position
39 assert(through_return() == 42);
40
41 // every layer is peeled, not just one
42 let c: i64 := chained();
43 assert(c == 42);
44
45 // the reference itself is still available when that is what is asked for
46 let keep := shared();
47 assert(*keep == 42);
48}
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
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”
1// TYPECHECK_ERROR[T0024]
2// #649: RFC-0067a §3a's read-copy requires the referent to be `Copy` -- reading a
3// non-`Copy` value out of a shared reference at a `let` binding must be rejected,
4// not silently duplicated.
5struct NotCopy { v: String }
6
7fun main() {
8 let owned := NotCopy { v = "x" };
9 let r: &NotCopy := &owned;
10 let copy: NotCopy := r;
11}
typecheck errorT0024
List<T>
List<T> is the standard growable-sequence type. Use it when you need to append, pop, or otherwise mutate a sequence. Use T[] when the sequence is fixed after construction.
fun main() {
var xs: List<i64> := List::new();
xs.push(1);
xs.push(2);
xs.push(3);
println(xs.len().to_string()); // 3
let last := xs.pop(); // Some { value = 3 }
}
Construction:
| Form | Description |
|---|---|
List::new() | Empty list |
List::from(arr) | Construct from a T[] — copies elements |
Methods:
| Method | Signature | Description |
|---|---|---|
push | (&var self, value: T) | Append an element |
pop | (&var self) -> Perhaps<T> | Remove and return the last element, or None |
len | (&self) -> i64 | Number of elements |
get | (&self, index: i64) -> Perhaps<T> | Bounds-checked access |
as_slice | (&self) -> T[] | View as an immutable array (no copy) |
List<T> does not implicitly coerce to T[]. Call .as_slice() to get a read-only view.
as_slice returns a live borrowed view rather than a copied resultas_slice returns the same underlying storage, and the result remains a view for as long as
it is used, bounded by self's lifetime. "No copy" therefore describes the value's full
lifetime, not only the call itself.
Formal rules
Dynamic Semantics №1
List::new() creates an empty list, and List::from(source) creates a list containing
the elements of source.
Referenced by: rfc-0054
Tested by
1fun main() {
2 // to_string method
3 assert(0.to_string() == "0");
4 assert(42.to_string() == "42");
5 assert((-7).to_string() == "-7");
6 assert(1.5.to_string() == "1.5");
7 assert(0.0.to_string() == "0");
8 assert(true.to_string() == "true");
9 assert(false.to_string() == "false");
10 // String::len
11 assert("".len() == 0);
12 assert("hello".len() == 5);
13 assert("abc".len() == 3);
14 // string concatenation
15 assert("foo" + "bar" == "foobar");
16 assert("" + "xyz" == "xyz");
17 assert("abc" + "" == "abc");
18 assert("hello" + ", " + "world" == "hello, world");
19 let who := "world";
20 assert("hello, ${who}" == "hello, world");
21 assert("n=${42}" == "n=42");
22 assert("flag=${true}" == "flag=true");
23 assert("value=${\"x\"}" == "value=x");
24 assert("pair=${\"x\" + \"y\"}" == "pair=xy");
25 assert("\${value}" == "\${value}");
26 assert("$5" == "$5");
27 // List<T>: new, push, len, get, pop, as_slice, from
28 var lst: List<i64> := List::new();
29 assert((&lst).len() == 0);
30 lst.push(10);
31 lst.push(20);
32 lst.push(30);
33 assert((&lst).len() == 3);
34 // get returns Perhaps<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Perhaps::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Perhaps::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Perhaps::Some { value } => assert(value == 30),
46 None => assert(false),
47 };
48 assert((&lst).len() == 2);
49 // as_slice returns a T[] view
50 lst.push(99);
51 let sl := (&lst).as_slice();
52 assert(sl[0] == 10);
53 assert(sl[2] == 99);
54 // List::from copies an existing T[] array
55 let src: i64[] := [1, 2, 3, 4, 5];
56 let lst2 := List::from(src);
57 assert(lst2.len() == 5);
58 // Building a list with a loop then converting to T[]
59 var built: List<i64> := List::new();
60 var i := 1;
61 while (i <= 5) {
62 built.push(i * i);
63 i += 1;
64 }
65 assert((&built).len() == 5);
66 let built_arr := (&built).as_slice();
67 assert(built_arr[0] == 1);
68 assert(built_arr[4] == 25);
69 // get at boundary indices
70 var boundary: List<i64> := List::new();
71 boundary.push(100);
72 boundary.push(200);
73 boundary.push(300);
74 match ((&boundary).get(0)) {
75 Perhaps::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Perhaps::Some { value } => assert(value == 300),
80 None => assert(false),
81 };
82 // pop on empty list returns None
83 var empty_lst: List<i64> := List::new();
84 match (empty_lst.pop()) {
85 Perhaps::Some { value } => assert(false),
86 None => assert(true),
87 };
88 // pop until empty, verifying each value
89 var drain: List<i64> := List::new();
90 drain.push(7);
91 drain.push(8);
92 drain.push(9);
93 match (drain.pop()) {
94 Perhaps::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Perhaps::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Perhaps::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Perhaps::Some { value } => assert(false),
107 None => assert(true),
108 };
109 assert(drain.len() == 0);
110 // push after pop
111 var reuse: List<i64> := List::new();
112 reuse.push(1);
113 reuse.push(2);
114 reuse.pop();
115 reuse.push(99);
116 assert((&reuse).len() == 2);
117 match ((&reuse).get(1)) {
118 Perhaps::Some { value } => assert(value == 99),
119 None => assert(false),
120 };
121 // List::from on empty array
122 let empty_src: i64[] := [];
123 let lst_from_empty := List::from(empty_src);
124 assert(lst_from_empty.len() == 0);
125 // as_slice on empty list produces empty array
126 let empty_slice := (&empty_lst).as_slice();
127 assert(empty_slice.len() == 0);
128 // List<String>
129 var words: List<String> := List::new();
130 words.push("hello");
131 words.push("world");
132 assert((&words).len() == 2);
133 match ((&words).get(0)) {
134 Perhaps::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Perhaps::Some { value } => assert(value == "world"),
139 None => assert(false),
140 };
141 assert((&words).len() == 1);
142 // List<f64>
143 var floats: List<f64> := List::new();
144 floats.push(1.5);
145 floats.push(2.5);
146 floats.push(3.5);
147 assert((&floats).len() == 3);
148 match ((&floats).get(1)) {
149 Perhaps::Some { value } => assert(value == 2.5),
150 None => assert(false),
151 };
152 // List<boolean>
153 var flags: List<boolean> := List::new();
154 flags.push(true);
155 flags.push(false);
156 flags.push(true);
157 assert((&flags).len() == 3);
158 match ((&flags).get(2)) {
159 Perhaps::Some { value } => assert(value == true),
160 None => assert(false),
161 };
162 // for-in over as_slice result
163 var sum_lst: List<i64> := List::new();
164 sum_lst.push(10);
165 sum_lst.push(20);
166 sum_lst.push(30);
167 var total := 0;
168 for (x in sum_lst.as_slice()) {
169 total += x;
170 }
171 assert(total == 60);
172}
passes
Dynamic Semantics №2
push appends an element; pop removes and returns the last element, or None for an
empty list.
Referenced by: rfc-0054
Tested by
1fun main() {
2 // to_string method
3 assert(0.to_string() == "0");
4 assert(42.to_string() == "42");
5 assert((-7).to_string() == "-7");
6 assert(1.5.to_string() == "1.5");
7 assert(0.0.to_string() == "0");
8 assert(true.to_string() == "true");
9 assert(false.to_string() == "false");
10 // String::len
11 assert("".len() == 0);
12 assert("hello".len() == 5);
13 assert("abc".len() == 3);
14 // string concatenation
15 assert("foo" + "bar" == "foobar");
16 assert("" + "xyz" == "xyz");
17 assert("abc" + "" == "abc");
18 assert("hello" + ", " + "world" == "hello, world");
19 let who := "world";
20 assert("hello, ${who}" == "hello, world");
21 assert("n=${42}" == "n=42");
22 assert("flag=${true}" == "flag=true");
23 assert("value=${\"x\"}" == "value=x");
24 assert("pair=${\"x\" + \"y\"}" == "pair=xy");
25 assert("\${value}" == "\${value}");
26 assert("$5" == "$5");
27 // List<T>: new, push, len, get, pop, as_slice, from
28 var lst: List<i64> := List::new();
29 assert((&lst).len() == 0);
30 lst.push(10);
31 lst.push(20);
32 lst.push(30);
33 assert((&lst).len() == 3);
34 // get returns Perhaps<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Perhaps::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Perhaps::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Perhaps::Some { value } => assert(value == 30),
46 None => assert(false),
47 };
48 assert((&lst).len() == 2);
49 // as_slice returns a T[] view
50 lst.push(99);
51 let sl := (&lst).as_slice();
52 assert(sl[0] == 10);
53 assert(sl[2] == 99);
54 // List::from copies an existing T[] array
55 let src: i64[] := [1, 2, 3, 4, 5];
56 let lst2 := List::from(src);
57 assert(lst2.len() == 5);
58 // Building a list with a loop then converting to T[]
59 var built: List<i64> := List::new();
60 var i := 1;
61 while (i <= 5) {
62 built.push(i * i);
63 i += 1;
64 }
65 assert((&built).len() == 5);
66 let built_arr := (&built).as_slice();
67 assert(built_arr[0] == 1);
68 assert(built_arr[4] == 25);
69 // get at boundary indices
70 var boundary: List<i64> := List::new();
71 boundary.push(100);
72 boundary.push(200);
73 boundary.push(300);
74 match ((&boundary).get(0)) {
75 Perhaps::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Perhaps::Some { value } => assert(value == 300),
80 None => assert(false),
81 };
82 // pop on empty list returns None
83 var empty_lst: List<i64> := List::new();
84 match (empty_lst.pop()) {
85 Perhaps::Some { value } => assert(false),
86 None => assert(true),
87 };
88 // pop until empty, verifying each value
89 var drain: List<i64> := List::new();
90 drain.push(7);
91 drain.push(8);
92 drain.push(9);
93 match (drain.pop()) {
94 Perhaps::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Perhaps::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Perhaps::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Perhaps::Some { value } => assert(false),
107 None => assert(true),
108 };
109 assert(drain.len() == 0);
110 // push after pop
111 var reuse: List<i64> := List::new();
112 reuse.push(1);
113 reuse.push(2);
114 reuse.pop();
115 reuse.push(99);
116 assert((&reuse).len() == 2);
117 match ((&reuse).get(1)) {
118 Perhaps::Some { value } => assert(value == 99),
119 None => assert(false),
120 };
121 // List::from on empty array
122 let empty_src: i64[] := [];
123 let lst_from_empty := List::from(empty_src);
124 assert(lst_from_empty.len() == 0);
125 // as_slice on empty list produces empty array
126 let empty_slice := (&empty_lst).as_slice();
127 assert(empty_slice.len() == 0);
128 // List<String>
129 var words: List<String> := List::new();
130 words.push("hello");
131 words.push("world");
132 assert((&words).len() == 2);
133 match ((&words).get(0)) {
134 Perhaps::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Perhaps::Some { value } => assert(value == "world"),
139 None => assert(false),
140 };
141 assert((&words).len() == 1);
142 // List<f64>
143 var floats: List<f64> := List::new();
144 floats.push(1.5);
145 floats.push(2.5);
146 floats.push(3.5);
147 assert((&floats).len() == 3);
148 match ((&floats).get(1)) {
149 Perhaps::Some { value } => assert(value == 2.5),
150 None => assert(false),
151 };
152 // List<boolean>
153 var flags: List<boolean> := List::new();
154 flags.push(true);
155 flags.push(false);
156 flags.push(true);
157 assert((&flags).len() == 3);
158 match ((&flags).get(2)) {
159 Perhaps::Some { value } => assert(value == true),
160 None => assert(false),
161 };
162 // for-in over as_slice result
163 var sum_lst: List<i64> := List::new();
164 sum_lst.push(10);
165 sum_lst.push(20);
166 sum_lst.push(30);
167 var total := 0;
168 for (x in sum_lst.as_slice()) {
169 total += x;
170 }
171 assert(total == 60);
172}
passes
Dynamic Semantics №3
len reports the list's current number of elements, including changes made by push
and pop.
Referenced by: rfc-0054
Tested by
1fun main() {
2 // to_string method
3 assert(0.to_string() == "0");
4 assert(42.to_string() == "42");
5 assert((-7).to_string() == "-7");
6 assert(1.5.to_string() == "1.5");
7 assert(0.0.to_string() == "0");
8 assert(true.to_string() == "true");
9 assert(false.to_string() == "false");
10 // String::len
11 assert("".len() == 0);
12 assert("hello".len() == 5);
13 assert("abc".len() == 3);
14 // string concatenation
15 assert("foo" + "bar" == "foobar");
16 assert("" + "xyz" == "xyz");
17 assert("abc" + "" == "abc");
18 assert("hello" + ", " + "world" == "hello, world");
19 let who := "world";
20 assert("hello, ${who}" == "hello, world");
21 assert("n=${42}" == "n=42");
22 assert("flag=${true}" == "flag=true");
23 assert("value=${\"x\"}" == "value=x");
24 assert("pair=${\"x\" + \"y\"}" == "pair=xy");
25 assert("\${value}" == "\${value}");
26 assert("$5" == "$5");
27 // List<T>: new, push, len, get, pop, as_slice, from
28 var lst: List<i64> := List::new();
29 assert((&lst).len() == 0);
30 lst.push(10);
31 lst.push(20);
32 lst.push(30);
33 assert((&lst).len() == 3);
34 // get returns Perhaps<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Perhaps::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Perhaps::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Perhaps::Some { value } => assert(value == 30),
46 None => assert(false),
47 };
48 assert((&lst).len() == 2);
49 // as_slice returns a T[] view
50 lst.push(99);
51 let sl := (&lst).as_slice();
52 assert(sl[0] == 10);
53 assert(sl[2] == 99);
54 // List::from copies an existing T[] array
55 let src: i64[] := [1, 2, 3, 4, 5];
56 let lst2 := List::from(src);
57 assert(lst2.len() == 5);
58 // Building a list with a loop then converting to T[]
59 var built: List<i64> := List::new();
60 var i := 1;
61 while (i <= 5) {
62 built.push(i * i);
63 i += 1;
64 }
65 assert((&built).len() == 5);
66 let built_arr := (&built).as_slice();
67 assert(built_arr[0] == 1);
68 assert(built_arr[4] == 25);
69 // get at boundary indices
70 var boundary: List<i64> := List::new();
71 boundary.push(100);
72 boundary.push(200);
73 boundary.push(300);
74 match ((&boundary).get(0)) {
75 Perhaps::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Perhaps::Some { value } => assert(value == 300),
80 None => assert(false),
81 };
82 // pop on empty list returns None
83 var empty_lst: List<i64> := List::new();
84 match (empty_lst.pop()) {
85 Perhaps::Some { value } => assert(false),
86 None => assert(true),
87 };
88 // pop until empty, verifying each value
89 var drain: List<i64> := List::new();
90 drain.push(7);
91 drain.push(8);
92 drain.push(9);
93 match (drain.pop()) {
94 Perhaps::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Perhaps::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Perhaps::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Perhaps::Some { value } => assert(false),
107 None => assert(true),
108 };
109 assert(drain.len() == 0);
110 // push after pop
111 var reuse: List<i64> := List::new();
112 reuse.push(1);
113 reuse.push(2);
114 reuse.pop();
115 reuse.push(99);
116 assert((&reuse).len() == 2);
117 match ((&reuse).get(1)) {
118 Perhaps::Some { value } => assert(value == 99),
119 None => assert(false),
120 };
121 // List::from on empty array
122 let empty_src: i64[] := [];
123 let lst_from_empty := List::from(empty_src);
124 assert(lst_from_empty.len() == 0);
125 // as_slice on empty list produces empty array
126 let empty_slice := (&empty_lst).as_slice();
127 assert(empty_slice.len() == 0);
128 // List<String>
129 var words: List<String> := List::new();
130 words.push("hello");
131 words.push("world");
132 assert((&words).len() == 2);
133 match ((&words).get(0)) {
134 Perhaps::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Perhaps::Some { value } => assert(value == "world"),
139 None => assert(false),
140 };
141 assert((&words).len() == 1);
142 // List<f64>
143 var floats: List<f64> := List::new();
144 floats.push(1.5);
145 floats.push(2.5);
146 floats.push(3.5);
147 assert((&floats).len() == 3);
148 match ((&floats).get(1)) {
149 Perhaps::Some { value } => assert(value == 2.5),
150 None => assert(false),
151 };
152 // List<boolean>
153 var flags: List<boolean> := List::new();
154 flags.push(true);
155 flags.push(false);
156 flags.push(true);
157 assert((&flags).len() == 3);
158 match ((&flags).get(2)) {
159 Perhaps::Some { value } => assert(value == true),
160 None => assert(false),
161 };
162 // for-in over as_slice result
163 var sum_lst: List<i64> := List::new();
164 sum_lst.push(10);
165 sum_lst.push(20);
166 sum_lst.push(30);
167 var total := 0;
168 for (x in sum_lst.as_slice()) {
169 total += x;
170 }
171 assert(total == 60);
172}
passes
Dynamic Semantics №4
get(i) returns Some for an in-bounds element and None when i is out of bounds.
Referenced by: rfc-0054
Tested by
1fun main() {
2 // to_string method
3 assert(0.to_string() == "0");
4 assert(42.to_string() == "42");
5 assert((-7).to_string() == "-7");
6 assert(1.5.to_string() == "1.5");
7 assert(0.0.to_string() == "0");
8 assert(true.to_string() == "true");
9 assert(false.to_string() == "false");
10 // String::len
11 assert("".len() == 0);
12 assert("hello".len() == 5);
13 assert("abc".len() == 3);
14 // string concatenation
15 assert("foo" + "bar" == "foobar");
16 assert("" + "xyz" == "xyz");
17 assert("abc" + "" == "abc");
18 assert("hello" + ", " + "world" == "hello, world");
19 let who := "world";
20 assert("hello, ${who}" == "hello, world");
21 assert("n=${42}" == "n=42");
22 assert("flag=${true}" == "flag=true");
23 assert("value=${\"x\"}" == "value=x");
24 assert("pair=${\"x\" + \"y\"}" == "pair=xy");
25 assert("\${value}" == "\${value}");
26 assert("$5" == "$5");
27 // List<T>: new, push, len, get, pop, as_slice, from
28 var lst: List<i64> := List::new();
29 assert((&lst).len() == 0);
30 lst.push(10);
31 lst.push(20);
32 lst.push(30);
33 assert((&lst).len() == 3);
34 // get returns Perhaps<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Perhaps::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Perhaps::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Perhaps::Some { value } => assert(value == 30),
46 None => assert(false),
47 };
48 assert((&lst).len() == 2);
49 // as_slice returns a T[] view
50 lst.push(99);
51 let sl := (&lst).as_slice();
52 assert(sl[0] == 10);
53 assert(sl[2] == 99);
54 // List::from copies an existing T[] array
55 let src: i64[] := [1, 2, 3, 4, 5];
56 let lst2 := List::from(src);
57 assert(lst2.len() == 5);
58 // Building a list with a loop then converting to T[]
59 var built: List<i64> := List::new();
60 var i := 1;
61 while (i <= 5) {
62 built.push(i * i);
63 i += 1;
64 }
65 assert((&built).len() == 5);
66 let built_arr := (&built).as_slice();
67 assert(built_arr[0] == 1);
68 assert(built_arr[4] == 25);
69 // get at boundary indices
70 var boundary: List<i64> := List::new();
71 boundary.push(100);
72 boundary.push(200);
73 boundary.push(300);
74 match ((&boundary).get(0)) {
75 Perhaps::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Perhaps::Some { value } => assert(value == 300),
80 None => assert(false),
81 };
82 // pop on empty list returns None
83 var empty_lst: List<i64> := List::new();
84 match (empty_lst.pop()) {
85 Perhaps::Some { value } => assert(false),
86 None => assert(true),
87 };
88 // pop until empty, verifying each value
89 var drain: List<i64> := List::new();
90 drain.push(7);
91 drain.push(8);
92 drain.push(9);
93 match (drain.pop()) {
94 Perhaps::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Perhaps::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Perhaps::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Perhaps::Some { value } => assert(false),
107 None => assert(true),
108 };
109 assert(drain.len() == 0);
110 // push after pop
111 var reuse: List<i64> := List::new();
112 reuse.push(1);
113 reuse.push(2);
114 reuse.pop();
115 reuse.push(99);
116 assert((&reuse).len() == 2);
117 match ((&reuse).get(1)) {
118 Perhaps::Some { value } => assert(value == 99),
119 None => assert(false),
120 };
121 // List::from on empty array
122 let empty_src: i64[] := [];
123 let lst_from_empty := List::from(empty_src);
124 assert(lst_from_empty.len() == 0);
125 // as_slice on empty list produces empty array
126 let empty_slice := (&empty_lst).as_slice();
127 assert(empty_slice.len() == 0);
128 // List<String>
129 var words: List<String> := List::new();
130 words.push("hello");
131 words.push("world");
132 assert((&words).len() == 2);
133 match ((&words).get(0)) {
134 Perhaps::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Perhaps::Some { value } => assert(value == "world"),
139 None => assert(false),
140 };
141 assert((&words).len() == 1);
142 // List<f64>
143 var floats: List<f64> := List::new();
144 floats.push(1.5);
145 floats.push(2.5);
146 floats.push(3.5);
147 assert((&floats).len() == 3);
148 match ((&floats).get(1)) {
149 Perhaps::Some { value } => assert(value == 2.5),
150 None => assert(false),
151 };
152 // List<boolean>
153 var flags: List<boolean> := List::new();
154 flags.push(true);
155 flags.push(false);
156 flags.push(true);
157 assert((&flags).len() == 3);
158 match ((&flags).get(2)) {
159 Perhaps::Some { value } => assert(value == true),
160 None => assert(false),
161 };
162 // for-in over as_slice result
163 var sum_lst: List<i64> := List::new();
164 sum_lst.push(10);
165 sum_lst.push(20);
166 sum_lst.push(30);
167 var total := 0;
168 for (x in sum_lst.as_slice()) {
169 total += x;
170 }
171 assert(total == 60);
172}
passes
Legality Rule №1
A List<T> is distinct from T[]; obtaining its array view requires an explicit
.as_slice() call.
Referenced by: rfc-0054
Tested by
1fun main() {
2 // to_string method
3 assert(0.to_string() == "0");
4 assert(42.to_string() == "42");
5 assert((-7).to_string() == "-7");
6 assert(1.5.to_string() == "1.5");
7 assert(0.0.to_string() == "0");
8 assert(true.to_string() == "true");
9 assert(false.to_string() == "false");
10 // String::len
11 assert("".len() == 0);
12 assert("hello".len() == 5);
13 assert("abc".len() == 3);
14 // string concatenation
15 assert("foo" + "bar" == "foobar");
16 assert("" + "xyz" == "xyz");
17 assert("abc" + "" == "abc");
18 assert("hello" + ", " + "world" == "hello, world");
19 let who := "world";
20 assert("hello, ${who}" == "hello, world");
21 assert("n=${42}" == "n=42");
22 assert("flag=${true}" == "flag=true");
23 assert("value=${\"x\"}" == "value=x");
24 assert("pair=${\"x\" + \"y\"}" == "pair=xy");
25 assert("\${value}" == "\${value}");
26 assert("$5" == "$5");
27 // List<T>: new, push, len, get, pop, as_slice, from
28 var lst: List<i64> := List::new();
29 assert((&lst).len() == 0);
30 lst.push(10);
31 lst.push(20);
32 lst.push(30);
33 assert((&lst).len() == 3);
34 // get returns Perhaps<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Perhaps::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Perhaps::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Perhaps::Some { value } => assert(value == 30),
46 None => assert(false),
47 };
48 assert((&lst).len() == 2);
49 // as_slice returns a T[] view
50 lst.push(99);
51 let sl := (&lst).as_slice();
52 assert(sl[0] == 10);
53 assert(sl[2] == 99);
54 // List::from copies an existing T[] array
55 let src: i64[] := [1, 2, 3, 4, 5];
56 let lst2 := List::from(src);
57 assert(lst2.len() == 5);
58 // Building a list with a loop then converting to T[]
59 var built: List<i64> := List::new();
60 var i := 1;
61 while (i <= 5) {
62 built.push(i * i);
63 i += 1;
64 }
65 assert((&built).len() == 5);
66 let built_arr := (&built).as_slice();
67 assert(built_arr[0] == 1);
68 assert(built_arr[4] == 25);
69 // get at boundary indices
70 var boundary: List<i64> := List::new();
71 boundary.push(100);
72 boundary.push(200);
73 boundary.push(300);
74 match ((&boundary).get(0)) {
75 Perhaps::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Perhaps::Some { value } => assert(value == 300),
80 None => assert(false),
81 };
82 // pop on empty list returns None
83 var empty_lst: List<i64> := List::new();
84 match (empty_lst.pop()) {
85 Perhaps::Some { value } => assert(false),
86 None => assert(true),
87 };
88 // pop until empty, verifying each value
89 var drain: List<i64> := List::new();
90 drain.push(7);
91 drain.push(8);
92 drain.push(9);
93 match (drain.pop()) {
94 Perhaps::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Perhaps::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Perhaps::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Perhaps::Some { value } => assert(false),
107 None => assert(true),
108 };
109 assert(drain.len() == 0);
110 // push after pop
111 var reuse: List<i64> := List::new();
112 reuse.push(1);
113 reuse.push(2);
114 reuse.pop();
115 reuse.push(99);
116 assert((&reuse).len() == 2);
117 match ((&reuse).get(1)) {
118 Perhaps::Some { value } => assert(value == 99),
119 None => assert(false),
120 };
121 // List::from on empty array
122 let empty_src: i64[] := [];
123 let lst_from_empty := List::from(empty_src);
124 assert(lst_from_empty.len() == 0);
125 // as_slice on empty list produces empty array
126 let empty_slice := (&empty_lst).as_slice();
127 assert(empty_slice.len() == 0);
128 // List<String>
129 var words: List<String> := List::new();
130 words.push("hello");
131 words.push("world");
132 assert((&words).len() == 2);
133 match ((&words).get(0)) {
134 Perhaps::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Perhaps::Some { value } => assert(value == "world"),
139 None => assert(false),
140 };
141 assert((&words).len() == 1);
142 // List<f64>
143 var floats: List<f64> := List::new();
144 floats.push(1.5);
145 floats.push(2.5);
146 floats.push(3.5);
147 assert((&floats).len() == 3);
148 match ((&floats).get(1)) {
149 Perhaps::Some { value } => assert(value == 2.5),
150 None => assert(false),
151 };
152 // List<boolean>
153 var flags: List<boolean> := List::new();
154 flags.push(true);
155 flags.push(false);
156 flags.push(true);
157 assert((&flags).len() == 3);
158 match ((&flags).get(2)) {
159 Perhaps::Some { value } => assert(value == true),
160 None => assert(false),
161 };
162 // for-in over as_slice result
163 var sum_lst: List<i64> := List::new();
164 sum_lst.push(10);
165 sum_lst.push(20);
166 sum_lst.push(30);
167 var total := 0;
168 for (x in sum_lst.as_slice()) {
169 total += x;
170 }
171 assert(total == 60);
172}
passes
Type Ascription
The : operator asserts that an expression has a given type without performing any runtime conversionL1. It is a pure type-inference hint — no code is emitted at runtime.
Type ascription is mainly an ergonomics feature. Most code should type-check from
its surrounding context alone; : is for the cases where spelling out the intended
type inline is clearer than introducing a separate annotated binding.
fun main() -> i64 {
let xs := [] : i64[];
let x := 1 : i64;
if (xs.len() == 0) { x } else { 0 }
}
Ascription fails at compile time if the inferred type of the sub-expression cannot be unified with the ascribed typeL2. For example, 1 : String is invalid. Use as to convert between types; use : only when the value already has the target type.
fun main() -> i64 {
let y := 1 : String;
return 0;
}
Formal rules
Legality Rule №1
expr : T constrains expr to type T and supplies T as its expected type; it performs
no runtime conversion.
Referenced by: rfc-0021, rfc-0023
Tested by (2)
1// Stage 8: type ascription operator `:`.
2
3// Empty array element type resolved via ascription
4let n: i64[] := [] : i64[];
5
6// Identity ascription on a literal
7let x: i64 := 1 : i64;
8
9// Ascription on a variable reference
10fun check_var() {
11 let v: boolean := true;
12 let w: boolean := v : boolean;
13}
14
15// Ascription in argument position
16fun take_ints(arr: i64[]) -> i64 { arr.len() }
17let _: i64 := take_ints([] : i64[]);
18
19// Ascription disambiguates two empty-array arguments
20fun two_arrays(a: i64[], b: f64[]) -> i64 { a.len() }
21let _: i64 := two_arrays([] : i64[], [] : f64[]);
22
23// Ascription on a struct literal
24struct Point { x: i64, y: i64 }
25fun check_struct() {
26 let p: Point := Point { x = 1, y = 2 } : Point;
27}
28
29// Ascription on a tuple
30fun check_tuple() {
31 let t: (i64, boolean) := (1, true) : (i64, boolean);
32}
33
34// Ascription as the tail expression of a function body
35fun returns_ascribed() -> i64 {
36 42 : i64
37}
38
39// Ascription resolves the type of an unannotated let binding
40fun check_inferred() {
41 let arr := [] : i64[];
42 let _: i64 := arr.len();
43}
44
45// Ascription inside a binary expression operand
46fun check_binop() {
47 let _: boolean := (1 : i64) == 1;
48}
49
50// as conversion still works alongside ascription
51let f: f64 := 1 as f64;
passes
1// Turbofish disambiguates T, which appears only in the return position;
2// ascription disambiguates the otherwise-ambiguous `None` argument, since a
3// generic-scheme callee's own parameter types aren't available as hints for
4// its arguments at all (unlike a concrete, non-generic callee) -- both
5// mechanisms are doing real, independent work in the same call.
6fun make<T>(fallback: Perhaps<i64>) -> T {
7 return make(fallback);
8}
9
10fun main() {
11 make::<i64>(None : Perhaps<i64>);
12}
passes
Legality Rule №2
An ascription is valid only when the expression's type unifies with the ascribed type; otherwise it is a type error.
Referenced by: rfc-0021
Tested by
1// Stage 8 negative: ascription with incompatible type is a type error.
2// `1 : f64` is an error — use `1 as f64` to convert.
3
4let z: f64 := 1 : f64; // ERROR[T0001]
typecheck errorT0001at 4
Legality Rule №3
An expression may contain at most one type ascription; a second : in the same ascription
position is a parse error.
Referenced by: rfc-0021
Tested by
1// RFC-0021 §4: an expression accepts at most one type ascription.
2let value := 1 : i64 : i64;
parse errorP0001
When ascription helps
Type inference uses surrounding expected types. That expected type can come from a let annotation, a function return type, a callee's parameter types, or the surrounding expression context.
Because of that, ambiguous literals like [] and None often type-check without explicit ascription when the context already determines their type:
fun zip_lengths(a: i64[], b: String[]) -> i64 {
return a.len() + b.len();
}
fun make_row(use_default: boolean, fallback: i64[]) -> i64[] {
return match (use_default) {
true => [],
false => fallback,
};
}
fun first_or_default(items: i64[], fallback: Perhaps<i64>) -> i64 {
return match (fallback) {
Some { value } => value,
None => if (items.len() > 0) { items[0] } else { 0 },
};
}
fun main() -> i64 {
let total := zip_lengths([], ["a", "b"]);
let row := make_row(true, [1, 2, 3]);
let first := first_or_default([1, 2, 3], None);
return total + row.len() + first;
}
Ascription is still useful when no surrounding context fixes the type:
fun main() -> i64 {
let arr := [] : i64[];
let value := None : Perhaps<i64>;
match (value) {
Some { value } => value + arr.len(),
None => arr.len(),
}
}
Without such context, ambiguous literals remain a type error. For example, let x = None; does not provide enough information to infer the element type.
fun main() -> i64 {
let x := None;
return 0;
}
Type Casting
The as operator performs an explicit conversion from expr's type to TD1. It desugars to a call to the From aspect and is infallible — the result is the target type directly.
fun main() {
let x: i32 := 1000i32;
let b: i8 := x as i8; // wraps: 1000 mod 256 → -24
let f: f32 := x as f32; // 1000.0f32
let u: u64 := x as u64; // 1000u64
let pi: f64 := 3.14;
let n: i32 := pi as i32; // truncates toward zero → 3
}
All pairwise casts among i8, i16, i32, i64, u8, u16, u32, u64, f32, f64 are supported. Narrowing integer casts wrap (two's-complement truncation). f64-to-integer casts truncate toward zero.
Because as desugars to From, user-defined types become castable by implementing From<SourceType> for the target type.
Formal rules
Dynamic Semantics №1
expr as T evaluates an explicit conversion of expr to T via From<S>::from (where
S is expr's type) and produces a value of type T. Not restricted to numeric types —
any type with an applicable From<S> implementation is a valid cast target.
Referenced by: rfc-0021
Tested by
1// Stage 8: type ascription operator `:`.
2
3// Empty array element type resolved via ascription
4let n: i64[] := [] : i64[];
5
6// Identity ascription on a literal
7let x: i64 := 1 : i64;
8
9// Ascription on a variable reference
10fun check_var() {
11 let v: boolean := true;
12 let w: boolean := v : boolean;
13}
14
15// Ascription in argument position
16fun take_ints(arr: i64[]) -> i64 { arr.len() }
17let _: i64 := take_ints([] : i64[]);
18
19// Ascription disambiguates two empty-array arguments
20fun two_arrays(a: i64[], b: f64[]) -> i64 { a.len() }
21let _: i64 := two_arrays([] : i64[], [] : f64[]);
22
23// Ascription on a struct literal
24struct Point { x: i64, y: i64 }
25fun check_struct() {
26 let p: Point := Point { x = 1, y = 2 } : Point;
27}
28
29// Ascription on a tuple
30fun check_tuple() {
31 let t: (i64, boolean) := (1, true) : (i64, boolean);
32}
33
34// Ascription as the tail expression of a function body
35fun returns_ascribed() -> i64 {
36 42 : i64
37}
38
39// Ascription resolves the type of an unannotated let binding
40fun check_inferred() {
41 let arr := [] : i64[];
42 let _: i64 := arr.len();
43}
44
45// Ascription inside a binary expression operand
46fun check_binop() {
47 let _: boolean := (1 : i64) == 1;
48}
49
50// as conversion still works alongside ascription
51let f: f64 := 1 as f64;
passes
Generics
Perhaps<T>, Result<T, E>, T[])v0.1.0User-defined generic functions and typesv0.3.0Types and functions can be parameterized with <T> syntax.
struct Stack<T> {
items: T[],
}
fun first<T>(arr: T[]) -> Perhaps<T> {
if (arr.len() == 0) {
return None;
}
return Some { value = arr[0] };
}
fun main() -> i64 {
let stack := Stack { items = [1, 2, 3] };
match (first(stack.items)) {
Some { value } => value,
None => 0,
}
}
Row bounds
A bound written as a row accepts any type carrying at least the listed fields:
fun squared_magnitude<record T: { x: f64, y: f64, .. }>(p: T) -> f64 {
p.x * p.x + p.y * p.y
}
The trailing .. is load-bearing. It stands for "and a rest I am not naming," and its
presence is what makes the bound open:
fun g<record T: { x: f64 }>(p: T) // closed: T's row is exactly `x`
fun h<record T: { x: f64, .. }>(p: T) // open: T has at least `x`
A record pattern's own trailing .. reads the bound's listed fields the same way
field accessL6 does, and — unlike field access
— can discard the rest of an open bound's unlisted fields rather than being unable to name
them at all:
fun describe<record T: { x: f64, .. }>(p: T) -> String {
match (p) {
{ x, .. } => "x is ${x}, plus whatever else the caller passed",
}
}
The .. is required to match an open bound at all — its full field set isn't known
here, so a pattern that doesn't end in .. can never be exhaustive:
fun bad<record T: { x: f64, .. }>(p: T) -> f64 {
match (p) {
{ x } => x, // error: open bound's field set isn't known here; add `..`
}
}
A closed bound's fields are fully known, so .. there is optional sugar rather than a
requirement — a pattern matching a closed bound must still name every field the bound
lists unless it uses ..:
fun get_x<record T: { x: f64, y: f64 }>(p: T) -> f64 {
match (p) {
{ x, y } => x, // OK: every field of the closed bound is named
// { x } => x, // error: `y` isn't named and there's no `..`
}
}
Naming a field the bound doesn't list is still rejected, .. or not — the pattern's rest
form discards unnamed fields, not fields the bound never promised are there:
fun bad2<record T: { x: f64, .. }>(p: T) -> f64 {
match (p) {
{ x, z, .. } => x, // error: no field `z` on the bound
}
}
A field may omit its type to constrain the label only — { x } means "carries an x,
whatever its type":
fun f<record T: { x, .. }>(p: T) // has an `x` of some type
fun g<record T: { x, y: f64, .. }>(p: T) // any-typed `x`, `f64` `y`
Negation reuses the ! that bounds already accept, and is the complement of the positive
bound — just as !Copy means "does not implement Copy". It takes no .., since absence
has no rest to quantify over:
fun send<record T: !{ token }>(t: T) -> i64 { … } // carries no `token` at all
fun tag<record T: !{ id: String }>(t: T) -> i64 { … } // no `String`-typed `id`
Note the second form is satisfied by a record whose id is an i64 — it does not have a
String id. Write !{ id } for "no id of any type".
A row bound is satisfied by a record, not by a nominal struct. The record marker on the
type parameter says so at the declaration; a bare <T: { … }> is an error.
The marker may be written at the parameter or in a where clause — the two are equivalent,
and a parameter is record-kinded if either one carries it:
fun f<record T: { x: f64, .. }>(p: T) -> f64
fun g<T>(p: T) -> f64 where record T: { x: f64, .. }
The row bound is optional. <record T> on its own means "any record, whatever its
fields" — the only way to write that, since a bound of { .. } alone is not accepted:
fun labels<record T>(x: T) -> Symbol[] // any record; no constraint on its fields
squared_magnitude({ x = 3.0, y = 4.0 }); // a record — satisfies the bound
squared_magnitude(some_point); // a struct — does not
Nominal structs do not satisfy row bounds. Named records are planned, not implemented;
they would provide a nominal record kind. See public/rfcs/2-accepted/rfc-0120-named-records.md
(RFC-0120: Named Records) — a plain path mention rather than a link while rfcs/ is
excluded from the website (see metel-website's docusaurus.config.ts), so this doesn't
become a broken link once RFCs sync through.
Why row capability is opt-in
A nominal type's API is what it declares. An anonymous record's API is what it contains.
Once a type satisfies row bounds, its field names and types are part of its public interface,
whether the author intended that or not. Renaming a field breaks every caller who wrote a
bound mentioning it; adding one can make the type accidentally satisfy a bound its author
never heard of. On a struct, a field rename is an internal change.
That is why structural capability is opt-in rather than automatic:
| encapsulation | structural flexibility | |
|---|---|---|
struct | layout is private; the API is what you declare | none |
Most types want the first. A value whose shape is genuinely the contract — a coordinate pair or a configuration fragment — can use an anonymous record.
What satisfies which bound
Both bound kinds are opted into; they differ only in granularity. An aspect bound is
opted into per aspect, by writing an implementation. A row bound is opted into per type,
by choosing the record kind. Nothing is implicit in either direction.
struct's row is not visible to row-bound satisfactionEvery struct is represented internally as (brand, row) (see Ownership —
Narrowing§Narrowing). The table's "no" is a visibility gate, not the absence
of a row: a plain struct's row is never visible to row-bound satisfaction, regardless of
narrowing or projection — including at full width, where its content is identical to a
same-shaped record's. This preserves the same observable outcome as before, expressed on the
branded-row mechanism itself.
non-local aspect (Display) | local aspect | row bound | |
|---|---|---|---|
struct | yes, with an impl | yes, with an impl | no |
enum | yes, with an impl | yes, with an impl | no — sums, not products |
| anonymous record | no — see below | yes, with an impl | yes |
An anonymous record has no owning module, so the orphan rule permits an implementation only
for an aspect local to the implementing module. Every standard-library aspect is non-local,
which means no anonymous record is Display and println("${r}") does not work on one.
Auto-derived aspects are unaffected — Send and Sync are computed from field composition
rather than declared.
Implementing an aspect for a record
Three forms, with different rules:
extend { x: f64, y: f64 }: MyAspect { … } // one concrete row
extend<row R: { x: f64, .. }> { ..R }: MyAspect { … } // every row of a given shape
extend<row R> { ..R }: MyAspect { … } // every row
None of the three are available in v0.12.0 — this contradicted the "Not available in
v0.12.0" callout above until corrected here; confirmed directly, extend { x: f64, y: f64 }: MyAspect { … } still fails with the same "cannot extend an anonymous record type" rejection
tuples and records both hit. The first form is the one this design intends to land first —
exactly one structural type, permitted once the aspect is local — but it is not implemented
yet, unlike the equivalent one-concrete-target form for arrays (extend<T> T[]: Aspect,
already supported). The second and third additionally require row variables, which don't
exist at all yet. The second also needs overlap checking between row bounds — two
shape-conditional implementations can be incomparable rather than one being more specific,
so they must be disjoint. The third additionally needs a way to require an aspect of every
field in the row, which does not yet exist either.
Formal rules
Legality Rule №1
A row bound requires record on its type parameter, either at the parameter declaration
or in a where constraint; record without a row bound is also a legal any-record bound.
Referenced by: rfc-0118
Tested by (2)
1fun closed_ok<record T: { x: i64, y: i64 }>(_value: T) -> i64 { 1 }
2
3fun open_ok<record T: { x: i64, .. }>(_value: T) -> i64 { 2 }
4
5fun label_only_ok<record T: { token }>(_value: T) -> i64 { 3 }
6
7fun mixed_ok<record T: { token, y: i64, .. }>(_value: T) -> i64 { 4 }
8
9fun where_marker_ok<T>(_value: T) -> i64
10where record T: { x: i64, .. } {
11 5
12}
13
14fun any_record<record T>(_value: T) -> i64 { 6 }
15
16fun neg_typed_ok<record T: !{ x: f64 }>(_value: T) -> i64 { 7 }
17
18fun neg_label_ok<record T: !{ z }>(_value: T) -> i64 { 8 }
19
20fun main() {
21 assert(closed_ok({ x = 1, y = 2 }) == 1);
22 assert(open_ok({ x = 1, y = 2, extra = 3 }) == 2);
23 assert(label_only_ok({ token = "id" }) == 3);
24 assert(mixed_ok({ token = true, y = 9, extra = 1 }) == 4);
25 assert(where_marker_ok({ x = 1, extra = 2 }) == 5);
26 assert(any_record({ anything = 1 }) == 6);
27 assert(neg_typed_ok({ x = 1 }) == 7);
28 assert(neg_label_ok({ x = 1 }) == 8);
29}
passes
1// RFC-0118 §1: the `record` marker and a row bound may be written at the
2// generic parameter's own declaration AND separately in a `where` clause for
3// the same parameter; the two positions compose. Also exercises §2a (a bound
4// field, `x`, may omit its type) and §2 (a negative where-clause bound is
5// enforced, not just parsed).
6fun f<record T: { x, y: i64, .. }>(value: T) -> i64
7where record T: !{ z } {
8 value.y
9}
10
11fun main() {
12 let ok := { x = "hi", y = 5 };
13 let result := f(ok);
14 assert(result == 5);
15}
passes
Legality Rule №2
A negative row bound is satisfied only when none of its named fields match; it accepts no
trailing .. and a negative bound in a where clause is enforced like an inline one.
Referenced by: rfc-0118
Tested by (3)
1// RFC-0118 §2: a negative row bound written in a `where` clause is enforced
2// the same as one written inline -- a record carrying the forbidden label is
3// rejected, not silently accepted because the bound lives in a separate
4// clause from the parameter's own declaration.
5fun f<record T: { x, y: i64, .. }>(value: T) -> i64
6where record T: !{ z } {
7 value.y
8}
9
10fun main() {
11 let bad := { x = 1, y = 2, z = 3 };
12 let result := f(bad);
13}
typecheck errorT0012at 12:20“negative row bound `!{ z }`”
1// Negative (RFC-0118 §2): a negative row bound names labels that must be absent, so there
2// is no rest to quantify over and `..` is meaningless. Rejected rather than ignored.
3fun f<record T: !{ x, .. }>(v: T) -> i64 { 1 }
4
5fun main() { }
parse errorP0001“negative row bound takes no”
1// RFC-0118 §1: the `record` marker and a row bound may be written at the
2// generic parameter's own declaration AND separately in a `where` clause for
3// the same parameter; the two positions compose. Also exercises §2a (a bound
4// field, `x`, may omit its type) and §2 (a negative where-clause bound is
5// enforced, not just parsed).
6fun f<record T: { x, y: i64, .. }>(value: T) -> i64
7where record T: !{ z } {
8 value.y
9}
10
11fun main() {
12 let ok := { x = "hi", y = 5 };
13 let result := f(ok);
14 assert(result == 5);
15}
passes
Legality Rule №3
A field in a row bound may omit its type, constraining the field label while accepting any field type.
Referenced by: rfc-0118
Tested by (2)
1fun closed_ok<record T: { x: i64, y: i64 }>(_value: T) -> i64 { 1 }
2
3fun open_ok<record T: { x: i64, .. }>(_value: T) -> i64 { 2 }
4
5fun label_only_ok<record T: { token }>(_value: T) -> i64 { 3 }
6
7fun mixed_ok<record T: { token, y: i64, .. }>(_value: T) -> i64 { 4 }
8
9fun where_marker_ok<T>(_value: T) -> i64
10where record T: { x: i64, .. } {
11 5
12}
13
14fun any_record<record T>(_value: T) -> i64 { 6 }
15
16fun neg_typed_ok<record T: !{ x: f64 }>(_value: T) -> i64 { 7 }
17
18fun neg_label_ok<record T: !{ z }>(_value: T) -> i64 { 8 }
19
20fun main() {
21 assert(closed_ok({ x = 1, y = 2 }) == 1);
22 assert(open_ok({ x = 1, y = 2, extra = 3 }) == 2);
23 assert(label_only_ok({ token = "id" }) == 3);
24 assert(mixed_ok({ token = true, y = 9, extra = 1 }) == 4);
25 assert(where_marker_ok({ x = 1, extra = 2 }) == 5);
26 assert(any_record({ anything = 1 }) == 6);
27 assert(neg_typed_ok({ x = 1 }) == 7);
28 assert(neg_label_ok({ x = 1 }) == 8);
29}
passes
1// RFC-0118 §1: the `record` marker and a row bound may be written at the
2// generic parameter's own declaration AND separately in a `where` clause for
3// the same parameter; the two positions compose. Also exercises §2a (a bound
4// field, `x`, may omit its type) and §2 (a negative where-clause bound is
5// enforced, not just parsed).
6fun f<record T: { x, y: i64, .. }>(value: T) -> i64
7where record T: !{ z } {
8 value.y
9}
10
11fun main() {
12 let ok := { x = "hi", y = 5 };
13 let result := f(ok);
14 assert(result == 5);
15}
passes
Legality Rule №4
Only a record satisfies a row bound; a nominal struct is rejected even when it has matching fields.
Referenced by: rfc-0118
Tested by (2)
1// Regression (metel-core#857, RFC-0137 slice 1's own normalization rule, and
2// RFC-0137 sec3's worked example): a projection naming every field a struct
3// declares normalizes back to the plain struct type rather than staying a
4// distinct branded residual. Confirms the normalization doesn't accidentally
5// earn row-bound eligibility -- h.{ fd, name }, full width, is rejected by a row
6// bound the exact same way a bare `Handle` value already is.
7
8struct Handle { fd: i64, name: String }
9
10fun wants_a_record<record T: { fd: i64, name: String, .. }>(t: T) -> i64 { t.fd }
11
12fun main() {
13 let h := Handle { fd = 3, name = "x" };
14 let _ := wants_a_record(h.{ fd, name });
15}
typecheck errorT0012“struct never satisfies a row bound”
1struct Point {
2 x: i64,
3}
4
5fun need_record<record T: { x: i64, .. }>(_value: T) -> i64 { 0 }
6
7fun main() {
8 let _ := need_record(Point { x = 1 });
9}
typecheck errorT0012“struct never satisfies a row bound”
Legality Rule №5
Brace syntax after a parameter or let annotation denotes an exact record type, while the
same syntax in a generic parameter or where constraint denotes a row bound.
Referenced by: rfc-0118
Tested by
1// RFC-0118 §4: the same `{ ... }` brace syntax means an RFC-0116 closed record type
2// after `:` in a param/let annotation, and a row bound after `:` in a generic_param or
3// where_constraint -- distinguished by position alone, both in one program.
4
5fun takes_record(p: { x: i64, y: i64 }) -> i64 {
6 p.x + p.y
7}
8
9fun takes_row_bound<record T: { x: i64, .. }>(v: T) -> i64 {
10 v.x
11}
12
13fun main() {
14 let r := { x = 1, y = 2 };
15 assert(takes_record(r) == 3);
16
17 let wider := { x = 5, y = 6, z = 7 };
18 assert(takes_row_bound(wider) == 5);
19}
passes
Legality Rule №6
A field a row bound lists is accessible via field access (p.x) from inside the function
body; a field the bound doesn't list is not, even when a caller's concrete argument
happens to carry it.
Tested by
1// #645: dot-access to a field explicitly named in a row bound, through an abstract,
2// row-bounded generic type parameter — both closed and open bounds, typed and untyped
3// field forms, and writing through a mutable reference.
4
5fun get_x<record T: { x: f64 }>(p: T) -> f64 {
6 p.x
7}
8
9fun squared_magnitude<record T: { x: f64, y: f64, .. }>(p: T) -> f64 {
10 p.x * p.x + p.y * p.y
11}
12
13fun get_name<record T: { name, .. }>(p: T) -> i64 {
14 p.name
15}
16
17fun bump<record T: { count: i64, .. }>(p: &var T) {
18 p.count += 1;
19}
20
21fun main() {
22 assert(get_x({ x = 3.0 }) == 3.0);
23 assert(squared_magnitude({ x = 3.0, y = 4.0 }) == 25.0);
24 assert(squared_magnitude({ x = 3.0, y = 4.0, extra = "ignored" }) == 25.0);
25 assert(get_name({ name = 42, other = "hi" }) == 42);
26
27 var r := { count = 1 };
28 bump(&var r);
29 assert(r.count == 2);
30}
passes
Legality Rule №7
A record pattern's trailing .. binds only the fields it names against a row-bounded
type parameter and discards the rest, the same as it does against a named struct. It is
required to match an open bound at all, since the bound's full field set isn't known;
for a closed bound it is optional, but the pattern must otherwise name every field the
bound lists. Naming a field the bound doesn't list is rejected regardless of ...
Tested by (4)
1// #646: a record pattern's trailing `..` reads a row-bounded type parameter's listed
2// fields and discards the rest -- required for an open bound (whose full field set isn't
3// known here), optional sugar for a closed bound (whose fields are already exhaustively
4// listed by the bound itself).
5
6fun describe<record T: { x: f64, .. }>(p: T) -> f64 {
7 match (p) {
8 { x, .. } => x,
9 }
10}
11
12fun get_x_closed<record T: { x: f64 }>(p: T) -> f64 {
13 match (p) {
14 { x, .. } => x,
15 }
16}
17
18fun get_x_closed_no_rest<record T: { x: f64, y: f64 }>(p: T) -> f64 {
19 match (p) {
20 { x, y } => x + y,
21 }
22}
23
24fun main() {
25 assert(describe({ x = 3.0 }) == 3.0);
26 assert(describe({ x = 3.0, y = 4.0, label = "ignored" }) == 3.0);
27 assert(get_x_closed({ x = 5.0 }) == 5.0);
28 assert(get_x_closed_no_rest({ x = 1.0, y = 2.0 }) == 3.0);
29}
passes
1// #646: a closed row bound's fields are fully known, but a record pattern without `..`
2// must still name every one of them -- the same completeness rule an anonymous record or
3// named struct pattern already enforces.
4fun get_x<record T: { x: f64, y: f64 }>(p: T) -> f64 {
5 match (p) {
6 { x } => x,
7 }
8}
9
10fun main() {
11 println(get_x({ x = 1.0, y = 2.0 }));
12}
typecheck errorT0001at 6
1// #646: `..` discards fields the pattern doesn't name -- it doesn't let the pattern name
2// a field the bound never promised is there, even one a particular caller happens to pass.
3fun describe<record T: { x: f64, .. }>(p: T) -> f64 {
4 match (p) {
5 { x, z, .. } => x + z,
6 }
7}
8
9fun main() {
10 println(describe({ x = 1.0, z = 2.0 }));
11}
typecheck errorT0003at 5
1// #646: an open row bound's full field set isn't known here, so a record pattern that
2// doesn't end in `..` can never be exhaustive against it.
3fun describe<record T: { x: f64, .. }>(p: T) -> f64 {
4 match (p) {
5 { x } => x,
6 }
7}
8
9fun main() {
10 println(describe({ x = 1.0 }));
11}
typecheck errorT0001at 5
Never Type
! (Never) is the uninhabited bottom type — no value of type ! can ever be
constructed. A loop with no reachable break has type !:
fun main() -> i64 {
let result: i64 := loop { break 42; };
return result;
}
return <expr>, panic(<message>), loop { } with no reachable break, and break/continue used as value expressions in loop context all have type !. If any sub-expression has type !, that sub-expression diverges before the outer expression can produce a value, so the outer expression's type is unconstrained and any type is accepted in that position.
Subtyping and coercion
! is a subtype of every type — ! <: T for all T — so an expression of type ! coerces implicitly, with no cast, to any context expecting T. This is what makes the rule above sound: code after a diverging expression is unreachable, but still typechecks against whatever its context requires.
Match exhaustiveness
A match whose scrutinee has type ! needs no arms — an empty match is vacuously exhaustive, since no value of type ! can ever reach it:
fun unreachable_code(x: !) -> i64 {
match x { } // exhaustive — no arms needed
}
More generally, an enum variant whose payload type is ! is uninhabited — no value of that variant can ever be constructed — and a match may omit the arm for an uninhabited variant while remaining exhaustive:
enum Foo {
A { x: i64 },
B { y: ! },
}
fun handle(f: Foo) -> i64 {
match (f) {
Foo::A { x } => x,
// Foo::B omitted — exhaustive; B is uninhabited
}
}
Inhabited-singleton coercion
If an enum has exactly one inhabited variant (every other variant's payload is !) and that variant has exactly one field, a value of the enum type coerces implicitly to the field's type — the compiler inserts the destructuring, no explicit match required:
enum Wrapper<T> {
Present { value: T },
Absent { _: ! },
}
fun infallible() -> Wrapper<i64> { Wrapper::Present { value = 42 } }
fun main() -> i64 {
let x: i64 := infallible(); // implicit coercion via the inhabited-singleton rule
return x;
}
Result<T, !> satisfies this: Ok { value: T } is the one inhabited variant with one field, so a Result<T, !>-returning function's caller can use the result as a plain T with no match. Perhaps<!> does not satisfy it — None is inhabited but has zero fields — so Perhaps<!> never coerces implicitly to anything, though nothing prevents it from arising through generic instantiation.
! as a return type
A function annotated -> ! promises never to return; every control-flow path must end in a diverging expression, checked by the compiler:
fun abort(msg: String) -> ! {
panic(msg);
}
A -> ! function containing a reachable return is a type error.
Formal rules
Legality Rule №1
! is uninhabited: no terminating expression can construct a value of that type.
Referenced by: rfc-0078
Tested by
1// Positive: RFC-0078 §3.2 -- an enum variant whose payload is `!` is
2// uninhabited, so a match may omit its arm and still be exhaustive.
3
4enum Foo {
5 A { x: i64 },
6 B { y: ! },
7}
8
9fun handle(f: Foo) -> i64 {
10 match (f) {
11 Foo::A { x } => x,
12 // Foo::B omitted -- exhaustive; B is uninhabited.
13 }
14}
15
16fun main() -> i64 {
17 handle(Foo::A { x = 5 })
18}
passes
Legality Rule №2
! is a subtype of every type, and an expression of type ! implicitly coerces to any
expected type.
Referenced by: rfc-0078
Tested by
1// Positive: RFC-0078 §1.1/§2 -- `panic(msg)` has type `!`, which coerces
2// implicitly to any type wherever the surrounding context is otherwise
3// unconstrained.
4
5fun pick(cond: boolean) -> i64 {
6 if (cond) {
7 panic("nope")
8 } else {
9 5
10 }
11}
12
13fun main() -> i64 {
14 pick(false)
15}
passes
Legality Rule №3
Code made unreachable by a diverging expression remains typechecked in its surrounding type context.
Referenced by: rfc-0078
Tested by
1// Positive: RFC-0078 §3.2/§4.1 -- writing the arm for an uninhabited variant is
2// allowed (merely unreachable, not rejected); the compiler may warn but must
3// not error.
4
5fun use_result(r: Result<i64, !>) -> i64 {
6 match (r) {
7 Result::Ok { value } => value,
8 Result::Err { error } => panic("unreachable"),
9 }
10}
11
12fun main() -> i64 {
13 use_result(Result::Ok { value = 3 })
14}
passes
Dynamic Semantics №1
return, panic, a non-breaking loop, and value-position break or continue
diverge and have type !; an enclosing expression cannot produce a value after such a
subexpression diverges.
Referenced by: rfc-0078
Tested by (2)
1// RUNTIME_ERROR[boom]
2// RFC-0078: panic(msg) always panics (R0014) with the given message.
3fun main() {
4 panic("boom");
5}
runtime errorR0014“boom”
1// Positive: RFC-0078 §1.1/§2 -- `panic(msg)` has type `!`, which coerces
2// implicitly to any type wherever the surrounding context is otherwise
3// unconstrained.
4
5fun pick(cond: boolean) -> i64 {
6 if (cond) {
7 panic("nope")
8 } else {
9 5
10 }
11}
12
13fun main() -> i64 {
14 pick(false)
15}
passes
Legality Rule №4
Match exhaustiveness excludes impossible scrutinee values and uninhabited variants.
Referenced by: rfc-0078
Tested by (2)
1// Positive: RFC-0078 §3.2 -- an enum variant whose payload is `!` is
2// uninhabited, so a match may omit its arm and still be exhaustive.
3
4enum Foo {
5 A { x: i64 },
6 B { y: ! },
7}
8
9fun handle(f: Foo) -> i64 {
10 match (f) {
11 Foo::A { x } => x,
12 // Foo::B omitted -- exhaustive; B is uninhabited.
13 }
14}
15
16fun main() -> i64 {
17 handle(Foo::A { x = 5 })
18}
passes
1// RFC-0078 §3.2: matching only the inhabited variant of an enum with a
2// `!`-payload variant, exercised end-to-end to confirm dispatch and value
3// extraction work correctly at runtime, not just at typecheck time.
4
5enum Foo {
6 A { x: i64 },
7 B { y: ! },
8}
9
10fun handle(f: Foo) -> i64 {
11 match (f) {
12 Foo::A { x } => x,
13 }
14}
15
16fun main() {
17 assert(handle(Foo::A { x = 5 }) == 5);
18}
passes
Legality Rule №5
A match whose scrutinee has type ! is exhaustive with no arms.
Referenced by: rfc-0078
Tested by
1// RFC-0078 §3.2: matching only the inhabited variant of an enum with a
2// `!`-payload variant, exercised end-to-end to confirm dispatch and value
3// extraction work correctly at runtime, not just at typecheck time.
4
5enum Foo {
6 A { x: i64 },
7 B { y: ! },
8}
9
10fun handle(f: Foo) -> i64 {
11 match (f) {
12 Foo::A { x } => x,
13 }
14}
15
16fun main() {
17 assert(handle(Foo::A { x = 5 }) == 5);
18}
passes
Legality Rule №6
An enum variant containing a ! payload is uninhabited; its match arm may be omitted or,
if written, is unreachable but not rejected.
Referenced by: rfc-0078
Tested by (3)
1// Positive: RFC-0078 §3.2 -- an enum variant whose payload is `!` is
2// uninhabited, so a match may omit its arm and still be exhaustive.
3
4enum Foo {
5 A { x: i64 },
6 B { y: ! },
7}
8
9fun handle(f: Foo) -> i64 {
10 match (f) {
11 Foo::A { x } => x,
12 // Foo::B omitted -- exhaustive; B is uninhabited.
13 }
14}
15
16fun main() -> i64 {
17 handle(Foo::A { x = 5 })
18}
passes
1// Positive: RFC-0078 §3.2/§4.1 -- writing the arm for an uninhabited variant is
2// allowed (merely unreachable, not rejected); the compiler may warn but must
3// not error.
4
5fun use_result(r: Result<i64, !>) -> i64 {
6 match (r) {
7 Result::Ok { value } => value,
8 Result::Err { error } => panic("unreachable"),
9 }
10}
11
12fun main() -> i64 {
13 use_result(Result::Ok { value = 3 })
14}
passes
1// RFC-0078 §3.2: matching only the inhabited variant of an enum with a
2// `!`-payload variant, exercised end-to-end to confirm dispatch and value
3// extraction work correctly at runtime, not just at typecheck time.
4
5enum Foo {
6 A { x: i64 },
7 B { y: ! },
8}
9
10fun handle(f: Foo) -> i64 {
11 match (f) {
12 Foo::A { x } => x,
13 }
14}
15
16fun main() {
17 assert(handle(Foo::A { x = 5 }) == 5);
18}
passes
Legality Rule №7
An enum with exactly one inhabited, single-field variant implicitly coerces to that field's type; zero-field or multi-field inhabited variants do not receive this coercion.
Referenced by: rfc-0078
Tested by
1// RFC-0078 §3.3: inhabited-singleton coercion, exercised end-to-end (not just
2// typecheck) to confirm the coerced runtime value is actually correct.
3
4enum Wrapper<T> {
5 Present { value: T },
6 Absent { placeholder: ! },
7}
8
9fun infallible() -> Wrapper<i64> {
10 Wrapper::Present { value = 42 }
11}
12
13fun infallible_result() -> Result<i64, !> {
14 Result::Ok { value = 7 }
15}
16
17fun main() {
18 let x: i64 := infallible();
19 let y: i64 := infallible_result();
20 assert(x == 42);
21 assert(y == 7);
22 assert(x + y == 49);
23}
passes
Dynamic Semantics №2
When every arm of a match diverges, the match expression has type !.
Referenced by: rfc-0078
Tested by
1// Positive: RFC-0078 §1.1/§2 -- `panic(msg)` has type `!`, which coerces
2// implicitly to any type wherever the surrounding context is otherwise
3// unconstrained.
4
5fun pick(cond: boolean) -> i64 {
6 if (cond) {
7 panic("nope")
8 } else {
9 5
10 }
11}
12
13fun main() -> i64 {
14 pick(false)
15}
passes
Legality Rule №8
Result<T, !> has an uninhabited Err variant and therefore only an Ok value can be
constructed.
Referenced by: rfc-0078
Tested by
1// RFC-0078 §3.3: inhabited-singleton coercion, exercised end-to-end (not just
2// typecheck) to confirm the coerced runtime value is actually correct.
3
4enum Wrapper<T> {
5 Present { value: T },
6 Absent { placeholder: ! },
7}
8
9fun infallible() -> Wrapper<i64> {
10 Wrapper::Present { value = 42 }
11}
12
13fun infallible_result() -> Result<i64, !> {
14 Result::Ok { value = 7 }
15}
16
17fun main() {
18 let x: i64 := infallible();
19 let y: i64 := infallible_result();
20 assert(x == 42);
21 assert(y == 7);
22 assert(x + y == 49);
23}
passes
Legality Rule №9
Result<T, !> satisfies the inhabited-singleton coercion rule and a match omitting Err
is exhaustive.
Referenced by: rfc-0078
Tested by (2)
1// Positive: RFC-0078 §4.1 -- Result<T, !> satisfies the uninhabited-variant rule
2// as a special case of the general rule (§3.2); a match omitting `Err` is
3// exhaustive.
4
5fun use_result(r: Result<i64, !>) -> i64 {
6 match (r) {
7 Result::Ok { value } => value,
8 // Result::Err omitted -- exhaustive; Err is uninhabited when E = !.
9 }
10}
11
12fun main() -> i64 {
13 use_result(Result::Ok { value = 3 })
14}
passes
1// Positive: RFC-0078 §3.2/§4.1 -- writing the arm for an uninhabited variant is
2// allowed (merely unreachable, not rejected); the compiler may warn but must
3// not error.
4
5fun use_result(r: Result<i64, !>) -> i64 {
6 match (r) {
7 Result::Ok { value } => value,
8 Result::Err { error } => panic("unreachable"),
9 }
10}
11
12fun main() -> i64 {
13 use_result(Result::Ok { value = 3 })
14}
passes
Legality Rule №10
Perhaps<!> has only its zero-field None variant inhabited; it does not coerce to a
field type.
Referenced by: rfc-0078
Tested by
1// RFC-0078 §5: Perhaps<!> has only one inhabited variant -- None. The Some
2// variant would require a value of type !, which cannot be constructed, so
3// a match omitting Some is exhaustive.
4fun use_perhaps(p: Perhaps<!>) -> i64 {
5 match (p) {
6 Perhaps::None => 0,
7 // Some omitted -- exhaustive; Some is uninhabited when T = !.
8 }
9}
10
11fun main() -> i64 {
12 use_perhaps(Perhaps::None)
13}
passes
Legality Rule №11
A function declared -> ! is legal only when every reachable control-flow path diverges;
a reachable ordinary return is a type error.
Referenced by: rfc-0078
Tested by (3)
1// Positive: RFC-0078 §6 -- a function declared `-> !` typechecks when every
2// path genuinely diverges (a `panic` tail expression, or a `loop` with no
3// reachable `break`).
4
5fun abort(msg: String) -> ! {
6 panic(msg)
7}
8
9fun loop_forever() -> ! {
10 loop { }
11}
12
13fun abort_via_return(msg: String) -> ! {
14 // Also fine: the `return`ed expression itself never produces a value.
15 return panic(msg);
16}
17
18fun main() {}
passes
1// Issue #229/RFC-0078 §6: a bare `panic(msg);` (trailing `;`, not tail
2// position) as a function's last statement must still be recognized as
3// divergent. Previously `fun_body_diverges` only special-cased the removed
4// `TypedStmt::Return`/`Break`/`Continue` variants directly and otherwise
5// always returned `false` for any other last-statement shape -- a latent gap
6// that made this exact case (a semicolon-terminated diverging call, not a
7// tail expression) incorrectly rejected under `-> !`.
8fun abort_stmt(msg: String) -> ! {
9 panic(msg);
10}
11
12fun main() {}
passes
1// Negative: RFC-0078 §6 -- a function declared `-> !` containing a reachable,
2// ordinary `return` (one whose value is NOT itself `!`-typed) is a type error:
3// the function actually returns, which `-> !` forbids.
4
5fun bad() -> ! { // ERROR[T0016]
6 return 5;
7}
8
9fun main() {}
typecheck errorT0016at 5
Perhaps<T>
Perhaps<T> is the built-in optional type. There is no null — all absence is expressed via Perhaps<T>.
The type of None is Perhaps<T> for some T that must be determinable from contextL1. If no context constrains T — for example, a bare let x = None with no annotation and no subsequent use that pins the element type — the program is a type error. An explicit annotation is required in that case:
None and Some are ordinary variants of Perhaps<T>, not literalsNone and Some have no special status in the grammar or the type system. They resolve exactly as Red does for a user-declared enum Colour { Red, .. } — bare where the expected type determines the enum, qualified (Perhaps::None) anywhere. Everything said here about needing a determinable type follows from that general rule rather than from a rule about None specifically, and the same is true of Result<T, E>'s Ok/Err. See §Unqualified variant construct….
fun main() -> i64 {
let x: Perhaps<i64> := None;
match (x) {
Some { value } => value,
None => 0,
}
}
fun main() -> i64 {
let result: Perhaps<i64> := None;
let value: Perhaps<i64> := Some { value = 42 };
match (value) {
Some { value } => value,
None => match (result) {
Some { value } => value,
None => 0,
},
}
}
Use match to unwrap safely:
struct User {
id: i64,
}
fun find_user(id: i64) -> Perhaps<User> {
if (id == 1) {
return Some { value = User { id = 1 } };
}
return None;
}
fun main() -> i64 {
match (find_user(1)) {
Some { value } => value.id,
None => 0,
}
}
.yolo() unwraps, panicking if the value is None:
struct User {
id: i64,
}
fun find_user(id: i64) -> Perhaps<User> {
if (id == 1) {
return Some { value = User { id = 1 } };
}
return None;
}
fun main() -> i64 {
let user := find_user(1).yolo();
return user.id;
}
Formal rules
Legality Rule №1
None is the empty variant of Perhaps<T> and is valid only where the expected type
determines T; Perhaps::None is valid wherever the qualified variant is named.
Referenced by: rfc-0020, rfc-0111
Tested by (3)
1fun find(arr: i64[], target: i64) -> Perhaps<i64> {
2 var i := 0;
3 while (i < arr.len()) {
4 if (arr[i as u64] == target) {
5 return Perhaps::Some { value = i };
6 }
7 i += 1;
8 }
9 None
10}
11
12fun main() {
13 // None literal matches the None pattern.
14 let n: Perhaps<i64> := None;
15 let r1 := match (n) { None => -1, Perhaps::Some { value } => value, };
16 assert(r1 == -1);
17 // Perhaps::Some construction and field extraction via match.
18 let s := Perhaps::Some { value = 42 };
19 let r2 := match (s) { None => -1, Perhaps::Some { value } => value, };
20 assert(r2 == 42);
21 // Perhaps as function return type — found case.
22 let arr := [10, 20, 30, 40];
23 let idx := find(arr, 30);
24 let r3 := match (idx) { None => -1, Perhaps::Some { value } => value, };
25 assert(r3 == 2);
26 // Perhaps as function return type — not-found case.
27 let idx2 := find(arr, 99);
28 let r4 := match (idx2) { None => -1, Perhaps::Some { value } => value, };
29 assert(r4 == -1);
30 // Perhaps in expression position — the match result is usable directly.
31 let doubled := match (find(arr, 20)) {
32 Perhaps::Some { value } => value * 2,
33 None => 0,
34 };
35 assert(doubled == 2);
36}
passes
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
Result<T, E>
Result<T, E> represents the outcome of a fallible operation:
fun divide(a: f64, b: f64) -> Result<f64, String> {
if (b == 0.0) {
return Err { error = "division by zero" };
}
return Ok { value = a / b };
}
fun main() -> i64 {
match (divide(8.0, 2.0)) {
Ok { value } => value as i64,
Err { error } => 0,
}
}
Use match to handle both cases, or The operator D2
to propagate errors.
.yolo()Panics D1 also works on Result<T, E>,
panicking on Err.