Skip to main content

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;