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