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//!
6//! ## ABI policy (ADR-006 §2.7.5)
7//!
8//! Per ADR-006 §2.7.5, this module enforces the cross-crate ABI split:
9//!
10//! - [`RawCallableInvoker::invoke`] — **stable extension contract**.
11//! Stays on raw `&u64` / `&[u64]` so CFFI extensions don't recompile
12//! when the runtime's internal carrier shape changes. Conversion to/
13//! from [`KindedSlot`] happens **inside `shape-runtime` at the
14//! boundary** (the VM-side `invoke_callable` adapter constructs
15//! `KindedSlot`s from raw bits + the typed registry's `NativeKind`s
16//! before runtime-tier dispatch and unpacks back to `u64` for the
17//! extension call).
18//! - [`ModuleFn`] (internal Rust trait object) — **migrates to
19//! `KindedSlot`**. Lives entirely inside `shape-runtime` with no
20//! recompilation concern.
21//! - [`FrameInfo`] (internal carrier) — **migrates to `KindedSlot`**.
22//! The pre-existing manual `Clone` / `Drop` calling
23//! `value_word_drop::vw_clone` / `vw_drop_slice` collapses to default:
24//! `KindedSlot::Drop` / `Clone` carry the refcount discipline now,
25//! so `Vec<KindedSlot>` push / pop / clone preserve the WB2.4 retain
26//! semantics by construction.
27
28use crate::type_schema::{TypeSchema, TypeSchemaRegistry};
29use shape_value::KindedSlot;
30use std::collections::HashMap;
31use std::ffi::c_void;
32use std::sync::Arc;
33
34/// Raw callable invoker as a function pointer + opaque context.
35///
36/// This is the `Send`-safe, `'static`-safe form of `invoke_callable` that
37/// extensions (e.g., CFFI) can store in long-lived structs like callback
38/// userdata. The context pointer is valid for the duration of the
39/// originating module function call.
40///
41/// Per ADR-006 §2.7.5 the raw-bits signature `(*mut c_void, &u64, &[u64])
42/// -> Result<u64, String>` is the **stable extension contract** — it
43/// does not migrate to [`KindedSlot`]. The conversion to [`KindedSlot`]
44/// happens inside the runtime-side adapter that constructs this invoker
45/// (the adapter reads the parallel `NativeKind` from the typed registry
46/// and builds a `KindedSlot` for runtime-tier dispatch before unpacking
47/// back to `u64` for the extension call).
48#[derive(Clone, Copy)]
49pub struct RawCallableInvoker {
50 pub ctx: *mut c_void,
51 pub invoke: unsafe fn(*mut c_void, &u64, &[u64]) -> Result<u64, String>,
52}
53
54impl RawCallableInvoker {
55 /// Invoke a Shape callable through this raw invoker.
56 ///
57 /// # Safety
58 /// The caller must ensure `self.ctx` is still valid (i.e., the originating
59 /// VM module call is still on the stack).
60 pub unsafe fn call(&self, callable: &u64, args: &[u64]) -> Result<u64, String> {
61 unsafe { (self.invoke)(self.ctx, callable, args) }
62 }
63}
64
65/// Information about a single VM call frame, captured at a point in time.
66///
67/// **Refcount discipline via [`KindedSlot`].** Per ADR-006 §2.7 the
68/// `locals` / `upvalues` / `args` vectors hold [`KindedSlot`]s — the
69/// GENERIC_CARRIER vector form. `KindedSlot`'s explicit `Drop` and
70/// `Clone` impls dispatch on `NativeKind` to retire / bump heap
71/// refcounts, so `Vec<KindedSlot>` push / pop / clone preserve the
72/// WB2.4 / WB2.5 retain-on-read invariant by construction. The manual
73/// `Clone` / `Drop` impls calling `vw_clone` / `vw_drop_slice` that
74/// pre-bulldozer code carried are no longer needed.
75#[derive(Debug, Clone)]
76pub struct FrameInfo {
77 pub function_id: Option<u16>,
78 pub function_name: String,
79 pub blob_hash: Option<[u8; 32]>,
80 pub local_ip: usize,
81 pub locals: Vec<KindedSlot>,
82 pub upvalues: Option<Vec<KindedSlot>>,
83 pub args: Vec<KindedSlot>,
84}
85
86/// Trait providing read access to VM state for state module functions.
87pub trait VmStateAccessor: Send + Sync {
88 fn current_frame(&self) -> Option<FrameInfo>;
89 fn all_frames(&self) -> Vec<FrameInfo>;
90 fn caller_frame(&self) -> Option<FrameInfo>;
91 fn current_args(&self) -> Vec<KindedSlot>;
92 fn current_locals(&self) -> Vec<(String, KindedSlot)>;
93 fn module_bindings(&self) -> Vec<(String, KindedSlot)>;
94 /// Total instruction count at the time of capture. Default impl for compat.
95 fn instruction_count(&self) -> usize {
96 0
97 }
98}
99
100/// Execution context available to module functions during a VM call.
101///
102/// The VM constructs this before each module function dispatch and passes
103/// it by reference.
104pub struct ModuleContext<'a> {
105 /// Type schema registry — lookup types by name or ID.
106 pub schemas: &'a TypeSchemaRegistry,
107
108 /// Invoke a Shape callable (function/closure) from host code.
109 pub invoke_callable:
110 Option<&'a dyn Fn(&KindedSlot, &[KindedSlot]) -> Result<KindedSlot, String>>,
111
112 /// Raw invoker for extensions that need to capture a callable invoker
113 /// beyond the borrow lifetime (e.g., CFFI callback userdata).
114 /// Valid only for the duration of the current module function call.
115 pub raw_invoker: Option<RawCallableInvoker>,
116
117 /// Content-addressed function hashes indexed by function ID.
118 /// Provided by the VM when content-addressed metadata is available.
119 /// Uses raw `[u8; 32]` to avoid a dependency on `shape-vm`'s `FunctionHash`.
120 pub function_hashes: Option<&'a [Option<[u8; 32]>]>,
121
122 /// Read-only access to VM state (call frames, locals, etc.).
123 /// Provided by the VM when state introspection is needed.
124 pub vm_state: Option<&'a dyn VmStateAccessor>,
125
126 /// Permissions granted to the current execution context.
127 /// When `Some`, module functions check this before performing I/O.
128 /// When `None`, all operations are allowed (backwards compatible).
129 pub granted_permissions: Option<shape_abi_v1::PermissionSet>,
130
131 /// Scope constraints for the current execution context.
132 /// Narrows permissions to specific paths, hosts, etc.
133 pub scope_constraints: Option<shape_abi_v1::ScopeConstraints>,
134
135 /// Callback for `state.resume()` to request full VM state restoration.
136 /// The module function stores the snapshot; the dispatch loop applies it
137 /// after the current instruction completes.
138 pub set_pending_resume: Option<&'a dyn Fn(KindedSlot)>,
139
140 /// Callback for `state.resume_frame()` to request mid-function resume.
141 /// Stores (ip_offset, locals) so the dispatch loop can override the
142 /// call frame set up by invoke_callable.
143 pub set_pending_frame_resume: Option<&'a dyn Fn(usize, Vec<KindedSlot>)>,
144}
145
146/// Check whether the current execution context has a required permission.
147///
148/// If `granted_permissions` is `None`, all operations are allowed (backwards
149/// compatible with code that predates the permission system). If `Some`, the
150/// specific permission must be present in the set.
151pub fn check_permission(
152 ctx: &ModuleContext,
153 permission: shape_abi_v1::Permission,
154) -> Result<(), String> {
155 if let Some(ref granted) = ctx.granted_permissions {
156 if !granted.contains(&permission) {
157 return Err(format!(
158 "Permission denied: {} ({})",
159 permission.description(),
160 permission.name()
161 ));
162 }
163 }
164 Ok(())
165}
166
167/// Check permission and enforce filesystem path scope constraints.
168///
169/// After verifying the base permission (`FsRead`, `FsWrite`, or `FsScoped`),
170/// checks `ScopeConstraints::allowed_paths` when present. If the scope
171/// constraints list paths, the target path must match at least one (prefix
172/// match). An empty `allowed_paths` list means all paths are permitted.
173pub fn check_fs_permission(
174 ctx: &ModuleContext,
175 permission: shape_abi_v1::Permission,
176 path: &str,
177) -> Result<(), String> {
178 check_permission(ctx, permission)?;
179
180 if let Some(ref constraints) = ctx.scope_constraints {
181 if !constraints.allowed_paths.is_empty() {
182 let target = std::path::Path::new(path);
183 let allowed = constraints.allowed_paths.iter().any(|pattern| {
184 // Support glob-style prefix matching: "/data/**" matches
185 // anything under /data/, and "/tmp/*" matches direct children.
186 let pattern = pattern.trim_end_matches("**").trim_end_matches('*');
187 let prefix = std::path::Path::new(pattern.trim_end_matches('/'));
188 target.starts_with(prefix)
189 });
190 if !allowed {
191 return Err(format!(
192 "Scope constraint denied: path '{}' is not in allowed paths",
193 path
194 ));
195 }
196 }
197 }
198 Ok(())
199}
200
201/// Check permission and enforce network host scope constraints.
202///
203/// After verifying the base permission (`NetConnect`, `NetListen`, or
204/// `NetScoped`), checks `ScopeConstraints::allowed_hosts` when present.
205/// If the scope constraints list hosts, the target address must match at
206/// least one (supports `host:port` and `*.domain.com` wildcards).
207pub fn check_net_permission(
208 ctx: &ModuleContext,
209 permission: shape_abi_v1::Permission,
210 address: &str,
211) -> Result<(), String> {
212 check_permission(ctx, permission)?;
213
214 if let Some(ref constraints) = ctx.scope_constraints {
215 if !constraints.allowed_hosts.is_empty() {
216 // Extract host (and optional port) from the address.
217 let target_host = address.split(':').next().unwrap_or(address);
218 let allowed = constraints.allowed_hosts.iter().any(|pattern| {
219 let pattern_host = pattern.split(':').next().unwrap_or(pattern);
220 // Wildcard: *.example.com matches sub.example.com
221 if let Some(suffix) = pattern_host.strip_prefix("*.") {
222 target_host.ends_with(suffix) && target_host.len() > suffix.len()
223 } else {
224 // Exact host match (port part is ignored for scope check)
225 target_host == pattern_host
226 }
227 });
228 if !allowed {
229 return Err(format!(
230 "Scope constraint denied: address '{}' is not in allowed hosts",
231 address
232 ));
233 }
234 }
235 }
236 Ok(())
237}
238
239/// A module function callable from Shape (synchronous).
240///
241/// Per ADR-006 §2.7.5 (cross-crate ABI policy), this internal Rust trait
242/// object migrates to [`KindedSlot`] — it lives entirely inside
243/// `shape-runtime` with no cross-crate ABI concern. Stable extension
244/// contracts (`RawCallableInvoker::invoke`) stay on raw bits.
245///
246/// Takes a slice of `KindedSlot` arguments plus a `ModuleContext` that
247/// provides access to the type schema registry and a callable invoker.
248/// The function must be `Send + Sync` for thread safety.
249pub type ModuleFn = Arc<
250 dyn for<'ctx> Fn(&[KindedSlot], &ModuleContext<'ctx>) -> Result<KindedSlot, String>
251 + Send
252 + Sync,
253>;
254
255/// One entry in the VM's per-process module-function table
256/// (`module_fn_table`), indexed by positional `u32` id.
257///
258/// Phase 4c.4: the legacy `ModuleFn` ABI escape hatch was deleted. All
259/// stdlib and test fixtures route through the typed registry.
260///
261/// - [`Self::Typed`]: synchronous typed-return native function. The
262/// body returns [`crate::typed_module_exports::TypedReturn`] directly;
263/// the dispatch boundary projects the typed return into a kinded slot
264/// per ADR-006 §2.7 — no round-trip through a synthesized runtime
265/// value.
266/// - [`Self::TypedAsync`]: async typed-return native function. The body
267/// returns a future of `TypedReturn`; the synchronous dispatch path
268/// blocks on the future and applies the same kind-threaded projection
269/// at the boundary.
270#[derive(Clone)]
271pub enum ModuleFnEntry {
272 Typed(crate::typed_module_exports::TypedModuleFunction),
273 TypedAsync(crate::typed_module_exports::TypedModuleAsyncFunction),
274}
275
276/// Visibility policy for one extension export.
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub enum ModuleExportVisibility {
279 /// Normal module API: available in runtime + comptime contexts.
280 Public,
281 /// Only callable from comptime contexts.
282 ComptimeOnly,
283 /// Internal helper: callable but hidden from normal user-facing discovery.
284 Internal,
285}
286
287impl Default for ModuleExportVisibility {
288 fn default() -> Self {
289 Self::Public
290 }
291}
292
293/// Schema for a single parameter of a module function.
294/// Used by LSP for completions and by validation for type checking.
295#[derive(Debug, Clone)]
296pub struct ModuleParam {
297 pub name: String,
298 pub type_name: String,
299 pub required: bool,
300 pub description: String,
301 pub default_snippet: Option<String>,
302 pub allowed_values: Option<Vec<String>>,
303 pub nested_params: Option<Vec<ModuleParam>>,
304}
305
306impl Default for ModuleParam {
307 fn default() -> Self {
308 Self {
309 name: String::new(),
310 type_name: "any".to_string(),
311 required: false,
312 description: String::new(),
313 default_snippet: None,
314 allowed_values: None,
315 nested_params: None,
316 }
317 }
318}
319
320/// Schema for a module function — describes parameters and return type.
321/// Used by LSP for completions, hover, and signature help.
322#[derive(Debug, Clone)]
323pub struct ModuleFunction {
324 pub description: String,
325 pub params: Vec<ModuleParam>,
326 pub return_type: Option<String>,
327}
328
329/// Bundled module artifact from an extension.
330#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct ModuleArtifact {
332 /// Import path for this module (e.g. "duckdb", "duckdb.query")
333 pub module_path: String,
334 /// Optional Shape source payload.
335 pub source: Option<String>,
336 /// Optional precompiled payload (opaque host format).
337 pub compiled: Option<Vec<u8>>,
338}
339
340/// A Rust-implemented module exposed via `<name>`.
341#[derive(Clone)]
342pub struct ModuleExports {
343 /// Module name (e.g., "csv", "json", "duckdb")
344 pub name: String,
345 /// Human-readable description of this module
346 pub description: String,
347 /// Function schemas for LSP + validation: name → schema
348 pub schemas: HashMap<String, ModuleFunction>,
349 /// Export visibility controls: name → visibility.
350 pub export_visibility: HashMap<String, ModuleExportVisibility>,
351 /// Shape source files bundled with this extension.
352 /// Compiled and merged with core stdlib at startup.
353 /// Vec of (filename, source_code) pairs.
354 ///
355 /// Legacy compatibility field. New code should use `module_artifacts`.
356 pub shape_sources: Vec<(String, String)>,
357 /// Bundled module artifacts (source/compiled/both).
358 pub module_artifacts: Vec<ModuleArtifact>,
359 /// Method intrinsics for fast dispatch on typed Objects.
360 /// Outer key: type name (e.g., "DuckDbQuery")
361 /// Inner key: method name (e.g., "build_sql")
362 /// Dispatched BEFORE callable-property and UFCS fallback.
363 pub method_intrinsics: HashMap<String, HashMap<String, ModuleFn>>,
364 /// Type schemas to register in the VM's runtime TypeSchemaRegistry.
365 /// Extensions can use this to declare types that the runtime can use
366 /// for TypedObject creation and field validation.
367 pub type_schemas: Vec<TypeSchema>,
368 /// Typed-return ABI registry (Phase 4b).
369 ///
370 /// Authoritative registry for native-module function bodies. Every
371 /// export here declares its return type via
372 /// [`crate::typed_module_exports::TypedReturn`] / [`crate::typed_module_exports::ConcreteType`];
373 /// the kind-threaded marshal layer projects the typed return into
374 /// a kinded slot at the dispatch boundary inside the VM, not in
375 /// the body (ADR-006 §2.7). Phase 4c.4 deleted the legacy
376 /// `exports`/`async_exports` `ModuleFn` parallel registry — every
377 /// callable function body lives here.
378 pub typed_exports: crate::typed_module_exports::TypedModuleExports,
379}
380
381impl ModuleExports {
382 /// Create a new extension module.
383 pub fn new(name: impl Into<String>) -> Self {
384 Self {
385 name: name.into(),
386 description: String::new(),
387 schemas: HashMap::new(),
388 export_visibility: HashMap::new(),
389 shape_sources: Vec::new(),
390 module_artifacts: Vec::new(),
391 method_intrinsics: HashMap::new(),
392 type_schemas: Vec::new(),
393 typed_exports: crate::typed_module_exports::TypedModuleExports::new(),
394 }
395 }
396
397 /// Mutable access to the typed-return registry. Used by
398 /// [`crate::typed_module_exports::register_typed_function`] to record
399 /// the typed-body entry.
400 pub fn typed_exports_mut(
401 &mut self,
402 ) -> &mut crate::typed_module_exports::TypedModuleExports {
403 &mut self.typed_exports
404 }
405
406 /// Read-only access to the typed-return registry.
407 pub fn typed_exports(&self) -> &crate::typed_module_exports::TypedModuleExports {
408 &self.typed_exports
409 }
410
411 /// Register only the LSP/validation schema and visibility for an
412 /// exported name. The actual function body lives in `typed_exports`
413 /// and is dispatched directly via `ModuleFnEntry::Typed` /
414 /// `ModuleFnEntry::TypedAsync` — see
415 /// `register_typed_function`/`register_typed_async_function` and the
416 /// test-only `register_test_function*` helpers.
417 pub fn add_schema_only(
418 &mut self,
419 name: impl Into<String>,
420 schema: ModuleFunction,
421 ) -> &mut Self {
422 let name = name.into();
423 self.schemas.insert(name.clone(), schema);
424 self.export_visibility.entry(name).or_default();
425 self
426 }
427
428 /// Set visibility for one export name.
429 pub fn set_export_visibility(
430 &mut self,
431 name: impl Into<String>,
432 visibility: ModuleExportVisibility,
433 ) -> &mut Self {
434 self.export_visibility.insert(name.into(), visibility);
435 self
436 }
437
438 /// Resolve visibility for one export (defaults to Public).
439 pub fn export_visibility(&self, name: &str) -> ModuleExportVisibility {
440 self.export_visibility
441 .get(name)
442 .copied()
443 .unwrap_or_default()
444 }
445
446 /// Return true when the export can be called in the current compiler mode.
447 pub fn is_export_available(&self, name: &str, comptime_mode: bool) -> bool {
448 match self.export_visibility(name) {
449 ModuleExportVisibility::Public => true,
450 ModuleExportVisibility::ComptimeOnly => comptime_mode,
451 ModuleExportVisibility::Internal => true,
452 }
453 }
454
455 /// Return true when the export should appear in user-facing completion/hover surfaces.
456 pub fn is_export_public_surface(&self, name: &str, comptime_mode: bool) -> bool {
457 match self.export_visibility(name) {
458 ModuleExportVisibility::Public => true,
459 ModuleExportVisibility::ComptimeOnly => comptime_mode,
460 ModuleExportVisibility::Internal => false,
461 }
462 }
463
464 /// List exports available for the requested mode (sync + async).
465 pub fn export_names_available(&self, comptime_mode: bool) -> Vec<&str> {
466 self.export_names()
467 .into_iter()
468 .filter(|name| self.is_export_available(name, comptime_mode))
469 .collect()
470 }
471
472 /// List user-facing exports for completion/hover (sync + async).
473 pub fn export_names_public_surface(&self, comptime_mode: bool) -> Vec<&str> {
474 self.export_names()
475 .into_iter()
476 .filter(|name| self.is_export_public_surface(name, comptime_mode))
477 .collect()
478 }
479
480 /// Bundle a Shape source file with this extension.
481 /// The source will be compiled and merged with stdlib at startup.
482 pub fn add_shape_source(&mut self, filename: &str, source: &str) -> &mut Self {
483 self.module_artifacts.push(ModuleArtifact {
484 module_path: filename.to_string(),
485 source: Some(source.to_string()),
486 compiled: None,
487 });
488 self.shape_sources
489 .push((filename.to_string(), source.to_string()));
490 self
491 }
492
493 /// Register a bundled module artifact (source/compiled/both).
494 pub fn add_shape_artifact(
495 &mut self,
496 module_path: impl Into<String>,
497 source: Option<String>,
498 compiled: Option<Vec<u8>>,
499 ) -> &mut Self {
500 self.module_artifacts.push(ModuleArtifact {
501 module_path: module_path.into(),
502 source,
503 compiled,
504 });
505 self
506 }
507
508 /// Register a method intrinsic for fast dispatch on typed Objects.
509 /// Called before callable-property and UFCS fallback in handle_object_method().
510 pub fn add_intrinsic<F>(&mut self, type_name: &str, method_name: &str, f: F) -> &mut Self
511 where
512 F: for<'ctx> Fn(&[KindedSlot], &ModuleContext<'ctx>) -> Result<KindedSlot, String>
513 + Send
514 + Sync
515 + 'static,
516 {
517 self.method_intrinsics
518 .entry(type_name.to_string())
519 .or_default()
520 .insert(method_name.to_string(), Arc::new(f));
521 self
522 }
523
524 /// Register a type schema that the VM will add to its runtime registry.
525 /// Returns the schema ID for reference.
526 pub fn add_type_schema(&mut self, schema: TypeSchema) -> crate::type_schema::SchemaId {
527 let id = schema.id;
528 self.type_schemas.push(schema);
529 id
530 }
531
532 /// Check if this module exports a given name (sync or async).
533 pub fn has_export(&self, name: &str) -> bool {
534 self.typed_exports.functions.contains_key(name)
535 || self.typed_exports.async_functions.contains_key(name)
536 }
537
538 // `invoke_export` and `TypedReturn::into_value_word()` are deleted.
539 // The replacement is the Phase 2b kind-threaded marshal layer that
540 // projects `TypedReturn` directly into a typed slot without round-
541 // tripping through a synthesized runtime value.
542
543 /// Check if a function is async.
544 pub fn is_async(&self, name: &str) -> bool {
545 self.typed_exports.async_functions.contains_key(name)
546 }
547
548 /// Get the schema for an exported function.
549 pub fn get_schema(&self, name: &str) -> Option<&ModuleFunction> {
550 self.schemas.get(name)
551 }
552
553 /// List all export names (sync + async).
554 pub fn export_names(&self) -> Vec<&str> {
555 let mut names: Vec<&str> = self
556 .typed_exports
557 .functions
558 .keys()
559 .chain(self.typed_exports.async_functions.keys())
560 .map(|s| s.as_str())
561 .collect();
562 names.sort_unstable();
563 names.dedup();
564 names
565 }
566
567 /// Convert this module's schema to a `ParsedModuleSchema` for the semantic
568 /// analyzer, mirroring the conversion in `BytecodeExecutor::module_schemas()`.
569 pub fn to_parsed_schema(&self) -> crate::extensions::ParsedModuleSchema {
570 let functions = self
571 .schemas
572 .iter()
573 .filter(|(name, _)| self.is_export_public_surface(name, false))
574 .map(|(name, schema)| crate::extensions::ParsedModuleFunction {
575 name: name.clone(),
576 description: schema.description.clone(),
577 params: schema.params.iter().map(|p| p.type_name.clone()).collect(),
578 return_type: schema.return_type.clone(),
579 })
580 .collect();
581 crate::extensions::ParsedModuleSchema {
582 module_name: self.name.clone(),
583 functions,
584 artifacts: Vec::new(),
585 }
586 }
587
588 /// Return `ParsedModuleSchema` entries for all shipped native stdlib modules.
589 /// Used during engine initialization to make these globals visible at compile time.
590 pub fn stdlib_module_schemas() -> Vec<crate::extensions::ParsedModuleSchema> {
591 crate::stdlib::all_stdlib_modules()
592 .into_iter()
593 .map(|m| m.to_parsed_schema())
594 .collect()
595 }
596}
597
598impl std::fmt::Debug for ModuleExports {
599 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600 f.debug_struct("ModuleExports")
601 .field("name", &self.name)
602 .field("description", &self.description)
603 .field(
604 "typed_exports",
605 &self
606 .typed_exports
607 .functions
608 .keys()
609 .chain(self.typed_exports.async_functions.keys())
610 .collect::<Vec<_>>(),
611 )
612 .field("schemas", &self.schemas.keys().collect::<Vec<_>>())
613 .field(
614 "shape_sources",
615 &self
616 .shape_sources
617 .iter()
618 .map(|(f, _)| f)
619 .collect::<Vec<_>>(),
620 )
621 .field(
622 "method_intrinsics",
623 &self.method_intrinsics.keys().collect::<Vec<_>>(),
624 )
625 .finish()
626 }
627}
628
629/// Registry of all extension modules.
630///
631/// Created at startup and populated from loaded plugin capabilities.
632/// Lookup is by canonical path only (e.g. `"std::core::json"`).
633#[derive(Default)]
634pub struct ModuleExportRegistry {
635 modules: HashMap<String, ModuleExports>,
636}
637
638impl ModuleExportRegistry {
639 /// Create a new empty registry.
640 pub fn new() -> Self {
641 Self {
642 modules: HashMap::new(),
643 }
644 }
645
646 /// Register a extension module.
647 pub fn register(&mut self, module: ModuleExports) {
648 let canonical = module.name.clone();
649 self.modules.insert(canonical, module);
650 }
651
652 /// Get a module by canonical name.
653 pub fn get(&self, name: &str) -> Option<&ModuleExports> {
654 self.modules.get(name)
655 }
656
657 /// Check if a module exists by canonical name.
658 pub fn has(&self, name: &str) -> bool {
659 self.get(name).is_some()
660 }
661
662 /// List all registered module names.
663 pub fn module_names(&self) -> Vec<&str> {
664 self.modules.keys().map(|s| s.as_str()).collect()
665 }
666
667 /// Get all registered modules.
668 pub fn modules(&self) -> &HashMap<String, ModuleExports> {
669 &self.modules
670 }
671}
672
673impl std::fmt::Debug for ModuleExportRegistry {
674 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
675 f.debug_struct("ModuleExportRegistry")
676 .field("modules", &self.modules.keys().collect::<Vec<_>>())
677 .finish()
678 }
679}
680
681#[cfg(test)]
682#[path = "module_exports_tests.rs"]
683mod tests;