c218d875 · core f63c1e69 · built 2026-10-26 22:39 UTCType 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 (2)
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
1fun rhs<T>(unused: T, mark: &var i64) -> i64 {
2 *mark := *mark * 10 + 1;
3 4
4}
5
6fun index(mark: &var i64) -> u64 {
7 *mark := *mark * 10 + 2;
8 0u64
9}
10
11fun update<T>(unused: T, values: &var [i64; 2], mark: &var i64) {
12 values[index(mark)] += rhs(unused, mark);
13}
14
15fun arithmetic<T>(unused: T) -> i64 {
16 var number := 6;
17 number += 2;
18 number -= 1;
19 number *= 3;
20 number /= 3;
21 number %= 4;
22 number
23}
24
25fun main() {
26 var values := [3, 5];
27 var mark := 0;
28 update(0, &var values, &var mark);
29 assert(mark == 12);
30 assert(values[0u64] == 7);
31 assert(arithmetic(0) == 3);
32}
passes
Legality Rule №2
Conversion between numeric types is written with an explicit as cast.
Referenced by: rfc-0007
Tested by (2)
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
1fun index<T>(value: T, mark: &var i64) -> i64 {
2 *mark := *mark + 1;
3 1
4}
5
6fun write<T>(values: &var [T; 2], value: T, mark: &var i64) {
7 values[index(0, mark) as u64] := value;
8}
9
10fun read<T: Copy>(values: &[T; 2], mark: &var i64) -> T {
11 values[index(0, mark) as u64]
12}
13
14fun converted<T>(unused: T, value: i64) -> f64 {
15 value as f64
16}
17
18fun main() {
19 var values := [3, 4];
20 var mark := 0;
21 write(&var values, 7, &var mark);
22 assert(mark == 1);
23 assert(read(&values, &var mark) == 7);
24 assert(mark == 2);
25 assert(converted(0, 9) == 9.0);
26}
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
GAP-TYPES-001A function parameter is a monotype, so it cannot be used at two types in one body.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 Option<i64>-returning function.
6fun find(arr: [i64], target: i64) -> Option<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 Option<i64>.
19fun first_positive(arr: [i64]) -> Option<i64> {
20 let result: Option<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 Option<i64>-returning function.
6fun find(arr: [i64], target: i64) -> Option<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 Option<i64>.
19fun first_positive(arr: [i64]) -> Option<i64> {
20 let result: Option<i64> := loop {
21 break None;
22 };
23 result
24}
passes
Tuples
Tuples are lightweight anonymous product types.
GAP-TYPES-009RFC-0178RFC-0151A tuple cannot be split into its first element and the rest, or extended, in a generic body.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”
Records and Rows at a Glance
Every product type in Metel has a row: the set of its labelled fields, each with a
type. An §Anonymous records is its row. A struct or record
declaration is a brand (its name, fixed at declaration) paired with a row. Rows are
what generic code can talk about structurally, in three increasingly general ways:
| To say | Write | Specified in |
|---|---|---|
| "a record with at least these fields" | <record T: { x: f64, .. }> | §Row bounds |
| "a record with no such field" | <record T: !{ token }> | §Row bounds |
| "…and name the rest, so a signature can carry it" | <row R> with { x: f64, ..R } | §Open rows |
| "…the rest is this field plus a remainder" | where R = { token: Token, ..Rest } | §Open rows |
| "every field's type satisfies an aspect" | where all R: Copy | §Field wise row constraints |
| "this impl applies to every record of this shape" | extend<row R: { x, .. }> { ..R }: A | Implementing an aspect for a record§Implementing an aspect for a… |
Which types a row bound or row-conditional impl can match is decided by the declaration kind, not by the fields written:
| row visible to bounds and row-conditional impls | inherent methods | |
|---|---|---|
| anonymous record | yes | no |
record (§Records) | yes | yes |
struct | no | yes |
enum | no | yes |
Narrowing connects these to ownership. Moving a field out of a value leaves a value with
a smaller row (§Narrowing); a row-conditional impl or bound is
checked against that current row. Passing a wider record by value to a parameter that
names only some of its fields is width subtyping, and is allowed only when no
non-Copy field would be forgotten (§Open rows).
The example below uses each piece once:
record Session<row R> { id: i64, data: { ..R } }
// A bound: any record carrying `user: i64`.
fun user_of<record T: { user: i64, .. }>(e: T) -> i64 { e.user }
// A row variable: carry the rest through unchanged.
fun keep<row R>(p: { user: i64, ..R }) -> { user: i64, ..R } { p }
// Row-conditional methods: which exist depends on the session's row.
extend<row R: { token: String, .. }> Session<..R> {
fun send(self) -> i64 { self.id * 10 }
}
extend<row R: !{ token }> Session<..R> {
fun login(self) -> i64 { self.id }
}
fun main() {
assert(user_of({ user = 7, kind = 1 }) == 7);
assert(keep({ user = 1, extra = 2 }).extra == 2);
let anon: Session<{ user: i64 }> := Session { id = 2, data = { user = 1 } };
let authed: Session<{ token: String }> := Session { id = 1, data = { token = "t" } };
assert(anon.login() == 2);
assert(authed.send() == 10);
}
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: for one concrete row, or for every row of a given shape (Implementing an aspect for a record§Implementing an aspect for a…).
- No custom
Drop.Dropis a standard-library aspect and never local to ordinary user code, so teardown logic belongs to nominal types only.
Arrays are the other structural target: extend<T> [T]: MyAspect { … } is supported, per
the orphan-rule carve-out for structural type constructors — see
§Structural aspect bounds.
Auto-derived aspects are unaffected.
Projection
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`
Handle.{} // the type: Handle's brand with no fields remaining
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.
If the nominal type itself declares no fields, this is a full-width projection and
normalizes to that nominal type. An empty branded residual of a non-empty type
is not the anonymous record type {} or Unit.
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 satisfies an impl-based aspect bound only through an extend on a
record target whose row it has (Implementing an aspect for a
record§Implementing an aspect for a…); with no such implementation the bound is
rejected with T0012.
Referenced by: rfc-0116
Tested by
1// Negative (RFC-0116 §3): an anonymous record satisfies an impl-based aspect only through an
2// `extend` on a record target (`std::core`'s `Display` for a record needs every field to be
3// `Display`). A record with a field that has no `Display` is rejected at the call site, like a
4// tuple or a struct without the impl -- not accepted and then blown up at run time.
5struct Opaque { v: i64 }
6
7fun show<T: Display>(x: T) -> String { x.to_string() }
8
9fun main() {
10 let r := { x = Opaque { v = 1 } };
11 let s := show(r);
12}
typecheck errorT0012“does not implement `Display`”
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.
[T] rather than postfix T[]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 buffer[T] 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.
GAP-TYPES-002How a growable List<T> allocates and grows its storage, and where it ends and T[] and [T; N] begin, is not fully specified.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, rfc-0171
Tested by (5)
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 assert(show([6, 7]) == "[6, 7]");
6}
7
8fun show<T: Display>(value: T) -> String {
9 value.to_string()
10}
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
1fun set_first(xs: [i64]) {
2 xs[0] := 9; // ERROR[T0023]
3}
4fun main() {}
typecheck errorT0023at 2“cannot assign through `[T]`”
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 (18)
1type Pair<T> := { left: T, right: T };
2type Grid := [i64; 3];
3record Cell { value: i64, grid: Grid }
4type Residual<row R> := { ..R };
5fun swap(p: Pair<i64>) -> Pair<i64> { { left = p.right, right = p.left } }
6fun strip(c: Cell) -> Residual<{ grid: Grid }> {
7 let { value: _, ..rest } := c;
8 rest
9}
10fun main() {
11 let s := swap({ left = 1, right = 2 });
12 assert(s.left == 2);
13 let r := strip(Cell { value = 9, grid = [1, 2, 3] });
14 assert(r.grid[1] == 2);
15 extend_ok();
16}
17fun extend_ok() {
18 let g: Grid := [4, 5, 6];
19 var total := 0;
20 for (x in g) { total := total + x; }
21 assert(total == 15);
22}
passes
1record Series<row R> { points: [i64; 3], extra: { ..R } }
2extend<row R: { unit: String, .. }> Series<..R> { fun total(&self) -> i64 { self.points[0] + self.points[1] + self.points[2] } }
3extend<row R: !{ unit }> Series<..R> { fun total(&self) -> i64 { 0 } }
4fun main() {
5 let a := Series { points = [1, 2, 3], extra = { unit = "m" } };
6 let b := Series { points = [1, 2, 3], extra = {} };
7 assert(a.total() == 6);
8 assert(b.total() == 0);
9}
passes
1fun main() {
2 let xs := [1, 2, 3];
3 let y := xs["a"]; // ERROR[T0001]
4}
typecheck errorT0001at 3“array index must be u64”
1fun main() {
2 let xs := [1, "a"]; // ERROR[T0001]
3}
typecheck errorT0001at 2“cannot unify”
1fun main() {
2 let xs := [1, 2, 3];
3 xs.push(4); // ERROR[T0003]
4}
typecheck errorT0003at 3“no method `push` on array type”
1fun main() {
2 let n := 5;
3 for (x in n) { } // ERROR[T0001]
4}
typecheck errorT0001at 3“cannot unify”
1record Row { name: String }
2fun main() {
3 let rows: [Row] := [Row { name = "a".to_string() }];
4 for (r in rows) { let n := r.name; } // ERROR[T0019]
5}
typecheck errorT0019at 4“borrowed from a `[T]` view”
1record Token { value: String, id: i64 }
2fun count(xs: [{}]) -> i64 { xs.len() as i64 }
3fun main() {
4 let a := Token { value = "a".to_string(), id = 1 };
5 let { value: _, id: _, ..e1 } := a;
6 let b := Token { value = "b".to_string(), id = 2 };
7 let { value: _, id: _, ..e2 } := b;
8 let es: [{}] := [e1, e2];
9 assert(count(es) == 2);
10 let c := Token { value = "c".to_string(), id = 3 };
11 let { value: _, id: _, ..e3 } := c;
12 match (e3) { whole => { let _w: {} := whole; } }
13}
passes
1fun head<row R>(v: { xs: [i64; 3], ..R }) -> i64 where all R: Copy {
2 let w := v;
3 v.xs[0] + w.xs[1]
4}
5fun main() { assert(head({ xs = [1, 2, 3], flag = true }) == 3); }
passes
1enum Shape { Circle { r: i64, tag: String }, Square { s: i64, tag: String } }
2record Item { id: i64, shape: [i64; 2] }
3fun area(s: Shape) -> i64 {
4 match (s) {
5 Shape::Circle { r, tag } => r * 3 + tag.len(),
6 Shape::Square { s, tag } => s * s + tag.len(),
7 }
8}
9fun total<row R>(v: { n: i64, ..R }) -> i64 where all R: Copy {
10 match (v) { { n, ..rest } => n }
11}
12fun main() {
13 assert(area(Shape::Circle { r = 2, tag = "ab" }) == 8);
14 assert(area(Shape::Square { s = 3, tag = "" }) == 9);
15 assert(total({ n = 5, flag = true }) == 5);
16 let it := Item { id = 1, shape = [2, 3] };
17 match (it.shape) { [a, b] => assert(a + b == 5) }
18 let r: Option<{ n: i64 }> := Option::Some { value = { n = 4 } };
19 match (r) { Option::Some { value } => assert(value.n == 4), None => assert(false) }
20}
passes
1record Counter { n: i64, label: String }
2fun main() {
3 let c := Counter { n = 1, label = "c" };
4 let { label, ..rest } := c;
5 let f := [rest] once || { rest.n + 1 };
6 assert(f() == 2);
7 assert(label == "c");
8 let arr: [i64; 3] := [1, 2, 3];
9 let g := [arr] || { arr[0] + arr[2] };
10 assert(g() == 4);
11 assert(g() == 4);
12}
passes
1record Frame { rows: [i64; 3], label: String }
2record Grid { cells: [i64; 3] }
3fun consume(g: Grid) -> i64 { g.cells[0] }
4fun rest_rows() {
5 let f := Frame { rows = [1, 2, 3], label = "f" };
6 let { label, ..rest } := f;
7 assert(label == "f");
8 var total := 0;
9 for (r in rest.rows) { total := total + r; }
10 assert(total == 6);
11}
12fun empty_rest() {
13 let g := Grid { cells = [7, 8, 9] };
14 let { cells, ..rest } := g;
15 assert(cells[2] == 9);
16 let again := Grid { cells = cells };
17 assert(consume(again) == 7);
18}
19fun main() {
20 rest_rows();
21 empty_rest();
22}
passes
1// RP x DY, RP x CO, ER x PM, ER x CO, AR x OW, AR x GA
2aspect Name { fun name(&self) -> String; }
3record User { id: i64, name: String, tags: [String; 2] }
4extend<row R: { name: String, .. }> { ..R }: Name { fun name(&self) -> String { self.name.clone() } }
5fun first<T: Copy>(xs: [T; 2]) -> T { xs[0] }
6fun main() {
7 let u := User { id = 1, name = "ann", tags = ["a", "b"] };
8 let { id, ..rest } := u;
9 let d: &dyn Name := &rest;
10 assert(d.name() == "ann");
11 assert(id == 1);
12 let users := [User { id = 2, name = "b", tags = ["x", "y"] }, User { id = 3, name = "c", tags = ["p", "q"] }];
13 var ids := 0;
14 for (w in users) { ids := ids + w.id; }
15 assert(ids == 5);
16 let t := User { id = 4, name = "d", tags = ["m", "n"] };
17 let tags := t.tags;
18 let name := t.name;
19 assert(tags[0] == "m");
20 assert(name == "d");
21 assert(first([5, 6]) == 5);
22 let { id: _, name: _, tags: _, ..empty } := User { id = 5, name = "e", tags = ["u", "v"] };
23 let _e: {} := empty;
24}
passes
1fun first_two<T: Copy>(xs: [T; 3]) -> [T; 2] { [xs[0], xs[1]] }
2fun main() {
3 let f := first_two([4, 5, 6]);
4 assert(f[1] == 5);
5 let g := first_two([true, false, true]);
6 assert(g[0]);
7}
passes
1record Samples { values: [i64; 3] }
2fun main() {
3 let s := Samples { values = [1, 2] }; // ERROR[T0001]
4}
typecheck errorT0001at 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
1import lib::{Event, events, weight, Doc, make, first_generic};
2fun main() {
3 var total := 0;
4 for (e in events()) { total := total + weight(e); }
5 assert(total == 10);
6 let d := make();
7 let title := d.title;
8 let pages := d.pages;
9 assert(title == "t");
10 assert(first_generic(pages) == 3);
11}
passes
1import shapes::{Frame, Weigh, strip_label, total, empty_after};
2fun main() {
3 let f := Frame { public_rows = [1, 2, 3], label = "x" };
4 let stripped := strip_label(f);
5 assert(stripped.public_rows[2] == 3);
6 assert(total(stripped) == 6);
7 assert(total({ public_rows = [4, 5, 6], extra = true }) == 15);
8 let e := empty_after({ label = "gone".to_string() });
9 assert(e.weigh() == 1);
10 assert(stripped.weigh() == 1);
11}
passes
Legality Rule №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 (5)
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 rhs<T>(value: T, mark: &var i64) -> T {
2 *mark := *mark * 10 + 1;
3 value
4}
5
6fun index(mark: &var i64) -> u64 {
7 *mark := *mark * 10 + 2;
8 0u64
9}
10
11fun write<T>(values: &var [T; 1], value: T, mark: &var i64) {
12 values[index(mark)] := rhs(value, mark);
13}
14
15fun main() {
16 var values := [0];
17 var mark := 0;
18 write(&var values, 7, &var mark);
19 assert(mark == 12);
20 assert(values[0u64] == 7);
21}
passes
1fun position<T>(unused: T, mark: &var i64) -> u64 {
2 *mark := *mark * 10 + 2;
3 0u64
4}
5
6fun element<T>(unused: T, mark: &var i64) -> i64 {
7 *mark := *mark * 10 + 1;
8 7
9}
10
11fun copied<T: Copy>(value: T, index: u64) -> T {
12 [value, value][index]
13}
14
15fun ordered<T>(value: T, other: T, mark: &var i64) -> i64 {
16 [element(value, mark), 0][position(other, mark)]
17}
18
19fun borrowed<T>(value: T, other: T, mark: &var i64) -> i64 {
20 (&[element(value, mark), 0])[position(other, mark)]
21}
22
23fun main() {
24 assert(copied(9, 1u64) == 9);
25 var mark := 0;
26 assert(ordered(0, 0, &var mark) == 7);
27 assert(mark == 12);
28 mark := 0;
29 assert(borrowed(0, 0, &var mark) == 7);
30 assert(mark == 12);
31}
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 (4)
1record Batch { samples: [i64; 3], label: String }
2type Samples := [i64; 3];
3type Sum := once || -> i64;
4
5fun sum_head<row R>(value: { samples: Samples, ..R }) -> i64 where all R: Copy {
6 var total := 0;
7 for (sample in value.samples) { total := total + sample; }
8 total
9}
10
11fun main() {
12 let batch := Batch { samples = [1, 2, 3], label = "batch" };
13 let { label, ..rest } := batch;
14 assert(label == "batch");
15 let copied := rest.clone();
16 let callback: Sum := [copied] once || { sum_head(copied) };
17 assert(callback() == 6);
18 assert(sum_head({ samples = rest.samples, extra = true }) == 6);
19 match (rest.samples) {
20 [first, second, third] => assert(first + second + third == 6),
21 }
22}
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
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
GAP-OWNERSHIP-001#1293RFC-0122RFC-0067Nothing prevents a shared and an exclusive reference to the same place from coexisting."Exclusive" means shared XOR exclusive: a place may have any number of &T borrows, or
exactly one &var T, never both.
&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 (selectors only); the explicit dereference operator *p (v0.11.0)
reads through a reference and writes 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
Reading the value a reference points to is always spelled *r. 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; // explicit read: copies the referent out
return y;
}
A reference is never implicitly converted to its referent, at any position: not a let
or var annotation, a call argument, a return value, a break value, the tail
expression of a body, an if/else branch, a match arm, or an ascription. Each is a
type error, T0001, whose help line names the fix:
fun get(r: &i64) -> i64 {
r // error[T0001]: expected i64, found &i64
// = help: read the value out with `*r` (copies a Copy referent)
}
fun bump(p: &var i64) -> i64 {
*p += 1;
*p // tail expression: explicit read
}
A chain of reference layers is read one * per layer:
fun main() -> i64 {
let x := 42;
let r: &i64 := &x;
let rr: &&i64 := &r;
let y: i64 := **rr; // one `*` for each layer
return y;
}
&var T still coerces to &T where a shared reference is expected
(spec.types.references.legality-1L1). A non-Copy T
cannot be read out of a reference: *r of a non-Copy referent is a move out of a
reference, rejected by the move checker (see §Ownership).
Formal rules
Legality Rule №1
A reference-typed expression is never implicitly converted to its referent type. Where a
non-reference type is expected, a reference-typed expression is a type error; *r reads
the referent out, one * per reference layer, and only a Copy referent can be read out.
Referenced by: rfc-0067a, rfc-0185
Tested by (10)
1// RFC-0185: reading a plain value out of a reference at a `return` is spelled `*p`;
2// the reference is never converted implicitly.
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-0185: an explicit read `*a` at an implicit tail expression (no `return`
2// keyword), through an `if`/`else` whose branches read out of references.
3fun pick(cond: boolean, a: &i64, b: &i64) -> i64 {
4 if (cond) { *a } else { *b }
5}
6
7fun main() {
8 let x := 10;
9 let y := 20;
10 assert(pick(true, &x, &y) == 10);
11 assert(pick(false, &x, &y) == 20);
12}
passes
1// RFC-0185: an explicit read `*p` as the value of a `loop { break value; }`.
2fun main() {
3 var n := 0;
4 let p: &var i64 := &var n;
5 let result: i64 := loop {
6 break *p;
7 };
8 assert(result == 0);
9}
passes
1// RFC-0185: reading a plain value out of a chain of references takes one `*` per
2// layer.
3fun main() {
4 let n := 7;
5 let r: &i64 := &n;
6 let rr: &&i64 := &r;
7 let rrr: &&&i64 := &rr;
8
9 let x: i64 := **rr;
10 assert(x == 7);
11
12 let y: i64 := ***rrr;
13 assert(y == 7);
14
15 // Mixed shared/mut chain.
16 var m := 9;
17 let mr: &var i64 := &var m;
18 let rmr: &&var i64 := &mr;
19 let z: i64 := **rmr;
20 assert(z == 9);
21
22 // An explicit read through a chain at `return`, not just `let`.
23 assert(read_through_chain(&rr) == 7);
24}
25
26fun read_through_chain(rrp: &&&i64) -> i64 {
27 return ***rrp;
28}
passes
1// RFC-0185: a call returning `&T` is read out with `*` exactly like a reference
2// binding -- same position, same types, same spelling (formerly RFC-0112 §1.0).
3
4fun shared() -> &i64 {
5 let a := 42;
6 return &a;
7}
8
9fun chained() -> &&i64 {
10 let a := 42;
11 let r: &i64 := &a;
12 return &r;
13}
14
15fun through_return() -> i64 {
16 return *shared();
17}
18
19fun main() {
20 // let annotation
21 let n: i64 := *shared();
22 assert(n == 42);
23
24 // binding the call result first, then copying out at the annotation
25 let r := shared();
26 let m: i64 := *r;
27 assert(m == 42);
28
29 // ascription
30 let a := (*shared()): i64;
31 assert(a == 42);
32
33 // return position
34 assert(through_return() == 42);
35
36 // every layer is peeled, not just one
37 let c: i64 := **chained();
38 assert(c == 42);
39
40 // the reference itself is still available when that is what is asked for
41 let keep := shared();
42 assert(*keep == 42);
43}
passes
1// RFC-0045: Mutable address-of lvalue paths — fat pointer (MutFieldPointer) tests.
2// RFC-0067a: *T/*mut T renamed to &T/&var T. RFC-0185: reads are explicit `*p`.
3
4struct Point { x: i64, y: i64 }
5struct Rect { top_left: Point, bottom_right: Point }
6
7fun main() {
8 // &var struct.field — write through fat pointer.
9 var p := Point { x = 1, y = 2 };
10 let px: &var i64 := &var p.x;
11 *px := 10;
12 assert(p.x == 10);
13 assert(p.y == 2);
14
15 // &var struct.field — compound assign through fat pointer.
16 *px += 5;
17 assert(p.x == 15);
18
19 // Read through fat pointer — explicit `*px` yields current field value.
20 let read_x: i64 := *px;
21 assert(read_x == 15);
22
23 // &var tuple element.
24 var t := (100, 200);
25 let t1: &var i64 := &var t.1;
26 *t1 := 999;
27 assert(t.1 == 999);
28 assert(t.0 == 100);
29
30 // &var array element.
31 var arr: [i64; 3] := [1, 2, 3];
32 let a1: &var i64 := &var arr[1];
33 *a1 := 42;
34 assert(arr[0] == 1);
35 assert(arr[1] == 42);
36 assert(arr[2] == 3);
37
38 // Compound assign through array element fat pointer.
39 *a1 += 8;
40 assert(arr[1] == 50);
41
42 // Chained path: &var outer.inner.field (nested struct).
43 var r := Rect {
44 top_left = Point { x = 0, y = 0 },
45 bottom_right = Point { x = 10, y = 10 },
46 };
47 let brx: &var i64 := &var r.bottom_right.x;
48 *brx := 20;
49 assert(r.bottom_right.x == 20);
50 assert(r.top_left.x == 0);
51
52 // Auto-deref field access through MutFieldReference — FieldAccess auto-deref.
53 var q := Point { x = 5, y = 7 };
54 let qptr: &var Point := &var q;
55 assert(qptr.x == 5);
56 qptr.y := 99;
57 assert(q.y == 99);
58}
passes
1// RFC-0185: ascription never reads through a reference, but it still drives the
2// unsizing coercions an expected type already implies (dyn erasure, `[T; N]` -> `[T]`).
3aspect Shape { fun area(&self) -> i64; }
4struct Sq { s: i64 }
5extend Sq: Shape { fun area(&self) -> i64 { self.s * self.s } }
6
7fun main() {
8 let d := (Sq { s = 3 }: dyn Shape);
9 assert(d.area() == 9);
10
11 let v := ([1, 2, 3]: [i64]);
12 assert(v.len() == 3);
13
14 let n := 5;
15 let r: &i64 := &n;
16 assert((*r: i64) == 5);
17}
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[T0001]
2// RFC-0185 (formerly #649): a reference is never implicitly read out as its referent,
3// whether or not the referent is `Copy` -- a `let` annotation does not dereference.
4struct NotCopy { v: String }
5
6fun main() {
7 let owned := NotCopy { v = "x" };
8 let r: &NotCopy := &owned;
9 let copy: NotCopy := r;
10}
typecheck errorT0001“read the value out with `*r`”
1// TYPECHECK_ERROR[T0001]
2// RFC-0185: even a `Copy` referent is not read out implicitly. The diagnostic names
3// the fix: `*r`.
4fun main() {
5 let x := 42;
6 let r: &i64 := &x;
7 let y: i64 := r;
8}
typecheck errorT0001“expected i64, found &i64”
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 a borrowed view 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) -> Option<T> | Remove and return the last element, or None |
len | (&self) -> i64 | Number of elements |
get | (&self, index: i64) -> Option<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 Option<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Option::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Option::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Option::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 Option::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Option::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 Option::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 Option::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Option::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Option::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Option::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 Option::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 Option::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Option::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 Option::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 Option::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 Option<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Option::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Option::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Option::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 Option::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Option::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 Option::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 Option::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Option::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Option::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Option::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 Option::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 Option::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Option::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 Option::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 Option::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 Option<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Option::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Option::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Option::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 Option::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Option::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 Option::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 Option::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Option::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Option::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Option::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 Option::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 Option::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Option::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 Option::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 Option::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 Option<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Option::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Option::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Option::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 Option::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Option::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 Option::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 Option::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Option::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Option::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Option::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 Option::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 Option::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Option::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 Option::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 Option::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 Option<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Option::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Option::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Option::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 Option::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Option::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 Option::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 Option::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Option::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Option::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Option::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 Option::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 Option::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Option::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 Option::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 Option::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 typeL1. It is a type-inference hint that also supplies the expected type for unsizing coercions; it never reads through a reference.
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 never
reads a value out of a reference; it may apply the unsizing coercions that an expected type
already drives (dyn erasure, [T; N] to [T], singleton types).
Referenced by: rfc-0021, rfc-0023
Tested by (3)
1// RFC-0185: ascription never reads through a reference, but it still drives the
2// unsizing coercions an expected type already implies (dyn erasure, `[T; N]` -> `[T]`).
3aspect Shape { fun area(&self) -> i64; }
4struct Sq { s: i64 }
5extend Sq: Shape { fun area(&self) -> i64 { self.s * self.s } }
6
7fun main() {
8 let d := (Sq { s = 3 }: dyn Shape);
9 assert(d.area() == 9);
10
11 let v := ([1, 2, 3]: [i64]);
12 assert(v.len() == 3);
13
14 let n := 5;
15 let r: &i64 := &n;
16 assert((*r: i64) == 5);
17}
passes
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: Option<i64>) -> T {
7 return make(fallback);
8}
9
10fun main() {
11 make::<i64>(None : Option<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: Option<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 built := make_row(true, [1, 2, 3]);
let first := first_or_default([1, 2, 3], None);
return total + built.len() + first;
}
Ascription is still useful when no surrounding context fixes the type:
fun main() -> i64 {
let arr := [] : [i64];
let value := None : Option<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
Option<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]) -> Option<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,
}
}
Rigid type parameters
GAP-TYPES-005An operator such as + or < on a bare type parameter is rejected, and no aspect exists that grants it.A declared type parameter is opaque inside its own definition: it is equal only to itself, and supports only what its declared bounds entail. A generic definition is accepted or rejected on that basis alone, never by trying it against particular arguments.
fun pick<T>(a: T) -> i64 { a } // error: expected i64, found T
fun id2<T, U>(a: T) -> U { a } // error: expected U, found T
The declared bounds (inline T: A, where clauses, and the enclosing extend's bounds)
are the whole contract: a use is allowed when the bounds entail it, and is an error at
the definition otherwise.
Formal rules
Legality Rule №1
Inside a generic function or method, each declared type parameter, including row
parameters and the parameters of the enclosing extend, is equal only to itself or to a
type that the definition's declared bounds state is equal to it. Unifying it with a
concrete type, or with another declared parameter, in any other way is a type error
(T0001) at the definition. The declared bounds state equalities in exactly two places:
a row equation (where R = { label: Type, ..Rest }, in which Rest is derived from R
and lacks the named labels, so R never equals Rest) and an associated-type binding
(T: Iterable<Item = X> makes T::Item equal to X). A closure or nested generic
function inside the body sees the enclosing parameters as rigid; a recursive generic call
instantiates the callee's parameters afresh.
Tested by (20)
1type Callback := once || -> i64;
2fun invoke(f: Callback) -> i64 { f() }
3
4fun transition<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
5 { ..r, token = "secret" }
6}
7
8fun copied_tail<row R: !{token}>(r: { id: i64, ..R }) -> i64 where all R: Copy {
9 let { id, ..rest } := r;
10 let copy := rest;
11 let callback := [copy, id] once || {
12 let extended := transition(copy);
13 let { token, ..again } := extended;
14 id + token.len() as i64
15 };
16 invoke(callback)
17}
18
19fun main() {
20 assert(copied_tail({ id = 3, count = 7 }) == 9);
21 assert(copied_tail({ id = 5 }) == 11);
22}
passes
1fun add<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
2 { ..r, token = "secret" }
3}
4
5fun forward<row R: !{token}>(r: { ..R }) -> i64 where all R: Copy {
6 let callback := [r] once || {
7 let added := add(r);
8 let copied := r;
9 7
10 };
11 callback()
12}
13
14fun main() {
15 assert(forward({ n = 1 }) == 7);
16 assert(forward({}) == 7);
17}
passes
1fun add<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
2 { ..r, token = "secret" }
3}
4
5fun forward<row R: !{token}>(r: { ..R }) -> i64 {
6 let callback := [r] once || {
7 let added := add(r);
8 let again := r; // ERROR[T0019]
9 7
10 };
11 callback()
12}
13
14fun main() {}
typecheck errorT0019“use of moved value `r`”
1struct NotClone {}
2
3fun clone_head<row R>(value: { fixed: NotClone, ..R })
4where all R: Clone {
5 value.clone() // ERROR[T0035]
6}
7
8fun main() {}
typecheck errorT0035
1fun clone_row<row R>(value: &{ ..R }) -> { ..R } where all R: Clone {
2 value.clone()
3}
4
5fun copy_head<row R>(value: { id: i64, ..R }) -> i64 where all R: Copy {
6 value.id
7}
8
9record CopyRow { id: i64, count: i64 }
10type CopyAlias := CopyRow;
11
12fun main() {
13 let original := { name = "owned", count = 4 };
14 let cloned := clone_row(&original);
15 let { name, ..tail } := cloned;
16 assert(name == "owned");
17 assert(tail.count == 4);
18 assert(original.name == "owned");
19 let empty := clone_row(&{});
20 let with_id := { ..empty, id = 8 };
21 assert(copy_head(with_id) == 8);
22 let copied: CopyAlias := CopyRow { id = 3, count = 7 };
23 assert(copy_head(copied) == 3);
24 assert(copied.count == 7);
25}
passes
1fun duplicate_rest<row R, row Rest>(value: { ..R }) -> ({ ..Rest }, { ..Rest })
2where R = { token: String, ..Rest }, all Rest: Copy {
3 let { token: _, ..rest } := value;
4 let first := rest;
5 let second := rest;
6 (first, second)
7}
8aspect Relay { fun pass<T>(self, value: T) -> T; }
9struct Courier {}
10extend Courier: Relay {
11 fun pass<T>(self, value: T) -> T { value }
12}
13fun extract<T>(value: { payload: T }) -> T {
14 let { payload, ..rest } := value;
15 let empty: {} := rest;
16 payload
17}
18struct Owned { text: String }
19fun forward<T>(value: T) -> T { value }
20fun main() {
21 assert(extract({ payload = "generic owned" }) == "generic owned");
22 assert(extract({ payload = 17 }) == 17);
23 let empty_pair := duplicate_rest({ token = "discarded" });
24 let left: {} := empty_pair.0;
25 let right: {} := empty_pair.1;
26 let pair := duplicate_rest({ token = "discarded", count = 8, enabled = true });
27 assert(pair.0.count == 8);
28 assert(pair.1.enabled);
29 let { token: _, ..empty } := { token = "owned" };
30 let forwarded: {} := Courier {}.pass(empty);
31 let data := { token = "discarded", payload = "kept" };
32 let var { token: _, ..tail } := data;
33 tail.payload := "restored";
34 assert(Courier {}.pass(tail).payload == "restored");
35 let owned := Owned { text = "removed" };
36 let text := owned.text;
37 let forwarded_nominal: Owned.{} := Courier {}.pass(forward(owned));
38 assert(text == "removed");
39}
passes
1// RFC-0179: a closure that captures an open-row parameter and passes it on forgets nothing, so
2// no `where all R: Copy` is needed.
3type Callback := once || -> i64;
4fun head<row R>(value: { id: i64, ..R }) -> i64 { value.id }
5fun accepted<row R>(value: { id: i64, ..R }) -> i64 {
6 let callback: Callback := [value] once || {
7 head(value)
8 };
9 callback()
10}
11fun main() {
12 assert(accepted({ id = 5, text = "kept".to_string() }) == 5);
13}
passes
1fun dup<T>(x: T) -> (T, T) { (x, x) } // ERROR[T0019]
2fun main() {}
typecheck errorT0019at 1“use of moved value”
1fun make_clone<T: Clone>(value: T) -> once || -> T {
2 let owned := value.clone();
3 [owned] once || { owned.clone() }
4}
5
6fun main() {
7 let callback := make_clone("generic closure".to_string());
8 assert(callback() == "generic closure");
9}
passes
1record Box<T> { value: T }
2
3fun clone_box<T: Clone>(value: Box<T>) -> Box<T> {
4 value.clone()
5}
6
7fun main() {
8 let original := Box { value = "generic record".to_string() };
9 let copied := clone_box(original);
10 assert(copied.value == "generic record");
11}
passes
1struct Pair { left: String, right: String }
2fun main() {
3 var pair := Pair { left = "gone", right = "kept" };
4 let left := pair.left;
5 let repair := [pair] once |extra| { (pair.right, extra) };
6 let result := repair(7);
7 assert(result.0 == "kept");
8 assert(result.1 == 7);
9
10 var empty_pair := Pair { left = "old-left", right = "old-right" };
11 let old_left := empty_pair.left;
12 let old_right := empty_pair.right;
13 let refill := [empty_pair] once var |extra| {
14 empty_pair.left := "new-left";
15 empty_pair.right := "new-right";
16 (empty_pair, extra)
17 };
18 let restored := refill(9);
19 assert(restored.0.left == "new-left");
20 assert(restored.0.right == "new-right");
21 assert(restored.1 == 9);
22}
passes
1fun duplicate_rest<row R, row Rest>(value: { ..R }) -> ({ ..Rest }, { ..Rest })
2where R = { token: String, ..Rest } {
3 let { token: _, ..rest } := value;
4 let first := rest;
5 (first, rest) // ERROR[T0019]
6}
7fun main() {}
typecheck errorT0019“use of moved value `rest`”
1type Envelope<T> := (T, i64);
2fun collapse<T>(value: Envelope<T>) -> Envelope<i64> { value } // ERROR[T0001]
3fun main() {}
typecheck errorT0001“a declared type parameter is opaque inside its own definition”
1fun first_two<T: Copy>(xs: [T; 3]) -> [T; 2] { [xs[0], xs[1]] }
2fun main() {
3 let f := first_two([4, 5, 6]);
4 assert(f[1] == 5);
5 let g := first_two([true, false, true]);
6 assert(g[0]);
7}
passes
1// RG x DY, RG x PM, RG x CO, AS x PM
2aspect Show { fun show(&self) -> String; }
3struct Num { v: i64 }
4extend Num: Show { fun show(&self) -> String { "num".to_string() } }
5fun show_all<T: Show>(xs: [T]) -> i64 { var n := 0; for (x in xs) { if (x.show().len() > 0) { n := n + 1; } } n }
6fun describe(d: &dyn Show) -> String { d.show() }
7fun pick<T: Copy>(o: Option<T>, d: T) -> T { match (o) { Option::Some { value } => value, None => d } }
8record Pt { x: i64, y: i64 }
9fun main() {
10 assert(show_all([Num { v = 1 }, Num { v = 2 }]) == 2);
11 assert(describe(&Num { v = 3 }) == "num");
12 assert(pick(Option::Some { value = 3 }, 9) == 3);
13 let n: Option<i64> := None;
14 assert(pick(n, 9) == 9);
15 let p := Pt { x = 1, y = 2 };
16 match (p.clone()) { Pt { x, y } => assert(x + y == 3) }
17 assert("${p}" == "Pt { x: 1, y: 2 }");
18}
passes
1fun collapse<T>(value: { payload: T }) -> i64 {
2 let { payload, ..rest } := value;
3 payload // ERROR[T0001]
4}
5fun main() {}
typecheck errorT0001“a declared type parameter is opaque inside its own definition”
1record Packet<row R> { data: { ..R } }
2type EmptyRow := {};
3type EmptyPacket := Packet<{}>;
4type Envelope<T> := (T, i64);
5fun same<row R>(value: Envelope<{ ..R }>) -> Envelope<{ ..R }> { value }
6fun copy_twice<row R>(value: { ..R }) -> Envelope<({ ..R }, { ..R })>
7where all R: Copy { ((value, value), 11) }
8fun strip<row R, row Rest>(value: Packet<..R>) -> Packet<..Rest>
9where R = { token: String, ..Rest } {
10 let { token: _, ..rest } := value.data;
11 Packet { data = rest }
12}
13fun identity<T>(value: Envelope<T>) -> Envelope<T> { value }
14aspect Carrier { fun pass<T>(self, value: Envelope<T>) -> Envelope<T>; }
15struct Courier {}
16extend Courier: Carrier { fun pass<T>(self, value: Envelope<T>) -> Envelope<T> { value } }
17fun main() {
18 let wrapped := same(({ text = "owned", count = 2 }, 3));
19 let value := wrapped.0;
20 assert(value.count == 2);
21 assert(value.text == "owned");
22 let pair := copy_twice({ count = 5 });
23 assert(pair.0.0.count + pair.0.1.count == 10);
24 let empty_pair := copy_twice({});
25 let empty: EmptyRow := empty_pair.0.0;
26 let stripped := strip(Packet { data = { token = "gone", text = "kept" } });
27 assert(stripped.data.text == "kept");
28 let stripped_empty: EmptyPacket := strip(Packet { data = { token = "gone" } });
29 let envelope := Courier {}.pass(identity((stripped_empty, 9)));
30 assert(envelope.1 == 9);
31}
passes
1// RFC-0173 D1: an associated-type binding is a fact while the body is checked.
2aspect Container { type Item; fun get(self) -> Item; }
3
4struct IntBox { value: i64 }
5extend IntBox: Container { type Item := i64; fun get(self) -> i64 { self.value } }
6
7fun wrong<T: Container<Item = i64>>(value: T) -> String {
8 value.get() // ERROR[T0001]
9}
10
11fun main() { wrong(IntBox { value = 1 }); }
typecheck errorT0001at 8
1type Callback := once || -> i64;
2fun invoke(f: Callback) -> i64 { f() }
3
4fun transition<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
5 { ..r, token = "secret" }
6}
7
8fun copied_tail<row R: !{token}>(r: { id: i64, ..R }) -> i64 where all R: Copy {
9 let { id, ..rest } := r;
10 let copy := rest;
11 let callback := [copy] once || { // ERROR[T0026]
12 let extended := transition(copy);
13 let { token, ..again } := extended;
14 id + token.len() as i64
15 };
16 invoke(callback)
17}
18
19fun main() {
20 copied_tail({ id = 3, count = 7 });
21}
typecheck errorT0026at 11“`id` is captured but not listed”
1import helper::make;
2import helper::strip;
3import helper::callback;
4import helper::EmptyPacket;
5import helper::Callback;
6
7fun main() {
8 let empty: EmptyPacket := strip(make(7, { token = "gone" }));
9 let run: Callback := callback(empty);
10 assert(run() == 7);
11 let with_tail := strip(make(9, { token = "gone", samples = [1, 2] }));
12 assert(with_tail.id == 9);
13 assert(with_tail.data.samples[1] == 2);
14}
passes
Legality Rule №2
A body may use a parameter T only in ways its declared bounds Γ entail, by membership
in Γ and with no transitive closure: a method call x.m() iff some bound in Γ(T)
declares m; a field access x.label on a row-bounded parameter iff a row bound in Γ
names label, at the type that bound declares; a method of a conditional impl on a
type built from T iff the impl's conditions are entailed by Γ. A use not granted by
Γ is T0035. Passing x where a callee requires U: A is entailed iff A ∈ Γ(T),
and otherwise is T0012. where all R: A grants A to the fields of R, not to R,
and is forwarded only to another where all R: A bound. T::Assoc is an opaque
projection equal only to itself or to what an associated-type binding in Γ declares. A
bare parameter is not callable. Impl resolution for a type built from T assumes Γ.
Tested by (15)
1// RFC-0173 D6: a generic body may select a record-target conditional method
2// when its `all R: Mark` entitlement matches the impl's condition.
3
4aspect Mark {}
5aspect Label { fun label(&self) -> String; }
6
7extend<row R> { ..R }: Label where all R: Mark {
8 fun label(&self) -> String { "marked".to_string() }
9}
10
11fun use<row R>(value: { ..R }) -> String where all R: Mark {
12 value.label()
13}
passes
1// RFC-0173 D6: an abstract record row cannot use a record-target conditional
2// method unless its declared all-fields entitlement entails the impl condition.
3
4aspect Mark {}
5aspect Label { fun label(&self) -> String; }
6
7extend<row R> { ..R }: Label where all R: Mark {
8 fun label(&self) -> String { "marked".to_string() }
9}
10
11fun use<row R>(value: { ..R }) -> String {
12 value.label() // ERROR[T0035]
13}
typecheck errorT0035at 12
1aspect Source { type Item; fun take(self) -> Item; }
2record Holder<T> { value: T }
3extend<T> Holder<T>: Source {
4 type Item := T;
5 fun take(self) -> T { self.value }
6}
7fun empty_from<S: Source<Item = { token: String }>>(source: S) -> {} {
8 let { token: _, ..rest } := source.take();
9 rest
10}
11fun main() {
12 let empty: {} := empty_from(Holder { value = { token = "owned" } });
13}
passes
1aspect Transform<A, B> {
2 fun transform(&self, value: A) -> B;
3 fun again(&self, value: A) -> B { self.transform(value) }
4 fun identity<T>(&self, value: T) -> T { value }
5}
6struct Doubler {}
7extend Doubler: Transform<i64, i64> {
8 fun transform(&self, value: i64) -> i64 { value * 2 }
9}
10fun generic<T: Transform<i64, i64>>(value: &T, input: i64) -> i64 {
11 let kept: String := value.identity("kept");
12 value.again(input)
13}
14struct Wrapped<T: Transform<i64, i64>> { value: T }
15extend<T> Wrapped<T> {
16 fun apply(&self, value: i64) -> i64 { self.value.again(value) }
17}
18fun main() {
19 assert(generic(&Doubler {}, 6) == 12);
20 assert(Wrapped { value = Doubler {} }.apply(7) == 14);
21}
passes
1// RFC-0173 D6(3) / #1323: a conditional inherent method is unavailable in
2// a generic body until the receiver argument's declared bounds entail the
3// impl condition.
4
5aspect Tag {
6 fun tag(self) -> i64;
7}
8
9struct Box<T> {
10 value: T,
11}
12
13extend i64: Tag {
14 fun tag(self) -> i64 { 7 }
15}
16
17extend<T: Tag> Box<T> {
18 fun tagged(self) -> i64 { 1 }
19}
20
21fun use_box<U>(box: Box<U>) -> i64 {
22 box.tagged() // ERROR[T0035]
23}
typecheck errorT0035at 22
1record Session<row R> { data: { ..R } }
2extend<row R: !{ token }> Session<..R> {
3 fun cleared(&self) -> i64 { 2 }
4}
5fun strip<row R, row Rest>(session: Session<..R>) -> i64
6where R = { token: String, ..Rest } {
7 let { token: _, ..rest } := session.data;
8 let cleared: Session<..Rest> := Session { data = rest };
9 cleared.cleared()
10}
11fun main() {
12 assert(strip(Session { data = { token = "gone" } }) == 2);
13 assert(strip(Session { data = { token = "gone", kept = "here" } }) == 2);
14}
passes
1aspect Mark { fun mark(&self) -> i64; }
2extend<row R> { ..R }: Mark where all R: Copy {
3 fun mark(&self) -> i64 { 7 }
4}
5fun unchecked<row R>(value: &{ ..R }) -> i64 { value.mark() } // ERROR[T0035]
6fun main() {}
typecheck errorT0035“conditional implementation requirements are not entailed”
1record Session<row R> { id: i64, data: { ..R } }
2extend<row R: { token: String, .. }> Session<..R> {
3 fun kind(self) -> i64 { 1 }
4}
5extend<row R: !{ token }> Session<..R> {
6 fun kind(self) -> i64 { 2 }
7}
8aspect Mark { fun mark(&self) -> i64; }
9extend<row R> { ..R }: Mark where all R: Copy {
10 fun mark(&self) -> i64 { 7 }
11}
12fun marked<row R>(value: &{ ..R }) -> i64 where all R: Copy {
13 value.mark()
14}
15fun remove_token<row R, row Rest>(session: Session<..R>) -> Session<..Rest>
16where R = { token: String, ..Rest } {
17 let { id, data } := session;
18 let { token: _, ..rest } := data;
19 let cleared: Session<..Rest> := Session { id = id, data = rest };
20 cleared
21}
22fun has_token<row R: { token: String, .. }>(session: Session<..R>) -> i64 {
23 session.kind()
24}
25fun main() {
26 assert(has_token(Session { id = 3, data = { token = "t", text = "owned" } }) == 1);
27 assert(remove_token(Session { id = 3, data = { token = "t" } }).kind() == 2);
28 assert(remove_token(Session { id = 5, data = { token = "t", text = "kept" } }).kind() == 2);
29 assert(marked(&{}) == 7);
30 assert(marked(&{ count = 4, flag = true }) == 7);
31}
passes
1// RFC-0173 D6(2): matching declared bounds forward to the callee.
2
3aspect Tag { fun tag(self) -> i64; }
4extend i64: Tag { fun tag(self) -> i64 { 7 } }
5
6fun needs_tag<T: Tag>(value: T) -> i64 { value.tag() }
7
8fun forward<U: Tag>(value: U) -> i64 { needs_tag(value) }
9
10fun main() { println(forward(1)); }
passes
1// RFC-0173 D6(2): a caller cannot pass an unconstrained declared parameter
2// to a callee whose corresponding parameter requires Tag.
3
4aspect Tag { fun tag(self) -> i64; }
5
6fun needs_tag<T: Tag>(value: T) -> i64 { value.tag() }
7
8fun forward<U>(value: U) -> i64 {
9 needs_tag(value) // ERROR[T0012]
10}
typecheck errorT0012at 9
1fun absent<row R: !{token}>(values: [{..R}]) -> i64 { values.len() }
2
3fun caller<row T>() -> i64 {
4 absent::<T>([]) // ERROR[T0012]
5}
6
7fun main() {}
typecheck errorT0012at 4
1// RFC-0173 D6(3): a declared row bound makes the corresponding conditional
2// inherent method visible within the generic body.
3
4record Session<row R> {
5 id: i64,
6 data: { ..R },
7}
8
9extend<row R: { token: String, .. }> Session<..R> {
10 fun with_token(self) -> i64 { self.id }
11}
12
13fun use_session<row R: { token: String, .. }>(session: Session<..R>) -> i64 {
14 session.with_token()
15}
16
17fun main() {
18 let session: Session<{ token: String }> := Session {
19 id = 1,
20 data = { token = "token" },
21 };
22 println(use_session(session));
23}
passes
1// A negative row bound also entails a negative row-conditional impl condition.
2
3record Session<row R> {
4 id: i64,
5 data: { ..R },
6}
7
8extend<row R: !{ token }> Session<..R> {
9 fun without_token(self) -> i64 { self.id }
10}
11
12fun use_session<row R: !{ token }>(session: Session<..R>) -> i64 {
13 session.without_token()
14}
passes
1// RFC-0173 D6(3): a row-conditional inherent method is unavailable in a
2// generic body until the row parameter's declared bounds entail the condition.
3
4record Session<row R> {
5 id: i64,
6 data: { ..R },
7}
8
9extend<row R: { token: String, .. }> Session<..R> {
10 fun with_token(self) -> i64 { self.id }
11}
12
13fun use_session<row R>(session: Session<..R>) -> i64 {
14 session.with_token() // ERROR[T0035]
15}
typecheck errorT0035at 14
1import left::LeftPacket;
2import left::LeftRead;
3import left::inspect_left;
4import left::identity_left;
5import right::RightPacket;
6import right::RightRead;
7import right::inspect_right;
8import right::identity_right;
9
10fun main() {
11 let left := identity_left(LeftPacket { data = { count = 4, text = "left" } });
12 let right := identity_right(RightPacket { data = { count = 4, text = "right" } });
13 assert(inspect_left(&left) == 4);
14 assert(inspect_right(&right) == 104);
15 let left_dyn: dyn LeftRead := left;
16 let right_dyn: dyn RightRead := right;
17 assert(left_dyn.read() == 4);
18 assert(right_dyn.read() == 104);
19 let empty_left: LeftPacket<{}> := identity_left(LeftPacket { data = {} });
20 let empty_right: RightPacket<{}> := identity_right(RightPacket { data = {} });
21}
passes
Legality Rule №3
An operator on a bare type parameter (a + b over T) is rejected (T0005): no bound
grants it, because no aspect grants an operator yet. T: Eq grants == through its eq
method.
Tested by
1// Negative (RFC-0173 D5): no aspect grants an arithmetic or ordering operator yet, so
2// such an operator on a bare declared parameter is rejected (GAP-TYPES-005). The
3// workaround is to pass the operation in, or to write the function for the concrete types.
4fun add<T>(a: T, b: T) -> T { a + b } // ERROR[T0005]
5
6fun main() { add(1, 2); }
typecheck errorT0005
Dynamic Semantics №1
A well-formed generic definition is accepted or rejected independently of any call. An implementation may still check a body again with concrete types, but that re-check never accepts a definition the rules above reject, and a body the definition check accepted cannot be rejected by it: if it is, the two checks disagree, which is the internal error §I0010 generic definition and…, never a diagnostic about the call site.
Tested by (4)
1// Parity test for METEL-121: generic function bodies that previously used the
2// untyped evaluator path now go through construction-at-call-time.
3// Exercises multiple generic patterns that both pipelines must handle identically.
4
5fun identity<T>(x: T) -> T { x }
6
7fun first<A, B>(a: A, _b: B) -> A { a }
8
9fun swap_apply<T, U>(x: T, f: |T| -> U) -> U { f(x) }
10
11fun add_all<T>(a: T, b: T, c: T, combine: |T, T| -> T) -> T {
12 combine(combine(a, b), c)
13}
14
15fun main() {
16 // identity through various types
17 assert(identity(42) == 42);
18 assert(identity(true) == true);
19 assert(identity("hello") == "hello");
20
21 // first/second selection
22 assert(first(1, 2) == 1);
23 assert(first("a", 99) == "a");
24
25 // higher-order generic with i64
26 let doubled := swap_apply(5, |x: i64| -> i64 { x * 2 });
27 assert(doubled == 10);
28
29 let negated := swap_apply(7, |x: i64| -> i64 { x * -1 });
30 assert(negated == -7);
31
32 // multi-arg generic
33 let sum := add_all(1, 2, 3, |a: i64, b: i64| -> i64 { a + b });
34 assert(sum == 6);
35}
passes
1fun helper() -> i64 { 4 }
2
3fun invalid<T>(value: T) -> i64 {
4 let helper := 3;
5 let callback := [value] once || { helper }; // ERROR[T0026]
6 0
7}
8
9fun main() {}
typecheck errorT0026at 5“`helper` is captured but not listed”
1fun helper() -> i64 { 4 }
2
3fun use_helper<T>(value: T) -> i64 {
4 let n := 3;
5 let explicit := [n] || -> i64 { helper() + n };
6 let implicit := || -> i64 { helper() + n };
7 explicit() + implicit()
8}
9
10fun main() {
11 assert(use_helper(1) == 14);
12 assert(use_helper("owned") == 14);
13}
passes
1// Capture completeness is checked even when the generic function is never called.
2fun invalid<T>(value: T) -> i64 {
3 let id := 7;
4 let callback := [value] once || { id }; // ERROR[T0026]
5 0
6}
7
8fun main() {}
typecheck errorT0026at 4“`id` is captured but not listed”
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.
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.
Open rows
Available in v0.14.0. RFC-0121 open rows support row extension, decomposition, reusable type positions, and row-conditional implementations.
GAP-TYPES-008RFC-0178No form combines the fields of two rows into one record: merging two generic records has no spelling.A row-kinded generic parameter abstracts over "the rest of a row" rather than a
concrete shape. The binder is row R; every use of the variable is written ..R,
distinguishing it from a bare type variable in every position a row can appear —
a record type's tail ({ x: f64, ..R }), a struct's own residual projection
(Handle.{ ..R }), and a generic argument (Session<..R>):
fun get_x<row R>(p: { x: f64, ..R }) -> f64 { p.x }
Extension needs no operator — the new label goes directly in the row literal:
RequestBuilder<{ ..R, auth: String }>
Removal from an abstract row uses a rest pattern. A where equation names both halves
and states how they compose, which simultaneously subsumes a row bound (R = { token, ..Rest }
already implies R: { token, .. }) and names the remainder:
extend<row R, row Rest> Session<..R>
where R = { token: Token, ..Rest }
{
fun authenticate(self) -> Session<..Rest> { ... }
}
The equation constrains the type. In a generic body, { token, ..rest } consumes a record
and binds its unnamed fields as an owned anonymous record. A row spread ({ ..rest, auth = a })
places an owned record's fields into a new record. The details and current implementation
limit are specified below.
Reading a field the equation promises is:
fun split<row R, row Rest>(p: { token: i64, ..R }) -> i64
where R = { extra: i64, ..Rest }
{
p.token + p.extra
}
fun main() {
assert(split({ token = 1, extra = 2, other = 3 }) == 3);
}
For a rest pattern, the checker removes the explicitly named labels from the strongest
decomposition entailed by the scrutinee's type and declared row facts. Other known fields
and the open tail remain. Thus, if S = { token: Token, extra: i64, ..Rest }, then
{ token, ..rest } gives rest: { extra: i64, ..Rest }. A presence-only bound such as
R: { token, .. } proves that token exists but does not determine the remainder type;
the pattern is rejected with a distinct “remainder type cannot be determined” diagnostic.
If all fields are removed from a closed row, the remainder has empty anonymous-record type
{}, not Unit. Bare .. in a pattern remains discard-only and does not produce a value.
A record literal accepts at most one spread, including for concrete rows. Written labels must be proven absent from the spread row; collisions do not override. Initializers evaluate from left to right, while field order/layout is determined by the resulting row/type, not initializer order.
Legality Rule №4
A record literal contains at most one spread; each explicitly initialized label must be
entailed absent from the spread row, and an absent-label fact is required for abstract rows.
An absent-label fact is a closed row without the label, a decomposition whose remainder
excludes it, or a label-only negative bound (R: !{ token }). A typed negative bound such as
R: !{ token: String } is not one: it only rules out that type, and R may still hold the
label at another type. A collision is an error, not an override. Spreading an owned record moves its fields into
the result and consumes the source. Spreading through a reference copies the fields only
when every field is provably Copy; otherwise it is rejected with T0033. Initializers
evaluate left-to-right, while the resulting record layout is determined by its row/type.
A row variable may also be the argument of a nominal type's own row parameter, written
..R in the argument list. A bound on that row (where R = { auth: String, .. }) is
checked at each use:
record Builder<row R> { name: String, data: { ..R } }
fun auth_of<row R>(b: Builder<..R>) -> String where R = { auth: String, .. } {
b.data.auth
}
fun main() {
let b := Builder { name = "req", data = { auth = "tok", retries = 3 } };
assert(auth_of(b) == "tok");
}
An argument whose row lacks a required label is rejected with T0012:
record Builder<row R> { name: String, data: { ..R } }
fun auth_of<row R>(b: Builder<..R>) -> String where R = { auth: String, .. } {
b.data.auth
}
fun main() {
let bare := Builder { name = "bare", data = { retries = 1 } };
let _ := auth_of(bare);
}
A tail-only parameter { ..R } transfers the entire owned row, and a parameter that names
fields ({ f: T, ..R }) takes the whole record too, so the two spellings accept the same
programs and an owned remainder may hold non-Copy fields.
Width subtyping. A record with extra fields may be passed to a parameter that names
only some of them ({ x: f64, ..R }); the extras bind to R. By value the parameter takes
the whole record: the callee owns the extra fields and may move, return or drop them, so
they need not be Copy:
fun get_x<row R>(p: { x: i64, ..R }) -> i64 { p.x }
fun main() {
let wide := { x = 1, name = "ada".to_string() };
assert(get_x(wide) == 1);
}
Taking the record by reference does not move it, so the caller keeps every field:
fun get_x<row R>(p: &{ x: i64, ..R }) -> i64 { p.x }
fun main() {
let wide := { x = 1, name = "ada" };
assert(get_x(&wide) == 1);
assert(wide.name == "ada");
}
Inside a generic body the extras are abstract, and the body owns them too, so it can pass
the parameter on by value with no bound. where all R: Copy is only needed to use the row
more than once:
fun consume<row R>(p: { x: i64, ..R }) -> i64 { p.x }
fun outer<row R>(s: { x: i64, ..R }) -> i64 {
consume(s) + 1
}
fun main() {
assert(outer({ x = 1, k = "kept".to_string(), flag = true }) == 2);
}
An open record with an anonymous tail ({ x: T, .. }) is not accepted everywhere; see
legality-5L5, as decided by
RFC-0181. Three limits are recorded as gaps.
GAP-TYPES-010RFC-0181acceptedAn open record with an anonymous tail is rejected as a return type, so an API cannot promise only some fields of the record it returns.A function cannot return an open record while hiding its extra fields: an anonymous tail in a return type is T0032. Omit the return type, write the record in full, or name a row that comes from an argument.
GAP-TYPES-011RFC-0181acceptedAn open record with an anonymous tail is rejected inside a function type, and a named row fixes the row for the whole signature, so no written type means 'works for every wider record'.A written function type cannot say it accepts any record with at least some fields. A named row
(|{ x: T, ..R }| -> U) works for callbacks over records of one shape.
GAP-TYPES-012RFC-0181acceptedAn anonymous open record is rejected as a field type, so a field cannot hold 'any record with at least these fields'.A record or struct field cannot have an open-record type with an anonymous tail; give the record a row parameter instead.
Formal rules
Referenced by: rfc-0178
Tested by (13)
1fun strip<row R, row Rest>(d: { ..R }) -> { ..Rest }
2where R = { token: String, ..Rest } {
3 let { token: _, ..rest } := d;
4 rest
5}
6
7fun strip_extra<row R, row Rest>(d: { ..R }) -> { user: i64, ..Rest }
8where R = { token: String, user: i64, ..Rest } {
9 let var { token: _, ..rest } := d;
10 rest.user := rest.user + 1;
11 rest
12}
13
14fun strip_two<row R, row Mid, row Rest>(d: { ..R }) -> { ..Rest }
15where R = { token: String, ..Mid }, Mid = { user: i64, ..Rest } {
16 let { token: _, user: _, ..rest } := d;
17 rest
18}
19
20fun take<row R>(a: { x: i64, ..R }, b: { y: i64, ..R }) -> { ..R } {
21 let { x: _, ..rest } := a;
22 rest
23}
24
25record Box<row R> { data: { ..R } }
26record Credentials { token: String, name: String }
27extend<row R> Box<..R> {
28 fun strip<row Rest>(self) -> { ..Rest }
29 where R = { token: String, ..Rest } {
30 let { token: _, ..rest } := self.data;
31 rest
32 }
33 fun from_arg<row Q, row Rest>(self, d: { id: i64, ..Q }) -> { ..Rest }
34 where Q = { token: String, ..Rest } {
35 let { id: _, token: _, ..rest } := d;
36 rest
37 }
38 fun add<row Q: !{token}>(self, d: { ..Q }) -> { token: String, ..Q } {
39 { token = "new", ..d }
40 }
41}
42
43fun main() {
44 assert(strip({ token = "t", user = 3 }).user == 3);
45 assert(strip({ token = "t", owned = "kept" }).owned == "kept");
46 assert(strip(Credentials { token = "t", name = "Ada" }).name == "Ada");
47 let empty: {} := strip({ token = "only" });
48 assert(strip_extra({ token = "t", user = 3, active = true }).active);
49 assert(strip_extra({ token = "t", user = 3 }).user == 4);
50 assert(strip_two({ token = "t", user = 3, active = true }).active);
51 assert(take({ x = 1, z = true }, { y = 2, z = false }).z);
52 let b := Box { data = { token = "t", x = true } };
53 assert(b.strip().x);
54 let from_arg_box := Box { data = { token = "t", x = true } };
55 assert(from_arg_box.from_arg({ id = 1, token = "t", y = true }).y);
56 let add_box := Box { data = { token = "t", x = true } };
57 assert(add_box.add({ z = 5 }).z == 5);
58 println("ok");
59}
passes
1fun strip_two<row R, row Mid, row Rest>(d: { ..R }) -> { ..Rest }
2where R = { token: String, ..Mid }, Mid = { user: i64, ..Rest } {
3 let { token: _, user: _, ..rest } := d;
4 rest
5}
6fun main() {
7 strip_two({ token = "t", active = true }); // ERROR[T0012]
8}
typecheck errorT0012at 7
1type Callback := once || -> i64;
2fun invoke(f: Callback) -> i64 { f() }
3
4fun transition<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
5 { ..r, token = "secret" }
6}
7
8fun copied_tail<row R: !{token}>(r: { id: i64, ..R }) -> i64 where all R: Copy {
9 let { id, ..rest } := r;
10 let copy := rest;
11 let callback := [copy, id] once || {
12 let extended := transition(copy);
13 let { token, ..again } := extended;
14 id + token.len() as i64
15 };
16 invoke(callback)
17}
18
19fun main() {
20 assert(copied_tail({ id = 3, count = 7 }) == 9);
21 assert(copied_tail({ id = 5 }) == 11);
22}
passes
1fun add<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
2 { ..r, token = "secret" }
3}
4
5fun forward<row R: !{token}>(r: { ..R }) -> i64 where all R: Copy {
6 let callback := [r] once || {
7 let added := add(r);
8 let copied := r;
9 7
10 };
11 callback()
12}
13
14fun main() {
15 assert(forward({ n = 1 }) == 7);
16 assert(forward({}) == 7);
17}
passes
1fun clone_row<row R>(value: &{ ..R }) -> { ..R } where all R: Clone {
2 value.clone()
3}
4
5fun copy_head<row R>(value: { id: i64, ..R }) -> i64 where all R: Copy {
6 value.id
7}
8
9record CopyRow { id: i64, count: i64 }
10type CopyAlias := CopyRow;
11
12fun main() {
13 let original := { name = "owned", count = 4 };
14 let cloned := clone_row(&original);
15 let { name, ..tail } := cloned;
16 assert(name == "owned");
17 assert(tail.count == 4);
18 assert(original.name == "owned");
19 let empty := clone_row(&{});
20 let with_id := { ..empty, id = 8 };
21 assert(copy_head(with_id) == 8);
22 let copied: CopyAlias := CopyRow { id = 3, count = 7 };
23 assert(copy_head(copied) == 3);
24 assert(copied.count == 7);
25}
passes
1// Whole-record open rows x pattern matching x rest patterns (RFC-0179, RFC-0178): one arm of a
2// match destructures the remainder out of the owned parameter, the other leaves it to drop.
3fun split<row R>(p: { kind: i64, ..R }) -> i64 {
4 match (p.kind) {
5 1 => {
6 let { kind: _, ..rest } := p;
7 10
8 },
9 _ => 20,
10 }
11}
12
13fun main() {
14 assert(split({ kind = 1, text = "t".to_string() }) == 10);
15 assert(split({ kind = 2, text = "u".to_string() }) == 20);
16}
passes
1// Whole-record open rows x rest patterns x ownership (RFC-0179, RFC-0178): the callee owns the
2// whole record, destructures the remainder out and returns it; the caller owns the result.
3fun strip<row Rest>(d: { token: String, ..Rest }) -> { ..Rest } {
4 let { token: _, ..rest } := d;
5 rest
6}
7
8fun main() {
9 let d := { token = "t".to_string(), user = "u".to_string(), n = 3 };
10 let r := strip(d);
11 assert(r.user == "u");
12 assert(r.n == 3);
13}
passes
1fun take_id<row R, row Rest>(r: { id: i64, ..R }) -> i64
2where R = { extra: i64, ..Rest }
3{
4 let var { id, ..rest } := r;
5 id := id + 1;
6 rest.extra := rest.extra + 1;
7 id + rest.extra
8}
9
10fun take_extra(r: { id: i64, extra: i64 }) -> i64 {
11 let { id: _, extra } := r;
12 extra
13}
14
15fun add_auth<row R: !{ auth }>(r: { ..R }, auth: i64) -> { ..R, auth: i64 } {
16 { ..r, auth = auth }
17}
18
19fun copy_row(r: &{ x: i64, y: i64 }) -> { x: i64, y: i64 } {
20 { ..r }
21}
22
23record PublicPair {
24 id: i64,
25 extra: i64,
26}
27
28fun nominal_remainder(pair: PublicPair) -> i64 {
29 match (pair) {
30 { id, ..tail } => id + tail.extra
31 }
32}
33
34fun main() -> i64 {
35 assert(take_id({ id = 7, extra = 5 }) == 14);
36 assert(take_extra({ id = 8, extra = 9 }) == 9);
37 let r := add_auth({ id = 7 }, 5);
38 let total := take_id({ id = 7, extra = 5 });
39 let anonymous := match ({ id = total, extra = r.auth }) {
40 { id, ..rest } => id + rest.extra
41 };
42 let original := { x = 4, y = 6 };
43 let copied := copy_row(&original);
44 anonymous + nominal_remainder(PublicPair { id = 2, extra = 3 }) + copied.x + original.y
45}
passes
1fun take<row R>(a: { x: i64, ..R }, b: { y: i64, ..R }) -> { ..R } {
2 let { x: _, ..rest } := a;
3 rest
4}
5fun main() {
6 println(take({ x = 1, z = true }, { y = 2, z = 8 }).z); // ERROR[T0001]
7}
typecheck errorT0001at 6
1fun bad(r: &{ data: String }) -> { data: String } {
2 { ..r } // ERROR[T0033]
3}
4
5fun main() {}
typecheck errorT0033at 2“through a reference”
1// metel-core#1456: only a label-only exclusion proves a label absent, so only it allows a spread
2// that writes that label. A typed exclusion still works as the bound it is.
3fun add_inline<row R: !{ token }>(d: { ..R }) -> { token: String, ..R } {
4 { token = "secret".to_string(), ..d }
5}
6
7fun add_where<row R>(d: { ..R }) -> { token: String, ..R } where R: !{ token } {
8 { token = "secret".to_string(), ..d }
9}
10
11fun add_both<row R: !{ token } + !{ token: String }>(d: { ..R }) -> { token: String, ..R } {
12 { token = "secret".to_string(), ..d }
13}
14
15// A typed exclusion constrains callers by type only: a `token: i64` still satisfies it.
16fun typed_only<row R: !{ token: String }>(d: { ..R }) -> i64 { 1 }
17
18fun main() {
19 assert(add_inline({ user = 3 }).token == "secret");
20 assert(add_where({ user = 4 }).user == 4);
21 assert(add_both({ user = 5 }).token == "secret");
22 assert(typed_only({ token = 5 }) == 1);
23}
passes
1// metel-core#1456: `!{ token: String }` only rules out a `String` `token`; the row may still hold
2// `token` at another type, so it is not an absent-label fact and a spread cannot rely on it
3// (spec.types.generics.open-rows.legality-4).
4fun add_token<row R: !{ token: String }>(d: { ..R }) -> { token: String, ..R } {
5 { token = "secret".to_string(), ..d } // ERROR[T0012]
6}
7
8fun main() {}
typecheck errorT0012at 5“cannot prove field `token` absent from the spread row”
1// The same rule for a negative bound written in a `where` clause.
2fun add_token<row R>(d: { ..R }) -> { token: String, ..R } where R: !{ token: String } {
3 { token = "secret".to_string(), ..d } // ERROR[T0012]
4}
5
6fun main() {}
typecheck errorT0012at 3“cannot prove field `token` absent from the spread row”
Legality Rule №1
A generic parameter may be declared row R (parallel to record T); every use of a
row variable outside its own binder is written ..R (or anonymous ..) — in a record
type's tail, a struct's own residual-projection tail, or generic-argument position. A
bare identifier in any of these type positions is a type variable, never a row; row and
type variables are therefore never ambiguous with each other.
An open record type may occur in a reusable type position, including a function or instance-method parameter, a return type, a local annotation, or a nominal field. Its row tail is substituted and checked wherever the enclosing type is instantiated.
A struct's residual-projection tail (Handle.{ fd, ..R }, Handle.{ ..R }) is narrower: it is
accepted only as the type of a function parameter, and anywhere else (a return type, an
annotation, a field, under &) it is a parse error naming that restriction. A row argument
to a nominal type (Session<..R>), an open record, or a closed projection (Handle.{ fd })
covers the same ground in those positions. A projection that names no fields
(Handle.{ ..R }) takes a whole struct, like { ..R }, and so does one that names fields
(Handle.{ fd, ..R }); see legality-3L3.
A row argument is a row variable (..R), an anonymous .., or a literal row: an open or
closed record type written after ... Req<..{ host: String, ..R }> is Req at the row R
extended with host; Req<..{ host: String }> is Req at exactly that one field, and
Req<..{}> at the empty row. A literal row is the same type the checker infers for a record
literal with a spread, so a written return type and an inferred one agree. The labels it
adds must be provably absent from the tail, exactly as for a spread (R: !{ host }), and it is
accepted only where the generic parameter is declared row. An anonymous tail inside a literal
follows the positions in legality-5L5.
record Req<row R> { data: { ..R } }
fun empty() -> Req<..{}> { Req { data = {} } }
fun add_host<row R: !{ host }>(r: Req<..R>, h: String) -> Req<..{ host: String, ..R }> {
Req { data = { host = h, ..r.data } }
}
extend<row R: { host: String, .. }> Req<..R> {
fun build(self) -> String { "GET " + self.data.host }
}
fun main() {
assert(add_host(empty(), "a.com").build() == "GET a.com");
}
Referenced by: rfc-0121, rfc-0180
Tested by (30)
1// RFC-0121 item 2 (metel-core#1310): an anonymous row argument is accepted in
2// an instance method parameter type and receives a fresh row variable.
3record Session<row R> { id: i64, data: { ..R } }
4struct Tool { public n: i64 }
5
6extend Tool {
7 fun take(&self, s: Session<..>) -> i64 { s.id + self.n }
8}
9
10fun main() {
11 let tool := Tool { n = 1 };
12 let session := Session { id = 2, data = { tag = "ok" } };
13 assert(tool.take(session) == 3);
14}
passes
1// metel-core#1493: the field-wise bound on a literal row's tail is enforced at the call, so a
2// remainder holding a non-Copy field is rejected.
3record Req<row R> { data: { ..R } }
4
5fun id_of<row R>(r: Req<..{ id: i64, ..R }>) -> i64 where all R: Copy { r.data.id }
6
7fun main() {
8 let r := Req { data = { id = 4, name = "n".to_string() } };
9 let _ := id_of(r); // ERROR[T0012]
10}
typecheck errorT0012at 9“does not implement `Copy`”
1// metel-core#1473 (RFC-0180, RFC-0181): an anonymous tail inside a literal row follows the same
2// positional rule as any anonymous open record; in a return type it is T0032.
3record Req<row R> { data: { ..R } }
4
5fun add<row R: !{ host }>(r: Req<..R>) -> Req<..{ host: String, .. }> { // ERROR[T0032]
6 Req { data = { host = "x".to_string(), ..r.data } }
7}
8
9fun main() {}
typecheck errorT0032at 5“return type”
1// metel-core#1473 (RFC-0180): a closed literal row, `..{ host: String }`, is exactly those
2// fields, and is usable as a binding annotation.
3record Req<row R> { data: { ..R } }
4
5fun main() {
6 let r: Req<..{ host: String }> := Req { data = { host = "x" } };
7 assert(r.data.host == "x");
8 let e: Req<..{}> := Req { data = {} };
9}
passes
1// metel-core#1473 (RFC-0180): a closed literal row names exactly those fields; a value with an
2// extra field does not match it.
3record Req<row R> { data: { ..R } }
4
5fun main() {
6 let r: Req<..{ host: String }> := Req { data = { host = "x", port = 1 } }; // ERROR[T0001]
7}
typecheck errorT0001at 6“cannot unify”
1// metel-core#1473 (RFC-0180): a literal row is an ordinary type argument inside an alias body
2// and in a record field's type.
3record Req<row R> { data: { ..R } }
4type WithHost<row R> := Req<..{ host: String, ..R }>;
5record Wrapped<row R> { inner: Req<..{ id: i64, ..R }> }
6
7fun add<row R: !{ host }>(r: Req<..R>, h: String) -> WithHost<..R> {
8 Req { data = { host = h, ..r.data } }
9}
10
11fun main() {
12 let r := add(Req { data = { port = 1 } }, "x");
13 assert(r.data.host == "x");
14 let w := Wrapped { inner = Req { data = { id = 1, name = "n".to_string() } } };
15 assert(w.inner.data.id == 1);
16 assert(w.inner.data.name == "n");
17}
passes
1// metel-core#1473 (RFC-0180): a typestate builder written as methods. Each setter returns a
2// `Req` whose row gained a field, so `build` only exists once `host` has been set.
3record Req<row R> { data: { ..R } }
4
5extend<row R: !{ host }> Req<..R> {
6 fun host(self, h: String) -> Req<..{ host: String, ..R }> {
7 Req { data = { host = h, ..self.data } }
8 }
9}
10
11extend<row R: !{ port }> Req<..R> {
12 fun port(self, p: i64) -> Req<..{ port: i64, ..R }> {
13 Req { data = { port = p, ..self.data } }
14 }
15}
16
17extend<row R: { host: String, .. }> Req<..R> {
18 fun build(self) -> String { "GET " + self.data.host }
19}
20
21fun main() {
22 let r := Req { data = {} }.host("a.com").port(80);
23 assert(r.build() == "GET a.com");
24 assert(r.data.port == 80);
25 let swapped := Req { data = {} }.port(1).host("b.com");
26 assert(swapped.build() == "GET b.com");
27}
passes
1// metel-core#1473 (RFC-0180): a parameter typed with a literal row requires its fields at the call.
2record Req<row R> { data: { ..R } }
3
4fun host_of<row R>(r: Req<..{ host: String, ..R }>) -> String { r.data.host }
5
6fun main() {
7 let r := Req { data = { port = 1 } };
8 let _ := host_of(r); // ERROR[T0001]
9}
typecheck errorT0001at 8“cannot unify”
1// metel-core#1473 (RFC-0180): a label written in a literal row must be provably absent from the
2// tail, exactly as for a spread; `R` needs the bound `R: !{ host }`.
3record Req<row R> { data: { ..R } }
4
5fun add<row R>(r: Req<..R>, h: String) -> Req<..{ host: String, ..R }> {
6 Req { data = { host = h, ..r.data } } // ERROR[T0012]
7}
8
9fun main() {}
typecheck errorT0012at 6“bound the row parameter with `!{ host }`”
1// metel-core#1473 (RFC-0180): a function that adds a field to a nominal's row can write its
2// return type with a literal row, `Req<..{ host: String, ..R }>`; `..{}` is the empty row.
3record Req<row R> { data: { ..R } }
4
5fun empty() -> Req<..{}> { Req { data = {} } }
6
7fun add_host<row R: !{ host }>(r: Req<..R>, h: String) -> Req<..{ host: String, ..R }> {
8 Req { data = { host = h, ..r.data } }
9}
10
11fun add_port<row R: !{ port }>(r: Req<..R>, p: i64) -> Req<..{ port: i64, ..R }> {
12 Req { data = { port = p, ..r.data } }
13}
14
15// a parameter can name the row it needs; the tail carries everything else
16fun host_of<row R>(r: Req<..{ host: String, ..R }>) -> String { r.data.host }
17
18fun main() {
19 let r := add_port(add_host(empty(), "a.com"), 80);
20 assert(host_of(r) == "a.com");
21 assert(r.data.port == 80);
22}
passes
1// metel-core#1473 (RFC-0180): a literal row where an ordinary type parameter is expected is a
2// kind error.
3record Box<T> { value: T }
4
5fun f(b: Box<..{ x: i64 }>) -> i64 { 0 } // ERROR[T0012]
6
7fun main() {}
typecheck errorT0012at 5“ordinary type, not a `row`”
1// RFC-0121 item 2 (#1310): method signatures freshen anonymous row arguments
2// independently, just as free-function parameters do.
3record Session<row R> { id: i64, data: { ..R } }
4
5extend<row R> Session<..R> {
6 fun total(self, left: Session<..>, right: Session<..>) -> i64 {
7 self.id + left.id + right.id
8 }
9}
10
11fun main() {
12 let x := Session { id = 1, data = { left = 1 } };
13 let y := Session { id = 2, data = { right = "ok" } };
14 let z := Session { id = 3, data = { third = true } };
15 assert(x.total(y, z) == 6);
16}
passes
1struct Handle { fd: i64, name: String }
2
3struct Holder<row R> { v: Handle.{ ..R } } // ERROR[P0001]
4
5fun main() {}
parse errorP0001at 3“is accepted only as a function parameter type”
1struct Handle { fd: i64, name: String }
2
3fun main() {
4 let x: Handle.{ ..R } := Handle { fd = 1, name = "n".to_string() }; // ERROR[P0001]
5}
parse errorP0001at 4“is accepted only as a function parameter type”
1struct Handle { fd: i64, name: String }
2
3fun keep<row R>(h: Handle.{ fd, ..R }) -> Handle.{ fd, ..R } { // ERROR[P0001]
4 h
5}
6
7fun main() {}
parse errorP0001at 3“is accepted only as a function parameter type”
1struct Handle { fd: i64, name: String }
2
3fun read<row R>(h: &Handle.{ fd, ..R }) -> i64 { h.fd } // ERROR[P0001]
4
5fun main() {}
parse errorP0001at 3“is accepted only as a function parameter type”
1// metel-core#1460: `Handle.{ ..R }` names no field of its own, so its tail absorbs the whole
2// argument and nothing is forgotten -- the RFC-0121 section 4 forget rule does not apply, exactly
3// as it does not for the anonymous `{ ..R }`. A whole struct with a non-`Copy` field passes.
4struct Handle { fd: i64, name: String }
5
6fun any_handle<row R>(h: Handle.{ ..R }) -> i64 { 1 }
7
8fun fd_of<row R, row Rest>(h: Handle.{ ..R }) -> i64 where R = { fd: i64, ..Rest } { h.fd }
9
10fun read_fd<row R>(h: Handle.{ fd, ..R }) -> i64 { h.fd }
11
12fun anon<row R>(d: { ..R }) -> i64 { 1 }
13
14fun main() {
15 assert(any_handle(Handle { fd = 3, name = "x".to_string() }) == 1);
16 assert(fd_of(Handle { fd = 3, name = "x".to_string() }) == 3);
17 assert(anon({ fd = 3, name = "x".to_string() }) == 1);
18
19 // a residual with a Copy remainder also passes a named-field projection
20 let h := Handle { fd = 5, name = "y".to_string() };
21 let n := h.name;
22 assert(read_fd(h) == 5);
23 assert(n == "y");
24}
passes
1// Literal rows x field-wise bounds (RFC-0180, RFC-0123, metel-core#1493): `where all R: Copy`
2// names the tail of a literal row argument, and a call whose remainder is Copy is accepted.
3record Req<row R> { data: { ..R } }
4
5fun id_of<row R>(r: Req<..{ id: i64, ..R }>) -> (i64, i64) where all R: Copy {
6 (r.data.id, r.data.id)
7}
8
9fun main() {
10 let r := Req { data = { id = 4, n = 1 } };
11 assert(id_of(r).0 == 4);
12}
passes
1// Literal rows x closures x matching x chained row extension (RFC-0180, #1472): a closure builds
2// a request through a consuming setter, and the result's field is matched on.
3record Req<row R> { data: { ..R } }
4
5extend<row R: !{ host }> Req<..R> {
6 fun host(self, h: String) -> Req<..{ host: String, ..R }> {
7 Req { data = { host = h, ..self.data } }
8 }
9}
10
11fun main() {
12 let build := |h: String| { Req { data = {} }.host(h) };
13 let r := build("z".to_string());
14 match (r.data.host.len()) {
15 1 => { assert(r.data.host == "z"); },
16 _ => { assert(false); },
17 }
18}
passes
1// Literal rows x ownership x patterns (RFC-0180): a consuming setter chain, then the result is
2// destructured field by field.
3record Req<row R> { data: { ..R } }
4
5extend<row R: !{ host }> Req<..R> {
6 fun host(self, h: String) -> Req<..{ host: String, ..R }> {
7 Req { data = { host = h, ..self.data } }
8 }
9}
10
11fun main() {
12 let r := Req { data = {} }.host("a".to_string());
13 let { data } := r;
14 let { host } := data;
15 assert(host == "a");
16}
passes
1// Literal rows x dyn aspects (RFC-0180, RFC-0008): a record produced by a literal-row setter is
2// coerced to a `dyn` object and dispatched through a row-conditional impl.
3aspect Named {
4 fun name(&self) -> String;
5}
6
7record Req<row R> { data: { ..R } }
8
9extend<row R: { host: String, .. }> Req<..R>: Named {
10 fun name(&self) -> String { self.data.host.clone() }
11}
12
13extend<row R: !{ host }> Req<..R> {
14 fun host(self, h: String) -> Req<..{ host: String, ..R }> {
15 Req { data = { host = h, ..self.data } }
16 }
17}
18
19fun main() {
20 let r := Req { data = {} }.host("n".to_string());
21 let d: dyn Named := r;
22 assert(d.name() == "n");
23}
passes
1// Literal rows x field-wise record aspects (RFC-0180, #1350): a record whose row was extended by
2// a literal-row setter is cloned field by field.
3record Req<row R> { data: { ..R } }
4
5extend<row R: !{ host }> Req<..R> {
6 fun host(self, h: String) -> Req<..{ host: String, ..R }> {
7 Req { data = { host = h, ..self.data } }
8 }
9}
10
11fun main() {
12 let r := Req { data = {} }.host("a".to_string());
13 let c := r.clone();
14 assert(c.data.host == "a");
15 assert(r.data.host == "a");
16}
passes
1// Literal rows x generic functions (RFC-0180): setters whose added field has a generic type,
2// chained so the earlier fields survive in the written return row.
3record Box<row R> { data: { ..R } }
4
5fun first<T, row R: !{ first }>(b: Box<..R>, t: T) -> Box<..{ first: T, ..R }> {
6 Box { data = { first = t, ..b.data } }
7}
8
9fun second<T, row R: !{ second }>(b: Box<..R>, t: T) -> Box<..{ second: T, ..R }> {
10 Box { data = { second = t, ..b.data } }
11}
12
13fun main() {
14 let b := second(first(Box { data = {} }, 5), "x".to_string());
15 assert(b.data.second == "x");
16 assert(b.data.first == 5);
17}
passes
1// Literal rows x closures x written function types (RFC-0180, RFC-0154): a written function
2// type whose parameter and return carry literal rows, implemented by an annotated closure.
3record Req<row R> { data: { ..R } }
4
5fun apply<row R: !{ host }>(
6 r: Req<..R>,
7 f: |Req<..R>| -> Req<..{ host: String, ..R }>,
8) -> Req<..{ host: String, ..R }> {
9 f(r)
10}
11
12fun main() {
13 let r := Req { data = { port = 1 } };
14 let out := apply(r, |q: Req<..{ port: i64 }>| { Req { data = { host = "h".to_string(), ..q.data } } });
15 assert(out.data.host == "h");
16 assert(out.data.port == 1);
17}
passes
1record Packet<row R> { data: { ..R } }
2type EmptyRow := {};
3type EmptyPacket := Packet<{}>;
4type Envelope<T> := (T, i64);
5fun same<row R>(value: Envelope<{ ..R }>) -> Envelope<{ ..R }> { value }
6fun copy_twice<row R>(value: { ..R }) -> Envelope<({ ..R }, { ..R })>
7where all R: Copy { ((value, value), 11) }
8fun strip<row R, row Rest>(value: Packet<..R>) -> Packet<..Rest>
9where R = { token: String, ..Rest } {
10 let { token: _, ..rest } := value.data;
11 Packet { data = rest }
12}
13fun identity<T>(value: Envelope<T>) -> Envelope<T> { value }
14aspect Carrier { fun pass<T>(self, value: Envelope<T>) -> Envelope<T>; }
15struct Courier {}
16extend Courier: Carrier { fun pass<T>(self, value: Envelope<T>) -> Envelope<T> { value } }
17fun main() {
18 let wrapped := same(({ text = "owned", count = 2 }, 3));
19 let value := wrapped.0;
20 assert(value.count == 2);
21 assert(value.text == "owned");
22 let pair := copy_twice({ count = 5 });
23 assert(pair.0.0.count + pair.0.1.count == 10);
24 let empty_pair := copy_twice({});
25 let empty: EmptyRow := empty_pair.0.0;
26 let stripped := strip(Packet { data = { token = "gone", text = "kept" } });
27 assert(stripped.data.text == "kept");
28 let stripped_empty: EmptyPacket := strip(Packet { data = { token = "gone" } });
29 let envelope := Courier {}.pass(identity((stripped_empty, 9)));
30 assert(envelope.1 == 9);
31}
passes
1// RFC-0121: row extension permits the tail before newly-added fields.
2record Box<row R> {
3 value: { ..R, auth: String },
4}
5
6fun main() {
7 let boxed := Box { value = { id = 1, auth = "ok" } };
8 assert(boxed.value.id == 1);
9 assert(boxed.value.auth == "ok");
10}
passes
1// metel-core#1425: row tails are alias parameters, not free names after expansion.
2record Packet<row R> { data: { ..R } }
3type Row<row R> := { ..R };
4type Tagged<T, row R> := { tag: T, ..R };
5type PacketAlias<row R> := Packet<..R>;
6type Nested<row R> := (Row<..R>, PacketAlias<..R>);
7fun identity<row R>(value: Row<..R>) -> Row<..R> { value }
8fun tagged<T, row R>(tag: T, value: Row<..R>) -> Tagged<T, ..R>
9where R: !{tag} { { tag = tag, ..value } }
10fun main() {
11 let empty: Row<{}> := identity({});
12 let value: Row<{ count: i64, text: String }> := identity({ count = 7, text = "owned" });
13 assert(value.count == 7);
14 assert(value.text == "owned");
15 let tagged_value := tagged(9, { text = "tail" });
16 assert(tagged_value.tag == 9);
17 assert(tagged_value.text == "tail");
18 let packet: PacketAlias<{}> := Packet { data = {} };
19 let pair: Nested<{ count: i64 }> := ({ count = 3 }, Packet { data = { count = 5 } });
20 assert(pair.0.count + pair.1.data.count == 8);
21}
passes
1// RFC-0121 item 6: native functions carry open-row parameter bounds on their
2// schemes, just as ordinary free functions do.
3
4native(@std.core.println) fun shout<row R>(x: { a: i64, ..R });
5
6fun main() {}
passes
1import helper::make;
2import helper::strip;
3import helper::callback;
4import helper::EmptyPacket;
5import helper::Callback;
6
7fun main() {
8 let empty: EmptyPacket := strip(make(7, { token = "gone" }));
9 let run: Callback := callback(empty);
10 assert(run() == 7);
11 let with_tail := strip(make(9, { token = "gone", samples = [1, 2] }));
12 assert(with_tail.id == 9);
13 assert(with_tail.data.samples[1] == 2);
14}
passes
1import model::Packet as Renamed;
2import model::inspect;
3import model::identity;
4fun main() {
5 assert(inspect(&Renamed { data = {} }) == 1);
6 let value: Renamed<{ token: String }> := identity(Renamed { data = { token = "owned" } });
7 assert(inspect(&value) == 1);
8 assert(value.data.token == "owned");
9}
passes
Legality Rule №2
A row literal's trailing ..R (or anonymous ..) extends that row with whatever
fields R carries. A where R = { label: Type, ..Rest } equation decomposes a row
variable R into one named field and a remainder Rest, and simultaneously bounds R
to carry at least that field (equivalent to R: { label: Type, .. }).
Referenced by: rfc-0121
Tested by (12)
1// RFC-0121 remainder (metel-core#1306, #1310): a nominal field may extend a
2// row parameter. Constructing the nominal type infers the tail from the
3// record's fields that the extension does not name.
4
5record Wrap<row R> { data: { x: i64, ..R } }
6
7fun main() {
8 var wrap := Wrap { data = { x = 1, y = 2 } };
9 wrap.data.x := 3;
10 assert(wrap.data.x == 3);
11 assert(wrap.data.y == 2);
12 println("ok");
13}
passes
1// metel-core#1444: `where all R: A` constrains the fields of a row that is the row
2// argument of a named record (`Packet<..R>`), not only an open-row parameter's tail.
3aspect Mark { fun mark(&self) -> i64; }
4record Packet<row R> { data: { ..R } }
5
6extend<row R> Packet<..R>: Mark where all R: Copy {
7 fun mark(&self) -> i64 {
8 // the grant lets the inner row be copied out of a reference
9 let first := self.data;
10 let second := self.data;
11 1
12 }
13}
14
15fun count<row R>(p: &Packet<..R>) -> i64 where all R: Copy { 1 }
16
17fun both<row R>(p: Packet<..R>) -> ({ ..R }, { ..R }) where all R: Copy {
18 let d := p.data;
19 (d, d)
20}
21
22fun main() {
23 let p := Packet { data = { a = 1, flag = true } };
24 assert(p.mark() == 1);
25 assert(count(&p) == 1);
26 let pair := both(p);
27 assert(pair.0.a + pair.1.a == 2);
28 let empty := Packet { data = {} };
29 assert(count(&empty) == 1);
30 assert(empty.mark() == 1);
31}
passes
1// RFC-0121 / #1381: anonymous row arguments are fresh in reusable type positions,
2// including return types and local annotations.
3record Session<row R> { id: i64, data: { ..R } }
4
5fun make() -> Session<..> {
6 let typed: Session<..> := Session { id = 1, data = { n = 1 } };
7 typed
8}
9
10fun main() {
11}
passes
1// RFC-0179: `Handle.{ fd, ..R }` given a whole `Handle` takes it whole; `name` is not `Copy` and
2// is the callee's to drop.
3struct Handle { fd: i64, name: String }
4
5fun read_fd<row R>(h: Handle.{ fd, ..R }) -> i64 { h.fd }
6
7fun main() {
8 let h := Handle { fd = 3, name = "x".to_string() };
9 assert(read_fd(h) == 3);
10}
passes
1// metel-core#1460: `Handle.{ ..R }` names no field of its own, so its tail absorbs the whole
2// argument and nothing is forgotten -- the RFC-0121 section 4 forget rule does not apply, exactly
3// as it does not for the anonymous `{ ..R }`. A whole struct with a non-`Copy` field passes.
4struct Handle { fd: i64, name: String }
5
6fun any_handle<row R>(h: Handle.{ ..R }) -> i64 { 1 }
7
8fun fd_of<row R, row Rest>(h: Handle.{ ..R }) -> i64 where R = { fd: i64, ..Rest } { h.fd }
9
10fun read_fd<row R>(h: Handle.{ fd, ..R }) -> i64 { h.fd }
11
12fun anon<row R>(d: { ..R }) -> i64 { 1 }
13
14fun main() {
15 assert(any_handle(Handle { fd = 3, name = "x".to_string() }) == 1);
16 assert(fd_of(Handle { fd = 3, name = "x".to_string() }) == 3);
17 assert(anon({ fd = 3, name = "x".to_string() }) == 1);
18
19 // a residual with a Copy remainder also passes a named-field projection
20 let h := Handle { fd = 5, name = "y".to_string() };
21 let n := h.name;
22 assert(read_fd(h) == 5);
23 assert(n == "y");
24}
passes
1fun duplicate_rest<row R, row Rest>(value: { ..R }) -> ({ ..Rest }, { ..Rest })
2where R = { token: String, ..Rest }, all Rest: Copy {
3 let { token: _, ..rest } := value;
4 let first := rest;
5 let second := rest;
6 (first, second)
7}
8aspect Relay { fun pass<T>(self, value: T) -> T; }
9struct Courier {}
10extend Courier: Relay {
11 fun pass<T>(self, value: T) -> T { value }
12}
13fun extract<T>(value: { payload: T }) -> T {
14 let { payload, ..rest } := value;
15 let empty: {} := rest;
16 payload
17}
18struct Owned { text: String }
19fun forward<T>(value: T) -> T { value }
20fun main() {
21 assert(extract({ payload = "generic owned" }) == "generic owned");
22 assert(extract({ payload = 17 }) == 17);
23 let empty_pair := duplicate_rest({ token = "discarded" });
24 let left: {} := empty_pair.0;
25 let right: {} := empty_pair.1;
26 let pair := duplicate_rest({ token = "discarded", count = 8, enabled = true });
27 assert(pair.0.count == 8);
28 assert(pair.1.enabled);
29 let { token: _, ..empty } := { token = "owned" };
30 let forwarded: {} := Courier {}.pass(empty);
31 let data := { token = "discarded", payload = "kept" };
32 let var { token: _, ..tail } := data;
33 tail.payload := "restored";
34 assert(Courier {}.pass(tail).payload == "restored");
35 let owned := Owned { text = "removed" };
36 let text := owned.text;
37 let forwarded_nominal: Owned.{} := Courier {}.pass(forward(owned));
38 assert(text == "removed");
39}
passes
1record Row { id: i64, name: String }
2fun key_of<row R>(v: { k: i64, ..R }) -> i64 where all R: Copy { v.k }
3fun main() {
4 let rows: [Row] := [Row { id = 1, name = "a" }, Row { id = 2, name = "b" }];
5 var names := "";
6 for (r in rows) { names := names + r.name + r.id.to_string(); }
7 assert(names == "a1b2");
8 let copies := [{ k = 1, v = true }, { k = 2, v = false }];
9 var s := 0;
10 for (c in copies) { s := s + key_of(c); }
11 assert(s == 3);
12}
passes
1fun head<row R>(v: { xs: [i64; 3], ..R }) -> i64 where all R: Copy {
2 let w := v;
3 v.xs[0] + w.xs[1]
4}
5fun main() { assert(head({ xs = [1, 2, 3], flag = true }) == 3); }
passes
1fun need_id<row R>(v: { id: i64, ..R }) -> i64 { v.id }
2fun main() {
3 need_id({ name = "x" }); // ERROR[T0012]
4}
typecheck errorT0012at 3“requires label `id`”
1record Session<row R> { id: i64, data: { ..R } }
2extend<row R: { token: String, .. }> Session<..R> {
3 fun kind(self) -> i64 { 1 }
4}
5extend<row R: !{ token }> Session<..R> {
6 fun kind(self) -> i64 { 2 }
7}
8aspect Mark { fun mark(&self) -> i64; }
9extend<row R> { ..R }: Mark where all R: Copy {
10 fun mark(&self) -> i64 { 7 }
11}
12fun marked<row R>(value: &{ ..R }) -> i64 where all R: Copy {
13 value.mark()
14}
15fun remove_token<row R, row Rest>(session: Session<..R>) -> Session<..Rest>
16where R = { token: String, ..Rest } {
17 let { id, data } := session;
18 let { token: _, ..rest } := data;
19 let cleared: Session<..Rest> := Session { id = id, data = rest };
20 cleared
21}
22fun has_token<row R: { token: String, .. }>(session: Session<..R>) -> i64 {
23 session.kind()
24}
25fun main() {
26 assert(has_token(Session { id = 3, data = { token = "t", text = "owned" } }) == 1);
27 assert(remove_token(Session { id = 3, data = { token = "t" } }).kind() == 2);
28 assert(remove_token(Session { id = 5, data = { token = "t", text = "kept" } }).kind() == 2);
29 assert(marked(&{}) == 7);
30 assert(marked(&{ count = 4, flag = true }) == 7);
31}
passes
1// RFC-0121 / #1380: an open row may occur in a nominal field type.
2record Box<row R> {
3 value: { x: i64, ..R },
4}
5
6fun main() {
7 let boxed := Box { value = { x = 1, extra = "ok" } };
8 assert(boxed.value.x == 1);
9}
passes
1// RFC-0121 / #1380: an open row may be carried through a reusable return type.
2fun identity<row R>(value: { x: i64, ..R }) -> ({ x: i64, ..R }, i64) {
3 let typed: { x: i64, ..R } := value;
4 (typed, 1)
5}
6
7fun tail_identity<row R>(value: { ..R }) -> { ..R } {
8 value
9}
10
11fun main() {
12 let source := { x = 1, extra = "ok" };
13 let result := identity(source);
14 assert(result.0.x == 1);
15
16 let tail := tail_identity({ extra = 2 });
17 assert(tail.extra == 2);
18}
passes
Legality Rule №3
A by-value parameter whose type has an open row tail ({ x: Type, ..R }, or
Handle.{ fd, ..R }) takes its argument whole. The call needs only that the argument has
the named fields at the declared types; the fields the parameter does not name are the
callee's to move, return or drop, so they need not be Copy, and a generic body that
forwards such a parameter to another by-value open-row function needs no where all R: Copy.
A parameter taken through a reference (&{ x: Type, ..R }, &var) never moves the value
and forgets nothing.
A nominal argument whose type has a Drop impl is the one exception: dropping it inside the
callee needs drop dispatch against a narrowed residual (RFC-0137 §5, #949), which is not
implemented, so passing it to an open-row parameter is rejected with T0036. A plain record
that only contains a Drop value is unaffected.
Referenced by: rfc-0121, rfc-0179
Tested by (20)
1// RFC-0179: an open-row parameter owns its whole argument, so a generic body can pass it on to
2// another by-value open-row function without `where all R: Copy`; nothing is forgotten.
3fun keep_x<row R>(value: { x: i64, ..R }) -> i64 { value.x }
4
5fun forward<row R>(value: { x: i64, ..R }) -> i64 {
6 keep_x(value)
7}
8
9fun main() {
10 assert(forward({ x = 4, label = "kept".to_string() }) == 4);
11}
passes
1// RFC-0121 §4 / RFC-0123: `where all R: Copy` grants the definition-time
2// entitlement to narrow an abstract open-row value by value.
3fun keep_x<row R>(value: { x: i64, ..R }) -> i64 { value.x }
4
5fun forward<row R>(value: { x: i64, ..R }) -> i64 where all R: Copy {
6 keep_x(value)
7}
8
9fun main() {
10 assert(forward({ x = 1, y = 2 }) == 1);
11}
passes
1// RFC-0179: a method's open-row parameter takes its argument whole, so a record with a
2// non-`Copy` field the parameter does not name is accepted; the callee owns `name`.
3struct Handle { fd: i64 }
4
5extend Handle {
6 fun add<row R>(self, t: { x: i64, ..R }) -> i64 { self.fd + t.x }
7}
8
9fun main() {
10 let h := Handle { fd = 1 };
11 let r := { x = 2, name = "s".to_string() };
12 assert(h.add(r) == 3);
13}
passes
1// RFC-0179: `Handle.{ fd, ..R }` given a whole `Handle` takes it whole; `name` is not `Copy` and
2// is the callee's to drop.
3struct Handle { fd: i64, name: String }
4
5fun read_fd<row R>(h: Handle.{ fd, ..R }) -> i64 { h.fd }
6
7fun main() {
8 let h := Handle { fd = 3, name = "x".to_string() };
9 assert(read_fd(h) == 3);
10}
passes
1// RFC-0179 section 1.5: passing a `Drop` brand to a projection-tailed parameter would drop a
2// narrowed `Handle.{ fd }`, which needs drop dispatch against a narrowed residual (#949).
3struct Handle { fd: i64, name: String }
4
5extend Handle: Drop {
6 fun drop(&var self) { }
7}
8
9fun read_fd<row R>(h: Handle.{ fd, ..R }) -> i64 { h.fd }
10
11fun main() {
12 let h := Handle { fd = 3, name = "x".to_string() };
13 let _ := read_fd(h); // ERROR[T0036]
14}
typecheck errorT0036at 13“drop dispatch against a narrowed residual”
1// RFC-0179: an open-row parameter owns the whole argument. `label` is not `Copy`, and the
2// callee drops it with the parameter; nothing is rejected at the call.
3fun get_x<row R>(p: { x: f64, ..R }) -> f64 { p.x }
4
5fun main() {
6 let r := { x = 1.0, label = "gone".to_string() };
7 assert(get_x(r) == 1.0);
8}
passes
1// RFC-0179 section 1.5: only a nominal `Drop` brand is rejected. A plain record that merely
2// contains a `Drop` value in its remainder is dropped whole, which is ordinary.
3struct Handle { fd: i64 }
4
5extend Handle: Drop {
6 fun drop(&var self) { }
7}
8
9fun get_x<row R>(p: { x: i64, ..R }) -> i64 { p.x }
10
11fun main() {
12 let r := { x = 7, handle = Handle { fd = 1 } };
13 assert(get_x(r) == 7);
14}
passes
1// RFC-0179: the residual-projection tail form (`Handle.{ fd, ..R }`) owns the whole `Handle`
2// too, so the non-`Copy` `name` is not forgotten and the call is accepted.
3struct Handle { fd: i64, name: String }
4
5fun get_fd<row R>(h: Handle.{ fd, ..R }) -> i64 { h.fd }
6
7fun main() {
8 let h := Handle { fd = 1, name = "gone".to_string() };
9 assert(get_fd(h) == 1);
10}
passes
1record Batch { samples: [i64; 3], label: String }
2type Samples := [i64; 3];
3type Sum := once || -> i64;
4
5fun sum_head<row R>(value: { samples: Samples, ..R }) -> i64 where all R: Copy {
6 var total := 0;
7 for (sample in value.samples) { total := total + sample; }
8 total
9}
10
11fun main() {
12 let batch := Batch { samples = [1, 2, 3], label = "batch" };
13 let { label, ..rest } := batch;
14 assert(label == "batch");
15 let copied := rest.clone();
16 let callback: Sum := [copied] once || { sum_head(copied) };
17 assert(callback() == 6);
18 assert(sum_head({ samples = rest.samples, extra = true }) == 6);
19 match (rest.samples) {
20 [first, second, third] => assert(first + second + third == 6),
21 }
22}
passes
1record User { id: i64, name: String, count: i64 }
2type UserAlias := User;
3type Callback := once || -> i64;
4
5fun clone_head<row R>(value: { id: i64, ..R }) -> { id: i64, ..R }
6where all R: Clone {
7 value.clone()
8}
9
10fun main() {
11 let row_clone := clone_head({ id = 9 });
12 assert(row_clone.id == 9);
13 let row_clone_with_tail := clone_head({ id = 3, count = 5 });
14 assert(row_clone_with_tail.id + row_clone_with_tail.count == 8);
15
16 let user: UserAlias := User { id = 3, name = "moved", count = 5 };
17 let name := user.name;
18 let residual := user.clone();
19 assert(name == "moved");
20 assert(residual.id + residual.count == 8);
21 let callback: Callback := [residual] once || {
22 residual.id + residual.count
23 };
24 assert(callback() == 8);
25}
passes
1// RFC-0179: a closure that captures an open-row parameter and passes it on forgets nothing, so
2// no `where all R: Copy` is needed.
3type Callback := once || -> i64;
4fun head<row R>(value: { id: i64, ..R }) -> i64 { value.id }
5fun accepted<row R>(value: { id: i64, ..R }) -> i64 {
6 let callback: Callback := [value] once || {
7 head(value)
8 };
9 callback()
10}
11fun main() {
12 assert(accepted({ id = 5, text = "kept".to_string() }) == 5);
13}
passes
1// Anonymous-tail open records x type aliases x whole-record rows (RFC-0181, RFC-0160, RFC-0179):
2// an alias whose right-hand side has an anonymous tail is a fresh row at each use, as a
3// parameter and as a binding annotation, and the String remainder is carried whole.
4type Loose := { x: i64, .. };
5
6fun take(p: Loose) -> i64 { p.x }
7
8fun main() {
9 assert(take({ x = 1, s = "n".to_string() }) == 1);
10 let a: Loose := { x = 2, t = 3 };
11 assert(a.x == 2);
12}
passes
1// Anonymous-tail annotations x whole-record open rows (RFC-0181 decision, RFC-0179): a binding
2// annotated with an anonymous tail keeps its extra fields, and is then passed by value to an
3// open-row parameter whose remainder holds a String.
4fun take<row R>(p: { x: i64, ..R }) -> i64 { p.x }
5
6fun main() {
7 let a: { x: i64, .. } := { x = 1, name = "n".to_string() };
8 assert(take(a) == 1);
9}
passes
1// Whole-record open rows x type aliases x generics (RFC-0179, RFC-0160): a row-parameterised
2// alias spells the open-row parameter, and a record with a String remainder is passed whole.
3type Wide<row R> := { x: i64, ..R };
4
5fun take<row R>(p: Wide<..R>) -> i64 { p.x }
6
7fun main() {
8 let r := { x = 1, name = "n".to_string() };
9 assert(take(r) == 1);
10}
passes
1// Whole-record open rows x ownership (RFC-0179): the argument moves into the parameter whole, so
2// a later read of one of its non-Copy fields is a use of a moved value.
3fun take<row R>(p: { x: i64, ..R }) -> i64 { p.x }
4
5fun main() {
6 let r := { x = 1, name = "s".to_string() };
7 let _ := take(r);
8 let _ := r.name; // ERROR[T0019]
9}
typecheck errorT0019at 8“moved value”
1// Whole-record open rows x closures (RFC-0179): a closure with an open-row parameter owns the
2// whole argument, so records with non-Copy fields of different shapes are all accepted.
3fun main() {
4 let describe := |p: { id: i64, .. }| { p.id };
5 let r := { id = 7, label = "owned".to_string(), tags = ["a".to_string()] };
6 assert(describe(r) == 7);
7 let r2 := { id = 9, other = 1 };
8 assert(describe(r2) == 9);
9}
passes
1// Whole-record open rows x row-conditional impls (RFC-0179, RFC-0121 section 3): a method of a
2// conditional impl takes an open-row parameter whose remainder holds a String.
3record Cfg<row R> { data: { ..R } }
4
5extend<row R: { port: i64, .. }> Cfg<..R> {
6 fun with<row Q>(self, extra: { n: i64, ..Q }) -> i64 { self.data.port + extra.n }
7}
8
9fun main() {
10 let c := Cfg { data = { port = 80, host = "h".to_string() } };
11 assert(c.with({ n = 2, note = "x".to_string() }) == 82);
12}
passes
1// Whole-record open rows x pattern matching x rest patterns (RFC-0179, RFC-0178): one arm of a
2// match destructures the remainder out of the owned parameter, the other leaves it to drop.
3fun split<row R>(p: { kind: i64, ..R }) -> i64 {
4 match (p.kind) {
5 1 => {
6 let { kind: _, ..rest } := p;
7 10
8 },
9 _ => 20,
10 }
11}
12
13fun main() {
14 assert(split({ kind = 1, text = "t".to_string() }) == 10);
15 assert(split({ kind = 2, text = "u".to_string() }) == 20);
16}
passes
1// Whole-record open rows x rest patterns x ownership (RFC-0179, RFC-0178): the callee owns the
2// whole record, destructures the remainder out and returns it; the caller owns the result.
3fun strip<row Rest>(d: { token: String, ..Rest }) -> { ..Rest } {
4 let { token: _, ..rest } := d;
5 rest
6}
7
8fun main() {
9 let d := { token = "t".to_string(), user = "u".to_string(), n = 3 };
10 let r := strip(d);
11 assert(r.user == "u");
12 assert(r.n == 3);
13}
passes
1// Residual privacy x partial moves x whole-record open rows (metel-core#1488, RFC-0179): code
2// outside the module moves a public non-Copy field out, which narrows the value to a residual
3// that hides the private field; the residual is passed on generically and to a projection-tail
4// parameter that names only a public field.
5import ticket::Ticket;
6import ticket::make;
7
8fun id_of<row R>(t: Ticket.{ id, ..R }) -> i64 { t.id }
9fun pass<T>(x: T) -> T { x }
10
11fun main() {
12 let t := make();
13 let label := t.label;
14 let kept := pass(t);
15 assert(id_of(kept) == 1);
16 assert(label == "l");
17}
passes
Legality Rule №5
An open record type whose tail is anonymous, { x: T, .. } or { .. }, denotes a record
with at least the named fields and a remainder that is a fresh row at each occurrence. It
is accepted:
- as the type of a function or method parameter;
- as the annotation of a
letorvarbinding; - as the annotation of a closure parameter; and
- as the right-hand side of a type alias, where each use of the alias is its own fresh row.
In a let or var annotation the fresh row is solved by the initializer, so the binding has
the initializer's type and every one of its fields stays readable; the annotation names the
fields that must be present and does not narrow the value. The field types written in the
annotation apply to the initializer, so { x: u8, .. } makes x a u8. A closure whose parameter carries
such an annotation is polymorphic in that row: each call instantiates it afresh, so one closure
accepts records of different shapes.
fun main() {
let a: { x: i64, .. } := { x = 1, y = "s".to_string() };
assert(a.y == "s");
let get := |p: { x: i64, .. }| { p.x };
assert(get({ x = 1, y = 2 }) == 1);
assert(get({ x = 5 }) == 5);
assert(get({ x = 7, flag = true }) == 7);
}
An open record with an anonymous tail is rejected with T0032 as a return type, inside a
written function type, and as a record or struct field. In a return type or function type
the tail has no construct that solves it, and a field's type would differ between values of one
nominal type. A named tail ({ x: T, ..R }, R declared row) is not affected by this rule
and is accepted wherever R is in scope.
fun make() -> { x: i64, .. } {
{ x = 1, y = 2 }
}
fun main() {
let _ := make();
}
Referenced by: rfc-0181
Tested by (13)
1// metel-core#1478: the fixed fields of an anonymous-tail annotation are real types. `x` is a `u8`,
2// so reading it where an `i32` is required is a mismatch.
3fun main() {
4 let c: { x: u8, .. } := { x = 200, y = 2 };
5 let e: i32 := c.x; // ERROR[T0001]
6}
typecheck errorT0001at 5“cannot unify u8 with i32”
1// metel-core#1478: a closure with an open-row parameter still requires the named fields at the call.
2fun main() {
3 let get := |p: { x: i64, .. }| { p.x };
4 let _ := get({ x = 1 });
5 let _ := get({ y = 1 }); // ERROR[T0001]
6}
typecheck errorT0001at 5“the record is missing field `x` (expected `{ x: i64, .. }`, found `{ y: integer }`)”
1// metel-core#1478: a closure parameter annotated with an anonymous open record generalizes with
2// the closure, so one closure accepts records of different shapes. This is the only way to write
3// a row-polymorphic closure; `|p| { p.x }` cannot infer the struct.
4fun main() {
5 let get := |p: { x: i64, .. }| { p.x };
6 assert(get({ x = 1, y = 2 }) == 1);
7 assert(get({ x = 5 }) == 5);
8 assert(get({ x = 7, name = "n".to_string() }) == 7);
9}
passes
1// metel-core#1478: nor does one in a written function type (RFC-0181).
2fun apply(f: |{ x: i64, .. }| -> i64) -> i64 { // ERROR[T0032]
3 1
4}
5fun main() { let _ := 1; }
typecheck errorT0032at 2“share the one `R` between the parameter and the result”
1// metel-core#1478: an open record with an anonymous tail is a valid annotation on a `let` or
2// `var`. The tail is a fresh inference variable, so the binding has the initializer's full type
3// and the extra fields stay readable. It used to fail on an internal `<open-row>` placeholder.
4fun main() {
5 let a: { x: i64, .. } := { x = 1, y = "s".to_string() };
6 assert(a.x == 1);
7 assert(a.y == "s");
8
9 var b: { x: i64, .. } := { x = 1, y = 2 };
10 b.x := 9;
11 assert(b.x == 9 && b.y == 2);
12
13 // the annotation's fixed fields still type the initializer: `x` is a `u8`, not an `i64`
14 let c: { x: u8, .. } := { x = 200, y = 2 };
15 let d: u8 := c.x;
16 assert(d == 200);
17}
passes
1// metel-core#1478: the annotation still requires its fixed fields.
2fun main() {
3 let _ := 1;
4 let a: { x: i64, .. } := { z = 1 }; // ERROR[T0001]
5}
typecheck errorT0001at 4“the record is missing field `x` (expected `{ x: i64, .. }`, found `{ z: integer }`)”
1// metel-core#1478: an anonymous open record in a return type has no settled meaning (RFC-0181).
2fun make() -> { x: i64, .. } { // ERROR[T0032]
3 { x = 1, y = 2 }
4}
5fun main() { let _ := make(); }
typecheck errorT0032at 2“omit the return type so the exact record is inferred”
1// Anonymous-tail open records x type aliases x whole-record rows (RFC-0181, RFC-0160, RFC-0179):
2// an alias whose right-hand side has an anonymous tail is a fresh row at each use, as a
3// parameter and as a binding annotation, and the String remainder is carried whole.
4type Loose := { x: i64, .. };
5
6fun take(p: Loose) -> i64 { p.x }
7
8fun main() {
9 assert(take({ x = 1, s = "n".to_string() }) == 1);
10 let a: Loose := { x = 2, t = 3 };
11 assert(a.x == 2);
12}
passes
1// Anonymous-tail annotations x whole-record open rows (RFC-0181 decision, RFC-0179): a binding
2// annotated with an anonymous tail keeps its extra fields, and is then passed by value to an
3// open-row parameter whose remainder holds a String.
4fun take<row R>(p: { x: i64, ..R }) -> i64 { p.x }
5
6fun main() {
7 let a: { x: i64, .. } := { x = 1, name = "n".to_string() };
8 assert(take(a) == 1);
9}
passes
1// Whole-record open rows x closures (RFC-0179): a closure with an open-row parameter owns the
2// whole argument, so records with non-Copy fields of different shapes are all accepted.
3fun main() {
4 let describe := |p: { id: i64, .. }| { p.id };
5 let r := { id = 7, label = "owned".to_string(), tags = ["a".to_string()] };
6 assert(describe(r) == 7);
7 let r2 := { id = 9, other = 1 };
8 assert(describe(r2) == 9);
9}
passes
1// metel-core#1484: and so does an aspect method declaration.
2aspect Source {
3 fun view(&self) -> { x: i64, .. }; // ERROR[T0032]
4}
5
6fun main() { let _ := 1; }
typecheck errorT0032at 3“An aspect method declaration does not infer its return type”
1// metel-core#1484: and a closure's return annotation.
2fun main() {
3 let f := |a: i64| -> { x: i64, .. } { { x = a, y = 1 } }; // ERROR[T0032]
4 let _ := f(1);
5}
typecheck errorT0032at 3“omit the closure's return annotation so the exact record is inferred”
1// metel-core#1484: a method's return type gets the same T0032 text as a free function's,
2// naming the alternatives that work for a method: omitting the type returns (), it does not infer.
3struct Box { n: i64 }
4
5extend Box {
6 fun view(&self) -> { x: i64, .. } { // ERROR[T0032]
7 { x = self.n, y = 1 }
8 }
9}
10
11fun main() { let _ := 1; }
typecheck errorT0032at 6“A method does not infer its return type”
Field-wise row constraints
A constraint applies an aspect bound to every field of a row, rather than to the
row's own type as a whole — distinct from a row bound (R: { x: f64, .. }), which
constrains the row's shape (which labels it has), not its contents (what the field
types can do):
extend<row R> { ..R }: Display where all R: Display { … }
fun consume<row R>(r: { ..R }) where all R: Copy { … }
all R: A holds when every field type in R satisfies A; it holds vacuously on an
empty row. A per-field exception composes through ordinary row decomposition (where R = { x: T, ..Rest } where T: SomeOtherBound, all Rest: A) rather than needing a
heterogeneous form of its own.
R must be a row the declaration can walk: the tail of an open-row parameter
({ ..R }), the remainder of a where decomposition, the tail of an extend target
(extend<row R> { ..R }), or a declared row generic used as the row argument of a
named type (Packet<..R>), in a function signature or an extend target. A row that
none of these positions mentions has no fields to constrain and is rejected.
aspect Mark { fun mark(&self) -> i64; }
record Packet<row R> { data: { ..R } }
fun duplicate<row R>(p: Packet<..R>) -> ({ ..R }, { ..R }) where all R: Copy {
let d := p.data;
(d, d)
}
extend<row R> Packet<..R>: Mark where all R: Copy { fun mark(&self) -> i64 { 1 } }
fun main() {
let p := Packet { data = { a = 1 } };
assert(p.mark() == 1);
let pair := duplicate(p);
assert(pair.0.a + pair.1.a == 2);
}
Formal rules
Legality Rule №1
where all R: Aspect is a WhereConstraint alternative, sibling to an ordinary bound
(ident : BoundList) and a row equation (ident = Type, §Open rows
above): it holds exactly when every field type in row R satisfies Aspect,
vacuously on an empty row. It constrains R's field contents; it does not by itself
constrain which labels R has (that is an ordinary row bound, composed separately).
Referenced by: rfc-0123
Tested by (29)
1// RFC-0123 sec1 (metel-core#1302): `where all R: Aspect` holds when every field
2// of the row `R` -- the argument's fields beyond the ones the parameter names --
3// satisfies the aspect. It is vacuous on an empty remainder, covers a named
4// record argument as well as an anonymous one, and in a `where R = { .., ..Rest }`
5// decomposition `all Rest: A` skips the decomposed labels and `all R: A` does not.
6
7aspect Tag { fun tag(self) -> i64; }
8extend i64: Tag { fun tag(self) -> i64 { 1 } }
9extend f64: Tag { fun tag(self) -> i64 { 2 } }
10
11record Item { id: i64, weight: f64 }
12
13fun id_of<row R>(p: { id: i64, ..R }) -> i64 where all R: Tag { p.id }
14
15fun token_of<row R, row Rest>(p: { ..R }) -> i64
16where R = { token: i64, ..Rest }, all Rest: Copy
17{
18 p.token
19}
20
21fun whole<row R, row Rest>(p: { ..R }) -> i64
22where R = { token: i64, ..Rest }, all R: Tag
23{
24 p.token
25}
26
27fun main() {
28 assert(id_of({ id = 7, w = 1.5, h = 3 }) == 7);
29 assert(id_of({ id = 8 }) == 8);
30 assert(token_of({ token = 4, a = 1, b = 2.0 }) == 4);
31 assert(whole({ token = 5, ratio = 0.5 }) == 5);
32 let item := Item { id = 9, weight = 2.0 };
33 assert(id_of(item) == 9);
34 println("ok");
35}
passes
1// RFC-0123 sec1 (`spec.types.generics.field-wise-row-constraints.legality-1`): `all R: A`
2// holds vacuously when the row has no fields beyond the ones the parameter names, and it
3// constrains the *contents* of the row, not which labels it has -- any labels will do, so
4// long as their types satisfy the aspect.
5
6aspect Tag { fun tag(self) -> i64; }
7extend i64: Tag { fun tag(self) -> i64 { 1 } }
8extend boolean: Tag { fun tag(self) -> i64 { 2 } }
9
10fun id_of<row R>(p: { id: i64, ..R }) -> i64 where all R: Tag { p.id }
11
12fun main() {
13 assert(id_of({ id = 1 }) == 1); // empty remainder
14 assert(id_of({ id = 2, a = 1 }) == 2); // one label
15 assert(id_of({ id = 3, zebra = true, apple = 5 }) == 3); // other labels, other Tag types
16 println("ok");
17}
passes
1// RFC-0123 sec1 (metel-core#1302): `where all R: Tag` is checked at the call --
2// `label: String` has no `Tag` impl, so the call is rejected and the error names
3// the field.
4
5aspect Tag { fun tag(self) -> i64; }
6extend i64: Tag { fun tag(self) -> i64 { 1 } }
7
8fun id_of<row R>(p: { id: i64, ..R }) -> i64 where all R: Tag { p.id }
9
10fun main() {
11 let _ := id_of({ id = 1, count = 2, label = "x".to_string() });
12}
typecheck errorT0012“field `label`”
1record Packet<row R> { data: { ..R } }
2fun count<row R>(p: &Packet<..R>) -> i64 where all R: Copy { 1 }
3fun main() {
4 let p := Packet { data = { a = "s".to_string() } };
5 let _ := count(&p); // ERROR[T0012]
6}
typecheck errorT0012at 5“field `a` of type `String` does not implement `Copy`”
1aspect Mark { fun mark(&self) -> i64; }
2record Packet<row R> { data: { ..R } }
3extend<row R> Packet<..R>: Mark where all R: Copy { fun mark(&self) -> i64 { 1 } }
4fun main() {
5 let p := Packet { data = { a = "s".to_string() } };
6 let _ := p.mark(); // ERROR[T0012]
7}
typecheck errorT0012at 6“field `a` of type `String` does not implement `Copy`”
1// metel-core#1444: `where all R: A` constrains the fields of a row that is the row
2// argument of a named record (`Packet<..R>`), not only an open-row parameter's tail.
3aspect Mark { fun mark(&self) -> i64; }
4record Packet<row R> { data: { ..R } }
5
6extend<row R> Packet<..R>: Mark where all R: Copy {
7 fun mark(&self) -> i64 {
8 // the grant lets the inner row be copied out of a reference
9 let first := self.data;
10 let second := self.data;
11 1
12 }
13}
14
15fun count<row R>(p: &Packet<..R>) -> i64 where all R: Copy { 1 }
16
17fun both<row R>(p: Packet<..R>) -> ({ ..R }, { ..R }) where all R: Copy {
18 let d := p.data;
19 (d, d)
20}
21
22fun main() {
23 let p := Packet { data = { a = 1, flag = true } };
24 assert(p.mark() == 1);
25 assert(count(&p) == 1);
26 let pair := both(p);
27 assert(pair.0.a + pair.1.a == 2);
28 let empty := Packet { data = {} };
29 assert(count(&empty) == 1);
30 assert(empty.mark() == 1);
31}
passes
1// RFC-0123 sec1 (metel-core#1302): in a decomposition, `all R: Copy` covers the
2// decomposed labels too (here `name: String`), where `all Rest: Copy` would not.
3
4fun pick<row R, row Rest>(p: { ..R }) -> i64
5where R = { id: i64, name: String, ..Rest }, all R: Copy
6{
7 p.id
8}
9
10fun main() {
11 let _ := pick({ id = 1, name = "x".to_string() });
12}
typecheck errorT0012“field `name`”
1record Packet<row R> { data: { ..R } }
2fun unrelated<row R>(n: i64) -> i64 where all R: Copy { n } // ERROR[T0012]
3fun main() {}
typecheck errorT0012at 2“must name the row tail”
1fun add<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
2 { ..r, token = "secret" }
3}
4
5fun forward<row R: !{token}>(r: { ..R }) -> i64 where all R: Copy {
6 let callback := [r] once || {
7 let added := add(r);
8 let copied := r;
9 7
10 };
11 callback()
12}
13
14fun main() {
15 assert(forward({ n = 1 }) == 7);
16 assert(forward({}) == 7);
17}
passes
1struct NotClone {}
2
3fun clone_head<row R>(value: { fixed: NotClone, ..R })
4where all R: Clone {
5 value.clone() // ERROR[T0035]
6}
7
8fun main() {}
typecheck errorT0035
1// RFC-0123 sec1 / metel-core#1302: `where all R: Aspect` on an impl whose target is
2// `{ ..R }` -- the impl applies to a record only when every field's type satisfies the
3// aspect. It holds for anonymous and nominal records alike and through an aspect bound.
4
5aspect Label { fun label(&self) -> String; }
6aspect Mark { fun mark(&self) -> i64; }
7
8extend i64: Mark { fun mark(&self) -> i64 { 1 } }
9extend f64: Mark { fun mark(&self) -> i64 { 2 } }
10
11extend<row R> { ..R }: Label where all R: Mark {
12 fun label(&self) -> String { "all marked".to_string() }
13}
14
15record Pair { a: i64, b: f64 }
16
17fun show<T: Label>(x: &T) -> String { x.label() }
18
19fun main() {
20 assert({ a = 1, b = 2.0 }.label() == "all marked");
21 assert({ a = 1 }.label() == "all marked");
22 let p := Pair { a = 1, b = 2.0 };
23 assert(p.label() == "all marked");
24 assert(show(&p) == "all marked");
25 println("ok");
26}
passes
1// RFC-0123 sec1 / metel-core#1302: `String` has no `Mark` impl, so the record
2// `{ a, s }` does not satisfy `all R: Mark` and the impl does not apply to it.
3
4aspect Label { fun label(&self) -> String; }
5aspect Mark { fun mark(&self) -> i64; }
6
7extend i64: Mark { fun mark(&self) -> i64 { 1 } }
8
9extend<row R> { ..R }: Label where all R: Mark {
10 fun label(&self) -> String { "all marked".to_string() }
11}
12
13fun main() {
14 let _ := { a = 1, s = "x".to_string() }.label();
15}
typecheck errorT0012“field `s`”
1type Pair<T> := (T, T);
2fun duplicate<row R>(value: { ..R }) -> Pair<{ ..R }> where all R: Copy {
3 (value, value)
4}
5fun main() { duplicate({ text = "owned" }); } // ERROR[T0012]
typecheck errorT0012“field `text` of type `String` does not implement `Copy`”
1record Wrapper<T> { inner: T, tag: String }
2fun inner_of<T: Copy>(w: Wrapper<T>) -> T {
3 let { tag: _, ..rest } := w;
4 rest.inner
5}
6fun sum_all<row R>(v: { a: i64, b: i64, ..R }) -> i64 where all R: Copy { v.a + v.b }
7aspect Source { type Item; fun take(self) -> Item; }
8struct Holder { value: { a: i64, b: i64, c: boolean } }
9extend Holder: Source { type Item := { a: i64, b: i64, c: boolean }; fun take(self) -> { a: i64, b: i64, c: boolean } { self.value } }
10fun use_source<S: Source<Item = { a: i64, b: i64, c: boolean }>>(s: S) -> i64 { sum_all(s.take()) }
11fun main() {
12 assert(inner_of(Wrapper { inner = 7, tag = "t" }) == 7);
13 assert(use_source(Holder { value = { a = 1, b = 2, c = true } }) == 3);
14}
passes
1aspect Mark { fun mark(&self) -> i64; }
2extend<row R> { ..R }: Mark where all R: Copy {
3 fun mark(&self) -> i64 { 7 }
4}
5fun unchecked<row R>(value: &{ ..R }) -> i64 { value.mark() } // ERROR[T0035]
6fun main() {}
typecheck errorT0035“conditional implementation requirements are not entailed”
1record Batch { samples: [i64; 3], label: String }
2type Samples := [i64; 3];
3type Sum := once || -> i64;
4
5fun sum_head<row R>(value: { samples: Samples, ..R }) -> i64 where all R: Copy {
6 var total := 0;
7 for (sample in value.samples) { total := total + sample; }
8 total
9}
10
11fun main() {
12 let batch := Batch { samples = [1, 2, 3], label = "batch" };
13 let { label, ..rest } := batch;
14 assert(label == "batch");
15 let copied := rest.clone();
16 let callback: Sum := [copied] once || { sum_head(copied) };
17 assert(callback() == 6);
18 assert(sum_head({ samples = rest.samples, extra = true }) == 6);
19 match (rest.samples) {
20 [first, second, third] => assert(first + second + third == 6),
21 }
22}
passes
1fun clone_row<row R>(value: &{ ..R }) -> { ..R } where all R: Clone {
2 value.clone()
3}
4
5fun copy_head<row R>(value: { id: i64, ..R }) -> i64 where all R: Copy {
6 value.id
7}
8
9record CopyRow { id: i64, count: i64 }
10type CopyAlias := CopyRow;
11
12fun main() {
13 let original := { name = "owned", count = 4 };
14 let cloned := clone_row(&original);
15 let { name, ..tail } := cloned;
16 assert(name == "owned");
17 assert(tail.count == 4);
18 assert(original.name == "owned");
19 let empty := clone_row(&{});
20 let with_id := { ..empty, id = 8 };
21 assert(copy_head(with_id) == 8);
22 let copied: CopyAlias := CopyRow { id = 3, count = 7 };
23 assert(copy_head(copied) == 3);
24 assert(copied.count == 7);
25}
passes
1fun duplicate_rest<row R, row Rest>(value: { ..R }) -> ({ ..Rest }, { ..Rest })
2where R = { token: String, ..Rest }, all Rest: Copy {
3 let { token: _, ..rest } := value;
4 let first := rest;
5 let second := rest;
6 (first, second)
7}
8aspect Relay { fun pass<T>(self, value: T) -> T; }
9struct Courier {}
10extend Courier: Relay {
11 fun pass<T>(self, value: T) -> T { value }
12}
13fun extract<T>(value: { payload: T }) -> T {
14 let { payload, ..rest } := value;
15 let empty: {} := rest;
16 payload
17}
18struct Owned { text: String }
19fun forward<T>(value: T) -> T { value }
20fun main() {
21 assert(extract({ payload = "generic owned" }) == "generic owned");
22 assert(extract({ payload = 17 }) == 17);
23 let empty_pair := duplicate_rest({ token = "discarded" });
24 let left: {} := empty_pair.0;
25 let right: {} := empty_pair.1;
26 let pair := duplicate_rest({ token = "discarded", count = 8, enabled = true });
27 assert(pair.0.count == 8);
28 assert(pair.1.enabled);
29 let { token: _, ..empty } := { token = "owned" };
30 let forwarded: {} := Courier {}.pass(empty);
31 let data := { token = "discarded", payload = "kept" };
32 let var { token: _, ..tail } := data;
33 tail.payload := "restored";
34 assert(Courier {}.pass(tail).payload == "restored");
35 let owned := Owned { text = "removed" };
36 let text := owned.text;
37 let forwarded_nominal: Owned.{} := Courier {}.pass(forward(owned));
38 assert(text == "removed");
39}
passes
1record User { id: i64, name: String, count: i64 }
2type UserAlias := User;
3type Callback := once || -> i64;
4
5fun clone_head<row R>(value: { id: i64, ..R }) -> { id: i64, ..R }
6where all R: Clone {
7 value.clone()
8}
9
10fun main() {
11 let row_clone := clone_head({ id = 9 });
12 assert(row_clone.id == 9);
13 let row_clone_with_tail := clone_head({ id = 3, count = 5 });
14 assert(row_clone_with_tail.id + row_clone_with_tail.count == 8);
15
16 let user: UserAlias := User { id = 3, name = "moved", count = 5 };
17 let name := user.name;
18 let residual := user.clone();
19 assert(name == "moved");
20 assert(residual.id + residual.count == 8);
21 let callback: Callback := [residual] once || {
22 residual.id + residual.count
23 };
24 assert(callback() == 8);
25}
passes
1record Ledger { id: i64, memo: String, count: i64 }
2type LedgerAlias := Ledger;
3
4fun main() {
5 let original: LedgerAlias := Ledger { id = 4, memo = "keep", count = 6 };
6 let copied := original.clone();
7 let memo := copied.memo;
8 let callback := [copied] once || { copied.id + copied.count };
9 assert(memo == "keep");
10 assert(callback() == 10);
11}
passes
1record Row { id: i64, name: String }
2fun key_of<row R>(v: { k: i64, ..R }) -> i64 where all R: Copy { v.k }
3fun main() {
4 let rows: [Row] := [Row { id = 1, name = "a" }, Row { id = 2, name = "b" }];
5 var names := "";
6 for (r in rows) { names := names + r.name + r.id.to_string(); }
7 assert(names == "a1b2");
8 let copies := [{ k = 1, v = true }, { k = 2, v = false }];
9 var s := 0;
10 for (c in copies) { s := s + key_of(c); }
11 assert(s == 3);
12}
passes
1fun head<row R>(v: { xs: [i64; 3], ..R }) -> i64 where all R: Copy {
2 let w := v;
3 v.xs[0] + w.xs[1]
4}
5fun main() { assert(head({ xs = [1, 2, 3], flag = true }) == 3); }
passes
1enum Shape { Circle { r: i64, tag: String }, Square { s: i64, tag: String } }
2record Item { id: i64, shape: [i64; 2] }
3fun area(s: Shape) -> i64 {
4 match (s) {
5 Shape::Circle { r, tag } => r * 3 + tag.len(),
6 Shape::Square { s, tag } => s * s + tag.len(),
7 }
8}
9fun total<row R>(v: { n: i64, ..R }) -> i64 where all R: Copy {
10 match (v) { { n, ..rest } => n }
11}
12fun main() {
13 assert(area(Shape::Circle { r = 2, tag = "ab" }) == 8);
14 assert(area(Shape::Square { s = 3, tag = "" }) == 9);
15 assert(total({ n = 5, flag = true }) == 5);
16 let it := Item { id = 1, shape = [2, 3] };
17 match (it.shape) { [a, b] => assert(a + b == 5) }
18 let r: Option<{ n: i64 }> := Option::Some { value = { n = 4 } };
19 match (r) { Option::Some { value } => assert(value.n == 4), None => assert(false) }
20}
passes
1struct Opaque { v: i64 }
2record Wrap { o: Opaque }
3fun main() {
4 let w := Wrap { o = Opaque { v = 1 } };
5 println("${w}"); // ERROR[T0012]
6}
typecheck errorT0012at 5“field `o` of type `Opaque` does not implement `Display`”
1record Pair { left: String, right: String }
2type PairAlias := Pair;
3type Restore := once var || -> PairAlias;
4
5aspect State { fun state(&self) -> i64; }
6extend<row R: { right: String, .. }> { ..R }: State {
7 fun state(&self) -> i64 { 1 }
8}
9extend<row R: !{ right }> { ..R }: State {
10 fun state(&self) -> i64 { 0 }
11}
12
13fun main() {
14 var pair: PairAlias := Pair { left = "left", right = "right" };
15 assert(pair.state() == 1);
16 let left := pair.left;
17 let partial := pair.clone();
18 assert(partial.right == "right");
19 assert(pair.state() == 1);
20 let right := pair.right;
21 assert(pair.state() == 0);
22 let empty: Pair.{} := pair.clone();
23 assert(empty.state() == 0);
24 let restore: Restore := [pair] once var || -> PairAlias {
25 pair.left := "restored";
26 pair.right := "again";
27 assert(pair.state() == 1);
28 pair
29 };
30 match (restore()) {
31 { left, ..rest } => {
32 assert(left == "restored");
33 assert(rest.right == "again");
34 assert(rest.state() == 1);
35 },
36 }
37}
passes
1record Item { id: i64, name: String, tags: [String; 2] }
2record Token { value: String }
3fun clone_rest() {
4 let it := Item { id = 1, name = "n", tags = ["a", "b"] };
5 let { id, ..rest } := it;
6 assert(id == 1);
7 let c := rest.clone();
8 assert("${c}" == "{ name: n, tags: [a, b] }");
9 assert(c.name == rest.name);
10}
11
12fun clone_empty() {
13 let t := Token { value = "x" };
14 let { value: _, ..rest } := t;
15 let c := rest.clone();
16 assert("${c}" == "{}");
17 assert("${rest}" == "{}");
18}
19
20fun main() {
21 clone_rest();
22 clone_empty();
23}
passes
1record Packet<row R> { data: { ..R } }
2type EmptyRow := {};
3type EmptyPacket := Packet<{}>;
4type Envelope<T> := (T, i64);
5fun same<row R>(value: Envelope<{ ..R }>) -> Envelope<{ ..R }> { value }
6fun copy_twice<row R>(value: { ..R }) -> Envelope<({ ..R }, { ..R })>
7where all R: Copy { ((value, value), 11) }
8fun strip<row R, row Rest>(value: Packet<..R>) -> Packet<..Rest>
9where R = { token: String, ..Rest } {
10 let { token: _, ..rest } := value.data;
11 Packet { data = rest }
12}
13fun identity<T>(value: Envelope<T>) -> Envelope<T> { value }
14aspect Carrier { fun pass<T>(self, value: Envelope<T>) -> Envelope<T>; }
15struct Courier {}
16extend Courier: Carrier { fun pass<T>(self, value: Envelope<T>) -> Envelope<T> { value } }
17fun main() {
18 let wrapped := same(({ text = "owned", count = 2 }, 3));
19 let value := wrapped.0;
20 assert(value.count == 2);
21 assert(value.text == "owned");
22 let pair := copy_twice({ count = 5 });
23 assert(pair.0.0.count + pair.0.1.count == 10);
24 let empty_pair := copy_twice({});
25 let empty: EmptyRow := empty_pair.0.0;
26 let stripped := strip(Packet { data = { token = "gone", text = "kept" } });
27 assert(stripped.data.text == "kept");
28 let stripped_empty: EmptyPacket := strip(Packet { data = { token = "gone" } });
29 let envelope := Courier {}.pass(identity((stripped_empty, 9)));
30 assert(envelope.1 == 9);
31}
passes
1record Session<row R> { id: i64, data: { ..R } }
2extend<row R: { token: String, .. }> Session<..R> {
3 fun kind(self) -> i64 { 1 }
4}
5extend<row R: !{ token }> Session<..R> {
6 fun kind(self) -> i64 { 2 }
7}
8aspect Mark { fun mark(&self) -> i64; }
9extend<row R> { ..R }: Mark where all R: Copy {
10 fun mark(&self) -> i64 { 7 }
11}
12fun marked<row R>(value: &{ ..R }) -> i64 where all R: Copy {
13 value.mark()
14}
15fun remove_token<row R, row Rest>(session: Session<..R>) -> Session<..Rest>
16where R = { token: String, ..Rest } {
17 let { id, data } := session;
18 let { token: _, ..rest } := data;
19 let cleared: Session<..Rest> := Session { id = id, data = rest };
20 cleared
21}
22fun has_token<row R: { token: String, .. }>(session: Session<..R>) -> i64 {
23 session.kind()
24}
25fun main() {
26 assert(has_token(Session { id = 3, data = { token = "t", text = "owned" } }) == 1);
27 assert(remove_token(Session { id = 3, data = { token = "t" } }).kind() == 2);
28 assert(remove_token(Session { id = 5, data = { token = "t", text = "kept" } }).kind() == 2);
29 assert(marked(&{}) == 7);
30 assert(marked(&{ count = 4, flag = true }) == 7);
31}
passes
1import shapes::{Frame, Weigh, strip_label, total, empty_after};
2fun main() {
3 let f := Frame { public_rows = [1, 2, 3], label = "x" };
4 let stripped := strip_label(f);
5 assert(stripped.public_rows[2] == 3);
6 assert(total(stripped) == 6);
7 assert(total({ public_rows = [4, 5, 6], extra = true }) == 15);
8 let e := empty_after({ label = "gone".to_string() });
9 assert(e.weigh() == 1);
10 assert(stripped.weigh() == 1);
11}
passes
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
All three are permitted once the aspect is local to the implementing module: the first is
exactly one structural type, the others a row condition on the receiver. The receiver
matches an impl when its row meets the condition, whether it is an anonymous record or a
nominal §Records. Methods are checked per call against the
concrete receiver, as for every structural target, and the aspect's default methods are
inherited as for any impl. The field types a target writes must be spelled out: one that
names an impl type parameter (extend<T> { w: T }: A) is rejected, since no receiver's
field type could equal it.
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, which is specified below. The third is most useful with a way to require an
aspect of every field in the row (§Field wise row constraints above, all R: Aspect), which a record-target
impl accepts as where all R: Aspect. The standard library implements Display for every record of Display fields this way
(its body is a built-in, LIMIT-TYPES-003) and Copy for every record of Copy fields (a
bodyless impl): such a record is copied, not moved.
Two shape-conditional impls are disjoint when some label one requires the other forbids:
aspect Describe { fun describe(&self) -> String; }
extend<row R: { name: String, .. }> { ..R }: Describe {
fun describe(&self) -> String { "named ${self.name}" }
}
extend<row R: !{ name }> { ..R }: Describe {
fun describe(&self) -> String { "anonymous" }
}
fun main() {
assert({ name = "ada", age = 36 }.describe() == "named ada");
assert({ age = 36 }.describe() == "anonymous");
}
The impl is chosen from the value's current row, so a partial move changes it:
record Handle { id: i64, name: String }
aspect Describe { fun state(&self) -> String; }
extend<row R: !{ name }> { ..R }: Describe {
fun state(&self) -> String { "no name" }
}
extend<row R: { name: String, .. }> { ..R }: Describe {
fun state(&self) -> String { "has name" }
}
fun main() {
var h := Handle { id = 1, name = "secret" };
assert(h.state() == "has name");
let name := h.name; // h : Handle.{ id }
assert(h.state() == "no name");
assert(name == "secret");
}
Two impls whose conditions do not exclude each other overlap and are rejected with T0015:
aspect Describe { fun describe(&self) -> String; }
extend<row R: { name: String, .. }> { ..R }: Describe {
fun describe(&self) -> String { "a" }
}
extend<row R: { id: i64, .. }> { ..R }: Describe {
fun describe(&self) -> String { "b" }
}
Formal rules
Legality Rule №1
A row-conditional impl (extend<row R: { ... }> { ..R }: Aspect or extend<row R> { ..R }: Aspect) resolves against a value's current row, not only its widest
possible one — for a struct, that is the row of its current residual (narrower after a
partial move, wider again after the moved field is restored), exactly the same
current-row behavior Named Records§Records states for record's own
row-bound satisfaction.
Referenced by: rfc-0121
Tested by (14)
1// RFC-0121 remainder (metel-core#1306): a partially moved nominal record is
2// checked against record-target row conditions using its residual row.
3
4record Handle { id: i64, name: String }
5
6aspect Describe { fun state(&self) -> String; }
7
8extend<row R: !{ name }> { ..R }: Describe {
9 fun state(&self) -> String { "no name" }
10}
11
12extend<row R: { name: String, .. }> { ..R }: Describe {
13 fun state(&self) -> String { "has name" }
14}
15
16fun main() {
17 var h := Handle { id = 1, name = "secret" };
18 let name := h.name;
19 assert(name == "secret");
20 assert(h.state() == "no name");
21 println("ok");
22}
passes
1record Session<row R> { data: { ..R } }
2extend<row R: !{ token }> Session<..R> {
3 fun cleared(&self) -> i64 { 2 }
4}
5fun strip<row R, row Rest>(session: Session<..R>) -> i64
6where R = { token: String, ..Rest } {
7 let { token: _, ..rest } := session.data;
8 let cleared: Session<..Rest> := Session { data = rest };
9 cleared.cleared()
10}
11fun main() {
12 assert(strip(Session { data = { token = "gone" } }) == 2);
13 assert(strip(Session { data = { token = "gone", kept = "here" } }) == 2);
14}
passes
1record Notice { title: String, priority: i64 }
2aspect Summary { fun summary(&self) -> String; }
3
4extend<row R: { title: String, .. }> { ..R }: Summary {
5 fun summary(&self) -> String { self.title.clone() }
6}
7
8fun main() {
9 let notice := Notice { title = "captured dispatch", priority = 2 };
10 let callback := [notice] once || { notice.summary() };
11 assert(callback() == "captured dispatch");
12}
passes
1// IM x TA, IM x OW, IM x PM, IM x CO
2record Session<row R> { id: i64, data: { ..R } }
3type Open<row R> := Session<..R>;
4extend<row R: { token: String, .. }> Session<..R> { fun kind(&self) -> i64 { 1 } }
5extend<row R: !{ token }> Session<..R> { fun kind(&self) -> i64 { 2 } }
6fun via_alias<row R: { token: String, .. }>(s: Open<..R>) -> i64 { s.kind() }
7fun main() {
8 let s := Session { id = 1, data = { token = "t", n = 1 } };
9 assert(via_alias(s) == 1);
10 let a := Session { id = 2, data = { token = "x" } };
11 let b := Session { id = 3, data = { n = 5 } };
12 let all := [a.kind(), b.kind()];
13 var sum := 0;
14 for (k in all) { sum := sum + k; }
15 assert(sum == 3);
16 let moved := b;
17 assert(moved.kind() == 2);
18 match (moved) { Session { id, data } => assert(id == 3) }
19}
passes
1record Series<row R> { points: [i64; 3], extra: { ..R } }
2extend<row R: { unit: String, .. }> Series<..R> { fun total(&self) -> i64 { self.points[0] + self.points[1] + self.points[2] } }
3extend<row R: !{ unit }> Series<..R> { fun total(&self) -> i64 { 0 } }
4fun main() {
5 let a := Series { points = [1, 2, 3], extra = { unit = "m" } };
6 let b := Series { points = [1, 2, 3], extra = {} };
7 assert(a.total() == 6);
8 assert(b.total() == 0);
9}
passes
1record Session<row R> { id: i64, data: { ..R } }
2extend<row R: { token: String, .. }> Session<..R> { fun kind(&self) -> i64 { 1 } }
3extend<row R: !{ token }> Session<..R> { fun kind(&self) -> i64 { 2 } }
4fun main() {
5 let s := Session { id = 1, data = { count = 3 } };
6 let t := s.clone();
7 assert(t.kind() == 2);
8 assert(s.kind() == 2);
9 assert("${s}" == "Session { data: { count: 3 }, id: 1 }");
10}
passes
1record Session<row R> { id: i64, data: { ..R } }
2extend<row R: { token: String, .. }> Session<..R> { fun kind(&self) -> i64 { 1 } }
3extend<row R: !{ token }> Session<..R> { fun kind(&self) -> i64 { 2 } }
4fun after_rest() {
5 let s := Session { id = 1, data = { token = "t", n = 5 } };
6 assert(s.kind() == 1);
7 let { id, data } := s;
8 let { token: _, ..rest } := data;
9 let s2 := Session { id = id, data = rest };
10 assert(s2.kind() == 2);
11}
12
13fun empty_and_stripped() {
14 let s := Session { id = 1, data = { token = "t" } };
15 let { id, data } := s;
16 let { token: _, ..rest } := data;
17 let s2 := Session { id = id, data = rest };
18 assert(s2.kind() == 2);
19 let e := Session { id = 2, data = {} };
20 assert(e.kind() == 2);
21}
22
23fun main() {
24 after_rest();
25 empty_and_stripped();
26}
passes
1aspect Describe { fun describe(&self) -> String; }
2record Point { x: i64, y: i64 }
3extend Point: Describe { fun describe(&self) -> String { "point".to_string() } }
4extend<row R> { ..R }: Describe where all R: Copy { fun describe(&self) -> String { "copy-row".to_string() } }
5record Packet<row R> { data: { ..R } }
6extend<row R> Packet<..R>: Describe { fun describe(&self) -> String { "packet".to_string() } }
7fun show(d: &dyn Describe) -> String { d.describe() }
8fun main() {
9 let p := Point { x = 1, y = 2 };
10 assert(show(&p) == "point");
11 let pk := Packet { data = { a = 1 } };
12 assert(show(&pk) == "packet");
13 let anon := { a = 1, b = true };
14 assert(show(&anon) == "copy-row");
15 let boxed: dyn Describe := Packet { data = { n = 2 } };
16 assert(boxed.describe() == "packet");
17}
passes
1// A record rest pattern drops the nominal brand (spec.expressions "Record rest patterns"),
2// so aspect dispatch on the remainder follows its anonymous static type, not the hidden
3// brand of the record it came from (metel-core#1446). A whole value (here `u`, after a
4// `Copy` field read that does not narrow it) still dispatches to its nominal impl.
5aspect Describe { fun describe(&self) -> String; }
6record Point { x: i64, y: i64 }
7extend Point: Describe { fun describe(&self) -> String { "point".to_string() } }
8extend<row R> { ..R }: Describe where all R: Copy { fun describe(&self) -> String { "copy-row".to_string() } }
9
10record Tok { value: String, id: i64, n: i64 }
11aspect Keyed { fun key(&self) -> String; }
12extend Tok: Keyed { fun key(&self) -> String { "tok".to_string() } }
13extend<row R: { id: i64, .. }> { ..R }: Keyed where all R: Copy { fun key(&self) -> String { "row".to_string() } }
14
15fun erase(p: Point) -> {} {
16 let { x: _, y: _, ..rest } := p;
17 rest
18}
19
20fun show(d: &dyn Describe) -> String { d.describe() }
21
22fun main() {
23 let e: {} := erase(Point { x = 1, y = 2 });
24 assert(e.describe() == "copy-row");
25 assert(show(&e) == "copy-row");
26
27 let t := Tok { value = "x".to_string(), id = 1, n = 2 };
28 let { value: _, ..rest } := t;
29 assert(rest.key() == "row");
30
31 let u := Point { x = 1, y = 2 };
32 let moved := u.x;
33 assert(u.describe() == "point");
34 assert(moved == 1);
35}
passes
1record Pair { left: String, right: String }
2type PairAlias := Pair;
3type Restore := once var || -> PairAlias;
4
5aspect State { fun state(&self) -> i64; }
6extend<row R: { right: String, .. }> { ..R }: State {
7 fun state(&self) -> i64 { 1 }
8}
9extend<row R: !{ right }> { ..R }: State {
10 fun state(&self) -> i64 { 0 }
11}
12
13fun main() {
14 var pair: PairAlias := Pair { left = "left", right = "right" };
15 assert(pair.state() == 1);
16 let left := pair.left;
17 let partial := pair.clone();
18 assert(partial.right == "right");
19 assert(pair.state() == 1);
20 let right := pair.right;
21 assert(pair.state() == 0);
22 let empty: Pair.{} := pair.clone();
23 assert(empty.state() == 0);
24 let restore: Restore := [pair] once var || -> PairAlias {
25 pair.left := "restored";
26 pair.right := "again";
27 assert(pair.state() == 1);
28 pair
29 };
30 match (restore()) {
31 { left, ..rest } => {
32 assert(left == "restored");
33 assert(rest.right == "again");
34 assert(rest.state() == 1);
35 },
36 }
37}
passes
1record Item { name: String, token: String }
2aspect Describe { fun describe(&self) -> String; }
3extend<row R: { name: String, .. }> { ..R }: Describe {
4 fun describe(&self) -> String { self.name.clone() }
5}
6type View := dyn Describe;
7type Callback := once || -> String;
8fun display(value: &View) -> String { value.describe() }
9
10fun main() {
11 let item := Item { name = "kept", token = "discarded" };
12 let token := item.token;
13 let object: View := item;
14 assert(display(&object) == "kept");
15 let callback: Callback := [object] once || { object.describe() };
16 assert(callback() == "kept");
17 let anonymous: View := { name = "anonymous" };
18 assert(display(&anonymous) == "anonymous");
19}
passes
1record Session<row R> { id: i64, data: { ..R } }
2extend<row R: { token: String, .. }> Session<..R> {
3 fun kind(self) -> i64 { 1 }
4}
5extend<row R: !{ token }> Session<..R> {
6 fun kind(self) -> i64 { 2 }
7}
8aspect Mark { fun mark(&self) -> i64; }
9extend<row R> { ..R }: Mark where all R: Copy {
10 fun mark(&self) -> i64 { 7 }
11}
12fun marked<row R>(value: &{ ..R }) -> i64 where all R: Copy {
13 value.mark()
14}
15fun remove_token<row R, row Rest>(session: Session<..R>) -> Session<..Rest>
16where R = { token: String, ..Rest } {
17 let { id, data } := session;
18 let { token: _, ..rest } := data;
19 let cleared: Session<..Rest> := Session { id = id, data = rest };
20 cleared
21}
22fun has_token<row R: { token: String, .. }>(session: Session<..R>) -> i64 {
23 session.kind()
24}
25fun main() {
26 assert(has_token(Session { id = 3, data = { token = "t", text = "owned" } }) == 1);
27 assert(remove_token(Session { id = 3, data = { token = "t" } }).kind() == 2);
28 assert(remove_token(Session { id = 5, data = { token = "t", text = "kept" } }).kind() == 2);
29 assert(marked(&{}) == 7);
30 assert(marked(&{ count = 4, flag = true }) == 7);
31}
passes
1import left::LeftPacket;
2import left::LeftRead;
3import left::inspect_left;
4import left::identity_left;
5import right::RightPacket;
6import right::RightRead;
7import right::inspect_right;
8import right::identity_right;
9
10fun main() {
11 let left := identity_left(LeftPacket { data = { count = 4, text = "left" } });
12 let right := identity_right(RightPacket { data = { count = 4, text = "right" } });
13 assert(inspect_left(&left) == 4);
14 assert(inspect_right(&right) == 104);
15 let left_dyn: dyn LeftRead := left;
16 let right_dyn: dyn RightRead := right;
17 assert(left_dyn.read() == 4);
18 assert(right_dyn.read() == 104);
19 let empty_left: LeftPacket<{}> := identity_left(LeftPacket { data = {} });
20 let empty_right: RightPacket<{}> := identity_right(RightPacket { data = {} });
21}
passes
1record Packet<row R> { data: { ..R } }
2extend<row R: !{ token }> Packet<..R> { fun cleared(&self) -> i64 { 4 } }
3fun rejected<row R: !{ token: String }>(packet: &Packet<..R>) -> i64 {
4 packet.cleared() // ERROR[T0035]
5}
6fun main() {}
typecheck errorT0035“no row-conditional implementation matches”
Legality Rule №2
When a value's brand has an ordinary (brand-keyed) impl of an aspect and its row also satisfies a row-conditional impl of the same aspect, the brand-keyed impl is selected: brand-exact dispatch is checked first, and a match there short-circuits row-conditional resolution entirely rather than conflicting with it.
Tested by (2)
1// RFC-0121 sec3 / `spec.types.generics.row-conditional-impls.legality-2`: when a
2// nominal record has its own (brand-keyed) impl of an aspect and its row also satisfies
3// a record-target impl of the same aspect, the brand-keyed impl is selected -- also
4// through an aspect bound.
5
6aspect Describe { fun describe(&self) -> String; }
7
8record Tagged { w: i64 }
9record Plain { w: i64, z: i64 }
10
11extend Tagged: Describe {
12 fun describe(&self) -> String { "brand".to_string() }
13}
14
15extend<row R: { w: i64, .. }> { ..R }: Describe {
16 fun describe(&self) -> String { "row".to_string() }
17}
18
19fun show<T: Describe>(x: &T) -> String { x.describe() }
20
21fun main() {
22 let tagged := Tagged { w = 1 };
23 let plain := Plain { w = 1, z = 2 };
24 let anon := { w = 1 };
25 assert(tagged.describe() == "brand");
26 assert(plain.describe() == "row");
27 assert(anon.describe() == "row");
28 assert(show(&tagged) == "brand");
29 assert(show(&plain) == "row");
30 assert(show(&anon) == "row");
31 println("ok");
32}
passes
1// RFC-0121 item 5 (metel-core#833, legality-2): an impl for one specific
2// instantiation is not in conflict with a row-conditional impl its row also
3// satisfies, and it is the one selected.
4
5aspect Describe { fun describe(self) -> i64; }
6
7record Session<row R> { id: i64, data: { ..R } }
8
9extend Session<{ token: String }>: Describe {
10 fun describe(self) -> i64 { 1 }
11}
12
13extend<row R: { token: String, .. }> Session<..R>: Describe {
14 fun describe(self) -> i64 { 2 }
15}
16
17fun main() {
18 let exact: Session<{ token: String }> :=
19 Session { id = 1, data = { token = "t" } };
20 let wider: Session<{ token: String, extra: i64 }> :=
21 Session { id = 2, data = { token = "t", extra = 1 } };
22 assert(exact.describe() == 1);
23 assert(wider.describe() == 2);
24 println("ok");
25}
passes
Legality Rule №3
Two row-conditional impls of the same aspect do not conflict when some field name both
conditions mention is required present by one and absent (or incompatibly typed) by the
other — rows being open means no other fact can correlate two row conditions, so this
is the only way two of them can be proven disjoint. Any other pair of row-conditional
impls of the same aspect overlaps, and is rejected exactly as two overlapping ordinary
impls are. Inside a generic body over an unconstrained <row R>, a row-conditional
method is not visible unless R's own bound entails the impl's condition.
Tested by (6)
1aspect Mark { fun mark(&self) -> i64; }
2extend<row R> { ..R }: Mark where all R: Copy {
3 fun mark(&self) -> i64 { 7 }
4}
5fun unchecked<row R>(value: &{ ..R }) -> i64 { value.mark() } // ERROR[T0035]
6fun main() {}
typecheck errorT0035“conditional implementation requirements are not entailed”
1record Session<row R> { id: i64, data: { ..R } }
2extend<row R: { token: String, .. }> Session<..R> { fun kind(&self) -> i64 { 1 } }
3extend<row R: !{ token }> Session<..R> { fun kind(&self) -> i64 { 2 } }
4fun main() {
5 let s := Session { id = 1, data = { count = 3 } };
6 let t := s.clone();
7 assert(t.kind() == 2);
8 assert(s.kind() == 2);
9 assert("${s}" == "Session { data: { count: 3 }, id: 1 }");
10}
passes
1record Session<row R> { id: i64, data: { ..R } }
2extend<row R: { token: String, .. }> Session<..R> { fun kind(&self) -> i64 { 1 } }
3extend<row R: !{ token }> Session<..R> { fun kind(&self) -> i64 { 2 } }
4fun after_rest() {
5 let s := Session { id = 1, data = { token = "t", n = 5 } };
6 assert(s.kind() == 1);
7 let { id, data } := s;
8 let { token: _, ..rest } := data;
9 let s2 := Session { id = id, data = rest };
10 assert(s2.kind() == 2);
11}
12
13fun empty_and_stripped() {
14 let s := Session { id = 1, data = { token = "t" } };
15 let { id, data } := s;
16 let { token: _, ..rest } := data;
17 let s2 := Session { id = id, data = rest };
18 assert(s2.kind() == 2);
19 let e := Session { id = 2, data = {} };
20 assert(e.kind() == 2);
21}
22
23fun main() {
24 after_rest();
25 empty_and_stripped();
26}
passes
1// RFC-0173 D6(3): a declared row bound makes the corresponding conditional
2// inherent method visible within the generic body.
3
4record Session<row R> {
5 id: i64,
6 data: { ..R },
7}
8
9extend<row R: { token: String, .. }> Session<..R> {
10 fun with_token(self) -> i64 { self.id }
11}
12
13fun use_session<row R: { token: String, .. }>(session: Session<..R>) -> i64 {
14 session.with_token()
15}
16
17fun main() {
18 let session: Session<{ token: String }> := Session {
19 id = 1,
20 data = { token = "token" },
21 };
22 println(use_session(session));
23}
passes
1// A negative row bound also entails a negative row-conditional impl condition.
2
3record Session<row R> {
4 id: i64,
5 data: { ..R },
6}
7
8extend<row R: !{ token }> Session<..R> {
9 fun without_token(self) -> i64 { self.id }
10}
11
12fun use_session<row R: !{ token }>(session: Session<..R>) -> i64 {
13 session.without_token()
14}
passes
1// RFC-0173 D6(3): a row-conditional inherent method is unavailable in a
2// generic body until the row parameter's declared bounds entail the condition.
3
4record Session<row R> {
5 id: i64,
6 data: { ..R },
7}
8
9extend<row R: { token: String, .. }> Session<..R> {
10 fun with_token(self) -> i64 { self.id }
11}
12
13fun use_session<row R>(session: Session<..R>) -> i64 {
14 session.with_token() // ERROR[T0035]
15}
typecheck errorT0035at 14
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 (3)
1// RFC-0121 / metel-core#1307: a block-local record keeps its declaration
2// identity when passed to a generic row-bounded function.
3
4fun needs_fd<record T: { fd: i64, .. }>(value: T) -> i64 {
5 value.fd
6}
7
8fun main() {
9 record Handle { fd: i64 }
10 let handle := Handle { fd = 3 };
11 assert(needs_fd(handle) == 3);
12 println("ok");
13}
passes
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 (6)
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
1// metel-core#1456: only a label-only exclusion proves a label absent, so only it allows a spread
2// that writes that label. A typed exclusion still works as the bound it is.
3fun add_inline<row R: !{ token }>(d: { ..R }) -> { token: String, ..R } {
4 { token = "secret".to_string(), ..d }
5}
6
7fun add_where<row R>(d: { ..R }) -> { token: String, ..R } where R: !{ token } {
8 { token = "secret".to_string(), ..d }
9}
10
11fun add_both<row R: !{ token } + !{ token: String }>(d: { ..R }) -> { token: String, ..R } {
12 { token = "secret".to_string(), ..d }
13}
14
15// A typed exclusion constrains callers by type only: a `token: i64` still satisfies it.
16fun typed_only<row R: !{ token: String }>(d: { ..R }) -> i64 { 1 }
17
18fun main() {
19 assert(add_inline({ user = 3 }).token == "secret");
20 assert(add_where({ user = 4 }).user == 4);
21 assert(add_both({ user = 5 }).token == "secret");
22 assert(typed_only({ token = 5 }) == 1);
23}
passes
1// metel-core#1456: `!{ token: String }` only rules out a `String` `token`; the row may still hold
2// `token` at another type, so it is not an absent-label fact and a spread cannot rely on it
3// (spec.types.generics.open-rows.legality-4).
4fun add_token<row R: !{ token: String }>(d: { ..R }) -> { token: String, ..R } {
5 { token = "secret".to_string(), ..d } // ERROR[T0012]
6}
7
8fun main() {}
typecheck errorT0012at 5“cannot prove field `token` absent from the spread row”
1// The same rule for a negative bound written in a `where` clause.
2fun add_token<row R>(d: { ..R }) -> { token: String, ..R } where R: !{ token: String } {
3 { token = "secret".to_string(), ..d } // ERROR[T0012]
4}
5
6fun main() {}
typecheck errorT0012at 3“cannot prove field `token` absent from the spread row”
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. Option<!> does not satisfy it — None is inhabited but has zero fields — so Option<!> 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
Option<!> 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: Option<!> 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: Option<!>) -> i64 {
5 match (p) {
6 Option::None => 0,
7 // Some omitted -- exhaustive; Some is uninhabited when T = !.
8 }
9}
10
11fun main() -> i64 {
12 use_perhaps(Option::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
Option<T>
Option<T> is the built-in optional type. There is no null — all absence is expressed via Option<T>.
The type of None is Option<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 Option<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 (Option::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: Option<i64> := None;
match (x) {
Some { value } => value,
None => 0,
}
}
fun main() -> i64 {
let result: Option<i64> := None;
let value: Option<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) -> Option<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) -> Option<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 Option<T> and is valid only where the expected type
determines T; Option::None is valid wherever the qualified variant is named.
Referenced by: rfc-0020, rfc-0111
Tested by (3)
1fun find(arr: [i64], target: i64) -> Option<i64> {
2 var i := 0;
3 while (i < arr.len()) {
4 if (arr[i as u64] == target) {
5 return Option::Some { value = i };
6 }
7 i += 1;
8 }
9 None
10}
11
12fun main() {
13 // None literal matches the None pattern.
14 let n: Option<i64> := None;
15 let r1 := match (n) { None => -1, Option::Some { value } => value, };
16 assert(r1 == -1);
17 // Option::Some construction and field extraction via match.
18 let s := Option::Some { value = 42 };
19 let r2 := match (s) { None => -1, Option::Some { value } => value, };
20 assert(r2 == 42);
21 // Option 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, Option::Some { value } => value, };
25 assert(r3 == 2);
26 // Option as function return type — not-found case.
27 let idx2 := find(arr, 99);
28 let r4 := match (idx2) { None => -1, Option::Some { value } => value, };
29 assert(r4 == -1);
30 // Option in expression position — the match result is usable directly.
31 let doubled := match (find(arr, 20)) {
32 Option::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: Option<i64>,
6 nothing: Option<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: Option<i64> := Some { value = 5 };
24 let q: Option<i64> := None;
25 assert(paint(annotated_closure()) == 1);
26
27 // An empty array literal is deferred too, and is *genuinely* polymorphic -- the
28 // #285 check is scoped to bare variants precisely so this keeps working.
29 let mk := || { [] };
30 let ints: [i64] := mk();
31 let strs: [String] := mk();
32 assert(ints.len() == 0);
33 assert(strs.len() == 0);
34}
passes
Legality Rule №2
The built-in optional type is named Option. std::core declares no type named Perhaps: a
reference to Perhaps that resolves to nothing is T0003,
and its message states that Perhaps is now Option. That covers Perhaps written as a type,
as a path or the leading segment of one (Perhaps::None, std::core::Perhaps), as an expression
or pattern name, and in an import. A module that declares its own Perhaps is unaffected, and
Option may be shadowed like Result and List
(scoping-and-shadowing legality-9Variables L9).
Referenced by: rfc-0184
Tested by (10)
1// RFC-0184 (metel-core#1625): after the rename Perhaps is no name of std::core's, so a module's own Perhaps is a plain user type
2// that sits beside the core Option; neither shadows the other and no hint applies.
3enum Perhaps { Maybe, Not }
4
5fun main() {
6 let m := Perhaps::Maybe;
7 var s := 0;
8 match (m) {
9 Perhaps::Maybe => { s := 1; },
10 Perhaps::Not => { s := 2; },
11 }
12 assert(s == 1);
13 let o: Option<i64> := Some { value = 3 };
14 assert(o.unwrap_or(0) == 3);
15}
passes
1// RFC-0184 (metel-core#1625): the removed name used as a bare expression name.
2fun main() {
3 let a := Perhaps;
4}
typecheck errorT0003“`Perhaps` is now `Option`”
1// RFC-0184 (metel-core#1625): importing the removed name from std::core is T0003 and says Perhaps is now Option.
2import std::core::Perhaps;
3
4fun main() { }
typecheck errorT0003“`Perhaps` is now `Option`”
1// RFC-0184 (metel-core#1625): the removed name written as a parameter type.
2fun f(x: Perhaps<i64>) -> i64 { 0 }
3
4fun main() { }
typecheck errorT0003“`Perhaps` is now `Option`”
1// RFC-0184 (metel-core#1625): the removed name as the leading segment of a path.
2fun main() {
3 let a := Perhaps::None;
4}
typecheck errorT0003“`Perhaps` is now `Option`”
1// RFC-0184 (metel-core#1625): the removed name as the enum of a pattern.
2fun main() {
3 let v: Option<i64> := None;
4 match (v) {
5 Perhaps::None => { },
6 Perhaps::Some { value } => { },
7 }
8}
typecheck errorT0003“`Perhaps` is now `Option`”
1// RFC-0184 (metel-core#1625): the removed name as the enum of a variant literal.
2fun main() {
3 let a := Perhaps::Some { value = 1 };
4}
typecheck errorT0003“`Perhaps` is now `Option`”
1// RFC-0184 (metel-core#1625): the removed name qualified through std::core in a type position.
2fun main() {
3 let a: std::core::Perhaps<i64> := None;
4}
typecheck errorT0003“`Perhaps` is now `Option`”
1// RFC-0184 (metel-core#1625): the removed name qualified through std::core in an expression path.
2fun main() {
3 let a := std::core::Perhaps::None;
4}
typecheck errorT0003“`Perhaps` is now `Option`”
1// RFC-0184 (metel-core#1625): Option<T> is the built-in optional type: annotations, qualified paths and std::core's own
2// APIs all spell it.
3fun first(xs: [i64]) -> Option<i64> {
4 if (xs.len() == 0) {
5 return None;
6 }
7 Some { value = xs[0] }
8}
9
10fun main() {
11 assert(first([4, 5]).unwrap_or(0) == 4);
12 assert(first([]).is_none());
13 let q: std::core::Option<i64> := std::core::Option::Some { value = 2 };
14 assert(q.unwrap_or(0) == 2);
15 let r: Result<i64, String> := Ok { value = 1 };
16 assert(r.ok().unwrap_or(0) == 1);
17}
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.
Known gaps
GAP-TYPES-001 — No higher-rank polymorphism. A function parameter is a monotype, so it cannot be used at two types in one body. Open record ↗knownGAP-TYPES-002 — The storage model of List<T> and its boundary with T[] and [T; N] is not specified. How a growable List<T> allocates and grows its storage, and where it ends and T[] and [T; N] begin, is not fully specified. Open record ↗knownGAP-TYPES-005 — No spelling for an operator on a generic parameter. An operator such as + or < on a bare type parameter is rejected, and no aspect exists that grants it. Open record ↗knownGAP-TYPES-008 — A record literal accepts at most one row spread. No form combines the fields of two rows into one record: merging two generic records has no spelling. Open record ↗knownGAP-TYPES-009 — Tuples have no rest pattern or spread over their numeric-label row. A tuple cannot be split into its first element and the rest, or extended, in a generic body. Open record ↗acceptedGAP-TYPES-010 — A function cannot return an open record while hiding its extra fields. An open record with an anonymous tail is rejected as a return type, so an API cannot promise only some fields of the record it returns. Open record ↗acceptedGAP-TYPES-011 — A written function type cannot say it accepts any record with at least these fields. An open record with an anonymous tail is rejected inside a function type, and a named row fixes the row for the whole signature, so no written type means 'works for every wider record'. Open record ↗acceptedGAP-TYPES-012 — A record or struct field cannot have an open-record type with an anonymous tail. An anonymous open record is rejected as a field type, so a field cannot hold 'any record with at least these fields'. Open record ↗Resolved (3)
GAP-TYPES-003 — There is no nominal record kind, so named records are unavailable. RESOLVED: RFC-0120 (Named Records) specifies and now implements a nominal record kind that satisfies row bounds. Open record ↗resolvedGAP-TYPES-004 — A blanket row-conditional aspect impl's body cannot require an aspect of every field. RESOLVED: where all R: Aspect (RFC-0123) now specifies the per-field quantifier a blanket row-conditional impl's body needs. Open record ↗resolvedGAP-TYPES-006 — A generic body cannot construct or consume a row remainder. A where equation can type the remainder of a row, but no form lets a body take an abstract row apart or build one, so typestate over a row cannot be implemented generically. Open record ↗