Skip to main content

harn_vm/
compiler.rs

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