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