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