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_alignin nucleus)
matches the CanonicalABI in-memory variant layout, so only the flat
representation (call stack) needs conversion:- flat form =
i32discriminant + joined payload slots (per specflatten_variant/join: equal types stay,i32/f32join toi32,
anything else joins toi64). - 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).
- flat form =
- Case order between the WIT type and the blr
extern typedeclaration 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 inconvert_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 (wasmtimeCanonicalAbiInfo::variantcontributes nothing forNonecases).
Test strategy
Fixtures live in a dedicated test world (existing WIT worlds untouched):package blr:sums / interface sums-interface (naming is dictated byblr_path_to_component_path in compiler).
| Tier | Test | Covers |
|---|---|---|
| unit | flatten_type on sums (unit-only, mixed, join widening, record-in-case, sum-in-sum) | flatten |
| unit | component_type_tests variant + record-with-variant (wasmprinter snapshots) | component type emission |
| unit | convert_external_type Sum | core type conversion |
| unit | val_to_string(Val::Variant) | printing |
| integration | sum_param.blr — all cases through sums::area | param lift, joined slots, padding, unit case, record payload |
| integration | sum_ret.blr — sums::pick (unit-only cases) + match | non-heap [i32] result wrap |
| integration | sum_in_record.blr — sums::pack/sums::unwrap + .s select | heap result wrap, tuple-with-variant param, variant store/load |
| integration | sum_main_ret.blr — main() -> shape | component-level variant result + printing |
| regression | sums (local sums), df | no regressions |
Dev verification: BLR_SNAPSHOT=... RUST_LOG=debug cargo test to inspectnucleus.wat/root.wat; snapshot comparison
for branch diffs.
Work items
| # | Item | Location | Status |
|---|---|---|---|
| M0a | Remove sum externs (filter, Node, BinaryOperator, Binary, Expr) from df — WIT keeps list<node>, blr has no lists | stdlib/src/df.blr | done |
| M0b | New WIT test world blr:sums (sums-interface) with shape/tiny/wrap + area/pick/pack/unwrap | stdlib/wit/sums.wit | done |
| M0c | Host impl + linker registration | stdlib/src/sums.rs, stdlib/src/lib.rs, lang/src/exec.rs | done |
| M0d | blr extern declarations for the fixture | stdlib/src/sums.blr | done |
| M1a | Expr::type_of for Tag/Case | lang/src/compiler/core/mod.rs | done |
| M1b | Core sexpr parse of external Sum | lang/src/compiler/core/sexpr.rs | done |
| M1c | Sum lift/lower helpers + AppExternal param variant + tuple-with-variant param | lang/src/compiler/nucleus/mod.rs | done |
| M1d | emit_store_expr/emit_load for nested variant (pointer copy) | lang/src/compiler/nucleus/mod.rs | done |
| M2a | AppExternal variant results: non-heap ([i32]) and heap (flat resv + wrap) | lang/src/compiler/nucleus/mod.rs | done |
| M2b | Component type emission: ExternalType::Sum (instance) and core Variant (root) | lang/src/compiler/nucleus/component.rs | done |
| M2c | val_to_string for Val::Variant | lang/src/lib.rs | done |
| M3 | fmt, clippy -D warnings, full workspace tests | done |
Out of scope (deferred follow-ups)
→ done, see list-types. Thelist<T>support
remainingdf::filterfollow-up (WITu32→s64+ alphabetical case
reorder, df.blr extern declarations, hostfilterimpl, df.blr test
extension) is recorded in its "Deferred: df integration" section.- Row polymorphism over sums:
TypApp::Rowmonomorph
(mantle/monomorph.rs), rowsubst(mantle/simplify.rs,mantle/mod.rs),subst_typonTag(mantle/simplify.rs). Context::Liftflattening 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-langfailsdf
integration at theType::Variantparamtodo!();sumsgreen. - 2026-08-31: M0 (fixtures) done — WIT world, host impl, linker, blr externs;
df sum externs removed (NOTE left instdlib/src/df.rs). - 2026-08-31: M1 (param path) done.
sum_param.blrgreen: all fourshape
cases (incl.noneunit +rectangle/pointrecord payloads) flow throughsums::areaand lift to the canonical flat ABI. Key bugs found & fixed while
bisecting the unit case:Expr::Tagleft the (align-adjusted) payload address on the stack even for
aunitpayload (nothing gets stored), corrupting every following value —unitpayloads now skip the payload-addr/STORE emission.Expr::Unitlowered toi32.const -1butType::UnitisValType::I64
on the core stack — nowi64.const -1.Expr::Caseread an uninitializedptrlocal and advanced it by the full
sumsizeinstead ofdt_size— nowlocal.tees the scrutinee and
offsets bydt_size(payload base), matchingemit_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 toconvert_to_ext_typfor records;rectreordered to{h, w}in bothsums.blrandsums.wit. Before the fix the host silently sawRect{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) andsum_in_record.blr(sums::packheap result, variant nested in a record
field,.sselect,sums::unwrap) green; 30/30 integration tests.
Nested variants are inlined in field slots (canonical in-memory layout):emit_store_exprcopies the sum region withmemory.copy,emit_load
adjusts the pointer,emit_value_slotslifts in place. Pre-existing
limitation hit while testing: a secondmatchin one body (using a
record'styp_absmethod twice) ICEs inmantle/simplify.rs("Unbound
variable encountered in simplification") — row-polymorphism territory,
deferred. - 2026-08-31: M2c + export side done.
sum_main_ret.blr—main() -> 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 theMemoryoption already
passed formain; the component type is the top-level variant defined
type + type export emitted byEmitComponent::convert_ty_to_ctyp.val_to_string(Val::Variant)printscase(payload)(wasmtime's
componentVal::Variantcarries the case name, not an index).
Unit tests added:flatten_typeon 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_stringvariant. Workspace-wide:cargo fmtclean,cargo clippy --workspace -- -D warningsclean, all tests pass.