Skip to main content

harn_vm/
compiler.rs

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