Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Reified types and layout

Every value’s type is available at runtime as a type descriptor. Slots are untagged 8-byte values; bytecode is typed, so hot paths carry no tags. This page is the language-facing contract; the descriptor itself is VM data, not a script value.

Where reification is language-facing

SurfaceMechanism
is type tests — concrete and trait RHSthe value cell’s descriptor (see Traits and dispatch)
opaque.downcast<T> recoverythe box’s recorded runtime type (see opaque — erasure and downcast)
type_id<T>()the compile-time type-identity constant
host boundary checksevery crossing is checked against the declared parameter type
diagnosticsstack traces and host tooling read the same descriptors

type_id<T>()

use ink::{ Logger };

struct Point { x: i32; y: i32; }

pub fn main() {
    let log = Logger.new("t");
    let TID_POINT: u32 = type_id<Point>();
    log.info(f"{TID_POINT} eq={type_id<Point>() == TID_POINT}");
}
23 eq=true
  • type_id<T>() -> u32 — the identity of the instantiated type: unique per VM run, stable across modules, comparable only. Vec<f32> ≠ Vec<f64>; Point = Point wherever declared.
  • It is a compile-time constant — a load-time expression (legal in module-level let initializers, see Modules and visibility), folded from the type table, never executed.
  • There is no size_of<T>() / align_of<T>() and no runtime payload footprint accessor: value size and alignment are implementation details, not a language surface.
  • Constructing a value from raw bytes is deliberately not provided: it could forge private fields and class invariants.

Value representation — slot arrays

Every struct and class value lives in a heap cell: header + (vtable, when the type has impls) + the payload.

  • The payload is a slot array: one untagged 8-byte slot per field, in declaration order. Primitive fields are widened into their slot (sign/zero-extended; a float is stored at the canonical 64-bit width); composite fields are cell-handle slots.
  • Visibility, generic parameters, and impl blocks add nothing — the payload depends only on the field list. Field access is by index; heap accounting charges fields × 8 bytes.
  • Buffers of primitive elements (Vec<f32>, [i32]) stay flat, packed to the element’s machine width; composite elements are one handle slot each — Vec<Point> stores one handle per element.
  • Enums are tagged cells (a tag slot plus a payload slot); dataless enum variants are immortal singleton cells.
  • Nullable ?prim element storage inside sequence backings is the raw payload plus a one-byte nil tag — no per-element cell.
  • bytes at the engine level is a u8 array cell; a str is an immutable, COW-shared UTF-8 block, and a slice of it is a small view cell (see String slicing and views).

Cells and vtables

RutCell := Header { rc, type id } VTable* Payload
VTable := { exact type id, dispose trampoline, trait method slots }
  • The exact runtime type lives in the cell (via the vtable when present, the header otherwise). Every cell is minted at construction with its vtable already attached.
  • Trait method ids are assigned globally per trait instantiation at compile time (Slice<Point> ≠ Slice<str>); a type’s vtable fills every slot of every trait instantiation it has an impl for — user impl blocks, auto-fills, and registry entries alike.
  • Widening a composite to a trait I reuses the same cell and vtable: the trait-typed value is the handle plus the vtable pointer — no allocation, no copy. The vtable reserves no base-prefix room: there is no inheritance.
  • A call through a trait object is two loads and an indirect jump (the receiver’s vtable, the method slot). Trait members never devirtualize; inherent calls bind directly:
Op::CallTrait { recv, slot: 3, args }   // d.draw(g) — vtable slot 3
Op::Call     { func: "Circle$area", recv, args }   // c.area() — inherent

The type test

is with a concrete right-hand side lowers to: load the object’s exact type id, compare — a compile-time constant comparison when the static type already answers. The trait-RHS capability probe is: exact-type compare, then a flat scan of the descriptor’s registered impls — no inheritance chain to walk (see Traits and dispatch).

opaque interacts with exactly one of these reads: is reads the box’s own type, so every payload probe misses; only opaque.downcast<T>’s match test keeps reading the payload — the one legitimate see-through.

Boxing

Storing a value into an erasure box widens by the slot discipline: an integer box stores the 64-bit sign/zero-extended value, a float the 64-bit width. A kind check plus opaque.downcast<i64>() / downcast<f64>() / downcast<bool>() / downcast<str>() is therefore total in-branch: within the successful branch the value’s type is the exact concrete type, and everything downstream optimizes as if the value had never been erased.