Skip to main content

ion_core/
compiler.rs

1//! AST → Bytecode compiler for the Ion VM.
2
3use crate::ast::*;
4use crate::bytecode::{Chunk, Op};
5use crate::error::IonError;
6use crate::value::{FnChunkCache, Value};
7
8/// A local variable tracked at compile time for stack-slot resolution.
9#[derive(Debug, Clone)]
10struct Local {
11    name: String,
12    depth: usize,
13}
14
15fn unmatched_label_msg(keyword: &str, label: Option<&str>) -> String {
16    match label {
17        Some(name) => format!("{keyword} with unknown label '{name}"),
18        None => format!("{keyword} outside of loop"),
19    }
20}
21
22/// One enclosing loop's compile-time bookkeeping. The compiler maintains a
23/// stack of these so labeled break/continue can target outer frames by name.
24#[derive(Debug)]
25struct LoopFrame {
26    label: Option<String>,
27    /// Pending break jumps to patch when this loop ends.
28    break_jumps: Vec<usize>,
29    /// Bytecode offset of the loop's continue target.
30    continue_target: usize,
31    /// True when the frame represents a `for` loop (needs IterDrop on break /
32    /// Unit placeholder on continue).
33    is_for_loop: bool,
34    /// Scope depth at the start of this loop (for scope cleanup on break/continue).
35    scope_depth: usize,
36}
37
38pub struct Compiler {
39    chunk: Chunk,
40    /// Precompiled function body chunks, keyed by fn_id.
41    pub fn_chunks: FnChunkCache,
42    /// Whether the next expression is in tail position (for TCO).
43    in_tail_position: bool,
44    /// Compile-time local variable tracking for stack-slot resolution.
45    locals: Vec<Local>,
46    /// Current scope depth.
47    scope_depth: usize,
48    /// Whether locals must also be defined in env (needed when closures exist).
49    needs_env_locals: bool,
50    /// Stack of enclosing loops for break/continue resolution.
51    loop_stack: Vec<LoopFrame>,
52    /// Current `async {}` nesting depth while compiling async-runtime syntax.
53    #[cfg(feature = "async-runtime")]
54    async_scope_depth: usize,
55}
56
57impl Default for Compiler {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl Compiler {
64    pub fn new() -> Self {
65        Self {
66            chunk: Chunk::new(),
67            fn_chunks: FnChunkCache::new(),
68            in_tail_position: false,
69            locals: Vec::new(),
70            scope_depth: 0,
71            needs_env_locals: true, // conservative default for top-level
72            loop_stack: Vec::new(),
73            #[cfg(feature = "async-runtime")]
74            async_scope_depth: 0,
75        }
76    }
77
78    /// Find the index of the loop frame in `loop_stack` that a labeled (or
79    /// unlabeled) break/continue should target. Returns `None` if no enclosing
80    /// loop matches.
81    fn resolve_loop_target(&self, label: Option<&str>) -> Option<usize> {
82        match label {
83            None => self.loop_stack.len().checked_sub(1),
84            Some(want) => self
85                .loop_stack
86                .iter()
87                .rposition(|f| f.label.as_deref() == Some(want)),
88        }
89    }
90
91    /// Check if a list of statements contains any closures (lambdas or inner fn decls).
92    fn stmts_have_closures(stmts: &[Stmt]) -> bool {
93        for stmt in stmts {
94            match &stmt.kind {
95                StmtKind::FnDecl { body: _, .. } => {
96                    // Inner fn decl itself is a closure; also check its body
97                    return true;
98                    // Note: we don't need to recurse — just the presence of a fn decl
99                    // in the current scope means outer locals might be captured
100                }
101                StmtKind::ExprStmt { expr, .. } if Self::expr_has_closures(expr) => {
102                    return true;
103                }
104                StmtKind::Let { value, .. } if Self::expr_has_closures(value) => {
105                    return true;
106                }
107                StmtKind::For { body, iter, .. } => {
108                    if Self::expr_has_closures(iter) {
109                        return true;
110                    }
111                    if Self::stmts_have_closures(body) {
112                        return true;
113                    }
114                }
115                StmtKind::While { cond, body, .. } => {
116                    if Self::expr_has_closures(cond) {
117                        return true;
118                    }
119                    if Self::stmts_have_closures(body) {
120                        return true;
121                    }
122                }
123                StmtKind::Loop { body, .. } if Self::stmts_have_closures(body) => {
124                    return true;
125                }
126                StmtKind::Return { value: Some(e) } if Self::expr_has_closures(e) => {
127                    return true;
128                }
129                StmtKind::Assign { value, .. } if Self::expr_has_closures(value) => {
130                    return true;
131                }
132                StmtKind::WhileLet { expr, body, .. } => {
133                    if Self::expr_has_closures(expr) {
134                        return true;
135                    }
136                    if Self::stmts_have_closures(body) {
137                        return true;
138                    }
139                }
140                _ => {}
141            }
142        }
143        false
144    }
145
146    fn expr_has_closures(expr: &Expr) -> bool {
147        match &expr.kind {
148            ExprKind::Lambda { .. } => true,
149            ExprKind::If {
150                cond,
151                then_body,
152                else_body,
153            } => {
154                Self::expr_has_closures(cond)
155                    || Self::stmts_have_closures(then_body)
156                    || else_body
157                        .as_ref()
158                        .is_some_and(|b| Self::stmts_have_closures(b))
159            }
160            ExprKind::Block(stmts) => Self::stmts_have_closures(stmts),
161            ExprKind::Call { func, args } => {
162                Self::expr_has_closures(func)
163                    || args.iter().any(|a| Self::expr_has_closures(&a.value))
164            }
165            ExprKind::MethodCall { expr, args, .. } => {
166                Self::expr_has_closures(expr)
167                    || args.iter().any(|a| Self::expr_has_closures(&a.value))
168            }
169            ExprKind::BinOp { left, right, .. } => {
170                Self::expr_has_closures(left) || Self::expr_has_closures(right)
171            }
172            ExprKind::UnaryOp { expr, .. } => Self::expr_has_closures(expr),
173            ExprKind::PipeOp { left, right } => {
174                Self::expr_has_closures(left) || Self::expr_has_closures(right)
175            }
176            ExprKind::Match { expr, arms } => {
177                Self::expr_has_closures(expr)
178                    || arms.iter().any(|a| Self::expr_has_closures(&a.body))
179            }
180            ExprKind::List(items) => items.iter().any(|e| match e {
181                ListEntry::Elem(expr) | ListEntry::Spread(expr) => Self::expr_has_closures(expr),
182            }),
183            ExprKind::Tuple(items) => items.iter().any(Self::expr_has_closures),
184            ExprKind::ListComp {
185                expr, iter, cond, ..
186            } => {
187                Self::expr_has_closures(expr)
188                    || Self::expr_has_closures(iter)
189                    || cond.as_ref().is_some_and(|c| Self::expr_has_closures(c))
190            }
191            ExprKind::IfLet {
192                expr,
193                then_body,
194                else_body,
195                ..
196            } => {
197                Self::expr_has_closures(expr)
198                    || Self::stmts_have_closures(then_body)
199                    || else_body
200                        .as_ref()
201                        .is_some_and(|b| Self::stmts_have_closures(b))
202            }
203            ExprKind::TryCatch { body, handler, .. } => {
204                Self::stmts_have_closures(body) || Self::stmts_have_closures(handler)
205            }
206            ExprKind::LoopExpr(stmts) => Self::stmts_have_closures(stmts),
207            ExprKind::Range { start, end, .. } => {
208                Self::expr_has_closures(start) || Self::expr_has_closures(end)
209            }
210            ExprKind::Dict(entries) => entries.iter().any(|e| match e {
211                DictEntry::KeyValue(k, v) => {
212                    Self::expr_has_closures(k) || Self::expr_has_closures(v)
213                }
214                DictEntry::Spread(expr) => Self::expr_has_closures(expr),
215            }),
216            ExprKind::DictComp {
217                key,
218                value,
219                iter,
220                cond,
221                ..
222            } => {
223                Self::expr_has_closures(key)
224                    || Self::expr_has_closures(value)
225                    || Self::expr_has_closures(iter)
226                    || cond.as_ref().is_some_and(|c| Self::expr_has_closures(c))
227            }
228            ExprKind::FieldAccess { expr, .. }
229            | ExprKind::Try(expr)
230            | ExprKind::SomeExpr(expr)
231            | ExprKind::OkExpr(expr)
232            | ExprKind::ErrExpr(expr) => Self::expr_has_closures(expr),
233            ExprKind::Index { expr, index } => {
234                Self::expr_has_closures(expr) || Self::expr_has_closures(index)
235            }
236            ExprKind::Slice {
237                expr, start, end, ..
238            } => {
239                Self::expr_has_closures(expr)
240                    || start.as_ref().is_some_and(|s| Self::expr_has_closures(s))
241                    || end.as_ref().is_some_and(|e| Self::expr_has_closures(e))
242            }
243            ExprKind::FStr(parts) => parts.iter().any(|p| match p {
244                FStrPart::Expr(e) => Self::expr_has_closures(e),
245                _ => false,
246            }),
247            ExprKind::StructConstruct { fields, spread, .. } => {
248                fields.iter().any(|(_, e)| Self::expr_has_closures(e))
249                    || spread.as_ref().is_some_and(|s| Self::expr_has_closures(s))
250            }
251            _ => false,
252        }
253    }
254
255    /// Try to constant-fold a binary operation on two literal operands.
256    fn try_fold_binop(left: &Expr, op: &BinOp, right: &Expr) -> Option<Value> {
257        match (&left.kind, op, &right.kind) {
258            // Int op Int
259            (ExprKind::Int(a), BinOp::Add, ExprKind::Int(b)) => {
260                Some(Value::Int(a.wrapping_add(*b)))
261            }
262            (ExprKind::Int(a), BinOp::Sub, ExprKind::Int(b)) => {
263                Some(Value::Int(a.wrapping_sub(*b)))
264            }
265            (ExprKind::Int(a), BinOp::Mul, ExprKind::Int(b)) => {
266                Some(Value::Int(a.wrapping_mul(*b)))
267            }
268            (ExprKind::Int(a), BinOp::Div, ExprKind::Int(b)) if *b != 0 => Some(Value::Int(a / b)),
269            (ExprKind::Int(a), BinOp::Mod, ExprKind::Int(b)) if *b != 0 => Some(Value::Int(a % b)),
270            (ExprKind::Int(a), BinOp::Eq, ExprKind::Int(b)) => Some(Value::Bool(a == b)),
271            (ExprKind::Int(a), BinOp::Ne, ExprKind::Int(b)) => Some(Value::Bool(a != b)),
272            (ExprKind::Int(a), BinOp::Lt, ExprKind::Int(b)) => Some(Value::Bool(a < b)),
273            (ExprKind::Int(a), BinOp::Gt, ExprKind::Int(b)) => Some(Value::Bool(a > b)),
274            (ExprKind::Int(a), BinOp::Le, ExprKind::Int(b)) => Some(Value::Bool(a <= b)),
275            (ExprKind::Int(a), BinOp::Ge, ExprKind::Int(b)) => Some(Value::Bool(a >= b)),
276            (ExprKind::Int(a), BinOp::BitAnd, ExprKind::Int(b)) => Some(Value::Int(a & b)),
277            (ExprKind::Int(a), BinOp::BitOr, ExprKind::Int(b)) => Some(Value::Int(a | b)),
278            (ExprKind::Int(a), BinOp::BitXor, ExprKind::Int(b)) => Some(Value::Int(a ^ b)),
279            (ExprKind::Int(a), BinOp::Shl, ExprKind::Int(b)) if (0..64).contains(b) => {
280                Some(Value::Int(a << (*b as u32)))
281            }
282            (ExprKind::Int(a), BinOp::Shr, ExprKind::Int(b)) if (0..64).contains(b) => {
283                Some(Value::Int(a >> (*b as u32)))
284            }
285            // Float op Float
286            (ExprKind::Float(a), BinOp::Add, ExprKind::Float(b)) => Some(Value::Float(a + b)),
287            (ExprKind::Float(a), BinOp::Sub, ExprKind::Float(b)) => Some(Value::Float(a - b)),
288            (ExprKind::Float(a), BinOp::Mul, ExprKind::Float(b)) => Some(Value::Float(a * b)),
289            (ExprKind::Float(a), BinOp::Div, ExprKind::Float(b)) => Some(Value::Float(a / b)),
290            (ExprKind::Float(a), BinOp::Mod, ExprKind::Float(b)) => Some(Value::Float(a % b)),
291            // Int op Float / Float op Int
292            (ExprKind::Int(a), BinOp::Add, ExprKind::Float(b)) => Some(Value::Float(*a as f64 + b)),
293            (ExprKind::Float(a), BinOp::Add, ExprKind::Int(b)) => Some(Value::Float(a + *b as f64)),
294            (ExprKind::Int(a), BinOp::Sub, ExprKind::Float(b)) => Some(Value::Float(*a as f64 - b)),
295            (ExprKind::Float(a), BinOp::Sub, ExprKind::Int(b)) => Some(Value::Float(a - *b as f64)),
296            (ExprKind::Int(a), BinOp::Mul, ExprKind::Float(b)) => Some(Value::Float(*a as f64 * b)),
297            (ExprKind::Float(a), BinOp::Mul, ExprKind::Int(b)) => Some(Value::Float(a * *b as f64)),
298            (ExprKind::Int(a), BinOp::Div, ExprKind::Float(b)) => Some(Value::Float(*a as f64 / b)),
299            (ExprKind::Float(a), BinOp::Div, ExprKind::Int(b)) => Some(Value::Float(a / *b as f64)),
300            // String concat
301            (ExprKind::Str(a), BinOp::Add, ExprKind::Str(b)) => {
302                let mut s = a.clone();
303                s.push_str(b);
304                Some(Value::Str(s))
305            }
306            // Bool logic
307            (ExprKind::Bool(a), BinOp::And, ExprKind::Bool(b)) => Some(Value::Bool(*a && *b)),
308            (ExprKind::Bool(a), BinOp::Or, ExprKind::Bool(b)) => Some(Value::Bool(*a || *b)),
309            (ExprKind::Bool(a), BinOp::Eq, ExprKind::Bool(b)) => Some(Value::Bool(a == b)),
310            (ExprKind::Bool(a), BinOp::Ne, ExprKind::Bool(b)) => Some(Value::Bool(a != b)),
311            _ => None,
312        }
313    }
314
315    /// Try to constant-fold a unary operation on a literal operand.
316    fn try_fold_unary(op: &UnaryOp, inner: &Expr) -> Option<Value> {
317        match (op, &inner.kind) {
318            (UnaryOp::Neg, ExprKind::Int(v)) => Some(Value::Int(-v)),
319            (UnaryOp::Neg, ExprKind::Float(v)) => Some(Value::Float(-v)),
320            (UnaryOp::Not, ExprKind::Bool(v)) => Some(Value::Bool(!v)),
321            _ => None,
322        }
323    }
324
325    /// Check if a statement is terminal (control never continues past it).
326    fn stmt_is_terminal(stmt: &Stmt) -> bool {
327        matches!(
328            &stmt.kind,
329            StmtKind::Return { .. } | StmtKind::Break { .. } | StmtKind::Continue { .. }
330        )
331    }
332
333    /// Resolve a local variable name to its slot index (searching innermost first).
334    fn resolve_local(&self, name: &str) -> Option<usize> {
335        for (i, local) in self.locals.iter().enumerate().rev() {
336            if local.name == name {
337                return Some(i);
338            }
339        }
340        None
341    }
342
343    /// Add a local variable to the compile-time tracking.
344    fn add_local(&mut self, name: String, _mutable: bool) {
345        self.locals.push(Local {
346            name,
347            depth: self.scope_depth,
348        });
349    }
350
351    /// Begin a new compile-time scope and emit PushScope.
352    fn begin_scope(&mut self, line: usize) {
353        self.scope_depth += 1;
354        self.chunk.emit_op(Op::PushScope, line);
355    }
356
357    /// Emit a variable read (GetLocalSlot or GetGlobal).
358    fn emit_get_var(&mut self, name: &str, line: usize) {
359        if let Some(slot) = self.resolve_local(name) {
360            self.chunk.emit_op_u16(Op::GetLocalSlot, slot as u16, line);
361        } else {
362            let idx = self.chunk.add_constant(Value::Str(name.to_string()));
363            self.chunk.emit_op_u16(Op::GetGlobal, idx, line);
364        }
365    }
366
367    /// Define a new local variable. When closures exist, also stores in env.
368    /// The value must be on top of the stack.
369    fn emit_define_local(&mut self, name: &str, mutable: bool, line: usize) {
370        self.add_local(name.to_string(), mutable);
371        if self.needs_env_locals {
372            // Dup value: one copy for env (closure capture), one for slot
373            self.chunk.emit_op(Op::Dup, line);
374            let idx = self.chunk.add_constant(Value::Str(name.to_string()));
375            self.chunk.emit_op_u16(Op::DefineLocal, idx, line);
376            self.chunk.emit(if mutable { 1 } else { 0 }, line);
377        }
378        self.chunk
379            .emit_op_u8(Op::DefineLocalSlot, if mutable { 1 } else { 0 }, line);
380    }
381
382    /// Emit a variable write (SetLocalSlot or SetGlobal).
383    fn emit_set_var(&mut self, name: &str, line: usize) {
384        if let Some(slot) = self.resolve_local(name) {
385            self.chunk.emit_op_u16(Op::SetLocalSlot, slot as u16, line);
386        } else {
387            let idx = self.chunk.add_constant(Value::Str(name.to_string()));
388            self.chunk.emit_op_u16(Op::SetGlobal, idx, line);
389        }
390    }
391
392    /// End the current compile-time scope, emit PopScope.
393    fn end_scope(&mut self, line: usize) {
394        while let Some(local) = self.locals.last() {
395            if local.depth < self.scope_depth {
396                break;
397            }
398            self.locals.pop();
399        }
400        self.scope_depth -= 1;
401        self.chunk.emit_op(Op::PopScope, line);
402    }
403
404    pub fn compile_program(mut self, program: &Program) -> Result<(Chunk, FnChunkCache), IonError> {
405        let len = program.stmts.len();
406        for (i, stmt) in program.stmts.iter().enumerate() {
407            let is_last = i == len - 1;
408            match &stmt.kind {
409                StmtKind::ExprStmt { expr, has_semi } => {
410                    self.compile_expr(expr)?;
411                    if is_last && !has_semi {
412                        // Keep the value as the program result
413                    } else {
414                        self.chunk.emit_op(Op::Pop, stmt.span.line);
415                    }
416                }
417                _ => {
418                    self.compile_stmt(stmt)?;
419                    if is_last {
420                        // Statements produce Unit as the program result
421                        self.chunk.emit_op(Op::Unit, stmt.span.line);
422                    }
423                }
424            }
425            if !is_last && Self::stmt_is_terminal(stmt) {
426                break;
427            }
428        }
429        if program.stmts.is_empty() {
430            self.chunk.emit_op(Op::Unit, 0);
431        }
432        self.chunk.emit_op(Op::Return, 0);
433        self.chunk.peephole_optimize();
434        Ok((self.chunk, self.fn_chunks))
435    }
436
437    fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), IonError> {
438        let line = stmt.span.line;
439        match &stmt.kind {
440            StmtKind::Let {
441                mutable,
442                pattern,
443                type_ann,
444                value,
445            } => {
446                self.compile_expr(value)?;
447                if let Some(ann) = type_ann {
448                    let type_name = Self::type_ann_to_string(ann);
449                    let idx = self.chunk.add_constant(Value::Str(type_name));
450                    self.chunk.emit_op_u16(Op::CheckType, idx, line);
451                }
452                self.compile_checked_let_pattern(pattern, *mutable, line)?;
453            }
454            StmtKind::ExprStmt { expr, .. } => {
455                self.compile_expr(expr)?;
456                self.chunk.emit_op(Op::Pop, line);
457            }
458            StmtKind::FnDecl { name, params, body } => {
459                self.compile_fn_decl(name, params, body, line)?;
460            }
461            StmtKind::For {
462                label,
463                pattern,
464                iter,
465                body,
466            } => {
467                self.compile_for(label.clone(), pattern, iter, body, line)?;
468            }
469            StmtKind::While { label, cond, body } => {
470                self.compile_while(label.clone(), cond, body, line)?;
471            }
472            StmtKind::Loop { label, body } => {
473                self.compile_loop(label.clone(), body, line)?;
474            }
475            StmtKind::Break { label, value } => {
476                let target_idx = match self.resolve_loop_target(label.as_deref()) {
477                    Some(idx) => idx,
478                    None => {
479                        return Err(IonError::runtime(
480                            unmatched_label_msg("break", label.as_deref()),
481                            line,
482                            0,
483                        ));
484                    }
485                };
486                if let Some(expr) = value {
487                    self.compile_expr(expr)?;
488                } else {
489                    self.chunk.emit_op(Op::Unit, line);
490                }
491                let target_scope = self.loop_stack[target_idx].scope_depth;
492                for _ in target_scope..self.scope_depth {
493                    self.chunk.emit_op(Op::PopScope, line);
494                }
495                // Drop iterators of every for-loop frame from innermost down to
496                // and including the target (we're exiting all of them).
497                for frame in self.loop_stack[target_idx..].iter().rev() {
498                    if frame.is_for_loop {
499                        self.chunk.emit_op(Op::IterDrop, line);
500                    }
501                }
502                let jump = self.chunk.emit_jump(Op::Jump, line);
503                self.loop_stack[target_idx].break_jumps.push(jump);
504            }
505            StmtKind::Continue { label } => {
506                let target_idx = match self.resolve_loop_target(label.as_deref()) {
507                    Some(idx) => idx,
508                    None => {
509                        return Err(IonError::runtime(
510                            unmatched_label_msg("continue", label.as_deref()),
511                            line,
512                            0,
513                        ));
514                    }
515                };
516                let target_scope = self.loop_stack[target_idx].scope_depth;
517                for _ in target_scope..self.scope_depth {
518                    self.chunk.emit_op(Op::PopScope, line);
519                }
520                // Drop iterators of inner for-loops we're exiting (everything
521                // strictly above the target frame).
522                for frame in self.loop_stack[target_idx + 1..].iter().rev() {
523                    if frame.is_for_loop {
524                        self.chunk.emit_op(Op::IterDrop, line);
525                    }
526                }
527                let target_frame = &self.loop_stack[target_idx];
528                if target_frame.is_for_loop {
529                    // Push Unit placeholder consumed by IterNext on the next iteration.
530                    self.chunk.emit_op(Op::Unit, line);
531                }
532                let offset = self.chunk.len() - target_frame.continue_target + 3;
533                self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
534            }
535            StmtKind::Return { value } => {
536                if let Some(expr) = value {
537                    let saved = self.in_tail_position;
538                    self.in_tail_position = true;
539                    self.compile_expr(expr)?;
540                    self.in_tail_position = saved;
541                } else {
542                    self.chunk.emit_op(Op::Unit, line);
543                }
544                self.chunk.emit_op(Op::Return, line);
545            }
546            StmtKind::Assign { target, op, value } => {
547                self.compile_assign(target, op, value, line)?;
548                self.chunk.emit_op(Op::Pop, line); // discard assignment result
549            }
550            StmtKind::Use { path, imports } => {
551                self.compile_use(path, imports, line)?;
552            }
553            StmtKind::WhileLet {
554                label,
555                pattern,
556                expr,
557                body,
558            } => {
559                let loop_start = self.chunk.len();
560                self.loop_stack.push(LoopFrame {
561                    label: label.clone(),
562                    break_jumps: Vec::new(),
563                    continue_target: loop_start,
564                    is_for_loop: false,
565                    scope_depth: self.scope_depth,
566                });
567
568                // Evaluate expression
569                self.compile_expr(expr)?;
570
571                // Test pattern
572                self.chunk.emit_op(Op::Dup, line); // keep value for binding
573                self.compile_pattern_test(pattern, line)?;
574
575                let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
576                self.chunk.emit_op(Op::Pop, line); // pop true
577
578                // Pattern matched — bind and execute body
579                self.begin_scope(line);
580                self.compile_pattern_bind(pattern, line)?;
581                for stmt in body {
582                    self.compile_stmt(stmt)?;
583                    if Self::stmt_is_terminal(stmt) {
584                        break;
585                    }
586                }
587                self.end_scope(line);
588
589                let offset = self.chunk.len() - loop_start + 3;
590                self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
591
592                self.chunk.patch_jump(exit_jump);
593                self.chunk.emit_op(Op::Pop, line); // pop false
594                self.chunk.emit_op(Op::Pop, line); // pop the duped value
595
596                let frame = self.loop_stack.pop().expect("loop frame");
597                for jump in &frame.break_jumps {
598                    self.chunk.patch_jump(*jump);
599                }
600            }
601        }
602        Ok(())
603    }
604
605    fn compile_expr(&mut self, expr: &Expr) -> Result<(), IonError> {
606        let line = expr.span.line;
607        let col = expr.span.col;
608        // Save tail position — only Call, If, Block, Match, IfLet propagate it
609        let was_tail = self.in_tail_position;
610        self.in_tail_position = false;
611        match &expr.kind {
612            ExprKind::Int(n) => {
613                self.chunk.emit_constant(Value::Int(*n), line);
614            }
615            ExprKind::Float(n) => {
616                self.chunk.emit_constant(Value::Float(*n), line);
617            }
618            ExprKind::Bool(b) => {
619                self.chunk
620                    .emit_op(if *b { Op::True } else { Op::False }, line);
621            }
622            ExprKind::Str(s) => {
623                self.chunk.emit_constant(Value::Str(s.clone()), line);
624            }
625            ExprKind::Bytes(b) => {
626                self.chunk.emit_constant(Value::Bytes(b.clone()), line);
627            }
628            ExprKind::Unit => {
629                self.chunk.emit_op(Op::Unit, line);
630            }
631            ExprKind::None => {
632                self.chunk.emit_op(Op::None, line);
633            }
634            ExprKind::SomeExpr(inner) => {
635                self.compile_expr(inner)?;
636                self.chunk.emit_op(Op::WrapSome, line);
637            }
638            ExprKind::OkExpr(inner) => {
639                self.compile_expr(inner)?;
640                self.chunk.emit_op(Op::WrapOk, line);
641            }
642            ExprKind::ErrExpr(inner) => {
643                self.compile_expr(inner)?;
644                self.chunk.emit_op(Op::WrapErr, line);
645            }
646
647            ExprKind::Ident(name) => {
648                if let Some(slot) = self.resolve_local(name) {
649                    self.chunk.emit_op_u16(Op::GetLocalSlot, slot as u16, line);
650                } else {
651                    let idx = self.chunk.add_constant(Value::Str(name.clone()));
652                    self.chunk.emit_op_u16(Op::GetGlobal, idx, line);
653                }
654            }
655
656            ExprKind::ModulePath(segments) => {
657                // Load root module as global, then chain GetField for each segment
658                let root_idx = self.chunk.add_constant(Value::Str(segments[0].clone()));
659                self.chunk.emit_op_u16(Op::GetGlobal, root_idx, line);
660                for seg in &segments[1..] {
661                    let idx = self.chunk.add_constant(Value::Str(seg.clone()));
662                    self.chunk.emit_op_u16_span(Op::GetField, idx, line, col);
663                }
664            }
665
666            ExprKind::BinOp { left, op, right } => {
667                // Constant folding: evaluate at compile time if both sides are literals
668                let folded = Self::try_fold_binop(left, op, right);
669                if let Some(val) = folded {
670                    self.chunk.emit_constant(val, line);
671                } else {
672                    match op {
673                        BinOp::And => {
674                            self.compile_expr(left)?;
675                            let jump = self.chunk.emit_jump(Op::And, line);
676                            self.chunk.emit_op(Op::Pop, line);
677                            self.compile_expr(right)?;
678                            self.chunk.patch_jump(jump);
679                        }
680                        BinOp::Or => {
681                            self.compile_expr(left)?;
682                            let jump = self.chunk.emit_jump(Op::Or, line);
683                            self.chunk.emit_op(Op::Pop, line);
684                            self.compile_expr(right)?;
685                            self.chunk.patch_jump(jump);
686                        }
687                        _ => {
688                            self.compile_expr(left)?;
689                            self.compile_expr(right)?;
690                            match op {
691                                BinOp::Add => self.chunk.emit_op_span(Op::Add, line, col),
692                                BinOp::Sub => self.chunk.emit_op_span(Op::Sub, line, col),
693                                BinOp::Mul => self.chunk.emit_op_span(Op::Mul, line, col),
694                                BinOp::Div => self.chunk.emit_op_span(Op::Div, line, col),
695                                BinOp::Mod => self.chunk.emit_op_span(Op::Mod, line, col),
696                                BinOp::Eq => self.chunk.emit_op(Op::Eq, line),
697                                BinOp::Ne => self.chunk.emit_op(Op::NotEq, line),
698                                BinOp::Lt => self.chunk.emit_op(Op::Lt, line),
699                                BinOp::Gt => self.chunk.emit_op(Op::Gt, line),
700                                BinOp::Le => self.chunk.emit_op(Op::LtEq, line),
701                                BinOp::Ge => self.chunk.emit_op(Op::GtEq, line),
702                                BinOp::BitAnd => self.chunk.emit_op(Op::BitAnd, line),
703                                BinOp::BitOr => self.chunk.emit_op(Op::BitOr, line),
704                                BinOp::BitXor => self.chunk.emit_op(Op::BitXor, line),
705                                BinOp::Shl => self.chunk.emit_op(Op::Shl, line),
706                                BinOp::Shr => self.chunk.emit_op(Op::Shr, line),
707                                _ => unreachable!(),
708                            }
709                        }
710                    }
711                }
712            }
713
714            ExprKind::UnaryOp { op, expr: inner } => {
715                let folded = Self::try_fold_unary(op, inner);
716                if let Some(val) = folded {
717                    self.chunk.emit_constant(val, line);
718                } else {
719                    self.compile_expr(inner)?;
720                    match op {
721                        UnaryOp::Neg => self.chunk.emit_op_span(Op::Neg, line, col),
722                        UnaryOp::Not => self.chunk.emit_op_span(Op::Not, line, col),
723                    }
724                }
725            }
726
727            ExprKind::If {
728                cond,
729                then_body,
730                else_body,
731            } => {
732                // Condition is not in tail position (already cleared)
733                self.compile_expr(cond)?;
734                let then_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
735                self.chunk.emit_op(Op::Pop, line); // pop condition
736                self.begin_scope(line);
737                // Both branches inherit tail position
738                self.in_tail_position = was_tail;
739                self.compile_block_expr(then_body, line)?;
740                self.end_scope(line);
741                let else_jump = self.chunk.emit_jump(Op::Jump, line);
742                self.chunk.patch_jump(then_jump);
743                self.chunk.emit_op(Op::Pop, line); // pop condition
744                if let Some(else_stmts) = else_body {
745                    self.begin_scope(line);
746                    self.in_tail_position = was_tail;
747                    self.compile_block_expr(else_stmts, line)?;
748                    self.end_scope(line);
749                } else {
750                    self.chunk.emit_op(Op::Unit, line);
751                }
752                self.chunk.patch_jump(else_jump);
753            }
754
755            ExprKind::Block(stmts) => {
756                self.begin_scope(line);
757                self.in_tail_position = was_tail;
758                self.compile_block_expr(stmts, line)?;
759                self.end_scope(line);
760            }
761
762            ExprKind::Call { func, args } => {
763                let has_named = args.iter().any(|a| a.name.is_some());
764                // Sub-expressions are not in tail position (already cleared above)
765                self.compile_expr(func)?;
766                for arg in args {
767                    self.compile_expr(&arg.value)?;
768                }
769                if has_named {
770                    // Emit CallNamed: total_args, named_count, then (position, name_idx) pairs
771                    let named: Vec<(u8, u16)> = args
772                        .iter()
773                        .enumerate()
774                        .filter_map(|(i, a)| {
775                            a.name
776                                .as_ref()
777                                .map(|n| (i as u8, self.chunk.add_constant(Value::Str(n.clone()))))
778                        })
779                        .collect();
780                    self.chunk.emit_op(Op::CallNamed, line);
781                    self.chunk.emit(args.len() as u8, line);
782                    self.chunk.emit(named.len() as u8, line);
783                    for (pos, name_idx) in named {
784                        self.chunk.emit(pos, line);
785                        self.chunk.emit((name_idx >> 8) as u8, line);
786                        self.chunk.emit(name_idx as u8, line);
787                    }
788                } else {
789                    let op = if was_tail { Op::TailCall } else { Op::Call };
790                    self.chunk.emit_op_u8_span(op, args.len() as u8, line, col);
791                }
792            }
793
794            ExprKind::List(items) => {
795                let has_spread = items.iter().any(|e| matches!(e, ListEntry::Spread(_)));
796                if has_spread {
797                    // Build empty list, then append/extend entries one by one
798                    self.chunk.emit_op_u16(Op::BuildList, 0, line);
799                    for entry in items {
800                        match entry {
801                            ListEntry::Elem(expr) => {
802                                self.compile_expr(expr)?;
803                                self.chunk.emit_op(Op::ListAppend, line);
804                            }
805                            ListEntry::Spread(expr) => {
806                                self.compile_expr(expr)?;
807                                self.chunk.emit_op(Op::ListExtend, line);
808                            }
809                        }
810                    }
811                } else {
812                    // Fast path: no spreads, use BuildList directly
813                    for entry in items {
814                        if let ListEntry::Elem(expr) = entry {
815                            self.compile_expr(expr)?;
816                        }
817                    }
818                    self.chunk
819                        .emit_op_u16(Op::BuildList, items.len() as u16, line);
820                }
821            }
822
823            ExprKind::Tuple(items) => {
824                for item in items {
825                    self.compile_expr(item)?;
826                }
827                self.chunk
828                    .emit_op_u16(Op::BuildTuple, items.len() as u16, line);
829            }
830
831            ExprKind::Dict(entries) => {
832                let has_spread = entries.iter().any(|e| matches!(e, DictEntry::Spread(_)));
833                if has_spread {
834                    // Build empty dict, then insert/merge entries one by one
835                    self.chunk.emit_op_u16(Op::BuildDict, 0, line);
836                    for entry in entries {
837                        match entry {
838                            DictEntry::KeyValue(k, v) => {
839                                self.compile_expr(k)?;
840                                self.compile_expr(v)?;
841                                self.chunk.emit_op(Op::DictInsert, line);
842                            }
843                            DictEntry::Spread(expr) => {
844                                self.compile_expr(expr)?;
845                                self.chunk.emit_op(Op::DictMerge, line);
846                            }
847                        }
848                    }
849                } else {
850                    // Fast path: no spreads, use BuildDict directly
851                    let count = entries.len() as u16;
852                    for entry in entries {
853                        if let DictEntry::KeyValue(k, v) = entry {
854                            self.compile_expr(k)?;
855                            self.compile_expr(v)?;
856                        }
857                    }
858                    self.chunk.emit_op_u16(Op::BuildDict, count, line);
859                }
860            }
861
862            ExprKind::FieldAccess { expr: inner, field } => {
863                self.compile_expr(inner)?;
864                let idx = self.chunk.add_constant(Value::Str(field.clone()));
865                self.chunk.emit_op_u16_span(Op::GetField, idx, line, col);
866            }
867
868            ExprKind::Index { expr: inner, index } => {
869                self.compile_expr(inner)?;
870                self.compile_expr(index)?;
871                self.chunk.emit_op_span(Op::GetIndex, line, col);
872            }
873
874            ExprKind::Slice {
875                expr: inner,
876                start,
877                end,
878                inclusive,
879            } => {
880                self.compile_expr(inner)?;
881                let mut flags: u8 = 0;
882                if let Some(s) = start {
883                    self.compile_expr(s)?;
884                    flags |= 1; // has_start
885                }
886                if let Some(e) = end {
887                    self.compile_expr(e)?;
888                    flags |= 2; // has_end
889                }
890                if *inclusive {
891                    flags |= 4; // inclusive
892                }
893                self.chunk.emit_op_u8(Op::Slice, flags, line);
894            }
895
896            ExprKind::MethodCall {
897                expr: inner,
898                method,
899                args,
900            } => {
901                self.compile_expr(inner)?;
902                for arg in args {
903                    self.compile_expr(&arg.value)?;
904                }
905                let idx = self.chunk.add_constant(Value::Str(method.clone()));
906                self.chunk.emit_op_u16_span(Op::MethodCall, idx, line, col);
907                self.chunk.emit_span(args.len() as u8, line, col);
908            }
909
910            ExprKind::Lambda { params, body } => {
911                // Build lambda body as a single expression statement for tree-walk fallback
912                let body_stmt = Stmt {
913                    kind: StmtKind::ExprStmt {
914                        expr: *body.clone(),
915                        has_semi: false,
916                    },
917                    span: expr.span,
918                };
919                // Precompile lambda body
920                let mut fn_compiler = Compiler::new();
921                fn_compiler.in_tail_position = true;
922                fn_compiler.needs_env_locals = Self::expr_has_closures(body);
923                // Pre-register parameters as locals
924                for p in params {
925                    fn_compiler.add_local(p.clone(), false);
926                }
927                // When closures exist, also define params in env so they can be captured
928                if fn_compiler.needs_env_locals {
929                    for (i, p) in params.iter().enumerate() {
930                        fn_compiler
931                            .chunk
932                            .emit_op_u16(Op::GetLocalSlot, i as u16, line);
933                        let idx = fn_compiler.chunk.add_constant(Value::Str(p.clone()));
934                        fn_compiler.chunk.emit_op_u16(Op::DefineLocal, idx, line);
935                        fn_compiler.chunk.emit(0, line);
936                    }
937                }
938                fn_compiler.compile_expr(body)?;
939                fn_compiler.chunk.emit_op(Op::Return, line);
940                fn_compiler.chunk.peephole_optimize();
941                let compiled_chunk = fn_compiler.chunk;
942                self.fn_chunks.extend(fn_compiler.fn_chunks);
943
944                let fn_value = Value::Fn(crate::value::IonFn::new(
945                    "<lambda>".to_string(),
946                    params
947                        .iter()
948                        .map(|n| crate::ast::Param {
949                            name: n.clone(),
950                            default: None,
951                        })
952                        .collect(),
953                    vec![body_stmt],
954                    std::collections::HashMap::new(),
955                ));
956                // Associate precompiled chunk with fn_id
957                if let Value::Fn(ref ion_fn) = fn_value {
958                    self.fn_chunks.insert(ion_fn.fn_id, compiled_chunk);
959                }
960                let fn_idx = self.chunk.add_constant(fn_value);
961                self.chunk.emit_op_u16(Op::Closure, fn_idx, line);
962            }
963
964            ExprKind::FStr(parts) => {
965                for part in parts {
966                    match part {
967                        FStrPart::Literal(s) => {
968                            self.chunk.emit_constant(Value::Str(s.clone()), line);
969                        }
970                        FStrPart::Expr(expr) => {
971                            self.compile_expr(expr)?;
972                        }
973                    }
974                }
975                self.chunk
976                    .emit_op_u16(Op::BuildFString, parts.len() as u16, line);
977            }
978
979            ExprKind::PipeOp { left, right } => {
980                // Desugar: left |> right(args)  →  right(left, args)
981                // Compile func first, then piped value as first arg, then other args
982                match &right.kind {
983                    ExprKind::Call { func, args } => {
984                        self.compile_expr(func)?;
985                        self.compile_expr(left)?; // piped value = first arg
986                        for arg in args {
987                            self.compile_expr(&arg.value)?;
988                        }
989                        self.chunk
990                            .emit_op_u8(Op::Call, (args.len() + 1) as u8, line);
991                    }
992                    _ => {
993                        // bare function: left |> func  →  func(left)
994                        self.compile_expr(right)?;
995                        self.compile_expr(left)?;
996                        self.chunk.emit_op_u8(Op::Call, 1, line);
997                    }
998                }
999            }
1000
1001            ExprKind::Try(inner) => {
1002                self.compile_expr(inner)?;
1003                self.chunk.emit_op(Op::Try, line);
1004            }
1005
1006            ExprKind::Range {
1007                start,
1008                end,
1009                inclusive,
1010            } => {
1011                self.compile_expr(start)?;
1012                self.compile_expr(end)?;
1013                self.chunk
1014                    .emit_op_u8(Op::BuildRange, if *inclusive { 1 } else { 0 }, line);
1015            }
1016
1017            ExprKind::LoopExpr(body) => {
1018                self.compile_loop(None, body, line)?;
1019            }
1020
1021            ExprKind::Match {
1022                expr: subject,
1023                arms,
1024            } => {
1025                self.compile_match(subject, arms, line)?;
1026            }
1027
1028            ExprKind::ListComp {
1029                expr: item_expr,
1030                pattern,
1031                iter,
1032                cond,
1033            } => {
1034                self.compile_list_comp(item_expr, pattern, iter, cond.as_deref(), line)?;
1035            }
1036
1037            ExprKind::DictComp {
1038                key,
1039                value,
1040                pattern,
1041                iter,
1042                cond,
1043            } => {
1044                self.compile_dict_comp(key, value, pattern, iter, cond.as_deref(), line)?;
1045            }
1046
1047            ExprKind::IfLet {
1048                pattern,
1049                expr: inner,
1050                then_body,
1051                else_body,
1052            } => {
1053                // Evaluate the expression (not in tail position — already cleared)
1054                self.compile_expr(inner)?;
1055
1056                // Test pattern
1057                self.chunk.emit_op(Op::Dup, line); // keep value for binding
1058                self.compile_pattern_test(pattern, line)?;
1059
1060                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1061                self.chunk.emit_op(Op::Pop, line); // pop true
1062
1063                // Pattern matched — bind variables in new scope
1064                self.begin_scope(line);
1065                self.compile_pattern_bind(pattern, line)?;
1066                self.in_tail_position = was_tail;
1067                self.compile_block_expr(then_body, line)?;
1068                self.end_scope(line);
1069
1070                let end_jump = self.chunk.emit_jump(Op::Jump, line);
1071
1072                self.chunk.patch_jump(else_jump);
1073                self.chunk.emit_op(Op::Pop, line); // pop false
1074                self.chunk.emit_op(Op::Pop, line); // pop the duped value
1075
1076                if let Some(else_stmts) = else_body {
1077                    self.begin_scope(line);
1078                    self.in_tail_position = was_tail;
1079                    self.compile_block_expr(else_stmts, line)?;
1080                    self.end_scope(line);
1081                } else {
1082                    self.chunk.emit_op(Op::Unit, line);
1083                }
1084
1085                self.chunk.patch_jump(end_jump);
1086            }
1087
1088            ExprKind::StructConstruct {
1089                name,
1090                fields,
1091                spread,
1092            } => {
1093                if let Some(spread_expr) = spread {
1094                    self.compile_expr(spread_expr)?;
1095                    for (fname, fexpr) in fields {
1096                        self.chunk.emit_constant(Value::Str(fname.clone()), line);
1097                        self.compile_expr(fexpr)?;
1098                    }
1099                    let type_idx = self.chunk.add_constant(Value::Str(name.clone()));
1100                    let field_count = (0x8000 | fields.len()) as u16;
1101                    self.chunk.emit_op(Op::ConstructStruct, line);
1102                    self.chunk.emit((type_idx >> 8) as u8, line);
1103                    self.chunk.emit((type_idx & 0xff) as u8, line);
1104                    self.chunk.emit((field_count >> 8) as u8, line);
1105                    self.chunk.emit((field_count & 0xff) as u8, line);
1106                } else {
1107                    for (fname, fexpr) in fields {
1108                        self.chunk.emit_constant(Value::Str(fname.clone()), line);
1109                        self.compile_expr(fexpr)?;
1110                    }
1111                    let type_idx = self.chunk.add_constant(Value::Str(name.clone()));
1112                    let count = fields.len() as u16;
1113                    self.chunk.emit_op(Op::ConstructStruct, line);
1114                    self.chunk.emit((type_idx >> 8) as u8, line);
1115                    self.chunk.emit((type_idx & 0xff) as u8, line);
1116                    self.chunk.emit((count >> 8) as u8, line);
1117                    self.chunk.emit((count & 0xff) as u8, line);
1118                }
1119            }
1120            ExprKind::EnumVariant { enum_name, variant } => {
1121                let enum_idx = self.chunk.add_constant(Value::Str(enum_name.clone()));
1122                let variant_idx = self.chunk.add_constant(Value::Str(variant.clone()));
1123                self.chunk.emit_op(Op::ConstructEnum, line);
1124                self.chunk.emit((enum_idx >> 8) as u8, line);
1125                self.chunk.emit((enum_idx & 0xff) as u8, line);
1126                self.chunk.emit((variant_idx >> 8) as u8, line);
1127                self.chunk.emit((variant_idx & 0xff) as u8, line);
1128                self.chunk.emit(0u8, line);
1129            }
1130            ExprKind::EnumVariantCall {
1131                enum_name,
1132                variant,
1133                args,
1134            } => {
1135                for arg in args {
1136                    self.compile_expr(arg)?;
1137                }
1138                let enum_idx = self.chunk.add_constant(Value::Str(enum_name.clone()));
1139                let variant_idx = self.chunk.add_constant(Value::Str(variant.clone()));
1140                self.chunk.emit_op(Op::ConstructEnum, line);
1141                self.chunk.emit((enum_idx >> 8) as u8, line);
1142                self.chunk.emit((enum_idx & 0xff) as u8, line);
1143                self.chunk.emit((variant_idx >> 8) as u8, line);
1144                self.chunk.emit((variant_idx & 0xff) as u8, line);
1145                self.chunk.emit(args.len() as u8, line);
1146            }
1147
1148            #[cfg(feature = "async-runtime")]
1149            ExprKind::AsyncBlock(body) => {
1150                self.begin_scope(line);
1151                let saved_async_depth = self.async_scope_depth;
1152                self.async_scope_depth += 1;
1153                self.in_tail_position = was_tail;
1154                let result = self.compile_block_expr(body, line);
1155                self.async_scope_depth = saved_async_depth;
1156                result?;
1157                self.end_scope(line);
1158            }
1159            #[cfg(feature = "async-runtime")]
1160            ExprKind::SpawnExpr(inner) => {
1161                self.compile_spawn_expr(inner, line, col)?;
1162            }
1163            #[cfg(feature = "async-runtime")]
1164            ExprKind::AwaitExpr(inner) => {
1165                self.compile_expr(inner)?;
1166                self.chunk.emit_op_span(Op::AwaitTask, line, col);
1167            }
1168            #[cfg(feature = "async-runtime")]
1169            ExprKind::SelectExpr(branches) => {
1170                self.compile_select_expr(branches, line, col)?;
1171            }
1172            #[cfg(all(not(feature = "async-runtime"), feature = "legacy-threaded-concurrency"))]
1173            ExprKind::AsyncBlock(_)
1174            | ExprKind::SpawnExpr(_)
1175            | ExprKind::AwaitExpr(_)
1176            | ExprKind::SelectExpr(_) => {
1177                return Err(IonError::runtime(
1178                    ion_str!(
1179                        "legacy-threaded-concurrency is not supported in bytecode VM"
1180                    )
1181                    .to_string(),
1182                    line,
1183                    col,
1184                ));
1185            }
1186            #[cfg(all(not(feature = "async-runtime"), not(feature = "legacy-threaded-concurrency")))]
1187            ExprKind::AsyncBlock(_)
1188            | ExprKind::SpawnExpr(_)
1189            | ExprKind::AwaitExpr(_)
1190            | ExprKind::SelectExpr(_) => {
1191                return Err(IonError::runtime(
1192                    ion_str!("concurrency syntax requires 'async-runtime'").to_string(),
1193                    line,
1194                    col,
1195                ));
1196            }
1197
1198            ExprKind::TryCatch { body, var, handler } => {
1199                // TryBegin catch_offset  (jump to catch block on error)
1200                let try_begin_patch = self.chunk.emit_jump(Op::TryBegin, line);
1201
1202                // Compile try body
1203                self.begin_scope(line);
1204                let old_tail = self.in_tail_position;
1205                self.in_tail_position = false;
1206                for (i, stmt) in body.iter().enumerate() {
1207                    if i == body.len() - 1 {
1208                        if let crate::ast::StmtKind::ExprStmt { expr, .. } = &stmt.kind {
1209                            self.compile_expr(expr)?;
1210                        } else {
1211                            self.compile_stmt(stmt)?;
1212                            self.chunk.emit_op(Op::Unit, line);
1213                        }
1214                    } else {
1215                        self.compile_stmt(stmt)?;
1216                    }
1217                }
1218                if body.is_empty() {
1219                    self.chunk.emit_op(Op::Unit, line);
1220                }
1221                self.in_tail_position = old_tail;
1222                self.end_scope(line);
1223
1224                // TryEnd jump_offset  (no error: pop handler, jump over catch)
1225                let try_end_patch = self.chunk.emit_jump(Op::TryEnd, line);
1226
1227                // Patch TryBegin to point here (catch block start)
1228                self.chunk.patch_jump(try_begin_patch);
1229
1230                // Catch block: error message string is on stack
1231                self.begin_scope(line);
1232                self.emit_define_local(var, false, line);
1233                for (i, stmt) in handler.iter().enumerate() {
1234                    if i == handler.len() - 1 {
1235                        if let crate::ast::StmtKind::ExprStmt { expr, .. } = &stmt.kind {
1236                            self.compile_expr(expr)?;
1237                        } else {
1238                            self.compile_stmt(stmt)?;
1239                            self.chunk.emit_op(Op::Unit, line);
1240                        }
1241                    } else {
1242                        self.compile_stmt(stmt)?;
1243                    }
1244                }
1245                if handler.is_empty() {
1246                    self.chunk.emit_op(Op::Unit, line);
1247                }
1248                self.end_scope(line);
1249
1250                // Patch TryEnd jump to skip catch block
1251                self.chunk.patch_jump(try_end_patch);
1252            }
1253        }
1254        self.in_tail_position = was_tail;
1255        Ok(())
1256    }
1257
1258    fn compile_block_expr(&mut self, stmts: &[Stmt], line: usize) -> Result<(), IonError> {
1259        if stmts.is_empty() {
1260            self.chunk.emit_op(Op::Unit, line);
1261            return Ok(());
1262        }
1263        let len = stmts.len();
1264        let saved_tail = self.in_tail_position;
1265        for (i, stmt) in stmts.iter().enumerate() {
1266            let is_last = i == len - 1;
1267            // Only the last expression (without semicolon) inherits tail position
1268            if !is_last {
1269                self.in_tail_position = false;
1270            } else {
1271                self.in_tail_position = saved_tail;
1272            }
1273            match &stmt.kind {
1274                StmtKind::ExprStmt { expr, has_semi } => {
1275                    if is_last && *has_semi {
1276                        self.in_tail_position = false;
1277                    }
1278                    self.compile_expr(expr)?;
1279                    if is_last && !has_semi {
1280                        // Keep value
1281                    } else {
1282                        self.chunk.emit_op(Op::Pop, stmt.span.line);
1283                    }
1284                }
1285                _ => {
1286                    self.in_tail_position = false;
1287                    self.compile_stmt(stmt)?;
1288                    if is_last {
1289                        self.chunk.emit_op(Op::Unit, stmt.span.line);
1290                    }
1291                }
1292            }
1293            // Dead code elimination: skip remaining statements after terminal
1294            if !is_last && Self::stmt_is_terminal(stmt) {
1295                break;
1296            }
1297        }
1298        self.in_tail_position = saved_tail;
1299        Ok(())
1300    }
1301
1302    #[cfg(feature = "async-runtime")]
1303    fn compile_spawn_expr(&mut self, expr: &Expr, line: usize, col: usize) -> Result<(), IonError> {
1304        if self.async_scope_depth == 0 {
1305            return Err(IonError::runtime(
1306                ion_str!("spawn is only allowed inside async {}").to_string(),
1307                line,
1308                col,
1309            ));
1310        }
1311        let ExprKind::Call { func, args } = &expr.kind else {
1312            return Err(IonError::runtime(
1313                ion_str!("spawn currently requires a function call in the bytecode VM").to_string(),
1314                line,
1315                col,
1316            ));
1317        };
1318        let has_named = args.iter().any(|arg| arg.name.is_some());
1319        self.compile_expr(func)?;
1320        for arg in args {
1321            self.compile_expr(&arg.value)?;
1322        }
1323        if has_named {
1324            let named: Vec<(u8, u16)> = args
1325                .iter()
1326                .enumerate()
1327                .filter_map(|(i, arg)| {
1328                    arg.name
1329                        .as_ref()
1330                        .map(|name| (i as u8, self.chunk.add_constant(Value::Str(name.clone()))))
1331                })
1332                .collect();
1333            self.chunk.emit_op_span(Op::SpawnCallNamed, line, col);
1334            self.chunk.emit(args.len() as u8, line);
1335            self.chunk.emit(named.len() as u8, line);
1336            for (position, name_idx) in named {
1337                self.chunk.emit(position, line);
1338                self.chunk.emit((name_idx >> 8) as u8, line);
1339                self.chunk.emit(name_idx as u8, line);
1340            }
1341        } else {
1342            self.chunk
1343                .emit_op_u8_span(Op::SpawnCall, args.len() as u8, line, col);
1344        }
1345        Ok(())
1346    }
1347
1348    fn compile_use(
1349        &mut self,
1350        path: &[String],
1351        imports: &UseImports,
1352        line: usize,
1353    ) -> Result<(), IonError> {
1354        match imports {
1355            UseImports::Glob => {
1356                self.compile_module_path(path, line, 0)?;
1357                self.chunk.emit_op(Op::ImportGlob, line);
1358            }
1359            UseImports::Names(names) => {
1360                for name in names {
1361                    self.compile_module_path_member(path, name, line, 0)?;
1362                    self.emit_define_local(name, false, line);
1363                }
1364            }
1365            UseImports::Single(name) => {
1366                self.compile_module_path_member(path, name, line, 0)?;
1367                self.emit_define_local(name, false, line);
1368            }
1369        }
1370        Ok(())
1371    }
1372
1373    fn compile_module_path_member(
1374        &mut self,
1375        path: &[String],
1376        name: &str,
1377        line: usize,
1378        col: usize,
1379    ) -> Result<(), IonError> {
1380        let mut segments = path.to_vec();
1381        segments.push(name.to_string());
1382        self.compile_module_path(&segments, line, col)
1383    }
1384
1385    fn compile_module_path(
1386        &mut self,
1387        segments: &[String],
1388        line: usize,
1389        col: usize,
1390    ) -> Result<(), IonError> {
1391        let Some(root) = segments.first() else {
1392            return Err(IonError::runtime("empty module path", line, col));
1393        };
1394        let root_idx = self.chunk.add_constant(Value::Str(root.clone()));
1395        self.chunk.emit_op_u16(Op::GetGlobal, root_idx, line);
1396        for segment in &segments[1..] {
1397            let idx = self.chunk.add_constant(Value::Str(segment.clone()));
1398            self.chunk.emit_op_u16_span(Op::GetField, idx, line, col);
1399        }
1400        Ok(())
1401    }
1402
1403    #[cfg(feature = "async-runtime")]
1404    fn compile_select_expr(
1405        &mut self,
1406        branches: &[SelectBranch],
1407        line: usize,
1408        col: usize,
1409    ) -> Result<(), IonError> {
1410        if branches.is_empty() {
1411            return Err(IonError::runtime(
1412                ion_str!("select requires at least one branch").to_string(),
1413                line,
1414                col,
1415            ));
1416        }
1417        if branches.len() > u8::MAX as usize {
1418            return Err(IonError::runtime(
1419                ion_str!("select has too many branches").to_string(),
1420                line,
1421                col,
1422            ));
1423        }
1424
1425        for branch in branches {
1426            self.compile_spawn_expr(&branch.future_expr, line, col)?;
1427        }
1428        self.chunk
1429            .emit_op_u8_span(Op::SelectTasks, branches.len() as u8, line, col);
1430
1431        let mut end_jumps = Vec::with_capacity(branches.len());
1432        for (idx, branch) in branches.iter().enumerate() {
1433            self.chunk.emit_op(Op::Dup, line);
1434            self.chunk.emit_constant(Value::Int(0), line);
1435            self.chunk.emit_op(Op::GetIndex, line);
1436            self.chunk.emit_constant(Value::Int(idx as i64), line);
1437            self.chunk.emit_op(Op::Eq, line);
1438            let next_branch = self.chunk.emit_jump(Op::JumpIfFalse, line);
1439            self.chunk.emit_op(Op::Pop, line);
1440
1441            self.begin_scope(line);
1442            self.chunk.emit_op(Op::Dup, line);
1443            self.chunk.emit_constant(Value::Int(1), line);
1444            self.chunk.emit_op(Op::GetIndex, line);
1445            self.compile_checked_let_pattern(&branch.pattern, false, line)?;
1446            self.chunk.emit_op(Op::Pop, line);
1447            self.compile_expr(&branch.body)?;
1448            self.end_scope(line);
1449            end_jumps.push(self.chunk.emit_jump(Op::Jump, line));
1450
1451            self.chunk.patch_jump(next_branch);
1452            self.chunk.emit_op(Op::Pop, line);
1453        }
1454
1455        self.chunk.emit_op(Op::Pop, line);
1456        self.chunk.emit_op(Op::Unit, line);
1457        for jump in end_jumps {
1458            self.chunk.patch_jump(jump);
1459        }
1460        Ok(())
1461    }
1462
1463    fn type_ann_to_string(ann: &TypeAnn) -> String {
1464        match ann {
1465            TypeAnn::Simple(name) => name.clone(),
1466            TypeAnn::Option(inner) => format!("Option<{}>", Self::type_ann_to_string(inner)),
1467            TypeAnn::Result(ok, err) => format!(
1468                "Result<{}, {}>",
1469                Self::type_ann_to_string(ok),
1470                Self::type_ann_to_string(err)
1471            ),
1472            TypeAnn::List(inner) => format!("list<{}>", Self::type_ann_to_string(inner)),
1473            TypeAnn::Dict(k, v) => format!(
1474                "dict<{}, {}>",
1475                Self::type_ann_to_string(k),
1476                Self::type_ann_to_string(v)
1477            ),
1478        }
1479    }
1480
1481    fn compile_let_pattern(
1482        &mut self,
1483        pattern: &Pattern,
1484        mutable: bool,
1485        line: usize,
1486    ) -> Result<(), IonError> {
1487        match pattern {
1488            Pattern::Ident(name) => {
1489                self.emit_define_local(name, mutable, line);
1490            }
1491            Pattern::Int(_)
1492            | Pattern::Float(_)
1493            | Pattern::Bool(_)
1494            | Pattern::Str(_)
1495            | Pattern::Bytes(_)
1496            | Pattern::None => {
1497                self.chunk.emit_op(Op::Pop, line);
1498            }
1499            Pattern::Some(inner) => {
1500                self.chunk.emit_op_u8(Op::MatchArm, 1, line);
1501                self.compile_let_pattern(inner, mutable, line)?;
1502            }
1503            Pattern::Ok(inner) => {
1504                self.chunk.emit_op_u8(Op::MatchArm, 2, line);
1505                self.compile_let_pattern(inner, mutable, line)?;
1506            }
1507            Pattern::Err(inner) => {
1508                self.chunk.emit_op_u8(Op::MatchArm, 3, line);
1509                self.compile_let_pattern(inner, mutable, line)?;
1510            }
1511            Pattern::Tuple(pats) => {
1512                // Value is on stack. Destructure it.
1513                for (i, pat) in pats.iter().enumerate() {
1514                    self.chunk.emit_op(Op::Dup, line);
1515                    self.chunk.emit_constant(Value::Int(i as i64), line);
1516                    self.chunk.emit_op(Op::GetIndex, line);
1517                    self.compile_let_pattern(pat, mutable, line)?;
1518                }
1519                self.chunk.emit_op(Op::Pop, line); // pop the original tuple
1520            }
1521            Pattern::List(pats, rest) => {
1522                for (i, pat) in pats.iter().enumerate() {
1523                    self.chunk.emit_op(Op::Dup, line);
1524                    self.chunk.emit_constant(Value::Int(i as i64), line);
1525                    self.chunk.emit_op(Op::GetIndex, line);
1526                    self.compile_let_pattern(pat, mutable, line)?;
1527                }
1528                if let Some(rest_pat) = rest {
1529                    self.chunk.emit_op(Op::Dup, line);
1530                    self.chunk
1531                        .emit_constant(Value::Int(pats.len() as i64), line);
1532                    self.chunk.emit_op_u8(Op::Slice, 1, line); // has_start only
1533                    self.compile_let_pattern(rest_pat, mutable, line)?;
1534                }
1535                self.chunk.emit_op(Op::Pop, line);
1536            }
1537            Pattern::Struct { fields, .. } => {
1538                for (field, pattern) in fields {
1539                    self.emit_match_string_operand(Op::MatchArm, 6, field, line);
1540                    self.chunk.emit_op_u8(Op::MatchArm, 1, line);
1541                    if let Some(pattern) = pattern {
1542                        self.compile_let_pattern(pattern, mutable, line)?;
1543                    } else {
1544                        self.emit_define_local(field, mutable, line);
1545                    }
1546                }
1547                self.chunk.emit_op(Op::Pop, line);
1548            }
1549            Pattern::EnumVariant { fields, .. } => match fields {
1550                EnumPatternFields::None => {
1551                    self.chunk.emit_op(Op::Pop, line);
1552                }
1553                EnumPatternFields::Positional(pats) => {
1554                    for (i, pat) in pats.iter().enumerate() {
1555                        self.chunk.emit_op_u8(Op::MatchArm, 7, line);
1556                        self.chunk.emit(i as u8, line);
1557                        self.compile_let_pattern(pat, mutable, line)?;
1558                    }
1559                    self.chunk.emit_op(Op::Pop, line);
1560                }
1561                EnumPatternFields::Named(_) => {
1562                    return Err(IonError::runtime(
1563                        ion_str!("named enum pattern binding not supported in bytecode VM")
1564                            .to_string(),
1565                        line,
1566                        0,
1567                    ));
1568                }
1569            },
1570            Pattern::Wildcard => {
1571                self.chunk.emit_op(Op::Pop, line);
1572            }
1573        }
1574        Ok(())
1575    }
1576
1577    fn compile_checked_let_pattern(
1578        &mut self,
1579        pattern: &Pattern,
1580        mutable: bool,
1581        line: usize,
1582    ) -> Result<(), IonError> {
1583        match pattern {
1584            Pattern::Ident(_) | Pattern::Wildcard => {
1585                self.compile_let_pattern(pattern, mutable, line)
1586            }
1587            _ => {
1588                let test_keeps_matched_value = matches!(
1589                    pattern,
1590                    Pattern::Some(_)
1591                        | Pattern::Ok(_)
1592                        | Pattern::Err(_)
1593                        | Pattern::Tuple(_)
1594                        | Pattern::List(_, _)
1595                        | Pattern::Struct { .. }
1596                        | Pattern::EnumVariant { .. }
1597                );
1598                self.chunk.emit_op(Op::Dup, line);
1599                self.compile_pattern_test(pattern, line)?;
1600                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1601                self.chunk.emit_op(Op::Pop, line); // pop true
1602                self.compile_let_pattern(pattern, mutable, line)?;
1603                if test_keeps_matched_value {
1604                    self.chunk.emit_op(Op::Pop, line); // pop original matched value
1605                }
1606                let end_jump = self.chunk.emit_jump(Op::Jump, line);
1607                self.chunk.patch_jump(fail_jump);
1608                self.chunk.emit_op(Op::Pop, line); // pop false
1609                self.chunk.emit_op(Op::Pop, line); // pop original value
1610                self.chunk.emit_op(Op::MatchEnd, line);
1611                self.chunk.patch_jump(end_jump);
1612                Ok(())
1613            }
1614        }
1615    }
1616
1617    fn compile_fn_decl(
1618        &mut self,
1619        name: &str,
1620        params: &[Param],
1621        body: &[Stmt],
1622        line: usize,
1623    ) -> Result<(), IonError> {
1624        // Compile function body into a separate chunk
1625        let mut fn_compiler = Compiler::new();
1626        fn_compiler.in_tail_position = true;
1627        // Only dual-define locals if body contains closures
1628        fn_compiler.needs_env_locals = Self::stmts_have_closures(body);
1629        // Pre-register parameters as locals (they'll be pushed by the VM)
1630        for param in params {
1631            fn_compiler.add_local(param.name.clone(), false);
1632        }
1633        // When closures exist, also define params in env so they can be captured
1634        if fn_compiler.needs_env_locals {
1635            for (i, param) in params.iter().enumerate() {
1636                fn_compiler
1637                    .chunk
1638                    .emit_op_u16(Op::GetLocalSlot, i as u16, line);
1639                let idx = fn_compiler
1640                    .chunk
1641                    .add_constant(Value::Str(param.name.clone()));
1642                fn_compiler.chunk.emit_op_u16(Op::DefineLocal, idx, line);
1643                fn_compiler.chunk.emit(0, line); // not mutable
1644            }
1645        }
1646        fn_compiler.compile_block_expr(body, line)?;
1647        fn_compiler.chunk.emit_op(Op::Return, line);
1648        fn_compiler.chunk.peephole_optimize();
1649        let compiled_chunk = fn_compiler.chunk;
1650        // Collect any nested function chunks
1651        self.fn_chunks.extend(fn_compiler.fn_chunks);
1652
1653        let fn_value = Value::Fn(crate::value::IonFn::new(
1654            name.to_string(),
1655            params.to_vec(),
1656            body.to_vec(), // Keep AST body for tree-walk fallback
1657            std::collections::HashMap::new(),
1658        ));
1659        // Extract fn_id to associate with precompiled chunk
1660        if let Value::Fn(ref ion_fn) = fn_value {
1661            self.fn_chunks.insert(ion_fn.fn_id, compiled_chunk);
1662        }
1663
1664        // Define the function in the current scope
1665        self.chunk.emit_constant(fn_value, line);
1666        self.emit_define_local(name, false, line);
1667        Ok(())
1668    }
1669
1670    fn compile_for(
1671        &mut self,
1672        label: Option<String>,
1673        pattern: &Pattern,
1674        iter: &Expr,
1675        body: &[Stmt],
1676        line: usize,
1677    ) -> Result<(), IonError> {
1678        // Evaluate the iterator expression
1679        self.compile_expr(iter)?;
1680
1681        // Convert to iterable (the VM will handle this)
1682        self.chunk.emit_op(Op::IterInit, line);
1683
1684        let loop_start = self.chunk.len();
1685        self.loop_stack.push(LoopFrame {
1686            label,
1687            break_jumps: Vec::new(),
1688            continue_target: loop_start,
1689            is_for_loop: true,
1690            scope_depth: self.scope_depth,
1691        });
1692
1693        // Get next item or jump to end
1694        let exit_jump = self.chunk.emit_jump(Op::IterNext, line);
1695
1696        // Bind pattern
1697        self.begin_scope(line);
1698        self.compile_checked_let_pattern(pattern, false, line)?;
1699
1700        // Execute body (with dead code elimination)
1701        for stmt in body {
1702            self.compile_stmt(stmt)?;
1703            if Self::stmt_is_terminal(stmt) {
1704                break;
1705            }
1706        }
1707        self.end_scope(line);
1708
1709        // Push placeholder for IterNext to pop on next iteration
1710        self.chunk.emit_op(Op::Unit, line);
1711
1712        // Loop back
1713        let offset = self.chunk.len() - loop_start + 3;
1714        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
1715
1716        self.chunk.patch_jump(exit_jump);
1717        // Pop the iterator placeholder
1718        self.chunk.emit_op(Op::Pop, line);
1719
1720        let frame = self.loop_stack.pop().expect("loop frame");
1721        for jump in &frame.break_jumps {
1722            self.chunk.patch_jump(*jump);
1723        }
1724        Ok(())
1725    }
1726
1727    fn compile_while(
1728        &mut self,
1729        label: Option<String>,
1730        cond: &Expr,
1731        body: &[Stmt],
1732        line: usize,
1733    ) -> Result<(), IonError> {
1734        let loop_start = self.chunk.len();
1735        self.loop_stack.push(LoopFrame {
1736            label,
1737            break_jumps: Vec::new(),
1738            continue_target: loop_start,
1739            is_for_loop: false,
1740            scope_depth: self.scope_depth,
1741        });
1742
1743        self.compile_expr(cond)?;
1744        let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1745        self.chunk.emit_op(Op::Pop, line); // pop condition
1746
1747        self.begin_scope(line);
1748        for stmt in body {
1749            self.compile_stmt(stmt)?;
1750            if Self::stmt_is_terminal(stmt) {
1751                break;
1752            }
1753        }
1754        self.end_scope(line);
1755
1756        let offset = self.chunk.len() - loop_start + 3;
1757        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
1758
1759        self.chunk.patch_jump(exit_jump);
1760        self.chunk.emit_op(Op::Pop, line); // pop condition
1761
1762        let frame = self.loop_stack.pop().expect("loop frame");
1763        for jump in &frame.break_jumps {
1764            self.chunk.patch_jump(*jump);
1765        }
1766        Ok(())
1767    }
1768
1769    fn compile_loop(
1770        &mut self,
1771        label: Option<String>,
1772        body: &[Stmt],
1773        line: usize,
1774    ) -> Result<(), IonError> {
1775        let loop_start = self.chunk.len();
1776        self.loop_stack.push(LoopFrame {
1777            label,
1778            break_jumps: Vec::new(),
1779            continue_target: loop_start,
1780            is_for_loop: false,
1781            scope_depth: self.scope_depth,
1782        });
1783
1784        self.begin_scope(line);
1785        for stmt in body {
1786            self.compile_stmt(stmt)?;
1787            if Self::stmt_is_terminal(stmt) {
1788                break;
1789            }
1790        }
1791        self.end_scope(line);
1792
1793        let offset = self.chunk.len() - loop_start + 3;
1794        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
1795
1796        let frame = self.loop_stack.pop().expect("loop frame");
1797        for jump in &frame.break_jumps {
1798            self.chunk.patch_jump(*jump);
1799        }
1800        Ok(())
1801    }
1802
1803    fn compile_assign(
1804        &mut self,
1805        target: &AssignTarget,
1806        op: &AssignOp,
1807        value: &Expr,
1808        line: usize,
1809    ) -> Result<(), IonError> {
1810        match target {
1811            AssignTarget::Ident(name) => {
1812                match op {
1813                    AssignOp::Eq => {
1814                        self.compile_expr(value)?;
1815                    }
1816                    AssignOp::PlusEq | AssignOp::MinusEq | AssignOp::StarEq | AssignOp::SlashEq => {
1817                        self.emit_get_var(name, line);
1818                        self.compile_expr(value)?;
1819                        match op {
1820                            AssignOp::PlusEq => self.chunk.emit_op(Op::Add, line),
1821                            AssignOp::MinusEq => self.chunk.emit_op(Op::Sub, line),
1822                            AssignOp::StarEq => self.chunk.emit_op(Op::Mul, line),
1823                            AssignOp::SlashEq => self.chunk.emit_op(Op::Div, line),
1824                            _ => unreachable!(),
1825                        }
1826                    }
1827                }
1828                self.emit_set_var(name, line);
1829            }
1830            AssignTarget::Index(obj_expr, index_expr) => {
1831                // For index assignment, we need to:
1832                // 1. Get the container, 2. Modify it, 3. Write it back
1833                // This only works when obj_expr is an Ident (variable)
1834                let var_name = match &obj_expr.kind {
1835                    ExprKind::Ident(name) => name.clone(),
1836                    _ => {
1837                        return Err(IonError::runtime(
1838                            ion_str!("index assignment only supported on variables").to_string(),
1839                            line,
1840                            0,
1841                        ))
1842                    }
1843                };
1844
1845                // Get the container
1846                self.compile_expr(obj_expr)?;
1847                self.compile_expr(index_expr)?;
1848
1849                // Compute new value
1850                match op {
1851                    AssignOp::Eq => {
1852                        self.compile_expr(value)?;
1853                    }
1854                    _ => {
1855                        // Get old value for compound assignment
1856                        self.compile_expr(obj_expr)?;
1857                        self.compile_expr(index_expr)?;
1858                        self.chunk.emit_op(Op::GetIndex, line);
1859                        self.compile_expr(value)?;
1860                        match op {
1861                            AssignOp::PlusEq => self.chunk.emit_op(Op::Add, line),
1862                            AssignOp::MinusEq => self.chunk.emit_op(Op::Sub, line),
1863                            AssignOp::StarEq => self.chunk.emit_op(Op::Mul, line),
1864                            AssignOp::SlashEq => self.chunk.emit_op(Op::Div, line),
1865                            _ => unreachable!(),
1866                        }
1867                    }
1868                }
1869
1870                // Stack: [..., obj, index, new_value]
1871                self.chunk.emit_op(Op::SetIndex, line);
1872                // SetIndex returns the modified container — write it back
1873                self.emit_set_var(&var_name, line);
1874            }
1875            AssignTarget::Field(obj_expr, field) => {
1876                let var_name = match &obj_expr.kind {
1877                    ExprKind::Ident(name) => name.clone(),
1878                    _ => {
1879                        return Err(IonError::runtime(
1880                            ion_str!("field assignment only supported on variables").to_string(),
1881                            line,
1882                            0,
1883                        ))
1884                    }
1885                };
1886
1887                self.compile_expr(obj_expr)?;
1888
1889                match op {
1890                    AssignOp::Eq => {
1891                        self.compile_expr(value)?;
1892                    }
1893                    _ => {
1894                        self.chunk.emit_op(Op::Dup, line);
1895                        let get_idx = self.chunk.add_constant(Value::Str(field.clone()));
1896                        self.chunk.emit_op_u16(Op::GetField, get_idx, line);
1897                        self.compile_expr(value)?;
1898                        match op {
1899                            AssignOp::PlusEq => self.chunk.emit_op(Op::Add, line),
1900                            AssignOp::MinusEq => self.chunk.emit_op(Op::Sub, line),
1901                            AssignOp::StarEq => self.chunk.emit_op(Op::Mul, line),
1902                            AssignOp::SlashEq => self.chunk.emit_op(Op::Div, line),
1903                            _ => unreachable!(),
1904                        }
1905                    }
1906                }
1907
1908                // Stack: [..., obj, new_value]
1909                let field_idx = self.chunk.add_constant(Value::Str(field.clone()));
1910                self.chunk.emit_op_u16(Op::SetField, field_idx, line);
1911                // SetField returns the modified container — write it back
1912                self.emit_set_var(&var_name, line);
1913            }
1914        }
1915        Ok(())
1916    }
1917
1918    /// Compile a function body to a standalone chunk (for VM-native function execution).
1919    pub fn compile_fn_body(
1920        mut self,
1921        params: &[Param],
1922        body: &[Stmt],
1923        line: usize,
1924    ) -> Result<Chunk, IonError> {
1925        self.in_tail_position = true;
1926        self.needs_env_locals = Self::stmts_have_closures(body);
1927        // Pre-register parameters as locals
1928        for param in params {
1929            self.add_local(param.name.clone(), false);
1930        }
1931        // When closures exist, also define params in env so they can be captured
1932        if self.needs_env_locals {
1933            for (i, param) in params.iter().enumerate() {
1934                self.chunk.emit_op_u16(Op::GetLocalSlot, i as u16, line);
1935                let idx = self.chunk.add_constant(Value::Str(param.name.clone()));
1936                self.chunk.emit_op_u16(Op::DefineLocal, idx, line);
1937                self.chunk.emit(0, line); // not mutable
1938            }
1939        }
1940        self.compile_block_expr(body, line)?;
1941        self.chunk.emit_op(Op::Return, line);
1942        Ok(self.chunk)
1943    }
1944
1945    fn compile_match(
1946        &mut self,
1947        subject: &Expr,
1948        arms: &[MatchArm],
1949        line: usize,
1950    ) -> Result<(), IonError> {
1951        let was_tail = self.in_tail_position;
1952        // Store subject in a hidden temp variable (not in tail position)
1953        self.begin_scope(line);
1954        self.in_tail_position = false;
1955        self.compile_expr(subject)?;
1956        let tmp_name = "__match_subject__";
1957        self.emit_define_local(tmp_name, false, line);
1958        let subject_slot = self.locals.len() - 1;
1959
1960        let mut end_jumps = Vec::new();
1961
1962        for arm in arms {
1963            // Load subject for pattern test
1964            self.chunk
1965                .emit_op_u16(Op::GetLocalSlot, subject_slot as u16, line);
1966
1967            // Emit pattern test — consumes subject copy, pushes bool
1968            self.compile_pattern_test(&arm.pattern, line)?;
1969
1970            // If guard exists, test it too (only if pattern matched)
1971            if let Some(guard) = &arm.guard {
1972                let skip_guard = self.chunk.emit_jump(Op::JumpIfFalse, line);
1973                self.chunk.emit_op(Op::Pop, line); // pop true
1974                self.compile_expr(guard)?;
1975                let after_guard = self.chunk.emit_jump(Op::Jump, line);
1976                self.chunk.patch_jump(skip_guard);
1977                // false stays on stack — jump lands here
1978                self.chunk.patch_jump(after_guard);
1979            }
1980
1981            let next_arm = self.chunk.emit_jump(Op::JumpIfFalse, line);
1982            self.chunk.emit_op(Op::Pop, line); // pop true
1983
1984            // Bind pattern variables in new scope
1985            self.begin_scope(line);
1986            self.chunk
1987                .emit_op_u16(Op::GetLocalSlot, subject_slot as u16, line);
1988            self.compile_pattern_bind(&arm.pattern, line)?;
1989
1990            // Compile arm body — inherits tail position
1991            self.in_tail_position = was_tail;
1992            self.compile_expr(&arm.body)?;
1993            self.end_scope(line);
1994
1995            end_jumps.push(self.chunk.emit_jump(Op::Jump, line));
1996
1997            self.chunk.patch_jump(next_arm);
1998            self.chunk.emit_op(Op::Pop, line); // pop false
1999        }
2000
2001        // No arm matched — runtime error (matches interpreter behavior)
2002        self.chunk.emit_op(Op::MatchEnd, line);
2003
2004        for j in end_jumps {
2005            self.chunk.patch_jump(j);
2006        }
2007
2008        self.end_scope(line); // pop the match subject scope
2009        Ok(())
2010    }
2011
2012    fn emit_match_string_operand(&mut self, op: Op, kind: u8, value: &str, line: usize) {
2013        let idx = self.chunk.add_constant(Value::Str(value.to_string()));
2014        self.chunk.emit_op_u8(op, kind, line);
2015        self.chunk.emit((idx >> 8) as u8, line);
2016        self.chunk.emit((idx & 0xff) as u8, line);
2017    }
2018
2019    fn emit_match_enum_operand(
2020        &mut self,
2021        enum_name: &str,
2022        variant: &str,
2023        arity: usize,
2024        line: usize,
2025    ) -> Result<(), IonError> {
2026        if arity > u8::MAX as usize {
2027            return Err(IonError::runtime(
2028                ion_str!("enum pattern has too many fields").to_string(),
2029                line,
2030                0,
2031            ));
2032        }
2033        let enum_idx = self.chunk.add_constant(Value::Str(enum_name.to_string()));
2034        let variant_idx = self.chunk.add_constant(Value::Str(variant.to_string()));
2035        self.chunk.emit_op_u8(Op::MatchBegin, 7, line);
2036        self.chunk.emit((enum_idx >> 8) as u8, line);
2037        self.chunk.emit((enum_idx & 0xff) as u8, line);
2038        self.chunk.emit((variant_idx >> 8) as u8, line);
2039        self.chunk.emit((variant_idx & 0xff) as u8, line);
2040        self.chunk.emit(arity as u8, line);
2041        Ok(())
2042    }
2043
2044    fn compile_struct_field_test(
2045        &mut self,
2046        field: &str,
2047        pattern: Option<&Pattern>,
2048        line: usize,
2049    ) -> Result<(), IonError> {
2050        self.emit_match_string_operand(Op::MatchArm, 6, field, line);
2051        self.chunk.emit_op_u8(Op::MatchBegin, 1, line); // field exists: Option::Some
2052        let missing_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2053        self.chunk.emit_op(Op::Pop, line); // pop true
2054        self.chunk.emit_op_u8(Op::MatchArm, 1, line); // unwrap Some(field)
2055        if let Some(pattern) = pattern {
2056            self.compile_pattern_test(pattern, line)?;
2057        } else {
2058            self.chunk.emit_op(Op::Pop, line); // shorthand field binding always matches
2059            self.chunk.emit_op(Op::True, line);
2060        }
2061        let end = self.chunk.emit_jump(Op::Jump, line);
2062        self.chunk.patch_jump(missing_jump);
2063        self.chunk.patch_jump(end);
2064        Ok(())
2065    }
2066
2067    /// Compile a pattern test: consumes the value on stack, pushes bool.
2068    fn compile_pattern_test(&mut self, pattern: &Pattern, line: usize) -> Result<(), IonError> {
2069        match pattern {
2070            Pattern::Wildcard | Pattern::Ident(_) => {
2071                self.chunk.emit_op(Op::Pop, line); // consume value
2072                self.chunk.emit_op(Op::True, line); // always matches
2073            }
2074            Pattern::Int(n) => {
2075                self.chunk.emit_constant(Value::Int(*n), line);
2076                self.chunk.emit_op(Op::Eq, line);
2077            }
2078            Pattern::Float(n) => {
2079                self.chunk.emit_constant(Value::Float(*n), line);
2080                self.chunk.emit_op(Op::Eq, line);
2081            }
2082            Pattern::Bool(b) => {
2083                self.chunk
2084                    .emit_op(if *b { Op::True } else { Op::False }, line);
2085                self.chunk.emit_op(Op::Eq, line);
2086            }
2087            Pattern::Str(s) => {
2088                self.chunk.emit_constant(Value::Str(s.clone()), line);
2089                self.chunk.emit_op(Op::Eq, line);
2090            }
2091            Pattern::Bytes(b) => {
2092                self.chunk.emit_constant(Value::Bytes(b.clone()), line);
2093                self.chunk.emit_op(Op::Eq, line);
2094            }
2095            Pattern::None => {
2096                // Check if value is Option(None)
2097                self.chunk.emit_op(Op::None, line);
2098                self.chunk.emit_op(Op::Eq, line);
2099            }
2100            Pattern::Some(inner) => {
2101                // Test: is it Some(x)? Use MatchArm opcode for complex patterns
2102                // For now, test structurally: use a simpler encoding
2103                // We'll use the MatchBegin/MatchArm opcodes repurposed:
2104                // Actually, let's just emit inline checks.
2105                // Stack has value. We need to check if it's Some(_) and test inner.
2106                self.chunk.emit_op_u8(Op::MatchBegin, 1, line); // 1 = test Some
2107                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2108                self.chunk.emit_op(Op::Pop, line); // pop true
2109                                                   // Now unwrap the Some and test inner pattern
2110                self.chunk.emit_op_u8(Op::MatchArm, 1, line); // 1 = unwrap Some
2111                self.compile_pattern_test(inner, line)?;
2112                let end = self.chunk.emit_jump(Op::Jump, line);
2113                self.chunk.patch_jump(fail_jump);
2114                // false stays
2115                self.chunk.patch_jump(end);
2116            }
2117            Pattern::Ok(inner) => {
2118                self.chunk.emit_op_u8(Op::MatchBegin, 2, line); // 2 = test Ok
2119                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2120                self.chunk.emit_op(Op::Pop, line);
2121                self.chunk.emit_op_u8(Op::MatchArm, 2, line); // 2 = unwrap Ok
2122                self.compile_pattern_test(inner, line)?;
2123                let end = self.chunk.emit_jump(Op::Jump, line);
2124                self.chunk.patch_jump(fail_jump);
2125                self.chunk.patch_jump(end);
2126            }
2127            Pattern::Err(inner) => {
2128                self.chunk.emit_op_u8(Op::MatchBegin, 3, line); // 3 = test Err
2129                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2130                self.chunk.emit_op(Op::Pop, line);
2131                self.chunk.emit_op_u8(Op::MatchArm, 3, line); // 3 = unwrap Err
2132                self.compile_pattern_test(inner, line)?;
2133                let end = self.chunk.emit_jump(Op::Jump, line);
2134                self.chunk.patch_jump(fail_jump);
2135                self.chunk.patch_jump(end);
2136            }
2137            Pattern::Tuple(pats) => {
2138                // Check: is it a tuple of the right length, and do all sub-patterns match?
2139                self.chunk.emit_op_u8(Op::MatchBegin, 4, line); // 4 = test Tuple
2140                self.chunk.emit(pats.len() as u8, line); // expected length
2141                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2142                self.chunk.emit_op(Op::Pop, line); // pop true
2143                                                   // Test each element
2144                for (i, pat) in pats.iter().enumerate() {
2145                    // Load the subject again and index into it
2146                    self.chunk.emit_op_u8(Op::MatchArm, 4, line); // 4 = get tuple element
2147                    self.chunk.emit(i as u8, line);
2148                    self.compile_pattern_test(pat, line)?;
2149                    let sub_fail = self.chunk.emit_jump(Op::JumpIfFalse, line);
2150                    self.chunk.emit_op(Op::Pop, line); // pop true, continue
2151                    if i == pats.len() - 1 {
2152                        // All matched
2153                        self.chunk.emit_op(Op::True, line);
2154                    }
2155                    // Patch sub_fail to push false and skip remaining
2156                    let sub_end = self.chunk.emit_jump(Op::Jump, line);
2157                    self.chunk.patch_jump(sub_fail);
2158                    // false stays on stack
2159                    self.chunk.patch_jump(sub_end);
2160                }
2161                if pats.is_empty() {
2162                    self.chunk.emit_op(Op::True, line);
2163                }
2164                let end = self.chunk.emit_jump(Op::Jump, line);
2165                self.chunk.patch_jump(fail_jump);
2166                // false stays
2167                self.chunk.patch_jump(end);
2168            }
2169            Pattern::List(pats, rest) => {
2170                // Check: is it a list with at least pats.len() elements (or exact if no rest)?
2171                let has_rest = rest.is_some();
2172                self.chunk.emit_op_u8(Op::MatchBegin, 5, line); // 5 = test List
2173                self.chunk.emit(pats.len() as u8, line); // min/exact length
2174                self.chunk.emit(if has_rest { 1 } else { 0 }, line); // has_rest flag
2175                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2176                self.chunk.emit_op(Op::Pop, line); // pop true
2177                                                   // Test each element pattern
2178                for (i, pat) in pats.iter().enumerate() {
2179                    self.chunk.emit_op_u8(Op::MatchArm, 5, line); // 5 = get list element
2180                    self.chunk.emit(i as u8, line);
2181                    self.compile_pattern_test(pat, line)?;
2182                    let sub_fail = self.chunk.emit_jump(Op::JumpIfFalse, line);
2183                    self.chunk.emit_op(Op::Pop, line); // pop true
2184                    if i == pats.len() - 1 {
2185                        self.chunk.emit_op(Op::True, line);
2186                    }
2187                    let sub_end = self.chunk.emit_jump(Op::Jump, line);
2188                    self.chunk.patch_jump(sub_fail);
2189                    self.chunk.patch_jump(sub_end);
2190                }
2191                if pats.is_empty() {
2192                    self.chunk.emit_op(Op::True, line);
2193                }
2194                let end = self.chunk.emit_jump(Op::Jump, line);
2195                self.chunk.patch_jump(fail_jump);
2196                self.chunk.patch_jump(end);
2197            }
2198            Pattern::Struct { name, fields } => {
2199                self.emit_match_string_operand(Op::MatchBegin, 6, name, line);
2200                let type_fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2201                self.chunk.emit_op(Op::Pop, line); // pop true
2202
2203                let mut field_fail_jumps = Vec::new();
2204                for (field, pattern) in fields {
2205                    self.compile_struct_field_test(field, pattern.as_ref(), line)?;
2206                    field_fail_jumps.push(self.chunk.emit_jump(Op::JumpIfFalse, line));
2207                    self.chunk.emit_op(Op::Pop, line); // pop true
2208                }
2209
2210                self.chunk.emit_op(Op::True, line);
2211                let end = self.chunk.emit_jump(Op::Jump, line);
2212                self.chunk.patch_jump(type_fail_jump);
2213                for jump in field_fail_jumps {
2214                    self.chunk.patch_jump(jump);
2215                }
2216                self.chunk.patch_jump(end);
2217            }
2218            Pattern::EnumVariant {
2219                enum_name,
2220                variant,
2221                fields,
2222            } => match fields {
2223                EnumPatternFields::None => {
2224                    self.emit_match_enum_operand(enum_name, variant, 0, line)?;
2225                }
2226                EnumPatternFields::Positional(pats) => {
2227                    self.emit_match_enum_operand(enum_name, variant, pats.len(), line)?;
2228                    let variant_fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2229                    self.chunk.emit_op(Op::Pop, line); // pop true
2230
2231                    let mut field_fail_jumps = Vec::new();
2232                    for (i, pat) in pats.iter().enumerate() {
2233                        self.chunk.emit_op_u8(Op::MatchArm, 7, line);
2234                        self.chunk.emit(i as u8, line);
2235                        self.compile_pattern_test(pat, line)?;
2236                        field_fail_jumps.push(self.chunk.emit_jump(Op::JumpIfFalse, line));
2237                        self.chunk.emit_op(Op::Pop, line); // pop true
2238                    }
2239
2240                    self.chunk.emit_op(Op::True, line);
2241                    let end = self.chunk.emit_jump(Op::Jump, line);
2242                    self.chunk.patch_jump(variant_fail_jump);
2243                    for jump in field_fail_jumps {
2244                        self.chunk.patch_jump(jump);
2245                    }
2246                    self.chunk.patch_jump(end);
2247                }
2248                EnumPatternFields::Named(_) => {
2249                    return Err(IonError::runtime(
2250                        ion_str!("named enum patterns not supported in bytecode VM match")
2251                            .to_string(),
2252                        line,
2253                        0,
2254                    ));
2255                }
2256            },
2257        }
2258        Ok(())
2259    }
2260
2261    /// Bind pattern variables: consumes value on stack.
2262    fn compile_pattern_bind(&mut self, pattern: &Pattern, line: usize) -> Result<(), IonError> {
2263        match pattern {
2264            Pattern::Wildcard => {
2265                self.chunk.emit_op(Op::Pop, line);
2266            }
2267            Pattern::Ident(name) => {
2268                self.emit_define_local(name, false, line);
2269            }
2270            Pattern::Int(_)
2271            | Pattern::Float(_)
2272            | Pattern::Bool(_)
2273            | Pattern::Str(_)
2274            | Pattern::Bytes(_)
2275            | Pattern::None => {
2276                self.chunk.emit_op(Op::Pop, line); // no bindings for literals
2277            }
2278            Pattern::Some(inner) => {
2279                // Unwrap the Some value
2280                self.chunk.emit_op_u8(Op::MatchArm, 1, line); // unwrap Some
2281                self.compile_pattern_bind(inner, line)?;
2282            }
2283            Pattern::Ok(inner) => {
2284                self.chunk.emit_op_u8(Op::MatchArm, 2, line); // unwrap Ok
2285                self.compile_pattern_bind(inner, line)?;
2286            }
2287            Pattern::Err(inner) => {
2288                self.chunk.emit_op_u8(Op::MatchArm, 3, line); // unwrap Err
2289                self.compile_pattern_bind(inner, line)?;
2290            }
2291            Pattern::Tuple(pats) => {
2292                for (i, pat) in pats.iter().enumerate() {
2293                    self.chunk.emit_op(Op::Dup, line); // dup tuple
2294                    self.chunk.emit_constant(Value::Int(i as i64), line);
2295                    self.chunk.emit_op(Op::GetIndex, line);
2296                    self.compile_pattern_bind(pat, line)?;
2297                }
2298                self.chunk.emit_op(Op::Pop, line); // pop tuple
2299            }
2300            Pattern::List(pats, rest) => {
2301                // Bind each element
2302                for (i, pat) in pats.iter().enumerate() {
2303                    self.chunk.emit_op(Op::Dup, line); // dup list
2304                    self.chunk.emit_constant(Value::Int(i as i64), line);
2305                    self.chunk.emit_op(Op::GetIndex, line);
2306                    self.compile_pattern_bind(pat, line)?;
2307                }
2308                // If there's a rest pattern, bind the remaining elements
2309                if let Some(rest_pat) = rest {
2310                    self.chunk.emit_op(Op::Dup, line); // dup list
2311                                                       // Slice from pats.len() to end
2312                    self.chunk
2313                        .emit_constant(Value::Int(pats.len() as i64), line);
2314                    // Use Slice with has_start only
2315                    self.chunk.emit_op_u8(Op::Slice, 1, line); // flags: has_start=1
2316                    self.compile_pattern_bind(rest_pat, line)?;
2317                }
2318                self.chunk.emit_op(Op::Pop, line); // pop list
2319            }
2320            Pattern::Struct { fields, .. } => {
2321                for (field, pattern) in fields {
2322                    self.emit_match_string_operand(Op::MatchArm, 6, field, line);
2323                    self.chunk.emit_op_u8(Op::MatchArm, 1, line); // unwrap Some(field)
2324                    if let Some(pattern) = pattern {
2325                        self.compile_pattern_bind(pattern, line)?;
2326                    } else {
2327                        self.emit_define_local(field, false, line);
2328                    }
2329                }
2330                self.chunk.emit_op(Op::Pop, line);
2331            }
2332            Pattern::EnumVariant { fields, .. } => match fields {
2333                EnumPatternFields::None => {
2334                    self.chunk.emit_op(Op::Pop, line);
2335                }
2336                EnumPatternFields::Positional(pats) => {
2337                    for (i, pat) in pats.iter().enumerate() {
2338                        self.chunk.emit_op_u8(Op::MatchArm, 7, line);
2339                        self.chunk.emit(i as u8, line);
2340                        self.compile_pattern_bind(pat, line)?;
2341                    }
2342                    self.chunk.emit_op(Op::Pop, line);
2343                }
2344                EnumPatternFields::Named(_) => {
2345                    return Err(IonError::runtime(
2346                        ion_str!("named enum pattern binding not supported in bytecode VM")
2347                            .to_string(),
2348                        line,
2349                        0,
2350                    ));
2351                }
2352            },
2353        }
2354        Ok(())
2355    }
2356
2357    fn compile_list_comp(
2358        &mut self,
2359        item_expr: &Expr,
2360        pattern: &Pattern,
2361        iter: &Expr,
2362        cond: Option<&Expr>,
2363        line: usize,
2364    ) -> Result<(), IonError> {
2365        // Build an empty list, then iterate and append
2366        self.chunk.emit_op_u16(Op::BuildList, 0, line); // empty list on stack
2367
2368        // Evaluate iterator
2369        self.compile_expr(iter)?;
2370        self.chunk.emit_op(Op::IterInit, line);
2371
2372        let loop_start = self.chunk.len();
2373        let exit_jump = self.chunk.emit_jump(Op::IterNext, line);
2374
2375        // Bind pattern in scope
2376        self.begin_scope(line);
2377        self.compile_checked_let_pattern(pattern, false, line)?;
2378
2379        // If there's a condition, check it
2380        if let Some(cond_expr) = cond {
2381            self.compile_expr(cond_expr)?;
2382            let skip_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2383            self.chunk.emit_op(Op::Pop, line); // pop true
2384
2385            // Compile item expression and append
2386            self.compile_expr(item_expr)?;
2387            self.chunk.emit_op(Op::ListAppend, line);
2388
2389            let after = self.chunk.emit_jump(Op::Jump, line);
2390            self.chunk.patch_jump(skip_jump);
2391            self.chunk.emit_op(Op::Pop, line); // pop false
2392            self.chunk.patch_jump(after);
2393        } else {
2394            // Compile item expression and append
2395            self.compile_expr(item_expr)?;
2396            self.chunk.emit_op(Op::ListAppend, line);
2397        }
2398
2399        self.end_scope(line);
2400
2401        // Push placeholder for IterNext to pop on next iteration
2402        self.chunk.emit_op(Op::Unit, line);
2403
2404        // Loop back
2405        let offset = self.chunk.len() - loop_start + 3;
2406        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
2407
2408        self.chunk.patch_jump(exit_jump);
2409        self.chunk.emit_op(Op::Pop, line); // pop exhausted iterator placeholder
2410                                           // List is still on stack
2411        Ok(())
2412    }
2413
2414    fn compile_dict_comp(
2415        &mut self,
2416        key_expr: &Expr,
2417        value_expr: &Expr,
2418        pattern: &Pattern,
2419        iter: &Expr,
2420        cond: Option<&Expr>,
2421        line: usize,
2422    ) -> Result<(), IonError> {
2423        // Build an empty dict, then iterate and insert
2424        self.chunk.emit_op_u16(Op::BuildDict, 0, line);
2425
2426        self.compile_expr(iter)?;
2427        self.chunk.emit_op(Op::IterInit, line);
2428
2429        let loop_start = self.chunk.len();
2430        let exit_jump = self.chunk.emit_jump(Op::IterNext, line);
2431
2432        self.begin_scope(line);
2433        self.compile_checked_let_pattern(pattern, false, line)?;
2434
2435        if let Some(cond_expr) = cond {
2436            self.compile_expr(cond_expr)?;
2437            let skip_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2438            self.chunk.emit_op(Op::Pop, line);
2439
2440            self.compile_expr(key_expr)?;
2441            self.compile_expr(value_expr)?;
2442            self.chunk.emit_op(Op::DictInsert, line);
2443
2444            let after = self.chunk.emit_jump(Op::Jump, line);
2445            self.chunk.patch_jump(skip_jump);
2446            self.chunk.emit_op(Op::Pop, line);
2447            self.chunk.patch_jump(after);
2448        } else {
2449            self.compile_expr(key_expr)?;
2450            self.compile_expr(value_expr)?;
2451            self.chunk.emit_op(Op::DictInsert, line);
2452        }
2453
2454        self.end_scope(line);
2455
2456        // Push placeholder for IterNext to pop on next iteration
2457        self.chunk.emit_op(Op::Unit, line);
2458
2459        let offset = self.chunk.len() - loop_start + 3;
2460        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
2461
2462        self.chunk.patch_jump(exit_jump);
2463        self.chunk.emit_op(Op::Pop, line);
2464        Ok(())
2465    }
2466}