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