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