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