Skip to main content

lex_bytecode/compiler/
mod.rs

1//! M4 compiler: canonical AST → bytecode.
2
3use crate::op::*;
4use crate::program::*;
5use indexmap::IndexMap;
6use lex_ast as a;
7
8mod constpool;
9mod free_vars;
10mod liveness;
11mod lowering;
12mod peephole;
13
14use constpool::ConstPool;
15use free_vars::free_vars;
16use liveness::apply_last_load_takes;
17use lowering::{apply_arena_lowering, apply_escape_lowering};
18use peephole::{
19    apply_peephole, apply_peephole_slice2, apply_peephole_slice3, apply_peephole_slice4,
20    apply_peephole_slice5, apply_peephole_slice6, apply_peephole_slice7, apply_peephole_slice9,
21};
22
23pub fn compile_program(stages: &[a::Stage]) -> Program {
24    let mut p = Program {
25        constants: Vec::new(),
26        functions: Vec::new(),
27        function_names: IndexMap::new(),
28        module_aliases: IndexMap::new(),
29        entry: None,
30        record_shapes: Vec::new(),
31    };
32
33    // Collect imports as alias → module-name. The module name is the part
34    // after `std.` (so `import "std.io" as io` ⇒ alias `io` → module `io`).
35    for s in stages {
36        if let a::Stage::Import(i) = s {
37            let module = i.reference.strip_prefix("std.").unwrap_or(&i.reference).to_string();
38            p.module_aliases.insert(i.alias.clone(), module);
39        }
40    }
41
42    for s in stages {
43        if let a::Stage::FnDecl(fd) = s {
44            let idx = p.functions.len() as u32;
45            p.function_names.insert(fd.name.clone(), idx);
46            p.functions.push(Function {
47                name: fd.name.clone(),
48                arity: fd.params.len() as u16,
49                locals_count: 0,
50                code: Vec::new(),
51                effects: fd.effects.iter().map(|e| DeclaredEffect {
52                    kind: e.name.clone(),
53                    arg: e.arg.as_ref().map(|a| match a {
54                        a::EffectArg::Str { value } => EffectArg::Str(value.clone()),
55                        a::EffectArg::Int { value } => EffectArg::Int(*value),
56                        a::EffectArg::Ident { value } => EffectArg::Ident(value.clone()),
57                    }),
58                }).collect(),
59                // Filled in at the end of the compile pass, once `code`
60                // and `locals_count` are final. See #222.
61                body_hash: crate::program::ZERO_BODY_HASH,
62                // Per-param refinement predicates for runtime check
63                // (#209 slice 3). Lifted directly from each param's
64                // `TypeExpr::Refined` if present; `None` otherwise.
65                refinements: fd.params.iter().map(|p| match &p.ty {
66                    a::TypeExpr::Refined { binding, predicate, .. } =>
67                        Some(crate::program::Refinement {
68                            binding: binding.clone(),
69                            predicate: (**predicate).clone(),
70                        }),
71                    _ => None,
72                }).collect(),
73                // Filled in below once the FnCompiler counts emit sites.
74                field_ic_sites: 0,
75            });
76        }
77    }
78
79    let mut pool = ConstPool::default();
80    let function_names = p.function_names.clone();
81    let module_aliases = p.module_aliases.clone();
82    let mut pending_lambdas: Vec<PendingLambda> = Vec::new();
83    // #461 slice 7: collect `type Foo = { ... }` aliases so
84    // `record_field_types` can resolve a parameter's named record
85    // type to its field layout. Without this, `r :: R` where
86    // `type R = { x :: Int, y :: Int }` falls through to Unknown
87    // and the typed-Add lowering misses on `r.x + r.y`.
88    let mut type_aliases: IndexMap<String, a::TypeExpr> = IndexMap::new();
89    for s in stages {
90        if let a::Stage::TypeDecl(td) = s {
91            // Parameterized type aliases (`type Box[T] = ...`) are
92            // out of scope for this slice — without monomorphization
93            // we can't know what T resolves to. Skip them.
94            if td.params.is_empty() {
95                type_aliases.insert(td.name.clone(), td.definition.clone());
96            }
97        }
98    }
99
100    for s in stages {
101        if let a::Stage::FnDecl(_) = s {
102            // Build a NodeId map for *this* stage so the compiler can stamp
103            // each Call/EffectCall opcode with the originating AST node.
104            let id_map = lex_ast::expr_ids(s);
105            let fd = match s { a::Stage::FnDecl(fd) => fd, _ => unreachable!() };
106            let mut fc = FnCompiler {
107                code: Vec::new(),
108                locals: IndexMap::new(),
109                next_local: 0,
110                peak_local: 0,
111                local_types: IndexMap::new(),
112                local_record_field_types: IndexMap::new(),
113                field_get_sites: 0,
114                pool: &mut pool,
115                function_names: &function_names,
116                module_aliases: &module_aliases,
117                id_map: &id_map,
118                pending_lambdas: &mut pending_lambdas,
119                next_fn_id: &mut p.functions,
120            };
121            for param in &fd.params {
122                let i = fc.next_local;
123                fc.locals.insert(param.name.clone(), i);
124                fc.local_types.insert(param.name.clone(), classify_type_expr(&param.ty));
125                // #461 slice 7: inline-record parameter (`r ::
126                // { x :: Int, y :: Int }`) — populate the per-local
127                // field-type map so `r.x + r.y` classifies as
128                // Int+Int → IntAdd, which slice 7 then fuses.
129                if let Some(ftypes) = record_field_types(&param.ty, &type_aliases) {
130                    fc.local_record_field_types.insert(param.name.clone(), ftypes);
131                }
132                fc.next_local += 1;
133                fc.peak_local = fc.next_local;
134            }
135            fc.compile_expr(&fd.body, true);
136            fc.code.push(Op::Return);
137            let code = std::mem::take(&mut fc.code);
138            let peak = fc.peak_local;
139            let field_sites = fc.field_get_sites as u16;
140            drop(fc);
141            let idx = function_names[&fd.name];
142            p.functions[idx as usize].code = code;
143            p.functions[idx as usize].field_ic_sites = field_sites;
144            p.functions[idx as usize].locals_count = peak;
145        }
146    }
147
148    // Compile pending lambdas in FIFO order. Each lambda may emit further
149    // lambdas; loop until the queue drains.
150    while let Some(pl) = pending_lambdas.pop() {
151        let id_map = std::collections::HashMap::new();
152        let mut fc = FnCompiler {
153            code: Vec::new(),
154            locals: IndexMap::new(),
155            next_local: 0,
156            peak_local: 0,
157            local_types: IndexMap::new(),
158            local_record_field_types: IndexMap::new(),
159            field_get_sites: 0,
160            pool: &mut pool,
161            function_names: &function_names,
162            module_aliases: &module_aliases,
163            id_map: &id_map,
164            pending_lambdas: &mut pending_lambdas,
165            next_fn_id: &mut p.functions,
166        };
167        for name in &pl.capture_names {
168            let i = fc.next_local;
169            fc.locals.insert(name.clone(), i);
170            // Captures' static types aren't known at this layer
171            // — the closure's environment carries them dynamically.
172            // Conservative fallback; binop lowering stays correct
173            // because Unknown classifies through to NumAdd.
174            fc.local_types.insert(name.clone(), NumTy::Unknown);
175            fc.next_local += 1;
176            fc.peak_local = fc.next_local;
177        }
178        for p in &pl.params {
179            let i = fc.next_local;
180            fc.locals.insert(p.name.clone(), i);
181            fc.local_types.insert(p.name.clone(), classify_type_expr(&p.ty));
182            fc.next_local += 1;
183            fc.peak_local = fc.next_local;
184        }
185        fc.compile_expr(&pl.body, true);
186        fc.code.push(Op::Return);
187        let code = std::mem::take(&mut fc.code);
188        let peak = fc.peak_local;
189        let field_sites = fc.field_get_sites as u16;
190        drop(fc);
191        p.functions[pl.fn_id as usize].code = code;
192        p.functions[pl.fn_id as usize].field_ic_sites = field_sites;
193        p.functions[pl.fn_id as usize].locals_count = peak;
194    }
195
196    // #464 step 2: escape-analysis-driven lowering. Rewrites
197    // `MakeRecord` at non-escaping sites to `AllocStackRecord`, which
198    // the VM allocates in the frame's stack-record arena instead of
199    // on the heap. Runs on raw bytecode (before the peephole passes)
200    // so the escape analysis — which itself walks raw bytecode — sees
201    // exactly the program it was designed for.
202    //
203    // The peephole passes that follow do not match on MakeRecord /
204    // AllocStackRecord, so swapping one for the other doesn't disturb
205    // any pattern. `compute_body_hash` lowers AllocStackRecord back
206    // to the legacy MakeRecord form (#222), so closure identity is
207    // invariant under this lowering.
208    //
209    // Escape hatch: `LEX_NO_STACK_RECORDS=1` skips the lowering
210    // entirely (#464 step 3). The flag exists so the bench can A/B
211    // the same source under matched VM/peephole conditions; in
212    // production code the pass always runs.
213    if std::env::var_os("LEX_NO_STACK_RECORDS").is_none() {
214        let escape_index = crate::escape::build_escape_index(&p.functions);
215        for f in p.functions.iter_mut() {
216            apply_escape_lowering(&mut f.code, &f.name, &escape_index);
217        }
218    }
219
220    // #463 slice 2b-i: arena-eligibility lowering. Runs **after**
221    // `apply_escape_lowering` and targets the remaining `MakeRecord`
222    // / `MakeTuple` sites — those the stack pass left alone because
223    // they cross the frame boundary, but the request-scope analysis
224    // proves they stay inside the active `EffectHandler` arena
225    // scope. The two passes form a three-tier allocation hierarchy:
226    //
227    //   frame-local        → AllocStackRecord  (#464, cheapest)
228    //   request-local      → AllocArenaRecord  (#463, this slice)
229    //   escapes request    → MakeRecord        (heap, status quo)
230    //
231    // Order matters: a site that fits the stack tier should land
232    // there (cheapest), so the stack pass runs first. The arena
233    // pass's match doesn't fire on AllocStackRecord, so already-
234    // stack-lowered sites stay stack-lowered. Sites that escape the
235    // frame and the request both pass through unchanged.
236    //
237    // Escape hatch: `LEX_NO_ARENA_RECORDS=1` skips the lowering,
238    // mirroring `LEX_NO_STACK_RECORDS`. The slice-2b-i bench uses
239    // this to A/B identical source under matched VM conditions.
240    //
241    // `body_hash` invariance: `compute_body_hash` decodes
242    // `AllocArenaRecord` / `AllocArenaTuple` back to their legacy
243    // `MakeRecord` / `MakeTuple` form, so closure identity (#222) is
244    // bit-identical across this and the stack lowering.
245    if std::env::var_os("LEX_NO_ARENA_RECORDS").is_none() {
246        let arena_index = crate::arena::build_arena_index(&p.functions);
247        for f in p.functions.iter_mut() {
248            apply_arena_lowering(&mut f.code, &f.name, &arena_index);
249        }
250    }
251
252    // Peephole pass (#461 superinstructions). Rewrites fusable opcode
253    // patterns into single dispatch steps. Runs before `body_hash`
254    // computation, but `compute_body_hash` decomposes each fused op
255    // back to its primitive form on hash — so closure identity (#222)
256    // is invariant under this pass and the order doesn't matter.
257    //
258    // Slices run sequentially: slice 2 looks for slice-1 output
259    // followed by a StoreLocal, so it must follow slice 1. Slice 3
260    // (LoadLocal + LoadLocal + IntAdd) is disjoint from both — its
261    // second slot is LoadLocal, not PushConst — so it can run in
262    // either order. Run it last to keep the slice 1/2 contract
263    // (slice 2 expects to see slice-1 output) untouched. Slice 4 is
264    // slice 3 for IntSub / IntMul (same pattern, different terminator);
265    // disjoint from every prior slice because the terminator op
266    // disambiguates, so order between slice 3 and slice 4 is free.
267    for f in p.functions.iter_mut() {
268        apply_peephole(&mut f.code, &pool.pool);
269        apply_peephole_slice2(&mut f.code);
270        apply_peephole_slice3(&mut f.code);
271        apply_peephole_slice4(&mut f.code);
272        // Slice 5 — jump-aware fusion of the loop-condition idiom
273        // (LoadLocal + LoadLocal/PushConst + IntLt + JumpIfNot).
274        // Runs after slices 3/4 because their 3-slot windows
275        // overlap slice 5's 4-slot window at position 0 and 1; if
276        // slice 3 fired first and consumed `LoadLocal + LoadLocal +
277        // IntAdd`, the `IntLt + JumpIfNot` that follows would not
278        // be a fusion candidate. Since slice 3's terminator is
279        // `IntAdd` and slice 5's is `IntLt`, the two don't compete
280        // on the same site — order between them is technically free
281        // but conventionally slice N runs after slice N-1.
282        apply_peephole_slice5(&mut f.code, &pool.pool);
283        // Slice 6 — absorb the match-scrutinee dance
284        // (`LoadLocal + StoreLocal` immediately preceding a slice-5
285        // fused op that reads the just-stored local). Must run after
286        // slice 5 since it matches on slice 5's output.
287        apply_peephole_slice6(&mut f.code);
288        // Slice 7/8 — fuse `LoadLocal + GetField + IntAdd|IntSub|IntMul`,
289        // the accumulator-with-field-read idiom. Disjoint from every
290        // earlier slice (only this one matches a GetField at slot 1),
291        // so order is independent — placed near the end for chronology.
292        apply_peephole_slice7(&mut f.code);
293        // Slice 9 — fuse the *remaining* bare `LoadLocal + GetField`
294        // pairs (those slice 7/8 didn't consume): chain-head field
295        // reads (`r.x` in `r.x + r.y`), standalone `r.field` reads
296        // (`r.total`), and field reads feeding non-add/sub/mul ops.
297        // MUST run after slice 7/8 — otherwise it would greedily eat
298        // the `LoadLocal + GetField` prefix of an `acc OP r.field`
299        // triple and prevent the 3-op fusion. Slice 7/8's tombstone
300        // GetFields are preceded by their fused op (not a bare
301        // LoadLocal), so slice 9 never matches them.
302        apply_peephole_slice9(&mut f.code);
303    }
304
305    // #774: move-out loads. A `LoadLocal` whose slot is dead on every
306    // path after it becomes `TakeLocal`. Runs after the peepholes so
307    // every fusion still fires. Escape hatch: `LEX_NO_TAKE_LOCALS=1`,
308    // for A/B benches, mirroring the two lowering flags above.
309    if std::env::var_os("LEX_NO_TAKE_LOCALS").is_none() {
310        for f in p.functions.iter_mut() {
311            apply_last_load_takes(&mut f.code);
312        }
313    }
314
315    // Final pass: stamp every function with its content hash now that
316    // every body is finalized (#222). Trampolines installed via
317    // `install_trampoline` already have it; recomputing is cheap and
318    // makes the invariant easier to read at this top level.
319    for f in p.functions.iter_mut() {
320        if f.body_hash == crate::program::ZERO_BODY_HASH {
321            f.body_hash = crate::program::compute_body_hash(
322                f.arity, f.locals_count, &f.code, &pool.record_shapes);
323        }
324    }
325
326    p.constants = pool.pool;
327    p.record_shapes = pool.record_shapes;
328    p
329}
330
331#[derive(Debug, Clone)]
332struct PendingLambda {
333    fn_id: u32,
334    /// Names of captured outer-scope locals, in order.
335    capture_names: Vec<String>,
336    params: Vec<a::Param>,
337    body: a::CExpr,
338}
339
340struct FnCompiler<'a> {
341    code: Vec<Op>,
342    locals: IndexMap<String, u16>,
343    next_local: u16,
344    /// Peak local usage seen during compilation (for VM frame sizing).
345    peak_local: u16,
346    /// Inferred numeric type of each local for typed numeric-op
347    /// lowering (#461). Populated when binding function parameters
348    /// (from their declared `TypeExpr::Named { name: "Int", .. }`
349    /// or `"Float"`) and when binding `let name := value` where
350    /// the RHS classifies statically. Used by `compile_binop` to
351    /// emit `Op::IntAdd` / `Op::FloatAdd` instead of the
352    /// polymorphic `Op::NumAdd` when both operands' types are
353    /// statically known. Conservative: falls back to `NumTy::Unknown`
354    /// (and the polymorphic op) whenever a type isn't locally
355    /// derivable.
356    ///
357    /// Keyed by local *name* (parallel to `locals`) rather than by
358    /// slot index so shadowed bindings are handled correctly via
359    /// `IndexMap`'s insertion-order semantics.
360    local_types: IndexMap<String, NumTy>,
361    /// Per-local map of statically-known field types (#461 slice 7).
362    /// Populated when a local is bound from a `RecordLit` whose
363    /// fields all classify to non-`Unknown` `NumTy`s. Lets
364    /// `classify_expr(FieldAccess { value: Var(name), field })`
365    /// return a precise `NumTy` instead of falling back to
366    /// `Unknown` — which in turn unlocks the typed-Add lowering
367    /// (`+` over two Ints → `IntAdd`) on `r.field + r.field`
368    /// chains, which slice 7 then fuses into
369    /// `LoadLocalGetFieldAdd`.
370    ///
371    /// Only the literal-binding case is tracked here; annotated
372    /// `let r :: R := ...` would require resolving the type alias
373    /// `R` to its field-type map, which the compiler doesn't yet
374    /// have. Future slice work.
375    local_record_field_types: IndexMap<String, IndexMap<String, NumTy>>,
376    /// Per-function counter for `Op::GetField` site indices (#462
377    /// slice 1). Each `Op::GetField` emit allocates the next index
378    /// here, giving every field-access site within this function a
379    /// stable identifier independent of pc. The VM uses
380    /// `(fn_id, site_idx)` as the inline-cache key, so the cache
381    /// survives the future dispatch rewrite (#461) and a JIT (#465).
382    field_get_sites: u32,
383    pool: &'a mut ConstPool,
384    function_names: &'a IndexMap<String, u32>,
385    module_aliases: &'a IndexMap<String, String>,
386    /// CExpr address → NodeId, populated per stage via `lex_ast::expr_ids`.
387    id_map: &'a std::collections::HashMap<*const a::CExpr, lex_ast::NodeId>,
388    /// Queue of lambdas discovered during compilation; each gets a fresh
389    /// fn_id and is compiled in a later pass.
390    pending_lambdas: &'a mut Vec<PendingLambda>,
391    /// Mutable view of the function table — used to allocate fn_ids for
392    /// freshly-discovered lambdas.
393    next_fn_id: &'a mut Vec<Function>,
394}
395
396/// Lightweight numeric-type classification used by `compile_binop`
397/// to decide whether to emit `IntAdd` / `FloatAdd` (specialized,
398/// fast) or `NumAdd` (polymorphic, runtime-typed dispatch). #461
399/// typed-lowering pass — conservative: anything not provably one
400/// of these returns `Unknown` and falls back to the polymorphic op.
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402enum NumTy { Int, Float, Unknown }
403
404/// #461 slice 7: extract a `field_name -> NumTy` map from a record
405/// type expression. Resolves named types (`r :: R`) via
406/// `type_aliases` and returns `None` if the type ultimately isn't
407/// a record literal.
408fn record_field_types(
409    ty: &a::TypeExpr,
410    type_aliases: &IndexMap<String, a::TypeExpr>,
411) -> Option<IndexMap<String, NumTy>> {
412    match ty {
413        a::TypeExpr::Record { fields } => {
414            let mut m = IndexMap::new();
415            for f in fields {
416                m.insert(f.name.clone(), classify_type_expr(&f.ty));
417            }
418            Some(m)
419        }
420        a::TypeExpr::Refined { base, .. } => record_field_types(base, type_aliases),
421        a::TypeExpr::Named { name, args } if args.is_empty() => {
422            // Resolve the alias and recurse. Cycle protection isn't
423            // needed here — a cyclic type alias would have been
424            // rejected by `lex-types::check_program` upstream.
425            type_aliases.get(name).and_then(|t| record_field_types(t, type_aliases))
426        }
427        _ => None,
428    }
429}
430
431fn classify_type_expr(ty: &a::TypeExpr) -> NumTy {
432    match ty {
433        a::TypeExpr::Named { name, args } if args.is_empty() => match name.as_str() {
434            "Int" => NumTy::Int,
435            "Float" => NumTy::Float,
436            _ => NumTy::Unknown,
437        },
438        // `Refined { base, .. }` (#209) — classify by the base type;
439        // the refinement predicate doesn't change the value's primitive shape.
440        a::TypeExpr::Refined { base, .. } => classify_type_expr(base),
441        _ => NumTy::Unknown,
442    }
443}
444
445impl<'a> FnCompiler<'a> {
446    fn alloc_local(&mut self, name: &str) -> u16 {
447        let i = self.next_local;
448        self.locals.insert(name.into(), i);
449        self.next_local += 1;
450        if self.next_local > self.peak_local { self.peak_local = self.next_local; }
451        i
452    }
453    fn emit(&mut self, op: Op) { self.code.push(op); }
454
455    fn compile_expr(&mut self, e: &a::CExpr, tail: bool) {
456        match e {
457            a::CExpr::Literal { value } => self.compile_lit(value),
458            a::CExpr::Var { name } => {
459                if let Some(slot) = self.locals.get(name) {
460                    self.emit(Op::LoadLocal(*slot));
461                } else if let Some(&fn_id) = self.function_names.get(name) {
462                    // Function name used as a *value* (e.g. as a record-field
463                    // initializer or fold-callback arg) — materialize it as a
464                    // closure with no captures. The runtime already accepts
465                    // `Value::Closure { fn_id, captures: vec![] }` and
466                    // `CallClosure` dispatches it. (#169)
467                    self.emit(Op::MakeClosure { fn_id, capture_count: 0 });
468                } else {
469                    // Should be caught at type-check time; the type checker
470                    // walks every Var. If we land here it's a compiler bug,
471                    // not a user typo.
472                    panic!("unknown var in compiler: {name}");
473                }
474            }
475            a::CExpr::Let { name, ty, value, body } => {
476                // Classify the RHS for typed-op lowering (#461). Prefer
477                // the declared annotation when present (cheap O(1)
478                // lookup); fall back to classifying the value
479                // expression structurally.
480                let nty = match ty {
481                    Some(t) => classify_type_expr(t),
482                    None => self.classify_expr(value),
483                };
484                // #461 slice 7: when the RHS is a record literal,
485                // remember the field types so `name.field` accesses
486                // downstream can classify precisely. Without this,
487                // `r.x + r.y` falls through to `NumAdd`, blocking
488                // the slice-7 fusion.
489                if let a::CExpr::RecordLit { fields } = value.as_ref() {
490                    let mut ftypes = IndexMap::new();
491                    for f in fields {
492                        let fty = self.classify_expr(&f.value);
493                        ftypes.insert(f.name.clone(), fty);
494                    }
495                    self.local_record_field_types.insert(name.clone(), ftypes);
496                }
497                self.compile_expr(value, false);
498                let slot = self.alloc_local(name);
499                self.local_types.insert(name.clone(), nty);
500                self.emit(Op::StoreLocal(slot));
501                self.compile_expr(body, tail);
502            }
503            a::CExpr::Block { statements, result } => {
504                for s in statements {
505                    self.compile_expr(s, false);
506                    self.emit(Op::Pop);
507                }
508                self.compile_expr(result, tail);
509            }
510            a::CExpr::Call { callee, args } => self.compile_call(e, callee, args, tail),
511            a::CExpr::Constructor { name, args } => {
512                for a in args { self.compile_expr(a, false); }
513                let name_idx = self.pool.variant(name);
514                self.emit(Op::MakeVariant { name_idx, arity: args.len() as u16 });
515            }
516            a::CExpr::Match { scrutinee, arms } => self.compile_match(scrutinee, arms, tail),
517            a::CExpr::RecordLit { fields } => {
518                let mut idxs = Vec::with_capacity(fields.len());
519                for f in fields {
520                    self.compile_expr(&f.value, false);
521                    idxs.push(self.pool.field(&f.name));
522                }
523                let field_count = idxs.len() as u16;
524                let shape_idx = self.pool.record_shape(idxs);
525                self.emit(Op::MakeRecord { shape_idx, field_count });
526            }
527            a::CExpr::TupleLit { items } => {
528                for it in items { self.compile_expr(it, false); }
529                self.emit(Op::MakeTuple(items.len() as u16));
530            }
531            a::CExpr::ListLit { items } => {
532                for it in items { self.compile_expr(it, false); }
533                self.emit(Op::MakeList(items.len() as u32));
534            }
535            a::CExpr::FieldAccess { value, field } => {
536                self.compile_expr(value, false);
537                let name_idx = self.pool.field(field);
538                let site_idx = self.field_get_sites;
539                self.field_get_sites += 1;
540                self.emit(Op::GetField { name_idx, site_idx });
541            }
542            a::CExpr::BinOp { op, lhs, rhs } => self.compile_binop(op, lhs, rhs),
543            a::CExpr::UnaryOp { op, expr } => {
544                self.compile_expr(expr, false);
545                match op.as_str() {
546                    "-" => self.emit(Op::NumNeg),
547                    "not" => self.emit(Op::BoolNot),
548                    other => panic!("unknown unary: {other}"),
549                }
550            }
551            a::CExpr::Lambda { params, body, .. } => self.compile_lambda(params, body),
552            a::CExpr::Return { value } => {
553                self.compile_expr(value, true);
554                self.emit(Op::Return);
555            }
556        }
557    }
558
559    fn compile_lit(&mut self, l: &a::CLit) {
560        let i = match l {
561            a::CLit::Int { value } => self.pool.int(*value),
562            a::CLit::Bool { value } => self.pool.bool(*value),
563            a::CLit::Float { value } => {
564                let f: f64 = value.parse().unwrap_or(0.0);
565                self.pool.float(f)
566            }
567            a::CLit::Str { value } => self.pool.str(value),
568            a::CLit::Bytes { value: _ } => {
569                // Stub: M4 doesn't use bytes literals in §3.13 examples.
570                let i = self.pool.pool.len() as u32;
571                self.pool.pool.push(Const::Bytes(Vec::new()));
572                i
573            }
574            a::CLit::Unit => self.pool.unit(),
575        };
576        self.emit(Op::PushConst(i));
577    }
578
579    fn compile_call(&mut self, call_expr: &a::CExpr, callee: &a::CExpr, args: &[a::CExpr], tail: bool) {
580        let node_id = self
581            .id_map
582            .get(&(call_expr as *const a::CExpr))
583            .map(|n| n.as_str().to_string())
584            .unwrap_or_else(|| "n_?".into());
585        let node_id_idx = self.pool.node_id(&node_id);
586
587        // Module function call: `alias.op(args)` where `alias` is an imported
588        // module ⇒ EffectCall, except for higher-order pure ops where we
589        // emit inline bytecode using CallClosure (the closure-arg can't be
590        // serialized through the effect handler).
591        if let a::CExpr::FieldAccess { value, field } = callee {
592            if let a::CExpr::Var { name } = value.as_ref() {
593                if let Some(module) = self.module_aliases.get(name) {
594                    if self.try_emit_higher_order(module, field, args, node_id_idx) {
595                        let _ = tail;
596                        return;
597                    }
598                    for a in args { self.compile_expr(a, false); }
599                    let kind_idx = self.pool.str(module);
600                    let op_idx = self.pool.str(field);
601                    self.emit(Op::EffectCall {
602                        kind_idx,
603                        op_idx,
604                        arity: args.len() as u16,
605                        node_id_idx,
606                    });
607                    let _ = tail;
608                    return;
609                }
610            }
611        }
612        match callee {
613            a::CExpr::Var { name } if self.function_names.contains_key(name) => {
614                for a in args { self.compile_expr(a, false); }
615                let fn_id = self.function_names[name];
616                if tail {
617                    self.emit(Op::TailCall { fn_id, arity: args.len() as u16, node_id_idx });
618                } else {
619                    self.emit(Op::Call { fn_id, arity: args.len() as u16, node_id_idx });
620                }
621            }
622            a::CExpr::Var { name } if self.locals.contains_key(name) => {
623                // First-class function value bound to a local. Push the
624                // closure, then args, then CallClosure.
625                let slot = self.locals[name];
626                self.emit(Op::LoadLocal(slot));
627                for a in args { self.compile_expr(a, false); }
628                self.emit(Op::CallClosure { arity: args.len() as u16, node_id_idx });
629            }
630            // Lambda directly applied — push closure + args + CallClosure.
631            other => {
632                self.compile_expr(other, false);
633                for a in args { self.compile_expr(a, false); }
634                self.emit(Op::CallClosure { arity: args.len() as u16, node_id_idx });
635            }
636        }
637    }
638
639    fn compile_binop(&mut self, op: &str, lhs: &a::CExpr, rhs: &a::CExpr) {
640        // #461 typed lowering: if we can statically prove both
641        // operands are the same numeric type, emit the typed
642        // primitive (`IntAdd` / `FloatAdd`) instead of the
643        // polymorphic `NumAdd` that runtime-matches on operand
644        // shape. The fast path skips one match per arithmetic op
645        // *and* unblocks downstream peephole fusions (slice 1)
646        // that scan for typed primitives. Conservative fallback
647        // to the polymorphic op when either side classifies as
648        // `Unknown`, so correctness for `Float` / mixed code is
649        // unchanged.
650        let lhs_ty = self.classify_expr(lhs);
651        let rhs_ty = self.classify_expr(rhs);
652        let typed = match (lhs_ty, rhs_ty) {
653            (NumTy::Int, NumTy::Int) => NumTy::Int,
654            (NumTy::Float, NumTy::Float) => NumTy::Float,
655            _ => NumTy::Unknown,
656        };
657        self.compile_expr(lhs, false);
658        self.compile_expr(rhs, false);
659        match (op, typed) {
660            ("+",  NumTy::Int)     => self.emit(Op::IntAdd),
661            ("+",  NumTy::Float)   => self.emit(Op::FloatAdd),
662            ("+",  NumTy::Unknown) => self.emit(Op::NumAdd),
663            ("-",  NumTy::Int)     => self.emit(Op::IntSub),
664            ("-",  NumTy::Float)   => self.emit(Op::FloatSub),
665            ("-",  NumTy::Unknown) => self.emit(Op::NumSub),
666            ("*",  NumTy::Int)     => self.emit(Op::IntMul),
667            ("*",  NumTy::Float)   => self.emit(Op::FloatMul),
668            ("*",  NumTy::Unknown) => self.emit(Op::NumMul),
669            ("/",  NumTy::Int)     => self.emit(Op::IntDiv),
670            ("/",  NumTy::Float)   => self.emit(Op::FloatDiv),
671            ("/",  NumTy::Unknown) => self.emit(Op::NumDiv),
672            // Int has %; Float doesn't (NumMod will reject at runtime).
673            ("%",  NumTy::Int)     => self.emit(Op::IntMod),
674            ("%",  _)              => self.emit(Op::NumMod),
675            ("==", NumTy::Int)     => self.emit(Op::IntEq),
676            ("==", NumTy::Float)   => self.emit(Op::FloatEq),
677            ("==", NumTy::Unknown) => self.emit(Op::NumEq),
678            ("!=", NumTy::Int)     => { self.emit(Op::IntEq);   self.emit(Op::BoolNot); }
679            ("!=", NumTy::Float)   => { self.emit(Op::FloatEq); self.emit(Op::BoolNot); }
680            ("!=", NumTy::Unknown) => { self.emit(Op::NumEq);   self.emit(Op::BoolNot); }
681            ("<",  NumTy::Int)     => self.emit(Op::IntLt),
682            ("<",  NumTy::Float)   => self.emit(Op::FloatLt),
683            ("<",  NumTy::Unknown) => self.emit(Op::NumLt),
684            ("<=", NumTy::Int)     => self.emit(Op::IntLe),
685            ("<=", NumTy::Float)   => self.emit(Op::FloatLe),
686            ("<=", NumTy::Unknown) => self.emit(Op::NumLe),
687            (">",  NumTy::Int)     => { self.emit_swap_top2(); self.emit(Op::IntLt); }
688            (">",  NumTy::Float)   => { self.emit_swap_top2(); self.emit(Op::FloatLt); }
689            (">",  NumTy::Unknown) => { self.emit_swap_top2(); self.emit(Op::NumLt); }
690            (">=", NumTy::Int)     => { self.emit_swap_top2(); self.emit(Op::IntLe); }
691            (">=", NumTy::Float)   => { self.emit_swap_top2(); self.emit(Op::FloatLe); }
692            (">=", NumTy::Unknown) => { self.emit_swap_top2(); self.emit(Op::NumLe); }
693            ("and", _) => self.emit(Op::BoolAnd),
694            ("or",  _) => self.emit(Op::BoolOr),
695            (other, _) => panic!("unknown binop: {other:?}"),
696        }
697    }
698
699    /// Classify an expression's static numeric type for #461 typed
700    /// lowering. Strictly conservative: only returns `Int` / `Float`
701    /// when the type is locally derivable from a literal, an
702    /// already-classified local, or a binary op on two same-typed
703    /// operands. Everything else (function calls, field access,
704    /// match expressions, ...) falls back to `Unknown` and the
705    /// polymorphic NumAdd-family op.
706    fn classify_expr(&self, e: &a::CExpr) -> NumTy {
707        match e {
708            a::CExpr::Literal { value: a::CLit::Int { .. } } => NumTy::Int,
709            a::CExpr::Literal { value: a::CLit::Float { .. } } => NumTy::Float,
710            a::CExpr::Var { name } =>
711                self.local_types.get(name).copied().unwrap_or(NumTy::Unknown),
712            a::CExpr::BinOp { op, lhs, rhs } => {
713                // Numeric ops preserve the operand type (Int+Int=Int,
714                // Float+Float=Float). Comparison/logical ops yield
715                // Bool, not a numeric type — return Unknown.
716                let is_numeric = matches!(op.as_str(), "+" | "-" | "*" | "/" | "%");
717                if !is_numeric { return NumTy::Unknown; }
718                match (self.classify_expr(lhs), self.classify_expr(rhs)) {
719                    (NumTy::Int, NumTy::Int) => NumTy::Int,
720                    (NumTy::Float, NumTy::Float) => NumTy::Float,
721                    _ => NumTy::Unknown,
722                }
723            }
724            a::CExpr::UnaryOp { op, expr } if op == "-" => self.classify_expr(expr),
725            // #461 slice 7: `r.field` access where `r` is a local
726            // bound from a record literal. Reads the per-local
727            // field-type map populated at the let-binding site.
728            // Unknown otherwise (record argument with `:: R`
729            // annotation, helper-returned record, etc.) — those
730            // would need type-alias resolution to classify.
731            a::CExpr::FieldAccess { value, field } => {
732                if let a::CExpr::Var { name } = value.as_ref() {
733                    if let Some(ftypes) = self.local_record_field_types.get(name) {
734                        return ftypes.get(field).copied().unwrap_or(NumTy::Unknown);
735                    }
736                }
737                NumTy::Unknown
738            }
739            // Let-expressions: the let-binding mutates `local_types`
740            // *during* compile_expr; classifying ahead of time would
741            // require simulating that. Conservative fallback.
742            _ => NumTy::Unknown,
743        }
744    }
745
746    fn emit_swap_top2(&mut self) {
747        let a = self.alloc_local("__swap_a");
748        let b = self.alloc_local("__swap_b");
749        self.emit(Op::StoreLocal(b));
750        self.emit(Op::StoreLocal(a));
751        self.emit(Op::LoadLocal(b));
752        self.emit(Op::LoadLocal(a));
753    }
754
755    fn compile_match(&mut self, scrutinee: &a::CExpr, arms: &[a::Arm], tail: bool) {
756        self.compile_expr(scrutinee, false);
757        let scrut_slot = self.alloc_local("__scrut");
758        self.emit(Op::StoreLocal(scrut_slot));
759
760        let mut end_jumps: Vec<usize> = Vec::new();
761        for arm in arms {
762            let arm_start_locals = self.next_local;
763            let arm_start_locals_map = self.locals.clone();
764
765            self.emit(Op::LoadLocal(scrut_slot));
766            let mut bindings: Vec<(String, u16)> = Vec::new();
767            let fail_jumps: Vec<usize> = self.compile_pattern_test(&arm.pattern, &mut bindings);
768
769            self.compile_expr(&arm.body, tail);
770            let j_end = self.code.len();
771            self.emit(Op::Jump(0));
772            end_jumps.push(j_end);
773
774            let fail_target = self.code.len() as i32;
775            for j in fail_jumps {
776                // #337: PConstructor patterns now register an
777                // unconditional `Op::Jump` for the failure path
778                // (alongside the existing `Op::JumpIfNot` from
779                // PLiteral / nested constructor tests). Patch
780                // either shape.
781                match &mut self.code[j] {
782                    Op::JumpIfNot(off) => *off = fail_target - (j as i32 + 1),
783                    Op::Jump(off)      => *off = fail_target - (j as i32 + 1),
784                    _ => {}
785                }
786            }
787            self.next_local = arm_start_locals;
788            self.locals = arm_start_locals_map;
789        }
790        let panic_msg_idx = self.pool.str("non-exhaustive match");
791        self.emit(Op::Panic(panic_msg_idx));
792
793        let end_target = self.code.len() as i32;
794        for j in end_jumps {
795            if let Op::Jump(off) = &mut self.code[j] {
796                *off = end_target - (j as i32 + 1);
797            }
798        }
799    }
800
801    fn compile_pattern_test(&mut self, p: &a::Pattern, bindings: &mut Vec<(String, u16)>) -> Vec<usize> {
802        let mut fails = Vec::new();
803        match p {
804            a::Pattern::PWild => { self.emit(Op::Pop); }
805            a::Pattern::PVar { name } => {
806                let slot = self.alloc_local(name);
807                self.emit(Op::StoreLocal(slot));
808                bindings.push((name.clone(), slot));
809            }
810            a::Pattern::PLiteral { value } => {
811                self.compile_lit(value);
812                match value {
813                    a::CLit::Str { .. } => self.emit(Op::StrEq),
814                    a::CLit::Bytes { .. } => self.emit(Op::BytesEq),
815                    // Typed-lowering for numeric literal patterns
816                    // (#461 slice 5 prerequisite). The pattern only
817                    // reaches its test when the scrutinee has the
818                    // literal's type (the type checker rejects
819                    // mismatches), so emit the type-specific Eq.
820                    // The body_hash decoder lowers IntEq/FloatEq to
821                    // NumEq at hash time so closure identity (#222)
822                    // is unchanged. Enables slice 5's
823                    // `LoadLocal + PushConst + IntEq + JumpIfNot`
824                    // peephole to fire on pattern-match arm tests.
825                    a::CLit::Int { .. } => self.emit(Op::IntEq),
826                    a::CLit::Float { .. } => self.emit(Op::FloatEq),
827                    _ => self.emit(Op::NumEq),
828                }
829                let j = self.code.len();
830                self.emit(Op::JumpIfNot(0));
831                fails.push(j);
832            }
833            a::Pattern::PConstructor { name, args } => {
834                let name_idx = self.pool.variant(name);
835                // #337: the failure path must drop the duplicated
836                // scrutinee so subsequent match arms see a clean
837                // stack. The previous shape
838                //   Dup; TestVariant; JumpIfNot(fail);
839                // left `[scrut]` on the stack at the fail target,
840                // poisoning later arms — e.g. a wildcard `_` arm
841                // whose body referenced an unrelated value would
842                // pop the leaked scrutinee instead of its own value.
843                //
844                // New shape: branch on success, fall through to a
845                // failure cleanup that pops the dup'd scrutinee
846                // before jumping. The registered fail-jump is an
847                // unconditional `Op::Jump`; `compile_match`'s patch
848                // loop accepts both `JumpIfNot` and `Jump`.
849                self.emit(Op::Dup);                   // [scrut, scrut]
850                self.emit(Op::TestVariant(name_idx)); // [scrut, Bool]
851                let j_success = self.code.len();
852                self.emit(Op::JumpIf(0));             // pop Bool. success → [scrut]
853                self.emit(Op::Pop);                   // failure cleanup: [scrut] → []
854                let j_fail = self.code.len();
855                self.emit(Op::Jump(0));               // → fail target with []
856                fails.push(j_fail);
857                let success_target = self.code.len() as i32;
858                if let Op::JumpIf(off) = &mut self.code[j_success] {
859                    *off = success_target - (j_success as i32 + 1);
860                }
861                if args.is_empty() {
862                    self.emit(Op::Pop);
863                } else if args.len() == 1 {
864                    self.emit(Op::GetVariantArg(0));
865                    let sub_fails = self.compile_pattern_test(&args[0], bindings);
866                    fails.extend(sub_fails);
867                } else {
868                    let slot = self.alloc_local("__variant");
869                    self.emit(Op::StoreLocal(slot));
870                    for (i, arg) in args.iter().enumerate() {
871                        self.emit(Op::LoadLocal(slot));
872                        self.emit(Op::GetVariantArg(i as u16));
873                        let sub_fails = self.compile_pattern_test(arg, bindings);
874                        fails.extend(sub_fails);
875                    }
876                }
877            }
878            a::Pattern::PRecord { fields } => {
879                let slot = self.alloc_local("__record");
880                self.emit(Op::StoreLocal(slot));
881                for f in fields {
882                    self.emit(Op::LoadLocal(slot));
883                    let name_idx = self.pool.field(&f.name);
884                    let site_idx = self.field_get_sites;
885                    self.field_get_sites += 1;
886                    self.emit(Op::GetField { name_idx, site_idx });
887                    let sub_fails = self.compile_pattern_test(&f.pattern, bindings);
888                    fails.extend(sub_fails);
889                }
890            }
891            a::Pattern::PTuple { items } => {
892                let slot = self.alloc_local("__tuple");
893                self.emit(Op::StoreLocal(slot));
894                for (i, item) in items.iter().enumerate() {
895                    self.emit(Op::LoadLocal(slot));
896                    self.emit(Op::GetElem(i as u16));
897                    let sub_fails = self.compile_pattern_test(item, bindings);
898                    fails.extend(sub_fails);
899                }
900            }
901        }
902        fails
903    }
904
905    /// Compile a Lambda: collect free variables that resolve to outer-scope
906    /// locals, register a synthetic function, emit MakeClosure with the
907    /// captured values pushed in order.
908    fn compile_lambda(&mut self, params: &[a::Param], body: &a::CExpr) {
909        // Free vars = vars referenced in body that aren't bound locally.
910        let mut bound: std::collections::HashSet<String> = params.iter().map(|p| p.name.clone()).collect();
911        let mut frees: Vec<String> = Vec::new();
912        free_vars(body, &mut bound, &mut frees);
913
914        // Filter to those that are in the enclosing locals (captures).
915        // Don't exclude names that *also* exist in `function_names`:
916        // if the name is in `locals`, the local shadows the global
917        // within this scope, and the lambda needs to capture the
918        // local's value, not the global fn. (#339) Names that are
919        // ONLY in `function_names` (no local) stay external — the
920        // lambda's body resolves them at call time, same as the
921        // enclosing fn would.
922        let captures: Vec<String> = frees.into_iter()
923            .filter(|n| self.locals.contains_key(n))
924            .collect();
925
926        // Allocate a fresh fn_id by appending a placeholder Function.
927        let fn_id = self.next_fn_id.len() as u32;
928        self.next_fn_id.push(Function {
929            name: format!("__lambda_{fn_id}"),
930            arity: (captures.len() + params.len()) as u16,
931            locals_count: 0,
932            code: Vec::new(),
933            effects: Vec::new(),
934            // See #222: filled in at the end of the compile pass.
935            body_hash: crate::program::ZERO_BODY_HASH,
936            // Lambdas don't carry refinements at the surface today
937            // (closure params don't accept `Type{x | ...}` syntax in
938            // the parser). #209 stays focused on top-level fn decls;
939            // closure-param refinements are a follow-up.
940            refinements: Vec::new(),
941            // Lambda body hasn't been compiled yet; filled in by the
942            // deferred lambda-compile pass after FnCompiler walks it.
943            field_ic_sites: 0,
944        });
945
946        // Emit code at the lambda site: load each captured local, then MakeClosure.
947        for c in &captures {
948            let slot = *self.locals.get(c).expect("free var must be in scope");
949            self.emit(Op::LoadLocal(slot));
950        }
951        self.emit(Op::MakeClosure { fn_id, capture_count: captures.len() as u16 });
952
953        // Queue the body for later compilation.
954        self.pending_lambdas.push(PendingLambda {
955            fn_id,
956            capture_names: captures,
957            params: params.to_vec(),
958            body: body.clone(),
959        });
960    }
961
962    /// Higher-order stdlib ops on Result/Option whose function arg is a
963    /// closure. Emit inline: pattern-match on the variant, invoke the
964    /// closure when applicable, return wrapped result.
965    fn try_emit_higher_order(
966        &mut self,
967        module: &str,
968        op: &str,
969        args: &[a::CExpr],
970        node_id_idx: u32,
971    ) -> bool {
972        match (module, op) {
973            ("result", "map") => self.emit_variant_map(args, "Ok", true),
974            ("result", "and_then") => self.emit_variant_map(args, "Ok", false),
975            ("result", "map_err") => self.emit_variant_map(args, "Err", true),
976            ("result", "or_else") => self.emit_variant_or_else(args, "Err", 1),
977            ("option", "map") => self.emit_variant_map(args, "Some", true),
978            ("option", "and_then") => self.emit_variant_map(args, "Some", false),
979            ("option", "or_else") => self.emit_variant_or_else(args, "None", 0),
980            ("option", "unwrap_or_else") => self.emit_option_unwrap_or_else(args),
981            ("result", "unwrap_or_else") => self.emit_result_unwrap_or_else(args),
982            ("list", "map") => self.emit_list_map(args),
983            ("list", "par_map") => self.emit_list_par_map(args),
984            ("list", "sort_by") => self.emit_list_sort_by(args),
985            ("list", "filter") => self.emit_list_filter(args),
986            ("list", "fold") => self.emit_list_fold(args),
987            ("iter", "from_list") => self.emit_iter_from_list(args),
988            ("iter", "unfold")    => self.emit_iter_unfold(args),
989            ("iter", "next")      => self.emit_iter_next(args),
990            ("iter", "is_empty")  => self.emit_iter_is_empty(args),
991            ("iter", "count")     => self.emit_iter_count(args),
992            ("iter", "take")      => self.emit_iter_take(args),
993            ("iter", "skip")      => self.emit_iter_skip(args),
994            ("iter", "to_list")   => self.emit_iter_to_list(args),
995            ("iter", "collect")   => self.emit_iter_to_list(args),
996            ("iter", "map")       => self.emit_iter_map(args),
997            ("iter", "filter")    => self.emit_iter_filter(args),
998            ("iter", "fold")      => self.emit_iter_fold(args),
999            ("map", "fold") => self.emit_map_fold(args, node_id_idx),
1000            ("flow", "sequential") => self.emit_flow_sequential(args),
1001            ("flow", "branch") => self.emit_flow_branch(args),
1002            ("flow", "retry") => self.emit_flow_retry(args),
1003            ("flow", "retry_with_backoff") => self.emit_flow_retry_with_backoff(args),
1004            ("flow", "parallel") => self.emit_flow_parallel(args),
1005            ("flow", "parallel_list") => self.emit_flow_parallel_list(args),
1006            _ => return false,
1007        }
1008        true
1009    }
1010
1011    /// `list.map(xs, f)` — native map op (#464). Pushes `xs` then `f`
1012    /// and emits a single `Op::ListMap`. The previous inlined loop
1013    /// re-`LoadLocal`'d (cloned) the whole input and accumulator lists
1014    /// each iteration — O(n²); the native op owns the list and builds
1015    /// the result with one pre-sized allocation.
1016    fn emit_list_map(&mut self, args: &[a::CExpr]) {
1017        self.compile_expr(&args[0], false); // xs
1018        self.compile_expr(&args[1], false); // f
1019        let nid = self.pool.node_id("n_list_map");
1020        self.emit(Op::ListMap { node_id_idx: nid });
1021    }
1022
1023    /// `list.par_map(xs, f)` (#305 slice 1). Pushes `xs` and `f`,
1024    /// then emits a single `Op::ParallelMap` — the VM applies `f`
1025    /// to each element on OS-thread tasks, capped by
1026    /// `LEX_PAR_MAX_CONCURRENCY`. Returns the result list in input
1027    /// order.
1028    fn emit_list_par_map(&mut self, args: &[a::CExpr]) {
1029        self.compile_expr(&args[0], false);
1030        self.compile_expr(&args[1], false);
1031        let nid = self.pool.node_id("n_list_par_map");
1032        self.emit(Op::ParallelMap { node_id_idx: nid });
1033    }
1034
1035    /// `list.sort_by(xs, f)` (#338). Pushes `xs` and the key-fn
1036    /// `f`, then emits a single `Op::SortByKey` — the VM invokes
1037    /// `f` on each element to derive a sortable key, stable-sorts
1038    /// by key, and returns the values in sorted order. Keys must
1039    /// resolve to `Int` / `Float` / `Str`; mixed-type pairs are
1040    /// treated as equal by the comparator (preserving insertion
1041    /// order via the stable sort).
1042    fn emit_list_sort_by(&mut self, args: &[a::CExpr]) {
1043        self.compile_expr(&args[0], false);
1044        self.compile_expr(&args[1], false);
1045        let nid = self.pool.node_id("n_list_sort_by");
1046        self.emit(Op::SortByKey { node_id_idx: nid });
1047    }
1048
1049    /// `list.filter(xs, pred)` — native filter op (#464). Same
1050    /// rationale as `emit_list_map`.
1051    fn emit_list_filter(&mut self, args: &[a::CExpr]) {
1052        self.compile_expr(&args[0], false); // xs
1053        self.compile_expr(&args[1], false); // pred
1054        let nid = self.pool.node_id("n_list_filter");
1055        self.emit(Op::ListFilter { node_id_idx: nid });
1056    }
1057
1058    /// `list.fold(xs, init, f)` — native left-fold op (#464). Same
1059    /// rationale as `emit_list_map`. Stack: `[xs, init, f]`.
1060    fn emit_list_fold(&mut self, args: &[a::CExpr]) {
1061        self.compile_expr(&args[0], false); // xs
1062        self.compile_expr(&args[1], false); // init
1063        self.compile_expr(&args[2], false); // f
1064        let nid = self.pool.node_id("n_list_fold");
1065        self.emit(Op::ListFold { node_id_idx: nid });
1066    }
1067
1068    // ── Iter[T] operations (#364) ─────────────────────────────────────────
1069    // Internal representation: `Value::Variant("__IterEager", [list, idx])`
1070    // for the eager form (a List backing store + Int cursor) and
1071    // `Value::Variant("__IterLazy", [seed, step_closure])` for the lazy form
1072    // produced by `iter.unfold` (#376). Both are tagged variants so each op
1073    // can `TestVariant` at runtime to dispatch. The names start with `__` so
1074    // they can't be written by user code (uppercase ASCII-letter is required
1075    // for constructor names, and the underscores keep them out of the
1076    // user-namespace by convention).
1077
1078    /// `iter.from_list(xs)` — wrap a list in an eager iterator at position 0.
1079    fn emit_iter_from_list(&mut self, args: &[a::CExpr]) {
1080        self.compile_expr(&args[0], false);
1081        let zero = self.pool.int(0);
1082        self.emit(Op::PushConst(zero));
1083        let v = self.pool.variant("__IterEager");
1084        self.emit(Op::MakeVariant { name_idx: v, arity: 2 });
1085    }
1086
1087    /// `iter.next(it)` — advance one step; returns `Option[(T, Iter[T])]`.
1088    ///
1089    /// Dispatches on the iter's variant tag:
1090    /// - `__IterLazy(seed, step)` (#376) → invoke `step(seed)`. On
1091    ///   `Some((t, s'))` wrap as `Some((t, __IterLazy(s', step)))`; on
1092    ///   `None` propagate `None`. The seed advances forward each call.
1093    /// - `__IterCursor(handle)` (#379) → effect-call `sql.cursor_next(handle)`
1094    ///   which returns `Option[T]`. On `Some(row)` wrap as
1095    ///   `Some((row, __IterCursor(handle)))`; on `None` propagate. Handle
1096    ///   stays stable across calls — state is server-side / mpsc-buffered.
1097    /// - `__IterEager(list, idx)` → existing positional cursor.
1098    fn emit_iter_next(&mut self, args: &[a::CExpr]) {
1099        self.compile_expr(&args[0], false);
1100        let it = self.alloc_local("__in_it");
1101        self.emit(Op::StoreLocal(it));
1102
1103        // Dispatch: TestVariant pops; we Dup to keep the iter around.
1104        self.emit(Op::LoadLocal(it));
1105        self.emit(Op::Dup);
1106        let lazy_name = self.pool.variant("__IterLazy");
1107        self.emit(Op::TestVariant(lazy_name));
1108        let j_to_check_cursor = self.code.len();
1109        self.emit(Op::JumpIfNot(0));
1110
1111        // ── lazy path ────────────────────────────────────────────────
1112        // The Dup'd iter is on stack but we've consumed it via TestVariant,
1113        // so reload from the local.
1114        self.emit(Op::LoadLocal(it));
1115        self.emit(Op::GetVariantArg(0)); // seed
1116        let seed = self.alloc_local("__in_seed");
1117        self.emit(Op::StoreLocal(seed));
1118
1119        self.emit(Op::LoadLocal(it));
1120        self.emit(Op::GetVariantArg(1)); // step closure
1121        let step = self.alloc_local("__in_step");
1122        self.emit(Op::StoreLocal(step));
1123
1124        // Call step(seed) → Option[(T, S)].
1125        let nid_lazy = self.pool.node_id("n_iter_next_lazy");
1126        self.emit(Op::LoadLocal(step));
1127        self.emit(Op::LoadLocal(seed));
1128        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid_lazy });
1129        let opt = self.alloc_local("__in_opt");
1130        self.emit(Op::StoreLocal(opt));
1131
1132        // If `step` returned None, propagate it directly.
1133        self.emit(Op::LoadLocal(opt));
1134        let some_name = self.pool.variant("Some");
1135        self.emit(Op::TestVariant(some_name));
1136        let j_lazy_none = self.code.len();
1137        self.emit(Op::JumpIfNot(0));
1138
1139        // Some((t, new_seed)) — extract the inner tuple, repackage as
1140        // Some((t, __IterLazy(new_seed, step))) so the next call advances.
1141        self.emit(Op::LoadLocal(opt));
1142        self.emit(Op::GetVariantArg(0));     // (t, new_seed)
1143        let pair = self.alloc_local("__in_pair");
1144        self.emit(Op::StoreLocal(pair));
1145
1146        self.emit(Op::LoadLocal(pair));
1147        self.emit(Op::GetElem(0));           // t
1148        self.emit(Op::LoadLocal(pair));
1149        self.emit(Op::GetElem(1));           // new_seed
1150        self.emit(Op::LoadLocal(step));      // step closure
1151        let lazy_v = self.pool.variant("__IterLazy");
1152        self.emit(Op::MakeVariant { name_idx: lazy_v, arity: 2 }); // __IterLazy(new_seed, step)
1153        self.emit(Op::MakeTuple(2));         // (t, new_iter)
1154        let some_v = self.pool.variant("Some");
1155        self.emit(Op::MakeVariant { name_idx: some_v, arity: 1 });
1156        let j_after_lazy = self.code.len();
1157        self.emit(Op::Jump(0));
1158
1159        // Lazy → None: just forward the None.
1160        let none_t = self.code.len() as i32;
1161        if let Op::JumpIfNot(off) = &mut self.code[j_lazy_none] {
1162            *off = none_t - (j_lazy_none as i32 + 1);
1163        }
1164        let none_v = self.pool.variant("None");
1165        self.emit(Op::MakeVariant { name_idx: none_v, arity: 0 });
1166        let j_after_lazy_none = self.code.len();
1167        self.emit(Op::Jump(0));
1168
1169        // ── cursor path (#379) ───────────────────────────────────────
1170        let cursor_check_t = self.code.len() as i32;
1171        if let Op::JumpIfNot(off) = &mut self.code[j_to_check_cursor] {
1172            *off = cursor_check_t - (j_to_check_cursor as i32 + 1);
1173        }
1174
1175        self.emit(Op::LoadLocal(it));
1176        self.emit(Op::Dup);
1177        let cursor_name = self.pool.variant("__IterCursor");
1178        self.emit(Op::TestVariant(cursor_name));
1179        let j_to_eager = self.code.len();
1180        self.emit(Op::JumpIfNot(0));
1181
1182        // Cursor path: extract handle, effect-call sql.cursor_next(handle).
1183        // The handler returns Option[T] directly. We then wrap as
1184        // Some((T, __IterCursor(handle))) or forward None.
1185        self.emit(Op::LoadLocal(it));
1186        self.emit(Op::GetVariantArg(0));     // handle
1187        let handle = self.alloc_local("__in_handle");
1188        self.emit(Op::StoreLocal(handle));
1189
1190        let kind_idx = self.pool.str("sql");
1191        let op_idx = self.pool.str("cursor_next");
1192        let nid_cursor = self.pool.node_id("n_iter_next_cursor");
1193        self.emit(Op::LoadLocal(handle));
1194        self.emit(Op::EffectCall {
1195            kind_idx,
1196            op_idx,
1197            arity: 1,
1198            node_id_idx: nid_cursor,
1199        });
1200        let cur_opt = self.alloc_local("__in_cur_opt");
1201        self.emit(Op::StoreLocal(cur_opt));
1202
1203        self.emit(Op::LoadLocal(cur_opt));
1204        let some_c = self.pool.variant("Some");
1205        self.emit(Op::TestVariant(some_c));
1206        let j_cursor_none = self.code.len();
1207        self.emit(Op::JumpIfNot(0));
1208
1209        // Some(row): build Some((row, __IterCursor(handle)))
1210        self.emit(Op::LoadLocal(cur_opt));
1211        self.emit(Op::GetVariantArg(0));     // row
1212        self.emit(Op::LoadLocal(handle));
1213        let cursor_v = self.pool.variant("__IterCursor");
1214        self.emit(Op::MakeVariant { name_idx: cursor_v, arity: 1 });
1215        self.emit(Op::MakeTuple(2));         // (row, __IterCursor(handle))
1216        let some_c2 = self.pool.variant("Some");
1217        self.emit(Op::MakeVariant { name_idx: some_c2, arity: 1 });
1218        let j_after_cursor = self.code.len();
1219        self.emit(Op::Jump(0));
1220
1221        // Cursor → None
1222        let cursor_none_t = self.code.len() as i32;
1223        if let Op::JumpIfNot(off) = &mut self.code[j_cursor_none] {
1224            *off = cursor_none_t - (j_cursor_none as i32 + 1);
1225        }
1226        let none_c = self.pool.variant("None");
1227        self.emit(Op::MakeVariant { name_idx: none_c, arity: 0 });
1228        let j_after_cursor_none = self.code.len();
1229        self.emit(Op::Jump(0));
1230
1231        // ── eager path ───────────────────────────────────────────────
1232        let eager_t = self.code.len() as i32;
1233        if let Op::JumpIfNot(off) = &mut self.code[j_to_eager] {
1234            *off = eager_t - (j_to_eager as i32 + 1);
1235        }
1236
1237        self.emit(Op::LoadLocal(it));
1238        self.emit(Op::GetVariantArg(0));
1239        let list = self.alloc_local("__in_list");
1240        self.emit(Op::StoreLocal(list));
1241
1242        self.emit(Op::LoadLocal(it));
1243        self.emit(Op::GetVariantArg(1));
1244        let idx = self.alloc_local("__in_idx");
1245        self.emit(Op::StoreLocal(idx));
1246
1247        // if idx < len(list)
1248        self.emit(Op::LoadLocal(idx));
1249        self.emit(Op::LoadLocal(list));
1250        self.emit(Op::GetListLen);
1251        self.emit(Op::IntLt);
1252        let j_eager_else = self.code.len();
1253        self.emit(Op::JumpIfNot(0));
1254
1255        // Some((item, __IterEager(list, idx+1)))
1256        self.emit(Op::LoadLocal(list));
1257        self.emit(Op::LoadLocal(idx));
1258        self.emit(Op::GetListElemDyn);
1259
1260        self.emit(Op::LoadLocal(list));
1261        self.emit(Op::LoadLocal(idx));
1262        let one = self.pool.int(1);
1263        self.emit(Op::PushConst(one));
1264        self.emit(Op::IntAdd);
1265        let eager_v = self.pool.variant("__IterEager");
1266        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1267        self.emit(Op::MakeTuple(2));
1268        let some_e = self.pool.variant("Some");
1269        self.emit(Op::MakeVariant { name_idx: some_e, arity: 1 });
1270        let j_after_eager = self.code.len();
1271        self.emit(Op::Jump(0));
1272
1273        // Eager → None
1274        let eager_none_t = self.code.len() as i32;
1275        if let Op::JumpIfNot(off) = &mut self.code[j_eager_else] {
1276            *off = eager_none_t - (j_eager_else as i32 + 1);
1277        }
1278        let none_e = self.pool.variant("None");
1279        self.emit(Op::MakeVariant { name_idx: none_e, arity: 0 });
1280
1281        // Converge all paths.
1282        let end = self.code.len() as i32;
1283        if let Op::Jump(off) = &mut self.code[j_after_lazy] {
1284            *off = end - (j_after_lazy as i32 + 1);
1285        }
1286        if let Op::Jump(off) = &mut self.code[j_after_lazy_none] {
1287            *off = end - (j_after_lazy_none as i32 + 1);
1288        }
1289        if let Op::Jump(off) = &mut self.code[j_after_cursor] {
1290            *off = end - (j_after_cursor as i32 + 1);
1291        }
1292        if let Op::Jump(off) = &mut self.code[j_after_cursor_none] {
1293            *off = end - (j_after_cursor_none as i32 + 1);
1294        }
1295        if let Op::Jump(off) = &mut self.code[j_after_eager] {
1296            *off = end - (j_after_eager as i32 + 1);
1297        }
1298    }
1299
1300    /// `iter.unfold(seed, step)` — lazy iterator that calls `step(seed)` on
1301    /// each `iter.next` and threads the new seed forward. Internal value
1302    /// shape: `__IterLazy(seed, step)`. Step has type `(S) -> Option[(T, S)]`;
1303    /// returning `None` ends the iteration (#376).
1304    fn emit_iter_unfold(&mut self, args: &[a::CExpr]) {
1305        self.compile_expr(&args[0], false); // seed
1306        self.compile_expr(&args[1], false); // step
1307        let lazy = self.pool.variant("__IterLazy");
1308        self.emit(Op::MakeVariant { name_idx: lazy, arity: 2 });
1309    }
1310
1311    /// `iter.is_empty(it)` — true iff no further element. v1 supports the
1312    /// eager form O(1); on a lazy iter the seed sits in slot 0 and is not a
1313    /// List, so the VM trips on `GetListLen` rather than returning a wrong
1314    /// answer. Callers needing lazy support should materialize with
1315    /// `iter.to_list` first or call `iter.next` and pattern-match.
1316    fn emit_iter_is_empty(&mut self, args: &[a::CExpr]) {
1317        self.compile_expr(&args[0], false);
1318        let it = self.alloc_local("__ie_it");
1319        self.emit(Op::StoreLocal(it));
1320
1321        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1)); // idx
1322        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0)); // list
1323        self.emit(Op::GetListLen);                                     // len
1324        self.emit(Op::IntLt);                                          // idx < len
1325        self.emit(Op::BoolNot);                                        // NOT(idx < len)
1326    }
1327
1328    /// `iter.count(it)` — number of remaining elements (v1: eager-only).
1329    fn emit_iter_count(&mut self, args: &[a::CExpr]) {
1330        self.compile_expr(&args[0], false);
1331        let it = self.alloc_local("__ic_it");
1332        self.emit(Op::StoreLocal(it));
1333
1334        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1335        self.emit(Op::GetListLen);                                     // len
1336        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1)); // idx
1337        self.emit(Op::IntSub);                                         // len - idx
1338    }
1339
1340    /// `iter.take(it, n)` — collect up to n elements, return as new Iter.
1341    fn emit_iter_take(&mut self, args: &[a::CExpr]) {
1342        self.compile_expr(&args[0], false);
1343        let it   = self.alloc_local("__itk_it");
1344        self.emit(Op::StoreLocal(it));
1345
1346        self.compile_expr(&args[1], false);
1347        let n    = self.alloc_local("__itk_n");
1348        self.emit(Op::StoreLocal(n));
1349
1350        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1351        let list = self.alloc_local("__itk_list");
1352        self.emit(Op::StoreLocal(list));
1353
1354        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1355        let i    = self.alloc_local("__itk_i");
1356        self.emit(Op::StoreLocal(i));
1357
1358        self.emit(Op::MakeList(0));
1359        let out  = self.alloc_local("__itk_out");
1360        self.emit(Op::StoreLocal(out));
1361
1362        let zero = self.pool.int(0);
1363        self.emit(Op::PushConst(zero));
1364        let cnt  = self.alloc_local("__itk_cnt");
1365        self.emit(Op::StoreLocal(cnt));
1366
1367        let loop_top = self.code.len();
1368
1369        // while cnt < n
1370        self.emit(Op::LoadLocal(cnt));
1371        self.emit(Op::LoadLocal(n));
1372        self.emit(Op::IntLt);
1373        let j_exit_n = self.code.len();
1374        self.emit(Op::JumpIfNot(0));
1375
1376        // AND i < len(list)
1377        self.emit(Op::LoadLocal(i));
1378        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1379        self.emit(Op::IntLt);
1380        let j_exit_l = self.code.len();
1381        self.emit(Op::JumpIfNot(0));
1382
1383        // out = out ++ [list[i]]
1384        self.emit(Op::LoadLocal(out));
1385        self.emit(Op::LoadLocal(list));
1386        self.emit(Op::LoadLocal(i));
1387        self.emit(Op::GetListElemDyn);
1388        self.emit(Op::ListAppend);
1389        self.emit(Op::StoreLocal(out));
1390
1391        let one = self.pool.int(1);
1392        // i = i + 1
1393        self.emit(Op::LoadLocal(i));
1394        self.emit(Op::PushConst(one));
1395        self.emit(Op::IntAdd);
1396        self.emit(Op::StoreLocal(i));
1397        // cnt = cnt + 1
1398        self.emit(Op::LoadLocal(cnt));
1399        self.emit(Op::PushConst(one));
1400        self.emit(Op::IntAdd);
1401        self.emit(Op::StoreLocal(cnt));
1402
1403        let jback = self.code.len();
1404        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1405
1406        let exit_t = self.code.len() as i32;
1407        if let Op::JumpIfNot(off) = &mut self.code[j_exit_n] { *off = exit_t - (j_exit_n as i32 + 1); }
1408        if let Op::JumpIfNot(off) = &mut self.code[j_exit_l] { *off = exit_t - (j_exit_l as i32 + 1); }
1409
1410        // return new __IterEager(out, 0)
1411        self.emit(Op::LoadLocal(out));
1412        self.emit(Op::PushConst(zero));
1413        let eager_v = self.pool.variant("__IterEager");
1414        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1415    }
1416
1417    /// `iter.skip(it, n)` — advance cursor by n (or to end), return new Iter.
1418    fn emit_iter_skip(&mut self, args: &[a::CExpr]) {
1419        self.compile_expr(&args[0], false);
1420        let it   = self.alloc_local("__isk_it");
1421        self.emit(Op::StoreLocal(it));
1422
1423        self.compile_expr(&args[1], false);
1424        let n    = self.alloc_local("__isk_n");
1425        self.emit(Op::StoreLocal(n));
1426
1427        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1428        let list = self.alloc_local("__isk_list");
1429        self.emit(Op::StoreLocal(list));
1430
1431        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1432        let idx  = self.alloc_local("__isk_idx");
1433        self.emit(Op::StoreLocal(idx));
1434
1435        // raw = idx + n
1436        self.emit(Op::LoadLocal(idx));
1437        self.emit(Op::LoadLocal(n));
1438        self.emit(Op::IntAdd);
1439        let raw  = self.alloc_local("__isk_raw");
1440        self.emit(Op::StoreLocal(raw));
1441
1442        // new_idx = if raw < len then raw else len
1443        self.emit(Op::LoadLocal(raw));
1444        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1445        self.emit(Op::IntLt);
1446        let j_use_raw = self.code.len();
1447        self.emit(Op::JumpIf(0));
1448
1449        // use len
1450        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1451        let j_end = self.code.len();
1452        self.emit(Op::Jump(0));
1453
1454        // use raw
1455        let raw_t = self.code.len() as i32;
1456        if let Op::JumpIf(off) = &mut self.code[j_use_raw] { *off = raw_t - (j_use_raw as i32 + 1); }
1457        self.emit(Op::LoadLocal(raw));
1458
1459        let end_t = self.code.len() as i32;
1460        if let Op::Jump(off) = &mut self.code[j_end] { *off = end_t - (j_end as i32 + 1); }
1461
1462        // new_idx on stack; build new __IterEager(list, new_idx)
1463        let new_idx = self.alloc_local("__isk_ni");
1464        self.emit(Op::StoreLocal(new_idx));
1465        self.emit(Op::LoadLocal(list));
1466        self.emit(Op::LoadLocal(new_idx));
1467        let eager_v = self.pool.variant("__IterEager");
1468        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1469    }
1470
1471    /// `iter.to_list(it)` — materialise remaining elements into a List.
1472    ///
1473    /// Dispatches on the iter variant (#376):
1474    /// - `__IterLazy`: repeatedly call `step(seed)`; on `Some((t, s'))` append
1475    ///   `t` and continue with `s'`; on `None` stop. May hang on truly
1476    ///   infinite producers — that's documented as a v1 limitation, the
1477    ///   step-limit-protected caller is what catches misuse.
1478    /// - `__IterEager`: slice the backing list from `idx` onward (O(n) walk).
1479    fn emit_iter_to_list(&mut self, args: &[a::CExpr]) {
1480        self.compile_expr(&args[0], false);
1481        let it = self.alloc_local("__itl_it");
1482        self.emit(Op::StoreLocal(it));
1483
1484        // Build the output list up-front, shared across both paths.
1485        self.emit(Op::MakeList(0));
1486        let out = self.alloc_local("__itl_out");
1487        self.emit(Op::StoreLocal(out));
1488
1489        // Dispatch on variant tag.
1490        self.emit(Op::LoadLocal(it));
1491        let lazy_name = self.pool.variant("__IterLazy");
1492        self.emit(Op::TestVariant(lazy_name));
1493        let j_to_eager = self.code.len();
1494        self.emit(Op::JumpIfNot(0));
1495
1496        // ── lazy path ─────────────────────────────────────────────────
1497        // seed and step closure live in locals; we update seed each iteration.
1498        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1499        let seed = self.alloc_local("__itl_seed");
1500        self.emit(Op::StoreLocal(seed));
1501
1502        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1503        let step = self.alloc_local("__itl_step");
1504        self.emit(Op::StoreLocal(step));
1505
1506        let lazy_loop = self.code.len();
1507        let nid_lazy = self.pool.node_id("n_iter_to_list_lazy");
1508        self.emit(Op::LoadLocal(step));
1509        self.emit(Op::LoadLocal(seed));
1510        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid_lazy });
1511        let opt = self.alloc_local("__itl_opt");
1512        self.emit(Op::StoreLocal(opt));
1513
1514        // If None, drop out of the lazy loop.
1515        self.emit(Op::LoadLocal(opt));
1516        let some_name = self.pool.variant("Some");
1517        self.emit(Op::TestVariant(some_name));
1518        let j_lazy_exit = self.code.len();
1519        self.emit(Op::JumpIfNot(0));
1520
1521        // Some((t, new_seed)): append t to out, replace seed.
1522        self.emit(Op::LoadLocal(opt));
1523        self.emit(Op::GetVariantArg(0));
1524        let pair = self.alloc_local("__itl_pair");
1525        self.emit(Op::StoreLocal(pair));
1526
1527        self.emit(Op::LoadLocal(out));
1528        self.emit(Op::LoadLocal(pair)); self.emit(Op::GetElem(0));
1529        self.emit(Op::ListAppend);
1530        self.emit(Op::StoreLocal(out));
1531
1532        self.emit(Op::LoadLocal(pair)); self.emit(Op::GetElem(1));
1533        self.emit(Op::StoreLocal(seed));
1534
1535        let jback_lazy = self.code.len();
1536        self.emit(Op::Jump((lazy_loop as i32) - (jback_lazy as i32 + 1)));
1537
1538        let lazy_exit_t = self.code.len() as i32;
1539        if let Op::JumpIfNot(off) = &mut self.code[j_lazy_exit] {
1540            *off = lazy_exit_t - (j_lazy_exit as i32 + 1);
1541        }
1542        let j_after_lazy = self.code.len();
1543        self.emit(Op::Jump(0));
1544
1545        // ── eager path ────────────────────────────────────────────────
1546        let eager_t = self.code.len() as i32;
1547        if let Op::JumpIfNot(off) = &mut self.code[j_to_eager] {
1548            *off = eager_t - (j_to_eager as i32 + 1);
1549        }
1550
1551        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1552        let list = self.alloc_local("__itl_list");
1553        self.emit(Op::StoreLocal(list));
1554
1555        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1556        let i = self.alloc_local("__itl_i");
1557        self.emit(Op::StoreLocal(i));
1558
1559        let loop_top = self.code.len();
1560        self.emit(Op::LoadLocal(i));
1561        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1562        self.emit(Op::IntLt);
1563        let j_exit = self.code.len();
1564        self.emit(Op::JumpIfNot(0));
1565
1566        self.emit(Op::LoadLocal(out));
1567        self.emit(Op::LoadLocal(list));
1568        self.emit(Op::LoadLocal(i));
1569        self.emit(Op::GetListElemDyn);
1570        self.emit(Op::ListAppend);
1571        self.emit(Op::StoreLocal(out));
1572
1573        self.emit(Op::LoadLocal(i));
1574        let one = self.pool.int(1);
1575        self.emit(Op::PushConst(one));
1576        self.emit(Op::IntAdd);
1577        self.emit(Op::StoreLocal(i));
1578
1579        let jback = self.code.len();
1580        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1581
1582        let exit_t = self.code.len() as i32;
1583        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
1584            *off = exit_t - (j_exit as i32 + 1);
1585        }
1586
1587        // Converge: lazy path falls through here too.
1588        let converge = self.code.len() as i32;
1589        if let Op::Jump(off) = &mut self.code[j_after_lazy] {
1590            *off = converge - (j_after_lazy as i32 + 1);
1591        }
1592        self.emit(Op::LoadLocal(out));
1593    }
1594
1595    /// `iter.map(it, f)` — apply `f` to each remaining element; returns new Iter.
1596    fn emit_iter_map(&mut self, args: &[a::CExpr]) {
1597        self.compile_expr(&args[0], false);
1598        let it   = self.alloc_local("__im_it");
1599        self.emit(Op::StoreLocal(it));
1600
1601        self.compile_expr(&args[1], false);
1602        let f    = self.alloc_local("__im_f");
1603        self.emit(Op::StoreLocal(f));
1604
1605        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1606        let list = self.alloc_local("__im_list");
1607        self.emit(Op::StoreLocal(list));
1608
1609        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1610        let i    = self.alloc_local("__im_i");
1611        self.emit(Op::StoreLocal(i));
1612
1613        self.emit(Op::MakeList(0));
1614        let out  = self.alloc_local("__im_out");
1615        self.emit(Op::StoreLocal(out));
1616
1617        let loop_top = self.code.len();
1618        self.emit(Op::LoadLocal(i));
1619        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1620        self.emit(Op::IntLt);
1621        let j_exit = self.code.len();
1622        self.emit(Op::JumpIfNot(0));
1623
1624        let nid = self.pool.node_id("n_iter_map");
1625        self.emit(Op::LoadLocal(out));
1626        self.emit(Op::LoadLocal(f));
1627        self.emit(Op::LoadLocal(list));
1628        self.emit(Op::LoadLocal(i));
1629        self.emit(Op::GetListElemDyn);
1630        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1631        self.emit(Op::ListAppend);
1632        self.emit(Op::StoreLocal(out));
1633
1634        self.emit(Op::LoadLocal(i));
1635        let one = self.pool.int(1);
1636        self.emit(Op::PushConst(one));
1637        self.emit(Op::IntAdd);
1638        self.emit(Op::StoreLocal(i));
1639
1640        let jback = self.code.len();
1641        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1642
1643        let exit_t = self.code.len() as i32;
1644        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1645
1646        let zero = self.pool.int(0);
1647        self.emit(Op::LoadLocal(out));
1648        self.emit(Op::PushConst(zero));
1649        let eager_v = self.pool.variant("__IterEager");
1650        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1651    }
1652
1653    /// `iter.filter(it, pred)` — keep elements where pred is true; returns new Iter.
1654    fn emit_iter_filter(&mut self, args: &[a::CExpr]) {
1655        self.compile_expr(&args[0], false);
1656        let it   = self.alloc_local("__if_it");
1657        self.emit(Op::StoreLocal(it));
1658
1659        self.compile_expr(&args[1], false);
1660        let f    = self.alloc_local("__if_f");
1661        self.emit(Op::StoreLocal(f));
1662
1663        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1664        let list = self.alloc_local("__if_list");
1665        self.emit(Op::StoreLocal(list));
1666
1667        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1668        let i    = self.alloc_local("__if_i");
1669        self.emit(Op::StoreLocal(i));
1670
1671        self.emit(Op::MakeList(0));
1672        let out  = self.alloc_local("__if_out");
1673        self.emit(Op::StoreLocal(out));
1674
1675        let loop_top = self.code.len();
1676        self.emit(Op::LoadLocal(i));
1677        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1678        self.emit(Op::IntLt);
1679        let j_exit = self.code.len();
1680        self.emit(Op::JumpIfNot(0));
1681
1682        // elem := list[i]
1683        self.emit(Op::LoadLocal(list));
1684        self.emit(Op::LoadLocal(i));
1685        self.emit(Op::GetListElemDyn);
1686        let x    = self.alloc_local("__if_x");
1687        self.emit(Op::StoreLocal(x));
1688
1689        let nid = self.pool.node_id("n_iter_filter");
1690        self.emit(Op::LoadLocal(f));
1691        self.emit(Op::LoadLocal(x));
1692        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1693        let j_skip = self.code.len();
1694        self.emit(Op::JumpIfNot(0));
1695
1696        self.emit(Op::LoadLocal(out));
1697        self.emit(Op::LoadLocal(x));
1698        self.emit(Op::ListAppend);
1699        self.emit(Op::StoreLocal(out));
1700
1701        let skip_t = self.code.len() as i32;
1702        if let Op::JumpIfNot(off) = &mut self.code[j_skip] { *off = skip_t - (j_skip as i32 + 1); }
1703
1704        self.emit(Op::LoadLocal(i));
1705        let one = self.pool.int(1);
1706        self.emit(Op::PushConst(one));
1707        self.emit(Op::IntAdd);
1708        self.emit(Op::StoreLocal(i));
1709
1710        let jback = self.code.len();
1711        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1712
1713        let exit_t = self.code.len() as i32;
1714        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1715
1716        let zero = self.pool.int(0);
1717        self.emit(Op::LoadLocal(out));
1718        self.emit(Op::PushConst(zero));
1719        let eager_v = self.pool.variant("__IterEager");
1720        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1721    }
1722
1723    /// `iter.fold(it, init, f)` — left fold over remaining elements.
1724    fn emit_iter_fold(&mut self, args: &[a::CExpr]) {
1725        self.compile_expr(&args[0], false);
1726        let it   = self.alloc_local("__ifo_it");
1727        self.emit(Op::StoreLocal(it));
1728
1729        self.compile_expr(&args[1], false);
1730        let acc  = self.alloc_local("__ifo_acc");
1731        self.emit(Op::StoreLocal(acc));
1732
1733        self.compile_expr(&args[2], false);
1734        let f    = self.alloc_local("__ifo_f");
1735        self.emit(Op::StoreLocal(f));
1736
1737        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1738        let list = self.alloc_local("__ifo_list");
1739        self.emit(Op::StoreLocal(list));
1740
1741        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1742        let i    = self.alloc_local("__ifo_i");
1743        self.emit(Op::StoreLocal(i));
1744
1745        let loop_top = self.code.len();
1746        self.emit(Op::LoadLocal(i));
1747        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1748        self.emit(Op::IntLt);
1749        let j_exit = self.code.len();
1750        self.emit(Op::JumpIfNot(0));
1751
1752        let nid = self.pool.node_id("n_iter_fold");
1753        self.emit(Op::LoadLocal(f));
1754        self.emit(Op::LoadLocal(acc));
1755        self.emit(Op::LoadLocal(list));
1756        self.emit(Op::LoadLocal(i));
1757        self.emit(Op::GetListElemDyn);
1758        self.emit(Op::CallClosure { arity: 2, node_id_idx: nid });
1759        self.emit(Op::StoreLocal(acc));
1760
1761        self.emit(Op::LoadLocal(i));
1762        let one = self.pool.int(1);
1763        self.emit(Op::PushConst(one));
1764        self.emit(Op::IntAdd);
1765        self.emit(Op::StoreLocal(i));
1766
1767        let jback = self.code.len();
1768        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1769
1770        let exit_t = self.code.len() as i32;
1771        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1772        self.emit(Op::LoadLocal(acc));
1773    }
1774
1775    /// `map.fold(m, init, f)` — left fold over `Map[K, V]` entries with a
1776    /// three-arg combiner `f(acc, k, v)`. Iteration order matches
1777    /// `map.entries` (BTreeMap-sorted by key). Materializes the entry
1778    /// list once via the runtime's `("map", "entries")` op, then runs
1779    /// the same inline loop as `list.fold`.
1780    fn emit_map_fold(&mut self, args: &[a::CExpr], node_id_idx: u32) {
1781        // xs := map.entries(m)
1782        self.compile_expr(&args[0], false);
1783        let map_kind = self.pool.str("map");
1784        let entries_op = self.pool.str("entries");
1785        self.emit(Op::EffectCall {
1786            kind_idx: map_kind,
1787            op_idx: entries_op,
1788            arity: 1,
1789            node_id_idx,
1790        });
1791        let xs = self.alloc_local("__mf_xs");
1792        self.emit(Op::StoreLocal(xs));
1793
1794        // acc := init
1795        self.compile_expr(&args[1], false);
1796        let acc = self.alloc_local("__mf_acc");
1797        self.emit(Op::StoreLocal(acc));
1798
1799        // f := <closure>
1800        self.compile_expr(&args[2], false);
1801        let f = self.alloc_local("__mf_f");
1802        self.emit(Op::StoreLocal(f));
1803
1804        // i := 0
1805        let zero = self.pool.int(0);
1806        self.emit(Op::PushConst(zero));
1807        let i = self.alloc_local("__mf_i");
1808        self.emit(Op::StoreLocal(i));
1809
1810        // loop_top: while i < len(xs)
1811        let loop_top = self.code.len();
1812        self.emit(Op::LoadLocal(i));
1813        self.emit(Op::LoadLocal(xs));
1814        self.emit(Op::GetListLen);
1815        self.emit(Op::IntLt);
1816        let j_exit = self.code.len();
1817        self.emit(Op::JumpIfNot(0));
1818
1819        // pair := xs[i]
1820        self.emit(Op::LoadLocal(xs));
1821        self.emit(Op::LoadLocal(i));
1822        self.emit(Op::GetListElemDyn);
1823        let pair = self.alloc_local("__mf_pair");
1824        self.emit(Op::StoreLocal(pair));
1825
1826        // acc := f(acc, pair.0, pair.1)
1827        let nid = self.pool.node_id("n_map_fold");
1828        self.emit(Op::LoadLocal(f));
1829        self.emit(Op::LoadLocal(acc));
1830        self.emit(Op::LoadLocal(pair));
1831        self.emit(Op::GetElem(0));
1832        self.emit(Op::LoadLocal(pair));
1833        self.emit(Op::GetElem(1));
1834        self.emit(Op::CallClosure { arity: 3, node_id_idx: nid });
1835        self.emit(Op::StoreLocal(acc));
1836
1837        // i := i + 1
1838        self.emit(Op::LoadLocal(i));
1839        let one = self.pool.int(1);
1840        self.emit(Op::PushConst(one));
1841        self.emit(Op::IntAdd);
1842        self.emit(Op::StoreLocal(i));
1843
1844        let jump_back = self.code.len();
1845        let back = (loop_top as i32) - (jump_back as i32 + 1);
1846        self.emit(Op::Jump(back));
1847
1848        let exit_target = self.code.len() as i32;
1849        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
1850            *off = exit_target - (j_exit as i32 + 1);
1851        }
1852        self.emit(Op::LoadLocal(acc));
1853    }
1854
1855    /// Inline pattern: `<module>.map(v, f)` and friends.
1856    /// `wrap_with`: variant tag whose payload triggers the call (Ok / Some / Err).
1857    /// `wrap_result`: if true, wrap the closure's result back in `wrap_with`
1858    /// (map shape); if false, expect the closure to return a wrapped value
1859    /// itself (and_then shape).
1860    fn emit_variant_map(
1861        &mut self,
1862        args: &[a::CExpr],
1863        wrap_with: &str,
1864        wrap_result: bool,
1865    ) {
1866        // args[0] = the wrapped value (Result/Option), args[1] = closure
1867        let wrap_idx = self.pool.variant(wrap_with);
1868
1869        // Compile and store the value into a local, evaluate closure on top of stack.
1870        self.compile_expr(&args[0], false);
1871        let val_slot = self.alloc_local("__hov");
1872        self.emit(Op::StoreLocal(val_slot));
1873
1874        self.compile_expr(&args[1], false);
1875        let f_slot = self.alloc_local("__hof");
1876        self.emit(Op::StoreLocal(f_slot));
1877
1878        // Stack discipline:
1879        //   load val ⇒ [v]
1880        //   dup     ⇒ [v, v]
1881        //   test    ⇒ [v, Bool]
1882        //   jumpifnot ⇒ [v]
1883        // Both branches end with [v] before the branch body.
1884        self.emit(Op::LoadLocal(val_slot));
1885        self.emit(Op::Dup);
1886        self.emit(Op::TestVariant(wrap_idx));
1887        let j_skip = self.code.len();
1888        self.emit(Op::JumpIfNot(0));
1889
1890        // Matched arm: extract payload, call closure on it.
1891        self.emit(Op::GetVariantArg(0));
1892        let arg_slot = self.alloc_local("__hov_arg");
1893        self.emit(Op::StoreLocal(arg_slot));
1894        self.emit(Op::LoadLocal(f_slot));
1895        self.emit(Op::LoadLocal(arg_slot));
1896        let nid = self.pool.node_id("n_hov");
1897        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1898        if wrap_result {
1899            self.emit(Op::MakeVariant { name_idx: wrap_idx, arity: 1 });
1900        }
1901        let j_end = self.code.len();
1902        self.emit(Op::Jump(0));
1903
1904        // Skip arm: stack already has [v] from the failed Dup; nothing to do.
1905        let skip_target = self.code.len() as i32;
1906        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
1907            *off = skip_target - (j_skip as i32 + 1);
1908        }
1909
1910        let end_target = self.code.len() as i32;
1911        if let Op::Jump(off) = &mut self.code[j_end] {
1912            *off = end_target - (j_end as i32 + 1);
1913        }
1914    }
1915
1916    /// Sibling of `emit_variant_map` for the recovery combinators
1917    /// `result.or_else` and `option.or_else`. Differences from
1918    /// `emit_variant_map`:
1919    ///   - matches on the *negative* variant (`Err` / `None`)
1920    ///   - the closure's result becomes the call's result directly,
1921    ///     with no wrapping (it is itself a `Result` / `Option`)
1922    ///   - `option.or_else`'s closure takes zero args (`None` has no
1923    ///     payload to forward)
1924    fn emit_variant_or_else(
1925        &mut self,
1926        args: &[a::CExpr],
1927        match_on: &str,
1928        closure_arity: u16,
1929    ) {
1930        let match_idx = self.pool.variant(match_on);
1931
1932        self.compile_expr(&args[0], false);
1933        let val_slot = self.alloc_local("__hoe");
1934        self.emit(Op::StoreLocal(val_slot));
1935
1936        self.compile_expr(&args[1], false);
1937        let f_slot = self.alloc_local("__hoe_f");
1938        self.emit(Op::StoreLocal(f_slot));
1939
1940        // Stack discipline mirrors emit_variant_map:
1941        //   load val      ⇒ [v]
1942        //   dup           ⇒ [v, v]
1943        //   test          ⇒ [v, Bool]
1944        //   jumpifnot     ⇒ [v]
1945        // The unmatched arm leaves [v] (Ok/Some unchanged); the
1946        // matched arm pops [v] and pushes the closure's result.
1947        self.emit(Op::LoadLocal(val_slot));
1948        self.emit(Op::Dup);
1949        self.emit(Op::TestVariant(match_idx));
1950        let j_skip = self.code.len();
1951        self.emit(Op::JumpIfNot(0));
1952
1953        // Matched arm: pop the duplicate left on the stack,
1954        // then call the closure with whatever payload it expects.
1955        self.emit(Op::Pop);
1956        self.emit(Op::LoadLocal(f_slot));
1957        if closure_arity == 1 {
1958            self.emit(Op::LoadLocal(val_slot));
1959            self.emit(Op::GetVariantArg(0));
1960        }
1961        let nid = self.pool.node_id("n_hoe");
1962        self.emit(Op::CallClosure { arity: closure_arity, node_id_idx: nid });
1963
1964        let j_end = self.code.len();
1965        self.emit(Op::Jump(0));
1966
1967        // Unmatched arm: stack already holds [v]; nothing to do.
1968        let skip_target = self.code.len() as i32;
1969        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
1970            *off = skip_target - (j_skip as i32 + 1);
1971        }
1972
1973        let end_target = self.code.len() as i32;
1974        if let Op::Jump(off) = &mut self.code[j_end] {
1975            *off = end_target - (j_end as i32 + 1);
1976        }
1977    }
1978
1979    /// `option.unwrap_or_else(opt, f)` — lazy default via zero-arg thunk.
1980    ///   Some(x) → x          (unwrap; no wrapping)
1981    ///   None    → f()        (call thunk; return its result directly)
1982    fn emit_option_unwrap_or_else(&mut self, args: &[a::CExpr]) {
1983        let some_idx = self.pool.variant("Some");
1984
1985        // Compile opt and f; stash both so they're accessible on both arms.
1986        self.compile_expr(&args[0], false);
1987        let val_slot = self.alloc_local("__uoe_val");
1988        self.emit(Op::StoreLocal(val_slot));
1989
1990        self.compile_expr(&args[1], false);
1991        let f_slot = self.alloc_local("__uoe_f");
1992        self.emit(Op::StoreLocal(f_slot));
1993
1994        // Test whether opt is Some.
1995        //   load val ⇒ [v]
1996        //   dup      ⇒ [v, v]
1997        //   test     ⇒ [v, Bool]
1998        //   jumpifnot → None arm
1999        self.emit(Op::LoadLocal(val_slot));
2000        self.emit(Op::Dup);
2001        self.emit(Op::TestVariant(some_idx));
2002        let j_none = self.code.len();
2003        self.emit(Op::JumpIfNot(0));
2004
2005        // Some arm: extract the payload from [v] left on the stack.
2006        self.emit(Op::GetVariantArg(0));
2007        let j_end = self.code.len();
2008        self.emit(Op::Jump(0));
2009
2010        // None arm: pop the [v] duplicate, call the thunk.
2011        let none_target = self.code.len() as i32;
2012        if let Op::JumpIfNot(off) = &mut self.code[j_none] {
2013            *off = none_target - (j_none as i32 + 1);
2014        }
2015        self.emit(Op::Pop);
2016        self.emit(Op::LoadLocal(f_slot));
2017        let nid = self.pool.node_id("n_uoe");
2018        self.emit(Op::CallClosure { arity: 0, node_id_idx: nid });
2019
2020        // Patch jump-to-end from Some arm.
2021        let end_target = self.code.len() as i32;
2022        if let Op::Jump(off) = &mut self.code[j_end] {
2023            *off = end_target - (j_end as i32 + 1);
2024        }
2025    }
2026
2027    /// `result.unwrap_or_else(res, f)` — lazy fallback over the Err payload.
2028    ///   Ok(x)  → x        (unwrap; no wrapping)
2029    ///   Err(e) → f(e)     (call closure with the error; result returned directly)
2030    /// Sibling of `emit_option_unwrap_or_else`; differs only in matching on
2031    /// `Ok` and forwarding the `Err` payload to a one-arg closure. (#679)
2032    fn emit_result_unwrap_or_else(&mut self, args: &[a::CExpr]) {
2033        let ok_idx = self.pool.variant("Ok");
2034
2035        self.compile_expr(&args[0], false);
2036        let val_slot = self.alloc_local("__ruoe_val");
2037        self.emit(Op::StoreLocal(val_slot));
2038
2039        self.compile_expr(&args[1], false);
2040        let f_slot = self.alloc_local("__ruoe_f");
2041        self.emit(Op::StoreLocal(f_slot));
2042
2043        // Test whether res is Ok.
2044        //   load val ⇒ [v]
2045        //   dup      ⇒ [v, v]
2046        //   test     ⇒ [v, Bool]
2047        //   jumpifnot → Err arm
2048        self.emit(Op::LoadLocal(val_slot));
2049        self.emit(Op::Dup);
2050        self.emit(Op::TestVariant(ok_idx));
2051        let j_err = self.code.len();
2052        self.emit(Op::JumpIfNot(0));
2053
2054        // Ok arm: extract the payload from [v] left on the stack.
2055        self.emit(Op::GetVariantArg(0));
2056        let j_end = self.code.len();
2057        self.emit(Op::Jump(0));
2058
2059        // Err arm: pop the [v] duplicate, call f with the Err payload.
2060        let err_target = self.code.len() as i32;
2061        if let Op::JumpIfNot(off) = &mut self.code[j_err] {
2062            *off = err_target - (j_err as i32 + 1);
2063        }
2064        self.emit(Op::Pop);
2065        self.emit(Op::LoadLocal(f_slot));
2066        self.emit(Op::LoadLocal(val_slot));
2067        self.emit(Op::GetVariantArg(0));
2068        let nid = self.pool.node_id("n_ruoe");
2069        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
2070
2071        // Patch jump-to-end from Ok arm.
2072        let end_target = self.code.len() as i32;
2073        if let Op::Jump(off) = &mut self.code[j_end] {
2074            *off = end_target - (j_end as i32 + 1);
2075        }
2076    }
2077
2078    // ---- std.flow trampolines ----------------------------------------
2079    //
2080    // Each flow.<op>(c1, c2, ...) call site:
2081    //   1. compiles its closure args and leaves them on the stack
2082    //   2. registers a fresh "trampoline" Function whose body invokes
2083    //      those captured closures appropriately
2084    //   3. emits MakeClosure { fn_id: trampoline, capture_count: N }
2085    //
2086    // The trampoline's parameter layout is [capture_0, ..., capture_{N-1},
2087    // arg_0, ...]: captures first, the closure's own args after.
2088
2089    /// Allocate a fresh fn_id for a trampoline and install its bytecode.
2090    /// Trampolines are the one Function-creation path that already has
2091    /// the body in hand at install time (top-level fns and lambdas have
2092    /// it filled in later), so we compute `body_hash` immediately. The
2093    /// final hash pass at the end of `compile_program` is a no-op here.
2094    fn install_trampoline(&mut self, name: &str, arity: u16, locals_count: u16, code: Vec<Op>) -> u32 {
2095        let fn_id = self.next_fn_id.len() as u32;
2096        let body_hash = crate::program::compute_body_hash(
2097            arity, locals_count, &code, &self.pool.record_shapes);
2098        self.next_fn_id.push(Function {
2099            name: name.into(),
2100            arity,
2101            locals_count,
2102            code,
2103            effects: Vec::new(),
2104            body_hash,
2105            // Trampolines (flow.sequential / parallel / etc.) don't
2106            // surface refined params at this layer.
2107            refinements: Vec::new(),
2108            // Trampolines never emit `Op::GetField` — they're pure
2109            // scaffolding. Leaving this at 0 means the VM allocates
2110            // an empty IC slot.
2111            field_ic_sites: 0,
2112        });
2113        fn_id
2114    }
2115
2116    /// `flow.sequential(f, g)` returns a closure `(x) -> g(f(x))`.
2117    fn emit_flow_sequential(&mut self, args: &[a::CExpr]) {
2118        // Push f, g; build the trampoline closure with 2 captures.
2119        self.compile_expr(&args[0], false);
2120        self.compile_expr(&args[1], false);
2121        let nid = self.pool.node_id("n_flow_sequential");
2122        let code = vec![
2123            // Locals: [f=0, g=1, x=2]
2124            Op::LoadLocal(0),                                  // push f
2125            Op::LoadLocal(2),                                  // push x
2126            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = f(x)
2127            // stack: [r]
2128            Op::StoreLocal(3),                                 // tmp = r
2129            Op::LoadLocal(1),                                  // push g
2130            Op::LoadLocal(3),                                  // push tmp
2131            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = g(tmp)
2132            Op::Return,
2133        ];
2134        let fn_id = self.install_trampoline("__flow_sequential", 3, 4, code);
2135        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
2136    }
2137
2138    /// `flow.parallel(fa, fb)` returns a closure `() -> (fa(), fb())`.
2139    /// Implementation is sequential: each function is called in order
2140    /// and the results are packed into a 2-tuple. The spec (§11.2)
2141    /// allows the runtime to apply true parallelism here; that needs
2142    /// a thread-safe handler split and is left to a follow-up. The
2143    /// signature is what users program against — sequential vs threaded
2144    /// is an implementation detail invisible to the type system.
2145    fn emit_flow_parallel(&mut self, args: &[a::CExpr]) {
2146        // Push fa, fb; build a 0-arg trampoline closure with 2 captures.
2147        self.compile_expr(&args[0], false);
2148        self.compile_expr(&args[1], false);
2149        let nid = self.pool.node_id("n_flow_parallel");
2150        let code = vec![
2151            // Locals: [fa=0, fb=1]
2152            Op::LoadLocal(0),                                  // push fa
2153            Op::CallClosure { arity: 0, node_id_idx: nid },    // a = fa()
2154            Op::LoadLocal(1),                                  // push fb
2155            Op::CallClosure { arity: 0, node_id_idx: nid },    // b = fb()
2156            Op::MakeTuple(2),                                  // (a, b)
2157            Op::Return,
2158        ];
2159        let fn_id = self.install_trampoline("__flow_parallel", 2, 2, code);
2160        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
2161    }
2162
2163    /// `flow.parallel_list(actions)` runs each 0-arg closure in `actions`
2164    /// and returns the results as a list in input order. Variadic
2165    /// counterpart to `flow.parallel`. Sequential under the hood — the
2166    /// spec (§11.2) reserves true threading for a future scheduler.
2167    /// Compiled inline (mirrors `list.map`) so closure args can flow
2168    /// through `CallClosure` without a heap-allocated trampoline.
2169    fn emit_flow_parallel_list(&mut self, args: &[a::CExpr]) {
2170        // xs := actions
2171        self.compile_expr(&args[0], false);
2172        let xs = self.alloc_local("__fpl_xs");
2173        self.emit(Op::StoreLocal(xs));
2174
2175        // out := []
2176        self.emit(Op::MakeList(0));
2177        let out = self.alloc_local("__fpl_out");
2178        self.emit(Op::StoreLocal(out));
2179
2180        // i := 0
2181        let zero = self.pool.int(0);
2182        self.emit(Op::PushConst(zero));
2183        let i = self.alloc_local("__fpl_i");
2184        self.emit(Op::StoreLocal(i));
2185
2186        // loop_top: while i < len(xs) { ... }
2187        let loop_top = self.code.len();
2188        self.emit(Op::LoadLocal(i));
2189        self.emit(Op::LoadLocal(xs));
2190        self.emit(Op::GetListLen);
2191        self.emit(Op::IntLt);
2192        let j_exit = self.code.len();
2193        self.emit(Op::JumpIfNot(0));
2194
2195        // body: out := out ++ [xs[i]()]
2196        let nid = self.pool.node_id("n_flow_parallel_list");
2197        self.emit(Op::LoadLocal(out));
2198        self.emit(Op::LoadLocal(xs));
2199        self.emit(Op::LoadLocal(i));
2200        self.emit(Op::GetListElemDyn);
2201        self.emit(Op::CallClosure { arity: 0, node_id_idx: nid });
2202        self.emit(Op::ListAppend);
2203        self.emit(Op::StoreLocal(out));
2204
2205        // i := i + 1
2206        self.emit(Op::LoadLocal(i));
2207        let one = self.pool.int(1);
2208        self.emit(Op::PushConst(one));
2209        self.emit(Op::IntAdd);
2210        self.emit(Op::StoreLocal(i));
2211
2212        // jump back
2213        let jump_back = self.code.len();
2214        let back = (loop_top as i32) - (jump_back as i32 + 1);
2215        self.emit(Op::Jump(back));
2216
2217        // exit: patch j_exit, push out
2218        let exit_target = self.code.len() as i32;
2219        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
2220            *off = exit_target - (j_exit as i32 + 1);
2221        }
2222        self.emit(Op::LoadLocal(out));
2223    }
2224
2225    /// `flow.branch(cond, t, f)` returns a closure `(x) -> if cond(x) then t(x) else f(x)`.
2226    fn emit_flow_branch(&mut self, args: &[a::CExpr]) {
2227        self.compile_expr(&args[0], false);
2228        self.compile_expr(&args[1], false);
2229        self.compile_expr(&args[2], false);
2230        let nid = self.pool.node_id("n_flow_branch");
2231        let mut code = vec![
2232            // Locals: [cond=0, t=1, f=2, x=3]
2233            Op::LoadLocal(0),                               // push cond
2234            Op::LoadLocal(3),                               // push x
2235            Op::CallClosure { arity: 1, node_id_idx: nid }, // bool
2236        ];
2237        let j_false = code.len();
2238        code.push(Op::JumpIfNot(0));                        // patched
2239        // true arm: t(x)
2240        code.push(Op::LoadLocal(1));
2241        code.push(Op::LoadLocal(3));
2242        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
2243        code.push(Op::Return);
2244        // false arm
2245        let false_target = code.len() as i32;
2246        if let Op::JumpIfNot(off) = &mut code[j_false] {
2247            *off = false_target - (j_false as i32 + 1);
2248        }
2249        code.push(Op::LoadLocal(2));
2250        code.push(Op::LoadLocal(3));
2251        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
2252        code.push(Op::Return);
2253
2254        let fn_id = self.install_trampoline("__flow_branch", 4, 4, code);
2255        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
2256    }
2257
2258    /// `flow.retry(f, max_attempts)` returns a closure `(x) -> Result[U, E]`
2259    /// that calls `f(x)` up to `max_attempts` times, returning the first
2260    /// `Ok` or the final `Err`.
2261    fn emit_flow_retry(&mut self, args: &[a::CExpr]) {
2262        self.compile_expr(&args[0], false);
2263        self.compile_expr(&args[1], false);
2264        let call_nid = self.pool.node_id("n_flow_retry");
2265        let ok_idx = self.pool.variant("Ok");
2266        let zero_const = self.pool.int(0);
2267        let one_const = self.pool.int(1);
2268        // Locals: [f=0, max=1, x=2, i=3, last=4]
2269        let mut code = vec![
2270            // i := 0
2271            Op::PushConst(zero_const),
2272            Op::StoreLocal(3),
2273        ];
2274        // loop_top: while i < max
2275        let loop_top = code.len() as i32;
2276        code.push(Op::LoadLocal(3));
2277        code.push(Op::LoadLocal(1));
2278        code.push(Op::IntLt);
2279        let j_done = code.len();
2280        code.push(Op::JumpIfNot(0));                       // patched
2281
2282        // body: r := f(x); last := r
2283        code.push(Op::LoadLocal(0));
2284        code.push(Op::LoadLocal(2));
2285        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
2286        code.push(Op::StoreLocal(4));
2287
2288        // Test variant Ok on last; if so, return last.
2289        code.push(Op::LoadLocal(4));
2290        code.push(Op::TestVariant(ok_idx));
2291        let j_was_err = code.len();
2292        code.push(Op::JumpIfNot(0));                       // patched: skip return
2293        code.push(Op::LoadLocal(4));
2294        code.push(Op::Return);
2295
2296        // was_err: i := i + 1; jump loop_top
2297        let was_err_target = code.len() as i32;
2298        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
2299            *off = was_err_target - (j_was_err as i32 + 1);
2300        }
2301        code.push(Op::LoadLocal(3));
2302        code.push(Op::PushConst(one_const));
2303        code.push(Op::IntAdd);
2304        code.push(Op::StoreLocal(3));
2305        let pc_after_jump = code.len() as i32 + 1;
2306        code.push(Op::Jump(loop_top - pc_after_jump));
2307
2308        // done: return last (the final Err, or Unit if max=0).
2309        let done_target = code.len() as i32;
2310        if let Op::JumpIfNot(off) = &mut code[j_done] {
2311            *off = done_target - (j_done as i32 + 1);
2312        }
2313        code.push(Op::LoadLocal(4));
2314        code.push(Op::Return);
2315
2316        let fn_id = self.install_trampoline("__flow_retry", 3, 5, code);
2317        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
2318    }
2319
2320    /// `flow.retry_with_backoff(f, attempts, base_ms)` (#226). Variant
2321    /// of `flow.retry` that sleeps between attempts. The first
2322    /// attempt fires immediately; attempt k > 1 waits `base_ms *
2323    /// 2^(k-2)` ms before retrying. Sleeps go through
2324    /// `time.sleep_ms`, which is why the resulting closure carries
2325    /// `[time]` in its effect row even though the underlying `f` is
2326    /// pure.
2327    fn emit_flow_retry_with_backoff(&mut self, args: &[a::CExpr]) {
2328        // Push captures: f, max, base_ms. The trampoline takes one
2329        // call-time arg `x`, so capture_count = 3, arity = 4.
2330        self.compile_expr(&args[0], false);
2331        self.compile_expr(&args[1], false);
2332        self.compile_expr(&args[2], false);
2333        let call_nid    = self.pool.node_id("n_flow_retry_backoff");
2334        let sleep_nid   = self.pool.node_id("n_flow_retry_backoff_sleep");
2335        let kind_idx    = self.pool.str("time");
2336        let op_idx      = self.pool.str("sleep_ms");
2337        let ok_idx      = self.pool.variant("Ok");
2338        let zero_const  = self.pool.int(0);
2339        let one_const   = self.pool.int(1);
2340        let two_const   = self.pool.int(2);
2341        // Locals layout:
2342        //   0=f, 1=max, 2=base_ms (captures)
2343        //   3=x (arg)
2344        //   4=i, 5=last, 6=next_delay (working state)
2345        let mut code = vec![
2346            // next_delay := base_ms
2347            Op::LoadLocal(2),
2348            Op::StoreLocal(6),
2349            // i := 0
2350            Op::PushConst(zero_const),
2351            Op::StoreLocal(4),
2352        ];
2353
2354        let loop_top = code.len() as i32;
2355        // while i < max
2356        code.push(Op::LoadLocal(4));
2357        code.push(Op::LoadLocal(1));
2358        code.push(Op::IntLt);
2359        let j_done = code.len();
2360        code.push(Op::JumpIfNot(0)); // patched
2361
2362        // if i > 0: time.sleep_ms(next_delay); next_delay := next_delay * 2
2363        code.push(Op::PushConst(zero_const));
2364        code.push(Op::LoadLocal(4));
2365        code.push(Op::IntLt);                // 0 < i ?
2366        let j_no_sleep = code.len();
2367        code.push(Op::JumpIfNot(0));         // patched: skip the sleep block
2368        // Sleep
2369        code.push(Op::LoadLocal(6));         // arg = next_delay
2370        code.push(Op::EffectCall {
2371            kind_idx, op_idx, arity: 1, node_id_idx: sleep_nid,
2372        });
2373        code.push(Op::Pop);                  // discard the Unit result
2374        // next_delay := next_delay * 2
2375        code.push(Op::LoadLocal(6));
2376        code.push(Op::PushConst(two_const));
2377        code.push(Op::NumMul);
2378        code.push(Op::StoreLocal(6));
2379        // patch the no-sleep skip
2380        let after_sleep = code.len() as i32;
2381        if let Op::JumpIfNot(off) = &mut code[j_no_sleep] {
2382            *off = after_sleep - (j_no_sleep as i32 + 1);
2383        }
2384
2385        // last := f(x)
2386        code.push(Op::LoadLocal(0));
2387        code.push(Op::LoadLocal(3));
2388        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
2389        code.push(Op::StoreLocal(5));
2390
2391        // if Ok(last): return last
2392        code.push(Op::LoadLocal(5));
2393        code.push(Op::TestVariant(ok_idx));
2394        let j_was_err = code.len();
2395        code.push(Op::JumpIfNot(0)); // patched
2396        code.push(Op::LoadLocal(5));
2397        code.push(Op::Return);
2398
2399        // was_err: i := i + 1; jump loop_top
2400        let was_err_target = code.len() as i32;
2401        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
2402            *off = was_err_target - (j_was_err as i32 + 1);
2403        }
2404        code.push(Op::LoadLocal(4));
2405        code.push(Op::PushConst(one_const));
2406        code.push(Op::IntAdd);
2407        code.push(Op::StoreLocal(4));
2408        let pc_after_jump = code.len() as i32 + 1;
2409        code.push(Op::Jump(loop_top - pc_after_jump));
2410
2411        // done: return last (the final Err, or Unit if max=0).
2412        let done_target = code.len() as i32;
2413        if let Op::JumpIfNot(off) = &mut code[j_done] {
2414            *off = done_target - (j_done as i32 + 1);
2415        }
2416        code.push(Op::LoadLocal(5));
2417        code.push(Op::Return);
2418
2419        let fn_id = self.install_trampoline("__flow_retry_backoff", 4, 7, code);
2420        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
2421    }
2422}