Skip to main content

shape_vm/executor/
call_convention.rs

1//! Function and closure call convention, execution wrappers, and async resolution.
2//!
3//! # Wave 7 — value-call ABI rebuild (foundation sub-cluster: W7-frame-setup)
4//!
5//! ADR-006 §2.7.11 / Q12 lifts the parallel-kind invariant of §2.7.7 (stack)
6//! and §2.7.8 (cells) across the call-frame boundary: every dispatch
7//! entry-point in this module carries kinds on `KindedSlot` carriers
8//! (callee + args + return). The W7 playbook
9//! (`docs/cluster-audits/wave-7-cc1-playbook.md`) carves the migration
10//! into 6 sub-clusters — see playbook §3 / §5 for the ordering.
11//!
12//! W7-frame-setup (this sub-cluster, Round 1) owns the three internal
13//! frame-setup helpers:
14//!
15//! 1. [`call_function_with_nb_args`] — non-closure frame setup from a
16//!    `&[KindedSlot]` arg slice. Each arg flows into the new frame's
17//!    locals via `stack_write_kinded` per playbook 6.5 §3 (caller owns
18//!    shares; the dispatch shell `mem::forget`s its arg vec after this
19//!    function returns to transfer the share).
20//! 2. [`call_closure_with_nb_args_keepalive`] — closure frame setup,
21//!    threading capture kinds via `OwnedClosureBlock::read_capture_kinded`
22//!    (§2.7.8 / Q10) and the B9 lockstep companion fields
23//!    `closure_heap_bits` + `closure_heap_kind` on `CallFrame`. The
24//!    pre-§2.7.8 `_upvalue_bits: Vec<u64>` parameter is replaced by
25//!    `closure_block: &OwnedClosureBlock` — capture data flows from the
26//!    cell-storage parallel-kind track, not from a side-channel
27//!    raw-bits payload.
28//! 3. [`call_function_from_stack`] — fast-path frame setup where the
29//!    args are already on the value stack from the producing
30//!    `Push…`/`LoadLocal…` opcodes. Pops `arg_count` slots via
31//!    `pop_kinded` and writes each into its new local slot via
32//!    `stack_write_kinded`. Sentinel-fills omitted-arg locals with
33//!    `(0u64, NativeKind::Bool)` per playbook 6.5 §2 Null/Unit row.
34//!
35//! The remaining entry-points in this module — `execute_function_by_name`
36//! / `_by_id` / `execute_closure` / `execute_function_fast` /
37//! `execute_function_with_named_args` / `resume` / `execute_with_async` /
38//! `resolve_spawned_task` / `call_value_immediate_nb` /
39//! `jit_trampoline_call_closure` — stay `todo!()` until their respective
40//! sub-clusters (W7-cv-static, W7-cv-async, W7-cv-method, W7-op-call-value)
41//! land in Rounds 2 / 3.
42//!
43//! # `_raw` pair-slice family — deleted (W7-cv-polymorphic, Round 3)
44//!
45//! `call_value_immediate_raw`, `call_function_with_raw_args`, and
46//! `call_closure_with_raw_args` carried the `&[(u64, NativeKind)]`
47//! pair-slice form pre-§2.7.11. ADR-006 §2.7.11 migration-scope
48//! refinement (post-W7 audit, 2026-05-09) rejected this shape on §2.7.6
49//! / Q8 carrier-API-bound grounds at the runtime tier, and
50//! W7-cv-polymorphic (Round 3) deleted all three entry-points — their
51//! callers route through `call_value_immediate_nb` /
52//! `call_function_with_nb_args` / `call_closure_with_nb_args_keepalive`
53//! over `&[KindedSlot]` instead. `jit_trampoline_call_closure` is the
54//! only `_raw` survivor — it is the §2.7.5 cross-crate stable FFI
55//! consumer where the parallel-pair shape is canonical (consumers
56//! translate `&[KindedSlot]` → raw u64 at the FFI boundary, single
57//! direction).
58//!
59//! # Forbidden patterns (W7 playbook §6 — refused on sight)
60//!
61//! - `Vec<KindedSlot>` by-move parameter (#12 — caller owns shares;
62//!   by-move desynchronizes drop accounting). Borrow-only `&[..]`.
63//! - `&[(u64, NativeKind)]` pair-slice as a runtime-tier dispatch ABI
64//!   (#13 — §2.7.6 / Q8 carrier-API-bound; pair-slice rejected at
65//!   runtime tier, allowed only at the §2.7.5 stable-FFI boundary —
66//!   `jit_trampoline_call_closure` is the sole survivor).
67//! - Bool-default fallback for unresolved-kind capture at frame setup
68//!   (#16 — §2.7.8 #4; correct response is surface-and-stop, panic
69//!   from `read_capture_kinded` is diagnostic, not fallback).
70//! - Re-introducing `_upvalue_bits: Vec<u64>` parameter — the deleted
71//!   pre-§2.7.8 ABI shape; replacement is `&OwnedClosureBlock`.
72//! - Renaming the deleted kind-blind value-call ABI by hypothetical
73//!   role per CLAUDE.md "Renames to refuse on sight" (#18) — describe
74//!   deleted code by name (the pre-§2.7.11 raw-u64 entry-points) or
75//!   by deletion-fate (the kind-blind value-call ABI), never via the
76//!   bridge/probe/helper/hop/translator/adapter/shim framing the
77//!   2026-05-09 broadening enumerates.
78//!
79//! The B9 lockstep invariant
80//! (`closure_heap_bits.is_some() == closure_heap_kind.is_some()`) is
81//! enforced via `debug_assert_eq!` at every frame-construction site.
82
83use shape_value::v2::closure_raw::{OwnedClosureBlock, typed_closure_function_id};
84use shape_value::{HeapKind, HeapValue, KindedSlot, NativeKind, ValueSlot, VMError};
85
86use super::task_scheduler::TaskStatus;
87use super::vm_impl::stack::clone_with_kind;
88
89use super::{CallFrame, VirtualMachine};
90
91impl VirtualMachine {
92    /// Execute a named function with arguments, returning its result.
93    ///
94    /// **W7-cv-method (Round 3 close).** Resolves `name` to `func_id` via
95    /// the program function table and routes to
96    /// [`execute_function_by_id`] per W7 playbook §4.
97    pub fn execute_function_by_name(
98        &mut self,
99        name: &str,
100        args: Vec<KindedSlot>,
101        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
102    ) -> Result<KindedSlot, VMError> {
103        let func_id = self
104            .program
105            .functions
106            .iter()
107            .position(|f| f.name == name)
108            .ok_or_else(|| VMError::RuntimeError(format!("Function '{}' not found", name)))?
109            as u16;
110        self.execute_function_by_id(func_id, args, ctx)
111    }
112
113    /// Execute a function by its ID with positional arguments.
114    ///
115    /// **W7-cv-method (Round 3 close).** Captures `saved_depth` before
116    /// frame setup, routes through [`call_function_with_nb_args`], drives
117    /// the callee to completion via
118    /// [`execute_until_call_depth`](Self::execute_until_call_depth), and
119    /// pops the result via the kinded API (W7 playbook §4 + §2.7.10 / Q11
120    /// dispatch shape).
121    ///
122    /// **Ownership.** Each `KindedSlot` in `args` holds a strong-count
123    /// share. cluster-1.5 v2-raw-empirical-isolation-and-fix
124    /// (2026-05-17): post-fix `call_function_with_nb_args` is share-
125    /// neutral (clones each arg before frame-write so the frame's
126    /// teardown `truncate_stack` retire balances the in-helper clone;
127    /// caller's carrier shares are preserved by the borrow-only
128    /// `&[KindedSlot]` signature). We let `args` drop normally at
129    /// scope exit — the per-slot `KindedSlot::Drop` retires each
130    /// caller-owned share. The legacy `mem::forget` pre-fix was a
131    /// load-bearing leak that compensated for the missing clone in
132    /// the helper; with the helper now share-neutral, the forget would
133    /// LEAK one share per heap-bearing arg.
134    pub fn execute_function_by_id(
135        &mut self,
136        func_id: u16,
137        args: Vec<KindedSlot>,
138        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
139    ) -> Result<KindedSlot, VMError> {
140        let saved_call_depth = self.call_stack.len();
141        self.call_function_with_nb_args(func_id, &args)?;
142        self.execute_until_call_depth(saved_call_depth, ctx)?;
143        let (bits, kind) = self.pop_kinded()?;
144        // `args` drops here; per-slot `KindedSlot::Drop` retires each
145        // caller-owned share. The helper's internal `clone_with_kind`
146        // before frame-write ensures the new frame owns an independent
147        // share retired at `truncate_stack` teardown.
148        drop(args);
149        Ok(KindedSlot::new(ValueSlot::from_raw(bits), kind))
150    }
151
152    /// Execute a closure with its captured upvalues and arguments.
153    ///
154    /// **W7-cv-method (Round 3 close).** The pre-§2.7.8 `_upvalue_bits:
155    /// Vec<u64>` parameter — the deleted-ABI raw-bits shape — is replaced
156    /// by `closure_block: &OwnedClosureBlock`. Captures flow from the
157    /// block's parallel-kind track via `read_capture_kinded` inside
158    /// [`call_closure_with_nb_args_keepalive`], not from a side-channel
159    /// payload (W7 playbook §4 + ADR-006 §2.7.8 / Q10).
160    ///
161    /// The keep-alive companion fields carry the closure-self share so
162    /// `op_return` / `op_return_value` release it via `drop_with_kind`
163    /// on frame teardown — same B9 lockstep pattern as
164    /// `call_value_immediate_nb`'s closure arm.
165    pub fn execute_closure(
166        &mut self,
167        closure_block: &OwnedClosureBlock,
168        args: Vec<KindedSlot>,
169        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
170    ) -> Result<KindedSlot, VMError> {
171        // SAFETY: `closure_block` is a live borrow into a TypedClosureHeader
172        // block allocated by `alloc_typed_closure`; its `as_ptr()` points
173        // to a valid header per the construction invariant.
174        let function_id = unsafe { typed_closure_function_id(closure_block.as_ptr()) };
175
176        let saved_call_depth = self.call_stack.len();
177        // No keep-alive carrier — the synthetic dispatch path: the block
178        // lifetime is guaranteed by the borrow held across this call,
179        // so `closure_heap_bits` / `closure_heap_kind` are both `None`
180        // (B9 lockstep `Some(..)` ↔ `Some(..)`).
181        //
182        // cluster-1.5 v2-raw-empirical-isolation-and-fix (2026-05-17):
183        // post-fix `call_closure_with_nb_args_keepalive` is share-
184        // neutral (clones each arg + capture before frame-write); we
185        // let `args` drop normally at scope exit to retire the caller-
186        // owned shares. The legacy `mem::forget` was a load-bearing
187        // leak compensating for the missing clone — see
188        // `execute_function_by_id` for the matching ownership-comment
189        // rewrite.
190        self.call_closure_with_nb_args_keepalive(function_id, closure_block, &args, None, None)?;
191        self.execute_until_call_depth(saved_call_depth, ctx)?;
192        let (bits, kind) = self.pop_kinded()?;
193        drop(args);
194        Ok(KindedSlot::new(ValueSlot::from_raw(bits), kind))
195    }
196
197    /// Fast function execution for hot loops (backtesting).
198    ///
199    /// **W7-cv-method (Round 3 close).** Pre-computed `func_id`, no name
200    /// lookup, no args (callers that need args route through
201    /// `execute_function_by_id`). Same `saved_depth` pattern as the
202    /// other public entry-points.
203    pub fn execute_function_fast(
204        &mut self,
205        func_id: u16,
206        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
207    ) -> Result<KindedSlot, VMError> {
208        let saved_call_depth = self.call_stack.len();
209        self.call_function_with_nb_args(func_id, &[])?;
210        self.execute_until_call_depth(saved_call_depth, ctx)?;
211        let (bits, kind) = self.pop_kinded()?;
212        Ok(KindedSlot::new(ValueSlot::from_raw(bits), kind))
213    }
214
215    /// Execute a function with named arguments.
216    ///
217    /// **W7-cv-method (Round 3 close).** Maps `&[(String, KindedSlot)]`
218    /// to a positional `Vec<KindedSlot>` via `descriptor.param_names`
219    /// lookup, then routes through [`execute_function_by_id`] per W7
220    /// playbook §4. Missing positional slots are sentinel-filled with
221    /// `(NONE_BITS, NativeKind::Bool)` per W6.5 §2 Null/Unit row —
222    /// Drop/Clone-no-op so the pre-population is leak-free.
223    ///
224    /// **Ownership.** The caller's named-args carry one share per slot;
225    /// `clone_with_kind` is NOT used (we re-home the slot's bits by
226    /// reading the slot directly). After mapping, the positional vec
227    /// owns the same shares; they transfer into the new frame via the
228    /// `execute_function_by_id` `mem::forget` discipline.
229    pub fn execute_function_with_named_args(
230        &mut self,
231        func_id: u16,
232        named_args: &[(String, KindedSlot)],
233        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
234    ) -> Result<KindedSlot, VMError> {
235        let (arity, param_names) = {
236            let function = self
237                .program
238                .functions
239                .get(func_id as usize)
240                .ok_or(VMError::InvalidCall)?;
241            (function.arity as usize, function.param_names.clone())
242        };
243
244        // Sentinel-fill positional slots with Null/Unit row (W6.5 §2):
245        // NONE_BITS + NativeKind::Bool is Drop/Clone-no-op, so the
246        // pre-fill is leak-free until the named-arg loop overwrites
247        // each present slot below.
248        let mut args: Vec<KindedSlot> = (0..arity)
249            .map(|_| KindedSlot::new(ValueSlot::none(), NativeKind::Bool))
250            .collect();
251
252        for (name, value) in named_args {
253            if let Some(idx) = param_names.iter().position(|p| p == name) {
254                if idx < args.len() {
255                    // The caller owns the named-args slice's shares (they
256                    // pass `&[(String, KindedSlot)]` by borrow). Bump the
257                    // refcount once via `clone_with_kind` so the
258                    // positional vec owns an independent share that
259                    // transfers cleanly into the new frame; the caller's
260                    // outer slot stays live for them to drop. Sentinel
261                    // pair released by `KindedSlot::Drop` on the
262                    // `args[idx] = ...` write below — Drop-no-op for
263                    // (NONE_BITS, Bool).
264                    super::vm_impl::stack::clone_with_kind(value.slot.raw(), value.kind);
265                    args[idx] = KindedSlot::new(
266                        ValueSlot::from_raw(value.slot.raw()),
267                        value.kind,
268                    );
269                }
270            }
271        }
272
273        self.execute_function_by_id(func_id, args, ctx)
274    }
275
276    /// Resume execution after a suspension.
277    ///
278    /// **§2.7.4 Phase-2c — stays `todo!()`.** The suspension shape
279    /// requires snapshot-tier work: the resume body's pre-§2.7.7 form
280    /// pushed `value` onto the stack and re-entered the suspendable
281    /// dispatch loop, but the snapshot/restore family
282    /// (`apply_pending_resume` / `apply_pending_frame_resume` in
283    /// `executor/resume.rs`) is itself §2.7.4 deferred — its bodies
284    /// return `VMError::NotImplemented(PHASE_2C_SNAPSHOT_SURFACE)`. Until
285    /// the snapshot rebuild lands a kind-threaded
286    /// `slot_to_serializable` / `serializable_to_slot` pair plus the
287    /// §2.7.8 cell-storage parallel-kind tracks for `module_bindings`
288    /// and frame-resume payloads, this entry-point cannot be wired —
289    /// surface-and-stop trigger per W7 playbook §8 (snapshot-tier
290    /// resume).
291    pub fn resume(
292        &mut self,
293        _value: KindedSlot,
294        _ctx: Option<&mut shape_runtime::context::ExecutionContext>,
295    ) -> Result<super::ExecutionResult, VMError> {
296        todo!(
297            "phase-2c — see ADR-006 §2.7.4 / §2.7.11 out-of-scope: \
298             resume() depends on snapshot-tier rebuild (executor/resume.rs \
299             apply_pending_resume / apply_pending_frame_resume)"
300        )
301    }
302
303    /// Execute with automatic async task resolution.
304    ///
305    /// **Filled by W7-cv-async (Round 3 close).** Per W7 playbook §4
306    /// W7-cv-async row, sync-resolution only — suspension state crossing
307    /// a `call_value_immediate_*` boundary is OUT OF SCOPE per ADR-006
308    /// §2.7.11 out-of-scope clause (Phase-2c snapshot tier; same
309    /// out-of-scope clause as §2.7.10).
310    ///
311    /// Drives the program forward via `execute_fast(ctx)` (the standard
312    /// run-to-halt loop that pops the top-of-stack result on completion).
313    /// Inline task resolution at `op_await` / `op_join_await` sites in
314    /// `executor/async_ops/mod.rs` is the integration point with
315    /// [`resolve_spawned_task`] (below) — once the §2.7.4 task-scheduler
316    /// kinded-ABI re-light closes those `todo!()` arms, the await-site
317    /// handler invokes `resolve_spawned_task(task_id)` directly inside
318    /// the dispatch loop, and this driver re-enters `execute_fast` to
319    /// continue the program after the suspended `op_await` opcode
320    /// returns.
321    ///
322    /// The pre-bulldozer `execute_with_async` shape — drive a `loop`
323    /// over `task_scheduler.iter_pending()` calling `resolve_spawned_task`
324    /// per ready task — depends on a public iterator over
325    /// `TaskScheduler.callables` that does not exist in the current
326    /// scheduler API surface (W7-cv-async owns only `call_convention.rs`
327    /// per W7 playbook §10 forbidden zones — `task_scheduler.rs` is
328    /// out-of-territory). When a future cluster lands the iteration
329    /// API, the loop body in this function is the natural extension
330    /// point: while there is a `Pending`-with-callable task, call
331    /// `resolve_spawned_task(id)` and discard the per-task result; the
332    /// program-level result still comes from `execute_fast` at the end.
333    pub fn execute_with_async(
334        &mut self,
335        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
336    ) -> Result<KindedSlot, VMError> {
337        // Sync-resolution only. The §2.7.11 out-of-scope clause is
338        // explicit: suspension state crossing a `call_value_immediate_*`
339        // boundary is Phase-2c snapshot-tier work and stays outside
340        // Wave 7. The bytecode loop drives the program; per-await-site
341        // inline resolution is the `resolve_spawned_task` integration
342        // point handed to the async_ops dispatch arms when the §2.7.4
343        // scheduler kinded-ABI re-light lands.
344        self.execute_fast(ctx)
345    }
346
347    /// Resolve a spawned task by executing its callable synchronously.
348    ///
349    /// **Filled by W7-cv-async (Round 3 close).** Per W7 playbook §4
350    /// W7-cv-async row body shape: look up the task's callable from the
351    /// scheduler, route through `call_closure_with_nb_args_keepalive`
352    /// for closure callables (or `call_function_with_nb_args` for raw
353    /// function-id callables), drive the callee to completion via
354    /// `execute_until_call_depth(saved_depth, None)`, pop the result
355    /// via `pop_kinded`, cache it, and return.
356    ///
357    /// The scheduler stores callables as `(u64, NativeKind)` pairs per
358    /// the §2.7.7 carrier shape (Wave 6.5 R-async-time / E-async close
359    /// already migrated `task_scheduler.rs` off `ValueWord`). The two
360    /// expected callable kinds match the `call_value_immediate_nb`
361    /// dispatch shape from W7-cv-static (Round 2 close):
362    ///
363    /// - `Ptr(HeapKind::Closure)` — recover `OwnedClosureBlock` via
364    ///   `slot.as_heap_value()` + `HeapValue::ClosureRaw(block)` per
365    ///   ADR-005 §1 single-discriminator. Function-id reads from the
366    ///   `TypedClosureHeader` via the unsafe `typed_closure_function_id`
367    ///   helper (the canonical accessor; `OwnedClosureBlock` has no
368    ///   safe public accessor for `function_id`). Frame setup carries
369    ///   the closure-self share through `closure_heap_bits` /
370    ///   `closure_heap_kind` per the B9 lockstep companion fields, so
371    ///   `op_return` releases it via `drop_with_kind` at frame teardown.
372    ///   Spawned tasks have no caller-supplied args — the closure runs
373    ///   with `&[]` for the arg slice.
374    /// - `UInt64` — function-id callable. The bits encode the function
375    ///   id as a raw `u64` payload (`UInt64` is the §2.7.11 callee-
376    ///   classification kind for function references; same convention
377    ///   as `call_value_immediate_nb`'s `UInt64` arm). No Arc share
378    ///   to drop; `drop_with_kind(_, NativeKind::UInt64)` is a no-op.
379    ///   Routes through `call_function_with_nb_args(func_id, &[])` —
380    ///   the non-closure entry-point in this module's frame-setup
381    ///   family (W7-frame-setup, Round 1 close).
382    ///
383    /// **Cached fast-path.** If `task_scheduler.get_result(task_id)`
384    /// returns `TaskStatus::Completed((bits, kind))`, the cached share
385    /// is cloned via `clone_with_kind` and returned directly — same
386    /// pattern as `TaskScheduler::resolve_task` (cached entry retains
387    /// its share; caller gets a fresh share). Cancelled tasks surface
388    /// as `RuntimeError`.
389    ///
390    /// **Suspension out of scope.** If the callee's body suspends mid-
391    /// execution (an `op_await` / `op_suspend` inside the spawned
392    /// closure), the suspension shape crossing this `call_value_immediate_*`
393    /// frame boundary is §2.7.4 Phase-2c snapshot-tier (W7 playbook
394    /// §9 risk row, ADR-006 §2.7.11 out-of-scope clause). The current
395    /// body drives `execute_until_call_depth` to a definite return; a
396    /// `VMError::Suspended` mid-call is propagated upward and the
397    /// task's cached entry remains `Pending` until a future Phase-2c
398    /// rebuild lands the snapshot-tier resumption.
399    pub(in crate::executor) fn resolve_spawned_task(
400        &mut self,
401        task_id: u64,
402    ) -> Result<KindedSlot, VMError> {
403        // Cached fast-path — the scheduler already holds a Completed
404        // share for this task. Hand out a fresh share via
405        // `clone_with_kind` so the cached entry retains its own.
406        match self.task_scheduler.get_result(task_id) {
407            Some(TaskStatus::Completed((bits, kind))) => {
408                let bits = *bits;
409                let kind = *kind;
410                clone_with_kind(bits, kind);
411                return Ok(KindedSlot::new(ValueSlot::from_raw(bits), kind));
412            }
413            Some(TaskStatus::Cancelled) => {
414                return Err(VMError::RuntimeError(format!(
415                    "Task {} was cancelled",
416                    task_id
417                )));
418            }
419            // Pending or unknown — fall through to the take-callable path.
420            Some(TaskStatus::Pending) | None => {}
421        }
422
423        // Take ownership of the callable share — `take_callable`
424        // transfers the strong-count from the scheduler map to us.
425        let (callable_bits, callable_kind) =
426            self.task_scheduler.take_callable(task_id).ok_or_else(|| {
427                VMError::RuntimeError(format!("No callable registered for task {}", task_id))
428            })?;
429
430        // Capture call-stack depth BEFORE frame setup pushes a new
431        // frame. The callee's `op_return` / `op_return_value` pops its
432        // frame, returning the call-stack depth to this saved value;
433        // `execute_until_call_depth(saved_depth, None)` is the canonical
434        // "drive callee to completion" loop. Same pattern as
435        // `call_value_immediate_nb` (W7-cv-static, Round 2 close).
436        let saved_call_depth = self.call_stack.len();
437
438        match callable_kind {
439            NativeKind::Ptr(HeapKind::Closure) => {
440                // Recover `OwnedClosureBlock` via the §2.7.6 / Q8 heap
441                // dispatch path: construct a `ValueSlot` from the raw
442                // bits, call `as_heap_value()`, pattern-match the
443                // `HeapValue::ClosureRaw(block)` arm per ADR-005 §1
444                // single-discriminator. The pattern mirrors
445                // `call_value_immediate_nb`'s closure arm verbatim —
446                // diverging would re-introduce a forbidden parallel
447                // dispatch surface.
448                let callable_slot = ValueSlot::from_raw(callable_bits);
449                let block: &OwnedClosureBlock = match callable_slot.as_heap_value() {
450                    HeapValue::ClosureRaw(b) => b,
451                    other => {
452                        // Drop the callable share before surfacing the
453                        // error so refcount discipline holds (playbook
454                        // §3 drop discipline). `drop_with_kind` on
455                        // `Ptr(HeapKind::Closure)` releases the
456                        // `Arc<HeapValue>` share per W7-closure-retain
457                        // (Round 2.5 close).
458                        let type_name = other.type_name();
459                        super::vm_impl::stack::drop_with_kind(callable_bits, callable_kind);
460                        debug_assert!(
461                            false,
462                            "resolve_spawned_task: HeapKind::Closure label with \
463                             non-ClosureRaw HeapValue payload: {:?}",
464                            type_name
465                        );
466                        return Err(VMError::RuntimeError(format!(
467                            "resolve_spawned_task: HeapKind::Closure label with \
468                             non-ClosureRaw payload: {}",
469                            type_name
470                        )));
471                    }
472                };
473                // SAFETY: `block` is a live `OwnedClosureBlock` borrowed
474                // through the live `&HeapValue` returned by
475                // `as_heap_value()`; its `as_ptr()` points to a
476                // `TypedClosureHeader` block allocated by
477                // `alloc_typed_closure` per the construction invariant.
478                let function_id = unsafe { typed_closure_function_id(block.as_ptr()) };
479
480                // Frame setup. The B9 lockstep companion fields carry
481                // the closure-self share so `op_return` /
482                // `op_return_value` can release it via
483                // `drop_with_kind(bits, kind)` on frame teardown — the
484                // share transfers from `take_callable` into the
485                // `CallFrame.closure_heap_bits` field. Spawned tasks
486                // have no caller args, so the arg slice is empty.
487                self.call_closure_with_nb_args_keepalive(
488                    function_id,
489                    block,
490                    &[],
491                    Some(callable_bits),
492                    Some(callable_kind),
493                )?;
494            }
495            NativeKind::UInt64 => {
496                // Function-id callable: bits encode the function id as
497                // a raw `u64` payload. `UInt64` is the §2.7.11 callee-
498                // classification kind for function references — same
499                // convention as `call_value_immediate_nb`'s `UInt64`
500                // arm (W7-cv-static, Round 2 close). No Arc share to
501                // drop; `drop_with_kind(_, NativeKind::UInt64)` is a
502                // no-op.
503                //
504                // Truncate to `u16` since `BytecodeProgram::functions`
505                // is indexed by `u16` and `call_function_with_nb_args`
506                // takes `func_id: u16`. A bits value that doesn't index
507                // into the function table surfaces as
508                // `VMError::InvalidCall` from `call_function_with_nb_args`
509                // itself per its existing
510                // `program.functions.get(func_id as usize).ok_or(VMError::InvalidCall)?`
511                // guard.
512                let function_id = callable_bits as u16;
513                self.call_function_with_nb_args(function_id, &[])?;
514            }
515            other => {
516                // Unsupported callable kind — release the share before
517                // surfacing (playbook §3). The kind classification list
518                // for spawned-task callables matches
519                // `call_value_immediate_nb` (W7-cv-static): closure or
520                // function-id; trait-object closure dispatch (W9 TR
521                // territory) routes through this RuntimeError until
522                // that wave lands.
523                super::vm_impl::stack::drop_with_kind(callable_bits, callable_kind);
524                return Err(VMError::RuntimeError(format!(
525                    "resolve_spawned_task: callable must be \
526                     NativeKind::Ptr(HeapKind::Closure) or \
527                     NativeKind::UInt64, got {:?}",
528                    other
529                )));
530            }
531        }
532
533        // Drive the callee to completion. `execute_until_call_depth`
534        // returns when `self.call_stack.len() == saved_call_depth`
535        // (the callee's frame has been popped by `op_return`). The
536        // return value is left on the value stack by `op_return_value`;
537        // `pop_kinded` transfers the share cleanly into the result
538        // `KindedSlot`.
539        //
540        // §2.7.4 Phase-2c — suspension state crossing this frame
541        // boundary stays out of scope per ADR-006 §2.7.11 out-of-scope
542        // clause. A `VMError::Suspended` propagates upward; the
543        // task's cached entry remains `Pending`.
544        self.execute_until_call_depth(saved_call_depth, None)?;
545        let (result_bits, result_kind) = self.pop_kinded()?;
546
547        // Cache the result — clone the share so the scheduler entry
548        // and the returned `KindedSlot` each own one independent
549        // strong-count. Same pattern as `TaskScheduler::resolve_task`
550        // and `try_resolve_external` cached-completion paths.
551        clone_with_kind(result_bits, result_kind);
552        self.task_scheduler.complete(task_id, result_bits, result_kind);
553        Ok(KindedSlot::new(ValueSlot::from_raw(result_bits), result_kind))
554    }
555
556    /// Non-closure frame setup from a `&[KindedSlot]` arg slice
557    /// (ADR-006 §2.7.10 / Q11 caller-side carrier; W7 playbook §4).
558    ///
559    /// Pushes a fresh `CallFrame` for `func_id` and threads each arg's
560    /// `(bits, kind)` into the new frame's locals via
561    /// `stack_write_kinded`. The B9 lockstep companion fields
562    /// `closure_heap_bits` / `closure_heap_kind` are both `None` —
563    /// non-closure calls own no closure-self share.
564    ///
565    /// **Ownership.** The caller (the `op_call_value` dispatch shell or
566    /// a public entry-point such as `execute_function_by_id`) owns one
567    /// strong-count share per arg slot. This function transfers each
568    /// share into the new frame's local slot via `stack_write_kinded`
569    /// (which drops the prior occupant — a sentinel after the
570    /// `resize_with` below — and installs the new bits). The dispatch
571    /// shell calls `mem::forget` on its arg vec after this function
572    /// returns to release the source-side carriers without dropping
573    /// the shares. Same pattern as the §2.7.10 `op_call_method`
574    /// dispatch shell.
575    pub(crate) fn call_function_with_nb_args(
576        &mut self,
577        func_id: u16,
578        args: &[KindedSlot],
579    ) -> Result<(), VMError> {
580        let (locals_count, entry_point) = {
581            let func = self
582                .program
583                .functions
584                .get(func_id as usize)
585                .ok_or(VMError::InvalidCall)?;
586            (func.locals_count as usize, func.entry_point)
587        };
588        let blob_hash = self.blob_hash_for_function(func_id);
589
590        let base_pointer = self.sp;
591        let needed = base_pointer + locals_count;
592        if needed > self.stack.len() {
593            // ADR-006 §2.7.7 / §2.7.8 lockstep growth: data + parallel
594            // kind track grow together. Sentinel pair `(NONE_BITS,
595            // NativeKind::Bool)` is Drop/Clone-no-op so the freshly
596            // resized window is leak-free until each slot is written
597            // by the arg-thread loop / left as the omitted-arg
598            // sentinel (W6.5 §2 Null/Unit row).
599            self.stack.resize_with(needed * 2 + 1, || Self::NONE_BITS);
600            self.kinds.resize(needed * 2 + 1, NativeKind::Bool);
601        }
602
603        let return_ip = self.ip;
604        self.call_stack.push(CallFrame {
605            return_ip,
606            base_pointer,
607            locals_count,
608            function_id: Some(func_id),
609            upvalues: None,
610            blob_hash,
611            closure_heap_bits: None,
612            // ADR-006 §2.7.8 / Q10: lockstep companion to
613            // `closure_heap_bits`. Non-closure call → both `None`.
614            closure_heap_kind: None,
615        });
616
617        // Walk args and thread each into the new frame's local at
618        // `base_pointer + i`. Per W7 playbook §4 / W6.5 §3, the
619        // `stack_write_kinded` write transfers the share into the
620        // local slot (drops the sentinel from the resize above —
621        // a no-op).
622        //
623        // cluster-1.5 v2-raw-empirical-isolation-and-fix (2026-05-17):
624        // `slot.slot.raw()` is a raw bit read — does NOT bump the arg's
625        // refcount. Each caller-side `KindedSlot` carrier still owns
626        // its share (the call path may receive args by borrow, e.g.
627        // via `call_value_immediate_nb`'s callsites that pass
628        // `&[KindedSlot]` from a locally-constructed array). The frame
629        // teardown `truncate_stack(bp)` at `op_return_value` releases
630        // each arg's share via `drop_with_kind`. Without the
631        // `clone_with_kind` below, the two releases retire one share
632        // more than was acquired — for heap-bearing kinds this
633        // surfaces as a use-after-free. Mirror precedent: see the
634        // matching fix in `call_closure_with_nb_args_keepalive` below.
635        for (i, slot) in args.iter().enumerate() {
636            crate::executor::vm_impl::stack::clone_with_kind(slot.slot.raw(), slot.kind);
637            self.stack_write_kinded(base_pointer + i, slot.slot.raw(), slot.kind);
638        }
639
640        self.sp = base_pointer + locals_count;
641        self.ip = entry_point;
642        Ok(())
643    }
644
645    /// Closure frame setup with no closure-self keep-alive (synthetic
646    /// dispatch where the block lifetime is guaranteed externally — e.g.
647    /// `execute_closure` from the public VM entry-point family). Thin
648    /// forwarder over [`call_closure_with_nb_args_keepalive`] with
649    /// `(None, None)` for the B9 lockstep companion fields.
650    ///
651    /// The `_upvalue_bits: Vec<u64>` parameter is the deleted pre-§2.7.8
652    /// ABI shape — the kinded replacement takes a borrowed
653    /// `OwnedClosureBlock` per ADR-006 §2.7.8 / Q10 (the cell-storage
654    /// parallel-kind track is the canonical capture-kind source).
655    pub(crate) fn call_closure_with_nb_args(
656        &mut self,
657        func_id: u16,
658        closure_block: &OwnedClosureBlock,
659        args: &[KindedSlot],
660    ) -> Result<(), VMError> {
661        self.call_closure_with_nb_args_keepalive(func_id, closure_block, args, None, None)
662    }
663
664    /// Closure frame setup from a borrowed `OwnedClosureBlock` plus an
665    /// `&[KindedSlot]` arg slice (ADR-006 §2.7.8 / Q10 cell-storage
666    /// parallel-kind invariant; §2.7.10 / Q11 dispatch-slice carrier;
667    /// §2.7.11 / Q12 value-call ABI; W7 playbook §4).
668    ///
669    /// Captures flow via `OwnedClosureBlock::read_capture_kinded(idx)`
670    /// — the kind comes directly from the closure layout's
671    /// `capture_native_kinds` track, threaded into the new frame's
672    /// reserved capture-locals via `stack_write_kinded`. Args follow
673    /// the captures, occupying `[base_pointer + capture_count ..
674    /// base_pointer + capture_count + args.len()]`.
675    ///
676    /// The B9 lockstep companion fields `closure_heap_bits` /
677    /// `closure_heap_kind` carry the closure-self share (`Some` for
678    /// closure dispatch through `op_call_value` / `op_call_closure`;
679    /// `None` for synthetic / trampoline-style construction where the
680    /// block lifetime is guaranteed externally). The
681    /// `debug_assert_eq!` below enforces both fields are `Some`
682    /// together or `None` together at every observable boundary.
683    ///
684    /// **Ownership.** The caller owns one strong-count share per arg
685    /// slot and one share for `closure_heap_bits` (when `Some`); both
686    /// transfer into the new frame via `stack_write_kinded` and the
687    /// `CallFrame.closure_heap_bits` field respectively. Capture reads
688    /// via `read_capture_kinded` are raw-bit reads — the shares stay
689    /// owned by the `OwnedClosureBlock` (which the caller passes by
690    /// borrow); the closure_heap_bits keep-alive ensures the block
691    /// outlives the callee's pointer dereferences. Cell-storage
692    /// captures (`OwnedMutable` / `Shared`) load through the new
693    /// frame's `LoadOwnedClosureSelf` opcode using the kind from
694    /// `closure_heap_kind` — see §2.7.8 / Q10 for the cell read flow.
695    pub(crate) fn call_closure_with_nb_args_keepalive(
696        &mut self,
697        func_id: u16,
698        closure_block: &OwnedClosureBlock,
699        args: &[KindedSlot],
700        closure_heap_bits: Option<u64>,
701        closure_heap_kind: Option<NativeKind>,
702    ) -> Result<(), VMError> {
703        debug_assert_eq!(
704            closure_heap_bits.is_some(),
705            closure_heap_kind.is_some(),
706            "ADR-006 §2.7.8 / Q10: closure_heap_bits and closure_heap_kind \
707             must be Some together or None together"
708        );
709
710        let (locals_count, entry_point) = {
711            let func = self
712                .program
713                .functions
714                .get(func_id as usize)
715                .ok_or(VMError::InvalidCall)?;
716            (func.locals_count as usize, func.entry_point)
717        };
718        let blob_hash = self.blob_hash_for_function(func_id);
719
720        let layout = closure_block.layout();
721        let capture_count = layout.capture_count();
722
723        let base_pointer = self.sp;
724        let needed = base_pointer + locals_count;
725        if needed > self.stack.len() {
726            // ADR-006 §2.7.7 / §2.7.8 lockstep growth — see
727            // `call_function_with_nb_args` for the sentinel-pair
728            // rationale.
729            self.stack.resize_with(needed * 2 + 1, || Self::NONE_BITS);
730            self.kinds.resize(needed * 2 + 1, NativeKind::Bool);
731        }
732
733        let return_ip = self.ip;
734        self.call_stack.push(CallFrame {
735            return_ip,
736            base_pointer,
737            locals_count,
738            function_id: Some(func_id),
739            upvalues: None,
740            blob_hash,
741            closure_heap_bits,
742            // ADR-006 §2.7.8 / Q10: lockstep companion to
743            // `closure_heap_bits`. The `debug_assert_eq!` above
744            // guarantees `Some(..)` ↔ `Some(..)`.
745            closure_heap_kind,
746        });
747
748        // Walk captures from the closure layout's parallel-kind track
749        // (ADR-006 §2.7.8 / Q10). `read_capture_kinded(idx)` returns
750        // `(bits, kind)` directly — the kind comes from
751        // `layout.capture_native_kinds[idx]`, set at closure
752        // construction by the producing `MakeClosure` opcode. No
753        // fabrication, no Bool-default fallback (§2.7.8 #4 forbidden);
754        // a misalignment between layout and stored bits is a
755        // construction-side bug that surfaces as a panic from
756        // `read_capture_kinded` itself (W7 playbook §8 surface-and-stop).
757        //
758        // cluster-1.5 v2-raw-empirical-isolation-and-fix (2026-05-17):
759        // `read_capture_kinded` is a raw bit read — does NOT bump the
760        // capture's refcount. The frame's `truncate_stack(bp)` at
761        // `op_return_value` teardown WILL release each capture's share
762        // via `drop_with_kind`. The closure block itself ALSO owns one
763        // share per capture (released at `OwnedClosureBlock::Drop` via
764        // the capture-mask walk). Without the `clone_with_kind` below,
765        // both releases retire one share more than was acquired — same
766        // pattern as the Round 13 T5 `closure_heap_bits` companion fix
767        // for the closure-self share. Mirror precedent: the explicit
768        // `clone_with_kind` at `call_value_immediate_nb` line 870 for
769        // the callee carrier.
770        for capture_idx in 0..capture_count {
771            // SAFETY: the block was constructed by the producing
772            // `MakeClosure` opcode with `capture_count` initialised
773            // capture slots; the borrow from the dispatch shell holds
774            // the block live for the duration of this call.
775            let (bits, kind) = unsafe { closure_block.read_capture_kinded(capture_idx) };
776            crate::executor::vm_impl::stack::clone_with_kind(bits, kind);
777            self.stack_write_kinded(base_pointer + capture_idx, bits, kind);
778        }
779
780        // Walk args and thread each into the local slot following the
781        // captures.
782        //
783        // cluster-1.5 v2-raw-empirical-isolation-and-fix (2026-05-17):
784        // `slot.slot.raw()` is a raw bit read — does NOT bump the arg's
785        // refcount. The caller (e.g. `v2_filter` at
786        // `hashmap_methods.rs:1640`) constructs each `KindedSlot` arg
787        // locally; those carriers own one share each, released at
788        // scope exit via `KindedSlot::Drop`. The frame's
789        // `truncate_stack(bp)` at `op_return_value` teardown WILL
790        // ALSO release each arg's share via `drop_with_kind`. Without
791        // the `clone_with_kind` below, the two releases retire one
792        // share more than was acquired — for heap-bearing arg kinds
793        // (`String`, `Ptr(...)`, `StringV2`, `DecimalV2`) this is a
794        // use-after-free on the underlying allocation. SIGABRT chain
795        // surfaces at `crates/shape-value/src/v2/typed_array.rs:317`
796        // (`drop_array_heap` element walk) when the freed Arc<String>
797        // data is later re-read via `kept_keys[i].as_str()` in
798        // `build_filtered_kref` — the str data ptr aliases freshly
799        // alloc'd memory and the next StringObj alloc reuses the same
800        // slot, corrupting refcount metadata.
801        //
802        // Mirror precedent: the explicit `clone_with_kind` at
803        // `call_value_immediate_nb` line 870 for the callee carrier
804        // (Round 13 T5 share-accounting fix, audit doc
805        // `docs/cluster-audits/w17-vm-call-value-closure-kind-mismatch-audit.md`).
806        let arg_base = base_pointer + capture_count;
807        for (i, slot) in args.iter().enumerate() {
808            crate::executor::vm_impl::stack::clone_with_kind(slot.slot.raw(), slot.kind);
809            self.stack_write_kinded(arg_base + i, slot.slot.raw(), slot.kind);
810        }
811
812        self.sp = base_pointer + locals_count;
813        self.ip = entry_point;
814        Ok(())
815    }
816
817    /// `call_value_immediate` (kinded carrier form): dispatches on the
818    /// callee's `KindedSlot.kind`. ADR-006 §2.7.11 / Q12 caller-side
819    /// shape — both callee and args travel as `KindedSlot`.
820    ///
821    /// **Filled by W7-cv-static (Round 2 close).** Per W7 playbook §4:
822    /// matches on `callee.kind` and routes — `Ptr(HeapKind::Closure)`
823    /// recovers the `OwnedClosureBlock` via `slot.as_heap_value()` +
824    /// `HeapValue::ClosureRaw` (single discriminator per ADR-005 §1)
825    /// and routes to `call_closure_with_nb_args_keepalive`; `UInt64`
826    /// callee bits are the function-id and route to
827    /// `call_function_with_nb_args`. Both arms drive the callee to
828    /// completion via `execute_until_call_depth(saved_depth, ctx)`
829    /// (the call-stack-bounded run loop in `dispatch.rs`) and pop the
830    /// result from the value stack via `pop_kinded`. Other kinds fall
831    /// through to a `RuntimeError` (`VMError::TypeError` is
832    /// `&'static str`-bound and incompatible with the format!-style
833    /// dynamic-kind error message; the convention used by the existing
834    /// `op_call_value` surfaces is `RuntimeError(format!(...))`). The
835    /// `HeapValue::HostClosure` variant referenced in pre-Wave-7 docs
836    /// has been deleted; only `ClosureRaw` survives in the
837    /// closure-dispatch path.
838    pub fn call_value_immediate_nb(
839        &mut self,
840        callee: &KindedSlot,
841        args: &[KindedSlot],
842        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
843    ) -> Result<KindedSlot, VMError> {
844        // Capture the call-stack depth BEFORE frame setup pushes a new
845        // frame. After the callee's `op_return` / `op_return_value`
846        // pops its frame, the call-stack depth returns to this saved
847        // value — `execute_until_call_depth(saved_depth, ctx)` is the
848        // canonical "drive callee to completion" loop (the playbook's
849        // notional `run_until_return` lives here under that name; see
850        // `dispatch.rs::execute_until_call_depth`).
851        let saved_call_depth = self.call_stack.len();
852
853        match callee.kind {
854            NativeKind::Ptr(shape_value::HeapKind::Closure) => {
855                // Recover `OwnedClosureBlock` via the §2.7.6 / Q8 heap
856                // dispatch path: `slot.as_heap_value()` returns
857                // `&HeapValue`, pattern-match the
858                // `HeapValue::ClosureRaw(block)` arm per ADR-005 §1
859                // single-discriminator. A `HeapKind::Closure` label
860                // with any other `HeapValue` payload is a
861                // construction-side bug at the producing
862                // `op_make_closure`; debug_assert in dev, surface as a
863                // RuntimeError in release (the post-§2.7.11 dispatch
864                // shell must not silently fabricate a kind — playbook
865                // §6 #6 polymorphic-fallthrough forbidden).
866                let block: &OwnedClosureBlock = match callee.slot.as_heap_value() {
867                    HeapValue::ClosureRaw(block) => block,
868                    other => {
869                        debug_assert!(
870                            false,
871                            "call_value_immediate_nb: HeapKind::Closure label with \
872                             non-ClosureRaw HeapValue payload: {:?}",
873                            other.type_name()
874                        );
875                        return Err(VMError::RuntimeError(format!(
876                            "call_value_immediate_nb: HeapKind::Closure label with \
877                             non-ClosureRaw payload: {}",
878                            other.type_name()
879                        )));
880                    }
881                };
882                // Recover the function-id from the typed closure
883                // header. `OwnedClosureBlock` has no safe public
884                // accessor for `function_id`; the canonical path is
885                // the unsafe `typed_closure_function_id(block.as_ptr())`
886                // helper used by the block's own `Debug` impl
887                // (`closure_raw.rs:215`). The block's borrow keeps
888                // the underlying header live for the duration of this
889                // read.
890                //
891                // SAFETY: `block` is a live `OwnedClosureBlock`
892                // (borrowed through the live `&HeapValue` returned by
893                // `as_heap_value()`); its `as_ptr()` points to a
894                // `TypedClosureHeader` block allocated by
895                // `alloc_typed_closure` per the construction
896                // invariant.
897                let function_id = unsafe { typed_closure_function_id(block.as_ptr()) };
898
899                // Frame setup. The B9 lockstep companion fields carry
900                // the closure-self share so `op_return` /
901                // `op_return_value` can release it via
902                // `drop_with_kind(bits, kind)` on frame teardown.
903                // `closure_heap_bits` is the raw slot bits (`Box<HeapValue>`
904                // pointer) and `closure_heap_kind` is the matching
905                // `NativeKind::Ptr(HeapKind::Closure)`.
906                //
907                // Round 13 T5 share-accounting fix (W17-vm-call-value-
908                // closure-kind-mismatch, audit doc
909                // `docs/cluster-audits/w17-vm-call-value-closure-kind-mismatch-audit.md`
910                // §4 Option B). The `callee` carrier owns one
911                // `Arc<HeapValue>` strong-count share — transferred
912                // from the stack via `pop_kinded` in the
913                // `dispatch_call_value_immediate` shell
914                // (`control_flow/mod.rs:408-409`). The carrier `Drop`
915                // releases that share at end of dispatch via
916                // `drop_with_kind`. The frame's
917                // `closure_heap_bits` companion ALSO releases via
918                // `drop_with_kind` at `op_return` / `op_return_value`
919                // teardown (`control_flow/mod.rs:712-726` / `:774-788`).
920                // Without an explicit `clone_with_kind` here the two
921                // releases retire one share more than was acquired —
922                // the closure `Arc<HeapValue>` reaches refcount 0
923                // before the closure-self binding's
924                // `Arc::decrement_strong_count` runs, freeing the
925                // header that `op_make_closure`'s producer share at
926                // Local 1 still references. On the next iteration
927                // `CloneLocal Local(1)` reads the dangling bits and
928                // races the allocator — surfacing as
929                // `HeapKind::Closure label with non-ClosureRaw payload`
930                // in debug or `Invalid function call` in release (the
931                // bogus `function_id` read from the freed header fails
932                // the `program.functions.get(func_id)` bounds check at
933                // `call_closure_with_nb_args_keepalive`).
934                //
935                // The §2.7.7 / Q9 retain-on-read primitive is the
936                // canonical kind-aware refcount bump — no tag decode,
937                // no `is_heap()` probe, no Bool-default fallback. Same
938                // share-balance pattern as
939                // `execute_function_with_named_args` (lines 246-250)
940                // which clones each named-arg into the positional vec.
941                super::vm_impl::stack::clone_with_kind(callee.slot.raw(), callee.kind);
942                self.call_closure_with_nb_args_keepalive(
943                    function_id,
944                    block,
945                    args,
946                    Some(callee.slot.raw()),
947                    Some(callee.kind),
948                )?;
949
950                // Drive the callee to completion. `execute_until_call_depth`
951                // returns when `self.call_stack.len() == saved_call_depth`
952                // (i.e. the callee's frame has been popped by `op_return`).
953                // The return value is left on the value stack by
954                // `op_return_value`; pop it via the kinded API so the
955                // share transfers cleanly into the result `KindedSlot`.
956                self.execute_until_call_depth(saved_call_depth, ctx)?;
957                let (bits, kind) = self.pop_kinded()?;
958                Ok(KindedSlot::new(ValueSlot::from_raw(bits), kind))
959            }
960            NativeKind::UInt64 => {
961                // Function-id callee: `callee.slot.raw()` is the
962                // function-id encoded as raw `u64` bits (§2.7.11 / Q12
963                // — `UInt64` is the §2.7.11 callee-classification kind
964                // for function references). Truncate to `u16` since
965                // `BytecodeProgram::functions` is indexed by `u16` and
966                // both Round 1 frame-setup helpers (`call_function_with_nb_args`,
967                // `call_closure_with_nb_args_keepalive`) take `func_id: u16`.
968                // A bits value that doesn't index into the function
969                // table surfaces as `VMError::InvalidCall` from
970                // `call_function_with_nb_args` itself (per its
971                // existing `program.functions.get(func_id as usize)
972                // .ok_or(VMError::InvalidCall)?` guard) — the playbook
973                // §8 surface-and-stop trigger ("UInt64 callee bits don't
974                // match a real function-id") routes through that path.
975                let function_id = callee.slot.raw() as u16;
976
977                self.call_function_with_nb_args(function_id, args)?;
978
979                // Drive callee to completion and pop the result; same
980                // pattern as the closure arm above.
981                self.execute_until_call_depth(saved_call_depth, ctx)?;
982                let (bits, kind) = self.pop_kinded()?;
983                Ok(KindedSlot::new(ValueSlot::from_raw(bits), kind))
984            }
985            // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
986            // ModuleFn callee — the slot bits are a `module_fn_id`
987            // (cast to u64), indexing the VM's `module_fn_table` per
988            // the `populate_module_objects` construction-side contract.
989            // Dispatch goes directly through `invoke_module_fn_id_stub`
990            // (sync `Typed` or async `TypedAsync` per the §2.7.4
991            // task-scheduler boundary in `vm_impl/modules.rs`). The
992            // dispatcher converts the `&[KindedSlot]` args at the
993            // boundary to the body's `&[u64]` slice + `ModuleContext`,
994            // then projects the `TypedReturn` back to a `KindedSlot`
995            // via `project_typed_return`. Pure-discriminator inline-
996            // scalar dispatch (no Arc bookkeeping on the callee
997            // bits — `clone_with_kind` / `drop_with_kind` arms are
998            // no-op for `HeapKind::ModuleFn`).
999            NativeKind::Ptr(shape_value::HeapKind::ModuleFn) => {
1000                let module_fn_id = callee.slot.raw() as usize;
1001                // `invoke_module_fn_id_stub` returns a fresh KindedSlot
1002                // whose share was minted by `project_typed_return`. We
1003                // return it directly; the dispatch shell at
1004                // `dispatch_call_value_immediate` transfers the share
1005                // into the caller's stack slot via `push_kinded` +
1006                // `mem::forget` on the result carrier.
1007                self.invoke_module_fn_id_stub(module_fn_id, args)
1008            }
1009            // Match is exhaustive: Closure, UInt64, ModuleFn,
1010            // all-others-error. No polymorphic fall-through that
1011            // fabricates kinds (W7 playbook §6 #6 forbidden). Per §8
1012            // surface-and-stop: trait-object closure dispatch
1013            // (`Ptr(HeapKind::TypedObject)` carrying a `dyn Trait`
1014            // vtable) is W9 TR territory and routes through this
1015            // RuntimeError until that wave lands.
1016            other => Err(VMError::RuntimeError(format!(
1017                "call_value_immediate_nb: callee must be \
1018                 NativeKind::Ptr(HeapKind::Closure), \
1019                 NativeKind::Ptr(HeapKind::ModuleFn), or NativeKind::UInt64, \
1020                 got {:?}",
1021                other
1022            ))),
1023        }
1024    }
1025
1026    /// Trampoline entry: call a closure by `func_id` with pre-extracted
1027    /// raw upvalue bits and raw args, returning the result as raw `u64`
1028    /// bits.
1029    ///
1030    /// **W7-cv-method (Round 3 close).** This is the **only** `_raw`
1031    /// survivor in `call_convention.rs` per ADR-006 §2.7.11
1032    /// migration-scope refinement: it is the §2.7.5 cross-crate
1033    /// stable-FFI consumer where the parallel-pair shape (raw `u64`
1034    /// data + `NativeKind`) is canonical. Consumers translate
1035    /// `&[KindedSlot]` → raw `u64` at the FFI boundary; this function
1036    /// is the inverse hop on the runtime side.
1037    ///
1038    /// Body wraps `args` as a transient `&[KindedSlot]` slice (no Arc
1039    /// bump — the JIT pre-incremented each share before crossing the
1040    /// boundary), constructs a fresh `OwnedClosureBlock` from
1041    /// `upvalue_bits` per the existing closure-construction convention
1042    /// (allocate → write each capture's bits at its layout offset →
1043    /// `OwnedClosureBlock::from_raw`), routes through
1044    /// [`call_closure_with_nb_args_keepalive`], drives the callee, pops
1045    /// the result via `pop_kinded`, and returns the bits as raw `u64`
1046    /// (the kind is discarded — the JIT caller knows the static return
1047    /// kind from the callee signature).
1048    ///
1049    /// **Ownership.** Each `(bits, kind)` in `upvalue_bits` carries a
1050    /// pre-incremented share. We transfer those shares into the new
1051    /// closure block via `write_capture_typed` (which stores the bit
1052    /// pattern without bumping the refcount). The `OwnedClosureBlock`
1053    /// then owns the captures' shares — its `Drop` walks the layout's
1054    /// capture masks and releases them. Same for `args`: the JIT
1055    /// pre-incremented; we hand each transient `KindedSlot` over by
1056    /// move (no clone), and `call_closure_with_nb_args_keepalive`
1057    /// transfers the shares into the new frame via `stack_write_kinded`.
1058    /// We `mem::forget` the transient args vec so its `Drop` does not
1059    /// double-free.
1060    pub fn jit_trampoline_call_closure(
1061        &mut self,
1062        func_id: u16,
1063        upvalue_bits: &[(u64, NativeKind)],
1064        args: &[(u64, NativeKind)],
1065        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
1066    ) -> Result<u64, VMError> {
1067        use shape_value::v2::closure_raw::{alloc_typed_closure, write_capture_raw_u64};
1068        use std::sync::Arc;
1069
1070        // Source the closure layout from the program's per-function
1071        // side-table (`closure_function_layouts[func_id]`). A `None`
1072        // entry means the function is not a closure — the JIT-side
1073        // `dispatch_call_via_trampoline_vm` should have routed bare
1074        // function callees through `call_value_immediate_nb` instead;
1075        // landing here with `None` is a JIT codegen bug. Surface as a
1076        // RuntimeError per W7 playbook §8.
1077        let layout_arc: Arc<shape_value::v2::closure_layout::ClosureLayout> = self
1078            .program
1079            .closure_function_layouts
1080            .get(func_id as usize)
1081            .and_then(|opt| opt.clone())
1082            .ok_or_else(|| {
1083                VMError::RuntimeError(format!(
1084                    "jit_trampoline_call_closure: no ClosureLayout for func_id {} \
1085                     (program.closure_function_layouts entry is None — JIT codegen bug)",
1086                    func_id
1087                ))
1088            })?;
1089
1090        debug_assert_eq!(
1091            upvalue_bits.len(),
1092            layout_arc.capture_count(),
1093            "jit_trampoline_call_closure: upvalue_bits.len() {} != layout.capture_count() {}",
1094            upvalue_bits.len(),
1095            layout_arc.capture_count()
1096        );
1097
1098        // Allocate a fresh closure block and write each capture's bits
1099        // at its layout offset. The JIT pre-incremented each heap-typed
1100        // share before crossing the FFI boundary — `write_capture_raw_u64`
1101        // stores the bit pattern without bumping the refcount, so the
1102        // shares transfer cleanly into the new block. The block's Drop
1103        // (via `release_typed_closure` triggered by `OwnedClosureBlock`)
1104        // releases each capture's share via the layout's heap/owned/
1105        // shared capture masks.
1106        //
1107        // SAFETY: `alloc_typed_closure` returns a freshly-zeroed block
1108        // sized for `layout_arc.total_heap_size()`; refcount is 1.
1109        // `write_capture_raw_u64` writes the 8-byte capture slot at
1110        // `layout.heap_capture_offset(i)` which is in-bounds for every
1111        // `i < capture_count()`. The `type_id = 0` placeholder matches
1112        // what the trampoline-side construction had pre-§2.7.11 (the
1113        // typed-closure machinery does not key dispatch on `type_id`
1114        // for trampoline-bound closures).
1115        let block = unsafe {
1116            let ptr = alloc_typed_closure(func_id, 0, &layout_arc);
1117            for (i, (bits, _kind)) in upvalue_bits.iter().enumerate() {
1118                write_capture_raw_u64(ptr, &layout_arc, i, *bits);
1119            }
1120            OwnedClosureBlock::from_raw(ptr, layout_arc)
1121        };
1122
1123        // Wrap args as a transient `&[KindedSlot]` slice. No Arc bump:
1124        // the JIT pre-incremented each share before crossing the FFI
1125        // boundary; the transient KindedSlots own those shares now,
1126        // and `call_closure_with_nb_args_keepalive` transfers them into
1127        // the new frame's locals via `stack_write_kinded`. We
1128        // `mem::forget` the vec at the end so its `Drop` does not
1129        // double-free.
1130        let kinded_args: Vec<KindedSlot> = args
1131            .iter()
1132            .map(|(bits, kind)| KindedSlot::new(ValueSlot::from_raw(*bits), *kind))
1133            .collect();
1134
1135        let saved_call_depth = self.call_stack.len();
1136        // No keep-alive carrier: the closure block lives for the
1137        // duration of this call via the local `block` binding (its
1138        // `Drop` at end-of-function releases it — but only after
1139        // `execute_until_call_depth` has returned, by which point the
1140        // callee's frame has been popped). B9 lockstep: both `None`.
1141        self.call_closure_with_nb_args_keepalive(func_id, &block, &kinded_args, None, None)?;
1142        std::mem::forget(kinded_args);
1143
1144        self.execute_until_call_depth(saved_call_depth, ctx)?;
1145        let (bits, _kind) = self.pop_kinded()?;
1146        // Return raw bits. The kind is discarded — the JIT caller
1147        // knows the static return kind from the callee signature.
1148        Ok(bits)
1149    }
1150
1151    /// Trampoline entry: dispatch a method call on a kinded receiver +
1152    /// kinded args, returning the result as raw `u64` bits.
1153    ///
1154    /// **W12-jit-call-method-shell-rebuild (Phase 3 cluster-0 Round 10 /
1155    /// 8B.2 close).** Sibling to [`jit_trampoline_call_closure`]; same
1156    /// §2.7.5 cross-crate stable-FFI consumer shape. The JIT-side
1157    /// `jit_call_method` shell pops `(bits, kind)` pairs from the JIT's
1158    /// `ctx.stack` + `ctx.stack_kinds` parallel-kind track per §2.7.7 / Q9,
1159    /// passes them across the FFI boundary as `&[(u64, NativeKind)]`
1160    /// pair-slices, and this function converts them to the kinded
1161    /// carrier form before delegating to
1162    /// [`dispatch_method_kinded`](Self::dispatch_method_kinded) — the
1163    /// §2.7.10 / Q11 kinded method-dispatch entry shared with
1164    /// `op_call_method`.
1165    ///
1166    /// **Pair-slice → KindedSlot conversion is single-direction** per
1167    /// the module-level docstring's "sole `_raw` survivor" rule. The
1168    /// pair-slice is the canonical §2.7.5 boundary shape; internally
1169    /// only `&[KindedSlot]` flows. Forbidden alternatives (per ADR-006
1170    /// §2.7.6 / Q8 + §2.7.10 / Q11):
1171    /// - parallel `&[NativeKind]` second-slice parameter (carrier-API-
1172    ///   bound rejection — kind goes on the carrier struct, not a
1173    ///   side-channel);
1174    /// - decoding receiver kind from `receiver.0` raw bits via tag-bit
1175    ///   probe (the deleted §2.7.7 #4 / #7 dispatch);
1176    /// - Bool-default kinded carrier for unknown receiver kind
1177    ///   (§2.7.7 #9 — the surface-and-stop discipline forbids this).
1178    ///
1179    /// **Ownership.** Each `(bits, kind)` pair carries a pre-incremented
1180    /// share installed by the JIT producer (per §2.7.7 retain-on-read
1181    /// semantics on the JIT-side stack). The transient `KindedSlot`
1182    /// carriers adopt those shares for the call duration. PHF handlers
1183    /// borrow-only (`&[KindedSlot]` per §2.7.10 / Q11), so the carriers
1184    /// retain ownership of the JIT-pre-incremented shares throughout
1185    /// dispatch. When the carriers `Drop` at end of scope, each kind's
1186    /// `drop_with_kind` releases its share — balancing the JIT-side
1187    /// retain-before-crossing pattern. The returned `KindedSlot`'s share
1188    /// is transferred back to the JIT caller as raw u64 bits via
1189    /// `mem::forget`; the kind is discarded — the JIT caller knows the
1190    /// static return kind from the callee method signature at the
1191    /// §2.7.5 stamp-at-compile-time producing site.
1192    ///
1193    /// **Lifetime accounting contrast vs. `jit_trampoline_call_closure`.**
1194    /// The closure trampoline's `mem::forget(kinded_args)` (line 1035)
1195    /// is because the args were transferred into the callee's frame
1196    /// locals via `stack_write_kinded` — the shares moved into the
1197    /// frame, so the transient carriers must NOT release them. Method
1198    /// dispatch's PHF handlers do not transfer the shares anywhere —
1199    /// they only borrow — so the transient carriers DO release at end
1200    /// of scope. Both patterns preserve §2.7.7 retain-on-read +
1201    /// drop-on-write discipline; the difference is which slot owns the
1202    /// share at the call's exit boundary.
1203    pub fn jit_trampoline_call_method(
1204        &mut self,
1205        method_name: &str,
1206        receiver: (u64, NativeKind),
1207        args: &[(u64, NativeKind)],
1208        ctx: Option<&mut shape_runtime::context::ExecutionContext>,
1209    ) -> Result<u64, VMError> {
1210        // Wrap receiver + args as transient `&[KindedSlot]` per the
1211        // §2.7.10 / Q11 dispatch-slice form (`args[0]` is the receiver,
1212        // `args[1..]` are the call args). No Arc bump: the JIT
1213        // pre-incremented each share before crossing the FFI boundary;
1214        // the transient KindedSlots adopt those shares, dispatch
1215        // borrow-only, and release on scope exit via `KindedSlot::Drop`.
1216        let mut kinded_args: Vec<KindedSlot> = Vec::with_capacity(args.len() + 1);
1217        let (rbits, rkind) = receiver;
1218        kinded_args.push(KindedSlot::new(ValueSlot::from_raw(rbits), rkind));
1219        for (bits, kind) in args.iter().copied() {
1220            kinded_args.push(KindedSlot::new(ValueSlot::from_raw(bits), kind));
1221        }
1222
1223        let result = self.dispatch_method_kinded(&kinded_args, method_name, ctx)?;
1224
1225        // Transfer the result share back to the JIT caller as raw bits.
1226        // The kind is discarded — the JIT caller knows the static return
1227        // kind from the callee method signature at the §2.7.5 stamp-at-
1228        // compile-time producing site.
1229        let bits = result.slot.raw();
1230        std::mem::forget(result);
1231
1232        // `kinded_args` drops here. `KindedSlot::Drop` dispatches on
1233        // each entry's kind and retires the JIT-pre-incremented share
1234        // via `drop_with_kind` — no bare `vw_drop`, no Bool-default
1235        // (forbidden §2.7.7 #9), no decode (forbidden §2.7.7 #4 / #7).
1236        Ok(bits)
1237    }
1238
1239    /// Fast-path frame setup: args are already on the value stack at
1240    /// `[self.sp - arg_count .. self.sp]` from the producing push
1241    /// opcodes (e.g. `LoadLocal*`, `PushConst`). The new frame's
1242    /// `base_pointer` is exactly `self.sp - arg_count`, so those
1243    /// slots — already carrying the right `(bits, kind)` pairs on the
1244    /// parallel-kind track — become the new frame's locals 0..arg_count
1245    /// in place, with no per-slot pop/write copy round-trip.
1246    ///
1247    /// Per W7 playbook §4 / W6.5 §3, the share lives once: each arg's
1248    /// strong-count share was installed into the slot by its producing
1249    /// opcode and stays in the slot across the frame transition. No
1250    /// `clone_with_kind`, no `drop_with_kind` — the slot is the share's
1251    /// home throughout.
1252    ///
1253    /// Omitted-arg locals (when `arg_count < locals_count`) are
1254    /// sentinel-filled with `(NONE_BITS, NativeKind::Bool)` per W6.5
1255    /// §2 Null/Unit row — Drop/Clone are no-ops on this pair so the
1256    /// pre-population is leak-free.
1257    ///
1258    /// The B9 lockstep companion fields `closure_heap_bits` /
1259    /// `closure_heap_kind` are both `None` — non-closure call.
1260    pub(crate) fn call_function_from_stack(
1261        &mut self,
1262        func_id: u16,
1263        arg_count: usize,
1264    ) -> Result<(), VMError> {
1265        let func = self
1266            .program
1267            .functions
1268            .get(func_id as usize)
1269            .ok_or(VMError::InvalidCall)?;
1270        let locals_count = func.locals_count as usize;
1271        let blob_hash = self.blob_hash_for_function(func_id);
1272        let entry_point = func.entry_point;
1273
1274        if self.sp < arg_count {
1275            return Err(VMError::StackUnderflow);
1276        }
1277
1278        let base_pointer = self.sp - arg_count;
1279        let needed = base_pointer + locals_count;
1280        if needed > self.stack.len() {
1281            // ADR-006 §2.7.7 / §2.7.8 lockstep growth: data + parallel
1282            // kind track grow together. Sentinel pair is `(NONE_BITS,
1283            // NativeKind::Bool)` — Drop/Clone are no-ops on this pair
1284            // so the pre-population window is leak-free.
1285            self.stack.resize_with(needed * 2 + 1, || Self::NONE_BITS);
1286            self.kinds.resize(needed * 2 + 1, NativeKind::Bool);
1287        }
1288
1289        let return_ip = self.ip;
1290        self.call_stack.push(CallFrame {
1291            return_ip,
1292            base_pointer,
1293            locals_count,
1294            function_id: Some(func_id),
1295            upvalues: None,
1296            blob_hash,
1297            closure_heap_bits: None,
1298            // ADR-006 §2.7.8 / Q10: lockstep with `closure_heap_bits`.
1299            // Non-closure fast path → both `None`.
1300            closure_heap_kind: None,
1301        });
1302
1303        // Sentinel-fill omitted-arg locals (W6.5 §2 Null/Unit row).
1304        // Slots `[base_pointer .. base_pointer + arg_count]` already
1305        // hold the pushed args; slots `[base_pointer + arg_count ..
1306        // base_pointer + locals_count]` may carry stale shares from
1307        // a prior frame's teardown. `stack_write_kinded` releases the
1308        // prior occupant via `drop_with_kind` before installing the
1309        // sentinel.
1310        for i in arg_count..locals_count {
1311            self.stack_write_kinded(base_pointer + i, Self::NONE_BITS, NativeKind::Bool);
1312        }
1313
1314        self.sp = base_pointer + locals_count;
1315        self.ip = entry_point;
1316        Ok(())
1317    }
1318}