6 min read 1251 words Updated Sep 04, 2026 Created Sep 04, 2026

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 setLayout order
none (purely local type)sorted (today's behavior)
one distinct declared orderthat declared order (zero-copy: layout == WIT order)
multiple distinct ordersstable 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 — mantle Row::ClosedType::labeled_tuple/variant
    (the single normalization point; rows arrive sorted from
    lower_row_inner at crust/mod.rs:397)
  • core/mod.rs:621 (+ nucleus record-literal emit at
    nucleus/mod.rs:594) —
    Expr::Tuple fields, stored positionally
  • core/mod.rs:180/94Expr::Access field selects are index-based
    (fields[*field]); indices must be re-permuted with the field vec
  • nucleus/mod.rs:615Expr::Tag(typ, idx, _) case index, same re-permutation
  • core/mod.rs:688/693convert_external_type Record/Sum (remote item
    types; declared order already)

Already correct, no change: core import signatures
(emit_external_item_tyflatten_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

  1. LayoutOrders table — driver pre-pass in root_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, collect canonical-sorted key → declared label order (records:
      recursively canonical (label, field type) pairs; sums: case labels).
      Store in Database; thread through the nucleus_module pipeline to the
      mantle→core translation.
  2. Re-permutation in the mantle→core translation — field/case vecs
    emitted in table order; Access indices, Tag case indices, and record
    literal field lists re-permuted by the same table. One translation unit;
    all consumers below stay consistent.
  3. LayoutPlan (rare path) — at each AppExternal
    (nucleus/mod.rs:429 has both orders in hand: the declared ft and the
    value's core type): permutation from type order to the declared ft
    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.
  4. 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 — node and
    binaryoperator in stdlib/wit/world.wit are reordered to alphabetical
    when the df externs are re-enabled, rather than depending on this work.

Test / fixture strategy

  • sums.wit + sums.blr:
    revert rect to 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:sums world: rect2 {h: f64, w: f64} +
    shape2 (with a rect2 payload) + area2: func(s: shape2) -> f64, and
    tiny2 {b, a} + pick2: func(x: s64) -> tiny2; host impl in sums.rs.
    sum_dispute.blr integration test: exercise dt remap (register crossing via
    pick2) and WIT-layout region copy (memory crossing via area2).
  • 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

#ItemPhase
A1LayoutOrders table + air pre-pass in root_component (cache or re-run parse+expand) + Database threadingA
A2Re-permutation in mantle→core translation (type rows, literal fields, Access idx, Tag idx, convert_external_type)A
A3Remove alphabet panics; rect{w, h} in fixture; run all fixturesA
A4Byte-identity WAT gate for alphabetical programsA
B1LayoutPlan at AppExternal: permutation + dt remap + region materialization (identity fast path)B
B2Dispute fixtures (rect2/shape2/area2, tiny2/pick2) + host impl + sum_dispute.blrB
C1Unit tests: table collection/winner/stability, re-permutation index consistency, plan identity + bit-cast sanityC
C2This plan file updated with status/log; cargo fmt, cargo clippy --workspace -- -D warnings, full workspace testsC

Risks

  • Driver pre-pass (re-running parse+expand or adding an air cache) is the
    chunkiest plumbing; mitigate by reusing existing memoization
    (db.item_sources pattern).
  • 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. mantle subst/row operations on lowered rows).
  • register_binary_operator_imports/register_conversion_imports/
    register_runtime inject items after lowering — verify none carry record
    rows needing the table.

Open questions

  1. 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).
  2. 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.