Skip to main content

harn_vm/
compiler.rs

1use harn_lexer::StringSegment;
2use harn_parser::{BindingPattern, Node, SNode, TypedParam};
3
4use crate::chunk::{Chunk, CompiledFunction, Constant, Op};
5
6/// Compile error.
7#[derive(Debug)]
8pub struct CompileError {
9    pub message: String,
10    pub line: u32,
11}
12
13impl std::fmt::Display for CompileError {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(f, "Compile error at line {}: {}", self.line, self.message)
16    }
17}
18
19impl std::error::Error for CompileError {}
20
21/// Tracks loop context for break/continue compilation.
22struct LoopContext {
23    /// Offset of the loop start (for continue).
24    start_offset: usize,
25    /// Positions of break jumps that need patching to the loop end.
26    break_patches: Vec<usize>,
27    /// True if this is a for-in loop (has an iterator to clean up on break).
28    has_iterator: bool,
29    /// Number of exception handlers active at loop entry.
30    handler_depth: usize,
31    /// Number of pending finally bodies at loop entry.
32    finally_depth: usize,
33}
34
35/// Compiles an AST into bytecode.
36pub struct Compiler {
37    chunk: Chunk,
38    line: u32,
39    column: u32,
40    /// Track enum type names so PropertyAccess on them can produce EnumVariant.
41    enum_names: std::collections::HashSet<String>,
42    /// Stack of active loop contexts for break/continue.
43    loop_stack: Vec<LoopContext>,
44    /// Current depth of exception handlers (for cleanup on break/continue).
45    handler_depth: usize,
46    /// Stack of pending finally bodies for return/break/continue handling.
47    finally_bodies: Vec<Vec<SNode>>,
48    /// Counter for unique temp variable names.
49    temp_counter: usize,
50}
51
52impl Compiler {
53    pub fn new() -> Self {
54        Self {
55            chunk: Chunk::new(),
56            line: 1,
57            column: 1,
58            enum_names: std::collections::HashSet::new(),
59            loop_stack: Vec::new(),
60            handler_depth: 0,
61            finally_bodies: Vec::new(),
62            temp_counter: 0,
63        }
64    }
65
66    /// Compile a program (list of top-level nodes) into a Chunk.
67    /// Finds the entry pipeline and compiles its body, including inherited bodies.
68    pub fn compile(mut self, program: &[SNode]) -> Result<Chunk, CompileError> {
69        // Pre-scan the entire program for enum declarations (including inside pipelines)
70        // so we can recognize EnumName.Variant as enum construction.
71        Self::collect_enum_names(program, &mut self.enum_names);
72        // Built-in Result enum is always available
73        self.enum_names.insert("Result".to_string());
74
75        // Compile all top-level non-pipeline declarations first (fn, enum, etc.)
76        for sn in program {
77            match &sn.node {
78                Node::ImportDecl { .. } | Node::SelectiveImport { .. } => {
79                    self.compile_node(sn)?;
80                }
81                _ => {}
82            }
83        }
84
85        // Find entry pipeline
86        let main = program
87            .iter()
88            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == "default"))
89            .or_else(|| {
90                program
91                    .iter()
92                    .find(|sn| matches!(&sn.node, Node::Pipeline { .. }))
93            });
94
95        if let Some(sn) = main {
96            if let Node::Pipeline { body, extends, .. } = &sn.node {
97                // If this pipeline extends another, compile the parent chain first
98                if let Some(parent_name) = extends {
99                    self.compile_parent_pipeline(program, parent_name)?;
100                }
101                self.compile_block(body)?;
102            }
103        }
104
105        self.chunk.emit(Op::Nil, self.line);
106        self.chunk.emit(Op::Return, self.line);
107        Ok(self.chunk)
108    }
109
110    /// Compile a specific named pipeline (for test runners).
111    pub fn compile_named(
112        mut self,
113        program: &[SNode],
114        pipeline_name: &str,
115    ) -> Result<Chunk, CompileError> {
116        Self::collect_enum_names(program, &mut self.enum_names);
117
118        for sn in program {
119            if matches!(
120                &sn.node,
121                Node::ImportDecl { .. } | Node::SelectiveImport { .. }
122            ) {
123                self.compile_node(sn)?;
124            }
125        }
126
127        let target = program
128            .iter()
129            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == pipeline_name));
130
131        if let Some(sn) = target {
132            if let Node::Pipeline { body, extends, .. } = &sn.node {
133                if let Some(parent_name) = extends {
134                    self.compile_parent_pipeline(program, parent_name)?;
135                }
136                self.compile_block(body)?;
137            }
138        }
139
140        self.chunk.emit(Op::Nil, self.line);
141        self.chunk.emit(Op::Return, self.line);
142        Ok(self.chunk)
143    }
144
145    /// Recursively compile parent pipeline bodies (for extends).
146    fn compile_parent_pipeline(
147        &mut self,
148        program: &[SNode],
149        parent_name: &str,
150    ) -> Result<(), CompileError> {
151        let parent = program
152            .iter()
153            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == parent_name));
154        if let Some(sn) = parent {
155            if let Node::Pipeline { body, extends, .. } = &sn.node {
156                // Recurse if this parent also extends another
157                if let Some(grandparent) = extends {
158                    self.compile_parent_pipeline(program, grandparent)?;
159                }
160                // Compile parent body - pop all statement values
161                for stmt in body {
162                    self.compile_node(stmt)?;
163                    if Self::produces_value(&stmt.node) {
164                        self.chunk.emit(Op::Pop, self.line);
165                    }
166                }
167            }
168        }
169        Ok(())
170    }
171
172    /// Emit bytecode preamble for default parameter values.
173    /// For each param with a default at index i, emits:
174    ///   GetArgc; PushInt (i+1); GreaterEqual; JumpIfTrue <skip>;
175    ///   [compile default expr]; DefLet param_name; <skip>:
176    fn emit_default_preamble(&mut self, params: &[TypedParam]) -> Result<(), CompileError> {
177        for (i, param) in params.iter().enumerate() {
178            if let Some(default_expr) = &param.default_value {
179                self.chunk.emit(Op::GetArgc, self.line);
180                let threshold_idx = self.chunk.add_constant(Constant::Int((i + 1) as i64));
181                self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
182                // argc >= (i+1) means arg was provided
183                self.chunk.emit(Op::GreaterEqual, self.line);
184                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
185                // Pop the boolean from JumpIfTrue (it doesn't pop)
186                self.chunk.emit(Op::Pop, self.line);
187                // Compile the default expression
188                self.compile_node(default_expr)?;
189                let name_idx = self
190                    .chunk
191                    .add_constant(Constant::String(param.name.clone()));
192                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
193                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
194                self.chunk.patch_jump(skip_jump);
195                // Pop the boolean left by JumpIfTrue on the true path
196                self.chunk.emit(Op::Pop, self.line);
197                self.chunk.patch_jump(end_jump);
198            }
199        }
200        Ok(())
201    }
202
203    /// Emit runtime type checks for parameters with type annotations.
204    /// For each param with a type annotation, emits CheckType(var_name, type_name)
205    /// or calls __assert_shape for shape types.
206    fn emit_type_checks(&mut self, params: &[TypedParam]) {
207        for param in params {
208            if let Some(type_expr) = &param.type_expr {
209                // Handle shape types via __assert_shape builtin call
210                if let harn_parser::TypeExpr::Shape(fields) = type_expr {
211                    let spec = Self::shape_to_spec_string(fields);
212                    // Emit: __assert_shape(param_value, param_name, spec)
213                    let fn_idx = self
214                        .chunk
215                        .add_constant(Constant::String("__assert_shape".into()));
216                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
217                    let var_idx = self
218                        .chunk
219                        .add_constant(Constant::String(param.name.clone()));
220                    self.chunk.emit_u16(Op::GetVar, var_idx, self.line);
221                    let name_idx = self
222                        .chunk
223                        .add_constant(Constant::String(param.name.clone()));
224                    self.chunk.emit_u16(Op::Constant, name_idx, self.line);
225                    let spec_idx = self.chunk.add_constant(Constant::String(spec));
226                    self.chunk.emit_u16(Op::Constant, spec_idx, self.line);
227                    self.chunk.emit_u8(Op::Call, 3, self.line);
228                    self.chunk.emit(Op::Pop, self.line);
229                    continue;
230                }
231
232                let type_name = Self::type_expr_to_runtime_name(type_expr);
233                if let Some(type_name) = type_name {
234                    let var_idx = self
235                        .chunk
236                        .add_constant(Constant::String(param.name.clone()));
237                    let type_idx = self.chunk.add_constant(Constant::String(type_name));
238                    self.chunk.emit_u16(Op::CheckType, var_idx, self.line);
239                    // Emit the type name index as two extra bytes
240                    let hi = (type_idx >> 8) as u8;
241                    let lo = type_idx as u8;
242                    self.chunk.code.push(hi);
243                    self.chunk.code.push(lo);
244                }
245            }
246        }
247    }
248
249    /// Serialize a list of ShapeFields into a spec string for __assert_shape.
250    /// Format: `name:string,age:int,active:?bool,addr:{city:string,zip:string}`
251    fn shape_to_spec_string(fields: &[harn_parser::ShapeField]) -> String {
252        fields
253            .iter()
254            .map(|f| {
255                let opt = if f.optional { "?" } else { "" };
256                let type_str = Self::type_expr_to_spec(&f.type_expr);
257                format!("{}:{}{}", f.name, opt, type_str)
258            })
259            .collect::<Vec<_>>()
260            .join(",")
261    }
262
263    /// Convert a TypeExpr into a spec string fragment for shape validation.
264    fn type_expr_to_spec(type_expr: &harn_parser::TypeExpr) -> String {
265        match type_expr {
266            harn_parser::TypeExpr::Named(name) => name.clone(),
267            harn_parser::TypeExpr::Shape(fields) => {
268                let inner = Self::shape_to_spec_string(fields);
269                format!("{{{}}}", inner)
270            }
271            harn_parser::TypeExpr::List(_) => "list".to_string(),
272            harn_parser::TypeExpr::DictType(_, _) => "dict".to_string(),
273            harn_parser::TypeExpr::Union(_) => {
274                // Union types are not validated at runtime for shapes
275                "any".to_string()
276            }
277            harn_parser::TypeExpr::FnType { .. } => "closure".to_string(),
278        }
279    }
280
281    /// Convert a TypeExpr to a runtime type name string for CheckType.
282    fn type_expr_to_runtime_name(type_expr: &harn_parser::TypeExpr) -> Option<String> {
283        match type_expr {
284            harn_parser::TypeExpr::Named(name) => match name.as_str() {
285                "int" | "float" | "string" | "bool" | "list" | "dict" | "set" | "nil"
286                | "closure" => Some(name.clone()),
287                _ => None, // Unknown types are not checked at runtime
288            },
289            _ => None, // Union types, shapes, etc. are not checked at runtime
290        }
291    }
292
293    /// Emit the extra u16 type name index after a TryCatchSetup jump.
294    fn emit_type_name_extra(&mut self, type_name_idx: u16) {
295        let hi = (type_name_idx >> 8) as u8;
296        let lo = type_name_idx as u8;
297        self.chunk.code.push(hi);
298        self.chunk.code.push(lo);
299        self.chunk.lines.push(self.line);
300        self.chunk.columns.push(self.column);
301        self.chunk.lines.push(self.line);
302        self.chunk.columns.push(self.column);
303    }
304
305    /// Compile a try/catch body block (produces a value on the stack).
306    fn compile_try_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
307        if body.is_empty() {
308            self.chunk.emit(Op::Nil, self.line);
309        } else {
310            self.compile_block(body)?;
311            if !Self::produces_value(&body.last().unwrap().node) {
312                self.chunk.emit(Op::Nil, self.line);
313            }
314        }
315        Ok(())
316    }
317
318    /// Compile catch error binding (error value is on stack from handler).
319    fn compile_catch_binding(&mut self, error_var: &Option<String>) -> Result<(), CompileError> {
320        if let Some(var_name) = error_var {
321            let idx = self.chunk.add_constant(Constant::String(var_name.clone()));
322            self.chunk.emit_u16(Op::DefLet, idx, self.line);
323        } else {
324            self.chunk.emit(Op::Pop, self.line);
325        }
326        Ok(())
327    }
328
329    /// Compile finally body inline, discarding its result value.
330    fn compile_finally_inline(&mut self, finally_body: &[SNode]) -> Result<(), CompileError> {
331        if !finally_body.is_empty() {
332            self.compile_block(finally_body)?;
333            // Finally body's value is discarded — only the try/catch value matters
334            if Self::produces_value(&finally_body.last().unwrap().node) {
335                self.chunk.emit(Op::Pop, self.line);
336            }
337        }
338        Ok(())
339    }
340
341    /// Compile rethrow pattern: save error to temp var, run finally, re-throw.
342    fn compile_rethrow_with_finally(&mut self, finally_body: &[SNode]) -> Result<(), CompileError> {
343        // Error is on the stack from the handler
344        self.temp_counter += 1;
345        let temp_name = format!("__finally_err_{}__", self.temp_counter);
346        let err_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
347        self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
348        self.compile_finally_inline(finally_body)?;
349        let get_idx = self.chunk.add_constant(Constant::String(temp_name));
350        self.chunk.emit_u16(Op::GetVar, get_idx, self.line);
351        self.chunk.emit(Op::Throw, self.line);
352        Ok(())
353    }
354
355    fn compile_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
356        for (i, snode) in stmts.iter().enumerate() {
357            self.compile_node(snode)?;
358            let is_last = i == stmts.len() - 1;
359            if is_last {
360                // If the last statement doesn't produce a value, push nil
361                // so the block always leaves exactly one value on the stack.
362                if !Self::produces_value(&snode.node) {
363                    self.chunk.emit(Op::Nil, self.line);
364                }
365            } else {
366                // Only pop if the statement leaves a value on the stack
367                if Self::produces_value(&snode.node) {
368                    self.chunk.emit(Op::Pop, self.line);
369                }
370            }
371        }
372        Ok(())
373    }
374
375    fn compile_node(&mut self, snode: &SNode) -> Result<(), CompileError> {
376        self.line = snode.span.line as u32;
377        self.column = snode.span.column as u32;
378        self.chunk.set_column(self.column);
379        match &snode.node {
380            Node::IntLiteral(n) => {
381                let idx = self.chunk.add_constant(Constant::Int(*n));
382                self.chunk.emit_u16(Op::Constant, idx, self.line);
383            }
384            Node::FloatLiteral(n) => {
385                let idx = self.chunk.add_constant(Constant::Float(*n));
386                self.chunk.emit_u16(Op::Constant, idx, self.line);
387            }
388            Node::StringLiteral(s) => {
389                let idx = self.chunk.add_constant(Constant::String(s.clone()));
390                self.chunk.emit_u16(Op::Constant, idx, self.line);
391            }
392            Node::BoolLiteral(true) => self.chunk.emit(Op::True, self.line),
393            Node::BoolLiteral(false) => self.chunk.emit(Op::False, self.line),
394            Node::NilLiteral => self.chunk.emit(Op::Nil, self.line),
395            Node::DurationLiteral(ms) => {
396                let idx = self.chunk.add_constant(Constant::Duration(*ms));
397                self.chunk.emit_u16(Op::Constant, idx, self.line);
398            }
399
400            Node::Identifier(name) => {
401                let idx = self.chunk.add_constant(Constant::String(name.clone()));
402                self.chunk.emit_u16(Op::GetVar, idx, self.line);
403            }
404
405            Node::LetBinding { pattern, value, .. } => {
406                self.compile_node(value)?;
407                self.compile_destructuring(pattern, false)?;
408            }
409
410            Node::VarBinding { pattern, value, .. } => {
411                self.compile_node(value)?;
412                self.compile_destructuring(pattern, true)?;
413            }
414
415            Node::Assignment {
416                target, value, op, ..
417            } => {
418                if let Node::Identifier(name) = &target.node {
419                    let idx = self.chunk.add_constant(Constant::String(name.clone()));
420                    if let Some(op) = op {
421                        self.chunk.emit_u16(Op::GetVar, idx, self.line);
422                        self.compile_node(value)?;
423                        self.emit_compound_op(op)?;
424                        self.chunk.emit_u16(Op::SetVar, idx, self.line);
425                    } else {
426                        self.compile_node(value)?;
427                        self.chunk.emit_u16(Op::SetVar, idx, self.line);
428                    }
429                } else if let Node::PropertyAccess { object, property } = &target.node {
430                    // obj.field = value → SetProperty
431                    if let Some(var_name) = self.root_var_name(object) {
432                        let var_idx = self.chunk.add_constant(Constant::String(var_name.clone()));
433                        let prop_idx = self.chunk.add_constant(Constant::String(property.clone()));
434                        if let Some(op) = op {
435                            // compound: obj.field += value
436                            self.compile_node(target)?; // push current obj.field
437                            self.compile_node(value)?;
438                            self.emit_compound_op(op)?;
439                        } else {
440                            self.compile_node(value)?;
441                        }
442                        // Stack: [new_value]
443                        // SetProperty reads var_idx from env, sets prop, writes back
444                        self.chunk.emit_u16(Op::SetProperty, prop_idx, self.line);
445                        // Encode the variable name index as a second u16
446                        let hi = (var_idx >> 8) as u8;
447                        let lo = var_idx as u8;
448                        self.chunk.code.push(hi);
449                        self.chunk.code.push(lo);
450                        self.chunk.lines.push(self.line);
451                        self.chunk.columns.push(self.column);
452                        self.chunk.lines.push(self.line);
453                        self.chunk.columns.push(self.column);
454                    }
455                } else if let Node::SubscriptAccess { object, index } = &target.node {
456                    // obj[idx] = value → SetSubscript
457                    if let Some(var_name) = self.root_var_name(object) {
458                        let var_idx = self.chunk.add_constant(Constant::String(var_name.clone()));
459                        if let Some(op) = op {
460                            self.compile_node(target)?;
461                            self.compile_node(value)?;
462                            self.emit_compound_op(op)?;
463                        } else {
464                            self.compile_node(value)?;
465                        }
466                        self.compile_node(index)?;
467                        self.chunk.emit_u16(Op::SetSubscript, var_idx, self.line);
468                    }
469                }
470            }
471
472            Node::BinaryOp { op, left, right } => {
473                // Short-circuit operators
474                match op.as_str() {
475                    "&&" => {
476                        self.compile_node(left)?;
477                        let jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
478                        self.chunk.emit(Op::Pop, self.line);
479                        self.compile_node(right)?;
480                        self.chunk.patch_jump(jump);
481                        // Normalize to bool
482                        self.chunk.emit(Op::Not, self.line);
483                        self.chunk.emit(Op::Not, self.line);
484                        return Ok(());
485                    }
486                    "||" => {
487                        self.compile_node(left)?;
488                        let jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
489                        self.chunk.emit(Op::Pop, self.line);
490                        self.compile_node(right)?;
491                        self.chunk.patch_jump(jump);
492                        self.chunk.emit(Op::Not, self.line);
493                        self.chunk.emit(Op::Not, self.line);
494                        return Ok(());
495                    }
496                    "??" => {
497                        self.compile_node(left)?;
498                        self.chunk.emit(Op::Dup, self.line);
499                        // Check if nil: push nil, compare
500                        self.chunk.emit(Op::Nil, self.line);
501                        self.chunk.emit(Op::NotEqual, self.line);
502                        let jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
503                        self.chunk.emit(Op::Pop, self.line); // pop the not-equal result
504                        self.chunk.emit(Op::Pop, self.line); // pop the nil value
505                        self.compile_node(right)?;
506                        let end = self.chunk.emit_jump(Op::Jump, self.line);
507                        self.chunk.patch_jump(jump);
508                        self.chunk.emit(Op::Pop, self.line); // pop the not-equal result
509                        self.chunk.patch_jump(end);
510                        return Ok(());
511                    }
512                    "|>" => {
513                        self.compile_node(left)?;
514                        // If the RHS contains `_` placeholders, desugar into a closure:
515                        //   value |> func(_, arg)  =>  value |> { __pipe -> func(__pipe, arg) }
516                        if contains_pipe_placeholder(right) {
517                            let replaced = replace_pipe_placeholder(right);
518                            let closure_node = SNode::dummy(Node::Closure {
519                                params: vec![TypedParam {
520                                    name: "__pipe".into(),
521                                    type_expr: None,
522                                    default_value: None,
523                                }],
524                                body: vec![replaced],
525                            });
526                            self.compile_node(&closure_node)?;
527                        } else {
528                            self.compile_node(right)?;
529                        }
530                        self.chunk.emit(Op::Pipe, self.line);
531                        return Ok(());
532                    }
533                    _ => {}
534                }
535
536                self.compile_node(left)?;
537                self.compile_node(right)?;
538                match op.as_str() {
539                    "+" => self.chunk.emit(Op::Add, self.line),
540                    "-" => self.chunk.emit(Op::Sub, self.line),
541                    "*" => self.chunk.emit(Op::Mul, self.line),
542                    "/" => self.chunk.emit(Op::Div, self.line),
543                    "%" => self.chunk.emit(Op::Mod, self.line),
544                    "==" => self.chunk.emit(Op::Equal, self.line),
545                    "!=" => self.chunk.emit(Op::NotEqual, self.line),
546                    "<" => self.chunk.emit(Op::Less, self.line),
547                    ">" => self.chunk.emit(Op::Greater, self.line),
548                    "<=" => self.chunk.emit(Op::LessEqual, self.line),
549                    ">=" => self.chunk.emit(Op::GreaterEqual, self.line),
550                    _ => {
551                        return Err(CompileError {
552                            message: format!("Unknown operator: {op}"),
553                            line: self.line,
554                        })
555                    }
556                }
557            }
558
559            Node::UnaryOp { op, operand } => {
560                self.compile_node(operand)?;
561                match op.as_str() {
562                    "-" => self.chunk.emit(Op::Negate, self.line),
563                    "!" => self.chunk.emit(Op::Not, self.line),
564                    _ => {}
565                }
566            }
567
568            Node::Ternary {
569                condition,
570                true_expr,
571                false_expr,
572            } => {
573                self.compile_node(condition)?;
574                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
575                self.chunk.emit(Op::Pop, self.line);
576                self.compile_node(true_expr)?;
577                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
578                self.chunk.patch_jump(else_jump);
579                self.chunk.emit(Op::Pop, self.line);
580                self.compile_node(false_expr)?;
581                self.chunk.patch_jump(end_jump);
582            }
583
584            Node::FunctionCall { name, args } => {
585                let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
586                // Push function name as string constant
587                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
588                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
589
590                if has_spread {
591                    // Build the args into a single list using the flush-and-concat
592                    // pattern (same as ListLiteral with spreads).
593                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
594                    let mut pending = 0u16;
595                    for arg in args {
596                        if let Node::Spread(inner) = &arg.node {
597                            if pending > 0 {
598                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
599                                self.chunk.emit(Op::Add, self.line);
600                                pending = 0;
601                            }
602                            self.compile_node(inner)?;
603                            self.chunk.emit(Op::Dup, self.line);
604                            let assert_idx = self
605                                .chunk
606                                .add_constant(Constant::String("__assert_list".into()));
607                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
608                            self.chunk.emit(Op::Swap, self.line);
609                            self.chunk.emit_u8(Op::Call, 1, self.line);
610                            self.chunk.emit(Op::Pop, self.line);
611                            self.chunk.emit(Op::Add, self.line);
612                        } else {
613                            self.compile_node(arg)?;
614                            pending += 1;
615                        }
616                    }
617                    if pending > 0 {
618                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
619                        self.chunk.emit(Op::Add, self.line);
620                    }
621                    self.chunk.emit(Op::CallSpread, self.line);
622                } else {
623                    // Push arguments normally
624                    for arg in args {
625                        self.compile_node(arg)?;
626                    }
627                    self.chunk.emit_u8(Op::Call, args.len() as u8, self.line);
628                }
629            }
630
631            Node::MethodCall {
632                object,
633                method,
634                args,
635            } => {
636                // Check if this is an enum variant construction with args: EnumName.Variant(args)
637                if let Node::Identifier(name) = &object.node {
638                    if self.enum_names.contains(name) {
639                        // Compile args, then BuildEnum
640                        for arg in args {
641                            self.compile_node(arg)?;
642                        }
643                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
644                        let var_idx = self.chunk.add_constant(Constant::String(method.clone()));
645                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
646                        let hi = (var_idx >> 8) as u8;
647                        let lo = var_idx as u8;
648                        self.chunk.code.push(hi);
649                        self.chunk.code.push(lo);
650                        self.chunk.lines.push(self.line);
651                        self.chunk.columns.push(self.column);
652                        self.chunk.lines.push(self.line);
653                        self.chunk.columns.push(self.column);
654                        let fc = args.len() as u16;
655                        let fhi = (fc >> 8) as u8;
656                        let flo = fc as u8;
657                        self.chunk.code.push(fhi);
658                        self.chunk.code.push(flo);
659                        self.chunk.lines.push(self.line);
660                        self.chunk.columns.push(self.column);
661                        self.chunk.lines.push(self.line);
662                        self.chunk.columns.push(self.column);
663                        return Ok(());
664                    }
665                }
666                self.compile_node(object)?;
667                for arg in args {
668                    self.compile_node(arg)?;
669                }
670                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
671                self.chunk
672                    .emit_method_call(name_idx, args.len() as u8, self.line);
673            }
674
675            Node::OptionalMethodCall {
676                object,
677                method,
678                args,
679            } => {
680                self.compile_node(object)?;
681                for arg in args {
682                    self.compile_node(arg)?;
683                }
684                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
685                self.chunk
686                    .emit_method_call_opt(name_idx, args.len() as u8, self.line);
687            }
688
689            Node::PropertyAccess { object, property } => {
690                // Check if this is an enum variant construction: EnumName.Variant
691                if let Node::Identifier(name) = &object.node {
692                    if self.enum_names.contains(name) {
693                        // Emit BuildEnum with 0 fields
694                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
695                        let var_idx = self.chunk.add_constant(Constant::String(property.clone()));
696                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
697                        let hi = (var_idx >> 8) as u8;
698                        let lo = var_idx as u8;
699                        self.chunk.code.push(hi);
700                        self.chunk.code.push(lo);
701                        self.chunk.lines.push(self.line);
702                        self.chunk.columns.push(self.column);
703                        self.chunk.lines.push(self.line);
704                        self.chunk.columns.push(self.column);
705                        // 0 fields
706                        self.chunk.code.push(0);
707                        self.chunk.code.push(0);
708                        self.chunk.lines.push(self.line);
709                        self.chunk.columns.push(self.column);
710                        self.chunk.lines.push(self.line);
711                        self.chunk.columns.push(self.column);
712                        return Ok(());
713                    }
714                }
715                self.compile_node(object)?;
716                let idx = self.chunk.add_constant(Constant::String(property.clone()));
717                self.chunk.emit_u16(Op::GetProperty, idx, self.line);
718            }
719
720            Node::OptionalPropertyAccess { object, property } => {
721                self.compile_node(object)?;
722                let idx = self.chunk.add_constant(Constant::String(property.clone()));
723                self.chunk.emit_u16(Op::GetPropertyOpt, idx, self.line);
724            }
725
726            Node::SubscriptAccess { object, index } => {
727                self.compile_node(object)?;
728                self.compile_node(index)?;
729                self.chunk.emit(Op::Subscript, self.line);
730            }
731
732            Node::SliceAccess { object, start, end } => {
733                self.compile_node(object)?;
734                if let Some(s) = start {
735                    self.compile_node(s)?;
736                } else {
737                    self.chunk.emit(Op::Nil, self.line);
738                }
739                if let Some(e) = end {
740                    self.compile_node(e)?;
741                } else {
742                    self.chunk.emit(Op::Nil, self.line);
743                }
744                self.chunk.emit(Op::Slice, self.line);
745            }
746
747            Node::IfElse {
748                condition,
749                then_body,
750                else_body,
751            } => {
752                self.compile_node(condition)?;
753                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
754                self.chunk.emit(Op::Pop, self.line);
755                self.compile_block(then_body)?;
756                if let Some(else_body) = else_body {
757                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
758                    self.chunk.patch_jump(else_jump);
759                    self.chunk.emit(Op::Pop, self.line);
760                    self.compile_block(else_body)?;
761                    self.chunk.patch_jump(end_jump);
762                } else {
763                    self.chunk.patch_jump(else_jump);
764                    self.chunk.emit(Op::Pop, self.line);
765                    self.chunk.emit(Op::Nil, self.line);
766                }
767            }
768
769            Node::WhileLoop { condition, body } => {
770                let loop_start = self.chunk.current_offset();
771                self.loop_stack.push(LoopContext {
772                    start_offset: loop_start,
773                    break_patches: Vec::new(),
774                    has_iterator: false,
775                    handler_depth: self.handler_depth,
776                    finally_depth: self.finally_bodies.len(),
777                });
778                self.compile_node(condition)?;
779                let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
780                self.chunk.emit(Op::Pop, self.line); // pop condition
781                                                     // Compile body statements, popping all results
782                for sn in body {
783                    self.compile_node(sn)?;
784                    if Self::produces_value(&sn.node) {
785                        self.chunk.emit(Op::Pop, self.line);
786                    }
787                }
788                // Jump back to condition
789                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
790                self.chunk.patch_jump(exit_jump);
791                self.chunk.emit(Op::Pop, self.line); // pop condition
792                                                     // Patch all break jumps to here
793                let ctx = self.loop_stack.pop().unwrap();
794                for patch_pos in ctx.break_patches {
795                    self.chunk.patch_jump(patch_pos);
796                }
797                self.chunk.emit(Op::Nil, self.line);
798            }
799
800            Node::ForIn {
801                pattern,
802                iterable,
803                body,
804            } => {
805                // Compile iterable
806                self.compile_node(iterable)?;
807                // Initialize iterator
808                self.chunk.emit(Op::IterInit, self.line);
809                let loop_start = self.chunk.current_offset();
810                self.loop_stack.push(LoopContext {
811                    start_offset: loop_start,
812                    break_patches: Vec::new(),
813                    has_iterator: true,
814                    handler_depth: self.handler_depth,
815                    finally_depth: self.finally_bodies.len(),
816                });
817                // Try to get next item — jumps to end if exhausted
818                let exit_jump_pos = self.chunk.emit_jump(Op::IterNext, self.line);
819                // Define loop variable(s) with current item (item is on stack from IterNext)
820                self.compile_destructuring(pattern, true)?;
821                // Compile body statements, popping all results
822                for sn in body {
823                    self.compile_node(sn)?;
824                    if Self::produces_value(&sn.node) {
825                        self.chunk.emit(Op::Pop, self.line);
826                    }
827                }
828                // Loop back
829                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
830                self.chunk.patch_jump(exit_jump_pos);
831                // Patch all break jumps to here
832                let ctx = self.loop_stack.pop().unwrap();
833                for patch_pos in ctx.break_patches {
834                    self.chunk.patch_jump(patch_pos);
835                }
836                // Push nil as result (iterator state was consumed)
837                self.chunk.emit(Op::Nil, self.line);
838            }
839
840            Node::ReturnStmt { value } => {
841                let has_pending_finally = !self.finally_bodies.is_empty();
842
843                if has_pending_finally {
844                    // Inside try-finally: compile value, save to temp,
845                    // run pending finallys, restore value, then return.
846                    if let Some(val) = value {
847                        self.compile_node(val)?;
848                    } else {
849                        self.chunk.emit(Op::Nil, self.line);
850                    }
851                    self.temp_counter += 1;
852                    let temp_name = format!("__return_val_{}__", self.temp_counter);
853                    let save_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
854                    self.chunk.emit_u16(Op::DefVar, save_idx, self.line);
855                    // Emit all pending finallys (innermost first = reverse order)
856                    let finallys: Vec<_> = self.finally_bodies.iter().rev().cloned().collect();
857                    for fb in &finallys {
858                        self.compile_finally_inline(fb)?;
859                    }
860                    let restore_idx = self.chunk.add_constant(Constant::String(temp_name));
861                    self.chunk.emit_u16(Op::GetVar, restore_idx, self.line);
862                    self.chunk.emit(Op::Return, self.line);
863                } else {
864                    // No pending finally — original behavior with tail call optimization
865                    if let Some(val) = value {
866                        if let Node::FunctionCall { name, args } = &val.node {
867                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
868                            self.chunk.emit_u16(Op::Constant, name_idx, self.line);
869                            for arg in args {
870                                self.compile_node(arg)?;
871                            }
872                            self.chunk
873                                .emit_u8(Op::TailCall, args.len() as u8, self.line);
874                        } else if let Node::BinaryOp { op, left, right } = &val.node {
875                            if op == "|>" {
876                                self.compile_node(left)?;
877                                self.compile_node(right)?;
878                                self.chunk.emit(Op::Swap, self.line);
879                                self.chunk.emit_u8(Op::TailCall, 1, self.line);
880                            } else {
881                                self.compile_node(val)?;
882                            }
883                        } else {
884                            self.compile_node(val)?;
885                        }
886                    } else {
887                        self.chunk.emit(Op::Nil, self.line);
888                    }
889                    self.chunk.emit(Op::Return, self.line);
890                }
891            }
892
893            Node::BreakStmt => {
894                if self.loop_stack.is_empty() {
895                    return Err(CompileError {
896                        message: "break outside of loop".to_string(),
897                        line: self.line,
898                    });
899                }
900                // Copy values out to avoid borrow conflict
901                let ctx = self.loop_stack.last().unwrap();
902                let finally_depth = ctx.finally_depth;
903                let handler_depth = ctx.handler_depth;
904                let has_iterator = ctx.has_iterator;
905                // Pop exception handlers that were pushed inside the loop
906                for _ in handler_depth..self.handler_depth {
907                    self.chunk.emit(Op::PopHandler, self.line);
908                }
909                // Emit pending finallys that are inside the loop
910                if self.finally_bodies.len() > finally_depth {
911                    let finallys: Vec<_> = self.finally_bodies[finally_depth..]
912                        .iter()
913                        .rev()
914                        .cloned()
915                        .collect();
916                    for fb in &finallys {
917                        self.compile_finally_inline(fb)?;
918                    }
919                }
920                if has_iterator {
921                    self.chunk.emit(Op::PopIterator, self.line);
922                }
923                let patch = self.chunk.emit_jump(Op::Jump, self.line);
924                self.loop_stack
925                    .last_mut()
926                    .unwrap()
927                    .break_patches
928                    .push(patch);
929            }
930
931            Node::ContinueStmt => {
932                if self.loop_stack.is_empty() {
933                    return Err(CompileError {
934                        message: "continue outside of loop".to_string(),
935                        line: self.line,
936                    });
937                }
938                let ctx = self.loop_stack.last().unwrap();
939                let finally_depth = ctx.finally_depth;
940                let handler_depth = ctx.handler_depth;
941                let loop_start = ctx.start_offset;
942                for _ in handler_depth..self.handler_depth {
943                    self.chunk.emit(Op::PopHandler, self.line);
944                }
945                if self.finally_bodies.len() > finally_depth {
946                    let finallys: Vec<_> = self.finally_bodies[finally_depth..]
947                        .iter()
948                        .rev()
949                        .cloned()
950                        .collect();
951                    for fb in &finallys {
952                        self.compile_finally_inline(fb)?;
953                    }
954                }
955                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
956            }
957
958            Node::ListLiteral(elements) => {
959                let has_spread = elements.iter().any(|e| matches!(&e.node, Node::Spread(_)));
960                if !has_spread {
961                    for el in elements {
962                        self.compile_node(el)?;
963                    }
964                    self.chunk
965                        .emit_u16(Op::BuildList, elements.len() as u16, self.line);
966                } else {
967                    // Build with spreads: accumulate segments into lists and concat
968                    // Start with empty list
969                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
970                    let mut pending = 0u16;
971                    for el in elements {
972                        if let Node::Spread(inner) = &el.node {
973                            // First, build list from pending non-spread elements
974                            if pending > 0 {
975                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
976                                // Concat accumulated + pending segment
977                                self.chunk.emit(Op::Add, self.line);
978                                pending = 0;
979                            }
980                            // Concat with the spread expression (with type check)
981                            self.compile_node(inner)?;
982                            self.chunk.emit(Op::Dup, self.line);
983                            let assert_idx = self
984                                .chunk
985                                .add_constant(Constant::String("__assert_list".into()));
986                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
987                            self.chunk.emit(Op::Swap, self.line);
988                            self.chunk.emit_u8(Op::Call, 1, self.line);
989                            self.chunk.emit(Op::Pop, self.line);
990                            self.chunk.emit(Op::Add, self.line);
991                        } else {
992                            self.compile_node(el)?;
993                            pending += 1;
994                        }
995                    }
996                    if pending > 0 {
997                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
998                        self.chunk.emit(Op::Add, self.line);
999                    }
1000                }
1001            }
1002
1003            Node::DictLiteral(entries) => {
1004                let has_spread = entries
1005                    .iter()
1006                    .any(|e| matches!(&e.value.node, Node::Spread(_)));
1007                if !has_spread {
1008                    for entry in entries {
1009                        self.compile_node(&entry.key)?;
1010                        self.compile_node(&entry.value)?;
1011                    }
1012                    self.chunk
1013                        .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
1014                } else {
1015                    // Build with spreads: use empty dict + Add for merging
1016                    self.chunk.emit_u16(Op::BuildDict, 0, self.line);
1017                    let mut pending = 0u16;
1018                    for entry in entries {
1019                        if let Node::Spread(inner) = &entry.value.node {
1020                            // Flush pending entries
1021                            if pending > 0 {
1022                                self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1023                                self.chunk.emit(Op::Add, self.line);
1024                                pending = 0;
1025                            }
1026                            // Merge spread dict via Add (with type check)
1027                            self.compile_node(inner)?;
1028                            self.chunk.emit(Op::Dup, self.line);
1029                            let assert_idx = self
1030                                .chunk
1031                                .add_constant(Constant::String("__assert_dict".into()));
1032                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1033                            self.chunk.emit(Op::Swap, self.line);
1034                            self.chunk.emit_u8(Op::Call, 1, self.line);
1035                            self.chunk.emit(Op::Pop, self.line);
1036                            self.chunk.emit(Op::Add, self.line);
1037                        } else {
1038                            self.compile_node(&entry.key)?;
1039                            self.compile_node(&entry.value)?;
1040                            pending += 1;
1041                        }
1042                    }
1043                    if pending > 0 {
1044                        self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1045                        self.chunk.emit(Op::Add, self.line);
1046                    }
1047                }
1048            }
1049
1050            Node::InterpolatedString(segments) => {
1051                let mut part_count = 0u16;
1052                for seg in segments {
1053                    match seg {
1054                        StringSegment::Literal(s) => {
1055                            let idx = self.chunk.add_constant(Constant::String(s.clone()));
1056                            self.chunk.emit_u16(Op::Constant, idx, self.line);
1057                            part_count += 1;
1058                        }
1059                        StringSegment::Expression(expr_str) => {
1060                            // Parse and compile the embedded expression
1061                            let mut lexer = harn_lexer::Lexer::new(expr_str);
1062                            if let Ok(tokens) = lexer.tokenize() {
1063                                let mut parser = harn_parser::Parser::new(tokens);
1064                                if let Ok(snode) = parser.parse_single_expression() {
1065                                    self.compile_node(&snode)?;
1066                                    // Convert result to string for concatenation
1067                                    let to_str = self
1068                                        .chunk
1069                                        .add_constant(Constant::String("to_string".into()));
1070                                    self.chunk.emit_u16(Op::Constant, to_str, self.line);
1071                                    self.chunk.emit(Op::Swap, self.line);
1072                                    self.chunk.emit_u8(Op::Call, 1, self.line);
1073                                    part_count += 1;
1074                                } else {
1075                                    // Fallback: treat as literal string
1076                                    let idx =
1077                                        self.chunk.add_constant(Constant::String(expr_str.clone()));
1078                                    self.chunk.emit_u16(Op::Constant, idx, self.line);
1079                                    part_count += 1;
1080                                }
1081                            }
1082                        }
1083                    }
1084                }
1085                if part_count > 1 {
1086                    self.chunk.emit_u16(Op::Concat, part_count, self.line);
1087                }
1088            }
1089
1090            Node::FnDecl {
1091                name, params, body, ..
1092            } => {
1093                // Compile function body into a separate chunk
1094                let mut fn_compiler = Compiler::new();
1095                fn_compiler.enum_names = self.enum_names.clone();
1096                fn_compiler.emit_default_preamble(params)?;
1097                fn_compiler.emit_type_checks(params);
1098                fn_compiler.compile_block(body)?;
1099                fn_compiler.chunk.emit(Op::Nil, self.line);
1100                fn_compiler.chunk.emit(Op::Return, self.line);
1101
1102                let func = CompiledFunction {
1103                    name: name.clone(),
1104                    params: TypedParam::names(params),
1105                    default_start: TypedParam::default_start(params),
1106                    chunk: fn_compiler.chunk,
1107                };
1108                let fn_idx = self.chunk.functions.len();
1109                self.chunk.functions.push(func);
1110
1111                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1112                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1113                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1114            }
1115
1116            Node::Closure { params, body } => {
1117                let mut fn_compiler = Compiler::new();
1118                fn_compiler.enum_names = self.enum_names.clone();
1119                fn_compiler.emit_default_preamble(params)?;
1120                fn_compiler.emit_type_checks(params);
1121                fn_compiler.compile_block(body)?;
1122                // If block didn't end with return, the last value is on the stack
1123                fn_compiler.chunk.emit(Op::Return, self.line);
1124
1125                let func = CompiledFunction {
1126                    name: "<closure>".to_string(),
1127                    params: TypedParam::names(params),
1128                    default_start: TypedParam::default_start(params),
1129                    chunk: fn_compiler.chunk,
1130                };
1131                let fn_idx = self.chunk.functions.len();
1132                self.chunk.functions.push(func);
1133
1134                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1135            }
1136
1137            Node::ThrowStmt { value } => {
1138                self.compile_node(value)?;
1139                self.chunk.emit(Op::Throw, self.line);
1140            }
1141
1142            Node::MatchExpr { value, arms } => {
1143                self.compile_node(value)?;
1144                let mut end_jumps = Vec::new();
1145                for arm in arms {
1146                    match &arm.pattern.node {
1147                        // Wildcard `_` — always matches
1148                        Node::Identifier(name) if name == "_" => {
1149                            self.chunk.emit(Op::Pop, self.line); // pop match value
1150                            self.compile_match_body(&arm.body)?;
1151                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1152                        }
1153                        // Enum destructuring: EnumConstruct pattern
1154                        Node::EnumConstruct {
1155                            enum_name,
1156                            variant,
1157                            args: pat_args,
1158                        } => {
1159                            // Check if the match value is this enum variant
1160                            self.chunk.emit(Op::Dup, self.line);
1161                            let en_idx =
1162                                self.chunk.add_constant(Constant::String(enum_name.clone()));
1163                            let vn_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1164                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1165                            let hi = (vn_idx >> 8) as u8;
1166                            let lo = vn_idx as u8;
1167                            self.chunk.code.push(hi);
1168                            self.chunk.code.push(lo);
1169                            self.chunk.lines.push(self.line);
1170                            self.chunk.columns.push(self.column);
1171                            self.chunk.lines.push(self.line);
1172                            self.chunk.columns.push(self.column);
1173                            // Stack: [match_value, bool]
1174                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1175                            self.chunk.emit(Op::Pop, self.line); // pop bool
1176
1177                            // Destructure: bind field variables from the enum's fields
1178                            // The match value is still on the stack; we need to extract fields
1179                            for (i, pat_arg) in pat_args.iter().enumerate() {
1180                                if let Node::Identifier(binding_name) = &pat_arg.node {
1181                                    // Dup the match value, get .fields, subscript [i]
1182                                    self.chunk.emit(Op::Dup, self.line);
1183                                    let fields_idx = self
1184                                        .chunk
1185                                        .add_constant(Constant::String("fields".to_string()));
1186                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1187                                    let idx_const =
1188                                        self.chunk.add_constant(Constant::Int(i as i64));
1189                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1190                                    self.chunk.emit(Op::Subscript, self.line);
1191                                    let name_idx = self
1192                                        .chunk
1193                                        .add_constant(Constant::String(binding_name.clone()));
1194                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1195                                }
1196                            }
1197
1198                            self.chunk.emit(Op::Pop, self.line); // pop match value
1199                            self.compile_match_body(&arm.body)?;
1200                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1201                            self.chunk.patch_jump(skip);
1202                            self.chunk.emit(Op::Pop, self.line); // pop bool
1203                        }
1204                        // Enum variant without args: PropertyAccess(EnumName, Variant)
1205                        Node::PropertyAccess { object, property } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1206                        {
1207                            let enum_name = if let Node::Identifier(n) = &object.node {
1208                                n.clone()
1209                            } else {
1210                                unreachable!()
1211                            };
1212                            self.chunk.emit(Op::Dup, self.line);
1213                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1214                            let vn_idx =
1215                                self.chunk.add_constant(Constant::String(property.clone()));
1216                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1217                            let hi = (vn_idx >> 8) as u8;
1218                            let lo = vn_idx as u8;
1219                            self.chunk.code.push(hi);
1220                            self.chunk.code.push(lo);
1221                            self.chunk.lines.push(self.line);
1222                            self.chunk.columns.push(self.column);
1223                            self.chunk.lines.push(self.line);
1224                            self.chunk.columns.push(self.column);
1225                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1226                            self.chunk.emit(Op::Pop, self.line); // pop bool
1227                            self.chunk.emit(Op::Pop, self.line); // pop match value
1228                            self.compile_match_body(&arm.body)?;
1229                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1230                            self.chunk.patch_jump(skip);
1231                            self.chunk.emit(Op::Pop, self.line); // pop bool
1232                        }
1233                        // Enum destructuring via MethodCall: EnumName.Variant(bindings...)
1234                        // Parser produces MethodCall for EnumName.Variant(x) patterns
1235                        Node::MethodCall {
1236                            object,
1237                            method,
1238                            args: pat_args,
1239                        } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1240                        {
1241                            let enum_name = if let Node::Identifier(n) = &object.node {
1242                                n.clone()
1243                            } else {
1244                                unreachable!()
1245                            };
1246                            // Check if the match value is this enum variant
1247                            self.chunk.emit(Op::Dup, self.line);
1248                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1249                            let vn_idx = self.chunk.add_constant(Constant::String(method.clone()));
1250                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1251                            let hi = (vn_idx >> 8) as u8;
1252                            let lo = vn_idx as u8;
1253                            self.chunk.code.push(hi);
1254                            self.chunk.code.push(lo);
1255                            self.chunk.lines.push(self.line);
1256                            self.chunk.columns.push(self.column);
1257                            self.chunk.lines.push(self.line);
1258                            self.chunk.columns.push(self.column);
1259                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1260                            self.chunk.emit(Op::Pop, self.line); // pop bool
1261
1262                            // Destructure: bind field variables
1263                            for (i, pat_arg) in pat_args.iter().enumerate() {
1264                                if let Node::Identifier(binding_name) = &pat_arg.node {
1265                                    self.chunk.emit(Op::Dup, self.line);
1266                                    let fields_idx = self
1267                                        .chunk
1268                                        .add_constant(Constant::String("fields".to_string()));
1269                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1270                                    let idx_const =
1271                                        self.chunk.add_constant(Constant::Int(i as i64));
1272                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1273                                    self.chunk.emit(Op::Subscript, self.line);
1274                                    let name_idx = self
1275                                        .chunk
1276                                        .add_constant(Constant::String(binding_name.clone()));
1277                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1278                                }
1279                            }
1280
1281                            self.chunk.emit(Op::Pop, self.line); // pop match value
1282                            self.compile_match_body(&arm.body)?;
1283                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1284                            self.chunk.patch_jump(skip);
1285                            self.chunk.emit(Op::Pop, self.line); // pop bool
1286                        }
1287                        // Binding pattern: bare identifier (not a literal)
1288                        Node::Identifier(name) => {
1289                            // Bind the match value to this name, always matches
1290                            self.chunk.emit(Op::Dup, self.line); // dup for binding
1291                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1292                            self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1293                            self.chunk.emit(Op::Pop, self.line); // pop match value
1294                            self.compile_match_body(&arm.body)?;
1295                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1296                        }
1297                        // Dict pattern: {key: literal, key: binding, ...}
1298                        Node::DictLiteral(entries)
1299                            if entries
1300                                .iter()
1301                                .all(|e| matches!(&e.key.node, Node::StringLiteral(_))) =>
1302                        {
1303                            // Check type is dict: dup, call type_of, compare "dict"
1304                            self.chunk.emit(Op::Dup, self.line);
1305                            let typeof_idx =
1306                                self.chunk.add_constant(Constant::String("type_of".into()));
1307                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1308                            self.chunk.emit(Op::Swap, self.line);
1309                            self.chunk.emit_u8(Op::Call, 1, self.line);
1310                            let dict_str = self.chunk.add_constant(Constant::String("dict".into()));
1311                            self.chunk.emit_u16(Op::Constant, dict_str, self.line);
1312                            self.chunk.emit(Op::Equal, self.line);
1313                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1314                            self.chunk.emit(Op::Pop, self.line); // pop bool
1315
1316                            // Check literal constraints
1317                            let mut constraint_skips = Vec::new();
1318                            let mut bindings = Vec::new();
1319                            for entry in entries {
1320                                if let Node::StringLiteral(key) = &entry.key.node {
1321                                    match &entry.value.node {
1322                                        // Literal value → constraint: dict[key] == value
1323                                        Node::StringLiteral(_)
1324                                        | Node::IntLiteral(_)
1325                                        | Node::FloatLiteral(_)
1326                                        | Node::BoolLiteral(_)
1327                                        | Node::NilLiteral => {
1328                                            self.chunk.emit(Op::Dup, self.line);
1329                                            let key_idx = self
1330                                                .chunk
1331                                                .add_constant(Constant::String(key.clone()));
1332                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1333                                            self.chunk.emit(Op::Subscript, self.line);
1334                                            self.compile_node(&entry.value)?;
1335                                            self.chunk.emit(Op::Equal, self.line);
1336                                            let skip =
1337                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1338                                            self.chunk.emit(Op::Pop, self.line); // pop bool
1339                                            constraint_skips.push(skip);
1340                                        }
1341                                        // Identifier → binding: bind dict[key] to variable
1342                                        Node::Identifier(binding) => {
1343                                            bindings.push((key.clone(), binding.clone()));
1344                                        }
1345                                        _ => {
1346                                            // Complex expression constraint
1347                                            self.chunk.emit(Op::Dup, self.line);
1348                                            let key_idx = self
1349                                                .chunk
1350                                                .add_constant(Constant::String(key.clone()));
1351                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1352                                            self.chunk.emit(Op::Subscript, self.line);
1353                                            self.compile_node(&entry.value)?;
1354                                            self.chunk.emit(Op::Equal, self.line);
1355                                            let skip =
1356                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1357                                            self.chunk.emit(Op::Pop, self.line);
1358                                            constraint_skips.push(skip);
1359                                        }
1360                                    }
1361                                }
1362                            }
1363
1364                            // All constraints passed — emit bindings
1365                            for (key, binding) in &bindings {
1366                                self.chunk.emit(Op::Dup, self.line);
1367                                let key_idx =
1368                                    self.chunk.add_constant(Constant::String(key.clone()));
1369                                self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1370                                self.chunk.emit(Op::Subscript, self.line);
1371                                let name_idx =
1372                                    self.chunk.add_constant(Constant::String(binding.clone()));
1373                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1374                            }
1375
1376                            self.chunk.emit(Op::Pop, self.line); // pop match value
1377                            self.compile_match_body(&arm.body)?;
1378                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1379
1380                            // All failures jump here: pop the false bool, leave match_value
1381                            let fail_target = self.chunk.code.len();
1382                            self.chunk.emit(Op::Pop, self.line); // pop bool
1383                                                                 // Patch all failure jumps to the shared cleanup point
1384                            for skip in constraint_skips {
1385                                self.chunk.patch_jump_to(skip, fail_target);
1386                            }
1387                            self.chunk.patch_jump_to(skip_type, fail_target);
1388                        }
1389                        // List pattern: [literal, binding, ...]
1390                        Node::ListLiteral(elements) => {
1391                            // Check type is list: dup, call type_of, compare "list"
1392                            self.chunk.emit(Op::Dup, self.line);
1393                            let typeof_idx =
1394                                self.chunk.add_constant(Constant::String("type_of".into()));
1395                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1396                            self.chunk.emit(Op::Swap, self.line);
1397                            self.chunk.emit_u8(Op::Call, 1, self.line);
1398                            let list_str = self.chunk.add_constant(Constant::String("list".into()));
1399                            self.chunk.emit_u16(Op::Constant, list_str, self.line);
1400                            self.chunk.emit(Op::Equal, self.line);
1401                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1402                            self.chunk.emit(Op::Pop, self.line); // pop bool
1403
1404                            // Check length: dup, call len, compare >= elements.len()
1405                            self.chunk.emit(Op::Dup, self.line);
1406                            let len_idx = self.chunk.add_constant(Constant::String("len".into()));
1407                            self.chunk.emit_u16(Op::Constant, len_idx, self.line);
1408                            self.chunk.emit(Op::Swap, self.line);
1409                            self.chunk.emit_u8(Op::Call, 1, self.line);
1410                            let count = self
1411                                .chunk
1412                                .add_constant(Constant::Int(elements.len() as i64));
1413                            self.chunk.emit_u16(Op::Constant, count, self.line);
1414                            self.chunk.emit(Op::GreaterEqual, self.line);
1415                            let skip_len = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1416                            self.chunk.emit(Op::Pop, self.line); // pop bool
1417
1418                            // Check literal constraints and collect bindings
1419                            let mut constraint_skips = Vec::new();
1420                            let mut bindings = Vec::new();
1421                            for (i, elem) in elements.iter().enumerate() {
1422                                match &elem.node {
1423                                    Node::Identifier(name) if name != "_" => {
1424                                        bindings.push((i, name.clone()));
1425                                    }
1426                                    Node::Identifier(_) => {} // wildcard _
1427                                    // Literal constraint
1428                                    _ => {
1429                                        self.chunk.emit(Op::Dup, self.line);
1430                                        let idx_const =
1431                                            self.chunk.add_constant(Constant::Int(i as i64));
1432                                        self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1433                                        self.chunk.emit(Op::Subscript, self.line);
1434                                        self.compile_node(elem)?;
1435                                        self.chunk.emit(Op::Equal, self.line);
1436                                        let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1437                                        self.chunk.emit(Op::Pop, self.line);
1438                                        constraint_skips.push(skip);
1439                                    }
1440                                }
1441                            }
1442
1443                            // Emit bindings
1444                            for (i, name) in &bindings {
1445                                self.chunk.emit(Op::Dup, self.line);
1446                                let idx_const = self.chunk.add_constant(Constant::Int(*i as i64));
1447                                self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1448                                self.chunk.emit(Op::Subscript, self.line);
1449                                let name_idx =
1450                                    self.chunk.add_constant(Constant::String(name.clone()));
1451                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1452                            }
1453
1454                            self.chunk.emit(Op::Pop, self.line); // pop match value
1455                            self.compile_match_body(&arm.body)?;
1456                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1457
1458                            // All failures jump here: pop the false bool
1459                            let fail_target = self.chunk.code.len();
1460                            self.chunk.emit(Op::Pop, self.line); // pop bool
1461                            for skip in constraint_skips {
1462                                self.chunk.patch_jump_to(skip, fail_target);
1463                            }
1464                            self.chunk.patch_jump_to(skip_len, fail_target);
1465                            self.chunk.patch_jump_to(skip_type, fail_target);
1466                        }
1467                        // Literal/expression pattern — compare with Equal
1468                        _ => {
1469                            self.chunk.emit(Op::Dup, self.line);
1470                            self.compile_node(&arm.pattern)?;
1471                            self.chunk.emit(Op::Equal, self.line);
1472                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1473                            self.chunk.emit(Op::Pop, self.line); // pop bool
1474                            self.chunk.emit(Op::Pop, self.line); // pop match value
1475                            self.compile_match_body(&arm.body)?;
1476                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1477                            self.chunk.patch_jump(skip);
1478                            self.chunk.emit(Op::Pop, self.line); // pop bool
1479                        }
1480                    }
1481                }
1482                // No match — pop value, push nil
1483                self.chunk.emit(Op::Pop, self.line);
1484                self.chunk.emit(Op::Nil, self.line);
1485                for j in end_jumps {
1486                    self.chunk.patch_jump(j);
1487                }
1488            }
1489
1490            Node::RangeExpr {
1491                start,
1492                end,
1493                inclusive,
1494            } => {
1495                // Compile as __range__(start, end, inclusive_bool) builtin call
1496                let name_idx = self
1497                    .chunk
1498                    .add_constant(Constant::String("__range__".to_string()));
1499                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
1500                self.compile_node(start)?;
1501                self.compile_node(end)?;
1502                if *inclusive {
1503                    self.chunk.emit(Op::True, self.line);
1504                } else {
1505                    self.chunk.emit(Op::False, self.line);
1506                }
1507                self.chunk.emit_u8(Op::Call, 3, self.line);
1508            }
1509
1510            Node::GuardStmt {
1511                condition,
1512                else_body,
1513            } => {
1514                // guard condition else { body }
1515                // Compile condition; if truthy, skip else_body
1516                self.compile_node(condition)?;
1517                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
1518                self.chunk.emit(Op::Pop, self.line); // pop condition
1519                                                     // Compile else_body
1520                self.compile_block(else_body)?;
1521                // Pop result of else_body (guard is a statement, not expression)
1522                if !else_body.is_empty() && Self::produces_value(&else_body.last().unwrap().node) {
1523                    self.chunk.emit(Op::Pop, self.line);
1524                }
1525                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1526                self.chunk.patch_jump(skip_jump);
1527                self.chunk.emit(Op::Pop, self.line); // pop condition
1528                self.chunk.patch_jump(end_jump);
1529                self.chunk.emit(Op::Nil, self.line);
1530            }
1531
1532            Node::Block(stmts) => {
1533                if stmts.is_empty() {
1534                    self.chunk.emit(Op::Nil, self.line);
1535                } else {
1536                    self.compile_block(stmts)?;
1537                }
1538            }
1539
1540            Node::DeadlineBlock { duration, body } => {
1541                self.compile_node(duration)?;
1542                self.chunk.emit(Op::DeadlineSetup, self.line);
1543                if body.is_empty() {
1544                    self.chunk.emit(Op::Nil, self.line);
1545                } else {
1546                    self.compile_block(body)?;
1547                }
1548                self.chunk.emit(Op::DeadlineEnd, self.line);
1549            }
1550
1551            Node::MutexBlock { body } => {
1552                // v1: single-threaded, just compile the body
1553                if body.is_empty() {
1554                    self.chunk.emit(Op::Nil, self.line);
1555                } else {
1556                    // Compile body, but pop intermediate values and push nil at the end.
1557                    // The body typically contains statements (assignments) that don't produce values.
1558                    for sn in body {
1559                        self.compile_node(sn)?;
1560                        if Self::produces_value(&sn.node) {
1561                            self.chunk.emit(Op::Pop, self.line);
1562                        }
1563                    }
1564                    self.chunk.emit(Op::Nil, self.line);
1565                }
1566            }
1567
1568            Node::YieldExpr { .. } => {
1569                // v1: yield is host-integration only, emit nil
1570                self.chunk.emit(Op::Nil, self.line);
1571            }
1572
1573            Node::AskExpr { fields } => {
1574                // Compile as a dict literal and call llm_call builtin
1575                // For v1, just build the dict (llm_call requires async)
1576                for entry in fields {
1577                    self.compile_node(&entry.key)?;
1578                    self.compile_node(&entry.value)?;
1579                }
1580                self.chunk
1581                    .emit_u16(Op::BuildDict, fields.len() as u16, self.line);
1582            }
1583
1584            Node::EnumConstruct {
1585                enum_name,
1586                variant,
1587                args,
1588            } => {
1589                // Push field values onto the stack, then BuildEnum
1590                for arg in args {
1591                    self.compile_node(arg)?;
1592                }
1593                let enum_idx = self.chunk.add_constant(Constant::String(enum_name.clone()));
1594                let var_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1595                // BuildEnum: enum_name_idx, variant_idx, field_count
1596                self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
1597                let hi = (var_idx >> 8) as u8;
1598                let lo = var_idx as u8;
1599                self.chunk.code.push(hi);
1600                self.chunk.code.push(lo);
1601                self.chunk.lines.push(self.line);
1602                self.chunk.columns.push(self.column);
1603                self.chunk.lines.push(self.line);
1604                self.chunk.columns.push(self.column);
1605                let fc = args.len() as u16;
1606                let fhi = (fc >> 8) as u8;
1607                let flo = fc as u8;
1608                self.chunk.code.push(fhi);
1609                self.chunk.code.push(flo);
1610                self.chunk.lines.push(self.line);
1611                self.chunk.columns.push(self.column);
1612                self.chunk.lines.push(self.line);
1613                self.chunk.columns.push(self.column);
1614            }
1615
1616            Node::StructConstruct {
1617                struct_name,
1618                fields,
1619            } => {
1620                // Build as a dict with a __struct__ key for metadata
1621                let struct_key = self
1622                    .chunk
1623                    .add_constant(Constant::String("__struct__".to_string()));
1624                let struct_val = self
1625                    .chunk
1626                    .add_constant(Constant::String(struct_name.clone()));
1627                self.chunk.emit_u16(Op::Constant, struct_key, self.line);
1628                self.chunk.emit_u16(Op::Constant, struct_val, self.line);
1629
1630                for entry in fields {
1631                    self.compile_node(&entry.key)?;
1632                    self.compile_node(&entry.value)?;
1633                }
1634                self.chunk
1635                    .emit_u16(Op::BuildDict, (fields.len() + 1) as u16, self.line);
1636            }
1637
1638            Node::ImportDecl { path } => {
1639                let idx = self.chunk.add_constant(Constant::String(path.clone()));
1640                self.chunk.emit_u16(Op::Import, idx, self.line);
1641            }
1642
1643            Node::SelectiveImport { names, path } => {
1644                let path_idx = self.chunk.add_constant(Constant::String(path.clone()));
1645                let names_str = names.join(",");
1646                let names_idx = self.chunk.add_constant(Constant::String(names_str));
1647                self.chunk
1648                    .emit_u16(Op::SelectiveImport, path_idx, self.line);
1649                let hi = (names_idx >> 8) as u8;
1650                let lo = names_idx as u8;
1651                self.chunk.code.push(hi);
1652                self.chunk.code.push(lo);
1653                self.chunk.lines.push(self.line);
1654                self.chunk.columns.push(self.column);
1655                self.chunk.lines.push(self.line);
1656                self.chunk.columns.push(self.column);
1657            }
1658
1659            Node::TryOperator { operand } => {
1660                self.compile_node(operand)?;
1661                self.chunk.emit(Op::TryUnwrap, self.line);
1662            }
1663
1664            Node::ImplBlock { type_name, methods } => {
1665                // Compile each method as a closure and store in __impl_TypeName dict.
1666                // Build key-value pairs on stack, then BuildDict.
1667                for method_sn in methods {
1668                    if let Node::FnDecl {
1669                        name, params, body, ..
1670                    } = &method_sn.node
1671                    {
1672                        // Method name key
1673                        let key_idx = self.chunk.add_constant(Constant::String(name.clone()));
1674                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1675
1676                        // Compile method body as closure
1677                        let mut fn_compiler = Compiler::new();
1678                        fn_compiler.enum_names = self.enum_names.clone();
1679                        fn_compiler.emit_default_preamble(params)?;
1680                        fn_compiler.emit_type_checks(params);
1681                        fn_compiler.compile_block(body)?;
1682                        fn_compiler.chunk.emit(Op::Nil, self.line);
1683                        fn_compiler.chunk.emit(Op::Return, self.line);
1684
1685                        let func = CompiledFunction {
1686                            name: format!("{}.{}", type_name, name),
1687                            params: TypedParam::names(params),
1688                            default_start: TypedParam::default_start(params),
1689                            chunk: fn_compiler.chunk,
1690                        };
1691                        let fn_idx = self.chunk.functions.len();
1692                        self.chunk.functions.push(func);
1693                        self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1694                    }
1695                }
1696                let method_count = methods
1697                    .iter()
1698                    .filter(|m| matches!(m.node, Node::FnDecl { .. }))
1699                    .count();
1700                self.chunk
1701                    .emit_u16(Op::BuildDict, method_count as u16, self.line);
1702                let impl_name = format!("__impl_{}", type_name);
1703                let name_idx = self.chunk.add_constant(Constant::String(impl_name));
1704                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1705            }
1706
1707            Node::StructDecl { name, .. } => {
1708                // Compile a constructor function: StructName({field: val, ...}) -> StructInstance
1709                let mut fn_compiler = Compiler::new();
1710                fn_compiler.enum_names = self.enum_names.clone();
1711                let params = vec![TypedParam::untyped("__fields")];
1712                fn_compiler.emit_default_preamble(&params)?;
1713
1714                // Call __make_struct(struct_name, fields_dict) to tag the dict
1715                let make_idx = fn_compiler
1716                    .chunk
1717                    .add_constant(Constant::String("__make_struct".into()));
1718                fn_compiler
1719                    .chunk
1720                    .emit_u16(Op::Constant, make_idx, self.line);
1721                let sname_idx = fn_compiler
1722                    .chunk
1723                    .add_constant(Constant::String(name.clone()));
1724                fn_compiler
1725                    .chunk
1726                    .emit_u16(Op::Constant, sname_idx, self.line);
1727                let fields_idx = fn_compiler
1728                    .chunk
1729                    .add_constant(Constant::String("__fields".into()));
1730                fn_compiler
1731                    .chunk
1732                    .emit_u16(Op::GetVar, fields_idx, self.line);
1733                fn_compiler.chunk.emit_u8(Op::Call, 2, self.line);
1734                fn_compiler.chunk.emit(Op::Return, self.line);
1735
1736                let func = CompiledFunction {
1737                    name: name.clone(),
1738                    params: TypedParam::names(&params),
1739                    default_start: None,
1740                    chunk: fn_compiler.chunk,
1741                };
1742                let fn_idx = self.chunk.functions.len();
1743                self.chunk.functions.push(func);
1744                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1745                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1746                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1747            }
1748
1749            // Declarations that only register metadata (no runtime effect needed for v1)
1750            Node::Pipeline { .. }
1751            | Node::OverrideDecl { .. }
1752            | Node::TypeDecl { .. }
1753            | Node::EnumDecl { .. }
1754            | Node::InterfaceDecl { .. } => {
1755                self.chunk.emit(Op::Nil, self.line);
1756            }
1757
1758            Node::TryCatch {
1759                body,
1760                error_var,
1761                error_type,
1762                catch_body,
1763                finally_body,
1764            } => {
1765                // Extract the type name for typed catch (e.g., "AppError")
1766                let type_name = error_type.as_ref().and_then(|te| {
1767                    if let harn_parser::TypeExpr::Named(name) = te {
1768                        Some(name.clone())
1769                    } else {
1770                        None
1771                    }
1772                });
1773
1774                let type_name_idx = if let Some(ref tn) = type_name {
1775                    self.chunk.add_constant(Constant::String(tn.clone()))
1776                } else {
1777                    self.chunk.add_constant(Constant::String(String::new()))
1778                };
1779
1780                let has_catch = !catch_body.is_empty() || error_var.is_some();
1781                let has_finally = finally_body.is_some();
1782
1783                if has_catch && has_finally {
1784                    // === try-catch-finally ===
1785                    let finally_body = finally_body.as_ref().unwrap();
1786
1787                    // Push finally body onto pending stack for return/break handling
1788                    self.finally_bodies.push(finally_body.clone());
1789
1790                    // 1. TryCatchSetup for try body
1791                    self.handler_depth += 1;
1792                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1793                    self.emit_type_name_extra(type_name_idx);
1794
1795                    // 2. Compile try body
1796                    self.compile_try_body(body)?;
1797
1798                    // 3. PopHandler + inline finally (success path)
1799                    self.handler_depth -= 1;
1800                    self.chunk.emit(Op::PopHandler, self.line);
1801                    self.compile_finally_inline(finally_body)?;
1802                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1803
1804                    // 4. Catch entry
1805                    self.chunk.patch_jump(catch_jump);
1806                    self.compile_catch_binding(error_var)?;
1807
1808                    // 5. Inner try around catch body (so finally runs if catch throws)
1809                    self.handler_depth += 1;
1810                    let rethrow_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1811                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1812                    self.emit_type_name_extra(empty_type);
1813
1814                    // 6. Compile catch body
1815                    self.compile_try_body(catch_body)?;
1816
1817                    // 7. PopHandler + inline finally (catch success path)
1818                    self.handler_depth -= 1;
1819                    self.chunk.emit(Op::PopHandler, self.line);
1820                    self.compile_finally_inline(finally_body)?;
1821                    let end_jump2 = self.chunk.emit_jump(Op::Jump, self.line);
1822
1823                    // 8. Rethrow handler: save error, run finally, re-throw
1824                    self.chunk.patch_jump(rethrow_jump);
1825                    self.compile_rethrow_with_finally(finally_body)?;
1826
1827                    self.chunk.patch_jump(end_jump);
1828                    self.chunk.patch_jump(end_jump2);
1829
1830                    self.finally_bodies.pop();
1831                } else if has_finally {
1832                    // === try-finally (no catch) ===
1833                    let finally_body = finally_body.as_ref().unwrap();
1834
1835                    self.finally_bodies.push(finally_body.clone());
1836
1837                    // 1. TryCatchSetup to error path
1838                    self.handler_depth += 1;
1839                    let error_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1840                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1841                    self.emit_type_name_extra(empty_type);
1842
1843                    // 2. Compile try body
1844                    self.compile_try_body(body)?;
1845
1846                    // 3. PopHandler + inline finally (success path)
1847                    self.handler_depth -= 1;
1848                    self.chunk.emit(Op::PopHandler, self.line);
1849                    self.compile_finally_inline(finally_body)?;
1850                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1851
1852                    // 4. Error path: save error, run finally, re-throw
1853                    self.chunk.patch_jump(error_jump);
1854                    self.compile_rethrow_with_finally(finally_body)?;
1855
1856                    self.chunk.patch_jump(end_jump);
1857
1858                    self.finally_bodies.pop();
1859                } else {
1860                    // === try-catch (no finally) — original behavior ===
1861
1862                    // 1. TryCatchSetup
1863                    self.handler_depth += 1;
1864                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1865                    self.emit_type_name_extra(type_name_idx);
1866
1867                    // 2. Compile try body
1868                    self.compile_try_body(body)?;
1869
1870                    // 3. PopHandler + jump past catch
1871                    self.handler_depth -= 1;
1872                    self.chunk.emit(Op::PopHandler, self.line);
1873                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1874
1875                    // 4. Catch entry
1876                    self.chunk.patch_jump(catch_jump);
1877                    self.compile_catch_binding(error_var)?;
1878
1879                    // 5. Compile catch body
1880                    self.compile_try_body(catch_body)?;
1881
1882                    // 6. Patch end
1883                    self.chunk.patch_jump(end_jump);
1884                }
1885            }
1886
1887            Node::TryExpr { body } => {
1888                // try { body } — returns Result.Ok(value) or Result.Err(error)
1889
1890                // 1. Set up try-catch handler (untyped)
1891                self.handler_depth += 1;
1892                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1893                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1894                self.emit_type_name_extra(empty_type);
1895
1896                // 2. Compile try body (leaves value on stack)
1897                self.compile_try_body(body)?;
1898
1899                // 3. PopHandler (success path)
1900                self.handler_depth -= 1;
1901                self.chunk.emit(Op::PopHandler, self.line);
1902
1903                // 4. Wrap in Result.Ok: push "Ok", swap, call Ok(value)
1904                let ok_idx = self.chunk.add_constant(Constant::String("Ok".to_string()));
1905                self.chunk.emit_u16(Op::Constant, ok_idx, self.line);
1906                self.chunk.emit(Op::Swap, self.line);
1907                self.chunk.emit_u8(Op::Call, 1, self.line);
1908
1909                // 5. Jump past error handler
1910                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1911
1912                // 6. Error handler: error value is on stack
1913                self.chunk.patch_jump(catch_jump);
1914
1915                // 7. Wrap in Result.Err: push "Err", swap, call Err(error)
1916                let err_idx = self.chunk.add_constant(Constant::String("Err".to_string()));
1917                self.chunk.emit_u16(Op::Constant, err_idx, self.line);
1918                self.chunk.emit(Op::Swap, self.line);
1919                self.chunk.emit_u8(Op::Call, 1, self.line);
1920
1921                // 8. Patch end
1922                self.chunk.patch_jump(end_jump);
1923            }
1924
1925            Node::Retry { count, body } => {
1926                // Compile count expression into a mutable counter variable
1927                self.compile_node(count)?;
1928                let counter_name = "__retry_counter__";
1929                let counter_idx = self
1930                    .chunk
1931                    .add_constant(Constant::String(counter_name.to_string()));
1932                self.chunk.emit_u16(Op::DefVar, counter_idx, self.line);
1933
1934                // Also store the last error for re-throwing
1935                self.chunk.emit(Op::Nil, self.line);
1936                let err_name = "__retry_last_error__";
1937                let err_idx = self
1938                    .chunk
1939                    .add_constant(Constant::String(err_name.to_string()));
1940                self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
1941
1942                // Loop start
1943                let loop_start = self.chunk.current_offset();
1944
1945                // Set up try/catch (untyped - empty type name)
1946                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1947                // Emit empty type name for untyped catch
1948                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1949                let hi = (empty_type >> 8) as u8;
1950                let lo = empty_type as u8;
1951                self.chunk.code.push(hi);
1952                self.chunk.code.push(lo);
1953                self.chunk.lines.push(self.line);
1954                self.chunk.columns.push(self.column);
1955                self.chunk.lines.push(self.line);
1956                self.chunk.columns.push(self.column);
1957
1958                // Compile body
1959                self.compile_block(body)?;
1960
1961                // Success: pop handler, jump to end
1962                self.chunk.emit(Op::PopHandler, self.line);
1963                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1964
1965                // Catch handler
1966                self.chunk.patch_jump(catch_jump);
1967                // Save the error value for potential re-throw
1968                self.chunk.emit(Op::Dup, self.line);
1969                self.chunk.emit_u16(Op::SetVar, err_idx, self.line);
1970                // Pop the error value
1971                self.chunk.emit(Op::Pop, self.line);
1972
1973                // Decrement counter
1974                self.chunk.emit_u16(Op::GetVar, counter_idx, self.line);
1975                let one_idx = self.chunk.add_constant(Constant::Int(1));
1976                self.chunk.emit_u16(Op::Constant, one_idx, self.line);
1977                self.chunk.emit(Op::Sub, self.line);
1978                self.chunk.emit(Op::Dup, self.line);
1979                self.chunk.emit_u16(Op::SetVar, counter_idx, self.line);
1980
1981                // If counter > 0, jump to loop start
1982                let zero_idx = self.chunk.add_constant(Constant::Int(0));
1983                self.chunk.emit_u16(Op::Constant, zero_idx, self.line);
1984                self.chunk.emit(Op::Greater, self.line);
1985                let retry_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1986                self.chunk.emit(Op::Pop, self.line); // pop condition
1987                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
1988
1989                // No more retries — re-throw the last error
1990                self.chunk.patch_jump(retry_jump);
1991                self.chunk.emit(Op::Pop, self.line); // pop condition
1992                self.chunk.emit_u16(Op::GetVar, err_idx, self.line);
1993                self.chunk.emit(Op::Throw, self.line);
1994
1995                self.chunk.patch_jump(end_jump);
1996                // Push nil as the result of a successful retry block
1997                self.chunk.emit(Op::Nil, self.line);
1998            }
1999
2000            Node::Parallel {
2001                count,
2002                variable,
2003                body,
2004            } => {
2005                self.compile_node(count)?;
2006                let mut fn_compiler = Compiler::new();
2007                fn_compiler.enum_names = self.enum_names.clone();
2008                fn_compiler.compile_block(body)?;
2009                fn_compiler.chunk.emit(Op::Return, self.line);
2010                let params = vec![variable.clone().unwrap_or_else(|| "__i__".to_string())];
2011                let func = CompiledFunction {
2012                    name: "<parallel>".to_string(),
2013                    params,
2014                    default_start: None,
2015                    chunk: fn_compiler.chunk,
2016                };
2017                let fn_idx = self.chunk.functions.len();
2018                self.chunk.functions.push(func);
2019                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2020                self.chunk.emit(Op::Parallel, self.line);
2021            }
2022
2023            Node::ParallelMap {
2024                list,
2025                variable,
2026                body,
2027            } => {
2028                self.compile_node(list)?;
2029                let mut fn_compiler = Compiler::new();
2030                fn_compiler.enum_names = self.enum_names.clone();
2031                fn_compiler.compile_block(body)?;
2032                fn_compiler.chunk.emit(Op::Return, self.line);
2033                let func = CompiledFunction {
2034                    name: "<parallel_map>".to_string(),
2035                    params: vec![variable.clone()],
2036                    default_start: None,
2037                    chunk: fn_compiler.chunk,
2038                };
2039                let fn_idx = self.chunk.functions.len();
2040                self.chunk.functions.push(func);
2041                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2042                self.chunk.emit(Op::ParallelMap, self.line);
2043            }
2044
2045            Node::SpawnExpr { body } => {
2046                let mut fn_compiler = Compiler::new();
2047                fn_compiler.enum_names = self.enum_names.clone();
2048                fn_compiler.compile_block(body)?;
2049                fn_compiler.chunk.emit(Op::Return, self.line);
2050                let func = CompiledFunction {
2051                    name: "<spawn>".to_string(),
2052                    params: vec![],
2053                    default_start: None,
2054                    chunk: fn_compiler.chunk,
2055                };
2056                let fn_idx = self.chunk.functions.len();
2057                self.chunk.functions.push(func);
2058                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2059                self.chunk.emit(Op::Spawn, self.line);
2060            }
2061            Node::SelectExpr {
2062                cases,
2063                timeout,
2064                default_body,
2065            } => {
2066                // Desugar select into: builtin call + index-based dispatch.
2067                //
2068                // Step 1: Push builtin name, compile channel list, optionally
2069                //         compile timeout duration, then Call.
2070                // Step 2: Store result dict in temp, dispatch on result.index.
2071
2072                let builtin_name = if timeout.is_some() {
2073                    "__select_timeout"
2074                } else if default_body.is_some() {
2075                    "__select_try"
2076                } else {
2077                    "__select_list"
2078                };
2079
2080                // Push builtin name (callee goes below args on stack)
2081                let name_idx = self
2082                    .chunk
2083                    .add_constant(Constant::String(builtin_name.into()));
2084                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
2085
2086                // Build channel list (arg 1)
2087                for case in cases {
2088                    self.compile_node(&case.channel)?;
2089                }
2090                self.chunk
2091                    .emit_u16(Op::BuildList, cases.len() as u16, self.line);
2092
2093                // If timeout, compile duration (arg 2)
2094                if let Some((duration_expr, _)) = timeout {
2095                    self.compile_node(duration_expr)?;
2096                    self.chunk.emit_u8(Op::Call, 2, self.line);
2097                } else {
2098                    self.chunk.emit_u8(Op::Call, 1, self.line);
2099                }
2100
2101                // Store result in temp var
2102                self.temp_counter += 1;
2103                let result_name = format!("__sel_result_{}__", self.temp_counter);
2104                let result_idx = self
2105                    .chunk
2106                    .add_constant(Constant::String(result_name.clone()));
2107                self.chunk.emit_u16(Op::DefVar, result_idx, self.line);
2108
2109                // Dispatch on result.index
2110                let mut end_jumps = Vec::new();
2111
2112                for (i, case) in cases.iter().enumerate() {
2113                    let get_r = self
2114                        .chunk
2115                        .add_constant(Constant::String(result_name.clone()));
2116                    self.chunk.emit_u16(Op::GetVar, get_r, self.line);
2117                    let idx_prop = self.chunk.add_constant(Constant::String("index".into()));
2118                    self.chunk.emit_u16(Op::GetProperty, idx_prop, self.line);
2119                    let case_i = self.chunk.add_constant(Constant::Int(i as i64));
2120                    self.chunk.emit_u16(Op::Constant, case_i, self.line);
2121                    self.chunk.emit(Op::Equal, self.line);
2122                    let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2123                    self.chunk.emit(Op::Pop, self.line);
2124
2125                    // Bind variable = result.value
2126                    let get_r2 = self
2127                        .chunk
2128                        .add_constant(Constant::String(result_name.clone()));
2129                    self.chunk.emit_u16(Op::GetVar, get_r2, self.line);
2130                    let val_prop = self.chunk.add_constant(Constant::String("value".into()));
2131                    self.chunk.emit_u16(Op::GetProperty, val_prop, self.line);
2132                    let var_idx = self
2133                        .chunk
2134                        .add_constant(Constant::String(case.variable.clone()));
2135                    self.chunk.emit_u16(Op::DefLet, var_idx, self.line);
2136
2137                    self.compile_try_body(&case.body)?;
2138                    end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2139                    self.chunk.patch_jump(skip);
2140                    self.chunk.emit(Op::Pop, self.line);
2141                }
2142
2143                // Timeout/default fallthrough (index == -1)
2144                if let Some((_, ref timeout_body)) = timeout {
2145                    self.compile_try_body(timeout_body)?;
2146                } else if let Some(ref def_body) = default_body {
2147                    self.compile_try_body(def_body)?;
2148                } else {
2149                    self.chunk.emit(Op::Nil, self.line);
2150                }
2151
2152                for ej in end_jumps {
2153                    self.chunk.patch_jump(ej);
2154                }
2155            }
2156            Node::Spread(_) => {
2157                return Err(CompileError {
2158                    message: "spread (...) can only be used inside list literals, dict literals, or function call arguments".into(),
2159                    line: self.line,
2160                });
2161            }
2162        }
2163        Ok(())
2164    }
2165
2166    /// Compile a destructuring binding pattern.
2167    /// Expects the RHS value to already be on the stack.
2168    /// After this, the value is consumed (popped) and each binding is defined.
2169    fn compile_destructuring(
2170        &mut self,
2171        pattern: &BindingPattern,
2172        is_mutable: bool,
2173    ) -> Result<(), CompileError> {
2174        let def_op = if is_mutable { Op::DefVar } else { Op::DefLet };
2175        match pattern {
2176            BindingPattern::Identifier(name) => {
2177                // Simple case: just define the variable
2178                let idx = self.chunk.add_constant(Constant::String(name.clone()));
2179                self.chunk.emit_u16(def_op, idx, self.line);
2180            }
2181            BindingPattern::Dict(fields) => {
2182                // Stack has the dict value.
2183                // Emit runtime type check: __assert_dict(value)
2184                self.chunk.emit(Op::Dup, self.line);
2185                let assert_idx = self
2186                    .chunk
2187                    .add_constant(Constant::String("__assert_dict".into()));
2188                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
2189                self.chunk.emit(Op::Swap, self.line);
2190                self.chunk.emit_u8(Op::Call, 1, self.line);
2191                self.chunk.emit(Op::Pop, self.line); // discard nil result
2192
2193                // For each non-rest field: dup dict, push key string, subscript, define var.
2194                // For rest field: dup dict, call __dict_rest builtin.
2195                let non_rest: Vec<_> = fields.iter().filter(|f| !f.is_rest).collect();
2196                let rest_field = fields.iter().find(|f| f.is_rest);
2197
2198                for field in &non_rest {
2199                    self.chunk.emit(Op::Dup, self.line);
2200                    let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
2201                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2202                    self.chunk.emit(Op::Subscript, self.line);
2203                    let binding_name = field.alias.as_deref().unwrap_or(&field.key);
2204                    let name_idx = self
2205                        .chunk
2206                        .add_constant(Constant::String(binding_name.to_string()));
2207                    self.chunk.emit_u16(def_op, name_idx, self.line);
2208                }
2209
2210                if let Some(rest) = rest_field {
2211                    // Call the __dict_rest builtin: __dict_rest(dict, [keys_to_exclude])
2212                    // Push function name
2213                    let fn_idx = self
2214                        .chunk
2215                        .add_constant(Constant::String("__dict_rest".into()));
2216                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
2217                    // Swap so dict is above function name: [fn, dict]
2218                    self.chunk.emit(Op::Swap, self.line);
2219                    // Build the exclusion keys list
2220                    for field in &non_rest {
2221                        let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
2222                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2223                    }
2224                    self.chunk
2225                        .emit_u16(Op::BuildList, non_rest.len() as u16, self.line);
2226                    // Call __dict_rest(dict, keys_list) — 2 args
2227                    self.chunk.emit_u8(Op::Call, 2, self.line);
2228                    let rest_name = &rest.key;
2229                    let rest_idx = self.chunk.add_constant(Constant::String(rest_name.clone()));
2230                    self.chunk.emit_u16(def_op, rest_idx, self.line);
2231                } else {
2232                    // Pop the source dict
2233                    self.chunk.emit(Op::Pop, self.line);
2234                }
2235            }
2236            BindingPattern::List(elements) => {
2237                // Stack has the list value.
2238                // Emit runtime type check: __assert_list(value)
2239                self.chunk.emit(Op::Dup, self.line);
2240                let assert_idx = self
2241                    .chunk
2242                    .add_constant(Constant::String("__assert_list".into()));
2243                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
2244                self.chunk.emit(Op::Swap, self.line);
2245                self.chunk.emit_u8(Op::Call, 1, self.line);
2246                self.chunk.emit(Op::Pop, self.line); // discard nil result
2247
2248                let non_rest: Vec<_> = elements.iter().filter(|e| !e.is_rest).collect();
2249                let rest_elem = elements.iter().find(|e| e.is_rest);
2250
2251                for (i, elem) in non_rest.iter().enumerate() {
2252                    self.chunk.emit(Op::Dup, self.line);
2253                    let idx_const = self.chunk.add_constant(Constant::Int(i as i64));
2254                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
2255                    self.chunk.emit(Op::Subscript, self.line);
2256                    let name_idx = self.chunk.add_constant(Constant::String(elem.name.clone()));
2257                    self.chunk.emit_u16(def_op, name_idx, self.line);
2258                }
2259
2260                if let Some(rest) = rest_elem {
2261                    // Slice the list from index non_rest.len() to end: list[n..]
2262                    // Slice op takes: object, start, end on stack
2263                    // self.chunk.emit(Op::Dup, self.line); -- list is still on stack
2264                    let start_idx = self
2265                        .chunk
2266                        .add_constant(Constant::Int(non_rest.len() as i64));
2267                    self.chunk.emit_u16(Op::Constant, start_idx, self.line);
2268                    self.chunk.emit(Op::Nil, self.line); // end = nil (to end)
2269                    self.chunk.emit(Op::Slice, self.line);
2270                    let rest_name_idx =
2271                        self.chunk.add_constant(Constant::String(rest.name.clone()));
2272                    self.chunk.emit_u16(def_op, rest_name_idx, self.line);
2273                } else {
2274                    // Pop the source list
2275                    self.chunk.emit(Op::Pop, self.line);
2276                }
2277            }
2278        }
2279        Ok(())
2280    }
2281
2282    /// Check if a node produces a value on the stack that needs to be popped.
2283    fn produces_value(node: &Node) -> bool {
2284        match node {
2285            // These nodes do NOT produce a value on the stack
2286            Node::LetBinding { .. }
2287            | Node::VarBinding { .. }
2288            | Node::Assignment { .. }
2289            | Node::ReturnStmt { .. }
2290            | Node::FnDecl { .. }
2291            | Node::ImplBlock { .. }
2292            | Node::StructDecl { .. }
2293            | Node::ThrowStmt { .. }
2294            | Node::BreakStmt
2295            | Node::ContinueStmt => false,
2296            // These compound nodes explicitly produce a value
2297            Node::TryCatch { .. }
2298            | Node::TryExpr { .. }
2299            | Node::Retry { .. }
2300            | Node::GuardStmt { .. }
2301            | Node::DeadlineBlock { .. }
2302            | Node::MutexBlock { .. }
2303            | Node::Spread(_) => true,
2304            // All other expressions produce values
2305            _ => true,
2306        }
2307    }
2308}
2309
2310impl Compiler {
2311    /// Compile a function body into a CompiledFunction (for import support).
2312    pub fn compile_fn_body(
2313        &mut self,
2314        params: &[TypedParam],
2315        body: &[SNode],
2316    ) -> Result<CompiledFunction, CompileError> {
2317        let mut fn_compiler = Compiler::new();
2318        fn_compiler.compile_block(body)?;
2319        fn_compiler.chunk.emit(Op::Nil, 0);
2320        fn_compiler.chunk.emit(Op::Return, 0);
2321        Ok(CompiledFunction {
2322            name: String::new(),
2323            params: TypedParam::names(params),
2324            default_start: TypedParam::default_start(params),
2325            chunk: fn_compiler.chunk,
2326        })
2327    }
2328
2329    /// Compile a match arm body, ensuring it always pushes exactly one value.
2330    fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
2331        if body.is_empty() {
2332            self.chunk.emit(Op::Nil, self.line);
2333        } else {
2334            self.compile_block(body)?;
2335            // If the last statement doesn't produce a value, push nil
2336            if !Self::produces_value(&body.last().unwrap().node) {
2337                self.chunk.emit(Op::Nil, self.line);
2338            }
2339        }
2340        Ok(())
2341    }
2342
2343    /// Emit the binary op instruction for a compound assignment operator.
2344    fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
2345        match op {
2346            "+" => self.chunk.emit(Op::Add, self.line),
2347            "-" => self.chunk.emit(Op::Sub, self.line),
2348            "*" => self.chunk.emit(Op::Mul, self.line),
2349            "/" => self.chunk.emit(Op::Div, self.line),
2350            "%" => self.chunk.emit(Op::Mod, self.line),
2351            _ => {
2352                return Err(CompileError {
2353                    message: format!("Unknown compound operator: {op}"),
2354                    line: self.line,
2355                })
2356            }
2357        }
2358        Ok(())
2359    }
2360
2361    /// Extract the root variable name from a (possibly nested) access expression.
2362    fn root_var_name(&self, node: &SNode) -> Option<String> {
2363        match &node.node {
2364            Node::Identifier(name) => Some(name.clone()),
2365            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
2366                self.root_var_name(object)
2367            }
2368            Node::SubscriptAccess { object, .. } => self.root_var_name(object),
2369            _ => None,
2370        }
2371    }
2372}
2373
2374impl Compiler {
2375    /// Recursively collect all enum type names from the AST.
2376    fn collect_enum_names(nodes: &[SNode], names: &mut std::collections::HashSet<String>) {
2377        for sn in nodes {
2378            match &sn.node {
2379                Node::EnumDecl { name, .. } => {
2380                    names.insert(name.clone());
2381                }
2382                Node::Pipeline { body, .. } => {
2383                    Self::collect_enum_names(body, names);
2384                }
2385                Node::FnDecl { body, .. } => {
2386                    Self::collect_enum_names(body, names);
2387                }
2388                Node::Block(stmts) => {
2389                    Self::collect_enum_names(stmts, names);
2390                }
2391                _ => {}
2392            }
2393        }
2394    }
2395}
2396
2397impl Default for Compiler {
2398    fn default() -> Self {
2399        Self::new()
2400    }
2401}
2402
2403/// Check if an AST node contains `_` identifier (pipe placeholder).
2404fn contains_pipe_placeholder(node: &SNode) -> bool {
2405    match &node.node {
2406        Node::Identifier(name) if name == "_" => true,
2407        Node::FunctionCall { args, .. } => args.iter().any(contains_pipe_placeholder),
2408        Node::MethodCall { object, args, .. } => {
2409            contains_pipe_placeholder(object) || args.iter().any(contains_pipe_placeholder)
2410        }
2411        Node::BinaryOp { left, right, .. } => {
2412            contains_pipe_placeholder(left) || contains_pipe_placeholder(right)
2413        }
2414        Node::UnaryOp { operand, .. } => contains_pipe_placeholder(operand),
2415        Node::ListLiteral(items) => items.iter().any(contains_pipe_placeholder),
2416        Node::PropertyAccess { object, .. } => contains_pipe_placeholder(object),
2417        Node::SubscriptAccess { object, index } => {
2418            contains_pipe_placeholder(object) || contains_pipe_placeholder(index)
2419        }
2420        _ => false,
2421    }
2422}
2423
2424/// Replace all `_` identifiers with `__pipe` in an AST node (for pipe placeholder desugaring).
2425fn replace_pipe_placeholder(node: &SNode) -> SNode {
2426    let new_node = match &node.node {
2427        Node::Identifier(name) if name == "_" => Node::Identifier("__pipe".into()),
2428        Node::FunctionCall { name, args } => Node::FunctionCall {
2429            name: name.clone(),
2430            args: args.iter().map(replace_pipe_placeholder).collect(),
2431        },
2432        Node::MethodCall {
2433            object,
2434            method,
2435            args,
2436        } => Node::MethodCall {
2437            object: Box::new(replace_pipe_placeholder(object)),
2438            method: method.clone(),
2439            args: args.iter().map(replace_pipe_placeholder).collect(),
2440        },
2441        Node::BinaryOp { op, left, right } => Node::BinaryOp {
2442            op: op.clone(),
2443            left: Box::new(replace_pipe_placeholder(left)),
2444            right: Box::new(replace_pipe_placeholder(right)),
2445        },
2446        Node::UnaryOp { op, operand } => Node::UnaryOp {
2447            op: op.clone(),
2448            operand: Box::new(replace_pipe_placeholder(operand)),
2449        },
2450        Node::ListLiteral(items) => {
2451            Node::ListLiteral(items.iter().map(replace_pipe_placeholder).collect())
2452        }
2453        Node::PropertyAccess { object, property } => Node::PropertyAccess {
2454            object: Box::new(replace_pipe_placeholder(object)),
2455            property: property.clone(),
2456        },
2457        Node::SubscriptAccess { object, index } => Node::SubscriptAccess {
2458            object: Box::new(replace_pipe_placeholder(object)),
2459            index: Box::new(replace_pipe_placeholder(index)),
2460        },
2461        _ => return node.clone(),
2462    };
2463    SNode::new(new_node, node.span)
2464}
2465
2466#[cfg(test)]
2467mod tests {
2468    use super::*;
2469    use harn_lexer::Lexer;
2470    use harn_parser::Parser;
2471
2472    fn compile_source(source: &str) -> Chunk {
2473        let mut lexer = Lexer::new(source);
2474        let tokens = lexer.tokenize().unwrap();
2475        let mut parser = Parser::new(tokens);
2476        let program = parser.parse().unwrap();
2477        Compiler::new().compile(&program).unwrap()
2478    }
2479
2480    #[test]
2481    fn test_compile_arithmetic() {
2482        let chunk = compile_source("pipeline test(task) { let x = 2 + 3 }");
2483        assert!(!chunk.code.is_empty());
2484        // Should have constants: 2, 3, "x"
2485        assert!(chunk.constants.contains(&Constant::Int(2)));
2486        assert!(chunk.constants.contains(&Constant::Int(3)));
2487    }
2488
2489    #[test]
2490    fn test_compile_function_call() {
2491        let chunk = compile_source("pipeline test(task) { log(42) }");
2492        let disasm = chunk.disassemble("test");
2493        assert!(disasm.contains("CALL"));
2494    }
2495
2496    #[test]
2497    fn test_compile_if_else() {
2498        let chunk =
2499            compile_source(r#"pipeline test(task) { if true { log("yes") } else { log("no") } }"#);
2500        let disasm = chunk.disassemble("test");
2501        assert!(disasm.contains("JUMP_IF_FALSE"));
2502        assert!(disasm.contains("JUMP"));
2503    }
2504
2505    #[test]
2506    fn test_compile_while() {
2507        let chunk = compile_source("pipeline test(task) { var i = 0\n while i < 5 { i = i + 1 } }");
2508        let disasm = chunk.disassemble("test");
2509        assert!(disasm.contains("JUMP_IF_FALSE"));
2510        // Should have a backward jump
2511        assert!(disasm.contains("JUMP"));
2512    }
2513
2514    #[test]
2515    fn test_compile_closure() {
2516        let chunk = compile_source("pipeline test(task) { let f = { x -> x * 2 } }");
2517        assert!(!chunk.functions.is_empty());
2518        assert_eq!(chunk.functions[0].params, vec!["x"]);
2519    }
2520
2521    #[test]
2522    fn test_compile_list() {
2523        let chunk = compile_source("pipeline test(task) { let a = [1, 2, 3] }");
2524        let disasm = chunk.disassemble("test");
2525        assert!(disasm.contains("BUILD_LIST"));
2526    }
2527
2528    #[test]
2529    fn test_compile_dict() {
2530        let chunk = compile_source(r#"pipeline test(task) { let d = {name: "test"} }"#);
2531        let disasm = chunk.disassemble("test");
2532        assert!(disasm.contains("BUILD_DICT"));
2533    }
2534
2535    #[test]
2536    fn test_disassemble() {
2537        let chunk = compile_source("pipeline test(task) { log(2 + 3) }");
2538        let disasm = chunk.disassemble("test");
2539        // Should be readable
2540        assert!(disasm.contains("CONSTANT"));
2541        assert!(disasm.contains("ADD"));
2542        assert!(disasm.contains("CALL"));
2543    }
2544}