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}