Skip to main content

harn_vm/
compiler.rs

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