Lexical Structure
Comments
// Single-line comment
/* Multi-line
comment */
Multi-line comments do not nest.
Identifiers
Identifiers start with a letter (a–z, A–Z) or underscore, followed by any combination of letters, digits, or underscores.
identifier := [a-zA-Z_][a-zA-Z0-9_]*
By convention:
- Types, structs, enums, and aspects use
PascalCase - Variables, functions, and fields use
snake_case
Keywords
as aspect break continue else enum export
extend false for fun if impl import
let loop match public return root self
std struct super true var where while
Literals
Integers — decimal, with optional _ separators:
42
1_000_000
A suffix pins the literal to a specific sized typeL1:
42i32 // i32
255u8 // u8
1_000i64 // i64
An unsuffixed integer literal defaults to i64Sized numeric t… L3 when no context constrains its type.
Floats:
3.14
2.0
A suffix pins the literal to a specific sized float type:
3.14f32 // f32
2.0f64 // f64
An unsuffixed float literal defaults to f64Sized numeric t… L3 when no context constrains its type.
Integer and float are distinct types and do not implicitly coerceL3.
Polymorphic literal coercion. When the surrounding context provides a numeric type — a let annotation, a function parameter type, a struct field type, or a return type — an unsuffixed literal adopts that type automaticallySized numeric t… L3:
let x: i32 := 10; // 10 is i32
let y: u8 := 255; // 255 is u8
let z: f32 := 3.14; // 3.14 is f32
fun add(a: i32, b: i32) -> i32 { a + b }
let r := add(1, 2); // 1 and 2 are i32
struct Pixel { r: u8, g: u8, b: u8 }
let p := Pixel { r = 255, g = 128, b = 0 }; // fields are u8
Arithmetic and comparison operators propagate the type from a sized operand to an unsuffixed sibling:
let x: i32 := 10i32;
let y := x + 5; // 5 adopts i32; y is i32
assert(x > 5); // 5 adopts i32
Characters — single-quoted Unicode scalar values:
'a'
'\n'
'\t'
'\\'
'\''
'\u{1F600}'
The type of a character literal is CharL5.
Charv0.8.0Strings — double-quoted UTF-8:
| Sequence | Meaning |
|---|---|
\n | Newline |
\t | Tab |
\\ | Backslash |
\" | Double quote |
\r | Carriage return |
String interpolation. A string literal may contain one or more ${expr} placeholders:
let name := "world";
let msg := "hello, ${name}!"; // "hello, world!"
let n := 42;
let s := "n=${n}"; // "n=42"
The expression inside ${…} may be any expression whose type implements the Display aspect (i.e. has a .to_string() method). The placeholder desugars to .to_string() concatenated with the surrounding literal fragments using +. String literals may appear inside ${…}:
let x := "${if (true) { "yes" } else { "no" }}";
"Any expression" is deliberate, and includes control flow, closures, and side effects.
Because ${…} re-parses its content as an ordinary expression, and if/match/loop and
immediately-invoked closures are all ordinary expressions, a ${…} placeholder is not
limited to "format an already-computed value" — a loop, a mutation, or a call with an
observable effect can run as a side effect of constructing the string. This is a deliberate
design choice (metel-core#704↗),
not an oversight: restricting ${…} to calls only would break idiomatic usage this corpus
already depends on (the if/else example above), for a purity guarantee the rest of the
language does not otherwise make today. This puts Metel's interpolation with Kotlin's,
Swift's, and C#'s full-expression model rather than Rust's macro-based one — Rust needs
format! to be a macro because it has no string-literal grammar rule of its own to attach
interpolation to; Metel does, so no macro workaround is needed.
This is not necessarily permanent. Once an effect system exists, whether an effect-performing call should be allowed inside
${…}is an open design question — not a soundness one, since effect-row inference sees the fully lowered form regardless of whether the effectful call sits inside a literal or not, but a discoverability one: a${…}site reads as data, and nothing marks it as a place a computation can suspend and hand control to a handler. Two narrower restrictions (comptime-only, place-expressions-only) are already ruled out against the current corpus; an effect-axis restriction specifically —${…}may not perform an effect — is the only one that would remain viable, and it is only expressible once an effect system lands, so today's full-expression scope holds by default until then. Seealgebraic-effects.md§15 and Open Question 7 (active design report, not yet an RFC).
fun side_effect() -> i64 {
println("side effect!");
7
}
fun main() {
println("start ${side_effect()} end");
}
// prints:
// side effect!
// start 7 end
The call inside ${…} runs — and its own println fires — while the outer string is still
being constructed, before println("start ${side_effect()} end")'s own argument is even
fully evaluated. Per the Dynamic Semantics rules below, each placeholder's expression is
evaluated exactly once, in source order, with the same evaluation semantics as anywhere
else expr is legal.
String concatenation. Two String values may be joined with +:
let full := "hello" + ", " + "world"; // "hello, world"
Formal rules
Dynamic Semantics №1
A string literal may contain ${expr} placeholders; each placeholder's expression is
rendered to text and the result is a String.
Referenced by: rfc-0010
Tested by
1fun main() {
2 // to_string method
3 assert(0.to_string() == "0");
4 assert(42.to_string() == "42");
5 assert((-7).to_string() == "-7");
6 assert(1.5.to_string() == "1.5");
7 assert(0.0.to_string() == "0");
8 assert(true.to_string() == "true");
9 assert(false.to_string() == "false");
10 // String::len
11 assert("".len() == 0);
12 assert("hello".len() == 5);
13 assert("abc".len() == 3);
14 // string concatenation
15 assert("foo" + "bar" == "foobar");
16 assert("" + "xyz" == "xyz");
17 assert("abc" + "" == "abc");
18 assert("hello" + ", " + "world" == "hello, world");
19 let who := "world";
20 assert("hello, ${who}" == "hello, world");
21 assert("n=${42}" == "n=42");
22 assert("flag=${true}" == "flag=true");
23 assert("value=${\"x\"}" == "value=x");
24 assert("pair=${\"x\" + \"y\"}" == "pair=xy");
25 assert("\${value}" == "\${value}");
26 assert("$5" == "$5");
27 // List<T>: new, push, len, get, pop, as_slice, from
28 var lst: List<i64> := List::new();
29 assert((&lst).len() == 0);
30 lst.push(10);
31 lst.push(20);
32 lst.push(30);
33 assert((&lst).len() == 3);
34 // get returns Perhaps<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Perhaps::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Perhaps::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Perhaps::Some { value } => assert(value == 30),
46 None => assert(false),
47 };
48 assert((&lst).len() == 2);
49 // as_slice returns a T[] view
50 lst.push(99);
51 let sl := (&lst).as_slice();
52 assert(sl[0] == 10);
53 assert(sl[2] == 99);
54 // List::from copies an existing T[] array
55 let src: i64[] := [1, 2, 3, 4, 5];
56 let lst2 := List::from(src);
57 assert(lst2.len() == 5);
58 // Building a list with a loop then converting to T[]
59 var built: List<i64> := List::new();
60 var i := 1;
61 while (i <= 5) {
62 built.push(i * i);
63 i += 1;
64 }
65 assert((&built).len() == 5);
66 let built_arr := (&built).as_slice();
67 assert(built_arr[0] == 1);
68 assert(built_arr[4] == 25);
69 // get at boundary indices
70 var boundary: List<i64> := List::new();
71 boundary.push(100);
72 boundary.push(200);
73 boundary.push(300);
74 match ((&boundary).get(0)) {
75 Perhaps::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Perhaps::Some { value } => assert(value == 300),
80 None => assert(false),
81 };
82 // pop on empty list returns None
83 var empty_lst: List<i64> := List::new();
84 match (empty_lst.pop()) {
85 Perhaps::Some { value } => assert(false),
86 None => assert(true),
87 };
88 // pop until empty, verifying each value
89 var drain: List<i64> := List::new();
90 drain.push(7);
91 drain.push(8);
92 drain.push(9);
93 match (drain.pop()) {
94 Perhaps::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Perhaps::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Perhaps::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Perhaps::Some { value } => assert(false),
107 None => assert(true),
108 };
109 assert(drain.len() == 0);
110 // push after pop
111 var reuse: List<i64> := List::new();
112 reuse.push(1);
113 reuse.push(2);
114 reuse.pop();
115 reuse.push(99);
116 assert((&reuse).len() == 2);
117 match ((&reuse).get(1)) {
118 Perhaps::Some { value } => assert(value == 99),
119 None => assert(false),
120 };
121 // List::from on empty array
122 let empty_src: i64[] := [];
123 let lst_from_empty := List::from(empty_src);
124 assert(lst_from_empty.len() == 0);
125 // as_slice on empty list produces empty array
126 let empty_slice := (&empty_lst).as_slice();
127 assert(empty_slice.len() == 0);
128 // List<String>
129 var words: List<String> := List::new();
130 words.push("hello");
131 words.push("world");
132 assert((&words).len() == 2);
133 match ((&words).get(0)) {
134 Perhaps::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Perhaps::Some { value } => assert(value == "world"),
139 None => assert(false),
140 };
141 assert((&words).len() == 1);
142 // List<f64>
143 var floats: List<f64> := List::new();
144 floats.push(1.5);
145 floats.push(2.5);
146 floats.push(3.5);
147 assert((&floats).len() == 3);
148 match ((&floats).get(1)) {
149 Perhaps::Some { value } => assert(value == 2.5),
150 None => assert(false),
151 };
152 // List<boolean>
153 var flags: List<boolean> := List::new();
154 flags.push(true);
155 flags.push(false);
156 flags.push(true);
157 assert((&flags).len() == 3);
158 match ((&flags).get(2)) {
159 Perhaps::Some { value } => assert(value == true),
160 None => assert(false),
161 };
162 // for-in over as_slice result
163 var sum_lst: List<i64> := List::new();
164 sum_lst.push(10);
165 sum_lst.push(20);
166 sum_lst.push(30);
167 var total := 0;
168 for (x in sum_lst.as_slice()) {
169 total += x;
170 }
171 assert(total == 60);
172}
passes
Dynamic Semantics №2
Placeholder expressions are evaluated once each, in source order.
Referenced by: rfc-0010
Tested by
1// RFC-0010 §3: interpolation evaluates each placeholder exactly once, left to right.
2fun next(counter: &var i64) -> i64 {
3 *counter += 1;
4 *counter
5}
6
7fun main() {
8 var counter := 0;
9 let message := "${next(&var counter)} ${next(&var counter)}";
10 assert(message == "1 2");
11 assert(counter == 2);
12}
passes
Dynamic Semantics №3
Interpolation combines literal fragments and rendered placeholder values using ordinary string-concatenation semantics.
Referenced by: rfc-0010
Tested by
1fun main() {
2 // to_string method
3 assert(0.to_string() == "0");
4 assert(42.to_string() == "42");
5 assert((-7).to_string() == "-7");
6 assert(1.5.to_string() == "1.5");
7 assert(0.0.to_string() == "0");
8 assert(true.to_string() == "true");
9 assert(false.to_string() == "false");
10 // String::len
11 assert("".len() == 0);
12 assert("hello".len() == 5);
13 assert("abc".len() == 3);
14 // string concatenation
15 assert("foo" + "bar" == "foobar");
16 assert("" + "xyz" == "xyz");
17 assert("abc" + "" == "abc");
18 assert("hello" + ", " + "world" == "hello, world");
19 let who := "world";
20 assert("hello, ${who}" == "hello, world");
21 assert("n=${42}" == "n=42");
22 assert("flag=${true}" == "flag=true");
23 assert("value=${\"x\"}" == "value=x");
24 assert("pair=${\"x\" + \"y\"}" == "pair=xy");
25 assert("\${value}" == "\${value}");
26 assert("$5" == "$5");
27 // List<T>: new, push, len, get, pop, as_slice, from
28 var lst: List<i64> := List::new();
29 assert((&lst).len() == 0);
30 lst.push(10);
31 lst.push(20);
32 lst.push(30);
33 assert((&lst).len() == 3);
34 // get returns Perhaps<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Perhaps::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Perhaps::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Perhaps::Some { value } => assert(value == 30),
46 None => assert(false),
47 };
48 assert((&lst).len() == 2);
49 // as_slice returns a T[] view
50 lst.push(99);
51 let sl := (&lst).as_slice();
52 assert(sl[0] == 10);
53 assert(sl[2] == 99);
54 // List::from copies an existing T[] array
55 let src: i64[] := [1, 2, 3, 4, 5];
56 let lst2 := List::from(src);
57 assert(lst2.len() == 5);
58 // Building a list with a loop then converting to T[]
59 var built: List<i64> := List::new();
60 var i := 1;
61 while (i <= 5) {
62 built.push(i * i);
63 i += 1;
64 }
65 assert((&built).len() == 5);
66 let built_arr := (&built).as_slice();
67 assert(built_arr[0] == 1);
68 assert(built_arr[4] == 25);
69 // get at boundary indices
70 var boundary: List<i64> := List::new();
71 boundary.push(100);
72 boundary.push(200);
73 boundary.push(300);
74 match ((&boundary).get(0)) {
75 Perhaps::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Perhaps::Some { value } => assert(value == 300),
80 None => assert(false),
81 };
82 // pop on empty list returns None
83 var empty_lst: List<i64> := List::new();
84 match (empty_lst.pop()) {
85 Perhaps::Some { value } => assert(false),
86 None => assert(true),
87 };
88 // pop until empty, verifying each value
89 var drain: List<i64> := List::new();
90 drain.push(7);
91 drain.push(8);
92 drain.push(9);
93 match (drain.pop()) {
94 Perhaps::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Perhaps::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Perhaps::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Perhaps::Some { value } => assert(false),
107 None => assert(true),
108 };
109 assert(drain.len() == 0);
110 // push after pop
111 var reuse: List<i64> := List::new();
112 reuse.push(1);
113 reuse.push(2);
114 reuse.pop();
115 reuse.push(99);
116 assert((&reuse).len() == 2);
117 match ((&reuse).get(1)) {
118 Perhaps::Some { value } => assert(value == 99),
119 None => assert(false),
120 };
121 // List::from on empty array
122 let empty_src: i64[] := [];
123 let lst_from_empty := List::from(empty_src);
124 assert(lst_from_empty.len() == 0);
125 // as_slice on empty list produces empty array
126 let empty_slice := (&empty_lst).as_slice();
127 assert(empty_slice.len() == 0);
128 // List<String>
129 var words: List<String> := List::new();
130 words.push("hello");
131 words.push("world");
132 assert((&words).len() == 2);
133 match ((&words).get(0)) {
134 Perhaps::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Perhaps::Some { value } => assert(value == "world"),
139 None => assert(false),
140 };
141 assert((&words).len() == 1);
142 // List<f64>
143 var floats: List<f64> := List::new();
144 floats.push(1.5);
145 floats.push(2.5);
146 floats.push(3.5);
147 assert((&floats).len() == 3);
148 match ((&floats).get(1)) {
149 Perhaps::Some { value } => assert(value == 2.5),
150 None => assert(false),
151 };
152 // List<boolean>
153 var flags: List<boolean> := List::new();
154 flags.push(true);
155 flags.push(false);
156 flags.push(true);
157 assert((&flags).len() == 3);
158 match ((&flags).get(2)) {
159 Perhaps::Some { value } => assert(value == true),
160 None => assert(false),
161 };
162 // for-in over as_slice result
163 var sum_lst: List<i64> := List::new();
164 sum_lst.push(10);
165 sum_lst.push(20);
166 sum_lst.push(30);
167 var total := 0;
168 for (x in sum_lst.as_slice()) {
169 total += x;
170 }
171 assert(total == 60);
172}
passes
Dynamic Semantics №4
Within a string literal, \${ produces the literal characters ${.
Referenced by: rfc-0010
Tested by
1fun main() {
2 // to_string method
3 assert(0.to_string() == "0");
4 assert(42.to_string() == "42");
5 assert((-7).to_string() == "-7");
6 assert(1.5.to_string() == "1.5");
7 assert(0.0.to_string() == "0");
8 assert(true.to_string() == "true");
9 assert(false.to_string() == "false");
10 // String::len
11 assert("".len() == 0);
12 assert("hello".len() == 5);
13 assert("abc".len() == 3);
14 // string concatenation
15 assert("foo" + "bar" == "foobar");
16 assert("" + "xyz" == "xyz");
17 assert("abc" + "" == "abc");
18 assert("hello" + ", " + "world" == "hello, world");
19 let who := "world";
20 assert("hello, ${who}" == "hello, world");
21 assert("n=${42}" == "n=42");
22 assert("flag=${true}" == "flag=true");
23 assert("value=${\"x\"}" == "value=x");
24 assert("pair=${\"x\" + \"y\"}" == "pair=xy");
25 assert("\${value}" == "\${value}");
26 assert("$5" == "$5");
27 // List<T>: new, push, len, get, pop, as_slice, from
28 var lst: List<i64> := List::new();
29 assert((&lst).len() == 0);
30 lst.push(10);
31 lst.push(20);
32 lst.push(30);
33 assert((&lst).len() == 3);
34 // get returns Perhaps<T> (bounds-checked)
35 match ((&lst).get(1)) {
36 Perhaps::Some { value } => assert(value == 20),
37 None => assert(false),
38 };
39 match ((&lst).get(99)) {
40 Perhaps::Some { value } => assert(false),
41 None => assert(true),
42 };
43 // pop removes and returns the last element
44 match (lst.pop()) {
45 Perhaps::Some { value } => assert(value == 30),
46 None => assert(false),
47 };
48 assert((&lst).len() == 2);
49 // as_slice returns a T[] view
50 lst.push(99);
51 let sl := (&lst).as_slice();
52 assert(sl[0] == 10);
53 assert(sl[2] == 99);
54 // List::from copies an existing T[] array
55 let src: i64[] := [1, 2, 3, 4, 5];
56 let lst2 := List::from(src);
57 assert(lst2.len() == 5);
58 // Building a list with a loop then converting to T[]
59 var built: List<i64> := List::new();
60 var i := 1;
61 while (i <= 5) {
62 built.push(i * i);
63 i += 1;
64 }
65 assert((&built).len() == 5);
66 let built_arr := (&built).as_slice();
67 assert(built_arr[0] == 1);
68 assert(built_arr[4] == 25);
69 // get at boundary indices
70 var boundary: List<i64> := List::new();
71 boundary.push(100);
72 boundary.push(200);
73 boundary.push(300);
74 match ((&boundary).get(0)) {
75 Perhaps::Some { value } => assert(value == 100),
76 None => assert(false),
77 };
78 match ((&boundary).get(2)) {
79 Perhaps::Some { value } => assert(value == 300),
80 None => assert(false),
81 };
82 // pop on empty list returns None
83 var empty_lst: List<i64> := List::new();
84 match (empty_lst.pop()) {
85 Perhaps::Some { value } => assert(false),
86 None => assert(true),
87 };
88 // pop until empty, verifying each value
89 var drain: List<i64> := List::new();
90 drain.push(7);
91 drain.push(8);
92 drain.push(9);
93 match (drain.pop()) {
94 Perhaps::Some { value } => assert(value == 9),
95 None => assert(false),
96 };
97 match (drain.pop()) {
98 Perhaps::Some { value } => assert(value == 8),
99 None => assert(false),
100 };
101 match (drain.pop()) {
102 Perhaps::Some { value } => assert(value == 7),
103 None => assert(false),
104 };
105 match (drain.pop()) {
106 Perhaps::Some { value } => assert(false),
107 None => assert(true),
108 };
109 assert(drain.len() == 0);
110 // push after pop
111 var reuse: List<i64> := List::new();
112 reuse.push(1);
113 reuse.push(2);
114 reuse.pop();
115 reuse.push(99);
116 assert((&reuse).len() == 2);
117 match ((&reuse).get(1)) {
118 Perhaps::Some { value } => assert(value == 99),
119 None => assert(false),
120 };
121 // List::from on empty array
122 let empty_src: i64[] := [];
123 let lst_from_empty := List::from(empty_src);
124 assert(lst_from_empty.len() == 0);
125 // as_slice on empty list produces empty array
126 let empty_slice := (&empty_lst).as_slice();
127 assert(empty_slice.len() == 0);
128 // List<String>
129 var words: List<String> := List::new();
130 words.push("hello");
131 words.push("world");
132 assert((&words).len() == 2);
133 match ((&words).get(0)) {
134 Perhaps::Some { value } => assert(value == "hello"),
135 None => assert(false),
136 };
137 match (words.pop()) {
138 Perhaps::Some { value } => assert(value == "world"),
139 None => assert(false),
140 };
141 assert((&words).len() == 1);
142 // List<f64>
143 var floats: List<f64> := List::new();
144 floats.push(1.5);
145 floats.push(2.5);
146 floats.push(3.5);
147 assert((&floats).len() == 3);
148 match ((&floats).get(1)) {
149 Perhaps::Some { value } => assert(value == 2.5),
150 None => assert(false),
151 };
152 // List<boolean>
153 var flags: List<boolean> := List::new();
154 flags.push(true);
155 flags.push(false);
156 flags.push(true);
157 assert((&flags).len() == 3);
158 match ((&flags).get(2)) {
159 Perhaps::Some { value } => assert(value == true),
160 None => assert(false),
161 };
162 // for-in over as_slice result
163 var sum_lst: List<i64> := List::new();
164 sum_lst.push(10);
165 sum_lst.push(20);
166 sum_lst.push(30);
167 var total := 0;
168 for (x in sum_lst.as_slice()) {
169 total += x;
170 }
171 assert(total == 60);
172}
passes
Legality Rule №1
An integer literal with an integer suffix has the suffix's sized integer type; a float literal with a float suffix has the suffix's sized float type.
Tested by (2)
1fun main() {
2 let a: i8 := 42i8;
3 let b: i16 := 1000i16;
4 let c: i32 := 100000i32;
5 let d: u8 := 255u8;
6 let e: u16 := 65535u16;
7 let f: u32 := 4294967295u32;
8 let g: u64 := 18446744073709551615u64;
9
10 assert(a == 42i8);
11 assert(b == 1000i16);
12 assert(c == 100000i32);
13 assert(d == 255u8);
14 assert(e == 65535u16);
15 assert(f == 4294967295u32);
16 assert(g == 18446744073709551615u64);
17}
passes
Legality Rule №3
An integer literal and a float literal do not implicitly coerce between integer and float types.
Tested by
1fun main() {
2 // An unsuffixed `5` could adopt f64 here; the i64 suffix makes this literal concrete.
3 let _value: f64 := 5i64;
4}
typecheck errorT0001“cannot unify i64 with f64”
Legality Rule №5
A character literal has type Char.
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
Booleans: true, false
Absence literal: None
Operators
| Category | Operators |
|---|---|
| Arithmetic | + - * / % |
| Compound assign | += -= *= /= %= |
| Comparison | == != < <= > >= |
| Logical | && || ! |
| Assignment | = |
| Error prop | ? |
| Type cast | as |
| Path | :: |
| Range | .. ..= (for use in for-in only) |