Skip to main content

harn_vm/
compiler.rs

1use harn_lexer::StringSegment;
2use harn_parser::{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}
32
33/// Compiles an AST into bytecode.
34pub struct Compiler {
35    chunk: Chunk,
36    line: u32,
37    column: u32,
38    /// Track enum type names so PropertyAccess on them can produce EnumVariant.
39    enum_names: std::collections::HashSet<String>,
40    /// Stack of active loop contexts for break/continue.
41    loop_stack: Vec<LoopContext>,
42    /// Current depth of exception handlers (for cleanup on break/continue).
43    handler_depth: usize,
44}
45
46impl Compiler {
47    pub fn new() -> Self {
48        Self {
49            chunk: Chunk::new(),
50            line: 1,
51            column: 1,
52            enum_names: std::collections::HashSet::new(),
53            loop_stack: Vec::new(),
54            handler_depth: 0,
55        }
56    }
57
58    /// Compile a program (list of top-level nodes) into a Chunk.
59    /// Finds the entry pipeline and compiles its body, including inherited bodies.
60    pub fn compile(mut self, program: &[SNode]) -> Result<Chunk, CompileError> {
61        // Pre-scan the entire program for enum declarations (including inside pipelines)
62        // so we can recognize EnumName.Variant as enum construction.
63        Self::collect_enum_names(program, &mut self.enum_names);
64
65        // Compile all top-level non-pipeline declarations first (fn, enum, etc.)
66        for sn in program {
67            match &sn.node {
68                Node::ImportDecl { .. } | Node::SelectiveImport { .. } => {
69                    self.compile_node(sn)?;
70                }
71                _ => {}
72            }
73        }
74
75        // Find entry pipeline
76        let main = program
77            .iter()
78            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == "default"))
79            .or_else(|| {
80                program
81                    .iter()
82                    .find(|sn| matches!(&sn.node, Node::Pipeline { .. }))
83            });
84
85        if let Some(sn) = main {
86            if let Node::Pipeline { body, extends, .. } = &sn.node {
87                // If this pipeline extends another, compile the parent chain first
88                if let Some(parent_name) = extends {
89                    self.compile_parent_pipeline(program, parent_name)?;
90                }
91                self.compile_block(body)?;
92            }
93        }
94
95        self.chunk.emit(Op::Nil, self.line);
96        self.chunk.emit(Op::Return, self.line);
97        Ok(self.chunk)
98    }
99
100    /// Compile a specific named pipeline (for test runners).
101    pub fn compile_named(
102        mut self,
103        program: &[SNode],
104        pipeline_name: &str,
105    ) -> Result<Chunk, CompileError> {
106        Self::collect_enum_names(program, &mut self.enum_names);
107
108        for sn in program {
109            if matches!(
110                &sn.node,
111                Node::ImportDecl { .. } | Node::SelectiveImport { .. }
112            ) {
113                self.compile_node(sn)?;
114            }
115        }
116
117        let target = program
118            .iter()
119            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == pipeline_name));
120
121        if let Some(sn) = target {
122            if let Node::Pipeline { body, extends, .. } = &sn.node {
123                if let Some(parent_name) = extends {
124                    self.compile_parent_pipeline(program, parent_name)?;
125                }
126                self.compile_block(body)?;
127            }
128        }
129
130        self.chunk.emit(Op::Nil, self.line);
131        self.chunk.emit(Op::Return, self.line);
132        Ok(self.chunk)
133    }
134
135    /// Recursively compile parent pipeline bodies (for extends).
136    fn compile_parent_pipeline(
137        &mut self,
138        program: &[SNode],
139        parent_name: &str,
140    ) -> Result<(), CompileError> {
141        let parent = program
142            .iter()
143            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == parent_name));
144        if let Some(sn) = parent {
145            if let Node::Pipeline { body, extends, .. } = &sn.node {
146                // Recurse if this parent also extends another
147                if let Some(grandparent) = extends {
148                    self.compile_parent_pipeline(program, grandparent)?;
149                }
150                // Compile parent body - pop all statement values
151                for stmt in body {
152                    self.compile_node(stmt)?;
153                    if Self::produces_value(&stmt.node) {
154                        self.chunk.emit(Op::Pop, self.line);
155                    }
156                }
157            }
158        }
159        Ok(())
160    }
161
162    fn compile_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
163        for (i, snode) in stmts.iter().enumerate() {
164            self.compile_node(snode)?;
165            let is_last = i == stmts.len() - 1;
166            if is_last {
167                // If the last statement doesn't produce a value, push nil
168                // so the block always leaves exactly one value on the stack.
169                if !Self::produces_value(&snode.node) {
170                    self.chunk.emit(Op::Nil, self.line);
171                }
172            } else {
173                // Only pop if the statement leaves a value on the stack
174                if Self::produces_value(&snode.node) {
175                    self.chunk.emit(Op::Pop, self.line);
176                }
177            }
178        }
179        Ok(())
180    }
181
182    fn compile_node(&mut self, snode: &SNode) -> Result<(), CompileError> {
183        self.line = snode.span.line as u32;
184        self.column = snode.span.column as u32;
185        self.chunk.set_column(self.column);
186        match &snode.node {
187            Node::IntLiteral(n) => {
188                let idx = self.chunk.add_constant(Constant::Int(*n));
189                self.chunk.emit_u16(Op::Constant, idx, self.line);
190            }
191            Node::FloatLiteral(n) => {
192                let idx = self.chunk.add_constant(Constant::Float(*n));
193                self.chunk.emit_u16(Op::Constant, idx, self.line);
194            }
195            Node::StringLiteral(s) => {
196                let idx = self.chunk.add_constant(Constant::String(s.clone()));
197                self.chunk.emit_u16(Op::Constant, idx, self.line);
198            }
199            Node::BoolLiteral(true) => self.chunk.emit(Op::True, self.line),
200            Node::BoolLiteral(false) => self.chunk.emit(Op::False, self.line),
201            Node::NilLiteral => self.chunk.emit(Op::Nil, self.line),
202            Node::DurationLiteral(ms) => {
203                let idx = self.chunk.add_constant(Constant::Duration(*ms));
204                self.chunk.emit_u16(Op::Constant, idx, self.line);
205            }
206
207            Node::Identifier(name) => {
208                let idx = self.chunk.add_constant(Constant::String(name.clone()));
209                self.chunk.emit_u16(Op::GetVar, idx, self.line);
210            }
211
212            Node::LetBinding { name, value, .. } => {
213                self.compile_node(value)?;
214                let idx = self.chunk.add_constant(Constant::String(name.clone()));
215                self.chunk.emit_u16(Op::DefLet, idx, self.line);
216            }
217
218            Node::VarBinding { name, value, .. } => {
219                self.compile_node(value)?;
220                let idx = self.chunk.add_constant(Constant::String(name.clone()));
221                self.chunk.emit_u16(Op::DefVar, idx, self.line);
222            }
223
224            Node::Assignment {
225                target, value, op, ..
226            } => {
227                if let Node::Identifier(name) = &target.node {
228                    let idx = self.chunk.add_constant(Constant::String(name.clone()));
229                    if let Some(op) = op {
230                        self.chunk.emit_u16(Op::GetVar, idx, self.line);
231                        self.compile_node(value)?;
232                        self.emit_compound_op(op)?;
233                        self.chunk.emit_u16(Op::SetVar, idx, self.line);
234                    } else {
235                        self.compile_node(value)?;
236                        self.chunk.emit_u16(Op::SetVar, idx, self.line);
237                    }
238                } else if let Node::PropertyAccess { object, property } = &target.node {
239                    // obj.field = value → SetProperty
240                    if let Some(var_name) = self.root_var_name(object) {
241                        let var_idx = self.chunk.add_constant(Constant::String(var_name.clone()));
242                        let prop_idx = self.chunk.add_constant(Constant::String(property.clone()));
243                        if let Some(op) = op {
244                            // compound: obj.field += value
245                            self.compile_node(target)?; // push current obj.field
246                            self.compile_node(value)?;
247                            self.emit_compound_op(op)?;
248                        } else {
249                            self.compile_node(value)?;
250                        }
251                        // Stack: [new_value]
252                        // SetProperty reads var_idx from env, sets prop, writes back
253                        self.chunk.emit_u16(Op::SetProperty, prop_idx, self.line);
254                        // Encode the variable name index as a second u16
255                        let hi = (var_idx >> 8) as u8;
256                        let lo = var_idx as u8;
257                        self.chunk.code.push(hi);
258                        self.chunk.code.push(lo);
259                        self.chunk.lines.push(self.line);
260                        self.chunk.columns.push(self.column);
261                        self.chunk.lines.push(self.line);
262                        self.chunk.columns.push(self.column);
263                    }
264                } else if let Node::SubscriptAccess { object, index } = &target.node {
265                    // obj[idx] = value → SetSubscript
266                    if let Some(var_name) = self.root_var_name(object) {
267                        let var_idx = self.chunk.add_constant(Constant::String(var_name.clone()));
268                        if let Some(op) = op {
269                            self.compile_node(target)?;
270                            self.compile_node(value)?;
271                            self.emit_compound_op(op)?;
272                        } else {
273                            self.compile_node(value)?;
274                        }
275                        self.compile_node(index)?;
276                        self.chunk.emit_u16(Op::SetSubscript, var_idx, self.line);
277                    }
278                }
279            }
280
281            Node::BinaryOp { op, left, right } => {
282                // Short-circuit operators
283                match op.as_str() {
284                    "&&" => {
285                        self.compile_node(left)?;
286                        let jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
287                        self.chunk.emit(Op::Pop, self.line);
288                        self.compile_node(right)?;
289                        self.chunk.patch_jump(jump);
290                        // Normalize to bool
291                        self.chunk.emit(Op::Not, self.line);
292                        self.chunk.emit(Op::Not, self.line);
293                        return Ok(());
294                    }
295                    "||" => {
296                        self.compile_node(left)?;
297                        let jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
298                        self.chunk.emit(Op::Pop, self.line);
299                        self.compile_node(right)?;
300                        self.chunk.patch_jump(jump);
301                        self.chunk.emit(Op::Not, self.line);
302                        self.chunk.emit(Op::Not, self.line);
303                        return Ok(());
304                    }
305                    "??" => {
306                        self.compile_node(left)?;
307                        self.chunk.emit(Op::Dup, self.line);
308                        // Check if nil: push nil, compare
309                        self.chunk.emit(Op::Nil, self.line);
310                        self.chunk.emit(Op::NotEqual, self.line);
311                        let jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
312                        self.chunk.emit(Op::Pop, self.line); // pop the not-equal result
313                        self.chunk.emit(Op::Pop, self.line); // pop the nil value
314                        self.compile_node(right)?;
315                        let end = self.chunk.emit_jump(Op::Jump, self.line);
316                        self.chunk.patch_jump(jump);
317                        self.chunk.emit(Op::Pop, self.line); // pop the not-equal result
318                        self.chunk.patch_jump(end);
319                        return Ok(());
320                    }
321                    "|>" => {
322                        self.compile_node(left)?;
323                        self.compile_node(right)?;
324                        self.chunk.emit(Op::Pipe, self.line);
325                        return Ok(());
326                    }
327                    _ => {}
328                }
329
330                self.compile_node(left)?;
331                self.compile_node(right)?;
332                match op.as_str() {
333                    "+" => self.chunk.emit(Op::Add, self.line),
334                    "-" => self.chunk.emit(Op::Sub, self.line),
335                    "*" => self.chunk.emit(Op::Mul, self.line),
336                    "/" => self.chunk.emit(Op::Div, self.line),
337                    "%" => self.chunk.emit(Op::Mod, self.line),
338                    "==" => self.chunk.emit(Op::Equal, self.line),
339                    "!=" => self.chunk.emit(Op::NotEqual, self.line),
340                    "<" => self.chunk.emit(Op::Less, self.line),
341                    ">" => self.chunk.emit(Op::Greater, self.line),
342                    "<=" => self.chunk.emit(Op::LessEqual, self.line),
343                    ">=" => self.chunk.emit(Op::GreaterEqual, self.line),
344                    _ => {
345                        return Err(CompileError {
346                            message: format!("Unknown operator: {op}"),
347                            line: self.line,
348                        })
349                    }
350                }
351            }
352
353            Node::UnaryOp { op, operand } => {
354                self.compile_node(operand)?;
355                match op.as_str() {
356                    "-" => self.chunk.emit(Op::Negate, self.line),
357                    "!" => self.chunk.emit(Op::Not, self.line),
358                    _ => {}
359                }
360            }
361
362            Node::Ternary {
363                condition,
364                true_expr,
365                false_expr,
366            } => {
367                self.compile_node(condition)?;
368                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
369                self.chunk.emit(Op::Pop, self.line);
370                self.compile_node(true_expr)?;
371                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
372                self.chunk.patch_jump(else_jump);
373                self.chunk.emit(Op::Pop, self.line);
374                self.compile_node(false_expr)?;
375                self.chunk.patch_jump(end_jump);
376            }
377
378            Node::FunctionCall { name, args } => {
379                // Push function name as string constant
380                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
381                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
382                // Push arguments
383                for arg in args {
384                    self.compile_node(arg)?;
385                }
386                self.chunk.emit_u8(Op::Call, args.len() as u8, self.line);
387            }
388
389            Node::MethodCall {
390                object,
391                method,
392                args,
393            } => {
394                // Check if this is an enum variant construction with args: EnumName.Variant(args)
395                if let Node::Identifier(name) = &object.node {
396                    if self.enum_names.contains(name) {
397                        // Compile args, then BuildEnum
398                        for arg in args {
399                            self.compile_node(arg)?;
400                        }
401                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
402                        let var_idx = self.chunk.add_constant(Constant::String(method.clone()));
403                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
404                        let hi = (var_idx >> 8) as u8;
405                        let lo = var_idx as u8;
406                        self.chunk.code.push(hi);
407                        self.chunk.code.push(lo);
408                        self.chunk.lines.push(self.line);
409                        self.chunk.columns.push(self.column);
410                        self.chunk.lines.push(self.line);
411                        self.chunk.columns.push(self.column);
412                        let fc = args.len() as u16;
413                        let fhi = (fc >> 8) as u8;
414                        let flo = fc as u8;
415                        self.chunk.code.push(fhi);
416                        self.chunk.code.push(flo);
417                        self.chunk.lines.push(self.line);
418                        self.chunk.columns.push(self.column);
419                        self.chunk.lines.push(self.line);
420                        self.chunk.columns.push(self.column);
421                        return Ok(());
422                    }
423                }
424                self.compile_node(object)?;
425                for arg in args {
426                    self.compile_node(arg)?;
427                }
428                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
429                self.chunk
430                    .emit_method_call(name_idx, args.len() as u8, self.line);
431            }
432
433            Node::OptionalMethodCall {
434                object,
435                method,
436                args,
437            } => {
438                self.compile_node(object)?;
439                for arg in args {
440                    self.compile_node(arg)?;
441                }
442                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
443                self.chunk
444                    .emit_method_call_opt(name_idx, args.len() as u8, self.line);
445            }
446
447            Node::PropertyAccess { object, property } => {
448                // Check if this is an enum variant construction: EnumName.Variant
449                if let Node::Identifier(name) = &object.node {
450                    if self.enum_names.contains(name) {
451                        // Emit BuildEnum with 0 fields
452                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
453                        let var_idx = self.chunk.add_constant(Constant::String(property.clone()));
454                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
455                        let hi = (var_idx >> 8) as u8;
456                        let lo = var_idx as u8;
457                        self.chunk.code.push(hi);
458                        self.chunk.code.push(lo);
459                        self.chunk.lines.push(self.line);
460                        self.chunk.columns.push(self.column);
461                        self.chunk.lines.push(self.line);
462                        self.chunk.columns.push(self.column);
463                        // 0 fields
464                        self.chunk.code.push(0);
465                        self.chunk.code.push(0);
466                        self.chunk.lines.push(self.line);
467                        self.chunk.columns.push(self.column);
468                        self.chunk.lines.push(self.line);
469                        self.chunk.columns.push(self.column);
470                        return Ok(());
471                    }
472                }
473                self.compile_node(object)?;
474                let idx = self.chunk.add_constant(Constant::String(property.clone()));
475                self.chunk.emit_u16(Op::GetProperty, idx, self.line);
476            }
477
478            Node::OptionalPropertyAccess { object, property } => {
479                self.compile_node(object)?;
480                let idx = self.chunk.add_constant(Constant::String(property.clone()));
481                self.chunk.emit_u16(Op::GetPropertyOpt, idx, self.line);
482            }
483
484            Node::SubscriptAccess { object, index } => {
485                self.compile_node(object)?;
486                self.compile_node(index)?;
487                self.chunk.emit(Op::Subscript, self.line);
488            }
489
490            Node::SliceAccess { object, start, end } => {
491                self.compile_node(object)?;
492                if let Some(s) = start {
493                    self.compile_node(s)?;
494                } else {
495                    self.chunk.emit(Op::Nil, self.line);
496                }
497                if let Some(e) = end {
498                    self.compile_node(e)?;
499                } else {
500                    self.chunk.emit(Op::Nil, self.line);
501                }
502                self.chunk.emit(Op::Slice, self.line);
503            }
504
505            Node::IfElse {
506                condition,
507                then_body,
508                else_body,
509            } => {
510                self.compile_node(condition)?;
511                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
512                self.chunk.emit(Op::Pop, self.line);
513                self.compile_block(then_body)?;
514                if let Some(else_body) = else_body {
515                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
516                    self.chunk.patch_jump(else_jump);
517                    self.chunk.emit(Op::Pop, self.line);
518                    self.compile_block(else_body)?;
519                    self.chunk.patch_jump(end_jump);
520                } else {
521                    self.chunk.patch_jump(else_jump);
522                    self.chunk.emit(Op::Pop, self.line);
523                    self.chunk.emit(Op::Nil, self.line);
524                }
525            }
526
527            Node::WhileLoop { condition, body } => {
528                let loop_start = self.chunk.current_offset();
529                self.loop_stack.push(LoopContext {
530                    start_offset: loop_start,
531                    break_patches: Vec::new(),
532                    has_iterator: false,
533                    handler_depth: self.handler_depth,
534                });
535                self.compile_node(condition)?;
536                let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
537                self.chunk.emit(Op::Pop, self.line); // pop condition
538                                                     // Compile body statements, popping all results
539                for sn in body {
540                    self.compile_node(sn)?;
541                    if Self::produces_value(&sn.node) {
542                        self.chunk.emit(Op::Pop, self.line);
543                    }
544                }
545                // Jump back to condition
546                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
547                self.chunk.patch_jump(exit_jump);
548                self.chunk.emit(Op::Pop, self.line); // pop condition
549                                                     // Patch all break jumps to here
550                let ctx = self.loop_stack.pop().unwrap();
551                for patch_pos in ctx.break_patches {
552                    self.chunk.patch_jump(patch_pos);
553                }
554                self.chunk.emit(Op::Nil, self.line);
555            }
556
557            Node::ForIn {
558                variable,
559                iterable,
560                body,
561            } => {
562                // Compile iterable
563                self.compile_node(iterable)?;
564                // Variable name
565                let var_idx = self.chunk.add_constant(Constant::String(variable.clone()));
566                // Initialize iterator
567                self.chunk.emit(Op::IterInit, self.line);
568                let loop_start = self.chunk.current_offset();
569                self.loop_stack.push(LoopContext {
570                    start_offset: loop_start,
571                    break_patches: Vec::new(),
572                    has_iterator: true,
573                    handler_depth: self.handler_depth,
574                });
575                // Try to get next item — jumps to end if exhausted
576                let exit_jump_pos = self.chunk.emit_jump(Op::IterNext, self.line);
577                // Define loop variable with current item (item is on stack from IterNext)
578                self.chunk.emit_u16(Op::DefVar, var_idx, self.line);
579                // Compile body statements, popping all results
580                for sn in body {
581                    self.compile_node(sn)?;
582                    if Self::produces_value(&sn.node) {
583                        self.chunk.emit(Op::Pop, self.line);
584                    }
585                }
586                // Loop back
587                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
588                self.chunk.patch_jump(exit_jump_pos);
589                // Patch all break jumps to here
590                let ctx = self.loop_stack.pop().unwrap();
591                for patch_pos in ctx.break_patches {
592                    self.chunk.patch_jump(patch_pos);
593                }
594                // Push nil as result (iterator state was consumed)
595                self.chunk.emit(Op::Nil, self.line);
596            }
597
598            Node::ReturnStmt { value } => {
599                if let Some(val) = value {
600                    // Tail call optimization: if returning a direct function call,
601                    // emit TailCall instead of Call to reuse the current frame.
602                    if let Node::FunctionCall { name, args } = &val.node {
603                        let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
604                        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
605                        for arg in args {
606                            self.compile_node(arg)?;
607                        }
608                        self.chunk
609                            .emit_u8(Op::TailCall, args.len() as u8, self.line);
610                    } else if let Node::BinaryOp { op, left, right } = &val.node {
611                        if op == "|>" {
612                            // Tail pipe optimization: `return x |> f` becomes a tail call.
613                            // Compile left side (value) — inner pipes compile normally.
614                            self.compile_node(left)?;
615                            // Compile right side (callable reference).
616                            self.compile_node(right)?;
617                            // Stack is now [value, callable]. TailCall expects [callable, args...],
618                            // so swap to get [callable, value] then tail-call with 1 arg.
619                            self.chunk.emit(Op::Swap, self.line);
620                            self.chunk.emit_u8(Op::TailCall, 1, self.line);
621                        } else {
622                            self.compile_node(val)?;
623                        }
624                    } else {
625                        self.compile_node(val)?;
626                    }
627                } else {
628                    self.chunk.emit(Op::Nil, self.line);
629                }
630                self.chunk.emit(Op::Return, self.line);
631            }
632
633            Node::BreakStmt => {
634                if self.loop_stack.is_empty() {
635                    return Err(CompileError {
636                        message: "break outside of loop".to_string(),
637                        line: self.line,
638                    });
639                }
640                let ctx = self.loop_stack.last().unwrap();
641                // Pop exception handlers that were pushed inside the loop
642                for _ in ctx.handler_depth..self.handler_depth {
643                    self.chunk.emit(Op::PopHandler, self.line);
644                }
645                // Pop iterator if breaking from a for-in loop
646                if ctx.has_iterator {
647                    self.chunk.emit(Op::PopIterator, self.line);
648                }
649                let patch = self.chunk.emit_jump(Op::Jump, self.line);
650                self.loop_stack
651                    .last_mut()
652                    .unwrap()
653                    .break_patches
654                    .push(patch);
655            }
656
657            Node::ContinueStmt => {
658                if self.loop_stack.is_empty() {
659                    return Err(CompileError {
660                        message: "continue outside of loop".to_string(),
661                        line: self.line,
662                    });
663                }
664                let ctx = self.loop_stack.last().unwrap();
665                // Pop exception handlers that were pushed inside the loop
666                for _ in ctx.handler_depth..self.handler_depth {
667                    self.chunk.emit(Op::PopHandler, self.line);
668                }
669                let loop_start = ctx.start_offset;
670                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
671            }
672
673            Node::ListLiteral(elements) => {
674                for el in elements {
675                    self.compile_node(el)?;
676                }
677                self.chunk
678                    .emit_u16(Op::BuildList, elements.len() as u16, self.line);
679            }
680
681            Node::DictLiteral(entries) => {
682                for entry in entries {
683                    self.compile_node(&entry.key)?;
684                    self.compile_node(&entry.value)?;
685                }
686                self.chunk
687                    .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
688            }
689
690            Node::InterpolatedString(segments) => {
691                let mut part_count = 0u16;
692                for seg in segments {
693                    match seg {
694                        StringSegment::Literal(s) => {
695                            let idx = self.chunk.add_constant(Constant::String(s.clone()));
696                            self.chunk.emit_u16(Op::Constant, idx, self.line);
697                            part_count += 1;
698                        }
699                        StringSegment::Expression(expr_str) => {
700                            // Parse and compile the embedded expression
701                            let mut lexer = harn_lexer::Lexer::new(expr_str);
702                            if let Ok(tokens) = lexer.tokenize() {
703                                let mut parser = harn_parser::Parser::new(tokens);
704                                if let Ok(snode) = parser.parse_single_expression() {
705                                    self.compile_node(&snode)?;
706                                    // Convert result to string for concatenation
707                                    let to_str = self
708                                        .chunk
709                                        .add_constant(Constant::String("to_string".into()));
710                                    self.chunk.emit_u16(Op::Constant, to_str, self.line);
711                                    self.chunk.emit(Op::Swap, self.line);
712                                    self.chunk.emit_u8(Op::Call, 1, self.line);
713                                    part_count += 1;
714                                } else {
715                                    // Fallback: treat as literal string
716                                    let idx =
717                                        self.chunk.add_constant(Constant::String(expr_str.clone()));
718                                    self.chunk.emit_u16(Op::Constant, idx, self.line);
719                                    part_count += 1;
720                                }
721                            }
722                        }
723                    }
724                }
725                if part_count > 1 {
726                    self.chunk.emit_u16(Op::Concat, part_count, self.line);
727                }
728            }
729
730            Node::FnDecl {
731                name, params, body, ..
732            } => {
733                // Compile function body into a separate chunk
734                let mut fn_compiler = Compiler::new();
735                fn_compiler.enum_names = self.enum_names.clone();
736                fn_compiler.compile_block(body)?;
737                fn_compiler.chunk.emit(Op::Nil, self.line);
738                fn_compiler.chunk.emit(Op::Return, self.line);
739
740                let func = CompiledFunction {
741                    name: name.clone(),
742                    params: TypedParam::names(params),
743                    chunk: fn_compiler.chunk,
744                };
745                let fn_idx = self.chunk.functions.len();
746                self.chunk.functions.push(func);
747
748                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
749                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
750                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
751            }
752
753            Node::Closure { params, body } => {
754                let mut fn_compiler = Compiler::new();
755                fn_compiler.enum_names = self.enum_names.clone();
756                fn_compiler.compile_block(body)?;
757                // If block didn't end with return, the last value is on the stack
758                fn_compiler.chunk.emit(Op::Return, self.line);
759
760                let func = CompiledFunction {
761                    name: "<closure>".to_string(),
762                    params: TypedParam::names(params),
763                    chunk: fn_compiler.chunk,
764                };
765                let fn_idx = self.chunk.functions.len();
766                self.chunk.functions.push(func);
767
768                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
769            }
770
771            Node::ThrowStmt { value } => {
772                self.compile_node(value)?;
773                self.chunk.emit(Op::Throw, self.line);
774            }
775
776            Node::MatchExpr { value, arms } => {
777                self.compile_node(value)?;
778                let mut end_jumps = Vec::new();
779                for arm in arms {
780                    match &arm.pattern.node {
781                        // Wildcard `_` — always matches
782                        Node::Identifier(name) if name == "_" => {
783                            self.chunk.emit(Op::Pop, self.line); // pop match value
784                            self.compile_match_body(&arm.body)?;
785                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
786                        }
787                        // Enum destructuring: EnumConstruct pattern
788                        Node::EnumConstruct {
789                            enum_name,
790                            variant,
791                            args: pat_args,
792                        } => {
793                            // Check if the match value is this enum variant
794                            self.chunk.emit(Op::Dup, self.line);
795                            let en_idx =
796                                self.chunk.add_constant(Constant::String(enum_name.clone()));
797                            let vn_idx = self.chunk.add_constant(Constant::String(variant.clone()));
798                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
799                            let hi = (vn_idx >> 8) as u8;
800                            let lo = vn_idx as u8;
801                            self.chunk.code.push(hi);
802                            self.chunk.code.push(lo);
803                            self.chunk.lines.push(self.line);
804                            self.chunk.columns.push(self.column);
805                            self.chunk.lines.push(self.line);
806                            self.chunk.columns.push(self.column);
807                            // Stack: [match_value, bool]
808                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
809                            self.chunk.emit(Op::Pop, self.line); // pop bool
810
811                            // Destructure: bind field variables from the enum's fields
812                            // The match value is still on the stack; we need to extract fields
813                            for (i, pat_arg) in pat_args.iter().enumerate() {
814                                if let Node::Identifier(binding_name) = &pat_arg.node {
815                                    // Dup the match value, get .fields, subscript [i]
816                                    self.chunk.emit(Op::Dup, self.line);
817                                    let fields_idx = self
818                                        .chunk
819                                        .add_constant(Constant::String("fields".to_string()));
820                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
821                                    let idx_const =
822                                        self.chunk.add_constant(Constant::Int(i as i64));
823                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
824                                    self.chunk.emit(Op::Subscript, self.line);
825                                    let name_idx = self
826                                        .chunk
827                                        .add_constant(Constant::String(binding_name.clone()));
828                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
829                                }
830                            }
831
832                            self.chunk.emit(Op::Pop, self.line); // pop match value
833                            self.compile_match_body(&arm.body)?;
834                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
835                            self.chunk.patch_jump(skip);
836                            self.chunk.emit(Op::Pop, self.line); // pop bool
837                        }
838                        // Enum variant without args: PropertyAccess(EnumName, Variant)
839                        Node::PropertyAccess { object, property } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
840                        {
841                            let enum_name = if let Node::Identifier(n) = &object.node {
842                                n.clone()
843                            } else {
844                                unreachable!()
845                            };
846                            self.chunk.emit(Op::Dup, self.line);
847                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
848                            let vn_idx =
849                                self.chunk.add_constant(Constant::String(property.clone()));
850                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
851                            let hi = (vn_idx >> 8) as u8;
852                            let lo = vn_idx as u8;
853                            self.chunk.code.push(hi);
854                            self.chunk.code.push(lo);
855                            self.chunk.lines.push(self.line);
856                            self.chunk.columns.push(self.column);
857                            self.chunk.lines.push(self.line);
858                            self.chunk.columns.push(self.column);
859                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
860                            self.chunk.emit(Op::Pop, self.line); // pop bool
861                            self.chunk.emit(Op::Pop, self.line); // pop match value
862                            self.compile_match_body(&arm.body)?;
863                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
864                            self.chunk.patch_jump(skip);
865                            self.chunk.emit(Op::Pop, self.line); // pop bool
866                        }
867                        // Enum destructuring via MethodCall: EnumName.Variant(bindings...)
868                        // Parser produces MethodCall for EnumName.Variant(x) patterns
869                        Node::MethodCall {
870                            object,
871                            method,
872                            args: pat_args,
873                        } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
874                        {
875                            let enum_name = if let Node::Identifier(n) = &object.node {
876                                n.clone()
877                            } else {
878                                unreachable!()
879                            };
880                            // Check if the match value is this enum variant
881                            self.chunk.emit(Op::Dup, self.line);
882                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
883                            let vn_idx = self.chunk.add_constant(Constant::String(method.clone()));
884                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
885                            let hi = (vn_idx >> 8) as u8;
886                            let lo = vn_idx as u8;
887                            self.chunk.code.push(hi);
888                            self.chunk.code.push(lo);
889                            self.chunk.lines.push(self.line);
890                            self.chunk.columns.push(self.column);
891                            self.chunk.lines.push(self.line);
892                            self.chunk.columns.push(self.column);
893                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
894                            self.chunk.emit(Op::Pop, self.line); // pop bool
895
896                            // Destructure: bind field variables
897                            for (i, pat_arg) in pat_args.iter().enumerate() {
898                                if let Node::Identifier(binding_name) = &pat_arg.node {
899                                    self.chunk.emit(Op::Dup, self.line);
900                                    let fields_idx = self
901                                        .chunk
902                                        .add_constant(Constant::String("fields".to_string()));
903                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
904                                    let idx_const =
905                                        self.chunk.add_constant(Constant::Int(i as i64));
906                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
907                                    self.chunk.emit(Op::Subscript, self.line);
908                                    let name_idx = self
909                                        .chunk
910                                        .add_constant(Constant::String(binding_name.clone()));
911                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
912                                }
913                            }
914
915                            self.chunk.emit(Op::Pop, self.line); // pop match value
916                            self.compile_match_body(&arm.body)?;
917                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
918                            self.chunk.patch_jump(skip);
919                            self.chunk.emit(Op::Pop, self.line); // pop bool
920                        }
921                        // Binding pattern: bare identifier (not a literal)
922                        Node::Identifier(name) => {
923                            // Bind the match value to this name, always matches
924                            self.chunk.emit(Op::Dup, self.line); // dup for binding
925                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
926                            self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
927                            self.chunk.emit(Op::Pop, self.line); // pop match value
928                            self.compile_match_body(&arm.body)?;
929                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
930                        }
931                        // Literal/expression pattern — compare with Equal
932                        _ => {
933                            self.chunk.emit(Op::Dup, self.line);
934                            self.compile_node(&arm.pattern)?;
935                            self.chunk.emit(Op::Equal, self.line);
936                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
937                            self.chunk.emit(Op::Pop, self.line); // pop bool
938                            self.chunk.emit(Op::Pop, self.line); // pop match value
939                            self.compile_match_body(&arm.body)?;
940                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
941                            self.chunk.patch_jump(skip);
942                            self.chunk.emit(Op::Pop, self.line); // pop bool
943                        }
944                    }
945                }
946                // No match — pop value, push nil
947                self.chunk.emit(Op::Pop, self.line);
948                self.chunk.emit(Op::Nil, self.line);
949                for j in end_jumps {
950                    self.chunk.patch_jump(j);
951                }
952            }
953
954            Node::RangeExpr {
955                start,
956                end,
957                inclusive,
958            } => {
959                // Compile as __range__(start, end, inclusive_bool) builtin call
960                let name_idx = self
961                    .chunk
962                    .add_constant(Constant::String("__range__".to_string()));
963                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
964                self.compile_node(start)?;
965                self.compile_node(end)?;
966                if *inclusive {
967                    self.chunk.emit(Op::True, self.line);
968                } else {
969                    self.chunk.emit(Op::False, self.line);
970                }
971                self.chunk.emit_u8(Op::Call, 3, self.line);
972            }
973
974            Node::GuardStmt {
975                condition,
976                else_body,
977            } => {
978                // guard condition else { body }
979                // Compile condition; if truthy, skip else_body
980                self.compile_node(condition)?;
981                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
982                self.chunk.emit(Op::Pop, self.line); // pop condition
983                                                     // Compile else_body
984                self.compile_block(else_body)?;
985                // Pop result of else_body (guard is a statement, not expression)
986                if !else_body.is_empty() && Self::produces_value(&else_body.last().unwrap().node) {
987                    self.chunk.emit(Op::Pop, self.line);
988                }
989                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
990                self.chunk.patch_jump(skip_jump);
991                self.chunk.emit(Op::Pop, self.line); // pop condition
992                self.chunk.patch_jump(end_jump);
993                self.chunk.emit(Op::Nil, self.line);
994            }
995
996            Node::Block(stmts) => {
997                if stmts.is_empty() {
998                    self.chunk.emit(Op::Nil, self.line);
999                } else {
1000                    self.compile_block(stmts)?;
1001                }
1002            }
1003
1004            Node::DeadlineBlock { duration, body } => {
1005                self.compile_node(duration)?;
1006                self.chunk.emit(Op::DeadlineSetup, self.line);
1007                if body.is_empty() {
1008                    self.chunk.emit(Op::Nil, self.line);
1009                } else {
1010                    self.compile_block(body)?;
1011                }
1012                self.chunk.emit(Op::DeadlineEnd, self.line);
1013            }
1014
1015            Node::MutexBlock { body } => {
1016                // v1: single-threaded, just compile the body
1017                if body.is_empty() {
1018                    self.chunk.emit(Op::Nil, self.line);
1019                } else {
1020                    // Compile body, but pop intermediate values and push nil at the end.
1021                    // The body typically contains statements (assignments) that don't produce values.
1022                    for sn in body {
1023                        self.compile_node(sn)?;
1024                        if Self::produces_value(&sn.node) {
1025                            self.chunk.emit(Op::Pop, self.line);
1026                        }
1027                    }
1028                    self.chunk.emit(Op::Nil, self.line);
1029                }
1030            }
1031
1032            Node::YieldExpr { .. } => {
1033                // v1: yield is host-integration only, emit nil
1034                self.chunk.emit(Op::Nil, self.line);
1035            }
1036
1037            Node::AskExpr { fields } => {
1038                // Compile as a dict literal and call llm_call builtin
1039                // For v1, just build the dict (llm_call requires async)
1040                for entry in fields {
1041                    self.compile_node(&entry.key)?;
1042                    self.compile_node(&entry.value)?;
1043                }
1044                self.chunk
1045                    .emit_u16(Op::BuildDict, fields.len() as u16, self.line);
1046            }
1047
1048            Node::EnumConstruct {
1049                enum_name,
1050                variant,
1051                args,
1052            } => {
1053                // Push field values onto the stack, then BuildEnum
1054                for arg in args {
1055                    self.compile_node(arg)?;
1056                }
1057                let enum_idx = self.chunk.add_constant(Constant::String(enum_name.clone()));
1058                let var_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1059                // BuildEnum: enum_name_idx, variant_idx, field_count
1060                self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
1061                let hi = (var_idx >> 8) as u8;
1062                let lo = var_idx as u8;
1063                self.chunk.code.push(hi);
1064                self.chunk.code.push(lo);
1065                self.chunk.lines.push(self.line);
1066                self.chunk.columns.push(self.column);
1067                self.chunk.lines.push(self.line);
1068                self.chunk.columns.push(self.column);
1069                let fc = args.len() as u16;
1070                let fhi = (fc >> 8) as u8;
1071                let flo = fc as u8;
1072                self.chunk.code.push(fhi);
1073                self.chunk.code.push(flo);
1074                self.chunk.lines.push(self.line);
1075                self.chunk.columns.push(self.column);
1076                self.chunk.lines.push(self.line);
1077                self.chunk.columns.push(self.column);
1078            }
1079
1080            Node::StructConstruct {
1081                struct_name,
1082                fields,
1083            } => {
1084                // Build as a dict with a __struct__ key for metadata
1085                let struct_key = self
1086                    .chunk
1087                    .add_constant(Constant::String("__struct__".to_string()));
1088                let struct_val = self
1089                    .chunk
1090                    .add_constant(Constant::String(struct_name.clone()));
1091                self.chunk.emit_u16(Op::Constant, struct_key, self.line);
1092                self.chunk.emit_u16(Op::Constant, struct_val, self.line);
1093
1094                for entry in fields {
1095                    self.compile_node(&entry.key)?;
1096                    self.compile_node(&entry.value)?;
1097                }
1098                self.chunk
1099                    .emit_u16(Op::BuildDict, (fields.len() + 1) as u16, self.line);
1100            }
1101
1102            Node::ImportDecl { path } => {
1103                let idx = self.chunk.add_constant(Constant::String(path.clone()));
1104                self.chunk.emit_u16(Op::Import, idx, self.line);
1105            }
1106
1107            Node::SelectiveImport { names, path } => {
1108                let path_idx = self.chunk.add_constant(Constant::String(path.clone()));
1109                let names_str = names.join(",");
1110                let names_idx = self.chunk.add_constant(Constant::String(names_str));
1111                self.chunk
1112                    .emit_u16(Op::SelectiveImport, path_idx, self.line);
1113                let hi = (names_idx >> 8) as u8;
1114                let lo = names_idx as u8;
1115                self.chunk.code.push(hi);
1116                self.chunk.code.push(lo);
1117                self.chunk.lines.push(self.line);
1118                self.chunk.columns.push(self.column);
1119                self.chunk.lines.push(self.line);
1120                self.chunk.columns.push(self.column);
1121            }
1122
1123            // Declarations that only register metadata (no runtime effect needed for v1)
1124            Node::Pipeline { .. }
1125            | Node::OverrideDecl { .. }
1126            | Node::TypeDecl { .. }
1127            | Node::EnumDecl { .. }
1128            | Node::StructDecl { .. }
1129            | Node::InterfaceDecl { .. } => {
1130                self.chunk.emit(Op::Nil, self.line);
1131            }
1132
1133            Node::TryCatch {
1134                body,
1135                error_var,
1136                error_type,
1137                catch_body,
1138            } => {
1139                // Extract the type name for typed catch (e.g., "AppError")
1140                let type_name = error_type.as_ref().and_then(|te| {
1141                    // TypeExpr is a Named(String) for simple type names
1142                    if let harn_parser::TypeExpr::Named(name) = te {
1143                        Some(name.clone())
1144                    } else {
1145                        None
1146                    }
1147                });
1148
1149                // Store the error type name as a constant (or empty string for untyped)
1150                let type_name_idx = if let Some(ref tn) = type_name {
1151                    self.chunk.add_constant(Constant::String(tn.clone()))
1152                } else {
1153                    self.chunk.add_constant(Constant::String(String::new()))
1154                };
1155
1156                // 1. Emit TryCatchSetup with placeholder offset to catch handler
1157                self.handler_depth += 1;
1158                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1159                // Emit the type name index as extra u16 after the jump offset
1160                let hi = (type_name_idx >> 8) as u8;
1161                let lo = type_name_idx as u8;
1162                self.chunk.code.push(hi);
1163                self.chunk.code.push(lo);
1164                self.chunk.lines.push(self.line);
1165                self.chunk.columns.push(self.column);
1166                self.chunk.lines.push(self.line);
1167                self.chunk.columns.push(self.column);
1168
1169                // 2. Compile try body
1170                if body.is_empty() {
1171                    self.chunk.emit(Op::Nil, self.line);
1172                } else {
1173                    self.compile_block(body)?;
1174                    // If last statement doesn't produce a value, push nil
1175                    if !Self::produces_value(&body.last().unwrap().node) {
1176                        self.chunk.emit(Op::Nil, self.line);
1177                    }
1178                }
1179
1180                // 3. Emit PopHandler (successful try body completion)
1181                self.handler_depth -= 1;
1182                self.chunk.emit(Op::PopHandler, self.line);
1183
1184                // 4. Emit Jump past catch body
1185                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1186
1187                // 5. Patch the catch offset to point here
1188                self.chunk.patch_jump(catch_jump);
1189
1190                // 6. Error value is on the stack from the handler.
1191                //    If error_var exists, bind it; otherwise pop the error value.
1192                if let Some(var_name) = error_var {
1193                    let idx = self.chunk.add_constant(Constant::String(var_name.clone()));
1194                    self.chunk.emit_u16(Op::DefLet, idx, self.line);
1195                } else {
1196                    self.chunk.emit(Op::Pop, self.line);
1197                }
1198
1199                // 7. Compile catch body
1200                if catch_body.is_empty() {
1201                    self.chunk.emit(Op::Nil, self.line);
1202                } else {
1203                    self.compile_block(catch_body)?;
1204                    if !Self::produces_value(&catch_body.last().unwrap().node) {
1205                        self.chunk.emit(Op::Nil, self.line);
1206                    }
1207                }
1208
1209                // 8. Patch the end jump
1210                self.chunk.patch_jump(end_jump);
1211            }
1212
1213            Node::Retry { count, body } => {
1214                // Compile count expression into a mutable counter variable
1215                self.compile_node(count)?;
1216                let counter_name = "__retry_counter__";
1217                let counter_idx = self
1218                    .chunk
1219                    .add_constant(Constant::String(counter_name.to_string()));
1220                self.chunk.emit_u16(Op::DefVar, counter_idx, self.line);
1221
1222                // Also store the last error for re-throwing
1223                self.chunk.emit(Op::Nil, self.line);
1224                let err_name = "__retry_last_error__";
1225                let err_idx = self
1226                    .chunk
1227                    .add_constant(Constant::String(err_name.to_string()));
1228                self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
1229
1230                // Loop start
1231                let loop_start = self.chunk.current_offset();
1232
1233                // Set up try/catch (untyped - empty type name)
1234                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1235                // Emit empty type name for untyped catch
1236                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1237                let hi = (empty_type >> 8) as u8;
1238                let lo = empty_type as u8;
1239                self.chunk.code.push(hi);
1240                self.chunk.code.push(lo);
1241                self.chunk.lines.push(self.line);
1242                self.chunk.columns.push(self.column);
1243                self.chunk.lines.push(self.line);
1244                self.chunk.columns.push(self.column);
1245
1246                // Compile body
1247                self.compile_block(body)?;
1248
1249                // Success: pop handler, jump to end
1250                self.chunk.emit(Op::PopHandler, self.line);
1251                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1252
1253                // Catch handler
1254                self.chunk.patch_jump(catch_jump);
1255                // Save the error value for potential re-throw
1256                self.chunk.emit(Op::Dup, self.line);
1257                self.chunk.emit_u16(Op::SetVar, err_idx, self.line);
1258                // Pop the error value
1259                self.chunk.emit(Op::Pop, self.line);
1260
1261                // Decrement counter
1262                self.chunk.emit_u16(Op::GetVar, counter_idx, self.line);
1263                let one_idx = self.chunk.add_constant(Constant::Int(1));
1264                self.chunk.emit_u16(Op::Constant, one_idx, self.line);
1265                self.chunk.emit(Op::Sub, self.line);
1266                self.chunk.emit(Op::Dup, self.line);
1267                self.chunk.emit_u16(Op::SetVar, counter_idx, self.line);
1268
1269                // If counter > 0, jump to loop start
1270                let zero_idx = self.chunk.add_constant(Constant::Int(0));
1271                self.chunk.emit_u16(Op::Constant, zero_idx, self.line);
1272                self.chunk.emit(Op::Greater, self.line);
1273                let retry_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1274                self.chunk.emit(Op::Pop, self.line); // pop condition
1275                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
1276
1277                // No more retries — re-throw the last error
1278                self.chunk.patch_jump(retry_jump);
1279                self.chunk.emit(Op::Pop, self.line); // pop condition
1280                self.chunk.emit_u16(Op::GetVar, err_idx, self.line);
1281                self.chunk.emit(Op::Throw, self.line);
1282
1283                self.chunk.patch_jump(end_jump);
1284                // Push nil as the result of a successful retry block
1285                self.chunk.emit(Op::Nil, self.line);
1286            }
1287
1288            Node::Parallel {
1289                count,
1290                variable,
1291                body,
1292            } => {
1293                self.compile_node(count)?;
1294                let mut fn_compiler = Compiler::new();
1295                fn_compiler.enum_names = self.enum_names.clone();
1296                fn_compiler.compile_block(body)?;
1297                fn_compiler.chunk.emit(Op::Return, self.line);
1298                let params = vec![variable.clone().unwrap_or_else(|| "__i__".to_string())];
1299                let func = CompiledFunction {
1300                    name: "<parallel>".to_string(),
1301                    params,
1302                    chunk: fn_compiler.chunk,
1303                };
1304                let fn_idx = self.chunk.functions.len();
1305                self.chunk.functions.push(func);
1306                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1307                self.chunk.emit(Op::Parallel, self.line);
1308            }
1309
1310            Node::ParallelMap {
1311                list,
1312                variable,
1313                body,
1314            } => {
1315                self.compile_node(list)?;
1316                let mut fn_compiler = Compiler::new();
1317                fn_compiler.enum_names = self.enum_names.clone();
1318                fn_compiler.compile_block(body)?;
1319                fn_compiler.chunk.emit(Op::Return, self.line);
1320                let func = CompiledFunction {
1321                    name: "<parallel_map>".to_string(),
1322                    params: vec![variable.clone()],
1323                    chunk: fn_compiler.chunk,
1324                };
1325                let fn_idx = self.chunk.functions.len();
1326                self.chunk.functions.push(func);
1327                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1328                self.chunk.emit(Op::ParallelMap, self.line);
1329            }
1330
1331            Node::SpawnExpr { body } => {
1332                let mut fn_compiler = Compiler::new();
1333                fn_compiler.enum_names = self.enum_names.clone();
1334                fn_compiler.compile_block(body)?;
1335                fn_compiler.chunk.emit(Op::Return, self.line);
1336                let func = CompiledFunction {
1337                    name: "<spawn>".to_string(),
1338                    params: vec![],
1339                    chunk: fn_compiler.chunk,
1340                };
1341                let fn_idx = self.chunk.functions.len();
1342                self.chunk.functions.push(func);
1343                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1344                self.chunk.emit(Op::Spawn, self.line);
1345            }
1346        }
1347        Ok(())
1348    }
1349
1350    /// Check if a node produces a value on the stack that needs to be popped.
1351    fn produces_value(node: &Node) -> bool {
1352        match node {
1353            // These nodes do NOT produce a value on the stack
1354            Node::LetBinding { .. }
1355            | Node::VarBinding { .. }
1356            | Node::Assignment { .. }
1357            | Node::ReturnStmt { .. }
1358            | Node::FnDecl { .. }
1359            | Node::ThrowStmt { .. }
1360            | Node::BreakStmt
1361            | Node::ContinueStmt => false,
1362            // These compound nodes explicitly produce a value
1363            Node::TryCatch { .. }
1364            | Node::Retry { .. }
1365            | Node::GuardStmt { .. }
1366            | Node::DeadlineBlock { .. }
1367            | Node::MutexBlock { .. } => true,
1368            // All other expressions produce values
1369            _ => true,
1370        }
1371    }
1372}
1373
1374impl Compiler {
1375    /// Compile a function body into a CompiledFunction (for import support).
1376    pub fn compile_fn_body(
1377        &mut self,
1378        params: &[TypedParam],
1379        body: &[SNode],
1380    ) -> Result<CompiledFunction, CompileError> {
1381        let mut fn_compiler = Compiler::new();
1382        fn_compiler.compile_block(body)?;
1383        fn_compiler.chunk.emit(Op::Nil, 0);
1384        fn_compiler.chunk.emit(Op::Return, 0);
1385        Ok(CompiledFunction {
1386            name: String::new(),
1387            params: TypedParam::names(params),
1388            chunk: fn_compiler.chunk,
1389        })
1390    }
1391
1392    /// Compile a match arm body, ensuring it always pushes exactly one value.
1393    fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
1394        if body.is_empty() {
1395            self.chunk.emit(Op::Nil, self.line);
1396        } else {
1397            self.compile_block(body)?;
1398            // If the last statement doesn't produce a value, push nil
1399            if !Self::produces_value(&body.last().unwrap().node) {
1400                self.chunk.emit(Op::Nil, self.line);
1401            }
1402        }
1403        Ok(())
1404    }
1405
1406    /// Emit the binary op instruction for a compound assignment operator.
1407    fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
1408        match op {
1409            "+" => self.chunk.emit(Op::Add, self.line),
1410            "-" => self.chunk.emit(Op::Sub, self.line),
1411            "*" => self.chunk.emit(Op::Mul, self.line),
1412            "/" => self.chunk.emit(Op::Div, self.line),
1413            "%" => self.chunk.emit(Op::Mod, self.line),
1414            _ => {
1415                return Err(CompileError {
1416                    message: format!("Unknown compound operator: {op}"),
1417                    line: self.line,
1418                })
1419            }
1420        }
1421        Ok(())
1422    }
1423
1424    /// Extract the root variable name from a (possibly nested) access expression.
1425    fn root_var_name(&self, node: &SNode) -> Option<String> {
1426        match &node.node {
1427            Node::Identifier(name) => Some(name.clone()),
1428            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
1429                self.root_var_name(object)
1430            }
1431            Node::SubscriptAccess { object, .. } => self.root_var_name(object),
1432            _ => None,
1433        }
1434    }
1435}
1436
1437impl Compiler {
1438    /// Recursively collect all enum type names from the AST.
1439    fn collect_enum_names(nodes: &[SNode], names: &mut std::collections::HashSet<String>) {
1440        for sn in nodes {
1441            match &sn.node {
1442                Node::EnumDecl { name, .. } => {
1443                    names.insert(name.clone());
1444                }
1445                Node::Pipeline { body, .. } => {
1446                    Self::collect_enum_names(body, names);
1447                }
1448                Node::FnDecl { body, .. } => {
1449                    Self::collect_enum_names(body, names);
1450                }
1451                Node::Block(stmts) => {
1452                    Self::collect_enum_names(stmts, names);
1453                }
1454                _ => {}
1455            }
1456        }
1457    }
1458}
1459
1460impl Default for Compiler {
1461    fn default() -> Self {
1462        Self::new()
1463    }
1464}
1465
1466#[cfg(test)]
1467mod tests {
1468    use super::*;
1469    use harn_lexer::Lexer;
1470    use harn_parser::Parser;
1471
1472    fn compile_source(source: &str) -> Chunk {
1473        let mut lexer = Lexer::new(source);
1474        let tokens = lexer.tokenize().unwrap();
1475        let mut parser = Parser::new(tokens);
1476        let program = parser.parse().unwrap();
1477        Compiler::new().compile(&program).unwrap()
1478    }
1479
1480    #[test]
1481    fn test_compile_arithmetic() {
1482        let chunk = compile_source("pipeline test(task) { let x = 2 + 3 }");
1483        assert!(!chunk.code.is_empty());
1484        // Should have constants: 2, 3, "x"
1485        assert!(chunk.constants.contains(&Constant::Int(2)));
1486        assert!(chunk.constants.contains(&Constant::Int(3)));
1487    }
1488
1489    #[test]
1490    fn test_compile_function_call() {
1491        let chunk = compile_source("pipeline test(task) { log(42) }");
1492        let disasm = chunk.disassemble("test");
1493        assert!(disasm.contains("CALL"));
1494    }
1495
1496    #[test]
1497    fn test_compile_if_else() {
1498        let chunk =
1499            compile_source(r#"pipeline test(task) { if true { log("yes") } else { log("no") } }"#);
1500        let disasm = chunk.disassemble("test");
1501        assert!(disasm.contains("JUMP_IF_FALSE"));
1502        assert!(disasm.contains("JUMP"));
1503    }
1504
1505    #[test]
1506    fn test_compile_while() {
1507        let chunk = compile_source("pipeline test(task) { var i = 0\n while i < 5 { i = i + 1 } }");
1508        let disasm = chunk.disassemble("test");
1509        assert!(disasm.contains("JUMP_IF_FALSE"));
1510        // Should have a backward jump
1511        assert!(disasm.contains("JUMP"));
1512    }
1513
1514    #[test]
1515    fn test_compile_closure() {
1516        let chunk = compile_source("pipeline test(task) { let f = { x -> x * 2 } }");
1517        assert!(!chunk.functions.is_empty());
1518        assert_eq!(chunk.functions[0].params, vec!["x"]);
1519    }
1520
1521    #[test]
1522    fn test_compile_list() {
1523        let chunk = compile_source("pipeline test(task) { let a = [1, 2, 3] }");
1524        let disasm = chunk.disassemble("test");
1525        assert!(disasm.contains("BUILD_LIST"));
1526    }
1527
1528    #[test]
1529    fn test_compile_dict() {
1530        let chunk = compile_source(r#"pipeline test(task) { let d = {name: "test"} }"#);
1531        let disasm = chunk.disassemble("test");
1532        assert!(disasm.contains("BUILD_DICT"));
1533    }
1534
1535    #[test]
1536    fn test_disassemble() {
1537        let chunk = compile_source("pipeline test(task) { log(2 + 3) }");
1538        let disasm = chunk.disassemble("test");
1539        // Should be readable
1540        assert!(disasm.contains("CONSTANT"));
1541        assert!(disasm.contains("ADD"));
1542        assert!(disasm.contains("CALL"));
1543    }
1544}