Declared Layout Order: Relaxing the Alphabetical WIT Ordering Constraint
Status: Planned (2026-08-31, not started)
Follow-up to sum-types. That work forced extern record fields and
sum cases to be declared alphabetically, because blr types records/sums with
canonically label-sorted rows while the component ABI is positional. This plan
removes that constraint so an extern type can be declared in the (natural) WIT
order, without paying a copy in the common case, without relaxing the
structural nature of blr types.
Decision
Field/case order becomes a program-global layout hint, keyed by the
structural type, resolved per label set as:
| Declarations of the label set | Layout order |
|---|---|
| none (purely local type) | sorted (today's behavior) |
| one distinct declared order | that declared order (zero-copy: layout == WIT order) |
| multiple distinct orders | stable winner: lexicographically first (module path, declaration); losers get the remap (rare, accepted cost) |
Identity is untouched: the row machinery (sort, ClosedRow::merge, equatable
in crust/ty) keeps operating on sorted
canonical rows. Declared order is a layout hint, not part of the type. The
permutation is applied exactly once, at the mantle→core translation, so the
nucleus emits offsets/dt/indices consistently by construction and needs no
changes for the common case.
Order-sensitivity map (verified)
Order is settled/consumed at these sites only:
core/mod.rs:342/350— mantleRow::Closed→Type::labeled_tuple/variant
(the single normalization point; rows arrive sorted fromlower_row_innerat crust/mod.rs:397)core/mod.rs:621(+nucleusrecord-literal emit at
nucleus/mod.rs:594) —Expr::Tuplefields, stored positionallycore/mod.rs:180/94—Expr::Accessfield selects are index-based
(fields[*field]); indices must be re-permuted with the field vecnucleus/mod.rs:615—Expr::Tag(typ, idx, _)case index, same re-permutationcore/mod.rs:688/693—convert_external_typeRecord/Sum (remote item
types; declared order already)
Already correct, no change: core import signatures
(emit_external_item_ty → flatten_function_type(&item.external_typ),
nucleus/mod.rs:141) and the root-component instance type
(EmitInstanceType::convert_ext_ty_to_ctyp,
nucleus/component.rs) are
emitted in declared order. EmitComponent::convert_ty_to_ctyp (main's own
component types) is unconstrained: the host reads lifted values by case/field
name.
The four parts
LayoutOrderstable — driver pre-pass inroot_component
(compiler/mod.rs:91): parse+expand main- transitive imports at the air level (air rows still carry declared
order — the existing alphabet panics at crust/mod.rs:447/482 fire on
declaration order, proving it), walk extern declarations + their type
aliases, collectcanonical-sorted key → declared label order(records:
recursively canonical(label, field type)pairs; sums: case labels).
Store inDatabase; thread through thenucleus_modulepipeline to the
mantle→core translation.
- transitive imports at the air level (air rows still carry declared
- Re-permutation in the mantle→core translation — field/case vecs
emitted in table order;Accessindices,Tagcase indices, and record
literal field lists re-permuted by the same table. One translation unit;
all consumers below stay consistent. LayoutPlan(rare path) — at eachAppExternal
(nucleus/mod.rs:429 has both orders in hand: the declaredftand the
value's core type): permutation from type order to the declaredft
order, dt remap, and WIT-layout region materialization (alloc + per-field
store in declared order; nested case order). Identity in the common case →
zero copy, zero remap. Slot joins are commutative over the same multiset,
so the plan is a pure permutation + discriminant remap (no re-widening
logic); per-position types are identical or bit-castable.- Remove the alphabet panics (crust/mod.rs:447, 482). New contract:
declared order must match the host WIT declaration order; a mismatch
surfaces as a wasmtime component-type mismatch at instantiation (WIT type
identity matches records/variants positionally with name equality). No
compile-time check is possible — the compiler does not see the host WIT.
Corner cases
- Record literals / local-only types have no declaration → sorted layout;
literal field order is syntactic sugar (the compiler stores at the type's
computed offsets). - Multi-order disputes: the winner rule (first by module path) is stable
under additions — a newly added same-label-set declaration never steals the
win, so existing layouts don't shift. - Same label set, same order in several declarations → one table entry,
all identity, no penalty. - Structural sharing: two externs may use the same canonical blr type with
different WIT orders; the plan is keyed per extern item, so both work —
exactly the decoupling this is after.
ABI / notes
- Large values cross in-memory; when plan ≠ identity the caller must
materialize a WIT-layout region instead of passing its own heap pointer
(and reverse on results). Identity plans keep the current zero-copy path
and byte-identical emitted code for alphabetical programs. - Layout becomes a program-global property: two separate blr programs may
lay out the same label set differently. Fine while programs compile
self-contained; a consideration if blr ever shares precompiled components
across program boundaries. - The host-side instance type matching (declared order vs WIT order) is the
runtime safety net; a mismatch is a wasmtime instantiation type error. - list-types (done) added
list<T>; its deferred df
follow-up reuses the fixture workaround decided there —nodeandbinaryoperatorinstdlib/wit/world.witare reordered to alphabetical
when the df externs are re-enabled, rather than depending on this work.
Test / fixture strategy
- sums.wit + sums.blr:
revertrectto natural{w, h}(non-alphabetical) — the original
swap-bug detector.sum_param/sum_in_record/sum_ret/sum_main_ret
must pass with correct values on the zero-copy path. - New dispute fixtures in the
blr:sumsworld:rect2 {h: f64, w: f64}+shape2(with arect2payload) +area2: func(s: shape2) -> f64, andtiny2 {b, a}+pick2: func(x: s64) -> tiny2; host impl in sums.rs.sum_dispute.blrintegration test: exercise dt remap (register crossing viapick2) and WIT-layout region copy (memory crossing viaarea2). - Byte-identity gate: BLR_SNAPSHOT before/after on an all-alphabetical
program → core WAT must be unchanged (proof of zero regression in the
common case). All 27 pre-existing fixtures keep alphabetical declarations →
table = sorted → untouched.
Work items
| # | Item | Phase |
|---|---|---|
| A1 | LayoutOrders table + air pre-pass in root_component (cache or re-run parse+expand) + Database threading | A |
| A2 | Re-permutation in mantle→core translation (type rows, literal fields, Access idx, Tag idx, convert_external_type) | A |
| A3 | Remove alphabet panics; rect → {w, h} in fixture; run all fixtures | A |
| A4 | Byte-identity WAT gate for alphabetical programs | A |
| B1 | LayoutPlan at AppExternal: permutation + dt remap + region materialization (identity fast path) | B |
| B2 | Dispute fixtures (rect2/shape2/area2, tiny2/pick2) + host impl + sum_dispute.blr | B |
| C1 | Unit tests: table collection/winner/stability, re-permutation index consistency, plan identity + bit-cast sanity | C |
| C2 | This plan file updated with status/log; cargo fmt, cargo clippy --workspace -- -D warnings, full workspace tests | C |
Risks
- Driver pre-pass (re-running parse+expand or adding an air cache) is the
chunkiest plumbing; mitigate by reusing existing memoization
(db.item_sourcespattern). - Index re-permutation (
Access/Tag) is the subtlest correctness surface;
mitigate with the heavy record/select fixtures (record_poly,sums,df). - Grep for any row-order assumption that crosses the core boundary before
implementing A2 (e.g. mantlesubst/row operations on lowered rows). register_binary_operator_imports/register_conversion_imports/register_runtimeinject items after lowering — verify none carry record
rows needing the table.
Open questions
- Phasing: ship A alone first, or A+B together? Current lean: A+B
(A without B is only complete while no dispute fixture exists; B's plan is
the safety net for genuinely multi-order programs). - Winner rule on disputes: first-by-module-path (proposed; layout-stable
under additions) vs alphabetical-among-declarations (more canonical, but
the alphabetically-unlucky declaration always copies).
Progress log
- 2026-08-31: Plan written (design analysis + order-sensitivity map +
verified sites). Not started.