Skip to main content

harn_vm/
compiler.rs

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