shape-vm 0.3.2

Stack-based bytecode virtual machine for the Shape programming language
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
//! Object and array operations for the VM executor.
//!
//! Handles: NewArray, NewObject, GetProp, SetProp, Length, ArrayPush, ArrayPop,
//! MakeClosure, MergeObject, NewTypedObject, TypedMergeObject, CallMethod, MakeRange,
//! WrapTypeAnnotation, SliceAccess.
//!
//! ## Wave 6.5 substep-2 (D-objects-mod) — SURFACE
//!
//! This file is the dispatch shell for generic-object opcodes. The substep-1
//! shim deletion (`push_raw_u64` / `pop_raw_u64` / `push_native_i64` /
//! `stack_read_owned` / `stack_peek_raw`) bound this territory at 39 mandatory
//! shim sites. The pre-Wave-6 file body, however, is built on top of types and
//! helpers that the strict-typing bulldozer **already deleted before
//! substep-1** — it does not compile against the current `shape-value` crate
//! and cannot be migrated by mechanical shim rename:
//!
//! - `shape_value::ValueWord` / `shape_value::ValueWordExt`
//!   (deleted — see `crates/shape-value/src/lib.rs`'s post-bulldozer header).
//! - `shape_value::value_word_drop::vw_drop` /
//!   `shape_value::value_word_drop::vw_clone`
//!   (deleted — replaced by `clone_with_kind` / `drop_with_kind` keyed on
//!    `NativeKind`, ADR-006 §2.7.7).
//! - `ValueWord::from_raw_bits` / `ValueWord::from_*` /
//!   `ValueWord::into_raw_bits` (constructors and accessors all gone with the
//!    type itself).
//! - `as_heap_ref()` (forbidden — playbook §4 #7; replaced by
//!   `slot.as_heap_value()` on `KindedSlot::slot`).
//! - `tag_bits::*` / `is_tagged()` / the deleted W-series ValueWord
//!   synthesizer (forbidden — playbook §4 #7).
//!
//! On top of those, the `MethodHandler` ABI itself was **kind-less in
//! both directions** pre-Wave-γ. ADR-006 §2.7.9 / Q11 (Wave-γ
//! `G-method-fn-v2-abi`) flipped `MethodFnV2` to
//! `fn(&mut VM, &[KindedSlot], _) -> Result<KindedSlot, VMError>` —
//! the kinded carrier slice form per §2.7.1 case 4. The dispatch
//! shell now sources every kind from the §2.7.7 stack parallel-
//! `Vec<NativeKind>` track via `pop_kinded()` (no fabrication), and
//! pushes the returned `KindedSlot` via `push_kinded()` (kind from
//! the handler-returned carrier — no fabrication). The Bool-default
//! rationalization the W-series formalized is no longer reachable.
//! With the ABI in place this dispatch shell becomes a mechanical
//! `pop_kinded` / `push_kinded` / `slot.as_heap_value()` rewrite per
//! playbook §10 D-objects-mod row — Wave-γ-followup territory.
//!
//! Cross-cluster dependencies for the architectural close-out:
//!
//! 1. `D-raw-helpers` rewrites/deletes `objects/raw_helpers.rs` (currently
//!    the carrier for `tag_bits::*` and `extract_heap_ref`). Every Cluster D
//!    sibling file (`property_access.rs`, `array_operations.rs`,
//!    `array_joins.rs`, `concurrency_methods.rs`, `channel_methods.rs`,
//!    `number_methods.rs`, etc.) calls `extract_heap_ref(args[0])` for
//!    HeapValue dispatch — same shape needed here for the receiver bits.
//! 2. Wave-γ-followup body migration: per ADR-006 §2.7.9 / Q11 the
//!    `MethodFnV2` ABI is kinded (`&[KindedSlot]` /
//!    `Result<KindedSlot, VMError>`); ~150 PHF handler bodies stayed
//!    `NotImplemented(SURFACE)` after the ABI flip (Wave-γ
//!    `G-method-fn-v2-abi` close) and are migrated body-by-body in
//!    follow-up sub-clusters per the M-datatable Wave-β `joins.rs`
//!    precedent at close commit `eb78699`.
//! 3. The remaining `ValueWord::from_*` heap-construction sites
//!    (`ValueWord::from_heap_value(HeapValue::Range { .. })`,
//!    `ValueWord::from_type_annotated_value`, `ValueWord::from_array`, etc.)
//!    rewrite to `Arc::into_raw + push_kinded(_, NativeKind::Ptr(HeapKind::*))`
//!    per playbook §3 per-`HeapKind` push pattern.
//!
//! Per playbook §7.4 ("File compiles cleanly OR un-compiling sites have a
//! documented surface") and §8 surface-and-stop trigger ("Cross-cluster
//! migration cascade"), this file's bodies are replaced with
//! `VMError::NotImplemented(SURFACE: ...)` placeholders documenting the
//! cascade. Function signatures and module declarations are preserved so
//! external callers (`dispatch.rs`, `additional/mod.rs`, `compiler/*`)
//! continue to compile.
//!
//! ## Migration status snapshot (substep-2 close)
//!
//! - Mandatory shim hits: 0 (the 39 `push_raw_u64` / `pop_raw_u64` call sites
//!   are gone — they were inside the bodies that this commit replaces with
//!   surface markers).
//! - Sibling shim hits: 0 (none in pre-existing file; verified at audit).
//! - Forbidden-pattern carry-overs: 0 (`ValueWord`, `as_heap_ref`, `vw_drop`,
//!   `value_word_drop`, `as_vw_ref`, `tag_bits`, and the deleted ValueWord
//!   synthesizer are all gone; the `extract_heap_ref` import lived in the
//!   now-deleted bodies and is not reintroduced).
//! - Surfaces: 6 (`exec_objects` opcode dispatch + 5 method-dispatch entries:
//!   `op_call_method`, `op_make_range`, `op_wrap_type_annotation`,
//!   `dispatch_method_handler`, plus the v2 typed-array PHF fast path baked
//!   into `op_call_method`).
//!
//! See `docs/cluster-audits/phase-1b-vm-wave-6-5-playbook.md` §10 row
//! `D-objects-mod`, §7.4, §8, and ADR-006 §2.7.6 (Q8) / §2.7.7 (Q9).

// PHF method registry
pub mod method_registry;
// Raw u64 extraction helpers (v2 — no ValueWord) — D-raw-helpers territory.
pub mod raw_helpers;

// Property access operations (GetProp, SetProp, Length) — D-prop-access territory.
pub mod property_access;

// Object creation operations (NewArray, NewObject, NewTypedObject) — D-obj-create territory.
pub mod object_creation;

// Object merge operations (MergeObject, TypedMergeObject) — D-obj-tail territory.
pub mod object_operations;

