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