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
Program Entry Point
main function is checked during typechecking, not lazily at evaluator startupThe root module must declare exactly one function named main, callable with zero
arguments, with a body the type checker can type. This is a purely static, declarative
fact — answerable from the declaration table alone, with no dependency on program
execution — and a violation is a compile-time error, not a runtime one.
Formal rules
Legality Rule №1
The root module declares exactly one function named main, non-generic, callable with
zero arguments, with a body the type checker can type. Violating this is a compile-time
error, not a runtime one.
Tested by (4)
1// RUNTIME_ERROR[no main]
2let x := 1;
typecheck errorT0031“no main”
1// `main` exists but is a binding, not a function -- R0002's other documented
2// trigger (the sibling case is a generic main, see error-codes.md).
3let main := 5;
typecheck errorT0031“not a function”
1// `main` exists and is a function, but is itself generic -- RFC-0167's other
2// documented `main`-shape violation (the sibling case is `neg_08`'s "not a
3// function" one). The program entry point must be non-generic.
4fun main<T>() {}
typecheck errorT0031“generic”
1// `main` exists, is a function, and is non-generic, but takes an argument --
2// spec.functions.program-entry-point.legality-1 requires it be callable with
3// zero arguments.
4fun main(x: i64) {
5 let _ := x;
6}
typecheck errorT0031“zero arguments”
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
GAP-FUNCTIONS-001A module can declare several free functions with one name and the compiler resolves calls by exact argument types, but the spec states no rules for it, including whether an overloaded name can be used as a value.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.
GAP-FUNCTIONS-003RFC-0163A written function type is always move-only; the copy |T| -> U qualifier and the Erased (capability unknown) state are not available.GAP-FUNCTIONS-002RFC-0161Callable<A, B> is not defined in std::core, so a function or closure type cannot satisfy an aspect bound and dyn Callable is unavailable.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 (7)
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
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);
}
A closure parameter may be annotated with an open record whose tail is anonymous
(|p: { x: i64, .. }| { p.x }); the closure is then polymorphic in that row and accepts
records of different shapes (open rowsGenerics L5).
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 (5)
1fun helper() -> i64 { 4 }
2
3fun use_helper<T>(value: T) -> i64 {
4 let n := 3;
5 let explicit := [n] || -> i64 { helper() + n };
6 let implicit := || -> i64 { helper() + n };
7 explicit() + implicit()
8}
9
10fun main() {
11 assert(use_helper(1) == 14);
12 assert(use_helper("owned") == 14);
13}
passes
1// 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”
1// The same shape is legal when the definition grants `Copy` (#273).
2fun keep<A: Copy>(a: A) -> |i64| -> (i64, A) {
3 |b: i64| -> (i64, A) { (b, a) }
4}
5
6fun main() {
7 let f := keep(1);
8 let pair := f(2);
9 assert(pair.0 == 2);
10 assert(pair.1 == 1);
11}
passes
1// RFC-0050 legality-6 for a declared type parameter: `A` has no `Copy` bound, so
2// the implicit capture is rejected at the definition even though the only
3// instantiation (`i64`) is `Copy` (#273).
4fun swap_vals<A>(a: A) -> |i64| -> (i64, A) {
5 |b: i64| -> (i64, A) { (b, a) }
6}
7
8fun main() {
9 let f := swap_vals(1);
10 let pair := f(2);
11}
typecheck errorT0026“add a capture list”
1// RFC-0050 legality-6: a closure with no capture list may capture only `Copy`
2// locals. `a` is an unannotated (inferred) parameter, so it has no `Copy` grant
3// at the definition; whether some later call would pass a `Copy` type is not
4// consulted (#273).
5let swap_vals := |a| { |b| { (b, a) } };
6
7fun main() {
8 let pair := swap_vals(1)(2);
9}
typecheck errorT0026“add a capture list”
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 (15)
1fun helper() -> i64 { 4 }
2
3fun invalid<T>(value: T) -> i64 {
4 let helper := 3;
5 let callback := [value] once || { helper }; // ERROR[T0026]
6 0
7}
8
9fun main() {}
typecheck errorT0026at 5“`helper` is captured but not listed”
1struct Pair { left: String, right: String }
2fun invalid(empty: Pair.{}) -> String {
3 let callback := [empty] once || -> String { empty.left }; // ERROR[T0019]
4 callback()
5}
6fun main() {}
typecheck errorT0019
1struct Pair { left: String, right: String }
2type Empty := Pair.{};
3type Callback := once || -> Pair;
4
5fun empty() -> Pair.{} {
6 let pair := Pair { left = "old", right = "gone" };
7 let left := pair.left;
8 let right := pair.right;
9 pair
10}
11
12fun rebuild(empty: Pair.{}) -> Pair {
13 var restored := empty;
14 restored.left := "new";
15 restored.right := "back";
16 restored
17}
18
19fun captured(empty: Empty) -> Pair {
20 let callback: Callback := [empty] once || {
21 var restored := empty;
22 restored.left := "new";
23 restored.right := "back";
24 restored
25 };
26 callback()
27}
28
29fun written(empty: Pair.{}) -> Pair {
30 let callback := [empty] once || -> Pair {
31 var restored := empty;
32 restored.left := "new";
33 restored.right := "back";
34 restored
35 };
36 callback()
37}
38
39fun main() {
40 assert(rebuild(empty()).right == "back");
41 assert(captured(empty()).left == "new");
42 assert(written(empty()).right == "back");
43}
passes
1fun helper() -> i64 { 4 }
2
3fun use_helper<T>(value: T) -> i64 {
4 let n := 3;
5 let explicit := [n] || -> i64 { helper() + n };
6 let implicit := || -> i64 { helper() + n };
7 explicit() + implicit()
8}
9
10fun main() {
11 assert(use_helper(1) == 14);
12 assert(use_helper("owned") == 14);
13}
passes
1type Callback := once || -> i64;
2fun invoke(f: Callback) -> i64 { f() }
3
4fun transition<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
5 { ..r, token = "secret" }
6}
7
8fun copied_tail<row R: !{token}>(r: { id: i64, ..R }) -> i64 where all R: Copy {
9 let { id, ..rest } := r;
10 let copy := rest;
11 let callback := [copy, id] once || {
12 let extended := transition(copy);
13 let { token, ..again } := extended;
14 id + token.len() as i64
15 };
16 invoke(callback)
17}
18
19fun main() {
20 assert(copied_tail({ id = 3, count = 7 }) == 9);
21 assert(copied_tail({ id = 5 }) == 11);
22}
passes
1fun add<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
2 { ..r, token = "secret" }
3}
4
5fun forward<row R: !{token}>(r: { ..R }) -> i64 where all R: Copy {
6 let callback := [r] once || {
7 let added := add(r);
8 let copied := r;
9 7
10 };
11 callback()
12}
13
14fun main() {
15 assert(forward({ n = 1 }) == 7);
16 assert(forward({}) == 7);
17}
passes
1record Ledger { id: i64, memo: String, count: i64 }
2type LedgerAlias := Ledger;
3
4fun main() {
5 let original: LedgerAlias := Ledger { id = 4, memo = "keep", count = 6 };
6 let copied := original.clone();
7 let memo := copied.memo;
8 let callback := [copied] once || { copied.id + copied.count };
9 assert(memo == "keep");
10 assert(callback() == 10);
11}
passes
1// RFC-0179: a closure that captures an open-row parameter and passes it on forgets nothing, so
2// no `where all R: Copy` is needed.
3type Callback := once || -> i64;
4fun head<row R>(value: { id: i64, ..R }) -> i64 { value.id }
5fun accepted<row R>(value: { id: i64, ..R }) -> i64 {
6 let callback: Callback := [value] once || {
7 head(value)
8 };
9 callback()
10}
11fun main() {
12 assert(accepted({ id = 5, text = "kept".to_string() }) == 5);
13}
passes
1fun main() {
2 let s := "x".to_string();
3 let f := || { s }; // ERROR[T0026]
4 f();
5}
typecheck errorT0026at 3“add a capture list”
1record User { name: String }
2aspect Describe { fun describe(&self) -> String; }
3extend User: Describe {
4 fun describe(&self) -> String { self.name.clone() }
5}
6
7type View := dyn Describe;
8type Callback := once || -> String;
9
10fun main() {
11 let user := User { name = "captured" };
12 let view: View := user;
13 let callback: Callback := [view] once || { view.describe() };
14 assert(callback() == "captured");
15}
passes
1// old x old: TA x OW, TA x CO, CL x PM, CL x CO, DY x CO, OW x CO, PM x CO, PM x GA
2type Names := [String];
3type Handler := once |i64| -> i64;
4aspect Speak { fun speak(&self) -> String; }
5struct Dog {}
6extend Dog: Speak { fun speak(&self) -> String { "woof".to_string() } }
7enum Msg { Num { n: i64 }, Text { s: String } }
8fun apply(h: Handler, v: i64) -> i64 { h(v) }
9fun identity<T>(x: T) -> T { x }
10fun main() {
11 let names: Names := ["a".to_string(), "b".to_string()];
12 let first := names[0].clone();
13 assert(first == "a");
14 var total := 0;
15 for (n in names) { total := total + n.len(); }
16 assert(total == 2);
17 let add := 10;
18 let h: Handler := [add] once |v: i64| { v + add };
19 assert(apply(h, 5) == 15);
20 let msgs := [Msg::Num { n = 1 }, Msg::Text { s = "hi".to_string() }];
21 var acc := 0;
22 for (m in msgs) {
23 let f := [m] once || {
24 match (m) { Msg::Num { n } => n, Msg::Text { s } => s.len() }
25 };
26 acc := acc + f();
27 }
28 assert(acc == 3);
29 let dogs: [dyn Speak] := [Dog {}, Dog {}];
30 var barks := 0;
31 for (d in dogs) { if (d.speak() == "woof") { barks := barks + 1; } }
32 assert(barks == 2);
33 let moved := names;
34 assert(moved.len() == 2);
35 match (identity(Msg::Num { n = 4 })) { Msg::Num { n } => assert(n == 4), Msg::Text { s } => assert(false) }
36 let o: Perhaps<[i64]> := Perhaps::Some { value = [1, 2] };
37 match (o) { Perhaps::Some { value } => assert(value.len() == 2), None => assert(false) }
38}
passes
1record Counter { n: i64, label: String }
2fun main() {
3 let c := Counter { n = 1, label = "c" };
4 let { label, ..rest } := c;
5 let f := [rest] once || { rest.n + 1 };
6 assert(f() == 2);
7 assert(label == "c");
8 let arr: [i64; 3] := [1, 2, 3];
9 let g := [arr] || { arr[0] + arr[2] };
10 assert(g() == 4);
11 assert(g() == 4);
12}
passes
1type Callback := once || -> i64;
2fun invoke(f: Callback) -> i64 { f() }
3
4fun transition<row R: !{token}>(r: { ..R }) -> { token: String, ..R } {
5 { ..r, token = "secret" }
6}
7
8fun copied_tail<row R: !{token}>(r: { id: i64, ..R }) -> i64 where all R: Copy {
9 let { id, ..rest } := r;
10 let copy := rest;
11 let callback := [copy] once || { // ERROR[T0026]
12 let extended := transition(copy);
13 let { token, ..again } := extended;
14 id + token.len() as i64
15 };
16 invoke(callback)
17}
18
19fun main() {
20 copied_tail({ id = 3, count = 7 });
21}
typecheck errorT0026at 11“`id` is captured but not listed”
1// Capture completeness is checked even when the generic function is never called.
2fun invalid<T>(value: T) -> i64 {
3 let id := 7;
4 let callback := [value] once || { id }; // ERROR[T0026]
5 0
6}
7
8fun main() {}
typecheck errorT0026at 4“`id` is captured but not listed”
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 inc();
13 inc();
14
15 // `p` is move-only, so only `inc` holds it; `read` snapshots `x` once `inc` is done,
16 // which sees the writes `inc` made through the pointer.
17 let read := [x] || -> i64 {
18 x
19 };
20 assert(read() == 12);
21}
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 (19)
1record Batch { samples: [i64; 3], label: String }
2type Samples := [i64; 3];
3type Sum := once || -> i64;
4
5fun sum_head<row R>(value: { samples: Samples, ..R }) -> i64 where all R: Copy {
6 var total := 0;
7 for (sample in value.samples) { total := total + sample; }
8 total
9}
10
11fun main() {
12 let batch := Batch { samples = [1, 2, 3], label = "batch" };
13 let { label, ..rest } := batch;
14 assert(label == "batch");
15 let copied := rest.clone();
16 let callback: Sum := [copied] once || { sum_head(copied) };
17 assert(callback() == 6);
18 assert(sum_head({ samples = rest.samples, extra = true }) == 6);
19 match (rest.samples) {
20 [first, second, third] => assert(first + second + third == 6),
21 }
22}
passes
1record User { id: i64, name: String, count: i64 }
2type UserAlias := User;
3type Callback := once || -> i64;
4
5fun clone_head<row R>(value: { id: i64, ..R }) -> { id: i64, ..R }
6where all R: Clone {
7 value.clone()
8}
9
10fun main() {
11 let row_clone := clone_head({ id = 9 });
12 assert(row_clone.id == 9);
13 let row_clone_with_tail := clone_head({ id = 3, count = 5 });
14 assert(row_clone_with_tail.id + row_clone_with_tail.count == 8);
15
16 let user: UserAlias := User { id = 3, name = "moved", count = 5 };
17 let name := user.name;
18 let residual := user.clone();
19 assert(name == "moved");
20 assert(residual.id + residual.count == 8);
21 let callback: Callback := [residual] once || {
22 residual.id + residual.count
23 };
24 assert(callback() == 8);
25}
passes
1record Ledger { id: i64, memo: String, count: i64 }
2type LedgerAlias := Ledger;
3
4fun main() {
5 let original: LedgerAlias := Ledger { id = 4, memo = "keep", count = 6 };
6 let copied := original.clone();
7 let memo := copied.memo;
8 let callback := [copied] once || { copied.id + copied.count };
9 assert(memo == "keep");
10 assert(callback() == 10);
11}
passes
1record Notice { title: String, priority: i64 }
2aspect Summary { fun summary(&self) -> String; }
3
4extend<row R: { title: String, .. }> { ..R }: Summary {
5 fun summary(&self) -> String { self.title.clone() }
6}
7
8fun main() {
9 let notice := Notice { title = "captured dispatch", priority = 2 };
10 let callback := [notice] once || { notice.summary() };
11 assert(callback() == "captured dispatch");
12}
passes
1record User { name: String }
2aspect Describe { fun describe(&self) -> String; }
3extend User: Describe {
4 fun describe(&self) -> String { self.name.clone() }
5}
6
7type View := dyn Describe;
8type Callback := once || -> String;
9
10fun main() {
11 let user := User { name = "captured" };
12 let view: View := user;
13 let callback: Callback := [view] once || { view.describe() };
14 assert(callback() == "captured");
15}
passes
1fun make_clone<T: Clone>(value: T) -> once || -> T {
2 let owned := value.clone();
3 [owned] once || { owned.clone() }
4}
5
6fun main() {
7 let callback := make_clone("generic closure".to_string());
8 assert(callback() == "generic closure");
9}
passes
1struct Pair { left: String, right: String }
2fun main() {
3 var pair := Pair { left = "gone", right = "kept" };
4 let left := pair.left;
5 let repair := [pair] once |extra| { (pair.right, extra) };
6 let result := repair(7);
7 assert(result.0 == "kept");
8 assert(result.1 == 7);
9
10 var empty_pair := Pair { left = "old-left", right = "old-right" };
11 let old_left := empty_pair.left;
12 let old_right := empty_pair.right;
13 let refill := [empty_pair] once var |extra| {
14 empty_pair.left := "new-left";
15 empty_pair.right := "new-right";
16 (empty_pair, extra)
17 };
18 let restored := refill(9);
19 assert(restored.0.left == "new-left");
20 assert(restored.0.right == "new-right");
21 assert(restored.1 == 9);
22}
passes
1record Pair { left: String, right: String }
2type PairAlias := Pair;
3type Restore := once var || -> PairAlias;
4
5aspect State { fun state(&self) -> i64; }
6extend<row R: { right: String, .. }> { ..R }: State {
7 fun state(&self) -> i64 { 1 }
8}
9extend<row R: !{ right }> { ..R }: State {
10 fun state(&self) -> i64 { 0 }
11}
12
13fun main() {
14 var pair: PairAlias := Pair { left = "left", right = "right" };
15 assert(pair.state() == 1);
16 let left := pair.left;
17 let partial := pair.clone();
18 assert(partial.right == "right");
19 assert(pair.state() == 1);
20 let right := pair.right;
21 assert(pair.state() == 0);
22 let empty: Pair.{} := pair.clone();
23 assert(empty.state() == 0);
24 let restore: Restore := [pair] once var || -> PairAlias {
25 pair.left := "restored";
26 pair.right := "again";
27 assert(pair.state() == 1);
28 pair
29 };
30 match (restore()) {
31 { left, ..rest } => {
32 assert(left == "restored");
33 assert(rest.right == "again");
34 assert(rest.state() == 1);
35 },
36 }
37}
passes
1record Item { name: String, token: String }
2aspect Describe { fun describe(&self) -> String; }
3extend<row R: { name: String, .. }> { ..R }: Describe {
4 fun describe(&self) -> String { self.name.clone() }
5}
6type View := dyn Describe;
7type Callback := once || -> String;
8fun display(value: &View) -> String { value.describe() }
9
10fun main() {
11 let item := Item { name = "kept", token = "discarded" };
12 let token := item.token;
13 let object: View := item;
14 assert(display(&object) == "kept");
15 let callback: Callback := [object] once || { object.describe() };
16 assert(callback() == "kept");
17 let anonymous: View := { name = "anonymous" };
18 assert(display(&anonymous) == "anonymous");
19}
passes
1struct Pair { left: String, right: String }
2fun take(value: Pair.{}) {}
3fun main() {
4 var pair := Pair { left = "old", right = "gone" };
5 let left := pair.left;
6 let right := pair.right;
7 take(pair);
8 let callback := [pair] once var || -> Pair { // ERROR[T0019]
9 pair.left := "new";
10 pair.right := "back";
11 pair
12 };
13}
typecheck errorT0019at 8
1struct Pair { left: String, right: String }
2type Callback := once var || -> Pair;
3
4fun main() {
5 var pair := Pair { left = "old", right = "gone" };
6 let left := pair.left;
7 let right := pair.right;
8 let callback: Callback := [pair] once var || -> Pair {
9 pair.left := "new";
10 pair.right := "back";
11 pair
12 };
13 assert(callback().left == "new");
14
15 var partial := Pair { left = "gone", right = "kept" };
16 let removed := partial.left;
17 let restore := [partial] once var || -> Pair {
18 partial.left := "restored";
19 assert(partial.right == "kept");
20 partial
21 };
22 assert(restore().left == "restored");
23
24 var anonymous := { left = "old", right = "gone" };
25 let row_left := anonymous.left;
26 let restore_row := [anonymous] once var || -> { right: String } {
27 anonymous.right := "back";
28 anonymous
29 };
30 assert(restore_row().right == "back");
31
32 let empty_anonymous := { owned = "gone" };
33 let owned := empty_anonymous.owned;
34 let empty_callback := [empty_anonymous] once || -> {} { empty_anonymous };
35 let empty: {} := empty_callback();
36}
passes
1struct Pair { left: String, right: String }
2fun take(value: Pair.{}) {}
3fun main() {
4 var pair := Pair { left = "old", right = "gone" };
5 let left := pair.left;
6 let right := pair.right;
7 let callback := [pair] once |extra| { extra };
8 take(pair); // ERROR[T0019]
9}
typecheck errorT0019at 8
1struct Pair { left: String, right: String }
2fun take(value: Pair.{}) {}
3fun main() {
4 var pair := Pair { left = "old", right = "gone" };
5 let left := pair.left;
6 let right := pair.right;
7 let callback := [pair] once || -> i64 {
8 let pair := 3;
9 pair
10 };
11 take(pair); // ERROR[T0019]
12}
typecheck errorT0019at 11
1struct Pair { left: String, right: String }
2fun take(value: Pair.{}) {}
3fun main() {
4 var pair := Pair { left = "old", right = "gone" };
5 let left := pair.left;
6 let right := pair.right;
7 let callback := [pair] once var || -> Pair {
8 pair.left := "new";
9 pair.right := "back";
10 pair
11 };
12 take(pair); // ERROR[T0019]
13}
typecheck errorT0019at 12
1import helper::make_message;
2import helper::MessageCallback;
3
4fun main() {
5 let callback: MessageCallback := make_message("from helper");
6 assert(callback() == "from helper");
7}
passes
1import helper::make;
2import helper::strip;
3import helper::callback;
4import helper::EmptyPacket;
5import helper::Callback;
6
7fun main() {
8 let empty: EmptyPacket := strip(make(7, { token = "gone" }));
9 let run: Callback := callback(empty);
10 assert(run() == 7);
11 let with_tail := strip(make(9, { token = "gone", samples = [1, 2] }));
12 assert(with_tail.id == 9);
13 assert(with_tail.data.samples[1] == 2);
14}
passes
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
GAP-OWNERSHIP-001RFC-0122RFC-0067Nothing prevents a shared and an exclusive reference to the same place from coexisting.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
LIMIT-EVALUATION-005RFC-0071closure environments are not destroyed until destructors run.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
Known gaps
GAP-FUNCTIONS-001 — Free-function overloading is implemented but the Language Spec does not describe it. A module can declare several free functions with one name and the compiler resolves calls by exact argument types, but the spec states no rules for it, including whether an overloaded name can be used as a value. Open record ↗plannedGAP-FUNCTIONS-002 — Callable<A, B> does not exist, so a function type cannot be bounded by an aspect. Callable<A, B> is not defined in std::core, so a function or closure type cannot satisfy an aspect bound and dyn Callable is unavailable. Open record ↗plannedGAP-FUNCTIONS-003 — Written function types are move-only, with no copy qualifier or Erased state yet. A written function type is always move-only; the copy |T| -> U qualifier and the Erased (capability unknown) state are not available. Open record ↗