Runtime
Panics
A panic is a hard, unrecoverable runtime errorD1. It prints a message and exits the process with a non-zero status. Panics cannot be caught.
Panics are triggered by:
.yolo()onNoneor anErr- Out-of-bounds array access
- Integer division by zero
assert(false)orassert(false, msg)panic(msg), called directly
Formal rules
Dynamic Semantics №1
A panic prints its message, terminates the process with a non-zero status, and cannot be
caught. Calling panic, a failing assert, .yolo() on an absent or error variant,
out-of-bounds array access, and integer division by zero trigger a panic.
Tested by
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”
Built-in Functions
These are available in every module without any import declaration (provided by std::core auto-import):
| Name | Signature | Description |
|---|---|---|
print | <T>(x: T) | Print to stdout, no newline |
println | <T>(x: T) | Print to stdout with newline |
clock | () -> i64 | Unix timestamp in milliseconds |
assert | (cond: boolean) | Panic with "assertion failed" if cond is false |
assert | (cond: boolean, msg: String) | Overload: panic with msg if cond is false |
dbg | <T>(x: T) -> T | Print [dbg] <value> to stderr and return the value unchanged |
panic | (msg: String) -> ! | Panic unconditionally with msg (RFC-0078) |
assert is overloaded — the two-argument form carries the panic messageD2.
panic's return type ! coerces to whatever type its calling context expectsNever type L2 — see §Never type.
print and println require their argument to implement DisplayL1
(print<T: Display>): passing a struct or enum with no Display
implementation is a compile-time error. A value whose type implements Display
is printed through that implementation's to_stringD1 — user structs and enums,
not only the built-in primitives.
String length is a method, not a free function: "hello".len() returns the
number of characters (Unicode scalar values). Strings are concatenated with
the + operator.
Formal rules
Legality Rule №1
print and println accept only values whose type implements Display.
Tested by
1// println/print require Display (METEL-181): passing a type with no
2// Display impl is a compile-time error, not a runtime panic.
3
4struct Test {
5 attr: i8,
6}
7
8fun main() {
9 let x := Test { attr = 1i8 };
10 println(x); // ERROR[T0012]
11}
typecheck errorT0012“does not implement `Display`”
Dynamic Semantics №1
print writes a Display value's to_string result to stdout without a newline; println
writes the same result followed by a newline.
Tested by
1// print/println must dispatch a user-defined `Display` impl, not only format
2// primitives. Before METEL-192 these calls typechecked but panicked at runtime
3// with R0009 because the host formatter only handled primitive values. The
4// public print/println now lower to `x.to_string()` in-language, so any
5// `Display` value (struct, enum, primitive) prints via its own impl.
6
7struct Point { x: i64, y: i64 }
8
9extend Point: Display {
10 fun to_string(&self) -> String {
11 "(${self.x}, ${self.y})"
12 }
13}
14
15enum Color { Red, Green }
16
17extend Color: Display {
18 fun to_string(&self) -> String {
19 match (self) {
20 Color::Red => "red",
21 Color::Green => "green",
22 }
23 }
24}
25
26fun main() {
27 // struct with a user Display impl
28 println(Point { x = 1, y = 2 });
29 // enum with a user Display impl
30 print(Color::Red);
31 println(Color::Green);
32 // primitives must still print unchanged
33 println(42);
34 println("done");
35}
passes
Dynamic Semantics №2
assert(false) panics with "assertion failed"; assert(false, msg) panics with msg.
Tested by
1fun main() {
2 assert(false, "custom assertion failure");
3}
runtime errorR0012“custom assertion failure”
Dynamic Semantics №3
dbg(v) writes its debug rendering to stderr and evaluates to v unchanged.
Tested by
1// dbg(x) — print-and-return: prints to stderr, returns value unchanged.
2
3fun main() {
4 // Scalar types pass through unchanged.
5 let x: i64 := dbg(42);
6 assert(x == 42);
7
8 let b: boolean := dbg(true);
9 assert(b == true);
10
11 let s: String := dbg("hello");
12 assert(s == "hello");
13
14 // Arithmetic expression passed through.
15 let y: i64 := dbg(2 + 3);
16 assert(y == 5);
17
18 // Inline: dbg(x) inside a larger expression.
19 let z: i64 := dbg(10) * 2;
20 assert(z == 20);
21}
passes
Dynamic Semantics №4
clock() returns the current Unix timestamp in milliseconds.
Tested by
Built-in Aspects
The following aspects are pre-implemented for built-in types:
Display
aspect Display {
fun to_string(&self) -> String;
}
i64, f64, boolean, String, and Char implement DisplayL1. .to_string() returns the canonical string representation. print and println accept any Display type.
Formal rules
Legality Rule №1
i64, f64, boolean, String, and Char have built-in Display implementations whose
to_string methods return their canonical string representations.
Tested by (2)
1fun main() {
2 // to_string method
3 assert(0.to_string() == "0");
4 assert(42.to_string() == "42");
5 assert((-7).to_string() == "-7");
6 assert(1.5.to_string() == "1.5");
7 assert(0.0.to_string() == "0");
8 assert(true.to_string() == "true");
9 assert(false.to_string() == "false");
10 // String::len
11 assert("".len() == 0);
12 assert("hello".len() == 5);
13 assert("abc".len() == 3);
14 // string concatenation
15 assert("foo" + "bar" == "foobar");
16 assert("" + "xyz" == "xyz");
17 assert("abc" + "" == "abc");
18 assert("hello" + ", " + "world" == "hello, world");
19 let who := "world";
20 assert("hello, ${who}" == "hello, world");
21 assert("n=${42}" == "n=42");
22 assert("flag=${true}" == "flag=true");
23 assert("value=${\"x\"}" == "value=x");
24 assert("pair=${\"x\" + \"y\"}" == "pair=xy");
25 assert("\${value}" == "\${value}");
26 assert("$5" == "$5");
27 // List<T>: new, push, len, get, pop, as_slice, from
28 var lst: List<i64> := List::new();
29 assert((&lst).len() == 0);
30 lst.push(10);
31 lst.push(20);
32 lst.push(30);
33 assert((&lst).len() == 3);
34 // get returns Perhaps<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Perhaps::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Perhaps::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Perhaps::Some { value } => assert(value == 30),
46 None => assert(false),
47 };
48 assert((&lst).len() == 2);
49 // as_slice returns a T[] view
50 lst.push(99);
51 let sl := (&lst).as_slice();
52 assert(sl[0] == 10);
53 assert(sl[2] == 99);
54 // List::from copies an existing T[] array
55 let src: i64[] := [1, 2, 3, 4, 5];
56 let lst2 := List::from(src);
57 assert(lst2.len() == 5);
58 // Building a list with a loop then converting to T[]
59 var built: List<i64> := List::new();
60 var i := 1;
61 while (i <= 5) {
62 built.push(i * i);
63 i += 1;
64 }
65 assert((&built).len() == 5);
66 let built_arr := (&built).as_slice();
67 assert(built_arr[0] == 1);
68 assert(built_arr[4] == 25);
69 // get at boundary indices
70 var boundary: List<i64> := List::new();
71 boundary.push(100);
72 boundary.push(200);
73 boundary.push(300);
74 match ((&boundary).get(0)) {
75 Perhaps::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Perhaps::Some { value } => assert(value == 300),
80 None => assert(false),
81 };
82 // pop on empty list returns None
83 var empty_lst: List<i64> := List::new();
84 match (empty_lst.pop()) {
85 Perhaps::Some { value } => assert(false),
86 None => assert(true),
87 };
88 // pop until empty, verifying each value
89 var drain: List<i64> := List::new();
90 drain.push(7);
91 drain.push(8);
92 drain.push(9);
93 match (drain.pop()) {
94 Perhaps::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Perhaps::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Perhaps::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Perhaps::Some { value } => assert(false),
107 None => assert(true),
108 };
109 assert(drain.len() == 0);
110 // push after pop
111 var reuse: List<i64> := List::new();
112 reuse.push(1);
113 reuse.push(2);
114 reuse.pop();
115 reuse.push(99);
116 assert((&reuse).len() == 2);
117 match ((&reuse).get(1)) {
118 Perhaps::Some { value } => assert(value == 99),
119 None => assert(false),
120 };
121 // List::from on empty array
122 let empty_src: i64[] := [];
123 let lst_from_empty := List::from(empty_src);
124 assert(lst_from_empty.len() == 0);
125 // as_slice on empty list produces empty array
126 let empty_slice := (&empty_lst).as_slice();
127 assert(empty_slice.len() == 0);
128 // List<String>
129 var words: List<String> := List::new();
130 words.push("hello");
131 words.push("world");
132 assert((&words).len() == 2);
133 match ((&words).get(0)) {
134 Perhaps::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Perhaps::Some { value } => assert(value == "world"),
139 None => assert(false),
140 };
141 assert((&words).len() == 1);
142 // List<f64>
143 var floats: List<f64> := List::new();
144 floats.push(1.5);
145 floats.push(2.5);
146 floats.push(3.5);
147 assert((&floats).len() == 3);
148 match ((&floats).get(1)) {
149 Perhaps::Some { value } => assert(value == 2.5),
150 None => assert(false),
151 };
152 // List<boolean>
153 var flags: List<boolean> := List::new();
154 flags.push(true);
155 flags.push(false);
156 flags.push(true);
157 assert((&flags).len() == 3);
158 match ((&flags).get(2)) {
159 Perhaps::Some { value } => assert(value == true),
160 None => assert(false),
161 };
162 // for-in over as_slice result
163 var sum_lst: List<i64> := List::new();
164 sum_lst.push(10);
165 sum_lst.push(20);
166 sum_lst.push(30);
167 var total := 0;
168 for (x in sum_lst.as_slice()) {
169 total += x;
170 }
171 assert(total == 60);
172}
passes
1// The primitive Display impls and the numeric From cross-product are declared
2// in the embedded std::core source (stdlib/core.mtl) and bound to host
3// implementations via native keys (METEL-181). This locks the derived path:
4// no hand-registered builtin backs any of these.
5fun main() {
6 // Display::to_string on every displayable primitive kind
7 assert(42i8.to_string() == "42");
8 assert(42i16.to_string() == "42");
9 assert(42i32.to_string() == "42");
10 assert(42i64.to_string() == "42");
11 assert(42u8.to_string() == "42");
12 assert(42u16.to_string() == "42");
13 assert(42u32.to_string() == "42");
14 assert(42u64.to_string() == "42");
15 assert(true.to_string() == "true");
16 assert('A'.to_string() == "A");
17 assert("hi".to_string() == "hi");
18
19 // Numeric From conversions via casts (int → int, int → float, float → int)
20 let a: i8 := 7i64 as i8;
21 assert(a == 7i8);
22 let b: u64 := 7i8 as u64;
23 assert(b == 7u64);
24 let c: f32 := 2i64 as f32;
25 let d: i64 := c as i64;
26 assert(d == 2);
27 let e: f64 := 3i32 as f64;
28 let f: i32 := e as i32;
29 assert(f == 3);
30
31 // Char ↔ u32 (Unicode code point)
32 let code: u32 := 'Z' as u32;
33 assert(code == 90u32);
34 let ch: Char := 90u32 as Char;
35 assert(ch == 'Z');
36
37 println("ok");
38}
passes
Iterable<T>
aspect Iterable<T> {
fun next(&var self) -> Perhaps<T>;
}
T[] (array) and Range (from .. / ..=) implement Iterable<T>L1. User-defined types may implement it to be usable in for-in.
Formal rules
Legality Rule №1
Arrays and ranges implement Iterable<T>; a user-defined type is usable in for-in only
when it implements that aspect.
Tested by
1// User-defined Iterable via aspect — for-in dispatches through next().
2
3aspect Iterable<T> {
4 fun next(&var self) -> Perhaps<T>;
5}
6
7struct Counter {
8 current: i64,
9 limit: i64,
10}
11
12extend Counter {
13 fun new(limit: i64) -> Counter {
14 return Counter { current = 0, limit = limit };
15 }
16}
17
18extend Counter: Iterable<i64> {
19 fun next(&var self) -> Perhaps<i64> {
20 if (self.current < self.limit) {
21 let val := self.current;
22 self.current := self.current + 1;
23 return Perhaps::Some { value = val };
24 }
25 return None;
26 }
27}
28
29fun main() {
30 var sum := 0;
31 let c := Counter::new(5);
32 for (x in c) {
33 sum += x;
34 }
35 assert(sum == 10); // 0+1+2+3+4
36}
passes
From<S>
aspect From<S> {
fun from(value: S) -> Self;
}
i64 implements From<f64> (truncating cast) and f64 implements From<i64>L1. The as operator desugars to T::from(value)Type casting D1. User-defined types may implement From<S> to enable as casts and ? error coercion.
Formal rules
Legality Rule №1
i64 implements From<f64> and f64 implements From<i64>; user-defined From<S>
implementations make their target type available for as casts and ? error coercion.
Tested by (2)
1// `?` operator with From coercion: E1 != E2.
2
3aspect From<T> {
4 fun from(value: T) -> Self;
5}
6
7struct ParseError {
8 msg: String,
9}
10
11struct AppError {
12 msg: String,
13}
14
15extend AppError: From<ParseError> {
16 fun from(value: ParseError) -> AppError {
17 return AppError { msg = "parse error: " + value.msg };
18 }
19}
20
21fun parse_int(s: String) -> Result<i64, ParseError> {
22 if (s == "42") {
23 return Result::Ok { value = 42 };
24 }
25 return Result::Err { error = ParseError { msg = "invalid integer" } };
26}
27
28fun load(s: String) -> Result<i64, AppError> {
29 let n := parse_int(s)?;
30 return Result::Ok { value = n * 2 };
31}
32
33fun main() {
34 match (load("42")) {
35 Result::Ok { value } => assert(value == 84),
36 Result::Err { error } => assert(false),
37 };
38
39 match (load("bad")) {
40 Result::Ok { value } => assert(false),
41 Result::Err { error } => assert(error.msg == "parse error: invalid integer"),
42 };
43}
passes
1// The primitive Display impls and the numeric From cross-product are declared
2// in the embedded std::core source (stdlib/core.mtl) and bound to host
3// implementations via native keys (METEL-181). This locks the derived path:
4// no hand-registered builtin backs any of these.
5fun main() {
6 // Display::to_string on every displayable primitive kind
7 assert(42i8.to_string() == "42");
8 assert(42i16.to_string() == "42");
9 assert(42i32.to_string() == "42");
10 assert(42i64.to_string() == "42");
11 assert(42u8.to_string() == "42");
12 assert(42u16.to_string() == "42");
13 assert(42u32.to_string() == "42");
14 assert(42u64.to_string() == "42");
15 assert(true.to_string() == "true");
16 assert('A'.to_string() == "A");
17 assert("hi".to_string() == "hi");
18
19 // Numeric From conversions via casts (int → int, int → float, float → int)
20 let a: i8 := 7i64 as i8;
21 assert(a == 7i8);
22 let b: u64 := 7i8 as u64;
23 assert(b == 7u64);
24 let c: f32 := 2i64 as f32;
25 let d: i64 := c as i64;
26 assert(d == 2);
27 let e: f64 := 3i32 as f64;
28 let f: i32 := e as i32;
29 assert(f == 3);
30
31 // Char ↔ u32 (Unicode code point)
32 let code: u32 := 'Z' as u32;
33 assert(code == 90u32);
34 let ch: Char := 90u32 as Char;
35 assert(ch == 'Z');
36
37 println("ok");
38}
passes
String Methods
Utilities since v0.9.0. All index-based operations count Unicode scalar values (matching
.len()), and are total — out-of-range indices clamp or returnNonerather than panicking.
| Method | Signature | Description |
|---|---|---|
.len() | () -> i64 | Number of characters (Unicode scalars) |
.is_empty() | () -> boolean | Whether the string has no characters |
.to_string() | () -> String | Returns the string itself |
.to_upper() | () -> String | Uppercased copy |
.to_lower() | () -> String | Lowercased copy |
.trim() | () -> String | Whitespace removed from both ends |
.trim_start() | () -> String | Leading whitespace removed |
.trim_end() | () -> String | Trailing whitespace removed |
.contains(needle) | (String) -> boolean | Whether needle occurs in the string |
.starts_with(prefix) | (String) -> boolean | Whether the string begins with prefix |
.ends_with(suffix) | (String) -> boolean | Whether the string ends with suffix |
.index_of(needle) | (String) -> Perhaps<i64> | Scalar index of the first occurrence, or None |
.split(sep) | (String) -> String[] | Split on each sep (empty sep ⇒ whole string) |
.replace(from, to) | (String, String) -> String | Replace every from with to |
.repeat(n) | (i64) -> String | The string repeated n times (n <= 0 ⇒ "") |
.chars() | () -> Char[] | The characters as an array |
.char_at(i) | (i64) -> Perhaps<Char> | Character at scalar index i, or None |
.substring(start, end) | (i64, i64) -> String | Scalar range [start, end), indices clamped |
| Associated function | Signature | Description |
|---|---|---|
String::join(parts, sep) | (String[], String) -> String | Concatenate parts with sep between each |
Strings are concatenated with the + operator.
Formal rules
Dynamic Semantics №1
String utility methods operate on Unicode scalar values; index-based operations are total,
clamping a slice boundary and returning None for an absent character or search result
rather than panicking.
Referenced by: rfc-0057
Tested by
1// std::core String utility methods (METEL-193). Native-backed methods on the
2// String primitive, auto-imported via std::core. Indexing is by Unicode scalar
3// and total (out-of-range clamps or yields None).
4
5fun main() {
6 // ── Case & trim ──────────────────────────────────────────────────────────
7 assert(" Hello, World ".trim() == "Hello, World");
8 assert(" x".trim_start() == "x");
9 assert("x ".trim_end() == "x");
10 assert("abc".to_upper() == "ABC");
11 assert("ABC".to_lower() == "abc");
12 assert("".is_empty());
13 assert(!"x".is_empty());
14
15 // ── Search & test ────────────────────────────────────────────────────────
16 assert("hello world".contains("world"));
17 assert(!"hello".contains("z"));
18 assert("hello".starts_with("he"));
19 assert("hello".ends_with("lo"));
20 assert("hello".index_of("l").unwrap_or(-1) == 2);
21 assert("hello".index_of("z").is_none());
22
23 // ── Split, join, replace ─────────────────────────────────────────────────
24 let parts := "a,b,c".split(",");
25 assert(parts.len() == 3);
26 assert(parts[0] == "a");
27 assert(parts[2] == "c");
28 assert("a-b-c".replace("-", "+") == "a+b+c");
29 assert("ab".repeat(3) == "ababab");
30 assert("ab".repeat(0) == "");
31 assert(String::join(["a", "b","c"], "-") == "a-b-c");
32 assert(String::join([] : String[], ",") == "");
33
34 // ── Chars & slicing ──────────────────────────────────────────────────────
35 // 'é' is one Unicode scalar, so chars()/len() count it once.
36 let cs := "héllo".chars();
37 assert(cs.len() == 5);
38 assert(cs[0] == 'h');
39 assert("abc".char_at(1).unwrap_or('z') == 'b');
40 assert("abc".char_at(9).is_none());
41 assert("abc".char_at(0 - 1).is_none());
42 assert("hello".substring(1, 4) == "ell");
43 assert("hello".substring(3, 100) == "lo"); // end clamps to length
44 assert("hello".substring(4, 2) == ""); // reversed range -> empty
45
46 println("ok");
47}
passes
Array Methods
T[] and [T; N] both expose:
| Method | Signature | Description |
|---|---|---|
.len() | () -> i64 | Number of elements |
Formal rules
Dynamic Semantics №1
Calling .len() on either T[] or [T; N] returns its number of elements.
Tested by
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
Char Methods
| Method / Function | Signature | Description |
|---|---|---|
u32::from(c)D1 | (Char) -> u32 | Unicode scalar value as a u32 |
Char::from(n)D1 | (u32) -> Char | Construct from a code point; runtime error if not a valid scalar value |
.to_string() | () -> String | Single-character string |
Formal rules
Dynamic Semantics №1
u32::from(c) returns c's Unicode scalar value, and Char::from(n) returns the matching
character or raises a runtime error when n is not a valid Unicode scalar value. A character's
to_string() result is its one-character string.
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
Core Sum Types
.yolo(), .ok_or(), .map_err(), and .ok()v0.10.0Perhaps<T> and Result<T, E> are the core optional/fallible types, both in
std::core and available unqualified. Their combinator methods let them be used
in pipelines without explicit match:
Perhaps<T> — Some { value: T } or None:
| Method | Signature | Description |
|---|---|---|
.is_some() | () -> boolean | Whether this is Some |
.is_none() | () -> boolean | Whether this is None |
.map(f) | <U>((T) -> U) -> Perhaps<U> | Transform the value, passing None through |
.and_then(f) | <U>((T) -> Perhaps<U>) -> Perhaps<U> | Chain a Perhaps-returning function |
.unwrap_or(d) | (T) -> T | The value, or d when None |
.unwrap_or_else(f) | (() -> T) -> T | The value, or f() when None |
.yolo() | () -> T | The value, or panics (R0013) when None |
.ok_or(error) | <E>(E) -> Result<T, E> | Some becomes Ok; None becomes Err(error) |
Result<T, E> — Ok { value: T } or Err { error: E }:
| Method | Signature | Description |
|---|---|---|
.is_ok() | () -> boolean | Whether this is Ok |
.is_err() | () -> boolean | Whether this is Err |
.map(f) | <U>((T) -> U) -> Result<U, E> | Transform the success value, passing Err through |
.and_then(f) | <U>((T) -> Result<U, E>) -> Result<U, E> | Chain a Result-returning function |
.unwrap_or(d) | (T) -> T | The success value, or d when Err |
.unwrap_or_else(f) | (() -> T) -> T | The success value, or f() when Err |
.yolo() | () -> T | The success value, or panics (R0013) when Err, including the error's debug representation |
.map_err(f) | <F>((E) -> F) -> Result<T, F> | Transform the error value, passing Ok through |
.ok() | () -> Perhaps<T> | Ok becomes Some; Err becomes None, discarding the error |
Formal rules
Dynamic Semantics №1
The listed Perhaps<T> and Result<T, E> combinators operate on their corresponding sum
variants: transforms preserve the non-selected variant, and predicates report which variant
is present.
Referenced by: rfc-0057
Tested by
1// std::core Perhaps<T> and Result<T, E> ergonomic methods (METEL-159).
2// Pure-Metel methods defined on the core sum types, auto-imported via std::core.
3//
4// map/and_then/unwrap_or/unwrap_or_else/map_err/ok all take `self` by value
5// (RFC-0067a SS3a: a value can never be moved out of a reference), so each
6// consuming call below gets its own fresh Perhaps/Result rather than reusing
7// one binding across several by-value calls.
8
9fun main() {
10 // ── Perhaps ──────────────────────────────────────────────────────────────
11 let some := Perhaps::Some { value = 21 };
12 let none: Perhaps<i64> := Perhaps::None;
13
14 assert((&some).is_some());
15 assert(!(&some).is_none());
16 assert((&none).is_none());
17 assert(!(&none).is_some());
18
19 // map transforms Some, passes through None
20 let some_map1 := Perhaps::Some { value = 21 };
21 assert(some_map1.map(|x: i64| -> i64 { x * 2 }).unwrap_or(0) == 42);
22 let none_map: Perhaps<i64> := Perhaps::None;
23 assert(none_map.map(|x: i64| -> i64 { x * 2 }).unwrap_or(-1) == -1);
24 // map may change the element type
25 let some_map2 := Perhaps::Some { value = 21 };
26 assert(some_map2.map(|x: i64| -> String { "v" }).unwrap_or("none") == "v");
27
28 // and_then chains a Perhaps-returning function
29 let some_and_then1 := Perhaps::Some { value = 21 };
30 assert(some_and_then1.and_then(|x: i64| -> Perhaps<i64> { Perhaps::Some { value = x + 1 } }).unwrap_or(0) == 22);
31 let some_and_then2 := Perhaps::Some { value = 21 };
32 assert(some_and_then2.and_then(|x: i64| -> Perhaps<i64> { Perhaps::None }).is_none());
33
34 // unwrap_or / unwrap_or_else
35 let some_unwrap1 := Perhaps::Some { value = 21 };
36 assert(some_unwrap1.unwrap_or(0) == 21);
37 let none_unwrap1: Perhaps<i64> := Perhaps::None;
38 assert(none_unwrap1.unwrap_or(7) == 7);
39 let none_unwrap2: Perhaps<i64> := Perhaps::None;
40 assert(none_unwrap2.unwrap_or_else(|| -> i64 { 9 }) == 9);
41 let some_unwrap2 := Perhaps::Some { value = 21 };
42 assert(some_unwrap2.unwrap_or_else(|| -> i64 { 9 }) == 21);
43
44 // ── Result ───────────────────────────────────────────────────────────────
45 let ok: Result<i64, String> := Result::Ok { value = 10 };
46 let err: Result<i64, String> := Result::Err { error = "boom" };
47
48 assert((&ok).is_ok());
49 assert(!(&ok).is_err());
50 assert((&err).is_err());
51 assert(!(&err).is_ok());
52
53 let ok_map: Result<i64, String> := Result::Ok { value = 10 };
54 assert(ok_map.map(|x: i64| -> i64 { x + 5 }).unwrap_or(0) == 15);
55 let err_map1: Result<i64, String> := Result::Err { error = "boom" };
56 assert(err_map1.map(|x: i64| -> i64 { x + 5 }).unwrap_or(99) == 99);
57 // map preserves the Err payload
58 let err_map2: Result<i64, String> := Result::Err { error = "boom" };
59 assert(err_map2.map(|x: i64| -> i64 { x + 5 }).is_err());
60
61 let ok_and_then1: Result<i64, String> := Result::Ok { value = 10 };
62 assert(ok_and_then1.and_then(|x: i64| -> Result<i64, String> { Result::Ok { value = x * 3 } }).unwrap_or(0) == 30);
63 let ok_and_then2: Result<i64, String> := Result::Ok { value = 10 };
64 assert(ok_and_then2.and_then(|x: i64| -> Result<i64, String> { Result::Err { error = "no" } }).is_err());
65
66 let ok_unwrap: Result<i64, String> := Result::Ok { value = 10 };
67 assert(ok_unwrap.unwrap_or(0) == 10);
68 let err_unwrap1: Result<i64, String> := Result::Err { error = "boom" };
69 assert(err_unwrap1.unwrap_or(3) == 3);
70 let err_unwrap2: Result<i64, String> := Result::Err { error = "boom" };
71 assert(err_unwrap2.unwrap_or_else(|| -> i64 { 4 }) == 4);
72
73 println("ok");
74}
passes
List<T>
List<T> is the growable collection type in std::core, available unqualified.
List<T>.set(i, value) overwrites an element in place.set(i, value) was added alongside RFC-0126 because index assignment through an immutable
T[] no longer works. It gives in-place algorithms, such as bubble sort, an operation for
replacing an element in a List<T>.
| Method / function | Signature | Description |
|---|---|---|
List::new() | () -> List<T> | A new empty list |
List::from(arr) | (T[]) -> List<T> | A list with a copy of the array's elements |
.push(x) | (&var self, T) | Append an element |
.pop() | (&var self) -> Perhaps<T> | Remove and return the last element |
.len() | () -> i64 | Number of elements |
.get(i) | (i64) -> Perhaps<T> | Element at index i, or None |
.set(i, value) | (&var self, i64, T) -> Perhaps<T> | Overwrite the element at i; returns the replaced value, or None if i is out of bounds |
.as_slice() | () -> T[] | The backing array |
.map(f) | <U>((T) -> U) -> List<U> | A new list of f applied to each element |
.filter(pred) | ((T) -> boolean) -> List<T> | The elements satisfying pred |
.fold(init, f) | <A>(A, (A, T) -> A) -> A | Reduce to a single value, left to right |
.find(pred) | ((T) -> boolean) -> Perhaps<T> | The first element satisfying pred |
.concat(other) | (&List | This list's elements followed by other's |
Formal rules
Dynamic Semantics №2
List<T> collection and iteration methods are methods of List<T> in std::core, not
free functions in separate collection or iteration modules.
Referenced by: rfc-0057
Tested by
1// std::core List<T> collection + iteration ergonomics (METEL-160).
2// Pure-Metel methods over the backing array, auto-imported via std::core.
3
4fun main() {
5 let xs := List::from([1, 2, 3, 4, 5]);
6
7 // map (same and changed element type)
8 let doubled := (&xs).map(|x: i64| -> i64 { x * 2 });
9 assert((&doubled).len() == 5);
10 assert((&doubled).get(0).unwrap_or(0) == 2);
11 assert((&doubled).get(4).unwrap_or(0) == 10);
12 let labels := (&xs).map(|x: i64| -> String { "n" });
13 assert((&labels).len() == 5);
14 assert((&labels).get(0).unwrap_or("") == "n");
15
16 // filter
17 let evens := (&xs).filter(|x: i64| -> boolean { x % 2 == 0 });
18 assert((&evens).len() == 2);
19 assert((&evens).get(0).unwrap_or(0) == 2);
20 assert((&evens).get(1).unwrap_or(0) == 4);
21 let none_pass := (&xs).filter(|x: i64| -> boolean { x > 99 });
22 assert((&none_pass).len() == 0);
23
24 // fold (sum and a type-changing fold to String length count)
25 let sum := (&xs).fold(0i64, |acc: i64, x: i64| -> i64 { acc + x });
26 assert(sum == 15);
27 let count := (&xs).fold(0i64, |acc: i64, x: i64| -> i64 { acc + 1 });
28 assert(count == 5);
29
30 // find
31 assert((&xs).find(|x: i64| -> boolean { x > 3 }).unwrap_or(0) == 4);
32 assert((&xs).find(|x: i64| -> boolean { x > 99 }).is_none());
33
34 // concat
35 let extra := List::from([6, 7]);
36 let joined := (&xs).concat(&extra);
37 assert((&joined).len() == 7);
38 assert((&joined).get(5).unwrap_or(0) == 6);
39 assert((&joined).get(6).unwrap_or(0) == 7);
40
41 // chaining transforms
42 let result := xs
43 .filter(|x: i64| -> boolean { x % 2 == 1 })
44 .map(|x: i64| -> i64 { x * 10 })
45 .fold(0i64, |a: i64, x: i64| -> i64 { a + x });
46 assert(result == 90);
47
48 println("ok");
49}
passes
Dynamic Semantics №1
List::from(source) copies the elements of source, so mutating the resulting list
does not mutate that source.
Referenced by: rfc-0054
Tested by
OsError
OsError is the error type returned by the host-backed standard-library modules
(std::fs, std::process). It is in std::core (available unqualified) and
implements Display.
| Method | Signature | Description |
|---|---|---|
.message() | () -> String | The human-readable error description |
Formal rules
Legality Rule №1
Host-backed fallible APIs use OsError, rather than String, as their error type; OsError
is available from std::core and implements Display.
Referenced by: rfc-0057
Tested by
1// std::fs host module (METEL-165): text file ops returning Result<_, OsError>.
2// Uses a unique temp directory and cleans up after itself.
3import std::fs::{
4 read_to_string, write_string, append_string, exists, read_dir,
5 create_dir_all, remove_file, remove_dir_all,
6};
7
8fun main() {
9 // create_dir_all + exists
10 assert(create_dir_all("/tmp/metel_fs_fixture_x7q2").is_ok());
11 assert(exists("/tmp/metel_fs_fixture_x7q2"));
12
13 // write then append, read back the concatenation
14 assert(write_string("/tmp/metel_fs_fixture_x7q2/note.txt", "hello").is_ok());
15 assert(append_string("/tmp/metel_fs_fixture_x7q2/note.txt", " world").is_ok());
16 assert(exists("/tmp/metel_fs_fixture_x7q2/note.txt"));
17 assert(read_to_string("/tmp/metel_fs_fixture_x7q2/note.txt").unwrap_or("") == "hello world");
18
19 // read_dir returns the single entry name
20 let entries := read_dir("/tmp/metel_fs_fixture_x7q2").unwrap_or([]);
21 assert(entries.len() == 1);
22 assert(entries[0] == "note.txt");
23
24 // error path: a missing file yields Err with a non-empty OsError message
25 match (read_to_string("/tmp/metel_fs_fixture_x7q2/missing.txt")) {
26 Result::Ok { value } => assert(false),
27 Result::Err { error } => assert(error.message().len() > 0),
28 }
29
30 // cleanup
31 assert(remove_file("/tmp/metel_fs_fixture_x7q2/note.txt").is_ok());
32 assert(remove_dir_all("/tmp/metel_fs_fixture_x7q2").is_ok());
33 assert(!exists("/tmp/metel_fs_fixture_x7q2"));
34
35 println("ok");
36}
passes
Standard Library Modules
These modules are not auto-imported — a program must import them explicitly
(e.g. import std::fs::{read_to_string, write_string};). Their operations are
host-backed.
std::env
Read-only process environment inspection.
| Function | Signature | Description |
|---|---|---|
get(name) | (String) -> Perhaps<String> | The value of an environment variable, or None |
vars() | () -> EnvVar[] | All environment variables (EnvVar { name, value }) |
Formal rules
Dynamic Semantics №1
std::env exposes read-only process-environment inspection through get and vars; it is
an explicitly imported host-backed module, not part of the automatic prelude.
Referenced by: rfc-0057
Tested by
1// std::env host module (METEL-164): explicit import, read-only environment
2// inspection. Deterministic — does not assume any particular variable is set.
3import std::env::{get, vars, EnvVar};
4
5fun main() {
6 // A name that should not exist in any test environment resolves to None.
7 assert(get("METEL_DEFINITELY_UNSET_9z8y7x").is_none());
8
9 // vars() yields an EnvVar[]; List ergonomics apply after List::from, and
10 // mapping preserves the element count.
11 let all := vars();
12 let names := List::from(all).map(|e: EnvVar| -> String { e.name });
13 assert(names.len() == all.len());
14
15 println("ok");
16}
passes
std::fs
Text-oriented file operations. Fallible operations return Result<_, OsError>.
| Function | Signature | Description |
|---|---|---|
read_to_string(path) | (String) -> Result<String, OsError> | Read an entire file into a string |
write_string(path, s) | (String, String) -> Result<(), OsError> | Write s, replacing any existing file |
append_string(path, s) | (String, String) -> Result<(), OsError> | Append s, creating the file if absent |
exists(path) | (String) -> boolean | Whether a file or directory exists |
read_dir(path) | (String) -> Result<String[], OsError> | The entry names within a directory |
create_dir(path) | (String) -> Result<(), OsError> | Create a single directory |
create_dir_all(path) | (String) -> Result<(), OsError> | Create a directory and all parents |
remove_file(path) | (String) -> Result<(), OsError> | Remove a file |
remove_dir(path) | (String) -> Result<(), OsError> | Remove an empty directory |
remove_dir_all(path) | (String) -> Result<(), OsError> | Remove a directory and its contents |
Formal rules
Dynamic Semantics №1
std::fs is an explicitly imported host-backed module whose text-oriented file operations
have the signatures listed above and report fallible outcomes as Result<_, OsError>.
Referenced by: rfc-0057
Tested by
1// std::fs host module (METEL-165): text file ops returning Result<_, OsError>.
2// Uses a unique temp directory and cleans up after itself.
3import std::fs::{
4 read_to_string, write_string, append_string, exists, read_dir,
5 create_dir_all, remove_file, remove_dir_all,
6};
7
8fun main() {
9 // create_dir_all + exists
10 assert(create_dir_all("/tmp/metel_fs_fixture_x7q2").is_ok());
11 assert(exists("/tmp/metel_fs_fixture_x7q2"));
12
13 // write then append, read back the concatenation
14 assert(write_string("/tmp/metel_fs_fixture_x7q2/note.txt", "hello").is_ok());
15 assert(append_string("/tmp/metel_fs_fixture_x7q2/note.txt", " world").is_ok());
16 assert(exists("/tmp/metel_fs_fixture_x7q2/note.txt"));
17 assert(read_to_string("/tmp/metel_fs_fixture_x7q2/note.txt").unwrap_or("") == "hello world");
18
19 // read_dir returns the single entry name
20 let entries := read_dir("/tmp/metel_fs_fixture_x7q2").unwrap_or([]);
21 assert(entries.len() == 1);
22 assert(entries[0] == "note.txt");
23
24 // error path: a missing file yields Err with a non-empty OsError message
25 match (read_to_string("/tmp/metel_fs_fixture_x7q2/missing.txt")) {
26 Result::Ok { value } => assert(false),
27 Result::Err { error } => assert(error.message().len() > 0),
28 }
29
30 // cleanup
31 assert(remove_file("/tmp/metel_fs_fixture_x7q2/note.txt").is_ok());
32 assert(remove_dir_all("/tmp/metel_fs_fixture_x7q2").is_ok());
33 assert(!exists("/tmp/metel_fs_fixture_x7q2"));
34
35 println("ok");
36}
passes
std::process
Command-line arguments and shell-free synchronous subprocess execution.
| Function | Signature | Description |
|---|---|---|
args() | () -> String[] | The process command-line arguments |
run(command, args) | (String, String[]) -> Result<ProcessOutput, OsError> | Run command with args, capturing output |
run executes the command directly — there is no shell, so quoting and shell
expansion never apply. A non-zero exit status is a successful Ok result, not
an error; only a failure to launch the command is an Err. The result type is
ProcessOutput { status: i64, stdout: String, stderr: String }.
Formal rules
Dynamic Semantics №1
std::process::run launches command directly with args, without shell parsing. A launched
program returns Ok(ProcessOutput) even for a non-zero exit status; only failure to launch
returns Err(OsError).
Tested by
1// std::process host module (METEL-166): argv and shell-free subprocess run.
2// Uses POSIX `true`/`false` for deterministic exit statuses.
3import std::process::{args, run, ProcessOutput};
4
5fun status_of(r: Result<ProcessOutput, OsError>) -> i64 {
6 match (r) {
7 Result::Ok { value } => value.status,
8 Result::Err { error } => -999,
9 }
10}
11
12fun main() {
13 // argv is always non-empty (at least the interpreter path).
14 assert(args().len() > 0);
15
16 // A successful command exits 0; a failing one exits non-zero — both are Ok
17 // (a non-zero status is a result, not an error).
18 assert(status_of(run("true", [])) == 0);
19 assert(status_of(run("false", [])) != 0);
20
21 // A command that cannot be launched is an Err carrying an OsError message.
22 match (run("metel_no_such_command_z9", [])) {
23 Result::Ok { value } => assert(false),
24 Result::Err { error } => assert(error.message().len() > 0),
25 }
26
27 println("ok");
28}
passes