use wasm_encoder::{
CodeSection, EntityType, Function, FunctionSection, ImportSection, MemoryType, Module,
TypeSection, ValType,
};
use wit_parser::abi::{WasmSignature, WasmType};
use wit_parser::{Function as WitFunction, Resolve, SizeAlign, Type};
use super::super::super::abi::emit::{BlobSlice, RecordLayout, MAX_UTF8_LEN, STRING_FLAT_BYTES};
use super::super::blob::NameInterner;
use super::super::cells::CellLayout;
use super::super::schema::{RECORD_FIELD_TUPLE_IDX, RECORD_FIELD_TUPLE_NAME, RECORD_INFO_FIELDS};
use super::super::{FuncClassified, FuncShape};
use super::plan::{desugar_map_aliases, ArmGuard, Cell, HandleKind, LiftPlan, MapAliases};
use super::sidetable::flags_info::FlagsRuntimeFill;
use super::sidetable::handle_info::HandleRuntimeFill;
use super::sidetable::record_info::RecordRuntimeFill;
use super::sidetable::variant_info::VariantRuntimeFill;
use super::sidetable::{CellSideData, CharScratch, TupleIdxSource};
use super::*;
const TEST_WIT: &str = r#"
package test:lift@0.0.1;
interface t {
enum color { red, green, blue }
enum mood { happy, sad }
flags fperms { read, write, exec }
flags fcaps { net, fs, time, env, rand }
variant shape { circle, sq(u32), tri(u32) }
record point { x: u32, y: s32 }
record solo { v: u32 }
record nested { p: point, c: color }
record two-enums { c: color, m: mood }
record pair { a: u8, b: u8 }
record point-and-tuple { p: point, t: tuple<u8, s32> }
record perms-pair { primary: fperms, secondary: fperms }
record shape-pair { lhs: shape, rhs: shape }
f-mixed: func(a: bool, s: string, b: list<u8>, x: s64);
f-color: func(c: color);
f-flags: func(p: fperms);
f-point: func(p: point);
f-mix-records: func(p: point, n: nested);
f-tuple: func(t: tuple<u8, s32>);
f-tuple-of-tuple: func(t: tuple<u8, tuple<s32, s32>>);
f-record-with-tuple: func(rt: point-and-tuple);
f-record-with-flags: func(rwf: perms-pair);
f-perms-result: func() -> perms-pair;
f-variant-shape: func(s: shape);
f-record-with-variant: func(rwv: shape-pair);
f-option-u32: func(o: option<u32>);
f-option-string: func(o: option<string>);
f-option-option: func(o: option<option<u32>>);
record point-and-option { p: point, o: option<u32> }
f-record-with-option: func(rwo: point-and-option);
f-result-u32-string: func(r: result<u32, string>);
f-result-unit-err: func(r: result<_, string>);
f-result-ok-unit: func(r: result<u32>);
f-result-both-unit: func(r: result);
// Joined-flat widening: ok=u32 → [I32], err=u64 → [I64];
// joined slot 1 = I64. Ok arm's Cell::IntegerZeroExt reads
// slot 1 as I32 → emit bitcast I64→I32.
f-result-u32-u64: func(r: result<u32, u64>);
// Three-arm widening: a(u32) → [I32], b(u64) → [I64],
// c(f64) → [F64]; joined slot 1 = I64. a + c arms widen,
// b matches.
variant tri-arm { a(u32), b(u64), c(f64) }
f-variant-tri-arm: func(v: tri-arm);
// F32 leaf in a widened slot: a(f32) → [F32], b(u64) → [I64];
// joined slot 1 = I64. a's f32 leaf reads slot 1 expecting
// F32 → emit bitcast I64→F32 via `lcl.widen_f32`.
variant f32-widen { a(f32), b(u64) }
f-variant-f32-widen: func(v: f32-widen);
resource my-res;
record handle-pair { primary: own<my-res>, secondary: borrow<my-res> }
f-handle-own: func(h: own<my-res>);
f-handle-borrow: func(h: borrow<my-res>);
f-record-with-handle: func(hp: handle-pair);
f-stream-u32: async func(s: stream<u32>);
f-future-string: async func(fut: future<string>);
f-stream-of-res: async func(s: stream<my-res>);
record stream-pair { events: stream<u32>, ack: future<u32> }
f-record-with-stream: async func(rs: stream-pair);
f-error-context: func(e: error-context);
f-result-with-err-ctx: func(r: result<s32, error-context>);
f-list-u32: func(xs: list<u32>);
f-list-string: func(xs: list<string>);
f-list-char: func(xs: list<char>);
f-list-option-u32: func(xs: list<option<u32>>);
f-list-option-string: func(xs: list<option<string>>);
f-list-option-option-u32: func(xs: list<option<option<u32>>>);
f-list-result-u32-string: func(xs: list<result<u32, string>>);
f-list-result-unit-string: func(xs: list<result<_, string>>);
f-list-tuple-u32-u32: func(xs: list<tuple<u32, u32>>);
f-list-tuple-u32-string: func(xs: list<tuple<u32, string>>);
f-list-tuple-of-tuple: func(xs: list<tuple<u32, tuple<s32, s32>>>);
f-list-handle-own: func(xs: list<own<my-res>>);
f-list-handle-borrow: func(xs: list<borrow<my-res>>);
f-list-error-context: func(xs: list<error-context>);
f-list-flags: func(xs: list<fperms>);
f-list-tuple-of-flags: func(xs: list<tuple<fperms, fperms>>);
// Mixed-width tuple element: fperms (3 bits) + fcaps (5 bits)
// — cumulative `scratch_offset_in_elem` for the second cell
// must equal `3 * STRING_FLAT_BYTES`, not 5x. Catches a
// regression where the offset is computed from a uniform
// per-cell stride.
f-list-tuple-mixed-flags: func(xs: list<tuple<fperms, fcaps>>);
f-list-tuple-of-handles:
func(xs: list<tuple<own<my-res>, borrow<my-res>>>);
record handle-and-stuff { h: own<my-res>, x: u32 }
f-handles-mixed-with-list:
func(top: own<my-res>, xs: list<own<my-res>>);
record list-char-pair { items: list<char>, scores: list<u32> }
f-record-with-list-char: func(rcp: list-char-pair);
f-result-list-u32: func() -> list<u32>;
f-result-list-char: func() -> list<char>;
f-list-of-list: func(xs: list<list<u32>>);
// `list<list<T>>` per inner-T kind in the depth-2 nested-list
// scope (scalar / char / option / result / tuple / enum). Each
// exercises the inner element class through the nested
// `emit_list_of_arm` recursion.
f-list-of-list-string: func(xs: list<list<string>>);
f-list-of-list-char: func(xs: list<list<char>>);
f-list-of-list-option-u32: func(xs: list<list<option<u32>>>);
f-list-of-list-result-u32-string:
func(xs: list<list<result<u32, string>>>);
f-list-of-list-tuple-u32-u32: func(xs: list<list<tuple<u32, u32>>>);
f-list-of-list-color: func(xs: list<list<color>>);
// Inner T = record: per-outer-iter cursors for both cell-array
// and record-info — pre-pass sums `inner_len_j * records_per_elem`
// into wrapper-level `next_record_idx`; emit snaps + advances
// `inner.record_slot_base` from `nested_inner_record_cursor`.
f-list-of-list-point: func(xs: list<list<point>>);
// Inner T = own / borrow — `Cell::Handle` rides nested
// `handle_cursor`. Borrow lives in this fixture only (echo
// signature in `tier2_shapes()` can't return borrow).
f-list-of-list-handle-own: func(xs: list<list<own<my-res>>>);
f-list-of-list-handle-borrow: func(xs: list<list<borrow<my-res>>>);
// Nested-list at result position — retptr-loaded compound.
f-result-list-of-list-u32: func() -> list<list<u32>>;
// Depth-3 nesting: scalar leaf pins the recursive
// `emit_pre_pass_data_walk` (the second pass walks the middle
// list's data per outer iter). Per-kind-leaf variants pin each
// kind's cursor recursion through three levels.
f-list-of-list-of-list-u32: func(xs: list<list<list<u32>>>);
f-list-of-list-of-list-char: func(xs: list<list<list<char>>>);
f-list-of-list-of-list-flags: func(xs: list<list<list<fperms>>>);
f-list-of-list-of-list-point: func(xs: list<list<list<point>>>);
f-list-of-list-of-list-shape: func(xs: list<list<list<shape>>>);
f-list-of-list-of-list-handle-own: func(xs: list<list<list<own<my-res>>>>);
// Multi-kind leaf — record carrying a handle field exercises
// both record + handle cursors through three levels in one shot.
record point-and-handle { p: point, h: own<my-res> }
f-list-of-list-of-list-record-with-handle:
func(xs: list<list<list<point-and-handle>>>);
// Depth-5 — the recursive second pass nests four deep
// (outer → mid → mid2 → mid3). No new code path beyond
// depth-3; pins that recursion is generic with no hardcoded
// depth ceiling.
f-list-of-list-of-list-of-list-of-list-u32:
func(xs: list<list<list<list<list<u32>>>>>);
// `list<wrapper-with-list>`: inner `list<T>` shares its parent's
// element plan with sibling cells. Each Cell::ListOf gets its
// own NestedListLocals entry keyed by cell_pos.
f-list-option-list-u32: func(xs: list<option<list<u32>>>);
f-list-tuple-with-list-u32: func(xs: list<tuple<u32, list<u32>>>);
record list-field-record { ys: list<u32> }
f-list-record-with-list: func(xs: list<list-field-record>);
variant list-arm-variant { v(list<u32>), empty }
f-list-variant-list-arm: func(xs: list<list-arm-variant>);
// Both arms carry a list — same joined ptr/len slots, distinct
// Cell::ListOf cells. Snap+advance in main emit must arm-gate or
// the inactive arm's sibling cell would double-advance the cursor.
variant list-arms-both-list { a(list<u32>), b(list<u32>) }
f-list-variant-both-arms-list: func(xs: list<list-arms-both-list>);
f-list-result-with-list-ok: func(xs: list<result<list<u32>, u32>>);
f-list-result-with-list-both: func(xs: list<result<list<u32>, list<u32>>>);
record list-pair { items: list<string>, scores: list<u32> }
f-list-of-record: func(xs: list<point>);
// `list<T, N>` (canon-ABI fixed-length list) flattens to
// `N × flat(T)` inlined — same shape as `tuple<T;N>`. Lift
// desugars at plan-build into `Cell::TupleOf` with N children.
f-fixed-list-u32: func(xs: list<u32, 3>);
// N=1: WIT forbids 1-tuples, so single-child TupleOf is only
// reachable via FixedLengthList.
f-fixed-list-u32-one: func(xs: list<u32, 1>);
// N=0: WIT grammar parses it, plan-build bails with a clear
// error rather than constructing an empty TupleOf.
f-fixed-list-u32-zero: func(xs: list<u32, 0>);
// N over the layout budget — pre-bail at plan-build so we
// can't overflow `bump_flat_slot`'s u32 counter.
f-fixed-list-u32-huge: func(xs: list<u32, 17>);
record point-and-fixed-list { p: point, xs: list<u32, 3> }
f-record-with-fixed-list: func(rwf: point-and-fixed-list);
// `list<list<u32, 3>>`: outer dynamic list, each element is a
// 3-wide TupleOf — exercises `Cell::TupleOf` as a list element
// via `PrestagedTupleIndices`.
f-list-of-fixed-list: func(xs: list<list<u32, 3>>);
// FixedLengthList nested in FixedLengthList — outer TupleOf
// wraps inner TupleOf children. Mirrors `nested_tuple_…` but
// through the desugar path.
f-fixed-list-of-fixed-list: func(xs: list<list<u32, 2>, 3>);
// Result-side coverage: `is_compound_result` must accept
// FixedLengthList so the wrapper retptr-loads it correctly.
f-result-fixed-list-u32: func() -> list<u32, 3>;
// `map<K, V>` is canonical-ABI shorthand for `list<tuple<K, V>>`.
// Lift desugars via a synthetic tuple typedef, then reuses the
// existing `Cell::ListOf` + `Cell::TupleOf` path.
f-map-string-u32: func(m: map<string, u32>);
record map-pair { primary: map<string, u32>, secondary: map<u32, string> }
f-record-with-map: func(rwm: map-pair);
f-result-map: func() -> map<string, u32>;
// map<K, V> ≡ list<tuple<K, V>>, so map<K, list<list<T>>>
// desugars to a list whose tuple element holds a nested list —
// same wrapper-with-list axis as the fixtures above.
f-map-of-list-of-list: func(m: map<string, list<list<u32>>>);
// Type-alias chain over Map: `desugar_map_aliases` must register
// the underlying Map typedef even when the function references
// it through an alias. Push then peels the alias and lands on
// the same Map typeid — alias map lookup succeeds.
type aliased-map = map<string, u32>;
f-aliased-map: func(m: aliased-map);
// `list<variant>` is still gated — covers the bail path that
// the deleted `list_of_compound_element_bails_at_plan_build`
// generic-test used to assert. New still-gated kinds drop in
// here as one-liners.
f-list-of-variant: func(xs: list<shape>);
// Multi-variant element — pins `variants_per_elem > 1` +
// cumulative `entry_offset_in_elem` so a uniform-stride bug
// would compute different cell-array indices than the
// cumulative-count formula.
f-list-tuple-of-variants: func(xs: list<tuple<shape, shape>>);
// Multi-record element with mismatched field counts (point=2,
// solo=1) — pins literal `tuples_offset_in_elem` so a
// uniform-stride bug (e.g. records_per_elem * max_fields *
// tuple_size) doesn't silently match the cumulative formula.
f-list-tuple-record-mixed: func(xs: list<tuple<point, solo>>);
f-result-list-list: func(r: result<list<u32>, list<u32>>);
variant list-or-int { with-list(list<u32>), plain(u32) }
f-variant-list-arm: func(v: list-or-int);
f-option-list: func(o: option<list<u32>>);
// Depth-2 nesting: outer result, inner variant, list in
// case 0 of the inner variant. The list-of cell must carry
// both `(outer disc, ok=0)` and `(inner disc, case=0)`.
f-result-of-variant-with-list:
func(v: result<list-or-int, u32>);
}
"#;
pub(super) struct TestResolve {
inner: Resolve,
aliases: MapAliases,
}
impl TestResolve {
fn resolve(&self) -> &Resolve {
&self.inner
}
fn aliases(&self) -> &MapAliases {
&self.aliases
}
}
fn test_resolve() -> TestResolve {
let mut inner = Resolve::new();
inner
.push_str("test.wit", TEST_WIT)
.expect("test WIT must parse");
let aliases = desugar_map_aliases(&mut inner);
TestResolve { inner, aliases }
}
fn setup() -> (TestResolve, NameInterner) {
(test_resolve(), NameInterner::new())
}
fn iface_id(resolve: &TestResolve) -> wit_parser::InterfaceId {
super::super::test_utils::iface_by_unversioned_qname(resolve.resolve(), "test:lift/t")
}
fn type_named(resolve: &TestResolve, name: &str) -> Type {
Type::Id(
resolve.resolve().interfaces[iface_id(resolve)]
.types
.get(name)
.copied()
.unwrap_or_else(|| panic!("type `{name}` not found in fixture")),
)
}
fn func_named<'a>(resolve: &'a TestResolve, name: &str) -> &'a WitFunction {
resolve.resolve().interfaces[iface_id(resolve)]
.functions
.get(name)
.unwrap_or_else(|| panic!("function `{name}` not found in fixture"))
}
fn plan_for(ty: &Type, resolve: &TestResolve, names: &mut NameInterner) -> LiftPlan {
LiftPlan::for_type(ty, resolve.resolve(), names, resolve.aliases())
.expect("test fixture must classify")
}
fn plan_for_named(name: &str, resolve: &TestResolve, names: &mut NameInterner) -> LiftPlan {
plan_for(&type_named(resolve, name), resolve, names)
}
fn plan_for_param(func_name: &str, resolve: &TestResolve, names: &mut NameInterner) -> LiftPlan {
plan_for(&func_named(resolve, func_name).params[0].ty, resolve, names)
}
#[track_caller]
fn assert_plan(plan: &LiftPlan, cells: Vec<Cell>, root: u32, slot_count: u32) {
assert_eq!(plan.cells, cells);
assert_eq!(plan.root(), root);
assert_eq!(plan.flat_slot_count, slot_count);
}
#[track_caller]
fn assert_plan_no_root(plan: &LiftPlan, cells: Vec<Cell>, slot_count: u32) {
assert_eq!(plan.cells, cells);
assert_eq!(plan.flat_slot_count, slot_count);
}
fn flags_cell(names: &mut NameInterner, flat_slot: u32, type_name: &str, items: &[&str]) -> Cell {
let type_name = names.intern(type_name);
let flag_names = items.iter().map(|n| names.intern(n)).collect();
Cell::Flags {
flat_slot,
type_name,
flag_names,
}
}
fn enum_cell(names: &mut NameInterner, flat_slot: u32, type_name: &str, cases: &[&str]) -> Cell {
let type_name = names.intern(type_name);
let case_names = cases.iter().map(|n| names.intern(n)).collect();
Cell::EnumCase {
flat_slot,
type_name,
case_names,
entry_offset: 0,
}
}
fn variant_cell(
names: &mut NameInterner,
disc_slot: u32,
type_name: &str,
cases: &[&str],
per_case_payload: Vec<Option<u32>>,
) -> Cell {
let type_name = names.intern(type_name);
let case_names = cases.iter().map(|n| names.intern(n)).collect();
Cell::Variant {
disc_slot,
per_case_payload,
type_name,
case_names,
}
}
fn record_of(names: &mut NameInterner, type_name: &str, fields: &[(&str, u32)]) -> Cell {
let type_name = names.intern(type_name);
let fields = fields.iter().map(|(n, i)| (names.intern(n), *i)).collect();
Cell::RecordOf { type_name, fields }
}
fn dummy_sig() -> WasmSignature {
WasmSignature {
params: Vec::new(),
results: Vec::new(),
indirect_params: false,
retptr: false,
}
}
fn make_param(ty: &Type, resolve: &TestResolve, names: &mut NameInterner) -> ParamLift {
ParamLift {
name: BlobSlice::EMPTY,
plan: plan_for(ty, resolve, names),
}
}
fn func_with_params(
resolve: &TestResolve,
names: &mut NameInterner,
param_names: &[&str],
) -> FuncClassified {
let params = param_names
.iter()
.map(|n| make_param(&type_named(resolve, n), resolve, names))
.collect();
FuncClassified {
shape: FuncShape::Sync,
result_ty: None,
import_module: String::new(),
import_field: String::new(),
export_name: String::new(),
export_sig: dummy_sig(),
import_sig: dummy_sig(),
needs_cabi_post: false,
fn_name_offset: 0,
fn_name_len: 0,
params,
result_lift: None,
borrow_drops: Vec::new(),
}
}
fn synth_record_info_layouts(resolve: &Resolve) -> (RecordLayout, RecordLayout) {
let mut sizes = SizeAlign::default();
sizes.fill(resolve);
let entry = RecordLayout::for_named_fields(
&sizes,
&[
("type-name".into(), Type::String),
(RECORD_INFO_FIELDS.into(), Type::String),
],
);
let tuple = RecordLayout::for_named_fields(
&sizes,
&[
(RECORD_FIELD_TUPLE_NAME.into(), Type::String),
(RECORD_FIELD_TUPLE_IDX.into(), Type::U32),
],
);
(entry, tuple)
}
fn synth_cell_layout() -> CellLayout {
let common_wit = include_str!("../../../../wit/common/world.wit");
let mut resolve = Resolve::new();
resolve
.push_str("common.wit", common_wit)
.expect("wit/common/world.wit must parse");
let common_id =
super::super::test_utils::iface_by_unversioned_qname(&resolve, "splicer:common/types");
let cell_id = resolve.interfaces[common_id]
.types
.get("cell")
.copied()
.expect("splicer:common/types must export `cell`");
let mut sizes = SizeAlign::default();
sizes.fill(&resolve);
CellLayout::from_resolve(&sizes, &resolve, cell_id)
}
fn synth_info_layout(record_name: &str) -> RecordLayout {
let common_wit = include_str!("../../../../wit/common/world.wit");
let mut resolve = Resolve::new();
resolve
.push_str("common.wit", common_wit)
.expect("wit/common/world.wit must parse");
let common_id =
super::super::test_utils::iface_by_unversioned_qname(&resolve, "splicer:common/types");
let record_id = resolve.interfaces[common_id]
.types
.get(record_name)
.copied()
.unwrap_or_else(|| panic!("splicer:common/types must export `{record_name}`"));
let mut sizes = SizeAlign::default();
sizes.fill(&resolve);
RecordLayout::for_record_typedef(&sizes, &resolve, record_id)
}
fn plan_param_types(plan: &LiftPlan, resolve: &Resolve) -> Vec<ValType> {
use super::super::super::abi::emit::wasm_type_to_val;
use super::super::super::abi::flat_types;
flat_types(resolve, &plan.source_ty, None)
.expect("plan source_ty must flatten within MAX_FLAT_PARAMS")
.into_iter()
.map(wasm_type_to_val)
.collect()
}
fn auto_cell_side_data(plan: &LiftPlan) -> Vec<CellSideData> {
const U32_BYTES: u32 = 4;
const FLAGS_SCRATCH_BASE: u32 = 0x1000;
const STUB_FLAG_NAME_STRIDE: u32 = 16;
const STUB_FLAG_NAME_LEN: u32 = 4;
const RECORD_TUPLES_BASE: u32 = 0x4000;
const RECORD_TUPLES_STRIDE: u32 = 16;
const STUB_RECORD_NAME_LEN: u32 = 4;
let mut record_idx: u32 = 0;
let mut tuple_cursor: u32 = 0;
let mut flags_cursor: u32 = FLAGS_SCRATCH_BASE;
let mut flags_idx: u32 = 0;
let mut variant_idx: u32 = 0;
let mut char_cursor: u32 = 0x3000;
let mut handle_idx: u32 = 0;
plan.cells
.iter()
.map(|op| match op {
Cell::RecordOf { fields, .. } => {
use super::sidetable::record_info::RecordSlotSource;
let entry_idx = record_idx;
let fields_ptr = (RECORD_TUPLES_BASE + entry_idx * RECORD_TUPLES_STRIDE) as i32;
let fill = RecordRuntimeFill {
slot_source: RecordSlotSource::Static {
entry_idx,
fields_ptr,
},
type_name: BlobSlice {
off: 0,
len: STUB_RECORD_NAME_LEN,
},
fields_len: fields.len() as u32,
};
record_idx += 1;
CellSideData::Record(Box::new(fill))
}
Cell::TupleOf { children } => {
let off = tuple_cursor;
let len = children.len() as u32;
tuple_cursor += len * U32_BYTES;
CellSideData::Tuple {
source: TupleIdxSource::Static(BlobSlice { off, len }),
}
}
Cell::Flags { flag_names, .. } => {
use super::sidetable::flags_info::FlagsSlotSource;
let scratch_addr = flags_cursor;
flags_cursor += flag_names.len() as u32 * STRING_FLAT_BYTES;
let fill = FlagsRuntimeFill {
slot_source: FlagsSlotSource::Static {
entry_idx: flags_idx,
scratch_addr: scratch_addr as i32,
},
type_name: BlobSlice {
off: 0,
len: STUB_FLAG_NAME_LEN,
},
flag_names: (0..flag_names.len() as u32)
.map(|i| BlobSlice {
off: i * STUB_FLAG_NAME_STRIDE,
len: STUB_FLAG_NAME_LEN,
})
.collect(),
};
flags_idx += 1;
CellSideData::Flags(Box::new(fill))
}
Cell::Variant {
case_names,
per_case_payload,
..
} => {
use super::sidetable::variant_info::VariantSlotSource;
let fill = VariantRuntimeFill {
slot_source: VariantSlotSource::Static {
entry_idx: variant_idx,
},
type_name: BlobSlice {
off: 0,
len: STUB_FLAG_NAME_LEN,
},
case_names: (0..case_names.len() as u32)
.map(|i| BlobSlice {
off: i * STUB_FLAG_NAME_STRIDE,
len: STUB_FLAG_NAME_LEN,
})
.collect(),
per_case_payload: per_case_payload.clone(),
};
variant_idx += 1;
CellSideData::Variant(Box::new(fill))
}
Cell::Char { .. } => {
let scratch_addr = char_cursor;
char_cursor += MAX_UTF8_LEN;
CellSideData::Char {
scratch: CharScratch::Static {
scratch_addr: scratch_addr as i32,
},
}
}
Cell::Handle { type_name, .. } => {
use super::sidetable::handle_info::HandleSlotSource;
let fill = HandleRuntimeFill {
slot_source: HandleSlotSource::Static(handle_idx),
type_name: *type_name,
};
handle_idx += 1;
CellSideData::Handle(Box::new(fill))
}
Cell::Bool { .. }
| Cell::IntegerSignExt { .. }
| Cell::IntegerZeroExt { .. }
| Cell::Integer64 { .. }
| Cell::FloatingF32 { .. }
| Cell::FloatingF64 { .. }
| Cell::Text { .. }
| Cell::Bytes { .. }
| Cell::EnumCase { .. }
| Cell::Option { .. }
| Cell::Result { .. }
| Cell::ListOf { .. } => CellSideData::None,
})
.collect()
}
fn validate_emit_lift_plan(plan: &LiftPlan, resolve: &Resolve) {
use super::super::super::indices::LocalsBuilder;
use crate::adapter::indices::FrozenLocals;
let mut sizes = SizeAlign::default();
sizes.fill(resolve);
let cell_layout = synth_cell_layout();
let handle_info_layout = synth_info_layout("handle-info");
let flags_info_layout = synth_info_layout("flags-info");
let record_info_layout = synth_info_layout("record-info");
let variant_info_layout = synth_info_layout("variant-info");
let (_, record_field_tuple_layout) = synth_record_info_layouts(resolve);
let cell_side = auto_cell_side_data(plan);
let param_types = plan_param_types(plan, resolve);
let n = plan.flat_slot_count;
let mut builder = LocalsBuilder::new(n);
let addr = builder.alloc_local(ValType::I32);
let st = builder.alloc_local(ValType::I32);
let ws = builder.alloc_local(ValType::I32);
let ext64 = builder.alloc_local(ValType::I64);
let ext_f64 = builder.alloc_local(ValType::F64);
let widen_i32_a = builder.alloc_local(ValType::I32);
let widen_i32_b = builder.alloc_local(ValType::I32);
let widen_f32 = builder.alloc_local(ValType::F32);
let flags_addr = builder.alloc_local(ValType::I32);
let flags_count = builder.alloc_local(ValType::I32);
let char_len = builder.alloc_local(ValType::I32);
let char_scratch_addr = builder.alloc_local(ValType::I32);
let list_elem_child_idx = builder.alloc_local(ValType::I32);
let tuple_slot_ptr = builder.alloc_local(ValType::I32);
let id_local = builder.alloc_local(ValType::I64);
let saved_bump = builder.alloc_local(ValType::I32);
let cells_base = builder.alloc_local(ValType::I32);
let next_cell_idx = builder.alloc_local(ValType::I32);
let handle_info_base = builder.alloc_local(ValType::I32);
let flags_info_base = builder.alloc_local(ValType::I32);
let record_info_base = builder.alloc_local(ValType::I32);
let variant_info_base = builder.alloc_local(ValType::I32);
let next_handle_idx = builder.alloc_local(ValType::I32);
let next_flags_idx = builder.alloc_local(ValType::I32);
let next_record_idx = builder.alloc_local(ValType::I32);
let next_variant_idx = builder.alloc_local(ValType::I32);
let list_elem_handle_base = builder.alloc_local(ValType::I32);
let handle_slot_addr = builder.alloc_local(ValType::I32);
let handle_payload_idx = builder.alloc_local(ValType::I32);
let list_elem_flags_base = builder.alloc_local(ValType::I32);
let list_elem_flags_scratch_base = builder.alloc_local(ValType::I32);
let flags_slot_addr = builder.alloc_local(ValType::I32);
let flags_payload_idx = builder.alloc_local(ValType::I32);
let list_elem_record_base = builder.alloc_local(ValType::I32);
let list_elem_record_tuples_base = builder.alloc_local(ValType::I32);
let record_slot_addr = builder.alloc_local(ValType::I32);
let record_payload_idx = builder.alloc_local(ValType::I32);
let record_tuples_slice_addr = builder.alloc_local(ValType::I32);
let list_elem_variant_base = builder.alloc_local(ValType::I32);
let variant_slot_addr = builder.alloc_local(ValType::I32);
let variant_payload_idx = builder.alloc_local(ValType::I32);
let list_locals = super::emit::alloc_list_emit_locals(
plan,
resolve,
&sizes,
record_field_tuple_layout.size,
&mut builder,
);
let FrozenLocals { locals } = builder.freeze();
let lcl = WrapperLocals {
addr,
st,
ws,
ext64,
ext_f64,
widen_i32_a,
widen_i32_b,
widen_f32,
flags_addr,
flags_count,
char_len: Some(char_len),
char_scratch_addr: Some(char_scratch_addr),
list_elem_child_idx: Some(list_elem_child_idx),
tuple_slot_ptr: Some(tuple_slot_ptr),
cells_base,
next_cell_idx,
handle_info_base: Some(handle_info_base),
flags_info_base: Some(flags_info_base),
record_info_base: Some(record_info_base),
variant_info_base: Some(variant_info_base),
next_handle_idx: Some(next_handle_idx),
next_flags_idx: Some(next_flags_idx),
next_record_idx: Some(next_record_idx),
next_variant_idx: Some(next_variant_idx),
list_elem_handle_base: Some(list_elem_handle_base),
handle_slot_addr: Some(handle_slot_addr),
handle_payload_idx: Some(handle_payload_idx),
list_elem_flags_base: Some(list_elem_flags_base),
list_elem_flags_scratch_base: Some(list_elem_flags_scratch_base),
flags_slot_addr: Some(flags_slot_addr),
flags_payload_idx: Some(flags_payload_idx),
list_elem_record_base: Some(list_elem_record_base),
list_elem_record_tuples_base: Some(list_elem_record_tuples_base),
record_slot_addr: Some(record_slot_addr),
record_payload_idx: Some(record_payload_idx),
record_tuples_slice_addr: Some(record_tuples_slice_addr),
list_elem_variant_base: Some(list_elem_variant_base),
variant_slot_addr: Some(variant_slot_addr),
variant_payload_idx: Some(variant_payload_idx),
result: None,
tr_addr: None,
id_local,
task_return_loads: None,
params_lower_seq: None,
saved_bump,
param_list_locals: Vec::new(),
};
let mut module = Module::new();
let mut types = TypeSection::new();
types.ty().function(
[ValType::I32, ValType::I32, ValType::I32, ValType::I32],
[ValType::I32],
);
types.ty().function(param_types.iter().copied(), []);
module.section(&types);
let mut imports = ImportSection::new();
imports.import(
"env",
"memory",
EntityType::Memory(MemoryType {
minimum: 1,
maximum: None,
memory64: false,
shared: false,
page_size_log2: None,
}),
);
imports.import("env", "cabi_realloc", EntityType::Function(0));
module.section(&imports);
let mut funcs = FunctionSection::new();
funcs.function(1);
module.section(&funcs);
let mut code = CodeSection::new();
let mut f = Function::new_with_locals_types(locals);
f.instructions().i32_const(0);
f.instructions().local_set(lcl.cells_base);
let lift_ctx = super::emit::LiftEmitCtx {
cell_layout: &cell_layout,
cabi_realloc_idx: 0,
handle_info: super::emit::HandleInfoOffsets::from_layout(&handle_info_layout),
flags_info: super::emit::FlagsInfoOffsets::from_layout(&flags_info_layout),
record_info: super::emit::RecordInfoOffsets::from_layout(
&record_info_layout,
&record_field_tuple_layout,
),
variant_info: super::emit::VariantInfoOffsets::from_layout(
&variant_info_layout,
super::super::super::abi::emit::option_payload_offset(&sizes, &Type::U32),
),
};
emit_lift_plan(
&mut f,
&lift_ctx,
plan,
super::emit::CellSideRefs {
cell_side: &cell_side,
},
0,
&lcl,
&list_locals,
);
f.instructions().end();
code.function(&f);
module.section(&code);
wasmparser::Validator::new()
.validate_all(&module.finish())
.expect("emit_lift_plan output must validate (list path)");
}
#[test]
fn primitives_assign_one_cell_one_slot() {
let r = test_resolve();
let mut names = NameInterner::new();
let cases: &[(Type, Cell)] = &[
(Type::Bool, Cell::Bool { flat_slot: 0 }),
(Type::S32, Cell::IntegerSignExt { flat_slot: 0 }),
(Type::U32, Cell::IntegerZeroExt { flat_slot: 0 }),
(Type::S64, Cell::Integer64 { flat_slot: 0 }),
(Type::F32, Cell::FloatingF32 { flat_slot: 0 }),
(Type::F64, Cell::FloatingF64 { flat_slot: 0 }),
];
for (ty, expected) in cases {
let plan = plan_for(ty, &r, &mut names);
assert_eq!(plan.cells, vec![expected.clone()], "{ty:?}");
assert_eq!(plan.flat_slot_count, 1, "{ty:?}");
}
}
#[test]
fn string_takes_two_flat_slots() {
let mut names = NameInterner::new();
let plan = plan_for(&Type::String, &test_resolve(), &mut names);
assert_eq!(
plan.cells,
vec![Cell::Text {
ptr_slot: 0,
len_slot: 1
}]
);
assert_eq!(plan.flat_slot_count, 2);
}
#[test]
fn list_u8_classifies_as_bytes_cell() {
let (r, mut names) = setup();
let bytes_ty = func_named(&r, "f-mixed").params[2].ty;
let plan = plan_for(&bytes_ty, &r, &mut names);
assert_plan_no_root(
&plan,
vec![Cell::Bytes {
ptr_slot: 0,
len_slot: 1,
}],
2,
);
}
#[test]
fn char_assigns_one_cell_one_slot() {
let r = test_resolve();
let mut names = NameInterner::new();
let plan = plan_for(&Type::Char, &r, &mut names);
assert_eq!(plan.cells, vec![Cell::Char { flat_slot: 0 }]);
assert_eq!(plan.flat_slot_count, 1);
}
#[test]
fn enum_carries_named_list_info() {
let (r, mut names) = setup();
assert_eq!(
plan_for_named("color", &r, &mut names).cells,
vec![enum_cell(&mut names, 0, "color", &["red", "green", "blue"])],
);
}
#[test]
fn flags_assigns_one_cell_one_slot() {
let (r, mut names) = setup();
let plan = plan_for_named("fperms", &r, &mut names);
assert_plan_no_root(
&plan,
vec![flags_cell(
&mut names,
0,
"fperms",
&["read", "write", "exec"],
)],
1,
);
}
#[test]
fn variant_lays_disc_first_then_arms_share_slots() {
let (r, mut names) = setup();
let plan = plan_for_named("shape", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::IntegerZeroExt { flat_slot: 1 },
variant_cell(
&mut names,
0,
"shape",
&["circle", "sq", "tri"],
vec![None, Some(0), Some(1)],
),
],
2,
2,
);
}
#[test]
fn record_with_variant_field_recurses_into_variant() {
let (r, mut names) = setup();
let plan = plan_for_named("shape-pair", &r, &mut names);
let shape_cases: &[&str] = &["circle", "sq", "tri"];
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::IntegerZeroExt { flat_slot: 1 },
variant_cell(
&mut names,
0,
"shape",
shape_cases,
vec![None, Some(0), Some(1)],
),
Cell::IntegerZeroExt { flat_slot: 3 },
Cell::IntegerZeroExt { flat_slot: 3 },
variant_cell(
&mut names,
2,
"shape",
shape_cases,
vec![None, Some(3), Some(4)],
),
record_of(&mut names, "shape-pair", &[("lhs", 2), ("rhs", 5)]),
],
6,
4,
);
}
#[test]
fn handle_assigns_one_cell_one_slot() {
let (r, mut names) = setup();
let plan = plan_for_param("f-handle-own", &r, &mut names);
let res_name = names.intern("my-res");
assert_plan(
&plan,
vec![Cell::Handle {
flat_slot: 0,
type_name: res_name,
kind: HandleKind::Resource,
}],
0,
1,
);
}
#[test]
fn borrow_handle_takes_same_shape_as_own() {
let (r, mut names) = setup();
let own_plan = plan_for_param("f-handle-own", &r, &mut names);
let borrow_plan = plan_for_param("f-handle-borrow", &r, &mut names);
assert_eq!(own_plan.cells, borrow_plan.cells);
assert_eq!(own_plan.flat_slot_count, borrow_plan.flat_slot_count);
}
#[test]
fn record_with_handle_field_recurses_into_handle() {
let (r, mut names) = setup();
let plan = plan_for_named("handle-pair", &r, &mut names);
let res_name = names.intern("my-res");
assert_plan(
&plan,
vec![
Cell::Handle {
flat_slot: 0,
type_name: res_name,
kind: HandleKind::Resource,
},
Cell::Handle {
flat_slot: 1,
type_name: res_name,
kind: HandleKind::Resource,
},
record_of(
&mut names,
"handle-pair",
&[("primary", 0), ("secondary", 1)],
),
],
2,
2,
);
}
#[test]
fn stream_handle_assigns_one_cell_one_slot() {
let (r, mut names) = setup();
let plan = plan_for_param("f-stream-u32", &r, &mut names);
let empty = names.intern("");
assert_plan_no_root(
&plan,
vec![Cell::Handle {
flat_slot: 0,
type_name: empty,
kind: HandleKind::Stream,
}],
1,
);
}
#[test]
fn future_handle_takes_same_shape_as_stream() {
let (r, mut names) = setup();
let plan = plan_for_param("f-future-string", &r, &mut names);
let empty = names.intern("");
assert_plan_no_root(
&plan,
vec![Cell::Handle {
flat_slot: 0,
type_name: empty,
kind: HandleKind::Future,
}],
1,
);
}
#[test]
fn stream_with_named_element_carries_element_type_name() {
let (r, mut names) = setup();
let plan = plan_for_param("f-stream-of-res", &r, &mut names);
let res_name = names.intern("my-res");
assert_eq!(
plan.cells,
vec![Cell::Handle {
flat_slot: 0,
type_name: res_name,
kind: HandleKind::Stream,
}],
);
}
#[test]
fn record_with_stream_and_future_fields_recurses_into_handle() {
let (r, mut names) = setup();
let plan = plan_for_named("stream-pair", &r, &mut names);
let empty = names.intern("");
assert_plan(
&plan,
vec![
Cell::Handle {
flat_slot: 0,
type_name: empty,
kind: HandleKind::Stream,
},
Cell::Handle {
flat_slot: 1,
type_name: empty,
kind: HandleKind::Future,
},
record_of(&mut names, "stream-pair", &[("events", 0), ("ack", 1)]),
],
2,
2,
);
}
#[test]
fn error_context_assigns_one_cell_one_slot() {
let (r, mut names) = setup();
let plan = plan_for_param("f-error-context", &r, &mut names);
let empty = names.intern("");
assert_plan(
&plan,
vec![Cell::Handle {
flat_slot: 0,
type_name: empty,
kind: HandleKind::ErrorContext,
}],
0,
1,
);
}
#[test]
fn result_with_error_context_err_arm_recurses_into_handle() {
let (r, mut names) = setup();
let plan = plan_for_param("f-result-with-err-ctx", &r, &mut names);
let empty = names.intern("");
assert_plan(
&plan,
vec![
Cell::IntegerSignExt { flat_slot: 1 },
Cell::Handle {
flat_slot: 1,
type_name: empty,
kind: HandleKind::ErrorContext,
},
Cell::Result {
disc_slot: 0,
ok_idx: Some(0),
err_idx: Some(1),
},
],
2,
2,
);
}
#[test]
fn list_of_primitive_carries_element_plan() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-u32", &r, &mut names);
assert_eq!(plan.cells.len(), 1);
let Cell::ListOf {
ptr_slot,
len_slot,
element_plan,
..
} = &plan.cells[0]
else {
panic!("expected Cell::ListOf, got {:?}", plan.cells[0]);
};
assert_eq!(*ptr_slot, 0);
assert_eq!(*len_slot, 1);
assert_eq!(plan.flat_slot_count, 2);
assert_eq!(plan.root(), 0);
assert_eq!(
element_plan.cells,
vec![Cell::IntegerZeroExt { flat_slot: 0 }],
);
assert_eq!(element_plan.flat_slot_count, 1);
assert_eq!(element_plan.root(), 0);
}
#[test]
fn list_of_string_element_plan_uses_two_local_slots() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-string", &r, &mut names);
assert_eq!(plan.flat_slot_count, 2);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!("expected Cell::ListOf");
};
assert_eq!(
element_plan.cells,
vec![Cell::Text {
ptr_slot: 0,
len_slot: 1,
}],
);
assert_eq!(element_plan.flat_slot_count, 2);
}
#[test]
fn list_of_char_element_plan_uses_one_local_slot() {
let r = test_resolve();
let mut names = NameInterner::new();
let plan = plan_for(&func_named(&r, "f-list-char").params[0].ty, &r, &mut names);
assert_eq!(plan.flat_slot_count, 2);
let Cell::ListOf {
ptr_slot,
len_slot,
element_plan,
..
} = &plan.cells[0]
else {
panic!("expected Cell::ListOf, got {:?}", plan.cells[0]);
};
assert_eq!(*ptr_slot, 0);
assert_eq!(*len_slot, 1);
assert_eq!(plan.root(), 0);
assert_eq!(element_plan.cells, vec![Cell::Char { flat_slot: 0 }]);
assert_eq!(element_plan.flat_slot_count, 1);
assert_eq!(element_plan.root(), 0);
}
#[test]
fn list_of_option_u32_element_plan_is_two_cells() {
let r = test_resolve();
let mut names = NameInterner::new();
let plan = plan_for(
&func_named(&r, "f-list-option-u32").params[0].ty,
&r,
&mut names,
);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!("expected ListOf, got {:?}", plan.cells[0]);
};
assert_eq!(
element_plan.cells,
vec![
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::Option {
disc_slot: 0,
child_idx: 0,
},
],
);
assert_eq!(element_plan.flat_slot_count, 2);
assert_eq!(element_plan.root(), 1);
}
#[test]
fn list_of_result_u32_string_element_plan_shape() {
let r = test_resolve();
let mut names = NameInterner::new();
let plan = plan_for(
&func_named(&r, "f-list-result-u32-string").params[0].ty,
&r,
&mut names,
);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!("expected ListOf, got {:?}", plan.cells[0]);
};
assert_eq!(element_plan.cells.len(), 3);
let Cell::Result {
disc_slot,
ok_idx,
err_idx,
} = element_plan.cells[2]
else {
panic!(
"expected Result at element-plan cell 2, got {:?}",
element_plan.cells[2]
);
};
assert_eq!(disc_slot, 0);
assert_eq!(ok_idx, Some(0));
assert_eq!(err_idx, Some(1));
}
#[test]
fn list_of_tuple_u32_string_element_plan_shape() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-tuple-u32-string", &r, &mut names);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!("expected ListOf, got {:?}", plan.cells[0]);
};
assert_eq!(element_plan.cells.len(), 3);
let Cell::TupleOf { children } = &element_plan.cells[2] else {
panic!(
"expected TupleOf at element-plan cell 2, got {:?}",
element_plan.cells[2]
);
};
assert_eq!(children.as_slice(), &[0, 1]);
assert_eq!(element_plan.root(), 2);
}
fn list_element_plan(plan: &LiftPlan, cell_idx: usize) -> &LiftPlan {
match &plan.cells[cell_idx] {
Cell::ListOf { element_plan, .. } => element_plan,
other => panic!("expected ListOf at cell {cell_idx}, got {other:?}"),
}
}
#[test]
fn walk_element_plan_counts_match_side_data_lockstep() {
use super::sidetable::flags_info::FlagsSlotSource;
use super::sidetable::handle_info::HandleSlotSource;
use super::sidetable::record_info::RecordSlotSource;
use super::sidetable::variant_info::VariantSlotSource;
let (r, mut names) = setup();
let (_, record_tuple_layout) = synth_record_info_layouts(r.resolve());
let record_tuple_size = record_tuple_layout.size;
for fn_name in [
"f-list-tuple-u32-string",
"f-list-tuple-of-tuple",
"f-list-tuple-u32-u32",
"f-list-handle-own",
"f-list-tuple-of-handles",
"f-list-error-context",
"f-list-flags",
"f-list-tuple-of-flags",
"f-list-tuple-mixed-flags",
"f-list-of-record",
"f-list-tuple-record-mixed",
"f-list-of-variant",
"f-list-tuple-of-variants",
] {
let plan = plan_for_param(fn_name, &r, &mut names);
let elem_plan = list_element_plan(&plan, 0);
let (side, counts) = super::emit::walk_element_plan(elem_plan, record_tuple_size);
assert_eq!(side.len(), elem_plan.cells.len());
let mut expected_chars = 0u32;
let mut expected_tuple_slots = 0u32;
let mut expected_handles = 0u32;
let mut expected_flags = 0u32;
let mut expected_flags_scratch = 0u32;
let mut expected_records = 0u32;
let mut expected_record_tuples_bytes = 0u32;
let mut expected_variants = 0u32;
let mut running_tuple_off = 0u32;
for (cell, sd) in elem_plan.cells.iter().zip(side.iter()) {
match cell {
Cell::Char { .. } => {
assert!(matches!(sd, CellSideData::Char { .. }));
expected_chars += 1;
}
Cell::TupleOf { children } => {
let CellSideData::Tuple {
source: TupleIdxSource::PerIteration { offset_in_elem },
} = sd
else {
panic!("expected PerIteration TupleOf side data, got {sd:?}");
};
assert_eq!(
*offset_in_elem, running_tuple_off,
"TupleOf offsets must be cumulative across element_plan",
);
running_tuple_off += children.len() as u32 * 4;
expected_tuple_slots += children.len() as u32;
}
Cell::Handle { .. } => {
let CellSideData::Handle(fill) = sd else {
panic!("expected Handle side data, got {sd:?}");
};
let HandleSlotSource::PerIteration { offset_in_elem } = fill.slot_source else {
panic!(
"list-element Handle must carry PerIteration slot source, \
got {:?}",
fill.slot_source,
);
};
assert_eq!(
offset_in_elem, expected_handles,
"Handle offset_in_elem must be the cumulative count",
);
expected_handles += 1;
}
Cell::Flags { flag_names, .. } => {
let CellSideData::Flags(fill) = sd else {
panic!("expected Flags side data, got {sd:?}");
};
let FlagsSlotSource::PerIteration {
entry_offset_in_elem,
scratch_offset_in_elem,
} = fill.slot_source
else {
panic!(
"list-element Flags must carry PerIteration slot source, \
got {:?}",
fill.slot_source,
);
};
assert_eq!(
entry_offset_in_elem, expected_flags,
"Flags entry_offset_in_elem must be the cumulative count",
);
assert_eq!(
scratch_offset_in_elem, expected_flags_scratch,
"Flags scratch_offset_in_elem must be cumulative \
(sum of prior cells' set-flags scratch sizes)",
);
expected_flags += 1;
expected_flags_scratch += flag_names.len() as u32 * STRING_FLAT_BYTES;
}
Cell::RecordOf { fields, .. } => {
let CellSideData::Record(fill) = sd else {
panic!("expected Record side data, got {sd:?}");
};
let RecordSlotSource::PerIteration {
entry_offset_in_elem,
tuples_offset_in_elem,
} = fill.slot_source
else {
panic!(
"list-element Record must carry PerIteration slot source, \
got {:?}",
fill.slot_source,
);
};
assert_eq!(
entry_offset_in_elem, expected_records,
"Record entry_offset_in_elem must be the cumulative count",
);
assert_eq!(
tuples_offset_in_elem, expected_record_tuples_bytes,
"Record tuples_offset_in_elem must be cumulative \
(sum of prior cells' field-tuples sizes)",
);
expected_records += 1;
expected_record_tuples_bytes += fields.len() as u32 * record_tuple_size;
}
Cell::Variant { .. } => {
let CellSideData::Variant(fill) = sd else {
panic!("expected Variant side data, got {sd:?}");
};
let VariantSlotSource::PerIteration {
entry_offset_in_elem,
} = fill.slot_source
else {
panic!(
"list-element Variant must carry PerIteration slot source, \
got {:?}",
fill.slot_source,
);
};
assert_eq!(
entry_offset_in_elem, expected_variants,
"Variant entry_offset_in_elem must be the cumulative count",
);
expected_variants += 1;
}
_ => assert!(matches!(sd, CellSideData::None)),
}
}
assert_eq!(
counts.chars, expected_chars,
"counts.chars drift in {fn_name}",
);
assert_eq!(
counts.tuple_idx_slots, expected_tuple_slots,
"counts.tuple_idx_slots drift in {fn_name}",
);
assert_eq!(
counts.handles, expected_handles,
"counts.handles drift in {fn_name}",
);
assert_eq!(
counts.flags, expected_flags,
"counts.flags drift in {fn_name}",
);
assert_eq!(
counts.flags_scratch_bytes, expected_flags_scratch,
"counts.flags_scratch_bytes drift in {fn_name}",
);
assert_eq!(
counts.records, expected_records,
"counts.records drift in {fn_name}",
);
assert_eq!(
counts.record_tuples_bytes, expected_record_tuples_bytes,
"counts.record_tuples_bytes drift in {fn_name}",
);
assert_eq!(
counts.variants, expected_variants,
"counts.variants drift in {fn_name}",
);
}
}
#[test]
fn walk_element_plan_pins_mixed_flags_scratch_bytes_literal() {
use super::sidetable::flags_info::FlagsSlotSource;
let (r, mut names) = setup();
let plan = plan_for_param("f-list-tuple-mixed-flags", &r, &mut names);
let elem_plan = list_element_plan(&plan, 0);
let (_, record_tuple_layout) = synth_record_info_layouts(r.resolve());
let (side, counts) = super::emit::walk_element_plan(elem_plan, record_tuple_layout.size);
let flags_offsets: Vec<(u32, u32)> = side
.iter()
.filter_map(|sd| match sd {
CellSideData::Flags(fill) => match fill.slot_source {
FlagsSlotSource::PerIteration {
entry_offset_in_elem,
scratch_offset_in_elem,
} => Some((entry_offset_in_elem, scratch_offset_in_elem)),
_ => panic!("list-element Flags must be PerIteration"),
},
_ => None,
})
.collect();
assert_eq!(
flags_offsets,
vec![(0, 0), (1, 3 * STRING_FLAT_BYTES)],
"tuple<fperms, fcaps> must give cell 0 (entry 0, scratch 0) \
and cell 1 (entry 1, scratch 3 * STRING_FLAT_BYTES)",
);
assert_eq!(counts.flags, 2);
assert_eq!(counts.flags_scratch_bytes, (3 + 5) * STRING_FLAT_BYTES);
}
#[test]
fn walk_element_plan_pins_mixed_record_tuples_bytes_literal() {
use super::sidetable::record_info::RecordSlotSource;
let (r, mut names) = setup();
let (_, record_tuple_layout) = synth_record_info_layouts(r.resolve());
let tuple_size = record_tuple_layout.size;
let plan = plan_for_param("f-list-tuple-record-mixed", &r, &mut names);
let elem_plan = list_element_plan(&plan, 0);
let (side, counts) = super::emit::walk_element_plan(elem_plan, tuple_size);
let record_offsets: Vec<(u32, u32)> = side
.iter()
.filter_map(|sd| match sd {
CellSideData::Record(fill) => match fill.slot_source {
RecordSlotSource::PerIteration {
entry_offset_in_elem,
tuples_offset_in_elem,
} => Some((entry_offset_in_elem, tuples_offset_in_elem)),
_ => panic!("list-element Record must be PerIteration"),
},
_ => None,
})
.collect();
assert_eq!(
record_offsets,
vec![(0, 0), (1, 2 * tuple_size)],
"tuple<point, solo> must give point (entry 0, tuples 0) \
and solo (entry 1, tuples 2 * tuple_size)",
);
assert_eq!(counts.records, 2);
assert_eq!(counts.record_tuples_bytes, (2 + 1) * tuple_size);
}
#[test]
fn walk_element_plan_zero_counts_for_scalar_only() {
let (r, mut names) = setup();
let (_, record_tuple_layout) = synth_record_info_layouts(r.resolve());
for fn_name in ["f-list-u32", "f-list-string"] {
let plan = plan_for_param(fn_name, &r, &mut names);
let elem_plan = list_element_plan(&plan, 0);
let (_, counts) = super::emit::walk_element_plan(elem_plan, record_tuple_layout.size);
assert_eq!(counts.chars, 0, "{fn_name} should have no char cells");
assert_eq!(
counts.tuple_idx_slots, 0,
"{fn_name} should have no tuple-idx slots",
);
assert_eq!(counts.handles, 0, "{fn_name} should have no handle cells");
assert_eq!(counts.flags, 0, "{fn_name} should have no flags cells");
assert_eq!(counts.records, 0, "{fn_name} should have no record cells");
assert_eq!(counts.variants, 0, "{fn_name} should have no variant cells");
}
}
#[test]
fn record_with_list_char_field_recurses_into_list() {
let r = test_resolve();
let mut names = NameInterner::new();
let plan = plan_for_named("list-char-pair", &r, &mut names);
assert_eq!(plan.cells.len(), 3);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!("expected ListOf at cell 0, got {:?}", plan.cells[0]);
};
assert_eq!(element_plan.cells, vec![Cell::Char { flat_slot: 0 }]);
assert!(matches!(plan.cells[1], Cell::ListOf { .. }));
assert_eq!(
plan.cells[2],
record_of(&mut names, "list-char-pair", &[("items", 0), ("scores", 1)],),
);
assert_eq!(plan.flat_slot_count, 4);
assert_eq!(plan.root(), 2);
}
#[test]
fn nested_list_inner_with_option_pins_child_idx_class() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-of-list-option-u32", &r, &mut names);
let [Cell::ListOf { element_plan, .. }] = plan.cells.as_slice() else {
panic!("expected one outer ListOf, got {:?}", plan.cells);
};
let [Cell::ListOf {
element_plan: inner_plan,
..
}] = element_plan.cells.as_slice()
else {
panic!("expected one inner ListOf, got {:?}", element_plan.cells);
};
assert_eq!(inner_plan.cells.len(), 2);
assert!(matches!(inner_plan.cells[1], Cell::Option { .. }));
assert!(plan.any_list_element_has_class(super::plan::ListElementClass::PrestagedChildIdx));
assert!(plan.any_list_element_has_class(super::plan::ListElementClass::PrestagedNestedList));
}
#[test]
fn nested_list_builds_with_listof_element() {
let (r, mut names) = setup();
let plan = LiftPlan::for_type(
&func_named(&r, "f-list-of-list").params[0].ty,
r.resolve(),
&mut names,
r.aliases(),
)
.expect("list<list<u32>> plan-build must succeed");
let [Cell::ListOf { element_plan, .. }] = plan.cells.as_slice() else {
panic!("expected one outer ListOf, got {:?}", plan.cells);
};
let [Cell::ListOf {
element_plan: inner_plan,
..
}] = element_plan.cells.as_slice()
else {
panic!(
"expected one inner ListOf in outer element plan, got {:?}",
element_plan.cells
);
};
assert!(matches!(
inner_plan.cells.as_slice(),
[Cell::IntegerZeroExt { flat_slot: 0 }]
));
}
#[test]
fn nested_list_under_wrapper_emits_valid_wasm() {
let (r, mut names) = setup();
for fixture in [
"f-list-option-list-u32",
"f-list-tuple-with-list-u32",
"f-list-record-with-list",
"f-list-variant-list-arm",
"f-list-variant-both-arms-list",
"f-list-result-with-list-ok",
"f-list-result-with-list-both",
] {
let plan = plan_for_param(fixture, &r, &mut names);
validate_emit_lift_plan(&plan, r.resolve());
}
}
#[test]
fn map_classifies_as_list_of_tuple() {
let (r, mut names) = setup();
let plan = plan_for_param("f-map-string-u32", &r, &mut names);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!("expected ListOf at cell 0, got {:?}", plan.cells[0]);
};
assert_eq!(element_plan.cells.len(), 3);
assert!(matches!(
element_plan.cells[0],
Cell::Text {
ptr_slot: 0,
len_slot: 1
}
));
assert!(matches!(
element_plan.cells[1],
Cell::IntegerZeroExt { flat_slot: 2 }
));
let Cell::TupleOf { children } = &element_plan.cells[2] else {
panic!(
"expected TupleOf at element-plan cell 2, got {:?}",
element_plan.cells[2]
);
};
assert_eq!(children.as_slice(), &[0, 1]);
assert_eq!(plan.flat_slot_count, 2);
}
#[test]
fn map_in_record_classifies_with_inner_list() {
let (r, mut names) = setup();
let plan = plan_for_param("f-record-with-map", &r, &mut names);
assert!(matches!(plan.cells[0], Cell::ListOf { .. }));
assert!(matches!(plan.cells[1], Cell::ListOf { .. }));
let Cell::RecordOf { fields, .. } = &plan.cells[2] else {
panic!("expected RecordOf at cell 2, got {:?}", plan.cells[2]);
};
assert_eq!(
fields.iter().map(|(_, idx)| *idx).collect::<Vec<_>>(),
vec![0, 1]
);
}
#[test]
fn map_with_nested_list_value_emits_valid_wasm() {
let (r, mut names) = setup();
let plan = plan_for_param("f-map-of-list-of-list", &r, &mut names);
validate_emit_lift_plan(&plan, r.resolve());
}
#[test]
fn map_through_type_alias_classifies() {
let (r, mut names) = setup();
let plan = plan_for_param("f-aliased-map", &r, &mut names);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!(
"aliased map must classify as ListOf at cell 0, got {:?}",
plan.cells[0]
);
};
assert!(matches!(
element_plan.cells.last(),
Some(Cell::TupleOf { .. })
));
}
#[test]
fn list_of_variant_classifies_with_perivariant_element() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-of-variant", &r, &mut names);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!("expected ListOf at cell 0, got {:?}", plan.cells[0]);
};
assert!(matches!(
element_plan.cells.last(),
Some(Cell::Variant { .. })
));
assert!(plan.has_list_elem_variant());
}
#[test]
fn list_of_record_classifies_with_perirecord_element() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-of-record", &r, &mut names);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!("expected ListOf at cell 0, got {:?}", plan.cells[0]);
};
assert!(matches!(
element_plan.cells.last(),
Some(Cell::RecordOf { .. })
));
assert!(plan.has_list_elem_record());
}
#[test]
fn list_inside_result_arm_carries_disc_guards() {
let (r, mut names) = setup();
let plan = plan_for_param("f-result-list-list", &r, &mut names);
assert_eq!(plan.cells.len(), 3);
let Cell::ListOf {
arm_guards: ok_guards,
..
} = &plan.cells[0]
else {
panic!("expected ok arm Cell::ListOf at 0");
};
let Cell::ListOf {
arm_guards: err_guards,
..
} = &plan.cells[1]
else {
panic!("expected err arm Cell::ListOf at 1");
};
assert!(matches!(plan.cells[2], Cell::Result { .. }));
assert_eq!(
ok_guards.as_slice(),
&[ArmGuard {
disc_slot: 0,
expected_disc: 0,
}]
);
assert_eq!(
err_guards.as_slice(),
&[ArmGuard {
disc_slot: 0,
expected_disc: 1,
}]
);
}
#[test]
fn list_inside_variant_arm_carries_case_disc_guard() {
let (r, mut names) = setup();
let plan = plan_for_param("f-variant-list-arm", &r, &mut names);
let Cell::ListOf { arm_guards, .. } = &plan.cells[0] else {
panic!("expected Cell::ListOf at 0");
};
assert_eq!(
arm_guards.as_slice(),
&[ArmGuard {
disc_slot: 0,
expected_disc: 0,
}]
);
}
#[test]
fn list_inside_nested_arms_stacks_guards_outer_to_inner() {
let (r, mut names) = setup();
let plan = plan_for_param("f-result-of-variant-with-list", &r, &mut names);
let Cell::ListOf { arm_guards, .. } = &plan.cells[0] else {
panic!("expected Cell::ListOf at 0, got {:?}", plan.cells[0]);
};
assert_eq!(
arm_guards.as_slice(),
&[
ArmGuard {
disc_slot: 0,
expected_disc: 0,
},
ArmGuard {
disc_slot: 1,
expected_disc: 0,
},
],
);
}
fn assert_arm_guards_match_joined_ancestry(plan: &LiftPlan) {
fn walk(plan: &LiftPlan, idx: u32, depth: usize) {
match &plan.cells[idx as usize] {
Cell::Result {
ok_idx, err_idx, ..
} => {
if let Some(i) = ok_idx {
walk(plan, *i, depth + 1);
}
if let Some(i) = err_idx {
walk(plan, *i, depth + 1);
}
}
Cell::Variant {
per_case_payload, ..
} => {
for slot in per_case_payload.iter().flatten() {
walk(plan, *slot, depth + 1);
}
}
Cell::Option { child_idx, .. } => walk(plan, *child_idx, depth),
Cell::RecordOf { fields, .. } => {
for (_, i) in fields {
walk(plan, *i, depth);
}
}
Cell::TupleOf { children } => {
for i in children {
walk(plan, *i, depth);
}
}
Cell::ListOf {
arm_guards,
element_plan,
..
} => {
assert_eq!(
arm_guards.len(),
depth,
"ListOf at cell {idx} has {} guards, expected {depth} for joined-arm ancestry",
arm_guards.len(),
);
assert_arm_guards_match_joined_ancestry(element_plan);
}
Cell::Bool { .. }
| Cell::IntegerSignExt { .. }
| Cell::IntegerZeroExt { .. }
| Cell::Integer64 { .. }
| Cell::FloatingF32 { .. }
| Cell::FloatingF64 { .. }
| Cell::Text { .. }
| Cell::Bytes { .. }
| Cell::Char { .. }
| Cell::EnumCase { .. }
| Cell::Flags { .. }
| Cell::Handle { .. } => {}
}
}
walk(plan, plan.root(), 0);
}
#[test]
fn arm_guards_match_joined_arm_ancestry_for_every_list_fixture() {
let (r, mut names) = setup();
let plans = [
plan_for_param("f-list-u32", &r, &mut names),
plan_for_param("f-list-string", &r, &mut names),
plan_for_param("f-list-char", &r, &mut names),
plan_for_param("f-list-option-u32", &r, &mut names),
plan_for_param("f-list-result-u32-string", &r, &mut names),
plan_for_param("f-list-tuple-u32-string", &r, &mut names),
plan_for_param("f-list-tuple-of-tuple", &r, &mut names),
plan_for_param("f-list-handle-own", &r, &mut names),
plan_for_param("f-list-handle-borrow", &r, &mut names),
plan_for_param("f-list-error-context", &r, &mut names),
plan_for_param("f-list-flags", &r, &mut names),
plan_for_param("f-list-tuple-of-flags", &r, &mut names),
plan_for_param("f-list-tuple-mixed-flags", &r, &mut names),
plan_for_param("f-list-tuple-of-handles", &r, &mut names),
plan_for_named("list-pair", &r, &mut names),
plan_for_named("list-char-pair", &r, &mut names),
plan_for_param("f-option-list", &r, &mut names),
plan_for_param("f-result-list-list", &r, &mut names),
plan_for_param("f-variant-list-arm", &r, &mut names),
plan_for_param("f-result-of-variant-with-list", &r, &mut names),
plan_for(
&func_named(&r, "f-handles-mixed-with-list").params[1].ty,
&r,
&mut names,
),
];
for plan in &plans {
assert_arm_guards_match_joined_ancestry(plan);
}
}
#[test]
fn list_inside_option_is_allowed() {
let (r, mut names) = setup();
let plan = plan_for_param("f-option-list", &r, &mut names);
assert_eq!(plan.cells.len(), 2);
let Cell::ListOf { arm_guards, .. } = &plan.cells[0] else {
panic!("expected Cell::ListOf at 0");
};
assert!(
arm_guards.is_empty(),
"option's payload slots aren't joined; ListOf should carry no guards"
);
assert!(matches!(plan.cells[1], Cell::Option { .. }));
}
#[test]
fn record_with_list_field_recurses_into_list() {
let (r, mut names) = setup();
let plan = plan_for_named("list-pair", &r, &mut names);
assert_eq!(plan.cells.len(), 3);
assert!(matches!(plan.cells[0], Cell::ListOf { .. }));
assert!(matches!(plan.cells[1], Cell::ListOf { .. }));
assert_eq!(
plan.cells[2],
record_of(&mut names, "list-pair", &[("items", 0), ("scores", 1)]),
);
assert_eq!(plan.flat_slot_count, 4);
assert_eq!(plan.root(), 2);
}
#[test]
fn record_with_flags_field_recurses_into_flags() {
let (r, mut names) = setup();
let plan = plan_for_named("perms-pair", &r, &mut names);
assert_plan(
&plan,
vec![
flags_cell(&mut names, 0, "fperms", &["read", "write", "exec"]),
flags_cell(&mut names, 1, "fperms", &["read", "write", "exec"]),
record_of(
&mut names,
"perms-pair",
&[("primary", 0), ("secondary", 1)],
),
],
2,
2,
);
}
#[test]
fn record_lays_children_before_parent() {
let (r, mut names) = setup();
let plan = plan_for_named("point", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerSignExt { flat_slot: 1 },
record_of(&mut names, "point", &[("x", 0), ("y", 1)]),
],
2,
2,
);
}
#[test]
fn nested_record_walks_depth_first() {
let (r, mut names) = setup();
let plan = plan_for_named("nested", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerSignExt { flat_slot: 1 },
record_of(&mut names, "point", &[("x", 0), ("y", 1)]),
enum_cell(&mut names, 2, "color", &["red", "green", "blue"]),
record_of(&mut names, "nested", &[("p", 2), ("c", 3)]),
],
4,
3,
);
}
#[test]
fn tuple_lays_children_before_parent() {
let (r, mut names) = setup();
let plan = plan_for_param("f-tuple", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerSignExt { flat_slot: 1 },
Cell::TupleOf {
children: vec![0, 1],
},
],
2,
2,
);
}
#[test]
fn nested_tuple_walks_depth_first() {
let (r, mut names) = setup();
let plan = plan_for_param("f-tuple-of-tuple", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerSignExt { flat_slot: 1 },
Cell::IntegerSignExt { flat_slot: 2 },
Cell::TupleOf {
children: vec![1, 2],
},
Cell::TupleOf {
children: vec![0, 3],
},
],
4,
3,
);
}
#[test]
fn record_with_tuple_field_recurses_into_tuple() {
let (r, mut names) = setup();
let plan = plan_for_named("point-and-tuple", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerSignExt { flat_slot: 1 },
record_of(&mut names, "point", &[("x", 0), ("y", 1)]),
Cell::IntegerZeroExt { flat_slot: 2 },
Cell::IntegerSignExt { flat_slot: 3 },
Cell::TupleOf {
children: vec![3, 4],
},
record_of(&mut names, "point-and-tuple", &[("p", 2), ("t", 5)]),
],
6,
4,
);
}
#[test]
fn fixed_length_list_desugars_to_tuple_of() {
let (r, mut names) = setup();
let plan = plan_for_param("f-fixed-list-u32", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::IntegerZeroExt { flat_slot: 2 },
Cell::TupleOf {
children: vec![0, 1, 2],
},
],
3,
3,
);
}
#[test]
fn record_with_fixed_length_list_field_recurses_into_tuple() {
let (r, mut names) = setup();
let plan = plan_for_named("point-and-fixed-list", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerSignExt { flat_slot: 1 },
record_of(&mut names, "point", &[("x", 0), ("y", 1)]),
Cell::IntegerZeroExt { flat_slot: 2 },
Cell::IntegerZeroExt { flat_slot: 3 },
Cell::IntegerZeroExt { flat_slot: 4 },
Cell::TupleOf {
children: vec![3, 4, 5],
},
record_of(&mut names, "point-and-fixed-list", &[("p", 2), ("xs", 6)]),
],
7,
5,
);
}
#[test]
fn list_of_fixed_length_list_routes_through_list_element_tuple() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-of-fixed-list", &r, &mut names);
let Cell::ListOf { element_plan, .. } = &plan.cells[0] else {
panic!("expected ListOf at cell 0, got {:?}", plan.cells[0]);
};
assert_eq!(element_plan.cells.len(), 4);
assert!(matches!(
element_plan.cells[0..3],
[
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::IntegerZeroExt { flat_slot: 2 },
],
));
let Cell::TupleOf { children } = &element_plan.cells[3] else {
panic!(
"expected TupleOf at element-plan cell 3, got {:?}",
element_plan.cells[3]
);
};
assert_eq!(children.as_slice(), &[0, 1, 2]);
assert_eq!(plan.flat_slot_count, 2);
}
#[test]
fn fixed_length_list_n_one_builds_single_child_tuple_of() {
let (r, mut names) = setup();
let plan = plan_for_param("f-fixed-list-u32-one", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::TupleOf { children: vec![0] },
],
1,
1,
);
}
#[test]
fn fixed_length_list_n_zero_bails_at_plan_build() {
let (r, mut names) = setup();
let err = LiftPlan::for_type(
&func_named(&r, "f-fixed-list-u32-zero").params[0].ty,
r.resolve(),
&mut names,
r.aliases(),
)
.expect_err("N == 0 must bail");
let msg = err.to_string();
assert!(msg.contains("requires N >= 1"), "got: {msg}");
assert!(msg.contains("github.com/ejrgilbert/splicer/issues"));
}
#[test]
fn fixed_length_list_n_over_budget_bails_at_plan_build() {
let (r, mut names) = setup();
let err = LiftPlan::for_type(
&func_named(&r, "f-fixed-list-u32-huge").params[0].ty,
r.resolve(),
&mut names,
r.aliases(),
)
.expect_err("N over budget must bail");
let msg = err.to_string();
assert!(msg.contains("exceeds tier-2 layout budget"), "got: {msg}");
assert!(msg.contains("github.com/ejrgilbert/splicer/issues"));
}
#[test]
fn fixed_length_list_of_fixed_length_list_walks_depth_first() {
let (r, mut names) = setup();
let plan = plan_for_param("f-fixed-list-of-fixed-list", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::TupleOf {
children: vec![0, 1],
},
Cell::IntegerZeroExt { flat_slot: 2 },
Cell::IntegerZeroExt { flat_slot: 3 },
Cell::TupleOf {
children: vec![3, 4],
},
Cell::IntegerZeroExt { flat_slot: 4 },
Cell::IntegerZeroExt { flat_slot: 5 },
Cell::TupleOf {
children: vec![6, 7],
},
Cell::TupleOf {
children: vec![2, 5, 8],
},
],
9,
6,
);
}
#[test]
fn option_allocates_disc_before_inner() {
let (r, mut names) = setup();
let plan = plan_for_param("f-option-u32", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::Option {
disc_slot: 0,
child_idx: 0,
},
],
1,
2,
);
}
#[test]
fn option_of_string_keeps_canonical_disc_first() {
let (r, mut names) = setup();
let plan = plan_for_param("f-option-string", &r, &mut names);
assert_plan_no_root(
&plan,
vec![
Cell::Text {
ptr_slot: 1,
len_slot: 2,
},
Cell::Option {
disc_slot: 0,
child_idx: 0,
},
],
3,
);
}
#[test]
fn nested_option_walks_disc_per_layer() {
let (r, mut names) = setup();
let plan = plan_for_param("f-option-option", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 2 },
Cell::Option {
disc_slot: 1,
child_idx: 0,
},
Cell::Option {
disc_slot: 0,
child_idx: 1,
},
],
2,
3,
);
}
#[test]
fn record_with_option_field_recurses_into_option() {
let (r, mut names) = setup();
let plan = plan_for_named("point-and-option", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 0 },
Cell::IntegerSignExt { flat_slot: 1 },
record_of(&mut names, "point", &[("x", 0), ("y", 1)]),
Cell::IntegerZeroExt { flat_slot: 3 },
Cell::Option {
disc_slot: 2,
child_idx: 3,
},
record_of(&mut names, "point-and-option", &[("p", 2), ("o", 4)]),
],
5,
4,
);
}
#[test]
fn result_u32_string_shares_arms_flat_slots() {
let (r, mut names) = setup();
let plan = plan_for_param("f-result-u32-string", &r, &mut names);
assert_plan(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::Text {
ptr_slot: 1,
len_slot: 2,
},
Cell::Result {
disc_slot: 0,
ok_idx: Some(0),
err_idx: Some(1),
},
],
2,
3,
);
}
#[test]
fn result_u32_u64_records_joined_flat_widening() {
let (r, mut names) = setup();
let plan = plan_for_param("f-result-u32-u64", &r, &mut names);
assert_plan_no_root(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::Integer64 { flat_slot: 1 },
Cell::Result {
disc_slot: 0,
ok_idx: Some(0),
err_idx: Some(1),
},
],
2,
);
assert!(plan.widening_for(0).is_none());
assert_eq!(plan.widening_for(1), Some(WasmType::I64));
}
#[test]
fn variant_f32_widen_records_f32_arm_widening() {
let (r, mut names) = setup();
let plan = plan_for_param("f-variant-f32-widen", &r, &mut names);
assert_eq!(plan.flat_slot_count, 2);
assert!(plan.widening_for(0).is_none());
assert_eq!(plan.widening_for(1), Some(WasmType::I64));
}
#[test]
fn variant_tri_arm_records_joined_flat_widening() {
let (r, mut names) = setup();
let plan = plan_for_param("f-variant-tri-arm", &r, &mut names);
assert_eq!(plan.flat_slot_count, 2);
assert!(plan.widening_for(0).is_none());
assert_eq!(plan.widening_for(1), Some(WasmType::I64));
}
#[test]
fn result_u32_string_records_no_widening() {
let (r, mut names) = setup();
let plan = plan_for_param("f-result-u32-string", &r, &mut names);
for slot in 0..plan.flat_slot_count {
assert!(
plan.widening_for(slot).is_none(),
"result<u32, string> slot {slot} should not widen",
);
}
}
#[test]
fn result_unit_ok_skips_ok_child() {
let (r, mut names) = setup();
let plan = plan_for_param("f-result-unit-err", &r, &mut names);
assert_plan_no_root(
&plan,
vec![
Cell::Text {
ptr_slot: 1,
len_slot: 2,
},
Cell::Result {
disc_slot: 0,
ok_idx: None,
err_idx: Some(0),
},
],
3,
);
}
#[test]
fn result_unit_err_skips_err_child() {
let (r, mut names) = setup();
let plan = plan_for_param("f-result-ok-unit", &r, &mut names);
assert_plan_no_root(
&plan,
vec![
Cell::IntegerZeroExt { flat_slot: 1 },
Cell::Result {
disc_slot: 0,
ok_idx: Some(0),
err_idx: None,
},
],
2,
);
}
#[test]
fn result_both_unit_is_disc_only() {
let (r, mut names) = setup();
let plan = plan_for_param("f-result-both-unit", &r, &mut names);
assert_plan(
&plan,
vec![Cell::Result {
disc_slot: 0,
ok_idx: None,
err_idx: None,
}],
0,
1,
);
}
#[test]
fn classify_func_params_yields_plan_relative_slots() {
let (r, mut names) = setup();
let params = classify_func_params(
r.resolve(),
func_named(&r, "f-mixed"),
&mut names,
r.aliases(),
)
.expect("f-mixed params must classify");
assert_eq!(
params[2].plan.cells,
vec![Cell::Bytes {
ptr_slot: 0,
len_slot: 1
}],
);
let bytes_ty = func_named(&r, "f-mixed").params[2].ty;
assert_eq!(
params[2].plan.cells,
plan_for(&bytes_ty, &r, &mut names).cells,
);
}
#[test]
fn param_plan_flat_slot_counts_compose_for_emit_local_base() {
let (r, mut names) = setup();
let params = classify_func_params(
r.resolve(),
func_named(&r, "f-mixed"),
&mut names,
r.aliases(),
)
.expect("f-mixed params must classify");
let starts: Vec<u32> = params
.iter()
.scan(0u32, |acc, p| {
let s = *acc;
*acc += p.plan.flat_slot_count;
Some(s)
})
.collect();
assert_eq!(starts, vec![0, 1, 3, 5]);
assert_eq!(params.last().unwrap().plan.flat_slot_count, 1);
}
#[test]
fn char_scratch_sizes_count_single_cell_char_result() {
use super::sidetable::char_info::char_scratch_sizes;
let (r, mut names) = setup();
let mut fd = func_with_params(&r, &mut names, &[]);
fd.result_ty = Some(Type::Char);
fd.result_lift = Some(ResultLift {
source: ResultSource::Direct(Cell::Char { flat_slot: 0 }),
});
assert_eq!(char_scratch_sizes(&[fd]), vec![MAX_UTF8_LEN]);
}
#[test]
fn flags_scratch_sizes_count_both_param_and_result_cells() {
use super::classify::CompoundResult;
use super::sidetable::flags_info::flags_scratch_sizes;
let (r, mut names) = setup();
let fd_param = func_with_params(&r, &mut names, &["fperms"]);
let mut fd_result = func_with_params(&r, &mut names, &[]);
fd_result.result_lift = Some(ResultLift {
source: ResultSource::Compound(CompoundResult {
ty: type_named(&r, "perms-pair"),
plan: plan_for_named("perms-pair", &r, &mut names),
}),
});
assert_eq!(
flags_scratch_sizes(&[fd_param, fd_result]),
vec![24, 24, 24]
);
}
#[test]
fn build_enum_info_blob_stamps_entry_offsets_for_multi_enum_plan() {
use super::plan::Cell;
use super::sidetable::enum_info::build_enum_info_blob;
let (r, mut names) = setup();
let mut fd = func_with_params(&r, &mut names, &["two-enums"]);
let entry_layout = synth_info_layout("enum-info");
let segment_id = super::super::blob::SymbolBases::new().alloc();
let _ = build_enum_info_blob(std::slice::from_mut(&mut fd), &entry_layout, segment_id);
let enum_offsets: Vec<u32> = fd.params[0]
.plan
.cells
.iter()
.filter_map(|c| match c {
Cell::EnumCase { entry_offset, .. } => Some(*entry_offset),
_ => None,
})
.collect();
assert_eq!(enum_offsets, vec![0, 3]);
}
#[test]
fn build_enum_info_blob_handles_direct_result_enum() {
use super::plan::Cell;
use super::sidetable::enum_info::build_enum_info_blob;
let (r, mut names) = setup();
let mut fd = func_with_params(&r, &mut names, &[]);
fd.result_lift = Some(ResultLift {
source: ResultSource::Direct(enum_cell(&mut names, 0, "color", &["red", "green", "blue"])),
});
let entry_layout = synth_info_layout("enum-info");
let segment_id = super::super::blob::SymbolBases::new().alloc();
let blob = build_enum_info_blob(std::slice::from_mut(&mut fd), &entry_layout, segment_id);
let sym = blob.per_result[0].expect("direct enum result must produce a SymRef");
assert_eq!(sym.len, 3);
let ResultSource::Direct(Cell::EnumCase { entry_offset, .. }) =
&fd.result_lift.as_ref().unwrap().source
else {
unreachable!();
};
assert_eq!(*entry_offset, 0);
}
#[test]
fn build_enum_info_blob_stamps_list_element_enum() {
use super::classify::ParamLift;
use super::plan::Cell;
use super::sidetable::enum_info::build_enum_info_blob;
let (r, mut names) = setup();
let mut fd = func_with_params(&r, &mut names, &[]);
fd.params.push(ParamLift {
name: names.intern("xs"),
plan: plan_for_param("f-list-of-list-color", &r, &mut names),
});
let entry_layout = synth_info_layout("enum-info");
let segment_id = super::super::blob::SymbolBases::new().alloc();
let blob = build_enum_info_blob(std::slice::from_mut(&mut fd), &entry_layout, segment_id);
let sym = blob.per_param[0][0].expect("nested enum must produce a SymRef");
assert_eq!(sym.len, 3);
let outer_list = &fd.params[0].plan.cells[0];
let Cell::ListOf {
element_plan: outer_elem,
..
} = outer_list
else {
panic!("outer cell must be ListOf");
};
let Cell::ListOf {
element_plan: inner_elem,
..
} = &outer_elem.cells[0]
else {
panic!("inner cell must be ListOf");
};
let Cell::EnumCase { entry_offset, .. } = &inner_elem.cells[0] else {
panic!("innermost cell must be EnumCase");
};
assert_eq!(*entry_offset, 0);
}
#[test]
fn enum_strings_dedup_across_funcs() {
let (r, mut names) = setup();
func_with_params(&r, &mut names, &["color"]);
func_with_params(&r, &mut names, &["color"]);
assert_eq!(names.into_bytes(), b"colorredgreenblue");
}
#[test]
fn name_interner_dedupes_record_strings_across_plans() {
let (r, mut names) = setup();
let _ = vec![
func_with_params(&r, &mut names, &["point"]),
func_with_params(&r, &mut names, &["point", "nested"]),
];
let bytes = names.into_bytes();
let count = |needle: &str| {
let n = needle.as_bytes();
bytes.windows(n.len()).filter(|w| *w == n).count()
};
assert_eq!(count("point"), 1);
assert_eq!(count("nested"), 1);
assert_eq!(count("x"), 1);
assert_eq!(count("y"), 1);
}
#[test]
fn build_record_info_maps_assigns_per_param_counts_and_cell_idx() {
use super::sidetable::record_info::RecordSlotSource;
let (r, mut names) = setup();
let funcs = vec![
func_with_params(&r, &mut names, &["point"]),
func_with_params(&r, &mut names, &["point", "nested"]),
];
let (_, tuple) = synth_record_info_layouts(r.resolve());
let maps = build_record_info_maps(&funcs, &tuple, 0);
assert_eq!(maps.per_param_count, vec![vec![1], vec![1, 2]]);
let expected: Vec<Vec<&[Option<u32>]>> = vec![
vec![&[None, None, Some(0)]],
vec![
&[None, None, Some(0)],
&[None, None, Some(0), None, Some(1)],
],
];
for (fn_idx, fn_expected) in expected.iter().enumerate() {
for (param_idx, param_expected) in fn_expected.iter().enumerate() {
let actual: Vec<Option<u32>> = maps
.per_cell_fill
.for_param(fn_idx, param_idx)
.iter()
.map(|f| {
f.as_ref().map(|fill| match fill.slot_source {
RecordSlotSource::Static { entry_idx, .. } => entry_idx,
RecordSlotSource::PerIteration { .. } => {
unreachable!("outer-plan walk produces only Static fills")
}
})
})
.collect();
assert_eq!(
actual.as_slice(),
*param_expected,
"fn {fn_idx} param {param_idx}",
);
}
}
assert_eq!(maps.tuples.bytes.len() as u32, 8 * tuple.size);
}
#[test]
fn emit_lift_plan_validates_every_classify_built_shape() {
let (r, mut names) = setup();
let plans = [
plan_for(&Type::Bool, &r, &mut names),
plan_for(&Type::S32, &r, &mut names),
plan_for(&Type::U32, &r, &mut names),
plan_for(&Type::S64, &r, &mut names),
plan_for(&Type::F32, &r, &mut names),
plan_for(&Type::F64, &r, &mut names),
plan_for(&Type::String, &r, &mut names),
plan_for(&Type::Char, &r, &mut names),
plan_for(&func_named(&r, "f-mixed").params[2].ty, &r, &mut names), plan_for_named("color", &r, &mut names),
plan_for_named("fperms", &r, &mut names),
plan_for_named("shape", &r, &mut names),
plan_for_named("point", &r, &mut names),
plan_for_named("nested", &r, &mut names),
plan_for_named("perms-pair", &r, &mut names),
plan_for_named("shape-pair", &r, &mut names),
plan_for_param("f-tuple", &r, &mut names),
plan_for_param("f-tuple-of-tuple", &r, &mut names),
plan_for_named("point-and-tuple", &r, &mut names),
plan_for_param("f-option-u32", &r, &mut names),
plan_for_param("f-option-string", &r, &mut names),
plan_for_param("f-option-option", &r, &mut names),
plan_for_named("point-and-option", &r, &mut names),
plan_for_param("f-result-u32-string", &r, &mut names),
plan_for_param("f-result-unit-err", &r, &mut names),
plan_for_param("f-result-ok-unit", &r, &mut names),
plan_for_param("f-result-both-unit", &r, &mut names),
plan_for_param("f-result-u32-u64", &r, &mut names),
plan_for_param("f-variant-tri-arm", &r, &mut names),
plan_for_param("f-variant-f32-widen", &r, &mut names),
plan_for_param("f-handle-own", &r, &mut names),
plan_for_param("f-handle-borrow", &r, &mut names),
plan_for_named("handle-pair", &r, &mut names),
plan_for_param("f-stream-u32", &r, &mut names),
plan_for_param("f-future-string", &r, &mut names),
plan_for_param("f-stream-of-res", &r, &mut names),
plan_for_named("stream-pair", &r, &mut names),
plan_for_param("f-error-context", &r, &mut names),
plan_for_param("f-result-with-err-ctx", &r, &mut names),
plan_for_param("f-list-u32", &r, &mut names),
plan_for_param("f-list-string", &r, &mut names),
plan_for_param("f-list-char", &r, &mut names),
plan_for_param("f-list-option-u32", &r, &mut names),
plan_for_param("f-list-option-string", &r, &mut names),
plan_for_param("f-list-option-option-u32", &r, &mut names),
plan_for_param("f-list-result-u32-string", &r, &mut names),
plan_for_param("f-list-result-unit-string", &r, &mut names),
plan_for_param("f-list-tuple-u32-u32", &r, &mut names),
plan_for_param("f-list-tuple-u32-string", &r, &mut names),
plan_for_param("f-list-tuple-of-tuple", &r, &mut names),
plan_for_param("f-list-handle-own", &r, &mut names),
plan_for_param("f-list-handle-borrow", &r, &mut names),
plan_for_param("f-list-error-context", &r, &mut names),
plan_for_param("f-list-tuple-of-handles", &r, &mut names),
plan_for_param("f-list-flags", &r, &mut names),
plan_for_param("f-list-tuple-of-flags", &r, &mut names),
plan_for_param("f-list-tuple-mixed-flags", &r, &mut names),
plan_for_param("f-list-of-record", &r, &mut names),
plan_for_param("f-list-tuple-record-mixed", &r, &mut names),
plan_for_param("f-list-of-variant", &r, &mut names),
plan_for_param("f-list-tuple-of-variants", &r, &mut names),
plan_for(
&func_named(&r, "f-handles-mixed-with-list").params[1].ty,
&r,
&mut names,
),
plan_for_named("list-pair", &r, &mut names),
plan_for_named("list-char-pair", &r, &mut names),
plan_for_param("f-map-string-u32", &r, &mut names),
plan_for_named("map-pair", &r, &mut names),
plan_for_param("f-result-list-list", &r, &mut names),
plan_for_param("f-variant-list-arm", &r, &mut names),
plan_for_param("f-result-of-variant-with-list", &r, &mut names),
plan_for_param("f-list-of-list", &r, &mut names),
plan_for_param("f-list-of-list-string", &r, &mut names),
plan_for_param("f-list-of-list-char", &r, &mut names),
plan_for_param("f-list-of-list-option-u32", &r, &mut names),
plan_for_param("f-list-of-list-result-u32-string", &r, &mut names),
plan_for_param("f-list-of-list-tuple-u32-u32", &r, &mut names),
plan_for_param("f-list-of-list-color", &r, &mut names),
plan_for_param("f-list-of-list-point", &r, &mut names),
plan_for_param("f-list-of-list-handle-own", &r, &mut names),
plan_for_param("f-list-of-list-handle-borrow", &r, &mut names),
plan_for_param("f-list-of-list-of-list-u32", &r, &mut names),
plan_for_param("f-list-of-list-of-list-char", &r, &mut names),
plan_for_param("f-list-of-list-of-list-flags", &r, &mut names),
plan_for_param("f-list-of-list-of-list-point", &r, &mut names),
plan_for_param("f-list-of-list-of-list-shape", &r, &mut names),
plan_for_param("f-list-of-list-of-list-handle-own", &r, &mut names),
plan_for_param("f-list-of-list-of-list-record-with-handle", &r, &mut names),
plan_for_param("f-list-of-list-of-list-of-list-of-list-u32", &r, &mut names),
plan_for_param("f-list-option-list-u32", &r, &mut names),
plan_for_param("f-list-tuple-with-list-u32", &r, &mut names),
plan_for_param("f-list-record-with-list", &r, &mut names),
plan_for_param("f-list-variant-list-arm", &r, &mut names),
plan_for_param("f-list-variant-both-arms-list", &r, &mut names),
plan_for_param("f-list-result-with-list-ok", &r, &mut names),
plan_for_param("f-list-result-with-list-both", &r, &mut names),
plan_for_param("f-map-of-list-of-list", &r, &mut names),
];
for plan in &plans {
validate_emit_lift_plan(plan, r.resolve());
}
}
#[test]
fn list_of_u32_emits_valid_wasm() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-u32", &r, &mut names);
validate_emit_lift_plan(&plan, r.resolve());
}
#[test]
fn list_of_string_emits_valid_wasm() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-string", &r, &mut names);
validate_emit_lift_plan(&plan, r.resolve());
}
#[test]
fn list_of_option_u32_emits_valid_wasm() {
let r = test_resolve();
let mut names = NameInterner::new();
let plan = plan_for(
&func_named(&r, "f-list-option-u32").params[0].ty,
&r,
&mut names,
);
validate_emit_lift_plan(&plan, r.resolve());
}
#[test]
fn list_of_result_u32_string_emits_valid_wasm() {
let r = test_resolve();
let mut names = NameInterner::new();
let plan = plan_for(
&func_named(&r, "f-list-result-u32-string").params[0].ty,
&r,
&mut names,
);
validate_emit_lift_plan(&plan, r.resolve());
}
#[test]
fn list_of_tuple_u32_string_emits_valid_wasm() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-tuple-u32-string", &r, &mut names);
validate_emit_lift_plan(&plan, r.resolve());
}
#[test]
fn list_of_tuple_of_tuple_emits_valid_wasm() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-tuple-of-tuple", &r, &mut names);
validate_emit_lift_plan(&plan, r.resolve());
}
#[test]
fn list_of_char_emits_valid_wasm() {
let (r, mut names) = setup();
let plan = plan_for_param("f-list-char", &r, &mut names);
validate_emit_lift_plan(&plan, r.resolve());
}
#[test]
fn list_of_enum_emits_valid_wasm() {
let r = Resolve::new();
let mut r = r;
r.push_str(
"list-of-enum.wit",
r#"
package test:list-enum@0.0.1;
interface t {
enum color { red, green, blue }
f: func(xs: list<color>);
}
"#,
)
.unwrap();
let aliases = desugar_map_aliases(&mut r);
let iface = super::super::test_utils::iface_by_unversioned_qname(&r, "test:list-enum/t");
let func_id = r.interfaces[iface]
.functions
.keys()
.find(|n| *n == "f")
.unwrap()
.clone();
let func = &r.interfaces[iface].functions[&func_id];
let mut names = NameInterner::new();
let plan = LiftPlan::for_type(&func.params[0].ty, &r, &mut names, &aliases)
.expect("list<color> must classify");
validate_emit_lift_plan(&plan, &r);
}
#[test]
fn list_result_classifies_as_compound() {
let (r, mut names) = setup();
let func = func_named(&r, "f-result-list-u32");
let result_lift = classify_result_lift(r.resolve(), func, true, &mut names, r.aliases())
.expect("list<u32> result must classify")
.expect("list<u32> result must produce a ResultLift");
let compound = result_lift
.compound()
.expect("list<u32> result must route through Compound");
assert!(
matches!(compound.plan.cells[0], Cell::ListOf { .. }),
"Compound result plan's root cell must be ListOf, got {:?}",
compound.plan.cells[0],
);
}
#[test]
fn map_result_classifies_as_compound() {
let (r, mut names) = setup();
let func = func_named(&r, "f-result-map");
let result_lift = classify_result_lift(r.resolve(), func, true, &mut names, r.aliases())
.expect("map<string,u32> result must classify")
.expect("map<string,u32> result must produce a ResultLift");
let compound = result_lift
.compound()
.expect("map<string,u32> result must route through Compound");
let Cell::ListOf { element_plan, .. } = &compound.plan.cells[0] else {
panic!(
"Compound map-result root must be ListOf, got {:?}",
compound.plan.cells[0]
);
};
assert!(matches!(
element_plan.cells.last(),
Some(Cell::TupleOf { .. })
));
}
#[test]
fn fixed_length_list_result_classifies_as_compound() {
let (r, mut names) = setup();
let func = func_named(&r, "f-result-fixed-list-u32");
let result_lift = classify_result_lift(r.resolve(), func, true, &mut names, r.aliases())
.expect("list<u32, 3> result must classify")
.expect("list<u32, 3> result must produce a ResultLift");
let compound = result_lift
.compound()
.expect("list<u32, 3> result must route through Compound");
let Cell::TupleOf { children } = compound.plan.cells.last().expect("non-empty plan") else {
panic!(
"Compound fixed-list-result root must be TupleOf, got {:?}",
compound.plan.cells.last()
);
};
assert_eq!(children.as_slice(), &[0, 1, 2]);
}
#[test]
fn list_char_result_classifies_as_compound() {
let r = test_resolve();
let mut names = NameInterner::new();
let func = func_named(&r, "f-result-list-char");
let result_lift = classify_result_lift(r.resolve(), func, true, &mut names, r.aliases())
.expect("list<char> result must classify")
.expect("list<char> result must produce a ResultLift");
let compound = result_lift
.compound()
.expect("list<char> result must route through Compound");
let Cell::ListOf { element_plan, .. } = &compound.plan.cells[0] else {
panic!(
"Compound result root must be ListOf, got {:?}",
compound.plan.cells[0]
);
};
assert_eq!(element_plan.cells, vec![Cell::Char { flat_slot: 0 }]);
}
#[test]
fn record_with_list_field_emits_valid_wasm() {
let (r, mut names) = setup();
let plan = plan_for_named("list-pair", &r, &mut names);
validate_emit_lift_plan(&plan, r.resolve());
}