← Back to AILP Home

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 {&copy, &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

Related