// Array operations (ArrayPush, ArrayPop, SliceAccess) — D-array-ops territory.
pub mod array_operations;

// Array method modules.
pub mod array_aggregation;
pub mod array_basic;
pub mod array_joins;
pub mod array_query;
pub mod array_sets;
pub mod array_sort;
pub mod array_transform;

// DataTable method handlers.
pub mod datatable_methods;

// (W15-column, 2026-05-10) `column_methods` deleted: ADR-006 §2.7.21 / Q22.
// `Column` is not a surviving `HeapKind` variant — its semantics are
// absorbed by `HeapKind::TableView` + `TableViewData::ColumnRef` (see
// `crates/shape-value/src/heap_value.rs`). The previous file held 11
// surface-only stubs and a stale PHF map; both are removed.

// IndexedTable method handlers.
pub mod indexed_table_methods;

// HashMap method handlers.
pub mod hashmap_methods;

// Set method handlers.
pub mod deque_methods;
pub mod priority_queue_methods;
pub mod set_methods;

// Number method handlers.
pub mod number_methods;

// String method handlers.
pub mod string_methods;

// Content method handlers.
pub mod content_methods;

// DateTime method handlers.
pub mod datetime_methods;

// Instant method handlers.
pub mod instant_methods;

// Matrix method handlers.
pub mod matrix_methods;

// Iterator method handlers.
pub mod iterator_methods;

// Range method handlers (W15-range, ADR-006 §2.7.23 / Q24, 2026-05-10).
pub mod range_methods;

// Typed array (Vec<int>, Vec<number>, Vec<bool>) method handlers.
pub mod typed_array_methods;

// W16.2-J.1 (2026-05-22): the V0.c per-kind typed-array handler modules
// (`typed_int_array_methods` + `typed_number_array_methods`, 667 LoC
// combined) DELETED. The kind-generic counterparts in
// `array_aggregation::handle_{sum,avg,min,max,count,reduce}_v2` +
// `array_basic::handle_{len,is_empty,first,last,push,pop,get,set,clone}_v2`
// (registered in `ARRAY_METHODS` and gained real bodies via W16.2-J.0,
// commit `fbe86020`) cover every method the per-kind handlers used to
// host. See `method_registry.rs` for the deletion banner.

// Concurrency primitive (Mutex<T>, Atomic<T>, Lazy<T>) method handlers.
pub mod concurrency_methods;

// Channel (MPSC sender/receiver) method handlers.
pub mod channel_methods;

// Concatenation opcodes (StringConcat, ArrayConcat) — dedicated v2 replacements
// for the generic Add overload on built-in heap types.
pub mod concat;

// Typed HashMap and String access opcodes — local-slot based, skip HeapValue dispatch.
pub mod typed_access;

use crate::{
    bytecode::{Instruction, OpCode, Operand},
    executor::VirtualMachine,
};
use shape_value::{HeapKind, HeapValue, KindedSlot, NativeKind, TemporalData, ValueSlot, VMError};

/// Select the method-registry PHF lookup for a v2-raw `TypedArray<T>`
/// receiver, classified by its stamped element-type discriminant.
///
/// **W16.2-J.1 (2026-05-22):** the per-kind PHF registries
/// `TYPED_INT_ARRAY_METHODS` + `TYPED_NUMBER_ARRAY_METHODS` were deleted
/// alongside their handler module files (`typed_int_array_methods.rs` /
/// `typed_number_array_methods.rs`, 667 LoC combined). The prereq W16.2-J.0
/// (commit `fbe86020`) migrated the kind-generic counterparts in
/// `array_aggregation::handle_{sum,avg,min,max,count,reduce}_v2` +
/// `array_basic::handle_{len,is_empty,first,last,push,pop,get,set,clone}_v2`
/// from `ckpt[2-5]_surface` stubs to real bodies delegating to the
/// `v2_array_detect::{sum,avg,min,max,push,pop,read,write}_element(s)`
/// primitives — those entries live in `ARRAY_METHODS`.
///
/// Result: every numeric `V2ElemType` arm now returns `None`; the caller
/// falls back to `ARRAY_METHODS` for the kind-generic implementation. The
/// surviving non-`None` arm is `V2ElemType::Bool` → `BOOL_ARRAY_METHODS`,
/// which carries closure-callback / aggregation residuals (`count`, `any`,
/// `all`, `toArray`) tracked by the W17 typed-carrier-monomorphization
/// workstream and still routes through the bool-specific handler set.
fn typed_array_method_registry(
    elem_type: crate::executor::v2_handlers::v2_array_detect::V2ElemType,
    method_name: &str,
) -> Option<method_registry::MethodHandler> {
    use crate::executor::v2_handlers::v2_array_detect::V2ElemType;
    match elem_type {
        // Numeric element kinds — fall through to ARRAY_METHODS via the
        // caller's `.or_else(...)` chain. W16.2-J.1 deleted the per-kind
        // PHFs that previously lived here; the kind-generic
        // `array_aggregation::*` / `array_basic::*` handlers in
        // ARRAY_METHODS now cover len/length/push/pop/first/last/get/set/
        // sum/avg/mean/min/max/clone uniformly.
        V2ElemType::I64
        | V2ElemType::I32
        | V2ElemType::I8
        | V2ElemType::U8
        | V2ElemType::I16
        | V2ElemType::U16
        | V2ElemType::U32
        | V2ElemType::F64
        | V2ElemType::F32 => None,
        // Bool carries closure-callback / aggregation residuals
        // (count / any / all / toArray) — W17 typed-carrier-
        // monomorphization territory. The kind-generic len/first/last/
        // isEmpty entries in BOOL_ARRAY_METHODS still alias to
        // `array_basic::handle_*_v2`, so semantics are uniform with
        // ARRAY_METHODS for those names.
        V2ElemType::Bool => method_registry::BOOL_ARRAY_METHODS
            .get(method_name)
            .copied(),
        // Char / String / Decimal / TypedObject have no dedicated typed-
        // array method registry — fall back to generic ARRAY_METHODS.
        V2ElemType::Char
        | V2ElemType::String
        | V2ElemType::Decimal
        | V2ElemType::TypedObject => None,
    }
}

