Skip to main content

shape_vm/
type_tracking.rs

1//! Type Tracking for Bytecode Compiler
2//!
3//! This module tracks known type information during compilation to enable
4//! type-specialized code generation. When a variable's type is known at
5//! compile time, the compiler can emit optimized opcodes for field access.
6//!
7//! # How Types Become Known
8//!
9//! Types are known in these situations:
10//! - Explicit type annotation: `let x: Candle = ...`
11//! - Constructor call: `let x = Candle { ... }`
12//! - Object literal: `let x = { a: 1, b: 2 }` (inline struct type)
13//! - Function with declared return type: `let x = get_candle()`
14//!
15//! # Usage
16//!
17//! The compiler uses this to emit typed field opcodes for dot access:
18//! - `GetFieldTyped` (specialized): Direct slot access by precomputed offset
19//! - `SetFieldTyped` (specialized): Direct slot update by precomputed offset
20//! Generic `GetProp`/`SetProp` are reserved for non-dot operations (index/slice).
21//!
22//! # Storage Type Hints
23//!
24//! For JIT optimization, we track storage types:
25//! - `StorageHint::NullableFloat64`: Option<f64> uses NaN sentinel
26//! - `StorageHint::Float64`: Plain f64, no nullability
27//!
28//! # Strict-typing contract: `prove_native_kind` and `ProofGap`
29//!
30//! Every typed-opcode emission site must call [`prove_native_kind`] and
31//! handle the [`ProofGap`] error. `ProofGap`'s constructor is private to
32//! this module: emit code cannot fabricate "I proved it". The Rust type
33//! system enforces the discipline.
34//!
35//! In Phases 2-4, the predicate panics in debug + test, returns a
36//! compile error in release. There is no fallback path.
37
38use std::collections::HashMap;
39
40use serde::{Deserialize, Serialize};
41use shape_ast::ast::TypeAnnotation;
42use shape_runtime::type_schema::{FieldType, SchemaId, TypeSchema, TypeSchemaRegistry};
43use shape_runtime::type_system::{BuiltinTypes, StorageType};
44use shape_value::v2::struct_layout::{FieldKind, StructLayout};
45
46/// Numeric type known at compile time for typed opcode emission.
47///
48/// When the compiler can determine the numeric subtype of an expression,
49/// it emits typed opcodes (e.g., `MulInt` instead of `Mul`) that skip
50/// runtime type dispatch entirely.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum NumericType {
53    /// Integer (i64) — the default integer type
54    Int,
55    /// Width-specific integer (i8, u8, i16, u16, i32, u32, u64)
56    IntWidth(shape_ast::IntWidth),
57    /// Floating point (f64)
58    Number,
59    /// Exact decimal (rust_decimal::Decimal)
60    Decimal,
61}
62
63
64// `NativeKind` (formerly `SlotKind`) is the single discriminator used at
65// every typed-ABI boundary. Moved to `shape-value::native_kind` so that
66// shape-runtime's marshal layer and shape-vm's compile-time proof share one
67// type. See `docs/defections.md` 2026-05-06 (Phase 2b unified marshal).
68pub use shape_value::NativeKind;
69
70/// Backwards-compatible alias. Prefer `NativeKind` in new code.
71pub type StorageHint = NativeKind;
72
73/// Convert a runtime `StorageType` to its marshal-layer `NativeKind`.
74///
75/// Free function because `StorageType` lives in shape-runtime and
76/// `NativeKind` lives in shape-value — Rust's orphan rule forbids an
77/// inherent `impl NativeKind { fn from_storage_type … }` here, and the
78/// previous `From<StorageType> for NativeKind` impl violated E0117
79/// (neither type is local to shape-vm). Callers explicitly invoke this
80/// fallible function and pattern-match `None` to surface "complex
81/// storage — kind not proven here" per CLAUDE.md type-system rules.
82///
83/// Returns `None` for complex `StorageType` variants
84/// (Array/Table/Object/Result/TaggedUnion/Function/Struct/Dynamic):
85/// the bulldozer deleted the `Unknown` sink, so callers must commit
86/// to a concrete kind. A `None` return is a "you must prove a typed
87/// kind here" signal — compiler-tier intermediate state per ADR-006
88/// §2.7.5.1.
89pub fn native_kind_from_storage_type(st: &StorageType) -> Option<NativeKind> {
90    match st {
91        StorageType::Float64 => Some(NativeKind::Float64),
92        StorageType::Int64 => Some(NativeKind::Int64),
93        StorageType::Bool => Some(NativeKind::Bool),
94        StorageType::String => Some(NativeKind::String),
95
96        StorageType::NullableFloat64 => Some(NativeKind::NullableFloat64),
97        StorageType::NullableInt64 => Some(NativeKind::NullableInt64),
98        StorageType::NullableBool => Some(NativeKind::Bool), // 3-state in Boxed
99
100        StorageType::Array(_)
101        | StorageType::Table { .. }
102        | StorageType::Object
103        | StorageType::Result { .. }
104        | StorageType::TaggedUnion { .. }
105        | StorageType::Function
106        | StorageType::Struct(_)
107        | StorageType::Dynamic => None,
108    }
109}
110
111/// Typed frame layout metadata.
112///
113/// A `FrameDescriptor` describes the storage layout for every local slot
114/// (parameters + locals) in a single function or top-level frame.  The JIT
115/// and VM use this to allocate registers / stack space with correct widths
116/// and to skip NaN-boxing for slots whose type is statically known.
117///
118/// This is the canonical replacement for the loose `Vec<StorageHint>` arrays
119/// that were previously threaded through `BytecodeProgram` and `Function`.
120///
121/// # Wire-format binding (ADR-006 §2.7.5.1)
122///
123/// `FrameDescriptor` is `#[derive(Serialize, Deserialize)]` and lives
124/// inside `FunctionBlob`, the content-hash unit for distributed
125/// bytecode. Per §2.7.5.1, `slots: Vec<NativeKind>` is post-proof — no
126/// `Vec<Option<NativeKind>>` wrap, no `NativeKind::Unknown` placeholder
127/// (both deleted by the bulldozer). By the time a `FrameDescriptor`
128/// reaches `FunctionBlob` construction, every slot's `NativeKind` is
129/// proven; an unproven slot is a compile error per CLAUDE.md type-system
130/// rules.
131///
132/// `return_kind: Option<NativeKind>` is the single-slot field carrying
133/// the function's return-value kind, or `None` for "no return value /
134/// kind not yet stamped". The single-slot `Option<NativeKind>` shape is
135/// the §2.7.8 / Q10 cell-storage pattern (single-slot fields take
136/// `Option<NativeKind>`); §2.7.5.1's "no `Option<NativeKind>` wrapping"
137/// rule targets the bulk `slots` Vec, not single-slot fields where
138/// `None` distinguishes "no return value" from "return value of kind X".
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct FrameDescriptor {
141    /// One entry per local slot (index 0 = first param or local).
142    /// Every slot's kind is proven at `FunctionBlob` construction time
143    /// per ADR-006 §2.7.5.1 — `NativeKind::Unknown` was deleted from the
144    /// enum and is never re-introduced under any name.
145    pub slots: Vec<NativeKind>,
146
147    /// Return type kind for the function, or `None` for "no return
148    /// value / kind not stamped". `Some(kind)` means the JIT boundary
149    /// ABI uses `kind` to unmarshal the return value from JIT-compiled
150    /// code; `None` means the host boundary has no return-kind to
151    /// unmarshal (e.g. top-level program with no terminal expression).
152    #[serde(default)]
153    pub return_kind: Option<NativeKind>,
154}
155
156impl FrameDescriptor {
157    /// Create an empty descriptor with no slots and an unset return
158    /// kind. Callers stamp `return_kind` to `Some(kind)` once the
159    /// function's return type is proven.
160    pub fn new() -> Self {
161        Self {
162            slots: Vec::new(),
163            return_kind: None,
164        }
165    }
166
167    /// Build a descriptor from an existing `Vec<NativeKind>` of proven
168    /// slot kinds. `return_kind` is left `None` for the caller to stamp.
169    pub fn from_slots(slots: Vec<NativeKind>) -> Self {
170        Self {
171            slots,
172            return_kind: None,
173        }
174    }
175
176    /// Number of slots described.
177    #[inline]
178    pub fn len(&self) -> usize {
179        self.slots.len()
180    }
181
182    /// Whether the descriptor is empty.
183    #[inline]
184    pub fn is_empty(&self) -> bool {
185        self.slots.is_empty()
186    }
187
188    /// Get the kind of a specific slot, or `None` for out-of-range
189    /// indices. Note: a present slot always has a proven `NativeKind`
190    /// (no `Unknown` sentinel post-bulldozer).
191    #[inline]
192    pub fn slot(&self, index: usize) -> Option<NativeKind> {
193        self.slots.get(index).copied()
194    }
195}
196
197impl Default for FrameDescriptor {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203/// The kind of variable: regular value, typed table, row view, or column.
204///
205/// Replaces the old `is_datatable` / `is_row_view` / `is_column` boolean flags,
206/// which were mutually exclusive but had no compiler enforcement.
207#[derive(Debug, Clone, PartialEq)]
208pub enum VariableKind {
209    /// Regular value (struct, primitive, object, etc.)
210    Value,
211    /// A DataTable with known row schema — Table<T>.
212    /// Closure methods (filter/map/etc.) propagate schema to row params.
213    Table { element_type: String },
214    /// A typed row from an Arrow DataTable — Row<T>.
215    /// Field access emits LoadColF64/I64/Bool/Str instead of GetProp.
216    RowView { element_type: String },
217    /// A typed column from an Arrow DataTable — Column<T>.
218    Column {
219        element_type: String,
220        column_type: String,
221    },
222    /// An indexed table — Indexed<T> with a designated index column.
223    /// Only Indexed tables can use resample/between operations.
224    Indexed {
225        element_type: String,
226        index_column: String,
227    },
228}
229
230/// Source-level ownership class for a binding slot.
231///
232/// This tracks how the binding was declared, independent of the value's type.
233/// Later storage planning uses this to decide whether a slot can stay direct,
234/// must allow aliasing, or should preserve reference representation.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
236pub enum BindingOwnershipClass {
237    /// `let` — immutable owned binding.
238    OwnedImmutable,
239    /// `let mut` — mutable owned binding.
240    OwnedMutable,
241    /// `var` — flexible/aliasable binding whose storage is chosen later.
242    Flexible,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246pub enum Aliasability {
247    /// Single owner, no aliasing possible.
248    Unique,
249    /// Shared via immutable references only.
250    SharedImmutable,
251    /// Shared with potential mutation (var semantics).
252    SharedMutable,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256pub enum MutationCapability {
257    /// Cannot be mutated (`let`).
258    Immutable,
259    /// Mutable by single owner (`let mut`).
260    LocalMutable,
261    /// Mutable with shared access (`var` captured/aliased).
262    SharedMutable,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
266pub enum EscapeStatus {
267    /// Stays within declaring scope.
268    Local,
269    /// Captured by a closure.
270    Captured,
271    /// Escapes declaring function (returned, stored in module state).
272    Escaped,
273}
274
275/// Planned runtime storage strategy for a binding slot.
276///
277/// `Deferred` is the initial state for ordinary bindings until a later planner
278/// decides whether the slot can stay direct or must be upgraded.
279///
280/// `LocalMutablePtr` (Closure Spec Phase D) marks a slot that lives on the
281/// stack AND has had a typed `*mut T` handed to a non-escaping closure env.
282/// The borrow checker has verified that no outer code races the closure over
283/// that slot's lifetime (the `ClosureCapture` was lowered as an exclusive
284/// borrow with `LoanSinkKind::ClosureEnvMut`). The binding is still direct in
285/// the sense that the slot stays on the caller stack — no `Arc<RwLock<>>`,
286/// no boxing. This is orthogonal to `Direct`/`UniqueHeap`: `Direct` means
287/// "no indirection and no closure-env sharing", `LocalMutablePtr` means "no
288/// indirection but a closure env holds a typed pointer into this slot".
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
290pub enum BindingStorageClass {
291    Deferred,
292    Direct,
293    UniqueHeap,
294    SharedCow,
295    Reference,
296    /// Phase D: stack-resident slot with a typed `*mut T` capture handed to a
297    /// non-escaping closure. See the doc comment on `BindingStorageClass`.
298    LocalMutablePtr,
299}
300
301/// Ownership/storage metadata for a binding slot.
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
303pub struct BindingSemantics {
304    pub ownership_class: BindingOwnershipClass,
305    pub storage_class: BindingStorageClass,
306    pub aliasability: Aliasability,
307    pub mutation_capability: MutationCapability,
308    pub escape_status: EscapeStatus,
309    /// Phase 5.B: If this binding was initialized from a call to a function
310    /// with a known return-ownership mode, record that mode here. Phase 5.C
311    /// consumes the hint to skip the Arc→Box `PromoteToOwned` at the callsite
312    /// when the callee has already returned a uniquely-owned value.
313    pub return_ownership_hint: Option<crate::mir::ReturnOwnershipMode>,
314}
315
316impl BindingSemantics {
317    pub const fn deferred(ownership_class: BindingOwnershipClass) -> Self {
318        Self {
319            ownership_class,
320            storage_class: BindingStorageClass::Deferred,
321            aliasability: Aliasability::Unique,
322            mutation_capability: match ownership_class {
323                BindingOwnershipClass::OwnedImmutable => MutationCapability::Immutable,
324                BindingOwnershipClass::OwnedMutable => MutationCapability::LocalMutable,
325                BindingOwnershipClass::Flexible => MutationCapability::SharedMutable,
326            },
327            escape_status: EscapeStatus::Local,
328            return_ownership_hint: None,
329        }
330    }
331}
332
333/// Type information for a variable
334#[derive(Debug, Clone)]
335pub struct VariableTypeInfo {
336    /// Schema ID if type is known and registered
337    pub schema_id: Option<SchemaId>,
338    /// Type name (e.g., "Candle", "Point")
339    pub type_name: Option<String>,
340    /// Whether the type is definitely known (vs inferred/uncertain)
341    pub is_definite: bool,
342    /// Storage hint for JIT optimization, or `None` when the kind is
343    /// not yet proven during compile-time inference.
344    ///
345    /// Per ADR-006 §2.7.5.1, this is compiler-tier intermediate state —
346    /// `VariableTypeInfo` is NOT serialized into `FunctionBlob`, so the
347    /// `Option<NativeKind>` wrap stays local to the analysis pass and
348    /// does not reach the wire format. By the time a `FrameDescriptor`
349    /// is constructed for `FunctionBlob`, the kind is proven and stored
350    /// flat in `FrameDescriptor.slots: Vec<NativeKind>` (no Option).
351    pub storage_hint: Option<StorageHint>,
352    /// Preserved concrete numeric runtime type (e.g. "i16", "u8", "f32", "i64")
353    /// derived from source annotations.
354    pub concrete_numeric_type: Option<String>,
355    /// What kind of variable this is (value, table, row view, column)
356    pub kind: VariableKind,
357    /// v2: For typed arrays, the element's FieldKind (enables typed array codegen)
358    pub v2_array_element_kind: Option<FieldKind>,
359    /// v2: For typed structs, the SchemaId referencing a StructLayout in TypeTracker
360    pub v2_struct_layout: Option<SchemaId>,
361}
362
363impl VariableTypeInfo {
364    /// Create type info for a known type
365    pub fn known(schema_id: SchemaId, type_name: String) -> Self {
366        let concrete_numeric_type = Self::infer_numeric_runtime_name(&type_name);
367        Self {
368            schema_id: Some(schema_id),
369            type_name: Some(type_name),
370            is_definite: true,
371            storage_hint: None,
372            concrete_numeric_type,
373            kind: VariableKind::Value,
374            v2_array_element_kind: None,
375            v2_struct_layout: None,
376        }
377    }
378
379    /// Create type info for an unknown/dynamic type
380    pub fn unknown() -> Self {
381        Self {
382            schema_id: None,
383            type_name: None,
384            is_definite: false,
385            storage_hint: None,
386            concrete_numeric_type: None,
387            kind: VariableKind::Value,
388            v2_array_element_kind: None,
389            v2_struct_layout: None,
390        }
391    }
392
393    /// Create type info for a type name that may or may not be registered
394    pub fn named(type_name: String) -> Self {
395        // Infer storage hint from common type names
396        let storage_hint = Self::infer_storage_hint(&type_name);
397        let concrete_numeric_type = Self::infer_numeric_runtime_name(&type_name);
398        Self {
399            schema_id: None,
400            type_name: Some(type_name),
401            is_definite: false,
402            storage_hint,
403            concrete_numeric_type,
404            kind: VariableKind::Value,
405            v2_array_element_kind: None,
406            v2_struct_layout: None,
407        }
408    }
409
410    /// Create type info with explicit storage hint
411    pub fn with_storage(type_name: String, storage_hint: StorageHint) -> Self {
412        let concrete_numeric_type = Self::infer_numeric_runtime_name(&type_name);
413        Self {
414            schema_id: None,
415            type_name: Some(type_name),
416            is_definite: true,
417            storage_hint: Some(storage_hint),
418            concrete_numeric_type,
419            kind: VariableKind::Value,
420            v2_array_element_kind: None,
421            v2_struct_layout: None,
422        }
423    }
424
425    /// Create type info for Option<f64> (NaN sentinel optimization)
426    pub fn nullable_number() -> Self {
427        Self {
428            schema_id: None,
429            type_name: Some("Option<Number>".to_string()),
430            is_definite: true,
431            storage_hint: Some(StorageHint::NullableFloat64),
432            concrete_numeric_type: Some("f64".to_string()),
433            kind: VariableKind::Value,
434            v2_array_element_kind: None,
435            v2_struct_layout: None,
436        }
437    }
438
439    /// Create type info for plain f64
440    pub fn number() -> Self {
441        Self {
442            schema_id: None,
443            type_name: Some("Number".to_string()),
444            is_definite: true,
445            storage_hint: Some(StorageHint::Float64),
446            concrete_numeric_type: Some("f64".to_string()),
447            kind: VariableKind::Value,
448            v2_array_element_kind: None,
449            v2_struct_layout: None,
450        }
451    }
452
453    /// Create type info for a RowView variable (typed row from Arrow DataTable).
454    pub fn row_view(schema_id: SchemaId, type_name: String) -> Self {
455        Self {
456            schema_id: Some(schema_id),
457            type_name: Some(type_name.clone()),
458            is_definite: true,
459            storage_hint: None,
460            concrete_numeric_type: None,
461            kind: VariableKind::RowView {
462                element_type: type_name,
463            },
464            v2_array_element_kind: None,
465            v2_struct_layout: None,
466        }
467    }
468
469    /// Create type info for a DataTable variable with known schema (Table<T>).
470    pub fn datatable(schema_id: SchemaId, type_name: String) -> Self {
471        Self {
472            schema_id: Some(schema_id),
473            type_name: Some(type_name.clone()),
474            is_definite: true,
475            storage_hint: None,
476            concrete_numeric_type: None,
477            kind: VariableKind::Table {
478                element_type: type_name,
479            },
480            v2_array_element_kind: None,
481            v2_struct_layout: None,
482        }
483    }
484
485    /// Create type info for a Column<T> variable (ColumnRef from Arrow DataTable).
486    pub fn column(schema_id: SchemaId, type_name: String, element_type: String) -> Self {
487        Self {
488            schema_id: Some(schema_id),
489            type_name: Some(type_name.clone()),
490            is_definite: true,
491            storage_hint: None,
492            concrete_numeric_type: None,
493            kind: VariableKind::Column {
494                element_type,
495                column_type: type_name,
496            },
497            v2_array_element_kind: None,
498            v2_struct_layout: None,
499        }
500    }
501
502    /// Create type info for an Indexed table variable — Indexed<T> with known index column.
503    pub fn indexed(schema_id: SchemaId, type_name: String, index_column: String) -> Self {
504        Self {
505            schema_id: Some(schema_id),
506            type_name: Some(type_name.clone()),
507            is_definite: true,
508            storage_hint: None,
509            concrete_numeric_type: None,
510            kind: VariableKind::Indexed {
511                element_type: type_name,
512                index_column,
513            },
514            v2_array_element_kind: None,
515            v2_struct_layout: None,
516        }
517    }
518
519    /// Check if this type is known (has schema ID)
520    pub fn is_known(&self) -> bool {
521        self.schema_id.is_some()
522    }
523
524    /// Check if this type uses NaN sentinel for nullability
525    pub fn uses_nan_sentinel(&self) -> bool {
526        self.storage_hint == Some(StorageHint::NullableFloat64)
527    }
528
529    /// Check if this variable is a DataTable (Table<T>)
530    pub fn is_datatable(&self) -> bool {
531        matches!(self.kind, VariableKind::Table { .. })
532    }
533
534    /// Check if this variable is a RowView (Row<T>)
535    pub fn is_row_view(&self) -> bool {
536        matches!(self.kind, VariableKind::RowView { .. })
537    }
538
539    /// Check if this variable is a Column (Column<T>)
540    pub fn is_column(&self) -> bool {
541        matches!(self.kind, VariableKind::Column { .. })
542    }
543
544    /// Check if this variable is an Indexed table (Indexed<T>)
545    pub fn is_indexed(&self) -> bool {
546        matches!(self.kind, VariableKind::Indexed { .. })
547    }
548
549    /// Infer storage hint from type name. Returns `None` when the
550    /// type name cannot be mapped to a concrete `NativeKind` —
551    /// per ADR-006 §2.7.5.1 / CLAUDE.md, this is the analysis-tier
552    /// "kind not yet proven" intermediate state, never an `Unknown`
553    /// sentinel.
554    fn infer_storage_hint(type_name: &str) -> Option<StorageHint> {
555        let trimmed = type_name.trim();
556
557        if let Some(inner) = Self::option_inner_type(trimmed) {
558            let inner = inner.trim();
559            if let Some(runtime) = BuiltinTypes::canonical_numeric_runtime_name(inner)
560                && let Some(hint) = Self::storage_hint_for_runtime_numeric(runtime, true)
561            {
562                return Some(hint);
563            }
564            if BuiltinTypes::is_bool_type_name(inner) {
565                return Some(StorageHint::Bool);
566            }
567            if BuiltinTypes::is_string_type_name(inner) {
568                return Some(StorageHint::String);
569            }
570            return None;
571        }
572
573        if let Some(runtime) = BuiltinTypes::canonical_numeric_runtime_name(trimmed)
574            && let Some(hint) = Self::storage_hint_for_runtime_numeric(runtime, false)
575        {
576            return Some(hint);
577        }
578        if BuiltinTypes::is_bool_type_name(trimmed) {
579            return Some(StorageHint::Bool);
580        }
581        if BuiltinTypes::is_string_type_name(trimmed) {
582            return Some(StorageHint::String);
583        }
584        None
585    }
586
587    fn option_inner_type(type_name: &str) -> Option<&str> {
588        type_name
589            .strip_prefix("Option<")
590            .and_then(|inner| inner.strip_suffix('>'))
591    }
592
593    fn storage_hint_for_runtime_numeric(runtime_name: &str, nullable: bool) -> Option<StorageHint> {
594        let base = match runtime_name {
595            "f32" | "f64" => StorageHint::Float64,
596            "i8" => StorageHint::Int8,
597            "u8" => StorageHint::UInt8,
598            "i16" => StorageHint::Int16,
599            "u16" => StorageHint::UInt16,
600            "i32" => StorageHint::Int32,
601            "u32" => StorageHint::UInt32,
602            "i64" => StorageHint::Int64,
603            "u64" => StorageHint::UInt64,
604            "isize" => StorageHint::IntSize,
605            "usize" => StorageHint::UIntSize,
606            _ => return None,
607        };
608        Some(base.with_nullability(nullable))
609    }
610
611    fn infer_numeric_runtime_name(type_name: &str) -> Option<String> {
612        let inner = if type_name.starts_with("Option<") && type_name.ends_with('>') {
613            &type_name["Option<".len()..type_name.len() - 1]
614        } else {
615            type_name
616        };
617        BuiltinTypes::canonical_numeric_runtime_name(inner).map(ToString::to_string)
618    }
619}
620
621/// Sweep phase 3c.1: snapshot of the type-tracker's local-types state
622/// captured before a nested function compilation that clears them.
623/// Wraps both the flat `local_types` map and the per-scope
624/// `local_type_scopes` stack so `pop_scope` semantics survive the
625/// round-trip.
626#[derive(Debug, Clone)]
627pub struct LocalTypesSnapshot {
628    pub local_types: HashMap<u16, VariableTypeInfo>,
629    pub local_type_scopes: Vec<HashMap<u16, VariableTypeInfo>>,
630}
631
632impl LocalTypesSnapshot {
633    pub fn len(&self) -> usize {
634        self.local_types.len()
635    }
636
637    pub fn is_empty(&self) -> bool {
638        self.local_types.is_empty()
639    }
640}
641
642/// Tracks type information for variables during compilation
643#[derive(Debug)]
644pub struct TypeTracker {
645    /// Type schema registry for looking up type definitions
646    schema_registry: TypeSchemaRegistry,
647
648    /// Type info for local variables (by slot index)
649    local_types: HashMap<u16, VariableTypeInfo>,
650
651    /// Type info for module_binding variables (by slot index)
652    binding_types: HashMap<u16, VariableTypeInfo>,
653
654    /// Binding ownership/storage metadata for locals.
655    local_binding_semantics: HashMap<u16, BindingSemantics>,
656
657    /// Binding ownership/storage metadata for module bindings.
658    binding_semantics: HashMap<u16, BindingSemantics>,
659
660    /// Scoped local type mappings (for scope push/pop)
661    local_type_scopes: Vec<HashMap<u16, VariableTypeInfo>>,
662
663    /// Scoped local binding metadata mappings (for scope push/pop).
664    local_binding_semantic_scopes: Vec<HashMap<u16, BindingSemantics>>,
665
666    /// Function return types (function name -> type name)
667    function_return_types: HashMap<String, String>,
668    /// Compile-time object schema contracts: schema id -> field type annotation.
669    ///
670    /// Used for callable typed-object fields where runtime schema stores only slot layout.
671    object_field_contracts: HashMap<SchemaId, HashMap<String, TypeAnnotation>>,
672
673    /// v2: Computed C-compatible struct layouts indexed by SchemaId.
674    /// Enables the compiler to look up field offsets at compile time for typed codegen.
675    pub v2_layouts: HashMap<SchemaId, StructLayout>,
676
677    /// Per-compiler counter for generating unique names for inline object
678    /// schemas (`__inline_obj_{N}`). Moved off the process-global
679    /// `INLINE_OBJECT_COUNTER` static in B1.8; scoping it to a single
680    /// compile makes the generated names deterministic per compile.
681    inline_object_counter: u64,
682}
683
684impl TypeTracker {
685    /// Create a new type tracker with the given schema registry
686    pub fn new(schema_registry: TypeSchemaRegistry) -> Self {
687        Self {
688            schema_registry,
689            local_types: HashMap::new(),
690            binding_types: HashMap::new(),
691            local_binding_semantics: HashMap::new(),
692            binding_semantics: HashMap::new(),
693            local_type_scopes: vec![HashMap::new()],
694            local_binding_semantic_scopes: vec![HashMap::new()],
695            function_return_types: HashMap::new(),
696            object_field_contracts: HashMap::new(),
697            v2_layouts: HashMap::new(),
698            inline_object_counter: 0,
699        }
700    }
701
702    /// Create a new type tracker with an empty registry
703    pub fn empty() -> Self {
704        Self::new(TypeSchemaRegistry::new())
705    }
706
707    /// Create a new type tracker with stdlib types pre-registered
708    pub fn with_stdlib() -> Self {
709        Self::new(TypeSchemaRegistry::with_stdlib_types())
710    }
711
712    /// Get the schema registry
713    pub fn schema_registry(&self) -> &TypeSchemaRegistry {
714        &self.schema_registry
715    }
716
717    /// Get mutable schema registry
718    pub fn schema_registry_mut(&mut self) -> &mut TypeSchemaRegistry {
719        &mut self.schema_registry
720    }
721
722    /// Push a new scope for local types
723    pub fn push_scope(&mut self) {
724        self.local_type_scopes.push(HashMap::new());
725        self.local_binding_semantic_scopes.push(HashMap::new());
726    }
727
728    /// Pop a scope, removing local type info for that scope
729    pub fn pop_scope(&mut self) {
730        if let Some(scope) = self.local_type_scopes.pop() {
731            // Remove type info for variables in this scope
732            for slot in scope.keys() {
733                self.local_types.remove(slot);
734            }
735        }
736        if let Some(scope) = self.local_binding_semantic_scopes.pop() {
737            for slot in scope.keys() {
738                self.local_binding_semantics.remove(slot);
739            }
740        }
741    }
742
743    /// Set type info for a local variable
744    pub fn set_local_type(&mut self, slot: u16, type_info: VariableTypeInfo) {
745        // Try to resolve schema ID if we have a type name but no schema ID
746        let resolved_info = if type_info.type_name.is_some() && type_info.schema_id.is_none() {
747            self.resolve_type_info(type_info)
748        } else {
749            type_info
750        };
751
752        // Track in current scope
753        if let Some(scope) = self.local_type_scopes.last_mut() {
754            scope.insert(slot, resolved_info.clone());
755        }
756        self.local_types.insert(slot, resolved_info);
757    }
758
759    /// Set type info for a module_binding variable
760    pub fn set_binding_type(&mut self, slot: u16, type_info: VariableTypeInfo) {
761        let resolved_info = if type_info.type_name.is_some() && type_info.schema_id.is_none() {
762            self.resolve_type_info(type_info)
763        } else {
764            type_info
765        };
766        self.binding_types.insert(slot, resolved_info);
767    }
768
769    /// Set ownership/storage metadata for a local binding.
770    pub fn set_local_binding_semantics(&mut self, slot: u16, semantics: BindingSemantics) {
771        if let Some(scope) = self.local_binding_semantic_scopes.last_mut() {
772            scope.insert(slot, semantics);
773        }
774        self.local_binding_semantics.insert(slot, semantics);
775    }
776
777    /// Set ownership/storage metadata for a module binding.
778    pub fn set_binding_semantics(&mut self, slot: u16, semantics: BindingSemantics) {
779        self.binding_semantics.insert(slot, semantics);
780    }
781
782    /// Update only the storage strategy for a local binding.
783    pub fn set_local_binding_storage_class(
784        &mut self,
785        slot: u16,
786        storage_class: BindingStorageClass,
787    ) {
788        if let Some(existing) = self.local_binding_semantics.get_mut(&slot) {
789            existing.storage_class = storage_class;
790        }
791        for scope in self.local_binding_semantic_scopes.iter_mut().rev() {
792            if let Some(existing) = scope.get_mut(&slot) {
793                existing.storage_class = storage_class;
794                break;
795            }
796        }
797    }
798
799    /// Update only the storage strategy for a module binding.
800    pub fn set_binding_storage_class(&mut self, slot: u16, storage_class: BindingStorageClass) {
801        if let Some(existing) = self.binding_semantics.get_mut(&slot) {
802            existing.storage_class = storage_class;
803        }
804    }
805
806    /// Get type info for a local variable
807    pub fn get_local_type(&self, slot: u16) -> Option<&VariableTypeInfo> {
808        self.local_types.get(&slot)
809    }
810
811    /// Get type info for a module_binding variable
812    pub fn get_binding_type(&self, slot: u16) -> Option<&VariableTypeInfo> {
813        self.binding_types.get(&slot)
814    }
815
816    /// Get ownership/storage metadata for a local binding.
817    pub fn get_local_binding_semantics(&self, slot: u16) -> Option<&BindingSemantics> {
818        self.local_binding_semantics.get(&slot)
819    }
820
821    /// Get ownership/storage metadata for a module binding.
822    pub fn get_binding_semantics(&self, slot: u16) -> Option<&BindingSemantics> {
823        self.binding_semantics.get(&slot)
824    }
825
826    /// Register a function's return type
827    pub fn register_function_return_type(&mut self, func_name: &str, return_type: &str) {
828        self.function_return_types
829            .insert(func_name.to_string(), return_type.to_string());
830    }
831
832    /// Get a function's return type
833    pub fn get_function_return_type(&self, func_name: &str) -> Option<&String> {
834        self.function_return_types.get(func_name)
835    }
836
837    /// Register compile-time field type contracts for an object schema id.
838    pub fn register_object_field_contracts(
839        &mut self,
840        schema_id: SchemaId,
841        fields: HashMap<String, TypeAnnotation>,
842    ) {
843        self.object_field_contracts.insert(schema_id, fields);
844    }
845
846    /// Lookup a compile-time field type contract for a schema field.
847    pub fn get_object_field_contract(
848        &self,
849        schema_id: SchemaId,
850        field_name: &str,
851    ) -> Option<&TypeAnnotation> {
852        self.object_field_contracts
853            .get(&schema_id)
854            .and_then(|fields| fields.get(field_name))
855    }
856
857    /// Resolve type name to schema ID
858    fn resolve_type_info(&self, mut type_info: VariableTypeInfo) -> VariableTypeInfo {
859        if let Some(ref type_name) = type_info.type_name {
860            if let Some(schema) = self.schema_registry.get(type_name) {
861                type_info.schema_id = Some(schema.id);
862                type_info.is_definite = true;
863            }
864        }
865        type_info
866    }
867
868    /// Get field offset for typed field access
869    ///
870    /// Returns (schema_id, field_offset, field_index) if type and field are known
871    pub fn get_typed_field_info(
872        &self,
873        type_name: &str,
874        field_name: &str,
875    ) -> Option<(SchemaId, usize, u16)> {
876        let schema = self.schema_registry.get(type_name)?;
877        let field = schema.get_field(field_name)?;
878        Some((schema.id, field.offset, field.index))
879    }
880
881    /// Get column index for a RowView field access.
882    ///
883    /// Returns the field index (used as col_id for ColumnAccess operand)
884    /// if the variable is a RowView and the field exists in its schema.
885    pub fn get_row_view_column_id(
886        &self,
887        slot: u16,
888        is_local: bool,
889        field_name: &str,
890    ) -> Option<u32> {
891        let type_info = if is_local {
892            self.get_local_type(slot)?
893        } else {
894            self.get_binding_type(slot)?
895        };
896        if !type_info.is_row_view() {
897            return None;
898        }
899        let type_name = type_info.type_name.as_ref()?;
900        let schema = self.schema_registry.get(type_name)?;
901        let field = schema.get_field(field_name)?;
902        Some(field.index as u32)
903    }
904
905    /// Check if we can use typed field access for a variable and field
906    pub fn can_use_typed_access(&self, slot: u16, is_local: bool, field_name: &str) -> bool {
907        let type_info = if is_local {
908            self.get_local_type(slot)
909        } else {
910            self.get_binding_type(slot)
911        };
912
913        if let Some(info) = type_info {
914            if let Some(ref type_name) = info.type_name {
915                return self
916                    .schema_registry
917                    .field_offset(type_name, field_name)
918                    .is_some();
919            }
920        }
921        false
922    }
923
924    /// Get storage hint for a local variable. Returns `None` for
925    /// unknown slots or when the slot's kind has not yet been proven —
926    /// per ADR-006 §2.7.5.1, callers handle the "not proven" arm
927    /// explicitly rather than dispatching on a placeholder sentinel.
928    pub fn get_local_storage_hint(&self, slot: u16) -> Option<StorageHint> {
929        self.get_local_type(slot).and_then(|info| info.storage_hint)
930    }
931
932    /// Get storage hint for a module_binding variable. Returns `None`
933    /// for unknown slots or unproven kinds — see
934    /// [`get_local_storage_hint`].
935    pub fn get_module_binding_storage_hint(&self, slot: u16) -> Option<StorageHint> {
936        self.get_binding_type(slot)
937            .and_then(|info| info.storage_hint)
938    }
939
940    /// Check if a local variable uses NaN sentinel for nullability
941    pub fn local_uses_nan_sentinel(&self, slot: u16) -> bool {
942        self.get_local_storage_hint(slot) == Some(StorageHint::NullableFloat64)
943    }
944
945    /// Check if a module_binding variable uses NaN sentinel for nullability
946    pub fn module_binding_uses_nan_sentinel(&self, slot: u16) -> bool {
947        self.get_module_binding_storage_hint(slot) == Some(StorageHint::NullableFloat64)
948    }
949
950    /// Clear all local type info (for function entry)
951    pub fn clear_locals(&mut self) {
952        self.local_types.clear();
953        self.local_binding_semantics.clear();
954        self.local_type_scopes.clear();
955        self.local_type_scopes.push(HashMap::new());
956        self.local_binding_semantic_scopes.clear();
957        self.local_binding_semantic_scopes.push(HashMap::new());
958    }
959
960    /// Track A.1C.2: snapshot the outer function's local binding
961    /// semantics before entering a nested function compilation. Pairs
962    /// with [`restore_local_binding_semantics`] to preserve ownership
963    /// / storage classification across `compile_function`'s
964    /// `clear_locals` call so subsequent closures in the outer scope
965    /// still observe the binding's classification (e.g. `var` →
966    /// `Flexible`, `let mut` → `OwnedMutable`).
967    pub fn snapshot_local_binding_semantics(&self) -> HashMap<u16, BindingSemantics> {
968        self.local_binding_semantics.clone()
969    }
970
971    /// Track A.1C.2: restore a previously-snapshotted local binding
972    /// semantics map. Used by `compile_function` after a nested closure
973    /// body compilation wipes the outer function's semantics.
974    pub fn restore_local_binding_semantics(&mut self, snapshot: HashMap<u16, BindingSemantics>) {
975        self.local_binding_semantics = snapshot;
976    }
977
978    /// Sweep phase 3c.1: snapshot the outer function's local type
979    /// information before entering a nested function compilation.
980    /// Pairs with [`restore_local_types`] to preserve typed-tracker
981    /// state (e.g. `let base = 100` → `int`) across
982    /// `compile_function`'s `clear_locals` call so post-closure
983    /// strict-typing-sweep code paths can still resolve outer-scope
984    /// identifier types — e.g. inferring `f`'s return type for
985    /// `let f = |x: int| x + base`.
986    ///
987    /// The snapshot covers both the flattened `local_types` map AND
988    /// the per-scope `local_type_scopes` stack — `pop_scope` (called
989    /// during closure compile teardown) reads from the scope stack to
990    /// know which `local_types` entries to evict, so a restore that
991    /// only re-installs `local_types` would have its outer-scope
992    /// entries silently re-evicted by the next `pop_scope` whose
993    /// scope stack still contains the inner-closure slot indices.
994    pub fn snapshot_local_types(&self) -> LocalTypesSnapshot {
995        LocalTypesSnapshot {
996            local_types: self.local_types.clone(),
997            local_type_scopes: self.local_type_scopes.clone(),
998        }
999    }
1000
1001    /// Sweep phase 3c.1: restore a previously-snapshotted local types
1002    /// map and scope stack. Used by `compile_function` after a nested
1003    /// closure body compilation wipes the outer function's type
1004    /// information.
1005    pub fn restore_local_types(&mut self, snapshot: LocalTypesSnapshot) {
1006        self.local_types = snapshot.local_types;
1007        self.local_type_scopes = snapshot.local_type_scopes;
1008    }
1009
1010    /// Register an inline object schema from field names.
1011    ///
1012    /// Creates a TypeSchema for an object literal with the given fields.
1013    /// All fields are typed `FieldType::Any` at the schema layer since
1014    /// the caller does not provide per-field type info — the
1015    /// post_inference_verify pass at
1016    /// `crates/shape-vm/src/compiler/post_inference_verify.rs` absorbs
1017    /// the resulting `__inline_obj_N` schemas via the W17.2-C narrowed
1018    /// transitional whitelist row pre-W17.3 (per audit §4.D.5 + §9.B.3
1019    /// supervisor ratify 2026-05-19).
1020    ///
1021    /// **Deprecation (W17.2-C):** prefer
1022    /// [`Self::register_inline_object_schema_typed`] when per-field
1023    /// types are known at the call site. The untyped variant routes
1024    /// internally through `register_inline_object_schema_typed` with
1025    /// each field stamped `FieldType::Any` — same `__inline_obj_N` name
1026    /// format + same verification-pass absorber. Per audit §4.D.5
1027    /// PROPAGATE disposition: callers SHOULD migrate to the typed
1028    /// variant; the verification-pass safety net catches any residual
1029    /// Any leakage at user-facing schemas. ADR-006 §2.7.5 producer-side
1030    /// stamp.
1031    ///
1032    /// Returns the SchemaId for use with NewTypedObject opcode.
1033    ///
1034    /// # Example
1035    /// ```ignore
1036    /// // For: let x = { a: 1, b: "hello" }
1037    /// let schema_id = tracker.register_inline_object_schema(&["a", "b"]);
1038    /// // Now emit NewTypedObject with schema_id
1039    /// ```
1040    #[deprecated(
1041        since = "0.3.0",
1042        note = "Prefer `register_inline_object_schema_typed` per audit \
1043                §4.D.5 W17.2-C (PROPAGATE per-field types at call site). \
1044                The untyped variant routes through the typed variant \
1045                with FieldType::Any per field; the post_inference_verify \
1046                pass absorbs via the __inline_obj_* transitional row."
1047    )]
1048    pub fn register_inline_object_schema(&mut self, field_names: &[&str]) -> SchemaId {
1049        // Route through the typed variant with FieldType::Any per field
1050        // — keeps schema-name format (`__inline_obj_N`) + deduplication
1051        // identical, while the deprecation nudges callers toward the
1052        // typed variant. Per audit §4.D.5 W17.2-C PROPAGATE.
1053        let typed_fields: Vec<(&str, FieldType)> = field_names
1054            .iter()
1055            .map(|name| (*name, FieldType::Any))
1056            .collect();
1057        self.register_inline_object_schema_typed(&typed_fields)
1058    }
1059
1060    /// Register an inline object schema with typed fields
1061    ///
1062    /// Like `register_inline_object_schema` but allows specifying field types
1063    /// for better JIT optimization. Deduplicates by matching both field names
1064    /// and types.
1065    pub fn register_inline_object_schema_typed(
1066        &mut self,
1067        fields: &[(&str, FieldType)],
1068    ) -> SchemaId {
1069        if let Some(existing) = self.schema_registry.type_names().find_map(|name| {
1070            self.schema_registry.get(name).and_then(|schema| {
1071                if schema.fields.len() != fields.len() {
1072                    return None;
1073                }
1074                let same = schema
1075                    .fields
1076                    .iter()
1077                    .zip(fields.iter())
1078                    .all(|(f, (n, t))| f.name == *n && f.field_type == *t);
1079                if same { Some(schema.id) } else { None }
1080            })
1081        }) {
1082            return existing;
1083        }
1084
1085        let id = self.inline_object_counter;
1086        self.inline_object_counter += 1;
1087        let type_name = format!("__inline_obj_{}", id);
1088        let field_defs: Vec<(String, FieldType)> = fields
1089            .iter()
1090            .map(|(name, ft)| (name.to_string(), ft.clone()))
1091            .collect();
1092        let schema = TypeSchema::new(&type_name, field_defs);
1093        let schema_id = schema.id;
1094        self.schema_registry.register(schema);
1095        schema_id
1096    }
1097
1098    /// Register a named struct schema (e.g. `Point { x, y }`)
1099    ///
1100    /// Unlike `register_inline_object_schema` which auto-generates names,
1101    /// this uses the actual struct type name so `.type()` can resolve it.
1102    pub fn register_named_object_schema(
1103        &mut self,
1104        type_name: &str,
1105        fields: &[(&str, FieldType)],
1106    ) -> SchemaId {
1107        let field_defs: Vec<(String, FieldType)> = fields
1108            .iter()
1109            .map(|(name, ft)| (name.to_string(), ft.clone()))
1110            .collect();
1111
1112        let schema = TypeSchema::new(type_name, field_defs);
1113        let schema_id = schema.id;
1114        self.schema_registry.register(schema);
1115
1116        schema_id
1117    }
1118
1119    /// Register an inline object schema with typed fields
1120    ///
1121    /// Like `register_inline_object_schema` but allows specifying field types
1122    /// for better JIT optimization.
1123    pub fn register_typed_object_schema(
1124        &mut self,
1125        field_defs: Vec<(String, FieldType)>,
1126    ) -> SchemaId {
1127        let id = self.inline_object_counter;
1128        self.inline_object_counter += 1;
1129        let type_name = format!("__inline_obj_{}", id);
1130
1131        let schema = TypeSchema::new(&type_name, field_defs);
1132        let schema_id = schema.id;
1133        self.schema_registry.register(schema);
1134
1135        schema_id
1136    }
1137
1138    // --- v2 helpers ---
1139
1140    /// Register a v2 StructLayout for the given schema ID.
1141    pub fn register_v2_layout(&mut self, schema_id: SchemaId, layout: StructLayout) {
1142        self.v2_layouts.insert(schema_id, layout);
1143    }
1144
1145    /// Look up a v2 StructLayout by schema ID.
1146    pub fn get_v2_layout(&self, schema_id: SchemaId) -> Option<&StructLayout> {
1147        self.v2_layouts.get(&schema_id)
1148    }
1149
1150    /// Check if a local slot is a typed array and return its element kind.
1151    pub fn is_typed_array(&self, slot: u16) -> Option<FieldKind> {
1152        self.local_types.get(&slot)?.v2_array_element_kind
1153    }
1154
1155    /// Check if a local slot has a v2 struct layout and return its schema ID.
1156    pub fn is_typed_struct(&self, slot: u16) -> Option<SchemaId> {
1157        self.local_types.get(&slot)?.v2_struct_layout
1158    }
1159}
1160
1161impl Default for TypeTracker {
1162    fn default() -> Self {
1163        Self::empty()
1164    }
1165}
1166
1167// ─────────────────────────────────────────────────────────────────────
1168// Strict-typing predicate: `prove_native_kind` and `ProofGap`
1169//
1170// Every site emitting a typed opcode (AddI64, MulF64, ReturnValueBool,
1171// LoadLocalI64, etc.) must call `prove_native_kind` and propagate any
1172// `ProofGap` it returns. `ProofGap` has no public constructor — only
1173// this module can produce one — so emit code cannot fabricate a
1174// successful proof. The Rust type system enforces the discipline.
1175//
1176// Phase 2 wires call sites to use this predicate. Phase 3 makes the
1177// predicate panic in debug + test on missing proof. Phase 5 makes it a
1178// hard compile error E_TYPED_OPCODE_WITHOUT_PROOF in release.
1179// ─────────────────────────────────────────────────────────────────────
1180
1181/// A type-system proof gap — the compiler tried to emit a typed opcode
1182/// but could not prove the operand kind. Surfaces as
1183/// `E_TYPED_OPCODE_WITHOUT_PROOF` in release; panics in debug + test.
1184///
1185/// Constructor is private. Only [`prove_native_kind`] can produce one.
1186#[derive(Debug)]
1187pub struct ProofGap {
1188    site: &'static str,
1189    detail: String,
1190    _seal: ProofGapSeal,
1191}
1192
1193/// Module-private seal token. External code cannot construct a
1194/// `ProofGap` because they cannot construct a `ProofGapSeal`.
1195#[derive(Debug)]
1196struct ProofGapSeal(());
1197
1198impl ProofGap {
1199    /// Site identifier (e.g. "emit_typed_arithmetic", "emit_return_value").
1200    pub fn site(&self) -> &'static str {
1201        self.site
1202    }
1203
1204    /// Human-readable explanation of what couldn't be proved.
1205    pub fn detail(&self) -> &str {
1206        &self.detail
1207    }
1208}
1209
1210impl std::fmt::Display for ProofGap {
1211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1212        write!(
1213            f,
1214            "E_TYPED_OPCODE_WITHOUT_PROOF at {}: {}",
1215            self.site, self.detail
1216        )
1217    }
1218}
1219
1220impl std::error::Error for ProofGap {}
1221
1222/// Prove that the given slot's content is a specific [`NativeKind`] at
1223/// emission time, or surface a [`ProofGap`].
1224///
1225/// # Phase 2 contract (current)
1226///
1227/// Phase 2 wires call sites to call this predicate. Until Phase 3, the
1228/// predicate is a no-op that returns the supplied `claimed_kind` —
1229/// callers can run end-to-end. Phase 3 hardens to debug-panic; Phase 5
1230/// hardens to release compile-error.
1231///
1232/// # Future strict mode (Phases 3-5)
1233///
1234/// The body will inspect the compiler's slot-kind tracker for the
1235/// declared kind at the source location and compare against
1236/// `claimed_kind`. Mismatch or unknown → `ProofGap`.
1237#[inline]
1238pub fn prove_native_kind(
1239    site: &'static str,
1240    claimed_kind: NativeKind,
1241) -> Result<NativeKind, ProofGap> {
1242    // Phase 2 stub: pass-through. Phase 3+ replaces with real proof check.
1243    Ok(claimed_kind)
1244}
1245
1246/// Construct a `ProofGap` from inside this module only. Used by the
1247/// predicate (Phase 3+) when proof fails.
1248#[allow(dead_code)]
1249fn proof_gap(site: &'static str, detail: impl Into<String>) -> ProofGap {
1250    ProofGap {
1251        site,
1252        detail: detail.into(),
1253        _seal: ProofGapSeal(()),
1254    }
1255}
1256
1257/// Surface a [`ProofGap`] for an operand whose compile-time type could not be
1258/// resolved to a concrete kind.
1259///
1260/// This is the emit-side soundness floor (ε-1 PART 1): a typed opcode requires
1261/// the compiler to *prove* the operand's `NativeKind`. When the type-inference
1262/// engine leaves a value as an unresolved `Type::Variable` (or a still-bounded
1263/// `Type::Constrained` variable), the emitter has nothing to prove against —
1264/// it must NOT fall back to a default kind (the historical `Float64` default
1265/// is exactly the silent-wrong path this guards). The caller turns the
1266/// returned `ProofGap` into a clean compile error per CLAUDE.md §Type System
1267/// Rules ("if the type can't be proven, it is a compile error").
1268pub fn proof_gap_unresolved_operand(site: &'static str, detail: impl Into<String>) -> ProofGap {
1269    proof_gap(site, detail)
1270}
1271
1272#[cfg(test)]
1273mod tests {
1274    use super::*;
1275    use shape_runtime::type_schema::TypeSchemaBuilder;
1276
1277    #[test]
1278    fn test_basic_type_tracking() {
1279        let mut registry = TypeSchemaRegistry::new();
1280
1281        TypeSchemaBuilder::new("Point")
1282            .f64_field("x")
1283            .f64_field("y")
1284            .register(&mut registry);
1285
1286        let mut tracker = TypeTracker::new(registry);
1287
1288        // Set type for local slot 0
1289        tracker.set_local_type(0, VariableTypeInfo::named("Point".to_string()));
1290
1291        // Check that we can use typed access
1292        assert!(tracker.can_use_typed_access(0, true, "x"));
1293        assert!(tracker.can_use_typed_access(0, true, "y"));
1294        assert!(!tracker.can_use_typed_access(0, true, "z")); // Unknown field
1295    }
1296
1297    #[test]
1298    fn test_scope_tracking() {
1299        let mut tracker = TypeTracker::empty();
1300
1301        // Declare in outer scope
1302        tracker.set_local_type(0, VariableTypeInfo::named("Outer".to_string()));
1303
1304        // Push inner scope
1305        tracker.push_scope();
1306        tracker.set_local_type(1, VariableTypeInfo::named("Inner".to_string()));
1307
1308        assert!(tracker.get_local_type(0).is_some());
1309        assert!(tracker.get_local_type(1).is_some());
1310
1311        // Pop inner scope
1312        tracker.pop_scope();
1313
1314        // Outer still exists, inner removed
1315        assert!(tracker.get_local_type(0).is_some());
1316        assert!(tracker.get_local_type(1).is_none());
1317    }
1318
1319    #[test]
1320    fn test_binding_semantics_scope_tracking() {
1321        let mut tracker = TypeTracker::empty();
1322
1323        tracker.set_local_binding_semantics(
1324            0,
1325            BindingSemantics::deferred(BindingOwnershipClass::OwnedImmutable),
1326        );
1327        tracker.set_binding_semantics(
1328            5,
1329            BindingSemantics::deferred(BindingOwnershipClass::Flexible),
1330        );
1331
1332        tracker.push_scope();
1333        tracker.set_local_binding_semantics(
1334            1,
1335            BindingSemantics::deferred(BindingOwnershipClass::OwnedMutable),
1336        );
1337
1338        assert_eq!(
1339            tracker
1340                .get_local_binding_semantics(0)
1341                .map(|s| s.ownership_class),
1342            Some(BindingOwnershipClass::OwnedImmutable)
1343        );
1344        assert_eq!(
1345            tracker
1346                .get_local_binding_semantics(1)
1347                .map(|s| s.ownership_class),
1348            Some(BindingOwnershipClass::OwnedMutable)
1349        );
1350        assert_eq!(
1351            tracker.get_binding_semantics(5).map(|s| s.ownership_class),
1352            Some(BindingOwnershipClass::Flexible)
1353        );
1354
1355        tracker.pop_scope();
1356
1357        assert!(tracker.get_local_binding_semantics(1).is_none());
1358        assert!(tracker.get_local_binding_semantics(0).is_some());
1359        assert!(tracker.get_binding_semantics(5).is_some());
1360    }
1361
1362    #[test]
1363    fn test_binding_storage_class_updates() {
1364        let mut tracker = TypeTracker::empty();
1365        tracker.set_local_binding_semantics(
1366            0,
1367            BindingSemantics::deferred(BindingOwnershipClass::OwnedMutable),
1368        );
1369        tracker.set_binding_semantics(
1370            4,
1371            BindingSemantics::deferred(BindingOwnershipClass::Flexible),
1372        );
1373
1374        tracker.set_local_binding_storage_class(0, BindingStorageClass::Reference);
1375        tracker.set_binding_storage_class(4, BindingStorageClass::SharedCow);
1376
1377        assert_eq!(
1378            tracker
1379                .get_local_binding_semantics(0)
1380                .map(|s| s.storage_class),
1381            Some(BindingStorageClass::Reference)
1382        );
1383        assert_eq!(
1384            tracker.get_binding_semantics(4).map(|s| s.storage_class),
1385            Some(BindingStorageClass::SharedCow)
1386        );
1387
1388        tracker.clear_locals();
1389        assert!(tracker.get_local_binding_semantics(0).is_none());
1390        assert!(tracker.get_binding_semantics(4).is_some());
1391    }
1392
1393    #[test]
1394    fn test_function_return_types() {
1395        let mut tracker = TypeTracker::empty();
1396
1397        tracker.register_function_return_type("get_point", "Point");
1398
1399        assert_eq!(
1400            tracker.get_function_return_type("get_point"),
1401            Some(&"Point".to_string())
1402        );
1403        assert!(tracker.get_function_return_type("unknown").is_none());
1404    }
1405
1406    #[test]
1407    fn test_typed_field_info() {
1408        let mut registry = TypeSchemaRegistry::new();
1409
1410        TypeSchemaBuilder::new("Vector3")
1411            .f64_field("x")
1412            .f64_field("y")
1413            .f64_field("z")
1414            .register(&mut registry);
1415
1416        let tracker = TypeTracker::new(registry);
1417
1418        let info = tracker.get_typed_field_info("Vector3", "y");
1419        assert!(info.is_some());
1420        let (schema_id, offset, index) = info.unwrap();
1421        assert!(schema_id > 0);
1422        assert_eq!(offset, 8); // Second field, 8 bytes offset
1423        assert_eq!(index, 1);
1424    }
1425
1426    #[test]
1427    fn test_unknown_type() {
1428        let tracker = TypeTracker::empty();
1429
1430        // Unknown type should not allow typed access
1431        assert!(!tracker.can_use_typed_access(0, true, "field"));
1432    }
1433
1434    #[test]
1435    fn test_binding_type_tracking() {
1436        let mut registry = TypeSchemaRegistry::new();
1437
1438        TypeSchemaBuilder::new("Config")
1439            .f64_field("threshold")
1440            .string_field("name")
1441            .register(&mut registry);
1442
1443        let mut tracker = TypeTracker::new(registry);
1444
1445        // Set type for module_binding slot 5
1446        tracker.set_binding_type(5, VariableTypeInfo::named("Config".to_string()));
1447
1448        assert!(tracker.can_use_typed_access(5, false, "threshold"));
1449        assert!(tracker.can_use_typed_access(5, false, "name"));
1450        assert!(!tracker.can_use_typed_access(5, false, "unknown"));
1451    }
1452
1453    #[test]
1454    fn test_storage_hint_inference() {
1455        // Primitive types
1456        assert_eq!(
1457            VariableTypeInfo::infer_storage_hint("Number"),
1458            Some(StorageHint::Float64)
1459        );
1460        assert_eq!(
1461            VariableTypeInfo::infer_storage_hint("Integer"),
1462            Some(StorageHint::Int64)
1463        );
1464        assert_eq!(
1465            VariableTypeInfo::infer_storage_hint("Bool"),
1466            Some(StorageHint::Bool)
1467        );
1468        assert_eq!(
1469            VariableTypeInfo::infer_storage_hint("String"),
1470            Some(StorageHint::String)
1471        );
1472
1473        // Nullable types
1474        assert_eq!(
1475            VariableTypeInfo::infer_storage_hint("Option<Number>"),
1476            Some(StorageHint::NullableFloat64)
1477        );
1478        assert_eq!(
1479            VariableTypeInfo::infer_storage_hint("Option<Integer>"),
1480            Some(StorageHint::NullableInt64)
1481        );
1482        assert_eq!(
1483            VariableTypeInfo::infer_storage_hint("Option<byte>"),
1484            Some(StorageHint::NullableUInt8)
1485        );
1486        assert_eq!(
1487            VariableTypeInfo::infer_storage_hint("Option<char>"),
1488            Some(StorageHint::NullableInt8)
1489        );
1490        assert_eq!(
1491            VariableTypeInfo::infer_storage_hint("Option<u32>"),
1492            Some(StorageHint::NullableUInt32)
1493        );
1494
1495        // Unknown types — analysis-tier "not yet proven" is `None`
1496        // per ADR-006 §2.7.5.1 (no `NativeKind::Unknown` sentinel).
1497        assert_eq!(
1498            VariableTypeInfo::infer_storage_hint("SomeCustomType"),
1499            None
1500        );
1501    }
1502
1503    #[test]
1504    fn test_width_integer_storage_hint_inference() {
1505        assert_eq!(
1506            VariableTypeInfo::infer_storage_hint("i8"),
1507            Some(StorageHint::Int8)
1508        );
1509        assert_eq!(
1510            VariableTypeInfo::infer_storage_hint("byte"),
1511            Some(StorageHint::UInt8)
1512        );
1513        assert_eq!(
1514            VariableTypeInfo::infer_storage_hint("char"),
1515            Some(StorageHint::Int8)
1516        );
1517        assert_eq!(
1518            VariableTypeInfo::infer_storage_hint("u16"),
1519            Some(StorageHint::UInt16)
1520        );
1521        assert_eq!(
1522            VariableTypeInfo::infer_storage_hint("i32"),
1523            Some(StorageHint::Int32)
1524        );
1525        assert_eq!(
1526            VariableTypeInfo::infer_storage_hint("u64"),
1527            Some(StorageHint::UInt64)
1528        );
1529        assert_eq!(
1530            VariableTypeInfo::infer_storage_hint("isize"),
1531            Some(StorageHint::IntSize)
1532        );
1533        assert_eq!(
1534            VariableTypeInfo::infer_storage_hint("usize"),
1535            Some(StorageHint::UIntSize)
1536        );
1537    }
1538
1539    #[test]
1540    fn test_concrete_numeric_type_inference() {
1541        assert_eq!(
1542            VariableTypeInfo::infer_numeric_runtime_name("int"),
1543            Some("i64".to_string())
1544        );
1545        assert_eq!(
1546            VariableTypeInfo::infer_numeric_runtime_name("i16"),
1547            Some("i16".to_string())
1548        );
1549        assert_eq!(
1550            VariableTypeInfo::infer_numeric_runtime_name("byte"),
1551            Some("u8".to_string())
1552        );
1553        assert_eq!(
1554            VariableTypeInfo::infer_numeric_runtime_name("Option<f32>"),
1555            Some("f32".to_string())
1556        );
1557        assert_eq!(
1558            VariableTypeInfo::infer_numeric_runtime_name("SomeCustomType"),
1559            None
1560        );
1561    }
1562
1563    #[test]
1564    fn test_native_kind_from_storage_type() {
1565        assert_eq!(
1566            native_kind_from_storage_type(&StorageType::Float64),
1567            Some(NativeKind::Float64)
1568        );
1569        assert_eq!(
1570            native_kind_from_storage_type(&StorageType::NullableFloat64),
1571            Some(NativeKind::NullableFloat64)
1572        );
1573        // Bulldozer deleted the Unknown sink — complex types now return None.
1574        assert_eq!(
1575            native_kind_from_storage_type(&StorageType::Dynamic),
1576            None
1577        );
1578    }
1579
1580    #[test]
1581    fn test_nullable_number_type() {
1582        let info = VariableTypeInfo::nullable_number();
1583        assert!(info.uses_nan_sentinel());
1584        assert_eq!(info.storage_hint, Some(StorageHint::NullableFloat64));
1585    }
1586
1587    #[test]
1588    fn test_row_view_column_id_resolution() {
1589        let mut registry = TypeSchemaRegistry::new();
1590
1591        TypeSchemaBuilder::new("Candle")
1592            .f64_field("open")
1593            .f64_field("high")
1594            .f64_field("low")
1595            .f64_field("close")
1596            .i64_field("volume")
1597            .register(&mut registry);
1598
1599        let mut tracker = TypeTracker::new(registry);
1600
1601        // Get schema ID for Candle
1602        let schema = tracker.schema_registry().get("Candle").unwrap();
1603        let schema_id = schema.id;
1604
1605        // Set local slot 0 as a RowView<Candle>
1606        tracker.set_local_type(
1607            0,
1608            VariableTypeInfo::row_view(schema_id, "Candle".to_string()),
1609        );
1610
1611        // Should resolve known fields
1612        assert_eq!(tracker.get_row_view_column_id(0, true, "open"), Some(0));
1613        assert_eq!(tracker.get_row_view_column_id(0, true, "high"), Some(1));
1614        assert_eq!(tracker.get_row_view_column_id(0, true, "close"), Some(3));
1615        assert_eq!(tracker.get_row_view_column_id(0, true, "volume"), Some(4));
1616
1617        // Should return None for unknown fields
1618        assert_eq!(tracker.get_row_view_column_id(0, true, "nonexistent"), None);
1619
1620        // Non-row-view variable should return None
1621        tracker.set_local_type(1, VariableTypeInfo::named("Candle".to_string()));
1622        assert_eq!(tracker.get_row_view_column_id(1, true, "open"), None);
1623    }
1624
1625    #[test]
1626    fn test_tracker_storage_hints() {
1627        let mut tracker = TypeTracker::empty();
1628
1629        // Set local with nullable type
1630        tracker.set_local_type(0, VariableTypeInfo::nullable_number());
1631        assert!(tracker.local_uses_nan_sentinel(0));
1632
1633        // Set local with regular number
1634        tracker.set_local_type(1, VariableTypeInfo::number());
1635        assert!(!tracker.local_uses_nan_sentinel(1));
1636
1637        // Unknown slot
1638        assert!(!tracker.local_uses_nan_sentinel(99));
1639    }
1640
1641    #[test]
1642    fn test_datatable_type_info() {
1643        let mut registry = TypeSchemaRegistry::new();
1644
1645        TypeSchemaBuilder::new("Trade")
1646            .f64_field("price")
1647            .i64_field("volume")
1648            .string_field("symbol")
1649            .register(&mut registry);
1650
1651        let mut tracker = TypeTracker::new(registry);
1652
1653        let schema = tracker.schema_registry().get("Trade").unwrap();
1654        let schema_id = schema.id;
1655
1656        // Create a datatable type info
1657        tracker.set_local_type(
1658            0,
1659            VariableTypeInfo::datatable(schema_id, "Trade".to_string()),
1660        );
1661
1662        let info = tracker.get_local_type(0).unwrap();
1663        assert!(info.is_datatable());
1664        assert!(!info.is_row_view());
1665        assert_eq!(info.schema_id, Some(schema_id));
1666        assert_eq!(info.type_name.as_deref(), Some("Trade"));
1667
1668        // RowView should not be a datatable
1669        tracker.set_local_type(
1670            1,
1671            VariableTypeInfo::row_view(schema_id, "Trade".to_string()),
1672        );
1673        let info = tracker.get_local_type(1).unwrap();
1674        assert!(!info.is_datatable());
1675        assert!(info.is_row_view());
1676    }
1677
1678    #[test]
1679    fn test_v2_struct_layout_registration() {
1680        use shape_value::v2::struct_layout::{FieldKind, StructLayout};
1681
1682        let mut tracker = TypeTracker::empty();
1683
1684        let layout = StructLayout::new(&[("x", FieldKind::F64), ("y", FieldKind::F64)]);
1685        assert_eq!(layout.total_size(), 24);
1686
1687        // Use a fake schema ID
1688        let schema_id: SchemaId = 42;
1689        tracker.register_v2_layout(schema_id, layout);
1690
1691        let retrieved = tracker.get_v2_layout(schema_id);
1692        assert!(retrieved.is_some());
1693        let retrieved = retrieved.unwrap();
1694        assert_eq!(retrieved.field_count(), 2);
1695        assert_eq!(retrieved.field_offset(0), 8);
1696        assert_eq!(retrieved.field_offset(1), 16);
1697        assert_eq!(retrieved.total_size(), 24);
1698
1699        // Non-existent schema ID returns None
1700        assert!(tracker.get_v2_layout(999).is_none());
1701    }
1702
1703    #[test]
1704    fn test_v2_typed_array_element_kind() {
1705        use shape_value::v2::struct_layout::FieldKind;
1706
1707        let mut tracker = TypeTracker::empty();
1708
1709        // Set up a local as a typed array of F64
1710        let mut info = VariableTypeInfo::named("Array<number>".to_string());
1711        info.v2_array_element_kind = Some(FieldKind::F64);
1712        tracker.set_local_type(0, info);
1713
1714        assert_eq!(tracker.is_typed_array(0), Some(FieldKind::F64));
1715        assert_eq!(tracker.is_typed_array(1), None); // unset slot
1716
1717        // Set up a local as a typed array of I32
1718        let mut info2 = VariableTypeInfo::named("Array<i32>".to_string());
1719        info2.v2_array_element_kind = Some(FieldKind::I32);
1720        tracker.set_local_type(1, info2);
1721
1722        assert_eq!(tracker.is_typed_array(1), Some(FieldKind::I32));
1723    }
1724
1725    #[test]
1726    fn test_v2_typed_struct_on_variable() {
1727        use shape_value::v2::struct_layout::{FieldKind, StructLayout};
1728
1729        let mut tracker = TypeTracker::empty();
1730
1731        let layout = StructLayout::new(&[
1732            ("name", FieldKind::Ptr),
1733            ("age", FieldKind::I32),
1734            ("score", FieldKind::F64),
1735        ]);
1736        let schema_id: SchemaId = 100;
1737        tracker.register_v2_layout(schema_id, layout);
1738
1739        // Set up a local with v2_struct_layout
1740        let mut info = VariableTypeInfo::named("Person".to_string());
1741        info.v2_struct_layout = Some(schema_id);
1742        tracker.set_local_type(0, info);
1743
1744        // Verify is_typed_struct returns the schema ID
1745        assert_eq!(tracker.is_typed_struct(0), Some(schema_id));
1746        assert_eq!(tracker.is_typed_struct(1), None);
1747
1748        // Verify we can look up the layout from the schema ID
1749        let layout = tracker.get_v2_layout(schema_id).unwrap();
1750        assert_eq!(layout.field_count(), 3);
1751        assert_eq!(layout.field_kind(0), FieldKind::Ptr);
1752        assert_eq!(layout.field_kind(1), FieldKind::I32);
1753        assert_eq!(layout.field_kind(2), FieldKind::F64);
1754        assert_eq!(layout.heap_field_mask, 0b001); // only field 0 is Ptr
1755    }
1756
1757    #[test]
1758    fn test_v2_fields_default_none() {
1759        // All constructors should default v2 fields to None
1760        let info = VariableTypeInfo::unknown();
1761        assert!(info.v2_array_element_kind.is_none());
1762        assert!(info.v2_struct_layout.is_none());
1763
1764        let info = VariableTypeInfo::number();
1765        assert!(info.v2_array_element_kind.is_none());
1766        assert!(info.v2_struct_layout.is_none());
1767
1768        let info = VariableTypeInfo::named("Foo".to_string());
1769        assert!(info.v2_array_element_kind.is_none());
1770        assert!(info.v2_struct_layout.is_none());
1771
1772        let info = VariableTypeInfo::known(1, "Bar".to_string());
1773        assert!(info.v2_array_element_kind.is_none());
1774        assert!(info.v2_struct_layout.is_none());
1775    }
1776
1777    /// ε-1 PART 1: the emit-side soundness guard surfaces a `ProofGap`,
1778    /// labelled `E_TYPED_OPCODE_WITHOUT_PROOF`, carrying the emission site and
1779    /// the human-readable reason. This is the clean diagnostic the binary-op
1780    /// emitter raises instead of stamping a default `NativeKind` on an operand
1781    /// whose type the inference engine could not resolve.
1782    #[test]
1783    fn test_proof_gap_unresolved_operand_surfaces_cleanly() {
1784        let gap = proof_gap_unresolved_operand(
1785            "emit_typed_arithmetic",
1786            "operand `x` of `Mul` has an unresolved type",
1787        );
1788        assert_eq!(gap.site(), "emit_typed_arithmetic");
1789        assert!(gap.detail().contains("unresolved type"));
1790        let rendered = gap.to_string();
1791        assert!(
1792            rendered.starts_with("E_TYPED_OPCODE_WITHOUT_PROOF at emit_typed_arithmetic:"),
1793            "diagnostic must be the labelled proof-gap form, got: {rendered}"
1794        );
1795        assert!(rendered.contains("operand `x` of `Mul`"));
1796    }
1797}