shape_vm/executor/mod.rs
1//! Virtual machine executor for Shape bytecode
2
3// Opcode category implementations (split into submodules)
4mod additional;
5mod arithmetic;
6mod async_ops;
7mod builtins;
8mod call_convention;
9mod comparison;
10mod control_flow;
11pub(crate) mod dispatch;
12mod exceptions;
13pub(crate) mod ic_fast_paths;
14mod jit_ops;
15mod logical;
16mod loops;
17pub(crate) mod objects;
18mod osr;
19mod resume;
20mod snapshot;
21mod stack_ops;
22pub mod state_builtins;
23pub mod time_travel;
24mod trait_object_ops;
25// W11-fup-C (Phase 3d, 2026-05-18): exposed `pub` so the JIT-side
26// `crates/shape-jit/src/ffi/v2/mod.rs` allocators can call
27// `v2_handlers::v2_array_detect::stamp_elem_type` + the
28// `ELEM_TYPE_*` constants at allocation time — mirror of the VM-side
29// `op_new_typed_array_*` stamp pattern (`v2_handlers/array.rs:40-81`).
30// Without the stamp the canonical `as_v2_typed_array` carrier-
31// recognition path (`v2_array_detect.rs:181-216`) reads
32// `_pad = 0 = ELEM_TYPE_UNKNOWN` and the print / method-dispatch
33// arms silently fall back to the non-typed scalar render (the
34// pre-fix empirical surface — `print(arr)` JIT output was the raw
35// pointer printed as a u64 scalar).
36pub mod v2_handlers;
37mod variables;
38pub(crate) mod vm_state_snapshot;
39mod window_join;
40
41// VM infrastructure modules
42pub mod debugger_integration;
43pub mod gc_integration;
44pub mod module_registry;
45pub mod printing;
46pub mod task_scheduler;
47pub mod typed_object_ops;
48pub mod utils;
49
50// Test module
51#[cfg(test)]
52mod tests;
53
54// Re-export async types for external use
55pub use async_ops::{AsyncExecutionResult, SuspensionInfo, WaitType};
56pub use control_flow::foreign_marshal;
57pub use control_flow::native_abi;
58pub use task_scheduler::{TaskScheduler, TaskStatus};
59
60/// Reserved future ID used to signal a snapshot suspension
61pub const SNAPSHOT_FUTURE_ID: u64 = u64::MAX;
62
63/// Error returned when a program requires permissions not granted by the host.
64#[derive(Debug, Clone)]
65pub enum PermissionError {
66 /// The program requires permissions not in the granted set.
67 InsufficientPermissions {
68 /// All permissions the program requires.
69 required: shape_abi_v1::PermissionSet,
70 /// Permissions the host granted.
71 granted: shape_abi_v1::PermissionSet,
72 /// Permissions required but not granted.
73 missing: shape_abi_v1::PermissionSet,
74 },
75 /// Linking failed before permission checking could occur.
76 LinkError(String),
77}
78
79impl std::fmt::Display for PermissionError {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 PermissionError::InsufficientPermissions { missing, .. } => {
83 let names: Vec<&str> = missing.iter().map(|p| p.name()).collect();
84 write!(
85 f,
86 "program requires permissions not granted: {}",
87 names.join(", ")
88 )
89 }
90 PermissionError::LinkError(msg) => write!(f, "link error: {msg}"),
91 }
92 }
93}
94
95impl std::error::Error for PermissionError {}
96
97/// Result of VM execution.
98///
99/// Wave-β R-misc migration (per ADR-006 §2.7 / Q7 + playbook §10
100/// E-execution row): the `Completed` variant carries a `KindedSlot` —
101/// the canonical post-`ValueWord` runtime-value carrier (raw bits +
102/// parallel `NativeKind`). Hosts that previously called methods on the
103/// returned `ValueWord` (e.g. `as_number_coerce`, `as_i64`, `as_str`)
104/// must dispatch on `result.kind()` and use the per-variant
105/// `KindedSlot::as_*` accessors per §2.7.6.
106#[derive(Debug, Clone)]
107pub enum ExecutionResult {
108 /// Execution completed normally with a typed value carrier.
109 Completed(shape_value::KindedSlot),
110 /// Execution suspended waiting for a future to resolve
111 Suspended {
112 /// The future ID that needs to be resolved
113 future_id: u64,
114 /// The instruction pointer to resume at
115 resume_ip: usize,
116 },
117}
118
119use std::collections::HashMap;
120use std::sync::Arc;
121use std::sync::atomic::AtomicU8;
122
123use crate::{
124 bytecode::{
125 BuiltinFunction, BytecodeProgram, FunctionBlob, FunctionHash, Instruction, Operand,
126 },
127 debugger::VMDebugger,
128 memory::{GCConfig, GarbageCollector},
129 tier::TierManager,
130};
131use shape_ast::data::Timeframe;
132
133use crate::constants::{DEFAULT_GC_TRIGGER_THRESHOLD, MAX_CALL_STACK_DEPTH, MAX_STACK_SIZE};
134// `KindedSlot` and `NativeKind` are the post-`ValueWord` runtime-value
135// carriers (ADR-006 §2.7); both used in the `ExecutionResult` /
136// `CallFrame` / `VirtualMachine` field types below. `HeapValue` /
137// `ValueSlot` / `VMError` are intentionally left unimported here —
138// fields that need them name the type via their fully-qualified path
139// to keep the executor `mod.rs` lean.
140use shape_value::{KindedSlot, NativeKind};
141/// VM configuration
142#[derive(Debug, Clone)]
143pub struct VMConfig {
144 /// Maximum stack size
145 pub max_stack_size: usize,
146 /// Maximum call depth
147 pub max_call_depth: usize,
148 /// Enable debug mode
149 pub debug_mode: bool,
150 /// Enable instruction tracing
151 pub trace_execution: bool,
152 /// Garbage collection configuration
153 pub gc_config: GCConfig,
154 /// Enable automatic garbage collection
155 pub auto_gc: bool,
156 /// GC trigger threshold (instructions between collections)
157 pub gc_trigger_threshold: usize,
158 /// Enable VM metrics collection (counters, tier/GC event ring buffers, histograms).
159 /// When false (default), `VirtualMachine.metrics` is `None` for zero overhead.
160 pub metrics_enabled: bool,
161 /// When true, automatically initialise the tracing GC heap (`shape-gc`) on
162 /// VM creation instead of relying on Arc reference counting.
163 ///
164 /// Requires the `gc` crate feature to be compiled in; otherwise this flag
165 /// is silently ignored.
166 pub use_tracing_gc: bool,
167}
168
169impl Default for VMConfig {
170 fn default() -> Self {
171 Self {
172 max_stack_size: MAX_STACK_SIZE,
173 max_call_depth: MAX_CALL_STACK_DEPTH,
174 debug_mode: false,
175 trace_execution: false,
176 gc_config: GCConfig::default(),
177 auto_gc: true,
178 gc_trigger_threshold: DEFAULT_GC_TRIGGER_THRESHOLD,
179 metrics_enabled: false,
180 use_tracing_gc: false,
181 }
182 }
183}
184
185/// Call frame for function calls
186#[derive(Debug)]
187pub struct CallFrame {
188 /// Return address
189 pub return_ip: usize,
190 /// Base pointer into the unified value stack where this frame's locals start
191 pub base_pointer: usize,
192 /// Number of locals
193 pub locals_count: usize,
194 /// Function index
195 pub function_id: Option<u16>,
196 /// Upvalues captured by this closure (None for regular functions).
197 ///
198 /// SURFACE (phase-2c): the legacy `shape_value::Upvalue` carrier was
199 /// deleted during the strict-typing bulldozer (the v1 ValueWord-tagged
200 /// closure-capture word). The replacement is the v2 typed closure
201 /// surface (`shape_value::v2::closure_raw::OwnedClosureBlock` /
202 /// `ClosureLayout`), but the wiring through `CallFrame` is part of
203 /// the §2.7.8 / Q10 cell-storage extension scheduled for
204 /// `B6-variables-loadptr` / `B7-closure-cells`. Until then this field
205 /// is a `Vec<u64>` of raw-bit captures, matching the existing layout
206 /// in `closure_raw::ClosureCell`. Consumers in
207 /// `executor/variables/mod.rs`, `executor/gc_integration.rs`,
208 /// `executor/state_builtins/introspection.rs`, and `crates/shape-vm/
209 /// src/remote.rs` are pre-existing Wave-α-broken and migrate together
210 /// with this surface — see ADR-006 §2.7.4 deferral pattern.
211 pub upvalues: Option<Vec<u64>>,
212 /// Content hash of the function blob being executed (for content-addressed state capture).
213 /// `None` for programs compiled without content-addressed metadata.
214 pub blob_hash: Option<FunctionHash>,
215 /// WB2.3 retain-on-read: optional owning `ValueWord` bits for the
216 /// closure HeapValue that backs this frame's `upvalues`. When the
217 /// frame is a closure call, `CaptureKind::OwnedMutable` / `Shared`
218 /// captures in `upvalues` are raw `*mut ValueWord` / `*const
219 /// SharedCell` pointer bits into this block's allocation — the
220 /// frame must hold this share so the block outlives the callee's
221 /// pointer dereferences.
222 ///
223 /// `None` for regular function calls and host-closure calls.
224 /// Released via `drop_with_kind(bits, kind)` on frame-pop (see
225 /// `op_return` / `op_return_value` cleanup) using the lockstep
226 /// `closure_heap_kind` companion below.
227 pub closure_heap_bits: Option<u64>,
228 /// ADR-006 §2.7.8 / Q10 — lockstep `NativeKind` companion for
229 /// `closure_heap_bits`. Both fields are `Some` together or `None`
230 /// together at every observable boundary; mixed states are a bug.
231 /// When `closure_heap_bits = Some(bits)`, this carries the
232 /// `NativeKind` that the teardown path (`op_return` /
233 /// `op_return_value`) feeds into `drop_with_kind(bits, kind)` —
234 /// replacing the forbidden `vw_drop(bits)` (§2.7.7 #8) and the
235 /// forbidden Bool-default fallback (§2.7.7 #9). For closure calls
236 /// the kind is `NativeKind::Ptr(HeapKind::Closure)` (the
237 /// `closure_heap_bits` always come from a TAG_HEAP `ValueWord`
238 /// pointing to a `HeapValue::ClosureRaw`).
239 pub closure_heap_kind: Option<shape_value::NativeKind>,
240}
241
242/// Function pointer type for JIT-compiled functions.
243/// `ctx` is a mutable pointer to VM execution context (e.g., stack base).
244/// `args` is a pointer to the argument buffer.
245/// Returns a NaN-boxed result as raw u64 bits.
246#[cfg(feature = "jit")]
247pub type JitFnPtr = unsafe extern "C" fn(*mut u8, *const u8) -> u64;
248
249/// Linked foreign-function handles.
250///
251/// Dynamic language runtimes are compiled/invoked through extension plugins.
252/// Native ABI entries (`extern "C"`) are linked directly through the VM's
253/// internal C ABI path.
254#[derive(Clone)]
255pub(crate) enum ForeignFunctionHandle {
256 Runtime {
257 runtime: std::sync::Arc<shape_runtime::plugins::language_runtime::PluginLanguageRuntime>,
258 compiled: shape_runtime::plugins::language_runtime::CompiledForeignFunction,
259 },
260 Native(std::sync::Arc<control_flow::native_abi::NativeLinkedFunction>),
261}
262
263/// The Shape virtual machine
264pub struct VirtualMachine {
265 /// Configuration
266 config: VMConfig,
267
268 /// The program being executed
269 pub(crate) program: BytecodeProgram,
270
271 /// Instruction pointer
272 ip: usize,
273
274 /// Unified value stack (pre-allocated, raw u64: 8 bytes per slot).
275 /// Locals live in register windows on this stack.
276 /// Each slot stores the raw bit pattern of a typed value, interpreted
277 /// according to the parallel `kinds` track. Ownership of any embedded
278 /// Arc refcounts is managed manually via `push_kinded`/`pop_kinded`/
279 /// `read_owned_kinded`/`stack_write_kinded` helpers (ADR-006 §2.7.7).
280 pub(crate) stack: Vec<u64>,
281
282 /// Parallel kind track (ADR-006 §2.7.7 / Q9).
283 ///
284 /// `kinds[i]` is the `NativeKind` interpretation of `stack[i]`. Index
285 /// invariant: `stack.len() == kinds.len()` at every API boundary.
286 /// WB2.4 retain-on-read uses this track for kind-aware clone/drop
287 /// dispatch (`clone_with_kind` / `drop_with_kind`); the deleted
288 /// tag_bits dispatch and the deleted `is_heap()` call do not run here.
289 pub(crate) kinds: Vec<shape_value::NativeKind>,
290
291 /// Stack pointer — logical top of the value stack.
292 /// `stack[0..sp]` are live values; `stack[sp..]` is pre-allocated dead space.
293 pub(crate) sp: usize,
294
295 /// ModuleBinding variables (raw u64 bit patterns; see `stack` comment).
296 ///
297 /// Lockstep with `module_binding_kinds` per ADR-006 §2.7.8 / Q10:
298 /// `module_bindings.len() == module_binding_kinds.len()` at every
299 /// observable boundary. Pad/resize/clear sites must update both vecs
300 /// together — see `module_binding_pad_to_kinded` for the canonical
301 /// resize helper.
302 pub(crate) module_bindings: Vec<u64>,
303
304 /// Parallel `NativeKind` track for `module_bindings` (ADR-006 §2.7.8 /
305 /// Q10). `module_binding_kinds[i]` is the `NativeKind` interpretation
306 /// of `module_bindings[i]`. Index invariant: lengths agree at every
307 /// API boundary.
308 ///
309 /// VM teardown dispatches `drop_with_kind(bits[i], kind[i])` per slot
310 /// — the kind-aware counterpart of the deleted `vw_drop`/
311 /// `vw_drop_slice` (forbidden #8 per §2.7.7). Slots written by typed-
312 /// scalar Store opcodes (`StoreModuleBindingI64` / `…F64` / `…Bool`
313 /// / etc.) carry their statically-known kind; PTR slots get the
314 /// matching `NativeKind::Ptr(HeapKind::*)` from the producer. Slots
315 /// pre-initialised by the resize-pad path use `NativeKind::Bool` as
316 /// the no-op-on-drop sentinel — the same convention as the stack's
317 /// dead-space pre-init (`init.rs:31`). This is **not** a Bool-default
318 /// fallback in the §2.7.7 #9 sense: every actual write threads the
319 /// caller's known kind, and the sentinel only persists for slots
320 /// that never received a store.
321 pub(crate) module_binding_kinds: Vec<NativeKind>,
322
323 /// Track A.1C.3: indices of module-binding slots that were
324 /// promoted to `Arc<parking_lot::Mutex<ValueWord>>` via
325 /// `AllocSharedModuleBinding`. Each tracked slot holds raw
326 /// `Arc::into_raw(...)` pointer bits — NOT a NaN-tagged ValueWord —
327 /// and must be reclaimed via `Arc::from_raw` once at VM drop. The
328 /// top-level `Drop` impl on VM consults this set before iterating
329 /// `module_bindings`.
330 pub(crate) shared_module_bindings: std::collections::HashSet<usize>,
331
332 /// Call stack
333 call_stack: Vec<CallFrame>,
334
335 /// Loop stack for break/continue
336 loop_stack: Vec<LoopContext>,
337 /// Timeframe stack for timeframe context
338 timeframe_stack: Vec<Option<Timeframe>>,
339
340 /// Integrated debugger
341 debugger: Option<VMDebugger>,
342
343 /// Garbage collector
344 gc: GarbageCollector,
345
346 /// Instruction counter (used for interrupt checking)
347 instruction_count: usize,
348
349 /// Exception handler stack for try/catch blocks
350 exception_handlers: Vec<ExceptionHandler>,
351
352 /// Builtin schema IDs for fixed-layout runtime objects (AnyError, TraceFrame, etc.)
353 pub(crate) builtin_schemas: shape_runtime::type_schema::BuiltinSchemaIds,
354
355 /// Last error location (line number) for LSP integration
356 /// Set by enrich_error_with_location when an error occurs
357 last_error_line: Option<u32>,
358
359 /// Last error file path for LSP integration
360 /// Set by enrich_error_with_location when an error occurs
361 last_error_file: Option<String>,
362
363 /// Uncaught exception payload captured at VM boundary.
364 ///
365 /// Set when an exception escapes with no handler so hosts can render
366 /// structured AnyError output without reparsing plain strings.
367 last_uncaught_exception: Option<KindedSlot>,
368
369 /// Whether module-level initialization code has been executed.
370 /// Used by `execute_function_by_name` to ensure module bindings
371 /// are initialized before calling the target function.
372 module_init_done: bool,
373
374 /// Output capture buffer for testing
375 /// When Some, print output is captured here instead of going to stdout
376 output_buffer: Option<Vec<String>>,
377
378 /// Extension module registry — single source of truth for all extension modules.
379 /// Used by extension dispatch, auto-available module_bindings, and LSP completions.
380 module_registry: shape_runtime::module_exports::ModuleExportRegistry,
381
382 /// Table of module-function entries indexed by usize ID.
383 /// `ValueWord::ModuleFunction(id)` references this table for dispatch.
384 ///
385 /// Phase 4c.3: entries are now sum-typed
386 /// (`Typed` / `TypedAsync` / `Legacy`) so the dispatch path can
387 /// route typed-return functions through a path that skips the
388 /// body-side `TypedReturn → ValueWord` round-trip.
389 module_fn_table: Vec<shape_runtime::module_exports::ModuleFnEntry>,
390
391 /// Runtime function name → index lookup for UFCS dispatch.
392 /// Populated after program load. Used by handle_object_method to find
393 /// type-scoped impl methods (e.g., "DuckDbQuery::filter") at runtime.
394 pub(crate) function_name_index: HashMap<String, u16>,
395
396 /// Method intrinsics for fast dispatch on typed Objects.
397 /// Populated from ModuleExports.method_intrinsics during module registration.
398 /// Checked in handle_object_method() after built-in methods, before UFCS.
399 extension_methods: HashMap<String, HashMap<String, shape_runtime::module_exports::ModuleFn>>,
400
401 /// Cache of resolved merged schemas: (left_id, right_id) → merged_id
402 merged_schema_cache: HashMap<(u32, u32), u32>,
403
404 /// Interrupt flag set by Ctrl+C handler (0 = none, >0 = interrupted)
405 interrupt: Arc<AtomicU8>,
406
407 /// Counter for generating unique future IDs (for SpawnTask).
408 ///
409 /// # Safety (single-threaded access)
410 ///
411 /// This is a plain `u64` rather than an `AtomicU64` because the VM executor
412 /// is inherently single-threaded: `VirtualMachine` is `!Sync` and all
413 /// execution happens on the thread that owns the VM instance. The counter
414 /// is only mutated by `next_future_id()` which requires `&mut self`,
415 /// guaranteeing exclusive access at compile time.
416 future_id_counter: u64,
417
418 /// Stack of async scopes for structured concurrency.
419 /// Each entry is a list of Future IDs spawned within that scope.
420 /// AsyncScopeEnter pushes a new Vec; AsyncScopeExit pops and cancels.
421 async_scope_stack: Vec<Vec<u64>>,
422
423 /// Task scheduler for async host runtime.
424 /// Stores spawned callables and tracks their completion status.
425 pub(crate) task_scheduler: task_scheduler::TaskScheduler,
426
427 /// Compiled foreign function handles (linked at pre-execution time).
428 /// Index corresponds to program.foreign_functions index.
429 pub(crate) foreign_fn_handles: Vec<Option<ForeignFunctionHandle>>,
430
431 /// Content hashes for each function, indexed by function_id.
432 /// Populated from `BytecodeProgram.content_addressed` or `LinkedProgram`.
433 /// `None` entries mean the function has no content-addressed metadata.
434 function_hashes: Vec<Option<FunctionHash>>,
435
436 /// Raw byte representation of `function_hashes` for passing to `ModuleContext`.
437 /// Kept in sync with `function_hashes`; avoids per-call allocation when
438 /// constructing `ModuleContext` (which uses `[u8; 32]` to avoid a dependency
439 /// on `FunctionHash`).
440 function_hash_raw: Vec<Option<[u8; 32]>>,
441
442 /// Reverse lookup for hash-first execution identity.
443 /// Maps function blob hash -> runtime function ID.
444 function_id_by_hash: HashMap<FunctionHash, u16>,
445
446 /// Entry points for each function, indexed by function_id.
447 /// Used to compute `local_ip = ip - function_entry_points[function_id]`
448 /// for content-addressed snapshot frames.
449 function_entry_points: Vec<usize>,
450
451 /// Effective execution entry IP for the currently loaded program.
452 /// Normal bytecode starts at 0; linked content-addressed programs start
453 /// at the entry function's `entry_point`.
454 program_entry_ip: usize,
455
456 /// Optional resource usage tracker for sandboxed execution.
457 /// When set, the dispatch loop calls `tick_instruction()` each cycle.
458 pub resource_usage: Option<crate::resource_limits::ResourceUsage>,
459
460 /// Time-travel debugger for recording and navigating VM state history.
461 /// `None` when time-travel debugging is not active.
462 pub(crate) time_travel: Option<time_travel::TimeTravel>,
463
464 /// GC heap (only present when `gc` feature is enabled).
465 #[cfg(feature = "gc")]
466 gc_heap: Option<shape_gc::GcHeap>,
467
468 /// Whether selective JIT compilation has been applied to the loaded program.
469 #[cfg(feature = "jit")]
470 jit_compiled: bool,
471
472 /// JIT dispatch table: function_id → extern "C" function pointer.
473 /// Populated by external JIT compilers (e.g., shape-jit) via `register_jit_function`.
474 #[cfg(feature = "jit")]
475 jit_dispatch_table: std::collections::HashMap<u16, JitFnPtr>,
476
477 /// Tiered compilation manager. Tracks per-function call counts and
478 /// coordinates background JIT compilation via channels.
479 /// `None` when tiered compilation is disabled.
480 tier_manager: Option<TierManager>,
481
482 /// Pending resume snapshot. Set by `state.resume()` stdlib function via
483 /// the `set_pending_resume` callback on `ModuleContext`. Consumed by the
484 /// dispatch loop after the current instruction completes.
485 ///
486 /// W17-state-tier-roundtrip (§2.7.4 + §2.7.5.1, Phase 2d Wave 3,
487 /// 2026-05-12): The state.resume body in
488 /// `state_builtins/introspection.rs` calls `set_pending_resume` (when
489 /// `ModuleContext.set_pending_resume` is wired) to queue the
490 /// snapshot KindedSlot here. `apply_pending_resume` consumes the
491 /// queue on the next dispatch tick; the actual resume reconstruction
492 /// path (decode the typed-object VmState payload → rebuild
493 /// stack/locals via `serializable_to_slot`) requires a typed-object
494 /// field-decode helper that lands with W17-marshal-return-arms.
495 /// Until that lands, `apply_pending_resume` returns a structured
496 /// `VMError::NotImplemented` carrying the `PHASE_2C_SNAPSHOT_SURFACE`
497 /// string (`executor/resume.rs:55`).
498 pub(crate) pending_resume: Option<KindedSlot>,
499
500 /// Pending single-frame resume data. Set by `state.resume_frame()` to
501 /// override IP and locals after function invocation sets up the call frame.
502 pub(crate) pending_frame_resume: Option<FrameResumeData>,
503
504 /// Optional VM metrics collector. `None` when `VMConfig.metrics_enabled`
505 /// is false (the default), giving zero per-instruction overhead.
506 pub metrics: Option<crate::metrics::VmMetrics>,
507
508 /// Per-function feedback vectors for inline cache profiling.
509 /// Indexed by function_id. None means no feedback collected for that function.
510 /// Only populated when tiered compilation is enabled.
511 feedback_vectors: Vec<Option<crate::feedback::FeedbackVector>>,
512
513 /// Megamorphic property lookup cache. Used when a property access site has
514 /// seen too many different schemas (>4 targets) and IC state is Megamorphic.
515 megamorphic_cache: crate::megamorphic_cache::MegamorphicCache,
516
517 /// Shape transition table + transition log owned by this VM.
518 ///
519 /// Replaces the process-global `GLOBAL_SHAPE_TABLE` / `SHAPE_TRANSITION_LOG`
520 /// statics. The VM installs this handle as the ambient
521 /// `shape_value::current_shape_table()` around every execution entry
522 /// point so HashMapData helpers (both VM-side and JIT-FFI-side) can
523 /// reach it without passing `&mut vm` through raw `extern "C"` calls.
524 pub(crate) shape_table: std::sync::Arc<shape_value::ShapeTableHandle>,
525}
526
527/// Data for resuming a single call frame mid-function.
528///
529/// SURFACE (phase-2c): consumed by `apply_pending_frame_resume` which is
530/// a Phase-2c stub (snapshot subsystem deferral, ADR-006 §2.7.4). Locals
531/// are carried as `Vec<KindedSlot>` so the rebuild path threads
532/// `NativeKind` for each local through the resumed frame.
533pub(crate) struct FrameResumeData {
534 /// IP offset within the function to resume at.
535 pub ip_offset: usize,
536 /// Locals to restore in the resumed frame.
537 pub locals: Vec<KindedSlot>,
538}
539
540/// Exception handler for try/catch blocks
541#[derive(Debug, Clone)]
542struct ExceptionHandler {
543 /// Instruction pointer to jump to on exception
544 catch_ip: usize,
545 /// Stack size when handler was set up (for unwinding)
546 stack_size: usize,
547 /// Call stack depth when handler was set up
548 call_depth: usize,
549}
550
551/// Loop context for break/continue
552#[derive(Debug)]
553struct LoopContext {
554 /// Start of loop body (for continue)
555 start: usize,
556 /// End of loop (for break)
557 end: usize,
558}
559
560/// Debug VM state snapshot for the debugger
561#[derive(Debug)]
562pub struct DebugVMState {
563 /// Current instruction pointer
564 pub ip: usize,
565 /// Call stack depth
566 pub call_stack_depth: usize,
567}
568
569pub(crate) mod vm_impl;
570
571/// Drop implementation for VirtualMachine.
572///
573/// Releases the strong-count share that each live stack slot and each
574/// module-binding slot owns over its heap-tagged payload. Per ADR-006
575/// §2.7.7 / §2.7.8, the stack and the module-binding store each carry a
576/// parallel `Vec<NativeKind>` track; teardown dispatches
577/// `drop_with_kind(bits, kind)` per slot — the kind-aware counterpart of
578/// the deleted `vw_drop_slice` call (forbidden #8 per §2.7.7).
579///
580/// The `shared_module_bindings` Arc reclamation still runs first because
581/// those slots hold raw `Arc::into_raw` pointer bits (not heap-tagged
582/// values) and the producer is the unique strong owner — the kind-aware
583/// drop loop over `module_bindings` skips them because the slot bits
584/// have been zeroed and `drop_with_kind` is a no-op on the zero bit
585/// pattern.
586impl Drop for VirtualMachine {
587 fn drop(&mut self) {
588 // Track A.1C.3: release Shared module-binding Arcs before
589 // draining the kinded bindings. These slots hold raw
590 // `Arc::into_raw(Arc::new(SharedCell))` pointer bits — they
591 // must be reclaimed via `Arc::from_raw`, not via the
592 // kinded-drop dispatch.
593 use shape_value::v2::closure_layout::SharedCell;
594 for &idx in &self.shared_module_bindings {
595 if idx >= self.module_bindings.len() {
596 continue;
597 }
598 let bits = self.module_bindings[idx];
599 self.module_bindings[idx] = 0u64;
600 // Zero the parallel kind slot to a no-op-on-drop sentinel
601 // so the lockstep loop below treats the cleared bits as a
602 // dead slot. Without this, a stale `Ptr(HeapKind::*)` kind
603 // would survive the bits-clear and double-release on the
604 // generic loop pass.
605 if idx < self.module_binding_kinds.len() {
606 self.module_binding_kinds[idx] = NativeKind::Bool;
607 }
608 let cell_ptr = bits as *const SharedCell;
609 if cell_ptr.is_null() {
610 continue;
611 }
612 // SAFETY: `cell_ptr` was produced by
613 // `Arc::into_raw(Arc::new(...))` in
614 // `op_alloc_shared_module_binding` and this is the unique
615 // release point for the strong share owned by the module-
616 // bindings slot. Capture-side Arc shares owned by still-
617 // live closures stay alive independently; the underlying
618 // SharedCell persists until every share is dropped.
619 unsafe {
620 drop(std::sync::Arc::from_raw(cell_ptr));
621 }
622 }
623 self.shared_module_bindings.clear();
624
625 // Release the live stack window. `self.sp` is the high-water
626 // mark of owned slots; everything above is already NONE_BITS
627 // sentinels (per-opcode cleanup invariant). Kind comes from
628 // the parallel `Vec<NativeKind>` track — ADR-006 §2.7.7
629 // dispatch surface.
630 let live = self.sp.min(self.stack.len()).min(self.kinds.len());
631 for i in 0..live {
632 let bits = self.stack[i];
633 let kind = self.kinds[i];
634 vm_impl::stack::drop_with_kind(bits, kind);
635 self.stack[i] = Self::NONE_BITS;
636 self.kinds[i] = NativeKind::Bool;
637 }
638
639 // Release every module binding via the parallel-kind track
640 // (ADR-006 §2.7.8 / Q10). `drop_with_kind` is a no-op on
641 // inline-scalar kinds (Int*/UInt*/Bool/Float64) so typed-scalar
642 // bindings cost a kind-check and return; heap-bearing bindings
643 // (`NativeKind::Ptr(HeapKind::*)` / `NativeKind::String`)
644 // release exactly one strong-count share via the matching
645 // `Arc::decrement_strong_count::<T>`. This closes the R-misc
646 // Wave-β "non-shared module bindings hold one strong share each
647 // that is not released at VM teardown" leak surface — once the
648 // kind track is populated lockstep with the bits track, the
649 // generic teardown sees the same kind every other §2.7.7
650 // dispatch surface uses.
651 //
652 // Defensive `min()` walks: if a producer push site grew
653 // `module_bindings` without growing `module_binding_kinds`
654 // (B6-round-2 territory not yet migrated), the lockstep
655 // invariant is violated. The debug-build assertion below
656 // surfaces such drift; release builds walk the prefix that
657 // both vecs cover. Prefix slots that exceed the kinds-vec
658 // length keep the legacy "leak rather than misdispatch"
659 // disposition until B6-round-2 lands the kind threading —
660 // explicitly NOT a Bool-default fallback (§2.7.7 #9 forbidden,
661 // §2.7.8 forbidden-shapes "Transitional Bool-default
662 // fallbacks"): the unwalked tail is the kind-source SURFACE,
663 // not a silent kind-fabrication.
664 debug_assert_eq!(
665 self.module_bindings.len(),
666 self.module_binding_kinds.len(),
667 "ADR-006 §2.7.8 / Q10 lockstep invariant violated at \
668 VirtualMachine::Drop: module_bindings.len() ({}) != \
669 module_binding_kinds.len() ({}). A push/resize site in \
670 cluster-B-round-2 territory (executor/variables/mod.rs) \
671 grew the bits vec without growing the kinds vec.",
672 self.module_bindings.len(),
673 self.module_binding_kinds.len(),
674 );
675 let bound = self
676 .module_bindings
677 .len()
678 .min(self.module_binding_kinds.len());
679 for i in 0..bound {
680 let bits = self.module_bindings[i];
681 let kind = self.module_binding_kinds[i];
682 vm_impl::stack::drop_with_kind(bits, kind);
683 self.module_bindings[i] = Self::NONE_BITS;
684 self.module_binding_kinds[i] = NativeKind::Bool;
685 }
686 // Zero any tail slots that exceeded the kinds-vec length so
687 // post-Drop reads (debug print, snapshot enumeration) do not
688 // see dangling pointer bits. The corresponding strong-count
689 // share leaks until the kinds vec catches up — that's the
690 // remaining B6-round-2 work, not the §2.7.8 structural
691 // extension this Drop is part of.
692 for slot in self.module_bindings[bound..].iter_mut() {
693 *slot = Self::NONE_BITS;
694 }
695 }
696}
697
698/// ADR-006 §2.7.8 / Q10 kinded module-binding accessors.
699///
700/// The §2.7.8 cell-storage extension grew `VirtualMachine.module_bindings`
701/// from a bare `Vec<u64>` to a `Vec<u64>` + parallel `Vec<NativeKind>`
702/// pair (`module_binding_kinds`). These methods are the lockstep-safe
703/// API every consumer SHOULD use; direct field access remains in place
704/// for the Wave-β B6-round-2 migration window but is replaced site-by-
705/// site as those handlers are rewritten.
706///
707/// The dispatch tables match `vm_impl::stack::clone_with_kind` /
708/// `drop_with_kind` exactly — same retain-on-read primitives the stack
709/// and `KindedSlot` use. No `vw_clone` (forbidden #8 per §2.7.7), no
710/// `is_heap` probe (forbidden #7), no Bool-default fallback when a
711/// kind-source gap appears (§2.7.7 #9, §2.7.8 forbidden-shapes).
712impl VirtualMachine {
713 /// Grow the module-binding store so that `index` is in bounds. Pads
714 /// the bits vec with `NONE_BITS` and the kinds vec with the no-op-on-
715 /// drop sentinel `NativeKind::Bool` — same convention as the stack's
716 /// dead-space pre-init in `init.rs:31`. Both vecs grow together so
717 /// the §2.7.8 lockstep invariant `module_bindings.len() ==
718 /// module_binding_kinds.len()` holds at the call boundary.
719 ///
720 /// The sentinel kind is **not** a Bool-default fallback in the §2.7.7
721 /// #9 sense: it marks "no value has ever been written to this slot",
722 /// not "we don't know the kind of an existing heap-bearing payload".
723 /// The first real write replaces it via `module_binding_write_kinded`.
724 #[inline]
725 pub(crate) fn module_binding_pad_to_kinded(&mut self, index: usize) {
726 while self.module_bindings.len() <= index {
727 self.module_bindings.push(Self::NONE_BITS);
728 self.module_binding_kinds.push(NativeKind::Bool);
729 }
730 debug_assert_eq!(
731 self.module_bindings.len(),
732 self.module_binding_kinds.len(),
733 "ADR-006 §2.7.8 / Q10 lockstep invariant",
734 );
735 }
736
737 /// Write a fresh kinded value into `module_bindings[index]`,
738 /// releasing the previous occupant via `drop_with_kind` (ADR-006
739 /// §2.7.8 / Q10 retain-on-overwrite). Mirrors `stack_write_kinded`
740 /// in `vm_impl/stack.rs:374`.
741 ///
742 /// **Ownership**: the new slot owns the strong-count share
743 /// transferred in by the caller. The caller MUST have retained the
744 /// share before calling (e.g. via `clone_with_kind` on the source
745 /// slot, or a fresh `Arc::into_raw`). The previous occupant's
746 /// share is released here and MUST NOT be accessed by the caller
747 /// afterwards.
748 #[inline]
749 pub(crate) fn module_binding_write_kinded(
750 &mut self,
751 index: usize,
752 bits: u64,
753 kind: NativeKind,
754 ) {
755 self.module_binding_pad_to_kinded(index);
756 let old_bits = self.module_bindings[index];
757 let old_kind = self.module_binding_kinds[index];
758 vm_impl::stack::drop_with_kind(old_bits, old_kind);
759 self.module_bindings[index] = bits;
760 self.module_binding_kinds[index] = kind;
761 }
762
763 /// Read the raw bits + kind at `module_bindings[index]` as a borrow
764 /// (no refcount change). The slot retains ownership of the share;
765 /// the caller MUST NOT drop the returned bits. Mirrors
766 /// `stack_read_kinded_raw` in `vm_impl/stack.rs:366`.
767 ///
768 /// Returns `(0, NativeKind::Bool)` for indices past the vec end —
769 /// matches the legacy `Vec<u64>` "uninitialised reads as zero"
770 /// behaviour with the no-op-on-drop kind sentinel paired in.
771 #[inline]
772 pub(crate) fn module_binding_read_kinded_raw(&self, index: usize) -> (u64, NativeKind) {
773 if index >= self.module_bindings.len() {
774 return (0u64, NativeKind::Bool);
775 }
776 debug_assert_eq!(
777 self.module_bindings.len(),
778 self.module_binding_kinds.len(),
779 "ADR-006 §2.7.8 / Q10 lockstep invariant violated at \
780 module_binding_read_kinded_raw",
781 );
782 // The lockstep invariant has been observed; if the kinds vec
783 // is short the bounded access falls back to the no-op sentinel
784 // rather than panicking on a release build.
785 let kind = self
786 .module_binding_kinds
787 .get(index)
788 .copied()
789 .unwrap_or(NativeKind::Bool);
790 (self.module_bindings[index], kind)
791 }
792
793 /// Read an **owning share** of `module_bindings[index]` as a
794 /// `KindedSlot`. Bumps the underlying `Arc<T>` strong-count via
795 /// `clone_with_kind` so the returned `KindedSlot` has an
796 /// independent share; the binding slot itself stays live. Mirrors
797 /// `read_owned_kinded` in `vm_impl/stack.rs:354`.
798 ///
799 /// Use this at every site that hands a binding to a runtime-tier
800 /// `KindedSlot` carrier (host-API `module_bindings()` enumeration,
801 /// snapshot serialisation, etc.).
802 #[inline]
803 pub(crate) fn module_binding_read_owned_kinded(&self, index: usize) -> KindedSlot {
804 let (bits, kind) = self.module_binding_read_kinded_raw(index);
805 vm_impl::stack::clone_with_kind(bits, kind);
806 KindedSlot::new(shape_value::ValueSlot::from_raw(bits), kind)
807 }
808
809 /// Take ownership of `module_bindings[index]`, replacing it with
810 /// the zero/Bool sentinel. Does NOT drop — the caller owns the
811 /// returned bits. Mirrors `stack_take_kinded` in
812 /// `vm_impl/stack.rs:385`.
813 #[inline]
814 pub(crate) fn module_binding_take_kinded(&mut self, index: usize) -> (u64, NativeKind) {
815 if index >= self.module_bindings.len() {
816 return (0u64, NativeKind::Bool);
817 }
818 let bits = self.module_bindings[index];
819 let kind = self
820 .module_binding_kinds
821 .get(index)
822 .copied()
823 .unwrap_or(NativeKind::Bool);
824 self.module_bindings[index] = Self::NONE_BITS;
825 if index < self.module_binding_kinds.len() {
826 self.module_binding_kinds[index] = NativeKind::Bool;
827 }
828 (bits, kind)
829 }
830
831 /// Length of the module-binding store (lockstep-checked).
832 #[inline]
833 pub(crate) fn module_bindings_len(&self) -> usize {
834 debug_assert_eq!(
835 self.module_bindings.len(),
836 self.module_binding_kinds.len(),
837 "ADR-006 §2.7.8 / Q10 lockstep invariant",
838 );
839 self.module_bindings.len()
840 }
841}
842
843/// Replace the active wire transport provider used by VM transport builtins.
844pub fn set_transport_provider(
845 provider: std::sync::Arc<dyn builtins::transport_provider::WireTransportProvider>,
846) {
847 builtins::transport_provider::set_transport_provider(provider);
848}
849
850/// Restore the default shape-wire transport provider.
851pub fn reset_transport_provider() {
852 builtins::transport_provider::reset_transport_provider();
853}
854
855/// Configure global QUIC settings used by `transport.quic()`.
856#[cfg(feature = "quic")]
857pub fn configure_quic_transport(
858 server_name: String,
859 root_certs_der: Vec<Vec<u8>>,
860 connect_timeout: Option<std::time::Duration>,
861) {
862 builtins::transport_provider::configure_quic_transport(
863 server_name,
864 root_certs_der,
865 connect_timeout,
866 );
867}
868
869/// Clear global QUIC settings used by `transport.quic()`.
870#[cfg(feature = "quic")]
871pub fn clear_quic_transport_config() {
872 builtins::transport_provider::clear_quic_transport_config();
873}
874
875/// Create the VM-backed `transport` module exports.
876pub(crate) fn create_transport_module_exports() -> shape_runtime::module_exports::ModuleExports {
877 builtins::transport_builtins::create_transport_module()
878}
879
880/// Create the VM-backed `remote` module exports.
881pub(crate) fn create_remote_module_exports() -> shape_runtime::module_exports::ModuleExports {
882 builtins::remote_builtins::create_remote_module()
883}
884
885/// Remap constant and string pool indices in a single instruction operand after
886/// a hot-patch splice. `const_offset` and `string_offset` are the starting
887/// indices in the global pools where the blob's local pools were appended.
888fn remap_operand(operand: &mut Option<Operand>, const_offset: usize, string_offset: usize) {
889 let Some(op) = operand.as_mut() else {
890 return;
891 };
892 match op {
893 Operand::Const(idx) => {
894 *idx = (*idx as usize + const_offset) as u16;
895 }
896 Operand::Property(idx) => {
897 *idx = (*idx as usize + string_offset) as u16;
898 }
899 Operand::Name(sid) => {
900 sid.0 = (sid.0 as usize + string_offset) as u32;
901 }
902 Operand::TypedMethodCall { string_id, .. } => {
903 *string_id = (*string_id as usize + string_offset) as u16;
904 }
905 // Other operands (Local, ModuleBinding, Offset, Function, Builtin,
906 // Count, ColumnIndex, TypedField, TypedObjectAlloc, TypedMerge,
907 // ColumnAccess, ForeignFunction) don't reference the constant or
908 // string pools.
909 _ => {}
910 }
911}
912
913#[cfg(test)]
914mod v2_stack_tests;