impl VirtualMachine {
    /// Dispatch shell for object opcodes.
    ///
    /// Each opcode arm currently calls into a sibling Cluster D file
    /// (`object_creation`, `property_access`, `array_operations`, etc.) whose
    /// own substep-2 migration is in flight under a peer Wave-α sub-cluster.
    /// The dispatch shell itself is kind-correct because it forwards to the
    /// per-opcode handler unchanged. The legacy entries that lived directly
    /// in `objects/mod.rs` (`op_call_method`, `op_wrap_type_annotation`,
    /// `op_make_range`) are surfaced below — see each function's doc comment
    /// for the architectural cascade ruling.
    #[inline(always)]
    pub(in crate::executor) fn exec_objects(
        &mut self,
        instruction: &Instruction,
        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
    ) -> Result<(), VMError> {
        use OpCode::*;
        match instruction.opcode {
            NewArray => self.op_new_array(instruction)?,
            NewTypedArray => self.op_new_typed_array(instruction)?,
            NewMatrix => self.op_new_matrix(instruction)?,
            NewObject => self.op_new_object(instruction)?,
            GetProp => self.op_get_prop(ctx)?,
            SetProp => self.op_set_prop()?,
            SetLocalIndex => self.op_set_local_index(instruction)?,
            SetModuleBindingIndex => self.op_set_module_binding_index(instruction)?,
            Length => self.op_length()?,
            ArrayPush => self.op_array_push()?,
            ArrayPushLocal => self.op_array_push_local(instruction)?,
            ArrayPop => self.op_array_pop()?,
            MakeClosure => self.op_make_closure(instruction)?,
            MergeObject => self.op_merge_object()?,
            NewTypedObject => self.op_new_typed_object(instruction)?,
            TypedMergeObject => self.op_typed_merge_object(instruction)?,
            WrapTypeAnnotation => self.op_wrap_type_annotation(instruction)?,
            SliceAccess => self.op_slice_access()?,
            MakeRange => self.op_make_range()?,
            _ => unreachable!(
                "exec_objects called with non-object opcode: {:?}",
                instruction.opcode
            ),
        }
        Ok(())
    }

    /// SURFACE: WrapTypeAnnotation cannot be migrated in this cluster.
    ///
    /// The pre-Wave-6 body popped a `ValueWord` and constructed a
    /// `ValueWord::from_type_annotated_value(name, inner)` wrapper. Both the
    /// `ValueWord` type and the `from_type_annotated_value` constructor were
    /// deleted by the strict-typing bulldozer before substep-1; there is no
    /// post-§2.7.7 wrapper shape. The annotation-wrap design itself needs
    /// re-thinking under ADR-006 (annotations as parallel metadata, not as a
    /// payload tag), which is outside the D-objects-mod sub-cluster's
    /// territory.
    ///
    /// Cross-cluster cascade: the compiler emitter currently produces
    /// `WrapTypeAnnotation` opcodes; that emit site is in `compiler/` and
    /// must coordinate with the kinded annotation-metadata model before this
    /// handler is rewritten.
    fn op_wrap_type_annotation(&mut self, _instruction: &Instruction) -> Result<(), VMError> {
        Err(VMError::NotImplemented(
            "SURFACE: WrapTypeAnnotation depends on the deleted ValueWord wrapper \
             type. Annotation wrapping needs a kinded redesign (ADR-006 §2.7.6 \
             / Q8) — see playbook §8 cross-cluster cascade. D-objects-mod scope \
             does not include the compiler emit site."
                .into(),
        ))
    }

