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}