Skip to main content

shape_vm/executor/objects/
mod.rs

1//! Object and array operations for the VM executor.
2//!
3//! Handles: NewArray, NewObject, GetProp, SetProp, Length, ArrayPush, ArrayPop,
4//! MakeClosure, MergeObject, NewTypedObject, TypedMergeObject, CallMethod, MakeRange,
5//! WrapTypeAnnotation, SliceAccess.
6//!
7//! ## Wave 6.5 substep-2 (D-objects-mod) — SURFACE
8//!
9//! This file is the dispatch shell for generic-object opcodes. The substep-1
10//! shim deletion (`push_raw_u64` / `pop_raw_u64` / `push_native_i64` /
11//! `stack_read_owned` / `stack_peek_raw`) bound this territory at 39 mandatory
12//! shim sites. The pre-Wave-6 file body, however, is built on top of types and
13//! helpers that the strict-typing bulldozer **already deleted before
14//! substep-1** — it does not compile against the current `shape-value` crate
15//! and cannot be migrated by mechanical shim rename:
16//!
17//! - `shape_value::ValueWord` / `shape_value::ValueWordExt`
18//!   (deleted — see `crates/shape-value/src/lib.rs`'s post-bulldozer header).
19//! - `shape_value::value_word_drop::vw_drop` /
20//!   `shape_value::value_word_drop::vw_clone`
21//!   (deleted — replaced by `clone_with_kind` / `drop_with_kind` keyed on
22//!    `NativeKind`, ADR-006 §2.7.7).
23//! - `ValueWord::from_raw_bits` / `ValueWord::from_*` /
24//!   `ValueWord::into_raw_bits` (constructors and accessors all gone with the
25//!    type itself).
26//! - `as_heap_ref()` (forbidden — playbook §4 #7; replaced by
27//!   `slot.as_heap_value()` on `KindedSlot::slot`).
28//! - `tag_bits::*` / `is_tagged()` / the deleted W-series ValueWord
29//!   synthesizer (forbidden — playbook §4 #7).
30//!
31//! On top of those, the `MethodHandler` ABI itself was **kind-less in
32//! both directions** pre-Wave-γ. ADR-006 §2.7.9 / Q11 (Wave-γ
33//! `G-method-fn-v2-abi`) flipped `MethodFnV2` to
34//! `fn(&mut VM, &[KindedSlot], _) -> Result<KindedSlot, VMError>` —
35//! the kinded carrier slice form per §2.7.1 case 4. The dispatch
36//! shell now sources every kind from the §2.7.7 stack parallel-
37//! `Vec<NativeKind>` track via `pop_kinded()` (no fabrication), and
38//! pushes the returned `KindedSlot` via `push_kinded()` (kind from
39//! the handler-returned carrier — no fabrication). The Bool-default
40//! rationalization the W-series formalized is no longer reachable.
41//! With the ABI in place this dispatch shell becomes a mechanical
42//! `pop_kinded` / `push_kinded` / `slot.as_heap_value()` rewrite per
43//! playbook §10 D-objects-mod row — Wave-γ-followup territory.
44//!
45//! Cross-cluster dependencies for the architectural close-out:
46//!
47//! 1. `D-raw-helpers` rewrites/deletes `objects/raw_helpers.rs` (currently
48//!    the carrier for `tag_bits::*` and `extract_heap_ref`). Every Cluster D
49//!    sibling file (`property_access.rs`, `array_operations.rs`,
50//!    `array_joins.rs`, `concurrency_methods.rs`, `channel_methods.rs`,
51//!    `number_methods.rs`, etc.) calls `extract_heap_ref(args[0])` for
52//!    HeapValue dispatch — same shape needed here for the receiver bits.
53//! 2. Wave-γ-followup body migration: per ADR-006 §2.7.9 / Q11 the
54//!    `MethodFnV2` ABI is kinded (`&[KindedSlot]` /
55//!    `Result<KindedSlot, VMError>`); ~150 PHF handler bodies stayed
56//!    `NotImplemented(SURFACE)` after the ABI flip (Wave-γ
57//!    `G-method-fn-v2-abi` close) and are migrated body-by-body in
58//!    follow-up sub-clusters per the M-datatable Wave-β `joins.rs`
59//!    precedent at close commit `eb78699`.
60//! 3. The remaining `ValueWord::from_*` heap-construction sites
61//!    (`ValueWord::from_heap_value(HeapValue::Range { .. })`,
62//!    `ValueWord::from_type_annotated_value`, `ValueWord::from_array`, etc.)
63//!    rewrite to `Arc::into_raw + push_kinded(_, NativeKind::Ptr(HeapKind::*))`
64//!    per playbook §3 per-`HeapKind` push pattern.
65//!
66//! Per playbook §7.4 ("File compiles cleanly OR un-compiling sites have a
67//! documented surface") and §8 surface-and-stop trigger ("Cross-cluster
68//! migration cascade"), this file's bodies are replaced with
69//! `VMError::NotImplemented(SURFACE: ...)` placeholders documenting the
70//! cascade. Function signatures and module declarations are preserved so
71//! external callers (`dispatch.rs`, `additional/mod.rs`, `compiler/*`)
72//! continue to compile.
73//!
74//! ## Migration status snapshot (substep-2 close)
75//!
76//! - Mandatory shim hits: 0 (the 39 `push_raw_u64` / `pop_raw_u64` call sites
77//!   are gone — they were inside the bodies that this commit replaces with
78//!   surface markers).
79//! - Sibling shim hits: 0 (none in pre-existing file; verified at audit).
80//! - Forbidden-pattern carry-overs: 0 (`ValueWord`, `as_heap_ref`, `vw_drop`,
81//!   `value_word_drop`, `as_vw_ref`, `tag_bits`, and the deleted ValueWord
82//!   synthesizer are all gone; the `extract_heap_ref` import lived in the
83//!   now-deleted bodies and is not reintroduced).
84//! - Surfaces: 6 (`exec_objects` opcode dispatch + 5 method-dispatch entries:
85//!   `op_call_method`, `op_make_range`, `op_wrap_type_annotation`,
86//!   `dispatch_method_handler`, plus the v2 typed-array PHF fast path baked
87//!   into `op_call_method`).
88//!
89//! See `docs/cluster-audits/phase-1b-vm-wave-6-5-playbook.md` §10 row
90//! `D-objects-mod`, §7.4, §8, and ADR-006 §2.7.6 (Q8) / §2.7.7 (Q9).
91
92// PHF method registry
93pub mod method_registry;
94// Raw u64 extraction helpers (v2 — no ValueWord) — D-raw-helpers territory.
95pub mod raw_helpers;
96
97// Property access operations (GetProp, SetProp, Length) — D-prop-access territory.
98pub mod property_access;
99
100// Object creation operations (NewArray, NewObject, NewTypedObject) — D-obj-create territory.
101pub mod object_creation;
102
103// Object merge operations (MergeObject, TypedMergeObject) — D-obj-tail territory.
104pub mod object_operations;
105
106// Array operations (ArrayPush, ArrayPop, SliceAccess) — D-array-ops territory.
107pub mod array_operations;
108
109// Array method modules.
110pub mod array_aggregation;
111pub mod array_basic;
112pub mod array_joins;
113pub mod array_query;
114pub mod array_sets;
115pub mod array_sort;
116pub mod array_transform;
117
118// DataTable method handlers.
119pub mod datatable_methods;
120
121// (W15-column, 2026-05-10) `column_methods` deleted: ADR-006 §2.7.21 / Q22.
122// `Column` is not a surviving `HeapKind` variant — its semantics are
123// absorbed by `HeapKind::TableView` + `TableViewData::ColumnRef` (see
124// `crates/shape-value/src/heap_value.rs`). The previous file held 11
125// surface-only stubs and a stale PHF map; both are removed.
126
127// IndexedTable method handlers.
128pub mod indexed_table_methods;
129
130// HashMap method handlers.
131pub mod hashmap_methods;
132
133// Set method handlers.
134pub mod deque_methods;
135pub mod priority_queue_methods;
136pub mod set_methods;
137
138// Number method handlers.
139pub mod number_methods;
140
141// String method handlers.
142pub mod string_methods;
143
144// Content method handlers.
145pub mod content_methods;
146
147// DateTime method handlers.
148pub mod datetime_methods;
149
150// Instant method handlers.
151pub mod instant_methods;
152
153// Matrix method handlers.
154pub mod matrix_methods;
155
156// Iterator method handlers.
157pub mod iterator_methods;
158
159// Range method handlers (W15-range, ADR-006 §2.7.23 / Q24, 2026-05-10).
160pub mod range_methods;
161
162// Typed array (Vec<int>, Vec<number>, Vec<bool>) method handlers.
163pub mod typed_array_methods;
164
165// W16.2-J.1 (2026-05-22): the V0.c per-kind typed-array handler modules
166// (`typed_int_array_methods` + `typed_number_array_methods`, 667 LoC
167// combined) DELETED. The kind-generic counterparts in
168// `array_aggregation::handle_{sum,avg,min,max,count,reduce}_v2` +
169// `array_basic::handle_{len,is_empty,first,last,push,pop,get,set,clone}_v2`
170// (registered in `ARRAY_METHODS` and gained real bodies via W16.2-J.0,
171// commit `fbe86020`) cover every method the per-kind handlers used to
172// host. See `method_registry.rs` for the deletion banner.
173
174// Concurrency primitive (Mutex<T>, Atomic<T>, Lazy<T>) method handlers.
175pub mod concurrency_methods;
176
177// Channel (MPSC sender/receiver) method handlers.
178pub mod channel_methods;
179
180// Concatenation opcodes (StringConcat, ArrayConcat) — dedicated v2 replacements
181// for the generic Add overload on built-in heap types.
182pub mod concat;
183
184// Typed HashMap and String access opcodes — local-slot based, skip HeapValue dispatch.
185pub mod typed_access;
186
187use crate::{
188    bytecode::{Instruction, OpCode, Operand},
189    executor::VirtualMachine,
190};
191use shape_value::{HeapKind, HeapValue, KindedSlot, NativeKind, TemporalData, ValueSlot, VMError};
192
193/// Select the method-registry PHF lookup for a v2-raw `TypedArray<T>`
194/// receiver, classified by its stamped element-type discriminant.
195///
196/// **W16.2-J.1 (2026-05-22):** the per-kind PHF registries
197/// `TYPED_INT_ARRAY_METHODS` + `TYPED_NUMBER_ARRAY_METHODS` were deleted
198/// alongside their handler module files (`typed_int_array_methods.rs` /
199/// `typed_number_array_methods.rs`, 667 LoC combined). The prereq W16.2-J.0
200/// (commit `fbe86020`) migrated the kind-generic counterparts in
201/// `array_aggregation::handle_{sum,avg,min,max,count,reduce}_v2` +
202/// `array_basic::handle_{len,is_empty,first,last,push,pop,get,set,clone}_v2`
203/// from `ckpt[2-5]_surface` stubs to real bodies delegating to the
204/// `v2_array_detect::{sum,avg,min,max,push,pop,read,write}_element(s)`
205/// primitives — those entries live in `ARRAY_METHODS`.
206///
207/// Result: every numeric `V2ElemType` arm now returns `None`; the caller
208/// falls back to `ARRAY_METHODS` for the kind-generic implementation. The
209/// surviving non-`None` arm is `V2ElemType::Bool` → `BOOL_ARRAY_METHODS`,
210/// which carries closure-callback / aggregation residuals (`count`, `any`,
211/// `all`, `toArray`) tracked by the W17 typed-carrier-monomorphization
212/// workstream and still routes through the bool-specific handler set.
213fn typed_array_method_registry(
214    elem_type: crate::executor::v2_handlers::v2_array_detect::V2ElemType,
215    method_name: &str,
216) -> Option<method_registry::MethodHandler> {
217    use crate::executor::v2_handlers::v2_array_detect::V2ElemType;
218    match elem_type {
219        // Numeric element kinds — fall through to ARRAY_METHODS via the
220        // caller's `.or_else(...)` chain. W16.2-J.1 deleted the per-kind
221        // PHFs that previously lived here; the kind-generic
222        // `array_aggregation::*` / `array_basic::*` handlers in
223        // ARRAY_METHODS now cover len/length/push/pop/first/last/get/set/
224        // sum/avg/mean/min/max/clone uniformly.
225        V2ElemType::I64
226        | V2ElemType::I32
227        | V2ElemType::I8
228        | V2ElemType::U8
229        | V2ElemType::I16
230        | V2ElemType::U16
231        | V2ElemType::U32
232        | V2ElemType::F64
233        | V2ElemType::F32 => None,
234        // Bool carries closure-callback / aggregation residuals
235        // (count / any / all / toArray) — W17 typed-carrier-
236        // monomorphization territory. The kind-generic len/first/last/
237        // isEmpty entries in BOOL_ARRAY_METHODS still alias to
238        // `array_basic::handle_*_v2`, so semantics are uniform with
239        // ARRAY_METHODS for those names.
240        V2ElemType::Bool => method_registry::BOOL_ARRAY_METHODS
241            .get(method_name)
242            .copied(),
243        // Char / String / Decimal / TypedObject have no dedicated typed-
244        // array method registry — fall back to generic ARRAY_METHODS.
245        V2ElemType::Char
246        | V2ElemType::String
247        | V2ElemType::Decimal
248        | V2ElemType::TypedObject => None,
249    }
250}
251
252impl VirtualMachine {
253    /// Dispatch shell for object opcodes.
254    ///
255    /// Each opcode arm currently calls into a sibling Cluster D file
256    /// (`object_creation`, `property_access`, `array_operations`, etc.) whose
257    /// own substep-2 migration is in flight under a peer Wave-α sub-cluster.
258    /// The dispatch shell itself is kind-correct because it forwards to the
259    /// per-opcode handler unchanged. The legacy entries that lived directly
260    /// in `objects/mod.rs` (`op_call_method`, `op_wrap_type_annotation`,
261    /// `op_make_range`) are surfaced below — see each function's doc comment
262    /// for the architectural cascade ruling.
263    #[inline(always)]
264    pub(in crate::executor) fn exec_objects(
265        &mut self,
266        instruction: &Instruction,
267        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
268    ) -> Result<(), VMError> {
269        use OpCode::*;
270        match instruction.opcode {
271            NewArray => self.op_new_array(instruction)?,
272            NewTypedArray => self.op_new_typed_array(instruction)?,
273            NewMatrix => self.op_new_matrix(instruction)?,
274            NewObject => self.op_new_object(instruction)?,
275            GetProp => self.op_get_prop(ctx)?,
276            SetProp => self.op_set_prop()?,
277            SetLocalIndex => self.op_set_local_index(instruction)?,
278            SetModuleBindingIndex => self.op_set_module_binding_index(instruction)?,
279            Length => self.op_length()?,
280            ArrayPush => self.op_array_push()?,
281            ArrayPushLocal => self.op_array_push_local(instruction)?,
282            ArrayPop => self.op_array_pop()?,
283            MakeClosure => self.op_make_closure(instruction)?,
284            MergeObject => self.op_merge_object()?,
285            NewTypedObject => self.op_new_typed_object(instruction)?,
286            TypedMergeObject => self.op_typed_merge_object(instruction)?,
287            WrapTypeAnnotation => self.op_wrap_type_annotation(instruction)?,
288            SliceAccess => self.op_slice_access()?,
289            MakeRange => self.op_make_range()?,
290            _ => unreachable!(
291                "exec_objects called with non-object opcode: {:?}",
292                instruction.opcode
293            ),
294        }
295        Ok(())
296    }
297
298    /// SURFACE: WrapTypeAnnotation cannot be migrated in this cluster.
299    ///
300    /// The pre-Wave-6 body popped a `ValueWord` and constructed a
301    /// `ValueWord::from_type_annotated_value(name, inner)` wrapper. Both the
302    /// `ValueWord` type and the `from_type_annotated_value` constructor were
303    /// deleted by the strict-typing bulldozer before substep-1; there is no
304    /// post-§2.7.7 wrapper shape. The annotation-wrap design itself needs
305    /// re-thinking under ADR-006 (annotations as parallel metadata, not as a
306    /// payload tag), which is outside the D-objects-mod sub-cluster's
307    /// territory.
308    ///
309    /// Cross-cluster cascade: the compiler emitter currently produces
310    /// `WrapTypeAnnotation` opcodes; that emit site is in `compiler/` and
311    /// must coordinate with the kinded annotation-metadata model before this
312    /// handler is rewritten.
313    fn op_wrap_type_annotation(&mut self, _instruction: &Instruction) -> Result<(), VMError> {
314        Err(VMError::NotImplemented(
315            "SURFACE: WrapTypeAnnotation depends on the deleted ValueWord wrapper \
316             type. Annotation wrapping needs a kinded redesign (ADR-006 §2.7.6 \
317             / Q8) — see playbook §8 cross-cluster cascade. D-objects-mod scope \
318             does not include the compiler emit site."
319                .into(),
320        ))
321    }
322
323    /// CallMethod dispatch shell (W16-op-call-method close).
324    ///
325    /// ADR-006 §2.7.10 / Q11 dispatch shell — pops the receiver +
326    /// arg-count call args from the §2.7.7 kinded stack, classifies
327    /// the receiver kind to pick the matching PHF method registry,
328    /// dispatches through `MethodFnV2`, and pushes the kinded result.
329    ///
330    /// Body shape per the W7-op-call-value precedent (close commit
331    /// `27812cf`, `executor/control_flow/mod.rs:dispatch_call_value_immediate`):
332    ///
333    /// 1. Pop `arg_count + 1` slots via `pop_kinded()` (receiver
334    ///    included). Each pop transfers one share (heap-bearing kinds)
335    ///    into the returned `(bits, kind)` pair (WB2.4 retain-on-read,
336    ///    §2.7.7); the `KindedSlot::new` carrier takes ownership of
337    ///    that share. Pop order is reverse of push order, so reverse
338    ///    the vec back to position-aligned order with `args[0]` =
339    ///    receiver.
340    /// 2. Decode `arg_count` + method name from
341    ///    `Operand::TypedMethodCall { arg_count, string_id, .. }`
342    ///    (`bytecode/opcode_defs.rs:2023`). The method name string is
343    ///    indexed via `string_id` into `self.program.strings`.
344    /// 3. Classify `args[0].kind` to pick a PHF registry per the
345    ///    §2.7.6 / Q8 heterogeneous-kind body pattern. Numeric / Bool
346    ///    / String scalars route to the matching scalar registry;
347    ///    `Ptr(HeapKind::*)` heap kinds route to the per-heap-kind
348    ///    registry, with `HeapKind::TypedArray` sub-classified on the
349    ///    inner `TypedArrayData::{I64, F64, Bool, ...}` variant via
350    ///    `slot.as_heap_value()` and `HeapKind::Temporal`
351    ///    sub-classified on the inner `TemporalData::{DateTime,
352    ///    TimeSpan, ...}` variant. The v2 typed-array fast path
353    ///    (`UInt64`-tagged raw `*mut TypedArray<T>` pointer) routes
354    ///    through `as_v2_typed_array`; post-W16.2-J.1 every numeric
355    ///    element kind falls through to the kind-generic
356    ///    `ARRAY_METHODS` PHF (per the `typed_array_method_registry`
357    ///    helper, which returns `None` for `V2ElemType::{I*, U*, F*}`).
358    ///    `V2ElemType::Bool` continues to route via
359    ///    `BOOL_ARRAY_METHODS`.
360    /// 4. PHF lookup keyed on `&str` method name returns the
361    ///    `MethodFnV2` handler. A miss surfaces a `RuntimeError`
362    ///    citing the receiver kind + method name; user-defined
363    ///    methods on `HeapValue::TypedObject` fall through to a UFCS
364    ///    function-name lookup (`function_name_index`) before the
365    ///    final `Unknown method` error. Closure / Future / Reference
366    ///    / SharedCell / FilterExpr receivers reject — they are not
367    ///    method-call targets.
368    /// 5. Dispatch: `handler(self, &args, ctx)` returns
369    ///    `Result<KindedSlot, VMError>`. The `&[KindedSlot]` borrow
370    ///    leaves the shares with the carriers in this stack frame —
371    ///    handlers borrow each entry per §2.7.10 / Q11 borrow-only
372    ///    ABI.
373    /// 6. Push the result via `push_kinded(result.raw(), result.kind())`
374    ///    and `std::mem::forget(result)` so the result share transfers
375    ///    cleanly to the stack (no double-drop). The `args` carriers
376    ///    drop at end of scope; `KindedSlot::Drop` dispatches on kind
377    ///    and releases each share via `drop_with_kind` (no bare
378    ///    `vw_drop`, no Bool-default fallback).
379    ///
380    /// Forbidden surfaces (per CLAUDE.md "Renames to refuse on sight"
381    /// + ADR-006 §2.7.10 / Q11): `Vec<KindedSlot>` by-move into a
382    /// dispatch helper; `args: &mut [KindedSlot]`; tag-bits decode on
383    /// receiver bits; `is_heap()` probe on raw bits; Bool-default
384    /// fallback for unknown kind; defection-attractor framing on
385    /// the method-dispatch ABI (`MethodFn` / `MethodFnLegacy` /
386    /// `dispatch_method_handler_raw` / `call_handler_with_u64_slice`).
387    ///
388    /// Surfaces remaining (out of W16 territory):
389    /// - **IC fast-path recording / hit**: `method_ic_check` /
390    ///   `method_ic_record` already accept the kinded `MethodFnV2`
391    ///   transmute (`ic_fast_paths.rs:42-44`) — wiring the IC
392    ///   recording at the dispatch shell is a downstream JIT-IC
393    ///   follow-up, not a correctness gate. The dispatch shell stays
394    ///   correct without IC; the IC adds speed only.
395    /// - **`HeapKind::Closure` receivers** (e.g. closure-as-trait-
396    ///   object dispatch). Trait-object dispatch goes through
397    ///   `op_dyn_method_call`, not `op_call_method`; the closure arm
398    ///   here rejects with a clear error.
399    pub fn op_call_method(
400        &mut self,
401        instruction: &Instruction,
402        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
403    ) -> Result<(), VMError> {
404        // ADR-006 §2.7.10 / Q11: arg_count + method name from operand
405        // (typed dispatch is the only emit shape per
406        // `compiler/expressions/function_calls.rs:2014` / `binary_ops.rs`
407        // / `unary_ops.rs`). Legacy stack-arg-count dispatch is gone.
408        let (arg_count, string_id, _method_id, _receiver_type_tag) = match instruction.operand {
409            Some(Operand::TypedMethodCall {
410                method_id,
411                arg_count,
412                string_id,
413                receiver_type_tag,
414            }) => (
415                arg_count as usize,
416                string_id as usize,
417                method_id,
418                receiver_type_tag,
419            ),
420            _ => return Err(VMError::InvalidOperand),
421        };
422
423        // ADR-006 §2.7.24 Q25.C: when the receiver is a trait object,
424        // route through the DynMethodCall dispatch shell instead of the
425        // standard CallMethod path. This handles the case where the
426        // compiler couldn't determine at compile-time that the receiver
427        // is a `dyn T` (e.g. `let b = a.clone_me()` where `clone_me`
428        // returns `Self` through a `BoxedReturn` thunk — the result is
429        // a trait object but the compiler emits the standard CallMethod
430        // opcode without a `dyn_locals` entry for `b`). Round-2: this
431        // fallback ensures correctness; a future amendment can teach
432        // type-inference to propagate `dyn T` through method-call
433        // result types and emit `DynMethodCall` at the compile site.
434        if self.sp >= arg_count + 1 {
435            let receiver_idx_check = self.sp - arg_count - 1;
436            let (_, receiver_kind_peek) = self.stack_read_kinded_raw(receiver_idx_check);
437            if receiver_kind_peek
438                == NativeKind::Ptr(shape_value::HeapKind::TraitObject)
439            {
440                // Reconstruct the instruction with `arg_count` /
441                // `string_id` operands and call into the dyn dispatch
442                // path. The TypedMethodCall operand layout matches
443                // exactly what `op_dyn_method_call` expects.
444                return self.exec_trait_object_ops(
445                    &Instruction::new(
446                        crate::bytecode::OpCode::DynMethodCall,
447                        Some(Operand::TypedMethodCall {
448                            method_id: _method_id,
449                            arg_count: arg_count as u16,
450                            string_id: string_id as u16,
451                            receiver_type_tag: _receiver_type_tag,
452                        }),
453                    ),
454                    ctx,
455                );
456            }
457        }
458
459        // Pop receiver + arg_count call args. Each pop_kinded transfers
460        // one share into the returned (bits, kind); the KindedSlot
461        // carrier takes ownership and releases via drop_with_kind on
462        // scope exit. ADR-006 §2.7.7 WB2.4 retain-on-read.
463        let total = arg_count + 1;
464        let mut args: Vec<KindedSlot> = Vec::with_capacity(total);
465        for _ in 0..total {
466            let (bits, kind) = self.pop_kinded()?;
467            args.push(KindedSlot::new(ValueSlot::from_raw(bits), kind));
468        }
469        // Pop is reverse of push order; flip so args[0] is the receiver.
470        args.reverse();
471
472        // Resolve method name. The string pool index was offset-fixed
473        // at link time (`executor/mod.rs:883`), so direct indexing is
474        // always in-range for a well-formed program. We clone into an
475        // owned `String` to release the immutable borrow on
476        // `self.program.strings` before the `dispatch_method_kinded`
477        // call below takes a mutable borrow on `self`.
478        let method_name: String = self
479            .program
480            .strings
481            .get(string_id)
482            .cloned()
483            .ok_or_else(|| {
484                VMError::RuntimeError(format!(
485                    "op_call_method: string_id {} out of bounds (pool size {})",
486                    string_id,
487                    self.program.strings.len()
488                ))
489            })?;
490
491        // Classify the receiver, resolve the handler, and dispatch via
492        // the shared `dispatch_method_kinded` entry — borrow-only ABI per
493        // §2.7.10 / Q11. The handler borrows each KindedSlot; share
494        // ownership stays with the carriers in `args`.
495        let result = self.dispatch_method_kinded(&args, &method_name, ctx)?;
496
497        // Transfer the result share onto the kinded stack. The result
498        // carrier is forgotten so its Drop does not double-release.
499        self.push_kinded(result.raw(), result.kind())?;
500        std::mem::forget(result);
501
502        // `args` carriers drop here. `KindedSlot::Drop` dispatches on
503        // each entry's kind and retires its share via the matching
504        // `Arc::decrement_strong_count::<T>` arm — no bare vw_drop
505        // (forbidden), no Bool-default fallback (forbidden §2.7.7 #9).
506        Ok(())
507    }
508
509    /// Shared method-dispatch entry: resolve the handler via
510    /// [`resolve_method_handler`](Self::resolve_method_handler) and call
511    /// it with the kinded carrier slice.
512    ///
513    /// Two callers consume this entry:
514    ///
515    /// 1. `op_call_method` (above) — VM-side dispatch shell after popping
516    ///    the receiver + args from the §2.7.7 stack parallel-kind track.
517    /// 2. `jit_trampoline_call_method` (in
518    ///    `crates/shape-vm/src/executor/call_convention.rs`) — the
519    ///    §2.7.5 cross-crate stable-FFI consumer that converts the JIT's
520    ///    pair-slice form into `&[KindedSlot]` carriers and delegates
521    ///    here for the actual dispatch.
522    ///
523    /// `args[0]` is the receiver, `args[1..]` are the call args. Every
524    /// entry's `kind` came from the §2.7.7 parallel-kind track at the
525    /// producing site — no fabrication. The handler borrows each
526    /// `KindedSlot` (§2.7.10 / Q11 borrow-only ABI); share ownership
527    /// stays with the carriers at the caller. The returned `KindedSlot`
528    /// owns its result share — the caller pushes it onto the stack or
529    /// transfers it across the FFI boundary, then `mem::forget`s the
530    /// returned carrier to balance refcounts.
531    pub(crate) fn dispatch_method_kinded(
532        &mut self,
533        args: &[KindedSlot],
534        method_name: &str,
535        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
536    ) -> Result<KindedSlot, VMError> {
537        // Phase 4 (trait Add/AddAssign for user types, 2026-05-16):
538        // Before falling into the PHF-based handler resolution, give
539        // user-defined methods (`impl Trait for X { method m(...) }` and
540        // `impl X { method m(...) }`) a chance to dispatch via UFCS on
541        // the receiver's concrete type name. The compiler registers each
542        // such method under the function name `"{TypeName}::{method}"`
543        // (see `compiler/statements.rs::desugar_impl_method`); we look
544        // that name up in `function_name_index` and, if found, call the
545        // function directly. This makes `a + b` work for `impl Add for
546        // Money` (binary_ops.rs emits `CallMethod("add")` after the
547        // operator-trait check fires), and likewise for any other user-
548        // authored method on a TypedObject.
549        //
550        // The PHF-based fallback below still handles built-in methods on
551        // TypedObject receivers (the `DATATABLE_METHODS` PHF covers the
552        // generic table-shaped methods) — UFCS takes precedence so users
553        // can shadow / extend the built-in surface with their own impls.
554        //
555        // We resolve the candidate function_id WITHOUT consuming `ctx`
556        // first, so we can re-thread `ctx` into the PHF handler when
557        // UFCS declines. The call path takes `ctx` only after the
558        // function_id resolves.
559        if let NativeKind::Ptr(HeapKind::TypedObject) = args[0].kind {
560            if let Some(function_id) = self.resolve_typed_object_ufcs(args, method_name) {
561                return self.invoke_typed_object_ufcs(args, function_id, ctx);
562            }
563        }
564        let handler = self.resolve_method_handler(args, method_name)?;
565        handler(self, args, ctx)
566    }
567
568    /// Resolve a `TypedObject`-receiver method name to a UFCS function id
569    /// (Phase 4 trait Add/AddAssign work, 2026-05-16).
570    ///
571    /// Reads the receiver's `schema_id` (which the v2-raw
572    /// `TypedObjectStorage` exposes at field offset, per
573    /// `heap_value.rs:3497`), looks up the concrete type name in
574    /// `program.type_schema_registry`, and checks
575    /// `function_name_index["{TypeName}::{method}"]`. Returns the
576    /// post-link function id if registered, `None` otherwise.
577    ///
578    /// `compiler/statements.rs::desugar_impl_method` is the producer that
579    /// registers `impl Add for Money { method add(other) ... }` as the
580    /// function `Money::add` in `function_name_index`.
581    ///
582    /// Caller invariant: `args[0].kind == NativeKind::Ptr(HeapKind::TypedObject)`.
583    /// SAFETY: dereferences `args[0].slot.raw()` as `*const TypedObjectStorage`
584    /// per §2.3 typed-Arc invariant + Wave 2 Round 4 D4 ckpt-3 v2-raw
585    /// migration; the borrowed `KindedSlot` in `args[0]` owns one share
586    /// so the pointee stays live for this scope.
587    fn resolve_typed_object_ufcs(
588        &self,
589        args: &[KindedSlot],
590        method_name: &str,
591    ) -> Option<u16> {
592        let receiver_bits = args[0].slot.raw();
593        if receiver_bits == 0 {
594            return None;
595        }
596        // SAFETY: per the caller's invariant the receiver is a
597        // `Ptr(HeapKind::TypedObject)` slot. Slot bits are
598        // `*const TypedObjectStorage` (v2-raw migration per
599        // `heap_value.rs:3497`); the borrowed `KindedSlot` carrier in
600        // `args[0]` owns one share so the pointee stays live for this
601        // scope. Transient borrow — no Arc reconstruction.
602        let schema_id = unsafe {
603            (*(receiver_bits as *const shape_value::TypedObjectStorage)).schema_id
604        };
605        let concrete_type_name = self
606            .program
607            .type_schema_registry
608            .get_by_id(schema_id as u32)
609            .map(|schema| schema.name.clone())?;
610        let function_name = format!("{}::{}", concrete_type_name, method_name);
611        self.function_name_index.get(&function_name).copied()
612    }
613
614    /// Invoke a UFCS-resolved Shape function on a TypedObject receiver +
615    /// args (Phase 4 trait Add/AddAssign work, 2026-05-16).
616    ///
617    /// Pushes receiver + args back onto the kinded stack (cloning shares
618    /// since the borrowed `args` carriers retain ownership of the
619    /// originals — the caller's `KindedSlot::Drop` will release those),
620    /// then sets up a fresh call frame via `call_function_with_nb_args`
621    /// + `execute_until_call_depth`, pops the function's return value
622    /// from the kinded stack, and returns it as a `KindedSlot` whose
623    /// carrier owns the result share.
624    ///
625    /// Mirrors `trait_object_ops.rs::invoke_dyn_unified` for the
626    /// non-Self-arg, non-BoxedReturn case (the typical user-defined
627    /// `impl Add for X { method add(other: X) -> X }` shape).
628    fn invoke_typed_object_ufcs(
629        &mut self,
630        args: &[KindedSlot],
631        function_id: u16,
632        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
633    ) -> Result<KindedSlot, VMError> {
634        // Phase 4 fix (2026-05-16): route through the canonical
635        // `execute_function_by_id` public entry-point — the same pattern
636        // `execute_function_with_named_args` uses for borrowed-args call
637        // sites (`call_convention.rs:211-256`). The earlier hand-rolled
638        // `call_function_with_nb_args + execute_until_call_depth` path
639        // had a non-deterministic double-free that surfaced on `+=`
640        // desugar fixtures (`m = m + Money{...}`); bisect attributed it
641        // to subtle interactions between the manual `self.sp =
642        // base_pointer` adjustment and downstream frame setup. Routing
643        // through the established public entry-point eliminates the
644        // surface — that helper is the §2.7.10/Q11 canonical shape for
645        // "borrowed args, owned-share-per-call invocation".
646        //
647        // Build an owned `Vec<KindedSlot>` for the new frame by bumping
648        // one share per arg via `clone_with_kind` (§2.7.7 WB2.4) — the
649        // borrowed `args` slice's carriers retain ownership of the
650        // originals (op_call_method's `args` carriers drop those at end
651        // of scope), so we mint independent shares for the called
652        // function's locals. `execute_function_by_id` then runs the
653        // standard call protocol: `call_function_with_nb_args` transfers
654        // shares into the new frame, `mem::forget(args)` balances, the
655        // function runs to completion, the return value is popped and
656        // returned as a `KindedSlot` whose carrier owns the result share.
657        let mut call_args: Vec<KindedSlot> = Vec::with_capacity(args.len());
658        for slot in args.iter() {
659            let bits = slot.slot.raw();
660            let kind = slot.kind;
661            crate::executor::vm_impl::stack::clone_with_kind(bits, kind);
662            call_args.push(KindedSlot::new(ValueSlot::from_raw(bits), kind));
663        }
664        self.execute_function_by_id(function_id, call_args, ctx)
665    }
666
667    /// Resolve a method handler from `(receiver_kind, method_name)`.
668    ///
669    /// Receiver classification per ADR-006 §2.7.6 / Q8 heterogeneous-
670    /// kind body pattern: scalar kinds map directly to scalar PHF
671    /// registries; `Ptr(HeapKind::*)` heap kinds map to the matching
672    /// per-heap-kind registry, with `TypedArray` and `Temporal`
673    /// sub-classified through `slot.as_heap_value()` matching to pick
674    /// the element-typed sub-registry. The `UInt64`-tagged v2 typed-
675    /// array fast path (`*mut TypedArray<T>` pointer with stamped
676    /// element-type byte) routes through `v2_array_detect`.
677    ///
678    /// Returns `Err(RuntimeError)` for unknown method on a known
679    /// receiver kind, or unsupported receiver kind. Falls through to
680    /// `function_name_index` UFCS for `HeapKind::TypedObject`
681    /// receivers when the method is not in `DATATABLE_METHODS` (the
682    /// dispatch table covering generic table-shaped methods is the
683    /// closest fit; user-defined methods land via UFCS).
684    fn resolve_method_handler(
685        &self,
686        args: &[KindedSlot],
687        method_name: &str,
688    ) -> Result<method_registry::MethodHandler, VMError> {
689        use crate::executor::v2_handlers::v2_array_detect::as_v2_typed_array;
690
691        let receiver = &args[0];
692        let kind = receiver.kind;
693
694        // Pure-scalar receivers — kind alone selects the registry.
695        let scalar_handler: Option<method_registry::MethodHandler> = match kind {
696            NativeKind::Float64
697            | NativeKind::NullableFloat64
698            | NativeKind::Int8
699            | NativeKind::NullableInt8
700            | NativeKind::UInt8
701            | NativeKind::NullableUInt8
702            | NativeKind::Int16
703            | NativeKind::NullableInt16
704            | NativeKind::UInt16
705            | NativeKind::NullableUInt16
706            | NativeKind::Int32
707            | NativeKind::NullableInt32
708            | NativeKind::UInt32
709            | NativeKind::NullableUInt32
710            | NativeKind::Int64
711            | NativeKind::NullableInt64
712            | NativeKind::NullableUInt64
713            | NativeKind::IntSize
714            | NativeKind::NullableIntSize
715            | NativeKind::UIntSize
716            | NativeKind::NullableUIntSize => method_registry::NUMBER_METHODS.get(method_name).copied(),
717            NativeKind::Bool => method_registry::BOOL_METHODS.get(method_name).copied(),
718            NativeKind::String => method_registry::STRING_METHODS.get(method_name).copied(),
719            // Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14):
720            // ADR-006 §2.7.5 amendment adds F32 + Char as scalar variants.
721            // F32 receivers route to NUMBER_METHODS (same numeric method
722            // surface as F64). Char receivers route to CHAR_METHODS — the
723            // existing receiver registry already covers char methods
724            // (`.to_uppercase()`, `.is_alphabetic()`, etc.) and was wired
725            // for the `NativeKind::Ptr(HeapKind::Char)` carrier; the same
726            // method surface applies regardless of which Char carrier
727            // label flows through (both labels store the same codepoint
728            // bits and method bodies read via `as_char` which recognizes
729            // both labels per the §2.7.5 amendment).
730            NativeKind::Float32 => method_registry::NUMBER_METHODS.get(method_name).copied(),
731            NativeKind::Char => method_registry::CHAR_METHODS.get(method_name).copied(),
732            // Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions
733            // (2026-05-14): the v2-raw `*const StringObj` / `*const DecimalObj`
734            // carrier receivers route to the same method registry as their
735            // Arc-wrapped siblings — the method-handler bodies dispatch on
736            // the carrier shape (the slot's kind label drives the per-
737            // carrier read of UTF-8 bytes / Decimal value). Method-handler
738            // body migration for v2-raw reads is the Agent A2 (producer)
739            // / consumer-side cluster-1 hardening territory; this row pins
740            // method-registry selection at the dispatch shell.
741            NativeKind::StringV2 => method_registry::STRING_METHODS.get(method_name).copied(),
742            // DecimalV2 routes to NUMBER_METHODS — same as the Arc-wrapped
743            // `HeapKind::Decimal` sibling per the heap-arm row below.
744            NativeKind::DecimalV2 => method_registry::NUMBER_METHODS.get(method_name).copied(),
745            // r5c-2-β-CKPT-C u64-carrier-disambiguation (2026-05-20):
746            // `Ptr(HeapKind::TypedArray)` is the single canonical carrier
747            // kind for every v2-raw `*mut TypedArray<T>` pointer (direct
748            // `NewTypedArray*` allocation + refcounted struct-field /
749            // closure-capture read). Classify via the stamped element-type
750            // byte → typed-array method registry. A genuine scalar `u64`
751            // (`NativeKind::UInt64`) routes purely to `NUMBER_METHODS` and
752            // is NEVER passed to `as_v2_typed_array` — the pre-fix shared
753            // arm dereferenced an arbitrary scalar `u64` value as a header
754            // pointer → SIGSEGV.
755            NativeKind::Ptr(HeapKind::TypedArray) => {
756                let bits = receiver.slot.raw();
757                if let Some(view) = as_v2_typed_array(bits, kind) {
758                    typed_array_method_registry(view.elem_type, method_name)
759                        .or_else(|| method_registry::ARRAY_METHODS.get(method_name).copied())
760                } else {
761                    // Kind says TypedArray but the bits failed v2 detection —
762                    // still an array receiver; fall back to generic methods.
763                    method_registry::ARRAY_METHODS.get(method_name).copied()
764                }
765            }
766            NativeKind::UInt64 => {
767                // Genuine scalar `u64` — numeric method surface only.
768                method_registry::NUMBER_METHODS.get(method_name).copied()
769            }
770            NativeKind::Ptr(_) => None,
771            // R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 +
772            // §2.7.7/Q9, 2026-05-19): `NativeKind::Null` receivers have
773            // no method dispatch surface — null has no methods.
774            NativeKind::Null => None,
775        };
776        if let Some(h) = scalar_handler {
777            return Ok(h);
778        }
779
780        // Heap receivers — dispatch on HeapKind, then sub-classify
781        // TypedArray / Temporal via `slot.as_heap_value()`.
782        if let NativeKind::Ptr(hk) = kind {
783            let heap_handler: Option<method_registry::MethodHandler> = match hk {
784                HeapKind::String => method_registry::STRING_METHODS.get(method_name).copied(),
785                HeapKind::Char => method_registry::CHAR_METHODS.get(method_name).copied(),
786                HeapKind::HashMap => method_registry::HASHMAP_METHODS.get(method_name).copied(),
787                HeapKind::HashSet => method_registry::SET_METHODS.get(method_name).copied(),
788                HeapKind::DataTable => method_registry::DATATABLE_METHODS
789                    .get(method_name)
790                    .copied(),
791                HeapKind::Iterator => method_registry::ITERATOR_METHODS.get(method_name).copied(),
792                HeapKind::Instant => method_registry::INSTANT_METHODS.get(method_name).copied(),
793                HeapKind::Content => method_registry::CONTENT_METHODS.get(method_name).copied(),
794                HeapKind::Decimal => method_registry::NUMBER_METHODS.get(method_name).copied(),
795                HeapKind::BigInt => method_registry::NUMBER_METHODS.get(method_name).copied(),
796                HeapKind::TypedArray => {
797                    // V3-S5 ckpt-5: TypedArrayData enum + outer
798                    // HeapValue::TypedArray arm DELETED at ckpt-1..ckpt-4.
799                    // Sub-classification by inner variant is gone; fall
800                    // through to the generic ARRAY_METHODS PHF. Per-element-
801                    // kind dispatch lands at ckpt-6 STRICT close via the
802                    // v2-raw `TypedArray<T>` direct-access target (caller
803                    // classifies element type from the v2 header's
804                    // element-type byte instead of the deleted variant).
805                    method_registry::ARRAY_METHODS.get(method_name).copied()
806                }
807                // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13):
808                // Matrix is a first-class HeapKind — receivers route
809                // directly to `MATRIX_METHODS` (no inner-TypedArrayData
810                // sub-classification two-step). MatrixSlice receivers
811                // route to `FLOAT_ARRAY_METHODS` (their methods are
812                // numeric-aggregations over a flat f64 region; the same
813                // PHF that handles `F64`-typed arrays applies).
814                HeapKind::Matrix => method_registry::MATRIX_METHODS.get(method_name).copied(),
815                HeapKind::MatrixSlice => method_registry::FLOAT_ARRAY_METHODS
816                    .get(method_name)
817                    .copied(),
818                HeapKind::Temporal => {
819                    // C1-temporal-lowering (Phase 2d Wave 2): Temporal
820                    // slots are `Arc::into_raw::<TemporalData>` — NOT a
821                    // `Box<HeapValue>` allocation. `as_heap_value()` would
822                    // be wrong-type recovery (5-arm receiver-recovery
823                    // soundness rule, CLAUDE.md / handover §0). Sub-
824                    // classify by directly borrowing `&TemporalData` from
825                    // the slot's Arc-raw pointer, mirroring
826                    // `objects/datetime_methods.rs::recv_temporal`.
827                    //
828                    // SAFETY: when receiver.kind == Ptr(HeapKind::Temporal),
829                    // receiver.slot.raw() is `Arc::into_raw::<TemporalData>`
830                    // (set by `op_push_const::Constant::Duration` /
831                    // `Constant::DateTimeExpr` arms, by
832                    // `temporal_result()` in datetime_methods.rs, and by
833                    // the §2.7.7 stack parallel-kind track). The carrier
834                    // owns one strong-count share for the dispatch
835                    // duration; the &TemporalData borrow's lifetime is
836                    // bounded by `args[0]`'s share ownership.
837                    let bits = receiver.slot.raw();
838                    if bits == 0 {
839                        None
840                    } else {
841                        let td: &TemporalData =
842                            unsafe { &*(bits as *const TemporalData) };
843                        match td {
844                            TemporalData::DateTime(_) => {
845                                method_registry::DATETIME_METHODS
846                                    .get(method_name)
847                                    .copied()
848                            }
849                            TemporalData::TimeSpan(_) | TemporalData::Duration(_) => {
850                                method_registry::TIMESPAN_METHODS
851                                    .get(method_name)
852                                    .copied()
853                            }
854                            // Timeframe / TimeReference / DateTimeExpr /
855                            // DataDateTimeRef have no method PHF — they
856                            // are language-level metadata, not method-
857                            // call targets. Fall through to
858                            // UnknownMethod.
859                            _ => None,
860                        }
861                    }
862                }
863                HeapKind::TypedObject => {
864                    // User-defined object methods land here. The
865                    // built-in DataTable PHF covers shared table-shape
866                    // methods; UFCS resolution below catches user-
867                    // defined `fn TypeName.method(self, ...)` shapes.
868                    method_registry::DATATABLE_METHODS
869                        .get(method_name)
870                        .copied()
871                }
872                HeapKind::TableView => method_registry::DATATABLE_METHODS
873                    .get(method_name)
874                    .copied(),
875                // Wave 15 W15-deque / W15-channel / W15-priority-queue
876                // closes (ADR-006 §2.7.19/Q20, §2.7.20/Q21, §2.7.18/Q19)
877                // — the new HeapKind ordinals 23/24/25 with their
878                // `*_METHODS` registries.
879                HeapKind::Deque => method_registry::DEQUE_METHODS.get(method_name).copied(),
880                HeapKind::Channel => method_registry::CHANNEL_METHODS.get(method_name).copied(),
881                HeapKind::PriorityQueue => method_registry::PRIORITY_QUEUE_METHODS
882                    .get(method_name)
883                    .copied(),
884                // W17-concurrency (ADR-006 §2.7.25, 2026-05-11): the
885                // new HeapKind ordinals 30/31/32 with their
886                // MUTEX_METHODS / ATOMIC_METHODS / LAZY_METHODS
887                // registries. Method-receiver classification routes
888                // `m.lock()` / `a.fetch_add(...)` / `l.get()` here.
889                HeapKind::Mutex => method_registry::MUTEX_METHODS.get(method_name).copied(),
890                HeapKind::Atomic => method_registry::ATOMIC_METHODS.get(method_name).copied(),
891                HeapKind::Lazy => method_registry::LAZY_METHODS.get(method_name).copied(),
892                // W15-range close (ADR-006 §2.7.23/Q24): Range receivers
893                // route to the RANGE_METHODS PHF.
894                HeapKind::Range => method_registry::RANGE_METHODS.get(method_name).copied(),
895                // W14-variant-codegen close (ADR-006 §2.7.17/Q18):
896                // Result/Option are typed-Arc carriers; method-call
897                // dispatch goes through op_is_ok / op_unwrap_ok / etc.
898                // typed opcodes, not through the generic method PHF.
899                // No method-PHF arm; falls through to UFCS / unknown.
900                HeapKind::Result | HeapKind::Option => None,
901                // ADR-006 §2.7.10 explicitly excludes the closure /
902                // future / reference / shared-cell / filter-expr
903                // discriminators from method-call dispatch — these are
904                // not user-callable receivers. Trait-object method
905                // calls go through `op_dyn_method_call`, not here —
906                // the compiler-emission tier (W17-trait-object-emission)
907                // emits `DynMethodCall` opcodes that walk the receiver's
908                // `Arc<TraitObjectStorage>::vtable` directly per
909                // ADR-006 §2.7.24 / Q25.C.5 `VTableEntry` shape, NOT
910                // through this generic method PHF.
911                HeapKind::Closure
912                | HeapKind::Future
913                | HeapKind::Reference
914                | HeapKind::SharedCell
915                | HeapKind::FilterExpr
916                | HeapKind::TraitObject
917                | HeapKind::IoHandle
918                | HeapKind::TaskGroup
919                | HeapKind::NativeView
920                | HeapKind::NativeScalar
921                // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
922                // ModuleFn references are not user-callable receivers
923                // via method-call dispatch — they route through
924                // op_call_value's `Ptr(HeapKind::ModuleFn)` arm directly
925                // (`invoke_module_fn_id_stub`), not through this generic
926                // PHF lookup.
927                | HeapKind::ModuleFn => None,
928            };
929            if let Some(h) = heap_handler {
930                return Ok(h);
931            }
932        }
933
934        // UFCS / unknown — surface the receiver kind in the error so
935        // call sites can diagnose. Per playbook §3 "surface-and-stop
936        // if PHF lookup API doesn't quite match", an unknown method
937        // is *not* a SURFACE — it's a real runtime error the program
938        // can hit, so we return `RuntimeError`, not `NotImplemented`.
939        Err(VMError::RuntimeError(format!(
940            "no method '{}' on receiver kind {:?}",
941            method_name, kind
942        )))
943    }
944
945    /// `MakeRange` opcode body — pop (start, end, inclusive) from the §2.7.7
946    /// kinded stack and push a fresh `Arc<RangeData>` slot with kind
947    /// `NativeKind::Ptr(HeapKind::Range)` (W15-range, ADR-006 §2.7.23 / Q24).
948    ///
949    /// Stack layout at entry (from `compiler/expressions/misc.rs:369`):
950    ///
951    /// ```ignore
952    /// [.., start_value, end_value, PushConst<Bool>(inclusive), MakeRange]
953    /// ```
954    ///
955    /// Popping order is reverse-push: `inclusive` first, then `end`, then
956    /// `start`. Per the surface syntax, `start_value` and `end_value` are
957    /// `int`-typed expressions (`0..10`); the `PushNull` placeholder for
958    /// open ranges (`..n` / `n..`) reaches this handler with kind
959    /// `NativeKind::Bool` and bits zero (the `PushNull` shape) — open
960    /// ranges are surfaced as a SURFACE error pending the iterator-tier
961    /// semantic (`for i in 0..` infinite loops are their own ADR
962    /// follow-up; matches the pre-strict-typing surface).
963    ///
964    /// Other-kind bounds (Decimal, BigInt, NativeScalar) similarly
965    /// surface — the post-strict-typing `RangeData { start: i64, end: i64,
966    /// .. }` shape only models i64 ranges at landing. Cross-kind range
967    /// bounds are tracked as a follow-up §2.7.23 amendment (mirror of the
968    /// W14 Result/Option payload-cardinality discussion).
969    pub(in crate::executor) fn op_make_range(&mut self) -> Result<(), VMError> {
970        use shape_value::{KindedSlot, NativeKind, ValueSlot, heap_value::RangeData};
971
972        // Pop in reverse-push order: inclusive flag first, then end, then start.
973        // We immediately wrap each pop result in a `KindedSlot` carrier so its
974        // `Drop` impl handles refcount release on every error path automatically
975        // — no manual `drop_with_kind` bookkeeping needed.
976        let incl_kinded = {
977            let (bits, kind) = self.pop_kinded()?;
978            KindedSlot::new(ValueSlot::from_raw(bits), kind)
979        };
980        let end_kinded = {
981            let (bits, kind) = self.pop_kinded()?;
982            KindedSlot::new(ValueSlot::from_raw(bits), kind)
983        };
984        let start_kinded = {
985            let (bits, kind) = self.pop_kinded()?;
986            KindedSlot::new(ValueSlot::from_raw(bits), kind)
987        };
988
989        // The `inclusive` operand is a `PushConst<Bool>` per
990        // `compiler/expressions/misc.rs:362-368`. Kind must be Bool —
991        // any other kind is a kind-source bug at the emit site.
992        let inclusive = match incl_kinded.kind() {
993            NativeKind::Bool => incl_kinded.slot().as_bool(),
994            _ => {
995                return Err(VMError::RuntimeError(
996                    "MakeRange: inclusive flag operand must be Bool (kind-source bug \
997                     at compile site — `compiler/expressions/misc.rs` emits a \
998                     `PushConst<Bool>` for the inclusive flag)".into(),
999                ));
1000            }
1001        };
1002
1003        // Bounds: only i64 supported at landing (ADR-006 §2.7.23). Other
1004        // kinds — Float64 (`0.0..1.0` would-be syntax), Decimal, BigInt,
1005        // NativeScalar — surface for the cross-kind Range payload
1006        // follow-up. Bool with zero bits IS the `PushNull` open-range
1007        // placeholder (`..n` / `n..` / `..`) emitted by the compiler;
1008        // surface that distinctly so the diagnostic is precise.
1009        let to_i64 = |k: &KindedSlot, side: &str| -> Result<i64, VMError> {
1010            match k.kind() {
1011                NativeKind::Int64 => Ok(k.slot().as_i64()),
1012                NativeKind::Bool if k.slot().raw() == 0 => Err(VMError::NotImplemented(format!(
1013                    "MakeRange: open-range bound on {side} side (PushNull placeholder) — \
1014                     SURFACE: open ranges (`..n` / `n..` / `..`) need the iterator-tier \
1015                     infinite-iter semantic per ADR-006 §2.7.23 follow-up. Closed ranges \
1016                     (`start..end` / `start..=end`) work today.",
1017                ))),
1018                other => Err(VMError::NotImplemented(format!(
1019                    "MakeRange: cross-kind bound on {side} side (got {other:?}) — \
1020                     SURFACE: post-strict-typing RangeData only models i64 ranges at \
1021                     landing. Cross-kind bounds (Decimal, BigInt, Float64, NativeScalar) \
1022                     tracked as ADR-006 §2.7.23 follow-up.",
1023                ))),
1024            }
1025        };
1026
1027        let start = to_i64(&start_kinded, "start")?;
1028        let end = to_i64(&end_kinded, "end")?;
1029
1030        let range = std::sync::Arc::new(RangeData::new(start, end, 1, inclusive));
1031        self.push_kinded_slot(KindedSlot::from_range(range))?;
1032        Ok(())
1033    }
1034}
1035
1036// ═════════════════════════════════════════════════════════════════════════════
1037// Tests removed during D-objects-mod surface.
1038// ═════════════════════════════════════════════════════════════════════════════
1039//
1040// The pre-Wave-6 `v2a_dispatch_tests` module exercised the v2 typed-array PHF
1041// dispatch through `op_call_method` and used `ValueWord::from_native_ptr` /
1042// `ValueWord::from_array` / `ValueWord::from_i64` for receiver construction.
1043// All four constructors are deleted with the type. The tests' canonical
1044// shape (PHF resolution + handler invocation) is independent of the
1045// dispatch shell and fits naturally in `method_registry.rs`'s own test
1046// module once the handler ABI migrates; they are not required to live here.
1047// Re-instated in the post-cascade rewrite under cluster
1048// `E-builtins-backlog` / `D-v2-array-detect`.