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