Skip to main content

shape_vm/executor/vm_impl/
modules.rs

1use super::super::*;
2
3impl VirtualMachine {
4    /// Register a built-in stdlib module into the VM's module registry.
5    /// Delegates to `register_extension` — this is a semantic alias to
6    /// distinguish VM-native stdlib modules from user-installed extension plugins.
7    pub fn register_stdlib_module(&mut self, module: shape_runtime::module_exports::ModuleExports) {
8        self.register_extension(module);
9    }
10
11    /// Register an external/user extension module (e.g. loaded from a .so plugin)
12    /// into the VM's module registry.
13    /// Also merges any method intrinsics for fast Object dispatch.
14    pub fn register_extension(&mut self, module: shape_runtime::module_exports::ModuleExports) {
15        // Merge method intrinsics
16        for (type_name, methods) in &module.method_intrinsics {
17            let entry = self.extension_methods.entry(type_name.clone()).or_default();
18            for (method_name, func) in methods {
19                entry.insert(method_name.clone(), func.clone());
20            }
21        }
22        // Expose module exports as methods on the module object type so
23        // `module.fn(...)` dispatches via CallMethod without UFCS rewrites.
24        let module_type_name = format!("__mod_{}", module.name);
25        let module_entry = self.extension_methods.entry(module_type_name).or_default();
26        for (export_name, func) in &module.exports {
27            module_entry.insert(export_name.clone(), func.clone());
28        }
29        for (export_name, async_fn) in &module.async_exports {
30            let async_fn = async_fn.clone();
31            let wrapped: shape_runtime::module_exports::ModuleFn = Arc::new(
32                move |args: &[ValueWord], _ctx: &shape_runtime::module_exports::ModuleContext| {
33                    let future = async_fn(args);
34                    tokio::task::block_in_place(|| {
35                        tokio::runtime::Handle::current().block_on(future)
36                    })
37                },
38            );
39            module_entry.insert(export_name.clone(), wrapped);
40        }
41        self.module_registry.register(module);
42    }
43
44    /// Register a ModuleFn in the table and return its ID (for ValueWord::ModuleFunction).
45    pub fn register_module_fn(&mut self, f: shape_runtime::module_exports::ModuleFn) -> usize {
46        let id = self.module_fn_table.len();
47        self.module_fn_table.push(f);
48        id
49    }
50
51    /// Invoke a registered module function with a scoped `ModuleContext`.
52    ///
53    /// The context provides access to the type schema registry, a callable
54    /// invoker closure, and a raw invoker that extensions can capture in
55    /// long-lived structs (e.g., CFFI callback userdata).
56    pub(crate) fn invoke_module_fn(
57        &mut self,
58        module_fn: &shape_runtime::module_exports::ModuleFn,
59        args: &[ValueWord],
60    ) -> Result<ValueWord, VMError> {
61        // SAFETY: The module function is called synchronously and the VM pointer
62        // remains valid for the duration of the call.  We use a raw pointer so
63        // that: (a) the callable invoker can re-enter the VM, and (b) we can
64        // simultaneously borrow the schema registry.
65        unsafe {
66            let vm_ptr = self as *mut VirtualMachine;
67
68            let invoker =
69                |callable: &ValueWord, call_args: &[ValueWord]| -> Result<ValueWord, String> {
70                    (*vm_ptr)
71                        .call_value_immediate_nb(callable, call_args, None)
72                        .map_err(|e| e.to_string())
73                };
74
75            unsafe fn vm_callable_invoker(
76                ctx: *mut std::ffi::c_void,
77                callable: &ValueWord,
78                args: &[ValueWord],
79            ) -> Result<ValueWord, String> {
80                let vm = unsafe { &mut *(ctx as *mut VirtualMachine) };
81                vm.call_value_immediate_nb(callable, args, None)
82                    .map_err(|err| err.to_string())
83            }
84
85            // Capture a read-only snapshot of VM state before dispatching.
86            // The snapshot lives on the stack and is referenced by ModuleContext
87            // for the duration of this synchronous call.
88            let vm_snapshot = (*vm_ptr).capture_vm_state();
89
90            let ctx = shape_runtime::module_exports::ModuleContext {
91                schemas: &(*vm_ptr).program.type_schema_registry,
92                invoke_callable: Some(&invoker),
93                raw_invoker: Some(shape_runtime::module_exports::RawCallableInvoker {
94                    ctx: vm_ptr as *mut std::ffi::c_void,
95                    invoke: vm_callable_invoker,
96                }),
97                function_hashes: if (*vm_ptr).function_hash_raw.is_empty() {
98                    None
99                } else {
100                    Some(&(*vm_ptr).function_hash_raw)
101                },
102                vm_state: Some(&vm_snapshot),
103                granted_permissions: None,
104                scope_constraints: None,
105                set_pending_resume: Some(&|snapshot| {
106                    // vm_ptr is valid for the duration of the module function call
107                    // (outer unsafe block covers this).
108                    (*vm_ptr).pending_resume = Some(snapshot);
109                }),
110                set_pending_frame_resume: Some(&|ip_offset, locals| {
111                    // vm_ptr is valid for the duration of the module function call
112                    // (outer unsafe block covers this).
113                    (*vm_ptr).pending_frame_resume = Some(FrameResumeData { ip_offset, locals });
114                }),
115            };
116
117            let result = module_fn(args, &ctx).map_err(VMError::RuntimeError);
118
119            // Check if the module function requested a VM state resume.
120            // If so, return a special error that the dispatch loop intercepts.
121            if (*vm_ptr).pending_resume.is_some() {
122                return Err(VMError::ResumeRequested);
123            }
124
125            result
126        }
127    }
128
129    /// Populate extension module objects as module_bindings (json, duckdb, etc.).
130    /// These are used by extension Shape code (e.g., `duckdb.query(...)`).
131    /// Call this after load_program().
132    pub fn populate_module_objects(&mut self) {
133        // Collect module data first to avoid borrow conflicts
134        let module_data: Vec<(
135            String,
136            Vec<(String, shape_runtime::module_exports::ModuleFn)>,
137            Vec<(String, shape_runtime::module_exports::AsyncModuleFn)>,
138            Vec<String>,
139        )> = self
140            .module_registry
141            .module_names()
142            .iter()
143            .filter_map(|name| {
144                let module = self.module_registry.get(name)?;
145                let sync_exports: Vec<_> = module
146                    .exports
147                    .iter()
148                    .map(|(k, v)| (k.clone(), v.clone()))
149                    .collect();
150                let async_exports: Vec<_> = module
151                    .async_exports
152                    .iter()
153                    .map(|(k, v)| (k.clone(), v.clone()))
154                    .collect();
155                let mut source_exports = Vec::new();
156                for artifact in &module.module_artifacts {
157                    if artifact.module_path != *name {
158                        continue;
159                    }
160                    let Some(source) = artifact.source.as_deref() else {
161                        continue;
162                    };
163                    if let Ok(exports) =
164                        shape_runtime::module_loader::collect_exported_function_names_from_source(
165                            &artifact.module_path,
166                            source,
167                        )
168                    {
169                        source_exports.extend(exports);
170                    }
171                }
172                source_exports.sort();
173                source_exports.dedup();
174                Some((
175                    name.to_string(),
176                    sync_exports,
177                    async_exports,
178                    source_exports,
179                ))
180            })
181            .collect();
182
183        for (module_name, sync_exports, async_exports, source_exports) in module_data {
184            // Find the module_binding index for this module name
185            let binding_idx = self
186                .program
187                .module_binding_names
188                .iter()
189                .position(|n| n == &module_name);
190
191            if let Some(idx) = binding_idx {
192                let mut obj = HashMap::new();
193
194                // Register sync exports directly
195                for (export_name, module_fn) in sync_exports {
196                    let fn_id = self.register_module_fn(module_fn);
197                    obj.insert(export_name, ValueWord::from_module_function(fn_id as u32));
198                }
199
200                // Wrap async exports: block_in_place + block_on at call time
201                for (export_name, async_fn) in async_exports {
202                    let wrapped: shape_runtime::module_exports::ModuleFn =
203                        Arc::new(move |args: &[ValueWord], _ctx: &shape_runtime::module_exports::ModuleContext| {
204                            let future = async_fn(args);
205                            tokio::task::block_in_place(|| {
206                                tokio::runtime::Handle::current().block_on(future)
207                            })
208                        });
209                    let fn_id = self.register_module_fn(wrapped);
210                    obj.insert(export_name, ValueWord::from_module_function(fn_id as u32));
211                }
212
213                // Add Shape-source exported functions (compiled into bytecode).
214                // These are regular VM functions, not host module functions.
215                for export_name in source_exports {
216                    if obj.contains_key(&export_name) {
217                        continue;
218                    }
219                    if let Some(&func_id) = self.function_name_index.get(&export_name) {
220                        obj.insert(export_name, ValueWord::from_function(func_id));
221                    }
222                }
223
224                // Module object schemas must be predeclared at compile time.
225                let cache_name = format!("__mod_{}", module_name);
226                let schema_id = if let Some(schema) = self.lookup_schema_by_name(&cache_name) {
227                    schema.id
228                } else {
229                    // Keep execution predictable: no runtime schema synthesis.
230                    // Missing module schema means compiler/loader setup is incomplete.
231                    continue;
232                };
233
234                // Look up schema to get field ordering
235                let Some(schema) = self.lookup_schema(schema_id) else {
236                    continue;
237                };
238                let field_order: Vec<String> =
239                    schema.fields.iter().map(|f| f.name.clone()).collect();
240
241                let mut slots = Vec::with_capacity(field_order.len());
242                let mut heap_mask: u64 = 0;
243                for (i, field_name) in field_order.iter().enumerate() {
244                    let nb_val = obj.get(field_name).cloned().unwrap_or_else(ValueWord::none);
245                    let (slot, is_heap) =
246                        crate::executor::objects::object_creation::nb_to_slot_with_field_type(
247                            &nb_val, None,
248                        );
249                    slots.push(slot);
250                    if is_heap {
251                        heap_mask |= 1u64 << i;
252                    }
253                }
254
255                let typed_nb = ValueWord::from_heap_value(HeapValue::TypedObject {
256                    schema_id: schema_id as u64,
257                    slots: slots.into_boxed_slice(),
258                    heap_mask,
259                });
260                if idx >= self.module_bindings.len() {
261                    self.module_bindings.resize_with(idx + 1, ValueWord::none);
262                }
263                // BARRIER: heap write site — overwrites module binding during typed object initialization
264                self.module_bindings[idx] = typed_nb;
265            }
266        }
267    }
268}