Skip to main content

shape_vm/executor/vm_impl/
modules.rs

1use super::super::*;
2// `VMError` is intentionally left out of the executor/mod.rs star-import
3// (see executor/mod.rs:126 comment); name it locally for the
4// `invoke_module_fn_id_stub` surface.
5use shape_value::VMError;
6
7/// Project a `TypedReturn` value into a `KindedSlot` ready for stack
8/// placement.
9///
10/// **W17-snapshot-roundtrip (Phase 2d Wave 2.6, 2026-05-11).** Implements
11/// the scalar/leaf return arms (`Concrete::*`) verbatim per ADR-006
12/// §2.7.4 — each arm picks its target `NativeKind` from the
13/// `ConcreteReturn` discriminator without intermediate value synthesis.
14/// Container / wrapper arms (`Ok`/`Err`/`Some`/`None`/typed objects)
15/// surface clean per §2.7.4 — building the typed-Arc `ResultData` /
16/// `OptionData` / `TypedObjectStorage` requires the per-arm KindedSlot
17/// projection path that lands in follow-up.
18fn project_typed_return(
19    tr: shape_runtime::typed_module_exports::TypedReturn,
20) -> Result<shape_value::KindedSlot, VMError> {
21    use shape_runtime::typed_module_exports::{ConcreteReturn, TypedReturn};
22    use shape_value::{KindedSlot, NativeKind, ValueSlot};
23    use std::sync::Arc;
24    match tr {
25        TypedReturn::Concrete(c) => match c {
26            ConcreteReturn::I64(i) => Ok(KindedSlot::new(
27                ValueSlot::from_raw(i as u64),
28                NativeKind::Int64,
29            )),
30            ConcreteReturn::F64(f) => Ok(KindedSlot::new(
31                ValueSlot::from_raw(f.to_bits()),
32                NativeKind::Float64,
33            )),
34            ConcreteReturn::Bool(b) => Ok(KindedSlot::new(
35                ValueSlot::from_raw(if b { 1 } else { 0 }),
36                NativeKind::Bool,
37            )),
38            ConcreteReturn::Unit => Ok(KindedSlot::new(
39                ValueSlot::from_raw(0),
40                NativeKind::Bool,
41            )),
42            ConcreteReturn::String(s) => {
43                Ok(KindedSlot::from_string_arc(Arc::new(s)))
44            }
45            ConcreteReturn::OpaqueTypedObject(hv) => {
46                // Wave 2 Round 4 D4 ckpt-final-prime² (2026-05-14): hv is
47                // `Arc<HeapValue::TypedObject(TypedObjectPtr)>`. Clone the
48                // wrapper (bumps v2-raw refcount); into_raw moves the share
49                // to the slot via `from_typed_object_raw`.
50                match &*hv {
51                    shape_value::heap_value::HeapValue::TypedObject(s) => Ok(
52                        KindedSlot::from_typed_object_raw(s.clone().into_raw()),
53                    ),
54                    other => Err(VMError::RuntimeError(format!(
55                        "project_typed_return: OpaqueTypedObject expected \
56                         HeapValue::TypedObject payload, got {:?}",
57                        other.kind()
58                    ))),
59                }
60            }
61            // R8 W6 G.1 W17-marshal-return-arms close (2026-05-24): explicit
62            // arms for ConcreteReturn::IoHandle (disc 16) +
63            // ConcreteReturn::DataTable (disc 15). Mirrors the 14 existing
64            // typed-Arc constructor precedents in `kinded_slot.rs` per
65            // ADR-005 §1 single-discriminator + ADR-006 §2.7.6 / Q8
66            // bounded carrier-API. Pre-fix: VM surfaced
67            // `project_typed_return: W17-marshal-return-arms` while JIT
68            // returned ec=0 garbage (slice-E §3.b-4 divergence). Drop /
69            // Clone arms already wired at `kinded_slot.rs` ~lines 863-870
70            // (Drop) / 1222-1229 (Clone). See
71            // `docs/cluster-audits/v0.3-r8w6-w17-factory-return-arms-audit.md`.
72            ConcreteReturn::IoHandle(h) => Ok(KindedSlot::from_io_handle(h)),
73            ConcreteReturn::DataTable(d) => Ok(KindedSlot::from_data_table(d)),
74            other => Err(VMError::NotImplemented(format!(
75                "project_typed_return: W17-marshal-return-arms residual — \
76                 ConcreteReturn::{:?} arm has no in-session KindedSlot \
77                 projection. Tracked as W17-followup. ADR-006 §2.7.4.",
78                std::mem::discriminant(&other)
79            ))),
80        },
81        other_tr => Err(VMError::NotImplemented(format!(
82            "project_typed_return: W17-snapshot-roundtrip surface — \
83             TypedReturn::{:?} container arm needs the per-arm KindedSlot \
84             projection path (typed-Arc ResultData/OptionData/\
85             TypedObjectStorage builders). Tracked as W17-marshal-return-arms \
86             follow-up. ADR-006 §2.7.4.",
87            std::mem::discriminant(&other_tr)
88        ))),
89    }
90}
91
92impl VirtualMachine {
93    /// Register a built-in stdlib module into the VM's module registry.
94    /// Delegates to `register_extension` — this is a semantic alias to
95    /// distinguish VM-native stdlib modules from user-installed extension plugins.
96    pub fn register_stdlib_module(&mut self, module: shape_runtime::module_exports::ModuleExports) {
97        self.register_extension(module);
98    }
99
100    /// Register an external/user extension module (e.g. loaded from a .so plugin)
101    /// into the VM's module registry.
102    /// Also merges any method intrinsics for fast Object dispatch.
103    ///
104    /// Phase-2c surface (ADR-006 §2.7.4 / §2.7.5): the body wraps each
105    /// `TypedModuleFunction` into a `ModuleFn` whose signature is
106    /// `Fn(&[ValueWord], &ModuleContext) -> Result<ValueWord, String>`.
107    /// `ValueWord` was deleted by the strict-typing bulldozer (no type to
108    /// import); the kinded rebuild per §2.7.5 makes `ModuleFn`'s argument
109    /// slice `&[KindedSlot]` and its return `Result<KindedSlot, String>`.
110    /// Extensions stay on the stable raw-bits ABI and convert at the
111    /// `RawCallableInvoker` boundary inside shape-runtime.
112    ///
113    /// The cross-crate `ModuleFn` signature change is shape-runtime
114    /// territory (R-shape-runtime sub-cluster) and the corresponding
115    /// `TypedReturn::into_value_word()` helper is also deleted; this
116    /// caller hand-off lands in the Phase-2c rebuild session.
117    pub fn register_extension(&mut self, module: shape_runtime::module_exports::ModuleExports) {
118        // Merge method intrinsics — these don't carry ValueWord shapes
119        // through the registration path and are safe to keep here.
120        for (type_name, methods) in &module.method_intrinsics {
121            let entry = self.extension_methods.entry(type_name.clone()).or_default();
122            for (method_name, func) in methods {
123                entry.insert(method_name.clone(), func.clone());
124            }
125        }
126        // The `module.typed_exports()` rewrap into `ModuleFn` (which
127        // marshals `TypedReturn -> ValueWord` at the boundary) is the
128        // Phase-2c host-API rebuild (ADR-006 §2.7.4 / §2.7.5):
129        // `ModuleFn` becomes `Fn(&[KindedSlot], _) -> Result<KindedSlot, _>`
130        // and the marshal step disappears (the typed body's
131        // `TypedReturn` is converted directly to `KindedSlot` inside
132        // shape-runtime).
133        self.module_registry.register(module);
134    }
135
136    /// Register a module-function entry in the table and return its ID.
137    ///
138    /// Phase-2c surface (ADR-006 §2.7.4 / §2.7.5): the
139    /// `ValueWord::ModuleFunction` carrier shape this function feeds
140    /// depends on the deleted `ValueWord` runtime representation.
141    /// Replaced with a kinded `NativeKind::ModuleFunction`-style ID
142    /// carrier in the Phase-2c rebuild.
143    pub fn register_module_fn_entry(
144        &mut self,
145        entry: shape_runtime::module_exports::ModuleFnEntry,
146    ) -> usize {
147        let id = self.module_fn_table.len();
148        self.module_fn_table.push(entry);
149        id
150    }
151
152    /// Invoke a module-function entry by ID.
153    ///
154    /// **W17-snapshot-roundtrip close (Phase 2d Wave 2.6, 2026-05-11).**
155    /// Lands the kinded shape per ADR-006 §2.7.4 / §2.7.5: takes
156    /// `&[KindedSlot]` and returns `Result<KindedSlot, VMError>`,
157    /// dispatching through the existing [`module_fn_table`] entry
158    /// (sum-typed `Typed` / `TypedAsync` per Phase 4c.3). The async
159    /// arm runs the future to completion on the ambient tokio
160    /// runtime; the sync arm calls the body directly with the slice's
161    /// raw `u64` bits and the registered `arg_kinds` table on the
162    /// receiver (the body is contract-bound to interpret each slot
163    /// per its `arg_kinds[i]`).
164    ///
165    /// Per `module_exports::ModuleContext`, the body receives a borrow
166    /// of the VM's type schema registry plus the optional invoker
167    /// hooks needed for callbacks back into the VM. The body is
168    /// `Send + Sync` so it can be invoked from worker tasks.
169    ///
170    /// Returns:
171    /// - `Ok(KindedSlot)` — successful invocation; the slot carries
172    ///   the projected `TypedReturn` value with the registered return
173    ///   type's `NativeKind`.
174    /// - `Err(VMError::InvalidCall)` — `fn_id` out of range for the
175    ///   current `module_fn_table`.
176    /// - `Err(VMError::RuntimeError(msg))` — body returned an error
177    ///   string; the message propagates verbatim.
178    /// - `Err(VMError::NotImplemented(msg))` — async body called with
179    ///   no ambient tokio runtime, or `TypedReturn::*` arm that needs
180    ///   the kind-threaded slot projection follow-up. Surface-and-stop
181    ///   per ADR-006 §2.7.4 — no Bool-default fallback.
182    pub(crate) fn invoke_module_fn_id_stub(
183        &mut self,
184        fn_id: usize,
185        args: &[shape_value::KindedSlot],
186    ) -> Result<shape_value::KindedSlot, VMError> {
187        let entry = self
188            .module_fn_table
189            .get(fn_id)
190            .ok_or(VMError::InvalidCall)?
191            .clone();
192
193        // **W17-state-tier-roundtrip (Phase 2d Wave 3, 2026-05-12).**
194        // Build a `ModuleContext` borrow against the live schema
195        // registry and capture a read-only `VmStateSnapshot` so state.*
196        // bodies can introspect the VM via `ctx.vm_state` (per
197        // ADR-006 §2.7.4 — state.* reads dispatched through the
198        // VmStateAccessor trait). The snapshot owns its own KindedSlot
199        // shares so the live VM is undisturbed.
200        let vm_state_snap = self.capture_vm_state();
201        let schema_registry: &shape_runtime::type_schema::TypeSchemaRegistry =
202            &self.program.type_schema_registry;
203        // SAFETY: extend the borrow lifetime to 'ctx via transmute is
204        // not needed here because `ModuleContext` is invariant on its
205        // lifetime parameter and the body call below holds the borrow
206        // for the duration of the dispatch.
207        let ctx = shape_runtime::module_exports::ModuleContext {
208            schemas: schema_registry,
209            invoke_callable: None,
210            raw_invoker: None,
211            function_hashes: None,
212            vm_state: Some(&vm_state_snap),
213            granted_permissions: None,
214            scope_constraints: None,
215            set_pending_resume: None,
216            set_pending_frame_resume: None,
217        };
218
219        match entry {
220            shape_runtime::module_exports::ModuleFnEntry::Typed(typed) => {
221                // The body takes `&[u64]` slot bits (per its kind table)
222                // and returns `Result<TypedReturn, String>`. Translate
223                // `&[KindedSlot]` to `Vec<u64>` at the boundary.
224                let raw_bits: Vec<u64> = args.iter().map(|s| s.slot().raw()).collect();
225                let typed_return = (typed.invoke)(&raw_bits, &ctx)
226                    .map_err(VMError::RuntimeError)?;
227                project_typed_return(typed_return)
228            }
229            shape_runtime::module_exports::ModuleFnEntry::TypedAsync(async_entry) => {
230                let raw_bits: Vec<u64> = args.iter().map(|s| s.slot().raw()).collect();
231                let fut = (async_entry.invoke)(raw_bits);
232                // Drive the future on the ambient tokio runtime. If no
233                // runtime is available we surface — async dispatch
234                // requires an explicit host runtime per the §2.7.4 task-
235                // scheduler boundary.
236                let typed_return = match tokio::runtime::Handle::try_current() {
237                    Ok(handle) => tokio::task::block_in_place(|| {
238                        handle.block_on(fut).map_err(VMError::RuntimeError)
239                    })?,
240                    Err(_) => {
241                        return Err(VMError::NotImplemented(
242                            "invoke_module_fn_id: async dispatch requires an \
243                             ambient tokio runtime — wrap the call in \
244                             tokio::runtime::Builder::new_current_thread().build() \
245                             or use a worker thread. ADR-006 §2.7.4 \
246                             task-scheduler boundary."
247                                .to_string(),
248                        ));
249                    }
250                };
251                project_typed_return(typed_return)
252            }
253        }
254    }
255
256    /// Populate extension module objects as module_bindings — W17-comptime-vm-dispatch rebuild.
257    ///
258    /// **W17-comptime-vm-dispatch (Phase 2d Wave 3, 2026-05-12).**
259    /// Per ADR-006 §2.7.26 amendment. Builds a kinded `TypedObject`
260    /// per registered extension module, with field slots that store
261    /// **module-function-id field references** as `Ptr(HeapKind::ModuleFn)`
262    /// inline-scalar payloads. The dispatch chain
263    /// `LoadModuleBinding(idx) + GetFieldTyped(...) + CallValue` routes
264    /// through:
265    ///
266    /// 1. `LoadModuleBinding(idx)` reads the kinded module-binding
267    ///    slot (TypedObject + `Ptr(HeapKind::TypedObject)` kind) and
268    ///    pushes it via `clone_with_kind` retain-on-read (§2.7.7).
269    /// 2. `GetFieldTyped { type_id, field_idx, field_type_tag }` pops
270    ///    the receiver, recovers the `Arc<TypedObjectStorage>` per
271    ///    ADR-005 §1, and reads the field. The compiler emits
272    ///    `field_type_tag = FIELD_TAG_ANY` for schema fields of
273    ///    `FieldType::Any` (the comptime predeclared schema shape).
274    ///    The `op_get_field_typed` body falls through to
275    ///    `push_field_value_with_kind`, which sources the kind from
276    ///    `storage.field_kinds[field_idx]` (the §2.7.7 parallel-kind
277    ///    track) — resolving hardening item (f) — and pushes the
278    ///    `module_fn_id as u64` bits with kind
279    ///    `Ptr(HeapKind::ModuleFn)`.
280    /// 3. `CallValue` pops args + callee, dispatches via
281    ///    `call_value_immediate_nb` whose `Ptr(HeapKind::ModuleFn)`
282    ///    arm routes to `invoke_module_fn_id_stub(bits as usize, args)` —
283    ///    the same path used by `W17-snapshot-roundtrip` for direct
284    ///    module-fn invocation.
285    ///
286    /// Per-module construction:
287    ///
288    /// - Look up the predeclared `__mod_<name>` schema (registered by
289    ///   `compiler/comptime.rs::ensure_module_object_schema` before
290    ///   bytecode compilation). The schema field names define the
291    ///   storage's field order; missing schemas are skipped (the
292    ///   module's exports remain unreachable through this binding).
293    /// - For each typed export (sync and async), register a
294    ///   `ModuleFnEntry::Typed` / `TypedAsync` into `module_fn_table`
295    ///   to obtain a `module_fn_id`.
296    /// - For each schema field, look up the matching `module_fn_id`,
297    ///   write a `ValueSlot::from_raw(module_fn_id as u64)` with
298    ///   `field_kinds[i] = Ptr(HeapKind::ModuleFn)` and `heap_mask`
299    ///   bit set. Unmatched fields (a schema field with no
300    ///   corresponding export) get the `(0u64, NativeKind::Bool)`
301    ///   sentinel pair — same shape as the
302    ///   `module_binding_pad_to_kinded` uninitialised-slot convention.
303    /// - Construct `Arc<TypedObjectStorage>` via the typed constructor
304    ///   per ADR-006 §2.4 and write the `Ptr(HeapKind::TypedObject)`
305    ///   slot to the module-binding via
306    ///   `module_binding_write_kinded` (§2.7.8 / Q10 lockstep).
307    ///
308    /// Resolves the upstream `populate_module_objects` no-op blocker
309    /// flagged by `W17-snapshot-roundtrip` (commit `fbfbfb6`). The
310    /// 4 comptime introspection forms wired by C2-comptime-rebuild
311    /// (`a5df165`) — `build_config` / `implements` / `warning` /
312    /// `error` — now dispatch end-to-end via VM mode.
313    pub fn populate_module_objects(&mut self) {
314        use shape_runtime::module_exports::ModuleFnEntry;
315        use shape_value::heap_value::TypedObjectStorage;
316        use shape_value::{HeapKind, NativeKind, ValueSlot};
317        use std::sync::Arc;
318
319        // Phase 1 — collect: gather per-module data without taking a
320        // mutable borrow on `self` while iterating the registry. The
321        // `register_module_fn_entry` call mutates `self.module_fn_table`,
322        // which conflicts with an active borrow on `self.module_registry`.
323        let module_names: Vec<String> = self
324            .module_registry
325            .module_names()
326            .iter()
327            .map(|s| s.to_string())
328            .collect();
329
330        for module_name in module_names {
331            // Resolve the module's typed exports. The `module_registry.get`
332            // borrow is local to this iteration and dropped before we
333            // mutate `module_fn_table` below.
334            let typed_entries: Vec<(String, ModuleFnEntry)> = {
335                let module = match self.module_registry.get(&module_name) {
336                    Some(m) => m,
337                    None => continue,
338                };
339                let typed = module.typed_exports();
340                let mut entries: Vec<(String, ModuleFnEntry)> =
341                    Vec::with_capacity(typed.functions.len() + typed.async_functions.len());
342                for (export_name, typed_fn) in &typed.functions {
343                    entries.push((
344                        export_name.clone(),
345                        ModuleFnEntry::Typed(typed_fn.clone()),
346                    ));
347                }
348                for (export_name, typed_async) in &typed.async_functions {
349                    entries.push((
350                        export_name.clone(),
351                        ModuleFnEntry::TypedAsync(typed_async.clone()),
352                    ));
353                }
354                entries
355            };
356
357            // Locate the binding index for this module — prefer the
358            // hidden native binding (`__imported_module__::<name>`,
359            // injected by the compiler's
360            // `ensure_hidden_native_module_binding`), fall back to the
361            // plain binding name. The hidden form is used when a Shape
362            // artifact module with the same name would otherwise
363            // shadow the native object.
364            let hidden_name = format!("__imported_module__::{}", module_name);
365            let binding_idx = self
366                .program
367                .module_binding_names
368                .iter()
369                .position(|n| n == &hidden_name)
370                .or_else(|| {
371                    self.program
372                        .module_binding_names
373                        .iter()
374                        .position(|n| n == &module_name)
375                });
376            let binding_idx = match binding_idx {
377                Some(i) => i,
378                None => continue, // No binding name — nothing to populate.
379            };
380
381            // Resolve the predeclared module-object schema. The schema
382            // is registered before compilation by
383            // `compiler/comptime.rs::ensure_module_object_schema`
384            // (under canonical name `__mod_<module_name>`).
385            // Without it we can't define a stable field order for
386            // the typed-object layout, so skip — the binding stays at
387            // the no-op-on-drop sentinel and any reference to a field
388            // through this binding will surface clean at GetFieldTyped.
389            let schema_name = format!("__mod_{}", module_name);
390            let schema = match self.lookup_schema_by_name(&schema_name) {
391                Some(s) => s.clone(),
392                None => continue,
393            };
394
395            // Register each typed entry into `module_fn_table` and
396            // build a name → module_fn_id lookup.
397            let mut fn_id_by_name: std::collections::HashMap<String, u64> =
398                std::collections::HashMap::with_capacity(typed_entries.len());
399            for (export_name, entry) in typed_entries {
400                let fn_id = self.register_module_fn_entry(entry);
401                fn_id_by_name.insert(export_name, fn_id as u64);
402            }
403
404            // Build the typed-object slot list in schema field order.
405            // Each field maps to either:
406            //   - a known module-fn-id → ValueSlot(fn_id) with kind
407            //     Ptr(HeapKind::ModuleFn) and heap_mask bit set, or
408            //   - no matching export → (0, NativeKind::Bool) sentinel
409            //     (same shape as the module-binding-pad uninitialised
410            //     slot convention — no Bool-default fallback for a
411            //     known-callable-but-missing field; the compiler
412            //     should have surfaced 'module has no export' earlier).
413            let field_count = schema.fields.len();
414            let mut slots: Vec<ValueSlot> = Vec::with_capacity(field_count);
415            let mut field_kinds: Vec<NativeKind> = Vec::with_capacity(field_count);
416            let mut heap_mask: u64 = 0;
417            for (i, field) in schema.fields.iter().enumerate() {
418                match fn_id_by_name.get(&field.name) {
419                    Some(&fn_id) => {
420                        // ModuleFn inline-scalar slot: bits = fn_id,
421                        // kind = Ptr(HeapKind::ModuleFn). Mark the
422                        // heap_mask bit so the read path sees the slot
423                        // as "kind-bearing" and dispatches through the
424                        // FIELD_TAG_ANY / field_kinds resolver in
425                        // op_get_field_typed.
426                        slots.push(ValueSlot::from_raw(fn_id));
427                        field_kinds.push(NativeKind::Ptr(HeapKind::ModuleFn));
428                        heap_mask |= 1u64 << i;
429                    }
430                    None => {
431                        // Schema field present but no typed export:
432                        // sentinel slot. `clone_with_kind` /
433                        // `drop_with_kind` are no-op on (0, Bool).
434                        slots.push(ValueSlot::from_raw(0));
435                        field_kinds.push(NativeKind::Bool);
436                    }
437                }
438            }
439
440            // Wave 2 Round 4 D4 ckpt-1: migrated to v2-raw `_new` per D1
441            // API surface. The raw pointer is directly the carrier bits;
442            // `module_binding_write_kinded` consumes a u64 + NativeKind
443            // pair so this site cleanly migrates without depending on
444            // the `HeapValue::TypedObject` variant signature flip.
445            let ptr = TypedObjectStorage::_new(
446                schema.id as u64,
447                slots.into_boxed_slice(),
448                heap_mask,
449                Arc::from(field_kinds.into_boxed_slice()),
450            );
451
452            // Hand off one share to the binding slot. The v2-raw pointer
453            // bits are the carrier directly (per ADR-006 §2.4 / D1's
454            // `from_typed_object_raw` constructor contract). One strong
455            // count owned by us is transferred into the binding via
456            // `module_binding_write_kinded`.
457            let bits = ptr as u64;
458            self.module_binding_write_kinded(
459                binding_idx,
460                bits,
461                NativeKind::Ptr(HeapKind::TypedObject),
462            );
463        }
464    }
465}