Skip to main content

lex_bytecode/
compiler.rs

1//! M4 compiler: canonical AST → bytecode.
2
3use crate::op::*;
4use crate::program::*;
5use indexmap::IndexMap;
6use lex_ast as a;
7
8#[derive(Default)]
9struct ConstPool {
10    pool: Vec<Const>,
11    fields: IndexMap<String, u32>,
12    variants: IndexMap<String, u32>,
13    node_ids: IndexMap<String, u32>,
14    ints: IndexMap<i64, u32>,
15    bools: IndexMap<u8, u32>,
16    strs: IndexMap<String, u32>,
17}
18
19impl ConstPool {
20    fn field(&mut self, name: &str) -> u32 {
21        if let Some(i) = self.fields.get(name) { return *i; }
22        let i = self.pool.len() as u32;
23        self.pool.push(Const::FieldName(name.into()));
24        self.fields.insert(name.into(), i);
25        i
26    }
27    fn variant(&mut self, name: &str) -> u32 {
28        if let Some(i) = self.variants.get(name) { return *i; }
29        let i = self.pool.len() as u32;
30        self.pool.push(Const::VariantName(name.into()));
31        self.variants.insert(name.into(), i);
32        i
33    }
34    fn node_id(&mut self, name: &str) -> u32 {
35        if let Some(i) = self.node_ids.get(name) { return *i; }
36        let i = self.pool.len() as u32;
37        self.pool.push(Const::NodeId(name.into()));
38        self.node_ids.insert(name.into(), i);
39        i
40    }
41    fn int(&mut self, n: i64) -> u32 {
42        if let Some(i) = self.ints.get(&n) { return *i; }
43        let i = self.pool.len() as u32;
44        self.pool.push(Const::Int(n));
45        self.ints.insert(n, i);
46        i
47    }
48    fn bool(&mut self, b: bool) -> u32 {
49        let key = b as u8;
50        if let Some(i) = self.bools.get(&key) { return *i; }
51        let i = self.pool.len() as u32;
52        self.pool.push(Const::Bool(b));
53        self.bools.insert(key, i);
54        i
55    }
56    fn str(&mut self, s: &str) -> u32 {
57        if let Some(i) = self.strs.get(s) { return *i; }
58        let i = self.pool.len() as u32;
59        self.pool.push(Const::Str(s.into()));
60        self.strs.insert(s.into(), i);
61        i
62    }
63    fn float(&mut self, f: f64) -> u32 {
64        // Floats: not deduped (NaN issues).
65        let i = self.pool.len() as u32;
66        self.pool.push(Const::Float(f));
67        i
68    }
69    fn unit(&mut self) -> u32 {
70        let i = self.pool.len() as u32;
71        self.pool.push(Const::Unit);
72        i
73    }
74}
75
76pub fn compile_program(stages: &[a::Stage]) -> Program {
77    let mut p = Program {
78        constants: Vec::new(),
79        functions: Vec::new(),
80        function_names: IndexMap::new(),
81        module_aliases: IndexMap::new(),
82        entry: None,
83    };
84
85    // Collect imports as alias → module-name. The module name is the part
86    // after `std.` (so `import "std.io" as io` ⇒ alias `io` → module `io`).
87    for s in stages {
88        if let a::Stage::Import(i) = s {
89            let module = i.reference.strip_prefix("std.").unwrap_or(&i.reference).to_string();
90            p.module_aliases.insert(i.alias.clone(), module);
91        }
92    }
93
94    for s in stages {
95        if let a::Stage::FnDecl(fd) = s {
96            let idx = p.functions.len() as u32;
97            p.function_names.insert(fd.name.clone(), idx);
98            p.functions.push(Function {
99                name: fd.name.clone(),
100                arity: fd.params.len() as u16,
101                locals_count: 0,
102                code: Vec::new(),
103                effects: fd.effects.iter().map(|e| DeclaredEffect {
104                    kind: e.name.clone(),
105                    arg: e.arg.as_ref().map(|a| match a {
106                        a::EffectArg::Str { value } => EffectArg::Str(value.clone()),
107                        a::EffectArg::Int { value } => EffectArg::Int(*value),
108                        a::EffectArg::Ident { value } => EffectArg::Ident(value.clone()),
109                    }),
110                }).collect(),
111                // Filled in at the end of the compile pass, once `code`
112                // and `locals_count` are final. See #222.
113                body_hash: crate::program::ZERO_BODY_HASH,
114                // Per-param refinement predicates for runtime check
115                // (#209 slice 3). Lifted directly from each param's
116                // `TypeExpr::Refined` if present; `None` otherwise.
117                refinements: fd.params.iter().map(|p| match &p.ty {
118                    a::TypeExpr::Refined { binding, predicate, .. } =>
119                        Some(crate::program::Refinement {
120                            binding: binding.clone(),
121                            predicate: (**predicate).clone(),
122                        }),
123                    _ => None,
124                }).collect(),
125            });
126        }
127    }
128
129    let mut pool = ConstPool::default();
130    let function_names = p.function_names.clone();
131    let module_aliases = p.module_aliases.clone();
132    let mut pending_lambdas: Vec<PendingLambda> = Vec::new();
133
134    for s in stages {
135        if let a::Stage::FnDecl(_) = s {
136            // Build a NodeId map for *this* stage so the compiler can stamp
137            // each Call/EffectCall opcode with the originating AST node.
138            let id_map = lex_ast::expr_ids(s);
139            let fd = match s { a::Stage::FnDecl(fd) => fd, _ => unreachable!() };
140            let mut fc = FnCompiler {
141                code: Vec::new(),
142                locals: IndexMap::new(),
143                next_local: 0,
144                peak_local: 0,
145                pool: &mut pool,
146                function_names: &function_names,
147                module_aliases: &module_aliases,
148                id_map: &id_map,
149                pending_lambdas: &mut pending_lambdas,
150                next_fn_id: &mut p.functions,
151            };
152            for param in &fd.params {
153                let i = fc.next_local;
154                fc.locals.insert(param.name.clone(), i);
155                fc.next_local += 1;
156                fc.peak_local = fc.next_local;
157            }
158            fc.compile_expr(&fd.body, true);
159            fc.code.push(Op::Return);
160            let code = std::mem::take(&mut fc.code);
161            let peak = fc.peak_local;
162            drop(fc);
163            let idx = function_names[&fd.name];
164            p.functions[idx as usize].code = code;
165            p.functions[idx as usize].locals_count = peak;
166        }
167    }
168
169    // Compile pending lambdas in FIFO order. Each lambda may emit further
170    // lambdas; loop until the queue drains.
171    while let Some(pl) = pending_lambdas.pop() {
172        let id_map = std::collections::HashMap::new();
173        let mut fc = FnCompiler {
174            code: Vec::new(),
175            locals: IndexMap::new(),
176            next_local: 0,
177            peak_local: 0,
178            pool: &mut pool,
179            function_names: &function_names,
180            module_aliases: &module_aliases,
181            id_map: &id_map,
182            pending_lambdas: &mut pending_lambdas,
183            next_fn_id: &mut p.functions,
184        };
185        for name in &pl.capture_names {
186            let i = fc.next_local;
187            fc.locals.insert(name.clone(), i);
188            fc.next_local += 1;
189            fc.peak_local = fc.next_local;
190        }
191        for p in &pl.params {
192            let i = fc.next_local;
193            fc.locals.insert(p.name.clone(), i);
194            fc.next_local += 1;
195            fc.peak_local = fc.next_local;
196        }
197        fc.compile_expr(&pl.body, true);
198        fc.code.push(Op::Return);
199        let code = std::mem::take(&mut fc.code);
200        let peak = fc.peak_local;
201        drop(fc);
202        p.functions[pl.fn_id as usize].code = code;
203        p.functions[pl.fn_id as usize].locals_count = peak;
204    }
205
206    // Final pass: stamp every function with its content hash now that
207    // every body is finalized (#222). Trampolines installed via
208    // `install_trampoline` already have it; recomputing is cheap and
209    // makes the invariant easier to read at this top level.
210    for f in p.functions.iter_mut() {
211        if f.body_hash == crate::program::ZERO_BODY_HASH {
212            f.body_hash = crate::program::compute_body_hash(
213                f.arity, f.locals_count, &f.code);
214        }
215    }
216
217    p.constants = pool.pool;
218    p
219}
220
221#[derive(Debug, Clone)]
222struct PendingLambda {
223    fn_id: u32,
224    /// Names of captured outer-scope locals, in order.
225    capture_names: Vec<String>,
226    params: Vec<a::Param>,
227    body: a::CExpr,
228}
229
230struct FnCompiler<'a> {
231    code: Vec<Op>,
232    locals: IndexMap<String, u16>,
233    next_local: u16,
234    /// Peak local usage seen during compilation (for VM frame sizing).
235    peak_local: u16,
236    pool: &'a mut ConstPool,
237    function_names: &'a IndexMap<String, u32>,
238    module_aliases: &'a IndexMap<String, String>,
239    /// CExpr address → NodeId, populated per stage via `lex_ast::expr_ids`.
240    id_map: &'a std::collections::HashMap<*const a::CExpr, lex_ast::NodeId>,
241    /// Queue of lambdas discovered during compilation; each gets a fresh
242    /// fn_id and is compiled in a later pass.
243    pending_lambdas: &'a mut Vec<PendingLambda>,
244    /// Mutable view of the function table — used to allocate fn_ids for
245    /// freshly-discovered lambdas.
246    next_fn_id: &'a mut Vec<Function>,
247}
248
249impl<'a> FnCompiler<'a> {
250    fn alloc_local(&mut self, name: &str) -> u16 {
251        let i = self.next_local;
252        self.locals.insert(name.into(), i);
253        self.next_local += 1;
254        if self.next_local > self.peak_local { self.peak_local = self.next_local; }
255        i
256    }
257    fn emit(&mut self, op: Op) { self.code.push(op); }
258
259    fn compile_expr(&mut self, e: &a::CExpr, tail: bool) {
260        match e {
261            a::CExpr::Literal { value } => self.compile_lit(value),
262            a::CExpr::Var { name } => {
263                if let Some(slot) = self.locals.get(name) {
264                    self.emit(Op::LoadLocal(*slot));
265                } else if let Some(&fn_id) = self.function_names.get(name) {
266                    // Function name used as a *value* (e.g. as a record-field
267                    // initializer or fold-callback arg) — materialize it as a
268                    // closure with no captures. The runtime already accepts
269                    // `Value::Closure { fn_id, captures: vec![] }` and
270                    // `CallClosure` dispatches it. (#169)
271                    self.emit(Op::MakeClosure { fn_id, capture_count: 0 });
272                } else {
273                    // Should be caught at type-check time; the type checker
274                    // walks every Var. If we land here it's a compiler bug,
275                    // not a user typo.
276                    panic!("unknown var in compiler: {name}");
277                }
278            }
279            a::CExpr::Let { name, ty: _, value, body } => {
280                self.compile_expr(value, false);
281                let slot = self.alloc_local(name);
282                self.emit(Op::StoreLocal(slot));
283                self.compile_expr(body, tail);
284            }
285            a::CExpr::Block { statements, result } => {
286                for s in statements {
287                    self.compile_expr(s, false);
288                    self.emit(Op::Pop);
289                }
290                self.compile_expr(result, tail);
291            }
292            a::CExpr::Call { callee, args } => self.compile_call(e, callee, args, tail),
293            a::CExpr::Constructor { name, args } => {
294                for a in args { self.compile_expr(a, false); }
295                let name_idx = self.pool.variant(name);
296                self.emit(Op::MakeVariant { name_idx, arity: args.len() as u16 });
297            }
298            a::CExpr::Match { scrutinee, arms } => self.compile_match(scrutinee, arms, tail),
299            a::CExpr::RecordLit { fields } => {
300                let mut idxs = Vec::with_capacity(fields.len());
301                for f in fields {
302                    self.compile_expr(&f.value, false);
303                    idxs.push(self.pool.field(&f.name));
304                }
305                self.emit(Op::MakeRecord { field_name_indices: idxs });
306            }
307            a::CExpr::TupleLit { items } => {
308                for it in items { self.compile_expr(it, false); }
309                self.emit(Op::MakeTuple(items.len() as u16));
310            }
311            a::CExpr::ListLit { items } => {
312                for it in items { self.compile_expr(it, false); }
313                self.emit(Op::MakeList(items.len() as u32));
314            }
315            a::CExpr::FieldAccess { value, field } => {
316                self.compile_expr(value, false);
317                let idx = self.pool.field(field);
318                self.emit(Op::GetField(idx));
319            }
320            a::CExpr::BinOp { op, lhs, rhs } => self.compile_binop(op, lhs, rhs),
321            a::CExpr::UnaryOp { op, expr } => {
322                self.compile_expr(expr, false);
323                match op.as_str() {
324                    "-" => self.emit(Op::NumNeg),
325                    "not" => self.emit(Op::BoolNot),
326                    other => panic!("unknown unary: {other}"),
327                }
328            }
329            a::CExpr::Lambda { params, body, .. } => self.compile_lambda(params, body),
330            a::CExpr::Return { value } => {
331                self.compile_expr(value, true);
332                self.emit(Op::Return);
333            }
334        }
335    }
336
337    fn compile_lit(&mut self, l: &a::CLit) {
338        let i = match l {
339            a::CLit::Int { value } => self.pool.int(*value),
340            a::CLit::Bool { value } => self.pool.bool(*value),
341            a::CLit::Float { value } => {
342                let f: f64 = value.parse().unwrap_or(0.0);
343                self.pool.float(f)
344            }
345            a::CLit::Str { value } => self.pool.str(value),
346            a::CLit::Bytes { value: _ } => {
347                // Stub: M4 doesn't use bytes literals in §3.13 examples.
348                let i = self.pool.pool.len() as u32;
349                self.pool.pool.push(Const::Bytes(Vec::new()));
350                i
351            }
352            a::CLit::Unit => self.pool.unit(),
353        };
354        self.emit(Op::PushConst(i));
355    }
356
357    fn compile_call(&mut self, call_expr: &a::CExpr, callee: &a::CExpr, args: &[a::CExpr], tail: bool) {
358        let node_id = self
359            .id_map
360            .get(&(call_expr as *const a::CExpr))
361            .map(|n| n.as_str().to_string())
362            .unwrap_or_else(|| "n_?".into());
363        let node_id_idx = self.pool.node_id(&node_id);
364
365        // Module function call: `alias.op(args)` where `alias` is an imported
366        // module ⇒ EffectCall, except for higher-order pure ops where we
367        // emit inline bytecode using CallClosure (the closure-arg can't be
368        // serialized through the effect handler).
369        if let a::CExpr::FieldAccess { value, field } = callee {
370            if let a::CExpr::Var { name } = value.as_ref() {
371                if let Some(module) = self.module_aliases.get(name) {
372                    if self.try_emit_higher_order(module, field, args, node_id_idx) {
373                        let _ = tail;
374                        return;
375                    }
376                    for a in args { self.compile_expr(a, false); }
377                    let kind_idx = self.pool.str(module);
378                    let op_idx = self.pool.str(field);
379                    self.emit(Op::EffectCall {
380                        kind_idx,
381                        op_idx,
382                        arity: args.len() as u16,
383                        node_id_idx,
384                    });
385                    let _ = tail;
386                    return;
387                }
388            }
389        }
390        match callee {
391            a::CExpr::Var { name } if self.function_names.contains_key(name) => {
392                for a in args { self.compile_expr(a, false); }
393                let fn_id = self.function_names[name];
394                if tail {
395                    self.emit(Op::TailCall { fn_id, arity: args.len() as u16, node_id_idx });
396                } else {
397                    self.emit(Op::Call { fn_id, arity: args.len() as u16, node_id_idx });
398                }
399            }
400            a::CExpr::Var { name } if self.locals.contains_key(name) => {
401                // First-class function value bound to a local. Push the
402                // closure, then args, then CallClosure.
403                let slot = self.locals[name];
404                self.emit(Op::LoadLocal(slot));
405                for a in args { self.compile_expr(a, false); }
406                self.emit(Op::CallClosure { arity: args.len() as u16, node_id_idx });
407            }
408            // Lambda directly applied — push closure + args + CallClosure.
409            other => {
410                self.compile_expr(other, false);
411                for a in args { self.compile_expr(a, false); }
412                self.emit(Op::CallClosure { arity: args.len() as u16, node_id_idx });
413            }
414        }
415    }
416
417    fn compile_binop(&mut self, op: &str, lhs: &a::CExpr, rhs: &a::CExpr) {
418        self.compile_expr(lhs, false);
419        self.compile_expr(rhs, false);
420        match op {
421            "+" => self.emit(Op::NumAdd),
422            "-" => self.emit(Op::NumSub),
423            "*" => self.emit(Op::NumMul),
424            "/" => self.emit(Op::NumDiv),
425            "%" => self.emit(Op::NumMod),
426            "==" => self.emit(Op::NumEq),
427            "!=" => { self.emit(Op::NumEq); self.emit(Op::BoolNot); }
428            "<" => self.emit(Op::NumLt),
429            "<=" => self.emit(Op::NumLe),
430            ">" => { self.emit_swap_top2(); self.emit(Op::NumLt); }
431            ">=" => { self.emit_swap_top2(); self.emit(Op::NumLe); }
432            "and" => self.emit(Op::BoolAnd),
433            "or" => self.emit(Op::BoolOr),
434            other => panic!("unknown binop: {other:?}"),
435        }
436    }
437
438    fn emit_swap_top2(&mut self) {
439        let a = self.alloc_local("__swap_a");
440        let b = self.alloc_local("__swap_b");
441        self.emit(Op::StoreLocal(b));
442        self.emit(Op::StoreLocal(a));
443        self.emit(Op::LoadLocal(b));
444        self.emit(Op::LoadLocal(a));
445    }
446
447    fn compile_match(&mut self, scrutinee: &a::CExpr, arms: &[a::Arm], tail: bool) {
448        self.compile_expr(scrutinee, false);
449        let scrut_slot = self.alloc_local("__scrut");
450        self.emit(Op::StoreLocal(scrut_slot));
451
452        let mut end_jumps: Vec<usize> = Vec::new();
453        for arm in arms {
454            let arm_start_locals = self.next_local;
455            let arm_start_locals_map = self.locals.clone();
456
457            self.emit(Op::LoadLocal(scrut_slot));
458            let mut bindings: Vec<(String, u16)> = Vec::new();
459            let fail_jumps: Vec<usize> = self.compile_pattern_test(&arm.pattern, &mut bindings);
460
461            self.compile_expr(&arm.body, tail);
462            let j_end = self.code.len();
463            self.emit(Op::Jump(0));
464            end_jumps.push(j_end);
465
466            let fail_target = self.code.len() as i32;
467            for j in fail_jumps {
468                // #337: PConstructor patterns now register an
469                // unconditional `Op::Jump` for the failure path
470                // (alongside the existing `Op::JumpIfNot` from
471                // PLiteral / nested constructor tests). Patch
472                // either shape.
473                match &mut self.code[j] {
474                    Op::JumpIfNot(off) => *off = fail_target - (j as i32 + 1),
475                    Op::Jump(off)      => *off = fail_target - (j as i32 + 1),
476                    _ => {}
477                }
478            }
479            self.next_local = arm_start_locals;
480            self.locals = arm_start_locals_map;
481        }
482        let panic_msg_idx = self.pool.str("non-exhaustive match");
483        self.emit(Op::Panic(panic_msg_idx));
484
485        let end_target = self.code.len() as i32;
486        for j in end_jumps {
487            if let Op::Jump(off) = &mut self.code[j] {
488                *off = end_target - (j as i32 + 1);
489            }
490        }
491    }
492
493    fn compile_pattern_test(&mut self, p: &a::Pattern, bindings: &mut Vec<(String, u16)>) -> Vec<usize> {
494        let mut fails = Vec::new();
495        match p {
496            a::Pattern::PWild => { self.emit(Op::Pop); }
497            a::Pattern::PVar { name } => {
498                let slot = self.alloc_local(name);
499                self.emit(Op::StoreLocal(slot));
500                bindings.push((name.clone(), slot));
501            }
502            a::Pattern::PLiteral { value } => {
503                self.compile_lit(value);
504                match value {
505                    a::CLit::Str { .. } => self.emit(Op::StrEq),
506                    a::CLit::Bytes { .. } => self.emit(Op::BytesEq),
507                    _ => self.emit(Op::NumEq),
508                }
509                let j = self.code.len();
510                self.emit(Op::JumpIfNot(0));
511                fails.push(j);
512            }
513            a::Pattern::PConstructor { name, args } => {
514                let name_idx = self.pool.variant(name);
515                // #337: the failure path must drop the duplicated
516                // scrutinee so subsequent match arms see a clean
517                // stack. The previous shape
518                //   Dup; TestVariant; JumpIfNot(fail);
519                // left `[scrut]` on the stack at the fail target,
520                // poisoning later arms — e.g. a wildcard `_` arm
521                // whose body referenced an unrelated value would
522                // pop the leaked scrutinee instead of its own value.
523                //
524                // New shape: branch on success, fall through to a
525                // failure cleanup that pops the dup'd scrutinee
526                // before jumping. The registered fail-jump is an
527                // unconditional `Op::Jump`; `compile_match`'s patch
528                // loop accepts both `JumpIfNot` and `Jump`.
529                self.emit(Op::Dup);                   // [scrut, scrut]
530                self.emit(Op::TestVariant(name_idx)); // [scrut, Bool]
531                let j_success = self.code.len();
532                self.emit(Op::JumpIf(0));             // pop Bool. success → [scrut]
533                self.emit(Op::Pop);                   // failure cleanup: [scrut] → []
534                let j_fail = self.code.len();
535                self.emit(Op::Jump(0));               // → fail target with []
536                fails.push(j_fail);
537                let success_target = self.code.len() as i32;
538                if let Op::JumpIf(off) = &mut self.code[j_success] {
539                    *off = success_target - (j_success as i32 + 1);
540                }
541                if args.is_empty() {
542                    self.emit(Op::Pop);
543                } else if args.len() == 1 {
544                    self.emit(Op::GetVariantArg(0));
545                    let sub_fails = self.compile_pattern_test(&args[0], bindings);
546                    fails.extend(sub_fails);
547                } else {
548                    let slot = self.alloc_local("__variant");
549                    self.emit(Op::StoreLocal(slot));
550                    for (i, arg) in args.iter().enumerate() {
551                        self.emit(Op::LoadLocal(slot));
552                        self.emit(Op::GetVariantArg(i as u16));
553                        let sub_fails = self.compile_pattern_test(arg, bindings);
554                        fails.extend(sub_fails);
555                    }
556                }
557            }
558            a::Pattern::PRecord { fields } => {
559                let slot = self.alloc_local("__record");
560                self.emit(Op::StoreLocal(slot));
561                for f in fields {
562                    self.emit(Op::LoadLocal(slot));
563                    let fi = self.pool.field(&f.name);
564                    self.emit(Op::GetField(fi));
565                    let sub_fails = self.compile_pattern_test(&f.pattern, bindings);
566                    fails.extend(sub_fails);
567                }
568            }
569            a::Pattern::PTuple { items } => {
570                let slot = self.alloc_local("__tuple");
571                self.emit(Op::StoreLocal(slot));
572                for (i, item) in items.iter().enumerate() {
573                    self.emit(Op::LoadLocal(slot));
574                    self.emit(Op::GetElem(i as u16));
575                    let sub_fails = self.compile_pattern_test(item, bindings);
576                    fails.extend(sub_fails);
577                }
578            }
579        }
580        fails
581    }
582
583    /// Compile a Lambda: collect free variables that resolve to outer-scope
584    /// locals, register a synthetic function, emit MakeClosure with the
585    /// captured values pushed in order.
586    fn compile_lambda(&mut self, params: &[a::Param], body: &a::CExpr) {
587        // Free vars = vars referenced in body that aren't bound locally.
588        let mut bound: std::collections::HashSet<String> = params.iter().map(|p| p.name.clone()).collect();
589        let mut frees: Vec<String> = Vec::new();
590        free_vars(body, &mut bound, &mut frees);
591
592        // Filter to those that are in the enclosing locals (captures).
593        // Don't exclude names that *also* exist in `function_names`:
594        // if the name is in `locals`, the local shadows the global
595        // within this scope, and the lambda needs to capture the
596        // local's value, not the global fn. (#339) Names that are
597        // ONLY in `function_names` (no local) stay external — the
598        // lambda's body resolves them at call time, same as the
599        // enclosing fn would.
600        let captures: Vec<String> = frees.into_iter()
601            .filter(|n| self.locals.contains_key(n))
602            .collect();
603
604        // Allocate a fresh fn_id by appending a placeholder Function.
605        let fn_id = self.next_fn_id.len() as u32;
606        self.next_fn_id.push(Function {
607            name: format!("__lambda_{fn_id}"),
608            arity: (captures.len() + params.len()) as u16,
609            locals_count: 0,
610            code: Vec::new(),
611            effects: Vec::new(),
612            // See #222: filled in at the end of the compile pass.
613            body_hash: crate::program::ZERO_BODY_HASH,
614            // Lambdas don't carry refinements at the surface today
615            // (closure params don't accept `Type{x | ...}` syntax in
616            // the parser). #209 stays focused on top-level fn decls;
617            // closure-param refinements are a follow-up.
618            refinements: Vec::new(),
619        });
620
621        // Emit code at the lambda site: load each captured local, then MakeClosure.
622        for c in &captures {
623            let slot = *self.locals.get(c).expect("free var must be in scope");
624            self.emit(Op::LoadLocal(slot));
625        }
626        self.emit(Op::MakeClosure { fn_id, capture_count: captures.len() as u16 });
627
628        // Queue the body for later compilation.
629        self.pending_lambdas.push(PendingLambda {
630            fn_id,
631            capture_names: captures,
632            params: params.to_vec(),
633            body: body.clone(),
634        });
635    }
636
637    /// Higher-order stdlib ops on Result/Option whose function arg is a
638    /// closure. Emit inline: pattern-match on the variant, invoke the
639    /// closure when applicable, return wrapped result.
640    fn try_emit_higher_order(
641        &mut self,
642        module: &str,
643        op: &str,
644        args: &[a::CExpr],
645        node_id_idx: u32,
646    ) -> bool {
647        match (module, op) {
648            ("result", "map") => self.emit_variant_map(args, "Ok", true),
649            ("result", "and_then") => self.emit_variant_map(args, "Ok", false),
650            ("result", "map_err") => self.emit_variant_map(args, "Err", true),
651            ("result", "or_else") => self.emit_variant_or_else(args, "Err", 1),
652            ("option", "map") => self.emit_variant_map(args, "Some", true),
653            ("option", "and_then") => self.emit_variant_map(args, "Some", false),
654            ("option", "or_else") => self.emit_variant_or_else(args, "None", 0),
655            ("option", "unwrap_or_else") => self.emit_option_unwrap_or_else(args),
656            ("list", "map") => self.emit_list_map(args),
657            ("list", "par_map") => self.emit_list_par_map(args),
658            ("list", "sort_by") => self.emit_list_sort_by(args),
659            ("list", "filter") => self.emit_list_filter(args),
660            ("list", "fold") => self.emit_list_fold(args),
661            ("iter", "from_list") => self.emit_iter_from_list(args),
662            ("iter", "next")      => self.emit_iter_next(args),
663            ("iter", "is_empty")  => self.emit_iter_is_empty(args),
664            ("iter", "count")     => self.emit_iter_count(args),
665            ("iter", "take")      => self.emit_iter_take(args),
666            ("iter", "skip")      => self.emit_iter_skip(args),
667            ("iter", "to_list")   => self.emit_iter_to_list(args),
668            ("iter", "map")       => self.emit_iter_map(args),
669            ("iter", "filter")    => self.emit_iter_filter(args),
670            ("iter", "fold")      => self.emit_iter_fold(args),
671            ("map", "fold") => self.emit_map_fold(args, node_id_idx),
672            ("flow", "sequential") => self.emit_flow_sequential(args),
673            ("flow", "branch") => self.emit_flow_branch(args),
674            ("flow", "retry") => self.emit_flow_retry(args),
675            ("flow", "retry_with_backoff") => self.emit_flow_retry_with_backoff(args),
676            ("flow", "parallel") => self.emit_flow_parallel(args),
677            ("flow", "parallel_list") => self.emit_flow_parallel_list(args),
678            _ => return false,
679        }
680        true
681    }
682
683    /// `list.map(xs, f)` — inline loop applying `f` to each element.
684    /// Stack contract: pushes the resulting List.
685    fn emit_list_map(&mut self, args: &[a::CExpr]) {
686        // Compile xs and f, store both as locals.
687        self.compile_expr(&args[0], false);
688        let xs = self.alloc_local("__lm_xs");
689        self.emit(Op::StoreLocal(xs));
690        self.compile_expr(&args[1], false);
691        let f = self.alloc_local("__lm_f");
692        self.emit(Op::StoreLocal(f));
693
694        // out := []
695        self.emit(Op::MakeList(0));
696        let out = self.alloc_local("__lm_out");
697        self.emit(Op::StoreLocal(out));
698
699        // i := 0
700        let zero = self.pool.int(0);
701        self.emit(Op::PushConst(zero));
702        let i = self.alloc_local("__lm_i");
703        self.emit(Op::StoreLocal(i));
704
705        // loop_top: while i < len(xs) { ... }
706        let loop_top = self.code.len();
707        self.emit(Op::LoadLocal(i));
708        self.emit(Op::LoadLocal(xs));
709        self.emit(Op::GetListLen);
710        self.emit(Op::NumLt);
711        let j_exit = self.code.len();
712        self.emit(Op::JumpIfNot(0));
713
714        // body: out := out ++ [f(xs[i])]
715        let nid = self.pool.node_id("n_list_map");
716        self.emit(Op::LoadLocal(out));
717        self.emit(Op::LoadLocal(f));
718        self.emit(Op::LoadLocal(xs));
719        self.emit(Op::LoadLocal(i));
720        self.emit(Op::GetListElemDyn);
721        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
722        self.emit(Op::ListAppend);
723        self.emit(Op::StoreLocal(out));
724
725        // i := i + 1
726        self.emit(Op::LoadLocal(i));
727        let one = self.pool.int(1);
728        self.emit(Op::PushConst(one));
729        self.emit(Op::NumAdd);
730        self.emit(Op::StoreLocal(i));
731
732        // jump back
733        let jump_back = self.code.len();
734        let back = (loop_top as i32) - (jump_back as i32 + 1);
735        self.emit(Op::Jump(back));
736
737        // exit: patch j_exit, push out
738        let exit_target = self.code.len() as i32;
739        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
740            *off = exit_target - (j_exit as i32 + 1);
741        }
742        self.emit(Op::LoadLocal(out));
743    }
744
745    /// `list.par_map(xs, f)` (#305 slice 1). Pushes `xs` and `f`,
746    /// then emits a single `Op::ParallelMap` — the VM applies `f`
747    /// to each element on OS-thread tasks, capped by
748    /// `LEX_PAR_MAX_CONCURRENCY`. Returns the result list in input
749    /// order.
750    fn emit_list_par_map(&mut self, args: &[a::CExpr]) {
751        self.compile_expr(&args[0], false);
752        self.compile_expr(&args[1], false);
753        let nid = self.pool.node_id("n_list_par_map");
754        self.emit(Op::ParallelMap { node_id_idx: nid });
755    }
756
757    /// `list.sort_by(xs, f)` (#338). Pushes `xs` and the key-fn
758    /// `f`, then emits a single `Op::SortByKey` — the VM invokes
759    /// `f` on each element to derive a sortable key, stable-sorts
760    /// by key, and returns the values in sorted order. Keys must
761    /// resolve to `Int` / `Float` / `Str`; mixed-type pairs are
762    /// treated as equal by the comparator (preserving insertion
763    /// order via the stable sort).
764    fn emit_list_sort_by(&mut self, args: &[a::CExpr]) {
765        self.compile_expr(&args[0], false);
766        self.compile_expr(&args[1], false);
767        let nid = self.pool.node_id("n_list_sort_by");
768        self.emit(Op::SortByKey { node_id_idx: nid });
769    }
770
771    /// `list.filter(xs, pred)` — keep elements where pred returns true.
772    fn emit_list_filter(&mut self, args: &[a::CExpr]) {
773        self.compile_expr(&args[0], false);
774        let xs = self.alloc_local("__lf_xs");
775        self.emit(Op::StoreLocal(xs));
776        self.compile_expr(&args[1], false);
777        let f = self.alloc_local("__lf_f");
778        self.emit(Op::StoreLocal(f));
779
780        self.emit(Op::MakeList(0));
781        let out = self.alloc_local("__lf_out");
782        self.emit(Op::StoreLocal(out));
783
784        let zero = self.pool.int(0);
785        self.emit(Op::PushConst(zero));
786        let i = self.alloc_local("__lf_i");
787        self.emit(Op::StoreLocal(i));
788
789        let loop_top = self.code.len();
790        self.emit(Op::LoadLocal(i));
791        self.emit(Op::LoadLocal(xs));
792        self.emit(Op::GetListLen);
793        self.emit(Op::NumLt);
794        let j_exit = self.code.len();
795        self.emit(Op::JumpIfNot(0));
796
797        // x := xs[i]
798        self.emit(Op::LoadLocal(xs));
799        self.emit(Op::LoadLocal(i));
800        self.emit(Op::GetListElemDyn);
801        let x = self.alloc_local("__lf_x");
802        self.emit(Op::StoreLocal(x));
803
804        // if pred(x) { out := out ++ [x] }
805        let nid = self.pool.node_id("n_list_filter");
806        self.emit(Op::LoadLocal(f));
807        self.emit(Op::LoadLocal(x));
808        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
809        let j_skip = self.code.len();
810        self.emit(Op::JumpIfNot(0));
811
812        self.emit(Op::LoadLocal(out));
813        self.emit(Op::LoadLocal(x));
814        self.emit(Op::ListAppend);
815        self.emit(Op::StoreLocal(out));
816
817        let skip_target = self.code.len() as i32;
818        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
819            *off = skip_target - (j_skip as i32 + 1);
820        }
821
822        // i := i + 1
823        self.emit(Op::LoadLocal(i));
824        let one = self.pool.int(1);
825        self.emit(Op::PushConst(one));
826        self.emit(Op::NumAdd);
827        self.emit(Op::StoreLocal(i));
828
829        let jump_back = self.code.len();
830        let back = (loop_top as i32) - (jump_back as i32 + 1);
831        self.emit(Op::Jump(back));
832
833        let exit_target = self.code.len() as i32;
834        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
835            *off = exit_target - (j_exit as i32 + 1);
836        }
837        self.emit(Op::LoadLocal(out));
838    }
839
840    /// `list.fold(xs, init, f)` — left fold with two-arg combiner.
841    fn emit_list_fold(&mut self, args: &[a::CExpr]) {
842        // args: xs, init, f
843        self.compile_expr(&args[0], false);
844        let xs = self.alloc_local("__lo_xs");
845        self.emit(Op::StoreLocal(xs));
846        self.compile_expr(&args[1], false);
847        let acc = self.alloc_local("__lo_acc");
848        self.emit(Op::StoreLocal(acc));
849        self.compile_expr(&args[2], false);
850        let f = self.alloc_local("__lo_f");
851        self.emit(Op::StoreLocal(f));
852
853        let zero = self.pool.int(0);
854        self.emit(Op::PushConst(zero));
855        let i = self.alloc_local("__lo_i");
856        self.emit(Op::StoreLocal(i));
857
858        let loop_top = self.code.len();
859        self.emit(Op::LoadLocal(i));
860        self.emit(Op::LoadLocal(xs));
861        self.emit(Op::GetListLen);
862        self.emit(Op::NumLt);
863        let j_exit = self.code.len();
864        self.emit(Op::JumpIfNot(0));
865
866        // acc := f(acc, xs[i])
867        let nid = self.pool.node_id("n_list_fold");
868        self.emit(Op::LoadLocal(f));
869        self.emit(Op::LoadLocal(acc));
870        self.emit(Op::LoadLocal(xs));
871        self.emit(Op::LoadLocal(i));
872        self.emit(Op::GetListElemDyn);
873        self.emit(Op::CallClosure { arity: 2, node_id_idx: nid });
874        self.emit(Op::StoreLocal(acc));
875
876        // i := i + 1
877        self.emit(Op::LoadLocal(i));
878        let one = self.pool.int(1);
879        self.emit(Op::PushConst(one));
880        self.emit(Op::NumAdd);
881        self.emit(Op::StoreLocal(i));
882
883        let jump_back = self.code.len();
884        let back = (loop_top as i32) - (jump_back as i32 + 1);
885        self.emit(Op::Jump(back));
886
887        let exit_target = self.code.len() as i32;
888        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
889            *off = exit_target - (j_exit as i32 + 1);
890        }
891        self.emit(Op::LoadLocal(acc));
892    }
893
894    // ── Iter[T] operations (#364) ─────────────────────────────────────────
895    // Internal representation: Value::Tuple([list, index]) where list is the
896    // backing List[T] and index is the current cursor (Int). All operations
897    // are compiler-inlined; no runtime effect dispatch is needed.
898
899    /// `iter.from_list(xs)` — wrap a list in an iterator at position 0.
900    fn emit_iter_from_list(&mut self, args: &[a::CExpr]) {
901        self.compile_expr(&args[0], false);
902        let zero = self.pool.int(0);
903        self.emit(Op::PushConst(zero));
904        self.emit(Op::MakeTuple(2));
905    }
906
907    /// `iter.next(it)` — advance one step; returns `Option[(T, Iter[T])]`.
908    fn emit_iter_next(&mut self, args: &[a::CExpr]) {
909        self.compile_expr(&args[0], false);
910        let it   = self.alloc_local("__in_it");
911        self.emit(Op::StoreLocal(it));
912
913        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(0));
914        let list = self.alloc_local("__in_list");
915        self.emit(Op::StoreLocal(list));
916
917        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(1));
918        let idx  = self.alloc_local("__in_idx");
919        self.emit(Op::StoreLocal(idx));
920
921        // condition: idx < len(list)
922        self.emit(Op::LoadLocal(idx));
923        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
924        self.emit(Op::NumLt);
925        let j_else = self.code.len();
926        self.emit(Op::JumpIfNot(0));
927
928        // Some((item, new_iter)):
929        self.emit(Op::LoadLocal(list));
930        self.emit(Op::LoadLocal(idx));
931        self.emit(Op::GetListElemDyn);              // item
932
933        self.emit(Op::LoadLocal(list));
934        self.emit(Op::LoadLocal(idx));
935        let one = self.pool.int(1);
936        self.emit(Op::PushConst(one));
937        self.emit(Op::NumAdd);
938        self.emit(Op::MakeTuple(2));                // new Iter = (list, idx+1)
939
940        self.emit(Op::MakeTuple(2));                // (item, new_iter)
941        let some_idx = self.pool.variant("Some");
942        self.emit(Op::MakeVariant { name_idx: some_idx, arity: 1 });
943        let j_end = self.code.len();
944        self.emit(Op::Jump(0));
945
946        // None:
947        let else_t = self.code.len() as i32;
948        if let Op::JumpIfNot(off) = &mut self.code[j_else] {
949            *off = else_t - (j_else as i32 + 1);
950        }
951        let none_idx = self.pool.variant("None");
952        self.emit(Op::MakeVariant { name_idx: none_idx, arity: 0 });
953
954        let end_t = self.code.len() as i32;
955        if let Op::Jump(off) = &mut self.code[j_end] {
956            *off = end_t - (j_end as i32 + 1);
957        }
958    }
959
960    /// `iter.is_empty(it)` — true iff the cursor is at or past the end.
961    fn emit_iter_is_empty(&mut self, args: &[a::CExpr]) {
962        self.compile_expr(&args[0], false);
963        let it = self.alloc_local("__ie_it");
964        self.emit(Op::StoreLocal(it));
965
966        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(1)); // idx
967        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(0)); // list
968        self.emit(Op::GetListLen);                               // len
969        self.emit(Op::NumLt);                                    // idx < len
970        self.emit(Op::BoolNot);                                  // NOT(idx < len)
971    }
972
973    /// `iter.count(it)` — number of remaining elements.
974    fn emit_iter_count(&mut self, args: &[a::CExpr]) {
975        self.compile_expr(&args[0], false);
976        let it = self.alloc_local("__ic_it");
977        self.emit(Op::StoreLocal(it));
978
979        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(0));
980        self.emit(Op::GetListLen);                  // len
981        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(1)); // idx
982        self.emit(Op::NumSub);                      // len - idx
983    }
984
985    /// `iter.take(it, n)` — collect up to n elements, return as new Iter.
986    fn emit_iter_take(&mut self, args: &[a::CExpr]) {
987        self.compile_expr(&args[0], false);
988        let it   = self.alloc_local("__itk_it");
989        self.emit(Op::StoreLocal(it));
990
991        self.compile_expr(&args[1], false);
992        let n    = self.alloc_local("__itk_n");
993        self.emit(Op::StoreLocal(n));
994
995        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(0));
996        let list = self.alloc_local("__itk_list");
997        self.emit(Op::StoreLocal(list));
998
999        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(1));
1000        let i    = self.alloc_local("__itk_i");
1001        self.emit(Op::StoreLocal(i));
1002
1003        self.emit(Op::MakeList(0));
1004        let out  = self.alloc_local("__itk_out");
1005        self.emit(Op::StoreLocal(out));
1006
1007        let zero = self.pool.int(0);
1008        self.emit(Op::PushConst(zero));
1009        let cnt  = self.alloc_local("__itk_cnt");
1010        self.emit(Op::StoreLocal(cnt));
1011
1012        let loop_top = self.code.len();
1013
1014        // while cnt < n
1015        self.emit(Op::LoadLocal(cnt));
1016        self.emit(Op::LoadLocal(n));
1017        self.emit(Op::NumLt);
1018        let j_exit_n = self.code.len();
1019        self.emit(Op::JumpIfNot(0));
1020
1021        // AND i < len(list)
1022        self.emit(Op::LoadLocal(i));
1023        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1024        self.emit(Op::NumLt);
1025        let j_exit_l = self.code.len();
1026        self.emit(Op::JumpIfNot(0));
1027
1028        // out = out ++ [list[i]]
1029        self.emit(Op::LoadLocal(out));
1030        self.emit(Op::LoadLocal(list));
1031        self.emit(Op::LoadLocal(i));
1032        self.emit(Op::GetListElemDyn);
1033        self.emit(Op::ListAppend);
1034        self.emit(Op::StoreLocal(out));
1035
1036        let one = self.pool.int(1);
1037        // i = i + 1
1038        self.emit(Op::LoadLocal(i));
1039        self.emit(Op::PushConst(one));
1040        self.emit(Op::NumAdd);
1041        self.emit(Op::StoreLocal(i));
1042        // cnt = cnt + 1
1043        self.emit(Op::LoadLocal(cnt));
1044        self.emit(Op::PushConst(one));
1045        self.emit(Op::NumAdd);
1046        self.emit(Op::StoreLocal(cnt));
1047
1048        let jback = self.code.len();
1049        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1050
1051        let exit_t = self.code.len() as i32;
1052        if let Op::JumpIfNot(off) = &mut self.code[j_exit_n] { *off = exit_t - (j_exit_n as i32 + 1); }
1053        if let Op::JumpIfNot(off) = &mut self.code[j_exit_l] { *off = exit_t - (j_exit_l as i32 + 1); }
1054
1055        // return (out, 0) as new Iter
1056        self.emit(Op::LoadLocal(out));
1057        self.emit(Op::PushConst(zero));
1058        self.emit(Op::MakeTuple(2));
1059    }
1060
1061    /// `iter.skip(it, n)` — advance cursor by n (or to end), return new Iter.
1062    fn emit_iter_skip(&mut self, args: &[a::CExpr]) {
1063        self.compile_expr(&args[0], false);
1064        let it   = self.alloc_local("__isk_it");
1065        self.emit(Op::StoreLocal(it));
1066
1067        self.compile_expr(&args[1], false);
1068        let n    = self.alloc_local("__isk_n");
1069        self.emit(Op::StoreLocal(n));
1070
1071        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(0));
1072        let list = self.alloc_local("__isk_list");
1073        self.emit(Op::StoreLocal(list));
1074
1075        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(1));
1076        let idx  = self.alloc_local("__isk_idx");
1077        self.emit(Op::StoreLocal(idx));
1078
1079        // raw = idx + n
1080        self.emit(Op::LoadLocal(idx));
1081        self.emit(Op::LoadLocal(n));
1082        self.emit(Op::NumAdd);
1083        let raw  = self.alloc_local("__isk_raw");
1084        self.emit(Op::StoreLocal(raw));
1085
1086        // new_idx = if raw < len then raw else len
1087        self.emit(Op::LoadLocal(raw));
1088        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1089        self.emit(Op::NumLt);
1090        let j_use_raw = self.code.len();
1091        self.emit(Op::JumpIf(0));
1092
1093        // use len
1094        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1095        let j_end = self.code.len();
1096        self.emit(Op::Jump(0));
1097
1098        // use raw
1099        let raw_t = self.code.len() as i32;
1100        if let Op::JumpIf(off) = &mut self.code[j_use_raw] { *off = raw_t - (j_use_raw as i32 + 1); }
1101        self.emit(Op::LoadLocal(raw));
1102
1103        let end_t = self.code.len() as i32;
1104        if let Op::Jump(off) = &mut self.code[j_end] { *off = end_t - (j_end as i32 + 1); }
1105
1106        // new_idx on stack; build new Iter
1107        let new_idx = self.alloc_local("__isk_ni");
1108        self.emit(Op::StoreLocal(new_idx));
1109        self.emit(Op::LoadLocal(list));
1110        self.emit(Op::LoadLocal(new_idx));
1111        self.emit(Op::MakeTuple(2));
1112    }
1113
1114    /// `iter.to_list(it)` — materialise remaining elements into a List.
1115    fn emit_iter_to_list(&mut self, args: &[a::CExpr]) {
1116        self.compile_expr(&args[0], false);
1117        let it   = self.alloc_local("__itl_it");
1118        self.emit(Op::StoreLocal(it));
1119
1120        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(0));
1121        let list = self.alloc_local("__itl_list");
1122        self.emit(Op::StoreLocal(list));
1123
1124        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(1));
1125        let i    = self.alloc_local("__itl_i");
1126        self.emit(Op::StoreLocal(i));
1127
1128        self.emit(Op::MakeList(0));
1129        let out  = self.alloc_local("__itl_out");
1130        self.emit(Op::StoreLocal(out));
1131
1132        let loop_top = self.code.len();
1133        self.emit(Op::LoadLocal(i));
1134        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1135        self.emit(Op::NumLt);
1136        let j_exit = self.code.len();
1137        self.emit(Op::JumpIfNot(0));
1138
1139        self.emit(Op::LoadLocal(out));
1140        self.emit(Op::LoadLocal(list));
1141        self.emit(Op::LoadLocal(i));
1142        self.emit(Op::GetListElemDyn);
1143        self.emit(Op::ListAppend);
1144        self.emit(Op::StoreLocal(out));
1145
1146        self.emit(Op::LoadLocal(i));
1147        let one = self.pool.int(1);
1148        self.emit(Op::PushConst(one));
1149        self.emit(Op::NumAdd);
1150        self.emit(Op::StoreLocal(i));
1151
1152        let jback = self.code.len();
1153        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1154
1155        let exit_t = self.code.len() as i32;
1156        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1157        self.emit(Op::LoadLocal(out));
1158    }
1159
1160    /// `iter.map(it, f)` — apply `f` to each remaining element; returns new Iter.
1161    fn emit_iter_map(&mut self, args: &[a::CExpr]) {
1162        self.compile_expr(&args[0], false);
1163        let it   = self.alloc_local("__im_it");
1164        self.emit(Op::StoreLocal(it));
1165
1166        self.compile_expr(&args[1], false);
1167        let f    = self.alloc_local("__im_f");
1168        self.emit(Op::StoreLocal(f));
1169
1170        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(0));
1171        let list = self.alloc_local("__im_list");
1172        self.emit(Op::StoreLocal(list));
1173
1174        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(1));
1175        let i    = self.alloc_local("__im_i");
1176        self.emit(Op::StoreLocal(i));
1177
1178        self.emit(Op::MakeList(0));
1179        let out  = self.alloc_local("__im_out");
1180        self.emit(Op::StoreLocal(out));
1181
1182        let loop_top = self.code.len();
1183        self.emit(Op::LoadLocal(i));
1184        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1185        self.emit(Op::NumLt);
1186        let j_exit = self.code.len();
1187        self.emit(Op::JumpIfNot(0));
1188
1189        let nid = self.pool.node_id("n_iter_map");
1190        self.emit(Op::LoadLocal(out));
1191        self.emit(Op::LoadLocal(f));
1192        self.emit(Op::LoadLocal(list));
1193        self.emit(Op::LoadLocal(i));
1194        self.emit(Op::GetListElemDyn);
1195        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1196        self.emit(Op::ListAppend);
1197        self.emit(Op::StoreLocal(out));
1198
1199        self.emit(Op::LoadLocal(i));
1200        let one = self.pool.int(1);
1201        self.emit(Op::PushConst(one));
1202        self.emit(Op::NumAdd);
1203        self.emit(Op::StoreLocal(i));
1204
1205        let jback = self.code.len();
1206        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1207
1208        let exit_t = self.code.len() as i32;
1209        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1210
1211        let zero = self.pool.int(0);
1212        self.emit(Op::LoadLocal(out));
1213        self.emit(Op::PushConst(zero));
1214        self.emit(Op::MakeTuple(2));
1215    }
1216
1217    /// `iter.filter(it, pred)` — keep elements where pred is true; returns new Iter.
1218    fn emit_iter_filter(&mut self, args: &[a::CExpr]) {
1219        self.compile_expr(&args[0], false);
1220        let it   = self.alloc_local("__if_it");
1221        self.emit(Op::StoreLocal(it));
1222
1223        self.compile_expr(&args[1], false);
1224        let f    = self.alloc_local("__if_f");
1225        self.emit(Op::StoreLocal(f));
1226
1227        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(0));
1228        let list = self.alloc_local("__if_list");
1229        self.emit(Op::StoreLocal(list));
1230
1231        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(1));
1232        let i    = self.alloc_local("__if_i");
1233        self.emit(Op::StoreLocal(i));
1234
1235        self.emit(Op::MakeList(0));
1236        let out  = self.alloc_local("__if_out");
1237        self.emit(Op::StoreLocal(out));
1238
1239        let loop_top = self.code.len();
1240        self.emit(Op::LoadLocal(i));
1241        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1242        self.emit(Op::NumLt);
1243        let j_exit = self.code.len();
1244        self.emit(Op::JumpIfNot(0));
1245
1246        // elem := list[i]
1247        self.emit(Op::LoadLocal(list));
1248        self.emit(Op::LoadLocal(i));
1249        self.emit(Op::GetListElemDyn);
1250        let x    = self.alloc_local("__if_x");
1251        self.emit(Op::StoreLocal(x));
1252
1253        let nid = self.pool.node_id("n_iter_filter");
1254        self.emit(Op::LoadLocal(f));
1255        self.emit(Op::LoadLocal(x));
1256        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1257        let j_skip = self.code.len();
1258        self.emit(Op::JumpIfNot(0));
1259
1260        self.emit(Op::LoadLocal(out));
1261        self.emit(Op::LoadLocal(x));
1262        self.emit(Op::ListAppend);
1263        self.emit(Op::StoreLocal(out));
1264
1265        let skip_t = self.code.len() as i32;
1266        if let Op::JumpIfNot(off) = &mut self.code[j_skip] { *off = skip_t - (j_skip as i32 + 1); }
1267
1268        self.emit(Op::LoadLocal(i));
1269        let one = self.pool.int(1);
1270        self.emit(Op::PushConst(one));
1271        self.emit(Op::NumAdd);
1272        self.emit(Op::StoreLocal(i));
1273
1274        let jback = self.code.len();
1275        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1276
1277        let exit_t = self.code.len() as i32;
1278        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1279
1280        let zero = self.pool.int(0);
1281        self.emit(Op::LoadLocal(out));
1282        self.emit(Op::PushConst(zero));
1283        self.emit(Op::MakeTuple(2));
1284    }
1285
1286    /// `iter.fold(it, init, f)` — left fold over remaining elements.
1287    fn emit_iter_fold(&mut self, args: &[a::CExpr]) {
1288        self.compile_expr(&args[0], false);
1289        let it   = self.alloc_local("__ifo_it");
1290        self.emit(Op::StoreLocal(it));
1291
1292        self.compile_expr(&args[1], false);
1293        let acc  = self.alloc_local("__ifo_acc");
1294        self.emit(Op::StoreLocal(acc));
1295
1296        self.compile_expr(&args[2], false);
1297        let f    = self.alloc_local("__ifo_f");
1298        self.emit(Op::StoreLocal(f));
1299
1300        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(0));
1301        let list = self.alloc_local("__ifo_list");
1302        self.emit(Op::StoreLocal(list));
1303
1304        self.emit(Op::LoadLocal(it)); self.emit(Op::GetElem(1));
1305        let i    = self.alloc_local("__ifo_i");
1306        self.emit(Op::StoreLocal(i));
1307
1308        let loop_top = self.code.len();
1309        self.emit(Op::LoadLocal(i));
1310        self.emit(Op::LoadLocal(list)); self.emit(Op::GetListLen);
1311        self.emit(Op::NumLt);
1312        let j_exit = self.code.len();
1313        self.emit(Op::JumpIfNot(0));
1314
1315        let nid = self.pool.node_id("n_iter_fold");
1316        self.emit(Op::LoadLocal(f));
1317        self.emit(Op::LoadLocal(acc));
1318        self.emit(Op::LoadLocal(list));
1319        self.emit(Op::LoadLocal(i));
1320        self.emit(Op::GetListElemDyn);
1321        self.emit(Op::CallClosure { arity: 2, node_id_idx: nid });
1322        self.emit(Op::StoreLocal(acc));
1323
1324        self.emit(Op::LoadLocal(i));
1325        let one = self.pool.int(1);
1326        self.emit(Op::PushConst(one));
1327        self.emit(Op::NumAdd);
1328        self.emit(Op::StoreLocal(i));
1329
1330        let jback = self.code.len();
1331        self.emit(Op::Jump((loop_top as i32) - (jback as i32 + 1)));
1332
1333        let exit_t = self.code.len() as i32;
1334        if let Op::JumpIfNot(off) = &mut self.code[j_exit] { *off = exit_t - (j_exit as i32 + 1); }
1335        self.emit(Op::LoadLocal(acc));
1336    }
1337
1338    /// `map.fold(m, init, f)` — left fold over `Map[K, V]` entries with a
1339    /// three-arg combiner `f(acc, k, v)`. Iteration order matches
1340    /// `map.entries` (BTreeMap-sorted by key). Materializes the entry
1341    /// list once via the runtime's `("map", "entries")` op, then runs
1342    /// the same inline loop as `list.fold`.
1343    fn emit_map_fold(&mut self, args: &[a::CExpr], node_id_idx: u32) {
1344        // xs := map.entries(m)
1345        self.compile_expr(&args[0], false);
1346        let map_kind = self.pool.str("map");
1347        let entries_op = self.pool.str("entries");
1348        self.emit(Op::EffectCall {
1349            kind_idx: map_kind,
1350            op_idx: entries_op,
1351            arity: 1,
1352            node_id_idx,
1353        });
1354        let xs = self.alloc_local("__mf_xs");
1355        self.emit(Op::StoreLocal(xs));
1356
1357        // acc := init
1358        self.compile_expr(&args[1], false);
1359        let acc = self.alloc_local("__mf_acc");
1360        self.emit(Op::StoreLocal(acc));
1361
1362        // f := <closure>
1363        self.compile_expr(&args[2], false);
1364        let f = self.alloc_local("__mf_f");
1365        self.emit(Op::StoreLocal(f));
1366
1367        // i := 0
1368        let zero = self.pool.int(0);
1369        self.emit(Op::PushConst(zero));
1370        let i = self.alloc_local("__mf_i");
1371        self.emit(Op::StoreLocal(i));
1372
1373        // loop_top: while i < len(xs)
1374        let loop_top = self.code.len();
1375        self.emit(Op::LoadLocal(i));
1376        self.emit(Op::LoadLocal(xs));
1377        self.emit(Op::GetListLen);
1378        self.emit(Op::NumLt);
1379        let j_exit = self.code.len();
1380        self.emit(Op::JumpIfNot(0));
1381
1382        // pair := xs[i]
1383        self.emit(Op::LoadLocal(xs));
1384        self.emit(Op::LoadLocal(i));
1385        self.emit(Op::GetListElemDyn);
1386        let pair = self.alloc_local("__mf_pair");
1387        self.emit(Op::StoreLocal(pair));
1388
1389        // acc := f(acc, pair.0, pair.1)
1390        let nid = self.pool.node_id("n_map_fold");
1391        self.emit(Op::LoadLocal(f));
1392        self.emit(Op::LoadLocal(acc));
1393        self.emit(Op::LoadLocal(pair));
1394        self.emit(Op::GetElem(0));
1395        self.emit(Op::LoadLocal(pair));
1396        self.emit(Op::GetElem(1));
1397        self.emit(Op::CallClosure { arity: 3, node_id_idx: nid });
1398        self.emit(Op::StoreLocal(acc));
1399
1400        // i := i + 1
1401        self.emit(Op::LoadLocal(i));
1402        let one = self.pool.int(1);
1403        self.emit(Op::PushConst(one));
1404        self.emit(Op::NumAdd);
1405        self.emit(Op::StoreLocal(i));
1406
1407        let jump_back = self.code.len();
1408        let back = (loop_top as i32) - (jump_back as i32 + 1);
1409        self.emit(Op::Jump(back));
1410
1411        let exit_target = self.code.len() as i32;
1412        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
1413            *off = exit_target - (j_exit as i32 + 1);
1414        }
1415        self.emit(Op::LoadLocal(acc));
1416    }
1417
1418    /// Inline pattern: `<module>.map(v, f)` and friends.
1419    /// `wrap_with`: variant tag whose payload triggers the call (Ok / Some / Err).
1420    /// `wrap_result`: if true, wrap the closure's result back in `wrap_with`
1421    /// (map shape); if false, expect the closure to return a wrapped value
1422    /// itself (and_then shape).
1423    fn emit_variant_map(
1424        &mut self,
1425        args: &[a::CExpr],
1426        wrap_with: &str,
1427        wrap_result: bool,
1428    ) {
1429        // args[0] = the wrapped value (Result/Option), args[1] = closure
1430        let wrap_idx = self.pool.variant(wrap_with);
1431
1432        // Compile and store the value into a local, evaluate closure on top of stack.
1433        self.compile_expr(&args[0], false);
1434        let val_slot = self.alloc_local("__hov");
1435        self.emit(Op::StoreLocal(val_slot));
1436
1437        self.compile_expr(&args[1], false);
1438        let f_slot = self.alloc_local("__hof");
1439        self.emit(Op::StoreLocal(f_slot));
1440
1441        // Stack discipline:
1442        //   load val ⇒ [v]
1443        //   dup     ⇒ [v, v]
1444        //   test    ⇒ [v, Bool]
1445        //   jumpifnot ⇒ [v]
1446        // Both branches end with [v] before the branch body.
1447        self.emit(Op::LoadLocal(val_slot));
1448        self.emit(Op::Dup);
1449        self.emit(Op::TestVariant(wrap_idx));
1450        let j_skip = self.code.len();
1451        self.emit(Op::JumpIfNot(0));
1452
1453        // Matched arm: extract payload, call closure on it.
1454        self.emit(Op::GetVariantArg(0));
1455        let arg_slot = self.alloc_local("__hov_arg");
1456        self.emit(Op::StoreLocal(arg_slot));
1457        self.emit(Op::LoadLocal(f_slot));
1458        self.emit(Op::LoadLocal(arg_slot));
1459        let nid = self.pool.node_id("n_hov");
1460        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1461        if wrap_result {
1462            self.emit(Op::MakeVariant { name_idx: wrap_idx, arity: 1 });
1463        }
1464        let j_end = self.code.len();
1465        self.emit(Op::Jump(0));
1466
1467        // Skip arm: stack already has [v] from the failed Dup; nothing to do.
1468        let skip_target = self.code.len() as i32;
1469        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
1470            *off = skip_target - (j_skip as i32 + 1);
1471        }
1472
1473        let end_target = self.code.len() as i32;
1474        if let Op::Jump(off) = &mut self.code[j_end] {
1475            *off = end_target - (j_end as i32 + 1);
1476        }
1477    }
1478
1479    /// Sibling of `emit_variant_map` for the recovery combinators
1480    /// `result.or_else` and `option.or_else`. Differences from
1481    /// `emit_variant_map`:
1482    ///   - matches on the *negative* variant (`Err` / `None`)
1483    ///   - the closure's result becomes the call's result directly,
1484    ///     with no wrapping (it is itself a `Result` / `Option`)
1485    ///   - `option.or_else`'s closure takes zero args (`None` has no
1486    ///     payload to forward)
1487    fn emit_variant_or_else(
1488        &mut self,
1489        args: &[a::CExpr],
1490        match_on: &str,
1491        closure_arity: u16,
1492    ) {
1493        let match_idx = self.pool.variant(match_on);
1494
1495        self.compile_expr(&args[0], false);
1496        let val_slot = self.alloc_local("__hoe");
1497        self.emit(Op::StoreLocal(val_slot));
1498
1499        self.compile_expr(&args[1], false);
1500        let f_slot = self.alloc_local("__hoe_f");
1501        self.emit(Op::StoreLocal(f_slot));
1502
1503        // Stack discipline mirrors emit_variant_map:
1504        //   load val      ⇒ [v]
1505        //   dup           ⇒ [v, v]
1506        //   test          ⇒ [v, Bool]
1507        //   jumpifnot     ⇒ [v]
1508        // The unmatched arm leaves [v] (Ok/Some unchanged); the
1509        // matched arm pops [v] and pushes the closure's result.
1510        self.emit(Op::LoadLocal(val_slot));
1511        self.emit(Op::Dup);
1512        self.emit(Op::TestVariant(match_idx));
1513        let j_skip = self.code.len();
1514        self.emit(Op::JumpIfNot(0));
1515
1516        // Matched arm: pop the duplicate left on the stack,
1517        // then call the closure with whatever payload it expects.
1518        self.emit(Op::Pop);
1519        self.emit(Op::LoadLocal(f_slot));
1520        if closure_arity == 1 {
1521            self.emit(Op::LoadLocal(val_slot));
1522            self.emit(Op::GetVariantArg(0));
1523        }
1524        let nid = self.pool.node_id("n_hoe");
1525        self.emit(Op::CallClosure { arity: closure_arity, node_id_idx: nid });
1526
1527        let j_end = self.code.len();
1528        self.emit(Op::Jump(0));
1529
1530        // Unmatched arm: stack already holds [v]; nothing to do.
1531        let skip_target = self.code.len() as i32;
1532        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
1533            *off = skip_target - (j_skip as i32 + 1);
1534        }
1535
1536        let end_target = self.code.len() as i32;
1537        if let Op::Jump(off) = &mut self.code[j_end] {
1538            *off = end_target - (j_end as i32 + 1);
1539        }
1540    }
1541
1542    /// `option.unwrap_or_else(opt, f)` — lazy default via zero-arg thunk.
1543    ///   Some(x) → x          (unwrap; no wrapping)
1544    ///   None    → f()        (call thunk; return its result directly)
1545    fn emit_option_unwrap_or_else(&mut self, args: &[a::CExpr]) {
1546        let some_idx = self.pool.variant("Some");
1547
1548        // Compile opt and f; stash both so they're accessible on both arms.
1549        self.compile_expr(&args[0], false);
1550        let val_slot = self.alloc_local("__uoe_val");
1551        self.emit(Op::StoreLocal(val_slot));
1552
1553        self.compile_expr(&args[1], false);
1554        let f_slot = self.alloc_local("__uoe_f");
1555        self.emit(Op::StoreLocal(f_slot));
1556
1557        // Test whether opt is Some.
1558        //   load val ⇒ [v]
1559        //   dup      ⇒ [v, v]
1560        //   test     ⇒ [v, Bool]
1561        //   jumpifnot → None arm
1562        self.emit(Op::LoadLocal(val_slot));
1563        self.emit(Op::Dup);
1564        self.emit(Op::TestVariant(some_idx));
1565        let j_none = self.code.len();
1566        self.emit(Op::JumpIfNot(0));
1567
1568        // Some arm: extract the payload from [v] left on the stack.
1569        self.emit(Op::GetVariantArg(0));
1570        let j_end = self.code.len();
1571        self.emit(Op::Jump(0));
1572
1573        // None arm: pop the [v] duplicate, call the thunk.
1574        let none_target = self.code.len() as i32;
1575        if let Op::JumpIfNot(off) = &mut self.code[j_none] {
1576            *off = none_target - (j_none as i32 + 1);
1577        }
1578        self.emit(Op::Pop);
1579        self.emit(Op::LoadLocal(f_slot));
1580        let nid = self.pool.node_id("n_uoe");
1581        self.emit(Op::CallClosure { arity: 0, node_id_idx: nid });
1582
1583        // Patch jump-to-end from Some arm.
1584        let end_target = self.code.len() as i32;
1585        if let Op::Jump(off) = &mut self.code[j_end] {
1586            *off = end_target - (j_end as i32 + 1);
1587        }
1588    }
1589
1590    // ---- std.flow trampolines ----------------------------------------
1591    //
1592    // Each flow.<op>(c1, c2, ...) call site:
1593    //   1. compiles its closure args and leaves them on the stack
1594    //   2. registers a fresh "trampoline" Function whose body invokes
1595    //      those captured closures appropriately
1596    //   3. emits MakeClosure { fn_id: trampoline, capture_count: N }
1597    //
1598    // The trampoline's parameter layout is [capture_0, ..., capture_{N-1},
1599    // arg_0, ...]: captures first, the closure's own args after.
1600
1601    /// Allocate a fresh fn_id for a trampoline and install its bytecode.
1602    /// Trampolines are the one Function-creation path that already has
1603    /// the body in hand at install time (top-level fns and lambdas have
1604    /// it filled in later), so we compute `body_hash` immediately. The
1605    /// final hash pass at the end of `compile_program` is a no-op here.
1606    fn install_trampoline(&mut self, name: &str, arity: u16, locals_count: u16, code: Vec<Op>) -> u32 {
1607        let fn_id = self.next_fn_id.len() as u32;
1608        let body_hash = crate::program::compute_body_hash(arity, locals_count, &code);
1609        self.next_fn_id.push(Function {
1610            name: name.into(),
1611            arity,
1612            locals_count,
1613            code,
1614            effects: Vec::new(),
1615            body_hash,
1616            // Trampolines (flow.sequential / parallel / etc.) don't
1617            // surface refined params at this layer.
1618            refinements: Vec::new(),
1619        });
1620        fn_id
1621    }
1622
1623    /// `flow.sequential(f, g)` returns a closure `(x) -> g(f(x))`.
1624    fn emit_flow_sequential(&mut self, args: &[a::CExpr]) {
1625        // Push f, g; build the trampoline closure with 2 captures.
1626        self.compile_expr(&args[0], false);
1627        self.compile_expr(&args[1], false);
1628        let nid = self.pool.node_id("n_flow_sequential");
1629        let code = vec![
1630            // Locals: [f=0, g=1, x=2]
1631            Op::LoadLocal(0),                                  // push f
1632            Op::LoadLocal(2),                                  // push x
1633            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = f(x)
1634            // stack: [r]
1635            Op::StoreLocal(3),                                 // tmp = r
1636            Op::LoadLocal(1),                                  // push g
1637            Op::LoadLocal(3),                                  // push tmp
1638            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = g(tmp)
1639            Op::Return,
1640        ];
1641        let fn_id = self.install_trampoline("__flow_sequential", 3, 4, code);
1642        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
1643    }
1644
1645    /// `flow.parallel(fa, fb)` returns a closure `() -> (fa(), fb())`.
1646    /// Implementation is sequential: each function is called in order
1647    /// and the results are packed into a 2-tuple. The spec (§11.2)
1648    /// allows the runtime to apply true parallelism here; that needs
1649    /// a thread-safe handler split and is left to a follow-up. The
1650    /// signature is what users program against — sequential vs threaded
1651    /// is an implementation detail invisible to the type system.
1652    fn emit_flow_parallel(&mut self, args: &[a::CExpr]) {
1653        // Push fa, fb; build a 0-arg trampoline closure with 2 captures.
1654        self.compile_expr(&args[0], false);
1655        self.compile_expr(&args[1], false);
1656        let nid = self.pool.node_id("n_flow_parallel");
1657        let code = vec![
1658            // Locals: [fa=0, fb=1]
1659            Op::LoadLocal(0),                                  // push fa
1660            Op::CallClosure { arity: 0, node_id_idx: nid },    // a = fa()
1661            Op::LoadLocal(1),                                  // push fb
1662            Op::CallClosure { arity: 0, node_id_idx: nid },    // b = fb()
1663            Op::MakeTuple(2),                                  // (a, b)
1664            Op::Return,
1665        ];
1666        let fn_id = self.install_trampoline("__flow_parallel", 2, 2, code);
1667        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
1668    }
1669
1670    /// `flow.parallel_list(actions)` runs each 0-arg closure in `actions`
1671    /// and returns the results as a list in input order. Variadic
1672    /// counterpart to `flow.parallel`. Sequential under the hood — the
1673    /// spec (§11.2) reserves true threading for a future scheduler.
1674    /// Compiled inline (mirrors `list.map`) so closure args can flow
1675    /// through `CallClosure` without a heap-allocated trampoline.
1676    fn emit_flow_parallel_list(&mut self, args: &[a::CExpr]) {
1677        // xs := actions
1678        self.compile_expr(&args[0], false);
1679        let xs = self.alloc_local("__fpl_xs");
1680        self.emit(Op::StoreLocal(xs));
1681
1682        // out := []
1683        self.emit(Op::MakeList(0));
1684        let out = self.alloc_local("__fpl_out");
1685        self.emit(Op::StoreLocal(out));
1686
1687        // i := 0
1688        let zero = self.pool.int(0);
1689        self.emit(Op::PushConst(zero));
1690        let i = self.alloc_local("__fpl_i");
1691        self.emit(Op::StoreLocal(i));
1692
1693        // loop_top: while i < len(xs) { ... }
1694        let loop_top = self.code.len();
1695        self.emit(Op::LoadLocal(i));
1696        self.emit(Op::LoadLocal(xs));
1697        self.emit(Op::GetListLen);
1698        self.emit(Op::NumLt);
1699        let j_exit = self.code.len();
1700        self.emit(Op::JumpIfNot(0));
1701
1702        // body: out := out ++ [xs[i]()]
1703        let nid = self.pool.node_id("n_flow_parallel_list");
1704        self.emit(Op::LoadLocal(out));
1705        self.emit(Op::LoadLocal(xs));
1706        self.emit(Op::LoadLocal(i));
1707        self.emit(Op::GetListElemDyn);
1708        self.emit(Op::CallClosure { arity: 0, node_id_idx: nid });
1709        self.emit(Op::ListAppend);
1710        self.emit(Op::StoreLocal(out));
1711
1712        // i := i + 1
1713        self.emit(Op::LoadLocal(i));
1714        let one = self.pool.int(1);
1715        self.emit(Op::PushConst(one));
1716        self.emit(Op::NumAdd);
1717        self.emit(Op::StoreLocal(i));
1718
1719        // jump back
1720        let jump_back = self.code.len();
1721        let back = (loop_top as i32) - (jump_back as i32 + 1);
1722        self.emit(Op::Jump(back));
1723
1724        // exit: patch j_exit, push out
1725        let exit_target = self.code.len() as i32;
1726        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
1727            *off = exit_target - (j_exit as i32 + 1);
1728        }
1729        self.emit(Op::LoadLocal(out));
1730    }
1731
1732    /// `flow.branch(cond, t, f)` returns a closure `(x) -> if cond(x) then t(x) else f(x)`.
1733    fn emit_flow_branch(&mut self, args: &[a::CExpr]) {
1734        self.compile_expr(&args[0], false);
1735        self.compile_expr(&args[1], false);
1736        self.compile_expr(&args[2], false);
1737        let nid = self.pool.node_id("n_flow_branch");
1738        let mut code = vec![
1739            // Locals: [cond=0, t=1, f=2, x=3]
1740            Op::LoadLocal(0),                               // push cond
1741            Op::LoadLocal(3),                               // push x
1742            Op::CallClosure { arity: 1, node_id_idx: nid }, // bool
1743        ];
1744        let j_false = code.len();
1745        code.push(Op::JumpIfNot(0));                        // patched
1746        // true arm: t(x)
1747        code.push(Op::LoadLocal(1));
1748        code.push(Op::LoadLocal(3));
1749        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
1750        code.push(Op::Return);
1751        // false arm
1752        let false_target = code.len() as i32;
1753        if let Op::JumpIfNot(off) = &mut code[j_false] {
1754            *off = false_target - (j_false as i32 + 1);
1755        }
1756        code.push(Op::LoadLocal(2));
1757        code.push(Op::LoadLocal(3));
1758        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
1759        code.push(Op::Return);
1760
1761        let fn_id = self.install_trampoline("__flow_branch", 4, 4, code);
1762        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
1763    }
1764
1765    /// `flow.retry(f, max_attempts)` returns a closure `(x) -> Result[U, E]`
1766    /// that calls `f(x)` up to `max_attempts` times, returning the first
1767    /// `Ok` or the final `Err`.
1768    fn emit_flow_retry(&mut self, args: &[a::CExpr]) {
1769        self.compile_expr(&args[0], false);
1770        self.compile_expr(&args[1], false);
1771        let call_nid = self.pool.node_id("n_flow_retry");
1772        let ok_idx = self.pool.variant("Ok");
1773        let zero_const = self.pool.int(0);
1774        let one_const = self.pool.int(1);
1775        // Locals: [f=0, max=1, x=2, i=3, last=4]
1776        let mut code = vec![
1777            // i := 0
1778            Op::PushConst(zero_const),
1779            Op::StoreLocal(3),
1780        ];
1781        // loop_top: while i < max
1782        let loop_top = code.len() as i32;
1783        code.push(Op::LoadLocal(3));
1784        code.push(Op::LoadLocal(1));
1785        code.push(Op::NumLt);
1786        let j_done = code.len();
1787        code.push(Op::JumpIfNot(0));                       // patched
1788
1789        // body: r := f(x); last := r
1790        code.push(Op::LoadLocal(0));
1791        code.push(Op::LoadLocal(2));
1792        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
1793        code.push(Op::StoreLocal(4));
1794
1795        // Test variant Ok on last; if so, return last.
1796        code.push(Op::LoadLocal(4));
1797        code.push(Op::TestVariant(ok_idx));
1798        let j_was_err = code.len();
1799        code.push(Op::JumpIfNot(0));                       // patched: skip return
1800        code.push(Op::LoadLocal(4));
1801        code.push(Op::Return);
1802
1803        // was_err: i := i + 1; jump loop_top
1804        let was_err_target = code.len() as i32;
1805        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
1806            *off = was_err_target - (j_was_err as i32 + 1);
1807        }
1808        code.push(Op::LoadLocal(3));
1809        code.push(Op::PushConst(one_const));
1810        code.push(Op::NumAdd);
1811        code.push(Op::StoreLocal(3));
1812        let pc_after_jump = code.len() as i32 + 1;
1813        code.push(Op::Jump(loop_top - pc_after_jump));
1814
1815        // done: return last (the final Err, or Unit if max=0).
1816        let done_target = code.len() as i32;
1817        if let Op::JumpIfNot(off) = &mut code[j_done] {
1818            *off = done_target - (j_done as i32 + 1);
1819        }
1820        code.push(Op::LoadLocal(4));
1821        code.push(Op::Return);
1822
1823        let fn_id = self.install_trampoline("__flow_retry", 3, 5, code);
1824        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
1825    }
1826
1827    /// `flow.retry_with_backoff(f, attempts, base_ms)` (#226). Variant
1828    /// of `flow.retry` that sleeps between attempts. The first
1829    /// attempt fires immediately; attempt k > 1 waits `base_ms *
1830    /// 2^(k-2)` ms before retrying. Sleeps go through
1831    /// `time.sleep_ms`, which is why the resulting closure carries
1832    /// `[time]` in its effect row even though the underlying `f` is
1833    /// pure.
1834    fn emit_flow_retry_with_backoff(&mut self, args: &[a::CExpr]) {
1835        // Push captures: f, max, base_ms. The trampoline takes one
1836        // call-time arg `x`, so capture_count = 3, arity = 4.
1837        self.compile_expr(&args[0], false);
1838        self.compile_expr(&args[1], false);
1839        self.compile_expr(&args[2], false);
1840        let call_nid    = self.pool.node_id("n_flow_retry_backoff");
1841        let sleep_nid   = self.pool.node_id("n_flow_retry_backoff_sleep");
1842        let kind_idx    = self.pool.str("time");
1843        let op_idx      = self.pool.str("sleep_ms");
1844        let ok_idx      = self.pool.variant("Ok");
1845        let zero_const  = self.pool.int(0);
1846        let one_const   = self.pool.int(1);
1847        let two_const   = self.pool.int(2);
1848        // Locals layout:
1849        //   0=f, 1=max, 2=base_ms (captures)
1850        //   3=x (arg)
1851        //   4=i, 5=last, 6=next_delay (working state)
1852        let mut code = vec![
1853            // next_delay := base_ms
1854            Op::LoadLocal(2),
1855            Op::StoreLocal(6),
1856            // i := 0
1857            Op::PushConst(zero_const),
1858            Op::StoreLocal(4),
1859        ];
1860
1861        let loop_top = code.len() as i32;
1862        // while i < max
1863        code.push(Op::LoadLocal(4));
1864        code.push(Op::LoadLocal(1));
1865        code.push(Op::NumLt);
1866        let j_done = code.len();
1867        code.push(Op::JumpIfNot(0)); // patched
1868
1869        // if i > 0: time.sleep_ms(next_delay); next_delay := next_delay * 2
1870        code.push(Op::PushConst(zero_const));
1871        code.push(Op::LoadLocal(4));
1872        code.push(Op::NumLt);                // 0 < i ?
1873        let j_no_sleep = code.len();
1874        code.push(Op::JumpIfNot(0));         // patched: skip the sleep block
1875        // Sleep
1876        code.push(Op::LoadLocal(6));         // arg = next_delay
1877        code.push(Op::EffectCall {
1878            kind_idx, op_idx, arity: 1, node_id_idx: sleep_nid,
1879        });
1880        code.push(Op::Pop);                  // discard the Unit result
1881        // next_delay := next_delay * 2
1882        code.push(Op::LoadLocal(6));
1883        code.push(Op::PushConst(two_const));
1884        code.push(Op::NumMul);
1885        code.push(Op::StoreLocal(6));
1886        // patch the no-sleep skip
1887        let after_sleep = code.len() as i32;
1888        if let Op::JumpIfNot(off) = &mut code[j_no_sleep] {
1889            *off = after_sleep - (j_no_sleep as i32 + 1);
1890        }
1891
1892        // last := f(x)
1893        code.push(Op::LoadLocal(0));
1894        code.push(Op::LoadLocal(3));
1895        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
1896        code.push(Op::StoreLocal(5));
1897
1898        // if Ok(last): return last
1899        code.push(Op::LoadLocal(5));
1900        code.push(Op::TestVariant(ok_idx));
1901        let j_was_err = code.len();
1902        code.push(Op::JumpIfNot(0)); // patched
1903        code.push(Op::LoadLocal(5));
1904        code.push(Op::Return);
1905
1906        // was_err: i := i + 1; jump loop_top
1907        let was_err_target = code.len() as i32;
1908        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
1909            *off = was_err_target - (j_was_err as i32 + 1);
1910        }
1911        code.push(Op::LoadLocal(4));
1912        code.push(Op::PushConst(one_const));
1913        code.push(Op::NumAdd);
1914        code.push(Op::StoreLocal(4));
1915        let pc_after_jump = code.len() as i32 + 1;
1916        code.push(Op::Jump(loop_top - pc_after_jump));
1917
1918        // done: return last (the final Err, or Unit if max=0).
1919        let done_target = code.len() as i32;
1920        if let Op::JumpIfNot(off) = &mut code[j_done] {
1921            *off = done_target - (j_done as i32 + 1);
1922        }
1923        code.push(Op::LoadLocal(5));
1924        code.push(Op::Return);
1925
1926        let fn_id = self.install_trampoline("__flow_retry_backoff", 4, 7, code);
1927        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
1928    }
1929}
1930
1931/// Collect free variables referenced in `e` that are not in `bound`.
1932/// Mutates `bound` to track let/lambda introductions during the walk;
1933/// the caller's set is preserved on return because Rust's borrow rules
1934/// force us to clone for sub-scopes that rebind a name.
1935fn free_vars(e: &a::CExpr, bound: &mut std::collections::HashSet<String>, out: &mut Vec<String>) {
1936    match e {
1937        a::CExpr::Literal { .. } => {}
1938        a::CExpr::Var { name } => {
1939            if !bound.contains(name) && !out.contains(name) {
1940                out.push(name.clone());
1941            }
1942        }
1943        a::CExpr::Call { callee, args } => {
1944            free_vars(callee, bound, out);
1945            for a in args { free_vars(a, bound, out); }
1946        }
1947        a::CExpr::Let { name, value, body, .. } => {
1948            free_vars(value, bound, out);
1949            let was_bound = bound.contains(name);
1950            bound.insert(name.clone());
1951            free_vars(body, bound, out);
1952            if !was_bound { bound.remove(name); }
1953        }
1954        a::CExpr::Match { scrutinee, arms } => {
1955            free_vars(scrutinee, bound, out);
1956            for arm in arms {
1957                let mut local_bound = bound.clone();
1958                pattern_binders(&arm.pattern, &mut local_bound);
1959                free_vars(&arm.body, &mut local_bound, out);
1960            }
1961        }
1962        a::CExpr::Block { statements, result } => {
1963            let mut local_bound = bound.clone();
1964            for s in statements { free_vars(s, &mut local_bound, out); }
1965            free_vars(result, &mut local_bound, out);
1966        }
1967        a::CExpr::Constructor { args, .. } => {
1968            for a in args { free_vars(a, bound, out); }
1969        }
1970        a::CExpr::RecordLit { fields } => {
1971            for f in fields { free_vars(&f.value, bound, out); }
1972        }
1973        a::CExpr::TupleLit { items } | a::CExpr::ListLit { items } => {
1974            for it in items { free_vars(it, bound, out); }
1975        }
1976        a::CExpr::FieldAccess { value, .. } => free_vars(value, bound, out),
1977        a::CExpr::Lambda { params, body, .. } => {
1978            let mut inner = bound.clone();
1979            for p in params { inner.insert(p.name.clone()); }
1980            free_vars(body, &mut inner, out);
1981        }
1982        a::CExpr::BinOp { lhs, rhs, .. } => {
1983            free_vars(lhs, bound, out);
1984            free_vars(rhs, bound, out);
1985        }
1986        a::CExpr::UnaryOp { expr, .. } => free_vars(expr, bound, out),
1987        a::CExpr::Return { value } => free_vars(value, bound, out),
1988    }
1989}
1990
1991fn pattern_binders(p: &a::Pattern, bound: &mut std::collections::HashSet<String>) {
1992    match p {
1993        a::Pattern::PWild | a::Pattern::PLiteral { .. } => {}
1994        a::Pattern::PVar { name } => { bound.insert(name.clone()); }
1995        a::Pattern::PConstructor { args, .. } => {
1996            for a in args { pattern_binders(a, bound); }
1997        }
1998        a::Pattern::PRecord { fields } => {
1999            for f in fields { pattern_binders(&f.pattern, bound); }
2000        }
2001        a::Pattern::PTuple { items } => {
2002            for it in items { pattern_binders(it, bound); }
2003        }
2004    }
2005}