5 min read 1122 words Updated Sep 04, 2026 Created Sep 04, 2026

Sum Types Over the Wasm Component Boundary

Status: Done (2026-08-31)

Local sums (tag/case over a locally declared sum type) already work end-to-end
(see test). This plan finishes sum
type support for types based over the Wasm component body: sums declared on
external Wasm components (WIT variants, ExternalType::Sum) flowing in and out
of external function calls, and as the return type of main.

ABI notes

  • Blr's core heap layout for a sum (discriminant + max case payload, see
    cases_dt_size_align in nucleus)
    matches the CanonicalABI in-memory variant layout, so only the flat
    representation (call stack) needs conversion:
    • flat form = i32 discriminant + joined payload slots (per spec
      flatten_variant/join: equal types stay, i32/f32 join to i32,
      anything else joins to i64).
    • Lift (heap → flat): load discriminant, then per active case load payload
      slots at the case's offsets, zero-fill slots the case lacks, bitcast
      widened slots.
    • Lower (flat → heap): alloc tagged value, store discriminant, per active
      case store payload slots (truncating widened slots to the low bits).
  • Case order between the WIT type and the blr extern type declaration is
    load-bearing: the ABI discriminant is the case index. Blr types sums (and
    records) with fields/cases canonically sorted by label (row
    unification), so the extern declaration and the WIT must both use
    alphabetical order. Enforced by panics in convert_to_ext_typ
    (crust) for sums and records.
  • WIT unit-payload cases (no-payload variants) flatten to zero slots:
    only the discriminant crosses the boundary (wasmtime
    CanonicalAbiInfo::variant contributes nothing for None cases).

Test strategy

Fixtures live in a dedicated test world (existing WIT worlds untouched):
package blr:sums / interface sums-interface (naming is dictated by
blr_path_to_component_path in compiler).

TierTestCovers
unitflatten_type on sums (unit-only, mixed, join widening, record-in-case, sum-in-sum)flatten
unitcomponent_type_tests variant + record-with-variant (wasmprinter snapshots)component type emission
unitconvert_external_type Sumcore type conversion
unitval_to_string(Val::Variant)printing
integrationsum_param.blr — all cases through sums::areaparam lift, joined slots, padding, unit case, record payload
integrationsum_ret.blrsums::pick (unit-only cases) + matchnon-heap [i32] result wrap
integrationsum_in_record.blrsums::pack/sums::unwrap + .s selectheap result wrap, tuple-with-variant param, variant store/load
integrationsum_main_ret.blrmain() -> shapecomponent-level variant result + printing
regressionsums (local sums), dfno regressions

Dev verification: BLR_SNAPSHOT=... RUST_LOG=debug cargo test to inspect
nucleus.wat/root.wat; snapshot comparison
for branch diffs.

Work items

#ItemLocationStatus
M0aRemove sum externs (filter, Node, BinaryOperator, Binary, Expr) from df — WIT keeps list<node>, blr has no listsstdlib/src/df.blrdone
M0bNew WIT test world blr:sums (sums-interface) with shape/tiny/wrap + area/pick/pack/unwrapstdlib/wit/sums.witdone
M0cHost impl + linker registrationstdlib/src/sums.rs, stdlib/src/lib.rs, lang/src/exec.rsdone
M0dblr extern declarations for the fixturestdlib/src/sums.blrdone
M1aExpr::type_of for Tag/Caselang/src/compiler/core/mod.rsdone
M1bCore sexpr parse of external Sumlang/src/compiler/core/sexpr.rsdone
M1cSum lift/lower helpers + AppExternal param variant + tuple-with-variant paramlang/src/compiler/nucleus/mod.rsdone
M1demit_store_expr/emit_load for nested variant (pointer copy)lang/src/compiler/nucleus/mod.rsdone
M2aAppExternal variant results: non-heap ([i32]) and heap (flat resv + wrap)lang/src/compiler/nucleus/mod.rsdone
M2bComponent type emission: ExternalType::Sum (instance) and core Variant (root)lang/src/compiler/nucleus/component.rsdone
M2cval_to_string for Val::Variantlang/src/lib.rsdone
M3fmt, clippy -D warnings, full workspace testsdone

