Functions
fun add(a: i64, b: i64) -> i64 {
return a + b;
}
fun main() -> i64 {
return add(2, 3);
}
Named Function Declarations
Parameter type annotations are optional when types can be inferred from contextL2. The return type follows -> and is also optional — a function with no return annotation and no return expr; returns ()D1. return expr; and bare return; are both valid.
Formal rules
Legality Rule №1
Named function declarations begin with fun; fun is not an anonymous-function
expression introducer.
Referenced by: rfc-0041
Tested by
1fun make_adder(x: i64) -> |i64| -> i64 {
2 |y: i64| -> i64 { x + y }
3}
4fun apply(f: |i64| -> i64, v: i64) -> i64 {
5 f(v)
6}
7fun main() {
8 let add5 := make_adder(5);
9 let r1 := add5(3);
10 assert(r1 == 8);
11 let r2 := add5(10);
12 assert(r2 == 15);
13 // Pass a closure to a higher-order function.
14 let add10 := make_adder(10);
15 let r3 := apply(add10, 7);
16 assert(r3 == 17);
17 // Closure created inline.
18 let double := |n: i64| -> i64 { n * 2 };
19 let r4 := double(6);
20 assert(r4 == 12);
21}
passes
Legality Rule №2
Function parameter and return-type annotations may be omitted when their types can be inferred from context.
Tested by
Dynamic Semantics №1
A function with no return annotation and no return expr; returns ().
Tested by
Associated Functions
extend blocks may contain functions with no self parameter. These are called on
the type via :: syntaxL1 and serve as
the canonical constructor pattern:
struct Point {
x: f64,
y: f64,
}
extend Point {
fun new(x: f64, y: f64) -> Point {
return Point { x = x, y = y };
}
}
fun main() -> i64 {
let p := Point::new(1.0, 2.0);
return p.x as i64;
}
Formal rules
Legality Rule №1
A function declared in an extend block without a self, &self, or &var self
parameter is an associated function and is called through its target type with ::.
Tested by
1// Regression: a type has both inherent methods and aspect methods.
2// Elaboration must mark the aspect call as Aspect{..} and the inherent call as Inherent,
3// so the evaluator routes each to the right impl without one shadowing the other.
4
5aspect Printable {
6 fun summary(&self) -> String;
7}
8
9struct Counter {
10 value: i64,
11 step: i64,
12}
13
14extend Counter {
15 // Inherent constructor and methods
16 fun new(start: i64, step: i64) -> Counter {
17 Counter { value = start, step = step }
18 }
19 fun tick(&var self) {
20 self.value := self.value + self.step;
21 }
22 fun value(&self) -> i64 { self.value }
23}
24
25extend Counter: Printable {
26 // Aspect method — must not conflict with the inherent `value` method above.
27 fun summary(&self) -> String {
28 "Counter(" + self.value.to_string() + ", step=" + self.step.to_string() + ")"
29 }
30}
31
32fun main() {
33 var c := Counter::new(0, 3);
34
35 // Inherent methods work
36 assert(c.value() == 0);
37 c.tick();
38 assert(c.value() == 3);
39 c.tick();
40 assert(c.value() == 6);
41
42 // Aspect method works alongside inherent methods
43 assert(c.summary() == "Counter(6, step=3)");
44
45 // Both still work after another tick
46 c.tick();
47 assert(c.value() == 9);
48 assert(c.summary() == "Counter(9, step=3)");
49}
passes
First-Class Functions
Functions are first-class values and can be assigned, passed, and returnedL2:
fun add(a: i64, b: i64) -> i64 {
return a + b;
}
fun apply(f: |i64| -> i64, x: i64) -> i64 {
return f(x);
}
fun main() -> i64 {
let f := add;
let inc := |x: i64| -> i64 { return x + 1; };
return f(1, 2) + apply(inc, 4);
}
The type of a function or closure is written as |ParamTypes| -> ReturnType (RFC-0154;
(ParamTypes) -> ReturnType before v0.13.0). -> and the return type are always present
in a written function type.
A named function declared with its own <T> generics (fun identity<T>(x: T) -> T { ... }) may always be called directly (identity(3), identity::<i64>(3)).
A generic named function may be bound with a bare, unannotated let; the binding stays
polymorphic, so its later uses may each instantiate it differently (let alias = identity; alias(3); alias("x");). It may also be passed as a higher-order argument when the receiving
parameter position is concrete — for example, apply(identity, 3) when apply's parameter
is |i64| -> i64, not itself generic.
Referencing it in a position where nothing pins down a concrete instantiation —
a parameter position that is itself still generic in the callee (rank-2), or an
expression position with no expected type and no enclosing let — is still
T0003. There is also no standalone instantiation-without-calling value form:
identity::<i64> not immediately followed by a call is a parse error, not a
type error — see TurbofishL3.
A written function type (|T| -> U) is move-only for its by-value useL3,
at every nesting depth: a function value may be called through it any number of
times, but using it by value twice — let a := f; let b := f; — is a
use-after-move, even when the underlying value (a named function, or a closure
whose captures are all Copy) is itself copyable (ClosuresL20).
Copyability doesn't survive the written type: it's erased the moment such a value
flows into a slot whose written type is a function typeL4 —
a let / var binding, an ascription, an argument, or a return — and isn't
recovered further downstream. A bare generic type parameter is not a written
function type and keeps the resolved value's own capability. The full surface —
a copy |T| -> U qualifier for an explicitly-copyable callable, and a distinct
"capability unknown" state — is deferred to RFC-0163 (v0.17.0), which refines
this move-only state rather than replacing it.
Formal rules
Legality Rule №1
Function and closure types use |ParameterTypes| -> ReturnType (RFC-0154); -> and the
return type are always written. Neither (ParameterTypes) -> ReturnType (the spelling
before v0.13.0) nor fun(ParameterTypes) -> ReturnType is a function-type syntax.
Referenced by: rfc-0041, rfc-0154
Tested by (2)
1fun make_adder(x: i64) -> |i64| -> i64 {
2 |y: i64| -> i64 { x + y }
3}
4fun apply(f: |i64| -> i64, v: i64) -> i64 {
5 f(v)
6}
7fun main() {
8 let add5 := make_adder(5);
9 let r1 := add5(3);
10 assert(r1 == 8);
11 let r2 := add5(10);
12 assert(r2 == 15);
13 // Pass a closure to a higher-order function.
14 let add10 := make_adder(10);
15 let r3 := apply(add10, 7);
16 assert(r3 == 17);
17 // Closure created inline.
18 let double := |n: i64| -> i64 { n * 2 };
19 let r4 := double(6);
20 assert(r4 == 12);
21}
passes
1fun takes(g: || -> i64) -> i64 {
2 g()
3}
4
5fun mk() -> || -> i64 {
6 || { 42 }
7}
8
9fun main() {
10 let tail := || { 42 };
11 let tail_n := tail();
12 assert(tail_n + 1 == 43);
13
14 let with_return := || { return 42; };
15 let return_n := with_return();
16 assert(return_n + 1 == 43);
17
18 let from_array := [|| { 42 }];
19 let array_n := from_array[0]();
20 assert(array_n + 1 == 43);
21
22 let add1 := |a: i64| { a + 1 };
23 let param_n := add1(1);
24 assert(param_n + 1 == 3);
25
26 let explicit_ret := || -> i64 { return 42; };
27 assert(explicit_ret() == 42);
28
29 let annotated_binding: || -> i64 := || { return 42; };
30 assert(annotated_binding() == 42);
31
32 let annotated_call := tail();
33 let annotated_site: i64 := annotated_call;
34 assert(annotated_site == 42);
35
36 assert(takes(|| { 42 }) == 42);
37 assert(mk()() == 42);
38}
passes
Legality Rule №2
A non-generic named function and a closure are values of their function type and may be
bound, passed as arguments, and returned as results. A generic named function (declared
with its own <T> generics) may be called directly, bound with a bare unannotated let
(staying polymorphic across that binding's own later uses), or passed as a higher-order
argument whose receiving parameter position is itself concrete. Referencing it anywhere
else that doesn't pin down a concrete instantiation — including a parameter position
that is itself still generic in the callee — is T0003.
Referenced by: rfc-0138
Tested by (8)
1// 03 — Functions and closures
2// Covers: named functions, first-class functions, closures, closure captures,
3// mut capture, generic functions, higher-order functions, closure type signatures,
4// unannotated parameters (types inferred from context).
5
6fun add(a: i64, b: i64) -> i64 {
7 return a + b;
8}
9
10fun apply(f: |i64| -> i64, x: i64) -> i64 {
11 return f(x);
12}
13
14fun apply2(f: |i64, i64| -> i64, a: i64, b: i64) -> i64 {
15 return f(a, b);
16}
17
18fun make_adder(n: i64) -> |i64| -> i64 {
19 return |x| { return x + n; }; // x inferred as i64 from return type
20}
21
22fun identity<T>(value: T) -> T {
23 return value;
24}
25
26fun map_array(arr: i64[], f: |i64| -> i64) -> i64[] {
27 var result: i64[] := [];
28 for (item in arr) {
29 array_push(result, f(item));
30 }
31 return result;
32}
33
34fun main() {
35 // --- basic call ---
36 println(int_to_string(add(3, 4))); // 7
37
38 // --- function assigned to variable ---
39 let f := add;
40 println(int_to_string(f(10, 20))); // 30
41
42 // --- closure passed to function (params inferred from apply's signature) ---
43 let doubled := apply(|x| { return x * 2; }, 5);
44 println(int_to_string(doubled)); // 10
45
46 // --- closure capturing an outer variable ---
47 let factor := 3;
48 let tripled := apply(|x| { return x * factor; }, 7);
49 println(int_to_string(tripled)); // 21
50
51 // --- closure mutating a captured mut variable ---
52 var count := 0;
53 let increment := || { count += 1; };
54 increment();
55 increment();
56 increment();
57 println(int_to_string(count)); // 3
58
59 // --- higher-order: make_adder returns a closure ---
60 let add5 := make_adder(5);
61 println(int_to_string(add5(10))); // 15
62 println(int_to_string(add5(100))); // 105
63
64 // --- generic function ---
65 println(int_to_string(identity(42))); // 42
66 println(identity("hello")); // hello
67
68 // --- apply2 with named function ---
69 println(int_to_string(apply2(add, 8, 9))); // 17
70
71 // --- map_array (closure params inferred from map_array's signature) ---
72 let nums: i64[] := [1, 2, 3, 4, 5];
73 let squares := map_array(nums, |x| { return x * x; });
74 for (n in squares) {
75 print(int_to_string(n) + " "); // 1 4 9 16 25
76 }
77 println("");
78}
passes
1// Regression (metel-core#736, RFC-0138§1): a bare reference to an already-declared
2// generic function -- `let alias = identity;`, with no call syntax at the
3// reference site -- used to fail typechecking entirely ("generic function
4// `identity` cannot be referenced except by direct call"), even though a
5// non-generic named function is genuinely first-class today (`let f = add;`).
6
7fun identity<T>(x: T) -> T { return x; }
8
9fun main() {
10 let alias := identity;
11 assert(alias(3) == 3);
12 assert(alias("hi") == "hi");
13}
passes
1// Regression (metel-core#736, RFC-0138): a `let`-bound alias of a generic
2// function stays genuinely polymorphic across its own later uses -- the same
3// let-polymorphism guarantee `52_let_polymorphism.mtl` already covers for a
4// closure literal, here for a bare reference to a *named* generic function.
5
6fun identity<T>(x: T) -> T { return x; }
7fun first_of_two<T>(a: T, b: T) -> T { return a; }
8
9fun main() {
10 let id_alias := identity;
11
12 // Used at i64, boolean, String.
13 assert(id_alias(42) == 42);
14 assert(id_alias(true));
15 assert(id_alias("hello") == "hello");
16
17 // In arithmetic context, and nested (an alias call feeding another).
18 assert(id_alias(10) + id_alias(20) == 30);
19 assert(id_alias(id_alias(99)) == 99);
20
21 // A second, independently generic alias in the same scope.
22 let pick_alias := first_of_two;
23 assert(pick_alias(7, 8) == 7);
24 assert(pick_alias("x", "y") == "x");
25}
passes
1// Regression (metel-core#736, RFC-0138§4): a generic function referenced by
2// name, passed directly as a higher-order argument to a *monomorphic* function
3// (its own parameter position fully concrete) -- rank-1, one instantiation at
4// this one call site. Companion to `99_list_map_named_function.mtl`, which
5// covers the same shape for a non-generic named function argument.
6
7fun identity<T>(x: T) -> T { return x; }
8
9fun apply_i64(f: |i64| -> i64, x: i64) -> i64 { return f(x); }
10fun apply_str(f: |String| -> String, x: String) -> String { return f(x); }
11
12fun main() {
13 assert(apply_i64(identity, 7) == 7);
14 assert(apply_str(identity, "hi") == "hi");
15}
passes
1// Regression (metel-core#736, RFC-0138 §5): the bare-reference and
2// higher-order-argument cases both need to work identically for a *nested*
3// generic function, not just a top-level one -- `hoist_fun_decls` already
4// makes nested mutual recursion work per-block on the inference side, and the
5// construction-side `fn_table` hoist mirrors that for this mechanism.
6
7fun main() {
8 fun identity<T>(x: T) -> T { return x; }
9 fun apply_i64(f: |i64| -> i64, x: i64) -> i64 { return f(x); }
10
11 let alias := identity;
12 assert(alias(5) == 5);
13 assert(alias("nested") == "nested");
14 assert(apply_i64(identity, 9) == 9);
15}
passes
1// #736 / RFC-0138: a bare reference to an already-declared generic function
2// now typechecks -- superseding the old `stage10_neg_04` fixture, which
3// asserted the call-only restriction this change lifts. See
4// `evaluator/generics/101_generic_fn_bare_reference.mtl` for the runtime
5// counterpart.
6
7fun identity<T>(x: T) -> T { return x; }
8
9fun main() {
10 let alias := identity;
11 alias(3);
12}
passes
1// #736 / RFC-0138 §4: a generic function named directly in a higher-order
2// argument position now typechecks, when the receiving parameter position is
3// itself concrete -- superseding the old `stage10_neg_05` fixture, which
4// asserted the call-only restriction this change lifts. `apply` itself being
5// non-generic isolates the case: it's `identity`'s own referenceability being
6// exercised here, not `apply`'s. See
7// `evaluator/generics/103_generic_fn_higher_order_argument.mtl` for the
8// runtime counterpart.
9
10fun identity<T>(x: T) -> T { return x; }
11fun apply(f: |i64| -> i64, x: i64) -> i64 { return f(x); }
12
13fun main() {
14 apply(identity, 3);
15}
passes
1// Negative, deliberately out of scope (RFC-0138 §5): a generic function passed to
2// a parameter position that is itself still generic in the callee -- rank-2
3// polymorphism -- stays call-only. #736/RFC-0138 only lifted the restriction for a
4// bare reference and a higher-order argument whose *receiving* parameter is
5// concrete (see stage10_10/stage10_11); here `apply_twice`'s own `f: F` is not.
6
7fun identity<T>(x: T) -> T { return x; }
8fun apply_twice<F, T>(f: F, x: T) -> T { return f(x); }
9
10fun main() {
11 apply_twice(identity, 5);
12}
typecheck errorT0003at 11
Legality Rule №3
A written function type (|T| -> U) has move-only by-value use-multiplicity at
every nesting depth, matching exactly against a copyable one: a value of it may
be called (subject to once / var) and moved, but not duplicated by value. A
bare generic parameter is not a written function type.
Referenced by: rfc-0166
Tested by (6)
1// v0.13.0 cross-feature (integration session, metel-core#956): struct pattern
2// matching binds a field whose declared type is a written function type
3// (RFC-0154 pipe notation), and that binding is move-only (RFC-0166). Calling
4// the bound closure once is fine; the negative half -- using it twice by value
5// -- is covered by typechecking/structs/neg_49.
6struct Pair { op: |i64| -> i64, n: i64 }
7
8fun main() {
9 let p := Pair { op = |x: i64| { x * 2 }, n = 5 };
10 let r := match (p) {
11 Pair { op, n } => op(n),
12 };
13 assert(r == 10);
14 println("ok");
15}
passes
1// v0.13.0 cross-feature (integration session, metel-core#956): a struct-pattern
2// binding of a written-function-type field (RFC-0154) is move-only (RFC-0166).
3// Using it by value twice inside the arm is a use-after-move under --move-check.
4struct Pair { op: |i64| -> i64, n: i64 }
5
6fun main() {
7 let p := Pair { op = |x: i64| { x * 2 }, n = 5 };
8 let r := match (p) {
9 Pair { op, n } => {
10 let a := op;
11 let b := op; // use after move
12 a(n) + b(n)
13 },
14 };
15 println("unreachable");
16}
typecheck errorT0019“moved”
1// v0.13.0 (RFC-0166): a parameter whose written type is a function type is
2// move-only inside the callee, regardless of what the caller passed. `add_one`
3// is a copyable named function, but `consume` sees `f: |i64| -> i64` as
4// move-only -- moving it into `a` leaves nothing for the second `let`.
5//
6// This is the migration case RFC-0166 calls out: a body that used a
7// bare-typed callback by value more than once. Needs move_check = true.
8
9fun add_one(x: i64) -> i64 { x + 1 }
10
11fun consume(f: |i64| -> i64) -> i64 {
12 let a := f; // moves `f`
13 let b := f; // moved-value error
14 a(1) + b(2)
15}
16
17fun main() {
18 println(consume(add_one));
19}
typecheck errorT0019“moved”
1// v0.13.0 (RFC-0166): a binding whose *written* type is a function type `|i64|
2// -> i64` is move-only, regardless of what was assigned to it. `add_one` is a
3// copyable named function, but the annotation erases that: `f` may be moved
4// once, and the second `let` is an ordinary use-after-move (RFC-0071 / T0019).
5//
6// Needs move_check = true -- this is the general affine-move check, not a
7// closure-specific always-on rule.
8
9fun add_one(x: i64) -> i64 { x + 1 }
10
11fun main() {
12 let f: |i64| -> i64 := add_one;
13 let a := f; // moves `f`
14 let b := f; // moved-value error -- `f` is move-only under RFC-0166
15 println(a(1) + b(2));
16}
typecheck errorT0019“moved”
1// v0.13.0 (RFC-0166): a *written* function type `|T| -> U` is move-only. A
2// function value the compiler proved copyable -- a named function here -- is
3// accepted into such a slot by moving. The written type does not carry the
4// copyability forward: `pick` returns its argument through a written function
5// return type, so the result the caller gets back is a plain move-only value.
6//
7// A `many` `reading` call is a shared borrow, not a consume, so calling a
8// written-fn-typed parameter any number of times is fine -- only a repeated
9// *by-value* use is the use-after-move. The companion negatives
10// `typechecking/functions/v0_13_0_neg_written_fn_type_use_after_move.mtl` and
11// `..._neg_written_fn_param_use_after_move.mtl` cover that direction.
12
13fun add_one(x: i64) -> i64 { x + 1 }
14
15// first-order: a named (Copy) function into a written `|i64| -> i64` parameter,
16// called twice -- repeated calls do not move.
17fun apply_twice(f: |i64| -> i64, x: i64) -> i64 { f(x) + f(x + 1) }
18
19// written function *return* type -- the value handed back is move-only.
20fun pick(f: |i64| -> i64) -> |i64| -> i64 { f }
21
22fun main() {
23 assert(apply_twice(add_one, 10) == 23); // (10+1) + (11+1)
24
25 let chosen := pick(add_one);
26 assert(chosen(9) == 10);
27
28 // a capture-free closure literal is copyable too and is accepted the same way.
29 assert(apply_twice(|n: i64| { n * 2 }, 10) == 42); // 20 + 22
30
31 println("ok");
32}
passes
1// v0.13.0 cross-feature (integration session, metel-core#956): the closure
2// mutation axis (RFC-0153), pipe notation (RFC-0154), and written function
3// types lowering to move-only (RFC-0166) together. A `[&var acc] var` closure
4// is moved -- once, move-only -- into a `var |i64| -> i64` parameter and called
5// twice inside the callee; call lowering dispatches on the value's own mutation
6// axis, and the `&var` capture writes through to the caller's `acc`.
7fun run_twice(f: var |i64| -> i64, x: i64) -> i64 {
8 let a := f(x);
9 f(a)
10}
11
12fun main() {
13 var acc := 0;
14 let step := [&var acc] var |d: i64| -> i64 { acc := acc + d; acc };
15 let r := run_twice(step, 3); // f(3): acc 0->3 ret 3 ; f(3): acc 3->6 ret 6
16 assert(r == 6);
17 assert(acc == 6);
18 println("ok");
19}
passes
Legality Rule №4
A function value the compiler proved copyable is accepted into a written function-type slot by moving; the slot's copyability is not carried by the written type and is not recoverable downstream.
Tested by (2)
1// v0.13.0 (RFC-0166): a binding whose *written* type is a function type `|i64|
2// -> i64` is move-only, regardless of what was assigned to it. `add_one` is a
3// copyable named function, but the annotation erases that: `f` may be moved
4// once, and the second `let` is an ordinary use-after-move (RFC-0071 / T0019).
5//
6// Needs move_check = true -- this is the general affine-move check, not a
7// closure-specific always-on rule.
8
9fun add_one(x: i64) -> i64 { x + 1 }
10
11fun main() {
12 let f: |i64| -> i64 := add_one;
13 let a := f; // moves `f`
14 let b := f; // moved-value error -- `f` is move-only under RFC-0166
15 println(a(1) + b(2));
16}
typecheck errorT0019“moved”
1// v0.13.0 (RFC-0166): a *written* function type `|T| -> U` is move-only. A
2// function value the compiler proved copyable -- a named function here -- is
3// accepted into such a slot by moving. The written type does not carry the
4// copyability forward: `pick` returns its argument through a written function
5// return type, so the result the caller gets back is a plain move-only value.
6//
7// A `many` `reading` call is a shared borrow, not a consume, so calling a
8// written-fn-typed parameter any number of times is fine -- only a repeated
9// *by-value* use is the use-after-move. The companion negatives
10// `typechecking/functions/v0_13_0_neg_written_fn_type_use_after_move.mtl` and
11// `..._neg_written_fn_param_use_after_move.mtl` cover that direction.
12
13fun add_one(x: i64) -> i64 { x + 1 }
14
15// first-order: a named (Copy) function into a written `|i64| -> i64` parameter,
16// called twice -- repeated calls do not move.
17fun apply_twice(f: |i64| -> i64, x: i64) -> i64 { f(x) + f(x + 1) }
18
19// written function *return* type -- the value handed back is move-only.
20fun pick(f: |i64| -> i64) -> |i64| -> i64 { f }
21
22fun main() {
23 assert(apply_twice(add_one, 10) == 23); // (10+1) + (11+1)
24
25 let chosen := pick(add_one);
26 assert(chosen(9) == 10);
27
28 // a capture-free closure literal is copyable too and is accepted the same way.
29 assert(apply_twice(|n: i64| { n * 2 }, 10) == 42); // 20 + 22
30
31 println("ok");
32}
passes
Closures
Anonymous functions are written with the [captures]? once? var? |params| (-> ret)? { body } formL1:
fun main() -> i64 {
let double := |x: i64| -> i64 { return x * 2; };
return double(5);
}
Capture lists
A closure that reads an outer binding captures it. A capture list […] before the
parameter list names each captured binding with a specifier:
fun main() {
var count := 0;
let cfg := Config::load(); // non-Copy, read-only in the closure
let name := "log"; // non-Copy, moved in
let handler := [&var count, &cfg, name] var |req: Request| -> Response {
count += 1;
route(req, cfg, name)
};
}
[&var x]capturesxby exclusive reference; the body may read and write it.[&x]capturesxby shared reference; the body may only read it.[x]capturesxby value — a copy for aCopybinding, a move for a non-Copyone (the outer binding is consumed).[x.clone()]captures an explicit independent copy of aClonebinding, leaving the outer binding usable.
The list is required whenever the closure references a free non-Copy local, or
captures anything by & / &varL6; it is omissible
only when every free variable is a Copy binding captured by value, or there are none.
When present it is exhaustiveL7: every free
local binding the body references must appear. Module-level functions, constants, types,
and aspects are not captures and never appear in the list.
A &var capture requires the outer binding to be declared varL13,
and a closure literal cannot reference its own let bindingL13.
A [&x] capture is read-only: assigning to it, or taking &var of it, in the body is a
compile error at the captureL21, not a silent
mutating upgrade. Capturing a free variable of a type parameter T follows the
bounds in scope at the closure's definitionL17 —
unbounded T is non-Copy for every instantiation. When an inner closure captures a
binding that is itself an enclosing closure's capture, the enclosing closure must list
it, an inner [s] cannot move out of an enclosing [&s] borrow, and an inner [s] that
moves an enclosing-held binding makes the enclosing closure
onceL22; an inner & / &var of an enclosing
by-value capture is an interim rejectionL11.
Call multiplicity and the mutation axis
A closure's function type carries two written qualifiers besides its parameter and return types:
once— invoking the closure consumes one of its captures. Written when the body moves a capture out (returns it, or passes it by value to something that takes ownership). Omitting it when the body consumes a capture is an errorL8; the default is many (reusable).var— invoking the closure mutates a capture. Written when the body assigns to a by-value capture, takes&varof one, or calls a&var selfmethod on one, and always when the closure captures[&var x]L25, regardless of what the body does through it. The default is reading.
The two qualifiers are order-insensitive as a type spellingL24;
in a closure literal the fixed order is [captures] once? var? (params)L23.
An unqualified literal in a typed position takes its once / var from the expected
typeL18 rather than defaulting and then failing.
Verification runs in a L19; the two axes are checked
independently.
A function value may be used where a less permissive multiplicity is
expectedL9 — a many value satisfies a once slot, a
reading value satisfies a var slot, a Copy value satisfies a non-Copy slot — at
first-order argument, ascription, field-init, and return positions. The reverse is
rejected. A conditional's type is the least-permissive of its arms, each arm widening to
it. Widening changes only the static typeD12: a
widened reading value keeps its plain call behaviour, and a var-typed field that holds
one is thereafter observed var, with no re-narrowingD14.
Whether a closure value is Copy is exactly whether every capture is
CopyL20; a Copy closure is necessarily many.
A mutating call needs exclusive access to the closure value for the call's
durationL10: the callee must be an owned binding, an
owned temporary, an exclusive projection off one, or a &var parameter — not a
shared-& callee. Overlapping and reentrant mutating calls on the same closure value
are rejectedD9. If a var call exits early via ? or
return, its partial mutations persist and the closure stays
callableD13; a panic is uncatchable and ends the
process, so no post-exit state is observable.
Capture semantics
By-value capture of a non-Copy binding moves it into the closure at creation, consuming
the outer binding; a Copy binding is copied; the captured environment is built once and
not re-cloned per callD5.
fun make_counter() -> var || -> i64 {
let n := 0;
[n] var || -> i64 { n += 1; n } // `n` moved in; writes persist
}
fun main() -> i64 {
var c := make_counter(); // `var`: a `mutating` call is a `&var self`-shaped borrow of `c` (legality-10)
c();
c() // returns 2 — state lives in `c`'s environment
}
A mutating closure's writes to its captures persist across
callsD7; a reading closure reads its environment in
place. Copying a Copy mutating closure gives the copy independent environment
stateD8 — the copies do not share a counter.
A [&x] / [&var x] capture stores a reference in the environment; the borrow is held
for the closure value's whole lifetime. Captured owned values are dropped when the
closure value is droppedD11, in capture-list order.
Closures satisfy no aspectsL12: ==, <, .clone(),
and other aspect-gated operations on closure values do not type-check — including Share
once RFC-0158 (1-under-review) adds it. A closure's Send / Sync follows the
aggregate rule over its capturesL14; a mutating
closure is not Sync.
Formal rules
Legality Rule №1
An anonymous function is written as an optional capture list […], optional once and/or
var qualifiers, a pipe-delimited parameter list |…|, an optional -> ReturnType
annotation, and a body block. It may appear wherever an expression is accepted. (RFC-0154:
before v0.13.0 the parameter list was parenthesized and -> was written before every
body; |…| self-disambiguates, so -> now appears only with a return type.)
Referenced by: rfc-0041, rfc-0154
Tested by (2)
1fun make_adder(x: i64) -> |i64| -> i64 {
2 |y: i64| -> i64 { x + y }
3}
4fun apply(f: |i64| -> i64, v: i64) -> i64 {
5 f(v)
6}
7fun main() {
8 let add5 := make_adder(5);
9 let r1 := add5(3);
10 assert(r1 == 8);
11 let r2 := add5(10);
12 assert(r2 == 15);
13 // Pass a closure to a higher-order function.
14 let add10 := make_adder(10);
15 let r3 := apply(add10, 7);
16 assert(r3 == 17);
17 // Closure created inline.
18 let double := |n: i64| -> i64 { n * 2 };
19 let r4 := double(6);
20 assert(r4 == 12);
21}
passes
1fun takes(g: || -> i64) -> i64 {
2 g()
3}
4
5fun mk() -> || -> i64 {
6 || { 42 }
7}
8
9fun main() {
10 let tail := || { 42 };
11 let tail_n := tail();
12 assert(tail_n + 1 == 43);
13
14 let with_return := || { return 42; };
15 let return_n := with_return();
16 assert(return_n + 1 == 43);
17
18 let from_array := [|| { 42 }];
19 let array_n := from_array[0]();
20 assert(array_n + 1 == 43);
21
22 let add1 := |a: i64| { a + 1 };
23 let param_n := add1(1);
24 assert(param_n + 1 == 3);
25
26 let explicit_ret := || -> i64 { return 42; };
27 assert(explicit_ret() == 42);
28
29 let annotated_binding: || -> i64 := || { return 42; };
30 assert(annotated_binding() == 42);
31
32 let annotated_call := tail();
33 let annotated_site: i64 := annotated_call;
34 assert(annotated_site == 42);
35
36 assert(takes(|| { 42 }) == 42);
37 assert(mk()() == 42);
38}
passes
Legality Rule №2
A closure literal is introduced by its |…| parameter list; the -> is written only when
a return type is (RFC-0154, superseding RFC-0041's rule that -> precede every body). A
bare block { … } with no preceding |…| is a block expression, not a closure.
Referenced by: rfc-0041, rfc-0154
Tested by
Legality Rule №3
A zero-argument closure is written || { body } (RFC-0154; () -> { body } before
v0.13.0). A bare block { … } is not an anonymous function.
Referenced by: rfc-0041
Tested by
1fun takes(g: || -> i64) -> i64 {
2 g()
3}
4
5fun mk() -> || -> i64 {
6 || { 42 }
7}
8
9fun main() {
10 let tail := || { 42 };
11 let tail_n := tail();
12 assert(tail_n + 1 == 43);
13
14 let with_return := || { return 42; };
15 let return_n := with_return();
16 assert(return_n + 1 == 43);
17
18 let from_array := [|| { 42 }];
19 let array_n := from_array[0]();
20 assert(array_n + 1 == 43);
21
22 let add1 := |a: i64| { a + 1 };
23 let param_n := add1(1);
24 assert(param_n + 1 == 3);
25
26 let explicit_ret := || -> i64 { return 42; };
27 assert(explicit_ret() == 42);
28
29 let annotated_binding: || -> i64 := || { return 42; };
30 assert(annotated_binding() == 42);
31
32 let annotated_call := tail();
33 let annotated_site: i64 := annotated_call;
34 assert(annotated_site == 42);
35
36 assert(takes(|| { 42 }) == 42);
37 assert(mk()() == 42);
38}
passes
Legality Rule №4
The former anonymous fun(parameters) -> return_type { body } spelling is rejected.
Referenced by: rfc-0041
Tested by
1fun main() {
2 let f = fun(x: i64) -> i64 { return x + 1; };
3}
parse errorP0001
Legality Rule №5
A capture list is [ followed by zero or more comma-separated capture items and ]. A
capture item is &var ident, &ident, ident, or ident.clone(). A binding may not
appear more than once in the list, under any combination of specifiers. The literal prefix
order is legality-23L23; the function-type spelling's
order rules are legality-24L24.
Referenced by: rfc-0050
Tested by
1// v0.13.0 closure cluster (RFC 0050 legality-5): a capture list holds all four
2// specifier forms -- `&var ident`, `&ident`, `ident` (bare), `ident.clone()`.
3//
4fun main() {
5 var count := 0;
6 let cfg := 10;
7 let tag := 42;
8 let name := "log";
9 var handler := [&var count, &cfg, tag, name.clone()] var |req: i64| {
10 count += 1;
11 req + cfg + tag
12 };
13 assert(handler(1) == 53);
14 assert(count == 1);
15 assert(name == "log"); // outer `name` still usable -- `[name.clone()]` didn't move it
16}
passes
Legality Rule №23
In a closure literal the prefixes appear in one fixed order: capture list, then once,
then var, then the parameter list. var once, a qualifier before the capture list, and
a capture list placed after a qualifier are parse errors — even though the corresponding
function type spelling is order-insensitive (legality-24L24).
Tested by
1// v0.13.0 closure cluster (RFC 0050 legality-23): the closure *literal*
2// prefix order is fixed -- capture list, then `once`, then `var`, then the
3// parameter list. `var once` (and a qualifier before the capture list) is a
4// parse error, even though the function *type* spelling is order-insensitive
5// (legality-24).
6//
7fun main() {
8 let c := 1;
9 let f := [c] var once || -> i64 { c }; // wrong order -- must be `once var`
10 f();
11}
parse error“var”
Legality Rule №24
As a function type spelling the once and var qualifiers are order-insensitive:
once var |T| -> U and var once |T| -> U denote the identical Type::Fun. The
fixed order of legality-23L23 is a grammar rule for
closure literals only.
Referenced by: rfc-0153, rfc-0154
Tested by
1// v0.13.0 closure cluster (RFC 0153 legality-24): as a function *type*
2// spelling, `once` and `var` are order-insensitive -- `var once (T) -> U` and
3// `once var (T) -> U` denote the identical type. (The closure *literal* prefix
4// order stays fixed -- legality-23.)
5//
6fun apply_it(f: var once |String| -> String) -> String {
7 f("x")
8}
9
10fun main() {
11 var log := 0;
12 let s := "seed";
13 // literal order is `once var` (legality-23); body mutates `log` (-> var) and
14 // moves captured `s` out (-> once), so `once var` is genuinely correct.
15 let g := [&var log, s] once var |extra: String| {
16 log += 1;
17 s
18 };
19 assert(apply_it(g) == "seed"); // param typed `var once` -- same type, unifies
20 assert(log == 1);
21}
passes
Legality Rule №6
A closure must carry a capture list if its body references a free non-Copy local
binding, or captures any binding by & or &var. Referencing a free non-Copy local
with no capture list is a compile error. A closure whose only free variables are Copy
bindings used by value, or which has no free variables, may omit the list.
Referenced by: rfc-0050
Tested by
1// v0.13.0 closure cluster (RFC 0050 legality-6): a closure must carry a
2// capture list if its body references a free non-Copy local binding.
3//
4fun main() {
5 let name := "log";
6 let greet := || { name }; // missing capture list for `name`
7 assert(greet() == "log");
8}
typecheck errorT0026“capture”
Legality Rule №7
When a closure has a capture list, every free local binding its body references must appear in the list, with a specifier consistent with how the body uses it. A referenced free local absent from a non-empty list is a compile error. Module-level functions, constants, types, and aspects are resolved by ordinary name resolution and are never capture items.
Tested by
1// v0.13.0 closure cluster (RFC 0050 legality-7): once a capture list is
2// present it must be exhaustive -- every free local the body references must
3// appear, even `Copy` ones. Module-level items are never capture items.
4//
5fun main() {
6 let a := 1;
7 let b := 2;
8 let f := [a] || { a + b }; // `b` referenced but missing from the list
9 f();
10}
typecheck error“capture”
Legality Rule №8
once is a written qualifier, verified against the body at the closure's creation site;
the default is many. A closure whose body moves a non-Copy capture out — returns it,
or passes it by value to something that takes ownership — written without once, is a
compile error naming the offending capture and the fix (add once, or stop moving the
capture).
Referenced by: rfc-0134
Tested by
1// v0.13.0 closure cluster (RFC 0134 legality-8): a closure whose body moves
2// a non-Copy capture out must be written `once`; omitting it is a compile
3// error at the definition site.
4//
5fun main() {
6 let s := "hello";
7 let take := [s] || { s }; // moves `s` out; missing `once`
8 take();
9}
typecheck errorT0027“once”
Legality Rule №25
var is a written qualifier, verified against the body at the closure's creation site;
the default is reading. A closure whose body assigns to a by-value capture, takes &var
of one, or calls a &var self method on one — and always a closure that captures any
binding [&var …], regardless of what the body does through it — written without var,
is a compile error naming the offending capture and the fix (add var, stop the mutation,
or capture [&x] instead).
Referenced by: rfc-0153
Tested by (2)
1// v0.13.0 closure cluster (RFC 0153 legality-25 / metel-core#959): a `[&var x]`
2// capture over a `var` binding still requires the `var` qualifier on the closure
3// literal itself. The diagnostic names the pipe spelling introduced by RFC-0154
4// -- `[...] var |...| { ... }` -- not the parenthesized form that RFC removed.
5fun main() {
6 var count := 0;
7 let bump := [&var count] || { count := count + 1; }; // missing `var` qualifier
8 bump();
9}
typecheck errorT0028“var |...| { ... }”
1// v0.13.0 closure cluster (RFC 0153 legality-25): a closure whose body
2// assigns to a by-value capture must be written `var`; omitting it is a
3// compile error at the definition site.
4//
5fun main() {
6 let n := 0;
7 let bump := [n] || { n := n + 1; n }; // mutates `n`; missing `var`
8 bump();
9}
typecheck errorT0028“var”
Legality Rule №9
A function value of call multiplicity m, mutation u, and Copy-ness c satisfies a
slot requiring m', u', c' when m is at least as permissive as m' (many ≥
once), u at least as permissive as u' (reading ≥ var), and c at least as
permissive as c' (Copy ≥ non-Copy). The reverse — a less permissive value into a
more permissive slot — is rejected.
Referenced by: rfc-0134, rfc-0152
Tested by (2)
1// v0.13.0 closure cluster (RFC 0152 legality-9): a `many` function value may
2// be used where a `once` slot is expected -- one-directional widening. The
3// widened value is still `many`; it just satisfies the more permissive
4// requirement.
5//
6fun call_once(f: once || -> i64) -> i64 {
7 f()
8}
9
10fun main() {
11 let five := || { 5 }; // `many` by default -- reads no captures
12 assert(call_once(five) == 5);
13}
passes
1// v0.13.0 closure cluster (RFC 0152 legality-9): widening is one-directional
2// -- a `once` function value may NOT be used where a `many` slot is
3// expected; the reverse of legality-9 is the ordinary type mismatch.
4//
5fun call_many(f: || -> String) -> String {
6 f() + f() // calls twice -- needs `many`
7}
8
9fun main() {
10 let s := "hello";
11 let once_closure := [s] once || { s }; // returns `s`, moving it out -- correctly `once`
12 call_many(once_closure); // error: `once` does not widen to a `many` slot
13}
typecheck error“once”
Legality Rule №15
The satisfaction rule of legality-9 applies at first-order positions only: a function-typed
argument passed to a function-typed parameter, a let / field ascription, a struct-field
initializer, and a return. Below the first level of function-type nesting an exact match is
required.
Referenced by: rfc-0152
Tested by
1// v0.13.0 closure cluster (RFC 0152 legality-15): widening applies at
2// first-order positions only. Below the first level of function-type nesting
3// an exact match is required -- RFC 0152 conservatively rejects mismatches a
4// settled higher-order (contravariant) rule would accept. That rule is
5// RFC 0155's, not v0.13.0's.
6//
7fun sink(cb: |var || -> i64| -> i64) -> i64 {
8 0
9}
10
11fun main() {
12 // `f`'s parameter is `reading () -> i64`; `sink` wants a nested `var () -> i64`.
13 // Nested position => exact match => rejected, even though `f` only reads it.
14 let f := |inner: || -> i64| { inner() };
15 sink(f);
16}
typecheck error
Legality Rule №16
A conditional or match expression whose arms are function-typed has, as its type, the
least-permissive arm type under legality-9's order, and each arm is widened to it. A
diverging (!-typed) arm does not contribute. A join that would require narrowing an
arm is the ordinary type mismatch.
Referenced by: rfc-0152
Tested by
1// v0.13.0 closure cluster (RFC 0152 legality-16): a conditional whose arms
2// are function-typed has, as its type, the least-permissive arm under the
3// widening order, and each arm widens to it. Here one arm is genuinely
4// `once`, so the whole `if` is `once` and the `many` arm widens in.
5//
6fun make_once(s: String) -> once || -> String {
7 [s] || { s }
8}
9
10fun call_once(f: once || -> String) -> String {
11 f()
12}
13
14fun main() {
15 let cond := true;
16 let picked := if (cond) {
17 make_once("a")
18 } else {
19 || { "b" } // `many` by default; widens `many -> once` into the join
20 };
21 assert(call_once(picked) == "a");
22}
passes
Legality Rule №10
A mutating call e(args) requires e to denote a place the caller can exclusively
borrow for the call's duration: an owned var binding, an owned temporary, an exclusive
(&var / owning) projection off one, or a &var parameter — the ordinary &var self
receiver rule. An owned but non-var (let) binding, or any shared-& callee (a &Self
/ &self receiver, a place reached through a & step, or an &-captured closure), is a
compile error. This holds for every mutating closure, whether it mutates its own
by-value captures or only drives mutation through a captured &var.
Referenced by: rfc-0153
Tested by
1// v0.13.0 closure cluster (RFC 0153 legality-10): a `mutating` call needs
2// exclusive access to the callee for the call's duration. Calling one through
3// a shared `&` reference (here a `&Cell` receiver) is a compile error.
4//
5struct Cell {
6 go: var || -> i64,
7}
8
9fun peek(c: &Cell) -> i64 {
10 (c.go)() // error (T0024): `var` closure called through a shared `&` reference
11}
12
13fun main() {
14 let n := 0;
15 let c := Cell { go = [n] var || { n := n + 1; n } };
16 assert(peek(&c) == 1);
17}
typecheck errorT0029“shared”
Legality Rule №11
An inner closure may not capture, by & or &var, a binding that is a by-value capture
of an enclosing closure. It may capture such a binding by value ([s], which moves it out
of the enclosing closure's environment). This restriction is lifted when the borrow
checker lands.
Tested by
1// v0.13.0 closure cluster (RFC 0050 legality-11): an inner closure may not
2// capture, by `&` or `&var`, a binding that is a by-value capture of an
3// enclosing closure. Interim rejection -- lifted when the borrow checker
4// (RFC 0122) lands.
5//
6fun main() {
7 let s := "data";
8 let outer := [s] || {
9 let inner := [&s] || { s == "data" }; // `&` into the enclosing closure's env
10 inner()
11 };
12 outer();
13}
typecheck error“borrow”
Legality Rule №12
A closure value satisfies no aspects: ==, <, .clone(), and other aspect-gated
operations on a closure value are a compile error. The only way to duplicate a closure
value is the by-value copy available when it is
Copy (legality-20L20). Structural equality of two
function types is a type relation and does not make their values comparable.
Tested by
1// v0.13.0 closure cluster (RFC 0134 legality-12): a closure value satisfies
2// no aspects. `==`, `<`, `.clone()` and other aspect-gated operations on a
3// closure value do not type-check.
4//
5fun main() {
6 let a := || { 1 };
7 let b := || { 1 };
8 assert(a == b); // error: closures do not satisfy `Eq`
9}
typecheck error
Legality Rule №13
A [&var ident] capture requires ident to be a var binding; capturing a non-var
binding by &var is a compile error. A closure literal cannot reference its own let
binding — the name is not in scope inside its own initializer.
Referenced by: rfc-0050
Tested by
1// v0.13.0 closure cluster (RFC 0050 legality-13): a `[&var ident]` capture
2// requires `ident` to be a `var` binding. (A closure literal also cannot
3// reference its own `let` binding -- not exercised here.)
4//
5fun main() {
6 let count := 0; // `let`, not `var`
7 var bump := [&var count] var || { count += 1; }; // error: `count` is not `var`
8 bump();
9}
typecheck error“var”
Legality Rule №14
A closure value is Send (respectively Sync) when every one of its captures is Send
(respectively Sync), applying the reference rules for &T / &var T captures. A
mutating closure value is additionally not Sync.
Tested by
1// v0.13.0 closure cluster (RFC 0153 legality-14): a closure's Send/Sync is
2// the aggregate rule over its captures, plus one closure-specific fact -- a
3// `mutating` closure value is not `Sync`.
4//
5// Also depends on Sync-bound checking (RFC 0080 / RFC 0096), so double-gated.
6fun needs_sync<T: Sync>(x: T) -> () {
7}
8
9fun main() {
10 let n := 0;
11 let m := [n] var || { n := n + 1; n };
12 needs_sync(m); // error: a `mutating` closure is not `Sync`
13}
typecheck error“Sync”
Legality Rule №17
Whether a captured free variable of type parameter T is Copy is decided from the
bounds in scope at the closure's definition, not re-decided per instantiation. A capture
of an unbounded T is non-Copy: [t] moves it, and a body that moves it out makes the
closure once for every instantiation of the enclosing generic, T = i64 included. A
definition wanting the copyable behaviour adds T: Copy, after which [t] is a copy and
consumes nothing.
Tested by
1// v0.13.0 closure cluster (RFC 0134 legality-17): a captured free variable of
2// type parameter `T` is classified from the bounds at the closure's
3// definition, not per instantiation. A moved unbounded-`T` capture makes the
4// closure `once` for *every* instantiation, `T = i64` included.
5//
6// Needs move_check = true (general moved-value check, RFC 0071).
7fun make<T>(t: T) -> once || -> T {
8 [t] once || { t }
9}
10
11fun main() {
12 let f := make::<i64>(5); // `T = i64` is Copy, but the closure is still `once`
13 let a := f();
14 let b := f(); // moved-value error -- definition-site classification, not per-monomorph
15}
typecheck error“moved”
Legality Rule №18
An unqualified closure literal in a position with an expected function type — a let or
parameter ascription, a struct-field initializer, a return, or the tail expression of a
typed block — is checked against that type's once / var qualifiers rather than taking
the many / reading default and then failing. When the expected type does not fix a
qualifier (an unresolved inference variable, or a bare generic parameter), the literal
takes the default and legality-9L9 widening resolves
any remaining gap at the concrete site.
Tested by
1// v0.13.0 closure cluster (RFC 0134 legality-18): an unqualified closure
2// literal in a typed position takes its `once` / `var` from the expected
3// type rather than defaulting to `many` / `reading` and then failing.
4//
5fun make(s: String) -> once || -> String {
6 [s] || { s } // no `once` written; the return type supplies it
7}
8
9fun main() {
10 let f := make("hi");
11 assert(f() == "hi");
12}
passes
Legality Rule №19
A closure literal is resolved in a fixed stage order: (1) capture classification
(referenced free variables, each one's Copy-ness, whether a list is required, and —
when present — its exhaustiveness and specifier-match); (2)
use_multiplicity (legality-20L20); (3) once
verification (legality-8L8); (4) var verification
(legality-25L25). The first failing stage is
reported; later stages are suppressed. Stages 3 and 4 are independent: a body that both
consumes and mutates without the qualifiers is reported against both.
Referenced by: rfc-0050
Tested by
1// v0.13.0 closure cluster (RFC 0050 legality-19): verification runs in a
2// fixed stage order -- capture classification first, then use_multiplicity,
3// then `once`, then `var`. A body that both omits a required capture list AND
4// mutates that binding is reported at stage 1 ("add a capture list"), not at
5// stage 4 ("add `var`").
6//
7fun main() {
8 var msg := "hi";
9 let f := || { msg := "bye"; }; // no capture list; `msg` is a free non-Copy local
10 f();
11}
typecheck errorT0026“capture”
Legality Rule №20
A closure value is Copy exactly when every one of its captures is Copy. [x] of a
Copy binding and [&x] (a shared reference is Copy) preserve it; [x] of a
non-Copy binding, [x.clone()] of a non-Copy type, and [&var x] (an exclusive
reference is not Copy) make the closure non-Copy. A Copy closure is necessarily
many — it holds nothing non-Copy for a call to consume.
Referenced by: rfc-0134
Tested by
1// v0.13.0 closure cluster (RFC 0134 legality-20): a closure value is `Copy`
2// exactly when every capture is `Copy`. `[n]` over `n: i64` yields a `Copy`
3// closure -- it can be copied and the original stays usable.
4//
5// Needs move_check = true (so a non-Copy closure would fail the reuse below).
6fun main() {
7 let n := 7;
8 let get := [n] || { n }; // n: i64 is Copy => `get` is Copy
9 let also := get; // copy, not move
10 assert(get() == 7); // original still usable after the copy
11 assert(also() == 7);
12}
passes
Legality Rule №21
A closure that captures x by shared reference [&x] and whose body assigns to x,
takes &var x, or calls a &var self method on it is a compile error at the capture —
the closure is not silently reclassified mutating. The fix is to capture [&var x],
which requires x to be a var binding (legality-13L13)
and makes the closure mutating (legality-25L25).
Tested by
1// v0.13.0 closure cluster (RFC 0050 legality-21): a `[&x]` capture is
2// read-only. A body that writes through it is a compile error at the
3// capture, not a silent upgrade to `mutating`.
4//
5fun main() {
6 var count := 0;
7 let bump := [&count] || {
8 count += 1; // error: `count` is captured by shared reference, not `&var`
9 };
10 bump();
11}
typecheck error“&var”
Legality Rule №22
When an inner closure captures a binding s that is itself a capture of an enclosing
closure: the enclosing closure must list s in its own capture list; an inner [s]
requires the enclosing closure to hold s by value (naming an enclosing [&s] /
[&var s] capture is a move out of borrowed content error); and an inner [s] that
moves an enclosing-held s makes the enclosing closure
once (legality-8L8), even if the inner closure is
never called. An inner [&s] / [&var s] does not change the enclosing closure's
multiplicity, but is subject to
legality-11L11's interim borrow restriction.
Referenced by: rfc-0050
Tested by
1// v0.13.0 closure cluster (RFC 0050 legality-22): evaluating an inner closure
2// literal performs the capture, so an inner `[s]` that moves an
3// enclosing-held `s` makes the *enclosing* closure `once` -- even though the
4// inner is only constructed, never called. Calling the enclosing closure
5// twice is then the ordinary moved-value error.
6//
7// Needs move_check = true (general moved-value check, RFC 0071).
8fun main() {
9 let s := "data";
10 let outer := [s] once || {
11 let inner := [s] once || { s }; // moves enclosing-held `s`; `outer` is declared `once`
12 inner
13 };
14 let a := outer();
15 let b := outer(); // moved-value error -- `outer` was consumed by the first call
16}
typecheck error“moved”
Dynamic Semantics №1
When a closure is created, each free variable named in its capture list (or, for a
listless closure, each free Copy variable) is placed into the closure's environment
according to its specifier: [x] moves a non-Copy value / copies a Copy value,
[x.clone()] stores an independent copy, [&x] / [&var x] store a reference.
Referenced by: rfc-0006
Tested by
1// Direct assignment to a captured var binding inside a closure operates on
2// the closure's deep-cloned copy of the env, not the original. The outer binding
3// must remain unchanged after the call.
4fun main() {
5 var x := 10;
6
7 let f := || -> () {
8 x := 42;
9 };
10
11 f();
12
13 assert(x == 10);
14}
passes
Dynamic Semantics №2
A reading closure does not modify its environment; a mutating closure modifies it in
place. In neither case does a write inside the closure body affect an outer binding that
was captured by value — the closure operates on its own environment copy.
Referenced by: rfc-0006
Tested by
1// Direct assignment to a captured var binding inside a closure operates on
2// the closure's deep-cloned copy of the env, not the original. The outer binding
3// must remain unchanged after the call.
4fun main() {
5 var x := 10;
6
7 let f := || -> () {
8 x := 42;
9 };
10
11 f();
12
13 assert(x == 10);
14}
passes
Dynamic Semantics №3
Closures that capture the same binding by [&x] / [&var x], or that capture the same
reference value, observe the same referent; a write through an exclusive reference by one
closure is visible through the others.
Referenced by: rfc-0006
Tested by (3)
1// Creates a tuple of three closures that all operate on the same mutable reference
2// Returns: (increment_by_1, increment_by_2, get_current_value)
3fun get_closures(ptr: &var i64) {
4 let first_ptr: &&var i64 := &ptr;
5 let second_ptr: &&var i64 := &ptr;
6 let third_ptr: &&var i64 := &ptr;
7
8 // First closure: increments the value at ptr by 1
9 let first_closure := || {
10 **first_ptr += 1; // Write through ptr, adding 1 to the value it points to
11 };
12
13 // Second closure: increments the value at ptr by 2
14 let second_closure := || {
15 **second_ptr += 2; // Write through ptr, adding 2 to the value it points to
16 };
17
18 // Third closure: reads and returns the current value at ptr
19 let third_closure := || -> i64 {
20 return third_ptr; // Type-directed copy: read the current value out of ptr
21 };
22
23 // Return all three closures as a tuple
24 // Each closure captures the same ptr, so they all operate on the same shared value
25 return (first_closure, second_closure, third_closure);
26}
27
28// Creates two independent sets of closures, each operating on the same shared value
29// Both closure sets point to the same memory location via ptr
30// Returns: ((set1_closure1, set1_closure2, set1_closure3), (set2_closure1, set2_closure2, set2_closure3))
31fun get_closure_sets(ptr: &var i64) {
32
33 // Note: shared_value is declared but not used (leftover from refactoring?)
34 var shared_value := 0;
35
36 // Create the first set of three closures, all capturing ptr
37 let first_closure_set := get_closures(ptr);
38
39 // Create the second set of three closures, also capturing the same ptr
40 // Both sets will operate on the same underlying value
41 let second_closure_set := get_closures(ptr);
42
43 // Return both closure sets as a nested tuple
44 (first_closure_set, second_closure_set)
45}
46
47fun apply_functions(funs : Array<&|| -> ()>) {
48 for(let fn in funs){
49 fn();
50 }
51}
52
53
54fun main() {
55
56 // Initialize a mutable integer that will be shared across all closures
57 var shared_value := 1;
58
59 // Get two closure sets; both sets operate on the same shared_value
60 // Structure: closure_sets.0 = first_closure_set, closure_sets.1 = second_closure_set
61 let closure_sets := get_closure_sets(&var shared_value);
62
63 apply_functions([&closure_sets.1.0, &closure_sets.0.1]);
64
65 // Verify that both closure sets see the same updated value (4)
66 // The third closure from each set returns the current value of shared_value
67 // Both should return 4 since they all reference the same memory location
68 assert(closure_sets.0.2() == shared_value && closure_sets.1.2() == shared_value);
69}
passes
1// A &var reference created in the outer scope and captured by a closure retains its
2// connection to the original binding: deep_clone_value preserves the inner Rc of a
3// reference value rather than copying it. Mutations through the captured reference
4// must be visible in the outer scope.
5fun main() {
6 var x := 10;
7 let p: &var i64 := &var x;
8 let p_ref: &&var i64 := &p;
9
10 let f := || -> () {
11 **p_ref := 42;
12 };
13
14 f();
15
16 assert(x == 42);
17
18 // The reference itself is also usable directly after the call.
19 assert((p: i64) == 42);
20}
passes
1// RFC-0006 §3: two closures may share mutable state only via an explicit shared
2// pointer. Both `inc` and `read` capture the same `&var i64`; a write through
3// one is visible through the other -- not because the closures share an
4// implicit environment, but because they share the pointer value itself.
5fun main() {
6 var x := 10;
7 let p: &var i64 := &var x;
8
9 let inc := [p] var || -> () {
10 *p += 1;
11 };
12 let read := [p] || -> i64 {
13 *p
14 };
15
16 inc();
17 inc();
18
19 assert(read() == 12);
20}
passes
Dynamic Semantics №4
A closure that escapes its defining function while holding a captured owned value keeps that value alive; the environment travels with the closure value. A closure holding a captured reference cannot outlive the referent (checked by the borrow checker when it lands; unenforced before then).
Referenced by: rfc-0006
Tested by
1// Creates a tuple of three closures that all operate on the same mutable reference
2// Returns: (increment_by_1, increment_by_2, get_current_value)
3fun get_closures(ptr: &var i64) {
4 let first_ptr: &&var i64 := &ptr;
5 let second_ptr: &&var i64 := &ptr;
6 let third_ptr: &&var i64 := &ptr;
7
8 // First closure: increments the value at ptr by 1
9 let first_closure := || {
10 **first_ptr += 1; // Write through ptr, adding 1 to the value it points to
11 };
12
13 // Second closure: increments the value at ptr by 2
14 let second_closure := || {
15 **second_ptr += 2; // Write through ptr, adding 2 to the value it points to
16 };
17
18 // Third closure: reads and returns the current value at ptr
19 let third_closure := || -> i64 {
20 return third_ptr; // Type-directed copy: read the current value out of ptr
21 };
22
23 // Return all three closures as a tuple
24 // Each closure captures the same ptr, so they all operate on the same shared value
25 return (first_closure, second_closure, third_closure);
26}
27
28// Creates two independent sets of closures, each operating on the same shared value
29// Both closure sets point to the same memory location via ptr
30// Returns: ((set1_closure1, set1_closure2, set1_closure3), (set2_closure1, set2_closure2, set2_closure3))
31fun get_closure_sets(ptr: &var i64) {
32
33 // Note: shared_value is declared but not used (leftover from refactoring?)
34 var shared_value := 0;
35
36 // Create the first set of three closures, all capturing ptr
37 let first_closure_set := get_closures(ptr);
38
39 // Create the second set of three closures, also capturing the same ptr
40 // Both sets will operate on the same underlying value
41 let second_closure_set := get_closures(ptr);
42
43 // Return both closure sets as a nested tuple
44 (first_closure_set, second_closure_set)
45}
46
47fun apply_functions(funs : Array<&|| -> ()>) {
48 for(let fn in funs){
49 fn();
50 }
51}
52
53
54fun main() {
55
56 // Initialize a mutable integer that will be shared across all closures
57 var shared_value := 1;
58
59 // Get two closure sets; both sets operate on the same shared_value
60 // Structure: closure_sets.0 = first_closure_set, closure_sets.1 = second_closure_set
61 let closure_sets := get_closure_sets(&var shared_value);
62
63 apply_functions([&closure_sets.1.0, &closure_sets.0.1]);
64
65 // Verify that both closure sets see the same updated value (4)
66 // The third closure from each set returns the current value of shared_value
67 // Both should return 4 since they all reference the same memory location
68 assert(closure_sets.0.2() == shared_value && closure_sets.1.2() == shared_value);
69}
passes
Dynamic Semantics №5
Capturing a non-Copy binding by value ([x]) moves it, consuming the outer binding; a
Copy binding captured by value is copied; [x.clone()] produces an independent copy
regardless of Copy-ness. The captured environment is constructed once, at closure
creation, and is not re-cloned per call.
Referenced by: rfc-0157
Tested by (3)
1// v0.13.0 closure cluster (RFC 0157 D5): capturing a non-Copy binding by
2// value moves it into the closure at creation; the captured environment is
3// built once and is not re-cloned per call.
4//
5fun main() {
6 let s := "hello";
7 let greet := [s] once || { s };
8 assert(greet() == "hello");
9}
passes
1// v0.13.0 closure cluster (RFC 0157 D5): `[s]` moves a non-Copy binding into
2// the closure, consuming the outer binding. Using it afterward is the
3// ordinary moved-value error (RFC 0134 §2 cites T0019's existing shape).
4//
5// Needs move_check = true: this is the general affine-move check (RFC 0071),
6// not one of the closure-specific always-on checks (ADR-0052 §1).
7fun main() {
8 let s := "hello";
9 let greet := [s] once || { s };
10 println(s); // moved-value error -- `s` was moved into `greet`
11}
typecheck errorT0019“moved”
1// v0.13.0 cross-feature (integration session, metel-core#956): closure capture
2// (RFC-0157 D5) meets move-triggered struct row narrowing (RFC-0137 slice 2).
3// A non-`Copy` field is moved out first, narrowing `h` to `Handle.{ fd }`; the
4// closure then captures the *narrowed* value by value. The capture list names
5// `h`, the residual moves into the environment once, and `--move-check` is
6// clean -- narrowing already removed the field that left.
7struct Handle { fd: i64, name: String }
8
9fun main() {
10 let h := Handle { fd = 7, name = "n" };
11 let taken := h.name; // h : Handle.{ fd }
12 let get := [h] once || { h.fd }; // captures the residual by value
13 assert(get() == 7);
14 println(taken);
15}
passes
Dynamic Semantics №7
A mutating closure's assignments to its by-value captures are retained in its
environment and are visible to subsequent calls of the same closure value — the closure
holds private mutable state.
Referenced by: rfc-0153
Tested by
1// v0.13.0 closure cluster (RFC 0153 dynamics-7): a `mutating` closure's
2// writes to its by-value captures persist across calls -- private mutable
3// state, with no per-call re-clone (RFC 0157 D5 is the base this relies on).
4//
5fun make_counter() -> var || -> i64 {
6 let n := 0;
7 [n] var || { n := n + 1; n }
8}
9
10fun main() {
11 var c := make_counter(); // `var`: each `c()` is a `&var self`-shaped borrow of `c` (legality-10)
12 assert(c() == 1);
13 assert(c() == 2); // state lives in `c`'s environment, not re-cloned
14}
passes
Dynamic Semantics №8
Copying a closure value whose captures are all Copy copies its environment. The copies
have independent environment state: a mutating call on one does not affect the other.
Referenced by: rfc-0153
Tested by
1// v0.13.0 closure cluster (RFC 0153 dynamics-8): copying a `Copy` `mutating`
2// closure copies its environment; the copies then have independent state.
3//
4fun main() {
5 let n := 0;
6 var c := [n] var || { n := n + 1; n };
7 var d := c; // `n: i64` is Copy => `c` is Copy; `d` gets an independent env
8 assert(c() == 1);
9 assert(c() == 2);
10 assert(d() == 1); // `d`'s counter did not advance with `c`'s
11}
passes
Dynamic Semantics №9
For the dynamic extent of a mutating call the callee place is exclusively borrowed. A
second mutating call on the same closure value reached from inside the first — directly
or through a structure the body can reach — is rejected: before the borrow checker lands,
as a runtime error; after, as a static borrow conflict.
Referenced by: rfc-0153
Tested by
1// v0.13.0 closure cluster (RFC 0153 dynamics-9): for the extent of a
2// `mutating` call the callee is exclusively borrowed. A second `mutating`
3// call on the same closure value reached from inside the first is rejected --
4// before the borrow checker lands, as a runtime error (R0015).
5//
6struct Cell {
7 go: var || -> i64,
8}
9
10fun main() {
11 var c := Cell { go = || { 0 } };
12 c.go := [&var c] var || {
13 (c.go)() + 1 // re-enters the same `var` closure while its first call is live
14 };
15 (c.go)(); // runtime error R0015 -- re-entrant call to a mutating closure
16}
runtime errorR0015“re-entrant”
Dynamic Semantics №10
A once call consumes the callee at the call expression, before the body runs. Any later
use of that closure value is a moved-value error, whether the body returned normally or
exited early.
Referenced by: rfc-0134
Tested by
1// v0.13.0 closure cluster (RFC 0134 dynamics-10): a `once` call consumes the
2// callee place at the call expression; a second call is the ordinary
3// moved-value error.
4//
5// Needs move_check = true (general moved-value check, RFC 0071).
6fun main() {
7 let s := "hello";
8 let take := [s] once || { s };
9 let first := take();
10 let second := take(); // moved-value error -- `take` was already consumed
11}
typecheck errorT0019“moved”
Dynamic Semantics №11
When a closure value is dropped, its environment is dropped: each owned capture is dropped
in capture-list order, as a struct's fields are. A once-consumed or partially-moved
environment drops only its still-owned captures.
Exempt from fixture coverage — blocked on metel-core#261: Destructor invocation and observable drop order are scheduled separately. Non-empty drop bodies are rejected until #261 lands, so a fixture cannot observe closure-environment field destruction or its order.
Dynamic Semantics №12
Multiplicity widening (legality-9L9) changes only the
static type at the slot, never the closure value's runtime behaviour. A reading closure
value widened into a var-typed slot is still invoked by the plain, non-exclusive call
path and consults no in-call flag, because call lowering branches on the closure value's
own mutation axis, not on the slot type. A many value in a once slot is likewise not
consumed by the call.
Tested by (2)
1// v0.13.0 closure cluster (RFC 0152 dynamics-12): widening is type-level
2// only. A `reading` closure widened into a `var` slot keeps its plain call
3// path -- no exclusive borrow, no in-call flag -- because call lowering
4// branches on the value's own mutation axis, not the slot type. So calling it
5// twice from inside a `var`-typed parameter is fine.
6//
7fun call_mut(f: var || -> i64) -> i64 {
8 f() + f()
9}
10
11fun main() {
12 let pure := || { 7 }; // `reading`, `many`
13 assert(call_mut(pure) == 14); // widens into the `var` slot, still dispatched plainly
14}
passes
1// v0.13.0 cross-feature (integration session, metel-core#956): the closure
2// mutation axis (RFC-0153), pipe notation (RFC-0154), and written function
3// types lowering to move-only (RFC-0166) together. A `[&var acc] var` closure
4// is moved -- once, move-only -- into a `var |i64| -> i64` parameter and called
5// twice inside the callee; call lowering dispatches on the value's own mutation
6// axis, and the `&var` capture writes through to the caller's `acc`.
7fun run_twice(f: var |i64| -> i64, x: i64) -> i64 {
8 let a := f(x);
9 f(a)
10}
11
12fun main() {
13 var acc := 0;
14 let step := [&var acc] var |d: i64| -> i64 { acc := acc + d; acc };
15 let r := run_twice(step, 3); // f(3): acc 0->3 ret 3 ; f(3): acc 3->6 ret 6
16 assert(r == 6);
17 assert(acc == 6);
18 println("ok");
19}
passes
Dynamic Semantics №13
An early exit from a mutating call is a ?-propagated Err or an early return, not a
panic — a panic is uncatchable and leaves no observable post-exit state
(runtime.mdPanics D1). On such an exit from a var
(non-once) call, mutations already made stay in the environment, the exclusive borrow
and the in-call flag release as the frame returns, and the closure remains an ordinary,
callable value; there is no rollback, and the next call runs the body from the top over
that state. A once / once var closure was already consumed at the call expression
(dynamics-10D10) regardless of how the body exited;
its still-owned fields are dropped when the value goes out of scope.
Tested by
1// v0.13.0 closure cluster (RFC 0153 dynamics-13): if a plain `var` (not
2// `once`) closure's body exits early via `?` or `return`, the mutations that
3// already ran stay visible and the closure remains callable in a
4// valid-but-partial state. (A `panic` is uncatchable and ends the process --
5// out of scope here.)
6//
7fun main() {
8 var log := 0;
9 var step := [&var log] var |stop: boolean| {
10 log += 1;
11 if (stop) {
12 return log; // early return, mid-mutation
13 }
14 log += 10;
15 log
16 };
17 assert(step(true) == 1); // returned after the first mutation only
18 assert(log == 1); // partial mutation is visible
19 assert(step(false) == 12); // closure still callable; resumes from log == 1
20 assert(log == 12);
21}
passes
Dynamic Semantics №14
Storing a reading closure into a var-typed field, or returning it where a var
function type is named, yields a value thereafter observed as var: every later read of
that field or result is a var value that callers must invoke under exclusive access
(legality-10L10), even though the underlying closure
never mutates. The coercion is one-way — there is no automatic re-narrowing back to
reading.
Tested by
1// v0.13.0 closure cluster (RFC 0152 dynamics-14): storing a `reading` closure
2// into a `var`-typed field coerces it into that field's declared type -- a
3// one-way precision loss. Later reads of the field yield a `var` value; there
4// is no automatic re-narrowing back to `reading`.
5//
6struct Box {
7 run: var || -> i64,
8}
9
10fun main() {
11 let pure := || { 3 }; // `reading`
12 let b := Box { run = pure }; // coerced into the `var`-typed field
13 assert((b.run)() == 3); // `b.run` is observed as `var () -> i64`
14}
passes
Turbofish
When a generic function's type parameters cannot be inferred from the arguments, they can be specified explicitly with turbofish syntax: name::<T, U>(args)L1.
fun identity<T>(x: T) -> T { x }
fun main() -> i64 {
let x := identity::<i64>(42);
return x;
}
Turbofish is most useful when two or more independent type parameters must be pinned at the call site — for example, a zip function that pairs elements from arrays of different types:
fun zip<A, B>(a: A[], b: B[]) -> (A, B)[] { /* ... */ }
fun main() {
let pairs := zip::<i64, String>([1, 2], ["a", "b"]);
}
Type ascription (: T) remains available for annotating the result type. Turbofish and ascription can be used together:
let result := parse::<i64>("42") : Perhaps<i64>;
Formal rules
Legality Rule №1
A generic call may supply explicit type arguments with name::<T, U>(arguments), pinning
each named parameter to the given type. A pinned type must satisfy that parameter's own
bounds (e.g. T: Display).
Referenced by: rfc-0023
Tested by
1fun identity<T>(x: T) -> T { x }
2fun first<A, B>(a: A, b: B) -> A { a }
3fun wrap<T>(x: T) -> List<T> {
4 var arr: List<T> := List::new();
5 arr.push(x);
6 arr
7}
8
9fun main() {
10 // Basic turbofish — explicit type matches inferred type
11 let x := identity::<i64>(42);
12 assert(x == 42);
13
14 let s := identity::<String>("hello");
15 assert(s == "hello");
16
17 let b := identity::<boolean>(true);
18 assert(b);
19
20 // Turbofish with two type parameters
21 let a := first::<i64, String>(10, "unused");
22 assert(a == 10);
23
24 let s2 := first::<String, i64>("kept", 99);
25 assert(s2 == "kept");
26
27 // Turbofish with compound types
28 let t := identity::<(i64, boolean)>((7, false));
29 assert(t.0 == 7);
30 assert(t.1 == false);
31
32 // Inference still works without turbofish
33 let y := identity(3.14);
34 assert(y == 3.14);
35
36 // Nested turbofish calls
37 let inner := identity::<i64>(identity::<i64>(5));
38 assert(inner == 5);
39
40 // Turbofish with List return type
41 let v := wrap::<i64>(99);
42 assert(v.len() == 1);
43}
passes
Legality Rule №2
The call's arguments must unify with their pinned types exactly as they would with an inferred one.
Referenced by: rfc-0023
Tested by (3)
1fun identity<T>(x: T) -> T { x }
2
3fun main() {
4 let x := identity::<i64>("hello");
5 println(x);
6}
typecheck errorT0001at 4
1fun clamp<T>(x: T, lo: T, hi: T) -> T { x }
2
3fun main() -> i32 {
4 return clamp::<i32>(5, 0, 10);
5}
passes
1// Turbofish disambiguates T, which appears only in the return position;
2// ascription disambiguates the otherwise-ambiguous `None` argument, since a
3// generic-scheme callee's own parameter types aren't available as hints for
4// its arguments at all (unlike a concrete, non-generic callee) -- both
5// mechanisms are doing real, independent work in the same call.
6fun make<T>(fallback: Perhaps<i64>) -> T {
7 return make(fallback);
8}
9
10fun main() {
11 make::<i64>(None : Perhaps<i64>);
12}
passes
Legality Rule №3
Turbofish is a call-postfix production, fused to the immediately following call's
parentheses. There is no standalone instantiation-without-calling value form:
name::<T> not immediately followed by (arguments) is a parse error.
Tested by
1// Negative, deliberately out of scope (RFC-0138 §5): the turbofish production is
2// fused to a following call's parentheses -- there is no standalone
3// instantiation-without-calling value form. `identity::<i64>` alone, not followed
4// by `(...)`, is a parse error, not a type error.
5
6fun identity<T>(x: T) -> T { return x; }
7
8fun main() {
9 let alias := identity::<i64>;
10}
parse error
The ? Operator
?v0.1.0From-based error coercionv0.4.0Inside a function returning Result<T, E>, ? propagates errors earlyD2:
fun parse_int(s: String) -> Result<i64, String> {
if (s == "21") {
return Ok { value = 21 };
}
return Err { error = "not a number" };
}
fun parse_and_double(s: String) -> Result<i64, String> {
let n := parse_int(s)?; // returns Err early if parse_int fails
return Ok { value = n * 2 };
}
fun main() -> i64 {
match (parse_and_double("21")) {
Ok { value } => value,
Err { error } => 0,
}
}
? desugars to: if the expression is Err(e), return Err(E2::from(e))
immediatelyD2 (where E2 is the enclosing
function's error type); otherwise unwrap to the Ok valueD1.
The inner expression's error type E1 and the function's return error type E2 must
satisfy E2: From<E1>L2. When E1 == E2 no
conversion is performed. When they differ, From::from is called automatically on the
error value before re-wrapping in Err.
? does not apply to Perhaps<T>L1 in this
language version. It is supported only for Result<T, E>, so using ? on a Perhaps
value is a type error (T0001) rather than an early None return.
Formal rules
Legality Rule №1
The ? operator requires a Result<T, E> operand; applying it to Perhaps<T> or any
other type is a type error.
Tested by
1// ? on a non-Result type should fail.
2fun might_fail() -> i64 {
3 42? // ERROR[T0001]
4}
typecheck errorT0001“cannot unify an integer literal with”
Legality Rule №2
The enclosing function's return type must be Result<U, E2>, and the operand error
type E1 must equal E2 or satisfy E2: From<E1>.
Tested by
1// ? with mismatched error types and no From impl must fail with T0007.
2// METEL-80 routes ? through From-based coercion; when no From impl exists,
3// the coercion is invalid and the typechecker emits T0007 (invalid cast).
4// From coercion for arbitrary type pairs is deferred to #13.
5
6fun inner() -> Result<i64, String> {
7 Result::Ok { value = 42 }
8}
9
10fun outer() -> Result<i64, i64> {
11 let x := inner()?; // ERROR[T0007]
12 Result::Ok { value = x }
13}
14
15fun main() {}
typecheck errorT0007at 11:21
Dynamic Semantics №1
Evaluating Ok { value }? produces value and evaluation continues in the enclosing
function.
Tested by
1// ? on Ok unwraps the value; ? on Err propagates out of the calling function.
2fun safe_div(a: i64, b: i64) -> Result<i64, i64> {
3 if (b == 0) {
4 Result::Err { error = -1 }
5 } else {
6 Result::Ok { value = a / b }
7 }
8}
9// Uses ? internally — if safe_div returns Ok(q), continues with Ok(q+1).
10// If safe_div returns Err(e), ? propagates so try_div returns Err(e).
11fun try_div(a: i64, b: i64) -> Result<i64, i64> {
12 let q := safe_div(a, b)?;
13 Result::Ok { value = q + 1 }
14}
15fun main() {
16 // ? on Ok: unwraps, continues, returns Ok(6).
17 let r1 := try_div(10, 2);
18 match (r1) {
19 Result::Ok { value } => assert(value == 6),
20 Result::Err { error } => assert(false, "expected Ok"),
21 };
22 // ? on Err: propagates, try_div returns Err(-1).
23 let r2 := try_div(10, 0);
24 match (r2) {
25 Result::Ok { value } => assert(false, "expected Err"),
26 Result::Err { error } => assert(error == -1),
27 };
28}
passes
Dynamic Semantics №2
Evaluating Err { error }? immediately returns Err { error } from the enclosing
function; when the error types differ, the returned error is E2::from(error).
Tested by
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
Native Functions (Standard Library Only)
Standard library declarations may be marked native, binding them to an
implementation provided by the host interpreter instead of a Metel body:
// from std::core — not writable in user code
native(@std.core.println) public fun println<T>(x: T);
native(@std.core.clock) public fun clock() -> i64;
A native declaration has no body — it ends with ; instead of a block. The
@-path inside the parentheses is the binding key that selects the host
implementation. The form is also valid on methods inside extend blocks; for
example, the primitive Display implementations in std::core are declared
this way:
extend i64: Display {
native(@std.core.to_string) fun to_string(&self) -> String;
}
native is reserved for the standard libraryL1. Using it in any module
outside the std namespace is a compile error, and user projects cannot place
modules under std:: (see §Modules). From the caller's side,
native functions are indistinguishable from ordinary functions: they are
imported, typechecked, and called exactly like any other declaration — the
binding key is an implementation detail of the standard library's source.
Native declarations must annotate every parameter typeL3; an omitted return
type means the function returns ().
Formal rules
Legality Rule №1
Only a declaration in the std namespace may use the native modifier; a native
declaration in a user module is rejected.
Tested by
1// `native` is a stdlib-only construct; declaring one in a user module (any
2// module whose path does not begin with `std`) is rejected.
3native(@std.core.println) fun shout(x: String); // ERROR[T0003]
4
5fun main() {}
typecheck errorT0003“native”
Legality Rule №2
A native declaration has a dotted @ host-binding key and no Metel body: it ends with
;.
Tested by
1// The `native(@…)` host-binding syntax parses: a bodyless function with a
2// dotted host key. (Stdlib-only enforcement happens at type-check, not parse.)
3native(@std.core.print) fun print_one(x: String);
4native(@std.core.clock) fun now() -> i64;
5
6fun main() {}
passes
Legality Rule №3
Every native-function parameter has an explicit type annotation. An omitted return type
denotes ().
Tested by