Dynamic types and mapping (v0.3 scope)
Platform spec node
Dynamic types and mapping (v0.3 scope)
Spec standingStandard
No architecture decision records under adr/ for this feature yet. Standard features must
publish at least one ADR or keep a ## Decisions summary on the hub.
0 revisions (git unavailable at build; counts may be empty)
No commits recorded for this path.
| Section id | Required | Found |
|---|---|---|
scope | yes | yes |
features | yes | yes |
Full tree: run pnpm verify:platform-spec-layout (writes src/generated/platform-spec-layout-report.json).
v0.3 scope
This article specifies the v0.3 dynamic typing surface: a GC-visible dynamic cell, compile-time object-to-object mapping for eligible [Serialize] shapes, and a runtime fallback when static shape information is unavailable at compile time.
Goals:
- Dynamic type representation in HIR/CLIF for schema-evolving payloads without language-level reflection.
- AOT object-to-object mapping for known serializable types at compile time.
- Runtime mapping for dynamic values when static shape is unavailable.
- Eligibility enforced by Serialization Mod analyzers — mapping applies only to types classified as serializable; codegen MUST consult that signal and MUST NOT bypass it.
HIR/CLIF representation
The dynamic surface (named alias until a language primitive lands) lowers to a pointer-sized CLIF value referencing a runtime DynamicCell header allocated through the runtime arena.
Cell header layout (normative):
| Field | Width | Semantics |
|---|---|---|
shape_id | u32 | Registered object shape tag used by mapping tables |
flags | u32 | Reserved (zero in v0.3) |
payload | pointer | Arena-allocated static object or inline buffer; traced when non-null |
Allocation: cell headers and mapping target objects MUST be allocated through the runtime arena builtins (dynamic_cell_create, dynamic_object_alloc) to honour Phase A single-mutator rules. Codegen MUST NOT introduce parallel allocators for dynamic cells.
GC tracing: non-null payload pointers are treated as heap references and traced by the runtime collector; null payloads are ignored.
Lowering entry points: cell creation, static wrap, checked cast, AOT map, and fallback map are emitted as imported runtime builtins from compiler/crates/beskid_codegen/src/lowering/expressions/dynamic.rs. Type mapping for the dynamic alias is centralized in compiler/crates/beskid_codegen/src/lowering/types.rs.
AOT object-to-object mapping
When both source and destination static shapes are known and mod-eligible, codegen emits a direct call to dynamic_map_aot(src_shape, dst_shape, src_ptr, dst_out) after eligibility checks.
Eligibility (normative):
- Delegated to Serialization Mod analyzers on the analysis surface; until a dedicated analyzer signal is threaded into
TypeResult, codegen applies the same structural rules as the[Serialize]contract: only structs whose fields are primitives or nested eligible structs, with deterministic declaration-order field lists. - Ineligible pairs MUST fail lowering with structured diagnostic
E2013(IneligibleSerializeMapping) — never silent mapping or runtime panic from codegen.
Mapping kinds (v0.3):
- Identity mapping: source and destination structs share field names, order, and types; field steps copy memory ranges in declaration order.
- Transform mapping: same eligibility gate; v0.3 registers explicit offset/size steps in the runtime shape table (future Serialization Mod generators populate the table).
Shape identifiers: stable per resolved item id (FNV-1a hash in v0.3) so AOT tables and runtime registration agree across codegen and tests.
Implementation: compiler/crates/beskid_codegen/src/lowering/expressions/mapping.rs (lowering) and compiler/crates/beskid_codegen/src/lowering/expressions/serialize.rs (eligibility).
Runtime fallback
When compile-time shape information is unavailable (value already wrapped in a DynamicCell), generated code calls dynamic_map_fallback(cell, dst_shape, dst_out).
Algorithm:
- Reject null cell payload →
DYNAMIC_ERR_NULL_PAYLOAD. - Require registered source and destination shapes →
DYNAMIC_ERR_UNKNOWN_SRC_SHAPE/DYNAMIC_ERR_UNKNOWN_DST_SHAPE. - Look up field steps for
(cell.shape_id, dst_shape)in the runtime mapping table. - On missing or incompatible mapping →
DYNAMIC_ERR_INCOMPATIBLE(E-dynamic-map-001); this path is deterministic and structured — no implicit reflection or best-effort coercion.
Fallback shares the same field-step table as the AOT path (compiler/crates/beskid_runtime/src/dynamic/table.rs, compiler/crates/beskid_runtime/src/dynamic/fallback.rs).
Eligibility and Serialization Mods
Mapping MUST NOT bypass Serialization Mod analyzer classification. Codegen reads the analyzer signal on the typed program (structural stand-in until mod host wiring lands) via require_mapping_eligible in serialize.rs.
Cross-reference: Compiler Mod SDK for analyzer contracts; Serialization packages for serializable type constraints.
Implementation anchors
- CLIF lowering:
compiler/crates/beskid_codegen/src/lowering/expressions/dynamic.rs - AOT mapping + eligibility:
compiler/crates/beskid_codegen/src/lowering/expressions/mapping.rs,compiler/crates/beskid_codegen/src/lowering/expressions/serialize.rs - Dynamic CLIF type mapping:
compiler/crates/beskid_codegen/src/lowering/types.rs - Codegen diagnostic
E2013:compiler/crates/beskid_codegen/src/diagnostics.rs - Runtime cell + tables + fallback:
compiler/crates/beskid_runtime/src/dynamic/ - C ABI builtins:
compiler/crates/beskid_runtime/src/builtins/dynamic.rs - ABI symbol registration:
compiler/crates/beskid_abi/src/symbols.rs,compiler/crates/beskid_abi/src/builtins.rs - Integration tests:
compiler/crates/beskid_tests/src/codegen/dynamic_types/,compiler/crates/beskid_tests/src/runtime/dynamic/
Decisions
Section titled “Decisions”-
Pointer-to-cell CLIF representation.
dynamiclowers to a single pointer (I64on 64-bit System V) rather than a split(shape, payload)pair in CLIF, keeping call conventions aligned with other heap objects and allowing checked casts to inspect the header at runtime. -
Arena-only allocation. Dynamic cells and mapping outputs use runtime arena builtins to preserve Phase A single-mutator invariants; embedders must not allocate parallel cell pools outside the Beskid heap.
-
Shared shape/mapping table for AOT and fallback. One registration API (
register_shape,register_mapping) serves compile-time-known mappings and runtime fallback probes, avoiding divergent copy semantics. -
Deterministic incompatibility errors. Missing mappings return stable integer status codes surfaced as
E-dynamic-map-001/DYNAMIC_ERR_INCOMPATIBLE; codegen ineligibility usesE2013so failures are diagnosable without undefined behaviour. -
Structural eligibility stand-in. Until Serialization Mod analyzers attach a dedicated bit on
TypeResult, codegen applies structural[Serialize]rules and refuses ineligible pairs at lowering time — never at runtime via reflection. -
FNV-1a shape ids from resolved items. v0.3 derives
shape_idfromItemIdbytes so tests and AOT lowering agree without a separate shape registry in the analysis crate; mod-generated tables will replace this hash in a later track.