Skip to main content

tl_compiler/
compiler.rs

1// ThinkingLanguage — AST-to-bytecode compiler
2// Compiles a TL Program into a Prototype (bytecode chunk).
3
4use std::sync::Arc;
5use tl_ast::*;
6use tl_errors::{RuntimeError, Span, TlError};
7
8use crate::chunk::*;
9use crate::opcode::*;
10
11/// Result of constant folding at compile time.
12#[derive(Debug, Clone)]
13enum FoldedConst {
14    Int(i64),
15    Float(f64),
16    Bool(bool),
17    String(String),
18}
19
20/// Compile error helper
21fn compile_err(msg: String) -> TlError {
22    TlError::Runtime(RuntimeError {
23        message: msg,
24        span: None,
25        stack_trace: vec![],
26    })
27}
28
29/// A local variable in the current scope.
30#[derive(Debug, Clone)]
31struct Local {
32    name: String,
33    depth: u32,
34    register: u8,
35    is_captured: bool,
36}
37
38/// Upvalue tracking during compilation.
39#[derive(Debug, Clone)]
40struct CompilerUpvalue {
41    is_local: bool,
42    index: u8,
43}
44
45/// Loop context for break/continue.
46#[derive(Debug, Clone)]
47struct LoopCtx {
48    /// Instruction indices of break jumps to patch
49    break_jumps: Vec<usize>,
50    /// Instruction index of loop start (for continue)
51    loop_start: usize,
52}
53
54/// Compiler state for one function scope.
55struct CompilerState {
56    proto: Prototype,
57    locals: Vec<Local>,
58    upvalues: Vec<CompilerUpvalue>,
59    scope_depth: u32,
60    next_register: u8,
61    loop_stack: Vec<LoopCtx>,
62    has_yield: bool,
63    /// Current source line (1-based), set by the compiler before emitting instructions
64    current_line: u32,
65    /// Dead code flag: set after compiling Return/Break/Continue in the current block
66    dead_code: bool,
67}
68
69impl CompilerState {
70    fn new(name: String) -> Self {
71        CompilerState {
72            proto: Prototype::new(name),
73            locals: Vec::new(),
74            upvalues: Vec::new(),
75            scope_depth: 0,
76            next_register: 0,
77            loop_stack: Vec::new(),
78            has_yield: false,
79            current_line: 0,
80            dead_code: false,
81        }
82    }
83
84    fn alloc_register(&mut self) -> u8 {
85        if self.next_register == 255 {
86            panic!("Register overflow: function too complex (max 255 registers)");
87        }
88        let r = self.next_register;
89        self.next_register += 1;
90        if self.next_register > self.proto.num_registers {
91            self.proto.num_registers = self.next_register;
92        }
93        r
94    }
95
96    fn free_register(&mut self) {
97        if self.next_register > 0 {
98            self.next_register -= 1;
99        }
100    }
101
102    fn emit(&mut self, inst: u32) {
103        self.proto.code.push(inst);
104        self.proto.lines.push(self.current_line);
105    }
106
107    fn emit_abc(&mut self, op: Op, a: u8, b: u8, c: u8, _line: u32) {
108        self.emit(encode_abc(op, a, b, c));
109    }
110
111    fn emit_abx(&mut self, op: Op, a: u8, bx: u16, _line: u32) {
112        self.emit(encode_abx(op, a, bx));
113    }
114
115    fn add_constant(&mut self, c: Constant) -> u16 {
116        let idx = self.proto.constants.len();
117        if idx >= 65535 {
118            panic!("Constant pool overflow: too many constants (max 65535)");
119        }
120        self.proto.constants.push(c);
121        idx as u16
122    }
123
124    fn current_pos(&self) -> usize {
125        self.proto.code.len()
126    }
127
128    fn patch_jump(&mut self, inst_pos: usize) {
129        let target = self.current_pos();
130        let offset = (target as i32 - inst_pos as i32 - 1) as i16;
131        let old = self.proto.code[inst_pos];
132        let op = (old >> 24) as u8;
133        let a = ((old >> 16) & 0xFF) as u8;
134        self.proto.code[inst_pos] = encode_abx(
135            Op::try_from(op).expect("patching valid instruction"),
136            a,
137            offset as u16,
138        );
139    }
140}
141
142/// The compiler: transforms AST into bytecode.
143pub struct Compiler {
144    states: Vec<CompilerState>,
145    /// Byte offset of each line start (for converting byte offsets to line numbers)
146    line_offsets: Vec<usize>,
147    /// Current line number (1-based), updated at each statement boundary
148    current_line: u32,
149}
150
151impl Compiler {
152    fn current(&mut self) -> &mut CompilerState {
153        self.states.last_mut().unwrap()
154    }
155
156    /// Build a table of byte offsets for the start of each line.
157    fn build_line_offsets(source: &str) -> Vec<usize> {
158        let mut offsets = vec![0]; // line 1 starts at byte 0
159        for (i, ch) in source.as_bytes().iter().enumerate() {
160            if *ch == b'\n' {
161                offsets.push(i + 1);
162            }
163        }
164        offsets
165    }
166
167    /// Convert a byte offset to a 1-based line number using binary search.
168    fn line_of(&self, byte_offset: usize) -> u32 {
169        match self.line_offsets.binary_search(&byte_offset) {
170            Ok(idx) => idx as u32 + 1,
171            Err(idx) => idx as u32, // idx is the line (1-based) since line_offsets[0]=0
172        }
173    }
174
175    /// Get the current line number for emit calls.
176    #[allow(dead_code)]
177    fn line(&self) -> u32 {
178        self.current_line
179    }
180
181    fn begin_scope(&mut self) {
182        self.current().scope_depth += 1;
183    }
184
185    fn end_scope(&mut self) {
186        let state = self.current();
187        state.scope_depth -= 1;
188        // Pop locals that are out of scope
189        while let Some(local) = state.locals.last() {
190            if local.depth <= state.scope_depth {
191                break;
192            }
193            let reg = local.register;
194            let captured = local.is_captured;
195            state.locals.pop();
196            if captured {
197                // Close upvalue — the VM will handle this
198                // We just need the register to be freed
199            }
200            // Free the register if it's the top
201            if reg + 1 == state.next_register {
202                state.next_register = reg;
203            }
204        }
205    }
206
207    fn add_local(&mut self, name: String) -> u8 {
208        let state = self.current();
209        let reg = state.alloc_register();
210        let depth = state.scope_depth;
211        state.locals.push(Local {
212            name,
213            depth,
214            register: reg,
215            is_captured: false,
216        });
217        state.proto.num_locals = state.proto.num_locals.max(state.locals.len() as u8);
218        reg
219    }
220
221    fn resolve_local(&self, name: &str) -> Option<u8> {
222        let state = self.states.last().unwrap();
223        for local in state.locals.iter().rev() {
224            if local.name == name {
225                return Some(local.register);
226            }
227        }
228        None
229    }
230
231    fn resolve_upvalue(&mut self, name: &str) -> Option<u8> {
232        let n = self.states.len();
233        if n < 2 {
234            return None;
235        }
236        self.resolve_upvalue_recursive(n - 1, name)
237    }
238
239    fn resolve_upvalue_recursive(&mut self, state_idx: usize, name: &str) -> Option<u8> {
240        if state_idx == 0 {
241            return None;
242        }
243        // Check enclosing function's locals
244        let enclosing = &mut self.states[state_idx - 1];
245        for i in (0..enclosing.locals.len()).rev() {
246            if enclosing.locals[i].name == name {
247                enclosing.locals[i].is_captured = true;
248                let reg = enclosing.locals[i].register;
249                return Some(self.add_upvalue(state_idx, true, reg));
250            }
251        }
252        // Check enclosing function's upvalues
253        if let Some(uv_idx) = self.resolve_upvalue_recursive(state_idx - 1, name) {
254            return Some(self.add_upvalue(state_idx, false, uv_idx));
255        }
256        None
257    }
258
259    fn add_upvalue(&mut self, state_idx: usize, is_local: bool, index: u8) -> u8 {
260        let state = &mut self.states[state_idx];
261        // Check if we already have this upvalue
262        for (i, uv) in state.upvalues.iter().enumerate() {
263            if uv.is_local == is_local && uv.index == index {
264                return i as u8;
265            }
266        }
267        let idx = state.upvalues.len() as u8;
268        state.upvalues.push(CompilerUpvalue { is_local, index });
269        idx
270    }
271
272    fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), TlError> {
273        // Dead code elimination: skip statements after return/break/continue
274        if self.current().dead_code {
275            return Ok(());
276        }
277
278        // Update current line from statement's span
279        let line = self.line_of(stmt.span.start);
280        self.current_line = line;
281        self.current().current_line = line;
282        match &stmt.kind {
283            StmtKind::Let { name, value, .. } => {
284                let reg = self.add_local(name.clone());
285                self.compile_expr(value, reg)?;
286                Ok(())
287            }
288            StmtKind::FnDecl {
289                name, params, body, ..
290            } => {
291                let reg = self.add_local(name.clone());
292                self.compile_function(name.clone(), params, body, false)?;
293                // The closure instruction already targets `reg`
294                // Actually we need to compile the function into a prototype
295                // and then emit a Closure instruction
296                let _ = reg; // handled inside compile_function
297                Ok(())
298            }
299            StmtKind::Expr(expr) => {
300                let reg = self.current().alloc_register();
301                self.compile_expr(expr, reg)?;
302                self.current().free_register();
303                Ok(())
304            }
305            StmtKind::Return(expr) => {
306                let reg = self.current().alloc_register();
307                match expr {
308                    Some(e) => self.compile_expr(e, reg)?,
309                    None => self.current().emit_abx(Op::LoadNone, reg, 0, 0),
310                }
311                self.current().emit_abc(Op::Return, reg, 0, 0, 0);
312                self.current().free_register();
313                self.current().dead_code = true;
314                Ok(())
315            }
316            StmtKind::If {
317                condition,
318                then_body,
319                else_ifs,
320                else_body,
321            } => self.compile_if(condition, then_body, else_ifs, else_body),
322            StmtKind::While { condition, body } => self.compile_while(condition, body),
323            StmtKind::For { name, iter, body } => self.compile_for(name, iter, body),
324            StmtKind::ParallelFor { name, iter, body } => {
325                // Compile as a regular for loop (rayon parallelism at VM level)
326                self.compile_for(name, iter, body)
327            }
328            StmtKind::Schema {
329                name,
330                fields,
331                version,
332                ..
333            } => self.compile_schema(name, fields, version),
334            StmtKind::Train {
335                name,
336                algorithm,
337                config,
338            } => self.compile_train(name, algorithm, config),
339            StmtKind::Pipeline {
340                name,
341                extract,
342                transform,
343                load,
344                schedule,
345                timeout,
346                retries,
347                on_failure,
348                on_success,
349            } => self.compile_pipeline(
350                name, extract, transform, load, schedule, timeout, retries, on_failure, on_success,
351            ),
352            StmtKind::StreamDecl {
353                name,
354                source,
355                transform: _,
356                sink: _,
357                window,
358                watermark,
359            } => self.compile_stream_decl(name, source, window, watermark),
360            StmtKind::SourceDecl {
361                name,
362                connector_type,
363                config,
364            } => self.compile_connector_decl(name, connector_type, config),
365            StmtKind::SinkDecl {
366                name,
367                connector_type,
368                config,
369            } => self.compile_connector_decl(name, connector_type, config),
370            StmtKind::StructDecl { name, fields, .. } => {
371                let reg = self.add_local(name.clone());
372                let field_names: Vec<Arc<str>> =
373                    fields.iter().map(|f| Arc::from(f.name.as_str())).collect();
374                let name_idx = self
375                    .current()
376                    .add_constant(Constant::String(Arc::from(name.as_str())));
377                // Store field names as string constants
378                let fields_idx = self.current().add_constant(Constant::AstExprList(
379                    field_names
380                        .iter()
381                        .map(|f| tl_ast::Expr::String(f.to_string()))
382                        .collect(),
383                ));
384                // High bit of c marks this as a declaration (not instance creation)
385                self.current().emit_abc(
386                    Op::NewStruct,
387                    reg,
388                    name_idx as u8,
389                    (fields_idx as u8) | 0x80,
390                    0,
391                );
392                Ok(())
393            }
394            StmtKind::EnumDecl { name, variants, .. } => {
395                let reg = self.add_local(name.clone());
396                let name_idx = self
397                    .current()
398                    .add_constant(Constant::String(Arc::from(name.as_str())));
399                // Store variant info
400                let variant_info: Vec<tl_ast::Expr> = variants
401                    .iter()
402                    .map(|v| tl_ast::Expr::String(format!("{}:{}", v.name, v.fields.len())))
403                    .collect();
404                let variants_idx = self
405                    .current()
406                    .add_constant(Constant::AstExprList(variant_info));
407                // High bit of c marks this as a declaration (not instance creation)
408                self.current().emit_abc(
409                    Op::NewStruct,
410                    reg,
411                    name_idx as u8,
412                    (variants_idx as u8) | 0x80,
413                    0,
414                );
415                // We reuse NewStruct op but tag it differently via the globals
416                // Actually, let's use a dedicated approach: store as global with special name
417                let global_idx = self.current().add_constant(Constant::String(Arc::from(
418                    format!("__enum_{name}").as_str(),
419                )));
420                self.current().emit_abx(Op::SetGlobal, reg, global_idx, 0);
421                // Also set as normal global
422                let name_g = self
423                    .current()
424                    .add_constant(Constant::String(Arc::from(name.as_str())));
425                self.current().emit_abx(Op::SetGlobal, reg, name_g, 0);
426                Ok(())
427            }
428            StmtKind::ImplBlock {
429                type_name, methods, ..
430            } => {
431                // Compile each method as a global function with mangled name Type::method
432                for method in methods {
433                    if let StmtKind::FnDecl {
434                        name: mname,
435                        params,
436                        body,
437                        ..
438                    } = &method.kind
439                    {
440                        let mangled = format!("{type_name}::{mname}");
441                        // add_local creates the local binding that compile_function looks up
442                        let reg = self.add_local(mangled.clone());
443                        self.compile_function(mangled.clone(), params, body, false)?;
444                        // Store as global so dispatch_method can find it
445                        let idx = self
446                            .current()
447                            .add_constant(Constant::String(Arc::from(mangled.as_str())));
448                        self.current().emit_abx(Op::SetGlobal, reg, idx, 0);
449                    }
450                }
451                Ok(())
452            }
453            StmtKind::TryCatch {
454                try_body,
455                catch_var,
456                catch_body,
457                finally_body,
458            } => {
459                // Emit TryBegin with offset to catch handler
460                let try_begin_pos = self.current().current_pos();
461                self.current().emit_abx(Op::TryBegin, 0, 0, 0); // patch later
462
463                // Compile try body
464                let saved_dead_code = self.current().dead_code;
465                self.begin_scope();
466                self.current().dead_code = false;
467                for stmt in try_body {
468                    self.compile_stmt(stmt)?;
469                }
470                self.end_scope();
471
472                // Emit TryEnd
473                self.current().emit_abx(Op::TryEnd, 0, 0, 0);
474
475                // Compile finally after try (success path)
476                if let Some(finally_stmts) = &finally_body {
477                    self.begin_scope();
478                    for stmt in finally_stmts {
479                        self.compile_stmt(stmt)?;
480                    }
481                    self.end_scope();
482                }
483
484                // Jump over catch
485                let jump_over_pos = self.current().current_pos();
486                self.current().emit_abx(Op::Jump, 0, 0, 0);
487
488                // Patch TryBegin to point here (catch handler)
489                self.current().patch_jump(try_begin_pos);
490
491                // Catch body: error value will be in a designated register
492                self.begin_scope();
493                self.current().dead_code = false;
494                let catch_reg = self.add_local(catch_var.clone());
495                // The VM will place the error value in catch_reg when it jumps here
496                // We mark it with a LoadNone that the VM will overwrite
497                self.current().emit_abx(Op::LoadNone, catch_reg, 0, 0);
498                for stmt in catch_body {
499                    self.compile_stmt(stmt)?;
500                }
501                self.end_scope();
502
503                // Compile finally after catch (error path)
504                if let Some(finally_stmts) = &finally_body {
505                    self.begin_scope();
506                    for stmt in finally_stmts {
507                        self.compile_stmt(stmt)?;
508                    }
509                    self.end_scope();
510                }
511
512                // Patch jump over catch
513                self.current().patch_jump(jump_over_pos);
514
515                // Restore dead_code (try/catch doesn't make surrounding code dead)
516                self.current().dead_code = saved_dead_code;
517                Ok(())
518            }
519            StmtKind::Throw(expr) => {
520                let reg = self.current().alloc_register();
521                self.compile_expr(expr, reg)?;
522                self.current().emit_abc(Op::Throw, reg, 0, 0, 0);
523                self.current().free_register();
524                Ok(())
525            }
526            StmtKind::Import { path, alias } => {
527                let reg = self.current().alloc_register();
528                let path_idx = self
529                    .current()
530                    .add_constant(Constant::String(Arc::from(path.as_str())));
531                let alias_idx = if let Some(a) = alias {
532                    self.current()
533                        .add_constant(Constant::String(Arc::from(a.as_str())))
534                } else {
535                    self.current().add_constant(Constant::String(Arc::from("")))
536                };
537                self.current().emit_abx(Op::Import, reg, path_idx, 0);
538                // Store alias info in next instruction
539                self.current().emit_abc(Op::Move, alias_idx as u8, 0, 0, 0);
540                self.current().free_register();
541                Ok(())
542            }
543            StmtKind::Test { .. } => {
544                // Tests are only run in test mode; skip during normal compilation
545                Ok(())
546            }
547            StmtKind::Use { item, .. } => self.compile_use(item),
548            StmtKind::ModDecl { .. } => {
549                // ModDecl is handled at load time, not compilation
550                Ok(())
551            }
552            StmtKind::TraitDef { .. } => {
553                // Trait definitions are type-checker only; no runtime code needed
554                Ok(())
555            }
556            StmtKind::TraitImpl {
557                type_name, methods, ..
558            } => {
559                // Compile as a regular impl block — trait impls are type-erased at runtime
560                for method in methods {
561                    if let StmtKind::FnDecl {
562                        name: mname,
563                        params,
564                        body,
565                        ..
566                    } = &method.kind
567                    {
568                        let mangled = format!("{type_name}::{mname}");
569                        let reg = self.add_local(mangled.clone());
570                        self.compile_function(mangled.clone(), params, body, false)?;
571                        let idx = self
572                            .current()
573                            .add_constant(Constant::String(Arc::from(mangled.as_str())));
574                        self.current().emit_abx(Op::SetGlobal, reg, idx, 0);
575                    }
576                }
577                Ok(())
578            }
579            StmtKind::Break => {
580                let state = self.current();
581                let pos = state.current_pos();
582                state.emit_abx(Op::Jump, 0, 0, 0);
583                if let Some(loop_ctx) = state.loop_stack.last_mut() {
584                    loop_ctx.break_jumps.push(pos);
585                }
586                self.current().dead_code = true;
587                Ok(())
588            }
589            StmtKind::Continue => {
590                let state = self.current();
591                if let Some(loop_ctx) = state.loop_stack.last() {
592                    let loop_start = loop_ctx.loop_start;
593                    let current = state.current_pos();
594                    let offset = (loop_start as i32 - current as i32 - 1) as i16;
595                    state.emit_abx(Op::Jump, 0, offset as u16, 0);
596                }
597                self.current().dead_code = true;
598                Ok(())
599            }
600            StmtKind::LetDestructure { pattern, value, .. } => {
601                let val_reg = self.current().alloc_register();
602                self.compile_expr(value, val_reg)?;
603                self.compile_let_destructure(pattern, val_reg)?;
604                // Note: don't free val_reg — locals were allocated above it
605                // and freeing it would allow reuse of registers occupied by locals
606                Ok(())
607            }
608            StmtKind::TypeAlias { .. } => {
609                // Type aliases are type-checker only; no runtime code needed
610                Ok(())
611            }
612            StmtKind::Migrate {
613                schema_name,
614                from_version,
615                to_version,
616                operations,
617            } => self.compile_migrate(schema_name, *from_version, *to_version, operations),
618            StmtKind::Agent {
619                name,
620                model,
621                system_prompt,
622                tools,
623                max_turns,
624                temperature,
625                max_tokens,
626                base_url,
627                api_key,
628                output_format,
629                on_tool_call,
630                on_complete,
631                mcp_servers,
632            } => self.compile_agent(
633                name,
634                model,
635                system_prompt,
636                tools,
637                max_turns,
638                temperature,
639                max_tokens,
640                base_url,
641                api_key,
642                output_format,
643                on_tool_call,
644                on_complete,
645                mcp_servers,
646            ),
647        }
648    }
649
650    /// Compile let-destructuring: bind pattern variables from a value register.
651    fn compile_let_destructure(&mut self, pattern: &Pattern, val_reg: u8) -> Result<(), TlError> {
652        match pattern {
653            Pattern::Binding(name) => {
654                let local = self.add_local(name.clone());
655                self.current().emit_abc(Op::Move, local, val_reg, 0, 0);
656            }
657            Pattern::Wildcard => {} // ignore value
658            Pattern::Struct { fields, .. } => {
659                for field in fields {
660                    let fname_const = self
661                        .current()
662                        .add_constant(Constant::String(Arc::from(field.name.as_str())));
663                    let bind_name = match &field.pattern {
664                        Some(Pattern::Binding(n)) => n.clone(),
665                        _ => field.name.clone(),
666                    };
667                    let local = self.add_local(bind_name);
668                    self.current().emit_abc(
669                        Op::ExtractNamedField,
670                        local,
671                        val_reg,
672                        fname_const as u8,
673                        0,
674                    );
675                }
676            }
677            Pattern::List { elements, rest } => {
678                for (i, elem_pat) in elements.iter().enumerate() {
679                    match elem_pat {
680                        Pattern::Binding(name) => {
681                            let local = self.add_local(name.clone());
682                            self.current()
683                                .emit_abc(Op::ExtractField, local, val_reg, i as u8, 0);
684                        }
685                        Pattern::Wildcard => {}
686                        _ => {}
687                    }
688                }
689                if let Some(rest_name) = rest {
690                    let local = self.add_local(rest_name.clone());
691                    self.current().emit_abc(
692                        Op::ExtractField,
693                        local,
694                        val_reg,
695                        (elements.len() as u8) | 0x80,
696                        0,
697                    );
698                }
699            }
700            Pattern::Enum { args, .. } => {
701                // Extract enum fields if variant matches (runtime will error if not)
702                for (i, arg_pat) in args.iter().enumerate() {
703                    if let Pattern::Binding(name) = arg_pat {
704                        let local = self.add_local(name.clone());
705                        self.current()
706                            .emit_abc(Op::ExtractField, local, val_reg, i as u8, 0);
707                    }
708                }
709            }
710            _ => {}
711        }
712        Ok(())
713    }
714
715    fn compile_function(
716        &mut self,
717        name: String,
718        params: &[Param],
719        body: &[Stmt],
720        is_closure_expr: bool,
721    ) -> Result<u8, TlError> {
722        // Determine the destination register for the closure
723        let dest_reg = if is_closure_expr {
724            // For closure expressions, the caller already allocated a dest register
725            // We don't allocate one here
726            0 // placeholder, will be set by caller
727        } else {
728            // For FnDecl, the local was already added
729            let state = self.states.last().unwrap();
730            state
731                .locals
732                .iter()
733                .rev()
734                .find(|l| l.name == name)
735                .map(|l| l.register)
736                .unwrap_or(0)
737        };
738
739        // Push new compiler state for the function
740        let mut fn_state = CompilerState::new(name);
741        fn_state.proto.arity = params.len() as u8;
742        fn_state.scope_depth = 1;
743
744        // Allocate registers for parameters
745        for param in params {
746            let reg = fn_state.alloc_register();
747            fn_state.locals.push(Local {
748                name: param.name.clone(),
749                depth: 1,
750                register: reg,
751                is_captured: false,
752            });
753        }
754        fn_state.proto.num_locals = params.len() as u8;
755
756        self.states.push(fn_state);
757
758        // Compile function body
759        // Track the last expression's register for implicit return
760        let mut last_expr_reg: Option<u8> = None;
761        for (i, stmt) in body.iter().enumerate() {
762            // Dead code elimination in function body
763            if self.current().dead_code {
764                break;
765            }
766            let is_last = i == body.len() - 1;
767            // Update line tracking
768            let line = self.line_of(stmt.span.start);
769            self.current_line = line;
770            self.current().current_line = line;
771            match &stmt.kind {
772                StmtKind::Expr(expr) if is_last => {
773                    // Last statement is an expression — compile and keep register for return
774                    let reg = self.current().alloc_register();
775                    self.compile_expr(expr, reg)?;
776                    last_expr_reg = Some(reg);
777                    // Don't free — we'll return it
778                }
779                StmtKind::If {
780                    condition,
781                    then_body,
782                    else_ifs,
783                    else_body,
784                } if is_last && else_body.is_some() => {
785                    // Last statement is if-else — compile as expression for implicit return
786                    let dest = self.current().alloc_register();
787                    self.compile_if_as_expr(condition, then_body, else_ifs, else_body, dest)?;
788                    last_expr_reg = Some(dest);
789                }
790                _ => {
791                    self.compile_stmt(stmt)?;
792                }
793            }
794        }
795
796        // If the last instruction isn't a Return, emit one
797        let needs_return = {
798            let state = self.states.last().unwrap();
799            if state.proto.code.is_empty() {
800                true
801            } else {
802                let last = *state.proto.code.last().unwrap();
803                decode_op(last) != Op::Return
804            }
805        };
806        if needs_return {
807            if let Some(reg) = last_expr_reg {
808                // Return the last expression value
809                self.current().emit_abc(Op::Return, reg, 0, 0, 0);
810            } else {
811                let state = self.current();
812                let reg = state.alloc_register();
813                state.emit_abx(Op::LoadNone, reg, 0, 0);
814                state.emit_abc(Op::Return, reg, 0, 0, 0);
815                state.free_register();
816            }
817        }
818
819        // Pop the function state
820        let fn_state = self.states.pop().unwrap();
821        let mut proto = fn_state.proto;
822        proto.is_generator = fn_state.has_yield;
823        proto.upvalue_defs = fn_state
824            .upvalues
825            .iter()
826            .map(|uv| UpvalueDef {
827                is_local: uv.is_local,
828                index: uv.index,
829            })
830            .collect();
831
832        // Add the prototype as a constant in the enclosing function
833        let proto_arc = Arc::new(proto);
834        let const_idx = self.current().add_constant(Constant::Prototype(proto_arc));
835
836        // Emit Closure instruction in the enclosing function
837        self.current().emit_abx(Op::Closure, dest_reg, const_idx, 0);
838
839        Ok(dest_reg)
840    }
841
842    fn compile_closure_expr(
843        &mut self,
844        params: &[Param],
845        body: &ClosureBody,
846        dest: u8,
847    ) -> Result<(), TlError> {
848        // Build the body statements based on closure body kind
849        let body_stmts = match body {
850            ClosureBody::Expr(e) => {
851                // Single-expression closure: wrap in return
852                vec![Stmt {
853                    kind: StmtKind::Return(Some(e.as_ref().clone())),
854                    span: Span::new(0, 0),
855                    doc_comment: None,
856                }]
857            }
858            ClosureBody::Block { stmts, expr } => {
859                // Block closure: compile stmts, then return tail expr (if any)
860                let mut all = stmts.clone();
861                if let Some(e) = expr {
862                    all.push(Stmt {
863                        kind: StmtKind::Return(Some(e.as_ref().clone())),
864                        span: Span::new(0, 0),
865                        doc_comment: None,
866                    });
867                }
868                all
869            }
870        };
871
872        // Push new compiler state
873        let mut fn_state = CompilerState::new("<closure>".to_string());
874        fn_state.proto.arity = params.len() as u8;
875        fn_state.scope_depth = 1;
876
877        for param in params {
878            let reg = fn_state.alloc_register();
879            fn_state.locals.push(Local {
880                name: param.name.clone(),
881                depth: 1,
882                register: reg,
883                is_captured: false,
884            });
885        }
886        fn_state.proto.num_locals = params.len() as u8;
887
888        self.states.push(fn_state);
889
890        for stmt in &body_stmts {
891            self.compile_stmt(stmt)?;
892        }
893
894        // Ensure return
895        let needs_return = {
896            let state = self.states.last().unwrap();
897            if state.proto.code.is_empty() {
898                true
899            } else {
900                let last = *state.proto.code.last().unwrap();
901                decode_op(last) != Op::Return
902            }
903        };
904        if needs_return {
905            let state = self.current();
906            let reg = state.alloc_register();
907            state.emit_abx(Op::LoadNone, reg, 0, 0);
908            state.emit_abc(Op::Return, reg, 0, 0, 0);
909            state.free_register();
910        }
911
912        let fn_state = self.states.pop().unwrap();
913        let mut proto = fn_state.proto;
914        proto.upvalue_defs = fn_state
915            .upvalues
916            .iter()
917            .map(|uv| UpvalueDef {
918                is_local: uv.is_local,
919                index: uv.index,
920            })
921            .collect();
922
923        let proto_arc = Arc::new(proto);
924        let const_idx = self.current().add_constant(Constant::Prototype(proto_arc));
925        self.current().emit_abx(Op::Closure, dest, const_idx, 0);
926
927        Ok(())
928    }
929
930    fn compile_if(
931        &mut self,
932        condition: &Expr,
933        then_body: &[Stmt],
934        else_ifs: &[(Expr, Vec<Stmt>)],
935        else_body: &Option<Vec<Stmt>>,
936    ) -> Result<(), TlError> {
937        let saved_dead_code = self.current().dead_code;
938        let cond_reg = self.current().alloc_register();
939        self.compile_expr(condition, cond_reg)?;
940
941        // Jump if false to else/end
942        let jump_false_pos = self.current().current_pos();
943        self.current().emit_abx(Op::JumpIfFalse, cond_reg, 0, 0);
944        self.current().free_register(); // free cond_reg
945
946        // Then body
947        self.begin_scope();
948        self.current().dead_code = false;
949        for stmt in then_body {
950            self.compile_stmt(stmt)?;
951        }
952        let then_dead = self.current().dead_code;
953        self.end_scope();
954
955        // Jump over else
956        let mut end_jumps = Vec::new();
957        let jump_end_pos = self.current().current_pos();
958        self.current().emit_abx(Op::Jump, 0, 0, 0);
959        end_jumps.push(jump_end_pos);
960
961        // Patch the false jump to here
962        self.current().patch_jump(jump_false_pos);
963
964        // Track whether all branches terminate
965        let mut all_branches_dead = then_dead;
966
967        // Else-ifs
968        for (ei_cond, ei_body) in else_ifs {
969            let cond_reg = self.current().alloc_register();
970            self.compile_expr(ei_cond, cond_reg)?;
971            let jf_pos = self.current().current_pos();
972            self.current().emit_abx(Op::JumpIfFalse, cond_reg, 0, 0);
973            self.current().free_register();
974
975            self.begin_scope();
976            self.current().dead_code = false;
977            for stmt in ei_body {
978                self.compile_stmt(stmt)?;
979            }
980            all_branches_dead = all_branches_dead && self.current().dead_code;
981            self.end_scope();
982
983            let je_pos = self.current().current_pos();
984            self.current().emit_abx(Op::Jump, 0, 0, 0);
985            end_jumps.push(je_pos);
986
987            self.current().patch_jump(jf_pos);
988        }
989
990        // Else body
991        if let Some(body) = else_body {
992            self.begin_scope();
993            self.current().dead_code = false;
994            for stmt in body {
995                self.compile_stmt(stmt)?;
996            }
997            all_branches_dead = all_branches_dead && self.current().dead_code;
998            self.end_scope();
999        } else {
1000            // No else branch means not all paths terminate
1001            all_branches_dead = false;
1002        }
1003
1004        // Patch all end jumps
1005        for pos in end_jumps {
1006            self.current().patch_jump(pos);
1007        }
1008
1009        // Dead code after if: only if ALL branches (including else) terminate
1010        self.current().dead_code = saved_dead_code || all_branches_dead;
1011
1012        Ok(())
1013    }
1014
1015    /// Compile if-else as an expression — each branch stores its last value into `dest`.
1016    fn compile_if_as_expr(
1017        &mut self,
1018        condition: &Expr,
1019        then_body: &[Stmt],
1020        else_ifs: &[(Expr, Vec<Stmt>)],
1021        else_body: &Option<Vec<Stmt>>,
1022        dest: u8,
1023    ) -> Result<(), TlError> {
1024        let cond_reg = self.current().alloc_register();
1025        self.compile_expr(condition, cond_reg)?;
1026        let jump_false_pos = self.current().current_pos();
1027        self.current().emit_abx(Op::JumpIfFalse, cond_reg, 0, 0);
1028        self.current().free_register(); // free cond_reg
1029
1030        // Then body — compile all but last as statements, last as expression into dest
1031        self.begin_scope();
1032        self.compile_body_with_result(then_body, dest)?;
1033        self.end_scope();
1034
1035        let mut end_jumps = Vec::new();
1036        let jump_end_pos = self.current().current_pos();
1037        self.current().emit_abx(Op::Jump, 0, 0, 0);
1038        end_jumps.push(jump_end_pos);
1039
1040        self.current().patch_jump(jump_false_pos);
1041
1042        // Else-ifs
1043        for (ei_cond, ei_body) in else_ifs {
1044            let cond_reg = self.current().alloc_register();
1045            self.compile_expr(ei_cond, cond_reg)?;
1046            let jf_pos = self.current().current_pos();
1047            self.current().emit_abx(Op::JumpIfFalse, cond_reg, 0, 0);
1048            self.current().free_register();
1049
1050            self.begin_scope();
1051            self.compile_body_with_result(ei_body, dest)?;
1052            self.end_scope();
1053
1054            let je_pos = self.current().current_pos();
1055            self.current().emit_abx(Op::Jump, 0, 0, 0);
1056            end_jumps.push(je_pos);
1057
1058            self.current().patch_jump(jf_pos);
1059        }
1060
1061        // Else body
1062        if let Some(body) = else_body {
1063            self.begin_scope();
1064            self.compile_body_with_result(body, dest)?;
1065            self.end_scope();
1066        }
1067
1068        for pos in end_jumps {
1069            self.current().patch_jump(pos);
1070        }
1071
1072        Ok(())
1073    }
1074
1075    /// Compile a block body, storing the last expression's value into `dest`.
1076    fn compile_body_with_result(&mut self, body: &[Stmt], dest: u8) -> Result<(), TlError> {
1077        for (i, stmt) in body.iter().enumerate() {
1078            let is_last = i == body.len() - 1;
1079            let line = self.line_of(stmt.span.start);
1080            self.current_line = line;
1081            self.current().current_line = line;
1082            match &stmt.kind {
1083                StmtKind::Expr(expr) if is_last => {
1084                    self.compile_expr(expr, dest)?;
1085                }
1086                StmtKind::If {
1087                    condition,
1088                    then_body,
1089                    else_ifs,
1090                    else_body,
1091                } if is_last && else_body.is_some() => {
1092                    self.compile_if_as_expr(condition, then_body, else_ifs, else_body, dest)?;
1093                }
1094                _ => {
1095                    self.compile_stmt(stmt)?;
1096                }
1097            }
1098        }
1099        Ok(())
1100    }
1101
1102    fn compile_while(&mut self, condition: &Expr, body: &[Stmt]) -> Result<(), TlError> {
1103        let loop_start = self.current().current_pos();
1104
1105        self.current().loop_stack.push(LoopCtx {
1106            break_jumps: Vec::new(),
1107            loop_start,
1108        });
1109
1110        let cond_reg = self.current().alloc_register();
1111        self.compile_expr(condition, cond_reg)?;
1112        let exit_jump = self.current().current_pos();
1113        self.current().emit_abx(Op::JumpIfFalse, cond_reg, 0, 0);
1114        self.current().free_register();
1115
1116        self.begin_scope();
1117        // Reset dead_code for loop body (continue brings flow back)
1118        let saved_dead_code = self.current().dead_code;
1119        self.current().dead_code = false;
1120        for stmt in body {
1121            self.compile_stmt(stmt)?;
1122        }
1123        self.end_scope();
1124
1125        // Jump back to start
1126        let current = self.current().current_pos();
1127        let offset = (loop_start as i32 - current as i32 - 1) as i16;
1128        self.current().emit_abx(Op::Jump, 0, offset as u16, 0);
1129
1130        // Patch exit jump
1131        self.current().patch_jump(exit_jump);
1132
1133        // Patch break jumps
1134        let loop_ctx = self.current().loop_stack.pop().unwrap();
1135        for pos in loop_ctx.break_jumps {
1136            self.current().patch_jump(pos);
1137        }
1138
1139        // Restore dead_code state (loop doesn't make surrounding code dead)
1140        self.current().dead_code = saved_dead_code;
1141
1142        Ok(())
1143    }
1144
1145    fn compile_for(&mut self, name: &str, iter: &Expr, body: &[Stmt]) -> Result<(), TlError> {
1146        // Evaluate iterator expression
1147        let list_reg = self.current().alloc_register();
1148        self.compile_expr(iter, list_reg)?;
1149
1150        // Create iterator (index counter)
1151        let iter_reg = self.current().alloc_register();
1152        let zero_const = self.current().add_constant(Constant::Int(0));
1153        self.current()
1154            .emit_abx(Op::LoadConst, iter_reg, zero_const, 0);
1155
1156        let loop_start = self.current().current_pos();
1157        self.current().loop_stack.push(LoopCtx {
1158            break_jumps: Vec::new(),
1159            loop_start,
1160        });
1161
1162        // ForIter: check if iterator is done, load value
1163        self.begin_scope();
1164        let val_reg = self.add_local(name.to_string());
1165        self.current()
1166            .emit_abc(Op::ForIter, iter_reg, list_reg, val_reg, 0);
1167        // The next instruction is the jump offset if done
1168        let exit_jump = self.current().current_pos();
1169        self.current().emit_abx(Op::Jump, 0, 0, 0); // placeholder, patched
1170
1171        // Body
1172        let saved_dead_code = self.current().dead_code;
1173        self.current().dead_code = false;
1174        for stmt in body {
1175            self.compile_stmt(stmt)?;
1176        }
1177
1178        self.end_scope();
1179
1180        // Jump back to loop start
1181        let current = self.current().current_pos();
1182        let offset = (loop_start as i32 - current as i32 - 1) as i16;
1183        self.current().emit_abx(Op::Jump, 0, offset as u16, 0);
1184
1185        // Patch exit jump
1186        self.current().patch_jump(exit_jump);
1187
1188        // Patch break jumps
1189        let loop_ctx = self.current().loop_stack.pop().unwrap();
1190        for pos in loop_ctx.break_jumps {
1191            self.current().patch_jump(pos);
1192        }
1193
1194        // Free iterator and list registers
1195        self.current().free_register(); // iter_reg
1196        self.current().free_register(); // list_reg
1197
1198        // Restore dead_code state
1199        self.current().dead_code = saved_dead_code;
1200
1201        Ok(())
1202    }
1203
1204    fn compile_schema(
1205        &mut self,
1206        name: &str,
1207        fields: &[SchemaField],
1208        version: &Option<i64>,
1209    ) -> Result<(), TlError> {
1210        // Schema compilation: store as a global with the schema data
1211        // We use a special constant and SetGlobal
1212        let reg = self.current().alloc_register();
1213
1214        // Encode schema as a constant with name + version + fields as string
1215        let ver_str = version.map_or(String::new(), |v| format!(":v{}", v));
1216        let schema_str = format!(
1217            "__schema__:{}{}:{}",
1218            name,
1219            ver_str,
1220            fields
1221                .iter()
1222                .map(|f| format!("{}:{:?}", f.name, f.type_ann))
1223                .collect::<Vec<_>>()
1224                .join(",")
1225        );
1226        let const_idx = self
1227            .current()
1228            .add_constant(Constant::String(Arc::from(schema_str.as_str())));
1229        self.current().emit_abx(Op::LoadConst, reg, const_idx, 0);
1230
1231        let name_idx = self
1232            .current()
1233            .add_constant(Constant::String(Arc::from(name)));
1234        self.current().emit_abx(Op::SetGlobal, reg, name_idx, 0);
1235        self.current().free_register();
1236
1237        // Also add as local
1238        let local_reg = self.add_local(name.to_string());
1239        self.current()
1240            .emit_abx(Op::LoadConst, local_reg, const_idx, 0);
1241
1242        Ok(())
1243    }
1244
1245    fn compile_migrate(
1246        &mut self,
1247        schema_name: &str,
1248        from_version: i64,
1249        to_version: i64,
1250        operations: &[MigrateOp],
1251    ) -> Result<(), TlError> {
1252        // Encode migration as a constant string with ops
1253        let mut ops_str = Vec::new();
1254        for op in operations {
1255            let s = match op {
1256                MigrateOp::AddColumn {
1257                    name,
1258                    type_ann,
1259                    default,
1260                } => {
1261                    let def_str = if let Some(d) = default {
1262                        format!(",default:{d:?}")
1263                    } else {
1264                        String::new()
1265                    };
1266                    format!("add:{name}:{type_ann:?}{def_str}")
1267                }
1268                MigrateOp::DropColumn { name } => format!("drop:{name}"),
1269                MigrateOp::RenameColumn { from, to } => format!("rename:{from}:{to}"),
1270                MigrateOp::AlterType { column, new_type } => format!("alter:{column}:{new_type:?}"),
1271                MigrateOp::AddConstraint { column, constraint } => {
1272                    format!("add_constraint:{column}:{constraint}")
1273                }
1274                MigrateOp::DropConstraint { column, constraint } => {
1275                    format!("drop_constraint:{column}:{constraint}")
1276                }
1277            };
1278            ops_str.push(s);
1279        }
1280        let migrate_str = format!(
1281            "__migrate__:{}:{}:{}:{}",
1282            schema_name,
1283            from_version,
1284            to_version,
1285            ops_str.join(";")
1286        );
1287        let reg = self.current().alloc_register();
1288        let const_idx = self
1289            .current()
1290            .add_constant(Constant::String(Arc::from(migrate_str.as_str())));
1291        self.current().emit_abx(Op::LoadConst, reg, const_idx, 0);
1292        // Store as a global migration record
1293        let name_key = format!("__migrate_{schema_name}_{from_version}_{to_version}");
1294        let name_idx = self
1295            .current()
1296            .add_constant(Constant::String(Arc::from(name_key.as_str())));
1297        self.current().emit_abx(Op::SetGlobal, reg, name_idx, 0);
1298        self.current().free_register();
1299        Ok(())
1300    }
1301
1302    fn compile_train(
1303        &mut self,
1304        name: &str,
1305        algorithm: &str,
1306        config: &[(String, Expr)],
1307    ) -> Result<(), TlError> {
1308        let dest = self.add_local(name.to_string());
1309
1310        // Store algorithm as constant
1311        let algo_idx = self
1312            .current()
1313            .add_constant(Constant::String(Arc::from(algorithm)));
1314
1315        // For any config value that's an Ident, ensure the local is exported as a global
1316        // so the VM's eval_ast_to_vm can resolve it at runtime.
1317        for (_key, val) in config {
1318            if let Expr::Ident(ident_name) = val
1319                && let Some(reg) = self.resolve_local(ident_name)
1320            {
1321                let name_idx = self
1322                    .current()
1323                    .add_constant(Constant::String(Arc::from(ident_name.as_str())));
1324                self.current().emit_abx(Op::SetGlobal, reg, name_idx, 0);
1325            }
1326        }
1327
1328        // Store config key-value pairs: compile each value expr, then store as AstExprList
1329        let config_exprs: Vec<tl_ast::Expr> = config
1330            .iter()
1331            .map(|(k, v)| {
1332                // Encode config as a list of NamedArg expressions
1333                tl_ast::Expr::NamedArg {
1334                    name: k.clone(),
1335                    value: Box::new(v.clone()),
1336                }
1337            })
1338            .collect();
1339        let config_idx = self
1340            .current()
1341            .add_constant(Constant::AstExprList(config_exprs));
1342
1343        // Emit Train instruction
1344        self.current()
1345            .emit_abc(Op::Train, dest, algo_idx as u8, config_idx as u8, 0);
1346
1347        // Also set as global
1348        let name_idx = self
1349            .current()
1350            .add_constant(Constant::String(Arc::from(name)));
1351        self.current().emit_abx(Op::SetGlobal, dest, name_idx, 0);
1352
1353        Ok(())
1354    }
1355
1356    #[allow(clippy::too_many_arguments)]
1357    fn compile_pipeline(
1358        &mut self,
1359        name: &str,
1360        extract: &[Stmt],
1361        transform: &[Stmt],
1362        load: &[Stmt],
1363        schedule: &Option<String>,
1364        timeout: &Option<String>,
1365        retries: &Option<i64>,
1366        _on_failure: &Option<Vec<Stmt>>,
1367        on_success: &Option<Vec<Stmt>>,
1368    ) -> Result<(), TlError> {
1369        let dest = self.add_local(name.to_string());
1370
1371        // Store pipeline config as constants
1372        let name_const = self
1373            .current()
1374            .add_constant(Constant::String(Arc::from(name)));
1375
1376        // Compile extract/transform/load blocks as AstExprList of the statements
1377        // For simplicity, store the blocks as AST and handle at runtime
1378        let mut all_stmts = Vec::new();
1379        all_stmts.extend(extract.to_vec());
1380        all_stmts.extend(transform.to_vec());
1381        all_stmts.extend(load.to_vec());
1382
1383        // Store config: schedule, timeout, retries as named args
1384        let mut config_exprs: Vec<tl_ast::Expr> = Vec::new();
1385
1386        if let Some(s) = schedule {
1387            config_exprs.push(tl_ast::Expr::NamedArg {
1388                name: "schedule".to_string(),
1389                value: Box::new(tl_ast::Expr::String(s.clone())),
1390            });
1391        }
1392        if let Some(t) = timeout {
1393            config_exprs.push(tl_ast::Expr::NamedArg {
1394                name: "timeout".to_string(),
1395                value: Box::new(tl_ast::Expr::String(t.clone())),
1396            });
1397        }
1398        if let Some(r) = retries {
1399            config_exprs.push(tl_ast::Expr::NamedArg {
1400                name: "retries".to_string(),
1401                value: Box::new(tl_ast::Expr::Int(*r)),
1402            });
1403        }
1404        let config_idx = self
1405            .current()
1406            .add_constant(Constant::AstExprList(config_exprs));
1407
1408        // Compile blocks inline for VM execution (shared scope)
1409        for stmt in extract {
1410            self.compile_stmt(stmt)?;
1411        }
1412        for stmt in transform {
1413            self.compile_stmt(stmt)?;
1414        }
1415        for stmt in load {
1416            self.compile_stmt(stmt)?;
1417        }
1418
1419        // Execute on_success block if present
1420        if let Some(success_block) = on_success {
1421            for stmt in success_block {
1422                self.compile_stmt(stmt)?;
1423            }
1424        }
1425
1426        // Emit PipelineExec to store the pipeline def
1427        self.current().emit_abc(
1428            Op::PipelineExec,
1429            dest,
1430            name_const as u8,
1431            config_idx as u8,
1432            0,
1433        );
1434
1435        // Set as global
1436        let gname = self
1437            .current()
1438            .add_constant(Constant::String(Arc::from(name)));
1439        self.current().emit_abx(Op::SetGlobal, dest, gname, 0);
1440
1441        Ok(())
1442    }
1443
1444    #[allow(clippy::too_many_arguments)]
1445    fn compile_agent(
1446        &mut self,
1447        name: &str,
1448        model: &str,
1449        system_prompt: &Option<String>,
1450        tools: &[(String, tl_ast::Expr)],
1451        max_turns: &Option<i64>,
1452        temperature: &Option<f64>,
1453        max_tokens: &Option<i64>,
1454        base_url: &Option<String>,
1455        api_key: &Option<String>,
1456        output_format: &Option<String>,
1457        on_tool_call: &Option<Vec<tl_ast::Stmt>>,
1458        on_complete: &Option<Vec<tl_ast::Stmt>>,
1459        mcp_servers: &[tl_ast::Expr],
1460    ) -> Result<(), TlError> {
1461        let dest = self.add_local(name.to_string());
1462
1463        // Store agent name as constant
1464        let name_const = self
1465            .current()
1466            .add_constant(Constant::String(Arc::from(name)));
1467
1468        // Build config as AstExprList of NamedArgs
1469        let mut config_exprs: Vec<tl_ast::Expr> = Vec::new();
1470
1471        // model (required)
1472        config_exprs.push(tl_ast::Expr::NamedArg {
1473            name: "model".to_string(),
1474            value: Box::new(tl_ast::Expr::String(model.to_string())),
1475        });
1476
1477        if let Some(sys) = system_prompt {
1478            config_exprs.push(tl_ast::Expr::NamedArg {
1479                name: "system".to_string(),
1480                value: Box::new(tl_ast::Expr::String(sys.clone())),
1481            });
1482        }
1483
1484        if let Some(n) = max_turns {
1485            config_exprs.push(tl_ast::Expr::NamedArg {
1486                name: "max_turns".to_string(),
1487                value: Box::new(tl_ast::Expr::Int(*n)),
1488            });
1489        }
1490
1491        if let Some(t) = temperature {
1492            config_exprs.push(tl_ast::Expr::NamedArg {
1493                name: "temperature".to_string(),
1494                value: Box::new(tl_ast::Expr::Float(*t)),
1495            });
1496        }
1497
1498        if let Some(n) = max_tokens {
1499            config_exprs.push(tl_ast::Expr::NamedArg {
1500                name: "max_tokens".to_string(),
1501                value: Box::new(tl_ast::Expr::Int(*n)),
1502            });
1503        }
1504
1505        if let Some(url) = base_url {
1506            config_exprs.push(tl_ast::Expr::NamedArg {
1507                name: "base_url".to_string(),
1508                value: Box::new(tl_ast::Expr::String(url.clone())),
1509            });
1510        }
1511
1512        if let Some(key) = api_key {
1513            config_exprs.push(tl_ast::Expr::NamedArg {
1514                name: "api_key".to_string(),
1515                value: Box::new(tl_ast::Expr::String(key.clone())),
1516            });
1517        }
1518
1519        if let Some(fmt) = output_format {
1520            config_exprs.push(tl_ast::Expr::NamedArg {
1521                name: "output_format".to_string(),
1522                value: Box::new(tl_ast::Expr::String(fmt.clone())),
1523            });
1524        }
1525
1526        // Encode tools as a list of NamedArgs: tool_name: { desc, params map expr }
1527        for (tool_name, tool_expr) in tools {
1528            config_exprs.push(tl_ast::Expr::NamedArg {
1529                name: format!("tool:{tool_name}"),
1530                value: Box::new(tool_expr.clone()),
1531            });
1532        }
1533
1534        // Encode MCP server references
1535        for (i, expr) in mcp_servers.iter().enumerate() {
1536            config_exprs.push(tl_ast::Expr::NamedArg {
1537                name: format!("mcp_server:{i}"),
1538                value: Box::new(expr.clone()),
1539            });
1540        }
1541
1542        // Encode lifecycle hook markers
1543        if on_tool_call.is_some() {
1544            config_exprs.push(tl_ast::Expr::NamedArg {
1545                name: "on_tool_call".to_string(),
1546                value: Box::new(tl_ast::Expr::Bool(true)),
1547            });
1548        }
1549        if on_complete.is_some() {
1550            config_exprs.push(tl_ast::Expr::NamedArg {
1551                name: "on_complete".to_string(),
1552                value: Box::new(tl_ast::Expr::Bool(true)),
1553            });
1554        }
1555
1556        let config_idx = self
1557            .current()
1558            .add_constant(Constant::AstExprList(config_exprs));
1559
1560        // Emit AgentExec opcode
1561        self.current()
1562            .emit_abc(Op::AgentExec, dest, name_const as u8, config_idx as u8, 0);
1563
1564        // Set as global
1565        let gname = self
1566            .current()
1567            .add_constant(Constant::String(Arc::from(name)));
1568        self.current().emit_abx(Op::SetGlobal, dest, gname, 0);
1569
1570        // Compile lifecycle hooks as global functions
1571        if let Some(stmts) = on_tool_call {
1572            let hook_name = format!("__agent_{name}_on_tool_call__");
1573            let params = vec![
1574                tl_ast::Param {
1575                    name: "tool_name".into(),
1576                    type_ann: None,
1577                },
1578                tl_ast::Param {
1579                    name: "tool_args".into(),
1580                    type_ann: None,
1581                },
1582                tl_ast::Param {
1583                    name: "tool_result".into(),
1584                    type_ann: None,
1585                },
1586            ];
1587            self.compile_agent_hook(&hook_name, &params, stmts)?;
1588        }
1589        if let Some(stmts) = on_complete {
1590            let hook_name = format!("__agent_{name}_on_complete__");
1591            let params = vec![tl_ast::Param {
1592                name: "result".into(),
1593                type_ann: None,
1594            }];
1595            self.compile_agent_hook(&hook_name, &params, stmts)?;
1596        }
1597
1598        Ok(())
1599    }
1600
1601    fn compile_agent_hook(
1602        &mut self,
1603        hook_name: &str,
1604        params: &[tl_ast::Param],
1605        body: &[tl_ast::Stmt],
1606    ) -> Result<(), TlError> {
1607        // Add a local for the hook function
1608        let local = self.add_local(hook_name.to_string());
1609
1610        // Compile the function body using compile_function
1611        let dest = self.compile_function(hook_name.to_string(), params, body, false)?;
1612
1613        // Set as global so exec_agent_loop can find it
1614        let gname = self
1615            .current()
1616            .add_constant(Constant::String(Arc::from(hook_name)));
1617        self.current().emit_abx(Op::SetGlobal, local, gname, 0);
1618        let _ = dest;
1619
1620        Ok(())
1621    }
1622
1623    fn compile_stream_decl(
1624        &mut self,
1625        name: &str,
1626        source: &Expr,
1627        window: &Option<tl_ast::WindowSpec>,
1628        watermark: &Option<String>,
1629    ) -> Result<(), TlError> {
1630        let dest = self.add_local(name.to_string());
1631
1632        // Compile source expression
1633        let src_reg = self.current().alloc_register();
1634        self.compile_expr(source, src_reg)?;
1635
1636        // Store stream config as constants
1637        let mut config_exprs: Vec<tl_ast::Expr> = Vec::new();
1638        config_exprs.push(tl_ast::Expr::NamedArg {
1639            name: "name".to_string(),
1640            value: Box::new(tl_ast::Expr::String(name.to_string())),
1641        });
1642
1643        if let Some(w) = window {
1644            let window_str = match w {
1645                tl_ast::WindowSpec::Tumbling(d) => format!("tumbling:{d}"),
1646                tl_ast::WindowSpec::Sliding(w, s) => format!("sliding:{w}:{s}"),
1647                tl_ast::WindowSpec::Session(g) => format!("session:{g}"),
1648            };
1649            config_exprs.push(tl_ast::Expr::NamedArg {
1650                name: "window".to_string(),
1651                value: Box::new(tl_ast::Expr::String(window_str)),
1652            });
1653        }
1654        if let Some(wm) = watermark {
1655            config_exprs.push(tl_ast::Expr::NamedArg {
1656                name: "watermark".to_string(),
1657                value: Box::new(tl_ast::Expr::String(wm.clone())),
1658            });
1659        }
1660        let config_idx = self
1661            .current()
1662            .add_constant(Constant::AstExprList(config_exprs));
1663
1664        // Emit StreamExec instruction
1665        self.current()
1666            .emit_abc(Op::StreamExec, dest, config_idx as u8, src_reg, 0);
1667        self.current().free_register(); // free src_reg
1668
1669        // Set as global
1670        let gname = self
1671            .current()
1672            .add_constant(Constant::String(Arc::from(name)));
1673        self.current().emit_abx(Op::SetGlobal, dest, gname, 0);
1674
1675        Ok(())
1676    }
1677
1678    fn compile_connector_decl(
1679        &mut self,
1680        name: &str,
1681        connector_type: &str,
1682        config: &[(String, Expr)],
1683    ) -> Result<(), TlError> {
1684        let dest = self.add_local(name.to_string());
1685
1686        // Store connector type as constant
1687        let type_idx = self
1688            .current()
1689            .add_constant(Constant::String(Arc::from(connector_type)));
1690
1691        // Store config as AstExprList
1692        let config_exprs: Vec<tl_ast::Expr> = config
1693            .iter()
1694            .map(|(k, v)| tl_ast::Expr::NamedArg {
1695                name: k.clone(),
1696                value: Box::new(v.clone()),
1697            })
1698            .collect();
1699        let config_idx = self
1700            .current()
1701            .add_constant(Constant::AstExprList(config_exprs));
1702
1703        // Emit ConnectorDecl instruction
1704        self.current()
1705            .emit_abc(Op::ConnectorDecl, dest, type_idx as u8, config_idx as u8, 0);
1706
1707        // Set as global
1708        let gname = self
1709            .current()
1710            .add_constant(Constant::String(Arc::from(name)));
1711        self.current().emit_abx(Op::SetGlobal, dest, gname, 0);
1712
1713        Ok(())
1714    }
1715
1716    // ── Constant Folding ─────────────────────────────────────
1717
1718    /// Try to evaluate a constant expression at compile time.
1719    /// Returns None if the expression contains non-constant parts.
1720    fn try_fold_const(expr: &Expr) -> Option<FoldedConst> {
1721        match expr {
1722            Expr::Int(n) => Some(FoldedConst::Int(*n)),
1723            Expr::Float(f) => Some(FoldedConst::Float(*f)),
1724            Expr::Bool(b) => Some(FoldedConst::Bool(*b)),
1725            // Only fold simple strings (no interpolation markers, no escape sequences).
1726            // Strings with escapes need processing via compile_string_interpolation.
1727            Expr::String(s) if !s.contains('{') && !s.contains('\\') => {
1728                Some(FoldedConst::String(s.clone()))
1729            }
1730            Expr::String(_) => None,
1731
1732            Expr::UnaryOp {
1733                op: UnaryOp::Neg,
1734                expr,
1735            } => match Self::try_fold_const(expr)? {
1736                FoldedConst::Int(n) => Some(FoldedConst::Int(-n)),
1737                FoldedConst::Float(f) => Some(FoldedConst::Float(-f)),
1738                _ => None,
1739            },
1740            Expr::UnaryOp {
1741                op: UnaryOp::Not,
1742                expr,
1743            } => match Self::try_fold_const(expr)? {
1744                FoldedConst::Bool(b) => Some(FoldedConst::Bool(!b)),
1745                _ => None,
1746            },
1747
1748            Expr::BinOp { left, op, right } => {
1749                let l = Self::try_fold_const(left)?;
1750                let r = Self::try_fold_const(right)?;
1751                Self::fold_binop(&l, op, &r)
1752            }
1753
1754            // Anything else (variables, calls, member access, etc.) cannot be folded
1755            _ => None,
1756        }
1757    }
1758
1759    /// Fold a binary operation on two constants.
1760    fn fold_binop(left: &FoldedConst, op: &BinOp, right: &FoldedConst) -> Option<FoldedConst> {
1761        match (left, right) {
1762            // Int op Int
1763            (FoldedConst::Int(a), FoldedConst::Int(b)) => match op {
1764                BinOp::Add => Some(FoldedConst::Int(a.checked_add(*b)?)),
1765                BinOp::Sub => Some(FoldedConst::Int(a.checked_sub(*b)?)),
1766                BinOp::Mul => Some(FoldedConst::Int(a.checked_mul(*b)?)),
1767                BinOp::Div => {
1768                    if *b == 0 {
1769                        return None;
1770                    } // defer to runtime error
1771                    Some(FoldedConst::Int(a / b))
1772                }
1773                BinOp::Mod => {
1774                    if *b == 0 {
1775                        return None;
1776                    }
1777                    Some(FoldedConst::Int(a % b))
1778                }
1779                BinOp::Pow => {
1780                    if *b >= 0 {
1781                        Some(FoldedConst::Int(a.pow(*b as u32)))
1782                    } else {
1783                        Some(FoldedConst::Float((*a as f64).powf(*b as f64)))
1784                    }
1785                }
1786                BinOp::Eq => Some(FoldedConst::Bool(a == b)),
1787                BinOp::Neq => Some(FoldedConst::Bool(a != b)),
1788                BinOp::Lt => Some(FoldedConst::Bool(a < b)),
1789                BinOp::Gt => Some(FoldedConst::Bool(a > b)),
1790                BinOp::Lte => Some(FoldedConst::Bool(a <= b)),
1791                BinOp::Gte => Some(FoldedConst::Bool(a >= b)),
1792                BinOp::And | BinOp::Or => None,
1793            },
1794
1795            // Float op Float
1796            (FoldedConst::Float(a), FoldedConst::Float(b)) => match op {
1797                BinOp::Add => Some(FoldedConst::Float(a + b)),
1798                BinOp::Sub => Some(FoldedConst::Float(a - b)),
1799                BinOp::Mul => Some(FoldedConst::Float(a * b)),
1800                BinOp::Div => {
1801                    if *b == 0.0 {
1802                        return None;
1803                    }
1804                    Some(FoldedConst::Float(a / b))
1805                }
1806                BinOp::Mod => {
1807                    if *b == 0.0 {
1808                        return None;
1809                    }
1810                    Some(FoldedConst::Float(a % b))
1811                }
1812                BinOp::Pow => Some(FoldedConst::Float(a.powf(*b))),
1813                BinOp::Eq => Some(FoldedConst::Bool(a == b)),
1814                BinOp::Neq => Some(FoldedConst::Bool(a != b)),
1815                BinOp::Lt => Some(FoldedConst::Bool(a < b)),
1816                BinOp::Gt => Some(FoldedConst::Bool(a > b)),
1817                BinOp::Lte => Some(FoldedConst::Bool(a <= b)),
1818                BinOp::Gte => Some(FoldedConst::Bool(a >= b)),
1819                BinOp::And | BinOp::Or => None,
1820            },
1821
1822            // Int op Float / Float op Int — promote to float
1823            (FoldedConst::Int(a), FoldedConst::Float(b)) => {
1824                let a = *a as f64;
1825                Self::fold_binop(&FoldedConst::Float(a), op, &FoldedConst::Float(*b))
1826            }
1827            (FoldedConst::Float(a), FoldedConst::Int(b)) => {
1828                let b = *b as f64;
1829                Self::fold_binop(&FoldedConst::Float(*a), op, &FoldedConst::Float(b))
1830            }
1831
1832            // String + String — concatenation
1833            (FoldedConst::String(a), FoldedConst::String(b)) if matches!(op, BinOp::Add) => {
1834                Some(FoldedConst::String(format!("{a}{b}")))
1835            }
1836            (FoldedConst::String(a), FoldedConst::String(b)) => match op {
1837                BinOp::Eq => Some(FoldedConst::Bool(a == b)),
1838                BinOp::Neq => Some(FoldedConst::Bool(a != b)),
1839                _ => None,
1840            },
1841
1842            // Bool op Bool
1843            (FoldedConst::Bool(a), FoldedConst::Bool(b)) => match op {
1844                BinOp::And => Some(FoldedConst::Bool(*a && *b)),
1845                BinOp::Or => Some(FoldedConst::Bool(*a || *b)),
1846                BinOp::Eq => Some(FoldedConst::Bool(a == b)),
1847                BinOp::Neq => Some(FoldedConst::Bool(a != b)),
1848                _ => None,
1849            },
1850
1851            _ => None,
1852        }
1853    }
1854
1855    // ── End Constant Folding ────────────────────────────────
1856
1857    fn compile_expr(&mut self, expr: &Expr, dest: u8) -> Result<(), TlError> {
1858        // Constant folding: try to evaluate the expression at compile time
1859        if let Some(folded) = Self::try_fold_const(expr) {
1860            match folded {
1861                FoldedConst::Int(n) => {
1862                    let idx = self.current().add_constant(Constant::Int(n));
1863                    self.current().emit_abx(Op::LoadConst, dest, idx, 0);
1864                }
1865                FoldedConst::Float(f) => {
1866                    let idx = self.current().add_constant(Constant::Float(f));
1867                    self.current().emit_abx(Op::LoadConst, dest, idx, 0);
1868                }
1869                FoldedConst::Bool(true) => {
1870                    self.current().emit_abx(Op::LoadTrue, dest, 0, 0);
1871                }
1872                FoldedConst::Bool(false) => {
1873                    self.current().emit_abx(Op::LoadFalse, dest, 0, 0);
1874                }
1875                FoldedConst::String(s) => {
1876                    let idx = self
1877                        .current()
1878                        .add_constant(Constant::String(Arc::from(s.as_str())));
1879                    self.current().emit_abx(Op::LoadConst, dest, idx, 0);
1880                }
1881            }
1882            return Ok(());
1883        }
1884
1885        match expr {
1886            Expr::Int(n) => {
1887                let idx = self.current().add_constant(Constant::Int(*n));
1888                self.current().emit_abx(Op::LoadConst, dest, idx, 0);
1889            }
1890            Expr::Float(f) => {
1891                let idx = self.current().add_constant(Constant::Float(*f));
1892                self.current().emit_abx(Op::LoadConst, dest, idx, 0);
1893            }
1894            Expr::Decimal(s) => {
1895                let idx = self
1896                    .current()
1897                    .add_constant(Constant::Decimal(Arc::from(s.as_str())));
1898                self.current().emit_abx(Op::LoadConst, dest, idx, 0);
1899            }
1900            Expr::String(s) => {
1901                self.compile_string_interpolation(s, dest)?;
1902            }
1903            Expr::Bool(true) => {
1904                self.current().emit_abx(Op::LoadTrue, dest, 0, 0);
1905            }
1906            Expr::Bool(false) => {
1907                self.current().emit_abx(Op::LoadFalse, dest, 0, 0);
1908            }
1909            Expr::None => {
1910                self.current().emit_abx(Op::LoadNone, dest, 0, 0);
1911            }
1912            Expr::Ident(name) => {
1913                // Try local first, then upvalue, then global
1914                if let Some(reg) = self.resolve_local(name) {
1915                    if reg != dest {
1916                        self.current().emit_abc(Op::Move, dest, reg, 0, 0);
1917                    }
1918                } else if let Some(uv) = self.resolve_upvalue(name) {
1919                    self.current().emit_abc(Op::GetUpvalue, dest, uv, 0, 0);
1920                } else {
1921                    let idx = self
1922                        .current()
1923                        .add_constant(Constant::String(Arc::from(name.as_str())));
1924                    self.current().emit_abx(Op::GetGlobal, dest, idx, 0);
1925                }
1926            }
1927            Expr::BinOp { left, op, right } => {
1928                // Short-circuit for And/Or
1929                match op {
1930                    BinOp::And => {
1931                        self.compile_expr(left, dest)?;
1932                        let jump_pos = self.current().current_pos();
1933                        self.current().emit_abx(Op::JumpIfFalse, dest, 0, 0);
1934                        self.compile_expr(right, dest)?;
1935                        self.current().patch_jump(jump_pos);
1936                        return Ok(());
1937                    }
1938                    BinOp::Or => {
1939                        self.compile_expr(left, dest)?;
1940                        let jump_pos = self.current().current_pos();
1941                        self.current().emit_abx(Op::JumpIfTrue, dest, 0, 0);
1942                        self.compile_expr(right, dest)?;
1943                        self.current().patch_jump(jump_pos);
1944                        return Ok(());
1945                    }
1946                    _ => {}
1947                }
1948
1949                let left_reg = self.current().alloc_register();
1950                let right_reg = self.current().alloc_register();
1951                self.compile_expr(left, left_reg)?;
1952                self.compile_expr(right, right_reg)?;
1953
1954                let vm_op = match op {
1955                    BinOp::Add => Op::Add,
1956                    BinOp::Sub => Op::Sub,
1957                    BinOp::Mul => Op::Mul,
1958                    BinOp::Div => Op::Div,
1959                    BinOp::Mod => Op::Mod,
1960                    BinOp::Pow => Op::Pow,
1961                    BinOp::Eq => Op::Eq,
1962                    BinOp::Neq => Op::Neq,
1963                    BinOp::Lt => Op::Lt,
1964                    BinOp::Gt => Op::Gt,
1965                    BinOp::Lte => Op::Lte,
1966                    BinOp::Gte => Op::Gte,
1967                    BinOp::And | BinOp::Or => unreachable!(), // handled above
1968                };
1969                self.current().emit_abc(vm_op, dest, left_reg, right_reg, 0);
1970                self.current().free_register(); // right
1971                self.current().free_register(); // left
1972            }
1973            Expr::UnaryOp { op, expr } => {
1974                let src = self.current().alloc_register();
1975                self.compile_expr(expr, src)?;
1976                match op {
1977                    UnaryOp::Neg => self.current().emit_abc(Op::Neg, dest, src, 0, 0),
1978                    UnaryOp::Not => self.current().emit_abc(Op::Not, dest, src, 0, 0),
1979                    UnaryOp::Ref => {
1980                        self.current().emit_abc(Op::MakeRef, dest, src, 0, 0);
1981                    }
1982                }
1983                self.current().free_register();
1984            }
1985            Expr::Await(expr) => {
1986                let src = self.current().alloc_register();
1987                self.compile_expr(expr, src)?;
1988                self.current().emit_abc(Op::Await, dest, src, 0, 0);
1989                self.current().free_register();
1990            }
1991            Expr::Try(inner) => {
1992                let src = self.current().alloc_register();
1993                self.compile_expr(inner, src)?;
1994                self.current().emit_abc(Op::TryPropagate, dest, src, 0, 0);
1995                self.current().free_register();
1996            }
1997            Expr::Yield(opt_expr) => {
1998                self.current().has_yield = true;
1999                match opt_expr {
2000                    Some(expr) => {
2001                        self.compile_expr(expr, dest)?;
2002                    }
2003                    None => {
2004                        self.current().emit_abx(Op::LoadNone, dest, 0, 0);
2005                    }
2006                }
2007                self.current().emit_abc(Op::Yield, dest, 0, 0, 0);
2008            }
2009            Expr::Call { function, args } => {
2010                self.compile_call(function, args, dest)?;
2011            }
2012            Expr::Pipe { left, right } => {
2013                self.compile_pipe(left, right, dest)?;
2014            }
2015            Expr::List(elements) => {
2016                if elements.is_empty() {
2017                    let start = self.current().next_register;
2018                    self.current().emit_abc(Op::NewList, dest, start, 0, 0);
2019                } else {
2020                    let start = self.current().next_register;
2021                    for el in elements {
2022                        let r = self.current().alloc_register();
2023                        self.compile_expr(el, r)?;
2024                    }
2025                    self.current()
2026                        .emit_abc(Op::NewList, dest, start, elements.len() as u8, 0);
2027                    for _ in elements {
2028                        self.current().free_register();
2029                    }
2030                }
2031            }
2032            Expr::Map(pairs) => {
2033                if pairs.is_empty() {
2034                    let start = self.current().next_register;
2035                    self.current().emit_abc(Op::NewMap, dest, start, 0, 0);
2036                } else {
2037                    let start = self.current().next_register;
2038                    for (key, val) in pairs {
2039                        let kr = self.current().alloc_register();
2040                        self.compile_expr(key, kr)?;
2041                        let vr = self.current().alloc_register();
2042                        self.compile_expr(val, vr)?;
2043                    }
2044                    self.current()
2045                        .emit_abc(Op::NewMap, dest, start, pairs.len() as u8, 0);
2046                    for _ in 0..pairs.len() * 2 {
2047                        self.current().free_register();
2048                    }
2049                }
2050            }
2051            Expr::Index { object, index } => {
2052                let obj_reg = self.current().alloc_register();
2053                let idx_reg = self.current().alloc_register();
2054                self.compile_expr(object, obj_reg)?;
2055                self.compile_expr(index, idx_reg)?;
2056                self.current()
2057                    .emit_abc(Op::GetIndex, dest, obj_reg, idx_reg, 0);
2058                self.current().free_register();
2059                self.current().free_register();
2060            }
2061            Expr::Block { stmts, expr } => {
2062                self.begin_scope();
2063                for stmt in stmts {
2064                    self.compile_stmt(stmt)?;
2065                }
2066                if let Some(e) = expr {
2067                    self.compile_expr(e, dest)?;
2068                } else {
2069                    self.current().emit_abx(Op::LoadNone, dest, 0, 0);
2070                }
2071                self.end_scope();
2072            }
2073            Expr::Case { arms } => {
2074                self.compile_case(arms, dest)?;
2075            }
2076            Expr::Match { subject, arms } => {
2077                self.compile_match(subject, arms, dest)?;
2078            }
2079            Expr::Closure { params, body, .. } => {
2080                self.compile_closure_expr(params, body, dest)?;
2081            }
2082            Expr::Range { start, end } => {
2083                // Compile to a builtin range() call
2084                let start_reg = self.current().alloc_register();
2085                let end_reg = self.current().alloc_register();
2086                self.compile_expr(start, start_reg)?;
2087                self.compile_expr(end, end_reg)?;
2088                // CallBuiltin Range with 2 args (ABx: dest, builtin_id; next: argc, first_arg)
2089                self.current()
2090                    .emit_abx(Op::CallBuiltin, dest, BuiltinId::Range as u16, 0);
2091                self.current().emit_abc(Op::Move, 2, start_reg, 0, 0);
2092                self.current().free_register();
2093                self.current().free_register();
2094            }
2095            Expr::NullCoalesce { expr, default } => {
2096                self.compile_expr(expr, dest)?;
2097                let skip_jump = self.current().current_pos();
2098                // If not None, skip default
2099                self.current().emit_abx(Op::JumpIfTrue, dest, 0, 0);
2100                self.compile_expr(default, dest)?;
2101                self.current().patch_jump(skip_jump);
2102            }
2103            Expr::Assign { target, value } => {
2104                if let Expr::Ident(name) = target.as_ref() {
2105                    self.compile_expr(value, dest)?;
2106                    if let Some(reg) = self.resolve_local(name) {
2107                        if reg != dest {
2108                            self.current().emit_abc(Op::Move, reg, dest, 0, 0);
2109                        }
2110                    } else if let Some(uv) = self.resolve_upvalue(name) {
2111                        self.current().emit_abc(Op::SetUpvalue, dest, uv, 0, 0);
2112                    } else {
2113                        let idx = self
2114                            .current()
2115                            .add_constant(Constant::String(Arc::from(name.as_str())));
2116                        self.current().emit_abx(Op::SetGlobal, dest, idx, 0);
2117                    }
2118                } else if let Expr::Index { object, index } = target.as_ref() {
2119                    // m["key"] = value or list[idx] = value
2120                    let obj_reg = self.current().alloc_register();
2121                    self.compile_expr(object, obj_reg)?;
2122                    let idx_reg = self.current().alloc_register();
2123                    self.compile_expr(index, idx_reg)?;
2124                    self.compile_expr(value, dest)?;
2125                    // SetIndex: a=value, b=object, c=index
2126                    self.current()
2127                        .emit_abc(Op::SetIndex, dest, obj_reg, idx_reg, 0);
2128                    // Write the modified object back to the variable
2129                    if let Expr::Ident(name) = object.as_ref() {
2130                        if let Some(reg) = self.resolve_local(name) {
2131                            self.current().emit_abc(Op::Move, reg, obj_reg, 0, 0);
2132                        } else {
2133                            let c_idx = self
2134                                .current()
2135                                .add_constant(Constant::String(Arc::from(name.as_str())));
2136                            self.current().emit_abx(Op::SetGlobal, obj_reg, c_idx, 0);
2137                        }
2138                    }
2139                    self.current().free_register(); // idx_reg
2140                    self.current().free_register(); // obj_reg
2141                } else if let Expr::Member { object, field } = target.as_ref() {
2142                    // s.field = value — struct field or map member assignment
2143                    if let Expr::Ident(name) = object.as_ref() {
2144                        let obj_reg = self.current().alloc_register();
2145                        self.compile_expr(object, obj_reg)?;
2146                        // Compile value
2147                        self.compile_expr(value, dest)?;
2148                        // Use SetIndex with string key for member assignment
2149                        let key_reg = self.current().alloc_register();
2150                        let key_idx = self
2151                            .current()
2152                            .add_constant(Constant::String(Arc::from(field.as_str())));
2153                        self.current().emit_abx(Op::LoadConst, key_reg, key_idx, 0);
2154                        self.current()
2155                            .emit_abc(Op::SetIndex, dest, obj_reg, key_reg, 0);
2156                        // Write back
2157                        if let Some(reg) = self.resolve_local(name) {
2158                            self.current().emit_abc(Op::Move, reg, obj_reg, 0, 0);
2159                        } else {
2160                            let c_idx = self
2161                                .current()
2162                                .add_constant(Constant::String(Arc::from(name.as_str())));
2163                            self.current().emit_abx(Op::SetGlobal, obj_reg, c_idx, 0);
2164                        }
2165                        self.current().free_register(); // key_reg
2166                        self.current().free_register(); // obj_reg
2167                    } else {
2168                        return Err(compile_err("Invalid assignment target".to_string()));
2169                    }
2170                } else {
2171                    return Err(compile_err("Invalid assignment target".to_string()));
2172                }
2173            }
2174            Expr::Member { object, field } => {
2175                let obj_reg = self.current().alloc_register();
2176                self.compile_expr(object, obj_reg)?;
2177                let field_idx = self
2178                    .current()
2179                    .add_constant(Constant::String(Arc::from(field.as_str())));
2180                self.current()
2181                    .emit_abc(Op::GetMember, dest, obj_reg, field_idx as u8, 0);
2182                self.current().free_register();
2183            }
2184            Expr::NamedArg { .. } => {
2185                // Named args are handled in call compilation
2186                self.current().emit_abx(Op::LoadNone, dest, 0, 0);
2187            }
2188            Expr::StructInit { name, fields } => {
2189                // Compile struct init: load type name, compile field values, emit NewStruct
2190                let name_idx = self
2191                    .current()
2192                    .add_constant(Constant::String(Arc::from(name.as_str())));
2193                let start = self.current().next_register;
2194                for (fname, fexpr) in fields {
2195                    let fname_reg = self.current().alloc_register();
2196                    let fname_idx = self
2197                        .current()
2198                        .add_constant(Constant::String(Arc::from(fname.as_str())));
2199                    self.current()
2200                        .emit_abx(Op::LoadConst, fname_reg, fname_idx, 0);
2201                    let fval_reg = self.current().alloc_register();
2202                    self.compile_expr(fexpr, fval_reg)?;
2203                }
2204                self.current()
2205                    .emit_abc(Op::NewStruct, dest, name_idx as u8, fields.len() as u8, 0);
2206                // Encode start register in next instruction
2207                self.current().emit_abc(Op::Move, start, 0, 0, 0);
2208                for _ in 0..fields.len() * 2 {
2209                    self.current().free_register();
2210                }
2211            }
2212            Expr::EnumVariant {
2213                enum_name,
2214                variant,
2215                args,
2216            } => {
2217                let full_name = format!("{enum_name}::{variant}");
2218                let name_idx = self
2219                    .current()
2220                    .add_constant(Constant::String(Arc::from(full_name.as_str())));
2221                let start = self.current().next_register;
2222                for arg in args {
2223                    let r = self.current().alloc_register();
2224                    self.compile_expr(arg, r)?;
2225                }
2226                self.current()
2227                    .emit_abc(Op::NewEnum, dest, name_idx as u8, start, 0);
2228                // Encode arg count
2229                self.current().emit_abc(Op::Move, args.len() as u8, 0, 0, 0);
2230                for _ in args {
2231                    self.current().free_register();
2232                }
2233            }
2234        }
2235        Ok(())
2236    }
2237
2238    fn compile_call(&mut self, function: &Expr, args: &[Expr], dest: u8) -> Result<(), TlError> {
2239        // Check for builtin calls
2240        if let Expr::Ident(name) = function
2241            && let Some(builtin_id) = BuiltinId::from_name(name)
2242        {
2243            return self.compile_builtin_call(builtin_id, args, dest);
2244        }
2245
2246        // Method call: obj.method(args) -> MethodCall
2247        if let Expr::Member { object, field } = function {
2248            let obj_reg = self.current().alloc_register();
2249            self.compile_expr(object, obj_reg)?;
2250            let method_idx = self
2251                .current()
2252                .add_constant(Constant::String(Arc::from(field.as_str())));
2253            let args_start = self.current().next_register;
2254            for arg in args {
2255                let r = self.current().alloc_register();
2256                self.compile_expr(arg, r)?;
2257            }
2258            self.current()
2259                .emit_abc(Op::MethodCall, dest, obj_reg, method_idx as u8, 0);
2260            // Next instruction: args_start, arg_count
2261            self.current()
2262                .emit_abc(Op::Move, args_start, args.len() as u8, 0, 0);
2263            for _ in args {
2264                self.current().free_register();
2265            }
2266            self.current().free_register(); // obj_reg
2267            return Ok(());
2268        }
2269
2270        // General function call
2271        let func_reg = self.current().alloc_register();
2272        self.compile_expr(function, func_reg)?;
2273
2274        let args_start = self.current().next_register;
2275        for arg in args {
2276            let r = self.current().alloc_register();
2277            self.compile_expr(arg, r)?;
2278        }
2279
2280        self.current()
2281            .emit_abc(Op::Call, func_reg, args_start, args.len() as u8, 0);
2282
2283        // Free arg registers
2284        for _ in args {
2285            self.current().free_register();
2286        }
2287
2288        // Move result from func_reg to dest
2289        if func_reg != dest {
2290            self.current().emit_abc(Op::Move, dest, func_reg, 0, 0);
2291        }
2292        self.current().free_register(); // func_reg
2293
2294        Ok(())
2295    }
2296
2297    fn compile_builtin_call(
2298        &mut self,
2299        builtin_id: BuiltinId,
2300        args: &[Expr],
2301        dest: u8,
2302    ) -> Result<(), TlError> {
2303        let args_start = self.current().next_register;
2304        for arg in args {
2305            let r = self.current().alloc_register();
2306            self.compile_expr(arg, r)?;
2307        }
2308
2309        self.current()
2310            .emit_abx(Op::CallBuiltin, dest, builtin_id as u16, 0);
2311        // Next instruction: A=arg_count, B=first_arg_reg
2312        self.current()
2313            .emit_abc(Op::Move, args.len() as u8, args_start, 0, 0);
2314
2315        for _ in args {
2316            self.current().free_register();
2317        }
2318
2319        Ok(())
2320    }
2321
2322    /// Emit instructions to mark the source of a pipe as moved (consumed).
2323    fn emit_pipe_move(&mut self, left: &Expr) {
2324        if let Expr::Ident(name) = left {
2325            if let Some(local_reg) = self.resolve_local(name) {
2326                self.current().emit_abc(Op::LoadMoved, local_reg, 0, 0, 0);
2327            } else {
2328                let moved_reg = self.current().alloc_register();
2329                self.current().emit_abc(Op::LoadMoved, moved_reg, 0, 0, 0);
2330                let idx = self
2331                    .current()
2332                    .add_constant(Constant::String(Arc::from(name.as_str())));
2333                self.current().emit_abx(Op::SetGlobal, moved_reg, idx, 0);
2334                self.current().free_register();
2335            }
2336        }
2337    }
2338
2339    /// Table operations recognized by the compiler for Op::TablePipe emission.
2340    /// Must match what the VM's handle_table_pipe expects for the legacy path.
2341    const TABLE_OPS: &'static [&'static str] = &[
2342        "filter",
2343        "select",
2344        "sort",
2345        "with",
2346        "aggregate",
2347        "join",
2348        "head",
2349        "limit",
2350        "collect",
2351        "show",
2352        "describe",
2353        "write_csv",
2354        "write_parquet",
2355        "sample",
2356        "window",
2357        "union",
2358    ];
2359
2360    fn compile_pipe(&mut self, left: &Expr, right: &Expr, dest: u8) -> Result<(), TlError> {
2361        // Try IR-optimized path for table pipe chains
2362        if let Some((source, ops)) = self.try_extract_table_pipe_chain(left, right)
2363            && let Ok(plan) = tl_ir::build_query_plan(&source, &ops)
2364        {
2365            let optimized = tl_ir::optimize(plan);
2366            let lowered = tl_ir::lower_plan(&optimized);
2367            return self.emit_optimized_plan(left, &source, &lowered, dest);
2368        }
2369        // Fall back to legacy path
2370        self.compile_pipe_legacy(left, right, dest)
2371    }
2372
2373    /// Try to extract a flat table pipe chain from nested Pipe expressions.
2374    /// Returns (source_expr, [(op_name, args)]) if all ops are table ops.
2375    fn try_extract_table_pipe_chain(
2376        &self,
2377        left: &Expr,
2378        right: &Expr,
2379    ) -> Option<(Expr, Vec<(String, Vec<Expr>)>)> {
2380        let mut ops = Vec::new();
2381
2382        // Extract op from the right side
2383        let (fname, args) = match right {
2384            Expr::Call { function, args } => {
2385                if let Expr::Ident(fname) = function.as_ref() {
2386                    (fname.as_str(), args.clone())
2387                } else {
2388                    return None;
2389                }
2390            }
2391            _ => return None,
2392        };
2393
2394        if !Self::TABLE_OPS.contains(&fname) {
2395            return None;
2396        }
2397
2398        ops.push((fname.to_string(), args));
2399
2400        // Walk left side to collect more pipe ops
2401        let source = self.extract_pipe_chain_left(left, &mut ops)?;
2402
2403        // Reverse because we collected from right to left
2404        ops.reverse();
2405
2406        Some((source, ops))
2407    }
2408
2409    /// Recursively extract pipe chain ops from the left side.
2410    fn extract_pipe_chain_left(
2411        &self,
2412        expr: &Expr,
2413        ops: &mut Vec<(String, Vec<Expr>)>,
2414    ) -> Option<Expr> {
2415        match expr {
2416            Expr::Pipe { left, right } => {
2417                // Extract the op from this pipe's right side
2418                let (fname, args) = match right.as_ref() {
2419                    Expr::Call { function, args } => {
2420                        if let Expr::Ident(fname) = function.as_ref() {
2421                            (fname.as_str(), args.clone())
2422                        } else {
2423                            return None;
2424                        }
2425                    }
2426                    _ => return None,
2427                };
2428
2429                if !Self::TABLE_OPS.contains(&fname) {
2430                    return None;
2431                }
2432
2433                ops.push((fname.to_string(), args));
2434
2435                // Continue extracting from the left side
2436                self.extract_pipe_chain_left(left, ops)
2437            }
2438            // Base case: not a pipe — this is the source expression
2439            other => Some(other.clone()),
2440        }
2441    }
2442
2443    /// Emit optimized table pipe operations from the lowered IR plan.
2444    fn emit_optimized_plan(
2445        &mut self,
2446        _original_left: &Expr,
2447        source: &Expr,
2448        lowered_ops: &[(String, Vec<Expr>)],
2449        dest: u8,
2450    ) -> Result<(), TlError> {
2451        let left_reg = self.current().alloc_register();
2452        self.compile_expr(source, left_reg)?;
2453
2454        // Mark the source variable as moved (consumed by pipe)
2455        self.emit_pipe_move(source);
2456
2457        // Emit TablePipe for each lowered operation
2458        for (op_name, args) in lowered_ops {
2459            let args_idx = self
2460                .current()
2461                .add_constant(Constant::AstExprList(args.clone()));
2462            let op_idx = self
2463                .current()
2464                .add_constant(Constant::String(Arc::from(op_name.as_str())));
2465            self.current()
2466                .emit_abc(Op::TablePipe, left_reg, op_idx as u8, args_idx as u8, 0);
2467        }
2468
2469        if left_reg != dest {
2470            self.current().emit_abc(Op::Move, dest, left_reg, 0, 0);
2471        }
2472        self.current().free_register();
2473        Ok(())
2474    }
2475
2476    fn compile_pipe_legacy(&mut self, left: &Expr, right: &Expr, dest: u8) -> Result<(), TlError> {
2477        let left_reg = self.current().alloc_register();
2478        self.compile_expr(left, left_reg)?;
2479
2480        // Mark the source variable as moved (consumed by pipe)
2481        self.emit_pipe_move(left);
2482
2483        match right {
2484            Expr::Call { function, args } => {
2485                if let Expr::Ident(fname) = function.as_ref() {
2486                    // Check if this is a table operation
2487                    if Self::TABLE_OPS.contains(&fname.as_str()) {
2488                        // Store AST args as constant for table pipe
2489                        let args_idx = self
2490                            .current()
2491                            .add_constant(Constant::AstExprList(args.clone()));
2492                        let op_idx = self
2493                            .current()
2494                            .add_constant(Constant::String(Arc::from(fname.as_str())));
2495                        self.current().emit_abc(
2496                            Op::TablePipe,
2497                            left_reg,
2498                            op_idx as u8,
2499                            args_idx as u8,
2500                            0,
2501                        );
2502                        if left_reg != dest {
2503                            self.current().emit_abc(Op::Move, dest, left_reg, 0, 0);
2504                        }
2505                        self.current().free_register();
2506                        return Ok(());
2507                    }
2508
2509                    // Check if it's a builtin
2510                    if let Some(builtin_id) = BuiltinId::from_name(fname) {
2511                        // Pipe into builtin: left becomes first arg
2512                        let args_start = left_reg; // reuse left_reg as first arg
2513                        for arg in args {
2514                            let r = self.current().alloc_register();
2515                            self.compile_expr(arg, r)?;
2516                        }
2517                        self.current()
2518                            .emit_abx(Op::CallBuiltin, dest, builtin_id as u16, 0);
2519                        self.current()
2520                            .emit_abc(Op::Move, (args.len() + 1) as u8, args_start, 0, 0);
2521                        for _ in args {
2522                            self.current().free_register();
2523                        }
2524                        self.current().free_register(); // left_reg
2525                        return Ok(());
2526                    }
2527                }
2528
2529                // General: left_val becomes first arg to call
2530                let func_reg = self.current().alloc_register();
2531                self.compile_expr(function, func_reg)?;
2532
2533                // Move left to be the first arg
2534                let args_start = self.current().next_register;
2535                let first_arg = self.current().alloc_register();
2536                self.current().emit_abc(Op::Move, first_arg, left_reg, 0, 0);
2537
2538                for arg in args {
2539                    let r = self.current().alloc_register();
2540                    self.compile_expr(arg, r)?;
2541                }
2542
2543                let total_args = args.len() + 1;
2544                self.current()
2545                    .emit_abc(Op::Call, func_reg, args_start, total_args as u8, 0);
2546
2547                for _ in 0..total_args {
2548                    self.current().free_register();
2549                }
2550
2551                if func_reg != dest {
2552                    self.current().emit_abc(Op::Move, dest, func_reg, 0, 0);
2553                }
2554                self.current().free_register(); // func_reg
2555            }
2556            Expr::Ident(name) => {
2557                // Pipe into named function with left as only arg
2558                if let Some(builtin_id) = BuiltinId::from_name(name) {
2559                    self.current()
2560                        .emit_abx(Op::CallBuiltin, dest, builtin_id as u16, 0);
2561                    self.current().emit_abc(Op::Move, 1, left_reg, 0, 0);
2562                } else {
2563                    let func_reg = self.current().alloc_register();
2564                    let name_idx = self
2565                        .current()
2566                        .add_constant(Constant::String(Arc::from(name.as_str())));
2567                    self.current()
2568                        .emit_abx(Op::GetGlobal, func_reg, name_idx, 0);
2569
2570                    let args_start = self.current().next_register;
2571                    let first_arg = self.current().alloc_register();
2572                    self.current().emit_abc(Op::Move, first_arg, left_reg, 0, 0);
2573
2574                    self.current()
2575                        .emit_abc(Op::Call, func_reg, args_start, 1, 0);
2576                    if func_reg != dest {
2577                        self.current().emit_abc(Op::Move, dest, func_reg, 0, 0);
2578                    }
2579                    self.current().free_register(); // first_arg
2580                    self.current().free_register(); // func_reg
2581                }
2582            }
2583            _ => {
2584                return Err(compile_err(
2585                    "Right side of |> must be a function call".to_string(),
2586                ));
2587            }
2588        }
2589
2590        self.current().free_register(); // left_reg
2591        Ok(())
2592    }
2593
2594    fn compile_case(&mut self, arms: &[MatchArm], dest: u8) -> Result<(), TlError> {
2595        let mut end_jumps = Vec::new();
2596        let mut has_default = false;
2597
2598        for arm in arms {
2599            if let Some(ref guard) = arm.guard {
2600                // Conditional arm: guard is the boolean condition
2601                let cond_reg = self.current().alloc_register();
2602                self.compile_expr(guard, cond_reg)?;
2603                let jump_false = self.current().current_pos();
2604                self.current().emit_abx(Op::JumpIfFalse, cond_reg, 0, 0);
2605                self.current().free_register();
2606
2607                self.compile_expr(&arm.body, dest)?;
2608                let jump_end = self.current().current_pos();
2609                self.current().emit_abx(Op::Jump, 0, 0, 0);
2610                end_jumps.push(jump_end);
2611
2612                self.current().patch_jump(jump_false);
2613            } else {
2614                // Default arm (Wildcard without guard)
2615                self.compile_expr(&arm.body, dest)?;
2616                has_default = true;
2617                break;
2618            }
2619        }
2620
2621        if !has_default {
2622            self.current().emit_abx(Op::LoadNone, dest, 0, 0);
2623        }
2624
2625        for pos in end_jumps {
2626            self.current().patch_jump(pos);
2627        }
2628
2629        Ok(())
2630    }
2631
2632    fn compile_match(
2633        &mut self,
2634        subject: &Expr,
2635        arms: &[MatchArm],
2636        dest: u8,
2637    ) -> Result<(), TlError> {
2638        let subj_reg = self.current().alloc_register();
2639        self.compile_expr(subject, subj_reg)?;
2640
2641        let mut end_jumps = Vec::new();
2642        let mut has_unconditional = false;
2643
2644        for arm in arms {
2645            // Check if this arm is unconditional (wildcard or unguarded binding)
2646            let is_unconditional = match &arm.pattern {
2647                Pattern::Wildcard => true,
2648                Pattern::Binding(_) if arm.guard.is_none() => true,
2649                Pattern::Struct { name: None, .. } if arm.guard.is_none() => true,
2650                _ => false,
2651            };
2652
2653            self.compile_match_arm(arm, subj_reg, dest, &mut end_jumps)?;
2654
2655            if is_unconditional {
2656                has_unconditional = true;
2657                break; // No point compiling more arms
2658            }
2659        }
2660
2661        if !has_unconditional {
2662            // No match — load None
2663            self.current().emit_abx(Op::LoadNone, dest, 0, 0);
2664            self.current().free_register(); // subj_reg
2665        }
2666
2667        for pos in end_jumps {
2668            self.current().patch_jump(pos);
2669        }
2670
2671        Ok(())
2672    }
2673
2674    /// Compile a single match arm against the subject in subj_reg.
2675    /// Returns Ok(true) if this arm is an unconditional match (wildcard/unguarded binding).
2676    fn compile_match_arm(
2677        &mut self,
2678        arm: &MatchArm,
2679        subj_reg: u8,
2680        dest: u8,
2681        end_jumps: &mut Vec<usize>,
2682    ) -> Result<(), TlError> {
2683        match &arm.pattern {
2684            Pattern::Wildcard => {
2685                self.compile_expr(&arm.body, dest)?;
2686                self.current().free_register(); // subj_reg
2687                for pos in end_jumps.drain(..) {
2688                    self.current().patch_jump(pos);
2689                }
2690                // Return via a special mechanism — we'll handle this by checking after the call
2691                // Actually, we can't return early from the caller. Instead, emit jump to end.
2692                // The caller won't emit more arms after wildcard since the loop will end.
2693                return Ok(());
2694            }
2695            Pattern::Binding(name) => {
2696                let local = self.add_local(name.clone());
2697                self.current().emit_abc(Op::Move, local, subj_reg, 0, 0);
2698
2699                if let Some(guard) = &arm.guard {
2700                    let guard_reg = self.current().alloc_register();
2701                    self.compile_expr(guard, guard_reg)?;
2702                    let jf = self.current().current_pos();
2703                    self.current().emit_abx(Op::JumpIfFalse, guard_reg, 0, 0);
2704                    self.current().free_register();
2705
2706                    self.compile_expr(&arm.body, dest)?;
2707                    let jump_end = self.current().current_pos();
2708                    self.current().emit_abx(Op::Jump, 0, 0, 0);
2709                    end_jumps.push(jump_end);
2710                    self.current().patch_jump(jf);
2711                } else {
2712                    // Unconditional match
2713                    self.compile_expr(&arm.body, dest)?;
2714                    self.current().free_register(); // subj_reg
2715                    for pos in end_jumps.drain(..) {
2716                        self.current().patch_jump(pos);
2717                    }
2718                    return Ok(());
2719                }
2720            }
2721            Pattern::Literal(expr) => {
2722                let pat_reg = self.current().alloc_register();
2723                self.compile_expr(expr, pat_reg)?;
2724                let result_reg = self.current().alloc_register();
2725                self.current()
2726                    .emit_abc(Op::TestMatch, subj_reg, pat_reg, result_reg, 0);
2727
2728                let jump_false = self.current().current_pos();
2729                self.current().emit_abx(Op::JumpIfFalse, result_reg, 0, 0);
2730                self.current().free_register(); // result_reg
2731                self.current().free_register(); // pat_reg
2732
2733                self.compile_guard_and_body(arm, dest, end_jumps, jump_false)?;
2734            }
2735            Pattern::Enum {
2736                type_name: _,
2737                variant,
2738                args,
2739            } => {
2740                let variant_const = self
2741                    .current()
2742                    .add_constant(Constant::String(Arc::from(variant.as_str())));
2743                let result_reg = self.current().alloc_register();
2744                self.current().emit_abc(
2745                    Op::MatchEnum,
2746                    subj_reg,
2747                    variant_const as u8,
2748                    result_reg,
2749                    0,
2750                );
2751
2752                let jump_false = self.current().current_pos();
2753                self.current().emit_abx(Op::JumpIfFalse, result_reg, 0, 0);
2754                self.current().free_register(); // result_reg
2755
2756                // Bind destructured fields
2757                for (i, arg_pat) in args.iter().enumerate() {
2758                    match arg_pat {
2759                        Pattern::Binding(name) => {
2760                            let local = self.add_local(name.clone());
2761                            self.current()
2762                                .emit_abc(Op::ExtractField, local, subj_reg, i as u8, 0);
2763                        }
2764                        Pattern::Wildcard => {}
2765                        _ => {}
2766                    }
2767                }
2768
2769                self.compile_guard_and_body(arm, dest, end_jumps, jump_false)?;
2770            }
2771            Pattern::Struct {
2772                name: struct_name,
2773                fields,
2774            } => {
2775                // For named structs, check struct type via TestMatch
2776                let jump_false = if let Some(sname) = struct_name {
2777                    let name_const = self
2778                        .current()
2779                        .add_constant(Constant::String(Arc::from(sname.as_str())));
2780                    let name_reg = self.current().alloc_register();
2781                    self.current()
2782                        .emit_abx(Op::LoadConst, name_reg, name_const, 0);
2783                    let result_reg = self.current().alloc_register();
2784                    self.current()
2785                        .emit_abc(Op::TestMatch, subj_reg, name_reg, result_reg, 0);
2786                    let jf = self.current().current_pos();
2787                    self.current().emit_abx(Op::JumpIfFalse, result_reg, 0, 0);
2788                    self.current().free_register(); // result_reg
2789                    self.current().free_register(); // name_reg
2790                    jf
2791                } else {
2792                    usize::MAX
2793                };
2794
2795                // Extract named fields
2796                for field in fields {
2797                    let fname_const = self
2798                        .current()
2799                        .add_constant(Constant::String(Arc::from(field.name.as_str())));
2800                    match &field.pattern {
2801                        None | Some(Pattern::Binding(_)) => {
2802                            let bind_name = match &field.pattern {
2803                                Some(Pattern::Binding(n)) => n.clone(),
2804                                _ => field.name.clone(),
2805                            };
2806                            let local = self.add_local(bind_name);
2807                            self.current().emit_abc(
2808                                Op::ExtractNamedField,
2809                                local,
2810                                subj_reg,
2811                                fname_const as u8,
2812                                0,
2813                            );
2814                        }
2815                        Some(Pattern::Wildcard) => {}
2816                        _ => {
2817                            let local = self.add_local(field.name.clone());
2818                            self.current().emit_abc(
2819                                Op::ExtractNamedField,
2820                                local,
2821                                subj_reg,
2822                                fname_const as u8,
2823                                0,
2824                            );
2825                        }
2826                    }
2827                }
2828
2829                if jump_false != usize::MAX {
2830                    self.compile_guard_and_body(arm, dest, end_jumps, jump_false)?;
2831                } else {
2832                    // No type check — just guard + body
2833                    if let Some(guard) = &arm.guard {
2834                        let guard_reg = self.current().alloc_register();
2835                        self.compile_expr(guard, guard_reg)?;
2836                        let gj = self.current().current_pos();
2837                        self.current().emit_abx(Op::JumpIfFalse, guard_reg, 0, 0);
2838                        self.current().free_register();
2839
2840                        self.compile_expr(&arm.body, dest)?;
2841                        let jump_end = self.current().current_pos();
2842                        self.current().emit_abx(Op::Jump, 0, 0, 0);
2843                        end_jumps.push(jump_end);
2844                        self.current().patch_jump(gj);
2845                    } else {
2846                        // Unconditional struct match
2847                        self.compile_expr(&arm.body, dest)?;
2848                        self.current().free_register(); // subj_reg
2849                        for pos in end_jumps.drain(..) {
2850                            self.current().patch_jump(pos);
2851                        }
2852                        return Ok(());
2853                    }
2854                }
2855            }
2856            Pattern::List { elements, rest } => {
2857                // Check list length
2858                let len_builtin_reg = self.current().alloc_register();
2859                // Call len(subj)
2860                self.current()
2861                    .emit_abx(Op::CallBuiltin, len_builtin_reg, BuiltinId::Len as u16, 0);
2862                self.current().emit_abc(Op::Move, 1, subj_reg, 0, 0); // arg count = 1
2863
2864                let expected_len_reg = self.current().alloc_register();
2865                let len_val = elements.len() as i64;
2866                let len_const = self.current().add_constant(Constant::Int(len_val));
2867                self.current()
2868                    .emit_abx(Op::LoadConst, expected_len_reg, len_const, 0);
2869
2870                let cmp_reg = self.current().alloc_register();
2871                if rest.is_some() {
2872                    self.current()
2873                        .emit_abc(Op::Gte, cmp_reg, len_builtin_reg, expected_len_reg, 0);
2874                } else {
2875                    self.current()
2876                        .emit_abc(Op::Eq, cmp_reg, len_builtin_reg, expected_len_reg, 0);
2877                }
2878                let jump_false = self.current().current_pos();
2879                self.current().emit_abx(Op::JumpIfFalse, cmp_reg, 0, 0);
2880                self.current().free_register(); // cmp_reg
2881                self.current().free_register(); // expected_len_reg
2882                self.current().free_register(); // len_builtin_reg
2883
2884                // Extract elements by index
2885                for (i, elem_pat) in elements.iter().enumerate() {
2886                    match elem_pat {
2887                        Pattern::Binding(name) => {
2888                            let local = self.add_local(name.clone());
2889                            self.current()
2890                                .emit_abc(Op::ExtractField, local, subj_reg, i as u8, 0);
2891                        }
2892                        Pattern::Wildcard => {}
2893                        _ => {}
2894                    }
2895                }
2896
2897                // Rest pattern
2898                if let Some(rest_name) = rest {
2899                    let local = self.add_local(rest_name.clone());
2900                    self.current().emit_abc(
2901                        Op::ExtractField,
2902                        local,
2903                        subj_reg,
2904                        (elements.len() as u8) | 0x80,
2905                        0,
2906                    );
2907                }
2908
2909                self.compile_guard_and_body(arm, dest, end_jumps, jump_false)?;
2910            }
2911            Pattern::Or(patterns) => {
2912                let mut match_jumps = Vec::new();
2913
2914                for sub_pat in patterns {
2915                    match sub_pat {
2916                        Pattern::Literal(expr) => {
2917                            let pat_reg = self.current().alloc_register();
2918                            self.compile_expr(expr, pat_reg)?;
2919                            let result_reg = self.current().alloc_register();
2920                            self.current().emit_abc(
2921                                Op::TestMatch,
2922                                subj_reg,
2923                                pat_reg,
2924                                result_reg,
2925                                0,
2926                            );
2927                            let jump_true = self.current().current_pos();
2928                            self.current().emit_abx(Op::JumpIfTrue, result_reg, 0, 0);
2929                            match_jumps.push(jump_true);
2930                            self.current().free_register(); // result_reg
2931                            self.current().free_register(); // pat_reg
2932                        }
2933                        Pattern::Enum { variant, .. } => {
2934                            let variant_const = self
2935                                .current()
2936                                .add_constant(Constant::String(Arc::from(variant.as_str())));
2937                            let result_reg = self.current().alloc_register();
2938                            self.current().emit_abc(
2939                                Op::MatchEnum,
2940                                subj_reg,
2941                                variant_const as u8,
2942                                result_reg,
2943                                0,
2944                            );
2945                            let jump_true = self.current().current_pos();
2946                            self.current().emit_abx(Op::JumpIfTrue, result_reg, 0, 0);
2947                            match_jumps.push(jump_true);
2948                            self.current().free_register();
2949                        }
2950                        Pattern::Wildcard | Pattern::Binding(_) => {
2951                            let jump = self.current().current_pos();
2952                            self.current().emit_abx(Op::Jump, 0, 0, 0);
2953                            match_jumps.push(jump);
2954                        }
2955                        _ => {}
2956                    }
2957                }
2958
2959                // None matched — skip body
2960                let jump_skip = self.current().current_pos();
2961                self.current().emit_abx(Op::Jump, 0, 0, 0);
2962
2963                // Patch match jumps to body
2964                for jt in &match_jumps {
2965                    self.current().patch_jump(*jt);
2966                }
2967
2968                // Guard + body
2969                if let Some(guard) = &arm.guard {
2970                    let guard_reg = self.current().alloc_register();
2971                    self.compile_expr(guard, guard_reg)?;
2972                    let gj = self.current().current_pos();
2973                    self.current().emit_abx(Op::JumpIfFalse, guard_reg, 0, 0);
2974                    self.current().free_register();
2975
2976                    self.compile_expr(&arm.body, dest)?;
2977                    let jump_end = self.current().current_pos();
2978                    self.current().emit_abx(Op::Jump, 0, 0, 0);
2979                    end_jumps.push(jump_end);
2980                    self.current().patch_jump(gj);
2981                } else {
2982                    self.compile_expr(&arm.body, dest)?;
2983                    let jump_end = self.current().current_pos();
2984                    self.current().emit_abx(Op::Jump, 0, 0, 0);
2985                    end_jumps.push(jump_end);
2986                }
2987
2988                self.current().patch_jump(jump_skip);
2989            }
2990        }
2991        Ok(())
2992    }
2993
2994    /// Helper: compile guard check (if any) + body, patch jump_false
2995    fn compile_guard_and_body(
2996        &mut self,
2997        arm: &MatchArm,
2998        dest: u8,
2999        end_jumps: &mut Vec<usize>,
3000        jump_false: usize,
3001    ) -> Result<(), TlError> {
3002        let guard_jump = if let Some(guard) = &arm.guard {
3003            let guard_reg = self.current().alloc_register();
3004            self.compile_expr(guard, guard_reg)?;
3005            let jf = self.current().current_pos();
3006            self.current().emit_abx(Op::JumpIfFalse, guard_reg, 0, 0);
3007            self.current().free_register();
3008            Some(jf)
3009        } else {
3010            None
3011        };
3012
3013        self.compile_expr(&arm.body, dest)?;
3014        let jump_end = self.current().current_pos();
3015        self.current().emit_abx(Op::Jump, 0, 0, 0);
3016        end_jumps.push(jump_end);
3017
3018        self.current().patch_jump(jump_false);
3019        if let Some(gj) = guard_jump {
3020            self.current().patch_jump(gj);
3021        }
3022        Ok(())
3023    }
3024
3025    /// Compile a string with interpolation. Parses `{var}` segments and emits
3026    /// code to load each variable and concatenate with the literal parts.
3027    fn compile_string_interpolation(&mut self, s: &str, dest: u8) -> Result<(), TlError> {
3028        // Parse the string into segments: literal parts and variable references
3029        let mut segments: Vec<StringSegment> = Vec::new();
3030        let mut chars = s.chars().peekable();
3031        let mut current_literal = String::new();
3032
3033        while let Some(ch) = chars.next() {
3034            if ch == '{' {
3035                let mut var_name = String::new();
3036                let mut depth = 1;
3037                for c in chars.by_ref() {
3038                    if c == '{' {
3039                        depth += 1;
3040                    } else if c == '}' {
3041                        depth -= 1;
3042                        if depth == 0 {
3043                            break;
3044                        }
3045                    }
3046                    var_name.push(c);
3047                }
3048                if !current_literal.is_empty() {
3049                    segments.push(StringSegment::Literal(std::mem::take(&mut current_literal)));
3050                }
3051                segments.push(StringSegment::Variable(var_name));
3052            } else if ch == '\\' {
3053                match chars.next() {
3054                    Some('n') => current_literal.push('\n'),
3055                    Some('t') => current_literal.push('\t'),
3056                    Some('\\') => current_literal.push('\\'),
3057                    Some('"') => current_literal.push('"'),
3058                    Some(c) => {
3059                        current_literal.push('\\');
3060                        current_literal.push(c);
3061                    }
3062                    None => current_literal.push('\\'),
3063                }
3064            } else {
3065                current_literal.push(ch);
3066            }
3067        }
3068        if !current_literal.is_empty() {
3069            segments.push(StringSegment::Literal(current_literal));
3070        }
3071
3072        if segments.is_empty() {
3073            // Empty string
3074            let idx = self.current().add_constant(Constant::String(Arc::from("")));
3075            self.current().emit_abx(Op::LoadConst, dest, idx, 0);
3076            return Ok(());
3077        }
3078
3079        // If no interpolation, just load the constant
3080        if segments.len() == 1
3081            && let StringSegment::Literal(ref lit) = segments[0]
3082        {
3083            let idx = self
3084                .current()
3085                .add_constant(Constant::String(Arc::from(lit.as_str())));
3086            self.current().emit_abx(Op::LoadConst, dest, idx, 0);
3087            return Ok(());
3088        }
3089
3090        // Compile first segment into dest
3091        self.compile_string_segment(&segments[0], dest)?;
3092
3093        // For each subsequent segment, compile it and concat with dest
3094        for segment in &segments[1..] {
3095            let tmp = self.current().alloc_register();
3096            self.compile_string_segment(segment, tmp)?;
3097            self.current().emit_abc(Op::Concat, dest, dest, tmp, 0);
3098            self.current().free_register();
3099        }
3100
3101        Ok(())
3102    }
3103
3104    fn compile_string_segment(&mut self, seg: &StringSegment, dest: u8) -> Result<(), TlError> {
3105        match seg {
3106            StringSegment::Literal(s) => {
3107                let idx = self
3108                    .current()
3109                    .add_constant(Constant::String(Arc::from(s.as_str())));
3110                self.current().emit_abx(Op::LoadConst, dest, idx, 0);
3111            }
3112            StringSegment::Variable(name) => {
3113                // Compile as an identifier lookup, then convert to string via builtin Str
3114                let var_reg = self.current().alloc_register();
3115                if let Some(reg) = self.resolve_local(name) {
3116                    if reg != var_reg {
3117                        self.current().emit_abc(Op::Move, var_reg, reg, 0, 0);
3118                    }
3119                } else if let Some(uv) = self.resolve_upvalue(name) {
3120                    self.current().emit_abc(Op::GetUpvalue, var_reg, uv, 0, 0);
3121                } else {
3122                    let idx = self
3123                        .current()
3124                        .add_constant(Constant::String(Arc::from(name.as_str())));
3125                    self.current().emit_abx(Op::GetGlobal, var_reg, idx, 0);
3126                }
3127                // Convert to string via CallBuiltin Str
3128                self.current()
3129                    .emit_abx(Op::CallBuiltin, dest, BuiltinId::Str as u16, 0);
3130                self.current().emit_abc(Op::Move, 1, var_reg, 0, 0); // 1 arg
3131                self.current().free_register(); // var_reg
3132            }
3133        }
3134        Ok(())
3135    }
3136
3137    fn compile_use(&mut self, item: &UseItem) -> Result<(), TlError> {
3138        // Encode use as an import operation. The VM handles resolution.
3139        // C=0xAB is a magic marker to distinguish from classic import (C=0).
3140        let reg = self.current().alloc_register();
3141        match item {
3142            UseItem::Single(path) => {
3143                let path_str = path.join(".");
3144                let path_idx = self
3145                    .current()
3146                    .add_constant(Constant::String(Arc::from(path_str.as_str())));
3147                self.current().emit_abx(Op::Import, reg, path_idx, 0);
3148                // A=0 (unused), B=kind=0 (Single), C=0xAB (use marker)
3149                self.current().emit_abc(Op::Move, 0, 0, 0xAB, 0);
3150            }
3151            UseItem::Group(prefix, names) => {
3152                let path_str = format!("{}.{{{}}}", prefix.join("."), names.join(","));
3153                let path_idx = self
3154                    .current()
3155                    .add_constant(Constant::String(Arc::from(path_str.as_str())));
3156                self.current().emit_abx(Op::Import, reg, path_idx, 0);
3157                self.current().emit_abc(Op::Move, 0, 1, 0xAB, 0); // B=1 Group
3158            }
3159            UseItem::Wildcard(path) => {
3160                let path_str = format!("{}.*", path.join("."));
3161                let path_idx = self
3162                    .current()
3163                    .add_constant(Constant::String(Arc::from(path_str.as_str())));
3164                self.current().emit_abx(Op::Import, reg, path_idx, 0);
3165                self.current().emit_abc(Op::Move, 0, 2, 0xAB, 0); // B=2 Wildcard
3166            }
3167            UseItem::Aliased(path, alias) => {
3168                let path_str = path.join(".");
3169                let path_idx = self
3170                    .current()
3171                    .add_constant(Constant::String(Arc::from(path_str.as_str())));
3172                let alias_idx = self
3173                    .current()
3174                    .add_constant(Constant::String(Arc::from(alias.as_str())));
3175                self.current().emit_abx(Op::Import, reg, path_idx, 0);
3176                self.current()
3177                    .emit_abc(Op::Move, alias_idx as u8, 3, 0xAB, 0); // B=3 Aliased
3178            }
3179        }
3180        self.current().free_register();
3181        Ok(())
3182    }
3183}
3184
3185enum StringSegment {
3186    Literal(String),
3187    Variable(String),
3188}
3189
3190/// Compile a TL program into a top-level Prototype.
3191/// Pass source text to enable line number tracking in bytecode.
3192pub fn compile(program: &Program) -> Result<Prototype, TlError> {
3193    compile_with_source(program, "")
3194}
3195
3196/// Compile a TL program with source text for line number tracking.
3197pub fn compile_with_source(program: &Program, source: &str) -> Result<Prototype, TlError> {
3198    let line_offsets = Compiler::build_line_offsets(source);
3199    let mut compiler = Compiler {
3200        states: vec![CompilerState::new("<main>".to_string())],
3201        line_offsets,
3202        current_line: 0,
3203    };
3204
3205    let stmts = &program.statements;
3206    let mut last_expr_reg: Option<u8> = None;
3207
3208    for (i, stmt) in stmts.iter().enumerate() {
3209        let is_last = i == stmts.len() - 1;
3210        // Update line tracking for this statement
3211        let line = compiler.line_of(stmt.span.start);
3212        compiler.current_line = line;
3213        compiler.current().current_line = line;
3214        match &stmt.kind {
3215            StmtKind::Expr(expr) if is_last => {
3216                // Last statement is an expression — keep register for implicit return
3217                let reg = compiler.current().alloc_register();
3218                compiler.compile_expr(expr, reg)?;
3219                last_expr_reg = Some(reg);
3220            }
3221            StmtKind::If {
3222                condition,
3223                then_body,
3224                else_ifs,
3225                else_body,
3226            } if is_last && else_body.is_some() => {
3227                let dest = compiler.current().alloc_register();
3228                compiler.compile_if_as_expr(condition, then_body, else_ifs, else_body, dest)?;
3229                last_expr_reg = Some(dest);
3230            }
3231            _ => {
3232                compiler.compile_stmt(stmt)?;
3233            }
3234        }
3235    }
3236
3237    // Record top-level locals for module export support
3238    {
3239        let state = &compiler.states[0];
3240        let top_locals: Vec<(String, u8)> = state
3241            .locals
3242            .iter()
3243            .filter(|l| l.depth == 0)
3244            .map(|l| (l.name.clone(), l.register))
3245            .collect();
3246        compiler.states[0].proto.top_level_locals = top_locals;
3247    }
3248
3249    // Add implicit return
3250    let state = &mut compiler.states[0];
3251    let needs_return = if state.proto.code.is_empty() {
3252        true
3253    } else {
3254        let last = *state.proto.code.last().unwrap();
3255        decode_op(last) != Op::Return
3256    };
3257    if needs_return {
3258        if let Some(reg) = last_expr_reg {
3259            state.emit_abc(Op::Return, reg, 0, 0, 0);
3260        } else {
3261            let reg = state.alloc_register();
3262            state.emit_abx(Op::LoadNone, reg, 0, 0);
3263            state.emit_abc(Op::Return, reg, 0, 0, 0);
3264        }
3265    }
3266
3267    let state = compiler.states.pop().unwrap();
3268    Ok(state.proto)
3269}
3270
3271#[cfg(test)]
3272mod tests {
3273    use super::*;
3274    use tl_parser::parse;
3275
3276    #[test]
3277    fn test_compile_int_literal() {
3278        let program = parse("42").unwrap();
3279        let proto = compile(&program).unwrap();
3280        assert!(!proto.code.is_empty());
3281        assert!(
3282            proto
3283                .constants
3284                .iter()
3285                .any(|c| matches!(c, Constant::Int(42)))
3286        );
3287    }
3288
3289    #[test]
3290    fn test_compile_add() {
3291        // With constant folding, 1 + 2 now folds to 3
3292        let program = parse("1 + 2").unwrap();
3293        let proto = compile(&program).unwrap();
3294        assert!(!proto.code.is_empty());
3295        // Should fold to constant 3
3296        assert!(
3297            proto
3298                .constants
3299                .iter()
3300                .any(|c| matches!(c, Constant::Int(3)))
3301        );
3302    }
3303
3304    #[test]
3305    fn test_compile_function() {
3306        let program = parse("fn add(a, b) { a + b }").unwrap();
3307        let proto = compile(&program).unwrap();
3308        // Should have a Closure instruction
3309        let has_closure = proto
3310            .code
3311            .iter()
3312            .any(|&inst| decode_op(inst) == Op::Closure);
3313        assert!(has_closure);
3314    }
3315
3316    #[test]
3317    fn test_compile_closure() {
3318        let program = parse("let f = (x) => x * 2").unwrap();
3319        let proto = compile(&program).unwrap();
3320        let has_closure = proto
3321            .code
3322            .iter()
3323            .any(|&inst| decode_op(inst) == Op::Closure);
3324        assert!(has_closure);
3325    }
3326
3327    // ── Phase 13: Constant Folding Tests ──────────────────────
3328
3329    #[test]
3330    fn test_fold_int_addition() {
3331        // 2 + 3 should fold to 5 at compile time
3332        let program = parse("2 + 3").unwrap();
3333        let proto = compile(&program).unwrap();
3334        assert!(
3335            proto
3336                .constants
3337                .iter()
3338                .any(|c| matches!(c, Constant::Int(5)))
3339        );
3340        // Should NOT have an Add instruction
3341        let has_add = proto.code.iter().any(|&inst| decode_op(inst) == Op::Add);
3342        assert!(
3343            !has_add,
3344            "Folded expression should not have Add instruction"
3345        );
3346    }
3347
3348    #[test]
3349    fn test_fold_float_multiplication() {
3350        let program = parse("2.0 * 3.0").unwrap();
3351        let proto = compile(&program).unwrap();
3352        assert!(
3353            proto
3354                .constants
3355                .iter()
3356                .any(|c| matches!(c, Constant::Float(f) if (*f - 6.0).abs() < f64::EPSILON))
3357        );
3358    }
3359
3360    #[test]
3361    fn test_fold_string_concatenation() {
3362        let program = parse("\"hello\" + \" world\"").unwrap();
3363        let proto = compile(&program).unwrap();
3364        assert!(
3365            proto
3366                .constants
3367                .iter()
3368                .any(|c| matches!(c, Constant::String(s) if s.as_ref() == "hello world"))
3369        );
3370    }
3371
3372    #[test]
3373    fn test_fold_negation() {
3374        let program = parse("-42").unwrap();
3375        let proto = compile(&program).unwrap();
3376        assert!(
3377            proto
3378                .constants
3379                .iter()
3380                .any(|c| matches!(c, Constant::Int(-42)))
3381        );
3382    }
3383
3384    #[test]
3385    fn test_fold_not() {
3386        let program = parse("not true").unwrap();
3387        let proto = compile(&program).unwrap();
3388        // Should emit LoadFalse
3389        let has_load_false = proto
3390            .code
3391            .iter()
3392            .any(|&inst| decode_op(inst) == Op::LoadFalse);
3393        assert!(has_load_false, "'not true' should fold to false");
3394    }
3395
3396    #[test]
3397    fn test_fold_nested_arithmetic() {
3398        // 2 + 3 * 4 should fold to 14
3399        let program = parse("2 + 3 * 4").unwrap();
3400        let proto = compile(&program).unwrap();
3401        assert!(
3402            proto
3403                .constants
3404                .iter()
3405                .any(|c| matches!(c, Constant::Int(14)))
3406        );
3407    }
3408
3409    #[test]
3410    fn test_no_fold_division_by_zero() {
3411        // 10 / 0 should NOT fold (defer to runtime error)
3412        let program = parse("10 / 0").unwrap();
3413        let proto = compile(&program).unwrap();
3414        // Should still have a Div instruction
3415        let has_div = proto.code.iter().any(|&inst| decode_op(inst) == Op::Div);
3416        assert!(has_div, "Division by zero should not be folded");
3417    }
3418
3419    #[test]
3420    fn test_no_fold_variable_reference() {
3421        // x + 1 should NOT fold (contains variable)
3422        let program = parse("let x = 5\nx + 1").unwrap();
3423        let proto = compile(&program).unwrap();
3424        let has_add = proto.code.iter().any(|&inst| decode_op(inst) == Op::Add);
3425        assert!(has_add, "Expression with variables should not be folded");
3426    }
3427
3428    #[test]
3429    fn test_no_fold_interpolated_string() {
3430        // String with { should not fold (interpolation)
3431        let program = parse("let x = 5\n\"{x} hello\"").unwrap();
3432        let proto = compile(&program).unwrap();
3433        // Should have string concatenation (Concat or multiple LoadConst + Add)
3434        // The key point is it doesn't try to fold the interpolated string
3435        assert!(!proto.code.is_empty());
3436    }
3437
3438    #[test]
3439    fn test_fold_transparent_to_runtime() {
3440        // Folded expressions should produce the same result as unfolded
3441        // This is verified by running both in the VM
3442        use crate::Vm;
3443        let program = parse("2 + 3 * 4").unwrap();
3444        let proto = compile(&program).unwrap();
3445        let mut vm = Vm::new();
3446        let result = vm.execute(&proto).unwrap();
3447        assert_eq!(result.to_string(), "14");
3448    }
3449
3450    // ── Phase 13: Dead Code Elimination Tests ─────────────────
3451
3452    #[test]
3453    fn test_dce_after_return() {
3454        // Code after return should not be compiled
3455        let program = parse("fn f() {\n  return 1\n  print(\"dead\")\n}").unwrap();
3456        let proto = compile(&program).unwrap();
3457        // Find the function prototype
3458        let fn_proto = proto
3459            .constants
3460            .iter()
3461            .find_map(|c| {
3462                if let Constant::Prototype(p) = c {
3463                    Some(p.clone())
3464                } else {
3465                    None
3466                }
3467            })
3468            .expect("should have function prototype");
3469        // The function should NOT have a CallBuiltin (print) after the Return
3470        let return_pos = fn_proto
3471            .code
3472            .iter()
3473            .position(|&inst| decode_op(inst) == Op::Return);
3474        assert!(return_pos.is_some(), "Should have a Return instruction");
3475        // No CallBuiltin after Return
3476        let after_return: Vec<_> = fn_proto.code[return_pos.unwrap() + 1..]
3477            .iter()
3478            .filter(|&&inst| decode_op(inst) == Op::CallBuiltin)
3479            .collect();
3480        assert!(
3481            after_return.is_empty(),
3482            "Should not have CallBuiltin after Return"
3483        );
3484    }
3485
3486    #[test]
3487    fn test_dce_after_break() {
3488        // Code after break in loop should not be compiled
3489        let program =
3490            parse("fn f() {\n  while true {\n    break\n    print(\"dead\")\n  }\n}").unwrap();
3491        let proto = compile(&program).unwrap();
3492        let fn_proto = proto
3493            .constants
3494            .iter()
3495            .find_map(|c| {
3496                if let Constant::Prototype(p) = c {
3497                    Some(p.clone())
3498                } else {
3499                    None
3500                }
3501            })
3502            .expect("should have function prototype");
3503        // Count CallBuiltin instructions — should be 0 (dead code eliminated)
3504        let call_builtins: Vec<_> = fn_proto
3505            .code
3506            .iter()
3507            .filter(|&&inst| decode_op(inst) == Op::CallBuiltin)
3508            .collect();
3509        assert!(
3510            call_builtins.is_empty(),
3511            "Should not have CallBuiltin after break"
3512        );
3513    }
3514
3515    #[test]
3516    fn test_dce_after_continue() {
3517        // Code after continue in loop should not be compiled
3518        let program =
3519            parse("fn f() {\n  while true {\n    continue\n    print(\"dead\")\n  }\n}").unwrap();
3520        let proto = compile(&program).unwrap();
3521        let fn_proto = proto
3522            .constants
3523            .iter()
3524            .find_map(|c| {
3525                if let Constant::Prototype(p) = c {
3526                    Some(p.clone())
3527                } else {
3528                    None
3529                }
3530            })
3531            .expect("should have function prototype");
3532        let call_builtins: Vec<_> = fn_proto
3533            .code
3534            .iter()
3535            .filter(|&&inst| decode_op(inst) == Op::CallBuiltin)
3536            .collect();
3537        assert!(
3538            call_builtins.is_empty(),
3539            "Should not have CallBuiltin after continue"
3540        );
3541    }
3542
3543    #[test]
3544    fn test_dce_if_both_branches_return() {
3545        // If both branches return, code after if is dead
3546        let program = parse("fn f(x) {\n  if x {\n    return 1\n  } else {\n    return 2\n  }\n  print(\"dead\")\n}").unwrap();
3547        let proto = compile(&program).unwrap();
3548        let fn_proto = proto
3549            .constants
3550            .iter()
3551            .find_map(|c| {
3552                if let Constant::Prototype(p) = c {
3553                    Some(p.clone())
3554                } else {
3555                    None
3556                }
3557            })
3558            .expect("should have function prototype");
3559        let call_builtins: Vec<_> = fn_proto
3560            .code
3561            .iter()
3562            .filter(|&&inst| decode_op(inst) == Op::CallBuiltin)
3563            .collect();
3564        assert!(
3565            call_builtins.is_empty(),
3566            "Should not have CallBuiltin after if where both branches return"
3567        );
3568    }
3569
3570    #[test]
3571    fn test_dce_if_one_branch_returns() {
3572        // If only one branch returns, code after if is NOT dead
3573        let program =
3574            parse("fn f(x) {\n  if x {\n    return 1\n  }\n  print(\"alive\")\n  return 0\n}")
3575                .unwrap();
3576        let proto = compile(&program).unwrap();
3577        let fn_proto = proto
3578            .constants
3579            .iter()
3580            .find_map(|c| {
3581                if let Constant::Prototype(p) = c {
3582                    Some(p.clone())
3583                } else {
3584                    None
3585                }
3586            })
3587            .expect("should have function prototype");
3588        let call_builtins: Vec<_> = fn_proto
3589            .code
3590            .iter()
3591            .filter(|&&inst| decode_op(inst) == Op::CallBuiltin)
3592            .collect();
3593        assert!(
3594            !call_builtins.is_empty(),
3595            "Should have CallBuiltin after if where only one branch returns"
3596        );
3597    }
3598
3599    #[test]
3600    fn test_dce_nested_function() {
3601        // Dead code in inner function doesn't affect outer
3602        let program = parse("fn outer() {\n  fn inner() {\n    return 1\n    print(\"dead\")\n  }\n  print(\"alive\")\n}").unwrap();
3603        let proto = compile(&program).unwrap();
3604        // The outer function should have CallBuiltin (print("alive"))
3605        let outer_fn = proto
3606            .constants
3607            .iter()
3608            .find_map(|c| {
3609                if let Constant::Prototype(p) = c {
3610                    if p.name == "outer" {
3611                        Some(p.clone())
3612                    } else {
3613                        None
3614                    }
3615                } else {
3616                    None
3617                }
3618            })
3619            .expect("should have outer function");
3620        let call_builtins: Vec<_> = outer_fn
3621            .code
3622            .iter()
3623            .filter(|&&inst| decode_op(inst) == Op::CallBuiltin)
3624            .collect();
3625        assert!(
3626            !call_builtins.is_empty(),
3627            "Outer function should have CallBuiltin"
3628        );
3629    }
3630
3631    #[test]
3632    fn test_dce_try_catch_independent() {
3633        // Return in try doesn't make catch dead
3634        let program =
3635            parse("fn f() {\n  try {\n    return 1\n  } catch e {\n    print(e)\n  }\n}").unwrap();
3636        let proto = compile(&program).unwrap();
3637        let fn_proto = proto
3638            .constants
3639            .iter()
3640            .find_map(|c| {
3641                if let Constant::Prototype(p) = c {
3642                    Some(p.clone())
3643                } else {
3644                    None
3645                }
3646            })
3647            .expect("should have function prototype");
3648        // Catch block should still have its instructions compiled
3649        let call_builtins: Vec<_> = fn_proto
3650            .code
3651            .iter()
3652            .filter(|&&inst| decode_op(inst) == Op::CallBuiltin)
3653            .collect();
3654        assert!(
3655            !call_builtins.is_empty(),
3656            "Catch block should not be eliminated by try body return"
3657        );
3658    }
3659
3660    #[test]
3661    fn test_dce_existing_tests_unaffected() {
3662        // Verify existing programs still compile and run correctly
3663        use crate::Vm;
3664        let tests = [
3665            ("let x = 42\nx", "42"),
3666            ("fn add(a, b) { a + b }\nadd(1, 2)", "3"),
3667            ("if true { 1 } else { 2 }", "1"),
3668        ];
3669        for (src, expected) in tests {
3670            let program = parse(src).unwrap();
3671            let proto = compile(&program).unwrap();
3672            let mut vm = Vm::new();
3673            let result = vm.execute(&proto).unwrap();
3674            assert_eq!(result.to_string(), expected, "Failed for: {src}");
3675        }
3676    }
3677}