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}
53
54impl Default for Compiler {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl Compiler {
61    pub fn new() -> Self {
62        Self {
63            chunk: Chunk::new(),
64            fn_chunks: FnChunkCache::new(),
65            in_tail_position: false,
66            locals: Vec::new(),
67            scope_depth: 0,
68            needs_env_locals: true, // conservative default for top-level
69            loop_stack: Vec::new(),
70        }
71    }
72
73    /// Find the index of the loop frame in `loop_stack` that a labeled (or
74    /// unlabeled) break/continue should target. Returns `None` if no enclosing
75    /// loop matches.
76    fn resolve_loop_target(&self, label: Option<&str>) -> Option<usize> {
77        match label {
78            None => self.loop_stack.len().checked_sub(1),
79            Some(want) => self
80                .loop_stack
81                .iter()
82                .rposition(|f| f.label.as_deref() == Some(want)),
83        }
84    }
85
86    /// Check if a list of statements contains any closures (lambdas or inner fn decls).
87    fn stmts_have_closures(stmts: &[Stmt]) -> bool {
88        for stmt in stmts {
89            match &stmt.kind {
90                StmtKind::FnDecl { body: _, .. } => {
91                    // Inner fn decl itself is a closure; also check its body
92                    return true;
93                    // Note: we don't need to recurse — just the presence of a fn decl
94                    // in the current scope means outer locals might be captured
95                }
96                StmtKind::ExprStmt { expr, .. } if Self::expr_has_closures(expr) => {
97                    return true;
98                }
99                StmtKind::Let { value, .. } if Self::expr_has_closures(value) => {
100                    return true;
101                }
102                StmtKind::For { body, iter, .. } => {
103                    if Self::expr_has_closures(iter) {
104                        return true;
105                    }
106                    if Self::stmts_have_closures(body) {
107                        return true;
108                    }
109                }
110                StmtKind::While { cond, body, .. } => {
111                    if Self::expr_has_closures(cond) {
112                        return true;
113                    }
114                    if Self::stmts_have_closures(body) {
115                        return true;
116                    }
117                }
118                StmtKind::Loop { body, .. } if Self::stmts_have_closures(body) => {
119                    return true;
120                }
121                StmtKind::Return { value: Some(e) } if Self::expr_has_closures(e) => {
122                    return true;
123                }
124                StmtKind::Assign { value, .. } if Self::expr_has_closures(value) => {
125                    return true;
126                }
127                StmtKind::WhileLet { expr, body, .. } => {
128                    if Self::expr_has_closures(expr) {
129                        return true;
130                    }
131                    if Self::stmts_have_closures(body) {
132                        return true;
133                    }
134                }
135                _ => {}
136            }
137        }
138        false
139    }
140
141    fn expr_has_closures(expr: &Expr) -> bool {
142        match &expr.kind {
143            ExprKind::Lambda { .. } => true,
144            ExprKind::If {
145                cond,
146                then_body,
147                else_body,
148            } => {
149                Self::expr_has_closures(cond)
150                    || Self::stmts_have_closures(then_body)
151                    || else_body
152                        .as_ref()
153                        .is_some_and(|b| Self::stmts_have_closures(b))
154            }
155            ExprKind::Block(stmts) => Self::stmts_have_closures(stmts),
156            ExprKind::Call { func, args } => {
157                Self::expr_has_closures(func)
158                    || args.iter().any(|a| Self::expr_has_closures(&a.value))
159            }
160            ExprKind::MethodCall { expr, args, .. } => {
161                Self::expr_has_closures(expr)
162                    || args.iter().any(|a| Self::expr_has_closures(&a.value))
163            }
164            ExprKind::BinOp { left, right, .. } => {
165                Self::expr_has_closures(left) || Self::expr_has_closures(right)
166            }
167            ExprKind::UnaryOp { expr, .. } => Self::expr_has_closures(expr),
168            ExprKind::PipeOp { left, right } => {
169                Self::expr_has_closures(left) || Self::expr_has_closures(right)
170            }
171            ExprKind::Match { expr, arms } => {
172                Self::expr_has_closures(expr)
173                    || arms.iter().any(|a| Self::expr_has_closures(&a.body))
174            }
175            ExprKind::List(items) => items.iter().any(|e| match e {
176                ListEntry::Elem(expr) | ListEntry::Spread(expr) => Self::expr_has_closures(expr),
177            }),
178            ExprKind::Tuple(items) => items.iter().any(Self::expr_has_closures),
179            ExprKind::ListComp {
180                expr, iter, cond, ..
181            } => {
182                Self::expr_has_closures(expr)
183                    || Self::expr_has_closures(iter)
184                    || cond.as_ref().is_some_and(|c| Self::expr_has_closures(c))
185            }
186            ExprKind::IfLet {
187                expr,
188                then_body,
189                else_body,
190                ..
191            } => {
192                Self::expr_has_closures(expr)
193                    || Self::stmts_have_closures(then_body)
194                    || else_body
195                        .as_ref()
196                        .is_some_and(|b| Self::stmts_have_closures(b))
197            }
198            ExprKind::TryCatch { body, handler, .. } => {
199                Self::stmts_have_closures(body) || Self::stmts_have_closures(handler)
200            }
201            ExprKind::LoopExpr(stmts) => Self::stmts_have_closures(stmts),
202            ExprKind::Range { start, end, .. } => {
203                Self::expr_has_closures(start) || Self::expr_has_closures(end)
204            }
205            ExprKind::Dict(entries) => entries.iter().any(|e| match e {
206                DictEntry::KeyValue(k, v) => {
207                    Self::expr_has_closures(k) || Self::expr_has_closures(v)
208                }
209                DictEntry::Spread(expr) => Self::expr_has_closures(expr),
210            }),
211            ExprKind::DictComp {
212                key,
213                value,
214                iter,
215                cond,
216                ..
217            } => {
218                Self::expr_has_closures(key)
219                    || Self::expr_has_closures(value)
220                    || Self::expr_has_closures(iter)
221                    || cond.as_ref().is_some_and(|c| Self::expr_has_closures(c))
222            }
223            ExprKind::FieldAccess { expr, .. }
224            | ExprKind::Try(expr)
225            | ExprKind::SomeExpr(expr)
226            | ExprKind::OkExpr(expr)
227            | ExprKind::ErrExpr(expr) => Self::expr_has_closures(expr),
228            ExprKind::Index { expr, index } => {
229                Self::expr_has_closures(expr) || Self::expr_has_closures(index)
230            }
231            ExprKind::Slice {
232                expr, start, end, ..
233            } => {
234                Self::expr_has_closures(expr)
235                    || start.as_ref().is_some_and(|s| Self::expr_has_closures(s))
236                    || end.as_ref().is_some_and(|e| Self::expr_has_closures(e))
237            }
238            ExprKind::FStr(parts) => parts.iter().any(|p| match p {
239                FStrPart::Expr(e) => Self::expr_has_closures(e),
240                _ => false,
241            }),
242            ExprKind::StructConstruct { fields, spread, .. } => {
243                fields.iter().any(|(_, e)| Self::expr_has_closures(e))
244                    || spread.as_ref().is_some_and(|s| Self::expr_has_closures(s))
245            }
246            _ => false,
247        }
248    }
249
250    /// Try to constant-fold a binary operation on two literal operands.
251    #[cfg(feature = "optimize")]
252    fn try_fold_binop(left: &Expr, op: &BinOp, right: &Expr) -> Option<Value> {
253        match (&left.kind, op, &right.kind) {
254            // Int op Int
255            (ExprKind::Int(a), BinOp::Add, ExprKind::Int(b)) => {
256                Some(Value::Int(a.wrapping_add(*b)))
257            }
258            (ExprKind::Int(a), BinOp::Sub, ExprKind::Int(b)) => {
259                Some(Value::Int(a.wrapping_sub(*b)))
260            }
261            (ExprKind::Int(a), BinOp::Mul, ExprKind::Int(b)) => {
262                Some(Value::Int(a.wrapping_mul(*b)))
263            }
264            (ExprKind::Int(a), BinOp::Div, ExprKind::Int(b)) if *b != 0 => Some(Value::Int(a / b)),
265            (ExprKind::Int(a), BinOp::Mod, ExprKind::Int(b)) if *b != 0 => Some(Value::Int(a % b)),
266            (ExprKind::Int(a), BinOp::Eq, ExprKind::Int(b)) => Some(Value::Bool(a == b)),
267            (ExprKind::Int(a), BinOp::Ne, ExprKind::Int(b)) => Some(Value::Bool(a != b)),
268            (ExprKind::Int(a), BinOp::Lt, ExprKind::Int(b)) => Some(Value::Bool(a < b)),
269            (ExprKind::Int(a), BinOp::Gt, ExprKind::Int(b)) => Some(Value::Bool(a > b)),
270            (ExprKind::Int(a), BinOp::Le, ExprKind::Int(b)) => Some(Value::Bool(a <= b)),
271            (ExprKind::Int(a), BinOp::Ge, ExprKind::Int(b)) => Some(Value::Bool(a >= b)),
272            (ExprKind::Int(a), BinOp::BitAnd, ExprKind::Int(b)) => Some(Value::Int(a & b)),
273            (ExprKind::Int(a), BinOp::BitOr, ExprKind::Int(b)) => Some(Value::Int(a | b)),
274            (ExprKind::Int(a), BinOp::BitXor, ExprKind::Int(b)) => Some(Value::Int(a ^ b)),
275            (ExprKind::Int(a), BinOp::Shl, ExprKind::Int(b)) if (0..64).contains(b) => {
276                Some(Value::Int(a << (*b as u32)))
277            }
278            (ExprKind::Int(a), BinOp::Shr, ExprKind::Int(b)) if (0..64).contains(b) => {
279                Some(Value::Int(a >> (*b as u32)))
280            }
281            // Float op Float
282            (ExprKind::Float(a), BinOp::Add, ExprKind::Float(b)) => Some(Value::Float(a + b)),
283            (ExprKind::Float(a), BinOp::Sub, ExprKind::Float(b)) => Some(Value::Float(a - b)),
284            (ExprKind::Float(a), BinOp::Mul, ExprKind::Float(b)) => Some(Value::Float(a * b)),
285            (ExprKind::Float(a), BinOp::Div, ExprKind::Float(b)) => Some(Value::Float(a / b)),
286            (ExprKind::Float(a), BinOp::Mod, ExprKind::Float(b)) => Some(Value::Float(a % b)),
287            // Int op Float / Float op Int
288            (ExprKind::Int(a), BinOp::Add, ExprKind::Float(b)) => Some(Value::Float(*a as f64 + b)),
289            (ExprKind::Float(a), BinOp::Add, ExprKind::Int(b)) => Some(Value::Float(a + *b as f64)),
290            (ExprKind::Int(a), BinOp::Sub, ExprKind::Float(b)) => Some(Value::Float(*a as f64 - b)),
291            (ExprKind::Float(a), BinOp::Sub, ExprKind::Int(b)) => Some(Value::Float(a - *b as f64)),
292            (ExprKind::Int(a), BinOp::Mul, ExprKind::Float(b)) => Some(Value::Float(*a as f64 * b)),
293            (ExprKind::Float(a), BinOp::Mul, ExprKind::Int(b)) => Some(Value::Float(a * *b as f64)),
294            (ExprKind::Int(a), BinOp::Div, ExprKind::Float(b)) => Some(Value::Float(*a as f64 / b)),
295            (ExprKind::Float(a), BinOp::Div, ExprKind::Int(b)) => Some(Value::Float(a / *b as f64)),
296            // String concat
297            (ExprKind::Str(a), BinOp::Add, ExprKind::Str(b)) => {
298                let mut s = a.clone();
299                s.push_str(b);
300                Some(Value::Str(s))
301            }
302            // Bool logic
303            (ExprKind::Bool(a), BinOp::And, ExprKind::Bool(b)) => Some(Value::Bool(*a && *b)),
304            (ExprKind::Bool(a), BinOp::Or, ExprKind::Bool(b)) => Some(Value::Bool(*a || *b)),
305            (ExprKind::Bool(a), BinOp::Eq, ExprKind::Bool(b)) => Some(Value::Bool(a == b)),
306            (ExprKind::Bool(a), BinOp::Ne, ExprKind::Bool(b)) => Some(Value::Bool(a != b)),
307            _ => None,
308        }
309    }
310
311    /// Try to constant-fold a unary operation on a literal operand.
312    #[cfg(feature = "optimize")]
313    fn try_fold_unary(op: &UnaryOp, inner: &Expr) -> Option<Value> {
314        match (op, &inner.kind) {
315            (UnaryOp::Neg, ExprKind::Int(v)) => Some(Value::Int(-v)),
316            (UnaryOp::Neg, ExprKind::Float(v)) => Some(Value::Float(-v)),
317            (UnaryOp::Not, ExprKind::Bool(v)) => Some(Value::Bool(!v)),
318            _ => None,
319        }
320    }
321
322    /// Check if a statement is terminal (control never continues past it).
323    #[cfg(feature = "optimize")]
324    fn stmt_is_terminal(stmt: &Stmt) -> bool {
325        matches!(
326            &stmt.kind,
327            StmtKind::Return { .. } | StmtKind::Break { .. } | StmtKind::Continue { .. }
328        )
329    }
330
331    /// Resolve a local variable name to its slot index (searching innermost first).
332    fn resolve_local(&self, name: &str) -> Option<usize> {
333        for (i, local) in self.locals.iter().enumerate().rev() {
334            if local.name == name {
335                return Some(i);
336            }
337        }
338        None
339    }
340
341    /// Add a local variable to the compile-time tracking.
342    fn add_local(&mut self, name: String, _mutable: bool) {
343        self.locals.push(Local {
344            name,
345            depth: self.scope_depth,
346        });
347    }
348
349    /// Begin a new compile-time scope and emit PushScope.
350    fn begin_scope(&mut self, line: usize) {
351        self.scope_depth += 1;
352        self.chunk.emit_op(Op::PushScope, line);
353    }
354
355    /// Emit a variable read (GetLocalSlot or GetGlobal).
356    fn emit_get_var(&mut self, name: &str, line: usize) {
357        if let Some(slot) = self.resolve_local(name) {
358            self.chunk.emit_op_u16(Op::GetLocalSlot, slot as u16, line);
359        } else {
360            let idx = self.chunk.add_constant(Value::Str(name.to_string()));
361            self.chunk.emit_op_u16(Op::GetGlobal, idx, line);
362        }
363    }
364
365    /// Define a new local variable. When closures exist, also stores in env.
366    /// The value must be on top of the stack.
367    fn emit_define_local(&mut self, name: &str, mutable: bool, line: usize) {
368        self.add_local(name.to_string(), mutable);
369        if self.needs_env_locals {
370            // Dup value: one copy for env (closure capture), one for slot
371            self.chunk.emit_op(Op::Dup, line);
372            let idx = self.chunk.add_constant(Value::Str(name.to_string()));
373            self.chunk.emit_op_u16(Op::DefineLocal, idx, line);
374            self.chunk.emit(if mutable { 1 } else { 0 }, line);
375        }
376        self.chunk
377            .emit_op_u8(Op::DefineLocalSlot, if mutable { 1 } else { 0 }, line);
378    }
379
380    /// Emit a variable write (SetLocalSlot or SetGlobal).
381    fn emit_set_var(&mut self, name: &str, line: usize) {
382        if let Some(slot) = self.resolve_local(name) {
383            self.chunk.emit_op_u16(Op::SetLocalSlot, slot as u16, line);
384        } else {
385            let idx = self.chunk.add_constant(Value::Str(name.to_string()));
386            self.chunk.emit_op_u16(Op::SetGlobal, idx, line);
387        }
388    }
389
390    /// End the current compile-time scope, emit PopScope.
391    fn end_scope(&mut self, line: usize) {
392        while let Some(local) = self.locals.last() {
393            if local.depth < self.scope_depth {
394                break;
395            }
396            self.locals.pop();
397        }
398        self.scope_depth -= 1;
399        self.chunk.emit_op(Op::PopScope, line);
400    }
401
402    pub fn compile_program(mut self, program: &Program) -> Result<(Chunk, FnChunkCache), IonError> {
403        let len = program.stmts.len();
404        for (i, stmt) in program.stmts.iter().enumerate() {
405            let is_last = i == len - 1;
406            match &stmt.kind {
407                StmtKind::ExprStmt { expr, has_semi } => {
408                    self.compile_expr(expr)?;
409                    if is_last && !has_semi {
410                        // Keep the value as the program result
411                    } else {
412                        self.chunk.emit_op(Op::Pop, stmt.span.line);
413                    }
414                }
415                _ => {
416                    self.compile_stmt(stmt)?;
417                    if is_last {
418                        // Statements produce Unit as the program result
419                        self.chunk.emit_op(Op::Unit, stmt.span.line);
420                    }
421                }
422            }
423            #[cfg(feature = "optimize")]
424            if !is_last && Self::stmt_is_terminal(stmt) {
425                break;
426            }
427        }
428        if program.stmts.is_empty() {
429            self.chunk.emit_op(Op::Unit, 0);
430        }
431        self.chunk.emit_op(Op::Return, 0);
432        #[cfg(feature = "optimize")]
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_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 { .. } => {
551                // Use statements modify global scope at runtime — bail to tree-walk
552                return Err(IonError::runtime(
553                    ion_str!("use statements not yet supported in VM"),
554                    line,
555                    0,
556                ));
557            }
558            StmtKind::WhileLet {
559                label,
560                pattern,
561                expr,
562                body,
563            } => {
564                let loop_start = self.chunk.len();
565                self.loop_stack.push(LoopFrame {
566                    label: label.clone(),
567                    break_jumps: Vec::new(),
568                    continue_target: loop_start,
569                    is_for_loop: false,
570                    scope_depth: self.scope_depth,
571                });
572
573                // Evaluate expression
574                self.compile_expr(expr)?;
575
576                // Test pattern
577                self.chunk.emit_op(Op::Dup, line); // keep value for binding
578                self.compile_pattern_test(pattern, line)?;
579
580                let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
581                self.chunk.emit_op(Op::Pop, line); // pop true
582
583                // Pattern matched — bind and execute body
584                self.begin_scope(line);
585                self.compile_pattern_bind(pattern, line)?;
586                for stmt in body {
587                    self.compile_stmt(stmt)?;
588                    #[cfg(feature = "optimize")]
589                    if Self::stmt_is_terminal(stmt) {
590                        break;
591                    }
592                }
593                self.end_scope(line);
594
595                let offset = self.chunk.len() - loop_start + 3;
596                self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
597
598                self.chunk.patch_jump(exit_jump);
599                self.chunk.emit_op(Op::Pop, line); // pop false
600                self.chunk.emit_op(Op::Pop, line); // pop the duped value
601
602                let frame = self.loop_stack.pop().expect("loop frame");
603                for jump in &frame.break_jumps {
604                    self.chunk.patch_jump(*jump);
605                }
606            }
607        }
608        Ok(())
609    }
610
611    fn compile_expr(&mut self, expr: &Expr) -> Result<(), IonError> {
612        let line = expr.span.line;
613        let col = expr.span.col;
614        // Save tail position — only Call, If, Block, Match, IfLet propagate it
615        let was_tail = self.in_tail_position;
616        self.in_tail_position = false;
617        match &expr.kind {
618            ExprKind::Int(n) => {
619                self.chunk.emit_constant(Value::Int(*n), line);
620            }
621            ExprKind::Float(n) => {
622                self.chunk.emit_constant(Value::Float(*n), line);
623            }
624            ExprKind::Bool(b) => {
625                self.chunk
626                    .emit_op(if *b { Op::True } else { Op::False }, line);
627            }
628            ExprKind::Str(s) => {
629                self.chunk.emit_constant(Value::Str(s.clone()), line);
630            }
631            ExprKind::Bytes(b) => {
632                self.chunk.emit_constant(Value::Bytes(b.clone()), line);
633            }
634            ExprKind::Unit => {
635                self.chunk.emit_op(Op::Unit, line);
636            }
637            ExprKind::None => {
638                self.chunk.emit_op(Op::None, line);
639            }
640            ExprKind::SomeExpr(inner) => {
641                self.compile_expr(inner)?;
642                self.chunk.emit_op(Op::WrapSome, line);
643            }
644            ExprKind::OkExpr(inner) => {
645                self.compile_expr(inner)?;
646                self.chunk.emit_op(Op::WrapOk, line);
647            }
648            ExprKind::ErrExpr(inner) => {
649                self.compile_expr(inner)?;
650                self.chunk.emit_op(Op::WrapErr, line);
651            }
652
653            ExprKind::Ident(name) => {
654                if let Some(slot) = self.resolve_local(name) {
655                    self.chunk.emit_op_u16(Op::GetLocalSlot, slot as u16, line);
656                } else {
657                    let idx = self.chunk.add_constant(Value::Str(name.clone()));
658                    self.chunk.emit_op_u16(Op::GetGlobal, idx, line);
659                }
660            }
661
662            ExprKind::ModulePath(segments) => {
663                // Load root module as global, then chain GetField for each segment
664                let root_idx = self.chunk.add_constant(Value::Str(segments[0].clone()));
665                self.chunk.emit_op_u16(Op::GetGlobal, root_idx, line);
666                for seg in &segments[1..] {
667                    let idx = self.chunk.add_constant(Value::Str(seg.clone()));
668                    self.chunk.emit_op_u16_span(Op::GetField, idx, line, col);
669                }
670            }
671
672            ExprKind::BinOp { left, op, right } => {
673                // Constant folding: evaluate at compile time if both sides are literals
674                #[cfg(feature = "optimize")]
675                let folded = Self::try_fold_binop(left, op, right);
676                #[cfg(not(feature = "optimize"))]
677                let folded: Option<Value> = None;
678                if let Some(val) = folded {
679                    self.chunk.emit_constant(val, line);
680                } else {
681                    match op {
682                        BinOp::And => {
683                            self.compile_expr(left)?;
684                            let jump = self.chunk.emit_jump(Op::And, line);
685                            self.chunk.emit_op(Op::Pop, line);
686                            self.compile_expr(right)?;
687                            self.chunk.patch_jump(jump);
688                        }
689                        BinOp::Or => {
690                            self.compile_expr(left)?;
691                            let jump = self.chunk.emit_jump(Op::Or, line);
692                            self.chunk.emit_op(Op::Pop, line);
693                            self.compile_expr(right)?;
694                            self.chunk.patch_jump(jump);
695                        }
696                        _ => {
697                            self.compile_expr(left)?;
698                            self.compile_expr(right)?;
699                            match op {
700                                BinOp::Add => self.chunk.emit_op_span(Op::Add, line, col),
701                                BinOp::Sub => self.chunk.emit_op_span(Op::Sub, line, col),
702                                BinOp::Mul => self.chunk.emit_op_span(Op::Mul, line, col),
703                                BinOp::Div => self.chunk.emit_op_span(Op::Div, line, col),
704                                BinOp::Mod => self.chunk.emit_op_span(Op::Mod, line, col),
705                                BinOp::Eq => self.chunk.emit_op(Op::Eq, line),
706                                BinOp::Ne => self.chunk.emit_op(Op::NotEq, line),
707                                BinOp::Lt => self.chunk.emit_op(Op::Lt, line),
708                                BinOp::Gt => self.chunk.emit_op(Op::Gt, line),
709                                BinOp::Le => self.chunk.emit_op(Op::LtEq, line),
710                                BinOp::Ge => self.chunk.emit_op(Op::GtEq, line),
711                                BinOp::BitAnd => self.chunk.emit_op(Op::BitAnd, line),
712                                BinOp::BitOr => self.chunk.emit_op(Op::BitOr, line),
713                                BinOp::BitXor => self.chunk.emit_op(Op::BitXor, line),
714                                BinOp::Shl => self.chunk.emit_op(Op::Shl, line),
715                                BinOp::Shr => self.chunk.emit_op(Op::Shr, line),
716                                _ => unreachable!(),
717                            }
718                        }
719                    }
720                }
721            }
722
723            ExprKind::UnaryOp { op, expr: inner } => {
724                #[cfg(feature = "optimize")]
725                let folded = Self::try_fold_unary(op, inner);
726                #[cfg(not(feature = "optimize"))]
727                let folded: Option<Value> = None;
728                if let Some(val) = folded {
729                    self.chunk.emit_constant(val, line);
730                } else {
731                    self.compile_expr(inner)?;
732                    match op {
733                        UnaryOp::Neg => self.chunk.emit_op_span(Op::Neg, line, col),
734                        UnaryOp::Not => self.chunk.emit_op_span(Op::Not, line, col),
735                    }
736                }
737            }
738
739            ExprKind::If {
740                cond,
741                then_body,
742                else_body,
743            } => {
744                // Condition is not in tail position (already cleared)
745                self.compile_expr(cond)?;
746                let then_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
747                self.chunk.emit_op(Op::Pop, line); // pop condition
748                self.begin_scope(line);
749                // Both branches inherit tail position
750                self.in_tail_position = was_tail;
751                self.compile_block_expr(then_body, line)?;
752                self.end_scope(line);
753                let else_jump = self.chunk.emit_jump(Op::Jump, line);
754                self.chunk.patch_jump(then_jump);
755                self.chunk.emit_op(Op::Pop, line); // pop condition
756                if let Some(else_stmts) = else_body {
757                    self.begin_scope(line);
758                    self.in_tail_position = was_tail;
759                    self.compile_block_expr(else_stmts, line)?;
760                    self.end_scope(line);
761                } else {
762                    self.chunk.emit_op(Op::Unit, line);
763                }
764                self.chunk.patch_jump(else_jump);
765            }
766
767            ExprKind::Block(stmts) => {
768                self.begin_scope(line);
769                self.in_tail_position = was_tail;
770                self.compile_block_expr(stmts, line)?;
771                self.end_scope(line);
772            }
773
774            ExprKind::Call { func, args } => {
775                let has_named = args.iter().any(|a| a.name.is_some());
776                // Sub-expressions are not in tail position (already cleared above)
777                self.compile_expr(func)?;
778                for arg in args {
779                    self.compile_expr(&arg.value)?;
780                }
781                if has_named {
782                    // Emit CallNamed: total_args, named_count, then (position, name_idx) pairs
783                    let named: Vec<(u8, u16)> = args
784                        .iter()
785                        .enumerate()
786                        .filter_map(|(i, a)| {
787                            a.name
788                                .as_ref()
789                                .map(|n| (i as u8, self.chunk.add_constant(Value::Str(n.clone()))))
790                        })
791                        .collect();
792                    self.chunk.emit_op(Op::CallNamed, line);
793                    self.chunk.emit(args.len() as u8, line);
794                    self.chunk.emit(named.len() as u8, line);
795                    for (pos, name_idx) in named {
796                        self.chunk.emit(pos, line);
797                        self.chunk.emit((name_idx >> 8) as u8, line);
798                        self.chunk.emit(name_idx as u8, line);
799                    }
800                } else {
801                    #[cfg(feature = "optimize")]
802                    let op = if was_tail { Op::TailCall } else { Op::Call };
803                    #[cfg(not(feature = "optimize"))]
804                    let op = Op::Call;
805                    self.chunk.emit_op_u8_span(op, args.len() as u8, line, col);
806                }
807            }
808
809            ExprKind::List(items) => {
810                let has_spread = items.iter().any(|e| matches!(e, ListEntry::Spread(_)));
811                if has_spread {
812                    // Build empty list, then append/extend entries one by one
813                    self.chunk.emit_op_u16(Op::BuildList, 0, line);
814                    for entry in items {
815                        match entry {
816                            ListEntry::Elem(expr) => {
817                                self.compile_expr(expr)?;
818                                self.chunk.emit_op(Op::ListAppend, line);
819                            }
820                            ListEntry::Spread(expr) => {
821                                self.compile_expr(expr)?;
822                                self.chunk.emit_op(Op::ListExtend, line);
823                            }
824                        }
825                    }
826                } else {
827                    // Fast path: no spreads, use BuildList directly
828                    for entry in items {
829                        if let ListEntry::Elem(expr) = entry {
830                            self.compile_expr(expr)?;
831                        }
832                    }
833                    self.chunk
834                        .emit_op_u16(Op::BuildList, items.len() as u16, line);
835                }
836            }
837
838            ExprKind::Tuple(items) => {
839                for item in items {
840                    self.compile_expr(item)?;
841                }
842                self.chunk
843                    .emit_op_u16(Op::BuildTuple, items.len() as u16, line);
844            }
845
846            ExprKind::Dict(entries) => {
847                let has_spread = entries.iter().any(|e| matches!(e, DictEntry::Spread(_)));
848                if has_spread {
849                    // Build empty dict, then insert/merge entries one by one
850                    self.chunk.emit_op_u16(Op::BuildDict, 0, line);
851                    for entry in entries {
852                        match entry {
853                            DictEntry::KeyValue(k, v) => {
854                                self.compile_expr(k)?;
855                                self.compile_expr(v)?;
856                                self.chunk.emit_op(Op::DictInsert, line);
857                            }
858                            DictEntry::Spread(expr) => {
859                                self.compile_expr(expr)?;
860                                self.chunk.emit_op(Op::DictMerge, line);
861                            }
862                        }
863                    }
864                } else {
865                    // Fast path: no spreads, use BuildDict directly
866                    let count = entries.len() as u16;
867                    for entry in entries {
868                        if let DictEntry::KeyValue(k, v) = entry {
869                            self.compile_expr(k)?;
870                            self.compile_expr(v)?;
871                        }
872                    }
873                    self.chunk.emit_op_u16(Op::BuildDict, count, line);
874                }
875            }
876
877            ExprKind::FieldAccess { expr: inner, field } => {
878                self.compile_expr(inner)?;
879                let idx = self.chunk.add_constant(Value::Str(field.clone()));
880                self.chunk.emit_op_u16_span(Op::GetField, idx, line, col);
881            }
882
883            ExprKind::Index { expr: inner, index } => {
884                self.compile_expr(inner)?;
885                self.compile_expr(index)?;
886                self.chunk.emit_op_span(Op::GetIndex, line, col);
887            }
888
889            ExprKind::Slice {
890                expr: inner,
891                start,
892                end,
893                inclusive,
894            } => {
895                self.compile_expr(inner)?;
896                let mut flags: u8 = 0;
897                if let Some(s) = start {
898                    self.compile_expr(s)?;
899                    flags |= 1; // has_start
900                }
901                if let Some(e) = end {
902                    self.compile_expr(e)?;
903                    flags |= 2; // has_end
904                }
905                if *inclusive {
906                    flags |= 4; // inclusive
907                }
908                self.chunk.emit_op_u8(Op::Slice, flags, line);
909            }
910
911            ExprKind::MethodCall {
912                expr: inner,
913                method,
914                args,
915            } => {
916                self.compile_expr(inner)?;
917                for arg in args {
918                    self.compile_expr(&arg.value)?;
919                }
920                let idx = self.chunk.add_constant(Value::Str(method.clone()));
921                self.chunk.emit_op_u16_span(Op::MethodCall, idx, line, col);
922                self.chunk.emit_span(args.len() as u8, line, col);
923            }
924
925            ExprKind::Lambda { params, body } => {
926                // Build lambda body as a single expression statement for tree-walk fallback
927                let body_stmt = Stmt {
928                    kind: StmtKind::ExprStmt {
929                        expr: *body.clone(),
930                        has_semi: false,
931                    },
932                    span: expr.span,
933                };
934                // Precompile lambda body
935                let mut fn_compiler = Compiler::new();
936                fn_compiler.in_tail_position = true;
937                fn_compiler.needs_env_locals = Self::expr_has_closures(body);
938                // Pre-register parameters as locals
939                for p in params {
940                    fn_compiler.add_local(p.clone(), false);
941                }
942                // When closures exist, also define params in env so they can be captured
943                if fn_compiler.needs_env_locals {
944                    for (i, p) in params.iter().enumerate() {
945                        fn_compiler
946                            .chunk
947                            .emit_op_u16(Op::GetLocalSlot, i as u16, line);
948                        let idx = fn_compiler.chunk.add_constant(Value::Str(p.clone()));
949                        fn_compiler.chunk.emit_op_u16(Op::DefineLocal, idx, line);
950                        fn_compiler.chunk.emit(0, line);
951                    }
952                }
953                fn_compiler.compile_expr(body)?;
954                fn_compiler.chunk.emit_op(Op::Return, line);
955                #[cfg(feature = "optimize")]
956                fn_compiler.chunk.peephole_optimize();
957                let compiled_chunk = fn_compiler.chunk;
958                self.fn_chunks.extend(fn_compiler.fn_chunks);
959
960                let fn_value = Value::Fn(crate::value::IonFn::new(
961                    "<lambda>".to_string(),
962                    params
963                        .iter()
964                        .map(|n| crate::ast::Param {
965                            name: n.clone(),
966                            default: None,
967                        })
968                        .collect(),
969                    vec![body_stmt],
970                    std::collections::HashMap::new(),
971                ));
972                // Associate precompiled chunk with fn_id
973                if let Value::Fn(ref ion_fn) = fn_value {
974                    self.fn_chunks.insert(ion_fn.fn_id, compiled_chunk);
975                }
976                let fn_idx = self.chunk.add_constant(fn_value);
977                self.chunk.emit_op_u16(Op::Closure, fn_idx, line);
978            }
979
980            ExprKind::FStr(parts) => {
981                for part in parts {
982                    match part {
983                        FStrPart::Literal(s) => {
984                            self.chunk.emit_constant(Value::Str(s.clone()), line);
985                        }
986                        FStrPart::Expr(expr) => {
987                            self.compile_expr(expr)?;
988                        }
989                    }
990                }
991                self.chunk
992                    .emit_op_u16(Op::BuildFString, parts.len() as u16, line);
993            }
994
995            ExprKind::PipeOp { left, right } => {
996                // Desugar: left |> right(args)  →  right(left, args)
997                // Compile func first, then piped value as first arg, then other args
998                match &right.kind {
999                    ExprKind::Call { func, args } => {
1000                        self.compile_expr(func)?;
1001                        self.compile_expr(left)?; // piped value = first arg
1002                        for arg in args {
1003                            self.compile_expr(&arg.value)?;
1004                        }
1005                        self.chunk
1006                            .emit_op_u8(Op::Call, (args.len() + 1) as u8, line);
1007                    }
1008                    _ => {
1009                        // bare function: left |> func  →  func(left)
1010                        self.compile_expr(right)?;
1011                        self.compile_expr(left)?;
1012                        self.chunk.emit_op_u8(Op::Call, 1, line);
1013                    }
1014                }
1015            }
1016
1017            ExprKind::Try(inner) => {
1018                self.compile_expr(inner)?;
1019                self.chunk.emit_op(Op::Try, line);
1020            }
1021
1022            ExprKind::Range {
1023                start,
1024                end,
1025                inclusive,
1026            } => {
1027                self.compile_expr(start)?;
1028                self.compile_expr(end)?;
1029                self.chunk
1030                    .emit_op_u8(Op::BuildRange, if *inclusive { 1 } else { 0 }, line);
1031            }
1032
1033            ExprKind::LoopExpr(body) => {
1034                self.compile_loop(None, body, line)?;
1035            }
1036
1037            ExprKind::Match {
1038                expr: subject,
1039                arms,
1040            } => {
1041                self.compile_match(subject, arms, line)?;
1042            }
1043
1044            ExprKind::ListComp {
1045                expr: item_expr,
1046                pattern,
1047                iter,
1048                cond,
1049            } => {
1050                self.compile_list_comp(item_expr, pattern, iter, cond.as_deref(), line)?;
1051            }
1052
1053            ExprKind::DictComp {
1054                key,
1055                value,
1056                pattern,
1057                iter,
1058                cond,
1059            } => {
1060                self.compile_dict_comp(key, value, pattern, iter, cond.as_deref(), line)?;
1061            }
1062
1063            ExprKind::IfLet {
1064                pattern,
1065                expr: inner,
1066                then_body,
1067                else_body,
1068            } => {
1069                // Evaluate the expression (not in tail position — already cleared)
1070                self.compile_expr(inner)?;
1071
1072                // Test pattern
1073                self.chunk.emit_op(Op::Dup, line); // keep value for binding
1074                self.compile_pattern_test(pattern, line)?;
1075
1076                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1077                self.chunk.emit_op(Op::Pop, line); // pop true
1078
1079                // Pattern matched — bind variables in new scope
1080                self.begin_scope(line);
1081                self.compile_pattern_bind(pattern, line)?;
1082                self.in_tail_position = was_tail;
1083                self.compile_block_expr(then_body, line)?;
1084                self.end_scope(line);
1085
1086                let end_jump = self.chunk.emit_jump(Op::Jump, line);
1087
1088                self.chunk.patch_jump(else_jump);
1089                self.chunk.emit_op(Op::Pop, line); // pop false
1090                self.chunk.emit_op(Op::Pop, line); // pop the duped value
1091
1092                if let Some(else_stmts) = else_body {
1093                    self.begin_scope(line);
1094                    self.in_tail_position = was_tail;
1095                    self.compile_block_expr(else_stmts, line)?;
1096                    self.end_scope(line);
1097                } else {
1098                    self.chunk.emit_op(Op::Unit, line);
1099                }
1100
1101                self.chunk.patch_jump(end_jump);
1102            }
1103
1104            // Features that fall back to tree-walk for now
1105            ExprKind::StructConstruct {
1106                name,
1107                fields,
1108                spread,
1109            } => {
1110                if let Some(spread_expr) = spread {
1111                    self.compile_expr(spread_expr)?;
1112                    for (fname, fexpr) in fields {
1113                        self.chunk.emit_constant(Value::Str(fname.clone()), line);
1114                        self.compile_expr(fexpr)?;
1115                    }
1116                    let type_idx = self.chunk.add_constant(Value::Str(name.clone()));
1117                    let field_count = (0x8000 | fields.len()) as u16;
1118                    self.chunk.emit_op(Op::ConstructStruct, line);
1119                    self.chunk.emit((type_idx >> 8) as u8, line);
1120                    self.chunk.emit((type_idx & 0xff) as u8, line);
1121                    self.chunk.emit((field_count >> 8) as u8, line);
1122                    self.chunk.emit((field_count & 0xff) as u8, line);
1123                } else {
1124                    for (fname, fexpr) in fields {
1125                        self.chunk.emit_constant(Value::Str(fname.clone()), line);
1126                        self.compile_expr(fexpr)?;
1127                    }
1128                    let type_idx = self.chunk.add_constant(Value::Str(name.clone()));
1129                    let count = fields.len() as u16;
1130                    self.chunk.emit_op(Op::ConstructStruct, line);
1131                    self.chunk.emit((type_idx >> 8) as u8, line);
1132                    self.chunk.emit((type_idx & 0xff) as u8, line);
1133                    self.chunk.emit((count >> 8) as u8, line);
1134                    self.chunk.emit((count & 0xff) as u8, line);
1135                }
1136            }
1137            ExprKind::EnumVariant { enum_name, variant } => {
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(0u8, line);
1146            }
1147            ExprKind::EnumVariantCall {
1148                enum_name,
1149                variant,
1150                args,
1151            } => {
1152                for arg in args {
1153                    self.compile_expr(arg)?;
1154                }
1155                let enum_idx = self.chunk.add_constant(Value::Str(enum_name.clone()));
1156                let variant_idx = self.chunk.add_constant(Value::Str(variant.clone()));
1157                self.chunk.emit_op(Op::ConstructEnum, line);
1158                self.chunk.emit((enum_idx >> 8) as u8, line);
1159                self.chunk.emit((enum_idx & 0xff) as u8, line);
1160                self.chunk.emit((variant_idx >> 8) as u8, line);
1161                self.chunk.emit((variant_idx & 0xff) as u8, line);
1162                self.chunk.emit(args.len() as u8, line);
1163            }
1164
1165            #[cfg(feature = "concurrency")]
1166            ExprKind::AsyncBlock(_)
1167            | ExprKind::SpawnExpr(_)
1168            | ExprKind::AwaitExpr(_)
1169            | ExprKind::SelectExpr(_) => {
1170                return Err(IonError::runtime(
1171                    ion_str!("concurrency not supported in bytecode VM").to_string(),
1172                    line,
1173                    col,
1174                ));
1175            }
1176            #[cfg(not(feature = "concurrency"))]
1177            ExprKind::AsyncBlock(_)
1178            | ExprKind::SpawnExpr(_)
1179            | ExprKind::AwaitExpr(_)
1180            | ExprKind::SelectExpr(_) => {
1181                return Err(IonError::runtime(
1182                    ion_str!("concurrency not available").to_string(),
1183                    line,
1184                    col,
1185                ));
1186            }
1187
1188            ExprKind::TryCatch { body, var, handler } => {
1189                // TryBegin catch_offset  (jump to catch block on error)
1190                let try_begin_patch = self.chunk.emit_jump(Op::TryBegin, line);
1191
1192                // Compile try body
1193                self.begin_scope(line);
1194                let old_tail = self.in_tail_position;
1195                self.in_tail_position = false;
1196                for (i, stmt) in body.iter().enumerate() {
1197                    if i == body.len() - 1 {
1198                        if let crate::ast::StmtKind::ExprStmt { expr, .. } = &stmt.kind {
1199                            self.compile_expr(expr)?;
1200                        } else {
1201                            self.compile_stmt(stmt)?;
1202                            self.chunk.emit_op(Op::Unit, line);
1203                        }
1204                    } else {
1205                        self.compile_stmt(stmt)?;
1206                    }
1207                }
1208                if body.is_empty() {
1209                    self.chunk.emit_op(Op::Unit, line);
1210                }
1211                self.in_tail_position = old_tail;
1212                self.end_scope(line);
1213
1214                // TryEnd jump_offset  (no error: pop handler, jump over catch)
1215                let try_end_patch = self.chunk.emit_jump(Op::TryEnd, line);
1216
1217                // Patch TryBegin to point here (catch block start)
1218                self.chunk.patch_jump(try_begin_patch);
1219
1220                // Catch block: error message string is on stack
1221                self.begin_scope(line);
1222                self.emit_define_local(var, false, line);
1223                for (i, stmt) in handler.iter().enumerate() {
1224                    if i == handler.len() - 1 {
1225                        if let crate::ast::StmtKind::ExprStmt { expr, .. } = &stmt.kind {
1226                            self.compile_expr(expr)?;
1227                        } else {
1228                            self.compile_stmt(stmt)?;
1229                            self.chunk.emit_op(Op::Unit, line);
1230                        }
1231                    } else {
1232                        self.compile_stmt(stmt)?;
1233                    }
1234                }
1235                if handler.is_empty() {
1236                    self.chunk.emit_op(Op::Unit, line);
1237                }
1238                self.end_scope(line);
1239
1240                // Patch TryEnd jump to skip catch block
1241                self.chunk.patch_jump(try_end_patch);
1242            }
1243        }
1244        self.in_tail_position = was_tail;
1245        Ok(())
1246    }
1247
1248    fn compile_block_expr(&mut self, stmts: &[Stmt], line: usize) -> Result<(), IonError> {
1249        if stmts.is_empty() {
1250            self.chunk.emit_op(Op::Unit, line);
1251            return Ok(());
1252        }
1253        let len = stmts.len();
1254        let saved_tail = self.in_tail_position;
1255        for (i, stmt) in stmts.iter().enumerate() {
1256            let is_last = i == len - 1;
1257            // Only the last expression (without semicolon) inherits tail position
1258            if !is_last {
1259                self.in_tail_position = false;
1260            } else {
1261                self.in_tail_position = saved_tail;
1262            }
1263            match &stmt.kind {
1264                StmtKind::ExprStmt { expr, has_semi } => {
1265                    if is_last && *has_semi {
1266                        self.in_tail_position = false;
1267                    }
1268                    self.compile_expr(expr)?;
1269                    if is_last && !has_semi {
1270                        // Keep value
1271                    } else {
1272                        self.chunk.emit_op(Op::Pop, stmt.span.line);
1273                    }
1274                }
1275                _ => {
1276                    self.in_tail_position = false;
1277                    self.compile_stmt(stmt)?;
1278                    if is_last {
1279                        self.chunk.emit_op(Op::Unit, stmt.span.line);
1280                    }
1281                }
1282            }
1283            // Dead code elimination: skip remaining statements after terminal
1284            #[cfg(feature = "optimize")]
1285            if !is_last && Self::stmt_is_terminal(stmt) {
1286                break;
1287            }
1288        }
1289        self.in_tail_position = saved_tail;
1290        Ok(())
1291    }
1292
1293    fn type_ann_to_string(ann: &TypeAnn) -> String {
1294        match ann {
1295            TypeAnn::Simple(name) => name.clone(),
1296            TypeAnn::Option(inner) => format!("Option<{}>", Self::type_ann_to_string(inner)),
1297            TypeAnn::Result(ok, err) => format!(
1298                "Result<{}, {}>",
1299                Self::type_ann_to_string(ok),
1300                Self::type_ann_to_string(err)
1301            ),
1302            TypeAnn::List(inner) => format!("list<{}>", Self::type_ann_to_string(inner)),
1303            TypeAnn::Dict(k, v) => format!(
1304                "dict<{}, {}>",
1305                Self::type_ann_to_string(k),
1306                Self::type_ann_to_string(v)
1307            ),
1308        }
1309    }
1310
1311    fn compile_let_pattern(
1312        &mut self,
1313        pattern: &Pattern,
1314        mutable: bool,
1315        line: usize,
1316    ) -> Result<(), IonError> {
1317        match pattern {
1318            Pattern::Ident(name) => {
1319                self.emit_define_local(name, mutable, line);
1320            }
1321            Pattern::Tuple(pats) => {
1322                // Value is on stack. Destructure it.
1323                for (i, pat) in pats.iter().enumerate() {
1324                    self.chunk.emit_op(Op::Dup, line);
1325                    self.chunk.emit_constant(Value::Int(i as i64), line);
1326                    self.chunk.emit_op(Op::GetIndex, line);
1327                    self.compile_let_pattern(pat, mutable, line)?;
1328                }
1329                self.chunk.emit_op(Op::Pop, line); // pop the original tuple
1330            }
1331            Pattern::List(pats, rest) => {
1332                for (i, pat) in pats.iter().enumerate() {
1333                    self.chunk.emit_op(Op::Dup, line);
1334                    self.chunk.emit_constant(Value::Int(i as i64), line);
1335                    self.chunk.emit_op(Op::GetIndex, line);
1336                    self.compile_let_pattern(pat, mutable, line)?;
1337                }
1338                if let Some(rest_pat) = rest {
1339                    self.chunk.emit_op(Op::Dup, line);
1340                    self.chunk
1341                        .emit_constant(Value::Int(pats.len() as i64), line);
1342                    self.chunk.emit_op_u8(Op::Slice, 1, line); // has_start only
1343                    self.compile_let_pattern(rest_pat, mutable, line)?;
1344                }
1345                self.chunk.emit_op(Op::Pop, line);
1346            }
1347            Pattern::Wildcard => {
1348                self.chunk.emit_op(Op::Pop, line);
1349            }
1350            _ => {
1351                return Err(IonError::runtime(
1352                    ion_str!("complex pattern not yet supported in bytecode VM let").to_string(),
1353                    line,
1354                    0,
1355                ));
1356            }
1357        }
1358        Ok(())
1359    }
1360
1361    fn compile_fn_decl(
1362        &mut self,
1363        name: &str,
1364        params: &[Param],
1365        body: &[Stmt],
1366        line: usize,
1367    ) -> Result<(), IonError> {
1368        // Compile function body into a separate chunk
1369        let mut fn_compiler = Compiler::new();
1370        fn_compiler.in_tail_position = true;
1371        // Only dual-define locals if body contains closures
1372        fn_compiler.needs_env_locals = Self::stmts_have_closures(body);
1373        // Pre-register parameters as locals (they'll be pushed by the VM)
1374        for param in params {
1375            fn_compiler.add_local(param.name.clone(), false);
1376        }
1377        // When closures exist, also define params in env so they can be captured
1378        if fn_compiler.needs_env_locals {
1379            for (i, param) in params.iter().enumerate() {
1380                fn_compiler
1381                    .chunk
1382                    .emit_op_u16(Op::GetLocalSlot, i as u16, line);
1383                let idx = fn_compiler
1384                    .chunk
1385                    .add_constant(Value::Str(param.name.clone()));
1386                fn_compiler.chunk.emit_op_u16(Op::DefineLocal, idx, line);
1387                fn_compiler.chunk.emit(0, line); // not mutable
1388            }
1389        }
1390        fn_compiler.compile_block_expr(body, line)?;
1391        fn_compiler.chunk.emit_op(Op::Return, line);
1392        #[cfg(feature = "optimize")]
1393        fn_compiler.chunk.peephole_optimize();
1394        let compiled_chunk = fn_compiler.chunk;
1395        // Collect any nested function chunks
1396        self.fn_chunks.extend(fn_compiler.fn_chunks);
1397
1398        let fn_value = Value::Fn(crate::value::IonFn::new(
1399            name.to_string(),
1400            params.to_vec(),
1401            body.to_vec(), // Keep AST body for tree-walk fallback
1402            std::collections::HashMap::new(),
1403        ));
1404        // Extract fn_id to associate with precompiled chunk
1405        if let Value::Fn(ref ion_fn) = fn_value {
1406            self.fn_chunks.insert(ion_fn.fn_id, compiled_chunk);
1407        }
1408
1409        // Define the function in the current scope
1410        self.chunk.emit_constant(fn_value, line);
1411        self.emit_define_local(name, false, line);
1412        Ok(())
1413    }
1414
1415    fn compile_for(
1416        &mut self,
1417        label: Option<String>,
1418        pattern: &Pattern,
1419        iter: &Expr,
1420        body: &[Stmt],
1421        line: usize,
1422    ) -> Result<(), IonError> {
1423        // Evaluate the iterator expression
1424        self.compile_expr(iter)?;
1425
1426        // Convert to iterable (the VM will handle this)
1427        self.chunk.emit_op(Op::IterInit, line);
1428
1429        let loop_start = self.chunk.len();
1430        self.loop_stack.push(LoopFrame {
1431            label,
1432            break_jumps: Vec::new(),
1433            continue_target: loop_start,
1434            is_for_loop: true,
1435            scope_depth: self.scope_depth,
1436        });
1437
1438        // Get next item or jump to end
1439        let exit_jump = self.chunk.emit_jump(Op::IterNext, line);
1440
1441        // Bind pattern
1442        self.begin_scope(line);
1443        self.compile_let_pattern(pattern, false, line)?;
1444
1445        // Execute body (with dead code elimination)
1446        for stmt in body {
1447            self.compile_stmt(stmt)?;
1448            #[cfg(feature = "optimize")]
1449            if Self::stmt_is_terminal(stmt) {
1450                break;
1451            }
1452        }
1453        self.end_scope(line);
1454
1455        // Push placeholder for IterNext to pop on next iteration
1456        self.chunk.emit_op(Op::Unit, line);
1457
1458        // Loop back
1459        let offset = self.chunk.len() - loop_start + 3;
1460        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
1461
1462        self.chunk.patch_jump(exit_jump);
1463        // Pop the iterator placeholder
1464        self.chunk.emit_op(Op::Pop, line);
1465
1466        let frame = self.loop_stack.pop().expect("loop frame");
1467        for jump in &frame.break_jumps {
1468            self.chunk.patch_jump(*jump);
1469        }
1470        Ok(())
1471    }
1472
1473    fn compile_while(
1474        &mut self,
1475        label: Option<String>,
1476        cond: &Expr,
1477        body: &[Stmt],
1478        line: usize,
1479    ) -> Result<(), IonError> {
1480        let loop_start = self.chunk.len();
1481        self.loop_stack.push(LoopFrame {
1482            label,
1483            break_jumps: Vec::new(),
1484            continue_target: loop_start,
1485            is_for_loop: false,
1486            scope_depth: self.scope_depth,
1487        });
1488
1489        self.compile_expr(cond)?;
1490        let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1491        self.chunk.emit_op(Op::Pop, line); // pop condition
1492
1493        self.begin_scope(line);
1494        for stmt in body {
1495            self.compile_stmt(stmt)?;
1496            #[cfg(feature = "optimize")]
1497            if Self::stmt_is_terminal(stmt) {
1498                break;
1499            }
1500        }
1501        self.end_scope(line);
1502
1503        let offset = self.chunk.len() - loop_start + 3;
1504        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
1505
1506        self.chunk.patch_jump(exit_jump);
1507        self.chunk.emit_op(Op::Pop, line); // pop condition
1508
1509        let frame = self.loop_stack.pop().expect("loop frame");
1510        for jump in &frame.break_jumps {
1511            self.chunk.patch_jump(*jump);
1512        }
1513        Ok(())
1514    }
1515
1516    fn compile_loop(
1517        &mut self,
1518        label: Option<String>,
1519        body: &[Stmt],
1520        line: usize,
1521    ) -> Result<(), IonError> {
1522        let loop_start = self.chunk.len();
1523        self.loop_stack.push(LoopFrame {
1524            label,
1525            break_jumps: Vec::new(),
1526            continue_target: loop_start,
1527            is_for_loop: false,
1528            scope_depth: self.scope_depth,
1529        });
1530
1531        self.begin_scope(line);
1532        for stmt in body {
1533            self.compile_stmt(stmt)?;
1534            #[cfg(feature = "optimize")]
1535            if Self::stmt_is_terminal(stmt) {
1536                break;
1537            }
1538        }
1539        self.end_scope(line);
1540
1541        let offset = self.chunk.len() - loop_start + 3;
1542        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
1543
1544        let frame = self.loop_stack.pop().expect("loop frame");
1545        for jump in &frame.break_jumps {
1546            self.chunk.patch_jump(*jump);
1547        }
1548        Ok(())
1549    }
1550
1551    fn compile_assign(
1552        &mut self,
1553        target: &AssignTarget,
1554        op: &AssignOp,
1555        value: &Expr,
1556        line: usize,
1557    ) -> Result<(), IonError> {
1558        match target {
1559            AssignTarget::Ident(name) => {
1560                match op {
1561                    AssignOp::Eq => {
1562                        self.compile_expr(value)?;
1563                    }
1564                    AssignOp::PlusEq | AssignOp::MinusEq | AssignOp::StarEq | AssignOp::SlashEq => {
1565                        self.emit_get_var(name, line);
1566                        self.compile_expr(value)?;
1567                        match op {
1568                            AssignOp::PlusEq => self.chunk.emit_op(Op::Add, line),
1569                            AssignOp::MinusEq => self.chunk.emit_op(Op::Sub, line),
1570                            AssignOp::StarEq => self.chunk.emit_op(Op::Mul, line),
1571                            AssignOp::SlashEq => self.chunk.emit_op(Op::Div, line),
1572                            _ => unreachable!(),
1573                        }
1574                    }
1575                }
1576                self.emit_set_var(name, line);
1577            }
1578            AssignTarget::Index(obj_expr, index_expr) => {
1579                // For index assignment, we need to:
1580                // 1. Get the container, 2. Modify it, 3. Write it back
1581                // This only works when obj_expr is an Ident (variable)
1582                let var_name = match &obj_expr.kind {
1583                    ExprKind::Ident(name) => name.clone(),
1584                    _ => {
1585                        return Err(IonError::runtime(
1586                            ion_str!("index assignment only supported on variables").to_string(),
1587                            line,
1588                            0,
1589                        ))
1590                    }
1591                };
1592
1593                // Get the container
1594                self.compile_expr(obj_expr)?;
1595                self.compile_expr(index_expr)?;
1596
1597                // Compute new value
1598                match op {
1599                    AssignOp::Eq => {
1600                        self.compile_expr(value)?;
1601                    }
1602                    _ => {
1603                        // Get old value for compound assignment
1604                        self.compile_expr(obj_expr)?;
1605                        self.compile_expr(index_expr)?;
1606                        self.chunk.emit_op(Op::GetIndex, line);
1607                        self.compile_expr(value)?;
1608                        match op {
1609                            AssignOp::PlusEq => self.chunk.emit_op(Op::Add, line),
1610                            AssignOp::MinusEq => self.chunk.emit_op(Op::Sub, line),
1611                            AssignOp::StarEq => self.chunk.emit_op(Op::Mul, line),
1612                            AssignOp::SlashEq => self.chunk.emit_op(Op::Div, line),
1613                            _ => unreachable!(),
1614                        }
1615                    }
1616                }
1617
1618                // Stack: [..., obj, index, new_value]
1619                self.chunk.emit_op(Op::SetIndex, line);
1620                // SetIndex returns the modified container — write it back
1621                self.emit_set_var(&var_name, line);
1622            }
1623            AssignTarget::Field(obj_expr, field) => {
1624                let var_name = match &obj_expr.kind {
1625                    ExprKind::Ident(name) => name.clone(),
1626                    _ => {
1627                        return Err(IonError::runtime(
1628                            ion_str!("field assignment only supported on variables").to_string(),
1629                            line,
1630                            0,
1631                        ))
1632                    }
1633                };
1634
1635                self.compile_expr(obj_expr)?;
1636
1637                match op {
1638                    AssignOp::Eq => {
1639                        self.compile_expr(value)?;
1640                    }
1641                    _ => {
1642                        self.chunk.emit_op(Op::Dup, line);
1643                        let get_idx = self.chunk.add_constant(Value::Str(field.clone()));
1644                        self.chunk.emit_op_u16(Op::GetField, get_idx, line);
1645                        self.compile_expr(value)?;
1646                        match op {
1647                            AssignOp::PlusEq => self.chunk.emit_op(Op::Add, line),
1648                            AssignOp::MinusEq => self.chunk.emit_op(Op::Sub, line),
1649                            AssignOp::StarEq => self.chunk.emit_op(Op::Mul, line),
1650                            AssignOp::SlashEq => self.chunk.emit_op(Op::Div, line),
1651                            _ => unreachable!(),
1652                        }
1653                    }
1654                }
1655
1656                // Stack: [..., obj, new_value]
1657                let field_idx = self.chunk.add_constant(Value::Str(field.clone()));
1658                self.chunk.emit_op_u16(Op::SetField, field_idx, line);
1659                // SetField returns the modified container — write it back
1660                self.emit_set_var(&var_name, line);
1661            }
1662        }
1663        Ok(())
1664    }
1665
1666    /// Compile a function body to a standalone chunk (for VM-native function execution).
1667    pub fn compile_fn_body(
1668        mut self,
1669        params: &[Param],
1670        body: &[Stmt],
1671        line: usize,
1672    ) -> Result<Chunk, IonError> {
1673        self.in_tail_position = true;
1674        self.needs_env_locals = Self::stmts_have_closures(body);
1675        // Pre-register parameters as locals
1676        for param in params {
1677            self.add_local(param.name.clone(), false);
1678        }
1679        // When closures exist, also define params in env so they can be captured
1680        if self.needs_env_locals {
1681            for (i, param) in params.iter().enumerate() {
1682                self.chunk.emit_op_u16(Op::GetLocalSlot, i as u16, line);
1683                let idx = self.chunk.add_constant(Value::Str(param.name.clone()));
1684                self.chunk.emit_op_u16(Op::DefineLocal, idx, line);
1685                self.chunk.emit(0, line); // not mutable
1686            }
1687        }
1688        self.compile_block_expr(body, line)?;
1689        self.chunk.emit_op(Op::Return, line);
1690        Ok(self.chunk)
1691    }
1692
1693    fn compile_match(
1694        &mut self,
1695        subject: &Expr,
1696        arms: &[MatchArm],
1697        line: usize,
1698    ) -> Result<(), IonError> {
1699        let was_tail = self.in_tail_position;
1700        // Store subject in a hidden temp variable (not in tail position)
1701        self.begin_scope(line);
1702        self.in_tail_position = false;
1703        self.compile_expr(subject)?;
1704        let tmp_name = "__match_subject__";
1705        self.emit_define_local(tmp_name, false, line);
1706        let subject_slot = self.locals.len() - 1;
1707
1708        let mut end_jumps = Vec::new();
1709
1710        for arm in arms {
1711            // Load subject for pattern test
1712            self.chunk
1713                .emit_op_u16(Op::GetLocalSlot, subject_slot as u16, line);
1714
1715            // Emit pattern test — consumes subject copy, pushes bool
1716            self.compile_pattern_test(&arm.pattern, line)?;
1717
1718            // If guard exists, test it too (only if pattern matched)
1719            if let Some(guard) = &arm.guard {
1720                let skip_guard = self.chunk.emit_jump(Op::JumpIfFalse, line);
1721                self.chunk.emit_op(Op::Pop, line); // pop true
1722                self.compile_expr(guard)?;
1723                let after_guard = self.chunk.emit_jump(Op::Jump, line);
1724                self.chunk.patch_jump(skip_guard);
1725                // false stays on stack — jump lands here
1726                self.chunk.patch_jump(after_guard);
1727            }
1728
1729            let next_arm = self.chunk.emit_jump(Op::JumpIfFalse, line);
1730            self.chunk.emit_op(Op::Pop, line); // pop true
1731
1732            // Bind pattern variables in new scope
1733            self.begin_scope(line);
1734            self.chunk
1735                .emit_op_u16(Op::GetLocalSlot, subject_slot as u16, line);
1736            self.compile_pattern_bind(&arm.pattern, line)?;
1737
1738            // Compile arm body — inherits tail position
1739            self.in_tail_position = was_tail;
1740            self.compile_expr(&arm.body)?;
1741            self.end_scope(line);
1742
1743            end_jumps.push(self.chunk.emit_jump(Op::Jump, line));
1744
1745            self.chunk.patch_jump(next_arm);
1746            self.chunk.emit_op(Op::Pop, line); // pop false
1747        }
1748
1749        // No arm matched — runtime error (matches interpreter behavior)
1750        self.chunk.emit_op(Op::MatchEnd, line);
1751
1752        for j in end_jumps {
1753            self.chunk.patch_jump(j);
1754        }
1755
1756        self.end_scope(line); // pop the match subject scope
1757        Ok(())
1758    }
1759
1760    /// Compile a pattern test: consumes the value on stack, pushes bool.
1761    fn compile_pattern_test(&mut self, pattern: &Pattern, line: usize) -> Result<(), IonError> {
1762        match pattern {
1763            Pattern::Wildcard | Pattern::Ident(_) => {
1764                self.chunk.emit_op(Op::Pop, line); // consume value
1765                self.chunk.emit_op(Op::True, line); // always matches
1766            }
1767            Pattern::Int(n) => {
1768                self.chunk.emit_constant(Value::Int(*n), line);
1769                self.chunk.emit_op(Op::Eq, line);
1770            }
1771            Pattern::Float(n) => {
1772                self.chunk.emit_constant(Value::Float(*n), line);
1773                self.chunk.emit_op(Op::Eq, line);
1774            }
1775            Pattern::Bool(b) => {
1776                self.chunk
1777                    .emit_op(if *b { Op::True } else { Op::False }, line);
1778                self.chunk.emit_op(Op::Eq, line);
1779            }
1780            Pattern::Str(s) => {
1781                self.chunk.emit_constant(Value::Str(s.clone()), line);
1782                self.chunk.emit_op(Op::Eq, line);
1783            }
1784            Pattern::Bytes(b) => {
1785                self.chunk.emit_constant(Value::Bytes(b.clone()), line);
1786                self.chunk.emit_op(Op::Eq, line);
1787            }
1788            Pattern::None => {
1789                // Check if value is Option(None)
1790                self.chunk.emit_op(Op::None, line);
1791                self.chunk.emit_op(Op::Eq, line);
1792            }
1793            Pattern::Some(inner) => {
1794                // Test: is it Some(x)? Use MatchArm opcode for complex patterns
1795                // For now, test structurally: use a simpler encoding
1796                // We'll use the MatchBegin/MatchArm opcodes repurposed:
1797                // Actually, let's just emit inline checks.
1798                // Stack has value. We need to check if it's Some(_) and test inner.
1799                self.chunk.emit_op_u8(Op::MatchBegin, 1, line); // 1 = test Some
1800                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1801                self.chunk.emit_op(Op::Pop, line); // pop true
1802                                                   // Now unwrap the Some and test inner pattern
1803                self.chunk.emit_op_u8(Op::MatchArm, 1, line); // 1 = unwrap Some
1804                self.compile_pattern_test(inner, line)?;
1805                let end = self.chunk.emit_jump(Op::Jump, line);
1806                self.chunk.patch_jump(fail_jump);
1807                // false stays
1808                self.chunk.patch_jump(end);
1809            }
1810            Pattern::Ok(inner) => {
1811                self.chunk.emit_op_u8(Op::MatchBegin, 2, line); // 2 = test Ok
1812                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1813                self.chunk.emit_op(Op::Pop, line);
1814                self.chunk.emit_op_u8(Op::MatchArm, 2, line); // 2 = unwrap Ok
1815                self.compile_pattern_test(inner, line)?;
1816                let end = self.chunk.emit_jump(Op::Jump, line);
1817                self.chunk.patch_jump(fail_jump);
1818                self.chunk.patch_jump(end);
1819            }
1820            Pattern::Err(inner) => {
1821                self.chunk.emit_op_u8(Op::MatchBegin, 3, line); // 3 = test Err
1822                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1823                self.chunk.emit_op(Op::Pop, line);
1824                self.chunk.emit_op_u8(Op::MatchArm, 3, line); // 3 = unwrap Err
1825                self.compile_pattern_test(inner, line)?;
1826                let end = self.chunk.emit_jump(Op::Jump, line);
1827                self.chunk.patch_jump(fail_jump);
1828                self.chunk.patch_jump(end);
1829            }
1830            Pattern::Tuple(pats) => {
1831                // Check: is it a tuple of the right length, and do all sub-patterns match?
1832                self.chunk.emit_op_u8(Op::MatchBegin, 4, line); // 4 = test Tuple
1833                self.chunk.emit(pats.len() as u8, line); // expected length
1834                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1835                self.chunk.emit_op(Op::Pop, line); // pop true
1836                                                   // Test each element
1837                for (i, pat) in pats.iter().enumerate() {
1838                    // Load the subject again and index into it
1839                    self.chunk.emit_op_u8(Op::MatchArm, 4, line); // 4 = get tuple element
1840                    self.chunk.emit(i as u8, line);
1841                    self.compile_pattern_test(pat, line)?;
1842                    let sub_fail = self.chunk.emit_jump(Op::JumpIfFalse, line);
1843                    self.chunk.emit_op(Op::Pop, line); // pop true, continue
1844                    if i == pats.len() - 1 {
1845                        // All matched
1846                        self.chunk.emit_op(Op::True, line);
1847                    }
1848                    // Patch sub_fail to push false and skip remaining
1849                    let sub_end = self.chunk.emit_jump(Op::Jump, line);
1850                    self.chunk.patch_jump(sub_fail);
1851                    // false stays on stack
1852                    self.chunk.patch_jump(sub_end);
1853                }
1854                if pats.is_empty() {
1855                    self.chunk.emit_op(Op::True, line);
1856                }
1857                let end = self.chunk.emit_jump(Op::Jump, line);
1858                self.chunk.patch_jump(fail_jump);
1859                // false stays
1860                self.chunk.patch_jump(end);
1861            }
1862            Pattern::List(pats, rest) => {
1863                // Check: is it a list with at least pats.len() elements (or exact if no rest)?
1864                let has_rest = rest.is_some();
1865                self.chunk.emit_op_u8(Op::MatchBegin, 5, line); // 5 = test List
1866                self.chunk.emit(pats.len() as u8, line); // min/exact length
1867                self.chunk.emit(if has_rest { 1 } else { 0 }, line); // has_rest flag
1868                let fail_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
1869                self.chunk.emit_op(Op::Pop, line); // pop true
1870                                                   // Test each element pattern
1871                for (i, pat) in pats.iter().enumerate() {
1872                    self.chunk.emit_op_u8(Op::MatchArm, 5, line); // 5 = get list element
1873                    self.chunk.emit(i as u8, line);
1874                    self.compile_pattern_test(pat, line)?;
1875                    let sub_fail = self.chunk.emit_jump(Op::JumpIfFalse, line);
1876                    self.chunk.emit_op(Op::Pop, line); // pop true
1877                    if i == pats.len() - 1 {
1878                        self.chunk.emit_op(Op::True, line);
1879                    }
1880                    let sub_end = self.chunk.emit_jump(Op::Jump, line);
1881                    self.chunk.patch_jump(sub_fail);
1882                    self.chunk.patch_jump(sub_end);
1883                }
1884                if pats.is_empty() {
1885                    self.chunk.emit_op(Op::True, line);
1886                }
1887                let end = self.chunk.emit_jump(Op::Jump, line);
1888                self.chunk.patch_jump(fail_jump);
1889                self.chunk.patch_jump(end);
1890            }
1891            _ => {
1892                // For complex patterns (EnumVariant, Struct), fall back
1893                return Err(IonError::runtime(
1894                    ion_str!("complex pattern not yet supported in bytecode VM match").to_string(),
1895                    line,
1896                    0,
1897                ));
1898            }
1899        }
1900        Ok(())
1901    }
1902
1903    /// Bind pattern variables: consumes value on stack.
1904    fn compile_pattern_bind(&mut self, pattern: &Pattern, line: usize) -> Result<(), IonError> {
1905        match pattern {
1906            Pattern::Wildcard => {
1907                self.chunk.emit_op(Op::Pop, line);
1908            }
1909            Pattern::Ident(name) => {
1910                self.emit_define_local(name, false, line);
1911            }
1912            Pattern::Int(_)
1913            | Pattern::Float(_)
1914            | Pattern::Bool(_)
1915            | Pattern::Str(_)
1916            | Pattern::Bytes(_)
1917            | Pattern::None => {
1918                self.chunk.emit_op(Op::Pop, line); // no bindings for literals
1919            }
1920            Pattern::Some(inner) => {
1921                // Unwrap the Some value
1922                self.chunk.emit_op_u8(Op::MatchArm, 1, line); // unwrap Some
1923                self.compile_pattern_bind(inner, line)?;
1924            }
1925            Pattern::Ok(inner) => {
1926                self.chunk.emit_op_u8(Op::MatchArm, 2, line); // unwrap Ok
1927                self.compile_pattern_bind(inner, line)?;
1928            }
1929            Pattern::Err(inner) => {
1930                self.chunk.emit_op_u8(Op::MatchArm, 3, line); // unwrap Err
1931                self.compile_pattern_bind(inner, line)?;
1932            }
1933            Pattern::Tuple(pats) => {
1934                for (i, pat) in pats.iter().enumerate() {
1935                    self.chunk.emit_op(Op::Dup, line); // dup tuple
1936                    self.chunk.emit_constant(Value::Int(i as i64), line);
1937                    self.chunk.emit_op(Op::GetIndex, line);
1938                    self.compile_pattern_bind(pat, line)?;
1939                }
1940                self.chunk.emit_op(Op::Pop, line); // pop tuple
1941            }
1942            Pattern::List(pats, rest) => {
1943                // Bind each element
1944                for (i, pat) in pats.iter().enumerate() {
1945                    self.chunk.emit_op(Op::Dup, line); // dup list
1946                    self.chunk.emit_constant(Value::Int(i as i64), line);
1947                    self.chunk.emit_op(Op::GetIndex, line);
1948                    self.compile_pattern_bind(pat, line)?;
1949                }
1950                // If there's a rest pattern, bind the remaining elements
1951                if let Some(rest_pat) = rest {
1952                    self.chunk.emit_op(Op::Dup, line); // dup list
1953                                                       // Slice from pats.len() to end
1954                    self.chunk
1955                        .emit_constant(Value::Int(pats.len() as i64), line);
1956                    // Use Slice with has_start only
1957                    self.chunk.emit_op_u8(Op::Slice, 1, line); // flags: has_start=1
1958                    self.compile_pattern_bind(rest_pat, line)?;
1959                }
1960                self.chunk.emit_op(Op::Pop, line); // pop list
1961            }
1962            _ => {
1963                return Err(IonError::runtime(
1964                    ion_str!("complex pattern binding not yet supported in bytecode VM")
1965                        .to_string(),
1966                    line,
1967                    0,
1968                ));
1969            }
1970        }
1971        Ok(())
1972    }
1973
1974    fn compile_list_comp(
1975        &mut self,
1976        item_expr: &Expr,
1977        pattern: &Pattern,
1978        iter: &Expr,
1979        cond: Option<&Expr>,
1980        line: usize,
1981    ) -> Result<(), IonError> {
1982        // Build an empty list, then iterate and append
1983        self.chunk.emit_op_u16(Op::BuildList, 0, line); // empty list on stack
1984
1985        // Evaluate iterator
1986        self.compile_expr(iter)?;
1987        self.chunk.emit_op(Op::IterInit, line);
1988
1989        let loop_start = self.chunk.len();
1990        let exit_jump = self.chunk.emit_jump(Op::IterNext, line);
1991
1992        // Bind pattern in scope
1993        self.begin_scope(line);
1994        self.compile_let_pattern(pattern, false, line)?;
1995
1996        // If there's a condition, check it
1997        if let Some(cond_expr) = cond {
1998            self.compile_expr(cond_expr)?;
1999            let skip_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2000            self.chunk.emit_op(Op::Pop, line); // pop true
2001
2002            // Compile item expression and append
2003            self.compile_expr(item_expr)?;
2004            self.chunk.emit_op(Op::ListAppend, line);
2005
2006            let after = self.chunk.emit_jump(Op::Jump, line);
2007            self.chunk.patch_jump(skip_jump);
2008            self.chunk.emit_op(Op::Pop, line); // pop false
2009            self.chunk.patch_jump(after);
2010        } else {
2011            // Compile item expression and append
2012            self.compile_expr(item_expr)?;
2013            self.chunk.emit_op(Op::ListAppend, line);
2014        }
2015
2016        self.end_scope(line);
2017
2018        // Push placeholder for IterNext to pop on next iteration
2019        self.chunk.emit_op(Op::Unit, line);
2020
2021        // Loop back
2022        let offset = self.chunk.len() - loop_start + 3;
2023        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
2024
2025        self.chunk.patch_jump(exit_jump);
2026        self.chunk.emit_op(Op::Pop, line); // pop exhausted iterator placeholder
2027                                           // List is still on stack
2028        Ok(())
2029    }
2030
2031    fn compile_dict_comp(
2032        &mut self,
2033        key_expr: &Expr,
2034        value_expr: &Expr,
2035        pattern: &Pattern,
2036        iter: &Expr,
2037        cond: Option<&Expr>,
2038        line: usize,
2039    ) -> Result<(), IonError> {
2040        // Build an empty dict, then iterate and insert
2041        self.chunk.emit_op_u16(Op::BuildDict, 0, line);
2042
2043        self.compile_expr(iter)?;
2044        self.chunk.emit_op(Op::IterInit, line);
2045
2046        let loop_start = self.chunk.len();
2047        let exit_jump = self.chunk.emit_jump(Op::IterNext, line);
2048
2049        self.begin_scope(line);
2050        self.compile_let_pattern(pattern, false, line)?;
2051
2052        if let Some(cond_expr) = cond {
2053            self.compile_expr(cond_expr)?;
2054            let skip_jump = self.chunk.emit_jump(Op::JumpIfFalse, line);
2055            self.chunk.emit_op(Op::Pop, line);
2056
2057            self.compile_expr(key_expr)?;
2058            self.compile_expr(value_expr)?;
2059            self.chunk.emit_op(Op::DictInsert, line);
2060
2061            let after = self.chunk.emit_jump(Op::Jump, line);
2062            self.chunk.patch_jump(skip_jump);
2063            self.chunk.emit_op(Op::Pop, line);
2064            self.chunk.patch_jump(after);
2065        } else {
2066            self.compile_expr(key_expr)?;
2067            self.compile_expr(value_expr)?;
2068            self.chunk.emit_op(Op::DictInsert, line);
2069        }
2070
2071        self.end_scope(line);
2072
2073        // Push placeholder for IterNext to pop on next iteration
2074        self.chunk.emit_op(Op::Unit, line);
2075
2076        let offset = self.chunk.len() - loop_start + 3;
2077        self.chunk.emit_op_u16(Op::Loop, offset as u16, line);
2078
2079        self.chunk.patch_jump(exit_jump);
2080        self.chunk.emit_op(Op::Pop, line);
2081        Ok(())
2082    }
2083}