The dyn surface — fat pointers and dynamic dispatch
v0.54.6 — Comprehensive hardening: function parameter dispatch verified, struct field tests, borrow forms locked in (UTH-028..033).
1. Overview
A value of type dyn T is a fat pointer:
%struct.DynTraitObj = { ptr data, ptr vtable }
data points to the underlying concrete value (or an alias to it,
for borrows). vtable is a global pointer to the method table for
the concrete type that produced this dyn T. The vtable is named
<TraitName>_vtable_<TypeName> and is emitted once per
impl T:for:U block.
dyn T exists in three places: local variables, struct fields, and
function parameters. Each has slightly different ABI but all three
share the fat-ptr layout.
2. dyn T as a local variable
trait:Speak = {
func:say = int32(Speak:self);
};
impl:Speak:for:Counter = {
func:say = int32(Counter:self) { pass self.n; };
};
func:main = int32() {
Counter:c = { n: 63i32 };
dyn Speak:d = c; // concrete → dyn coercion
int32:r = d.say(); // dynamic dispatch through vtable
exit r; // 63
};
What happens at dyn Speak:d = c;:
- Compiler boxes
conto the local stack frame (a copy alloca). - Builds a fat ptr
{&boxed_c, &Speak_vtable_Counter}. - Stores it in
d.
Mutations through d do not affect the caller's c in this
case because d owns its own copy. For caller-visible mutation use
the borrow form $$m dyn T:d = c; — see Section 5.
3. dyn T as a function parameter
This is the most common use case for dynamic dispatch:
trait:Valued = { func:val = int32(int32:self); };
impl:Valued:for:int32 = { func:val = int32(int32:self) { pass self; }; };
func:get_val = int32(dyn Valued:item) {
pass item.val(); // dispatches through vtable
};
func:main = int32() {
int32:x = 10i32;
int32:r = raw get_val(x); // x coerced to dyn Valued at call site
exit r; // 10
};
The caller boxes a copy and passes the fat ptr by value. The callee
owns the fat ptr; mutations on item.data do not affect the caller.
Multiple dyn parameters
func:sum_vals = int32(dyn Valued:a, dyn Valued:b) {
pass a.val() + b.val();
};
Each parameter gets its own independent fat pointer and vtable.
Mixed parameters
func:add_to_val = int32(int32:offset, dyn Valued:item) {
pass offset + item.val();
};
Regular and dyn parameters can be freely mixed.
Nested forwarding
A function can forward a dyn T parameter to another function:
func:inner = int32(dyn Valued:item) { pass item.val(); };
func:outer = int32(dyn Valued:item) { pass raw inner(item); };
[!WARNING] Reserved keywords as function names: The name
processis a reserved keyword (TOKEN_KW_PROCESS). Using it as a function name producesCannot call non-function type 'process'. Other reserved keywords includeresult,process,atomic,channel,signal. Use descriptive names likerun_dispatch,do_process, etc.
4. dyn T as a struct field
A struct field of type dyn T occupies 16 bytes (two pointers).
struct:Stage = { dyn Speak:performer; int32:volume; };
[!IMPORTANT] Struct literals require an explicit
dyn Tcoercion before assignment. Directconcrete → dynin a struct literal is not supported:nitpick Counter:c = { n: 63i32 }; dyn Speak:d = c; // step 1: explicit coercion Stage:s = { performer: d, volume: 10i32 }; // step 2: assign dyn to dyn field int32:r = s.performer.say(); // dispatches through vtable → 63
Heterogeneous assignment
Different concrete types can be assigned to the same dyn field type:
struct:Speaker = { int32:n; };
struct:Loud = { int32:n; };
impl:Speak:for:Speaker = { func:say = int32(Speaker:self) { pass self.n; }; };
impl:Speak:for:Loud = { func:say = int32(Loud:self) { pass self.n + 100i32; }; };
Speaker:s = { n: 42i32 };
Loud:l = { n: 7i32 };
dyn Speak:d1 = s;
dyn Speak:d2 = l;
Stage:s1 = { performer: d1, volume: 10i32 }; // vtable = Speak_vtable_Speaker
Stage:s2 = { performer: d2, volume: 11i32 }; // vtable = Speak_vtable_Loud
s1.performer.say(); // → 42
s2.performer.say(); // → 107
Field-level borrows are not yet supported — borrow the whole struct instead.
dyn T in Arrays
You can create homogeneous arrays of fat pointers, allowing iteration over heterogeneous concrete objects:
// Using the same d1 and d2 from above
dyn Speak[2]:performers = [d1, d2];
int32:i = 0i32;
while (i < 2i32) {
int32:r = performers[i].say();
i = i + 1i32;
}
This array stores two 16-byte fat pointers sequentially. The array itself does not store the concrete structs, only the pointers to them.
5. Borrow forms
$$i dyn T — immutable borrow
func:read_count = int32($$i dyn Countable:item) {
pass item.count(); // read-only dispatch
};
The call site builds a fat ptr whose data slot points to the
caller's storage (not a boxed copy). The callee can read through
the vtable but cannot mutate the caller's value.
$$m dyn T — mutable borrow
func:bump_it = int32($$m dyn Bumpable:b) {
int32:r = b.bump(); // mutation propagates to caller
pass r;
};
func:main = int32() {
Counter:c = { n: 7i32 };
int32:r = raw bump_it(c);
exit c.n; // 8 — mutation propagated through $$m dyn
};
For $$m dyn T:b, the call site builds a fat ptr whose data
slot is the caller's storage address (not a boxed copy). Writes
through self.n inside the impl land directly in the caller's
variable.
6. Heterogeneous branching
int32:which = 1i32;
A:a = { n: 5i32 };
B:bb = { n: 10i32 };
dyn Speak:d;
if (which == 0i32) {
d = a;
} else {
d = bb;
};
int32:r = d.say(); // 110 (B's impl adds 100)
d is statically typed dyn Speak but at run time can carry
either a Speak_vtable_A or a Speak_vtable_B. Dispatch picks
the right method based on whichever vtable d is currently holding.
7. Multi-method dispatch
A trait with multiple methods produces a vtable with one slot per method. Dispatch picks the slot by index at the call site:
trait:Double = {
func:a = int32(Double:self);
func:b = int32(Double:self);
};
vtable layout for impl:Double:for:Counter:
%Double_vtable_Counter = { ptr __dyn_Double_Counter_a,
ptr __dyn_Double_Counter_b };
d.a() loads index 0; d.b() loads index 1.
8. Static vs. dynamic dispatch
| Aspect | Static (impl method call) |
Dynamic (dyn T dispatch) |
|---|---|---|
| Resolved at | Compile time | Run time |
| Overhead | Zero (direct call) | Indirect call through vtable pointer |
| Inlining | Yes (compiler can inline) | No (opaque through vtable) |
| Heterogeneous collections | No (type must be known) | Yes (same dyn T for all impls) |
| Binary size | One copy per concrete call | One vtable + one dispatch thunk per impl |
Use static dispatch for performance-critical hot paths. Use dyn T
when you need heterogeneous containers, plugin architectures, or
callback registries.
9. Known limitations
dyn Trait1 + Trait2 — not yet supported (UTH-029)
The parser does not handle the + combinator in type position for
multi-bound dyn trait objects. A value that must satisfy two traits
currently requires manual composition:
// Workaround: define a combined trait
trait:Combined = {
func:pval = int32(Combined:self);
func:cval = int32(Combined:self);
};
pick on dyn T — not supported (UTH-031)
pick is a value matcher (pattern matching on literal values),
not a type matcher. Runtime type dispatch on dyn T to branch by
concrete type requires future syntax (e.g. pick type(d) { ... }).
No implicit coercion in struct literals (UTH-030)
Struct fields of type dyn T require an explicit dyn T coercion
before assignment in struct literals. Direct concrete → dyn in
field position is not supported.
No K-semantics for vtable dispatch (UTH-032)
dyn T has no formal K-framework model. The fat pointer layout and
vtable dispatch are implemented in the compiler and verified by
runtime tests, but are not formally specified in k-semantics/nitpick.k.
10. Error reference
ARIA-043: No impl of trait in scope
error: ARIA-043 — no impl of trait 'T' is in scope for any concrete type
Cause: Coercion to dyn T requires at least one impl T:for:U block.
Fix: Define the impl, or remove the dyn T declaration.
"Cannot call non-function type"
If you see this error on a function that accepts dyn T parameters,
check that the function name is not a reserved keyword. Common
reserved keywords: process, result, atomic, channel, signal.
See also
- Traits — defining and implementing traits
- Borrow semantics —
$$iand$$mborrow rules - Diagnostics — ARIA-043 and related errors