Skip to main content

shape_runtime/
module_exports.rs

1//! Runtime module export bindings for Shape extensions.
2//!
3//! This module defines the in-process representation used by VM/LSP/CLI after
4//! a plugin has been loaded through the ABI capability interfaces.
5
6use crate::type_schema::{TypeSchema, TypeSchemaRegistry};
7use shape_value::ValueWord;
8use std::collections::HashMap;
9use std::ffi::c_void;
10use std::future::Future;
11use std::pin::Pin;
12use std::sync::Arc;
13
14/// Raw callable invoker as a function pointer + opaque context.
15///
16/// This is the `Send`-safe, `'static`-safe form of `invoke_callable` that
17/// extensions (e.g., CFFI) can store in long-lived structs like callback
18/// userdata.  The context pointer is valid for the duration of the
19/// originating module function call.
20#[derive(Clone, Copy)]
21pub struct RawCallableInvoker {
22    pub ctx: *mut c_void,
23    pub invoke: unsafe fn(*mut c_void, &ValueWord, &[ValueWord]) -> Result<ValueWord, String>,
24}
25
26impl RawCallableInvoker {
27    /// Invoke a Shape callable through this raw invoker.
28    ///
29    /// # Safety
30    /// The caller must ensure `self.ctx` is still valid (i.e., the originating
31    /// VM module call is still on the stack).
32    pub unsafe fn call(
33        &self,
34        callable: &ValueWord,
35        args: &[ValueWord],
36    ) -> Result<ValueWord, String> {
37        unsafe { (self.invoke)(self.ctx, callable, args) }
38    }
39}
40
41/// Information about a single VM call frame, captured at a point in time.
42#[derive(Debug, Clone)]
43pub struct FrameInfo {
44    pub function_id: Option<u16>,
45    pub function_name: String,
46    pub blob_hash: Option<[u8; 32]>,
47    pub local_ip: usize,
48    pub locals: Vec<ValueWord>,
49    pub upvalues: Option<Vec<ValueWord>>,
50    pub args: Vec<ValueWord>,
51}
52
53/// Trait providing read access to VM state for state module functions.
54pub trait VmStateAccessor: Send + Sync {
55    fn current_frame(&self) -> Option<FrameInfo>;
56    fn all_frames(&self) -> Vec<FrameInfo>;
57    fn caller_frame(&self) -> Option<FrameInfo>;
58    fn current_args(&self) -> Vec<ValueWord>;
59    fn current_locals(&self) -> Vec<(String, ValueWord)>;
60    fn module_bindings(&self) -> Vec<(String, ValueWord)>;
61    /// Total instruction count at the time of capture. Default impl for compat.
62    fn instruction_count(&self) -> usize {
63        0
64    }
65}
66
67/// Execution context available to module functions during a VM call.
68///
69/// The VM constructs this before each module function dispatch and passes
70/// it by reference.
71pub struct ModuleContext<'a> {
72    /// Type schema registry — lookup types by name or ID.
73    pub schemas: &'a TypeSchemaRegistry,
74
75    /// Invoke a Shape callable (function/closure) from host code.
76    pub invoke_callable: Option<&'a dyn Fn(&ValueWord, &[ValueWord]) -> Result<ValueWord, String>>,
77
78    /// Raw invoker for extensions that need to capture a callable invoker
79    /// beyond the borrow lifetime (e.g., CFFI callback userdata).
80    /// Valid only for the duration of the current module function call.
81    pub raw_invoker: Option<RawCallableInvoker>,
82
83    /// Content-addressed function hashes indexed by function ID.
84    /// Provided by the VM when content-addressed metadata is available.
85    /// Uses raw `[u8; 32]` to avoid a dependency on `shape-vm`'s `FunctionHash`.
86    pub function_hashes: Option<&'a [Option<[u8; 32]>]>,
87
88    /// Read-only access to VM state (call frames, locals, etc.).
89    /// Provided by the VM when state introspection is needed.
90    pub vm_state: Option<&'a dyn VmStateAccessor>,
91
92    /// Permissions granted to the current execution context.
93    /// When `Some`, module functions check this before performing I/O.
94    /// When `None`, all operations are allowed (backwards compatible).
95    pub granted_permissions: Option<shape_abi_v1::PermissionSet>,
96
97    /// Scope constraints for the current execution context.
98    /// Narrows permissions to specific paths, hosts, etc.
99    pub scope_constraints: Option<shape_abi_v1::ScopeConstraints>,
100
101    /// Callback for `state.resume()` to request full VM state restoration.
102    /// The module function stores the snapshot; the dispatch loop applies it
103    /// after the current instruction completes.
104    pub set_pending_resume: Option<&'a dyn Fn(ValueWord)>,
105
106    /// Callback for `state.resume_frame()` to request mid-function resume.
107    /// Stores (ip_offset, locals) so the dispatch loop can override the
108    /// call frame set up by invoke_callable.
109    pub set_pending_frame_resume: Option<&'a dyn Fn(usize, Vec<ValueWord>)>,
110}
111
112/// Check whether the current execution context has a required permission.
113///
114/// If `granted_permissions` is `None`, all operations are allowed (backwards
115/// compatible with code that predates the permission system). If `Some`, the
116/// specific permission must be present in the set.
117pub fn check_permission(
118    ctx: &ModuleContext,
119    permission: shape_abi_v1::Permission,
120) -> Result<(), String> {
121    if let Some(ref granted) = ctx.granted_permissions {
122        if !granted.contains(&permission) {
123            return Err(format!(
124                "Permission denied: {} ({})",
125                permission.description(),
126                permission.name()
127            ));
128        }
129    }
130    Ok(())
131}
132
133/// A module function callable from Shape (synchronous).
134///
135/// Takes a slice of ValueWord arguments plus a `ModuleContext` that provides
136/// access to the type schema registry and a callable invoker.
137/// The function must be Send + Sync for thread safety.
138pub type ModuleFn = Arc<
139    dyn for<'ctx> Fn(&[ValueWord], &ModuleContext<'ctx>) -> Result<ValueWord, String> + Send + Sync,
140>;
141
142/// An async module function callable from Shape.
143///
144/// Returns a boxed future that resolves to a ValueWord result.
145/// The VM executor awaits this using the current tokio runtime.
146///
147/// Note: async functions do not receive a `ModuleContext` because the context
148/// borrows from the VM and cannot be sent across await points.
149pub type AsyncModuleFn = Arc<
150    dyn Fn(&[ValueWord]) -> Pin<Box<dyn Future<Output = Result<ValueWord, String>> + Send>>
151        + Send
152        + Sync,
153>;
154
155/// Visibility policy for one extension export.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum ModuleExportVisibility {
158    /// Normal module API: available in runtime + comptime contexts.
159    Public,
160    /// Only callable from comptime contexts.
161    ComptimeOnly,
162    /// Internal helper: callable but hidden from normal user-facing discovery.
163    Internal,
164}
165
166impl Default for ModuleExportVisibility {
167    fn default() -> Self {
168        Self::Public
169    }
170}
171
172/// Schema for a single parameter of a module function.
173/// Used by LSP for completions and by validation for type checking.
174#[derive(Debug, Clone)]
175pub struct ModuleParam {
176    pub name: String,
177    pub type_name: String,
178    pub required: bool,
179    pub description: String,
180    pub default_snippet: Option<String>,
181    pub allowed_values: Option<Vec<String>>,
182    pub nested_params: Option<Vec<ModuleParam>>,
183}
184
185impl Default for ModuleParam {
186    fn default() -> Self {
187        Self {
188            name: String::new(),
189            type_name: "any".to_string(),
190            required: false,
191            description: String::new(),
192            default_snippet: None,
193            allowed_values: None,
194            nested_params: None,
195        }
196    }
197}
198
199/// Schema for a module function — describes parameters and return type.
200/// Used by LSP for completions, hover, and signature help.
201#[derive(Debug, Clone)]
202pub struct ModuleFunction {
203    pub description: String,
204    pub params: Vec<ModuleParam>,
205    pub return_type: Option<String>,
206}
207
208/// Bundled module artifact from an extension.
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct ModuleArtifact {
211    /// Import path for this module (e.g. "duckdb", "duckdb.query")
212    pub module_path: String,
213    /// Optional Shape source payload.
214    pub source: Option<String>,
215    /// Optional precompiled payload (opaque host format).
216    pub compiled: Option<Vec<u8>>,
217}
218
219/// A Rust-implemented module exposed via `<name>`.
220#[derive(Clone)]
221pub struct ModuleExports {
222    /// Module name (e.g., "csv", "json", "duckdb")
223    pub name: String,
224    /// Human-readable description of this module
225    pub description: String,
226    /// Exported sync functions: name → implementation
227    pub exports: HashMap<String, ModuleFn>,
228    /// Exported async functions: name → implementation
229    pub async_exports: HashMap<String, AsyncModuleFn>,
230    /// Function schemas for LSP + validation: name → schema
231    pub schemas: HashMap<String, ModuleFunction>,
232    /// Export visibility controls: name → visibility.
233    pub export_visibility: HashMap<String, ModuleExportVisibility>,
234    /// Shape source files bundled with this extension.
235    /// Compiled and merged with core stdlib at startup.
236    /// Vec of (filename, source_code) pairs.
237    ///
238    /// Legacy compatibility field. New code should use `module_artifacts`.
239    pub shape_sources: Vec<(String, String)>,
240    /// Bundled module artifacts (source/compiled/both).
241    pub module_artifacts: Vec<ModuleArtifact>,
242    /// Method intrinsics for fast dispatch on typed Objects.
243    /// Outer key: type name (e.g., "DuckDbQuery")
244    /// Inner key: method name (e.g., "build_sql")
245    /// Dispatched BEFORE callable-property and UFCS fallback.
246    pub method_intrinsics: HashMap<String, HashMap<String, ModuleFn>>,
247    /// Type schemas to register in the VM's runtime TypeSchemaRegistry.
248    /// Extensions can use this to declare types that the runtime can use
249    /// for TypedObject creation and field validation.
250    pub type_schemas: Vec<TypeSchema>,
251}
252
253impl ModuleExports {
254    /// Create a new extension module.
255    pub fn new(name: impl Into<String>) -> Self {
256        Self {
257            name: name.into(),
258            description: String::new(),
259            exports: HashMap::new(),
260            async_exports: HashMap::new(),
261            schemas: HashMap::new(),
262            export_visibility: HashMap::new(),
263            shape_sources: Vec::new(),
264            module_artifacts: Vec::new(),
265            method_intrinsics: HashMap::new(),
266            type_schemas: Vec::new(),
267        }
268    }
269
270    /// Register an exported function.
271    pub fn add_function<F>(&mut self, name: impl Into<String>, f: F) -> &mut Self
272    where
273        F: for<'ctx> Fn(&[ValueWord], &ModuleContext<'ctx>) -> Result<ValueWord, String>
274            + Send
275            + Sync
276            + 'static,
277    {
278        let name = name.into();
279        self.exports.insert(name.clone(), Arc::new(f));
280        self.export_visibility.entry(name).or_default();
281        self
282    }
283
284    /// Register an exported function with its schema.
285    pub fn add_function_with_schema<F>(
286        &mut self,
287        name: impl Into<String>,
288        f: F,
289        schema: ModuleFunction,
290    ) -> &mut Self
291    where
292        F: for<'ctx> Fn(&[ValueWord], &ModuleContext<'ctx>) -> Result<ValueWord, String>
293            + Send
294            + Sync
295            + 'static,
296    {
297        let name = name.into();
298        self.exports.insert(name.clone(), Arc::new(f));
299        self.schemas.insert(name.clone(), schema);
300        self.export_visibility.entry(name).or_default();
301        self
302    }
303
304    /// Register an async exported function.
305    pub fn add_async_function<F, Fut>(&mut self, name: impl Into<String>, f: F) -> &mut Self
306    where
307        F: Fn(Vec<ValueWord>) -> Fut + Send + Sync + 'static,
308        Fut: Future<Output = Result<ValueWord, String>> + Send + 'static,
309    {
310        let name = name.into();
311        self.async_exports.insert(
312            name.clone(),
313            Arc::new(move |args: &[ValueWord]| {
314                let owned_args = args.to_vec();
315                Box::pin(f(owned_args))
316            }),
317        );
318        self.export_visibility.entry(name).or_default();
319        self
320    }
321
322    /// Register an async exported function with its schema.
323    pub fn add_async_function_with_schema<F, Fut>(
324        &mut self,
325        name: impl Into<String>,
326        f: F,
327        schema: ModuleFunction,
328    ) -> &mut Self
329    where
330        F: Fn(Vec<ValueWord>) -> Fut + Send + Sync + 'static,
331        Fut: Future<Output = Result<ValueWord, String>> + Send + 'static,
332    {
333        let name = name.into();
334        self.async_exports.insert(
335            name.clone(),
336            Arc::new(move |args: &[ValueWord]| {
337                let owned_args = args.to_vec();
338                Box::pin(f(owned_args))
339            }),
340        );
341        self.schemas.insert(name.clone(), schema);
342        self.export_visibility.entry(name).or_default();
343        self
344    }
345
346    /// Set visibility for one export name.
347    pub fn set_export_visibility(
348        &mut self,
349        name: impl Into<String>,
350        visibility: ModuleExportVisibility,
351    ) -> &mut Self {
352        self.export_visibility.insert(name.into(), visibility);
353        self
354    }
355
356    /// Resolve visibility for one export (defaults to Public).
357    pub fn export_visibility(&self, name: &str) -> ModuleExportVisibility {
358        self.export_visibility
359            .get(name)
360            .copied()
361            .unwrap_or_default()
362    }
363
364    /// Return true when the export can be called in the current compiler mode.
365    pub fn is_export_available(&self, name: &str, comptime_mode: bool) -> bool {
366        match self.export_visibility(name) {
367            ModuleExportVisibility::Public => true,
368            ModuleExportVisibility::ComptimeOnly => comptime_mode,
369            ModuleExportVisibility::Internal => true,
370        }
371    }
372
373    /// Return true when the export should appear in user-facing completion/hover surfaces.
374    pub fn is_export_public_surface(&self, name: &str, comptime_mode: bool) -> bool {
375        match self.export_visibility(name) {
376            ModuleExportVisibility::Public => true,
377            ModuleExportVisibility::ComptimeOnly => comptime_mode,
378            ModuleExportVisibility::Internal => false,
379        }
380    }
381
382    /// List exports available for the requested mode (sync + async).
383    pub fn export_names_available(&self, comptime_mode: bool) -> Vec<&str> {
384        self.export_names()
385            .into_iter()
386            .filter(|name| self.is_export_available(name, comptime_mode))
387            .collect()
388    }
389
390    /// List user-facing exports for completion/hover (sync + async).
391    pub fn export_names_public_surface(&self, comptime_mode: bool) -> Vec<&str> {
392        self.export_names()
393            .into_iter()
394            .filter(|name| self.is_export_public_surface(name, comptime_mode))
395            .collect()
396    }
397
398    /// Bundle a Shape source file with this extension.
399    /// The source will be compiled and merged with stdlib at startup.
400    pub fn add_shape_source(&mut self, filename: &str, source: &str) -> &mut Self {
401        self.module_artifacts.push(ModuleArtifact {
402            module_path: filename.to_string(),
403            source: Some(source.to_string()),
404            compiled: None,
405        });
406        self.shape_sources
407            .push((filename.to_string(), source.to_string()));
408        self
409    }
410
411    /// Register a bundled module artifact (source/compiled/both).
412    pub fn add_shape_artifact(
413        &mut self,
414        module_path: impl Into<String>,
415        source: Option<String>,
416        compiled: Option<Vec<u8>>,
417    ) -> &mut Self {
418        self.module_artifacts.push(ModuleArtifact {
419            module_path: module_path.into(),
420            source,
421            compiled,
422        });
423        self
424    }
425
426    /// Register a method intrinsic for fast dispatch on typed Objects.
427    /// Called before callable-property and UFCS fallback in handle_object_method().
428    pub fn add_intrinsic<F>(&mut self, type_name: &str, method_name: &str, f: F) -> &mut Self
429    where
430        F: for<'ctx> Fn(&[ValueWord], &ModuleContext<'ctx>) -> Result<ValueWord, String>
431            + Send
432            + Sync
433            + 'static,
434    {
435        self.method_intrinsics
436            .entry(type_name.to_string())
437            .or_default()
438            .insert(method_name.to_string(), Arc::new(f));
439        self
440    }
441
442    /// Register a type schema that the VM will add to its runtime registry.
443    /// Returns the schema ID for reference.
444    pub fn add_type_schema(&mut self, schema: TypeSchema) -> crate::type_schema::SchemaId {
445        let id = schema.id;
446        self.type_schemas.push(schema);
447        id
448    }
449
450    /// Check if this module exports a given name (sync or async).
451    pub fn has_export(&self, name: &str) -> bool {
452        self.exports.contains_key(name) || self.async_exports.contains_key(name)
453    }
454
455    /// Get a sync exported function by name.
456    pub fn get_export(&self, name: &str) -> Option<&ModuleFn> {
457        self.exports.get(name)
458    }
459
460    /// Get an async exported function by name.
461    pub fn get_async_export(&self, name: &str) -> Option<&AsyncModuleFn> {
462        self.async_exports.get(name)
463    }
464
465    /// Check if a function is async.
466    pub fn is_async(&self, name: &str) -> bool {
467        self.async_exports.contains_key(name)
468    }
469
470    /// Get the schema for an exported function.
471    pub fn get_schema(&self, name: &str) -> Option<&ModuleFunction> {
472        self.schemas.get(name)
473    }
474
475    /// List all export names (sync + async).
476    pub fn export_names(&self) -> Vec<&str> {
477        let mut names: Vec<&str> = self
478            .exports
479            .keys()
480            .chain(self.async_exports.keys())
481            .map(|s| s.as_str())
482            .collect();
483        names.sort_unstable();
484        names.dedup();
485        names
486    }
487
488    /// Convert this module's schema to a `ParsedModuleSchema` for the semantic
489    /// analyzer, mirroring the conversion in `BytecodeExecutor::module_schemas()`.
490    pub fn to_parsed_schema(&self) -> crate::extensions::ParsedModuleSchema {
491        let functions = self
492            .schemas
493            .iter()
494            .filter(|(name, _)| self.is_export_public_surface(name, false))
495            .map(|(name, schema)| crate::extensions::ParsedModuleFunction {
496                name: name.clone(),
497                description: schema.description.clone(),
498                params: schema.params.iter().map(|p| p.type_name.clone()).collect(),
499                return_type: schema.return_type.clone(),
500            })
501            .collect();
502        crate::extensions::ParsedModuleSchema {
503            module_name: self.name.clone(),
504            functions,
505            artifacts: Vec::new(),
506        }
507    }
508
509    /// Return `ParsedModuleSchema` entries for the VM-native stdlib modules
510    /// (regex, http, crypto, env, json). Used during engine initialization
511    /// to make these globals visible at compile time.
512    pub fn stdlib_module_schemas() -> Vec<crate::extensions::ParsedModuleSchema> {
513        vec![
514            crate::stdlib::regex::create_regex_module().to_parsed_schema(),
515            crate::stdlib::http::create_http_module().to_parsed_schema(),
516            crate::stdlib::crypto::create_crypto_module().to_parsed_schema(),
517            crate::stdlib::env::create_env_module().to_parsed_schema(),
518            crate::stdlib::json::create_json_module().to_parsed_schema(),
519            crate::stdlib::toml_module::create_toml_module().to_parsed_schema(),
520            crate::stdlib::yaml::create_yaml_module().to_parsed_schema(),
521            crate::stdlib::xml::create_xml_module().to_parsed_schema(),
522            crate::stdlib::compress::create_compress_module().to_parsed_schema(),
523            crate::stdlib::archive::create_archive_module().to_parsed_schema(),
524            crate::stdlib::parallel::create_parallel_module().to_parsed_schema(),
525            crate::stdlib::unicode::create_unicode_module().to_parsed_schema(),
526            crate::stdlib::csv_module::create_csv_module().to_parsed_schema(),
527            crate::stdlib::msgpack_module::create_msgpack_module().to_parsed_schema(),
528        ]
529    }
530}
531
532impl std::fmt::Debug for ModuleExports {
533    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
534        f.debug_struct("ModuleExports")
535            .field("name", &self.name)
536            .field("description", &self.description)
537            .field("exports", &self.exports.keys().collect::<Vec<_>>())
538            .field(
539                "async_exports",
540                &self.async_exports.keys().collect::<Vec<_>>(),
541            )
542            .field("schemas", &self.schemas.keys().collect::<Vec<_>>())
543            .field(
544                "shape_sources",
545                &self
546                    .shape_sources
547                    .iter()
548                    .map(|(f, _)| f)
549                    .collect::<Vec<_>>(),
550            )
551            .field(
552                "method_intrinsics",
553                &self.method_intrinsics.keys().collect::<Vec<_>>(),
554            )
555            .finish()
556    }
557}
558
559/// Registry of all extension modules.
560///
561/// Created at startup and populated from loaded plugin capabilities.
562#[derive(Default)]
563pub struct ModuleExportRegistry {
564    modules: HashMap<String, ModuleExports>,
565}
566
567impl ModuleExportRegistry {
568    /// Create a new empty registry.
569    pub fn new() -> Self {
570        Self {
571            modules: HashMap::new(),
572        }
573    }
574
575    /// Register a extension module.
576    pub fn register(&mut self, module: ModuleExports) {
577        self.modules.insert(module.name.clone(), module);
578    }
579
580    /// Get a module by name.
581    pub fn get(&self, name: &str) -> Option<&ModuleExports> {
582        self.modules.get(name)
583    }
584
585    /// Check if a module exists.
586    pub fn has(&self, name: &str) -> bool {
587        self.modules.contains_key(name)
588    }
589
590    /// List all registered module names.
591    pub fn module_names(&self) -> Vec<&str> {
592        self.modules.keys().map(|s| s.as_str()).collect()
593    }
594
595    /// Get all registered modules.
596    pub fn modules(&self) -> &HashMap<String, ModuleExports> {
597        &self.modules
598    }
599}
600
601impl std::fmt::Debug for ModuleExportRegistry {
602    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603        f.debug_struct("ModuleExportRegistry")
604            .field("modules", &self.modules.keys().collect::<Vec<_>>())
605            .finish()
606    }
607}
608
609#[cfg(test)]
610#[path = "module_exports_tests.rs"]
611mod tests;