Declarations
public may be prefixed to top-level fun, struct, enum, and aspect
declarations to mark them as accessible from other modules. Top-level let and var
bindings remain module-private. See §Visibility for
the full rules. The current public / var / extend surface spellings are the
spelling set introduced by RFC-0098.
Variables
Immutable Bindings
fun main() -> i64 {
let x := 42;
let name: String := "Vlad";
if (name == "Vlad") { return x; }
return 0;
}
let bindings cannot be reassigned and must always be initializedL1. Mutability lives entirely on the binding — a let binding is immutable regardless of what value it holds. This means:
x := newValueis rejected (reassignment)x.field = valueis rejected (field assignment through an immutable binding)&var xis rejected (taking a mutable reference to an immutable binding)
All three forms require var.
Formal rules
Legality Rule №1
A let binding must be initialized with the := separator and cannot be assigned
after initialization. := is the sole separator that introduces a kept binding; the
plain = spelling is a parse error (RFC-0136).
Referenced by: rfc-0042
Tested by (2)
1// RFC-0136: `let`/`var` declarations, plain reassignment, and associated-type
2// definition use `:=`. The old `=` separator is a hard parse error — no alias.
3// (Compound `+=` etc. and struct-field `=` are unaffected; see neg_15.)
4fun main() {
5 let x = 1;
6}
parse errorP0001
1// Compound assignment to an immutable binding is T0006, same as plain assign.
2let n: i64 := 10;
3n += 5; // ERROR[T0006]
typecheck errorT0006at 3
Legality Rule №2
A conditional aspect implementation may state its bounds inline on its type parameters or
in a where clause; the two spellings are equivalent.
Referenced by: rfc-0036
Tested by (4)
1// RFC-0036: an impl block may declare its own generics with an inline bound
2// (`impl<T: Bound> Aspect for Type<T>`). Real bound-satisfaction checking at each
3// instantiation (rejecting an unsatisfying T) is issue #241's job, not this one's —
4// this fixture locks in that the syntax parses, constructs, and dispatches,
5// including a call to another bounded method (`to_string()` via `Display`) on the
6// generic field itself — issue #267's regression coverage lives here too, since
7// that gap was found while first writing this fixture.
8
9struct Box1<T> {
10 value: T,
11}
12
13aspect Greet {
14 fun greet(&self) -> String;
15}
16
17extend<T: Display> Box1<T>: Greet {
18 fun greet(&self) -> String {
19 return "hello, " + self.value.to_string();
20 }
21}
22
23fun main() {
24 let a := Box1 { value = 5 };
25 assert(a.greet() == "hello, 5");
26}
passes
1// RFC-0036: the `where` form of a conditional impl (`impl Aspect for Type<T> where
2// T: Bound`), equivalent to the inline-bound form. Kept to a single impl block and
3// a method body that doesn't call another bounded method on the generic field
4// itself — see 69_conditional_impl_inline_bound.mtl's note on a pre-existing,
5// unrelated gap that combination currently hits.
6
7struct Box2<T> {
8 value: T,
9}
10
11aspect Farewell {
12 fun farewell(&self) -> String;
13}
14
15extend Box2<T>: Farewell where T: Display {
16 fun farewell(&self) -> String {
17 return "bye";
18 }
19}
20
21fun main() {
22 let b := Box2 { value = 9 };
23 assert(b.farewell() == "bye");
24}
passes
1// RFC-0082 + RFC-0036: an aspect's associated type may be defined by a
2// *conditional/generic* impl (`extend<T: Bound> Wrapper<T>: Container { ... }`),
3// not only by an impl for a fully concrete type (every existing stage13
4// fixture uses a concrete `extend IntBox: Container { ... }`). The
5// projection resolves through the generic impl at both call sites and
6// inside the impl's own method body, so this checks it actually dispatches
7// correctly at runtime, not just that it type-checks.
8
9aspect Container {
10 type Item: Display;
11 fun get(&self) -> Item;
12}
13
14struct Wrapper<T> {
15 value: T,
16}
17
18extend<T: Display> Wrapper<T>: Container {
19 type Item := T;
20 fun get(&self) -> T {
21 return self.value;
22 }
23}
24
25fun show_item<C: Container>(c: C) -> String {
26 return c.get().to_string();
27}
28
29fun main() {
30 let w := Wrapper { value = 42 };
31 assert(show_item(w) == "42");
32}
passes
1// RFC-0036 + core Iterable: a *generic* struct implementing `Iterable<T>`
2// generically (`extend<T> Wrapper<T>: Iterable<T> { ... }`), iterated via
3// `for (x in w)` sugar, not a direct `.next()` call. Every existing Iterable
4// fixture implements it for a concrete, non-generic target (e.g. `Counter:
5// Iterable<i64>`), so this is the first to exercise the generic case through
6// `for-in`.
7//
8// Regression for #257 (registry corruption) and a related dispatch gap it
9// exposed: for-in's element-type derivation went through two separate lookup
10// paths (type inference and typed-AST construction), and neither followed
11// the generic struct's polymorphic method scheme -- inference read a bogus
12// placeholder type recorded at registration time (the impl's own still-
13// generic type parameter, not a real concrete type), and construction only
14// checked the concrete-impl method_env, missing generic impls entirely. The
15// `elem + 1` below fails to typecheck as an integer unless the element type
16// is correctly recovered as the receiver's actual instantiation (i64 here),
17// not the bogus placeholder.
18
19aspect Iterable<T> {
20 fun next(&var self) -> Perhaps<T>;
21}
22
23struct Wrapper<T> {
24 value: T,
25 used: boolean,
26}
27
28extend<T> Wrapper<T>: Iterable<T> {
29 fun next(&var self) -> Perhaps<T> {
30 if (self.used) {
31 return None;
32 }
33 self.used := true;
34 return Perhaps::Some { value = self.value };
35 }
36}
37
38fun main() {
39 let var w := Wrapper { value = 42, used = false };
40 for (x in w) {
41 assert(x + 1 == 43);
42 }
43}
passes
Legality Rule №3
A conditional aspect implementation applies only to instantiations whose type arguments satisfy every bound stated by that implementation.
Referenced by: rfc-0036
Tested by
1// RFC-0036: an impl block may declare its own generics with an inline bound
2// (`impl<T: Bound> Aspect for Type<T>`). Real bound-satisfaction checking at each
3// instantiation (rejecting an unsatisfying T) is issue #241's job, not this one's —
4// this fixture locks in that the syntax parses, constructs, and dispatches,
5// including a call to another bounded method (`to_string()` via `Display`) on the
6// generic field itself — issue #267's regression coverage lives here too, since
7// that gap was found while first writing this fixture.
8
9struct Box1<T> {
10 value: T,
11}
12
13aspect Greet {
14 fun greet(&self) -> String;
15}
16
17extend<T: Display> Box1<T>: Greet {
18 fun greet(&self) -> String {
19 return "hello, " + self.value.to_string();
20 }
21}
22
23fun main() {
24 let a := Box1 { value = 5 };
25 assert(a.greet() == "hello, 5");
26}
passes
Legality Rule №4
Conditional implementation bounds are checked whenever the aspect is required, including method dispatch, bound satisfaction, and implementation selection.
Referenced by: rfc-0036
Tested by (6)
1// RFC-0036: an impl block may declare its own generics with an inline bound
2// (`impl<T: Bound> Aspect for Type<T>`). Real bound-satisfaction checking at each
3// instantiation (rejecting an unsatisfying T) is issue #241's job, not this one's —
4// this fixture locks in that the syntax parses, constructs, and dispatches,
5// including a call to another bounded method (`to_string()` via `Display`) on the
6// generic field itself — issue #267's regression coverage lives here too, since
7// that gap was found while first writing this fixture.
8
9struct Box1<T> {
10 value: T,
11}
12
13aspect Greet {
14 fun greet(&self) -> String;
15}
16
17extend<T: Display> Box1<T>: Greet {
18 fun greet(&self) -> String {
19 return "hello, " + self.value.to_string();
20 }
21}
22
23fun main() {
24 let a := Box1 { value = 5 };
25 assert(a.greet() == "hello, 5");
26}
passes
1// RFC-0036: the `where` form of a conditional impl (`impl Aspect for Type<T> where
2// T: Bound`), equivalent to the inline-bound form. Kept to a single impl block and
3// a method body that doesn't call another bounded method on the generic field
4// itself — see 69_conditional_impl_inline_bound.mtl's note on a pre-existing,
5// unrelated gap that combination currently hits.
6
7struct Box2<T> {
8 value: T,
9}
10
11aspect Farewell {
12 fun farewell(&self) -> String;
13}
14
15extend Box2<T>: Farewell where T: Display {
16 fun farewell(&self) -> String {
17 return "bye";
18 }
19}
20
21fun main() {
22 let b := Box2 { value = 9 };
23 assert(b.farewell() == "bye");
24}
passes
1// RFC-0036: runtime end-to-end confirmation that conditional impl dispatch
2// actually calls the correct method body and returns the expected value.
3
4aspect Display {
5 fun display(self) -> String;
6}
7
8extend i64: Display {
9 fun display(self) -> String { return "i64"; }
10}
11
12struct Box1<T> {
13 value: T,
14}
15
16aspect Label {
17 fun label(&self) -> String;
18}
19
20extend<T: Display> Box1<T>: Label {
21 fun label(&self) -> String {
22 return "display-box";
23 }
24}
25
26fun main() {
27 let a := Box1 { value = 5 };
28 assert(a.label() == "display-box");
29}
passes
1// RFC-0082 + RFC-0036: an aspect's associated type may be defined by a
2// *conditional/generic* impl (`extend<T: Bound> Wrapper<T>: Container { ... }`),
3// not only by an impl for a fully concrete type (every existing stage13
4// fixture uses a concrete `extend IntBox: Container { ... }`). The
5// projection resolves through the generic impl at both call sites and
6// inside the impl's own method body, so this checks it actually dispatches
7// correctly at runtime, not just that it type-checks.
8
9aspect Container {
10 type Item: Display;
11 fun get(&self) -> Item;
12}
13
14struct Wrapper<T> {
15 value: T,
16}
17
18extend<T: Display> Wrapper<T>: Container {
19 type Item := T;
20 fun get(&self) -> T {
21 return self.value;
22 }
23}
24
25fun show_item<C: Container>(c: C) -> String {
26 return c.get().to_string();
27}
28
29fun main() {
30 let w := Wrapper { value = 42 };
31 assert(show_item(w) == "42");
32}
passes
1// Cross-module conditional impl regression (RFC-0036 / merge_from boundary):
2// Pair<A, B> + conditional impl live in pair_module.mtl. This main.mtl imports
3// them and calls greet() on a satisfying concrete instantiation — exercising the
4// full path: import → merge_from → method_scheme lookup → bound check.
5//
6// A SECOND fixture (conditional_impl_cross_module_merge_neg) imports the same
7// module and calls greet() on a violating instantiation to confirm T0012.
8
9import pair_module::{Pair, Greet};
10
11fun main() {
12 let p := Pair { first = 5, second = "hello" };
13 let s: String := p.greet();
14 assert(s == "pair");
15}
passes
1// Cross-module conditional impl regression (RFC-0036 / merge_from boundary):
2// The conditional impl requires A: Display AND B: Display. Pair<i64, Pair<i64, i64>>
3// violates the bound for B (Pair itself has no Display impl) — greet() must fail
4// with T0012.
5
6import pair_module::{Pair, Greet};
7
8fun main() {
9 let inner: Pair<i64, i64> := Pair { first = 1, second = 2 };
10 let p := Pair { first = 5, second = inner };
11 let s: String := p.greet(); // ERROR[T0012]
12}
typecheck errorT0012at 11
Legality Rule №5
A type's declaration bounds and an aspect implementation's conditional bounds are independent; satisfying one does not satisfy the other.
Referenced by: rfc-0036
Tested by (5)
1// RFC-0036 §2.2: conditional impl with inline bound, called with a satisfying
2// concrete type.
3
4aspect Printable {
5 fun print(self) -> String;
6}
7
8extend i64: Printable {
9 fun print(self) -> String { return "i64"; }
10}
11
12struct Pair<A, B> {
13 first: A,
14 second: B,
15}
16
17extend<A: Printable, B: Printable> Pair<A, B>: Printable {
18 fun print(self) -> String {
19 return "pair";
20 }
21}
22
23fun main() {
24 let p := Pair { first = 5, second = 10 };
25 let s: String := p.print();
26}
passes
1// RFC-0036 §2.2: conditional impl with where clause, called with a satisfying
2// concrete type.
3
4aspect Printable {
5 fun print(self) -> String;
6}
7
8extend i64: Printable {
9 fun print(self) -> String { return "i64"; }
10}
11
12struct Pair<A, B> {
13 first: A,
14 second: B,
15}
16
17extend<A, B> Pair<A, B>: Printable where A: Printable, B: Printable {
18 fun print(self) -> String {
19 return "pair";
20 }
21}
22
23fun main() {
24 let p := Pair { first = 5, second = 10 };
25 let s: String := p.print();
26}
passes
1// RFC-0036 §2.2: two-param conditional impl, both bounds satisfied.
2
3aspect Printable {
4 fun print(self) -> String;
5}
6
7extend i64: Printable {
8 fun print(self) -> String { return "i64"; }
9}
10
11extend String: Printable {
12 fun print(self) -> String { return "string"; }
13}
14
15struct Pair<A, B> {
16 first: A,
17 second: B,
18}
19
20extend<A, B> Pair<A, B>: Printable where A: Printable, B: Printable {
21 fun print(self) -> String {
22 return "pair";
23 }
24}
25
26fun main() {
27 let p := Pair { first = 5, second = "hello" };
28 let s: String := p.print();
29}
passes
1// RFC-0036 §2.2: a conditional impl's satisfaction must be checked, not just
2// assumed unsatisfied, when the conditionally-implementing type is passed as
3// an argument bound on an UNRELATED generic function (not just via direct
4// method dispatch on the receiver itself).
5
6aspect Printable {
7 fun print(self) -> String;
8}
9
10extend i64: Printable {
11 fun print(self) -> String { return "i64"; }
12}
13
14struct Pair<A, B> {
15 first: A,
16 second: B,
17}
18
19extend<A: Printable, B: Printable> Pair<A, B>: Printable {
20 fun print(self) -> String {
21 return "pair";
22 }
23}
24
25fun describe<T: Printable>(x: T) -> String {
26 return x.print();
27}
28
29fun main() {
30 let p := Pair { first = 5, second = 10 };
31 let s: String := describe(p);
32}
passes
1// RFC-0036 §2.2: mirror of stage17_05 with a type argument that genuinely
2// does NOT satisfy the conditional impl's bound -> must still be rejected
3// when passed through an unrelated generic function's own bound.
4
5aspect Printable {
6 fun print(self) -> String;
7}
8
9struct NoPrint {
10 value: i64,
11}
12
13struct Pair<A, B> {
14 first: A,
15 second: B,
16}
17
18extend<A: Printable, B: Printable> Pair<A, B>: Printable {
19 fun print(self) -> String {
20 return "pair";
21 }
22}
23
24fun describe<T: Printable>(x: T) -> String {
25 return x.print();
26}
27
28fun main() {
29 let p := Pair { first = NoPrint { value = 1 }, second = NoPrint { value = 2 } };
30 let s: String := describe(p); // ERROR[T0012]
31}
typecheck errorT0012at 30
Legality Rule №6
A generic function using a conditional implementation must state the required bounds on its own type parameters; those bounds are not inferred from the function body.
Referenced by: rfc-0036
Tested by
1// RFC-0036 §2.3: conditional impl bounds visible through a generic function.
2
3aspect Printable {
4 fun print(self) -> String;
5}
6
7extend i64: Printable {
8 fun print(self) -> String { return "i64"; }
9}
10
11struct Pair<A, B> {
12 first: A,
13 second: B,
14}
15
16extend<A: Printable, B: Printable> Pair<A, B>: Printable {
17 fun print(self) -> String {
18 return "pair";
19 }
20}
21
22fun print_pair<A: Printable, B: Printable>(p: Pair<A, B>) {
23 let s: String := p.print();
24}
25
26fun main() {
27 let p := Pair { first = 5, second = 10 };
28 print_pair(p);
29}
passes
Legality Rule №7
Conditional implementations participate in the ordinary coherence and orphan-rule checks.
Referenced by: rfc-0036
Tested by
1// RFC-0036§3 and RFC-0036§3.2: unconditional impl + conditional impl for the same aspect/type
2// constructor → T0015 conflict. The unconditional impl has all-empty scoped
3// bounds, so provably_disjoint can never return true against it.
4
5aspect Display {
6 fun display(self) -> String;
7}
8
9aspect Printable {
10 fun print(self) -> String;
11}
12
13struct Pair<A, B> {
14 first: A,
15 second: B,
16}
17
18// Unconditional blanket impl — no bounds, scoped bounds are all-empty.
19extend<A, B> Pair<A, B>: Printable {
20 fun print(self) -> String { return "unconditional"; }
21}
22
23// Conditional impl — Display bound at position 0.
24extend<C: Display> Pair<C, C>: Printable { // ERROR[T0015]
25 fun print(self) -> String { return "conditional"; }
26}
27
28fun main() {}
typecheck errorT0015at 24:1“conflicting implementation”
Legality Rule №8
Two conditional implementations of the same aspect and target are disjoint only when an explicit negative bound in one directly negates a positive bound in the other; otherwise an overlapping pair is rejected with T0015.
Referenced by: rfc-0036
Tested by (3)
1// RFC-0036 §3.1's fix-up example verbatim: two conditional impls of the same
2// aspect for the same struct, one bounded `T: Clone2 + !Display2` and the
3// other `T: Display2` -- disjoint via the explicitly-added negation on
4// `Display2`, so no coherence conflict, even though this exercises the
5// multi-bound-conjunction path (positive AND negative bounds in one impl's
6// generic param), not just the single-bound case.
7
8aspect Clone2 {
9 fun clone2(self) -> i64;
10}
11
12aspect Display2 {
13 fun show(self) -> String;
14}
15
16aspect Serialize2 {
17 fun ser(self) -> String;
18}
19
20struct Wrapper<T> {
21 val: T,
22}
23
24extend<T: Clone2 + !Display2> Wrapper<T>: Serialize2 {
25 fun ser(self) -> String { return "no-display"; }
26}
27
28extend<T: Display2> Wrapper<T>: Serialize2 {
29 fun ser(self) -> String { return "display"; }
30}
31
32fun main() {}
passes
1// RFC-0036 §3.1's accepted example verbatim: `impl<T: Copy> ...` and
2// `impl<T: !Copy> ...` for the same aspect/struct are provably disjoint via
3// syntactic negation and must NOT be reported as a coherence conflict.
4//
5// (Previously believed unparseable and skipped by the original #241
6// implementation -- confirmed working end to end: parsing, use-site bound
7// checking, and coherence disjointness detection all handle `!Aspect`.)
8
9aspect Copy2 {
10 fun dup(self) -> i64;
11}
12
13aspect Serialize2 {
14 fun ser(self) -> String;
15}
16
17struct Wrapper<T> {
18 val: T,
19}
20
21extend<T: Copy2> Wrapper<T>: Serialize2 {
22 fun ser(self) -> String { return "copyable"; }
23}
24
25extend<T: !Copy2> Wrapper<T>: Serialize2 {
26 fun ser(self) -> String { return "not-copyable"; }
27}
28
29fun main() {}
passes
1// RFC-0061 §2: "Coherence rules for structural impl targets follow RFC-0060
2// §2 and RFC-0036 §3.1 without special cases." This is the structural-target
3// counterpart of conditional_impl_negation_disjoint_accepted (which targets a
4// nominal struct): `impl<T: Copy3> ...` and `impl<T: !Copy3> ...` for the
5// same locally-declared aspect on the *same structural target* (`T[]`) are
6// provably disjoint via syntactic negation and must not be reported as a
7// coherence conflict. `Serialize3` is declared locally, so the orphan rule
8// permits a user module to implement it for `T[]` at all (RFC-0061 §1).
9
10aspect Copy3 {
11 fun dup(self) -> i64;
12}
13
14aspect Serialize3 {
15 fun ser(self) -> String;
16}
17
18extend<T: Copy3> T[]: Serialize3 {
19 fun ser(self) -> String { return "copyable"; }
20}
21
22extend<T: !Copy3> T[]: Serialize3 {
23 fun ser(self) -> String { return "not-copyable"; }
24}
25
26fun main() {}
passes
Legality Rule №9
A conditional and an unconditional implementation of the same aspect for the same target conflict, because the unconditional implementation covers every conditional instantiation.
Referenced by: rfc-0036
Tested by
1// RFC-0036§3 and RFC-0036§3.2: unconditional impl + conditional impl for the same aspect/type
2// constructor → T0015 conflict. The unconditional impl has all-empty scoped
3// bounds, so provably_disjoint can never return true against it.
4
5aspect Display {
6 fun display(self) -> String;
7}
8
9aspect Printable {
10 fun print(self) -> String;
11}
12
13struct Pair<A, B> {
14 first: A,
15 second: B,
16}
17
18// Unconditional blanket impl — no bounds, scoped bounds are all-empty.
19extend<A, B> Pair<A, B>: Printable {
20 fun print(self) -> String { return "unconditional"; }
21}
22
23// Conditional impl — Display bound at position 0.
24extend<C: Display> Pair<C, C>: Printable { // ERROR[T0015]
25 fun print(self) -> String { return "conditional"; }
26}
27
28fun main() {}
typecheck errorT0015at 24:1“conflicting implementation”
Legality Rule №10
A conditional implementation is subject to the orphan rule: either its aspect or its target's outermost constructor must be local to the implementing module.
Referenced by: rfc-0036
Tested by
1// RFC-0036 §3.3: a conditional impl obeys the orphan rule just as an
2// unconditional impl does. This module owns neither imported half.
3import greet_aspect::Greet;
4import widget::Widget;
5
6extend<T: Display> Widget<T>: Greet { // ERROR[T0014]
7 fun greet(self) -> String { return "hi"; }
8}
9
10fun main() {}
typecheck errorT0014at 6:1“orphan implementation”
Legality Rule №11
When a conditional implementation's bound is unsatisfied, the compiler reports T0012 and identifies the unsatisfied condition.
Referenced by: rfc-0036
Tested by (2)
1// RFC-0036 §4: conditional impl bound not satisfied at use site -> T0012.
2
3aspect Printable {
4 fun print(self) -> String;
5}
6
7struct Pair<A, B> {
8 first: A,
9 second: B,
10}
11
12struct NoPrint {
13 value: i64,
14}
15
16extend<A: Printable, B: Printable> Pair<A, B>: Printable {
17 fun print(self) -> String {
18 return "pair";
19 }
20}
21
22fun main() {
23 let p := Pair { first = 5, second = NoPrint { value = 0 } };
24 let s: String := p.print(); // ERROR[T0012]
25}
typecheck errorT0012at 24
1// RFC-0036 §4: two-param conditional impl, one bound satisfied, one not -> T0012.
2
3aspect Printable {
4 fun print(self) -> String;
5}
6
7extend i64: Printable {
8 fun print(self) -> String { return "i64"; }
9}
10
11struct Pair<A, B> {
12 first: A,
13 second: B,
14}
15
16struct NoPrint {
17 value: i64,
18}
19
20extend<A: Printable, B: Printable> Pair<A, B>: Printable {
21 fun print(self) -> String {
22 return "pair";
23 }
24}
25
26fun main() {
27 let p := Pair { first = NoPrint { value = 0 }, second = 10 };
28 let s: String := p.print(); // ERROR[T0012]
29}
typecheck errorT0012at 28
Legality Rule №12
An implementation method's signature is compared against the aspect method's after
the aspect signature is specialized with the extend block's target type for
Self, its aspect arguments, and its associated-type definitions. After that
specialization the receiver form, ordinary parameter count and types, and result
type must be equal; method generic-parameter names compare alpha-equivalently.
Referenced by: rfc-0129
Tested by
1struct Point { x: i64 }
2
3aspect Describe {
4 fun describe(&self) -> String;
5}
6
7extend Point: Describe {
8 fun describe(&self, suffix: String) -> String { return suffix; }
9}
10
11fun main() {}
typecheck errorT0012“does not match the signature declared by aspect”
Legality Rule №13
After specialization (legality-12) and after normalizing away alpha-renaming,
conjunctive-bound order, duplicate bounds, inline-versus-where placement, and
generic-binder-versus-where record-kind placement, an implementation method's
generic-constraint conjunction must be structurally equal to the aspect method's.
Equality is over resolved atoms — every aspect, type, associated-type, and
row-label reference stands for the entity it resolves to, not its spelling — and
covers each parameter's record kind, the set of positive and negative aspect
bounds, the set of row bounds, and the set of associated-type equality bindings
(each identified by its resolved projection key and right-hand-side type after
specialization). Neither weakening nor strengthening a constraint conforms.
Referenced by: rfc-0129
Tested by (7)
1// RFC-0129 legality-13: identical method generic constraints conform, record
2// kind included. The impl repeats the aspect's `<record T>` verbatim.
3
4aspect Keep {
5 fun keep<record T>(&self, value: T) -> T;
6}
7
8struct Slot {}
9
10extend Slot: Keep {
11 fun keep<record T>(&self, value: T) -> T { return value; }
12}
13
14fun main() {}
passes
1// RFC-0129 legality-13 with §3 normalization: alpha-renaming the method generic
2// parameter (`T` -> `U`) and writing its bounds in a different order do not
3// change the admissible domain, so the impl still conforms.
4
5aspect Combine {
6 fun combine<T: Copy + Clone>(&self, a: T, b: T) -> T;
7}
8
9struct Merger {}
10
11extend Merger: Combine {
12 fun combine<U: Clone + Copy>(&self, a: U, b: U) -> U { return a; }
13}
14
15fun main() {}
passes
1// RFC-0129 legality-13 with §3 normalization: an inline bound on the aspect side
2// and the same bound in the impl's `where` clause are the same constraint. The
3// comparator folds the impl's binder and `where` constraints together first.
4
5aspect Render {
6 fun render<T: Copy>(&self, value: T) -> T;
7}
8
9struct Widget {}
10
11extend Widget: Render {
12 fun render<T>(&self, value: T) -> T where T: Copy { return value; }
13}
14
15fun main() {}
passes
1// RFC-0129 legality-13: an aspect implementation may not *strengthen* a method
2// generic constraint. `Guess::pick` declares `<T>` with no bound; the impl
3// requires `T: Copy`, which rejects instantiations the aspect admits.
4
5aspect Guess {
6 fun pick<T>(&self, a: T, b: T) -> T;
7}
8
9struct Chooser {}
10
11extend Chooser: Guess {
12 fun pick<T: Copy>(&self, a: T, b: T) -> T { return a; }
13}
14
15fun main() {}
typecheck errorT0012“does not match the signature declared by aspect”
1// RFC-0129 legality-13 (the metel-core#616 fix): record kind is part of the
2// generic-constraint comparison. `Store::keep` declares `<T>`; the impl requires
3// `<record T>`, so a caller with only a `Store` bound could call `keep(1)` while
4// the impl needs a record. Adding the record kind is strengthening -> rejected.
5
6aspect Store {
7 fun keep<T>(&self, value: T) -> T;
8}
9
10struct Box {}
11
12extend Box: Store {
13 fun keep<record T>(&self, value: T) -> T { return value; }
14}
15
16fun main() {}
typecheck errorT0012“does not match the signature declared by aspect”
1// RFC-0129 legality-13, conservative wrong-no. Dropping a bound (`<T: Copy>` ->
2// `<T>`) is a *safe widening* -- the impl accepts every instantiation the aspect
3// admits -- but RFC-0129's minimal interim rule compares constraint conjunctions
4// for structural equality, so it rejects this too. RFC-0129's deferred
5// admissible-domain-inclusion extension accepts the widening; update this
6// expectation to a positive fixture when that lands.
7
8aspect Relaxed {
9 fun pass<T: Copy>(&self, value: T) -> T;
10}
11
12struct Passthrough {}
13
14extend Passthrough: Relaxed {
15 fun pass<T>(&self, value: T) -> T { return value; }
16}
17
18fun main() {}
typecheck errorT0012“does not match the signature declared by aspect”
1// RFC-0129 legality-13, conservative wrong-no. `<record T>` -> `<T>` is a safe
2// widening: a plain `<T>` impl accepts every record the aspect admits. The
3// minimal structural-equality rule still rejects it; RFC-0129's deferred
4// admissible-domain-inclusion extension will accept it. Flip to a positive
5// fixture when that lands.
6
7aspect Inspect {
8 fun keep<record T>(&self, value: T) -> T;
9}
10
11struct Holder {}
12
13extend Holder: Inspect {
14 fun keep<T>(&self, value: T) -> T { return value; }
15}
16
17fun main() {}
typecheck errorT0012“does not match the signature declared by aspect”
Legality Rule №14
An implementation method whose signature (legality-12) or generic constraints (legality-13) do not conform is a type error on that method's own declaration, reported with T0012. Such a method does not satisfy the aspect and does not contribute to aspect-method dispatch.
Referenced by: rfc-0129
Tested by
1// RFC-0129 legality-13: an aspect implementation may not *strengthen* a method
2// generic constraint. `Guess::pick` declares `<T>` with no bound; the impl
3// requires `T: Copy`, which rejects instantiations the aspect admits.
4
5aspect Guess {
6 fun pick<T>(&self, a: T, b: T) -> T;
7}
8
9struct Chooser {}
10
11extend Chooser: Guess {
12 fun pick<T: Copy>(&self, a: T, b: T) -> T { return a; }
13}
14
15fun main() {}
typecheck errorT0012“does not match the signature declared by aspect”
Mutable Bindings
fun main() -> i64 {
var counter := 0;
counter := counter + 1;
counter += 1;
return counter;
}
var bindings can be reassigned and also must be initialized at declarationL1. Compound assignment operators +=, -=, *=, /=, %= are supported.
Formal rules
Legality Rule №1
A var binding must be initialized and may be assigned after initialization; var is the
mutable binding spelling. Both the initializer and a subsequent plain reassignment use the
:= separator (RFC-0136); the compound assignment operators +=, -=, *=, /=, %=
keep =. The bare = spelling for a var initializer or reassignment is a parse error.
Referenced by: rfc-0042, rfc-0098, rfc-0136
Tested by (3)
1fun main() {
2 // Basic counting.
3 var sum := 0;
4 for (var i := 0; i < 5; i += 1) { sum += i; }
5 assert(sum == 10);
6 // Break exits the loop.
7 var count := 0;
8 for (var i := 0; i < 100; i += 1) {
9 if (i == 5) { break; }
10 count += 1;
11 }
12 assert(count == 5);
13 // Continue still executes the step expression.
14 var c2 := 0;
15 for (var i := 0; i < 5; i += 1) {
16 if (i == 2) { continue; }
17 c2 += 1;
18 }
19 assert(c2 == 4);
20}
passes
1// RFC-0136: `let`/`var` declarations, plain reassignment, and associated-type
2// definition use `:=`. The old `=` separator is a hard parse error — no alias.
3// (Compound `+=` etc. and struct-field `=` are unaffected; see neg_15.)
4fun main() {
5 let x = 1;
6}
parse errorP0001
1// RFC-0136: plain reassignment uses `:=`. Old `x = e` is a parse error.
2fun main() {
3 var x := 0;
4 x = 1;
5}
parse errorP0001
Scoping and Shadowing
Variables are lexically scopedL1. Each block { } introduces a new scope. Inner scopes can shadow outer variables.
let and var declarations are sequentialL2 — a binding is visible only from its declaration point to the end of its containing block.
fun declarations are hoisted to the top of their containing blockL3. All fun declarations in a block are mutually visible to each other and to all other statements in that block, regardless of declaration order. This enables forward references and mutual recursion at any nesting level.
Hoisting is block-local: a fun declared in an inner block is not visible in the outer block. Normal lexical scoping applies across block boundaries — inner blocks see outer declarations, outer blocks do not see inner declarations.
fun is_even(n: i64) -> boolean {
if (n == 0) { return true; }
return is_odd(n - 1);
}
fun is_odd(n: i64) -> boolean {
if (n == 0) { return false; }
return is_even(n - 1);
}
fun outer() -> i64 {
inner();
fun inner() {
helper();
fun helper() { }
}
return 1;
}
fun main() -> i64 {
if (is_odd(3)) { return outer(); }
return 0;
}
An inner function remains scoped to its own block. For example, helper(); is valid inside inner(), but calling helper(); from outer() is a type error.
fun outer() {
fun inner() {
fun helper() { }
helper();
}
helper();
}
fun main() {
outer();
}
Top-level struct and enum declarations are hoisted to program scopeL4 — they may be
referenced before their declaration appears in the source.
Types declared inside a function body are local to that body from their declaration point onward; they are not visible from other functions.
fun make_point() -> Point {
return Point { x = 1.0, y = 2.0 }; // OK — Point is globally visible
}
struct Point {
x: f64,
y: f64,
}
fun inner() {
struct LocalPoint {
x: f64,
y: f64,
}
let p := LocalPoint { x = 1.0, y = 2.0 };
}
fun main() -> i64 {
inner();
let p := make_point();
return p.x as i64;
}
Top-level extend blocks follow the same declaration-order rule as the types they extend.
Formal rules
Legality Rule №1
Each block introduces a lexical scope. A declaration in an inner scope may shadow an outer declaration, and the outer declaration is not visible outside its own scope.
Tested by
1fun main() {
2 // Inner binding shadows outer; outer is unchanged after the if.
3 let x := 1;
4 let inner_x := if (true) { let x := 99; x } else { 0 };
5 assert(inner_x == 99);
6 assert(x == 1);
7}
passes
Legality Rule №2
A let or var binding is in scope from its declaration through the end of its containing
block, but not before its declaration.
Tested by
1fun main() {
2 let _before := value;
3 let value := 42;
4}
typecheck errorT0003“undefined name `value`”
Legality Rule №3
Function declarations are visible throughout their containing block regardless of source order, including to other functions in that block; this hoisting does not extend out of an inner block.
Tested by (2)
1// Test: forward references between top-level functions
2//
3// The pre-pass registers all top-level function names before inference begins,
4// so a function can call another declared later in the file.
5//
6// Inference order:
7// 1. Pre-pass: register both `even` and `odd` with fresh type variables
8// 2. Infer `is_nonzero` body: n > 0 gives boolean, matches return type boolean ✓
9// 3. Infer `classify` body: calls is_nonzero(n), which returns boolean ✓
10//
11// Expected inferred types:
12// is_nonzero(i64) -> boolean
13// classify(i64) -> boolean
14// result : boolean
15
16fun classify(n: i64) -> boolean {
17 is_nonzero(n)
18}
19
20fun is_nonzero(n: i64) -> boolean {
21 n > 0
22}
23
24let result := classify(5);
passes
1// Regression (metel-core#712): a nested `fun` forward-referenced from a `let`
2// initializer failed at runtime, even though the typechecker accepted it. Covers all
3// five forms #712 characterised, plus a bare-statement call with a `var` present.
4
5fun call_in_let_initializer() -> i64 {
6 let r := f();
7 fun f() -> i64 { 7 }
8 r
9}
10
11fun call_after_declaration() -> i64 {
12 fun g() -> i64 { 8 }
13 g()
14}
15
16// No let/var here, so this was already working (eager-build fast path).
17fun call_as_bare_statement_no_let_var() -> i64 {
18 h();
19 fun h() -> i64 { 9 }
20 h()
21}
22
23// Same shape with a var present -- the new fallback path, via a bare statement.
24fun call_as_bare_statement_with_var() -> i64 {
25 var hit := 0;
26 i();
27 fun i() { hit := 11; }
28 hit
29}
30
31fun mutual_recursion() -> boolean {
32 fun is_even(n: i64) -> boolean { if (n == 0) { true } else { is_odd(n - 1) } }
33 fun is_odd(n: i64) -> boolean { if (n == 0) { false } else { is_even(n - 1) } }
34 is_even(10) && is_odd(7)
35}
36
37fun top_level_caller() -> i64 { top_level_callee() }
38fun top_level_callee() -> i64 { 10 }
39
40fun main() {
41 assert(call_in_let_initializer() == 7);
42 assert(call_after_declaration() == 8);
43 assert(call_as_bare_statement_no_let_var() == 9);
44 assert(call_as_bare_statement_with_var() == 11);
45 assert(mutual_recursion());
46 assert(top_level_caller() == 10);
47}
passes
Legality Rule №4
Top-level struct and enum declarations are visible throughout the program regardless of source order. A type declared inside a function is visible only from its declaration through that function body.
Tested by
1// Local struct declarations are scoped to their enclosing function body.
2// Two functions may declare structs with the same name independently.
3
4fun make_point() -> i64 {
5 struct Point { x: i64, y: i64 }
6 let p := Point { x = 3, y = 4 };
7 p.x + p.y
8}
9
10// Same struct name, different fields — no collision with make_point's Point.
11fun make_named() -> i64 {
12 struct Point { label: i64 }
13 let p := Point { label = 99 };
14 p.label
15}
16
17fun main() {
18 assert(make_point() == 7);
19 assert(make_named() == 99);
20
21 // Local struct usable inline within the same block.
22 struct Vec2 { dx: i64, dy: i64 }
23 let v := Vec2 { dx = 10, dy = 20 };
24 assert(v.dx + v.dy == 30);
25}
passes
Structs
struct Point {
x: f64,
y: f64,
}
fun main() -> i64 {
let p := Point { x = 1.0, y = 2.0 };
return p.y as i64;
}
Instantiation and Field Access
=, not : — Point { x = 1.0 }; field declarations keep :struct Point {
x: f64,
y: f64,
}
fun main() -> i64 {
let p := Point { x = 1.0, y = 2.0 };
let x := p.x;
return x as i64;
}
When a local variable has the same name as a field, the = value part can be omitted
(shorthand field initL1):
struct Point {
x: f64,
y: f64,
}
fun main() -> i64 {
let x := 1.0;
let y := 2.0;
let p := Point { x, y };
return p.x as i64;
}
Shorthand and explicit fields may be mixed freely within one literal.
Zero-field structs may omit braces entirelyL2. These two forms are equivalentD2:
struct Empty {}
let a := Empty;
let b := Empty {};
Formal rules
Legality Rule №1
A struct-literal field initializer is ident, optionally followed by = expr. When = expr is present, ident names the field and expr its value. When omitted, ident must
name both the field and a local binding in scope at the literal (shorthand/punning field
init). Shorthand and explicit fields may be freely mixed within one struct literal.
Referenced by: rfc-0115
Tested by
1// Shorthand field initialisation: `Point { x, y }` desugars to `Point { x = x, y = y }`.
2
3struct Point { x: i64, y: i64 }
4
5struct Named { name: String, value: i64 }
6
7fun make_point(x: i64, y: i64) -> Point {
8 Point { x, y }
9}
10
11fun main() {
12 // Basic shorthand: both fields.
13 let x := 3;
14 let y := 7;
15 let p := Point { x, y };
16 assert(p.x == 3);
17 assert(p.y == 7);
18
19 // Mixed: shorthand and explicit.
20 let x2 := 10;
21 let p2 := Point { x = x2, y = 20 };
22 assert(p2.x == 10);
23 assert(p2.y == 20);
24
25 // Shorthand via function that uses it internally.
26 let p3 := make_point(5, 6);
27 assert(p3.x == 5);
28 assert(p3.y == 6);
29
30 // String field.
31 let name := "hello";
32 let value := 42;
33 let n := Named { name, value };
34 assert(n.value == 42);
35 assert(n.name.len() == 5);
36}
passes
Dynamic Semantics №1
A shorthand field ident in a struct literal evaluates identically to the explicit form
ident = ident: the field takes the value of the local binding named ident that is in
scope at the literal.
Referenced by: rfc-0115
Tested by
1// Shorthand field initialisation: `Point { x, y }` desugars to `Point { x = x, y = y }`.
2
3struct Point { x: i64, y: i64 }
4
5struct Named { name: String, value: i64 }
6
7fun make_point(x: i64, y: i64) -> Point {
8 Point { x, y }
9}
10
11fun main() {
12 // Basic shorthand: both fields.
13 let x := 3;
14 let y := 7;
15 let p := Point { x, y };
16 assert(p.x == 3);
17 assert(p.y == 7);
18
19 // Mixed: shorthand and explicit.
20 let x2 := 10;
21 let p2 := Point { x = x2, y = 20 };
22 assert(p2.x == 10);
23 assert(p2.y == 20);
24
25 // Shorthand via function that uses it internally.
26 let p3 := make_point(5, 6);
27 assert(p3.x == 5);
28 assert(p3.y == 6);
29
30 // String field.
31 let name := "hello";
32 let value := 42;
33 let n := Named { name, value };
34 assert(n.value == 42);
35 assert(n.name.len() == 5);
36}
passes
Legality Rule №2
A zero-field struct may be constructed either as its bare type name or with empty braces.
Referenced by: rfc-0106
Tested by
1struct Empty {}
2
3enum Flag {
4 On {},
5}
6
7fun use_empty(x: Empty) -> i64 {
8 1
9}
10
11fun use_flag(f: Flag) -> i64 {
12 match (f) {
13 Flag::On => 7,
14 }
15}
16
17fun main() {
18 let a := Empty;
19 let b := Empty {};
20 let x := Flag::On;
21 let y := Flag::On {};
22
23 assert(use_empty(a) == 1);
24 assert(use_empty(b) == 1);
25 assert(use_flag(x) == 7);
26 assert(use_flag(y) == 7);
27}
passes
Dynamic Semantics №2
For a zero-field struct, the bare and empty-brace constructor forms evaluate to the same struct value.
Referenced by: rfc-0106
Tested by
1struct Empty {}
2
3enum Flag {
4 On {},
5}
6
7fun use_empty(x: Empty) -> i64 {
8 1
9}
10
11fun use_flag(f: Flag) -> i64 {
12 match (f) {
13 Flag::On => 7,
14 }
15}
16
17fun main() {
18 let a := Empty;
19 let b := Empty {};
20 let x := Flag::On;
21 let y := Flag::On {};
22
23 assert(use_empty(a) == 1);
24 assert(use_empty(b) == 1);
25 assert(use_flag(x) == 7);
26 assert(use_flag(y) == 7);
27}
passes
Legality Rule №3
A struct with fields cannot omit its constructor fields; its bare type name is resolved as a name rather than as a constructor expression.
Referenced by: rfc-0106
Tested by
Methods
struct Point {
x: f64,
y: f64,
}
extend Point {
fun distance(self, other: Point) -> f64 {
let dx := self.x - other.x;
let dy := self.y - other.y;
return dx * dx + dy * dy; // squared distance
}
}
fun main() -> i64 {
let p := Point { x = 1.0, y = 2.0 };
let q := Point { x = 4.0, y = 6.0 };
let d := p.distance(q);
return d as i64;
}
self refers to the receiver. Methods are called with dot syntax.
Receiver Forms
Methods may declare one of three receiver forms:
self— value receiver&self— shared reference receiver&var self— mutable reference receiver
Value receivers follow ordinary Metel value semantics. Shared and mutable reference receivers operate on the original receiver storage and are the right forms for observers and in-place mutation.
struct Point {
x: f64,
y: f64,
}
extend Point {
fun length(&self) -> f64 {
self.x * self.x + self.y * self.y
}
}
struct Counter {
value: i64,
}
extend Counter {
fun increment(&var self) {
self.value += 1;
}
}
Calls requiring &var self need a mutable addressable receiver or a &var T
referenceL1. Calls requiring &self may use an addressable receiver or a &T / &var T
reference (RFC-0067a — missed when that RFC integrated *T/*mut T → &T/&var T
elsewhere; caught while integrating this batch).
struct Counter {
value: i64,
}
extend Counter {
fun increment(&var self) {
self.value += 1;
}
}
fun main() -> i64 {
var c := Counter { value = 1 };
c.increment();
return c.value;
}
Formal rules
Legality Rule №1
&var self is the mutable-reference receiver spelling and requires a mutable addressable
receiver or an &var T reference at the call site.
Referenced by: rfc-0044, rfc-0067a, rfc-0098
Tested by (3)
1struct Counter {
2 value: i64,
3}
4
5extend Counter {
6 fun increment(&var self) {
7 self.value += 1;
8 }
9
10 fun current(&self) -> i64 {
11 self.value
12 }
13}
14
15fun main() {
16 var counter := Counter { value = 0 };
17 counter.increment();
18 counter.increment();
19 assert(counter.current() == 2);
20}
passes
1// ── Struct used throughout ────────────────────────────────────────────────────
2
3struct Counter {
4 value: i64,
5 step: i64,
6}
7
8extend Counter {
9 fun new(step: i64) -> Counter {
10 Counter { value = 0, step = step }
11 }
12
13 // value receiver: returns a new Counter, does not mutate in place
14 fun with_value(&self, n: i64) -> Counter {
15 Counter { value = n, step = self.step }
16 }
17
18 // &self: read-only, non-consuming
19 fun get(&self) -> i64 {
20 self.value
21 }
22
23 fun is_positive(&self) -> boolean {
24 self.value > 0
25 }
26
27 // &var self: in-place mutation
28 fun tick(&var self) {
29 self.value += self.step;
30 }
31
32 fun reset(&var self) {
33 self.value := 0;
34 }
35
36 fun add(&var self, n: i64) {
37 self.value += n;
38 }
39}
40
41// ── Value receiver does not mutate the original ───────────────────────────────
42
43fun test_value_receiver() {
44 let c := Counter::new(1);
45 let d := c.with_value(42);
46 // c is unchanged — value receiver copied it
47 assert(c.get() == 0);
48 assert(d.get() == 42);
49}
50
51// ── &self is non-consuming: can be called repeatedly ─────────────────────────
52
53fun test_ref_receiver_repeated() {
54 var c := Counter::new(1);
55 c.tick();
56 assert(c.is_positive() == true);
57 assert(c.get() == 1);
58 // calling &self methods does not consume c — still usable
59 assert(c.is_positive() == true);
60 assert(c.get() == 1);
61}
62
63// ── &var self accumulates state across calls ──────────────────────────────────
64
65fun test_mut_receiver_accumulates() {
66 var c := Counter::new(3);
67 c.tick(); // 3
68 c.tick(); // 6
69 c.tick(); // 9
70 assert(c.get() == 9);
71 c.reset();
72 assert(c.get() == 0);
73 c.add(100);
74 assert(c.get() == 100);
75}
76
77// ── &self and &var self interleaved ───────────────────────────────────────────
78
79fun test_interleaved_receivers() {
80 var c := Counter::new(10);
81 assert(c.get() == 0);
82 c.tick();
83 assert(c.get() == 10);
84 assert(c.is_positive() == true);
85 c.tick();
86 assert(c.get() == 20);
87 c.reset();
88 assert(c.get() == 0);
89 assert(c.is_positive() == false);
90}
91
92// ── Calling through a &T reference (auto-deref, &self methods) ─────────────────
93
94fun test_ref_through_pointer() {
95 var c := Counter::new(1);
96 c.tick();
97 c.tick();
98 let p: &Counter := &c;
99 // &self method accessible through &T
100 assert(p.get() == 2);
101 assert(p.is_positive() == true);
102}
103
104// ── Calling through a &var T reference (auto-deref, &var self methods) ─────────
105
106fun test_mut_ref_through_mut_pointer() {
107 var c := Counter::new(5);
108 let p: &var Counter := &var c;
109 p.tick(); // 5
110 p.tick(); // 10
111 assert(c.get() == 10);
112 p.add(3);
113 assert(c.get() == 13);
114}
115
116// ── Reference and direct call see the same storage ─────────────────────────────
117
118fun test_pointer_and_direct_share_storage() {
119 var c := Counter::new(1);
120 let p: &var Counter := &var c;
121 p.tick(); // p increments through the pointer
122 c.tick(); // direct call increments same counter
123 assert(c.get() == 2);
124 assert(p.get() == 2);
125}
126
127// ── Builder pattern: chained value receivers ─────────────────────────────────
128
129struct Builder {
130 x: i64,
131 y: i64,
132 label: String,
133}
134
135extend Builder {
136 fun new() -> Builder {
137 Builder { x = 0, y = 0, label = "" }
138 }
139
140 fun set_x(self, x: i64) -> Builder {
141 Builder { x = x, y = self.y, label = self.label }
142 }
143
144 fun set_y(self, y: i64) -> Builder {
145 Builder { x = self.x, y = y, label = self.label }
146 }
147
148 fun sum(&self) -> i64 {
149 self.x + self.y
150 }
151}
152
153fun test_builder_pattern() {
154 let b := Builder::new().set_x(3).set_y(7);
155 assert(b.sum() == 10);
156 assert(b.x == 3);
157 assert(b.y == 7);
158}
159
160// ── Multiple pointers to independent counters ────────────────────────────────
161
162fun test_independent_pointers() {
163 var a := Counter::new(1);
164 var b := Counter::new(10);
165 let pa: &var Counter := &var a;
166 let pb: &var Counter := &var b;
167 pa.tick();
168 pb.tick();
169 pb.tick();
170 assert(a.get() == 1);
171 assert(b.get() == 20);
172}
173
174fun main() {
175 test_value_receiver();
176 test_ref_receiver_repeated();
177 test_mut_receiver_accumulates();
178 test_interleaved_receivers();
179 test_ref_through_pointer();
180 test_mut_ref_through_mut_pointer();
181 test_pointer_and_direct_share_storage();
182 test_builder_pattern();
183 test_independent_pointers();
184}
passes
1// Tests that &var self methods called on a nested struct field mutate the
2// original field in place (METEL-112).
3
4struct Counter {
5 value: i64,
6 step: i64,
7}
8
9extend Counter {
10 fun new(step: i64) -> Counter {
11 Counter { value = 0, step = step }
12 }
13
14 fun tick(&var self) {
15 self.value += self.step;
16 }
17
18 fun get(&self) -> i64 {
19 self.value
20 }
21
22 fun is_positive(&self) -> boolean {
23 self.value > 0
24 }
25
26 fun reset(&var self) {
27 self.value := 0;
28 }
29}
30
31struct Pair {
32 a: Counter,
33 b: Counter,
34}
35
36struct Nested {
37 outer: Pair,
38}
39
40// ── Basic nested field mutation ───────────────────────────────────────────────
41
42fun test_nested_field_mut() {
43 var pair := Pair { a = Counter::new(1), b = Counter::new(10) };
44 pair.a.tick();
45 pair.b.tick();
46 pair.b.tick();
47 assert(pair.a.get() == 1);
48 assert(pair.b.get() == 20);
49}
50
51// ── Multiple mutations accumulate ─────────────────────────────────────────────
52
53fun test_nested_mut_accumulates() {
54 var pair := Pair { a = Counter::new(3), b = Counter::new(0) };
55 pair.a.tick(); // 3
56 pair.a.tick(); // 6
57 pair.a.tick(); // 9
58 assert(pair.a.get() == 9);
59 pair.a.reset();
60 assert(pair.a.get() == 0);
61}
62
63// ── &self on nested field is unaffected ──────────────────────────────────────
64
65fun test_nested_ref_reads() {
66 var pair := Pair { a = Counter::new(5), b = Counter::new(0) };
67 pair.a.tick();
68 assert(pair.a.get() == 5);
69 assert(pair.a.is_positive() == true);
70 // repeated &self calls don't consume
71 assert(pair.a.get() == 5);
72}
73
74// ── Two independent nested fields ────────────────────────────────────────────
75
76fun test_two_independent_fields() {
77 var pair := Pair { a = Counter::new(1), b = Counter::new(2) };
78 pair.a.tick();
79 pair.b.tick();
80 pair.b.tick();
81 assert(pair.a.get() == 1);
82 assert(pair.b.get() == 4);
83}
84
85// ── Double nesting ───────────────────────────────────────────────────────────
86
87fun test_double_nested_field_mut() {
88 var n := Nested {
89 outer = Pair { a = Counter::new(7), b = Counter::new(0) }
90 };
91 n.outer.a.tick();
92 n.outer.a.tick();
93 assert(n.outer.a.get() == 14);
94}
95
96fun main() {
97 test_nested_field_mut();
98 test_nested_mut_accumulates();
99 test_nested_ref_reads();
100 test_two_independent_fields();
101 test_double_nested_field_mut();
102}
passes
Legality Rule №2
Methods may use self, &self, or &var self as their receiver.
Referenced by: rfc-0044
Tested by (2)
1struct Counter {
2 value: i64,
3}
4
5extend Counter {
6 fun increment(&var self) {
7 self.value += 1;
8 }
9
10 fun current(&self) -> i64 {
11 self.value
12 }
13}
14
15fun main() {
16 var counter := Counter { value = 0 };
17 counter.increment();
18 counter.increment();
19 assert(counter.current() == 2);
20}
passes
Dynamic Semantics №1
A value receiver receives the ordinary passed value, so a method that returns a changed value leaves the caller's original binding unchanged.
Referenced by: rfc-0044
Tested by
1// RFC-0044 §2: `self` (a plain value receiver) keeps value semantics -- the
2// method body observes the passed value and returns a new one; the original
3// binding is untouched.
4struct Counter {
5 value: i64,
6}
7
8extend Counter {
9 fun incremented(self) -> Counter {
10 Counter { value = self.value + 1 }
11 }
12}
13
14fun main() {
15 let original := Counter { value = 0 };
16 let next := original.incremented();
17 assert(original.value == 0);
18 assert(next.value == 1);
19}
passes
Legality Rule №3
An &self receiver reads the original receiver storage without consuming it.
Referenced by: rfc-0044
Tested by
1struct Counter {
2 value: i64,
3}
4
5extend Counter {
6 fun increment(&var self) {
7 self.value += 1;
8 }
9
10 fun current(&self) -> i64 {
11 self.value
12 }
13}
14
15fun main() {
16 var counter := Counter { value = 0 };
17 counter.increment();
18 counter.increment();
19 assert(counter.current() == 2);
20}
passes
Dynamic Semantics №2
An &var self receiver mutates the original receiver storage in place without consuming
the receiver.
Tested by (2)
1struct Counter {
2 value: i64,
3}
4
5extend Counter {
6 fun increment(&var self) {
7 self.value += 1;
8 }
9
10 fun current(&self) -> i64 {
11 self.value
12 }
13}
14
15fun main() {
16 var counter := Counter { value = 0 };
17 counter.increment();
18 counter.increment();
19 assert(counter.current() == 2);
20}
passes
1// Tests that &var self methods called on a nested struct field mutate the
2// original field in place (METEL-112).
3
4struct Counter {
5 value: i64,
6 step: i64,
7}
8
9extend Counter {
10 fun new(step: i64) -> Counter {
11 Counter { value = 0, step = step }
12 }
13
14 fun tick(&var self) {
15 self.value += self.step;
16 }
17
18 fun get(&self) -> i64 {
19 self.value
20 }
21
22 fun is_positive(&self) -> boolean {
23 self.value > 0
24 }
25
26 fun reset(&var self) {
27 self.value := 0;
28 }
29}
30
31struct Pair {
32 a: Counter,
33 b: Counter,
34}
35
36struct Nested {
37 outer: Pair,
38}
39
40// ── Basic nested field mutation ───────────────────────────────────────────────
41
42fun test_nested_field_mut() {
43 var pair := Pair { a = Counter::new(1), b = Counter::new(10) };
44 pair.a.tick();
45 pair.b.tick();
46 pair.b.tick();
47 assert(pair.a.get() == 1);
48 assert(pair.b.get() == 20);
49}
50
51// ── Multiple mutations accumulate ─────────────────────────────────────────────
52
53fun test_nested_mut_accumulates() {
54 var pair := Pair { a = Counter::new(3), b = Counter::new(0) };
55 pair.a.tick(); // 3
56 pair.a.tick(); // 6
57 pair.a.tick(); // 9
58 assert(pair.a.get() == 9);
59 pair.a.reset();
60 assert(pair.a.get() == 0);
61}
62
63// ── &self on nested field is unaffected ──────────────────────────────────────
64
65fun test_nested_ref_reads() {
66 var pair := Pair { a = Counter::new(5), b = Counter::new(0) };
67 pair.a.tick();
68 assert(pair.a.get() == 5);
69 assert(pair.a.is_positive() == true);
70 // repeated &self calls don't consume
71 assert(pair.a.get() == 5);
72}
73
74// ── Two independent nested fields ────────────────────────────────────────────
75
76fun test_two_independent_fields() {
77 var pair := Pair { a = Counter::new(1), b = Counter::new(2) };
78 pair.a.tick();
79 pair.b.tick();
80 pair.b.tick();
81 assert(pair.a.get() == 1);
82 assert(pair.b.get() == 4);
83}
84
85// ── Double nesting ───────────────────────────────────────────────────────────
86
87fun test_double_nested_field_mut() {
88 var n := Nested {
89 outer = Pair { a = Counter::new(7), b = Counter::new(0) }
90 };
91 n.outer.a.tick();
92 n.outer.a.tick();
93 assert(n.outer.a.get() == 14);
94}
95
96fun main() {
97 test_nested_field_mut();
98 test_nested_mut_accumulates();
99 test_nested_ref_reads();
100 test_two_independent_fields();
101 test_double_nested_field_mut();
102}
passes
Legality Rule №4
Dot-call syntax selects the receiver behavior declared in the method signature; callers do not supply a distinct receiver-mode syntax.
Referenced by: rfc-0044
Tested by (2)
1struct Counter {
2 value: i64,
3}
4
5extend Counter {
6 fun increment(&var self) {
7 self.value += 1;
8 }
9
10 fun current(&self) -> i64 {
11 self.value
12 }
13}
14
15fun main() {
16 var counter := Counter { value = 0 };
17 counter.increment();
18 counter.increment();
19 assert(counter.current() == 2);
20}
passes
1// Tests that &var self methods called on a nested struct field mutate the
2// original field in place (METEL-112).
3
4struct Counter {
5 value: i64,
6 step: i64,
7}
8
9extend Counter {
10 fun new(step: i64) -> Counter {
11 Counter { value = 0, step = step }
12 }
13
14 fun tick(&var self) {
15 self.value += self.step;
16 }
17
18 fun get(&self) -> i64 {
19 self.value
20 }
21
22 fun is_positive(&self) -> boolean {
23 self.value > 0
24 }
25
26 fun reset(&var self) {
27 self.value := 0;
28 }
29}
30
31struct Pair {
32 a: Counter,
33 b: Counter,
34}
35
36struct Nested {
37 outer: Pair,
38}
39
40// ── Basic nested field mutation ───────────────────────────────────────────────
41
42fun test_nested_field_mut() {
43 var pair := Pair { a = Counter::new(1), b = Counter::new(10) };
44 pair.a.tick();
45 pair.b.tick();
46 pair.b.tick();
47 assert(pair.a.get() == 1);
48 assert(pair.b.get() == 20);
49}
50
51// ── Multiple mutations accumulate ─────────────────────────────────────────────
52
53fun test_nested_mut_accumulates() {
54 var pair := Pair { a = Counter::new(3), b = Counter::new(0) };
55 pair.a.tick(); // 3
56 pair.a.tick(); // 6
57 pair.a.tick(); // 9
58 assert(pair.a.get() == 9);
59 pair.a.reset();
60 assert(pair.a.get() == 0);
61}
62
63// ── &self on nested field is unaffected ──────────────────────────────────────
64
65fun test_nested_ref_reads() {
66 var pair := Pair { a = Counter::new(5), b = Counter::new(0) };
67 pair.a.tick();
68 assert(pair.a.get() == 5);
69 assert(pair.a.is_positive() == true);
70 // repeated &self calls don't consume
71 assert(pair.a.get() == 5);
72}
73
74// ── Two independent nested fields ────────────────────────────────────────────
75
76fun test_two_independent_fields() {
77 var pair := Pair { a = Counter::new(1), b = Counter::new(2) };
78 pair.a.tick();
79 pair.b.tick();
80 pair.b.tick();
81 assert(pair.a.get() == 1);
82 assert(pair.b.get() == 4);
83}
84
85// ── Double nesting ───────────────────────────────────────────────────────────
86
87fun test_double_nested_field_mut() {
88 var n := Nested {
89 outer = Pair { a = Counter::new(7), b = Counter::new(0) }
90 };
91 n.outer.a.tick();
92 n.outer.a.tick();
93 assert(n.outer.a.get() == 14);
94}
95
96fun main() {
97 test_nested_field_mut();
98 test_nested_mut_accumulates();
99 test_nested_ref_reads();
100 test_two_independent_fields();
101 test_double_nested_field_mut();
102}
passes
Legality Rule №5
An Iterable<T> implementation declares next with an &var self receiver so repeated
calls can advance the same iterator value.
Referenced by: rfc-0044
Tested by
1// User-defined Iterable via aspect — for-in dispatches through next().
2
3aspect Iterable<T> {
4 fun next(&var self) -> Perhaps<T>;
5}
6
7struct Counter {
8 current: i64,
9 limit: i64,
10}
11
12extend Counter {
13 fun new(limit: i64) -> Counter {
14 return Counter { current = 0, limit = limit };
15 }
16}
17
18extend Counter: Iterable<i64> {
19 fun next(&var self) -> Perhaps<i64> {
20 if (self.current < self.limit) {
21 let val := self.current;
22 self.current := self.current + 1;
23 return Perhaps::Some { value = val };
24 }
25 return None;
26 }
27}
28
29fun main() {
30 var sum := 0;
31 let c := Counter::new(5);
32 for (x in c) {
33 sum += x;
34 }
35 assert(sum == 10); // 0+1+2+3+4
36}
passes
Legality Rule №6
An &var self method may be called through an &var T reference.
Referenced by: rfc-0044
Tested by
1aspect Bump {
2 fun bump(&var self);
3}
4
5struct C {
6 v: i64,
7}
8
9extend C: Bump {
10 fun bump(&var self) {
11 self.v := self.v + 1;
12 }
13}
14
15fun go<T: Bump>(x: &var T) {
16 x.bump();
17}
18
19fun main() {
20 var c := C { v = 0 };
21 go(&var c);
22 assert(c.v == 1);
23}
passes
Legality Rule №7
An aspect method may declare an &var self receiver, including Iterable<T>::next.
Referenced by: rfc-0044
Tested by
1// User-defined Iterable via aspect — for-in dispatches through next().
2
3aspect Iterable<T> {
4 fun next(&var self) -> Perhaps<T>;
5}
6
7struct Counter {
8 current: i64,
9 limit: i64,
10}
11
12extend Counter {
13 fun new(limit: i64) -> Counter {
14 return Counter { current = 0, limit = limit };
15 }
16}
17
18extend Counter: Iterable<i64> {
19 fun next(&var self) -> Perhaps<i64> {
20 if (self.current < self.limit) {
21 let val := self.current;
22 self.current := self.current + 1;
23 return Perhaps::Some { value = val };
24 }
25 return None;
26 }
27}
28
29fun main() {
30 var sum := 0;
31 let c := Counter::new(5);
32 for (x in c) {
33 sum += x;
34 }
35 assert(sum == 10); // 0+1+2+3+4
36}
passes
Generic Structs
struct Pair<A, B> {
first: A,
second: B,
}
fun main() -> i64 {
let p := Pair { first = 1, second = true };
return p.first;
}
Enums
enum Direction { North, South, East, West }
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
fun main() -> i64 {
let dir := Direction::North;
let s := Shape::Circle { radius = 5.0 };
let area := match (s) {
Circle { radius } => radius * radius * 3.14159,
Rectangle { width, height } => width * height,
};
match (dir) {
North => area as i64,
South => 0,
East => 0,
West => 0,
}
}
Variants may be unit (no data) or struct-like (named fields). A struct-like variant's
named fields follow the same public/private visibility rules as an ordinary
struct'sVisibility L6.
When a struct-like variant's field set is empty, both constructor spellings are acceptedL1:
enum Flag {
On {},
}
let x := Flag::On;
let y := Flag::On {};
Formal rules
Legality Rule №1
A zero-field enum variant may be constructed either as its qualified path or with empty braces.
Referenced by: rfc-0106
Tested by
1struct Empty {}
2
3enum Flag {
4 On {},
5}
6
7fun use_empty(x: Empty) -> i64 {
8 1
9}
10
11fun use_flag(f: Flag) -> i64 {
12 match (f) {
13 Flag::On => 7,
14 }
15}
16
17fun main() {
18 let a := Empty;
19 let b := Empty {};
20 let x := Flag::On;
21 let y := Flag::On {};
22
23 assert(use_empty(a) == 1);
24 assert(use_empty(b) == 1);
25 assert(use_flag(x) == 7);
26 assert(use_flag(y) == 7);
27}
passes
Dynamic Semantics №1
For a zero-field enum variant, the bare and empty-brace constructor forms evaluate to the same variant value.
Referenced by: rfc-0106
Tested by
1struct Empty {}
2
3enum Flag {
4 On {},
5}
6
7fun use_empty(x: Empty) -> i64 {
8 1
9}
10
11fun use_flag(f: Flag) -> i64 {
12 match (f) {
13 Flag::On => 7,
14 }
15}
16
17fun main() {
18 let a := Empty;
19 let b := Empty {};
20 let x := Flag::On;
21 let y := Flag::On {};
22
23 assert(use_empty(a) == 1);
24 assert(use_empty(b) == 1);
25 assert(use_flag(x) == 7);
26 assert(use_flag(y) == 7);
27}
passes
Instantiation
enum Direction { North, South, East, West }
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
fun main() -> i64 {
let dir := Direction::North;
let s := Shape::Circle { radius = 5.0 };
let area := match (s) {
Circle { radius } => radius * radius * 3.14159,
Rectangle { width, height } => width * height,
};
match (dir) {
North => area as i64,
South => 0,
East => 0,
West => 0,
}
}
Formal rules
Methods on Enums
extend blocks on enums follow the same syntax as structs:
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
extend Shape {
fun area(self) -> f64 {
match (self) {
Circle { radius } => 3.14159 * radius * radius,
Rectangle { width, height } => width * height,
}
}
}
fun main() -> i64 {
let s := Shape::Circle { radius = 5.0 };
return s.area() as i64;
}
Type Aliases
A type alias gives an existing type a name:
type Bytes := List<u8>;
type Handler := once var |Request, &Config| -> Response;
type Pair<A, B> := (A, B);
An alias is transparent — erased to its right-hand side before type checking, with no
nominal identity of its own. Pair<i64, boolean> and (i64, boolean) are the same type,
accepted in the same positions, satisfying the same bounds. An alias defines no impl, so
there is no coherence concern. It may be parameterised and may reference another alias;
type inside an aspect / extend block remains an associated-type
definitionL1 — position, not a
keyword, tells the two apart.
Because it is transparent, an alias is usable everywhere its expansion is: across module
boundariesL3 when public, in value and
pattern positionL4 when it names a plain type,
and lexically scopedL5 when declared inside a
body.
Formal rules
Legality Rule №1
A type alias is written public? type Name generic_params? := Type; at module scope or
in a function / block body. It introduces Name as a transparent synonym for Type:
every use of Name (with type arguments substituted for its generic parameters) is
replaced by Type before name resolution and type checking, and must supply exactly the
alias's declared number of type arguments.
Referenced by: rfc-0160
Tested by (3)
1// RFC-0160: a transparent type alias names an existing type. It is erased to its
2// right-hand side before typechecking, so an aliased name and its expansion are
3// interchangeable. Covers a plain alias, a parameterised alias, an alias of an
4// alias, and a function-type alias carrying its qualifiers.
5type Count := i64;
6type Predicate<T> := |T| -> boolean;
7type IntPred := Predicate<i64>;
8
9fun keep(p: IntPred, x: Count) -> boolean { p(x) }
10
11// The raw expansion is accepted wherever the alias is, and vice versa.
12fun keep_raw(p: |i64| -> boolean, x: i64) -> boolean { p(x) }
13
14fun main() {
15 // A written function type (`IntPred` expands to one) is move-only (RFC-0166),
16 // so each closure value is used by value once: the alias and its expansion
17 // are still shown interchangeable across `keep` / `keep_raw` / the `let`s.
18 assert(keep(|n: i64| { n % 2 == 1 }, 7));
19 assert(!keep(|n: i64| { n % 2 == 1 }, 8));
20 assert(keep_raw(|n: i64| { n % 2 == 1 }, 3));
21 let odd: IntPred := |n: i64| { n % 2 == 1 };
22 let also: Predicate<i64> := odd;
23 assert(also(5));
24 println("ok");
25}
passes
1// RFC-0160: an alias works in every type position — struct fields, parameters,
2// return types — and an alias of a tuple/record composes.
3type Pair<A, B> := (A, B);
4type Row := Pair<i64, boolean>;
5
6struct Holder {
7 slot: Row,
8}
9
10fun swap_first(h: Holder, v: i64) -> Row {
11 (v, h.slot.1)
12}
13
14fun main() {
15 let h := Holder { slot = (1, true) };
16 let r: Row := swap_first(h, 9);
17 assert(r.0 == 9);
18 assert(r.1);
19 println("ok");
20}
passes
1// RFC-0160 §3: an alias use must supply exactly the alias's declared number of
2// type arguments.
3type Pair<A, B> := (A, B);
4fun main() -> Pair<i64> { (1, 2) }
typecheck errorT0004“type argument”
Legality Rule №2
A type alias may not be recursive — neither directly nor through a chain of aliases. A
transparent alias has no finite expansion for a cycle; a genuinely recursive shape uses a
struct or enum indirection point.
Tested by
1// RFC-0160 OQ4: a transparent alias may not be recursive — direct or through a
2// chain. There is no finite expansion.
3type Json := Wrap<Json>;
4type Wrap<T> := T;
5fun main() { }
typecheck errorT0003“recursive type alias”
Legality Rule №3
A public module-level alias is part of its module's public surface exactly like a
struct / enum / fun declaration: another module brings it into scope by a named
import (import m::{A};), a renamed import (import m::A as B;), a glob
(import m::*;), one export re-export hop (export m::{A};), or a qualified path
(m::A) — every spelling denotes the identical erased type. Naming a non-public alias
from outside its declaring module, directly or through an export, is a visibility
error.
Tested by (4)
1// RFC-0160: a `public type` alias crosses module boundaries — imported by name,
2// under a local rename, and referenced with a qualified path. All three spell
3// the same erased type, interchangeable with its expansion.
4import geometry::{Vec2, Transform};
5import geometry::Vec2 as Point;
6
7fun apply(t: Transform, p: Vec2) -> Vec2 {
8 t(p)
9}
10
11fun main() {
12 let shift: Transform := |v: Vec2| { (v.0 + 1.0, v.1 + 1.0) };
13 let origin: Point := (0.0, 0.0);
14 let moved: geometry::Vec2 := apply(shift, origin);
15 assert(moved.0 == 1.0);
16 assert(moved.1 == 1.0);
17 println("ok");
18}
passes
1// RFC-0160 (metel-core#940): an alias that reaches this module through an
2// `export` re-export (one hop) resolves the same as a direct import — by name,
3// under a rename, and everywhere the erased type is written.
4import prelude::{Vec2, Transform};
5import prelude::Vec2 as Point;
6
7fun apply(t: Transform, p: Vec2) -> Vec2 {
8 t(p)
9}
10
11fun main() {
12 let shift: Transform := |v: Vec2| { (v.0 + 1.0, v.1 + 1.0) };
13 let origin: Point := (0.0, 0.0);
14 let moved: Vec2 := apply(shift, origin);
15 assert(moved.0 == 1.0);
16 assert(moved.1 == 1.0);
17 println("ok");
18}
passes
1// RFC-0160: referencing another module's non-`public` alias is a visibility
2// error, the same as any other private item.
3import secrets::Secret;
4
5fun main() {
6 let x: Secret := 1;
7 assert(x == 1);
8}
typecheck errorT0009“private to module”
Legality Rule №4
When an alias's expansion is a plain named type, the alias name may be written wherever
that type's own name is legal in an expression or pattern: a struct literal (P { … }),
a record projection (P.{ … }), an enum-variant path (D::Variant), or a match
pattern. An alias whose expansion is not a plain named type — a tuple, a function type, a
reference — or is still parameterised has no meaning in value position.
Tested by
1// RFC-0160 (metel-core#941): an alias for a plain named type stands in wherever
2// the real type name would in value / pattern position — a struct literal, a
3// record projection, an enum-variant path, and a match pattern.
4struct Point { x: i64, y: i64 }
5enum Dir { North, South }
6
7type P := Point;
8type D := Dir;
9
10fun origin() -> P {
11 P { x = 0, y = 0 }
12}
13
14fun main() {
15 let p := P { x = 3, y = 4 };
16 assert(p.x == 3);
17
18 let just_x := p.{ x };
19 assert(just_x.x == 3);
20
21 let d := D::South;
22 let code := match (d) {
23 D::North => 1,
24 D::South => 2,
25 };
26 assert(code == 2);
27
28 match (p) {
29 P { x, .. } => {
30 assert(x == 3);
31 }
32 }
33
34 assert(origin().y == 0);
35 println("ok");
36}
passes
Legality Rule №5
An alias declared inside a function or block body is visible throughout that body regardless of textual position, is never exported, and may name the enclosing function's generic parameters. It shadows an alias of the same name from an enclosing scope for the remainder of its block; the outer alias is unaffected outside it.
Tested by
1// RFC-0160: a `type` alias may be declared inside a function or block body, not
2// only at module scope. A block-local alias is lexically scoped — it shadows an
3// outer alias of the same name for the rest of that block and no further — and
4// it is visible in every nested type position, including a closure parameter
5// annotation.
6type Unit := i64;
7
8// The block-local `Unit` shadows the module alias within this body only.
9fun shadow_body() -> Unit {
10 type Unit := boolean;
11 let flag: Unit := true;
12 if (flag) { 1 } else { 0 }
13}
14
15// The shadow did not leak: `Unit` is the module alias (i64) again here.
16fun no_leak() -> Unit {
17 let n: Unit := 5;
18 n
19}
20
21// A block-local alias used to name a closure type, in a parameter annotation.
22fun apply_twice() -> i64 {
23 type Op := |i64| -> i64;
24 let double: Op := |x: i64| { x * 2 };
25 let apply := |f: Op, v: i64| { f(v) };
26 apply(double, 21)
27}
28
29fun main() {
30 assert(shadow_body() == 1);
31 assert(no_leak() == 5);
32 assert(apply_twice() == 42);
33 println("ok");
34}
passes
Aspects
aspect Printable {
fun print(self);
}
aspect Comparable {
fun compare(self, other: Self) -> i64;
}
fun main() -> i64 {
return 0;
}
Formal rules
Legality Rule №1
An aspect declaration is introduced with the aspect keyword. Its braced body declares
the methods and associated types that implementing types must provide.
Referenced by: rfc-0020
Tested by
1// 06 — Traits
2// Covers: aspectdefinition, impl Trait for Type, default methods, aspectbounds
3// in generic functions, Self type.
4
5aspectDescribable {
6 fun describe(self) -> String;
7}
8
9aspectComparable {
10 fun compare(self, other: Self) -> i64; // <0 less, 0 equal, >0 greater
11
12 // default method built on top of compare
13 fun is_less_than(self, other: Self) -> boolean {
14 return self.compare(other) < 0;
15 }
16
17 fun is_equal_to(self, other: Self) -> boolean {
18 return self.compare(other) == 0;
19 }
20}
21
22// --- a simple value type ---
23
24struct Temperature {
25 celsius: f64,
26}
27
28extend Temperature {
29 fun new(c: f64) -> Temperature {
30 return Temperature { celsius = c };
31 }
32
33 fun to_fahrenheit(self) -> f64 {
34 return self.celsius * 1.8 + 32.0;
35 }
36}
37
38extend Temperature: Describable {
39 fun describe(self) -> String {
40 return float_to_string(self.celsius) + "°C";
41 }
42}
43
44extend Temperature: Comparable {
45 fun compare(self, other: Temperature) -> i64 {
46 if (self.celsius < other.celsius) { return -1; }
47 if (self.celsius > other.celsius) { return 1; }
48 return 0;
49 }
50}
51
52// --- another type implementing the same traits ---
53
54struct Score {
55 points: i64,
56 label: String,
57}
58
59extend Score {
60 fun new(points: i64, label: String) -> Score {
61 return Score { points = points, label = label };
62 }
63}
64
65extend Score: Describable {
66 fun describe(self) -> String {
67 return self.label + ": " + int_to_string(self.points);
68 }
69}
70
71extend Score: Comparable {
72 fun compare(self, other: Score) -> i64 {
73 if (self.points < other.points) { return -1; }
74 if (self.points > other.points) { return 1; }
75 return 0;
76 }
77}
78
79// --- generic functions with aspectbounds ---
80
81fun print_description<T: Describable>(item: T) {
82 println(item.describe());
83}
84
85fun max_of<T: Comparable>(a: T, b: T) -> T {
86 if (a.compare(b) >= 0) {
87 return a;
88 }
89 return b;
90}
91
92fun main() {
93 let freezing := Temperature::new(0.0);
94 let boiling := Temperature::new(100.0);
95 let body := Temperature::new(37.0);
96
97 // --- aspectmethod ---
98 print_description(freezing); // 0.0°C
99 print_description(boiling); // 100.0°C
100
101 // --- default aspectmethods ---
102 println(bool_to_string(freezing.is_less_than(boiling))); // true
103 println(bool_to_string(boiling.is_less_than(freezing))); // false
104 println(bool_to_string(freezing.is_equal_to(freezing))); // true
105
106 // --- generic max ---
107 let hotter := max_of(freezing, boiling);
108 println(hotter.describe()); // 100.0°C
109
110 // --- same generic function with a different type ---
111 let s1 := Score::new(42, "Alice");
112 let s2 := Score::new(99, "Bob");
113 let s3 := Score::new(42, "Carol");
114
115 print_description(s1); // Alice: 42
116 print_description(s2); // Bob: 99
117
118 let winner := max_of(s1, s2);
119 println(winner.describe()); // Bob: 99
120
121 // --- default methods on Score ---
122 println(bool_to_string(s1.is_equal_to(s3))); // true (same points)
123 println(bool_to_string(s1.is_less_than(s2))); // true
124}
passes
Bodyless Aspect Declarations
aspect Copy2;
An aspect declaration may end with ; instead of a braced body when the body would be
empty already: zero methods and zero associated typesL1. This is pure sugar for
aspect Copy2 { }.
The shorter spelling does not promise that the aspect stays empty forever. If a later revision adds a method or associated type, the declaration simply switches back to the braced form.
Formal rules
Legality Rule №1
An aspect declaration with ; in place of a braced body is exactly equivalent to { } —
an aspect with zero methods and zero associated types. The bodyless production has no
syntax to carry a method or associated type, so this is pure notational sugar, not a
conditional exemption to check against a body that could otherwise be non-empty.
Referenced by: rfc-0103
Tested by
Implementing an Aspect
struct Point {
x: f64,
y: f64,
}
aspect Printable {
fun print(self);
}
extend Point: Printable {
fun print(self) {
print("(");
print(self.x.to_string());
print(", ");
print(self.y.to_string());
println(")");
}
}
fun main() {
let p := Point { x = 1.0, y = 2.0 };
p.print();
}
Aspect implementation method set. An extend Type: Aspect block must define
exactly the methods declared by Aspect: every declared method must be present unless it
has a default body, and no additional methods are permitted. Put a type-specific method
that is not part of the aspect in an inherent extend Type { ... } block; inherent and
aspect implementations may coexist for the same typeL1.
Aspect implementation method signatures. Each implementation method must
conform to the aspect's declaration of that method. The aspect signature is first
specializedL12 with
the block's target type for Self, its aspect arguments, and its associated-type
definitions; receiver form, ordinary parameter count and types, and result type
must then be equal. The method's generic constraints must be
structurally equalL13
to the aspect method's after normalization — neither weakened nor strengthened —
with record kind part of the comparison. A method that does not conform is
rejected at its own declarationL14
and does not satisfy the aspect.
aspect CopyOnly { fun pass<T: Copy>(value: T) -> T; }
extend Holder: CopyOnly {
fun pass<T: Copy>(value: T) -> T { value } // ok -- identical constraints
// fun pass<T>(value: T) -> T { value } // rejected: weakened (T0012)
}
aspect AnyValue { fun keep<T>(value: T) -> T; }
extend Holder: AnyValue {
fun keep<record T>(value: T) -> T { value } // rejected: strengthened (T0012)
}
Letting an implementation weaken a constraint — admissible-domain inclusion, e.g. accepting
<record T>in the aspect against a plain<T>implementation — is a later addition, RFC-0149. Until it lands, a widening is rejected here as a conservative wrong-no.
Conditional extend blocks. An aspect implementation for a
generic type may be conditional on its own type parameters satisfying additional
bounds, written in a where clause after the aspect clause (or inline, before the
target type):
struct Pair<A, B> { first: A, second: B }
extend Pair<A, B>: Printable where A: Printable, B: Printable {
fun print(self) { ... }
}
// equivalent, inline form:
extend<A: Printable, B: Printable> Pair<A, B>: Printable { ... }
Pair<i64, String> is Printable; Pair<i64, SomeNonPrintableType> is not — both
remain constructable, since a struct's own unconditional bounds (above) and an extend
block's conditional bounds are checked independently. The compiler checks a conditional
block's bounds at every point the aspect is required — method call, bound check, impl
selection — not at the block's own declaration site:
fun print_pair<A: Printable, B: Printable>(p: Pair<A, B>) {
p.print(); // ok -- conditional impl applies; A: Printable and B: Printable
}
fun use_pair(p: Pair<i64, SomeNonPrintable>) {
p.print(); // error T0012: Pair<i64, SomeNonPrintable> does not implement
// Printable, because SomeNonPrintable does not implement Printable
}
A generic function propagates a conditional extend block to its own callers by stating the bound explicitly — the compiler never infers which bounds a caller needs:
fun print_sorted<T: Comparable + Printable>(list: SortedList<T>) {
list.print(); // ok -- T: Printable, so the conditional extend block applies
}
Negative bounds may appear in a conditional extend block's where clause on the same
terms as positive ones:
extend<T: !Drop> Container<T>: BulkDrop { ... }
Coherence. Two conditional extend blocks of the same aspect for the same type are
a coherence error (T0015) unless they are provably disjoint. Disjointness is
established by syntactic negation only — one block must carry an explicit negative
bound that directly negates a positive bound in the other. The compiler performs no
inference beyond this direct check:
// Accepted -- T: !Copy directly negates T: Copy; provably disjoint
extend<T: Copy> Wrapper<T>: Serialize { ... }
extend<T: !Copy> Wrapper<T>: Serialize { ... }
// error T0015 -- no direct negation between Clone and Display; not provably disjoint
extend<T: Clone> Wrapper<T>: Serialize { ... }
extend<T: Display> Wrapper<T>: Serialize { ... }
A conditional extend block and an unconditional extend block for the same type
constructor are also a coherence error — the unconditional block already covers every
instantiation the conditional one would. Conditional blocks are subject to the same
orphan rule as unconditional ones (above): the aspect or the type's outermost
constructor must be local.
Bare-parameter blanket impls. extend<T: Bound> T: Aspect — where the target is
the block's own generic parameter rather than a named struct or enum wrapping it, e.g.
extend<T: Copy> T: Clone — is a distinct case from every other example in this
section, which all target a genuine named type (Pair<A, B>, Container<T>,
Wrapper<T>). A bare type parameter has no outermost type constructor for the orphan
rule (below) to check — it isn't declared in any module, including the block's own.
Target-locality is therefore vacuously unsatisfiable for this shape: such an extend
is permitted only through the aspect side of the orphan rule, never the target side.
// std::core — permitted: Clone is local to std::core
extend<T: Copy> T: Clone { fun clone(self: &T) -> T { self } }
// user module — permitted: MyAspect is local here
aspect MyAspect { fun tag(self) -> String; }
extend<T: Copy> T: MyAspect { fun tag(self) -> String { "copyable" } }
// user module — REJECTED (T0014): Display is foreign, and a bare-parameter
// target can never be local, anywhere
extend<T: Copy> T: Display { fun to_string(self) -> String { "?" } }
This confines any one aspect's bare-parameter blanket impl to a single module (its own
declaring module, or std::core for a built-in aspect) — no separate overlap-detection
mechanism is needed beyond the ordinary rule already stated above (two impls of the
same aspect conflict when some instantiation satisfies both): a competing
bare-parameter blanket from another module can never pass the orphan check in the
first place, and a concrete impl overlapping the blanket (e.g. a type implementing
Clone directly while also being Copy) is caught by the existing concrete-vs-blanket
overlap rule with no special case. See
public/rfcs/4-implemented/rfc-0097-orphan-rule-for-bare-parameter-blanket-impls.md.
Worked example — interaction with equality-constrained bounds. A conditional
extend block's where clause accepts the same equality-constrained bound form
Associated Types (above) specifies for ordinary function bounds, since both are stored
and checked as the same Bound structure:
aspect Container { type Item: Display; fun get(self) -> Item; }
struct Wrapper<T> { inner: T }
extend<T: Container<Item = i64>> Wrapper<T>: Printable {
fun print(self) { println(self.inner.get().to_string()); }
}
This composes without any new mechanism: the conditional block's bound-checking (this
section) and the equality-constraint-checking Associated Types already specifies are
the same call-site check, run once per bound in the where clause, regardless of
which kind of aspect the bound names.
Formal rules
Legality Rule №1
An inherent implementation is written extend Type { ... }; an aspect implementation is
written extend Type: Aspect { ... }, and both forms may coexist for the same type.
Referenced by: rfc-0098
Tested by (2)
1struct Point { x: i64 }
2
3aspect Describe {
4 fun describe(&self) -> String;
5}
6
7extend Point: Describe {
8 fun describe(&self) -> String { return "point"; }
9}
10
11extend Point {
12 fun twice(&self) -> i64 { return self.x * 2; }
13}
14
15fun main() {
16 let p := Point { x = 21 };
17 assert(p.describe() == "point");
18 assert(p.twice() == 42);
19}
passes
1// RFC-0060 §5: a negative impl beats a blanket positive impl for the same
2// concrete type -- permitted (no T0015), and the negative impl wins for bound
3// satisfaction (Foo<i64>: !Marker holds).
4
5aspect Marker {
6 fun mark(self) -> String;
7}
8
9struct Foo<T> {
10 value: T,
11}
12
13extend<T> Foo<T>: Marker {
14 fun mark(self) -> String { return "blanket"; }
15}
16
17extend Foo<i64>: !Marker;
18
19fun needs_not_marker<U: !Marker>(x: U) {}
20
21fun main() {
22 let f := Foo { value = 5 };
23 needs_not_marker(f);
24}
passes
dyn Aspect
dyn Aspect objects, their coercions, and heterogeneous List<dyn Aspect> collections are supporteddyn Aspect is an aspect object: a value whose concrete type is erased, with
dispatch happening through a vtable at runtime. It complements extends Aspect
(compile-time-fixed, zero-overhead) with the opposite trade-off — the concrete
type may vary at runtime, at the cost of an indirect call and a pointer's
worth of space:
fun show(x: &dyn Display) -> i64 { 0 }
fun holds_two(x: &var dyn Display) -> i64 { 0 }
fun many(x: dyn Display[]) -> i64 { 0 }
fun main() -> i64 { 0 }
Unlike extends Aspect — sugar for a fresh generic parameter, legal only in
parameter or return position — dyn Aspect is a real, existential type. It
may appear anywhere an ordinary type can: a let binding's annotation, a
struct field, a return type, behind &/&var, or as an array element.
Not every aspect can be used this way. An aspect is object-safe only if
every one of its methods can be dispatched through a vtable — see §Object safety below. A non-object-safe aspect is still fully usable
with extends Aspect (static dispatch); it just cannot appear in dyn position.
Formal rules
Legality Rule №1
dyn Aspect names a real, visible aspect — the same resolution extends Aspect
already uses — and is legal in any type position, with no restriction to
parameter or return position. An aspect object cannot be an extend target:
there is no one concrete type to register an impl against.
Referenced by: rfc-0008
Tested by (4)
1// Regression (metel-core#865, RFC-0008 slice 1): `dyn Aspect` parses and
2// typechecks in every position an ordinary type can appear in -- unlike
3// `impl Aspect`, it is a real existential type, not positionally restricted to
4// parameter/return position (RFC-0008 §1).
5
6struct Handle { fd: i64 }
7extend Handle: Display {
8 fun to_string(&self) -> String { "handle" }
9}
10
11fun by_value(x: dyn Display) -> i64 { 0 }
12fun by_ref(x: &dyn Display) -> i64 { 0 }
13fun by_mut_ref(x: &var dyn Display) -> i64 { 0 }
14fun as_array(x: dyn Display[]) -> i64 { 0 }
15// Return position: no coercion needed here (that's slice 2's job) -- the
16// parameter is already `&dyn Display`, so this just unifies identically-typed
17// positions against each other.
18fun passthrough(x: &dyn Display) -> &dyn Display { x }
19
20fun main() {}
passes
1// Regression (metel-core#865, RFC-0008 slice 1): `dyn Aspect<Args>` -- a
2// generic aspect's own type arguments lower correctly (RFC-0008 §1's
3// `dyn Callable<i64, i64>` example, using a user-defined aspect since
4// `Callable` itself isn't a real stdlib aspect yet).
5
6aspect Converter<A, B> {
7 fun convert(&self, x: A) -> B;
8}
9
10fun a(x: dyn Converter<i64, String>) -> i64 { 0 }
11
12fun main() {}
passes
1// Regression (metel-core#865, RFC-0008 slice 1): `dyn` reuses the same
2// aspect-name resolution `impl Aspect` already uses (`aspect_type`) -- an
3// unresolvable name is rejected the same way, not silently accepted.
4
5fun a(x: dyn NotReal) -> i64 { 0 }
6
7fun main() {}
typecheck errorT0003“unknown aspect”
1// Regression (metel-core#865, RFC-0008 slice 1): `dyn Aspect` cannot be an
2// `extend` target -- it's existential, there is no one concrete type to
3// register an impl against (the same reason `impl Aspect` can't be an impl
4// target either). Joins the existing tuple/record/fun/array-target rejections
5// this pass already covers.
6
7aspect Local {
8 fun f(&self) -> i64;
9}
10
11extend dyn Local: Local { // ERROR[T0001]
12}
13
14fun main() {}
typecheck errorT0001“cannot `extend` a `dyn Aspect` type”
Object safety
An aspect is object-safe only if every one of its methods satisfies three rules:
- Receiver rule. The method's first parameter must be
self: &Selforself: &var Self. A bare by-move receiver (self: Self), or no receiver at all (an associated function), is not object-safe — moving a value or locating an instance both need informationdyn Aspect's erasure has already discarded.Selfappearing anywhere else in the signature — a non-receiver parameter, the return type — is also not object-safe. - No generic methods. A method with its own type parameters cannot be dispatched through a vtable, because the vtable entry would need to be generated per instantiation. Such a method is excluded from the vtable — this does not by itself disqualify the rest of the aspect.
- No associated types in signature. A method whose signature references one of the aspect's own associated types makes that method's vtable entry depend on the concrete impl's binding for it, which erasure has discarded.
aspect Shape {
fun area(&self) -> f64; // object-safe: &self receiver, no Self, no assoc types
}
fun accepts_shape(x: dyn Shape) -> i64 { 0 }
fun main() -> i64 { 0 }
// `Clone::clone` returns `Self` -- not object-safe.
fun rejected(x: dyn Clone) -> i64 { 0 }
std::core::Drop needs no exception to rule 1: its one method is declared
fun drop(&var self); (RFC-0071), an ordinary &var Self receiver — dyn Drop is object-safe the same way dyn Shape above is.
Formal rules
Legality Rule №2
A method's first parameter must be self: &Self or self: &var Self to be
object-safe; a by-move receiver, no receiver at all, or Self in any other
signature position, is not.
Referenced by: rfc-0008
Tested by (4)
1// Regression (metel-core#865, RFC-0008 slice 1, §3's "Standard library
2// object-safety" list): `std::core::Drop` is object-safe by the ordinary
3// rule -- its one method, `fun drop(&var self);`, takes an ordinary `&var
4// Self` receiver (RFC-0071, revised 2026-08-27; was `self` by value, which
5// would have needed a dedicated rule-1 exception here -- see that revision's
6// note for why).
7
8fun a(x: dyn Drop) -> i64 { 0 }
9
10fun main() {}
passes
1// Regression (metel-core#865, RFC-0008 slice 1, §3 rule 1): `Clone` is not
2// object-safe -- `clone` returns `Self`, and dispatching through a vtable
3// requires knowing the concrete return size, which `dyn Aspect` erases. This
4// is RFC-0008 §3's own worked example, against the real stdlib `Clone`.
5
6fun a(x: dyn Clone) -> i64 { 0 }
7
8fun main() {}
typecheck errorT0025“Clone::clone returns Self”
1// Regression (metel-core#865, RFC-0008 slice 1, §3 rule 1): a by-move `self`
2// receiver is not object-safe -- moving a value requires knowing its size at
3// compile time, which `dyn Aspect` (unsized) doesn't have.
4
5aspect Consumer {
6 fun consume(self) -> i64;
7}
8
9fun a(x: dyn Consumer) -> i64 { 0 }
10
11fun main() {}
typecheck errorT0025“Consumer::consume takes self by value”
1// Regression (metel-core#865, RFC-0008 slice 1, §3 rule 1): a method with no
2// `self` receiver at all (an associated function) cannot be dispatched
3// through a vtable -- there is no instance to dispatch against.
4
5aspect Factory {
6 fun create() -> i64;
7}
8
9fun a(x: dyn Factory) -> i64 { 0 }
10
11fun main() {}
typecheck errorT0025“Factory::create has no `self` receiver”
Legality Rule №3
A method with its own generic parameters is excluded from the vtable without disqualifying the rest of the aspect — including when it is the aspect's only method, the same way a zero-method marker aspect is object-safe.
Referenced by: rfc-0008
Tested by (2)
1// Regression (metel-core#865, RFC-0008 slice 1, §3 rule 2): a method with its
2// own type parameters is excluded from the vtable, but doesn't by itself make
3// the aspect non-object-safe -- the aspect stays object-safe as long as its
4// non-generic methods satisfy rules 1 and 3 on their own.
5
6aspect Mapper {
7 fun map<U>(&self, f: U) -> i64;
8 fun describe(&self) -> String;
9}
10
11fun a(x: dyn Mapper) -> i64 { 0 }
12
13fun main() {}
passes
1// Regression (metel-core#865, RFC-0008 slice 1, §3 rule 2): an aspect whose
2// *only* method is generic is still object-safe -- it contributes zero vtable
3// entries, the same as a marker aspect declaring no methods at all (RFC-0008
4// §3's own Send/Sync example: "marker aspects with no methods; object-safe").
5// Verified against the real interpreter before writing this expectation:
6// rejecting this case was in the sub-issue's original acceptance-criteria
7// draft, but that was a mistake caught during implementation -- there is
8// nothing in RFC-0008 §3 that disqualifies a zero-vtable-entry aspect.
9
10aspect OnlyGeneric {
11 fun map<U>(&self, f: U) -> i64;
12}
13
14fun a(x: dyn OnlyGeneric) -> i64 { 0 }
15
16fun main() {}
passes
Legality Rule №4
A method whose signature references one of the aspect's own associated types is not object-safe.
Referenced by: rfc-0008, rfc-0082
Tested by
1// Regression (metel-core#865, RFC-0008 slice 1, §3 rule 3): an aspect whose
2// method signature references one of its own associated types is not
3// object-safe -- the vtable entry's shape would depend on the concrete impl's
4// binding for that associated type, which `dyn Aspect` has erased. Uses a
5// custom aspect since `Deref` (RFC-0008 §3's own example) isn't a real
6// stdlib aspect yet.
7
8aspect Container {
9 type Item;
10
11 fun get(&self) -> Item;
12}
13
14fun a(x: dyn Container) -> i64 { 0 }
15
16fun main() {}
typecheck errorT0025“references the associated type Item”
Coercion
A value of concrete type T coerces to dyn Aspect implicitly wherever a
dyn Aspect-typed value is expected — a let/mut binding, a function
argument, a return value — when T implements Aspect. No explicit cast is
needed, and the same coercion applies behind &/&var:
aspect Shape {
fun area(&self) -> f64;
}
struct Circle { radius: f64 }
extend Circle: Shape {
fun area(&self) -> f64 { 3.14159 * self.radius * self.radius }
}
fun main() -> i64 {
let shape: dyn Shape := Circle { radius = 2.0 };
let circle := Circle { radius = 1.0 };
let borrowed: &dyn Shape := &circle;
0
}
A concrete type that does not implement the target aspect is rejected at the coercion site itself — a compile-time error, not a deferred runtime failure:
struct Rock { }
fun main() -> i64 {
let x: dyn Display := Rock { };
0
}
Formal rules
Legality Rule №6
A concrete value coerces to dyn Aspect implicitly at a binding, var
reassignment, function or method argument, return value, break value,
(expr : Type) ascription, struct-literal field, or array-literal element
position — owned or behind &/&var — when its type implements the aspect;
rejected with T0012 when it does not.
A heterogeneous array literal — different concrete element types coerced
to dyn Aspect within one [...] expression — is accepted at a let/var
binding, each element checked against the declared element type
independently. A heterogeneous literal used directly as an argument or
field, with no annotated binding in between, isn't covered — bind it to a
let/var first.
Referenced by: rfc-0008
Tested by (26)
1// RFC-0008 §6: reassigning an existing `var`-bound `dyn Aspect` binding
2// coerces the new value the same way its original `let`/`var` did. Real gap
3// found during a targeted edge-case audit: `Expr::Assign`'s construction
4// never called `maybe_dyn_coerce` (confirmed this isn't a general
5// assignment-checking gap -- an ordinary `x = "hello"` on an `i64` var is
6// already correctly rejected; the hole was specific to `Type::Dyn`). See
7// neg_39 for the corresponding rejection.
8
9fun main() {
10 var x: dyn Display := 42;
11 assert(x.to_string() == "42");
12 x := "hello".to_string();
13 assert(x.to_string() == "hello");
14}
passes
1// RFC-0008 §6: an array-literal element declared `dyn Aspect` coerces the
2// same way any other hinted position does -- both the dynamically-sized
3// (`T[]`) and fixed-size (`[T; N]`) forms. Real gap found during a targeted
4// edge-case audit: `Expr::Array`'s two hinted construction branches never
5// called `maybe_dyn_coerce` (see neg_40 for the corresponding rejection).
6//
7// A *heterogeneous* array literal (different concrete element types in one
8// `[...]` expression) is exactly the same rule: metel-core#876 found Pass 1
9// unified every element against the *first* element's own type, with no
10// access to the annotation, rejecting two different concrete types outright
11// before either got a chance to coerce -- fixed by inferring each element
12// against the declared `dyn Aspect` element type directly when the literal
13// is annotated (see neg_44 for the corresponding rejection). `List<dyn
14// Aspect>` (RFC-0008 §7, metel-core#864) remains the idiomatic way to build
15// a heterogeneous collection incrementally, one `push` at a time; this is
16// the array-*literal* form of the same thing.
17
18struct Rock { }
19struct Pebble { }
20
21extend Rock: Display {
22 fun to_string(&self) -> String { "rock".to_string() }
23}
24
25extend Pebble: Display {
26 fun to_string(&self) -> String { "pebble".to_string() }
27}
28
29fun main() {
30 let arr: dyn Display[] := [Rock { }];
31 assert(arr[0].to_string() == "rock");
32
33 let sized: [dyn Display; 1] := [Rock { }];
34 assert(sized[0].to_string() == "rock");
35
36 let hetero: dyn Display[] := [Rock { }, Pebble { }];
37 assert(hetero[0].to_string() == "rock");
38 assert(hetero[1].to_string() == "pebble");
39
40 let hetero_sized: [dyn Display; 2] := [Rock { }, Pebble { }];
41 assert(hetero_sized[0].to_string() == "rock");
42 assert(hetero_sized[1].to_string() == "pebble");
43
44 var hetero_mut: dyn Display[] := [Rock { }, Pebble { }];
45 assert(hetero_mut[0].to_string() == "rock");
46
47 let empty: dyn Display[] := [];
48 assert(empty.len() == 0);
49}
passes
1// RFC-0008 §6: coercion at a function's return position -- both the
2// implicit tail-expression form and an explicit `return`. This already
3// worked (both go through the same `maybe_dyn_coerce` call sites `let`
4// bindings use — block-tail construction and `Expr::Return`), confirmed
5// directly and covered here since no existing fixture exercised it (see
6// neg_41 for the corresponding rejection).
7
8struct Rock { }
9
10extend Rock: Display {
11 fun to_string(&self) -> String { "rock".to_string() }
12}
13
14fun make_tail() -> dyn Display { Rock { } }
15fun make_explicit() -> dyn Display { return Rock { }; }
16
17fun main() {
18 assert(make_tail().to_string() == "rock");
19 assert(make_explicit().to_string() == "rock");
20}
passes
1// RFC-0008 §6: coercion at `break`, out of a `loop` expression whose value
2// binds to a `dyn Aspect`-typed `let`. Already worked (the same
3// `maybe_dyn_coerce` call site `let`/return use), confirmed directly and
4// covered here since no existing fixture exercised it (see neg_42 for the
5// corresponding rejection).
6
7struct Rock { }
8
9extend Rock: Display {
10 fun to_string(&self) -> String { "rock".to_string() }
11}
12
13fun main() {
14 var i := 0;
15 let x: dyn Display := loop {
16 i := i + 1;
17 if (i > 3) { break Rock { }; }
18 };
19 assert(x.to_string() == "rock");
20}
passes
1// RFC-0008 §6: coercion at an explicit `(expr : Type)` ascription. Already
2// worked (the same `maybe_dyn_coerce` call site every other hinted
3// expression-construction site uses), confirmed directly and covered here
4// since no existing fixture exercised it (see neg_43 for the corresponding
5// rejection).
6
7struct Rock { }
8
9extend Rock: Display {
10 fun to_string(&self) -> String { "rock".to_string() }
11}
12
13fun main() {
14 let x := (Rock { } : dyn Display);
15 assert(x.to_string() == "rock");
16}
passes
1// RFC-0008 §6: a generic aspect's own type args coerce correctly outside
2// `let` too (94 only covers `let`) -- here, a return position.
3
4aspect Callable<A, B> {
5 fun call(&self, arg: A) -> B;
6}
7
8struct Doubler { }
9
10extend Doubler: Callable<i64, i64> {
11 fun call(&self, arg: i64) -> i64 { arg * 2 }
12}
13
14fun make() -> dyn Callable<i64, i64> { Doubler { } }
15
16fun main() {
17 assert(make().call(21) == 42);
18}
passes
1// RFC-0008 §6: `&x` where `x` is already `dyn`-typed is an ordinary
2// reference -- no coercion needed, and confirming this doesn't trip over
3// the reference-coercion check `maybe_dyn_coerce` gained for 97/98 (that
4// check already special-cases an already-`Dyn` inner type as a no-op).
5
6struct Rock { }
7
8extend Rock: Display {
9 fun to_string(&self) -> String { "rock".to_string() }
10}
11
12fun show(x: &dyn Display) -> String { x.to_string() }
13
14fun main() {
15 let x: dyn Display := Rock { };
16 assert(show(&x) == "rock");
17}
passes
1// RFC-0008 slice 2: owned `dyn Aspect` values -- implicit coercion at a `let`
2// binding, dispatched through the aspect's vtable at the call site (§1/§6).
3// Both the unsuffixed-literal case (defaults to i64, then coerces) and an
4// already-concrete-typed source exercise the coercion.
5
6fun main() {
7 let x: dyn Display := 42;
8 assert(x.to_string() == "42");
9
10 let s: String := "hello".to_string();
11 let y: dyn Display := s;
12 assert(y.to_string() == "hello");
13
14 let f: dyn Display := 3.5;
15 assert(f.to_string() == "3.5");
16}
passes
1// RFC-0008 slice 2 / §1: `&dyn Aspect` -- a borrowed aspect object. No new
2// runtime representation needed for the reference itself (RFC-0008 §5): it's
3// an ordinary `Reference` to the concrete value, dispatched by its own
4// erased static type, not by any fat-pointer wrapping.
5
6fun main() {
7 let n: i64 := 42;
8 let x: &dyn Display := &n;
9 assert(x.to_string() == "42");
10
11 let s: String := "hi".to_string();
12 let y: &dyn Display := &s;
13 assert(y.to_string() == "hi");
14}
passes
1// RFC-0008 slice 2 / §6: implicit coercion at a function-argument position --
2// a distinct code path from the `let`/return coercion sites (argument
3// construction is enforced by direct `Type` equality, not the shared
4// `maybe_read_copy`/`maybe_singleton_coerce` hooks those sites use).
5
6fun show(x: dyn Display) -> String {
7 x.to_string()
8}
9
10fun main() {
11 assert(show(42) == "42");
12 assert(show("hi".to_string()) == "hi");
13}
passes
1// RFC-0008 slice 3 / §7: `List<dyn Aspect>` -- a heterogeneous collection,
2// each element a fat pointer to a different concrete type, dispatching
3// independently. `push`'s argument coerces to `dyn Shape` at the call site
4// the same way an ordinary `let`/argument coercion already does (slice 2) --
5// this fixture is what confirmed that coercion wasn't actually wired into
6// generic method-call arguments until this slice.
7//
8// Iterated via `.as_slice()`, not `for shape in shapes` directly: `List<T>`
9// doesn't implement `Iterable<T>` for any `T` today (a separate, pre-existing
10// gap, unrelated to `dyn Aspect` -- see metel-core#871), and this is the same
11// idiom `List<T>`'s own methods already use internally.
12
13aspect Shape {
14 fun area(&self) -> f64;
15}
16
17struct Circle { radius: f64 }
18struct Rectangle { w: f64, h: f64 }
19
20extend Circle: Shape {
21 fun area(&self) -> f64 { 3.14159 * self.radius * self.radius }
22}
23
24extend Rectangle: Shape {
25 fun area(&self) -> f64 { self.w * self.h }
26}
27
28fun main() {
29 var shapes: List<dyn Shape> := List::new();
30 shapes.push(Circle { radius = 5.0 });
31 shapes.push(Rectangle { w = 3.0, h = 4.0 });
32
33 var total := 0.0;
34 for (shape in shapes.as_slice()) {
35 total := total + shape.area();
36 }
37 // Circle: 3.14159 * 25 = 78.53975; Rectangle: 12.0; total = 90.53975
38 assert(total > 90.5 && total < 90.6);
39}
passes
1// RFC-0008 §6: coercion behind a shared reference (`&dyn Aspect`). Real gap
2// found during a targeted edge-case audit: `maybe_dyn_coerce` only ever
3// matched a bare `Type::Dyn`, never `Type::Reference(Dyn)` -- so this
4// position was never actually checked anywhere, at any site, even though
5// Pass 1's `unify` already accepted the shape (deliberately, deferring the
6// real check here). Fixed by adding a reference-peeling branch to
7// `maybe_dyn_coerce` itself, which closes the gap at every site at once
8// (see neg_36 for the corresponding rejection, and 91 for the same shape
9// at a `let` site specifically).
10
11struct Rock { }
12
13extend Rock: Display {
14 fun to_string(&self) -> String { "rock".to_string() }
15}
16
17fun show(x: &dyn Display) -> String { x.to_string() }
18
19fun main() {
20 assert(show(&Rock { }) == "rock");
21}
passes
1// RFC-0008 §6: coercion behind an exclusive reference (`&var dyn Aspect`) --
2// the mutable counterpart of 97. Mutation through the parameter must reach
3// the original binding, confirming the reference stays an ordinary
4// `Reference`/`MutReference` to the concrete value (no fat-pointer wrapping
5// needed for the borrowed forms at all, per RFC-0008 §1/§5).
6
7struct MyCounter { n: i64 }
8
9aspect Counter {
10 fun increment(&var self);
11 fun value(&self) -> i64;
12}
13
14extend MyCounter: Counter {
15 fun increment(&var self) { self.n := self.n + 1; }
16 fun value(&self) -> i64 { self.n }
17}
18
19fun bump(c: &var dyn Counter) { c.increment(); }
20
21fun main() {
22 var m := MyCounter { n = 0 };
23 bump(&var m);
24 bump(&var m);
25 assert(m.n == 2);
26}
passes
1// RFC-0008 §6: a struct field declared `dyn Aspect` coerces at the
2// struct-literal site, the same as any other hinted position. Real gap
3// found during a targeted edge-case audit: `Expr::StructLiteral`'s field
4// construction never called `maybe_dyn_coerce`, so a field of a type that
5// did not implement the aspect silently compiled and only panicked at
6// first use (see neg_38 for the corresponding rejection).
7
8struct Rock { }
9
10extend Rock: Display {
11 fun to_string(&self) -> String { "rock".to_string() }
12}
13
14struct Holder { item: dyn Display }
15
16fun main() {
17 let h := Holder { item = Rock { } };
18 assert(h.item.to_string() == "rock");
19}
passes
1struct Rock { }
2
3fun main() {
4 let x: dyn Display := Rock { };
5}
typecheck errorT0012“does not implement `Display`”
1struct Rock { }
2
3fun show(x: dyn Display) -> String {
4 x.to_string()
5}
6
7fun main() {
8 show(Rock { });
9}
typecheck errorT0012“does not implement `Display`”
1struct Rock { }
2
3fun main() {
4 var shapes: List<dyn Display> := List::new();
5 shapes.push(Rock { });
6}
typecheck errorT0012“does not implement `Display`”
1struct Rock { }
2
3fun show(x: &dyn Display) -> String { x.to_string() }
4
5fun main() {
6 show(&Rock { });
7}
typecheck errorT0012“does not implement `Display`”
1struct Rock { n: i64 }
2
3aspect Counter {
4 fun increment(&var self);
5}
6
7fun bump(c: &var dyn Counter) { }
8
9fun main() {
10 var r := Rock { n = 0 };
11 bump(&var r);
12}
typecheck errorT0012“does not implement `Counter`”
1struct Rock { }
2struct Holder { item: dyn Display }
3
4fun main() {
5 let h := Holder { item = Rock { } };
6}
typecheck errorT0012“does not implement `Display`”
1struct Rock { }
2
3fun main() {
4 var x: dyn Display := 42;
5 x := Rock { };
6}
typecheck errorT0012“does not implement `Display`”
1struct Rock { }
2
3fun main() {
4 let arr: dyn Display[] := [Rock { }];
5}
typecheck errorT0012“does not implement `Display`”
1struct Rock { }
2
3fun make() -> dyn Display { Rock { } }
4
5fun main() {
6 make();
7}
typecheck errorT0012“does not implement `Display`”
1struct Rock { }
2
3fun main() {
4 let x: dyn Display := loop {
5 break Rock { };
6 };
7}
typecheck errorT0012“does not implement `Display`”
1struct Rock { }
2
3fun main() {
4 let x := (Rock { } : dyn Display);
5}
typecheck errorT0012“does not implement `Display`”
1struct Rock { }
2struct Pebble { }
3
4extend Rock: Display {
5 fun to_string(&self) -> String { "rock".to_string() }
6}
7
8fun main() {
9 let arr: dyn Display[] := [Rock { }, Pebble { }];
10}
typecheck errorT0012“does not implement `Display`”
Dispatch
Calling a method on a dyn Aspect value — owned, &, or &var — resolves at
runtime to the wrapped concrete value's own implementation of the aspect.
Different concrete values behind the same dyn Aspect type dispatch
independently, including through mutation (&var self) on an owned binding:
aspect Shape {
fun area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Rectangle { w: f64, h: f64 }
extend Circle: Shape {
fun area(&self) -> f64 { 3.14159 * self.radius * self.radius }
}
extend Rectangle: Shape {
fun area(&self) -> f64 { self.w * self.h }
}
fun main() -> i64 {
let a: dyn Shape := Circle { radius = 2.0 };
let b: dyn Shape := Rectangle { w = 3.0, h = 4.0 };
// `a.area()` and `b.area()` each dispatch to their own concrete impl.
0
}
Formal rules
Dynamic Semantics №1
A method call through a dyn Aspect value — owned, &, or &var — resolves
at runtime to the implementation the wrapped concrete value's own type
provides for the aspect, independent of any other value coerced to the same
dyn Aspect type elsewhere in the program.
Referenced by: rfc-0008
Tested by (17)
1// RFC-0008 §6: reassigning an existing `var`-bound `dyn Aspect` binding
2// coerces the new value the same way its original `let`/`var` did. Real gap
3// found during a targeted edge-case audit: `Expr::Assign`'s construction
4// never called `maybe_dyn_coerce` (confirmed this isn't a general
5// assignment-checking gap -- an ordinary `x = "hello"` on an `i64` var is
6// already correctly rejected; the hole was specific to `Type::Dyn`). See
7// neg_39 for the corresponding rejection.
8
9fun main() {
10 var x: dyn Display := 42;
11 assert(x.to_string() == "42");
12 x := "hello".to_string();
13 assert(x.to_string() == "hello");
14}
passes
1// RFC-0008 §6: an array-literal element declared `dyn Aspect` coerces the
2// same way any other hinted position does -- both the dynamically-sized
3// (`T[]`) and fixed-size (`[T; N]`) forms. Real gap found during a targeted
4// edge-case audit: `Expr::Array`'s two hinted construction branches never
5// called `maybe_dyn_coerce` (see neg_40 for the corresponding rejection).
6//
7// A *heterogeneous* array literal (different concrete element types in one
8// `[...]` expression) is exactly the same rule: metel-core#876 found Pass 1
9// unified every element against the *first* element's own type, with no
10// access to the annotation, rejecting two different concrete types outright
11// before either got a chance to coerce -- fixed by inferring each element
12// against the declared `dyn Aspect` element type directly when the literal
13// is annotated (see neg_44 for the corresponding rejection). `List<dyn
14// Aspect>` (RFC-0008 §7, metel-core#864) remains the idiomatic way to build
15// a heterogeneous collection incrementally, one `push` at a time; this is
16// the array-*literal* form of the same thing.
17
18struct Rock { }
19struct Pebble { }
20
21extend Rock: Display {
22 fun to_string(&self) -> String { "rock".to_string() }
23}
24
25extend Pebble: Display {
26 fun to_string(&self) -> String { "pebble".to_string() }
27}
28
29fun main() {
30 let arr: dyn Display[] := [Rock { }];
31 assert(arr[0].to_string() == "rock");
32
33 let sized: [dyn Display; 1] := [Rock { }];
34 assert(sized[0].to_string() == "rock");
35
36 let hetero: dyn Display[] := [Rock { }, Pebble { }];
37 assert(hetero[0].to_string() == "rock");
38 assert(hetero[1].to_string() == "pebble");
39
40 let hetero_sized: [dyn Display; 2] := [Rock { }, Pebble { }];
41 assert(hetero_sized[0].to_string() == "rock");
42 assert(hetero_sized[1].to_string() == "pebble");
43
44 var hetero_mut: dyn Display[] := [Rock { }, Pebble { }];
45 assert(hetero_mut[0].to_string() == "rock");
46
47 let empty: dyn Display[] := [];
48 assert(empty.len() == 0);
49}
passes
1// RFC-0008 §6: coercion at a function's return position -- both the
2// implicit tail-expression form and an explicit `return`. This already
3// worked (both go through the same `maybe_dyn_coerce` call sites `let`
4// bindings use — block-tail construction and `Expr::Return`), confirmed
5// directly and covered here since no existing fixture exercised it (see
6// neg_41 for the corresponding rejection).
7
8struct Rock { }
9
10extend Rock: Display {
11 fun to_string(&self) -> String { "rock".to_string() }
12}
13
14fun make_tail() -> dyn Display { Rock { } }
15fun make_explicit() -> dyn Display { return Rock { }; }
16
17fun main() {
18 assert(make_tail().to_string() == "rock");
19 assert(make_explicit().to_string() == "rock");
20}
passes
1// RFC-0008 §6: coercion at `break`, out of a `loop` expression whose value
2// binds to a `dyn Aspect`-typed `let`. Already worked (the same
3// `maybe_dyn_coerce` call site `let`/return use), confirmed directly and
4// covered here since no existing fixture exercised it (see neg_42 for the
5// corresponding rejection).
6
7struct Rock { }
8
9extend Rock: Display {
10 fun to_string(&self) -> String { "rock".to_string() }
11}
12
13fun main() {
14 var i := 0;
15 let x: dyn Display := loop {
16 i := i + 1;
17 if (i > 3) { break Rock { }; }
18 };
19 assert(x.to_string() == "rock");
20}
passes
1// RFC-0008 §6: coercion at an explicit `(expr : Type)` ascription. Already
2// worked (the same `maybe_dyn_coerce` call site every other hinted
3// expression-construction site uses), confirmed directly and covered here
4// since no existing fixture exercised it (see neg_43 for the corresponding
5// rejection).
6
7struct Rock { }
8
9extend Rock: Display {
10 fun to_string(&self) -> String { "rock".to_string() }
11}
12
13fun main() {
14 let x := (Rock { } : dyn Display);
15 assert(x.to_string() == "rock");
16}
passes
1// RFC-0008 §6: a generic aspect's own type args coerce correctly outside
2// `let` too (94 only covers `let`) -- here, a return position.
3
4aspect Callable<A, B> {
5 fun call(&self, arg: A) -> B;
6}
7
8struct Doubler { }
9
10extend Doubler: Callable<i64, i64> {
11 fun call(&self, arg: i64) -> i64 { arg * 2 }
12}
13
14fun make() -> dyn Callable<i64, i64> { Doubler { } }
15
16fun main() {
17 assert(make().call(21) == 42);
18}
passes
1// RFC-0008 §6: `&x` where `x` is already `dyn`-typed is an ordinary
2// reference -- no coercion needed, and confirming this doesn't trip over
3// the reference-coercion check `maybe_dyn_coerce` gained for 97/98 (that
4// check already special-cases an already-`Dyn` inner type as a no-op).
5
6struct Rock { }
7
8extend Rock: Display {
9 fun to_string(&self) -> String { "rock".to_string() }
10}
11
12fun show(x: &dyn Display) -> String { x.to_string() }
13
14fun main() {
15 let x: dyn Display := Rock { };
16 assert(show(&x) == "rock");
17}
passes
1// RFC-0008 slice 2: owned `dyn Aspect` values -- implicit coercion at a `let`
2// binding, dispatched through the aspect's vtable at the call site (§1/§6).
3// Both the unsuffixed-literal case (defaults to i64, then coerces) and an
4// already-concrete-typed source exercise the coercion.
5
6fun main() {
7 let x: dyn Display := 42;
8 assert(x.to_string() == "42");
9
10 let s: String := "hello".to_string();
11 let y: dyn Display := s;
12 assert(y.to_string() == "hello");
13
14 let f: dyn Display := 3.5;
15 assert(f.to_string() == "3.5");
16}
passes
1// RFC-0008 slice 2 / §1: `&dyn Aspect` -- a borrowed aspect object. No new
2// runtime representation needed for the reference itself (RFC-0008 §5): it's
3// an ordinary `Reference` to the concrete value, dispatched by its own
4// erased static type, not by any fat-pointer wrapping.
5
6fun main() {
7 let n: i64 := 42;
8 let x: &dyn Display := &n;
9 assert(x.to_string() == "42");
10
11 let s: String := "hi".to_string();
12 let y: &dyn Display := &s;
13 assert(y.to_string() == "hi");
14}
passes
1// RFC-0008 slice 2: two different concrete types coerced to the same `dyn
2// Aspect`, each dispatching to its own impl -- confirms dispatch is
3// genuinely per-value (keyed by the wrapped value's own concrete type),
4// not baked in at the coercion site.
5
6aspect Shape {
7 fun area(&self) -> f64;
8}
9
10struct Circle { radius: f64 }
11struct Rectangle { w: f64, h: f64 }
12
13extend Circle: Shape {
14 fun area(&self) -> f64 { 3.14159 * self.radius * self.radius }
15}
16
17extend Rectangle: Shape {
18 fun area(&self) -> f64 { self.w * self.h }
19}
20
21fun main() {
22 let a: dyn Shape := Circle { radius = 2.0 };
23 let b: dyn Shape := Rectangle { w = 3.0, h = 4.0 };
24
25 assert(b.area() == 12.0);
26 let a_area := a.area();
27 assert(a_area > 12.5 && a_area < 12.6);
28}
passes
1// RFC-0008 slice 2 / §5: a `&var self` method dispatched through an owned,
2// mutably-bound `dyn Aspect` value. Mutation must reach the concrete value
3// behind the fat pointer, not the wrapper -- the same binding (`c`) observes
4// each mutation on the next call.
5
6aspect Counter {
7 fun increment(&var self);
8 fun value(&self) -> i64;
9}
10
11struct MyCounter { n: i64 }
12
13extend MyCounter: Counter {
14 fun increment(&var self) { self.n := self.n + 1; }
15 fun value(&self) -> i64 { self.n }
16}
17
18fun main() {
19 var c: dyn Counter := MyCounter { n = 0 };
20 c.increment();
21 c.increment();
22 c.increment();
23 assert(c.value() == 3);
24}
passes
1// RFC-0008 slice 2: `dyn Callable<i64, i64>` -- the aspect's own generic
2// params (`A`, `B`) substituted with this `dyn Aspect`'s type args, both in
3// method-call typechecking (the param/return types) and dispatch.
4
5aspect Callable<A, B> {
6 fun call(&self, arg: A) -> B;
7}
8
9struct Doubler { }
10
11extend Doubler: Callable<i64, i64> {
12 fun call(&self, arg: i64) -> i64 { arg * 2 }
13}
14
15fun main() {
16 let d: dyn Callable<i64, i64> := Doubler { };
17 assert(d.call(21) == 42);
18}
passes
1// RFC-0008 slice 2 / §6: implicit coercion at a function-argument position --
2// a distinct code path from the `let`/return coercion sites (argument
3// construction is enforced by direct `Type` equality, not the shared
4// `maybe_read_copy`/`maybe_singleton_coerce` hooks those sites use).
5
6fun show(x: dyn Display) -> String {
7 x.to_string()
8}
9
10fun main() {
11 assert(show(42) == "42");
12 assert(show("hi".to_string()) == "hi");
13}
passes
1// RFC-0008 slice 3 / §7: `List<dyn Aspect>` -- a heterogeneous collection,
2// each element a fat pointer to a different concrete type, dispatching
3// independently. `push`'s argument coerces to `dyn Shape` at the call site
4// the same way an ordinary `let`/argument coercion already does (slice 2) --
5// this fixture is what confirmed that coercion wasn't actually wired into
6// generic method-call arguments until this slice.
7//
8// Iterated via `.as_slice()`, not `for shape in shapes` directly: `List<T>`
9// doesn't implement `Iterable<T>` for any `T` today (a separate, pre-existing
10// gap, unrelated to `dyn Aspect` -- see metel-core#871), and this is the same
11// idiom `List<T>`'s own methods already use internally.
12
13aspect Shape {
14 fun area(&self) -> f64;
15}
16
17struct Circle { radius: f64 }
18struct Rectangle { w: f64, h: f64 }
19
20extend Circle: Shape {
21 fun area(&self) -> f64 { 3.14159 * self.radius * self.radius }
22}
23
24extend Rectangle: Shape {
25 fun area(&self) -> f64 { self.w * self.h }
26}
27
28fun main() {
29 var shapes: List<dyn Shape> := List::new();
30 shapes.push(Circle { radius = 5.0 });
31 shapes.push(Rectangle { w = 3.0, h = 4.0 });
32
33 var total := 0.0;
34 for (shape in shapes.as_slice()) {
35 total := total + shape.area();
36 }
37 // Circle: 3.14159 * 25 = 78.53975; Rectangle: 12.0; total = 90.53975
38 assert(total > 90.5 && total < 90.6);
39}
passes
1// RFC-0008 §6: coercion behind a shared reference (`&dyn Aspect`). Real gap
2// found during a targeted edge-case audit: `maybe_dyn_coerce` only ever
3// matched a bare `Type::Dyn`, never `Type::Reference(Dyn)` -- so this
4// position was never actually checked anywhere, at any site, even though
5// Pass 1's `unify` already accepted the shape (deliberately, deferring the
6// real check here). Fixed by adding a reference-peeling branch to
7// `maybe_dyn_coerce` itself, which closes the gap at every site at once
8// (see neg_36 for the corresponding rejection, and 91 for the same shape
9// at a `let` site specifically).
10
11struct Rock { }
12
13extend Rock: Display {
14 fun to_string(&self) -> String { "rock".to_string() }
15}
16
17fun show(x: &dyn Display) -> String { x.to_string() }
18
19fun main() {
20 assert(show(&Rock { }) == "rock");
21}
passes
1// RFC-0008 §6: coercion behind an exclusive reference (`&var dyn Aspect`) --
2// the mutable counterpart of 97. Mutation through the parameter must reach
3// the original binding, confirming the reference stays an ordinary
4// `Reference`/`MutReference` to the concrete value (no fat-pointer wrapping
5// needed for the borrowed forms at all, per RFC-0008 §1/§5).
6
7struct MyCounter { n: i64 }
8
9aspect Counter {
10 fun increment(&var self);
11 fun value(&self) -> i64;
12}
13
14extend MyCounter: Counter {
15 fun increment(&var self) { self.n := self.n + 1; }
16 fun value(&self) -> i64 { self.n }
17}
18
19fun bump(c: &var dyn Counter) { c.increment(); }
20
21fun main() {
22 var m := MyCounter { n = 0 };
23 bump(&var m);
24 bump(&var m);
25 assert(m.n == 2);
26}
passes
1// RFC-0008 §6: a struct field declared `dyn Aspect` coerces at the
2// struct-literal site, the same as any other hinted position. Real gap
3// found during a targeted edge-case audit: `Expr::StructLiteral`'s field
4// construction never called `maybe_dyn_coerce`, so a field of a type that
5// did not implement the aspect silently compiled and only panicked at
6// first use (see neg_38 for the corresponding rejection).
7
8struct Rock { }
9
10extend Rock: Display {
11 fun to_string(&self) -> String { "rock".to_string() }
12}
13
14struct Holder { item: dyn Display }
15
16fun main() {
17 let h := Holder { item = Rock { } };
18 assert(h.item.to_string() == "rock");
19}
passes
Heterogeneous Collections
List<dyn Aspect> holds values of different concrete types together, each
satisfying a common aspect (RFC-0008 §7). push's argument coerces to the
list's own dyn Aspect element type the same way any other argument
position does (see §Coercion above), and each element then
dispatches independently (see §Dispatch above):
aspect Shape {
fun area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Rectangle { w: f64, h: f64 }
extend Circle: Shape {
fun area(&self) -> f64 { 3.14159 * self.radius * self.radius }
}
extend Rectangle: Shape {
fun area(&self) -> f64 { self.w * self.h }
}
fun main() -> i64 {
var shapes: List<dyn Shape> := List::new();
shapes.push(Circle { radius = 2.0 });
shapes.push(Rectangle { w = 3.0, h = 4.0 });
for (shape in shapes.as_slice()) {
// each dispatches to its own concrete `area()`
}
0
}
A concrete type that does not implement the aspect is rejected at the
push call site itself, the same as any other argument-position coercion.
List<T> does not implement Iterable<T> for any T today, unrelated to
dyn Aspect — iterate via .as_slice() above, the same idiom List<T>'s
own methods (map/filter/fold/…) already use internally.
Aspect Implementation Coherence
Every (aspect, type) pair has at most one implementation visible to the program, independent of module load order. Two rules make this checkable without a whole-program scan.
Orphan rule. extend Type: Aspect is permitted only when at least one of Aspect
or Type's outermost type constructor is declared in the same module as the extend
block. Built-in aspects and built-in types count as local to std::core.
extend MyStruct: Display { ... } // ok: MyStruct is local
extend i64: MyAspect { ... } // ok: MyAspect is local
extend i64: Display { ... } // ok only inside std::core: both are foreign elsewhere
A violating extend block is T0014 — orphan implementation. The orphan rule is what
keeps coherence a local, per-module property: a module can only add aspect
implementations it owns at least one half of, so no other module's impls need to be
consulted to know whether a given one is even legal.
Overlap detection. Two extend blocks of the same aspect conflict when some
concrete type instantiation would satisfy both. extend List<i64>: Display and
extend List<String>: Display don't conflict — disjoint element types — but
registering either one twice does. A conflict is T0015 — conflicting implementation,
reported at both impl spans. Combined with the orphan rule, an overlap can only arise
within a single module or between a module and std::core, so this check is local too.
Closed-world assumption. The set of impls in a program is fixed at compile time — nothing visible at compilation can add an impl later. This is what makes Negative Bounds, below, dischargeable from absence alone: T: !Aspect holds whenever no impl, concrete or blanket, applies to T, without requiring an explicit negative impl for every excluded type. A blanket impl<T: Foo> Bar for T is expanded when checking applicability — T: !Bar is provable only once no applicable blanket covers T either.
Auto-impl aspects. Auto-impls are a separate mechanism from this coherence
section. For coherence purposes, an auto-impl is treated as an ordinary
positive impl generated by the compiler: overlap detection and negative-impl override
both apply to it the same way they apply to an explicit extend block, while the
orphan rule does not apply because there is no authored impl site.
Negative impl priority. See Negative Impls, below, for the mechanism itself; the priority order coherence establishes is: an explicit negative impl beats an auto-impl or blanket positive impl for the same type, but an explicit positive impl and an explicit negative impl for the same concrete type is itself a T0015 coherence error, not a priority question.
What this deliberately doesn't cover. Coherence here is scoped to a single program's module graph — a future package system, compiling packages separately, needs its own cross-package coherence model, not addressed here. Rejected alternatives (a global overlap check without the orphan rule, last-impl-wins ordering, an open-world assumption, specialisation) are recorded in the RFC, not repeated here — each fails a property this design keeps: coherence errors are local and order-independent, and overlapping impls are always illegal rather than resolved by specificity.
Formal rules
Legality Rule №5
An authored extend Type: Aspect is rejected with T0014 when neither the aspect nor
the target type's outermost constructor is local to the implementing module.
Referenced by: rfc-0060
Tested by
1// Regression test (RFC-0060/issue #238): orphan rule. `main.mtl` declares
2// neither `Greet` (from greet_aspect.mtl) nor `Widget` (from widget.mtl) — an
3// impl here owns no half of the pair, so it must be rejected as an orphan
4// implementation regardless of what the impl body does.
5
6import greet_aspect::Greet;
7import widget::Widget;
8
9extend Widget: Greet { // ERROR[T0014]
10 fun greet(self) -> String {
11 return "hi";
12 }
13}
14
15fun main() {}
typecheck errorT0014at 9
Legality Rule №6
Two positive implementations of the same aspect are rejected with T0015 when a concrete instantiation is covered by both implementations, including when a blanket implementation covers an explicit concrete target.
Referenced by: rfc-0060
Tested by (4)
1// TYPECHECK_ERROR[conflicting implementation]
2// RFC-0081§2.2 and RFC-0081§3, issue #264: a negative impl is final -- no positive impl may
3// coexist with a negative impl for the same type and aspect. This falls out
4// of the existing overlap check (issue #238/RFC-0060), which doesn't special-
5// case polarity: two impls of the same aspect for the same concrete type
6// already collide regardless of polarity. Locked in here as its own fixture
7// since it was never explicitly exercised before this issue.
8
9aspect Greet {
10 fun greet(&self) -> String;
11}
12
13struct Widget {
14 x: i64,
15}
16
17extend Widget: Greet {
18 fun greet(&self) -> String {
19 return "hi";
20 }
21}
22
23extend Widget: !Greet;
24
25fun main() {}
typecheck error“conflicting implementation”
1// RFC-0060 §2: a blanket impl and a concrete impl of the same aspect
2// conflict when the concrete instantiation is already covered by the
3// blanket -- the pre-#244 overlap check only ever compared identically-
4// shaped canonical targets, so this shape-crossing pair silently missed
5// each other. Fixed by treating TypeParam as a wildcard when comparing
6// canonicalized targets (issue #244).
7
8aspect Marker {
9 fun mark(self) -> String;
10}
11
12struct Foo<T> {
13 value: T,
14}
15
16extend<T> Foo<T>: Marker {
17 fun mark(self) -> String { return "blanket"; }
18}
19
20extend Foo<i64>: Marker { // ERROR[T0015]
21 fun mark(self) -> String { return "concrete"; }
22}
23
24fun main() {}
typecheck errorT0015at 20
1// RFC-0061 §2: "Coherence rules for structural impl targets follow RFC-0060
2// §2 and RFC-0036 §3.1 without special cases." This is the structural-target
3// counterpart of conditional_impl_negation_disjoint_accepted (which targets a
4// nominal struct): `impl<T: Copy3> ...` and `impl<T: !Copy3> ...` for the
5// same locally-declared aspect on the *same structural target* (`T[]`) are
6// provably disjoint via syntactic negation and must not be reported as a
7// coherence conflict. `Serialize3` is declared locally, so the orphan rule
8// permits a user module to implement it for `T[]` at all (RFC-0061 §1).
9
10aspect Copy3 {
11 fun dup(self) -> i64;
12}
13
14aspect Serialize3 {
15 fun ser(self) -> String;
16}
17
18extend<T: Copy3> T[]: Serialize3 {
19 fun ser(self) -> String { return "copyable"; }
20}
21
22extend<T: !Copy3> T[]: Serialize3 {
23 fun ser(self) -> String { return "not-copyable"; }
24}
25
26fun main() {}
passes
1// Regression test (RFC-0060/issue #238): overlap detection. Two impls of the
2// same aspect for the same concrete type conflict, independent of whether
3// their method bodies agree or disagree.
4
5aspect Describe {
6 fun describe(self) -> String;
7}
8
9struct Crate {
10 x: i64,
11}
12
13extend Crate: Describe {
14 fun describe(self) -> String {
15 return "a";
16 }
17}
18
19extend Crate: Describe { // ERROR[T0015]
20 fun describe(self) -> String {
21 return "b";
22 }
23}
24
25fun main() {}
typecheck errorT0015at 19
Legality Rule №7
A negative aspect bound is satisfied only when no reachable concrete or blanket implementation of that aspect applies to the argument type.
Referenced by: rfc-0060
Tested by
1// RFC-0072: `T: !Drop`-style negative bounds parse and mix with positive bounds.
2// Issue #233's scope is representation only — satisfaction checking (rejecting a
3// T that DOES implement the negated aspect) is issue #243's job, not this one's.
4// This fixture is enforced since #243 landed; it passes because `i64` has no
5// registered `Drop` impl, so the negative bound is satisfied.
6
7fun move_out<T: !Drop>(x: T) -> T {
8 return x;
9}
10
11fun mixed<T: Display + !Drop>(x: T) -> T {
12 return x;
13}
14
15fun main() {
16 assert(move_out(5) == 5);
17 assert(mixed(7) == 7);
18}
passes
Formal rules
Legality Rule №1
A bare-parameter blanket implementation is an extend whose target is one of its own
generic parameters with no wrapping type constructor, such as extend<T: Bound> T: Aspect.
Referenced by: rfc-0097
Tested by
1// RFC-0097: a bare-parameter blanket impl is permitted only through the
2// aspect side of the orphan rule. Here the target `T` is never local, but the
3// aspect is, so the impl is accepted.
4
5aspect Marker {
6 fun mark(self) -> String;
7}
8
9extend<T> T: Marker {
10 fun mark(self) -> String {
11 return "ok";
12 }
13}
14
15fun needs_marker<U: Marker>(x: U) {}
16
17fun main() {
18 needs_marker(42);
19}
passes
Legality Rule №2
A bare-parameter target is local to no module. Such an implementation is legal only when its aspect is local to the implementing module; target locality can never satisfy the orphan rule for this form.
Referenced by: rfc-0097
Tested by (2)
1// RFC-0097: a bare-parameter blanket impl can never satisfy the orphan rule via
2// the target side, since bare `T` has no declaring module at all. Using a
3// foreign aspect must therefore be rejected.
4
5extend<T> T: Display { // ERROR[T0014]
6 fun to_string(self) -> String {
7 return "?";
8 }
9}
10
11fun main() {}
typecheck errorT0014at 5
1// RFC-0097: a bare-parameter blanket impl is permitted only through the
2// aspect side of the orphan rule. Here the target `T` is never local, but the
3// aspect is, so the impl is accepted.
4
5aspect Marker {
6 fun mark(self) -> String;
7}
8
9extend<T> T: Marker {
10 fun mark(self) -> String {
11 return "ok";
12 }
13}
14
15fun needs_marker<U: Marker>(x: U) {}
16
17fun main() {
18 needs_marker(42);
19}
passes
Legality Rule №3
Two bare-parameter blanket implementations of the same aspect conflict when an instantiation can satisfy both bound sets, under the ordinary overlap rule; no bare-parameter-specific overlap rule applies.
Referenced by: rfc-0097
Tested by
1// RFC-0097 §3: overlap between two bare-parameter blankets of the same local aspect
2// needs no new machinery -- it is caught by the same overlap detection RFC-0060 §2
3// already uses for named targets. `Copy` implies `Clone` (RFC-0080), so any `T: Copy`
4// also satisfies `T: Clone`, and these two blankets are not syntactically disjoint.
5
6aspect Marker {}
7
8extend<T: Copy> T: Marker {}
9extend<T: Clone> T: Marker {}
10
11fun main() {}
typecheck errorT0015at 9
Legality Rule №4
The bare-parameter rule applies only when the target is the parameter itself. Named and structural targets remain subject to their ordinary orphan-rule locality rules.
Referenced by: rfc-0097
Tested by
1// TYPECHECK_ERROR[orphan implementation]
2// RFC-0061: structural type constructors (T[], tuples, fun types) are owned by
3// std::core for orphan-rule purposes. A user module implementing a std::core
4// aspect (Display) for a structural target (T[]) owns neither half of the pair —
5// this must still be rejected as an orphan implementation, exactly as for a
6// nominal target, confirming issue #233's ImplBlock generalization didn't bypass
7// the existing coherence pass (issue #238) for the new structural-target shape.
8
9extend<T: Display> T[]: Display {
10 fun to_string(&self) -> String {
11 return "array";
12 }
13}
14
15fun main() {}
typecheck error“orphan implementation”
Structural Aspect Bounds
Arrays (T[]), tuples ((A, B), …), and function types (|A| -> B) are
structural types — built into the language rather than declared by a user, with no
name that can serve as an impl target the ordinary way. For the orphan rule (above),
structural type constructors are treated as belonging to std::core: a user module
may write extend T[]: Aspect only when Aspect itself is local to that module.
Blanket impls for structural constructors. std::core declares aspect impls for
structural constructors using the conditional extend syntax (above):
// std::core
extend<T: Display> T[]: Display {
fun to_string(self: &T[]) -> String { ... }
}
This is what makes println([1, 2, 3]) compile: [1, 2, 3] has type i64[];
i64: Display; the conditional extend block applies. Coherence for structural impl
targets follows the same rules as any other conditional block (above) — two impls of
the same aspect for T[] conflict (T0015) unless one directly negates a bound the
other requires.
Without a matching impl, a structural type fails an aspect bound with a diagnostic naming the constructor:
T0012: i64[] does not implement Display
hint: arrays implement Display only when their element type does;
no extend<T: Display> T[]: Display is registered
Standard array impls. std::core provides Display, Clone, and Eq for
arrays, each conditional on the element type satisfying the same bound (element-wise
to_string/join, element-wise clone into new backing storage, and element-wise
equality respectively). These cannot be overridden by user code (orphan rule).
List<T> is a separate nominal struct; its impls coexist independently of the array
impls. Ord (RFC-0062, still 0-draft) and Hash (not yet proposed) array impls are
not provided in this language version — neither aspect exists in std::core at all yet,
for arrays or otherwise.
T[]'s Clone implementation is now a view copy, not an element-wise cloneOnce T[] owns nothing, an element-wise clone into new backing storage is impossible:
Clone::clone(&self) -> Self must produce a T[], which can borrow only from storage that
already exists and outlives it, not from a buffer the implementation just allocated.
Display and Eq are unaffected because they return String and boolean, not Self.
Tuples are deferred pending a decision on per-arity boilerplate vs. variadic generics — until then, tuples fail aspect bounds the same way arrays do without a matching impl ((i64, String) does not implement Display, with a hint to use a named struct instead).
Function types. A plain function and a closure share one type, |A| -> B (see §First class functions) — there is no separate fun(A) -> B function-pointer type or syntax; fun(A) -> B is a parse error. Callable<A, B> does not exist in std::core yet — despite being referenced elsewhere as the aspect a function type would formally satisfy, writing a bound or extends Callable<A, B> against it is a compile error (T0003, unknown aspect) today. A |A| -> B value behaves like Copy under --move-check (reusing one after copying it into another binding is accepted), but there is no working Clone: .clone() on a |A| -> B receiver fails to typecheck (T0002, cannot infer receiver type) regardless of annotation. Display, Eq, Ord, Hash, Send, Sync, and Drop are not implemented for function types either — there is no canonical string form, function equality is undecidable in general, Send/Sync aren't implemented for any type yet (RFC-0080, still 1-under-review), and there is no state to drop.
Array auto-impl propagation. T[]: Send, T[]: Sync, and T[]: Drop are not
provided in this language version.
Formal rules
Legality Rule №1
Structural type constructors are owned by std::core for orphan-rule purposes; outside
std::core, an implementation for one is legal only when the aspect is local.
Referenced by: rfc-0061
Tested by (2)
1// TYPECHECK_ERROR[orphan implementation]
2// RFC-0061: structural type constructors (T[], tuples, fun types) are owned by
3// std::core for orphan-rule purposes. A user module implementing a std::core
4// aspect (Display) for a structural target (T[]) owns neither half of the pair —
5// this must still be rejected as an orphan implementation, exactly as for a
6// nominal target, confirming issue #233's ImplBlock generalization didn't bypass
7// the existing coherence pass (issue #238) for the new structural-target shape.
8
9extend<T: Display> T[]: Display {
10 fun to_string(&self) -> String {
11 return "array";
12 }
13}
14
15fun main() {}
typecheck error“orphan implementation”
1// RFC-0061 §2: "Coherence rules for structural impl targets follow RFC-0060
2// §2 and RFC-0036 §3.1 without special cases." This is the structural-target
3// counterpart of conditional_impl_negation_disjoint_accepted (which targets a
4// nominal struct): `impl<T: Copy3> ...` and `impl<T: !Copy3> ...` for the
5// same locally-declared aspect on the *same structural target* (`T[]`) are
6// provably disjoint via syntactic negation and must not be reported as a
7// coherence conflict. `Serialize3` is declared locally, so the orphan rule
8// permits a user module to implement it for `T[]` at all (RFC-0061 §1).
9
10aspect Copy3 {
11 fun dup(self) -> i64;
12}
13
14aspect Serialize3 {
15 fun ser(self) -> String;
16}
17
18extend<T: Copy3> T[]: Serialize3 {
19 fun ser(self) -> String { return "copyable"; }
20}
21
22extend<T: !Copy3> T[]: Serialize3 {
23 fun ser(self) -> String { return "not-copyable"; }
24}
25
26fun main() {}
passes
Legality Rule №2
std::core may declare conditional implementations for structural constructors; a
generic structural target is registered and dispatched subject to its stated bounds.
Referenced by: rfc-0061
Tested by (9)
1// Positive: `T[]: Display` is itself conditional on `T: Display` (RFC-0061
2// §2/§4), so it must apply recursively when `T` is itself an array -- an
3// array-of-arrays is Display as long as the innermost element type is.
4// Exercises `type_satisfies_aspect`'s `Type::Array` arm recursing through
5// `check_conditional_entry` on a nested `Type::Array` argument rather than a
6// `Named`/primitive one.
7
8fun main() {
9 let grid: [i64[]; 2] := [[1, 2], [3, 4]];
10 assert(grid.to_string() == "[[1, 2], [3, 4]]");
11}
passes
1aspect Area {
2 fun area(&self) -> i64;
3}
4
5// The generic form is what RFC-0061 implements, and it dispatches.
6extend<T> T[]: Area {
7 fun area(&self) -> i64 { return 7; }
8}
9
10fun main() {
11 let a: i64[] := [1, 2];
12 assert(a.area() == 7);
13}
passes
1// Negative: an array's conditional impl is gated on the element type, not
2// granted unconditionally to every array. `Opaque[]` does not implement
3// `Display` because `Opaque` itself does not (RFC-0061 §2-3) -- this is the
4// array counterpart to stage19_neg_01 (tuple) and stage19_neg_02 (function).
5
6struct Opaque {
7 value: i64,
8}
9
10fun show<T: Display>(x: T) -> String {
11 return x.to_string();
12}
13
14fun main() {
15 let items := [Opaque { value = 1 }, Opaque { value = 2 }];
16 let _ := show(items); // ERROR[T0012]
17}
typecheck errorT0012at 16
1aspect Area {
2 fun area(&self) -> i64;
3}
4
5// RFC-0061 grants structural impl targets, but only the generic form is
6// registered. A concrete one has nowhere to key on, so its methods could never
7// be found — rejected rather than accepted-and-unreachable (metel-core#581).
8extend i64[]: Area {
9 fun area(&self) -> i64 { return 1; }
10}
11
12fun main() {
13}
typecheck errorT0001“could never be found”
1aspect Area {
2 fun area(&self) -> i64;
3}
4
5// RFC-0061 grants structural impl targets, but only the generic array form is
6// registered. A concrete one has nowhere to key on, so its methods could never
7// be found — rejected rather than accepted-and-unreachable (metel-core#581).
8extend |i64| -> i64: Area {
9 fun area(&self) -> i64 { return 1; }
10}
11
12fun main() {
13}
typecheck errorT0001“could never be found”
1aspect Area {
2 fun area(&self) -> i64;
3}
4
5// RFC-0061 grants structural impl targets, but only the generic form is
6// registered. A concrete one has nowhere to key on, so its methods could never
7// be found — rejected rather than accepted-and-unreachable (metel-core#581).
8extend { w: i64, h: i64 }: Area {
9 fun area(&self) -> i64 { return 1; }
10}
11
12fun main() {
13}
typecheck errorT0001“could never be found”
1aspect Area {
2 fun area(&self) -> i64;
3}
4
5// RFC-0061 grants structural impl targets, but only the generic form is
6// registered. A concrete one has nowhere to key on, so its methods could never
7// be found — rejected rather than accepted-and-unreachable (metel-core#581).
8extend (i64, i64): Area {
9 fun area(&self) -> i64 { return 1; }
10}
11
12fun main() {
13}
typecheck errorT0001“could never be found”
1// RFC-0061 §2: "Coherence rules for structural impl targets follow RFC-0060
2// §2 and RFC-0036 §3.1 without special cases." This is the structural-target
3// counterpart of conditional_impl_negation_disjoint_accepted (which targets a
4// nominal struct): `impl<T: Copy3> ...` and `impl<T: !Copy3> ...` for the
5// same locally-declared aspect on the *same structural target* (`T[]`) are
6// provably disjoint via syntactic negation and must not be reported as a
7// coherence conflict. `Serialize3` is declared locally, so the orphan rule
8// permits a user module to implement it for `T[]` at all (RFC-0061 §1).
9
10aspect Copy3 {
11 fun dup(self) -> i64;
12}
13
14aspect Serialize3 {
15 fun ser(self) -> String;
16}
17
18extend<T: Copy3> T[]: Serialize3 {
19 fun ser(self) -> String { return "copyable"; }
20}
21
22extend<T: !Copy3> T[]: Serialize3 {
23 fun ser(self) -> String { return "not-copyable"; }
24}
25
26fun main() {}
passes
1// RFC-0081 + RFC-0061: a blanket negative impl on a structural target (`T[]`)
2// must override a blanket positive impl the same way it already does for a
3// named generic target (see generic_negative_impl_blocks_positive_bound) --
4// negative-impl priority over a blanket positive is a property of the
5// aspect/target pair, not something that should depend on whether the
6// target happens to be `Named` or structural.
7
8aspect Marker {
9 fun mark(self) -> String;
10}
11
12extend<T> T[]: Marker {
13 fun mark(self) -> String { return "blanket"; }
14}
15
16extend<T> T[]: !Marker;
17
18fun needs_marker<U: Marker>(x: U) {}
19
20fun main() {
21 let xs := [1, 2, 3];
22 needs_marker(xs); // ERROR[T0012]
23}
typecheck errorT0012at 22
Legality Rule №3
Without an applicable structural implementation, using a structural type where an aspect bound is required is rejected with T0012.
Referenced by: rfc-0061
Tested by
1// Negative: an array's conditional impl is gated on the element type, not
2// granted unconditionally to every array. `Opaque[]` does not implement
3// `Display` because `Opaque` itself does not (RFC-0061 §2-3) -- this is the
4// array counterpart to stage19_neg_01 (tuple) and stage19_neg_02 (function).
5
6struct Opaque {
7 value: i64,
8}
9
10fun show<T: Display>(x: T) -> String {
11 return x.to_string();
12}
13
14fun main() {
15 let items := [Opaque { value = 1 }, Opaque { value = 2 }];
16 let _ := show(items); // ERROR[T0012]
17}
typecheck errorT0012at 16
Legality Rule №4
std::core provides Display and Eq for T[] when T satisfies the same aspect;
these implementations cannot be overridden by user code.
Referenced by: rfc-0061
Tested by
1// Positive: `T[]: Display` is itself conditional on `T: Display` (RFC-0061
2// §2/§4), so it must apply recursively when `T` is itself an array -- an
3// array-of-arrays is Display as long as the innermost element type is.
4// Exercises `type_satisfies_aspect`'s `Type::Array` arm recursing through
5// `check_conditional_entry` on a nested `Type::Array` argument rather than a
6// `Named`/primitive one.
7
8fun main() {
9 let grid: [i64[]; 2] := [[1, 2], [3, 4]];
10 assert(grid.to_string() == "[[1, 2], [3, 4]]");
11}
passes
Legality Rule №5
Array marker-aspect propagation is not part of structural implementation lookup.
Referenced by: rfc-0061
Tested by (2)
1fun main() {
2 let xs := [1, 2, 3];
3 assert(xs.to_string() == "[1, 2, 3]");
4 assert([4, 5].to_string() == "[4, 5]");
5}
passes
1struct Box {
2 value: i64,
3}
4
5extend Box: Display {
6 fun to_string(&self) -> String {
7 return self.value.to_string();
8 }
9}
10
11extend Box: Clone {
12 fun clone(&self) -> Self {
13 return Box { value = self.value };
14 }
15}
16
17fun main() {
18 let xs := [Box { value = 1 }, Box { value = 2 }, Box { value = 3 }];
19 let ys := xs.clone();
20 assert(ys.to_string() == "[1, 2, 3]");
21}
passes
Legality Rule №6
Tuple types have no standard blanket aspect implementations and therefore fail aspect bounds unless a separately specified implementation applies.
Referenced by: rfc-0061
Tested by
1// Stage 3 regression: the postfix `[]` array suffix must bind to tuple types in
2// every annotation position, not only named types (METEL-191).
3
4struct PairTable {
5 rows: (String, i64)[],
6}
7
8fun keep(rows: (String, i64)[]) -> (String, i64)[] {
9 rows
10}
11
12let rows_for_table: (String, i64)[] := [("alpha", 1), ("beta", 2)];
13let table := PairTable { rows = keep(rows_for_table) };
14let name: &String := &table.rows[0].0;
15let value: i64 := table.rows[0].1;
passes
Legality Rule №7
Function values have the ordinary function type |A| -> B; there is no separate
function-pointer type.
Referenced by: rfc-0061
Tested by
1fun increment(value: i64) -> i64 { value + 1 }
2
3fun apply(f: |i64| -> i64) -> i64 { f(1) }
4
5fun main() {
6 let f := increment;
7 assert(apply(f) == 2);
8 assert(apply(f) == 2);
9}
passes
Legality Rule №8
Callable<A, B> is not available in std::core, so function types do not currently
satisfy a Callable<A, B> bound.
Referenced by: rfc-0061
Tested by
1fun increment(value: i64) -> i64 { value + 1 }
2
3fun apply(f: |i64| -> i64) -> i64 { f(1) }
4
5fun main() {
6 let f := increment;
7 assert(apply(f) == 2);
8 assert(apply(f) == 2);
9}
passes
Legality Rule №9
Function values are copyable for move checking, but do not satisfy aspect bounds such as
Copy or Clone.
Referenced by: rfc-0061
Tested by
1fun increment(value: i64) -> i64 { value + 1 }
2
3fun apply(f: |i64| -> i64) -> i64 { f(1) }
4
5fun main() {
6 let f := increment;
7 assert(apply(f) == 2);
8 assert(apply(f) == 2);
9}
passes
Legality Rule №10
Function types do not implement Display, Eq, Ord, Hash, Send, Sync, or Drop.
Referenced by: rfc-0061
Tested by
1// Negative: function pointers do not implement `Eq` -- function equality is
2// undecidable in general (RFC-0061 §7.3). Expect T0012, the function-type
3// sibling of stage19_neg_02 (which covers `Display`) for a different aspect
4// from the "aspects function pointers do not implement" table.
5
6fun show_eq<T: Eq>(a: T, b: T) -> boolean {
7 return a.eq(&b);
8}
9
10fun plus_one(x: i64) -> i64 {
11 return x + 1;
12}
13
14fun minus_one(x: i64) -> i64 {
15 return x - 1;
16}
17
18fun main() {
19 let _ := show_eq(plus_one, minus_one); // ERROR[T0012]
20}
typecheck errorT0012at 19
Legality Rule №11
Closures and plain functions share the same function type; captures distinguish closure values at runtime rather than introducing a distinct closure type.
Referenced by: rfc-0061
Tested by
1fun main() {
2 let s := "hello";
3 let f := [s] once || -> String { return s; };
4 let again := s;
5}
typecheck errorT0019“use of moved value `s`”
Associated Types
An aspect may declare an associated type — a type-level output that each
implementing type must specify — with type Name;. An extend block defines it with
type Name = ConcreteType;:
aspect Deref {
type Target;
fun deref(self: &Self) -> &Target;
}
struct Boxed { value: i64 }
extend Boxed: Deref {
type Target = i64;
fun deref(self: &Boxed) -> &i64 { &self.value }
}
Inside the aspect block, the bare name (Target) is sugar for Self::Target. A bound
may be declared on the associated type, constraining every impl:
aspect Collection {
type Item: Display;
}
Projection. In a generic context where T: Aspect, the associated type is written
T::AssocType:
fun deref_display<T: Deref>(x: &T) where T::Target: Display {
println(x.deref());
}
T::AssocType is only valid when T: Aspect is in scope — writing it without that
bound is a compile error.
Equality constraints in bounds. Aspect<AssocType = ConcreteType> asserts both that
T implements Aspect and that its associated type equals a known type, pinning
T::AssocType to ConcreteType at every use:
fun deref_to_i64<T: Deref<Target = i64>>(x: &T) -> &i64 {
x.deref()
}
ConcreteType doesn't have to be a fixed, known type — it can be a fresh type
parameter instead, which is also how disambiguation works (below).
Disambiguation. When T is bound to two or more aspects that each declare an
associated type of the same name, the bare projection is ambiguous — a hard error,
matching the existing method-name-collision rule (Static Dispatch Only, below):
aspect Deref { type Target; fun deref(self: &Self) -> &Target; }
aspect Convert { type Target; fun convert(self: &Self) -> Target; }
fun f<T: Deref + Convert>(x: &T) -> T::Target { ... }
// error: T::Target is ambiguous — both Deref and Convert declare Target
There's no dedicated disambiguation syntax for this — the equality constraint above already covers it, by binding the associated type to a fresh type parameter rather than a concrete one:
fun f<T: Deref<Target = U> + Convert, U>(x: &T) -> U {
x.deref() // ordinary method dispatch — deref and convert are different
// method names, so this was never ambiguous to begin with
}
U is used unambiguously everywhere afterward — return type, where clauses, let
bindings — with no projection syntax involved. In practice this covers the real cases:
code reaches an associated type by calling the aspect's own uniquely-named method, and
the bare-projection ambiguity only arises when a type needs naming abstractly without
going through a call, which the fresh-variable equality constraint already handles.
Associated type vs. a type parameter on the aspect. Use an associated type when the
implementing type determines exactly one output (Deref::Target — a type has one deref
target). Use a type parameter on the aspect itself when a type may implement it for
multiple type arguments simultaneously (e.g. From<i64> and From<String> on the same
type). Writing extend X: Deref<i64> {} and extend X: Deref<String> {} side by side
would be the wrong model for Deref specifically — one type has one dereference target,
not several.
Object safety. An aspect with associated types is object-safe only if no method
signature references the associated type directly (see Static Dispatch Only, below, and
dyn Aspect, deferred to a future release). Deref above is not object-safe — deref
returns &Target, which varies per implementor, and a vtable entry cannot encode a
type that differs per implementation.
Negative bounds on projections such as where T::Target: !Copy are not specified in
this language version.
Formal rules
Legality Rule №1
An associated type declared with type Name; is part of the aspect interface and must
be defined by each implementation of that aspect.
Referenced by: rfc-0082
Tested by
1// RFC-0082: `type Name: Bound;` inside an `aspect` block and `type Name = Concrete;`
2// inside its `impl` parse, construct, and dispatch through ordinary method calls
3// on a concrete (non-generic) receiver. See 74_projection_call_site_resolution.mtl
4// for real projection resolution (`T::AssocType`) through a generic function, and
5// stage13_04/stage13_10 in typechecking/aspects for equality-constrained bounds
6// (`Aspect<AssocType = Concrete>`) — both implemented as of issue #242.
7
8aspect Container {
9 type Item: Display;
10 fun get(&self) -> Item;
11}
12
13struct IntBox {
14 value: i64,
15}
16
17extend IntBox: Container {
18 type Item := i64;
19 fun get(&self) -> i64 {
20 return self.value;
21 }
22}
23
24fun main() {
25 let b := IntBox { value = 42 };
26 assert(b.get() == 42);
27}
passes
Legality Rule №2
If an associated-type declaration has a bound, the concrete type supplied by every implementation must satisfy that bound.
Referenced by: rfc-0082
Tested by
1// RFC-0082 §2: impl provides all associated types declared by the aspect.
2// §1.1: the concrete binding satisfies the declared bound.
3// Baseline positive test — should typecheck without errors.
4
5aspect Container {
6 type Item: Display;
7 fun get(&self) -> Item;
8}
9
10struct IntBox {
11 value: i64,
12}
13
14extend IntBox: Container {
15 type Item := i64;
16 fun get(&self) -> i64 {
17 return self.value;
18 }
19}
20
21fun main() {
22 assert(1 == 1);
23}
passes
Legality Rule №3
Within an aspect or its implementation, a bare associated-type name denotes the
corresponding Self::Name projection and may be used in method signatures.
Referenced by: rfc-0082
Tested by (4)
1// RFC-0082 §3: `T::AssocType` in a generic function's signature resolves to the
2// concrete associated type at each call site, including when the value is
3// produced by calling an aspect method (§1.2 bare-name sugar) inside the
4// generic function's own body. Exercises the real, end-to-end path: the
5// generic function's return type is a real projection placeholder that gets
6// backfilled to the concrete type at the call site, not just a
7// parses-without-crashing declaration (see stage13_01 in typechecking/aspects
8// for that narrower check).
9
10aspect Container {
11 type Item: Display;
12 fun get(self) -> Item;
13}
14
15struct IntBox {
16 value: i64,
17}
18
19extend IntBox: Container {
20 type Item := i64;
21 fun get(self) -> i64 {
22 return self.value;
23 }
24}
25
26fun peek<T: Container>(x: T) -> T::Item {
27 return x.get();
28}
29
30fun main() {
31 let b := IntBox { value = 42 };
32 let v: i64 := peek(b);
33 assert(v == 42);
34}
passes
1// RFC-0082 §1.2: a bare associated-type name inside the aspect's own default
2// method body/signature (sugar for `Self::AssocType`) resolves to the concrete
3// binding a specific impl gives, both for the default method's pre-registered
4// signature and for type-checking its actual body.
5
6aspect Container {
7 type Item: Display;
8 fun get(self) -> Item;
9 fun get_twice(self) -> Item {
10 return self.get();
11 }
12}
13
14struct IntBox {
15 value: i64,
16}
17
18extend IntBox: Container {
19 type Item := i64;
20 fun get(self) -> i64 {
21 return self.value;
22 }
23}
24
25fun main() {
26 let b := IntBox { value = 42 };
27 let v: i64 := b.get_twice();
28 assert(v == 42);
29}
passes
1// #740 part A: RFC-0082 §1.2 says the bare-name sugar (`Item` alone) is sugar for
2// `Self::Item` -- so the spelled-out form must resolve identically inside the same
3// impl-block method signature, not just the sugar. Exercises both the return type
4// and the signature-conformance check against the aspect's own (sugared) declaration.
5
6aspect Container {
7 type Item;
8 fun get(&self) -> Item;
9}
10
11struct Box1 { v: i64 }
12
13extend Box1: Container {
14 type Item := i64;
15 fun get(&self) -> Self::Item {
16 return self.v;
17 }
18}
19
20fun main() {
21 let b := Box1 { v = 7 };
22 assert(b.get() == 7);
23}
passes
1// #740/#774 architectural revision: `Self` is bound as an ordinary type parameter
2// (like a bound generic `T`) for the whole of an impl-block method's inference, not
3// just its own param/return-type resolution -- so a body-internal `let`/`mut`
4// annotation naming `Self::AssocType` resolves too, through the same general
5// mechanism every other generic-param-aware position already uses. Also covers the
6// same shape for an *ordinary* bound generic (`T::Item`), confirmed as a real,
7// pre-existing gap independent of `Self` while investigating this.
8
9aspect Container {
10 type Item;
11 fun get(&self) -> Item;
12}
13
14struct Box1 { v: i64 }
15
16extend Box1: Container {
17 type Item := i64;
18 fun get(&self) -> i64 {
19 let x: Self::Item := self.v;
20 return x;
21 }
22}
23
24fun unwrap<T: Container>(c: &T) -> i64 {
25 let x: T::Item := c.get();
26 return x;
27}
28
29fun main() {
30 let b := Box1 { v = 7 };
31 assert(b.get() == 7);
32 assert(unwrap(&b) == 7);
33}
passes
Legality Rule №4
An implementation must define every associated type declared by its aspect; its definition fixes that projection to the implementation's concrete type.
Referenced by: rfc-0082
Tested by (3)
1// RFC-0082: `type Name: Bound;` inside an `aspect` block and `type Name = Concrete;`
2// inside its `impl` parse, construct, and dispatch through ordinary method calls
3// on a concrete (non-generic) receiver. See 74_projection_call_site_resolution.mtl
4// for real projection resolution (`T::AssocType`) through a generic function, and
5// stage13_04/stage13_10 in typechecking/aspects for equality-constrained bounds
6// (`Aspect<AssocType = Concrete>`) — both implemented as of issue #242.
7
8aspect Container {
9 type Item: Display;
10 fun get(&self) -> Item;
11}
12
13struct IntBox {
14 value: i64,
15}
16
17extend IntBox: Container {
18 type Item := i64;
19 fun get(&self) -> i64 {
20 return self.value;
21 }
22}
23
24fun main() {
25 let b := IntBox { value = 42 };
26 assert(b.get() == 42);
27}
passes
1// RFC-0082 + RFC-0036: an aspect's associated type may be defined by a
2// *conditional/generic* impl (`extend<T: Bound> Wrapper<T>: Container { ... }`),
3// not only by an impl for a fully concrete type (every existing stage13
4// fixture uses a concrete `extend IntBox: Container { ... }`). The
5// projection resolves through the generic impl at both call sites and
6// inside the impl's own method body, so this checks it actually dispatches
7// correctly at runtime, not just that it type-checks.
8
9aspect Container {
10 type Item: Display;
11 fun get(&self) -> Item;
12}
13
14struct Wrapper<T> {
15 value: T,
16}
17
18extend<T: Display> Wrapper<T>: Container {
19 type Item := T;
20 fun get(&self) -> T {
21 return self.value;
22 }
23}
24
25fun show_item<C: Container>(c: C) -> String {
26 return c.get().to_string();
27}
28
29fun main() {
30 let w := Wrapper { value = 42 };
31 assert(show_item(w) == "42");
32}
passes
1// RFC-0082 §2: impl provides all associated types declared by the aspect.
2// §1.1: the concrete binding satisfies the declared bound.
3// Baseline positive test — should typecheck without errors.
4
5aspect Container {
6 type Item: Display;
7 fun get(&self) -> Item;
8}
9
10struct IntBox {
11 value: i64,
12}
13
14extend IntBox: Container {
15 type Item := i64;
16 fun get(&self) -> i64 {
17 return self.value;
18 }
19}
20
21fun main() {
22 assert(1 == 1);
23}
passes
Legality Rule №5
A projection T::AssocType is valid only when the required T: Aspect bound is in
scope, and resolves to that implementation's associated type at an instantiation.
Referenced by: rfc-0082
Tested by (5)
1// RFC-0082 §3: `T::AssocType` in a generic function's signature resolves to the
2// concrete associated type at each call site, including when the value is
3// produced by calling an aspect method (§1.2 bare-name sugar) inside the
4// generic function's own body. Exercises the real, end-to-end path: the
5// generic function's return type is a real projection placeholder that gets
6// backfilled to the concrete type at the call site, not just a
7// parses-without-crashing declaration (see stage13_01 in typechecking/aspects
8// for that narrower check).
9
10aspect Container {
11 type Item: Display;
12 fun get(self) -> Item;
13}
14
15struct IntBox {
16 value: i64,
17}
18
19extend IntBox: Container {
20 type Item := i64;
21 fun get(self) -> i64 {
22 return self.value;
23 }
24}
25
26fun peek<T: Container>(x: T) -> T::Item {
27 return x.get();
28}
29
30fun main() {
31 let b := IntBox { value = 42 };
32 let v: i64 := peek(b);
33 assert(v == 42);
34}
passes
1// RFC-0082 + RFC-0036: an aspect's associated type may be defined by a
2// *conditional/generic* impl (`extend<T: Bound> Wrapper<T>: Container { ... }`),
3// not only by an impl for a fully concrete type (every existing stage13
4// fixture uses a concrete `extend IntBox: Container { ... }`). The
5// projection resolves through the generic impl at both call sites and
6// inside the impl's own method body, so this checks it actually dispatches
7// correctly at runtime, not just that it type-checks.
8
9aspect Container {
10 type Item: Display;
11 fun get(&self) -> Item;
12}
13
14struct Wrapper<T> {
15 value: T,
16}
17
18extend<T: Display> Wrapper<T>: Container {
19 type Item := T;
20 fun get(&self) -> T {
21 return self.value;
22 }
23}
24
25fun show_item<C: Container>(c: C) -> String {
26 return c.get().to_string();
27}
28
29fun main() {
30 let w := Wrapper { value = 42 };
31 assert(show_item(w) == "42");
32}
passes
1// #740 part B: `T::AssocType` in a generic function's return position must produce a
2// concrete inferred type at a call site whose argument fully determines T, exactly
3// as it does when a caller happens to check the result against an expected type
4// (compare 74_projection_call_site_resolution.mtl, whose only call site is
5// annotated). Before the fix, the projection's placeholder was recorded in a log
6// window that started *after* the return-type annotation had already been resolved,
7// so the scheme's own `assoc_projections` came back empty and this failed with
8// "cannot infer type" even though the argument alone determines everything.
9
10aspect Container {
11 type Item;
12 fun get(&self) -> Item;
13}
14
15struct Box1 { v: i64 }
16
17extend Box1: Container {
18 type Item := i64;
19 fun get(&self) -> i64 {
20 return self.v;
21 }
22}
23
24fun unwrap<T: Container>(c: &T) -> T::Item {
25 return c.get();
26}
27
28fun main() {
29 let b := Box1 { v = 7 };
30 let r := unwrap(&b);
31 assert(r == 7);
32 assert(unwrap(&b) == 7);
33}
passes
1// #740/#774 architectural revision: `Self` is bound as an ordinary type parameter
2// (like a bound generic `T`) for the whole of an impl-block method's inference, not
3// just its own param/return-type resolution -- so a body-internal `let`/`mut`
4// annotation naming `Self::AssocType` resolves too, through the same general
5// mechanism every other generic-param-aware position already uses. Also covers the
6// same shape for an *ordinary* bound generic (`T::Item`), confirmed as a real,
7// pre-existing gap independent of `Self` while investigating this.
8
9aspect Container {
10 type Item;
11 fun get(&self) -> Item;
12}
13
14struct Box1 { v: i64 }
15
16extend Box1: Container {
17 type Item := i64;
18 fun get(&self) -> i64 {
19 let x: Self::Item := self.v;
20 return x;
21 }
22}
23
24fun unwrap<T: Container>(c: &T) -> i64 {
25 let x: T::Item := c.get();
26 return x;
27}
28
29fun main() {
30 let b := Box1 { v = 7 };
31 assert(b.get() == 7);
32 assert(unwrap(&b) == 7);
33}
passes
1// RFC-0082 SS3: `T::AssocType` in return-type position, where T is a bound generic
2// parameter, is recognized as a projection (not a plain dotted name) and typechecks
3// -- this declaration-only fixture never calls `peek`, so it doesn't exercise real
4// call-site resolution to a concrete type; see
5// evaluator/aspects/74_projection_call_site_resolution.mtl for that (issue #242).
6
7aspect Container {
8 type Item;
9}
10
11fun peek<T: Container>(x: &T) -> T::Item {
12 return panic("not needed for this fixture");
13}
14
15fun main() {
16 assert(1 == 1);
17}
passes
Legality Rule №6
A bare projection whose name is declared by more than one of T's bound aspects is
ambiguous and is rejected.
Referenced by: rfc-0082
Tested by
1aspect Deref {
2 type Target;
3 fun deref(&self) -> Target;
4}
5
6aspect Convert {
7 type Target;
8 fun convert(&self) -> Target;
9}
10
11fun ambiguous<T: Deref + Convert>(x: &T) -> T::Target {
12 x.deref()
13}
14
15fun main() {}
typecheck errorT0013at 11
Legality Rule №7
An equality constraint such as Aspect<AssocType = U> pins the associated type to its
right-hand type; a fresh type parameter may therefore name an otherwise ambiguous
associated type.
Referenced by: rfc-0082
Tested by (3)
1// Positive: RFC-0082 §4 equality constraint (`Aspect<AssocType = ConcreteType>`)
2// pins T::Item to a known concrete type; a call whose impl's Item really is
3// that type succeeds.
4
5aspect Container {
6 type Item: Display;
7 fun get(self) -> Item;
8}
9
10struct IntBox {
11 value: i64,
12}
13
14extend IntBox: Container {
15 type Item := i64;
16 fun get(self) -> i64 {
17 return self.value;
18 }
19}
20
21fun needs_int_item<T: Container<Item = i64>>(x: T) -> i64 {
22 return x.get();
23}
24
25fun main() {
26 let b := IntBox { value = 7 };
27 assert(needs_int_item(b) == 7);
28}
passes
1// Negative: RFC-0082 §4 equality constraint (`Aspect<AssocType = ConcreteType>`)
2// is violated when the impl's actual associated type does not match. Expect
3// T0012.
4
5aspect Container {
6 type Item: Display;
7 fun get(self) -> Item;
8}
9
10struct StrBox {
11 value: String,
12}
13
14extend StrBox: Container {
15 type Item := String;
16 fun get(self) -> String {
17 return self.value;
18 }
19}
20
21fun needs_int_item<T: Container<Item = i64>>(x: T) -> i64 {
22 return x.get();
23}
24
25fun main() {
26 let b := StrBox { value = "hi" };
27 let _ := needs_int_item(b); // ERROR[T0012]
28}
typecheck errorT0012at 27
1aspect Deref {
2 type Target;
3 fun deref(&self) -> Target;
4}
5
6aspect Convert {
7 type Target;
8 fun convert(&self) -> Target;
9}
10
11struct Number { value: i64 }
12
13extend Number: Deref {
14 type Target := i64;
15 fun deref(&self) -> i64 { self.value }
16}
17
18extend Number: Convert {
19 type Target := String;
20 fun convert(&self) -> String { "number" }
21}
22
23fun deref_value<T: Deref<Target = U> + Convert, U>(x: &T) -> U {
24 x.deref()
25}
26
27fun main() -> i64 {
28 let n := Number { value = 7 };
29 deref_value(&n)
30}
passes
Default Methods
An aspect method may supply a default bodyD1. An extend block may omit any method that
has a default; the aspect's implementation is inherited automatically.
aspect Greet {
fun name(self) -> String;
fun greet(self) -> String {
return "Hello, " + self.name();
}
}
struct Person {
name: String,
}
extend Person: Greet {
fun name(self) -> String {
return self.name;
}
// greet() is inherited from the aspect default
}
fun main() {
let p := Person { name = "Ada" };
println(p.greet()); // Hello, Ada
}
A method without a default body must be provided by every extend blockL1; omitting it
is a compile-time error.
Formal rules
Dynamic Semantics №1
An aspect method with a body is a default implementation. An implementing extend block
that omits it inherits and dispatches to that body.
Tested by
1// Positive: aspect default methods can be omitted in impl blocks and inherited.
2
3struct Person {
4 name: String,
5}
6
7aspect Greet {
8 fun name(self) -> String;
9
10 fun greet(self) -> String {
11 return "Hello, " + self.name();
12 }
13}
14
15extend Person: Greet {
16 fun name(self) -> String {
17 return self.name;
18 }
19}
20
21fun main() {
22 let p := Person { name = "Ada" };
23 let msg: String := p.greet();
24}
passes
Legality Rule №1
An implementing extend block must provide every aspect method that has no default body.
Tested by
1// Negative: a required method (no default body) must still be provided even when
2// the impl provides other methods that have defaults in the aspect.
3
4struct Widget {
5 id: i64,
6}
7
8aspect Render {
9 fun draw(self) -> String;
10
11 fun preview(self) -> String {
12 return "preview: " + self.draw();
13 }
14}
15
16extend Widget: Render { // ERROR[T0012]
17 fun preview(self) -> String {
18 return "overriding the default";
19 }
20}
typecheck errorT0012“does not implement”
The Self Type
Self inside an aspect or an extend block refers to the concrete implementing typeL1.
In an aspect definition, Self is the implementing type at the call site:
aspect Comparable {
fun compare(self, other: Self) -> i64;
}
In a struct or enum extend block, Self is an alias for the type being implemented:
struct Point {
x: i64,
}
extend Point {
fun clone(self) -> Self {
self
}
fun same_as(self, other: Self) -> boolean {
self.x == other.x
}
}
Formal rules
Legality Rule №1
Within an aspect declaration, Self denotes the type implementing that aspect. Within an
extend block, it denotes the block's target type.
Tested by
1// Stage 5: `Self` should resolve in ordinary struct and enum method signatures.
2
3struct Point {
4 x: i64,
5}
6
7extend Point {
8 fun clone(&self) -> Self {
9 Point { x = self.x }
10 }
11
12 fun same_as(&self, other: Self) -> boolean {
13 self.x == other.x
14 }
15}
16
17enum Token {
18 i64 { value: i64 },
19}
20
21extend Token {
22 fun identity(self) -> Self {
23 self
24 }
25}
26
27let p := Point { x = 1 };
28let q: Point := p.clone();
29let same: boolean := p.same_as(q);
30
31let token := Token::i64 { value = 7 };
32let token_copy: Token := token.identity();
passes
Aspect Bounds on Function Type Parameters
A generic function type parameter may declare an aspect bound using : syntax. The bound requires that any concrete type substituted for the parameter implements the named aspect. The named aspect must resolve where the declaration is written; an unknown aspect is error T0003, even when the generic function is never called. Passing a type that does not satisfy a resolved bound is error T0012, with the span on the offending call-site argument.
fun print_pair<T: Printable>(a: T, b: T) {
a.print();
b.print();
}
Inside the function body the typechecker treats T as having all methods declared by its bound aspects in scope. Calling a method not declared by any bound aspect on a bounded type parameter is a type error.
Multiple bounds — inline + or where clause (equivalent). Multiple bounds on a single type parameter may be expressed inline using +, or via a where clause, or a mix of both. The typechecker merges all declared bounds before enforcement — a type argument must satisfy every bound.
// Inline +
fun process<T: Comparable + Printable>(x: T) { ... }
// where clause (equivalent)
fun process<T>(x: T) where T: Comparable + Printable { ... }
// Mix — inline single bound plus additional where clause bound (also valid)
fun process<T: Comparable>(x: T) where T: Printable { ... }
All three forms above have identical semantics. The recommended style is inline + for short bound lists and where clause for longer or multi-parameter constraints.
extends Aspect shorthand. For type parameters used only once in a signature and not referenced elsewhere, the anonymous shorthand extends Aspect may be used directly in parameter position:
fun print_all(items: extends Printable[]) { ... }
// equivalent to:
fun print_all<_T: Printable>(items: _T[]) { ... }
Each extends Aspect occurrence in a signature is a fresh, independent type variable. To constrain two parameters to the same type, use a named type parameter.
Return-position extends Aspect. A function may return extends Aspect instead of a named type. The caller sees an opaque type known only to satisfy Aspect — no boxing, no heap allocation, no vtable, since the concrete type is fixed by the function's own body:
aspect Printable {
fun print(self);
}
struct Adder { n: i64 }
extend Adder: Printable {
fun print(self) {
println("adds ${self.n}");
}
}
fun make_adder(n: i64) -> extends Printable {
Adder { n = n }
}
let add5 := make_adder(5);
add5.print(); // adds 5 — printable, but its concrete type is not nameable
A function returning extends Aspect must produce the same concrete type on every code path — the compiler resolves one fixed type per function definition, not per call:
fun bad(flag: boolean) -> extends Display {
if (flag) { 42 } else { "hello" } // error: branches return different concrete types
}
Two calls to the same function return values of the same opaque type; two different extends Aspect-returning functions never share an opaque type even if their concrete implementations coincide. Each occurrence of extends Aspect in a signature is independent (as in parameter position, above) — a function with both an extends Aspect parameter and return type may return the parameter directly, in which case ordinary type inference unifies the two independent type variables:
fun transform(x: extends Display) -> extends Display {
x // return type inferred to be the same concrete type as x's
}
The caller may call any method the declared aspect provides, store the value, and pass it to anything accepting the same opaque type or aspect bound — but may not name the concrete type, cast it, or call methods outside the aspect even if the concrete type has them. Ownership (ownership/Copy/Drop, not yet integrated — RFC-0071) applies to the concrete type normally; the caller cannot observe which impls it has beyond the declared aspect bound.
Worked example — interaction with associated types. A function may return extends Aspect where Aspect declares an associated type; the caller can still use the aspect's own methods to produce values of that associated type, and those values type-check normally, even though the caller cannot name the opaque type itself:
aspect Container { type Item: Display; fun get(self) -> Item; }
struct IntBox { value: i64 }
extend IntBox: Container { type Item := i64; fun get(self) -> i64 { self.value } }
fun make_box(n: i64) -> extends Container {
IntBox { value = n }
}
let v: i64 := make_box(42).get(); // resolves through Container's Item binding for
// IntBox, the same associated-type mechanism
// Associated Types (above) specifies -- the
// caller never names IntBox, only Container.
This composes for free: the opaque return type is a real concrete type internally (erased only from the caller's naming surface, not from the typechecker's own bookkeeping), so associated-type resolution runs exactly as it does for a named type.
extends Aspect in struct fields, aspect aliases, named linkage between an extends Aspect
parameter and return type, and multiple aspect bounds in return position are not part
of this language version.
Formal rules
Legality Rule №1
In a function parameter type, extends Aspect introduces an anonymous type parameter that
must satisfy Aspect.
Referenced by: rfc-0035, rfc-0130
Tested by (2)
1// RFC-0130: the anonymous type-position keyword is `extends Aspect`. The old
2// `impl Aspect` spelling is a hard parse error (`impl` stays reserved, like
3// `mut`/`pub` after RFC-0098) -- no compatibility alias.
4fun print_it(x: impl Display) -> String {
5 "${x}"
6}
7
8fun main() {}
parse errorP0001
1// Positive: `extends Aspect` in parameter position desugars to a fresh type param.
2
3aspect Printable {
4 fun print(self);
5}
6
7struct Label {
8 text: String,
9}
10
11extend Label: Printable {
12 fun print(self) {}
13}
14
15fun print_it(x: extends Printable) {
16 x.print()
17}
18
19fun run(label: Label) {
20 print_it(label)
21}
passes
Legality Rule №2
Each parameter-position extends Aspect occurrence introduces an independent anonymous type
parameter. Reusing one concrete type across parameters requires a named type parameter.
Referenced by: rfc-0035
Tested by
1// Positive: two `extends Aspect` params in one signature are independent type variables.
2// The function can be called with two different types that both satisfy the bound.
3
4aspect Printable {
5 fun print(self);
6}
7
8struct A { value: i64 }
9struct B { value: String }
10
11extend A: Printable { fun print(self) {} }
12extend B: Printable { fun print(self) {} }
13
14fun print_both(x: extends Printable, y: extends Printable) {
15 x.print();
16 y.print()
17}
18
19fun run(a: A, b: B) {
20 print_both(a, b)
21}
passes
Legality Rule №3
Anonymous extends Aspect parameter types may coexist with named type parameters; neither
constrains the other unless the signature states a relation between them.
Referenced by: rfc-0035
Tested by
1// Positive: extends Aspect param coexists with a where clause on a named type param.
2// Both are enforced independently.
3
4aspect Printable {
5 fun print(self);
6}
7
8aspect Serializable {
9 fun serialize(self) -> String;
10}
11
12struct A { value: i64 }
13struct B { name: String }
14
15extend A: Printable { fun print(self) {} }
16extend B: Serializable {
17 fun serialize(self) -> String { self.name }
18}
19
20fun process<U>(x: extends Printable, y: U) -> String where U: Serializable {
21 x.print();
22 y.serialize()
23}
24
25fun run(a: A, b: B) -> String {
26 process(a, b)
27}
passes
Legality Rule №4
Every argument passed to an extends Aspect parameter must implement the declared aspect;
an argument that does not is a T0012 type error.
Referenced by: rfc-0035
Tested by
1// Negative: `extends Aspect` param called with a type that does NOT implement
2// the required aspect. Expect T0012.
3
4aspect Printable {
5 fun print(self);
6}
7
8struct Plain { x: i64 }
9
10// Plain does not implement Printable.
11
12fun print_it(x: extends Printable) {
13 x.print()
14}
15
16fun bad(p: Plain) {
17 print_it(p) // ERROR[T0012]
18}
typecheck errorT0012at 17
Legality Rule №5
extends Aspect is rejected in a struct-field type annotation.
Referenced by: rfc-0035
Tested by
1// Negative: `extends Aspect` is not permitted in a struct field type.
2aspect P { fun p(&self); }
3
4struct Holder {
5 values: extends P[], // ERROR[T0022]
6}
7
8fun main() {}
typecheck errorT0022at 5
Legality Rule №6
extends Aspect is rejected in a local binding type annotation.
Referenced by: rfc-0035
Tested by
1// Negative: `extends Aspect` is not permitted inside a local let annotation.
2aspect P { fun p(&self); }
3
4struct L { t: String }
5
6extend L: P { fun p(&self) {} }
7
8fun main() {
9 let values: extends P[] := [L { t = "x" }]; // ERROR[T0022]
10}
typecheck errorT0022at 9
Legality Rule №7
At a generic-function call, each concrete type argument must satisfy every declared aspect bound; inferred type arguments are checked by the same rule.
Referenced by: rfc-0040
Tested by (2)
1// Positive: generic function with inline bound, called with a satisfying type.
2
3aspect Printable {
4 fun print(self);
5}
6
7struct Label {
8 text: String,
9}
10
11extend Label: Printable {
12 fun print(self) {}
13}
14
15fun print_it<T: Printable>(x: T) {
16 x.print()
17}
18
19fun run(label: Label) {
20 print_it(label)
21}
passes
1// Negative: generic function called with a type that does NOT implement
2// the required bound. Expect T0012.
3
4aspect Printable {
5 fun print(self);
6}
7
8struct Plain { x: i64 }
9
10// Plain does not implement Printable.
11
12fun print_it<T: Printable>(x: T) {
13 x.print()
14}
15
16fun bad(p: Plain) {
17 print_it(p) // ERROR[T0012]
18}
typecheck errorT0012at 17:13
Legality Rule №8
A type argument that does not satisfy a function type parameter's aspect bound is a T0012 error reported at the offending call-site argument.
Referenced by: rfc-0040
Tested by
1// Negative: generic function called with a type that does NOT implement
2// the required bound. Expect T0012.
3
4aspect Printable {
5 fun print(self);
6}
7
8struct Plain { x: i64 }
9
10// Plain does not implement Printable.
11
12fun print_it<T: Printable>(x: T) {
13 x.print()
14}
15
16fun bad(p: Plain) {
17 print_it(p) // ERROR[T0012]
18}
typecheck errorT0012at 17:13
Legality Rule №9
Within a generic function body, a bounded type parameter has the methods declared by each of its bound aspects available; methods outside those bounds are rejected.
Referenced by: rfc-0040
Tested by
1// Positive: function body can call methods from both inline and where clause bounds
2// on the same type parameter.
3
4aspect Printable {
5 fun print(&self);
6}
7
8aspect Serializable {
9 fun serialize(&self) -> String;
10}
11
12struct Record {
13 name: String,
14}
15
16extend Record: Printable {
17 fun print(&self) {}
18}
19
20extend Record: Serializable {
21 fun serialize(&self) -> String {
22 // Build a fresh String; returning self.name would move through shared self.
23 self.name + ""
24 }
25}
26
27fun process<T: Printable>(x: T) -> String where T: Serializable {
28 x.print();
29 x.serialize()
30}
31
32fun run(r: Record) -> String {
33 process(r)
34}
passes
Legality Rule №10
Inline + bounds, where-clause bounds, and a combination of the two have identical
semantics after their bounds are merged for each type parameter.
Referenced by: rfc-0040
Tested by
1// Positive: function body can call methods from both inline and where clause bounds
2// on the same type parameter.
3
4aspect Printable {
5 fun print(&self);
6}
7
8aspect Serializable {
9 fun serialize(&self) -> String;
10}
11
12struct Record {
13 name: String,
14}
15
16extend Record: Printable {
17 fun print(&self) {}
18}
19
20extend Record: Serializable {
21 fun serialize(&self) -> String {
22 // Build a fresh String; returning self.name would move through shared self.
23 self.name + ""
24 }
25}
26
27fun process<T: Printable>(x: T) -> String where T: Serializable {
28 x.print();
29 x.serialize()
30}
31
32fun run(r: Record) -> String {
33 process(r)
34}
passes
Legality Rule №11
Every bound in a multiple-bound list is independently required at a call site.
Referenced by: rfc-0040
Tested by
1// Positive: function body can call methods from both inline and where clause bounds
2// on the same type parameter.
3
4aspect Printable {
5 fun print(&self);
6}
7
8aspect Serializable {
9 fun serialize(&self) -> String;
10}
11
12struct Record {
13 name: String,
14}
15
16extend Record: Printable {
17 fun print(&self) {}
18}
19
20extend Record: Serializable {
21 fun serialize(&self) -> String {
22 // Build a fresh String; returning self.name would move through shared self.
23 self.name + ""
24 }
25}
26
27fun process<T: Printable>(x: T) -> String where T: Serializable {
28 x.print();
29 x.serialize()
30}
31
32fun run(r: Record) -> String {
33 process(r)
34}
passes
Legality Rule №12
The bound checks for a parameter introduced by extends Aspect are the same as for an
equivalent named type parameter.
Referenced by: rfc-0040
Tested by
1// Positive: extends Aspect param coexists with a where clause on a named type param.
2// Both are enforced independently.
3
4aspect Printable {
5 fun print(self);
6}
7
8aspect Serializable {
9 fun serialize(self) -> String;
10}
11
12struct A { value: i64 }
13struct B { name: String }
14
15extend A: Printable { fun print(self) {} }
16extend B: Serializable {
17 fun serialize(self) -> String { self.name }
18}
19
20fun process<U>(x: extends Printable, y: U) -> String where U: Serializable {
21 x.print();
22 y.serialize()
23}
24
25fun run(a: A, b: B) -> String {
26 process(a, b)
27}
passes
Legality Rule №13
Generic methods in an extend block enforce their own bounds, while bounds on the
enclosing type remain available in the method body.
Referenced by: rfc-0040
Tested by
1// Regression test for issue #746: a generic method's *own* bound, declared
2// inside an `extend` block, was not recognized for aspect-method dispatch --
3// neither when the target struct has no generics of its own
4// (`extend Foo { fun describe<U: Aspect>(...) }`) nor when the method's own
5// bound is combined with the struct's own bound (RFC-0040 §7: "Bounds on
6// generic functions defined inside impl blocks are enforced with the same
7// rules [as free functions]... the impl block's own type parameter bounds
8// are in scope and do not need to be re-declared on individual methods").
9aspect Comparable {
10 fun compare(self, other: Self) -> i64;
11}
12
13aspect Display2 {
14 fun show(self) -> String;
15}
16
17struct SortedList<T: Comparable> {
18 items: T[],
19}
20
21struct Num { value: i64 }
22
23extend Num: Comparable {
24 fun compare(self, other: Num) -> i64 { self.value - other.value }
25}
26
27extend Num: Display2 {
28 fun show(self) -> String { "num" }
29}
30
31extend SortedList<T> {
32 // `T: Comparable` comes from the struct; `U: Display2` is this
33 // method's own additional bound.
34 fun describe<U: Display2>(self, item: T, other: T, label: U) -> String {
35 let cmp := item.compare(other);
36 label.show()
37 }
38}
39
40// A method's own bound on an otherwise non-generic target -- the specific
41// shape #746 was filed against.
42struct Foo { x: i64 }
43
44extend Foo {
45 fun describe<U: Display2>(self, label: U) -> String {
46 label.show()
47 }
48}
49
50fun main() {
51 let list := SortedList { items = [Num { value = 1 }] };
52 let n := Num { value = 42 };
53 assert(list.describe(Num { value = 1 }, Num { value = 2 }, n) == "num");
54
55 let f := Foo { x = 1 };
56 assert(f.describe(n) == "num");
57}
passes
Legality Rule №14
A return-position extends Aspect has one concrete type for every path through its function
body; branches that produce different concrete types are rejected.
Referenced by: rfc-0037
Tested by
1// Negative: divergent branches should fail with T0001
2// RFC §1.1 example - function with different concrete types on different branches
3
4aspect Display {
5 fun display(&self) -> String;
6}
7
8struct MyInt {
9 value: i64,
10}
11
12struct MyString {
13 value: String,
14}
15
16extend MyInt: Display {
17 fun display(&self) -> String {
18 self.value.to_string()
19 }
20}
21
22extend MyString: Display {
23 fun display(&self) -> String {
24 self.value.clone()
25 }
26}
27
28// RFC §1.1 - divergent branches returning different concrete types
29fun bad(flag: boolean) -> extends Display {
30 if (flag) { // ERROR[T0001]
31 MyInt { value = 42 }
32 } else {
33 MyString { value = "hello".to_string() }
34 }
35}
36
37fun main() {
38 let result := bad(true);
39}
typecheck errorT0001at 30
Legality Rule №15
Each return-position extends Aspect occurrence is an independent opaque type. An
extends Aspect return may be inferred equal to an extends Aspect parameter when the body
returns that parameter directly.
Referenced by: rfc-0037
Tested by (2)
1// Positive: linked case where return position is linked to parameter position
2// RFC §2 transform example - caller should be able to name the concrete type
3
4aspect Display {
5 fun display(&self) -> String;
6}
7
8struct MyInt {
9 value: i64,
10}
11
12extend MyInt: Display {
13 fun display(&self) -> String {
14 self.value.to_string()
15 }
16}
17
18// RFC §2 transform example - return position linked to parameter
19fun transform(x: extends Display) -> extends Display {
20 x // returns the same value passed in
21}
22
23fun main() {
24 let original := MyInt { value = 42 };
25 let result := transform(original);
26
27 // In the linked case, the caller should be able to name the concrete type
28 let concrete: MyInt := result;
29 // This should be allowed because the return is linked to the parameter
30}
passes
1// Positive: tuple with independent extends Aspect positions
2// RFC §1.3 example - both positions should work independently
3
4aspect Display {
5 fun display(&self) -> String;
6}
7
8struct MyInt {
9 value: i64,
10}
11
12struct MyString {
13 value: String,
14}
15
16extend MyInt: Display {
17 fun display(&self) -> String {
18 self.value.to_string()
19 }
20}
21
22extend MyString: Display {
23 fun display(&self) -> String {
24 self.value.clone()
25 }
26}
27
28// RFC §1.3 example - tuple with two independent extends Display positions
29fun pair() -> (extends Display, extends Display) {
30 (MyInt { value = 42 }, MyString { value = "hello".to_string() })
31}
32
33fun main() {
34 let both := pair();
35
36 // Both positions should work independently
37 let int_display := both.0.display();
38 let str_display := both.1.display();
39}
passes
Legality Rule №16
A caller may use only the declared aspect interface of a return-position extends Aspect;
the caller may not name or cast its hidden concrete type.
Referenced by: rfc-0037
Tested by
1// Negative: caller cannot name concrete type should fail with T0018
2// Attempting to assign opaque return to concrete type variable
3
4aspect Display {
5 fun display(&self) -> String;
6}
7
8struct MyInt {
9 value: i64,
10}
11
12extend MyInt: Display {
13 fun display(&self) -> String {
14 self.value.to_string()
15 }
16}
17
18// Function returning extends Display
19fun make_int() -> extends Display {
20 MyInt { value = 42 }
21}
22
23fun main() {
24 let int_val := make_int();
25 // This should fail - cannot name the concrete type of opaque return
26 let concrete: MyInt := int_val; // ERROR[T0018]
27}
typecheck errorT0018at 26
Dynamic Semantics №1
Calls to the same extends Aspect-returning function produce values of the same opaque
type, and aspect methods declared for that return bound dispatch on those values.
Referenced by: rfc-0037
Tested by (2)
1// Positive: basic return-position extends Aspect without method calls
2// Should typecheck without any method calls
3
4aspect Display {
5 fun display(&self) -> String;
6}
7
8struct MyInt {
9 value: i64,
10}
11
12extend MyInt: Display {
13 fun display(&self) -> String {
14 self.value.to_string()
15 }
16}
17
18// Basic function returning extends Display - no method calls yet
19fun make_pair() -> extends Display {
20 MyInt { value = 42 }
21}
22
23fun main() {
24 let pair := make_pair();
25 // Don't call any methods - just use the value as extends Display
26 // This should typecheck without errors
27}
passes
1// Positive: method dispatch on return-position extends Aspect
2// Should typecheck and allow calling aspect methods
3
4aspect Display {
5 fun display(&self) -> String;
6}
7
8struct MyInt {
9 value: i64,
10}
11
12extend MyInt: Display {
13 fun display(&self) -> String {
14 self.value.to_string()
15 }
16}
17
18// Function returning extends Display
19fun make_int() -> extends Display {
20 MyInt { value = 42 }
21}
22
23fun main() {
24 let int_val := make_int();
25 // Call the aspect method - this should work now with Step 4
26 let s := int_val.display();
27 // We can't assert the result here since this is just typechecking,
28 // but the typecheck should pass
29}
passes
Dynamic Semantics №2
Return-position extends Aspect values follow the ordinary ownership behavior of their
concrete type; opacity changes what callers can name, not the value's ownership.
Referenced by: rfc-0037
Tested by
1// Positive: basic return-position extends Aspect without method calls
2// Should typecheck without any method calls
3
4aspect Display {
5 fun display(&self) -> String;
6}
7
8struct MyInt {
9 value: i64,
10}
11
12extend MyInt: Display {
13 fun display(&self) -> String {
14 self.value.to_string()
15 }
16}
17
18// Basic function returning extends Display - no method calls yet
19fun make_pair() -> extends Display {
20 MyInt { value = 42 }
21}
22
23fun main() {
24 let pair := make_pair();
25 // Don't call any methods - just use the value as extends Display
26 // This should typecheck without errors
27}
passes
Negative Bounds
T: !Aspect is the complement of T: Aspect: it asserts that T does not
implement the named aspect. ! binds tightly to the aspect name — T: !Drop + Clone
reads as T: (!Drop) + Clone — and positive and negative bounds may mix freely, inline
or in a where clause, on the same terms as ordinary bounds above.
fun move_out<T: !Drop>(value: T) -> T { ... }
Satisfaction. For a concrete type, T: !Aspect is satisfied exactly when no
implementation of Aspect for T is reachable — the same lookup used for a positive
bound, inverted. In a generic context, absence of a bound does not imply satisfaction:
a function requiring T: !Drop must declare it, since the type parameter could still be
instantiated with a Drop-implementing type otherwise.
Copy implies !Drop. Since Copy and Drop are mutually exclusive (see
Ownership, not yet integrated — RFC-0071), any type satisfying T: Copy automatically
satisfies T: !Drop, derived without an explicit declaration.
Compound types. T: !Drop is a claim about T itself, not its fields — a struct
with Drop-implementing fields but no impl Drop of its own satisfies !Drop; its
fields still drop normally through the ordinary per-field chain.
Negative bounds do not by themselves let a type opt out of an aspect an existing blanket impl would otherwise grant — that's Negative Impls, directly below. Negative bounds are a use-site constraint; negative impls are a definition-site declaration that affects what the negative-bound check finds. See Aspect Implementation Coherence, above, for exactly which impls are reachable in the first place.
Formal rules
Legality Rule №1
A negative bound is written T: !Aspect and may appear wherever a positive aspect bound
may appear; it binds tightly to the aspect name.
Referenced by: rfc-0072
Tested by
1// RFC-0072: `T: !Drop`-style negative bounds parse and mix with positive bounds.
2// Issue #233's scope is representation only — satisfaction checking (rejecting a
3// T that DOES implement the negated aspect) is issue #243's job, not this one's.
4// This fixture is enforced since #243 landed; it passes because `i64` has no
5// registered `Drop` impl, so the negative bound is satisfied.
6
7fun move_out<T: !Drop>(x: T) -> T {
8 return x;
9}
10
11fun mixed<T: Display + !Drop>(x: T) -> T {
12 return x;
13}
14
15fun main() {
16 assert(move_out(5) == 5);
17 assert(mixed(7) == 7);
18}
passes
Legality Rule №2
T: !Aspect is satisfied precisely when no reachable positive implementation of Aspect
applies to T.
Referenced by: rfc-0072
Tested by
1// RFC-0072: `T: !Drop`-style negative bounds parse and mix with positive bounds.
2// Issue #233's scope is representation only — satisfaction checking (rejecting a
3// T that DOES implement the negated aspect) is issue #243's job, not this one's.
4// This fixture is enforced since #243 landed; it passes because `i64` has no
5// registered `Drop` impl, so the negative bound is satisfied.
6
7fun move_out<T: !Drop>(x: T) -> T {
8 return x;
9}
10
11fun mixed<T: Display + !Drop>(x: T) -> T {
12 return x;
13}
14
15fun main() {
16 assert(move_out(5) == 5);
17 assert(mixed(7) == 7);
18}
passes
Legality Rule №3
For a concrete type, negative-bound satisfaction is determined by the reachable implementations of the negated aspect.
Referenced by: rfc-0072
Tested by
1// RFC-0072: `T: !Drop`-style negative bounds parse and mix with positive bounds.
2// Issue #233's scope is representation only — satisfaction checking (rejecting a
3// T that DOES implement the negated aspect) is issue #243's job, not this one's.
4// This fixture is enforced since #243 landed; it passes because `i64` has no
5// registered `Drop` impl, so the negative bound is satisfied.
6
7fun move_out<T: !Drop>(x: T) -> T {
8 return x;
9}
10
11fun mixed<T: Display + !Drop>(x: T) -> T {
12 return x;
13}
14
15fun main() {
16 assert(move_out(5) == 5);
17 assert(mixed(7) == 7);
18}
passes
Legality Rule №4
A generic type parameter does not satisfy a negative bound unless that bound is stated and its eventual instantiation satisfies it.
Referenced by: rfc-0072
Tested by
1// Negative: generic function with negative bound, called with a type that
2// DOES implement the negated aspect. Expect T0012.
3
4aspect Drop {
5 fun drop(self);
6}
7
8struct Resource { x: i64 }
9
10extend Resource: Drop {
11 fun drop(self) {}
12}
13
14fun move_out<T: !Drop>(x: T) -> T {
15 return x;
16}
17
18fun bad(r: Resource) {
19 move_out(r) // ERROR[T0012]
20}
typecheck errorT0012at 19:13“bound not satisfied”
Legality Rule №5
Every type satisfying Copy also satisfies !Drop; no type may satisfy both Copy and
Drop.
Referenced by: rfc-0072
Tested by
1// Positive: a `Copy` type satisfies a `!Drop` bound, concretely and through a
2// `T: Copy` generic.
3//
4// NOTE ON WHAT THIS ASSERTS. RFC-0072 §2.3 frames this as an *implication* —
5// `T: Copy` derives `T: !Drop`. Before `Copy`/`Drop` were real aspects, this fixture
6// tested it by giving one type both and relying on the override; that exact program is
7// now rejected by RFC-0071 §4's declaration-site mutual exclusion (stage5_neg_33).
8//
9// An earlier revision of this comment claimed the implication had therefore become
10// structurally guaranteed and untestable. **That was wrong**, and an adversarial review
11// disproved it (#302): §4 was enforced only for *concrete* impl targets, so two
12// overlapping conditional impls — `extend<T: Copy> Foo<T>: Copy` and
13// `extend<T: Display> Foo<T>: Drop` — gave `Foo<i64>` both, and the implication was
14// what let it through a `!Drop` bound.
15//
16// That hole is closed: `coherence` now checks the two aspects against each other for
17// open targets too. The implication is still not *structurally* guaranteed — it holds
18// because two separate checks enforce §4, not because the type system makes the
19// combination unrepresentable — so it stays worth testing. See
20// `typechecking/structs/stage5_neg_34_copy_and_drop_overlapping_conditional_impls.mtl`
21// for the case that used to slip through.
22//
23// What this fixture covers is the ordinary path only: a `Copy` type satisfies `!Drop`,
24// concretely and through a `T: Copy` generic. It does not isolate the implication —
25// `Both` has no `Drop` impl, so `!Drop` would hold regardless.
26
27struct Both {
28 x: i64,
29}
30
31extend Both: Copy;
32
33fun needs_no_drop<T: !Drop>(x: T) -> T {
34 return x;
35}
36
37// Forwarding through a `T: Copy` parameter: at instantiation `T` is `Both`, which
38// cannot implement `Drop`, so the `!Drop` bound holds.
39fun via_copy<T: Copy>(x: T) -> T {
40 return needs_no_drop(x);
41}
42
43fun main() {
44 let b := Both { x = 1 };
45 let direct := needs_no_drop(b);
46 assert(direct.x == 1);
47
48 let through_generic := via_copy(b);
49 assert(through_generic.x == 1);
50}
passes
Legality Rule №6
T: !Drop concerns T's own Drop implementation, not whether any of its fields
implement Drop.
Referenced by: rfc-0072
Tested by
1// RFC-0072 §2.4: `!Drop` concerns the compound type's own impl, not the
2// impls of its fields. `Handle` implements Drop; `Wrapper` deliberately does not.
3aspect Drop { fun drop(self); }
4
5struct Handle { fd: i64 }
6extend Handle: Drop { fun drop(self) {} }
7
8struct Wrapper { inner: Handle }
9
10fun needs_no_drop<T: !Drop>(value: T) {}
11
12fun main() {
13 needs_no_drop(Wrapper { inner = Handle { fd = 3 } });
14}
passes
Legality Rule №7
Negative bounds are permitted in where clauses and are equivalent there to inline
negative bounds.
Referenced by: rfc-0072
Tested by
1// Positive: `where T: !Drop` spelling (RFC-0072 §3) is equivalent to the
2// inline `<T: !Drop>` form.
3
4aspect Drop {
5 fun drop(self);
6}
7
8struct Clean { x: i64 }
9
10// Clean does NOT implement Drop.
11
12fun move_out<T>(x: T) -> T where T: !Drop {
13 return x;
14}
15
16fun run(c: Clean) {
17 move_out(c)
18}
passes
Legality Rule №8
A negative bound on a conditional implementation is checked at each instantiation on the same terms as a positive conditional bound.
Referenced by: rfc-0072
Tested by
1// Test that a negative bound (!Drop) correctly rejects when the type DOES implement Drop via conditional impl
2aspect Drop { fun drop(self); }
3struct Arena<T: !Drop> { items: T[] }
4struct Pair<A, B> { first: A, second: B }
5// Pair implements Drop when BOTH A and B implement Drop
6extend<A: Drop, B: Drop> Pair<A, B>: Drop { fun drop(self) {} }
7struct Resource { x: i64 }
8extend Resource: Drop { fun drop(self) {} }
9
10// This should FAIL: Arena<Pair<Resource, Resource>> should be rejected because
11// Pair<Resource, Resource> DOES implement Drop via the conditional impl,
12// violating the !Drop bound. Before the fix, this would incorrectly succeed.
13fun bad(r: Resource) -> Arena<Pair<Resource, Resource>> {
14 Arena { items = [Pair { first = r, second = r }] } // ERROR[T0012]
15}
16
17fun main() {}
typecheck errorT0012at 14:5“bound not satisfied”
Legality Rule №9
Negative bounds are use-site constraints and do not themselves declare that a type lacks an aspect implementation.
Referenced by: rfc-0072
Tested by
1// RFC-0081's motivating generic form: a blanket negative impl must satisfy a
2// `!Aspect` bound even when a blanket positive impl for the same target head
3// also exists. The negative impl wins for every `Foo<T>` instantiation.
4
5aspect Marker {
6 fun mark(self) -> String;
7}
8
9struct Foo<T> {
10 value: T,
11}
12
13extend<T> Foo<T>: Marker {
14 fun mark(self) -> String { return "blanket"; }
15}
16
17extend<T> Foo<T>: !Marker;
18
19fun needs_not_marker<U: !Marker>(x: U) {}
20
21fun main() {
22 let f := Foo { value = 5 };
23 needs_not_marker(f);
24}
passes
Legality Rule №10
Explicit negative implementations are a distinct definition-site mechanism that affects which implementations negative-bound checking finds.
Referenced by: rfc-0072
Tested by
1// RFC-0081's motivating generic form: a blanket negative impl must satisfy a
2// `!Aspect` bound even when a blanket positive impl for the same target head
3// also exists. The negative impl wins for every `Foo<T>` instantiation.
4
5aspect Marker {
6 fun mark(self) -> String;
7}
8
9struct Foo<T> {
10 value: T,
11}
12
13extend<T> Foo<T>: Marker {
14 fun mark(self) -> String { return "blanket"; }
15}
16
17extend<T> Foo<T>: !Marker;
18
19fun needs_not_marker<U: !Marker>(x: U) {}
20
21fun main() {
22 let f := Foo { value = 5 };
23 needs_not_marker(f);
24}
passes
Negative Impls
A library author declares that a type definitively does not implement an aspect
with extend Type: !Aspect; — body always empty, since a negative impl is a
declaration of non-implementation, not a definition of behavior:
extend<T> Rc<T>: !Send;
extend<T> Rc<T>: !Sync;
More generally, a bodyless extend block is permitted whenever the body would be empty
already (RFC-0102):
aspect Copy2;
struct Handle { id: i64 }
extend Handle: Copy2;
extend Handle: !Send;
extend Handle: Copy2, !Send;
A methodless aspect declaration may itself be written bodylessly as aspect Name;
(RFC-0103), as in the Copy2 example above.
extend Type: Aspect; is valid only when every method of Aspect already has a
default body and the aspect declares no associated types. extend Type: !Aspect; is
always valid, and the braced negative form is retired in favor of the bodyless one.
Why this needs its own mechanism, not just the absence of a positive impl. A
blanket impl can inadvertently grant an aspect to a type that must not have it — Rc<T>
would satisfy an auto-derived Send blanket (its field is an ordinary, Send-by-value
integer) even though sharing it across fibers is unsound. A negative impl overrides any
blanket that would otherwise apply: Rc<T>: !Send holds for all T, regardless of what
a blanket impl elsewhere says.
Finality. No positive impl may coexist with a negative impl for the same type and
aspect — a concrete extend Type: Aspect alongside extend Type: !Aspect is a
coherence error. A negative impl overriding a blanket positive impl is the intended,
allowed case; a negative impl does not propagate to subtypes or supertypes (extend Rc<T>: !Send says nothing about Arc<T>).
Orphan rules apply the same way as positive impls (Aspect Implementation Coherence, above) — a negative impl is permitted only when the aspect or the type is local to the current module or stdlib. A positive and a negative impl for the same concrete type is T0015, the same coherence error two conflicting positive impls produce.
Formal rules
Legality Rule №1
A bodyless positive extend Type: Aspect; is legal exactly when the corresponding empty
braced implementation is legal: the aspect has no required methods and no associated type
requiring a binding.
Referenced by: rfc-0102
Tested by
1struct Pair {
2 left: i64,
3 right: boolean,
4}
5
6extend Pair: Copy;
7
8enum MaybeInt {
9 Some { value: i64 },
10 None,
11}
12
13extend MaybeInt: Copy;
14
15fun id_copy<T: Copy>(x: T) -> T {
16 x
17}
18
19fun main() {
20 let si: i8 := id_copy(7i8);
21 let ui: u16 := id_copy(9u16);
22 let ff: f32 := id_copy(1.5f32);
23 let bb: boolean := id_copy(true);
24 let cc: Char := id_copy('Q');
25 assert(si == 7i8);
26 assert(ui == 9u16);
27 assert(ff == 1.5f32);
28 assert(bb);
29 assert(cc == 'Q');
30
31 let tuple := id_copy((1, true));
32 assert(tuple.0 == 1);
33 assert(tuple.1);
34
35 let seed: [i64; 3] := [1, 2, 3];
36 let fixed := id_copy(seed);
37 assert(fixed[0] == 1);
38 assert(fixed[2] == 3);
39
40 let pair := id_copy(Pair { left = 4, right = false });
41 assert(pair.left == 4);
42 assert(!pair.right);
43
44 let maybe := id_copy(MaybeInt::Some { value = 8 });
45 match (maybe) {
46 MaybeInt::Some { value } => assert(value == 8),
47 MaybeInt::None => assert(false),
48 }
49
50 let n := 12;
51 let shared: &i64 := &n;
52 let shared2 := id_copy(shared);
53 assert((shared2: i64) == 12);
54}
passes
Dynamic Semantics №1
A bodyless single-aspect extend has the same declaration semantics as the corresponding
empty braced implementation; it introduces no bodyless-specific validation category.
Referenced by: rfc-0102
Tested by
1// RFC-0102 §2/§5: a bodyless `extend` desugars to an ordinary empty impl (no separate
2// "marker aspect" category), and multiple aspects -- including a negated one -- may be
3// listed in a single bodyless extend block, comma-separated.
4
5aspect Copy2 {}
6aspect Sendable {}
7aspect Displayable {
8 fun to_string(&self) -> String;
9}
10
11struct Handle { fd: i64 }
12
13extend Handle: Copy2, Sendable, !Displayable;
14
15fun needs_copy2<T: Copy2>(x: &T) -> boolean { return true; }
16fun needs_sendable<T: Sendable>(x: &T) -> boolean { return true; }
17fun needs_not_displayable<T: !Displayable>(x: &T) -> boolean { return true; }
18
19fun main() {
20 let h := Handle { fd = 1 };
21 assert(needs_copy2(&h));
22 assert(needs_sendable(&h));
23 assert(needs_not_displayable(&h));
24}
passes
Legality Rule №2
An explicit negative implementation overrides an applicable blanket positive implementation for its concrete target, while an explicit positive and explicit negative implementation for that same target are rejected with T0015.
Referenced by: rfc-0060, rfc-0081
Tested by (2)
1// TYPECHECK_ERROR[conflicting implementation]
2// RFC-0081§2.2 and RFC-0081§3, issue #264: a negative impl is final -- no positive impl may
3// coexist with a negative impl for the same type and aspect. This falls out
4// of the existing overlap check (issue #238/RFC-0060), which doesn't special-
5// case polarity: two impls of the same aspect for the same concrete type
6// already collide regardless of polarity. Locked in here as its own fixture
7// since it was never explicitly exercised before this issue.
8
9aspect Greet {
10 fun greet(&self) -> String;
11}
12
13struct Widget {
14 x: i64,
15}
16
17extend Widget: Greet {
18 fun greet(&self) -> String {
19 return "hi";
20 }
21}
22
23extend Widget: !Greet;
24
25fun main() {}
typecheck error“conflicting implementation”
1// RFC-0060 §5: a negative impl beats a blanket positive impl for the same
2// concrete type -- permitted (no T0015), and the negative impl wins for bound
3// satisfaction (Foo<i64>: !Marker holds).
4
5aspect Marker {
6 fun mark(self) -> String;
7}
8
9struct Foo<T> {
10 value: T,
11}
12
13extend<T> Foo<T>: Marker {
14 fun mark(self) -> String { return "blanket"; }
15}
16
17extend Foo<i64>: !Marker;
18
19fun needs_not_marker<U: !Marker>(x: U) {}
20
21fun main() {
22 let f := Foo { value = 5 };
23 needs_not_marker(f);
24}
passes
Legality Rule №3
A negative implementation must use the bodyless spelling extend Type: !Aspect;; the
braced spelling is rejected.
Referenced by: rfc-0102
Tested by
1// RFC-0102 §3: the bodyless form is mandatory for a negative extend, not optional --
2// the explicit-braces spelling is rejected outright.
3
4aspect Displayable {
5 fun to_string(&self) -> String;
6}
7
8struct Handle { fd: i64 }
9
10extend Handle: !Displayable { }
11
12fun main() {}
parse error“negative `extend` must use the bodyless”
Dynamic Semantics №2
A bodyless multi-aspect extend Type: A, B, !C; is equivalent to independent bodyless
single-aspect declarations for A, B, and !C.
Referenced by: rfc-0102
Tested by
1// RFC-0102 §2/§5: a bodyless `extend` desugars to an ordinary empty impl (no separate
2// "marker aspect" category), and multiple aspects -- including a negated one -- may be
3// listed in a single bodyless extend block, comma-separated.
4
5aspect Copy2 {}
6aspect Sendable {}
7aspect Displayable {
8 fun to_string(&self) -> String;
9}
10
11struct Handle { fd: i64 }
12
13extend Handle: Copy2, Sendable, !Displayable;
14
15fun needs_copy2<T: Copy2>(x: &T) -> boolean { return true; }
16fun needs_sendable<T: Sendable>(x: &T) -> boolean { return true; }
17fun needs_not_displayable<T: !Displayable>(x: &T) -> boolean { return true; }
18
19fun main() {
20 let h := Handle { fd = 1 };
21 assert(needs_copy2(&h));
22 assert(needs_sendable(&h));
23 assert(needs_not_displayable(&h));
24}
passes
Legality Rule №4
A negative implementation is a bodyless declaration of non-implementation: it provides no required or default aspect methods, and may name a generic or concrete target.
Referenced by: rfc-0081
Tested by (3)
1// RFC-0081: `impl !Aspect for Type {}` (negative impls) parses with an empty body
2// and does not register the type as implementing the aspect. Full negative-impl
3// coherence (finality, priority over blanket impls) is issue #264's job — this
4// fixture only locks in that the syntax is accepted and harmless alongside
5// ordinary positive impls elsewhere in the same program.
6
7struct Handle {
8 fd: i64,
9}
10
11extend Handle: !Drop;
12
13aspect Greet {
14 fun greet(&self) -> String;
15}
16
17extend Handle: Greet {
18 fun greet(&self) -> String {
19 return "handle";
20 }
21}
22
23fun main() {
24 let h := Handle { fd = 3 };
25 assert(h.greet() == "handle");
26}
passes
1// RFC-0081, issue #264: `impl !Aspect for Type {}` is a declaration of
2// non-implementation, not a real impl missing overrides -- it must not be
3// required to provide the aspect's (non-default) methods. Regression: this
4// previously failed with T0003 "does not implement ... required by aspect",
5// because the "does this impl provide every required method" check didn't
6// know a negative impl provides nothing by design.
7
8aspect Greet {
9 fun greet(&self) -> String;
10}
11
12struct Robot {
13 id: i64,
14}
15
16extend Robot: !Greet;
17
18fun main() {
19 assert(1 == 1);
20}
passes
1// TYPECHECK_ERROR[no method]
2// RFC-0081, issue #264: a negative impl must not inherit the aspect's
3// default-bodied methods either -- that would make the type appear to
4// implement the aspect via inherited defaults, exactly backwards from what
5// `impl !Aspect for Type {}` means. Regression: this previously succeeded,
6// silently registering `greeting` (a default method) as callable on Robot.
7
8aspect Greet {
9 fun name(&self) -> String;
10 fun greeting(&self) -> String {
11 return "Hi, " + self.name();
12 }
13}
14
15struct Robot {
16 id: i64,
17}
18
19extend Robot: !Greet;
20
21fun main() {
22 let r := Robot { id = 1 };
23 println(r.greeting());
24}
typecheck error“no method”
Legality Rule №5
An explicit negative implementation overrides an applicable blanket positive implementation for its target and satisfies a corresponding negative bound; it applies only to that target, not to another nominal type.
Referenced by: rfc-0081
Tested by (4)
1// RFC-0081: a blanket generic negative impl is part of the accepted surface,
2// not just concrete `impl !Aspect for Foo<i64> {}` cases. It must override a
3// blanket positive impl for every matching instantiation, so `Foo<boolean>`
4// fails a positive `Marker` bound here.
5
6aspect Marker {
7 fun mark(self) -> String;
8}
9
10struct Foo<T> {
11 value: T,
12}
13
14extend<T> Foo<T>: Marker {
15 fun mark(self) -> String { return "blanket"; }
16}
17
18extend<T> Foo<T>: !Marker;
19
20fun needs_marker<U: Marker>(x: U) {}
21
22fun main() {
23 let f := Foo { value = true };
24 needs_marker(f); // ERROR[T0012]
25}
typecheck errorT0012at 24
1// RFC-0081 + RFC-0061: a blanket negative impl on a structural target (`T[]`)
2// must override a blanket positive impl the same way it already does for a
3// named generic target (see generic_negative_impl_blocks_positive_bound) --
4// negative-impl priority over a blanket positive is a property of the
5// aspect/target pair, not something that should depend on whether the
6// target happens to be `Named` or structural.
7
8aspect Marker {
9 fun mark(self) -> String;
10}
11
12extend<T> T[]: Marker {
13 fun mark(self) -> String { return "blanket"; }
14}
15
16extend<T> T[]: !Marker;
17
18fun needs_marker<U: Marker>(x: U) {}
19
20fun main() {
21 let xs := [1, 2, 3];
22 needs_marker(xs); // ERROR[T0012]
23}
typecheck errorT0012at 22
1// RFC-0081's motivating generic form: a blanket negative impl must satisfy a
2// `!Aspect` bound even when a blanket positive impl for the same target head
3// also exists. The negative impl wins for every `Foo<T>` instantiation.
4
5aspect Marker {
6 fun mark(self) -> String;
7}
8
9struct Foo<T> {
10 value: T,
11}
12
13extend<T> Foo<T>: Marker {
14 fun mark(self) -> String { return "blanket"; }
15}
16
17extend<T> Foo<T>: !Marker;
18
19fun needs_not_marker<U: !Marker>(x: U) {}
20
21fun main() {
22 let f := Foo { value = 5 };
23 needs_not_marker(f);
24}
passes
1// RFC-0081 §2.4: a negative impl applies only to its own target. `Rc` meets
2// the negative bound, while the separately positive `Arc` must not inherit it.
3aspect Send {
4 fun send(self);
5}
6
7struct Rc { value: i64 }
8struct Arc { value: i64 }
9
10extend Rc: !Send;
11extend Arc: Send { fun send(self) {} }
12
13fun needs_not_send<T: !Send>(value: T) {}
14
15fun main() {
16 needs_not_send(Rc { value = 1 });
17 needs_not_send(Arc { value = 2 }); // ERROR[T0012]
18}
typecheck errorT0012at 17:19“bound not satisfied”
Legality Rule №6
Negative implementations obey the ordinary orphan rule: the aspect or the target's outermost
constructor must be local to the module containing the extend declaration.
Referenced by: rfc-0081
Tested by
1// RFC-0081 §3: "Negative impls follow the same orphan rules as positive impls" --
2// this module owns neither Marker nor Widget, so even a negative impl is rejected.
3import marker_aspect::Marker;
4import widget::Widget;
5
6extend Widget: !Marker; // ERROR[T0014]
7
8fun main() {}
typecheck errorT0014at 6:1“orphan implementation”
Aspect Bounds on Struct and Enum Type Parameters
A struct or enum generic type parameter may declare an aspect bound. The bound is enforced at construction time: instantiating the type with a concrete type argument that does not implement the bound is error T0012, with the span on the offending type argument at the construction call site.
struct SortedList<T: Comparable> {
items: T[],
}
// error[T0012]: NonComparable does not implement Comparable
let list = SortedList<NonComparable> { items = [] }
The same inline + and where clause forms apply, with identical semantics:
// Multiple inline bounds
struct Window<T: Comparable + Printable> { items: T[] }
// where clause (equivalent)
struct Cache<K, V> where K: Hashable + Comparable { entries: Pair<K, V>[] }
Bound propagation. A struct's bounds are automatically available — without re-declaration — in:
extendblocks on the same struct:extend SortedList<T>hasT: Comparablein scopeextend Struct<T>: AspectNameblocks: the struct's bounds are inherited- Match arm bodies when matching a value of the bounded struct or enum type
The bound is an invariant of the type, not of the binding site. It propagates wherever a value of that type is used. See Conditional Impl Blocks, above, for how this interacts with an aspect impl's own additional bounds.
Formal rules
Legality Rule №1
A generic struct or enum parameter may carry aspect bounds inline, in a where clause,
or in both forms. + joins multiple bounds, and bounds from the two forms on the same
parameter are combined.
Referenced by: rfc-0034
Tested by
1// Positive: inline bound + additional where clause bound on the same param,
2// both satisfied.
3
4aspect Printable {
5 fun print(self);
6}
7
8aspect Comparable {
9 fun compare(self, other: Self) -> i64;
10}
11
12struct SortedCache<T: Printable> where T: Comparable {
13 items: T[],
14}
15
16struct Entry {
17 value: i64,
18}
19
20extend Entry: Printable {
21 fun print(self) {}
22}
23
24extend Entry: Comparable {
25 fun compare(self, other: Entry) -> i64 {
26 self.value - other.value
27 }
28}
29
30fun make(e: Entry) -> SortedCache<Entry> {
31 SortedCache { items = [e] }
32}
passes
Legality Rule №2
Constructing a bounded struct or enum with a concrete type argument that does not satisfy every declared aspect bound is rejected with T0012 at that type argument.
Referenced by: rfc-0034
Tested by
1// Negative: enum with aspect bound constructed with a type that does NOT
2// implement the bound. Expect T0012.
3
4aspect Printable {
5 fun print(self);
6}
7
8enum Container<T: Printable> {
9 Some { value: T },
10 Empty {},
11}
12
13struct Plain { x: i64 }
14
15// Plain does not implement Printable.
16
17fun bad(p: Plain) -> Container<Plain> {
18 Container::Some { value = p } // ERROR[T0012]
19}
typecheck errorT0012
Legality Rule №3
An inherent extend Struct<T> inherits the declared aspect bounds of Struct<T>; its
methods may use those aspect operations on T without restating the bounds.
Referenced by: rfc-0034
Tested by
1// Positive: impl method on a bounded generic struct. The method takes a T-typed
2// param, calls an aspect method on it, and returns a value. Tests that:
3// 1. The method is stored as a polymorphic scheme (not in concrete method_env)
4// 2. The scheme is instantiated correctly at the call site
5// 3. The bound's aspect methods are available in the method body
6
7aspect Comparable {
8 fun compare(self, other: Self) -> i64;
9}
10
11struct SortedList<T: Comparable> {
12 items: T[],
13}
14
15struct Num { value: i64 }
16
17extend Num: Comparable {
18 fun compare(self, other: Num) -> i64 { self.value - other.value }
19}
20
21extend SortedList<T> {
22 fun rank(self, item: T, pivot: T) -> i64 {
23 item.compare(pivot)
24 }
25}
26
27fun run(s: SortedList<Num>, a: Num, b: Num) -> i64 {
28 s.rank(a, b)
29}
passes
Legality Rule №4
An aspect implementation extend Struct<T>: Aspect likewise inherits Struct<T>'s
declared aspect bounds without a duplicate declaration.
Referenced by: rfc-0034
Tested by
1// RFC-0034 §4: declaration bounds propagate to aspect extends.
2aspect Comparable {
3 fun compare(self, other: Self) -> i64;
4}
5
6aspect Labelled {
7 fun label(self) -> i64;
8}
9
10struct SortedList<T: Comparable> { item: T }
11
12extend SortedList<T>: Labelled {
13 fun label(self) -> i64 { self.item.compare(self.item) }
14}
15
16struct Num { value: i64 }
17extend Num: Comparable {
18 fun compare(self, other: Num) -> i64 { self.value - other.value }
19}
20
21fun main() -> i64 {
22 let n := Num { value = 4 };
23 let list := SortedList { item = n };
24 list.label()
25}
passes
Legality Rule №5
A match arm's body, when matching a value of a bounded struct or enum type, has that type parameter's declared aspect bounds available the same way any other use site does — no re-declaration needed.
Referenced by: rfc-0034
Tested by
1aspect Show {
2 fun show(&self) -> String;
3}
4
5struct Wrapper<T: Show> { value: T }
6
7extend i64: Show {
8 fun show(&self) -> String { "i64" }
9}
10
11fun describe<T: Show>(w: Wrapper<T>) -> String {
12 match (w) {
13 Wrapper { value } => value.show(),
14 }
15}
16
17fun main() {
18 let w := Wrapper { value = 5 };
19 println(describe(w));
20}
passes
Static Dispatch Only
All aspect dispatch in Metel is static (monomorphised at compile time)D1. There are no vtables, no heap allocation, and no runtime type erasure for aspects.
Method resolution must also be unambiguous at compile time. If the same receiver
type implements two different aspects that both define the same method name, a call
like value.method() is rejected with T0013L1 rather than resolved by declaration order.
dyn Aspect (runtime-dispatched existential types with vtable-based dispatch) is not
part of this language version. All polymorphism goes through generic type parameters
with aspect bounds.
Aspect objects (dyn Aspect) are not part of the language. All polymorphism is via generics (static dispatch).
Formal rules
Dynamic Semantics №1
Aspect method calls are resolved statically for their concrete type arguments; aspect values use neither runtime type erasure nor vtable dispatch.
Exempt from fixture coverage — untestable: Whether the compiler uses monomorphisation rather than vtables is a compilation-strategy property, not behavior an .mtl fixture can observe.
Legality Rule №1
If applicable aspects for the same receiver type provide the same method name, an unqualified dot call is ambiguous and is rejected with T0013.
Tested by
1// Regression: two distinct aspects define the same method name on the same receiver type.
2// Static dispatch must reject the ambiguous call at compile time instead of silently
3// picking whichever impl elaboration visits first.
4
5aspect A {
6 fun label(self) -> i64;
7}
8
9aspect B {
10 fun label(self) -> i64;
11}
12
13struct S { value: i64 }
14
15extend S: A {
16 fun label(self) -> i64 { 1 }
17}
18
19extend S: B {
20 fun label(self) -> i64 { 2 }
21}
22
23fun main() {
24 let s := S { value = 0 };
25 let _n := s.label();
26}
passes