Skip to main content

shape_vm/compiler/
helpers.rs

1//! Helper methods for bytecode compilation
2
3use crate::borrow_checker::BorrowMode;
4use crate::bytecode::{BuiltinFunction, Constant, Instruction, OpCode, Operand};
5use crate::type_tracking::{NumericType, StorageHint, TypeTracker, VariableTypeInfo};
6use shape_ast::ast::{Spanned, TypeAnnotation};
7use shape_ast::error::{Result, ShapeError};
8use std::collections::{BTreeSet, HashMap};
9
10use super::{BytecodeCompiler, DropKind, ParamPassMode};
11
12/// Extract the core error message from a ShapeError, stripping redundant
13/// "Type error:", "Runtime error:", "Compile error:", etc. prefixes that
14/// thiserror's Display impl adds.  This prevents nested comptime errors
15/// from accumulating multiple prefixes like
16/// "Runtime error: Comptime block evaluation failed: Runtime error: …".
17pub(crate) fn strip_error_prefix(e: &ShapeError) -> String {
18    let msg = e.to_string();
19    // Known prefixes added by thiserror Display
20    const PREFIXES: &[&str] = &[
21        "Runtime error: ",
22        "Type error: ",
23        "Semantic error: ",
24        "Parse error: ",
25        "VM error: ",
26        "Lexical error: ",
27    ];
28    let mut s = msg.as_str();
29    // Strip at most 3 layers of prefix to handle deep nesting
30    for _ in 0..3 {
31        let mut stripped = false;
32        for prefix in PREFIXES {
33            if let Some(rest) = s.strip_prefix(prefix) {
34                s = rest;
35                stripped = true;
36                break;
37            }
38        }
39        // Also strip the comptime wrapping messages themselves
40        const COMPTIME_PREFIXES: &[&str] = &[
41            "Comptime block evaluation failed: ",
42            "Comptime handler execution failed: ",
43            "Comptime block directive processing failed: ",
44        ];
45        for prefix in COMPTIME_PREFIXES {
46            if let Some(rest) = s.strip_prefix(prefix) {
47                s = rest;
48                stripped = true;
49                break;
50            }
51        }
52        if !stripped {
53            break;
54        }
55    }
56    s.to_string()
57}
58
59impl BytecodeCompiler {
60    fn scalar_type_name_from_numeric(numeric_type: NumericType) -> &'static str {
61        match numeric_type {
62            NumericType::Int | NumericType::IntWidth(_) => "int",
63            NumericType::Number => "number",
64            NumericType::Decimal => "decimal",
65        }
66    }
67
68    fn array_type_name_from_numeric(numeric_type: NumericType) -> &'static str {
69        match numeric_type {
70            NumericType::Int | NumericType::IntWidth(_) => "Vec<int>",
71            NumericType::Number => "Vec<number>",
72            NumericType::Decimal => "Vec<decimal>",
73        }
74    }
75
76    fn is_array_type_name(type_name: Option<&str>) -> bool {
77        matches!(type_name, Some(name) if name.starts_with("Vec<") && name.ends_with('>'))
78    }
79
80    /// Convert a source annotation to a tracked type name when we have a
81    /// canonical runtime representation for it.
82    pub(super) fn tracked_type_name_from_annotation(type_ann: &TypeAnnotation) -> Option<String> {
83        match type_ann {
84            TypeAnnotation::Basic(name) | TypeAnnotation::Reference(name) => Some(name.clone()),
85            TypeAnnotation::Array(inner) => Some(format!("Vec<{}>", inner.to_type_string())),
86            // Keep the canonical Vec<T> naming even if a Generic slips through.
87            TypeAnnotation::Generic { name, args } if name == "Vec" && args.len() == 1 => {
88                Some(format!("Vec<{}>", args[0].to_type_string()))
89            }
90            TypeAnnotation::Generic { name, args } if name == "Mat" && args.len() == 1 => {
91                Some(format!("Mat<{}>", args[0].to_type_string()))
92            }
93            _ => None,
94        }
95    }
96
97    /// Mark a local/module binding slot as an array with numeric element type.
98    ///
99    /// Used by `x = x.push(value)` in-place mutation lowering so subsequent
100    /// indexed reads can recover numeric hints.
101    pub(super) fn mark_slot_as_numeric_array(
102        &mut self,
103        slot: u16,
104        is_local: bool,
105        numeric_type: NumericType,
106    ) {
107        let info =
108            VariableTypeInfo::named(Self::array_type_name_from_numeric(numeric_type).to_string());
109        if is_local {
110            self.type_tracker.set_local_type(slot, info);
111        } else {
112            self.type_tracker.set_binding_type(slot, info);
113        }
114    }
115
116    /// Mark a local/module binding slot as a scalar numeric type.
117    pub(super) fn mark_slot_as_numeric_scalar(
118        &mut self,
119        slot: u16,
120        is_local: bool,
121        numeric_type: NumericType,
122    ) {
123        let info =
124            VariableTypeInfo::named(Self::scalar_type_name_from_numeric(numeric_type).to_string());
125        if is_local {
126            self.type_tracker.set_local_type(slot, info);
127        } else {
128            self.type_tracker.set_binding_type(slot, info);
129        }
130    }
131
132    /// Seed numeric hints from expression usage in arithmetic contexts.
133    ///
134    /// - `x` in numeric arithmetic becomes scalar numeric (`int`/`number`/`decimal`).
135    /// - `arr[i]` implies `arr` is `Vec<numeric>`.
136    pub(super) fn seed_numeric_hint_from_expr(
137        &mut self,
138        expr: &shape_ast::ast::Expr,
139        numeric_type: NumericType,
140    ) {
141        match expr {
142            shape_ast::ast::Expr::Identifier(name, _) => {
143                if let Some(local_idx) = self.resolve_local(name) {
144                    self.mark_slot_as_numeric_scalar(local_idx, true, numeric_type);
145                    return;
146                }
147                let scoped_name = self
148                    .resolve_scoped_module_binding_name(name)
149                    .unwrap_or_else(|| name.to_string());
150                if let Some(binding_idx) = self.module_bindings.get(&scoped_name).copied() {
151                    self.mark_slot_as_numeric_scalar(binding_idx, false, numeric_type);
152                }
153            }
154            shape_ast::ast::Expr::IndexAccess {
155                object,
156                end_index: None,
157                ..
158            } => {
159                if let shape_ast::ast::Expr::Identifier(name, _) = object.as_ref() {
160                    if let Some(local_idx) = self.resolve_local(name) {
161                        self.mark_slot_as_numeric_array(local_idx, true, numeric_type);
162                        return;
163                    }
164                    let scoped_name = self
165                        .resolve_scoped_module_binding_name(name)
166                        .unwrap_or_else(|| name.to_string());
167                    if let Some(binding_idx) = self.module_bindings.get(&scoped_name).copied() {
168                        self.mark_slot_as_numeric_array(binding_idx, false, numeric_type);
169                    }
170                }
171            }
172            _ => {}
173        }
174    }
175
176    fn recover_or_bail_with_null_placeholder(&mut self, err: ShapeError) -> Result<()> {
177        if self.should_recover_compile_diagnostics() {
178            self.errors.push(err);
179            self.emit(Instruction::simple(OpCode::PushNull));
180            Ok(())
181        } else {
182            Err(err)
183        }
184    }
185
186    pub(super) fn compile_expr_as_value_or_placeholder(
187        &mut self,
188        expr: &shape_ast::ast::Expr,
189    ) -> Result<()> {
190        match self.compile_expr(expr) {
191            Ok(()) => Ok(()),
192            Err(err) => self.recover_or_bail_with_null_placeholder(err),
193        }
194    }
195
196    /// Emit an instruction and return its index
197    /// Also records the current source line and file in debug info
198    pub(super) fn emit(&mut self, instruction: Instruction) -> usize {
199        let idx = self.program.emit(instruction);
200        // Record line number and file for this instruction
201        if self.current_line > 0 {
202            self.program.debug_info.line_numbers.push((
203                idx,
204                self.current_file_id,
205                self.current_line,
206            ));
207        }
208        idx
209    }
210
211    /// Emit a boolean constant
212    pub(super) fn emit_bool(&mut self, value: bool) {
213        let const_idx = self.program.add_constant(Constant::Bool(value));
214        self.emit(Instruction::new(
215            OpCode::PushConst,
216            Some(Operand::Const(const_idx)),
217        ));
218    }
219
220    /// Emit a unit constant
221    pub(super) fn emit_unit(&mut self) {
222        let const_idx = self.program.add_constant(Constant::Unit);
223        self.emit(Instruction::new(
224            OpCode::PushConst,
225            Some(Operand::Const(const_idx)),
226        ));
227    }
228
229    /// Emit a jump instruction with placeholder offset.
230    ///
231    /// When `opcode` is `JumpIfFalse` and the immediately preceding instruction
232    /// is a typed or trusted comparison (produces a known bool), upgrades to
233    /// `JumpIfFalseTrusted` which skips `is_truthy()` dispatch.
234    pub(super) fn emit_jump(&mut self, mut opcode: OpCode, dummy: i32) -> usize {
235        if opcode == OpCode::JumpIfFalse && self.last_instruction_produces_bool() {
236            opcode = OpCode::JumpIfFalseTrusted;
237        }
238        self.emit(Instruction::new(opcode, Some(Operand::Offset(dummy))))
239    }
240
241    /// Returns true if the last emitted instruction always produces a boolean result.
242    fn last_instruction_produces_bool(&self) -> bool {
243        self.program
244            .instructions
245            .last()
246            .map(|instr| {
247                matches!(
248                    instr.opcode,
249                    OpCode::GtInt
250                        | OpCode::GtNumber
251                        | OpCode::GtDecimal
252                        | OpCode::LtInt
253                        | OpCode::LtNumber
254                        | OpCode::LtDecimal
255                        | OpCode::GteInt
256                        | OpCode::GteNumber
257                        | OpCode::GteDecimal
258                        | OpCode::LteInt
259                        | OpCode::LteNumber
260                        | OpCode::LteDecimal
261                        | OpCode::EqInt
262                        | OpCode::EqNumber
263                        | OpCode::NeqInt
264                        | OpCode::NeqNumber
265                        | OpCode::Gt
266                        | OpCode::Lt
267                        | OpCode::Gte
268                        | OpCode::Lte
269                        | OpCode::Eq
270                        | OpCode::Neq
271                        | OpCode::Not
272                        | OpCode::GtIntTrusted
273                        | OpCode::LtIntTrusted
274                        | OpCode::GteIntTrusted
275                        | OpCode::LteIntTrusted
276                        | OpCode::GtNumberTrusted
277                        | OpCode::LtNumberTrusted
278                        | OpCode::GteNumberTrusted
279                        | OpCode::LteNumberTrusted
280                )
281            })
282            .unwrap_or(false)
283    }
284
285    /// Patch a jump instruction with the correct offset
286    pub(super) fn patch_jump(&mut self, jump_idx: usize) {
287        let offset = self.program.current_offset() as i32 - jump_idx as i32 - 1;
288        self.program.instructions[jump_idx] = Instruction::new(
289            self.program.instructions[jump_idx].opcode,
290            Some(Operand::Offset(offset)),
291        );
292    }
293
294    /// Compile function call arguments, enabling `&` reference expressions.
295    ///
296    /// Each call's arguments get their own borrow region so that borrows from
297    /// `&` references are released after the call returns. This matches Rust's
298    /// semantics: temporary borrows from function arguments don't persist beyond
299    /// the call. Sequential calls like `inc(&a); inc(&a)` are correctly allowed.
300    pub(super) fn compile_call_args(
301        &mut self,
302        args: &[shape_ast::ast::Expr],
303        expected_param_modes: Option<&[ParamPassMode]>,
304    ) -> Result<Vec<(u16, u16)>> {
305        let saved = self.in_call_args;
306        let saved_mode = self.current_call_arg_borrow_mode;
307        self.in_call_args = true;
308        self.borrow_checker.enter_region();
309        self.call_arg_module_binding_ref_writebacks.push(Vec::new());
310
311        let mut first_error: Option<ShapeError> = None;
312        for (idx, arg) in args.iter().enumerate() {
313            let pass_mode = expected_param_modes
314                .and_then(|modes| modes.get(idx).copied())
315                .unwrap_or(ParamPassMode::ByValue);
316            self.current_call_arg_borrow_mode = match pass_mode {
317                ParamPassMode::ByRefExclusive => Some(BorrowMode::Exclusive),
318                ParamPassMode::ByRefShared => Some(BorrowMode::Shared),
319                ParamPassMode::ByValue => None,
320            };
321
322            let arg_result = match pass_mode {
323                ParamPassMode::ByRefExclusive | ParamPassMode::ByRefShared => {
324                    let borrow_mode = if pass_mode.is_exclusive() {
325                        BorrowMode::Exclusive
326                    } else {
327                        BorrowMode::Shared
328                    };
329                    if matches!(arg, shape_ast::ast::Expr::Reference { .. }) {
330                        self.compile_expr(arg)
331                    } else {
332                        self.compile_implicit_reference_arg(arg, borrow_mode)
333                    }
334                }
335                ParamPassMode::ByValue => {
336                    if let shape_ast::ast::Expr::Reference { span, .. } = arg {
337                        let message = if expected_param_modes.is_some() {
338                            "[B0004] unexpected `&` argument: target parameter is not a reference parameter".to_string()
339                        } else {
340                            "[B0004] cannot pass `&` to a callable value without a declared reference contract; \
341                             call a named function with known parameter modes or add an explicit callable type"
342                                .to_string()
343                        };
344                        Err(ShapeError::SemanticError {
345                            message,
346                            location: Some(self.span_to_source_location(*span)),
347                        })
348                    } else {
349                        self.compile_expr(arg)
350                    }
351                }
352            };
353
354            if let Err(err) = arg_result {
355                if self.should_recover_compile_diagnostics() {
356                    self.errors.push(err);
357                    // Keep stack arity consistent for downstream call codegen.
358                    self.emit(Instruction::simple(OpCode::PushNull));
359                    continue;
360                }
361                first_error = Some(err);
362                break;
363            }
364        }
365
366        self.current_call_arg_borrow_mode = saved_mode;
367        self.borrow_checker.exit_region();
368        self.in_call_args = saved;
369        let writebacks = self
370            .call_arg_module_binding_ref_writebacks
371            .pop()
372            .unwrap_or_default();
373        if let Some(err) = first_error {
374            Err(err)
375        } else {
376            Ok(writebacks)
377        }
378    }
379
380    pub(super) fn current_arg_borrow_mode(&self) -> BorrowMode {
381        self.current_call_arg_borrow_mode
382            .unwrap_or(BorrowMode::Exclusive)
383    }
384
385    pub(super) fn record_call_arg_module_binding_writeback(
386        &mut self,
387        local: u16,
388        module_binding: u16,
389    ) {
390        if let Some(stack) = self.call_arg_module_binding_ref_writebacks.last_mut() {
391            stack.push((local, module_binding));
392        }
393    }
394
395    fn compile_implicit_reference_arg(
396        &mut self,
397        arg: &shape_ast::ast::Expr,
398        mode: BorrowMode,
399    ) -> Result<()> {
400        use shape_ast::ast::Expr;
401        match arg {
402            Expr::Identifier(name, span) => self.compile_reference_identifier(name, *span, mode),
403            _ if mode == BorrowMode::Exclusive => Err(ShapeError::SemanticError {
404                message: "[B0004] mutable reference arguments must be simple variables".to_string(),
405                location: Some(self.span_to_source_location(arg.span())),
406            }),
407            _ => {
408                self.compile_expr(arg)?;
409                let temp = self.declare_temp_local("__arg_ref_")?;
410                self.emit(Instruction::new(
411                    OpCode::StoreLocal,
412                    Some(Operand::Local(temp)),
413                ));
414                let source_loc = self.span_to_source_location(arg.span());
415                self.borrow_checker.create_borrow(
416                    temp,
417                    temp,
418                    mode,
419                    arg.span(),
420                    Some(source_loc),
421                )?;
422                self.emit(Instruction::new(
423                    OpCode::MakeRef,
424                    Some(Operand::Local(temp)),
425                ));
426                Ok(())
427            }
428        }
429    }
430
431    pub(super) fn compile_reference_identifier(
432        &mut self,
433        name: &str,
434        span: shape_ast::ast::Span,
435        mode: BorrowMode,
436    ) -> Result<()> {
437        if let Some(local_idx) = self.resolve_local(name) {
438            // Reject exclusive borrows of const variables
439            if mode == BorrowMode::Exclusive && self.const_locals.contains(&local_idx) {
440                return Err(ShapeError::SemanticError {
441                    message: format!(
442                        "Cannot pass const variable '{}' by exclusive reference",
443                        name
444                    ),
445                    location: Some(self.span_to_source_location(span)),
446                });
447            }
448            // Reject exclusive borrows of immutable `let` variables
449            if mode == BorrowMode::Exclusive && self.immutable_locals.contains(&local_idx) {
450                return Err(ShapeError::SemanticError {
451                    message: format!(
452                        "Cannot borrow immutable variable '{}' as exclusive (&mut). Use `let mut` or `var` for mutable bindings",
453                        name
454                    ),
455                    location: Some(self.span_to_source_location(span)),
456                });
457            }
458            if self.ref_locals.contains(&local_idx) {
459                // Forward an existing reference parameter by value (TAG_REF).
460                self.emit(Instruction::new(
461                    OpCode::LoadLocal,
462                    Some(Operand::Local(local_idx)),
463                ));
464                return Ok(());
465            }
466            let source_loc = self.span_to_source_location(span);
467            self.borrow_checker
468                .create_borrow(local_idx, local_idx, mode, span, Some(source_loc))
469                .map_err(|e| match e {
470                    ShapeError::SemanticError { message, location } => {
471                        let user_msg = message
472                            .replace(&format!("(slot {})", local_idx), &format!("'{}'", name));
473                        ShapeError::SemanticError {
474                            message: user_msg,
475                            location,
476                        }
477                    }
478                    other => other,
479                })?;
480            self.emit(Instruction::new(
481                OpCode::MakeRef,
482                Some(Operand::Local(local_idx)),
483            ));
484            Ok(())
485        } else if let Some(scoped_name) = self.resolve_scoped_module_binding_name(name) {
486            let Some(&binding_idx) = self.module_bindings.get(&scoped_name) else {
487                return Err(ShapeError::SemanticError {
488                    message: format!(
489                        "[B0004] reference argument must be a local or module_binding variable, got '{}'",
490                        name
491                    ),
492                    location: Some(self.span_to_source_location(span)),
493                });
494            };
495            // Reject exclusive borrows of const module bindings
496            if mode == BorrowMode::Exclusive && self.const_module_bindings.contains(&binding_idx) {
497                return Err(ShapeError::SemanticError {
498                    message: format!(
499                        "Cannot pass const variable '{}' by exclusive reference",
500                        name
501                    ),
502                    location: Some(self.span_to_source_location(span)),
503                });
504            }
505            // Reject exclusive borrows of immutable `let` module bindings
506            if mode == BorrowMode::Exclusive
507                && self.immutable_module_bindings.contains(&binding_idx)
508            {
509                return Err(ShapeError::SemanticError {
510                    message: format!(
511                        "Cannot borrow immutable variable '{}' as exclusive (&mut). Use `let mut` or `var` for mutable bindings",
512                        name
513                    ),
514                    location: Some(self.span_to_source_location(span)),
515                });
516            }
517            // Borrow module_bindings via a local shadow and write it back after the call.
518            let shadow_local = self.declare_temp_local("__module_binding_ref_shadow_")?;
519            self.emit(Instruction::new(
520                OpCode::LoadModuleBinding,
521                Some(Operand::ModuleBinding(binding_idx)),
522            ));
523            self.emit(Instruction::new(
524                OpCode::StoreLocal,
525                Some(Operand::Local(shadow_local)),
526            ));
527            let source_loc = self.span_to_source_location(span);
528            self.borrow_checker.create_borrow(
529                shadow_local,
530                shadow_local,
531                mode,
532                span,
533                Some(source_loc),
534            )?;
535            self.emit(Instruction::new(
536                OpCode::MakeRef,
537                Some(Operand::Local(shadow_local)),
538            ));
539            self.record_call_arg_module_binding_writeback(shadow_local, binding_idx);
540            Ok(())
541        } else if let Some(func_idx) = self.find_function(name) {
542            // Function name passed as reference argument: create a temporary local
543            // with the function constant and make a reference to it.
544            let temp = self.declare_temp_local("__fn_ref_")?;
545            let const_idx = self
546                .program
547                .add_constant(Constant::Function(func_idx as u16));
548            self.emit(Instruction::new(
549                OpCode::PushConst,
550                Some(Operand::Const(const_idx)),
551            ));
552            self.emit(Instruction::new(
553                OpCode::StoreLocal,
554                Some(Operand::Local(temp)),
555            ));
556            let source_loc = self.span_to_source_location(span);
557            self.borrow_checker
558                .create_borrow(temp, temp, mode, span, Some(source_loc))?;
559            self.emit(Instruction::new(
560                OpCode::MakeRef,
561                Some(Operand::Local(temp)),
562            ));
563            Ok(())
564        } else {
565            Err(ShapeError::SemanticError {
566                message: format!(
567                    "[B0004] reference argument must be a local or module_binding variable, got '{}'",
568                    name
569                ),
570                location: Some(self.span_to_source_location(span)),
571            })
572        }
573    }
574
575    /// Push a new scope
576    pub(super) fn push_scope(&mut self) {
577        self.locals.push(HashMap::new());
578        self.type_tracker.push_scope();
579        self.borrow_checker.enter_region();
580    }
581
582    /// Pop a scope
583    pub(super) fn pop_scope(&mut self) {
584        self.borrow_checker.exit_region();
585        self.locals.pop();
586        self.type_tracker.pop_scope();
587    }
588
589    /// Declare a local variable
590    pub(super) fn declare_local(&mut self, name: &str) -> Result<u16> {
591        let idx = self.next_local;
592        self.next_local += 1;
593
594        if let Some(scope) = self.locals.last_mut() {
595            scope.insert(name.to_string(), idx);
596        }
597
598        Ok(idx)
599    }
600
601    /// Resolve a local variable
602    pub(super) fn resolve_local(&self, name: &str) -> Option<u16> {
603        for scope in self.locals.iter().rev() {
604            if let Some(&idx) = scope.get(name) {
605                return Some(idx);
606            }
607        }
608        None
609    }
610
611    /// Declare a temporary local variable
612    pub(super) fn declare_temp_local(&mut self, prefix: &str) -> Result<u16> {
613        let name = format!("{}{}", prefix, self.next_local);
614        self.declare_local(&name)
615    }
616
617    /// Set type info for an existing local variable
618    pub(super) fn set_local_type_info(&mut self, slot: u16, type_name: &str) {
619        let info = if let Some(schema) = self.type_tracker.schema_registry().get(type_name) {
620            VariableTypeInfo::known(schema.id, type_name.to_string())
621        } else {
622            VariableTypeInfo::named(type_name.to_string())
623        };
624        self.type_tracker.set_local_type(slot, info);
625    }
626
627    /// Set type info for a module_binding variable
628    pub(super) fn set_module_binding_type_info(&mut self, slot: u16, type_name: &str) {
629        let info = if let Some(schema) = self.type_tracker.schema_registry().get(type_name) {
630            VariableTypeInfo::known(schema.id, type_name.to_string())
631        } else {
632            VariableTypeInfo::named(type_name.to_string())
633        };
634        self.type_tracker.set_binding_type(slot, info);
635    }
636
637    /// Capture local storage hints for a compiled function.
638    ///
639    /// Must be called before the function scope is popped so the type tracker still
640    /// has local slot metadata. Also populates the function's `FrameDescriptor` so
641    /// the verifier and executor can use per-slot type info for trusted opcodes.
642    pub(super) fn capture_function_local_storage_hints(&mut self, func_idx: usize) {
643        let Some(func) = self.program.functions.get(func_idx) else {
644            return;
645        };
646        let hints: Vec<StorageHint> = (0..func.locals_count)
647            .map(|slot| self.type_tracker.get_local_storage_hint(slot))
648            .collect();
649
650        // Populate FrameDescriptor on the function for trusted opcode verification.
651        let has_any_known = hints.iter().any(|h| *h != StorageHint::Unknown);
652        if has_any_known {
653            self.program.functions[func_idx].frame_descriptor = Some(
654                crate::type_tracking::FrameDescriptor::from_slots(hints.clone()),
655            );
656        }
657
658        if self.program.function_local_storage_hints.len() <= func_idx {
659            self.program
660                .function_local_storage_hints
661                .resize(func_idx + 1, Vec::new());
662        }
663        self.program.function_local_storage_hints[func_idx] = hints;
664    }
665
666    /// Populate program-level storage hints for top-level locals and module bindings.
667    pub(super) fn populate_program_storage_hints(&mut self) {
668        let top_hints: Vec<StorageHint> = (0..self.next_local)
669            .map(|slot| self.type_tracker.get_local_storage_hint(slot))
670            .collect();
671        self.program.top_level_local_storage_hints = top_hints.clone();
672
673        // Build top-level FrameDescriptor so JIT can use per-slot type info
674        let has_any_known = top_hints.iter().any(|h| *h != StorageHint::Unknown);
675        if has_any_known {
676            self.program.top_level_frame =
677                Some(crate::type_tracking::FrameDescriptor::from_slots(top_hints));
678        }
679
680        let mut module_binding_hints = vec![StorageHint::Unknown; self.module_bindings.len()];
681        for &idx in self.module_bindings.values() {
682            if let Some(slot) = module_binding_hints.get_mut(idx as usize) {
683                *slot = self.type_tracker.get_module_binding_storage_hint(idx);
684            }
685        }
686        self.program.module_binding_storage_hints = module_binding_hints;
687
688        if self.program.function_local_storage_hints.len() < self.program.functions.len() {
689            self.program
690                .function_local_storage_hints
691                .resize(self.program.functions.len(), Vec::new());
692        } else if self.program.function_local_storage_hints.len() > self.program.functions.len() {
693            self.program
694                .function_local_storage_hints
695                .truncate(self.program.functions.len());
696        }
697    }
698
699    /// Propagate the current expression's inferred type metadata to a target slot.
700    ///
701    /// Used by assignment sites to keep mutable locals/module_bindings typed when
702    /// safe, and to clear stale hints when assigning unknown/dynamic values.
703    pub(super) fn propagate_assignment_type_to_slot(
704        &mut self,
705        slot: u16,
706        is_local: bool,
707        allow_number_hint: bool,
708    ) {
709        if let Some(ref info) = self.last_expr_type_info {
710            if info.is_indexed()
711                || info.is_datatable()
712                || info.schema_id.is_some()
713                || Self::is_array_type_name(info.type_name.as_deref())
714            {
715                if is_local {
716                    self.type_tracker.set_local_type(slot, info.clone());
717                } else {
718                    self.type_tracker.set_binding_type(slot, info.clone());
719                }
720                return;
721            }
722        }
723
724        if let Some(schema_id) = self.last_expr_schema {
725            let schema_name = self
726                .type_tracker
727                .schema_registry()
728                .get_by_id(schema_id)
729                .map(|s| s.name.clone())
730                .unwrap_or_else(|| format!("__anon_{}", schema_id));
731            let info = VariableTypeInfo::known(schema_id, schema_name);
732            if is_local {
733                self.type_tracker.set_local_type(slot, info);
734            } else {
735                self.type_tracker.set_binding_type(slot, info);
736            }
737            return;
738        }
739
740        if let Some(numeric_type) = self.last_expr_numeric_type {
741            let (type_name, hint) = match numeric_type {
742                crate::type_tracking::NumericType::Int => ("int", StorageHint::Int64),
743                crate::type_tracking::NumericType::IntWidth(w) => {
744                    use shape_ast::IntWidth;
745                    let hint = match w {
746                        IntWidth::I8 => StorageHint::Int8,
747                        IntWidth::U8 => StorageHint::UInt8,
748                        IntWidth::I16 => StorageHint::Int16,
749                        IntWidth::U16 => StorageHint::UInt16,
750                        IntWidth::I32 => StorageHint::Int32,
751                        IntWidth::U32 => StorageHint::UInt32,
752                        IntWidth::U64 => StorageHint::UInt64,
753                    };
754                    (w.type_name(), hint)
755                }
756                crate::type_tracking::NumericType::Number => {
757                    if !allow_number_hint {
758                        if is_local {
759                            self.type_tracker
760                                .set_local_type(slot, VariableTypeInfo::unknown());
761                        } else {
762                            self.type_tracker
763                                .set_binding_type(slot, VariableTypeInfo::unknown());
764                        }
765                        return;
766                    }
767                    ("number", StorageHint::Float64)
768                }
769                // Decimal typed opcodes are not JIT-compiled yet.
770                crate::type_tracking::NumericType::Decimal => {
771                    if is_local {
772                        self.type_tracker
773                            .set_local_type(slot, VariableTypeInfo::unknown());
774                    } else {
775                        self.type_tracker
776                            .set_binding_type(slot, VariableTypeInfo::unknown());
777                    }
778                    return;
779                }
780            };
781            let info = VariableTypeInfo::with_storage(type_name.to_string(), hint);
782            if is_local {
783                self.type_tracker.set_local_type(slot, info);
784            } else {
785                self.type_tracker.set_binding_type(slot, info);
786            }
787            return;
788        }
789
790        // Assignment to an unknown/dynamic expression invalidates prior hints.
791        if is_local {
792            self.type_tracker
793                .set_local_type(slot, VariableTypeInfo::unknown());
794        } else {
795            self.type_tracker
796                .set_binding_type(slot, VariableTypeInfo::unknown());
797        }
798    }
799
800    /// Propagate current expression type metadata to an identifier target.
801    ///
802    /// Reference locals are skipped because assignment writes through to a pointee.
803    pub(super) fn propagate_assignment_type_to_identifier(&mut self, name: &str) {
804        if let Some(local_idx) = self.resolve_local(name) {
805            if self.ref_locals.contains(&local_idx) {
806                return;
807            }
808            self.propagate_assignment_type_to_slot(local_idx, true, true);
809            return;
810        }
811
812        let scoped_name = self
813            .resolve_scoped_module_binding_name(name)
814            .unwrap_or_else(|| name.to_string());
815        let binding_idx = self.get_or_create_module_binding(&scoped_name);
816        self.propagate_assignment_type_to_slot(binding_idx, false, true);
817    }
818
819    /// Get the type tracker (for external configuration)
820    pub fn type_tracker(&self) -> &TypeTracker {
821        &self.type_tracker
822    }
823
824    /// Get mutable type tracker (for registering types)
825    pub fn type_tracker_mut(&mut self) -> &mut TypeTracker {
826        &mut self.type_tracker
827    }
828
829    /// Resolve a column name to its index using the data schema.
830    /// Returns an error if no schema is provided or the column doesn't exist.
831    pub(super) fn resolve_column_index(&self, field: &str) -> Result<u32> {
832        self.program
833            .data_schema
834            .as_ref()
835            .ok_or_else(|| ShapeError::RuntimeError {
836                message: format!(
837                    "No data schema provided. Cannot resolve field '{}'. \
838                     Hint: Use stdlib/finance to load market data with OHLCV schema.",
839                    field
840                ),
841                location: None,
842            })?
843            .get_index(field)
844            .ok_or_else(|| ShapeError::RuntimeError {
845                message: format!(
846                    "Unknown column '{}' in data schema. Available columns: {:?}",
847                    field,
848                    self.program
849                        .data_schema
850                        .as_ref()
851                        .map(|s| &s.column_names)
852                        .unwrap_or(&vec![])
853                ),
854                location: None,
855            })
856    }
857
858    /// Check if a field name is a known data column in the schema.
859    pub(super) fn is_data_column(&self, field: &str) -> bool {
860        self.program
861            .data_schema
862            .as_ref()
863            .map(|s| s.get_index(field).is_some())
864            .unwrap_or(false)
865    }
866
867    /// Collect all outer scope variables
868    pub(super) fn collect_outer_scope_vars(&self) -> Vec<String> {
869        let mut names = BTreeSet::new();
870        for scope in &self.locals {
871            for name in scope.keys() {
872                names.insert(name.clone());
873            }
874        }
875        for name in self.module_bindings.keys() {
876            names.insert(name.clone());
877        }
878        names.into_iter().collect()
879    }
880
881    /// Get or create a module_binding variable
882    pub(super) fn get_or_create_module_binding(&mut self, name: &str) -> u16 {
883        if let Some(&idx) = self.module_bindings.get(name) {
884            idx
885        } else {
886            let idx = self.next_global;
887            self.next_global += 1;
888            self.module_bindings.insert(name.to_string(), idx);
889            idx
890        }
891    }
892
893    pub(super) fn resolve_scoped_module_binding_name(&self, name: &str) -> Option<String> {
894        if self.module_bindings.contains_key(name) {
895            return Some(name.to_string());
896        }
897        for module_path in self.module_scope_stack.iter().rev() {
898            let candidate = format!("{}::{}", module_path, name);
899            if self.module_bindings.contains_key(&candidate) {
900                return Some(candidate);
901            }
902        }
903        None
904    }
905
906    pub(super) fn resolve_scoped_function_name(&self, name: &str) -> Option<String> {
907        if self.program.functions.iter().any(|f| f.name == name) {
908            return Some(name.to_string());
909        }
910        for module_path in self.module_scope_stack.iter().rev() {
911            let candidate = format!("{}::{}", module_path, name);
912            if self.program.functions.iter().any(|f| f.name == candidate) {
913                return Some(candidate);
914            }
915        }
916        None
917    }
918
919    /// Find a function by name
920    pub(super) fn find_function(&self, name: &str) -> Option<usize> {
921        // Check function aliases first (e.g., __original__ -> shadow function).
922        if let Some(actual_name) = self.function_aliases.get(name) {
923            if let Some(idx) = self
924                .program
925                .functions
926                .iter()
927                .position(|f| f.name == *actual_name)
928            {
929                return Some(idx);
930            }
931        }
932
933        // Try direct/scoped resolution
934        if let Some(resolved) = self.resolve_scoped_function_name(name) {
935            if let Some(idx) = self
936                .program
937                .functions
938                .iter()
939                .position(|f| f.name == resolved)
940            {
941                return Some(idx);
942            }
943        }
944
945        // If direct lookup failed, check imported_names for alias -> original name mapping.
946        // When a function is imported with an alias (e.g., `use { foo as bar } from "module"`),
947        // the function is registered under its original (possibly module-qualified) name,
948        // but the user refers to it by the alias.
949        if let Some(imported) = self.imported_names.get(name) {
950            let original = &imported.original_name;
951            // Try direct match on the original name
952            if let Some(idx) = self
953                .program
954                .functions
955                .iter()
956                .position(|f| f.name == *original)
957            {
958                return Some(idx);
959            }
960            // Try scoped resolution on the original name
961            if let Some(resolved) = self.resolve_scoped_function_name(original) {
962                if let Some(idx) = self
963                    .program
964                    .functions
965                    .iter()
966                    .position(|f| f.name == resolved)
967                {
968                    return Some(idx);
969                }
970            }
971        }
972
973        None
974    }
975
976    /// Resolve the receiver's type name for extend method dispatch.
977    ///
978    /// Determines the Shape type name from all available compiler state:
979    /// - `last_expr_type_info.type_name` for TypedObjects (e.g., "Point", "Candle")
980    /// - `last_expr_numeric_type` for numeric types → "Int", "Number", "Decimal"
981    /// - Receiver expression analysis for arrays, strings, booleans
982    ///
983    /// Returns the base type name (e.g., "Vec" not "Vec<int>") suitable for
984    /// extend method lookup as "Type.method".
985    pub(super) fn resolve_receiver_extend_type(
986        &self,
987        receiver: &shape_ast::ast::Expr,
988        receiver_type_info: &Option<crate::type_tracking::VariableTypeInfo>,
989        _receiver_schema: Option<u32>,
990    ) -> Option<String> {
991        // 1. Numeric type from typed opcode tracking — checked first because
992        //    the type tracker stores lowercase names ("int", "number") while
993        //    extend blocks use capitalized TypeName ("Int", "Number", "Decimal").
994        if let Some(numeric) = self.last_expr_numeric_type {
995            return Some(
996                match numeric {
997                    crate::type_tracking::NumericType::Int
998                    | crate::type_tracking::NumericType::IntWidth(_) => "Int",
999                    crate::type_tracking::NumericType::Number => "Number",
1000                    crate::type_tracking::NumericType::Decimal => "Decimal",
1001                }
1002                .to_string(),
1003            );
1004        }
1005
1006        // 2. TypedObject type name (user-defined types like Point, Candle)
1007        if let Some(info) = receiver_type_info {
1008            if let Some(type_name) = &info.type_name {
1009                // Strip generic params: "Vec<int>" → "Vec"
1010                let base = type_name.split('<').next().unwrap_or(type_name);
1011                return Some(base.to_string());
1012            }
1013        }
1014
1015        // 3. Infer from receiver expression shape
1016        match receiver {
1017            shape_ast::ast::Expr::Literal(lit, _) => match lit {
1018                shape_ast::ast::Literal::String(_)
1019                | shape_ast::ast::Literal::FormattedString { .. }
1020                | shape_ast::ast::Literal::ContentString { .. } => Some("String".to_string()),
1021                shape_ast::ast::Literal::Bool(_) => Some("Bool".to_string()),
1022                _ => None,
1023            },
1024            shape_ast::ast::Expr::Array(..) => Some("Vec".to_string()),
1025            _ => None,
1026        }
1027    }
1028
1029    /// Emit store instruction for an identifier
1030    pub(super) fn emit_store_identifier(&mut self, name: &str) -> Result<()> {
1031        // Mutable closure captures: emit StoreClosure to write to the shared upvalue
1032        if let Some(&upvalue_idx) = self.mutable_closure_captures.get(name) {
1033            self.emit(Instruction::new(
1034                OpCode::StoreClosure,
1035                Some(Operand::Local(upvalue_idx)),
1036            ));
1037            return Ok(());
1038        }
1039        if let Some(local_idx) = self.resolve_local(name) {
1040            if self.ref_locals.contains(&local_idx) {
1041                self.emit(Instruction::new(
1042                    OpCode::DerefStore,
1043                    Some(Operand::Local(local_idx)),
1044                ));
1045            } else {
1046                self.emit(Instruction::new(
1047                    OpCode::StoreLocal,
1048                    Some(Operand::Local(local_idx)),
1049                ));
1050                // Patch StoreLocal → StoreLocalTyped for width-typed locals
1051                if let Some(type_name) = self
1052                    .type_tracker
1053                    .get_local_type(local_idx)
1054                    .and_then(|info| info.type_name.as_deref())
1055                {
1056                    if let Some(w) = shape_ast::IntWidth::from_name(type_name) {
1057                        if let Some(last) = self.program.instructions.last_mut() {
1058                            if last.opcode == OpCode::StoreLocal {
1059                                last.opcode = OpCode::StoreLocalTyped;
1060                                last.operand = Some(Operand::TypedLocal(
1061                                    local_idx,
1062                                    crate::bytecode::NumericWidth::from_int_width(w),
1063                                ));
1064                            }
1065                        }
1066                    }
1067                }
1068            }
1069        } else {
1070            let scoped_name = self
1071                .resolve_scoped_module_binding_name(name)
1072                .unwrap_or_else(|| name.to_string());
1073            let binding_idx = self.get_or_create_module_binding(&scoped_name);
1074            self.emit(Instruction::new(
1075                OpCode::StoreModuleBinding,
1076                Some(Operand::ModuleBinding(binding_idx)),
1077            ));
1078        }
1079        Ok(())
1080    }
1081
1082    /// Get built-in function by name
1083    pub(super) fn get_builtin_function(&self, name: &str) -> Option<BuiltinFunction> {
1084        match name {
1085            // Option type constructor
1086            "Some" => Some(BuiltinFunction::SomeCtor),
1087            "Ok" => Some(BuiltinFunction::OkCtor),
1088            "Err" => Some(BuiltinFunction::ErrCtor),
1089            "HashMap" => Some(BuiltinFunction::HashMapCtor),
1090            "Set" => Some(BuiltinFunction::SetCtor),
1091            "Deque" => Some(BuiltinFunction::DequeCtor),
1092            "PriorityQueue" => Some(BuiltinFunction::PriorityQueueCtor),
1093            "Mutex" => Some(BuiltinFunction::MutexCtor),
1094            "Atomic" => Some(BuiltinFunction::AtomicCtor),
1095            "Lazy" => Some(BuiltinFunction::LazyCtor),
1096            "Channel" => Some(BuiltinFunction::ChannelCtor),
1097            // Json navigation helpers
1098            "__json_object_get" => Some(BuiltinFunction::JsonObjectGet),
1099            "__json_array_at" => Some(BuiltinFunction::JsonArrayAt),
1100            "__json_object_keys" => Some(BuiltinFunction::JsonObjectKeys),
1101            "__json_array_len" => Some(BuiltinFunction::JsonArrayLen),
1102            "__json_object_len" => Some(BuiltinFunction::JsonObjectLen),
1103            "__intrinsic_vec_abs" => Some(BuiltinFunction::IntrinsicVecAbs),
1104            "__intrinsic_vec_sqrt" => Some(BuiltinFunction::IntrinsicVecSqrt),
1105            "__intrinsic_vec_ln" => Some(BuiltinFunction::IntrinsicVecLn),
1106            "__intrinsic_vec_exp" => Some(BuiltinFunction::IntrinsicVecExp),
1107            "__intrinsic_vec_add" => Some(BuiltinFunction::IntrinsicVecAdd),
1108            "__intrinsic_vec_sub" => Some(BuiltinFunction::IntrinsicVecSub),
1109            "__intrinsic_vec_mul" => Some(BuiltinFunction::IntrinsicVecMul),
1110            "__intrinsic_vec_div" => Some(BuiltinFunction::IntrinsicVecDiv),
1111            "__intrinsic_vec_max" => Some(BuiltinFunction::IntrinsicVecMax),
1112            "__intrinsic_vec_min" => Some(BuiltinFunction::IntrinsicVecMin),
1113            "__intrinsic_vec_select" => Some(BuiltinFunction::IntrinsicVecSelect),
1114            "__intrinsic_matmul_vec" => Some(BuiltinFunction::IntrinsicMatMulVec),
1115            "__intrinsic_matmul_mat" => Some(BuiltinFunction::IntrinsicMatMulMat),
1116
1117            // Existing builtins
1118            "abs" => Some(BuiltinFunction::Abs),
1119            "min" => Some(BuiltinFunction::Min),
1120            "max" => Some(BuiltinFunction::Max),
1121            "sqrt" => Some(BuiltinFunction::Sqrt),
1122            "ln" => Some(BuiltinFunction::Ln),
1123            "pow" => Some(BuiltinFunction::Pow),
1124            "exp" => Some(BuiltinFunction::Exp),
1125            "log" => Some(BuiltinFunction::Log),
1126            "floor" => Some(BuiltinFunction::Floor),
1127            "ceil" => Some(BuiltinFunction::Ceil),
1128            "round" => Some(BuiltinFunction::Round),
1129            "sin" => Some(BuiltinFunction::Sin),
1130            "cos" => Some(BuiltinFunction::Cos),
1131            "tan" => Some(BuiltinFunction::Tan),
1132            "asin" => Some(BuiltinFunction::Asin),
1133            "acos" => Some(BuiltinFunction::Acos),
1134            "atan" => Some(BuiltinFunction::Atan),
1135            "stddev" => Some(BuiltinFunction::StdDev),
1136            "slice" => Some(BuiltinFunction::Slice),
1137            "push" => Some(BuiltinFunction::Push),
1138            "pop" => Some(BuiltinFunction::Pop),
1139            "first" => Some(BuiltinFunction::First),
1140            "last" => Some(BuiltinFunction::Last),
1141            "zip" => Some(BuiltinFunction::Zip),
1142            "filled" => Some(BuiltinFunction::Filled),
1143            "map" | "__intrinsic_map" => Some(BuiltinFunction::Map),
1144            "filter" | "__intrinsic_filter" => Some(BuiltinFunction::Filter),
1145            "reduce" | "__intrinsic_reduce" => Some(BuiltinFunction::Reduce),
1146            "forEach" => Some(BuiltinFunction::ForEach),
1147            "find" => Some(BuiltinFunction::Find),
1148            "findIndex" => Some(BuiltinFunction::FindIndex),
1149            "some" => Some(BuiltinFunction::Some),
1150            "every" => Some(BuiltinFunction::Every),
1151            "print" => Some(BuiltinFunction::Print),
1152            "format" => Some(BuiltinFunction::Format),
1153            "len" | "count" => Some(BuiltinFunction::Len),
1154            // "throw" removed: Shape uses Result types
1155            "__intrinsic_snapshot" | "snapshot" => Some(BuiltinFunction::Snapshot),
1156            "exit" => Some(BuiltinFunction::Exit),
1157            "range" => Some(BuiltinFunction::Range),
1158            "is_number" | "isNumber" => Some(BuiltinFunction::IsNumber),
1159            "is_string" | "isString" => Some(BuiltinFunction::IsString),
1160            "is_bool" | "isBool" => Some(BuiltinFunction::IsBool),
1161            "is_array" | "isArray" => Some(BuiltinFunction::IsArray),
1162            "is_object" | "isObject" => Some(BuiltinFunction::IsObject),
1163            "is_data_row" | "isDataRow" => Some(BuiltinFunction::IsDataRow),
1164            "to_string" | "toString" => Some(BuiltinFunction::ToString),
1165            "to_number" | "toNumber" => Some(BuiltinFunction::ToNumber),
1166            "to_bool" | "toBool" => Some(BuiltinFunction::ToBool),
1167            "__into_int" => Some(BuiltinFunction::IntoInt),
1168            "__into_number" => Some(BuiltinFunction::IntoNumber),
1169            "__into_decimal" => Some(BuiltinFunction::IntoDecimal),
1170            "__into_bool" => Some(BuiltinFunction::IntoBool),
1171            "__into_string" => Some(BuiltinFunction::IntoString),
1172            "__try_into_int" => Some(BuiltinFunction::TryIntoInt),
1173            "__try_into_number" => Some(BuiltinFunction::TryIntoNumber),
1174            "__try_into_decimal" => Some(BuiltinFunction::TryIntoDecimal),
1175            "__try_into_bool" => Some(BuiltinFunction::TryIntoBool),
1176            "__try_into_string" => Some(BuiltinFunction::TryIntoString),
1177            "__native_ptr_size" => Some(BuiltinFunction::NativePtrSize),
1178            "__native_ptr_new_cell" => Some(BuiltinFunction::NativePtrNewCell),
1179            "__native_ptr_free_cell" => Some(BuiltinFunction::NativePtrFreeCell),
1180            "__native_ptr_read_ptr" => Some(BuiltinFunction::NativePtrReadPtr),
1181            "__native_ptr_write_ptr" => Some(BuiltinFunction::NativePtrWritePtr),
1182            "__native_table_from_arrow_c" => Some(BuiltinFunction::NativeTableFromArrowC),
1183            "__native_table_from_arrow_c_typed" => {
1184                Some(BuiltinFunction::NativeTableFromArrowCTyped)
1185            }
1186            "__native_table_bind_type" => Some(BuiltinFunction::NativeTableBindType),
1187            "fold" => Some(BuiltinFunction::ControlFold),
1188
1189            // Math intrinsics
1190            "__intrinsic_sum" | "sum" => Some(BuiltinFunction::IntrinsicSum),
1191            "__intrinsic_mean" | "mean" => Some(BuiltinFunction::IntrinsicMean),
1192            "__intrinsic_min" => Some(BuiltinFunction::IntrinsicMin),
1193            "__intrinsic_max" => Some(BuiltinFunction::IntrinsicMax),
1194            "__intrinsic_std" | "std" => Some(BuiltinFunction::IntrinsicStd),
1195            "__intrinsic_variance" | "variance" => Some(BuiltinFunction::IntrinsicVariance),
1196
1197            // Random intrinsics
1198            "__intrinsic_random" => Some(BuiltinFunction::IntrinsicRandom),
1199            "__intrinsic_random_int" => Some(BuiltinFunction::IntrinsicRandomInt),
1200            "__intrinsic_random_seed" => Some(BuiltinFunction::IntrinsicRandomSeed),
1201            "__intrinsic_random_normal" => Some(BuiltinFunction::IntrinsicRandomNormal),
1202            "__intrinsic_random_array" => Some(BuiltinFunction::IntrinsicRandomArray),
1203
1204            // Distribution intrinsics
1205            "__intrinsic_dist_uniform" => Some(BuiltinFunction::IntrinsicDistUniform),
1206            "__intrinsic_dist_lognormal" => Some(BuiltinFunction::IntrinsicDistLognormal),
1207            "__intrinsic_dist_exponential" => Some(BuiltinFunction::IntrinsicDistExponential),
1208            "__intrinsic_dist_poisson" => Some(BuiltinFunction::IntrinsicDistPoisson),
1209            "__intrinsic_dist_sample_n" => Some(BuiltinFunction::IntrinsicDistSampleN),
1210
1211            // Stochastic process intrinsics
1212            "__intrinsic_brownian_motion" => Some(BuiltinFunction::IntrinsicBrownianMotion),
1213            "__intrinsic_gbm" => Some(BuiltinFunction::IntrinsicGbm),
1214            "__intrinsic_ou_process" => Some(BuiltinFunction::IntrinsicOuProcess),
1215            "__intrinsic_random_walk" => Some(BuiltinFunction::IntrinsicRandomWalk),
1216
1217            // Rolling intrinsics
1218            "__intrinsic_rolling_sum" => Some(BuiltinFunction::IntrinsicRollingSum),
1219            "__intrinsic_rolling_mean" => Some(BuiltinFunction::IntrinsicRollingMean),
1220            "__intrinsic_rolling_std" => Some(BuiltinFunction::IntrinsicRollingStd),
1221            "__intrinsic_rolling_min" => Some(BuiltinFunction::IntrinsicRollingMin),
1222            "__intrinsic_rolling_max" => Some(BuiltinFunction::IntrinsicRollingMax),
1223            "__intrinsic_ema" => Some(BuiltinFunction::IntrinsicEma),
1224            "__intrinsic_linear_recurrence" => Some(BuiltinFunction::IntrinsicLinearRecurrence),
1225
1226            // Series intrinsics
1227            "__intrinsic_shift" => Some(BuiltinFunction::IntrinsicShift),
1228            "__intrinsic_diff" => Some(BuiltinFunction::IntrinsicDiff),
1229            "__intrinsic_pct_change" => Some(BuiltinFunction::IntrinsicPctChange),
1230            "__intrinsic_fillna" => Some(BuiltinFunction::IntrinsicFillna),
1231            "__intrinsic_cumsum" => Some(BuiltinFunction::IntrinsicCumsum),
1232            "__intrinsic_cumprod" => Some(BuiltinFunction::IntrinsicCumprod),
1233            "__intrinsic_clip" => Some(BuiltinFunction::IntrinsicClip),
1234
1235            // Statistical intrinsics
1236            "__intrinsic_correlation" => Some(BuiltinFunction::IntrinsicCorrelation),
1237            "__intrinsic_covariance" => Some(BuiltinFunction::IntrinsicCovariance),
1238            "__intrinsic_percentile" => Some(BuiltinFunction::IntrinsicPercentile),
1239            "__intrinsic_median" => Some(BuiltinFunction::IntrinsicMedian),
1240
1241            // Character code intrinsics
1242            "__intrinsic_char_code" => Some(BuiltinFunction::IntrinsicCharCode),
1243            "__intrinsic_from_char_code" => Some(BuiltinFunction::IntrinsicFromCharCode),
1244
1245            // Series access
1246            "__intrinsic_series" => Some(BuiltinFunction::IntrinsicSeries),
1247
1248            // Reflection
1249            "reflect" => Some(BuiltinFunction::Reflect),
1250
1251            // Additional math builtins
1252            "sign" => Some(BuiltinFunction::Sign),
1253            "gcd" => Some(BuiltinFunction::Gcd),
1254            "lcm" => Some(BuiltinFunction::Lcm),
1255            "hypot" => Some(BuiltinFunction::Hypot),
1256            "clamp" => Some(BuiltinFunction::Clamp),
1257            "isNaN" | "is_nan" => Some(BuiltinFunction::IsNaN),
1258            "isFinite" | "is_finite" => Some(BuiltinFunction::IsFinite),
1259
1260            _ => None,
1261        }
1262    }
1263
1264    /// Check if a builtin function requires arg count
1265    pub(super) fn builtin_requires_arg_count(&self, builtin: BuiltinFunction) -> bool {
1266        matches!(
1267            builtin,
1268            BuiltinFunction::Abs
1269                | BuiltinFunction::Min
1270                | BuiltinFunction::Max
1271                | BuiltinFunction::Sqrt
1272                | BuiltinFunction::Ln
1273                | BuiltinFunction::Pow
1274                | BuiltinFunction::Exp
1275                | BuiltinFunction::Log
1276                | BuiltinFunction::Floor
1277                | BuiltinFunction::Ceil
1278                | BuiltinFunction::Round
1279                | BuiltinFunction::Sin
1280                | BuiltinFunction::Cos
1281                | BuiltinFunction::Tan
1282                | BuiltinFunction::Asin
1283                | BuiltinFunction::Acos
1284                | BuiltinFunction::Atan
1285                | BuiltinFunction::StdDev
1286                | BuiltinFunction::Range
1287                | BuiltinFunction::Slice
1288                | BuiltinFunction::Push
1289                | BuiltinFunction::Pop
1290                | BuiltinFunction::First
1291                | BuiltinFunction::Last
1292                | BuiltinFunction::Zip
1293                | BuiltinFunction::Map
1294                | BuiltinFunction::Filter
1295                | BuiltinFunction::Reduce
1296                | BuiltinFunction::ForEach
1297                | BuiltinFunction::Find
1298                | BuiltinFunction::FindIndex
1299                | BuiltinFunction::Some
1300                | BuiltinFunction::Every
1301                | BuiltinFunction::SomeCtor
1302                | BuiltinFunction::OkCtor
1303                | BuiltinFunction::ErrCtor
1304                | BuiltinFunction::HashMapCtor
1305                | BuiltinFunction::SetCtor
1306                | BuiltinFunction::DequeCtor
1307                | BuiltinFunction::PriorityQueueCtor
1308                | BuiltinFunction::MutexCtor
1309                | BuiltinFunction::AtomicCtor
1310                | BuiltinFunction::LazyCtor
1311                | BuiltinFunction::ChannelCtor
1312                | BuiltinFunction::Print
1313                | BuiltinFunction::Format
1314                | BuiltinFunction::Len
1315                // BuiltinFunction::Throw removed
1316                | BuiltinFunction::Snapshot
1317                | BuiltinFunction::ObjectRest
1318                | BuiltinFunction::IsNumber
1319                | BuiltinFunction::IsString
1320                | BuiltinFunction::IsBool
1321                | BuiltinFunction::IsArray
1322                | BuiltinFunction::IsObject
1323                | BuiltinFunction::IsDataRow
1324                | BuiltinFunction::ToString
1325                | BuiltinFunction::ToNumber
1326                | BuiltinFunction::ToBool
1327                | BuiltinFunction::IntoInt
1328                | BuiltinFunction::IntoNumber
1329                | BuiltinFunction::IntoDecimal
1330                | BuiltinFunction::IntoBool
1331                | BuiltinFunction::IntoString
1332                | BuiltinFunction::TryIntoInt
1333                | BuiltinFunction::TryIntoNumber
1334                | BuiltinFunction::TryIntoDecimal
1335                | BuiltinFunction::TryIntoBool
1336                | BuiltinFunction::TryIntoString
1337                | BuiltinFunction::NativePtrSize
1338                | BuiltinFunction::NativePtrNewCell
1339                | BuiltinFunction::NativePtrFreeCell
1340                | BuiltinFunction::NativePtrReadPtr
1341                | BuiltinFunction::NativePtrWritePtr
1342                | BuiltinFunction::NativeTableFromArrowC
1343                | BuiltinFunction::NativeTableFromArrowCTyped
1344                | BuiltinFunction::NativeTableBindType
1345                | BuiltinFunction::ControlFold
1346                | BuiltinFunction::IntrinsicSum
1347                | BuiltinFunction::IntrinsicMean
1348                | BuiltinFunction::IntrinsicMin
1349                | BuiltinFunction::IntrinsicMax
1350                | BuiltinFunction::IntrinsicStd
1351                | BuiltinFunction::IntrinsicVariance
1352                | BuiltinFunction::IntrinsicRandom
1353                | BuiltinFunction::IntrinsicRandomInt
1354                | BuiltinFunction::IntrinsicRandomSeed
1355                | BuiltinFunction::IntrinsicRandomNormal
1356                | BuiltinFunction::IntrinsicRandomArray
1357                | BuiltinFunction::IntrinsicDistUniform
1358                | BuiltinFunction::IntrinsicDistLognormal
1359                | BuiltinFunction::IntrinsicDistExponential
1360                | BuiltinFunction::IntrinsicDistPoisson
1361                | BuiltinFunction::IntrinsicDistSampleN
1362                | BuiltinFunction::IntrinsicBrownianMotion
1363                | BuiltinFunction::IntrinsicGbm
1364                | BuiltinFunction::IntrinsicOuProcess
1365                | BuiltinFunction::IntrinsicRandomWalk
1366                | BuiltinFunction::IntrinsicRollingSum
1367                | BuiltinFunction::IntrinsicRollingMean
1368                | BuiltinFunction::IntrinsicRollingStd
1369                | BuiltinFunction::IntrinsicRollingMin
1370                | BuiltinFunction::IntrinsicRollingMax
1371                | BuiltinFunction::IntrinsicEma
1372                | BuiltinFunction::IntrinsicLinearRecurrence
1373                | BuiltinFunction::IntrinsicShift
1374                | BuiltinFunction::IntrinsicDiff
1375                | BuiltinFunction::IntrinsicPctChange
1376                | BuiltinFunction::IntrinsicFillna
1377                | BuiltinFunction::IntrinsicCumsum
1378                | BuiltinFunction::IntrinsicCumprod
1379                | BuiltinFunction::IntrinsicClip
1380                | BuiltinFunction::IntrinsicCorrelation
1381                | BuiltinFunction::IntrinsicCovariance
1382                | BuiltinFunction::IntrinsicPercentile
1383                | BuiltinFunction::IntrinsicMedian
1384                | BuiltinFunction::IntrinsicCharCode
1385                | BuiltinFunction::IntrinsicFromCharCode
1386                | BuiltinFunction::IntrinsicSeries
1387                | BuiltinFunction::IntrinsicVecAbs
1388                | BuiltinFunction::IntrinsicVecSqrt
1389                | BuiltinFunction::IntrinsicVecLn
1390                | BuiltinFunction::IntrinsicVecExp
1391                | BuiltinFunction::IntrinsicVecAdd
1392                | BuiltinFunction::IntrinsicVecSub
1393                | BuiltinFunction::IntrinsicVecMul
1394                | BuiltinFunction::IntrinsicVecDiv
1395                | BuiltinFunction::IntrinsicVecMax
1396                | BuiltinFunction::IntrinsicVecMin
1397                | BuiltinFunction::IntrinsicVecSelect
1398                | BuiltinFunction::IntrinsicMatMulVec
1399                | BuiltinFunction::IntrinsicMatMulMat
1400                | BuiltinFunction::Sign
1401                | BuiltinFunction::Gcd
1402                | BuiltinFunction::Lcm
1403                | BuiltinFunction::Hypot
1404                | BuiltinFunction::Clamp
1405                | BuiltinFunction::IsNaN
1406                | BuiltinFunction::IsFinite
1407        )
1408    }
1409
1410    /// Check if a method name is a known built-in method on any VM type.
1411    /// Used by UFCS to determine if `receiver.method(args)` should be dispatched
1412    /// as a built-in method call or rewritten to `method(receiver, args)`.
1413    pub(super) fn is_known_builtin_method(method: &str) -> bool {
1414        // Array methods (from ARRAY_METHODS PHF map)
1415        matches!(method,
1416            "map" | "filter" | "reduce" | "forEach" | "find" | "findIndex"
1417            | "some" | "every" | "sort" | "groupBy" | "flatMap"
1418            | "len" | "length" | "first" | "last" | "reverse" | "slice"
1419            | "concat" | "take" | "drop" | "skip"
1420            | "indexOf" | "includes"
1421            | "join" | "flatten" | "unique" | "distinct" | "distinctBy"
1422            | "sum" | "avg" | "min" | "max" | "count"
1423            | "where" | "select" | "orderBy" | "thenBy" | "takeWhile"
1424            | "skipWhile" | "single" | "any" | "all"
1425            | "innerJoin" | "leftJoin" | "crossJoin"
1426            | "union" | "intersect" | "except"
1427        )
1428        // DataTable methods (from DATATABLE_METHODS PHF map)
1429        || matches!(method,
1430            "columns" | "column" | "head" | "tail" | "mean" | "std"
1431            | "describe" | "aggregate" | "group_by" | "index_by" | "indexBy"
1432            | "simulate" | "toMat" | "to_mat"
1433        )
1434        // Column methods (from COLUMN_METHODS PHF map)
1435        || matches!(method, "toArray")
1436        // IndexedTable methods (from INDEXED_TABLE_METHODS PHF map)
1437        || matches!(method, "resample" | "between")
1438        // Number methods handled inline in op_call_method
1439        || matches!(method,
1440            "toFixed" | "toInt" | "toNumber" | "to_number" | "floor" | "ceil" | "round"
1441            | "abs" | "sign" | "clamp"
1442        )
1443        // String methods handled inline
1444        || matches!(method,
1445            "toUpperCase" | "toLowerCase" | "trim" | "contains" | "startsWith"
1446            | "endsWith" | "split" | "replace" | "substring" | "charAt"
1447            | "padStart" | "padEnd" | "repeat" | "toString"
1448        )
1449        // Object methods handled by handle_object_method
1450        || matches!(method, "keys" | "values" | "has" | "get" | "set" | "len")
1451        // Universal intrinsic methods
1452        || matches!(method, "type")
1453    }
1454
1455    /// Try to track a `Table<T>` type annotation as a DataTable variable.
1456    ///
1457    /// If the annotation is `Generic { name: "Table", args: [Reference(T)] }`,
1458    /// looks up T's schema and marks the variable as `is_datatable`.
1459    pub(super) fn try_track_datatable_type(
1460        &mut self,
1461        type_ann: &shape_ast::ast::TypeAnnotation,
1462        slot: u16,
1463        is_local: bool,
1464    ) -> shape_ast::error::Result<()> {
1465        use shape_ast::ast::TypeAnnotation;
1466        if let TypeAnnotation::Generic { name, args } = type_ann {
1467            if name == "Table" && args.len() == 1 {
1468                let inner_name = match &args[0] {
1469                    TypeAnnotation::Reference(t) => Some(t.as_str()),
1470                    TypeAnnotation::Basic(t) => Some(t.as_str()),
1471                    _ => None,
1472                };
1473                if let Some(type_name) = inner_name {
1474                    let schema_id = self
1475                        .type_tracker
1476                        .schema_registry()
1477                        .get(type_name)
1478                        .map(|s| s.id);
1479                    if let Some(sid) = schema_id {
1480                        let info = crate::type_tracking::VariableTypeInfo::datatable(
1481                            sid,
1482                            type_name.to_string(),
1483                        );
1484                        if is_local {
1485                            self.type_tracker.set_local_type(slot, info);
1486                        } else {
1487                            self.type_tracker.set_binding_type(slot, info);
1488                        }
1489                    } else {
1490                        return Err(shape_ast::error::ShapeError::SemanticError {
1491                            message: format!(
1492                                "Unknown type '{}' in Table<{}> annotation",
1493                                type_name, type_name
1494                            ),
1495                            location: None,
1496                        });
1497                    }
1498                }
1499            }
1500        }
1501        Ok(())
1502    }
1503
1504    /// Check if a variable is a RowView (typed row from Arrow DataTable).
1505    pub(super) fn is_row_view_variable(&self, name: &str) -> bool {
1506        if let Some(local_idx) = self.resolve_local(name) {
1507            if let Some(info) = self.type_tracker.get_local_type(local_idx) {
1508                return info.is_row_view();
1509            }
1510        }
1511        if let Some(&binding_idx) = self.module_bindings.get(name) {
1512            if let Some(info) = self.type_tracker.get_binding_type(binding_idx) {
1513                return info.is_row_view();
1514            }
1515        }
1516        false
1517    }
1518
1519    /// Get the available field names for a RowView variable's schema.
1520    pub(super) fn get_row_view_field_names(&self, name: &str) -> Option<Vec<String>> {
1521        let type_name = if let Some(local_idx) = self.resolve_local(name) {
1522            self.type_tracker
1523                .get_local_type(local_idx)
1524                .and_then(|info| {
1525                    if info.is_row_view() {
1526                        info.type_name.clone()
1527                    } else {
1528                        None
1529                    }
1530                })
1531        } else if let Some(&binding_idx) = self.module_bindings.get(name) {
1532            self.type_tracker
1533                .get_binding_type(binding_idx)
1534                .and_then(|info| {
1535                    if info.is_row_view() {
1536                        info.type_name.clone()
1537                    } else {
1538                        None
1539                    }
1540                })
1541        } else {
1542            None
1543        };
1544
1545        if let Some(tn) = type_name {
1546            if let Some(schema) = self.type_tracker.schema_registry().get(&tn) {
1547                return Some(schema.field_names().map(|n| n.to_string()).collect());
1548            }
1549        }
1550        None
1551    }
1552
1553    /// Try to resolve a property access on a RowView variable to a column ID.
1554    ///
1555    /// Returns `Some(col_id)` if the variable is a tracked RowView and the field
1556    /// exists in its schema. Returns `None` if the variable isn't a RowView or
1557    /// the field is unknown (caller should emit a compile-time error).
1558    pub(super) fn try_resolve_row_view_column(
1559        &self,
1560        var_name: &str,
1561        field_name: &str,
1562    ) -> Option<u32> {
1563        // Check locals first, then module_bindings
1564        if let Some(local_idx) = self.resolve_local(var_name) {
1565            return self
1566                .type_tracker
1567                .get_row_view_column_id(local_idx, true, field_name);
1568        }
1569        if let Some(&binding_idx) = self.module_bindings.get(var_name) {
1570            return self
1571                .type_tracker
1572                .get_row_view_column_id(binding_idx, false, field_name);
1573        }
1574        None
1575    }
1576
1577    /// Determine the appropriate LoadCol opcode for a RowView field.
1578    ///
1579    /// Looks up the field's FieldType and maps it to the corresponding opcode.
1580    /// Falls back to LoadColF64 if the type can't be determined.
1581    pub(super) fn row_view_field_opcode(&self, var_name: &str, field_name: &str) -> OpCode {
1582        use shape_runtime::type_schema::FieldType;
1583
1584        let type_name = if let Some(local_idx) = self.resolve_local(var_name) {
1585            self.type_tracker
1586                .get_local_type(local_idx)
1587                .and_then(|info| info.type_name.clone())
1588        } else if let Some(&binding_idx) = self.module_bindings.get(var_name) {
1589            self.type_tracker
1590                .get_binding_type(binding_idx)
1591                .and_then(|info| info.type_name.clone())
1592        } else {
1593            None
1594        };
1595
1596        if let Some(type_name) = type_name {
1597            if let Some(schema) = self.type_tracker.schema_registry().get(&type_name) {
1598                if let Some(field) = schema.get_field(field_name) {
1599                    return match field.field_type {
1600                        FieldType::F64 => OpCode::LoadColF64,
1601                        FieldType::I64 | FieldType::Timestamp => OpCode::LoadColI64,
1602                        FieldType::Bool => OpCode::LoadColBool,
1603                        FieldType::String => OpCode::LoadColStr,
1604                        _ => OpCode::LoadColF64, // default
1605                    };
1606                }
1607            }
1608        }
1609        OpCode::LoadColF64 // default
1610    }
1611
1612    /// Resolve the NumericType for a RowView field (used for typed opcode emission).
1613    pub(super) fn resolve_row_view_field_numeric_type(
1614        &self,
1615        var_name: &str,
1616        field_name: &str,
1617    ) -> Option<crate::type_tracking::NumericType> {
1618        use crate::type_tracking::NumericType;
1619        use shape_runtime::type_schema::FieldType;
1620
1621        let type_name = if let Some(local_idx) = self.resolve_local(var_name) {
1622            self.type_tracker
1623                .get_local_type(local_idx)
1624                .and_then(|info| info.type_name.clone())
1625        } else if let Some(&binding_idx) = self.module_bindings.get(var_name) {
1626            self.type_tracker
1627                .get_binding_type(binding_idx)
1628                .and_then(|info| info.type_name.clone())
1629        } else {
1630            None
1631        };
1632
1633        if let Some(type_name) = type_name {
1634            if let Some(schema) = self.type_tracker.schema_registry().get(&type_name) {
1635                if let Some(field) = schema.get_field(field_name) {
1636                    return match field.field_type {
1637                        FieldType::F64 => Some(NumericType::Number),
1638                        FieldType::I64 | FieldType::Timestamp => Some(NumericType::Int),
1639                        FieldType::Decimal => Some(NumericType::Decimal),
1640                        _ => None,
1641                    };
1642                }
1643            }
1644        }
1645        None
1646    }
1647
1648    /// Convert a TypeAnnotation to a FieldType for TypeSchema registration
1649    pub(super) fn type_annotation_to_field_type(
1650        ann: &shape_ast::ast::TypeAnnotation,
1651    ) -> shape_runtime::type_schema::FieldType {
1652        use shape_ast::ast::TypeAnnotation;
1653        use shape_runtime::type_schema::FieldType;
1654        match ann {
1655            TypeAnnotation::Basic(s) => match s.as_str() {
1656                "number" | "float" | "f64" | "f32" => FieldType::F64,
1657                "i8" => FieldType::I8,
1658                "u8" => FieldType::U8,
1659                "i16" => FieldType::I16,
1660                "u16" => FieldType::U16,
1661                "i32" => FieldType::I32,
1662                "u32" => FieldType::U32,
1663                "u64" => FieldType::U64,
1664                "int" | "i64" | "integer" | "isize" | "usize" | "byte" | "char" => FieldType::I64,
1665                "string" | "str" => FieldType::String,
1666                "decimal" => FieldType::Decimal,
1667                "bool" | "boolean" => FieldType::Bool,
1668                "timestamp" => FieldType::Timestamp,
1669                // Non-primitive type names (e.g. "Server", "Inner") are nested
1670                // object references.  The parser emits Basic for `ident` matches
1671                // inside `basic_type`, so treat unknown names as Object references
1672                // to enable typed field access on nested structs.
1673                other => FieldType::Object(other.to_string()),
1674            },
1675            TypeAnnotation::Reference(s) => FieldType::Object(s.clone()),
1676            TypeAnnotation::Array(inner) => {
1677                FieldType::Array(Box::new(Self::type_annotation_to_field_type(inner)))
1678            }
1679            TypeAnnotation::Optional(_) => FieldType::Any, // Optional needs NaN boxing
1680            TypeAnnotation::Generic { name, .. } => match name.as_str() {
1681                // Generic containers that need NaN boxing
1682                "HashMap" | "Map" | "Result" | "Option" | "Set" => FieldType::Any,
1683                // User-defined generic structs — preserve the type name
1684                other => FieldType::Object(other.to_string()),
1685            },
1686            _ => FieldType::Any,
1687        }
1688    }
1689
1690    /// Evaluate an annotation argument expression to a string representation.
1691    /// Only handles compile-time evaluable expressions (literals).
1692    pub(super) fn eval_annotation_arg(expr: &shape_ast::ast::Expr) -> Option<String> {
1693        use shape_ast::ast::{Expr, Literal};
1694        match expr {
1695            Expr::Literal(Literal::String(s), _) => Some(s.clone()),
1696            Expr::Literal(Literal::Number(n), _) => Some(n.to_string()),
1697            Expr::Literal(Literal::Int(i), _) => Some(i.to_string()),
1698            Expr::Literal(Literal::Bool(b), _) => Some(b.to_string()),
1699            _ => None,
1700        }
1701    }
1702
1703    /// Get the schema ID for a `Table<T>` type annotation, if applicable.
1704    ///
1705    /// Returns `Some(schema_id)` if the annotation is `Table<T>` and `T` is a registered
1706    /// TypeSchema. Returns `None` otherwise.
1707    pub(super) fn get_table_schema_id(
1708        &self,
1709        type_ann: &shape_ast::ast::TypeAnnotation,
1710    ) -> Option<u16> {
1711        use shape_ast::ast::TypeAnnotation;
1712        if let TypeAnnotation::Generic { name, args } = type_ann {
1713            if name == "Table" && args.len() == 1 {
1714                let inner_name = match &args[0] {
1715                    TypeAnnotation::Reference(t) | TypeAnnotation::Basic(t) => Some(t.as_str()),
1716                    _ => None,
1717                };
1718                if let Some(type_name) = inner_name {
1719                    return self
1720                        .type_tracker
1721                        .schema_registry()
1722                        .get(type_name)
1723                        .map(|s| s.id as u16);
1724                }
1725            }
1726        }
1727        None
1728    }
1729
1730    // ===== Drop scope management =====
1731
1732    /// Push a new drop scope. Must be paired with pop_drop_scope().
1733    pub(super) fn push_drop_scope(&mut self) {
1734        self.drop_locals.push(Vec::new());
1735    }
1736
1737    /// Pop the current drop scope, emitting DropCall instructions for all
1738    /// tracked locals in reverse order.
1739    pub(super) fn pop_drop_scope(&mut self) -> Result<()> {
1740        // Emit DropCall for each tracked local in reverse order
1741        if let Some(locals) = self.drop_locals.pop() {
1742            for (local_idx, is_async) in locals.into_iter().rev() {
1743                self.emit(Instruction::new(
1744                    OpCode::LoadLocal,
1745                    Some(Operand::Local(local_idx)),
1746                ));
1747                let opcode = if is_async {
1748                    OpCode::DropCallAsync
1749                } else {
1750                    OpCode::DropCall
1751                };
1752                self.emit(Instruction::simple(opcode));
1753            }
1754        }
1755        Ok(())
1756    }
1757
1758    /// Track a local variable as needing Drop at scope exit.
1759    pub(super) fn track_drop_local(&mut self, local_idx: u16, is_async: bool) {
1760        if let Some(scope) = self.drop_locals.last_mut() {
1761            scope.push((local_idx, is_async));
1762        }
1763    }
1764
1765    /// Resolve the DropKind for a local variable's type.
1766    /// Returns None if the type is unknown or has no Drop impl.
1767    pub(super) fn local_drop_kind(&self, local_idx: u16) -> Option<DropKind> {
1768        let type_name = self
1769            .type_tracker
1770            .get_local_type(local_idx)
1771            .and_then(|info| info.type_name.as_ref())?;
1772        self.drop_type_info.get(type_name).copied()
1773    }
1774
1775    /// Resolve DropKind from a type annotation.
1776    pub(super) fn annotation_drop_kind(&self, type_ann: &TypeAnnotation) -> Option<DropKind> {
1777        let type_name = Self::tracked_type_name_from_annotation(type_ann)?;
1778        self.drop_type_info.get(&type_name).copied()
1779    }
1780
1781    /// Emit drops for all scopes being exited (used by return/break/continue).
1782    /// `scopes_to_exit` is the number of drop scopes to emit drops for.
1783    pub(super) fn emit_drops_for_early_exit(&mut self, scopes_to_exit: usize) -> Result<()> {
1784        let total = self.drop_locals.len();
1785        if scopes_to_exit > total {
1786            return Ok(());
1787        }
1788        // Collect locals from scopes being exited (innermost first)
1789        let mut scopes: Vec<Vec<(u16, bool)>> = Vec::new();
1790        for i in (total - scopes_to_exit..total).rev() {
1791            let locals = self.drop_locals.get(i).cloned().unwrap_or_default();
1792            scopes.push(locals);
1793        }
1794        // Now emit DropCall instructions
1795        for locals in scopes {
1796            for (local_idx, is_async) in locals.into_iter().rev() {
1797                self.emit(Instruction::new(
1798                    OpCode::LoadLocal,
1799                    Some(Operand::Local(local_idx)),
1800                ));
1801                let opcode = if is_async {
1802                    OpCode::DropCallAsync
1803                } else {
1804                    OpCode::DropCall
1805                };
1806                self.emit(Instruction::simple(opcode));
1807            }
1808        }
1809        Ok(())
1810    }
1811}
1812
1813#[cfg(test)]
1814mod tests {
1815    use super::super::BytecodeCompiler;
1816    use shape_ast::ast::TypeAnnotation;
1817    use shape_runtime::type_schema::FieldType;
1818
1819    #[test]
1820    fn test_type_annotation_to_field_type_array_recursive() {
1821        let ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Basic("int".to_string())));
1822        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
1823        assert_eq!(ft, FieldType::Array(Box::new(FieldType::I64)));
1824    }
1825
1826    #[test]
1827    fn test_type_annotation_to_field_type_optional() {
1828        let ann = TypeAnnotation::Optional(Box::new(TypeAnnotation::Basic("int".to_string())));
1829        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
1830        assert_eq!(ft, FieldType::Any);
1831    }
1832
1833    #[test]
1834    fn test_type_annotation_to_field_type_generic_hashmap() {
1835        let ann = TypeAnnotation::Generic {
1836            name: "HashMap".to_string(),
1837            args: vec![
1838                TypeAnnotation::Basic("string".to_string()),
1839                TypeAnnotation::Basic("int".to_string()),
1840            ],
1841        };
1842        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
1843        assert_eq!(ft, FieldType::Any);
1844    }
1845
1846    #[test]
1847    fn test_type_annotation_to_field_type_generic_user_struct() {
1848        let ann = TypeAnnotation::Generic {
1849            name: "MyContainer".to_string(),
1850            args: vec![TypeAnnotation::Basic("string".to_string())],
1851        };
1852        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
1853        assert_eq!(ft, FieldType::Object("MyContainer".to_string()));
1854    }
1855}