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            ("map", "fold") => self.emit_map_fold(args, node_id_idx),
662            ("flow", "sequential") => self.emit_flow_sequential(args),
663            ("flow", "branch") => self.emit_flow_branch(args),
664            ("flow", "retry") => self.emit_flow_retry(args),
665            ("flow", "retry_with_backoff") => self.emit_flow_retry_with_backoff(args),
666            ("flow", "parallel") => self.emit_flow_parallel(args),
667            ("flow", "parallel_list") => self.emit_flow_parallel_list(args),
668            _ => return false,
669        }
670        true
671    }
672
673    /// `list.map(xs, f)` — inline loop applying `f` to each element.
674    /// Stack contract: pushes the resulting List.
675    fn emit_list_map(&mut self, args: &[a::CExpr]) {
676        // Compile xs and f, store both as locals.
677        self.compile_expr(&args[0], false);
678        let xs = self.alloc_local("__lm_xs");
679        self.emit(Op::StoreLocal(xs));
680        self.compile_expr(&args[1], false);
681        let f = self.alloc_local("__lm_f");
682        self.emit(Op::StoreLocal(f));
683
684        // out := []
685        self.emit(Op::MakeList(0));
686        let out = self.alloc_local("__lm_out");
687        self.emit(Op::StoreLocal(out));
688
689        // i := 0
690        let zero = self.pool.int(0);
691        self.emit(Op::PushConst(zero));
692        let i = self.alloc_local("__lm_i");
693        self.emit(Op::StoreLocal(i));
694
695        // loop_top: while i < len(xs) { ... }
696        let loop_top = self.code.len();
697        self.emit(Op::LoadLocal(i));
698        self.emit(Op::LoadLocal(xs));
699        self.emit(Op::GetListLen);
700        self.emit(Op::NumLt);
701        let j_exit = self.code.len();
702        self.emit(Op::JumpIfNot(0));
703
704        // body: out := out ++ [f(xs[i])]
705        let nid = self.pool.node_id("n_list_map");
706        self.emit(Op::LoadLocal(out));
707        self.emit(Op::LoadLocal(f));
708        self.emit(Op::LoadLocal(xs));
709        self.emit(Op::LoadLocal(i));
710        self.emit(Op::GetListElemDyn);
711        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
712        self.emit(Op::ListAppend);
713        self.emit(Op::StoreLocal(out));
714
715        // i := i + 1
716        self.emit(Op::LoadLocal(i));
717        let one = self.pool.int(1);
718        self.emit(Op::PushConst(one));
719        self.emit(Op::NumAdd);
720        self.emit(Op::StoreLocal(i));
721
722        // jump back
723        let jump_back = self.code.len();
724        let back = (loop_top as i32) - (jump_back as i32 + 1);
725        self.emit(Op::Jump(back));
726
727        // exit: patch j_exit, push out
728        let exit_target = self.code.len() as i32;
729        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
730            *off = exit_target - (j_exit as i32 + 1);
731        }
732        self.emit(Op::LoadLocal(out));
733    }
734
735    /// `list.par_map(xs, f)` (#305 slice 1). Pushes `xs` and `f`,
736    /// then emits a single `Op::ParallelMap` — the VM applies `f`
737    /// to each element on OS-thread tasks, capped by
738    /// `LEX_PAR_MAX_CONCURRENCY`. Returns the result list in input
739    /// order.
740    fn emit_list_par_map(&mut self, args: &[a::CExpr]) {
741        self.compile_expr(&args[0], false);
742        self.compile_expr(&args[1], false);
743        let nid = self.pool.node_id("n_list_par_map");
744        self.emit(Op::ParallelMap { node_id_idx: nid });
745    }
746
747    /// `list.sort_by(xs, f)` (#338). Pushes `xs` and the key-fn
748    /// `f`, then emits a single `Op::SortByKey` — the VM invokes
749    /// `f` on each element to derive a sortable key, stable-sorts
750    /// by key, and returns the values in sorted order. Keys must
751    /// resolve to `Int` / `Float` / `Str`; mixed-type pairs are
752    /// treated as equal by the comparator (preserving insertion
753    /// order via the stable sort).
754    fn emit_list_sort_by(&mut self, args: &[a::CExpr]) {
755        self.compile_expr(&args[0], false);
756        self.compile_expr(&args[1], false);
757        let nid = self.pool.node_id("n_list_sort_by");
758        self.emit(Op::SortByKey { node_id_idx: nid });
759    }
760
761    /// `list.filter(xs, pred)` — keep elements where pred returns true.
762    fn emit_list_filter(&mut self, args: &[a::CExpr]) {
763        self.compile_expr(&args[0], false);
764        let xs = self.alloc_local("__lf_xs");
765        self.emit(Op::StoreLocal(xs));
766        self.compile_expr(&args[1], false);
767        let f = self.alloc_local("__lf_f");
768        self.emit(Op::StoreLocal(f));
769
770        self.emit(Op::MakeList(0));
771        let out = self.alloc_local("__lf_out");
772        self.emit(Op::StoreLocal(out));
773
774        let zero = self.pool.int(0);
775        self.emit(Op::PushConst(zero));
776        let i = self.alloc_local("__lf_i");
777        self.emit(Op::StoreLocal(i));
778
779        let loop_top = self.code.len();
780        self.emit(Op::LoadLocal(i));
781        self.emit(Op::LoadLocal(xs));
782        self.emit(Op::GetListLen);
783        self.emit(Op::NumLt);
784        let j_exit = self.code.len();
785        self.emit(Op::JumpIfNot(0));
786
787        // x := xs[i]
788        self.emit(Op::LoadLocal(xs));
789        self.emit(Op::LoadLocal(i));
790        self.emit(Op::GetListElemDyn);
791        let x = self.alloc_local("__lf_x");
792        self.emit(Op::StoreLocal(x));
793
794        // if pred(x) { out := out ++ [x] }
795        let nid = self.pool.node_id("n_list_filter");
796        self.emit(Op::LoadLocal(f));
797        self.emit(Op::LoadLocal(x));
798        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
799        let j_skip = self.code.len();
800        self.emit(Op::JumpIfNot(0));
801
802        self.emit(Op::LoadLocal(out));
803        self.emit(Op::LoadLocal(x));
804        self.emit(Op::ListAppend);
805        self.emit(Op::StoreLocal(out));
806
807        let skip_target = self.code.len() as i32;
808        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
809            *off = skip_target - (j_skip as i32 + 1);
810        }
811
812        // i := i + 1
813        self.emit(Op::LoadLocal(i));
814        let one = self.pool.int(1);
815        self.emit(Op::PushConst(one));
816        self.emit(Op::NumAdd);
817        self.emit(Op::StoreLocal(i));
818
819        let jump_back = self.code.len();
820        let back = (loop_top as i32) - (jump_back as i32 + 1);
821        self.emit(Op::Jump(back));
822
823        let exit_target = self.code.len() as i32;
824        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
825            *off = exit_target - (j_exit as i32 + 1);
826        }
827        self.emit(Op::LoadLocal(out));
828    }
829
830    /// `list.fold(xs, init, f)` — left fold with two-arg combiner.
831    fn emit_list_fold(&mut self, args: &[a::CExpr]) {
832        // args: xs, init, f
833        self.compile_expr(&args[0], false);
834        let xs = self.alloc_local("__lo_xs");
835        self.emit(Op::StoreLocal(xs));
836        self.compile_expr(&args[1], false);
837        let acc = self.alloc_local("__lo_acc");
838        self.emit(Op::StoreLocal(acc));
839        self.compile_expr(&args[2], false);
840        let f = self.alloc_local("__lo_f");
841        self.emit(Op::StoreLocal(f));
842
843        let zero = self.pool.int(0);
844        self.emit(Op::PushConst(zero));
845        let i = self.alloc_local("__lo_i");
846        self.emit(Op::StoreLocal(i));
847
848        let loop_top = self.code.len();
849        self.emit(Op::LoadLocal(i));
850        self.emit(Op::LoadLocal(xs));
851        self.emit(Op::GetListLen);
852        self.emit(Op::NumLt);
853        let j_exit = self.code.len();
854        self.emit(Op::JumpIfNot(0));
855
856        // acc := f(acc, xs[i])
857        let nid = self.pool.node_id("n_list_fold");
858        self.emit(Op::LoadLocal(f));
859        self.emit(Op::LoadLocal(acc));
860        self.emit(Op::LoadLocal(xs));
861        self.emit(Op::LoadLocal(i));
862        self.emit(Op::GetListElemDyn);
863        self.emit(Op::CallClosure { arity: 2, node_id_idx: nid });
864        self.emit(Op::StoreLocal(acc));
865
866        // i := i + 1
867        self.emit(Op::LoadLocal(i));
868        let one = self.pool.int(1);
869        self.emit(Op::PushConst(one));
870        self.emit(Op::NumAdd);
871        self.emit(Op::StoreLocal(i));
872
873        let jump_back = self.code.len();
874        let back = (loop_top as i32) - (jump_back as i32 + 1);
875        self.emit(Op::Jump(back));
876
877        let exit_target = self.code.len() as i32;
878        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
879            *off = exit_target - (j_exit as i32 + 1);
880        }
881        self.emit(Op::LoadLocal(acc));
882    }
883
884    /// `map.fold(m, init, f)` — left fold over `Map[K, V]` entries with a
885    /// three-arg combiner `f(acc, k, v)`. Iteration order matches
886    /// `map.entries` (BTreeMap-sorted by key). Materializes the entry
887    /// list once via the runtime's `("map", "entries")` op, then runs
888    /// the same inline loop as `list.fold`.
889    fn emit_map_fold(&mut self, args: &[a::CExpr], node_id_idx: u32) {
890        // xs := map.entries(m)
891        self.compile_expr(&args[0], false);
892        let map_kind = self.pool.str("map");
893        let entries_op = self.pool.str("entries");
894        self.emit(Op::EffectCall {
895            kind_idx: map_kind,
896            op_idx: entries_op,
897            arity: 1,
898            node_id_idx,
899        });
900        let xs = self.alloc_local("__mf_xs");
901        self.emit(Op::StoreLocal(xs));
902
903        // acc := init
904        self.compile_expr(&args[1], false);
905        let acc = self.alloc_local("__mf_acc");
906        self.emit(Op::StoreLocal(acc));
907
908        // f := <closure>
909        self.compile_expr(&args[2], false);
910        let f = self.alloc_local("__mf_f");
911        self.emit(Op::StoreLocal(f));
912
913        // i := 0
914        let zero = self.pool.int(0);
915        self.emit(Op::PushConst(zero));
916        let i = self.alloc_local("__mf_i");
917        self.emit(Op::StoreLocal(i));
918
919        // loop_top: while i < len(xs)
920        let loop_top = self.code.len();
921        self.emit(Op::LoadLocal(i));
922        self.emit(Op::LoadLocal(xs));
923        self.emit(Op::GetListLen);
924        self.emit(Op::NumLt);
925        let j_exit = self.code.len();
926        self.emit(Op::JumpIfNot(0));
927
928        // pair := xs[i]
929        self.emit(Op::LoadLocal(xs));
930        self.emit(Op::LoadLocal(i));
931        self.emit(Op::GetListElemDyn);
932        let pair = self.alloc_local("__mf_pair");
933        self.emit(Op::StoreLocal(pair));
934
935        // acc := f(acc, pair.0, pair.1)
936        let nid = self.pool.node_id("n_map_fold");
937        self.emit(Op::LoadLocal(f));
938        self.emit(Op::LoadLocal(acc));
939        self.emit(Op::LoadLocal(pair));
940        self.emit(Op::GetElem(0));
941        self.emit(Op::LoadLocal(pair));
942        self.emit(Op::GetElem(1));
943        self.emit(Op::CallClosure { arity: 3, node_id_idx: nid });
944        self.emit(Op::StoreLocal(acc));
945
946        // i := i + 1
947        self.emit(Op::LoadLocal(i));
948        let one = self.pool.int(1);
949        self.emit(Op::PushConst(one));
950        self.emit(Op::NumAdd);
951        self.emit(Op::StoreLocal(i));
952
953        let jump_back = self.code.len();
954        let back = (loop_top as i32) - (jump_back as i32 + 1);
955        self.emit(Op::Jump(back));
956
957        let exit_target = self.code.len() as i32;
958        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
959            *off = exit_target - (j_exit as i32 + 1);
960        }
961        self.emit(Op::LoadLocal(acc));
962    }
963
964    /// Inline pattern: `<module>.map(v, f)` and friends.
965    /// `wrap_with`: variant tag whose payload triggers the call (Ok / Some / Err).
966    /// `wrap_result`: if true, wrap the closure's result back in `wrap_with`
967    /// (map shape); if false, expect the closure to return a wrapped value
968    /// itself (and_then shape).
969    fn emit_variant_map(
970        &mut self,
971        args: &[a::CExpr],
972        wrap_with: &str,
973        wrap_result: bool,
974    ) {
975        // args[0] = the wrapped value (Result/Option), args[1] = closure
976        let wrap_idx = self.pool.variant(wrap_with);
977
978        // Compile and store the value into a local, evaluate closure on top of stack.
979        self.compile_expr(&args[0], false);
980        let val_slot = self.alloc_local("__hov");
981        self.emit(Op::StoreLocal(val_slot));
982
983        self.compile_expr(&args[1], false);
984        let f_slot = self.alloc_local("__hof");
985        self.emit(Op::StoreLocal(f_slot));
986
987        // Stack discipline:
988        //   load val ⇒ [v]
989        //   dup     ⇒ [v, v]
990        //   test    ⇒ [v, Bool]
991        //   jumpifnot ⇒ [v]
992        // Both branches end with [v] before the branch body.
993        self.emit(Op::LoadLocal(val_slot));
994        self.emit(Op::Dup);
995        self.emit(Op::TestVariant(wrap_idx));
996        let j_skip = self.code.len();
997        self.emit(Op::JumpIfNot(0));
998
999        // Matched arm: extract payload, call closure on it.
1000        self.emit(Op::GetVariantArg(0));
1001        let arg_slot = self.alloc_local("__hov_arg");
1002        self.emit(Op::StoreLocal(arg_slot));
1003        self.emit(Op::LoadLocal(f_slot));
1004        self.emit(Op::LoadLocal(arg_slot));
1005        let nid = self.pool.node_id("n_hov");
1006        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
1007        if wrap_result {
1008            self.emit(Op::MakeVariant { name_idx: wrap_idx, arity: 1 });
1009        }
1010        let j_end = self.code.len();
1011        self.emit(Op::Jump(0));
1012
1013        // Skip arm: stack already has [v] from the failed Dup; nothing to do.
1014        let skip_target = self.code.len() as i32;
1015        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
1016            *off = skip_target - (j_skip as i32 + 1);
1017        }
1018
1019        let end_target = self.code.len() as i32;
1020        if let Op::Jump(off) = &mut self.code[j_end] {
1021            *off = end_target - (j_end as i32 + 1);
1022        }
1023    }
1024
1025    /// Sibling of `emit_variant_map` for the recovery combinators
1026    /// `result.or_else` and `option.or_else`. Differences from
1027    /// `emit_variant_map`:
1028    ///   - matches on the *negative* variant (`Err` / `None`)
1029    ///   - the closure's result becomes the call's result directly,
1030    ///     with no wrapping (it is itself a `Result` / `Option`)
1031    ///   - `option.or_else`'s closure takes zero args (`None` has no
1032    ///     payload to forward)
1033    fn emit_variant_or_else(
1034        &mut self,
1035        args: &[a::CExpr],
1036        match_on: &str,
1037        closure_arity: u16,
1038    ) {
1039        let match_idx = self.pool.variant(match_on);
1040
1041        self.compile_expr(&args[0], false);
1042        let val_slot = self.alloc_local("__hoe");
1043        self.emit(Op::StoreLocal(val_slot));
1044
1045        self.compile_expr(&args[1], false);
1046        let f_slot = self.alloc_local("__hoe_f");
1047        self.emit(Op::StoreLocal(f_slot));
1048
1049        // Stack discipline mirrors emit_variant_map:
1050        //   load val      ⇒ [v]
1051        //   dup           ⇒ [v, v]
1052        //   test          ⇒ [v, Bool]
1053        //   jumpifnot     ⇒ [v]
1054        // The unmatched arm leaves [v] (Ok/Some unchanged); the
1055        // matched arm pops [v] and pushes the closure's result.
1056        self.emit(Op::LoadLocal(val_slot));
1057        self.emit(Op::Dup);
1058        self.emit(Op::TestVariant(match_idx));
1059        let j_skip = self.code.len();
1060        self.emit(Op::JumpIfNot(0));
1061
1062        // Matched arm: pop the duplicate left on the stack,
1063        // then call the closure with whatever payload it expects.
1064        self.emit(Op::Pop);
1065        self.emit(Op::LoadLocal(f_slot));
1066        if closure_arity == 1 {
1067            self.emit(Op::LoadLocal(val_slot));
1068            self.emit(Op::GetVariantArg(0));
1069        }
1070        let nid = self.pool.node_id("n_hoe");
1071        self.emit(Op::CallClosure { arity: closure_arity, node_id_idx: nid });
1072
1073        let j_end = self.code.len();
1074        self.emit(Op::Jump(0));
1075
1076        // Unmatched arm: stack already holds [v]; nothing to do.
1077        let skip_target = self.code.len() as i32;
1078        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
1079            *off = skip_target - (j_skip as i32 + 1);
1080        }
1081
1082        let end_target = self.code.len() as i32;
1083        if let Op::Jump(off) = &mut self.code[j_end] {
1084            *off = end_target - (j_end as i32 + 1);
1085        }
1086    }
1087
1088    /// `option.unwrap_or_else(opt, f)` — lazy default via zero-arg thunk.
1089    ///   Some(x) → x          (unwrap; no wrapping)
1090    ///   None    → f()        (call thunk; return its result directly)
1091    fn emit_option_unwrap_or_else(&mut self, args: &[a::CExpr]) {
1092        let some_idx = self.pool.variant("Some");
1093
1094        // Compile opt and f; stash both so they're accessible on both arms.
1095        self.compile_expr(&args[0], false);
1096        let val_slot = self.alloc_local("__uoe_val");
1097        self.emit(Op::StoreLocal(val_slot));
1098
1099        self.compile_expr(&args[1], false);
1100        let f_slot = self.alloc_local("__uoe_f");
1101        self.emit(Op::StoreLocal(f_slot));
1102
1103        // Test whether opt is Some.
1104        //   load val ⇒ [v]
1105        //   dup      ⇒ [v, v]
1106        //   test     ⇒ [v, Bool]
1107        //   jumpifnot → None arm
1108        self.emit(Op::LoadLocal(val_slot));
1109        self.emit(Op::Dup);
1110        self.emit(Op::TestVariant(some_idx));
1111        let j_none = self.code.len();
1112        self.emit(Op::JumpIfNot(0));
1113
1114        // Some arm: extract the payload from [v] left on the stack.
1115        self.emit(Op::GetVariantArg(0));
1116        let j_end = self.code.len();
1117        self.emit(Op::Jump(0));
1118
1119        // None arm: pop the [v] duplicate, call the thunk.
1120        let none_target = self.code.len() as i32;
1121        if let Op::JumpIfNot(off) = &mut self.code[j_none] {
1122            *off = none_target - (j_none as i32 + 1);
1123        }
1124        self.emit(Op::Pop);
1125        self.emit(Op::LoadLocal(f_slot));
1126        let nid = self.pool.node_id("n_uoe");
1127        self.emit(Op::CallClosure { arity: 0, node_id_idx: nid });
1128
1129        // Patch jump-to-end from Some arm.
1130        let end_target = self.code.len() as i32;
1131        if let Op::Jump(off) = &mut self.code[j_end] {
1132            *off = end_target - (j_end as i32 + 1);
1133        }
1134    }
1135
1136    // ---- std.flow trampolines ----------------------------------------
1137    //
1138    // Each flow.<op>(c1, c2, ...) call site:
1139    //   1. compiles its closure args and leaves them on the stack
1140    //   2. registers a fresh "trampoline" Function whose body invokes
1141    //      those captured closures appropriately
1142    //   3. emits MakeClosure { fn_id: trampoline, capture_count: N }
1143    //
1144    // The trampoline's parameter layout is [capture_0, ..., capture_{N-1},
1145    // arg_0, ...]: captures first, the closure's own args after.
1146
1147    /// Allocate a fresh fn_id for a trampoline and install its bytecode.
1148    /// Trampolines are the one Function-creation path that already has
1149    /// the body in hand at install time (top-level fns and lambdas have
1150    /// it filled in later), so we compute `body_hash` immediately. The
1151    /// final hash pass at the end of `compile_program` is a no-op here.
1152    fn install_trampoline(&mut self, name: &str, arity: u16, locals_count: u16, code: Vec<Op>) -> u32 {
1153        let fn_id = self.next_fn_id.len() as u32;
1154        let body_hash = crate::program::compute_body_hash(arity, locals_count, &code);
1155        self.next_fn_id.push(Function {
1156            name: name.into(),
1157            arity,
1158            locals_count,
1159            code,
1160            effects: Vec::new(),
1161            body_hash,
1162            // Trampolines (flow.sequential / parallel / etc.) don't
1163            // surface refined params at this layer.
1164            refinements: Vec::new(),
1165        });
1166        fn_id
1167    }
1168
1169    /// `flow.sequential(f, g)` returns a closure `(x) -> g(f(x))`.
1170    fn emit_flow_sequential(&mut self, args: &[a::CExpr]) {
1171        // Push f, g; build the trampoline closure with 2 captures.
1172        self.compile_expr(&args[0], false);
1173        self.compile_expr(&args[1], false);
1174        let nid = self.pool.node_id("n_flow_sequential");
1175        let code = vec![
1176            // Locals: [f=0, g=1, x=2]
1177            Op::LoadLocal(0),                                  // push f
1178            Op::LoadLocal(2),                                  // push x
1179            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = f(x)
1180            // stack: [r]
1181            Op::StoreLocal(3),                                 // tmp = r
1182            Op::LoadLocal(1),                                  // push g
1183            Op::LoadLocal(3),                                  // push tmp
1184            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = g(tmp)
1185            Op::Return,
1186        ];
1187        let fn_id = self.install_trampoline("__flow_sequential", 3, 4, code);
1188        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
1189    }
1190
1191    /// `flow.parallel(fa, fb)` returns a closure `() -> (fa(), fb())`.
1192    /// Implementation is sequential: each function is called in order
1193    /// and the results are packed into a 2-tuple. The spec (§11.2)
1194    /// allows the runtime to apply true parallelism here; that needs
1195    /// a thread-safe handler split and is left to a follow-up. The
1196    /// signature is what users program against — sequential vs threaded
1197    /// is an implementation detail invisible to the type system.
1198    fn emit_flow_parallel(&mut self, args: &[a::CExpr]) {
1199        // Push fa, fb; build a 0-arg trampoline closure with 2 captures.
1200        self.compile_expr(&args[0], false);
1201        self.compile_expr(&args[1], false);
1202        let nid = self.pool.node_id("n_flow_parallel");
1203        let code = vec![
1204            // Locals: [fa=0, fb=1]
1205            Op::LoadLocal(0),                                  // push fa
1206            Op::CallClosure { arity: 0, node_id_idx: nid },    // a = fa()
1207            Op::LoadLocal(1),                                  // push fb
1208            Op::CallClosure { arity: 0, node_id_idx: nid },    // b = fb()
1209            Op::MakeTuple(2),                                  // (a, b)
1210            Op::Return,
1211        ];
1212        let fn_id = self.install_trampoline("__flow_parallel", 2, 2, code);
1213        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
1214    }
1215
1216    /// `flow.parallel_list(actions)` runs each 0-arg closure in `actions`
1217    /// and returns the results as a list in input order. Variadic
1218    /// counterpart to `flow.parallel`. Sequential under the hood — the
1219    /// spec (§11.2) reserves true threading for a future scheduler.
1220    /// Compiled inline (mirrors `list.map`) so closure args can flow
1221    /// through `CallClosure` without a heap-allocated trampoline.
1222    fn emit_flow_parallel_list(&mut self, args: &[a::CExpr]) {
1223        // xs := actions
1224        self.compile_expr(&args[0], false);
1225        let xs = self.alloc_local("__fpl_xs");
1226        self.emit(Op::StoreLocal(xs));
1227
1228        // out := []
1229        self.emit(Op::MakeList(0));
1230        let out = self.alloc_local("__fpl_out");
1231        self.emit(Op::StoreLocal(out));
1232
1233        // i := 0
1234        let zero = self.pool.int(0);
1235        self.emit(Op::PushConst(zero));
1236        let i = self.alloc_local("__fpl_i");
1237        self.emit(Op::StoreLocal(i));
1238
1239        // loop_top: while i < len(xs) { ... }
1240        let loop_top = self.code.len();
1241        self.emit(Op::LoadLocal(i));
1242        self.emit(Op::LoadLocal(xs));
1243        self.emit(Op::GetListLen);
1244        self.emit(Op::NumLt);
1245        let j_exit = self.code.len();
1246        self.emit(Op::JumpIfNot(0));
1247
1248        // body: out := out ++ [xs[i]()]
1249        let nid = self.pool.node_id("n_flow_parallel_list");
1250        self.emit(Op::LoadLocal(out));
1251        self.emit(Op::LoadLocal(xs));
1252        self.emit(Op::LoadLocal(i));
1253        self.emit(Op::GetListElemDyn);
1254        self.emit(Op::CallClosure { arity: 0, node_id_idx: nid });
1255        self.emit(Op::ListAppend);
1256        self.emit(Op::StoreLocal(out));
1257
1258        // i := i + 1
1259        self.emit(Op::LoadLocal(i));
1260        let one = self.pool.int(1);
1261        self.emit(Op::PushConst(one));
1262        self.emit(Op::NumAdd);
1263        self.emit(Op::StoreLocal(i));
1264
1265        // jump back
1266        let jump_back = self.code.len();
1267        let back = (loop_top as i32) - (jump_back as i32 + 1);
1268        self.emit(Op::Jump(back));
1269
1270        // exit: patch j_exit, push out
1271        let exit_target = self.code.len() as i32;
1272        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
1273            *off = exit_target - (j_exit as i32 + 1);
1274        }
1275        self.emit(Op::LoadLocal(out));
1276    }
1277
1278    /// `flow.branch(cond, t, f)` returns a closure `(x) -> if cond(x) then t(x) else f(x)`.
1279    fn emit_flow_branch(&mut self, args: &[a::CExpr]) {
1280        self.compile_expr(&args[0], false);
1281        self.compile_expr(&args[1], false);
1282        self.compile_expr(&args[2], false);
1283        let nid = self.pool.node_id("n_flow_branch");
1284        let mut code = vec![
1285            // Locals: [cond=0, t=1, f=2, x=3]
1286            Op::LoadLocal(0),                               // push cond
1287            Op::LoadLocal(3),                               // push x
1288            Op::CallClosure { arity: 1, node_id_idx: nid }, // bool
1289        ];
1290        let j_false = code.len();
1291        code.push(Op::JumpIfNot(0));                        // patched
1292        // true arm: t(x)
1293        code.push(Op::LoadLocal(1));
1294        code.push(Op::LoadLocal(3));
1295        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
1296        code.push(Op::Return);
1297        // false arm
1298        let false_target = code.len() as i32;
1299        if let Op::JumpIfNot(off) = &mut code[j_false] {
1300            *off = false_target - (j_false as i32 + 1);
1301        }
1302        code.push(Op::LoadLocal(2));
1303        code.push(Op::LoadLocal(3));
1304        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
1305        code.push(Op::Return);
1306
1307        let fn_id = self.install_trampoline("__flow_branch", 4, 4, code);
1308        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
1309    }
1310
1311    /// `flow.retry(f, max_attempts)` returns a closure `(x) -> Result[U, E]`
1312    /// that calls `f(x)` up to `max_attempts` times, returning the first
1313    /// `Ok` or the final `Err`.
1314    fn emit_flow_retry(&mut self, args: &[a::CExpr]) {
1315        self.compile_expr(&args[0], false);
1316        self.compile_expr(&args[1], false);
1317        let call_nid = self.pool.node_id("n_flow_retry");
1318        let ok_idx = self.pool.variant("Ok");
1319        let zero_const = self.pool.int(0);
1320        let one_const = self.pool.int(1);
1321        // Locals: [f=0, max=1, x=2, i=3, last=4]
1322        let mut code = vec![
1323            // i := 0
1324            Op::PushConst(zero_const),
1325            Op::StoreLocal(3),
1326        ];
1327        // loop_top: while i < max
1328        let loop_top = code.len() as i32;
1329        code.push(Op::LoadLocal(3));
1330        code.push(Op::LoadLocal(1));
1331        code.push(Op::NumLt);
1332        let j_done = code.len();
1333        code.push(Op::JumpIfNot(0));                       // patched
1334
1335        // body: r := f(x); last := r
1336        code.push(Op::LoadLocal(0));
1337        code.push(Op::LoadLocal(2));
1338        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
1339        code.push(Op::StoreLocal(4));
1340
1341        // Test variant Ok on last; if so, return last.
1342        code.push(Op::LoadLocal(4));
1343        code.push(Op::TestVariant(ok_idx));
1344        let j_was_err = code.len();
1345        code.push(Op::JumpIfNot(0));                       // patched: skip return
1346        code.push(Op::LoadLocal(4));
1347        code.push(Op::Return);
1348
1349        // was_err: i := i + 1; jump loop_top
1350        let was_err_target = code.len() as i32;
1351        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
1352            *off = was_err_target - (j_was_err as i32 + 1);
1353        }
1354        code.push(Op::LoadLocal(3));
1355        code.push(Op::PushConst(one_const));
1356        code.push(Op::NumAdd);
1357        code.push(Op::StoreLocal(3));
1358        let pc_after_jump = code.len() as i32 + 1;
1359        code.push(Op::Jump(loop_top - pc_after_jump));
1360
1361        // done: return last (the final Err, or Unit if max=0).
1362        let done_target = code.len() as i32;
1363        if let Op::JumpIfNot(off) = &mut code[j_done] {
1364            *off = done_target - (j_done as i32 + 1);
1365        }
1366        code.push(Op::LoadLocal(4));
1367        code.push(Op::Return);
1368
1369        let fn_id = self.install_trampoline("__flow_retry", 3, 5, code);
1370        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
1371    }
1372
1373    /// `flow.retry_with_backoff(f, attempts, base_ms)` (#226). Variant
1374    /// of `flow.retry` that sleeps between attempts. The first
1375    /// attempt fires immediately; attempt k > 1 waits `base_ms *
1376    /// 2^(k-2)` ms before retrying. Sleeps go through
1377    /// `time.sleep_ms`, which is why the resulting closure carries
1378    /// `[time]` in its effect row even though the underlying `f` is
1379    /// pure.
1380    fn emit_flow_retry_with_backoff(&mut self, args: &[a::CExpr]) {
1381        // Push captures: f, max, base_ms. The trampoline takes one
1382        // call-time arg `x`, so capture_count = 3, arity = 4.
1383        self.compile_expr(&args[0], false);
1384        self.compile_expr(&args[1], false);
1385        self.compile_expr(&args[2], false);
1386        let call_nid    = self.pool.node_id("n_flow_retry_backoff");
1387        let sleep_nid   = self.pool.node_id("n_flow_retry_backoff_sleep");
1388        let kind_idx    = self.pool.str("time");
1389        let op_idx      = self.pool.str("sleep_ms");
1390        let ok_idx      = self.pool.variant("Ok");
1391        let zero_const  = self.pool.int(0);
1392        let one_const   = self.pool.int(1);
1393        let two_const   = self.pool.int(2);
1394        // Locals layout:
1395        //   0=f, 1=max, 2=base_ms (captures)
1396        //   3=x (arg)
1397        //   4=i, 5=last, 6=next_delay (working state)
1398        let mut code = vec![
1399            // next_delay := base_ms
1400            Op::LoadLocal(2),
1401            Op::StoreLocal(6),
1402            // i := 0
1403            Op::PushConst(zero_const),
1404            Op::StoreLocal(4),
1405        ];
1406
1407        let loop_top = code.len() as i32;
1408        // while i < max
1409        code.push(Op::LoadLocal(4));
1410        code.push(Op::LoadLocal(1));
1411        code.push(Op::NumLt);
1412        let j_done = code.len();
1413        code.push(Op::JumpIfNot(0)); // patched
1414
1415        // if i > 0: time.sleep_ms(next_delay); next_delay := next_delay * 2
1416        code.push(Op::PushConst(zero_const));
1417        code.push(Op::LoadLocal(4));
1418        code.push(Op::NumLt);                // 0 < i ?
1419        let j_no_sleep = code.len();
1420        code.push(Op::JumpIfNot(0));         // patched: skip the sleep block
1421        // Sleep
1422        code.push(Op::LoadLocal(6));         // arg = next_delay
1423        code.push(Op::EffectCall {
1424            kind_idx, op_idx, arity: 1, node_id_idx: sleep_nid,
1425        });
1426        code.push(Op::Pop);                  // discard the Unit result
1427        // next_delay := next_delay * 2
1428        code.push(Op::LoadLocal(6));
1429        code.push(Op::PushConst(two_const));
1430        code.push(Op::NumMul);
1431        code.push(Op::StoreLocal(6));
1432        // patch the no-sleep skip
1433        let after_sleep = code.len() as i32;
1434        if let Op::JumpIfNot(off) = &mut code[j_no_sleep] {
1435            *off = after_sleep - (j_no_sleep as i32 + 1);
1436        }
1437
1438        // last := f(x)
1439        code.push(Op::LoadLocal(0));
1440        code.push(Op::LoadLocal(3));
1441        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
1442        code.push(Op::StoreLocal(5));
1443
1444        // if Ok(last): return last
1445        code.push(Op::LoadLocal(5));
1446        code.push(Op::TestVariant(ok_idx));
1447        let j_was_err = code.len();
1448        code.push(Op::JumpIfNot(0)); // patched
1449        code.push(Op::LoadLocal(5));
1450        code.push(Op::Return);
1451
1452        // was_err: i := i + 1; jump loop_top
1453        let was_err_target = code.len() as i32;
1454        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
1455            *off = was_err_target - (j_was_err as i32 + 1);
1456        }
1457        code.push(Op::LoadLocal(4));
1458        code.push(Op::PushConst(one_const));
1459        code.push(Op::NumAdd);
1460        code.push(Op::StoreLocal(4));
1461        let pc_after_jump = code.len() as i32 + 1;
1462        code.push(Op::Jump(loop_top - pc_after_jump));
1463
1464        // done: return last (the final Err, or Unit if max=0).
1465        let done_target = code.len() as i32;
1466        if let Op::JumpIfNot(off) = &mut code[j_done] {
1467            *off = done_target - (j_done as i32 + 1);
1468        }
1469        code.push(Op::LoadLocal(5));
1470        code.push(Op::Return);
1471
1472        let fn_id = self.install_trampoline("__flow_retry_backoff", 4, 7, code);
1473        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
1474    }
1475}
1476
1477/// Collect free variables referenced in `e` that are not in `bound`.
1478/// Mutates `bound` to track let/lambda introductions during the walk;
1479/// the caller's set is preserved on return because Rust's borrow rules
1480/// force us to clone for sub-scopes that rebind a name.
1481fn free_vars(e: &a::CExpr, bound: &mut std::collections::HashSet<String>, out: &mut Vec<String>) {
1482    match e {
1483        a::CExpr::Literal { .. } => {}
1484        a::CExpr::Var { name } => {
1485            if !bound.contains(name) && !out.contains(name) {
1486                out.push(name.clone());
1487            }
1488        }
1489        a::CExpr::Call { callee, args } => {
1490            free_vars(callee, bound, out);
1491            for a in args { free_vars(a, bound, out); }
1492        }
1493        a::CExpr::Let { name, value, body, .. } => {
1494            free_vars(value, bound, out);
1495            let was_bound = bound.contains(name);
1496            bound.insert(name.clone());
1497            free_vars(body, bound, out);
1498            if !was_bound { bound.remove(name); }
1499        }
1500        a::CExpr::Match { scrutinee, arms } => {
1501            free_vars(scrutinee, bound, out);
1502            for arm in arms {
1503                let mut local_bound = bound.clone();
1504                pattern_binders(&arm.pattern, &mut local_bound);
1505                free_vars(&arm.body, &mut local_bound, out);
1506            }
1507        }
1508        a::CExpr::Block { statements, result } => {
1509            let mut local_bound = bound.clone();
1510            for s in statements { free_vars(s, &mut local_bound, out); }
1511            free_vars(result, &mut local_bound, out);
1512        }
1513        a::CExpr::Constructor { args, .. } => {
1514            for a in args { free_vars(a, bound, out); }
1515        }
1516        a::CExpr::RecordLit { fields } => {
1517            for f in fields { free_vars(&f.value, bound, out); }
1518        }
1519        a::CExpr::TupleLit { items } | a::CExpr::ListLit { items } => {
1520            for it in items { free_vars(it, bound, out); }
1521        }
1522        a::CExpr::FieldAccess { value, .. } => free_vars(value, bound, out),
1523        a::CExpr::Lambda { params, body, .. } => {
1524            let mut inner = bound.clone();
1525            for p in params { inner.insert(p.name.clone()); }
1526            free_vars(body, &mut inner, out);
1527        }
1528        a::CExpr::BinOp { lhs, rhs, .. } => {
1529            free_vars(lhs, bound, out);
1530            free_vars(rhs, bound, out);
1531        }
1532        a::CExpr::UnaryOp { expr, .. } => free_vars(expr, bound, out),
1533        a::CExpr::Return { value } => free_vars(value, bound, out),
1534    }
1535}
1536
1537fn pattern_binders(p: &a::Pattern, bound: &mut std::collections::HashSet<String>) {
1538    match p {
1539        a::Pattern::PWild | a::Pattern::PLiteral { .. } => {}
1540        a::Pattern::PVar { name } => { bound.insert(name.clone()); }
1541        a::Pattern::PConstructor { args, .. } => {
1542            for a in args { pattern_binders(a, bound); }
1543        }
1544        a::Pattern::PRecord { fields } => {
1545            for f in fields { pattern_binders(&f.pattern, bound); }
1546        }
1547        a::Pattern::PTuple { items } => {
1548            for it in items { pattern_binders(it, bound); }
1549        }
1550    }
1551}