Skip to main content

harn_vm/
compiler.rs

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