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