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