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                if let Op::JumpIfNot(off) = &mut self.code[j] {
469                    *off = fail_target - (j as i32 + 1);
470                }
471            }
472            self.next_local = arm_start_locals;
473            self.locals = arm_start_locals_map;
474        }
475        let panic_msg_idx = self.pool.str("non-exhaustive match");
476        self.emit(Op::Panic(panic_msg_idx));
477
478        let end_target = self.code.len() as i32;
479        for j in end_jumps {
480            if let Op::Jump(off) = &mut self.code[j] {
481                *off = end_target - (j as i32 + 1);
482            }
483        }
484    }
485
486    fn compile_pattern_test(&mut self, p: &a::Pattern, bindings: &mut Vec<(String, u16)>) -> Vec<usize> {
487        let mut fails = Vec::new();
488        match p {
489            a::Pattern::PWild => { self.emit(Op::Pop); }
490            a::Pattern::PVar { name } => {
491                let slot = self.alloc_local(name);
492                self.emit(Op::StoreLocal(slot));
493                bindings.push((name.clone(), slot));
494            }
495            a::Pattern::PLiteral { value } => {
496                self.compile_lit(value);
497                match value {
498                    a::CLit::Str { .. } => self.emit(Op::StrEq),
499                    a::CLit::Bytes { .. } => self.emit(Op::BytesEq),
500                    _ => self.emit(Op::NumEq),
501                }
502                let j = self.code.len();
503                self.emit(Op::JumpIfNot(0));
504                fails.push(j);
505            }
506            a::Pattern::PConstructor { name, args } => {
507                let name_idx = self.pool.variant(name);
508                self.emit(Op::Dup);
509                self.emit(Op::TestVariant(name_idx));
510                let j = self.code.len();
511                self.emit(Op::JumpIfNot(0));
512                fails.push(j);
513                if args.is_empty() {
514                    self.emit(Op::Pop);
515                } else if args.len() == 1 {
516                    self.emit(Op::GetVariantArg(0));
517                    let sub_fails = self.compile_pattern_test(&args[0], bindings);
518                    fails.extend(sub_fails);
519                } else {
520                    let slot = self.alloc_local("__variant");
521                    self.emit(Op::StoreLocal(slot));
522                    for (i, arg) in args.iter().enumerate() {
523                        self.emit(Op::LoadLocal(slot));
524                        self.emit(Op::GetVariantArg(i as u16));
525                        let sub_fails = self.compile_pattern_test(arg, bindings);
526                        fails.extend(sub_fails);
527                    }
528                }
529            }
530            a::Pattern::PRecord { fields } => {
531                let slot = self.alloc_local("__record");
532                self.emit(Op::StoreLocal(slot));
533                for f in fields {
534                    self.emit(Op::LoadLocal(slot));
535                    let fi = self.pool.field(&f.name);
536                    self.emit(Op::GetField(fi));
537                    let sub_fails = self.compile_pattern_test(&f.pattern, bindings);
538                    fails.extend(sub_fails);
539                }
540            }
541            a::Pattern::PTuple { items } => {
542                let slot = self.alloc_local("__tuple");
543                self.emit(Op::StoreLocal(slot));
544                for (i, item) in items.iter().enumerate() {
545                    self.emit(Op::LoadLocal(slot));
546                    self.emit(Op::GetElem(i as u16));
547                    let sub_fails = self.compile_pattern_test(item, bindings);
548                    fails.extend(sub_fails);
549                }
550            }
551        }
552        fails
553    }
554
555    /// Compile a Lambda: collect free variables that resolve to outer-scope
556    /// locals, register a synthetic function, emit MakeClosure with the
557    /// captured values pushed in order.
558    fn compile_lambda(&mut self, params: &[a::Param], body: &a::CExpr) {
559        // Free vars = vars referenced in body that aren't bound locally.
560        let mut bound: std::collections::HashSet<String> = params.iter().map(|p| p.name.clone()).collect();
561        let mut frees: Vec<String> = Vec::new();
562        free_vars(body, &mut bound, &mut frees);
563
564        // Filter to those that are in the enclosing locals (captures) —
565        // skip globals (function names) which are referenced directly.
566        let captures: Vec<String> = frees.into_iter()
567            .filter(|n| self.locals.contains_key(n) && !self.function_names.contains_key(n))
568            .collect();
569
570        // Allocate a fresh fn_id by appending a placeholder Function.
571        let fn_id = self.next_fn_id.len() as u32;
572        self.next_fn_id.push(Function {
573            name: format!("__lambda_{fn_id}"),
574            arity: (captures.len() + params.len()) as u16,
575            locals_count: 0,
576            code: Vec::new(),
577            effects: Vec::new(),
578            // See #222: filled in at the end of the compile pass.
579            body_hash: crate::program::ZERO_BODY_HASH,
580            // Lambdas don't carry refinements at the surface today
581            // (closure params don't accept `Type{x | ...}` syntax in
582            // the parser). #209 stays focused on top-level fn decls;
583            // closure-param refinements are a follow-up.
584            refinements: Vec::new(),
585        });
586
587        // Emit code at the lambda site: load each captured local, then MakeClosure.
588        for c in &captures {
589            let slot = *self.locals.get(c).expect("free var must be in scope");
590            self.emit(Op::LoadLocal(slot));
591        }
592        self.emit(Op::MakeClosure { fn_id, capture_count: captures.len() as u16 });
593
594        // Queue the body for later compilation.
595        self.pending_lambdas.push(PendingLambda {
596            fn_id,
597            capture_names: captures,
598            params: params.to_vec(),
599            body: body.clone(),
600        });
601    }
602
603    /// Higher-order stdlib ops on Result/Option whose function arg is a
604    /// closure. Emit inline: pattern-match on the variant, invoke the
605    /// closure when applicable, return wrapped result.
606    fn try_emit_higher_order(
607        &mut self,
608        module: &str,
609        op: &str,
610        args: &[a::CExpr],
611        node_id_idx: u32,
612    ) -> bool {
613        match (module, op) {
614            ("result", "map") => self.emit_variant_map(args, "Ok", true),
615            ("result", "and_then") => self.emit_variant_map(args, "Ok", false),
616            ("result", "map_err") => self.emit_variant_map(args, "Err", true),
617            ("result", "or_else") => self.emit_variant_or_else(args, "Err", 1),
618            ("option", "map") => self.emit_variant_map(args, "Some", true),
619            ("option", "and_then") => self.emit_variant_map(args, "Some", false),
620            ("option", "or_else") => self.emit_variant_or_else(args, "None", 0),
621            ("list", "map") => self.emit_list_map(args),
622            ("list", "filter") => self.emit_list_filter(args),
623            ("list", "fold") => self.emit_list_fold(args),
624            ("map", "fold") => self.emit_map_fold(args, node_id_idx),
625            ("flow", "sequential") => self.emit_flow_sequential(args),
626            ("flow", "branch") => self.emit_flow_branch(args),
627            ("flow", "retry") => self.emit_flow_retry(args),
628            ("flow", "retry_with_backoff") => self.emit_flow_retry_with_backoff(args),
629            ("flow", "parallel") => self.emit_flow_parallel(args),
630            ("flow", "parallel_list") => self.emit_flow_parallel_list(args),
631            _ => return false,
632        }
633        true
634    }
635
636    /// `list.map(xs, f)` — inline loop applying `f` to each element.
637    /// Stack contract: pushes the resulting List.
638    fn emit_list_map(&mut self, args: &[a::CExpr]) {
639        // Compile xs and f, store both as locals.
640        self.compile_expr(&args[0], false);
641        let xs = self.alloc_local("__lm_xs");
642        self.emit(Op::StoreLocal(xs));
643        self.compile_expr(&args[1], false);
644        let f = self.alloc_local("__lm_f");
645        self.emit(Op::StoreLocal(f));
646
647        // out := []
648        self.emit(Op::MakeList(0));
649        let out = self.alloc_local("__lm_out");
650        self.emit(Op::StoreLocal(out));
651
652        // i := 0
653        let zero = self.pool.int(0);
654        self.emit(Op::PushConst(zero));
655        let i = self.alloc_local("__lm_i");
656        self.emit(Op::StoreLocal(i));
657
658        // loop_top: while i < len(xs) { ... }
659        let loop_top = self.code.len();
660        self.emit(Op::LoadLocal(i));
661        self.emit(Op::LoadLocal(xs));
662        self.emit(Op::GetListLen);
663        self.emit(Op::NumLt);
664        let j_exit = self.code.len();
665        self.emit(Op::JumpIfNot(0));
666
667        // body: out := out ++ [f(xs[i])]
668        let nid = self.pool.node_id("n_list_map");
669        self.emit(Op::LoadLocal(out));
670        self.emit(Op::LoadLocal(f));
671        self.emit(Op::LoadLocal(xs));
672        self.emit(Op::LoadLocal(i));
673        self.emit(Op::GetListElemDyn);
674        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
675        self.emit(Op::ListAppend);
676        self.emit(Op::StoreLocal(out));
677
678        // i := i + 1
679        self.emit(Op::LoadLocal(i));
680        let one = self.pool.int(1);
681        self.emit(Op::PushConst(one));
682        self.emit(Op::NumAdd);
683        self.emit(Op::StoreLocal(i));
684
685        // jump back
686        let jump_back = self.code.len();
687        let back = (loop_top as i32) - (jump_back as i32 + 1);
688        self.emit(Op::Jump(back));
689
690        // exit: patch j_exit, push out
691        let exit_target = self.code.len() as i32;
692        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
693            *off = exit_target - (j_exit as i32 + 1);
694        }
695        self.emit(Op::LoadLocal(out));
696    }
697
698    /// `list.filter(xs, pred)` — keep elements where pred returns true.
699    fn emit_list_filter(&mut self, args: &[a::CExpr]) {
700        self.compile_expr(&args[0], false);
701        let xs = self.alloc_local("__lf_xs");
702        self.emit(Op::StoreLocal(xs));
703        self.compile_expr(&args[1], false);
704        let f = self.alloc_local("__lf_f");
705        self.emit(Op::StoreLocal(f));
706
707        self.emit(Op::MakeList(0));
708        let out = self.alloc_local("__lf_out");
709        self.emit(Op::StoreLocal(out));
710
711        let zero = self.pool.int(0);
712        self.emit(Op::PushConst(zero));
713        let i = self.alloc_local("__lf_i");
714        self.emit(Op::StoreLocal(i));
715
716        let loop_top = self.code.len();
717        self.emit(Op::LoadLocal(i));
718        self.emit(Op::LoadLocal(xs));
719        self.emit(Op::GetListLen);
720        self.emit(Op::NumLt);
721        let j_exit = self.code.len();
722        self.emit(Op::JumpIfNot(0));
723
724        // x := xs[i]
725        self.emit(Op::LoadLocal(xs));
726        self.emit(Op::LoadLocal(i));
727        self.emit(Op::GetListElemDyn);
728        let x = self.alloc_local("__lf_x");
729        self.emit(Op::StoreLocal(x));
730
731        // if pred(x) { out := out ++ [x] }
732        let nid = self.pool.node_id("n_list_filter");
733        self.emit(Op::LoadLocal(f));
734        self.emit(Op::LoadLocal(x));
735        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
736        let j_skip = self.code.len();
737        self.emit(Op::JumpIfNot(0));
738
739        self.emit(Op::LoadLocal(out));
740        self.emit(Op::LoadLocal(x));
741        self.emit(Op::ListAppend);
742        self.emit(Op::StoreLocal(out));
743
744        let skip_target = self.code.len() as i32;
745        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
746            *off = skip_target - (j_skip as i32 + 1);
747        }
748
749        // i := i + 1
750        self.emit(Op::LoadLocal(i));
751        let one = self.pool.int(1);
752        self.emit(Op::PushConst(one));
753        self.emit(Op::NumAdd);
754        self.emit(Op::StoreLocal(i));
755
756        let jump_back = self.code.len();
757        let back = (loop_top as i32) - (jump_back as i32 + 1);
758        self.emit(Op::Jump(back));
759
760        let exit_target = self.code.len() as i32;
761        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
762            *off = exit_target - (j_exit as i32 + 1);
763        }
764        self.emit(Op::LoadLocal(out));
765    }
766
767    /// `list.fold(xs, init, f)` — left fold with two-arg combiner.
768    fn emit_list_fold(&mut self, args: &[a::CExpr]) {
769        // args: xs, init, f
770        self.compile_expr(&args[0], false);
771        let xs = self.alloc_local("__lo_xs");
772        self.emit(Op::StoreLocal(xs));
773        self.compile_expr(&args[1], false);
774        let acc = self.alloc_local("__lo_acc");
775        self.emit(Op::StoreLocal(acc));
776        self.compile_expr(&args[2], false);
777        let f = self.alloc_local("__lo_f");
778        self.emit(Op::StoreLocal(f));
779
780        let zero = self.pool.int(0);
781        self.emit(Op::PushConst(zero));
782        let i = self.alloc_local("__lo_i");
783        self.emit(Op::StoreLocal(i));
784
785        let loop_top = self.code.len();
786        self.emit(Op::LoadLocal(i));
787        self.emit(Op::LoadLocal(xs));
788        self.emit(Op::GetListLen);
789        self.emit(Op::NumLt);
790        let j_exit = self.code.len();
791        self.emit(Op::JumpIfNot(0));
792
793        // acc := f(acc, xs[i])
794        let nid = self.pool.node_id("n_list_fold");
795        self.emit(Op::LoadLocal(f));
796        self.emit(Op::LoadLocal(acc));
797        self.emit(Op::LoadLocal(xs));
798        self.emit(Op::LoadLocal(i));
799        self.emit(Op::GetListElemDyn);
800        self.emit(Op::CallClosure { arity: 2, node_id_idx: nid });
801        self.emit(Op::StoreLocal(acc));
802
803        // i := i + 1
804        self.emit(Op::LoadLocal(i));
805        let one = self.pool.int(1);
806        self.emit(Op::PushConst(one));
807        self.emit(Op::NumAdd);
808        self.emit(Op::StoreLocal(i));
809
810        let jump_back = self.code.len();
811        let back = (loop_top as i32) - (jump_back as i32 + 1);
812        self.emit(Op::Jump(back));
813
814        let exit_target = self.code.len() as i32;
815        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
816            *off = exit_target - (j_exit as i32 + 1);
817        }
818        self.emit(Op::LoadLocal(acc));
819    }
820
821    /// `map.fold(m, init, f)` — left fold over `Map[K, V]` entries with a
822    /// three-arg combiner `f(acc, k, v)`. Iteration order matches
823    /// `map.entries` (BTreeMap-sorted by key). Materializes the entry
824    /// list once via the runtime's `("map", "entries")` op, then runs
825    /// the same inline loop as `list.fold`.
826    fn emit_map_fold(&mut self, args: &[a::CExpr], node_id_idx: u32) {
827        // xs := map.entries(m)
828        self.compile_expr(&args[0], false);
829        let map_kind = self.pool.str("map");
830        let entries_op = self.pool.str("entries");
831        self.emit(Op::EffectCall {
832            kind_idx: map_kind,
833            op_idx: entries_op,
834            arity: 1,
835            node_id_idx,
836        });
837        let xs = self.alloc_local("__mf_xs");
838        self.emit(Op::StoreLocal(xs));
839
840        // acc := init
841        self.compile_expr(&args[1], false);
842        let acc = self.alloc_local("__mf_acc");
843        self.emit(Op::StoreLocal(acc));
844
845        // f := <closure>
846        self.compile_expr(&args[2], false);
847        let f = self.alloc_local("__mf_f");
848        self.emit(Op::StoreLocal(f));
849
850        // i := 0
851        let zero = self.pool.int(0);
852        self.emit(Op::PushConst(zero));
853        let i = self.alloc_local("__mf_i");
854        self.emit(Op::StoreLocal(i));
855
856        // loop_top: while i < len(xs)
857        let loop_top = self.code.len();
858        self.emit(Op::LoadLocal(i));
859        self.emit(Op::LoadLocal(xs));
860        self.emit(Op::GetListLen);
861        self.emit(Op::NumLt);
862        let j_exit = self.code.len();
863        self.emit(Op::JumpIfNot(0));
864
865        // pair := xs[i]
866        self.emit(Op::LoadLocal(xs));
867        self.emit(Op::LoadLocal(i));
868        self.emit(Op::GetListElemDyn);
869        let pair = self.alloc_local("__mf_pair");
870        self.emit(Op::StoreLocal(pair));
871
872        // acc := f(acc, pair.0, pair.1)
873        let nid = self.pool.node_id("n_map_fold");
874        self.emit(Op::LoadLocal(f));
875        self.emit(Op::LoadLocal(acc));
876        self.emit(Op::LoadLocal(pair));
877        self.emit(Op::GetElem(0));
878        self.emit(Op::LoadLocal(pair));
879        self.emit(Op::GetElem(1));
880        self.emit(Op::CallClosure { arity: 3, node_id_idx: nid });
881        self.emit(Op::StoreLocal(acc));
882
883        // i := i + 1
884        self.emit(Op::LoadLocal(i));
885        let one = self.pool.int(1);
886        self.emit(Op::PushConst(one));
887        self.emit(Op::NumAdd);
888        self.emit(Op::StoreLocal(i));
889
890        let jump_back = self.code.len();
891        let back = (loop_top as i32) - (jump_back as i32 + 1);
892        self.emit(Op::Jump(back));
893
894        let exit_target = self.code.len() as i32;
895        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
896            *off = exit_target - (j_exit as i32 + 1);
897        }
898        self.emit(Op::LoadLocal(acc));
899    }
900
901    /// Inline pattern: `<module>.map(v, f)` and friends.
902    /// `wrap_with`: variant tag whose payload triggers the call (Ok / Some / Err).
903    /// `wrap_result`: if true, wrap the closure's result back in `wrap_with`
904    /// (map shape); if false, expect the closure to return a wrapped value
905    /// itself (and_then shape).
906    fn emit_variant_map(
907        &mut self,
908        args: &[a::CExpr],
909        wrap_with: &str,
910        wrap_result: bool,
911    ) {
912        // args[0] = the wrapped value (Result/Option), args[1] = closure
913        let wrap_idx = self.pool.variant(wrap_with);
914
915        // Compile and store the value into a local, evaluate closure on top of stack.
916        self.compile_expr(&args[0], false);
917        let val_slot = self.alloc_local("__hov");
918        self.emit(Op::StoreLocal(val_slot));
919
920        self.compile_expr(&args[1], false);
921        let f_slot = self.alloc_local("__hof");
922        self.emit(Op::StoreLocal(f_slot));
923
924        // Stack discipline:
925        //   load val ⇒ [v]
926        //   dup     ⇒ [v, v]
927        //   test    ⇒ [v, Bool]
928        //   jumpifnot ⇒ [v]
929        // Both branches end with [v] before the branch body.
930        self.emit(Op::LoadLocal(val_slot));
931        self.emit(Op::Dup);
932        self.emit(Op::TestVariant(wrap_idx));
933        let j_skip = self.code.len();
934        self.emit(Op::JumpIfNot(0));
935
936        // Matched arm: extract payload, call closure on it.
937        self.emit(Op::GetVariantArg(0));
938        let arg_slot = self.alloc_local("__hov_arg");
939        self.emit(Op::StoreLocal(arg_slot));
940        self.emit(Op::LoadLocal(f_slot));
941        self.emit(Op::LoadLocal(arg_slot));
942        let nid = self.pool.node_id("n_hov");
943        self.emit(Op::CallClosure { arity: 1, node_id_idx: nid });
944        if wrap_result {
945            self.emit(Op::MakeVariant { name_idx: wrap_idx, arity: 1 });
946        }
947        let j_end = self.code.len();
948        self.emit(Op::Jump(0));
949
950        // Skip arm: stack already has [v] from the failed Dup; nothing to do.
951        let skip_target = self.code.len() as i32;
952        if let Op::JumpIfNot(off) = &mut self.code[j_skip] {
953            *off = skip_target - (j_skip as i32 + 1);
954        }
955
956        let end_target = self.code.len() as i32;
957        if let Op::Jump(off) = &mut self.code[j_end] {
958            *off = end_target - (j_end as i32 + 1);
959        }
960    }
961
962    /// Sibling of `emit_variant_map` for the recovery combinators
963    /// `result.or_else` and `option.or_else`. Differences from
964    /// `emit_variant_map`:
965    ///   - matches on the *negative* variant (`Err` / `None`)
966    ///   - the closure's result becomes the call's result directly,
967    ///     with no wrapping (it is itself a `Result` / `Option`)
968    ///   - `option.or_else`'s closure takes zero args (`None` has no
969    ///     payload to forward)
970    fn emit_variant_or_else(
971        &mut self,
972        args: &[a::CExpr],
973        match_on: &str,
974        closure_arity: u16,
975    ) {
976        let match_idx = self.pool.variant(match_on);
977
978        self.compile_expr(&args[0], false);
979        let val_slot = self.alloc_local("__hoe");
980        self.emit(Op::StoreLocal(val_slot));
981
982        self.compile_expr(&args[1], false);
983        let f_slot = self.alloc_local("__hoe_f");
984        self.emit(Op::StoreLocal(f_slot));
985
986        // Stack discipline mirrors emit_variant_map:
987        //   load val      ⇒ [v]
988        //   dup           ⇒ [v, v]
989        //   test          ⇒ [v, Bool]
990        //   jumpifnot     ⇒ [v]
991        // The unmatched arm leaves [v] (Ok/Some unchanged); the
992        // matched arm pops [v] and pushes the closure's result.
993        self.emit(Op::LoadLocal(val_slot));
994        self.emit(Op::Dup);
995        self.emit(Op::TestVariant(match_idx));
996        let j_skip = self.code.len();
997        self.emit(Op::JumpIfNot(0));
998
999        // Matched arm: pop the duplicate left on the stack,
1000        // then call the closure with whatever payload it expects.
1001        self.emit(Op::Pop);
1002        self.emit(Op::LoadLocal(f_slot));
1003        if closure_arity == 1 {
1004            self.emit(Op::LoadLocal(val_slot));
1005            self.emit(Op::GetVariantArg(0));
1006        }
1007        let nid = self.pool.node_id("n_hoe");
1008        self.emit(Op::CallClosure { arity: closure_arity, node_id_idx: nid });
1009
1010        let j_end = self.code.len();
1011        self.emit(Op::Jump(0));
1012
1013        // Unmatched arm: stack already holds [v]; 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    // ---- std.flow trampolines ----------------------------------------
1026    //
1027    // Each flow.<op>(c1, c2, ...) call site:
1028    //   1. compiles its closure args and leaves them on the stack
1029    //   2. registers a fresh "trampoline" Function whose body invokes
1030    //      those captured closures appropriately
1031    //   3. emits MakeClosure { fn_id: trampoline, capture_count: N }
1032    //
1033    // The trampoline's parameter layout is [capture_0, ..., capture_{N-1},
1034    // arg_0, ...]: captures first, the closure's own args after.
1035
1036    /// Allocate a fresh fn_id for a trampoline and install its bytecode.
1037    /// Trampolines are the one Function-creation path that already has
1038    /// the body in hand at install time (top-level fns and lambdas have
1039    /// it filled in later), so we compute `body_hash` immediately. The
1040    /// final hash pass at the end of `compile_program` is a no-op here.
1041    fn install_trampoline(&mut self, name: &str, arity: u16, locals_count: u16, code: Vec<Op>) -> u32 {
1042        let fn_id = self.next_fn_id.len() as u32;
1043        let body_hash = crate::program::compute_body_hash(arity, locals_count, &code);
1044        self.next_fn_id.push(Function {
1045            name: name.into(),
1046            arity,
1047            locals_count,
1048            code,
1049            effects: Vec::new(),
1050            body_hash,
1051            // Trampolines (flow.sequential / parallel / etc.) don't
1052            // surface refined params at this layer.
1053            refinements: Vec::new(),
1054        });
1055        fn_id
1056    }
1057
1058    /// `flow.sequential(f, g)` returns a closure `(x) -> g(f(x))`.
1059    fn emit_flow_sequential(&mut self, args: &[a::CExpr]) {
1060        // Push f, g; build the trampoline closure with 2 captures.
1061        self.compile_expr(&args[0], false);
1062        self.compile_expr(&args[1], false);
1063        let nid = self.pool.node_id("n_flow_sequential");
1064        let code = vec![
1065            // Locals: [f=0, g=1, x=2]
1066            Op::LoadLocal(0),                                  // push f
1067            Op::LoadLocal(2),                                  // push x
1068            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = f(x)
1069            // stack: [r]
1070            Op::StoreLocal(3),                                 // tmp = r
1071            Op::LoadLocal(1),                                  // push g
1072            Op::LoadLocal(3),                                  // push tmp
1073            Op::CallClosure { arity: 1, node_id_idx: nid },    // r = g(tmp)
1074            Op::Return,
1075        ];
1076        let fn_id = self.install_trampoline("__flow_sequential", 3, 4, code);
1077        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
1078    }
1079
1080    /// `flow.parallel(fa, fb)` returns a closure `() -> (fa(), fb())`.
1081    /// Implementation is sequential: each function is called in order
1082    /// and the results are packed into a 2-tuple. The spec (§11.2)
1083    /// allows the runtime to apply true parallelism here; that needs
1084    /// a thread-safe handler split and is left to a follow-up. The
1085    /// signature is what users program against — sequential vs threaded
1086    /// is an implementation detail invisible to the type system.
1087    fn emit_flow_parallel(&mut self, args: &[a::CExpr]) {
1088        // Push fa, fb; build a 0-arg trampoline closure with 2 captures.
1089        self.compile_expr(&args[0], false);
1090        self.compile_expr(&args[1], false);
1091        let nid = self.pool.node_id("n_flow_parallel");
1092        let code = vec![
1093            // Locals: [fa=0, fb=1]
1094            Op::LoadLocal(0),                                  // push fa
1095            Op::CallClosure { arity: 0, node_id_idx: nid },    // a = fa()
1096            Op::LoadLocal(1),                                  // push fb
1097            Op::CallClosure { arity: 0, node_id_idx: nid },    // b = fb()
1098            Op::MakeTuple(2),                                  // (a, b)
1099            Op::Return,
1100        ];
1101        let fn_id = self.install_trampoline("__flow_parallel", 2, 2, code);
1102        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
1103    }
1104
1105    /// `flow.parallel_list(actions)` runs each 0-arg closure in `actions`
1106    /// and returns the results as a list in input order. Variadic
1107    /// counterpart to `flow.parallel`. Sequential under the hood — the
1108    /// spec (§11.2) reserves true threading for a future scheduler.
1109    /// Compiled inline (mirrors `list.map`) so closure args can flow
1110    /// through `CallClosure` without a heap-allocated trampoline.
1111    fn emit_flow_parallel_list(&mut self, args: &[a::CExpr]) {
1112        // xs := actions
1113        self.compile_expr(&args[0], false);
1114        let xs = self.alloc_local("__fpl_xs");
1115        self.emit(Op::StoreLocal(xs));
1116
1117        // out := []
1118        self.emit(Op::MakeList(0));
1119        let out = self.alloc_local("__fpl_out");
1120        self.emit(Op::StoreLocal(out));
1121
1122        // i := 0
1123        let zero = self.pool.int(0);
1124        self.emit(Op::PushConst(zero));
1125        let i = self.alloc_local("__fpl_i");
1126        self.emit(Op::StoreLocal(i));
1127
1128        // loop_top: while i < len(xs) { ... }
1129        let loop_top = self.code.len();
1130        self.emit(Op::LoadLocal(i));
1131        self.emit(Op::LoadLocal(xs));
1132        self.emit(Op::GetListLen);
1133        self.emit(Op::NumLt);
1134        let j_exit = self.code.len();
1135        self.emit(Op::JumpIfNot(0));
1136
1137        // body: out := out ++ [xs[i]()]
1138        let nid = self.pool.node_id("n_flow_parallel_list");
1139        self.emit(Op::LoadLocal(out));
1140        self.emit(Op::LoadLocal(xs));
1141        self.emit(Op::LoadLocal(i));
1142        self.emit(Op::GetListElemDyn);
1143        self.emit(Op::CallClosure { arity: 0, node_id_idx: nid });
1144        self.emit(Op::ListAppend);
1145        self.emit(Op::StoreLocal(out));
1146
1147        // i := i + 1
1148        self.emit(Op::LoadLocal(i));
1149        let one = self.pool.int(1);
1150        self.emit(Op::PushConst(one));
1151        self.emit(Op::NumAdd);
1152        self.emit(Op::StoreLocal(i));
1153
1154        // jump back
1155        let jump_back = self.code.len();
1156        let back = (loop_top as i32) - (jump_back as i32 + 1);
1157        self.emit(Op::Jump(back));
1158
1159        // exit: patch j_exit, push out
1160        let exit_target = self.code.len() as i32;
1161        if let Op::JumpIfNot(off) = &mut self.code[j_exit] {
1162            *off = exit_target - (j_exit as i32 + 1);
1163        }
1164        self.emit(Op::LoadLocal(out));
1165    }
1166
1167    /// `flow.branch(cond, t, f)` returns a closure `(x) -> if cond(x) then t(x) else f(x)`.
1168    fn emit_flow_branch(&mut self, args: &[a::CExpr]) {
1169        self.compile_expr(&args[0], false);
1170        self.compile_expr(&args[1], false);
1171        self.compile_expr(&args[2], false);
1172        let nid = self.pool.node_id("n_flow_branch");
1173        let mut code = vec![
1174            // Locals: [cond=0, t=1, f=2, x=3]
1175            Op::LoadLocal(0),                               // push cond
1176            Op::LoadLocal(3),                               // push x
1177            Op::CallClosure { arity: 1, node_id_idx: nid }, // bool
1178        ];
1179        let j_false = code.len();
1180        code.push(Op::JumpIfNot(0));                        // patched
1181        // true arm: t(x)
1182        code.push(Op::LoadLocal(1));
1183        code.push(Op::LoadLocal(3));
1184        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
1185        code.push(Op::Return);
1186        // false arm
1187        let false_target = code.len() as i32;
1188        if let Op::JumpIfNot(off) = &mut code[j_false] {
1189            *off = false_target - (j_false as i32 + 1);
1190        }
1191        code.push(Op::LoadLocal(2));
1192        code.push(Op::LoadLocal(3));
1193        code.push(Op::CallClosure { arity: 1, node_id_idx: nid });
1194        code.push(Op::Return);
1195
1196        let fn_id = self.install_trampoline("__flow_branch", 4, 4, code);
1197        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
1198    }
1199
1200    /// `flow.retry(f, max_attempts)` returns a closure `(x) -> Result[U, E]`
1201    /// that calls `f(x)` up to `max_attempts` times, returning the first
1202    /// `Ok` or the final `Err`.
1203    fn emit_flow_retry(&mut self, args: &[a::CExpr]) {
1204        self.compile_expr(&args[0], false);
1205        self.compile_expr(&args[1], false);
1206        let call_nid = self.pool.node_id("n_flow_retry");
1207        let ok_idx = self.pool.variant("Ok");
1208        let zero_const = self.pool.int(0);
1209        let one_const = self.pool.int(1);
1210        // Locals: [f=0, max=1, x=2, i=3, last=4]
1211        let mut code = vec![
1212            // i := 0
1213            Op::PushConst(zero_const),
1214            Op::StoreLocal(3),
1215        ];
1216        // loop_top: while i < max
1217        let loop_top = code.len() as i32;
1218        code.push(Op::LoadLocal(3));
1219        code.push(Op::LoadLocal(1));
1220        code.push(Op::NumLt);
1221        let j_done = code.len();
1222        code.push(Op::JumpIfNot(0));                       // patched
1223
1224        // body: r := f(x); last := r
1225        code.push(Op::LoadLocal(0));
1226        code.push(Op::LoadLocal(2));
1227        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
1228        code.push(Op::StoreLocal(4));
1229
1230        // Test variant Ok on last; if so, return last.
1231        code.push(Op::LoadLocal(4));
1232        code.push(Op::TestVariant(ok_idx));
1233        let j_was_err = code.len();
1234        code.push(Op::JumpIfNot(0));                       // patched: skip return
1235        code.push(Op::LoadLocal(4));
1236        code.push(Op::Return);
1237
1238        // was_err: i := i + 1; jump loop_top
1239        let was_err_target = code.len() as i32;
1240        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
1241            *off = was_err_target - (j_was_err as i32 + 1);
1242        }
1243        code.push(Op::LoadLocal(3));
1244        code.push(Op::PushConst(one_const));
1245        code.push(Op::NumAdd);
1246        code.push(Op::StoreLocal(3));
1247        let pc_after_jump = code.len() as i32 + 1;
1248        code.push(Op::Jump(loop_top - pc_after_jump));
1249
1250        // done: return last (the final Err, or Unit if max=0).
1251        let done_target = code.len() as i32;
1252        if let Op::JumpIfNot(off) = &mut code[j_done] {
1253            *off = done_target - (j_done as i32 + 1);
1254        }
1255        code.push(Op::LoadLocal(4));
1256        code.push(Op::Return);
1257
1258        let fn_id = self.install_trampoline("__flow_retry", 3, 5, code);
1259        self.emit(Op::MakeClosure { fn_id, capture_count: 2 });
1260    }
1261
1262    /// `flow.retry_with_backoff(f, attempts, base_ms)` (#226). Variant
1263    /// of `flow.retry` that sleeps between attempts. The first
1264    /// attempt fires immediately; attempt k > 1 waits `base_ms *
1265    /// 2^(k-2)` ms before retrying. Sleeps go through
1266    /// `time.sleep_ms`, which is why the resulting closure carries
1267    /// `[time]` in its effect row even though the underlying `f` is
1268    /// pure.
1269    fn emit_flow_retry_with_backoff(&mut self, args: &[a::CExpr]) {
1270        // Push captures: f, max, base_ms. The trampoline takes one
1271        // call-time arg `x`, so capture_count = 3, arity = 4.
1272        self.compile_expr(&args[0], false);
1273        self.compile_expr(&args[1], false);
1274        self.compile_expr(&args[2], false);
1275        let call_nid    = self.pool.node_id("n_flow_retry_backoff");
1276        let sleep_nid   = self.pool.node_id("n_flow_retry_backoff_sleep");
1277        let kind_idx    = self.pool.str("time");
1278        let op_idx      = self.pool.str("sleep_ms");
1279        let ok_idx      = self.pool.variant("Ok");
1280        let zero_const  = self.pool.int(0);
1281        let one_const   = self.pool.int(1);
1282        let two_const   = self.pool.int(2);
1283        // Locals layout:
1284        //   0=f, 1=max, 2=base_ms (captures)
1285        //   3=x (arg)
1286        //   4=i, 5=last, 6=next_delay (working state)
1287        let mut code = vec![
1288            // next_delay := base_ms
1289            Op::LoadLocal(2),
1290            Op::StoreLocal(6),
1291            // i := 0
1292            Op::PushConst(zero_const),
1293            Op::StoreLocal(4),
1294        ];
1295
1296        let loop_top = code.len() as i32;
1297        // while i < max
1298        code.push(Op::LoadLocal(4));
1299        code.push(Op::LoadLocal(1));
1300        code.push(Op::NumLt);
1301        let j_done = code.len();
1302        code.push(Op::JumpIfNot(0)); // patched
1303
1304        // if i > 0: time.sleep_ms(next_delay); next_delay := next_delay * 2
1305        code.push(Op::PushConst(zero_const));
1306        code.push(Op::LoadLocal(4));
1307        code.push(Op::NumLt);                // 0 < i ?
1308        let j_no_sleep = code.len();
1309        code.push(Op::JumpIfNot(0));         // patched: skip the sleep block
1310        // Sleep
1311        code.push(Op::LoadLocal(6));         // arg = next_delay
1312        code.push(Op::EffectCall {
1313            kind_idx, op_idx, arity: 1, node_id_idx: sleep_nid,
1314        });
1315        code.push(Op::Pop);                  // discard the Unit result
1316        // next_delay := next_delay * 2
1317        code.push(Op::LoadLocal(6));
1318        code.push(Op::PushConst(two_const));
1319        code.push(Op::NumMul);
1320        code.push(Op::StoreLocal(6));
1321        // patch the no-sleep skip
1322        let after_sleep = code.len() as i32;
1323        if let Op::JumpIfNot(off) = &mut code[j_no_sleep] {
1324            *off = after_sleep - (j_no_sleep as i32 + 1);
1325        }
1326
1327        // last := f(x)
1328        code.push(Op::LoadLocal(0));
1329        code.push(Op::LoadLocal(3));
1330        code.push(Op::CallClosure { arity: 1, node_id_idx: call_nid });
1331        code.push(Op::StoreLocal(5));
1332
1333        // if Ok(last): return last
1334        code.push(Op::LoadLocal(5));
1335        code.push(Op::TestVariant(ok_idx));
1336        let j_was_err = code.len();
1337        code.push(Op::JumpIfNot(0)); // patched
1338        code.push(Op::LoadLocal(5));
1339        code.push(Op::Return);
1340
1341        // was_err: i := i + 1; jump loop_top
1342        let was_err_target = code.len() as i32;
1343        if let Op::JumpIfNot(off) = &mut code[j_was_err] {
1344            *off = was_err_target - (j_was_err as i32 + 1);
1345        }
1346        code.push(Op::LoadLocal(4));
1347        code.push(Op::PushConst(one_const));
1348        code.push(Op::NumAdd);
1349        code.push(Op::StoreLocal(4));
1350        let pc_after_jump = code.len() as i32 + 1;
1351        code.push(Op::Jump(loop_top - pc_after_jump));
1352
1353        // done: return last (the final Err, or Unit if max=0).
1354        let done_target = code.len() as i32;
1355        if let Op::JumpIfNot(off) = &mut code[j_done] {
1356            *off = done_target - (j_done as i32 + 1);
1357        }
1358        code.push(Op::LoadLocal(5));
1359        code.push(Op::Return);
1360
1361        let fn_id = self.install_trampoline("__flow_retry_backoff", 4, 7, code);
1362        self.emit(Op::MakeClosure { fn_id, capture_count: 3 });
1363    }
1364}
1365
1366/// Collect free variables referenced in `e` that are not in `bound`.
1367/// Mutates `bound` to track let/lambda introductions during the walk;
1368/// the caller's set is preserved on return because Rust's borrow rules
1369/// force us to clone for sub-scopes that rebind a name.
1370fn free_vars(e: &a::CExpr, bound: &mut std::collections::HashSet<String>, out: &mut Vec<String>) {
1371    match e {
1372        a::CExpr::Literal { .. } => {}
1373        a::CExpr::Var { name } => {
1374            if !bound.contains(name) && !out.contains(name) {
1375                out.push(name.clone());
1376            }
1377        }
1378        a::CExpr::Call { callee, args } => {
1379            free_vars(callee, bound, out);
1380            for a in args { free_vars(a, bound, out); }
1381        }
1382        a::CExpr::Let { name, value, body, .. } => {
1383            free_vars(value, bound, out);
1384            let was_bound = bound.contains(name);
1385            bound.insert(name.clone());
1386            free_vars(body, bound, out);
1387            if !was_bound { bound.remove(name); }
1388        }
1389        a::CExpr::Match { scrutinee, arms } => {
1390            free_vars(scrutinee, bound, out);
1391            for arm in arms {
1392                let mut local_bound = bound.clone();
1393                pattern_binders(&arm.pattern, &mut local_bound);
1394                free_vars(&arm.body, &mut local_bound, out);
1395            }
1396        }
1397        a::CExpr::Block { statements, result } => {
1398            let mut local_bound = bound.clone();
1399            for s in statements { free_vars(s, &mut local_bound, out); }
1400            free_vars(result, &mut local_bound, out);
1401        }
1402        a::CExpr::Constructor { args, .. } => {
1403            for a in args { free_vars(a, bound, out); }
1404        }
1405        a::CExpr::RecordLit { fields } => {
1406            for f in fields { free_vars(&f.value, bound, out); }
1407        }
1408        a::CExpr::TupleLit { items } | a::CExpr::ListLit { items } => {
1409            for it in items { free_vars(it, bound, out); }
1410        }
1411        a::CExpr::FieldAccess { value, .. } => free_vars(value, bound, out),
1412        a::CExpr::Lambda { params, body, .. } => {
1413            let mut inner = bound.clone();
1414            for p in params { inner.insert(p.name.clone()); }
1415            free_vars(body, &mut inner, out);
1416        }
1417        a::CExpr::BinOp { lhs, rhs, .. } => {
1418            free_vars(lhs, bound, out);
1419            free_vars(rhs, bound, out);
1420        }
1421        a::CExpr::UnaryOp { expr, .. } => free_vars(expr, bound, out),
1422        a::CExpr::Return { value } => free_vars(value, bound, out),
1423    }
1424}
1425
1426fn pattern_binders(p: &a::Pattern, bound: &mut std::collections::HashSet<String>) {
1427    match p {
1428        a::Pattern::PWild | a::Pattern::PLiteral { .. } => {}
1429        a::Pattern::PVar { name } => { bound.insert(name.clone()); }
1430        a::Pattern::PConstructor { args, .. } => {
1431            for a in args { pattern_binders(a, bound); }
1432        }
1433        a::Pattern::PRecord { fields } => {
1434            for f in fields { pattern_binders(&f.pattern, bound); }
1435        }
1436        a::Pattern::PTuple { items } => {
1437            for it in items { pattern_binders(it, bound); }
1438        }
1439    }
1440}