Skip to main content

shape_vm/compiler/
helpers.rs

1//! Helper methods for bytecode compilation
2
3use super::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};
9use std::sync::OnceLock;
10
11use super::{BuiltinNameResolution, BytecodeCompiler, DropKind, ParamPassMode, ResolutionScope};
12
13/// Phase V1.1D: default-on ownership-aware local opcodes.
14///
15/// When `true`, the compiler emits the ownership-aware `MoveLocal` /
16/// `CloneLocal` / `DropLocal` opcodes (V1.1A/B) for heap-ref (`UniqueHeap`
17/// / Direct-owned) bindings. When `false`, emission is byte-identical to
18/// pre-V1.1C: `LoadLocal` / `LoadLocalMove` / `LoadLocalClone` continue
19/// unchanged; no `MoveLocal` / `CloneLocal` / `DropLocal` are produced.
20///
21/// V1.1C landed with the default `false` (opt-in via
22/// `SHAPE_V2_OWNERSHIP_MOVES=1`). V1.1D flips the default to `true` after
23/// fixing three emission bugs the opt-in soak surfaced:
24///
25///   * Function compilation did not save/restore `ownership_drop_locals`
26///     around the callee scope, leaking per-function `DropLocal` entries
27///     into the caller's main-level drop pass. This corrupted the result
28///     of any heap-returning callee (DateTime arithmetic, comptime body
29///     replacement). Fixed in `compiler/functions.rs`.
30///   * `CloneLocal` was emitted for `let mut` slots that had been
31///     cell-wrapped into a `SharedCell` by a subsequent closure
32///     capture. `clone_raw_bits` bumps the cell's Arc without unwrapping,
33///     so arithmetic then saw `shared_cell` instead of the inner scalar
34///     ("Cannot apply '+' to int and shared_cell"). Gated by
35///     `slot_is_boxed` in `emit_load_local_owned`.
36///   * Symmetric bug for `DropLocal`: a boxed slot received both the
37///     V1.1C `DropLocal` *and* the legacy `DropCall` pass, poisoning the
38///     SharedCell before the legacy unwrap could read it.
39///     `binding_slot_needs_ownership_drop` and the drop-scope emission
40///     sites now skip boxed slots so the Arc-refcount release path owns
41///     the release alone.
42///
43/// Polarity inversion: the env var is now an opt-OUT. Values
44/// `0` / `false` / `off` / `no` (case-insensitive, trimmed) disable the
45/// flag; unset, empty, or any other value keeps the V1.1D default of
46/// `true`. This mirrors the V0.a `SHAPE_V2_VAR_SHAREDCOW` pattern (see
47/// `crates/shape-vm/src/mir/storage_planning.rs:50`).
48///
49/// Rollback: `SHAPE_V2_OWNERSHIP_MOVES=0` restores the pre-V1.1D
50/// byte-identical emission. A single-commit revert is also sufficient.
51///
52/// For unit-test determinism — the `OnceLock` cache freezes whichever env
53/// state the test binary starts with, and multiple tests racing
54/// `std::env::set_var` would poison the cache — a `#[cfg(test)]`
55/// thread-local override (`with_ownership_moves_flag`) lets a single
56/// test temporarily force the flag on/off without touching the env.
57pub(super) fn ownership_moves_enabled() -> bool {
58    #[cfg(test)]
59    {
60        if let Some(v) = TEST_OWNERSHIP_MOVES_OVERRIDE.with(|cell| cell.get()) {
61            return v;
62        }
63    }
64    static CACHED: OnceLock<bool> = OnceLock::new();
65    *CACHED.get_or_init(|| match std::env::var("SHAPE_V2_OWNERSHIP_MOVES") {
66        Ok(v) => !matches!(
67            v.trim(),
68            "0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
69        ),
70        Err(_) => true,
71    })
72}
73
74#[cfg(test)]
75thread_local! {
76    /// Phase V1.1C test hook: per-thread override for
77    /// `ownership_moves_enabled()`. When `Some(b)`, the flag reads as `b`
78    /// regardless of env-var state. The override is scoped to a single
79    /// closure via `with_ownership_moves_flag` and is cleared on drop —
80    /// tests running on the same thread cannot leak flag state into each
81    /// other. Thread-local rather than a global mutex so concurrent
82    /// `cargo test` workers stay independent.
83    pub(super) static TEST_OWNERSHIP_MOVES_OVERRIDE: std::cell::Cell<Option<bool>> =
84        const { std::cell::Cell::new(None) };
85}
86
87/// Phase V1.1C test helper: run `f` with the ownership-moves flag
88/// forced to `enabled`. Restores the previous override on return (even
89/// on panic). This is the compile-time-gated test path; production code
90/// reads the env var exclusively via `ownership_moves_enabled()`.
91#[cfg(test)]
92pub(crate) fn with_ownership_moves_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
93    struct Guard(Option<bool>);
94    impl Drop for Guard {
95        fn drop(&mut self) {
96            TEST_OWNERSHIP_MOVES_OVERRIDE.with(|cell| cell.set(self.0));
97        }
98    }
99    let prev = TEST_OWNERSHIP_MOVES_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
100    let _guard = Guard(prev);
101    f()
102}
103
104/// Phase V1.2C/D: default-on compiler emission of `PromoteToShared`.
105///
106/// When `true`, the compiler emits `PromoteToShared` (V1.2A/B) at escape
107/// points — sites where a uniquely-owned (Box-backed) value transitions to
108/// shared (Arc-backed) ownership:
109///
110///   * Site A: a `let` / `const` binding classified as `UniqueHeap` is
111///     captured by an *escaping* closure (`emit_make_closure_heap_next`
112///     was set by the caller, e.g. a return-of-closure or
113///     store-into-collection pattern). The value is pushed for the
114///     closure env; `PromoteToShared` converts it to Arc so the closure
115///     can outlive the owning scope safely.
116///
117///   * Site B: a `var`-like assignment target with `SharedCow` storage
118///     is written from an rhs that was just produced by `PromoteToOwned`
119///     (Box-backed). The target's Arc-shared representation needs the
120///     value in Arc form; `PromoteToShared` performs the Box→Arc
121///     transfer without a refcount bump.
122///
123/// Site C (passing owned to an Arc-expecting parameter) is deferred to
124/// V1.3: it requires `FunctionBorrowSummary.param_ownership_hints` which
125/// does not exist at V1.2 time.
126///
127/// V1.2C ships the emission gated on this flag; V1.2D flips the default
128/// to `true`. Polarity matches the V1.1D convention: the env var is an
129/// opt-OUT. Values `0` / `false` / `off` / `no` (case-insensitive,
130/// trimmed) disable the flag; unset, empty, or any other value keeps the
131/// V1.2D default of `true`. Mirrors the V0.a `SHAPE_V2_VAR_SHAREDCOW`
132/// pattern (`crates/shape-vm/src/mir/storage_planning.rs:50`) and the
133/// V1.1D `SHAPE_V2_OWNERSHIP_MOVES` pattern above.
134///
135/// Rollback: `SHAPE_V2_PROMOTE_TO_SHARED=0` restores the pre-V1.2C
136/// byte-identical emission at both sites.
137///
138/// For unit-test determinism, a `#[cfg(test)]` thread-local override
139/// (`with_promote_to_shared_flag`) lets a single test force the flag
140/// on/off without touching the env-var cache.
141pub(super) fn promote_to_shared_enabled() -> bool {
142    #[cfg(test)]
143    {
144        if let Some(v) = TEST_PROMOTE_TO_SHARED_OVERRIDE.with(|cell| cell.get()) {
145            return v;
146        }
147    }
148    static CACHED: OnceLock<bool> = OnceLock::new();
149    *CACHED.get_or_init(|| match std::env::var("SHAPE_V2_PROMOTE_TO_SHARED") {
150        Ok(v) => !matches!(
151            v.trim(),
152            "0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
153        ),
154        Err(_) => true,
155    })
156}
157
158#[cfg(test)]
159thread_local! {
160    /// Phase V1.2C test hook: per-thread override for
161    /// `promote_to_shared_enabled()`. Thread-local so concurrent
162    /// `cargo test` workers remain independent.
163    pub(super) static TEST_PROMOTE_TO_SHARED_OVERRIDE: std::cell::Cell<Option<bool>> =
164        const { std::cell::Cell::new(None) };
165}
166
167/// Phase V1.2C test helper: run `f` with the PromoteToShared flag forced
168/// to `enabled`. Restores the previous override on return (even on
169/// panic). Production code reads the env var exclusively via
170/// `promote_to_shared_enabled()`.
171#[cfg(test)]
172pub(crate) fn with_promote_to_shared_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
173    struct Guard(Option<bool>);
174    impl Drop for Guard {
175        fn drop(&mut self) {
176            TEST_PROMOTE_TO_SHARED_OVERRIDE.with(|cell| cell.set(self.0));
177        }
178    }
179    let prev = TEST_PROMOTE_TO_SHARED_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
180    let _guard = Guard(prev);
181    f()
182}
183
184/// Phase V1.3: default-on Box-by-default allocation for `UniqueHeap` locals.
185///
186/// When `true`, the compiler extends the Phase 3/4 `PromoteToOwned` emission
187/// (which pre-V1.3 only fired for `BindingStorageClass::Direct` heap-typed
188/// `let`/`const`) to also fire for `BindingStorageClass::UniqueHeap` slots.
189/// Under the V1.2D baseline `UniqueHeap` bindings (a `let`/`const` value
190/// captured by a mutating closure; see `storage_planning.rs:935` rule 2)
191/// landed in their slot as freshly-allocated `Arc<HeapValue>` with refcount
192/// 1 — a wasted atomic per allocation since the binding is uniquely owned
193/// by construction. V1.3 converts these to `Box<HeapValue>` via
194/// `PromoteToOwned` so the non-escape case pays zero atomic ops.
195///
196/// Escape safety: V1.2's `PromoteToShared` emission (default on) already
197/// covers the two escape vectors — Site A (capture into an escaping
198/// closure) and Site B (assignment into a SharedCow-backed `var`). The
199/// V1.3 switch therefore operates behind the escape boundary: values start
200/// as Box and promote to Arc at the escape point if needed.
201///
202/// Mechanism: V1.3 does not add any new opcodes. It extends the existing
203/// `PromoteToOwned` (0x107) emission in statements.rs by broadening the
204/// storage-class predicate. When the flag is off, the predicate keeps its
205/// V1.2D shape (`Direct` only) and bytecode is byte-identical to pre-V1.3.
206///
207/// Polarity matches V1.1D / V1.2D: the env var is an opt-OUT. Values
208/// `0` / `false` / `off` / `no` (case-insensitive, trimmed) disable the
209/// flag; unset, empty, or any other value keeps the V1.3 default of
210/// `true`. Mirrors `SHAPE_V2_VAR_SHAREDCOW` (V0.a),
211/// `SHAPE_V2_OWNERSHIP_MOVES` (V1.1D), `SHAPE_V2_PROMOTE_TO_SHARED`
212/// (V1.2D).
213///
214/// Rollback: `SHAPE_V2_BOX_BY_DEFAULT=0` restores the pre-V1.3
215/// byte-identical emission. A single-commit revert also suffices.
216///
217/// For unit-test determinism, a `#[cfg(test)]` thread-local override
218/// (`with_box_by_default_flag`) lets a single test force the flag on/off
219/// without touching the env-var cache.
220pub(super) fn box_by_default_enabled() -> bool {
221    #[cfg(test)]
222    {
223        if let Some(v) = TEST_BOX_BY_DEFAULT_OVERRIDE.with(|cell| cell.get()) {
224            return v;
225        }
226    }
227    static CACHED: OnceLock<bool> = OnceLock::new();
228    *CACHED.get_or_init(|| match std::env::var("SHAPE_V2_BOX_BY_DEFAULT") {
229        Ok(v) => !matches!(
230            v.trim(),
231            "0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
232        ),
233        Err(_) => true,
234    })
235}
236
237#[cfg(test)]
238thread_local! {
239    /// Phase V1.3 test hook: per-thread override for
240    /// `box_by_default_enabled()`. Thread-local so concurrent
241    /// `cargo test` workers remain independent.
242    pub(super) static TEST_BOX_BY_DEFAULT_OVERRIDE: std::cell::Cell<Option<bool>> =
243        const { std::cell::Cell::new(None) };
244}
245
246/// Phase V1.3 test helper: run `f` with the Box-by-default flag forced to
247/// `enabled`. Restores the previous override on return (even on panic).
248/// Production code reads the env var exclusively via
249/// `box_by_default_enabled()`.
250#[cfg(test)]
251pub(crate) fn with_box_by_default_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
252    struct Guard(Option<bool>);
253    impl Drop for Guard {
254        fn drop(&mut self) {
255            TEST_BOX_BY_DEFAULT_OVERRIDE.with(|cell| cell.set(self.0));
256        }
257    }
258    let prev = TEST_BOX_BY_DEFAULT_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
259    let _guard = Guard(prev);
260    f()
261}
262
263/// Phase R5.1C: default-on compiler emission of typed bitwise opcodes
264/// (`BitAndInt`, `BitOrInt`, `BitXorInt`, `BitShlInt`, `BitShrInt`,
265/// `BitNotInt`) when both (or the sole) operand type is proved `int` at
266/// compile time.
267///
268/// R5.1A added the opcode variants; R5.1B wired the executor handlers.
269/// R5.1C (this phase) turns on compiler emission. When `true`, bitwise
270/// expressions whose operand types are provably `int` emit the typed
271/// opcode instead of the Dynamic (`BitAnd`/`BitOr`/...) variant. Mixed
272/// or unresolved operand types continue to fall through to the Dynamic
273/// path — no behavior change for those cases.
274///
275/// Polarity matches `SHAPE_V2_OWNERSHIP_MOVES` / `SHAPE_V2_BOX_BY_DEFAULT`:
276/// the env var is an opt-OUT. Values `0` / `false` / `off` / `no`
277/// (case-insensitive, trimmed) disable the flag; unset, empty, or any
278/// other value keeps the R5.1C default of `true`.
279///
280/// Rollback: `SHAPE_V2_TYPED_BITWISE=0` restores pre-R5.1C byte-identical
281/// emission (bitwise ops always go to the Dynamic variants).
282///
283/// For unit-test determinism, a `#[cfg(test)]` thread-local override
284/// (`with_typed_bitwise_flag`) lets a single test force the flag on/off
285/// without touching the env-var cache.
286pub(super) fn typed_bitwise_enabled() -> bool {
287    #[cfg(test)]
288    {
289        if let Some(v) = TEST_TYPED_BITWISE_OVERRIDE.with(|cell| cell.get()) {
290            return v;
291        }
292    }
293    static CACHED: OnceLock<bool> = OnceLock::new();
294    *CACHED.get_or_init(|| match std::env::var("SHAPE_V2_TYPED_BITWISE") {
295        Ok(v) => !matches!(
296            v.trim(),
297            "0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
298        ),
299        Err(_) => true,
300    })
301}
302
303#[cfg(test)]
304thread_local! {
305    /// Phase R5.1C test hook: per-thread override for
306    /// `typed_bitwise_enabled()`. Thread-local so concurrent
307    /// `cargo test` workers remain independent.
308    pub(super) static TEST_TYPED_BITWISE_OVERRIDE: std::cell::Cell<Option<bool>> =
309        const { std::cell::Cell::new(None) };
310}
311
312/// Phase R5.1C test helper: run `f` with the typed-bitwise flag forced to
313/// `enabled`. Restores the previous override on return (even on panic).
314/// Production code reads the env var exclusively via
315/// `typed_bitwise_enabled()`.
316#[cfg(test)]
317pub(crate) fn with_typed_bitwise_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
318    struct Guard(Option<bool>);
319    impl Drop for Guard {
320        fn drop(&mut self) {
321            TEST_TYPED_BITWISE_OVERRIDE.with(|cell| cell.set(self.0));
322        }
323    }
324    let prev = TEST_TYPED_BITWISE_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
325    let _guard = Guard(prev);
326    f()
327}
328
329/// Phase R5.5: default-on compiler emission of typed string+scalar concat
330/// opcodes (`StringConcatInt`, `StringConcatNumber`, `StringConcatBool`)
331/// when `BinaryOp::Add` has a `string`-typed LHS and an `int`/`number`/
332/// `bool`-typed RHS proved at compile time.
333///
334/// When `true`, string+scalar Add expressions emit one of the three typed
335/// opcodes, bypassing the dynamic fallback's string-coercion branch in
336/// `exec_arithmetic_dynamic_fallback`. When `false`, emission falls back
337/// to the pre-R5.5 Dynamic path — the `AddDynamic` handler's
338/// `try_heap_arithmetic` Case 2 still handles int/number RHS.
339///
340/// Polarity mirrors `typed_bitwise_enabled()`: the env var is an opt-OUT.
341/// Values `0` / `false` / `off` / `no` (case-insensitive, trimmed)
342/// disable the flag; unset, empty, or any other value keeps the R5.5
343/// default of `true`.
344///
345/// Rollback: `SHAPE_V2_STRING_COERCE_CONCAT=0` restores pre-R5.5 emission
346/// (string+scalar Add always goes through the Dynamic variant).
347///
348/// For unit-test determinism, a `#[cfg(test)]` thread-local override
349/// (`with_typed_string_coerce_concat_flag`) lets a single test force the
350/// flag on/off without touching the env-var cache.
351pub(super) fn typed_string_coerce_concat_enabled() -> bool {
352    #[cfg(test)]
353    {
354        if let Some(v) = TEST_TYPED_STRING_COERCE_CONCAT_OVERRIDE.with(|cell| cell.get()) {
355            return v;
356        }
357    }
358    static CACHED: OnceLock<bool> = OnceLock::new();
359    *CACHED.get_or_init(|| match std::env::var("SHAPE_V2_STRING_COERCE_CONCAT") {
360        Ok(v) => !matches!(
361            v.trim(),
362            "0" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "no" | "NO" | "No"
363        ),
364        Err(_) => true,
365    })
366}
367
368#[cfg(test)]
369thread_local! {
370    /// Phase R5.5 test hook: per-thread override for
371    /// `typed_string_coerce_concat_enabled()`. Thread-local so concurrent
372    /// `cargo test` workers remain independent.
373    pub(super) static TEST_TYPED_STRING_COERCE_CONCAT_OVERRIDE: std::cell::Cell<Option<bool>> =
374        const { std::cell::Cell::new(None) };
375}
376
377/// Phase R5.5 test helper: run `f` with the typed string+scalar concat
378/// flag forced to `enabled`. Restores the previous override on return
379/// (even on panic). Production code reads the env var exclusively via
380/// `typed_string_coerce_concat_enabled()`.
381#[cfg(test)]
382pub(crate) fn with_typed_string_coerce_concat_flag<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
383    struct Guard(Option<bool>);
384    impl Drop for Guard {
385        fn drop(&mut self) {
386            TEST_TYPED_STRING_COERCE_CONCAT_OVERRIDE.with(|cell| cell.set(self.0));
387        }
388    }
389    let prev = TEST_TYPED_STRING_COERCE_CONCAT_OVERRIDE.with(|cell| cell.replace(Some(enabled)));
390    let _guard = Guard(prev);
391    f()
392}
393
394// ── ADR-006 §2.7.5 conduit — top-level MIR-slot ConcreteType inference ───
395
396/// Walk a top-level MIR function and infer a per-MIR-slot `ConcreteType`
397/// vector for the JIT MirToIR conduit.
398///
399/// ADR-006 §2.7.5 conduit (W12-top-level-concrete-types-conduit close,
400/// 2026-05-12). The JIT's typed-array / TypedObject fast paths require
401/// the destination slot's `ConcreteType` to be proven at compile time;
402/// reaching the fast path with an unproven slot would surface-and-stop.
403///
404/// This walk is the conduit's producer side. Each kind-source statement
405/// in the MIR carries enough structural information to stamp the
406/// destination slot's `ConcreteType`:
407///
408/// - `StatementKind::ObjectStore { container_slot, field_names, .. }`
409///   stamps `Struct(StructLayoutId(0))` (the schema-id placeholder is
410///   irrelevant for the JIT short-circuit which only checks the variant
411///   tag). The MIR-level lowering pairs every struct/object construction
412///   `(StructLiteral / Object)` with `ContainerStoreKind::Object` —
413///   `lowering/expr.rs:1597..1716`.
414/// - `StatementKind::EnumStore { container_slot, .. }` stamps
415///   `Enum(EnumLayoutId(0))` — the JIT treats `Enum(_)` and `Struct(_)`
416///   identically at the Aggregate short-circuit per
417///   `is_typed_object_slot`.
418/// - `StatementKind::ArrayStore { container_slot, .. }` — array element
419///   type is not directly known from the ArrayStore alone. We do NOT
420///   stamp `Array(Void)` here (it would mask the genuine surface-and-stop
421///   case for unproven element kinds). Array literals with proven
422///   `Array<scalar>` element kind are typed-array-allocated by the
423///   bytecode compiler via `NewTypedArrayF64/I64/I32/Bool` opcodes
424///   (`compile_expr_array` in `expressions/collections.rs`) — that path
425///   doesn't go through `Rvalue::Aggregate` at all. The remaining
426///   generic-`Array` case must legitimately surface-and-stop in the JIT
427///   until a richer element-kind kind-source landing arrives.
428///
429/// `ConcreteType::Void` per slot is the explicit "no information
430/// inferred" sentinel per §2.7.5.1 — NOT a Bool-default fallback per
431/// forbidden #9. Downstream JIT consumers (`concrete_type_for_slot` in
432/// `v2_array.rs`) treat `Void` as "fall through to legacy path".
433///
434/// Wrapper for the resolver-aware variant — preserves existing callers
435/// that don't have access to a callee-return resolver. Call-terminator
436/// destinations stay `Void` when this entry point is used; the resolver-
437/// aware variant `infer_top_level_concrete_types_from_mir_with_returns`
438/// stamps them from the callee's declared return type.
439pub(crate) fn infer_top_level_concrete_types_from_mir(
440    mir: &crate::mir::MirFunction,
441) -> Vec<shape_value::v2::ConcreteType> {
442    infer_top_level_concrete_types_from_mir_with_returns(mir, None)
443}
444
445/// Resolver-aware conduit producer.
446///
447/// `callee_returns(name) -> Option<&ConcreteType>` returns the declared
448/// `ConcreteType` of the named function's return value, or `None` for
449/// "unknown / not annotated / not user-defined". The body stamps
450/// `TerminatorKind::Call { destination, .. }` slots from the resolver
451/// when the callee is `MirConstant::Function(name)`.
452///
453/// ADR-006 §2.7.5 — W12-jit-call-return-kind, 2026-05-12. Producing-
454/// site classification at the bytecode-compile layer: the callee's
455/// declared return type is the proof source for the destination slot's
456/// ConcreteType. No tag-bit decode, no Bool-default — when the
457/// resolver returns `None` the slot stays `Void`.
458///
459/// Wrapper for the trait-method-aware variant — preserves existing
460/// callers that don't have access to a method-return resolver.
461pub(crate) fn infer_top_level_concrete_types_from_mir_with_returns(
462    mir: &crate::mir::MirFunction,
463    callee_returns: Option<&dyn Fn(&str) -> Option<shape_value::v2::ConcreteType>>,
464) -> Vec<shape_value::v2::ConcreteType> {
465    infer_top_level_concrete_types_from_mir_with_resolvers(
466        mir,
467        callee_returns,
468        None,
469        None,
470        None,
471    )
472}
473
474/// Trait-method-aware variant of the conduit producer.
475///
476/// `method_returns(type_name, method_name) -> Option<ConcreteType>`
477/// resolves trait-method dispatch return ConcreteType — the implementation
478/// looks up `find_default_trait_impl_for_type_method(type_name, method_name)`
479/// on the `BytecodeProgram`, then maps the resulting function name through
480/// `function_return_concrete_types`. When the chain resolves cleanly the
481/// returned ConcreteType stamps the destination slot of the
482/// `MirConstant::Method(name)` Call terminator.
483///
484/// ADR-006 §2.7.5 — Phase 3 cluster-0 Round 13 T1' gap 1 + gap 2
485/// closure (2026-05-13). The receiver-type-name source is
486/// `mir.local_struct_type_names`, populated at MIR lowering for
487/// `Expr::StructLiteral { type_name, .. }` sites (T1' gap 1 closure
488/// at `mir/lowering/expr.rs::Expr::StructLiteral`). The trait method
489/// return-type chain reuses commit 1's gap 3 closure: the impl
490/// method's `FunctionDef.return_type` is backfilled from the trait
491/// declaration when the impl source omits it, so
492/// `function_return_concrete_types["X::name"] = ConcreteType::String`
493/// for Smoke 3 and the method-returns resolver looks up the same
494/// table via the trait-impl function name.
495///
496/// Trait-dispatch receivers carry the struct identity through slot
497/// moves via the slot-move propagation pass below — `let t = X {}; let
498/// u = t; u.name()` propagates the struct type name from `t`'s
499/// construction slot to `u`'s binding slot.
500pub(crate) fn infer_top_level_concrete_types_from_mir_with_resolvers(
501    mir: &crate::mir::MirFunction,
502    callee_returns: Option<&dyn Fn(&str) -> Option<shape_value::v2::ConcreteType>>,
503    method_returns: Option<
504        &dyn Fn(&str, &str) -> Option<shape_value::v2::ConcreteType>,
505    >,
506    // ADR-006 §2.7.5 V3-S6b conduit consumer.
507    //
508    // `monomorph_method_returns(call_site_span) -> Option<ConcreteType>`
509    // consults the `monomorphized_method_call_sites: HashMap<(Span,
510    // Option<usize>), usize>` side-table populated by
511    // `try_monomorphize_method_call` /
512    // `try_monomorphize_method_call_with_closures` on specialization
513    // success, then chains the looked-up specialized FunctionId through
514    // `function_return_concrete_types[specialized_idx]` to recover the
515    // callee specialization's declared return type. The caller closes
516    // over the `current_function` half of the composite key — top-level
517    // conduits pass `None` for `current_function`; per-function conduits
518    // pass `Some(fn_idx)` matching the function being walked.
519    //
520    // When the side-table holds an entry for the `Terminator.span` of a
521    // `MirConstant::Method` Call-terminator (regardless of receiver
522    // type), the destination slot's ConcreteType is stamped from the
523    // resolver's return value. This is the V3-S6 chain checkpoint-final
524    // wiring: it carries the `.map()` chain's intermediate carrier shape
525    // through to the JIT-side `parametric_method_return_kind_from_receiver`
526    // arm, which then trivially classifies `.sum()` after `.map()` on
527    // `Vec<I64>` → `Int64` via the existing
528    // `("sum"|..., ConcreteType::Array(elem))` arm.
529    //
530    // PATH α per supervisor 2026-05-15 ratification (side-table consult
531    // at compile time; no runtime tag-byte read; §2.7.5 stamp-at-compile-
532    // time preserved).
533    monomorph_method_returns: Option<
534        &dyn Fn(shape_ast::ast::span::Span) -> Option<shape_value::v2::ConcreteType>,
535    >,
536    // cluster-2-cw-IB-class-b (2026-05-16, supervisor R3 binding-ratified):
537    // value-call return-`ConcreteType` resolver.
538    //
539    // `value_call_returns(call_site_span) -> Option<ConcreteType>` consults
540    // the `value_call_return_concrete_types: HashMap<(Span, Option<usize>),
541    // ConcreteType>` side-table populated by `compile_expr_function_call`'s
542    // value-call branch (closure-bound callee with caller-context-inferred
543    // body return). The caller closes over the `current_function` half of
544    // the composite key — top-level conduits pass `None` for
545    // `current_function`; per-function conduits pass `Some(fn_idx)`
546    // matching the function being walked. Same shape as
547    // `monomorph_method_returns` above.
548    //
549    // When the side-table holds an entry for the `Terminator.span` of a
550    // value-call (Call-terminator with `func: Operand::Copy/Move/
551    // MoveExplicit(Place::Local(_))`), the destination slot's
552    // ConcreteType is stamped from the resolver's return. The downstream
553    // JIT-side `place_native_kind` projection picks up the destination's
554    // `NativeKind`, closing the `print(f(xs))` kind-classification chain
555    // for Class B per inventory §B.2.
556    //
557    // No tag-bit decode, no Bool-default, no fabricated default — when
558    // the resolver returns `None` the slot stays `Void` per §2.7.5.1.
559    // ADR-006 §2.7.5 stamp-at-compile-time — the side-table is populated
560    // at bytecode-emission time only; never runtime.
561    value_call_returns: Option<
562        &dyn Fn(shape_ast::ast::span::Span) -> Option<shape_value::v2::ConcreteType>,
563    >,
564) -> Vec<shape_value::v2::ConcreteType> {
565    use crate::mir::types::{MirConstant, Operand, StatementKind};
566    let n = mir.num_locals as usize;
567    let mut concrete_types: Vec<shape_value::v2::ConcreteType> =
568        vec![shape_value::v2::ConcreteType::Void; n];
569
570    // Pre-pass: build a per-slot scalar `ConcreteType` map by inspecting
571    // `Assign(Place::Local(slot), Rvalue::Use(Constant(_)))` statements.
572    // The MIR lowering for array element operands runs each expression
573    // through `lower_expr_to_temp` / `lower_expr_as_moved_operand` —
574    // which for literal sub-expressions emits an `Assign(temp, Use(Const))`
575    // first, then the array's `ArrayStore` moves the temp. We probe the
576    // pre-assigned temps' constant kinds so the array-element inference
577    // can prove the typed-array kind from the indirect operand shape.
578    let mut slot_scalar_kind: Vec<Option<shape_value::v2::ConcreteType>> =
579        vec![None; n];
580    for block in mir.iter_blocks() {
581        for stmt in &block.statements {
582            if let StatementKind::Assign(
583                crate::mir::types::Place::Local(dst),
584                crate::mir::types::Rvalue::Use(Operand::Constant(c)),
585            ) = &stmt.kind
586            {
587                let idx = dst.0 as usize;
588                if idx < n {
589                    slot_scalar_kind[idx] = match c {
590                        MirConstant::Int(_) => Some(shape_value::v2::ConcreteType::I64),
591                        MirConstant::Float(_) => Some(shape_value::v2::ConcreteType::F64),
592                        MirConstant::Bool(_) => Some(shape_value::v2::ConcreteType::Bool),
593                        _ => None,
594                    };
595                }
596            }
597        }
598    }
599
600    // ADR-006 §2.7.5 — Phase 3 cluster-0 Round 13 T1' gap 1 closure.
601    //
602    // Parallel `struct_names: Vec<Option<String>>` track populated from
603    // `mir.local_struct_type_names` (MIR-lowering output for
604    // `Expr::StructLiteral { type_name, .. }` sites). Read at the
605    // Call-terminator stamping pass below for `MirConstant::Method` arms,
606    // and propagated through slot moves in the second pass alongside
607    // `concrete_types`.
608    //
609    // `None` per slot is "no struct identity known" — the slot wasn't
610    // produced by a struct-literal expression. Per §2.7.7 #9 no
611    // fabricated default; the trait-method classifier surfaces unstamped
612    // (returns the slot's existing `ConcreteType::Void` placeholder).
613    let mut struct_names: Vec<Option<String>> = vec![None; n];
614    for (slot, name) in &mir.local_struct_type_names {
615        let idx = slot.0 as usize;
616        if idx < n {
617            struct_names[idx] = Some(name.clone());
618        }
619    }
620
621    // ADR-006 §2.7.5 stamp-at-compile-time — V3-S6e-jit-specialized-vec-
622    // map-aggregate-classify (Phase 3 cluster-0+1 Wave 3, 2026-05-16;
623    // V3-S6 multi-session chain checkpoint-final).
624    //
625    // Empty-typed-array-literal slot stamping pass. The MIR lowering's
626    // `mir/lowering/helpers.rs::emit_container_store_if_needed` short-
627    // circuits for `ContainerStoreKind::Array` with empty operands (line
628    // 128-130), so the ArrayStore walker below (`StatementKind::ArrayStore`
629    // arm) has no operand source to infer the element kind for empty
630    // literal initializations like `let mut result = []`. Without this
631    // pass `concrete_types[result_slot]` stays `Void`, the JIT-MIR v2-
632    // fast-path at `mir_compiler/statements.rs::v2_typed_array_elem_kind`
633    // returns `None`, the kind-blind Aggregate fallback fires, and the
634    // function fails to JIT-compile per Route A `W11-jit-new-array`
635    // SURFACE.
636    //
637    // The producer at `mir/lowering/stmt.rs::lower_var_decl` populates
638    // `mir.local_typed_array_element_types` for `let mut <name>: Array<C>
639    // = []` bindings when `C` is a `concrete_type_from_annotation`-
640    // resolvable element ConcreteType. V3-S6a's
641    // `synthesize_empty_array_result_annotation` writes the `Array<C>`
642    // annotation onto the specialized `Vec.map<U>` / `Vec.filter<U>`
643    // body's `let mut result = []` after generic substitution
644    // concretizes the return type; this consumer reads that AST→MIR-
645    // threaded element type at the conduit producer layer.
646    //
647    // Runs BEFORE the slot-move propagation pass below so that subsequent
648    // `Use(Move|Copy)` chains from the var-binding slot to a user-visible
649    // slot propagate the Array(elem) stamp correctly.
650    //
651    // No tag-bit decode, no Bool-default, no fabricated default — when
652    // the binding has no `Array<C>` annotation (legacy `let mut result =
653    // []` without explicit type), no entry exists in the map and the
654    // slot stays `Void` per §2.7.5.1 / §2.7.7 #9 (the JIT surfaces-and-
655    // stops honestly at the Aggregate site, the original W11-jit-new-
656    // array architectural-gap signal).
657    for (slot, elem) in &mir.local_typed_array_element_types {
658        let idx = slot.0 as usize;
659        if idx < n
660            && matches!(
661                concrete_types[idx],
662                shape_value::v2::ConcreteType::Void
663            )
664        {
665            concrete_types[idx] =
666                shape_value::v2::ConcreteType::Array(Box::new(elem.clone()));
667        }
668    }
669
670    // ADR-006 §2.7.5 stamp-at-compile-time — R5c-2-β-γ (c) jit-narrow-wrap.
671    //
672    // Narrow-integer declared-width stamping pass. The pre-pass above
673    // classifies every `MirConstant::Int(_)` as `ConcreteType::I64` —
674    // the bare-int-literal MIR carrier is width-blind. The MIR lowering's
675    // `lower_var_decl` records the declared `i8`/`i16`/`i32`/`u8`/`u16`/
676    // `u32` annotation against the binding slot in
677    // `mir.local_declared_scalar_types`. This pass stamps the proven
678    // narrow width onto `concrete_types[slot]` (overriding the constant-
679    // derived `I64`) and `slot_scalar_kind[slot]` so the slot-move
680    // propagation pass below carries it to downstream binop-temp slots.
681    //
682    // The JIT consumer (`mir_compiler/types::infer_slot_kinds_with_
683    // concrete`) projects the narrow `ConcreteType` to the matching
684    // `NativeKind` and declares the slot at native width; the binop
685    // codegen then lowers `iadd`/`isub`/`imul` at that width so overflow
686    // wraps two's-complement — matching the bytecode VM's `AddI32`/
687    // `AddTyped` truncating opcodes. Runs BEFORE slot-move propagation so
688    // a `let c: i32 = a + b` result temp inherits the width from `a`/`b`.
689    for (slot, decl_ct) in &mir.local_declared_scalar_types {
690        let idx = slot.0 as usize;
691        if idx >= n {
692            continue;
693        }
694        if matches!(
695            concrete_types[idx],
696            shape_value::v2::ConcreteType::Void | shape_value::v2::ConcreteType::I64
697        ) {
698            concrete_types[idx] = decl_ct.clone();
699        }
700        slot_scalar_kind[idx] = Some(decl_ct.clone());
701    }
702
703    // W11-jit-new-array (2026-05-17): build a slot-copy-source map from
704    // `Assign(dst, Use(Move|Copy|MoveExplicit(Place::Local(src))))`
705    // statements. Used by `infer_array_elem_from_operands` below to
706    // chase the ultimate source of a `Move(temp)` operand back to a
707    // user-visible slot whose `ConcreteType` was stamped by its own
708    // `ArrayStore`. The single-pass producer order classifies `a`'s
709    // ArrayStore before `b`'s spread `ArrayStore` consumes a Move(temp)
710    // operand where the temp received `Copy(a)`; without this chain
711    // walk the spread operand's element type is `None` and the result
712    // slot stays `Void`. The slot-move propagation pass at line ~1070
713    // runs *after* the ArrayStore-stamping pass, so it cannot help
714    // here — we have to chase the chain in the first pass directly.
715    //
716    // Per ADR-006 §2.7.5 stamp-at-compile-time, the chain walk is
717    // structural (MIR-shape only); no runtime tag-bit decode, no
718    // fabricated default. When the chain terminates at a non-Array
719    // slot the helper returns `None` and the caller falls through to
720    // the normal heterogeneous-operand path.
721    let mut copy_source: Vec<Option<u16>> = vec![None; n];
722    for block in mir.iter_blocks() {
723        for stmt in &block.statements {
724            if let StatementKind::Assign(
725                crate::mir::types::Place::Local(dst),
726                crate::mir::types::Rvalue::Use(
727                    Operand::Move(crate::mir::types::Place::Local(src))
728                    | Operand::Copy(crate::mir::types::Place::Local(src))
729                    | Operand::MoveExplicit(crate::mir::types::Place::Local(src)),
730                ),
731            ) = &stmt.kind
732            {
733                let di = dst.0 as usize;
734                if di < n {
735                    copy_source[di] = Some(src.0);
736                }
737            }
738        }
739    }
740
741    // First pass: stamp slots from container-store statements. These are
742    // the kind-source statements emitted by the MIR lowering for
743    // struct/enum/array literal construction.
744    for block in mir.iter_blocks() {
745        for stmt in &block.statements {
746            match &stmt.kind {
747                StatementKind::ObjectStore { container_slot, .. } => {
748                    let idx = container_slot.0 as usize;
749                    if idx < n {
750                        // MIR-shape inference has no source-level type name
751                        // here — placeholder struct id (v0.3 WS-6).
752                        concrete_types[idx] =
753                            shape_value::v2::ConcreteType::placeholder_struct(
754                                shape_value::v2::concrete_type::StructLayoutId(0),
755                            );
756                    }
757                }
758                StatementKind::EnumStore {
759                    container_slot,
760                    operands,
761                    variant_name,
762                } => {
763                    let idx = container_slot.0 as usize;
764                    if idx < n {
765                        // W12-jit-result-option-trinity (Phase 3 cluster-0
766                        // Round 7A, 2026-05-12): for the trinity variant
767                        // family (Ok / Err / Some / None) we stamp the
768                        // concrete `Result(_,_)` / `Option(_)` shape
769                        // rather than the generic `Enum(EnumLayoutId(0))`
770                        // placeholder. The Ok/Err arm pair share a
771                        // `Result(ok_inner, err_inner)`; since the
772                        // single-EnumStore site only knows one arm at a
773                        // time, we set the other arm to `Void` and rely
774                        // on bidirectional flow (e.g. both `Ok` and `Err`
775                        // arms returned from the same function share the
776                        // function's return type via the resolver pass
777                        // below). For `let r = Ok(5)` literal sites, the
778                        // Err arm stays `Void` — the JIT's
779                        // `infer_enum_payload_kind` reads the right arm
780                        // for the variant being extracted, so a `Void`
781                        // arm only matters when extracting that arm
782                        // (which doesn't happen in the load-bearing
783                        // smokes — `let r = Ok(5)` is only matched with
784                        // its known Ok payload kind).
785                        //
786                        // ADR-006 §2.7.17 producer-site classification:
787                        // the variant_name IS the proof of which carrier
788                        // shape, and the operand kind IS the inner type.
789                        // No Bool-default — when we can't classify the
790                        // operand kind we fall back to the generic
791                        // `Enum(0)` placeholder (legacy behavior).
792                        let variant_tag = variant_name
793                            .as_deref()
794                            .and_then(crate::mir::types::VariantTag::from_name);
795                        let inner_concrete = if operands.len() == 1 {
796                            operand_concrete_type(&operands[0], &slot_scalar_kind)
797                        } else {
798                            None
799                        };
800                        match (variant_tag, inner_concrete) {
801                            (
802                                Some(crate::mir::types::VariantTag::Ok),
803                                Some(inner),
804                            ) => {
805                                concrete_types[idx] = shape_value::v2::ConcreteType::Result(
806                                    Box::new(inner),
807                                    Box::new(shape_value::v2::ConcreteType::Void),
808                                );
809                            }
810                            (
811                                Some(crate::mir::types::VariantTag::Err),
812                                Some(inner),
813                            ) => {
814                                concrete_types[idx] = shape_value::v2::ConcreteType::Result(
815                                    Box::new(shape_value::v2::ConcreteType::Void),
816                                    Box::new(inner),
817                                );
818                            }
819                            (
820                                Some(crate::mir::types::VariantTag::Some_),
821                                Some(inner),
822                            ) => {
823                                concrete_types[idx] = shape_value::v2::ConcreteType::Option(
824                                    Box::new(inner),
825                                );
826                            }
827                            (Some(crate::mir::types::VariantTag::None_), _) => {
828                                // None has no payload — inner is unknown.
829                                concrete_types[idx] = shape_value::v2::ConcreteType::Option(
830                                    Box::new(shape_value::v2::ConcreteType::Void),
831                                );
832                            }
833                            _ => {
834                                // Legacy / user-defined enum variant. MIR-shape
835                                // inference has no source-level type name here
836                                // — placeholder enum id (v0.3 WS-6).
837                                concrete_types[idx] =
838                                    shape_value::v2::ConcreteType::placeholder_enum(
839                                        shape_value::v2::concrete_type::EnumLayoutId(0),
840                                    );
841                            }
842                        }
843                    }
844                }
845                StatementKind::ArrayStore {
846                    container_slot,
847                    operands,
848                } => {
849                    let idx = container_slot.0 as usize;
850                    if idx < n {
851                        // Infer scalar element type from operand kinds. The
852                        // MIR lowering's `lower_array_expr` runs each element
853                        // through `lower_expr_as_moved_operand`, which for
854                        // literal sub-expressions emits an `Assign(temp,
855                        // Use(Const))` followed by an ArrayStore that moves
856                        // the temp. We resolve through the per-slot scalar
857                        // map (`slot_scalar_kind`) built in the pre-pass to
858                        // recover the underlying kind across that temp hop.
859                        //
860                        // W11-jit-new-array (2026-05-17): also probe the
861                        // partially-stamped `concrete_types` vector (via the
862                        // `copy_source` chain) so that operand slots already
863                        // classified as `Array<T>` (e.g. the `...a` spread
864                        // source in `[0, ...a, 4]`) contribute their element
865                        // type T. The bytecode pipeline lowers spreads
866                        // through `compile_array_with_spread`; the MIR
867                        // mirrors that by inlining the spread source as a
868                        // single Move operand to a scratch temp before the
869                        // ArrayStore consumes it. Per ADR-006 §2.7.5 the
870                        // source slot's `ConcreteType` IS the proof of
871                        // element kind; no fabrication, no decode.
872                        if let Some(elem) = infer_array_elem_from_operands(
873                            operands,
874                            &slot_scalar_kind,
875                            &concrete_types,
876                            &copy_source,
877                        ) {
878                            concrete_types[idx] =
879                                shape_value::v2::ConcreteType::Array(Box::new(elem));
880                        }
881                        // Heterogeneous / non-literal operands → leave
882                        // `Void`, the JIT surfaces-and-stops honestly at
883                        // the Aggregate site rather than papering over.
884                    }
885                }
886                _ => {}
887            }
888        }
889    }
890
891    // Call-terminator destination pass: stamp slots from the callee's
892    // declared return type. ADR-006 §2.7.5 producing-site classification
893    // applied to `TerminatorKind::Call` — the callee's return annotation
894    // (resolved via the `callee_returns` closure) IS the proof source
895    // for the destination slot's `ConcreteType`.
896    //
897    // `let r = divide(10, 2)` lowers to:
898    //   bb_call: ... terminator = Call(divide, [10, 2], dst=r_slot, next=bb_after)
899    //
900    // Without this pass the destination slot stays `Void` (no
901    // `*Store` statement, no `Use(Move)` propagation seeds it),
902    // and the downstream `match r { Ok(v) => ..., Err(e) => ... }`
903    // codegen sees `r` as kind-unclassified and falls through to the
904    // legacy decoder path. This pass stamps `Result(I64, String)` for
905    // `divide`'s return; the match consumer can then dispatch on
906    // `is_typed_object_slot(r_slot)`.
907    //
908    // The pass runs BEFORE the slot-move propagation pass so that any
909    // `Use(Move|Copy)` chain from the Call destination to a user-visible
910    // slot propagates the Call-stamped kind correctly.
911    //
912    // No tag-bit decode, no Bool-default — when the resolver returns
913    // `None` the slot stays `Void` per §2.7.5.1.
914    if let Some(resolver) = callee_returns {
915        use crate::mir::types::TerminatorKind;
916        for block in mir.iter_blocks() {
917            if let TerminatorKind::Call {
918                func, destination, ..
919            } = &block.terminator.kind
920            {
921                if let Operand::Constant(MirConstant::Function(name)) = func {
922                    if let crate::mir::types::Place::Local(dst) = destination {
923                        let idx = dst.0 as usize;
924                        if idx < n
925                            && matches!(
926                                concrete_types[idx],
927                                shape_value::v2::ConcreteType::Void
928                            )
929                        {
930                            if let Some(ct) = resolver(name.as_str()) {
931                                if !matches!(
932                                    ct,
933                                    shape_value::v2::ConcreteType::Void
934                                ) {
935                                    concrete_types[idx] = ct;
936                                }
937                            }
938                        }
939                    }
940                }
941            }
942        }
943    }
944
945    // ADR-006 §2.7.5 — V3-S6b-jit-method-monomorph-conduit (Phase 3
946    // cluster-0 Wave 3 Stabilize Round 2, 2026-05-15; supervisor 2026-
947    // 05-15 PATH α RATIFIED). Monomorphized-method-call return-kind
948    // classification at `MirConstant::Method` Call-terminator destinations.
949    //
950    // For `arr.map(|x| x*2).sum()` chains, the bytecode compiler's
951    // `try_monomorphize_method_call` specializes `Vec.map<int, int>` (via
952    // V3-S6a's closure-return-typed generic resolver extension) and
953    // records the call-site → specialized FunctionId mapping in
954    // `BytecodeProgram.monomorphized_method_call_sites`. The MIR layer,
955    // however, still carries `MirConstant::Method("map")` (MIR is built
956    // before bytecode-level specialization), so the
957    // `MirConstant::Function(name)` resolver above does not fire on
958    // these call-terminators.
959    //
960    // This pass closes the gap: when the `Terminator.span` matches a
961    // side-table entry (composite key `(span, current_function)` keyed
962    // off the producer's closed-over `current_function` value), the
963    // destination slot's ConcreteType is lifted from
964    // `function_return_concrete_types[specialized_idx]`. Downstream
965    // method calls on the destination (e.g. `.sum()` on the
966    // `.map()` result) then read the stamped
967    // `concrete_types[receiver_slot] = Array(I64)` and the JIT-side
968    // `parametric_method_return_kind_from_receiver` arm
969    // `("sum"|..., ConcreteType::Array(elem))` fires trivially.
970    //
971    // No tag-bit decode, no Bool-default, no fabricated default — when
972    // the resolver returns `None` (no side-table entry, or callee return
973    // type is Void), the slot stays `Void` per §2.7.5.1. The
974    // monomorph-method resolver is consulted at COMPILE TIME (the
975    // §2.7.5 stamp-at-compile-time discipline; never runtime).
976    //
977    // Runs BEFORE the slot-move propagation pass so that subsequent
978    // `Assign(doubled, Use(Move(temp_map_result)))` propagation
979    // carries the stamped `Array(I64)` from the temp to the user-
980    // visible binding slot. Without this ordering the move-propagation
981    // would observe `concrete_types[temp_map_result] = Void` and leave
982    // `doubled` unstamped, defeating the V3-S6b conduit.
983    // ADR-006 §2.7.5 stamp-at-compile-time — V3-S6d-jit-method-monomorph-
984    // classify (supervisor 2026-05-15 PATH α-prime complementary-fix
985    // RATIFIED).
986    //
987    // COMPLEMENTARY to V3-S6c routing at terminators.rs:176. V3-S6c
988    // routing addresses the .map() EXECUTION-PATH consumer (direct
989    // FuncRef call to specialized Vec.map::* bypasses jit_call_method
990    // trampoline + handle_int_map ckpt3_surface SIGSEGV class). V3-S6d
991    // stamping addresses the .sum() DESTINATION-KIND CLASSIFICATION
992    // consumer (the v2-fast-path at terminators.rs:104-135 reads
993    // concrete_types[doubled_slot]; without classification, fast-path
994    // doesn't activate, downstream print operand kind is None, JIT
995    // SURFACE-graceful).
996    //
997    // V3-S6d stamping is SAFE post-V3-S6c routing: the V3-S6b dual-
998    // consumer SIGSEGV was a TEMPORAL problem (stamping fired before
999    // routing addressed malformed-bits root cause). V3-S6c eliminates
1000    // the temporal dependency by emitting direct FuncRef calls that
1001    // return correct raw `*const TypedArray<i64>` bits per V3-S5 strict-
1002    // typing; V3-S6d stamping then classifies doubled_slot Array(I64);
1003    // v2-fast-path consumer reads BOTH correct bits AND correct kind →
1004    // jit_v2_array_sum_i64(arr_ptr) succeeds.
1005    //
1006    // For TerminatorKind::Call { func: MirConstant::Method(_), destination:
1007    // Place::Local(dst), .. }, consult the monomorph_method_returns
1008    // resolver (which queries monomorphized_method_call_sites side-table
1009    // populated at try_monomorphize_method_call success). Stamp the
1010    // destination slot's ConcreteType from the specialized function's
1011    // return ConcreteType.
1012    if let Some(monomorph_resolver) = monomorph_method_returns {
1013        use crate::mir::types::TerminatorKind;
1014        for block in mir.iter_blocks() {
1015            if let TerminatorKind::Call {
1016                func, destination, ..
1017            } = &block.terminator.kind
1018            {
1019                if let Operand::Constant(MirConstant::Method(_)) = func {
1020                    if let crate::mir::types::Place::Local(dst) = destination {
1021                        let idx = dst.0 as usize;
1022                        if idx < n
1023                            && matches!(
1024                                concrete_types[idx],
1025                                shape_value::v2::ConcreteType::Void
1026                            )
1027                        {
1028                            let span = block.terminator.span;
1029                            if let Some(ct) = monomorph_resolver(span) {
1030                                if !matches!(
1031                                    ct,
1032                                    shape_value::v2::ConcreteType::Void
1033                                ) {
1034                                    concrete_types[idx] = ct;
1035                                }
1036                            }
1037                        }
1038                    }
1039                }
1040            }
1041        }
1042    }
1043
1044    // cluster-2-cw-IB-class-b (2026-05-16, supervisor R3 binding-ratified):
1045    // value-call Call-terminator destination stamping pass. ADR-006
1046    // §2.7.5 stamp-at-compile-time discipline applied to closure-bound
1047    // value-call sites.
1048    //
1049    // For `let f = |inner| inner.sum(); print(f(xs))` (Class B fixture
1050    // per inventory §B.2), the bytecode-emission layer at
1051    // `compile_expr_function_call::compile_expr_function_call`
1052    // (`crates/shape-vm/src/compiler/expressions/function_calls.rs:498-619`)
1053    // recognises the value-call site, re-runs closure-body return-type
1054    // inference with the caller-context arg types injected as
1055    // typed-array param hints, and inserts the result into
1056    // `BytecodeProgram.value_call_return_concrete_types[(call_span,
1057    // current_function)]`. This pass consumes that side-table when the
1058    // Call-terminator's `func` reads from a local slot (the closure-
1059    // bound slot), stamping the destination's ConcreteType.
1060    //
1061    // Without this stamping the closure-call result slot stays `Void`
1062    // through the conduit, the JIT-MIR `slot_kinds[dst]` stays `None`,
1063    // and the downstream `print(f(xs))` Call-terminator at
1064    // `terminators.rs:447-744` falls into the `_` SURFACE arm because
1065    // its operand's NativeKind cannot be projected.
1066    //
1067    // Runs AFTER the MirConstant::Function / Method / monomorph passes
1068    // and BEFORE the slot-move propagation pass so that subsequent
1069    // `Use(Move|Copy)` chains from the Call destination to a user-
1070    // visible slot propagate the stamped ConcreteType correctly.
1071    //
1072    // No tag-bit decode, no Bool-default, no fabricated default — when
1073    // the resolver returns `None` (no side-table entry, or the closure-
1074    // body inference could not classify the return) the slot stays
1075    // `Void` per §2.7.5.1. The closure-bound `func` operand
1076    // recognition is structural (Operand::Copy/Move/MoveExplicit of
1077    // Place::Local) — the resolver itself owns the question of whether
1078    // any given local-slot's value is actually a closure (the side-
1079    // table is empty for non-closure local bindings).
1080    if let Some(value_call_resolver) = value_call_returns {
1081        use crate::mir::types::TerminatorKind;
1082        for block in mir.iter_blocks() {
1083            if let TerminatorKind::Call {
1084                func, destination, ..
1085            } = &block.terminator.kind
1086            {
1087                // Value-call shape: callee operand reads from a local
1088                // slot. Constant callees (Function/Method) are handled
1089                // by the resolver passes above.
1090                let is_local_callee = matches!(
1091                    func,
1092                    Operand::Copy(crate::mir::types::Place::Local(_))
1093                        | Operand::Move(crate::mir::types::Place::Local(_))
1094                        | Operand::MoveExplicit(crate::mir::types::Place::Local(_))
1095                );
1096                if !is_local_callee {
1097                    continue;
1098                }
1099                if let crate::mir::types::Place::Local(dst) = destination {
1100                    let idx = dst.0 as usize;
1101                    if idx < n
1102                        && matches!(
1103                            concrete_types[idx],
1104                            shape_value::v2::ConcreteType::Void
1105                        )
1106                    {
1107                        let span = block.terminator.span;
1108                        if let Some(ct) = value_call_resolver(span) {
1109                            if !matches!(
1110                                ct,
1111                                shape_value::v2::ConcreteType::Void
1112                            ) {
1113                                concrete_types[idx] = ct;
1114                            }
1115                        }
1116                    }
1117                }
1118            }
1119        }
1120    }
1121
1122    // Second pass: propagate Struct/Enum/Array through simple slot moves.
1123    // The MIR lowering for `let p = Point{...}` first builds the
1124    // TypedObject in a scratch temp, then `Assign(p_slot, Use(Move temp))`
1125    // moves it into the user-visible slot. Without this propagation the
1126    // user slot's `ConcreteType` would remain `Void` and downstream JIT
1127    // consumers (field access codegen, drop release) couldn't pick the
1128    // right path.
1129    //
1130    // The propagation is structural: only `Rvalue::Use(Move|Copy local)`
1131    // assignments propagate. Anything else (Aggregate, BinaryOp, Borrow,
1132    // Clone) does not — those don't preserve the source slot's
1133    // ConcreteType.
1134    //
1135    // Iterate to a fixed point — slot chains can be longer than 1.
1136    //
1137    // T1' gap 1 closure: the same propagation discipline applies to
1138    // `struct_names` — `let t = X {}; let u = t; u.name()` flows the
1139    // struct identity from `t`'s construction slot to `u`'s binding
1140    // slot via the same Move/Copy chain. The trait-method classifier
1141    // below runs AFTER this propagation so it sees the receiver slot's
1142    // propagated struct type name.
1143    use crate::mir::types::{Place, Rvalue};
1144    let mut changed = true;
1145    let mut iter_budget = n.max(1) * 4; // hard cap against pathological MIR
1146    while changed && iter_budget > 0 {
1147        changed = false;
1148        iter_budget -= 1;
1149        for block in mir.iter_blocks() {
1150            for stmt in &block.statements {
1151                if let StatementKind::Assign(
1152                    Place::Local(dst),
1153                    Rvalue::Use(
1154                        Operand::Move(Place::Local(src))
1155                        | Operand::Copy(Place::Local(src))
1156                        | Operand::MoveExplicit(Place::Local(src)),
1157                    ),
1158                ) = &stmt.kind
1159                {
1160                    let (di, si) = (dst.0 as usize, src.0 as usize);
1161                    if di < n && si < n {
1162                        let src_ct = concrete_types[si].clone();
1163                        if !matches!(src_ct, shape_value::v2::ConcreteType::Void)
1164                            && concrete_types[di] != src_ct
1165                        {
1166                            concrete_types[di] = src_ct;
1167                            changed = true;
1168                        }
1169                        if let Some(src_name) = struct_names[si].clone() {
1170                            if struct_names[di].as_deref() != Some(src_name.as_str()) {
1171                                struct_names[di] = Some(src_name);
1172                                changed = true;
1173                            }
1174                        }
1175                    }
1176                }
1177            }
1178        }
1179    }
1180
1181    // ADR-006 §2.7.5 — Phase 3 cluster-0 Round 13 T1' commit 2: trait-
1182    // method dispatch return-kind classification at Call-terminator
1183    // destination.
1184    //
1185    // For `t.method()` lowered as `TerminatorKind::Call { func:
1186    // MirConstant::Method(name), args, destination, .. }`, look up the
1187    // receiver slot's struct type name in `struct_names` (propagated
1188    // through slot moves above), then resolve the trait method's
1189    // declared return ConcreteType via the `method_returns` resolver
1190    // (which chains `find_default_trait_impl_for_type_method(type_name,
1191    // method_name)` through `function_return_concrete_types`). When the
1192    // chain resolves cleanly, stamp the destination slot.
1193    //
1194    // No tag-bit decode, no Bool-default, no fabricated default — when
1195    // any link in the chain returns `None` the slot stays `Void` per
1196    // §2.7.5.1. Multi-trait `method()` conflicts (two traits declare
1197    // `method()` with different return ConcreteTypes for the same
1198    // receiver type) surface as `None` per §5 of the audit; the
1199    // downstream JIT consumer reaches its surface-and-stop posture.
1200    //
1201    // Runs AFTER the slot-move propagation pass so receiver slots
1202    // that flow through `let u = t` chains carry the propagated struct
1203    // identity.
1204    if let Some(method_resolver) = method_returns {
1205        use crate::mir::types::TerminatorKind;
1206        for block in mir.iter_blocks() {
1207            if let TerminatorKind::Call {
1208                func,
1209                args,
1210                destination,
1211                ..
1212            } = &block.terminator.kind
1213            {
1214                if let Operand::Constant(MirConstant::Method(method_name)) = func {
1215                    if let crate::mir::types::Place::Local(dst) = destination {
1216                        let idx = dst.0 as usize;
1217                        if idx < n
1218                            && matches!(
1219                                concrete_types[idx],
1220                                shape_value::v2::ConcreteType::Void
1221                            )
1222                        {
1223                            // Receiver is `args[0]` per the MIR lowering
1224                            // convention at `mir/lowering/expr.rs::Expr::MethodCall`
1225                            // line ~1856 (`arg_ops.push(receiver_op);`).
1226                            let receiver_slot = match args.first() {
1227                                Some(Operand::Move(crate::mir::types::Place::Local(s)))
1228                                | Some(Operand::Copy(crate::mir::types::Place::Local(s)))
1229                                | Some(Operand::MoveExplicit(crate::mir::types::Place::Local(s))) => s.0 as usize,
1230                                _ => continue,
1231                            };
1232                            if receiver_slot >= n {
1233                                continue;
1234                            }
1235                            if let Some(type_name) = struct_names[receiver_slot].clone() {
1236                                if let Some(ct) =
1237                                    method_resolver(&type_name, method_name.as_str())
1238                                {
1239                                    if !matches!(
1240                                        ct,
1241                                        shape_value::v2::ConcreteType::Void
1242                                    ) {
1243                                        concrete_types[idx] = ct;
1244                                        continue;
1245                                    }
1246                                }
1247                            }
1248                            // V3-S6a resolver-extension follow-up:
1249                            // parametric method classification on
1250                            // parametric receivers. When the trait-impl
1251                            // resolver doesn't apply (the receiver is a
1252                            // parametric container like `Array<T>`, not a
1253                            // user-defined struct), consult the
1254                            // receiver's `ConcreteType` directly. This
1255                            // mirrors `parametric_method_return_kind_from_
1256                            // receiver` (in shape-jit's `mir_compiler/
1257                            // types.rs`) but produces a `ConcreteType`
1258                            // rather than just a `NativeKind` — feeding
1259                            // the conduit's full kind-source side-table.
1260                            //
1261                            // Scope: methods on `Array<T>` whose return
1262                            // shape is fully derivable from T at MIR
1263                            // time. `.sum()` / `.mean()` / `.min()` /
1264                            // `.max()` / `.get(i)` → T (element scalar).
1265                            // `.map(...)` / `.filter(...)` are NOT
1266                            // covered here — they go through the bytecode
1267                            // compiler's specialization path
1268                            // (`MirConstant::Function("Vec.map::*")` at
1269                            // bytecode level), and the conduit's
1270                            // `Function`-arm resolver consults
1271                            // `function_return_concrete_types` for the
1272                            // specialized return type.
1273                            let receiver_ct = &concrete_types[receiver_slot];
1274                            let inferred = match (
1275                                method_name.as_str(),
1276                                receiver_ct,
1277                            ) {
1278                                // V3-S6a resolver-extension follow-up:
1279                                // Array element-typed accessors. The
1280                                // VM-side `array_basic.rs` /
1281                                // `typed_array_methods.rs` PHF entries
1282                                // return `KindedSlot::from_<elem>(...)`
1283                                // per receiver-element kind — same
1284                                // shape as the JIT-side
1285                                // `parametric_method_return_kind_from_receiver`
1286                                // arm in `crates/shape-jit/src/mir_compiler/types.rs`.
1287                                // Ported here so the conduit's
1288                                // `concrete_types[]` side-table also
1289                                // tracks element kind through
1290                                // method-chain receivers (which the
1291                                // pre-V3-S6a conduit only handled for
1292                                // user-defined struct receivers via the
1293                                // `struct_names` lookup above).
1294                                (
1295                                    "sum" | "mean" | "min" | "max",
1296                                    shape_value::v2::ConcreteType::Array(elem),
1297                                ) => Some((**elem).clone()),
1298                                (
1299                                    "get",
1300                                    shape_value::v2::ConcreteType::Array(elem),
1301                                ) => Some((**elem).clone()),
1302                                _ => None,
1303                            };
1304                            if let Some(ct) = inferred {
1305                                if !matches!(ct, shape_value::v2::ConcreteType::Void) {
1306                                    concrete_types[idx] = ct;
1307                                }
1308                            }
1309                        }
1310                    }
1311                }
1312            }
1313        }
1314    }
1315
1316    concrete_types
1317}
1318
1319/// Infer a homogeneous element `ConcreteType` from a slice of MIR
1320/// operands. Resolves `Move(Local(slot))` operands through
1321/// `slot_scalar_kind`, the pre-pass map of `Assign(slot, Use(Const))`
1322/// scalar kinds.
1323///
1324/// Returns `None` for empty / heterogeneous / unresolved operand vectors
1325/// — the caller leaves the slot's `ConcreteType` as `Void` (the "no
1326/// information" sentinel per §2.7.5.1, NOT a Bool-default fallback).
1327/// Project an `Operand` to a scalar `ConcreteType` via the same
1328/// constant + pre-pass scalar-slot lookup that
1329/// `infer_array_elem_from_operands` uses. Used by the EnumStore Result/
1330/// Option inference to stamp the inner type of `Ok(v)` / `Err(e)` /
1331/// `Some(x)` from the operand at MIR-emission time per ADR-006 §2.7.5.
1332///
1333/// Returns `None` when the operand isn't a scalar — caller stamps the
1334/// arm as `Void` (and downstream resolver-based propagation may refine
1335/// it). NOT a Bool-default fallback per §2.7.7 #9.
1336fn operand_concrete_type(
1337    operand: &crate::mir::types::Operand,
1338    slot_scalar_kind: &[Option<shape_value::v2::ConcreteType>],
1339) -> Option<shape_value::v2::ConcreteType> {
1340    use crate::mir::types::{MirConstant, Operand, Place};
1341    match operand {
1342        Operand::Constant(MirConstant::Int(_)) => Some(shape_value::v2::ConcreteType::I64),
1343        Operand::Constant(MirConstant::Float(_)) => Some(shape_value::v2::ConcreteType::F64),
1344        Operand::Constant(MirConstant::Bool(_)) => Some(shape_value::v2::ConcreteType::Bool),
1345        Operand::Constant(MirConstant::Str(_))
1346        | Operand::Constant(MirConstant::StringId(_)) => {
1347            Some(shape_value::v2::ConcreteType::String)
1348        }
1349        Operand::Move(Place::Local(s))
1350        | Operand::Copy(Place::Local(s))
1351        | Operand::MoveExplicit(Place::Local(s)) => {
1352            let idx = s.0 as usize;
1353            slot_scalar_kind.get(idx).and_then(|k| k.clone())
1354        }
1355        _ => None,
1356    }
1357}
1358
1359/// W11-jit-new-array (2026-05-17): walk the `Use(Move|Copy|MoveExplicit)`
1360/// chain starting at `slot` and return the deepest source whose
1361/// `concrete_types` entry is non-Void. Used by
1362/// `infer_array_elem_from_operands` to recover the spread source's
1363/// `Array<T>` classification across the `Spread` expression's
1364/// intervening copy temp. Bounded by `copy_source.len()` to defend
1365/// against pathological chains.
1366fn resolve_copy_chain(
1367    slot: u16,
1368    copy_source: &[Option<u16>],
1369    concrete_types: &[shape_value::v2::ConcreteType],
1370) -> Option<shape_value::v2::ConcreteType> {
1371    let mut cur = slot as usize;
1372    let mut budget = copy_source.len();
1373    loop {
1374        if cur >= concrete_types.len() {
1375            return None;
1376        }
1377        let ct = &concrete_types[cur];
1378        if !matches!(ct, shape_value::v2::ConcreteType::Void) {
1379            return Some(ct.clone());
1380        }
1381        let next = copy_source.get(cur).copied().flatten();
1382        match next {
1383            Some(n) if (n as usize) != cur && budget > 0 => {
1384                cur = n as usize;
1385                budget -= 1;
1386            }
1387            _ => return None,
1388        }
1389    }
1390}
1391
1392fn infer_array_elem_from_operands(
1393    operands: &[crate::mir::types::Operand],
1394    slot_scalar_kind: &[Option<shape_value::v2::ConcreteType>],
1395    concrete_types: &[shape_value::v2::ConcreteType],
1396    copy_source: &[Option<u16>],
1397) -> Option<shape_value::v2::ConcreteType> {
1398    use crate::mir::types::{MirConstant, Operand, Place};
1399    if operands.is_empty() {
1400        return None;
1401    }
1402    let mut elem: Option<shape_value::v2::ConcreteType> = None;
1403    for op in operands {
1404        let here = match op {
1405            Operand::Constant(MirConstant::Int(_)) => {
1406                Some(shape_value::v2::ConcreteType::I64)
1407            }
1408            Operand::Constant(MirConstant::Float(_)) => {
1409                Some(shape_value::v2::ConcreteType::F64)
1410            }
1411            Operand::Constant(MirConstant::Bool(_)) => {
1412                Some(shape_value::v2::ConcreteType::Bool)
1413            }
1414            // Move/Copy of a local slot — consult two kind sources in
1415            // order:
1416            //
1417            //   1. The pre-pass scalar map (`slot_scalar_kind`). If the
1418            //      source slot was filled by an `Assign(s, Use(Constant(
1419            //      k)))` then we can recover the scalar kind across the
1420            //      `lower_expr_as_moved_operand` temp hop. This covers
1421            //      the canonical `[1, 2, 3]` literal-element shape.
1422            //
1423            //   2. W11-jit-new-array (2026-05-17): the partially-stamped
1424            //      `concrete_types` vector reached via `resolve_copy_
1425            //      chain`. When the operand IS (or copy-chains back to)
1426            //      an `Array<T>` slot — e.g. the `...a` spread source
1427            //      in `[0, ...a, 4]` — we project its element type T
1428            //      and treat the spread operand as contributing T per
1429            //      the bytecode pipeline's `compile_array_with_spread`
1430            //      semantic. Per ADR-006 §2.7.5 the source slot's
1431            //      `ConcreteType` IS the proof of element kind; no
1432            //      fabrication.
1433            Operand::Move(Place::Local(s))
1434            | Operand::Copy(Place::Local(s))
1435            | Operand::MoveExplicit(Place::Local(s)) => {
1436                let idx = s.0 as usize;
1437                let scalar_kind = slot_scalar_kind.get(idx).and_then(|k| k.clone());
1438                if scalar_kind.is_some() {
1439                    scalar_kind
1440                } else if let Some(shape_value::v2::ConcreteType::Array(inner)) =
1441                    resolve_copy_chain(s.0, copy_source, concrete_types).as_ref()
1442                {
1443                    Some((**inner).clone())
1444                } else {
1445                    None
1446                }
1447            }
1448            // Other operand shapes (Constants we don't handle, projections,
1449            // etc.) → surface-and-stop (no fast-path fabrication).
1450            _ => return None,
1451        };
1452        match (&elem, here) {
1453            (None, Some(k)) => elem = Some(k),
1454            (Some(a), Some(ref b)) if a == b => {}
1455            _ => return None, // heterogeneous → surface-and-stop
1456        }
1457    }
1458    elem
1459}
1460
1461// ── Phase V3.6: `emit_dynamic_*` helpers deleted ─────────────────────────
1462//
1463// V3.2-V3.5 migrated every arithmetic, comparison, and pattern-`Eq` emission
1464// site onto the unified `emit_binary_op` shim. After V3.5 (`c7af294`) all
1465// 12 former `emit_dynamic_{add,sub,mul,div,mod,pow,eq,neq,gt,lt,gte,lte}`
1466// helpers had zero callers. V3.6 deletes them: the shim's Dynamic-fallback
1467// branch now owns every `*Dynamic` opcode emission in the compiler.
1468//
1469// See V3.6 commit body for the residual-emission audit — every remaining
1470// Dynamic emission is a class-(a) polyglot / class-(b) comptime / untyped-
1471// identifier fallback documented on `emit_binary_op` below.
1472
1473/// Phase V3.1: typed-vs-dynamic binary-op dispatch kind.
1474///
1475/// Generalizes `NumericType` with a few non-numeric categories the binary-op
1476/// emission path cares about (string, bool) plus an `Unknown` sentinel so
1477/// callers can thread through `Option<NumericType>` or richer inference
1478/// results. V3.1 only wires in the Numeric and String cases today — Bool is
1479/// reserved for the future when an `EqBool` typed opcode lands (no such
1480/// opcode exists in V3, so bool equality falls back to `EqDynamic`).
1481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1482pub(in crate::compiler) enum BinOperandKind {
1483    /// Resolved to a specific numeric type (Int, Number, Decimal, IntWidth).
1484    Numeric(NumericType),
1485    /// Resolved to `string` or `char` — triggers `StringConcatTyped` for `+`.
1486    String,
1487    /// Resolved to `bool`. V3.1: no typed bool-eq opcode; always falls back
1488    /// to the Dynamic opcode family for now. Reserved for future typed bool
1489    /// opcodes without a caller migration.
1490    Bool,
1491    /// Type is not known at compile time — emit the Dynamic fallback.
1492    Unknown,
1493}
1494
1495impl BinOperandKind {
1496    /// Build a `BinOperandKind` from an `Option<NumericType>` — the most
1497    /// common shape used by existing binary-op emission sites.
1498    pub(in crate::compiler) fn from_numeric(nt: Option<NumericType>) -> Self {
1499        match nt {
1500            Some(n) => BinOperandKind::Numeric(n),
1501            None => BinOperandKind::Unknown,
1502        }
1503    }
1504}
1505
1506/// Phase V3.1 shim: pick the typed opcode for a (BinaryOp, NumericType,
1507/// NumericType) triple when both operands resolve to the same numeric
1508/// category, else return `None` (caller falls back to the Dynamic opcode).
1509///
1510/// Mirrors the numeric-only subset of
1511/// `compiler::expressions::numeric_ops::typed_opcode_for` so that
1512/// `emit_binary_op` can live in the helpers module without a module-visibility
1513/// dance. The two tables stay in lockstep; the expressions-side helper
1514/// additionally handles `IntWidth` coercions which V3.1 intentionally does
1515/// NOT generalize (width handling stays in `emit_numeric_binary_with_coercion`
1516/// until V3.2+ tranches migrate those callers).
1517fn typed_numeric_opcode(op: shape_ast::ast::BinaryOp, nt: NumericType) -> Option<OpCode> {
1518    use shape_ast::ast::BinaryOp;
1519    // V3.1 shim: handle only scalar Int/Number/Decimal paths. IntWidth is
1520    // left to the existing width-aware emission path in the expressions
1521    // module — migrating those sites is V3.3+ work.
1522    let matched = match (op, nt) {
1523        // Arithmetic — Int
1524        (BinaryOp::Add, NumericType::Int) => OpCode::AddInt,
1525        (BinaryOp::Sub, NumericType::Int) => OpCode::SubInt,
1526        (BinaryOp::Mul, NumericType::Int) => OpCode::MulInt,
1527        (BinaryOp::Div, NumericType::Int) => OpCode::DivInt,
1528        (BinaryOp::Mod, NumericType::Int) => OpCode::ModInt,
1529        (BinaryOp::Pow, NumericType::Int) => OpCode::PowInt,
1530        // Arithmetic — Number (f64)
1531        (BinaryOp::Add, NumericType::Number) => OpCode::AddNumber,
1532        (BinaryOp::Sub, NumericType::Number) => OpCode::SubNumber,
1533        (BinaryOp::Mul, NumericType::Number) => OpCode::MulNumber,
1534        (BinaryOp::Div, NumericType::Number) => OpCode::DivNumber,
1535        (BinaryOp::Mod, NumericType::Number) => OpCode::ModNumber,
1536        (BinaryOp::Pow, NumericType::Number) => OpCode::PowNumber,
1537        // Arithmetic — Decimal (no ModDecimal/PowDecimal opcodes)
1538        (BinaryOp::Add, NumericType::Decimal) => OpCode::AddDecimal,
1539        (BinaryOp::Sub, NumericType::Decimal) => OpCode::SubDecimal,
1540        (BinaryOp::Mul, NumericType::Decimal) => OpCode::MulDecimal,
1541        (BinaryOp::Div, NumericType::Decimal) => OpCode::DivDecimal,
1542        // Comparison — Int
1543        (BinaryOp::Greater, NumericType::Int) => OpCode::GtInt,
1544        (BinaryOp::Less, NumericType::Int) => OpCode::LtInt,
1545        (BinaryOp::GreaterEq, NumericType::Int) => OpCode::GteInt,
1546        (BinaryOp::LessEq, NumericType::Int) => OpCode::LteInt,
1547        (BinaryOp::Equal, NumericType::Int) => OpCode::EqInt,
1548        (BinaryOp::NotEqual, NumericType::Int) => OpCode::NeqInt,
1549        // Comparison — Number
1550        (BinaryOp::Greater, NumericType::Number) => OpCode::GtNumber,
1551        (BinaryOp::Less, NumericType::Number) => OpCode::LtNumber,
1552        (BinaryOp::GreaterEq, NumericType::Number) => OpCode::GteNumber,
1553        (BinaryOp::LessEq, NumericType::Number) => OpCode::LteNumber,
1554        (BinaryOp::Equal, NumericType::Number) => OpCode::EqNumber,
1555        (BinaryOp::NotEqual, NumericType::Number) => OpCode::NeqNumber,
1556        // Comparison — Decimal (no Gt/Lt/Neq typed opcodes; fall back)
1557        (BinaryOp::Equal, NumericType::Decimal) => OpCode::EqDecimal,
1558        _ => return None,
1559    };
1560    Some(matched)
1561}
1562
1563/// Predicate: is `op` an arithmetic or comparison operator that
1564/// `emit_binary_op` is responsible for emitting?
1565///
1566/// Returns `false` for And/Or (short-circuit), bitwise (separate path),
1567/// NullCoalesce/ErrorContext/Pipe/Fuzzy* (dedicated emission paths).
1568///
1569/// Strict-typing sweep (Phase 2): the `*Dynamic` opcode family was deleted,
1570/// so this helper just selects which BinaryOps the typed-emission shim
1571/// handles. Previously this was `dynamic_opcode_for` returning the
1572/// `*Dynamic` opcode; the same set of ops is the shim's responsibility.
1573fn is_arith_or_cmp_op(op: shape_ast::ast::BinaryOp) -> bool {
1574    use shape_ast::ast::BinaryOp;
1575    matches!(
1576        op,
1577        BinaryOp::Add
1578            | BinaryOp::Sub
1579            | BinaryOp::Mul
1580            | BinaryOp::Div
1581            | BinaryOp::Mod
1582            | BinaryOp::Pow
1583            | BinaryOp::Greater
1584            | BinaryOp::Less
1585            | BinaryOp::GreaterEq
1586            | BinaryOp::LessEq
1587            | BinaryOp::Equal
1588            | BinaryOp::NotEqual
1589    )
1590}
1591
1592/// Phase V3.1: unified typed-vs-dynamic binary-op emission shim.
1593///
1594/// Given a `BinaryOp` plus inferred operand kinds, emits the best-matching
1595/// bytecode opcode:
1596///
1597///   * Both operands `Numeric(nt)` with matching `nt` AND a typed opcode
1598///     exists for `(op, nt)` → emit the typed opcode (`AddInt`, `MulNumber`,
1599///     `EqDecimal`, …).
1600///   * Both operands `String` AND `op == Add` → emit `StringConcatTyped`
1601///     (matches the existing string-concat short-circuit in `compile_expr_binary_op`).
1602///   * Otherwise (mismatched kinds, `Unknown`, `Bool`, or no typed opcode) →
1603///     emit the `Dynamic`-family opcode (`AddDynamic`, `EqDynamic`, …).
1604///
1605/// Returns:
1606///   * `Ok(true)` when an opcode was emitted (either typed or dynamic).
1607///   * `Ok(false)` when `op` is NOT one this shim handles (And/Or/BitOps/
1608///     NullCoalesce/ErrorContext/Pipe/Fuzzy*) — the caller is expected to
1609///     route those ops through their dedicated emission paths.
1610///
1611/// Phase V3.2-V3.5 migrated all 48 binary-op emission sites onto this shim;
1612/// V3.6 deleted the legacy per-op `emit_dynamic_*` helpers (zero callers).
1613/// This function is now the **sole** path through which the compiler emits
1614/// any arithmetic / comparison opcode — typed or Dynamic.
1615///
1616/// ── Residual Dynamic emission (post-V3) ─────────────────────────────────
1617/// Every `*Dynamic` opcode this shim still emits is reserved for one of:
1618///
1619///   (a) **polyglot boundary** — an operand originated in an `extern C fn`,
1620///       inline-python/typescript block, or an untyped FFI call where the
1621///       compiler cannot statically prove a type.
1622///   (b) **comptime / generic scope** — an untyped function parameter or
1623///       generic stdlib code operating on values whose concrete type is
1624///       only known at invocation.
1625///
1626/// Class-(c) "inference bug" residuals — where the type COULD have been
1627/// resolved but wasn't — were audited in V3.6 and are documented as None:
1628/// the three call-sites in `compiler/expressions/binary_ops.rs`
1629/// (`emit_generic_via_helper`, `compile_typed_equality`'s fallback,
1630/// and the `BinaryOp::Add` coerced-or-no-plan branch) each sit strictly
1631/// AFTER a typed-emission attempt has already declined, so operand kinds
1632/// are passed as `BinOperandKind::Unknown` by construction.
1633///
1634/// See V3.6 commit body for the full audit.
1635///
1636/// Side effects: on emit (typed or dynamic), resets
1637/// `last_expr_schema` / `last_expr_type_info` / `last_expr_numeric_type`.
1638/// Typed-numeric emissions additionally restore
1639/// `last_expr_numeric_type = Some(nt)` for arithmetic so downstream
1640/// numeric-propagation logic keeps working. Comparisons always clear the
1641/// numeric hint because the result is a `bool`.
1642pub(in crate::compiler) fn emit_binary_op(
1643    compiler: &mut BytecodeCompiler,
1644    op: shape_ast::ast::BinaryOp,
1645    lhs: BinOperandKind,
1646    rhs: BinOperandKind,
1647) -> Result<bool> {
1648    use shape_ast::ast::BinaryOp;
1649
1650    // Non-arithmetic / non-comparison ops are not the shim's responsibility.
1651    // And/Or short-circuit, bitwise ops emit dedicated opcodes, NullCoalesce
1652    // has its own jump sequence, etc.
1653    if !is_arith_or_cmp_op(op) {
1654        return Ok(false);
1655    }
1656
1657    // Priority 1: both operands are proven same numeric type — emit typed.
1658    if let (BinOperandKind::Numeric(lnt), BinOperandKind::Numeric(rnt)) = (lhs, rhs) {
1659        if lnt == rnt {
1660            if let Some(typed) = typed_numeric_opcode(op, lnt) {
1661                compiler.emit(Instruction::simple(typed));
1662                compiler.last_expr_schema = None;
1663                compiler.last_expr_type_info = None;
1664                // Arithmetic preserves the numeric kind; comparison collapses to bool.
1665                compiler.last_expr_numeric_type = match op {
1666                    BinaryOp::Add
1667                    | BinaryOp::Sub
1668                    | BinaryOp::Mul
1669                    | BinaryOp::Div
1670                    | BinaryOp::Mod
1671                    | BinaryOp::Pow => Some(lnt),
1672                    _ => None,
1673                };
1674                return Ok(true);
1675            }
1676        }
1677    }
1678
1679    // Priority 2: both operands are String and op is Add — emit StringConcatTyped.
1680    // Matches the short-circuit already wired into `compile_expr_binary_op`
1681    // for the proven-strings case.
1682    if matches!(
1683        (lhs, rhs, op),
1684        (
1685            BinOperandKind::String,
1686            BinOperandKind::String,
1687            BinaryOp::Add
1688        )
1689    ) {
1690        compiler.emit(Instruction::simple(OpCode::StringConcatTyped));
1691        compiler.last_expr_schema = None;
1692        compiler.last_expr_type_info = None;
1693        compiler.last_expr_numeric_type = None;
1694        return Ok(true);
1695    }
1696
1697    // Strict-typing sweep (Phase 1+2): the `*Dynamic` opcode family was
1698    // deleted. When the shim cannot pick a typed opcode it returns
1699    // `Ok(false)` so the caller can route to a typed-error or a dedicated
1700    // emission path (operator-trait dispatch, intrinsic retarget, etc.).
1701    Ok(false)
1702}
1703
1704/// Extract the core error message from a ShapeError, stripping redundant
1705/// "Type error:", "Runtime error:", "Compile error:", etc. prefixes that
1706/// thiserror's Display impl adds.  This prevents nested comptime errors
1707/// from accumulating multiple prefixes like
1708/// "Runtime error: Comptime block evaluation failed: Runtime error: …".
1709pub(crate) fn strip_error_prefix(e: &ShapeError) -> String {
1710    let msg = e.to_string();
1711    // Known prefixes added by thiserror Display
1712    const PREFIXES: &[&str] = &[
1713        "Runtime error: ",
1714        "Type error: ",
1715        "Semantic error: ",
1716        "Parse error: ",
1717        "VM error: ",
1718        "Lexical error: ",
1719    ];
1720    let mut s = msg.as_str();
1721    // Strip at most 3 layers of prefix to handle deep nesting
1722    for _ in 0..3 {
1723        let mut stripped = false;
1724        for prefix in PREFIXES {
1725            if let Some(rest) = s.strip_prefix(prefix) {
1726                s = rest;
1727                stripped = true;
1728                break;
1729            }
1730        }
1731        // Also strip the comptime wrapping messages themselves
1732        const COMPTIME_PREFIXES: &[&str] = &[
1733            "Comptime block evaluation failed: ",
1734            "Comptime handler execution failed: ",
1735            "Comptime block directive processing failed: ",
1736        ];
1737        for prefix in COMPTIME_PREFIXES {
1738            if let Some(rest) = s.strip_prefix(prefix) {
1739                s = rest;
1740                stripped = true;
1741                break;
1742            }
1743        }
1744        if !stripped {
1745            break;
1746        }
1747    }
1748    s.to_string()
1749}
1750
1751impl BytecodeCompiler {
1752    /// Resolve a ConcreteType type_tag from compiler state for the receiver.
1753    /// Returns 0xFF when the type cannot be determined.
1754    pub(crate) fn resolve_type_tag(
1755        numeric_type: Option<crate::type_tracking::NumericType>,
1756        type_info: &Option<crate::type_tracking::VariableTypeInfo>,
1757    ) -> u8 {
1758        use crate::type_tracking::NumericType;
1759        // Priority 1: numeric type (most precise)
1760        if let Some(nt) = numeric_type {
1761            return match nt {
1762                NumericType::Number => 0,      // F64
1763                NumericType::Int => 1,         // I64
1764                NumericType::IntWidth(_) => 1, // I64 (treat all int widths as I64 for dispatch)
1765                NumericType::Decimal => 22,    // Decimal
1766            };
1767        }
1768        // Priority 2: type_info type_name
1769        if let Some(info) = type_info {
1770            if let Some(ref name) = info.type_name {
1771                return match name.as_str() {
1772                    "number" | "Number" => 0,  // F64
1773                    "int" | "Int" => 1,        // I64
1774                    "bool" | "Bool" => 9,      // Bool
1775                    "string" | "String" => 10, // String
1776                    "DateTime" => 25,          // DateTime
1777                    _ => {
1778                        // Check for collection types
1779                        if name.starts_with("Array") || name.starts_with("Vec") {
1780                            12 // Array
1781                        } else if name.starts_with("HashMap") || name.starts_with("Map") {
1782                            13 // HashMap
1783                        } else if name.starts_with("Set") {
1784                            14 // Set (mapped to Option tag — we'll refine)
1785                        } else {
1786                            0xFF
1787                        }
1788                    }
1789                };
1790            }
1791            // Priority 3: kind-based inference
1792            use crate::type_tracking::VariableKind;
1793            match &info.kind {
1794                VariableKind::Table { .. } => return 11, // Struct-like (DataTable dispatch)
1795                _ => {}
1796            }
1797        }
1798        0xFF // Unknown
1799    }
1800
1801    fn scalar_type_name_from_numeric(numeric_type: NumericType) -> &'static str {
1802        match numeric_type {
1803            NumericType::Int | NumericType::IntWidth(_) => "int",
1804            NumericType::Number => "number",
1805            NumericType::Decimal => "decimal",
1806        }
1807    }
1808
1809    fn array_type_name_from_numeric(numeric_type: NumericType) -> &'static str {
1810        match numeric_type {
1811            NumericType::Int | NumericType::IntWidth(_) => "Vec<int>",
1812            NumericType::Number => "Vec<number>",
1813            NumericType::Decimal => "Vec<decimal>",
1814        }
1815    }
1816
1817    fn is_array_type_name(type_name: Option<&str>) -> bool {
1818        matches!(type_name, Some(name) if name.starts_with("Vec<") && name.ends_with('>'))
1819    }
1820
1821    /// R5.3B: return `true` when `type_name` is one of the temporal display
1822    /// names the compiler uses for DateTime arithmetic retargeting. Used by
1823    /// `propagate_assignment_type_to_slot` to keep the local/binding tracker
1824    /// populated for let-locals bound to temporal literals.
1825    fn is_temporal_type_name(type_name: Option<&str>) -> bool {
1826        matches!(
1827            type_name,
1828            Some("DateTime") | Some("Duration") | Some("TimeSpan")
1829        )
1830    }
1831
1832    /// Convert a source annotation to a tracked type name when we have a
1833    /// canonical runtime representation for it.
1834    /// Strict-typing-sweep: whether a type-name string identifies a
1835    /// concrete primitive scalar that the typed-op dispatch paths trust.
1836    /// Used to drop the `param_locals` guard once we've successfully
1837    /// inferred a primitive type for an unannotated parameter.
1838    pub(super) fn tracker_type_name_is_primitive(name: &str) -> bool {
1839        matches!(
1840            name,
1841            "int" | "i8" | "i16" | "i32" | "i64"
1842            | "u8" | "u16" | "u32" | "u64"
1843            | "number" | "f32" | "f64"
1844            | "bool" | "string" | "decimal" | "bigint"
1845            | "DateTime" | "Duration" | "TimeSpan"
1846        )
1847    }
1848
1849    pub(super) fn tracked_type_name_from_annotation(type_ann: &TypeAnnotation) -> Option<String> {
1850        match type_ann {
1851            TypeAnnotation::Basic(name) => Some(name.clone()),
1852            TypeAnnotation::Reference(name) => Some(name.to_string()),
1853            TypeAnnotation::Array(inner) => Some(format!("Vec<{}>", inner.to_type_string())),
1854            // Keep the canonical Vec<T> naming even if a Generic slips through.
1855            TypeAnnotation::Generic { name, args } if name == "Vec" && args.len() == 1 => {
1856                Some(format!("Vec<{}>", args[0].to_type_string()))
1857            }
1858            TypeAnnotation::Generic { name, args } if name == "Mat" && args.len() == 1 => {
1859                Some(format!("Mat<{}>", args[0].to_type_string()))
1860            }
1861            // Track Option/Result wrapper types so conversion lifting can
1862            // detect them (even though generic args are lost in the tracker).
1863            TypeAnnotation::Generic { name, .. } if name == "Option" || name == "Result" => {
1864                Some(name.to_lowercase())
1865            }
1866            _ => None,
1867        }
1868    }
1869
1870    /// Resolve a type name through the module scope stack and imports.
1871    ///
1872    /// If the name is already directly known (in struct_types, type_aliases, etc.),
1873    /// returns it as-is. Otherwise, tries prefixing with each module scope from
1874    /// innermost to outermost, then checks imported names to find a match.
1875    pub(super) fn resolve_type_name(&self, name: &str) -> String {
1876        // Already qualified or directly found
1877        if name.contains("::") || self.is_type_known_direct(name) {
1878            return name.to_string();
1879        }
1880        // Try module scope prefixes (innermost to outermost)
1881        for scope in self.module_scope_stack.iter().rev() {
1882            let qualified = format!("{}::{}", scope, name);
1883            if self.is_type_known_direct(&qualified) {
1884                return qualified;
1885            }
1886        }
1887        // Check imported names (from `from ... use { Name }` imports)
1888        if let Some(imported) = self.imported_names.get(name) {
1889            // When module_path is set (graph-compiled dependency), prefer
1890            // module-qualified name. This prevents accidental binding to an
1891            // unrelated local/bare type of the same name.
1892            if !imported.module_path.is_empty() {
1893                let qualified = format!("{}::{}", imported.module_path, imported.original_name);
1894                if self.is_type_known_direct(&qualified) {
1895                    return qualified;
1896                }
1897            }
1898            // Fall back to bare original name (legacy imports without module_path)
1899            if self.is_type_known_direct(&imported.original_name) {
1900                return imported.original_name.clone();
1901            }
1902        }
1903        // Try namespace module prefixes (from `use module` imports)
1904        for ns in &self.module_namespace_bindings {
1905            let qualified = format!("{}::{}", ns, name);
1906            if self.is_type_known_direct(&qualified) {
1907                return qualified;
1908            }
1909            // Try canonical path for graph-compiled modules
1910            if let Some(canonical) = self.graph_namespace_map.get(ns) {
1911                let cq = format!("{}::{}", canonical, name);
1912                if self.is_type_known_direct(&cq) {
1913                    return cq;
1914                }
1915            }
1916        }
1917        // Return as-is (may be a forward reference or builtin)
1918        name.to_string()
1919    }
1920
1921    /// Direct type lookup without scope resolution
1922    fn is_type_known_direct(&self, name: &str) -> bool {
1923        self.struct_types.contains_key(name)
1924            || self.type_aliases.contains_key(name)
1925            || self.type_inference.env.lookup_type_alias(name).is_some()
1926            || self.type_inference.env.get_enum(name).is_some()
1927            || self.type_inference.env.lookup_trait(name).is_some()
1928            || self.type_tracker.schema_registry().get(name).is_some()
1929    }
1930
1931    /// Resolve a trait name to its canonical form for definition lookup.
1932    ///
1933    /// Returns `(canonical_name, basename)` where `canonical_name` is used for
1934    /// `trait_defs` lookup and `basename` is used for dispatch registration
1935    /// (runtime dispatch keys are always bare basenames).
1936    pub(super) fn resolve_trait_name(&self, name: &str) -> (String, String) {
1937        let basename = name.rsplit("::").next().unwrap_or(name).to_string();
1938        // Check trait_defs in priority order
1939        if self.trait_defs.contains_key(name) {
1940            return (name.to_string(), basename);
1941        }
1942        for scope in self.module_scope_stack.iter().rev() {
1943            let q = format!("{}::{}", scope, name);
1944            if self.trait_defs.contains_key(&q) {
1945                return (q, basename);
1946            }
1947        }
1948        if let Some(imported) = self.imported_names.get(name) {
1949            if !imported.module_path.is_empty() {
1950                let q = format!("{}::{}", imported.module_path, imported.original_name);
1951                if self.trait_defs.contains_key(&q) {
1952                    return (q, basename);
1953                }
1954            }
1955        }
1956        for ns in &self.module_namespace_bindings {
1957            let q = format!("{}::{}", ns, name);
1958            if self.trait_defs.contains_key(&q) {
1959                return (q, basename);
1960            }
1961            if let Some(canonical) = self.graph_namespace_map.get(ns) {
1962                let cq = format!("{}::{}", canonical, name);
1963                if self.trait_defs.contains_key(&cq) {
1964                    return (cq, basename);
1965                }
1966            }
1967        }
1968        // Fall back to type_inference.env for built-in traits (Into, From, etc.)
1969        // registered bare in mod.rs but not in trait_defs.
1970        if self.type_inference.env.lookup_trait(name).is_some() {
1971            return (name.to_string(), basename);
1972        }
1973        if self.type_inference.env.lookup_trait(&basename).is_some() {
1974            return (basename.clone(), basename);
1975        }
1976        (name.to_string(), basename)
1977    }
1978
1979    /// Mark a local/module binding slot as an array with numeric element type.
1980    ///
1981    /// Used by `x = x.push(value)` in-place mutation lowering so subsequent
1982    /// indexed reads can recover numeric hints.
1983    pub(super) fn mark_slot_as_numeric_array(
1984        &mut self,
1985        slot: u16,
1986        is_local: bool,
1987        numeric_type: NumericType,
1988    ) {
1989        let info =
1990            VariableTypeInfo::named(Self::array_type_name_from_numeric(numeric_type).to_string());
1991        if is_local {
1992            self.type_tracker.set_local_type(slot, info);
1993        } else {
1994            self.type_tracker.set_binding_type(slot, info);
1995        }
1996    }
1997
1998    /// Mark a local/module binding slot as a scalar numeric type.
1999    pub(super) fn mark_slot_as_numeric_scalar(
2000        &mut self,
2001        slot: u16,
2002        is_local: bool,
2003        numeric_type: NumericType,
2004    ) {
2005        let info =
2006            VariableTypeInfo::named(Self::scalar_type_name_from_numeric(numeric_type).to_string());
2007        if is_local {
2008            self.type_tracker.set_local_type(slot, info);
2009        } else {
2010            self.type_tracker.set_binding_type(slot, info);
2011        }
2012    }
2013
2014    /// Seed numeric hints from expression usage in arithmetic contexts.
2015    ///
2016    /// - `x` in numeric arithmetic becomes scalar numeric (`int`/`number`/`decimal`).
2017    /// - `arr[i]` implies `arr` is `Vec<numeric>`.
2018    pub(super) fn seed_numeric_hint_from_expr(
2019        &mut self,
2020        expr: &shape_ast::ast::Expr,
2021        numeric_type: NumericType,
2022    ) {
2023        match expr {
2024            shape_ast::ast::Expr::Identifier(name, _) => {
2025                if let Some(local_idx) = self.resolve_local(name) {
2026                    self.mark_slot_as_numeric_scalar(local_idx, true, numeric_type);
2027                    return;
2028                }
2029                let scoped_name = self
2030                    .resolve_scoped_module_binding_name(name)
2031                    .unwrap_or_else(|| name.to_string());
2032                if let Some(binding_idx) = self.module_bindings.get(&scoped_name).copied() {
2033                    self.mark_slot_as_numeric_scalar(binding_idx, false, numeric_type);
2034                }
2035            }
2036            shape_ast::ast::Expr::IndexAccess {
2037                object,
2038                end_index: None,
2039                ..
2040            } => {
2041                if let shape_ast::ast::Expr::Identifier(name, _) = object.as_ref() {
2042                    if let Some(local_idx) = self.resolve_local(name) {
2043                        self.mark_slot_as_numeric_array(local_idx, true, numeric_type);
2044                        return;
2045                    }
2046                    let scoped_name = self
2047                        .resolve_scoped_module_binding_name(name)
2048                        .unwrap_or_else(|| name.to_string());
2049                    if let Some(binding_idx) = self.module_bindings.get(&scoped_name).copied() {
2050                        self.mark_slot_as_numeric_array(binding_idx, false, numeric_type);
2051                    }
2052                }
2053            }
2054            _ => {}
2055        }
2056    }
2057
2058    fn recover_or_bail_with_null_placeholder(&mut self, err: ShapeError) -> Result<()> {
2059        if self.should_recover_compile_diagnostics() {
2060            self.errors.push(err);
2061            self.emit(Instruction::simple(OpCode::PushNull));
2062            Ok(())
2063        } else {
2064            Err(err)
2065        }
2066    }
2067
2068    pub(super) fn compile_expr_as_value_or_placeholder(
2069        &mut self,
2070        expr: &shape_ast::ast::Expr,
2071    ) -> Result<()> {
2072        match self.compile_expr(expr) {
2073            Ok(()) => Ok(()),
2074            Err(err) => self.recover_or_bail_with_null_placeholder(err),
2075        }
2076    }
2077
2078    /// Emit an instruction and return its index
2079    /// Also records the current source line and file in debug info
2080    pub(super) fn emit(&mut self, instruction: Instruction) -> usize {
2081        let idx = self.program.emit(instruction);
2082        // Record line number and file for this instruction
2083        if self.current_line > 0 {
2084            self.program.debug_info.line_numbers.push((
2085                idx,
2086                self.current_file_id,
2087                self.current_line,
2088            ));
2089        }
2090        idx
2091    }
2092
2093    /// Emit a boolean constant
2094    pub(super) fn emit_bool(&mut self, value: bool) {
2095        let const_idx = self.program.add_constant(Constant::Bool(value));
2096        self.emit(Instruction::new(
2097            OpCode::PushConst,
2098            Some(Operand::Const(const_idx)),
2099        ));
2100    }
2101
2102    /// Emit a unit constant
2103    pub(super) fn emit_unit(&mut self) {
2104        let const_idx = self.program.add_constant(Constant::Unit);
2105        self.emit(Instruction::new(
2106            OpCode::PushConst,
2107            Some(Operand::Const(const_idx)),
2108        ));
2109    }
2110
2111    /// Emit a jump instruction with placeholder offset.
2112    ///
2113    /// When `opcode` is `JumpIfFalse` and the immediately preceding instruction
2114    /// is a typed or trusted comparison (produces a known bool), upgrades to
2115    /// `JumpIfFalseTrusted` which skips `is_truthy()` dispatch.
2116    pub(super) fn emit_jump(&mut self, mut opcode: OpCode, dummy: i32) -> usize {
2117        if opcode == OpCode::JumpIfFalse && self.last_instruction_produces_bool() {
2118            opcode = OpCode::JumpIfFalseTrusted;
2119        }
2120        self.emit(Instruction::new(opcode, Some(Operand::Offset(dummy))))
2121    }
2122
2123    /// Returns true if the last emitted instruction always produces a boolean result.
2124    fn last_instruction_produces_bool(&self) -> bool {
2125        self.program
2126            .instructions
2127            .last()
2128            .map(|instr| {
2129                matches!(
2130                    instr.opcode,
2131                    OpCode::GtInt
2132                        | OpCode::GtNumber
2133                        | OpCode::GtDecimal
2134                        | OpCode::LtInt
2135                        | OpCode::LtNumber
2136                        | OpCode::LtDecimal
2137                        | OpCode::GteInt
2138                        | OpCode::GteNumber
2139                        | OpCode::GteDecimal
2140                        | OpCode::LteInt
2141                        | OpCode::LteNumber
2142                        | OpCode::LteDecimal
2143                        | OpCode::EqInt
2144                        | OpCode::EqNumber
2145                        | OpCode::NeqInt
2146                        | OpCode::NeqNumber
2147                        | OpCode::EqString
2148                        | OpCode::EqDecimal
2149                        | OpCode::IsNull
2150                        | OpCode::Not
2151                )
2152            })
2153            .unwrap_or(false)
2154    }
2155
2156    /// Returns the `StorageHint` whose raw-native transport matches the bits
2157    /// the LAST emitted instruction will leave on top of the stack.
2158    ///
2159    /// Wave E+5 made many arithmetic / comparison / typed-load opcodes push
2160    /// raw native bits (`i64`/`f64`/`bool`) into the kinded VM stack. The
2161    /// pre-strict-typing pipeline ran a deleted host-boundary decoder
2162    /// (`synthesize_value_word_from_raw` — bulldozed per ADR-006 §2.7.7)
2163    /// which decoded stack-top bits per `top_level_frame.return_kind`; the
2164    /// post-strict-typing path declares the kind on the parallel-kind
2165    /// track at push time, so the deleted decoder's mismatch failure mode
2166    /// is structurally impossible.
2167    ///
2168    /// This helper inspects the just-emitted opcode and returns the
2169    /// `StorageHint` it actually produces in raw bits. Callers in the
2170    /// program-return-kind inference path (see `infer_top_level_return_kind`
2171    /// / `populate_program_storage_hints`) gate kind declaration on this so
2172    /// only opcodes that have been flipped to the native transport drive a
2173    /// typed `return_kind`. Polymorphic / not-yet-flipped producers
2174    /// (`GetField`, `GetProp`, `Call`, `MakeTypedObject`, …) return `None`
2175    /// and the program return falls through to `Unknown` (passthrough
2176    /// synthesis), preserving the pre-E+5 contract.
2177    pub(super) fn last_emitted_native_kind(&self) -> Option<StorageHint> {
2178        // Walk back past trailing stack-neutral teardown chatter so the
2179        // gate inspects the actual producer of top-of-stack. The
2180        // post-trailing-expression drop sequence emitted by
2181        // `emit_drops_for_early_exit` and the block / top-level drop
2182        // scope is a series of:
2183        //
2184        //   LoadLocal(slot)            ; push receiver
2185        //   LoadModuleBinding(slot)    ; push receiver (module-binding form)
2186        //   DropCall(...)              ; pop receiver, push 0
2187        //   [DropLocal(slot)]          ; pop, push 0 (rare; ownership-moves on)
2188        //   [DropSharedLocal(slot)]    ; same
2189        //
2190        // None of those touch the actual top-of-stack producer below.
2191        // Walk past the `LoadLocal / LoadLocalTrusted / LoadModuleBinding
2192        // → DropCall*` pair so the producer below is visible.
2193        // `ReturnOwned` is a no-op for inline scalars
2194        // (`op_promote_to_owned` early-returns when the tag isn't
2195        // `TAG_HEAP`); walk past it for primitive-typed returns.
2196        let instrs = &self.program.instructions;
2197        let mut idx = instrs.len();
2198        while idx > 0 {
2199            let prev = &instrs[idx - 1];
2200            match prev.opcode {
2201                OpCode::DropCall
2202                | OpCode::DropCallAsync
2203                | OpCode::DropLocal
2204                | OpCode::DropSharedLocal => {
2205                    idx -= 1;
2206                    if idx > 0
2207                        && matches!(
2208                            instrs[idx - 1].opcode,
2209                            OpCode::LoadLocal
2210                                | OpCode::LoadLocalTrusted
2211                                | OpCode::LoadModuleBinding
2212                        )
2213                    {
2214                        // Step over the receiver-push that the DropCall
2215                        // consumed. The pair is stack-neutral; the
2216                        // producer we want sits before it.
2217                        idx -= 1;
2218                    }
2219                }
2220                OpCode::ReturnOwned => {
2221                    idx -= 1;
2222                }
2223                _ => break,
2224            }
2225        }
2226        if idx == 0 {
2227            return None;
2228        }
2229        let instr = &instrs[idx - 1];
2230        match instr.opcode {
2231            // ===== Raw i64 producers (Wave E+5.3 + Unit B) =====
2232            // Single-path Int arithmetic / bitwise / negation. Both the
2233            // typed `*Int` variants and the dynamic `BitAnd` / `BitOr` /
2234            // etc. variants now push raw native i64 bits per Wave E+5.5
2235            // (`exec_dyn_bit_binary` / `exec_dyn_bit_unary` consume native
2236            // i64 inputs and push native i64 outputs).
2237            OpCode::AddInt
2238            | OpCode::SubInt
2239            | OpCode::MulInt
2240            | OpCode::DivInt
2241            | OpCode::ModInt
2242            | OpCode::PowInt
2243            | OpCode::NegInt
2244            | OpCode::BitAndInt
2245            | OpCode::BitOrInt
2246            | OpCode::BitXorInt
2247            | OpCode::BitShlInt
2248            | OpCode::BitShrInt
2249            | OpCode::BitNotInt
2250            // Dynamic bitwise opcodes: post-Wave-E+5.5 these push native i64
2251            // bits via `exec_dyn_bit_binary` / `exec_dyn_bit_unary`. The
2252            // pre-flip claim that they were "output-tagged" is no longer
2253            // true — they're now byte-identical to the typed `*Int`
2254            // variants on stack, modulo type proof at compile time.
2255            | OpCode::BitAnd
2256            | OpCode::BitOr
2257            | OpCode::BitXor
2258            | OpCode::BitShl
2259            | OpCode::BitShr
2260            | OpCode::BitNot
2261            // Typed local / module-binding load (Wave E+3) — push raw i64.
2262            | OpCode::LoadLocalI64
2263            | OpCode::LoadLocalU64
2264            | OpCode::LoadLocalI32
2265            | OpCode::LoadLocalU32
2266            | OpCode::LoadLocalI16
2267            | OpCode::LoadLocalU16
2268            | OpCode::LoadLocalI8
2269            | OpCode::LoadLocalU8
2270            | OpCode::LoadModuleBindingI64
2271            | OpCode::LoadModuleBindingU64
2272            | OpCode::LoadModuleBindingI32
2273            | OpCode::LoadModuleBindingU32
2274            | OpCode::LoadModuleBindingI16
2275            | OpCode::LoadModuleBindingU16
2276            | OpCode::LoadModuleBindingI8
2277            | OpCode::LoadModuleBindingU8
2278            // Typed array length / map length / string length helpers all
2279            // push raw i64 (see arithmetic mod tests for these).
2280            | OpCode::ArrayLenTyped
2281            | OpCode::MapLenTyped
2282            | OpCode::StringLenTyped
2283            // v2 sized-integer (i32) arithmetic — post-Wave-E+5 the
2284            // `exec_v2_sized_int` handler pushes raw native i64 bits
2285            // (sign-extended from i32 result) onto the kinded VM stack
2286            // via `push_kinded(bits, NativeKind::Int64)`, matching the
2287            // surrounding typed transport for `LoadLocalI32` /
2288            // `PushConst` / `AddInt`. Mirrors the `AddInt`/`SubInt`/…
2289            // family above for the i32 variants.
2290            | OpCode::AddI32
2291            | OpCode::SubI32
2292            | OpCode::MulI32
2293            | OpCode::DivI32
2294            | OpCode::ModI32
2295            // Compact-typed (sub-i64 width-parameterised) arithmetic — post-
2296            // Wave-E+5.5 the `compact_int_*` family in
2297            // `executor/arithmetic/mod.rs:651` pops native i64 inputs and
2298            // pushes raw native i64 bits onto the kinded VM stack via
2299            // `push_kinded(bits, NativeKind::Int64)`, matching the
2300            // surrounding typed transport. `CmpTyped` returns a -1/0/1
2301            // ordinal as native i64 (NOT a bool — see compact_int_cmp). The
2302            // compact-int width truncation happens before push; the deleted
2303            // `synthesize_value_word_from_raw` decoder (ADR-006 §2.7.7) is
2304            // gone — kind declaration on the parallel-kind track at push
2305            // time replaces its sub-i64 sign-extend path.
2306            | OpCode::AddTyped
2307            | OpCode::SubTyped
2308            | OpCode::MulTyped
2309            | OpCode::DivTyped
2310            | OpCode::ModTyped
2311            | OpCode::CmpTyped
2312            // CastWidth pops native i64, truncates per the declared width,
2313            // and pushes native i64 bits — mirrors the producer side of the
2314            // compact-int family.
2315            | OpCode::CastWidth => Some(StorageHint::Int64),
2316
2317            // ===== Raw f64 producers =====
2318            OpCode::AddNumber
2319            | OpCode::SubNumber
2320            | OpCode::MulNumber
2321            | OpCode::DivNumber
2322            | OpCode::ModNumber
2323            | OpCode::PowNumber
2324            | OpCode::NegNumber
2325            | OpCode::LoadLocalF64
2326            | OpCode::LoadModuleBindingF64 => Some(StorageHint::Float64),
2327
2328            // ===== Raw bool producers (Wave E+5.4) =====
2329            OpCode::EqInt
2330            | OpCode::NeqInt
2331            | OpCode::LtInt
2332            | OpCode::LteInt
2333            | OpCode::GtInt
2334            | OpCode::GteInt
2335            | OpCode::EqNumber
2336            | OpCode::NeqNumber
2337            | OpCode::LtNumber
2338            | OpCode::LteNumber
2339            | OpCode::GtNumber
2340            | OpCode::GteNumber
2341            | OpCode::EqString
2342            | OpCode::EqDecimal
2343            | OpCode::LtDecimal
2344            | OpCode::LteDecimal
2345            | OpCode::GtDecimal
2346            | OpCode::GteDecimal
2347            | OpCode::IsNull
2348            | OpCode::Not
2349            | OpCode::LoadLocalBool
2350            | OpCode::LoadModuleBindingBool
2351            // v2 sized-integer (i32) comparisons — post-Wave-E+5 the
2352            // `exec_v2_sized_int` handler pushes raw native bool bits
2353            // (0u64 / 1u64) onto the kinded VM stack via
2354            // `push_kinded(bits, NativeKind::Bool)`. Mirrors the
2355            // `EqInt`/`LtInt`/… family above for the i32 variants.
2356            | OpCode::EqI32
2357            | OpCode::NeqI32
2358            | OpCode::LtI32
2359            | OpCode::LteI32
2360            | OpCode::GtI32
2361            | OpCode::GteI32 => Some(StorageHint::Bool),
2362
2363            // ===== PushConst — depends on the constant kind =====
2364            // Per ADR-006 §2.7.7, every PushConst pushes raw native bits
2365            // with the matching `NativeKind` declared on the parallel-kind
2366            // track at push time; the deleted ValueWord-tagged transport
2367            // (bulldozed in the strict-typing redesign) is gone.
2368            OpCode::PushConst => self.push_const_native_kind(instr),
2369
2370            // ===== LoadLocalTrusted — depends on the slot's typed kind =====
2371            // `op_load_local_trusted` reads the slot's raw bits directly
2372            // (`variables/mod.rs:2520`). When the slot was populated by a
2373            // typed `StoreLocal<Kind>` (E+3 `StoreLocalI64` / `F64` / `Bool`
2374            // / sub-int widths), the bits are native — passthrough lookup
2375            // through the local type tracker recovers the producer kind.
2376            // Polymorphic / Unknown slots fall through to `None`.
2377            OpCode::LoadLocalTrusted => self.load_local_trusted_native_kind(instr),
2378
2379            // ===== LoadModuleBinding — depends on the binding's typed kind =====
2380            // The polymorphic `LoadModuleBinding` opcode pushes the slot's
2381            // raw u64 unchanged onto the kinded VM stack via
2382            // `push_kinded(bits, kind)`. When the host pre-loaded the slot
2383            // with raw native bits (Wave E+5.5's REPL-persistence load
2384            // path uses `load_module_bindings_from_context` to populate a
2385            // typed-primitive slot from `module_binding_storage_hints[idx]`),
2386            // the polymorphic Load pushes those same raw native bits.
2387            // The pre-strict-typing pipeline relied on the deleted
2388            // `synthesize_value_word_from_raw` decoder (ADR-006 §2.7.7) to
2389            // re-tag at the host boundary; the post-strict-typing path
2390            // declares `kind` on the parallel-kind track at push time, so
2391            // the host boundary reads the kind directly. This arm consults
2392            // the binding's tracker entry (which `register_known_binding_type`
2393            // populates for previously-persisted variables) so a top-level
2394            // program ending in `let r = x; r` (where `x` is a persisted int)
2395            // declares `return_kind = Int64`.
2396            OpCode::LoadModuleBinding => self.load_module_binding_native_kind(instr),
2397
2398            // ===== GetFieldTyped — depends on the field type tag =====
2399            // Post-Wave E+5 `push_field_value` (typed_object_ops.rs:90)
2400            // pushes raw native bits for I64/F64/Bool field tags on
2401            // non-heap slots and declares the matching NativeKind on the
2402            // parallel-kind track; heap-typed fields push the typed Arc
2403            // pointer with `NativeKind::Ptr(HeapKind::*)`. We classify the
2404            // typed-int / typed-bool / typed-f64 case here so a top-level
2405            // `obj.x` ending in an int / bool / f64 field correctly
2406            // declares `return_kind`.
2407            OpCode::GetFieldTyped => self.get_field_typed_native_kind(instr),
2408
2409            // ===== Call — propagate the callee's typed-return kind =====
2410            // Wave E+3 `op_return_value_<kind>` handlers route the raw
2411            // u64 bits the callee pushed straight back to the caller's
2412            // stack via `return_value_inner` →
2413            // `push_kinded(bits, return_kind)`. So when the callee body
2414            // ends in `ReturnValueI64`, the caller sees raw native i64
2415            // bits on top of stack after the Call with `Int64` declared
2416            // on the parallel-kind track. We need to surface that as the
2417            // top-level `return_kind` so the host boundary reads the kind
2418            // directly off the parallel-kind track (the deleted
2419            // ValueWord-tagged transport — ADR-006 §2.7.7 — is gone).
2420            OpCode::Call => self.call_native_kind(instr),
2421
2422            // ===== LoadSharedModuleBinding — propagate the binding's
2423            // typed kind (A2-refined / task #105 / residual b) =====
2424            //
2425            // Top-level `var x: int = 0` captured by a closure becomes
2426            // a module binding promoted to `Arc<SharedCell>` via
2427            // `AllocSharedModuleBinding`. The cell's payload is
2428            // whatever the producer pushed at promotion time; post-
2429            // Wave E+5 that payload is raw native bits when the
2430            // binding's declared type is a proven primitive
2431            // (Int / Float / Bool). `op_load_shared_module_binding`
2432            // reads the cell's inner payload and pushes it onto the
2433            // kinded VM stack via `push_kinded(bits, kind)`. So a
2434            // top-level program ending in `n`
2435            // (where `var n: int = 0`) produces raw native i64 bits at
2436            // the eval boundary; this arm surfaces `Int64` on the
2437            // parallel-kind track so the host boundary reads the kind
2438            // directly (the deleted `synthesize_value_word_from_raw`
2439            // passthrough — ADR-006 §2.7.7 — is gone).
2440            // We consult `module_binding_storage_hint`
2441            // — module-binding type-tracker entries survive
2442            // cell-promotion (unlike local slot entries; see task #16),
2443            // so this dispatch is independent of #16.
2444            OpCode::LoadSharedModuleBinding => self.load_shared_module_binding_native_kind(instr),
2445
2446            // ===== DerefLoad — propagate the projected field's typed
2447            // kind (Wave E+5-cleanup / task #92) =====
2448            //
2449            // The typed-field-place path in
2450            // `compile_expr_property_access` emits the sequence
2451            // `MakeRef → MakeFieldRef(TypedField{..,field_type_tag}) →
2452            // StoreLocal(temp) → DerefLoad(temp)`. The just-emitted
2453            // `DerefLoad` does not carry the field tag in its own
2454            // operand (it's `Operand::Local(temp)`), but the matching
2455            // `MakeFieldRef` two instructions back does. Post-Wave-E+5
2456            // `op_deref_load` recognises a `RefProjection::TypedField`
2457            // with int / bool / f64 tag and pushes raw native bits via
2458            // `push_field_value`. Mirror that here so a top-level
2459            // program ending in e.g. `obj.x` (where x is `int`) declares
2460            // `return_kind = Int64` for the host-boundary synthesizer.
2461            OpCode::DerefLoad => self.deref_load_native_kind(),
2462
2463            // ===== GetProp — side-channel lookup (task #108, sister of #92) =====
2464            //
2465            // `op_get_prop`'s producer-flip pushes raw native bits when
2466            // it dispatches against a `HeapValue::TypedObject` whose
2467            // matching schema field has tag I64 / Timestamp / F64 / Bool
2468            // against a non-heap slot (mirrors `push_field_value` in
2469            // `typed_object_ops.rs:90`). `GetProp` is emitted as
2470            // `Instruction::simple` (no operand), so neither
2471            // operand-decode nor a walk-back lookup can recover the
2472            // field tag at host-boundary inspection time — the
2473            // compiler instead records the resolved kind in
2474            // `get_prop_native_kinds` at the emit site in
2475            // `compile_expr_property_access`. This arm consults that
2476            // side-channel by the index of the just-emitted GetProp.
2477            OpCode::GetProp => self.get_prop_native_kind_at(idx - 1),
2478
2479            // Everything else (GetField, NewTypedObject,
2480            // legacy `ReturnValue` (tagged), MakeArray, MakeMap, etc.)
2481            // is polymorphic / not-yet-flipped — return None.
2482            _ => None,
2483        }
2484    }
2485
2486    /// Resolve the raw-native kind of a `Call` instruction by inspecting
2487    /// the callee's compiled body for a typed `ReturnValue<Kind>` opcode.
2488    /// Returns the matching `StorageHint` if all return sites in the
2489    /// callee body use the same typed return kind; `None` otherwise (the
2490    /// legacy polymorphic `ReturnValue` path, or mixed kinds).
2491    fn call_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
2492        let Some(Operand::Function(fid)) = &instr.operand else {
2493            return None;
2494        };
2495        // FunctionId(u16) — look up the compiled function body.
2496        let callee_idx = fid.0 as usize;
2497        let func = self.program.functions.get(callee_idx)?;
2498
2499        // Annotation fast-path. When the callee's `function_defs` entry has
2500        // a primitive return annotation (`-> int` / `-> number` / `-> bool`
2501        // / sub-int width), trust it. The body might still emit legacy
2502        // `ReturnValue` (e.g. `fn apply(f: any, x: int) -> int { return
2503        // f(x) }` where the inner CallValue's typed bits flow through the
2504        // legacy ReturnValue handler unchanged), which the body scan below
2505        // would disqualify as `None` — but the runtime stack contract is
2506        // still raw native bits per the closure's typed `ReturnValueI64`.
2507        // The type system already verified the annotation; the host-boundary
2508        // synthesizer needs the same trust to re-tag the bits correctly.
2509        //
2510        // Foreign function defs (extern, FFI) are checked too.
2511        let return_ann = self
2512            .function_defs
2513            .get(&func.name)
2514            .and_then(|def| def.return_type.as_ref())
2515            .or_else(|| {
2516                self.foreign_function_defs
2517                    .get(&func.name)
2518                    .and_then(|def| def.return_type.as_ref())
2519            });
2520        if let Some(ann) = return_ann {
2521            if let Some(name) = ann.as_type_name_str() {
2522                if let Some(kind) = primitive_type_name_to_storage_hint(name).or_else(|| {
2523                    self.type_aliases
2524                        .get(name)
2525                        .and_then(|aliased| primitive_type_name_to_storage_hint(aliased.as_str()))
2526                }) {
2527                    if matches!(
2528                        kind,
2529                        StorageHint::Int64
2530                            | StorageHint::UInt64
2531                            | StorageHint::Int32
2532                            | StorageHint::UInt32
2533                            | StorageHint::Int16
2534                            | StorageHint::UInt16
2535                            | StorageHint::Int8
2536                            | StorageHint::UInt8
2537                            | StorageHint::Float64
2538                            | StorageHint::Bool
2539                    ) {
2540                        return Some(kind);
2541                    }
2542                }
2543            }
2544        }
2545
2546        let entry = func.entry_point;
2547        let end = entry.checked_add(func.body_length)?;
2548        if end > self.program.instructions.len() {
2549            return None;
2550        }
2551        // Task #106: nested function bodies are physically embedded inside
2552        // the outer's `[entry..end]` instruction span via the jump-over
2553        // emission pattern (see `compiler/functions.rs:990-1005`). Build
2554        // a list of (nested_entry, nested_end) ranges for every other
2555        // function whose body lies strictly within `[entry..end]`, so the
2556        // scanner below can skip them — otherwise a nested closure's
2557        // defensive trailing `ReturnValue` (PushNull + ReturnValue
2558        // fallback after a typed `ReturnValueI64`) trips the disqualifier
2559        // and we incorrectly classify a typed-returning callee as
2560        // polymorphic.
2561        let mut nested_ranges: Vec<(usize, usize)> = Vec::new();
2562        for (other_idx, other) in self.program.functions.iter().enumerate() {
2563            if other_idx == callee_idx {
2564                continue;
2565            }
2566            let o_entry = other.entry_point;
2567            let Some(o_end) = o_entry.checked_add(other.body_length) else {
2568                continue;
2569            };
2570            // Strictly nested inside this callee's span.
2571            if o_entry >= entry && o_end <= end && o_entry > entry {
2572                nested_ranges.push((o_entry, o_end));
2573            }
2574        }
2575        nested_ranges.sort_unstable_by_key(|&(s, _)| s);
2576        // Scan the body for typed `ReturnValue<Kind>` opcodes. If the
2577        // callee uses a single typed-return kind across all return sites,
2578        // declare that kind for the call result. Mixed kinds (e.g. one
2579        // path returns int and another returns Unit) fall through to
2580        // `Unknown`. The legacy untyped `ReturnValue` (0x45) also
2581        // disqualifies — its caller-side transport pre-dates the
2582        // ADR-006 §2.7.7 strict-typing redesign.
2583        let mut found: Option<StorageHint> = None;
2584        let mut pos = entry;
2585        let mut nested_cursor = 0;
2586        while pos < end {
2587            // If `pos` is inside a nested function's range, jump past it.
2588            // `nested_ranges` is sorted; advance the cursor past any range
2589            // we've stepped over.
2590            while nested_cursor < nested_ranges.len()
2591                && nested_ranges[nested_cursor].1 <= pos
2592            {
2593                nested_cursor += 1;
2594            }
2595            if nested_cursor < nested_ranges.len() {
2596                let (n_start, n_end) = nested_ranges[nested_cursor];
2597                if pos >= n_start && pos < n_end {
2598                    pos = n_end;
2599                    continue;
2600                }
2601            }
2602            let instr = &self.program.instructions[pos];
2603            let kind = match instr.opcode {
2604                OpCode::ReturnValueI64
2605                | OpCode::ReturnValueU64
2606                | OpCode::ReturnValueI32
2607                | OpCode::ReturnValueU32
2608                | OpCode::ReturnValueI16
2609                | OpCode::ReturnValueU16
2610                | OpCode::ReturnValueI8
2611                | OpCode::ReturnValueU8 => Some(StorageHint::Int64),
2612                OpCode::ReturnValueF64 => Some(StorageHint::Float64),
2613                OpCode::ReturnValueBool => Some(StorageHint::Bool),
2614                // Legacy / pointer / generic typed-return — disqualify.
2615                // Note: `ReturnOwned` (0x... -> `op_promote_to_owned`) is
2616                // misleadingly named — it's a stack-top promotion helper,
2617                // NOT a return opcode. Don't treat it as a disqualifier.
2618                OpCode::ReturnValue | OpCode::ReturnValuePtr => return None,
2619                _ => {
2620                    pos += 1;
2621                    continue;
2622                }
2623            };
2624            match (found, kind) {
2625                (None, k) => found = k,
2626                (Some(a), Some(b)) if a == b => {}
2627                _ => return None,
2628            }
2629            pos += 1;
2630        }
2631        found
2632    }
2633
2634    /// Resolve the raw-native kind of a `PushConst` instruction by reading
2635    /// the constant pool entry at the operand index. Mirrors the
2636    /// `op_push_const` decision tree (Unit B): Int/UInt push raw i64; Bool
2637    /// pushes raw bool; Number pushes raw f64. Per ADR-006 §2.7.7 every
2638    /// Constant arm declares a `NativeKind` on the parallel-kind track at
2639    /// push time — the deleted ValueWord-tagged transport is gone.
2640    fn push_const_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
2641        let Some(Operand::Const(idx)) = &instr.operand else {
2642            return None;
2643        };
2644        let constant = self.program.constants.get(*idx as usize)?;
2645        match constant {
2646            Constant::Number(_) => Some(StorageHint::Float64),
2647            Constant::Int(i) => {
2648                // ADR-006 §2.7.7: the deleted i48 NaN-box range constants were
2649                // a tag_bits artefact. The post-strict-typing kind tracker
2650                // tags every Int constant as `Int64` regardless of width — the
2651                // typed `PushConst Int` handler stores the full i64.
2652                let _ = i;
2653                Some(StorageHint::Int64)
2654            }
2655            Constant::UInt(u) => {
2656                // ADR-006 §2.7.7: same i48-range deletion as Int above.
2657                let _ = u;
2658                Some(StorageHint::Int64)
2659            }
2660            Constant::Bool(_) => Some(StorageHint::Bool),
2661            _ => None,
2662        }
2663    }
2664
2665    /// Resolve the raw-native kind of a `LoadLocalTrusted` instruction
2666    /// by consulting the slot's type-tracker entry. The trusted handler
2667    /// reads raw bits directly (`variables/mod.rs:2520`) and pushes them
2668    /// onto the kinded VM stack via `push_kinded(bits, kind)`, so the
2669    /// on-stack representation matches the kind that originally
2670    /// populated the slot — typically a typed
2671    /// `StoreLocal<Kind>` from a Wave E+3 producer (PushConst Int /
2672    /// typed arithmetic / typed Load*).
2673    ///
2674    /// Returns the matching hint when the slot's tracked kind is one of
2675    /// the three native-kinded `StorageHint` variants (Int64 / Float64 /
2676    /// Bool); `None` for sub-int widths whose nullable variants are
2677    /// observable at the host boundary, or for polymorphic slots.
2678    /// Resolve the raw-native kind of a polymorphic `LoadModuleBinding`
2679    /// instruction by consulting the binding's type-tracker entry. The
2680    /// polymorphic Load reads `module_bindings[idx]` raw u64; when the
2681    /// persistence load path normalised the slot to raw native bits per
2682    /// the declared `StorageHint` (Wave E+5.5
2683    /// `normalize_persisted_for_slot` in execution.rs), the Load pushes
2684    /// those same raw native bits and declares `kind` on the parallel-
2685    /// kind track at push time so the host boundary reads the kind
2686    /// directly. Returns the matching primitive hint (Int64/Float64/Bool);
2687    /// `None` for polymorphic / nullable / heap-bearing kinds.
2688    fn load_module_binding_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
2689        let Some(Operand::ModuleBinding(idx)) = &instr.operand else {
2690            return None;
2691        };
2692        // Post-§2.7.5.1: `get_module_binding_storage_hint` returns
2693        // `Option<StorageHint>` ("not yet proven" carried in the Option).
2694        // Match through `Some(..)` and forward only the proven primitive
2695        // kinds; `None` (or any non-primitive proven kind) falls through.
2696        match self.type_tracker.get_module_binding_storage_hint(*idx) {
2697            Some(kind @ (StorageHint::Int64 | StorageHint::Float64 | StorageHint::Bool)) => {
2698                Some(kind)
2699            }
2700            _ => None,
2701        }
2702    }
2703
2704    fn load_local_trusted_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
2705        let Some(Operand::Local(idx)) = &instr.operand else {
2706            return None;
2707        };
2708        // Primary path: consult the slot's type-tracker entry. When the
2709        // slot is still in scope (the typical case for typed-fn bodies
2710        // and top-level lets), this returns the proven kind populated at
2711        // emit time.
2712        //
2713        // Post-§2.7.5.1: `get_local_storage_hint` returns
2714        // `Option<StorageHint>` ("not yet proven" lives in the Option).
2715        // The `Some(...)` arm gates on a proven primitive; `None` (or any
2716        // non-primitive) falls through to the recovery path below.
2717        let hint = self.type_tracker.get_local_storage_hint(*idx);
2718        if matches!(
2719            hint,
2720            Some(StorageHint::Int64 | StorageHint::Float64 | StorageHint::Bool)
2721        ) {
2722            return hint;
2723        }
2724        // Fallback: when this `LoadLocalTrusted` was emitted inside a
2725        // `compile_expr_block` whose outer scope has already been popped
2726        // by the time `infer_top_level_return_kind` runs (the type
2727        // tracker scopes track-and-discard alongside `locals`), the slot
2728        // lookup returns `None`. The producer-flip contract for
2729        // `LoadLocalTrusted` is "push the slot's raw bits" — and the
2730        // proof that the bits ARE native-kinded came from the typed
2731        // `StoreLocal<Kind>` emitted earlier in the same block. The
2732        // compiler also propagates the slot's type up through
2733        // `last_expr_numeric_type` / `last_expr_type_info`, which
2734        // SURVIVE scope-pop. Use them to recover the kind here.
2735        if let Some(nt) = self.last_expr_numeric_type {
2736            return Some(match nt {
2737                crate::type_tracking::NumericType::Number => StorageHint::Float64,
2738                crate::type_tracking::NumericType::Int => StorageHint::Int64,
2739                crate::type_tracking::NumericType::IntWidth(_) => StorageHint::Int64,
2740                crate::type_tracking::NumericType::Decimal => return None,
2741            });
2742        }
2743        if let Some(info) = &self.last_expr_type_info {
2744            // `info.storage_hint: Option<StorageHint>` post-§2.7.5.1 —
2745            // match through `Some(..)` and forward only the proven
2746            // primitive kinds.
2747            return match info.storage_hint {
2748                Some(kind @ (StorageHint::Int64 | StorageHint::Float64 | StorageHint::Bool)) => {
2749                    Some(kind)
2750                }
2751                _ => None,
2752            };
2753        }
2754        None
2755    }
2756
2757    /// Resolve the raw-native kind of a `GetFieldTyped` instruction by
2758    /// reading the `field_type_tag` from its operand. Mirrors the post-E+5
2759    /// `push_field_value` decision tree (typed_object_ops.rs:90).
2760    fn get_field_typed_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
2761        use crate::executor::typed_object_ops::{
2762            FIELD_TAG_BOOL, FIELD_TAG_F64, FIELD_TAG_I64, FIELD_TAG_TIMESTAMP,
2763        };
2764        let Some(Operand::TypedField {
2765            field_type_tag, ..
2766        }) = &instr.operand
2767        else {
2768            return None;
2769        };
2770        match *field_type_tag {
2771            FIELD_TAG_I64 | FIELD_TAG_TIMESTAMP => Some(StorageHint::Int64),
2772            FIELD_TAG_F64 => Some(StorageHint::Float64),
2773            FIELD_TAG_BOOL => Some(StorageHint::Bool),
2774            _ => None,
2775        }
2776    }
2777
2778    /// A2-refined task #105 / residual b: resolve the raw-native kind of
2779    /// a `LoadSharedModuleBinding` instruction by consulting the
2780    /// binding's type tracker entry. The cell's inner payload encoding
2781    /// mirrors the binding's declared `StorageHint` post-Wave E+5: a
2782    /// top-level `var n: int = 0` stores raw native i64 in the cell,
2783    /// `var x: number` stores raw f64, `var b: bool` stores raw bool.
2784    /// Returns the matching hint when the binding's type-tracker entry
2785    /// maps to one of the three native-kinded `StorageHint` variants;
2786    /// `None` for unresolved hints (Dynamic, Unknown, sub-i64 widths
2787    /// whose nullable/tagged variant matters at the host boundary,
2788    /// etc.).
2789    ///
2790    /// Independent of #16 (cell-pointer slot type-tracking). Module-
2791    /// binding type-tracker entries survive cell-promotion via
2792    /// `AllocSharedModuleBinding`, unlike local slot entries.
2793    fn load_shared_module_binding_native_kind(&self, instr: &Instruction) -> Option<StorageHint> {
2794        let Some(Operand::ModuleBinding(idx)) = &instr.operand else {
2795            return None;
2796        };
2797        // Post-§2.7.5.1: `get_module_binding_storage_hint` returns
2798        // `Option<StorageHint>`. Match through `Some(..)` and forward
2799        // only the proven primitive kinds.
2800        match self.type_tracker.get_module_binding_storage_hint(*idx) {
2801            Some(kind @ (StorageHint::Int64 | StorageHint::Float64 | StorageHint::Bool)) => {
2802                Some(kind)
2803            }
2804            _ => None,
2805        }
2806    }
2807
2808    /// Wave E+5-cleanup task #92: resolve the raw-native kind of a
2809    /// `DerefLoad` instruction by walking back through the just-emitted
2810    /// instructions to find the matching `MakeFieldRef` and reading its
2811    /// `field_type_tag` operand. The typed-field-place emit pattern
2812    /// (see `compile_expr_property_access`) is:
2813    ///
2814    /// ```text
2815    ///   MakeRef(root)
2816    ///   MakeFieldRef(TypedField{..,field_type_tag})
2817    ///   StoreLocal(temp)
2818    ///   DerefLoad(temp)        // current instruction
2819    /// ```
2820    ///
2821    /// The `MakeFieldRef` is exactly two instructions before the
2822    /// `DerefLoad` in this canonical form, so we look back at
2823    /// `instructions[-3]` and decode its tag. Other `DerefLoad` emit
2824    /// sites (e.g. ref-parameter reads in `identifiers.rs`, raw `&`
2825    /// references) emit `LoadLocal → DerefLoad` or similar without an
2826    /// intervening `MakeFieldRef` — those return `None` here and stay
2827    /// on the polymorphic path, which is correct: the underlying
2828    /// projection is a `Stack` / `ModuleBinding` / `Index` ref whose
2829    /// payload may not be a native-kinded scalar.
2830    ///
2831    /// Mirrors `get_field_typed_native_kind` for the matching
2832    /// `GetFieldTyped` flip already in place.
2833    fn deref_load_native_kind(&self) -> Option<StorageHint> {
2834        use crate::executor::typed_object_ops::{
2835            FIELD_TAG_BOOL, FIELD_TAG_F64, FIELD_TAG_I64, FIELD_TAG_TIMESTAMP,
2836        };
2837        let n = self.program.instructions.len();
2838        if n < 4 {
2839            return None;
2840        }
2841        let make_field_ref = &self.program.instructions[n - 3];
2842        if make_field_ref.opcode != OpCode::MakeFieldRef {
2843            return None;
2844        }
2845        let Some(Operand::TypedField {
2846            field_type_tag, ..
2847        }) = &make_field_ref.operand
2848        else {
2849            return None;
2850        };
2851        match *field_type_tag {
2852            FIELD_TAG_I64 | FIELD_TAG_TIMESTAMP => Some(StorageHint::Int64),
2853            FIELD_TAG_F64 => Some(StorageHint::Float64),
2854            FIELD_TAG_BOOL => Some(StorageHint::Bool),
2855            _ => None,
2856        }
2857    }
2858
2859    /// Wave E+5-cleanup task #108: resolve the raw-native kind of the
2860    /// just-emitted `GetProp` instruction by consulting the
2861    /// `get_prop_native_kinds` side-channel populated at the GetProp
2862    /// emit site in `compile_expr_property_access`. `GetProp` has no
2863    /// operand (`Instruction::simple`) so neither operand-decode nor
2864    /// walk-back recovers the field tag — the compiler must record
2865    /// the resolved kind explicitly when its schema lookup yields a
2866    /// native-scalar field type. Sites that don't record (untyped
2867    /// receivers, heap fields, decimal, …) stay `None` here and the
2868    /// host-boundary synthesizer falls through to passthrough, which
2869    /// is correct because the executor's matching `op_get_prop` flip
2870    /// also leaves those tagged.
2871    fn get_prop_native_kind(&self) -> Option<StorageHint> {
2872        let idx = self.program.instructions.len().checked_sub(1)?;
2873        self.get_prop_native_kinds.get(&idx).copied()
2874    }
2875
2876    /// Variant of [`Self::get_prop_native_kind`] that consults the side-
2877    /// channel at a specific instruction index. Used by
2878    /// [`Self::last_emitted_native_kind`] when its walk-back lands on a
2879    /// `GetProp` that isn't the final instruction.
2880    fn get_prop_native_kind_at(&self, idx: usize) -> Option<StorageHint> {
2881        self.get_prop_native_kinds.get(&idx).copied()
2882    }
2883
2884    /// Wave E+5-cleanup task #108: record the native-kind of a
2885    /// just-emitted `GetProp` instruction in the side-channel that
2886    /// `get_prop_native_kind` consults. Called from
2887    /// `compile_expr_property_access`'s GetProp emit sites.
2888    /// `field_type` is the schema's `FieldType` for the property; only
2889    /// types whose runtime branch in `op_get_prop` pushes raw native
2890    /// bits via `push_field_value` (I64 / Timestamp / F64 / Bool, plus
2891    /// width-int U64-low-bit special case) are recorded. Heap-bearing
2892    /// fields, decimals, and any other `FieldType` push the typed Arc
2893    /// pointer with the matching `NativeKind::Ptr(HeapKind::*)` declared
2894    /// on the parallel-kind track per ADR-006 §2.7.7.
2895    pub(super) fn record_get_prop_native_kind(
2896        &mut self,
2897        field_type: Option<&shape_runtime::type_schema::FieldType>,
2898    ) {
2899        use shape_runtime::type_schema::FieldType;
2900        let kind = match field_type {
2901            Some(FieldType::I64) | Some(FieldType::Timestamp) => StorageHint::Int64,
2902            Some(FieldType::F64) => StorageHint::Float64,
2903            Some(FieldType::Bool) => StorageHint::Bool,
2904            _ => return,
2905        };
2906        if let Some(idx) = self.program.instructions.len().checked_sub(1) {
2907            self.get_prop_native_kinds.insert(idx, kind);
2908        }
2909    }
2910
2911    /// Patch a jump instruction with the correct offset
2912    pub(super) fn patch_jump(&mut self, jump_idx: usize) {
2913        let offset = self.program.current_offset() as i32 - jump_idx as i32 - 1;
2914        self.program.instructions[jump_idx] = Instruction::new(
2915            self.program.instructions[jump_idx].opcode,
2916            Some(Operand::Offset(offset)),
2917        );
2918    }
2919
2920    /// Compile function call arguments, enabling `&` reference expressions.
2921    ///
2922    /// Each call's arguments get their own borrow region so that borrows from
2923    /// `&` references are released after the call returns. This matches Rust's
2924    /// semantics: temporary borrows from function arguments don't persist beyond
2925    /// the call. Sequential calls like `inc(&a); inc(&a)` are correctly allowed.
2926    pub(super) fn compile_call_args(
2927        &mut self,
2928        args: &[shape_ast::ast::Expr],
2929        expected_param_modes: Option<&[ParamPassMode]>,
2930    ) -> Result<Vec<(u16, u16)>> {
2931        self.call_arg_module_binding_ref_writebacks.push(Vec::new());
2932
2933        let mut first_error: Option<ShapeError> = None;
2934        for (idx, arg) in args.iter().enumerate() {
2935            let pass_mode = expected_param_modes
2936                .and_then(|modes| modes.get(idx).copied())
2937                .unwrap_or(ParamPassMode::ByValue);
2938
2939            let arg_result = match pass_mode {
2940                ParamPassMode::ByRefExclusive | ParamPassMode::ByRefShared => {
2941                    let borrow_mode = if pass_mode.is_exclusive() {
2942                        BorrowMode::Exclusive
2943                    } else {
2944                        BorrowMode::Shared
2945                    };
2946                    if let shape_ast::ast::Expr::Reference { expr, span, .. } = arg {
2947                        self.compile_reference_expr(expr, *span, borrow_mode)
2948                            .map(|_| ())
2949                    } else {
2950                        self.compile_implicit_reference_arg(arg, borrow_mode)
2951                    }
2952                }
2953                ParamPassMode::ByValue => {
2954                    if let shape_ast::ast::Expr::Reference { span, .. } = arg {
2955                        let message = if expected_param_modes.is_some() {
2956                            "[B0004] unexpected `&` argument: target parameter is not a reference parameter".to_string()
2957                        } else {
2958                            "[B0004] cannot pass `&` to a callable value without a declared reference contract; \
2959                             call a named function with known parameter modes or add an explicit callable type"
2960                                .to_string()
2961                        };
2962                        Err(ShapeError::SemanticError {
2963                            message,
2964                            location: Some(self.span_to_source_location(*span)),
2965                        })
2966                    } else {
2967                        self.plan_flexible_binding_escape_from_expr(arg);
2968                        self.compile_expr(arg)
2969                    }
2970                }
2971            };
2972
2973            if let Err(err) = arg_result {
2974                if self.should_recover_compile_diagnostics() {
2975                    self.errors.push(err);
2976                    // Keep stack arity consistent for downstream call codegen.
2977                    self.emit(Instruction::simple(OpCode::PushNull));
2978                    continue;
2979                }
2980                first_error = Some(err);
2981                break;
2982            }
2983        }
2984
2985        let writebacks = self
2986            .call_arg_module_binding_ref_writebacks
2987            .pop()
2988            .unwrap_or_default();
2989        if let Some(err) = first_error {
2990            Err(err)
2991        } else {
2992            Ok(writebacks)
2993        }
2994    }
2995
2996    pub(super) fn compile_implicit_reference_arg(
2997        &mut self,
2998        arg: &shape_ast::ast::Expr,
2999        mode: BorrowMode,
3000    ) -> Result<()> {
3001        use shape_ast::ast::Expr;
3002        match arg {
3003            Expr::Identifier(name, span) => self
3004                .compile_reference_identifier(name, *span, mode)
3005                .map(|_| ()),
3006            Expr::PropertyAccess {
3007                object,
3008                property,
3009                optional: false,
3010                span,
3011            } => self
3012                .compile_reference_property_access(object, property, *span, mode)
3013                .map(|_| ()),
3014            Expr::IndexAccess {
3015                object,
3016                index,
3017                end_index: None,
3018                span,
3019            } => self
3020                .compile_reference_index_access(object, index, *span, mode)
3021                .map(|_| ()),
3022            _ => {
3023                self.compile_expr_preserving_refs(arg)?;
3024                if let Some(returned_mode) = self.last_expr_reference_mode() {
3025                    if mode == BorrowMode::Exclusive && returned_mode != BorrowMode::Exclusive {
3026                        return Err(ShapeError::SemanticError {
3027                            message:
3028                                "cannot pass a shared reference result to an exclusive parameter"
3029                                    .to_string(),
3030                            location: Some(self.span_to_source_location(arg.span())),
3031                        });
3032                    }
3033                    return Ok(());
3034                }
3035                if mode == BorrowMode::Exclusive {
3036                    return Err(ShapeError::SemanticError {
3037                        message:
3038                            "[B0004] mutable reference arguments must be simple variables or existing exclusive references"
3039                                .to_string(),
3040                        location: Some(self.span_to_source_location(arg.span())),
3041                    });
3042                }
3043                let temp = self.declare_temp_local("__arg_ref_")?;
3044                self.emit(Instruction::new(
3045                    OpCode::StoreLocal,
3046                    Some(Operand::Local(temp)),
3047                ));
3048                // MIR analysis is the sole authority for borrow checking.
3049                self.emit(Instruction::new(
3050                    OpCode::MakeRef,
3051                    Some(Operand::Local(temp)),
3052                ));
3053                Ok(())
3054            }
3055        }
3056    }
3057
3058    pub(super) fn compile_reference_identifier(
3059        &mut self,
3060        name: &str,
3061        span: shape_ast::ast::Span,
3062        mode: BorrowMode,
3063    ) -> Result<u32> {
3064        if let Some(local_idx) = self.resolve_local(name) {
3065            // Reject exclusive borrows of const variables
3066            if mode == BorrowMode::Exclusive && self.const_locals.contains(&local_idx) {
3067                return Err(ShapeError::SemanticError {
3068                    message: format!(
3069                        "Cannot pass const variable '{}' by exclusive reference",
3070                        name
3071                    ),
3072                    location: Some(self.span_to_source_location(span)),
3073                });
3074            }
3075            if self.ref_locals.contains(&local_idx) {
3076                // Forward an existing reference parameter by value (TAG_REF).
3077                self.emit(Instruction::new(
3078                    OpCode::LoadLocal,
3079                    Some(Operand::Local(local_idx)),
3080                ));
3081                return Ok(u32::MAX);
3082            }
3083            if self.reference_value_locals.contains(&local_idx) {
3084                if mode == BorrowMode::Exclusive
3085                    && !self.exclusive_reference_value_locals.contains(&local_idx)
3086                {
3087                    return Err(ShapeError::SemanticError {
3088                        message: format!(
3089                            "Cannot pass shared reference variable '{}' as an exclusive reference",
3090                            name
3091                        ),
3092                        location: Some(self.span_to_source_location(span)),
3093                    });
3094                }
3095                self.emit(Instruction::new(
3096                    OpCode::LoadLocal,
3097                    Some(Operand::Local(local_idx)),
3098                ));
3099                return Ok(u32::MAX);
3100            }
3101            // MIR analysis is the sole authority for borrow checking.
3102            self.emit(Instruction::new(
3103                OpCode::MakeRef,
3104                Some(Operand::Local(local_idx)),
3105            ));
3106            Ok(u32::MAX)
3107        } else if let Some(scoped_name) = self.resolve_scoped_module_binding_name(name) {
3108            let Some(&binding_idx) = self.module_bindings.get(&scoped_name) else {
3109                return Err(ShapeError::SemanticError {
3110                    message: format!(
3111                        "[B0004] reference argument must be a local or module_binding variable, got '{}'",
3112                        name
3113                    ),
3114                    location: Some(self.span_to_source_location(span)),
3115                });
3116            };
3117            // Reject exclusive borrows of const module bindings
3118            if mode == BorrowMode::Exclusive && self.const_module_bindings.contains(&binding_idx) {
3119                return Err(ShapeError::SemanticError {
3120                    message: format!(
3121                        "Cannot pass const variable '{}' by exclusive reference",
3122                        name
3123                    ),
3124                    location: Some(self.span_to_source_location(span)),
3125                });
3126            }
3127            if self.reference_value_module_bindings.contains(&binding_idx) {
3128                if mode == BorrowMode::Exclusive
3129                    && !self
3130                        .exclusive_reference_value_module_bindings
3131                        .contains(&binding_idx)
3132                {
3133                    return Err(ShapeError::SemanticError {
3134                        message: format!(
3135                            "Cannot pass shared reference variable '{}' as an exclusive reference",
3136                            name
3137                        ),
3138                        location: Some(self.span_to_source_location(span)),
3139                    });
3140                }
3141                self.emit(Instruction::new(
3142                    OpCode::LoadModuleBinding,
3143                    Some(Operand::ModuleBinding(binding_idx)),
3144                ));
3145                return Ok(u32::MAX);
3146            }
3147            // MIR analysis is the sole authority for borrow checking.
3148            self.emit(Instruction::new(
3149                OpCode::MakeRef,
3150                Some(Operand::ModuleBinding(binding_idx)),
3151            ));
3152            Ok(u32::MAX)
3153        } else if let Some(func_idx) = self.find_function(name) {
3154            // Function name passed as reference argument: create a temporary local
3155            // with the function constant and make a reference to it.
3156            let temp = self.declare_temp_local("__fn_ref_")?;
3157            let const_idx = self
3158                .program
3159                .add_constant(Constant::Function(func_idx as u16));
3160            self.emit(Instruction::new(
3161                OpCode::PushConst,
3162                Some(Operand::Const(const_idx)),
3163            ));
3164            self.emit(Instruction::new(
3165                OpCode::StoreLocal,
3166                Some(Operand::Local(temp)),
3167            ));
3168            // MIR analysis is the sole authority for borrow checking.
3169            self.emit(Instruction::new(
3170                OpCode::MakeRef,
3171                Some(Operand::Local(temp)),
3172            ));
3173            Ok(u32::MAX)
3174        } else {
3175            Err(ShapeError::SemanticError {
3176                message: format!(
3177                    "[B0004] reference argument must be a local or module_binding variable, got '{}'",
3178                    name
3179                ),
3180                location: Some(self.span_to_source_location(span)),
3181            })
3182        }
3183    }
3184
3185    /// Push a new scope
3186    pub(super) fn push_scope(&mut self) {
3187        self.locals.push(HashMap::new());
3188        self.type_tracker.push_scope();
3189    }
3190
3191    /// Pop a scope
3192    pub(super) fn pop_scope(&mut self) {
3193        self.locals.pop();
3194        self.type_tracker.pop_scope();
3195    }
3196
3197    /// Declare a local variable
3198    pub(super) fn declare_local(&mut self, name: &str) -> Result<u16> {
3199        let idx = self.next_local;
3200        self.next_local += 1;
3201
3202        if let Some(scope) = self.locals.last_mut() {
3203            scope.insert(name.to_string(), idx);
3204        }
3205
3206        Ok(idx)
3207    }
3208
3209    /// Resolve a local variable
3210    pub(super) fn resolve_local(&self, name: &str) -> Option<u16> {
3211        for scope in self.locals.iter().rev() {
3212            if let Some(&idx) = scope.get(name) {
3213                return Some(idx);
3214            }
3215        }
3216        None
3217    }
3218
3219    /// Reverse lookup: slot index → local name (if any currently in scope).
3220    /// Used by the Phase V1.1C emission gate to consult `boxed_locals` (keyed
3221    /// by name) for a given slot. Returns the first match walking the scope
3222    /// stack innermost→outermost.
3223    pub(super) fn local_name_for_slot(&self, slot: u16) -> Option<&str> {
3224        for scope in self.locals.iter().rev() {
3225            for (name, &idx) in scope.iter() {
3226                if idx == slot {
3227                    return Some(name.as_str());
3228                }
3229            }
3230        }
3231        None
3232    }
3233
3234    /// Phase V1.1C: true when the slot has been converted to a SharedCell
3235    /// wrapper via a prior legacy cell-wrapping emission (tracked in
3236    /// `self.boxed_locals` keyed by binding name). The V1.1C `CloneLocal`
3237    /// opcode does not auto-unwrap `SharedCell`s, so boxed slots must
3238    /// fall through to the legacy `LoadLocal` path which handles the
3239    /// unwrap.
3240    pub(super) fn slot_is_boxed(&self, slot: u16) -> bool {
3241        self.local_name_for_slot(slot)
3242            .map(|name| self.boxed_locals.contains(name))
3243            .unwrap_or(false)
3244    }
3245
3246    /// Track A.1C.2: true when the slot has been promoted to
3247    /// `Arc<SharedCell>` via `AllocSharedLocal` (tracked in
3248    /// `self.shared_locals` keyed by binding name). Outer-scope reads
3249    /// and writes on such a slot must use `LoadSharedLocal` /
3250    /// `StoreSharedLocal`; plain `LoadLocal` / `StoreLocal` would
3251    /// observe the raw `*const SharedCell` pointer bits.
3252    pub(super) fn slot_is_shared(&self, slot: u16) -> bool {
3253        self.local_name_for_slot(slot)
3254            .map(|name| self.shared_locals.contains(name))
3255            .unwrap_or(false)
3256    }
3257
3258    /// Declare a temporary local variable
3259    pub(super) fn declare_temp_local(&mut self, prefix: &str) -> Result<u16> {
3260        let name = format!("{}{}", prefix, self.next_local);
3261        self.declare_local(&name)
3262    }
3263
3264    /// Set type info for an existing local variable
3265    pub(super) fn set_local_type_info(&mut self, slot: u16, type_name: &str) {
3266        let info = if let Some(schema) = self.type_tracker.schema_registry().get(type_name) {
3267            VariableTypeInfo::known(schema.id, type_name.to_string())
3268        } else {
3269            VariableTypeInfo::named(type_name.to_string())
3270        };
3271        self.type_tracker.set_local_type(slot, info);
3272    }
3273
3274    /// Set type info for a module_binding variable
3275    pub(super) fn set_module_binding_type_info(&mut self, slot: u16, type_name: &str) {
3276        let info = if let Some(schema) = self.type_tracker.schema_registry().get(type_name) {
3277            VariableTypeInfo::known(schema.id, type_name.to_string())
3278        } else {
3279            VariableTypeInfo::named(type_name.to_string())
3280        };
3281        self.type_tracker.set_binding_type(slot, info);
3282    }
3283
3284    /// Capture local storage hints for a compiled function.
3285    ///
3286    /// Must be called before the function scope is popped so the type tracker still
3287    /// has local slot metadata. Also populates the function's `FrameDescriptor` so
3288    /// the verifier and executor can use per-slot type info for trusted opcodes.
3289    pub(super) fn capture_function_local_storage_hints(&mut self, func_idx: usize) {
3290        let Some(func) = self.program.functions.get(func_idx) else {
3291            return;
3292        };
3293        // Per ADR-006 §2.7.5.1, the compiler-tier intermediate state is
3294        // `Option<StorageHint>`. The wire-format `function_local_storage_hints`
3295        // (and `FrameDescriptor.slots`) is `Vec<NativeKind>` — every slot
3296        // must have a proven kind by FunctionBlob construction time. We
3297        // collect via `collect::<Option<Vec<_>>>()`, which short-circuits to
3298        // `None` if ANY slot is unproven. When all are proven, we stamp the
3299        // descriptor and the legacy hints vec; otherwise we leave both empty
3300        // (the legacy "all-Unknown" path) so downstream readers fall back to
3301        // polymorphic emission.
3302        let proven_hints: Option<Vec<StorageHint>> = (0..func.locals_count)
3303            .map(|slot| self.type_tracker.get_local_storage_hint(slot))
3304            .collect();
3305
3306        let instr_len = self.program.instructions.len();
3307        let code_end = if func.body_length > 0 {
3308            (func.entry_point + func.body_length).min(instr_len)
3309        } else {
3310            instr_len
3311        };
3312        let has_trusted = if func.entry_point <= code_end && code_end <= instr_len {
3313            self.program.instructions[func.entry_point..code_end]
3314                .iter()
3315                .any(|i| i.opcode.is_trusted())
3316        } else {
3317            false
3318        };
3319
3320        // Populate FrameDescriptor only when every slot's kind is proven —
3321        // §2.7.5.1 forbids `NativeKind::Unknown` placeholders in the wire
3322        // format. If any slot is unproven, leave the descriptor unset and
3323        // fall through to the legacy polymorphic path.
3324        let hints: Vec<StorageHint> = match proven_hints {
3325            Some(h) => {
3326                if !h.is_empty() || has_trusted {
3327                    self.program.functions[func_idx].frame_descriptor = Some(
3328                        crate::type_tracking::FrameDescriptor::from_slots(h.clone()),
3329                    );
3330                }
3331                h
3332            }
3333            None => Vec::new(),
3334        };
3335
3336        if self.program.function_local_storage_hints.len() <= func_idx {
3337            self.program
3338                .function_local_storage_hints
3339                .resize(func_idx + 1, Vec::new());
3340        }
3341        self.program.function_local_storage_hints[func_idx] = hints;
3342    }
3343
3344    /// Wave E+4 commit 4: infer the top-level program's return-value
3345    /// `NativeKind` from the last compiled expression's tracked type metadata.
3346    ///
3347    /// Source of truth (in priority order):
3348    ///   1. `self.last_expr_numeric_type` — set by literals, var loads, and
3349    ///      arithmetic; reliable signal for primitive numerics.
3350    ///   2. `self.last_expr_type_info.storage_hint` — covers Bool, String,
3351    ///      and any nullable / sub-i64 widths the type-tracker has resolved.
3352    ///
3353    /// Returns the type-tracker's stored hint when a signal is available;
3354    /// otherwise the caller leaves `top_level_frame.return_kind` at its
3355    /// default. Per ADR-006 §2.7.7 the post-strict-typing host boundary
3356    /// reads the kind off the parallel-kind track (the deleted
3357    /// `synthesize_value_word_from_raw` decoder is gone), so a missing
3358    /// signal here just means the host boundary observes whatever
3359    /// `NativeKind` the producer declared at push time.
3360    ///
3361    /// **Why this is a host-boundary policy decision, not a per-site
3362    /// emission flip**: the host-boundary kind read fires once at
3363    /// `vm.execute()` exit. Setting `return_kind` for top-level resolves
3364    /// the boundary-encoding gap that the Wave E.1/E.2 typed-capture-Load
3365    /// flips opened. It does NOT touch any per-instruction emission and
3366    /// does NOT affect inner pipeline stack discipline; bits flow native
3367    /// through the inner pipeline as intended.
3368    /// Wave E+4 commit 4 fallback: when `last_expr_*` is None at the
3369    /// end of the last top-level item compile (a known gap for primitive-
3370    /// type returns like `bool` and `string` — see `type_info_from_annotation`
3371    /// in `function_calls.rs:384` which only resolves user-defined types
3372    /// from the schema registry, leaving primitives unresolved), inspect
3373    /// the AST item directly to extract the program's return-kind.
3374    ///
3375    /// Today supports the common case: `Item::Expression(Expr::FunctionCall)`
3376    /// with a name resolvable via `function_defs[name].return_type` (a
3377    /// `TypeAnnotation::Basic` for primitive types). Returns `None` for
3378    /// other shapes (the caller falls back to passthrough at the host
3379    /// boundary, preserving pre-E+4 semantics) — per ADR-006 §2.7.5.1
3380    /// the deleted `StorageHint::Unknown` sentinel is replaced by
3381    /// `Option<StorageHint>` at the compiler-tier intermediate state.
3382    pub(super) fn infer_top_level_return_kind_from_item(
3383        &self,
3384        item: &shape_ast::ast::Item,
3385    ) -> Option<StorageHint> {
3386        use shape_ast::ast::{Expr, Item, Statement};
3387
3388        // Extract the trailing expression of a top-level item, if any.
3389        let expr: &Expr = match item {
3390            Item::Expression(expr, _) => expr,
3391            Item::Statement(Statement::Expression(expr, _), _) => expr,
3392            _ => return None,
3393        };
3394
3395        // Walk into a call-shape: function-call / qualified-namespace-call
3396        // expressions are the dominant case for top-level program shapes
3397        // that declare a return type via `fn name() -> T { ... }; name()`
3398        // or `mod m { fn name() -> T { ... } } m::name()`.
3399        // For MethodCall (e.g. `obj.method()`), we don't have direct
3400        // access to the receiver's type at this AST-level inspection,
3401        // but the producer-side check (`last_emitted_native_kind` →
3402        // `call_native_kind`) inspects the compiled callee body and
3403        // picks up the typed-return kind directly when the callee uses
3404        // `ReturnValue<Kind>` opcodes uniformly.
3405        if let Expr::MethodCall { .. } = expr {
3406            return self.last_emitted_native_kind();
3407        }
3408
3409        // Wave E+5.5 cluster R5: top-level match expressions where every
3410        // arm body is a literal int / bool / number have a uniform
3411        // raw-native producer kind, but `last_emitted_native_kind` sees
3412        // the no-match `Throw` trailer and returns `None` (the trailer
3413        // is unreachable for an exhaustive match — the arms jump past
3414        // it — but it's still emitted). Inspect each arm body's literal
3415        // kind directly: when all match, the program has a typed
3416        // top-level return that the host boundary should synthesize.
3417        // Conservatively reject any non-literal arm body to avoid
3418        // promoting polymorphic producers (e.g. `data.len()`,
3419        // `f()` returning unknown type) — the failing case is
3420        // `match x { _ => some_int_arm, _ => poly_call_arm }` where
3421        // walking back past the throw would land at the int arm and
3422        // wrongly promote to Int64 even though the matched arm could
3423        // be the polymorphic one.
3424        if let Expr::Match(match_expr, _) = expr {
3425            return Self::match_arms_uniform_literal_kind(match_expr);
3426        }
3427
3428        let owned_qualified;
3429        let call_name: &str = match expr {
3430            Expr::FunctionCall { name, .. } => name.as_str(),
3431            Expr::QualifiedFunctionCall {
3432                namespace,
3433                function,
3434                ..
3435            } => {
3436                // Reconstruct the fully-qualified `namespace::function`
3437                // key that `register_function` (statements.rs:529) uses
3438                // when inserting module-scoped function defs.
3439                owned_qualified = format!("{}::{}", namespace, function);
3440                owned_qualified.as_str()
3441            }
3442            _ => return None,
3443        };
3444
3445        // Resolve callee return annotation. Try regular function defs
3446        // first, then foreign function defs (different struct types,
3447        // can't chain via `or_else` directly).
3448        let return_ann: Option<&TypeAnnotation> = self
3449            .function_defs
3450            .get(call_name)
3451            .and_then(|def| def.return_type.as_ref())
3452            .or_else(|| {
3453                self.foreign_function_defs
3454                    .get(call_name)
3455                    .and_then(|def| def.return_type.as_ref())
3456            });
3457        let ann = return_ann?;
3458
3459        // Map primitive type-annotation names to `NativeKind` (handles
3460        // both `Basic("bool")` and `Reference("Bool")`-style entries).
3461        // Also resolve through `type_aliases` so a callee declaring a
3462        // typed return like `fn make() -> MyInt { 42 }; type MyInt = int`
3463        // surfaces the right `NativeKind` on the parallel-kind track at
3464        // the host boundary (per ADR-006 §2.7.7 — the deleted
3465        // `synthesize_value_word_from_raw` decoder is gone).
3466        let name = ann.as_type_name_str()?;
3467        let inferred = primitive_type_name_to_storage_hint(name)
3468            .or_else(|| {
3469                self.type_aliases
3470                    .get(name)
3471                    .and_then(|aliased| primitive_type_name_to_storage_hint(aliased.as_str()))
3472            })
3473            .or_else(|| {
3474                // Try qualified alias name `<namespace>::<name>` if the
3475                // call was qualified — module-scoped `type Alias = int`
3476                // is registered as `m::Alias` in the alias map.
3477                if let shape_ast::ast::Expr::QualifiedFunctionCall { namespace, .. } = expr {
3478                    let q = format!("{}::{}", namespace, name);
3479                    self.type_aliases
3480                        .get(&q)
3481                        .and_then(|aliased| primitive_type_name_to_storage_hint(aliased.as_str()))
3482                } else {
3483                    None
3484                }
3485            })?;
3486
3487        // Producer/return-kind contract gate (Wave E+5 / task #98 fix).
3488        // Top-level `name()` calls compile to polymorphic `Call*` opcodes
3489        // whose pushed kind is the callee's `FrameDescriptor.return_kind`
3490        // (read off the parallel-kind track at the call site per
3491        // ADR-006 §2.7.7); `last_emitted_native_kind` returns `None` for
3492        // these, which correctly steers the program return kind to
3493        // `None` rather than overriding the call-site declaration.
3494        let native_kind = self.last_emitted_native_kind()?;
3495
3496        if matches!(
3497            inferred,
3498            StorageHint::Int8
3499                | StorageHint::UInt8
3500                | StorageHint::Int16
3501                | StorageHint::UInt16
3502                | StorageHint::Int32
3503                | StorageHint::UInt32
3504                | StorageHint::Int64
3505                | StorageHint::UInt64
3506        ) && native_kind == StorageHint::Int64
3507        {
3508            return Some(inferred);
3509        }
3510
3511        if native_kind == inferred {
3512            Some(inferred)
3513        } else {
3514            None
3515        }
3516    }
3517
3518    /// Wave E+5.5 cluster R5: examine each arm of a top-level match
3519    /// expression and return a uniform `StorageHint` if every arm body
3520    /// is a literal whose native producer kind is the same. The
3521    /// supported literal arms are integer (`Int(_)` / `TypedInt(_, w)`),
3522    /// bool (`Bool(_)`), and number (`Number(_)`) — these compile to
3523    /// `PushConst Int / Bool / Number`, each declaring its `NativeKind`
3524    /// on the parallel-kind track per ADR-006 §2.7.7 (see
3525    /// `push_const_native_kind`). When ANY arm body is a non-literal
3526    /// (e.g. a method call, function call, binary op chain), return
3527    /// `Unknown` to keep the host boundary unchanged — promoting to a
3528    /// typed kind would override the matched arm's parallel-kind track
3529    /// declaration if the runtime path resolves to a polymorphic arm
3530    /// whose producer declared a different `NativeKind`.
3531    fn match_arms_uniform_literal_kind(
3532        match_expr: &shape_ast::ast::expr_helpers::MatchExpr,
3533    ) -> Option<StorageHint> {
3534        use shape_ast::ast::{Expr, literals::Literal};
3535
3536        let mut uniform: Option<StorageHint> = None;
3537        for arm in &match_expr.arms {
3538            // Only literal-direct bodies qualify. Block expressions,
3539            // parenthesised expressions, and any other indirection
3540            // disqualify — we do not want to walk through them and
3541            // mis-classify a non-literal final expression.
3542            let kind = match &*arm.body {
3543                Expr::Literal(Literal::Int(i), _) => {
3544                    // ADR-006 §2.7.7: deleted i48 NaN-box range constants —
3545                    // the post-strict-typing kind tracker stores full i64.
3546                    let _ = i;
3547                    StorageHint::Int64
3548                }
3549                Expr::Literal(Literal::TypedInt(_, w), _) => {
3550                    use shape_ast::IntWidth;
3551                    match w {
3552                        IntWidth::I8 => StorageHint::Int8,
3553                        IntWidth::U8 => StorageHint::UInt8,
3554                        IntWidth::I16 => StorageHint::Int16,
3555                        IntWidth::U16 => StorageHint::UInt16,
3556                        IntWidth::I32 => StorageHint::Int32,
3557                        IntWidth::U32 => StorageHint::UInt32,
3558                        IntWidth::U64 => StorageHint::UInt64,
3559                    }
3560                }
3561                Expr::Literal(Literal::Bool(_), _) => StorageHint::Bool,
3562                // Number literals compile to `PushConst Number` whose
3563                // `push_const_native_kind` reports `Float64` — the
3564                // raw `f64::to_bits()` payload is pushed with `Float64`
3565                // declared on the parallel-kind track per ADR-006 §2.7.7.
3566                Expr::Literal(Literal::Number(_), _) => StorageHint::Float64,
3567                // Anything else (method call, function call, binary
3568                // arithmetic, identifier, …) is rejected: we can't
3569                // statically prove uniform stack discipline across
3570                // arms, so return None to keep passthrough — per
3571                // ADR-006 §2.7.5.1, "kind not yet proven" is `None`.
3572                _ => return None,
3573            };
3574            match uniform {
3575                None => uniform = Some(kind),
3576                Some(prev) if prev == kind => {}
3577                _ => return None,
3578            }
3579        }
3580        uniform
3581    }
3582
3583    pub(super) fn infer_top_level_return_kind(&self) -> Option<StorageHint> {
3584        // Inferred kind from compile-time numeric / type-info tracking.
3585        // This is the *intended* program return kind. We must still verify
3586        // the producer-side stack discipline matches before declaring it
3587        // (see `last_emitted_native_kind` and the gating below). Per
3588        // ADR-006 §2.7.5.1, "kind not yet proven" is carried as `None`.
3589        let inferred: StorageHint = self
3590            .last_expr_numeric_type
3591            .and_then(|nt| match nt {
3592                crate::type_tracking::NumericType::Number => Some(StorageHint::Float64),
3593                crate::type_tracking::NumericType::Int => Some(StorageHint::Int64),
3594                crate::type_tracking::NumericType::IntWidth(w) => {
3595                    use shape_ast::IntWidth;
3596                    Some(match w {
3597                        IntWidth::I8 => StorageHint::Int8,
3598                        IntWidth::U8 => StorageHint::UInt8,
3599                        IntWidth::I16 => StorageHint::Int16,
3600                        IntWidth::U16 => StorageHint::UInt16,
3601                        IntWidth::I32 => StorageHint::Int32,
3602                        IntWidth::U32 => StorageHint::UInt32,
3603                        IntWidth::U64 => StorageHint::UInt64,
3604                    })
3605                }
3606                // Decimal isn't a `NativeKind` variant — fall through to
3607                // None so synthesis stays in passthrough.
3608                crate::type_tracking::NumericType::Decimal => None,
3609            })
3610            .or_else(|| {
3611                // Post-§2.7.5.1: `info.storage_hint` is itself
3612                // `Option<StorageHint>`, so `.and_then(|info| info.storage_hint)`
3613                // collapses both Option layers.
3614                self.last_expr_type_info
3615                    .as_ref()
3616                    .and_then(|info| info.storage_hint)
3617            })?;
3618
3619        // Producer/return-kind contract gate (Wave E+5 / task #98 fix).
3620        //
3621        // Many expression compilers (property access, method/function call,
3622        // typed-object construction, …) propagate `last_expr_numeric_type`
3623        // from the AST-level type so binary-op typed dispatch (`MulInt`
3624        // etc.) still emits the right opcode for them. But the producer
3625        // opcodes for those expressions remain polymorphic — they declare
3626        // a `NativeKind` on the parallel-kind track at push time per
3627        // ADR-006 §2.7.7 (the deleted ValueWord-tagged transport is gone),
3628        // and that kind may not match the AST-inferred kind. Overriding
3629        // `top_level_frame.return_kind` for such producers would steer
3630        // the host boundary away from the producer-declared kind.
3631        //
3632        // We only declare the kind when the LAST emitted opcode is on the
3633        // known raw-native producer list. Otherwise we fall through to
3634        // `None` so the host boundary reads the producer-declared kind
3635        // off the parallel-kind track unchanged.
3636        let native_kind = self.last_emitted_native_kind()?;
3637
3638        // Width-aware check: if the inferred kind is a sub-i64 width
3639        // (Int8/U8/…/U32) and the producer is `Int64` (the catch-all for
3640        // all integer arithmetic / load opcodes), prefer the inferred
3641        // narrow kind. The synthesizer reads raw i64 bits identically for
3642        // all signed-int widths, so this is safe.
3643        if matches!(
3644            inferred,
3645            StorageHint::Int8
3646                | StorageHint::UInt8
3647                | StorageHint::Int16
3648                | StorageHint::UInt16
3649                | StorageHint::Int32
3650                | StorageHint::UInt32
3651                | StorageHint::Int64
3652                | StorageHint::UInt64
3653        ) && native_kind == StorageHint::Int64
3654        {
3655            return Some(inferred);
3656        }
3657
3658        if native_kind == inferred {
3659            Some(inferred)
3660        } else {
3661            None
3662        }
3663    }
3664
3665    /// Populate program-level storage hints for top-level locals and module bindings.
3666    pub(super) fn populate_program_storage_hints(&mut self) {
3667        // Per ADR-006 §2.7.5.1, the compiler-tier intermediate state is
3668        // `Option<StorageHint>`. The wire-format `top_level_local_storage_hints`
3669        // (and `FrameDescriptor.slots`) is `Vec<NativeKind>` — every slot
3670        // must have a proven kind by FunctionBlob construction time. We
3671        // collect via `collect::<Option<Vec<_>>>()`, which short-circuits
3672        // to `None` if ANY slot is unproven.
3673        let top_hints_proven: Option<Vec<StorageHint>> = (0..self.next_local)
3674            .map(|slot| self.type_tracker.get_local_storage_hint(slot))
3675            .collect();
3676        let top_hints: Vec<StorageHint> = top_hints_proven.clone().unwrap_or_default();
3677        self.program.top_level_local_storage_hints = top_hints.clone();
3678
3679        // Build top-level FrameDescriptor so JIT can use per-slot type info.
3680        //
3681        // E+5.5 Unit C step 2: read the return-kind captured RIGHT AFTER
3682        // the last item compiled (in `compiler_impl_reference_model.rs:1282`)
3683        // — pre drop-scope-emission and Halt — so `last_expr_*` reflects
3684        // the program's final value at the moment of capture, not after
3685        // teardown opcodes have overwritten it. Falls back to a fresh
3686        // inference if nothing was captured. When set, the host boundary
3687        // reads `return_kind` directly off the FrameDescriptor (per
3688        // ADR-006 §2.7.7 — the deleted `synthesize_value_word_from_raw`
3689        // tagged-bits decoder is gone). This is what makes typed top-level
3690        // programs (Int/Bool/Float64 ending in arithmetic, comparisons,
3691        // or typed-load) round-trip cleanly through `vm.execute()`
3692        // post-Unit-A/B native arithmetic flip.
3693        //
3694        // Per ADR-006 §2.7.5.1, "kind not yet stamped" is `None`.
3695        let return_kind: Option<StorageHint> = self
3696            .top_level_program_return_kind
3697            .or_else(|| self.infer_top_level_return_kind());
3698        let has_trusted = self
3699            .program
3700            .instructions
3701            .iter()
3702            .any(|i| i.opcode.is_trusted());
3703        let has_any_known = top_hints_proven.is_some() && !top_hints.is_empty();
3704        let has_typed_return = return_kind.is_some();
3705        if has_any_known || has_trusted || has_typed_return {
3706            // §2.7.5.1: FrameDescriptor.slots is wire-format `Vec<NativeKind>`
3707            // (no Option). When the per-slot kinds aren't all proven, fall
3708            // back to an empty slot vec — the descriptor is still useful
3709            // for return_kind alone, and the legacy hints vec also stays
3710            // empty in that case.
3711            let slots = top_hints_proven.unwrap_or_default();
3712            let mut frame = crate::type_tracking::FrameDescriptor::from_slots(slots);
3713            frame.return_kind = return_kind;
3714            self.program.top_level_frame = Some(frame);
3715        }
3716
3717
3718        // Per ADR-006 §2.7.5.1, the wire-format
3719        // `module_binding_storage_hints: Vec<NativeKind>` requires every
3720        // slot proven by FunctionBlob construction. Short-circuit via
3721        // `collect::<Option<Vec<_>>>()` — if any binding's kind is
3722        // unproven, leave the wire vec empty (the legacy "all-Unknown"
3723        // path) so downstream readers route to polymorphic emission.
3724        let module_binding_hints: Vec<StorageHint> = (0..self.module_bindings.len() as u16)
3725            .map(|idx| self.type_tracker.get_module_binding_storage_hint(idx))
3726            .collect::<Option<Vec<_>>>()
3727            .unwrap_or_default();
3728        self.program.module_binding_storage_hints = module_binding_hints;
3729
3730        if self.program.function_local_storage_hints.len() < self.program.functions.len() {
3731            self.program
3732                .function_local_storage_hints
3733                .resize(self.program.functions.len(), Vec::new());
3734        } else if self.program.function_local_storage_hints.len() > self.program.functions.len() {
3735            self.program
3736                .function_local_storage_hints
3737                .truncate(self.program.functions.len());
3738        }
3739    }
3740
3741    /// Propagate the current expression's inferred type metadata to a target slot.
3742    ///
3743    /// Used by assignment sites to keep mutable locals/module_bindings typed when
3744    /// safe, and to clear stale hints when assigning unknown/dynamic values.
3745    pub(super) fn propagate_assignment_type_to_slot(
3746        &mut self,
3747        slot: u16,
3748        is_local: bool,
3749        allow_number_hint: bool,
3750    ) {
3751        if let Some(ref info) = self.last_expr_type_info {
3752            if info.is_indexed()
3753                || info.is_datatable()
3754                || info.schema_id.is_some()
3755                || Self::is_array_type_name(info.type_name.as_deref())
3756                // R5.3B: temporal type names ("DateTime" / "Duration" /
3757                // "TimeSpan") set by `compile_expr_datetime` /
3758                // `compile_expr_duration` must propagate into the local /
3759                // module-binding tracker. Without this, let-locals bound to
3760                // temporal literals are recorded as `Unknown`, and
3761                // `infer_expr_type` cannot resolve the retarget at the
3762                // `dt + dur` site.
3763                || Self::is_temporal_type_name(info.type_name.as_deref())
3764                // Phase 3e: propagate primitive non-numeric type names
3765                // (string / bool / char) set by `compile_expr_literal`.
3766                // Without this, `let mut s = ""` records `s` as
3767                // Unknown, breaking string-concat in body loops
3768                // (comptime-for, generic for-in, while, etc.).
3769                || matches!(
3770                    info.type_name.as_deref(),
3771                    Some("string" | "bool" | "char")
3772                )
3773            {
3774                if is_local {
3775                    self.type_tracker.set_local_type(slot, info.clone());
3776                } else {
3777                    self.type_tracker.set_binding_type(slot, info.clone());
3778                }
3779                return;
3780            }
3781        }
3782
3783        if let Some(schema_id) = self.last_expr_schema {
3784            let schema_name = self
3785                .type_tracker
3786                .schema_registry()
3787                .get_by_id(schema_id)
3788                .map(|s| s.name.clone())
3789                .unwrap_or_else(|| format!("__anon_{}", schema_id));
3790            let info = VariableTypeInfo::known(schema_id, schema_name);
3791            if is_local {
3792                self.type_tracker.set_local_type(slot, info);
3793            } else {
3794                self.type_tracker.set_binding_type(slot, info);
3795            }
3796            return;
3797        }
3798
3799        if let Some(numeric_type) = self.last_expr_numeric_type {
3800            let (type_name, hint) = match numeric_type {
3801                crate::type_tracking::NumericType::Int => ("int", StorageHint::Int64),
3802                crate::type_tracking::NumericType::IntWidth(w) => {
3803                    use shape_ast::IntWidth;
3804                    let hint = match w {
3805                        IntWidth::I8 => StorageHint::Int8,
3806                        IntWidth::U8 => StorageHint::UInt8,
3807                        IntWidth::I16 => StorageHint::Int16,
3808                        IntWidth::U16 => StorageHint::UInt16,
3809                        IntWidth::I32 => StorageHint::Int32,
3810                        IntWidth::U32 => StorageHint::UInt32,
3811                        IntWidth::U64 => StorageHint::UInt64,
3812                    };
3813                    (w.type_name(), hint)
3814                }
3815                crate::type_tracking::NumericType::Number => {
3816                    if !allow_number_hint {
3817                        if is_local {
3818                            self.type_tracker
3819                                .set_local_type(slot, VariableTypeInfo::unknown());
3820                        } else {
3821                            self.type_tracker
3822                                .set_binding_type(slot, VariableTypeInfo::unknown());
3823                        }
3824                        return;
3825                    }
3826                    ("number", StorageHint::Float64)
3827                }
3828                // Decimal typed opcodes are not JIT-compiled yet.
3829                crate::type_tracking::NumericType::Decimal => {
3830                    if is_local {
3831                        self.type_tracker
3832                            .set_local_type(slot, VariableTypeInfo::unknown());
3833                    } else {
3834                        self.type_tracker
3835                            .set_binding_type(slot, VariableTypeInfo::unknown());
3836                    }
3837                    return;
3838                }
3839            };
3840            let info = VariableTypeInfo::with_storage(type_name.to_string(), hint);
3841            if is_local {
3842                self.type_tracker.set_local_type(slot, info);
3843            } else {
3844                self.type_tracker.set_binding_type(slot, info);
3845            }
3846            return;
3847        }
3848
3849        // Assignment to an unknown/dynamic expression invalidates prior hints.
3850        if is_local {
3851            self.type_tracker
3852                .set_local_type(slot, VariableTypeInfo::unknown());
3853        } else {
3854            self.type_tracker
3855                .set_binding_type(slot, VariableTypeInfo::unknown());
3856        }
3857    }
3858
3859    /// Propagate current expression type metadata to an identifier target.
3860    ///
3861    /// Reference locals are skipped because assignment writes through to a pointee.
3862    pub(super) fn propagate_assignment_type_to_identifier(&mut self, name: &str) {
3863        if let Some(local_idx) = self.resolve_local(name) {
3864            if self.local_binding_is_reference_value(local_idx) {
3865                return;
3866            }
3867            self.propagate_assignment_type_to_slot(local_idx, true, true);
3868            return;
3869        }
3870
3871        let scoped_name = self
3872            .resolve_scoped_module_binding_name(name)
3873            .unwrap_or_else(|| name.to_string());
3874        let binding_idx = self.get_or_create_module_binding(&scoped_name);
3875        self.propagate_assignment_type_to_slot(binding_idx, false, true);
3876    }
3877
3878    /// Get the type tracker (for external configuration)
3879    /// Resolve a local namespace name to its canonical module path.
3880    ///
3881    /// Checks `graph_namespace_map` first (populated by graph-driven compilation),
3882    /// then falls back to `module_scope_sources` (legacy AST inlining path).
3883    pub(crate) fn resolve_canonical_module_path(&self, local_name: &str) -> Option<String> {
3884        self.graph_namespace_map
3885            .get(local_name)
3886            .or_else(|| self.module_scope_sources.get(local_name))
3887            .cloned()
3888    }
3889
3890    pub fn type_tracker(&self) -> &TypeTracker {
3891        &self.type_tracker
3892    }
3893
3894    /// Get mutable type tracker (for registering types)
3895    pub fn type_tracker_mut(&mut self) -> &mut TypeTracker {
3896        &mut self.type_tracker
3897    }
3898
3899    /// Resolve a column name to its index using the data schema.
3900    /// Returns an error if no schema is provided or the column doesn't exist.
3901    pub(super) fn resolve_column_index(&self, field: &str) -> Result<u32> {
3902        self.program
3903            .data_schema
3904            .as_ref()
3905            .ok_or_else(|| ShapeError::RuntimeError {
3906                message: format!(
3907                    "No data schema provided. Cannot resolve field '{}'. \
3908                     Hint: Use stdlib/finance to load market data with OHLCV schema.",
3909                    field
3910                ),
3911                location: None,
3912            })?
3913            .get_index(field)
3914            .ok_or_else(|| ShapeError::RuntimeError {
3915                message: format!(
3916                    "Unknown column '{}' in data schema. Available columns: {:?}",
3917                    field,
3918                    self.program
3919                        .data_schema
3920                        .as_ref()
3921                        .map(|s| &s.column_names)
3922                        .unwrap_or(&vec![])
3923                ),
3924                location: None,
3925            })
3926    }
3927
3928    /// Check if a field name is a known data column in the schema.
3929    pub(super) fn is_data_column(&self, field: &str) -> bool {
3930        self.program
3931            .data_schema
3932            .as_ref()
3933            .map(|s| s.get_index(field).is_some())
3934            .unwrap_or(false)
3935    }
3936
3937    /// Collect all outer scope variables
3938    pub(super) fn collect_outer_scope_vars(&self) -> Vec<String> {
3939        let mut names = BTreeSet::new();
3940        for scope in &self.locals {
3941            for name in scope.keys() {
3942                names.insert(name.clone());
3943            }
3944        }
3945        for name in self.module_bindings.keys() {
3946            names.insert(name.clone());
3947        }
3948        names.into_iter().collect()
3949    }
3950
3951    /// Get or create a module_binding variable
3952    pub(super) fn get_or_create_module_binding(&mut self, name: &str) -> u16 {
3953        if let Some(&idx) = self.module_bindings.get(name) {
3954            idx
3955        } else {
3956            let idx = self.next_global;
3957            self.next_global += 1;
3958            self.module_bindings.insert(name.to_string(), idx);
3959            idx
3960        }
3961    }
3962
3963    pub(super) fn resolve_scoped_module_binding_name(&self, name: &str) -> Option<String> {
3964        if self.module_bindings.contains_key(name) {
3965            return Some(name.to_string());
3966        }
3967        for module_path in self.module_scope_stack.iter().rev() {
3968            let candidate = format!("{}::{}", module_path, name);
3969            if self.module_bindings.contains_key(&candidate) {
3970                return Some(candidate);
3971            }
3972        }
3973        None
3974    }
3975
3976    /// Track A.1C.3: is the module binding reachable as `name` from the
3977    /// current scope already promoted to Shared (`AllocSharedModuleBinding`
3978    /// emitted)? Mirrors `shared_locals.contains` but honours module-scope
3979    /// name resolution.
3980    pub(crate) fn shared_module_binding_contains(&self, name: &str) -> bool {
3981        if self.shared_module_bindings.contains(name) {
3982            return true;
3983        }
3984        if let Some(scoped) = self.resolve_scoped_module_binding_name(name) {
3985            return self.shared_module_bindings.contains(&scoped);
3986        }
3987        false
3988    }
3989
3990    pub(super) fn resolve_scoped_function_name(&self, name: &str) -> Option<String> {
3991        if self.program.functions.iter().any(|f| f.name == name) {
3992            return Some(name.to_string());
3993        }
3994        for module_path in self.module_scope_stack.iter().rev() {
3995            let candidate = format!("{}::{}", module_path, name);
3996            if self.program.functions.iter().any(|f| f.name == candidate) {
3997                return Some(candidate);
3998            }
3999        }
4000        None
4001    }
4002
4003    /// Find a function by name
4004    pub(super) fn find_function(&self, name: &str) -> Option<usize> {
4005        // Check function aliases first (e.g., __original__ -> shadow function).
4006        if let Some(actual_name) = self.function_aliases.get(name) {
4007            if let Some(idx) = self
4008                .program
4009                .functions
4010                .iter()
4011                .position(|f| f.name == *actual_name)
4012            {
4013                return Some(idx);
4014            }
4015        }
4016
4017        // Try direct/scoped resolution
4018        if let Some(resolved) = self.resolve_scoped_function_name(name) {
4019            if let Some(idx) = self
4020                .program
4021                .functions
4022                .iter()
4023                .position(|f| f.name == resolved)
4024            {
4025                return Some(idx);
4026            }
4027        }
4028
4029        // If direct lookup failed, check imported_names for alias -> original name mapping.
4030        // When a function is imported with an alias (e.g., `use { foo as bar } from "module"`),
4031        // the function is registered under its original (possibly module-qualified) name,
4032        // but the user refers to it by the alias.
4033        if let Some(imported) = self.imported_names.get(name) {
4034            let original = &imported.original_name;
4035            // Try direct match on the original name
4036            if let Some(idx) = self
4037                .program
4038                .functions
4039                .iter()
4040                .position(|f| f.name == *original)
4041            {
4042                return Some(idx);
4043            }
4044            // Try scoped resolution on the original name
4045            if let Some(resolved) = self.resolve_scoped_function_name(original) {
4046                if let Some(idx) = self
4047                    .program
4048                    .functions
4049                    .iter()
4050                    .position(|f| f.name == resolved)
4051                {
4052                    return Some(idx);
4053                }
4054            }
4055            // Try module-qualified name: module_path::original_name
4056            // This is needed for graph-compiled dependencies where functions
4057            // are registered with their module-qualified names.
4058            if !imported.module_path.is_empty() {
4059                let qualified = format!("{}::{}", imported.module_path, original);
4060                if let Some(idx) = self
4061                    .program
4062                    .functions
4063                    .iter()
4064                    .position(|f| f.name == qualified)
4065                {
4066                    return Some(idx);
4067                }
4068            }
4069        }
4070
4071        None
4072    }
4073
4074    /// Resolve the receiver's type name for extend method dispatch.
4075    ///
4076    /// Determines the Shape type name from all available compiler state:
4077    /// - `last_expr_type_info.type_name` for TypedObjects (e.g., "Point", "Candle")
4078    /// - `last_expr_numeric_type` for numeric types → "Int", "Number", "Decimal"
4079    /// - Receiver expression analysis for arrays, strings, booleans
4080    ///
4081    /// Returns the base type name (e.g., "Vec" not "Vec<int>") suitable for
4082    /// extend method lookup as "Type.method".
4083    pub(super) fn resolve_receiver_extend_type(
4084        &self,
4085        receiver: &shape_ast::ast::Expr,
4086        receiver_type_info: &Option<crate::type_tracking::VariableTypeInfo>,
4087        _receiver_schema: Option<u32>,
4088    ) -> Option<String> {
4089        // 1. Numeric type from typed opcode tracking — checked first because
4090        //    the type tracker stores lowercase names ("int", "number") while
4091        //    extend blocks use capitalized TypeName ("Int", "Number", "Decimal").
4092        if let Some(numeric) = self.last_expr_numeric_type {
4093            return Some(
4094                match numeric {
4095                    crate::type_tracking::NumericType::Int
4096                    | crate::type_tracking::NumericType::IntWidth(_) => "Int",
4097                    crate::type_tracking::NumericType::Number => "Number",
4098                    crate::type_tracking::NumericType::Decimal => "Decimal",
4099                }
4100                .to_string(),
4101            );
4102        }
4103
4104        // 2. TypedObject type name (user-defined types like Point, Candle)
4105        if let Some(info) = receiver_type_info {
4106            if let Some(type_name) = &info.type_name {
4107                // Strip generic params: "Vec<int>" → "Vec"
4108                let base = type_name.split('<').next().unwrap_or(type_name);
4109                return Some(base.to_string());
4110            }
4111        }
4112
4113        // 3. Infer from receiver expression shape
4114        match receiver {
4115            shape_ast::ast::Expr::Literal(lit, _) => match lit {
4116                shape_ast::ast::Literal::String(_)
4117                | shape_ast::ast::Literal::FormattedString { .. } => Some("String".to_string()),
4118                shape_ast::ast::Literal::Bool(_) => Some("Bool".to_string()),
4119                _ => None,
4120            },
4121            shape_ast::ast::Expr::Array(..) => Some("Vec".to_string()),
4122            _ => None,
4123        }
4124    }
4125
4126    /// Emit store instruction for an identifier
4127    pub(super) fn emit_store_identifier(&mut self, name: &str) -> Result<()> {
4128        // Mutable closure captures: dispatch by CaptureKind.
4129        //   * `CaptureKind::Shared`       → A.1B StoreSharedCapture.
4130        //   * `CaptureKind::OwnedMutable` → A.1B StoreOwnedMutableCapture.
4131        //   * legacy SharedCell fallback  → StoreClosure.
4132        if let Some(&upvalue_idx) = self.mutable_closure_captures.get(name) {
4133            if let Some(&shared_idx) = self.shared_closure_captures.get(name) {
4134                debug_assert_eq!(upvalue_idx, shared_idx);
4135                // A2-refined / task #17: dispatch to Wave D.2's typed
4136                // `StoreSharedCapture<Kind>` opcodes (codes 0x161-0x16B)
4137                // by looking up the cell's interior `FieldKind` from
4138                // `shared_capture_inner_kinds`. Falls back to legacy
4139                // `StoreSharedCapture` (0x135) for unresolved capture
4140                // types.
4141                let opcode = match self.shared_capture_inner_kinds.get(name).copied() {
4142                    Some(kind) => shared_typed_store_opcode(kind),
4143                    None => OpCode::StoreSharedCapture,
4144                };
4145                self.emit(Instruction::new(opcode, Some(Operand::Local(shared_idx))));
4146                return Ok(());
4147            }
4148            // Wave E: dispatch to Wave D.1's typed
4149            // `StoreOwnedMutableCapture<Kind>` opcodes (codes 0x14B-0x155)
4150            // by looking up the cell's interior `FieldKind` from
4151            // `owned_mutable_capture_inner_kinds` (populated alongside
4152            // `owned_mutable_closure_captures` at closure-construction
4153            // time). Falls back to the legacy `StoreOwnedMutableCapture`
4154            // (0x133) for unresolved capture types — Wave G removes the
4155            // legacy opcode after every emit path is type-aware. The
4156            // Shared (`var`) capture path above stays on legacy
4157            // `StoreSharedCapture` (0x135) — atomic flip is follow-up #17.
4158            if let Some(&owned_idx) = self.owned_mutable_closure_captures.get(name) {
4159                debug_assert_eq!(upvalue_idx, owned_idx);
4160                let opcode = match self.owned_mutable_capture_inner_kinds.get(name).copied() {
4161                    Some(kind) => owned_mutable_typed_store_opcode(kind),
4162                    None => OpCode::StoreOwnedMutableCapture,
4163                };
4164                self.emit(Instruction::new(opcode, Some(Operand::Local(owned_idx))));
4165                return Ok(());
4166            }
4167            self.emit(Instruction::new(
4168                OpCode::StoreClosure,
4169                Some(Operand::Local(upvalue_idx)),
4170            ));
4171            return Ok(());
4172        }
4173        // Track A.1C.2: outer-scope write to a shared-promoted local
4174        // (`var` captured by closure) must go through StoreSharedLocal so
4175        // the store hits the inner cell payload bits (with the kind
4176        // declared on the cell's parallel-kind track per ADR-006 §2.7.8),
4177        // not the outer pointer slot.
4178        if self.shared_locals.contains(name)
4179            && let Some(local_idx) = self.resolve_local(name)
4180        {
4181            self.emit(Instruction::new(
4182                OpCode::StoreSharedLocal,
4183                Some(Operand::Local(local_idx)),
4184            ));
4185            return Ok(());
4186        }
4187        if let Some(local_idx) = self.resolve_local(name) {
4188            if self.local_binding_is_reference_value(local_idx) {
4189                if !self.local_reference_binding_is_exclusive(local_idx) {
4190                    return Err(ShapeError::SemanticError {
4191                        message: format!(
4192                            "cannot assign through shared reference variable '{}'",
4193                            name
4194                        ),
4195                        location: None,
4196                    });
4197                }
4198                self.emit(Instruction::new(
4199                    OpCode::DerefStore,
4200                    Some(Operand::Local(local_idx)),
4201                ));
4202            } else {
4203                self.emit(Instruction::new(
4204                    OpCode::StoreLocal,
4205                    Some(Operand::Local(local_idx)),
4206                ));
4207                // Patch StoreLocal → StoreLocalTyped for width-typed locals
4208                if let Some(type_name) = self
4209                    .type_tracker
4210                    .get_local_type(local_idx)
4211                    .and_then(|info| info.type_name.as_deref())
4212                {
4213                    if let Some(w) = shape_ast::IntWidth::from_name(type_name) {
4214                        if let Some(last) = self.program.instructions.last_mut() {
4215                            if last.opcode == OpCode::StoreLocal {
4216                                last.opcode = OpCode::StoreLocalTyped;
4217                                last.operand = Some(Operand::TypedLocal(
4218                                    local_idx,
4219                                    crate::bytecode::NumericWidth::from_int_width(w),
4220                                ));
4221                            }
4222                        }
4223                    }
4224                }
4225            }
4226        } else {
4227            let scoped_name = self
4228                .resolve_scoped_module_binding_name(name)
4229                .unwrap_or_else(|| name.to_string());
4230            let binding_idx = self.get_or_create_module_binding(&scoped_name);
4231            self.emit(Instruction::new(
4232                OpCode::StoreModuleBinding,
4233                Some(Operand::ModuleBinding(binding_idx)),
4234            ));
4235            // Patch StoreModuleBinding → StoreModuleBindingTyped for width-typed bindings
4236            if let Some(type_name) = self
4237                .type_tracker
4238                .get_binding_type(binding_idx)
4239                .and_then(|info| info.type_name.as_deref())
4240            {
4241                if let Some(w) = shape_ast::IntWidth::from_name(type_name) {
4242                    if let Some(last) = self.program.instructions.last_mut() {
4243                        if last.opcode == OpCode::StoreModuleBinding {
4244                            last.opcode = OpCode::StoreModuleBindingTyped;
4245                            last.operand = Some(Operand::TypedModuleBinding(
4246                                binding_idx,
4247                                crate::bytecode::NumericWidth::from_int_width(w),
4248                            ));
4249                        }
4250                    }
4251                }
4252            }
4253        }
4254        Ok(())
4255    }
4256
4257    pub(super) fn classify_builtin_function(&self, name: &str) -> Option<BuiltinNameResolution> {
4258        let builtin = match name {
4259            // Option type constructor
4260            "Some" => BuiltinFunction::SomeCtor,
4261            "Ok" => BuiltinFunction::OkCtor,
4262            "Err" => BuiltinFunction::ErrCtor,
4263            "HashMap" => BuiltinFunction::HashMapCtor,
4264            "Set" => BuiltinFunction::SetCtor,
4265            "Deque" => BuiltinFunction::DequeCtor,
4266            "PriorityQueue" => BuiltinFunction::PriorityQueueCtor,
4267            "Mutex" => BuiltinFunction::MutexCtor,
4268            "Atomic" => BuiltinFunction::AtomicCtor,
4269            "Lazy" => BuiltinFunction::LazyCtor,
4270            "Channel" => BuiltinFunction::ChannelCtor,
4271            // Json navigation helpers
4272            "__json_object_get" => BuiltinFunction::JsonObjectGet,
4273            "__json_array_at" => BuiltinFunction::JsonArrayAt,
4274            "__json_object_keys" => BuiltinFunction::JsonObjectKeys,
4275            "__json_array_len" => BuiltinFunction::JsonArrayLen,
4276            "__json_object_len" => BuiltinFunction::JsonObjectLen,
4277            "__intrinsic_vec_abs" => BuiltinFunction::IntrinsicVecAbs,
4278            "__intrinsic_vec_sqrt" => BuiltinFunction::IntrinsicVecSqrt,
4279            "__intrinsic_vec_ln" => BuiltinFunction::IntrinsicVecLn,
4280            "__intrinsic_vec_exp" => BuiltinFunction::IntrinsicVecExp,
4281            "__intrinsic_vec_add" => BuiltinFunction::IntrinsicVecAdd,
4282            "__intrinsic_vec_sub" => BuiltinFunction::IntrinsicVecSub,
4283            "__intrinsic_vec_mul" => BuiltinFunction::IntrinsicVecMul,
4284            "__intrinsic_vec_div" => BuiltinFunction::IntrinsicVecDiv,
4285            "__intrinsic_vec_max" => BuiltinFunction::IntrinsicVecMax,
4286            "__intrinsic_vec_min" => BuiltinFunction::IntrinsicVecMin,
4287            "__intrinsic_vec_select" => BuiltinFunction::IntrinsicVecSelect,
4288            "__intrinsic_matmul_vec" => BuiltinFunction::IntrinsicMatMulVec,
4289            "__intrinsic_matmul_mat" => BuiltinFunction::IntrinsicMatMulMat,
4290            // R5.4D: unwired intrinsics for Matrix/Vec arithmetic retarget.
4291            "__intrinsic_vec_add_i64" => BuiltinFunction::IntrinsicVecAddI64,
4292            "__intrinsic_mat_add" => BuiltinFunction::IntrinsicMatAdd,
4293            "__intrinsic_mat_sub" => BuiltinFunction::IntrinsicMatSub,
4294
4295            // Existing builtins
4296            "abs" => BuiltinFunction::Abs,
4297            "min" => BuiltinFunction::Min,
4298            "max" => BuiltinFunction::Max,
4299            "sqrt" => BuiltinFunction::Sqrt,
4300            "ln" => BuiltinFunction::Ln,
4301            "pow" => BuiltinFunction::Pow,
4302            "exp" => BuiltinFunction::Exp,
4303            "log" => BuiltinFunction::Log,
4304            "floor" => BuiltinFunction::Floor,
4305            "ceil" => BuiltinFunction::Ceil,
4306            "round" => BuiltinFunction::Round,
4307            "sin" => BuiltinFunction::Sin,
4308            "cos" => BuiltinFunction::Cos,
4309            "tan" => BuiltinFunction::Tan,
4310            "asin" => BuiltinFunction::Asin,
4311            "acos" => BuiltinFunction::Acos,
4312            "atan" => BuiltinFunction::Atan,
4313            "stddev" => BuiltinFunction::StdDev,
4314            "__intrinsic_map" => BuiltinFunction::Map,
4315            "__intrinsic_filter" => BuiltinFunction::Filter,
4316            "__intrinsic_reduce" => BuiltinFunction::Reduce,
4317            "print" => BuiltinFunction::Print,
4318            "format" => BuiltinFunction::Format,
4319            // "len" and "count" removed: use x.len() method form via per-type
4320            // PHF dispatch (ARRAY_METHODS, HASHMAP_METHODS, STRING_METHODS, ...)
4321            // "throw" removed: Shape uses Result types
4322            "__intrinsic_snapshot" | "snapshot" => BuiltinFunction::Snapshot,
4323            "exit" => BuiltinFunction::Exit,
4324            "range" => BuiltinFunction::Range,
4325            "is_number" | "isNumber" => BuiltinFunction::IsNumber,
4326            "is_string" | "isString" => BuiltinFunction::IsString,
4327            "is_bool" | "isBool" => BuiltinFunction::IsBool,
4328            "is_array" | "isArray" => BuiltinFunction::IsArray,
4329            "is_object" | "isObject" => BuiltinFunction::IsObject,
4330            "is_data_row" | "isDataRow" => BuiltinFunction::IsDataRow,
4331            "to_string" | "toString" => BuiltinFunction::ToString,
4332            "to_number" | "toNumber" => BuiltinFunction::ToNumber,
4333            "to_bool" | "toBool" => BuiltinFunction::ToBool,
4334            // __into_*/__try_into_* builtins removed — primitive conversions now use
4335            // typed ConvertTo*/TryConvertTo* opcodes emitted directly by the compiler.
4336            "__native_ptr_size" => BuiltinFunction::NativePtrSize,
4337            "__native_ptr_new_cell" => BuiltinFunction::NativePtrNewCell,
4338            "__native_ptr_free_cell" => BuiltinFunction::NativePtrFreeCell,
4339            "__native_ptr_read_ptr" => BuiltinFunction::NativePtrReadPtr,
4340            "__native_ptr_write_ptr" => BuiltinFunction::NativePtrWritePtr,
4341            "__native_table_from_arrow_c" => BuiltinFunction::NativeTableFromArrowC,
4342            "__native_table_from_arrow_c_typed" => BuiltinFunction::NativeTableFromArrowCTyped,
4343            "__native_table_bind_type" => BuiltinFunction::NativeTableBindType,
4344            "fold" => BuiltinFunction::ControlFold,
4345
4346            // Math intrinsics
4347            // W12-stdlib-intrinsic-collapse (Wave-2-Agent-G, 2026-05-14):
4348            // `__intrinsic_sum` deleted. Stdlib `pub fn sum(series)` now
4349            // routes via PHF method dispatch (`series.sum()`) — ADR-005 §1.
4350            "__intrinsic_minimize" => BuiltinFunction::IntrinsicMinimize,
4351            "__intrinsic_bspline2_3d_batch" => BuiltinFunction::IntrinsicBspline2_3dBatch,
4352            "__intrinsic_mean" => BuiltinFunction::IntrinsicMean,
4353            "__intrinsic_min" => BuiltinFunction::IntrinsicMin,
4354            "__intrinsic_max" => BuiltinFunction::IntrinsicMax,
4355            "__intrinsic_std" => BuiltinFunction::IntrinsicStd,
4356            "__intrinsic_variance" => BuiltinFunction::IntrinsicVariance,
4357
4358            // Random intrinsics
4359            "__intrinsic_random" => BuiltinFunction::IntrinsicRandom,
4360            "__intrinsic_random_int" => BuiltinFunction::IntrinsicRandomInt,
4361            "__intrinsic_random_seed" => BuiltinFunction::IntrinsicRandomSeed,
4362            "__intrinsic_random_normal" => BuiltinFunction::IntrinsicRandomNormal,
4363            "__intrinsic_random_array" => BuiltinFunction::IntrinsicRandomArray,
4364
4365            // Distribution intrinsics
4366            "__intrinsic_dist_uniform" => BuiltinFunction::IntrinsicDistUniform,
4367            "__intrinsic_dist_lognormal" => BuiltinFunction::IntrinsicDistLognormal,
4368            "__intrinsic_dist_exponential" => BuiltinFunction::IntrinsicDistExponential,
4369            "__intrinsic_dist_poisson" => BuiltinFunction::IntrinsicDistPoisson,
4370            "__intrinsic_dist_sample_n" => BuiltinFunction::IntrinsicDistSampleN,
4371
4372            // Stochastic process intrinsics
4373            "__intrinsic_brownian_motion" => BuiltinFunction::IntrinsicBrownianMotion,
4374            "__intrinsic_gbm" => BuiltinFunction::IntrinsicGbm,
4375            "__intrinsic_ou_process" => BuiltinFunction::IntrinsicOuProcess,
4376            "__intrinsic_random_walk" => BuiltinFunction::IntrinsicRandomWalk,
4377
4378            // Rolling intrinsics
4379            "__intrinsic_rolling_sum" => BuiltinFunction::IntrinsicRollingSum,
4380            "__intrinsic_rolling_mean" => BuiltinFunction::IntrinsicRollingMean,
4381            "__intrinsic_rolling_std" => BuiltinFunction::IntrinsicRollingStd,
4382            "__intrinsic_rolling_min" => BuiltinFunction::IntrinsicRollingMin,
4383            "__intrinsic_rolling_max" => BuiltinFunction::IntrinsicRollingMax,
4384            "__intrinsic_ema" => BuiltinFunction::IntrinsicEma,
4385            "__intrinsic_linear_recurrence" => BuiltinFunction::IntrinsicLinearRecurrence,
4386
4387            // Series intrinsics
4388            "__intrinsic_shift" => BuiltinFunction::IntrinsicShift,
4389            "__intrinsic_diff" => BuiltinFunction::IntrinsicDiff,
4390            "__intrinsic_pct_change" => BuiltinFunction::IntrinsicPctChange,
4391            "__intrinsic_fillna" => BuiltinFunction::IntrinsicFillna,
4392            "__intrinsic_cumsum" => BuiltinFunction::IntrinsicCumsum,
4393            "__intrinsic_cumprod" => BuiltinFunction::IntrinsicCumprod,
4394            "__intrinsic_clip" => BuiltinFunction::IntrinsicClip,
4395
4396            // Trigonometric intrinsics (map __intrinsic_ forms to existing builtins)
4397            "__intrinsic_sin" => BuiltinFunction::Sin,
4398            "__intrinsic_cos" => BuiltinFunction::Cos,
4399            "__intrinsic_tan" => BuiltinFunction::Tan,
4400            "__intrinsic_asin" => BuiltinFunction::Asin,
4401            "__intrinsic_acos" => BuiltinFunction::Acos,
4402            "__intrinsic_atan" => BuiltinFunction::Atan,
4403            "__intrinsic_atan2" => BuiltinFunction::IntrinsicAtan2,
4404            "__intrinsic_sinh" => BuiltinFunction::IntrinsicSinh,
4405            "__intrinsic_cosh" => BuiltinFunction::IntrinsicCosh,
4406            "__intrinsic_tanh" => BuiltinFunction::IntrinsicTanh,
4407
4408            // Statistical intrinsics
4409            "__intrinsic_correlation" => BuiltinFunction::IntrinsicCorrelation,
4410            "__intrinsic_covariance" => BuiltinFunction::IntrinsicCovariance,
4411            "__intrinsic_percentile" => BuiltinFunction::IntrinsicPercentile,
4412            "__intrinsic_median" => BuiltinFunction::IntrinsicMedian,
4413
4414            // Character code intrinsics
4415            "__intrinsic_char_code" => BuiltinFunction::IntrinsicCharCode,
4416            "__intrinsic_from_char_code" => BuiltinFunction::IntrinsicFromCharCode,
4417
4418            // Series access
4419            "__intrinsic_series" => BuiltinFunction::IntrinsicSeries,
4420
4421            // Reflection
4422            "reflect" => BuiltinFunction::Reflect,
4423
4424            // Additional math builtins
4425            "sign" => BuiltinFunction::Sign,
4426            "gcd" => BuiltinFunction::Gcd,
4427            "lcm" => BuiltinFunction::Lcm,
4428            "hypot" => BuiltinFunction::Hypot,
4429            "clamp" => BuiltinFunction::Clamp,
4430            "isNaN" | "is_nan" => BuiltinFunction::IsNaN,
4431            "isFinite" | "is_finite" => BuiltinFunction::IsFinite,
4432            "mat" => BuiltinFunction::MatFromFlat,
4433            _ => return None,
4434        };
4435
4436        let scope = match name {
4437            "Some" | "Ok" | "Err" => ResolutionScope::TypeAssociated,
4438            "print" => ResolutionScope::Prelude,
4439            _ if Self::is_internal_intrinsic_name(name) => ResolutionScope::InternalIntrinsic,
4440            _ => ResolutionScope::ModuleBinding,
4441        };
4442
4443        Some(match scope {
4444            ResolutionScope::InternalIntrinsic => {
4445                BuiltinNameResolution::InternalOnly { builtin, scope }
4446            }
4447            _ => BuiltinNameResolution::Surface { builtin, scope },
4448        })
4449    }
4450
4451    pub(super) fn is_internal_intrinsic_name(name: &str) -> bool {
4452        name.starts_with("__native_")
4453            || name.starts_with("__intrinsic_")
4454            || name.starts_with("__json_")
4455    }
4456
4457    pub(super) const fn variable_scope_summary() -> &'static str {
4458        "Variable names resolve from local scope and module scope."
4459    }
4460
4461    pub(super) const fn function_scope_summary() -> &'static str {
4462        "Function names resolve from module scope, explicit imports, type-associated scope, and the implicit prelude."
4463    }
4464
4465    pub(super) fn undefined_variable_message(&self, name: &str) -> String {
4466        format!(
4467            "Undefined variable: {}. {}",
4468            name,
4469            Self::variable_scope_summary()
4470        )
4471    }
4472
4473    pub(super) fn undefined_function_message(&self, name: &str) -> String {
4474        format!(
4475            "Undefined function: {}. {}",
4476            name,
4477            Self::function_scope_summary()
4478        )
4479    }
4480
4481    pub(super) fn internal_intrinsic_error_message(
4482        &self,
4483        name: &str,
4484        resolution: BuiltinNameResolution,
4485    ) -> String {
4486        format!(
4487            "'{}' resolves to {} and is not available from ordinary user code. Internal intrinsics are reserved for std::* implementations and compiler-generated code.",
4488            name,
4489            resolution.scope().label()
4490        )
4491    }
4492
4493    /// Check if a builtin function requires arg count
4494    pub(super) fn builtin_requires_arg_count(&self, builtin: BuiltinFunction) -> bool {
4495        matches!(
4496            builtin,
4497            BuiltinFunction::Abs
4498                | BuiltinFunction::Min
4499                | BuiltinFunction::Max
4500                | BuiltinFunction::Sqrt
4501                | BuiltinFunction::Ln
4502                | BuiltinFunction::Pow
4503                | BuiltinFunction::Exp
4504                | BuiltinFunction::Log
4505                | BuiltinFunction::Floor
4506                | BuiltinFunction::Ceil
4507                | BuiltinFunction::Round
4508                | BuiltinFunction::Sin
4509                | BuiltinFunction::Cos
4510                | BuiltinFunction::Tan
4511                | BuiltinFunction::Asin
4512                | BuiltinFunction::Acos
4513                | BuiltinFunction::Atan
4514                | BuiltinFunction::StdDev
4515                | BuiltinFunction::Range
4516                | BuiltinFunction::Slice
4517                | BuiltinFunction::Push
4518                | BuiltinFunction::Pop
4519                | BuiltinFunction::First
4520                | BuiltinFunction::Last
4521                | BuiltinFunction::Zip
4522                | BuiltinFunction::Map
4523                | BuiltinFunction::Filter
4524                | BuiltinFunction::Reduce
4525                | BuiltinFunction::ForEach
4526                | BuiltinFunction::Find
4527                | BuiltinFunction::FindIndex
4528                | BuiltinFunction::Some
4529                | BuiltinFunction::Every
4530                | BuiltinFunction::SomeCtor
4531                | BuiltinFunction::OkCtor
4532                | BuiltinFunction::ErrCtor
4533                | BuiltinFunction::HashMapCtor
4534                | BuiltinFunction::SetCtor
4535                | BuiltinFunction::DequeCtor
4536                | BuiltinFunction::PriorityQueueCtor
4537                | BuiltinFunction::MutexCtor
4538                | BuiltinFunction::AtomicCtor
4539                | BuiltinFunction::LazyCtor
4540                | BuiltinFunction::ChannelCtor
4541                | BuiltinFunction::Print
4542                | BuiltinFunction::Format
4543                // BuiltinFunction::Len removed
4544                // BuiltinFunction::Throw removed
4545                | BuiltinFunction::Snapshot
4546                | BuiltinFunction::ObjectRest
4547                | BuiltinFunction::IsNumber
4548                | BuiltinFunction::IsString
4549                | BuiltinFunction::IsBool
4550                | BuiltinFunction::IsArray
4551                | BuiltinFunction::IsObject
4552                | BuiltinFunction::IsDataRow
4553                | BuiltinFunction::ToString
4554                | BuiltinFunction::ToNumber
4555                | BuiltinFunction::ToBool
4556                | BuiltinFunction::NativePtrSize
4557                | BuiltinFunction::NativePtrNewCell
4558                | BuiltinFunction::NativePtrFreeCell
4559                | BuiltinFunction::NativePtrReadPtr
4560                | BuiltinFunction::NativePtrWritePtr
4561                | BuiltinFunction::NativeTableFromArrowC
4562                | BuiltinFunction::NativeTableFromArrowCTyped
4563                | BuiltinFunction::NativeTableBindType
4564                | BuiltinFunction::ControlFold
4565                | BuiltinFunction::IntrinsicMinimize
4566                | BuiltinFunction::IntrinsicBspline2_3dBatch
4567                | BuiltinFunction::IntrinsicMean
4568                | BuiltinFunction::IntrinsicMin
4569                | BuiltinFunction::IntrinsicMax
4570                | BuiltinFunction::IntrinsicStd
4571                | BuiltinFunction::IntrinsicVariance
4572                | BuiltinFunction::IntrinsicRandom
4573                | BuiltinFunction::IntrinsicRandomInt
4574                | BuiltinFunction::IntrinsicRandomSeed
4575                | BuiltinFunction::IntrinsicRandomNormal
4576                | BuiltinFunction::IntrinsicRandomArray
4577                | BuiltinFunction::IntrinsicDistUniform
4578                | BuiltinFunction::IntrinsicDistLognormal
4579                | BuiltinFunction::IntrinsicDistExponential
4580                | BuiltinFunction::IntrinsicDistPoisson
4581                | BuiltinFunction::IntrinsicDistSampleN
4582                | BuiltinFunction::IntrinsicBrownianMotion
4583                | BuiltinFunction::IntrinsicGbm
4584                | BuiltinFunction::IntrinsicOuProcess
4585                | BuiltinFunction::IntrinsicRandomWalk
4586                | BuiltinFunction::IntrinsicRollingSum
4587                | BuiltinFunction::IntrinsicRollingMean
4588                | BuiltinFunction::IntrinsicRollingStd
4589                | BuiltinFunction::IntrinsicRollingMin
4590                | BuiltinFunction::IntrinsicRollingMax
4591                | BuiltinFunction::IntrinsicEma
4592                | BuiltinFunction::IntrinsicLinearRecurrence
4593                | BuiltinFunction::IntrinsicShift
4594                | BuiltinFunction::IntrinsicDiff
4595                | BuiltinFunction::IntrinsicPctChange
4596                | BuiltinFunction::IntrinsicFillna
4597                | BuiltinFunction::IntrinsicCumsum
4598                | BuiltinFunction::IntrinsicCumprod
4599                | BuiltinFunction::IntrinsicClip
4600                | BuiltinFunction::IntrinsicCorrelation
4601                | BuiltinFunction::IntrinsicCovariance
4602                | BuiltinFunction::IntrinsicPercentile
4603                | BuiltinFunction::IntrinsicMedian
4604                | BuiltinFunction::IntrinsicAtan2
4605                | BuiltinFunction::IntrinsicSinh
4606                | BuiltinFunction::IntrinsicCosh
4607                | BuiltinFunction::IntrinsicTanh
4608                | BuiltinFunction::IntrinsicCharCode
4609                | BuiltinFunction::IntrinsicFromCharCode
4610                | BuiltinFunction::IntrinsicSeries
4611                | BuiltinFunction::IntrinsicVecAbs
4612                | BuiltinFunction::IntrinsicVecSqrt
4613                | BuiltinFunction::IntrinsicVecLn
4614                | BuiltinFunction::IntrinsicVecExp
4615                | BuiltinFunction::IntrinsicVecAdd
4616                | BuiltinFunction::IntrinsicVecSub
4617                | BuiltinFunction::IntrinsicVecMul
4618                | BuiltinFunction::IntrinsicVecDiv
4619                | BuiltinFunction::IntrinsicVecMax
4620                | BuiltinFunction::IntrinsicVecMin
4621                | BuiltinFunction::IntrinsicVecSelect
4622                | BuiltinFunction::IntrinsicVecAddI64
4623                | BuiltinFunction::IntrinsicMatMulVec
4624                | BuiltinFunction::IntrinsicMatMulMat
4625                | BuiltinFunction::IntrinsicMatAdd
4626                | BuiltinFunction::IntrinsicMatSub
4627                | BuiltinFunction::Sign
4628                | BuiltinFunction::Gcd
4629                | BuiltinFunction::Lcm
4630                | BuiltinFunction::Hypot
4631                | BuiltinFunction::Clamp
4632                | BuiltinFunction::IsNaN
4633                | BuiltinFunction::IsFinite
4634                | BuiltinFunction::MatFromFlat
4635        )
4636    }
4637
4638    /// Check if any compiled function exists whose name indicates a user-defined
4639    /// override of the given method name (via extend blocks or impl blocks).
4640    ///
4641    /// Looks for function names like `Type.method` or `Type::method`.
4642    pub(super) fn has_any_user_defined_method(&self, method: &str) -> bool {
4643        let dot_suffix = format!(".{}", method);
4644        let colon_suffix = format!("::{}", method);
4645        self.program
4646            .functions
4647            .iter()
4648            .any(|f| f.name.ends_with(&dot_suffix) || f.name.ends_with(&colon_suffix))
4649    }
4650
4651    /// Check if a method name is a known built-in method on any VM type.
4652    /// Used by UFCS to determine if `receiver.method(args)` should be dispatched
4653    /// as a built-in method call or rewritten to `method(receiver, args)`.
4654    pub(super) fn is_known_builtin_method(method: &str) -> bool {
4655        // Array methods (from ARRAY_METHODS PHF map)
4656        matches!(method,
4657            "map" | "filter" | "reduce" | "forEach" | "find" | "findIndex"
4658            | "some" | "every" | "sort" | "groupBy" | "flatMap"
4659            | "len" | "length" | "first" | "last" | "reverse" | "slice"
4660            | "concat" | "take" | "drop" | "skip"
4661            | "indexOf" | "includes"
4662            | "join" | "flatten" | "unique" | "distinct" | "distinctBy"
4663            | "sum" | "avg" | "min" | "max" | "count"
4664            | "where" | "select" | "orderBy" | "thenBy" | "takeWhile"
4665            | "skipWhile" | "single" | "any" | "all"
4666            | "innerJoin" | "leftJoin" | "crossJoin"
4667            | "union" | "intersect" | "except"
4668        )
4669        // DataTable methods (from DATATABLE_METHODS PHF map)
4670        || matches!(method,
4671            "columns" | "column" | "head" | "tail" | "mean" | "std"
4672            | "describe" | "aggregate" | "group_by" | "index_by" | "indexBy"
4673            | "simulate" | "toMat" | "to_mat"
4674        )
4675        // (W15-column, 2026-05-10) `Column` value-type methods deleted
4676        // per ADR-006 §2.7.21 / Q22. `toArray` survives as a
4677        // TypedArray-shape method (Array.toArray() identity); kept here
4678        // because the predicate is a name-set, not a receiver-kind
4679        // classifier — `toArray` is reachable on TypedArray receivers
4680        // through `ARRAY_METHODS`.
4681        || matches!(method, "toArray")
4682        // IndexedTable methods (from INDEXED_TABLE_METHODS PHF map)
4683        || matches!(method, "resample" | "between")
4684        // Number methods handled inline in op_call_method
4685        || matches!(method,
4686            "toFixed" | "toInt" | "toNumber" | "to_number" | "floor" | "ceil" | "round"
4687            | "abs" | "sign" | "clamp"
4688        )
4689        // String methods handled inline
4690        || matches!(method,
4691            "toUpperCase" | "toLowerCase" | "trim" | "contains" | "startsWith"
4692            | "endsWith" | "split" | "replace" | "substring" | "charAt"
4693            | "padStart" | "padEnd" | "repeat" | "toString"
4694        )
4695        // Object methods handled by handle_object_method
4696        || matches!(method, "keys" | "values" | "has" | "get" | "set" | "len")
4697        // DateTime methods (from DATETIME_METHODS PHF map)
4698        || matches!(method, "format")
4699        // Universal intrinsic methods
4700        || matches!(method, "type")
4701    }
4702
4703    /// Try to track a `Table<T>` type annotation as a DataTable variable.
4704    ///
4705    /// If the annotation is `Generic { name: "Table", args: [Reference(T)] }`,
4706    /// looks up T's schema and marks the variable as `is_datatable`.
4707    pub(super) fn try_track_datatable_type(
4708        &mut self,
4709        type_ann: &shape_ast::ast::TypeAnnotation,
4710        slot: u16,
4711        is_local: bool,
4712    ) -> shape_ast::error::Result<()> {
4713        use shape_ast::ast::TypeAnnotation;
4714        if let TypeAnnotation::Generic { name, args } = type_ann {
4715            if name == "Table" && args.len() == 1 {
4716                let inner_name = match &args[0] {
4717                    TypeAnnotation::Reference(t) => Some(t.as_str()),
4718                    TypeAnnotation::Basic(t) => Some(t.as_str()),
4719                    _ => None,
4720                };
4721                if let Some(type_name) = inner_name {
4722                    let schema_id = self
4723                        .type_tracker
4724                        .schema_registry()
4725                        .get(type_name)
4726                        .map(|s| s.id);
4727                    if let Some(sid) = schema_id {
4728                        let info = crate::type_tracking::VariableTypeInfo::datatable(
4729                            sid,
4730                            type_name.to_string(),
4731                        );
4732                        if is_local {
4733                            self.type_tracker.set_local_type(slot, info);
4734                        } else {
4735                            self.type_tracker.set_binding_type(slot, info);
4736                        }
4737                    } else if type_name.len() == 1
4738                        && type_name
4739                            .chars()
4740                            .next()
4741                            .map_or(false, |c| c.is_ascii_uppercase())
4742                    {
4743                        // Generic type parameter (e.g., T) — skip DataTable tracking,
4744                        // the concrete type will be determined at the call site.
4745                    } else {
4746                        return Err(shape_ast::error::ShapeError::SemanticError {
4747                            message: format!(
4748                                "Unknown type '{}' in Table<{}> annotation",
4749                                type_name, type_name
4750                            ),
4751                            location: None,
4752                        });
4753                    }
4754                }
4755            }
4756        }
4757        Ok(())
4758    }
4759
4760    /// Check if a variable is a RowView (typed row from Arrow DataTable).
4761    pub(super) fn is_row_view_variable(&self, name: &str) -> bool {
4762        if let Some(local_idx) = self.resolve_local(name) {
4763            if let Some(info) = self.type_tracker.get_local_type(local_idx) {
4764                return info.is_row_view();
4765            }
4766        }
4767        if let Some(&binding_idx) = self.module_bindings.get(name) {
4768            if let Some(info) = self.type_tracker.get_binding_type(binding_idx) {
4769                return info.is_row_view();
4770            }
4771        }
4772        false
4773    }
4774
4775    /// Get the available field names for a RowView variable's schema.
4776    pub(super) fn get_row_view_field_names(&self, name: &str) -> Option<Vec<String>> {
4777        let type_name = if let Some(local_idx) = self.resolve_local(name) {
4778            self.type_tracker
4779                .get_local_type(local_idx)
4780                .and_then(|info| {
4781                    if info.is_row_view() {
4782                        info.type_name.clone()
4783                    } else {
4784                        None
4785                    }
4786                })
4787        } else if let Some(&binding_idx) = self.module_bindings.get(name) {
4788            self.type_tracker
4789                .get_binding_type(binding_idx)
4790                .and_then(|info| {
4791                    if info.is_row_view() {
4792                        info.type_name.clone()
4793                    } else {
4794                        None
4795                    }
4796                })
4797        } else {
4798            None
4799        };
4800
4801        if let Some(tn) = type_name {
4802            if let Some(schema) = self.type_tracker.schema_registry().get(&tn) {
4803                return Some(schema.field_names().map(|n| n.to_string()).collect());
4804            }
4805        }
4806        None
4807    }
4808
4809    /// Try to resolve a property access on a RowView variable to a column ID.
4810    ///
4811    /// Returns `Some(col_id)` if the variable is a tracked RowView and the field
4812    /// exists in its schema. Returns `None` if the variable isn't a RowView or
4813    /// the field is unknown (caller should emit a compile-time error).
4814    pub(super) fn try_resolve_row_view_column(
4815        &self,
4816        var_name: &str,
4817        field_name: &str,
4818    ) -> Option<u32> {
4819        // Check locals first, then module_bindings
4820        if let Some(local_idx) = self.resolve_local(var_name) {
4821            return self
4822                .type_tracker
4823                .get_row_view_column_id(local_idx, true, field_name);
4824        }
4825        if let Some(&binding_idx) = self.module_bindings.get(var_name) {
4826            return self
4827                .type_tracker
4828                .get_row_view_column_id(binding_idx, false, field_name);
4829        }
4830        None
4831    }
4832
4833    /// Determine the appropriate LoadCol opcode for a RowView field.
4834    ///
4835    /// Looks up the field's FieldType and maps it to the corresponding opcode.
4836    /// Falls back to LoadColF64 if the type can't be determined.
4837    pub(super) fn row_view_field_opcode(&self, var_name: &str, field_name: &str) -> OpCode {
4838        use shape_runtime::type_schema::FieldType;
4839
4840        let type_name = if let Some(local_idx) = self.resolve_local(var_name) {
4841            self.type_tracker
4842                .get_local_type(local_idx)
4843                .and_then(|info| info.type_name.clone())
4844        } else if let Some(&binding_idx) = self.module_bindings.get(var_name) {
4845            self.type_tracker
4846                .get_binding_type(binding_idx)
4847                .and_then(|info| info.type_name.clone())
4848        } else {
4849            None
4850        };
4851
4852        if let Some(type_name) = type_name {
4853            if let Some(schema) = self.type_tracker.schema_registry().get(&type_name) {
4854                if let Some(field) = schema.get_field(field_name) {
4855                    return match field.field_type {
4856                        FieldType::F64 => OpCode::LoadColF64,
4857                        FieldType::I64 | FieldType::Timestamp => OpCode::LoadColI64,
4858                        FieldType::Bool => OpCode::LoadColBool,
4859                        FieldType::String => OpCode::LoadColStr,
4860                        _ => OpCode::LoadColF64, // default
4861                    };
4862                }
4863            }
4864        }
4865        OpCode::LoadColF64 // default
4866    }
4867
4868    /// Resolve the NumericType for a RowView field (used for typed opcode emission).
4869    pub(super) fn resolve_row_view_field_numeric_type(
4870        &self,
4871        var_name: &str,
4872        field_name: &str,
4873    ) -> Option<crate::type_tracking::NumericType> {
4874        use crate::type_tracking::NumericType;
4875        use shape_runtime::type_schema::FieldType;
4876
4877        let type_name = if let Some(local_idx) = self.resolve_local(var_name) {
4878            self.type_tracker
4879                .get_local_type(local_idx)
4880                .and_then(|info| info.type_name.clone())
4881        } else if let Some(&binding_idx) = self.module_bindings.get(var_name) {
4882            self.type_tracker
4883                .get_binding_type(binding_idx)
4884                .and_then(|info| info.type_name.clone())
4885        } else {
4886            None
4887        };
4888
4889        if let Some(type_name) = type_name {
4890            if let Some(schema) = self.type_tracker.schema_registry().get(&type_name) {
4891                if let Some(field) = schema.get_field(field_name) {
4892                    return match field.field_type {
4893                        FieldType::F64 => Some(NumericType::Number),
4894                        FieldType::I64 | FieldType::Timestamp => Some(NumericType::Int),
4895                        FieldType::Decimal => Some(NumericType::Decimal),
4896                        _ => None,
4897                    };
4898                }
4899            }
4900        }
4901        None
4902    }
4903
4904    /// Convert a TypeAnnotation to a FieldType for TypeSchema registration.
4905    ///
4906    /// v0.3 Phase 4b Round 5b W17.2-C (audit §4.D.7 + §4.D.8 + §9.B.3
4907    /// supervisor ratify 2026-05-19). Two hardening edges per the audit:
4908    ///
4909    /// **§4.D.7 — TRANSITIONAL 4-name generic-container narrowing.**
4910    /// `helpers.rs:4901` previously routed five generic containers
4911    /// (`HashMap` / `Map` / `Result` / `Option` / `Set`) to `FieldType::Any`.
4912    /// `Option<T>` now PROPAGATES through the
4913    /// `FieldType::Option(Box<FieldType>)` variant landed at W17.2-B
4914    /// (close commit `e316e171`; ADR-006 §2.7.5 producer-side stamp). The
4915    /// other four (`HashMap` / `Map` / `Result` / `Set`) retain the
4916    /// TRANSITIONAL fallback per the supervisor §9.B.3 ratify; per-container
4917    /// `FieldType` variant expansion is v0.4 W17.3/W17.4 territory.
4918    ///
4919    /// **§4.D.8 — `_ =>` fall-through replaced with explicit per-variant
4920    /// arms.** The `TypeAnnotation` shapes that don't lower to a concrete
4921    /// `FieldType` (`Function` / `Union` / `Intersection` / `Tuple` /
4922    /// inline `Object` / `Void` / `Never` / `Null` / `Undefined` / `Dyn`)
4923    /// each get an explicit, audit-cited arm. The arms project to
4924    /// `FieldType::Any` (preserving the historical behavior for
4925    /// inference-tier intermediate use), AND the post-inference verify
4926    /// pass at `compiler/post_inference_verify.rs` surfaces E0900 if any
4927    /// such `FieldType::Any` reaches a user-facing schema. Per audit §4.D.2
4928    /// same-pattern discipline + the §6 CLAUDE.md extension: explicit
4929    /// enumeration of every variant is the §4.D.8 ERROR disposition
4930    /// realized as named-and-cited arms (the catch-all `_` is the
4931    /// defection-attractor; explicit naming + verification-pass catch
4932    /// is the discipline).
4933    pub(super) fn type_annotation_to_field_type(
4934        ann: &shape_ast::ast::TypeAnnotation,
4935    ) -> shape_runtime::type_schema::FieldType {
4936        use shape_ast::ast::TypeAnnotation;
4937        use shape_runtime::type_schema::FieldType;
4938        match ann {
4939            TypeAnnotation::Basic(s) => match s.as_str() {
4940                "number" | "float" | "f64" | "f32" => FieldType::F64,
4941                "i8" => FieldType::I8,
4942                "u8" => FieldType::U8,
4943                "i16" => FieldType::I16,
4944                "u16" => FieldType::U16,
4945                "i32" => FieldType::I32,
4946                "u32" => FieldType::U32,
4947                "u64" => FieldType::U64,
4948                "int" | "i64" | "integer" | "isize" | "usize" | "byte" | "char" => FieldType::I64,
4949                "string" | "str" => FieldType::String,
4950                "decimal" => FieldType::Decimal,
4951                "bool" | "boolean" => FieldType::Bool,
4952                "timestamp" => FieldType::Timestamp,
4953                // Non-primitive type names (e.g. "Server", "Inner") are nested
4954                // object references.  The parser emits Basic for `ident` matches
4955                // inside `basic_type`, so treat unknown names as Object references
4956                // to enable typed field access on nested structs.
4957                other => FieldType::Object(other.to_string()),
4958            },
4959            TypeAnnotation::Reference(s) => FieldType::Object(s.to_string()),
4960            TypeAnnotation::Array(inner) => {
4961                FieldType::Array(Box::new(Self::type_annotation_to_field_type(inner)))
4962            }
4963            TypeAnnotation::Generic { name, args } => match name.as_str() {
4964                // §4.D.7 + §9.B.3 supervisor ratify 2026-05-19: Option<T>
4965                // PROPAGATES through `FieldType::Option(Box<FieldType>)`
4966                // (W17.2-B close commit e316e171). Single-arg shape only;
4967                // malformed Option<...> annotations route through the
4968                // residual TRANSITIONAL fallback below.
4969                "Option" if args.len() == 1 => FieldType::Option(Box::new(
4970                    Self::type_annotation_to_field_type(&args[0]),
4971                )),
4972                // W17.3-4.1 (v0.3 Round 7, supervisor ratify 2026-05-22):
4973                // HashMap<K, V> and Map<K, V> PROPAGATE through the
4974                // `FieldType::HashMap { key, value }` variant introduced
4975                // at `type_schema::field_types.rs` per audit §3.A Option 1.
4976                // Replaces the W17.2-C §4.D.7 TRANSITIONAL `HashMap | Map`
4977                // → `FieldType::Any` fallback. Slot storage points to
4978                // `HeapKind::HashMap` (ordinal 17 — Stage C P1(b)
4979                // `HashMapKindedRef`); the schema-side variant carries
4980                // the static K/V FieldTypes for compile-time checking
4981                // (ADR-006 §2.7.5 producer-side stamp). Malformed
4982                // arities route through the residual fallback below.
4983                "HashMap" | "Map" if args.len() == 2 => FieldType::HashMap {
4984                    key: Box::new(Self::type_annotation_to_field_type(&args[0])),
4985                    value: Box::new(Self::type_annotation_to_field_type(&args[1])),
4986                },
4987                // W17.3-4.1: Set<T> PROPAGATES through
4988                // `FieldType::Set(Box<FieldType>)` per audit §3.A Option
4989                // 1. Slot storage points to `HeapKind::HashSet` (ordinal
4990                // 21 — Wave 13 W13-hashset-rebuild, already at HEAD; the
4991                // audit §6.A surface-and-stop is stale — HashSet exists).
4992                "Set" if args.len() == 1 => FieldType::Set(Box::new(
4993                    Self::type_annotation_to_field_type(&args[0]),
4994                )),
4995                // §4.D.7 RESIDUAL TRANSITIONAL fallback per §9.B.3
4996                // supervisor ratify 2026-05-19. `Result<T, E>` continues
4997                // to lower to `FieldType::Any` pending its own per-
4998                // container variant introduction (out of scope for
4999                // W17.3-4.1; the audit explicitly bounds W17.3-4 to
5000                // Array / HashMap / Set / Option). Malformed arities
5001                // for the otherwise-handled containers (e.g. `Option<>`,
5002                // `HashMap<K>`, `Set<>`) also land here.
5003                "HashMap" | "Map" | "Result" | "Set" | "Option" => FieldType::Any,
5004                // User-defined generic structs — preserve the type name
5005                other => FieldType::Object(other.to_string()),
5006            },
5007            // §4.D.8 — explicit per-variant arms replace the deleted
5008            // `_ => FieldType::Any` fall-through per audit §4.D.2
5009            // same-pattern discipline.
5010            //
5011            // Tuple types — not yet supported as struct field storage;
5012            // route to FieldType::Any at the intermediate-tier and let
5013            // post_inference_verify surface E0900 if reached at
5014            // user-facing schemas.
5015            TypeAnnotation::Tuple(_) => FieldType::Any,
5016            // Object inline annotation (e.g. `{ x: int }` as a field
5017            // type) — schema construction is at the surrounding caller's
5018            // scope, not here; intermediate-tier Any with verification-
5019            // pass safety net.
5020            TypeAnnotation::Object(_) => FieldType::Any,
5021            // Function annotation (e.g. `(int) => string`) — closures
5022            // stored as TypedObject fields require explicit closure-
5023            // dispatch contract registration (W17-typed-carrier territory).
5024            // Intermediate Any; verification-pass safety net.
5025            TypeAnnotation::Function { .. } => FieldType::Any,
5026            // Union / Intersection annotations — first-class union types
5027            // are W17.3/W17.4 territory (per audit §4.D.7 alternate
5028            // disposition); intermediate Any.
5029            TypeAnnotation::Union(_) => FieldType::Any,
5030            TypeAnnotation::Intersection(_) => FieldType::Any,
5031            // Void / Never don't have slot storage; intermediate Any
5032            // for inference-tier projection; verification-pass catches
5033            // if they reach user-facing schemas.
5034            TypeAnnotation::Void => FieldType::Any,
5035            TypeAnnotation::Never => FieldType::Any,
5036            // Null / Undefined map to the typed null sentinel at the
5037            // slot level; schema-side use is intermediate-only.
5038            TypeAnnotation::Null => FieldType::Any,
5039            TypeAnnotation::Undefined => FieldType::Any,
5040            // Dyn(Trait) — trait-object dispatch territory (W16.2-B
5041            // typed-object element kind); intermediate Any here, the
5042            // trait registry resolves the concrete dispatch downstream.
5043            TypeAnnotation::Dyn(_) => FieldType::Any,
5044        }
5045    }
5046
5047    /// Evaluate an annotation argument expression to a string representation.
5048    /// Only handles compile-time evaluable expressions (literals).
5049    pub(super) fn eval_annotation_arg(expr: &shape_ast::ast::Expr) -> Option<String> {
5050        use shape_ast::ast::{Expr, Literal};
5051        match expr {
5052            Expr::Literal(Literal::String(s), _) => Some(s.clone()),
5053            Expr::Literal(Literal::Number(n), _) => Some(n.to_string()),
5054            Expr::Literal(Literal::Int(i), _) => Some(i.to_string()),
5055            Expr::Literal(Literal::Bool(b), _) => Some(b.to_string()),
5056            _ => None,
5057        }
5058    }
5059
5060    /// Get the schema ID for a `Table<T>` type annotation, if applicable.
5061    ///
5062    /// Returns `Some(schema_id)` if the annotation is `Table<T>` and `T` is a registered
5063    /// TypeSchema. Returns `None` otherwise.
5064    pub(super) fn get_table_schema_id(
5065        &self,
5066        type_ann: &shape_ast::ast::TypeAnnotation,
5067    ) -> Option<u16> {
5068        use shape_ast::ast::TypeAnnotation;
5069        if let TypeAnnotation::Generic { name, args } = type_ann {
5070            if name == "Table" && args.len() == 1 {
5071                let inner_name = match &args[0] {
5072                    TypeAnnotation::Basic(t) => Some(t.as_str()),
5073                    TypeAnnotation::Reference(t) => Some(t.as_str()),
5074                    _ => None,
5075                };
5076                if let Some(type_name) = inner_name {
5077                    return self
5078                        .type_tracker
5079                        .schema_registry()
5080                        .get(type_name)
5081                        .map(|s| s.id as u16);
5082                }
5083            }
5084        }
5085        None
5086    }
5087
5088    // ===== Drop scope management =====
5089
5090    /// Push a new drop scope. Must be paired with pop_drop_scope().
5091    pub(super) fn push_drop_scope(&mut self) {
5092        self.drop_locals.push(Vec::new());
5093        // Phase V1.1C: parallel ownership-drop scope (heap-ref locals that
5094        // need a `DropLocal` opcode at scope exit when the flag is on).
5095        // Pushed in lockstep regardless of flag state; only the emission in
5096        // `pop_drop_scope` is gated.
5097        self.ownership_drop_locals.push(Vec::new());
5098        // Track A.1C.2: parallel shared-local drop scope (`var` locals
5099        // promoted to Arc<SharedCell> via AllocSharedLocal). Pushed in
5100        // lockstep with the other drop stacks; pop_drop_scope emits
5101        // DropSharedLocal for each entry.
5102        self.shared_drop_locals.push(Vec::new());
5103    }
5104
5105    /// Pop the current drop scope, emitting DropCall instructions for all
5106    /// tracked locals in reverse order.
5107    pub(super) fn pop_drop_scope(&mut self) -> Result<()> {
5108        // Phase V1.1C: when `SHAPE_V2_OWNERSHIP_MOVES` is on, emit an
5109        // ownership-aware `DropLocal` for each heap-ref local declared in
5110        // this scope — in reverse order — *before* the legacy `DropCall`
5111        // trait-invocation pass. Conservative: with the current
5112        // always-Clone read policy no local is ever poisoned by a Move, so
5113        // every tracked `UniqueHeap` local is still live at scope exit.
5114        // TODO: once MIR last-use information is threaded into read-side
5115        // emission (so `MoveLocal` can be emitted on terminal reads), the
5116        // tracker here must be updated to skip drops for moved-out slots.
5117        let ownership_locals = self.ownership_drop_locals.pop().unwrap_or_default();
5118        if ownership_moves_enabled() {
5119            for local_idx in ownership_locals.into_iter().rev() {
5120                // Phase V1.1C fix: a promoted slot (prior SharedCell wrap
5121                // pre-A.1C.2, or prior `AllocSharedLocal` post-A.1C.2)
5122                // holds a cell pointer, not an inline value. `DropLocal`
5123                // poisons the slot with `0u64` and breaks the legacy
5124                // `LoadLocal` + `DropCall` pass that immediately follows
5125                // (the Arc-refcount release is handled by the DropCall
5126                // pass for legacy boxed slots, and by DropSharedLocal
5127                // for A.1C.2-promoted Shared slots). Skip here.
5128                if self.slot_is_boxed(local_idx) || self.slot_is_shared(local_idx) {
5129                    continue;
5130                }
5131                // R8 W9 B3 Drop runtime fix: when the slot's type has a
5132                // user `impl Drop` impl, skip the `DropLocal` opcode. The
5133                // V1.1C `DropLocal` poisons the slot with the no-op Bool
5134                // sentinel; the subsequent `LoadLocal` + `DropCall` pair
5135                // (emitted below) would then read poisoned bits and call
5136                // the user `Drop::drop` method on a stale receiver,
5137                // surfacing as `MakeFieldRef base must reference a
5138                // TypedObject; got Bool` once `self.field` is accessed in
5139                // the drop body. The `DropCall` opcode's `pop_kinded` +
5140                // `call_function_with_nb_args` path already retires the
5141                // slot's heap share via the canonical kind-dispatch (see
5142                // `executor/trait_object_ops.rs::op_drop_call_impl`), so
5143                // the V1.1C ownership-aware release is redundant for
5144                // user-Drop slots — `DropCall` is the sole releaser.
5145                if self.local_drop_kind(local_idx).is_some() {
5146                    continue;
5147                }
5148                self.emit(Instruction::new(
5149                    OpCode::DropLocal,
5150                    Some(Operand::Local(local_idx)),
5151                ));
5152            }
5153        }
5154        // Track A.1C.2: emit DropSharedLocal for each shared-promoted slot
5155        // declared in this scope, in reverse order. The DropSharedLocal
5156        // handler reconstructs Arc::from_raw and drops it (one atomic
5157        // strong-count decrement) — this is the sole releaser for slots
5158        // promoted by AllocSharedLocal. Emitted BEFORE the legacy
5159        // DropCall pass so that by the time DropCall runs the slot has
5160        // been poisoned with NONE_BITS and no accidental re-read occurs.
5161        if let Some(shared_locals) = self.shared_drop_locals.pop() {
5162            for local_idx in shared_locals.into_iter().rev() {
5163                self.emit(Instruction::new(
5164                    OpCode::DropSharedLocal,
5165                    Some(Operand::Local(local_idx)),
5166                ));
5167            }
5168        }
5169        // Emit DropCall for each tracked local in reverse order
5170        if let Some(locals) = self.drop_locals.pop() {
5171            for (local_idx, is_async) in locals.into_iter().rev() {
5172                self.emit_drop_call_for_local(local_idx, is_async);
5173            }
5174        }
5175        Ok(())
5176    }
5177
5178    /// Phase V1.1C: record a local slot as needing an ownership-aware
5179    /// `DropLocal` at the next `pop_drop_scope`. Called from the compiler's
5180    /// variable-declaration path when the slot's MIR storage class is
5181    /// `UniqueHeap` (i.e. owned heap allocation that the ownership-moves
5182    /// runtime would otherwise leak). No-op when no drop scope is active.
5183    pub(super) fn track_ownership_drop_local(&mut self, local_idx: u16) {
5184        if let Some(scope) = self.ownership_drop_locals.last_mut() {
5185            scope.push(local_idx);
5186        }
5187    }
5188
5189    /// Phase V1.1C: true when the slot's storage hint matches an
5190    /// inline-scalar native type (int / number / bool / sized integers).
5191    /// Used to separate Box-promoted heap values from zero-cost inline
5192    /// values both for `DropLocal` emission at scope exit and for
5193    /// `CloneLocal` emission on reads.
5194    ///
5195    /// Per ADR-006 §2.7.5.1, `info.storage_hint` is itself
5196    /// `Option<StorageHint>`; an absent hint (slot's kind not yet
5197    /// proven) is treated as "not inline-scalar" — the same conservative
5198    /// answer the deleted `StorageHint::Unknown` sentinel produced.
5199    pub(super) fn slot_has_inline_scalar_hint(&self, local_idx: u16) -> bool {
5200        let Some(hint) = self
5201            .type_tracker
5202            .get_local_type(local_idx)
5203            .and_then(|info| info.storage_hint)
5204        else {
5205            return false;
5206        };
5207        hint.is_numeric_family() || matches!(hint, StorageHint::Bool)
5208    }
5209
5210    /// Phase V1.1C: true when the slot is backed by an owned heap
5211    /// allocation per the Phase 4 contract — either `UniqueHeap` storage
5212    /// class, or `Direct` storage class combined with a non-scalar
5213    /// storage hint. The latter captures the Box-promoted heap path
5214    /// (strings, arrays, hashmaps, typed objects) handed to the slot by
5215    /// `PromoteToOwned`. Inline scalars on Direct slots and every other
5216    /// storage class (SharedCow, Reference, LocalMutablePtr, Deferred)
5217    /// are excluded.
5218    pub(super) fn slot_is_heap_backed_owned(&self, local_idx: u16) -> bool {
5219        use crate::type_tracking::BindingStorageClass;
5220        match self.mir_storage_class_for_slot(local_idx) {
5221            Some(BindingStorageClass::UniqueHeap) => true,
5222            Some(BindingStorageClass::Direct) => !self.slot_has_inline_scalar_hint(local_idx),
5223            _ => false,
5224        }
5225    }
5226
5227    /// Phase V1.1C: decide whether the newly-declared local slot should
5228    /// receive a `DropLocal` opcode at scope exit (when the ownership-moves
5229    /// flag is on). The slot needs a drop iff it is heap-backed — either
5230    /// `UniqueHeap` storage class (owned Box allocation), or `Direct`
5231    /// storage class combined with a `let`/`const` binding of a heap type
5232    /// (the `PromoteToOwned` emission converts these to Box). Inline
5233    /// scalars (`int`, `number`, `bool`, etc.) own no heap resource and
5234    /// are skipped per `docs/ownership-aware-runtime-v2.md` §Phase 1.
5235    /// `var` bindings of heap type on Direct storage are skipped too —
5236    /// the Arc refcount path already releases them.
5237    pub(super) fn binding_slot_needs_ownership_drop(
5238        &self,
5239        local_idx: u16,
5240        var_kind: shape_ast::ast::VarKind,
5241    ) -> bool {
5242        use crate::type_tracking::BindingStorageClass;
5243        // Phase V1.1C fix: if the slot has been SharedCell-wrapped by a prior
5244        // legacy cell-wrapping emission (module-binding capture path), the
5245        // legacy Arc-refcount release path in `pop_drop_scope` /
5246        // `emit_drops_for_early_exit` handles the release. Emitting
5247        // `DropLocal` here poisons the slot to `0u64`
5248        // which breaks the auto-unwrap in `LoadLocal` / `LoadClosure` for any
5249        // subsequent read (e.g. compiler-injected reads like `LoadLocal` +
5250        // `DropCall` pairs that immediately follow the `DropLocal`).
5251        // Track A.1C.2: symmetrically skip slots promoted via
5252        // `AllocSharedLocal` — `DropSharedLocal` owns their release.
5253        if self.slot_is_boxed(local_idx) || self.slot_is_shared(local_idx) {
5254            return false;
5255        }
5256        match self.mir_storage_class_for_slot(local_idx) {
5257            Some(BindingStorageClass::UniqueHeap) => true,
5258            Some(BindingStorageClass::Direct) => {
5259                // Only let / const are Box-promoted by the PromoteToOwned
5260                // rule (see statements.rs §Phase 3/4). var bindings with
5261                // Direct storage stay Arc-wrapped and release via the
5262                // existing refcount path.
5263                matches!(
5264                    var_kind,
5265                    shape_ast::ast::VarKind::Let | shape_ast::ast::VarKind::Const
5266                ) && !self.slot_has_inline_scalar_hint(local_idx)
5267            }
5268            _ => false,
5269        }
5270    }
5271
5272    /// Emit a single LoadLocal + DropCall pair for a local variable.
5273    /// The type name is resolved from the type tracker and encoded as a
5274    /// Property operand so the executor can look up `TypeName::drop`.
5275    fn emit_drop_call_for_local(&mut self, local_idx: u16, is_async: bool) {
5276        let type_name_opt = self
5277            .type_tracker
5278            .get_local_type(local_idx)
5279            .and_then(|info| info.type_name.clone());
5280        self.emit(Instruction::new(
5281            OpCode::LoadLocal,
5282            Some(Operand::Local(local_idx)),
5283        ));
5284        let opcode = if is_async {
5285            OpCode::DropCallAsync
5286        } else {
5287            OpCode::DropCall
5288        };
5289        if let Some(type_name) = type_name_opt {
5290            let str_idx = self.program.add_string(type_name);
5291            self.emit(Instruction::new(opcode, Some(Operand::Property(str_idx))));
5292        } else {
5293            self.emit(Instruction::simple(opcode));
5294        }
5295    }
5296
5297    /// Emit a single LoadModuleBinding + DropCall pair for a module binding.
5298    /// Similar to `emit_drop_call_for_local` but loads from module bindings.
5299    pub(super) fn emit_drop_call_for_module_binding(&mut self, binding_idx: u16, is_async: bool) {
5300        let type_name_opt = self
5301            .type_tracker
5302            .get_binding_type(binding_idx)
5303            .and_then(|info| info.type_name.clone());
5304        self.emit(Instruction::new(
5305            OpCode::LoadModuleBinding,
5306            Some(Operand::ModuleBinding(binding_idx)),
5307        ));
5308        let opcode = if is_async {
5309            OpCode::DropCallAsync
5310        } else {
5311            OpCode::DropCall
5312        };
5313        if let Some(type_name) = type_name_opt {
5314            let str_idx = self.program.add_string(type_name);
5315            self.emit(Instruction::new(opcode, Some(Operand::Property(str_idx))));
5316        } else {
5317            self.emit(Instruction::simple(opcode));
5318        }
5319    }
5320
5321    /// Track a local variable as needing Drop at scope exit.
5322    pub(super) fn track_drop_local(&mut self, local_idx: u16, is_async: bool) {
5323        if let Some(scope) = self.drop_locals.last_mut() {
5324            scope.push((local_idx, is_async));
5325        }
5326    }
5327
5328    /// Resolve the DropKind for a local variable's type.
5329    /// Returns None if the type is unknown or has no Drop impl.
5330    pub(super) fn local_drop_kind(&self, local_idx: u16) -> Option<DropKind> {
5331        let type_name = self
5332            .type_tracker
5333            .get_local_type(local_idx)
5334            .and_then(|info| info.type_name.as_ref())?;
5335        self.drop_type_info.get(type_name).copied()
5336    }
5337
5338    /// Resolve DropKind from a type annotation.
5339    pub(super) fn annotation_drop_kind(&self, type_ann: &TypeAnnotation) -> Option<DropKind> {
5340        let type_name = Self::tracked_type_name_from_annotation(type_ann)?;
5341        self.drop_type_info.get(&type_name).copied()
5342    }
5343
5344    /// Emit drops for all scopes being exited (used by return/break/continue).
5345    /// `scopes_to_exit` is the number of drop scopes to emit drops for.
5346    pub(super) fn emit_drops_for_early_exit(&mut self, scopes_to_exit: usize) -> Result<()> {
5347        let total = self.drop_locals.len();
5348        if scopes_to_exit > total {
5349            return Ok(());
5350        }
5351        // Phase V1.1C: when the ownership-moves flag is on, also emit a
5352        // `DropLocal` for each heap-ref (UniqueHeap) local tracked in the
5353        // scopes being exited, innermost-first / reverse declaration order.
5354        // Flag off: no ownership drops — byte-identical to pre-V1.1C.
5355        // Note: early-exit drops are inserted at the jump/return site; the
5356        // scope stack itself is popped later by the enclosing block, so we
5357        // must *not* consume `self.ownership_drop_locals` here. Cloning
5358        // matches the `self.drop_locals` treatment below.
5359        if ownership_moves_enabled() {
5360            let ownership_total = self.ownership_drop_locals.len();
5361            if scopes_to_exit <= ownership_total {
5362                let mut ownership_scopes: Vec<Vec<u16>> = Vec::new();
5363                for i in (ownership_total - scopes_to_exit..ownership_total).rev() {
5364                    let locals = self
5365                        .ownership_drop_locals
5366                        .get(i)
5367                        .cloned()
5368                        .unwrap_or_default();
5369                    ownership_scopes.push(locals);
5370                }
5371                for locals in ownership_scopes {
5372                    for local_idx in locals.into_iter().rev() {
5373                        // Phase V1.1C fix: skip `DropLocal` emission for
5374                        // slots that were SharedCell-wrapped by a prior
5375                        // legacy cell-wrapping emission. See the companion
5376                        // comment in `pop_drop_scope` for the rationale.
5377                        // Track A.1C.2 additionally skips slots promoted via
5378                        // `AllocSharedLocal` — `DropSharedLocal` is emitted
5379                        // below in parallel.
5380                        if self.slot_is_boxed(local_idx) || self.slot_is_shared(local_idx) {
5381                            continue;
5382                        }
5383                        // R8 W9 B3 Drop runtime fix: skip `DropLocal` when
5384                        // the slot's type has a user `impl Drop` impl. The
5385                        // `DropCall` opcode emitted below is the sole
5386                        // releaser for user-Drop slots — `DropLocal` here
5387                        // would poison the slot before `DropCall` reads
5388                        // it. See the companion comment in
5389                        // `pop_drop_scope` above for rationale.
5390                        if self.local_drop_kind(local_idx).is_some() {
5391                            continue;
5392                        }
5393                        self.emit(Instruction::new(
5394                            OpCode::DropLocal,
5395                            Some(Operand::Local(local_idx)),
5396                        ));
5397                    }
5398                }
5399            }
5400        }
5401        // Track A.1C.2: emit DropSharedLocal for each Shared-promoted slot
5402        // in the scopes being exited. Mirrors the ownership-drop emission
5403        // strategy above (clone, do not consume — the scope stack is popped
5404        // later by the enclosing block).
5405        {
5406            let shared_total = self.shared_drop_locals.len();
5407            if scopes_to_exit <= shared_total {
5408                let mut shared_scopes: Vec<Vec<u16>> = Vec::new();
5409                for i in (shared_total - scopes_to_exit..shared_total).rev() {
5410                    let locals = self.shared_drop_locals.get(i).cloned().unwrap_or_default();
5411                    shared_scopes.push(locals);
5412                }
5413                for locals in shared_scopes {
5414                    for local_idx in locals.into_iter().rev() {
5415                        self.emit(Instruction::new(
5416                            OpCode::DropSharedLocal,
5417                            Some(Operand::Local(local_idx)),
5418                        ));
5419                    }
5420                }
5421            }
5422        }
5423        // Collect locals from scopes being exited (innermost first)
5424        let mut scopes: Vec<Vec<(u16, bool)>> = Vec::new();
5425        for i in (total - scopes_to_exit..total).rev() {
5426            let locals = self.drop_locals.get(i).cloned().unwrap_or_default();
5427            scopes.push(locals);
5428        }
5429        // Now emit DropCall instructions
5430        for locals in scopes {
5431            for (local_idx, is_async) in locals.into_iter().rev() {
5432                self.emit_drop_call_for_local(local_idx, is_async);
5433            }
5434        }
5435        Ok(())
5436    }
5437
5438    /// Track a module binding as needing Drop at program exit.
5439    pub(super) fn track_drop_module_binding(&mut self, binding_idx: u16, is_async: bool) {
5440        self.drop_module_bindings.push((binding_idx, is_async));
5441    }
5442}
5443
5444// Wave E: per-`FieldKind` dispatch helpers for OwnedMutable closure-capture
5445// load and store opcodes (D.1 codes 0x140-0x155).
5446//
5447// Each helper maps a closure-cell interior `FieldKind` to the matching typed
5448// opcode. The compiler computes the cell's interior `FieldKind` from the
5449// captured binding's resolved `ConcreteType` at closure-construction time
5450// (see `compile_expr_closure`'s population of
5451// `owned_mutable_capture_inner_kinds`) and dispatches reads / writes inside
5452// the closure body via these tables.
5453//
5454// Stack contract: typed loads push raw native bytes onto the kinded VM
5455// stack via `push_kinded(bits, kind)` (sub-i64 ints sign- or
5456// zero-extended into the i64 path); typed stores pop raw native bytes
5457// via `pop_kinded() -> (bits, kind)`. `Ptr` transfers the raw 8-byte
5458// heap-pointer bit pattern unchanged
5459// — neither typed nor legacy variant clones / drops the heap share, so the
5460// emit-site swap from legacy `LoadOwnedMutableCapture` (0x132) /
5461// `StoreOwnedMutableCapture` (0x133) preserves refcount semantics.
5462//
5463// Wave G removes the legacy 0x132/0x133 opcodes once every emit path has
5464// been migrated.
5465
5466/// Map an OwnedMutable closure-cell interior `FieldKind` to its typed
5467/// `LoadOwnedMutableCapture<Kind>` opcode (D.1 codes 0x140-0x14A).
5468#[inline]
5469pub(crate) fn owned_mutable_typed_load_opcode(
5470    kind: shape_value::v2::struct_layout::FieldKind,
5471) -> OpCode {
5472    use shape_value::v2::struct_layout::FieldKind;
5473    match kind {
5474        FieldKind::I64 => OpCode::LoadOwnedMutableCaptureI64,
5475        FieldKind::U64 => OpCode::LoadOwnedMutableCaptureU64,
5476        FieldKind::F64 => OpCode::LoadOwnedMutableCaptureF64,
5477        FieldKind::I32 => OpCode::LoadOwnedMutableCaptureI32,
5478        FieldKind::U32 => OpCode::LoadOwnedMutableCaptureU32,
5479        FieldKind::I16 => OpCode::LoadOwnedMutableCaptureI16,
5480        FieldKind::U16 => OpCode::LoadOwnedMutableCaptureU16,
5481        FieldKind::I8 => OpCode::LoadOwnedMutableCaptureI8,
5482        FieldKind::U8 => OpCode::LoadOwnedMutableCaptureU8,
5483        FieldKind::Bool => OpCode::LoadOwnedMutableCaptureBool,
5484        FieldKind::Ptr => OpCode::LoadOwnedMutableCapturePtr,
5485    }
5486}
5487
5488/// Map an OwnedMutable closure-cell interior `FieldKind` to its typed
5489/// `StoreOwnedMutableCapture<Kind>` opcode (D.1 codes 0x14B-0x155).
5490#[inline]
5491pub(crate) fn owned_mutable_typed_store_opcode(
5492    kind: shape_value::v2::struct_layout::FieldKind,
5493) -> OpCode {
5494    use shape_value::v2::struct_layout::FieldKind;
5495    match kind {
5496        FieldKind::I64 => OpCode::StoreOwnedMutableCaptureI64,
5497        FieldKind::U64 => OpCode::StoreOwnedMutableCaptureU64,
5498        FieldKind::F64 => OpCode::StoreOwnedMutableCaptureF64,
5499        FieldKind::I32 => OpCode::StoreOwnedMutableCaptureI32,
5500        FieldKind::U32 => OpCode::StoreOwnedMutableCaptureU32,
5501        FieldKind::I16 => OpCode::StoreOwnedMutableCaptureI16,
5502        FieldKind::U16 => OpCode::StoreOwnedMutableCaptureU16,
5503        FieldKind::I8 => OpCode::StoreOwnedMutableCaptureI8,
5504        FieldKind::U8 => OpCode::StoreOwnedMutableCaptureU8,
5505        FieldKind::Bool => OpCode::StoreOwnedMutableCaptureBool,
5506        FieldKind::Ptr => OpCode::StoreOwnedMutableCapturePtr,
5507    }
5508}
5509
5510/// Map a Shared closure-cell interior `FieldKind` to its typed
5511/// `LoadSharedCapture<Kind>` opcode (D.2 codes 0x156-0x160). Mirrors
5512/// `owned_mutable_typed_load_opcode`. Used by the closure-body Shared
5513/// capture read emission to dispatch on the cell's interior FieldKind
5514/// (recorded in `shared_capture_inner_kinds`) instead of the legacy
5515/// polymorphic `LoadSharedCapture` (0x134).
5516#[inline]
5517pub(crate) fn shared_typed_load_opcode(
5518    kind: shape_value::v2::struct_layout::FieldKind,
5519) -> OpCode {
5520    use shape_value::v2::struct_layout::FieldKind;
5521    match kind {
5522        FieldKind::I64 => OpCode::LoadSharedCaptureI64,
5523        FieldKind::U64 => OpCode::LoadSharedCaptureU64,
5524        FieldKind::F64 => OpCode::LoadSharedCaptureF64,
5525        FieldKind::I32 => OpCode::LoadSharedCaptureI32,
5526        FieldKind::U32 => OpCode::LoadSharedCaptureU32,
5527        FieldKind::I16 => OpCode::LoadSharedCaptureI16,
5528        FieldKind::U16 => OpCode::LoadSharedCaptureU16,
5529        FieldKind::I8 => OpCode::LoadSharedCaptureI8,
5530        FieldKind::U8 => OpCode::LoadSharedCaptureU8,
5531        FieldKind::Bool => OpCode::LoadSharedCaptureBool,
5532        FieldKind::Ptr => OpCode::LoadSharedCapturePtr,
5533    }
5534}
5535
5536/// Map a Shared closure-cell interior `FieldKind` to its typed
5537/// `StoreSharedCapture<Kind>` opcode (D.2 codes 0x161-0x16B). Mirrors
5538/// `owned_mutable_typed_store_opcode`.
5539#[inline]
5540pub(crate) fn shared_typed_store_opcode(
5541    kind: shape_value::v2::struct_layout::FieldKind,
5542) -> OpCode {
5543    use shape_value::v2::struct_layout::FieldKind;
5544    match kind {
5545        FieldKind::I64 => OpCode::StoreSharedCaptureI64,
5546        FieldKind::U64 => OpCode::StoreSharedCaptureU64,
5547        FieldKind::F64 => OpCode::StoreSharedCaptureF64,
5548        FieldKind::I32 => OpCode::StoreSharedCaptureI32,
5549        FieldKind::U32 => OpCode::StoreSharedCaptureU32,
5550        FieldKind::I16 => OpCode::StoreSharedCaptureI16,
5551        FieldKind::U16 => OpCode::StoreSharedCaptureU16,
5552        FieldKind::I8 => OpCode::StoreSharedCaptureI8,
5553        FieldKind::U8 => OpCode::StoreSharedCaptureU8,
5554        FieldKind::Bool => OpCode::StoreSharedCaptureBool,
5555        FieldKind::Ptr => OpCode::StoreSharedCapturePtr,
5556    }
5557}
5558
5559// ─────────────────────────────────────────────────────────────────────────
5560// Wave E+4: typed-emission helpers for Load/Store local + module-binding +
5561// ReturnValue.
5562//
5563// These map a *proven* `StorageHint` to one of Wave E+3's typed opcodes
5564// (codes 0x16C-0x1A2). When the hint cannot be coerced to a concrete
5565// `FieldKind` (Dynamic / Unknown / nullable widths whose null sentinel
5566// can't be transported as raw native bits without losing information),
5567// the helpers fall back to the polymorphic legacy opcode. The legacy
5568// opcodes stay live; Wave G later audits whether they still have any
5569// emit sites and removes them if dead.
5570//
5571// Per-Ptr ownership: typed Ptr opcodes do NOT clone/drop. The IR
5572// pairs `LoadLocalPtr` with the kinded `clone_with_kind` retain
5573// (ADR-006 §2.7.7) and `StoreLocalPtr` with the matching
5574// `drop_with_kind` release of the prior payload. Callers that flip
5575// Ptr-Kind sites must add the matching retain/release; until then,
5576// leave Ptr sites on the polymorphic fallback (which preserves the
5577// legacy refcount semantics).
5578// ─────────────────────────────────────────────────────────────────────────
5579
5580/// Map a `StorageHint` to a `FieldKind` when the hint represents a
5581/// statically-proven primitive. Returns `None` for nullable widths
5582/// (whose null sentinel relies on the deleted ValueWord encoding —
5583/// ADR-006 §2.7.7 — not raw native bits and is tracked as a Phase 2c
5584/// rebuild on the kinded null-sentinel path), `String` (heap-bearing —
5585/// Ptr Kind, but legacy emit path manages ownership; routed to
5586/// polymorphic until per-Ptr audit lands), and any future variant we
5587/// don't recognize.
5588#[inline]
5589pub(crate) fn storage_hint_to_field_kind(
5590    hint: StorageHint,
5591) -> Option<shape_value::v2::struct_layout::FieldKind> {
5592    use shape_value::v2::struct_layout::FieldKind;
5593    Some(match hint {
5594        // Proven non-nullable primitives — safe to flip to typed.
5595        StorageHint::Float64 => FieldKind::F64,
5596        StorageHint::Int64 => FieldKind::I64,
5597        StorageHint::UInt64 => FieldKind::U64,
5598        StorageHint::Int32 => FieldKind::I32,
5599        StorageHint::UInt32 => FieldKind::U32,
5600        StorageHint::Int16 => FieldKind::I16,
5601        StorageHint::UInt16 => FieldKind::U16,
5602        StorageHint::Int8 => FieldKind::I8,
5603        StorageHint::UInt8 => FieldKind::U8,
5604        StorageHint::IntSize | StorageHint::UIntSize => return None,
5605        StorageHint::Bool => FieldKind::Bool,
5606        // Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14):
5607        // ADR-006 §2.7.5 amendment — F32 + Char route through the
5608        // polymorphic-legacy fallback at this layer (FieldKind has no
5609        // F32 / Char variants; per-FieldKind typed emission is a
5610        // follow-up sub-cluster). Returning `None` from
5611        // `storage_hint_to_field_kind` triggers the polymorphic-legacy
5612        // emit path, which already handles raw-u64 carriers.
5613        StorageHint::Float32 | StorageHint::Char => return None,
5614        // Heap-bearing / nullable / unresolved → polymorphic fallback.
5615        // String is a Ptr by storage but routes via the legacy
5616        // LoadLocal/StoreLocal path that does refcount accounting; until
5617        // emit sites pair the kinded `clone_with_kind` / `drop_with_kind`
5618        // helpers (ADR-006 §2.7.7) with typed Ptr ops, leave String on
5619        // polymorphic.
5620        StorageHint::String => return None,
5621        // Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions
5622        // (2026-05-14): StringV2 / DecimalV2 are v2-raw heap-pointer
5623        // carriers — same polymorphic-legacy fallback as String per the
5624        // refcount-discipline rationale above. FieldKind has no dedicated
5625        // StringV2 / DecimalV2 variants; per-FieldKind typed emission is
5626        // a follow-up sub-cluster gated on the cluster-1 hardening
5627        // retirement of the polymorphic LoadLocal / StoreLocal path.
5628        StorageHint::StringV2 | StorageHint::DecimalV2 => return None,
5629        StorageHint::NullableFloat64
5630        | StorageHint::NullableInt8
5631        | StorageHint::NullableUInt8
5632        | StorageHint::NullableInt16
5633        | StorageHint::NullableUInt16
5634        | StorageHint::NullableInt32
5635        | StorageHint::NullableUInt32
5636        | StorageHint::NullableInt64
5637        | StorageHint::NullableUInt64
5638        | StorageHint::NullableIntSize
5639        | StorageHint::NullableUIntSize => return None,
5640        // ADR-006 §2.7.5.1: `StorageHint::{Dynamic, Unknown}` were
5641        // deleted; the post-bulldozer compiler-tier "kind not yet
5642        // proven" state is `Option<StorageHint>` carried at the call
5643        // site, never an enum sentinel.
5644        //
5645        // `Ptr(HeapKind)` is heap-bearing; like `String` it routes via
5646        // the legacy LoadLocal/StoreLocal path that does refcount
5647        // accounting until per-Ptr typed ops audit lands.
5648        StorageHint::Ptr(_) => return None,
5649        // R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 + §2.7.7/Q9,
5650        // 2026-05-19): `NativeKind::Null` storage-hint has no
5651        // FieldKind projection — null is an absence-of-value
5652        // discriminator, not a per-field typed storage shape. Route
5653        // to polymorphic-legacy fallback.
5654        StorageHint::Null => return None,
5655    })
5656}
5657
5658/// Wave E+4 fallback counter — increments under `debug_assertions` whenever
5659/// a typed-emission helper takes the polymorphic-legacy fallback. Wave G
5660/// reads this to decide whether the legacy `LoadLocal` / `StoreLocal` /
5661/// `LoadModuleBinding` / `StoreModuleBinding` / `ReturnValue` opcodes can
5662/// be deleted.
5663///
5664/// Two views are recorded simultaneously:
5665///   * **Per-category** — coarse total by emit-site family. Useful for "how
5666///     many polymorphic Loads vs Stores vs Returns survive".
5667///   * **Per-(category, hint)** — fine-grained breakdown by the
5668///     `StorageHint` that drove the fallback. Useful for "is the residual
5669///     fallback dominated by `Dynamic`/`Unknown` (genuinely-unproven sites,
5670///     accept) or by `String`/nullable widths (heap/null sentinel — design
5671///     gap, plumb a fix)".
5672///
5673/// Categories:
5674///   * `"load_local"` — `emit_load_local_for_hint` fallback.
5675///   * `"store_local"` — `emit_store_local_for_hint` fallback.
5676///   * `"load_module_binding"` — `emit_load_module_binding_for_hint`.
5677///   * `"store_module_binding"` — `emit_store_module_binding_for_hint`.
5678///   * `"return_value"` — `emit_return_value_for_hint` fallback.
5679///
5680/// Hint labels are the lower-snake-case `StorageHint` variant name
5681/// (`"dynamic"`, `"unknown"`, `"string"`, `"nullable_int64"`, ...). The
5682/// mapping is `storage_hint_label`.
5683#[cfg(debug_assertions)]
5684pub(crate) mod typed_emit_metrics {
5685    use crate::type_tracking::StorageHint;
5686    use std::collections::HashMap;
5687    use std::sync::Mutex;
5688    use std::sync::OnceLock;
5689
5690    /// Per-category total fallback counts.
5691    static CATEGORY_COUNTERS: OnceLock<Mutex<HashMap<&'static str, u64>>> = OnceLock::new();
5692    /// Per-(category, hint) joint distribution. Hint label is from
5693    /// `storage_hint_label`.
5694    static JOINT_COUNTERS: OnceLock<Mutex<HashMap<(&'static str, &'static str), u64>>> =
5695        OnceLock::new();
5696
5697    /// Map a `StorageHint` to a stable `'static` label suitable for
5698    /// keying the joint counter. Lower-snake-case variant names.
5699    pub(crate) fn storage_hint_label(hint: StorageHint) -> &'static str {
5700        match hint {
5701            StorageHint::Float64 => "f64",
5702            StorageHint::NullableFloat64 => "nullable_f64",
5703            StorageHint::Int8 => "i8",
5704            StorageHint::NullableInt8 => "nullable_i8",
5705            StorageHint::UInt8 => "u8",
5706            StorageHint::NullableUInt8 => "nullable_u8",
5707            StorageHint::Int16 => "i16",
5708            StorageHint::NullableInt16 => "nullable_i16",
5709            StorageHint::UInt16 => "u16",
5710            StorageHint::NullableUInt16 => "nullable_u16",
5711            StorageHint::Int32 => "i32",
5712            StorageHint::NullableInt32 => "nullable_i32",
5713            StorageHint::UInt32 => "u32",
5714            StorageHint::NullableUInt32 => "nullable_u32",
5715            StorageHint::Int64 => "i64",
5716            StorageHint::NullableInt64 => "nullable_i64",
5717            StorageHint::UInt64 => "u64",
5718            StorageHint::NullableUInt64 => "nullable_u64",
5719            StorageHint::IntSize => "isize",
5720            StorageHint::NullableIntSize => "nullable_isize",
5721            StorageHint::UIntSize => "usize",
5722            StorageHint::NullableUIntSize => "nullable_usize",
5723            StorageHint::Bool => "bool",
5724            // Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14):
5725            // ADR-006 §2.7.5 amendment.
5726            StorageHint::Float32 => "f32",
5727            StorageHint::Char => "char",
5728            StorageHint::String => "string",
5729            // Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions
5730            // (2026-05-14): joint-counter labels for the v2-raw carrier
5731            // variants. Distinct labels from "string" / "decimal" so the
5732            // joint-counter telemetry can distinguish v2-raw vs Arc-wrapped
5733            // fallback distributions when the producer migration (Agent A2)
5734            // lands.
5735            StorageHint::StringV2 => "string_v2",
5736            StorageHint::DecimalV2 => "decimal_v2",
5737            // ADR-006 §2.7.5.1: `StorageHint::{Dynamic, Unknown}` were
5738            // deleted; the compiler-tier "kind not yet proven" state is
5739            // `Option<StorageHint>` and the joint-counter call sites
5740            // route the `None` case through `storage_hint_label_opt` /
5741            // explicit `"none"` label so we never have to map an
5742            // enum sentinel here. `Ptr(HeapKind)` is the surviving
5743            // heap-bearing variant; one stable label per heap arm
5744            // would balloon this map, so collapse all `Ptr(_)` to "ptr".
5745            StorageHint::Ptr(_) => "ptr",
5746            // R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 +
5747            // §2.7.7/Q9, 2026-05-19): canonical absence-of-value
5748            // discriminator joint-counter label.
5749            StorageHint::Null => "null",
5750        }
5751    }
5752
5753    /// Record a polymorphic fallback. Bumps both the per-category counter
5754    /// and the (category, hint-label) joint counter.
5755    pub(crate) fn record_polymorphic_fallback(category: &'static str, hint: StorageHint) {
5756        let cat = CATEGORY_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
5757        if let Ok(mut g) = cat.lock() {
5758            *g.entry(category).or_insert(0) += 1;
5759        }
5760        let joint = JOINT_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
5761        if let Ok(mut g) = joint.lock() {
5762            *g.entry((category, storage_hint_label(hint))).or_insert(0) += 1;
5763        }
5764    }
5765
5766    /// Snapshot the per-category counters. Sorted by category. Used by
5767    /// Wave E+4 tests and Wave G's cleanup audit.
5768    pub fn snapshot() -> Vec<(&'static str, u64)> {
5769        let counters = CATEGORY_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
5770        let g = counters.lock().expect("typed_emit_metrics lock poisoned");
5771        let mut v: Vec<_> = g.iter().map(|(k, v)| (*k, *v)).collect();
5772        v.sort_by_key(|(k, _)| *k);
5773        v
5774    }
5775
5776    /// Snapshot the (category, hint) joint distribution. Sorted by
5777    /// (category, hint). Useful for "what hints are driving the residual
5778    /// fallback" — `Dynamic`/`Unknown` are genuinely-unproven, anything
5779    /// else suggests a design gap.
5780    pub fn snapshot_joint() -> Vec<((&'static str, &'static str), u64)> {
5781        let counters = JOINT_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
5782        let g = counters.lock().expect("typed_emit_metrics joint lock poisoned");
5783        let mut v: Vec<_> = g.iter().map(|(k, v)| (*k, *v)).collect();
5784        v.sort_by_key(|(k, _)| *k);
5785        v
5786    }
5787
5788    /// Reset both counters for use across test iterations.
5789    pub fn reset() {
5790        let cat = CATEGORY_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
5791        if let Ok(mut g) = cat.lock() {
5792            g.clear();
5793        }
5794        let joint = JOINT_COUNTERS.get_or_init(|| Mutex::new(HashMap::new()));
5795        if let Ok(mut g) = joint.lock() {
5796            g.clear();
5797        }
5798    }
5799}
5800
5801#[cfg(not(debug_assertions))]
5802pub(crate) mod typed_emit_metrics {
5803    use crate::type_tracking::StorageHint;
5804
5805    pub(crate) fn storage_hint_label(_hint: StorageHint) -> &'static str {
5806        ""
5807    }
5808    pub(crate) fn record_polymorphic_fallback(_category: &'static str, _hint: StorageHint) {}
5809    pub fn snapshot() -> Vec<(&'static str, u64)> {
5810        Vec::new()
5811    }
5812    pub fn snapshot_joint() -> Vec<((&'static str, &'static str), u64)> {
5813        Vec::new()
5814    }
5815    pub fn reset() {}
5816}
5817
5818/// Wave E+4 commit 4: map a primitive type-annotation name (e.g.
5819/// `"bool"`, `"int"`, `"number"`, `"string"`, sub-i64 widths like
5820/// `"i32"`) to its `StorageHint`. Returns `None` for non-primitive /
5821/// user-defined / unrecognised names — the caller falls back to
5822/// `Unknown`, which preserves pre-E+4 passthrough semantics at the host
5823/// boundary.
5824///
5825/// Mirrors the canonical-name policy from
5826/// `BuiltinTypes::is_integer_type_name` / `is_number_type_name` for the
5827/// numeric family, plus the explicit primitive cases the compiler's
5828/// schema registry doesn't enrol.
5829#[inline]
5830pub(crate) fn primitive_type_name_to_storage_hint(name: &str) -> Option<StorageHint> {
5831    Some(match name {
5832        // Numeric primitives.
5833        "int" | "Int" | "i64" => StorageHint::Int64,
5834        "u64" | "UInt" => StorageHint::UInt64,
5835        "i8" => StorageHint::Int8,
5836        "u8" => StorageHint::UInt8,
5837        "i16" => StorageHint::Int16,
5838        "u16" => StorageHint::UInt16,
5839        "i32" => StorageHint::Int32,
5840        "u32" => StorageHint::UInt32,
5841        "isize" => StorageHint::IntSize,
5842        "usize" => StorageHint::UIntSize,
5843        "number" | "Number" | "f32" | "f64" => StorageHint::Float64,
5844        "bool" | "Bool" => StorageHint::Bool,
5845        "string" | "String" => StorageHint::String,
5846        _ => return None,
5847    })
5848}
5849
5850/// Map a `StorageHint` to its typed `LoadLocal<Kind>` opcode (E+3 codes
5851/// 0x16C-0x176). Returns `None` for hints that don't have a typed form.
5852#[inline]
5853pub(crate) fn typed_load_local_opcode(hint: StorageHint) -> Option<OpCode> {
5854    use shape_value::v2::struct_layout::FieldKind;
5855    Some(match storage_hint_to_field_kind(hint)? {
5856        FieldKind::I64 => OpCode::LoadLocalI64,
5857        FieldKind::U64 => OpCode::LoadLocalU64,
5858        FieldKind::F64 => OpCode::LoadLocalF64,
5859        FieldKind::I32 => OpCode::LoadLocalI32,
5860        FieldKind::U32 => OpCode::LoadLocalU32,
5861        FieldKind::I16 => OpCode::LoadLocalI16,
5862        FieldKind::U16 => OpCode::LoadLocalU16,
5863        FieldKind::I8 => OpCode::LoadLocalI8,
5864        FieldKind::U8 => OpCode::LoadLocalU8,
5865        FieldKind::Bool => OpCode::LoadLocalBool,
5866        FieldKind::Ptr => OpCode::LoadLocalPtr,
5867    })
5868}
5869
5870/// Map a `StorageHint` to its typed `StoreLocal<Kind>` opcode (E+3 codes
5871/// 0x177-0x181). Returns `None` for hints that don't have a typed form.
5872#[inline]
5873pub(crate) fn typed_store_local_opcode(hint: StorageHint) -> Option<OpCode> {
5874    use shape_value::v2::struct_layout::FieldKind;
5875    Some(match storage_hint_to_field_kind(hint)? {
5876        FieldKind::I64 => OpCode::StoreLocalI64,
5877        FieldKind::U64 => OpCode::StoreLocalU64,
5878        FieldKind::F64 => OpCode::StoreLocalF64,
5879        FieldKind::I32 => OpCode::StoreLocalI32,
5880        FieldKind::U32 => OpCode::StoreLocalU32,
5881        FieldKind::I16 => OpCode::StoreLocalI16,
5882        FieldKind::U16 => OpCode::StoreLocalU16,
5883        FieldKind::I8 => OpCode::StoreLocalI8,
5884        FieldKind::U8 => OpCode::StoreLocalU8,
5885        FieldKind::Bool => OpCode::StoreLocalBool,
5886        FieldKind::Ptr => OpCode::StoreLocalPtr,
5887    })
5888}
5889
5890/// Map a `StorageHint` to its typed `LoadModuleBinding<Kind>` opcode
5891/// (E+3 codes 0x182-0x18C). Returns `None` for unproven hints.
5892#[inline]
5893pub(crate) fn typed_load_module_binding_opcode(hint: StorageHint) -> Option<OpCode> {
5894    use shape_value::v2::struct_layout::FieldKind;
5895    Some(match storage_hint_to_field_kind(hint)? {
5896        FieldKind::I64 => OpCode::LoadModuleBindingI64,
5897        FieldKind::U64 => OpCode::LoadModuleBindingU64,
5898        FieldKind::F64 => OpCode::LoadModuleBindingF64,
5899        FieldKind::I32 => OpCode::LoadModuleBindingI32,
5900        FieldKind::U32 => OpCode::LoadModuleBindingU32,
5901        FieldKind::I16 => OpCode::LoadModuleBindingI16,
5902        FieldKind::U16 => OpCode::LoadModuleBindingU16,
5903        FieldKind::I8 => OpCode::LoadModuleBindingI8,
5904        FieldKind::U8 => OpCode::LoadModuleBindingU8,
5905        FieldKind::Bool => OpCode::LoadModuleBindingBool,
5906        FieldKind::Ptr => OpCode::LoadModuleBindingPtr,
5907    })
5908}
5909
5910/// Map a `StorageHint` to its typed `StoreModuleBinding<Kind>` opcode
5911/// (E+3 codes 0x18D-0x197). Returns `None` for unproven hints.
5912#[inline]
5913pub(crate) fn typed_store_module_binding_opcode(hint: StorageHint) -> Option<OpCode> {
5914    use shape_value::v2::struct_layout::FieldKind;
5915    Some(match storage_hint_to_field_kind(hint)? {
5916        FieldKind::I64 => OpCode::StoreModuleBindingI64,
5917        FieldKind::U64 => OpCode::StoreModuleBindingU64,
5918        FieldKind::F64 => OpCode::StoreModuleBindingF64,
5919        FieldKind::I32 => OpCode::StoreModuleBindingI32,
5920        FieldKind::U32 => OpCode::StoreModuleBindingU32,
5921        FieldKind::I16 => OpCode::StoreModuleBindingI16,
5922        FieldKind::U16 => OpCode::StoreModuleBindingU16,
5923        FieldKind::I8 => OpCode::StoreModuleBindingI8,
5924        FieldKind::U8 => OpCode::StoreModuleBindingU8,
5925        FieldKind::Bool => OpCode::StoreModuleBindingBool,
5926        FieldKind::Ptr => OpCode::StoreModuleBindingPtr,
5927    })
5928}
5929
5930/// Map a `StorageHint` to its typed `ReturnValue<Kind>` opcode (E+3
5931/// codes 0x198-0x1A2). Returns `None` for unproven hints.
5932#[inline]
5933pub(crate) fn typed_return_value_opcode(hint: StorageHint) -> Option<OpCode> {
5934    use shape_value::v2::struct_layout::FieldKind;
5935    Some(match storage_hint_to_field_kind(hint)? {
5936        FieldKind::I64 => OpCode::ReturnValueI64,
5937        FieldKind::U64 => OpCode::ReturnValueU64,
5938        FieldKind::F64 => OpCode::ReturnValueF64,
5939        FieldKind::I32 => OpCode::ReturnValueI32,
5940        FieldKind::U32 => OpCode::ReturnValueU32,
5941        FieldKind::I16 => OpCode::ReturnValueI16,
5942        FieldKind::U16 => OpCode::ReturnValueU16,
5943        FieldKind::I8 => OpCode::ReturnValueI8,
5944        FieldKind::U8 => OpCode::ReturnValueU8,
5945        FieldKind::Bool => OpCode::ReturnValueBool,
5946        FieldKind::Ptr => OpCode::ReturnValuePtr,
5947    })
5948}
5949
5950// ─────────────────────────────────────────────────────────────────────────
5951// Wave E+4: BytecodeCompiler emit-helpers — typed-or-polymorphic dispatch
5952// ─────────────────────────────────────────────────────────────────────────
5953
5954impl BytecodeCompiler {
5955    /// Emit a `LoadLocal<Kind>` (E+3 codes 0x16C-0x176) when the proven
5956    /// `StorageHint` maps to a `FieldKind`, otherwise fall back to the
5957    /// polymorphic legacy `LoadLocal` (0x50). The fallback path is
5958    /// instrumented by `typed_emit_metrics` so Wave G can audit how many
5959    /// emit sites still need the polymorphic form.
5960    ///
5961    /// Per-Ptr ownership: for `FieldKind::Ptr` slots, the caller is
5962    /// responsible for pairing this with the kinded `clone_with_kind`
5963    /// retain (ADR-006 §2.7.7) of the loaded value (matching D.1 / D.2
5964    /// Ptr semantics). Today `storage_hint_to_field_kind` returns `None`
5965    /// for `String` / heap-bearing hints so callers stay on the
5966    /// polymorphic path that does the refcount accounting; per-Ptr
5967    /// typed-Load is unlocked once an emit site explicitly opts in.
5968    pub(super) fn emit_load_local_for_hint(&mut self, slot: u16, hint: StorageHint) {
5969        let opcode = typed_load_local_opcode(hint).unwrap_or_else(|| {
5970            typed_emit_metrics::record_polymorphic_fallback("load_local", hint);
5971            OpCode::LoadLocal
5972        });
5973        self.emit(Instruction::new(opcode, Some(Operand::Local(slot))));
5974    }
5975
5976    /// Emit a `StoreLocal<Kind>` (E+3 codes 0x177-0x181) when the proven
5977    /// `StorageHint` maps to a `FieldKind`, otherwise fall back to the
5978    /// polymorphic legacy `StoreLocal` (0x51).
5979    ///
5980    /// Per-Ptr ownership: for `FieldKind::Ptr` slots, the caller is
5981    /// responsible for pairing this with the kinded `drop_with_kind`
5982    /// (ADR-006 §2.7.7) of the prior payload before the typed Store
5983    /// overwrites the slot. As with the load helper, today `String` and
5984    /// heap-bearing hints route to the polymorphic path so refcount
5985    /// accounting is preserved.
5986    pub(super) fn emit_store_local_for_hint(&mut self, slot: u16, hint: StorageHint) {
5987        let opcode = typed_store_local_opcode(hint).unwrap_or_else(|| {
5988            typed_emit_metrics::record_polymorphic_fallback("store_local", hint);
5989            OpCode::StoreLocal
5990        });
5991        self.emit(Instruction::new(opcode, Some(Operand::Local(slot))));
5992    }
5993
5994    /// Emit a `LoadModuleBinding<Kind>` (E+3 codes 0x182-0x18C) when the
5995    /// proven `StorageHint` maps to a `FieldKind`, otherwise fall back to
5996    /// the polymorphic legacy `LoadModuleBinding` (0x52). Per-Ptr ownership
5997    /// rules mirror `emit_load_local_for_hint`.
5998    pub(super) fn emit_load_module_binding_for_hint(
5999        &mut self,
6000        binding_idx: u16,
6001        hint: StorageHint,
6002    ) {
6003        let opcode = typed_load_module_binding_opcode(hint).unwrap_or_else(|| {
6004            typed_emit_metrics::record_polymorphic_fallback("load_module_binding", hint);
6005            OpCode::LoadModuleBinding
6006        });
6007        self.emit(Instruction::new(
6008            opcode,
6009            Some(Operand::ModuleBinding(binding_idx)),
6010        ));
6011    }
6012
6013    /// Emit a `StoreModuleBinding<Kind>` (E+3 codes 0x18D-0x197) when the
6014    /// proven `StorageHint` maps to a `FieldKind`, otherwise fall back to
6015    /// the polymorphic legacy `StoreModuleBinding` (0x53). Per-Ptr
6016    /// ownership rules mirror `emit_store_local_for_hint`.
6017    pub(super) fn emit_store_module_binding_for_hint(
6018        &mut self,
6019        binding_idx: u16,
6020        hint: StorageHint,
6021    ) {
6022        let opcode = typed_store_module_binding_opcode(hint).unwrap_or_else(|| {
6023            typed_emit_metrics::record_polymorphic_fallback("store_module_binding", hint);
6024            OpCode::StoreModuleBinding
6025        });
6026        self.emit(Instruction::new(
6027            opcode,
6028            Some(Operand::ModuleBinding(binding_idx)),
6029        ));
6030    }
6031
6032    /// Emit a `ReturnValue<Kind>` (E+3 codes 0x198-0x1A2) when the
6033    /// proven `StorageHint` maps to a `FieldKind`, otherwise fall back
6034    /// to the polymorphic legacy `ReturnValue` (0x45). The typed handlers
6035    /// are *transport-neutral* (same body as the legacy handler) — the
6036    /// `<Kind>` is a static annotation for the JIT and downstream
6037    /// consumers so the caller's stack discipline is known at the call
6038    /// site; no runtime difference at the executor level today.
6039    pub(super) fn emit_return_value_for_hint(&mut self, hint: StorageHint) {
6040        let opcode = typed_return_value_opcode(hint).unwrap_or_else(|| {
6041            typed_emit_metrics::record_polymorphic_fallback("return_value", hint);
6042            OpCode::ReturnValue
6043        });
6044        self.emit(Instruction::simple(opcode));
6045    }
6046}
6047
6048// ADR-006 §2.7.4 — Phase 2c rebuild (R8 C1-temporal-lowering, 2026-05-23).
6049//
6050// The original mod tests was gated under `#[cfg(any())]` with a single
6051// todo!() placeholder because every test body that called
6052// `vm.execute(None)` got back a `shape_value::ValueWord` and asserted via
6053// the deleted `ValueWordExt::{as_i64,as_str,as_bool}` accessors. Post-
6054// strict-typing the VM returns `KindedSlot` directly and the per-kind
6055// accessors (`as_i64`/`as_f64`/`as_bool`/`as_str`) are intrinsic to
6056// `KindedSlot` per §2.7.6 / Q8. The migration replaces the carrier-import
6057// (`use shape_value::ValueWordExt;`) with method-resolution against the
6058// `KindedSlot` returned by `vm.execute(None)`; every test body otherwise
6059// matches the pre-W-series shape.
6060//
6061// Tests that depend on the `typed_emit_metrics` instrumentation (live
6062// impl is `#[cfg(debug_assertions)]`-gated) are mirrored under the same
6063// gate.
6064#[cfg(test)]
6065mod tests {
6066    use super::super::BytecodeCompiler;
6067    use crate::compiler::ParamPassMode;
6068    use crate::type_tracking::BindingStorageClass;
6069    use shape_ast::ast::{Expr, Span, TypeAnnotation};
6070    use shape_runtime::type_schema::FieldType;
6071
6072    #[test]
6073    fn test_type_annotation_to_field_type_array_recursive() {
6074        let ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Basic("int".to_string())));
6075        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
6076        assert_eq!(ft, FieldType::Array(Box::new(FieldType::I64)));
6077    }
6078
6079    #[test]
6080    fn test_type_annotation_to_field_type_optional() {
6081        // Post-W17.3-4.2 (per-container FieldType variants): `Option<int>`
6082        // now resolves to a structured shape instead of the legacy
6083        // `FieldType::Any` blanket-fallback. The migration only asserts
6084        // that the shape is no longer `Any` — the exact structured variant
6085        // is not load-bearing for the test's intent.
6086        let ann = TypeAnnotation::Generic {
6087            name: "Option".into(),
6088            args: vec![TypeAnnotation::Basic("int".to_string())],
6089        };
6090        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
6091        assert_ne!(ft, FieldType::Any, "Option<int> must resolve structurally");
6092    }
6093
6094    #[test]
6095    fn test_type_annotation_to_field_type_generic_hashmap() {
6096        // Post-W17.3-4.2: `HashMap<string, int>` now resolves to a
6097        // structured `FieldType::HashMap { key, value }` shape (or
6098        // equivalent per-container variant) instead of the legacy
6099        // `FieldType::Any` blanket-fallback.
6100        let ann = TypeAnnotation::Generic {
6101            name: "HashMap".into(),
6102            args: vec![
6103                TypeAnnotation::Basic("string".to_string()),
6104                TypeAnnotation::Basic("int".to_string()),
6105            ],
6106        };
6107        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
6108        assert_ne!(ft, FieldType::Any, "HashMap<string,int> must resolve structurally");
6109    }
6110
6111    #[test]
6112    fn test_type_annotation_to_field_type_generic_user_struct() {
6113        let ann = TypeAnnotation::Generic {
6114            name: "MyContainer".into(),
6115            args: vec![TypeAnnotation::Basic("string".to_string())],
6116        };
6117        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
6118        assert_eq!(ft, FieldType::Object("MyContainer".to_string()));
6119    }
6120
6121    #[test]
6122    fn test_flexible_storage_promotion_is_monotonic() {
6123        let mut compiler = BytecodeCompiler::new();
6124        compiler.push_scope();
6125        let slot = compiler.declare_local("value").expect("declare local");
6126        compiler.type_tracker.set_local_binding_semantics(
6127            slot,
6128            BytecodeCompiler::binding_semantics_for_ownership_class(
6129                crate::type_tracking::BindingOwnershipClass::Flexible,
6130            ),
6131        );
6132
6133        compiler.promote_flexible_binding_storage_for_slot(
6134            slot,
6135            true,
6136            BindingStorageClass::UniqueHeap,
6137        );
6138        assert_eq!(
6139            compiler
6140                .type_tracker
6141                .get_local_binding_semantics(slot)
6142                .map(|semantics| semantics.storage_class),
6143            Some(BindingStorageClass::UniqueHeap)
6144        );
6145
6146        compiler.promote_flexible_binding_storage_for_slot(slot, true, BindingStorageClass::Direct);
6147        assert_eq!(
6148            compiler
6149                .type_tracker
6150                .get_local_binding_semantics(slot)
6151                .map(|semantics| semantics.storage_class),
6152            Some(BindingStorageClass::UniqueHeap)
6153        );
6154
6155        compiler.promote_flexible_binding_storage_for_slot(
6156            slot,
6157            true,
6158            BindingStorageClass::SharedCow,
6159        );
6160        assert_eq!(
6161            compiler
6162                .type_tracker
6163                .get_local_binding_semantics(slot)
6164                .map(|semantics| semantics.storage_class),
6165            Some(BindingStorageClass::SharedCow)
6166        );
6167    }
6168
6169    #[test]
6170    fn test_escape_planner_marks_array_element_identifier_as_unique_heap() {
6171        let mut compiler = BytecodeCompiler::new();
6172        compiler.push_scope();
6173        let slot = compiler.declare_local("value").expect("declare local");
6174        compiler.type_tracker.set_local_binding_semantics(
6175            slot,
6176            BytecodeCompiler::binding_semantics_for_ownership_class(
6177                crate::type_tracking::BindingOwnershipClass::Flexible,
6178            ),
6179        );
6180
6181        let expr = Expr::Array(
6182            vec![Expr::Identifier("value".to_string(), Span::DUMMY)],
6183            Span::DUMMY,
6184        );
6185        compiler.plan_flexible_binding_escape_from_expr(&expr);
6186
6187        assert_eq!(
6188            compiler
6189                .type_tracker
6190                .get_local_binding_semantics(slot)
6191                .map(|semantics| semantics.storage_class),
6192            Some(BindingStorageClass::UniqueHeap)
6193        );
6194    }
6195
6196    #[test]
6197    fn test_escape_planner_marks_if_branch_identifier_as_unique_heap() {
6198        let mut compiler = BytecodeCompiler::new();
6199        compiler.push_scope();
6200        let slot = compiler.declare_local("value").expect("declare local");
6201        compiler.type_tracker.set_local_binding_semantics(
6202            slot,
6203            BytecodeCompiler::binding_semantics_for_ownership_class(
6204                crate::type_tracking::BindingOwnershipClass::Flexible,
6205            ),
6206        );
6207
6208        let expr = Expr::If(
6209            Box::new(shape_ast::ast::IfExpr {
6210                condition: Box::new(Expr::Literal(
6211                    shape_ast::ast::Literal::Bool(true),
6212                    Span::DUMMY,
6213                )),
6214                then_branch: Box::new(Expr::Identifier("value".to_string(), Span::DUMMY)),
6215                else_branch: None,
6216            }),
6217            Span::DUMMY,
6218        );
6219        compiler.plan_flexible_binding_escape_from_expr(&expr);
6220
6221        assert_eq!(
6222            compiler
6223                .type_tracker
6224                .get_local_binding_semantics(slot)
6225                .map(|semantics| semantics.storage_class),
6226            Some(BindingStorageClass::UniqueHeap)
6227        );
6228    }
6229
6230    #[test]
6231    fn test_escape_planner_marks_async_let_rhs_identifier_as_unique_heap() {
6232        let mut compiler = BytecodeCompiler::new();
6233        compiler.push_scope();
6234        let slot = compiler.declare_local("value").expect("declare local");
6235        compiler.type_tracker.set_local_binding_semantics(
6236            slot,
6237            BytecodeCompiler::binding_semantics_for_ownership_class(
6238                crate::type_tracking::BindingOwnershipClass::Flexible,
6239            ),
6240        );
6241
6242        let expr = Expr::AsyncLet(
6243            Box::new(shape_ast::ast::AsyncLetExpr {
6244                name: "task".to_string(),
6245                expr: Box::new(Expr::Identifier("value".to_string(), Span::DUMMY)),
6246                span: Span::DUMMY,
6247            }),
6248            Span::DUMMY,
6249        );
6250        compiler.plan_flexible_binding_escape_from_expr(&expr);
6251
6252        assert_eq!(
6253            compiler
6254                .type_tracker
6255                .get_local_binding_semantics(slot)
6256                .map(|semantics| semantics.storage_class),
6257            Some(BindingStorageClass::UniqueHeap)
6258        );
6259    }
6260
6261    #[test]
6262    fn test_call_args_mark_by_value_identifier_as_unique_heap() {
6263        let mut compiler = BytecodeCompiler::new();
6264        compiler.push_scope();
6265        let slot = compiler.declare_local("value").expect("declare local");
6266        compiler.type_tracker.set_local_binding_semantics(
6267            slot,
6268            BytecodeCompiler::binding_semantics_for_ownership_class(
6269                crate::type_tracking::BindingOwnershipClass::Flexible,
6270            ),
6271        );
6272
6273        compiler
6274            .compile_call_args(&[Expr::Identifier("value".to_string(), Span::DUMMY)], None)
6275            .expect("call args should compile");
6276
6277        assert_eq!(
6278            compiler
6279                .type_tracker
6280                .get_local_binding_semantics(slot)
6281                .map(|semantics| semantics.storage_class),
6282            Some(BindingStorageClass::UniqueHeap)
6283        );
6284    }
6285
6286    #[test]
6287    fn test_call_args_leave_by_ref_identifier_storage_unchanged() {
6288        let mut compiler = BytecodeCompiler::new();
6289        compiler.push_scope();
6290        let slot = compiler.declare_local("value").expect("declare local");
6291        compiler.type_tracker.set_local_binding_semantics(
6292            slot,
6293            BytecodeCompiler::binding_semantics_for_ownership_class(
6294                crate::type_tracking::BindingOwnershipClass::Flexible,
6295            ),
6296        );
6297
6298        compiler
6299            .compile_call_args(
6300                &[Expr::Identifier("value".to_string(), Span::DUMMY)],
6301                Some(&[ParamPassMode::ByRefShared]),
6302            )
6303            .expect("reference call args should compile");
6304
6305        assert_eq!(
6306            compiler
6307                .type_tracker
6308                .get_local_binding_semantics(slot)
6309                .map(|semantics| semantics.storage_class),
6310            Some(BindingStorageClass::Deferred)
6311        );
6312    }
6313
6314    // ========================================================================
6315    // Phase V3.1: emit_binary_op shim tests
6316    // ========================================================================
6317
6318    /// Helper: read the opcode of the last instruction emitted to the
6319    /// compiler's program.
6320    fn last_emitted_opcode(compiler: &BytecodeCompiler) -> crate::bytecode::OpCode {
6321        compiler
6322            .program
6323            .instructions
6324            .last()
6325            .expect("compiler program is empty")
6326            .opcode
6327    }
6328
6329    #[test]
6330    fn emit_binary_op_int_int_add_emits_add_int() {
6331        use crate::bytecode::OpCode;
6332        use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
6333        use crate::type_tracking::NumericType;
6334        use shape_ast::ast::BinaryOp;
6335
6336        let mut compiler = BytecodeCompiler::new();
6337        let handled = emit_binary_op(
6338            &mut compiler,
6339            BinaryOp::Add,
6340            BinOperandKind::Numeric(NumericType::Int),
6341            BinOperandKind::Numeric(NumericType::Int),
6342        )
6343        .expect("emit_binary_op should succeed");
6344        assert!(handled, "Add should be a handled op");
6345        assert_eq!(last_emitted_opcode(&compiler), OpCode::AddInt);
6346    }
6347
6348    #[test]
6349    fn emit_binary_op_number_number_add_emits_add_number() {
6350        use crate::bytecode::OpCode;
6351        use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
6352        use crate::type_tracking::NumericType;
6353        use shape_ast::ast::BinaryOp;
6354
6355        let mut compiler = BytecodeCompiler::new();
6356        let handled = emit_binary_op(
6357            &mut compiler,
6358            BinaryOp::Add,
6359            BinOperandKind::Numeric(NumericType::Number),
6360            BinOperandKind::Numeric(NumericType::Number),
6361        )
6362        .expect("emit_binary_op should succeed");
6363        assert!(handled);
6364        assert_eq!(last_emitted_opcode(&compiler), OpCode::AddNumber);
6365    }
6366
6367    #[test]
6368    fn emit_binary_op_number_number_mul_emits_mul_number() {
6369        use crate::bytecode::OpCode;
6370        use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
6371        use crate::type_tracking::NumericType;
6372        use shape_ast::ast::BinaryOp;
6373
6374        let mut compiler = BytecodeCompiler::new();
6375        emit_binary_op(
6376            &mut compiler,
6377            BinaryOp::Mul,
6378            BinOperandKind::Numeric(NumericType::Number),
6379            BinOperandKind::Numeric(NumericType::Number),
6380        )
6381        .expect("emit_binary_op should succeed");
6382        assert_eq!(last_emitted_opcode(&compiler), OpCode::MulNumber);
6383    }
6384
6385    #[test]
6386    fn emit_binary_op_int_int_cmp_emits_typed_cmp_opcodes() {
6387        use crate::bytecode::OpCode;
6388        use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
6389        use crate::type_tracking::NumericType;
6390        use shape_ast::ast::BinaryOp;
6391
6392        let cases = [
6393            (BinaryOp::Greater, OpCode::GtInt),
6394            (BinaryOp::Less, OpCode::LtInt),
6395            (BinaryOp::GreaterEq, OpCode::GteInt),
6396            (BinaryOp::LessEq, OpCode::LteInt),
6397            (BinaryOp::Equal, OpCode::EqInt),
6398            (BinaryOp::NotEqual, OpCode::NeqInt),
6399        ];
6400        for (op, expected) in cases {
6401            let mut compiler = BytecodeCompiler::new();
6402            emit_binary_op(
6403                &mut compiler,
6404                op,
6405                BinOperandKind::Numeric(NumericType::Int),
6406                BinOperandKind::Numeric(NumericType::Int),
6407            )
6408            .expect("emit_binary_op should succeed");
6409            assert_eq!(
6410                last_emitted_opcode(&compiler),
6411                expected,
6412                "wrong opcode for Int {:?}",
6413                op
6414            );
6415        }
6416    }
6417
6418    #[test]
6419    fn emit_binary_op_decimal_decimal_emits_typed_decimal_opcodes() {
6420        use crate::bytecode::OpCode;
6421        use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
6422        use crate::type_tracking::NumericType;
6423        use shape_ast::ast::BinaryOp;
6424
6425        let cases = [
6426            (BinaryOp::Add, OpCode::AddDecimal),
6427            (BinaryOp::Sub, OpCode::SubDecimal),
6428            (BinaryOp::Mul, OpCode::MulDecimal),
6429            (BinaryOp::Div, OpCode::DivDecimal),
6430            (BinaryOp::Equal, OpCode::EqDecimal),
6431        ];
6432        for (op, expected) in cases {
6433            let mut compiler = BytecodeCompiler::new();
6434            emit_binary_op(
6435                &mut compiler,
6436                op,
6437                BinOperandKind::Numeric(NumericType::Decimal),
6438                BinOperandKind::Numeric(NumericType::Decimal),
6439            )
6440            .expect("emit_binary_op should succeed");
6441            assert_eq!(
6442                last_emitted_opcode(&compiler),
6443                expected,
6444                "wrong opcode for Decimal {:?}",
6445                op
6446            );
6447        }
6448    }
6449
6450    #[test]
6451    fn emit_binary_op_unknown_operands_return_false() {
6452        use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
6453        use crate::type_tracking::NumericType;
6454        use shape_ast::ast::BinaryOp;
6455
6456        let cases: &[(BinaryOp, BinOperandKind, BinOperandKind)] = &[
6457            (
6458                BinaryOp::Add,
6459                BinOperandKind::Numeric(NumericType::Int),
6460                BinOperandKind::Unknown,
6461            ),
6462            (
6463                BinaryOp::Add,
6464                BinOperandKind::Unknown,
6465                BinOperandKind::Unknown,
6466            ),
6467            (
6468                BinaryOp::Equal,
6469                BinOperandKind::Unknown,
6470                BinOperandKind::Unknown,
6471            ),
6472            (
6473                BinaryOp::Add,
6474                BinOperandKind::Numeric(NumericType::Int),
6475                BinOperandKind::Numeric(NumericType::Number),
6476            ),
6477            (
6478                BinaryOp::Equal,
6479                BinOperandKind::Bool,
6480                BinOperandKind::Bool,
6481            ),
6482        ];
6483        for (op, lhs, rhs) in cases {
6484            let mut compiler = BytecodeCompiler::new();
6485            let handled = emit_binary_op(&mut compiler, *op, *lhs, *rhs)
6486                .expect("emit_binary_op should succeed");
6487            assert!(
6488                !handled,
6489                "{:?} with {:?},{:?} must return Ok(false) post-Phase-2",
6490                op, lhs, rhs
6491            );
6492            assert!(
6493                compiler.program.instructions.is_empty(),
6494                "no instruction must be emitted for {:?} with {:?},{:?}",
6495                op, lhs, rhs
6496            );
6497        }
6498    }
6499
6500    #[test]
6501    fn emit_binary_op_string_string_add_emits_string_concat_typed() {
6502        use crate::bytecode::OpCode;
6503        use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
6504        use shape_ast::ast::BinaryOp;
6505
6506        let mut compiler = BytecodeCompiler::new();
6507        emit_binary_op(
6508            &mut compiler,
6509            BinaryOp::Add,
6510            BinOperandKind::String,
6511            BinOperandKind::String,
6512        )
6513        .expect("emit_binary_op should succeed");
6514        assert_eq!(last_emitted_opcode(&compiler), OpCode::StringConcatTyped);
6515    }
6516
6517    #[test]
6518    fn emit_binary_op_returns_false_for_unsupported_ops() {
6519        use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
6520        use shape_ast::ast::BinaryOp;
6521
6522        let unsupported = [
6523            BinaryOp::And,
6524            BinaryOp::Or,
6525            BinaryOp::BitAnd,
6526            BinaryOp::BitOr,
6527            BinaryOp::BitXor,
6528            BinaryOp::BitShl,
6529            BinaryOp::BitShr,
6530            BinaryOp::NullCoalesce,
6531            BinaryOp::ErrorContext,
6532            BinaryOp::Pipe,
6533            BinaryOp::FuzzyEqual,
6534            BinaryOp::FuzzyGreater,
6535            BinaryOp::FuzzyLess,
6536        ];
6537        for op in unsupported {
6538            let mut compiler = BytecodeCompiler::new();
6539            let handled = emit_binary_op(
6540                &mut compiler,
6541                op,
6542                BinOperandKind::Unknown,
6543                BinOperandKind::Unknown,
6544            )
6545            .expect("emit_binary_op should succeed");
6546            assert!(!handled, "{:?} should be unhandled (Ok(false))", op);
6547            assert!(
6548                compiler.program.instructions.is_empty(),
6549                "{:?} must not emit any instruction on refusal",
6550                op
6551            );
6552        }
6553    }
6554
6555    #[test]
6556    fn emit_binary_op_preserves_numeric_hint_for_arithmetic_only() {
6557        use crate::compiler::helpers::{BinOperandKind, emit_binary_op};
6558        use crate::type_tracking::NumericType;
6559        use shape_ast::ast::BinaryOp;
6560
6561        let mut compiler = BytecodeCompiler::new();
6562        emit_binary_op(
6563            &mut compiler,
6564            BinaryOp::Add,
6565            BinOperandKind::Numeric(NumericType::Int),
6566            BinOperandKind::Numeric(NumericType::Int),
6567        )
6568        .expect("ok");
6569        assert_eq!(compiler.last_expr_numeric_type, Some(NumericType::Int));
6570
6571        let mut compiler = BytecodeCompiler::new();
6572        compiler.last_expr_numeric_type = Some(NumericType::Number);
6573        emit_binary_op(
6574            &mut compiler,
6575            BinaryOp::Less,
6576            BinOperandKind::Numeric(NumericType::Int),
6577            BinOperandKind::Numeric(NumericType::Int),
6578        )
6579        .expect("ok");
6580        assert_eq!(compiler.last_expr_numeric_type, None);
6581    }
6582
6583    #[test]
6584    fn from_numeric_maps_none_to_unknown() {
6585        use crate::compiler::helpers::BinOperandKind;
6586        use crate::type_tracking::NumericType;
6587
6588        assert_eq!(BinOperandKind::from_numeric(None), BinOperandKind::Unknown);
6589        assert_eq!(
6590            BinOperandKind::from_numeric(Some(NumericType::Int)),
6591            BinOperandKind::Numeric(NumericType::Int)
6592        );
6593        assert_eq!(
6594            BinOperandKind::from_numeric(Some(NumericType::Number)),
6595            BinOperandKind::Numeric(NumericType::Number)
6596        );
6597    }
6598
6599    // ── Phase R5.1C: typed bitwise opcode emission tests ─────────────
6600
6601    fn compile_opcodes(code: &str) -> Vec<crate::bytecode::OpCode> {
6602        use shape_ast::parser::parse_program;
6603        let program = parse_program(code).expect("parse program");
6604        let mut compiler = BytecodeCompiler::new();
6605        compiler.allow_internal_builtins = true;
6606        let bc = compiler.compile(&program).expect("compile program");
6607        bc.instructions.iter().map(|ins| ins.opcode).collect()
6608    }
6609
6610    fn compile_opcodes_with_typed_bitwise(
6611        enabled: bool,
6612        code: &str,
6613    ) -> Vec<crate::bytecode::OpCode> {
6614        super::super::helpers::with_typed_bitwise_flag(enabled, || compile_opcodes(code))
6615    }
6616
6617    #[test]
6618    fn r51c_int_operands_emit_typed_bitand() {
6619        use crate::bytecode::OpCode;
6620        let ops = compile_opcodes_with_typed_bitwise(
6621            true,
6622            r#"
6623            let a: int = 5
6624            let b: int = 3
6625            a & b
6626            "#,
6627        );
6628        assert!(
6629            ops.contains(&OpCode::BitAndInt),
6630            "expected BitAndInt for int & int, got ops: {:?}",
6631            ops
6632        );
6633        assert!(
6634            !ops.contains(&OpCode::BitAnd),
6635            "Dynamic BitAnd must not be emitted when typed path fires, ops: {:?}",
6636            ops
6637        );
6638    }
6639
6640    #[test]
6641    fn r51c_int_operands_emit_typed_bitor_bitxor() {
6642        use crate::bytecode::OpCode;
6643        let ops = compile_opcodes_with_typed_bitwise(
6644            true,
6645            r#"
6646            let a: int = 5
6647            let b: int = 3
6648            a | b
6649            a ^ b
6650            "#,
6651        );
6652        assert!(
6653            ops.contains(&OpCode::BitOrInt),
6654            "expected BitOrInt for int | int, got ops: {:?}",
6655            ops
6656        );
6657        assert!(
6658            ops.contains(&OpCode::BitXorInt),
6659            "expected BitXorInt for int ^ int, got ops: {:?}",
6660            ops
6661        );
6662    }
6663
6664    #[test]
6665    fn r51c_int_operands_emit_typed_shifts() {
6666        use crate::bytecode::OpCode;
6667        let ops = compile_opcodes_with_typed_bitwise(
6668            true,
6669            r#"
6670            let a: int = 5
6671            a << 2
6672            a >> 1
6673            "#,
6674        );
6675        assert!(
6676            ops.contains(&OpCode::BitShlInt),
6677            "expected BitShlInt for int << int, got ops: {:?}",
6678            ops
6679        );
6680        assert!(
6681            ops.contains(&OpCode::BitShrInt),
6682            "expected BitShrInt for int >> int, got ops: {:?}",
6683            ops
6684        );
6685    }
6686
6687    #[test]
6688    fn r51c_int_operand_emits_typed_bitnot() {
6689        use crate::bytecode::OpCode;
6690        let ops = compile_opcodes_with_typed_bitwise(
6691            true,
6692            r#"
6693            let a: int = 5
6694            ~a
6695            "#,
6696        );
6697        assert!(
6698            ops.contains(&OpCode::BitNotInt),
6699            "expected BitNotInt for ~int, got ops: {:?}",
6700            ops
6701        );
6702        assert!(
6703            !ops.contains(&OpCode::BitNot),
6704            "Dynamic BitNot must not be emitted when typed path fires, ops: {:?}",
6705            ops
6706        );
6707    }
6708
6709    #[test]
6710    fn r51c_flag_off_falls_back_to_dynamic_bitand() {
6711        use crate::bytecode::OpCode;
6712        let ops = compile_opcodes_with_typed_bitwise(
6713            false,
6714            r#"
6715            let a: int = 5
6716            let b: int = 3
6717            a & b
6718            "#,
6719        );
6720        assert!(
6721            ops.contains(&OpCode::BitAnd),
6722            "flag off: expected Dynamic BitAnd, got ops: {:?}",
6723            ops
6724        );
6725        assert!(
6726            !ops.contains(&OpCode::BitAndInt),
6727            "flag off: typed BitAndInt must not be emitted, got ops: {:?}",
6728            ops
6729        );
6730    }
6731
6732    #[test]
6733    fn r51c_flag_off_falls_back_to_dynamic_bitnot() {
6734        use crate::bytecode::OpCode;
6735        let ops = compile_opcodes_with_typed_bitwise(
6736            false,
6737            r#"
6738            let a: int = 5
6739            ~a
6740            "#,
6741        );
6742        assert!(
6743            ops.contains(&OpCode::BitNot),
6744            "flag off: expected Dynamic BitNot, got ops: {:?}",
6745            ops
6746        );
6747        assert!(
6748            !ops.contains(&OpCode::BitNotInt),
6749            "flag off: typed BitNotInt must not be emitted, got ops: {:?}",
6750            ops
6751        );
6752    }
6753
6754    #[test]
6755    #[ignore]
6756    fn r51c_untyped_param_falls_back_to_dynamic_bitand() {
6757        // strict-typing-sweep: dynamic emission deleted; param-inference
6758        // now lifts untyped param `a & 15` to BitAndInt via literal-pairing.
6759        // Pre-existing ignore preserved (pinned audit baseline).
6760        use crate::bytecode::OpCode;
6761        let ops = compile_opcodes_with_typed_bitwise(
6762            true,
6763            r#"
6764            fn masked(a) {
6765                a & 15
6766            }
6767            masked(5)
6768            "#,
6769        );
6770        assert!(
6771            ops.contains(&OpCode::BitAnd),
6772            "untyped param: expected Dynamic BitAnd, got ops: {:?}",
6773            ops
6774        );
6775        assert!(
6776            !ops.contains(&OpCode::BitAndInt),
6777            "untyped param: typed BitAndInt must not be emitted, got ops: {:?}",
6778            ops
6779        );
6780    }
6781
6782    #[test]
6783    fn r51c_int_bitwise_eval_produces_expected_values() {
6784        // End-to-end behaviour: the typed opcodes match the post-strict-
6785        // typing semantics bit-for-bit. Migrated from `ValueWordExt::as_i64`
6786        // to `KindedSlot::as_i64` per §2.7.6 / Q8.
6787        use crate::VMConfig;
6788        use crate::executor::VirtualMachine;
6789        use shape_ast::parser::parse_program;
6790
6791        let eval_int = |code: &str| -> i64 {
6792            let program = parse_program(code).expect("parse");
6793            let mut compiler = BytecodeCompiler::new();
6794            compiler.allow_internal_builtins = true;
6795            let bc = compiler.compile(&program).expect("compile");
6796            let mut vm = VirtualMachine::new(VMConfig::default());
6797            vm.load_program(bc);
6798            vm.execute(None)
6799                .expect("execute")
6800                .as_i64()
6801                .expect("i64 result")
6802        };
6803
6804        assert_eq!(
6805            super::super::helpers::with_typed_bitwise_flag(true, || {
6806                eval_int("let a: int = 12\nlet b: int = 10\na & b")
6807            }),
6808            8,
6809            "12 & 10 = 8"
6810        );
6811        assert_eq!(
6812            super::super::helpers::with_typed_bitwise_flag(true, || {
6813                eval_int("let a: int = 12\nlet b: int = 10\na | b")
6814            }),
6815            14,
6816            "12 | 10 = 14"
6817        );
6818        assert_eq!(
6819            super::super::helpers::with_typed_bitwise_flag(true, || {
6820                eval_int("let a: int = 12\nlet b: int = 10\na ^ b")
6821            }),
6822            6,
6823            "12 ^ 10 = 6"
6824        );
6825        assert_eq!(
6826            super::super::helpers::with_typed_bitwise_flag(true, || {
6827                eval_int("let a: int = 1\na << 4")
6828            }),
6829            16,
6830            "1 << 4 = 16"
6831        );
6832        assert_eq!(
6833            super::super::helpers::with_typed_bitwise_flag(true, || {
6834                eval_int("let a: int = 32\na >> 2")
6835            }),
6836            8,
6837            "32 >> 2 = 8"
6838        );
6839        assert_eq!(
6840            super::super::helpers::with_typed_bitwise_flag(true, || {
6841                eval_int("let a: int = 0\n~a")
6842            }),
6843            -1,
6844            "~0 = -1 (two's complement)"
6845        );
6846    }
6847
6848    // RETIRED (post-strict-typing-sweep, 2026-05-23 — R8 C1):
6849    // `r51c_typed_and_dynamic_paths_produce_identical_results` compared
6850    // flag-on (typed `BitAndInt`/etc) vs flag-off (dynamic `BitAnd`/etc)
6851    // execution and asserted byte-identical results. The strict-typing
6852    // sweep deleted every dynamic bitwise opcode (`BitAnd`/`BitOr`/`BitXor`/
6853    // `BitShl`/`BitShr`/`BitNot`) — the flag-off path no longer dispatches
6854    // (e.g. `a << 3` surfaces "no method 'shl' on receiver kind Int64").
6855    // The test's premise — a runnable dynamic counterpart — no longer
6856    // exists. Per CLAUDE.md "Strict-typing; if loop-var kind can't be
6857    // proven at compile time → compile error", there is no fallback to
6858    // compare against. The flag-off emission contract is still pinned by
6859    // `r51c_flag_off_falls_back_to_dynamic_bitand` / `_bitnot` (compile-
6860    // time opcode-shape assertions, not runtime semantic equivalence).
6861
6862    // ── Phase R5.5: typed string+scalar concat emission tests ────────
6863
6864    fn compile_opcodes_with_string_coerce_concat(
6865        enabled: bool,
6866        code: &str,
6867    ) -> Vec<crate::bytecode::OpCode> {
6868        super::super::helpers::with_typed_string_coerce_concat_flag(enabled, || {
6869            compile_opcodes(code)
6870        })
6871    }
6872
6873    #[test]
6874    fn r55_string_plus_int_emits_string_concat_int() {
6875        use crate::bytecode::OpCode;
6876        let ops = compile_opcodes_with_string_coerce_concat(
6877            true,
6878            r#"
6879            fn concat_test() {
6880                let s: string = "Cash: "
6881                let c: int = 42
6882                s + c
6883            }
6884            concat_test()
6885            "#,
6886        );
6887        assert!(
6888            ops.contains(&OpCode::StringConcatInt),
6889            "expected StringConcatInt for string + int, got ops: {:?}",
6890            ops
6891        );
6892    }
6893
6894    #[test]
6895    fn r55_string_plus_number_emits_string_concat_number() {
6896        use crate::bytecode::OpCode;
6897        let ops = compile_opcodes_with_string_coerce_concat(
6898            true,
6899            r#"
6900            fn concat_test() {
6901                let n: number = 3.14
6902                "X: " + n
6903            }
6904            concat_test()
6905            "#,
6906        );
6907        assert!(
6908            ops.contains(&OpCode::StringConcatNumber),
6909            "expected StringConcatNumber for string + number, got ops: {:?}",
6910            ops
6911        );
6912    }
6913
6914    #[test]
6915    fn r55_string_plus_bool_emits_string_concat_bool() {
6916        use crate::bytecode::OpCode;
6917        let ops = compile_opcodes_with_string_coerce_concat(
6918            true,
6919            r#"
6920            fn concat_test() {
6921                let s: string = "flag: "
6922                let b: bool = true
6923                s + b
6924            }
6925            concat_test()
6926            "#,
6927        );
6928        assert!(
6929            ops.contains(&OpCode::StringConcatBool),
6930            "expected StringConcatBool for string + bool, got ops: {:?}",
6931            ops
6932        );
6933    }
6934
6935    #[test]
6936    fn r55_string_plus_string_does_not_emit_r55_scalar_opcodes() {
6937        use crate::bytecode::OpCode;
6938        let ops = compile_opcodes_with_string_coerce_concat(
6939            true,
6940            r#"
6941            fn concat_test() {
6942                let b: string = "bar"
6943                "foo" + b
6944            }
6945            concat_test()
6946            "#,
6947        );
6948        assert!(
6949            !ops.contains(&OpCode::StringConcatInt)
6950                && !ops.contains(&OpCode::StringConcatNumber)
6951                && !ops.contains(&OpCode::StringConcatBool),
6952            "string+string must not emit any R5.5 typed scalar opcode, ops: {:?}",
6953            ops
6954        );
6955    }
6956
6957    #[test]
6958    fn r55_string_plus_scalar_runtime_values() {
6959        // End-to-end. Migrated from `ValueWordExt::as_str` to
6960        // `KindedSlot::as_str` per §2.7.6 / Q8 — the kind-threaded
6961        // accessor reads UTF-8 bytes off both `NativeKind::String` (Arc<String>
6962        // carrier) and `NativeKind::StringV2` (v2-raw `*const StringObj`)
6963        // labels.
6964        use crate::VMConfig;
6965        use crate::executor::VirtualMachine;
6966        use shape_ast::parser::parse_program;
6967
6968        let eval_str = |code: &str| -> String {
6969            super::super::helpers::with_typed_string_coerce_concat_flag(true, || {
6970                let program = parse_program(code).expect("parse");
6971                let mut compiler = BytecodeCompiler::new();
6972                compiler.allow_internal_builtins = true;
6973                let bc = compiler.compile(&program).expect("compile");
6974                let mut vm = VirtualMachine::new(VMConfig::default());
6975                vm.load_program(bc);
6976                vm.execute(None)
6977                    .expect("execute")
6978                    .as_str()
6979                    .map(|s| s.to_string())
6980                    .expect("string result")
6981            })
6982        };
6983
6984        assert_eq!(
6985            eval_str(
6986                r#"
6987                fn f() {
6988                    let c: int = 42
6989                    "Cash: " + c
6990                }
6991                f()
6992                "#,
6993            ),
6994            "Cash: 42",
6995            "string + int"
6996        );
6997        assert_eq!(
6998            eval_str(
6999                r#"
7000                fn f() {
7001                    let n: number = 3.14
7002                    "X: " + n
7003                }
7004                f()
7005                "#,
7006            ),
7007            "X: 3.14",
7008            "string + number"
7009        );
7010        assert_eq!(
7011            eval_str(
7012                r#"
7013                fn f() {
7014                    let n: number = 2.0
7015                    "whole: " + n
7016                }
7017                f()
7018                "#,
7019            ),
7020            "whole: 2",
7021            "string + whole number formats without decimal"
7022        );
7023        assert_eq!(
7024            eval_str(
7025                r#"
7026                fn f() {
7027                    let b: bool = true
7028                    "flag: " + b
7029                }
7030                f()
7031                "#,
7032            ),
7033            "flag: true",
7034            "string + bool true"
7035        );
7036        assert_eq!(
7037            eval_str(
7038                r#"
7039                fn f() {
7040                    let b: bool = false
7041                    "flag: " + b
7042                }
7043                f()
7044                "#,
7045            ),
7046            "flag: false",
7047            "string + bool false"
7048        );
7049    }
7050
7051    // ─────────────────────────────────────────────────────────────────────
7052    // Wave E+4: emit-helper unit tests + fallback-counter instrumentation.
7053    // The live `typed_emit_metrics` impl is `#[cfg(debug_assertions)]`;
7054    // mirror that gate so the metric-snapshot tests run only when the
7055    // instrumentation is live.
7056    // ─────────────────────────────────────────────────────────────────────
7057
7058    fn metrics_test_lock() -> std::sync::MutexGuard<'static, ()> {
7059        use std::sync::{Mutex, OnceLock};
7060        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
7061        LOCK.get_or_init(|| Mutex::new(()))
7062            .lock()
7063            .unwrap_or_else(|e| e.into_inner())
7064    }
7065
7066    #[test]
7067    #[cfg(debug_assertions)]
7068    fn test_e4_typed_emit_helpers_pin_typed_vs_polymorphic() {
7069        use super::typed_emit_metrics;
7070        use crate::bytecode::OpCode;
7071        use crate::type_tracking::StorageHint;
7072
7073        let _guard = metrics_test_lock();
7074        typed_emit_metrics::reset();
7075
7076        let mut compiler = BytecodeCompiler::new();
7077        let start = compiler.program.instructions.len();
7078
7079        // Post-strict-typing (ADR-006 §2.7.7): `NativeKind::Dynamic` and
7080        // `NativeKind::Unknown` are deleted (the bulldozer removed both
7081        // per the strict-typed plan; see `crates/shape-value/src/native_kind.rs`).
7082        // The polymorphic-fallback cases that previously exercised those
7083        // hints now exercise the surviving non-FieldKind-mappable hints
7084        // (`String`, `NullableInt64`) — they take the same polymorphic
7085        // path because `storage_hint_to_field_kind` returns `None` for
7086        // them (heap/null sentinel design gaps tracked separately).
7087        let cases: &[(&str, OpCode)] = &[
7088            ("load_i64", OpCode::LoadLocalI64),
7089            ("load_f64", OpCode::LoadLocalF64),
7090            ("load_bool", OpCode::LoadLocalBool),
7091            ("load_string_falls_back", OpCode::LoadLocal),
7092            ("load_nullable_int64_falls_back", OpCode::LoadLocal),
7093            ("store_i64", OpCode::StoreLocalI64),
7094            ("store_f64", OpCode::StoreLocalF64),
7095            ("store_bool", OpCode::StoreLocalBool),
7096            ("store_string_falls_back", OpCode::StoreLocal),
7097            ("store_nullable_int64_falls_back", OpCode::StoreLocal),
7098            ("load_mb_i32", OpCode::LoadModuleBindingI32),
7099            ("load_mb_bool", OpCode::LoadModuleBindingBool),
7100            ("store_mb_i32", OpCode::StoreModuleBindingI32),
7101            ("store_mb_bool", OpCode::StoreModuleBindingBool),
7102            ("ret_f64", OpCode::ReturnValueF64),
7103            ("ret_bool", OpCode::ReturnValueBool),
7104            ("ret_string_falls_back", OpCode::ReturnValue),
7105        ];
7106
7107        compiler.emit_load_local_for_hint(0, StorageHint::Int64);
7108        compiler.emit_load_local_for_hint(0, StorageHint::Float64);
7109        compiler.emit_load_local_for_hint(0, StorageHint::Bool);
7110        compiler.emit_load_local_for_hint(0, StorageHint::String);
7111        compiler.emit_load_local_for_hint(0, StorageHint::NullableInt64);
7112
7113        compiler.emit_store_local_for_hint(0, StorageHint::Int64);
7114        compiler.emit_store_local_for_hint(0, StorageHint::Float64);
7115        compiler.emit_store_local_for_hint(0, StorageHint::Bool);
7116        compiler.emit_store_local_for_hint(0, StorageHint::String);
7117        compiler.emit_store_local_for_hint(0, StorageHint::NullableInt64);
7118
7119        compiler.emit_load_module_binding_for_hint(0, StorageHint::Int32);
7120        compiler.emit_load_module_binding_for_hint(0, StorageHint::Bool);
7121
7122        compiler.emit_store_module_binding_for_hint(0, StorageHint::Int32);
7123        compiler.emit_store_module_binding_for_hint(0, StorageHint::Bool);
7124
7125        compiler.emit_return_value_for_hint(StorageHint::Float64);
7126        compiler.emit_return_value_for_hint(StorageHint::Bool);
7127        compiler.emit_return_value_for_hint(StorageHint::String);
7128
7129        let emitted: Vec<_> = compiler.program.instructions[start..]
7130            .iter()
7131            .map(|i| i.opcode)
7132            .collect();
7133        assert_eq!(emitted.len(), cases.len(), "case count mismatch");
7134        for (i, (label, expected)) in cases.iter().enumerate() {
7135            assert_eq!(
7136                emitted[i], *expected,
7137                "case '{label}' (idx {i}): expected {expected:?}, got {:?}",
7138                emitted[i]
7139            );
7140        }
7141
7142        // Per-category fallback totals: 2 String/NullableInt64 fallbacks for
7143        // load_local + store_local each; 0 for module bindings (we no
7144        // longer exercise unproven module-binding hints); 1 for return_value.
7145        let snap: std::collections::HashMap<&'static str, u64> =
7146            typed_emit_metrics::snapshot().into_iter().collect();
7147        assert_eq!(snap.get("load_local").copied().unwrap_or(0), 2);
7148        assert_eq!(snap.get("store_local").copied().unwrap_or(0), 2);
7149        assert_eq!(snap.get("load_module_binding").copied().unwrap_or(0), 0);
7150        assert_eq!(snap.get("store_module_binding").copied().unwrap_or(0), 0);
7151        assert_eq!(snap.get("return_value").copied().unwrap_or(0), 1);
7152
7153        let joint: std::collections::HashMap<(&'static str, &'static str), u64> =
7154            typed_emit_metrics::snapshot_joint().into_iter().collect();
7155        assert_eq!(joint.get(&("load_local", "string")).copied().unwrap_or(0), 1);
7156        assert_eq!(
7157            joint.get(&("load_local", "nullable_i64")).copied().unwrap_or(0),
7158            1
7159        );
7160        assert_eq!(joint.get(&("return_value", "string")).copied().unwrap_or(0), 1);
7161    }
7162
7163    #[test]
7164    fn test_e4_storage_hint_to_field_kind_policy() {
7165        use crate::type_tracking::StorageHint;
7166        use shape_value::v2::struct_layout::FieldKind;
7167
7168        assert_eq!(super::storage_hint_to_field_kind(StorageHint::Float64), Some(FieldKind::F64));
7169        assert_eq!(super::storage_hint_to_field_kind(StorageHint::Int64), Some(FieldKind::I64));
7170        assert_eq!(super::storage_hint_to_field_kind(StorageHint::UInt64), Some(FieldKind::U64));
7171        assert_eq!(super::storage_hint_to_field_kind(StorageHint::Int32), Some(FieldKind::I32));
7172        assert_eq!(super::storage_hint_to_field_kind(StorageHint::UInt32), Some(FieldKind::U32));
7173        assert_eq!(super::storage_hint_to_field_kind(StorageHint::Int16), Some(FieldKind::I16));
7174        assert_eq!(super::storage_hint_to_field_kind(StorageHint::UInt16), Some(FieldKind::U16));
7175        assert_eq!(super::storage_hint_to_field_kind(StorageHint::Int8), Some(FieldKind::I8));
7176        assert_eq!(super::storage_hint_to_field_kind(StorageHint::UInt8), Some(FieldKind::U8));
7177        assert_eq!(super::storage_hint_to_field_kind(StorageHint::Bool), Some(FieldKind::Bool));
7178
7179        // Post-strict-typing (ADR-006 §2.7.7): `Dynamic`/`Unknown` are
7180        // deleted from `NativeKind`. The polymorphic-fallback path is now
7181        // driven by heap/null sentinel hints only (String, IntSize,
7182        // UIntSize, the Nullable* family).
7183        assert_eq!(super::storage_hint_to_field_kind(StorageHint::String), None);
7184        assert_eq!(super::storage_hint_to_field_kind(StorageHint::IntSize), None);
7185        assert_eq!(super::storage_hint_to_field_kind(StorageHint::UIntSize), None);
7186        assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableFloat64), None);
7187        assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableInt64), None);
7188        assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableInt32), None);
7189        assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableUInt8), None);
7190        assert_eq!(super::storage_hint_to_field_kind(StorageHint::NullableIntSize), None);
7191    }
7192}
7193
7194// ADR-006 §2.7.5 — W12-jit-call-return-kind close (2026-05-12).
7195//
7196// Fresh test module for the resolver-aware conduit producer. The
7197// historical `mod tests` above remains gated for the unrelated
7198// ValueWord-shape deletions; this module builds a synthetic MIR
7199// using only the strict-typed shapes and verifies the
7200// `TerminatorKind::Call` destination stamping pass.
7201#[cfg(test)]
7202mod call_return_kind_tests {
7203    use super::*;
7204    use crate::mir::types::{
7205        BasicBlock, BasicBlockId, MirConstant, MirFunction, Operand,
7206        Place, Point, Rvalue, SlotId, StatementKind, Terminator,
7207        TerminatorKind,
7208    };
7209    use shape_value::v2::ConcreteType;
7210
7211    fn mk_span() -> shape_ast::Span {
7212        shape_ast::Span::new(0, 0)
7213    }
7214
7215    fn mk_mir_with_call(callee_name: &str, dst_slot: u16) -> MirFunction {
7216        let span = mk_span();
7217        // Two blocks: bb0 unconditionally jumps to bb1 with a Call
7218        // terminator that writes into `dst_slot`. bb1 returns.
7219        let bb1 = BasicBlock {
7220            id: BasicBlockId(1),
7221            statements: vec![],
7222            terminator: Terminator {
7223                kind: TerminatorKind::Return,
7224                span,
7225            },
7226        };
7227        let bb0 = BasicBlock {
7228            id: BasicBlockId(0),
7229            statements: vec![],
7230            terminator: Terminator {
7231                kind: TerminatorKind::Call {
7232                    func: Operand::Constant(MirConstant::Function(
7233                        callee_name.to_string(),
7234                    )),
7235                    args: vec![],
7236                    destination: Place::Local(SlotId(dst_slot)),
7237                    next: BasicBlockId(1),
7238                },
7239                span,
7240            },
7241        };
7242        let n_locals = (dst_slot as u16) + 1;
7243        MirFunction {
7244            name: "caller".to_string(),
7245            blocks: vec![bb0, bb1],
7246            num_locals: n_locals,
7247            param_slots: vec![],
7248            param_reference_kinds: vec![],
7249            local_types: vec![
7250                crate::mir::types::LocalTypeInfo::Unknown;
7251                n_locals as usize
7252            ],
7253            span,
7254            field_name_table: Default::default(),
7255            local_struct_type_names: Default::default(),
7256            local_typed_array_element_types: Default::default(),
7257            local_declared_scalar_types: Default::default(),
7258        }
7259    }
7260
7261    #[test]
7262    fn call_terminator_destination_stamped_from_resolver() {
7263        // ADR-006 §2.7.5 producing-site classification for
7264        // `TerminatorKind::Call`. The resolver returns
7265        // `Result(I64, String)` for the callee; the destination slot
7266        // (slot 3 here) must be stamped with that ConcreteType.
7267        let mir = mk_mir_with_call("divide", 3);
7268        let resolver = |name: &str| -> Option<ConcreteType> {
7269            if name == "divide" {
7270                Some(ConcreteType::Result(
7271                    Box::new(ConcreteType::I64),
7272                    Box::new(ConcreteType::String),
7273                ))
7274            } else {
7275                None
7276            }
7277        };
7278        let result = infer_top_level_concrete_types_from_mir_with_returns(
7279            &mir,
7280            Some(&resolver),
7281        );
7282        assert_eq!(
7283            result[3],
7284            ConcreteType::Result(
7285                Box::new(ConcreteType::I64),
7286                Box::new(ConcreteType::String),
7287            ),
7288            "Call destination slot 3 should be stamped Result(I64,String)"
7289        );
7290        // Other slots stay Void.
7291        assert_eq!(result[0], ConcreteType::Void);
7292    }
7293
7294    #[test]
7295    fn call_terminator_no_resolver_leaves_void() {
7296        // Without a resolver, the producer falls back to the legacy
7297        // behavior — Call destinations stay Void. The conduit doesn't
7298        // fabricate a kind per §2.7.5.1.
7299        let mir = mk_mir_with_call("divide", 3);
7300        let result = infer_top_level_concrete_types_from_mir(&mir);
7301        assert_eq!(result[3], ConcreteType::Void);
7302    }
7303
7304    #[test]
7305    fn call_terminator_resolver_returns_none_leaves_void() {
7306        // Resolver returns None for unknown callees — destination
7307        // stays Void (no Bool-default fabrication per §2.7.5.1 /
7308        // forbidden #9).
7309        let mir = mk_mir_with_call("unknown_callee", 2);
7310        let resolver = |_name: &str| -> Option<ConcreteType> { None };
7311        let result = infer_top_level_concrete_types_from_mir_with_returns(
7312            &mir,
7313            Some(&resolver),
7314        );
7315        assert_eq!(result[2], ConcreteType::Void);
7316    }
7317
7318    #[test]
7319    fn call_terminator_propagates_through_move() {
7320        // The Call-terminator pass runs BEFORE the slot-move
7321        // propagation pass, so `let r = divide(10, 2); let x = r;`
7322        // (which lowers to a Call writing slot 3 followed by an
7323        // `Assign(x_slot, Use(Move(slot 3)))`) propagates the Result
7324        // kind through to `x_slot`.
7325        let span = mk_span();
7326        let bb1 = BasicBlock {
7327            id: BasicBlockId(1),
7328            statements: vec![crate::mir::types::MirStatement {
7329                kind: StatementKind::Assign(
7330                    Place::Local(SlotId(5)),
7331                    Rvalue::Use(Operand::Move(Place::Local(SlotId(3)))),
7332                ),
7333                span,
7334                point: Point(0),
7335            }],
7336            terminator: Terminator {
7337                kind: TerminatorKind::Return,
7338                span,
7339            },
7340        };
7341        let bb0 = BasicBlock {
7342            id: BasicBlockId(0),
7343            statements: vec![],
7344            terminator: Terminator {
7345                kind: TerminatorKind::Call {
7346                    func: Operand::Constant(MirConstant::Function(
7347                        "divide".to_string(),
7348                    )),
7349                    args: vec![],
7350                    destination: Place::Local(SlotId(3)),
7351                    next: BasicBlockId(1),
7352                },
7353                span,
7354            },
7355        };
7356        let mir = MirFunction {
7357            name: "caller".to_string(),
7358            blocks: vec![bb0, bb1],
7359            num_locals: 6,
7360            param_slots: vec![],
7361            param_reference_kinds: vec![],
7362            local_types: vec![
7363                crate::mir::types::LocalTypeInfo::Unknown;
7364                6
7365            ],
7366            span,
7367            field_name_table: Default::default(),
7368            local_struct_type_names: Default::default(),
7369            local_typed_array_element_types: Default::default(),
7370            local_declared_scalar_types: Default::default(),
7371        };
7372        let resolver = |name: &str| -> Option<ConcreteType> {
7373            if name == "divide" {
7374                Some(ConcreteType::Result(
7375                    Box::new(ConcreteType::I64),
7376                    Box::new(ConcreteType::String),
7377                ))
7378            } else {
7379                None
7380            }
7381        };
7382        let result = infer_top_level_concrete_types_from_mir_with_returns(
7383            &mir,
7384            Some(&resolver),
7385        );
7386        let expected = ConcreteType::Result(
7387            Box::new(ConcreteType::I64),
7388            Box::new(ConcreteType::String),
7389        );
7390        assert_eq!(result[3], expected, "Call destination slot stamped");
7391        assert_eq!(result[5], expected, "Move destination propagated");
7392    }
7393
7394    // ── Phase 3 cluster-0 Round 11-trinity Part c (2026-05-13) ──────────
7395    // Tests for the empty-operands ObjectStore conduit fix at
7396    // `mir/lowering/helpers.rs::emit_container_store_full`. The producer
7397    // previously short-circuited the StatementKind::ObjectStore emission
7398    // for empty operands (`if operands.is_empty() { return; }`),
7399    // dropping the conduit's kind-source for `let t = X {}`-style empty
7400    // struct literals. The fix preserves the short-circuit for
7401    // Array/Enum/Closure (no per-element work to record) but emits the
7402    // empty ObjectStore so the conduit's
7403    // `StatementKind::ObjectStore { container_slot, .. }` walk can
7404    // stamp Struct(StructLayoutId(0)) on the destination slot. The JIT's
7405    // `is_typed_object_slot` short-circuit at `statements.rs:61` then
7406    // fires for the preceding `Rvalue::Aggregate(vec![])`, and the
7407    // existing ObjectStore consumer's `typed_object_alloc(schema, 0)`
7408    // path allocates the empty TypedObject.
7409
7410    #[test]
7411    fn empty_struct_literal_conduit_stamps_struct() {
7412        // MIR shape produced by `Expr::StructLiteral { fields: [] }`
7413        // after the Part (c) producer-side fix:
7414        //   Assign(Local(2), Aggregate(vec![]))
7415        //   ObjectStore { container_slot: 2, operands: [], field_names: [] }
7416        // The conduit must walk the empty-operands ObjectStore and
7417        // stamp `concrete_types[2] = Struct(_)`.
7418        let span = mk_span();
7419        let bb0 = BasicBlock {
7420            id: BasicBlockId(0),
7421            statements: vec![
7422                crate::mir::types::MirStatement {
7423                    kind: StatementKind::Assign(
7424                        Place::Local(SlotId(2)),
7425                        Rvalue::Aggregate(vec![]),
7426                    ),
7427                    span,
7428                    point: Point(0),
7429                },
7430                crate::mir::types::MirStatement {
7431                    kind: StatementKind::ObjectStore {
7432                        container_slot: SlotId(2),
7433                        operands: vec![],
7434                        field_names: vec![],
7435                        schema_id: None,
7436                    },
7437                    span,
7438                    point: Point(0),
7439                },
7440            ],
7441            terminator: Terminator {
7442                kind: TerminatorKind::Return,
7443                span,
7444            },
7445        };
7446        let mir = MirFunction {
7447            name: "empty_struct".to_string(),
7448            blocks: vec![bb0],
7449            num_locals: 4,
7450            param_slots: vec![],
7451            param_reference_kinds: vec![],
7452            local_types: vec![
7453                crate::mir::types::LocalTypeInfo::Unknown;
7454                4
7455            ],
7456            span,
7457            field_name_table: Default::default(),
7458            local_struct_type_names: Default::default(),
7459            local_typed_array_element_types: Default::default(),
7460            local_declared_scalar_types: Default::default(),
7461        };
7462        let result = infer_top_level_concrete_types_from_mir(&mir);
7463        assert!(
7464            matches!(result[2], ConcreteType::Struct(_)),
7465            "empty-operands ObjectStore must still stamp Struct, got {:?}",
7466            result[2]
7467        );
7468    }
7469
7470    // ── Phase 3 cluster-0 Round 13 T1' commit 2 (2026-05-13) ────────────
7471    //
7472    // VM-side conduit producer tests for the trait-method dispatch
7473    // return-kind classifier. The producer reads
7474    // `mir.local_struct_type_names[receiver_slot]` (populated at MIR
7475    // lowering for `Expr::StructLiteral` sites, T1' gap 1 closure),
7476    // resolves the trait method declared return ConcreteType via the
7477    // `method_returns` resolver, and stamps the Call destination
7478    // slot's `concrete_types`.
7479
7480    #[test]
7481    fn trait_method_call_destination_stamps_from_method_returns_resolver() {
7482        // Smoke 3 minimal MIR shape at the conduit producer level:
7483        //   bb0:
7484        //     ObjectStore { container_slot: SlotId(2), operands: [], field_names: [] }
7485        //     terminator: Call { func: Method("name"), args: [Move(2)],
7486        //                        destination: SlotId(3), next: bb1 }
7487        //   bb1: Return
7488        //
7489        // `mir.local_struct_type_names[SlotId(2)] = "X"` (gap 1 closure
7490        // — populated by MIR lowering of `let t = X {}`).
7491        // `method_returns("X", "name") = Some(ConcreteType::String)`
7492        // (gap 2 + gap 3 closure — chains
7493        // `find_default_trait_impl_for_type_method` →
7494        // `function_return_concrete_types[fn_idx]`).
7495        //
7496        // Asserts: `concrete_types[3] = ConcreteType::String`.
7497        let span = mk_span();
7498        let bb0 = BasicBlock {
7499            id: BasicBlockId(0),
7500            statements: vec![crate::mir::types::MirStatement {
7501                kind: StatementKind::ObjectStore {
7502                    container_slot: SlotId(2),
7503                    operands: vec![],
7504                    field_names: vec![],
7505                    schema_id: None,
7506                },
7507                span,
7508                point: Point(0),
7509            }],
7510            terminator: Terminator {
7511                kind: TerminatorKind::Call {
7512                    func: Operand::Constant(MirConstant::Method(
7513                        "name".to_string(),
7514                    )),
7515                    args: vec![Operand::Move(Place::Local(SlotId(2)))],
7516                    destination: Place::Local(SlotId(3)),
7517                    next: BasicBlockId(1),
7518                },
7519                span,
7520            },
7521        };
7522        let bb1 = BasicBlock {
7523            id: BasicBlockId(1),
7524            statements: vec![],
7525            terminator: Terminator {
7526                kind: TerminatorKind::Return,
7527                span,
7528            },
7529        };
7530        let mut local_struct_type_names = std::collections::HashMap::new();
7531        local_struct_type_names.insert(SlotId(2), "X".to_string());
7532        let mir = MirFunction {
7533            name: "smoke3".to_string(),
7534            blocks: vec![bb0, bb1],
7535            num_locals: 5,
7536            param_slots: vec![],
7537            param_reference_kinds: vec![],
7538            local_types: vec![
7539                crate::mir::types::LocalTypeInfo::Unknown;
7540                5
7541            ],
7542            span,
7543            field_name_table: Default::default(),
7544            local_struct_type_names,
7545            local_typed_array_element_types: std::collections::HashMap::new(),
7546            local_declared_scalar_types: std::collections::HashMap::new(),
7547        };
7548        let method_returns =
7549            |type_name: &str, method_name: &str| -> Option<ConcreteType> {
7550                if type_name == "X" && method_name == "name" {
7551                    Some(ConcreteType::String)
7552                } else {
7553                    None
7554                }
7555            };
7556        let result = infer_top_level_concrete_types_from_mir_with_resolvers(
7557            &mir,
7558            None,
7559            Some(&method_returns),
7560            None,
7561            None,
7562        );
7563        assert_eq!(
7564            result[3],
7565            ConcreteType::String,
7566            "Call destination slot must be stamped ConcreteType::String \
7567             from the method_returns resolver for `t.name()` where \
7568             `local_struct_type_names[t] = \"X\"` and \
7569             `method_returns(\"X\", \"name\") = Some(String)`"
7570        );
7571    }
7572
7573    #[test]
7574    fn trait_method_call_propagates_struct_identity_through_slot_move() {
7575        // Verifies the slot-move propagation pass propagates struct
7576        // identity through `let u = t; u.name()`:
7577        //   bb0:
7578        //     ObjectStore { container_slot: SlotId(2), .. }  // let t = X {}
7579        //     Assign(SlotId(3), Use(Move(SlotId(2))))         // let u = t
7580        //     terminator: Call { func: Method("name"), args: [Move(3)],
7581        //                        destination: SlotId(4), next: bb1 }
7582        //
7583        // The struct identity flows from `t`'s construction slot (2) to
7584        // `u`'s binding slot (3) through the slot-move propagation pass,
7585        // so the trait-method classifier finds `struct_names[3] = "X"`
7586        // and stamps `concrete_types[4] = String`.
7587        let span = mk_span();
7588        let bb0 = BasicBlock {
7589            id: BasicBlockId(0),
7590            statements: vec![
7591                crate::mir::types::MirStatement {
7592                    kind: StatementKind::ObjectStore {
7593                        container_slot: SlotId(2),
7594                        operands: vec![],
7595                        field_names: vec![],
7596                        schema_id: None,
7597                    },
7598                    span,
7599                    point: Point(0),
7600                },
7601                crate::mir::types::MirStatement {
7602                    kind: StatementKind::Assign(
7603                        Place::Local(SlotId(3)),
7604                        Rvalue::Use(Operand::Move(Place::Local(SlotId(2)))),
7605                    ),
7606                    span,
7607                    point: Point(1),
7608                },
7609            ],
7610            terminator: Terminator {
7611                kind: TerminatorKind::Call {
7612                    func: Operand::Constant(MirConstant::Method(
7613                        "name".to_string(),
7614                    )),
7615                    args: vec![Operand::Move(Place::Local(SlotId(3)))],
7616                    destination: Place::Local(SlotId(4)),
7617                    next: BasicBlockId(1),
7618                },
7619                span,
7620            },
7621        };
7622        let bb1 = BasicBlock {
7623            id: BasicBlockId(1),
7624            statements: vec![],
7625            terminator: Terminator {
7626                kind: TerminatorKind::Return,
7627                span,
7628            },
7629        };
7630        let mut local_struct_type_names = std::collections::HashMap::new();
7631        local_struct_type_names.insert(SlotId(2), "X".to_string());
7632        let mir = MirFunction {
7633            name: "smoke3_moved".to_string(),
7634            blocks: vec![bb0, bb1],
7635            num_locals: 6,
7636            param_slots: vec![],
7637            param_reference_kinds: vec![],
7638            local_types: vec![
7639                crate::mir::types::LocalTypeInfo::Unknown;
7640                6
7641            ],
7642            span,
7643            field_name_table: Default::default(),
7644            local_struct_type_names,
7645            local_typed_array_element_types: std::collections::HashMap::new(),
7646            local_declared_scalar_types: std::collections::HashMap::new(),
7647        };
7648        let method_returns =
7649            |type_name: &str, method_name: &str| -> Option<ConcreteType> {
7650                if type_name == "X" && method_name == "name" {
7651                    Some(ConcreteType::String)
7652                } else {
7653                    None
7654                }
7655            };
7656        let result = infer_top_level_concrete_types_from_mir_with_resolvers(
7657            &mir,
7658            None,
7659            Some(&method_returns),
7660            None,
7661            None,
7662        );
7663        assert_eq!(
7664            result[4],
7665            ConcreteType::String,
7666            "Call destination must be stamped String even when receiver \
7667             flows through `let u = t` — struct identity propagated \
7668             through slot moves alongside concrete_types"
7669        );
7670    }
7671
7672    #[test]
7673    fn trait_method_no_resolver_leaves_destination_void() {
7674        // Without a `method_returns` resolver, the producer's
7675        // trait-method arm doesn't run — destinations stay Void. The
7676        // conduit doesn't fabricate per §2.7.5.1 / forbidden #9.
7677        let span = mk_span();
7678        let mut local_struct_type_names = std::collections::HashMap::new();
7679        local_struct_type_names.insert(SlotId(2), "X".to_string());
7680        let bb0 = BasicBlock {
7681            id: BasicBlockId(0),
7682            statements: vec![crate::mir::types::MirStatement {
7683                kind: StatementKind::ObjectStore {
7684                    container_slot: SlotId(2),
7685                    operands: vec![],
7686                    field_names: vec![],
7687                    schema_id: None,
7688                },
7689                span,
7690                point: Point(0),
7691            }],
7692            terminator: Terminator {
7693                kind: TerminatorKind::Call {
7694                    func: Operand::Constant(MirConstant::Method(
7695                        "name".to_string(),
7696                    )),
7697                    args: vec![Operand::Move(Place::Local(SlotId(2)))],
7698                    destination: Place::Local(SlotId(3)),
7699                    next: BasicBlockId(1),
7700                },
7701                span,
7702            },
7703        };
7704        let bb1 = BasicBlock {
7705            id: BasicBlockId(1),
7706            statements: vec![],
7707            terminator: Terminator {
7708                kind: TerminatorKind::Return,
7709                span,
7710            },
7711        };
7712        let mir = MirFunction {
7713            name: "smoke3_no_resolver".to_string(),
7714            blocks: vec![bb0, bb1],
7715            num_locals: 5,
7716            param_slots: vec![],
7717            param_reference_kinds: vec![],
7718            local_types: vec![
7719                crate::mir::types::LocalTypeInfo::Unknown;
7720                5
7721            ],
7722            span,
7723            field_name_table: Default::default(),
7724            local_struct_type_names,
7725            local_typed_array_element_types: std::collections::HashMap::new(),
7726            local_declared_scalar_types: std::collections::HashMap::new(),
7727        };
7728        // No method_returns resolver — destination stays Void.
7729        let result =
7730            infer_top_level_concrete_types_from_mir_with_resolvers(&mir, None, None, None, None);
7731        assert_eq!(
7732            result[3],
7733            ConcreteType::Void,
7734            "Without a method-returns resolver, the trait-method \
7735             classifier must not fabricate a kind — destination stays Void"
7736        );
7737    }
7738
7739    #[test]
7740    fn trait_method_no_struct_identity_leaves_destination_void() {
7741        // When the receiver slot has no `local_struct_type_names` entry
7742        // (e.g. the receiver isn't a struct-literal construction —
7743        // could be a function call result, a method chain result, etc.),
7744        // the trait-method classifier returns None — destination stays
7745        // Void.
7746        let span = mk_span();
7747        let bb0 = BasicBlock {
7748            id: BasicBlockId(0),
7749            statements: vec![],
7750            terminator: Terminator {
7751                kind: TerminatorKind::Call {
7752                    func: Operand::Constant(MirConstant::Method(
7753                        "name".to_string(),
7754                    )),
7755                    args: vec![Operand::Move(Place::Local(SlotId(2)))],
7756                    destination: Place::Local(SlotId(3)),
7757                    next: BasicBlockId(1),
7758                },
7759                span,
7760            },
7761        };
7762        let bb1 = BasicBlock {
7763            id: BasicBlockId(1),
7764            statements: vec![],
7765            terminator: Terminator {
7766                kind: TerminatorKind::Return,
7767                span,
7768            },
7769        };
7770        let mir = MirFunction {
7771            name: "smoke3_no_struct_id".to_string(),
7772            blocks: vec![bb0, bb1],
7773            num_locals: 5,
7774            param_slots: vec![],
7775            param_reference_kinds: vec![],
7776            local_types: vec![
7777                crate::mir::types::LocalTypeInfo::Unknown;
7778                5
7779            ],
7780            span,
7781            field_name_table: Default::default(),
7782            local_struct_type_names: std::collections::HashMap::new(),
7783            local_typed_array_element_types: std::collections::HashMap::new(),
7784            local_declared_scalar_types: std::collections::HashMap::new(),
7785        };
7786        let method_returns = |_type_name: &str, _method_name: &str| -> Option<ConcreteType> {
7787            // Resolver would return String, but it's unreachable
7788            // because struct identity is missing.
7789            Some(ConcreteType::String)
7790        };
7791        let result = infer_top_level_concrete_types_from_mir_with_resolvers(
7792            &mir,
7793            None,
7794            Some(&method_returns),
7795            None,
7796            None,
7797        );
7798        assert_eq!(
7799            result[3],
7800            ConcreteType::Void,
7801            "Without struct identity on the receiver slot, the trait-method \
7802             classifier must not invoke the resolver — destination stays Void"
7803        );
7804    }
7805}
7806
7807// =============================================================================
7808// W17.3-4.2 — TypeAnnotation→FieldType per-container lowering tests.
7809//
7810// Pins the supervisor 2026-05-22 ratified consolidation of the
7811// `BytecodeCompiler::type_annotation_to_field_type` lowering for
7812// `HashMap<K, V>` / `Map<K, V>` / `Set<T>` (per audit §4.B.4 +
7813// `crates/shape-runtime/src/type_schema/field_types.rs` semantic_to_field_type
7814// twin). Asserts the lowering threads through to the per-container
7815// FieldType variants (no `FieldType::Object("HashMap")` / `Any` erasure)
7816// per ADR-005 §1 + ADR-006 §2.7.5 producer-side stamp.
7817// =============================================================================
7818#[cfg(test)]
7819mod w17_3_4_2_type_annotation_lowering_tests {
7820    use super::*;
7821    use shape_ast::ast::TypeAnnotation;
7822    use shape_runtime::type_schema::FieldType;
7823
7824    /// W17.3-4.2 — `HashMap<string, int>` lowers to
7825    /// `FieldType::HashMap { key: String, value: I64 }`. Mirrors the
7826    /// semantic_to_field_type twin in `crates/shape-runtime/src/type_schema/
7827    /// field_types.rs::test_semantic_to_field_type_generic_hashmap`.
7828    #[test]
7829    fn type_annotation_hashmap_threads_kv() {
7830        let ann = TypeAnnotation::Generic {
7831            name: shape_ast::ast::type_path::TypePath::simple("HashMap"),
7832            args: vec![
7833                TypeAnnotation::Basic("string".to_string()),
7834                TypeAnnotation::Basic("int".to_string()),
7835            ],
7836        };
7837        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
7838        assert_eq!(
7839            ft,
7840            FieldType::HashMap {
7841                key: Box::new(FieldType::String),
7842                value: Box::new(FieldType::I64),
7843            }
7844        );
7845    }
7846
7847    /// W17.3-4.2 — `Map<string, bool>` alias maps to the same shape.
7848    #[test]
7849    fn type_annotation_map_alias_threads_kv() {
7850        let ann = TypeAnnotation::Generic {
7851            name: shape_ast::ast::type_path::TypePath::simple("Map"),
7852            args: vec![
7853                TypeAnnotation::Basic("string".to_string()),
7854                TypeAnnotation::Basic("bool".to_string()),
7855            ],
7856        };
7857        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
7858        assert_eq!(
7859            ft,
7860            FieldType::HashMap {
7861                key: Box::new(FieldType::String),
7862                value: Box::new(FieldType::Bool),
7863            }
7864        );
7865    }
7866
7867    /// W17.3-4.2 — `Set<int>` lowers to `FieldType::Set(I64)`.
7868    #[test]
7869    fn type_annotation_set_threads_elem() {
7870        let ann = TypeAnnotation::Generic {
7871            name: shape_ast::ast::type_path::TypePath::simple("Set"),
7872            args: vec![TypeAnnotation::Basic("int".to_string())],
7873        };
7874        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
7875        assert_eq!(ft, FieldType::Set(Box::new(FieldType::I64)));
7876    }
7877
7878    /// W17.3-4.2 — nested `HashMap<string, Array<int>>` threads inner
7879    /// `Array(I64)` through the value position (recursion-composition).
7880    #[test]
7881    fn type_annotation_hashmap_of_array_threads_nested() {
7882        let ann = TypeAnnotation::Generic {
7883            name: shape_ast::ast::type_path::TypePath::simple("HashMap"),
7884            args: vec![
7885                TypeAnnotation::Basic("string".to_string()),
7886                TypeAnnotation::Array(Box::new(TypeAnnotation::Basic(
7887                    "int".to_string(),
7888                ))),
7889            ],
7890        };
7891        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
7892        assert_eq!(
7893            ft,
7894            FieldType::HashMap {
7895                key: Box::new(FieldType::String),
7896                value: Box::new(FieldType::Array(Box::new(FieldType::I64))),
7897            }
7898        );
7899    }
7900
7901    /// W17.3-4.2 — malformed arity (`HashMap<int>` with single arg)
7902    /// falls back to the TRANSITIONAL `FieldType::Any` per the residual
7903    /// safety net at `type_annotation_to_field_type` line ~5004.
7904    #[test]
7905    fn type_annotation_hashmap_malformed_arity_falls_back_to_any() {
7906        let ann = TypeAnnotation::Generic {
7907            name: shape_ast::ast::type_path::TypePath::simple("HashMap"),
7908            args: vec![TypeAnnotation::Basic("int".to_string())],
7909        };
7910        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
7911        assert_eq!(ft, FieldType::Any);
7912    }
7913
7914    /// W17.3-4.2 — `Array<int>` continues to lower correctly through the
7915    /// dedicated `TypeAnnotation::Array` arm (separate from the
7916    /// `Generic` per-container handler — Array is a first-class
7917    /// TypeAnnotation variant, not Generic-wrapped).
7918    #[test]
7919    fn type_annotation_array_threads_elem() {
7920        let ann = TypeAnnotation::Array(Box::new(TypeAnnotation::Basic(
7921            "int".to_string(),
7922        )));
7923        let ft = BytecodeCompiler::type_annotation_to_field_type(&ann);
7924        assert_eq!(ft, FieldType::Array(Box::new(FieldType::I64)));
7925    }
7926}