Skip to main content

logicaffeine_compile/vm/wasm/
module.rs

1//! The direct AOT backend: a whole [`CompiledProgram`] → one self-contained `.wasm` module,
2//! with no rustc/cargo/wasm-bindgen.
3//!
4//! The module exports `main` (the synthesized top-level body) and one wasm function per user
5//! function, and imports only the host's `env.print_*` output sinks. Function index space is
6//! `[imports 0..K][main = K][functions[i] = K+1+i]`, so `Op::Call { func }` lowers to a plain
7//! `call (K+1+func)`. The per-function body reuses the shared dispatch-loop lowering
8//! ([`super::cfg`]); types come from [`super::kind`] (the bytecode's arithmetic/`Show` are
9//! runtime-polymorphic, so the AOT path infers them statically).
10//!
11//! Everything outside the supported scalar fragment is rejected with [`WasmLowerError`] — the
12//! backend is total on what it accepts and never miscompiles; the corpus lock turns each
13//! refusal into a tracked, shrinking gap rather than a silent skip.
14
15use super::cfg::{assemble_dispatch_loop, Blocks};
16use super::encode::*;
17use super::kind::{self, FieldLayout, FieldNested, Kind, KindTable, ParamShape};
18use super::regsplit;
19use super::WasmLowerError;
20use crate::semantics::builtins::BuiltinId;
21use crate::vm::instruction::{BoundaryType, CompiledFunction, CompiledProgram, Constant, EnumTypeDef, Op, StructTypeDef};
22use crate::Interner;
23use logicaffeine_language::analysis::{PolicyCondition, PolicyRegistry};
24
25type R<T> = std::result::Result<T, WasmLowerError>;
26
27/// Initial linear-memory size, in 64 KiB pages, for a heap-using module (4 MiB). The bump allocator
28/// never frees, so this is the working-set ceiling; 4 MiB comfortably holds the build-then-scan
29/// programs (n-element arrays, `+`-built strings, per-index Text cuts) at realistic sizes.
30const HEAP_PAGES: u32 = 64;
31
32/// Reserved linear-memory slot (an `i32`) holding the offline net-inbox FIFO handle — lives in the
33/// null-reserved low-16 region (bytes 0..16 are never bump-allocated), so it never collides with the
34/// heap. `Listen` writes the handle here; `Send`/`Stream`/`Await` read it (see the net-op lowerings).
35const NET_INBOX_ADDR: i32 = 8;
36
37/// A host function the emitted module imports (under the `env` namespace) to display a `Show`.
38#[derive(Clone, Copy, PartialEq, Eq)]
39enum HostFn {
40    PrintI64,
41    PrintBool,
42    /// `print_char(code: i64)` — display one Unicode scalar. The host reconstructs `char` from the
43    /// code point and emits its UTF-8 bytes (a `RuntimeValue::Char` displays as the character
44    /// itself), so `Show \`a\`` prints `a`, not the numeric code point.
45    PrintChar,
46    PrintF64,
47    /// `print_date(days: i32)` / `print_moment(nanos: i64)` — display a temporal value (the host
48    /// formats via `RuntimeValue::Date`/`Moment`'s `to_display_string`, so no reimplementation).
49    PrintDate,
50    PrintMoment,
51    /// `print_duration(nanos: i64)` / `print_time(nanos: i64)` — display a `RuntimeValue::Duration`
52    /// (magnitude-bucketed `5s`/`3h`/…) / `Time` (`HH:MM:SS[.frac]`), host-formatted like the VM.
53    PrintDuration,
54    PrintTime,
55    /// `pow_ff(base: f64, exp: f64) -> f64` — `f64::powf`, for a Float exponent.
56    PowFf,
57    /// `pow_fi(base: f64, exp: i64) -> f64` — `f64::powi`, for an Int exponent (exact repeated
58    /// multiplication, NOT `powf` — the two differ in the last bit).
59    PowFi,
60    /// `today() -> i32` (days since epoch) / `now() -> i64` (nanos since epoch) — the clock,
61    /// honoring the test fixed-clock so tw==vm==wasm.
62    Today,
63    Now,
64    /// `print_seq_i64(handle: i32)` / `print_seq_f64(handle: i32)` — display a whole sequence. The
65    /// host reads the stable header `[len][cap][data_ptr]` + the element buffer out of the
66    /// exported linear memory and formats `[e0, e1, …]` exactly as `RuntimeValue::List`'s
67    /// `to_display_string` (each element by its scalar display), so no format reimplementation.
68    PrintSeqI64,
69    /// `print_seq_bool(handle: i32)` — like [`PrintSeqI64`] but each i64-0/1 element renders as
70    /// `true`/`false` (a `Seq of Bool` = `RuntimeValue::List` of `ListRepr::Bools`), so the whole
71    /// sequence displays `[true, false, …]` rather than `[1, 0, …]`.
72    PrintSeqBool,
73    /// `print_seq_word32(handle: i32)` / `print_seq_word64(handle: i32)` — display a whole `Seq of
74    /// Word32`/`Word64` (a crypto state array) as `[u0, u1, …]` with each element its UNSIGNED decimal
75    /// (`4294967295`, not `-1`). Word32 elements ride the low word of their 8-byte slot; Word64 the full
76    /// slot — matching `RuntimeValue::List` of `Word`.
77    PrintSeqWord32,
78    PrintSeqWord64,
79    PrintSeqF64,
80    /// `print_text(handle: i32)` — display a UTF-8 string. The host reads the header `[len][cap]
81    /// [data_ptr]` + the `len` bytes at `data_ptr` and emits them verbatim (a `RuntimeValue::Text`
82    /// displays as its raw contents), so no formatting reimplementation.
83    PrintText,
84    /// `print_seq_text(handle: i32)` — display a whole sequence of Text. The host reads the seq
85    /// header + each element (a Text handle) and formats `[s0, s1, …]` (the elements unquoted,
86    /// comma-space separated), matching `RuntimeValue::List`'s `to_display_string` for Text elements.
87    PrintSeqText,
88    /// `print_set_i64(handle: i32)` — display a whole `Set of Int` as `{e0, e1, …}`. The VM's `Set`
89    /// is an insertion-ordered `Vec` (NOT a hashset), and the AOT set stores elements in that same
90    /// order, so the display is deterministic and byte-identical. (A whole `Map` displays the same way
91    /// via `lower_show_map`: the VM's `MapStorage` is an insertion-ordered `IndexMap`, matching the
92    /// AOT's append order, so `{k: v, …}` is byte-identical too.)
93    PrintSetI64,
94    /// `print_set_text(handle: i32)` — display a whole `Set of Text` as `{s0, s1, …}` (elements
95    /// unquoted, insertion order). Like [`PrintSetI64`] but each slot's low word is a `Text` handle
96    /// the host reads out of memory — matching `RuntimeValue::Set::to_display_string` for Text.
97    PrintSetText,
98    /// `fmt_i64_into(buf: i32, val: i64) -> i32` — write the decimal of `val`
99    /// (`RuntimeValue::Int(val).to_display_string()`) into `buf` and return the byte length. Used
100    /// to stringify an Int operand of a `Concat` (string interpolation `"… {n} …"`). `buf` is a
101    /// module-allocated 24-byte scratch (an i64 decimal is ≤ 20 chars + sign).
102    FmtI64Into,
103    /// `fmt_f64_into(buf: i32, val: f64) -> i32` — like [`FmtI64Into`] for a Float operand (the
104    /// shared shortest-round-trip display, `logicaffeine_data::fmt::fmt_f64`); `buf` is a 340-byte
105    /// scratch (the widest possible output, the smallest subnormal, is ~326 bytes — spec-locked
106    /// by `fmt::tests::worst_case_width_fits_wasm_scratch`).
107    FmtF64Into,
108    /// `fmt_bool_into(buf: i32, val: i32) -> i32` — writes `"true"`/`"false"`; `buf` is 8 bytes.
109    FmtBoolInto,
110    /// `fmt_f64_prec_into(buf: i32, val: f64, prec: i32) -> i32` — write `format!("{:.prec}", val)`
111    /// (the interpolation `.N` precision spec `"{x:.9}"` → `apply_format_spec`) and return the length.
112    /// `buf` is a `340 + prec`-byte scratch (worst-case `f64` integer width + the decimals).
113    FmtF64PrecInto,
114    /// `fmt_align_into(buf: i32, text: i32, width: i32, align: i32) -> i32` — pad the `Text` `text`'s
115    /// display to `width` (space fill, char-counted) into `buf`, returning the byte length. `align`
116    /// selects `format!("{:>w$}", …)` (0, right — also the bare-width `{x:6}`), `{:<w$}` (1, left), or
117    /// `{:^w$}` (2, center) — the SAME Rust `format!` `apply_format_spec` runs, so bit-identical. `buf`
118    /// is sized `text_len + width` (padding adds at most `width` single-byte spaces).
119    FmtAlignInto,
120    /// `args() -> i32` — the command-line arguments as a `Seq of Text` handle (the host builds the
121    /// argv sequence in the module's linear memory and returns its handle), so a program reading its
122    /// problem size from argv (`parseInt(item 2 of args())`) compiles to standalone wasm.
123    Args,
124    /// `parse_int(handle: i32) -> i64` — parse a `Text` (UTF-8 in linear memory) to an Int, exactly as
125    /// the VM's `BuiltinId::ParseInt` (`str::parse::<i64>`), trapping on a non-numeric string.
126    ParseInt,
127    /// `parse_float(handle: i32) -> f64` — parse a `Text` to a Float, exactly as the VM's
128    /// `BuiltinId::ParseFloat` (`str::trim().parse::<f64>`), trapping on a non-numeric string.
129    ParseFloat,
130    /// `parse_timestamp(handle: i32) -> i64` — parse an RFC-3339 `Text` to a `Moment` (nanoseconds
131    /// since the epoch) via `temporal::parse_rfc3339`, trapping on a malformed timestamp.
132    ParseTimestamp,
133    /// `temporal_component(nanos: i64, which: i32) -> i64` — one calendar/clock component of a `Moment`
134    /// (`which`: 0 year, 1 month, 2 day, 3 hour, 4 minute, 5 second, 6 weekday, 7 iso-week, 8 quarter),
135    /// computed by the SAME `temporal::civil_from_unix_nanos`/`weekday_from_days`/`iso_week_from_days`
136    /// the VM uses — so `the year of m` is bit-identical across tiers.
137    TemporalComponent,
138    /// `temporal_component_date(days: i32, which: i32) -> i64` — the calendar component of a `Date`
139    /// (days since the epoch), `which` in {0 year, 1 month, 2 day, 6 weekday, 7 iso-week, 8 quarter}.
140    /// Computed straight from `temporal::civil_from_days`/`weekday_from_days`/`iso_week_from_days` (the
141    /// VM's exact Date path) over the full `i32` day range — no `Moment` nanos round-trip (which would
142    /// overflow `i64` past ~year 2262). Clock components (hour/minute/second) don't apply to a `Date`
143    /// and are refused at lowering, matching the VM's runtime error.
144    TemporalComponentDate,
145    /// `fmt_seq_i64_into(buf: i32, handle: i32) -> i32` — write a whole `Seq of Int`'s display
146    /// `[e0, e1, …]` (`RuntimeValue::List::to_display_string`) into `buf` and return the byte length.
147    /// Used to stringify a SEQUENCE operand of a `+`/`format` (`"Positives: " + positives`). `buf` is
148    /// sized `len*24 + 8` (each i64 ≤ 20 digits + sign + `", "`, plus the brackets).
149    FmtSeqI64Into,
150    /// `fmt_seq_bool_into(buf: i32, handle: i32) -> i32` — like [`FmtSeqI64Into`] for a `Seq of Bool`,
151    /// rendering each i64-0/1 element as `true`/`false` (`buf` sized `len*7 + 8` — `"false"` + `", "`).
152    /// Used to stringify a whole bool sequence operand of a `+`/`format`.
153    FmtSeqBoolInto,
154    /// `fmt_set_i64_into(buf: i32, handle: i32) -> i32` — like [`FmtSeqI64Into`] for a `Set of Int`,
155    /// whose display is `{e0, e1, …}` (insertion order, matching the VM's `Vec`-backed Set).
156    FmtSetI64Into,
157    /// `print_rational(num: i64, den: i64)` — display an exact `Rational` as `num/den`, or just `num`
158    /// when `den == 1` (matching the VM, whose `from_rational` downsizes a whole quotient to an `Int`).
159    PrintRational,
160    /// `print_nothing()` — display the Optional null value as "nothing" (matching the tree-walker's
161    /// `RuntimeValue::Nothing` display). The `Some` arm of an `Optional` `Show` uses the inner scalar's
162    /// own sink instead; this covers the null-handle arm.
163    PrintNothing,
164    /// `print_word(v: i64)` — display a Word as its UNSIGNED decimal (`(v as u64).to_string()`, matching
165    /// `WordVal`'s Display). A `Word32` is zero-extended to `i64` (`i64.extend_i32_u`) before the call,
166    /// so its top 32 bits are zero and the `u64` reading is exactly the `u32` value.
167    PrintWord,
168    /// `logos_rt_bigint_from_i64(x: i64) -> i32` — the linked `logicaffeine_base::BigInt` runtime: box a
169    /// scalar into a BigInt handle. LINKER MODE ONLY: an undefined symbol `rust-lld` resolves against the
170    /// prebuilt base runtime object (there is no `env` host behind it), so it never appears in a
171    /// standalone module. The three `Bigint*` sinks together lower an overflowing integer `Op::Pow`.
172    BigintFromI64,
173    /// `logos_rt_bigint_pow(base: i32, exp: i64) -> i32` — raise a BigInt handle to an integer power,
174    /// returning a fresh handle (the exact big integer, no overflow). Linker mode only.
175    BigintPow,
176    /// `logos_rt_bigint_to_text(h: i32) -> i32` — render a BigInt handle to a `Text` handle laid out in
177    /// the emitter's `[len][cap][data_ptr][refcount]` ABI in the shared linear memory, so a `Show` reads
178    /// it through the ordinary `print_text` path. Linker mode only.
179    BigintToText,
180    /// `logos_rt_bigint_mul(a: i32, b: i32) -> i32` — exact big-integer multiply of two BigInt handles,
181    /// returning a fresh handle. Lets `(2^100) * (3^50)` keep computing on real BigInts. Linker only.
182    BigintMul,
183    /// `logos_rt_bigint_add(a: i32, b: i32) -> i32` — exact big-integer addition of two handles. Linker only.
184    BigintAdd,
185    /// `logos_rt_bigint_sub(a: i32, b: i32) -> i32` — exact big-integer subtraction (may be negative;
186    /// `to_text` renders the sign). Linker only.
187    BigintSub,
188    /// `logos_rt_bigint_div(a: i32, b: i32) -> i32` — exact big-integer quotient (`div_rem().0`, the SAME
189    /// truncating division the VM uses), traps on a zero divisor. Linker only.
190    BigintDiv,
191    /// `logos_rt_bigint_mod(a: i32, b: i32) -> i32` — exact big-integer remainder (`div_rem().1`, matching
192    /// the VM's `the remainder of a and b`), traps on a zero divisor. Linker only.
193    BigintMod,
194    /// `logos_rt_complex_from_i64(re: i64, im: i64) -> i32` — build an EXACT `Complex` (Rational
195    /// components) from two integer parts, returning an i32 handle. `logos_rt_complex_{add,sub,mul}(a,
196    /// b) -> i32` do exact complex arithmetic; `logos_rt_complex_to_text(h) -> i32` renders it (`re±imi`)
197    /// to a Text handle. Linker mode only (the exact Rational-backed runtime, mirroring the BigInt ABI).
198    ComplexFromI64,
199    ComplexAdd,
200    ComplexSub,
201    ComplexMul,
202    ComplexToText,
203    /// `logos_rt_modular_from_i64(v: i64, n: i64) -> i32` / `_add`/`_sub`/`_mul`/`_to_text` — the ℤ/nℤ
204    /// analog of the Complex ABI over `logicaffeine_base::Modular` (i32 handle). Linker mode only.
205    ModularFromI64,
206    ModularAdd,
207    ModularSub,
208    ModularMul,
209    ModularToText,
210    /// `logos_rt_decimal_from_text(h)` parses a Text handle → exact Decimal; `_from_i64(x)` promotes an
211    /// Int; `_add`/`_sub`/`_mul`/`_to_text` are the exact base-10 ABI over `base::Decimal`. Linker only.
212    DecimalFromText,
213    DecimalFromI64,
214    DecimalAdd,
215    DecimalSub,
216    DecimalMul,
217    DecimalToText,
218    /// `logos_rt_money_from_decimal(dec, cur)` / `_from_i64(v, cur)` build a Money (currency Text read
219    /// from shared memory); `_add`/`_sub` require matching currency; `_to_text` renders it. Linker only.
220    MoneyFromDecimal,
221    MoneyFromI64,
222    MoneyAdd,
223    MoneySub,
224    MoneyToText,
225    QuantityOfI64,
226    QuantityConvert,
227    QuantityAdd,
228    QuantitySub,
229    QuantityMul,
230    QuantityDiv,
231    QuantityToText,
232    RationalFromI64,
233    RationalFromBigint,
234    RationalAdd,
235    RationalSub,
236    RationalMul,
237    RationalDiv,
238    RationalToText,
239    RationalFloor,
240    RationalCeil,
241    RationalRound,
242    RationalAbs,
243    UuidParse,
244    UuidNil,
245    UuidMax,
246    UuidDns,
247    UuidUrl,
248    UuidOid,
249    UuidX500,
250    UuidVersion,
251    UuidEq,
252    UuidToText,
253    UuidFromPtr,
254    PrintSpan,
255    MomentAddSpan,
256    DateAddSpan,
257    FormatTimestampRt,
258    MonthsBetweenRt,
259    YearsBetweenRt,
260    InZoneRt,
261    LocalInstantRt,
262    Lanes16FromBytes,
263    Lanes8FromWords,
264    Lanes4W64FromWords,
265    LanesSplat16,
266    LanesSplat8,
267    LanesToSeq,
268    LanesShuffle,
269    LanesInterleaveLo,
270    LanesInterleaveHi,
271    LanesByteAdd,
272    LanesMaddubs,
273    LanesPackus,
274    LanesShrBytes,
275    DecimalToRational,
276    MoneySetRate,
277    MoneyToCurrency,
278    MoneySetRatesInt,
279    MoneySetRatesRational,
280    MoneySetRatesDecimal,
281    WriteWireResidual,
282    WireBytesInt,
283    WireBytesBool,
284    WireBytesFloat,
285    WireBytesText,
286    ReadWireFrame,
287    ReadWireProgramRt,
288    DynamicToText,
289    RunAccepted,
290    Sha1Rnds4,
291    Sha1Msg1,
292    Sha1Msg2,
293    Sha1Nexte,
294    Lanes4Add,
295    Lanes4Xor,
296    /// `logos_rt_alloc(size: i32) -> i32` — a raw 8-aligned block from the runtime's allocator. Linker
297    /// mode seeds the emitter's bump allocator (`__heap_ptr`) from ONE such SLAB at a `main` prologue, so
298    /// the emitter's heap and the runtime's `dlmalloc` never overlap in the shared linear memory.
299    RtAlloc,
300}
301
302/// Stable order — also the order host functions take their wasm import indices.
303/// A `set_rates(map)`'s VALUE kind — the kind of a `SetIndex` value written into the map, resolved by
304/// following `Move` aliases of `target` (the call arg is a Move-copy of the built map register) to any
305/// register a `SetIndex` populated. `None` if no populating `SetIndex` is statically visible.
306fn set_rates_value_kind(ops: &[Op], target: u16, kinds: &KindTable) -> Option<Kind> {
307    let mut aliases = std::collections::HashSet::new();
308    aliases.insert(target);
309    let mut changed = true;
310    while changed {
311        changed = false;
312        for op in ops {
313            if let Op::Move { dst, src } = op {
314                if aliases.contains(dst) && aliases.insert(*src) {
315                    changed = true;
316                }
317                if aliases.contains(src) && aliases.insert(*dst) {
318                    changed = true;
319                }
320            }
321        }
322    }
323    ops.iter().find_map(|op| match op {
324        Op::SetIndex { collection, value, .. } if aliases.contains(collection) => kinds.get(*value as usize),
325        _ => None,
326    })
327}
328
329/// The `logos_rt_wire_bytes_*` runtime host for `wireBytes(value)` (linker mode) by the ARGUMENT'S kind
330/// — each reconstructs the corresponding `RuntimeValue` and marshals it via the REAL codec. `None` for a
331/// kind not yet reconstructed (a composite: soundly refused). Shared by the import scan and the lowering.
332fn wire_bytes_host_fn(arg_kind: Option<Kind>) -> Option<HostFn> {
333    match arg_kind {
334        Some(Kind::Int) => Some(HostFn::WireBytesInt),
335        Some(Kind::Bool) => Some(HostFn::WireBytesBool),
336        Some(Kind::Float) => Some(HostFn::WireBytesFloat),
337        Some(Kind::Text) => Some(HostFn::WireBytesText),
338        _ => None,
339    }
340}
341
342/// The `logos_rt_lanes_*` runtime host for a general-`LanesVal` SIMD builtin (linker mode), or `None`
343/// if `b` is not one of them. Shared by the import scan and the lowering so both agree on the op→host
344/// map. Distinct from the SHA-1 `Kind::Lanes` (inline `Lanes4Word32`) ops, which are their own hosts.
345fn lanes_v_host_fn(b: BuiltinId) -> Option<HostFn> {
346    Some(match b {
347        BuiltinId::Lanes16Word8Make => HostFn::Lanes16FromBytes,
348        BuiltinId::Lanes8Word32 => HostFn::Lanes8FromWords,
349        BuiltinId::Lanes4Word64 => HostFn::Lanes4W64FromWords,
350        BuiltinId::Splat16Word8 => HostFn::LanesSplat16,
351        BuiltinId::Splat8Word32 => HostFn::LanesSplat8,
352        BuiltinId::SeqOfLanes16W8 | BuiltinId::SeqOfLanes8 => HostFn::LanesToSeq,
353        BuiltinId::Shuffle16 => HostFn::LanesShuffle,
354        BuiltinId::InterleaveLo16 => HostFn::LanesInterleaveLo,
355        BuiltinId::InterleaveHi16 => HostFn::LanesInterleaveHi,
356        BuiltinId::ByteAdd16 => HostFn::LanesByteAdd,
357        BuiltinId::Maddubs16 => HostFn::LanesMaddubs,
358        BuiltinId::Packus16 => HostFn::LanesPackus,
359        BuiltinId::ShrBytes16 => HostFn::LanesShrBytes,
360        _ => return None,
361    })
362}
363
364const HOST_FNS: [HostFn; 139] = [
365    HostFn::PrintI64,
366    HostFn::PrintBool,
367    HostFn::PrintChar,
368    HostFn::PrintF64,
369    HostFn::PrintDate,
370    HostFn::PrintMoment,
371    HostFn::PrintDuration,
372    HostFn::PrintTime,
373    HostFn::PowFf,
374    HostFn::PowFi,
375    HostFn::Today,
376    HostFn::Now,
377    HostFn::PrintSeqI64,
378    HostFn::PrintSeqBool,
379    HostFn::PrintSeqWord32,
380    HostFn::PrintSeqWord64,
381    HostFn::PrintSeqF64,
382    HostFn::PrintText,
383    HostFn::PrintSeqText,
384    HostFn::PrintSetI64,
385    HostFn::FmtI64Into,
386    HostFn::FmtF64Into,
387    HostFn::FmtBoolInto,
388    HostFn::FmtF64PrecInto,
389    HostFn::FmtAlignInto,
390    HostFn::Args,
391    HostFn::ParseInt,
392    HostFn::FmtSeqI64Into,
393    HostFn::FmtSeqBoolInto,
394    HostFn::FmtSetI64Into,
395    HostFn::PrintSetText,
396    HostFn::PrintRational,
397    HostFn::PrintNothing,
398    HostFn::ParseFloat,
399    HostFn::ParseTimestamp,
400    HostFn::TemporalComponent,
401    HostFn::TemporalComponentDate,
402    HostFn::PrintWord,
403    HostFn::BigintFromI64,
404    HostFn::BigintPow,
405    HostFn::BigintToText,
406    HostFn::BigintMul,
407    HostFn::BigintAdd,
408    HostFn::BigintSub,
409    HostFn::BigintDiv,
410    HostFn::BigintMod,
411    HostFn::ComplexFromI64,
412    HostFn::ComplexAdd,
413    HostFn::ComplexSub,
414    HostFn::ComplexMul,
415    HostFn::ComplexToText,
416    HostFn::ModularFromI64,
417    HostFn::ModularAdd,
418    HostFn::ModularSub,
419    HostFn::ModularMul,
420    HostFn::ModularToText,
421    HostFn::DecimalFromText,
422    HostFn::DecimalFromI64,
423    HostFn::DecimalAdd,
424    HostFn::DecimalSub,
425    HostFn::DecimalMul,
426    HostFn::DecimalToText,
427    HostFn::MoneyFromDecimal,
428    HostFn::MoneyFromI64,
429    HostFn::MoneyAdd,
430    HostFn::MoneySub,
431    HostFn::MoneyToText,
432    HostFn::QuantityOfI64,
433    HostFn::QuantityConvert,
434    HostFn::QuantityAdd,
435    HostFn::QuantitySub,
436    HostFn::QuantityMul,
437    HostFn::QuantityDiv,
438    HostFn::QuantityToText,
439    HostFn::RationalFromI64,
440    HostFn::RationalFromBigint,
441    HostFn::RationalAdd,
442    HostFn::RationalSub,
443    HostFn::RationalMul,
444    HostFn::RationalDiv,
445    HostFn::RationalToText,
446    HostFn::RationalFloor,
447    HostFn::RationalCeil,
448    HostFn::RationalRound,
449    HostFn::RationalAbs,
450    HostFn::UuidParse,
451    HostFn::UuidNil,
452    HostFn::UuidMax,
453    HostFn::UuidDns,
454    HostFn::UuidUrl,
455    HostFn::UuidOid,
456    HostFn::UuidX500,
457    HostFn::UuidVersion,
458    HostFn::UuidEq,
459    HostFn::UuidToText,
460    HostFn::UuidFromPtr,
461    HostFn::PrintSpan,
462    HostFn::MomentAddSpan,
463    HostFn::DateAddSpan,
464    HostFn::FormatTimestampRt,
465    HostFn::MonthsBetweenRt,
466    HostFn::YearsBetweenRt,
467    HostFn::InZoneRt,
468    HostFn::LocalInstantRt,
469    HostFn::Lanes16FromBytes,
470    HostFn::Lanes8FromWords,
471    HostFn::Lanes4W64FromWords,
472    HostFn::LanesSplat16,
473    HostFn::LanesSplat8,
474    HostFn::LanesToSeq,
475    HostFn::LanesShuffle,
476    HostFn::LanesInterleaveLo,
477    HostFn::LanesInterleaveHi,
478    HostFn::LanesByteAdd,
479    HostFn::LanesMaddubs,
480    HostFn::LanesPackus,
481    HostFn::LanesShrBytes,
482    HostFn::DecimalToRational,
483    HostFn::MoneySetRate,
484    HostFn::MoneyToCurrency,
485    HostFn::MoneySetRatesInt,
486    HostFn::MoneySetRatesRational,
487    HostFn::MoneySetRatesDecimal,
488    HostFn::WriteWireResidual,
489    HostFn::WireBytesInt,
490    HostFn::WireBytesBool,
491    HostFn::WireBytesFloat,
492    HostFn::WireBytesText,
493    HostFn::ReadWireFrame,
494    HostFn::ReadWireProgramRt,
495    HostFn::DynamicToText,
496    HostFn::RunAccepted,
497    HostFn::Sha1Rnds4,
498    HostFn::Sha1Msg1,
499    HostFn::Sha1Msg2,
500    HostFn::Sha1Nexte,
501    HostFn::Lanes4Add,
502    HostFn::Lanes4Xor,
503    HostFn::RtAlloc,
504];
505
506impl HostFn {
507    fn field(self) -> &'static str {
508        match self {
509            HostFn::PrintI64 => "print_i64",
510            HostFn::PrintBool => "print_bool",
511            HostFn::PrintChar => "print_char",
512            HostFn::PrintF64 => "print_f64",
513            HostFn::PrintDate => "print_date",
514            HostFn::PrintMoment => "print_moment",
515            HostFn::PrintDuration => "print_duration",
516            HostFn::PrintTime => "print_time",
517            HostFn::PowFf => "pow_ff",
518            HostFn::PowFi => "pow_fi",
519            HostFn::Today => "today",
520            HostFn::Now => "now",
521            HostFn::PrintSeqI64 => "print_seq_i64",
522            HostFn::PrintSeqBool => "print_seq_bool",
523            HostFn::PrintSeqWord32 => "print_seq_word32",
524            HostFn::PrintSeqWord64 => "print_seq_word64",
525            HostFn::PrintSeqF64 => "print_seq_f64",
526            HostFn::PrintText => "print_text",
527            HostFn::PrintSeqText => "print_seq_text",
528            HostFn::PrintSetI64 => "print_set_i64",
529            HostFn::PrintSetText => "print_set_text",
530            HostFn::FmtI64Into => "fmt_i64_into",
531            HostFn::FmtF64Into => "fmt_f64_into",
532            HostFn::FmtBoolInto => "fmt_bool_into",
533            HostFn::FmtF64PrecInto => "fmt_f64_prec_into",
534            HostFn::FmtAlignInto => "fmt_align_into",
535            HostFn::Args => "args",
536            HostFn::ParseInt => "parse_int",
537            HostFn::ParseFloat => "parse_float",
538            HostFn::ParseTimestamp => "parse_timestamp",
539            HostFn::TemporalComponent => "temporal_component",
540            HostFn::TemporalComponentDate => "temporal_component_date",
541            HostFn::PrintWord => "print_word",
542            HostFn::FmtSeqI64Into => "fmt_seq_i64_into",
543            HostFn::FmtSeqBoolInto => "fmt_seq_bool_into",
544            HostFn::FmtSetI64Into => "fmt_set_i64_into",
545            HostFn::PrintRational => "print_rational",
546            HostFn::PrintNothing => "print_nothing",
547            HostFn::BigintFromI64 => "logos_rt_bigint_from_i64",
548            HostFn::BigintPow => "logos_rt_bigint_pow",
549            HostFn::BigintToText => "logos_rt_bigint_to_text",
550            HostFn::BigintMul => "logos_rt_bigint_mul",
551            HostFn::BigintAdd => "logos_rt_bigint_add",
552            HostFn::BigintSub => "logos_rt_bigint_sub",
553            HostFn::BigintDiv => "logos_rt_bigint_div",
554            HostFn::BigintMod => "logos_rt_bigint_mod",
555            HostFn::ComplexFromI64 => "logos_rt_complex_from_i64",
556            HostFn::ComplexAdd => "logos_rt_complex_add",
557            HostFn::ComplexSub => "logos_rt_complex_sub",
558            HostFn::ComplexMul => "logos_rt_complex_mul",
559            HostFn::ComplexToText => "logos_rt_complex_to_text",
560            HostFn::ModularFromI64 => "logos_rt_modular_from_i64",
561            HostFn::ModularAdd => "logos_rt_modular_add",
562            HostFn::ModularSub => "logos_rt_modular_sub",
563            HostFn::ModularMul => "logos_rt_modular_mul",
564            HostFn::ModularToText => "logos_rt_modular_to_text",
565            HostFn::DecimalFromText => "logos_rt_decimal_from_text",
566            HostFn::DecimalFromI64 => "logos_rt_decimal_from_i64",
567            HostFn::DecimalAdd => "logos_rt_decimal_add",
568            HostFn::DecimalSub => "logos_rt_decimal_sub",
569            HostFn::DecimalMul => "logos_rt_decimal_mul",
570            HostFn::DecimalToText => "logos_rt_decimal_to_text",
571            HostFn::MoneyFromDecimal => "logos_rt_money_from_decimal",
572            HostFn::MoneyFromI64 => "logos_rt_money_from_i64",
573            HostFn::MoneyAdd => "logos_rt_money_add",
574            HostFn::MoneySub => "logos_rt_money_sub",
575            HostFn::MoneyToText => "logos_rt_money_to_text",
576            HostFn::QuantityOfI64 => "logos_rt_quantity_of_i64",
577            HostFn::QuantityConvert => "logos_rt_quantity_convert",
578            HostFn::QuantityAdd => "logos_rt_quantity_add",
579            HostFn::QuantitySub => "logos_rt_quantity_sub",
580            HostFn::QuantityMul => "logos_rt_quantity_mul",
581            HostFn::QuantityDiv => "logos_rt_quantity_div",
582            HostFn::QuantityToText => "logos_rt_quantity_to_text",
583            HostFn::RationalFromI64 => "logos_rt_rational_from_i64",
584            HostFn::RationalFromBigint => "logos_rt_rational_from_bigint",
585            HostFn::RationalAdd => "logos_rt_rational_add",
586            HostFn::RationalSub => "logos_rt_rational_sub",
587            HostFn::RationalMul => "logos_rt_rational_mul",
588            HostFn::RationalDiv => "logos_rt_rational_div",
589            HostFn::RationalToText => "logos_rt_rational_to_text",
590            HostFn::RationalFloor => "logos_rt_rational_floor",
591            HostFn::RationalCeil => "logos_rt_rational_ceil",
592            HostFn::RationalRound => "logos_rt_rational_round",
593            HostFn::RationalAbs => "logos_rt_rational_abs",
594            HostFn::UuidParse => "logos_rt_uuid_parse",
595            HostFn::UuidNil => "logos_rt_uuid_nil",
596            HostFn::UuidMax => "logos_rt_uuid_max",
597            HostFn::UuidDns => "logos_rt_uuid_dns",
598            HostFn::UuidUrl => "logos_rt_uuid_url",
599            HostFn::UuidOid => "logos_rt_uuid_oid",
600            HostFn::UuidX500 => "logos_rt_uuid_x500",
601            HostFn::UuidVersion => "logos_rt_uuid_version",
602            HostFn::UuidEq => "logos_rt_uuid_eq",
603            HostFn::UuidToText => "logos_rt_uuid_to_text",
604            HostFn::UuidFromPtr => "logos_rt_uuid_from_ptr",
605            HostFn::PrintSpan => "print_span",
606            HostFn::MomentAddSpan => "logos_rt_moment_add_span",
607            HostFn::DateAddSpan => "logos_rt_date_add_span",
608            HostFn::FormatTimestampRt => "logos_rt_format_timestamp",
609            HostFn::MonthsBetweenRt => "logos_rt_months_between",
610            HostFn::YearsBetweenRt => "logos_rt_years_between",
611            HostFn::InZoneRt => "logos_rt_in_zone",
612            HostFn::LocalInstantRt => "logos_rt_local_instant",
613            HostFn::Lanes16FromBytes => "logos_rt_lanes16_from_bytes",
614            HostFn::Lanes8FromWords => "logos_rt_lanes8_from_words",
615            HostFn::Lanes4W64FromWords => "logos_rt_lanes4w64_from_words",
616            HostFn::LanesSplat16 => "logos_rt_lanes_splat16",
617            HostFn::LanesSplat8 => "logos_rt_lanes_splat8",
618            HostFn::LanesToSeq => "logos_rt_lanes_to_seq",
619            HostFn::LanesShuffle => "logos_rt_lanes_shuffle",
620            HostFn::LanesInterleaveLo => "logos_rt_lanes_interleave_lo",
621            HostFn::LanesInterleaveHi => "logos_rt_lanes_interleave_hi",
622            HostFn::LanesByteAdd => "logos_rt_lanes_byte_add",
623            HostFn::LanesMaddubs => "logos_rt_lanes_maddubs",
624            HostFn::LanesPackus => "logos_rt_lanes_packus",
625            HostFn::LanesShrBytes => "logos_rt_lanes_shr_bytes",
626            HostFn::DecimalToRational => "logos_rt_decimal_to_rational",
627            HostFn::MoneySetRate => "logos_rt_set_rate",
628            HostFn::MoneyToCurrency => "logos_rt_to_currency",
629            HostFn::MoneySetRatesInt => "logos_rt_set_rates_int",
630            HostFn::MoneySetRatesRational => "logos_rt_set_rates_rational",
631            HostFn::MoneySetRatesDecimal => "logos_rt_set_rates_decimal",
632            HostFn::WriteWireResidual => "write_wire_residual",
633            HostFn::WireBytesInt => "logos_rt_wire_bytes_int",
634            HostFn::WireBytesBool => "logos_rt_wire_bytes_bool",
635            HostFn::WireBytesFloat => "logos_rt_wire_bytes_float",
636            HostFn::WireBytesText => "logos_rt_wire_bytes_text",
637            HostFn::ReadWireFrame => "read_wire_frame",
638            HostFn::ReadWireProgramRt => "logos_rt_read_wire_program",
639            HostFn::DynamicToText => "logos_rt_dynamic_to_text",
640            HostFn::RunAccepted => "logos_rt_run_accepted",
641            HostFn::Sha1Rnds4 => "logos_rt_sha1rnds4",
642            HostFn::Sha1Msg1 => "logos_rt_sha1msg1",
643            HostFn::Sha1Msg2 => "logos_rt_sha1msg2",
644            HostFn::Sha1Nexte => "logos_rt_sha1nexte",
645            HostFn::Lanes4Add => "logos_rt_lanes4_add",
646            HostFn::Lanes4Xor => "logos_rt_lanes4_xor",
647            HostFn::RtAlloc => "logos_rt_alloc",
648        }
649    }
650
651    /// The wasm parameter value-types this host function takes.
652    fn params(self) -> Vec<u8> {
653        match self {
654            HostFn::PrintI64 | HostFn::PrintChar => vec![I64],
655            HostFn::PrintBool | HostFn::PrintDate => vec![I32],
656            HostFn::PrintF64 => vec![F64],
657            HostFn::PrintMoment | HostFn::PrintDuration | HostFn::PrintTime | HostFn::PrintSpan => vec![I64],
658            HostFn::MomentAddSpan => vec![I64, I32, I32],
659            HostFn::DateAddSpan => vec![I32, I32, I32],
660            HostFn::FormatTimestampRt => vec![I64],
661            HostFn::MonthsBetweenRt | HostFn::YearsBetweenRt => vec![I64, I64],
662            HostFn::InZoneRt | HostFn::LocalInstantRt => vec![I64, I32],
663            HostFn::Lanes16FromBytes | HostFn::Lanes8FromWords | HostFn::Lanes4W64FromWords | HostFn::LanesToSeq => vec![I32],
664            HostFn::LanesSplat16 | HostFn::LanesSplat8 => vec![I64],
665            HostFn::LanesShuffle | HostFn::LanesInterleaveLo | HostFn::LanesInterleaveHi | HostFn::LanesByteAdd | HostFn::LanesMaddubs | HostFn::LanesPackus => vec![I32, I32],
666            HostFn::LanesShrBytes => vec![I32, I64],
667            HostFn::DecimalToRational | HostFn::MoneySetRatesInt | HostFn::MoneySetRatesRational | HostFn::MoneySetRatesDecimal => vec![I32],
668            HostFn::MoneySetRate | HostFn::MoneyToCurrency => vec![I32, I32],
669            HostFn::WriteWireResidual => vec![I32, I32],
670            HostFn::WireBytesInt | HostFn::WireBytesBool => vec![I64],
671            HostFn::WireBytesFloat => vec![F64],
672            HostFn::WireBytesText => vec![I32],
673            HostFn::ReadWireFrame | HostFn::ReadWireProgramRt => vec![I32, I32],
674            HostFn::DynamicToText => vec![I32],
675            HostFn::RunAccepted => vec![I32, I64, I64, I64],
676            HostFn::Sha1Rnds4 => vec![I32, I32, I64],
677            HostFn::Sha1Msg1 | HostFn::Sha1Msg2 | HostFn::Sha1Nexte | HostFn::Lanes4Add | HostFn::Lanes4Xor => vec![I32, I32],
678            HostFn::PowFf => vec![F64, F64],
679            HostFn::PowFi => vec![F64, I64],
680            HostFn::Today | HostFn::Now => vec![],
681            HostFn::PrintSeqI64 | HostFn::PrintSeqBool | HostFn::PrintSeqWord32 | HostFn::PrintSeqWord64 | HostFn::PrintSeqF64 | HostFn::PrintText | HostFn::PrintSeqText | HostFn::PrintSetI64 | HostFn::PrintSetText => vec![I32],
682            HostFn::FmtI64Into => vec![I32, I64],
683            HostFn::FmtF64Into => vec![I32, F64],
684            HostFn::FmtBoolInto => vec![I32, I64], // a Bool rides an i64 local (0/1)
685            HostFn::FmtF64PrecInto => vec![I32, F64, I32],
686            HostFn::FmtAlignInto => vec![I32, I32, I32, I32],
687            HostFn::FmtSeqI64Into | HostFn::FmtSeqBoolInto | HostFn::FmtSetI64Into => vec![I32, I32],
688            HostFn::Args => vec![],
689            HostFn::ParseInt | HostFn::ParseFloat | HostFn::ParseTimestamp => vec![I32],
690            HostFn::PrintRational => vec![I64, I64],
691            HostFn::PrintNothing => vec![],
692            HostFn::TemporalComponent => vec![I64, I32],
693            HostFn::TemporalComponentDate => vec![I32, I32],
694            HostFn::PrintWord => vec![I64],
695            HostFn::BigintFromI64 => vec![I64],
696            HostFn::BigintPow => vec![I32, I64],
697            HostFn::BigintToText => vec![I32],
698            HostFn::BigintMul | HostFn::BigintAdd | HostFn::BigintSub | HostFn::BigintDiv | HostFn::BigintMod => vec![I32, I32],
699            HostFn::ComplexFromI64 => vec![I64, I64],
700            HostFn::ComplexAdd | HostFn::ComplexSub | HostFn::ComplexMul => vec![I32, I32],
701            HostFn::ComplexToText => vec![I32],
702            HostFn::ModularFromI64 => vec![I64, I64],
703            HostFn::ModularAdd | HostFn::ModularSub | HostFn::ModularMul => vec![I32, I32],
704            HostFn::ModularToText => vec![I32],
705            HostFn::DecimalFromI64 => vec![I64],
706            HostFn::DecimalFromText | HostFn::DecimalToText => vec![I32],
707            HostFn::DecimalAdd | HostFn::DecimalSub | HostFn::DecimalMul => vec![I32, I32],
708            HostFn::MoneyFromI64 => vec![I64, I32],
709            HostFn::MoneyFromDecimal | HostFn::MoneyAdd | HostFn::MoneySub => vec![I32, I32],
710            HostFn::MoneyToText => vec![I32],
711            HostFn::QuantityOfI64 => vec![I64, I32],
712            HostFn::QuantityConvert
713            | HostFn::QuantityAdd
714            | HostFn::QuantitySub
715            | HostFn::QuantityMul
716            | HostFn::QuantityDiv => vec![I32, I32],
717            HostFn::QuantityToText => vec![I32],
718            HostFn::RationalFromI64 => vec![I64],
719            HostFn::RationalFromBigint
720            | HostFn::RationalToText
721            | HostFn::RationalFloor
722            | HostFn::RationalCeil
723            | HostFn::RationalRound
724            | HostFn::RationalAbs => vec![I32],
725            HostFn::RationalAdd | HostFn::RationalSub | HostFn::RationalMul | HostFn::RationalDiv => vec![I32, I32],
726            HostFn::UuidNil | HostFn::UuidMax | HostFn::UuidDns | HostFn::UuidUrl | HostFn::UuidOid | HostFn::UuidX500 => vec![],
727            HostFn::UuidParse | HostFn::UuidVersion | HostFn::UuidToText | HostFn::UuidFromPtr => vec![I32],
728            HostFn::UuidEq => vec![I32, I32],
729            HostFn::RtAlloc => vec![I32],
730        }
731    }
732
733    /// The wasm result value-types this host function returns.
734    fn results(self) -> Vec<u8> {
735        match self {
736            HostFn::PrintI64
737            | HostFn::PrintBool
738            | HostFn::PrintChar
739            | HostFn::PrintF64
740            | HostFn::PrintDate
741            | HostFn::PrintMoment
742            | HostFn::PrintDuration
743            | HostFn::PrintTime
744            | HostFn::PrintSpan
745            | HostFn::PrintSeqI64
746            | HostFn::PrintSeqBool
747            | HostFn::PrintSeqWord32
748            | HostFn::PrintSeqWord64
749            | HostFn::PrintSeqF64
750            | HostFn::PrintText
751            | HostFn::PrintSeqText
752            | HostFn::PrintSetI64
753            | HostFn::PrintSetText
754            | HostFn::PrintRational
755            | HostFn::PrintNothing
756            | HostFn::PrintWord => vec![],
757            HostFn::PowFf | HostFn::PowFi | HostFn::ParseFloat => vec![F64],
758            HostFn::Today | HostFn::FmtI64Into | HostFn::FmtF64Into | HostFn::FmtBoolInto | HostFn::FmtF64PrecInto | HostFn::FmtAlignInto | HostFn::FmtSeqI64Into | HostFn::FmtSeqBoolInto | HostFn::FmtSetI64Into | HostFn::Args
759            | HostFn::BigintFromI64 | HostFn::BigintPow | HostFn::BigintToText | HostFn::BigintMul | HostFn::BigintAdd | HostFn::BigintSub | HostFn::BigintDiv | HostFn::BigintMod | HostFn::ComplexFromI64 | HostFn::ComplexAdd | HostFn::ComplexSub | HostFn::ComplexMul | HostFn::ComplexToText | HostFn::ModularFromI64 | HostFn::ModularAdd | HostFn::ModularSub | HostFn::ModularMul | HostFn::ModularToText | HostFn::DecimalFromText | HostFn::DecimalFromI64 | HostFn::DecimalAdd | HostFn::DecimalSub | HostFn::DecimalMul | HostFn::DecimalToText | HostFn::MoneyFromDecimal | HostFn::MoneyFromI64 | HostFn::MoneyAdd | HostFn::MoneySub | HostFn::MoneyToText | HostFn::QuantityOfI64 | HostFn::QuantityConvert | HostFn::QuantityAdd | HostFn::QuantitySub | HostFn::QuantityMul | HostFn::QuantityDiv | HostFn::QuantityToText | HostFn::RationalFromI64 | HostFn::RationalFromBigint | HostFn::RationalAdd | HostFn::RationalSub | HostFn::RationalMul | HostFn::RationalDiv | HostFn::RationalToText | HostFn::RationalFloor | HostFn::RationalCeil | HostFn::RationalRound | HostFn::RationalAbs | HostFn::UuidParse | HostFn::UuidNil | HostFn::UuidMax | HostFn::UuidDns | HostFn::UuidUrl | HostFn::UuidOid | HostFn::UuidX500 | HostFn::UuidEq | HostFn::UuidToText | HostFn::UuidFromPtr | HostFn::DateAddSpan | HostFn::Sha1Rnds4 | HostFn::Sha1Msg1 | HostFn::Sha1Msg2 | HostFn::Sha1Nexte | HostFn::Lanes4Add | HostFn::Lanes4Xor | HostFn::RtAlloc | HostFn::FormatTimestampRt | HostFn::InZoneRt | HostFn::Lanes16FromBytes | HostFn::Lanes8FromWords | HostFn::Lanes4W64FromWords | HostFn::LanesSplat16 | HostFn::LanesSplat8 | HostFn::LanesToSeq | HostFn::LanesShuffle | HostFn::LanesInterleaveLo | HostFn::LanesInterleaveHi | HostFn::LanesByteAdd | HostFn::LanesMaddubs | HostFn::LanesPackus | HostFn::LanesShrBytes | HostFn::DecimalToRational | HostFn::MoneySetRate | HostFn::MoneyToCurrency | HostFn::MoneySetRatesInt | HostFn::MoneySetRatesRational | HostFn::MoneySetRatesDecimal | HostFn::WireBytesInt | HostFn::WireBytesBool | HostFn::WireBytesFloat | HostFn::WireBytesText | HostFn::ReadWireFrame | HostFn::ReadWireProgramRt | HostFn::DynamicToText => vec![I32],
760            HostFn::Now | HostFn::ParseInt | HostFn::ParseTimestamp | HostFn::TemporalComponent | HostFn::TemporalComponentDate | HostFn::UuidVersion | HostFn::MomentAddSpan | HostFn::MonthsBetweenRt | HostFn::YearsBetweenRt | HostFn::LocalInstantRt | HostFn::WriteWireResidual | HostFn::RunAccepted => vec![I64],
761        }
762    }
763
764    /// The sink that displays a value of kind `k` — matching the tree-walker's
765    /// `to_display_string` for that scalar kind. `None` for a kind without a scalar sink yet
766    /// (e.g. a sequence, whose formatted display lands with the heap value model).
767    fn for_show(k: Kind) -> Option<HostFn> {
768        Some(match k {
769            Kind::Int => HostFn::PrintI64,
770            Kind::Bool => HostFn::PrintBool,
771            Kind::Char => HostFn::PrintChar,
772            Kind::Float => HostFn::PrintF64,
773            Kind::Date => HostFn::PrintDate,
774            Kind::Moment => HostFn::PrintMoment,
775            Kind::Duration => HostFn::PrintDuration,
776            Kind::Time => HostFn::PrintTime,
777            Kind::Span => HostFn::PrintSpan,
778            Kind::SeqInt => HostFn::PrintSeqI64,
779            Kind::SeqBool => HostFn::PrintSeqBool,
780            Kind::SeqWord32 => HostFn::PrintSeqWord32,
781            Kind::SeqWord64 => HostFn::PrintSeqWord64,
782            Kind::SeqFloat => HostFn::PrintSeqF64,
783            Kind::SeqText => HostFn::PrintSeqText,
784            // A never-refined empty sequence formats as `[]`; the i64 sink reads zero elements.
785            Kind::SeqAny => HostFn::PrintSeqI64,
786            Kind::Text => HostFn::PrintText,
787            // A whole struct's `TypeName { field: val, … }` display is assembled by `lower_show_struct`
788            // (fields in DETERMINISTIC alphabetical order, matching the VM's now-sorted `HashMap`
789            // display) — handled by the `Op::Show` dispatch, not this per-kind scalar-sink table.
790            Kind::Struct => return None,
791            // A whole `Set of Int` is insertion-ordered in both the VM (a `Vec`) and the AOT backend,
792            // so it displays deterministically: `{e0, e1, …}`.
793            Kind::Set => HostFn::PrintSetI64,
794            Kind::SetText | Kind::CrdtSetText => HostFn::PrintSetText,
795            // A whole Map's `{k: v, …}` display is assembled by `lower_show_map` (a runtime entry loop
796            // in insertion order, matching the VM's `IndexMap`), not a single scalar sink — so the
797            // `Op::Show` dispatch handles it directly, not this per-kind host table.
798            Kind::Map => return None,
799            // Showing an enum value (`Ctor` / `Ctor(args)`) lands with the argument payload model.
800            Kind::Enum => return None,
801            // A closure has no display form.
802            Kind::Closure => return None,
803            // A `BigInt` `Show` is a TWO-step lowering (`logos_rt_bigint_to_text` → `print_text`), not a
804            // single sink, so it is handled directly by `lower_show` / the import scan — not here.
805            Kind::BigInt => return None,
806            // A `Complex` `Show` likewise renders via `logos_rt_complex_to_text` → `print_text` (dispatch).
807            Kind::Complex => return None,
808            // A `Modular` `Show` renders via `logos_rt_modular_to_text` → `print_text` (dispatch).
809            Kind::Modular => return None,
810            // A `Decimal` `Show` renders via `logos_rt_decimal_to_text` → `print_text` (dispatch).
811            Kind::Decimal => return None,
812            // A `Money` `Show` renders via `logos_rt_money_to_text` → `print_text` (dispatch).
813            Kind::Money => return None,
814            Kind::Quantity => return None,
815            Kind::Uuid => return None,
816            Kind::Lanes => return None,
817            // A SIMD lane vector is not `Show`n directly (it is unpacked back to a `Seq` first).
818            Kind::LanesV => return None,
819            // A dynamic (wire-decoded) value `Show`s via a two-step `logos_rt_dynamic_to_text` → `print_text`
820            // dispatch (like the numeric `to_text` handles), NOT a single scalar sink.
821            Kind::Dynamic => return None,
822            // A whole heterogeneous tuple's display (mixed element formats) is deferred; element
823            // access (`item N of t`) at each position's kind works.
824            Kind::Tuple => return None,
825            // A whole sequence of structs would need each struct's display, which is non-
826            // deterministic (struct field order); element access (`item N of xs`) works.
827            Kind::SeqStruct => return None,
828            // A whole sequence of enums (each `Ctor`/`Ctor(args)`) is deferred; iteration +
829            // `Inspect`/`TestArm` on each element works.
830            Kind::SeqEnum => return None,
831            // A whole nested sequence's display is deferred (needs a per-row formatter); row access
832            // (`item N of m` → a `SeqInt`) and element access work.
833            Kind::SeqSeqInt => return None,
834            // A `Rational` is Shown via a dedicated two-arg (`num`, `den`) path in `Op::Show`, not this
835            // single-value sink — so it never reaches here.
836            Kind::Rational => return None,
837            // An `Optional` is Shown via a dedicated null-check path in `Op::Show` (null → "nothing",
838            // else the boxed inner) — so it never reaches this single-value sink.
839            Kind::Optional => return None,
840            // A `Word32`/`Word64` Shows as its UNSIGNED value via a dedicated `print_word` path in
841            // `Op::Show` (Word32 zero-extends to i64 first) — never this signed single-value sink.
842            Kind::Word32 | Kind::Word64 => return None,
843        })
844    }
845}
846
847/// Interns `(params, results)` value-type signatures into a deduped Type section.
848#[derive(Default)]
849struct TypeTable {
850    sigs: Vec<(Vec<u8>, Vec<u8>)>,
851}
852
853impl TypeTable {
854    fn intern(&mut self, params: Vec<u8>, results: Vec<u8>) -> u32 {
855        if let Some(i) = self.sigs.iter().position(|(p, r)| *p == params && *r == results) {
856            return i as u32;
857        }
858        self.sigs.push((params, results));
859        (self.sigs.len() - 1) as u32
860    }
861
862    fn encode(&self) -> Vec<u8> {
863        let mut out = Vec::new();
864        leb_u32(&mut out, self.sigs.len() as u32);
865        for (params, results) in &self.sigs {
866            out.push(0x60); // func type
867            leb_u32(&mut out, params.len() as u32);
868            out.extend_from_slice(params);
869            leb_u32(&mut out, results.len() as u32);
870            out.extend_from_slice(results);
871        }
872        out
873    }
874}
875
876/// One function the assembler will emit: its rebased ops, inferred register kinds, parameter
877/// count, register frame size, result kind (`None` = returns nothing / void), and which basic
878/// blocks are statically reachable (dead blocks — e.g. the monomorphized-out branch of an
879/// `and`/`or` runtime type-dispatch — are emitted as a single `unreachable`).
880struct Plan {
881    ops: Vec<Op>,
882    kinds: KindTable,
883    num_params: u32,
884    num_regs: u32,
885    result: Option<Kind>,
886    reachable_blocks: Vec<bool>,
887    /// Per-op struct slot/field/count map (see [`kind::struct_layout`]) — drives `NewStruct`
888    /// sizing and `StructInsert`/`GetField` slot addressing.
889    structs: kind::StructLayout,
890    /// Per-pc: whether this `StructInsert` must copy-on-write its target to preserve struct value
891    /// semantics (see [`cow_struct_inserts`]).
892    cow_inserts: Vec<bool>,
893    /// Per composite-handle register, its resolved access SHAPE — `structs.reg_shape` (params + local
894    /// structs) COMPLETED post-inference with locally-built map/enum/het-tuple shapes (which need the
895    /// register kinds inference produced). Read at each `MakeClosure` to type a captured composite.
896    reg_shape: std::collections::HashMap<u16, kind::ParamShape>,
897    /// If this function `Return`s a single statically-known closure (every reachable closure `Return`
898    /// agreeing on one body function index), that index — so a caller of this function can resolve a
899    /// `CallValue` on the returned handle. `None` if it returns no closure, or returns more than one.
900    return_closure: Option<u16>,
901    /// A STUB: a function unreachable from `Main` that the AOT dropped (its body is a single `unreachable`
902    /// trap, its type `() -> ()`). This lets a program import a large stdlib module and compile even when
903    /// the UNUSED functions use ops the AOT can't lower (e.g. `uuid_v5` pulls in `uuid.lg`, whose unused
904    /// `uuidParse`/`decodeNibbles` use a `Lanes16Word8` SIMD vocabulary the backend doesn't have).
905    stub: bool,
906}
907
908impl Plan {
909    /// A dropped (unreachable) function — a `() -> ()` body that just traps.
910    fn stub() -> Plan {
911        Plan {
912            ops: Vec::new(),
913            kinds: KindTable::empty(),
914            num_params: 0,
915            num_regs: 0,
916            result: None,
917            reachable_blocks: Vec::new(),
918            structs: kind::StructLayout::default(),
919            cow_inserts: Vec::new(),
920            reg_shape: std::collections::HashMap::new(),
921            return_closure: None,
922            stub: true,
923        }
924    }
925}
926
927/// Infer register kinds with reachability-DCE: walk the CFG to find the reachable
928/// blocks, then run the strict kind inference over reachable ops only (so a dead
929/// branch's writes never poison a register). Returns the kinds plus the per-block
930/// reachability the emitter uses.
931fn infer_with_reachability(
932    ops: &[Op],
933    constants: &[Constant],
934    struct_types: &[StructTypeDef],
935    enum_types: &[EnumTypeDef],
936    fn_return_types: &[Option<BoundaryType>],
937    num_regs: usize,
938    seeds: &[Option<Kind>],
939    ret_of: &dyn Fn(usize) -> Option<Kind>,
940    global_of: &dyn Fn(u16) -> Option<Kind>,
941    closure_ret: &dyn Fn(usize) -> Option<Kind>,
942    ret_layout: &dyn Fn(u16) -> Option<FieldLayout>,
943    fn_return_closure: &dyn Fn(u16) -> Option<u16>,
944    param_layouts: &[(u16, ParamShape)],
945    param_closures: &[(u16, u16)],
946    linked: bool,
947    functions: &[crate::vm::instruction::CompiledFunction],
948) -> R<(KindTable, Vec<bool>, Vec<bool>)> {
949    let blocks = Blocks::new(ops).ok_or(WasmLowerError::Unsupported("jump target escapes the function"))?;
950    let (pc_reach, block_reach) = reachability(ops, &blocks);
951    // The general Int-overflow→BigInt promotion set (empty unless linked): registers only observed or
952    // fed into more BigInt arithmetic, computed once over this region's post-regsplit ops.
953    let bigint_demand = kind::bigint_demanded_regs(ops, functions, linked);
954    let kinds = kind::infer(ops, constants, struct_types, enum_types, fn_return_types, num_regs, seeds, ret_of, global_of, closure_ret, ret_layout, fn_return_closure, param_layouts, param_closures, &pc_reach, linked, &bigint_demand)?;
955    Ok((kinds, pc_reach, block_reach))
956}
957
958/// Compute reachability from the entry block. Returns `(per-pc reachable,
959/// per-block reachable)`.
960fn reachability(ops: &[Op], blocks: &Blocks) -> (Vec<bool>, Vec<bool>) {
961    let nb = blocks.num_blocks();
962    let mut block_reach = vec![false; nb];
963    let mut stack = vec![0usize];
964    while let Some(k) = stack.pop() {
965        if block_reach[k] {
966            continue;
967        }
968        block_reach[k] = true;
969        for s in block_successors(ops, blocks, k) {
970            if !block_reach[s] {
971                stack.push(s);
972            }
973        }
974    }
975    let mut pc_reach = vec![false; ops.len()];
976    for (k, &live) in block_reach.iter().enumerate() {
977        if live {
978            for pc in blocks.start(k)..blocks.end(k) {
979                pc_reach[pc] = true;
980            }
981        }
982    }
983    (pc_reach, block_reach)
984}
985
986/// The successor blocks of block `k`.
987fn block_successors(ops: &[Op], blocks: &Blocks, k: usize) -> Vec<usize> {
988    let n = ops.len();
989    let fallthrough = |pc: usize| -> Vec<usize> {
990        if pc + 1 < n {
991            vec![blocks.block_of(pc + 1)]
992        } else {
993            vec![]
994        }
995    };
996    for pc in blocks.start(k)..blocks.end(k) {
997        match ops[pc] {
998            Op::Jump { target } => return vec![blocks.block_of(target)],
999            Op::JumpIfFalse { target, .. } | Op::JumpIfTrue { target, .. } => {
1000                let mut s = vec![blocks.block_of(target)];
1001                s.extend(fallthrough(pc));
1002                return s;
1003            }
1004            // `IterNext` branches to `exit` (the matching `IterPop`) when the snapshot is
1005            // exhausted, else falls through to the loop body — both blocks are reachable.
1006            Op::IterNext { exit, .. } => {
1007                let mut s = vec![blocks.block_of(exit)];
1008                s.extend(fallthrough(pc));
1009                return s;
1010            }
1011            Op::Return { .. } | Op::ReturnNothing | Op::Halt | Op::FailWith { .. } => return vec![],
1012            _ => {}
1013        }
1014    }
1015    // Fell through the block end with no terminator.
1016    if blocks.end(k) < n {
1017        vec![blocks.block_of(blocks.end(k))]
1018    } else {
1019        vec![]
1020    }
1021}
1022
1023/// Lower a whole program to a self-contained WebAssembly module (the bytes of a `.wasm` file).
1024/// Returns [`WasmLowerError::Unsupported`] for any program outside the scalar fragment — a
1025/// sound refusal, never a wrong module.
1026pub fn assemble_program(program: &CompiledProgram, policies: &PolicyRegistry, interner: &Interner) -> R<Vec<u8>> {
1027    assemble_program_impl(program, policies, interner, false)
1028}
1029
1030/// Linker-mode assembly: an integer `Op::Pow` lowers to the real `logicaffeine_base::BigInt` runtime
1031/// (`logos_rt_bigint_from_i64`→`_pow`→`_to_text`) yielding a `Text` handle, so an overflowing
1032/// `x to the power of y` computes the exact big integer the VM's BigInt promotion prints instead of
1033/// trapping. The module imports `env.__linear_memory` (the linker supplies one shared memory) and the
1034/// three `logos_rt_bigint_*` functions by undefined symbol; [`super::reloc::module_to_relocatable`] +
1035/// `rust-lld` link it against the prebuilt base runtime. Emitter-side heap allocation is refused (two
1036/// allocators over one linear memory would corrupt it — the only heap value is the runtime-built Text).
1037pub(crate) fn assemble_program_linked(program: &CompiledProgram, policies: &PolicyRegistry, interner: &Interner) -> R<Vec<u8>> {
1038    assemble_program_impl(program, policies, interner, true)
1039}
1040
1041fn assemble_program_impl(program: &CompiledProgram, policies: &PolicyRegistry, interner: &Interner, linked: bool) -> R<Vec<u8>> {
1042    // ---- 1. Plan Main + every user function (rebase, infer kinds, resolve result kind) ----
1043    let layout = code_layout(program)?;
1044    let ret_of = |fi: usize| -> Option<Kind> { declared_result(program, fi) };
1045    let no_closure = |_: usize| -> Option<Kind> { None };
1046    let no_ret_layout = |_: u16| -> Option<FieldLayout> { None };
1047    let no_ret_closure = |_: u16| -> Option<u16> { None };
1048    let no_param_origin = |_: usize, _: usize| -> Option<u16> { None };
1049
1050    // Plan Main once with no resolvers — it types both the values stored into globals (read below)
1051    // AND the local registers a `MakeClosure` captures (the cross-scope closure-capture flow below).
1052    let num_globals = program.globals.len();
1053    let no_globals = |_: u16| None;
1054    let no_caps: Vec<Vec<Option<Kind>>> = Vec::new();
1055    let main_p1 = plan_main(program, &layout, &ret_of, &no_globals, &no_closure, &no_ret_layout, &no_ret_closure, linked)?;
1056    let global_kinds: Vec<Option<Kind>> = {
1057        let mut gk = vec![None; num_globals];
1058        for op in &main_p1.ops {
1059            if let Op::GlobalSet { idx, src } = *op {
1060                if let Some(k) = main_p1.kinds.get(src as usize) {
1061                    gk[idx as usize] = Some(k);
1062                }
1063            }
1064        }
1065        gk
1066    };
1067    let global_of = |idx: u16| -> Option<Kind> { global_kinds.get(idx as usize).copied().flatten() };
1068    // Per global, the body function index of the CLOSURE it holds (a Main `Let f be (…) -> …` used in a
1069    // function/closure is promoted to a global), so a closure capturing such a global resolves the call
1070    // to the captured closure — the global analog of the local captured-closure trace.
1071    let global_closures: Vec<Option<u16>> = {
1072        let mut gc = vec![None; num_globals];
1073        for op in &main_p1.ops {
1074            if let Op::GlobalSet { idx, src } = *op {
1075                if let Some(&c) = main_p1.structs.closure_of.get(&src) {
1076                    gc[idx as usize] = Some(c);
1077                }
1078            }
1079        }
1080        gc
1081    };
1082    let global_closure_of = |idx: u16| -> Option<u16> { global_closures.get(idx as usize).copied().flatten() };
1083
1084    // CROSS-SCOPE + NESTED CLOSURE PLANNING — a FIXPOINT. A closure's capture kinds come from where it
1085    // is BUILT (its `MakeClosure`); its result kind flows UP to callers. A NESTED closure (built inside
1086    // another closure body) crosses BOTH directions over more levels than the old 2-pass could resolve
1087    // (the inner's captures need the outer planned; the outer's result needs the inner planned). So:
1088    // plan the non-closure functions (no captures → clean), then ITERATE — extract capture kinds/shapes
1089    // from EVERY currently-planned region (so a closure built inside an already-planned closure body
1090    // gets its captures) and re-plan every function with the latest captures + inferred call-result
1091    // kinds, TOLERATING a closure whose captures aren't resolvable yet (`.ok()`; it succeeds a later
1092    // round once its build-region is planned). Converges in O(nesting depth) rounds; the final STRICT
1093    // pass below is the backstop that rejects a genuinely-unsupported function.
1094    let no_shapes: Vec<Vec<Option<ParamShape>>> = Vec::new();
1095    let no_capture_closures: Vec<Vec<Option<u16>>> = Vec::new();
1096    let mut plans: Vec<Option<Plan>> = (0..program.functions.len()).map(|_| None).collect();
1097    for fi in 0..program.functions.len() {
1098        if !layout.is_closure[fi] {
1099            plans[fi] = Some(plan_function(program, fi, &layout, &ret_of, &global_of, &no_closure, &no_ret_layout, &no_ret_closure, &no_param_origin, &no_caps, &no_shapes, &no_capture_closures, false, linked)?);
1100        }
1101    }
1102    let mut capture_kinds: Vec<Vec<Option<Kind>>> = Vec::new();
1103    let mut capture_shapes: Vec<Vec<Option<ParamShape>>> = Vec::new();
1104    let mut capture_closures: Vec<Vec<Option<u16>>> = Vec::new();
1105    for _ in 0..(program.functions.len() + 4) {
1106        let (ck, cs, cc) = extract_capture_kinds(program, &main_p1, &plans, &global_of, &global_closure_of);
1107        capture_kinds = ck;
1108        capture_shapes = cs;
1109        capture_closures = cc;
1110        let cur_results: Vec<Option<Kind>> = plans.iter().map(|p| p.as_ref().and_then(|p| p.result)).collect();
1111        let cur_layouts: Vec<Option<FieldLayout>> = plans.iter().map(|p| p.as_ref().and_then(fn_return_struct_layout)).collect();
1112        let cur_ret_closures: Vec<Option<u16>> = plans.iter().map(|p| p.as_ref().and_then(|p| p.return_closure)).collect();
1113        let closure_ret = |fi: usize| cur_results.get(fi).copied().flatten();
1114        let ret_of_i =
1115            |fi: usize| cur_results.get(fi).copied().flatten().or_else(|| declared_result(program, fi));
1116        let ret_layout_of = |func: u16| cur_layouts.get(func as usize).cloned().flatten();
1117        let ret_closure_of = |func: u16| cur_ret_closures.get(func as usize).copied().flatten();
1118        // CLOSURES AS ARGUMENTS. Re-plan Main with this round's resolvers so its `closure_of` reflects
1119        // a returned closure bound to a local (then passed straight into a function); attribute every
1120        // call's closure arguments to the parameters they feed, so `f(args)` resolves when `f` is a
1121        // parameter. Recomputed each round — a param's origin can resolve transitively as callers plan.
1122        let main_iter = plan_main(program, &layout, &ret_of_i, &global_of, &closure_ret, &ret_layout_of, &ret_closure_of, linked).ok();
1123        let mut scan_plans: Vec<&Plan> = vec![main_iter.as_ref().unwrap_or(&main_p1)];
1124        scan_plans.extend(plans.iter().flatten());
1125        let param_origins = compute_param_origins(program, &scan_plans);
1126        let param_origin = |fi: usize, pi: usize| param_origins.get(fi).and_then(|v| v.get(pi)).copied().flatten();
1127        let mut progress = false;
1128        for fi in 0..program.functions.len() {
1129            if let Ok(p) = plan_function(program, fi, &layout, &ret_of_i, &global_of, &closure_ret, &ret_layout_of, &ret_closure_of, &param_origin, &capture_kinds, &capture_shapes, &capture_closures, false, linked) {
1130                let changed = plans[fi]
1131                    .as_ref()
1132                    .map_or(true, |x| x.result != p.result || x.return_closure != p.return_closure);
1133                if changed {
1134                    progress = true;
1135                }
1136                plans[fi] = Some(p);
1137            }
1138        }
1139        if !progress {
1140            break;
1141        }
1142    }
1143    let fns1: Vec<Plan> = plans.into_iter().map(|p| p.expect("every function planned by the fixpoint")).collect();
1144
1145    // Pass 2 (STRICT) re-plans every function WITH the resolvers (a call's inferred result kind +
1146    // struct-return field layout), so cross-region `f(…)'s field` resolves and a genuinely unknown
1147    // return is rejected rather than deferred. `capture_kinds` carries through so closure bodies keep
1148    // their capture typing. Resolvers affect only `GetField`, so pass-1 results/layouts stay valid.
1149    let fn_results_p1: Vec<Option<Kind>> = fns1.iter().map(|p| p.result).collect();
1150    let ret_layouts: Vec<Option<FieldLayout>> = fns1.iter().map(fn_return_struct_layout).collect();
1151    let ret_closures: Vec<Option<u16>> = fns1.iter().map(|p| p.return_closure).collect();
1152    let closure_ret = |fi: usize| -> Option<Kind> { fn_results_p1.get(fi).copied().flatten() };
1153    let ret_of_inferred =
1154        |fi: usize| -> Option<Kind> { fn_results_p1.get(fi).copied().flatten().or_else(|| declared_result(program, fi)) };
1155    let ret_layout_of = |func: u16| -> Option<FieldLayout> { ret_layouts.get(func as usize).cloned().flatten() };
1156    let ret_closure_of = |func: u16| -> Option<u16> { ret_closures.get(func as usize).copied().flatten() };
1157    // Main is planned first (it doesn't depend on the strict `fns`, only on the converged resolvers)
1158    // so it can join the param-origin scan — a closure argument is frequently passed from Main.
1159    let main = plan_main(program, &layout, &ret_of_inferred, &global_of, &closure_ret, &ret_layout_of, &ret_closure_of, linked)?;
1160    let scan_plans: Vec<&Plan> = std::iter::once(&main).chain(fns1.iter()).collect();
1161    let param_origins = compute_param_origins(program, &scan_plans);
1162    let param_origin = |fi: usize, pi: usize| param_origins.get(fi).and_then(|v| v.get(pi)).copied().flatten();
1163    // Function-level DCE: a function unreachable from `Main` (via direct `Call` / closure `MakeClosure`
1164    // edges, transitively) is dropped to a trap stub instead of being strictly planned. This lets a
1165    // program demand-import a large stdlib module and compile even when its UNUSED functions use ops the
1166    // backend can't lower — `uuid_v5` pulls in all of `uuid.lg`, whose unused `uuidParse`/`decodeNibbles`
1167    // need a `Lanes16Word8` SIMD vocabulary the AOT doesn't have; dropping them is sound (never called).
1168    let reachable = {
1169        // Main's region is `[0..main_end)` — the boundary where the REGULAR functions begin. It must be
1170        // `main_end`, NOT the min entry_pc over all functions: an INLINE closure (`Let f be (x)->…` in
1171        // Main) is emitted inside Main's region with a smaller entry_pc, so keying on the min would
1172        // truncate Main's slice before the `MakeClosure` op that references it — stubbing a live closure
1173        // body into a trap. A reached regular function's own region (`func_region[fi]`, which spans its
1174        // interleaved inline-closure holes) then collects its inline closures transitively below.
1175        let main_end = layout.main_end.min(program.code.len());
1176        let mut r = vec![false; program.functions.len()];
1177        let mut stack: Vec<usize> = Vec::new();
1178        // Every op carrying a `func` edge (the complete call graph): a direct `Call`, a `MakeClosure`
1179        // body, and the two task-spawn forms (`Spawn`/`SpawnHandle`). Missing any would stub a live
1180        // callee into a trap.
1181        let collect = |ops: &[Op], stack: &mut Vec<usize>| {
1182            for op in ops {
1183                if let Op::Call { func, .. }
1184                | Op::MakeClosure { func, .. }
1185                | Op::Spawn { func, .. }
1186                | Op::SpawnHandle { func, .. } = op
1187                {
1188                    stack.push(*func as usize);
1189                }
1190            }
1191        };
1192        collect(&program.code[0..main_end], &mut stack);
1193        while let Some(fi) = stack.pop() {
1194            if fi >= r.len() || r[fi] {
1195                continue;
1196            }
1197            r[fi] = true;
1198            let (s, e) = layout.func_region[fi];
1199            if s <= e && e <= program.code.len() {
1200                collect(&program.code[s..e], &mut stack);
1201            }
1202        }
1203        r
1204    };
1205    let fns: Vec<Plan> = {
1206        let mut v = Vec::with_capacity(program.functions.len());
1207        for fi in 0..program.functions.len() {
1208            if !reachable[fi] {
1209                v.push(Plan::stub());
1210                continue;
1211            }
1212            v.push(plan_function(program, fi, &layout, &ret_of_inferred, &global_of, &closure_ret, &ret_layout_of, &ret_closure_of, &param_origin, &capture_kinds, &capture_shapes, &capture_closures, true, linked)?);
1213        }
1214        v
1215    };
1216
1217    // Whether the program uses the emitter's heap value model (a linear memory + bump allocator), an
1218    // iterator stack, or closures — computed HERE (before the import scan) so linker mode can import the
1219    // runtime allocator for the slab + refuse the shapes it can't yet share.
1220    // A heap op, OR a `LoadConst` of a Text literal (which materializes a Text object in memory).
1221    let loads_text = |op: &Op| matches!(op, Op::LoadConst { idx, .. } if matches!(program.constants.get(*idx as usize), Some(Constant::Text(_))));
1222    // A Text-typed `+`/`+=` (string concatenation) allocates a fresh Text, so it needs the heap even
1223    // if the program has no Text literal (it builds from a Text-valued variable).
1224    let concats_text = |p: &Plan, op: &Op| match *op {
1225        Op::Add { dst, .. } | Op::AddAssign { dst, .. } => p.kinds.get(dst as usize) == Some(Kind::Text),
1226        _ => false,
1227    };
1228    let uses_heap = std::iter::once(&main)
1229        .chain(fns.iter())
1230        .any(|p| p.ops.iter().any(|op| op_uses_heap(op) || loads_text(op) || concats_text(p, op)));
1231    let uses_iter = std::iter::once(&main)
1232        .chain(fns.iter())
1233        .any(|p| p.ops.iter().any(|op| matches!(op, Op::IterPrepare { .. })));
1234    // Closures need a function table (for `call_indirect`) in the self-contained path; emit it only
1235    // when the program has one. LINKER MODE lowers a closure `CallValue` to a DIRECT `call` instead (the
1236    // callee is statically resolved, or already refused), so it needs NEITHER a table NOR an element
1237    // section — nothing the reloc transform can't handle. (A closure captured through a truly dynamic
1238    // value stays refused inside `lower_call_value`.)
1239    let has_closures = layout.is_closure.iter().any(|&c| c);
1240
1241    // ---- 2. Which host functions are used (in stable order) → their import indices ----
1242    let mut used = Vec::new();
1243    let note = |h: HostFn, used: &mut Vec<HostFn>| {
1244        if !used.contains(&h) {
1245            used.push(h);
1246        }
1247    };
1248    // The host formatter a value of a given kind is stringified through (in a `Concat`, a Text-typed
1249    // `+`, or a whole-tuple / payload-enum `Show`). A kind with no scalar formatter notes nothing —
1250    // the per-op lowering soundly refuses it, so no import is wasted on a shape it can't emit.
1251    let note_fmt_kind = |k: Option<Kind>, used: &mut Vec<HostFn>| match k {
1252        Some(Kind::Int) => note(HostFn::FmtI64Into, used),
1253        Some(Kind::Float) => note(HostFn::FmtF64Into, used),
1254        Some(Kind::Bool) => note(HostFn::FmtBoolInto, used),
1255        // A whole `Seq of Int` / `Set of Int` operand is stringified by its collection formatter.
1256        Some(Kind::SeqInt | Kind::SeqAny) => note(HostFn::FmtSeqI64Into, used),
1257        // A whole `Seq of Bool` operand renders `[true, false, …]` via its own formatter.
1258        Some(Kind::SeqBool) => note(HostFn::FmtSeqBoolInto, used),
1259        Some(Kind::Set) => note(HostFn::FmtSetI64Into, used),
1260        // A BigInt operand of a concat is stringified to its decimal Text via the runtime.
1261        Some(Kind::BigInt) => note(HostFn::BigintToText, used),
1262        _ => {}
1263    };
1264    // A stringified operand (in a `Concat` or a Text-typed `+`) needs its scalar host formatter.
1265    let note_operand_fmt = |plan: &Plan, r: u16, used: &mut Vec<HostFn>| note_fmt_kind(plan.kinds.get(r as usize), used);
1266    for plan in std::iter::once(&main).chain(fns.iter()) {
1267        for op in &plan.ops {
1268            match *op {
1269                // Note the sink for any Show whose kind is known. An unknown-kind Show is NOT an
1270                // error here: it is either in a statically-dead block (never lowered — e.g. the
1271                // `Show` after an unbound-variable `FailWith`) or a genuinely unsupported kind that
1272                // the reachability-respecting per-block lowering will reject. Erroring here would
1273                // wrongly reject a program whose only unknown-kind Show is dead code.
1274                Op::Show { src } => {
1275                    if let Some(elems) = plan.structs.tuple_layouts.get(&src) {
1276                        // A whole-tuple `Show` (any tuple, homogeneous or not) assembles its `(…)`
1277                        // display as a Text and prints it, stringifying each element by its formatter.
1278                        note(HostFn::PrintText, &mut used);
1279                        for &e in elems {
1280                            note_operand_fmt(plan, e, &mut used);
1281                        }
1282                    } else if plan.kinds.get(src as usize) == Some(Kind::Enum) {
1283                        // A whole-enum `Show` prints the live variant's name via `print_text`; a
1284                        // PAYLOAD variant additionally stringifies each field inline (`Ctor(f0, …)`),
1285                        // so note every field type's formatter across the enum's variants.
1286                        note(HostFn::PrintText, &mut used);
1287                        if let Some(def) = plan
1288                            .structs
1289                            .ind_type_of
1290                            .get(&src)
1291                            .and_then(|tn| program.enum_types.iter().find(|e| &e.name == tn))
1292                        {
1293                            for v in &def.variants {
1294                                for ft in &v.field_types {
1295                                    note_fmt_kind(kind::boundary_to_kind(ft), &mut used);
1296                                }
1297                            }
1298                        }
1299                    } else if plan.kinds.get(src as usize) == Some(Kind::Map) {
1300                        // A whole-map `Show` assembles `{k: v, …}` and prints it; note `print_text` plus
1301                        // the key AND value formatters (resolved from the last SetIndex's registers).
1302                        note(HostFn::PrintText, &mut used);
1303                        if let Some(&kr) = plan.structs.map_set_key.get(&src) {
1304                            note_fmt_kind(plan.kinds.get(kr as usize), &mut used);
1305                        }
1306                        if let Some(&vr) = plan.structs.map_set_value.get(&src) {
1307                            note_fmt_kind(plan.kinds.get(vr as usize), &mut used);
1308                        }
1309                    } else if plan.kinds.get(src as usize) == Some(Kind::SeqSeqInt) {
1310                        // A nested int-seq `Show` prints `[[…], …]`: `print_text` for the assembled
1311                        // outer string, and the scalar seq formatter for each inner `Seq of Int`.
1312                        note(HostFn::PrintText, &mut used);
1313                        note(HostFn::FmtSeqI64Into, &mut used);
1314                    } else if plan.kinds.get(src as usize) == Some(Kind::SeqEnum) {
1315                        // A whole `Seq of Enum` `Show` prints `[e0, …]` via `print_text`; each element's
1316                        // payload fields (if any) stringify through their own formatters.
1317                        note(HostFn::PrintText, &mut used);
1318                        if let Some(def) = plan
1319                            .structs
1320                            .seq_elem_ind_type
1321                            .get(&src)
1322                            .and_then(|tn| program.enum_types.iter().find(|e| &e.name == tn))
1323                        {
1324                            for v in &def.variants {
1325                                for ft in &v.field_types {
1326                                    note_fmt_kind(kind::boundary_to_kind(ft), &mut used);
1327                                }
1328                            }
1329                        }
1330                    } else if plan.kinds.get(src as usize) == Some(Kind::Struct) {
1331                        // A whole struct `Show` prints `TypeName { … }` via `print_text`; each field
1332                        // stringifies through its own formatter.
1333                        note(HostFn::PrintText, &mut used);
1334                        if let Some(def) = plan
1335                            .structs
1336                            .struct_name_of
1337                            .get(&src)
1338                            .and_then(|tn| program.struct_types.iter().find(|s| &s.name == tn))
1339                        {
1340                            for (_, bt) in &def.fields {
1341                                note_fmt_kind(kind::boundary_to_kind(bt), &mut used);
1342                            }
1343                        }
1344                    } else if plan.kinds.get(src as usize) == Some(Kind::SeqStruct) {
1345                        // A whole `Seq of Struct` `Show` — `print_text` plus each element struct's field
1346                        // formatters (resolved from the seq's element struct type).
1347                        note(HostFn::PrintText, &mut used);
1348                        if let Some(def) = plan
1349                            .structs
1350                            .seq_elem_struct_name
1351                            .get(&src)
1352                            .and_then(|tn| program.struct_types.iter().find(|s| &s.name == tn))
1353                        {
1354                            for (_, bt) in &def.fields {
1355                                note_fmt_kind(kind::boundary_to_kind(bt), &mut used);
1356                            }
1357                        }
1358                    } else if plan.kinds.get(src as usize) == Some(Kind::Rational) {
1359                        // A `Rational` `Show`: LINKER mode renders the BigInt-backed handle via
1360                        // `logos_rt_rational_to_text`→`print_text`; the self-contained i64/i64 value uses
1361                        // the dedicated two-arg (num, den) `print_rational` host sink.
1362                        if linked {
1363                            note(HostFn::RationalToText, &mut used);
1364                            note(HostFn::PrintText, &mut used);
1365                        } else {
1366                            note(HostFn::PrintRational, &mut used);
1367                        }
1368                    } else if plan.kinds.get(src as usize) == Some(Kind::Optional) {
1369                        // An `Optional` `Show` uses `print_nothing` for the null handle, and the boxed
1370                        // inner scalar's own sink for the present (`Some`) case.
1371                        note(HostFn::PrintNothing, &mut used);
1372                        let inner = plan.structs.opt_inner.get(&src).copied().unwrap_or(Kind::Int);
1373                        if let Some(h) = HostFn::for_show(inner) {
1374                            note(h, &mut used);
1375                        }
1376                    } else if matches!(plan.kinds.get(src as usize), Some(Kind::Word32) | Some(Kind::Word64)) {
1377                        // A `Word` `Show` prints its unsigned value via `print_word`.
1378                        note(HostFn::PrintWord, &mut used);
1379                    } else if plan.kinds.get(src as usize) == Some(Kind::BigInt) {
1380                        // A `BigInt` `Show` renders the handle to a decimal Text then prints it.
1381                        note(HostFn::BigintToText, &mut used);
1382                        note(HostFn::PrintText, &mut used);
1383                    } else if plan.kinds.get(src as usize) == Some(Kind::Complex) {
1384                        // A `Complex` `Show` renders the handle to `re±imi` Text then prints it.
1385                        note(HostFn::ComplexToText, &mut used);
1386                        note(HostFn::PrintText, &mut used);
1387                    } else if plan.kinds.get(src as usize) == Some(Kind::Modular) {
1388                        note(HostFn::ModularToText, &mut used);
1389                        note(HostFn::PrintText, &mut used);
1390                    } else if plan.kinds.get(src as usize) == Some(Kind::Decimal) {
1391                        note(HostFn::DecimalToText, &mut used);
1392                        note(HostFn::PrintText, &mut used);
1393                    } else if plan.kinds.get(src as usize) == Some(Kind::Money) {
1394                        note(HostFn::MoneyToText, &mut used);
1395                        note(HostFn::PrintText, &mut used);
1396                    } else if plan.kinds.get(src as usize) == Some(Kind::Quantity) {
1397                        note(HostFn::QuantityToText, &mut used);
1398                        note(HostFn::PrintText, &mut used);
1399                    } else if plan.kinds.get(src as usize) == Some(Kind::Uuid) {
1400                        note(HostFn::UuidToText, &mut used);
1401                        note(HostFn::PrintText, &mut used);
1402                    } else if plan.kinds.get(src as usize) == Some(Kind::Dynamic) {
1403                        // A wire-decoded DYNAMIC value renders via `to_display_string` then prints.
1404                        note(HostFn::DynamicToText, &mut used);
1405                        note(HostFn::PrintText, &mut used);
1406                    } else if let Some(h) = plan.kinds.get(src as usize).and_then(HostFn::for_show) {
1407                        note(h, &mut used);
1408                    }
1409                }
1410                Op::CallBuiltin { builtin: BuiltinId::Pow, args_start, .. } => {
1411                    let bk = plan.kinds.get(args_start as usize);
1412                    let ek = plan.kinds.get((args_start + 1) as usize);
1413                    if let Some(h) = pow_host_for(bk, ek) {
1414                        note(h, &mut used);
1415                    }
1416                }
1417                // `a ** b` (the operator) shares `pow`'s host `pow_ff`/`pow_fi` for a Float result.
1418                // In LINKER mode an integer power drives the `logos_rt_bigint_*` runtime, leaving a
1419                // BigInt HANDLE (its `to_text` render is noted by the `Show` arm, not here).
1420                Op::Pow { lhs, rhs, .. } => {
1421                    let bk = plan.kinds.get(lhs as usize);
1422                    let ek = plan.kinds.get(rhs as usize);
1423                    if linked && bk == Some(Kind::Int) && ek == Some(Kind::Int) {
1424                        note(HostFn::BigintFromI64, &mut used);
1425                        note(HostFn::BigintPow, &mut used);
1426                    } else if let Some(h) = pow_host_for(bk, ek) {
1427                        note(h, &mut used);
1428                    }
1429                }
1430                // `+ - * / %` producing a BigInt (linker mode) call the matching `logos_rt_bigint_*` sink;
1431                // an `Int` operand is promoted with `from_i64`. Keyed on the result kind (which is BigInt
1432                // iff an operand was), so it wins over the numeric / Text-concat `Add` arms below.
1433                Op::Mul { dst, lhs, rhs }
1434                | Op::Add { dst, lhs, rhs }
1435                | Op::Sub { dst, lhs, rhs }
1436                | Op::Div { dst, lhs, rhs }
1437                | Op::Mod { dst, lhs, rhs }
1438                    if linked && plan.kinds.get(dst as usize) == Some(Kind::BigInt) =>
1439                {
1440                    note(
1441                        match *op {
1442                            Op::Add { .. } => HostFn::BigintAdd,
1443                            Op::Sub { .. } => HostFn::BigintSub,
1444                            Op::Div { .. } => HostFn::BigintDiv,
1445                            Op::Mod { .. } => HostFn::BigintMod,
1446                            _ => HostFn::BigintMul,
1447                        },
1448                        &mut used,
1449                    );
1450                    if plan.kinds.get(lhs as usize) == Some(Kind::Int) || plan.kinds.get(rhs as usize) == Some(Kind::Int) {
1451                        note(HostFn::BigintFromI64, &mut used);
1452                    }
1453                }
1454                // `+ - *` producing a Complex (linker mode) call the matching `logos_rt_complex_*` sink;
1455                // an `Int` operand is promoted with `from_i64` (real `n + 0i`).
1456                Op::Add { dst, lhs, rhs } | Op::Sub { dst, lhs, rhs } | Op::Mul { dst, lhs, rhs }
1457                    if linked && plan.kinds.get(dst as usize) == Some(Kind::Complex) =>
1458                {
1459                    note(
1460                        match *op {
1461                            Op::Add { .. } => HostFn::ComplexAdd,
1462                            Op::Sub { .. } => HostFn::ComplexSub,
1463                            _ => HostFn::ComplexMul,
1464                        },
1465                        &mut used,
1466                    );
1467                    if plan.kinds.get(lhs as usize) == Some(Kind::Int) || plan.kinds.get(rhs as usize) == Some(Kind::Int) {
1468                        note(HostFn::ComplexFromI64, &mut used);
1469                    }
1470                }
1471                // `complex(re, im)` builds a Complex handle via the runtime.
1472                Op::CallBuiltin { builtin: BuiltinId::Complex, .. } if linked => note(HostFn::ComplexFromI64, &mut used),
1473                Op::Add { dst, lhs, rhs } | Op::Sub { dst, lhs, rhs } | Op::Mul { dst, lhs, rhs }
1474                    if linked && plan.kinds.get(dst as usize) == Some(Kind::Modular) =>
1475                {
1476                    note(match *op { Op::Add { .. } => HostFn::ModularAdd, Op::Sub { .. } => HostFn::ModularSub, _ => HostFn::ModularMul }, &mut used);
1477                    if plan.kinds.get(lhs as usize) == Some(Kind::Int) || plan.kinds.get(rhs as usize) == Some(Kind::Int) { note(HostFn::ModularFromI64, &mut used); }
1478                }
1479                Op::CallBuiltin { builtin: BuiltinId::Modular, .. } if linked => note(HostFn::ModularFromI64, &mut used),
1480                Op::Add { dst, lhs, rhs } | Op::Sub { dst, lhs, rhs } | Op::Mul { dst, lhs, rhs }
1481                    if linked && plan.kinds.get(dst as usize) == Some(Kind::Decimal) =>
1482                {
1483                    note(match *op { Op::Add { .. } => HostFn::DecimalAdd, Op::Sub { .. } => HostFn::DecimalSub, _ => HostFn::DecimalMul }, &mut used);
1484                    if plan.kinds.get(lhs as usize) == Some(Kind::Int) || plan.kinds.get(rhs as usize) == Some(Kind::Int) { note(HostFn::DecimalFromI64, &mut used); }
1485                }
1486                Op::CallBuiltin { builtin: BuiltinId::Decimal, .. } if linked => note(HostFn::DecimalFromText, &mut used),
1487                Op::Add { dst, lhs, rhs } | Op::Sub { dst, lhs, rhs }
1488                    if linked && plan.kinds.get(dst as usize) == Some(Kind::Money) =>
1489                {
1490                    let _ = (lhs, rhs);
1491                    note(match *op { Op::Add { .. } => HostFn::MoneyAdd, _ => HostFn::MoneySub }, &mut used);
1492                }
1493                Op::CallBuiltin { builtin: BuiltinId::Money, .. } if linked => { note(HostFn::MoneyFromDecimal, &mut used); note(HostFn::MoneyFromI64, &mut used); }
1494                Op::Add { dst, lhs, rhs } | Op::Sub { dst, lhs, rhs } | Op::Mul { dst, lhs, rhs } | Op::Div { dst, lhs, rhs }
1495                    if linked && plan.kinds.get(dst as usize) == Some(Kind::Quantity) =>
1496                {
1497                    let _ = (lhs, rhs);
1498                    note(match *op {
1499                        Op::Add { .. } => HostFn::QuantityAdd,
1500                        Op::Sub { .. } => HostFn::QuantitySub,
1501                        Op::Mul { .. } => HostFn::QuantityMul,
1502                        _ => HostFn::QuantityDiv,
1503                    }, &mut used);
1504                }
1505                Op::CallBuiltin { builtin: BuiltinId::Quantity, .. } if linked => note(HostFn::QuantityOfI64, &mut used),
1506                Op::CallBuiltin { builtin: BuiltinId::Convert, .. } if linked => note(HostFn::QuantityConvert, &mut used),
1507                // `+ - * /` on a Rational operand (linker): the BigInt-backed runtime op + a `from_i64`/
1508                // `from_bigint` promotion for any Int/BigInt operand that must widen to a Rational first.
1509                // (`/` here is a bare `Div` on two Rationals — the exact-division literal is `ExactDiv`.)
1510                Op::Add { dst, lhs, rhs } | Op::Sub { dst, lhs, rhs } | Op::Mul { dst, lhs, rhs } | Op::Div { dst, lhs, rhs }
1511                    if linked && plan.kinds.get(dst as usize) == Some(Kind::Rational) =>
1512                {
1513                    note(match *op {
1514                        Op::Add { .. } => HostFn::RationalAdd,
1515                        Op::Sub { .. } => HostFn::RationalSub,
1516                        Op::Mul { .. } => HostFn::RationalMul,
1517                        _ => HostFn::RationalDiv,
1518                    }, &mut used);
1519                    for r in [lhs, rhs] {
1520                        match plan.kinds.get(r as usize) {
1521                            Some(Kind::Int) => note(HostFn::RationalFromI64, &mut used),
1522                            Some(Kind::BigInt) => note(HostFn::RationalFromBigint, &mut used),
1523                            _ => {}
1524                        }
1525                    }
1526                }
1527                // `a / b` in a Rational context (`ExactDiv`, linker): the BigInt-backed division + the same
1528                // operand promotions (an Int/Int `7 / 2` promotes both, `r / 2` promotes only the Int).
1529                Op::ExactDiv { lhs, rhs, .. } if linked => {
1530                    note(HostFn::RationalDiv, &mut used);
1531                    for r in [lhs, rhs] {
1532                        match plan.kinds.get(r as usize) {
1533                            Some(Kind::Int) => note(HostFn::RationalFromI64, &mut used),
1534                            Some(Kind::BigInt) => note(HostFn::RationalFromBigint, &mut used),
1535                            _ => {}
1536                        }
1537                    }
1538                }
1539                // `floor`/`ceil`/`round`/`abs` of a Rational (linker): the exact `logos_rt_rational_*` sink.
1540                Op::CallBuiltin { builtin: b @ (BuiltinId::Floor | BuiltinId::Ceil | BuiltinId::Round | BuiltinId::Abs), args_start, .. }
1541                    if linked && plan.kinds.get(args_start as usize) == Some(Kind::Rational) =>
1542                {
1543                    note(match b {
1544                        BuiltinId::Floor => HostFn::RationalFloor,
1545                        BuiltinId::Ceil => HostFn::RationalCeil,
1546                        BuiltinId::Round => HostFn::RationalRound,
1547                        _ => HostFn::RationalAbs,
1548                    }, &mut used);
1549                }
1550                // The `Uuid`-producing / -reading builtins (linker): each maps to its `logos_rt_uuid_*` sink.
1551                Op::CallBuiltin { builtin: b @ (BuiltinId::Uuid | BuiltinId::UuidNil | BuiltinId::UuidMax | BuiltinId::UuidDns | BuiltinId::UuidUrl | BuiltinId::UuidOid | BuiltinId::UuidX500 | BuiltinId::UuidVersion), .. } if linked => {
1552                    note(match b {
1553                        BuiltinId::Uuid => HostFn::UuidParse,
1554                        BuiltinId::UuidNil => HostFn::UuidNil,
1555                        BuiltinId::UuidMax => HostFn::UuidMax,
1556                        BuiltinId::UuidDns => HostFn::UuidDns,
1557                        BuiltinId::UuidUrl => HostFn::UuidUrl,
1558                        BuiltinId::UuidOid => HostFn::UuidOid,
1559                        BuiltinId::UuidX500 => HostFn::UuidX500,
1560                        _ => HostFn::UuidVersion,
1561                    }, &mut used);
1562                }
1563                // `uuid_from_bytes(seq)` boxes a Uuid from a packed 16-byte block via the runtime.
1564                Op::CallBuiltin { builtin: BuiltinId::UuidFromBytes, .. } if linked => note(HostFn::UuidFromPtr, &mut used),
1565                // The four SHA-1 SHA-NI ops call the `logos_rt_sha1*` runtime (base::sha_ops spec).
1566                Op::CallBuiltin { builtin: b @ (BuiltinId::Sha1Rnds4 | BuiltinId::Sha1Msg1 | BuiltinId::Sha1Msg2 | BuiltinId::Sha1Nexte), .. } if linked => {
1567                    note(match b {
1568                        BuiltinId::Sha1Rnds4 => HostFn::Sha1Rnds4,
1569                        BuiltinId::Sha1Msg1 => HostFn::Sha1Msg1,
1570                        BuiltinId::Sha1Msg2 => HostFn::Sha1Msg2,
1571                        _ => HostFn::Sha1Nexte,
1572                    }, &mut used);
1573                }
1574                // Lane-wise `Lanes + Lanes` / `lanes += lanes` → the `logos_rt_lanes4_add` runtime.
1575                Op::Add { lhs, .. } if linked && plan.kinds.get(lhs as usize) == Some(Kind::Lanes) => note(HostFn::Lanes4Add, &mut used),
1576                Op::AddAssign { dst, .. } if linked && plan.kinds.get(dst as usize) == Some(Kind::Lanes) => note(HostFn::Lanes4Add, &mut used),
1577                // Lane-wise `Lanes xor Lanes` → the `logos_rt_lanes4_xor` runtime.
1578                Op::BitXor { lhs, .. } if linked && plan.kinds.get(lhs as usize) == Some(Kind::Lanes) => note(HostFn::Lanes4Xor, &mut used),
1579                // `Moment/Date ± Span` calendar arithmetic (linker) — the base's width picks the sink.
1580                Op::Add { lhs, rhs, .. } | Op::Sub { lhs, rhs, .. }
1581                    if linked && (plan.kinds.get(lhs as usize) == Some(Kind::Span) || plan.kinds.get(rhs as usize) == Some(Kind::Span)) =>
1582                {
1583                    let base = if plan.kinds.get(lhs as usize) == Some(Kind::Span) { rhs } else { lhs };
1584                    note(if plan.kinds.get(base as usize) == Some(Kind::Date) { HostFn::DateAddSpan } else { HostFn::MomentAddSpan }, &mut used);
1585                }
1586                // `uuid == uuid` (equality on two Uuid operands, linker) — the 16-byte `logos_rt_uuid_eq`.
1587                Op::Eq { lhs, rhs, .. } | Op::NotEq { lhs, rhs, .. }
1588                    if linked && plan.kinds.get(lhs as usize) == Some(Kind::Uuid) && plan.kinds.get(rhs as usize) == Some(Kind::Uuid) =>
1589                {
1590                    note(HostFn::UuidEq, &mut used);
1591                }
1592                Op::LoadToday { .. } => note(HostFn::Today, &mut used),
1593                Op::LoadNow { .. } => note(HostFn::Now, &mut used),
1594                Op::Args { .. } => note(HostFn::Args, &mut used),
1595                Op::CallBuiltin { builtin: BuiltinId::ParseInt, .. } => note(HostFn::ParseInt, &mut used),
1596                Op::CallBuiltin { builtin: BuiltinId::ParseFloat, .. } => note(HostFn::ParseFloat, &mut used),
1597                Op::CallBuiltin { builtin: BuiltinId::ParseTimestamp, .. } => note(HostFn::ParseTimestamp, &mut used),
1598                // `writeWireResidual(text)` frames the Text (`[len:u32][bytes]`) out to the host sink.
1599                Op::CallBuiltin { builtin: BuiltinId::WriteWireResidual, .. } => note(HostFn::WriteWireResidual, &mut used),
1600                // LINKER-mode extended temporal (calendar logic in `base::temporal`): `format_timestamp`
1601                // → a `Text` handle, `months_between`/`years_between` → an `Int`.
1602                Op::CallBuiltin { builtin: BuiltinId::FormatTimestamp, .. } if linked => note(HostFn::FormatTimestampRt, &mut used),
1603                Op::CallBuiltin { builtin: BuiltinId::MonthsBetween, .. } if linked => note(HostFn::MonthsBetweenRt, &mut used),
1604                Op::CallBuiltin { builtin: BuiltinId::YearsBetween, .. } if linked => note(HostFn::YearsBetweenRt, &mut used),
1605                // `in_zone(m, "zone")` → a `Text` (local wall-clock), `local_instant(m, "zone")` → a `Moment`.
1606                Op::CallBuiltin { builtin: BuiltinId::InZone, .. } if linked => note(HostFn::InZoneRt, &mut used),
1607                Op::CallBuiltin { builtin: BuiltinId::LocalInstant, .. } if linked => note(HostFn::LocalInstantRt, &mut used),
1608                // The general SIMD lane vocabulary (`base::LanesVal`) — the SSE byte/word-lane ops a Logos
1609                // codec compiles to. Each notes its `logos_rt_lanes_*` runtime fn (linker mode only).
1610                Op::CallBuiltin { builtin: b, .. } if linked && lanes_v_host_fn(b).is_some() => {
1611                    note(lanes_v_host_fn(b).unwrap(), &mut used)
1612                }
1613                // Money FX (linker): `to_currency`→convert; `set_rate` installs one rate (coercing the
1614                // rate arg Int→Rational / Decimal→Rational); `set_rates` installs a whole Map (the runtime
1615                // reads it — all three value-kind variants are noted since the map's value kind is resolved
1616                // at lowering; an imported-but-uncalled runtime fn is harmless, all are defined).
1617                Op::CallBuiltin { builtin: BuiltinId::ToCurrency, .. } if linked => note(HostFn::MoneyToCurrency, &mut used),
1618                Op::CallBuiltin { builtin: BuiltinId::SetRate, args_start, .. } if linked => {
1619                    note(HostFn::MoneySetRate, &mut used);
1620                    match plan.kinds.get((args_start + 1) as usize) {
1621                        Some(Kind::Int) => note(HostFn::RationalFromI64, &mut used),
1622                        Some(Kind::Decimal) => note(HostFn::DecimalToRational, &mut used),
1623                        _ => {}
1624                    }
1625                }
1626                Op::CallBuiltin { builtin: BuiltinId::SetRates, .. } if linked => {
1627                    note(HostFn::MoneySetRatesInt, &mut used);
1628                    note(HostFn::MoneySetRatesRational, &mut used);
1629                    note(HostFn::MoneySetRatesDecimal, &mut used);
1630                }
1631                // `wireBytes(value)` — marshal the value via the REAL codec (by the arg's kind).
1632                Op::CallBuiltin { builtin: BuiltinId::WireBytes, args_start, .. } if linked => {
1633                    if let Some(h) = wire_bytes_host_fn(plan.kinds.get(args_start as usize)) {
1634                        note(h, &mut used);
1635                    }
1636                }
1637                // `readWireProgram()` reads a host frame then DECODES it to a dynamic value; `run_accepted`
1638                // sandbox-evals a wire-received shipped function through the acceptance contract.
1639                Op::CallBuiltin { builtin: BuiltinId::ReadWireProgram, .. } if linked => {
1640                    note(HostFn::ReadWireFrame, &mut used);
1641                    note(HostFn::ReadWireProgramRt, &mut used);
1642                    // `readWireProgram` bump-allocs a receive buffer via `emit_alloc`, which in LINKER mode
1643                    // calls `logos_rt_alloc` — it MUST be imported, else `emit_alloc` falls back to the
1644                    // `__heap_ptr` global (undeclared in a linked module → an invalid global relocation).
1645                    note(HostFn::RtAlloc, &mut used);
1646                }
1647                Op::CallBuiltin { builtin: BuiltinId::RunAccepted, .. } if linked => note(HostFn::RunAccepted, &mut used),
1648                Op::CallBuiltin {
1649                    builtin:
1650                        BuiltinId::YearOf | BuiltinId::MonthOf | BuiltinId::DayOf | BuiltinId::WeekdayOf | BuiltinId::HourOf
1651                        | BuiltinId::MinuteOf | BuiltinId::SecondOf | BuiltinId::WeekOf | BuiltinId::QuarterOf,
1652                    args_start,
1653                    ..
1654                } => {
1655                    // A `Date` argument uses the day-based `temporal_component_date`; a `Moment` uses the
1656                    // nanos-based `temporal_component`. (An unknown-kind arg in dead code notes nothing.)
1657                    if plan.kinds.get(args_start as usize) == Some(Kind::Date) {
1658                        note(HostFn::TemporalComponentDate, &mut used);
1659                    } else {
1660                        note(HostFn::TemporalComponent, &mut used);
1661                    }
1662                }
1663                // `format(x)` stringifies its argument with the same host formatter a Concat operand uses.
1664                Op::CallBuiltin { builtin: BuiltinId::Format, args_start, arg_count, .. } if arg_count > 0 => {
1665                    note_operand_fmt(plan, args_start, &mut used);
1666                }
1667                // A `Concat` — or a Text-typed `+`/`+=` (string concatenation) — stringifies its
1668                // operands; a non-Text operand needs its host formatter.
1669                Op::Concat { lhs, rhs, .. } => {
1670                    note_operand_fmt(plan, lhs, &mut used);
1671                    note_operand_fmt(plan, rhs, &mut used);
1672                }
1673                Op::Add { dst, lhs, rhs } if plan.kinds.get(dst as usize) == Some(Kind::Text) => {
1674                    note_operand_fmt(plan, lhs, &mut used);
1675                    note_operand_fmt(plan, rhs, &mut used);
1676                }
1677                Op::AddAssign { dst, src } if plan.kinds.get(dst as usize) == Some(Kind::Text) => {
1678                    note_operand_fmt(plan, dst, &mut used);
1679                    note_operand_fmt(plan, src, &mut used);
1680                }
1681                // A formatted piece: a `.N` precision spec (`"{x:.9}"`) uses the float-precision host; an
1682                // alignment/width spec (`"{x:>6}"`) stringifies the value (its own formatter) then pads
1683                // via `fmt_align_into`. The lowering re-derives the exact spec; here we just ensure every
1684                // host it can reach is imported.
1685                Op::FormatValue { src, spec, .. } => {
1686                    let spec_s = match spec {
1687                        u32::MAX => None,
1688                        idx => match program.constants.get(idx as usize) {
1689                            Some(Constant::Text(s)) => Some(s.as_str()),
1690                            _ => None,
1691                        },
1692                    };
1693                    if matches!(spec_s, Some(s) if s.starts_with('.')) {
1694                        note(HostFn::FmtF64PrecInto, &mut used);
1695                    } else {
1696                        note(HostFn::FmtAlignInto, &mut used);
1697                        note_fmt_kind(plan.kinds.get(src as usize), &mut used);
1698                    }
1699                }
1700                _ => {}
1701            }
1702        }
1703    }
1704    // LINKER MODE with an emitter heap or an iterator stack imports the runtime allocator, to seed each
1705    // one's slab at the `main` prologue.
1706    if linked && (uses_heap || uses_iter) {
1707        note(HostFn::RtAlloc, &mut used);
1708    }
1709    // Re-sort into the canonical HOST_FNS order so indices are deterministic.
1710    let imports: Vec<HostFn> = HOST_FNS.iter().copied().filter(|h| used.contains(h)).collect();
1711    let host_index = |h: HostFn| -> Option<u32> { imports.iter().position(|x| *x == h).map(|i| i as u32) };
1712    let num_imports = imports.len() as u32;
1713    let main_index = num_imports;
1714    let fn_base = num_imports + 1; // wasm index of program.functions[0]
1715
1716    // ---- 3. Type table: host functions, then Main, then each function ----
1717    let mut types = TypeTable::default();
1718    let host_type: Vec<u32> = imports.iter().map(|h| types.intern(h.params(), h.results())).collect();
1719    let main_type = types.intern(vec![], result_valtypes(main.result));
1720    let mut fn_types: Vec<u32> = Vec::with_capacity(fns.len());
1721    let mut fn_param_valtypes: Vec<Vec<u8>> = Vec::with_capacity(fns.len());
1722    for p in &fns {
1723        let params: Vec<u8> = (0..p.num_params).map(|r| p.kinds.valtype(r as usize)).collect();
1724        fn_types.push(types.intern(params.clone(), result_valtypes(p.result)));
1725        fn_param_valtypes.push(params);
1726    }
1727
1728    // ---- 4. Emit each function body, with the call/host context resolved ----
1729    let heap_global = num_globals as u32; // `__heap_ptr` follows the user globals
1730    let iter_global = heap_global + u32::from(uses_heap); // `__iter_sp` follows `__heap_ptr`
1731    let fn_results: Vec<Option<Kind>> = fns.iter().map(|p| p.result).collect();
1732    // `capture_kinds` (computed in the planning pass, global + local) is the same source the closure
1733    // body signatures were seeded from, so `MakeClosure`'s store / `CallValue`'s load / the signature
1734    // all agree on each capture's valtype.
1735    let rt_alloc = if linked && uses_heap { host_index(HostFn::RtAlloc) } else { None };
1736    let ctx = Ctx { constants: &program.constants, host_index: &host_index, fn_base, heap_global, iter_global, fn_type: &fn_types, fn_param_valtypes: &fn_param_valtypes, fn_results: &fn_results, functions: &program.functions, capture_kinds: &capture_kinds, enum_types: &program.enum_types, struct_types: &program.struct_types, policies, interner, linked, rt_alloc };
1737    let mut main_body = emit_body(&main, &ctx)?;
1738    // LINKER MODE iterator-stack prologue: the emitter heap draws each block straight from the runtime
1739    // allocator (see [`emit_alloc`]), so it is UNBOUNDED and needs no slab. The ITERATOR STACK, though, is
1740    // a contiguous DOWN-growing region, so it's seeded from the TOP of one runtime `logos_rt_alloc` SLAB
1741    // (which `dlmalloc` owns → no collision). Spliced right after `main`'s local declarations
1742    // (`encode_locals` is the body's exact prefix), so it runs before any iteration: `__iter_sp` inits to
1743    // 0 in the global section, then this sets it to `slab_base + SLAB` = the slab top.
1744    if linked && uses_iter {
1745        const SLAB: i32 = (HEAP_PAGES * 65536) as i32;
1746        let rt_alloc = host_index(HostFn::RtAlloc).ok_or(WasmLowerError::Unsupported("logos_rt_alloc not imported"))?;
1747        let locals_len = encode_locals(&main).len();
1748        let mut prologue = Vec::new();
1749        i32_const(&mut prologue, SLAB);
1750        prologue.push(0x10); // call logos_rt_alloc(SLAB)
1751        leb_u32(&mut prologue, rt_alloc);
1752        i32_const(&mut prologue, SLAB);
1753        prologue.push(0x6A); // i32.add → slab base + SLAB = the slab TOP
1754        prologue.push(0x24); // global.set __iter_sp (= slab top; the iterator stack grows down)
1755        leb_u32(&mut prologue, iter_global);
1756        main_body.splice(locals_len..locals_len, prologue);
1757    }
1758    let mut fn_bodies = Vec::with_capacity(fns.len());
1759    for p in &fns {
1760        fn_bodies.push(emit_body(p, &ctx)?);
1761    }
1762
1763    // ---- 5. Assemble the module ----
1764    let mut module = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
1765    section(&mut module, 1, &types.encode());
1766
1767    // Import section (id 2): each host sink as a function import in `env`. In LINKER mode the program
1768    // also imports the ONE shared linear memory — `rust-lld` creates the output memory and resolves this
1769    // import (and the base runtime object's own `env.__linear_memory`) to it. A memory import occupies no
1770    // function-index slot, so the host functions keep their `host_index` indices regardless.
1771    let mut imp = Vec::new();
1772    leb_u32(&mut imp, imports.len() as u32 + u32::from(linked));
1773    for (i, h) in imports.iter().enumerate() {
1774        encode_name(&mut imp, "env");
1775        encode_name(&mut imp, h.field());
1776        imp.push(0x00); // import kind: function
1777        leb_u32(&mut imp, host_type[i]);
1778    }
1779    if linked {
1780        encode_name(&mut imp, "env");
1781        encode_name(&mut imp, "__linear_memory");
1782        imp.push(0x02); // import kind: memory
1783        imp.push(0x00); // limits: min only
1784        leb_u32(&mut imp, 0); // min 0 pages — lld unions this with the runtime's need + grows on demand
1785    }
1786    section(&mut module, 2, &imp);
1787
1788    // Function section (id 3): Main + each user function (type indices).
1789    let mut func = Vec::new();
1790    leb_u32(&mut func, 1 + fns.len() as u32);
1791    leb_u32(&mut func, main_type);
1792    for t in &fn_types {
1793        leb_u32(&mut func, *t);
1794    }
1795    section(&mut module, 3, &func);
1796
1797    // Table section (id 4): one funcref table holding every user function, so a closure's
1798    // `CallValue` can `call_indirect` it by index. (Emitted only when the program has closures AND is
1799    // self-contained — linker mode direct-calls closures, so it needs no table.)
1800    if has_closures && !linked {
1801        let mut table = Vec::new();
1802        leb_u32(&mut table, 1); // one table
1803        table.push(0x70); // elemtype: funcref
1804        table.push(0x00); // limits: min only
1805        leb_u32(&mut table, fns.len() as u32); // min = number of functions
1806        section(&mut module, 4, &table);
1807    }
1808
1809    // Memory section (id 5): one linear memory for the heap value model, exported as "memory". The
1810    // bump allocator never frees, so a build-then-scan program (a 1000-element array, an n-character
1811    // string built by `+`, a search that cuts a one-char Text per index) needs real headroom — one
1812    // page (64 KiB) overflows on any non-tiny input. `HEAP_PAGES` * 64 KiB is the address space; the
1813    // iterator stack grows down from its top, the heap up from 16, so they meet only at exhaustion.
1814    // LINKER MODE does NOT define a memory — it IMPORTS the shared one (above) and the bump allocator
1815    // draws from a runtime slab, so the runtime's `dlmalloc` owns the address space.
1816    if uses_heap && !linked {
1817        let mut mem = Vec::new();
1818        leb_u32(&mut mem, 1); // one memory
1819        mem.push(0x00); // limits: min only
1820        leb_u32(&mut mem, HEAP_PAGES); // min HEAP_PAGES pages
1821        section(&mut module, 5, &mem);
1822    }
1823
1824    // Global section (id 6): one mutable global per promoted Main binding (zero-initialized),
1825    // plus the bump-allocator pointer `__heap_ptr` when the program uses the heap.
1826    if num_globals > 0 || uses_heap || uses_iter {
1827        let mut glob = Vec::new();
1828        leb_u32(&mut glob, num_globals as u32 + u32::from(uses_heap) + u32::from(uses_iter));
1829        for gk in &global_kinds {
1830            let vt = gk.map(Kind::wasm_valtype).unwrap_or(I64);
1831            glob.push(vt);
1832            glob.push(0x01); // mutable
1833            // The zero-initializer MUST match the global's valtype — an `i32` (handle-kind) global
1834            // with an `i64.const 0` init is invalid wasm. `i32` covers any heap handle (struct, enum,
1835            // seq, map, …) a promoted Main binding may hold.
1836            if vt == F64 {
1837                glob.push(0x44); // f64.const 0
1838                glob.extend_from_slice(&0f64.to_le_bytes());
1839            } else if vt == I32 {
1840                i32_const(&mut glob, 0);
1841            } else {
1842                glob.push(0x42); // i64.const 0
1843                leb_i64(&mut glob, 0);
1844            }
1845            glob.push(0x0B); // end of init expr
1846        }
1847        if uses_heap {
1848            // `__heap_ptr`: mutable i32. Self-contained: init 16 (the low 16 bytes stay reserved/null).
1849            // LINKER MODE: init 0 — the `main` prologue seeds it from a runtime `logos_rt_alloc` SLAB, so
1850            // the bump region lives inside memory the runtime's `dlmalloc` owns (no collision).
1851            glob.push(I32);
1852            glob.push(0x01); // mutable
1853            i32_const(&mut glob, if linked { 0 } else { 16 });
1854            glob.push(0x0B);
1855        }
1856        if uses_iter {
1857            // `__iter_sp`: mutable i32. Self-contained: init to the memory top; each `IterPrepare`
1858            // decrements it by a 12-byte frame, so it grows down toward the up-growing heap. LINKER MODE:
1859            // init 0 — the `main` prologue seeds it from the TOP of a runtime `logos_rt_alloc` SLAB.
1860            glob.push(I32);
1861            glob.push(0x01); // mutable
1862            i32_const(&mut glob, if linked { 0 } else { (HEAP_PAGES * 65536) as i32 });
1863            glob.push(0x0B);
1864        }
1865        section(&mut module, 6, &glob);
1866    }
1867
1868    // Export section (id 7): the synthesized top-level body as `main`, plus `memory` if this module
1869    // DEFINES one. In linker mode the memory is imported (the linker re-exports it), so we don't.
1870    let export_memory = uses_heap && !linked;
1871    let mut export = Vec::new();
1872    leb_u32(&mut export, 1 + u32::from(export_memory));
1873    encode_name(&mut export, "main");
1874    export.push(0x00); // export kind: function
1875    leb_u32(&mut export, main_index);
1876    if export_memory {
1877        encode_name(&mut export, "memory");
1878        export.push(0x02); // export kind: memory
1879        leb_u32(&mut export, 0); // memory index 0
1880    }
1881    section(&mut module, 7, &export);
1882
1883    // Element section (id 9): an active segment filling table slot `i` with function `i`'s funcref
1884    // (wasm index `fn_base + i`), so a closure storing function index `i` `call_indirect`s table[i].
1885    // Self-contained only — linker mode direct-calls closures (no table to fill).
1886    if has_closures && !linked {
1887        let mut elem = Vec::new();
1888        leb_u32(&mut elem, 1); // one segment
1889        leb_u32(&mut elem, 0); // flags 0: active, table 0, MVP funcref vec
1890        elem.push(0x41); // i32.const 0 (offset)
1891        leb_i32(&mut elem, 0);
1892        elem.push(0x0B); // end
1893        leb_u32(&mut elem, fns.len() as u32);
1894        for i in 0..fns.len() as u32 {
1895            leb_u32(&mut elem, fn_base + i);
1896        }
1897        section(&mut module, 9, &elem);
1898    }
1899
1900    // Code section (id 10): Main, then each function (each prefixed by its byte length).
1901    let mut code = Vec::new();
1902    leb_u32(&mut code, 1 + fn_bodies.len() as u32);
1903    for entry in std::iter::once(&main_body).chain(fn_bodies.iter()) {
1904        leb_u32(&mut code, entry.len() as u32);
1905        code.extend_from_slice(entry);
1906    }
1907    section(&mut module, 10, &code);
1908
1909    Ok(module)
1910}
1911
1912/// The declared result kind of `program.functions[fi]` (`None` for void / undeclared) — used
1913/// to type `Op::Call` results before the callee's own body is inferred.
1914fn declared_result(program: &CompiledProgram, fi: usize) -> Option<Kind> {
1915    program.functions.get(fi).and_then(|f| f.ret_kind).map(Kind::from_slot)
1916}
1917
1918/// How the flat `code` stream partitions into wasm functions. Regular (`## To`) functions are
1919/// appended after Main as a contiguous, entry-sorted tail. A *closure body* (the target of a
1920/// `MakeClosure`) is an INLINE lambda: it is emitted in the middle of its enclosing region, jumped
1921/// over by a `Jump` at `entry_pc - 1` (the "jover") whose target is the body's end. So Main is no
1922/// longer a clean prefix — it is `[0, main_end)` with the inline closure bodies excised.
1923struct CodeLayout {
1924    /// Main's region end (exclusive): the first regular-function entry, or `code.len()`.
1925    main_end: usize,
1926    /// Per-function `[start, end)` in the code stream.
1927    func_region: Vec<(usize, usize)>,
1928    /// Every closure body's `[start, end)` — the holes to excise from an enclosing region.
1929    closure_ranges: Vec<(usize, usize)>,
1930    /// `is_closure[fi]` — whether function `fi` is a closure body (a `MakeClosure` target).
1931    is_closure: Vec<bool>,
1932}
1933
1934/// Resolve [`CodeLayout`] from the program: classify each function (a `MakeClosure` target is a
1935/// closure body), find each closure body's `[entry, jover_target)` range, and place the regular
1936/// functions as the contiguous tail after `main_end`.
1937fn code_layout(program: &CompiledProgram) -> R<CodeLayout> {
1938    let code_len = program.code.len();
1939    let nf = program.functions.len();
1940    let mut is_closure = vec![false; nf];
1941    for op in &program.code {
1942        if let Op::MakeClosure { func, .. } = *op {
1943            if (func as usize) < nf {
1944                is_closure[func as usize] = true;
1945            }
1946        }
1947    }
1948    let mut func_region = vec![(0usize, 0usize); nf];
1949    let mut closure_ranges = Vec::new();
1950    for (fi, f) in program.functions.iter().enumerate() {
1951        if is_closure[fi] {
1952            let entry = f.entry_pc;
1953            let end = match (entry >= 1).then(|| program.code.get(entry - 1)).flatten() {
1954                Some(Op::Jump { target }) if *target > entry && *target <= code_len => *target,
1955                _ => return Err(WasmLowerError::Unsupported("closure body without a jump-over")),
1956            };
1957            func_region[fi] = (entry, end);
1958            closure_ranges.push((entry, end));
1959        }
1960    }
1961    // Regular functions: the entry-sorted tail. Each ends at the next regular entry (closures
1962    // interleaved inside it become holes), the last at `code_len`.
1963    let mut regular: Vec<usize> = (0..nf).filter(|&fi| !is_closure[fi]).collect();
1964    regular.sort_by_key(|&fi| program.functions[fi].entry_pc);
1965    let main_end = regular.first().map(|&fi| program.functions[fi].entry_pc).unwrap_or(code_len);
1966    for w in 0..regular.len() {
1967        let fi = regular[w];
1968        let start = program.functions[fi].entry_pc;
1969        let end = regular.get(w + 1).map(|&g| program.functions[g].entry_pc).unwrap_or(code_len);
1970        func_region[fi] = (start, end);
1971    }
1972    Ok(CodeLayout { main_end, func_region, closure_ranges, is_closure })
1973}
1974
1975/// The maximal closure ranges contained in `[start, end)` (the DIRECT inline-closure children of a
1976/// region) — a closure nested inside another closure is excised with its parent, not here.
1977fn child_holes(closure_ranges: &[(usize, usize)], start: usize, end: usize) -> Vec<(usize, usize)> {
1978    closure_ranges
1979        .iter()
1980        .copied()
1981        .filter(|&(e, t)| {
1982            e >= start
1983                && t <= end
1984                && (e, t) != (start, end)
1985                && !closure_ranges
1986                    .iter()
1987                    .any(|&(e2, t2)| (e2, t2) != (e, t) && e2 <= e && t <= t2 && e2 >= start && t2 <= end)
1988        })
1989        .collect()
1990}
1991
1992/// Extract one wasm function's op slice from `code[start..end)`: drop the `holes` (inline closure
1993/// bodies, each a separate wasm function) and rebase every jump-like target to the kept ops'
1994/// 0-based indices. A target that escapes the region or lands inside a hole is a hard error.
1995fn extract_region(code: &[Op], start: usize, end: usize, holes: &[(usize, usize)]) -> R<Vec<Op>> {
1996    let in_hole = |pc: usize| holes.iter().any(|&(s, e)| pc >= s && pc < e);
1997    let mut new_index = vec![usize::MAX; end];
1998    let mut kept = Vec::new();
1999    for pc in start..end {
2000        if !in_hole(pc) {
2001            new_index[pc] = kept.len();
2002            kept.push(pc);
2003        }
2004    }
2005    let rebase = |t: usize| -> R<usize> {
2006        if t >= start && t < end && new_index[t] != usize::MAX {
2007            Ok(new_index[t])
2008        } else {
2009            Err(WasmLowerError::Unsupported("jump target escapes the function"))
2010        }
2011    };
2012    let mut ops = Vec::with_capacity(kept.len());
2013    for &pc in &kept {
2014        ops.push(match code[pc] {
2015            Op::Jump { target } => Op::Jump { target: rebase(target)? },
2016            Op::JumpIfFalse { cond, target } => Op::JumpIfFalse { cond, target: rebase(target)? },
2017            Op::JumpIfTrue { cond, target } => Op::JumpIfTrue { cond, target: rebase(target)? },
2018            Op::IterNext { dst, exit } => Op::IterNext { dst, exit: rebase(exit)? },
2019            other => other,
2020        });
2021    }
2022    Ok(ops)
2023}
2024
2025/// Plan the synthesized top-level `main`: `code[0 .. main_end)` with inline closure bodies excised,
2026/// the Main register frame, no parameters, void result (it ends in `Halt`).
2027fn plan_main(
2028    program: &CompiledProgram,
2029    layout: &CodeLayout,
2030    ret_of: &dyn Fn(usize) -> Option<Kind>,
2031    global_of: &dyn Fn(u16) -> Option<Kind>,
2032    closure_ret: &dyn Fn(usize) -> Option<Kind>,
2033    ret_layout: &dyn Fn(u16) -> Option<FieldLayout>,
2034    fn_return_closure: &dyn Fn(u16) -> Option<u16>,
2035    linked: bool,
2036) -> R<Plan> {
2037    let holes = child_holes(&layout.closure_ranges, 0, layout.main_end);
2038    let ops = extract_region(&program.code, 0, layout.main_end, &holes)?;
2039    let num_regs = program.register_count as u32;
2040    // Split any register the VM reused across disjoint live ranges of conflicting wasm types into one
2041    // local per range (Main has no parameters to pin). Identity unless a real conflict exists.
2042    let (ops, num_regs) = regsplit::split_registers(&ops, num_regs, 0, &program.functions);
2043    let fn_return_types = fn_return_types(program);
2044    let (kinds, _pc_reach, reachable_blocks) =
2045        infer_with_reachability(&ops, &program.constants, &program.struct_types, &program.enum_types, &fn_return_types, num_regs as usize, &[], ret_of, global_of, closure_ret, ret_layout, fn_return_closure, &[], &[], linked, &program.functions)?;
2046    let structs = kind::struct_layout(&ops, &program.constants, &program.struct_types, &program.enum_types, &fn_return_types, ret_layout, fn_return_closure, &[], &[]);
2047    let cow_inserts = cow_struct_inserts(&ops, num_regs, &program.functions);
2048    let reg_shape = complete_reg_shape(&structs, &kinds, program);
2049    Ok(Plan { ops, kinds, num_params: 0, num_regs, result: None, reachable_blocks, structs, cow_inserts, reg_shape, return_closure: None, stub: false })
2050}
2051
2052/// Each function's declared RETURN type, indexed by function index — for typing a caller's inline use
2053/// of a returned composite (`item k of f()`, `Inspect f()`).
2054fn fn_return_types(program: &CompiledProgram) -> Vec<Option<BoundaryType>> {
2055    program.functions.iter().map(|f| f.return_type.clone()).collect()
2056}
2057
2058/// Complete a plan's per-register access SHAPE: `struct_layout`'s `reg_shape` (parameter + cross-region
2059/// + locally-built struct, all kind-free) EXTENDED with locally-built map / enum / heterogeneous-tuple
2060/// shapes — which need the inferred register kinds, unavailable inside `struct_layout`. A param shape
2061/// already present is never overridden. A captured local-built composite then resolves like any other.
2062fn complete_reg_shape(
2063    structs: &kind::StructLayout,
2064    kinds: &KindTable,
2065    program: &CompiledProgram,
2066) -> std::collections::HashMap<u16, ParamShape> {
2067    let mut out = structs.reg_shape.clone();
2068    for (reg, value_reg) in &structs.map_set_value {
2069        if !out.contains_key(reg) {
2070            if let Some(vk) = kinds.get(*value_reg as usize) {
2071                out.insert(*reg, ParamShape::Map(vk));
2072            }
2073        }
2074    }
2075    for (reg, elems) in &structs.tuple_layouts {
2076        if !out.contains_key(reg) {
2077            if let Some(ks) = elems.iter().map(|e| kinds.get(*e as usize)).collect::<Option<Vec<Kind>>>() {
2078                // Heterogeneous only — a homogeneous tuple lays out as a self-describing `Seq`.
2079                if !ks.windows(2).all(|w| w[0] == w[1]) {
2080                    out.insert(*reg, ParamShape::Tuple(ks));
2081                }
2082            }
2083        }
2084    }
2085    for (reg, name) in &structs.ind_type_of {
2086        if !out.contains_key(reg) {
2087            if let Some(variants) = kind::resolve_enum_variants(name, &program.enum_types) {
2088                out.insert(*reg, ParamShape::Enum(variants));
2089            }
2090        }
2091    }
2092    out
2093}
2094
2095/// Each closure's captured VALUE kinds (by function index, then capture index), read from where the
2096/// closure is BUILT (`MakeClosure`) — in Main or a non-closure function (the `region_plans`, by
2097/// function index; closures are `None` there). A captured GLOBAL resolves via `global_of`; a captured
2098/// LOCAL from the enclosing region's inferred register kind. A capture not reachable here (e.g. one
2099/// built inside another closure body, which this pre-pass doesn't plan) stays `None` → seeded `Int`,
2100/// so a composite such capture rejects soundly rather than miscompiling.
2101fn extract_capture_kinds(
2102    program: &CompiledProgram,
2103    main_p1: &Plan,
2104    region_plans: &[Option<Plan>],
2105    global_of: &dyn Fn(u16) -> Option<Kind>,
2106    global_closure_of: &dyn Fn(u16) -> Option<u16>,
2107) -> (Vec<Vec<Option<Kind>>>, Vec<Vec<Option<ParamShape>>>, Vec<Vec<Option<u16>>>) {
2108    let mut kinds: Vec<Vec<Option<Kind>>> = program
2109        .functions
2110        .iter()
2111        .map(|f| f.captures.iter().map(|(_, g)| g.and_then(|gi| global_of(gi))).collect())
2112        .collect();
2113    let mut shapes: Vec<Vec<Option<ParamShape>>> =
2114        program.functions.iter().map(|f| vec![None; f.captures.len()]).collect();
2115    // Per closure fi, per capture k: the body function index of a captured CLOSURE value (so calling
2116    // the captured closure resolves — closure composition like `(n) -> add1(add1(n))`). A captured
2117    // GLOBAL closure (a Main-`Let` closure promoted to a global) is resolved here; a captured LOCAL
2118    // closure is filled from the build-site `closure_of` in the loop below.
2119    let mut closures: Vec<Vec<Option<u16>>> = program
2120        .functions
2121        .iter()
2122        .map(|f| f.captures.iter().map(|(_, g)| g.and_then(|gi| global_closure_of(gi))).collect())
2123        .collect();
2124    for plan in std::iter::once(main_p1).chain(region_plans.iter().flatten()) {
2125        for op in &plan.ops {
2126            if let Op::MakeClosure { func, locals_start, .. } = *op {
2127                let fi = func as usize;
2128                let mut local_k: u16 = 0;
2129                for (k, (_sym, global)) in program.functions[fi].captures.iter().enumerate() {
2130                    if global.is_none() {
2131                        let reg = locals_start + local_k;
2132                        local_k += 1;
2133                        if let Some(kind) = plan.kinds.get(reg as usize) {
2134                            kinds[fi][k] = Some(kind);
2135                        }
2136                        // A captured composite's SHAPE comes from the enclosing region's unified
2137                        // `reg_shape` at the capture register — covering a function PARAMETER (struct/
2138                        // map/enum/het-tuple) and a locally-built struct, so the closure body resolves
2139                        // `capturedstruct's field` / `item k of capturedmap` / `Inspect capturedenum`
2140                        // exactly as a composite parameter does.
2141                        if let Some(shape) = plan.reg_shape.get(&reg) {
2142                            shapes[fi][k] = Some(shape.clone());
2143                        }
2144                        // A captured CLOSURE value: its statically-traced body function index at the
2145                        // build site, so the closure body can `call_indirect` the captured closure.
2146                        if let Some(&c) = plan.structs.closure_of.get(&reg) {
2147                            closures[fi][k] = Some(c);
2148                        }
2149                    }
2150                }
2151            }
2152        }
2153    }
2154    (kinds, shapes, closures)
2155}
2156
2157/// Plan `program.functions[fi]`: extract its region (regular functions are the appended tail; a
2158/// closure body is its inline `[entry, jover_target)` slice), excise any inline closures nested
2159/// inside it, rebase jump targets to 0-based, seed parameter kinds, infer register kinds, and
2160/// resolve its result kind (the declared `ret_kind`, else inferred from its `Return` operands).
2161/// A *capturing* closure (one that reads an enclosing local) needs the capture ABI and is deferred.
2162#[allow(clippy::too_many_arguments)]
2163fn plan_function(
2164    program: &CompiledProgram,
2165    fi: usize,
2166    layout: &CodeLayout,
2167    ret_of: &dyn Fn(usize) -> Option<Kind>,
2168    global_of: &dyn Fn(u16) -> Option<Kind>,
2169    closure_ret: &dyn Fn(usize) -> Option<Kind>,
2170    ret_layout: &dyn Fn(u16) -> Option<FieldLayout>,
2171    fn_return_closure: &dyn Fn(u16) -> Option<u16>,
2172    param_origin: &dyn Fn(usize, usize) -> Option<u16>,
2173    capture_kinds: &[Vec<Option<Kind>>],
2174    capture_shapes: &[Vec<Option<ParamShape>>],
2175    capture_closures: &[Vec<Option<u16>>],
2176    strict: bool,
2177    linked: bool,
2178) -> R<Plan> {
2179    let f = &program.functions[fi];
2180    let (start, end) = layout.func_region[fi];
2181    if start >= end || end > program.code.len() {
2182        return Err(WasmLowerError::Unsupported("malformed function bounds"));
2183    }
2184    let holes = child_holes(&layout.closure_ranges, start, end);
2185    let ops = extract_region(&program.code, start, end, &holes)?;
2186
2187    // A capturing closure body receives, after its real parameters, each capture's VALUE and a
2188    // present-FLAG (regs `p..p+cap_n` and `p+cap_n..p+2·cap_n`; see `CallValue`'s frame setup).
2189    // Real params + flags are Int by the closure contract; each capture VALUE is typed by its source
2190    // kind (`capture_kinds[fi]`, computed once from the `MakeClosure` site — a captured global's kind
2191    // or a captured local's enclosing-region kind), so closing over a composite (a `Seq`/`Map`/struct/
2192    // … handle) works whether it's global or function-local. `CallValue` passes them; `MakeClosure`
2193    // fills the closure object they are loaded from. A non-capturing function uses its declared kinds.
2194    let cap_n = f.captures.len() as u32;
2195    let num_params = f.param_count as u32 + 2 * cap_n;
2196    let mut seeds: Vec<Option<Kind>> = if cap_n > 0 {
2197        let mut s = vec![Some(Kind::Int); num_params as usize];
2198        // Real params take their DECLARED kind (a `(n: Float)` capturing closure must seed `n` as f64,
2199        // not the all-Int flag/entry-guard default) — the capturing-closure analog of the non-capturing
2200        // `function_param_seeds`. Capture VALUES then take their source kind; the present-FLAGS stay Int.
2201        for i in 0..f.param_count as usize {
2202            if let Some(Some(bt)) = f.param_types.get(i) {
2203                if let Some(k) = kind::boundary_to_kind(bt) {
2204                    s[i] = Some(k);
2205                }
2206            }
2207        }
2208        let caps = capture_kinds.get(fi).map(|v| v.as_slice()).unwrap_or(&[]);
2209        for k in 0..f.captures.len() {
2210            if let Some(kind) = caps.get(k).copied().flatten() {
2211                s[f.param_count as usize + k] = Some(kind);
2212            }
2213        }
2214        s
2215    } else {
2216        kind::function_param_seeds(f)
2217    };
2218    let num_regs = f.register_count as u32;
2219    // Split any register reused across disjoint live ranges of conflicting wasm types; parameters
2220    // (and capture/range members) are pinned so the function signature and range operands are
2221    // untouched. Identity unless a real conflict exists.
2222    let (ops, num_regs) = regsplit::split_registers(&ops, num_regs, num_params, &program.functions);
2223    // Struct PARAMETERS' field layouts, resolved from the bytecode's `struct_types` — so `p's field`
2224    // resolves inside a `f(p: Point)` body (parameters are pinned by the splitter, so their registers
2225    // are unchanged). Only non-capturing functions have declared parameter types.
2226    let mut param_layouts = if cap_n == 0 { param_seeds(f, program) } else { Vec::new() };
2227    // A capture of a composite GLOBAL is typed with that global's SHAPE (the capture analog of a
2228    // composite parameter), so `item k of capturedmap` / `capturedstruct's field` / `Inspect
2229    // capturedenum` resolve in the closure body. The capture VALUE register (after the real params)
2230    // already has the right Kind seed; this adds its layout/value-kind/variant resolution.
2231    let local_shapes = capture_shapes.get(fi).map(|v| v.as_slice()).unwrap_or(&[]);
2232    for (k, (_sym, global)) in f.captures.iter().enumerate() {
2233        let shape = if let Some(gidx) = global {
2234            // A captured GLOBAL composite is typed by the global's resolved type (`global_types`).
2235            program
2236                .global_types
2237                .get(*gidx as usize)
2238                .and_then(|t| t.as_ref())
2239                .and_then(|bt| boundary_to_param_shape(bt, program))
2240        } else {
2241            // A captured LOCAL composite is typed by its shape read from the enclosing region's plan.
2242            local_shapes.get(k).cloned().flatten()
2243        };
2244        if let Some(shape) = shape {
2245            param_layouts.push((f.param_count + k as u16, shape));
2246        }
2247    }
2248    // A closure PARAMETER whose single statically-known origin a whole-program pass resolved: its
2249    // register (= the param index, pinned by the splitter) is pre-bound to that closure body, so
2250    // `f(args)` inside this function resolves its callee like a returned/local closure (closures as
2251    // arguments). Only the real params (`0..param_count`) — captures/flags are never closure args.
2252    let mut param_closures: Vec<(u16, u16)> =
2253        (0..f.param_count).filter_map(|i| param_origin(fi, i as usize).map(|c| (i, c))).collect();
2254    // A captured CLOSURE value (register `param_count + k`) is bound to its traced body function, so
2255    // calling the captured closure resolves its callee like a local one — closure composition.
2256    if let Some(caps) = capture_closures.get(fi) {
2257        for (k, c) in caps.iter().enumerate() {
2258            if let Some(c) = c {
2259                param_closures.push((f.param_count + k as u16, *c));
2260            }
2261        }
2262    }
2263    // A closure PARAMETER carries an i32 handle, not the i64 its `Closure` declared type would otherwise
2264    // seed (there is no `BoundaryType::Closure`). The origin pass already proved this param IS a closure,
2265    // so type it `Kind::Closure` — the closure-handle argument then matches and lowers/loads as i32.
2266    for &(reg, _) in &param_closures {
2267        if let Some(slot) = seeds.get_mut(reg as usize) {
2268            *slot = Some(Kind::Closure);
2269        }
2270    }
2271    let fn_rt = fn_return_types(program);
2272    let (kinds, pc_reach, reachable_blocks) =
2273        infer_with_reachability(&ops, &program.constants, &program.struct_types, &program.enum_types, &fn_rt, num_regs as usize, &seeds, ret_of, global_of, closure_ret, ret_layout, fn_return_closure, &param_layouts, &param_closures, linked, &program.functions)?;
2274
2275    // Result kind: the declared `ret_kind` (a scalar `SlotKind`) when present, EXCEPT a non-scalar
2276    // (handle) return — a `-> Point` etc. — cannot be a `SlotKind`, so its `ret_kind` defaults to
2277    // Int; the inferred kind (from the `Return` operands) then wins on a value-type disagreement.
2278    let inferred = infer_result(&ops, &kinds, &pc_reach, strict)?;
2279    let result = match (f.ret_kind.map(Kind::from_slot), inferred) {
2280        (Some(d), Some(i)) if d.wasm_valtype() != i.wasm_valtype() => Some(i),
2281        (Some(d), _) => Some(d),
2282        (None, i) => i,
2283    };
2284    let structs = kind::struct_layout(&ops, &program.constants, &program.struct_types, &program.enum_types, &fn_rt, ret_layout, fn_return_closure, &param_layouts, &param_closures);
2285    let cow_inserts = cow_struct_inserts(&ops, num_regs, &program.functions);
2286    let reg_shape = complete_reg_shape(&structs, &kinds, program);
2287    let return_closure = infer_return_closure(&ops, &kinds, &structs, &pc_reach);
2288    Ok(Plan { ops, kinds, num_params, num_regs, result, reachable_blocks, structs, cow_inserts, reg_shape, return_closure, stub: false })
2289}
2290
2291/// WHICH closure (body function index) this function returns, if any — the closure-valued analog of
2292/// [`fn_return_struct_layout`]. Scans REACHABLE closure-typed `Return`s (a nested closure's inline
2293/// body is unreachable here, so its `Return` is ignored); yields `Some(c)` iff they all agree on the
2294/// same statically-traced origin `c` (via `closure_of`, which is `Move`-aliased). `None` if it
2295/// returns no closure or more than one — a caller then cannot resolve a `CallValue` on the result.
2296fn infer_return_closure(ops: &[Op], kinds: &KindTable, structs: &kind::StructLayout, pc_reach: &[bool]) -> Option<u16> {
2297    let mut found: Option<u16> = None;
2298    for (pc, op) in ops.iter().enumerate() {
2299        if !pc_reach.get(pc).copied().unwrap_or(true) {
2300            continue;
2301        }
2302        if let Op::Return { src } = *op {
2303            if kinds.get(src as usize) != Some(Kind::Closure) {
2304                continue;
2305            }
2306            match structs.closure_of.get(&src) {
2307                Some(&c) if found.is_none() => found = Some(c),
2308                Some(&c) if found == Some(c) => {}
2309                _ => return None,
2310            }
2311        }
2312    }
2313    found
2314}
2315
2316/// WHICH closure each function PARAMETER is always passed — the param-side, whole-program analog of
2317/// [`infer_return_closure`]. For every `Call`/`CallValue` in every planned region, attribute each
2318/// argument register's statically-traced closure origin (the caller's `closure_of`) to the callee's
2319/// matching parameter index. A parameter is bound to closure `c` iff EVERY observed call passes that
2320/// same `c` and nothing else; any disagreement (a different closure, or an untraced argument) yields
2321/// `None`, so a genuinely polymorphic / opaque closure parameter stays soundly rejected. Result
2322/// indexed `[fi][param_idx]`. Lets `f(args)` resolve its callee when `f` is a closure ARGUMENT.
2323/// `plans` is every region to scan (Main + the planned functions).
2324fn compute_param_origins(program: &CompiledProgram, plans: &[&Plan]) -> Vec<Vec<Option<u16>>> {
2325    use std::collections::{HashMap, HashSet};
2326    let mut obs: HashMap<(usize, usize), HashSet<Option<u16>>> = HashMap::new();
2327    for plan in plans {
2328        for (pc, op) in plan.ops.iter().enumerate() {
2329            let (func, args_start, arg_count) = match *op {
2330                Op::Call { func, args_start, arg_count, .. } => (Some(func as usize), args_start, arg_count),
2331                Op::CallValue { args_start, arg_count, .. } => {
2332                    (plan.structs.callee_func.get(pc).copied().flatten().map(|f| f as usize), args_start, arg_count)
2333                }
2334                _ => continue,
2335            };
2336            let Some(func) = func else { continue };
2337            for i in 0..arg_count {
2338                let origin = plan.structs.closure_of.get(&(args_start + i)).copied();
2339                obs.entry((func, i as usize)).or_default().insert(origin);
2340            }
2341        }
2342    }
2343    (0..program.functions.len())
2344        .map(|fi| {
2345            let pc = program.functions[fi].param_count as usize;
2346            (0..pc)
2347                .map(|i| match obs.get(&(fi, i)) {
2348                    Some(set) if set.len() == 1 => set.iter().next().copied().flatten(),
2349                    _ => None,
2350                })
2351                .collect()
2352        })
2353        .collect()
2354}
2355
2356/// Resolve a composite [`BoundaryType`] to its access-resolution [`ParamShape`] — the shared bridge
2357/// used to seed a parameter (its declared type), a closure capture (the captured global's type), or
2358/// any other cross-region composite. `None` for a scalar / self-describing type (no shape needed) or
2359/// one whose layout/kinds don't resolve (the access then stays soundly rejected).
2360fn boundary_to_param_shape(bt: &BoundaryType, program: &CompiledProgram) -> Option<ParamShape> {
2361    match bt {
2362        BoundaryType::Struct(name) => kind::resolve_named_layout(name, &program.struct_types, &program.constants)
2363            .map(ParamShape::Struct),
2364        BoundaryType::Map(_, value) => kind::boundary_to_kind(value).map(ParamShape::Map),
2365        BoundaryType::Enum(name) => kind::resolve_enum_variants(name, &program.enum_types).map(ParamShape::Enum),
2366        BoundaryType::Tuple(elems) => {
2367            elems.iter().map(kind::boundary_to_kind).collect::<Option<Vec<Kind>>>().map(ParamShape::Tuple)
2368        }
2369        _ => None,
2370    }
2371}
2372
2373/// Each non-scalar PARAMETER's access-resolution shape (parameter register `i` → [`ParamShape`]),
2374/// from the bytecode's `param_types`/`struct_types`. Seeded into `struct_layout` so `param's field`
2375/// (struct) and `item k of m` (map) resolve like a cross-region access. A struct/map whose layout
2376/// or value kind is unresolvable is simply omitted, leaving the access soundly rejected.
2377fn param_seeds(f: &CompiledFunction, program: &CompiledProgram) -> Vec<(u16, ParamShape)> {
2378    let mut out = Vec::new();
2379    for (i, pt) in f.param_types.iter().enumerate() {
2380        if let Some(shape) = pt.as_ref().and_then(|bt| boundary_to_param_shape(bt, program)) {
2381            out.push((i as u16, shape));
2382        }
2383    }
2384    out
2385}
2386
2387/// Infer a function's result kind from its `Return` operands (they must agree); `None` (void) if it
2388/// only `ReturnNothing`s or never returns a value. With `strict == false` (the first planning pass),
2389/// a `Return` of a not-yet-known kind is DEFERRED (skipped), not an error — it may resolve in the
2390/// second pass once callee return layouts are threaded (a function returning the result of a
2391/// struct-returning function). The strict (final) pass errors on a genuinely unknown return.
2392fn infer_result(ops: &[Op], kinds: &KindTable, pc_reach: &[bool], strict: bool) -> R<Option<Kind>> {
2393    let mut result = None;
2394    for (pc, op) in ops.iter().enumerate() {
2395        // A nested closure's body is emitted INLINE in this region (its parent jumps over it; it is
2396        // reached only through the closure call into its own separate function). That inline copy is
2397        // unreachable HERE, so its `Return` must not contribute to — or poison — this region's result.
2398        if !pc_reach.get(pc).copied().unwrap_or(true) {
2399            continue;
2400        }
2401        if let Op::Return { src } = *op {
2402            let k = match kinds.get(src as usize) {
2403                Some(k) => k,
2404                None if !strict => continue,
2405                None => return Err(WasmLowerError::Unsupported("return of an unknown-kind value")),
2406            };
2407            match result {
2408                None => result = Some(k),
2409                Some(prev) if prev == k => {}
2410                // A `SeqAny` return (an empty `new Seq of T` return path, or a recursive-call result
2411                // not yet refined during the fixpoint) refines to a concrete sibling sequence — the
2412                // same rule `unify_strict` applies to register kinds. So a `mergeSort` returning
2413                // `arr` (SeqInt) on the base case and `result` (SeqAny until the recursion resolves)
2414                // on the recursive case agrees on SeqInt instead of falsely reading as mixed.
2415                Some(Kind::SeqAny) if k.is_seq() => result = Some(k),
2416                Some(prev) if prev.is_seq() && k == Kind::SeqAny => {}
2417                Some(_) => return Err(WasmLowerError::Unsupported("function returns mixed kinds")),
2418            }
2419        }
2420    }
2421    Ok(result)
2422}
2423
2424/// A function's RETURN struct layout, resolved to `(field name-const-idx, field kind)` — `None`
2425/// unless the function returns a `Struct` whose every field kind is known. Lets a caller resolve
2426/// `f(…)'s field` cross-region (the returned struct is built in this — the callee's — region, so
2427/// its field-defining registers are not visible to the caller; the resolved kinds bridge that).
2428fn fn_return_struct_layout(plan: &Plan) -> Option<FieldLayout> {
2429    for op in &plan.ops {
2430        if let Op::Return { src } = *op {
2431            if plan.kinds.get(src as usize) == Some(Kind::Struct) {
2432                let layout = plan.structs.reg_layout.get(&src)?;
2433                // The field KINDS bridge the region boundary; a struct-typed field is additionally
2434                // named from the callee's `struct_name_of` (the field value's `NewStruct` type), so a
2435                // caller re-seeds it and resolves `f(…)'s inner's v` — symmetric with the param path.
2436                // A map/enum field of a RETURNED struct stays `FieldNested::None` (its value kind /
2437                // variant layout isn't recoverable from the plan's nameless `Kind`) — a narrower gap.
2438                let resolved: FieldLayout = layout
2439                    .iter()
2440                    .filter_map(|&(f, vr)| {
2441                        plan.kinds.get(vr as usize).map(|k| {
2442                            let nested = match (k, plan.structs.struct_name_of.get(&vr)) {
2443                                (Kind::Struct, Some(name)) => FieldNested::Struct(name.clone()),
2444                                _ => FieldNested::None,
2445                            };
2446                            (f, k, nested)
2447                        })
2448                    })
2449                    .collect();
2450                return (resolved.len() == layout.len()).then_some(resolved);
2451            }
2452        }
2453    }
2454    None
2455}
2456
2457/// The wasm result-type vector for a function/result kind (`[]` for void, else `[valtype]`).
2458fn result_valtypes(result: Option<Kind>) -> Vec<u8> {
2459    match result {
2460        Some(k) => vec![k.wasm_valtype()],
2461        None => vec![],
2462    }
2463}
2464
2465/// Per-function emission context: the constant pool, how to resolve a host sink's import
2466/// index, and the wasm index of `program.functions[0]` (so `Op::Call { func }` →
2467/// `call (fn_base + func)`).
2468struct Ctx<'a> {
2469    constants: &'a [Constant],
2470    host_index: &'a dyn Fn(HostFn) -> Option<u32>,
2471    fn_base: u32,
2472    /// The wasm global index of the bump-allocator pointer `__heap_ptr` (the heap value model's
2473    /// linear-memory cursor); it follows the user globals.
2474    heap_global: u32,
2475    /// The wasm global index of the iterator-stack pointer `__iter_sp` (it follows `__heap_ptr`),
2476    /// for `Repeat` iteration. Grows *down* from the top of memory toward the up-growing heap;
2477    /// each live `Repeat` owns one 12-byte frame `[snapshot_ptr:i32][cursor:i32][len:i32]`.
2478    iter_global: u32,
2479    /// The wasm type index of each `program.functions[i]` (by function index), so a `CallValue`'s
2480    /// `call_indirect` can name the callee closure body's signature (= that function's own type).
2481    fn_type: &'a [u32],
2482    /// Each `program.functions[i]`'s ACTUAL emitted parameter value types (from its plan), so a direct
2483    /// `Call` matches the real signature — including a `Closure` parameter that lowers to i32, which the
2484    /// declared-type `function_param_seeds` cannot see (there is no `BoundaryType::Closure`).
2485    fn_param_valtypes: &'a [Vec<u8>],
2486    /// Each `program.functions[i]`'s result kind (`None` = void) — tells a `CallValue` whether the
2487    /// `call_indirect` leaves a value to bind into its destination.
2488    fn_results: &'a [Option<Kind>],
2489    /// The program's functions — `MakeClosure`/`CallValue` read the callee's `captures` (count and
2490    /// each capture's local-register-vs-global source) to fill / pass the closure object.
2491    functions: &'a [crate::vm::instruction::CompiledFunction],
2492    /// Each function's capture VALUE kinds (by function index, then capture index) — a captured
2493    /// global's kind, else `None` (a local capture, stored/loaded as Int). `MakeClosure` stores and
2494    /// `CallValue` loads each capture slot at this kind's width, matching the body's seeded signature.
2495    capture_kinds: &'a [Vec<Option<Kind>>],
2496    /// The program's enum type definitions (variant names + payload field types) — a whole-enum
2497    /// `Show` reads the variant set to emit its tag→name dispatch (`lower_show_enum`).
2498    enum_types: &'a [EnumTypeDef],
2499    /// The program's struct type definitions (field names in declaration order) — a `CheckPolicy`
2500    /// resolves a policy condition's `field` to its slot index via the subject's declared fields.
2501    struct_types: &'a [StructTypeDef],
2502    /// The `## Policy` registry (predicate + capability conditions) — `CheckPolicy` compiles the
2503    /// resolved condition inline (field access + `text_eq` + and/or), trapping when it is false.
2504    policies: &'a PolicyRegistry,
2505    /// The interner, to resolve a policy condition's `Symbol` field/value/predicate names to the
2506    /// strings the struct-field lookup + `text_eq` literal need.
2507    interner: &'a Interner,
2508    /// Linker mode: an integer `Op::Pow` lowers to the `logos_rt_bigint_*` runtime (a `Text` handle)
2509    /// rather than the self-contained i64 exponentiation-by-squaring. Set only by
2510    /// [`assemble_program_linked`]; the standalone emitter leaves it `false`.
2511    linked: bool,
2512    /// Linker mode + emitter heap: the import index of `logos_rt_alloc`. When `Some`, [`emit_alloc`] draws
2513    /// each block straight from the runtime allocator (`dlmalloc`, growing linear memory on demand) rather
2514    /// than a fixed bump slab — so the emitter heap is UNBOUNDED. `None` = the standalone bump path.
2515    rt_alloc: Option<u32>,
2516}
2517
2518/// Whether an op terminated its basic block (a branch / return), so the block emitter stops.
2519#[derive(PartialEq, Eq)]
2520enum Flow {
2521    Straight,
2522    Terminated,
2523}
2524
2525/// Emit one function's complete Code-section entry (its locals declaration followed by the
2526/// dispatch-loop body), from a [`Plan`].
2527fn emit_body(plan: &Plan, _ctx: &Ctx) -> R<Vec<u8>> {
2528    // A dropped (unreachable-from-Main) function: no locals, a lone `unreachable` trap. It is never
2529    // called (that's why it was dropped), so its `() -> ()` type is inert — this just lets the module
2530    // link when an imported stdlib carries functions the AOT can't lower but the program never uses.
2531    if plan.stub {
2532        return Ok(vec![0x00 /* 0 local groups */, 0x00 /* unreachable */, 0x0B /* end */]);
2533    }
2534    let ctx = _ctx;
2535    let blocks = Blocks::new(&plan.ops).ok_or(WasmLowerError::Unsupported("jump target escapes the function"))?;
2536    let pc_local = plan.num_regs;
2537
2538    let mut blocks_code: Vec<Vec<u8>> = Vec::with_capacity(blocks.num_blocks());
2539    for k in 0..blocks.num_blocks() {
2540        // A statically-dead block (e.g. the monomorphized-out branch of an `and`/`or` runtime
2541        // type-dispatch) is never branched to — emit a lone `unreachable` rather than lower its
2542        // ops, which may reference registers whose kinds were (correctly) never inferred.
2543        if !plan.reachable_blocks.get(k).copied().unwrap_or(true) {
2544            blocks_code.push(vec![0x00]); // unreachable
2545            continue;
2546        }
2547        let mut code = Vec::new();
2548        let mut terminated = false;
2549        for pc in blocks.start(k)..blocks.end(k) {
2550            if lower_op(pc, plan, ctx, &blocks, k, &mut code)? == Flow::Terminated {
2551                terminated = true;
2552                break;
2553            }
2554        }
2555        if !terminated {
2556            let end = blocks.end(k);
2557            if end >= plan.ops.len() {
2558                // Fell off the end of the function: void returns, a typed function cannot.
2559                match plan.result {
2560                    None => code.push(0x0F),   // return
2561                    Some(_) => code.push(0x00), // unreachable
2562                }
2563            } else {
2564                let next = blocks.block_of(end);
2565                code.push(0x41); // i32.const next-block
2566                leb_u32(&mut code, next as u32);
2567                local_set(&mut code, pc_local);
2568                code.push(0x0C); // br $loop
2569                leb_u32(&mut code, blocks.br_loop(k));
2570            }
2571        }
2572        blocks_code.push(code);
2573    }
2574
2575    let mut body = assemble_dispatch_loop(pc_local, &blocks_code);
2576    if plan.result.is_some() {
2577        body.push(0x00); // unreachable: a typed function always returns inside a block
2578    }
2579    body.push(0x0B); // end function
2580
2581    let mut entry = encode_locals(plan);
2582    entry.extend(body);
2583    Ok(entry)
2584}
2585
2586/// Lower one op into the current block's code, returning whether it terminated the block.
2587fn lower_op(pc: usize, plan: &Plan, ctx: &Ctx, blocks: &Blocks, k: usize, code: &mut Vec<u8>) -> R<Flow> {
2588    let kinds = &plan.kinds;
2589    match plan.ops[pc] {
2590        Op::LoadConst { dst, idx } => {
2591            // A register the inference promoted to Float (an Int-initialized accumulator, `Let sum
2592            // be 0`) holds an `f64` local, so an Int/Bool literal flowing into it materializes as
2593            // `f64.const` — the exact promotion the VM performs at the first float op.
2594            let dst_float = kinds.get(dst as usize) == Some(Kind::Float);
2595            match ctx.constants.get(idx as usize).ok_or(WasmLowerError::Unsupported("constant index out of range"))? {
2596                Constant::Int(v) if dst_float => {
2597                    code.push(0x44); // f64.const
2598                    code.extend_from_slice(&(*v as f64).to_le_bytes());
2599                }
2600                Constant::Int(v) => {
2601                    code.push(0x42); // i64.const
2602                    leb_i64(code, *v);
2603                }
2604                Constant::Bool(b) if dst_float => {
2605                    code.push(0x44); // f64.const
2606                    code.extend_from_slice(&(if *b { 1.0f64 } else { 0.0f64 }).to_le_bytes());
2607                }
2608                Constant::Bool(b) => {
2609                    code.push(0x42); // i64.const (truthy-Int boolean: 0/1)
2610                    leb_i64(code, i64::from(*b));
2611                }
2612                Constant::Char(c) => {
2613                    code.push(0x42); // i64.const — the Unicode code point (`char as u32`)
2614                    leb_i64(code, i64::from(*c as u32));
2615                }
2616                Constant::Nothing => {
2617                    // Reads as the Int `0` (an i64 local): the read-as-zero CRDT-counter/dead-`Zone`-name
2618                    // default, and the `nothing` literal of `x is equal to nothing` (the compare special-
2619                    // cases the `Optional` side against this `0`). A genuine `Optional` comes from
2620                    // `ChanTryRecv`, not this const.
2621                    code.push(0x42); // i64.const 0
2622                    leb_i64(code, 0);
2623                }
2624                Constant::Float(f) => {
2625                    code.push(0x44); // f64.const
2626                    code.extend_from_slice(&f.to_le_bytes());
2627                }
2628                // Temporal scalars ride a single i64 (tick/day count). A `select`'s `After N seconds`
2629                // ticks register is the only one the corpus loads — it is dead in the deterministic
2630                // model (`SelectArmTimeout` is a no-op; the timeout fires whenever no recv arm is ready)
2631                // but still materializes so its local declares.
2632                Constant::Duration(v) | Constant::Moment(v) | Constant::Time(v) => {
2633                    code.push(0x42); // i64.const
2634                    leb_i64(code, *v);
2635                }
2636                Constant::Date(v) => {
2637                    // A `Date` is days-since-epoch in an `i32` local (`Kind::Date` → i32, matching
2638                    // `LoadToday`'s i32 host result), so the literal materializes as `i32.const` — an
2639                    // `i64.const` would mismatch the declared local valtype. `i32_const` signs the LEB,
2640                    // so pre-1970 dates (negative days) encode correctly.
2641                    i32_const(code, *v);
2642                }
2643                // A `Span` packs its two i32 fields into one i64 local: `months` in the high word, `days`
2644                // in the low word (the calendar-arith lowering unpacks them for the runtime call).
2645                Constant::Span { months, days } => {
2646                    let packed = ((*months as i64) << 32) | ((*days as u32) as i64);
2647                    code.push(0x42); // i64.const
2648                    leb_i64(code, packed);
2649                }
2650                Constant::Text(s) => {
2651                    // Build a fresh Text object in linear memory; it leaves the handle on the
2652                    // stack for the `local_set` below.
2653                    lower_text_literal(code, ctx, plan.num_regs, s.as_bytes());
2654                }
2655                _ => return Err(WasmLowerError::Unsupported("non-scalar constant")),
2656            }
2657            local_set(code, dst as u32);
2658            Ok(Flow::Straight)
2659        }
2660        // unobservable (its source is dead). The RETAIN below is a harmless over-retain
2661        // (COW resolves it) — correctness identical to `Move`.
2662        // `EnsureOwned` is the interpreter's call-site COW barrier; the WASM AOT
2663        // enforces value semantics by copy-on-write at each element write, so the
2664        // barrier is a no-op here (correctness is preserved without it).
2665        Op::EnsureOwned { .. } => Ok(Flow::Straight),
2666        Op::Move { dst, src } => {
2667            // Moving an `i64` source into a Float-promoted destination converts (the VM's Int→Float
2668            // promotion); same-kind moves are a plain copy.
2669            if kinds.get(dst as usize) == Some(Kind::Float) && kinds.get(src as usize) != Some(Kind::Float) {
2670                push_as_f64(code, src, kinds.get(src as usize))?;
2671            } else {
2672                local_get(code, src as u32);
2673            }
2674            local_set(code, dst as u32);
2675            // Aliasing a mutable heap object (`Let cs be c's items`) gains a second holder — RETAIN so
2676            // a later mutation of either copy-on-writes instead of clobbering the other.
2677            if cow_clonable(kinds.get(dst as usize)) {
2678                emit_retain(code, dst);
2679            }
2680            Ok(Flow::Straight)
2681        }
2682        // A Text-typed `+` (`add_join` resolved the result to Text) is string concatenation — route to
2683        // `lower_concat`, which stringifies both operands and joins them. Everything else is numeric.
2684        // `+ - *` on a BigInt result (linker mode) are exact big-integer arithmetic via the runtime; an
2685        // `Int` operand promotes to a BigInt. Checked FIRST so a `BigInt` `+` never falls to the numeric
2686        // (or string-concat) path. The result kind is `BigInt` iff `op_kind_effect` saw a BigInt operand.
2687        Op::Add { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::BigInt) => {
2688            lower_bigint_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::BigintAdd)
2689        }
2690        Op::Sub { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::BigInt) => {
2691            lower_bigint_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::BigintSub)
2692        }
2693        Op::Mul { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::BigInt) => {
2694            lower_bigint_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::BigintMul)
2695        }
2696        Op::Div { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::BigInt) => {
2697            lower_bigint_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::BigintDiv)
2698        }
2699        Op::Mod { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::BigInt) => {
2700            lower_bigint_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::BigintMod)
2701        }
2702        Op::Add { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Complex) => {
2703            lower_complex_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::ComplexAdd)
2704        }
2705        Op::Sub { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Complex) => {
2706            lower_complex_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::ComplexSub)
2707        }
2708        Op::Mul { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Complex) => {
2709            lower_complex_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::ComplexMul)
2710        }
2711        Op::Add { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Modular) => {
2712            lower_modular_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::ModularAdd)
2713        }
2714        Op::Sub { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Modular) => {
2715            lower_modular_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::ModularSub)
2716        }
2717        Op::Mul { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Modular) => {
2718            lower_modular_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::ModularMul)
2719        }
2720        Op::Add { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Decimal) => {
2721            lower_decimal_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::DecimalAdd)
2722        }
2723        Op::Sub { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Decimal) => {
2724            lower_decimal_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::DecimalSub)
2725        }
2726        Op::Mul { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Decimal) => {
2727            lower_decimal_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::DecimalMul)
2728        }
2729        Op::Add { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Money) => {
2730            lower_money_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::MoneyAdd)
2731        }
2732        Op::Sub { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Money) => {
2733            lower_money_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::MoneySub)
2734        }
2735        Op::Add { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Quantity) => {
2736            lower_quantity_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::QuantityAdd)
2737        }
2738        Op::Sub { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Quantity) => {
2739            lower_quantity_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::QuantitySub)
2740        }
2741        Op::Mul { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Quantity) => {
2742            lower_quantity_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::QuantityMul)
2743        }
2744        Op::Div { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Quantity) => {
2745            lower_quantity_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::QuantityDiv)
2746        }
2747        // `Moment/Date + Span` (calendar arithmetic, commutes): the Span operand carries the months/days,
2748        // the other is the base. Guarded on the SPAN operand (not dst) so it beats the `Moment + Duration`
2749        // i64-add arm above (whose result is also Moment).
2750        Op::Add { dst, lhs, rhs }
2751            if ctx.linked && (kinds.get(lhs as usize) == Some(Kind::Span) || kinds.get(rhs as usize) == Some(Kind::Span)) =>
2752        {
2753            let (base, span) = if kinds.get(lhs as usize) == Some(Kind::Span) { (rhs, lhs) } else { (lhs, rhs) };
2754            let is_date = kinds.get(base as usize) == Some(Kind::Date);
2755            return lower_span_add(code, ctx, plan.num_regs, dst, base, span, is_date, false);
2756        }
2757        // `Moment/Date - Span` steps the calendar backward — the span (rhs) is negated.
2758        Op::Sub { dst, lhs, rhs } if ctx.linked && kinds.get(rhs as usize) == Some(Kind::Span) => {
2759            let is_date = kinds.get(lhs as usize) == Some(Kind::Date);
2760            return lower_span_add(code, ctx, plan.num_regs, dst, lhs, rhs, is_date, true);
2761        }
2762        // Lane-wise `Lanes + Lanes` (the SHA-1 block fold `st + abcdSave`) — a `logos_rt_lanes4_add` call.
2763        Op::Add { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Lanes) => {
2764            let idx = (ctx.host_index)(HostFn::Lanes4Add).ok_or(WasmLowerError::Unsupported("lanes4_add not imported"))?;
2765            local_get(code, lhs as u32);
2766            local_get(code, rhs as u32);
2767            code.push(0x10);
2768            leb_u32(code, idx);
2769            local_set(code, dst as u32);
2770            Ok(Flow::Straight)
2771        }
2772        Op::Add { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Rational) => {
2773            lower_rational_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::RationalAdd)
2774        }
2775        Op::Sub { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Rational) => {
2776            lower_rational_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::RationalSub)
2777        }
2778        Op::Mul { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Rational) => {
2779            lower_rational_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::RationalMul)
2780        }
2781        Op::Div { dst, lhs, rhs } if ctx.linked && kinds.get(dst as usize) == Some(Kind::Rational) => {
2782            lower_rational_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::RationalDiv)
2783        }
2784        Op::Add { dst, lhs, rhs } => {
2785            if kinds.get(dst as usize) == Some(Kind::Text) {
2786                lower_concat(code, kinds, ctx, plan.num_regs, dst, lhs, rhs)?;
2787                Ok(Flow::Straight)
2788            } else {
2789                lower_arith(code, kinds, dst, lhs, rhs, ArithOp::Add)
2790            }
2791        }
2792        Op::Sub { dst, lhs, rhs } => lower_arith(code, kinds, dst, lhs, rhs, ArithOp::Sub),
2793        Op::Mul { dst, lhs, rhs } => lower_arith(code, kinds, dst, lhs, rhs, ArithOp::Mul),
2794        Op::Div { dst, lhs, rhs } => lower_arith(code, kinds, dst, lhs, rhs, ArithOp::Div),
2795        Op::FloorDiv { dst, lhs, rhs } => lower_floordiv_regs(code, kinds, plan.num_regs, dst, lhs, rhs),
2796        Op::Mod { dst, lhs, rhs } => lower_arith(code, kinds, dst, lhs, rhs, ArithOp::Mod),
2797        // `a ** b` — exponentiation. Float-result cases use the host `pow_ff`/`pow_fi`; `Int^Int`
2798        // is the in-module overflow-trapping squaring loop (a `2**100`-style overflow traps, the
2799        // BigInt-promoting frontier, matching `Mul`). A negative Int exponent traps (VM → Float).
2800        Op::Pow { dst, lhs, rhs } => lower_pow_regs(code, kinds, ctx, plan.num_regs, dst, lhs, rhs),
2801        // `lhs / 2^k` (the Oracle's power-of-two division form) = a plain signed division by the
2802        // constant `1<<k` — `i64.div_s` matches the VM's `lhs.div(1<<k)` (truncate toward zero, and
2803        // `2^k > 0` so no INT_MIN/-1 trap). Emitted only for an Oracle-proven `Int` lhs (i64).
2804        Op::DivPow2 { dst, lhs, k } => {
2805            local_get(code, lhs as u32);
2806            code.push(0x42); // i64.const 2^k
2807            leb_i64(code, 1i64 << k);
2808            code.push(0x7F); // i64.div_s
2809            local_set(code, dst as u32);
2810            Ok(Flow::Straight)
2811        }
2812        // `lhs / c` or `lhs % c` by the precomputed magic reciprocal — mirror the VM's `magic_eval`
2813        // (Granlund–Montgomery mul-high + shift) bit-for-bit.
2814        Op::MagicDivU { dst, lhs, magic, more, mul_back } => {
2815            lower_magic_div(code, plan.num_regs, dst, lhs, magic, more, mul_back);
2816            Ok(Flow::Straight)
2817        }
2818        // `a / b` in a `Rational` context → an exact reduced `Rational`. LINKER mode uses the
2819        // BigInt-backed runtime (`logos_rt_rational_div`, arbitrary precision, promoting Int/BigInt
2820        // operands to Rational first); self-contained mode uses the inline i64/i64 reduce.
2821        Op::ExactDiv { dst, lhs, rhs } if ctx.linked => {
2822            lower_rational_binop(code, kinds, ctx, dst, lhs, rhs, HostFn::RationalDiv)
2823        }
2824        Op::ExactDiv { dst, lhs, rhs } => {
2825            lower_exact_div(code, ctx, plan.num_regs, dst, lhs, rhs);
2826            Ok(Flow::Straight)
2827        }
2828        // Pure native-region metadata — the AOT's accesses are checked, so the bound guard is a no-op
2829        // (exactly as the VM, where `RegionBoundsGuard` is a `pc += 1`).
2830        Op::RegionBoundsGuard { .. } => Ok(Flow::Straight),
2831        Op::AddAssign { dst, src } => {
2832            if kinds.get(dst as usize) == Some(Kind::Text) {
2833                lower_concat(code, kinds, ctx, plan.num_regs, dst, dst, src)?;
2834                Ok(Flow::Straight)
2835            } else if ctx.linked && kinds.get(dst as usize) == Some(Kind::Lanes) {
2836                // `lanes += lanes` (the SHA-1 `Set e0 to e0 + m0`) — lane-wise add into `dst`.
2837                let idx = (ctx.host_index)(HostFn::Lanes4Add).ok_or(WasmLowerError::Unsupported("lanes4_add not imported"))?;
2838                local_get(code, dst as u32);
2839                local_get(code, src as u32);
2840                code.push(0x10);
2841                leb_u32(code, idx);
2842                local_set(code, dst as u32);
2843                Ok(Flow::Straight)
2844            } else {
2845                lower_arith(code, kinds, dst, dst, src, ArithOp::Add)
2846            }
2847        }
2848        // `^ & |` scalar lowering only — a Set operand (set algebra) has no register-scalar
2849        // form here yet, so it fails loud rather than i64-punning a handle.
2850        // Bitwise `xor`/`&`/`|` also close over the Word ring (`Word32` → `i32.*`, `Word64` → `i64.*`),
2851        // so crypto written with the `xor` keyword compiles alongside the `word_and`/`word_or` builtins.
2852        Op::BitXor { dst, lhs, rhs } => match kinds.get(lhs as usize) {
2853            Some(Kind::Int) | Some(Kind::Word64) => {
2854                arith(code, 0x85, dst, lhs, rhs); // i64.xor
2855                Ok(Flow::Straight)
2856            }
2857            Some(Kind::Word32) => {
2858                arith(code, 0x73, dst, lhs, rhs); // i32.xor
2859                Ok(Flow::Straight)
2860            }
2861            // Lane-wise `Lanes xor Lanes` (the SHA-1 message-schedule fold) via the runtime.
2862            Some(Kind::Lanes) if ctx.linked => {
2863                let idx = (ctx.host_index)(HostFn::Lanes4Xor).ok_or(WasmLowerError::Unsupported("lanes4_xor not imported"))?;
2864                local_get(code, lhs as u32);
2865                local_get(code, rhs as u32);
2866                code.push(0x10);
2867                leb_u32(code, idx);
2868                local_set(code, dst as u32);
2869                Ok(Flow::Straight)
2870            }
2871            _ => Err(WasmLowerError::Unsupported("`^` of a non-Int/Word value")),
2872        },
2873        Op::BitAnd { dst, lhs, rhs } => match kinds.get(lhs as usize) {
2874            Some(Kind::Int) | Some(Kind::Bool) | Some(Kind::Word64) => {
2875                arith(code, 0x83, dst, lhs, rhs); // i64.and
2876                Ok(Flow::Straight)
2877            }
2878            Some(Kind::Word32) => {
2879                arith(code, 0x71, dst, lhs, rhs); // i32.and
2880                Ok(Flow::Straight)
2881            }
2882            _ => Err(WasmLowerError::Unsupported("`&` of a non-Int/Bool/Word value")),
2883        },
2884        Op::BitOr { dst, lhs, rhs } => match kinds.get(lhs as usize) {
2885            Some(Kind::Int) | Some(Kind::Bool) | Some(Kind::Word64) => {
2886                arith(code, 0x84, dst, lhs, rhs); // i64.or
2887                Ok(Flow::Straight)
2888            }
2889            Some(Kind::Word32) => {
2890                arith(code, 0x72, dst, lhs, rhs); // i32.or
2891                Ok(Flow::Straight)
2892            }
2893            _ => Err(WasmLowerError::Unsupported("`|` of a non-Int/Bool/Word value")),
2894        },
2895        Op::Shl { dst, lhs, rhs } => {
2896            arith(code, 0x86, dst, lhs, rhs); // i64.shl
2897            Ok(Flow::Straight)
2898        }
2899        Op::Shr { dst, lhs, rhs } => {
2900            arith(code, 0x87, dst, lhs, rhs); // i64.shr_s
2901            Ok(Flow::Straight)
2902        }
2903        Op::Not { dst, src } => {
2904            // `not` is LOGICAL — truthiness in, Bool out (`~` lowers to `x ^ -1`
2905            // in the parser, never through here). Bool and Int share the zero
2906            // test: `x == 0` (i64.eqz → i32) widened back to i64 0/1.
2907            match kinds.get(src as usize) {
2908                Some(Kind::Bool) | Some(Kind::Int) => {
2909                    local_get(code, src as u32);
2910                    code.push(0x50); // i64.eqz
2911                    code.push(0xAD); // i64.extend_i32_u
2912                    local_set(code, dst as u32);
2913                }
2914                _ => return Err(WasmLowerError::Unsupported("Not of a non-Int/Bool value")),
2915            }
2916            Ok(Flow::Straight)
2917        }
2918        Op::Lt { dst, lhs, rhs } => lower_compare(code, kinds, dst, lhs, rhs, Cmp::Lt),
2919        Op::Gt { dst, lhs, rhs } => lower_compare(code, kinds, dst, lhs, rhs, Cmp::Gt),
2920        Op::LtEq { dst, lhs, rhs } => lower_compare(code, kinds, dst, lhs, rhs, Cmp::Le),
2921        Op::GtEq { dst, lhs, rhs } => lower_compare(code, kinds, dst, lhs, rhs, Cmp::Ge),
2922        Op::ApproxEq { dst, lhs, rhs } => lower_approx_eq(code, kinds, dst, lhs, rhs),
2923        Op::Eq { dst, lhs, rhs } => {
2924            if kinds.get(lhs as usize) == Some(Kind::Text) && kinds.get(rhs as usize) == Some(Kind::Text) {
2925                lower_text_eq(code, plan.num_regs, dst, lhs, rhs, false);
2926                Ok(Flow::Straight)
2927            } else if ctx.linked && kinds.get(lhs as usize) == Some(Kind::Uuid) && kinds.get(rhs as usize) == Some(Kind::Uuid) {
2928                lower_uuid_eq(code, ctx, dst, lhs, rhs, false)
2929            } else {
2930                lower_compare(code, kinds, dst, lhs, rhs, Cmp::Eq)
2931            }
2932        }
2933        Op::NotEq { dst, lhs, rhs } => {
2934            if kinds.get(lhs as usize) == Some(Kind::Text) && kinds.get(rhs as usize) == Some(Kind::Text) {
2935                lower_text_eq(code, plan.num_regs, dst, lhs, rhs, true);
2936                Ok(Flow::Straight)
2937            } else if ctx.linked && kinds.get(lhs as usize) == Some(Kind::Uuid) && kinds.get(rhs as usize) == Some(Kind::Uuid) {
2938                lower_uuid_eq(code, ctx, dst, lhs, rhs, true)
2939            } else {
2940                lower_compare(code, kinds, dst, lhs, rhs, Cmp::Ne)
2941            }
2942        }
2943        Op::Jump { target } => {
2944            code.push(0x41);
2945            leb_u32(code, blocks.block_of(target) as u32);
2946            local_set(code, plan.num_regs);
2947            code.push(0x0C);
2948            leb_u32(code, blocks.br_loop(k));
2949            Ok(Flow::Terminated)
2950        }
2951        Op::JumpIfFalse { cond, target } => {
2952            emit_cond_jump(code, cond, true, blocks.block_of(target), blocks.block_of(pc + 1), plan.num_regs, blocks.br_loop(k));
2953            Ok(Flow::Terminated)
2954        }
2955        Op::JumpIfTrue { cond, target } => {
2956            emit_cond_jump(code, cond, false, blocks.block_of(target), blocks.block_of(pc + 1), plan.num_regs, blocks.br_loop(k));
2957            Ok(Flow::Terminated)
2958        }
2959        Op::GlobalGet { dst, idx } => {
2960            code.push(0x23); // global.get
2961            leb_u32(code, idx as u32);
2962            local_set(code, dst as u32);
2963            // Reading a global heap handle aliases the global's object — RETAIN so a mutation of the
2964            // read copy copy-on-writes instead of clobbering the global.
2965            if cow_clonable(kinds.get(dst as usize)) {
2966                emit_retain(code, dst);
2967            }
2968            Ok(Flow::Straight)
2969        }
2970        Op::LoadToday { dst } => {
2971            let idx = (ctx.host_index)(HostFn::Today).ok_or(WasmLowerError::Unsupported("today not imported"))?;
2972            code.push(0x10); // call today -> i32 (Date)
2973            leb_u32(code, idx);
2974            local_set(code, dst as u32);
2975            Ok(Flow::Straight)
2976        }
2977        // A new empty sequence: bump-allocate a 16-byte header `[len=0][cap=0][data_ptr=0]` and
2978        // hand back its (stable) pointer. Growth on push reallocs the data buffer, not the header,
2979        // so the register holding this handle never needs updating.
2980        // A new empty sequence or map — both are a zeroed 16-byte header `[len/num=0][cap=0]
2981        // [data_ptr=0]`; the element/entry shape differs only at use.
2982        Op::NewEmptyList { dst } | Op::NewEmptyListI32 { dst } | Op::NewEmptyMap { dst } | Op::NewEmptySet { dst } => {
2983            emit_empty_header(code, ctx, plan.num_regs, dst as u32);
2984            Ok(Flow::Straight)
2985        }
2986        // `length of seq` = the header's `len` field (an Int).
2987        Op::Length { dst, collection } => {
2988            local_get(code, collection as u32);
2989            i32_load(code, 0); // header len (i32)
2990            code.push(0xAD); // i64.extend_i32_u → Int
2991            local_set(code, dst as u32);
2992            Ok(Flow::Straight)
2993        }
2994        Op::ListPush { list, value } => {
2995            emit_cow(code, kinds, &plan.structs, ctx, plan.num_regs, list)?;
2996            lower_list_push(code, kinds, ctx, plan.num_regs, list, value)?;
2997            // Pushing a mutable heap VALUE stores it as an element (a second holder) — RETAIN so a
2998            // later mutation of the original copy-on-writes instead of mutating the stored element.
2999            if cow_clonable(kinds.get(value as usize)) {
3000                emit_retain(code, value);
3001            }
3002            Ok(Flow::Straight)
3003        }
3004        Op::ListPushField { obj, field, src } => {
3005            lower_list_push_field(code, plan, kinds, ctx, obj, field, src)?;
3006            if cow_clonable(kinds.get(src as usize)) {
3007                emit_retain(code, src);
3008            }
3009            Ok(Flow::Straight)
3010        }
3011        Op::ListPop { list, dst } => {
3012            emit_cow(code, kinds, &plan.structs, ctx, plan.num_regs, list)?;
3013            lower_list_pop(code, kinds, dst, list)?;
3014            // The popped element's slot stays physically in the (now-shorter) buffer, so `dst`
3015            // aliases it until a later push overwrites it — RETAIN a clonable handle so mutating
3016            // `dst` copy-on-writes (same discipline as an `Index` extract).
3017            if cow_clonable(kinds.get(dst as usize)) {
3018                emit_retain(code, dst);
3019            }
3020            Ok(Flow::Straight)
3021        }
3022        // `IndexUnchecked` (the Oracle-proven-in-bounds optimizer form) executes IDENTICALLY to `Index`
3023        // in the VM — the "unchecked" only drops the bounds branch inside a native region. The AOT keeps
3024        // its bounds check (a safe superset), so the two lower to the same code.
3025        Op::Index { dst, collection, index } | Op::IndexUnchecked { dst, collection, index } => {
3026            match kinds.get(collection as usize) {
3027                Some(Kind::Map) => lower_map_get(code, kinds, plan.num_regs, dst, collection, index)?,
3028                Some(Kind::Text) => lower_text_index(code, ctx, plan.num_regs, dst, collection, index)?,
3029                _ => lower_index(code, kinds, dst, collection, index)?,
3030            }
3031            // `Let row be item k of matrix` extracts a handle that aliases the element in the
3032            // container — RETAIN so mutating the extracted row copy-on-writes.
3033            if cow_clonable(kinds.get(dst as usize)) {
3034                emit_retain(code, dst);
3035            }
3036            Ok(Flow::Straight)
3037        }
3038        // `SetIndexUnchecked` is the Oracle-proven-in-bounds form of `SetIndex` (identical in the VM);
3039        // lowered the same (the AOT keeps its bounds check).
3040        Op::SetIndex { collection, index, value } | Op::SetIndexUnchecked { collection, index, value } => {
3041            emit_cow(code, kinds, &plan.structs, ctx, plan.num_regs, collection)?;
3042            if kinds.get(collection as usize) == Some(Kind::Map) {
3043                lower_map_insert(code, kinds, ctx, plan.num_regs, collection, index, value)?;
3044            } else {
3045                lower_set_index(code, kinds, collection, index, value)?;
3046            }
3047            // Storing a mutable heap VALUE as an element/map-value (a second holder) — RETAIN so a
3048            // later mutation of the original copy-on-writes instead of mutating the stored one.
3049            if cow_clonable(kinds.get(value as usize)) {
3050                emit_retain(code, value);
3051            }
3052            Ok(Flow::Straight)
3053        }
3054        Op::NewRange { dst, start, end } => {
3055            lower_new_range(code, ctx, plan.num_regs, dst, start, end);
3056            Ok(Flow::Straight)
3057        }
3058        // A list literal `[…]` and a homogeneous tuple `(…)` allocate the same buffer via
3059        // `lower_new_list`; a heterogeneous tuple (inferred `Kind::Tuple`) stores each element at
3060        // its own width via `lower_new_tuple_het`.
3061        Op::NewList { dst, start, count } => {
3062            lower_new_list(code, kinds, ctx, plan.num_regs, dst, start, count)?;
3063            Ok(Flow::Straight)
3064        }
3065        Op::NewTuple { dst, start, count } => {
3066            if kinds.get(dst as usize) == Some(Kind::Tuple) {
3067                lower_new_tuple_het(code, kinds, ctx, plan.num_regs, dst, start, count)?;
3068            } else {
3069                lower_new_list(code, kinds, ctx, plan.num_regs, dst, start, count)?;
3070            }
3071            Ok(Flow::Straight)
3072        }
3073        Op::DestructureTuple { src, start, count } => {
3074            lower_destructure_tuple(code, kinds, src, start, count)?;
3075            Ok(Flow::Straight)
3076        }
3077        Op::IterPrepare { iterable } => {
3078            lower_iter_prepare(code, kinds, ctx, plan.num_regs, iterable)?;
3079            Ok(Flow::Straight)
3080        }
3081        Op::IterNext { dst, exit } => {
3082            lower_iter_next(code, kinds, ctx, blocks, k, plan.num_regs, dst, exit, pc);
3083            Ok(Flow::Terminated)
3084        }
3085        Op::SetAdd { set, value } => {
3086            emit_cow(code, kinds, &plan.structs, ctx, plan.num_regs, set)?;
3087            lower_set_add(code, kinds, ctx, plan.num_regs, set, value)?;
3088            Ok(Flow::Straight)
3089        }
3090        Op::RemoveFrom { collection, value } => {
3091            emit_cow(code, kinds, &plan.structs, ctx, plan.num_regs, collection)?;
3092            lower_remove_from(code, kinds, plan.num_regs, collection, value)?;
3093            Ok(Flow::Straight)
3094        }
3095        Op::UnionOp { dst, lhs, rhs } => {
3096            lower_union(code, kinds, ctx, plan.num_regs, dst, lhs, rhs)?;
3097            Ok(Flow::Straight)
3098        }
3099        Op::IntersectOp { dst, lhs, rhs } => {
3100            lower_intersect(code, kinds, ctx, plan.num_regs, dst, lhs, rhs)?;
3101            Ok(Flow::Straight)
3102        }
3103        Op::Contains { dst, collection, value } => {
3104            if kinds.get(collection as usize) == Some(Kind::Map) {
3105                lower_map_contains(code, kinds, plan.num_regs, dst, collection, value)?;
3106            } else {
3107                lower_contains(code, kinds, plan.num_regs, dst, collection, value)?;
3108            }
3109            Ok(Flow::Straight)
3110        }
3111        Op::SliceOp { dst, collection, start, end } => {
3112            lower_slice(code, kinds, ctx, plan.num_regs, dst, collection, start, end)?;
3113            Ok(Flow::Straight)
3114        }
3115        Op::SeqConcat { dst, lhs, rhs } => {
3116            lower_seq_concat(code, kinds, ctx, plan.num_regs, dst, lhs, rhs)?;
3117            Ok(Flow::Straight)
3118        }
3119        Op::Concat { dst, lhs, rhs } => {
3120            lower_concat(code, kinds, ctx, plan.num_regs, dst, lhs, rhs)?;
3121            Ok(Flow::Straight)
3122        }
3123        Op::FormatValue { dst, src, spec, debug_prefix } => {
3124            lower_format_value(code, kinds, ctx, plan.num_regs, dst, src, spec, debug_prefix)?;
3125            Ok(Flow::Straight)
3126        }
3127        Op::DeepClone { dst, src } => {
3128            lower_deep_clone(code, kinds, &plan.structs, ctx, plan.num_regs, dst, src)?;
3129            Ok(Flow::Straight)
3130        }
3131        // `copy(x)` — the builtin form of a deep clone (an independent copy of a heap value; the value
3132        // itself for a scalar), the same lowering as `Op::DeepClone`.
3133        Op::CallBuiltin { dst, builtin: BuiltinId::Copy, args_start, .. } => {
3134            lower_deep_clone(code, kinds, &plan.structs, ctx, plan.num_regs, dst, args_start)?;
3135            Ok(Flow::Straight)
3136        }
3137        Op::NewStruct { dst, .. } => {
3138            let count = plan.structs.count.get(pc).copied().flatten().unwrap_or(0);
3139            lower_new_struct(code, ctx, plan.num_regs, count, dst);
3140            Ok(Flow::Straight)
3141        }
3142        Op::StructInsert { obj, value, .. } => {
3143            let slot = plan.structs.slot.get(pc).copied().flatten().ok_or(WasmLowerError::Unsupported("struct insert with no static slot"))?;
3144            let cow = plan.cow_inserts.get(pc).copied().unwrap_or(true);
3145            lower_struct_insert(code, kinds, ctx, plan.num_regs, slot, obj, value, cow)?;
3146            Ok(Flow::Straight)
3147        }
3148        Op::GetField { dst, obj, .. } => {
3149            let slot = plan.structs.slot.get(pc).copied().flatten().ok_or(WasmLowerError::Unsupported("field access with no static slot"))?;
3150            lower_get_field(code, kinds, slot, dst, obj)?;
3151            // `Let cs be c's items` aliases the field's mutable heap object — RETAIN so a mutation of
3152            // the extracted handle copy-on-writes rather than mutating the field in place.
3153            if cow_clonable(kinds.get(dst as usize)) {
3154                emit_retain(code, dst);
3155            }
3156            Ok(Flow::Straight)
3157        }
3158        Op::CheckPolicy { subject, predicate, is_capability, object, .. } => {
3159            lower_check_policy(code, plan, ctx, subject, predicate, is_capability, object)?;
3160            Ok(Flow::Straight)
3161        }
3162        Op::CrdtBump { obj, field, amount, negate } => {
3163            lower_crdt_bump(code, plan, ctx, obj, field, amount, negate)?;
3164            Ok(Flow::Straight)
3165        }
3166        Op::CrdtMerge { target, source } => {
3167            lower_crdt_merge(code, plan, target, source)?;
3168            Ok(Flow::Straight)
3169        }
3170        Op::NewCrdt { dst, kind } => {
3171            // A fresh CRDT collection is empty: an RGA/sequence (kind 1) gets a `[0][0][0]` header, a
3172            // divergent register (else) an empty `Text`. Single-replica, these ARE the underlying
3173            // collection (mutated via the in-place `CrdtAppend`/`CrdtResolve` ops). An OR-Set (0/3) is
3174            // REFUSED for now: `Add X to <obj>'s <set-field>` compiles to `SetAdd` on a `GetField`
3175            // result, which the value-semantics COW clones — so the add would not reach the shared
3176            // field. That needs field-collection mutation to bypass COW (the retain/release-placement
3177            // soundness obligation), so an OR-Set field `Shared` struct stays deferred, not miscompiled.
3178            match kind {
3179                0 | 1 | 3 => emit_empty_header(code, ctx, plan.num_regs, dst as u32),
3180                _ => {
3181                    lower_text_literal(code, ctx, plan.num_regs, b"");
3182                    local_set(code, dst as u32);
3183                }
3184            }
3185            Ok(Flow::Straight)
3186        }
3187        Op::CrdtResolve { obj, field, value } => {
3188            lower_crdt_resolve(code, plan, kinds, obj, field, value)?;
3189            Ok(Flow::Straight)
3190        }
3191        Op::CrdtAppend { seq, value } => {
3192            lower_crdt_append(code, plan, kinds, ctx, seq, value)?;
3193            Ok(Flow::Straight)
3194        }
3195        // ── DETERMINISTIC SINGLE-THREAD CONCURRENCY (matches the seeded cooperative scheduler for the
3196        //    non-blocking guide shapes; the corpus lock proves tw == VM(driven) == AOT). ──
3197        // A `Pipe`/channel is a FIFO queue: `new Pipe` → empty seq, `Send` → append (in place,
3198        // mutable-shared, no COW), `Receive` → pop the FRONT.
3199        Op::ChanNew { dst, .. } => {
3200            emit_empty_header(code, ctx, plan.num_regs, dst as u32);
3201            Ok(Flow::Straight)
3202        }
3203        Op::ChanSend { chan, val } => {
3204            lower_list_push(code, kinds, ctx, plan.num_regs, chan, val)?;
3205            Ok(Flow::Straight)
3206        }
3207        Op::ChanRecv { dst, chan } => {
3208            lower_chan_recv(code, kinds, plan.num_regs, dst, chan)?;
3209            Ok(Flow::Straight)
3210        }
3211        // Non-blocking `Try to receive`: a non-empty pipe pops its front value into a fresh Optional
3212        // box (`Some`, handle != 0); an empty pipe yields `Nothing` (handle 0). The single-task
3213        // scheduler never parks a try-recv, so there is no blocking/trap path (unlike `ChanRecv`).
3214        Op::ChanTryRecv { dst, chan } => {
3215            lower_chan_try_recv(code, kinds, ctx, plan.num_regs, dst, chan)?;
3216            Ok(Flow::Straight)
3217        }
3218        // Non-blocking `Try to send`: the unbounded FIFO always has room, so the value is appended (a
3219        // plain queue push) and the success result is `Bool(true)` (an i64 `1`), matching the
3220        // scheduler's `do_try_send`.
3221        Op::ChanTrySend { dst, chan, val } => {
3222            lower_list_push(code, kinds, ctx, plan.num_regs, chan, val)?;
3223            code.push(0x42); // i64.const 1 — Bool(true) rides an i64 local (0/1)
3224            leb_i64(code, 1);
3225            local_set(code, dst as u32);
3226            Ok(Flow::Straight)
3227        }
3228        // `Close` a pipe: the scheduler's close resumes the closer with `Nothing` and never mutates the
3229        // queue (a closed-and-empty recv still just yields `Nothing`). With no result register and no
3230        // observable queue effect in the deterministic single-task model, it lowers to nothing.
3231        Op::ChanClose { .. } => Ok(Flow::Straight),
3232        // `Launch a task to f(args)` — a fire-and-forget task runs SYNCHRONOUSLY (the deterministic
3233        // scheduler runs each launched task to completion in launch order, which for these
3234        // non-blocking bodies is exactly a call). `SpawnHandle` also yields a dead `Int` dummy handle.
3235        Op::Spawn { func, args_start, arg_count } => {
3236            if emit_sync_call(code, kinds, ctx, func, args_start, arg_count)? {
3237                code.push(0x1A); // drop the (discarded) result
3238            }
3239            Ok(Flow::Straight)
3240        }
3241        Op::SpawnHandle { dst, func, args_start, arg_count } => {
3242            if emit_sync_call(code, kinds, ctx, func, args_start, arg_count)? {
3243                code.push(0x1A); // drop
3244            }
3245            code.push(0x42); // i64.const 0 — the dummy handle
3246            leb_i64(code, 0);
3247            local_set(code, dst as u32);
3248            Ok(Flow::Straight)
3249        }
3250        // `Stop <job>` — the task already ran to completion synchronously, so cancelling is a no-op.
3251        Op::TaskAbort { .. } => Ok(Flow::Straight),
3252        // `Await <task>` for its value — the task ran to completion synchronously at its `SpawnHandle`, so
3253        // awaiting is just reading its handle register (`SpawnHandle` leaves an i64 handle there). No
3254        // compiler emit site produces this today (the language's `Await` lowers to `Select`/`Net` await),
3255        // so it is never exercised — but the emitter handles it (a pass-through), never refuses it.
3256        Op::TaskAwait { dst, handle } => {
3257            local_get(code, handle as u32);
3258            local_set(code, dst as u32);
3259            Ok(Flow::Straight)
3260        }
3261        // `Sleep N` — under the deterministic scheduler a sleep advances VIRTUAL time only (like a
3262        // `Select` `After` arm's tick), with no observable output effect on the non-racing shapes, so
3263        // the AOT lowers it to a no-op — matching the seeded scheduler that drives `vm_outcome_concurrent`.
3264        Op::Sleep { .. } => Ok(Flow::Straight),
3265        // ── DETERMINISTIC `select` (`Await the first of: …`). Each arm registers via a
3266        //    `SelectArm*` op (no code — the winning `SelectWait` reads them back); `SelectWait`
3267        //    resolves the winner exactly as the seeded scheduler does for the non-racing shapes: a
3268        //    recv arm whose queue is non-empty wins (pop-front into its var); otherwise the timeout
3269        //    arm fires. The following per-arm `Eq`/jump dispatch (emitted by the compiler) runs the
3270        //    winning branch's body. ──
3271        Op::SelectArmRecv { .. } | Op::SelectArmTimeout { .. } => Ok(Flow::Straight),
3272        Op::SelectWait { dst_arm } => {
3273            lower_select_wait(code, plan, kinds, blocks, k, pc, dst_arm)?;
3274            Ok(Flow::Straight)
3275        }
3276        // ── DETERMINISTIC OFFLINE NETWORKING with LOOPBACK delivery (matches the interpreter/VM offline
3277        //    `NetInbox` loopback: with no relay, a `Send`/`Stream` delivers into our OWN local inbox and
3278        //    a matching `Await` reads it back — the oracle output is transport-independent). The AOT
3279        //    models the inbox as ONE local FIFO queue whose handle lives in a reserved memory slot
3280        //    (`NET_INBOX_ADDR`, inside the null-reserved low-16 region): `Listen` creates it, `Send`/
3281        //    `Stream` push, `Await` pops. `Connect`/`Sync` remain single-node no-ops. ──
3282        Op::NetConnect { .. } | Op::NetSync { .. } => Ok(Flow::Straight),
3283        Op::NetListen { .. } => {
3284            // Create the empty local inbox FIFO; stash its handle at the reserved slot.
3285            emit_empty_header(code, ctx, plan.num_regs, plan.num_regs + 8);
3286            i32_const(code, NET_INBOX_ADDR);
3287            local_get(code, plan.num_regs + 8);
3288            i32_store(code, 0);
3289            Ok(Flow::Straight)
3290        }
3291        Op::NetSend { msg, .. } => {
3292            let elem = kinds.get(msg as usize).ok_or(WasmLowerError::Unsupported("Send of an unknown-kind message"))?;
3293            i32_const(code, NET_INBOX_ADDR);
3294            i32_load(code, 0);
3295            local_set(code, plan.num_regs + 8);
3296            lower_list_push_at(code, elem, ctx, plan.num_regs, plan.num_regs + 8, msg)?;
3297            if cow_clonable(kinds.get(msg as usize)) {
3298                emit_retain(code, msg);
3299            }
3300            Ok(Flow::Straight)
3301        }
3302        Op::NetStream { values, .. } => {
3303            // A batch STREAM delivers the whole list; loopback pushes the list HANDLE (single-node, no
3304            // framing) so `Await stream` pops the same list — byte-faithful to the frame/deframe round-trip.
3305            let elem = kinds.get(values as usize).ok_or(WasmLowerError::Unsupported("Stream of an unknown-kind list"))?;
3306            i32_const(code, NET_INBOX_ADDR);
3307            i32_load(code, 0);
3308            local_set(code, plan.num_regs + 8);
3309            lower_list_push_at(code, elem, ctx, plan.num_regs, plan.num_regs + 8, values)?;
3310            if cow_clonable(kinds.get(values as usize)) {
3311                emit_retain(code, values);
3312            }
3313            Ok(Flow::Straight)
3314        }
3315        Op::NetAwait { dst, .. } => {
3316            let elem = kinds.get(dst as usize).ok_or(WasmLowerError::Unsupported("Await of an unknown-kind message"))?;
3317            i32_const(code, NET_INBOX_ADDR);
3318            i32_load(code, 0);
3319            local_set(code, plan.num_regs + 8);
3320            emit_pop_front(code, elem, plan.num_regs + 8, plan.num_regs + 5, dst as u32)?;
3321            if cow_clonable(kinds.get(dst as usize)) {
3322                emit_retain(code, dst);
3323            }
3324            Ok(Flow::Straight)
3325        }
3326        Op::NetMakePeer { dst, addr } => {
3327            local_get(code, addr as u32);
3328            local_set(code, dst as u32);
3329            Ok(Flow::Straight)
3330        }
3331        Op::NewInductive { dst, ctor, args_start, count, .. } => {
3332            lower_new_inductive(code, kinds, ctx, plan.num_regs, dst, ctor, args_start, count)?;
3333            Ok(Flow::Straight)
3334        }
3335        Op::TestArm { dst, target, variant } => {
3336            lower_test_arm(code, dst, target, variant);
3337            Ok(Flow::Straight)
3338        }
3339        Op::BindArm { dst, target, index, .. } => {
3340            lower_bind_arm(code, kinds, dst, target, index)?;
3341            Ok(Flow::Straight)
3342        }
3343        Op::MakeClosure { dst, func, locals_start } => {
3344            lower_make_closure(code, ctx, plan.num_regs, dst, func, locals_start)?;
3345            Ok(Flow::Straight)
3346        }
3347        Op::CallValue { dst, callee, args_start, arg_count, .. } => {
3348            // A heap argument to a closure call gains the closure body's parameter as a second holder
3349            // — RETAIN, like a direct `Call` (value semantics across the closure boundary).
3350            for a in 0..arg_count {
3351                let arg = args_start + a;
3352                if cow_clonable(kinds.get(arg as usize)) {
3353                    emit_retain(code, arg);
3354                }
3355            }
3356            lower_call_value(code, plan, ctx, pc, dst, callee, args_start, arg_count)?;
3357            Ok(Flow::Straight)
3358        }
3359        Op::IterPop => {
3360            // Drop the top iterator frame: `__iter_sp += 12`.
3361            global_get(code, ctx.iter_global);
3362            i32_const(code, 12);
3363            code.push(0x6A); // i32.add
3364            global_set(code, ctx.iter_global);
3365            Ok(Flow::Straight)
3366        }
3367        Op::LoadNow { dst } => {
3368            let idx = (ctx.host_index)(HostFn::Now).ok_or(WasmLowerError::Unsupported("now not imported"))?;
3369            code.push(0x10); // call now -> i64 (Moment)
3370            leb_u32(code, idx);
3371            local_set(code, dst as u32);
3372            Ok(Flow::Straight)
3373        }
3374        Op::GlobalSet { idx, src } => {
3375            // Storing a heap handle into a global makes the global a second holder — RETAIN so a later
3376            // mutation through another register copy-on-writes rather than clobbering the global.
3377            if cow_clonable(kinds.get(src as usize)) {
3378                emit_retain(code, src);
3379            }
3380            local_get(code, src as u32);
3381            code.push(0x24); // global.set
3382            leb_u32(code, idx as u32);
3383            Ok(Flow::Straight)
3384        }
3385        Op::Call { dst, func, args_start, arg_count } => {
3386            // Value semantics: a heap argument gains the callee's parameter as a SECOND holder, so
3387            // RETAIN it (bump word 12) before the call. A mutation inside the callee then copy-on-writes
3388            // instead of clobbering the caller's value — the VM's `Rc`-clone-on-argument-bind.
3389            for a in 0..arg_count {
3390                let arg = args_start + a;
3391                if cow_clonable(kinds.get(arg as usize)) {
3392                    emit_retain(code, arg);
3393                }
3394            }
3395            // Pass each argument at the callee's declared parameter value type, promoting an `Int`
3396            // argument to `f64` for a `Float` parameter (`half(9)` → `half(9.0)`) instead of pushing
3397            // an `i64` where the signature wants `f64` (invalid wasm).
3398            let pvts = ctx.fn_param_valtypes.get(func as usize).ok_or(WasmLowerError::Unsupported("call of unknown function"))?;
3399            for a in 0..arg_count {
3400                let arg = args_start + a;
3401                let arg_vt = kinds.valtype(arg as usize);
3402                let param_vt = pvts.get(a as usize).copied().unwrap_or(I64);
3403                if arg_vt == param_vt {
3404                    local_get(code, arg as u32);
3405                } else if arg_vt == I64 && param_vt == F64 {
3406                    push_as_f64(code, arg, kinds.get(arg as usize))?;
3407                } else {
3408                    return Err(WasmLowerError::Unsupported("call argument type does not match the parameter"));
3409                }
3410            }
3411            code.push(0x10); // call
3412            leb_u32(code, ctx.fn_base + func as u32);
3413            // A void callee leaves nothing on the stack; only bind a result when it returns one.
3414            if ctx.fn_results.get(func as usize).copied().flatten().is_some() {
3415                local_set(code, dst as u32);
3416            }
3417            Ok(Flow::Straight)
3418        }
3419        Op::CallBuiltin { dst, builtin: BuiltinId::Pow, args_start, arg_count } => {
3420            lower_pow(code, kinds, ctx, plan.num_regs, dst, args_start, arg_count)
3421        }
3422        // `parseInt(text)` — a host call: push the Text handle, `call parse_int` → i64.
3423        Op::CallBuiltin { dst, builtin: BuiltinId::ParseInt, args_start, .. } => {
3424            let idx = (ctx.host_index)(HostFn::ParseInt).ok_or(WasmLowerError::Unsupported("parse_int host not imported"))?;
3425            local_get(code, args_start as u32);
3426            code.push(0x10); // call parse_int
3427            leb_u32(code, idx);
3428            local_set(code, dst as u32);
3429            Ok(Flow::Straight)
3430        }
3431        // `parseFloat(text) -> Float` — the host parses the `Text` handle (`str::parse::<f64>` after a
3432        // trim, matching the VM), returning the f64 directly.
3433        Op::CallBuiltin { dst, builtin: BuiltinId::ParseFloat, args_start, .. } => {
3434            let idx = (ctx.host_index)(HostFn::ParseFloat).ok_or(WasmLowerError::Unsupported("parse_float host not imported"))?;
3435            local_get(code, args_start as u32);
3436            code.push(0x10); // call parse_float
3437            leb_u32(code, idx);
3438            local_set(code, dst as u32);
3439            Ok(Flow::Straight)
3440        }
3441        // `chr(code) -> Text` — a one-character Text built inline from the Unicode scalar value
3442        // (`lower_chr`: UTF-8 encode + a fresh Text object), trapping on an invalid code point.
3443        Op::CallBuiltin { dst, builtin: BuiltinId::Chr, args_start, .. } => {
3444            lower_chr(code, ctx, plan.num_regs, dst, args_start);
3445            Ok(Flow::Straight)
3446        }
3447        // `parse_timestamp(text) -> Moment` — the host parses the RFC-3339 `Text` handle to nanoseconds.
3448        Op::CallBuiltin { dst, builtin: BuiltinId::ParseTimestamp, args_start, .. } => {
3449            let idx = (ctx.host_index)(HostFn::ParseTimestamp).ok_or(WasmLowerError::Unsupported("parse_timestamp host not imported"))?;
3450            local_get(code, args_start as u32);
3451            code.push(0x10); // call parse_timestamp
3452            leb_u32(code, idx);
3453            local_set(code, dst as u32);
3454            Ok(Flow::Straight)
3455        }
3456        // `writeWireResidual(text) -> Int` — frame the Text's bytes (`[len:u32][bytes]`) out to the host
3457        // wire sink (`write_wire_residual(data_ptr, len)`, returning the byte count), the residual-emit
3458        // half of the wire-program protocol. Requires a `Text` argument (its `[len@0][…][data_ptr@8]`
3459        // header is read in place); a non-Text is refused, never mis-lowered.
3460        Op::CallBuiltin { dst, builtin: BuiltinId::WriteWireResidual, args_start, .. } => {
3461            if kinds.get(args_start as usize) != Some(Kind::Text) {
3462                return Err(WasmLowerError::Unsupported("writeWireResidual requires a Text argument"));
3463            }
3464            let idx = (ctx.host_index)(HostFn::WriteWireResidual).ok_or(WasmLowerError::Unsupported("write_wire_residual host not imported"))?;
3465            local_get(code, args_start as u32);
3466            i32_load(code, 8); // the Text's data_ptr
3467            local_get(code, args_start as u32);
3468            i32_load(code, 0); // the Text's byte length
3469            code.push(0x10); // call write_wire_residual → the byte count
3470            leb_u32(code, idx);
3471            local_set(code, dst as u32);
3472            Ok(Flow::Straight)
3473        }
3474        // Calendar/clock component extractors on a `Moment` (`the year of m`, …) → the single
3475        // `temporal_component(nanos, which)` host. Each builtin passes a distinct `which` selector; the
3476        // host computes via the same `temporal::*` the VM uses. A `Date` argument (whose hour/min/sec
3477        // error and whose others need a days→civil path) is not yet lowered — it is refused, not
3478        // mis-lowered, so the corpus biconditional stays sound.
3479        Op::CallBuiltin {
3480            dst,
3481            builtin:
3482                b @ (BuiltinId::YearOf
3483                | BuiltinId::MonthOf
3484                | BuiltinId::DayOf
3485                | BuiltinId::WeekdayOf
3486                | BuiltinId::HourOf
3487                | BuiltinId::MinuteOf
3488                | BuiltinId::SecondOf
3489                | BuiltinId::WeekOf
3490                | BuiltinId::QuarterOf),
3491            args_start,
3492            ..
3493        } => {
3494            let which: i32 = match b {
3495                BuiltinId::YearOf => 0,
3496                BuiltinId::MonthOf => 1,
3497                BuiltinId::DayOf => 2,
3498                BuiltinId::HourOf => 3,
3499                BuiltinId::MinuteOf => 4,
3500                BuiltinId::SecondOf => 5,
3501                BuiltinId::WeekdayOf => 6,
3502                BuiltinId::WeekOf => 7,
3503                BuiltinId::QuarterOf => 8,
3504                _ => unreachable!(),
3505            };
3506            match kinds.get(args_start as usize) {
3507                Some(Kind::Moment) => {
3508                    let idx = (ctx.host_index)(HostFn::TemporalComponent).ok_or(WasmLowerError::Unsupported("temporal_component host not imported"))?;
3509                    local_get(code, args_start as u32); // the Moment (i64 nanoseconds)
3510                    i32_const(code, which);
3511                    code.push(0x10); // call temporal_component
3512                    leb_u32(code, idx);
3513                    local_set(code, dst as u32);
3514                }
3515                // A `Date` (days since epoch): the day-based components go through
3516                // `temporal_component_date`; hour/minute/second have no meaning on a Date (the VM
3517                // errors), so they are refused rather than mis-lowered.
3518                Some(Kind::Date) => {
3519                    if matches!(b, BuiltinId::HourOf | BuiltinId::MinuteOf | BuiltinId::SecondOf) {
3520                        return Err(WasmLowerError::Unsupported("clock component (hour/minute/second) of a Date — a Date has no time-of-day"));
3521                    }
3522                    let idx = (ctx.host_index)(HostFn::TemporalComponentDate).ok_or(WasmLowerError::Unsupported("temporal_component_date host not imported"))?;
3523                    local_get(code, args_start as u32); // the Date (i32 days since epoch)
3524                    i32_const(code, which);
3525                    code.push(0x10); // call temporal_component_date
3526                    leb_u32(code, idx);
3527                    local_set(code, dst as u32);
3528                }
3529                _ => {
3530                    return Err(WasmLowerError::Unsupported("temporal component of a non-temporal value"));
3531                }
3532            }
3533            Ok(Flow::Straight)
3534        }
3535        // Moment arithmetic + calendar/clock extraction — SELF-CONTAINED i64/i32, matching the VM's
3536        // `builtins.rs` exactly (no host, no runtime):
3537        //   `seconds_between(a, b) = (b - a) / 1e9`             (truncating i64 div) -> Int
3538        //   `add_seconds(m, n)     = m + n * 1e9`                                    -> Moment
3539        //   `date_of(m)            = m.div_euclid(NANOS_PER_DAY) as i32`  (FLOOR div) -> Date
3540        //   `time_of(m)            = m.rem_euclid(NANOS_PER_DAY)`  (non-neg remainder) -> Time
3541        // Each requires a `Moment` first argument (the VM errors otherwise), so a non-Moment is refused,
3542        // never mis-lowered. `div_euclid`/`rem_euclid` are open-coded branchlessly (the divisor is the
3543        // positive constant `NANOS_PER_DAY`): `floor = trunc - (rem < 0)`, `euclid_rem = rem + D·(rem < 0)`.
3544        Op::CallBuiltin {
3545            dst,
3546            builtin: b @ (BuiltinId::SecondsBetween | BuiltinId::AddSeconds | BuiltinId::DateOf | BuiltinId::TimeOf),
3547            args_start,
3548            ..
3549        } => {
3550            const NANOS_PER_DAY: i64 = 86_400_000_000_000;
3551            const NANOS_PER_SEC: i64 = 1_000_000_000;
3552            if kinds.get(args_start as usize) != Some(Kind::Moment) {
3553                return Err(WasmLowerError::Unsupported("temporal arithmetic requires a Moment argument"));
3554            }
3555            let i64c = |code: &mut Vec<u8>, v: i64| {
3556                code.push(0x42); // i64.const
3557                leb_i64(code, v);
3558            };
3559            match b {
3560                BuiltinId::SecondsBetween => {
3561                    local_get(code, (args_start + 1) as u32); // b
3562                    local_get(code, args_start as u32); // a
3563                    code.push(0x7D); // i64.sub  → b - a
3564                    i64c(code, NANOS_PER_SEC);
3565                    code.push(0x7F); // i64.div_s → (b - a) / 1e9
3566                }
3567                BuiltinId::AddSeconds => {
3568                    local_get(code, args_start as u32); // m
3569                    local_get(code, (args_start + 1) as u32); // n
3570                    i64c(code, NANOS_PER_SEC);
3571                    code.push(0x7E); // i64.mul → n * 1e9
3572                    code.push(0x7C); // i64.add → m + n*1e9
3573                }
3574                BuiltinId::DateOf => {
3575                    local_get(code, args_start as u32);
3576                    i64c(code, NANOS_PER_DAY);
3577                    code.push(0x7F); // i64.div_s → q_trunc
3578                    local_get(code, args_start as u32);
3579                    i64c(code, NANOS_PER_DAY);
3580                    code.push(0x81); // i64.rem_s → r_trunc
3581                    i64c(code, 0);
3582                    code.push(0x53); // i64.lt_s → (r_trunc < 0):i32
3583                    code.push(0xAD); // i64.extend_i32_u → 0/1 : i64
3584                    code.push(0x7D); // i64.sub → q_trunc - (r_trunc<0) = floor(m/D)
3585                    code.push(0xA7); // i32.wrap_i64 → i32 days-since-epoch (Date)
3586                }
3587                BuiltinId::TimeOf => {
3588                    local_get(code, args_start as u32);
3589                    i64c(code, NANOS_PER_DAY);
3590                    code.push(0x81); // i64.rem_s → r_trunc
3591                    local_get(code, args_start as u32);
3592                    i64c(code, NANOS_PER_DAY);
3593                    code.push(0x81); // i64.rem_s → r_trunc (again, as the addend base)
3594                    i64c(code, 0);
3595                    code.push(0x53); // i64.lt_s → (r_trunc < 0):i32
3596                    code.push(0xAD); // i64.extend_i32_u → 0/1 : i64
3597                    i64c(code, NANOS_PER_DAY);
3598                    code.push(0x7E); // i64.mul → D·(r_trunc<0)
3599                    code.push(0x7C); // i64.add → r_trunc + D·(r_trunc<0) = rem_euclid
3600                }
3601                _ => unreachable!(),
3602            }
3603            local_set(code, dst as u32);
3604            Ok(Flow::Straight)
3605        }
3606        // LINKER-mode extended temporal — the calendar logic lives in `base::temporal`, so these
3607        // delegate to it via a runtime call (guaranteeing bit-identity with the VM): `format_timestamp(m)`
3608        // → a `Text` handle (RFC-3339 UTC), `months_between`/`years_between`(a, b) → an `Int`. Each needs a
3609        // `Moment` argument and the linker; a self-contained module refuses them rather than mis-lowering.
3610        Op::CallBuiltin {
3611            dst,
3612            builtin: b @ (BuiltinId::FormatTimestamp | BuiltinId::MonthsBetween | BuiltinId::YearsBetween | BuiltinId::InZone | BuiltinId::LocalInstant),
3613            args_start,
3614            ..
3615        } => {
3616            if !ctx.linked {
3617                return Err(WasmLowerError::Unsupported("format_timestamp/months_between/years_between/in_zone/local_instant need the linked runtime (base::temporal)"));
3618            }
3619            if kinds.get(args_start as usize) != Some(Kind::Moment) {
3620                return Err(WasmLowerError::Unsupported("extended temporal requires a Moment argument"));
3621            }
3622            let (rt, two_args) = match b {
3623                BuiltinId::FormatTimestamp => (HostFn::FormatTimestampRt, false),
3624                BuiltinId::MonthsBetween => (HostFn::MonthsBetweenRt, true),
3625                BuiltinId::YearsBetween => (HostFn::YearsBetweenRt, true),
3626                // `in_zone`/`local_instant` take a Moment + a zone-name Text (the runtime reads the Text
3627                // handle from the shared memory), so the 2nd argument must be a `Text`.
3628                BuiltinId::InZone => (HostFn::InZoneRt, true),
3629                BuiltinId::LocalInstant => (HostFn::LocalInstantRt, true),
3630                _ => unreachable!(),
3631            };
3632            if matches!(b, BuiltinId::InZone | BuiltinId::LocalInstant)
3633                && kinds.get((args_start + 1) as usize) != Some(Kind::Text)
3634            {
3635                return Err(WasmLowerError::Unsupported("in_zone/local_instant require a Text zone name"));
3636            }
3637            let idx = (ctx.host_index)(rt).ok_or(WasmLowerError::Unsupported("extended temporal runtime fn not imported"))?;
3638            local_get(code, args_start as u32);
3639            if two_args {
3640                local_get(code, (args_start + 1) as u32);
3641            }
3642            code.push(0x10); // call the logos_rt_* runtime fn
3643            leb_u32(code, idx);
3644            local_set(code, dst as u32);
3645            Ok(Flow::Straight)
3646        }
3647        // The general SIMD lane vocabulary (`base::LanesVal`, LINKER MODE): constructors (`lanes16Word8`/
3648        // `lanes8Word32`/`lanes4Word64` from a `Seq`, `splat16Word8`/`splat8Word32` from an `Int`) and the
3649        // extractors (`seqOfLanes16W8`/`seqOfLanes8` → a `Seq`) take one argument; the byte/word-lane ops
3650        // (`shuffle16`/`interleave*`/`byteAdd16`/`maddubs16`/`packus16`) and `shrBytes16` take two. Each is
3651        // a single `logos_rt_lanes_*` call delegating to the pure-Rust `base::word` spec.
3652        Op::CallBuiltin { dst, builtin: b, args_start, .. } if ctx.linked && lanes_v_host_fn(b).is_some() => {
3653            let rt = lanes_v_host_fn(b).expect("guarded by lanes_v_host_fn(b).is_some()");
3654            let idx = (ctx.host_index)(rt).ok_or(WasmLowerError::Unsupported("lane-vector runtime fn not imported"))?;
3655            let two = matches!(
3656                b,
3657                BuiltinId::Shuffle16
3658                    | BuiltinId::InterleaveLo16
3659                    | BuiltinId::InterleaveHi16
3660                    | BuiltinId::ByteAdd16
3661                    | BuiltinId::Maddubs16
3662                    | BuiltinId::Packus16
3663                    | BuiltinId::ShrBytes16
3664            );
3665            local_get(code, args_start as u32);
3666            if two {
3667                local_get(code, (args_start + 1) as u32);
3668            }
3669            code.push(0x10); // call the logos_rt_lanes_* runtime fn
3670            leb_u32(code, idx);
3671            local_set(code, dst as u32);
3672            Ok(Flow::Straight)
3673        }
3674        // Money FX (LINKER MODE, over `base::money`'s ambient rate table): `set_rate(code, rate)` installs
3675        // one rate (coercing the rate arg to a `Rational` handle IN-PLACE by kind — `Int`→`rational_from_i64`,
3676        // `Decimal`→`decimal_to_rational`, `Rational`→as-is), returning the `Nothing` handle (0).
3677        Op::CallBuiltin { dst, builtin: BuiltinId::SetRate, args_start, .. } if ctx.linked => {
3678            let set_idx = (ctx.host_index)(HostFn::MoneySetRate).ok_or(WasmLowerError::Unsupported("set_rate runtime fn not imported"))?;
3679            local_get(code, args_start as u32); // the currency-code Text handle
3680            match kinds.get((args_start + 1) as usize) {
3681                Some(Kind::Rational) => local_get(code, (args_start + 1) as u32),
3682                Some(Kind::Int) => {
3683                    let c = (ctx.host_index)(HostFn::RationalFromI64).ok_or(WasmLowerError::Unsupported("rational_from_i64 not imported"))?;
3684                    local_get(code, (args_start + 1) as u32);
3685                    code.push(0x10);
3686                    leb_u32(code, c);
3687                }
3688                Some(Kind::Decimal) => {
3689                    let c = (ctx.host_index)(HostFn::DecimalToRational).ok_or(WasmLowerError::Unsupported("decimal_to_rational not imported"))?;
3690                    local_get(code, (args_start + 1) as u32);
3691                    code.push(0x10);
3692                    leb_u32(code, c);
3693                }
3694                _ => return Err(WasmLowerError::Unsupported("set_rate rate must be an Int, Decimal, or Rational")),
3695            }
3696            code.push(0x10); // call set_rate → 0 (Nothing)
3697            leb_u32(code, set_idx);
3698            local_set(code, dst as u32);
3699            Ok(Flow::Straight)
3700        }
3701        // `to_currency(money, code)` — convert a `Money` into the named currency via the ambient rates.
3702        Op::CallBuiltin { dst, builtin: BuiltinId::ToCurrency, args_start, .. } if ctx.linked => {
3703            let idx = (ctx.host_index)(HostFn::MoneyToCurrency).ok_or(WasmLowerError::Unsupported("to_currency runtime fn not imported"))?;
3704            local_get(code, args_start as u32); // the Money handle
3705            local_get(code, (args_start + 1) as u32); // the currency-code Text handle
3706            code.push(0x10);
3707            leb_u32(code, idx);
3708            local_set(code, dst as u32);
3709            Ok(Flow::Straight)
3710        }
3711        // `set_rates(map)` — install a whole `Map of <code> to <rate>`, dispatched on the map's VALUE kind
3712        // (resolved by scanning the region for the `SetIndex` that populated it), returning `Nothing` (0).
3713        Op::CallBuiltin { dst, builtin: BuiltinId::SetRates, args_start, .. } if ctx.linked => {
3714            let vk = set_rates_value_kind(&plan.ops, args_start, kinds);
3715            let rt = match vk {
3716                Some(Kind::Int) => HostFn::MoneySetRatesInt,
3717                Some(Kind::Rational) => HostFn::MoneySetRatesRational,
3718                Some(Kind::Decimal) => HostFn::MoneySetRatesDecimal,
3719                _ => return Err(WasmLowerError::Unsupported("set_rates needs a Map of currency code to an Int/Decimal/Rational rate whose value kind is statically known")),
3720            };
3721            let idx = (ctx.host_index)(rt).ok_or(WasmLowerError::Unsupported("set_rates runtime fn not imported"))?;
3722            local_get(code, args_start as u32); // the Map handle
3723            code.push(0x10);
3724            leb_u32(code, idx);
3725            local_set(code, dst as u32);
3726            Ok(Flow::Straight)
3727        }
3728        // `wireBytes(value) -> Seq of Int` (LINKER MODE) — marshal the value to its wire bytes via the REAL
3729        // `logicaffeine_compile::concurrency::marshal::encode_value_raw` (a `logos_rt_wire_bytes_*` runtime
3730        // fn reconstructs the `RuntimeValue` from the AOT value, by kind, and encodes), byte-identical to
3731        // the VM's `bytes_to_seq(encode_value_raw(v))`. A composite value's reconstruction is not yet wired
3732        // (soundly refused). `gc-sections` keeps the compiler out of any module that never calls this.
3733        Op::CallBuiltin { dst, builtin: BuiltinId::WireBytes, args_start, .. } if ctx.linked => {
3734            let h = wire_bytes_host_fn(kinds.get(args_start as usize))
3735                .ok_or(WasmLowerError::Unsupported("wireBytes of this value kind is not yet reconstructed for the wire codec"))?;
3736            let idx = (ctx.host_index)(h).ok_or(WasmLowerError::Unsupported("wire_bytes runtime fn not imported"))?;
3737            local_get(code, args_start as u32); // the value (i64 / f64 / i32 Text handle by kind)
3738            code.push(0x10);
3739            leb_u32(code, idx);
3740            local_set(code, dst as u32);
3741            Ok(Flow::Straight)
3742        }
3743        // `readWireProgram() -> a DYNAMIC value` (LINKER MODE) — alloc a scratch buffer, have the host
3744        // (`read_wire_frame`) write the wire frame into it (returning the byte length), then DECODE it via
3745        // the REAL `decode_value_raw` to a leaked `Box<RuntimeValue>` (Kind::Dynamic). The buffer size caps
3746        // the received program. The result's concrete type is only known at runtime — that's why it's boxed.
3747        Op::CallBuiltin { dst, builtin: BuiltinId::ReadWireProgram, .. } if ctx.linked => {
3748            const WIRE_BUF: i32 = 1 << 16;
3749            let frame = (ctx.host_index)(HostFn::ReadWireFrame).ok_or(WasmLowerError::Unsupported("read_wire_frame host not imported"))?;
3750            let decode = (ctx.host_index)(HostFn::ReadWireProgramRt).ok_or(WasmLowerError::Unsupported("read_wire_program runtime fn not imported"))?;
3751            let buf = plan.num_regs + 8; // an i32 heap scratch local (num_regs+5..+11)
3752            i32_const(code, WIRE_BUF);
3753            emit_alloc(code, ctx, buf); // buf = bump-alloc(WIRE_BUF)
3754            local_get(code, buf); // decode's 1st arg
3755            local_get(code, buf);
3756            i32_const(code, WIRE_BUF);
3757            code.push(0x10); // read_wire_frame(buf, WIRE_BUF) -> len
3758            leb_u32(code, frame);
3759            code.push(0x10); // logos_rt_read_wire_program(buf, len) -> dynamic handle
3760            leb_u32(code, decode);
3761            local_set(code, dst as u32);
3762            Ok(Flow::Straight)
3763        }
3764        // `run_accepted(fn, arg, lo, hi) -> Int` (LINKER MODE) — sandbox-eval a wire-received SHIPPED
3765        // function through `AcceptanceContract::apply`. `fn` MUST be a `Dynamic` (a `readWireProgram`
3766        // result holding a `Function{generated}`); an ordinary compiled closure is not a shipped function,
3767        // so a non-Dynamic first arg is refused (the VM refuses ordinary closures at runtime, likewise).
3768        Op::CallBuiltin { dst, builtin: BuiltinId::RunAccepted, args_start, .. } if ctx.linked => {
3769            if kinds.get(args_start as usize) != Some(Kind::Dynamic) {
3770                return Err(WasmLowerError::Unsupported("run_accepted requires a wire-received (dynamic) shipped function"));
3771            }
3772            let idx = (ctx.host_index)(HostFn::RunAccepted).ok_or(WasmLowerError::Unsupported("run_accepted runtime fn not imported"))?;
3773            local_get(code, args_start as u32); // the dynamic function handle (i32)
3774            local_get(code, (args_start + 1) as u32); // arg (i64)
3775            local_get(code, (args_start + 2) as u32); // lo (i64)
3776            local_get(code, (args_start + 3) as u32); // hi (i64)
3777            code.push(0x10); // logos_rt_run_accepted(fn, arg, lo, hi) -> i64
3778            leb_u32(code, idx);
3779            local_set(code, dst as u32);
3780            Ok(Flow::Straight)
3781        }
3782        // `format(x) -> Text` — `x.to_display_string()` as a Text, the SAME materialization a `+`
3783        // concat performs on a non-Text operand (an empty `format()` yields an empty Text).
3784        Op::CallBuiltin { dst, builtin: BuiltinId::Format, args_start, arg_count } => {
3785            if arg_count == 0 {
3786                lower_text_literal(code, ctx, plan.num_regs, b""); // leaves the handle on the stack
3787                local_set(code, dst as u32);
3788            } else {
3789                // `emit_stringify` writes the Text handle straight into the `dst` local.
3790                emit_stringify(code, ctx, plan.num_regs, args_start as u32, kinds.get(args_start as usize), dst as u32)?;
3791            }
3792            Ok(Flow::Straight)
3793        }
3794        // `repeatSeq(x, n)` — a fresh `n`-element sequence of the scalar `x` (`[x] * n`).
3795        Op::CallBuiltin { dst, builtin: BuiltinId::RepeatSeq, args_start, arg_count } => {
3796            if arg_count != 2 {
3797                return Err(WasmLowerError::Unsupported("repeatSeq arity"));
3798            }
3799            lower_repeat_seq(code, kinds, ctx, plan.num_regs, dst, args_start)?;
3800            Ok(Flow::Straight)
3801        }
3802        // `money(amount, "USD")` — build an EXACT Money handle. `amount` is a Decimal handle or an
3803        // Int (whole units); the currency is a Text code resolved by `currency::by_code` in the runtime.
3804        Op::CallBuiltin { dst, builtin: BuiltinId::Money, args_start, arg_count } if ctx.linked && arg_count == 2 => {
3805            if kinds.get((args_start + 1) as usize) != Some(Kind::Text) {
3806                return Err(WasmLowerError::Unsupported("money(amount, currency) with a non-Text currency"));
3807            }
3808            let host = match kinds.get(args_start as usize) {
3809                Some(Kind::Decimal) => HostFn::MoneyFromDecimal,
3810                Some(Kind::Int) => HostFn::MoneyFromI64,
3811                _ => return Err(WasmLowerError::Unsupported("money amount must be an Int or Decimal")),
3812            };
3813            let make = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("money constructor not imported"))?;
3814            local_get(code, args_start as u32);
3815            local_get(code, (args_start + 1) as u32);
3816            code.push(0x10);
3817            leb_u32(code, make);
3818            local_set(code, dst as u32);
3819            return Ok(Flow::Straight);
3820        }
3821        // `quantity(v, "unit")` — build an EXACT Quantity handle: an Int magnitude + a unit name Text
3822        // resolved by `units::by_name` in the runtime (the `5 meters` literal lowers to this too).
3823        Op::CallBuiltin { dst, builtin: BuiltinId::Quantity, args_start, arg_count } if ctx.linked && arg_count == 2 => {
3824            if kinds.get((args_start + 1) as usize) != Some(Kind::Text) {
3825                return Err(WasmLowerError::Unsupported("quantity(value, unit) with a non-Text unit"));
3826            }
3827            if kinds.get(args_start as usize) != Some(Kind::Int) {
3828                return Err(WasmLowerError::Unsupported("quantity magnitude must be an Int"));
3829            }
3830            let make = (ctx.host_index)(HostFn::QuantityOfI64).ok_or(WasmLowerError::Unsupported("quantity constructor not imported"))?;
3831            local_get(code, args_start as u32);
3832            local_get(code, (args_start + 1) as u32);
3833            code.push(0x10);
3834            leb_u32(code, make);
3835            local_set(code, dst as u32);
3836            return Ok(Flow::Straight);
3837        }
3838        // `convert(q, "unit")` (the surface `X in <unit>`) — re-express a Quantity in a new display unit
3839        // of the SAME dimension (dimension-checked at compile time); the runtime keeps the SI magnitude.
3840        Op::CallBuiltin { dst, builtin: BuiltinId::Convert, args_start, arg_count } if ctx.linked && arg_count == 2 => {
3841            if kinds.get(args_start as usize) != Some(Kind::Quantity) {
3842                return Err(WasmLowerError::Unsupported("convert() requires a Quantity"));
3843            }
3844            if kinds.get((args_start + 1) as usize) != Some(Kind::Text) {
3845                return Err(WasmLowerError::Unsupported("convert(q, unit) with a non-Text unit"));
3846            }
3847            let make = (ctx.host_index)(HostFn::QuantityConvert).ok_or(WasmLowerError::Unsupported("quantity convert not imported"))?;
3848            local_get(code, args_start as u32);
3849            local_get(code, (args_start + 1) as u32);
3850            code.push(0x10);
3851            leb_u32(code, make);
3852            local_set(code, dst as u32);
3853            return Ok(Flow::Straight);
3854        }
3855        // `decimal("…")` — parse a Text arg into an exact Decimal via the runtime.
3856        Op::CallBuiltin { dst, builtin: BuiltinId::Decimal, args_start, arg_count } if ctx.linked && arg_count == 1 => {
3857            if kinds.get(args_start as usize) != Some(Kind::Text) {
3858                return Err(WasmLowerError::Unsupported("decimal(x) with a non-Text argument"));
3859            }
3860            let from = (ctx.host_index)(HostFn::DecimalFromText).ok_or(WasmLowerError::Unsupported("decimal_from_text not imported"))?;
3861            local_get(code, args_start as u32);
3862            code.push(0x10);
3863            leb_u32(code, from);
3864            local_set(code, dst as u32);
3865            return Ok(Flow::Straight);
3866        }
3867        // `complex(re, im)` — build an EXACT Complex handle from two Int components via the runtime.
3868        Op::CallBuiltin { dst, builtin: BuiltinId::Modular, args_start, arg_count } if ctx.linked && arg_count == 2 => {
3869            if kinds.get(args_start as usize) != Some(Kind::Int) || kinds.get((args_start + 1) as usize) != Some(Kind::Int) {
3870                return Err(WasmLowerError::Unsupported("modular(v, n) with non-Int components"));
3871            }
3872            let from = (ctx.host_index)(HostFn::ModularFromI64).ok_or(WasmLowerError::Unsupported("modular_from_i64 not imported"))?;
3873            local_get(code, args_start as u32);
3874            local_get(code, (args_start + 1) as u32);
3875            code.push(0x10);
3876            leb_u32(code, from);
3877            local_set(code, dst as u32);
3878            return Ok(Flow::Straight);
3879        }
3880        Op::CallBuiltin { dst, builtin: BuiltinId::Complex, args_start, arg_count } if ctx.linked && arg_count == 2 => {
3881            if kinds.get(args_start as usize) != Some(Kind::Int) || kinds.get((args_start + 1) as usize) != Some(Kind::Int) {
3882                return Err(WasmLowerError::Unsupported("complex(re, im) with non-Int components"));
3883            }
3884            let from = (ctx.host_index)(HostFn::ComplexFromI64).ok_or(WasmLowerError::Unsupported("complex_from_i64 not imported"))?;
3885            local_get(code, args_start as u32);
3886            local_get(code, (args_start + 1) as u32);
3887            code.push(0x10); // call logos_rt_complex_from_i64
3888            leb_u32(code, from);
3889            local_set(code, dst as u32);
3890            Ok(Flow::Straight)
3891        }
3892        // `floor`/`ceil`/`round`/`abs` of a LINKED `Rational`: EXACT rounding on the BigInt-backed
3893        // fraction (`logos_rt_rational_*`) — floor/ceil/round yield a BigInt handle, abs a Rational one —
3894        // never the lossy `f64` path. Handled here (not `lower_builtin`) for the `ctx` host table.
3895        Op::CallBuiltin { dst, builtin: b @ (BuiltinId::Floor | BuiltinId::Ceil | BuiltinId::Round | BuiltinId::Abs), args_start, .. }
3896            if ctx.linked && kinds.get(args_start as usize) == Some(Kind::Rational) =>
3897        {
3898            let host = match b {
3899                BuiltinId::Floor => HostFn::RationalFloor,
3900                BuiltinId::Ceil => HostFn::RationalCeil,
3901                BuiltinId::Round => HostFn::RationalRound,
3902                _ => HostFn::RationalAbs,
3903            };
3904            lower_rational_unary(code, ctx, dst, args_start, host)
3905        }
3906        // The LINKED `Uuid` builtins: `uuid("…")` parse + `uuid_version` take one arg; the `uuid_nil`/
3907        // `uuid_max`/`uuid_dns`/… constants take none. Each is a direct `logos_rt_uuid_*` call.
3908        Op::CallBuiltin { dst, builtin: b @ (BuiltinId::Uuid | BuiltinId::UuidNil | BuiltinId::UuidMax | BuiltinId::UuidDns | BuiltinId::UuidUrl | BuiltinId::UuidOid | BuiltinId::UuidX500 | BuiltinId::UuidVersion), args_start, .. }
3909            if ctx.linked =>
3910        {
3911            let host = match b {
3912                BuiltinId::Uuid => HostFn::UuidParse,
3913                BuiltinId::UuidNil => HostFn::UuidNil,
3914                BuiltinId::UuidMax => HostFn::UuidMax,
3915                BuiltinId::UuidDns => HostFn::UuidDns,
3916                BuiltinId::UuidUrl => HostFn::UuidUrl,
3917                BuiltinId::UuidOid => HostFn::UuidOid,
3918                BuiltinId::UuidX500 => HostFn::UuidX500,
3919                _ => HostFn::UuidVersion,
3920            };
3921            let idx = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("uuid builtin not imported"))?;
3922            // `uuid(text)` and `uuid_version(u)` consume the one arg; the constants take none.
3923            if matches!(b, BuiltinId::Uuid | BuiltinId::UuidVersion) {
3924                local_get(code, args_start as u32);
3925            }
3926            code.push(0x10);
3927            leb_u32(code, idx);
3928            local_set(code, dst as u32);
3929            return Ok(Flow::Straight);
3930        }
3931        // Byte interop: `text_bytes`/`uuid_bytes` build a `Seq of Int` of raw bytes; `text_from_bytes`
3932        // rebuilds a `Text`; `uuid_from_bytes` packs 16 bytes and boxes a `Uuid` (linker). Emitter-heap
3933        // seq/Text construction — no host except `uuid_from_bytes`'s `logos_rt_uuid_from_ptr`.
3934        Op::CallBuiltin { dst, builtin: BuiltinId::TextBytes, args_start, .. } => {
3935            lower_text_bytes(code, ctx, plan.num_regs, dst, args_start);
3936            return Ok(Flow::Straight);
3937        }
3938        Op::CallBuiltin { dst, builtin: BuiltinId::UuidBytes, args_start, .. } if ctx.linked => {
3939            lower_uuid_bytes(code, ctx, plan.num_regs, dst, args_start);
3940            return Ok(Flow::Straight);
3941        }
3942        Op::CallBuiltin { dst, builtin: BuiltinId::TextFromBytes, args_start, .. } => {
3943            lower_text_from_bytes(code, ctx, plan.num_regs, dst, args_start);
3944            return Ok(Flow::Straight);
3945        }
3946        Op::CallBuiltin { dst, builtin: BuiltinId::UuidFromBytes, args_start, .. } if ctx.linked => {
3947            return lower_uuid_from_bytes(code, ctx, plan.num_regs, dst, args_start);
3948        }
3949        // The SHA-1 SHA-NI lane vocabulary (linker): construction/unpack are inline, the four rounds call
3950        // the `logos_rt_sha1*` runtime (which delegates to `base::sha_ops`).
3951        Op::CallBuiltin { dst, builtin: BuiltinId::Lanes4Of, args_start, .. } if ctx.linked => {
3952            lower_lanes4_of(code, ctx, plan.num_regs, dst, [args_start, args_start + 1, args_start + 2, args_start + 3]);
3953            return Ok(Flow::Straight);
3954        }
3955        Op::CallBuiltin { dst, builtin: BuiltinId::Lanes4Word32Make, args_start, .. } if ctx.linked => {
3956            lower_lanes4_word32(code, ctx, plan.num_regs, dst, args_start);
3957            return Ok(Flow::Straight);
3958        }
3959        Op::CallBuiltin { dst, builtin: BuiltinId::SeqOfLanes4W32, args_start, .. } if ctx.linked => {
3960            lower_seq_of_lanes4(code, ctx, plan.num_regs, dst, args_start);
3961            return Ok(Flow::Straight);
3962        }
3963        Op::CallBuiltin { dst, builtin: b @ (BuiltinId::Sha1Rnds4 | BuiltinId::Sha1Msg1 | BuiltinId::Sha1Msg2 | BuiltinId::Sha1Nexte), args_start, .. } if ctx.linked => {
3964            let (host, ternary) = match b {
3965                BuiltinId::Sha1Rnds4 => (HostFn::Sha1Rnds4, true),
3966                BuiltinId::Sha1Msg1 => (HostFn::Sha1Msg1, false),
3967                BuiltinId::Sha1Msg2 => (HostFn::Sha1Msg2, false),
3968                _ => (HostFn::Sha1Nexte, false),
3969            };
3970            return lower_sha1_op(code, ctx, dst, args_start, host, ternary);
3971        }
3972        Op::CallBuiltin { dst, builtin, args_start, arg_count } => {
3973            lower_builtin(code, kinds, dst, builtin, args_start, arg_count)
3974        }
3975        // `args()` — the host returns the argv `Seq of Text` handle (built in this module's memory).
3976        Op::Args { dst } => {
3977            let idx = (ctx.host_index)(HostFn::Args).ok_or(WasmLowerError::Unsupported("args host not imported"))?;
3978            code.push(0x10); // call args
3979            leb_u32(code, idx);
3980            local_set(code, dst as u32);
3981            Ok(Flow::Straight)
3982        }
3983        Op::Show { src } => {
3984            // A whole tuple assembles its `(e0, e1, …)` display inline from the static layout
3985            // (deterministic order), then prints it — no per-kind host sink exists for it. This is
3986            // keyed on the tuple LAYOUT (populated by `NewTuple`), not the Kind: a HOMOGENEOUS tuple
3987            // like `(10, 20)` collapses to `Kind::SeqInt` for its element machinery yet must still
3988            // display with tuple parens `(10, 20)`, not list brackets `[10, 20]`. A real list literal
3989            // (`NewList`) is absent from `tuple_layouts`, so it keeps its `[…]` sink.
3990            if plan.structs.tuple_layouts.contains_key(&src) {
3991                lower_show_tuple(code, plan, ctx, src)?;
3992                return Ok(Flow::Straight);
3993            }
3994            let kind = kinds.get(src as usize).ok_or(WasmLowerError::Unsupported("Show of an unknown-kind value"))?;
3995            // A whole enum (`North`, `Ctor`) prints its variant name via a tag→name dispatch built
3996            // from the enum type's variant set — no per-kind host sink exists for it.
3997            if kind == Kind::Enum {
3998                lower_show_enum(code, plan, ctx, src)?;
3999                return Ok(Flow::Straight);
4000            }
4001            // A whole struct (`Point { x: 1, y: 2 }`) — fields in deterministic alphabetical order,
4002            // matching the VM's now-sorted `HashMap` display.
4003            if kind == Kind::Struct {
4004                lower_show_struct(code, plan, ctx, src)?;
4005                return Ok(Flow::Straight);
4006            }
4007            // A whole `Seq of Struct` (`[Point { x: 1, y: 2 }, …]`) — each element struct rendered in
4008            // the same deterministic field order, concatenated into the `[…]` list display.
4009            if kind == Kind::SeqStruct {
4010                lower_show_seqstruct(code, plan, ctx, src)?;
4011                return Ok(Flow::Straight);
4012            }
4013            // A whole Map assembles its `{k0: v0, k1: v1, …}` display inline by iterating its entries
4014            // in stored order — which is INSERTION order, matching the VM's `IndexMap` (they share the
4015            // same `MapStorage`), so the rendering is byte-identical.
4016            // A NESTED int sequence (`[[1, 2], [3, 4]]`) assembles `[[…], […]]` by iterating the outer
4017            // seq and rendering each inner `Seq of Int` with the scalar seq formatter — deterministic
4018            // (both tiers store lists in insertion order).
4019            if kind == Kind::SeqSeqInt {
4020                lower_show_seqseq(code, ctx, plan.num_regs, src)?;
4021                return Ok(Flow::Straight);
4022            }
4023            // A whole `Seq of Enum` (`[North, South]`, `[Circle(5), Dot]`) renders `[e0, e1, …]`, each
4024            // element by the enum's tag→name dispatch (nullary name or `Ctor(fields)`), insertion order.
4025            if kind == Kind::SeqEnum {
4026                lower_show_seqenum(code, plan, ctx, src)?;
4027                return Ok(Flow::Straight);
4028            }
4029            if kind == Kind::Map {
4030                lower_show_map(code, plan, kinds, ctx, src)?;
4031                return Ok(Flow::Straight);
4032            }
4033            // A `Rational`: LINKER mode renders the BigInt-backed handle via `logos_rt_rational_to_text`
4034            // (`num/den`, or `num` when whole — exactly the VM's `Rational::to_string`) then prints it;
4035            // the self-contained i64/i64 value loads its two words (num@0, den@8) and hands them to the
4036            // `print_rational` host, which renders `num/den` (or `num` when `den == 1`).
4037            if kind == Kind::Rational {
4038                if ctx.linked {
4039                    let to_text = (ctx.host_index)(HostFn::RationalToText).ok_or(WasmLowerError::Unsupported("rational_to_text not imported"))?;
4040                    let print_text = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("print_text not imported"))?;
4041                    local_get(code, src as u32);
4042                    code.push(0x10);
4043                    leb_u32(code, to_text);
4044                    code.push(0x10);
4045                    leb_u32(code, print_text);
4046                    return Ok(Flow::Straight);
4047                }
4048                let idx = (ctx.host_index)(HostFn::PrintRational).ok_or(WasmLowerError::Unsupported("Show sink not imported"))?;
4049                local_get(code, src as u32);
4050                i64_load(code, 0);
4051                local_get(code, src as u32);
4052                i64_load(code, 8);
4053                code.push(0x10); // call print_rational
4054                leb_u32(code, idx);
4055                return Ok(Flow::Straight);
4056            }
4057            // A `BigInt` handle (linker mode): render it to a decimal `Text` now
4058            // (`logos_rt_bigint_to_text` — deferred until here so a Pow/`*` chain could keep computing on
4059            // real BigInts) and print that Text.
4060            if kind == Kind::BigInt {
4061                let to_text = (ctx.host_index)(HostFn::BigintToText).ok_or(WasmLowerError::Unsupported("bigint_to_text not imported"))?;
4062                let print_text = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("print_text not imported"))?;
4063                local_get(code, src as u32); // i32 BigInt handle
4064                code.push(0x10);
4065                leb_u32(code, to_text); // -> i32 Text handle
4066                code.push(0x10);
4067                leb_u32(code, print_text); // print the decimal
4068                return Ok(Flow::Straight);
4069            }
4070            // A `Complex` handle: render `re±imi` to a Text via the runtime, then print it.
4071            if kind == Kind::Complex {
4072                let to_text = (ctx.host_index)(HostFn::ComplexToText).ok_or(WasmLowerError::Unsupported("complex_to_text not imported"))?;
4073                let print_text = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("print_text not imported"))?;
4074                local_get(code, src as u32); // i32 Complex handle
4075                code.push(0x10);
4076                leb_u32(code, to_text); // -> i32 Text handle
4077                code.push(0x10);
4078                leb_u32(code, print_text);
4079                return Ok(Flow::Straight);
4080            }
4081            // A `Modular` handle: render `v (mod n)` via the runtime, then print it.
4082            if kind == Kind::Modular {
4083                let to_text = (ctx.host_index)(HostFn::ModularToText).ok_or(WasmLowerError::Unsupported("modular_to_text not imported"))?;
4084                let print_text = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("print_text not imported"))?;
4085                local_get(code, src as u32);
4086                code.push(0x10);
4087                leb_u32(code, to_text);
4088                code.push(0x10);
4089                leb_u32(code, print_text);
4090                return Ok(Flow::Straight);
4091            }
4092            if kind == Kind::Decimal {
4093                let to_text = (ctx.host_index)(HostFn::DecimalToText).ok_or(WasmLowerError::Unsupported("decimal_to_text not imported"))?;
4094                let print_text = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("print_text not imported"))?;
4095                local_get(code, src as u32);
4096                code.push(0x10);
4097                leb_u32(code, to_text);
4098                code.push(0x10);
4099                leb_u32(code, print_text);
4100                return Ok(Flow::Straight);
4101            }
4102            if kind == Kind::Money {
4103                let to_text = (ctx.host_index)(HostFn::MoneyToText).ok_or(WasmLowerError::Unsupported("money_to_text not imported"))?;
4104                let print_text = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("print_text not imported"))?;
4105                local_get(code, src as u32);
4106                code.push(0x10);
4107                leb_u32(code, to_text);
4108                code.push(0x10);
4109                leb_u32(code, print_text);
4110                return Ok(Flow::Straight);
4111            }
4112            // A `Quantity` handle: render `<magnitude> <symbol>` (or the dimension signature) via the
4113            // runtime, mirroring the interpreter's `QuantityValue::display`, then print it.
4114            if kind == Kind::Quantity {
4115                let to_text = (ctx.host_index)(HostFn::QuantityToText).ok_or(WasmLowerError::Unsupported("quantity_to_text not imported"))?;
4116                let print_text = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("print_text not imported"))?;
4117                local_get(code, src as u32);
4118                code.push(0x10);
4119                leb_u32(code, to_text);
4120                code.push(0x10);
4121                leb_u32(code, print_text);
4122                return Ok(Flow::Straight);
4123            }
4124            // A `Uuid` handle: render the canonical lowercase form via `logos_rt_uuid_to_text`, then print.
4125            if kind == Kind::Uuid {
4126                let to_text = (ctx.host_index)(HostFn::UuidToText).ok_or(WasmLowerError::Unsupported("uuid_to_text not imported"))?;
4127                let print_text = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("print_text not imported"))?;
4128                local_get(code, src as u32);
4129                code.push(0x10);
4130                leb_u32(code, to_text);
4131                code.push(0x10);
4132                leb_u32(code, print_text);
4133                return Ok(Flow::Straight);
4134            }
4135            // A wire-decoded DYNAMIC value: render its boxed `RuntimeValue` via `to_display_string`, print.
4136            if kind == Kind::Dynamic {
4137                let to_text = (ctx.host_index)(HostFn::DynamicToText).ok_or(WasmLowerError::Unsupported("dynamic_to_text not imported"))?;
4138                let print_text = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("print_text not imported"))?;
4139                local_get(code, src as u32);
4140                code.push(0x10);
4141                leb_u32(code, to_text);
4142                code.push(0x10);
4143                leb_u32(code, print_text);
4144                return Ok(Flow::Straight);
4145            }
4146            // An `Optional` handle: a null (`0`) handle prints "nothing"; otherwise the boxed inner
4147            // scalar (`box[0]`) is loaded at its own width and printed via its own sink. The present
4148            // inner kind comes from `opt_inner` (the producing channel's element kind).
4149            if kind == Kind::Optional {
4150                let nothing = (ctx.host_index)(HostFn::PrintNothing).ok_or(WasmLowerError::Unsupported("Show sink not imported"))?;
4151                let inner = plan.structs.opt_inner.get(&src).copied().unwrap_or(Kind::Int);
4152                let some_host = HostFn::for_show(inner).ok_or(WasmLowerError::Unsupported("Show of an optional with a non-scalar inner"))?;
4153                let some_idx = (ctx.host_index)(some_host).ok_or(WasmLowerError::Unsupported("Show sink not imported"))?;
4154                local_get(code, src as u32);
4155                code.push(0x45); // i32.eqz → is the handle null (Nothing)?
4156                code.push(0x04);
4157                code.push(0x40); // if (void)
4158                code.push(0x10); // call print_nothing
4159                leb_u32(code, nothing);
4160                code.push(0x05); // else (Some)
4161                local_get(code, src as u32);
4162                emit_slot_load(code, Some(inner), 0)?; // box[0] → the inner value at its width
4163                if inner == Kind::Bool {
4164                    code.push(0xA7); // i32.wrap_i64 — print_bool takes an i32
4165                }
4166                code.push(0x10); // call print_<inner>
4167                leb_u32(code, some_idx);
4168                code.push(0x0B); // end if
4169                return Ok(Flow::Straight);
4170            }
4171            // A `Word32`/`Word64` Shows as its UNSIGNED value via `print_word` — a `Word32` is
4172            // zero-extended to `i64` first so the host's `u64` reading equals the `u32` value.
4173            if kind == Kind::Word32 || kind == Kind::Word64 {
4174                let idx = (ctx.host_index)(HostFn::PrintWord).ok_or(WasmLowerError::Unsupported("Show sink not imported"))?;
4175                local_get(code, src as u32);
4176                if kind == Kind::Word32 {
4177                    code.push(0xAD); // i64.extend_i32_u
4178                }
4179                code.push(0x10); // call print_word
4180                leb_u32(code, idx);
4181                return Ok(Flow::Straight);
4182            }
4183            let host = HostFn::for_show(kind).ok_or(WasmLowerError::Unsupported("Show of a non-scalar value"))?;
4184            let idx = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("Show sink not imported"))?;
4185            local_get(code, src as u32);
4186            if kind == Kind::Bool {
4187                code.push(0xA7); // i32.wrap_i64 — print_bool takes an i32
4188            }
4189            code.push(0x10); // call print_*
4190            leb_u32(code, idx);
4191            Ok(Flow::Straight)
4192        }
4193        Op::Return { src } => {
4194            if plan.result.is_none() {
4195                return Err(WasmLowerError::Unsupported("value return from a void function"));
4196            }
4197            local_get(code, src as u32);
4198            code.push(0x0F); // return
4199            Ok(Flow::Terminated)
4200        }
4201        Op::ReturnNothing => {
4202            match plan.result {
4203                None => code.push(0x0F),    // return (void)
4204                Some(_) => code.push(0x00), // unreachable (typed function: never returns nothing)
4205            }
4206            Ok(Flow::Terminated)
4207        }
4208        Op::Halt => {
4209            code.push(0x0F); // return — Main is void
4210            Ok(Flow::Terminated)
4211        }
4212        // A runtime failure (`FailWith`, e.g. an undefined variable or an explicit fail). A
4213        // standalone module has no VM to surface the message, so it traps — the documented
4214        // error contract (the `wasm_traps_where_treewalker_errors` lock proves tw-errors ⟺
4215        // wasm-traps). The message constant is intentionally dropped.
4216        Op::FailWith { .. } => {
4217            code.push(0x00); // unreachable → trap
4218            Ok(Flow::Terminated)
4219        }
4220        other => Err(unsupported_op(&other)),
4221    }
4222}
4223
4224#[derive(Clone, Copy)]
4225enum ArithOp {
4226    Add,
4227    Sub,
4228    Mul,
4229    Div,
4230    Mod,
4231}
4232
4233/// Lower a binary arithmetic op, dispatching on the result kind: checked `i64` for `Int`
4234/// (traps on signed overflow, matching the VM's exact-int → BigInt contract), native `f64` for
4235/// `Float`.
4236fn lower_arith(code: &mut Vec<u8>, kinds: &KindTable, dst: u16, lhs: u16, rhs: u16, op: ArithOp) -> R<Flow> {
4237    match kinds.get(dst as usize) {
4238        Some(Kind::Int) => {
4239            // Integer arithmetic needs two `i64` operands; a Float operand with an `Int` result is a
4240            // kind inconsistency — reject rather than emit an `i64` op on an `f64` (invalid wasm).
4241            if kinds.valtype(lhs as usize) != I64 || kinds.valtype(rhs as usize) != I64 {
4242                return Err(WasmLowerError::Unsupported("integer arithmetic with a non-integer operand"));
4243            }
4244            match op {
4245                ArithOp::Add => emit_checked_addsub(code, false, dst, lhs, rhs),
4246                ArithOp::Sub => emit_checked_addsub(code, true, dst, lhs, rhs),
4247                ArithOp::Mul => emit_checked_mul(code, dst, lhs, rhs),
4248                ArithOp::Div => arith(code, 0x7F, dst, lhs, rhs), // i64.div_s (traps on /0, MIN/-1)
4249                ArithOp::Mod => arith(code, 0x81, dst, lhs, rhs), // i64.rem_s
4250            }
4251        }
4252        // Word arithmetic is the ℤ/2ⁿ ring: native wasm `i32`/`i64` ops WRAP by definition (no overflow
4253        // check, unlike `Int`'s checked path), and division/remainder are UNSIGNED — matching `WordVal`.
4254        Some(Kind::Word32) => {
4255            let opcode = match op {
4256                ArithOp::Add => 0x6A, // i32.add
4257                ArithOp::Sub => 0x6B, // i32.sub
4258                ArithOp::Mul => 0x6C, // i32.mul
4259                ArithOp::Div => 0x6E, // i32.div_u
4260                ArithOp::Mod => 0x70, // i32.rem_u
4261            };
4262            arith(code, opcode, dst, lhs, rhs);
4263        }
4264        Some(Kind::Word64) => {
4265            let opcode = match op {
4266                ArithOp::Add => 0x7C, // i64.add
4267                ArithOp::Sub => 0x7D, // i64.sub
4268                ArithOp::Mul => 0x7E, // i64.mul
4269                ArithOp::Div => 0x80, // i64.div_u
4270                ArithOp::Mod => 0x82, // i64.rem_u
4271            };
4272            arith(code, opcode, dst, lhs, rhs);
4273        }
4274        Some(Kind::Float) => {
4275            let opcode = match op {
4276                ArithOp::Add => 0xA0,
4277                ArithOp::Sub => 0xA1,
4278                ArithOp::Mul => 0xA2,
4279                ArithOp::Div => 0xA3,
4280                ArithOp::Mod => return Err(WasmLowerError::Unsupported("float modulo")),
4281            };
4282            // Promote an `Int` operand to `f64` (matching the tree-walker's mixed-expression
4283            // promotion), so `3 + 1.5` etc. compile instead of emitting an `f64` op on an `i64`.
4284            push_as_f64(code, lhs, kinds.get(lhs as usize))?;
4285            push_as_f64(code, rhs, kinds.get(rhs as usize))?;
4286            code.push(opcode);
4287            local_set(code, dst as u32);
4288        }
4289        // Temporal arithmetic (`Duration ± Duration = Duration`, `Moment ± Duration = Moment`) is i64
4290        // nanos that WRAP (matching the VM's `wrapping_add`/`wrapping_sub`) — no overflow check, and only
4291        // `+`/`-` (the kind arms never route `× ÷ %` here).
4292        Some(Kind::Duration) | Some(Kind::Time) | Some(Kind::Moment) => {
4293            let opcode = match op {
4294                ArithOp::Add => 0x7C, // i64.add
4295                ArithOp::Sub => 0x7D, // i64.sub
4296                _ => return Err(WasmLowerError::Unsupported("only + and - on temporal values")),
4297            };
4298            arith(code, opcode, dst, lhs, rhs);
4299        }
4300        _ => return Err(WasmLowerError::Unsupported("arithmetic on a non-numeric value")),
4301    }
4302    Ok(Flow::Straight)
4303}
4304
4305#[derive(Clone, Copy)]
4306enum Cmp {
4307    Lt,
4308    Gt,
4309    Le,
4310    Ge,
4311    Eq,
4312    Ne,
4313}
4314
4315/// Lower a comparison, dispatching on the *operand* kind (the result is always a `Bool` i64
4316/// 0/1). `i64` signed compares for `Int`, ordered `f64` compares for `Float`.
4317fn lower_compare(code: &mut Vec<u8>, kinds: &KindTable, dst: u16, lhs: u16, rhs: u16, cmp: Cmp) -> R<Flow> {
4318    let lf = kinds.valtype(lhs as usize) == F64;
4319    let rf = kinds.valtype(rhs as usize) == F64;
4320    if lf || rf {
4321        // Mixed Int/Float equality is EXACT — mathematical values (`1 equals
4322        // 1.0` is true, but 2^53+1 never equals the float 2^53). The test:
4323        // convert-both-ways equality (i→f64 rounds, f→i64 truncates — both
4324        // agreeing pins the exact value) with an upper guard at 2^63 where
4325        // the saturating truncation would alias i64::MAX. NaN fails the f64
4326        // compare; a fractional f fails the i64 compare.
4327        if lf != rf {
4328            if matches!(cmp, Cmp::Eq | Cmp::Ne) {
4329                let (int_reg, float_reg) = if lf { (rhs, lhs) } else { (lhs, rhs) };
4330                // c1: (int as f64) == f
4331                local_get(code, int_reg as u32);
4332                code.push(0xB9); // f64.convert_i64_s
4333                local_get(code, float_reg as u32);
4334                code.push(0x61); // f64.eq
4335                // c2: int == trunc_sat(f)
4336                local_get(code, int_reg as u32);
4337                local_get(code, float_reg as u32);
4338                code.push(0xFC); // saturating-truncation prefix
4339                code.push(0x06); // i64.trunc_sat_f64_s
4340                code.push(0x51); // i64.eq
4341                code.push(0x71); // i32.and
4342                // c3: f < 2^63 (above it trunc_sat aliases i64::MAX)
4343                local_get(code, float_reg as u32);
4344                code.push(0x44); // f64.const 2^63
4345                code.extend_from_slice(&9223372036854775808.0f64.to_le_bytes());
4346                code.push(0x63); // f64.lt
4347                code.push(0x71); // i32.and
4348                if matches!(cmp, Cmp::Ne) {
4349                    code.push(0x45); // i32.eqz — negate
4350                }
4351                code.push(0xAD); // i64.extend_i32_u
4352                local_set(code, dst as u32);
4353                return Ok(Flow::Straight);
4354            }
4355        }
4356        let opcode = match cmp {
4357            Cmp::Lt => 0x63, // f64.lt
4358            Cmp::Gt => 0x64, // f64.gt
4359            Cmp::Le => 0x65, // f64.le
4360            Cmp::Ge => 0x66, // f64.ge
4361            Cmp::Eq => 0x61, // f64.eq (both Float)
4362            Cmp::Ne => 0x62, // f64.ne (both Float)
4363        };
4364        push_as_f64(code, lhs, kinds.get(lhs as usize))?;
4365        push_as_f64(code, rhs, kinds.get(rhs as usize))?;
4366        code.push(opcode);
4367        code.push(0xAD); // i64.extend_i32_u — keep the VM's truthy-Int boolean width
4368        local_set(code, dst as u32);
4369        return Ok(Flow::Straight);
4370    }
4371    // `x is (not) equal to nothing` — the only comparison an `Optional` takes part in. Read the
4372    // Optional operand's i32 handle and test it against the null (`0`) handle; the other operand is
4373    // the `nothing` literal (the Int `0`), whose own width is irrelevant, so we compare against a
4374    // fresh `i32.const 0` rather than it. Ordering (`<`/`>`) on an Optional is nonsensical → rejected.
4375    let (lk, rk) = (kinds.get(lhs as usize), kinds.get(rhs as usize));
4376    if lk == Some(Kind::Optional) || rk == Some(Kind::Optional) {
4377        let opt = if lk == Some(Kind::Optional) { lhs } else { rhs };
4378        let opcode = match cmp {
4379            Cmp::Eq => 0x46, // i32.eq → handle == 0 (is nothing)
4380            Cmp::Ne => 0x47, // i32.ne → handle != 0 (is present)
4381            _ => return Err(WasmLowerError::Unsupported("ordering comparison of optional values")),
4382        };
4383        local_get(code, opt as u32);
4384        i32_const(code, 0);
4385        code.push(opcode);
4386        code.push(0xAD); // i64.extend_i32_u — the VM's truthy-Int boolean width
4387        local_set(code, dst as u32);
4388        return Ok(Flow::Straight);
4389    }
4390    let operand = kinds.get(lhs as usize).or_else(|| kinds.get(rhs as usize));
4391    match operand {
4392        // A `Char` compares by code point (`char`'s own ordering), so an `i64` compare of the
4393        // stored `char as u32` is byte-identical to the VM's `Char` comparison.
4394        Some(Kind::Int) | Some(Kind::Bool) | Some(Kind::Char) | Some(Kind::Duration) | Some(Kind::Time) | Some(Kind::Span) => {
4395            let opcode = match cmp {
4396                Cmp::Lt => 0x53, // i64.lt_s
4397                Cmp::Gt => 0x55, // i64.gt_s
4398                Cmp::Le => 0x57, // i64.le_s
4399                Cmp::Ge => 0x59, // i64.ge_s
4400                Cmp::Eq => 0x51, // i64.eq
4401                Cmp::Ne => 0x52, // i64.ne
4402            };
4403            compare(code, opcode, dst, lhs, rhs);
4404        }
4405        // Words compare by their UNSIGNED value (the ℤ/2ⁿ ring order) — `Word32` as `i32` unsigned,
4406        // `Word64` as `i64` unsigned, matching `WordVal`'s `to_u64`-based comparison.
4407        Some(Kind::Word32) => {
4408            let opcode = match cmp {
4409                Cmp::Lt => 0x49, // i32.lt_u
4410                Cmp::Gt => 0x4B, // i32.gt_u
4411                Cmp::Le => 0x4D, // i32.le_u
4412                Cmp::Ge => 0x4F, // i32.ge_u
4413                Cmp::Eq => 0x46, // i32.eq
4414                Cmp::Ne => 0x47, // i32.ne
4415            };
4416            compare(code, opcode, dst, lhs, rhs);
4417        }
4418        Some(Kind::Word64) => {
4419            let opcode = match cmp {
4420                Cmp::Lt => 0x54, // i64.lt_u
4421                Cmp::Gt => 0x56, // i64.gt_u
4422                Cmp::Le => 0x58, // i64.le_u
4423                Cmp::Ge => 0x5A, // i64.ge_u
4424                Cmp::Eq => 0x51, // i64.eq
4425                Cmp::Ne => 0x52, // i64.ne
4426            };
4427            compare(code, opcode, dst, lhs, rhs);
4428        }
4429        // A Float operand has value type `F64`, so it always took the promotion path above.
4430        Some(Kind::Float) => unreachable!("float comparison handled by the f64 promotion path"),
4431        Some(Kind::Date) | Some(Kind::Moment) => {
4432            return Err(WasmLowerError::Unsupported("comparison of temporal values"))
4433        }
4434        Some(Kind::SeqInt) | Some(Kind::SeqBool) | Some(Kind::SeqFloat) | Some(Kind::SeqText) | Some(Kind::SeqStruct) | Some(Kind::SeqEnum) | Some(Kind::SeqSeqInt) | Some(Kind::SeqAny) | Some(Kind::SeqWord32) | Some(Kind::SeqWord64) => {
4435            return Err(WasmLowerError::Unsupported("comparison of sequences"))
4436        }
4437        Some(Kind::Text) => return Err(WasmLowerError::Unsupported("comparison of text values")),
4438        Some(Kind::Struct) => return Err(WasmLowerError::Unsupported("comparison of struct values")),
4439        Some(Kind::Map) => return Err(WasmLowerError::Unsupported("comparison of map values")),
4440        Some(Kind::Set) | Some(Kind::SetText) | Some(Kind::CrdtSetText) => return Err(WasmLowerError::Unsupported("comparison of set values")),
4441        Some(Kind::Enum) => return Err(WasmLowerError::Unsupported("comparison of enum values")),
4442        Some(Kind::Closure) => return Err(WasmLowerError::Unsupported("comparison of closure values")),
4443        Some(Kind::Tuple) => return Err(WasmLowerError::Unsupported("comparison of tuple values")),
4444        Some(Kind::Rational) => return Err(WasmLowerError::Unsupported("comparison of rational values")),
4445        Some(Kind::BigInt) => return Err(WasmLowerError::Unsupported("comparison of bigint values")),
4446        Some(Kind::Complex) => return Err(WasmLowerError::Unsupported("comparison of complex values")),
4447        Some(Kind::Modular) => return Err(WasmLowerError::Unsupported("comparison of modular values")),
4448        Some(Kind::Decimal) => return Err(WasmLowerError::Unsupported("comparison of decimal values")),
4449        Some(Kind::Money) => return Err(WasmLowerError::Unsupported("comparison of money values")),
4450        Some(Kind::Quantity) => return Err(WasmLowerError::Unsupported("comparison of quantity values")),
4451        Some(Kind::Uuid) => return Err(WasmLowerError::Unsupported("ordering comparison of uuid values")),
4452        Some(Kind::LanesV) => return Err(WasmLowerError::Unsupported("comparison of lane-vector values")),
4453        Some(Kind::Dynamic) => return Err(WasmLowerError::Unsupported("comparison of dynamic wire values")),
4454        Some(Kind::Lanes) => return Err(WasmLowerError::Unsupported("comparison of lane vectors")),
4455        // An `Optional` operand is fully handled by the is-nothing special case above (either operand
4456        // being `Optional` returns early), so it never reaches this by-operand-kind dispatch.
4457        Some(Kind::Optional) => unreachable!("optional comparisons handled by the is-nothing path above"),
4458        None => return Err(WasmLowerError::Unsupported("comparison of unknown-kind values")),
4459    }
4460    Ok(Flow::Straight)
4461}
4462
4463/// Push a register as an `f64` on the wasm stack — directly for a Float, via `f64.convert_i64_s`
4464/// for an Int.
4465/// `a is approximately b` — the shared isclose semantics
4466/// (`logicaffeine_data::ops::logos_approx_eq`), lowered as pure f64
4467/// instructions so the result is bit-identical to every other engine:
4468/// `(a == b) || |a - b| <= max(1e-9 * max(|a|, |b|), 1e-12)`.
4469fn lower_approx_eq(code: &mut Vec<u8>, kinds: &KindTable, dst: u16, lhs: u16, rhs: u16) -> R<Flow> {
4470    let lk = kinds.get(lhs as usize);
4471    let rk = kinds.get(rhs as usize);
4472    // c1: a == b (the exact fast path — also makes inf ≈ inf hold).
4473    push_as_f64(code, lhs, lk)?;
4474    push_as_f64(code, rhs, rk)?;
4475    code.push(0x61); // f64.eq → i32
4476    // diff = |a - b|
4477    push_as_f64(code, lhs, lk)?;
4478    push_as_f64(code, rhs, rk)?;
4479    code.push(0xA1); // f64.sub
4480    code.push(0x99); // f64.abs
4481    // tol = max(1e-9 * max(|a|, |b|), 1e-12)
4482    push_as_f64(code, lhs, lk)?;
4483    code.push(0x99); // f64.abs
4484    push_as_f64(code, rhs, rk)?;
4485    code.push(0x99); // f64.abs
4486    code.push(0xA5); // f64.max
4487    code.push(0x44); // f64.const 1e-9
4488    code.extend_from_slice(&1e-9f64.to_le_bytes());
4489    code.push(0xA2); // f64.mul
4490    code.push(0x44); // f64.const 1e-12
4491    code.extend_from_slice(&1e-12f64.to_le_bytes());
4492    code.push(0xA5); // f64.max
4493    // c2: diff <= tol
4494    code.push(0x65); // f64.le → i32
4495    code.push(0x72); // i32.or (c1 | c2)
4496    code.push(0xAD); // i64.extend_i32_u — the VM's truthy-Int boolean width
4497    local_set(code, dst as u32);
4498    Ok(Flow::Straight)
4499}
4500
4501fn push_as_f64(code: &mut Vec<u8>, reg: u16, kind: Option<Kind>) -> R<()> {
4502    local_get(code, reg as u32);
4503    match kind {
4504        Some(Kind::Float) => {}
4505        Some(Kind::Int) => code.push(0xB9), // f64.convert_i64_s
4506        _ => return Err(WasmLowerError::Unsupported("numeric builtin on a non-number")),
4507    }
4508    Ok(())
4509}
4510
4511/// Lower a numeric builtin call, bit-exactly matching the VM (`semantics/builtins.rs`).
4512fn lower_builtin(code: &mut Vec<u8>, kinds: &KindTable, dst: u16, builtin: BuiltinId, args_start: u16, arg_count: u16) -> R<Flow> {
4513    let arg = args_start;
4514    let ak = kinds.get(arg as usize);
4515    match builtin {
4516        // `sqrt` → Float (Int converts first), matching `(n as f64).sqrt()`.
4517        BuiltinId::Sqrt => {
4518            push_as_f64(code, arg, ak)?;
4519            code.push(0x9F); // f64.sqrt
4520            local_set(code, dst as u32);
4521            Ok(Flow::Straight)
4522        }
4523        // `floor`/`ceil` → Int via the SATURATING truncation (matches `f.floor() as i64`), or the
4524        // identity on an already-whole Int. (A LINKED `Rational` arg is handled in the main `lower_op`
4525        // match, which has the `ctx` host table — the exact `logos_rt_rational_*` rounding.)
4526        BuiltinId::Floor => lower_floor_ceil(code, ak, dst, arg, 0x9C), // f64.floor
4527        BuiltinId::Ceil => lower_floor_ceil(code, ak, dst, arg, 0x9B),  // f64.ceil
4528        // `round` → round-half-AWAY-from-zero (Rust's `f64::round`, NOT wasm's round-half-even):
4529        // `trunc(x + copysign(0.5, x))`, then the saturating cast. Identity on a whole Int.
4530        BuiltinId::Round => match ak {
4531            Some(Kind::Float) => {
4532                local_get(code, arg as u32);
4533                code.push(0x44); // f64.const 0.5
4534                code.extend_from_slice(&0.5f64.to_le_bytes());
4535                local_get(code, arg as u32);
4536                code.push(0xA6); // f64.copysign → 0.5 with x's sign
4537                code.push(0xA0); // f64.add → x + copysign(0.5, x)
4538                code.push(0x9D); // f64.trunc
4539                code.push(0xFC); // saturating-truncation prefix
4540                leb_u32(code, 6); // i64.trunc_sat_f64_s
4541                local_set(code, dst as u32);
4542                Ok(Flow::Straight)
4543            }
4544            Some(Kind::Int) => {
4545                local_get(code, arg as u32);
4546                local_set(code, dst as u32);
4547                Ok(Flow::Straight)
4548            }
4549            _ => Err(WasmLowerError::Unsupported("round of a non-number")),
4550        },
4551        // `abs`: f64.abs for a Float; for an Int, `x < 0 ? -x : x` with an i64::MIN overflow trap
4552        // (|i64::MIN| does not fit i64 — the VM promotes to BigInt; the standalone module traps,
4553        // matching the checked-arithmetic contract).
4554        BuiltinId::Abs => match ak {
4555            Some(Kind::Float) => {
4556                local_get(code, arg as u32);
4557                code.push(0x99); // f64.abs
4558                local_set(code, dst as u32);
4559                Ok(Flow::Straight)
4560            }
4561            Some(Kind::Int) => {
4562                // trap if arg == i64::MIN
4563                local_get(code, arg as u32);
4564                code.push(0x42); // i64.const i64::MIN
4565                leb_i64(code, i64::MIN);
4566                code.push(0x51); // i64.eq
4567                code.push(0x04);
4568                code.push(0x40); // if (void)
4569                code.push(0x00); // unreachable
4570                code.push(0x0B); // end
4571                // x < 0 ? -x : x
4572                code.push(0x42);
4573                leb_i64(code, 0); // i64.const 0
4574                local_get(code, arg as u32);
4575                code.push(0x7D); // i64.sub → -x  (value-if-true)
4576                local_get(code, arg as u32); // x  (value-if-false)
4577                local_get(code, arg as u32);
4578                code.push(0x42);
4579                leb_i64(code, 0);
4580                code.push(0x53); // i64.lt_s → x < 0 (selector)
4581                code.push(0x1B); // select
4582                local_set(code, dst as u32);
4583                Ok(Flow::Straight)
4584            }
4585            _ => Err(WasmLowerError::Unsupported("abs of a non-number")),
4586        },
4587        BuiltinId::Min => lower_minmax(code, kinds, dst, args_start, arg_count, false),
4588        BuiltinId::Max => lower_minmax(code, kinds, dst, args_start, arg_count, true),
4589        // `count_ones(n)` → the population count of the Int's u64 bit pattern (`i64.popcnt`), an Int.
4590        // Matches the VM's `(n as u64).count_ones() as i64`.
4591        BuiltinId::CountOnes => {
4592            local_get(code, arg as u32);
4593            code.push(0x7B); // i64.popcnt
4594            local_set(code, dst as u32);
4595            Ok(Flow::Straight)
4596        }
4597        // ── Word ring (ℤ/2ⁿ) construct / extract ──
4598        // `word32(n)` = the low 32 bits of the Int (`i32.wrap_i64`); `word64(n)` = the Int's bits
4599        // unchanged (an `i64` IS the u64 representation).
4600        BuiltinId::Word32 => {
4601            local_get(code, arg as u32);
4602            code.push(0xA7); // i32.wrap_i64
4603            local_set(code, dst as u32);
4604            Ok(Flow::Straight)
4605        }
4606        BuiltinId::Word64 => {
4607            local_get(code, arg as u32);
4608            local_set(code, dst as u32);
4609            Ok(Flow::Straight)
4610        }
4611        // `intOfWord32(w)` = zero-extend the u32 to Int (`i64.extend_i32_u`); `intOfWord64(w)` = the
4612        // 64-bit pattern unchanged (Int is i64).
4613        BuiltinId::IntOfWord32 => {
4614            local_get(code, arg as u32);
4615            code.push(0xAD); // i64.extend_i32_u
4616            local_set(code, dst as u32);
4617            Ok(Flow::Straight)
4618        }
4619        BuiltinId::IntOfWord64 => {
4620            local_get(code, arg as u32);
4621            local_set(code, dst as u32);
4622            Ok(Flow::Straight)
4623        }
4624        // ── Word rotate (`rotl`/`rotr`) — native `i32.rotl`/`i64.rotl` etc., dispatched on the word
4625        //    operand's width. The rotate count (an Int) narrows to `i32` for a `Word32`. ──
4626        BuiltinId::Rotl | BuiltinId::Rotr => {
4627            let is_l = matches!(builtin, BuiltinId::Rotl);
4628            match kinds.get(args_start as usize) {
4629                Some(Kind::Word32) => {
4630                    local_get(code, args_start as u32);
4631                    local_get(code, (args_start + 1) as u32);
4632                    code.push(0xA7); // i32.wrap_i64 — count as i32
4633                    code.push(if is_l { 0x77 } else { 0x78 }); // i32.rotl / i32.rotr
4634                }
4635                Some(Kind::Word64) => {
4636                    local_get(code, args_start as u32);
4637                    local_get(code, (args_start + 1) as u32);
4638                    code.push(if is_l { 0x89 } else { 0x8A }); // i64.rotl / i64.rotr
4639                }
4640                _ => return Err(WasmLowerError::Unsupported("rotate of a non-Word value")),
4641            }
4642            local_set(code, dst as u32);
4643            Ok(Flow::Straight)
4644        }
4645        // ── Word bitwise `word_and`/`word_or`/`word_not` — native and/or/xor, dispatched on width. ──
4646        BuiltinId::Wand | BuiltinId::Wor => {
4647            let is_and = matches!(builtin, BuiltinId::Wand);
4648            let op32 = if is_and { 0x71 } else { 0x72 }; // i32.and / i32.or
4649            let op64 = if is_and { 0x83 } else { 0x84 }; // i64.and / i64.or
4650            match kinds.get(args_start as usize) {
4651                Some(Kind::Word32) => {
4652                    local_get(code, args_start as u32);
4653                    local_get(code, (args_start + 1) as u32);
4654                    code.push(op32);
4655                }
4656                Some(Kind::Word64) => {
4657                    local_get(code, args_start as u32);
4658                    local_get(code, (args_start + 1) as u32);
4659                    code.push(op64);
4660                }
4661                _ => return Err(WasmLowerError::Unsupported("word_and/or of a non-Word value")),
4662            }
4663            local_set(code, dst as u32);
4664            Ok(Flow::Straight)
4665        }
4666        // `word_not(w)` = XOR with all-ones (wasm has no `i32.not`).
4667        BuiltinId::Wnot => {
4668            match kinds.get(arg as usize) {
4669                Some(Kind::Word32) => {
4670                    local_get(code, arg as u32);
4671                    i32_const(code, -1);
4672                    code.push(0x73); // i32.xor
4673                }
4674                Some(Kind::Word64) => {
4675                    local_get(code, arg as u32);
4676                    i64c(code, -1);
4677                    code.push(0x85); // i64.xor
4678                }
4679                _ => return Err(WasmLowerError::Unsupported("word_not of a non-Word value")),
4680            }
4681            local_set(code, dst as u32);
4682            Ok(Flow::Straight)
4683        }
4684        // ── `word64Shl`/`word64Shr`/`word64And` — the Word64 shift/mask primitives (Keccak). ──
4685        BuiltinId::Word64Shl | BuiltinId::Word64Shr | BuiltinId::Word64And => {
4686            local_get(code, args_start as u32);
4687            local_get(code, (args_start + 1) as u32);
4688            code.push(match builtin {
4689                BuiltinId::Word64Shl => 0x86, // i64.shl
4690                BuiltinId::Word64Shr => 0x88, // i64.shr_u (Word64 is unsigned)
4691                _ => 0x83,                    // i64.and
4692            });
4693            local_set(code, dst as u32);
4694            Ok(Flow::Straight)
4695        }
4696        // ── `word32Shr` — logical right-shift of a Word32 (SHA-256 `σ0`/`σ1`). The shift amount is an
4697        //    Int (i64) so it narrows to i32; `i32.shr_u` is unsigned, so the vacated high bits are 0. ──
4698        BuiltinId::Word32Shr => {
4699            local_get(code, args_start as u32);
4700            local_get(code, (args_start + 1) as u32);
4701            code.push(0xA7); // i32.wrap_i64 — shift amount as i32
4702            code.push(0x76); // i32.shr_u
4703            local_set(code, dst as u32);
4704            Ok(Flow::Straight)
4705        }
4706        _ => Err(WasmLowerError::Unsupported("builtin not yet lowered")),
4707    }
4708}
4709
4710/// `floor`/`ceil` (`fop` = `f64.floor`/`f64.ceil`): a Float rounds then truncates with the
4711/// saturating cast to match the VM's `as i64`; an already-whole Int is returned unchanged.
4712fn lower_floor_ceil(code: &mut Vec<u8>, arg_kind: Option<Kind>, dst: u16, arg: u16, fop: u8) -> R<Flow> {
4713    match arg_kind {
4714        Some(Kind::Float) => {
4715            local_get(code, arg as u32);
4716            code.push(fop); // f64.floor / f64.ceil
4717            code.push(0xFC); // saturating-truncation prefix
4718            leb_u32(code, 6); // i64.trunc_sat_f64_s
4719            local_set(code, dst as u32);
4720        }
4721        Some(Kind::Int) => {
4722            local_get(code, arg as u32);
4723            local_set(code, dst as u32);
4724        }
4725        _ => return Err(WasmLowerError::Unsupported("floor/ceil of a non-number")),
4726    }
4727    Ok(Flow::Straight)
4728}
4729
4730/// `min`/`max` (`is_max` selects max). `Int×Int` is an `i64` compare + `select`. Any Float
4731/// operand promotes both to `f64` and uses `f64.min`/`f64.max` — but with explicit NaN guards,
4732/// because Rust's `f64::min`/`max` return the NON-NaN argument whereas raw `f64.min`/`f64.max`
4733/// return NaN. The `±0` case already agrees between the two.
4734fn lower_minmax(code: &mut Vec<u8>, kinds: &KindTable, dst: u16, args_start: u16, arg_count: u16, is_max: bool) -> R<Flow> {
4735    if arg_count != 2 {
4736        return Err(WasmLowerError::Unsupported("min/max arity"));
4737    }
4738    let (a, b) = (args_start, args_start + 1);
4739    let (ak, bk) = (kinds.get(a as usize), kinds.get(b as usize));
4740    match (ak, bk) {
4741        (Some(Kind::Int), Some(Kind::Int)) => {
4742            local_get(code, a as u32); // value-if-true
4743            local_get(code, b as u32); // value-if-false
4744            local_get(code, a as u32);
4745            local_get(code, b as u32);
4746            code.push(if is_max { 0x55 } else { 0x53 }); // i64.gt_s / i64.lt_s
4747            code.push(0x1B); // select
4748            local_set(code, dst as u32);
4749            Ok(Flow::Straight)
4750        }
4751        (a_some, b_some) if a_some.is_some() && b_some.is_some() => {
4752            let fop = if is_max { 0x98 } else { 0x97 }; // f64.max / f64.min
4753            // result = a_nan ? b_f : (b_nan ? a_f : fop(a_f, b_f))
4754            push_as_f64(code, b, bk)?; // [b_f]              (value-if-true for the outer select)
4755            push_as_f64(code, a, ak)?; // [b_f, a_f]         (value-if-true for the inner select)
4756            push_as_f64(code, a, ak)?;
4757            push_as_f64(code, b, bk)?;
4758            code.push(fop); // [b_f, a_f, fop(a_f,b_f)]      (value-if-false for the inner select)
4759            push_as_f64(code, b, bk)?;
4760            push_as_f64(code, b, bk)?;
4761            code.push(0x62); // f64.ne → b_nan               (inner selector)
4762            code.push(0x1B); // select → [b_f, inner]
4763            push_as_f64(code, a, ak)?;
4764            push_as_f64(code, a, ak)?;
4765            code.push(0x62); // f64.ne → a_nan                (outer selector)
4766            code.push(0x1B); // select → [result]
4767            local_set(code, dst as u32);
4768            Ok(Flow::Straight)
4769        }
4770        _ => Err(WasmLowerError::Unsupported("min/max of non-numbers")),
4771    }
4772}
4773
4774/// The host `pow` import a `pow(base, exp)` needs, by operand kinds: any `^Float` uses
4775/// `pow_ff` (`f64::powf`); `Float^Int` uses `pow_fi` (`f64::powi`); `Int^Int` uses neither (an
4776/// integer exponentiation-by-squaring loop, [`lower_int_pow`]).
4777fn pow_host_for(base: Option<Kind>, exp: Option<Kind>) -> Option<HostFn> {
4778    match (base, exp) {
4779        (_, Some(Kind::Float)) => Some(HostFn::PowFf),
4780        (Some(Kind::Float), Some(Kind::Int)) => Some(HostFn::PowFi),
4781        _ => None,
4782    }
4783}
4784
4785/// Lower `pow(base, exp)` bit-exactly. Float-result cases (any Float operand) defer to the host
4786/// `pow_ff`/`pow_fi` (so `powf` vs `powi` exactly match the VM). `Int^Int` is computed in-module
4787/// by [`lower_int_pow`] (Int result, overflow-trapping). A negative Int exponent (VM → Float)
4788/// traps, since a Float cannot ride the Int result slot.
4789fn lower_pow(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, args_start: u16, arg_count: u16) -> R<Flow> {
4790    if arg_count != 2 {
4791        return Err(WasmLowerError::Unsupported("pow arity"));
4792    }
4793    lower_pow_regs(code, kinds, ctx, num_regs, dst, args_start, args_start + 1)
4794}
4795
4796/// Shared core of `pow(base, exp)` and the `**` operator (`Op::Pow`): the two operands are given as
4797/// explicit registers (adjacent for the builtin, arbitrary for the operator). Float-result cases
4798/// defer to the host `pow_ff`/`pow_fi`; `Int^Int` is the in-module overflow-trapping squaring loop.
4799/// Push an i32 `BigInt` handle for `reg` onto the wasm stack: a `Kind::BigInt` register pushes its
4800/// handle directly; an `Int` register is promoted with `logos_rt_bigint_from_i64`. Brings a mixed
4801/// `BigInt <op> Int` operand to a common handle type before a `logos_rt_bigint_*` call.
4802fn push_bigint_operand(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, reg: u16) -> R<()> {
4803    match kinds.get(reg as usize) {
4804        Some(Kind::BigInt) => local_get(code, reg as u32),
4805        Some(Kind::Int) => {
4806            let from_i64 = (ctx.host_index)(HostFn::BigintFromI64).ok_or(WasmLowerError::Unsupported("bigint_from_i64 not imported"))?;
4807            local_get(code, reg as u32);
4808            code.push(0x10);
4809            leb_u32(code, from_i64);
4810        }
4811        _ => return Err(WasmLowerError::Unsupported("bigint arithmetic operand must be a BigInt or an Int")),
4812    }
4813    Ok(())
4814}
4815
4816/// `dst = lhs <op> rhs` as an exact big-integer binary operation (linker mode) — `op` is the
4817/// `logos_rt_bigint_{mul,add,sub}` sink. Both operands are brought to BigInt handles (an `Int` operand
4818/// promoted via `from_i64`); the resulting handle binds `dst`.
4819fn lower_bigint_binop(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, dst: u16, lhs: u16, rhs: u16, host: HostFn) -> R<Flow> {
4820    let op = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("bigint op not imported"))?;
4821    push_bigint_operand(code, kinds, ctx, lhs)?;
4822    push_bigint_operand(code, kinds, ctx, rhs)?;
4823    code.push(0x10);
4824    leb_u32(code, op);
4825    local_set(code, dst as u32);
4826    Ok(Flow::Straight)
4827}
4828
4829/// Push an i32 `Complex` handle for `reg`: a `Kind::Complex` register pushes its handle directly; an
4830/// `Int` operand `n` promotes to the real Complex `n + 0i` via `logos_rt_complex_from_i64(n, 0)`.
4831fn push_complex_operand(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, reg: u16) -> R<()> {
4832    match kinds.get(reg as usize) {
4833        Some(Kind::Complex) => local_get(code, reg as u32),
4834        Some(Kind::Int) => {
4835            let from = (ctx.host_index)(HostFn::ComplexFromI64).ok_or(WasmLowerError::Unsupported("complex_from_i64 not imported"))?;
4836            local_get(code, reg as u32);
4837            code.push(0x42); // i64.const 0 — the imaginary part of a promoted real
4838            leb_i64(code, 0);
4839            code.push(0x10);
4840            leb_u32(code, from);
4841        }
4842        _ => return Err(WasmLowerError::Unsupported("complex arithmetic operand must be a Complex or an Int")),
4843    }
4844    Ok(())
4845}
4846
4847/// `dst = lhs <op> rhs` as exact complex arithmetic (linker mode) — `op` is the
4848/// `logos_rt_complex_{add,sub,mul}` sink; both operands become Complex handles (an `Int` promoted).
4849fn lower_complex_binop(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, dst: u16, lhs: u16, rhs: u16, host: HostFn) -> R<Flow> {
4850    let op = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("complex op not imported"))?;
4851    push_complex_operand(code, kinds, ctx, lhs)?;
4852    push_complex_operand(code, kinds, ctx, rhs)?;
4853    code.push(0x10);
4854    leb_u32(code, op);
4855    local_set(code, dst as u32);
4856    Ok(Flow::Straight)
4857}
4858
4859/// Push an i32 `Modular` handle for `reg` (a `Kind::Modular` register). An `Int` operand can NOT be
4860/// promoted (the ring modulus is unknown), so a non-Modular operand is soundly refused.
4861fn push_modular_operand(code: &mut Vec<u8>, kinds: &KindTable, reg: u16) -> R<()> {
4862    match kinds.get(reg as usize) {
4863        Some(Kind::Modular) => local_get(code, reg as u32),
4864        _ => return Err(WasmLowerError::Unsupported("modular arithmetic operand must be a Modular")),
4865    }
4866    Ok(())
4867}
4868
4869/// `dst = lhs <op> rhs` as exact ℤ/nℤ arithmetic (linker mode) — `op` is the `logos_rt_modular_*` sink.
4870fn lower_modular_binop(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, dst: u16, lhs: u16, rhs: u16, host: HostFn) -> R<Flow> {
4871    let op = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("modular op not imported"))?;
4872    push_modular_operand(code, kinds, lhs)?;
4873    push_modular_operand(code, kinds, rhs)?;
4874    code.push(0x10);
4875    leb_u32(code, op);
4876    local_set(code, dst as u32);
4877    Ok(Flow::Straight)
4878}
4879
4880/// Push an i32 `Decimal` handle for `reg`: a `Kind::Decimal` register pushes its handle; an `Int`
4881/// operand `n` promotes to the exact Decimal `n` via `logos_rt_decimal_from_i64(n)` (`price * 3`).
4882fn push_decimal_operand(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, reg: u16) -> R<()> {
4883    match kinds.get(reg as usize) {
4884        Some(Kind::Decimal) => local_get(code, reg as u32),
4885        Some(Kind::Int) => {
4886            let from = (ctx.host_index)(HostFn::DecimalFromI64).ok_or(WasmLowerError::Unsupported("decimal_from_i64 not imported"))?;
4887            local_get(code, reg as u32);
4888            code.push(0x10);
4889            leb_u32(code, from);
4890        }
4891        _ => return Err(WasmLowerError::Unsupported("decimal arithmetic operand must be a Decimal or an Int")),
4892    }
4893    Ok(())
4894}
4895
4896/// `dst = lhs <op> rhs` as exact base-10 arithmetic (linker mode) — `op` is the `logos_rt_decimal_*` sink.
4897fn lower_decimal_binop(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, dst: u16, lhs: u16, rhs: u16, host: HostFn) -> R<Flow> {
4898    let op = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("decimal op not imported"))?;
4899    push_decimal_operand(code, kinds, ctx, lhs)?;
4900    push_decimal_operand(code, kinds, ctx, rhs)?;
4901    code.push(0x10);
4902    leb_u32(code, op);
4903    local_set(code, dst as u32);
4904    Ok(Flow::Straight)
4905}
4906
4907fn push_money_operand(code: &mut Vec<u8>, kinds: &KindTable, _ctx: &Ctx, reg: u16) -> R<()> {
4908    match kinds.get(reg as usize) {
4909        Some(Kind::Money) => local_get(code, reg as u32),
4910        _ => return Err(WasmLowerError::Unsupported("money arithmetic operand must be a Money value")),
4911    }
4912    Ok(())
4913}
4914
4915/// `dst = lhs <op> rhs` as exact currency arithmetic (linker mode) — `op` is the `logos_rt_money_*` sink.
4916fn lower_money_binop(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, dst: u16, lhs: u16, rhs: u16, host: HostFn) -> R<Flow> {
4917    let op = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("money op not imported"))?;
4918    push_money_operand(code, kinds, ctx, lhs)?;
4919    push_money_operand(code, kinds, ctx, rhs)?;
4920    code.push(0x10);
4921    leb_u32(code, op);
4922    local_set(code, dst as u32);
4923    Ok(Flow::Straight)
4924}
4925
4926fn push_quantity_operand(code: &mut Vec<u8>, kinds: &KindTable, _ctx: &Ctx, reg: u16) -> R<()> {
4927    match kinds.get(reg as usize) {
4928        Some(Kind::Quantity) => local_get(code, reg as u32),
4929        // Scalar-scaling a Quantity by a bare number (`q * 2`) has no linked runtime sink yet — refuse
4930        // it cleanly rather than pass a scalar where the runtime expects a Quantity handle.
4931        _ => return Err(WasmLowerError::Unsupported("quantity arithmetic operand must be a Quantity value")),
4932    }
4933    Ok(())
4934}
4935
4936/// `dst = lhs <op> rhs` as exact dimensional arithmetic (linker mode) — `op` is the `logos_rt_quantity_*`
4937/// sink. `+`/`-` keep the left display unit; `×`/`÷` combine dimensions and render in SI/dimension form.
4938fn lower_quantity_binop(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, dst: u16, lhs: u16, rhs: u16, host: HostFn) -> R<Flow> {
4939    let op = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("quantity op not imported"))?;
4940    push_quantity_operand(code, kinds, ctx, lhs)?;
4941    push_quantity_operand(code, kinds, ctx, rhs)?;
4942    code.push(0x10);
4943    leb_u32(code, op);
4944    local_set(code, dst as u32);
4945    Ok(Flow::Straight)
4946}
4947
4948/// Push a Rational operand as a `logos_rt_rational` handle: a Rational rides as-is, an Int widens via
4949/// `from_i64`, a BigInt via `from_bigint` (matching the VM's `rat_of` view of every exact number as a
4950/// Rational). Any other operand is refused (a Rational never mixes with a Float or a heap value).
4951fn push_rational_operand(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, reg: u16) -> R<()> {
4952    match kinds.get(reg as usize) {
4953        Some(Kind::Rational) => local_get(code, reg as u32),
4954        Some(Kind::Int) => {
4955            let from = (ctx.host_index)(HostFn::RationalFromI64).ok_or(WasmLowerError::Unsupported("rational_from_i64 not imported"))?;
4956            local_get(code, reg as u32);
4957            code.push(0x10);
4958            leb_u32(code, from);
4959        }
4960        Some(Kind::BigInt) => {
4961            let from = (ctx.host_index)(HostFn::RationalFromBigint).ok_or(WasmLowerError::Unsupported("rational_from_bigint not imported"))?;
4962            local_get(code, reg as u32);
4963            code.push(0x10);
4964            leb_u32(code, from);
4965        }
4966        _ => return Err(WasmLowerError::Unsupported("rational arithmetic operand must be a Rational, Int, or BigInt")),
4967    }
4968    Ok(())
4969}
4970
4971/// `dst = lhs <op> rhs` as exact BigInt-backed rational arithmetic (linker mode) — `op` is the
4972/// `logos_rt_rational_*` sink. Operands promote to Rational handles first, so num/den stay exact past i64.
4973fn lower_rational_binop(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, dst: u16, lhs: u16, rhs: u16, host: HostFn) -> R<Flow> {
4974    let op = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("rational op not imported"))?;
4975    push_rational_operand(code, kinds, ctx, lhs)?;
4976    push_rational_operand(code, kinds, ctx, rhs)?;
4977    code.push(0x10);
4978    leb_u32(code, op);
4979    local_set(code, dst as u32);
4980    Ok(Flow::Straight)
4981}
4982
4983/// `dst = <op>(arg)` on a Rational handle (linker mode) — `op` is a unary `logos_rt_rational_*` sink
4984/// (`floor`/`ceil`/`round` returning a BigInt handle, `abs` a Rational handle).
4985fn lower_rational_unary(code: &mut Vec<u8>, ctx: &Ctx, dst: u16, arg: u16, host: HostFn) -> R<Flow> {
4986    let op = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("rational unary op not imported"))?;
4987    local_get(code, arg as u32);
4988    code.push(0x10);
4989    leb_u32(code, op);
4990    local_set(code, dst as u32);
4991    Ok(Flow::Straight)
4992}
4993
4994/// `dst = (lhs == rhs)` (or `!=` when `negate`) on two Uuid handles (linker mode) — `logos_rt_uuid_eq`
4995/// compares the 16 bytes and returns an i32 0/1, extended to the i64 a `Bool` register holds (matching
4996/// [`lower_text_eq`]'s i64-boolean convention).
4997fn lower_uuid_eq(code: &mut Vec<u8>, ctx: &Ctx, dst: u16, lhs: u16, rhs: u16, negate: bool) -> R<Flow> {
4998    let eq = (ctx.host_index)(HostFn::UuidEq).ok_or(WasmLowerError::Unsupported("uuid_eq not imported"))?;
4999    local_get(code, lhs as u32);
5000    local_get(code, rhs as u32);
5001    code.push(0x10);
5002    leb_u32(code, eq); // → i32 0/1
5003    if negate {
5004        code.push(0x45); // i32.eqz → logical NOT
5005    }
5006    code.push(0xAD); // i64.extend_i32_u → the i64 a Bool register holds
5007    local_set(code, dst as u32);
5008    Ok(Flow::Straight)
5009}
5010
5011/// `dst = base <±> span` calendar arithmetic (linker mode): unpack the `Span` i64 (`months` high word,
5012/// `days` low word), negate for subtraction, then call `logos_rt_moment_add_span` (Moment base, i64) or
5013/// `logos_rt_date_add_span` (Date base, i32). `base`/`dst` share the base's width (Moment i64 / Date i32).
5014fn lower_span_add(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, base: u16, span: u16, is_date: bool, negate: bool) -> R<Flow> {
5015    let host = if is_date { HostFn::DateAddSpan } else { HostFn::MomentAddSpan };
5016    let idx = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("span add not imported"))?;
5017    let (months, days) = (num_regs + 5, num_regs + 6);
5018    // months = (span >> 32) as i32 (arithmetic — a Span's months can be negative)
5019    local_get(code, span as u32);
5020    code.push(0x42);
5021    leb_i64(code, 32);
5022    code.push(0x87); // i64.shr_s
5023    code.push(0xA7); // i32.wrap_i64
5024    local_set(code, months);
5025    // days = span as i32 (low word)
5026    local_get(code, span as u32);
5027    code.push(0xA7); // i32.wrap_i64
5028    local_set(code, days);
5029    if negate {
5030        for r in [months, days] {
5031            i32_const(code, 0);
5032            local_get(code, r);
5033            code.push(0x6B); // i32.sub → 0 - r
5034            local_set(code, r);
5035        }
5036    }
5037    // dst = host(base, months, days)
5038    local_get(code, base as u32);
5039    local_get(code, months);
5040    local_get(code, days);
5041    code.push(0x10);
5042    leb_u32(code, idx);
5043    local_set(code, dst as u32);
5044    Ok(Flow::Straight)
5045}
5046
5047fn lower_pow_regs(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, base: u16, exp: u16) -> R<Flow> {
5048    let (bk, ek) = (kinds.get(base as usize), kinds.get(exp as usize));
5049    match (bk, ek) {
5050        (_, Some(Kind::Float)) => {
5051            push_as_f64(code, base, bk)?;
5052            local_get(code, exp as u32); // exp is already f64
5053            let idx = (ctx.host_index)(HostFn::PowFf).ok_or(WasmLowerError::Unsupported("pow_ff not imported"))?;
5054            code.push(0x10);
5055            leb_u32(code, idx);
5056            local_set(code, dst as u32);
5057            Ok(Flow::Straight)
5058        }
5059        (Some(Kind::Float), Some(Kind::Int)) => {
5060            local_get(code, base as u32);
5061            local_get(code, exp as u32);
5062            let idx = (ctx.host_index)(HostFn::PowFi).ok_or(WasmLowerError::Unsupported("pow_fi not imported"))?;
5063            code.push(0x10);
5064            leb_u32(code, idx);
5065            local_set(code, dst as u32);
5066            Ok(Flow::Straight)
5067        }
5068        (Some(Kind::Int), Some(Kind::Int)) if ctx.linked => {
5069            // LINKER MODE: compute the exact big integer via the real `logicaffeine_base::BigInt`
5070            // runtime — `from_i64(base)` → `pow(handle, exp)` — leaving a BigInt HANDLE in `dst`. No
5071            // overflow, no trap. The handle stays a handle (rendered to a decimal `Text` only at `Show`,
5072            // via `lower_show`), so a downstream `*` keeps multiplying on real BigInts.
5073            let from_i64 = (ctx.host_index)(HostFn::BigintFromI64).ok_or(WasmLowerError::Unsupported("bigint_from_i64 not imported"))?;
5074            let pow = (ctx.host_index)(HostFn::BigintPow).ok_or(WasmLowerError::Unsupported("bigint_pow not imported"))?;
5075            local_get(code, base as u32); // i64 base
5076            code.push(0x10);
5077            leb_u32(code, from_i64); // -> i32 BigInt handle
5078            local_get(code, exp as u32); // i64 exponent
5079            code.push(0x10);
5080            leb_u32(code, pow); // (handle, exp) -> i32 BigInt handle
5081            local_set(code, dst as u32);
5082            Ok(Flow::Straight)
5083        }
5084        (Some(Kind::Int), Some(Kind::Int)) => {
5085            lower_int_pow(code, num_regs, dst, base, exp);
5086            Ok(Flow::Straight)
5087        }
5088        _ => Err(WasmLowerError::Unsupported("pow of non-numbers")),
5089    }
5090}
5091
5092/// `a // b` — floor division (toward negative infinity). `Int`: `i64.div_s` truncates toward
5093/// zero, so correct by one when the remainder is nonzero AND the operands differ in sign —
5094/// `q - ((r != 0) & ((r ^ b) < 0))`, borrowing one pow i64 scratch (`num_regs+1`) for `r`.
5095/// `i64.div_s`/`rem_s` trap on `/0` and `i64::MIN // -1` (the BigInt-promotion frontier,
5096/// matching `Op::Div`). `Float`: `f64.floor(a / b)`, promoting an Int operand. `Word`: unsigned
5097/// `div_u` (floor == truncation on non-negative). Mirrors `arith::floor_divide`.
5098fn lower_floordiv_regs(code: &mut Vec<u8>, kinds: &KindTable, num_regs: u32, dst: u16, lhs: u16, rhs: u16) -> R<Flow> {
5099    match kinds.get(dst as usize) {
5100        Some(Kind::Int) => {
5101            if kinds.valtype(lhs as usize) != I64 || kinds.valtype(rhs as usize) != I64 {
5102                return Err(WasmLowerError::Unsupported("integer floor division with a non-integer operand"));
5103            }
5104            let s_r = num_regs + 1; // borrow a pow i64 scratch to hold the remainder
5105            // s_r = a % b   (rem_s traps on b == 0)
5106            local_get(code, lhs as u32);
5107            local_get(code, rhs as u32);
5108            code.push(0x81); // i64.rem_s
5109            local_set(code, s_r);
5110            // q = a / b     (div_s traps on b == 0 and i64::MIN / -1)
5111            local_get(code, lhs as u32);
5112            local_get(code, rhs as u32);
5113            code.push(0x7F); // i64.div_s
5114            // corr = (r != 0) & ((r ^ b) < 0)  → 1 when the truncated quotient overshot
5115            local_get(code, s_r);
5116            code.push(0x42);
5117            leb_i64(code, 0); // i64.const 0
5118            code.push(0x52); // i64.ne → i32
5119            local_get(code, s_r);
5120            local_get(code, rhs as u32);
5121            code.push(0x85); // i64.xor
5122            code.push(0x42);
5123            leb_i64(code, 0); // i64.const 0
5124            code.push(0x53); // i64.lt_s → i32
5125            code.push(0x71); // i32.and
5126            code.push(0xAD); // i64.extend_i32_u → i64 corr
5127            code.push(0x7D); // i64.sub → q - corr
5128            local_set(code, dst as u32);
5129        }
5130        Some(Kind::Word32) => arith(code, 0x6E, dst, lhs, rhs), // i32.div_u
5131        Some(Kind::Word64) => arith(code, 0x80, dst, lhs, rhs), // i64.div_u
5132        Some(Kind::Float) => {
5133            push_as_f64(code, lhs, kinds.get(lhs as usize))?;
5134            push_as_f64(code, rhs, kinds.get(rhs as usize))?;
5135            code.push(0xA3); // f64.div
5136            code.push(0x9C); // f64.floor
5137            local_set(code, dst as u32);
5138        }
5139        _ => return Err(WasmLowerError::Unsupported("floor division on a non-numeric value")),
5140    }
5141    Ok(Flow::Straight)
5142}
5143
5144/// `base^exp` for two Ints, by exponentiation-by-squaring, using three reserved scratch locals
5145/// (`num_regs+1..=num_regs+3`). Each multiply is overflow-checked (traps → matches the VM's
5146/// promote-to-BigInt contract). A negative exponent traps (the VM yields a Float).
5147fn lower_int_pow(code: &mut Vec<u8>, num_regs: u32, dst: u16, base: u16, exp: u16) {
5148    let result = (num_regs + 1) as u16;
5149    let base_s = (num_regs + 2) as u16;
5150    let exp_s = (num_regs + 3) as u16;
5151    // A product temp distinct from result/base_s — `emit_checked_mul` writes its dst BEFORE
5152    // re-reading lhs for the overflow check, so dst must not alias lhs (else the check sees the
5153    // product and falsely traps).
5154    let tmp = (num_regs + 4) as u16;
5155    // if exp < 0: trap
5156    local_get(code, exp as u32);
5157    code.push(0x42);
5158    leb_i64(code, 0);
5159    code.push(0x53); // i64.lt_s
5160    code.push(0x04);
5161    code.push(0x40); // if
5162    code.push(0x00); // unreachable
5163    code.push(0x0B); // end
5164    // result = 1; base_s = base; exp_s = exp
5165    code.push(0x42);
5166    leb_i64(code, 1);
5167    local_set(code, result as u32);
5168    local_get(code, base as u32);
5169    local_set(code, base_s as u32);
5170    local_get(code, exp as u32);
5171    local_set(code, exp_s as u32);
5172    // block $exit { loop $loop {
5173    code.push(0x02);
5174    code.push(0x40);
5175    code.push(0x03);
5176    code.push(0x40);
5177    //   if exp_s == 0 → br $exit  (while exp_s != 0)
5178    local_get(code, exp_s as u32);
5179    code.push(0x50); // i64.eqz
5180    code.push(0x0D);
5181    leb_u32(code, 1); // br_if $exit
5182    //   if (exp_s & 1) != 0: result = result * base_s  (checked)
5183    local_get(code, exp_s as u32);
5184    code.push(0x42);
5185    leb_i64(code, 1);
5186    code.push(0x83); // i64.and
5187    code.push(0xA7); // i32.wrap_i64 (low bit as the i32 condition)
5188    code.push(0x04);
5189    code.push(0x40); // if
5190    emit_checked_mul(code, tmp, result, base_s); // tmp = result * base_s (no aliasing)
5191    local_get(code, tmp as u32);
5192    local_set(code, result as u32); // result = tmp
5193    code.push(0x0B); // end if
5194    //   exp_s >>= 1
5195    local_get(code, exp_s as u32);
5196    code.push(0x42);
5197    leb_i64(code, 1);
5198    code.push(0x87); // i64.shr_s
5199    local_set(code, exp_s as u32);
5200    //   if exp_s != 0: base_s = base_s * base_s  (checked; skip the unused final square)
5201    local_get(code, exp_s as u32);
5202    code.push(0x50); // i64.eqz
5203    code.push(0x45); // i32.eqz → exp_s != 0
5204    code.push(0x04);
5205    code.push(0x40); // if
5206    emit_checked_mul(code, tmp, base_s, base_s); // tmp = base_s * base_s (no aliasing)
5207    local_get(code, tmp as u32);
5208    local_set(code, base_s as u32); // base_s = tmp
5209    code.push(0x0B); // end if
5210    code.push(0x0C);
5211    leb_u32(code, 0); // br $loop
5212    code.push(0x0B); // end loop
5213    code.push(0x0B); // end block
5214    // dst = result
5215    local_get(code, result as u32);
5216    local_set(code, dst as u32);
5217}
5218
5219/// Bump-allocate `size` bytes (the `size` is the i32 already on the stack). Leaves the
5220/// 8-aligned pointer in `dst_scratch` and advances `__heap_ptr` past it. No free (a finite-run
5221/// leak; growth-on-push leaks the old buffer).
5222fn emit_alloc(code: &mut Vec<u8>, ctx: &Ctx, dst_scratch: u32) {
5223    // stack: [size]
5224    if let Some(rt_alloc) = ctx.rt_alloc {
5225        // LINKER MODE: each block comes straight from the runtime allocator (`dlmalloc`, which grows
5226        // linear memory on demand), so the emitter heap is UNBOUNDED — a program allocating past any fixed
5227        // slab can't run off the end or collide with the runtime's region.
5228        code.push(0x10); // call logos_rt_alloc(size)
5229        leb_u32(code, rt_alloc);
5230        local_set(code, dst_scratch); // dst_scratch = ptr
5231    } else {
5232        // Self-contained: bump the `__heap_ptr` global (8-aligned, no free).
5233        global_get(code, ctx.heap_global);
5234        i32_const(code, 7);
5235        code.push(0x6A); // i32.add
5236        i32_const(code, -8);
5237        code.push(0x71); // i32.and → aligned p
5238        local_tee(code, dst_scratch); // dst_scratch = p; stack [size, p]
5239        code.push(0x6A); // i32.add → p + size
5240        global_set(code, ctx.heap_global);
5241    }
5242}
5243
5244/// `Push value to seq` (Int sequence): reallocate the data buffer to `(len+1)` elements, copy
5245/// the old elements, append `value`, and update the header in place (so the handle is stable).
5246/// O(n) per push — correctness first; geometric capacity is a later refinement.
5247fn lower_list_push(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, list: u16, value: u16) -> R<()> {
5248    let elem = kinds.get(list as usize).and_then(Kind::seq_elem).ok_or(WasmLowerError::Unsupported("push to a sequence of unknown element kind"))?;
5249    lower_list_push_at(code, elem, ctx, num_regs, list as u32, value)
5250}
5251
5252/// The core of [`lower_list_push`], parameterized by the element kind and the LOCAL holding the seq
5253/// handle (rather than deriving them from a register) — so a struct FIELD seq (`ListPushField`, whose
5254/// handle lives in a struct slot, kind in the struct layout) can push through the same amortized path.
5255fn lower_list_push_at(code: &mut Vec<u8>, elem: Kind, ctx: &Ctx, num_regs: u32, lst: u32, value: u16) -> R<()> {
5256    // Int/Float/Text(handle) elements all occupy an 8-byte slot — only the load/store opcode differs.
5257    let elem_load = seq_elem_load(elem)?;
5258    let elem_store = seq_elem_store(elem)?;
5259    let (hs_new, hs_i, hs_cap) = (num_regs + 5, num_regs + 6, num_regs + 7);
5260    // AMORTIZED growth. The header tracks `cap` (allocated element slots) alongside `len`; both
5261    // NewEmptyList (cap 0) and NewList (cap = count) set it to the true buffer size, so this stays
5262    // sound. When a slot is free we write in place (O(1)); otherwise we double the capacity, copy,
5263    // and repoint `data_ptr` — total work O(n) over n pushes, not the O(n²) a copy-every-push does
5264    // (which exhausts a small linear memory on a build-then-scan array like counting_sort's counts).
5265    //
5266    // if len < cap { in place } else { grow }
5267    local_get(code, lst);
5268    i32_load(code, 0); // len
5269    local_get(code, lst);
5270    i32_load(code, 4); // cap
5271    code.push(0x48); // i32.lt_s → len < cap
5272    code.push(0x04);
5273    code.push(0x40); // if (void)
5274    {
5275        // data_ptr + len*8 = value
5276        local_get(code, lst);
5277        i32_load(code, 8); // data_ptr
5278        local_get(code, lst);
5279        i32_load(code, 0); // len
5280        i32_const(code, 8);
5281        code.push(0x6C);
5282        code.push(0x6A); // data_ptr + len*8
5283        local_get(code, value as u32);
5284        elem_store(code, 0);
5285    }
5286    code.push(0x05); // else
5287    {
5288        // new_cap = cap == 0 ? 4 : cap * 2
5289        i32_const(code, 4);
5290        local_get(code, lst);
5291        i32_load(code, 4);
5292        i32_const(code, 2);
5293        code.push(0x6C); // cap * 2
5294        local_get(code, lst);
5295        i32_load(code, 4);
5296        code.push(0x45); // i32.eqz → cap == 0
5297        code.push(0x1B); // select → (cap==0) ? 4 : cap*2
5298        local_set(code, hs_cap);
5299        // new = alloc(new_cap * 8)
5300        local_get(code, hs_cap);
5301        i32_const(code, 8);
5302        code.push(0x6C);
5303        emit_alloc(code, ctx,hs_new);
5304        // for i in 0..len: new[i] = old[i]
5305        i32_const(code, 0);
5306        local_set(code, hs_i);
5307        code.push(0x02);
5308        code.push(0x40); // block $exit
5309        code.push(0x03);
5310        code.push(0x40); // loop $loop
5311        local_get(code, hs_i);
5312        local_get(code, lst);
5313        i32_load(code, 0); // len
5314        code.push(0x4E); // i32.ge_s → i >= len
5315        code.push(0x0D);
5316        leb_u32(code, 1); // br_if $exit
5317        local_get(code, hs_new);
5318        local_get(code, hs_i);
5319        i32_const(code, 8);
5320        code.push(0x6C);
5321        code.push(0x6A); // new + i*8
5322        local_get(code, lst);
5323        i32_load(code, 8); // old data_ptr
5324        local_get(code, hs_i);
5325        i32_const(code, 8);
5326        code.push(0x6C);
5327        code.push(0x6A); // old_data + i*8
5328        elem_load(code, 0);
5329        elem_store(code, 0); // new[i] = old[i]
5330        local_get(code, hs_i);
5331        i32_const(code, 1);
5332        code.push(0x6A);
5333        local_set(code, hs_i);
5334        code.push(0x0C);
5335        leb_u32(code, 0); // br $loop
5336        code.push(0x0B); // end loop
5337        code.push(0x0B); // end block
5338        // new[len] = value
5339        local_get(code, hs_new);
5340        local_get(code, lst);
5341        i32_load(code, 0); // len
5342        i32_const(code, 8);
5343        code.push(0x6C);
5344        code.push(0x6A);
5345        local_get(code, value as u32);
5346        elem_store(code, 0);
5347        // header: data_ptr = new
5348        local_get(code, lst);
5349        local_get(code, hs_new);
5350        i32_store(code, 8);
5351        // header: cap = new_cap
5352        local_get(code, lst);
5353        local_get(code, hs_cap);
5354        i32_store(code, 4);
5355    }
5356    code.push(0x0B); // end if
5357    // header: len = len + 1
5358    local_get(code, lst);
5359    local_get(code, lst);
5360    i32_load(code, 0);
5361    i32_const(code, 1);
5362    code.push(0x6A);
5363    i32_store(code, 0);
5364    Ok(())
5365}
5366
5367/// `Pop from list into dst` (`ListPop`) — remove and return the last element. Mirror of
5368/// `list_pop`: load `data_ptr[(len-1)]` at the element width, then decrement the header `len`
5369/// (leaving `cap`/`data_ptr` intact — the slot is simply no longer live, exactly as `Vec::pop`
5370/// shrinks length without freeing). Popping an empty list has no scalar `Nothing` representation,
5371/// so the guarded `else` yields a typed zero; the tree-walker's `Nothing` only arises from an
5372/// over-pop the corpus never performs, and the guard keeps the load in bounds regardless.
5373fn lower_list_pop(code: &mut Vec<u8>, kinds: &KindTable, dst: u16, list: u16) -> R<()> {
5374    let elem = kinds.get(list as usize).and_then(Kind::seq_elem).ok_or(WasmLowerError::Unsupported("pop from a sequence of unknown element kind"))?;
5375    let elem_load = seq_elem_load(elem)?;
5376    let vt = elem.wasm_valtype();
5377    let lst = list as u32;
5378    // if len > 0 { dst = data_ptr[(len-1)*8]; len -= 1 } else { dst = <typed zero> }
5379    local_get(code, lst);
5380    i32_load(code, 0); // len
5381    i32_const(code, 0);
5382    code.push(0x4A); // i32.gt_s → len > 0
5383    code.push(0x04);
5384    code.push(vt); // if (result <element valtype>)
5385    {
5386        // data_ptr + (len-1)*8 → load the last element (left on the stack as the `if` result)
5387        local_get(code, lst);
5388        i32_load(code, 8); // data_ptr
5389        local_get(code, lst);
5390        i32_load(code, 0); // len
5391        i32_const(code, 1);
5392        code.push(0x6B); // i32.sub → len-1
5393        i32_const(code, 8);
5394        code.push(0x6C); // *8
5395        code.push(0x6A); // data_ptr + (len-1)*8
5396        elem_load(code, 0);
5397        // header: len = len - 1
5398        local_get(code, lst);
5399        local_get(code, lst);
5400        i32_load(code, 0);
5401        i32_const(code, 1);
5402        code.push(0x6B); // i32.sub
5403        i32_store(code, 0);
5404    }
5405    code.push(0x05); // else
5406    match vt {
5407        F64 => {
5408            code.push(0x44);
5409            code.extend_from_slice(&0.0f64.to_le_bytes());
5410        }
5411        I64 => {
5412            code.push(0x42);
5413            leb_i64(code, 0);
5414        }
5415        _ => i32_const(code, 0),
5416    }
5417    code.push(0x0B); // end if
5418    local_set(code, dst as u32);
5419    Ok(())
5420}
5421
5422/// Bounds-check a 1-based `index` into the Int sequence `collection` (trap on `index < 1` or
5423/// `index > len`, as the standalone module has no VM to surface the error), then leave the
5424/// element address `data_ptr + (index-1)*8` on the stack.
5425fn emit_seq_elem_addr(code: &mut Vec<u8>, kinds: &KindTable, collection: u16, index: u16) -> R<()> {
5426    // Every element (scalar OR handle) occupies an 8-byte slot, so the address arithmetic is the
5427    // same — only the caller's load/store width differs. A heterogeneous `Tuple` has the identical
5428    // header+slot layout (the caller picks the width from the static position kind).
5429    match kinds.get(collection as usize) {
5430        Some(Kind::Tuple) => {}
5431        other if other.and_then(Kind::seq_elem).is_some() => {}
5432        _ => return Err(WasmLowerError::Unsupported("index of a non-scalar sequence")),
5433    }
5434    let col = collection as u32;
5435    // trap if index < 1 || index > len
5436    local_get(code, index as u32);
5437    code.push(0x42);
5438    leb_i64(code, 1);
5439    code.push(0x53); // i64.lt_s
5440    local_get(code, index as u32);
5441    local_get(code, col);
5442    i32_load(code, 0);
5443    code.push(0xAD); // len → i64
5444    code.push(0x55); // i64.gt_s
5445    code.push(0x72); // i32.or
5446    code.push(0x04);
5447    code.push(0x40); // if
5448    code.push(0x00); // unreachable
5449    code.push(0x0B); // end
5450    // address = data_ptr + (index-1)*8
5451    local_get(code, col);
5452    i32_load(code, 8); // data_ptr
5453    local_get(code, index as u32);
5454    code.push(0x42);
5455    leb_i64(code, 1);
5456    code.push(0x7D); // i64.sub
5457    code.push(0xA7); // i32.wrap_i64
5458    i32_const(code, 8);
5459    code.push(0x6C); // i32.mul
5460    code.push(0x6A); // i32.add
5461    Ok(())
5462}
5463
5464/// The element kind of the sequence in `collection` (Int or Float), for load/store width.
5465fn seq_elem_kind(kinds: &KindTable, collection: u16) -> R<Kind> {
5466    kinds.get(collection as usize).and_then(Kind::seq_elem).ok_or(WasmLowerError::Unsupported("sequence of unknown element kind"))
5467}
5468
5469/// The load opcode for a sequence element of `elem` kind: `Float` → `f64`, `Int`/`Bool`/`Moment` →
5470/// `i64`, a heap kind (`Text`/`Struct`/…) → `i32` (the handle in the slot's low word). Each slot is
5471/// 8 bytes regardless (so the `i64`-stride `emit_seq_copy`/`emit_seq_elem_addr` are kind-agnostic).
5472fn seq_elem_load(elem: Kind) -> R<fn(&mut Vec<u8>, u32)> {
5473    Ok(match elem.wasm_valtype() {
5474        F64 => f64_load,
5475        I64 => i64_load,
5476        _ => i32_load,
5477    })
5478}
5479
5480/// The store opcode mirroring [`seq_elem_load`].
5481fn seq_elem_store(elem: Kind) -> R<fn(&mut Vec<u8>, u32)> {
5482    Ok(match elem.wasm_valtype() {
5483        F64 => f64_store,
5484        I64 => i64_store,
5485        _ => i32_store,
5486    })
5487}
5488
5489/// `item index of seq` / `item N of tuple` — load the (bounds-checked) element at its kind's width.
5490/// For a heterogeneous `Tuple` the width is the constant position's element kind, which is the
5491/// inferred kind of `dst` (resolved via the `tuple_value` track); otherwise it's the seq element.
5492fn lower_index(code: &mut Vec<u8>, kinds: &KindTable, dst: u16, collection: u16, index: u16) -> R<()> {
5493    let elem = if kinds.get(collection as usize) == Some(Kind::Tuple) {
5494        kinds.get(dst as usize).ok_or(WasmLowerError::Unsupported("tuple element of unknown kind"))?
5495    } else {
5496        seq_elem_kind(kinds, collection)?
5497    };
5498    let load = seq_elem_load(elem)?;
5499    emit_seq_elem_addr(code, kinds, collection, index)?;
5500    load(code, 0);
5501    local_set(code, dst as u32);
5502    Ok(())
5503}
5504
5505/// `item index of text` — a one-character `Text` cut from `text` at the (1-based, bounds-checked)
5506/// BYTE position, matching the VM's ASCII fast path (`item i of "abc"` → `RuntimeValue::Text` of one
5507/// byte). A fresh 16-byte header + 1-byte data buffer holds the extracted byte; the result compares
5508/// to a literal (`item i of text equals " "`) through the existing `Text` byte-equality path. (A
5509/// multi-byte UTF-8 string would need a char decode to match the VM's general path — ASCII only here,
5510/// which is every string the corpus indexes.)
5511fn lower_text_index(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, collection: u16, index: u16) -> R<()> {
5512    let (hdr, data) = (num_regs + 5, num_regs + 6);
5513    let col = collection as u32;
5514    // trap if index < 1 || index > byte_len
5515    local_get(code, index as u32);
5516    code.push(0x42);
5517    leb_i64(code, 1);
5518    code.push(0x53); // i64.lt_s → index < 1
5519    local_get(code, index as u32);
5520    local_get(code, col);
5521    i32_load(code, 0); // byte len
5522    code.push(0xAD); // i64.extend_i32_s
5523    code.push(0x55); // i64.gt_s → index > len
5524    code.push(0x72); // i32.or
5525    code.push(0x04);
5526    code.push(0x40); // if
5527    code.push(0x00); // unreachable
5528    code.push(0x0B); // end
5529    // hdr = alloc(16); data = alloc(1)
5530    i32_const(code, 16);
5531    emit_alloc(code, ctx,hdr);
5532    i32_const(code, 1);
5533    emit_alloc(code, ctx,data);
5534    // data[0] = byte at (text.data_ptr + (index - 1))
5535    local_get(code, data); // store8 destination
5536    local_get(code, col);
5537    i32_load(code, 8); // text data_ptr
5538    local_get(code, index as u32);
5539    code.push(0x42);
5540    leb_i64(code, 1);
5541    code.push(0x7D); // i64.sub → index - 1
5542    code.push(0xA7); // i32.wrap_i64
5543    code.push(0x6A); // text data_ptr + (index - 1)
5544    i32_load8_u(code, 0);
5545    i32_store8(code, 0);
5546    // header: len = 1, cap = 1, data_ptr = data
5547    local_get(code, hdr);
5548    i32_const(code, 1);
5549    i32_store(code, 0);
5550    local_get(code, hdr);
5551    i32_const(code, 1);
5552    i32_store(code, 4);
5553    local_get(code, hdr);
5554    local_get(code, data);
5555    i32_store(code, 8);
5556    local_get(code, hdr);
5557    local_set(code, dst as u32);
5558    Ok(())
5559}
5560
5561/// Build a fresh `Seq of Int` from `len_reg` raw bytes at `base_reg` (each byte → one i64 element), the
5562/// emitter-heap `[len][cap][data_ptr]` seq with an i64 element buffer. Shared by `text_bytes` (a Text's
5563/// data_ptr) and `uuid_bytes` (the 16 bytes at a Uuid handle). Leaves the seq handle in `dst`.
5564fn emit_bytes_to_seq(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, base_reg: u32, len_reg: u32) {
5565    let (hdr, data, i) = (num_regs + 7, num_regs + 8, num_regs + 9);
5566    i32_const(code, 16);
5567    emit_alloc(code, ctx, hdr);
5568    // data = alloc(len * 8) — one i64 element per byte.
5569    local_get(code, len_reg);
5570    i32_const(code, 8);
5571    code.push(0x6C); // i32.mul
5572    emit_alloc(code, ctx, data);
5573    // header: len = cap = len_reg, data_ptr = data
5574    local_get(code, hdr);
5575    local_get(code, len_reg);
5576    i32_store(code, 0);
5577    local_get(code, hdr);
5578    local_get(code, len_reg);
5579    i32_store(code, 4);
5580    local_get(code, hdr);
5581    local_get(code, data);
5582    i32_store(code, 8);
5583    // for i in 0..len { data[i] = (i64) base[i] }
5584    i32_const(code, 0);
5585    local_set(code, i);
5586    code.push(0x02);
5587    code.push(0x40); // block
5588    code.push(0x03);
5589    code.push(0x40); // loop
5590    local_get(code, i);
5591    local_get(code, len_reg);
5592    code.push(0x4E); // i32.ge_s
5593    code.push(0x0D);
5594    leb_u32(code, 1); // br_if block (done)
5595    // dst addr = data + i*8
5596    local_get(code, data);
5597    local_get(code, i);
5598    i32_const(code, 8);
5599    code.push(0x6C); // i32.mul
5600    code.push(0x6A); // i32.add
5601    // value = base[i] zero-extended to i64 (a byte is 0..255)
5602    local_get(code, base_reg);
5603    local_get(code, i);
5604    code.push(0x6A); // i32.add
5605    i32_load8_u(code, 0);
5606    code.push(0xAD); // i64.extend_i32_u
5607    i64_store(code, 0);
5608    local_get(code, i);
5609    i32_const(code, 1);
5610    code.push(0x6A); // i32.add
5611    local_set(code, i);
5612    code.push(0x0C);
5613    leb_u32(code, 0); // br loop
5614    code.push(0x0B); // end loop
5615    code.push(0x0B); // end block
5616    local_get(code, hdr);
5617    local_set(code, dst as u32);
5618}
5619
5620/// `text_bytes(text)` — the Text's UTF-8 bytes as a `Seq of Int`.
5621fn lower_text_bytes(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, text: u16) {
5622    let (base, len) = (num_regs + 5, num_regs + 6);
5623    local_get(code, text as u32);
5624    i32_load(code, 8); // text data_ptr
5625    local_set(code, base);
5626    local_get(code, text as u32);
5627    i32_load(code, 0); // text byte len
5628    local_set(code, len);
5629    emit_bytes_to_seq(code, ctx, num_regs, dst, base, len);
5630}
5631
5632/// `uuid_bytes(u)` — the 16 raw bytes of a Uuid (the handle is a `Box<[u8; 16]>`, bytes at `*handle`).
5633fn lower_uuid_bytes(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, uuid: u16) {
5634    let (base, len) = (num_regs + 5, num_regs + 6);
5635    local_get(code, uuid as u32);
5636    local_set(code, base);
5637    i32_const(code, 16);
5638    local_set(code, len);
5639    emit_bytes_to_seq(code, ctx, num_regs, dst, base, len);
5640}
5641
5642/// `text_from_bytes(seq)` — a `Text` from the low byte of each `Seq of Int` element.
5643fn lower_text_from_bytes(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, seq: u16) {
5644    let (hdr, data, i, len, src) = (num_regs + 7, num_regs + 8, num_regs + 9, num_regs + 6, num_regs + 5);
5645    local_get(code, seq as u32);
5646    i32_load(code, 0); // element count
5647    local_set(code, len);
5648    local_get(code, seq as u32);
5649    i32_load(code, 8); // seq data_ptr
5650    local_set(code, src);
5651    i32_const(code, 16);
5652    emit_alloc(code, ctx, hdr);
5653    local_get(code, len); // data = alloc(len) — one byte each
5654    emit_alloc(code, ctx, data);
5655    local_get(code, hdr);
5656    local_get(code, len);
5657    i32_store(code, 0);
5658    local_get(code, hdr);
5659    local_get(code, len);
5660    i32_store(code, 4);
5661    local_get(code, hdr);
5662    local_get(code, data);
5663    i32_store(code, 8);
5664    // for i in 0..len { data[i] = low byte of seq[i] (= src[i*8], little-endian) }
5665    i32_const(code, 0);
5666    local_set(code, i);
5667    code.push(0x02);
5668    code.push(0x40);
5669    code.push(0x03);
5670    code.push(0x40);
5671    local_get(code, i);
5672    local_get(code, len);
5673    code.push(0x4E);
5674    code.push(0x0D);
5675    leb_u32(code, 1);
5676    local_get(code, data);
5677    local_get(code, i);
5678    code.push(0x6A); // data + i
5679    local_get(code, src);
5680    local_get(code, i);
5681    i32_const(code, 8);
5682    code.push(0x6C);
5683    code.push(0x6A); // src + i*8
5684    i32_load8_u(code, 0); // low byte of the i64 element
5685    i32_store8(code, 0);
5686    local_get(code, i);
5687    i32_const(code, 1);
5688    code.push(0x6A);
5689    local_set(code, i);
5690    code.push(0x0C);
5691    leb_u32(code, 0);
5692    code.push(0x0B);
5693    code.push(0x0B);
5694    local_get(code, hdr);
5695    local_set(code, dst as u32);
5696}
5697
5698/// `uuid_from_bytes(seq)` — pack the low byte of the first 16 `Seq of Int` elements into a contiguous
5699/// 16-byte block, then hand it to `logos_rt_uuid_from_ptr` to box a `base::Uuid` (LINKER MODE only).
5700fn lower_uuid_from_bytes(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, seq: u16) -> R<Flow> {
5701    let from_ptr = (ctx.host_index)(HostFn::UuidFromPtr).ok_or(WasmLowerError::Unsupported("uuid_from_ptr not imported"))?;
5702    let (block, i, src) = (num_regs + 7, num_regs + 9, num_regs + 5);
5703    local_get(code, seq as u32);
5704    i32_load(code, 8); // seq data_ptr
5705    local_set(code, src);
5706    i32_const(code, 16);
5707    emit_alloc(code, ctx, block);
5708    // for i in 0..16 { block[i] = low byte of seq[i] }
5709    i32_const(code, 0);
5710    local_set(code, i);
5711    code.push(0x02);
5712    code.push(0x40);
5713    code.push(0x03);
5714    code.push(0x40);
5715    local_get(code, i);
5716    i32_const(code, 16);
5717    code.push(0x4E);
5718    code.push(0x0D);
5719    leb_u32(code, 1);
5720    local_get(code, block);
5721    local_get(code, i);
5722    code.push(0x6A);
5723    local_get(code, src);
5724    local_get(code, i);
5725    i32_const(code, 8);
5726    code.push(0x6C);
5727    code.push(0x6A);
5728    i32_load8_u(code, 0);
5729    i32_store8(code, 0);
5730    local_get(code, i);
5731    i32_const(code, 1);
5732    code.push(0x6A);
5733    local_set(code, i);
5734    code.push(0x0C);
5735    leb_u32(code, 0);
5736    code.push(0x0B);
5737    code.push(0x0B);
5738    // dst = logos_rt_uuid_from_ptr(block)
5739    local_get(code, block);
5740    code.push(0x10);
5741    leb_u32(code, from_ptr);
5742    local_set(code, dst as u32);
5743    Ok(Flow::Straight)
5744}
5745
5746/// `lanes4Of(a, b, c, d)` — pack four `Word32` (i32) into a fresh 16-byte `[u32; 4]` lane block, lane
5747/// `i` at byte `i*4` (the `base::Lanes4Word32` layout the SHA-1 runtime reads).
5748fn lower_lanes4_of(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, words: [u16; 4]) {
5749    let block = num_regs + 7;
5750    i32_const(code, 16);
5751    emit_alloc(code, ctx, block);
5752    for (i, w) in words.iter().enumerate() {
5753        local_get(code, block);
5754        local_get(code, *w as u32);
5755        i32_store(code, (i * 4) as u32);
5756    }
5757    local_get(code, block);
5758    local_set(code, dst as u32);
5759}
5760
5761/// `lanes4Word32(seq)` — pack the first four `Word32` elements of a `Seq of Word32` (each an i32 in the
5762/// low word of its 8-byte slot) into a lane block.
5763fn lower_lanes4_word32(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, seq: u16) {
5764    let block = num_regs + 7;
5765    i32_const(code, 16);
5766    emit_alloc(code, ctx, block);
5767    for i in 0..4u32 {
5768        local_get(code, block);
5769        // value = seq.data_ptr[i] (i32 at data_ptr + i*8)
5770        local_get(code, seq as u32);
5771        i32_load(code, 8);
5772        i32_const(code, (i * 8) as i32);
5773        code.push(0x6A); // i32.add
5774        i32_load(code, 0);
5775        i32_store(code, i * 4);
5776    }
5777    local_get(code, block);
5778    local_set(code, dst as u32);
5779}
5780
5781/// `seqOfLanes4W32(lanes)` — unpack a lane block back into a fresh `Seq of Word32` (4 elements, each the
5782/// lane's i32 in the low word of an 8-byte slot).
5783fn lower_seq_of_lanes4(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, lanes: u16) {
5784    let (hdr, data) = (num_regs + 7, num_regs + 8);
5785    i32_const(code, 16);
5786    emit_alloc(code, ctx, hdr);
5787    i32_const(code, 32); // 4 elements × 8-byte slots
5788    emit_alloc(code, ctx, data);
5789    local_get(code, hdr);
5790    i32_const(code, 4);
5791    i32_store(code, 0);
5792    local_get(code, hdr);
5793    i32_const(code, 4);
5794    i32_store(code, 4);
5795    local_get(code, hdr);
5796    local_get(code, data);
5797    i32_store(code, 8);
5798    for i in 0..4u32 {
5799        local_get(code, data);
5800        local_get(code, lanes as u32);
5801        i32_load(code, i * 4); // lane i
5802        i32_store(code, i * 8); // element i slot
5803    }
5804    local_get(code, hdr);
5805    local_set(code, dst as u32);
5806}
5807
5808/// The four SHA-1 SHA-NI ops — direct `logos_rt_sha1*` calls over lane-block handles. `Sha1Rnds4` also
5809/// takes the round-function selector (an `Int`, i64); the others are binary.
5810fn lower_sha1_op(code: &mut Vec<u8>, ctx: &Ctx, dst: u16, args_start: u16, host: HostFn, ternary: bool) -> R<Flow> {
5811    let idx = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("sha1 op not imported"))?;
5812    local_get(code, args_start as u32);
5813    local_get(code, (args_start + 1) as u32);
5814    if ternary {
5815        local_get(code, (args_start + 2) as u32); // func selector (i64)
5816    }
5817    code.push(0x10);
5818    leb_u32(code, idx);
5819    local_set(code, dst as u32);
5820    Ok(Flow::Straight)
5821}
5822
5823/// `Set item index of seq to value` — store into the (bounds-checked) element.
5824fn lower_set_index(code: &mut Vec<u8>, kinds: &KindTable, collection: u16, index: u16, value: u16) -> R<()> {
5825    let elem = seq_elem_kind(kinds, collection)?;
5826    let store = seq_elem_store(elem)?;
5827    emit_seq_elem_addr(code, kinds, collection, index)?; // [addr]
5828    local_get(code, value as u32); // [addr, value]
5829    store(code, 0);
5830    Ok(())
5831}
5832
5833/// `start to end` (inclusive Int range): bump-allocate a header + a `count`-element data buffer
5834/// and fill it with `start, start+1, …, end` (empty when `end < start`).
5835fn lower_new_range(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, start: u16, end: u16) {
5836    let (hdr, data, idx) = (num_regs + 5, num_regs + 6, num_regs + 7); // i32 scratch
5837    let cnt = num_regs + 1; // reuse an i64 (pow) scratch
5838    // cnt = max(0, end - start + 1)
5839    local_get(code, end as u32);
5840    local_get(code, start as u32);
5841    code.push(0x7D); // i64.sub
5842    code.push(0x42);
5843    leb_i64(code, 1);
5844    code.push(0x7C); // i64.add → end-start+1
5845    local_set(code, cnt);
5846    local_get(code, cnt);
5847    code.push(0x42);
5848    leb_i64(code, 0);
5849    code.push(0x53); // i64.lt_s
5850    code.push(0x04);
5851    code.push(0x40); // if (cnt < 0)
5852    code.push(0x42);
5853    leb_i64(code, 0);
5854    local_set(code, cnt); // cnt = 0
5855    code.push(0x0B);
5856    // header = alloc(16); data = alloc(cnt*8)
5857    i32_const(code, 16);
5858    emit_alloc(code, ctx,hdr);
5859    local_get(code, cnt);
5860    code.push(0xA7); // i32.wrap_i64
5861    i32_const(code, 8);
5862    code.push(0x6C); // i32.mul
5863    emit_alloc(code, ctx,data);
5864    // header: len, cap = cnt; data_ptr = data
5865    local_get(code, hdr);
5866    local_get(code, cnt);
5867    code.push(0xA7);
5868    i32_store(code, 0);
5869    local_get(code, hdr);
5870    local_get(code, cnt);
5871    code.push(0xA7);
5872    i32_store(code, 4);
5873    local_get(code, hdr);
5874    local_get(code, data);
5875    i32_store(code, 8);
5876    // for i in 0..cnt: data[i] = start + i
5877    i32_const(code, 0);
5878    local_set(code, idx);
5879    code.push(0x02);
5880    code.push(0x40); // block
5881    code.push(0x03);
5882    code.push(0x40); // loop
5883    local_get(code, idx);
5884    local_get(code, cnt);
5885    code.push(0xA7);
5886    code.push(0x4E); // i32.ge_s → i >= cnt
5887    code.push(0x0D);
5888    leb_u32(code, 1); // br_if exit
5889    local_get(code, data);
5890    local_get(code, idx);
5891    i32_const(code, 8);
5892    code.push(0x6C);
5893    code.push(0x6A); // data + i*8
5894    local_get(code, start as u32);
5895    local_get(code, idx);
5896    code.push(0xAC); // i64.extend_i32_s
5897    code.push(0x7C); // i64.add → start + i
5898    i64_store(code, 0);
5899    local_get(code, idx);
5900    i32_const(code, 1);
5901    code.push(0x6A);
5902    local_set(code, idx); // i++
5903    code.push(0x0C);
5904    leb_u32(code, 0); // br loop
5905    code.push(0x0B);
5906    code.push(0x0B); // end loop, end block
5907    local_get(code, hdr);
5908    local_set(code, dst as u32);
5909}
5910
5911/// `repeatSeq(x, n)` — a fresh `n`-element sequence, each slot a copy of the SCALAR `x` (the WASM
5912/// mirror of `[x] * n` / `n copies of x`). Bump-allocate a header + `n*8` data buffer and fill each
5913/// 8-byte slot with `x` in a runtime loop (`n < 0` → empty). Scalar element kinds only — a reference
5914/// element (whose per-slot copy must be an INDEPENDENT deep copy) defers to the VM.
5915fn lower_repeat_seq(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, args_start: u16) -> R<()> {
5916    let value = args_start; // the element x
5917    let n = args_start + 1; // the count (i64)
5918    let is_float = matches!(kinds.get(value as usize), Some(Kind::Float));
5919    if !matches!(kinds.get(value as usize), Some(Kind::Int) | Some(Kind::Float)) {
5920        return Err(WasmLowerError::Unsupported("repeatSeq of a non-scalar element (deep-copy)"));
5921    }
5922    let (hdr, data, idx) = (num_regs + 5, num_regs + 6, num_regs + 7); // i32 scratch
5923    let cnt = num_regs + 1; // reuse an i64 (pow) scratch
5924    // cnt = max(0, n)
5925    local_get(code, n as u32);
5926    local_set(code, cnt);
5927    local_get(code, cnt);
5928    code.push(0x42);
5929    leb_i64(code, 0);
5930    code.push(0x53); // i64.lt_s
5931    code.push(0x04);
5932    code.push(0x40); // if (cnt < 0)
5933    code.push(0x42);
5934    leb_i64(code, 0);
5935    local_set(code, cnt); // cnt = 0
5936    code.push(0x0B);
5937    // header = alloc(16); data = alloc(cnt*8)
5938    i32_const(code, 16);
5939    emit_alloc(code, ctx, hdr);
5940    local_get(code, cnt);
5941    code.push(0xA7); // i32.wrap_i64
5942    i32_const(code, 8);
5943    code.push(0x6C); // i32.mul
5944    emit_alloc(code, ctx, data);
5945    // header: len = cap = cnt; data_ptr = data
5946    local_get(code, hdr);
5947    local_get(code, cnt);
5948    code.push(0xA7);
5949    i32_store(code, 0);
5950    local_get(code, hdr);
5951    local_get(code, cnt);
5952    code.push(0xA7);
5953    i32_store(code, 4);
5954    local_get(code, hdr);
5955    local_get(code, data);
5956    i32_store(code, 8);
5957    // for i in 0..cnt: data[i] = value
5958    i32_const(code, 0);
5959    local_set(code, idx);
5960    code.push(0x02);
5961    code.push(0x40); // block
5962    code.push(0x03);
5963    code.push(0x40); // loop
5964    local_get(code, idx);
5965    local_get(code, cnt);
5966    code.push(0xA7);
5967    code.push(0x4E); // i32.ge_s → i >= cnt
5968    code.push(0x0D);
5969    leb_u32(code, 1); // br_if exit
5970    local_get(code, data);
5971    local_get(code, idx);
5972    i32_const(code, 8);
5973    code.push(0x6C);
5974    code.push(0x6A); // data + i*8
5975    local_get(code, value as u32);
5976    if is_float {
5977        f64_store(code, 0);
5978    } else {
5979        i64_store(code, 0);
5980    }
5981    local_get(code, idx);
5982    i32_const(code, 1);
5983    code.push(0x6A);
5984    local_set(code, idx); // i++
5985    code.push(0x0C);
5986    leb_u32(code, 0); // br loop
5987    code.push(0x0B);
5988    code.push(0x0B); // end loop, end block
5989    local_get(code, hdr);
5990    local_set(code, dst as u32);
5991    Ok(())
5992}
5993
5994/// `[e0, e1, …]` (list / homogeneous tuple literal): bump-allocate a header + a `count`-element data
5995/// buffer and store the registers `start..start+count` (unrolled, no loop — `count` is a compile
5996/// constant). Elements are Int, Float, or Text(handle); mixed-kind elements are rejected.
5997fn lower_new_list(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, start: u16, count: u16) -> R<()> {
5998    let want = if count > 0 {
5999        kinds.get(start as usize).ok_or(WasmLowerError::Unsupported("list literal of unknown element kind"))?
6000    } else {
6001        Kind::Int
6002    };
6003    let elem_store = seq_elem_store(want)?;
6004    for j in 0..count {
6005        if kinds.get((start + j) as usize) != Some(want) {
6006            return Err(WasmLowerError::Unsupported("list literal with mixed element kinds"));
6007        }
6008    }
6009    let (hdr, data) = (num_regs + 5, num_regs + 6);
6010    i32_const(code, 16);
6011    emit_alloc(code, ctx,hdr);
6012    i32_const(code, i32::from(count) * 8);
6013    emit_alloc(code, ctx,data);
6014    // header: len = cap = count; data_ptr = data
6015    local_get(code, hdr);
6016    i32_const(code, i32::from(count));
6017    i32_store(code, 0);
6018    local_get(code, hdr);
6019    i32_const(code, i32::from(count));
6020    i32_store(code, 4);
6021    local_get(code, hdr);
6022    local_get(code, data);
6023    i32_store(code, 8);
6024    // data[j] = R[start+j]  (the store's offset field carries j*8)
6025    for j in 0..count {
6026        local_get(code, data);
6027        local_get(code, (start + j) as u32);
6028        elem_store(code, u32::from(j) * 8);
6029    }
6030    local_get(code, hdr);
6031    local_set(code, dst as u32);
6032    Ok(())
6033}
6034
6035/// `Let (a, b, …) be t` (`DestructureTuple`) — bind each destructured register `start + i` to tuple
6036/// slot `i`, loaded at that target's own width (`emit_slot_load` by the register's kind). Both a
6037/// heterogeneous tuple and a homogeneous one (which rides a `SeqX`) lay their elements out at 8-byte
6038/// slots, so `data_ptr + i*8` is the slot address in both.
6039fn lower_destructure_tuple(code: &mut Vec<u8>, kinds: &KindTable, src: u16, start: u16, count: u16) -> R<()> {
6040    for i in 0..count {
6041        let dst = start + i;
6042        local_get(code, src as u32);
6043        i32_load(code, 8); // data_ptr
6044        emit_slot_load(code, kinds.get(dst as usize), u32::from(i) * 8)?;
6045        local_set(code, dst as u32);
6046    }
6047    Ok(())
6048}
6049
6050/// A HETEROGENEOUS tuple `(a, b, …)` — like [`lower_new_list`] but each slot stores its element at
6051/// that element's OWN width (`emit_slot_store` by the register's kind); `item N of t` reads it back
6052/// at the matching width. Header `[len][cap][data_ptr]`, `len = cap = count`.
6053fn lower_new_tuple_het(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, start: u16, count: u16) -> R<()> {
6054    let (hdr, data) = (num_regs + 5, num_regs + 6);
6055    i32_const(code, 16);
6056    emit_alloc(code, ctx,hdr);
6057    i32_const(code, i32::from(count) * 8);
6058    emit_alloc(code, ctx,data);
6059    for (off, v) in [(0u32, i32::from(count)), (4, i32::from(count))] {
6060        local_get(code, hdr);
6061        i32_const(code, v);
6062        i32_store(code, off);
6063    }
6064    local_get(code, hdr);
6065    local_get(code, data);
6066    i32_store(code, 8);
6067    for j in 0..count {
6068        local_get(code, data);
6069        local_get(code, (start + j) as u32);
6070        emit_slot_store(code, kinds.get((start + j) as usize), u32::from(j) * 8)?;
6071    }
6072    local_get(code, hdr);
6073    local_set(code, dst as u32);
6074    Ok(())
6075}
6076
6077/// An enum constructor (`NewInductive`) — allocate a `8*(1+count)`-byte object whose first word is
6078/// the TAG (the constructor name's constant index) and whose following 8-byte slots hold the
6079/// `count` argument payloads (`args_start..args_start+count`), each stored at the width of its kind.
6080/// `BindArm` reads slot `index` back at offset `8*(1+index)`. A nullary constructor (`count == 0`)
6081/// is just the tag — the layout `TestArm` already reads at offset 0.
6082fn lower_new_inductive(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, ctor: u32, args_start: u16, count: u16) -> R<()> {
6083    let hs = num_regs + 5;
6084    i32_const(code, 8 * (1 + i32::from(count)));
6085    emit_alloc(code, ctx,hs);
6086    local_get(code, hs);
6087    i32_const(code, ctor as i32); // tag = constructor name's constant index
6088    i32_store(code, 0);
6089    for k in 0..count {
6090        let arg = args_start + k;
6091        local_get(code, hs);
6092        local_get(code, arg as u32);
6093        emit_slot_store(code, kinds.get(arg as usize), 8 * (1 + u32::from(k)))?;
6094    }
6095    local_get(code, hs);
6096    local_set(code, dst as u32);
6097    Ok(())
6098}
6099
6100/// `If it is a Circle (radius: r)` (`BindArm`) — load the matched value's `index`-th payload slot
6101/// (offset `8*(1+index)`) into `dst`, at the width of `dst`'s inferred kind. Only reached on the
6102/// matching variant (the compiler gates each arm's binds behind its `TestArm`/`JumpIfFalse`), so
6103/// the slot always holds that constructor's argument.
6104fn lower_bind_arm(code: &mut Vec<u8>, kinds: &KindTable, dst: u16, target: u16, index: u16) -> R<()> {
6105    if kinds.get(target as usize) != Some(Kind::Enum) {
6106        return Err(WasmLowerError::Unsupported("payload binding on a non-enum target"));
6107    }
6108    // A `When V (binds)` arm for a variant the target is NOT runs dead — the preceding `TestArm` +
6109    // `JumpIfFalse` skip it — and may bind an index past the actual variant's arity, so the bound
6110    // payload has no kind. The load never executes; default it to the `i64` the `None`-kind local is
6111    // declared as (`valtype`) so the slot load is merely valtype-consistent, not a refusal. (A live
6112    // bind always resolves a concrete kind from the construction site, so this only hits dead arms —
6113    // register splitting can isolate such a dead bind whose kind register-reuse formerly supplied.)
6114    let kind = kinds.get(dst as usize).or(Some(Kind::Int));
6115    local_get(code, target as u32);
6116    emit_slot_load(code, kind, 8 * (1 + u32::from(index)))?;
6117    local_set(code, dst as u32);
6118    Ok(())
6119}
6120
6121/// `(x) -> …` (`MakeClosure`) — bump-allocate the closure object `[func_idx:i32][value_k:i64 ×
6122/// cap_n][flag_k:i64 × cap_n]` and hand back its handle. `func_idx` (word 0) is the table slot
6123/// `CallValue` `call_indirect`s. Each capture's VALUE is snapshotted now — a global capture from
6124/// `GlobalGet`, a local one from its moved-into register `locals_start + local_k` — and its
6125/// present-FLAG is set to 1 (a wasm global is always initialized, so always "captured"). The body
6126/// reads value/flag back as trailing parameters (see [`plan_function`]).
6127/// The wasm valtype of function `func`'s capture `k` — a captured GLOBAL's kind (so a composite
6128/// handle stores/loads as `i32`), else `I64` (a captured local, or an unknown kind). `MakeClosure`'s
6129/// store, `CallValue`'s load, and the closure body's seeded signature all read it, so they agree.
6130fn capture_valtype(ctx: &Ctx, func: u16, k: usize) -> u8 {
6131    ctx.capture_kinds
6132        .get(func as usize)
6133        .and_then(|v| v.get(k))
6134        .copied()
6135        .flatten()
6136        .map(Kind::wasm_valtype)
6137        .unwrap_or(I64)
6138}
6139
6140fn lower_make_closure(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, func: u16, locals_start: u16) -> R<()> {
6141    let caps = ctx
6142        .functions
6143        .get(func as usize)
6144        .map(|f| f.captures.as_slice())
6145        .ok_or(WasmLowerError::Unsupported("closure of unknown function"))?;
6146    let cap_n = caps.len() as u32;
6147    let hs = num_regs + 5;
6148    i32_const(code, 8 * (1 + 2 * cap_n) as i32);
6149    emit_alloc(code, ctx,hs);
6150    local_get(code, hs);
6151    i32_const(code, i32::from(func)); // function index = table slot
6152    i32_store(code, 0);
6153    let mut local_k: u16 = 0;
6154    for (k, (_sym, global)) in caps.iter().enumerate() {
6155        // value_k @ 8 + 8k — stored at the capture's own kind (a captured global may be a composite
6156        // handle, i32) so it round-trips through the closure object losslessly.
6157        local_get(code, hs);
6158        match global {
6159            Some(gidx) => global_get(code, u32::from(*gidx)),
6160            None => {
6161                local_get(code, (locals_start + local_k) as u32);
6162                local_k += 1;
6163            }
6164        }
6165        let off = 8 + 8 * k as u32;
6166        match capture_valtype(ctx, func, k) {
6167            I32 => i32_store(code, off),
6168            F64 => f64_store(code, off),
6169            _ => i64_store(code, off),
6170        }
6171        // flag_k @ 8 + 8*cap_n + 8k = 1 (present)
6172        local_get(code, hs);
6173        code.push(0x42); // i64.const 1
6174        leb_i64(code, 1);
6175        i64_store(code, 8 + 8 * cap_n + 8 * k as u32);
6176    }
6177    local_get(code, hs);
6178    local_set(code, dst as u32);
6179    Ok(())
6180}
6181
6182/// `f(args)` (`CallValue`) — push the arguments, then (for a capturing closure) each capture's
6183/// stored value and present-flag, then the closure's function index (the table slot), and
6184/// `call_indirect` through the module's function table. The push order matches the callee body's
6185/// parameter layout `[args][values][flags]`. The static signature is the callee body's own type
6186/// (resolved via the closure's statically-traced construction site); a value result binds to `dst`.
6187fn lower_call_value(code: &mut Vec<u8>, plan: &Plan, ctx: &Ctx, pc: usize, dst: u16, callee: u16, args_start: u16, arg_count: u16) -> R<()> {
6188    let func = plan
6189        .structs
6190        .callee_func
6191        .get(pc)
6192        .copied()
6193        .flatten()
6194        .ok_or(WasmLowerError::Unsupported("indirect call to a closure of unknown origin"))?;
6195    let type_idx = *ctx
6196        .fn_type
6197        .get(func as usize)
6198        .ok_or(WasmLowerError::Unsupported("closure call: unknown callee type"))?;
6199    let cap_n = ctx.functions.get(func as usize).map(|f| f.captures.len() as u32).unwrap_or(0);
6200    for k in 0..arg_count {
6201        local_get(code, (args_start + k) as u32);
6202    }
6203    // capture values (each at its own kind), then present flags, from the closure object
6204    for k in 0..cap_n {
6205        local_get(code, callee as u32);
6206        let off = 8 + 8 * k;
6207        match capture_valtype(ctx, func, k as usize) {
6208            I32 => i32_load(code, off),
6209            F64 => f64_load(code, off),
6210            _ => i64_load(code, off),
6211        }
6212    }
6213    for k in 0..cap_n {
6214        local_get(code, callee as u32);
6215        i64_load(code, 8 + 8 * cap_n + 8 * k);
6216    }
6217    // The callee body `func` is statically known here (an unresolvable origin was already refused), so
6218    // LINKER MODE emits a DIRECT `call` — no function table, no `call_indirect` type, and therefore no
6219    // element/table sections the reloc transform can't yet relocate. The self-contained path keeps the
6220    // fully-general `call_indirect` through the module's table.
6221    if ctx.linked {
6222        code.push(0x10); // call fn_base+func (direct)
6223        leb_u32(code, ctx.fn_base + func as u32);
6224    } else {
6225        local_get(code, callee as u32);
6226        i32_load(code, 0); // closure.func_idx = table slot
6227        code.push(0x11); // call_indirect
6228        leb_u32(code, type_idx);
6229        leb_u32(code, 0); // table 0
6230    }
6231    // Bind the result iff the callee returns one (else the call leaves nothing on the stack).
6232    if ctx.fn_results.get(func as usize).copied().flatten().is_some() {
6233        local_set(code, dst as u32);
6234    }
6235    Ok(())
6236}
6237
6238/// `it is Variant` (`TestArm`) — compare the target's tag (constructor constant index) against
6239/// `variant` (the same constant index for that name, by dedup) → Bool. An `i32.eq` on tags is the
6240/// VM's constructor-name string compare.
6241fn lower_test_arm(code: &mut Vec<u8>, dst: u16, target: u16, variant: u32) {
6242    local_get(code, target as u32);
6243    i32_load(code, 0); // tag
6244    i32_const(code, variant as i32);
6245    code.push(0x46); // i32.eq
6246    code.push(0xAD); // i64.extend_i32_u → Bool
6247    local_set(code, dst as u32);
6248}
6249
6250/// Bump-allocate a zeroed 16-byte collection header `[len/num=0][cap=0][data_ptr=0]` and store its
6251/// handle in `dst` — the empty form of a sequence or a map (their element/entry shape differs only
6252/// at use, not at creation).
6253fn emit_empty_header(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u32) {
6254    let hs = num_regs + 5;
6255    global_get(code, ctx.heap_global);
6256    i32_const(code, 7);
6257    code.push(0x6A);
6258    i32_const(code, -8);
6259    code.push(0x71);
6260    local_tee(code, hs);
6261    i32_const(code, 16);
6262    code.push(0x6A);
6263    global_set(code, ctx.heap_global);
6264    for off in [0u32, 4, 8] {
6265        local_get(code, hs);
6266        i32_const(code, 0);
6267        i32_store(code, off);
6268    }
6269    local_get(code, hs);
6270    local_set(code, dst);
6271}
6272
6273/// COPY-ON-WRITE reference counting mirrors the VM's `Rc` so value semantics (`LOGOS_VALUE_SEMANTICS`,
6274/// now default-on in the tree-walker/VM) holds in native wasm. The 4th header word (offset 12, the
6275/// spare slot past `[len][cap][data_ptr]`) holds the count of EXTRA references beyond the owner — so 0
6276/// means "uniquely owned" and needs no initialization (wasm linear memory is zero-initialized and the
6277/// bump allocator never reuses a slot, so every fresh header's word 12 is already 0). A `Call`
6278/// argument RETAINS (`++`, gaining the callee's parameter as a second holder); a mutation op checks
6279/// the count and clones first when it is nonzero, so the write can't be seen through the other holder.
6280fn emit_retain(code: &mut Vec<u8>, reg: u16) {
6281    local_get(code, reg as u32);
6282    local_get(code, reg as u32);
6283    i32_load(code, 12);
6284    i32_const(code, 1);
6285    code.push(0x6A); // extra_refs + 1
6286    i32_store(code, 12);
6287}
6288
6289/// The mutable heap kinds a copy-on-write clone can currently reproduce (everything `lower_deep_clone`
6290/// handles). A kind outside this set skips the COW guard and mutates in place — sound as long as no
6291/// aliasing test exercises it (`Map` COW is added in a follow-up).
6292fn cow_clonable(k: Option<Kind>) -> bool {
6293    matches!(k, Some(Kind::SeqInt) | Some(Kind::SeqBool) | Some(Kind::SeqFloat) | Some(Kind::SeqAny) | Some(Kind::Set) | Some(Kind::SetText) | Some(Kind::SeqSeqInt) | Some(Kind::Map))
6294}
6295
6296/// `cow(reg)` — the copy-on-write guard emitted before a mutation. If the object has any extra
6297/// reference (header word 12 nonzero) it is replaced by a fresh deep clone (word 12 == 0), so the
6298/// mutation stays private — exactly as the VM's `maybe_cow` clones an `Rc` whose `strong_count > 1`.
6299/// A uniquely-owned object mutates in place (the common build-then-scan case pays only a load+branch).
6300fn emit_cow(code: &mut Vec<u8>, kinds: &KindTable, structs: &kind::StructLayout, ctx: &Ctx, num_regs: u32, reg: u16) -> R<()> {
6301    if !cow_clonable(kinds.get(reg as usize)) {
6302        return Ok(());
6303    }
6304    local_get(code, reg as u32);
6305    i32_load(code, 12); // extra_refs
6306    code.push(0x04);
6307    code.push(0x40); // if extra_refs != 0
6308    lower_deep_clone(code, kinds, structs, ctx, num_regs, reg, reg)?;
6309    code.push(0x0B); // end
6310    Ok(())
6311}
6312
6313/// Byte-equality of the two `Text` handles in locals `a`/`b` → `1`/`0` in local `out` (using local
6314/// `idx` as a byte-loop counter). The value-local form of [`lower_text_eq`]'s scan, so it can be a
6315/// per-entry key compare inside a map's element loop.
6316fn emit_text_eq_to(code: &mut Vec<u8>, a: u32, b: u32, out: u32, idx: u32) {
6317    i32_const(code, 1);
6318    local_set(code, out); // assume equal
6319    local_get(code, a);
6320    i32_load(code, 0);
6321    local_get(code, b);
6322    i32_load(code, 0);
6323    code.push(0x47); // i32.ne (lengths differ?)
6324    code.push(0x04);
6325    code.push(0x40); // if (lengths differ)
6326    i32_const(code, 0);
6327    local_set(code, out);
6328    code.push(0x05); // else: byte-compare
6329    i32_const(code, 0);
6330    local_set(code, idx);
6331    code.push(0x02);
6332    code.push(0x40);
6333    code.push(0x03);
6334    code.push(0x40);
6335    local_get(code, idx);
6336    local_get(code, a);
6337    i32_load(code, 0);
6338    code.push(0x4E); // i32.ge_s → idx >= len
6339    code.push(0x0D);
6340    leb_u32(code, 1); // br_if block (all matched)
6341    local_get(code, a);
6342    i32_load(code, 8);
6343    local_get(code, idx);
6344    code.push(0x6A);
6345    i32_load8_u(code, 0);
6346    local_get(code, b);
6347    i32_load(code, 8);
6348    local_get(code, idx);
6349    code.push(0x6A);
6350    i32_load8_u(code, 0);
6351    code.push(0x47); // i32.ne
6352    code.push(0x04);
6353    code.push(0x40); // if (byte mismatch)
6354    i32_const(code, 0);
6355    local_set(code, out);
6356    code.push(0x0C);
6357    leb_u32(code, 2); // br block (not equal)
6358    code.push(0x0B);
6359    local_get(code, idx);
6360    i32_const(code, 1);
6361    code.push(0x6A);
6362    local_set(code, idx);
6363    code.push(0x0C);
6364    leb_u32(code, 0); // br loop
6365    code.push(0x0B); // end loop
6366    code.push(0x0B); // end block
6367    code.push(0x0B); // end outer if
6368}
6369
6370/// Is a `Map`'s key an `Int` (i64, false) or `Text` (i32 handle, true)? Other key kinds are refused.
6371fn map_key_text(kinds: &KindTable, key: u16) -> R<bool> {
6372    match kinds.get(key as usize) {
6373        Some(Kind::Int) => Ok(false),
6374        Some(Kind::Text) => Ok(true),
6375        _ => Err(WasmLowerError::Unsupported("map key must be Int or Text")),
6376    }
6377}
6378
6379/// Leave on the stack `1`/`0` for whether map entry `idx`'s key equals the query register `key` —
6380/// `i64.eq` for an Int key, or a `Text` byte-equality (via [`emit_text_eq_to`], into scratch
6381/// +8/+9/+10) for a Text key. `idx_local` is the entry index local.
6382fn emit_map_key_compare(code: &mut Vec<u8>, num_regs: u32, key_text: bool, m: u32, idx_local: u32, key: u16) {
6383    if key_text {
6384        let (entry_key, eq, tidx) = (num_regs + 8, num_regs + 9, num_regs + 10);
6385        emit_map_entry_addr(code, m, idx_local);
6386        i32_load(code, 0); // entry key handle
6387        local_set(code, entry_key);
6388        emit_text_eq_to(code, key as u32, entry_key, eq, tidx);
6389        local_get(code, eq); // i32 result
6390    } else {
6391        emit_map_entry_addr(code, m, idx_local);
6392        i64_load(code, 0); // entry key
6393        local_get(code, key as u32);
6394        code.push(0x51); // i64.eq
6395    }
6396}
6397
6398/// The store opcode for a map's VALUE slot (offset 8 of a 16-byte entry), by the value's kind: a
6399/// `Float` is an `f64`, an `Int`/`Bool` an `i64`. Any other kind (a handle-valued map) is deferred.
6400fn map_value_store(k: Option<Kind>) -> R<fn(&mut Vec<u8>, u32)> {
6401    match k {
6402        Some(Kind::Float) => Ok(f64_store),
6403        Some(Kind::Int) | Some(Kind::Bool) | Some(Kind::Moment) | Some(Kind::Duration) | Some(Kind::Time) | Some(Kind::Span) => Ok(i64_store),
6404        // A handle-valued map (`Map of K to Seq`/Struct/Text/…): the i32 handle rides the low word of
6405        // the 8-byte value slot; the entry copy in `emit_map_clone` moves the whole slot as an i64.
6406        Some(vk) if vk.wasm_valtype() == I32 => Ok(i32_store),
6407        _ => Err(WasmLowerError::Unsupported("map value of an unsupported kind")),
6408    }
6409}
6410
6411/// The load opcode mirroring [`map_value_store`] (the value's kind is the `Index` destination's).
6412fn map_value_load(k: Option<Kind>) -> R<fn(&mut Vec<u8>, u32)> {
6413    match k {
6414        Some(Kind::Float) => Ok(f64_load),
6415        Some(Kind::Int) | Some(Kind::Bool) | Some(Kind::Moment) | Some(Kind::Duration) | Some(Kind::Time) | Some(Kind::Span) => Ok(i64_load),
6416        Some(vk) if vk.wasm_valtype() == I32 => Ok(i32_load),
6417        _ => Err(WasmLowerError::Unsupported("map value of unknown/unsupported kind")),
6418    }
6419}
6420
6421/// Leave `data_ptr + idx*16` (the address of map entry `idx`, a `[key:i64][value:i64]` pair) on
6422/// the stack.
6423fn emit_map_entry_addr(code: &mut Vec<u8>, m: u32, idx: u32) {
6424    local_get(code, m);
6425    i32_load(code, 8); // data_ptr
6426    local_get(code, idx);
6427    i32_const(code, 16);
6428    code.push(0x6C); // i32.mul
6429    code.push(0x6A); // i32.add
6430}
6431
6432/// `Map of {Int,Text} to Int` insert (`Set item key of m to value`): linear-scan for `key` (i64.eq
6433/// for an Int key, byte-equality for a Text key); if present overwrite its value, else append a new
6434/// `[key][value]` entry (reallocating the entry buffer, like `ListPush`). The value must be `Int`.
6435fn lower_map_insert(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, map: u16, key: u16, value: u16) -> R<()> {
6436    let key_text = map_key_text(kinds, key)?;
6437    let val_store = map_value_store(kinds.get(value as usize))?;
6438    let m = map as u32;
6439    let (idx, found, new) = (num_regs + 5, num_regs + 6, num_regs + 7);
6440    // scan for an existing key
6441    i32_const(code, 0);
6442    local_set(code, idx);
6443    i32_const(code, 0);
6444    local_set(code, found);
6445    code.push(0x02);
6446    code.push(0x40); // block
6447    code.push(0x03);
6448    code.push(0x40); // loop
6449    local_get(code, idx);
6450    local_get(code, m);
6451    i32_load(code, 0); // num_entries
6452    code.push(0x4E); // i32.ge_s → idx >= num
6453    code.push(0x0D);
6454    leb_u32(code, 1); // br_if block
6455    emit_map_key_compare(code, num_regs, key_text, m, idx, key);
6456    code.push(0x04);
6457    code.push(0x40); // if (key matches)
6458    emit_map_entry_addr(code, m, idx);
6459    local_get(code, value as u32);
6460    val_store(code, 8); // entry.value = value (at the value kind's width)
6461    i32_const(code, 1);
6462    local_set(code, found);
6463    code.push(0x0C);
6464    leb_u32(code, 2); // br block (out of if→loop→block)
6465    code.push(0x0B); // end if
6466    local_get(code, idx);
6467    i32_const(code, 1);
6468    code.push(0x6A);
6469    local_set(code, idx);
6470    code.push(0x0C);
6471    leb_u32(code, 0); // br loop
6472    code.push(0x0B); // end loop
6473    code.push(0x0B); // end block
6474    // if not found: append a new entry (realloc the buffer to num+1 entries)
6475    local_get(code, found);
6476    code.push(0x45); // i32.eqz
6477    code.push(0x04);
6478    code.push(0x40); // if (!found)
6479    // new = alloc((num+1) * 16)
6480    local_get(code, m);
6481    i32_load(code, 0);
6482    i32_const(code, 1);
6483    code.push(0x6A);
6484    i32_const(code, 16);
6485    code.push(0x6C);
6486    emit_alloc(code, ctx,new);
6487    // copy old entries: for i in 0..num: new[i] = old[i] (key then value)
6488    i32_const(code, 0);
6489    local_set(code, idx);
6490    code.push(0x02);
6491    code.push(0x40);
6492    code.push(0x03);
6493    code.push(0x40);
6494    local_get(code, idx);
6495    local_get(code, m);
6496    i32_load(code, 0);
6497    code.push(0x4E);
6498    code.push(0x0D);
6499    leb_u32(code, 1);
6500    for field in [0u32, 8] {
6501        // new[idx].field = old[idx].field
6502        local_get(code, new);
6503        local_get(code, idx);
6504        i32_const(code, 16);
6505        code.push(0x6C);
6506        code.push(0x6A); // new entry addr
6507        emit_map_entry_addr(code, m, idx); // old entry addr
6508        i64_load(code, field);
6509        i64_store(code, field);
6510    }
6511    local_get(code, idx);
6512    i32_const(code, 1);
6513    code.push(0x6A);
6514    local_set(code, idx);
6515    code.push(0x0C);
6516    leb_u32(code, 0);
6517    code.push(0x0B);
6518    code.push(0x0B);
6519    // new[num] = (key, value) — the key is an i64 (Int) or an i32 handle (Text)
6520    local_get(code, new);
6521    local_get(code, m);
6522    i32_load(code, 0);
6523    i32_const(code, 16);
6524    code.push(0x6C);
6525    code.push(0x6A); // addr of new entry slot
6526    local_get(code, key as u32);
6527    if key_text {
6528        i32_store(code, 0);
6529    } else {
6530        i64_store(code, 0);
6531    }
6532    local_get(code, new);
6533    local_get(code, m);
6534    i32_load(code, 0);
6535    i32_const(code, 16);
6536    code.push(0x6C);
6537    code.push(0x6A);
6538    local_get(code, value as u32);
6539    val_store(code, 8);
6540    // header: data_ptr = new; num_entries += 1
6541    local_get(code, m);
6542    local_get(code, new);
6543    i32_store(code, 8);
6544    local_get(code, m);
6545    local_get(code, m);
6546    i32_load(code, 0);
6547    i32_const(code, 1);
6548    code.push(0x6A);
6549    i32_store(code, 0);
6550    code.push(0x0B); // end if (!found)
6551    Ok(())
6552}
6553
6554/// `Map of Int to Int` get (`item key of m`): linear-scan for `key`, load its value into `dst`;
6555/// trap if absent (the tree-walker raises "Key not found", and the standalone module has no VM).
6556fn lower_map_get(code: &mut Vec<u8>, kinds: &KindTable, num_regs: u32, dst: u16, map: u16, key: u16) -> R<()> {
6557    let key_text = map_key_text(kinds, key)?;
6558    let val_load = map_value_load(kinds.get(dst as usize))?;
6559    let m = map as u32;
6560    let (idx, found) = (num_regs + 5, num_regs + 6);
6561    i32_const(code, 0);
6562    local_set(code, idx);
6563    i32_const(code, 0);
6564    local_set(code, found);
6565    code.push(0x02);
6566    code.push(0x40);
6567    code.push(0x03);
6568    code.push(0x40);
6569    local_get(code, idx);
6570    local_get(code, m);
6571    i32_load(code, 0);
6572    code.push(0x4E);
6573    code.push(0x0D);
6574    leb_u32(code, 1);
6575    emit_map_key_compare(code, num_regs, key_text, m, idx, key);
6576    code.push(0x04);
6577    code.push(0x40); // if (match)
6578    emit_map_entry_addr(code, m, idx);
6579    val_load(code, 8); // value (at the value kind's width)
6580    local_set(code, dst as u32);
6581    i32_const(code, 1);
6582    local_set(code, found);
6583    code.push(0x0C);
6584    leb_u32(code, 2);
6585    code.push(0x0B); // end if
6586    local_get(code, idx);
6587    i32_const(code, 1);
6588    code.push(0x6A);
6589    local_set(code, idx);
6590    code.push(0x0C);
6591    leb_u32(code, 0);
6592    code.push(0x0B);
6593    code.push(0x0B);
6594    // absent key → trap
6595    local_get(code, found);
6596    code.push(0x45); // i32.eqz
6597    code.push(0x04);
6598    code.push(0x40);
6599    code.push(0x00); // unreachable
6600    code.push(0x0B);
6601    Ok(())
6602}
6603
6604/// `m contains key` (Map): linear-scan for the key → Bool i64 0/1 in `dst`.
6605fn lower_map_contains(code: &mut Vec<u8>, kinds: &KindTable, num_regs: u32, dst: u16, map: u16, key: u16) -> R<()> {
6606    let key_text = map_key_text(kinds, key)?;
6607    let m = map as u32;
6608    let idx = num_regs + 5;
6609    code.push(0x42);
6610    leb_i64(code, 0);
6611    local_set(code, dst as u32); // dst = 0
6612    i32_const(code, 0);
6613    local_set(code, idx);
6614    code.push(0x02);
6615    code.push(0x40);
6616    code.push(0x03);
6617    code.push(0x40);
6618    local_get(code, idx);
6619    local_get(code, m);
6620    i32_load(code, 0);
6621    code.push(0x4E);
6622    code.push(0x0D);
6623    leb_u32(code, 1);
6624    emit_map_key_compare(code, num_regs, key_text, m, idx, key);
6625    code.push(0x04);
6626    code.push(0x40); // if (match)
6627    code.push(0x42);
6628    leb_i64(code, 1);
6629    local_set(code, dst as u32); // dst = 1
6630    code.push(0x0C);
6631    leb_u32(code, 2); // break (found)
6632    code.push(0x0B);
6633    local_get(code, idx);
6634    i32_const(code, 1);
6635    code.push(0x6A);
6636    local_set(code, idx);
6637    code.push(0x0C);
6638    leb_u32(code, 0);
6639    code.push(0x0B);
6640    code.push(0x0B);
6641    Ok(())
6642}
6643
6644/// Byte-equality of two `Text` handles in locals `a`/`b`, leaving an i32 (1 = equal, 0 = not) on the
6645/// wasm stack — the handle-in-local, on-stack-result form of [`lower_text_eq`], so it drops into a
6646/// set scan's comparison exactly where an `i64.eq` would. Uses `+9` (byte index) and `+10` (result)
6647/// scratch, distinct from a set scan's `+5`/`+6`/`+7` and the `+8` slot-handle temp.
6648fn emit_text_handles_eq(code: &mut Vec<u8>, num_regs: u32, a: u32, b: u32) {
6649    let (idx, res) = (num_regs + 9, num_regs + 10);
6650    i32_const(code, 1);
6651    local_set(code, res); // assume equal
6652    // if len_a != len_b → not equal
6653    local_get(code, a);
6654    i32_load(code, 0);
6655    local_get(code, b);
6656    i32_load(code, 0);
6657    code.push(0x47); // i32.ne
6658    code.push(0x04);
6659    code.push(0x40); // if (lengths differ)
6660    i32_const(code, 0);
6661    local_set(code, res);
6662    code.push(0x05); // else — compare bytes
6663    i32_const(code, 0);
6664    local_set(code, idx);
6665    code.push(0x02);
6666    code.push(0x40); // block $done
6667    code.push(0x03);
6668    code.push(0x40); // loop
6669    local_get(code, idx);
6670    local_get(code, a);
6671    i32_load(code, 0); // len
6672    code.push(0x4E); // i32.ge_s → idx >= len (all matched)
6673    code.push(0x0D);
6674    leb_u32(code, 1); // br_if $done
6675    // a.data[idx] != b.data[idx] ?
6676    local_get(code, a);
6677    i32_load(code, 8);
6678    local_get(code, idx);
6679    code.push(0x6A);
6680    i32_load8_u(code, 0);
6681    local_get(code, b);
6682    i32_load(code, 8);
6683    local_get(code, idx);
6684    code.push(0x6A);
6685    i32_load8_u(code, 0);
6686    code.push(0x47); // i32.ne
6687    code.push(0x04);
6688    code.push(0x40); // if (byte differs)
6689    i32_const(code, 0);
6690    local_set(code, res);
6691    code.push(0x0C);
6692    leb_u32(code, 2); // br $done (mismatch)
6693    code.push(0x0B); // end if
6694    local_get(code, idx);
6695    i32_const(code, 1);
6696    code.push(0x6A);
6697    local_set(code, idx);
6698    code.push(0x0C);
6699    leb_u32(code, 0); // br loop
6700    code.push(0x0B); // end loop
6701    code.push(0x0B); // end block
6702    code.push(0x0B); // end outer if
6703    local_get(code, res); // leave the result on the stack
6704}
6705
6706/// Emit "does `set[idx]` equal `value`?", leaving an i32 (1/0) on the wasm stack — the element
6707/// comparison shared by the set scans. Int: `i64.eq` of the slot element and `value`. `SetText`:
6708/// byte-equality of the slot's `Text` handle and `value`'s (`emit_text_handles_eq`, via the `+8`
6709/// slot-handle temp). Both read the 8-byte slot at `data_ptr + idx*8`.
6710fn emit_set_elem_eq(code: &mut Vec<u8>, num_regs: u32, set: u32, idx: u32, value: u32, is_text: bool) {
6711    local_get(code, set);
6712    i32_load(code, 8); // data_ptr
6713    local_get(code, idx);
6714    i32_const(code, 8);
6715    code.push(0x6C);
6716    code.push(0x6A); // slot addr = data_ptr + idx*8
6717    if is_text {
6718        i32_load(code, 0); // the slot's Text handle
6719        local_set(code, num_regs + 8);
6720        emit_text_handles_eq(code, num_regs, num_regs + 8, value);
6721    } else {
6722        i64_load(code, 0); // the slot's i64 element
6723        local_get(code, value);
6724        code.push(0x51); // i64.eq
6725    }
6726}
6727
6728/// `Remove value from c` (`RemoveFrom`) on a `Set of Int`/`Set of Text` or `Map of Int to Int` —
6729/// linear-scan for the value (a Set element, or a Map key at entry offset 0); if found, swap-remove
6730/// it (overwrite the found slot with the last slot and decrement the count). Set/Map are
6731/// order-independent so swap-remove is byte-identical to the VM for length/contains/get. A `Set of
6732/// Text` compares by BYTE equality (`emit_set_elem_eq` text path), not handle identity.
6733fn lower_remove_from(code: &mut Vec<u8>, kinds: &KindTable, num_regs: u32, collection: u16, value: u16) -> R<()> {
6734    let set_text = matches!(kinds.get(collection as usize), Some(Kind::SetText) | Some(Kind::CrdtSetText));
6735    if !set_text && kinds.get(value as usize) != Some(Kind::Int) {
6736        return Err(WasmLowerError::Unsupported("remove of a non-Int value"));
6737    }
6738    let stride: i32 = match kinds.get(collection as usize) {
6739        Some(Kind::Set) | Some(Kind::SetText) | Some(Kind::CrdtSetText) => 8,
6740        Some(Kind::Map) => 16,
6741        _ => return Err(WasmLowerError::Unsupported("remove from a non-set/map collection")),
6742    };
6743    let c = collection as u32;
6744    let (idx, found) = (num_regs + 5, num_regs + 6);
6745    i32_const(code, 0);
6746    local_set(code, idx);
6747    i32_const(code, 0);
6748    local_set(code, found);
6749    code.push(0x02);
6750    code.push(0x40);
6751    code.push(0x03);
6752    code.push(0x40);
6753    local_get(code, idx);
6754    local_get(code, c);
6755    i32_load(code, 0); // num
6756    code.push(0x4E); // i32.ge_s
6757    code.push(0x0D);
6758    leb_u32(code, 1); // br_if block
6759    // data[idx] (element / key at offset 0) == value ? — a `Set of Text` compares by bytes (stride
6760    // is 8, a set), an Int set / Map key by `i64.eq` at its stride.
6761    if set_text {
6762        emit_set_elem_eq(code, num_regs, c, idx, value as u32, true);
6763    } else {
6764        local_get(code, c);
6765        i32_load(code, 8);
6766        local_get(code, idx);
6767        i32_const(code, stride);
6768        code.push(0x6C);
6769        code.push(0x6A);
6770        i64_load(code, 0);
6771        local_get(code, value as u32);
6772        code.push(0x51); // i64.eq
6773    }
6774    code.push(0x04);
6775    code.push(0x40); // if (match)
6776    // swap-remove: copy the last slot over slot idx (one i64 for a Set, two for a Map entry)
6777    let offs: &[u32] = if stride == 16 { &[0, 8] } else { &[0] };
6778    for &off in offs {
6779        local_get(code, c);
6780        i32_load(code, 8);
6781        local_get(code, idx);
6782        i32_const(code, stride);
6783        code.push(0x6C);
6784        code.push(0x6A); // dst slot base
6785        local_get(code, c);
6786        i32_load(code, 8);
6787        local_get(code, c);
6788        i32_load(code, 0);
6789        i32_const(code, 1);
6790        code.push(0x6B); // i32.sub → num-1
6791        i32_const(code, stride);
6792        code.push(0x6C);
6793        code.push(0x6A); // src (last) slot base
6794        i64_load(code, off);
6795        i64_store(code, off);
6796    }
6797    // num -= 1
6798    local_get(code, c);
6799    local_get(code, c);
6800    i32_load(code, 0);
6801    i32_const(code, 1);
6802    code.push(0x6B);
6803    i32_store(code, 0);
6804    i32_const(code, 1);
6805    local_set(code, found);
6806    code.push(0x0C);
6807    leb_u32(code, 2); // br block (removed)
6808    code.push(0x0B); // end if
6809    local_get(code, idx);
6810    i32_const(code, 1);
6811    code.push(0x6A);
6812    local_set(code, idx);
6813    code.push(0x0C);
6814    leb_u32(code, 0);
6815    code.push(0x0B);
6816    code.push(0x0B);
6817    Ok(())
6818}
6819
6820/// `Set of Int` add (`Add value to s`): linear-scan for `value`; if already present do nothing,
6821/// else append it (reallocating the element buffer, like `ListPush`). `value` must be `Int`.
6822fn lower_set_add(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, set: u16, value: u16) -> R<()> {
6823    let is_text = matches!(kinds.get(set as usize), Some(Kind::SetText) | Some(Kind::CrdtSetText));
6824    if is_text {
6825        if kinds.get(value as usize) != Some(Kind::Text) {
6826            return Err(WasmLowerError::Unsupported("adding a non-Text value to a Set of Text"));
6827        }
6828    } else if kinds.get(value as usize) != Some(Kind::Int) {
6829        return Err(WasmLowerError::Unsupported("set with a non-Int value (only Set of Int/Text)"));
6830    }
6831    emit_set_add_elem(code, ctx, num_regs, set as u32, value as u32, is_text);
6832    Ok(())
6833}
6834
6835/// The add-if-absent core of [`lower_set_add`], over raw locals: scan the set whose handle is in
6836/// `set` for the value already held in `elem`; if absent, append it (realloc, like `ListPush`).
6837/// Uses the `+5`/`+6`/`+7` i32 scratch (`idx`/`found`/`new`) and an 8-byte stride. `set` and
6838/// `elem` must be locals untouched by those scratch slots — so union/intersect can drive it in a
6839/// loop with their own outer counter (`+9`) and loaded element (`+1`). `is_text` selects BYTE
6840/// equality (a `Set of Text`; the slot's low word is a Text handle) over `i64.eq`.
6841fn emit_set_add_elem(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, s: u32, elem: u32, is_text: bool) {
6842    let (idx, found, new) = (num_regs + 5, num_regs + 6, num_regs + 7);
6843    // scan for an existing equal value
6844    i32_const(code, 0);
6845    local_set(code, idx);
6846    i32_const(code, 0);
6847    local_set(code, found);
6848    code.push(0x02);
6849    code.push(0x40);
6850    code.push(0x03);
6851    code.push(0x40);
6852    local_get(code, idx);
6853    local_get(code, s);
6854    i32_load(code, 0); // num
6855    code.push(0x4E); // i32.ge_s
6856    code.push(0x0D);
6857    leb_u32(code, 1); // br_if block
6858    emit_set_elem_eq(code, num_regs, s, idx, elem, is_text); // set[idx] == elem ?
6859    code.push(0x04);
6860    code.push(0x40); // if (present)
6861    i32_const(code, 1);
6862    local_set(code, found);
6863    code.push(0x0C);
6864    leb_u32(code, 2); // br block (already a member)
6865    code.push(0x0B);
6866    local_get(code, idx);
6867    i32_const(code, 1);
6868    code.push(0x6A);
6869    local_set(code, idx);
6870    code.push(0x0C);
6871    leb_u32(code, 0);
6872    code.push(0x0B);
6873    code.push(0x0B);
6874    // if not present: append (realloc to num+1 elements, like ListPush)
6875    local_get(code, found);
6876    code.push(0x45); // i32.eqz
6877    code.push(0x04);
6878    code.push(0x40); // if (!present)
6879    local_get(code, s);
6880    i32_load(code, 0);
6881    i32_const(code, 1);
6882    code.push(0x6A);
6883    i32_const(code, 8);
6884    code.push(0x6C);
6885    emit_alloc(code, ctx,new); // new = alloc((num+1)*8)
6886    emit_seq_copy(code, idx, new, s, s, false); // copy the num existing values
6887    local_get(code, new);
6888    local_get(code, s);
6889    i32_load(code, 0);
6890    i32_const(code, 8);
6891    code.push(0x6C);
6892    code.push(0x6A);
6893    local_get(code, elem);
6894    if is_text {
6895        // The value is a Text handle: store it in the slot's low word (the freshly-alloc'd buffer's
6896        // high word is zero, matching how the Int path leaves it).
6897        i32_store(code, 0); // new[num] = handle
6898    } else {
6899        i64_store(code, 0); // new[num] = value
6900    }
6901    local_get(code, s);
6902    local_get(code, new);
6903    i32_store(code, 8); // data_ptr = new
6904    local_get(code, s);
6905    local_get(code, s);
6906    i32_load(code, 0);
6907    i32_const(code, 1);
6908    code.push(0x6A);
6909    i32_store(code, 0); // num += 1
6910    code.push(0x0B); // end if (!present)
6911}
6912
6913/// Set membership of the i64 in `elem` against the set whose handle is in `set` → `1`/`0` in `out`
6914/// (using `idx` as the byte-loop counter). The value-local form of [`lower_contains`]'s scan, so
6915/// it can gate per-element copies inside the intersection loop.
6916fn emit_set_contains_to(code: &mut Vec<u8>, set: u32, elem: u32, out: u32, idx: u32) {
6917    i32_const(code, 0);
6918    local_set(code, out); // assume absent
6919    i32_const(code, 0);
6920    local_set(code, idx);
6921    code.push(0x02);
6922    code.push(0x40); // block $exit
6923    code.push(0x03);
6924    code.push(0x40); // loop $loop
6925    local_get(code, idx);
6926    local_get(code, set);
6927    i32_load(code, 0); // num
6928    code.push(0x4E); // i32.ge_s
6929    code.push(0x0D);
6930    leb_u32(code, 1); // br_if $exit
6931    local_get(code, set);
6932    i32_load(code, 8); // data_ptr
6933    local_get(code, idx);
6934    i32_const(code, 8);
6935    code.push(0x6C);
6936    code.push(0x6A);
6937    i64_load(code, 0); // element
6938    local_get(code, elem);
6939    code.push(0x51); // i64.eq
6940    code.push(0x04);
6941    code.push(0x40); // if (present)
6942    i32_const(code, 1);
6943    local_set(code, out);
6944    code.push(0x0C);
6945    leb_u32(code, 2); // br $exit
6946    code.push(0x0B); // end if
6947    local_get(code, idx);
6948    i32_const(code, 1);
6949    code.push(0x6A);
6950    local_set(code, idx);
6951    code.push(0x0C);
6952    leb_u32(code, 0); // br $loop
6953    code.push(0x0B); // end loop
6954    code.push(0x0B); // end block
6955}
6956
6957/// Walk the source set `src` element-by-element (outer counter in `+9`, each element loaded into the
6958/// i64 scratch `+1`) and feed each through [`emit_set_add_elem`] into `result` (whose add-if-absent
6959/// dedups). With `filter = Some(other)`, only elements also present in `other` are copied — the
6960/// intersection gate. The inner add uses `+5`/`+6`/`+7`; the membership scan uses `+10`/`+11`; none
6961/// collide with the outer counter, the element, or `result` (`+8`), so the nest is sound.
6962fn emit_set_copy_loop(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, result: u32, src: u32, filter: Option<u32>) {
6963    let outer = num_regs + 9;
6964    let elem = num_regs + 1;
6965    let (cidx, cfound) = (num_regs + 10, num_regs + 11);
6966    i32_const(code, 0);
6967    local_set(code, outer);
6968    code.push(0x02);
6969    code.push(0x40); // block $exit
6970    code.push(0x03);
6971    code.push(0x40); // loop $loop
6972    local_get(code, outer);
6973    local_get(code, src);
6974    i32_load(code, 0); // len(src)
6975    code.push(0x4E); // i32.ge_s
6976    code.push(0x0D);
6977    leb_u32(code, 1); // br_if $exit
6978    // elem = src.data[outer*8]
6979    local_get(code, src);
6980    i32_load(code, 8);
6981    local_get(code, outer);
6982    i32_const(code, 8);
6983    code.push(0x6C);
6984    code.push(0x6A);
6985    i64_load(code, 0);
6986    local_set(code, elem);
6987    match filter {
6988        None => emit_set_add_elem(code, ctx, num_regs, result, elem, false),
6989        Some(other) => {
6990            emit_set_contains_to(code, other, elem, cfound, cidx);
6991            local_get(code, cfound);
6992            code.push(0x04);
6993            code.push(0x40); // if (in `other`)
6994            emit_set_add_elem(code, ctx, num_regs, result, elem, false);
6995            code.push(0x0B); // end if
6996        }
6997    }
6998    // outer++
6999    local_get(code, outer);
7000    i32_const(code, 1);
7001    code.push(0x6A);
7002    local_set(code, outer);
7003    code.push(0x0C);
7004    leb_u32(code, 0); // br $loop
7005    code.push(0x0B); // end loop
7006    code.push(0x0B); // end block
7007}
7008
7009/// `a union b` — a fresh `Set` of `a`'s elements (in `a`'s order) followed by `b`'s not-already-
7010/// present elements (in `b`'s order), matching `semantics::collections::union` byte-for-byte
7011/// (`add`'s dedup makes empty-then-add-all-of-a-then-add-all-of-b equal to clone-a-then-add-b,
7012/// since a Set has no internal duplicates). Built into the `+8` scratch handle, then bound to
7013/// `dst` — so a `dst` aliasing `lhs`/`rhs` reads the originals fully before being overwritten.
7014fn lower_union(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, lhs: u16, rhs: u16) -> R<()> {
7015    require_set(kinds, lhs)?;
7016    require_set(kinds, rhs)?;
7017    let result = num_regs + 8;
7018    emit_empty_header(code, ctx, num_regs, result);
7019    emit_set_copy_loop(code, ctx, num_regs, result, lhs as u32, None);
7020    emit_set_copy_loop(code, ctx, num_regs, result, rhs as u32, None);
7021    local_get(code, result);
7022    local_set(code, dst as u32);
7023    Ok(())
7024}
7025
7026/// `a intersection b` — a fresh `Set` of `a`'s elements (in `a`'s order) that are also in `b`,
7027/// matching `semantics::collections::intersection`. Same build-into-`+8`-then-bind discipline as
7028/// [`lower_union`].
7029fn lower_intersect(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, lhs: u16, rhs: u16) -> R<()> {
7030    require_set(kinds, lhs)?;
7031    require_set(kinds, rhs)?;
7032    let result = num_regs + 8;
7033    emit_empty_header(code, ctx, num_regs, result);
7034    emit_set_copy_loop(code, ctx, num_regs, result, lhs as u32, Some(rhs as u32));
7035    local_get(code, result);
7036    local_set(code, dst as u32);
7037    Ok(())
7038}
7039
7040/// Require a `Set` operand for union/intersection (our Sets only ever hold Int, so a `Set` kind is
7041/// enough to license the 8-byte-stride element copy).
7042fn require_set(kinds: &KindTable, r: u16) -> R<()> {
7043    if kinds.get(r as usize) == Some(Kind::Set) {
7044        Ok(())
7045    } else {
7046        Err(WasmLowerError::Unsupported("union/intersect of a non-Set value"))
7047    }
7048}
7049
7050/// `seq contains value` / `set contains value` — a linear membership scan (value equality,
7051/// matching the tree-walker's `List::position` / set membership): `dst = 1` if any element equals
7052/// `value`, else `0`. Int/Set-of-Int compare with `i64.eq`, Float with `f64.eq`.
7053fn lower_contains(code: &mut Vec<u8>, kinds: &KindTable, num_regs: u32, dst: u16, collection: u16, value: u16) -> R<()> {
7054    let set_text = matches!(kinds.get(collection as usize), Some(Kind::SetText) | Some(Kind::CrdtSetText));
7055    let elem = match kinds.get(collection as usize) {
7056        Some(Kind::Set) => Kind::Int, // a Set of Int holds i64 values
7057        Some(Kind::SetText) | Some(Kind::CrdtSetText) => Kind::Text, // a Set of Text scans by byte equality
7058        _ => seq_elem_kind(kinds, collection)?,
7059    };
7060    // A `Set of Text` `contains` DOES value (byte) equality (below); a plain `seq of Text contains
7061    // text` is still deferred (the scalar membership scan would wrongly compare Text handle pointers).
7062    if elem == Kind::Text && !set_text {
7063        return Err(WasmLowerError::Unsupported("contains over a sequence of Text (needs byte-equality)"));
7064    }
7065    let (elem_load, elem_eq): (fn(&mut Vec<u8>, u32), u8) =
7066        if elem == Kind::Float { (f64_load, 0x61) } else { (i64_load, 0x51) };
7067    let idx = num_regs + 5; // i32 scratch
7068    let col = collection as u32;
7069    // dst = 0
7070    code.push(0x42);
7071    leb_i64(code, 0);
7072    local_set(code, dst as u32);
7073    // idx = 0
7074    i32_const(code, 0);
7075    local_set(code, idx);
7076    code.push(0x02);
7077    code.push(0x40); // block $exit
7078    code.push(0x03);
7079    code.push(0x40); // loop $loop
7080    // if idx >= len: break
7081    local_get(code, idx);
7082    local_get(code, col);
7083    i32_load(code, 0); // len
7084    code.push(0x4E); // i32.ge_s
7085    code.push(0x0D);
7086    leb_u32(code, 1); // br_if $exit
7087    // element == value ? — a Set of Text compares bytes, everything else `i64.eq`/`f64.eq`.
7088    if set_text {
7089        emit_set_elem_eq(code, num_regs, col, idx, value as u32, true);
7090    } else {
7091        local_get(code, col);
7092        i32_load(code, 8); // data_ptr
7093        local_get(code, idx);
7094        i32_const(code, 8);
7095        code.push(0x6C);
7096        code.push(0x6A);
7097        elem_load(code, 0);
7098        local_get(code, value as u32);
7099        code.push(elem_eq);
7100    }
7101    code.push(0x04);
7102    code.push(0x40); // if (void)
7103    code.push(0x42);
7104    leb_i64(code, 1);
7105    local_set(code, dst as u32); // dst = 1
7106    code.push(0x0C);
7107    leb_u32(code, 2); // br $exit (out of if → loop → block)
7108    code.push(0x0B); // end if
7109    // idx++
7110    local_get(code, idx);
7111    i32_const(code, 1);
7112    code.push(0x6A);
7113    local_set(code, idx);
7114    code.push(0x0C);
7115    leb_u32(code, 0); // br $loop
7116    code.push(0x0B); // end loop
7117    code.push(0x0B); // end block
7118    Ok(())
7119}
7120
7121/// `lhs + rhs` (the tree-walker's `arith::concat`): stringify each operand and concatenate the
7122/// UTF-8 bytes into a fresh `Text`. A `Text` operand is its own bytes; an `Int` operand is
7123/// formatted by the host `fmt_i64_into` (string interpolation `"… {n} …"` lowers to this). Other
7124/// operand kinds (Float/Bool stringification) are not yet built.
7125fn lower_concat(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, lhs: u16, rhs: u16) -> R<()> {
7126    // Stringify each operand into a Text handle held in a dedicated scratch local (the byte-copy
7127    // below reads their headers; `emit_stringify` only borrows the +5/+6/+7 temps, so the two
7128    // handles in +8/+9 survive across both calls and the copy).
7129    let (a, b) = (num_regs + 8, num_regs + 9);
7130    emit_stringify(code, ctx, num_regs, lhs as u32, kinds.get(lhs as usize), a)?;
7131    emit_stringify(code, ctx, num_regs, rhs as u32, kinds.get(rhs as usize), b)?;
7132    emit_text_concat(code, ctx, num_regs, a, b, dst as u32);
7133    Ok(())
7134}
7135
7136/// The alignment + width of a non-precision format spec: `>N` (right, code 0), `<N` (left, 1), `^N`
7137/// (center, 2), or a bare width `N` (right — matching `apply_format_spec`'s default). `None` for a spec
7138/// this backend doesn't lower (e.g. the `$` currency spec).
7139fn parse_align_spec(spec: &str) -> Option<(i32, u32)> {
7140    if let Some(first) = spec.as_bytes().first() {
7141        let align = match first {
7142            b'>' => Some(0),
7143            b'<' => Some(1),
7144            b'^' => Some(2),
7145            _ => None,
7146        };
7147        if let Some(a) = align {
7148            return spec[1..].parse::<u32>().ok().map(|w| (a, w));
7149        }
7150    }
7151    // A bare width is right-aligned (`format!("{:>w$}", s)`).
7152    spec.parse::<u32>().ok().map(|w| (0, w))
7153}
7154
7155/// `"{x:.9}"` / `"{x:>6}"` — a formatted interpolation piece: render `src` under its format spec into a
7156/// fresh `Text` (the VM's `apply_format_spec`). Two families are lowered: `.N` precision (numeric →
7157/// `format!("{:.N}", val)`, the `fmt_f64_prec_into` host) and alignment/width (`>N`/`<N`/`^N`/bare-`N` →
7158/// stringify the value then pad to width via `fmt_align_into`). The spec is a compile-time Text
7159/// constant. The `$` currency spec and the debug prefix (`{x=…}`) are refused (a documented deferral).
7160fn lower_format_value(
7161    code: &mut Vec<u8>,
7162    kinds: &KindTable,
7163    ctx: &Ctx,
7164    num_regs: u32,
7165    dst: u16,
7166    src: u16,
7167    spec: u32,
7168    debug_prefix: u32,
7169) -> R<()> {
7170    if debug_prefix != u32::MAX {
7171        return Err(WasmLowerError::Unsupported("formatted value with a debug prefix"));
7172    }
7173    let spec_s = match spec {
7174        u32::MAX => return Err(WasmLowerError::Unsupported("formatted value without a spec")),
7175        idx => match ctx.constants.get(idx as usize) {
7176            Some(Constant::Text(s)) => s.as_str(),
7177            _ => return Err(WasmLowerError::Unsupported("format spec is not a text constant")),
7178        },
7179    };
7180    // Alignment / bare-width spec: stringify the value (any stringifiable kind), then pad to `width`
7181    // with spaces via `fmt_align_into` (the SAME Rust `format!` `apply_format_spec` runs).
7182    if !spec_s.starts_with('.') {
7183        let (align, width) = parse_align_spec(spec_s).ok_or(WasmLowerError::Unsupported("unsupported format spec"))?;
7184        let fidx = (ctx.host_index)(HostFn::FmtAlignInto).ok_or(WasmLowerError::Unsupported("align formatter not imported"))?;
7185        let (hdr, data, len, text) = (num_regs + 5, num_regs + 6, num_regs + 7, num_regs + 9);
7186        // text = stringify(src) — a Text handle (`fmt_align_into` reads its bytes)
7187        emit_stringify(code, ctx, num_regs, src as u32, kinds.get(src as usize), text)?;
7188        // data = alloc(text.len + width) — padding adds at most `width` single-byte spaces
7189        local_get(code, text);
7190        i32_load(code, 0);
7191        i32_const(code, width as i32);
7192        code.push(0x6A); // i32.add
7193        emit_alloc(code, ctx, data);
7194        // len = fmt_align_into(data, text, width, align)
7195        local_get(code, data);
7196        local_get(code, text);
7197        i32_const(code, width as i32);
7198        i32_const(code, align);
7199        code.push(0x10); // call
7200        leb_u32(code, fidx);
7201        local_set(code, len);
7202        // hdr = alloc(16); header = [len, cap=len, data_ptr=data]
7203        i32_const(code, 16);
7204        emit_alloc(code, ctx, hdr);
7205        for off in [0u32, 4] {
7206            local_get(code, hdr);
7207            local_get(code, len);
7208            i32_store(code, off);
7209        }
7210        local_get(code, hdr);
7211        local_get(code, data);
7212        i32_store(code, 8);
7213        local_get(code, hdr);
7214        local_set(code, dst as u32);
7215        return Ok(());
7216    }
7217    let prec = spec_s
7218        .strip_prefix('.')
7219        .and_then(|r| r.parse::<u32>().ok())
7220        .ok_or(WasmLowerError::Unsupported("unsupported format spec (only `.N` precision)"))?;
7221    match kinds.get(src as usize) {
7222        Some(Kind::Float) | Some(Kind::Int) | Some(Kind::Bool) => {}
7223        _ => return Err(WasmLowerError::Unsupported("format `.N` on a non-numeric value")),
7224    }
7225    let (hdr, data, len) = (num_regs + 5, num_regs + 6, num_regs + 7);
7226    // hdr = alloc(16); data = alloc(340 + prec) bytes (worst-case f64 integer width + the decimals)
7227    i32_const(code, 16);
7228    emit_alloc(code, ctx,hdr);
7229    i32_const(code, (340 + prec) as i32);
7230    emit_alloc(code, ctx,data);
7231    // len = fmt_f64_prec_into(data, src as f64, prec)
7232    let fidx = (ctx.host_index)(HostFn::FmtF64PrecInto).ok_or(WasmLowerError::Unsupported("precision formatter not imported"))?;
7233    local_get(code, data);
7234    push_as_f64(code, src, kinds.get(src as usize))?;
7235    i32_const(code, prec as i32);
7236    code.push(0x10); // call
7237    leb_u32(code, fidx);
7238    local_set(code, len);
7239    // header: len = cap = len; data_ptr = data
7240    for off in [0u32, 4] {
7241        local_get(code, hdr);
7242        local_get(code, len);
7243        i32_store(code, off);
7244    }
7245    local_get(code, hdr);
7246    local_get(code, data);
7247    i32_store(code, 8);
7248    local_get(code, hdr);
7249    local_set(code, dst as u32);
7250    Ok(())
7251}
7252
7253/// Concatenate the two `Text` handles in locals `a` and `b` into a fresh `Text` whose handle is
7254/// stored in local `out`. Uses the +5/+6/+7 scratch as temps (so `a`/`b`/`out` must be other
7255/// locals); `out` may alias `a` (all reads happen before the final store).
7256fn emit_text_concat(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, a: u32, b: u32, out: u32) {
7257    let (hdr, data, idx) = (num_regs + 5, num_regs + 6, num_regs + 7);
7258    i32_const(code, 16);
7259    emit_alloc(code, ctx,hdr);
7260    // data = alloc(len_a + len_b) bytes (the allocator re-aligns the next pointer, so the byte
7261    // buffer need not be padded)
7262    local_get(code, a);
7263    i32_load(code, 0);
7264    local_get(code, b);
7265    i32_load(code, 0);
7266    code.push(0x6A); // len_a + len_b
7267    emit_alloc(code, ctx,data);
7268    emit_byte_copy(code, idx, data, a, a, false); // a bytes at [0, len_a)
7269    emit_byte_copy(code, idx, data, a, b, true); // b bytes at [len_a, len_a+len_b)
7270    for off in [0u32, 4] {
7271        local_get(code, hdr);
7272        local_get(code, a);
7273        i32_load(code, 0);
7274        local_get(code, b);
7275        i32_load(code, 0);
7276        code.push(0x6A);
7277        i32_store(code, off);
7278    }
7279    local_get(code, hdr);
7280    local_get(code, data);
7281    i32_store(code, 8);
7282    local_get(code, hdr);
7283    local_set(code, out);
7284}
7285
7286/// Store opcode for an 8-byte struct field slot, by the value's kind (`i64`/`f64`/`i32`-handle).
7287/// An unknown kind cannot be stored at a definite width, so it is refused.
7288fn emit_slot_store(code: &mut Vec<u8>, k: Option<Kind>, off: u32) -> R<()> {
7289    match k {
7290        Some(Kind::Float) => f64_store(code, off),
7291        Some(Kind::Int) | Some(Kind::Bool) | Some(Kind::Char) | Some(Kind::Moment) | Some(Kind::Duration) | Some(Kind::Time) | Some(Kind::Span) | Some(Kind::Word64) => i64_store(code, off),
7292        Some(Kind::Date) | Some(Kind::SeqInt) | Some(Kind::SeqBool) | Some(Kind::SeqFloat) | Some(Kind::SeqText) | Some(Kind::SeqStruct) | Some(Kind::SeqEnum) | Some(Kind::SeqSeqInt) | Some(Kind::SeqAny) | Some(Kind::Text) | Some(Kind::Struct) | Some(Kind::Map) | Some(Kind::Set) | Some(Kind::SetText) | Some(Kind::CrdtSetText) | Some(Kind::Enum) | Some(Kind::Closure) | Some(Kind::Tuple) | Some(Kind::Rational) | Some(Kind::Optional) | Some(Kind::Word32) | Some(Kind::SeqWord32) | Some(Kind::SeqWord64) | Some(Kind::BigInt) | Some(Kind::Complex) | Some(Kind::Modular) | Some(Kind::Decimal) | Some(Kind::Money) | Some(Kind::Quantity) | Some(Kind::Uuid) | Some(Kind::Lanes) | Some(Kind::LanesV) | Some(Kind::Dynamic) => {
7293            i32_store(code, off)
7294        }
7295        None => return Err(WasmLowerError::Unsupported("struct field of unknown kind")),
7296    }
7297    Ok(())
7298}
7299
7300/// Load opcode for an 8-byte struct field slot — the mirror of [`emit_slot_store`].
7301fn emit_slot_load(code: &mut Vec<u8>, k: Option<Kind>, off: u32) -> R<()> {
7302    match k {
7303        Some(Kind::Float) => f64_load(code, off),
7304        Some(Kind::Int) | Some(Kind::Bool) | Some(Kind::Char) | Some(Kind::Moment) | Some(Kind::Duration) | Some(Kind::Time) | Some(Kind::Span) | Some(Kind::Word64) => i64_load(code, off),
7305        Some(Kind::Date) | Some(Kind::SeqInt) | Some(Kind::SeqBool) | Some(Kind::SeqFloat) | Some(Kind::SeqText) | Some(Kind::SeqStruct) | Some(Kind::SeqEnum) | Some(Kind::SeqSeqInt) | Some(Kind::SeqAny) | Some(Kind::Text) | Some(Kind::Struct) | Some(Kind::Map) | Some(Kind::Set) | Some(Kind::SetText) | Some(Kind::CrdtSetText) | Some(Kind::Enum) | Some(Kind::Closure) | Some(Kind::Tuple) | Some(Kind::Rational) | Some(Kind::Optional) | Some(Kind::Word32) | Some(Kind::SeqWord32) | Some(Kind::SeqWord64) | Some(Kind::BigInt) | Some(Kind::Complex) | Some(Kind::Modular) | Some(Kind::Decimal) | Some(Kind::Money) | Some(Kind::Quantity) | Some(Kind::Uuid) | Some(Kind::Lanes) | Some(Kind::LanesV) | Some(Kind::Dynamic) => {
7306            i32_load(code, off)
7307        }
7308        None => return Err(WasmLowerError::Unsupported("struct field of unknown kind")),
7309    }
7310    Ok(())
7311}
7312
7313/// `a new T with …` (`NewStruct`) — bump-allocate the header + a `count`-slot (8 bytes each) field
7314/// buffer (`count` from the static layout). Slots are filled by the following `StructInsert`s
7315/// (every field is inserted, provided or default-filled), so they need no zero-init.
7316fn lower_new_struct(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, count: u16, dst: u16) {
7317    let (hdr, data) = (num_regs + 5, num_regs + 6);
7318    i32_const(code, 16);
7319    emit_alloc(code, ctx,hdr);
7320    i32_const(code, i32::from(count) * 8);
7321    emit_alloc(code, ctx,data);
7322    local_get(code, hdr);
7323    i32_const(code, i32::from(count));
7324    i32_store(code, 0); // num_fields
7325    local_get(code, hdr);
7326    i32_const(code, i32::from(count));
7327    i32_store(code, 4); // cap
7328    local_get(code, hdr);
7329    local_get(code, data);
7330    i32_store(code, 8); // data_ptr
7331    local_get(code, hdr);
7332    local_set(code, dst as u32);
7333}
7334
7335/// `Set obj's field to value` (`StructInsert`) — store the value into its static slot, at the
7336/// width of the value's kind.
7337/// Which `StructInsert`s need COPY-ON-WRITE — a flow-sensitive uniqueness pass that keeps Logos's
7338/// struct VALUE semantics without cloning on the read path. Structs are heap objects behind shared
7339/// handles here, but the tree-walker/VM copy a struct on field access and assignment, so mutating an
7340/// extracted or aliased struct must not write through to the original. Rather than clone on every
7341/// `GetField` (the hot read path), we clone lazily at the WRITE: a `StructInsert` whose target is not
7342/// provably a uniquely-owned fresh struct copies first, then mutates the copy.
7343///
7344/// `owned` holds registers currently bound to a unique, unaliased struct: set by `NewStruct`/
7345/// `DeepClone`; preserved across the construction `StructInsert`s that fill it and across `GetField`
7346/// reads of it (reading a field does not alias the struct); cleared the moment the struct is consumed
7347/// as a value anywhere else (stored into a field/collection, moved, returned, passed to a call — any
7348/// op's def/use footprint, via the exhaustive [`regsplit::op_def_uses`], so no aliasing operand is
7349/// missed). State is reset at every basic-block leader, a conservative join (a struct owned on only
7350/// some incoming paths is treated as shared → it copies, never miscompiles). So construction and
7351/// reads cost nothing; only a write to a possibly-shared struct pays one copy.
7352fn cow_struct_inserts(ops: &[Op], num_regs: u32, functions: &[CompiledFunction]) -> Vec<bool> {
7353    let mut cow = vec![false; ops.len()];
7354    let Some(blocks) = Blocks::new(ops) else {
7355        // Not self-contained (rejected downstream); be safe and copy every struct write.
7356        for (pc, op) in ops.iter().enumerate() {
7357            if matches!(op, Op::StructInsert { .. }) {
7358                cow[pc] = true;
7359            }
7360        }
7361        return cow;
7362    };
7363    let mut owned = vec![false; num_regs as usize];
7364    let is_owned = |owned: &[bool], r: u16| (r as usize) < owned.len() && owned[r as usize];
7365    let set_owned = |owned: &mut [bool], r: u16, v: bool| {
7366        if (r as usize) < owned.len() {
7367            owned[r as usize] = v;
7368        }
7369    };
7370    for (pc, op) in ops.iter().enumerate() {
7371        if blocks.is_leader(pc) {
7372            owned.iter_mut().for_each(|o| *o = false);
7373        }
7374        match op {
7375            Op::NewStruct { dst, .. } | Op::DeepClone { dst, .. } => set_owned(&mut owned, *dst, true),
7376            Op::StructInsert { obj, value, .. } => {
7377                if !is_owned(&owned, *obj) {
7378                    cow[pc] = true; // mutating a possibly-shared struct → copy first
7379                }
7380                set_owned(&mut owned, *value, false); // the stored value is now aliased by obj.field
7381                set_owned(&mut owned, *obj, true); // obj is uniquely owned after (fresh copy, or already)
7382            }
7383            Op::GetField { dst, .. } => set_owned(&mut owned, *dst, false), // a borrowed field handle
7384            _ => {
7385                let (defs, uses) = regsplit::op_def_uses(op, functions);
7386                for r in defs.into_iter().chain(uses) {
7387                    set_owned(&mut owned, r, false);
7388                }
7389            }
7390        }
7391    }
7392    cow
7393}
7394
7395/// `Set obj's field to value` / a construction field-fill (`StructInsert`). When `cow`, the target
7396/// struct may be shared (extracted by `GetField` or stored elsewhere), so — to honor value semantics
7397/// — flat-copy it into a fresh object bound back to `obj` and mutate THAT; otherwise mutate in place.
7398fn lower_struct_insert(
7399    code: &mut Vec<u8>,
7400    kinds: &KindTable,
7401    ctx: &Ctx,
7402    num_regs: u32,
7403    slot: u16,
7404    obj: u16,
7405    value: u16,
7406    cow: bool,
7407) -> R<()> {
7408    // The target must be a known Struct handle (an i32). A struct param has no static layout, so it
7409    // is typed as a scalar here — reject rather than treat the scalar as a heap pointer (which the
7410    // copy-on-write below would, emitting invalid wasm).
7411    if kinds.get(obj as usize) != Some(Kind::Struct) {
7412        return Err(WasmLowerError::Unsupported("struct field set on a value with no known struct layout"));
7413    }
7414    if cow {
7415        let (hdr, data, idx) = (num_regs + 5, num_regs + 6, num_regs + 7);
7416        emit_buffer_clone(code, ctx, hdr, data, idx, obj as u32, false);
7417        local_get(code, hdr);
7418        local_set(code, obj as u32);
7419    }
7420    let off = u32::from(slot) * 8;
7421    local_get(code, obj as u32);
7422    i32_load(code, 8); // data_ptr
7423    local_get(code, value as u32);
7424    emit_slot_store(code, kinds.get(value as usize), off)
7425}
7426
7427/// `obj's field` (`GetField`) — load the value from its static slot, at the width of the field's
7428/// kind (the inferred kind of `dst`).
7429fn lower_get_field(code: &mut Vec<u8>, kinds: &KindTable, slot: u16, dst: u16, obj: u16) -> R<()> {
7430    if kinds.get(obj as usize) != Some(Kind::Struct) {
7431        return Err(WasmLowerError::Unsupported("field access on a value with no known struct layout"));
7432    }
7433    let off = u32::from(slot) * 8;
7434    local_get(code, obj as u32);
7435    i32_load(code, 8); // data_ptr
7436    emit_slot_load(code, kinds.get(dst as usize), off)?;
7437    local_set(code, dst as u32);
7438    Ok(())
7439}
7440
7441/// Whether a value of `kind` is its own deep clone — a scalar with no sub-structure to copy
7442/// recursively (`Int`/`Bool`/`Char`/`Float`/`Date`/`Moment`). A handle kind (Text/Seq/Struct/…) is not.
7443fn is_clone_trivial(kind: Option<Kind>) -> bool {
7444matches!(kind, Some(Kind::Int | Kind::Bool | Kind::Char | Kind::Float | Kind::Date | Kind::Moment | Kind::Duration | Kind::Time | Kind::Span))
7445}
7446
7447/// A struct field this backend can deep-clone: a scalar (copied flat with the field buffer) or a
7448/// `Text`/`Set`/scalar-sequence handle (clonable into an independent buffer one level down). A field
7449/// that is itself a struct / map / enum / nested-handle sequence needs deeper recursion and is not
7450/// cloned here (the struct clone is then refused, soundly).
7451fn field_clone_ok(kind: Option<Kind>) -> bool {
7452    is_clone_trivial(kind)
7453        || matches!(kind, Some(Kind::Text | Kind::SeqInt | Kind::SeqBool | Kind::SeqFloat | Kind::SeqAny | Kind::Set))
7454}
7455
7456/// Clone a `Text` (byte buffer) or sequence/`Set` (8-byte-slot buffer) whose handle is in `src_local`
7457/// into a fresh, independent object, leaving the new handle in `hdr`. `is_text` selects the copy
7458/// stride; `hdr`/`data`/`idx` are three scratch locals. The fresh buffer is why a later in-place
7459/// mutation of the clone (or the original) cannot be seen through the other.
7460fn emit_buffer_clone(code: &mut Vec<u8>, ctx: &Ctx, hdr: u32, data: u32, idx: u32, src_local: u32, is_text: bool) {
7461    i32_const(code, 16);
7462    emit_alloc(code, ctx,hdr);
7463    local_get(code, src_local);
7464    i32_load(code, 0); // len (bytes for Text, element count otherwise)
7465    if !is_text {
7466        i32_const(code, 8);
7467        code.push(0x6C); // i32.mul — 8-byte slots
7468    }
7469    emit_alloc(code, ctx,data);
7470    if is_text {
7471        emit_byte_copy(code, idx, data, src_local, src_local, false);
7472    } else {
7473        emit_seq_copy(code, idx, data, src_local, src_local, false);
7474    }
7475    for off in [0u32, 4] {
7476        local_get(code, hdr);
7477        local_get(code, src_local);
7478        i32_load(code, 0); // len → both len and cap
7479        i32_store(code, off);
7480    }
7481    local_get(code, hdr);
7482    local_get(code, data);
7483    i32_store(code, 8); // data_ptr
7484}
7485
7486/// Clone a `Map` — a `num_entries × 16`-byte buffer of `[key@0][value@8]` pairs — whose handle is in
7487/// `src_local`, into a fresh independent map left in `hdr`. Flat entry copy (`num_entries × 2` `i64`
7488/// slots): the corpus maps have Text/scalar keys and scalar values, so the pairs copy by value; a
7489/// handle value would stay shared (a deep value clone is a later refinement). `hdr`/`data`/`idx` are
7490/// three scratch locals. The fresh buffer is why a later `Set item k of map` on the clone (or the
7491/// original) is invisible through the other holder.
7492fn emit_map_clone(code: &mut Vec<u8>, ctx: &Ctx, hdr: u32, data: u32, idx: u32, src_local: u32) {
7493    i32_const(code, 16);
7494    emit_alloc(code, ctx,hdr);
7495    // data = alloc(num_entries * 16)
7496    local_get(code, src_local);
7497    i32_load(code, 0); // num_entries
7498    i32_const(code, 16);
7499    code.push(0x6C);
7500    emit_alloc(code, ctx,data);
7501    // copy num_entries*2 i64 slots (each entry = key slot + value slot)
7502    i32_const(code, 0);
7503    local_set(code, idx);
7504    code.push(0x02);
7505    code.push(0x40); // block
7506    code.push(0x03);
7507    code.push(0x40); // loop
7508    local_get(code, idx);
7509    local_get(code, src_local);
7510    i32_load(code, 0);
7511    i32_const(code, 2);
7512    code.push(0x6C); // num_entries * 2
7513    code.push(0x4E); // idx >= num*2
7514    code.push(0x0D);
7515    leb_u32(code, 1); // br_if block
7516    local_get(code, data);
7517    local_get(code, idx);
7518    i32_const(code, 8);
7519    code.push(0x6C);
7520    code.push(0x6A); // data + idx*8
7521    local_get(code, src_local);
7522    i32_load(code, 8);
7523    local_get(code, idx);
7524    i32_const(code, 8);
7525    code.push(0x6C);
7526    code.push(0x6A); // src_data + idx*8
7527    i64_load(code, 0);
7528    i64_store(code, 0);
7529    local_get(code, idx);
7530    i32_const(code, 1);
7531    code.push(0x6A);
7532    local_set(code, idx);
7533    code.push(0x0C);
7534    leb_u32(code, 0); // br loop
7535    code.push(0x0B); // end loop
7536    code.push(0x0B); // end block
7537    for off in [0u32, 4] {
7538        local_get(code, hdr);
7539        local_get(code, src_local);
7540        i32_load(code, 0);
7541        i32_store(code, off);
7542    }
7543    local_get(code, hdr);
7544    local_get(code, data);
7545    i32_store(code, 8);
7546}
7547
7548/// After a nested sequence's OUTER handle buffer has been flat-cloned into `data` (its element count
7549/// in `outer_hdr`'s word 0), replace each element handle with a deep clone of the value it points to,
7550/// so the clone owns independent inner sequences (a flat copy would share them). `is_text` selects
7551/// the inner copy stride. A runtime loop over the (statically-unknown) element count; scratch
7552/// `counter` plus `isrc`/`ihdr`/`idata`/`iidx` (the inner buffer clone) are all disjoint from
7553/// `outer_hdr`/`data`.
7554#[allow(clippy::too_many_arguments)]
7555fn emit_clone_each_element(
7556    code: &mut Vec<u8>,
7557    ctx: &Ctx,
7558    outer_hdr: u32,
7559    data: u32,
7560    counter: u32,
7561    isrc: u32,
7562    ihdr: u32,
7563    idata: u32,
7564    iidx: u32,
7565    is_text: bool,
7566) {
7567    i32_const(code, 0);
7568    local_set(code, counter);
7569    code.push(0x02);
7570    code.push(0x40); // block
7571    code.push(0x03);
7572    code.push(0x40); // loop
7573    local_get(code, counter);
7574    local_get(code, outer_hdr);
7575    i32_load(code, 0); // element count
7576    code.push(0x4E); // i32.ge_s
7577    code.push(0x0D);
7578    leb_u32(code, 1); // br_if exit
7579    // isrc = data[counter*8] — the inner handle the flat copy duplicated
7580    local_get(code, data);
7581    local_get(code, counter);
7582    i32_const(code, 8);
7583    code.push(0x6C); // i32.mul
7584    code.push(0x6A); // i32.add
7585    i32_load(code, 0);
7586    local_set(code, isrc);
7587    emit_buffer_clone(code, ctx, ihdr, idata, iidx, isrc, is_text);
7588    // data[counter*8] = ihdr (the fresh, independent inner handle)
7589    local_get(code, data);
7590    local_get(code, counter);
7591    i32_const(code, 8);
7592    code.push(0x6C); // i32.mul
7593    code.push(0x6A); // i32.add
7594    local_get(code, ihdr);
7595    i32_store(code, 0);
7596    // counter++
7597    local_get(code, counter);
7598    i32_const(code, 1);
7599    code.push(0x6A); // i32.add
7600    local_set(code, counter);
7601    code.push(0x0C);
7602    leb_u32(code, 0); // br loop
7603    code.push(0x0B); // end loop
7604    code.push(0x0B); // end block
7605}
7606
7607/// `a copy of x` (`DeepClone`) — an independent deep copy. A scalar is its own value (copied); a
7608/// `Text` gets a fresh byte buffer; a sequence/struct of trivially-cloneable (scalar) elements gets
7609/// a fresh element buffer. The new buffer means a later mutation of the clone (or the original)
7610/// cannot be seen through the other — value semantics, matching the tree-walker's `deep_clone`. A
7611/// composite holding HANDLE sub-values (a struct with a Text/Seq field, a Seq of handles) needs a
7612/// recursive clone (a generated per-type helper); deferred — soundly rejected, never miscopied.
7613fn lower_deep_clone(code: &mut Vec<u8>, kinds: &KindTable, structs: &kind::StructLayout, ctx: &Ctx, num_regs: u32, dst: u16, src: u16) -> R<()> {
7614    let (hdr, data, idx) = (num_regs + 5, num_regs + 6, num_regs + 7);
7615    let s = src as u32;
7616    match kinds.get(src as usize) {
7617        // A scalar value is immutable — the clone is just the value.
7618        Some(Kind::Int) | Some(Kind::Bool) | Some(Kind::Float) | Some(Kind::Date) | Some(Kind::Moment) | Some(Kind::Duration) | Some(Kind::Time) | Some(Kind::Span) => {
7619            local_get(code, s);
7620            local_set(code, dst as u32);
7621            return Ok(());
7622        }
7623        // A struct whose fields are scalars and/or clonable handles (`Text`/`Set`/scalar-sequence):
7624        // clone the header + the field buffer (8-byte slots, count = `num_fields`), then RECURSIVELY
7625        // clone each handle field one level so the clone owns independent sub-buffers — a flat copy
7626        // would SHARE a mutable inner sequence, defeating value semantics. Scalar fields are already
7627        // independent after the flat copy. The field layout flows to `dst` (see `struct_layout`), so
7628        // `clone's field` still resolves.
7629        Some(Kind::Struct)
7630            if structs
7631                .reg_layout
7632                .get(&src)
7633                .is_some_and(|l| l.iter().all(|&(_, vr)| field_clone_ok(kinds.get(vr as usize)))) =>
7634        {
7635            i32_const(code, 16);
7636            emit_alloc(code, ctx,hdr);
7637            local_get(code, s);
7638            i32_load(code, 0); // num_fields
7639            i32_const(code, 8);
7640            code.push(0x6C); // i32.mul
7641            emit_alloc(code, ctx,data);
7642            emit_seq_copy(code, idx, data, s, s, false); // flat-copy num_fields 8-byte slots
7643            for off in [0u32, 4] {
7644                local_get(code, hdr);
7645                local_get(code, s);
7646                i32_load(code, 0);
7647                i32_store(code, off);
7648            }
7649            local_get(code, hdr);
7650            local_get(code, data);
7651            i32_store(code, 8);
7652            // Recursively clone each handle field IN PLACE in the cloned buffer (`data`), overwriting
7653            // its flat-copied shared handle with a fresh one. Scratch `+8..+11` is disjoint from the
7654            // struct's `+5..+7`. The layout is compile-time, so this unrolls per handle field.
7655            let (fsrc, fhdr, fdata, fidx) = (num_regs + 8, num_regs + 9, num_regs + 10, num_regs + 11);
7656            let layout = structs.reg_layout.get(&src).expect("guarded by is_some_and above");
7657            for (slot, &(_, vr)) in layout.iter().enumerate() {
7658                let fk = kinds.get(vr as usize);
7659                if is_clone_trivial(fk) {
7660                    continue; // scalar — the flat copy already produced an independent value
7661                }
7662                let off = slot as u32 * 8;
7663                local_get(code, data);
7664                i32_load(code, off); // the shared handle the flat copy duplicated
7665                local_set(code, fsrc);
7666                emit_buffer_clone(code, ctx, fhdr, fdata, fidx, fsrc, fk == Some(Kind::Text));
7667                local_get(code, data);
7668                local_get(code, fhdr); // the fresh, independent handle
7669                i32_store(code, off);
7670            }
7671            local_get(code, hdr);
7672            local_set(code, dst as u32);
7673            return Ok(());
7674        }
7675        // Text → fresh byte buffer; a sequence or `Set of Int` → fresh 8-byte-element buffer (a Set
7676        // is a flat scalar buffer like `SeqInt`, so the same copy is an independent deep clone).
7677        // A `SetText` clones like a `Set` (flat 8-byte-slot copy) — the shared `Text` element handles
7678        // are immutable in Logos, so the clone sharing them is sound (no deeper recursion needed).
7679        Some(k @ (Kind::Text | Kind::SeqInt | Kind::SeqBool | Kind::SeqFloat | Kind::SeqAny | Kind::Set | Kind::SetText)) => {
7680            emit_buffer_clone(code, ctx, hdr, data, idx, s, k == Kind::Text);
7681            local_get(code, hdr);
7682            local_set(code, dst as u32);
7683            Ok(())
7684        }
7685        // A Map — its `[key][value]` entry buffer flat-copied into a fresh, independent map.
7686        Some(Kind::Map) => {
7687            emit_map_clone(code, ctx, hdr, data, idx, s);
7688            local_get(code, hdr);
7689            local_set(code, dst as u32);
7690            Ok(())
7691        }
7692        // A Seq of Seq (an Int matrix): clone the outer handle buffer, then clone EACH inner sequence
7693        // so the rows are independent (a flat copy would share the row handles). `idx` is free again
7694        // after the outer copy, so it doubles as the per-row loop counter; `+8..+11` clone each row.
7695        Some(Kind::SeqSeqInt) => {
7696            emit_buffer_clone(code, ctx, hdr, data, idx, s, false);
7697            let (isrc, ihdr, idata, iidx) = (num_regs + 8, num_regs + 9, num_regs + 10, num_regs + 11);
7698            emit_clone_each_element(code, ctx, hdr, data, idx, isrc, ihdr, idata, iidx, false);
7699            local_get(code, hdr);
7700            local_set(code, dst as u32);
7701            Ok(())
7702        }
7703        _ => Err(WasmLowerError::Unsupported("deep clone of an unsupported value kind")),
7704    }
7705}
7706
7707/// `lhs equals rhs` / `lhs is not rhs` on two `Text` values — byte equality (unequal length ⇒ not
7708/// equal, else compare bytes). Result is a `Bool` i64 0/1 in `dst` (`negate` flips it for `!=`).
7709fn lower_text_eq(code: &mut Vec<u8>, num_regs: u32, dst: u16, lhs: u16, rhs: u16, negate: bool) {
7710    let (a, b) = (lhs as u32, rhs as u32);
7711    let idx = num_regs + 7;
7712    // dst = 1 (assume equal)
7713    code.push(0x42);
7714    leb_i64(code, 1);
7715    local_set(code, dst as u32);
7716    // if len_a != len_b → not equal; else compare bytes
7717    local_get(code, a);
7718    i32_load(code, 0);
7719    local_get(code, b);
7720    i32_load(code, 0);
7721    code.push(0x47); // i32.ne
7722    code.push(0x04);
7723    code.push(0x40); // if (lengths differ)
7724    code.push(0x42);
7725    leb_i64(code, 0);
7726    local_set(code, dst as u32); // dst = 0
7727    code.push(0x05); // else (same length)
7728    i32_const(code, 0);
7729    local_set(code, idx);
7730    code.push(0x02);
7731    code.push(0x40); // block
7732    code.push(0x03);
7733    code.push(0x40); // loop
7734    local_get(code, idx);
7735    local_get(code, a);
7736    i32_load(code, 0);
7737    code.push(0x4E); // i32.ge_s → idx >= len
7738    code.push(0x0D);
7739    leb_u32(code, 1); // br_if block (all matched)
7740    // a[idx] != b[idx] ?
7741    local_get(code, a);
7742    i32_load(code, 8);
7743    local_get(code, idx);
7744    code.push(0x6A);
7745    i32_load8_u(code, 0);
7746    local_get(code, b);
7747    i32_load(code, 8);
7748    local_get(code, idx);
7749    code.push(0x6A);
7750    i32_load8_u(code, 0);
7751    code.push(0x47); // i32.ne
7752    code.push(0x04);
7753    code.push(0x40); // if (mismatch)
7754    code.push(0x42);
7755    leb_i64(code, 0);
7756    local_set(code, dst as u32); // dst = 0
7757    code.push(0x0C);
7758    leb_u32(code, 2); // br block (out of inner-if → loop → block)
7759    code.push(0x0B); // end inner if
7760    local_get(code, idx);
7761    i32_const(code, 1);
7762    code.push(0x6A);
7763    local_set(code, idx);
7764    code.push(0x0C);
7765    leb_u32(code, 0); // br loop
7766    code.push(0x0B); // end loop
7767    code.push(0x0B); // end block
7768    code.push(0x0B); // end outer if
7769    if negate {
7770        local_get(code, dst as u32);
7771        code.push(0x50); // i64.eqz → i32
7772        code.push(0xAD); // i64.extend_i32_u
7773        local_set(code, dst as u32);
7774    }
7775}
7776
7777/// Materialize the value in local `src` (of kind `kind`) as a `Text` handle stored in local `out`.
7778/// A `Text` is itself (just copied); an `Int`/`Float`/`Bool` is formatted into a fresh buffer by
7779/// the matching host formatter (which returns the byte length) and wrapped in a header. Uses the
7780/// +5/+6/+7 scratch as temps. Value-based (a local + kind, not a register) so it serves both
7781/// `Concat`'s operands and `Show`'s slot-loaded struct fields.
7782fn emit_stringify(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, src: u32, kind: Option<Kind>, out: u32) -> R<()> {
7783    match kind {
7784        Some(Kind::Text) => {
7785            local_get(code, src);
7786            local_set(code, out);
7787        }
7788        // A `BigInt` operand of a concat (`"x = " + (2^200)`): render it to a decimal `Text` via the
7789        // runtime, then stringify AS that Text — matching the VM, which appends the decimal to the text.
7790        Some(Kind::BigInt) => {
7791            let to_text = (ctx.host_index)(HostFn::BigintToText).ok_or(WasmLowerError::Unsupported("bigint_to_text not imported"))?;
7792            local_get(code, src);
7793            code.push(0x10); // call
7794            leb_u32(code, to_text);
7795            local_set(code, out);
7796        }
7797        Some(k @ (Kind::Int | Kind::Float | Kind::Bool)) => {
7798            // Stringify a scalar via the matching host formatter writing into a fresh buffer.
7799            let (host, bufsize) = match k {
7800                Kind::Int => (HostFn::FmtI64Into, 24), // ≤ 20 decimal digits + sign
7801                Kind::Float => (HostFn::FmtF64Into, 340), // worst-case shortest-round-trip f64 width (~326)
7802                _ => (HostFn::FmtBoolInto, 8), // "true"/"false"
7803            };
7804            let (h, data, tmp) = (num_regs + 5, num_regs + 6, num_regs + 7);
7805            i32_const(code, 16);
7806            emit_alloc(code, ctx,h);
7807            i32_const(code, bufsize);
7808            emit_alloc(code, ctx,data);
7809            // len = fmt_*_into(data, src)
7810            let fidx = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("text formatter not imported"))?;
7811            local_get(code, data);
7812            local_get(code, src);
7813            code.push(0x10); // call
7814            leb_u32(code, fidx);
7815            local_set(code, tmp); // tmp = len
7816            // header: len = cap = tmp; data_ptr = data
7817            local_get(code, h);
7818            local_get(code, tmp);
7819            i32_store(code, 0);
7820            local_get(code, h);
7821            local_get(code, tmp);
7822            i32_store(code, 4);
7823            local_get(code, h);
7824            local_get(code, data);
7825            i32_store(code, 8);
7826            local_get(code, h);
7827            local_set(code, out);
7828        }
7829        // A whole `Seq of Int` / `Set of Int` operand — the host formats `[e0, …]` / `{e0, …}` out of
7830        // linear memory into a buffer sized from the collection's length (`len*24 + 8`, worst-case i64
7831        // decimal + `", "` + brackets), which is then wrapped in a Text header. Matches
7832        // `RuntimeValue::List`/`Set::to_display_string` (insertion order = the AOT's storage order).
7833        Some(k @ (Kind::SeqInt | Kind::SeqBool | Kind::SeqAny | Kind::Set)) => {
7834            let host = match k {
7835                Kind::Set => HostFn::FmtSetI64Into,
7836                Kind::SeqBool => HostFn::FmtSeqBoolInto, // renders `[true, false, …]` (`len*24+8` is ample)
7837                _ => HostFn::FmtSeqI64Into,
7838            };
7839            let (h, data, tmp) = (num_regs + 5, num_regs + 6, num_regs + 7);
7840            i32_const(code, 16);
7841            emit_alloc(code, ctx,h);
7842            // data = alloc(len*24 + 8)
7843            local_get(code, src);
7844            i32_load(code, 0); // len
7845            i32_const(code, 24);
7846            code.push(0x6C); // i32.mul
7847            i32_const(code, 8);
7848            code.push(0x6A); // + 8
7849            emit_alloc(code, ctx,data);
7850            // len = fmt_{seq,set}_i64_into(data, src)
7851            let fidx = (ctx.host_index)(host).ok_or(WasmLowerError::Unsupported("collection formatter not imported"))?;
7852            local_get(code, data);
7853            local_get(code, src);
7854            code.push(0x10); // call
7855            leb_u32(code, fidx);
7856            local_set(code, tmp);
7857            local_get(code, h);
7858            local_get(code, tmp);
7859            i32_store(code, 0);
7860            local_get(code, h);
7861            local_get(code, tmp);
7862            i32_store(code, 4);
7863            local_get(code, h);
7864            local_get(code, data);
7865            i32_store(code, 8);
7866            local_get(code, h);
7867            local_set(code, out);
7868        }
7869        _ => return Err(WasmLowerError::Unsupported("concat operand kind cannot be stringified yet")),
7870    }
7871    Ok(())
7872}
7873
7874/// Build the display `Text` of the struct in `handle` (type `def`) into `out` — `TypeName { f: v, … }`
7875/// (`RuntimeValue::Struct::to_display_string`) with the fields in DETERMINISTIC alphabetical order (the
7876/// VM sorts its `HashMap` fields by name; this sorts the DECLARED fields the same way). Each field's
7877/// value is loaded from its declared slot (`data_ptr + slot*8`) at the field type's width and
7878/// stringified. An empty struct is just its name. `part`/`field_i32` are caller-supplied scratch kept
7879/// distinct from `out`. The reusable core of [`lower_show_struct`] and [`lower_show_seqstruct`].
7880fn emit_struct_display(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, def: &StructTypeDef, handle: u32, out: u32, part: u32, field_i32: u32) -> R<()> {
7881    let mut order: Vec<usize> = (0..def.fields.len()).collect();
7882    order.sort_by(|&a, &b| def.fields[a].0.cmp(&def.fields[b].0));
7883    if def.fields.is_empty() {
7884        lower_text_literal(code, ctx, num_regs, def.name.as_bytes());
7885        local_set(code, out);
7886        return Ok(());
7887    }
7888    lower_text_literal(code, ctx, num_regs, format!("{} {{ ", def.name).as_bytes());
7889    local_set(code, out);
7890    let n = order.len();
7891    for (j, &i) in order.iter().enumerate() {
7892        let (fname, bt) = &def.fields[i];
7893        let ek = kind::boundary_to_kind(bt);
7894        lower_text_literal(code, ctx, num_regs, format!("{fname}: ").as_bytes());
7895        local_set(code, part);
7896        emit_text_concat(code, ctx, num_regs, out, part, out);
7897        // load field `i`'s slot value (declared slot = `i`, at `data_ptr + i*8`)
7898        let elem_tmp = match ek.map(Kind::wasm_valtype) {
7899            Some(F64) => num_regs + 12,
7900            Some(I64) => num_regs + 1,
7901            _ => field_i32,
7902        };
7903        local_get(code, handle);
7904        i32_load(code, 8); // data_ptr
7905        emit_slot_load(code, ek, (i as u32) * 8)?;
7906        local_set(code, elem_tmp);
7907        emit_stringify(code, ctx, num_regs, elem_tmp, ek, part)?;
7908        emit_text_concat(code, ctx, num_regs, out, part, out);
7909        if j + 1 < n {
7910            lower_text_literal(code, ctx, num_regs, b", ");
7911            local_set(code, part);
7912            emit_text_concat(code, ctx, num_regs, out, part, out);
7913        }
7914    }
7915    lower_text_literal(code, ctx, num_regs, b" }");
7916    local_set(code, part);
7917    emit_text_concat(code, ctx, num_regs, out, part, out);
7918    Ok(())
7919}
7920
7921fn lower_show_struct(code: &mut Vec<u8>, plan: &Plan, ctx: &Ctx, src: u16) -> R<()> {
7922    let type_name = plan
7923        .structs
7924        .struct_name_of
7925        .get(&src)
7926        .ok_or(WasmLowerError::Unsupported("Show of a struct whose type is not statically known"))?;
7927    let def = ctx
7928        .struct_types
7929        .iter()
7930        .find(|s| &s.name == type_name)
7931        .ok_or(WasmLowerError::Unsupported("Show of an unknown struct type"))?;
7932    let print_idx = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("Show struct: print_text not imported"))?;
7933    let num_regs = plan.num_regs;
7934    let out = num_regs + 8;
7935    emit_struct_display(code, ctx, num_regs, def, src as u32, out, num_regs + 9, num_regs + 10)?;
7936    local_get(code, out);
7937    code.push(0x10); // call print_text
7938    leb_u32(code, print_idx);
7939    Ok(())
7940}
7941
7942/// `Show <Seq of Struct>` — `[TypeName { … }, …]` over the sequence in insertion order, each element
7943/// rendered by [`emit_struct_display`]. A RUNTIME loop: element `i` is an i32 struct handle at
7944/// `data_ptr+i*8`; its display is built into `out` and concatenated onto the outer `[…]` accumulator.
7945/// The element struct type comes from `seq_elem_struct_name` (the homogeneous list's element).
7946fn lower_show_seqstruct(code: &mut Vec<u8>, plan: &Plan, ctx: &Ctx, src: u16) -> R<()> {
7947    let type_name = plan
7948        .structs
7949        .seq_elem_struct_name
7950        .get(&src)
7951        .ok_or(WasmLowerError::Unsupported("Show of a Seq of Struct whose element type is not statically known"))?;
7952    let def = ctx
7953        .struct_types
7954        .iter()
7955        .find(|s| &s.name == type_name)
7956        .ok_or(WasmLowerError::Unsupported("Show seq-struct: unknown element struct type"))?;
7957    let print_idx = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("Show seq-struct: print_text not imported"))?;
7958    let num_regs = plan.num_regs;
7959    let m = src as u32;
7960    let (outer_acc, out, i, elem) = (num_regs + 8, num_regs + 9, num_regs + 10, num_regs + 11);
7961    let (part, field_i32) = (num_regs + 13, num_regs + 14);
7962    lower_text_literal(code, ctx, num_regs, b"[");
7963    local_set(code, outer_acc);
7964    i32_const(code, 0);
7965    local_set(code, i);
7966    code.push(0x02);
7967    code.push(0x40); // block
7968    code.push(0x03);
7969    code.push(0x40); // loop
7970    local_get(code, i);
7971    local_get(code, m);
7972    i32_load(code, 0);
7973    code.push(0x4E); // i32.ge_s
7974    code.push(0x0D);
7975    leb_u32(code, 1);
7976    // separator ", " before every element after the first
7977    local_get(code, i);
7978    code.push(0x45);
7979    code.push(0x04);
7980    code.push(0x40);
7981    code.push(0x05);
7982    lower_text_literal(code, ctx, num_regs, b", ");
7983    local_set(code, part);
7984    emit_text_concat(code, ctx, num_regs, outer_acc, part, outer_acc);
7985    code.push(0x0B); // end if
7986    // elem = seq element i (i32 struct handle at data_ptr + i*8)
7987    local_get(code, m);
7988    i32_load(code, 8);
7989    local_get(code, i);
7990    i32_const(code, 8);
7991    code.push(0x6C);
7992    code.push(0x6A);
7993    i32_load(code, 0);
7994    local_set(code, elem);
7995    emit_struct_display(code, ctx, num_regs, def, elem, out, part, field_i32)?;
7996    emit_text_concat(code, ctx, num_regs, outer_acc, out, outer_acc);
7997    local_get(code, i);
7998    i32_const(code, 1);
7999    code.push(0x6A);
8000    local_set(code, i);
8001    code.push(0x0C);
8002    leb_u32(code, 0);
8003    code.push(0x0B); // end loop
8004    code.push(0x0B); // end block
8005    lower_text_literal(code, ctx, num_regs, b"]");
8006    local_set(code, part);
8007    emit_text_concat(code, ctx, num_regs, outer_acc, part, outer_acc);
8008    local_get(code, outer_acc);
8009    code.push(0x10); // call print_text
8010    leb_u32(code, print_idx);
8011    Ok(())
8012}
8013
8014/// `Show <Seq of Seq of Int>` — `[[…], […]]` over the outer sequence in stored (insertion) order,
8015/// matching the VM's `RuntimeValue::List` of `List`s. A RUNTIME loop (outer length is dynamic): each
8016/// outer element is an `i32` handle (low word of its 8-byte slot) to an inner `Seq of Int`, which the
8017/// scalar seq formatter (`emit_stringify` of `Kind::SeqInt` → `fmt_seq_i64_into`) renders as `[e0, …]`;
8018/// the outer wraps them in `[…]` with `", "` separators. Byte-identical to the VM's nested display.
8019fn lower_show_seqseq(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, src: u16) -> R<()> {
8020    let print_idx = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("Show seq-of-seq: print_text not imported"))?;
8021    let m = src as u32;
8022    // acc = accumulator Text; part = each inner render; i = outer loop counter; inner = the inner
8023    // `Seq of Int` handle (all outside the +5/+6/+7 scratch `emit_stringify`/concat clobber).
8024    let (acc, part, i, inner) = (num_regs + 8, num_regs + 9, num_regs + 10, num_regs + 11);
8025    // acc = "["
8026    lower_text_literal(code, ctx, num_regs, b"[");
8027    local_set(code, acc);
8028    i32_const(code, 0);
8029    local_set(code, i);
8030    code.push(0x02);
8031    code.push(0x40); // block
8032    code.push(0x03);
8033    code.push(0x40); // loop
8034    // if i >= outer_len: br block
8035    local_get(code, i);
8036    local_get(code, m);
8037    i32_load(code, 0); // outer len
8038    code.push(0x4E); // i32.ge_s
8039    code.push(0x0D);
8040    leb_u32(code, 1); // br_if block
8041    // separator: entries after the first get ", "
8042    local_get(code, i);
8043    code.push(0x45); // i32.eqz
8044    code.push(0x04);
8045    code.push(0x40); // if (i == 0): nothing
8046    code.push(0x05); // else
8047    lower_text_literal(code, ctx, num_regs, b", ");
8048    local_set(code, part);
8049    emit_text_concat(code, ctx, num_regs, acc, part, acc);
8050    code.push(0x0B); // end if
8051    // inner = outer element i (i32 handle at data_ptr + i*8)
8052    local_get(code, m);
8053    i32_load(code, 8); // data_ptr
8054    local_get(code, i);
8055    i32_const(code, 8);
8056    code.push(0x6C); // i32.mul
8057    code.push(0x6A); // i32.add
8058    i32_load(code, 0); // the inner Seq handle
8059    local_set(code, inner);
8060    // part = stringify(inner as Seq of Int); acc += part
8061    emit_stringify(code, ctx, num_regs, inner, Some(Kind::SeqInt), part)?;
8062    emit_text_concat(code, ctx, num_regs, acc, part, acc);
8063    // i += 1; br loop
8064    local_get(code, i);
8065    i32_const(code, 1);
8066    code.push(0x6A);
8067    local_set(code, i);
8068    code.push(0x0C);
8069    leb_u32(code, 0); // br loop
8070    code.push(0x0B); // end loop
8071    code.push(0x0B); // end block
8072    // acc += "]"; print_text(acc)
8073    lower_text_literal(code, ctx, num_regs, b"]");
8074    local_set(code, part);
8075    emit_text_concat(code, ctx, num_regs, acc, part, acc);
8076    local_get(code, acc);
8077    code.push(0x10); // call print_text
8078    leb_u32(code, print_idx);
8079    Ok(())
8080}
8081
8082/// `Show <map>` — `RuntimeValue::Map::to_display_string` = `{k0: v0, k1: v1, …}` over the map's
8083/// entries in STORED order, which is INSERTION order (the VM's `MapStorage` is an `IndexMap`, the same
8084/// order the AOT's linear map appends in), so the rendering is byte-identical. Unlike the tuple/enum
8085/// Show this is a RUNTIME loop (the entry count is dynamic): iterate `i` in `0..num_entries`, and for
8086/// each entry `[key@0][value@8]` (16 bytes) stringify the key and value by their kinds and concat
8087/// `k: v` onto the accumulator (with `", "` between entries). Key/value kinds come from the last
8088/// `SetIndex`'s registers (`map_set_key`/`map_set_value`) — a LOCALLY-BUILT map with Int/Text keys.
8089fn lower_show_map(code: &mut Vec<u8>, plan: &Plan, kinds: &KindTable, ctx: &Ctx, src: u16) -> R<()> {
8090    let key_reg = plan
8091        .structs
8092        .map_set_key
8093        .get(&src)
8094        .copied()
8095        .ok_or(WasmLowerError::Unsupported("Show of a map whose key kind is not statically known"))?;
8096    let val_reg = plan
8097        .structs
8098        .map_set_value
8099        .get(&src)
8100        .copied()
8101        .ok_or(WasmLowerError::Unsupported("Show of a map whose value kind is not statically known"))?;
8102    let key_kind = kinds.get(key_reg as usize);
8103    let val_kind = kinds.get(val_reg as usize);
8104    let key_text = match key_kind {
8105        Some(Kind::Text) => true,
8106        Some(Kind::Int) => false,
8107        _ => return Err(WasmLowerError::Unsupported("Show of a map with a non-Int/Text key")),
8108    };
8109    let val_load = map_value_load(val_kind)?; // the value's slot load at its kind's width
8110    let print_idx = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("Show map: print_text not imported"))?;
8111    let num_regs = plan.num_regs;
8112    let m = src as u32;
8113    // acc = accumulator Text; part = each stringified piece; i = the entry loop counter. Key/value
8114    // temps borrow width-matched scratch (idle during a Show): the key is fully stringified before the
8115    // value is loaded, so they may share a slot.
8116    let (acc, part, i) = (num_regs + 8, num_regs + 9, num_regs + 10);
8117    let key_tmp = if key_text { num_regs + 11 } else { num_regs + 1 };
8118    let val_tmp = match val_kind.map(Kind::wasm_valtype) {
8119        Some(F64) => num_regs + 12,
8120        Some(I64) => num_regs + 1,
8121        _ => num_regs + 11,
8122    };
8123    // acc = "{"
8124    lower_text_literal(code, ctx, num_regs, b"{");
8125    local_set(code, acc);
8126    // i = 0
8127    i32_const(code, 0);
8128    local_set(code, i);
8129    code.push(0x02);
8130    code.push(0x40); // block
8131    code.push(0x03);
8132    code.push(0x40); // loop
8133    // if i >= num_entries: br block
8134    local_get(code, i);
8135    local_get(code, m);
8136    i32_load(code, 0); // num_entries
8137    code.push(0x4E); // i32.ge_s
8138    code.push(0x0D);
8139    leb_u32(code, 1); // br_if block
8140    // separator: entries after the first are prefixed with ", "
8141    local_get(code, i);
8142    code.push(0x45); // i32.eqz → i == 0 ?
8143    code.push(0x04);
8144    code.push(0x40); // if (i == 0): nothing
8145    code.push(0x05); // else (i != 0)
8146    lower_text_literal(code, ctx, num_regs, b", ");
8147    local_set(code, part);
8148    emit_text_concat(code, ctx, num_regs, acc, part, acc);
8149    code.push(0x0B); // end if
8150    // key = entry[i].key (offset 0), stringified, appended
8151    emit_map_entry_addr(code, m, i);
8152    if key_text {
8153        i32_load(code, 0);
8154    } else {
8155        i64_load(code, 0);
8156    }
8157    local_set(code, key_tmp);
8158    emit_stringify(code, ctx, num_regs, key_tmp, key_kind, part)?;
8159    emit_text_concat(code, ctx, num_regs, acc, part, acc);
8160    // ": "
8161    lower_text_literal(code, ctx, num_regs, b": ");
8162    local_set(code, part);
8163    emit_text_concat(code, ctx, num_regs, acc, part, acc);
8164    // value = entry[i].value (offset 8), stringified, appended
8165    emit_map_entry_addr(code, m, i);
8166    val_load(code, 8);
8167    local_set(code, val_tmp);
8168    emit_stringify(code, ctx, num_regs, val_tmp, val_kind, part)?;
8169    emit_text_concat(code, ctx, num_regs, acc, part, acc);
8170    // i += 1
8171    local_get(code, i);
8172    i32_const(code, 1);
8173    code.push(0x6A);
8174    local_set(code, i);
8175    code.push(0x0C);
8176    leb_u32(code, 0); // br loop
8177    code.push(0x0B); // end loop
8178    code.push(0x0B); // end block
8179    // acc += "}"; print_text(acc)
8180    lower_text_literal(code, ctx, num_regs, b"}");
8181    local_set(code, part);
8182    emit_text_concat(code, ctx, num_regs, acc, part, acc);
8183    local_get(code, acc);
8184    code.push(0x10); // call print_text
8185    leb_u32(code, print_idx);
8186    Ok(())
8187}
8188
8189/// `Show tuple` — the tree-walker displays a heterogeneous tuple as `(e0, e1, …)`, each element by
8190/// its own scalar display. The tuple layout (element registers → kinds, via `structs.tuple_layouts`)
8191/// is known at compile time, so this UNROLLS: build the `Text` `"("`, then for each element load its
8192/// 8-byte slot, stringify it, and concat it onto the accumulator (with a `", "` separator between
8193/// elements), close with `")"`, and `print_text` the assembled string — byte-identical to
8194/// `RuntimeValue::Tuple::to_display_string`. Deterministic (tuple element order is fixed), unlike a
8195/// struct/map whose display order the tree-walker randomizes (hence those stay deferred). The
8196/// accumulator/separator live in the `+8`/`+9` handle scratch (which `emit_text_concat` preserves);
8197/// the element value is loaded into the `+1` (i64), `+12` (f64), or `+10` (i32-handle) temp by width.
8198fn lower_show_tuple(code: &mut Vec<u8>, plan: &Plan, ctx: &Ctx, src: u16) -> R<()> {
8199    let elems = plan
8200        .structs
8201        .tuple_layouts
8202        .get(&src)
8203        .ok_or(WasmLowerError::Unsupported("Show of a tuple with no static layout"))?;
8204    let num_regs = plan.num_regs;
8205    let (acc, part) = (num_regs + 8, num_regs + 9);
8206    // acc = "("
8207    lower_text_literal(code, ctx, num_regs, b"(");
8208    local_set(code, acc);
8209    let n = elems.len();
8210    for (i, &elem_reg) in elems.iter().enumerate() {
8211        let ek = plan.kinds.get(elem_reg as usize);
8212        // Load slot i (the element value at its width) into a matching-typed temp local.
8213        let elem_tmp = match ek.map(Kind::wasm_valtype) {
8214            Some(F64) => num_regs + 12, // f64 temp
8215            Some(I64) => num_regs + 1,  // i64 temp (borrows a pow scratch — idle during Show)
8216            _ => num_regs + 10,         // i32 handle temp (Text/…)
8217        };
8218        local_get(code, src as u32);
8219        i32_load(code, 8); // data_ptr
8220        emit_slot_load(code, ek, (i as u32) * 8)?;
8221        local_set(code, elem_tmp);
8222        // part = stringify(element); acc = acc + part
8223        emit_stringify(code, ctx, num_regs, elem_tmp, ek, part)?;
8224        emit_text_concat(code, ctx, num_regs, acc, part, acc);
8225        // ", " between elements
8226        if i + 1 < n {
8227            lower_text_literal(code, ctx, num_regs, b", ");
8228            local_set(code, part);
8229            emit_text_concat(code, ctx, num_regs, acc, part, acc);
8230        }
8231    }
8232    // acc = acc + ")"
8233    lower_text_literal(code, ctx, num_regs, b")");
8234    local_set(code, part);
8235    emit_text_concat(code, ctx, num_regs, acc, part, acc);
8236    // print_text(acc)
8237    let idx = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("Show tuple: print_text not imported"))?;
8238    local_get(code, acc);
8239    code.push(0x10); // call print_text
8240    leb_u32(code, idx);
8241    Ok(())
8242}
8243
8244/// `Show <enum>` — a nullary variant displays as just its constructor name (`RuntimeValue::Inductive`
8245/// with empty args → `ind.constructor.clone()`). The enum handle's first word is the TAG (the
8246/// constructor name's constant index; `NewInductive`'s `ctor = add_const(Text(name))`), so this emits
8247/// a tag→name dispatch: for each variant of `src`'s enum type (resolved via `ind_type_of` →
8248/// `enum_types`), `if stored_tag == const_idx(name) { print_text name }`. Exactly one branch matches
8249/// (the live variant), so exactly one name prints — byte-identical to the tree-walker. Restricted to
8250/// ALL-NULLARY enum types; a payload variant (`Ctor(args)`) display is a later increment (soundly
8251/// refused, so such a Show stays deferred rather than miscompiling).
8252/// Build the display `Text` of the enum value in `handle` (its type `def`) into `out` — a nullary
8253/// variant renders as its constructor name, a payload variant as `Ctor(f0, f1, …)`
8254/// (`format!("{}({})", ctor, join(", "))`), matching `RuntimeValue::Inductive::to_display_string`. A
8255/// tag→name dispatch (`stored_tag == const_idx(name)`) selects the live variant; exactly one matches,
8256/// so `out` is always written. `part`/`field_i32` are scratch the CALLER must keep distinct from `out`
8257/// and (for the sequence case) from its outer accumulator/counter/handle. The reusable core of
8258/// [`lower_show_enum`] (a scalar `Show`) and [`lower_show_seqenum`] (per element of a `Seq of Enum`).
8259fn emit_enum_display(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, def: &EnumTypeDef, handle: u32, out: u32, part: u32, field_i32: u32) -> R<()> {
8260    for v in &def.variants {
8261        // The variant's tag = the constant-pool index of its name `Text` (constant dedup makes this the
8262        // exact value `NewInductive` stored). A variant NEVER constructed anywhere is absent from the
8263        // pool — it can't be the runtime value, so it needs no branch.
8264        let Some(tag) = ctx.constants.iter().position(|c| matches!(c, Constant::Text(n) if *n == v.name)) else {
8265            continue;
8266        };
8267        let tag = tag as i32;
8268        local_get(code, handle);
8269        i32_load(code, 0); // the enum handle's stored tag
8270        i32_const(code, tag);
8271        code.push(0x46); // i32.eq → stored_tag == variant_tag
8272        code.push(0x04);
8273        code.push(0x40); // if (void)
8274        // out = the constructor name (the whole display for a nullary variant)
8275        lower_text_literal(code, ctx, num_regs, v.name.as_bytes());
8276        local_set(code, out);
8277        if !v.field_types.is_empty() {
8278            // Append `(f0, f1, …)` — payload slots are stored INLINE after the tag at offset `8*(1+i)`.
8279            lower_text_literal(code, ctx, num_regs, b"(");
8280            local_set(code, part);
8281            emit_text_concat(code, ctx, num_regs, out, part, out);
8282            let n = v.field_types.len();
8283            for (i, ft) in v.field_types.iter().enumerate() {
8284                let ek = kind::boundary_to_kind(ft);
8285                let elem_tmp = match ek.map(Kind::wasm_valtype) {
8286                    Some(F64) => num_regs + 12,
8287                    Some(I64) => num_regs + 1,
8288                    _ => field_i32,
8289                };
8290                local_get(code, handle);
8291                emit_slot_load(code, ek, 8 * (1 + i as u32))?;
8292                local_set(code, elem_tmp);
8293                emit_stringify(code, ctx, num_regs, elem_tmp, ek, part)?;
8294                emit_text_concat(code, ctx, num_regs, out, part, out);
8295                if i + 1 < n {
8296                    lower_text_literal(code, ctx, num_regs, b", ");
8297                    local_set(code, part);
8298                    emit_text_concat(code, ctx, num_regs, out, part, out);
8299                }
8300            }
8301            lower_text_literal(code, ctx, num_regs, b")");
8302            local_set(code, part);
8303            emit_text_concat(code, ctx, num_regs, out, part, out);
8304        }
8305        code.push(0x0B); // end if
8306    }
8307    Ok(())
8308}
8309
8310fn lower_show_enum(code: &mut Vec<u8>, plan: &Plan, ctx: &Ctx, src: u16) -> R<()> {
8311    let type_name = plan
8312        .structs
8313        .ind_type_of
8314        .get(&src)
8315        .ok_or(WasmLowerError::Unsupported("Show of an enum whose type is not statically known"))?;
8316    let def = ctx
8317        .enum_types
8318        .iter()
8319        .find(|e| &e.name == type_name)
8320        .ok_or(WasmLowerError::Unsupported("Show of an unknown enum type"))?;
8321    let print_idx = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("Show enum: print_text not imported"))?;
8322    let num_regs = plan.num_regs;
8323    // out = the assembled display; +9/+10 are the piece + field-i32 scratch (the classic `Show` pool).
8324    let out = num_regs + 8;
8325    emit_enum_display(code, ctx, num_regs, def, src as u32, out, num_regs + 9, num_regs + 10)?;
8326    local_get(code, out);
8327    code.push(0x10); // call print_text
8328    leb_u32(code, print_idx);
8329    Ok(())
8330}
8331
8332/// `Show <Seq of Enum>` — `[e0, e1, …]` over the sequence in insertion order, each element rendered by
8333/// [`emit_enum_display`] (nullary name or `Ctor(fields)`). A RUNTIME loop: element `i` is an i32 enum
8334/// handle at `data_ptr+i*8`; its display is built into `out` and concatenated onto the outer `[…]`
8335/// accumulator. The element enum type comes from `seq_elem_ind_type` (the homogeneous list's element).
8336fn lower_show_seqenum(code: &mut Vec<u8>, plan: &Plan, ctx: &Ctx, src: u16) -> R<()> {
8337    let type_name = plan
8338        .structs
8339        .seq_elem_ind_type
8340        .get(&src)
8341        .ok_or(WasmLowerError::Unsupported("Show of a Seq of Enum whose element type is not statically known"))?;
8342    let def = ctx
8343        .enum_types
8344        .iter()
8345        .find(|e| &e.name == type_name)
8346        .ok_or(WasmLowerError::Unsupported("Show seq-enum: unknown element enum type"))?;
8347    let print_idx = (ctx.host_index)(HostFn::PrintText).ok_or(WasmLowerError::Unsupported("Show seq-enum: print_text not imported"))?;
8348    let num_regs = plan.num_regs;
8349    let m = src as u32;
8350    // Outer loop: `outer_acc` = `[…]`, `i` = counter, `elem` = the current enum handle. The per-element
8351    // display goes to `out`, using `part`/`field_i32` (+13/+14) kept distinct from all of the above.
8352    let (outer_acc, out, i, elem) = (num_regs + 8, num_regs + 9, num_regs + 10, num_regs + 11);
8353    let (part, field_i32) = (num_regs + 13, num_regs + 14);
8354    lower_text_literal(code, ctx, num_regs, b"[");
8355    local_set(code, outer_acc);
8356    i32_const(code, 0);
8357    local_set(code, i);
8358    code.push(0x02);
8359    code.push(0x40); // block
8360    code.push(0x03);
8361    code.push(0x40); // loop
8362    // if i >= len: br block
8363    local_get(code, i);
8364    local_get(code, m);
8365    i32_load(code, 0);
8366    code.push(0x4E); // i32.ge_s
8367    code.push(0x0D);
8368    leb_u32(code, 1);
8369    // separator ", " before every element after the first
8370    local_get(code, i);
8371    code.push(0x45); // i32.eqz
8372    code.push(0x04);
8373    code.push(0x40);
8374    code.push(0x05); // if (i==0) {} else
8375    lower_text_literal(code, ctx, num_regs, b", ");
8376    local_set(code, part);
8377    emit_text_concat(code, ctx, num_regs, outer_acc, part, outer_acc);
8378    code.push(0x0B); // end if
8379    // elem = seq element i (i32 enum handle at data_ptr + i*8)
8380    local_get(code, m);
8381    i32_load(code, 8);
8382    local_get(code, i);
8383    i32_const(code, 8);
8384    code.push(0x6C); // i32.mul
8385    code.push(0x6A); // i32.add
8386    i32_load(code, 0);
8387    local_set(code, elem);
8388    // out = display(elem); outer_acc += out
8389    emit_enum_display(code, ctx, num_regs, def, elem, out, part, field_i32)?;
8390    emit_text_concat(code, ctx, num_regs, outer_acc, out, outer_acc);
8391    // i += 1; br loop
8392    local_get(code, i);
8393    i32_const(code, 1);
8394    code.push(0x6A);
8395    local_set(code, i);
8396    code.push(0x0C);
8397    leb_u32(code, 0);
8398    code.push(0x0B); // end loop
8399    code.push(0x0B); // end block
8400    lower_text_literal(code, ctx, num_regs, b"]");
8401    local_set(code, part);
8402    emit_text_concat(code, ctx, num_regs, outer_acc, part, outer_acc);
8403    local_get(code, outer_acc);
8404    code.push(0x10); // call print_text
8405    leb_u32(code, print_idx);
8406    Ok(())
8407}
8408
8409/// The (slot index, boundary type) of `field_name` in struct type `type_name`, from the DECLARED
8410/// field order (= the AOT's 8-byte-slot storage order). `None` if the type or field is unknown.
8411fn policy_field_slot<'a>(ctx: &'a Ctx, type_name: &str, field_name: &str) -> Option<(u16, &'a BoundaryType)> {
8412    let def = ctx.struct_types.iter().find(|s| s.name == type_name)?;
8413    def.fields.iter().position(|(n, _)| n == field_name).map(|i| (i as u16, &def.fields[i].1))
8414}
8415
8416/// Load struct `reg`'s Text field (an `i32` handle in the low word of slot `slot`) into `dst_local`.
8417fn emit_load_text_field(code: &mut Vec<u8>, reg: u16, slot: u16, dst_local: u32) {
8418    local_get(code, reg as u32);
8419    i32_load(code, 8); // data_ptr
8420    i32_load(code, u32::from(slot) * 8); // the Text handle at slot*8
8421    local_set(code, dst_local);
8422}
8423
8424/// Compile a policy `condition` against `subject` (and optional `object`, `u16::MAX` = none) into an
8425/// i32 (1 = holds, 0 = fails) left on the wasm stack — mirroring `evaluate_policy_condition`. Only the
8426/// Text-field / predicate / and-or / cross-field forms are lowered (they cover the shipping policies);
8427/// a numeric / boolean field compare is soundly refused (the `CheckPolicy` then stays deferred).
8428fn emit_policy_condition(code: &mut Vec<u8>, plan: &Plan, ctx: &Ctx, cond: &PolicyCondition, subject: u16, object: u16) -> R<()> {
8429    let (f8, f11) = (plan.num_regs + 8, plan.num_regs + 11);
8430    match cond {
8431        PolicyCondition::FieldEquals { field, value, is_string_literal } => {
8432            if !is_string_literal {
8433                return Err(WasmLowerError::Unsupported("policy: non-Text field comparison"));
8434            }
8435            let subj_type = plan.structs.struct_name_of.get(&subject).ok_or(WasmLowerError::Unsupported("policy: subject has no struct type"))?;
8436            let field_name = ctx.interner.resolve(*field);
8437            let (slot, bt) = policy_field_slot(ctx, subj_type, field_name).ok_or(WasmLowerError::Unsupported("policy: field not in subject type"))?;
8438            if !matches!(bt, BoundaryType::Text) {
8439                return Err(WasmLowerError::Unsupported("policy: field is not Text"));
8440            }
8441            let value_str = ctx.interner.resolve(*value).to_string();
8442            emit_load_text_field(code, subject, slot, f8);
8443            lower_text_literal(code, ctx, plan.num_regs, value_str.as_bytes());
8444            local_set(code, f11);
8445            emit_text_handles_eq(code, plan.num_regs, f8, f11);
8446        }
8447        PolicyCondition::Predicate { predicate, .. } => {
8448            let subj_type = plan.structs.struct_name_of.get(&subject).ok_or(WasmLowerError::Unsupported("policy: subject has no struct type"))?;
8449            let subj_sym = ctx.interner.lookup(subj_type).ok_or(WasmLowerError::Unsupported("policy: subject type not interned"))?;
8450            let preds = ctx.policies.get_predicates(subj_sym).ok_or(WasmLowerError::Unsupported("policy: no predicates for subject type"))?;
8451            let pred = preds.iter().find(|p| p.predicate_name == *predicate).ok_or(WasmLowerError::Unsupported("policy: referenced predicate not found"))?;
8452            emit_policy_condition(code, plan, ctx, &pred.condition, subject, object)?;
8453        }
8454        PolicyCondition::SubjectFieldEqualsObjectField { subject_field, object_field, .. }
8455        | PolicyCondition::ObjectFieldEquals { subject: subject_field, field: object_field, .. } => {
8456            if object == u16::MAX {
8457                return Err(WasmLowerError::Unsupported("policy: cross-field compare needs an object"));
8458            }
8459            let subj_type = plan.structs.struct_name_of.get(&subject).ok_or(WasmLowerError::Unsupported("policy: subject has no struct type"))?;
8460            let obj_type = plan.structs.struct_name_of.get(&object).ok_or(WasmLowerError::Unsupported("policy: object has no struct type"))?;
8461            let sf = ctx.interner.resolve(*subject_field);
8462            let of = ctx.interner.resolve(*object_field);
8463            let (s_slot, s_bt) = policy_field_slot(ctx, subj_type, sf).ok_or(WasmLowerError::Unsupported("policy: subject field not found"))?;
8464            let (o_slot, o_bt) = policy_field_slot(ctx, obj_type, of).ok_or(WasmLowerError::Unsupported("policy: object field not found"))?;
8465            if !matches!(s_bt, BoundaryType::Text) || !matches!(o_bt, BoundaryType::Text) {
8466                return Err(WasmLowerError::Unsupported("policy: cross-field compare of non-Text fields"));
8467            }
8468            emit_load_text_field(code, subject, s_slot, f8);
8469            emit_load_text_field(code, object, o_slot, f11);
8470            emit_text_handles_eq(code, plan.num_regs, f8, f11);
8471        }
8472        PolicyCondition::Or(l, r) => {
8473            emit_policy_condition(code, plan, ctx, l, subject, object)?;
8474            emit_policy_condition(code, plan, ctx, r, subject, object)?;
8475            code.push(0x72); // i32.or
8476        }
8477        PolicyCondition::And(l, r) => {
8478            emit_policy_condition(code, plan, ctx, l, subject, object)?;
8479            emit_policy_condition(code, plan, ctx, r, subject, object)?;
8480            code.push(0x71); // i32.and
8481        }
8482        PolicyCondition::FieldBool { .. } => {
8483            return Err(WasmLowerError::Unsupported("policy: boolean field condition"));
8484        }
8485    }
8486    Ok(())
8487}
8488
8489/// `Check that <subject> is <predicate>` / `… can <action> <object>` (`CheckPolicy`) — resolve the
8490/// predicate/capability's condition from the `## Policy` registry, compile it inline, and TRAP
8491/// (`unreachable`) when it is false (the standalone module's analog of the VM's `check_policy` error);
8492/// when it holds, execution falls through to the following statements. Mirrors the VM semantics.
8493fn lower_check_policy(code: &mut Vec<u8>, plan: &Plan, ctx: &Ctx, subject: u16, predicate: crate::Symbol, is_capability: bool, object: u16) -> R<()> {
8494    let subj_type = plan.structs.struct_name_of.get(&subject).ok_or(WasmLowerError::Unsupported("CheckPolicy on a non-struct subject"))?;
8495    let subj_sym = ctx.interner.lookup(subj_type).ok_or(WasmLowerError::Unsupported("CheckPolicy subject type not interned"))?;
8496    let cond = if is_capability {
8497        let caps = ctx.policies.get_capabilities(subj_sym).ok_or(WasmLowerError::Unsupported("CheckPolicy: no capabilities for subject type"))?;
8498        caps.iter().find(|c| c.action == predicate).map(|c| &c.condition).ok_or(WasmLowerError::Unsupported("CheckPolicy: capability not found"))?
8499    } else {
8500        let preds = ctx.policies.get_predicates(subj_sym).ok_or(WasmLowerError::Unsupported("CheckPolicy: no predicates for subject type"))?;
8501        preds.iter().find(|p| p.predicate_name == predicate).map(|p| &p.condition).ok_or(WasmLowerError::Unsupported("CheckPolicy: predicate not found"))?
8502    };
8503    emit_policy_condition(code, plan, ctx, cond, subject, object)?;
8504    code.push(0x45); // i32.eqz → condition is FALSE
8505    code.push(0x04);
8506    code.push(0x40); // if (failed)
8507    code.push(0x00); // unreachable — the check failed (the VM errors here)
8508    code.push(0x0B); // end if
8509    Ok(())
8510}
8511
8512/// `Increase/Decrease <obj>'s <field> by <amount>` (`CrdtBump`) on a SINGLE-replica `Shared` struct's
8513/// `ConvergentCount` field. The VM stores such a counter as a plain `Int` struct field and bumps it
8514/// with `crdt_counter_bump` = `field.wrapping_add(±amount)` (a `Nothing` field reads as 0), so with one
8515/// replica this is exactly a struct-field read-modify-write — byte-identical. (Multi-replica MERGE,
8516/// which needs the per-replica CRDT object, stays deferred.) The field's slot is its declared position.
8517fn lower_crdt_bump(code: &mut Vec<u8>, plan: &Plan, ctx: &Ctx, obj: u16, field_const: u32, amount: u16, negate: bool) -> R<()> {
8518    let _ = ctx;
8519    // The counter's slot = its position in the struct's field layout (matched by the field-name const
8520    // index the op carries). `reg_layout` is populated from the actual `NewStruct`/`StructInsert` ops,
8521    // so it covers a `Shared` struct too (whose type may not be in the plain `struct_types`).
8522    let layout = plan.structs.reg_layout.get(&obj).ok_or(WasmLowerError::Unsupported("CrdtBump on a value with no struct layout"))?;
8523    let slot = layout.iter().position(|(fc, _)| *fc == field_const).ok_or(WasmLowerError::Unsupported("CrdtBump: field not in struct layout"))? as u32;
8524    let off = slot * 8;
8525    // obj.field = obj.field ± amount  (store: [addr, value] → i64.store)
8526    local_get(code, obj as u32);
8527    i32_load(code, 8); // data_ptr (store base address)
8528    local_get(code, obj as u32);
8529    i32_load(code, 8);
8530    i64_load(code, off); // current
8531    local_get(code, amount as u32);
8532    code.push(if negate { 0x7D } else { 0x7C }); // i64.sub / i64.add
8533    i64_store(code, off);
8534    Ok(())
8535}
8536
8537/// `Merge <source> into <target>` (`CrdtMerge`) of two same-typed `Shared` structs whose fields are
8538/// `ConvergentCount`/`Tally` counters. The VM merges field-by-field via `crdt_merge_field`, which for
8539/// two plain-`Int` counters is a SUM (`wrapping_add`) — so this adds each of the source's counter
8540/// slots into the target's, byte-identical for the single-replica-per-side case the guide shows. A
8541/// field that is NOT a plain-Int counter (a per-replica GCounter struct, a Set/Map/register CRDT)
8542/// needs the per-replica merge object and is soundly refused (that `Merge` stays deferred).
8543fn lower_crdt_merge(code: &mut Vec<u8>, plan: &Plan, target: u16, source: u16) -> R<()> {
8544    let layout = plan.structs.reg_layout.get(&target).ok_or(WasmLowerError::Unsupported("CrdtMerge on a value with no struct layout"))?;
8545    let fields: Vec<(u32, u16)> = layout.clone();
8546    for (slot, (_fc, value_reg)) in fields.iter().enumerate() {
8547        if plan.kinds.get(*value_reg as usize) != Some(Kind::Int) {
8548            return Err(WasmLowerError::Unsupported("CrdtMerge of a non-Int counter field"));
8549        }
8550        let off = (slot as u32) * 8;
8551        // target[slot] = target[slot] + source[slot]
8552        local_get(code, target as u32);
8553        i32_load(code, 8); // store base addr
8554        local_get(code, target as u32);
8555        i32_load(code, 8);
8556        i64_load(code, off); // target current
8557        local_get(code, source as u32);
8558        i32_load(code, 8);
8559        i64_load(code, off); // source
8560        code.push(0x7C); // i64.add
8561        i64_store(code, off);
8562    }
8563    Ok(())
8564}
8565
8566/// `Resolve <obj>'s <field> to <value>` (`CrdtResolve`) — a single-replica Divergent register just
8567/// takes the new value (the VM overwrites the field: `s.fields.insert(field, v)`). So this stores the
8568/// value handle into the field's slot (resolved from `reg_layout` by the field-name const), matching
8569/// a plain field write — byte-identical for one replica. (A merged multi-value register is deferred.)
8570fn lower_crdt_resolve(code: &mut Vec<u8>, plan: &Plan, kinds: &KindTable, obj: u16, field_const: u32, value: u16) -> R<()> {
8571    let layout = plan.structs.reg_layout.get(&obj).ok_or(WasmLowerError::Unsupported("CrdtResolve on a value with no struct layout"))?;
8572    let slot = layout.iter().position(|(fc, _)| *fc == field_const).ok_or(WasmLowerError::Unsupported("CrdtResolve: field not in struct layout"))? as u32;
8573    let off = slot * 8;
8574    local_get(code, obj as u32);
8575    i32_load(code, 8); // data_ptr (store base)
8576    local_get(code, value as u32);
8577    emit_slot_store(code, kinds.get(value as usize), off)?;
8578    Ok(())
8579}
8580
8581/// `Append <value> to <seq>` (`CrdtAppend`) — a single-replica RGA/sequence is just a growable list,
8582/// so this appends in place (the VM: a `List` → `list_push`). The CRDT collection is intentionally
8583/// MUTABLE-SHARED (the VM keeps it behind an `Rc` and says "appending in place propagates — no
8584/// write-back"), so this must NOT copy-on-write: it drives `lower_list_push` directly (whose in-place
8585/// header update the aliasing field sees). An OR-Set append routes to the byte-dedup set add.
8586fn lower_crdt_append(code: &mut Vec<u8>, plan: &Plan, kinds: &KindTable, ctx: &Ctx, seq: u16, value: u16) -> R<()> {
8587    match kinds.get(seq as usize) {
8588        Some(Kind::SeqText) | Some(Kind::SeqInt) | Some(Kind::SeqBool) | Some(Kind::SeqFloat) | Some(Kind::SeqAny) => {
8589            lower_list_push(code, kinds, ctx, plan.num_regs, seq, value)
8590        }
8591        Some(Kind::SetText) => {
8592            emit_set_add_elem(code, ctx, plan.num_regs, seq as u32, value as u32, true);
8593            Ok(())
8594        }
8595        Some(Kind::Set) => {
8596            emit_set_add_elem(code, ctx, plan.num_regs, seq as u32, value as u32, false);
8597            Ok(())
8598        }
8599        _ => Err(WasmLowerError::Unsupported("CrdtAppend to a non-collection CRDT")),
8600    }
8601}
8602
8603/// `Push src to obj's field` (`ListPushField`) — the direct field-seq push (`Push x to p's items`).
8604/// Resolve the field's slot (`reg_layout`, matched by the field-name const) and its element kind (from
8605/// the register that defined the field's value), load the field's seq handle, and push through the
8606/// shared amortized [`lower_list_push_at`]. The seq's header address is stable across the push, so the
8607/// struct's field slot keeps pointing at it (no write-back). COW `obj` first (value-semantic struct
8608/// mutation); the exercised programs own `obj` uniquely (a nested aliased field-seq is a COW frontier).
8609fn lower_list_push_field(code: &mut Vec<u8>, plan: &Plan, kinds: &KindTable, ctx: &Ctx, obj: u16, field_const: u32, src: u16) -> R<()> {
8610    emit_cow(code, kinds, &plan.structs, ctx, plan.num_regs, obj)?;
8611    let layout = plan.structs.reg_layout.get(&obj).ok_or(WasmLowerError::Unsupported("ListPushField on a value with no struct layout"))?;
8612    let slot = layout
8613        .iter()
8614        .position(|(fc, _)| *fc == field_const)
8615        .ok_or(WasmLowerError::Unsupported("ListPushField: field not in struct layout"))?;
8616    // The element width comes from the PUSHED value's kind (the field seq may have been default-filled
8617    // empty, leaving its declared element kind unrefined) — an Int rides an i64 slot, a Text/handle an
8618    // i32 in the low word, all 8-byte slots.
8619    let elem = kinds.get(src as usize).ok_or(WasmLowerError::Unsupported("ListPushField: unknown pushed-value kind"))?;
8620    let off = (slot as u32) * 8;
8621    let handle = plan.num_regs + 8; // i32 scratch, distinct from lower_list_push_at's +5/+6/+7
8622    // handle = obj.data_ptr[slot] (the field seq's i32 handle)
8623    local_get(code, obj as u32);
8624    i32_load(code, 8);
8625    i32_load(code, off);
8626    local_set(code, handle);
8627    lower_list_push_at(code, elem, ctx, plan.num_regs, handle, src)
8628}
8629
8630/// Emit a function's argument marshaling + `call` (the shared core of `Op::Call`, `Spawn`,
8631/// `SpawnHandle`) — retain clonable heap args (value semantics), pass each at the callee's declared
8632/// parameter valtype (promoting `Int`→`f64` where the signature wants it). Returns whether the callee
8633/// leaves a RESULT on the stack (the caller binds it, or drops it for a fire-and-forget spawn).
8634fn emit_sync_call(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, func: u16, args_start: u16, arg_count: u16) -> R<bool> {
8635    for a in 0..arg_count {
8636        let arg = args_start + a;
8637        if cow_clonable(kinds.get(arg as usize)) {
8638            emit_retain(code, arg);
8639        }
8640    }
8641    let pvts = ctx.fn_param_valtypes.get(func as usize).ok_or(WasmLowerError::Unsupported("call of unknown function"))?;
8642    for a in 0..arg_count {
8643        let arg = args_start + a;
8644        let arg_vt = kinds.valtype(arg as usize);
8645        let param_vt = pvts.get(a as usize).copied().unwrap_or(I64);
8646        if arg_vt == param_vt {
8647            local_get(code, arg as u32);
8648        } else if arg_vt == I64 && param_vt == F64 {
8649            push_as_f64(code, arg, kinds.get(arg as usize))?;
8650        } else {
8651            return Err(WasmLowerError::Unsupported("call argument type does not match the parameter"));
8652        }
8653    }
8654    code.push(0x10); // call
8655    leb_u32(code, ctx.fn_base + func as u32);
8656    Ok(ctx.fn_results.get(func as usize).copied().flatten().is_some())
8657}
8658
8659/// `Receive <dst> from <chan>` (`ChanRecv`) on a single-threaded FIFO channel — pop the FRONT element:
8660/// Non-blocking `Try to receive` (`ChanTryRecv`) → an `Optional`: a non-empty queue pops its front
8661/// element and boxes it (`Some` — a fresh 8-byte heap box holding the inner scalar; handle != 0); an
8662/// empty queue yields `Nothing` (handle `0`). There is no blocking/trap path — the deterministic
8663/// single-task scheduler resumes a try-recv immediately either way (`scheduler::do_try_recv`). The
8664/// present inner kind (for a later `Show`) is carried out-of-band in `opt_inner` from this channel.
8665fn lower_chan_try_recv(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, chan: u16) -> R<()> {
8666    let elem = kinds
8667        .get(chan as usize)
8668        .and_then(Kind::seq_elem)
8669        .ok_or(WasmLowerError::Unsupported("try-receive from a channel of unknown element kind"))?;
8670    let ch = chan as u32;
8671    let idx = num_regs + 5; // i32 pop-front shift-loop scratch
8672    let boxp = num_regs + 6; // i32 Optional box pointer
8673    // A width-matched scratch holds the popped value before it is stored into the box.
8674    let val = match elem {
8675        Kind::Float => num_regs + 12,                                       // f64 scratch
8676        Kind::Int | Kind::Bool | Kind::Char | Kind::Moment | Kind::Duration | Kind::Time | Kind::Span => num_regs + 1, // i64 scratch
8677        _ => num_regs + 7,                                                  // i32-handle scratch
8678    };
8679    // if len == 0 { dst = Nothing (0) } else { pop front into `val`; box it; dst = box handle }
8680    local_get(code, ch);
8681    i32_load(code, 0);
8682    code.push(0x45); // i32.eqz → queue empty?
8683    code.push(0x04);
8684    code.push(0x40); // if (void block type)
8685    i32_const(code, 0);
8686    local_set(code, dst as u32); // Nothing
8687    code.push(0x05); // else
8688    emit_pop_front(code, elem, ch, idx, val)?;
8689    i32_const(code, 8);
8690    emit_alloc(code, ctx,boxp);
8691    local_get(code, boxp);
8692    local_get(code, val);
8693    emit_slot_store(code, Some(elem), 0)?;
8694    local_get(code, boxp);
8695    local_set(code, dst as u32);
8696    code.push(0x0B); // end if
8697    Ok(())
8698}
8699
8700/// load `data_ptr[0]`, shift every later 8-byte slot down one, decrement `len`. A receive on an EMPTY
8701/// channel would BLOCK on the scheduler; a standalone module has none, so it traps (`unreachable`) —
8702/// the non-blocking send-then-receive shape never hits it.
8703fn lower_chan_recv(code: &mut Vec<u8>, kinds: &KindTable, num_regs: u32, dst: u16, chan: u16) -> R<()> {
8704    let elem = kinds.get(chan as usize).and_then(Kind::seq_elem).ok_or(WasmLowerError::Unsupported("receive from a channel of unknown element kind"))?;
8705    let ch = chan as u32;
8706    let idx = num_regs + 5;
8707    // if len == 0 → trap (blocking receive, no scheduler to resume it)
8708    local_get(code, ch);
8709    i32_load(code, 0);
8710    code.push(0x45); // i32.eqz
8711    code.push(0x04);
8712    code.push(0x40);
8713    code.push(0x00); // unreachable
8714    code.push(0x0B);
8715    emit_pop_front(code, elem, ch, idx, dst as u32)
8716}
8717
8718/// Pop the FRONT element of channel/queue `ch` into `dst`: `dst = data[0]`, shift `data[1..len]`
8719/// down one 8-byte slot, then `len -= 1`. Assumes `len > 0` (the caller guards or traps). Shared by
8720/// [`lower_chan_recv`] and the winning recv arm of a `select` ([`lower_select_wait`]).
8721fn emit_pop_front(code: &mut Vec<u8>, elem: Kind, ch: u32, idx: u32, dst: u32) -> R<()> {
8722    let elem_load = seq_elem_load(elem)?;
8723    // dst = data_ptr[0]
8724    local_get(code, ch);
8725    i32_load(code, 8);
8726    elem_load(code, 0);
8727    local_set(code, dst);
8728    // for i in 0..len-1: data[i] = data[i+1] (8-byte slot copy)
8729    i32_const(code, 0);
8730    local_set(code, idx);
8731    code.push(0x02);
8732    code.push(0x40);
8733    code.push(0x03);
8734    code.push(0x40);
8735    local_get(code, idx);
8736    local_get(code, ch);
8737    i32_load(code, 0);
8738    i32_const(code, 1);
8739    code.push(0x6B);
8740    code.push(0x4E); // i32.ge_s → idx >= len-1
8741    code.push(0x0D);
8742    leb_u32(code, 1);
8743    local_get(code, ch);
8744    i32_load(code, 8);
8745    local_get(code, idx);
8746    i32_const(code, 8);
8747    code.push(0x6C);
8748    code.push(0x6A);
8749    local_get(code, ch);
8750    i32_load(code, 8);
8751    local_get(code, idx);
8752    i32_const(code, 1);
8753    code.push(0x6A);
8754    i32_const(code, 8);
8755    code.push(0x6C);
8756    code.push(0x6A);
8757    i64_load(code, 0);
8758    i64_store(code, 0);
8759    local_get(code, idx);
8760    i32_const(code, 1);
8761    code.push(0x6A);
8762    local_set(code, idx);
8763    code.push(0x0C);
8764    leb_u32(code, 0);
8765    code.push(0x0B);
8766    code.push(0x0B);
8767    // len -= 1
8768    local_get(code, ch);
8769    local_get(code, ch);
8770    i32_load(code, 0);
8771    i32_const(code, 1);
8772    code.push(0x6B);
8773    i32_store(code, 0);
8774    Ok(())
8775}
8776
8777/// Resolve a `select` (`Await the first of …`) deterministically, writing the winning arm's index
8778/// into `dst_arm` (the following compiler-emitted per-arm `Eq`/jump dispatch then runs that branch).
8779///
8780/// The arms were registered by the `SelectArm*` ops preceding this `SelectWait` in the same block
8781/// (each emits no code); we read them back by scanning the block, resetting at any earlier
8782/// `SelectWait` so a second `select` in one block sees only its own arms.
8783///
8784/// The resolution mirrors the seeded cooperative scheduler for the shapes the AOT models (no true
8785/// racing): the FIRST recv arm whose FIFO queue is non-empty wins (pop-front into its bound var);
8786/// if no recv arm is ready, the timeout arm fires. When no recv arm is ready and there is no
8787/// timeout arm the scheduler would block forever — a deterministic deadlock, emitted as a trap.
8788fn lower_select_wait(code: &mut Vec<u8>, plan: &Plan, kinds: &KindTable, blocks: &Blocks, k: usize, pc: usize, dst_arm: u16) -> R<()> {
8789    #[derive(Clone, Copy)]
8790    enum Arm {
8791        Recv { chan: u16, var: u16 },
8792        Timeout,
8793    }
8794    let mut arms: Vec<Arm> = Vec::new();
8795    for j in blocks.start(k)..pc {
8796        match plan.ops[j] {
8797            Op::SelectArmRecv { chan, var } => arms.push(Arm::Recv { chan, var }),
8798            Op::SelectArmTimeout { .. } => arms.push(Arm::Timeout),
8799            Op::SelectWait { .. } => arms.clear(),
8800            _ => {}
8801        }
8802    }
8803    let da = dst_arm as u32;
8804    let timeout_idx = arms.iter().position(|a| matches!(a, Arm::Timeout));
8805
8806    // dst_arm = -1 (no winner yet).
8807    code.push(0x42); // i64.const
8808    leb_i64(code, -1);
8809    local_set(code, da);
8810
8811    // First ready recv arm wins: `if dst_arm == -1 && len(chan) > 0 { var = pop_front(chan); dst_arm = i }`.
8812    for (i, arm) in arms.iter().enumerate() {
8813        if let Arm::Recv { chan, var } = *arm {
8814            let elem = kinds
8815                .get(chan as usize)
8816                .and_then(Kind::seq_elem)
8817                .ok_or(WasmLowerError::Unsupported("select recv arm on a channel of unknown element kind"))?;
8818            local_get(code, da);
8819            code.push(0x42); // i64.const -1
8820            leb_i64(code, -1);
8821            code.push(0x51); // i64.eq → (dst_arm == -1)
8822            local_get(code, chan as u32);
8823            i32_load(code, 0); // len(chan)
8824            i32_const(code, 0);
8825            code.push(0x4A); // i32.gt_s → (len > 0)
8826            code.push(0x71); // i32.and
8827            code.push(0x04);
8828            code.push(0x40); // if (void)
8829            emit_pop_front(code, elem, chan as u32, plan.num_regs + 5, var as u32)?;
8830            code.push(0x42); // i64.const i
8831            leb_i64(code, i as i64);
8832            local_set(code, da);
8833            code.push(0x0B); // end if
8834        }
8835    }
8836
8837    // No recv arm ready → the timeout arm fires (or a deadlock trap if there is none).
8838    local_get(code, da);
8839    code.push(0x42); // i64.const -1
8840    leb_i64(code, -1);
8841    code.push(0x51); // i64.eq
8842    code.push(0x04);
8843    code.push(0x40); // if (void)
8844    match timeout_idx {
8845        Some(ti) => {
8846            code.push(0x42); // i64.const ti
8847            leb_i64(code, ti as i64);
8848            local_set(code, da);
8849        }
8850        None => code.push(0x00), // unreachable — no ready arm, no timeout: deadlock
8851    }
8852    code.push(0x0B); // end if
8853    Ok(())
8854}
8855
8856/// `i64.const v`.
8857fn i64c(code: &mut Vec<u8>, v: i64) {
8858    code.push(0x42);
8859    leb_i64(code, v);
8860}
8861
8862/// Lower `Op::MagicDivU` — `lhs / c` (`mul_back == 0`) or `lhs % c` (`mul_back == c`) via the
8863/// Granlund–Montgomery magic reciprocal, mirroring `vm::compiler::magic_eval` bit-for-bit. `magic`,
8864/// `more`, and `mul_back` are compile-time constants, so the flag paths are chosen HERE (in Rust) and
8865/// only the taken sequence is emitted. The 64×64→128 high word (`(magic * n) >> 64`) is computed from
8866/// 32-bit limbs (wasm has no mul-high). `lhs` is an Oracle-proven non-negative `Int` (i64).
8867fn lower_magic_div(code: &mut Vec<u8>, num_regs: u32, dst: u16, lhs: u16, magic: u64, more: u8, mul_back: i64) {
8868    const SHIFT_MASK: u8 = 0x3F;
8869    const ADD_MARKER: u8 = 0x40;
8870    const SHIFT_PATH: u8 = 0x80;
8871    let shift = (more & SHIFT_MASK) as i64;
8872    let q = num_regs + 1; // i64 scratch (reuses the integer-pow result slot)
8873    let n_lo = num_regs + 2;
8874    let n_hi = num_regs + 3;
8875    let hi = num_regs + 4;
8876    let mask: i64 = 0xFFFF_FFFF;
8877
8878    if more & SHIFT_PATH != 0 {
8879        // q = n >>u shift
8880        local_get(code, lhs as u32);
8881        i64c(code, shift);
8882        code.push(0x88); // i64.shr_u
8883        local_set(code, q);
8884    } else {
8885        // n limbs
8886        local_get(code, lhs as u32);
8887        i64c(code, mask);
8888        code.push(0x83); // i64.and
8889        local_set(code, n_lo);
8890        local_get(code, lhs as u32);
8891        i64c(code, 32);
8892        code.push(0x88); // shr_u
8893        local_set(code, n_hi);
8894        let m_lo = (magic & 0xFFFF_FFFF) as i64;
8895        let m_hi = (magic >> 32) as i64;
8896        // cross = (m_lo*n_lo >>u 32) + (m_hi*n_lo & mask) + (m_lo*n_hi & mask)  → into `q`
8897        i64c(code, m_lo);
8898        local_get(code, n_lo);
8899        code.push(0x7E); // mul
8900        i64c(code, 32);
8901        code.push(0x88); // >>u 32
8902        i64c(code, m_hi);
8903        local_get(code, n_lo);
8904        code.push(0x7E);
8905        i64c(code, mask);
8906        code.push(0x83); // & mask
8907        code.push(0x7C); // add
8908        i64c(code, m_lo);
8909        local_get(code, n_hi);
8910        code.push(0x7E);
8911        i64c(code, mask);
8912        code.push(0x83);
8913        code.push(0x7C); // add
8914        local_set(code, q); // q := cross
8915        // hi = m_hi*n_hi + (m_hi*n_lo >>u 32) + (m_lo*n_hi >>u 32) + (cross >>u 32)
8916        i64c(code, m_hi);
8917        local_get(code, n_hi);
8918        code.push(0x7E); // hi_hi
8919        i64c(code, m_hi);
8920        local_get(code, n_lo);
8921        code.push(0x7E);
8922        i64c(code, 32);
8923        code.push(0x88);
8924        code.push(0x7C); // + (hi_lo>>32)
8925        i64c(code, m_lo);
8926        local_get(code, n_hi);
8927        code.push(0x7E);
8928        i64c(code, 32);
8929        code.push(0x88);
8930        code.push(0x7C); // + (lo_hi>>32)
8931        local_get(code, q);
8932        i64c(code, 32);
8933        code.push(0x88);
8934        code.push(0x7C); // + (cross>>32)
8935        local_set(code, hi);
8936        if more & ADD_MARKER != 0 {
8937            // q = (((n - hi) >>u 1) + hi) >>u shift
8938            local_get(code, lhs as u32);
8939            local_get(code, hi);
8940            code.push(0x7D); // sub
8941            i64c(code, 1);
8942            code.push(0x88); // >>u 1
8943            local_get(code, hi);
8944            code.push(0x7C); // + hi
8945            i64c(code, shift);
8946            code.push(0x88); // >>u shift
8947            local_set(code, q);
8948        } else {
8949            // q = hi >>u shift
8950            local_get(code, hi);
8951            i64c(code, shift);
8952            code.push(0x88);
8953            local_set(code, q);
8954        }
8955    }
8956
8957    if mul_back == 0 {
8958        local_get(code, q);
8959        local_set(code, dst as u32);
8960    } else {
8961        // result = lhs - q * mul_back
8962        local_get(code, lhs as u32);
8963        local_get(code, q);
8964        i64c(code, mul_back);
8965        code.push(0x7E); // mul
8966        code.push(0x7D); // sub
8967        local_set(code, dst as u32);
8968    }
8969}
8970
8971/// Lower `Op::ExactDiv` — `dst = lhs / rhs` as an exact `Rational` (`7 / 2 → 7/2`). Normalizes the
8972/// sign (den > 0), reduces by `gcd(|num|, den)` (Euclidean), allocs a 16-byte `[num][den]` value, and
8973/// leaves its handle in `dst`. A whole quotient reduces to `den == 1` (so `Show` renders just the
8974/// integer, matching the VM's downsize). Traps on a zero divisor (the VM errors "Division by zero").
8975fn lower_exact_div(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, lhs: u16, rhs: u16) {
8976    let (num, den, a, b) = (num_regs + 1, num_regs + 2, num_regs + 3, num_regs + 4);
8977    let handle = num_regs + 5;
8978    // rhs == 0 → trap
8979    local_get(code, rhs as u32);
8980    code.push(0x50); // i64.eqz
8981    code.push(0x04);
8982    code.push(0x40);
8983    code.push(0x00); // unreachable
8984    code.push(0x0B);
8985    // num = lhs; den = rhs
8986    local_get(code, lhs as u32);
8987    local_set(code, num);
8988    local_get(code, rhs as u32);
8989    local_set(code, den);
8990    // if den < 0 { num = -num; den = -den }
8991    local_get(code, den);
8992    i64c(code, 0);
8993    code.push(0x53); // i64.lt_s
8994    code.push(0x04);
8995    code.push(0x40);
8996    i64c(code, 0);
8997    local_get(code, num);
8998    code.push(0x7D); // sub → -num
8999    local_set(code, num);
9000    i64c(code, 0);
9001    local_get(code, den);
9002    code.push(0x7D);
9003    local_set(code, den);
9004    code.push(0x0B); // end if
9005    // a = |num|
9006    local_get(code, num);
9007    local_set(code, a);
9008    local_get(code, a);
9009    i64c(code, 0);
9010    code.push(0x53); // a < 0
9011    code.push(0x04);
9012    code.push(0x40);
9013    i64c(code, 0);
9014    local_get(code, a);
9015    code.push(0x7D);
9016    local_set(code, a);
9017    code.push(0x0B);
9018    // b = den; gcd(a, b): while b != 0 { let r = a % b; a = b; b = r }
9019    local_get(code, den);
9020    local_set(code, b);
9021    code.push(0x02);
9022    code.push(0x40); // block
9023    code.push(0x03);
9024    code.push(0x40); // loop
9025    local_get(code, b);
9026    code.push(0x50); // i64.eqz
9027    code.push(0x0D);
9028    leb_u32(code, 1); // br_if exit (b == 0)
9029    local_get(code, a);
9030    local_get(code, b);
9031    code.push(0x81); // i64.rem_s → a % b (on stack)
9032    local_get(code, b);
9033    local_set(code, a); // a = old b
9034    local_set(code, b); // b = a % b
9035    code.push(0x0C);
9036    leb_u32(code, 0); // br loop
9037    code.push(0x0B); // end loop
9038    code.push(0x0B); // end block
9039    // num /= a (gcd) ; den /= a
9040    local_get(code, num);
9041    local_get(code, a);
9042    code.push(0x7F); // i64.div_s
9043    local_set(code, num);
9044    local_get(code, den);
9045    local_get(code, a);
9046    code.push(0x7F);
9047    local_set(code, den);
9048    // handle = alloc(16); store num@0, den@8
9049    i32_const(code, 16);
9050    emit_alloc(code, ctx,handle);
9051    local_get(code, handle);
9052    local_get(code, num);
9053    i64_store(code, 0);
9054    local_get(code, handle);
9055    local_get(code, den);
9056    i64_store(code, 8);
9057    local_get(code, handle);
9058    local_set(code, dst as u32);
9059}
9060
9061/// Emit `for i in 0..len(src): dest[(base_off + i)] = src_bytes[i]`, a single-byte copy loop (for
9062/// `Text`). `offset_by_a` shifts the destination by `len(a_for_offset)` (to append after the first
9063/// operand of a concat); otherwise the copy is index-aligned.
9064fn emit_byte_copy(code: &mut Vec<u8>, idx: u32, dest_data: u32, a_for_offset: u32, src: u32, offset_by_a: bool) {
9065    i32_const(code, 0);
9066    local_set(code, idx);
9067    code.push(0x02);
9068    code.push(0x40); // block
9069    code.push(0x03);
9070    code.push(0x40); // loop
9071    local_get(code, idx);
9072    local_get(code, src);
9073    i32_load(code, 0); // len(src)
9074    code.push(0x4E); // i32.ge_s
9075    code.push(0x0D);
9076    leb_u32(code, 1); // br_if exit
9077    // dest addr = dest_data + (offset + i)
9078    local_get(code, dest_data);
9079    if offset_by_a {
9080        local_get(code, a_for_offset);
9081        i32_load(code, 0); // len(a)
9082        local_get(code, idx);
9083        code.push(0x6A);
9084    } else {
9085        local_get(code, idx);
9086    }
9087    code.push(0x6A);
9088    // src byte = src_data[i]
9089    local_get(code, src);
9090    i32_load(code, 8); // src data_ptr
9091    local_get(code, idx);
9092    code.push(0x6A);
9093    i32_load8_u(code, 0);
9094    i32_store8(code, 0);
9095    // i++
9096    local_get(code, idx);
9097    i32_const(code, 1);
9098    code.push(0x6A);
9099    local_set(code, idx);
9100    code.push(0x0C);
9101    leb_u32(code, 0); // br loop
9102    code.push(0x0B); // end loop
9103    code.push(0x0B); // end block
9104}
9105
9106/// A string literal → a fresh `Text` object in linear memory, leaving its `i32` handle on the
9107/// stack. Bump-allocates a 16-byte header + an 8-aligned byte buffer, writes the UTF-8 bytes as
9108/// little-endian 8-byte `i64.store` chunks (the trailing chunk zero-padded past `len`, which is
9109/// never read), and fills the header `[len][cap][data_ptr]` (byte counts). Each execution makes a
9110/// fresh object, matching the tree-walker's value semantics (immutable, so the waste is benign).
9111fn lower_text_literal(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, bytes: &[u8]) {
9112    let len = bytes.len();
9113    let cap8 = (len + 7) & !7; // 8-aligned buffer so the last i64.store stays in bounds
9114    let (hdr, data) = (num_regs + 5, num_regs + 6);
9115    i32_const(code, 16);
9116    emit_alloc(code, ctx,hdr);
9117    i32_const(code, cap8 as i32);
9118    emit_alloc(code, ctx,data);
9119    for c in 0..(cap8 / 8) {
9120        let mut v: u64 = 0;
9121        for j in 0..8 {
9122            if let Some(&b) = bytes.get(c * 8 + j) {
9123                v |= (b as u64) << (j * 8);
9124            }
9125        }
9126        local_get(code, data);
9127        code.push(0x42); // i64.const
9128        leb_i64(code, v as i64);
9129        i64_store(code, (c * 8) as u32);
9130    }
9131    local_get(code, hdr);
9132    i32_const(code, len as i32);
9133    i32_store(code, 0); // len (bytes)
9134    local_get(code, hdr);
9135    i32_const(code, len as i32);
9136    i32_store(code, 4); // cap
9137    local_get(code, hdr);
9138    local_get(code, data);
9139    i32_store(code, 8); // data_ptr
9140    local_get(code, hdr); // leave the handle on the stack
9141}
9142
9143/// `chr(code) -> Text` — a single-character `Text` from a Unicode scalar value. The UTF-8 encoding is
9144/// computed INLINE (1–4 bytes selected by the code point's range) and packed little-endian into an
9145/// `i64`, then written into a fresh Text object (`[len][cap][data_ptr]` header + an 8-byte data
9146/// buffer). An invalid scalar (a surrogate `U+D800..=U+DFFF` or `> U+10FFFF`) traps, matching the
9147/// VM's `char::from_u32(..)` returning `None`.
9148fn lower_chr(code: &mut Vec<u8>, ctx: &Ctx, num_regs: u32, dst: u16, arg: u16) {
9149    let c = num_regs + 1; // i64 code point
9150    let packed = num_regs + 2; // i64 UTF-8 bytes, little-endian in the low bytes
9151    let len = num_regs + 7; // i32 byte count
9152    let (hdr, data) = (num_regs + 5, num_regs + 6);
9153    // Continuation byte `0x80 | ((c >> shift) & 0x3F)`, shifted into byte position `pos`, left on stack.
9154    let cont = |code: &mut Vec<u8>, shift: i64, pos: i64| {
9155        local_get(code, c);
9156        i64c(code, shift);
9157        code.push(0x88); // i64.shr_u
9158        i64c(code, 0x3F);
9159        code.push(0x83); // i64.and
9160        i64c(code, 0x80);
9161        code.push(0x84); // i64.or
9162        i64c(code, pos);
9163        code.push(0x86); // i64.shl
9164    };
9165    // Lead byte `mask | (c >> shift)`, left on stack.
9166    let lead = |code: &mut Vec<u8>, shift: i64, mask: i64| {
9167        local_get(code, c);
9168        i64c(code, shift);
9169        code.push(0x88); // i64.shr_u
9170        i64c(code, mask);
9171        code.push(0x84); // i64.or
9172    };
9173    // c = arg
9174    local_get(code, arg as u32);
9175    local_set(code, c);
9176    // Trap on an invalid scalar value: (u64)c > 0x10FFFF  ||  (0xD800 <= c <= 0xDFFF).
9177    local_get(code, c);
9178    i64c(code, 0x10FFFF);
9179    code.push(0x56); // i64.gt_u
9180    local_get(code, c);
9181    i64c(code, 0xD800);
9182    code.push(0x5A); // i64.ge_u
9183    local_get(code, c);
9184    i64c(code, 0xDFFF);
9185    code.push(0x58); // i64.le_u
9186    code.push(0x71); // i32.and → in-surrogate-range
9187    code.push(0x72); // i32.or  → invalid
9188    code.push(0x04);
9189    code.push(0x40); // if (invalid)
9190    code.push(0x00); // unreachable (trap)
9191    code.push(0x0B); // end
9192    // 1 byte: c < 0x80
9193    local_get(code, c);
9194    i64c(code, 0x80);
9195    code.push(0x54); // i64.lt_u
9196    code.push(0x04);
9197    code.push(0x40); // if
9198    local_get(code, c);
9199    local_set(code, packed);
9200    i32_const(code, 1);
9201    local_set(code, len);
9202    code.push(0x05); // else
9203    // 2 bytes: c < 0x800
9204    local_get(code, c);
9205    i64c(code, 0x800);
9206    code.push(0x54); // i64.lt_u
9207    code.push(0x04);
9208    code.push(0x40); // if
9209    lead(code, 6, 0xC0);
9210    cont(code, 0, 8);
9211    code.push(0x84); // i64.or
9212    local_set(code, packed);
9213    i32_const(code, 2);
9214    local_set(code, len);
9215    code.push(0x05); // else
9216    // 3 bytes: c < 0x10000
9217    local_get(code, c);
9218    i64c(code, 0x10000);
9219    code.push(0x54); // i64.lt_u
9220    code.push(0x04);
9221    code.push(0x40); // if
9222    lead(code, 12, 0xE0);
9223    cont(code, 6, 8);
9224    code.push(0x84); // i64.or
9225    cont(code, 0, 16);
9226    code.push(0x84); // i64.or
9227    local_set(code, packed);
9228    i32_const(code, 3);
9229    local_set(code, len);
9230    code.push(0x05); // else — 4 bytes
9231    lead(code, 18, 0xF0);
9232    cont(code, 12, 8);
9233    code.push(0x84); // i64.or
9234    cont(code, 6, 16);
9235    code.push(0x84); // i64.or
9236    cont(code, 0, 24);
9237    code.push(0x84); // i64.or
9238    local_set(code, packed);
9239    i32_const(code, 4);
9240    local_set(code, len);
9241    code.push(0x0B); // end (3-byte if)
9242    code.push(0x0B); // end (2-byte if)
9243    code.push(0x0B); // end (1-byte if)
9244    // Build the Text: 16-byte header + an 8-byte data buffer (max 4 UTF-8 bytes ≤ 8).
9245    i32_const(code, 16);
9246    emit_alloc(code, ctx,hdr);
9247    i32_const(code, 8);
9248    emit_alloc(code, ctx,data);
9249    local_get(code, data);
9250    local_get(code, packed);
9251    i64_store(code, 0); // the packed bytes (low `len` are valid, the rest zero)
9252    local_get(code, hdr);
9253    local_get(code, len);
9254    i32_store(code, 0); // len (bytes)
9255    local_get(code, hdr);
9256    local_get(code, len);
9257    i32_store(code, 4); // cap
9258    local_get(code, hdr);
9259    local_get(code, data);
9260    i32_store(code, 8); // data_ptr
9261    local_get(code, hdr);
9262    local_set(code, dst as u32);
9263}
9264
9265/// `lhs followed by rhs` — concatenate two sequences into a fresh one (the tree-walker's
9266/// `arith::seq_concat`: lhs's elements then rhs's). Both element kinds match (raw 8-byte copy).
9267fn lower_seq_concat(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, lhs: u16, rhs: u16) -> R<()> {
9268    seq_elem_kind(kinds, lhs)?;
9269    seq_elem_kind(kinds, rhs)?;
9270    let (a, b) = (lhs as u32, rhs as u32);
9271    let (hdr, data, idx) = (num_regs + 5, num_regs + 6, num_regs + 7);
9272    // hdr = alloc(16); data = alloc((len_a + len_b) * 8)
9273    i32_const(code, 16);
9274    emit_alloc(code, ctx,hdr);
9275    local_get(code, a);
9276    i32_load(code, 0);
9277    local_get(code, b);
9278    i32_load(code, 0);
9279    code.push(0x6A); // len_a + len_b
9280    i32_const(code, 8);
9281    code.push(0x6C);
9282    emit_alloc(code, ctx,data);
9283    // copy lhs: for i in 0..len_a: data[i*8] = a_data[i*8]
9284    emit_seq_copy(code, idx, data, a, a, false);
9285    // copy rhs: for i in 0..len_b: data[(len_a + i)*8] = b_data[i*8]
9286    emit_seq_copy(code, idx, data, a, b, true);
9287    // header: len = cap = len_a + len_b; data_ptr = data
9288    for off in [0u32, 4] {
9289        local_get(code, hdr);
9290        local_get(code, a);
9291        i32_load(code, 0);
9292        local_get(code, b);
9293        i32_load(code, 0);
9294        code.push(0x6A);
9295        i32_store(code, off);
9296    }
9297    local_get(code, hdr);
9298    local_get(code, data);
9299    i32_store(code, 8);
9300    local_get(code, hdr);
9301    local_set(code, dst as u32);
9302    Ok(())
9303}
9304
9305/// Emit `for i in 0..len(src): dest[(base_off + i)*8] = src_data[i*8]`, a raw 8-byte element copy
9306/// loop. When `offset_by_a` the destination index is shifted by `len(a_for_offset)` (used to append
9307/// the second operand of a concat after the first); otherwise the copy is index-aligned.
9308fn emit_seq_copy(code: &mut Vec<u8>, idx: u32, dest_data: u32, a_for_offset: u32, src: u32, offset_by_a: bool) {
9309    i32_const(code, 0);
9310    local_set(code, idx);
9311    code.push(0x02);
9312    code.push(0x40); // block
9313    code.push(0x03);
9314    code.push(0x40); // loop
9315    local_get(code, idx);
9316    local_get(code, src);
9317    i32_load(code, 0); // len(src)
9318    code.push(0x4E); // i32.ge_s
9319    code.push(0x0D);
9320    leb_u32(code, 1); // br_if exit
9321    // dest addr = dest_data + (offset + i)*8
9322    local_get(code, dest_data);
9323    if offset_by_a {
9324        local_get(code, a_for_offset);
9325        i32_load(code, 0); // len(a)
9326        local_get(code, idx);
9327        code.push(0x6A); // len(a) + i
9328    } else {
9329        local_get(code, idx);
9330    }
9331    i32_const(code, 8);
9332    code.push(0x6C);
9333    code.push(0x6A);
9334    // src addr = src_data + i*8
9335    local_get(code, src);
9336    i32_load(code, 8);
9337    local_get(code, idx);
9338    i32_const(code, 8);
9339    code.push(0x6C);
9340    code.push(0x6A);
9341    i64_load(code, 0);
9342    i64_store(code, 0);
9343    // i++
9344    local_get(code, idx);
9345    i32_const(code, 1);
9346    code.push(0x6A);
9347    local_set(code, idx);
9348    code.push(0x0C);
9349    leb_u32(code, 0); // br loop
9350    code.push(0x0B); // end loop
9351    code.push(0x0B); // end block
9352}
9353
9354/// `items start through end of seq` (1-based inclusive subsequence) — allocate a fresh sequence
9355/// and copy the in-range elements. Matches the tree-walker's `collections::slice` byte-for-byte,
9356/// including its out-of-range → empty rule: with `start0 = (start as usize).saturating_sub(1)` and
9357/// `end_excl = end as usize`, the result is non-empty iff `start0 < end_excl <= len` (the `usize`
9358/// casts are reproduced with unsigned i64 compares, so negative indices wrap huge → empty).
9359fn lower_slice(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, dst: u16, collection: u16, start: u16, end: u16) -> R<()> {
9360    seq_elem_kind(kinds, collection)?; // require a scalar sequence; the copy is raw 8-byte either way
9361    let col = collection as u32;
9362    let (s0, ee) = (num_regs + 1, num_regs + 2); // i64 scratch: start0, end_excl
9363    let (hdr, data, idx) = (num_regs + 5, num_regs + 6, num_regs + 7); // i32 scratch
9364    // hdr = alloc(16)
9365    i32_const(code, 16);
9366    emit_alloc(code, ctx,hdr);
9367    // start0 = (start == 0) ? 0 : (start - 1)   [= saturating_sub((start as u64), 1)]
9368    code.push(0x42);
9369    leb_i64(code, 0); // 0
9370    local_get(code, start as u32);
9371    code.push(0x42);
9372    leb_i64(code, 1);
9373    code.push(0x7D); // start - 1
9374    local_get(code, start as u32);
9375    code.push(0x50); // i64.eqz(start)
9376    code.push(0x1B); // select → eqz ? 0 : start-1
9377    local_set(code, s0);
9378    // end_excl = end
9379    local_get(code, end as u32);
9380    local_set(code, ee);
9381    // nonempty = (start0 <u end_excl) && (end_excl <=u len)
9382    local_get(code, s0);
9383    local_get(code, ee);
9384    code.push(0x54); // i64.lt_u
9385    local_get(code, ee);
9386    local_get(code, col);
9387    i32_load(code, 0); // len
9388    code.push(0xAD); // i64.extend_i32_u
9389    code.push(0x58); // i64.le_u
9390    code.push(0x71); // i32.and
9391    code.push(0x04);
9392    code.push(0x40); // if (nonempty)
9393    {
9394        // count*8 → data = alloc(count*8)
9395        local_get(code, ee);
9396        local_get(code, s0);
9397        code.push(0x7D);
9398        code.push(0xA7); // count (i32)
9399        i32_const(code, 8);
9400        code.push(0x6C);
9401        emit_alloc(code, ctx,data);
9402        // for i in 0..count: data[i*8] = src_data[(start0+i)*8]  (raw 8-byte copy)
9403        i32_const(code, 0);
9404        local_set(code, idx);
9405        code.push(0x02);
9406        code.push(0x40); // block
9407        code.push(0x03);
9408        code.push(0x40); // loop
9409        local_get(code, idx);
9410        local_get(code, ee);
9411        local_get(code, s0);
9412        code.push(0x7D);
9413        code.push(0xA7); // count
9414        code.push(0x4E); // i32.ge_s
9415        code.push(0x0D);
9416        leb_u32(code, 1); // br_if exit
9417        // dst addr = data + idx*8
9418        local_get(code, data);
9419        local_get(code, idx);
9420        i32_const(code, 8);
9421        code.push(0x6C);
9422        code.push(0x6A);
9423        // src addr = src_data_ptr + (start0_i32 + idx)*8
9424        local_get(code, col);
9425        i32_load(code, 8);
9426        local_get(code, s0);
9427        code.push(0xA7); // start0 as i32
9428        local_get(code, idx);
9429        code.push(0x6A); // start0 + idx
9430        i32_const(code, 8);
9431        code.push(0x6C);
9432        code.push(0x6A);
9433        i64_load(code, 0);
9434        i64_store(code, 0);
9435        // idx++
9436        local_get(code, idx);
9437        i32_const(code, 1);
9438        code.push(0x6A);
9439        local_set(code, idx);
9440        code.push(0x0C);
9441        leb_u32(code, 0); // br loop
9442        code.push(0x0B); // end loop
9443        code.push(0x0B); // end block
9444        // header: len = cap = count; data_ptr = data
9445        for off in [0u32, 4] {
9446            local_get(code, hdr);
9447            local_get(code, ee);
9448            local_get(code, s0);
9449            code.push(0x7D);
9450            code.push(0xA7);
9451            i32_store(code, off);
9452        }
9453        local_get(code, hdr);
9454        local_get(code, data);
9455        i32_store(code, 8);
9456    }
9457    code.push(0x05); // else (empty)
9458    for off in [0u32, 4, 8] {
9459        local_get(code, hdr);
9460        i32_const(code, 0);
9461        i32_store(code, off);
9462    }
9463    code.push(0x0B); // end if
9464    local_get(code, hdr);
9465    local_set(code, dst as u32);
9466    Ok(())
9467}
9468
9469/// `IterPrepare`: snapshot the sequence in `iterable` (a raw byte copy of its `len` 8-byte
9470/// elements into a fresh buffer — so a mutation inside the loop cannot perturb iteration, exactly
9471/// as the tree-walker's `iteration_snapshot` materializes a fresh `Vec`) and push a 12-byte frame
9472/// `[snapshot_ptr:i32 @0][cursor:i32 @4][len:i32 @8]` onto the down-growing iterator stack.
9473fn lower_iter_prepare(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, num_regs: u32, iterable: u16) -> R<()> {
9474    if !kinds.get(iterable as usize).map(Kind::is_seq).unwrap_or(false) {
9475        return Err(WasmLowerError::Unsupported("iteration over a non-sequence value"));
9476    }
9477    let it = iterable as u32;
9478    let (snap, idx) = (num_regs + 5, num_regs + 6); // i32 scratch
9479    // snap = alloc(len * 8)
9480    local_get(code, it);
9481    i32_load(code, 0); // len
9482    i32_const(code, 8);
9483    code.push(0x6C); // i32.mul
9484    emit_alloc(code, ctx,snap);
9485    // for i in 0..len: snap[i*8] = data_ptr[i*8]  (raw 8-byte copy; Int and Float are both 8 wide)
9486    i32_const(code, 0);
9487    local_set(code, idx);
9488    code.push(0x02);
9489    code.push(0x40); // block $exit
9490    code.push(0x03);
9491    code.push(0x40); // loop $loop
9492    local_get(code, idx);
9493    local_get(code, it);
9494    i32_load(code, 0); // len
9495    code.push(0x4E); // i32.ge_s → i >= len
9496    code.push(0x0D);
9497    leb_u32(code, 1); // br_if $exit
9498    // dst addr = snap + i*8
9499    local_get(code, snap);
9500    local_get(code, idx);
9501    i32_const(code, 8);
9502    code.push(0x6C);
9503    code.push(0x6A);
9504    // src value = data_ptr[i*8]
9505    local_get(code, it);
9506    i32_load(code, 8); // data_ptr
9507    local_get(code, idx);
9508    i32_const(code, 8);
9509    code.push(0x6C);
9510    code.push(0x6A);
9511    i64_load(code, 0);
9512    i64_store(code, 0);
9513    // i++
9514    local_get(code, idx);
9515    i32_const(code, 1);
9516    code.push(0x6A);
9517    local_set(code, idx);
9518    code.push(0x0C);
9519    leb_u32(code, 0); // br $loop
9520    code.push(0x0B); // end loop
9521    code.push(0x0B); // end block
9522    // push frame: __iter_sp -= 12
9523    global_get(code, ctx.iter_global);
9524    i32_const(code, 12);
9525    code.push(0x6B); // i32.sub
9526    global_set(code, ctx.iter_global);
9527    // frame[0] = snap; frame[4] = 0 (cursor); frame[8] = len
9528    global_get(code, ctx.iter_global);
9529    local_get(code, snap);
9530    i32_store(code, 0);
9531    global_get(code, ctx.iter_global);
9532    i32_const(code, 0);
9533    i32_store(code, 4);
9534    global_get(code, ctx.iter_global);
9535    local_get(code, it);
9536    i32_load(code, 0);
9537    i32_store(code, 8);
9538    Ok(())
9539}
9540
9541/// `IterNext { dst, exit }`: a conditional block terminator. If the top frame's `cursor < len`,
9542/// load `snapshot[cursor]` into `dst` (i64 for Int, f64 for Float), advance the cursor, and fall
9543/// through to the loop body; otherwise branch to `exit` (the matching `IterPop`). Either way it
9544/// sets the dispatch "next block" local and `br`s to `$loop`, like every other branch terminator.
9545#[allow(clippy::too_many_arguments)]
9546fn lower_iter_next(code: &mut Vec<u8>, kinds: &KindTable, ctx: &Ctx, blocks: &Blocks, k: usize, num_regs: u32, dst: u16, exit: usize, pc: usize) {
9547    // The loop variable's kind IS the element kind; load it at its width (`f64`/`i64`, or `i32` for
9548    // a heap handle like Text/Struct/Enum).
9549    let elem_load: fn(&mut Vec<u8>, u32) = match kinds.get(dst as usize).map(Kind::wasm_valtype) {
9550        Some(F64) => f64_load,
9551        Some(I64) => i64_load,
9552        _ => i32_load,
9553    };
9554    let ig = ctx.iter_global;
9555    let pc_local = num_regs;
9556    let fallthrough = blocks.block_of(pc + 1) as u32;
9557    let exit_block = blocks.block_of(exit) as u32;
9558    // cursor < len ?
9559    global_get(code, ig);
9560    i32_load(code, 4); // cursor
9561    global_get(code, ig);
9562    i32_load(code, 8); // len
9563    code.push(0x48); // i32.lt_s
9564    code.push(0x04);
9565    code.push(0x40); // if (void)
9566    // dst = snapshot[cursor]
9567    global_get(code, ig);
9568    i32_load(code, 0); // snapshot_ptr
9569    global_get(code, ig);
9570    i32_load(code, 4); // cursor
9571    i32_const(code, 8);
9572    code.push(0x6C); // i32.mul
9573    code.push(0x6A); // i32.add → element addr
9574    elem_load(code, 0);
9575    local_set(code, dst as u32);
9576    // cursor++  (frame[4] = cursor + 1)
9577    global_get(code, ig);
9578    global_get(code, ig);
9579    i32_load(code, 4);
9580    i32_const(code, 1);
9581    code.push(0x6A); // i32.add
9582    i32_store(code, 4);
9583    // next block = fallthrough (the loop body)
9584    code.push(0x41); // i32.const
9585    leb_u32(code, fallthrough);
9586    local_set(code, pc_local);
9587    code.push(0x05); // else
9588    // exhausted: next block = exit (the IterPop)
9589    code.push(0x41); // i32.const
9590    leb_u32(code, exit_block);
9591    local_set(code, pc_local);
9592    code.push(0x0B); // end if
9593    code.push(0x0C); // br $loop
9594    leb_u32(code, blocks.br_loop(k));
9595}
9596
9597/// Whether an op touches the heap value model's linear memory (so the module needs a memory +
9598/// `__heap_ptr` global). Grows as heap ops are lowered.
9599fn op_uses_heap(op: &Op) -> bool {
9600    matches!(
9601        op,
9602        Op::NewEmptyList { .. }
9603            | Op::NewEmptyListI32 { .. }
9604            | Op::Length { .. }
9605            | Op::ListPush { .. }
9606            | Op::ListPop { .. }
9607            | Op::Index { .. }
9608            | Op::IndexUnchecked { .. }
9609            | Op::SetIndex { .. }
9610            | Op::SetIndexUnchecked { .. }
9611            | Op::ListPushField { .. }
9612            // `ExactDiv` allocates a 16-byte Rational value in linear memory.
9613            | Op::ExactDiv { .. }
9614            | Op::NewRange { .. }
9615            | Op::NewList { .. }
9616            | Op::IterPrepare { .. }
9617            | Op::IterNext { .. }
9618            | Op::IterPop
9619            | Op::Contains { .. }
9620            | Op::SliceOp { .. }
9621            | Op::SeqConcat { .. }
9622            | Op::Concat { .. }
9623            | Op::FormatValue { .. }
9624            | Op::DeepClone { .. }
9625            | Op::NewStruct { .. }
9626            | Op::StructInsert { .. }
9627            | Op::GetField { .. }
9628            | Op::CheckPolicy { .. }
9629            | Op::CrdtBump { .. }
9630            | Op::CrdtMerge { .. }
9631            | Op::NewCrdt { .. }
9632            | Op::CrdtResolve { .. }
9633            | Op::CrdtAppend { .. }
9634            | Op::ChanNew { .. }
9635            | Op::ChanSend { .. }
9636            | Op::ChanRecv { .. }
9637            // `Try to send` appends to (reallocs) the FIFO; `Try to receive` allocs an Optional box.
9638            | Op::ChanTrySend { .. }
9639            | Op::ChanTryRecv { .. }
9640            // A `select` recv arm pops from a channel's FIFO queue in linear memory.
9641            | Op::SelectWait { .. }
9642            // Offline networking models the inbox as a local FIFO in linear memory.
9643            | Op::NetListen { .. }
9644            | Op::NetSend { .. }
9645            | Op::NetStream { .. }
9646            | Op::NetAwait { .. }
9647            | Op::NewEmptyMap { .. }
9648            | Op::NewEmptySet { .. }
9649            | Op::SetAdd { .. }
9650            | Op::RemoveFrom { .. }
9651            | Op::UnionOp { .. }
9652            | Op::IntersectOp { .. }
9653            | Op::NewInductive { .. }
9654            | Op::BindArm { .. }
9655            | Op::NewTuple { .. }
9656            | Op::DestructureTuple { .. }
9657            | Op::MakeClosure { .. }
9658            | Op::CallValue { .. }
9659            // `args()` returns a `Seq of Text` HANDLE the host builds in this module's linear memory,
9660            // so the module must export a memory for the host to write into.
9661            | Op::Args { .. }
9662            // `chr(code)` builds a one-character Text object in linear memory.
9663            | Op::CallBuiltin { builtin: BuiltinId::Chr, .. }
9664            // `repeatSeq(x, n)` bump-allocates a fresh `n`-element sequence.
9665            | Op::CallBuiltin { builtin: BuiltinId::RepeatSeq, .. }
9666            // Byte interop allocates a fresh seq / Text / 16-byte block in linear memory.
9667            | Op::CallBuiltin { builtin: BuiltinId::TextBytes, .. }
9668            | Op::CallBuiltin { builtin: BuiltinId::UuidBytes, .. }
9669            | Op::CallBuiltin { builtin: BuiltinId::TextFromBytes, .. }
9670            | Op::CallBuiltin { builtin: BuiltinId::UuidFromBytes, .. }
9671            | Op::CallBuiltin { builtin: BuiltinId::Lanes4Of, .. }
9672            | Op::CallBuiltin { builtin: BuiltinId::Lanes4Word32Make, .. }
9673            | Op::CallBuiltin { builtin: BuiltinId::SeqOfLanes4W32, .. }
9674            // `readWireProgram` bump-allocates a receive buffer (via `emit_alloc`), so it needs the runtime
9675            // allocator (`logos_rt_alloc`) imported — else `emit_alloc` falls to the `__heap_ptr` global,
9676            // undeclared in a linked module (an invalid global relocation in the emitted object).
9677            | Op::CallBuiltin { builtin: BuiltinId::ReadWireProgram, .. }
9678    )
9679}
9680
9681/// The local-declaration prefix of a Code entry: registers `num_params..num_regs` typed by
9682/// their inferred kind (coalesced into same-type groups), the i32 dispatch local, four i64
9683/// scratch locals (integer `pow`), and one i32 heap-allocation scratch.
9684fn encode_locals(plan: &Plan) -> Vec<u8> {
9685    let mut groups: Vec<(u32, u8)> = Vec::new();
9686    let mut push = |vt: u8, groups: &mut Vec<(u32, u8)>| match groups.last_mut() {
9687        Some((count, t)) if *t == vt => *count += 1,
9688        _ => groups.push((1, vt)),
9689    };
9690    for r in plan.num_params..plan.num_regs {
9691        push(plan.kinds.valtype(r as usize), &mut groups);
9692    }
9693    push(I32, &mut groups); // the dispatch "next block" local at index num_regs
9694    // Four i64 scratch locals (num_regs+1..=num_regs+4) for integer `pow`'s squaring loop
9695    // (result, base, exponent, and a product temp distinct from them all).
9696    push(I64, &mut groups);
9697    push(I64, &mut groups);
9698    push(I64, &mut groups);
9699    push(I64, &mut groups);
9700    // Seven i32 heap scratch locals (num_regs+5..+11): header/alloc temps and a fill index
9701    // (+5/+6/+7), two operand-handle holders for `Concat`'s stringify-then-byte-copy (+8/+9), and a
9702    // Text-keyed-Map per-entry key compare needs +8/+9/+10 — so seven cover the deepest user.
9703    push(I32, &mut groups);
9704    push(I32, &mut groups);
9705    push(I32, &mut groups);
9706    push(I32, &mut groups);
9707    push(I32, &mut groups);
9708    push(I32, &mut groups);
9709    push(I32, &mut groups);
9710    // One f64 scratch local (num_regs+12): a Float tuple element loaded from its 8-byte slot needs an
9711    // `f64` holder before `emit_stringify` (the i64/i32 scratch above cannot type it). `Show`-tuple only.
9712    push(F64, &mut groups);
9713    // Two more i32 scratch (num_regs+13/+14): the whole `Seq of Enum` `Show` NESTS an enum display
9714    // inside the outer sequence loop, so the per-element assembly needs locals distinct from the
9715    // outer accumulator/counter/handle (+8/+10/+11) — a separator/piece temp and a field-i32 temp.
9716    push(I32, &mut groups);
9717    push(I32, &mut groups);
9718
9719    let mut out = Vec::new();
9720    leb_u32(&mut out, groups.len() as u32);
9721    for (count, vt) in groups {
9722        leb_u32(&mut out, count);
9723        out.push(vt);
9724    }
9725    out
9726}
9727
9728/// Encode a UTF-8 name as a wasm name (length-prefixed bytes).
9729fn encode_name(out: &mut Vec<u8>, name: &str) {
9730    leb_u32(out, name.len() as u32);
9731    out.extend_from_slice(name.as_bytes());
9732}
9733
9734/// A short, stable name for an op the scalar backend does not lower — what the corpus lock
9735/// reports as the remaining gap.
9736fn unsupported_op(op: &Op) -> WasmLowerError {
9737    let what = match op {
9738        Op::ExactDiv { .. } => "exact division (Rational)",
9739        Op::DivPow2 { .. } | Op::MagicDivU { .. } => "oracle division op",
9740        Op::Concat { .. } => "text op",
9741        Op::IndexUnchecked { .. } => "unchecked index (IndexUnchecked)",
9742        Op::CallValue { .. } | Op::MakeClosure { .. } => "closure call",
9743        _ => "op",
9744    };
9745    WasmLowerError::Unsupported(what)
9746}