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 ©_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}