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                let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
667                self.compile_node(object)?;
668                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
669                if has_spread {
670                    // Build args into a single list (same pattern as FunctionCall spread)
671                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
672                    let mut pending = 0u16;
673                    for arg in args {
674                        if let Node::Spread(inner) = &arg.node {
675                            if pending > 0 {
676                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
677                                self.chunk.emit(Op::Add, self.line);
678                                pending = 0;
679                            }
680                            self.compile_node(inner)?;
681                            self.chunk.emit(Op::Dup, self.line);
682                            let assert_idx = self
683                                .chunk
684                                .add_constant(Constant::String("__assert_list".into()));
685                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
686                            self.chunk.emit(Op::Swap, self.line);
687                            self.chunk.emit_u8(Op::Call, 1, self.line);
688                            self.chunk.emit(Op::Pop, self.line);
689                            self.chunk.emit(Op::Add, self.line);
690                        } else {
691                            self.compile_node(arg)?;
692                            pending += 1;
693                        }
694                    }
695                    if pending > 0 {
696                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
697                        self.chunk.emit(Op::Add, self.line);
698                    }
699                    self.chunk
700                        .emit_u16(Op::MethodCallSpread, name_idx, self.line);
701                } else {
702                    for arg in args {
703                        self.compile_node(arg)?;
704                    }
705                    self.chunk
706                        .emit_method_call(name_idx, args.len() as u8, self.line);
707                }
708            }
709
710            Node::OptionalMethodCall {
711                object,
712                method,
713                args,
714            } => {
715                self.compile_node(object)?;
716                for arg in args {
717                    self.compile_node(arg)?;
718                }
719                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
720                self.chunk
721                    .emit_method_call_opt(name_idx, args.len() as u8, self.line);
722            }
723
724            Node::PropertyAccess { object, property } => {
725                // Check if this is an enum variant construction: EnumName.Variant
726                if let Node::Identifier(name) = &object.node {
727                    if self.enum_names.contains(name) {
728                        // Emit BuildEnum with 0 fields
729                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
730                        let var_idx = self.chunk.add_constant(Constant::String(property.clone()));
731                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
732                        let hi = (var_idx >> 8) as u8;
733                        let lo = var_idx as u8;
734                        self.chunk.code.push(hi);
735                        self.chunk.code.push(lo);
736                        self.chunk.lines.push(self.line);
737                        self.chunk.columns.push(self.column);
738                        self.chunk.lines.push(self.line);
739                        self.chunk.columns.push(self.column);
740                        // 0 fields
741                        self.chunk.code.push(0);
742                        self.chunk.code.push(0);
743                        self.chunk.lines.push(self.line);
744                        self.chunk.columns.push(self.column);
745                        self.chunk.lines.push(self.line);
746                        self.chunk.columns.push(self.column);
747                        return Ok(());
748                    }
749                }
750                self.compile_node(object)?;
751                let idx = self.chunk.add_constant(Constant::String(property.clone()));
752                self.chunk.emit_u16(Op::GetProperty, idx, self.line);
753            }
754
755            Node::OptionalPropertyAccess { object, property } => {
756                self.compile_node(object)?;
757                let idx = self.chunk.add_constant(Constant::String(property.clone()));
758                self.chunk.emit_u16(Op::GetPropertyOpt, idx, self.line);
759            }
760
761            Node::SubscriptAccess { object, index } => {
762                self.compile_node(object)?;
763                self.compile_node(index)?;
764                self.chunk.emit(Op::Subscript, self.line);
765            }
766
767            Node::SliceAccess { object, start, end } => {
768                self.compile_node(object)?;
769                if let Some(s) = start {
770                    self.compile_node(s)?;
771                } else {
772                    self.chunk.emit(Op::Nil, self.line);
773                }
774                if let Some(e) = end {
775                    self.compile_node(e)?;
776                } else {
777                    self.chunk.emit(Op::Nil, self.line);
778                }
779                self.chunk.emit(Op::Slice, self.line);
780            }
781
782            Node::IfElse {
783                condition,
784                then_body,
785                else_body,
786            } => {
787                self.compile_node(condition)?;
788                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
789                self.chunk.emit(Op::Pop, self.line);
790                self.compile_block(then_body)?;
791                if let Some(else_body) = else_body {
792                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
793                    self.chunk.patch_jump(else_jump);
794                    self.chunk.emit(Op::Pop, self.line);
795                    self.compile_block(else_body)?;
796                    self.chunk.patch_jump(end_jump);
797                } else {
798                    self.chunk.patch_jump(else_jump);
799                    self.chunk.emit(Op::Pop, self.line);
800                    self.chunk.emit(Op::Nil, self.line);
801                }
802            }
803
804            Node::WhileLoop { condition, body } => {
805                let loop_start = self.chunk.current_offset();
806                self.loop_stack.push(LoopContext {
807                    start_offset: loop_start,
808                    break_patches: Vec::new(),
809                    has_iterator: false,
810                    handler_depth: self.handler_depth,
811                    finally_depth: self.finally_bodies.len(),
812                });
813                self.compile_node(condition)?;
814                let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
815                self.chunk.emit(Op::Pop, self.line); // pop condition
816                                                     // Compile body statements, popping all results
817                for sn in body {
818                    self.compile_node(sn)?;
819                    if Self::produces_value(&sn.node) {
820                        self.chunk.emit(Op::Pop, self.line);
821                    }
822                }
823                // Jump back to condition
824                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
825                self.chunk.patch_jump(exit_jump);
826                self.chunk.emit(Op::Pop, self.line); // pop condition
827                                                     // Patch all break jumps to here
828                let ctx = self.loop_stack.pop().unwrap();
829                for patch_pos in ctx.break_patches {
830                    self.chunk.patch_jump(patch_pos);
831                }
832                self.chunk.emit(Op::Nil, self.line);
833            }
834
835            Node::ForIn {
836                pattern,
837                iterable,
838                body,
839            } => {
840                // Compile iterable
841                self.compile_node(iterable)?;
842                // Initialize iterator
843                self.chunk.emit(Op::IterInit, self.line);
844                let loop_start = self.chunk.current_offset();
845                self.loop_stack.push(LoopContext {
846                    start_offset: loop_start,
847                    break_patches: Vec::new(),
848                    has_iterator: true,
849                    handler_depth: self.handler_depth,
850                    finally_depth: self.finally_bodies.len(),
851                });
852                // Try to get next item — jumps to end if exhausted
853                let exit_jump_pos = self.chunk.emit_jump(Op::IterNext, self.line);
854                // Define loop variable(s) with current item (item is on stack from IterNext)
855                self.compile_destructuring(pattern, true)?;
856                // Compile body statements, popping all results
857                for sn in body {
858                    self.compile_node(sn)?;
859                    if Self::produces_value(&sn.node) {
860                        self.chunk.emit(Op::Pop, self.line);
861                    }
862                }
863                // Loop back
864                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
865                self.chunk.patch_jump(exit_jump_pos);
866                // Patch all break jumps to here
867                let ctx = self.loop_stack.pop().unwrap();
868                for patch_pos in ctx.break_patches {
869                    self.chunk.patch_jump(patch_pos);
870                }
871                // Push nil as result (iterator state was consumed)
872                self.chunk.emit(Op::Nil, self.line);
873            }
874
875            Node::ReturnStmt { value } => {
876                let has_pending_finally = !self.finally_bodies.is_empty();
877
878                if has_pending_finally {
879                    // Inside try-finally: compile value, save to temp,
880                    // run pending finallys, restore value, then return.
881                    if let Some(val) = value {
882                        self.compile_node(val)?;
883                    } else {
884                        self.chunk.emit(Op::Nil, self.line);
885                    }
886                    self.temp_counter += 1;
887                    let temp_name = format!("__return_val_{}__", self.temp_counter);
888                    let save_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
889                    self.chunk.emit_u16(Op::DefVar, save_idx, self.line);
890                    // Emit all pending finallys (innermost first = reverse order)
891                    let finallys: Vec<_> = self.finally_bodies.iter().rev().cloned().collect();
892                    for fb in &finallys {
893                        self.compile_finally_inline(fb)?;
894                    }
895                    let restore_idx = self.chunk.add_constant(Constant::String(temp_name));
896                    self.chunk.emit_u16(Op::GetVar, restore_idx, self.line);
897                    self.chunk.emit(Op::Return, self.line);
898                } else {
899                    // No pending finally — original behavior with tail call optimization
900                    if let Some(val) = value {
901                        if let Node::FunctionCall { name, args } = &val.node {
902                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
903                            self.chunk.emit_u16(Op::Constant, name_idx, self.line);
904                            for arg in args {
905                                self.compile_node(arg)?;
906                            }
907                            self.chunk
908                                .emit_u8(Op::TailCall, args.len() as u8, self.line);
909                        } else if let Node::BinaryOp { op, left, right } = &val.node {
910                            if op == "|>" {
911                                self.compile_node(left)?;
912                                self.compile_node(right)?;
913                                self.chunk.emit(Op::Swap, self.line);
914                                self.chunk.emit_u8(Op::TailCall, 1, self.line);
915                            } else {
916                                self.compile_node(val)?;
917                            }
918                        } else {
919                            self.compile_node(val)?;
920                        }
921                    } else {
922                        self.chunk.emit(Op::Nil, self.line);
923                    }
924                    self.chunk.emit(Op::Return, self.line);
925                }
926            }
927
928            Node::BreakStmt => {
929                if self.loop_stack.is_empty() {
930                    return Err(CompileError {
931                        message: "break outside of loop".to_string(),
932                        line: self.line,
933                    });
934                }
935                // Copy values out to avoid borrow conflict
936                let ctx = self.loop_stack.last().unwrap();
937                let finally_depth = ctx.finally_depth;
938                let handler_depth = ctx.handler_depth;
939                let has_iterator = ctx.has_iterator;
940                // Pop exception handlers that were pushed inside the loop
941                for _ in handler_depth..self.handler_depth {
942                    self.chunk.emit(Op::PopHandler, self.line);
943                }
944                // Emit pending finallys that are inside the loop
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                if has_iterator {
956                    self.chunk.emit(Op::PopIterator, self.line);
957                }
958                let patch = self.chunk.emit_jump(Op::Jump, self.line);
959                self.loop_stack
960                    .last_mut()
961                    .unwrap()
962                    .break_patches
963                    .push(patch);
964            }
965
966            Node::ContinueStmt => {
967                if self.loop_stack.is_empty() {
968                    return Err(CompileError {
969                        message: "continue outside of loop".to_string(),
970                        line: self.line,
971                    });
972                }
973                let ctx = self.loop_stack.last().unwrap();
974                let finally_depth = ctx.finally_depth;
975                let handler_depth = ctx.handler_depth;
976                let loop_start = ctx.start_offset;
977                for _ in handler_depth..self.handler_depth {
978                    self.chunk.emit(Op::PopHandler, self.line);
979                }
980                if self.finally_bodies.len() > finally_depth {
981                    let finallys: Vec<_> = self.finally_bodies[finally_depth..]
982                        .iter()
983                        .rev()
984                        .cloned()
985                        .collect();
986                    for fb in &finallys {
987                        self.compile_finally_inline(fb)?;
988                    }
989                }
990                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
991            }
992
993            Node::ListLiteral(elements) => {
994                let has_spread = elements.iter().any(|e| matches!(&e.node, Node::Spread(_)));
995                if !has_spread {
996                    for el in elements {
997                        self.compile_node(el)?;
998                    }
999                    self.chunk
1000                        .emit_u16(Op::BuildList, elements.len() as u16, self.line);
1001                } else {
1002                    // Build with spreads: accumulate segments into lists and concat
1003                    // Start with empty list
1004                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
1005                    let mut pending = 0u16;
1006                    for el in elements {
1007                        if let Node::Spread(inner) = &el.node {
1008                            // First, build list from pending non-spread elements
1009                            if pending > 0 {
1010                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
1011                                // Concat accumulated + pending segment
1012                                self.chunk.emit(Op::Add, self.line);
1013                                pending = 0;
1014                            }
1015                            // Concat with the spread expression (with type check)
1016                            self.compile_node(inner)?;
1017                            self.chunk.emit(Op::Dup, self.line);
1018                            let assert_idx = self
1019                                .chunk
1020                                .add_constant(Constant::String("__assert_list".into()));
1021                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1022                            self.chunk.emit(Op::Swap, self.line);
1023                            self.chunk.emit_u8(Op::Call, 1, self.line);
1024                            self.chunk.emit(Op::Pop, self.line);
1025                            self.chunk.emit(Op::Add, self.line);
1026                        } else {
1027                            self.compile_node(el)?;
1028                            pending += 1;
1029                        }
1030                    }
1031                    if pending > 0 {
1032                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
1033                        self.chunk.emit(Op::Add, self.line);
1034                    }
1035                }
1036            }
1037
1038            Node::DictLiteral(entries) => {
1039                let has_spread = entries
1040                    .iter()
1041                    .any(|e| matches!(&e.value.node, Node::Spread(_)));
1042                if !has_spread {
1043                    for entry in entries {
1044                        self.compile_node(&entry.key)?;
1045                        self.compile_node(&entry.value)?;
1046                    }
1047                    self.chunk
1048                        .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
1049                } else {
1050                    // Build with spreads: use empty dict + Add for merging
1051                    self.chunk.emit_u16(Op::BuildDict, 0, self.line);
1052                    let mut pending = 0u16;
1053                    for entry in entries {
1054                        if let Node::Spread(inner) = &entry.value.node {
1055                            // Flush pending entries
1056                            if pending > 0 {
1057                                self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1058                                self.chunk.emit(Op::Add, self.line);
1059                                pending = 0;
1060                            }
1061                            // Merge spread dict via Add (with type check)
1062                            self.compile_node(inner)?;
1063                            self.chunk.emit(Op::Dup, self.line);
1064                            let assert_idx = self
1065                                .chunk
1066                                .add_constant(Constant::String("__assert_dict".into()));
1067                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1068                            self.chunk.emit(Op::Swap, self.line);
1069                            self.chunk.emit_u8(Op::Call, 1, self.line);
1070                            self.chunk.emit(Op::Pop, self.line);
1071                            self.chunk.emit(Op::Add, self.line);
1072                        } else {
1073                            self.compile_node(&entry.key)?;
1074                            self.compile_node(&entry.value)?;
1075                            pending += 1;
1076                        }
1077                    }
1078                    if pending > 0 {
1079                        self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1080                        self.chunk.emit(Op::Add, self.line);
1081                    }
1082                }
1083            }
1084
1085            Node::InterpolatedString(segments) => {
1086                let mut part_count = 0u16;
1087                for seg in segments {
1088                    match seg {
1089                        StringSegment::Literal(s) => {
1090                            let idx = self.chunk.add_constant(Constant::String(s.clone()));
1091                            self.chunk.emit_u16(Op::Constant, idx, self.line);
1092                            part_count += 1;
1093                        }
1094                        StringSegment::Expression(expr_str) => {
1095                            // Parse and compile the embedded expression
1096                            let mut lexer = harn_lexer::Lexer::new(expr_str);
1097                            if let Ok(tokens) = lexer.tokenize() {
1098                                let mut parser = harn_parser::Parser::new(tokens);
1099                                if let Ok(snode) = parser.parse_single_expression() {
1100                                    self.compile_node(&snode)?;
1101                                    // Convert result to string for concatenation
1102                                    let to_str = self
1103                                        .chunk
1104                                        .add_constant(Constant::String("to_string".into()));
1105                                    self.chunk.emit_u16(Op::Constant, to_str, self.line);
1106                                    self.chunk.emit(Op::Swap, self.line);
1107                                    self.chunk.emit_u8(Op::Call, 1, self.line);
1108                                    part_count += 1;
1109                                } else {
1110                                    // Fallback: treat as literal string
1111                                    let idx =
1112                                        self.chunk.add_constant(Constant::String(expr_str.clone()));
1113                                    self.chunk.emit_u16(Op::Constant, idx, self.line);
1114                                    part_count += 1;
1115                                }
1116                            }
1117                        }
1118                    }
1119                }
1120                if part_count > 1 {
1121                    self.chunk.emit_u16(Op::Concat, part_count, self.line);
1122                }
1123            }
1124
1125            Node::FnDecl {
1126                name, params, body, ..
1127            } => {
1128                // Compile function body into a separate chunk
1129                let mut fn_compiler = Compiler::new();
1130                fn_compiler.enum_names = self.enum_names.clone();
1131                fn_compiler.emit_default_preamble(params)?;
1132                fn_compiler.emit_type_checks(params);
1133                fn_compiler.compile_block(body)?;
1134                fn_compiler.chunk.emit(Op::Nil, self.line);
1135                fn_compiler.chunk.emit(Op::Return, self.line);
1136
1137                let func = CompiledFunction {
1138                    name: name.clone(),
1139                    params: TypedParam::names(params),
1140                    default_start: TypedParam::default_start(params),
1141                    chunk: fn_compiler.chunk,
1142                };
1143                let fn_idx = self.chunk.functions.len();
1144                self.chunk.functions.push(func);
1145
1146                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1147                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1148                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1149            }
1150
1151            Node::Closure { params, body } => {
1152                let mut fn_compiler = Compiler::new();
1153                fn_compiler.enum_names = self.enum_names.clone();
1154                fn_compiler.emit_default_preamble(params)?;
1155                fn_compiler.emit_type_checks(params);
1156                fn_compiler.compile_block(body)?;
1157                // If block didn't end with return, the last value is on the stack
1158                fn_compiler.chunk.emit(Op::Return, self.line);
1159
1160                let func = CompiledFunction {
1161                    name: "<closure>".to_string(),
1162                    params: TypedParam::names(params),
1163                    default_start: TypedParam::default_start(params),
1164                    chunk: fn_compiler.chunk,
1165                };
1166                let fn_idx = self.chunk.functions.len();
1167                self.chunk.functions.push(func);
1168
1169                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1170            }
1171
1172            Node::ThrowStmt { value } => {
1173                self.compile_node(value)?;
1174                self.chunk.emit(Op::Throw, self.line);
1175            }
1176
1177            Node::MatchExpr { value, arms } => {
1178                self.compile_node(value)?;
1179                let mut end_jumps = Vec::new();
1180                for arm in arms {
1181                    match &arm.pattern.node {
1182                        // Wildcard `_` — always matches
1183                        Node::Identifier(name) if name == "_" => {
1184                            self.chunk.emit(Op::Pop, self.line); // pop match value
1185                            self.compile_match_body(&arm.body)?;
1186                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1187                        }
1188                        // Enum destructuring: EnumConstruct pattern
1189                        Node::EnumConstruct {
1190                            enum_name,
1191                            variant,
1192                            args: pat_args,
1193                        } => {
1194                            // Check if the match value is this enum variant
1195                            self.chunk.emit(Op::Dup, self.line);
1196                            let en_idx =
1197                                self.chunk.add_constant(Constant::String(enum_name.clone()));
1198                            let vn_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1199                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1200                            let hi = (vn_idx >> 8) as u8;
1201                            let lo = vn_idx as u8;
1202                            self.chunk.code.push(hi);
1203                            self.chunk.code.push(lo);
1204                            self.chunk.lines.push(self.line);
1205                            self.chunk.columns.push(self.column);
1206                            self.chunk.lines.push(self.line);
1207                            self.chunk.columns.push(self.column);
1208                            // Stack: [match_value, bool]
1209                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1210                            self.chunk.emit(Op::Pop, self.line); // pop bool
1211
1212                            // Destructure: bind field variables from the enum's fields
1213                            // The match value is still on the stack; we need to extract fields
1214                            for (i, pat_arg) in pat_args.iter().enumerate() {
1215                                if let Node::Identifier(binding_name) = &pat_arg.node {
1216                                    // Dup the match value, get .fields, subscript [i]
1217                                    self.chunk.emit(Op::Dup, self.line);
1218                                    let fields_idx = self
1219                                        .chunk
1220                                        .add_constant(Constant::String("fields".to_string()));
1221                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1222                                    let idx_const =
1223                                        self.chunk.add_constant(Constant::Int(i as i64));
1224                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1225                                    self.chunk.emit(Op::Subscript, self.line);
1226                                    let name_idx = self
1227                                        .chunk
1228                                        .add_constant(Constant::String(binding_name.clone()));
1229                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1230                                }
1231                            }
1232
1233                            self.chunk.emit(Op::Pop, self.line); // pop match value
1234                            self.compile_match_body(&arm.body)?;
1235                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1236                            self.chunk.patch_jump(skip);
1237                            self.chunk.emit(Op::Pop, self.line); // pop bool
1238                        }
1239                        // Enum variant without args: PropertyAccess(EnumName, Variant)
1240                        Node::PropertyAccess { object, property } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1241                        {
1242                            let enum_name = if let Node::Identifier(n) = &object.node {
1243                                n.clone()
1244                            } else {
1245                                unreachable!()
1246                            };
1247                            self.chunk.emit(Op::Dup, self.line);
1248                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1249                            let vn_idx =
1250                                self.chunk.add_constant(Constant::String(property.clone()));
1251                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1252                            let hi = (vn_idx >> 8) as u8;
1253                            let lo = vn_idx as u8;
1254                            self.chunk.code.push(hi);
1255                            self.chunk.code.push(lo);
1256                            self.chunk.lines.push(self.line);
1257                            self.chunk.columns.push(self.column);
1258                            self.chunk.lines.push(self.line);
1259                            self.chunk.columns.push(self.column);
1260                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1261                            self.chunk.emit(Op::Pop, self.line); // pop bool
1262                            self.chunk.emit(Op::Pop, self.line); // pop match value
1263                            self.compile_match_body(&arm.body)?;
1264                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1265                            self.chunk.patch_jump(skip);
1266                            self.chunk.emit(Op::Pop, self.line); // pop bool
1267                        }
1268                        // Enum destructuring via MethodCall: EnumName.Variant(bindings...)
1269                        // Parser produces MethodCall for EnumName.Variant(x) patterns
1270                        Node::MethodCall {
1271                            object,
1272                            method,
1273                            args: pat_args,
1274                        } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1275                        {
1276                            let enum_name = if let Node::Identifier(n) = &object.node {
1277                                n.clone()
1278                            } else {
1279                                unreachable!()
1280                            };
1281                            // Check if the match value is this enum variant
1282                            self.chunk.emit(Op::Dup, self.line);
1283                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1284                            let vn_idx = self.chunk.add_constant(Constant::String(method.clone()));
1285                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1286                            let hi = (vn_idx >> 8) as u8;
1287                            let lo = vn_idx as u8;
1288                            self.chunk.code.push(hi);
1289                            self.chunk.code.push(lo);
1290                            self.chunk.lines.push(self.line);
1291                            self.chunk.columns.push(self.column);
1292                            self.chunk.lines.push(self.line);
1293                            self.chunk.columns.push(self.column);
1294                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1295                            self.chunk.emit(Op::Pop, self.line); // pop bool
1296
1297                            // Destructure: bind field variables
1298                            for (i, pat_arg) in pat_args.iter().enumerate() {
1299                                if let Node::Identifier(binding_name) = &pat_arg.node {
1300                                    self.chunk.emit(Op::Dup, self.line);
1301                                    let fields_idx = self
1302                                        .chunk
1303                                        .add_constant(Constant::String("fields".to_string()));
1304                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1305                                    let idx_const =
1306                                        self.chunk.add_constant(Constant::Int(i as i64));
1307                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1308                                    self.chunk.emit(Op::Subscript, self.line);
1309                                    let name_idx = self
1310                                        .chunk
1311                                        .add_constant(Constant::String(binding_name.clone()));
1312                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1313                                }
1314                            }
1315
1316                            self.chunk.emit(Op::Pop, self.line); // pop match value
1317                            self.compile_match_body(&arm.body)?;
1318                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1319                            self.chunk.patch_jump(skip);
1320                            self.chunk.emit(Op::Pop, self.line); // pop bool
1321                        }
1322                        // Binding pattern: bare identifier (not a literal)
1323                        Node::Identifier(name) => {
1324                            // Bind the match value to this name, always matches
1325                            self.chunk.emit(Op::Dup, self.line); // dup for binding
1326                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1327                            self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1328                            self.chunk.emit(Op::Pop, self.line); // pop match value
1329                            self.compile_match_body(&arm.body)?;
1330                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1331                        }
1332                        // Dict pattern: {key: literal, key: binding, ...}
1333                        Node::DictLiteral(entries)
1334                            if entries
1335                                .iter()
1336                                .all(|e| matches!(&e.key.node, Node::StringLiteral(_))) =>
1337                        {
1338                            // Check type is dict: dup, call type_of, compare "dict"
1339                            self.chunk.emit(Op::Dup, self.line);
1340                            let typeof_idx =
1341                                self.chunk.add_constant(Constant::String("type_of".into()));
1342                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1343                            self.chunk.emit(Op::Swap, self.line);
1344                            self.chunk.emit_u8(Op::Call, 1, self.line);
1345                            let dict_str = self.chunk.add_constant(Constant::String("dict".into()));
1346                            self.chunk.emit_u16(Op::Constant, dict_str, self.line);
1347                            self.chunk.emit(Op::Equal, self.line);
1348                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1349                            self.chunk.emit(Op::Pop, self.line); // pop bool
1350
1351                            // Check literal constraints
1352                            let mut constraint_skips = Vec::new();
1353                            let mut bindings = Vec::new();
1354                            for entry in entries {
1355                                if let Node::StringLiteral(key) = &entry.key.node {
1356                                    match &entry.value.node {
1357                                        // Literal value → constraint: dict[key] == value
1358                                        Node::StringLiteral(_)
1359                                        | Node::IntLiteral(_)
1360                                        | Node::FloatLiteral(_)
1361                                        | Node::BoolLiteral(_)
1362                                        | Node::NilLiteral => {
1363                                            self.chunk.emit(Op::Dup, self.line);
1364                                            let key_idx = self
1365                                                .chunk
1366                                                .add_constant(Constant::String(key.clone()));
1367                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1368                                            self.chunk.emit(Op::Subscript, self.line);
1369                                            self.compile_node(&entry.value)?;
1370                                            self.chunk.emit(Op::Equal, self.line);
1371                                            let skip =
1372                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1373                                            self.chunk.emit(Op::Pop, self.line); // pop bool
1374                                            constraint_skips.push(skip);
1375                                        }
1376                                        // Identifier → binding: bind dict[key] to variable
1377                                        Node::Identifier(binding) => {
1378                                            bindings.push((key.clone(), binding.clone()));
1379                                        }
1380                                        _ => {
1381                                            // Complex expression constraint
1382                                            self.chunk.emit(Op::Dup, self.line);
1383                                            let key_idx = self
1384                                                .chunk
1385                                                .add_constant(Constant::String(key.clone()));
1386                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1387                                            self.chunk.emit(Op::Subscript, self.line);
1388                                            self.compile_node(&entry.value)?;
1389                                            self.chunk.emit(Op::Equal, self.line);
1390                                            let skip =
1391                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1392                                            self.chunk.emit(Op::Pop, self.line);
1393                                            constraint_skips.push(skip);
1394                                        }
1395                                    }
1396                                }
1397                            }
1398
1399                            // All constraints passed — emit bindings
1400                            for (key, binding) in &bindings {
1401                                self.chunk.emit(Op::Dup, self.line);
1402                                let key_idx =
1403                                    self.chunk.add_constant(Constant::String(key.clone()));
1404                                self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1405                                self.chunk.emit(Op::Subscript, self.line);
1406                                let name_idx =
1407                                    self.chunk.add_constant(Constant::String(binding.clone()));
1408                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1409                            }
1410
1411                            self.chunk.emit(Op::Pop, self.line); // pop match value
1412                            self.compile_match_body(&arm.body)?;
1413                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1414
1415                            // All failures jump here: pop the false bool, leave match_value
1416                            let fail_target = self.chunk.code.len();
1417                            self.chunk.emit(Op::Pop, self.line); // pop bool
1418                                                                 // Patch all failure jumps to the shared cleanup point
1419                            for skip in constraint_skips {
1420                                self.chunk.patch_jump_to(skip, fail_target);
1421                            }
1422                            self.chunk.patch_jump_to(skip_type, fail_target);
1423                        }
1424                        // List pattern: [literal, binding, ...]
1425                        Node::ListLiteral(elements) => {
1426                            // Check type is list: dup, call type_of, compare "list"
1427                            self.chunk.emit(Op::Dup, self.line);
1428                            let typeof_idx =
1429                                self.chunk.add_constant(Constant::String("type_of".into()));
1430                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1431                            self.chunk.emit(Op::Swap, self.line);
1432                            self.chunk.emit_u8(Op::Call, 1, self.line);
1433                            let list_str = self.chunk.add_constant(Constant::String("list".into()));
1434                            self.chunk.emit_u16(Op::Constant, list_str, self.line);
1435                            self.chunk.emit(Op::Equal, self.line);
1436                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1437                            self.chunk.emit(Op::Pop, self.line); // pop bool
1438
1439                            // Check length: dup, call len, compare >= elements.len()
1440                            self.chunk.emit(Op::Dup, self.line);
1441                            let len_idx = self.chunk.add_constant(Constant::String("len".into()));
1442                            self.chunk.emit_u16(Op::Constant, len_idx, self.line);
1443                            self.chunk.emit(Op::Swap, self.line);
1444                            self.chunk.emit_u8(Op::Call, 1, self.line);
1445                            let count = self
1446                                .chunk
1447                                .add_constant(Constant::Int(elements.len() as i64));
1448                            self.chunk.emit_u16(Op::Constant, count, self.line);
1449                            self.chunk.emit(Op::GreaterEqual, self.line);
1450                            let skip_len = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1451                            self.chunk.emit(Op::Pop, self.line); // pop bool
1452
1453                            // Check literal constraints and collect bindings
1454                            let mut constraint_skips = Vec::new();
1455                            let mut bindings = Vec::new();
1456                            for (i, elem) in elements.iter().enumerate() {
1457                                match &elem.node {
1458                                    Node::Identifier(name) if name != "_" => {
1459                                        bindings.push((i, name.clone()));
1460                                    }
1461                                    Node::Identifier(_) => {} // wildcard _
1462                                    // Literal constraint
1463                                    _ => {
1464                                        self.chunk.emit(Op::Dup, self.line);
1465                                        let idx_const =
1466                                            self.chunk.add_constant(Constant::Int(i as i64));
1467                                        self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1468                                        self.chunk.emit(Op::Subscript, self.line);
1469                                        self.compile_node(elem)?;
1470                                        self.chunk.emit(Op::Equal, self.line);
1471                                        let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1472                                        self.chunk.emit(Op::Pop, self.line);
1473                                        constraint_skips.push(skip);
1474                                    }
1475                                }
1476                            }
1477
1478                            // Emit bindings
1479                            for (i, name) in &bindings {
1480                                self.chunk.emit(Op::Dup, self.line);
1481                                let idx_const = self.chunk.add_constant(Constant::Int(*i as i64));
1482                                self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1483                                self.chunk.emit(Op::Subscript, self.line);
1484                                let name_idx =
1485                                    self.chunk.add_constant(Constant::String(name.clone()));
1486                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1487                            }
1488
1489                            self.chunk.emit(Op::Pop, self.line); // pop match value
1490                            self.compile_match_body(&arm.body)?;
1491                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1492
1493                            // All failures jump here: pop the false bool
1494                            let fail_target = self.chunk.code.len();
1495                            self.chunk.emit(Op::Pop, self.line); // pop bool
1496                            for skip in constraint_skips {
1497                                self.chunk.patch_jump_to(skip, fail_target);
1498                            }
1499                            self.chunk.patch_jump_to(skip_len, fail_target);
1500                            self.chunk.patch_jump_to(skip_type, fail_target);
1501                        }
1502                        // Literal/expression pattern — compare with Equal
1503                        _ => {
1504                            self.chunk.emit(Op::Dup, self.line);
1505                            self.compile_node(&arm.pattern)?;
1506                            self.chunk.emit(Op::Equal, self.line);
1507                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1508                            self.chunk.emit(Op::Pop, self.line); // pop bool
1509                            self.chunk.emit(Op::Pop, self.line); // pop match value
1510                            self.compile_match_body(&arm.body)?;
1511                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1512                            self.chunk.patch_jump(skip);
1513                            self.chunk.emit(Op::Pop, self.line); // pop bool
1514                        }
1515                    }
1516                }
1517                // No match — pop value, push nil
1518                self.chunk.emit(Op::Pop, self.line);
1519                self.chunk.emit(Op::Nil, self.line);
1520                for j in end_jumps {
1521                    self.chunk.patch_jump(j);
1522                }
1523            }
1524
1525            Node::RangeExpr {
1526                start,
1527                end,
1528                inclusive,
1529            } => {
1530                // Compile as __range__(start, end, inclusive_bool) builtin call
1531                let name_idx = self
1532                    .chunk
1533                    .add_constant(Constant::String("__range__".to_string()));
1534                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
1535                self.compile_node(start)?;
1536                self.compile_node(end)?;
1537                if *inclusive {
1538                    self.chunk.emit(Op::True, self.line);
1539                } else {
1540                    self.chunk.emit(Op::False, self.line);
1541                }
1542                self.chunk.emit_u8(Op::Call, 3, self.line);
1543            }
1544
1545            Node::GuardStmt {
1546                condition,
1547                else_body,
1548            } => {
1549                // guard condition else { body }
1550                // Compile condition; if truthy, skip else_body
1551                self.compile_node(condition)?;
1552                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
1553                self.chunk.emit(Op::Pop, self.line); // pop condition
1554                                                     // Compile else_body
1555                self.compile_block(else_body)?;
1556                // Pop result of else_body (guard is a statement, not expression)
1557                if !else_body.is_empty() && Self::produces_value(&else_body.last().unwrap().node) {
1558                    self.chunk.emit(Op::Pop, self.line);
1559                }
1560                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1561                self.chunk.patch_jump(skip_jump);
1562                self.chunk.emit(Op::Pop, self.line); // pop condition
1563                self.chunk.patch_jump(end_jump);
1564                self.chunk.emit(Op::Nil, self.line);
1565            }
1566
1567            Node::Block(stmts) => {
1568                if stmts.is_empty() {
1569                    self.chunk.emit(Op::Nil, self.line);
1570                } else {
1571                    self.compile_block(stmts)?;
1572                }
1573            }
1574
1575            Node::DeadlineBlock { duration, body } => {
1576                self.compile_node(duration)?;
1577                self.chunk.emit(Op::DeadlineSetup, self.line);
1578                if body.is_empty() {
1579                    self.chunk.emit(Op::Nil, self.line);
1580                } else {
1581                    self.compile_block(body)?;
1582                }
1583                self.chunk.emit(Op::DeadlineEnd, self.line);
1584            }
1585
1586            Node::MutexBlock { body } => {
1587                // v1: single-threaded, just compile the body
1588                if body.is_empty() {
1589                    self.chunk.emit(Op::Nil, self.line);
1590                } else {
1591                    // Compile body, but pop intermediate values and push nil at the end.
1592                    // The body typically contains statements (assignments) that don't produce values.
1593                    for sn in body {
1594                        self.compile_node(sn)?;
1595                        if Self::produces_value(&sn.node) {
1596                            self.chunk.emit(Op::Pop, self.line);
1597                        }
1598                    }
1599                    self.chunk.emit(Op::Nil, self.line);
1600                }
1601            }
1602
1603            Node::YieldExpr { .. } => {
1604                // v1: yield is host-integration only, emit nil
1605                self.chunk.emit(Op::Nil, self.line);
1606            }
1607
1608            Node::AskExpr { fields } => {
1609                // Compile as a dict literal and call llm_call builtin
1610                // For v1, just build the dict (llm_call requires async)
1611                for entry in fields {
1612                    self.compile_node(&entry.key)?;
1613                    self.compile_node(&entry.value)?;
1614                }
1615                self.chunk
1616                    .emit_u16(Op::BuildDict, fields.len() as u16, self.line);
1617            }
1618
1619            Node::EnumConstruct {
1620                enum_name,
1621                variant,
1622                args,
1623            } => {
1624                // Push field values onto the stack, then BuildEnum
1625                for arg in args {
1626                    self.compile_node(arg)?;
1627                }
1628                let enum_idx = self.chunk.add_constant(Constant::String(enum_name.clone()));
1629                let var_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1630                // BuildEnum: enum_name_idx, variant_idx, field_count
1631                self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
1632                let hi = (var_idx >> 8) as u8;
1633                let lo = var_idx as u8;
1634                self.chunk.code.push(hi);
1635                self.chunk.code.push(lo);
1636                self.chunk.lines.push(self.line);
1637                self.chunk.columns.push(self.column);
1638                self.chunk.lines.push(self.line);
1639                self.chunk.columns.push(self.column);
1640                let fc = args.len() as u16;
1641                let fhi = (fc >> 8) as u8;
1642                let flo = fc as u8;
1643                self.chunk.code.push(fhi);
1644                self.chunk.code.push(flo);
1645                self.chunk.lines.push(self.line);
1646                self.chunk.columns.push(self.column);
1647                self.chunk.lines.push(self.line);
1648                self.chunk.columns.push(self.column);
1649            }
1650
1651            Node::StructConstruct {
1652                struct_name,
1653                fields,
1654            } => {
1655                // Build as a dict with a __struct__ key for metadata
1656                let struct_key = self
1657                    .chunk
1658                    .add_constant(Constant::String("__struct__".to_string()));
1659                let struct_val = self
1660                    .chunk
1661                    .add_constant(Constant::String(struct_name.clone()));
1662                self.chunk.emit_u16(Op::Constant, struct_key, self.line);
1663                self.chunk.emit_u16(Op::Constant, struct_val, self.line);
1664
1665                for entry in fields {
1666                    self.compile_node(&entry.key)?;
1667                    self.compile_node(&entry.value)?;
1668                }
1669                self.chunk
1670                    .emit_u16(Op::BuildDict, (fields.len() + 1) as u16, self.line);
1671            }
1672
1673            Node::ImportDecl { path } => {
1674                let idx = self.chunk.add_constant(Constant::String(path.clone()));
1675                self.chunk.emit_u16(Op::Import, idx, self.line);
1676            }
1677
1678            Node::SelectiveImport { names, path } => {
1679                let path_idx = self.chunk.add_constant(Constant::String(path.clone()));
1680                let names_str = names.join(",");
1681                let names_idx = self.chunk.add_constant(Constant::String(names_str));
1682                self.chunk
1683                    .emit_u16(Op::SelectiveImport, path_idx, self.line);
1684                let hi = (names_idx >> 8) as u8;
1685                let lo = names_idx as u8;
1686                self.chunk.code.push(hi);
1687                self.chunk.code.push(lo);
1688                self.chunk.lines.push(self.line);
1689                self.chunk.columns.push(self.column);
1690                self.chunk.lines.push(self.line);
1691                self.chunk.columns.push(self.column);
1692            }
1693
1694            Node::TryOperator { operand } => {
1695                self.compile_node(operand)?;
1696                self.chunk.emit(Op::TryUnwrap, self.line);
1697            }
1698
1699            Node::ImplBlock { type_name, methods } => {
1700                // Compile each method as a closure and store in __impl_TypeName dict.
1701                // Build key-value pairs on stack, then BuildDict.
1702                for method_sn in methods {
1703                    if let Node::FnDecl {
1704                        name, params, body, ..
1705                    } = &method_sn.node
1706                    {
1707                        // Method name key
1708                        let key_idx = self.chunk.add_constant(Constant::String(name.clone()));
1709                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1710
1711                        // Compile method body as closure
1712                        let mut fn_compiler = Compiler::new();
1713                        fn_compiler.enum_names = self.enum_names.clone();
1714                        fn_compiler.emit_default_preamble(params)?;
1715                        fn_compiler.emit_type_checks(params);
1716                        fn_compiler.compile_block(body)?;
1717                        fn_compiler.chunk.emit(Op::Nil, self.line);
1718                        fn_compiler.chunk.emit(Op::Return, self.line);
1719
1720                        let func = CompiledFunction {
1721                            name: format!("{}.{}", type_name, name),
1722                            params: TypedParam::names(params),
1723                            default_start: TypedParam::default_start(params),
1724                            chunk: fn_compiler.chunk,
1725                        };
1726                        let fn_idx = self.chunk.functions.len();
1727                        self.chunk.functions.push(func);
1728                        self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1729                    }
1730                }
1731                let method_count = methods
1732                    .iter()
1733                    .filter(|m| matches!(m.node, Node::FnDecl { .. }))
1734                    .count();
1735                self.chunk
1736                    .emit_u16(Op::BuildDict, method_count as u16, self.line);
1737                let impl_name = format!("__impl_{}", type_name);
1738                let name_idx = self.chunk.add_constant(Constant::String(impl_name));
1739                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1740            }
1741
1742            Node::StructDecl { name, .. } => {
1743                // Compile a constructor function: StructName({field: val, ...}) -> StructInstance
1744                let mut fn_compiler = Compiler::new();
1745                fn_compiler.enum_names = self.enum_names.clone();
1746                let params = vec![TypedParam::untyped("__fields")];
1747                fn_compiler.emit_default_preamble(&params)?;
1748
1749                // Call __make_struct(struct_name, fields_dict) to tag the dict
1750                let make_idx = fn_compiler
1751                    .chunk
1752                    .add_constant(Constant::String("__make_struct".into()));
1753                fn_compiler
1754                    .chunk
1755                    .emit_u16(Op::Constant, make_idx, self.line);
1756                let sname_idx = fn_compiler
1757                    .chunk
1758                    .add_constant(Constant::String(name.clone()));
1759                fn_compiler
1760                    .chunk
1761                    .emit_u16(Op::Constant, sname_idx, self.line);
1762                let fields_idx = fn_compiler
1763                    .chunk
1764                    .add_constant(Constant::String("__fields".into()));
1765                fn_compiler
1766                    .chunk
1767                    .emit_u16(Op::GetVar, fields_idx, self.line);
1768                fn_compiler.chunk.emit_u8(Op::Call, 2, self.line);
1769                fn_compiler.chunk.emit(Op::Return, self.line);
1770
1771                let func = CompiledFunction {
1772                    name: name.clone(),
1773                    params: TypedParam::names(&params),
1774                    default_start: None,
1775                    chunk: fn_compiler.chunk,
1776                };
1777                let fn_idx = self.chunk.functions.len();
1778                self.chunk.functions.push(func);
1779                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1780                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1781                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1782            }
1783
1784            // Declarations that only register metadata (no runtime effect needed for v1)
1785            Node::Pipeline { .. }
1786            | Node::OverrideDecl { .. }
1787            | Node::TypeDecl { .. }
1788            | Node::EnumDecl { .. }
1789            | Node::InterfaceDecl { .. } => {
1790                self.chunk.emit(Op::Nil, self.line);
1791            }
1792
1793            Node::TryCatch {
1794                body,
1795                error_var,
1796                error_type,
1797                catch_body,
1798                finally_body,
1799            } => {
1800                // Extract the type name for typed catch (e.g., "AppError")
1801                let type_name = error_type.as_ref().and_then(|te| {
1802                    if let harn_parser::TypeExpr::Named(name) = te {
1803                        Some(name.clone())
1804                    } else {
1805                        None
1806                    }
1807                });
1808
1809                let type_name_idx = if let Some(ref tn) = type_name {
1810                    self.chunk.add_constant(Constant::String(tn.clone()))
1811                } else {
1812                    self.chunk.add_constant(Constant::String(String::new()))
1813                };
1814
1815                let has_catch = !catch_body.is_empty() || error_var.is_some();
1816                let has_finally = finally_body.is_some();
1817
1818                if has_catch && has_finally {
1819                    // === try-catch-finally ===
1820                    let finally_body = finally_body.as_ref().unwrap();
1821
1822                    // Push finally body onto pending stack for return/break handling
1823                    self.finally_bodies.push(finally_body.clone());
1824
1825                    // 1. TryCatchSetup for try body
1826                    self.handler_depth += 1;
1827                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1828                    self.emit_type_name_extra(type_name_idx);
1829
1830                    // 2. Compile try body
1831                    self.compile_try_body(body)?;
1832
1833                    // 3. PopHandler + inline finally (success path)
1834                    self.handler_depth -= 1;
1835                    self.chunk.emit(Op::PopHandler, self.line);
1836                    self.compile_finally_inline(finally_body)?;
1837                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1838
1839                    // 4. Catch entry
1840                    self.chunk.patch_jump(catch_jump);
1841                    self.compile_catch_binding(error_var)?;
1842
1843                    // 5. Inner try around catch body (so finally runs if catch throws)
1844                    self.handler_depth += 1;
1845                    let rethrow_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1846                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1847                    self.emit_type_name_extra(empty_type);
1848
1849                    // 6. Compile catch body
1850                    self.compile_try_body(catch_body)?;
1851
1852                    // 7. PopHandler + inline finally (catch success path)
1853                    self.handler_depth -= 1;
1854                    self.chunk.emit(Op::PopHandler, self.line);
1855                    self.compile_finally_inline(finally_body)?;
1856                    let end_jump2 = self.chunk.emit_jump(Op::Jump, self.line);
1857
1858                    // 8. Rethrow handler: save error, run finally, re-throw
1859                    self.chunk.patch_jump(rethrow_jump);
1860                    self.compile_rethrow_with_finally(finally_body)?;
1861
1862                    self.chunk.patch_jump(end_jump);
1863                    self.chunk.patch_jump(end_jump2);
1864
1865                    self.finally_bodies.pop();
1866                } else if has_finally {
1867                    // === try-finally (no catch) ===
1868                    let finally_body = finally_body.as_ref().unwrap();
1869
1870                    self.finally_bodies.push(finally_body.clone());
1871
1872                    // 1. TryCatchSetup to error path
1873                    self.handler_depth += 1;
1874                    let error_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1875                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1876                    self.emit_type_name_extra(empty_type);
1877
1878                    // 2. Compile try body
1879                    self.compile_try_body(body)?;
1880
1881                    // 3. PopHandler + inline finally (success path)
1882                    self.handler_depth -= 1;
1883                    self.chunk.emit(Op::PopHandler, self.line);
1884                    self.compile_finally_inline(finally_body)?;
1885                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1886
1887                    // 4. Error path: save error, run finally, re-throw
1888                    self.chunk.patch_jump(error_jump);
1889                    self.compile_rethrow_with_finally(finally_body)?;
1890
1891                    self.chunk.patch_jump(end_jump);
1892
1893                    self.finally_bodies.pop();
1894                } else {
1895                    // === try-catch (no finally) — original behavior ===
1896
1897                    // 1. TryCatchSetup
1898                    self.handler_depth += 1;
1899                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1900                    self.emit_type_name_extra(type_name_idx);
1901
1902                    // 2. Compile try body
1903                    self.compile_try_body(body)?;
1904
1905                    // 3. PopHandler + jump past catch
1906                    self.handler_depth -= 1;
1907                    self.chunk.emit(Op::PopHandler, self.line);
1908                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1909
1910                    // 4. Catch entry
1911                    self.chunk.patch_jump(catch_jump);
1912                    self.compile_catch_binding(error_var)?;
1913
1914                    // 5. Compile catch body
1915                    self.compile_try_body(catch_body)?;
1916
1917                    // 6. Patch end
1918                    self.chunk.patch_jump(end_jump);
1919                }
1920            }
1921
1922            Node::TryExpr { body } => {
1923                // try { body } — returns Result.Ok(value) or Result.Err(error)
1924
1925                // 1. Set up try-catch handler (untyped)
1926                self.handler_depth += 1;
1927                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1928                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1929                self.emit_type_name_extra(empty_type);
1930
1931                // 2. Compile try body (leaves value on stack)
1932                self.compile_try_body(body)?;
1933
1934                // 3. PopHandler (success path)
1935                self.handler_depth -= 1;
1936                self.chunk.emit(Op::PopHandler, self.line);
1937
1938                // 4. Wrap in Result.Ok: push "Ok", swap, call Ok(value)
1939                let ok_idx = self.chunk.add_constant(Constant::String("Ok".to_string()));
1940                self.chunk.emit_u16(Op::Constant, ok_idx, self.line);
1941                self.chunk.emit(Op::Swap, self.line);
1942                self.chunk.emit_u8(Op::Call, 1, self.line);
1943
1944                // 5. Jump past error handler
1945                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1946
1947                // 6. Error handler: error value is on stack
1948                self.chunk.patch_jump(catch_jump);
1949
1950                // 7. Wrap in Result.Err: push "Err", swap, call Err(error)
1951                let err_idx = self.chunk.add_constant(Constant::String("Err".to_string()));
1952                self.chunk.emit_u16(Op::Constant, err_idx, self.line);
1953                self.chunk.emit(Op::Swap, self.line);
1954                self.chunk.emit_u8(Op::Call, 1, self.line);
1955
1956                // 8. Patch end
1957                self.chunk.patch_jump(end_jump);
1958            }
1959
1960            Node::Retry { count, body } => {
1961                // Compile count expression into a mutable counter variable
1962                self.compile_node(count)?;
1963                let counter_name = "__retry_counter__";
1964                let counter_idx = self
1965                    .chunk
1966                    .add_constant(Constant::String(counter_name.to_string()));
1967                self.chunk.emit_u16(Op::DefVar, counter_idx, self.line);
1968
1969                // Also store the last error for re-throwing
1970                self.chunk.emit(Op::Nil, self.line);
1971                let err_name = "__retry_last_error__";
1972                let err_idx = self
1973                    .chunk
1974                    .add_constant(Constant::String(err_name.to_string()));
1975                self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
1976
1977                // Loop start
1978                let loop_start = self.chunk.current_offset();
1979
1980                // Set up try/catch (untyped - empty type name)
1981                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1982                // Emit empty type name for untyped catch
1983                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1984                let hi = (empty_type >> 8) as u8;
1985                let lo = empty_type as u8;
1986                self.chunk.code.push(hi);
1987                self.chunk.code.push(lo);
1988                self.chunk.lines.push(self.line);
1989                self.chunk.columns.push(self.column);
1990                self.chunk.lines.push(self.line);
1991                self.chunk.columns.push(self.column);
1992
1993                // Compile body
1994                self.compile_block(body)?;
1995
1996                // Success: pop handler, jump to end
1997                self.chunk.emit(Op::PopHandler, self.line);
1998                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1999
2000                // Catch handler
2001                self.chunk.patch_jump(catch_jump);
2002                // Save the error value for potential re-throw
2003                self.chunk.emit(Op::Dup, self.line);
2004                self.chunk.emit_u16(Op::SetVar, err_idx, self.line);
2005                // Pop the error value
2006                self.chunk.emit(Op::Pop, self.line);
2007
2008                // Decrement counter
2009                self.chunk.emit_u16(Op::GetVar, counter_idx, self.line);
2010                let one_idx = self.chunk.add_constant(Constant::Int(1));
2011                self.chunk.emit_u16(Op::Constant, one_idx, self.line);
2012                self.chunk.emit(Op::Sub, self.line);
2013                self.chunk.emit(Op::Dup, self.line);
2014                self.chunk.emit_u16(Op::SetVar, counter_idx, self.line);
2015
2016                // If counter > 0, jump to loop start
2017                let zero_idx = self.chunk.add_constant(Constant::Int(0));
2018                self.chunk.emit_u16(Op::Constant, zero_idx, self.line);
2019                self.chunk.emit(Op::Greater, self.line);
2020                let retry_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2021                self.chunk.emit(Op::Pop, self.line); // pop condition
2022                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
2023
2024                // No more retries — re-throw the last error
2025                self.chunk.patch_jump(retry_jump);
2026                self.chunk.emit(Op::Pop, self.line); // pop condition
2027                self.chunk.emit_u16(Op::GetVar, err_idx, self.line);
2028                self.chunk.emit(Op::Throw, self.line);
2029
2030                self.chunk.patch_jump(end_jump);
2031                // Push nil as the result of a successful retry block
2032                self.chunk.emit(Op::Nil, self.line);
2033            }
2034
2035            Node::Parallel {
2036                count,
2037                variable,
2038                body,
2039            } => {
2040                self.compile_node(count)?;
2041                let mut fn_compiler = Compiler::new();
2042                fn_compiler.enum_names = self.enum_names.clone();
2043                fn_compiler.compile_block(body)?;
2044                fn_compiler.chunk.emit(Op::Return, self.line);
2045                let params = vec![variable.clone().unwrap_or_else(|| "__i__".to_string())];
2046                let func = CompiledFunction {
2047                    name: "<parallel>".to_string(),
2048                    params,
2049                    default_start: None,
2050                    chunk: fn_compiler.chunk,
2051                };
2052                let fn_idx = self.chunk.functions.len();
2053                self.chunk.functions.push(func);
2054                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2055                self.chunk.emit(Op::Parallel, self.line);
2056            }
2057
2058            Node::ParallelMap {
2059                list,
2060                variable,
2061                body,
2062            } => {
2063                self.compile_node(list)?;
2064                let mut fn_compiler = Compiler::new();
2065                fn_compiler.enum_names = self.enum_names.clone();
2066                fn_compiler.compile_block(body)?;
2067                fn_compiler.chunk.emit(Op::Return, self.line);
2068                let func = CompiledFunction {
2069                    name: "<parallel_map>".to_string(),
2070                    params: vec![variable.clone()],
2071                    default_start: None,
2072                    chunk: fn_compiler.chunk,
2073                };
2074                let fn_idx = self.chunk.functions.len();
2075                self.chunk.functions.push(func);
2076                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2077                self.chunk.emit(Op::ParallelMap, self.line);
2078            }
2079
2080            Node::SpawnExpr { body } => {
2081                let mut fn_compiler = Compiler::new();
2082                fn_compiler.enum_names = self.enum_names.clone();
2083                fn_compiler.compile_block(body)?;
2084                fn_compiler.chunk.emit(Op::Return, self.line);
2085                let func = CompiledFunction {
2086                    name: "<spawn>".to_string(),
2087                    params: vec![],
2088                    default_start: None,
2089                    chunk: fn_compiler.chunk,
2090                };
2091                let fn_idx = self.chunk.functions.len();
2092                self.chunk.functions.push(func);
2093                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2094                self.chunk.emit(Op::Spawn, self.line);
2095            }
2096            Node::SelectExpr {
2097                cases,
2098                timeout,
2099                default_body,
2100            } => {
2101                // Desugar select into: builtin call + index-based dispatch.
2102                //
2103                // Step 1: Push builtin name, compile channel list, optionally
2104                //         compile timeout duration, then Call.
2105                // Step 2: Store result dict in temp, dispatch on result.index.
2106
2107                let builtin_name = if timeout.is_some() {
2108                    "__select_timeout"
2109                } else if default_body.is_some() {
2110                    "__select_try"
2111                } else {
2112                    "__select_list"
2113                };
2114
2115                // Push builtin name (callee goes below args on stack)
2116                let name_idx = self
2117                    .chunk
2118                    .add_constant(Constant::String(builtin_name.into()));
2119                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
2120
2121                // Build channel list (arg 1)
2122                for case in cases {
2123                    self.compile_node(&case.channel)?;
2124                }
2125                self.chunk
2126                    .emit_u16(Op::BuildList, cases.len() as u16, self.line);
2127
2128                // If timeout, compile duration (arg 2)
2129                if let Some((duration_expr, _)) = timeout {
2130                    self.compile_node(duration_expr)?;
2131                    self.chunk.emit_u8(Op::Call, 2, self.line);
2132                } else {
2133                    self.chunk.emit_u8(Op::Call, 1, self.line);
2134                }
2135
2136                // Store result in temp var
2137                self.temp_counter += 1;
2138                let result_name = format!("__sel_result_{}__", self.temp_counter);
2139                let result_idx = self
2140                    .chunk
2141                    .add_constant(Constant::String(result_name.clone()));
2142                self.chunk.emit_u16(Op::DefVar, result_idx, self.line);
2143
2144                // Dispatch on result.index
2145                let mut end_jumps = Vec::new();
2146
2147                for (i, case) in cases.iter().enumerate() {
2148                    let get_r = self
2149                        .chunk
2150                        .add_constant(Constant::String(result_name.clone()));
2151                    self.chunk.emit_u16(Op::GetVar, get_r, self.line);
2152                    let idx_prop = self.chunk.add_constant(Constant::String("index".into()));
2153                    self.chunk.emit_u16(Op::GetProperty, idx_prop, self.line);
2154                    let case_i = self.chunk.add_constant(Constant::Int(i as i64));
2155                    self.chunk.emit_u16(Op::Constant, case_i, self.line);
2156                    self.chunk.emit(Op::Equal, self.line);
2157                    let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2158                    self.chunk.emit(Op::Pop, self.line);
2159
2160                    // Bind variable = result.value
2161                    let get_r2 = self
2162                        .chunk
2163                        .add_constant(Constant::String(result_name.clone()));
2164                    self.chunk.emit_u16(Op::GetVar, get_r2, self.line);
2165                    let val_prop = self.chunk.add_constant(Constant::String("value".into()));
2166                    self.chunk.emit_u16(Op::GetProperty, val_prop, self.line);
2167                    let var_idx = self
2168                        .chunk
2169                        .add_constant(Constant::String(case.variable.clone()));
2170                    self.chunk.emit_u16(Op::DefLet, var_idx, self.line);
2171
2172                    self.compile_try_body(&case.body)?;
2173                    end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2174                    self.chunk.patch_jump(skip);
2175                    self.chunk.emit(Op::Pop, self.line);
2176                }
2177
2178                // Timeout/default fallthrough (index == -1)
2179                if let Some((_, ref timeout_body)) = timeout {
2180                    self.compile_try_body(timeout_body)?;
2181                } else if let Some(ref def_body) = default_body {
2182                    self.compile_try_body(def_body)?;
2183                } else {
2184                    self.chunk.emit(Op::Nil, self.line);
2185                }
2186
2187                for ej in end_jumps {
2188                    self.chunk.patch_jump(ej);
2189                }
2190            }
2191            Node::Spread(_) => {
2192                return Err(CompileError {
2193                    message: "spread (...) can only be used inside list literals, dict literals, or function call arguments".into(),
2194                    line: self.line,
2195                });
2196            }
2197        }
2198        Ok(())
2199    }
2200
2201    /// Compile a destructuring binding pattern.
2202    /// Expects the RHS value to already be on the stack.
2203    /// After this, the value is consumed (popped) and each binding is defined.
2204    fn compile_destructuring(
2205        &mut self,
2206        pattern: &BindingPattern,
2207        is_mutable: bool,
2208    ) -> Result<(), CompileError> {
2209        let def_op = if is_mutable { Op::DefVar } else { Op::DefLet };
2210        match pattern {
2211            BindingPattern::Identifier(name) => {
2212                // Simple case: just define the variable
2213                let idx = self.chunk.add_constant(Constant::String(name.clone()));
2214                self.chunk.emit_u16(def_op, idx, self.line);
2215            }
2216            BindingPattern::Dict(fields) => {
2217                // Stack has the dict value.
2218                // Emit runtime type check: __assert_dict(value)
2219                self.chunk.emit(Op::Dup, self.line);
2220                let assert_idx = self
2221                    .chunk
2222                    .add_constant(Constant::String("__assert_dict".into()));
2223                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
2224                self.chunk.emit(Op::Swap, self.line);
2225                self.chunk.emit_u8(Op::Call, 1, self.line);
2226                self.chunk.emit(Op::Pop, self.line); // discard nil result
2227
2228                // For each non-rest field: dup dict, push key string, subscript, define var.
2229                // For rest field: dup dict, call __dict_rest builtin.
2230                let non_rest: Vec<_> = fields.iter().filter(|f| !f.is_rest).collect();
2231                let rest_field = fields.iter().find(|f| f.is_rest);
2232
2233                for field in &non_rest {
2234                    self.chunk.emit(Op::Dup, self.line);
2235                    let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
2236                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2237                    self.chunk.emit(Op::Subscript, self.line);
2238                    let binding_name = field.alias.as_deref().unwrap_or(&field.key);
2239                    let name_idx = self
2240                        .chunk
2241                        .add_constant(Constant::String(binding_name.to_string()));
2242                    self.chunk.emit_u16(def_op, name_idx, self.line);
2243                }
2244
2245                if let Some(rest) = rest_field {
2246                    // Call the __dict_rest builtin: __dict_rest(dict, [keys_to_exclude])
2247                    // Push function name
2248                    let fn_idx = self
2249                        .chunk
2250                        .add_constant(Constant::String("__dict_rest".into()));
2251                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
2252                    // Swap so dict is above function name: [fn, dict]
2253                    self.chunk.emit(Op::Swap, self.line);
2254                    // Build the exclusion keys list
2255                    for field in &non_rest {
2256                        let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
2257                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2258                    }
2259                    self.chunk
2260                        .emit_u16(Op::BuildList, non_rest.len() as u16, self.line);
2261                    // Call __dict_rest(dict, keys_list) — 2 args
2262                    self.chunk.emit_u8(Op::Call, 2, self.line);
2263                    let rest_name = &rest.key;
2264                    let rest_idx = self.chunk.add_constant(Constant::String(rest_name.clone()));
2265                    self.chunk.emit_u16(def_op, rest_idx, self.line);
2266                } else {
2267                    // Pop the source dict
2268                    self.chunk.emit(Op::Pop, self.line);
2269                }
2270            }
2271            BindingPattern::List(elements) => {
2272                // Stack has the list value.
2273                // Emit runtime type check: __assert_list(value)
2274                self.chunk.emit(Op::Dup, self.line);
2275                let assert_idx = self
2276                    .chunk
2277                    .add_constant(Constant::String("__assert_list".into()));
2278                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
2279                self.chunk.emit(Op::Swap, self.line);
2280                self.chunk.emit_u8(Op::Call, 1, self.line);
2281                self.chunk.emit(Op::Pop, self.line); // discard nil result
2282
2283                let non_rest: Vec<_> = elements.iter().filter(|e| !e.is_rest).collect();
2284                let rest_elem = elements.iter().find(|e| e.is_rest);
2285
2286                for (i, elem) in non_rest.iter().enumerate() {
2287                    self.chunk.emit(Op::Dup, self.line);
2288                    let idx_const = self.chunk.add_constant(Constant::Int(i as i64));
2289                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
2290                    self.chunk.emit(Op::Subscript, self.line);
2291                    let name_idx = self.chunk.add_constant(Constant::String(elem.name.clone()));
2292                    self.chunk.emit_u16(def_op, name_idx, self.line);
2293                }
2294
2295                if let Some(rest) = rest_elem {
2296                    // Slice the list from index non_rest.len() to end: list[n..]
2297                    // Slice op takes: object, start, end on stack
2298                    // self.chunk.emit(Op::Dup, self.line); -- list is still on stack
2299                    let start_idx = self
2300                        .chunk
2301                        .add_constant(Constant::Int(non_rest.len() as i64));
2302                    self.chunk.emit_u16(Op::Constant, start_idx, self.line);
2303                    self.chunk.emit(Op::Nil, self.line); // end = nil (to end)
2304                    self.chunk.emit(Op::Slice, self.line);
2305                    let rest_name_idx =
2306                        self.chunk.add_constant(Constant::String(rest.name.clone()));
2307                    self.chunk.emit_u16(def_op, rest_name_idx, self.line);
2308                } else {
2309                    // Pop the source list
2310                    self.chunk.emit(Op::Pop, self.line);
2311                }
2312            }
2313        }
2314        Ok(())
2315    }
2316
2317    /// Check if a node produces a value on the stack that needs to be popped.
2318    fn produces_value(node: &Node) -> bool {
2319        match node {
2320            // These nodes do NOT produce a value on the stack
2321            Node::LetBinding { .. }
2322            | Node::VarBinding { .. }
2323            | Node::Assignment { .. }
2324            | Node::ReturnStmt { .. }
2325            | Node::FnDecl { .. }
2326            | Node::ImplBlock { .. }
2327            | Node::StructDecl { .. }
2328            | Node::ThrowStmt { .. }
2329            | Node::BreakStmt
2330            | Node::ContinueStmt => false,
2331            // These compound nodes explicitly produce a value
2332            Node::TryCatch { .. }
2333            | Node::TryExpr { .. }
2334            | Node::Retry { .. }
2335            | Node::GuardStmt { .. }
2336            | Node::DeadlineBlock { .. }
2337            | Node::MutexBlock { .. }
2338            | Node::Spread(_) => true,
2339            // All other expressions produce values
2340            _ => true,
2341        }
2342    }
2343}
2344
2345impl Compiler {
2346    /// Compile a function body into a CompiledFunction (for import support).
2347    pub fn compile_fn_body(
2348        &mut self,
2349        params: &[TypedParam],
2350        body: &[SNode],
2351    ) -> Result<CompiledFunction, CompileError> {
2352        let mut fn_compiler = Compiler::new();
2353        fn_compiler.compile_block(body)?;
2354        fn_compiler.chunk.emit(Op::Nil, 0);
2355        fn_compiler.chunk.emit(Op::Return, 0);
2356        Ok(CompiledFunction {
2357            name: String::new(),
2358            params: TypedParam::names(params),
2359            default_start: TypedParam::default_start(params),
2360            chunk: fn_compiler.chunk,
2361        })
2362    }
2363
2364    /// Compile a match arm body, ensuring it always pushes exactly one value.
2365    fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
2366        if body.is_empty() {
2367            self.chunk.emit(Op::Nil, self.line);
2368        } else {
2369            self.compile_block(body)?;
2370            // If the last statement doesn't produce a value, push nil
2371            if !Self::produces_value(&body.last().unwrap().node) {
2372                self.chunk.emit(Op::Nil, self.line);
2373            }
2374        }
2375        Ok(())
2376    }
2377
2378    /// Emit the binary op instruction for a compound assignment operator.
2379    fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
2380        match op {
2381            "+" => self.chunk.emit(Op::Add, self.line),
2382            "-" => self.chunk.emit(Op::Sub, self.line),
2383            "*" => self.chunk.emit(Op::Mul, self.line),
2384            "/" => self.chunk.emit(Op::Div, self.line),
2385            "%" => self.chunk.emit(Op::Mod, self.line),
2386            _ => {
2387                return Err(CompileError {
2388                    message: format!("Unknown compound operator: {op}"),
2389                    line: self.line,
2390                })
2391            }
2392        }
2393        Ok(())
2394    }
2395
2396    /// Extract the root variable name from a (possibly nested) access expression.
2397    fn root_var_name(&self, node: &SNode) -> Option<String> {
2398        match &node.node {
2399            Node::Identifier(name) => Some(name.clone()),
2400            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
2401                self.root_var_name(object)
2402            }
2403            Node::SubscriptAccess { object, .. } => self.root_var_name(object),
2404            _ => None,
2405        }
2406    }
2407}
2408
2409impl Compiler {
2410    /// Recursively collect all enum type names from the AST.
2411    fn collect_enum_names(nodes: &[SNode], names: &mut std::collections::HashSet<String>) {
2412        for sn in nodes {
2413            match &sn.node {
2414                Node::EnumDecl { name, .. } => {
2415                    names.insert(name.clone());
2416                }
2417                Node::Pipeline { body, .. } => {
2418                    Self::collect_enum_names(body, names);
2419                }
2420                Node::FnDecl { body, .. } => {
2421                    Self::collect_enum_names(body, names);
2422                }
2423                Node::Block(stmts) => {
2424                    Self::collect_enum_names(stmts, names);
2425                }
2426                _ => {}
2427            }
2428        }
2429    }
2430}
2431
2432impl Default for Compiler {
2433    fn default() -> Self {
2434        Self::new()
2435    }
2436}
2437
2438/// Check if an AST node contains `_` identifier (pipe placeholder).
2439fn contains_pipe_placeholder(node: &SNode) -> bool {
2440    match &node.node {
2441        Node::Identifier(name) if name == "_" => true,
2442        Node::FunctionCall { args, .. } => args.iter().any(contains_pipe_placeholder),
2443        Node::MethodCall { object, args, .. } => {
2444            contains_pipe_placeholder(object) || args.iter().any(contains_pipe_placeholder)
2445        }
2446        Node::BinaryOp { left, right, .. } => {
2447            contains_pipe_placeholder(left) || contains_pipe_placeholder(right)
2448        }
2449        Node::UnaryOp { operand, .. } => contains_pipe_placeholder(operand),
2450        Node::ListLiteral(items) => items.iter().any(contains_pipe_placeholder),
2451        Node::PropertyAccess { object, .. } => contains_pipe_placeholder(object),
2452        Node::SubscriptAccess { object, index } => {
2453            contains_pipe_placeholder(object) || contains_pipe_placeholder(index)
2454        }
2455        _ => false,
2456    }
2457}
2458
2459/// Replace all `_` identifiers with `__pipe` in an AST node (for pipe placeholder desugaring).
2460fn replace_pipe_placeholder(node: &SNode) -> SNode {
2461    let new_node = match &node.node {
2462        Node::Identifier(name) if name == "_" => Node::Identifier("__pipe".into()),
2463        Node::FunctionCall { name, args } => Node::FunctionCall {
2464            name: name.clone(),
2465            args: args.iter().map(replace_pipe_placeholder).collect(),
2466        },
2467        Node::MethodCall {
2468            object,
2469            method,
2470            args,
2471        } => Node::MethodCall {
2472            object: Box::new(replace_pipe_placeholder(object)),
2473            method: method.clone(),
2474            args: args.iter().map(replace_pipe_placeholder).collect(),
2475        },
2476        Node::BinaryOp { op, left, right } => Node::BinaryOp {
2477            op: op.clone(),
2478            left: Box::new(replace_pipe_placeholder(left)),
2479            right: Box::new(replace_pipe_placeholder(right)),
2480        },
2481        Node::UnaryOp { op, operand } => Node::UnaryOp {
2482            op: op.clone(),
2483            operand: Box::new(replace_pipe_placeholder(operand)),
2484        },
2485        Node::ListLiteral(items) => {
2486            Node::ListLiteral(items.iter().map(replace_pipe_placeholder).collect())
2487        }
2488        Node::PropertyAccess { object, property } => Node::PropertyAccess {
2489            object: Box::new(replace_pipe_placeholder(object)),
2490            property: property.clone(),
2491        },
2492        Node::SubscriptAccess { object, index } => Node::SubscriptAccess {
2493            object: Box::new(replace_pipe_placeholder(object)),
2494            index: Box::new(replace_pipe_placeholder(index)),
2495        },
2496        _ => return node.clone(),
2497    };
2498    SNode::new(new_node, node.span)
2499}
2500
2501#[cfg(test)]
2502mod tests {
2503    use super::*;
2504    use harn_lexer::Lexer;
2505    use harn_parser::Parser;
2506
2507    fn compile_source(source: &str) -> Chunk {
2508        let mut lexer = Lexer::new(source);
2509        let tokens = lexer.tokenize().unwrap();
2510        let mut parser = Parser::new(tokens);
2511        let program = parser.parse().unwrap();
2512        Compiler::new().compile(&program).unwrap()
2513    }
2514
2515    #[test]
2516    fn test_compile_arithmetic() {
2517        let chunk = compile_source("pipeline test(task) { let x = 2 + 3 }");
2518        assert!(!chunk.code.is_empty());
2519        // Should have constants: 2, 3, "x"
2520        assert!(chunk.constants.contains(&Constant::Int(2)));
2521        assert!(chunk.constants.contains(&Constant::Int(3)));
2522    }
2523
2524    #[test]
2525    fn test_compile_function_call() {
2526        let chunk = compile_source("pipeline test(task) { log(42) }");
2527        let disasm = chunk.disassemble("test");
2528        assert!(disasm.contains("CALL"));
2529    }
2530
2531    #[test]
2532    fn test_compile_if_else() {
2533        let chunk =
2534            compile_source(r#"pipeline test(task) { if true { log("yes") } else { log("no") } }"#);
2535        let disasm = chunk.disassemble("test");
2536        assert!(disasm.contains("JUMP_IF_FALSE"));
2537        assert!(disasm.contains("JUMP"));
2538    }
2539
2540    #[test]
2541    fn test_compile_while() {
2542        let chunk = compile_source("pipeline test(task) { var i = 0\n while i < 5 { i = i + 1 } }");
2543        let disasm = chunk.disassemble("test");
2544        assert!(disasm.contains("JUMP_IF_FALSE"));
2545        // Should have a backward jump
2546        assert!(disasm.contains("JUMP"));
2547    }
2548
2549    #[test]
2550    fn test_compile_closure() {
2551        let chunk = compile_source("pipeline test(task) { let f = { x -> x * 2 } }");
2552        assert!(!chunk.functions.is_empty());
2553        assert_eq!(chunk.functions[0].params, vec!["x"]);
2554    }
2555
2556    #[test]
2557    fn test_compile_list() {
2558        let chunk = compile_source("pipeline test(task) { let a = [1, 2, 3] }");
2559        let disasm = chunk.disassemble("test");
2560        assert!(disasm.contains("BUILD_LIST"));
2561    }
2562
2563    #[test]
2564    fn test_compile_dict() {
2565        let chunk = compile_source(r#"pipeline test(task) { let d = {name: "test"} }"#);
2566        let disasm = chunk.disassemble("test");
2567        assert!(disasm.contains("BUILD_DICT"));
2568    }
2569
2570    #[test]
2571    fn test_disassemble() {
2572        let chunk = compile_source("pipeline test(task) { log(2 + 3) }");
2573        let disasm = chunk.disassemble("test");
2574        // Should be readable
2575        assert!(disasm.contains("CONSTANT"));
2576        assert!(disasm.contains("ADD"));
2577        assert!(disasm.contains("CALL"));
2578    }
2579}