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 name == "todo" => {
623                // `todo()` — checker-recognized placeholder (never a real
624                // function, so it's not in `function_names`); compiles to
625                // the same unconditional trap emitted for a non-exhaustive
626                // match (`compile_match`, below). The checker guarantees
627                // `args` is empty and this arm is unreachable if the
628                // program declares its own `fn todo(...)`, since that
629                // shadowing name would already be caught by the
630                // `function_names` arm above.
631                let idx = self.pool.str("todo() reached");
632                self.emit(Op::Panic(idx));
633            }
634            a::CExpr::Var { name } if self.locals.contains_key(name) => {
635                // First-class function value bound to a local. Push the
636                // closure, then args, then CallClosure.
637                let slot = self.locals[name];
638                self.emit(Op::LoadLocal(slot));
639                for a in args { self.compile_expr(a, false); }
640                self.emit(Op::CallClosure { arity: args.len() as u16, node_id_idx });
641            }
642            // Lambda directly applied — push closure + args + CallClosure.
643            other => {
644                self.compile_expr(other, false);
645                for a in args { self.compile_expr(a, false); }
646                self.emit(Op::CallClosure { arity: args.len() as u16, node_id_idx });
647            }
648        }
649    }
650
651    fn compile_binop(&mut self, op: &str, lhs: &a::CExpr, rhs: &a::CExpr) {
652        // #461 typed lowering: if we can statically prove both
653        // operands are the same numeric type, emit the typed
654        // primitive (`IntAdd` / `FloatAdd`) instead of the
655        // polymorphic `NumAdd` that runtime-matches on operand
656        // shape. The fast path skips one match per arithmetic op
657        // *and* unblocks downstream peephole fusions (slice 1)
658        // that scan for typed primitives. Conservative fallback
659        // to the polymorphic op when either side classifies as
660        // `Unknown`, so correctness for `Float` / mixed code is
661        // unchanged.
662        let lhs_ty = self.classify_expr(lhs);
663        let rhs_ty = self.classify_expr(rhs);
664        let typed = match (lhs_ty, rhs_ty) {
665            (NumTy::Int, NumTy::Int) => NumTy::Int,
666            (NumTy::Float, NumTy::Float) => NumTy::Float,
667            _ => NumTy::Unknown,
668        };
669        self.compile_expr(lhs, false);
670        self.compile_expr(rhs, false);
671        match (op, typed) {
672            ("+",  NumTy::Int)     => self.emit(Op::IntAdd),
673            ("+",  NumTy::Float)   => self.emit(Op::FloatAdd),
674            ("+",  NumTy::Unknown) => self.emit(Op::NumAdd),
675            ("-",  NumTy::Int)     => self.emit(Op::IntSub),
676            ("-",  NumTy::Float)   => self.emit(Op::FloatSub),
677            ("-",  NumTy::Unknown) => self.emit(Op::NumSub),
678            ("*",  NumTy::Int)     => self.emit(Op::IntMul),
679            ("*",  NumTy::Float)   => self.emit(Op::FloatMul),
680            ("*",  NumTy::Unknown) => self.emit(Op::NumMul),
681            ("/",  NumTy::Int)     => self.emit(Op::IntDiv),
682            ("/",  NumTy::Float)   => self.emit(Op::FloatDiv),
683            ("/",  NumTy::Unknown) => self.emit(Op::NumDiv),
684            // Int has %; Float doesn't (NumMod will reject at runtime).
685            ("%",  NumTy::Int)     => self.emit(Op::IntMod),
686            ("%",  _)              => self.emit(Op::NumMod),
687            ("==", NumTy::Int)     => self.emit(Op::IntEq),
688            ("==", NumTy::Float)   => self.emit(Op::FloatEq),
689            ("==", NumTy::Unknown) => self.emit(Op::NumEq),
690            ("!=", NumTy::Int)     => { self.emit(Op::IntEq);   self.emit(Op::BoolNot); }
691            ("!=", NumTy::Float)   => { self.emit(Op::FloatEq); self.emit(Op::BoolNot); }
692            ("!=", NumTy::Unknown) => { self.emit(Op::NumEq);   self.emit(Op::BoolNot); }
693            ("<",  NumTy::Int)     => self.emit(Op::IntLt),
694            ("<",  NumTy::Float)   => self.emit(Op::FloatLt),
695            ("<",  NumTy::Unknown) => self.emit(Op::NumLt),
696            ("<=", NumTy::Int)     => self.emit(Op::IntLe),
697            ("<=", NumTy::Float)   => self.emit(Op::FloatLe),
698            ("<=", NumTy::Unknown) => self.emit(Op::NumLe),
699            (">",  NumTy::Int)     => { self.emit_swap_top2(); self.emit(Op::IntLt); }
700            (">",  NumTy::Float)   => { self.emit_swap_top2(); self.emit(Op::FloatLt); }
701            (">",  NumTy::Unknown) => { self.emit_swap_top2(); self.emit(Op::NumLt); }
702            (">=", NumTy::Int)     => { self.emit_swap_top2(); self.emit(Op::IntLe); }
703            (">=", NumTy::Float)   => { self.emit_swap_top2(); self.emit(Op::FloatLe); }
704            (">=", NumTy::Unknown) => { self.emit_swap_top2(); self.emit(Op::NumLe); }
705            ("and", _) => self.emit(Op::BoolAnd),
706            ("or",  _) => self.emit(Op::BoolOr),
707            (other, _) => panic!("unknown binop: {other:?}"),
708        }
709    }
710
711    /// Classify an expression's static numeric type for #461 typed
712    /// lowering. Strictly conservative: only returns `Int` / `Float`
713    /// when the type is locally derivable from a literal, an
714    /// already-classified local, or a binary op on two same-typed
715    /// operands. Everything else (function calls, field access,
716    /// match expressions, ...) falls back to `Unknown` and the
717    /// polymorphic NumAdd-family op.
718    fn classify_expr(&self, e: &a::CExpr) -> NumTy {
719        match e {
720            a::CExpr::Literal { value: a::CLit::Int { .. } } => NumTy::Int,
721            a::CExpr::Literal { value: a::CLit::Float { .. } } => NumTy::Float,
722            a::CExpr::Var { name } =>
723                self.local_types.get(name).copied().unwrap_or(NumTy::Unknown),
724            a::CExpr::BinOp { op, lhs, rhs } => {
725                // Numeric ops preserve the operand type (Int+Int=Int,
726                // Float+Float=Float). Comparison/logical ops yield
727                // Bool, not a numeric type — return Unknown.
728                let is_numeric = matches!(op.as_str(), "+" | "-" | "*" | "/" | "%");
729                if !is_numeric { return NumTy::Unknown; }
730                match (self.classify_expr(lhs), self.classify_expr(rhs)) {
731                    (NumTy::Int, NumTy::Int) => NumTy::Int,
732                    (NumTy::Float, NumTy::Float) => NumTy::Float,
733                    _ => NumTy::Unknown,
734                }
735            }
736            a::CExpr::UnaryOp { op, expr } if op == "-" => self.classify_expr(expr),
737            // #461 slice 7: `r.field` access where `r` is a local
738            // bound from a record literal. Reads the per-local
739            // field-type map populated at the let-binding site.
740            // Unknown otherwise (record argument with `:: R`
741            // annotation, helper-returned record, etc.) — those
742            // would need type-alias resolution to classify.
743            a::CExpr::FieldAccess { value, field } => {
744                if let a::CExpr::Var { name } = value.as_ref() {
745                    if let Some(ftypes) = self.local_record_field_types.get(name) {
746                        return ftypes.get(field).copied().unwrap_or(NumTy::Unknown);
747                    }
748                }
749                NumTy::Unknown
750            }
751            // Let-expressions: the let-binding mutates `local_types`
752            // *during* compile_expr; classifying ahead of time would
753            // require simulating that. Conservative fallback.
754            _ => NumTy::Unknown,
755        }
756    }
757
758    fn emit_swap_top2(&mut self) {
759        let a = self.alloc_local("__swap_a");
760        let b = self.alloc_local("__swap_b");
761        self.emit(Op::StoreLocal(b));
762        self.emit(Op::StoreLocal(a));
763        self.emit(Op::LoadLocal(b));
764        self.emit(Op::LoadLocal(a));
765    }
766
767    fn compile_match(&mut self, scrutinee: &a::CExpr, arms: &[a::Arm], tail: bool) {
768        self.compile_expr(scrutinee, false);
769        let scrut_slot = self.alloc_local("__scrut");
770        self.emit(Op::StoreLocal(scrut_slot));
771
772        let mut end_jumps: Vec<usize> = Vec::new();
773        for arm in arms {
774            let arm_start_locals = self.next_local;
775            let arm_start_locals_map = self.locals.clone();
776
777            self.emit(Op::LoadLocal(scrut_slot));
778            let mut bindings: Vec<(String, u16)> = Vec::new();
779            let fail_jumps: Vec<usize> = self.compile_pattern_test(&arm.pattern, &mut bindings);
780
781            self.compile_expr(&arm.body, tail);
782            let j_end = self.code.len();
783            self.emit(Op::Jump(0));
784            end_jumps.push(j_end);
785
786            let fail_target = self.code.len() as i32;
787            for j in fail_jumps {
788                // #337: PConstructor patterns now register an
789                // unconditional `Op::Jump` for the failure path
790                // (alongside the existing `Op::JumpIfNot` from
791                // PLiteral / nested constructor tests). Patch
792                // either shape.
793                match &mut self.code[j] {
794                    Op::JumpIfNot(off) => *off = fail_target - (j as i32 + 1),
795                    Op::Jump(off)      => *off = fail_target - (j as i32 + 1),
796                    _ => {}
797                }
798            }
799            self.next_local = arm_start_locals;
800            self.locals = arm_start_locals_map;
801        }
802        let panic_msg_idx = self.pool.str("non-exhaustive match");
803        self.emit(Op::Panic(panic_msg_idx));
804
805        let end_target = self.code.len() as i32;
806        for j in end_jumps {
807            if let Op::Jump(off) = &mut self.code[j] {
808                *off = end_target - (j as i32 + 1);
809            }
810        }
811    }
812
813    fn compile_pattern_test(&mut self, p: &a::Pattern, bindings: &mut Vec<(String, u16)>) -> Vec<usize> {
814        let mut fails = Vec::new();
815        match p {
816            a::Pattern::PWild => { self.emit(Op::Pop); }
817            a::Pattern::PVar { name } => {
818                let slot = self.alloc_local(name);
819                self.emit(Op::StoreLocal(slot));
820                bindings.push((name.clone(), slot));
821            }
822            a::Pattern::PLiteral { value } => {
823                self.compile_lit(value);
824                match value {
825                    a::CLit::Str { .. } => self.emit(Op::StrEq),
826                    a::CLit::Bytes { .. } => self.emit(Op::BytesEq),
827                    // Typed-lowering for numeric literal patterns
828                    // (#461 slice 5 prerequisite). The pattern only
829                    // reaches its test when the scrutinee has the
830                    // literal's type (the type checker rejects
831                    // mismatches), so emit the type-specific Eq.
832                    // The body_hash decoder lowers IntEq/FloatEq to
833                    // NumEq at hash time so closure identity (#222)
834                    // is unchanged. Enables slice 5's
835                    // `LoadLocal + PushConst + IntEq + JumpIfNot`
836                    // peephole to fire on pattern-match arm tests.
837                    a::CLit::Int { .. } => self.emit(Op::IntEq),
838                    a::CLit::Float { .. } => self.emit(Op::FloatEq),
839                    _ => self.emit(Op::NumEq),
840                }
841                let j = self.code.len();
842                self.emit(Op::JumpIfNot(0));
843                fails.push(j);
844            }
845            a::Pattern::PConstructor { name, args } => {
846                let name_idx = self.pool.variant(name);
847                // #337: the failure path must drop the duplicated
848                // scrutinee so subsequent match arms see a clean
849                // stack. The previous shape
850                //   Dup; TestVariant; JumpIfNot(fail);
851                // left `[scrut]` on the stack at the fail target,
852                // poisoning later arms — e.g. a wildcard `_` arm
853                // whose body referenced an unrelated value would
854                // pop the leaked scrutinee instead of its own value.
855                //
856                // New shape: branch on success, fall through to a
857                // failure cleanup that pops the dup'd scrutinee
858                // before jumping. The registered fail-jump is an
859                // unconditional `Op::Jump`; `compile_match`'s patch
860                // loop accepts both `JumpIfNot` and `Jump`.
861                self.emit(Op::Dup);                   // [scrut, scrut]
862                self.emit(Op::TestVariant(name_idx)); // [scrut, Bool]
863                let j_success = self.code.len();
864                self.emit(Op::JumpIf(0));             // pop Bool. success → [scrut]
865                self.emit(Op::Pop);                   // failure cleanup: [scrut] → []
866                let j_fail = self.code.len();
867                self.emit(Op::Jump(0));               // → fail target with []
868                fails.push(j_fail);
869                let success_target = self.code.len() as i32;
870                if let Op::JumpIf(off) = &mut self.code[j_success] {
871                    *off = success_target - (j_success as i32 + 1);
872                }
873                if args.is_empty() {
874                    self.emit(Op::Pop);
875                } else if args.len() == 1 {
876                    self.emit(Op::GetVariantArg(0));
877                    let sub_fails = self.compile_pattern_test(&args[0], bindings);
878                    fails.extend(sub_fails);
879                } else {
880                    let slot = self.alloc_local("__variant");
881                    self.emit(Op::StoreLocal(slot));
882                    for (i, arg) in args.iter().enumerate() {
883                        self.emit(Op::LoadLocal(slot));
884                        self.emit(Op::GetVariantArg(i as u16));
885                        let sub_fails = self.compile_pattern_test(arg, bindings);
886                        fails.extend(sub_fails);
887                    }
888                }
889            }
890            a::Pattern::PRecord { fields } => {
891                let slot = self.alloc_local("__record");
892                self.emit(Op::StoreLocal(slot));
893                for f in fields {
894                    self.emit(Op::LoadLocal(slot));
895                    let name_idx = self.pool.field(&f.name);
896                    let site_idx = self.field_get_sites;
897                    self.field_get_sites += 1;
898                    self.emit(Op::GetField { name_idx, site_idx });
899                    let sub_fails = self.compile_pattern_test(&f.pattern, bindings);
900                    fails.extend(sub_fails);
901                }
902            }
903            a::Pattern::PTuple { items } => {
904                let slot = self.alloc_local("__tuple");
905                self.emit(Op::StoreLocal(slot));
906                for (i, item) in items.iter().enumerate() {
907                    self.emit(Op::LoadLocal(slot));
908                    self.emit(Op::GetElem(i as u16));
909                    let sub_fails = self.compile_pattern_test(item, bindings);
910                    fails.extend(sub_fails);
911                }
912            }
913        }
914        fails
915    }
916
917    /// Compile a Lambda: collect free variables that resolve to outer-scope
918    /// locals, register a synthetic function, emit MakeClosure with the
919    /// captured values pushed in order.
920    fn compile_lambda(&mut self, params: &[a::Param], body: &a::CExpr) {
921        // Free vars = vars referenced in body that aren't bound locally.
922        let mut bound: std::collections::HashSet<String> = params.iter().map(|p| p.name.clone()).collect();
923        let mut frees: Vec<String> = Vec::new();
924        free_vars(body, &mut bound, &mut frees);
925
926        // Filter to those that are in the enclosing locals (captures).
927        // Don't exclude names that *also* exist in `function_names`:
928        // if the name is in `locals`, the local shadows the global
929        // within this scope, and the lambda needs to capture the
930        // local's value, not the global fn. (#339) Names that are
931        // ONLY in `function_names` (no local) stay external — the
932        // lambda's body resolves them at call time, same as the
933        // enclosing fn would.
934        let captures: Vec<String> = frees.into_iter()
935            .filter(|n| self.locals.contains_key(n))
936            .collect();
937
938        // Allocate a fresh fn_id by appending a placeholder Function.
939        let fn_id = self.next_fn_id.len() as u32;
940        self.next_fn_id.push(Function {
941            name: format!("__lambda_{fn_id}"),
942            arity: (captures.len() + params.len()) as u16,
943            locals_count: 0,
944            code: Vec::new(),
945            effects: Vec::new(),
946            // See #222: filled in at the end of the compile pass.
947            body_hash: crate::program::ZERO_BODY_HASH,
948            // Lambdas don't carry refinements at the surface today
949            // (closure params don't accept `Type{x | ...}` syntax in
950            // the parser). #209 stays focused on top-level fn decls;
951            // closure-param refinements are a follow-up.
952            refinements: Vec::new(),
953            // Lambda body hasn't been compiled yet; filled in by the
954            // deferred lambda-compile pass after FnCompiler walks it.
955            field_ic_sites: 0,
956        });
957
958        // Emit code at the lambda site: load each captured local, then MakeClosure.
959        for c in &captures {
960            let slot = *self.locals.get(c).expect("free var must be in scope");
961            self.emit(Op::LoadLocal(slot));
962        }
963        self.emit(Op::MakeClosure { fn_id, capture_count: captures.len() as u16 });
964
965        // Queue the body for later compilation.
966        self.pending_lambdas.push(PendingLambda {
967            fn_id,
968            capture_names: captures,
969            params: params.to_vec(),
970            body: body.clone(),
971        });
972    }
973
974    /// Higher-order stdlib ops on Result/Option whose function arg is a
975    /// closure. Emit inline: pattern-match on the variant, invoke the
976    /// closure when applicable, return wrapped result.
977    fn try_emit_higher_order(
978        &mut self,
979        module: &str,
980        op: &str,
981        args: &[a::CExpr],
982        node_id_idx: u32,
983    ) -> bool {
984        match (module, op) {
985            ("result", "map") => self.emit_variant_map(args, "Ok", true),
986            ("result", "and_then") => self.emit_variant_map(args, "Ok", false),
987            ("result", "map_err") => self.emit_variant_map(args, "Err", true),
988            ("result", "or_else") => self.emit_variant_or_else(args, "Err", 1),
989            ("option", "map") => self.emit_variant_map(args, "Some", true),
990            ("option", "and_then") => self.emit_variant_map(args, "Some", false),
991            ("option", "or_else") => self.emit_variant_or_else(args, "None", 0),
992            ("option", "unwrap_or_else") => self.emit_option_unwrap_or_else(args),
993            ("result", "unwrap_or_else") => self.emit_result_unwrap_or_else(args),
994            ("list", "map") => self.emit_list_map(args),
995            ("list", "par_map") => self.emit_list_par_map(args),
996            ("list", "sort_by") => self.emit_list_sort_by(args),
997            ("list", "filter") => self.emit_list_filter(args),
998            ("list", "fold") => self.emit_list_fold(args),
999            ("iter", "from_list") => self.emit_iter_from_list(args),
1000            ("iter", "unfold")    => self.emit_iter_unfold(args),
1001            ("iter", "next")      => self.emit_iter_next(args),
1002            ("iter", "is_empty")  => self.emit_iter_is_empty(args),
1003            ("iter", "count")     => self.emit_iter_count(args),
1004            ("iter", "take")      => self.emit_iter_take(args),
1005            ("iter", "skip")      => self.emit_iter_skip(args),
1006            ("iter", "to_list")   => self.emit_iter_to_list(args),
1007            ("iter", "collect")   => self.emit_iter_to_list(args),
1008            ("iter", "map")       => self.emit_iter_map(args),
1009            ("iter", "filter")    => self.emit_iter_filter(args),
1010            ("iter", "fold")      => self.emit_iter_fold(args),
1011            ("map", "fold") => self.emit_map_fold(args, node_id_idx),
1012            ("flow", "sequential") => self.emit_flow_sequential(args),
1013            ("flow", "branch") => self.emit_flow_branch(args),
1014            ("flow", "retry") => self.emit_flow_retry(args),
1015            ("flow", "retry_with_backoff") => self.emit_flow_retry_with_backoff(args),
1016            ("flow", "parallel") => self.emit_flow_parallel(args),
1017            ("flow", "parallel_list") => self.emit_flow_parallel_list(args),
1018            _ => return false,
1019        }
1020        true
1021    }
1022
1023    /// `list.map(xs, f)` — native map op (#464). Pushes `xs` then `f`
1024    /// and emits a single `Op::ListMap`. The previous inlined loop
1025    /// re-`LoadLocal`'d (cloned) the whole input and accumulator lists
1026    /// each iteration — O(n²); the native op owns the list and builds
1027    /// the result with one pre-sized allocation.
1028    fn emit_list_map(&mut self, args: &[a::CExpr]) {
1029        self.compile_expr(&args[0], false); // xs
1030        self.compile_expr(&args[1], false); // f
1031        let nid = self.pool.node_id("n_list_map");
1032        self.emit(Op::ListMap { node_id_idx: nid });
1033    }
1034
1035    /// `list.par_map(xs, f)` (#305 slice 1). Pushes `xs` and `f`,
1036    /// then emits a single `Op::ParallelMap` — the VM applies `f`
1037    /// to each element on OS-thread tasks, capped by
1038    /// `LEX_PAR_MAX_CONCURRENCY`. Returns the result list in input
1039    /// order.
1040    fn emit_list_par_map(&mut self, args: &[a::CExpr]) {
1041        self.compile_expr(&args[0], false);
1042        self.compile_expr(&args[1], false);
1043        let nid = self.pool.node_id("n_list_par_map");
1044        self.emit(Op::ParallelMap { node_id_idx: nid });
1045    }
1046
1047    /// `list.sort_by(xs, f)` (#338). Pushes `xs` and the key-fn
1048    /// `f`, then emits a single `Op::SortByKey` — the VM invokes
1049    /// `f` on each element to derive a sortable key, stable-sorts
1050    /// by key, and returns the values in sorted order. Keys must
1051    /// resolve to `Int` / `Float` / `Str`; mixed-type pairs are
1052    /// treated as equal by the comparator (preserving insertion
1053    /// order via the stable sort).
1054    fn emit_list_sort_by(&mut self, args: &[a::CExpr]) {
1055        self.compile_expr(&args[0], false);
1056        self.compile_expr(&args[1], false);
1057        let nid = self.pool.node_id("n_list_sort_by");
1058        self.emit(Op::SortByKey { node_id_idx: nid });
1059    }
1060
1061    /// `list.filter(xs, pred)` — native filter op (#464). Same
1062    /// rationale as `emit_list_map`.
1063    fn emit_list_filter(&mut self, args: &[a::CExpr]) {
1064        self.compile_expr(&args[0], false); // xs
1065        self.compile_expr(&args[1], false); // pred
1066        let nid = self.pool.node_id("n_list_filter");
1067        self.emit(Op::ListFilter { node_id_idx: nid });
1068    }
1069
1070    /// `list.fold(xs, init, f)` — native left-fold op (#464). Same
1071    /// rationale as `emit_list_map`. Stack: `[xs, init, f]`.
1072    fn emit_list_fold(&mut self, args: &[a::CExpr]) {
1073        self.compile_expr(&args[0], false); // xs
1074        self.compile_expr(&args[1], false); // init
1075        self.compile_expr(&args[2], false); // f
1076        let nid = self.pool.node_id("n_list_fold");
1077        self.emit(Op::ListFold { node_id_idx: nid });
1078    }
1079
1080    // ── Iter[T] operations (#364) ─────────────────────────────────────────
1081    // Internal representation: `Value::Variant("__IterEager", [list, idx])`
1082    // for the eager form (a List backing store + Int cursor) and
1083    // `Value::Variant("__IterLazy", [seed, step_closure])` for the lazy form
1084    // produced by `iter.unfold` (#376). Both are tagged variants so each op
1085    // can `TestVariant` at runtime to dispatch. The names start with `__` so
1086    // they can't be written by user code (uppercase ASCII-letter is required
1087    // for constructor names, and the underscores keep them out of the
1088    // user-namespace by convention).
1089
1090    /// `iter.from_list(xs)` — wrap a list in an eager iterator at position 0.
1091    fn emit_iter_from_list(&mut self, args: &[a::CExpr]) {
1092        self.compile_expr(&args[0], false);
1093        let zero = self.pool.int(0);
1094        self.emit(Op::PushConst(zero));
1095        let v = self.pool.variant("__IterEager");
1096        self.emit(Op::MakeVariant { name_idx: v, arity: 2 });
1097    }
1098
1099    /// `iter.next(it)` — advance one step; returns `Option[(T, Iter[T])]`.
1100    ///
1101    /// Dispatches on the iter's variant tag:
1102    /// - `__IterLazy(seed, step)` (#376) → invoke `step(seed)`. On
1103    ///   `Some((t, s'))` wrap as `Some((t, __IterLazy(s', step)))`; on
1104    ///   `None` propagate `None`. The seed advances forward each call.
1105    /// - `__IterCursor(handle)` (#379) → effect-call `sql.cursor_next(handle)`
1106    ///   which returns `Option[T]`. On `Some(row)` wrap as
1107    ///   `Some((row, __IterCursor(handle)))`; on `None` propagate. Handle
1108    ///   stays stable across calls — state is server-side / mpsc-buffered.
1109    /// - `__IterEager(list, idx)` → existing positional cursor.
1110    fn emit_iter_next(&mut self, args: &[a::CExpr]) {
1111        self.compile_expr(&args[0], false);
1112        let it = self.alloc_local("__in_it");
1113        self.emit(Op::StoreLocal(it));
1114
1115        // Dispatch: TestVariant pops; we Dup to keep the iter around.
1116        self.emit(Op::LoadLocal(it));
1117        self.emit(Op::Dup);
1118        let lazy_name = self.pool.variant("__IterLazy");
1119        self.emit(Op::TestVariant(lazy_name));
1120        let j_to_check_cursor = self.code.len();
1121        self.emit(Op::JumpIfNot(0));
1122
1123        // ── lazy path ────────────────────────────────────────────────
1124        // The Dup'd iter is on stack but we've consumed it via TestVariant,
1125        // so reload from the local.
1126        self.emit(Op::LoadLocal(it));
1127        self.emit(Op::GetVariantArg(0)); // seed
1128        let seed = self.alloc_local("__in_seed");
1129        self.emit(Op::StoreLocal(seed));
1130
1131        self.emit(Op::LoadLocal(it));
1132        self.emit(Op::GetVariantArg(1)); // step closure
1133        let step = self.alloc_local("__in_step");
1134        self.emit(Op::StoreLocal(step));
1135
1136        // Call step(seed) → Option[(T, S)].
1137        let nid_lazy = self.pool.node_id("n_iter_next_lazy");
1138        self.emit(Op::LoadLocal(step));
1139        self.emit(Op::LoadLocal(seed));
1140        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid_lazy });
1141        let opt = self.alloc_local("__in_opt");
1142        self.emit(Op::StoreLocal(opt));
1143
1144        // If `step` returned None, propagate it directly.
1145        self.emit(Op::LoadLocal(opt));
1146        let some_name = self.pool.variant("Some");
1147        self.emit(Op::TestVariant(some_name));
1148        let j_lazy_none = self.code.len();
1149        self.emit(Op::JumpIfNot(0));
1150
1151        // Some((t, new_seed)) — extract the inner tuple, repackage as
1152        // Some((t, __IterLazy(new_seed, step))) so the next call advances.
1153        self.emit(Op::LoadLocal(opt));
1154        self.emit(Op::GetVariantArg(0));     // (t, new_seed)
1155        let pair = self.alloc_local("__in_pair");
1156        self.emit(Op::StoreLocal(pair));
1157
1158        self.emit(Op::LoadLocal(pair));
1159        self.emit(Op::GetElem(0));           // t
1160        self.emit(Op::LoadLocal(pair));
1161        self.emit(Op::GetElem(1));           // new_seed
1162        self.emit(Op::LoadLocal(step));      // step closure
1163        let lazy_v = self.pool.variant("__IterLazy");
1164        self.emit(Op::MakeVariant { name_idx: lazy_v, arity: 2 }); // __IterLazy(new_seed, step)
1165        self.emit(Op::MakeTuple(2));         // (t, new_iter)
1166        let some_v = self.pool.variant("Some");
1167        self.emit(Op::MakeVariant { name_idx: some_v, arity: 1 });
1168        let j_after_lazy = self.code.len();
1169        self.emit(Op::Jump(0));
1170
1171        // Lazy → None: just forward the None.
1172        let none_t = self.code.len() as i32;
1173        if let Op::JumpIfNot(off) = &mut self.code[j_lazy_none] {
1174            *off = none_t - (j_lazy_none as i32 + 1);
1175        }
1176        let none_v = self.pool.variant("None");
1177        self.emit(Op::MakeVariant { name_idx: none_v, arity: 0 });
1178        let j_after_lazy_none = self.code.len();
1179        self.emit(Op::Jump(0));
1180
1181        // ── cursor path (#379) ───────────────────────────────────────
1182        let cursor_check_t = self.code.len() as i32;
1183        if let Op::JumpIfNot(off) = &mut self.code[j_to_check_cursor] {
1184            *off = cursor_check_t - (j_to_check_cursor as i32 + 1);
1185        }
1186
1187        self.emit(Op::LoadLocal(it));
1188        self.emit(Op::Dup);
1189        let cursor_name = self.pool.variant("__IterCursor");
1190        self.emit(Op::TestVariant(cursor_name));
1191        let j_to_eager = self.code.len();
1192        self.emit(Op::JumpIfNot(0));
1193
1194        // Cursor path: extract handle, effect-call sql.cursor_next(handle).
1195        // The handler returns Option[T] directly. We then wrap as
1196        // Some((T, __IterCursor(handle))) or forward None.
1197        self.emit(Op::LoadLocal(it));
1198        self.emit(Op::GetVariantArg(0));     // handle
1199        let handle = self.alloc_local("__in_handle");
1200        self.emit(Op::StoreLocal(handle));
1201
1202        let kind_idx = self.pool.str("sql");
1203        let op_idx = self.pool.str("cursor_next");
1204        let nid_cursor = self.pool.node_id("n_iter_next_cursor");
1205        self.emit(Op::LoadLocal(handle));
1206        self.emit(Op::EffectCall {
1207            kind_idx,
1208            op_idx,
1209            arity: 1,
1210            node_id_idx: nid_cursor,
1211        });
1212        let cur_opt = self.alloc_local("__in_cur_opt");
1213        self.emit(Op::StoreLocal(cur_opt));
1214
1215        self.emit(Op::LoadLocal(cur_opt));
1216        let some_c = self.pool.variant("Some");
1217        self.emit(Op::TestVariant(some_c));
1218        let j_cursor_none = self.code.len();
1219        self.emit(Op::JumpIfNot(0));
1220
1221        // Some(row): build Some((row, __IterCursor(handle)))
1222        self.emit(Op::LoadLocal(cur_opt));
1223        self.emit(Op::GetVariantArg(0));     // row
1224        self.emit(Op::LoadLocal(handle));
1225        let cursor_v = self.pool.variant("__IterCursor");
1226        self.emit(Op::MakeVariant { name_idx: cursor_v, arity: 1 });
1227        self.emit(Op::MakeTuple(2));         // (row, __IterCursor(handle))
1228        let some_c2 = self.pool.variant("Some");
1229        self.emit(Op::MakeVariant { name_idx: some_c2, arity: 1 });
1230        let j_after_cursor = self.code.len();
1231        self.emit(Op::Jump(0));
1232
1233        // Cursor → None
1234        let cursor_none_t = self.code.len() as i32;
1235        if let Op::JumpIfNot(off) = &mut self.code[j_cursor_none] {
1236            *off = cursor_none_t - (j_cursor_none as i32 + 1);
1237        }
1238        let none_c = self.pool.variant("None");
1239        self.emit(Op::MakeVariant { name_idx: none_c, arity: 0 });
1240        let j_after_cursor_none = self.code.len();
1241        self.emit(Op::Jump(0));
1242
1243        // ── eager path ───────────────────────────────────────────────
1244        let eager_t = self.code.len() as i32;
1245        if let Op::JumpIfNot(off) = &mut self.code[j_to_eager] {
1246            *off = eager_t - (j_to_eager as i32 + 1);
1247        }
1248
1249        self.emit(Op::LoadLocal(it));
1250        self.emit(Op::GetVariantArg(0));
1251        let list = self.alloc_local("__in_list");
1252        self.emit(Op::StoreLocal(list));
1253
1254        self.emit(Op::LoadLocal(it));
1255        self.emit(Op::GetVariantArg(1));
1256        let idx = self.alloc_local("__in_idx");
1257        self.emit(Op::StoreLocal(idx));
1258
1259        // if idx < len(list)
1260        self.emit(Op::LoadLocal(idx));
1261        self.emit(Op::LoadLocal(list));
1262        self.emit(Op::GetListLen);
1263        self.emit(Op::IntLt);
1264        let j_eager_else = self.code.len();
1265        self.emit(Op::JumpIfNot(0));
1266
1267        // Some((item, __IterEager(list, idx+1)))
1268        self.emit(Op::LoadLocal(list));
1269        self.emit(Op::LoadLocal(idx));
1270        self.emit(Op::GetListElemDyn);
1271
1272        self.emit(Op::LoadLocal(list));
1273        self.emit(Op::LoadLocal(idx));
1274        let one = self.pool.int(1);
1275        self.emit(Op::PushConst(one));
1276        self.emit(Op::IntAdd);
1277        let eager_v = self.pool.variant("__IterEager");
1278        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1279        self.emit(Op::MakeTuple(2));
1280        let some_e = self.pool.variant("Some");
1281        self.emit(Op::MakeVariant { name_idx: some_e, arity: 1 });
1282        let j_after_eager = self.code.len();
1283        self.emit(Op::Jump(0));
1284
1285        // Eager → None
1286        let eager_none_t = self.code.len() as i32;
1287        if let Op::JumpIfNot(off) = &mut self.code[j_eager_else] {
1288            *off = eager_none_t - (j_eager_else as i32 + 1);
1289        }
1290        let none_e = self.pool.variant("None");
1291        self.emit(Op::MakeVariant { name_idx: none_e, arity: 0 });
1292
1293        // Converge all paths.
1294        let end = self.code.len() as i32;
1295        if let Op::Jump(off) = &mut self.code[j_after_lazy] {
1296            *off = end - (j_after_lazy as i32 + 1);
1297        }
1298        if let Op::Jump(off) = &mut self.code[j_after_lazy_none] {
1299            *off = end - (j_after_lazy_none as i32 + 1);
1300        }
1301        if let Op::Jump(off) = &mut self.code[j_after_cursor] {
1302            *off = end - (j_after_cursor as i32 + 1);
1303        }
1304        if let Op::Jump(off) = &mut self.code[j_after_cursor_none] {
1305            *off = end - (j_after_cursor_none as i32 + 1);
1306        }
1307        if let Op::Jump(off) = &mut self.code[j_after_eager] {
1308            *off = end - (j_after_eager as i32 + 1);
1309        }
1310    }
1311
1312    /// `iter.unfold(seed, step)` — lazy iterator that calls `step(seed)` on
1313    /// each `iter.next` and threads the new seed forward. Internal value
1314    /// shape: `__IterLazy(seed, step)`. Step has type `(S) -> Option[(T, S)]`;
1315    /// returning `None` ends the iteration (#376).
1316    fn emit_iter_unfold(&mut self, args: &[a::CExpr]) {
1317        self.compile_expr(&args[0], false); // seed
1318        self.compile_expr(&args[1], false); // step
1319        let lazy = self.pool.variant("__IterLazy");
1320        self.emit(Op::MakeVariant { name_idx: lazy, arity: 2 });
1321    }
1322
1323    /// `iter.is_empty(it)` — true iff no further element. v1 supports the
1324    /// eager form O(1); on a lazy iter the seed sits in slot 0 and is not a
1325    /// List, so the VM trips on `GetListLen` rather than returning a wrong
1326    /// answer. Callers needing lazy support should materialize with
1327    /// `iter.to_list` first or call `iter.next` and pattern-match.
1328    fn emit_iter_is_empty(&mut self, args: &[a::CExpr]) {
1329        self.compile_expr(&args[0], false);
1330        let it = self.alloc_local("__ie_it");
1331        self.emit(Op::StoreLocal(it));
1332
1333        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1)); // idx
1334        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0)); // list
1335        self.emit(Op::GetListLen);                                     // len
1336        self.emit(Op::IntLt);                                          // idx < len
1337        self.emit(Op::BoolNot);                                        // NOT(idx < len)
1338    }
1339
1340    /// `iter.count(it)` — number of remaining elements (v1: eager-only).
1341    fn emit_iter_count(&mut self, args: &[a::CExpr]) {
1342        self.compile_expr(&args[0], false);
1343        let it = self.alloc_local("__ic_it");
1344        self.emit(Op::StoreLocal(it));
1345
1346        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1347        self.emit(Op::GetListLen);                                     // len
1348        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1)); // idx
1349        self.emit(Op::IntSub);                                         // len - idx
1350    }
1351
1352    /// `iter.take(it, n)` — collect up to n elements, return as new Iter.
1353    fn emit_iter_take(&mut self, args: &[a::CExpr]) {
1354        self.compile_expr(&args[0], false);
1355        let it   = self.alloc_local("__itk_it");
1356        self.emit(Op::StoreLocal(it));
1357
1358        self.compile_expr(&args[1], false);
1359        let n    = self.alloc_local("__itk_n");
1360        self.emit(Op::StoreLocal(n));
1361
1362        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1363        let list = self.alloc_local("__itk_list");
1364        self.emit(Op::StoreLocal(list));
1365
1366        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1367        let i    = self.alloc_local("__itk_i");
1368        self.emit(Op::StoreLocal(i));
1369
1370        self.emit(Op::MakeList(0));
1371        let out  = self.alloc_local("__itk_out");
1372        self.emit(Op::StoreLocal(out));
1373
1374        let zero = self.pool.int(0);
1375        self.emit(Op::PushConst(zero));
1376        let cnt  = self.alloc_local("__itk_cnt");
1377        self.emit(Op::StoreLocal(cnt));
1378
1379        let loop_top = self.code.len();
1380
1381        // while cnt < n
1382        self.emit(Op::LoadLocal(cnt));
1383        self.emit(Op::LoadLocal(n));
1384        self.emit(Op::IntLt);
1385        let j_exit_n = self.code.len();
1386        self.emit(Op::JumpIfNot(0));
1387
1388        // AND i < len(list)
1389        self.emit(Op::LoadLocal(i));
1390        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1391        self.emit(Op::IntLt);
1392        let j_exit_l = self.code.len();
1393        self.emit(Op::JumpIfNot(0));
1394
1395        // out = out ++ [list[i]]
1396        self.emit(Op::LoadLocal(out));
1397        self.emit(Op::LoadLocal(list));
1398        self.emit(Op::LoadLocal(i));
1399        self.emit(Op::GetListElemDyn);
1400        self.emit(Op::ListAppend);
1401        self.emit(Op::StoreLocal(out));
1402
1403        let one = self.pool.int(1);
1404        // i = i + 1
1405        self.emit(Op::LoadLocal(i));
1406        self.emit(Op::PushConst(one));
1407        self.emit(Op::IntAdd);
1408        self.emit(Op::StoreLocal(i));
1409        // cnt = cnt + 1
1410        self.emit(Op::LoadLocal(cnt));
1411        self.emit(Op::PushConst(one));
1412        self.emit(Op::IntAdd);
1413        self.emit(Op::StoreLocal(cnt));
1414
1415        let jback = self.code.len();
1416        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1417
1418        let exit_t = self.code.len() as i32;
1419        if let Op::JumpIfNot(off) = &mut self.code[j_exit_n] { *off = exit_t - (j_exit_n as i32 + 1); }
1420        if let Op::JumpIfNot(off) = &mut self.code[j_exit_l] { *off = exit_t - (j_exit_l as i32 + 1); }
1421
1422        // return new __IterEager(out, 0)
1423        self.emit(Op::LoadLocal(out));
1424        self.emit(Op::PushConst(zero));
1425        let eager_v = self.pool.variant("__IterEager");
1426        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1427    }
1428
1429    /// `iter.skip(it, n)` — advance cursor by n (or to end), return new Iter.
1430    fn emit_iter_skip(&mut self, args: &[a::CExpr]) {
1431        self.compile_expr(&args[0], false);
1432        let it   = self.alloc_local("__isk_it");
1433        self.emit(Op::StoreLocal(it));
1434
1435        self.compile_expr(&args[1], false);
1436        let n    = self.alloc_local("__isk_n");
1437        self.emit(Op::StoreLocal(n));
1438
1439        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1440        let list = self.alloc_local("__isk_list");
1441        self.emit(Op::StoreLocal(list));
1442
1443        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1444        let idx  = self.alloc_local("__isk_idx");
1445        self.emit(Op::StoreLocal(idx));
1446
1447        // raw = idx + n
1448        self.emit(Op::LoadLocal(idx));
1449        self.emit(Op::LoadLocal(n));
1450        self.emit(Op::IntAdd);
1451        let raw  = self.alloc_local("__isk_raw");
1452        self.emit(Op::StoreLocal(raw));
1453
1454        // new_idx = if raw < len then raw else len
1455        self.emit(Op::LoadLocal(raw));
1456        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1457        self.emit(Op::IntLt);
1458        let j_use_raw = self.code.len();
1459        self.emit(Op::JumpIf(0));
1460
1461        // use len
1462        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1463        let j_end = self.code.len();
1464        self.emit(Op::Jump(0));
1465
1466        // use raw
1467        let raw_t = self.code.len() as i32;
1468        if let Op::JumpIf(off) = &mut self.code[j_use_raw] { *off = raw_t - (j_use_raw as i32 + 1); }
1469        self.emit(Op::LoadLocal(raw));
1470
1471        let end_t = self.code.len() as i32;
1472        if let Op::Jump(off) = &mut self.code[j_end] { *off = end_t - (j_end as i32 + 1); }
1473
1474        // new_idx on stack; build new __IterEager(list, new_idx)
1475        let new_idx = self.alloc_local("__isk_ni");
1476        self.emit(Op::StoreLocal(new_idx));
1477        self.emit(Op::LoadLocal(list));
1478        self.emit(Op::LoadLocal(new_idx));
1479        let eager_v = self.pool.variant("__IterEager");
1480        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1481    }
1482
1483    /// `iter.to_list(it)` — materialise remaining elements into a List.
1484    ///
1485    /// Dispatches on the iter variant (#376):
1486    /// - `__IterLazy`: repeatedly call `step(seed)`; on `Some((t, s'))` append
1487    ///   `t` and continue with `s'`; on `None` stop. May hang on truly
1488    ///   infinite producers — that's documented as a v1 limitation, the
1489    ///   step-limit-protected caller is what catches misuse.
1490    /// - `__IterEager`: slice the backing list from `idx` onward (O(n) walk).
1491    fn emit_iter_to_list(&mut self, args: &[a::CExpr]) {
1492        self.compile_expr(&args[0], false);
1493        let it = self.alloc_local("__itl_it");
1494        self.emit(Op::StoreLocal(it));
1495
1496        // Build the output list up-front, shared across both paths.
1497        self.emit(Op::MakeList(0));
1498        let out = self.alloc_local("__itl_out");
1499        self.emit(Op::StoreLocal(out));
1500
1501        // Dispatch on variant tag.
1502        self.emit(Op::LoadLocal(it));
1503        let lazy_name = self.pool.variant("__IterLazy");
1504        self.emit(Op::TestVariant(lazy_name));
1505        let j_to_eager = self.code.len();
1506        self.emit(Op::JumpIfNot(0));
1507
1508        // ── lazy path ─────────────────────────────────────────────────
1509        // seed and step closure live in locals; we update seed each iteration.
1510        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1511        let seed = self.alloc_local("__itl_seed");
1512        self.emit(Op::StoreLocal(seed));
1513
1514        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1515        let step = self.alloc_local("__itl_step");
1516        self.emit(Op::StoreLocal(step));
1517
1518        let lazy_loop = self.code.len();
1519        let nid_lazy = self.pool.node_id("n_iter_to_list_lazy");
1520        self.emit(Op::LoadLocal(step));
1521        self.emit(Op::LoadLocal(seed));
1522        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid_lazy });
1523        let opt = self.alloc_local("__itl_opt");
1524        self.emit(Op::StoreLocal(opt));
1525
1526        // If None, drop out of the lazy loop.
1527        self.emit(Op::LoadLocal(opt));
1528        let some_name = self.pool.variant("Some");
1529        self.emit(Op::TestVariant(some_name));
1530        let j_lazy_exit = self.code.len();
1531        self.emit(Op::JumpIfNot(0));
1532
1533        // Some((t, new_seed)): append t to out, replace seed.
1534        self.emit(Op::LoadLocal(opt));
1535        self.emit(Op::GetVariantArg(0));
1536        let pair = self.alloc_local("__itl_pair");
1537        self.emit(Op::StoreLocal(pair));
1538
1539        self.emit(Op::LoadLocal(out));
1540        self.emit(Op::LoadLocal(pair)); self.emit(Op::GetElem(0));
1541        self.emit(Op::ListAppend);
1542        self.emit(Op::StoreLocal(out));
1543
1544        self.emit(Op::LoadLocal(pair)); self.emit(Op::GetElem(1));
1545        self.emit(Op::StoreLocal(seed));
1546
1547        let jback_lazy = self.code.len();
1548        self.emit(Op::Jump((lazy_loop as i32) - (jback_lazy as i32 + 1)));
1549
1550        let lazy_exit_t = self.code.len() as i32;
1551        if let Op::JumpIfNot(off) = &mut self.code[j_lazy_exit] {
1552            *off = lazy_exit_t - (j_lazy_exit as i32 + 1);
1553        }
1554        let j_after_lazy = self.code.len();
1555        self.emit(Op::Jump(0));
1556
1557        // ── eager path ────────────────────────────────────────────────
1558        let eager_t = self.code.len() as i32;
1559        if let Op::JumpIfNot(off) = &mut self.code[j_to_eager] {
1560            *off = eager_t - (j_to_eager as i32 + 1);
1561        }
1562
1563        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1564        let list = self.alloc_local("__itl_list");
1565        self.emit(Op::StoreLocal(list));
1566
1567        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1568        let i = self.alloc_local("__itl_i");
1569        self.emit(Op::StoreLocal(i));
1570
1571        let loop_top = self.code.len();
1572        self.emit(Op::LoadLocal(i));
1573        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1574        self.emit(Op::IntLt);
1575        let j_exit = self.code.len();
1576        self.emit(Op::JumpIfNot(0));
1577
1578        self.emit(Op::LoadLocal(out));
1579        self.emit(Op::LoadLocal(list));
1580        self.emit(Op::LoadLocal(i));
1581        self.emit(Op::GetListElemDyn);
1582        self.emit(Op::ListAppend);
1583        self.emit(Op::StoreLocal(out));
1584
1585        self.emit(Op::LoadLocal(i));
1586        let one = self.pool.int(1);
1587        self.emit(Op::PushConst(one));
1588        self.emit(Op::IntAdd);
1589        self.emit(Op::StoreLocal(i));
1590
1591        let jback = self.code.len();
1592        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1593
1594        let exit_t = self.code.len() as i32;
1595        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
1596            *off = exit_t - (j_exit as i32 + 1);
1597        }
1598
1599        // Converge: lazy path falls through here too.
1600        let converge = self.code.len() as i32;
1601        if let Op::Jump(off) = &mut self.code[j_after_lazy] {
1602            *off = converge - (j_after_lazy as i32 + 1);
1603        }
1604        self.emit(Op::LoadLocal(out));
1605    }
1606
1607    /// `iter.map(it, f)` — apply `f` to each remaining element; returns new Iter.
1608    fn emit_iter_map(&mut self, args: &[a::CExpr]) {
1609        self.compile_expr(&args[0], false);
1610        let it   = self.alloc_local("__im_it");
1611        self.emit(Op::StoreLocal(it));
1612
1613        self.compile_expr(&args[1], false);
1614        let f    = self.alloc_local("__im_f");
1615        self.emit(Op::StoreLocal(f));
1616
1617        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1618        let list = self.alloc_local("__im_list");
1619        self.emit(Op::StoreLocal(list));
1620
1621        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1622        let i    = self.alloc_local("__im_i");
1623        self.emit(Op::StoreLocal(i));
1624
1625        self.emit(Op::MakeList(0));
1626        let out  = self.alloc_local("__im_out");
1627        self.emit(Op::StoreLocal(out));
1628
1629        let loop_top = self.code.len();
1630        self.emit(Op::LoadLocal(i));
1631        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1632        self.emit(Op::IntLt);
1633        let j_exit = self.code.len();
1634        self.emit(Op::JumpIfNot(0));
1635
1636        let nid = self.pool.node_id("n_iter_map");
1637        self.emit(Op::LoadLocal(out));
1638        self.emit(Op::LoadLocal(f));
1639        self.emit(Op::LoadLocal(list));
1640        self.emit(Op::LoadLocal(i));
1641        self.emit(Op::GetListElemDyn);
1642        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1643        self.emit(Op::ListAppend);
1644        self.emit(Op::StoreLocal(out));
1645
1646        self.emit(Op::LoadLocal(i));
1647        let one = self.pool.int(1);
1648        self.emit(Op::PushConst(one));
1649        self.emit(Op::IntAdd);
1650        self.emit(Op::StoreLocal(i));
1651
1652        let jback = self.code.len();
1653        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1654
1655        let exit_t = self.code.len() as i32;
1656        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1657
1658        let zero = self.pool.int(0);
1659        self.emit(Op::LoadLocal(out));
1660        self.emit(Op::PushConst(zero));
1661        let eager_v = self.pool.variant("__IterEager");
1662        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1663    }
1664
1665    /// `iter.filter(it, pred)` — keep elements where pred is true; returns new Iter.
1666    fn emit_iter_filter(&mut self, args: &[a::CExpr]) {
1667        self.compile_expr(&args[0], false);
1668        let it   = self.alloc_local("__if_it");
1669        self.emit(Op::StoreLocal(it));
1670
1671        self.compile_expr(&args[1], false);
1672        let f    = self.alloc_local("__if_f");
1673        self.emit(Op::StoreLocal(f));
1674
1675        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1676        let list = self.alloc_local("__if_list");
1677        self.emit(Op::StoreLocal(list));
1678
1679        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1680        let i    = self.alloc_local("__if_i");
1681        self.emit(Op::StoreLocal(i));
1682
1683        self.emit(Op::MakeList(0));
1684        let out  = self.alloc_local("__if_out");
1685        self.emit(Op::StoreLocal(out));
1686
1687        let loop_top = self.code.len();
1688        self.emit(Op::LoadLocal(i));
1689        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1690        self.emit(Op::IntLt);
1691        let j_exit = self.code.len();
1692        self.emit(Op::JumpIfNot(0));
1693
1694        // elem := list[i]
1695        self.emit(Op::LoadLocal(list));
1696        self.emit(Op::LoadLocal(i));
1697        self.emit(Op::GetListElemDyn);
1698        let x    = self.alloc_local("__if_x");
1699        self.emit(Op::StoreLocal(x));
1700
1701        let nid = self.pool.node_id("n_iter_filter");
1702        self.emit(Op::LoadLocal(f));
1703        self.emit(Op::LoadLocal(x));
1704        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1705        let j_skip = self.code.len();
1706        self.emit(Op::JumpIfNot(0));
1707
1708        self.emit(Op::LoadLocal(out));
1709        self.emit(Op::LoadLocal(x));
1710        self.emit(Op::ListAppend);
1711        self.emit(Op::StoreLocal(out));
1712
1713        let skip_t = self.code.len() as i32;
1714        if let Op::JumpIfNot(off) = &mut self.code[j_skip] { *off = skip_t - (j_skip as i32 + 1); }
1715
1716        self.emit(Op::LoadLocal(i));
1717        let one = self.pool.int(1);
1718        self.emit(Op::PushConst(one));
1719        self.emit(Op::IntAdd);
1720        self.emit(Op::StoreLocal(i));
1721
1722        let jback = self.code.len();
1723        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1724
1725        let exit_t = self.code.len() as i32;
1726        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1727
1728        let zero = self.pool.int(0);
1729        self.emit(Op::LoadLocal(out));
1730        self.emit(Op::PushConst(zero));
1731        let eager_v = self.pool.variant("__IterEager");
1732        self.emit(Op::MakeVariant { name_idx: eager_v, arity: 2 });
1733    }
1734
1735    /// `iter.fold(it, init, f)` — left fold over remaining elements.
1736    fn emit_iter_fold(&mut self, args: &[a::CExpr]) {
1737        self.compile_expr(&args[0], false);
1738        let it   = self.alloc_local("__ifo_it");
1739        self.emit(Op::StoreLocal(it));
1740
1741        self.compile_expr(&args[1], false);
1742        let acc  = self.alloc_local("__ifo_acc");
1743        self.emit(Op::StoreLocal(acc));
1744
1745        self.compile_expr(&args[2], false);
1746        let f    = self.alloc_local("__ifo_f");
1747        self.emit(Op::StoreLocal(f));
1748
1749        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(0));
1750        let list = self.alloc_local("__ifo_list");
1751        self.emit(Op::StoreLocal(list));
1752
1753        self.emit(Op::LoadLocal(it)); self.emit(Op::GetVariantArg(1));
1754        let i    = self.alloc_local("__ifo_i");
1755        self.emit(Op::StoreLocal(i));
1756
1757        let loop_top = self.code.len();
1758        self.emit(Op::LoadLocal(i));
1759        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1760        self.emit(Op::IntLt);
1761        let j_exit = self.code.len();
1762        self.emit(Op::JumpIfNot(0));
1763
1764        let nid = self.pool.node_id("n_iter_fold");
1765        self.emit(Op::LoadLocal(f));
1766        self.emit(Op::LoadLocal(acc));
1767        self.emit(Op::LoadLocal(list));
1768        self.emit(Op::LoadLocal(i));
1769        self.emit(Op::GetListElemDyn);
1770        self.emit(Op::CallClosure { arity: 2, node_id_idx: nid });
1771        self.emit(Op::StoreLocal(acc));
1772
1773        self.emit(Op::LoadLocal(i));
1774        let one = self.pool.int(1);
1775        self.emit(Op::PushConst(one));
1776        self.emit(Op::IntAdd);
1777        self.emit(Op::StoreLocal(i));
1778
1779        let jback = self.code.len();
1780        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1781
1782        let exit_t = self.code.len() as i32;
1783        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1784        self.emit(Op::LoadLocal(acc));
1785    }
1786
1787    /// `map.fold(m, init, f)` — left fold over `Map[K, V]` entries with a
1788    /// three-arg combiner `f(acc, k, v)`. Iteration order matches
1789    /// `map.entries` (BTreeMap-sorted by key). Materializes the entry
1790    /// list once via the runtime's `("map", "entries")` op, then runs
1791    /// the same inline loop as `list.fold`.
1792    fn emit_map_fold(&mut self, args: &[a::CExpr], node_id_idx: u32) {
1793        // xs := map.entries(m)
1794        self.compile_expr(&args[0], false);
1795        let map_kind = self.pool.str("map");
1796        let entries_op = self.pool.str("entries");
1797        self.emit(Op::EffectCall {
1798            kind_idx: map_kind,
1799            op_idx: entries_op,
1800            arity: 1,
1801            node_id_idx,
1802        });
1803        let xs = self.alloc_local("__mf_xs");
1804        self.emit(Op::StoreLocal(xs));
1805
1806        // acc := init
1807        self.compile_expr(&args[1], false);
1808        let acc = self.alloc_local("__mf_acc");
1809        self.emit(Op::StoreLocal(acc));
1810
1811        // f := <closure>
1812        self.compile_expr(&args[2], false);
1813        let f = self.alloc_local("__mf_f");
1814        self.emit(Op::StoreLocal(f));
1815
1816        // i := 0
1817        let zero = self.pool.int(0);
1818        self.emit(Op::PushConst(zero));
1819        let i = self.alloc_local("__mf_i");
1820        self.emit(Op::StoreLocal(i));
1821
1822        // loop_top: while i < len(xs)
1823        let loop_top = self.code.len();
1824        self.emit(Op::LoadLocal(i));
1825        self.emit(Op::LoadLocal(xs));
1826        self.emit(Op::GetListLen);
1827        self.emit(Op::IntLt);
1828        let j_exit = self.code.len();
1829        self.emit(Op::JumpIfNot(0));
1830
1831        // pair := xs[i]
1832        self.emit(Op::LoadLocal(xs));
1833        self.emit(Op::LoadLocal(i));
1834        self.emit(Op::GetListElemDyn);
1835        let pair = self.alloc_local("__mf_pair");
1836        self.emit(Op::StoreLocal(pair));
1837
1838        // acc := f(acc, pair.0, pair.1)
1839        let nid = self.pool.node_id("n_map_fold");
1840        self.emit(Op::LoadLocal(f));
1841        self.emit(Op::LoadLocal(acc));
1842        self.emit(Op::LoadLocal(pair));
1843        self.emit(Op::GetElem(0));
1844        self.emit(Op::LoadLocal(pair));
1845        self.emit(Op::GetElem(1));
1846        self.emit(Op::CallClosure { arity: 3, node_id_idx: nid });
1847        self.emit(Op::StoreLocal(acc));
1848
1849        // i := i + 1
1850        self.emit(Op::LoadLocal(i));
1851        let one = self.pool.int(1);
1852        self.emit(Op::PushConst(one));
1853        self.emit(Op::IntAdd);
1854        self.emit(Op::StoreLocal(i));
1855
1856        let jump_back = self.code.len();
1857        let back = (loop_top as i32) - (jump_back as i32 + 1);
1858        self.emit(Op::Jump(back));
1859
1860        let exit_target = self.code.len() as i32;
1861        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
1862            *off = exit_target - (j_exit as i32 + 1);
1863        }
1864        self.emit(Op::LoadLocal(acc));
1865    }
1866
1867    /// Inline pattern: `<module>.map(v, f)` and friends.
1868    /// `wrap_with`: variant tag whose payload triggers the call (Ok / Some / Err).
1869    /// `wrap_result`: if true, wrap the closure's result back in `wrap_with`
1870    /// (map shape); if false, expect the closure to return a wrapped value
1871    /// itself (and_then shape).
1872    fn emit_variant_map(
1873        &mut self,
1874        args: &[a::CExpr],
1875        wrap_with: &str,
1876        wrap_result: bool,
1877    ) {
1878        // args[0] = the wrapped value (Result/Option), args[1] = closure
1879        let wrap_idx = self.pool.variant(wrap_with);
1880
1881        // Compile and store the value into a local, evaluate closure on top of stack.
1882        self.compile_expr(&args[0], false);
1883        let val_slot = self.alloc_local("__hov");
1884        self.emit(Op::StoreLocal(val_slot));
1885
1886        self.compile_expr(&args[1], false);
1887        let f_slot = self.alloc_local("__hof");
1888        self.emit(Op::StoreLocal(f_slot));
1889
1890        // Stack discipline:
1891        //   load val ⇒ [v]
1892        //   dup     ⇒ [v, v]
1893        //   test    ⇒ [v, Bool]
1894        //   jumpifnot ⇒ [v]
1895        // Both branches end with [v] before the branch body.
1896        self.emit(Op::LoadLocal(val_slot));
1897        self.emit(Op::Dup);
1898        self.emit(Op::TestVariant(wrap_idx));
1899        let j_skip = self.code.len();
1900        self.emit(Op::JumpIfNot(0));
1901
1902        // Matched arm: extract payload, call closure on it.
1903        self.emit(Op::GetVariantArg(0));
1904        let arg_slot = self.alloc_local("__hov_arg");
1905        self.emit(Op::StoreLocal(arg_slot));
1906        self.emit(Op::LoadLocal(f_slot));
1907        self.emit(Op::LoadLocal(arg_slot));
1908        let nid = self.pool.node_id("n_hov");
1909        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1910        if wrap_result {
1911            self.emit(Op::MakeVariant { name_idx: wrap_idx, arity: 1 });
1912        }
1913        let j_end = self.code.len();
1914        self.emit(Op::Jump(0));
1915
1916        // Skip arm: stack already has [v] from the failed Dup; nothing to do.
1917        let skip_target = self.code.len() as i32;
1918        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
1919            *off = skip_target - (j_skip as i32 + 1);
1920        }
1921
1922        let end_target = self.code.len() as i32;
1923        if let Op::Jump(off) = &mut self.code[j_end] {
1924            *off = end_target - (j_end as i32 + 1);
1925        }
1926    }
1927
1928    /// Sibling of `emit_variant_map` for the recovery combinators
1929    /// `result.or_else` and `option.or_else`. Differences from
1930    /// `emit_variant_map`:
1931    ///   - matches on the *negative* variant (`Err` / `None`)
1932    ///   - the closure's result becomes the call's result directly,
1933    ///     with no wrapping (it is itself a `Result` / `Option`)
1934    ///   - `option.or_else`'s closure takes zero args (`None` has no
1935    ///     payload to forward)
1936    fn emit_variant_or_else(
1937        &mut self,
1938        args: &[a::CExpr],
1939        match_on: &str,
1940        closure_arity: u16,
1941    ) {
1942        let match_idx = self.pool.variant(match_on);
1943
1944        self.compile_expr(&args[0], false);
1945        let val_slot = self.alloc_local("__hoe");
1946        self.emit(Op::StoreLocal(val_slot));
1947
1948        self.compile_expr(&args[1], false);
1949        let f_slot = self.alloc_local("__hoe_f");
1950        self.emit(Op::StoreLocal(f_slot));
1951
1952        // Stack discipline mirrors emit_variant_map:
1953        //   load val      ⇒ [v]
1954        //   dup           ⇒ [v, v]
1955        //   test          ⇒ [v, Bool]
1956        //   jumpifnot     ⇒ [v]
1957        // The unmatched arm leaves [v] (Ok/Some unchanged); the
1958        // matched arm pops [v] and pushes the closure's result.
1959        self.emit(Op::LoadLocal(val_slot));
1960        self.emit(Op::Dup);
1961        self.emit(Op::TestVariant(match_idx));
1962        let j_skip = self.code.len();
1963        self.emit(Op::JumpIfNot(0));
1964
1965        // Matched arm: pop the duplicate left on the stack,
1966        // then call the closure with whatever payload it expects.
1967        self.emit(Op::Pop);
1968        self.emit(Op::LoadLocal(f_slot));
1969        if closure_arity == 1 {
1970            self.emit(Op::LoadLocal(val_slot));
1971            self.emit(Op::GetVariantArg(0));
1972        }
1973        let nid = self.pool.node_id("n_hoe");
1974        self.emit(Op::CallClosure { arity: closure_arity, node_id_idx: nid });
1975
1976        let j_end = self.code.len();
1977        self.emit(Op::Jump(0));
1978
1979        // Unmatched arm: stack already holds [v]; nothing to do.
1980        let skip_target = self.code.len() as i32;
1981        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
1982            *off = skip_target - (j_skip as i32 + 1);
1983        }
1984
1985        let end_target = self.code.len() as i32;
1986        if let Op::Jump(off) = &mut self.code[j_end] {
1987            *off = end_target - (j_end as i32 + 1);
1988        }
1989    }
1990
1991    /// `option.unwrap_or_else(opt, f)` — lazy default via zero-arg thunk.
1992    ///   Some(x) → x          (unwrap; no wrapping)
1993    ///   None    → f()        (call thunk; return its result directly)
1994    fn emit_option_unwrap_or_else(&mut self, args: &[a::CExpr]) {
1995        let some_idx = self.pool.variant("Some");
1996
1997        // Compile opt and f; stash both so they're accessible on both arms.
1998        self.compile_expr(&args[0], false);
1999        let val_slot = self.alloc_local("__uoe_val");
2000        self.emit(Op::StoreLocal(val_slot));
2001
2002        self.compile_expr(&args[1], false);
2003        let f_slot = self.alloc_local("__uoe_f");
2004        self.emit(Op::StoreLocal(f_slot));
2005
2006        // Test whether opt is Some.
2007        //   load val ⇒ [v]
2008        //   dup      ⇒ [v, v]
2009        //   test     ⇒ [v, Bool]
2010        //   jumpifnot → None arm
2011        self.emit(Op::LoadLocal(val_slot));
2012        self.emit(Op::Dup);
2013        self.emit(Op::TestVariant(some_idx));
2014        let j_none = self.code.len();
2015        self.emit(Op::JumpIfNot(0));
2016
2017        // Some arm: extract the payload from [v] left on the stack.
2018        self.emit(Op::GetVariantArg(0));
2019        let j_end = self.code.len();
2020        self.emit(Op::Jump(0));
2021
2022        // None arm: pop the [v] duplicate, call the thunk.
2023        let none_target = self.code.len() as i32;
2024        if let Op::JumpIfNot(off) = &mut self.code[j_none] {
2025            *off = none_target - (j_none as i32 + 1);
2026        }
2027        self.emit(Op::Pop);
2028        self.emit(Op::LoadLocal(f_slot));
2029        let nid = self.pool.node_id("n_uoe");
2030        self.emit(Op::CallClosure { arity: 0, node_id_idx: nid });
2031
2032        // Patch jump-to-end from Some arm.
2033        let end_target = self.code.len() as i32;
2034        if let Op::Jump(off) = &mut self.code[j_end] {
2035            *off = end_target - (j_end as i32 + 1);
2036        }
2037    }
2038
2039    /// `result.unwrap_or_else(res, f)` — lazy fallback over the Err payload.
2040    ///   Ok(x)  → x        (unwrap; no wrapping)
2041    ///   Err(e) → f(e)     (call closure with the error; result returned directly)
2042    /// Sibling of `emit_option_unwrap_or_else`; differs only in matching on
2043    /// `Ok` and forwarding the `Err` payload to a one-arg closure. (#679)
2044    fn emit_result_unwrap_or_else(&mut self, args: &[a::CExpr]) {
2045        let ok_idx = self.pool.variant("Ok");
2046
2047        self.compile_expr(&args[0], false);
2048        let val_slot = self.alloc_local("__ruoe_val");
2049        self.emit(Op::StoreLocal(val_slot));
2050
2051        self.compile_expr(&args[1], false);
2052        let f_slot = self.alloc_local("__ruoe_f");
2053        self.emit(Op::StoreLocal(f_slot));
2054
2055        // Test whether res is Ok.
2056        //   load val ⇒ [v]
2057        //   dup      ⇒ [v, v]
2058        //   test     ⇒ [v, Bool]
2059        //   jumpifnot → Err arm
2060        self.emit(Op::LoadLocal(val_slot));
2061        self.emit(Op::Dup);
2062        self.emit(Op::TestVariant(ok_idx));
2063        let j_err = self.code.len();
2064        self.emit(Op::JumpIfNot(0));
2065
2066        // Ok arm: extract the payload from [v] left on the stack.
2067        self.emit(Op::GetVariantArg(0));
2068        let j_end = self.code.len();
2069        self.emit(Op::Jump(0));
2070
2071        // Err arm: pop the [v] duplicate, call f with the Err payload.
2072        let err_target = self.code.len() as i32;
2073        if let Op::JumpIfNot(off) = &mut self.code[j_err] {
2074            *off = err_target - (j_err as i32 + 1);
2075        }
2076        self.emit(Op::Pop);
2077        self.emit(Op::LoadLocal(f_slot));
2078        self.emit(Op::LoadLocal(val_slot));
2079        self.emit(Op::GetVariantArg(0));
2080        let nid = self.pool.node_id("n_ruoe");
2081        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
2082
2083        // Patch jump-to-end from Ok arm.
2084        let end_target = self.code.len() as i32;
2085        if let Op::Jump(off) = &mut self.code[j_end] {
2086            *off = end_target - (j_end as i32 + 1);
2087        }
2088    }
2089
2090    // ---- std.flow trampolines ----------------------------------------
2091    //
2092    // Each flow.<op>(c1, c2, ...) call site:
2093    //   1. compiles its closure args and leaves them on the stack
2094    //   2. registers a fresh "trampoline" Function whose body invokes
2095    //      those captured closures appropriately
2096    //   3. emits MakeClosure { fn_id: trampoline, capture_count: N }
2097    //
2098    // The trampoline's parameter layout is [capture_0, ..., capture_{N-1},
2099    // arg_0, ...]: captures first, the closure's own args after.
2100
2101    /// Allocate a fresh fn_id for a trampoline and install its bytecode.
2102    /// Trampolines are the one Function-creation path that already has
2103    /// the body in hand at install time (top-level fns and lambdas have
2104    /// it filled in later), so we compute `body_hash` immediately. The
2105    /// final hash pass at the end of `compile_program` is a no-op here.
2106    fn install_trampoline(&mut self, name: &str, arity: u16, locals_count: u16, code: Vec<Op>) -> u32 {
2107        let fn_id = self.next_fn_id.len() as u32;
2108        let body_hash = crate::program::compute_body_hash(
2109            arity, locals_count, &code, &self.pool.record_shapes);
2110        self.next_fn_id.push(Function {
2111            name: name.into(),
2112            arity,
2113            locals_count,
2114            code,
2115            effects: Vec::new(),
2116            body_hash,
2117            // Trampolines (flow.sequential / parallel / etc.) don't
2118            // surface refined params at this layer.
2119            refinements: Vec::new(),
2120            // Trampolines never emit `Op::GetField` — they're pure
2121            // scaffolding. Leaving this at 0 means the VM allocates
2122            // an empty IC slot.
2123            field_ic_sites: 0,
2124        });
2125        fn_id
2126    }
2127
2128    /// `flow.sequential(f, g)` returns a closure `(x) -> g(f(x))`.
2129    fn emit_flow_sequential(&mut self, args: &[a::CExpr]) {
2130        // Push f, g; build the trampoline closure with 2 captures.
2131        self.compile_expr(&args[0], false);
2132        self.compile_expr(&args[1], false);
2133        let nid = self.pool.node_id("n_flow_sequential");
2134        let code = vec![
2135            // Locals: [f=0, g=1, x=2]
2136            Op::LoadLocal(0),                                  // push f
2137            Op::LoadLocal(2),                                  // push x
2138            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = f(x)
2139            // stack: [r]
2140            Op::StoreLocal(3),                                 // tmp = r
2141            Op::LoadLocal(1),                                  // push g
2142            Op::LoadLocal(3),                                  // push tmp
2143            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = g(tmp)
2144            Op::Return,
2145        ];
2146        let fn_id = self.install_trampoline("__flow_sequential", 3, 4, code);
2147        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
2148    }
2149
2150    /// `flow.parallel(fa, fb)` returns a closure `() -> (fa(), fb())`.
2151    /// Implementation is sequential: each function is called in order
2152    /// and the results are packed into a 2-tuple. The spec (§11.2)
2153    /// allows the runtime to apply true parallelism here; that needs
2154    /// a thread-safe handler split and is left to a follow-up. The
2155    /// signature is what users program against — sequential vs threaded
2156    /// is an implementation detail invisible to the type system.
2157    fn emit_flow_parallel(&mut self, args: &[a::CExpr]) {
2158        // Push fa, fb; build a 0-arg trampoline closure with 2 captures.
2159        self.compile_expr(&args[0], false);
2160        self.compile_expr(&args[1], false);
2161        let nid = self.pool.node_id("n_flow_parallel");
2162        let code = vec![
2163            // Locals: [fa=0, fb=1]
2164            Op::LoadLocal(0),                                  // push fa
2165            Op::CallClosure { arity: 0, node_id_idx: nid },    // a = fa()
2166            Op::LoadLocal(1),                                  // push fb
2167            Op::CallClosure { arity: 0, node_id_idx: nid },    // b = fb()
2168            Op::MakeTuple(2),                                  // (a, b)
2169            Op::Return,
2170        ];
2171        let fn_id = self.install_trampoline("__flow_parallel", 2, 2, code);
2172        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
2173    }
2174
2175    /// `flow.parallel_list(actions)` runs each 0-arg closure in `actions`
2176    /// and returns the results as a list in input order. Variadic
2177    /// counterpart to `flow.parallel`. Sequential under the hood — the
2178    /// spec (§11.2) reserves true threading for a future scheduler.
2179    /// Compiled inline (mirrors `list.map`) so closure args can flow
2180    /// through `CallClosure` without a heap-allocated trampoline.
2181    fn emit_flow_parallel_list(&mut self, args: &[a::CExpr]) {
2182        // xs := actions
2183        self.compile_expr(&args[0], false);
2184        let xs = self.alloc_local("__fpl_xs");
2185        self.emit(Op::StoreLocal(xs));
2186
2187        // out := []
2188        self.emit(Op::MakeList(0));
2189        let out = self.alloc_local("__fpl_out");
2190        self.emit(Op::StoreLocal(out));
2191
2192        // i := 0
2193        let zero = self.pool.int(0);
2194        self.emit(Op::PushConst(zero));
2195        let i = self.alloc_local("__fpl_i");
2196        self.emit(Op::StoreLocal(i));
2197
2198        // loop_top: while i < len(xs) { ... }
2199        let loop_top = self.code.len();
2200        self.emit(Op::LoadLocal(i));
2201        self.emit(Op::LoadLocal(xs));
2202        self.emit(Op::GetListLen);
2203        self.emit(Op::IntLt);
2204        let j_exit = self.code.len();
2205        self.emit(Op::JumpIfNot(0));
2206
2207        // body: out := out ++ [xs[i]()]
2208        let nid = self.pool.node_id("n_flow_parallel_list");
2209        self.emit(Op::LoadLocal(out));
2210        self.emit(Op::LoadLocal(xs));
2211        self.emit(Op::LoadLocal(i));
2212        self.emit(Op::GetListElemDyn);
2213        self.emit(Op::CallClosure { arity: 0, node_id_idx: nid });
2214        self.emit(Op::ListAppend);
2215        self.emit(Op::StoreLocal(out));
2216
2217        // i := i + 1
2218        self.emit(Op::LoadLocal(i));
2219        let one = self.pool.int(1);
2220        self.emit(Op::PushConst(one));
2221        self.emit(Op::IntAdd);
2222        self.emit(Op::StoreLocal(i));
2223
2224        // jump back
2225        let jump_back = self.code.len();
2226        let back = (loop_top as i32) - (jump_back as i32 + 1);
2227        self.emit(Op::Jump(back));
2228
2229        // exit: patch j_exit, push out
2230        let exit_target = self.code.len() as i32;
2231        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
2232            *off = exit_target - (j_exit as i32 + 1);
2233        }
2234        self.emit(Op::LoadLocal(out));
2235    }
2236
2237    /// `flow.branch(cond, t, f)` returns a closure `(x) -> if cond(x) then t(x) else f(x)`.
2238    fn emit_flow_branch(&mut self, args: &[a::CExpr]) {
2239        self.compile_expr(&args[0], false);
2240        self.compile_expr(&args[1], false);
2241        self.compile_expr(&args[2], false);
2242        let nid = self.pool.node_id("n_flow_branch");
2243        let mut code = vec![
2244            // Locals: [cond=0, t=1, f=2, x=3]
2245            Op::LoadLocal(0),                               // push cond
2246            Op::LoadLocal(3),                               // push x
2247            Op::CallClosure { arity: 1, node_id_idx: nid }, // bool
2248        ];
2249        let j_false = code.len();
2250        code.push(Op::JumpIfNot(0));                        // patched
2251        // true arm: t(x)
2252        code.push(Op::LoadLocal(1));
2253        code.push(Op::LoadLocal(3));
2254        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
2255        code.push(Op::Return);
2256        // false arm
2257        let false_target = code.len() as i32;
2258        if let Op::JumpIfNot(off) = &mut code[j_false] {
2259            *off = false_target - (j_false as i32 + 1);
2260        }
2261        code.push(Op::LoadLocal(2));
2262        code.push(Op::LoadLocal(3));
2263        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
2264        code.push(Op::Return);
2265
2266        let fn_id = self.install_trampoline("__flow_branch", 4, 4, code);
2267        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
2268    }
2269
2270    /// `flow.retry(f, max_attempts)` returns a closure `(x) -> Result[U, E]`
2271    /// that calls `f(x)` up to `max_attempts` times, returning the first
2272    /// `Ok` or the final `Err`.
2273    fn emit_flow_retry(&mut self, args: &[a::CExpr]) {
2274        self.compile_expr(&args[0], false);
2275        self.compile_expr(&args[1], false);
2276        let call_nid = self.pool.node_id("n_flow_retry");
2277        let ok_idx = self.pool.variant("Ok");
2278        let zero_const = self.pool.int(0);
2279        let one_const = self.pool.int(1);
2280        // Locals: [f=0, max=1, x=2, i=3, last=4]
2281        let mut code = vec![
2282            // i := 0
2283            Op::PushConst(zero_const),
2284            Op::StoreLocal(3),
2285        ];
2286        // loop_top: while i < max
2287        let loop_top = code.len() as i32;
2288        code.push(Op::LoadLocal(3));
2289        code.push(Op::LoadLocal(1));
2290        code.push(Op::IntLt);
2291        let j_done = code.len();
2292        code.push(Op::JumpIfNot(0));                       // patched
2293
2294        // body: r := f(x); last := r
2295        code.push(Op::LoadLocal(0));
2296        code.push(Op::LoadLocal(2));
2297        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
2298        code.push(Op::StoreLocal(4));
2299
2300        // Test variant Ok on last; if so, return last.
2301        code.push(Op::LoadLocal(4));
2302        code.push(Op::TestVariant(ok_idx));
2303        let j_was_err = code.len();
2304        code.push(Op::JumpIfNot(0));                       // patched: skip return
2305        code.push(Op::LoadLocal(4));
2306        code.push(Op::Return);
2307
2308        // was_err: i := i + 1; jump loop_top
2309        let was_err_target = code.len() as i32;
2310        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
2311            *off = was_err_target - (j_was_err as i32 + 1);
2312        }
2313        code.push(Op::LoadLocal(3));
2314        code.push(Op::PushConst(one_const));
2315        code.push(Op::IntAdd);
2316        code.push(Op::StoreLocal(3));
2317        let pc_after_jump = code.len() as i32 + 1;
2318        code.push(Op::Jump(loop_top - pc_after_jump));
2319
2320        // done: return last (the final Err, or Unit if max=0).
2321        let done_target = code.len() as i32;
2322        if let Op::JumpIfNot(off) = &mut code[j_done] {
2323            *off = done_target - (j_done as i32 + 1);
2324        }
2325        code.push(Op::LoadLocal(4));
2326        code.push(Op::Return);
2327
2328        let fn_id = self.install_trampoline("__flow_retry", 3, 5, code);
2329        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
2330    }
2331
2332    /// `flow.retry_with_backoff(f, attempts, base_ms)` (#226). Variant
2333    /// of `flow.retry` that sleeps between attempts. The first
2334    /// attempt fires immediately; attempt k > 1 waits `base_ms *
2335    /// 2^(k-2)` ms before retrying. Sleeps go through
2336    /// `time.sleep_ms`, which is why the resulting closure carries
2337    /// `[time]` in its effect row even though the underlying `f` is
2338    /// pure.
2339    fn emit_flow_retry_with_backoff(&mut self, args: &[a::CExpr]) {
2340        // Push captures: f, max, base_ms. The trampoline takes one
2341        // call-time arg `x`, so capture_count = 3, arity = 4.
2342        self.compile_expr(&args[0], false);
2343        self.compile_expr(&args[1], false);
2344        self.compile_expr(&args[2], false);
2345        let call_nid    = self.pool.node_id("n_flow_retry_backoff");
2346        let sleep_nid   = self.pool.node_id("n_flow_retry_backoff_sleep");
2347        let kind_idx    = self.pool.str("time");
2348        let op_idx      = self.pool.str("sleep_ms");
2349        let ok_idx      = self.pool.variant("Ok");
2350        let zero_const  = self.pool.int(0);
2351        let one_const   = self.pool.int(1);
2352        let two_const   = self.pool.int(2);
2353        // Locals layout:
2354        //   0=f, 1=max, 2=base_ms (captures)
2355        //   3=x (arg)
2356        //   4=i, 5=last, 6=next_delay (working state)
2357        let mut code = vec![
2358            // next_delay := base_ms
2359            Op::LoadLocal(2),
2360            Op::StoreLocal(6),
2361            // i := 0
2362            Op::PushConst(zero_const),
2363            Op::StoreLocal(4),
2364        ];
2365
2366        let loop_top = code.len() as i32;
2367        // while i < max
2368        code.push(Op::LoadLocal(4));
2369        code.push(Op::LoadLocal(1));
2370        code.push(Op::IntLt);
2371        let j_done = code.len();
2372        code.push(Op::JumpIfNot(0)); // patched
2373
2374        // if i > 0: time.sleep_ms(next_delay); next_delay := next_delay * 2
2375        code.push(Op::PushConst(zero_const));
2376        code.push(Op::LoadLocal(4));
2377        code.push(Op::IntLt);                // 0 < i ?
2378        let j_no_sleep = code.len();
2379        code.push(Op::JumpIfNot(0));         // patched: skip the sleep block
2380        // Sleep
2381        code.push(Op::LoadLocal(6));         // arg = next_delay
2382        code.push(Op::EffectCall {
2383            kind_idx, op_idx, arity: 1, node_id_idx: sleep_nid,
2384        });
2385        code.push(Op::Pop);                  // discard the Unit result
2386        // next_delay := next_delay * 2
2387        code.push(Op::LoadLocal(6));
2388        code.push(Op::PushConst(two_const));
2389        code.push(Op::NumMul);
2390        code.push(Op::StoreLocal(6));
2391        // patch the no-sleep skip
2392        let after_sleep = code.len() as i32;
2393        if let Op::JumpIfNot(off) = &mut code[j_no_sleep] {
2394            *off = after_sleep - (j_no_sleep as i32 + 1);
2395        }
2396
2397        // last := f(x)
2398        code.push(Op::LoadLocal(0));
2399        code.push(Op::LoadLocal(3));
2400        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
2401        code.push(Op::StoreLocal(5));
2402
2403        // if Ok(last): return last
2404        code.push(Op::LoadLocal(5));
2405        code.push(Op::TestVariant(ok_idx));
2406        let j_was_err = code.len();
2407        code.push(Op::JumpIfNot(0)); // patched
2408        code.push(Op::LoadLocal(5));
2409        code.push(Op::Return);
2410
2411        // was_err: i := i + 1; jump loop_top
2412        let was_err_target = code.len() as i32;
2413        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
2414            *off = was_err_target - (j_was_err as i32 + 1);
2415        }
2416        code.push(Op::LoadLocal(4));
2417        code.push(Op::PushConst(one_const));
2418        code.push(Op::IntAdd);
2419        code.push(Op::StoreLocal(4));
2420        let pc_after_jump = code.len() as i32 + 1;
2421        code.push(Op::Jump(loop_top - pc_after_jump));
2422
2423        // done: return last (the final Err, or Unit if max=0).
2424        let done_target = code.len() as i32;
2425        if let Op::JumpIfNot(off) = &mut code[j_done] {
2426            *off = done_target - (j_done as i32 + 1);
2427        }
2428        code.push(Op::LoadLocal(5));
2429        code.push(Op::Return);
2430
2431        let fn_id = self.install_trampoline("__flow_retry_backoff", 4, 7, code);
2432        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
2433    }
2434}