List Type Support
Status: Done (2026-09-01)
Companion to knowledge/design/sum-types.md (this plan provides thelist<T> support its "re-enable df::filter" deferred item needs) andknowledge/design/declared-layout-order.md (interaction with its ordering
constraint — see deferred df section below).
Scope per user: lists can be constructed (literals) and passed as
external types (param and result across the component boundary), with
explicit list tests in the sums test world. No iteration, no mutation,
no length/index operators at this stage; lists are never embedded inside
records/sums in the fixtures (those arms stay todo!(), same as the existing
String arms at lang/src/compiler/nucleus/mod.rs:747 and :1154).
Not in this plan: the df integration. The host filter instdlib/src/df.rs stays todo!(); the df fixture work (WIT fixes,df.blr extern declarations, df test extension) is deferred — see
"Deferred: df integration" below. Its decisions are recorded so the
follow-up is unblocked.
Canonical ABI for lists (verified, wasmtime 40 / spec)
CanonicalAbiInfo:InterfaceType::List(_) => POINTER_PAIR
(wasmtime-environ-40component/types.rs: flat_count 2, size32 8, align32 4)
— lists have the exact same representation as strings.flatten_type(List) = [I32, I32].- In-memory layout (spec
store_list/load_list): an 8-byte header{begin: i32 @0, length: i32 @4},beginpointing at a contiguous array
of elements, each element stored inline atptr + i * elem_size(elem_type)(no array-of-pointers — alist<node>is a
contiguous run of inlinednodein-memory variants). - A 2-slot result triggers the existing resv path
(flatten_function_type, nucleus/mod.rs:1487-1496) exactly like String:
the caller allocs an 8-byte header-only resv (align 4, size 8); the
host adapter fillsbegin/lengthand allocates the element array via the
guestrealloc. The resv pointer is the blr list value. - Blr's existing String convention is precisely this layout (region
{begin@0, len@4}; literal construction nucleus/mod.rs:332-358; param lift
:441-458; resv arm :491-502). List is a mechanical mirror of String
plus runtime-sized element storage.
Value/region model
core::Type::List(Box<Type>); stack value = i32 region pointer.- Local list region: one
alloc(4, 8 + n * elem_size);region[0] = region + 8,region[1] = n; elements stored inline from
offset 8 (scalars direct; sum elements via the existing inlined-regionmemory.copyconvention; resource elements as i32 handles). - Literal
[e1, .., en](empty[]allowed) constructs the region. - Element type: inferred by unifying all elements to one
T
(list literal carries a freshT; literal type =List(T)). No annotation
required at the call site (extern aliases likedf::nodecan't be named in
annotations — inference resolves against the declared extern type, same as
the sum tags insum_param.blr).
Syntax
- Type:
list<T>— newTypeExprproduction.</>lexer tokens are new;
they must not perturb the existing">>"forward-expr token
(parser.lalrpop:131-137) — verify token ordering. - Expression:
[e1, e2]— the grammar already has a commented-out rule
at parser.lalrpop:197 (//"[" <Exprs> "]" => Expr::List(...)); uncomment
and adapt (allow empty). No clash:[...]in type position is a sum; in
expression position it's a list.
Touch-point map (from repo survey)
air
parser.lalrpop: lexer tokens (299-317);TypeExprrule (271-282) gainslist < TypeExpr >;Termlist literal (197, uncomment); empty-listExprsallowance (176-182 / 248-257).air/mod.rs:Expr::List(Vec<Self>)(76-113);TypeExpr::List(Box<TypeExpr>)
(138-149); air sexpr arms (air/sexpr.rs:246-265, 828-862).
crust (list is a first-class type constructor)
crust/ty.rs:114-138Type::List(+occurs_check158-189,mentions
191-215);unification.rsnormalize_ty148-171 +unify_ty_ty179-220;inst.rs:94-127;subst.rs:121-150(ty) +:152-271(expr);crust/sexpr.rs:154-269(+ expr sexpr 732/888/1122/1293).crust/mod.rs:_lower_typ(343-384) List arm;convert_to_ext_typ(411-514) List arm →ExternalType::List;
newcrust::Expr::List { elem var, values }(enum 90-147,id()150-167)
lowered next to the record arm (606-769);infer.rsliteral arm (80-86
area) unifying elements.
mantle
mantle/mod.rs:70-81Type::List,adjust715-739,lower_ty781-802;lower_expr1174-1283;monomorph.rs:272-283is_mono_type;simplify.rsexpr traversals;mantle/sexpr.rs:88-111+ From.
core
core/mod.rs:279-290Type::List;lower_typ321-357;convert_external_type673-699; newcore::Expr::List(+free_vars_aux
70-103,rename121-155,type_of157-189); mantle→core arm inconvert(582-663, next to theTuplearm at 621).core/sexpr.rs: Type To 34-81 / From 410-514 (novariantparse arm
exists — addlisttag plus the"list"tag to the allow-lists at
465-475 and 731-735);convert_external_type_to_core842-869; Expr sexpr
93-176 / 534-702.
nucleus (nucleus/mod.rs — 12 match sites)
emit_val_typ104 (→ I32); AppExternal param lift 439-483 (String arm
441-458 is the template:LocalTee;I32Load(0);I32Load(4));
AppExternal ret_on_heap resv 487-536 (String arm 491-502 is the
template:alloc(4, 8)header-only);Access585-592 (List: no arm /
unreachable — lists have no fields);emit_exprnew list-literal arm
(332-358 String arm is the construction template);emit_store_expr
729-795 (→todo!(), String parity);emit_case_slots840-890 (→todo!()parity — no sum case may carry a list in scope);emit_value_slots990-1048 (→todo!()parity — no record field may
carry a list in scope);flatten_fields1109-1117 (verify a List result
is resv-handled as a unit, not field-flattened);emit_load1138-1176
(→todo!()parity);size_align1193-1212 (→ (4,4) like String; note
the record-field-inlining gap this implies is pre-existing for String
too);core_flat_slots1568-1577 (→ 2);core_flat_tys1581-1593 (→[I32, I32]);flatten_type1504-1525 (List →[I32, I32]).- Element store inside the literal (new code, small): scalar =
emit_scalar_slot-style store at8 + i*elem_size; sum element =memory.copyof the element region (inlined-region convention); resource
element = i32 handle store. Sizes come fromsize_align/cases_dt_size_align.
component (nucleus/component.rs)
EmitInstanceType::convert_ext_ty_to_ctyp442-507 andEmitComponent::convert_ty_to_ctyp90-149: List arms emittingdefined_type().list(elem)— wasm-encoder 0.242.0 hasComponentDefinedTypeEncoder::list(types.rs:618); addemit_list_typenext toemit_record_type/emit_variant_type.
Instance-type list needs a synthetic export name (e.g.list<N>); matching
is structural, name only avoids clashes.
external types
external_type.rs:ExternalType::List(Box<ExternalType>)+ sexpr To
49-84 / From 130-263 + roundtrip test (281-331 pattern).
Fixture changes (this plan)
Sums world (stdlib/wit/sums.wit + stdlib/src/sums.blr +stdlib/src/sums.rs): two new interface functions + trivial host impls:
lsum: func(xs: list<f64>) -> f64; # flat-element list through the boundary
lreverse: func(xs: list<f64>) -> list<f64>; # list RESULT (exercises the resv path)
No other WIT changes in this plan.
Deferred: df integration (later follow-up, decisions recorded)
The df WIT (stdlib/wit/world.wit:3-29) still blocks df completion in two
ways besides lists; these are done in the follow-up (host filter staystodo!() until then):
binary {left: u32, ..., right: u32}→ change both fields tos64
(blr has no u32; unsigned types are a future blr item). [decided]- Case order — reorder
node {integer, binary}andbinaryoperator
(8 cases) in world.wit to alphabetical (panics at crust/mod.rs:447/482
otherwise; see declared-layout-order.md). [decided — no layout work;
reorder the fixture] stdlib/src/df.blrgains extern type declarations (sums.blr syntax):pub extern type binaryoperator = [ `addition unit | `concat unit | ... ] pub extern type binary = {left: i64, op: binaryoperator, right: i64} pub extern type node = [ `binary binary | `integer i64 ] pub extern fn filter(frame: DataFrame, predicate: list<node>) -> DataFrame;stdlib/src/df.rs— implement hostfilter(currentlytodo!();
semantics decision deferred with the implementation — traverse+identity
vs real datafusion predicate, the latter degenerate without a
column-name form innode). [deferred]- Extend
lang/tests/integration/df.blr: construct[ `integer(1), `binary({left: 2, op: `addition, right: 3}) ]and
calldf::filter(df::fromcsv("test.csv"), ...)→df::show(...)→1
(proves sum-in-list-in-param with nested record-in-sum).
Test strategy
- Sums-world list fixtures (new, in
stdlib/wit/sums.wit+ sums.blr +
sums.rs host):lsum(xs: list<f64>) -> f64(flat-element list through the
boundary) andlreverse(xs: list<f64>) -> list<f64>(exercises the
list-result resv path, which df alone never does —filterreturns a
resource). Both trivial host impls. lang/tests/integration/list_flat.blr:lsum([1.5, 2.5])→4;
empty-list caselsum([])→0.lang/tests/integration/list_ret.blr:lsum(lreverse([1.5, 2.5]))→4.
(The df.blr extension with a constructedlist<node>moves to the deferred
df follow-up — item 5 there.)- Unit tests: parser tests (list type + literal, incl.
[]);ExternalType::Listsexpr roundtrip;flatten_type/core_flat_tys
List; component-type snapshot withdefined_type().list(wasmprinter,component_type_testspattern); core sexpr roundtrip forType::List. - Gates: all pre-existing tests byte-green;
BLR_SNAPSHOTWAT inspection of a
list call to confirm(begin, len)slot emission + header layout;cargo fmt+cargo clippy --workspace -- -D warnings+ full workspace.
Work items
| # | Item | Phase |
|---|---|---|
| L1 | air: </> tokens (verify >>), list<T> TypeExpr, list literal rule (uncomment :197, allow empty), AST + sexpr | 1 |
| L2 | crust: Type::List through ty/unify/inst/subst/sexpr; crust::Expr::List + infer; _lower_typ; convert_to_ext_typ | 1 |
| L3 | mantle + core: type/expr arms, Type::List, core::Expr::List, sexpr (+ tag allow-lists), mantle→core conversion | 1 |
| L4 | nucleus: 12 match arms (param lift, resv ret, literal construction with element stores, flat facts); ExternalType::List + flatten_type + external sexpr | 1 |
| L5 | component: List arms + emit_list_type in both converters; defined_type().list | 1 |
| L6 | Unit tests for L1-L5 (parser, roundtrips, snapshots) | 1 |
| L7 | Sums-world fixtures: lsum/lreverse WIT + sums.blr decls + host impls; list_flat.blr + list_ret.blr integration tests | 2 |
| L8 | Hardening: snapshot WAT audit of list call sites, plan status updates (this file → knowledge/design, sum-types deferred item, declared-layout-order note), fmt/clippy/full, final commit | 3 |
Deferred (not work items here): the entire df integration — see
"Deferred: df integration" (WIT u32→s64 + case reorder, df.blr extern
declarations incl. filter, host filter impl, df.blr test extension).
Commits: one per phase (plumbing / fixtures+tests / hardening), per session
convention.
Risks
- LALRPOP lexer:
</>introduction next to">>"— run forward-expr and
precedence parser tests first; token-order regression is the top parser
risk. - Type-constructor surface (L2) is wide but mechanical; the row machinery is
untouched (lists are not rows). flatten_fieldsbehavior for a List in resv context (L4 note: verify a
list result is resv-handled as a unit, not field-flattened).
Decisions (2026-08-31)
- Scope: list support + explicit list tests only. df integration
deferred — hostfilterstaystodo!(), df fixture work later. - Empty list literal
[]: included. - For the deferred df follow-up (recorded so it's unblocked):
- WIT
binaryfieldsu32→s64(blr i64; unsigned is a future blr
item). - Reorder
node/binaryoperatorin world.wit to alphabetical — do
not do the declared-layout-order work. - Host
filtersemantics decided at that time (traverse+identity vs
real datafusion predicate; the latter is degenerate without a
column-name form innode).
- WIT
All questions resolved.
Progress log
- 2026-08-31: Plan written (ABI confirmed against wasmtime 40
POINTER_PAIR+ specstore_list/load_list; full touch-point map from
repo survey; existing commented-out grammar rule at parser.lalrpop:197
noted). Not started. - 2026-08-31: Decisions recorded — alphabetical WIT reorder (no layout work),
u32→s64, empty[]included; hostfilterlevel still open (a
clarification given for option (b)'s degeneracy). - 2026-08-31: Scope narrowed per user — df integration deferred (host
filterstaystodo!()); this plan ships list plumbing + sums-worldlsum/lreversefixtures +list_flat/list_retintegration tests.
df WIT decisions carried into "Deferred: df integration". - 2026-09-01: L1–L5 implemented (air syntax/AST, crust, mantle, core, nucleus,
component emission,ExternalType::List) — one commit per layer. - 2026-09-01: L6 unit tests landed (parser list literal/type, air + core +
external sexpr roundtrips incl. empty list,flatten_type/core_flat_tys/size_alignlist facts, component-type snapshot withdefined_type().listand the syntheticlist<N>export name). - 2026-09-01: L7 fixtures landed — sums-world
lsum/lreverse(WIT +
sums.blr + host) andlist_flat/list_retintegration tests. The new
tests exposed a codegen bug: the list region was allocated align 4, but
the canonical ABI requires thebeginpointer aligned to the element
alignment (8 for f64) — fixed by allocating the region atmax(4, elem_align). Added aprint_wasmdev example forBLR_SNAPSHOTWAT inspection. - 2026-09-01: L8 hardening — snapshot WAT audit confirmed the
(begin, len)
header layout (begin = region + 8) and the header-onlyalloc(4, 8)
resv path for list results; plan moved from.opencode/plans/toknowledge/design/; sum-types deferred item and declared-layout-order
note updated. fmt/clippy/full workspace green. Done.