Skip to main content

shape_jit/mir_compiler/
v2_array.rs

1//! Inline typed array codegen for the v2 runtime.
2//!
3//! Emits Cranelift IR for direct-memory-access typed array operations
4//! with zero FFI overhead and zero NaN-boxing.
5//!
6//! ## TypedArrayHeader layout (at the array pointer)
7//!
8//! ```text
9//! offset  0: refcount  (u32)
10//! offset  4: kind      (u16)
11//! offset  6: elem_type (u8)
12//! offset  7: _pad      (u8)
13//! offset  8: data      (*mut T)  — pointer to contiguous element buffer
14//! offset 16: len       (u32)
15//! offset 20: cap       (u32)
16//! ```
17//!
18//! ## Element sizes
19//!
20//! | NativeKind  | Cranelift type | Size (bytes) |
21//! |-----------|---------------|--------------|
22//! | Float64   | F64           | 8            |
23//! | Int64     | I64           | 8            |
24//! | Int32     | I32           | 4            |
25//! | Int16     | I16           | 2            |
26//! | Int8/Bool | I8            | 1            |
27
28use cranelift::prelude::*;
29use shape_value::v2::ConcreteType;
30use shape_vm::mir::types::{Operand, Place, SlotId};
31use shape_vm::type_tracking::NativeKind;
32
33use super::MirToIR;
34use super::types::is_v2_typed_array_slot;
35
36// ── TypedArrayHeader field offsets ───────────────────────────────────────────
37
38/// Offset of the `data` pointer field (`*mut T`) inside `TypedArrayHeader`.
39const DATA_PTR_OFFSET: i32 = 8;
40
41/// Offset of the `len` field (`u32`) inside `TypedArrayHeader`.
42const LEN_OFFSET: i32 = 16;
43
44// ── Helpers ─────────────────────────────────────────────────────────────────
45
46/// Return the (Cranelift IR type, element byte size) for a given `NativeKind`.
47///
48/// Panics on slot kinds that do not map to a scalar element type (e.g.
49/// `String`, `Dynamic`, `Unknown`).
50fn elem_type_info(kind: NativeKind) -> (types::Type, i64) {
51    match kind {
52        NativeKind::Float64 | NativeKind::NullableFloat64 => (types::F64, 8),
53        NativeKind::Int64 | NativeKind::NullableInt64 | NativeKind::UInt64 | NativeKind::NullableUInt64 => {
54            (types::I64, 8)
55        }
56        NativeKind::IntSize | NativeKind::NullableIntSize | NativeKind::UIntSize | NativeKind::NullableUIntSize => {
57            // Pointer-sized — 8 bytes on 64-bit targets.
58            (types::I64, 8)
59        }
60        NativeKind::Int32 | NativeKind::NullableInt32 | NativeKind::UInt32 | NativeKind::NullableUInt32 => {
61            (types::I32, 4)
62        }
63        NativeKind::Int16 | NativeKind::NullableInt16 | NativeKind::UInt16 | NativeKind::NullableUInt16 => {
64            (types::I16, 2)
65        }
66        NativeKind::Int8 | NativeKind::NullableInt8 | NativeKind::UInt8 | NativeKind::NullableUInt8 => {
67            (types::I8, 1)
68        }
69        NativeKind::Bool => (types::I8, 1),
70        // Phase 4b Round 4 W16.2-A op_new_array-typed-object-element (2026-05-18) —
71        // 8-byte raw pointer carrier (`*const TypedObjectStorage`). Same shape as
72        // NativeKind::StringV2 / DecimalV2 heap-pointer carriers.
73        NativeKind::StringV2 | NativeKind::DecimalV2 => (types::I64, 8),
74        NativeKind::Ptr(_) => (types::I64, 8),
75        other => panic!("v2_array: unsupported element NativeKind: {:?}", other),
76    }
77}
78
79// ── Implementation ──────────────────────────────────────────────────────────
80
81impl<'a, 'b> MirToIR<'a, 'b> {
82    /// Look up the `ConcreteType` (if any) the bytecode compiler recorded for
83    /// a local slot.
84    #[allow(dead_code)]
85    pub(crate) fn concrete_type_for_slot(&self, slot: SlotId) -> Option<&ConcreteType> {
86        let ct = self.concrete_types.get(slot.0 as usize)?;
87        if matches!(ct, ConcreteType::Void) {
88            None
89        } else {
90            Some(ct)
91        }
92    }
93
94    /// If the place is known to hold a v2 `Array<T>` whose element type
95    /// is a scalar primitive, return the matching element `NativeKind`.
96    /// Returns `None` for non-array places, arrays of non-scalar
97    /// elements, or unresolved types — caller falls back to legacy path.
98    ///
99    /// Two base shapes are recognised:
100    ///
101    /// - `Place::Local(slot)` — the slot's `ConcreteType` (threaded from
102    ///   `BytecodeProgram.top_level_local_concrete_types` per ADR-006
103    ///   §2.7.5, W12-top-level-concrete-types-conduit close 2026-05-12)
104    ///   is inspected via `is_v2_typed_array_slot`.
105    ///
106    /// - `Place::Field(_, field_idx)` — γ-CP5 7a (jit-typedarray-ptr):
107    ///   a struct field declared `Array<T>` carries a v2 `TypedArray<T>`
108    ///   pointer in its 8-byte slot. The element kind comes from the
109    ///   schema-derived `field_array_elem_kinds` map (stamped at
110    ///   `populate_field_byte_offsets_from_schemas` time). Without this
111    ///   arm `b.items[i]` (field-projected array base) fell through to
112    ///   the legacy `inline_array_get` which uses the v1 array layout
113    ///   (data@+0/len@+8) and read the wrong element offset for the v2
114    ///   `TypedArray<T>` (data@8/len@16) actually stored in the field.
115    pub(crate) fn v2_typed_array_elem_kind(&self, place: &Place) -> Option<NativeKind> {
116        match place {
117            Place::Local(s) => is_v2_typed_array_slot(&self.concrete_types, s.0),
118            Place::Field(_, field_idx) => {
119                let name = self.mir.field_name_table.get(field_idx)?;
120                self.field_array_elem_kinds.get(name).copied()
121            }
122            _ => None,
123        }
124    }
125
126    /// R8 W8 jit-aliased-cow-push v0.3 surface-and-stop helper.
127    ///
128    /// Scan ALL statements in ALL blocks of the current MIR function for
129    /// any `Operand::Move(Place::Local(slot))` or
130    /// `Operand::MoveExplicit(Place::Local(slot))` occurrence. Returns
131    /// true on first hit.
132    ///
133    /// Used by the typed-array `.push()` inline codegen to detect the
134    /// aliased-CoW SEGFAULT shape (see audit
135    /// `docs/cluster-audits/v0.3-r8w7-jit-aliased-cow-segfault-audit.md`
136    /// §3 / §6): when the receiver slot has been previously moved out of
137    /// (e.g. via `let alias = data` MIR-lowering at
138    /// `crates/shape-vm/src/mir/lowering/stmt.rs:269-273`), reading the
139    /// nulled slot at push time dereferences NULL → SIGSEGV in
140    /// `jit_v2_array_push`. The detector triggers a structured `Err` on
141    /// match, which the W12 fall-through (`shape-jit/src/executor.rs:
142    /// 170-194`) routes to the bytecode interpreter — VM == JIT.
143    ///
144    /// **Conservatism.** A function-wide scan is over-conservative: a
145    /// `Move(slot)` in an unreachable arm or strictly AFTER the push site
146    /// in execution order would still trigger the deopt. The
147    /// conservatism is binding-compliant for v0.3 — over-deopt costs JIT
148    /// throughput, never correctness. A precise data-flow check is v0.4
149    /// territory (alongside the deeper MIR-lowering fix that would not
150    /// emit `Move` from still-live `let`-source bindings at all).
151    ///
152    /// **Operand coverage.** Scans `Rvalue::Use` / `Rvalue::Clone`
153    /// operands, `Rvalue::BinaryOp` / `Rvalue::UnaryOp` operands,
154    /// `Rvalue::Aggregate` operands, `StatementKind::ArrayStore` /
155    /// `ObjectStore` / `EnumStore` / `ClosureCapture` / `TaskBoundary`
156    /// operands, and `TerminatorKind::Call` `func` + `args` operands +
157    /// `TerminatorKind::SwitchBool` `operand`. Other terminators
158    /// (`Goto` / `Return` / `Unreachable`) carry no operands.
159    pub(crate) fn mir_has_prior_move_of_slot(&self, slot: SlotId) -> bool {
160        use shape_vm::mir::types::{Operand, Rvalue, StatementKind, TerminatorKind};
161        let matches_slot = |op: &Operand| -> bool {
162            matches!(
163                op,
164                Operand::Move(Place::Local(s)) | Operand::MoveExplicit(Place::Local(s))
165                    if *s == slot
166            )
167        };
168        let rvalue_has_move = |rv: &Rvalue| -> bool {
169            match rv {
170                Rvalue::Use(op) | Rvalue::Clone(op) | Rvalue::UnaryOp(_, op) => matches_slot(op),
171                Rvalue::BinaryOp(_, lhs, rhs) => matches_slot(lhs) || matches_slot(rhs),
172                Rvalue::Aggregate(ops) => ops.iter().any(&matches_slot),
173                Rvalue::Borrow(_, _) => false,
174                Rvalue::EnumTest { operand, .. }
175                | Rvalue::EnumPayload { operand, .. }
176                | Rvalue::TypePatternTest { operand, .. }
177                | Rvalue::EnumDiscriminantTest { operand, .. } => matches_slot(operand),
178            }
179        };
180        for block in &self.mir.blocks {
181            for stmt in &block.statements {
182                match &stmt.kind {
183                    StatementKind::Assign(_, rv) => {
184                        if rvalue_has_move(rv) {
185                            return true;
186                        }
187                    }
188                    StatementKind::ArrayStore { operands, .. }
189                    | StatementKind::ObjectStore { operands, .. }
190                    | StatementKind::EnumStore { operands, .. }
191                    | StatementKind::ClosureCapture { operands, .. }
192                    | StatementKind::TaskBoundary(operands, _) => {
193                        if operands.iter().any(&matches_slot) {
194                            return true;
195                        }
196                    }
197                    StatementKind::Drop(_) | StatementKind::Nop => {}
198                }
199            }
200            match &block.terminator.kind {
201                TerminatorKind::Call { func, args, .. } => {
202                    if matches_slot(func) || args.iter().any(&matches_slot) {
203                        return true;
204                    }
205                }
206                TerminatorKind::SwitchBool { operand, .. } => {
207                    if matches_slot(operand) {
208                        return true;
209                    }
210                }
211                TerminatorKind::Goto(_)
212                | TerminatorKind::Return
213                | TerminatorKind::Unreachable => {}
214            }
215        }
216        false
217    }
218
219    /// True when the place's root local is known to hold a TypedObject
220    /// (`ConcreteType::Struct(_)` / `ConcreteType::Enum(_)` /
221    /// `ConcreteType::Option(_)` / `ConcreteType::Result(_, _)` /
222    /// `ConcreteType::Tuple(_)`). These all share the `HeapKind::TypedObject`
223    /// carrier and are materialised by the subsequent
224    /// `StatementKind::ObjectStore` / `EnumStore`.
225    ///
226    /// Used by the `Assign(Aggregate)` short-circuit in `statements.rs`:
227    /// when the bytecode compiler proved the destination slot is a
228    /// TypedObject, the preceding `Rvalue::Aggregate` is a MIR scratch step
229    /// — the real allocation happens in the following `ObjectStore`.
230    /// Skipping the Aggregate avoids the `Route A surface-and-stop`
231    /// previously hit at compile time for `Point { x, y }`-style literals.
232    ///
233    /// Source: the per-MirToIR `concrete_types` vector, threaded from
234    /// `BytecodeProgram.top_level_local_concrete_types` per ADR-006
235    /// §2.7.5 (W12-top-level-concrete-types-conduit close, 2026-05-12).
236    pub(crate) fn is_typed_object_slot(&self, place: &Place) -> bool {
237        let slot = match place {
238            Place::Local(s) => *s,
239            _ => return false,
240        };
241        let Some(ct) = self.concrete_types.get(slot.0 as usize) else {
242            return false;
243        };
244        matches!(
245            ct,
246            ConcreteType::Struct(_)
247                | ConcreteType::Enum(_)
248                | ConcreteType::Option(_)
249                | ConcreteType::Result(_, _)
250                | ConcreteType::Tuple(_)
251        )
252    }
253
254    /// Return the FFI `FuncRef` for `jit_v2_array_new_<elem>`.
255    ///
256    /// ckpt-6-prime Group X JIT FFI String/Decimal BUILD (2026-05-15):
257    /// extended with `StringV2` / `DecimalV2` arms routing to
258    /// `jit_new_typed_array_string` / `jit_new_typed_array_decimal`. These
259    /// allocate `TypedArray<*const StringObj>` / `TypedArray<*const
260    /// DecimalObj>` carriers per ADR-006 §2.7.5 + §2.7.24 Q25.A SUPERSEDED +
261    /// audit deliverable (b) §4.1.B. Per-element pointer payload is the
262    /// v2-raw heap-element shape produced by VM-side `NewStringV2` /
263    /// `NewDecimalV2` opcodes at
264    /// `crates/shape-vm/src/executor/v2_handlers/array.rs:803-858`.
265    pub(crate) fn v2_array_new_func(&self, elem: NativeKind) -> Option<cranelift::codegen::ir::FuncRef> {
266        match elem {
267            NativeKind::Float64 => Some(self.ffi.v2_array_new_f64),
268            NativeKind::Int64 | NativeKind::UInt64 => Some(self.ffi.v2_array_new_i64),
269            NativeKind::Int32 | NativeKind::UInt32 => Some(self.ffi.v2_array_new_i32),
270            NativeKind::Bool | NativeKind::Int8 | NativeKind::UInt8 => Some(self.ffi.v2_array_new_bool),
271            NativeKind::StringV2 => Some(self.ffi.v2_array_new_string),
272            NativeKind::DecimalV2 => Some(self.ffi.v2_array_new_decimal),
273            // Phase 4b Round 4 W16.2-A op_new_array-typed-object-element (2026-05-18) —
274            // v2-raw `TypedArray<*const TypedObjectStorage>` allocator per ADR-006
275            // §2.7.5 + audit `v0.3-w16-v3s5-ckpt56-strict-close-audit.md` §2.1.
276            // Per-element payload is an 8-byte `*const TypedObjectStorage` raw
277            // pointer; pushed via the generic `jit_v2_array_push` I64-shaped
278            // dispatcher (size=8 below). Mirrors the String/Decimal carriers.
279            NativeKind::Ptr(shape_value::HeapKind::TypedObject) => {
280                Some(self.ffi.v2_array_new_typed_object)
281            }
282            _ => None,
283        }
284    }
285
286    /// Return the element byte size for `NativeKind`s backed by the generic
287    /// `jit_v2_array_push` dispatcher, or `None` for unsupported kinds. The
288    /// caller uses the returned size as the `elem_size` I8 immediate passed
289    /// to the dispatcher.
290    ///
291    /// ckpt-6-prime Group X JIT FFI String/Decimal BUILD (2026-05-15):
292    /// `StringV2` / `DecimalV2` are 8-byte pointer carriers — the element
293    /// payload is a `*const StringObj` / `*const DecimalObj` raw pointer,
294    /// pushed via the generic `jit_v2_array_push` I64-shaped dispatcher.
295    pub(crate) fn v2_array_push_elem_size(&self, elem: NativeKind) -> Option<i64> {
296        match elem {
297            NativeKind::Float64 => Some(8),
298            NativeKind::Int64 | NativeKind::UInt64 => Some(8),
299            NativeKind::Int32 | NativeKind::UInt32 => Some(4),
300            NativeKind::Bool | NativeKind::Int8 | NativeKind::UInt8 => Some(1),
301            NativeKind::StringV2 | NativeKind::DecimalV2 => Some(8),
302            // Phase 4b Round 4 W16.2-A op_new_array-typed-object-element (2026-05-18) —
303            // 8-byte raw pointer carrier (`*const TypedObjectStorage`).
304            NativeKind::Ptr(shape_value::HeapKind::TypedObject) => Some(8),
305            _ => None,
306        }
307    }
308
309    /// Emit a call to the generic `jit_v2_array_push` FFI dispatcher. `val`
310    /// is the element value Cranelift SSA value already coerced to the
311    /// native Cranelift type for `elem` (via `coerce_to_v2_elem`). This
312    /// helper zero/sign-extends or bitcasts the value to I64 and passes
313    /// `elem_size` as an I8 immediate.
314    pub(crate) fn emit_v2_array_push_call(
315        &mut self,
316        arr_ptr: Value,
317        val: Value,
318        elem: NativeKind,
319    ) -> Result<(), String> {
320        let elem_size = match self.v2_array_push_elem_size(elem) {
321            Some(s) => s,
322            None => return Err(format!("v2_array_push: unsupported elem kind {:?}", elem)),
323        };
324        let bits = self.widen_to_i64_bits(val);
325        let size_val = self.builder.ins().iconst(types::I8, elem_size);
326        self.builder
327            .ins()
328            .call(self.ffi.v2_array_push, &[arr_ptr, bits, size_val]);
329        Ok(())
330    }
331
332    /// Widen/bitcast an arbitrary Cranelift element value into an I64 bit
333    /// pattern suitable for the generic `jit_v2_array_push` dispatcher.
334    fn widen_to_i64_bits(&mut self, val: Value) -> Value {
335        let val_type = self.builder.func.dfg.value_type(val);
336        if val_type == types::F64 {
337            self.builder.ins().bitcast(types::I64, MemFlags::new(), val)
338        } else if val_type == types::I64 {
339            val
340        } else if val_type == types::I32
341            || val_type == types::I16
342            || val_type == types::I8
343        {
344            // Zero-extend: the dispatcher uses only the low `elem_size` bytes,
345            // so sign bits above that are ignored.
346            self.builder.ins().uextend(types::I64, val)
347        } else {
348            val
349        }
350    }
351
352    /// Convert a Cranelift value into the native type expected by the v2
353    /// element store/push helpers for `elem`.
354    pub(crate) fn coerce_to_v2_elem(&mut self, val: Value, elem: NativeKind) -> Value {
355        let val_type = self.builder.func.dfg.value_type(val);
356        match elem {
357            NativeKind::Float64 => {
358                if val_type == types::F64 {
359                    val
360                } else if val_type == types::I64 {
361                    self.builder.ins().bitcast(types::F64, MemFlags::new(), val)
362                } else {
363                    let i64_val = if val_type == types::I32 {
364                        self.builder.ins().sextend(types::I64, val)
365                    } else if val_type == types::I8 {
366                        self.builder.ins().uextend(types::I64, val)
367                    } else {
368                        val
369                    };
370                    self.builder.ins().fcvt_from_sint(types::F64, i64_val)
371                }
372            }
373            NativeKind::Int64 | NativeKind::UInt64 => {
374                if val_type == types::I64 {
375                    let shifted = self.builder.ins().ishl_imm(val, 16);
376                    self.builder.ins().sshr_imm(shifted, 16)
377                } else if val_type == types::I32 {
378                    self.builder.ins().sextend(types::I64, val)
379                } else if val_type == types::I8 {
380                    self.builder.ins().uextend(types::I64, val)
381                } else {
382                    val
383                }
384            }
385            NativeKind::Int32 | NativeKind::UInt32 => {
386                if val_type == types::I32 {
387                    val
388                } else if val_type == types::I64 {
389                    let shifted = self.builder.ins().ishl_imm(val, 16);
390                    let i64_val = self.builder.ins().sshr_imm(shifted, 16);
391                    self.builder.ins().ireduce(types::I32, i64_val)
392                } else if val_type == types::I8 {
393                    self.builder.ins().uextend(types::I32, val)
394                } else {
395                    val
396                }
397            }
398            NativeKind::Bool | NativeKind::Int8 | NativeKind::UInt8 => {
399                if val_type == types::I8 {
400                    val
401                } else if val_type == types::I64 {
402                    self.builder.ins().ireduce(types::I8, val)
403                } else if val_type == types::I32 {
404                    self.builder.ins().ireduce(types::I8, val)
405                } else {
406                    val
407                }
408            }
409            // ckpt-6-prime Group X JIT FFI String/Decimal BUILD (2026-05-15):
410            // StringV2 / DecimalV2 elements are 8-byte raw pointers — the
411            // operand value is already an I64-shaped `*const StringObj` /
412            // `*const DecimalObj` produced by the per-element constant
413            // materializer in `emit_v2_array_aggregate`'s StringV2/DecimalV2
414            // arm. No coercion needed.
415            NativeKind::StringV2 | NativeKind::DecimalV2 => val,
416            // Phase 4b Round 4 W16.2-A op_new_array-typed-object-element (2026-05-18) —
417            // 8-byte raw pointer carrier, no coercion.
418            NativeKind::Ptr(shape_value::HeapKind::TypedObject) => val,
419            _ => val,
420        }
421    }
422
423    /// Coerce an arbitrary index Cranelift value into an `i32`.
424    pub(crate) fn coerce_index_to_i32(&mut self, index_val: Value) -> Value {
425        let idx_type = self.builder.func.dfg.value_type(index_val);
426        if idx_type == types::I32 {
427            index_val
428        } else if idx_type == types::F64 {
429            let i64_val = self
430                .builder
431                .ins()
432                .fcvt_to_sint_sat(types::I64, index_val);
433            self.builder.ins().ireduce(types::I32, i64_val)
434        } else if idx_type == types::I8 {
435            self.builder.ins().uextend(types::I32, index_val)
436        } else {
437            let shifted = self.builder.ins().ishl_imm(index_val, 16);
438            let payload = self.builder.ins().sshr_imm(shifted, 16);
439            self.builder.ins().ireduce(types::I32, payload)
440        }
441    }
442
443    /// Allocate a v2 typed array of the given element kind via FFI, then push
444    /// each operand value into it. Returns the raw `*mut TypedArray<T>` as an
445    /// `i64` Cranelift value, or `None` when no v2 helper exists.
446    ///
447    /// ckpt-6-prime Group X JIT FFI String/Decimal BUILD (2026-05-15):
448    /// `StringV2` element kind takes a kind-specific per-element path —
449    /// each `MirConstant::Str` / `MirConstant::StringId` operand is
450    /// materialized at JIT-compile time as a `*const StringObj` constant
451    /// via `crate::ffi::v2::string_obj_constant` (refcount-boosted permanent
452    /// share, mirroring `crate::ffi::string::arc_string_constant` for the
453    /// legacy `Arc<String>` carrier). The constant pointer is embedded as
454    /// an `iconst I64` and pushed via the generic `jit_v2_array_push`
455    /// dispatcher with elem_size=8. This is the JIT-side equivalent of the
456    /// VM's `NewStringV2` opcode + `TypedArrayPushString` per-element
457    /// transfer at `crates/shape-vm/src/executor/v2_handlers/array.rs:803`.
458    ///
459    /// `DecimalV2` element kind currently surfaces-and-stops at the MIR
460    /// producer site — `MirConstant` has no `Decimal` variant, so Array
461    /// <decimal> literals can't currently flow through MIR. Wiring the
462    /// per-element NewDecimalV2 equivalent requires MIR-side producer
463    /// support (`MirConstant::Decimal` variant or equivalent constant-pool
464    /// reference), which is downstream territory beyond Group X's JIT FFI
465    /// build scope.
466    pub(crate) fn emit_v2_array_aggregate(
467        &mut self,
468        operands: &[Operand],
469        elem: NativeKind,
470    ) -> Result<Option<Value>, String> {
471        let alloc_func = match self.v2_array_new_func(elem) {
472            Some(f) => f,
473            None => return Ok(None),
474        };
475        if self.v2_array_push_elem_size(elem).is_none() {
476            return Ok(None);
477        }
478
479        let cap = self.builder.ins().iconst(types::I32, operands.len() as i64);
480        let inst = self.builder.ins().call(alloc_func, &[cap]);
481        let arr_ptr = self.builder.inst_results(inst)[0];
482
483        match elem {
484            // ckpt-6-prime Group X JIT FFI String/Decimal BUILD: per-element
485            // NewStringV2 equivalent at the JIT mir_compiler dispatch site.
486            // Each operand must be a `MirConstant::Str` / `MirConstant::
487            // StringId` — the only producer sites for `NativeKind::StringV2`
488            // Array<string> literals per ADR-006 §2.7.5 + audit deliverable
489            // (b) §4.1.B. Other operand shapes structurally cannot produce
490            // a StringV2-kind value and surface-and-stop here (no Bool-
491            // default per §2.7.7 #9 / CLAUDE.md "Forbidden rationalizations").
492            NativeKind::StringV2 => {
493                use shape_vm::mir::types::MirConstant;
494                for op in operands {
495                    let s: String = match op {
496                        Operand::Constant(MirConstant::Str(s)) => s.clone(),
497                        Operand::Constant(MirConstant::StringId(id)) => {
498                            let idx = *id as usize;
499                            if idx >= self.strings.len() {
500                                return Err(format!(
501                                    "emit_v2_array_aggregate: StringV2 elem StringId({}) \
502                                     out of bounds (pool len = {}) — string-pool conduit \
503                                     mismatch at JIT compile time. ADR-006 §2.7.5 / Group X \
504                                     JIT FFI String/Decimal BUILD.",
505                                    id, self.strings.len()
506                                ));
507                            }
508                            self.strings[idx].clone()
509                        }
510                        other => {
511                            return Err(format!(
512                                "emit_v2_array_aggregate: SURFACE — StringV2 elem kind \
513                                 requires `MirConstant::Str` / `MirConstant::StringId` \
514                                 operand per Group X NewStringV2-equivalent dispatch \
515                                 (ADR-006 §2.7.5 + §2.7.24 Q25.A SUPERSEDED + audit \
516                                 deliverable (b) §4.1.B). Got: {:?}. No Bool-default \
517                                 fallback per §2.7.7 #9 / CLAUDE.md Forbidden \
518                                 rationalizations.",
519                                other
520                            ));
521                        }
522                    };
523                    // Compile-time materialize a `*const StringObj` permanent-
524                    // share constant (refcount=2; one share is the active
525                    // share transferred to the array, the other is the
526                    // constant's permanent share that survives JIT-function
527                    // Drop chains).
528                    let string_obj_ptr = crate::ffi::v2::string_obj_constant(&s);
529                    let val = self
530                        .builder
531                        .ins()
532                        .iconst(types::I64, string_obj_ptr as usize as i64);
533                    self.emit_v2_array_push_call(arr_ptr, val, elem)?;
534                }
535            }
536            // ckpt-6-prime Group X JIT FFI String/Decimal BUILD: per-element
537            // NewDecimalV2 equivalent surface-and-stop. `MirConstant` has no
538            // `Decimal` variant so Array<decimal> literals can't currently
539            // flow through MIR — the FFI allocator + carrier-routing is
540            // wired (above) but the per-element producer requires MIR-side
541            // support that's beyond Group X's JIT FFI build scope.
542            NativeKind::DecimalV2 => {
543                return Err(format!(
544                    "emit_v2_array_aggregate: SURFACE — DecimalV2 elem-kind \
545                     per-element materialization requires MIR-side producer \
546                     support (`MirConstant::Decimal` variant or equivalent \
547                     constant-pool reference) which is not yet wired. Group X \
548                     scope covers the JIT FFI allocator + carrier-routing \
549                     (jit_new_typed_array_decimal + v2_array_new_func \
550                     DecimalV2 arm); per-element materializer awaits the MIR \
551                     producer's wiring. ADR-006 §2.7.5 + §2.7.24 Q25.A \
552                     SUPERSEDED + audit deliverable (b) §4.1.B. {} operands \
553                     received; no Bool-default per §2.7.7 #9.",
554                    operands.len()
555                ));
556            }
557            _ => {
558                // Scalar element kinds (Float64/Int64/Int32/Bool/etc.) —
559                // existing inline path. compile_operand_raw produces a
560                // Cranelift SSA value already in the native element type;
561                // coerce_to_v2_elem normalizes and emit_v2_array_push_call
562                // routes through the generic dispatcher.
563                //
564                // γ-CP3 jit-array-builder (v0.3 NO-KNOWN-INCORRECTNESS).
565                // A scalar-element typed array can only be built from
566                // scalar-element operands. An array-builder construct that
567                // the JIT does NOT model — an array-spread element
568                // (`[...a, 4, 5]`), or the slice-shape `Aggregate([source,
569                // start])` the MIR producer emits for an open-range index
570                // (`xs[2..]`) and for a destructure-rest binding
571                // (`let [a, ...rest] = ...`) — carries a heap-pointer
572                // operand (the source `*mut TypedArray<T>`) where a scalar
573                // element value is required.
574                //
575                // Pre-γ-CP3 this loop blindly pushed the raw heap-pointer
576                // bits as an `Int64` element: the destination array then
577                // held the pointer integer instead of the spread/slice
578                // contents, and a downstream `.sum()` / `.len()` read
579                // uninitialized/garbage memory. The VM correctly surfaces
580                // these same constructs (`op_new_array` SURFACE, the V3-S5
581                // ckpt-5 consumer-cascade) — the JIT must match.
582                //
583                // Per ADR-006 §2.7.14 forbidden list ("Bool-default
584                // fallback for unknown element kinds") + §2.7.7 #9, the
585                // honest response is surface-and-stop: a structured `Err`
586                // that the W12 fall-through routes to the bytecode
587                // interpreter, which produces the VM's clean error.
588                // Real array-builder/slice JIT codegen is a follow-up,
589                // gated on the V3-S5 `op_new_array` construction rebuild
590                // landing VM-side first (implementing it before that
591                // would create a NEW VM/JIT divergence).
592                //
593                // Operands whose kind is genuinely unproven (`None`) flow
594                // through the existing scalar path unchanged — the
595                // detector fires ONLY on a proven heap-pointer kind, so
596                // ordinary scalar array literals (`[1, 2, 3]`) are not
597                // over-broadly bailed.
598                for op in operands {
599                    if let Some(NativeKind::Ptr(heap_kind)) =
600                        self.operand_slot_kind(op)
601                    {
602                        return Err(format!(
603                            "emit_v2_array_aggregate: SURFACE — scalar \
604                             element kind {:?} array has an operand with \
605                             heap-pointer kind Ptr({:?}). This is an \
606                             array-builder/slice construct the MIR-JIT \
607                             does not model (array-spread element, \
608                             open-range slice `xs[2..]`, or destructure-\
609                             rest `let [a, ...rest] = ...`). The JIT \
610                             surfaces-and-stops so the W12 fall-through \
611                             routes the program to the bytecode \
612                             interpreter, which surfaces the VM's clean \
613                             `op_new_array` / `SliceAccess` error — VM == \
614                             JIT, neither produces garbage. Real \
615                             array-builder codegen is a γ-CP3 follow-up, \
616                             gated on the V3-S5 op_new_array construction \
617                             rebuild. ADR-006 §2.7.14 / §2.7.7 #9.",
618                            elem, heap_kind
619                        ));
620                    }
621                    let raw = self.compile_operand_raw(op)?;
622                    let val = self.coerce_to_v2_elem(raw, elem);
623                    self.emit_v2_array_push_call(arr_ptr, val, elem)?;
624                }
625            }
626        }
627
628        Ok(Some(arr_ptr))
629    }
630
631    /// Try to emit an inline v2 typed-array method call.
632    pub(crate) fn try_emit_v2_array_method(
633        &mut self,
634        method_name: &str,
635        receiver: &Place,
636        rest_args: &[Operand],
637        destination: &Place,
638        elem: NativeKind,
639    ) -> Result<Option<()>, String> {
640        match method_name {
641            // γ-CP9 jit-groupby-surface (v0.3 NO-KNOWN-INCORRECTNESS item
642            // 9). `count` / `group` / `groupBy` on a typed array take a
643            // `|x| ...` closure predicate. The MIR-JIT has no inline
644            // codegen for these here, and the fall-through path
645            // (`jit_call_method` → VM trampoline) cannot carry the JIT-
646            // format NaN-boxed inline-closure carrier across the FFI
647            // boundary to the VM's v2-raw `Ptr(Closure)` ABI — the
648            // carrier-shape mismatch made the transient `kinded_args`
649            // drop SIGSEGV on the closure arg (array `groupBy(|x| ...)`
650            // crashed ec=139). A real inline JIT path would have to model
651            // the closure-callback ABI for typed arrays — W10 jit-
652            // playbook §5 / §2.7.4 territory — and would only re-create a
653            // VM/JIT divergence while the VM-side `handle_group_by_v2` /
654            // `handle_count_v2` still SURFACE.
655            //
656            // The honest fix is a compile-stage surface-and-stop (the
657            // γ-CP3 array-builder pattern): return a structured `Err`.
658            // The W12 fall-through (`docs/cluster-audits/v0.3-w12-jit-
659            // mode-semantics-close.md`) routes the whole program to the
660            // bytecode interpreter, which runs the method call with its
661            // own carrier-correct closure handling and produces the VM's
662            // behaviour verbatim. Net result: VM == JIT — both run the
663            // identical interpreter path, neither produces garbage or
664            // SIGSEGVs.
665            "count" | "group" | "groupBy" => {
666                let _ = (receiver, rest_args, destination, elem);
667                Err(format!(
668                    "γ-CP9 SURFACE: typed-array `.{}()` JIT codegen is \
669                     unimplemented (closure-callback ABI for typed-array \
670                     higher-order methods is W10 jit-playbook §5 / ADR-006 \
671                     §2.7.4 territory) — JIT compilation bails so the W12 \
672                     fall-through runs the interpreter",
673                    method_name,
674                ))
675            }
676            "length" | "len" => {
677                let arr_ptr = self.read_place(receiver)?;
678                let len_i32 = self.v2_array_len(arr_ptr);
679                let len_i64 = self.builder.ins().sextend(types::I64, len_i32);
680                self.release_old_value_if_heap(destination)?;
681                self.write_place(destination, len_i64)?;
682                Ok(Some(()))
683            }
684            "push" => {
685                if rest_args.len() != 1 {
686                    return Ok(None);
687                }
688                if self.v2_array_push_elem_size(elem).is_none() {
689                    return Ok(None);
690                }
691                // R8 W8 jit-aliased-cow-push v0.3 surface-and-stop
692                // (audit `docs/cluster-audits/v0.3-r8w7-jit-aliased-cow-
693                // segfault-audit.md` §4 fallback / §6, supervisor ratify
694                // 2026-05-24, memory-unsafety unconditional v0.3-gating).
695                //
696                // Reproducer:
697                //   var data: Array<int> = [1, 2, 3]
698                //   let alias = data        // MIR: Assign(alias, Use(Move(data)))
699                //   data.push(4)            // SEGFAULT under JIT
700                //
701                // Root cause: MIR lowering at `crates/shape-vm/src/mir/
702                // lowering/stmt.rs:269-273` emits `Operand::Move` for `let`
703                // bindings whose ownership is `Inferred` (per the
704                // `OwnershipModifier::Inferred` doc-comment in
705                // `shape-ast/src/ast/program.rs:107` — "For `let`: always
706                // move"). The JIT's `compile_operand` Move arm at
707                // `mir_compiler/ownership.rs:225-230` nulls the source slot
708                // after reading via `null_place`. The bytecode VM's
709                // `data.push(4)` compile path does NOT go through MIR — it
710                // emits CloneLocal-equivalent opcodes that keep the source
711                // live, which is why `--mode vm` correctly shows both
712                // `data` and `alias` as `[1, 2, 3, 4]` (the array is shared
713                // through the refcount-bumped aliasing).
714                //
715                // The post-MIR-Move JIT then reads NULL from `data`'s slot
716                // (verified empirically via gdb: `rbx = 0x0` at
717                // `jit_v2_array_push+39`) and the subsequent `mov
718                // 0x10(%rbx),%eax` dereferences NULL → SIGSEGV.
719                //
720                // The v0.3-binding-compliant fix is the audit §6 fallback:
721                // detect the at-risk shape statically (the receiver slot
722                // has been previously moved out of via `Operand::Move` /
723                // `MoveExplicit` in the same function) and surface-and-stop
724                // by returning `Err` — the existing W12 fall-through at
725                // `executor.rs:170-194` routes the whole program to the
726                // bytecode interpreter, which produces the VM's clean
727                // semantics. Eliminates the SEGFAULT (memory-unsafety, the
728                // v0.3 gating condition); the deeper MIR-lowering fix to
729                // not Move from a still-live `let`-source binding is v0.4
730                // territory (touches the documented `OwnershipModifier::
731                // Inferred` semantics).
732                //
733                // Refused defection-attractor framings per CLAUDE.md
734                // §Forbidden-Patterns: no Bool-default refcount probe, no
735                // decode kind from bits, no "preserve unverified path with
736                // a fallback flag", no CoW codegen (would diverge from VM
737                // semantics — VM does NOT clone-on-write, both aliases
738                // observe the in-place mutation).
739                if let Place::Local(recv_slot) = receiver {
740                    if self.mir_has_prior_move_of_slot(*recv_slot) {
741                        return Err(format!(
742                            "R8 W8 jit-aliased-cow-push SURFACE: typed-array \
743                             `.push()` receiver slot {} has a prior \
744                             `Operand::Move` / `MoveExplicit` in the same \
745                             function (e.g. `let alias = data` MIR-lowering \
746                             at `mir/lowering/stmt.rs:269-273` nulls `data`'s \
747                             slot post-Move). The JIT inline push would \
748                             dereference a NULL receiver pointer → SIGSEGV \
749                             (audit `v0.3-r8w7-jit-aliased-cow-segfault-\
750                             audit.md`). Surface-and-stop deopts the whole \
751                             program to the bytecode interpreter (W12 \
752                             fall-through at `shape-jit/src/executor.rs:\
753                             170-194`), which uses its own MIR-independent \
754                             compile that does NOT null the source slot — \
755                             VM == JIT semantics restored. Memory-unsafety \
756                             unconditional v0.3-gating per supervisor \
757                             2026-05-24 ruling.",
758                            recv_slot.0,
759                        ));
760                    }
761                }
762                let arr_ptr = self.read_place(receiver)?;
763                let raw_arg = self.compile_operand_raw(&rest_args[0])?;
764                let val = self.coerce_to_v2_elem(raw_arg, elem);
765                self.emit_v2_array_push_call(arr_ptr, val, elem)?;
766                let none_val = self.builder.ins().iconst(types::I64, 0i64);
767                self.release_old_value_if_heap(destination)?;
768                self.write_place(destination, none_val)?;
769                Ok(Some(()))
770            }
771            // Phase 4b Round 4 W15 LANG-9-spin-3-first JIT fix
772            // (2026-05-18). ADR-006 §2.7.5 producer-side stamp: inline the
773            // element-0 / element-(len-1) read via `v2_array_get` for the
774            // chained-receiver shape (`[..].map(..).first()` —
775            // F3b reproducer) where the receiver lacks a Place::Local
776            // binding to register `v2_typed_array_locals` against. The
777            // result kind matches the VM PHF (`typed_int_array_methods::
778            // first` returns Int64 for I64-element arrays) — see the
779            // sibling `parametric_method_return_kind_from_receiver`
780            // ("first"|"last"|"pop", Array(elem)) arm in
781            // `mir_compiler/types.rs` which stamps the same element kind
782            // into the JIT slot_kinds track.
783            //
784            // Bypasses the `jit_call_method` trampoline — the typed-
785            // element read is structurally cheap (load data+0 or
786            // data+(len-1)*size) and removes the FFI hop. Pre-fix the
787            // F3b reproducer fell through to `jit_call_method` →
788            // `typed_int_array_methods::first` returning the bare
789            // element bits with kind Int64; the JIT downstream
790            // `operand_slot_kind` previously returned `Ptr(Option)` via
791            // the pre-fix arm above and treated the bare element bits as
792            // an Option<T> pointer carrier → "None" rendered. The
793            // post-fix slot kind is Int64 (sibling §2.7.5 arm) but the
794            // chained-receiver shape's slot-bits trip a different
795            // downstream surface (the result kind+bits flow through
796            // unbinded chain slots without the let-binding's full
797            // §2.7.5 conduit). This arm closes that surface by
798            // structurally emitting the element read inline.
799            //
800            // Empty-array contract: returns the element default (0 for
801            // integers, 0.0 for floats, false for bools) via
802            // `v2_array_get`'s out-of-bounds branch in
803            // `mir_compiler/v2_array.rs::v2_array_get`. This mirrors
804            // the VM PHF's empty-array `KindedSlot::none()` Bool/0
805            // sentinel for the integer/float/bool element families.
806            // String / Decimal / Char element receivers fall through
807            // (Ok(None)) since the heap-element variants need carrier
808            // retain on read and are tracked separately under the
809            // V3-S5 ckpt-6 STRICT close.
810            "first" => {
811                if !rest_args.is_empty() {
812                    return Ok(None);
813                }
814                if !matches!(
815                    elem,
816                    NativeKind::Int64
817                        | NativeKind::UInt64
818                        | NativeKind::Int32
819                        | NativeKind::UInt32
820                        | NativeKind::Int16
821                        | NativeKind::UInt16
822                        | NativeKind::Int8
823                        | NativeKind::UInt8
824                        | NativeKind::Float64
825                        | NativeKind::Float32
826                        | NativeKind::Bool
827                ) {
828                    return Ok(None);
829                }
830                let arr_ptr = self.read_place(receiver)?;
831                let zero_idx = self.builder.ins().iconst(types::I32, 0);
832                let elem_val = self.v2_array_get(arr_ptr, zero_idx, elem);
833                self.release_old_value_if_heap(destination)?;
834                self.write_place(destination, elem_val)?;
835                Ok(Some(()))
836            }
837            "last" => {
838                if !rest_args.is_empty() {
839                    return Ok(None);
840                }
841                if !matches!(
842                    elem,
843                    NativeKind::Int64
844                        | NativeKind::UInt64
845                        | NativeKind::Int32
846                        | NativeKind::UInt32
847                        | NativeKind::Int16
848                        | NativeKind::UInt16
849                        | NativeKind::Int8
850                        | NativeKind::UInt8
851                        | NativeKind::Float64
852                        | NativeKind::Float32
853                        | NativeKind::Bool
854                ) {
855                    return Ok(None);
856                }
857                let arr_ptr = self.read_place(receiver)?;
858                let len_i32 = self.v2_array_len(arr_ptr);
859                let one = self.builder.ins().iconst(types::I32, 1);
860                let last_idx = self.builder.ins().isub(len_i32, one);
861                // `v2_array_get` performs an unsigned bounds check
862                // (`index < len`); when len==0 the resulting last_idx
863                // wraps to a large positive u32 that fails the bounds
864                // check and the OOB path returns the element default —
865                // mirrors the VM PHF's empty-array `KindedSlot::none()`
866                // sentinel for integer/float/bool element families.
867                let elem_val = self.v2_array_get(arr_ptr, last_idx, elem);
868                self.release_old_value_if_heap(destination)?;
869                self.write_place(destination, elem_val)?;
870                Ok(Some(()))
871            }
872            "sum" => {
873                // Phase C.3: Bypass method dispatch entirely — call the SIMD
874                // reduction FFI (`jit_v2_array_sum_f64` / `jit_v2_array_sum_i64`)
875                // in one shot. The FFI uses `wide::f64x4`/`wide::i64x4` lanes
876                // so AVX2/NEON-capable CPUs get a ~4x throughput over the
877                // scalar loop.
878                if !rest_args.is_empty() {
879                    return Ok(None);
880                }
881                let sum_func = match elem {
882                    NativeKind::Float64 => self.ffi.v2_array_sum_f64,
883                    NativeKind::Int64 | NativeKind::UInt64 => self.ffi.v2_array_sum_i64,
884                    _ => return Ok(None),
885                };
886                let arr_ptr = self.read_place(receiver)?;
887                let inst = self.builder.ins().call(sum_func, &[arr_ptr]);
888                let result = self.builder.inst_results(inst)[0];
889                self.release_old_value_if_heap(destination)?;
890                self.write_place(destination, result)?;
891                Ok(Some(()))
892            }
893            // f64-only SIMD reductions. Dispatched only for Array<number>.
894            "min" | "max" | "mean" | "avg" | "sumSquares" | "sum_squares" => {
895                if !rest_args.is_empty() {
896                    return Ok(None);
897                }
898                if !matches!(elem, NativeKind::Float64) {
899                    return Ok(None);
900                }
901                let func = match method_name {
902                    "min" => self.ffi.v2_array_min_f64,
903                    "max" => self.ffi.v2_array_max_f64,
904                    "mean" | "avg" => self.ffi.v2_array_mean_f64,
905                    "sumSquares" | "sum_squares" => self.ffi.v2_array_sum_squares_f64,
906                    _ => unreachable!(),
907                };
908                let arr_ptr = self.read_place(receiver)?;
909                let inst = self.builder.ins().call(func, &[arr_ptr]);
910                let result = self.builder.inst_results(inst)[0];
911                self.release_old_value_if_heap(destination)?;
912                self.write_place(destination, result)?;
913                Ok(Some(()))
914            }
915            // f64 scalar broadcast — returns a new Array<number>.
916            "scale" | "addScalar" | "add_scalar" => {
917                if rest_args.len() != 1 {
918                    return Ok(None);
919                }
920                if !matches!(elem, NativeKind::Float64) {
921                    return Ok(None);
922                }
923                let func = match method_name {
924                    "scale" => self.ffi.v2_array_scale_f64,
925                    "addScalar" | "add_scalar" => self.ffi.v2_array_add_scalar_f64,
926                    _ => unreachable!(),
927                };
928                let arr_ptr = self.read_place(receiver)?;
929                let raw = self.compile_operand_raw(&rest_args[0])?;
930                let scalar = self.coerce_to_v2_elem(raw, NativeKind::Float64);
931                let inst = self.builder.ins().call(func, &[arr_ptr, scalar]);
932                let new_arr = self.builder.inst_results(inst)[0];
933                self.release_old_value_if_heap(destination)?;
934                self.write_place(destination, new_arr)?;
935                Ok(Some(()))
936            }
937            // f64 element-wise binary ops — both operands are Array<number>,
938            // returns a new Array<number>.
939            "addArray" | "add_array" | "mulArray" | "mul_array" => {
940                if rest_args.len() != 1 {
941                    return Ok(None);
942                }
943                if !matches!(elem, NativeKind::Float64) {
944                    return Ok(None);
945                }
946                let func = match method_name {
947                    "addArray" | "add_array" => self.ffi.v2_array_add_f64,
948                    "mulArray" | "mul_array" => self.ffi.v2_array_mul_f64,
949                    _ => unreachable!(),
950                };
951                let arr_ptr = self.read_place(receiver)?;
952                let other = self.compile_operand_raw(&rest_args[0])?;
953                // The other argument is an Array<number> (pointer); no coercion
954                // needed, but make sure the value type is i64 before handoff.
955                let other_i64 = {
956                    let ty = self.builder.func.dfg.value_type(other);
957                    if ty == types::I64 {
958                        other
959                    } else {
960                        // Fall back to generic dispatch if we couldn't resolve
961                        // the other operand to a plain pointer-sized value.
962                        return Ok(None);
963                    }
964                };
965                let inst = self.builder.ins().call(func, &[arr_ptr, other_i64]);
966                let new_arr = self.builder.inst_results(inst)[0];
967                self.release_old_value_if_heap(destination)?;
968                self.write_place(destination, new_arr)?;
969                Ok(Some(()))
970            }
971            _ => Ok(None),
972        }
973    }
974
975    /// Inline typed array element read.
976    ///
977    /// Emits:
978    /// 1. Load `data` pointer from `[arr_ptr + 8]`
979    /// 2. Load `len` (u32) from `[arr_ptr + 16]`
980    /// 3. Bounds check: `if index >= len` raise an out-of-bounds error
981    ///    (early `return_` of `JIT_SIGNAL_INDEX_OUT_OF_BOUNDS` — VM/JIT parity)
982    /// 4. Compute element address: `data + index * elem_size`
983    /// 5. Load element with the correct Cranelift type
984    ///
985    /// `arr_ptr` is a Cranelift `i64` value pointing to a `TypedArrayHeader`.
986    /// `index` is a Cranelift `i32` value (unsigned index).
987    /// Returns the loaded element value (type depends on `elem_type`).
988    pub fn v2_array_get(
989        &mut self,
990        arr_ptr: Value,
991        index: Value,
992        elem_type: NativeKind,
993    ) -> Value {
994        let (cl_type, elem_size) = elem_type_info(elem_type);
995
996        // 1. Load data pointer (i64) from arr_ptr + DATA_PTR_OFFSET
997        let data_ptr = self
998            .builder
999            .ins()
1000            .load(types::I64, MemFlags::trusted(), arr_ptr, DATA_PTR_OFFSET);
1001
1002        // 2. Load length (u32) from arr_ptr + LEN_OFFSET
1003        let len = self
1004            .builder
1005            .ins()
1006            .load(types::I32, MemFlags::trusted(), arr_ptr, LEN_OFFSET);
1007
1008        // 3. Bounds check: if index >= len, branch to out-of-bounds block
1009        let in_bounds_block = self.builder.create_block();
1010        let oob_block = self.builder.create_block();
1011        let merge_block = self.builder.create_block();
1012
1013        // The merge block receives the result as a block parameter.
1014        self.builder.append_block_param(merge_block, cl_type);
1015
1016        let cmp = self
1017            .builder
1018            .ins()
1019            .icmp(IntCC::UnsignedLessThan, index, len);
1020        self.builder
1021            .ins()
1022            .brif(cmp, in_bounds_block, &[], oob_block, &[]);
1023
1024        // ── Out-of-bounds path: raise an out-of-bounds error ────────────
1025        //
1026        // WS-3 F1: the prior codegen fabricated the element-type zero here
1027        // (a default-constant `jump merge`). That silently produced a value
1028        // for a memory-unsafe access the VM correctly rejects with
1029        // `VMError::IndexOutOfBounds` — a VM/JIT divergence. The MirToIR
1030        // function returns `i32` (the `JittedStrategyFn` ABI), so an early
1031        // `return_` of the `JIT_SIGNAL_INDEX_OUT_OF_BOUNDS` signal is
1032        // type-correct. The executor maps that signal back to the VM's
1033        // `Index out of bounds` diagnostic. Mirrors the
1034        // `compile_int_divmod_guarded` clean-error fall-through shape.
1035        self.builder.switch_to_block(oob_block);
1036        self.builder.seal_block(oob_block);
1037        let oob_signal = self.narrow_iconst(
1038            types::I32,
1039            crate::context::JIT_SIGNAL_INDEX_OUT_OF_BOUNDS as i64,
1040        );
1041        self.builder.ins().return_(&[oob_signal]);
1042
1043        // ── In-bounds path: compute address and load element ────────────
1044        self.builder.switch_to_block(in_bounds_block);
1045        self.builder.seal_block(in_bounds_block);
1046
1047        // 4. Compute byte offset: index (u32) -> i64, then * elem_size
1048        let index_i64 = self.builder.ins().uextend(types::I64, index);
1049        let byte_offset = if (elem_size as u64).is_power_of_two() {
1050            let shift = (elem_size as u64).trailing_zeros() as i64;
1051            self.builder.ins().ishl_imm(index_i64, shift)
1052        } else {
1053            let size_val = self.builder.ins().iconst(types::I64, elem_size);
1054            self.builder.ins().imul(index_i64, size_val)
1055        };
1056        let elem_addr = self.builder.ins().iadd(data_ptr, byte_offset);
1057
1058        // 5. Load element with trusted flags (bounds already checked)
1059        let loaded = self
1060            .builder
1061            .ins()
1062            .load(cl_type, MemFlags::trusted(), elem_addr, 0);
1063
1064        self.builder.ins().jump(merge_block, &[loaded]);
1065
1066        // ── Merge ───────────────────────────────────────────────────────
1067        self.builder.switch_to_block(merge_block);
1068        self.builder.seal_block(merge_block);
1069
1070        self.builder.block_params(merge_block)[0]
1071    }
1072
1073    /// Inline typed array length.
1074    ///
1075    /// Emits a single `load i32 [arr_ptr + 16]`.
1076    pub fn v2_array_len(&mut self, arr_ptr: Value) -> Value {
1077        self.builder
1078            .ins()
1079            .load(types::I32, MemFlags::trusted(), arr_ptr, LEN_OFFSET)
1080    }
1081
1082    /// Inline typed array element write.
1083    ///
1084    /// Emits:
1085    /// 1. Load `data` pointer from `[arr_ptr + 8]`
1086    /// 2. Load `len` (u32) from `[arr_ptr + 16]`
1087    /// 3. Bounds check: `if index >= len` raise an out-of-bounds error
1088    ///    (early `return_` of `JIT_SIGNAL_INDEX_OUT_OF_BOUNDS` — VM/JIT parity)
1089    /// 4. Compute element address: `data + index * elem_size`
1090    /// 5. Store element with the correct Cranelift type
1091    ///
1092    /// `val` must be a Cranelift value whose type matches `elem_type`.
1093    pub fn v2_array_set(
1094        &mut self,
1095        arr_ptr: Value,
1096        index: Value,
1097        val: Value,
1098        elem_type: NativeKind,
1099    ) {
1100        let (_cl_type, elem_size) = elem_type_info(elem_type);
1101
1102        // 1. Load data pointer
1103        let data_ptr = self
1104            .builder
1105            .ins()
1106            .load(types::I64, MemFlags::trusted(), arr_ptr, DATA_PTR_OFFSET);
1107
1108        // 2. Load length
1109        let len = self
1110            .builder
1111            .ins()
1112            .load(types::I32, MemFlags::trusted(), arr_ptr, LEN_OFFSET);
1113
1114        // 3. Bounds check
1115        let in_bounds_block = self.builder.create_block();
1116        let oob_block = self.builder.create_block();
1117        let continue_block = self.builder.create_block();
1118
1119        let cmp = self
1120            .builder
1121            .ins()
1122            .icmp(IntCC::UnsignedLessThan, index, len);
1123        self.builder
1124            .ins()
1125            .brif(cmp, in_bounds_block, &[], oob_block, &[]);
1126
1127        // ── Out-of-bounds path: raise an out-of-bounds error ────────────
1128        //
1129        // WS-3 F1: the prior codegen silently skipped the store on OOB
1130        // (`brif` fell through to `continue_block`). That diverges from the
1131        // VM, which rejects the access with `VMError::IndexOutOfBounds`. The
1132        // MirToIR function returns `i32`, so an early `return_` of the
1133        // `JIT_SIGNAL_INDEX_OUT_OF_BOUNDS` signal is type-correct; the
1134        // executor maps it to the VM's `Index out of bounds` diagnostic.
1135        self.builder.switch_to_block(oob_block);
1136        self.builder.seal_block(oob_block);
1137        let oob_signal = self.narrow_iconst(
1138            types::I32,
1139            crate::context::JIT_SIGNAL_INDEX_OUT_OF_BOUNDS as i64,
1140        );
1141        self.builder.ins().return_(&[oob_signal]);
1142
1143        // ── In-bounds path: store element ───────────────────────────────
1144        self.builder.switch_to_block(in_bounds_block);
1145        self.builder.seal_block(in_bounds_block);
1146
1147        let index_i64 = self.builder.ins().uextend(types::I64, index);
1148        let byte_offset = if (elem_size as u64).is_power_of_two() {
1149            let shift = (elem_size as u64).trailing_zeros() as i64;
1150            self.builder.ins().ishl_imm(index_i64, shift)
1151        } else {
1152            let size_val = self.builder.ins().iconst(types::I64, elem_size);
1153            self.builder.ins().imul(index_i64, size_val)
1154        };
1155        let elem_addr = self.builder.ins().iadd(data_ptr, byte_offset);
1156
1157        self.builder
1158            .ins()
1159            .store(MemFlags::trusted(), val, elem_addr, 0);
1160
1161        self.builder.ins().jump(continue_block, &[]);
1162
1163        // ── Continue ────────────────────────────────────────────────────
1164        self.builder.switch_to_block(continue_block);
1165        self.builder.seal_block(continue_block);
1166    }
1167}
1168
1169// ═══════════════════════════════════════════════════════════════════════════
1170// Tests
1171// ═══════════════════════════════════════════════════════════════════════════
1172