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            if self.ref_locals.contains(&local_idx) {
449                // Forward an existing reference parameter by value (TAG_REF).
450                self.emit(Instruction::new(
451                    OpCode::LoadLocal,
452                    Some(Operand::Local(local_idx)),
453                ));
454                return Ok(());
455            }
456            let source_loc = self.span_to_source_location(span);
457            self.borrow_checker
458                .create_borrow(local_idx, local_idx, mode, span, Some(source_loc))
459                .map_err(|e| match e {
460                    ShapeError::SemanticError { message, location } => {
461                        let user_msg = message
462                            .replace(&format!("(slot {})", local_idx), &format!("'{}'", name));
463                        ShapeError::SemanticError {
464                            message: user_msg,
465                            location,
466                        }
467                    }
468                    other => other,
469                })?;
470            self.emit(Instruction::new(
471                OpCode::MakeRef,
472                Some(Operand::Local(local_idx)),
473            ));
474            Ok(())
475        } else if let Some(scoped_name) = self.resolve_scoped_module_binding_name(name) {
476            let Some(&binding_idx) = self.module_bindings.get(&scoped_name) else {
477                return Err(ShapeError::SemanticError {
478                    message: format!(
479                        "[B0004] reference argument must be a local or module_binding variable, got '{}'",
480                        name
481                    ),
482                    location: Some(self.span_to_source_location(span)),
483                });
484            };
485            // Reject exclusive borrows of const module bindings
486            if mode == BorrowMode::Exclusive && self.const_module_bindings.contains(&binding_idx) {
487                return Err(ShapeError::SemanticError {
488                    message: format!(
489                        "Cannot pass const variable '{}' by exclusive reference",
490                        name
491                    ),
492                    location: Some(self.span_to_source_location(span)),
493                });
494            }
495            // Borrow module_bindings via a local shadow and write it back after the call.
496            let shadow_local = self.declare_temp_local("__module_binding_ref_shadow_")?;
497            self.emit(Instruction::new(
498                OpCode::LoadModuleBinding,
499                Some(Operand::ModuleBinding(binding_idx)),
500            ));
501            self.emit(Instruction::new(
502                OpCode::StoreLocal,
503                Some(Operand::Local(shadow_local)),
504            ));
505            let source_loc = self.span_to_source_location(span);
506            self.borrow_checker.create_borrow(
507                shadow_local,
508                shadow_local,
509                mode,
510                span,
511                Some(source_loc),
512            )?;
513            self.emit(Instruction::new(
514                OpCode::MakeRef,
515                Some(Operand::Local(shadow_local)),
516            ));
517            self.record_call_arg_module_binding_writeback(shadow_local, binding_idx);
518            Ok(())
519        } else if let Some(func_idx) = self.find_function(name) {
520            // Function name passed as reference argument: create a temporary local
521            // with the function constant and make a reference to it.
522            let temp = self.declare_temp_local("__fn_ref_")?;
523            let const_idx = self
524                .program
525                .add_constant(Constant::Function(func_idx as u16));
526            self.emit(Instruction::new(
527                OpCode::PushConst,
528                Some(Operand::Const(const_idx)),
529            ));
530            self.emit(Instruction::new(
531                OpCode::StoreLocal,
532                Some(Operand::Local(temp)),
533            ));
534            let source_loc = self.span_to_source_location(span);
535            self.borrow_checker
536                .create_borrow(temp, temp, mode, span, Some(source_loc))?;
537            self.emit(Instruction::new(
538                OpCode::MakeRef,
539                Some(Operand::Local(temp)),
540            ));
541            Ok(())
542        } else {
543            Err(ShapeError::SemanticError {
544                message: format!(
545                    "[B0004] reference argument must be a local or module_binding variable, got '{}'",
546                    name
547                ),
548                location: Some(self.span_to_source_location(span)),
549            })
550        }
551    }
552
553    /// Push a new scope
554    pub(super) fn push_scope(&mut self) {
555        self.locals.push(HashMap::new());
556        self.type_tracker.push_scope();
557        self.borrow_checker.enter_region();
558    }
559
560    /// Pop a scope
561    pub(super) fn pop_scope(&mut self) {
562        self.borrow_checker.exit_region();
563        self.locals.pop();
564        self.type_tracker.pop_scope();
565    }
566
567    /// Declare a local variable
568    pub(super) fn declare_local(&mut self, name: &str) -> Result<u16> {
569        let idx = self.next_local;
570        self.next_local += 1;
571
572        if let Some(scope) = self.locals.last_mut() {
573            scope.insert(name.to_string(), idx);
574        }
575
576        Ok(idx)
577    }
578
579    /// Resolve a local variable
580    pub(super) fn resolve_local(&self, name: &str) -> Option<u16> {
581        for scope in self.locals.iter().rev() {
582            if let Some(&idx) = scope.get(name) {
583                return Some(idx);
584            }
585        }
586        None
587    }
588
589    /// Declare a temporary local variable
590    pub(super) fn declare_temp_local(&mut self, prefix: &str) -> Result<u16> {
591        let name = format!("{}{}", prefix, self.next_local);
592        self.declare_local(&name)
593    }
594
595    /// Set type info for an existing local variable
596    pub(super) fn set_local_type_info(&mut self, slot: u16, type_name: &str) {
597        let info = if let Some(schema) = self.type_tracker.schema_registry().get(type_name) {
598            VariableTypeInfo::known(schema.id, type_name.to_string())
599        } else {
600            VariableTypeInfo::named(type_name.to_string())
601        };
602        self.type_tracker.set_local_type(slot, info);
603    }
604
605    /// Set type info for a module_binding variable
606    pub(super) fn set_module_binding_type_info(&mut self, slot: u16, type_name: &str) {
607        let info = if let Some(schema) = self.type_tracker.schema_registry().get(type_name) {
608            VariableTypeInfo::known(schema.id, type_name.to_string())
609        } else {
610            VariableTypeInfo::named(type_name.to_string())
611        };
612        self.type_tracker.set_binding_type(slot, info);
613    }
614
615    /// Capture local storage hints for a compiled function.
616    ///
617    /// Must be called before the function scope is popped so the type tracker still
618    /// has local slot metadata. Also populates the function's `FrameDescriptor` so
619    /// the verifier and executor can use per-slot type info for trusted opcodes.
620    pub(super) fn capture_function_local_storage_hints(&mut self, func_idx: usize) {
621        let Some(func) = self.program.functions.get(func_idx) else {
622            return;
623        };
624        let hints: Vec<StorageHint> = (0..func.locals_count)
625            .map(|slot| self.type_tracker.get_local_storage_hint(slot))
626            .collect();
627
628        // Populate FrameDescriptor on the function for trusted opcode verification.
629        let has_any_known = hints.iter().any(|h| *h != StorageHint::Unknown);
630        let code_end = if func.body_length > 0 {
631            func.entry_point + func.body_length
632        } else {
633            self.program.instructions.len()
634        };
635        let has_trusted = self.program.instructions[func.entry_point..code_end]
636            .iter()
637            .any(|i| i.opcode.is_trusted());
638        if has_any_known || has_trusted {
639            self.program.functions[func_idx].frame_descriptor = Some(
640                crate::type_tracking::FrameDescriptor::from_slots(hints.clone()),
641            );
642        }
643
644        if self.program.function_local_storage_hints.len() <= func_idx {
645            self.program
646                .function_local_storage_hints
647                .resize(func_idx + 1, Vec::new());
648        }
649        self.program.function_local_storage_hints[func_idx] = hints;
650    }
651
652    /// Populate program-level storage hints for top-level locals and module bindings.
653    pub(super) fn populate_program_storage_hints(&mut self) {
654        let top_hints: Vec<StorageHint> = (0..self.next_local)
655            .map(|slot| self.type_tracker.get_local_storage_hint(slot))
656            .collect();
657        self.program.top_level_local_storage_hints = top_hints.clone();
658
659        // Build top-level FrameDescriptor so JIT can use per-slot type info
660        let has_any_known = top_hints.iter().any(|h| *h != StorageHint::Unknown);
661        let has_trusted = self.program.instructions
662            .iter()
663            .any(|i| i.opcode.is_trusted());
664        if has_any_known || has_trusted {
665            self.program.top_level_frame =
666                Some(crate::type_tracking::FrameDescriptor::from_slots(top_hints));
667        }
668
669        let mut module_binding_hints = vec![StorageHint::Unknown; self.module_bindings.len()];
670        for &idx in self.module_bindings.values() {
671            if let Some(slot) = module_binding_hints.get_mut(idx as usize) {
672                *slot = self.type_tracker.get_module_binding_storage_hint(idx);
673            }
674        }
675        self.program.module_binding_storage_hints = module_binding_hints;
676
677        if self.program.function_local_storage_hints.len() < self.program.functions.len() {
678            self.program
679                .function_local_storage_hints
680                .resize(self.program.functions.len(), Vec::new());
681        } else if self.program.function_local_storage_hints.len() > self.program.functions.len() {
682            self.program
683                .function_local_storage_hints
684                .truncate(self.program.functions.len());
685        }
686    }
687
688    /// Propagate the current expression's inferred type metadata to a target slot.
689    ///
690    /// Used by assignment sites to keep mutable locals/module_bindings typed when
691    /// safe, and to clear stale hints when assigning unknown/dynamic values.
692    pub(super) fn propagate_assignment_type_to_slot(
693        &mut self,
694        slot: u16,
695        is_local: bool,
696        allow_number_hint: bool,
697    ) {
698        if let Some(ref info) = self.last_expr_type_info {
699            if info.is_indexed()
700                || info.is_datatable()
701                || info.schema_id.is_some()
702                || Self::is_array_type_name(info.type_name.as_deref())
703            {
704                if is_local {
705                    self.type_tracker.set_local_type(slot, info.clone());
706                } else {
707                    self.type_tracker.set_binding_type(slot, info.clone());
708                }
709                return;
710            }
711        }
712
713        if let Some(schema_id) = self.last_expr_schema {
714            let schema_name = self
715                .type_tracker
716                .schema_registry()
717                .get_by_id(schema_id)
718                .map(|s| s.name.clone())
719                .unwrap_or_else(|| format!("__anon_{}", schema_id));
720            let info = VariableTypeInfo::known(schema_id, schema_name);
721            if is_local {
722                self.type_tracker.set_local_type(slot, info);
723            } else {
724                self.type_tracker.set_binding_type(slot, info);
725            }
726            return;
727        }
728
729        if let Some(numeric_type) = self.last_expr_numeric_type {
730            let (type_name, hint) = match numeric_type {
731                crate::type_tracking::NumericType::Int => ("int", StorageHint::Int64),
732                crate::type_tracking::NumericType::IntWidth(w) => {
733                    use shape_ast::IntWidth;
734                    let hint = match w {
735                        IntWidth::I8 => StorageHint::Int8,
736                        IntWidth::U8 => StorageHint::UInt8,
737                        IntWidth::I16 => StorageHint::Int16,
738                        IntWidth::U16 => StorageHint::UInt16,
739                        IntWidth::I32 => StorageHint::Int32,
740                        IntWidth::U32 => StorageHint::UInt32,
741                        IntWidth::U64 => StorageHint::UInt64,
742                    };
743                    (w.type_name(), hint)
744                }
745                crate::type_tracking::NumericType::Number => {
746                    if !allow_number_hint {
747                        if is_local {
748                            self.type_tracker
749                                .set_local_type(slot, VariableTypeInfo::unknown());
750                        } else {
751                            self.type_tracker
752                                .set_binding_type(slot, VariableTypeInfo::unknown());
753                        }
754                        return;
755                    }
756                    ("number", StorageHint::Float64)
757                }
758                // Decimal typed opcodes are not JIT-compiled yet.
759                crate::type_tracking::NumericType::Decimal => {
760                    if is_local {
761                        self.type_tracker
762                            .set_local_type(slot, VariableTypeInfo::unknown());
763                    } else {
764                        self.type_tracker
765                            .set_binding_type(slot, VariableTypeInfo::unknown());
766                    }
767                    return;
768                }
769            };
770            let info = VariableTypeInfo::with_storage(type_name.to_string(), hint);
771            if is_local {
772                self.type_tracker.set_local_type(slot, info);
773            } else {
774                self.type_tracker.set_binding_type(slot, info);
775            }
776            return;
777        }
778
779        // Assignment to an unknown/dynamic expression invalidates prior hints.
780        if is_local {
781            self.type_tracker
782                .set_local_type(slot, VariableTypeInfo::unknown());
783        } else {
784            self.type_tracker
785                .set_binding_type(slot, VariableTypeInfo::unknown());
786        }
787    }
788
789    /// Propagate current expression type metadata to an identifier target.
790    ///
791    /// Reference locals are skipped because assignment writes through to a pointee.
792    pub(super) fn propagate_assignment_type_to_identifier(&mut self, name: &str) {
793        if let Some(local_idx) = self.resolve_local(name) {
794            if self.ref_locals.contains(&local_idx) {
795                return;
796            }
797            self.propagate_assignment_type_to_slot(local_idx, true, true);
798            return;
799        }
800
801        let scoped_name = self
802            .resolve_scoped_module_binding_name(name)
803            .unwrap_or_else(|| name.to_string());
804        let binding_idx = self.get_or_create_module_binding(&scoped_name);
805        self.propagate_assignment_type_to_slot(binding_idx, false, true);
806    }
807
808    /// Get the type tracker (for external configuration)
809    pub fn type_tracker(&self) -> &TypeTracker {
810        &self.type_tracker
811    }
812
813    /// Get mutable type tracker (for registering types)
814    pub fn type_tracker_mut(&mut self) -> &mut TypeTracker {
815        &mut self.type_tracker
816    }
817
818    /// Resolve a column name to its index using the data schema.
819    /// Returns an error if no schema is provided or the column doesn't exist.
820    pub(super) fn resolve_column_index(&self, field: &str) -> Result<u32> {
821        self.program
822            .data_schema
823            .as_ref()
824            .ok_or_else(|| ShapeError::RuntimeError {
825                message: format!(
826                    "No data schema provided. Cannot resolve field '{}'. \
827                     Hint: Use stdlib/finance to load market data with OHLCV schema.",
828                    field
829                ),
830                location: None,
831            })?
832            .get_index(field)
833            .ok_or_else(|| ShapeError::RuntimeError {
834                message: format!(
835                    "Unknown column '{}' in data schema. Available columns: {:?}",
836                    field,
837                    self.program
838                        .data_schema
839                        .as_ref()
840                        .map(|s| &s.column_names)
841                        .unwrap_or(&vec![])
842                ),
843                location: None,
844            })
845    }
846
847    /// Check if a field name is a known data column in the schema.
848    pub(super) fn is_data_column(&self, field: &str) -> bool {
849        self.program
850            .data_schema
851            .as_ref()
852            .map(|s| s.get_index(field).is_some())
853            .unwrap_or(false)
854    }
855
856    /// Collect all outer scope variables
857    pub(super) fn collect_outer_scope_vars(&self) -> Vec<String> {
858        let mut names = BTreeSet::new();
859        for scope in &self.locals {
860            for name in scope.keys() {
861                names.insert(name.clone());
862            }
863        }
864        for name in self.module_bindings.keys() {
865            names.insert(name.clone());
866        }
867        names.into_iter().collect()
868    }
869
870    /// Get or create a module_binding variable
871    pub(super) fn get_or_create_module_binding(&mut self, name: &str) -> u16 {
872        if let Some(&idx) = self.module_bindings.get(name) {
873            idx
874        } else {
875            let idx = self.next_global;
876            self.next_global += 1;
877            self.module_bindings.insert(name.to_string(), idx);
878            idx
879        }
880    }
881
882    pub(super) fn resolve_scoped_module_binding_name(&self, name: &str) -> Option<String> {
883        if self.module_bindings.contains_key(name) {
884            return Some(name.to_string());
885        }
886        for module_path in self.module_scope_stack.iter().rev() {
887            let candidate = format!("{}::{}", module_path, name);
888            if self.module_bindings.contains_key(&candidate) {
889                return Some(candidate);
890            }
891        }
892        None
893    }
894
895    pub(super) fn resolve_scoped_function_name(&self, name: &str) -> Option<String> {
896        if self.program.functions.iter().any(|f| f.name == name) {
897            return Some(name.to_string());
898        }
899        for module_path in self.module_scope_stack.iter().rev() {
900            let candidate = format!("{}::{}", module_path, name);
901            if self.program.functions.iter().any(|f| f.name == candidate) {
902                return Some(candidate);
903            }
904        }
905        None
906    }
907
908    /// Find a function by name
909    pub(super) fn find_function(&self, name: &str) -> Option<usize> {
910        // Check function aliases first (e.g., __original__ -> shadow function).
911        if let Some(actual_name) = self.function_aliases.get(name) {
912            if let Some(idx) = self
913                .program
914                .functions
915                .iter()
916                .position(|f| f.name == *actual_name)
917            {
918                return Some(idx);
919            }
920        }
921
922        // Try direct/scoped resolution
923        if let Some(resolved) = self.resolve_scoped_function_name(name) {
924            if let Some(idx) = self
925                .program
926                .functions
927                .iter()
928                .position(|f| f.name == resolved)
929            {
930                return Some(idx);
931            }
932        }
933
934        // If direct lookup failed, check imported_names for alias -> original name mapping.
935        // When a function is imported with an alias (e.g., `use { foo as bar } from "module"`),
936        // the function is registered under its original (possibly module-qualified) name,
937        // but the user refers to it by the alias.
938        if let Some(imported) = self.imported_names.get(name) {
939            let original = &imported.original_name;
940            // Try direct match on the original name
941            if let Some(idx) = self
942                .program
943                .functions
944                .iter()
945                .position(|f| f.name == *original)
946            {
947                return Some(idx);
948            }
949            // Try scoped resolution on the original name
950            if let Some(resolved) = self.resolve_scoped_function_name(original) {
951                if let Some(idx) = self
952                    .program
953                    .functions
954                    .iter()
955                    .position(|f| f.name == resolved)
956                {
957                    return Some(idx);
958                }
959            }
960        }
961
962        None
963    }
964
965    /// Resolve the receiver's type name for extend method dispatch.
966    ///
967    /// Determines the Shape type name from all available compiler state:
968    /// - `last_expr_type_info.type_name` for TypedObjects (e.g., "Point", "Candle")
969    /// - `last_expr_numeric_type` for numeric types → "Int", "Number", "Decimal"
970    /// - Receiver expression analysis for arrays, strings, booleans
971    ///
972    /// Returns the base type name (e.g., "Vec" not "Vec<int>") suitable for
973    /// extend method lookup as "Type.method".
974    pub(super) fn resolve_receiver_extend_type(
975        &self,
976        receiver: &shape_ast::ast::Expr,
977        receiver_type_info: &Option<crate::type_tracking::VariableTypeInfo>,
978        _receiver_schema: Option<u32>,
979    ) -> Option<String> {
980        // 1. Numeric type from typed opcode tracking — checked first because
981        //    the type tracker stores lowercase names ("int", "number") while
982        //    extend blocks use capitalized TypeName ("Int", "Number", "Decimal").
983        if let Some(numeric) = self.last_expr_numeric_type {
984            return Some(
985                match numeric {
986                    crate::type_tracking::NumericType::Int
987                    | crate::type_tracking::NumericType::IntWidth(_) => "Int",
988                    crate::type_tracking::NumericType::Number => "Number",
989                    crate::type_tracking::NumericType::Decimal => "Decimal",
990                }
991                .to_string(),
992            );
993        }
994
995        // 2. TypedObject type name (user-defined types like Point, Candle)
996        if let Some(info) = receiver_type_info {
997            if let Some(type_name) = &info.type_name {
998                // Strip generic params: "Vec<int>" → "Vec"
999                let base = type_name.split('<').next().unwrap_or(type_name);
1000                return Some(base.to_string());
1001            }
1002        }
1003
1004        // 3. Infer from receiver expression shape
1005        match receiver {
1006            shape_ast::ast::Expr::Literal(lit, _) => match lit {
1007                shape_ast::ast::Literal::String(_)
1008                | shape_ast::ast::Literal::FormattedString { .. }
1009                | shape_ast::ast::Literal::ContentString { .. } => Some("String".to_string()),
1010                shape_ast::ast::Literal::Bool(_) => Some("Bool".to_string()),
1011                _ => None,
1012            },
1013            shape_ast::ast::Expr::Array(..) => Some("Vec".to_string()),
1014            _ => None,
1015        }
1016    }
1017
1018    /// Emit store instruction for an identifier
1019    pub(super) fn emit_store_identifier(&mut self, name: &str) -> Result<()> {
1020        // Mutable closure captures: emit StoreClosure to write to the shared upvalue
1021        if let Some(&upvalue_idx) = self.mutable_closure_captures.get(name) {
1022            self.emit(Instruction::new(
1023                OpCode::StoreClosure,
1024                Some(Operand::Local(upvalue_idx)),
1025            ));
1026            return Ok(());
1027        }
1028        if let Some(local_idx) = self.resolve_local(name) {
1029            if self.ref_locals.contains(&local_idx) {
1030                self.emit(Instruction::new(
1031                    OpCode::DerefStore,
1032                    Some(Operand::Local(local_idx)),
1033                ));
1034            } else {
1035                self.emit(Instruction::new(
1036                    OpCode::StoreLocal,
1037                    Some(Operand::Local(local_idx)),
1038                ));
1039                // Patch StoreLocal → StoreLocalTyped for width-typed locals
1040                if let Some(type_name) = self
1041                    .type_tracker
1042                    .get_local_type(local_idx)
1043                    .and_then(|info| info.type_name.as_deref())
1044                {
1045                    if let Some(w) = shape_ast::IntWidth::from_name(type_name) {
1046                        if let Some(last) = self.program.instructions.last_mut() {
1047                            if last.opcode == OpCode::StoreLocal {
1048                                last.opcode = OpCode::StoreLocalTyped;
1049                                last.operand = Some(Operand::TypedLocal(
1050                                    local_idx,
1051                                    crate::bytecode::NumericWidth::from_int_width(w),
1052                                ));
1053                            }
1054                        }
1055                    }
1056                }
1057            }
1058        } else {
1059            let scoped_name = self
1060                .resolve_scoped_module_binding_name(name)
1061                .unwrap_or_else(|| name.to_string());
1062            let binding_idx = self.get_or_create_module_binding(&scoped_name);
1063            self.emit(Instruction::new(
1064                OpCode::StoreModuleBinding,
1065                Some(Operand::ModuleBinding(binding_idx)),
1066            ));
1067        }
1068        Ok(())
1069    }
1070
1071    /// Get built-in function by name
1072    pub(super) fn get_builtin_function(&self, name: &str) -> Option<BuiltinFunction> {
1073        // Internal builtins are only accessible from stdlib functions.
1074        // User code must use the safe wrappers (e.g. std::core::math).
1075        // Note: __into_* and __try_into_* are NOT gated — the compiler generates
1076        // calls to them for type assertions (x as int, x try as int).
1077        if !self.allow_internal_builtins
1078            && (name.starts_with("__native_")
1079                || name.starts_with("__intrinsic_")
1080                || name.starts_with("__json_"))
1081        {
1082            return None;
1083        }
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            "__intrinsic_map" => Some(BuiltinFunction::Map),
1137            "__intrinsic_filter" => Some(BuiltinFunction::Filter),
1138            "__intrinsic_reduce" => Some(BuiltinFunction::Reduce),
1139            "print" => Some(BuiltinFunction::Print),
1140            "format" => Some(BuiltinFunction::Format),
1141            "len" | "count" => Some(BuiltinFunction::Len),
1142            // "throw" removed: Shape uses Result types
1143            "__intrinsic_snapshot" | "snapshot" => Some(BuiltinFunction::Snapshot),
1144            "exit" => Some(BuiltinFunction::Exit),
1145            "range" => Some(BuiltinFunction::Range),
1146            "is_number" | "isNumber" => Some(BuiltinFunction::IsNumber),
1147            "is_string" | "isString" => Some(BuiltinFunction::IsString),
1148            "is_bool" | "isBool" => Some(BuiltinFunction::IsBool),
1149            "is_array" | "isArray" => Some(BuiltinFunction::IsArray),
1150            "is_object" | "isObject" => Some(BuiltinFunction::IsObject),
1151            "is_data_row" | "isDataRow" => Some(BuiltinFunction::IsDataRow),
1152            "to_string" | "toString" => Some(BuiltinFunction::ToString),
1153            "to_number" | "toNumber" => Some(BuiltinFunction::ToNumber),
1154            "to_bool" | "toBool" => Some(BuiltinFunction::ToBool),
1155            "__into_int" => Some(BuiltinFunction::IntoInt),
1156            "__into_number" => Some(BuiltinFunction::IntoNumber),
1157            "__into_decimal" => Some(BuiltinFunction::IntoDecimal),
1158            "__into_bool" => Some(BuiltinFunction::IntoBool),
1159            "__into_string" => Some(BuiltinFunction::IntoString),
1160            "__try_into_int" => Some(BuiltinFunction::TryIntoInt),
1161            "__try_into_number" => Some(BuiltinFunction::TryIntoNumber),
1162            "__try_into_decimal" => Some(BuiltinFunction::TryIntoDecimal),
1163            "__try_into_bool" => Some(BuiltinFunction::TryIntoBool),
1164            "__try_into_string" => Some(BuiltinFunction::TryIntoString),
1165            "__native_ptr_size" => Some(BuiltinFunction::NativePtrSize),
1166            "__native_ptr_new_cell" => Some(BuiltinFunction::NativePtrNewCell),
1167            "__native_ptr_free_cell" => Some(BuiltinFunction::NativePtrFreeCell),
1168            "__native_ptr_read_ptr" => Some(BuiltinFunction::NativePtrReadPtr),
1169            "__native_ptr_write_ptr" => Some(BuiltinFunction::NativePtrWritePtr),
1170            "__native_table_from_arrow_c" => Some(BuiltinFunction::NativeTableFromArrowC),
1171            "__native_table_from_arrow_c_typed" => {
1172                Some(BuiltinFunction::NativeTableFromArrowCTyped)
1173            }
1174            "__native_table_bind_type" => Some(BuiltinFunction::NativeTableBindType),
1175            "fold" => Some(BuiltinFunction::ControlFold),
1176
1177            // Math intrinsics
1178            "__intrinsic_sum" => Some(BuiltinFunction::IntrinsicSum),
1179            "__intrinsic_mean" => Some(BuiltinFunction::IntrinsicMean),
1180            "__intrinsic_min" => Some(BuiltinFunction::IntrinsicMin),
1181            "__intrinsic_max" => Some(BuiltinFunction::IntrinsicMax),
1182            "__intrinsic_std" => Some(BuiltinFunction::IntrinsicStd),
1183            "__intrinsic_variance" => Some(BuiltinFunction::IntrinsicVariance),
1184
1185            // Random intrinsics
1186            "__intrinsic_random" => Some(BuiltinFunction::IntrinsicRandom),
1187            "__intrinsic_random_int" => Some(BuiltinFunction::IntrinsicRandomInt),
1188            "__intrinsic_random_seed" => Some(BuiltinFunction::IntrinsicRandomSeed),
1189            "__intrinsic_random_normal" => Some(BuiltinFunction::IntrinsicRandomNormal),
1190            "__intrinsic_random_array" => Some(BuiltinFunction::IntrinsicRandomArray),
1191
1192            // Distribution intrinsics
1193            "__intrinsic_dist_uniform" => Some(BuiltinFunction::IntrinsicDistUniform),
1194            "__intrinsic_dist_lognormal" => Some(BuiltinFunction::IntrinsicDistLognormal),
1195            "__intrinsic_dist_exponential" => Some(BuiltinFunction::IntrinsicDistExponential),
1196            "__intrinsic_dist_poisson" => Some(BuiltinFunction::IntrinsicDistPoisson),
1197            "__intrinsic_dist_sample_n" => Some(BuiltinFunction::IntrinsicDistSampleN),
1198
1199            // Stochastic process intrinsics
1200            "__intrinsic_brownian_motion" => Some(BuiltinFunction::IntrinsicBrownianMotion),
1201            "__intrinsic_gbm" => Some(BuiltinFunction::IntrinsicGbm),
1202            "__intrinsic_ou_process" => Some(BuiltinFunction::IntrinsicOuProcess),
1203            "__intrinsic_random_walk" => Some(BuiltinFunction::IntrinsicRandomWalk),
1204
1205            // Rolling intrinsics
1206            "__intrinsic_rolling_sum" => Some(BuiltinFunction::IntrinsicRollingSum),
1207            "__intrinsic_rolling_mean" => Some(BuiltinFunction::IntrinsicRollingMean),
1208            "__intrinsic_rolling_std" => Some(BuiltinFunction::IntrinsicRollingStd),
1209            "__intrinsic_rolling_min" => Some(BuiltinFunction::IntrinsicRollingMin),
1210            "__intrinsic_rolling_max" => Some(BuiltinFunction::IntrinsicRollingMax),
1211            "__intrinsic_ema" => Some(BuiltinFunction::IntrinsicEma),
1212            "__intrinsic_linear_recurrence" => Some(BuiltinFunction::IntrinsicLinearRecurrence),
1213
1214            // Series intrinsics
1215            "__intrinsic_shift" => Some(BuiltinFunction::IntrinsicShift),
1216            "__intrinsic_diff" => Some(BuiltinFunction::IntrinsicDiff),
1217            "__intrinsic_pct_change" => Some(BuiltinFunction::IntrinsicPctChange),
1218            "__intrinsic_fillna" => Some(BuiltinFunction::IntrinsicFillna),
1219            "__intrinsic_cumsum" => Some(BuiltinFunction::IntrinsicCumsum),
1220            "__intrinsic_cumprod" => Some(BuiltinFunction::IntrinsicCumprod),
1221            "__intrinsic_clip" => Some(BuiltinFunction::IntrinsicClip),
1222
1223            // Statistical intrinsics
1224            "__intrinsic_correlation" => Some(BuiltinFunction::IntrinsicCorrelation),
1225            "__intrinsic_covariance" => Some(BuiltinFunction::IntrinsicCovariance),
1226            "__intrinsic_percentile" => Some(BuiltinFunction::IntrinsicPercentile),
1227            "__intrinsic_median" => Some(BuiltinFunction::IntrinsicMedian),
1228
1229            // Character code intrinsics
1230            "__intrinsic_char_code" => Some(BuiltinFunction::IntrinsicCharCode),
1231            "__intrinsic_from_char_code" => Some(BuiltinFunction::IntrinsicFromCharCode),
1232
1233            // Series access
1234            "__intrinsic_series" => Some(BuiltinFunction::IntrinsicSeries),
1235
1236            // Reflection
1237            "reflect" => Some(BuiltinFunction::Reflect),
1238
1239            // Additional math builtins
1240            "sign" => Some(BuiltinFunction::Sign),
1241            "gcd" => Some(BuiltinFunction::Gcd),
1242            "lcm" => Some(BuiltinFunction::Lcm),
1243            "hypot" => Some(BuiltinFunction::Hypot),
1244            "clamp" => Some(BuiltinFunction::Clamp),
1245            "isNaN" | "is_nan" => Some(BuiltinFunction::IsNaN),
1246            "isFinite" | "is_finite" => Some(BuiltinFunction::IsFinite),
1247
1248            _ => None,
1249        }
1250    }
1251
1252    /// Check if a builtin function requires arg count
1253    pub(super) fn builtin_requires_arg_count(&self, builtin: BuiltinFunction) -> bool {
1254        matches!(
1255            builtin,
1256            BuiltinFunction::Abs
1257                | BuiltinFunction::Min
1258                | BuiltinFunction::Max
1259                | BuiltinFunction::Sqrt
1260                | BuiltinFunction::Ln
1261                | BuiltinFunction::Pow
1262                | BuiltinFunction::Exp
1263                | BuiltinFunction::Log
1264                | BuiltinFunction::Floor
1265                | BuiltinFunction::Ceil
1266                | BuiltinFunction::Round
1267                | BuiltinFunction::Sin
1268                | BuiltinFunction::Cos
1269                | BuiltinFunction::Tan
1270                | BuiltinFunction::Asin
1271                | BuiltinFunction::Acos
1272                | BuiltinFunction::Atan
1273                | BuiltinFunction::StdDev
1274                | BuiltinFunction::Range
1275                | BuiltinFunction::Slice
1276                | BuiltinFunction::Push
1277                | BuiltinFunction::Pop
1278                | BuiltinFunction::First
1279                | BuiltinFunction::Last
1280                | BuiltinFunction::Zip
1281                | BuiltinFunction::Map
1282                | BuiltinFunction::Filter
1283                | BuiltinFunction::Reduce
1284                | BuiltinFunction::ForEach
1285                | BuiltinFunction::Find
1286                | BuiltinFunction::FindIndex
1287                | BuiltinFunction::Some
1288                | BuiltinFunction::Every
1289                | BuiltinFunction::SomeCtor
1290                | BuiltinFunction::OkCtor
1291                | BuiltinFunction::ErrCtor
1292                | BuiltinFunction::HashMapCtor
1293                | BuiltinFunction::SetCtor
1294                | BuiltinFunction::DequeCtor
1295                | BuiltinFunction::PriorityQueueCtor
1296                | BuiltinFunction::MutexCtor
1297                | BuiltinFunction::AtomicCtor
1298                | BuiltinFunction::LazyCtor
1299                | BuiltinFunction::ChannelCtor
1300                | BuiltinFunction::Print
1301                | BuiltinFunction::Format
1302                | BuiltinFunction::Len
1303                // BuiltinFunction::Throw removed
1304                | BuiltinFunction::Snapshot
1305                | BuiltinFunction::ObjectRest
1306                | BuiltinFunction::IsNumber
1307                | BuiltinFunction::IsString
1308                | BuiltinFunction::IsBool
1309                | BuiltinFunction::IsArray
1310                | BuiltinFunction::IsObject
1311                | BuiltinFunction::IsDataRow
1312                | BuiltinFunction::ToString
1313                | BuiltinFunction::ToNumber
1314                | BuiltinFunction::ToBool
1315                | BuiltinFunction::IntoInt
1316                | BuiltinFunction::IntoNumber
1317                | BuiltinFunction::IntoDecimal
1318                | BuiltinFunction::IntoBool
1319                | BuiltinFunction::IntoString
1320                | BuiltinFunction::TryIntoInt
1321                | BuiltinFunction::TryIntoNumber
1322                | BuiltinFunction::TryIntoDecimal
1323                | BuiltinFunction::TryIntoBool
1324                | BuiltinFunction::TryIntoString
1325                | BuiltinFunction::NativePtrSize
1326                | BuiltinFunction::NativePtrNewCell
1327                | BuiltinFunction::NativePtrFreeCell
1328                | BuiltinFunction::NativePtrReadPtr
1329                | BuiltinFunction::NativePtrWritePtr
1330                | BuiltinFunction::NativeTableFromArrowC
1331                | BuiltinFunction::NativeTableFromArrowCTyped
1332                | BuiltinFunction::NativeTableBindType
1333                | BuiltinFunction::ControlFold
1334                | BuiltinFunction::IntrinsicSum
1335                | BuiltinFunction::IntrinsicMean
1336                | BuiltinFunction::IntrinsicMin
1337                | BuiltinFunction::IntrinsicMax
1338                | BuiltinFunction::IntrinsicStd
1339                | BuiltinFunction::IntrinsicVariance
1340                | BuiltinFunction::IntrinsicRandom
1341                | BuiltinFunction::IntrinsicRandomInt
1342                | BuiltinFunction::IntrinsicRandomSeed
1343                | BuiltinFunction::IntrinsicRandomNormal
1344                | BuiltinFunction::IntrinsicRandomArray
1345                | BuiltinFunction::IntrinsicDistUniform
1346                | BuiltinFunction::IntrinsicDistLognormal
1347                | BuiltinFunction::IntrinsicDistExponential
1348                | BuiltinFunction::IntrinsicDistPoisson
1349                | BuiltinFunction::IntrinsicDistSampleN
1350                | BuiltinFunction::IntrinsicBrownianMotion
1351                | BuiltinFunction::IntrinsicGbm
1352                | BuiltinFunction::IntrinsicOuProcess
1353                | BuiltinFunction::IntrinsicRandomWalk
1354                | BuiltinFunction::IntrinsicRollingSum
1355                | BuiltinFunction::IntrinsicRollingMean
1356                | BuiltinFunction::IntrinsicRollingStd
1357                | BuiltinFunction::IntrinsicRollingMin
1358                | BuiltinFunction::IntrinsicRollingMax
1359                | BuiltinFunction::IntrinsicEma
1360                | BuiltinFunction::IntrinsicLinearRecurrence
1361                | BuiltinFunction::IntrinsicShift
1362                | BuiltinFunction::IntrinsicDiff
1363                | BuiltinFunction::IntrinsicPctChange
1364                | BuiltinFunction::IntrinsicFillna
1365                | BuiltinFunction::IntrinsicCumsum
1366                | BuiltinFunction::IntrinsicCumprod
1367                | BuiltinFunction::IntrinsicClip
1368                | BuiltinFunction::IntrinsicCorrelation
1369                | BuiltinFunction::IntrinsicCovariance
1370                | BuiltinFunction::IntrinsicPercentile
1371                | BuiltinFunction::IntrinsicMedian
1372                | BuiltinFunction::IntrinsicCharCode
1373                | BuiltinFunction::IntrinsicFromCharCode
1374                | BuiltinFunction::IntrinsicSeries
1375                | BuiltinFunction::IntrinsicVecAbs
1376                | BuiltinFunction::IntrinsicVecSqrt
1377                | BuiltinFunction::IntrinsicVecLn
1378                | BuiltinFunction::IntrinsicVecExp
1379                | BuiltinFunction::IntrinsicVecAdd
1380                | BuiltinFunction::IntrinsicVecSub
1381                | BuiltinFunction::IntrinsicVecMul
1382                | BuiltinFunction::IntrinsicVecDiv
1383                | BuiltinFunction::IntrinsicVecMax
1384                | BuiltinFunction::IntrinsicVecMin
1385                | BuiltinFunction::IntrinsicVecSelect
1386                | BuiltinFunction::IntrinsicMatMulVec
1387                | BuiltinFunction::IntrinsicMatMulMat
1388                | BuiltinFunction::Sign
1389                | BuiltinFunction::Gcd
1390                | BuiltinFunction::Lcm
1391                | BuiltinFunction::Hypot
1392                | BuiltinFunction::Clamp
1393                | BuiltinFunction::IsNaN
1394                | BuiltinFunction::IsFinite
1395        )
1396    }
1397
1398    /// Check if a method name is a known built-in method on any VM type.
1399    /// Used by UFCS to determine if `receiver.method(args)` should be dispatched
1400    /// as a built-in method call or rewritten to `method(receiver, args)`.
1401    pub(super) fn is_known_builtin_method(method: &str) -> bool {
1402        // Array methods (from ARRAY_METHODS PHF map)
1403        matches!(method,
1404            "map" | "filter" | "reduce" | "forEach" | "find" | "findIndex"
1405            | "some" | "every" | "sort" | "groupBy" | "flatMap"
1406            | "len" | "length" | "first" | "last" | "reverse" | "slice"
1407            | "concat" | "take" | "drop" | "skip"
1408            | "indexOf" | "includes"
1409            | "join" | "flatten" | "unique" | "distinct" | "distinctBy"
1410            | "sum" | "avg" | "min" | "max" | "count"
1411            | "where" | "select" | "orderBy" | "thenBy" | "takeWhile"
1412            | "skipWhile" | "single" | "any" | "all"
1413            | "innerJoin" | "leftJoin" | "crossJoin"
1414            | "union" | "intersect" | "except"
1415        )
1416        // DataTable methods (from DATATABLE_METHODS PHF map)
1417        || matches!(method,
1418            "columns" | "column" | "head" | "tail" | "mean" | "std"
1419            | "describe" | "aggregate" | "group_by" | "index_by" | "indexBy"
1420            | "simulate" | "toMat" | "to_mat"
1421        )
1422        // Column methods (from COLUMN_METHODS PHF map)
1423        || matches!(method, "toArray")
1424        // IndexedTable methods (from INDEXED_TABLE_METHODS PHF map)
1425        || matches!(method, "resample" | "between")
1426        // Number methods handled inline in op_call_method
1427        || matches!(method,
1428            "toFixed" | "toInt" | "toNumber" | "to_number" | "floor" | "ceil" | "round"
1429            | "abs" | "sign" | "clamp"
1430        )
1431        // String methods handled inline
1432        || matches!(method,
1433            "toUpperCase" | "toLowerCase" | "trim" | "contains" | "startsWith"
1434            | "endsWith" | "split" | "replace" | "substring" | "charAt"
1435            | "padStart" | "padEnd" | "repeat" | "toString"
1436        )
1437        // Object methods handled by handle_object_method
1438        || matches!(method, "keys" | "values" | "has" | "get" | "set" | "len")
1439        // Universal intrinsic methods
1440        || matches!(method, "type")
1441    }
1442
1443    /// Try to track a `Table<T>` type annotation as a DataTable variable.
1444    ///
1445    /// If the annotation is `Generic { name: "Table", args: [Reference(T)] }`,
1446    /// looks up T's schema and marks the variable as `is_datatable`.
1447    pub(super) fn try_track_datatable_type(
1448        &mut self,
1449        type_ann: &shape_ast::ast::TypeAnnotation,
1450        slot: u16,
1451        is_local: bool,
1452    ) -> shape_ast::error::Result<()> {
1453        use shape_ast::ast::TypeAnnotation;
1454        if let TypeAnnotation::Generic { name, args } = type_ann {
1455            if name == "Table" && args.len() == 1 {
1456                let inner_name = match &args[0] {
1457                    TypeAnnotation::Reference(t) => Some(t.as_str()),
1458                    TypeAnnotation::Basic(t) => Some(t.as_str()),
1459                    _ => None,
1460                };
1461                if let Some(type_name) = inner_name {
1462                    let schema_id = self
1463                        .type_tracker
1464                        .schema_registry()
1465                        .get(type_name)
1466                        .map(|s| s.id);
1467                    if let Some(sid) = schema_id {
1468                        let info = crate::type_tracking::VariableTypeInfo::datatable(
1469                            sid,
1470                            type_name.to_string(),
1471                        );
1472                        if is_local {
1473                            self.type_tracker.set_local_type(slot, info);
1474                        } else {
1475                            self.type_tracker.set_binding_type(slot, info);
1476                        }
1477                    } else {
1478                        return Err(shape_ast::error::ShapeError::SemanticError {
1479                            message: format!(
1480                                "Unknown type '{}' in Table<{}> annotation",
1481                                type_name, type_name
1482                            ),
1483                            location: None,
1484                        });
1485                    }
1486                }
1487            }
1488        }
1489        Ok(())
1490    }
1491
1492    /// Check if a variable is a RowView (typed row from Arrow DataTable).
1493    pub(super) fn is_row_view_variable(&self, name: &str) -> bool {
1494        if let Some(local_idx) = self.resolve_local(name) {
1495            if let Some(info) = self.type_tracker.get_local_type(local_idx) {
1496                return info.is_row_view();
1497            }
1498        }
1499        if let Some(&binding_idx) = self.module_bindings.get(name) {
1500            if let Some(info) = self.type_tracker.get_binding_type(binding_idx) {
1501                return info.is_row_view();
1502            }
1503        }
1504        false
1505    }
1506
1507    /// Get the available field names for a RowView variable's schema.
1508    pub(super) fn get_row_view_field_names(&self, name: &str) -> Option<Vec<String>> {
1509        let type_name = if let Some(local_idx) = self.resolve_local(name) {
1510            self.type_tracker
1511                .get_local_type(local_idx)
1512                .and_then(|info| {
1513                    if info.is_row_view() {
1514                        info.type_name.clone()
1515                    } else {
1516                        None
1517                    }
1518                })
1519        } else if let Some(&binding_idx) = self.module_bindings.get(name) {
1520            self.type_tracker
1521                .get_binding_type(binding_idx)
1522                .and_then(|info| {
1523                    if info.is_row_view() {
1524                        info.type_name.clone()
1525                    } else {
1526                        None
1527                    }
1528                })
1529        } else {
1530            None
1531        };
1532
1533        if let Some(tn) = type_name {
1534            if let Some(schema) = self.type_tracker.schema_registry().get(&tn) {
1535                return Some(schema.field_names().map(|n| n.to_string()).collect());
1536            }
1537        }
1538        None
1539    }
1540
1541    /// Try to resolve a property access on a RowView variable to a column ID.
1542    ///
1543    /// Returns `Some(col_id)` if the variable is a tracked RowView and the field
1544    /// exists in its schema. Returns `None` if the variable isn't a RowView or
1545    /// the field is unknown (caller should emit a compile-time error).
1546    pub(super) fn try_resolve_row_view_column(
1547        &self,
1548        var_name: &str,
1549        field_name: &str,
1550    ) -> Option<u32> {
1551        // Check locals first, then module_bindings
1552        if let Some(local_idx) = self.resolve_local(var_name) {
1553            return self
1554                .type_tracker
1555                .get_row_view_column_id(local_idx, true, field_name);
1556        }
1557        if let Some(&binding_idx) = self.module_bindings.get(var_name) {
1558            return self
1559                .type_tracker
1560                .get_row_view_column_id(binding_idx, false, field_name);
1561        }
1562        None
1563    }
1564
1565    /// Determine the appropriate LoadCol opcode for a RowView field.
1566    ///
1567    /// Looks up the field's FieldType and maps it to the corresponding opcode.
1568    /// Falls back to LoadColF64 if the type can't be determined.
1569    pub(super) fn row_view_field_opcode(&self, var_name: &str, field_name: &str) -> OpCode {
1570        use shape_runtime::type_schema::FieldType;
1571
1572        let type_name = if let Some(local_idx) = self.resolve_local(var_name) {
1573            self.type_tracker
1574                .get_local_type(local_idx)
1575                .and_then(|info| info.type_name.clone())
1576        } else if let Some(&binding_idx) = self.module_bindings.get(var_name) {
1577            self.type_tracker
1578                .get_binding_type(binding_idx)
1579                .and_then(|info| info.type_name.clone())
1580        } else {
1581            None
1582        };
1583
1584        if let Some(type_name) = type_name {
1585            if let Some(schema) = self.type_tracker.schema_registry().get(&type_name) {
1586                if let Some(field) = schema.get_field(field_name) {
1587                    return match field.field_type {
1588                        FieldType::F64 => OpCode::LoadColF64,
1589                        FieldType::I64 | FieldType::Timestamp => OpCode::LoadColI64,
1590                        FieldType::Bool => OpCode::LoadColBool,
1591                        FieldType::String => OpCode::LoadColStr,
1592                        _ => OpCode::LoadColF64, // default
1593                    };
1594                }
1595            }
1596        }
1597        OpCode::LoadColF64 // default
1598    }
1599
1600    /// Resolve the NumericType for a RowView field (used for typed opcode emission).
1601    pub(super) fn resolve_row_view_field_numeric_type(
1602        &self,
1603        var_name: &str,
1604        field_name: &str,
1605    ) -> Option<crate::type_tracking::NumericType> {
1606        use crate::type_tracking::NumericType;
1607        use shape_runtime::type_schema::FieldType;
1608
1609        let type_name = if let Some(local_idx) = self.resolve_local(var_name) {
1610            self.type_tracker
1611                .get_local_type(local_idx)
1612                .and_then(|info| info.type_name.clone())
1613        } else if let Some(&binding_idx) = self.module_bindings.get(var_name) {
1614            self.type_tracker
1615                .get_binding_type(binding_idx)
1616                .and_then(|info| info.type_name.clone())
1617        } else {
1618            None
1619        };
1620
1621        if let Some(type_name) = type_name {
1622            if let Some(schema) = self.type_tracker.schema_registry().get(&type_name) {
1623                if let Some(field) = schema.get_field(field_name) {
1624                    return match field.field_type {
1625                        FieldType::F64 => Some(NumericType::Number),
1626                        FieldType::I64 | FieldType::Timestamp => Some(NumericType::Int),
1627                        FieldType::Decimal => Some(NumericType::Decimal),
1628                        _ => None,
1629                    };
1630                }
1631            }
1632        }
1633        None
1634    }
1635
1636    /// Convert a TypeAnnotation to a FieldType for TypeSchema registration
1637    pub(super) fn type_annotation_to_field_type(
1638        ann: &shape_ast::ast::TypeAnnotation,
1639    ) -> shape_runtime::type_schema::FieldType {
1640        use shape_ast::ast::TypeAnnotation;
1641        use shape_runtime::type_schema::FieldType;
1642        match ann {
1643            TypeAnnotation::Basic(s) => match s.as_str() {
1644                "number" | "float" | "f64" | "f32" => FieldType::F64,
1645                "i8" => FieldType::I8,
1646                "u8" => FieldType::U8,
1647                "i16" => FieldType::I16,
1648                "u16" => FieldType::U16,
1649                "i32" => FieldType::I32,
1650                "u32" => FieldType::U32,
1651                "u64" => FieldType::U64,
1652                "int" | "i64" | "integer" | "isize" | "usize" | "byte" | "char" => FieldType::I64,
1653                "string" | "str" => FieldType::String,
1654                "decimal" => FieldType::Decimal,
1655                "bool" | "boolean" => FieldType::Bool,
1656                "timestamp" => FieldType::Timestamp,
1657                // Non-primitive type names (e.g. "Server", "Inner") are nested
1658                // object references.  The parser emits Basic for `ident` matches
1659                // inside `basic_type`, so treat unknown names as Object references
1660                // to enable typed field access on nested structs.
1661                other => FieldType::Object(other.to_string()),
1662            },
1663            TypeAnnotation::Reference(s) => FieldType::Object(s.clone()),
1664            TypeAnnotation::Array(inner) => {
1665                FieldType::Array(Box::new(Self::type_annotation_to_field_type(inner)))
1666            }
1667            TypeAnnotation::Generic { name, .. } => match name.as_str() {
1668                // Generic containers that need NaN boxing
1669                "HashMap" | "Map" | "Result" | "Option" | "Set" => FieldType::Any,
1670                // User-defined generic structs — preserve the type name
1671                other => FieldType::Object(other.to_string()),
1672            },
1673            _ => FieldType::Any,
1674        }
1675    }
1676
1677    /// Evaluate an annotation argument expression to a string representation.
1678    /// Only handles compile-time evaluable expressions (literals).
1679    pub(super) fn eval_annotation_arg(expr: &shape_ast::ast::Expr) -> Option<String> {
1680        use shape_ast::ast::{Expr, Literal};
1681        match expr {
1682            Expr::Literal(Literal::String(s), _) => Some(s.clone()),
1683            Expr::Literal(Literal::Number(n), _) => Some(n.to_string()),
1684            Expr::Literal(Literal::Int(i), _) => Some(i.to_string()),
1685            Expr::Literal(Literal::Bool(b), _) => Some(b.to_string()),
1686            _ => None,
1687        }
1688    }
1689
1690    /// Get the schema ID for a `Table<T>` type annotation, if applicable.
1691    ///
1692    /// Returns `Some(schema_id)` if the annotation is `Table<T>` and `T` is a registered
1693    /// TypeSchema. Returns `None` otherwise.
1694    pub(super) fn get_table_schema_id(
1695        &self,
1696        type_ann: &shape_ast::ast::TypeAnnotation,
1697    ) -> Option<u16> {
1698        use shape_ast::ast::TypeAnnotation;
1699        if let TypeAnnotation::Generic { name, args } = type_ann {
1700            if name == "Table" && args.len() == 1 {
1701                let inner_name = match &args[0] {
1702                    TypeAnnotation::Reference(t) | TypeAnnotation::Basic(t) => Some(t.as_str()),
1703                    _ => None,
1704                };
1705                if let Some(type_name) = inner_name {
1706                    return self
1707                        .type_tracker
1708                        .schema_registry()
1709                        .get(type_name)
1710                        .map(|s| s.id as u16);
1711                }
1712            }
1713        }
1714        None
1715    }
1716
1717    // ===== Drop scope management =====
1718
1719    /// Push a new drop scope. Must be paired with pop_drop_scope().
1720    pub(super) fn push_drop_scope(&mut self) {
1721        self.drop_locals.push(Vec::new());
1722    }
1723
1724    /// Pop the current drop scope, emitting DropCall instructions for all
1725    /// tracked locals in reverse order.
1726    pub(super) fn pop_drop_scope(&mut self) -> Result<()> {
1727        // Emit DropCall for each tracked local in reverse order
1728        if let Some(locals) = self.drop_locals.pop() {
1729            for (local_idx, is_async) in locals.into_iter().rev() {
1730                self.emit_drop_call_for_local(local_idx, is_async);
1731            }
1732        }
1733        Ok(())
1734    }
1735
1736    /// Emit a single LoadLocal + DropCall pair for a local variable.
1737    /// The type name is resolved from the type tracker and encoded as a
1738    /// Property operand so the executor can look up `TypeName::drop`.
1739    fn emit_drop_call_for_local(&mut self, local_idx: u16, is_async: bool) {
1740        let type_name_opt = self
1741            .type_tracker
1742            .get_local_type(local_idx)
1743            .and_then(|info| info.type_name.clone());
1744        self.emit(Instruction::new(
1745            OpCode::LoadLocal,
1746            Some(Operand::Local(local_idx)),
1747        ));
1748        let opcode = if is_async {
1749            OpCode::DropCallAsync
1750        } else {
1751            OpCode::DropCall
1752        };
1753        if let Some(type_name) = type_name_opt {
1754            let str_idx = self.program.add_string(type_name);
1755            self.emit(Instruction::new(opcode, Some(Operand::Property(str_idx))));
1756        } else {
1757            self.emit(Instruction::simple(opcode));
1758        }
1759    }
1760
1761    /// Emit a single LoadModuleBinding + DropCall pair for a module binding.
1762    /// Similar to `emit_drop_call_for_local` but loads from module bindings.
1763    pub(super) fn emit_drop_call_for_module_binding(&mut self, binding_idx: u16, is_async: bool) {
1764        let type_name_opt = self
1765            .type_tracker
1766            .get_binding_type(binding_idx)
1767            .and_then(|info| info.type_name.clone());
1768        self.emit(Instruction::new(
1769            OpCode::LoadModuleBinding,
1770            Some(Operand::ModuleBinding(binding_idx)),
1771        ));
1772        let opcode = if is_async {
1773            OpCode::DropCallAsync
1774        } else {
1775            OpCode::DropCall
1776        };
1777        if let Some(type_name) = type_name_opt {
1778            let str_idx = self.program.add_string(type_name);
1779            self.emit(Instruction::new(opcode, Some(Operand::Property(str_idx))));
1780        } else {
1781            self.emit(Instruction::simple(opcode));
1782        }
1783    }
1784
1785    /// Track a local variable as needing Drop at scope exit.
1786    pub(super) fn track_drop_local(&mut self, local_idx: u16, is_async: bool) {
1787        if let Some(scope) = self.drop_locals.last_mut() {
1788            scope.push((local_idx, is_async));
1789        }
1790    }
1791
1792    /// Resolve the DropKind for a local variable's type.
1793    /// Returns None if the type is unknown or has no Drop impl.
1794    pub(super) fn local_drop_kind(&self, local_idx: u16) -> Option<DropKind> {
1795        let type_name = self
1796            .type_tracker
1797            .get_local_type(local_idx)
1798            .and_then(|info| info.type_name.as_ref())?;
1799        self.drop_type_info.get(type_name).copied()
1800    }
1801
1802    /// Resolve DropKind from a type annotation.
1803    pub(super) fn annotation_drop_kind(&self, type_ann: &TypeAnnotation) -> Option<DropKind> {
1804        let type_name = Self::tracked_type_name_from_annotation(type_ann)?;
1805        self.drop_type_info.get(&type_name).copied()
1806    }
1807
1808    /// Emit drops for all scopes being exited (used by return/break/continue).
1809    /// `scopes_to_exit` is the number of drop scopes to emit drops for.
1810    pub(super) fn emit_drops_for_early_exit(&mut self, scopes_to_exit: usize) -> Result<()> {
1811        let total = self.drop_locals.len();
1812        if scopes_to_exit > total {
1813            return Ok(());
1814        }
1815        // Collect locals from scopes being exited (innermost first)
1816        let mut scopes: Vec<Vec<(u16, bool)>> = Vec::new();
1817        for i in (total - scopes_to_exit..total).rev() {
1818            let locals = self.drop_locals.get(i).cloned().unwrap_or_default();
1819            scopes.push(locals);
1820        }
1821        // Now emit DropCall instructions
1822        for locals in scopes {
1823            for (local_idx, is_async) in locals.into_iter().rev() {
1824                self.emit_drop_call_for_local(local_idx, is_async);
1825            }
1826        }
1827        Ok(())
1828    }
1829
1830    /// Track a module binding as needing Drop at program exit.
1831    pub(super) fn track_drop_module_binding(&mut self, binding_idx: u16, is_async: bool) {
1832        self.drop_module_bindings.push((binding_idx, is_async));
1833    }
1834}
1835
1836#[cfg(test)]
1837mod tests {
1838    use super::super::BytecodeCompiler;
1839    use shape_ast::ast::TypeAnnotation;
1840    use shape_runtime::type_schema::FieldType;
1841
1842    #[test]
1843    fn test_type_annotation_to_field_type_array_recursive() {
1844        let ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Basic("int".to_string())));
1845        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
1846        assert_eq!(ft, FieldType::Array(Box::new(FieldType::I64)));
1847    }
1848
1849    #[test]
1850    fn test_type_annotation_to_field_type_optional() {
1851        let ann = TypeAnnotation::Generic {
1852            name: "Option".to_string(),
1853            args: vec![TypeAnnotation::Basic("int".to_string())],
1854        };
1855        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
1856        assert_eq!(ft, FieldType::Any);
1857    }
1858
1859    #[test]
1860    fn test_type_annotation_to_field_type_generic_hashmap() {
1861        let ann = TypeAnnotation::Generic {
1862            name: "HashMap".to_string(),
1863            args: vec![
1864                TypeAnnotation::Basic("string".to_string()),
1865                TypeAnnotation::Basic("int".to_string()),
1866            ],
1867        };
1868        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
1869        assert_eq!(ft, FieldType::Any);
1870    }
1871
1872    #[test]
1873    fn test_type_annotation_to_field_type_generic_user_struct() {
1874        let ann = TypeAnnotation::Generic {
1875            name: "MyContainer".to_string(),
1876            args: vec![TypeAnnotation::Basic("string".to_string())],
1877        };
1878        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
1879        assert_eq!(ft, FieldType::Object("MyContainer".to_string()));
1880    }
1881}