    /// CallMethod dispatch shell (W16-op-call-method close).
    ///
    /// ADR-006 §2.7.10 / Q11 dispatch shell — pops the receiver +
    /// arg-count call args from the §2.7.7 kinded stack, classifies
    /// the receiver kind to pick the matching PHF method registry,
    /// dispatches through `MethodFnV2`, and pushes the kinded result.
    ///
    /// Body shape per the W7-op-call-value precedent (close commit
    /// `27812cf`, `executor/control_flow/mod.rs:dispatch_call_value_immediate`):
    ///
    /// 1. Pop `arg_count + 1` slots via `pop_kinded()` (receiver
    ///    included). Each pop transfers one share (heap-bearing kinds)
    ///    into the returned `(bits, kind)` pair (WB2.4 retain-on-read,
    ///    §2.7.7); the `KindedSlot::new` carrier takes ownership of
    ///    that share. Pop order is reverse of push order, so reverse
    ///    the vec back to position-aligned order with `args[0]` =
    ///    receiver.
    /// 2. Decode `arg_count` + method name from
    ///    `Operand::TypedMethodCall { arg_count, string_id, .. }`
    ///    (`bytecode/opcode_defs.rs:2023`). The method name string is
    ///    indexed via `string_id` into `self.program.strings`.
    /// 3. Classify `args[0].kind` to pick a PHF registry per the
    ///    §2.7.6 / Q8 heterogeneous-kind body pattern. Numeric / Bool
    ///    / String scalars route to the matching scalar registry;
    ///    `Ptr(HeapKind::*)` heap kinds route to the per-heap-kind
    ///    registry, with `HeapKind::TypedArray` sub-classified on the
    ///    inner `TypedArrayData::{I64, F64, Bool, ...}` variant via
    ///    `slot.as_heap_value()` and `HeapKind::Temporal`
    ///    sub-classified on the inner `TemporalData::{DateTime,
    ///    TimeSpan, ...}` variant. The v2 typed-array fast path
    ///    (`UInt64`-tagged raw `*mut TypedArray<T>` pointer) routes
    ///    through `as_v2_typed_array`; post-W16.2-J.1 every numeric
    ///    element kind falls through to the kind-generic
    ///    `ARRAY_METHODS` PHF (per the `typed_array_method_registry`
    ///    helper, which returns `None` for `V2ElemType::{I*, U*, F*}`).
    ///    `V2ElemType::Bool` continues to route via
    ///    `BOOL_ARRAY_METHODS`.
    /// 4. PHF lookup keyed on `&str` method name returns the
    ///    `MethodFnV2` handler. A miss surfaces a `RuntimeError`
    ///    citing the receiver kind + method name; user-defined
    ///    methods on `HeapValue::TypedObject` fall through to a UFCS
    ///    function-name lookup (`function_name_index`) before the
    ///    final `Unknown method` error. Closure / Future / Reference
    ///    / SharedCell / FilterExpr receivers reject — they are not
    ///    method-call targets.
    /// 5. Dispatch: `handler(self, &args, ctx)` returns
    ///    `Result<KindedSlot, VMError>`. The `&[KindedSlot]` borrow
    ///    leaves the shares with the carriers in this stack frame —
    ///    handlers borrow each entry per §2.7.10 / Q11 borrow-only
    ///    ABI.
    /// 6. Push the result via `push_kinded(result.raw(), result.kind())`
    ///    and `std::mem::forget(result)` so the result share transfers
    ///    cleanly to the stack (no double-drop). The `args` carriers
    ///    drop at end of scope; `KindedSlot::Drop` dispatches on kind
    ///    and releases each share via `drop_with_kind` (no bare
    ///    `vw_drop`, no Bool-default fallback).
    ///
    /// Forbidden surfaces (per CLAUDE.md "Renames to refuse on sight"
    /// + ADR-006 §2.7.10 / Q11): `Vec<KindedSlot>` by-move into a
    /// dispatch helper; `args: &mut [KindedSlot]`; tag-bits decode on
    /// receiver bits; `is_heap()` probe on raw bits; Bool-default
    /// fallback for unknown kind; defection-attractor framing on
    /// the method-dispatch ABI (`MethodFn` / `MethodFnLegacy` /
    /// `dispatch_method_handler_raw` / `call_handler_with_u64_slice`).
    ///
    /// Surfaces remaining (out of W16 territory):
    /// - **IC fast-path recording / hit**: `method_ic_check` /
    ///   `method_ic_record` already accept the kinded `MethodFnV2`
    ///   transmute (`ic_fast_paths.rs:42-44`) — wiring the IC
    ///   recording at the dispatch shell is a downstream JIT-IC
    ///   follow-up, not a correctness gate. The dispatch shell stays
    ///   correct without IC; the IC adds speed only.
    /// - **`HeapKind::Closure` receivers** (e.g. closure-as-trait-
    ///   object dispatch). Trait-object dispatch goes through
    ///   `op_dyn_method_call`, not `op_call_method`; the closure arm
    ///   here rejects with a clear error.
    pub fn op_call_method(
        &mut self,
        instruction: &Instruction,
        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
    ) -> Result<(), VMError> {
        // ADR-006 §2.7.10 / Q11: arg_count + method name from operand
        // (typed dispatch is the only emit shape per
        // `compiler/expressions/function_calls.rs:2014` / `binary_ops.rs`
        // / `unary_ops.rs`). Legacy stack-arg-count dispatch is gone.
        let (arg_count, string_id, _method_id, _receiver_type_tag) = match instruction.operand {
            Some(Operand::TypedMethodCall {
                method_id,
                arg_count,
                string_id,
                receiver_type_tag,
            }) => (
                arg_count as usize,
                string_id as usize,
                method_id,
                receiver_type_tag,
            ),
            _ => return Err(VMError::InvalidOperand),
        };

        // ADR-006 §2.7.24 Q25.C: when the receiver is a trait object,
        // route through the DynMethodCall dispatch shell instead of the
        // standard CallMethod path. This handles the case where the
        // compiler couldn't determine at compile-time that the receiver
        // is a `dyn T` (e.g. `let b = a.clone_me()` where `clone_me`
        // returns `Self` through a `BoxedReturn` thunk — the result is
        // a trait object but the compiler emits the standard CallMethod
        // opcode without a `dyn_locals` entry for `b`). Round-2: this
        // fallback ensures correctness; a future amendment can teach
        // type-inference to propagate `dyn T` through method-call
        // result types and emit `DynMethodCall` at the compile site.
        if self.sp >= arg_count + 1 {
            let receiver_idx_check = self.sp - arg_count - 1;
            let (_, receiver_kind_peek) = self.stack_read_kinded_raw(receiver_idx_check);
            if receiver_kind_peek
                == NativeKind::Ptr(shape_value::HeapKind::TraitObject)
            {
                // Reconstruct the instruction with `arg_count` /
                // `string_id` operands and call into the dyn dispatch
                // path. The TypedMethodCall operand layout matches
                // exactly what `op_dyn_method_call` expects.
                return self.exec_trait_object_ops(
                    &Instruction::new(
                        crate::bytecode::OpCode::DynMethodCall,
                        Some(Operand::TypedMethodCall {
                            method_id: _method_id,
                            arg_count: arg_count as u16,
                            string_id: string_id as u16,
                            receiver_type_tag: _receiver_type_tag,
                        }),
                    ),
                    ctx,
                );
            }
        }

        // Pop receiver + arg_count call args. Each pop_kinded transfers
        // one share into the returned (bits, kind); the KindedSlot
        // carrier takes ownership and releases via drop_with_kind on
        // scope exit. ADR-006 §2.7.7 WB2.4 retain-on-read.
        let total = arg_count + 1;
        let mut args: Vec<KindedSlot> = Vec::with_capacity(total);
        for _ in 0..total {
            let (bits, kind) = self.pop_kinded()?;
            args.push(KindedSlot::new(ValueSlot::from_raw(bits), kind));
        }
        // Pop is reverse of push order; flip so args[0] is the receiver.
        args.reverse();

        // Resolve method name. The string pool index was offset-fixed
        // at link time (`executor/mod.rs:883`), so direct indexing is
        // always in-range for a well-formed program. We clone into an
        // owned `String` to release the immutable borrow on
        // `self.program.strings` before the `dispatch_method_kinded`
        // call below takes a mutable borrow on `self`.
        let method_name: String = self
            .program
            .strings
            .get(string_id)
            .cloned()
            .ok_or_else(|| {
                VMError::RuntimeError(format!(
                    "op_call_method: string_id {} out of bounds (pool size {})",
                    string_id,
                    self.program.strings.len()
                ))
            })?;

        // Classify the receiver, resolve the handler, and dispatch via
        // the shared `dispatch_method_kinded` entry — borrow-only ABI per
        // §2.7.10 / Q11. The handler borrows each KindedSlot; share
        // ownership stays with the carriers in `args`.
        let result = self.dispatch_method_kinded(&args, &method_name, ctx)?;

        // Transfer the result share onto the kinded stack. The result
        // carrier is forgotten so its Drop does not double-release.
        self.push_kinded(result.raw(), result.kind())?;
        std::mem::forget(result);

        // `args` carriers drop here. `KindedSlot::Drop` dispatches on
        // each entry's kind and retires its share via the matching
        // `Arc::decrement_strong_count::<T>` arm — no bare vw_drop
        // (forbidden), no Bool-default fallback (forbidden §2.7.7 #9).
        Ok(())
    }

