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