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) | Node::RawStringLiteral(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                                    rest: false,
621                                }],
622                                body: vec![replaced],
623                                fn_syntax: false,
624                            });
625                            self.compile_node(&closure_node)?;
626                        } else {
627                            self.compile_node(right)?;
628                        }
629                        self.chunk.emit(Op::Pipe, self.line);
630                        return Ok(());
631                    }
632                    _ => {}
633                }
634
635                self.compile_node(left)?;
636                self.compile_node(right)?;
637                match op.as_str() {
638                    "+" => self.chunk.emit(Op::Add, self.line),
639                    "-" => self.chunk.emit(Op::Sub, self.line),
640                    "*" => self.chunk.emit(Op::Mul, self.line),
641                    "/" => self.chunk.emit(Op::Div, self.line),
642                    "%" => self.chunk.emit(Op::Mod, self.line),
643                    "==" => self.chunk.emit(Op::Equal, self.line),
644                    "!=" => self.chunk.emit(Op::NotEqual, self.line),
645                    "<" => self.chunk.emit(Op::Less, self.line),
646                    ">" => self.chunk.emit(Op::Greater, self.line),
647                    "<=" => self.chunk.emit(Op::LessEqual, self.line),
648                    ">=" => self.chunk.emit(Op::GreaterEqual, self.line),
649                    "in" => self.chunk.emit(Op::Contains, self.line),
650                    "not_in" => {
651                        self.chunk.emit(Op::Contains, self.line);
652                        self.chunk.emit(Op::Not, self.line);
653                    }
654                    _ => {
655                        return Err(CompileError {
656                            message: format!("Unknown operator: {op}"),
657                            line: self.line,
658                        })
659                    }
660                }
661            }
662
663            Node::UnaryOp { op, operand } => {
664                self.compile_node(operand)?;
665                match op.as_str() {
666                    "-" => self.chunk.emit(Op::Negate, self.line),
667                    "!" => self.chunk.emit(Op::Not, self.line),
668                    _ => {}
669                }
670            }
671
672            Node::Ternary {
673                condition,
674                true_expr,
675                false_expr,
676            } => {
677                self.compile_node(condition)?;
678                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
679                self.chunk.emit(Op::Pop, self.line);
680                self.compile_node(true_expr)?;
681                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
682                self.chunk.patch_jump(else_jump);
683                self.chunk.emit(Op::Pop, self.line);
684                self.compile_node(false_expr)?;
685                self.chunk.patch_jump(end_jump);
686            }
687
688            Node::FunctionCall { name, args } => {
689                let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
690                // Push function name as string constant
691                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
692                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
693
694                if has_spread {
695                    // Build the args into a single list using the flush-and-concat
696                    // pattern (same as ListLiteral with spreads).
697                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
698                    let mut pending = 0u16;
699                    for arg in args {
700                        if let Node::Spread(inner) = &arg.node {
701                            if pending > 0 {
702                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
703                                self.chunk.emit(Op::Add, self.line);
704                                pending = 0;
705                            }
706                            self.compile_node(inner)?;
707                            self.chunk.emit(Op::Dup, self.line);
708                            let assert_idx = self
709                                .chunk
710                                .add_constant(Constant::String("__assert_list".into()));
711                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
712                            self.chunk.emit(Op::Swap, self.line);
713                            self.chunk.emit_u8(Op::Call, 1, self.line);
714                            self.chunk.emit(Op::Pop, self.line);
715                            self.chunk.emit(Op::Add, self.line);
716                        } else {
717                            self.compile_node(arg)?;
718                            pending += 1;
719                        }
720                    }
721                    if pending > 0 {
722                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
723                        self.chunk.emit(Op::Add, self.line);
724                    }
725                    self.chunk.emit(Op::CallSpread, self.line);
726                } else {
727                    // Push arguments normally
728                    for arg in args {
729                        self.compile_node(arg)?;
730                    }
731                    self.chunk.emit_u8(Op::Call, args.len() as u8, self.line);
732                }
733            }
734
735            Node::MethodCall {
736                object,
737                method,
738                args,
739            } => {
740                // Check if this is an enum variant construction with args: EnumName.Variant(args)
741                if let Node::Identifier(name) = &object.node {
742                    if self.enum_names.contains(name) {
743                        // Compile args, then BuildEnum
744                        for arg in args {
745                            self.compile_node(arg)?;
746                        }
747                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
748                        let var_idx = self.chunk.add_constant(Constant::String(method.clone()));
749                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
750                        let hi = (var_idx >> 8) as u8;
751                        let lo = var_idx as u8;
752                        self.chunk.code.push(hi);
753                        self.chunk.code.push(lo);
754                        self.chunk.lines.push(self.line);
755                        self.chunk.columns.push(self.column);
756                        self.chunk.lines.push(self.line);
757                        self.chunk.columns.push(self.column);
758                        let fc = args.len() as u16;
759                        let fhi = (fc >> 8) as u8;
760                        let flo = fc as u8;
761                        self.chunk.code.push(fhi);
762                        self.chunk.code.push(flo);
763                        self.chunk.lines.push(self.line);
764                        self.chunk.columns.push(self.column);
765                        self.chunk.lines.push(self.line);
766                        self.chunk.columns.push(self.column);
767                        return Ok(());
768                    }
769                }
770                let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
771                self.compile_node(object)?;
772                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
773                if has_spread {
774                    // Build args into a single list (same pattern as FunctionCall spread)
775                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
776                    let mut pending = 0u16;
777                    for arg in args {
778                        if let Node::Spread(inner) = &arg.node {
779                            if pending > 0 {
780                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
781                                self.chunk.emit(Op::Add, self.line);
782                                pending = 0;
783                            }
784                            self.compile_node(inner)?;
785                            self.chunk.emit(Op::Dup, self.line);
786                            let assert_idx = self
787                                .chunk
788                                .add_constant(Constant::String("__assert_list".into()));
789                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
790                            self.chunk.emit(Op::Swap, self.line);
791                            self.chunk.emit_u8(Op::Call, 1, self.line);
792                            self.chunk.emit(Op::Pop, self.line);
793                            self.chunk.emit(Op::Add, self.line);
794                        } else {
795                            self.compile_node(arg)?;
796                            pending += 1;
797                        }
798                    }
799                    if pending > 0 {
800                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
801                        self.chunk.emit(Op::Add, self.line);
802                    }
803                    self.chunk
804                        .emit_u16(Op::MethodCallSpread, name_idx, self.line);
805                } else {
806                    for arg in args {
807                        self.compile_node(arg)?;
808                    }
809                    self.chunk
810                        .emit_method_call(name_idx, args.len() as u8, self.line);
811                }
812            }
813
814            Node::OptionalMethodCall {
815                object,
816                method,
817                args,
818            } => {
819                self.compile_node(object)?;
820                for arg in args {
821                    self.compile_node(arg)?;
822                }
823                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
824                self.chunk
825                    .emit_method_call_opt(name_idx, args.len() as u8, self.line);
826            }
827
828            Node::PropertyAccess { object, property } => {
829                // Check if this is an enum variant construction: EnumName.Variant
830                if let Node::Identifier(name) = &object.node {
831                    if self.enum_names.contains(name) {
832                        // Emit BuildEnum with 0 fields
833                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
834                        let var_idx = self.chunk.add_constant(Constant::String(property.clone()));
835                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
836                        let hi = (var_idx >> 8) as u8;
837                        let lo = var_idx as u8;
838                        self.chunk.code.push(hi);
839                        self.chunk.code.push(lo);
840                        self.chunk.lines.push(self.line);
841                        self.chunk.columns.push(self.column);
842                        self.chunk.lines.push(self.line);
843                        self.chunk.columns.push(self.column);
844                        // 0 fields
845                        self.chunk.code.push(0);
846                        self.chunk.code.push(0);
847                        self.chunk.lines.push(self.line);
848                        self.chunk.columns.push(self.column);
849                        self.chunk.lines.push(self.line);
850                        self.chunk.columns.push(self.column);
851                        return Ok(());
852                    }
853                }
854                self.compile_node(object)?;
855                let idx = self.chunk.add_constant(Constant::String(property.clone()));
856                self.chunk.emit_u16(Op::GetProperty, idx, self.line);
857            }
858
859            Node::OptionalPropertyAccess { object, property } => {
860                self.compile_node(object)?;
861                let idx = self.chunk.add_constant(Constant::String(property.clone()));
862                self.chunk.emit_u16(Op::GetPropertyOpt, idx, self.line);
863            }
864
865            Node::SubscriptAccess { object, index } => {
866                self.compile_node(object)?;
867                self.compile_node(index)?;
868                self.chunk.emit(Op::Subscript, self.line);
869            }
870
871            Node::SliceAccess { object, start, end } => {
872                self.compile_node(object)?;
873                if let Some(s) = start {
874                    self.compile_node(s)?;
875                } else {
876                    self.chunk.emit(Op::Nil, self.line);
877                }
878                if let Some(e) = end {
879                    self.compile_node(e)?;
880                } else {
881                    self.chunk.emit(Op::Nil, self.line);
882                }
883                self.chunk.emit(Op::Slice, self.line);
884            }
885
886            Node::IfElse {
887                condition,
888                then_body,
889                else_body,
890            } => {
891                self.compile_node(condition)?;
892                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
893                self.chunk.emit(Op::Pop, self.line);
894                self.compile_scoped_block(then_body)?;
895                if let Some(else_body) = else_body {
896                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
897                    self.chunk.patch_jump(else_jump);
898                    self.chunk.emit(Op::Pop, self.line);
899                    self.compile_scoped_block(else_body)?;
900                    self.chunk.patch_jump(end_jump);
901                } else {
902                    self.chunk.patch_jump(else_jump);
903                    self.chunk.emit(Op::Pop, self.line);
904                    self.chunk.emit(Op::Nil, self.line);
905                }
906            }
907
908            Node::WhileLoop { condition, body } => {
909                let loop_start = self.chunk.current_offset();
910                self.loop_stack.push(LoopContext {
911                    start_offset: loop_start,
912                    break_patches: Vec::new(),
913                    has_iterator: false,
914                    handler_depth: self.handler_depth,
915                    finally_depth: self.finally_bodies.len(),
916                    scope_depth: self.scope_depth,
917                });
918                self.compile_node(condition)?;
919                let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
920                self.chunk.emit(Op::Pop, self.line); // pop condition
921                self.compile_scoped_statements(body)?;
922                // Jump back to condition
923                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
924                self.chunk.patch_jump(exit_jump);
925                self.chunk.emit(Op::Pop, self.line); // pop condition
926                                                     // Patch all break jumps to here
927                let ctx = self.loop_stack.pop().unwrap();
928                for patch_pos in ctx.break_patches {
929                    self.chunk.patch_jump(patch_pos);
930                }
931                self.chunk.emit(Op::Nil, self.line);
932            }
933
934            Node::ForIn {
935                pattern,
936                iterable,
937                body,
938            } => {
939                // Compile iterable
940                self.compile_node(iterable)?;
941                // Initialize iterator
942                self.chunk.emit(Op::IterInit, self.line);
943                let loop_start = self.chunk.current_offset();
944                self.loop_stack.push(LoopContext {
945                    start_offset: loop_start,
946                    break_patches: Vec::new(),
947                    has_iterator: true,
948                    handler_depth: self.handler_depth,
949                    finally_depth: self.finally_bodies.len(),
950                    scope_depth: self.scope_depth,
951                });
952                // Try to get next item — jumps to end if exhausted
953                let exit_jump_pos = self.chunk.emit_jump(Op::IterNext, self.line);
954                self.begin_scope();
955                // Define loop variable(s) with current item (item is on stack from IterNext)
956                self.compile_destructuring(pattern, true)?;
957                for sn in body {
958                    self.compile_node(sn)?;
959                    if Self::produces_value(&sn.node) {
960                        self.chunk.emit(Op::Pop, self.line);
961                    }
962                }
963                self.end_scope();
964                // Loop back
965                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
966                self.chunk.patch_jump(exit_jump_pos);
967                // Patch all break jumps to here
968                let ctx = self.loop_stack.pop().unwrap();
969                for patch_pos in ctx.break_patches {
970                    self.chunk.patch_jump(patch_pos);
971                }
972                // Push nil as result (iterator state was consumed)
973                self.chunk.emit(Op::Nil, self.line);
974            }
975
976            Node::ReturnStmt { value } => {
977                let has_pending_finally = !self.finally_bodies.is_empty();
978
979                if has_pending_finally {
980                    // Inside try-finally: compile value, save to temp,
981                    // run pending finallys, restore value, then return.
982                    if let Some(val) = value {
983                        self.compile_node(val)?;
984                    } else {
985                        self.chunk.emit(Op::Nil, self.line);
986                    }
987                    self.temp_counter += 1;
988                    let temp_name = format!("__return_val_{}__", self.temp_counter);
989                    let save_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
990                    self.chunk.emit_u16(Op::DefVar, save_idx, self.line);
991                    // Emit all pending finallys (innermost first = reverse order)
992                    let finallys: Vec<_> = self.finally_bodies.iter().rev().cloned().collect();
993                    for fb in &finallys {
994                        self.compile_finally_inline(fb)?;
995                    }
996                    let restore_idx = self.chunk.add_constant(Constant::String(temp_name));
997                    self.chunk.emit_u16(Op::GetVar, restore_idx, self.line);
998                    self.chunk.emit(Op::Return, self.line);
999                } else {
1000                    // No pending finally — original behavior with tail call optimization
1001                    if let Some(val) = value {
1002                        if let Node::FunctionCall { name, args } = &val.node {
1003                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1004                            self.chunk.emit_u16(Op::Constant, name_idx, self.line);
1005                            for arg in args {
1006                                self.compile_node(arg)?;
1007                            }
1008                            self.chunk
1009                                .emit_u8(Op::TailCall, args.len() as u8, self.line);
1010                        } else if let Node::BinaryOp { op, left, right } = &val.node {
1011                            if op == "|>" {
1012                                self.compile_node(left)?;
1013                                self.compile_node(right)?;
1014                                self.chunk.emit(Op::Swap, self.line);
1015                                self.chunk.emit_u8(Op::TailCall, 1, self.line);
1016                            } else {
1017                                self.compile_node(val)?;
1018                            }
1019                        } else {
1020                            self.compile_node(val)?;
1021                        }
1022                    } else {
1023                        self.chunk.emit(Op::Nil, self.line);
1024                    }
1025                    self.chunk.emit(Op::Return, self.line);
1026                }
1027            }
1028
1029            Node::BreakStmt => {
1030                if self.loop_stack.is_empty() {
1031                    return Err(CompileError {
1032                        message: "break outside of loop".to_string(),
1033                        line: self.line,
1034                    });
1035                }
1036                // Copy values out to avoid borrow conflict
1037                let ctx = self.loop_stack.last().unwrap();
1038                let finally_depth = ctx.finally_depth;
1039                let handler_depth = ctx.handler_depth;
1040                let has_iterator = ctx.has_iterator;
1041                let scope_depth = ctx.scope_depth;
1042                // Pop exception handlers that were pushed inside the loop
1043                for _ in handler_depth..self.handler_depth {
1044                    self.chunk.emit(Op::PopHandler, self.line);
1045                }
1046                // Emit pending finallys that are inside the loop
1047                if self.finally_bodies.len() > finally_depth {
1048                    let finallys: Vec<_> = self.finally_bodies[finally_depth..]
1049                        .iter()
1050                        .rev()
1051                        .cloned()
1052                        .collect();
1053                    for fb in &finallys {
1054                        self.compile_finally_inline(fb)?;
1055                    }
1056                }
1057                self.unwind_scopes_to(scope_depth);
1058                if has_iterator {
1059                    self.chunk.emit(Op::PopIterator, self.line);
1060                }
1061                let patch = self.chunk.emit_jump(Op::Jump, self.line);
1062                self.loop_stack
1063                    .last_mut()
1064                    .unwrap()
1065                    .break_patches
1066                    .push(patch);
1067            }
1068
1069            Node::ContinueStmt => {
1070                if self.loop_stack.is_empty() {
1071                    return Err(CompileError {
1072                        message: "continue outside of loop".to_string(),
1073                        line: self.line,
1074                    });
1075                }
1076                let ctx = self.loop_stack.last().unwrap();
1077                let finally_depth = ctx.finally_depth;
1078                let handler_depth = ctx.handler_depth;
1079                let loop_start = ctx.start_offset;
1080                let scope_depth = ctx.scope_depth;
1081                for _ in handler_depth..self.handler_depth {
1082                    self.chunk.emit(Op::PopHandler, self.line);
1083                }
1084                if self.finally_bodies.len() > finally_depth {
1085                    let finallys: Vec<_> = self.finally_bodies[finally_depth..]
1086                        .iter()
1087                        .rev()
1088                        .cloned()
1089                        .collect();
1090                    for fb in &finallys {
1091                        self.compile_finally_inline(fb)?;
1092                    }
1093                }
1094                self.unwind_scopes_to(scope_depth);
1095                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
1096            }
1097
1098            Node::ListLiteral(elements) => {
1099                let has_spread = elements.iter().any(|e| matches!(&e.node, Node::Spread(_)));
1100                if !has_spread {
1101                    for el in elements {
1102                        self.compile_node(el)?;
1103                    }
1104                    self.chunk
1105                        .emit_u16(Op::BuildList, elements.len() as u16, self.line);
1106                } else {
1107                    // Build with spreads: accumulate segments into lists and concat
1108                    // Start with empty list
1109                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
1110                    let mut pending = 0u16;
1111                    for el in elements {
1112                        if let Node::Spread(inner) = &el.node {
1113                            // First, build list from pending non-spread elements
1114                            if pending > 0 {
1115                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
1116                                // Concat accumulated + pending segment
1117                                self.chunk.emit(Op::Add, self.line);
1118                                pending = 0;
1119                            }
1120                            // Concat with the spread expression (with type check)
1121                            self.compile_node(inner)?;
1122                            self.chunk.emit(Op::Dup, self.line);
1123                            let assert_idx = self
1124                                .chunk
1125                                .add_constant(Constant::String("__assert_list".into()));
1126                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1127                            self.chunk.emit(Op::Swap, self.line);
1128                            self.chunk.emit_u8(Op::Call, 1, self.line);
1129                            self.chunk.emit(Op::Pop, self.line);
1130                            self.chunk.emit(Op::Add, self.line);
1131                        } else {
1132                            self.compile_node(el)?;
1133                            pending += 1;
1134                        }
1135                    }
1136                    if pending > 0 {
1137                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
1138                        self.chunk.emit(Op::Add, self.line);
1139                    }
1140                }
1141            }
1142
1143            Node::DictLiteral(entries) => {
1144                let has_spread = entries
1145                    .iter()
1146                    .any(|e| matches!(&e.value.node, Node::Spread(_)));
1147                if !has_spread {
1148                    for entry in entries {
1149                        self.compile_node(&entry.key)?;
1150                        self.compile_node(&entry.value)?;
1151                    }
1152                    self.chunk
1153                        .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
1154                } else {
1155                    // Build with spreads: use empty dict + Add for merging
1156                    self.chunk.emit_u16(Op::BuildDict, 0, self.line);
1157                    let mut pending = 0u16;
1158                    for entry in entries {
1159                        if let Node::Spread(inner) = &entry.value.node {
1160                            // Flush pending entries
1161                            if pending > 0 {
1162                                self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1163                                self.chunk.emit(Op::Add, self.line);
1164                                pending = 0;
1165                            }
1166                            // Merge spread dict via Add (with type check)
1167                            self.compile_node(inner)?;
1168                            self.chunk.emit(Op::Dup, self.line);
1169                            let assert_idx = self
1170                                .chunk
1171                                .add_constant(Constant::String("__assert_dict".into()));
1172                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1173                            self.chunk.emit(Op::Swap, self.line);
1174                            self.chunk.emit_u8(Op::Call, 1, self.line);
1175                            self.chunk.emit(Op::Pop, self.line);
1176                            self.chunk.emit(Op::Add, self.line);
1177                        } else {
1178                            self.compile_node(&entry.key)?;
1179                            self.compile_node(&entry.value)?;
1180                            pending += 1;
1181                        }
1182                    }
1183                    if pending > 0 {
1184                        self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1185                        self.chunk.emit(Op::Add, self.line);
1186                    }
1187                }
1188            }
1189
1190            Node::InterpolatedString(segments) => {
1191                let mut part_count = 0u16;
1192                for seg in segments {
1193                    match seg {
1194                        StringSegment::Literal(s) => {
1195                            let idx = self.chunk.add_constant(Constant::String(s.clone()));
1196                            self.chunk.emit_u16(Op::Constant, idx, self.line);
1197                            part_count += 1;
1198                        }
1199                        StringSegment::Expression(expr_str, expr_line, expr_col) => {
1200                            // Parse and compile the embedded expression
1201                            let mut lexer =
1202                                harn_lexer::Lexer::with_position(expr_str, *expr_line, *expr_col);
1203                            if let Ok(tokens) = lexer.tokenize() {
1204                                let mut parser = harn_parser::Parser::new(tokens);
1205                                if let Ok(snode) = parser.parse_single_expression() {
1206                                    self.compile_node(&snode)?;
1207                                    // Convert result to string for concatenation
1208                                    let to_str = self
1209                                        .chunk
1210                                        .add_constant(Constant::String("to_string".into()));
1211                                    self.chunk.emit_u16(Op::Constant, to_str, self.line);
1212                                    self.chunk.emit(Op::Swap, self.line);
1213                                    self.chunk.emit_u8(Op::Call, 1, self.line);
1214                                    part_count += 1;
1215                                } else {
1216                                    // Fallback: treat as literal string
1217                                    let idx =
1218                                        self.chunk.add_constant(Constant::String(expr_str.clone()));
1219                                    self.chunk.emit_u16(Op::Constant, idx, self.line);
1220                                    part_count += 1;
1221                                }
1222                            }
1223                        }
1224                    }
1225                }
1226                if part_count > 1 {
1227                    self.chunk.emit_u16(Op::Concat, part_count, self.line);
1228                }
1229            }
1230
1231            Node::FnDecl {
1232                name, params, body, ..
1233            } => {
1234                // Compile function body into a separate chunk
1235                let mut fn_compiler = Compiler::new();
1236                fn_compiler.enum_names = self.enum_names.clone();
1237                fn_compiler.emit_default_preamble(params)?;
1238                fn_compiler.emit_type_checks(params);
1239                let is_gen = body_contains_yield(body);
1240                fn_compiler.compile_block(body)?;
1241                fn_compiler.chunk.emit(Op::Nil, self.line);
1242                fn_compiler.chunk.emit(Op::Return, self.line);
1243
1244                let func = CompiledFunction {
1245                    name: name.clone(),
1246                    params: TypedParam::names(params),
1247                    default_start: TypedParam::default_start(params),
1248                    chunk: fn_compiler.chunk,
1249                    is_generator: is_gen,
1250                    has_rest_param: params.last().is_some_and(|p| p.rest),
1251                };
1252                let fn_idx = self.chunk.functions.len();
1253                self.chunk.functions.push(func);
1254
1255                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1256                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1257                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1258            }
1259
1260            Node::ToolDecl {
1261                name,
1262                description,
1263                params,
1264                return_type: _,
1265                body,
1266                ..
1267            } => {
1268                // 1. Compile the body as a closure (same as FnDecl)
1269                let mut fn_compiler = Compiler::new();
1270                fn_compiler.enum_names = self.enum_names.clone();
1271                fn_compiler.emit_default_preamble(params)?;
1272                fn_compiler.emit_type_checks(params);
1273                fn_compiler.compile_block(body)?;
1274                // Use closure-like return: the last expression value is the return value
1275                fn_compiler.chunk.emit(Op::Return, self.line);
1276
1277                let func = CompiledFunction {
1278                    name: name.clone(),
1279                    params: TypedParam::names(params),
1280                    default_start: TypedParam::default_start(params),
1281                    chunk: fn_compiler.chunk,
1282                    is_generator: false,
1283                    has_rest_param: params.last().is_some_and(|p| p.rest),
1284                };
1285                let fn_idx = self.chunk.functions.len();
1286                self.chunk.functions.push(func);
1287
1288                // 2. Push "tool_define" function name (Call convention: fn first, then args)
1289                let define_name = self
1290                    .chunk
1291                    .add_constant(Constant::String("tool_define".into()));
1292                self.chunk.emit_u16(Op::Constant, define_name, self.line);
1293
1294                // 3. Arg 1: tool_registry()
1295                let reg_name = self
1296                    .chunk
1297                    .add_constant(Constant::String("tool_registry".into()));
1298                self.chunk.emit_u16(Op::Constant, reg_name, self.line);
1299                self.chunk.emit_u8(Op::Call, 0, self.line);
1300
1301                // 4. Arg 2: tool name string
1302                let tool_name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1303                self.chunk.emit_u16(Op::Constant, tool_name_idx, self.line);
1304
1305                // 5. Arg 3: description string
1306                let desc = description.as_deref().unwrap_or("");
1307                let desc_idx = self.chunk.add_constant(Constant::String(desc.to_string()));
1308                self.chunk.emit_u16(Op::Constant, desc_idx, self.line);
1309
1310                // 6. Arg 4: config dict { parameters: {...}, handler: <closure> }
1311                // Build parameters dict: { param_name: { type: "<json_type>" }, ... }
1312                let mut param_count: u16 = 0;
1313                for p in params {
1314                    let pn_idx = self.chunk.add_constant(Constant::String(p.name.clone()));
1315                    self.chunk.emit_u16(Op::Constant, pn_idx, self.line);
1316
1317                    let type_str = match &p.type_expr {
1318                        Some(harn_parser::TypeExpr::Named(n)) => match n.as_str() {
1319                            "string" => "string",
1320                            "int" => "integer",
1321                            "float" => "number",
1322                            "bool" => "boolean",
1323                            _ => "string",
1324                        },
1325                        _ => "string",
1326                    };
1327                    let type_key = self.chunk.add_constant(Constant::String("type".into()));
1328                    self.chunk.emit_u16(Op::Constant, type_key, self.line);
1329                    let type_val = self.chunk.add_constant(Constant::String(type_str.into()));
1330                    self.chunk.emit_u16(Op::Constant, type_val, self.line);
1331                    let mut param_schema_entries: u16 = 1;
1332                    if let Some(default_expr) = &p.default_value {
1333                        let required_key =
1334                            self.chunk.add_constant(Constant::String("required".into()));
1335                        self.chunk.emit_u16(Op::Constant, required_key, self.line);
1336                        let required_val = self.chunk.add_constant(Constant::Bool(false));
1337                        self.chunk.emit_u16(Op::Constant, required_val, self.line);
1338                        let default_key =
1339                            self.chunk.add_constant(Constant::String("default".into()));
1340                        self.chunk.emit_u16(Op::Constant, default_key, self.line);
1341                        self.compile_node(default_expr)?;
1342                        param_schema_entries += 2;
1343                    }
1344                    self.chunk
1345                        .emit_u16(Op::BuildDict, param_schema_entries, self.line);
1346                    param_count += 1;
1347                }
1348                self.chunk.emit_u16(Op::BuildDict, param_count, self.line);
1349
1350                // Build config dict: { "parameters": <params_dict>, "handler": <closure> }
1351                let params_key = self
1352                    .chunk
1353                    .add_constant(Constant::String("parameters".into()));
1354                self.chunk.emit_u16(Op::Constant, params_key, self.line);
1355                self.chunk.emit(Op::Swap, self.line);
1356
1357                let handler_key = self.chunk.add_constant(Constant::String("handler".into()));
1358                self.chunk.emit_u16(Op::Constant, handler_key, self.line);
1359                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1360
1361                self.chunk.emit_u16(Op::BuildDict, 2, self.line);
1362
1363                // 7. Call tool_define(registry, name, description, config)
1364                self.chunk.emit_u8(Op::Call, 4, self.line);
1365
1366                // 8. Bind the result (the registry with the tool) as a let variable
1367                let bind_idx = self.chunk.add_constant(Constant::String(name.clone()));
1368                self.chunk.emit_u16(Op::DefLet, bind_idx, self.line);
1369            }
1370
1371            Node::Closure { params, body, .. } => {
1372                let mut fn_compiler = Compiler::new();
1373                fn_compiler.enum_names = self.enum_names.clone();
1374                fn_compiler.emit_default_preamble(params)?;
1375                fn_compiler.emit_type_checks(params);
1376                let is_gen = body_contains_yield(body);
1377                fn_compiler.compile_block(body)?;
1378                // If block didn't end with return, the last value is on the stack
1379                fn_compiler.chunk.emit(Op::Return, self.line);
1380
1381                let func = CompiledFunction {
1382                    name: "<closure>".to_string(),
1383                    params: TypedParam::names(params),
1384                    default_start: TypedParam::default_start(params),
1385                    chunk: fn_compiler.chunk,
1386                    is_generator: is_gen,
1387                    has_rest_param: false,
1388                };
1389                let fn_idx = self.chunk.functions.len();
1390                self.chunk.functions.push(func);
1391
1392                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1393            }
1394
1395            Node::ThrowStmt { value } => {
1396                self.compile_node(value)?;
1397                self.chunk.emit(Op::Throw, self.line);
1398            }
1399
1400            Node::MatchExpr { value, arms } => {
1401                self.compile_node(value)?;
1402                let mut end_jumps = Vec::new();
1403                for arm in arms {
1404                    match &arm.pattern.node {
1405                        // Wildcard `_` — always matches
1406                        Node::Identifier(name) if name == "_" => {
1407                            self.begin_scope();
1408                            self.chunk.emit(Op::Pop, self.line); // pop match value
1409                            self.compile_match_body(&arm.body)?;
1410                            self.end_scope();
1411                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1412                        }
1413                        // Enum destructuring: EnumConstruct pattern
1414                        Node::EnumConstruct {
1415                            enum_name,
1416                            variant,
1417                            args: pat_args,
1418                        } => {
1419                            // Check if the match value is this enum variant
1420                            self.chunk.emit(Op::Dup, self.line);
1421                            let en_idx =
1422                                self.chunk.add_constant(Constant::String(enum_name.clone()));
1423                            let vn_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1424                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1425                            let hi = (vn_idx >> 8) as u8;
1426                            let lo = vn_idx as u8;
1427                            self.chunk.code.push(hi);
1428                            self.chunk.code.push(lo);
1429                            self.chunk.lines.push(self.line);
1430                            self.chunk.columns.push(self.column);
1431                            self.chunk.lines.push(self.line);
1432                            self.chunk.columns.push(self.column);
1433                            // Stack: [match_value, bool]
1434                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1435                            self.chunk.emit(Op::Pop, self.line); // pop bool
1436                            self.begin_scope();
1437
1438                            // Destructure: bind field variables from the enum's fields
1439                            // The match value is still on the stack; we need to extract fields
1440                            for (i, pat_arg) in pat_args.iter().enumerate() {
1441                                if let Node::Identifier(binding_name) = &pat_arg.node {
1442                                    // Dup the match value, get .fields, subscript [i]
1443                                    self.chunk.emit(Op::Dup, self.line);
1444                                    let fields_idx = self
1445                                        .chunk
1446                                        .add_constant(Constant::String("fields".to_string()));
1447                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1448                                    let idx_const =
1449                                        self.chunk.add_constant(Constant::Int(i as i64));
1450                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1451                                    self.chunk.emit(Op::Subscript, self.line);
1452                                    let name_idx = self
1453                                        .chunk
1454                                        .add_constant(Constant::String(binding_name.clone()));
1455                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1456                                }
1457                            }
1458
1459                            self.chunk.emit(Op::Pop, self.line); // pop match value
1460                            self.compile_match_body(&arm.body)?;
1461                            self.end_scope();
1462                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1463                            self.chunk.patch_jump(skip);
1464                            self.chunk.emit(Op::Pop, self.line); // pop bool
1465                        }
1466                        // Enum variant without args: PropertyAccess(EnumName, Variant)
1467                        Node::PropertyAccess { object, property } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1468                        {
1469                            let enum_name = if let Node::Identifier(n) = &object.node {
1470                                n.clone()
1471                            } else {
1472                                unreachable!()
1473                            };
1474                            self.chunk.emit(Op::Dup, self.line);
1475                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1476                            let vn_idx =
1477                                self.chunk.add_constant(Constant::String(property.clone()));
1478                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1479                            let hi = (vn_idx >> 8) as u8;
1480                            let lo = vn_idx as u8;
1481                            self.chunk.code.push(hi);
1482                            self.chunk.code.push(lo);
1483                            self.chunk.lines.push(self.line);
1484                            self.chunk.columns.push(self.column);
1485                            self.chunk.lines.push(self.line);
1486                            self.chunk.columns.push(self.column);
1487                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1488                            self.chunk.emit(Op::Pop, self.line); // pop bool
1489                            self.begin_scope();
1490                            self.chunk.emit(Op::Pop, self.line); // pop match value
1491                            self.compile_match_body(&arm.body)?;
1492                            self.end_scope();
1493                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1494                            self.chunk.patch_jump(skip);
1495                            self.chunk.emit(Op::Pop, self.line); // pop bool
1496                        }
1497                        // Enum destructuring via MethodCall: EnumName.Variant(bindings...)
1498                        // Parser produces MethodCall for EnumName.Variant(x) patterns
1499                        Node::MethodCall {
1500                            object,
1501                            method,
1502                            args: pat_args,
1503                        } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1504                        {
1505                            let enum_name = if let Node::Identifier(n) = &object.node {
1506                                n.clone()
1507                            } else {
1508                                unreachable!()
1509                            };
1510                            // Check if the match value is this enum variant
1511                            self.chunk.emit(Op::Dup, self.line);
1512                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1513                            let vn_idx = self.chunk.add_constant(Constant::String(method.clone()));
1514                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1515                            let hi = (vn_idx >> 8) as u8;
1516                            let lo = vn_idx as u8;
1517                            self.chunk.code.push(hi);
1518                            self.chunk.code.push(lo);
1519                            self.chunk.lines.push(self.line);
1520                            self.chunk.columns.push(self.column);
1521                            self.chunk.lines.push(self.line);
1522                            self.chunk.columns.push(self.column);
1523                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1524                            self.chunk.emit(Op::Pop, self.line); // pop bool
1525                            self.begin_scope();
1526
1527                            // Destructure: bind field variables
1528                            for (i, pat_arg) in pat_args.iter().enumerate() {
1529                                if let Node::Identifier(binding_name) = &pat_arg.node {
1530                                    self.chunk.emit(Op::Dup, self.line);
1531                                    let fields_idx = self
1532                                        .chunk
1533                                        .add_constant(Constant::String("fields".to_string()));
1534                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1535                                    let idx_const =
1536                                        self.chunk.add_constant(Constant::Int(i as i64));
1537                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1538                                    self.chunk.emit(Op::Subscript, self.line);
1539                                    let name_idx = self
1540                                        .chunk
1541                                        .add_constant(Constant::String(binding_name.clone()));
1542                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1543                                }
1544                            }
1545
1546                            self.chunk.emit(Op::Pop, self.line); // pop match value
1547                            self.compile_match_body(&arm.body)?;
1548                            self.end_scope();
1549                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1550                            self.chunk.patch_jump(skip);
1551                            self.chunk.emit(Op::Pop, self.line); // pop bool
1552                        }
1553                        // Binding pattern: bare identifier (not a literal)
1554                        Node::Identifier(name) => {
1555                            self.begin_scope();
1556                            // Bind the match value to this name, always matches
1557                            self.chunk.emit(Op::Dup, self.line); // dup for binding
1558                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1559                            self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1560                            self.chunk.emit(Op::Pop, self.line); // pop match value
1561                            self.compile_match_body(&arm.body)?;
1562                            self.end_scope();
1563                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1564                        }
1565                        // Dict pattern: {key: literal, key: binding, ...}
1566                        Node::DictLiteral(entries)
1567                            if entries
1568                                .iter()
1569                                .all(|e| matches!(&e.key.node, Node::StringLiteral(_))) =>
1570                        {
1571                            // Check type is dict: dup, call type_of, compare "dict"
1572                            self.chunk.emit(Op::Dup, self.line);
1573                            let typeof_idx =
1574                                self.chunk.add_constant(Constant::String("type_of".into()));
1575                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1576                            self.chunk.emit(Op::Swap, self.line);
1577                            self.chunk.emit_u8(Op::Call, 1, self.line);
1578                            let dict_str = self.chunk.add_constant(Constant::String("dict".into()));
1579                            self.chunk.emit_u16(Op::Constant, dict_str, self.line);
1580                            self.chunk.emit(Op::Equal, self.line);
1581                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1582                            self.chunk.emit(Op::Pop, self.line); // pop bool
1583
1584                            // Check literal constraints
1585                            let mut constraint_skips = Vec::new();
1586                            let mut bindings = Vec::new();
1587                            self.begin_scope();
1588                            for entry in entries {
1589                                if let Node::StringLiteral(key) = &entry.key.node {
1590                                    match &entry.value.node {
1591                                        // Literal value → constraint: dict[key] == value
1592                                        Node::StringLiteral(_)
1593                                        | Node::IntLiteral(_)
1594                                        | Node::FloatLiteral(_)
1595                                        | Node::BoolLiteral(_)
1596                                        | Node::NilLiteral => {
1597                                            self.chunk.emit(Op::Dup, self.line);
1598                                            let key_idx = self
1599                                                .chunk
1600                                                .add_constant(Constant::String(key.clone()));
1601                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1602                                            self.chunk.emit(Op::Subscript, self.line);
1603                                            self.compile_node(&entry.value)?;
1604                                            self.chunk.emit(Op::Equal, self.line);
1605                                            let skip =
1606                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1607                                            self.chunk.emit(Op::Pop, self.line); // pop bool
1608                                            constraint_skips.push(skip);
1609                                        }
1610                                        // Identifier → binding: bind dict[key] to variable
1611                                        Node::Identifier(binding) => {
1612                                            bindings.push((key.clone(), binding.clone()));
1613                                        }
1614                                        _ => {
1615                                            // Complex expression constraint
1616                                            self.chunk.emit(Op::Dup, self.line);
1617                                            let key_idx = self
1618                                                .chunk
1619                                                .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                                            self.compile_node(&entry.value)?;
1623                                            self.chunk.emit(Op::Equal, self.line);
1624                                            let skip =
1625                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1626                                            self.chunk.emit(Op::Pop, self.line);
1627                                            constraint_skips.push(skip);
1628                                        }
1629                                    }
1630                                }
1631                            }
1632
1633                            // All constraints passed — emit bindings
1634                            for (key, binding) in &bindings {
1635                                self.chunk.emit(Op::Dup, self.line);
1636                                let key_idx =
1637                                    self.chunk.add_constant(Constant::String(key.clone()));
1638                                self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1639                                self.chunk.emit(Op::Subscript, self.line);
1640                                let name_idx =
1641                                    self.chunk.add_constant(Constant::String(binding.clone()));
1642                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1643                            }
1644
1645                            self.chunk.emit(Op::Pop, self.line); // pop match value
1646                            self.compile_match_body(&arm.body)?;
1647                            self.end_scope();
1648                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1649
1650                            let type_fail_target = self.chunk.code.len();
1651                            self.chunk.emit(Op::Pop, self.line); // pop bool
1652                            let next_arm_jump = self.chunk.emit_jump(Op::Jump, self.line);
1653                            let scoped_fail_target = self.chunk.code.len();
1654                            self.chunk.emit(Op::PopScope, self.line);
1655                            self.chunk.emit(Op::Pop, self.line); // pop bool
1656                            let next_arm_target = self.chunk.code.len();
1657
1658                            for skip in constraint_skips {
1659                                self.chunk.patch_jump_to(skip, scoped_fail_target);
1660                            }
1661                            self.chunk.patch_jump_to(skip_type, type_fail_target);
1662                            self.chunk.patch_jump_to(next_arm_jump, next_arm_target);
1663                        }
1664                        // List pattern: [literal, binding, ...]
1665                        Node::ListLiteral(elements) => {
1666                            // Check type is list: dup, call type_of, compare "list"
1667                            self.chunk.emit(Op::Dup, self.line);
1668                            let typeof_idx =
1669                                self.chunk.add_constant(Constant::String("type_of".into()));
1670                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1671                            self.chunk.emit(Op::Swap, self.line);
1672                            self.chunk.emit_u8(Op::Call, 1, self.line);
1673                            let list_str = self.chunk.add_constant(Constant::String("list".into()));
1674                            self.chunk.emit_u16(Op::Constant, list_str, self.line);
1675                            self.chunk.emit(Op::Equal, self.line);
1676                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1677                            self.chunk.emit(Op::Pop, self.line); // pop bool
1678
1679                            // Check length: dup, call len, compare >= elements.len()
1680                            self.chunk.emit(Op::Dup, self.line);
1681                            let len_idx = self.chunk.add_constant(Constant::String("len".into()));
1682                            self.chunk.emit_u16(Op::Constant, len_idx, self.line);
1683                            self.chunk.emit(Op::Swap, self.line);
1684                            self.chunk.emit_u8(Op::Call, 1, self.line);
1685                            let count = self
1686                                .chunk
1687                                .add_constant(Constant::Int(elements.len() as i64));
1688                            self.chunk.emit_u16(Op::Constant, count, self.line);
1689                            self.chunk.emit(Op::GreaterEqual, self.line);
1690                            let skip_len = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1691                            self.chunk.emit(Op::Pop, self.line); // pop bool
1692
1693                            // Check literal constraints and collect bindings
1694                            let mut constraint_skips = Vec::new();
1695                            let mut bindings = Vec::new();
1696                            self.begin_scope();
1697                            for (i, elem) in elements.iter().enumerate() {
1698                                match &elem.node {
1699                                    Node::Identifier(name) if name != "_" => {
1700                                        bindings.push((i, name.clone()));
1701                                    }
1702                                    Node::Identifier(_) => {} // wildcard _
1703                                    // Literal constraint
1704                                    _ => {
1705                                        self.chunk.emit(Op::Dup, self.line);
1706                                        let idx_const =
1707                                            self.chunk.add_constant(Constant::Int(i as i64));
1708                                        self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1709                                        self.chunk.emit(Op::Subscript, self.line);
1710                                        self.compile_node(elem)?;
1711                                        self.chunk.emit(Op::Equal, self.line);
1712                                        let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1713                                        self.chunk.emit(Op::Pop, self.line);
1714                                        constraint_skips.push(skip);
1715                                    }
1716                                }
1717                            }
1718
1719                            // Emit bindings
1720                            for (i, name) in &bindings {
1721                                self.chunk.emit(Op::Dup, self.line);
1722                                let idx_const = self.chunk.add_constant(Constant::Int(*i as i64));
1723                                self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1724                                self.chunk.emit(Op::Subscript, self.line);
1725                                let name_idx =
1726                                    self.chunk.add_constant(Constant::String(name.clone()));
1727                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1728                            }
1729
1730                            self.chunk.emit(Op::Pop, self.line); // pop match value
1731                            self.compile_match_body(&arm.body)?;
1732                            self.end_scope();
1733                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1734
1735                            let pre_scope_fail_target = self.chunk.code.len();
1736                            self.chunk.emit(Op::Pop, self.line); // pop bool
1737                            let next_arm_jump = self.chunk.emit_jump(Op::Jump, self.line);
1738                            let scoped_fail_target = self.chunk.code.len();
1739                            self.chunk.emit(Op::PopScope, self.line);
1740                            self.chunk.emit(Op::Pop, self.line); // pop bool
1741                            let next_arm_target = self.chunk.code.len();
1742                            for skip in constraint_skips {
1743                                self.chunk.patch_jump_to(skip, scoped_fail_target);
1744                            }
1745                            self.chunk.patch_jump_to(skip_len, pre_scope_fail_target);
1746                            self.chunk.patch_jump_to(skip_type, pre_scope_fail_target);
1747                            self.chunk.patch_jump_to(next_arm_jump, next_arm_target);
1748                        }
1749                        // Literal/expression pattern — compare with Equal
1750                        _ => {
1751                            self.chunk.emit(Op::Dup, self.line);
1752                            self.compile_node(&arm.pattern)?;
1753                            self.chunk.emit(Op::Equal, self.line);
1754                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1755                            self.chunk.emit(Op::Pop, self.line); // pop bool
1756                            self.begin_scope();
1757                            self.chunk.emit(Op::Pop, self.line); // pop match value
1758                            self.compile_match_body(&arm.body)?;
1759                            self.end_scope();
1760                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1761                            self.chunk.patch_jump(skip);
1762                            self.chunk.emit(Op::Pop, self.line); // pop bool
1763                        }
1764                    }
1765                }
1766                // No match — pop value, push nil
1767                self.chunk.emit(Op::Pop, self.line);
1768                self.chunk.emit(Op::Nil, self.line);
1769                for j in end_jumps {
1770                    self.chunk.patch_jump(j);
1771                }
1772            }
1773
1774            Node::RangeExpr {
1775                start,
1776                end,
1777                inclusive,
1778            } => {
1779                // Compile as __range__(start, end, inclusive_bool) builtin call
1780                let name_idx = self
1781                    .chunk
1782                    .add_constant(Constant::String("__range__".to_string()));
1783                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
1784                self.compile_node(start)?;
1785                self.compile_node(end)?;
1786                if *inclusive {
1787                    self.chunk.emit(Op::True, self.line);
1788                } else {
1789                    self.chunk.emit(Op::False, self.line);
1790                }
1791                self.chunk.emit_u8(Op::Call, 3, self.line);
1792            }
1793
1794            Node::GuardStmt {
1795                condition,
1796                else_body,
1797            } => {
1798                // guard condition else { body }
1799                // Compile condition; if truthy, skip else_body
1800                self.compile_node(condition)?;
1801                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
1802                self.chunk.emit(Op::Pop, self.line); // pop condition
1803                                                     // Compile else_body
1804                self.compile_scoped_block(else_body)?;
1805                // Pop result of else_body (guard is a statement, not expression)
1806                if !else_body.is_empty() && Self::produces_value(&else_body.last().unwrap().node) {
1807                    self.chunk.emit(Op::Pop, self.line);
1808                }
1809                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1810                self.chunk.patch_jump(skip_jump);
1811                self.chunk.emit(Op::Pop, self.line); // pop condition
1812                self.chunk.patch_jump(end_jump);
1813                self.chunk.emit(Op::Nil, self.line);
1814            }
1815
1816            Node::RequireStmt { condition, message } => {
1817                self.compile_node(condition)?;
1818                let ok_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
1819                self.chunk.emit(Op::Pop, self.line);
1820                if let Some(message) = message {
1821                    self.compile_node(message)?;
1822                } else {
1823                    let idx = self
1824                        .chunk
1825                        .add_constant(Constant::String("require condition failed".to_string()));
1826                    self.chunk.emit_u16(Op::Constant, idx, self.line);
1827                }
1828                self.chunk.emit(Op::Throw, self.line);
1829                self.chunk.patch_jump(ok_jump);
1830                self.chunk.emit(Op::Pop, self.line);
1831            }
1832
1833            Node::Block(stmts) => {
1834                self.compile_scoped_block(stmts)?;
1835            }
1836
1837            Node::DeadlineBlock { duration, body } => {
1838                self.compile_node(duration)?;
1839                self.chunk.emit(Op::DeadlineSetup, self.line);
1840                self.compile_scoped_block(body)?;
1841                self.chunk.emit(Op::DeadlineEnd, self.line);
1842            }
1843
1844            Node::MutexBlock { body } => {
1845                // v1: single-threaded, but still uses a lexical block scope.
1846                self.begin_scope();
1847                for sn in body {
1848                    self.compile_node(sn)?;
1849                    if Self::produces_value(&sn.node) {
1850                        self.chunk.emit(Op::Pop, self.line);
1851                    }
1852                }
1853                self.chunk.emit(Op::Nil, self.line);
1854                self.end_scope();
1855            }
1856
1857            Node::YieldExpr { value } => {
1858                if let Some(val) = value {
1859                    self.compile_node(val)?;
1860                } else {
1861                    self.chunk.emit(Op::Nil, self.line);
1862                }
1863                self.chunk.emit(Op::Yield, self.line);
1864            }
1865
1866            Node::AskExpr { fields } => {
1867                // Compile as a dict literal and call llm_call builtin
1868                // For v1, just build the dict (llm_call requires async)
1869                for entry in fields {
1870                    self.compile_node(&entry.key)?;
1871                    self.compile_node(&entry.value)?;
1872                }
1873                self.chunk
1874                    .emit_u16(Op::BuildDict, fields.len() as u16, self.line);
1875            }
1876
1877            Node::EnumConstruct {
1878                enum_name,
1879                variant,
1880                args,
1881            } => {
1882                // Push field values onto the stack, then BuildEnum
1883                for arg in args {
1884                    self.compile_node(arg)?;
1885                }
1886                let enum_idx = self.chunk.add_constant(Constant::String(enum_name.clone()));
1887                let var_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1888                // BuildEnum: enum_name_idx, variant_idx, field_count
1889                self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
1890                let hi = (var_idx >> 8) as u8;
1891                let lo = var_idx as u8;
1892                self.chunk.code.push(hi);
1893                self.chunk.code.push(lo);
1894                self.chunk.lines.push(self.line);
1895                self.chunk.columns.push(self.column);
1896                self.chunk.lines.push(self.line);
1897                self.chunk.columns.push(self.column);
1898                let fc = args.len() as u16;
1899                let fhi = (fc >> 8) as u8;
1900                let flo = fc as u8;
1901                self.chunk.code.push(fhi);
1902                self.chunk.code.push(flo);
1903                self.chunk.lines.push(self.line);
1904                self.chunk.columns.push(self.column);
1905                self.chunk.lines.push(self.line);
1906                self.chunk.columns.push(self.column);
1907            }
1908
1909            Node::StructConstruct {
1910                struct_name,
1911                fields,
1912            } => {
1913                // Build as a dict with a __struct__ key for metadata
1914                let struct_key = self
1915                    .chunk
1916                    .add_constant(Constant::String("__struct__".to_string()));
1917                let struct_val = self
1918                    .chunk
1919                    .add_constant(Constant::String(struct_name.clone()));
1920                self.chunk.emit_u16(Op::Constant, struct_key, self.line);
1921                self.chunk.emit_u16(Op::Constant, struct_val, self.line);
1922
1923                for entry in fields {
1924                    self.compile_node(&entry.key)?;
1925                    self.compile_node(&entry.value)?;
1926                }
1927                self.chunk
1928                    .emit_u16(Op::BuildDict, (fields.len() + 1) as u16, self.line);
1929            }
1930
1931            Node::ImportDecl { path } => {
1932                let idx = self.chunk.add_constant(Constant::String(path.clone()));
1933                self.chunk.emit_u16(Op::Import, idx, self.line);
1934            }
1935
1936            Node::SelectiveImport { names, path } => {
1937                let path_idx = self.chunk.add_constant(Constant::String(path.clone()));
1938                let names_str = names.join(",");
1939                let names_idx = self.chunk.add_constant(Constant::String(names_str));
1940                self.chunk
1941                    .emit_u16(Op::SelectiveImport, path_idx, self.line);
1942                let hi = (names_idx >> 8) as u8;
1943                let lo = names_idx as u8;
1944                self.chunk.code.push(hi);
1945                self.chunk.code.push(lo);
1946                self.chunk.lines.push(self.line);
1947                self.chunk.columns.push(self.column);
1948                self.chunk.lines.push(self.line);
1949                self.chunk.columns.push(self.column);
1950            }
1951
1952            Node::TryOperator { operand } => {
1953                self.compile_node(operand)?;
1954                self.chunk.emit(Op::TryUnwrap, self.line);
1955            }
1956
1957            Node::ImplBlock { type_name, methods } => {
1958                // Compile each method as a closure and store in __impl_TypeName dict.
1959                // Build key-value pairs on stack, then BuildDict.
1960                for method_sn in methods {
1961                    if let Node::FnDecl {
1962                        name, params, body, ..
1963                    } = &method_sn.node
1964                    {
1965                        // Method name key
1966                        let key_idx = self.chunk.add_constant(Constant::String(name.clone()));
1967                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1968
1969                        // Compile method body as closure
1970                        let mut fn_compiler = Compiler::new();
1971                        fn_compiler.enum_names = self.enum_names.clone();
1972                        fn_compiler.emit_default_preamble(params)?;
1973                        fn_compiler.emit_type_checks(params);
1974                        fn_compiler.compile_block(body)?;
1975                        fn_compiler.chunk.emit(Op::Nil, self.line);
1976                        fn_compiler.chunk.emit(Op::Return, self.line);
1977
1978                        let func = CompiledFunction {
1979                            name: format!("{}.{}", type_name, name),
1980                            params: TypedParam::names(params),
1981                            default_start: TypedParam::default_start(params),
1982                            chunk: fn_compiler.chunk,
1983                            is_generator: false,
1984                            has_rest_param: false,
1985                        };
1986                        let fn_idx = self.chunk.functions.len();
1987                        self.chunk.functions.push(func);
1988                        self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1989                    }
1990                }
1991                let method_count = methods
1992                    .iter()
1993                    .filter(|m| matches!(m.node, Node::FnDecl { .. }))
1994                    .count();
1995                self.chunk
1996                    .emit_u16(Op::BuildDict, method_count as u16, self.line);
1997                let impl_name = format!("__impl_{}", type_name);
1998                let name_idx = self.chunk.add_constant(Constant::String(impl_name));
1999                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
2000            }
2001
2002            Node::StructDecl { name, .. } => {
2003                // Compile a constructor function: StructName({field: val, ...}) -> StructInstance
2004                let mut fn_compiler = Compiler::new();
2005                fn_compiler.enum_names = self.enum_names.clone();
2006                let params = vec![TypedParam::untyped("__fields")];
2007                fn_compiler.emit_default_preamble(&params)?;
2008
2009                // Call __make_struct(struct_name, fields_dict) to tag the dict
2010                let make_idx = fn_compiler
2011                    .chunk
2012                    .add_constant(Constant::String("__make_struct".into()));
2013                fn_compiler
2014                    .chunk
2015                    .emit_u16(Op::Constant, make_idx, self.line);
2016                let sname_idx = fn_compiler
2017                    .chunk
2018                    .add_constant(Constant::String(name.clone()));
2019                fn_compiler
2020                    .chunk
2021                    .emit_u16(Op::Constant, sname_idx, self.line);
2022                let fields_idx = fn_compiler
2023                    .chunk
2024                    .add_constant(Constant::String("__fields".into()));
2025                fn_compiler
2026                    .chunk
2027                    .emit_u16(Op::GetVar, fields_idx, self.line);
2028                fn_compiler.chunk.emit_u8(Op::Call, 2, self.line);
2029                fn_compiler.chunk.emit(Op::Return, self.line);
2030
2031                let func = CompiledFunction {
2032                    name: name.clone(),
2033                    params: TypedParam::names(&params),
2034                    default_start: None,
2035                    chunk: fn_compiler.chunk,
2036                    is_generator: false,
2037                    has_rest_param: false,
2038                };
2039                let fn_idx = self.chunk.functions.len();
2040                self.chunk.functions.push(func);
2041                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2042                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
2043                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
2044            }
2045
2046            // Declarations that only register metadata (no runtime effect needed for v1)
2047            Node::Pipeline { .. }
2048            | Node::OverrideDecl { .. }
2049            | Node::TypeDecl { .. }
2050            | Node::EnumDecl { .. }
2051            | Node::InterfaceDecl { .. } => {
2052                self.chunk.emit(Op::Nil, self.line);
2053            }
2054
2055            Node::TryCatch {
2056                body,
2057                error_var,
2058                error_type,
2059                catch_body,
2060                finally_body,
2061            } => {
2062                // Extract the type name for typed catch (e.g., "AppError")
2063                let type_name = error_type.as_ref().and_then(|te| {
2064                    if let harn_parser::TypeExpr::Named(name) = te {
2065                        Some(name.clone())
2066                    } else {
2067                        None
2068                    }
2069                });
2070
2071                let type_name_idx = if let Some(ref tn) = type_name {
2072                    self.chunk.add_constant(Constant::String(tn.clone()))
2073                } else {
2074                    self.chunk.add_constant(Constant::String(String::new()))
2075                };
2076
2077                let has_catch = !catch_body.is_empty() || error_var.is_some();
2078                let has_finally = finally_body.is_some();
2079
2080                if has_catch && has_finally {
2081                    // === try-catch-finally ===
2082                    let finally_body = finally_body.as_ref().unwrap();
2083
2084                    // Push finally body onto pending stack for return/break handling
2085                    self.finally_bodies.push(finally_body.clone());
2086
2087                    // 1. TryCatchSetup for try body
2088                    self.handler_depth += 1;
2089                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2090                    self.emit_type_name_extra(type_name_idx);
2091
2092                    // 2. Compile try body
2093                    self.compile_try_body(body)?;
2094
2095                    // 3. PopHandler + inline finally (success path)
2096                    self.handler_depth -= 1;
2097                    self.chunk.emit(Op::PopHandler, self.line);
2098                    self.compile_finally_inline(finally_body)?;
2099                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2100
2101                    // 4. Catch entry
2102                    self.chunk.patch_jump(catch_jump);
2103                    self.begin_scope();
2104                    self.compile_catch_binding(error_var)?;
2105
2106                    // 5. Inner try around catch body (so finally runs if catch throws)
2107                    self.handler_depth += 1;
2108                    let rethrow_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2109                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2110                    self.emit_type_name_extra(empty_type);
2111
2112                    // 6. Compile catch body
2113                    self.compile_try_body(catch_body)?;
2114
2115                    // 7. PopHandler + inline finally (catch success path)
2116                    self.handler_depth -= 1;
2117                    self.chunk.emit(Op::PopHandler, self.line);
2118                    self.compile_finally_inline(finally_body)?;
2119                    self.end_scope();
2120                    let end_jump2 = self.chunk.emit_jump(Op::Jump, self.line);
2121
2122                    // 8. Rethrow handler: save error, run finally, re-throw
2123                    self.chunk.patch_jump(rethrow_jump);
2124                    self.compile_rethrow_with_finally(finally_body)?;
2125                    self.end_scope();
2126
2127                    self.chunk.patch_jump(end_jump);
2128                    self.chunk.patch_jump(end_jump2);
2129
2130                    self.finally_bodies.pop();
2131                } else if has_finally {
2132                    // === try-finally (no catch) ===
2133                    let finally_body = finally_body.as_ref().unwrap();
2134
2135                    self.finally_bodies.push(finally_body.clone());
2136
2137                    // 1. TryCatchSetup to error path
2138                    self.handler_depth += 1;
2139                    let error_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2140                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2141                    self.emit_type_name_extra(empty_type);
2142
2143                    // 2. Compile try body
2144                    self.compile_try_body(body)?;
2145
2146                    // 3. PopHandler + inline finally (success path)
2147                    self.handler_depth -= 1;
2148                    self.chunk.emit(Op::PopHandler, self.line);
2149                    self.compile_finally_inline(finally_body)?;
2150                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2151
2152                    // 4. Error path: save error, run finally, re-throw
2153                    self.chunk.patch_jump(error_jump);
2154                    self.compile_rethrow_with_finally(finally_body)?;
2155
2156                    self.chunk.patch_jump(end_jump);
2157
2158                    self.finally_bodies.pop();
2159                } else {
2160                    // === try-catch (no finally) — original behavior ===
2161
2162                    // 1. TryCatchSetup
2163                    self.handler_depth += 1;
2164                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2165                    self.emit_type_name_extra(type_name_idx);
2166
2167                    // 2. Compile try body
2168                    self.compile_try_body(body)?;
2169
2170                    // 3. PopHandler + jump past catch
2171                    self.handler_depth -= 1;
2172                    self.chunk.emit(Op::PopHandler, self.line);
2173                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2174
2175                    // 4. Catch entry
2176                    self.chunk.patch_jump(catch_jump);
2177                    self.begin_scope();
2178                    self.compile_catch_binding(error_var)?;
2179
2180                    // 5. Compile catch body
2181                    self.compile_try_body(catch_body)?;
2182                    self.end_scope();
2183
2184                    // 6. Patch end
2185                    self.chunk.patch_jump(end_jump);
2186                }
2187            }
2188
2189            Node::TryExpr { body } => {
2190                // try { body } — returns Result.Ok(value) or Result.Err(error)
2191
2192                // 1. Set up try-catch handler (untyped)
2193                self.handler_depth += 1;
2194                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2195                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2196                self.emit_type_name_extra(empty_type);
2197
2198                // 2. Compile try body (leaves value on stack)
2199                self.compile_try_body(body)?;
2200
2201                // 3. PopHandler (success path)
2202                self.handler_depth -= 1;
2203                self.chunk.emit(Op::PopHandler, self.line);
2204
2205                // 4. Wrap in Result.Ok: push "Ok", swap, call Ok(value)
2206                let ok_idx = self.chunk.add_constant(Constant::String("Ok".to_string()));
2207                self.chunk.emit_u16(Op::Constant, ok_idx, self.line);
2208                self.chunk.emit(Op::Swap, self.line);
2209                self.chunk.emit_u8(Op::Call, 1, self.line);
2210
2211                // 5. Jump past error handler
2212                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2213
2214                // 6. Error handler: error value is on stack
2215                self.chunk.patch_jump(catch_jump);
2216
2217                // 7. Wrap in Result.Err: push "Err", swap, call Err(error)
2218                let err_idx = self.chunk.add_constant(Constant::String("Err".to_string()));
2219                self.chunk.emit_u16(Op::Constant, err_idx, self.line);
2220                self.chunk.emit(Op::Swap, self.line);
2221                self.chunk.emit_u8(Op::Call, 1, self.line);
2222
2223                // 8. Patch end
2224                self.chunk.patch_jump(end_jump);
2225            }
2226
2227            Node::Retry { count, body } => {
2228                // Compile count expression into a mutable counter variable
2229                self.compile_node(count)?;
2230                let counter_name = "__retry_counter__";
2231                let counter_idx = self
2232                    .chunk
2233                    .add_constant(Constant::String(counter_name.to_string()));
2234                self.chunk.emit_u16(Op::DefVar, counter_idx, self.line);
2235
2236                // Also store the last error for re-throwing
2237                self.chunk.emit(Op::Nil, self.line);
2238                let err_name = "__retry_last_error__";
2239                let err_idx = self
2240                    .chunk
2241                    .add_constant(Constant::String(err_name.to_string()));
2242                self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
2243
2244                // Loop start
2245                let loop_start = self.chunk.current_offset();
2246
2247                // Set up try/catch (untyped - empty type name)
2248                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2249                // Emit empty type name for untyped catch
2250                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2251                let hi = (empty_type >> 8) as u8;
2252                let lo = empty_type as u8;
2253                self.chunk.code.push(hi);
2254                self.chunk.code.push(lo);
2255                self.chunk.lines.push(self.line);
2256                self.chunk.columns.push(self.column);
2257                self.chunk.lines.push(self.line);
2258                self.chunk.columns.push(self.column);
2259
2260                // Compile body
2261                self.compile_block(body)?;
2262
2263                // Success: pop handler, jump to end
2264                self.chunk.emit(Op::PopHandler, self.line);
2265                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2266
2267                // Catch handler
2268                self.chunk.patch_jump(catch_jump);
2269                // Save the error value for potential re-throw
2270                self.chunk.emit(Op::Dup, self.line);
2271                self.chunk.emit_u16(Op::SetVar, err_idx, self.line);
2272                // Pop the error value
2273                self.chunk.emit(Op::Pop, self.line);
2274
2275                // Decrement counter
2276                self.chunk.emit_u16(Op::GetVar, counter_idx, self.line);
2277                let one_idx = self.chunk.add_constant(Constant::Int(1));
2278                self.chunk.emit_u16(Op::Constant, one_idx, self.line);
2279                self.chunk.emit(Op::Sub, self.line);
2280                self.chunk.emit(Op::Dup, self.line);
2281                self.chunk.emit_u16(Op::SetVar, counter_idx, self.line);
2282
2283                // If counter > 0, jump to loop start
2284                let zero_idx = self.chunk.add_constant(Constant::Int(0));
2285                self.chunk.emit_u16(Op::Constant, zero_idx, self.line);
2286                self.chunk.emit(Op::Greater, self.line);
2287                let retry_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2288                self.chunk.emit(Op::Pop, self.line); // pop condition
2289                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
2290
2291                // No more retries — re-throw the last error
2292                self.chunk.patch_jump(retry_jump);
2293                self.chunk.emit(Op::Pop, self.line); // pop condition
2294                self.chunk.emit_u16(Op::GetVar, err_idx, self.line);
2295                self.chunk.emit(Op::Throw, self.line);
2296
2297                self.chunk.patch_jump(end_jump);
2298                // Push nil as the result of a successful retry block
2299                self.chunk.emit(Op::Nil, self.line);
2300            }
2301
2302            Node::Parallel {
2303                count,
2304                variable,
2305                body,
2306            } => {
2307                self.compile_node(count)?;
2308                let mut fn_compiler = Compiler::new();
2309                fn_compiler.enum_names = self.enum_names.clone();
2310                fn_compiler.compile_block(body)?;
2311                fn_compiler.chunk.emit(Op::Return, self.line);
2312                let params = vec![variable.clone().unwrap_or_else(|| "__i__".to_string())];
2313                let func = CompiledFunction {
2314                    name: "<parallel>".to_string(),
2315                    params,
2316                    default_start: None,
2317                    chunk: fn_compiler.chunk,
2318                    is_generator: false,
2319                    has_rest_param: false,
2320                };
2321                let fn_idx = self.chunk.functions.len();
2322                self.chunk.functions.push(func);
2323                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2324                self.chunk.emit(Op::Parallel, self.line);
2325            }
2326
2327            Node::ParallelMap {
2328                list,
2329                variable,
2330                body,
2331            } => {
2332                self.compile_node(list)?;
2333                let mut fn_compiler = Compiler::new();
2334                fn_compiler.enum_names = self.enum_names.clone();
2335                fn_compiler.compile_block(body)?;
2336                fn_compiler.chunk.emit(Op::Return, self.line);
2337                let func = CompiledFunction {
2338                    name: "<parallel_map>".to_string(),
2339                    params: vec![variable.clone()],
2340                    default_start: None,
2341                    chunk: fn_compiler.chunk,
2342                    is_generator: false,
2343                    has_rest_param: false,
2344                };
2345                let fn_idx = self.chunk.functions.len();
2346                self.chunk.functions.push(func);
2347                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2348                self.chunk.emit(Op::ParallelMap, self.line);
2349            }
2350
2351            Node::ParallelSettle {
2352                list,
2353                variable,
2354                body,
2355            } => {
2356                self.compile_node(list)?;
2357                let mut fn_compiler = Compiler::new();
2358                fn_compiler.enum_names = self.enum_names.clone();
2359                fn_compiler.compile_block(body)?;
2360                fn_compiler.chunk.emit(Op::Return, self.line);
2361                let func = CompiledFunction {
2362                    name: "<parallel_settle>".to_string(),
2363                    params: vec![variable.clone()],
2364                    default_start: None,
2365                    chunk: fn_compiler.chunk,
2366                    is_generator: false,
2367                    has_rest_param: false,
2368                };
2369                let fn_idx = self.chunk.functions.len();
2370                self.chunk.functions.push(func);
2371                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2372                self.chunk.emit(Op::ParallelSettle, self.line);
2373            }
2374
2375            Node::SpawnExpr { body } => {
2376                let mut fn_compiler = Compiler::new();
2377                fn_compiler.enum_names = self.enum_names.clone();
2378                fn_compiler.compile_block(body)?;
2379                fn_compiler.chunk.emit(Op::Return, self.line);
2380                let func = CompiledFunction {
2381                    name: "<spawn>".to_string(),
2382                    params: vec![],
2383                    default_start: None,
2384                    chunk: fn_compiler.chunk,
2385                    is_generator: false,
2386                    has_rest_param: false,
2387                };
2388                let fn_idx = self.chunk.functions.len();
2389                self.chunk.functions.push(func);
2390                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2391                self.chunk.emit(Op::Spawn, self.line);
2392            }
2393            Node::SelectExpr {
2394                cases,
2395                timeout,
2396                default_body,
2397            } => {
2398                // Desugar select into: builtin call + index-based dispatch.
2399                //
2400                // Step 1: Push builtin name, compile channel list, optionally
2401                //         compile timeout duration, then Call.
2402                // Step 2: Store result dict in temp, dispatch on result.index.
2403
2404                let builtin_name = if timeout.is_some() {
2405                    "__select_timeout"
2406                } else if default_body.is_some() {
2407                    "__select_try"
2408                } else {
2409                    "__select_list"
2410                };
2411
2412                // Push builtin name (callee goes below args on stack)
2413                let name_idx = self
2414                    .chunk
2415                    .add_constant(Constant::String(builtin_name.into()));
2416                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
2417
2418                // Build channel list (arg 1)
2419                for case in cases {
2420                    self.compile_node(&case.channel)?;
2421                }
2422                self.chunk
2423                    .emit_u16(Op::BuildList, cases.len() as u16, self.line);
2424
2425                // If timeout, compile duration (arg 2)
2426                if let Some((duration_expr, _)) = timeout {
2427                    self.compile_node(duration_expr)?;
2428                    self.chunk.emit_u8(Op::Call, 2, self.line);
2429                } else {
2430                    self.chunk.emit_u8(Op::Call, 1, self.line);
2431                }
2432
2433                // Store result in temp var
2434                self.temp_counter += 1;
2435                let result_name = format!("__sel_result_{}__", self.temp_counter);
2436                let result_idx = self
2437                    .chunk
2438                    .add_constant(Constant::String(result_name.clone()));
2439                self.chunk.emit_u16(Op::DefVar, result_idx, self.line);
2440
2441                // Dispatch on result.index
2442                let mut end_jumps = Vec::new();
2443
2444                for (i, case) in cases.iter().enumerate() {
2445                    let get_r = self
2446                        .chunk
2447                        .add_constant(Constant::String(result_name.clone()));
2448                    self.chunk.emit_u16(Op::GetVar, get_r, self.line);
2449                    let idx_prop = self.chunk.add_constant(Constant::String("index".into()));
2450                    self.chunk.emit_u16(Op::GetProperty, idx_prop, self.line);
2451                    let case_i = self.chunk.add_constant(Constant::Int(i as i64));
2452                    self.chunk.emit_u16(Op::Constant, case_i, self.line);
2453                    self.chunk.emit(Op::Equal, self.line);
2454                    let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2455                    self.chunk.emit(Op::Pop, self.line);
2456                    self.begin_scope();
2457
2458                    // Bind variable = result.value
2459                    let get_r2 = self
2460                        .chunk
2461                        .add_constant(Constant::String(result_name.clone()));
2462                    self.chunk.emit_u16(Op::GetVar, get_r2, self.line);
2463                    let val_prop = self.chunk.add_constant(Constant::String("value".into()));
2464                    self.chunk.emit_u16(Op::GetProperty, val_prop, self.line);
2465                    let var_idx = self
2466                        .chunk
2467                        .add_constant(Constant::String(case.variable.clone()));
2468                    self.chunk.emit_u16(Op::DefLet, var_idx, self.line);
2469
2470                    self.compile_try_body(&case.body)?;
2471                    self.end_scope();
2472                    end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2473                    self.chunk.patch_jump(skip);
2474                    self.chunk.emit(Op::Pop, self.line);
2475                }
2476
2477                // Timeout/default fallthrough (index == -1)
2478                if let Some((_, ref timeout_body)) = timeout {
2479                    self.compile_try_body(timeout_body)?;
2480                } else if let Some(ref def_body) = default_body {
2481                    self.compile_try_body(def_body)?;
2482                } else {
2483                    self.chunk.emit(Op::Nil, self.line);
2484                }
2485
2486                for ej in end_jumps {
2487                    self.chunk.patch_jump(ej);
2488                }
2489            }
2490            Node::Spread(_) => {
2491                return Err(CompileError {
2492                    message: "spread (...) can only be used inside list literals, dict literals, or function call arguments".into(),
2493                    line: self.line,
2494                });
2495            }
2496        }
2497        Ok(())
2498    }
2499
2500    /// Compile a destructuring binding pattern.
2501    /// Expects the RHS value to already be on the stack.
2502    /// After this, the value is consumed (popped) and each binding is defined.
2503    fn compile_destructuring(
2504        &mut self,
2505        pattern: &BindingPattern,
2506        is_mutable: bool,
2507    ) -> Result<(), CompileError> {
2508        let def_op = if is_mutable { Op::DefVar } else { Op::DefLet };
2509        match pattern {
2510            BindingPattern::Identifier(name) => {
2511                // Simple case: just define the variable
2512                let idx = self.chunk.add_constant(Constant::String(name.clone()));
2513                self.chunk.emit_u16(def_op, idx, self.line);
2514            }
2515            BindingPattern::Dict(fields) => {
2516                // Stack has the dict value.
2517                // Emit runtime type check: __assert_dict(value)
2518                self.chunk.emit(Op::Dup, self.line);
2519                let assert_idx = self
2520                    .chunk
2521                    .add_constant(Constant::String("__assert_dict".into()));
2522                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
2523                self.chunk.emit(Op::Swap, self.line);
2524                self.chunk.emit_u8(Op::Call, 1, self.line);
2525                self.chunk.emit(Op::Pop, self.line); // discard nil result
2526
2527                // For each non-rest field: dup dict, push key string, subscript, define var.
2528                // For rest field: dup dict, call __dict_rest builtin.
2529                let non_rest: Vec<_> = fields.iter().filter(|f| !f.is_rest).collect();
2530                let rest_field = fields.iter().find(|f| f.is_rest);
2531
2532                for field in &non_rest {
2533                    self.chunk.emit(Op::Dup, self.line);
2534                    let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
2535                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2536                    self.chunk.emit(Op::Subscript, self.line);
2537                    let binding_name = field.alias.as_deref().unwrap_or(&field.key);
2538                    let name_idx = self
2539                        .chunk
2540                        .add_constant(Constant::String(binding_name.to_string()));
2541                    self.chunk.emit_u16(def_op, name_idx, self.line);
2542                }
2543
2544                if let Some(rest) = rest_field {
2545                    // Call the __dict_rest builtin: __dict_rest(dict, [keys_to_exclude])
2546                    // Push function name
2547                    let fn_idx = self
2548                        .chunk
2549                        .add_constant(Constant::String("__dict_rest".into()));
2550                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
2551                    // Swap so dict is above function name: [fn, dict]
2552                    self.chunk.emit(Op::Swap, self.line);
2553                    // Build the exclusion keys list
2554                    for field in &non_rest {
2555                        let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
2556                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2557                    }
2558                    self.chunk
2559                        .emit_u16(Op::BuildList, non_rest.len() as u16, self.line);
2560                    // Call __dict_rest(dict, keys_list) — 2 args
2561                    self.chunk.emit_u8(Op::Call, 2, self.line);
2562                    let rest_name = &rest.key;
2563                    let rest_idx = self.chunk.add_constant(Constant::String(rest_name.clone()));
2564                    self.chunk.emit_u16(def_op, rest_idx, self.line);
2565                } else {
2566                    // Pop the source dict
2567                    self.chunk.emit(Op::Pop, self.line);
2568                }
2569            }
2570            BindingPattern::List(elements) => {
2571                // Stack has the list value.
2572                // Emit runtime type check: __assert_list(value)
2573                self.chunk.emit(Op::Dup, self.line);
2574                let assert_idx = self
2575                    .chunk
2576                    .add_constant(Constant::String("__assert_list".into()));
2577                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
2578                self.chunk.emit(Op::Swap, self.line);
2579                self.chunk.emit_u8(Op::Call, 1, self.line);
2580                self.chunk.emit(Op::Pop, self.line); // discard nil result
2581
2582                let non_rest: Vec<_> = elements.iter().filter(|e| !e.is_rest).collect();
2583                let rest_elem = elements.iter().find(|e| e.is_rest);
2584
2585                for (i, elem) in non_rest.iter().enumerate() {
2586                    self.chunk.emit(Op::Dup, self.line);
2587                    let idx_const = self.chunk.add_constant(Constant::Int(i as i64));
2588                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
2589                    self.chunk.emit(Op::Subscript, self.line);
2590                    let name_idx = self.chunk.add_constant(Constant::String(elem.name.clone()));
2591                    self.chunk.emit_u16(def_op, name_idx, self.line);
2592                }
2593
2594                if let Some(rest) = rest_elem {
2595                    // Slice the list from index non_rest.len() to end: list[n..]
2596                    // Slice op takes: object, start, end on stack
2597                    // self.chunk.emit(Op::Dup, self.line); -- list is still on stack
2598                    let start_idx = self
2599                        .chunk
2600                        .add_constant(Constant::Int(non_rest.len() as i64));
2601                    self.chunk.emit_u16(Op::Constant, start_idx, self.line);
2602                    self.chunk.emit(Op::Nil, self.line); // end = nil (to end)
2603                    self.chunk.emit(Op::Slice, self.line);
2604                    let rest_name_idx =
2605                        self.chunk.add_constant(Constant::String(rest.name.clone()));
2606                    self.chunk.emit_u16(def_op, rest_name_idx, self.line);
2607                } else {
2608                    // Pop the source list
2609                    self.chunk.emit(Op::Pop, self.line);
2610                }
2611            }
2612        }
2613        Ok(())
2614    }
2615
2616    /// Check if a node produces a value on the stack that needs to be popped.
2617    fn produces_value(node: &Node) -> bool {
2618        match node {
2619            // These nodes do NOT produce a value on the stack
2620            Node::LetBinding { .. }
2621            | Node::VarBinding { .. }
2622            | Node::Assignment { .. }
2623            | Node::ReturnStmt { .. }
2624            | Node::FnDecl { .. }
2625            | Node::ToolDecl { .. }
2626            | Node::ImplBlock { .. }
2627            | Node::StructDecl { .. }
2628            | Node::EnumDecl { .. }
2629            | Node::InterfaceDecl { .. }
2630            | Node::TypeDecl { .. }
2631            | Node::ThrowStmt { .. }
2632            | Node::BreakStmt
2633            | Node::ContinueStmt
2634            | Node::RequireStmt { .. } => false,
2635            // These compound nodes explicitly produce a value
2636            Node::TryCatch { .. }
2637            | Node::TryExpr { .. }
2638            | Node::Retry { .. }
2639            | Node::GuardStmt { .. }
2640            | Node::DeadlineBlock { .. }
2641            | Node::MutexBlock { .. }
2642            | Node::Spread(_) => true,
2643            // All other expressions produce values
2644            _ => true,
2645        }
2646    }
2647}
2648
2649impl Compiler {
2650    /// Compile a function body into a CompiledFunction (for import support).
2651    ///
2652    /// This path is used when a module is imported and its top-level `fn`
2653    /// declarations are loaded into the importer's environment. It MUST emit
2654    /// the same function preamble as the in-file `Node::FnDecl` path, or
2655    /// imported functions will behave differently from locally-defined ones —
2656    /// in particular, default parameter values would never be set and typed
2657    /// parameters would not be runtime-checked.
2658    ///
2659    /// `source_file`, when provided, tags the resulting chunk so runtime
2660    /// errors can attribute frames to the imported file rather than the
2661    /// entry-point pipeline.
2662    pub fn compile_fn_body(
2663        &mut self,
2664        params: &[TypedParam],
2665        body: &[SNode],
2666        source_file: Option<String>,
2667    ) -> Result<CompiledFunction, CompileError> {
2668        let mut fn_compiler = Compiler::new();
2669        fn_compiler.enum_names = self.enum_names.clone();
2670        fn_compiler.emit_default_preamble(params)?;
2671        fn_compiler.emit_type_checks(params);
2672        let is_gen = body_contains_yield(body);
2673        fn_compiler.compile_block(body)?;
2674        fn_compiler.chunk.emit(Op::Nil, 0);
2675        fn_compiler.chunk.emit(Op::Return, 0);
2676        fn_compiler.chunk.source_file = source_file;
2677        Ok(CompiledFunction {
2678            name: String::new(),
2679            params: TypedParam::names(params),
2680            default_start: TypedParam::default_start(params),
2681            chunk: fn_compiler.chunk,
2682            is_generator: is_gen,
2683            has_rest_param: false,
2684        })
2685    }
2686
2687    /// Compile a match arm body, ensuring it always pushes exactly one value.
2688    fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
2689        self.begin_scope();
2690        if body.is_empty() {
2691            self.chunk.emit(Op::Nil, self.line);
2692        } else {
2693            self.compile_block(body)?;
2694            if !Self::produces_value(&body.last().unwrap().node) {
2695                self.chunk.emit(Op::Nil, self.line);
2696            }
2697        }
2698        self.end_scope();
2699        Ok(())
2700    }
2701
2702    /// Emit the binary op instruction for a compound assignment operator.
2703    fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
2704        match op {
2705            "+" => self.chunk.emit(Op::Add, self.line),
2706            "-" => self.chunk.emit(Op::Sub, self.line),
2707            "*" => self.chunk.emit(Op::Mul, self.line),
2708            "/" => self.chunk.emit(Op::Div, self.line),
2709            "%" => self.chunk.emit(Op::Mod, self.line),
2710            _ => {
2711                return Err(CompileError {
2712                    message: format!("Unknown compound operator: {op}"),
2713                    line: self.line,
2714                })
2715            }
2716        }
2717        Ok(())
2718    }
2719
2720    /// Extract the root variable name from a (possibly nested) access expression.
2721    fn root_var_name(&self, node: &SNode) -> Option<String> {
2722        match &node.node {
2723            Node::Identifier(name) => Some(name.clone()),
2724            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
2725                self.root_var_name(object)
2726            }
2727            Node::SubscriptAccess { object, .. } => self.root_var_name(object),
2728            _ => None,
2729        }
2730    }
2731
2732    fn compile_top_level_declarations(&mut self, program: &[SNode]) -> Result<(), CompileError> {
2733        for sn in program {
2734            if matches!(
2735                &sn.node,
2736                Node::FnDecl { .. }
2737                    | Node::ToolDecl { .. }
2738                    | Node::ImplBlock { .. }
2739                    | Node::StructDecl { .. }
2740                    | Node::EnumDecl { .. }
2741                    | Node::InterfaceDecl { .. }
2742                    | Node::TypeDecl { .. }
2743            ) {
2744                self.compile_node(sn)?;
2745            }
2746        }
2747        Ok(())
2748    }
2749}
2750
2751impl Compiler {
2752    /// Recursively collect all enum type names from the AST.
2753    fn collect_enum_names(nodes: &[SNode], names: &mut std::collections::HashSet<String>) {
2754        for sn in nodes {
2755            match &sn.node {
2756                Node::EnumDecl { name, .. } => {
2757                    names.insert(name.clone());
2758                }
2759                Node::Pipeline { body, .. } => {
2760                    Self::collect_enum_names(body, names);
2761                }
2762                Node::FnDecl { body, .. } | Node::ToolDecl { body, .. } => {
2763                    Self::collect_enum_names(body, names);
2764                }
2765                Node::Block(stmts) => {
2766                    Self::collect_enum_names(stmts, names);
2767                }
2768                _ => {}
2769            }
2770        }
2771    }
2772
2773    fn collect_interface_methods(
2774        nodes: &[SNode],
2775        interfaces: &mut std::collections::HashMap<String, Vec<String>>,
2776    ) {
2777        for sn in nodes {
2778            match &sn.node {
2779                Node::InterfaceDecl { name, methods, .. } => {
2780                    let method_names: Vec<String> =
2781                        methods.iter().map(|m| m.name.clone()).collect();
2782                    interfaces.insert(name.clone(), method_names);
2783                }
2784                Node::Pipeline { body, .. }
2785                | Node::FnDecl { body, .. }
2786                | Node::ToolDecl { body, .. } => {
2787                    Self::collect_interface_methods(body, interfaces);
2788                }
2789                Node::Block(stmts) => {
2790                    Self::collect_interface_methods(stmts, interfaces);
2791                }
2792                _ => {}
2793            }
2794        }
2795    }
2796}
2797
2798impl Default for Compiler {
2799    fn default() -> Self {
2800        Self::new()
2801    }
2802}
2803
2804/// Check if a list of AST nodes contains any `yield` expression (used to detect generator functions).
2805fn body_contains_yield(nodes: &[SNode]) -> bool {
2806    nodes.iter().any(|sn| node_contains_yield(&sn.node))
2807}
2808
2809fn node_contains_yield(node: &Node) -> bool {
2810    match node {
2811        Node::YieldExpr { .. } => true,
2812        // Don't recurse into nested function/closure declarations — yield in a nested
2813        // function does NOT make the outer function a generator.
2814        Node::FnDecl { .. } | Node::Closure { .. } => false,
2815        Node::Block(stmts) => body_contains_yield(stmts),
2816        Node::IfElse {
2817            condition,
2818            then_body,
2819            else_body,
2820        } => {
2821            node_contains_yield(&condition.node)
2822                || body_contains_yield(then_body)
2823                || else_body.as_ref().is_some_and(|b| body_contains_yield(b))
2824        }
2825        Node::WhileLoop { condition, body } => {
2826            node_contains_yield(&condition.node) || body_contains_yield(body)
2827        }
2828        Node::ForIn { iterable, body, .. } => {
2829            node_contains_yield(&iterable.node) || body_contains_yield(body)
2830        }
2831        Node::TryCatch {
2832            body, catch_body, ..
2833        } => body_contains_yield(body) || body_contains_yield(catch_body),
2834        Node::TryExpr { body } => body_contains_yield(body),
2835        _ => false,
2836    }
2837}
2838
2839/// Check if an AST node contains `_` identifier (pipe placeholder).
2840fn contains_pipe_placeholder(node: &SNode) -> bool {
2841    match &node.node {
2842        Node::Identifier(name) if name == "_" => true,
2843        Node::FunctionCall { args, .. } => args.iter().any(contains_pipe_placeholder),
2844        Node::MethodCall { object, args, .. } => {
2845            contains_pipe_placeholder(object) || args.iter().any(contains_pipe_placeholder)
2846        }
2847        Node::BinaryOp { left, right, .. } => {
2848            contains_pipe_placeholder(left) || contains_pipe_placeholder(right)
2849        }
2850        Node::UnaryOp { operand, .. } => contains_pipe_placeholder(operand),
2851        Node::ListLiteral(items) => items.iter().any(contains_pipe_placeholder),
2852        Node::PropertyAccess { object, .. } => contains_pipe_placeholder(object),
2853        Node::SubscriptAccess { object, index } => {
2854            contains_pipe_placeholder(object) || contains_pipe_placeholder(index)
2855        }
2856        _ => false,
2857    }
2858}
2859
2860/// Replace all `_` identifiers with `__pipe` in an AST node (for pipe placeholder desugaring).
2861fn replace_pipe_placeholder(node: &SNode) -> SNode {
2862    let new_node = match &node.node {
2863        Node::Identifier(name) if name == "_" => Node::Identifier("__pipe".into()),
2864        Node::FunctionCall { name, args } => Node::FunctionCall {
2865            name: name.clone(),
2866            args: args.iter().map(replace_pipe_placeholder).collect(),
2867        },
2868        Node::MethodCall {
2869            object,
2870            method,
2871            args,
2872        } => Node::MethodCall {
2873            object: Box::new(replace_pipe_placeholder(object)),
2874            method: method.clone(),
2875            args: args.iter().map(replace_pipe_placeholder).collect(),
2876        },
2877        Node::BinaryOp { op, left, right } => Node::BinaryOp {
2878            op: op.clone(),
2879            left: Box::new(replace_pipe_placeholder(left)),
2880            right: Box::new(replace_pipe_placeholder(right)),
2881        },
2882        Node::UnaryOp { op, operand } => Node::UnaryOp {
2883            op: op.clone(),
2884            operand: Box::new(replace_pipe_placeholder(operand)),
2885        },
2886        Node::ListLiteral(items) => {
2887            Node::ListLiteral(items.iter().map(replace_pipe_placeholder).collect())
2888        }
2889        Node::PropertyAccess { object, property } => Node::PropertyAccess {
2890            object: Box::new(replace_pipe_placeholder(object)),
2891            property: property.clone(),
2892        },
2893        Node::SubscriptAccess { object, index } => Node::SubscriptAccess {
2894            object: Box::new(replace_pipe_placeholder(object)),
2895            index: Box::new(replace_pipe_placeholder(index)),
2896        },
2897        _ => return node.clone(),
2898    };
2899    SNode::new(new_node, node.span)
2900}
2901
2902#[cfg(test)]
2903mod tests {
2904    use super::*;
2905    use harn_lexer::Lexer;
2906    use harn_parser::Parser;
2907
2908    fn compile_source(source: &str) -> Chunk {
2909        let mut lexer = Lexer::new(source);
2910        let tokens = lexer.tokenize().unwrap();
2911        let mut parser = Parser::new(tokens);
2912        let program = parser.parse().unwrap();
2913        Compiler::new().compile(&program).unwrap()
2914    }
2915
2916    #[test]
2917    fn test_compile_arithmetic() {
2918        let chunk = compile_source("pipeline test(task) { let x = 2 + 3 }");
2919        assert!(!chunk.code.is_empty());
2920        // Should have constants: 2, 3, "x"
2921        assert!(chunk.constants.contains(&Constant::Int(2)));
2922        assert!(chunk.constants.contains(&Constant::Int(3)));
2923    }
2924
2925    #[test]
2926    fn test_compile_function_call() {
2927        let chunk = compile_source("pipeline test(task) { log(42) }");
2928        let disasm = chunk.disassemble("test");
2929        assert!(disasm.contains("CALL"));
2930    }
2931
2932    #[test]
2933    fn test_compile_if_else() {
2934        let chunk =
2935            compile_source(r#"pipeline test(task) { if true { log("yes") } else { log("no") } }"#);
2936        let disasm = chunk.disassemble("test");
2937        assert!(disasm.contains("JUMP_IF_FALSE"));
2938        assert!(disasm.contains("JUMP"));
2939    }
2940
2941    #[test]
2942    fn test_compile_while() {
2943        let chunk = compile_source("pipeline test(task) { var i = 0\n while i < 5 { i = i + 1 } }");
2944        let disasm = chunk.disassemble("test");
2945        assert!(disasm.contains("JUMP_IF_FALSE"));
2946        // Should have a backward jump
2947        assert!(disasm.contains("JUMP"));
2948    }
2949
2950    #[test]
2951    fn test_compile_closure() {
2952        let chunk = compile_source("pipeline test(task) { let f = { x -> x * 2 } }");
2953        assert!(!chunk.functions.is_empty());
2954        assert_eq!(chunk.functions[0].params, vec!["x"]);
2955    }
2956
2957    #[test]
2958    fn test_compile_list() {
2959        let chunk = compile_source("pipeline test(task) { let a = [1, 2, 3] }");
2960        let disasm = chunk.disassemble("test");
2961        assert!(disasm.contains("BUILD_LIST"));
2962    }
2963
2964    #[test]
2965    fn test_compile_dict() {
2966        let chunk = compile_source(r#"pipeline test(task) { let d = {name: "test"} }"#);
2967        let disasm = chunk.disassemble("test");
2968        assert!(disasm.contains("BUILD_DICT"));
2969    }
2970
2971    #[test]
2972    fn test_disassemble() {
2973        let chunk = compile_source("pipeline test(task) { log(2 + 3) }");
2974        let disasm = chunk.disassemble("test");
2975        // Should be readable
2976        assert!(disasm.contains("CONSTANT"));
2977        assert!(disasm.contains("ADD"));
2978        assert!(disasm.contains("CALL"));
2979    }
2980}