    /// Shared method-dispatch entry: resolve the handler via
    /// [`resolve_method_handler`](Self::resolve_method_handler) and call
    /// it with the kinded carrier slice.
    ///
    /// Two callers consume this entry:
    ///
    /// 1. `op_call_method` (above) — VM-side dispatch shell after popping
    ///    the receiver + args from the §2.7.7 stack parallel-kind track.
    /// 2. `jit_trampoline_call_method` (in
    ///    `crates/shape-vm/src/executor/call_convention.rs`) — the
    ///    §2.7.5 cross-crate stable-FFI consumer that converts the JIT's
    ///    pair-slice form into `&[KindedSlot]` carriers and delegates
    ///    here for the actual dispatch.
    ///
    /// `args[0]` is the receiver, `args[1..]` are the call args. Every
    /// entry's `kind` came from the §2.7.7 parallel-kind track at the
    /// producing site — no fabrication. The handler borrows each
    /// `KindedSlot` (§2.7.10 / Q11 borrow-only ABI); share ownership
    /// stays with the carriers at the caller. The returned `KindedSlot`
    /// owns its result share — the caller pushes it onto the stack or
    /// transfers it across the FFI boundary, then `mem::forget`s the
    /// returned carrier to balance refcounts.
    pub(crate) fn dispatch_method_kinded(
        &mut self,
        args: &[KindedSlot],
        method_name: &str,
        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
    ) -> Result<KindedSlot, VMError> {
        // Phase 4 (trait Add/AddAssign for user types, 2026-05-16):
        // Before falling into the PHF-based handler resolution, give
        // user-defined methods (`impl Trait for X { method m(...) }` and
        // `impl X { method m(...) }`) a chance to dispatch via UFCS on
        // the receiver's concrete type name. The compiler registers each
        // such method under the function name `"{TypeName}::{method}"`
        // (see `compiler/statements.rs::desugar_impl_method`); we look
        // that name up in `function_name_index` and, if found, call the
        // function directly. This makes `a + b` work for `impl Add for
        // Money` (binary_ops.rs emits `CallMethod("add")` after the
        // operator-trait check fires), and likewise for any other user-
        // authored method on a TypedObject.
        //
        // The PHF-based fallback below still handles built-in methods on
        // TypedObject receivers (the `DATATABLE_METHODS` PHF covers the
        // generic table-shaped methods) — UFCS takes precedence so users
        // can shadow / extend the built-in surface with their own impls.
        //
        // We resolve the candidate function_id WITHOUT consuming `ctx`
        // first, so we can re-thread `ctx` into the PHF handler when
        // UFCS declines. The call path takes `ctx` only after the
        // function_id resolves.
        if let NativeKind::Ptr(HeapKind::TypedObject) = args[0].kind {
            if let Some(function_id) = self.resolve_typed_object_ufcs(args, method_name) {
                return self.invoke_typed_object_ufcs(args, function_id, ctx);
            }
        }
        let handler = self.resolve_method_handler(args, method_name)?;
        handler(self, args, ctx)
    }

    /// Resolve a `TypedObject`-receiver method name to a UFCS function id
    /// (Phase 4 trait Add/AddAssign work, 2026-05-16).
    ///
    /// Reads the receiver's `schema_id` (which the v2-raw
    /// `TypedObjectStorage` exposes at field offset, per
    /// `heap_value.rs:3497`), looks up the concrete type name in
    /// `program.type_schema_registry`, and checks
    /// `function_name_index["{TypeName}::{method}"]`. Returns the
    /// post-link function id if registered, `None` otherwise.
    ///
    /// `compiler/statements.rs::desugar_impl_method` is the producer that
    /// registers `impl Add for Money { method add(other) ... }` as the
    /// function `Money::add` in `function_name_index`.
    ///
    /// Caller invariant: `args[0].kind == NativeKind::Ptr(HeapKind::TypedObject)`.
    /// SAFETY: dereferences `args[0].slot.raw()` as `*const TypedObjectStorage`
    /// per §2.3 typed-Arc invariant + Wave 2 Round 4 D4 ckpt-3 v2-raw
    /// migration; the borrowed `KindedSlot` in `args[0]` owns one share
    /// so the pointee stays live for this scope.
    fn resolve_typed_object_ufcs(
        &self,
        args: &[KindedSlot],
        method_name: &str,
    ) -> Option<u16> {
        let receiver_bits = args[0].slot.raw();
        if receiver_bits == 0 {
            return None;
        }
        // SAFETY: per the caller's invariant the receiver is a
        // `Ptr(HeapKind::TypedObject)` slot. Slot bits are
        // `*const TypedObjectStorage` (v2-raw migration per
        // `heap_value.rs:3497`); the borrowed `KindedSlot` carrier in
        // `args[0]` owns one share so the pointee stays live for this
        // scope. Transient borrow — no Arc reconstruction.
        let schema_id = unsafe {
            (*(receiver_bits as *const shape_value::TypedObjectStorage)).schema_id
        };
        let concrete_type_name = self
            .program
            .type_schema_registry
            .get_by_id(schema_id as u32)
            .map(|schema| schema.name.clone())?;
        let function_name = format!("{}::{}", concrete_type_name, method_name);
        self.function_name_index.get(&function_name).copied()
    }

    /// Invoke a UFCS-resolved Shape function on a TypedObject receiver +
    /// args (Phase 4 trait Add/AddAssign work, 2026-05-16).
    ///
    /// Pushes receiver + args back onto the kinded stack (cloning shares
    /// since the borrowed `args` carriers retain ownership of the
    /// originals — the caller's `KindedSlot::Drop` will release those),
    /// then sets up a fresh call frame via `call_function_with_nb_args`
    /// + `execute_until_call_depth`, pops the function's return value
    /// from the kinded stack, and returns it as a `KindedSlot` whose
    /// carrier owns the result share.
    ///
    /// Mirrors `trait_object_ops.rs::invoke_dyn_unified` for the
    /// non-Self-arg, non-BoxedReturn case (the typical user-defined
    /// `impl Add for X { method add(other: X) -> X }` shape).
    fn invoke_typed_object_ufcs(
        &mut self,
        args: &[KindedSlot],
        function_id: u16,
        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
    ) -> Result<KindedSlot, VMError> {
        // Phase 4 fix (2026-05-16): route through the canonical
        // `execute_function_by_id` public entry-point — the same pattern
        // `execute_function_with_named_args` uses for borrowed-args call
        // sites (`call_convention.rs:211-256`). The earlier hand-rolled
        // `call_function_with_nb_args + execute_until_call_depth` path
        // had a non-deterministic double-free that surfaced on `+=`
        // desugar fixtures (`m = m + Money{...}`); bisect attributed it
        // to subtle interactions between the manual `self.sp =
        // base_pointer` adjustment and downstream frame setup. Routing
        // through the established public entry-point eliminates the
        // surface — that helper is the §2.7.10/Q11 canonical shape for
        // "borrowed args, owned-share-per-call invocation".
        //
        // Build an owned `Vec<KindedSlot>` for the new frame by bumping
        // one share per arg via `clone_with_kind` (§2.7.7 WB2.4) — the
        // borrowed `args` slice's carriers retain ownership of the
        // originals (op_call_method's `args` carriers drop those at end
        // of scope), so we mint independent shares for the called
        // function's locals. `execute_function_by_id` then runs the
        // standard call protocol: `call_function_with_nb_args` transfers
        // shares into the new frame, `mem::forget(args)` balances, the
        // function runs to completion, the return value is popped and
        // returned as a `KindedSlot` whose carrier owns the result share.
        let mut call_args: Vec<KindedSlot> = Vec::with_capacity(args.len());
        for slot in args.iter() {
            let bits = slot.slot.raw();
            let kind = slot.kind;
            crate::executor::vm_impl::stack::clone_with_kind(bits, kind);
            call_args.push(KindedSlot::new(ValueSlot::from_raw(bits), kind));
        }
        self.execute_function_by_id(function_id, call_args, ctx)
    }

