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                let has_spread = elements.iter().any(|e| matches!(&e.node, Node::Spread(_)));
685                if !has_spread {
686                    for el in elements {
687                        self.compile_node(el)?;
688                    }
689                    self.chunk
690                        .emit_u16(Op::BuildList, elements.len() as u16, self.line);
691                } else {
692                    // Build with spreads: accumulate segments into lists and concat
693                    // Start with empty list
694                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
695                    let mut pending = 0u16;
696                    for el in elements {
697                        if let Node::Spread(inner) = &el.node {
698                            // First, build list from pending non-spread elements
699                            if pending > 0 {
700                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
701                                // Concat accumulated + pending segment
702                                self.chunk.emit(Op::Add, self.line);
703                                pending = 0;
704                            }
705                            // Concat with the spread expression (with type check)
706                            self.compile_node(inner)?;
707                            self.chunk.emit(Op::Dup, self.line);
708                            let assert_idx = self
709                                .chunk
710                                .add_constant(Constant::String("__assert_list".into()));
711                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
712                            self.chunk.emit(Op::Swap, self.line);
713                            self.chunk.emit_u8(Op::Call, 1, self.line);
714                            self.chunk.emit(Op::Pop, self.line);
715                            self.chunk.emit(Op::Add, self.line);
716                        } else {
717                            self.compile_node(el)?;
718                            pending += 1;
719                        }
720                    }
721                    if pending > 0 {
722                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
723                        self.chunk.emit(Op::Add, self.line);
724                    }
725                }
726            }
727
728            Node::DictLiteral(entries) => {
729                let has_spread = entries
730                    .iter()
731                    .any(|e| matches!(&e.value.node, Node::Spread(_)));
732                if !has_spread {
733                    for entry in entries {
734                        self.compile_node(&entry.key)?;
735                        self.compile_node(&entry.value)?;
736                    }
737                    self.chunk
738                        .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
739                } else {
740                    // Build with spreads: use empty dict + Add for merging
741                    self.chunk.emit_u16(Op::BuildDict, 0, self.line);
742                    let mut pending = 0u16;
743                    for entry in entries {
744                        if let Node::Spread(inner) = &entry.value.node {
745                            // Flush pending entries
746                            if pending > 0 {
747                                self.chunk.emit_u16(Op::BuildDict, pending, self.line);
748                                self.chunk.emit(Op::Add, self.line);
749                                pending = 0;
750                            }
751                            // Merge spread dict via Add (with type check)
752                            self.compile_node(inner)?;
753                            self.chunk.emit(Op::Dup, self.line);
754                            let assert_idx = self
755                                .chunk
756                                .add_constant(Constant::String("__assert_dict".into()));
757                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
758                            self.chunk.emit(Op::Swap, self.line);
759                            self.chunk.emit_u8(Op::Call, 1, self.line);
760                            self.chunk.emit(Op::Pop, self.line);
761                            self.chunk.emit(Op::Add, self.line);
762                        } else {
763                            self.compile_node(&entry.key)?;
764                            self.compile_node(&entry.value)?;
765                            pending += 1;
766                        }
767                    }
768                    if pending > 0 {
769                        self.chunk.emit_u16(Op::BuildDict, pending, self.line);
770                        self.chunk.emit(Op::Add, self.line);
771                    }
772                }
773            }
774
775            Node::InterpolatedString(segments) => {
776                let mut part_count = 0u16;
777                for seg in segments {
778                    match seg {
779                        StringSegment::Literal(s) => {
780                            let idx = self.chunk.add_constant(Constant::String(s.clone()));
781                            self.chunk.emit_u16(Op::Constant, idx, self.line);
782                            part_count += 1;
783                        }
784                        StringSegment::Expression(expr_str) => {
785                            // Parse and compile the embedded expression
786                            let mut lexer = harn_lexer::Lexer::new(expr_str);
787                            if let Ok(tokens) = lexer.tokenize() {
788                                let mut parser = harn_parser::Parser::new(tokens);
789                                if let Ok(snode) = parser.parse_single_expression() {
790                                    self.compile_node(&snode)?;
791                                    // Convert result to string for concatenation
792                                    let to_str = self
793                                        .chunk
794                                        .add_constant(Constant::String("to_string".into()));
795                                    self.chunk.emit_u16(Op::Constant, to_str, self.line);
796                                    self.chunk.emit(Op::Swap, self.line);
797                                    self.chunk.emit_u8(Op::Call, 1, self.line);
798                                    part_count += 1;
799                                } else {
800                                    // Fallback: treat as literal string
801                                    let idx =
802                                        self.chunk.add_constant(Constant::String(expr_str.clone()));
803                                    self.chunk.emit_u16(Op::Constant, idx, self.line);
804                                    part_count += 1;
805                                }
806                            }
807                        }
808                    }
809                }
810                if part_count > 1 {
811                    self.chunk.emit_u16(Op::Concat, part_count, self.line);
812                }
813            }
814
815            Node::FnDecl {
816                name, params, body, ..
817            } => {
818                // Compile function body into a separate chunk
819                let mut fn_compiler = Compiler::new();
820                fn_compiler.enum_names = self.enum_names.clone();
821                fn_compiler.compile_block(body)?;
822                fn_compiler.chunk.emit(Op::Nil, self.line);
823                fn_compiler.chunk.emit(Op::Return, self.line);
824
825                let func = CompiledFunction {
826                    name: name.clone(),
827                    params: TypedParam::names(params),
828                    chunk: fn_compiler.chunk,
829                };
830                let fn_idx = self.chunk.functions.len();
831                self.chunk.functions.push(func);
832
833                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
834                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
835                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
836            }
837
838            Node::Closure { params, body } => {
839                let mut fn_compiler = Compiler::new();
840                fn_compiler.enum_names = self.enum_names.clone();
841                fn_compiler.compile_block(body)?;
842                // If block didn't end with return, the last value is on the stack
843                fn_compiler.chunk.emit(Op::Return, self.line);
844
845                let func = CompiledFunction {
846                    name: "<closure>".to_string(),
847                    params: TypedParam::names(params),
848                    chunk: fn_compiler.chunk,
849                };
850                let fn_idx = self.chunk.functions.len();
851                self.chunk.functions.push(func);
852
853                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
854            }
855
856            Node::ThrowStmt { value } => {
857                self.compile_node(value)?;
858                self.chunk.emit(Op::Throw, self.line);
859            }
860
861            Node::MatchExpr { value, arms } => {
862                self.compile_node(value)?;
863                let mut end_jumps = Vec::new();
864                for arm in arms {
865                    match &arm.pattern.node {
866                        // Wildcard `_` — always matches
867                        Node::Identifier(name) if name == "_" => {
868                            self.chunk.emit(Op::Pop, self.line); // pop match value
869                            self.compile_match_body(&arm.body)?;
870                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
871                        }
872                        // Enum destructuring: EnumConstruct pattern
873                        Node::EnumConstruct {
874                            enum_name,
875                            variant,
876                            args: pat_args,
877                        } => {
878                            // Check if the match value is this enum variant
879                            self.chunk.emit(Op::Dup, self.line);
880                            let en_idx =
881                                self.chunk.add_constant(Constant::String(enum_name.clone()));
882                            let vn_idx = self.chunk.add_constant(Constant::String(variant.clone()));
883                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
884                            let hi = (vn_idx >> 8) as u8;
885                            let lo = vn_idx as u8;
886                            self.chunk.code.push(hi);
887                            self.chunk.code.push(lo);
888                            self.chunk.lines.push(self.line);
889                            self.chunk.columns.push(self.column);
890                            self.chunk.lines.push(self.line);
891                            self.chunk.columns.push(self.column);
892                            // Stack: [match_value, bool]
893                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
894                            self.chunk.emit(Op::Pop, self.line); // pop bool
895
896                            // Destructure: bind field variables from the enum's fields
897                            // The match value is still on the stack; we need to extract fields
898                            for (i, pat_arg) in pat_args.iter().enumerate() {
899                                if let Node::Identifier(binding_name) = &pat_arg.node {
900                                    // Dup the match value, get .fields, subscript [i]
901                                    self.chunk.emit(Op::Dup, self.line);
902                                    let fields_idx = self
903                                        .chunk
904                                        .add_constant(Constant::String("fields".to_string()));
905                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
906                                    let idx_const =
907                                        self.chunk.add_constant(Constant::Int(i as i64));
908                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
909                                    self.chunk.emit(Op::Subscript, self.line);
910                                    let name_idx = self
911                                        .chunk
912                                        .add_constant(Constant::String(binding_name.clone()));
913                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
914                                }
915                            }
916
917                            self.chunk.emit(Op::Pop, self.line); // pop match value
918                            self.compile_match_body(&arm.body)?;
919                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
920                            self.chunk.patch_jump(skip);
921                            self.chunk.emit(Op::Pop, self.line); // pop bool
922                        }
923                        // Enum variant without args: PropertyAccess(EnumName, Variant)
924                        Node::PropertyAccess { object, property } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
925                        {
926                            let enum_name = if let Node::Identifier(n) = &object.node {
927                                n.clone()
928                            } else {
929                                unreachable!()
930                            };
931                            self.chunk.emit(Op::Dup, self.line);
932                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
933                            let vn_idx =
934                                self.chunk.add_constant(Constant::String(property.clone()));
935                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
936                            let hi = (vn_idx >> 8) as u8;
937                            let lo = vn_idx as u8;
938                            self.chunk.code.push(hi);
939                            self.chunk.code.push(lo);
940                            self.chunk.lines.push(self.line);
941                            self.chunk.columns.push(self.column);
942                            self.chunk.lines.push(self.line);
943                            self.chunk.columns.push(self.column);
944                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
945                            self.chunk.emit(Op::Pop, self.line); // pop bool
946                            self.chunk.emit(Op::Pop, self.line); // pop match value
947                            self.compile_match_body(&arm.body)?;
948                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
949                            self.chunk.patch_jump(skip);
950                            self.chunk.emit(Op::Pop, self.line); // pop bool
951                        }
952                        // Enum destructuring via MethodCall: EnumName.Variant(bindings...)
953                        // Parser produces MethodCall for EnumName.Variant(x) patterns
954                        Node::MethodCall {
955                            object,
956                            method,
957                            args: pat_args,
958                        } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
959                        {
960                            let enum_name = if let Node::Identifier(n) = &object.node {
961                                n.clone()
962                            } else {
963                                unreachable!()
964                            };
965                            // Check if the match value is this enum variant
966                            self.chunk.emit(Op::Dup, self.line);
967                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
968                            let vn_idx = self.chunk.add_constant(Constant::String(method.clone()));
969                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
970                            let hi = (vn_idx >> 8) as u8;
971                            let lo = vn_idx as u8;
972                            self.chunk.code.push(hi);
973                            self.chunk.code.push(lo);
974                            self.chunk.lines.push(self.line);
975                            self.chunk.columns.push(self.column);
976                            self.chunk.lines.push(self.line);
977                            self.chunk.columns.push(self.column);
978                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
979                            self.chunk.emit(Op::Pop, self.line); // pop bool
980
981                            // Destructure: bind field variables
982                            for (i, pat_arg) in pat_args.iter().enumerate() {
983                                if let Node::Identifier(binding_name) = &pat_arg.node {
984                                    self.chunk.emit(Op::Dup, self.line);
985                                    let fields_idx = self
986                                        .chunk
987                                        .add_constant(Constant::String("fields".to_string()));
988                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
989                                    let idx_const =
990                                        self.chunk.add_constant(Constant::Int(i as i64));
991                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
992                                    self.chunk.emit(Op::Subscript, self.line);
993                                    let name_idx = self
994                                        .chunk
995                                        .add_constant(Constant::String(binding_name.clone()));
996                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
997                                }
998                            }
999
1000                            self.chunk.emit(Op::Pop, self.line); // pop match value
1001                            self.compile_match_body(&arm.body)?;
1002                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1003                            self.chunk.patch_jump(skip);
1004                            self.chunk.emit(Op::Pop, self.line); // pop bool
1005                        }
1006                        // Binding pattern: bare identifier (not a literal)
1007                        Node::Identifier(name) => {
1008                            // Bind the match value to this name, always matches
1009                            self.chunk.emit(Op::Dup, self.line); // dup for binding
1010                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1011                            self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1012                            self.chunk.emit(Op::Pop, self.line); // pop match value
1013                            self.compile_match_body(&arm.body)?;
1014                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1015                        }
1016                        // Dict pattern: {key: literal, key: binding, ...}
1017                        Node::DictLiteral(entries)
1018                            if entries
1019                                .iter()
1020                                .all(|e| matches!(&e.key.node, Node::StringLiteral(_))) =>
1021                        {
1022                            // Check type is dict: dup, call type_of, compare "dict"
1023                            self.chunk.emit(Op::Dup, self.line);
1024                            let typeof_idx =
1025                                self.chunk.add_constant(Constant::String("type_of".into()));
1026                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1027                            self.chunk.emit(Op::Swap, self.line);
1028                            self.chunk.emit_u8(Op::Call, 1, self.line);
1029                            let dict_str = self.chunk.add_constant(Constant::String("dict".into()));
1030                            self.chunk.emit_u16(Op::Constant, dict_str, self.line);
1031                            self.chunk.emit(Op::Equal, self.line);
1032                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1033                            self.chunk.emit(Op::Pop, self.line); // pop bool
1034
1035                            // Check literal constraints
1036                            let mut constraint_skips = Vec::new();
1037                            let mut bindings = Vec::new();
1038                            for entry in entries {
1039                                if let Node::StringLiteral(key) = &entry.key.node {
1040                                    match &entry.value.node {
1041                                        // Literal value → constraint: dict[key] == value
1042                                        Node::StringLiteral(_)
1043                                        | Node::IntLiteral(_)
1044                                        | Node::FloatLiteral(_)
1045                                        | Node::BoolLiteral(_)
1046                                        | Node::NilLiteral => {
1047                                            self.chunk.emit(Op::Dup, self.line);
1048                                            let key_idx = self
1049                                                .chunk
1050                                                .add_constant(Constant::String(key.clone()));
1051                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1052                                            self.chunk.emit(Op::Subscript, self.line);
1053                                            self.compile_node(&entry.value)?;
1054                                            self.chunk.emit(Op::Equal, self.line);
1055                                            let skip =
1056                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1057                                            self.chunk.emit(Op::Pop, self.line); // pop bool
1058                                            constraint_skips.push(skip);
1059                                        }
1060                                        // Identifier → binding: bind dict[key] to variable
1061                                        Node::Identifier(binding) => {
1062                                            bindings.push((key.clone(), binding.clone()));
1063                                        }
1064                                        _ => {
1065                                            // Complex expression constraint
1066                                            self.chunk.emit(Op::Dup, self.line);
1067                                            let key_idx = self
1068                                                .chunk
1069                                                .add_constant(Constant::String(key.clone()));
1070                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1071                                            self.chunk.emit(Op::Subscript, self.line);
1072                                            self.compile_node(&entry.value)?;
1073                                            self.chunk.emit(Op::Equal, self.line);
1074                                            let skip =
1075                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1076                                            self.chunk.emit(Op::Pop, self.line);
1077                                            constraint_skips.push(skip);
1078                                        }
1079                                    }
1080                                }
1081                            }
1082
1083                            // All constraints passed — emit bindings
1084                            for (key, binding) in &bindings {
1085                                self.chunk.emit(Op::Dup, self.line);
1086                                let key_idx =
1087                                    self.chunk.add_constant(Constant::String(key.clone()));
1088                                self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1089                                self.chunk.emit(Op::Subscript, self.line);
1090                                let name_idx =
1091                                    self.chunk.add_constant(Constant::String(binding.clone()));
1092                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1093                            }
1094
1095                            self.chunk.emit(Op::Pop, self.line); // pop match value
1096                            self.compile_match_body(&arm.body)?;
1097                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1098
1099                            // All failures jump here: pop the false bool, leave match_value
1100                            let fail_target = self.chunk.code.len();
1101                            self.chunk.emit(Op::Pop, self.line); // pop bool
1102                                                                 // Patch all failure jumps to the shared cleanup point
1103                            for skip in constraint_skips {
1104                                self.chunk.patch_jump_to(skip, fail_target);
1105                            }
1106                            self.chunk.patch_jump_to(skip_type, fail_target);
1107                        }
1108                        // List pattern: [literal, binding, ...]
1109                        Node::ListLiteral(elements) => {
1110                            // Check type is list: dup, call type_of, compare "list"
1111                            self.chunk.emit(Op::Dup, self.line);
1112                            let typeof_idx =
1113                                self.chunk.add_constant(Constant::String("type_of".into()));
1114                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1115                            self.chunk.emit(Op::Swap, self.line);
1116                            self.chunk.emit_u8(Op::Call, 1, self.line);
1117                            let list_str = self.chunk.add_constant(Constant::String("list".into()));
1118                            self.chunk.emit_u16(Op::Constant, list_str, self.line);
1119                            self.chunk.emit(Op::Equal, self.line);
1120                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1121                            self.chunk.emit(Op::Pop, self.line); // pop bool
1122
1123                            // Check length: dup, call len, compare >= elements.len()
1124                            self.chunk.emit(Op::Dup, self.line);
1125                            let len_idx = self.chunk.add_constant(Constant::String("len".into()));
1126                            self.chunk.emit_u16(Op::Constant, len_idx, self.line);
1127                            self.chunk.emit(Op::Swap, self.line);
1128                            self.chunk.emit_u8(Op::Call, 1, self.line);
1129                            let count = self
1130                                .chunk
1131                                .add_constant(Constant::Int(elements.len() as i64));
1132                            self.chunk.emit_u16(Op::Constant, count, self.line);
1133                            self.chunk.emit(Op::GreaterEqual, self.line);
1134                            let skip_len = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1135                            self.chunk.emit(Op::Pop, self.line); // pop bool
1136
1137                            // Check literal constraints and collect bindings
1138                            let mut constraint_skips = Vec::new();
1139                            let mut bindings = Vec::new();
1140                            for (i, elem) in elements.iter().enumerate() {
1141                                match &elem.node {
1142                                    Node::Identifier(name) if name != "_" => {
1143                                        bindings.push((i, name.clone()));
1144                                    }
1145                                    Node::Identifier(_) => {} // wildcard _
1146                                    // Literal constraint
1147                                    _ => {
1148                                        self.chunk.emit(Op::Dup, self.line);
1149                                        let idx_const =
1150                                            self.chunk.add_constant(Constant::Int(i as i64));
1151                                        self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1152                                        self.chunk.emit(Op::Subscript, self.line);
1153                                        self.compile_node(elem)?;
1154                                        self.chunk.emit(Op::Equal, self.line);
1155                                        let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1156                                        self.chunk.emit(Op::Pop, self.line);
1157                                        constraint_skips.push(skip);
1158                                    }
1159                                }
1160                            }
1161
1162                            // Emit bindings
1163                            for (i, name) in &bindings {
1164                                self.chunk.emit(Op::Dup, self.line);
1165                                let idx_const = self.chunk.add_constant(Constant::Int(*i as i64));
1166                                self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1167                                self.chunk.emit(Op::Subscript, self.line);
1168                                let name_idx =
1169                                    self.chunk.add_constant(Constant::String(name.clone()));
1170                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1171                            }
1172
1173                            self.chunk.emit(Op::Pop, self.line); // pop match value
1174                            self.compile_match_body(&arm.body)?;
1175                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1176
1177                            // All failures jump here: pop the false bool
1178                            let fail_target = self.chunk.code.len();
1179                            self.chunk.emit(Op::Pop, self.line); // pop bool
1180                            for skip in constraint_skips {
1181                                self.chunk.patch_jump_to(skip, fail_target);
1182                            }
1183                            self.chunk.patch_jump_to(skip_len, fail_target);
1184                            self.chunk.patch_jump_to(skip_type, fail_target);
1185                        }
1186                        // Literal/expression pattern — compare with Equal
1187                        _ => {
1188                            self.chunk.emit(Op::Dup, self.line);
1189                            self.compile_node(&arm.pattern)?;
1190                            self.chunk.emit(Op::Equal, self.line);
1191                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1192                            self.chunk.emit(Op::Pop, self.line); // pop bool
1193                            self.chunk.emit(Op::Pop, self.line); // pop match value
1194                            self.compile_match_body(&arm.body)?;
1195                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1196                            self.chunk.patch_jump(skip);
1197                            self.chunk.emit(Op::Pop, self.line); // pop bool
1198                        }
1199                    }
1200                }
1201                // No match — pop value, push nil
1202                self.chunk.emit(Op::Pop, self.line);
1203                self.chunk.emit(Op::Nil, self.line);
1204                for j in end_jumps {
1205                    self.chunk.patch_jump(j);
1206                }
1207            }
1208
1209            Node::RangeExpr {
1210                start,
1211                end,
1212                inclusive,
1213            } => {
1214                // Compile as __range__(start, end, inclusive_bool) builtin call
1215                let name_idx = self
1216                    .chunk
1217                    .add_constant(Constant::String("__range__".to_string()));
1218                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
1219                self.compile_node(start)?;
1220                self.compile_node(end)?;
1221                if *inclusive {
1222                    self.chunk.emit(Op::True, self.line);
1223                } else {
1224                    self.chunk.emit(Op::False, self.line);
1225                }
1226                self.chunk.emit_u8(Op::Call, 3, self.line);
1227            }
1228
1229            Node::GuardStmt {
1230                condition,
1231                else_body,
1232            } => {
1233                // guard condition else { body }
1234                // Compile condition; if truthy, skip else_body
1235                self.compile_node(condition)?;
1236                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
1237                self.chunk.emit(Op::Pop, self.line); // pop condition
1238                                                     // Compile else_body
1239                self.compile_block(else_body)?;
1240                // Pop result of else_body (guard is a statement, not expression)
1241                if !else_body.is_empty() && Self::produces_value(&else_body.last().unwrap().node) {
1242                    self.chunk.emit(Op::Pop, self.line);
1243                }
1244                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1245                self.chunk.patch_jump(skip_jump);
1246                self.chunk.emit(Op::Pop, self.line); // pop condition
1247                self.chunk.patch_jump(end_jump);
1248                self.chunk.emit(Op::Nil, self.line);
1249            }
1250
1251            Node::Block(stmts) => {
1252                if stmts.is_empty() {
1253                    self.chunk.emit(Op::Nil, self.line);
1254                } else {
1255                    self.compile_block(stmts)?;
1256                }
1257            }
1258
1259            Node::DeadlineBlock { duration, body } => {
1260                self.compile_node(duration)?;
1261                self.chunk.emit(Op::DeadlineSetup, self.line);
1262                if body.is_empty() {
1263                    self.chunk.emit(Op::Nil, self.line);
1264                } else {
1265                    self.compile_block(body)?;
1266                }
1267                self.chunk.emit(Op::DeadlineEnd, self.line);
1268            }
1269
1270            Node::MutexBlock { body } => {
1271                // v1: single-threaded, just compile the body
1272                if body.is_empty() {
1273                    self.chunk.emit(Op::Nil, self.line);
1274                } else {
1275                    // Compile body, but pop intermediate values and push nil at the end.
1276                    // The body typically contains statements (assignments) that don't produce values.
1277                    for sn in body {
1278                        self.compile_node(sn)?;
1279                        if Self::produces_value(&sn.node) {
1280                            self.chunk.emit(Op::Pop, self.line);
1281                        }
1282                    }
1283                    self.chunk.emit(Op::Nil, self.line);
1284                }
1285            }
1286
1287            Node::YieldExpr { .. } => {
1288                // v1: yield is host-integration only, emit nil
1289                self.chunk.emit(Op::Nil, self.line);
1290            }
1291
1292            Node::AskExpr { fields } => {
1293                // Compile as a dict literal and call llm_call builtin
1294                // For v1, just build the dict (llm_call requires async)
1295                for entry in fields {
1296                    self.compile_node(&entry.key)?;
1297                    self.compile_node(&entry.value)?;
1298                }
1299                self.chunk
1300                    .emit_u16(Op::BuildDict, fields.len() as u16, self.line);
1301            }
1302
1303            Node::EnumConstruct {
1304                enum_name,
1305                variant,
1306                args,
1307            } => {
1308                // Push field values onto the stack, then BuildEnum
1309                for arg in args {
1310                    self.compile_node(arg)?;
1311                }
1312                let enum_idx = self.chunk.add_constant(Constant::String(enum_name.clone()));
1313                let var_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1314                // BuildEnum: enum_name_idx, variant_idx, field_count
1315                self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
1316                let hi = (var_idx >> 8) as u8;
1317                let lo = var_idx as u8;
1318                self.chunk.code.push(hi);
1319                self.chunk.code.push(lo);
1320                self.chunk.lines.push(self.line);
1321                self.chunk.columns.push(self.column);
1322                self.chunk.lines.push(self.line);
1323                self.chunk.columns.push(self.column);
1324                let fc = args.len() as u16;
1325                let fhi = (fc >> 8) as u8;
1326                let flo = fc as u8;
1327                self.chunk.code.push(fhi);
1328                self.chunk.code.push(flo);
1329                self.chunk.lines.push(self.line);
1330                self.chunk.columns.push(self.column);
1331                self.chunk.lines.push(self.line);
1332                self.chunk.columns.push(self.column);
1333            }
1334
1335            Node::StructConstruct {
1336                struct_name,
1337                fields,
1338            } => {
1339                // Build as a dict with a __struct__ key for metadata
1340                let struct_key = self
1341                    .chunk
1342                    .add_constant(Constant::String("__struct__".to_string()));
1343                let struct_val = self
1344                    .chunk
1345                    .add_constant(Constant::String(struct_name.clone()));
1346                self.chunk.emit_u16(Op::Constant, struct_key, self.line);
1347                self.chunk.emit_u16(Op::Constant, struct_val, self.line);
1348
1349                for entry in fields {
1350                    self.compile_node(&entry.key)?;
1351                    self.compile_node(&entry.value)?;
1352                }
1353                self.chunk
1354                    .emit_u16(Op::BuildDict, (fields.len() + 1) as u16, self.line);
1355            }
1356
1357            Node::ImportDecl { path } => {
1358                let idx = self.chunk.add_constant(Constant::String(path.clone()));
1359                self.chunk.emit_u16(Op::Import, idx, self.line);
1360            }
1361
1362            Node::SelectiveImport { names, path } => {
1363                let path_idx = self.chunk.add_constant(Constant::String(path.clone()));
1364                let names_str = names.join(",");
1365                let names_idx = self.chunk.add_constant(Constant::String(names_str));
1366                self.chunk
1367                    .emit_u16(Op::SelectiveImport, path_idx, self.line);
1368                let hi = (names_idx >> 8) as u8;
1369                let lo = names_idx as u8;
1370                self.chunk.code.push(hi);
1371                self.chunk.code.push(lo);
1372                self.chunk.lines.push(self.line);
1373                self.chunk.columns.push(self.column);
1374                self.chunk.lines.push(self.line);
1375                self.chunk.columns.push(self.column);
1376            }
1377
1378            // Declarations that only register metadata (no runtime effect needed for v1)
1379            Node::Pipeline { .. }
1380            | Node::OverrideDecl { .. }
1381            | Node::TypeDecl { .. }
1382            | Node::EnumDecl { .. }
1383            | Node::StructDecl { .. }
1384            | Node::InterfaceDecl { .. } => {
1385                self.chunk.emit(Op::Nil, self.line);
1386            }
1387
1388            Node::TryCatch {
1389                body,
1390                error_var,
1391                error_type,
1392                catch_body,
1393            } => {
1394                // Extract the type name for typed catch (e.g., "AppError")
1395                let type_name = error_type.as_ref().and_then(|te| {
1396                    // TypeExpr is a Named(String) for simple type names
1397                    if let harn_parser::TypeExpr::Named(name) = te {
1398                        Some(name.clone())
1399                    } else {
1400                        None
1401                    }
1402                });
1403
1404                // Store the error type name as a constant (or empty string for untyped)
1405                let type_name_idx = if let Some(ref tn) = type_name {
1406                    self.chunk.add_constant(Constant::String(tn.clone()))
1407                } else {
1408                    self.chunk.add_constant(Constant::String(String::new()))
1409                };
1410
1411                // 1. Emit TryCatchSetup with placeholder offset to catch handler
1412                self.handler_depth += 1;
1413                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1414                // Emit the type name index as extra u16 after the jump offset
1415                let hi = (type_name_idx >> 8) as u8;
1416                let lo = type_name_idx as u8;
1417                self.chunk.code.push(hi);
1418                self.chunk.code.push(lo);
1419                self.chunk.lines.push(self.line);
1420                self.chunk.columns.push(self.column);
1421                self.chunk.lines.push(self.line);
1422                self.chunk.columns.push(self.column);
1423
1424                // 2. Compile try body
1425                if body.is_empty() {
1426                    self.chunk.emit(Op::Nil, self.line);
1427                } else {
1428                    self.compile_block(body)?;
1429                    // If last statement doesn't produce a value, push nil
1430                    if !Self::produces_value(&body.last().unwrap().node) {
1431                        self.chunk.emit(Op::Nil, self.line);
1432                    }
1433                }
1434
1435                // 3. Emit PopHandler (successful try body completion)
1436                self.handler_depth -= 1;
1437                self.chunk.emit(Op::PopHandler, self.line);
1438
1439                // 4. Emit Jump past catch body
1440                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1441
1442                // 5. Patch the catch offset to point here
1443                self.chunk.patch_jump(catch_jump);
1444
1445                // 6. Error value is on the stack from the handler.
1446                //    If error_var exists, bind it; otherwise pop the error value.
1447                if let Some(var_name) = error_var {
1448                    let idx = self.chunk.add_constant(Constant::String(var_name.clone()));
1449                    self.chunk.emit_u16(Op::DefLet, idx, self.line);
1450                } else {
1451                    self.chunk.emit(Op::Pop, self.line);
1452                }
1453
1454                // 7. Compile catch body
1455                if catch_body.is_empty() {
1456                    self.chunk.emit(Op::Nil, self.line);
1457                } else {
1458                    self.compile_block(catch_body)?;
1459                    if !Self::produces_value(&catch_body.last().unwrap().node) {
1460                        self.chunk.emit(Op::Nil, self.line);
1461                    }
1462                }
1463
1464                // 8. Patch the end jump
1465                self.chunk.patch_jump(end_jump);
1466            }
1467
1468            Node::Retry { count, body } => {
1469                // Compile count expression into a mutable counter variable
1470                self.compile_node(count)?;
1471                let counter_name = "__retry_counter__";
1472                let counter_idx = self
1473                    .chunk
1474                    .add_constant(Constant::String(counter_name.to_string()));
1475                self.chunk.emit_u16(Op::DefVar, counter_idx, self.line);
1476
1477                // Also store the last error for re-throwing
1478                self.chunk.emit(Op::Nil, self.line);
1479                let err_name = "__retry_last_error__";
1480                let err_idx = self
1481                    .chunk
1482                    .add_constant(Constant::String(err_name.to_string()));
1483                self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
1484
1485                // Loop start
1486                let loop_start = self.chunk.current_offset();
1487
1488                // Set up try/catch (untyped - empty type name)
1489                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1490                // Emit empty type name for untyped catch
1491                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1492                let hi = (empty_type >> 8) as u8;
1493                let lo = empty_type as u8;
1494                self.chunk.code.push(hi);
1495                self.chunk.code.push(lo);
1496                self.chunk.lines.push(self.line);
1497                self.chunk.columns.push(self.column);
1498                self.chunk.lines.push(self.line);
1499                self.chunk.columns.push(self.column);
1500
1501                // Compile body
1502                self.compile_block(body)?;
1503
1504                // Success: pop handler, jump to end
1505                self.chunk.emit(Op::PopHandler, self.line);
1506                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1507
1508                // Catch handler
1509                self.chunk.patch_jump(catch_jump);
1510                // Save the error value for potential re-throw
1511                self.chunk.emit(Op::Dup, self.line);
1512                self.chunk.emit_u16(Op::SetVar, err_idx, self.line);
1513                // Pop the error value
1514                self.chunk.emit(Op::Pop, self.line);
1515
1516                // Decrement counter
1517                self.chunk.emit_u16(Op::GetVar, counter_idx, self.line);
1518                let one_idx = self.chunk.add_constant(Constant::Int(1));
1519                self.chunk.emit_u16(Op::Constant, one_idx, self.line);
1520                self.chunk.emit(Op::Sub, self.line);
1521                self.chunk.emit(Op::Dup, self.line);
1522                self.chunk.emit_u16(Op::SetVar, counter_idx, self.line);
1523
1524                // If counter > 0, jump to loop start
1525                let zero_idx = self.chunk.add_constant(Constant::Int(0));
1526                self.chunk.emit_u16(Op::Constant, zero_idx, self.line);
1527                self.chunk.emit(Op::Greater, self.line);
1528                let retry_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1529                self.chunk.emit(Op::Pop, self.line); // pop condition
1530                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
1531
1532                // No more retries — re-throw the last error
1533                self.chunk.patch_jump(retry_jump);
1534                self.chunk.emit(Op::Pop, self.line); // pop condition
1535                self.chunk.emit_u16(Op::GetVar, err_idx, self.line);
1536                self.chunk.emit(Op::Throw, self.line);
1537
1538                self.chunk.patch_jump(end_jump);
1539                // Push nil as the result of a successful retry block
1540                self.chunk.emit(Op::Nil, self.line);
1541            }
1542
1543            Node::Parallel {
1544                count,
1545                variable,
1546                body,
1547            } => {
1548                self.compile_node(count)?;
1549                let mut fn_compiler = Compiler::new();
1550                fn_compiler.enum_names = self.enum_names.clone();
1551                fn_compiler.compile_block(body)?;
1552                fn_compiler.chunk.emit(Op::Return, self.line);
1553                let params = vec![variable.clone().unwrap_or_else(|| "__i__".to_string())];
1554                let func = CompiledFunction {
1555                    name: "<parallel>".to_string(),
1556                    params,
1557                    chunk: fn_compiler.chunk,
1558                };
1559                let fn_idx = self.chunk.functions.len();
1560                self.chunk.functions.push(func);
1561                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1562                self.chunk.emit(Op::Parallel, self.line);
1563            }
1564
1565            Node::ParallelMap {
1566                list,
1567                variable,
1568                body,
1569            } => {
1570                self.compile_node(list)?;
1571                let mut fn_compiler = Compiler::new();
1572                fn_compiler.enum_names = self.enum_names.clone();
1573                fn_compiler.compile_block(body)?;
1574                fn_compiler.chunk.emit(Op::Return, self.line);
1575                let func = CompiledFunction {
1576                    name: "<parallel_map>".to_string(),
1577                    params: vec![variable.clone()],
1578                    chunk: fn_compiler.chunk,
1579                };
1580                let fn_idx = self.chunk.functions.len();
1581                self.chunk.functions.push(func);
1582                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1583                self.chunk.emit(Op::ParallelMap, self.line);
1584            }
1585
1586            Node::SpawnExpr { body } => {
1587                let mut fn_compiler = Compiler::new();
1588                fn_compiler.enum_names = self.enum_names.clone();
1589                fn_compiler.compile_block(body)?;
1590                fn_compiler.chunk.emit(Op::Return, self.line);
1591                let func = CompiledFunction {
1592                    name: "<spawn>".to_string(),
1593                    params: vec![],
1594                    chunk: fn_compiler.chunk,
1595                };
1596                let fn_idx = self.chunk.functions.len();
1597                self.chunk.functions.push(func);
1598                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1599                self.chunk.emit(Op::Spawn, self.line);
1600            }
1601            Node::Spread(_) => {
1602                return Err(CompileError {
1603                    message: "spread (...) can only be used inside list or dict literals".into(),
1604                    line: self.line,
1605                });
1606            }
1607        }
1608        Ok(())
1609    }
1610
1611    /// Compile a destructuring binding pattern.
1612    /// Expects the RHS value to already be on the stack.
1613    /// After this, the value is consumed (popped) and each binding is defined.
1614    fn compile_destructuring(
1615        &mut self,
1616        pattern: &BindingPattern,
1617        is_mutable: bool,
1618    ) -> Result<(), CompileError> {
1619        let def_op = if is_mutable { Op::DefVar } else { Op::DefLet };
1620        match pattern {
1621            BindingPattern::Identifier(name) => {
1622                // Simple case: just define the variable
1623                let idx = self.chunk.add_constant(Constant::String(name.clone()));
1624                self.chunk.emit_u16(def_op, idx, self.line);
1625            }
1626            BindingPattern::Dict(fields) => {
1627                // Stack has the dict value.
1628                // Emit runtime type check: __assert_dict(value)
1629                self.chunk.emit(Op::Dup, self.line);
1630                let assert_idx = self
1631                    .chunk
1632                    .add_constant(Constant::String("__assert_dict".into()));
1633                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1634                self.chunk.emit(Op::Swap, self.line);
1635                self.chunk.emit_u8(Op::Call, 1, self.line);
1636                self.chunk.emit(Op::Pop, self.line); // discard nil result
1637
1638                // For each non-rest field: dup dict, push key string, subscript, define var.
1639                // For rest field: dup dict, call __dict_rest builtin.
1640                let non_rest: Vec<_> = fields.iter().filter(|f| !f.is_rest).collect();
1641                let rest_field = fields.iter().find(|f| f.is_rest);
1642
1643                for field in &non_rest {
1644                    self.chunk.emit(Op::Dup, self.line);
1645                    let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
1646                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1647                    self.chunk.emit(Op::Subscript, self.line);
1648                    let binding_name = field.alias.as_deref().unwrap_or(&field.key);
1649                    let name_idx = self
1650                        .chunk
1651                        .add_constant(Constant::String(binding_name.to_string()));
1652                    self.chunk.emit_u16(def_op, name_idx, self.line);
1653                }
1654
1655                if let Some(rest) = rest_field {
1656                    // Call the __dict_rest builtin: __dict_rest(dict, [keys_to_exclude])
1657                    // Push function name
1658                    let fn_idx = self
1659                        .chunk
1660                        .add_constant(Constant::String("__dict_rest".into()));
1661                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
1662                    // Swap so dict is above function name: [fn, dict]
1663                    self.chunk.emit(Op::Swap, self.line);
1664                    // Build the exclusion keys list
1665                    for field in &non_rest {
1666                        let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
1667                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1668                    }
1669                    self.chunk
1670                        .emit_u16(Op::BuildList, non_rest.len() as u16, self.line);
1671                    // Call __dict_rest(dict, keys_list) — 2 args
1672                    self.chunk.emit_u8(Op::Call, 2, self.line);
1673                    let rest_name = &rest.key;
1674                    let rest_idx = self.chunk.add_constant(Constant::String(rest_name.clone()));
1675                    self.chunk.emit_u16(def_op, rest_idx, self.line);
1676                } else {
1677                    // Pop the source dict
1678                    self.chunk.emit(Op::Pop, self.line);
1679                }
1680            }
1681            BindingPattern::List(elements) => {
1682                // Stack has the list value.
1683                // Emit runtime type check: __assert_list(value)
1684                self.chunk.emit(Op::Dup, self.line);
1685                let assert_idx = self
1686                    .chunk
1687                    .add_constant(Constant::String("__assert_list".into()));
1688                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1689                self.chunk.emit(Op::Swap, self.line);
1690                self.chunk.emit_u8(Op::Call, 1, self.line);
1691                self.chunk.emit(Op::Pop, self.line); // discard nil result
1692
1693                let non_rest: Vec<_> = elements.iter().filter(|e| !e.is_rest).collect();
1694                let rest_elem = elements.iter().find(|e| e.is_rest);
1695
1696                for (i, elem) in non_rest.iter().enumerate() {
1697                    self.chunk.emit(Op::Dup, self.line);
1698                    let idx_const = self.chunk.add_constant(Constant::Int(i as i64));
1699                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1700                    self.chunk.emit(Op::Subscript, self.line);
1701                    let name_idx = self.chunk.add_constant(Constant::String(elem.name.clone()));
1702                    self.chunk.emit_u16(def_op, name_idx, self.line);
1703                }
1704
1705                if let Some(rest) = rest_elem {
1706                    // Slice the list from index non_rest.len() to end: list[n..]
1707                    // Slice op takes: object, start, end on stack
1708                    // self.chunk.emit(Op::Dup, self.line); -- list is still on stack
1709                    let start_idx = self
1710                        .chunk
1711                        .add_constant(Constant::Int(non_rest.len() as i64));
1712                    self.chunk.emit_u16(Op::Constant, start_idx, self.line);
1713                    self.chunk.emit(Op::Nil, self.line); // end = nil (to end)
1714                    self.chunk.emit(Op::Slice, self.line);
1715                    let rest_name_idx =
1716                        self.chunk.add_constant(Constant::String(rest.name.clone()));
1717                    self.chunk.emit_u16(def_op, rest_name_idx, self.line);
1718                } else {
1719                    // Pop the source list
1720                    self.chunk.emit(Op::Pop, self.line);
1721                }
1722            }
1723        }
1724        Ok(())
1725    }
1726
1727    /// Check if a node produces a value on the stack that needs to be popped.
1728    fn produces_value(node: &Node) -> bool {
1729        match node {
1730            // These nodes do NOT produce a value on the stack
1731            Node::LetBinding { .. }
1732            | Node::VarBinding { .. }
1733            | Node::Assignment { .. }
1734            | Node::ReturnStmt { .. }
1735            | Node::FnDecl { .. }
1736            | Node::ThrowStmt { .. }
1737            | Node::BreakStmt
1738            | Node::ContinueStmt => false,
1739            // These compound nodes explicitly produce a value
1740            Node::TryCatch { .. }
1741            | Node::Retry { .. }
1742            | Node::GuardStmt { .. }
1743            | Node::DeadlineBlock { .. }
1744            | Node::MutexBlock { .. }
1745            | Node::Spread(_) => true,
1746            // All other expressions produce values
1747            _ => true,
1748        }
1749    }
1750}
1751
1752impl Compiler {
1753    /// Compile a function body into a CompiledFunction (for import support).
1754    pub fn compile_fn_body(
1755        &mut self,
1756        params: &[TypedParam],
1757        body: &[SNode],
1758    ) -> Result<CompiledFunction, CompileError> {
1759        let mut fn_compiler = Compiler::new();
1760        fn_compiler.compile_block(body)?;
1761        fn_compiler.chunk.emit(Op::Nil, 0);
1762        fn_compiler.chunk.emit(Op::Return, 0);
1763        Ok(CompiledFunction {
1764            name: String::new(),
1765            params: TypedParam::names(params),
1766            chunk: fn_compiler.chunk,
1767        })
1768    }
1769
1770    /// Compile a match arm body, ensuring it always pushes exactly one value.
1771    fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
1772        if body.is_empty() {
1773            self.chunk.emit(Op::Nil, self.line);
1774        } else {
1775            self.compile_block(body)?;
1776            // If the last statement doesn't produce a value, push nil
1777            if !Self::produces_value(&body.last().unwrap().node) {
1778                self.chunk.emit(Op::Nil, self.line);
1779            }
1780        }
1781        Ok(())
1782    }
1783
1784    /// Emit the binary op instruction for a compound assignment operator.
1785    fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
1786        match op {
1787            "+" => self.chunk.emit(Op::Add, self.line),
1788            "-" => self.chunk.emit(Op::Sub, self.line),
1789            "*" => self.chunk.emit(Op::Mul, self.line),
1790            "/" => self.chunk.emit(Op::Div, self.line),
1791            "%" => self.chunk.emit(Op::Mod, self.line),
1792            _ => {
1793                return Err(CompileError {
1794                    message: format!("Unknown compound operator: {op}"),
1795                    line: self.line,
1796                })
1797            }
1798        }
1799        Ok(())
1800    }
1801
1802    /// Extract the root variable name from a (possibly nested) access expression.
1803    fn root_var_name(&self, node: &SNode) -> Option<String> {
1804        match &node.node {
1805            Node::Identifier(name) => Some(name.clone()),
1806            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
1807                self.root_var_name(object)
1808            }
1809            Node::SubscriptAccess { object, .. } => self.root_var_name(object),
1810            _ => None,
1811        }
1812    }
1813}
1814
1815impl Compiler {
1816    /// Recursively collect all enum type names from the AST.
1817    fn collect_enum_names(nodes: &[SNode], names: &mut std::collections::HashSet<String>) {
1818        for sn in nodes {
1819            match &sn.node {
1820                Node::EnumDecl { name, .. } => {
1821                    names.insert(name.clone());
1822                }
1823                Node::Pipeline { body, .. } => {
1824                    Self::collect_enum_names(body, names);
1825                }
1826                Node::FnDecl { body, .. } => {
1827                    Self::collect_enum_names(body, names);
1828                }
1829                Node::Block(stmts) => {
1830                    Self::collect_enum_names(stmts, names);
1831                }
1832                _ => {}
1833            }
1834        }
1835    }
1836}
1837
1838impl Default for Compiler {
1839    fn default() -> Self {
1840        Self::new()
1841    }
1842}
1843
1844/// Check if an AST node contains `_` identifier (pipe placeholder).
1845fn contains_pipe_placeholder(node: &SNode) -> bool {
1846    match &node.node {
1847        Node::Identifier(name) if name == "_" => true,
1848        Node::FunctionCall { args, .. } => args.iter().any(contains_pipe_placeholder),
1849        Node::MethodCall { object, args, .. } => {
1850            contains_pipe_placeholder(object) || args.iter().any(contains_pipe_placeholder)
1851        }
1852        Node::BinaryOp { left, right, .. } => {
1853            contains_pipe_placeholder(left) || contains_pipe_placeholder(right)
1854        }
1855        Node::UnaryOp { operand, .. } => contains_pipe_placeholder(operand),
1856        Node::ListLiteral(items) => items.iter().any(contains_pipe_placeholder),
1857        Node::PropertyAccess { object, .. } => contains_pipe_placeholder(object),
1858        Node::SubscriptAccess { object, index } => {
1859            contains_pipe_placeholder(object) || contains_pipe_placeholder(index)
1860        }
1861        _ => false,
1862    }
1863}
1864
1865/// Replace all `_` identifiers with `__pipe` in an AST node (for pipe placeholder desugaring).
1866fn replace_pipe_placeholder(node: &SNode) -> SNode {
1867    let new_node = match &node.node {
1868        Node::Identifier(name) if name == "_" => Node::Identifier("__pipe".into()),
1869        Node::FunctionCall { name, args } => Node::FunctionCall {
1870            name: name.clone(),
1871            args: args.iter().map(replace_pipe_placeholder).collect(),
1872        },
1873        Node::MethodCall {
1874            object,
1875            method,
1876            args,
1877        } => Node::MethodCall {
1878            object: Box::new(replace_pipe_placeholder(object)),
1879            method: method.clone(),
1880            args: args.iter().map(replace_pipe_placeholder).collect(),
1881        },
1882        Node::BinaryOp { op, left, right } => Node::BinaryOp {
1883            op: op.clone(),
1884            left: Box::new(replace_pipe_placeholder(left)),
1885            right: Box::new(replace_pipe_placeholder(right)),
1886        },
1887        Node::UnaryOp { op, operand } => Node::UnaryOp {
1888            op: op.clone(),
1889            operand: Box::new(replace_pipe_placeholder(operand)),
1890        },
1891        Node::ListLiteral(items) => {
1892            Node::ListLiteral(items.iter().map(replace_pipe_placeholder).collect())
1893        }
1894        Node::PropertyAccess { object, property } => Node::PropertyAccess {
1895            object: Box::new(replace_pipe_placeholder(object)),
1896            property: property.clone(),
1897        },
1898        Node::SubscriptAccess { object, index } => Node::SubscriptAccess {
1899            object: Box::new(replace_pipe_placeholder(object)),
1900            index: Box::new(replace_pipe_placeholder(index)),
1901        },
1902        _ => return node.clone(),
1903    };
1904    SNode::new(new_node, node.span)
1905}
1906
1907#[cfg(test)]
1908mod tests {
1909    use super::*;
1910    use harn_lexer::Lexer;
1911    use harn_parser::Parser;
1912
1913    fn compile_source(source: &str) -> Chunk {
1914        let mut lexer = Lexer::new(source);
1915        let tokens = lexer.tokenize().unwrap();
1916        let mut parser = Parser::new(tokens);
1917        let program = parser.parse().unwrap();
1918        Compiler::new().compile(&program).unwrap()
1919    }
1920
1921    #[test]
1922    fn test_compile_arithmetic() {
1923        let chunk = compile_source("pipeline test(task) { let x = 2 + 3 }");
1924        assert!(!chunk.code.is_empty());
1925        // Should have constants: 2, 3, "x"
1926        assert!(chunk.constants.contains(&Constant::Int(2)));
1927        assert!(chunk.constants.contains(&Constant::Int(3)));
1928    }
1929
1930    #[test]
1931    fn test_compile_function_call() {
1932        let chunk = compile_source("pipeline test(task) { log(42) }");
1933        let disasm = chunk.disassemble("test");
1934        assert!(disasm.contains("CALL"));
1935    }
1936
1937    #[test]
1938    fn test_compile_if_else() {
1939        let chunk =
1940            compile_source(r#"pipeline test(task) { if true { log("yes") } else { log("no") } }"#);
1941        let disasm = chunk.disassemble("test");
1942        assert!(disasm.contains("JUMP_IF_FALSE"));
1943        assert!(disasm.contains("JUMP"));
1944    }
1945
1946    #[test]
1947    fn test_compile_while() {
1948        let chunk = compile_source("pipeline test(task) { var i = 0\n while i < 5 { i = i + 1 } }");
1949        let disasm = chunk.disassemble("test");
1950        assert!(disasm.contains("JUMP_IF_FALSE"));
1951        // Should have a backward jump
1952        assert!(disasm.contains("JUMP"));
1953    }
1954
1955    #[test]
1956    fn test_compile_closure() {
1957        let chunk = compile_source("pipeline test(task) { let f = { x -> x * 2 } }");
1958        assert!(!chunk.functions.is_empty());
1959        assert_eq!(chunk.functions[0].params, vec!["x"]);
1960    }
1961
1962    #[test]
1963    fn test_compile_list() {
1964        let chunk = compile_source("pipeline test(task) { let a = [1, 2, 3] }");
1965        let disasm = chunk.disassemble("test");
1966        assert!(disasm.contains("BUILD_LIST"));
1967    }
1968
1969    #[test]
1970    fn test_compile_dict() {
1971        let chunk = compile_source(r#"pipeline test(task) { let d = {name: "test"} }"#);
1972        let disasm = chunk.disassemble("test");
1973        assert!(disasm.contains("BUILD_DICT"));
1974    }
1975
1976    #[test]
1977    fn test_disassemble() {
1978        let chunk = compile_source("pipeline test(task) { log(2 + 3) }");
1979        let disasm = chunk.disassemble("test");
1980        // Should be readable
1981        assert!(disasm.contains("CONSTANT"));
1982        assert!(disasm.contains("ADD"));
1983        assert!(disasm.contains("CALL"));
1984    }
1985}