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