    /// Resolve a method handler from `(receiver_kind, method_name)`.
    ///
    /// Receiver classification per ADR-006 §2.7.6 / Q8 heterogeneous-
    /// kind body pattern: scalar kinds map directly to scalar PHF
    /// registries; `Ptr(HeapKind::*)` heap kinds map to the matching
    /// per-heap-kind registry, with `TypedArray` and `Temporal`
    /// sub-classified through `slot.as_heap_value()` matching to pick
    /// the element-typed sub-registry. The `UInt64`-tagged v2 typed-
    /// array fast path (`*mut TypedArray<T>` pointer with stamped
    /// element-type byte) routes through `v2_array_detect`.
    ///
    /// Returns `Err(RuntimeError)` for unknown method on a known
    /// receiver kind, or unsupported receiver kind. Falls through to
    /// `function_name_index` UFCS for `HeapKind::TypedObject`
    /// receivers when the method is not in `DATATABLE_METHODS` (the
    /// dispatch table covering generic table-shaped methods is the
    /// closest fit; user-defined methods land via UFCS).
    fn resolve_method_handler(
        &self,
        args: &[KindedSlot],
        method_name: &str,
    ) -> Result<method_registry::MethodHandler, VMError> {
        use crate::executor::v2_handlers::v2_array_detect::as_v2_typed_array;

        let receiver = &args[0];
        let kind = receiver.kind;

        // Pure-scalar receivers — kind alone selects the registry.
        let scalar_handler: Option<method_registry::MethodHandler> = match kind {
            NativeKind::Float64
            | NativeKind::NullableFloat64
            | NativeKind::Int8
            | NativeKind::NullableInt8
            | NativeKind::UInt8
            | NativeKind::NullableUInt8
            | NativeKind::Int16
            | NativeKind::NullableInt16
            | NativeKind::UInt16
            | NativeKind::NullableUInt16
            | NativeKind::Int32
            | NativeKind::NullableInt32
            | NativeKind::UInt32
            | NativeKind::NullableUInt32
            | NativeKind::Int64
            | NativeKind::NullableInt64
            | NativeKind::NullableUInt64
            | NativeKind::IntSize
            | NativeKind::NullableIntSize
            | NativeKind::UIntSize
            | NativeKind::NullableUIntSize => method_registry::NUMBER_METHODS.get(method_name).copied(),
            NativeKind::Bool => method_registry::BOOL_METHODS.get(method_name).copied(),
            NativeKind::String => method_registry::STRING_METHODS.get(method_name).copied(),
            // Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14):
            // ADR-006 §2.7.5 amendment adds F32 + Char as scalar variants.
            // F32 receivers route to NUMBER_METHODS (same numeric method
            // surface as F64). Char receivers route to CHAR_METHODS — the
            // existing receiver registry already covers char methods
            // (`.to_uppercase()`, `.is_alphabetic()`, etc.) and was wired
            // for the `NativeKind::Ptr(HeapKind::Char)` carrier; the same
            // method surface applies regardless of which Char carrier
            // label flows through (both labels store the same codepoint
            // bits and method bodies read via `as_char` which recognizes
            // both labels per the §2.7.5 amendment).
            NativeKind::Float32 => method_registry::NUMBER_METHODS.get(method_name).copied(),
            NativeKind::Char => method_registry::CHAR_METHODS.get(method_name).copied(),
            // Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions
            // (2026-05-14): the v2-raw `*const StringObj` / `*const DecimalObj`
            // carrier receivers route to the same method registry as their
            // Arc-wrapped siblings — the method-handler bodies dispatch on
            // the carrier shape (the slot's kind label drives the per-
            // carrier read of UTF-8 bytes / Decimal value). Method-handler
            // body migration for v2-raw reads is the Agent A2 (producer)
            // / consumer-side cluster-1 hardening territory; this row pins
            // method-registry selection at the dispatch shell.
            NativeKind::StringV2 => method_registry::STRING_METHODS.get(method_name).copied(),
            // DecimalV2 routes to NUMBER_METHODS — same as the Arc-wrapped
            // `HeapKind::Decimal` sibling per the heap-arm row below.
            NativeKind::DecimalV2 => method_registry::NUMBER_METHODS.get(method_name).copied(),
            // r5c-2-β-CKPT-C u64-carrier-disambiguation (2026-05-20):
            // `Ptr(HeapKind::TypedArray)` is the single canonical carrier
            // kind for every v2-raw `*mut TypedArray<T>` pointer (direct
            // `NewTypedArray*` allocation + refcounted struct-field /
            // closure-capture read). Classify via the stamped element-type
            // byte → typed-array method registry. A genuine scalar `u64`
            // (`NativeKind::UInt64`) routes purely to `NUMBER_METHODS` and
            // is NEVER passed to `as_v2_typed_array` — the pre-fix shared
            // arm dereferenced an arbitrary scalar `u64` value as a header
            // pointer → SIGSEGV.
            NativeKind::Ptr(HeapKind::TypedArray) => {
                let bits = receiver.slot.raw();
                if let Some(view) = as_v2_typed_array(bits, kind) {
                    typed_array_method_registry(view.elem_type, method_name)
                        .or_else(|| method_registry::ARRAY_METHODS.get(method_name).copied())
                } else {
                    // Kind says TypedArray but the bits failed v2 detection —
                    // still an array receiver; fall back to generic methods.
                    method_registry::ARRAY_METHODS.get(method_name).copied()
                }
            }
            NativeKind::UInt64 => {
                // Genuine scalar `u64` — numeric method surface only.
                method_registry::NUMBER_METHODS.get(method_name).copied()
            }
            NativeKind::Ptr(_) => None,
            // R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 +
            // §2.7.7/Q9, 2026-05-19): `NativeKind::Null` receivers have
            // no method dispatch surface — null has no methods.
            NativeKind::Null => None,
        };
        if let Some(h) = scalar_handler {
            return Ok(h);
        }

        // Heap receivers — dispatch on HeapKind, then sub-classify
        // TypedArray / Temporal via `slot.as_heap_value()`.
        if let NativeKind::Ptr(hk) = kind {
            let heap_handler: Option<method_registry::MethodHandler> = match hk {
                HeapKind::String => method_registry::STRING_METHODS.get(method_name).copied(),
                HeapKind::Char => method_registry::CHAR_METHODS.get(method_name).copied(),
                HeapKind::HashMap => method_registry::HASHMAP_METHODS.get(method_name).copied(),
                HeapKind::HashSet => method_registry::SET_METHODS.get(method_name).copied(),
                HeapKind::DataTable => method_registry::DATATABLE_METHODS
                    .get(method_name)
                    .copied(),
                HeapKind::Iterator => method_registry::ITERATOR_METHODS.get(method_name).copied(),
                HeapKind::Instant => method_registry::INSTANT_METHODS.get(method_name).copied(),
                HeapKind::Content => method_registry::CONTENT_METHODS.get(method_name).copied(),
                HeapKind::Decimal => method_registry::NUMBER_METHODS.get(method_name).copied(),
                HeapKind::BigInt => method_registry::NUMBER_METHODS.get(method_name).copied(),
                HeapKind::TypedArray => {
                    // V3-S5 ckpt-5: TypedArrayData enum + outer
                    // HeapValue::TypedArray arm DELETED at ckpt-1..ckpt-4.
                    // Sub-classification by inner variant is gone; fall
                    // through to the generic ARRAY_METHODS PHF. Per-element-
                    // kind dispatch lands at ckpt-6 STRICT close via the
                    // v2-raw `TypedArray<T>` direct-access target (caller
                    // classifies element type from the v2 header's
                    // element-type byte instead of the deleted variant).
                    method_registry::ARRAY_METHODS.get(method_name).copied()
                }
                // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13):
                // Matrix is a first-class HeapKind — receivers route
                // directly to `MATRIX_METHODS` (no inner-TypedArrayData
                // sub-classification two-step). MatrixSlice receivers
                // route to `FLOAT_ARRAY_METHODS` (their methods are
                // numeric-aggregations over a flat f64 region; the same
                // PHF that handles `F64`-typed arrays applies).
                HeapKind::Matrix => method_registry::MATRIX_METHODS.get(method_name).copied(),
                HeapKind::MatrixSlice => method_registry::FLOAT_ARRAY_METHODS
                    .get(method_name)
                    .copied(),
                HeapKind::Temporal => {
                    // C1-temporal-lowering (Phase 2d Wave 2): Temporal
                    // slots are `Arc::into_raw::<TemporalData>` — NOT a
                    // `Box<HeapValue>` allocation. `as_heap_value()` would
                    // be wrong-type recovery (5-arm receiver-recovery
                    // soundness rule, CLAUDE.md / handover §0). Sub-
                    // classify by directly borrowing `&TemporalData` from
                    // the slot's Arc-raw pointer, mirroring
                    // `objects/datetime_methods.rs::recv_temporal`.
                    //
                    // SAFETY: when receiver.kind == Ptr(HeapKind::Temporal),
                    // receiver.slot.raw() is `Arc::into_raw::<TemporalData>`
                    // (set by `op_push_const::Constant::Duration` /
                    // `Constant::DateTimeExpr` arms, by
                    // `temporal_result()` in datetime_methods.rs, and by
                    // the §2.7.7 stack parallel-kind track). The carrier
                    // owns one strong-count share for the dispatch
                    // duration; the &TemporalData borrow's lifetime is
                    // bounded by `args[0]`'s share ownership.
                    let bits = receiver.slot.raw();
                    if bits == 0 {
                        None
                    } else {
                        let td: &TemporalData =
                            unsafe { &*(bits as *const TemporalData) };
                        match td {
                            TemporalData::DateTime(_) => {
                                method_registry::DATETIME_METHODS
                                    .get(method_name)
                                    .copied()
                            }
                            TemporalData::TimeSpan(_) | TemporalData::Duration(_) => {
                                method_registry::TIMESPAN_METHODS
                                    .get(method_name)
                                    .copied()
                            }
                            // Timeframe / TimeReference / DateTimeExpr /
                            // DataDateTimeRef have no method PHF — they
                            // are language-level metadata, not method-
                            // call targets. Fall through to
                            // UnknownMethod.
                            _ => None,
                        }
                    }
                }
                HeapKind::TypedObject => {
                    // User-defined object methods land here. The
                    // built-in DataTable PHF covers shared table-shape
                    // methods; UFCS resolution below catches user-
                    // defined `fn TypeName.method(self, ...)` shapes.
                    method_registry::DATATABLE_METHODS
                        .get(method_name)
                        .copied()
                }
                HeapKind::TableView => method_registry::DATATABLE_METHODS
                    .get(method_name)
                    .copied(),
                // Wave 15 W15-deque / W15-channel / W15-priority-queue
                // closes (ADR-006 §2.7.19/Q20, §2.7.20/Q21, §2.7.18/Q19)
                // — the new HeapKind ordinals 23/24/25 with their
                // `*_METHODS` registries.
                HeapKind::Deque => method_registry::DEQUE_METHODS.get(method_name).copied(),
                HeapKind::Channel => method_registry::CHANNEL_METHODS.get(method_name).copied(),
                HeapKind::PriorityQueue => method_registry::PRIORITY_QUEUE_METHODS
                    .get(method_name)
                    .copied(),
                // W17-concurrency (ADR-006 §2.7.25, 2026-05-11): the
                // new HeapKind ordinals 30/31/32 with their
                // MUTEX_METHODS / ATOMIC_METHODS / LAZY_METHODS
                // registries. Method-receiver classification routes
                // `m.lock()` / `a.fetch_add(...)` / `l.get()` here.
                HeapKind::Mutex => method_registry::MUTEX_METHODS.get(method_name).copied(),
                HeapKind::Atomic => method_registry::ATOMIC_METHODS.get(method_name).copied(),
                HeapKind::Lazy => method_registry::LAZY_METHODS.get(method_name).copied(),
                // W15-range close (ADR-006 §2.7.23/Q24): Range receivers
                // route to the RANGE_METHODS PHF.
                HeapKind::Range => method_registry::RANGE_METHODS.get(method_name).copied(),
                // W14-variant-codegen close (ADR-006 §2.7.17/Q18):
                // Result/Option are typed-Arc carriers; method-call
                // dispatch goes through op_is_ok / op_unwrap_ok / etc.
                // typed opcodes, not through the generic method PHF.
                // No method-PHF arm; falls through to UFCS / unknown.
                HeapKind::Result | HeapKind::Option => None,
                // ADR-006 §2.7.10 explicitly excludes the closure /
                // future / reference / shared-cell / filter-expr
                // discriminators from method-call dispatch — these are
                // not user-callable receivers. Trait-object method
                // calls go through `op_dyn_method_call`, not here —
                // the compiler-emission tier (W17-trait-object-emission)
                // emits `DynMethodCall` opcodes that walk the receiver's
                // `Arc<TraitObjectStorage>::vtable` directly per
                // ADR-006 §2.7.24 / Q25.C.5 `VTableEntry` shape, NOT
                // through this generic method PHF.
                HeapKind::Closure
                | HeapKind::Future
                | HeapKind::Reference
                | HeapKind::SharedCell
                | HeapKind::FilterExpr
                | HeapKind::TraitObject
                | HeapKind::IoHandle
                | HeapKind::TaskGroup
                | HeapKind::NativeView
                | HeapKind::NativeScalar
                // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
                // ModuleFn references are not user-callable receivers
                // via method-call dispatch — they route through
                // op_call_value's `Ptr(HeapKind::ModuleFn)` arm directly
                // (`invoke_module_fn_id_stub`), not through this generic
                // PHF lookup.
                | HeapKind::ModuleFn => None,
            };
            if let Some(h) = heap_handler {
                return Ok(h);
            }
        }

        // UFCS / unknown — surface the receiver kind in the error so
        // call sites can diagnose. Per playbook §3 "surface-and-stop
        // if PHF lookup API doesn't quite match", an unknown method
        // is *not* a SURFACE — it's a real runtime error the program
        // can hit, so we return `RuntimeError`, not `NotImplemented`.
        Err(VMError::RuntimeError(format!(
            "no method '{}' on receiver kind {:?}",
            method_name, kind
        )))
    }

