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