Out of scope (deferred follow-ups)

  • list<T> support → done, see list-types. The
    remaining df::filter follow-up (WIT u32s64 + alphabetical case
    reorder, df.blr extern declarations, host filter impl, df.blr test
    extension) is recorded in its "Deferred: df integration" section.
  • Row polymorphism over sums: TypApp::Row monomorph
    (mantle/monomorph.rs), row subst (mantle/simplify.rs,
    mantle/mod.rs), subst_typ on Tag (mantle/simplify.rs).
  • Context::Lift flattening for >1-result exported functions.
  • Strings/closures as sum payloads (blocked on strings-in-tuples).

Progress log

  • 2026-08-31: Plan written. Baseline: cargo test -p blr-lang fails df
    integration at the Type::Variant param todo!(); sums green.
  • 2026-08-31: M0 (fixtures) done — WIT world, host impl, linker, blr externs;
    df sum externs removed (NOTE left in stdlib/src/df.rs).
  • 2026-08-31: M1 (param path) done. sum_param.blr green: all four shape
    cases (incl. none unit + rectangle/point record payloads) flow through
    sums::area and lift to the canonical flat ABI. Key bugs found & fixed while
    bisecting the unit case:
    • Expr::Tag left the (align-adjusted) payload address on the stack even for
      a unit payload (nothing gets stored), corrupting every following value —
      unit payloads now skip the payload-addr/STORE emission.
    • Expr::Unit lowered to i32.const -1 but Type::Unit is ValType::I64
      on the core stack — now i64.const -1.
    • Expr::Case read an uninitialized ptr local and advanced it by the full
      sum size instead of dt_size — now local.tees the scrutinee and
      offsets by dt_size (payload base), matching emit_sum_flat.
  • 2026-08-31: Discovered record field order is also ABI load-bearing (same
    sorted-row root cause as cases). Added the alphabetical-order panic to
    convert_to_ext_typ for records; rect reordered to {h, w} in both
    sums.blr and sums.wit. Before the fix the host silently saw
    Rect{w:4,h:3} for a {w:3,h:4} input.
  • 2026-08-31: M2a (result paths) done. sum_ret.blr (sums::pick
    non-heap bare-dt result wrapped to a heap value + match) and
    sum_in_record.blr (sums::pack heap result, variant nested in a record
    field, .s select, sums::unwrap) green; 30/30 integration tests.
    Nested variants are inlined in field slots (canonical in-memory layout):
    emit_store_expr copies the sum region with memory.copy, emit_load
    adjusts the pointer, emit_value_slots lifts in place. Pre-existing
    limitation hit while testing: a second match in one body (using a
    record's typ_abs method twice) ICEs in mantle/simplify.rs ("Unbound
    variable encountered in simplification") — row-polymorphism territory,
    deferred.
  • 2026-08-31: M2c + export side done. sum_main_ret.blrmain() -> S
    (locally declared sum; extern types can't be named in blr type
    annotations) — passes end-to-end: a large result lift expects the
    core function to return a guest-memory pointer (no resv param), and the
    adapter copies the region guest→host via the Memory option already
    passed for main; the component type is the top-level variant defined
    type + type export emitted by EmitComponent::convert_ty_to_ctyp.
    val_to_string(Val::Variant) prints case(payload) (wasmtime's
    component Val::Variant carries the case name, not an index).
    Unit tests added: flatten_type on sums (unit-only, joined slots, join
    widening, record-in-case, sum-in-sum), component-type variant and
    record-with-variant wasmprinter snapshots, convert_external_type_to_core
    Sum, val_to_string variant. Workspace-wide: cargo fmt clean,
    cargo clippy --workspace -- -D warnings clean, all tests pass.