Objects — dyn Trait
Overview
A dyn T value is a fat pointer — a pair of pointers:
{ ptr data, ptr vtable }
data points to the concrete value (owned copy or borrow). vtable points
to a global method table emitted once per impl T:for:U block, named
<TraitName>_vtable_<TypeName>. Together they enable runtime polymorphism:
the concrete type is unknown at the call site but the correct method is reached
through the vtable.
Fat pointers are always 16 bytes on all supported platforms.
dyn T as a local variable
trait:Speak = {
func:say = int32(Speak:self);
};
struct:Dog = { int32:n; };
struct:Cat = { int32:n; };
impl:Speak:for:Dog = { func:say = int32(Dog:self) { pass self.n; }; };
impl:Speak:for:Cat = { func:say = int32(Cat:self) { pass self.n + 10i32; }; };
func:main = int32() {
Dog:d = Dog{ n: 5i32 };
dyn Speak:s = d; // Dog → dyn Speak coercion
int32:r = s.say(); // dispatch through Speak_vtable_Dog
exit r; // 5
};
The compiler:
1. Allocates a copy of d on the local stack frame.
2. Builds fat ptr {©, &Speak_vtable_Dog}.
3. Stores it in s.
Mutations through s affect the local copy, not the original d.
dyn T as a struct field (v0.35.5)
A struct field can hold a dyn T fat pointer, enabling heterogeneous
containers where the concrete type is decided at runtime:
struct:Widget = { dyn Speak:inner; };
func:main = int32() {
Dog:d = Dog{ n: 42i32 };
Widget:w;
w.inner = d; // concrete → dyn Speak coercion into field
int32:r = w.inner.say(); // vtable dispatch through field
exit r; // 42
};
The field occupies exactly 16 bytes in the struct layout. You can reassign
w.inner to a different concrete type at any time:
Cat:c = Cat{ n: 3i32 };
w.inner = c; // now holds Cat; previous copy is dropped
int32:r2 = w.inner.say(); // dispatches through Speak_vtable_Cat → 13
Limitation (v0.35.5): field-level borrows through a dyn T field are not
yet tracked by the borrow checker. Do not take a $$m borrow of w.inner
while the whole w is borrowed. Use a whole-struct borrow instead.
dyn T as a function parameter
By value (callee owns a copy):
func:print_val = int32(dyn Speak:s) {
pass s.say();
};
By mutable borrow (mutations propagate back):
func:increment = int32($$m dyn Bumpable:b) {
pass b.bump();
};
func:main = int32() {
Counter:c = Counter{ n: 7i32 };
int32:_ = raw increment(c);
exit c.n; // 8 — mutation propagated through the borrow fat ptr
};
Heterogeneous branching
dyn T supports runtime switching between concrete types:
dyn Speak:s;
if (condition) {
s = dog_val;
} else {
s = cat_val;
};
int32:r = s.say(); // correct vtable used at runtime
Error: ARIA-043
ARIA-043 — no impl of trait 'T' is in scope for any concrete type
Emitted when a concrete value is assigned to a dyn T (local, field, or
parameter) and no matching impl T:for:U exists. Solution: define the impl,
or remove the dyn T declaration.
Planned extensions
- OBJ-DEC-003 (
[dyn Trait]arrays) — fat-pointer element arrays; requires arena pinning. Target: v0.36.x. - OBJ-DEC-004 (field borrow lifetime) — borrow through a
dyn Tfield; requires borrow-checker field-region propagation. Target: TBD.
Related
- traits/dyn.md — full fat-pointer ABI reference
- types/struct.md — struct declarations and field access
- memory_model/borrow.md — borrow semantics