    /// `MakeRange` opcode body — pop (start, end, inclusive) from the §2.7.7
    /// kinded stack and push a fresh `Arc<RangeData>` slot with kind
    /// `NativeKind::Ptr(HeapKind::Range)` (W15-range, ADR-006 §2.7.23 / Q24).
    ///
    /// Stack layout at entry (from `compiler/expressions/misc.rs:369`):
    ///
    /// ```ignore
    /// [.., start_value, end_value, PushConst<Bool>(inclusive), MakeRange]
    /// ```
    ///
    /// Popping order is reverse-push: `inclusive` first, then `end`, then
    /// `start`. Per the surface syntax, `start_value` and `end_value` are
    /// `int`-typed expressions (`0..10`); the `PushNull` placeholder for
    /// open ranges (`..n` / `n..`) reaches this handler with kind
    /// `NativeKind::Bool` and bits zero (the `PushNull` shape) — open
    /// ranges are surfaced as a SURFACE error pending the iterator-tier
    /// semantic (`for i in 0..` infinite loops are their own ADR
    /// follow-up; matches the pre-strict-typing surface).
    ///
    /// Other-kind bounds (Decimal, BigInt, NativeScalar) similarly
    /// surface — the post-strict-typing `RangeData { start: i64, end: i64,
    /// .. }` shape only models i64 ranges at landing. Cross-kind range
    /// bounds are tracked as a follow-up §2.7.23 amendment (mirror of the
    /// W14 Result/Option payload-cardinality discussion).
    pub(in crate::executor) fn op_make_range(&mut self) -> Result<(), VMError> {
        use shape_value::{KindedSlot, NativeKind, ValueSlot, heap_value::RangeData};

        // Pop in reverse-push order: inclusive flag first, then end, then start.
        // We immediately wrap each pop result in a `KindedSlot` carrier so its
        // `Drop` impl handles refcount release on every error path automatically
        // — no manual `drop_with_kind` bookkeeping needed.
        let incl_kinded = {
            let (bits, kind) = self.pop_kinded()?;
            KindedSlot::new(ValueSlot::from_raw(bits), kind)
        };
        let end_kinded = {
            let (bits, kind) = self.pop_kinded()?;
            KindedSlot::new(ValueSlot::from_raw(bits), kind)
        };
        let start_kinded = {
            let (bits, kind) = self.pop_kinded()?;
            KindedSlot::new(ValueSlot::from_raw(bits), kind)
        };

        // The `inclusive` operand is a `PushConst<Bool>` per
        // `compiler/expressions/misc.rs:362-368`. Kind must be Bool —
        // any other kind is a kind-source bug at the emit site.
        let inclusive = match incl_kinded.kind() {
            NativeKind::Bool => incl_kinded.slot().as_bool(),
            _ => {
                return Err(VMError::RuntimeError(
                    "MakeRange: inclusive flag operand must be Bool (kind-source bug \
                     at compile site — `compiler/expressions/misc.rs` emits a \
                     `PushConst<Bool>` for the inclusive flag)".into(),
                ));
            }
        };

        // Bounds: only i64 supported at landing (ADR-006 §2.7.23). Other
        // kinds — Float64 (`0.0..1.0` would-be syntax), Decimal, BigInt,
        // NativeScalar — surface for the cross-kind Range payload
        // follow-up. Bool with zero bits IS the `PushNull` open-range
        // placeholder (`..n` / `n..` / `..`) emitted by the compiler;
        // surface that distinctly so the diagnostic is precise.
        let to_i64 = |k: &KindedSlot, side: &str| -> Result<i64, VMError> {
            match k.kind() {
                NativeKind::Int64 => Ok(k.slot().as_i64()),
                NativeKind::Bool if k.slot().raw() == 0 => Err(VMError::NotImplemented(format!(
                    "MakeRange: open-range bound on {side} side (PushNull placeholder) — \
                     SURFACE: open ranges (`..n` / `n..` / `..`) need the iterator-tier \
                     infinite-iter semantic per ADR-006 §2.7.23 follow-up. Closed ranges \
                     (`start..end` / `start..=end`) work today.",
                ))),
                other => Err(VMError::NotImplemented(format!(
                    "MakeRange: cross-kind bound on {side} side (got {other:?}) — \
                     SURFACE: post-strict-typing RangeData only models i64 ranges at \
                     landing. Cross-kind bounds (Decimal, BigInt, Float64, NativeScalar) \
                     tracked as ADR-006 §2.7.23 follow-up.",
                ))),
            }
        };

        let start = to_i64(&start_kinded, "start")?;
        let end = to_i64(&end_kinded, "end")?;

        let range = std::sync::Arc::new(RangeData::new(start, end, 1, inclusive));
        self.push_kinded_slot(KindedSlot::from_range(range))?;
        Ok(())
    }
}

// ═════════════════════════════════════════════════════════════════════════════
// Tests removed during D-objects-mod surface.
// ═════════════════════════════════════════════════════════════════════════════
//
// The pre-Wave-6 `v2a_dispatch_tests` module exercised the v2 typed-array PHF
// dispatch through `op_call_method` and used `ValueWord::from_native_ptr` /
// `ValueWord::from_array` / `ValueWord::from_i64` for receiver construction.
// All four constructors are deleted with the type. The tests' canonical
// shape (PHF resolution + handler invocation) is independent of the
// dispatch shell and fits naturally in `method_registry.rs`'s own test
// module once the handler ABI migrates; they are not required to live here.
// Re-instated in the post-cascade rewrite under cluster
// `E-builtins-backlog` / `D-v2-array-detect`.