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