Skip to main content

shape_runtime/engine/
mod.rs

1//! Shape Engine - Unified execution interface
2//!
3//! This module provides a single, unified interface for executing all Shape code,
4//! replacing the multiple specialized executors with one powerful engine.
5
6// Submodules
7mod builder;
8mod execution;
9mod query_extraction;
10mod stdlib;
11mod types;
12
13// Re-export public types
14pub use crate::query_result::QueryType;
15pub use builder::ShapeEngineBuilder;
16use shape_value::KindedSlot;
17pub use types::{
18    EngineBootstrapState, ExecutionMetrics, ExecutionResult, ExecutionType, Message, MessageLevel,
19};
20
21use crate::Runtime;
22use crate::data::DataFrame;
23use shape_ast::error::{Result, ShapeError};
24
25#[cfg(feature = "jit")]
26use std::collections::HashMap;
27
28use crate::hashing::HashDigest;
29use crate::snapshot::{ContextSnapshot, ExecutionSnapshot, SemanticSnapshot, SnapshotStore};
30use serde::Serialize;
31use shape_ast::Program;
32use shape_wire::WireValue;
33
34/// Trait for evaluating individual expressions and statement blocks.
35///
36/// shape-vm implements this for BytecodeExecutor. The previous AST-walking
37/// executor consumers (StreamExecutor, WindowExecutor, JoinExecutor) were
38/// deleted by the strict-typing bulldozer (see `docs/defections.md`
39/// 2026-05-06: AST-evaluation runtime executors deletion). The
40/// `ast-walking-interpreter-strict-rebuild` workstream will reintroduce
41/// streaming/windowed/joined analytics on top of compiled bytecode + typed
42/// VM slots.
43pub trait ExpressionEvaluator: Send + Sync {
44    /// Evaluate a slice of statements and return the result.
45    fn eval_statements(
46        &self,
47        stmts: &[shape_ast::Statement],
48        ctx: &mut crate::context::ExecutionContext,
49    ) -> Result<KindedSlot>;
50
51    /// Evaluate a single expression and return the result.
52    fn eval_expr(
53        &self,
54        expr: &shape_ast::Expr,
55        ctx: &mut crate::context::ExecutionContext,
56    ) -> Result<KindedSlot>;
57}
58
59/// Result from ProgramExecutor::execute_program
60pub struct ProgramExecutorResult {
61    pub wire_value: WireValue,
62    pub type_info: Option<shape_wire::metadata::TypeInfo>,
63    pub execution_type: ExecutionType,
64    pub content_json: Option<serde_json::Value>,
65    pub content_html: Option<String>,
66    pub content_terminal: Option<String>,
67}
68
69/// Trait for executing Shape programs
70pub trait ProgramExecutor {
71    fn execute_program(
72        &mut self,
73        engine: &mut ShapeEngine,
74        program: &Program,
75    ) -> Result<ProgramExecutorResult>;
76}
77
78/// The unified Shape execution engine
79pub struct ShapeEngine {
80    /// The runtime environment
81    pub runtime: Runtime,
82    /// Default data for expressions/assignments
83    pub default_data: DataFrame,
84    /// JIT compilation cache (source hash -> compiled program)
85    #[cfg(feature = "jit")]
86    pub(crate) jit_cache: HashMap<u64, ()>,
87    /// Current source text for error messages (set before execution)
88    pub(crate) current_source: Option<String>,
89    /// Optional snapshot store for resumability
90    pub(crate) snapshot_store: Option<SnapshotStore>,
91    /// Last snapshot ID created
92    pub(crate) last_snapshot: Option<HashDigest>,
93    /// Script path for snapshot metadata
94    pub(crate) script_path: Option<String>,
95    /// Exported symbol names (persisted across REPL commands)
96    pub(crate) exported_symbols: std::collections::HashSet<String>,
97}
98
99impl ShapeEngine {
100    /// Create a new Shape engine
101    pub fn new() -> Result<Self> {
102        let mut runtime = Runtime::new_without_stdlib();
103        runtime.enable_persistent_context_without_data();
104
105        Ok(Self {
106            runtime,
107            default_data: DataFrame::default(),
108            #[cfg(feature = "jit")]
109            jit_cache: HashMap::new(),
110            current_source: None,
111            snapshot_store: None,
112            last_snapshot: None,
113            script_path: None,
114            exported_symbols: std::collections::HashSet::new(),
115        })
116    }
117
118    /// Create engine with data
119    pub fn with_data(data: DataFrame) -> Result<Self> {
120        let mut runtime = Runtime::new_without_stdlib();
121        runtime.enable_persistent_context(&data);
122        Ok(Self {
123            runtime,
124            default_data: data,
125            #[cfg(feature = "jit")]
126            jit_cache: HashMap::new(),
127            current_source: None,
128            snapshot_store: None,
129            last_snapshot: None,
130            script_path: None,
131            exported_symbols: std::collections::HashSet::new(),
132        })
133    }
134
135    /// Create engine with async data provider (Phase 6)
136    ///
137    /// This constructor sets up the engine with an async data provider.
138    /// Call `execute_async()` instead of `execute()` to use async prefetching.
139    pub fn with_async_provider(provider: crate::data::SharedAsyncProvider) -> Result<Self> {
140        let runtime_handle = tokio::runtime::Handle::try_current()
141            .map_err(|_| ShapeError::RuntimeError {
142                message: "No tokio runtime available. Ensure with_async_provider is called within a tokio context.".to_string(),
143                location: None,
144            })?;
145        let mut runtime = Runtime::new_without_stdlib();
146
147        // Create ExecutionContext with async provider
148        let ctx = crate::context::ExecutionContext::with_async_provider(provider, runtime_handle);
149        runtime.set_persistent_context(ctx);
150
151        Ok(Self {
152            runtime,
153            default_data: DataFrame::default(),
154            #[cfg(feature = "jit")]
155            jit_cache: HashMap::new(),
156            current_source: None,
157            snapshot_store: None,
158            last_snapshot: None,
159            script_path: None,
160            exported_symbols: std::collections::HashSet::new(),
161        })
162    }
163
164    /// Initialize REPL mode
165    ///
166    /// Call this once after creating the engine and loading stdlib,
167    /// but before executing any REPL commands. This configures output adapters
168    /// for REPL-friendly display.
169    pub fn init_repl(&mut self) {
170        // Set REPL output adapter to preserve PrintResult spans
171        if let Some(ctx) = self.runtime.persistent_context_mut() {
172            ctx.set_output_adapter(Box::new(crate::output_adapter::ReplAdapter));
173        }
174    }
175
176    /// Capture semantic/runtime state after stdlib bootstrap.
177    ///
178    /// Call this on an engine that has already loaded stdlib.
179    pub fn capture_bootstrap_state(&self) -> Result<EngineBootstrapState> {
180        let context =
181            self.runtime
182                .persistent_context()
183                .cloned()
184                .ok_or_else(|| ShapeError::RuntimeError {
185                    message: "No persistent context available for bootstrap capture".to_string(),
186                    location: None,
187                })?;
188        Ok(EngineBootstrapState {
189            semantic: SemanticSnapshot {
190                exported_symbols: self.exported_symbols.clone(),
191            },
192            context,
193        })
194    }
195
196    /// Apply a previously captured stdlib bootstrap state.
197    pub fn apply_bootstrap_state(&mut self, state: &EngineBootstrapState) {
198        self.exported_symbols = state.semantic.exported_symbols.clone();
199        self.runtime.set_persistent_context(state.context.clone());
200    }
201
202    /// Set the script path for snapshot metadata.
203    pub fn set_script_path(&mut self, path: impl Into<String>) {
204        self.script_path = Some(path.into());
205    }
206
207    /// Get the current script path, if set.
208    pub fn script_path(&self) -> Option<&str> {
209        self.script_path.as_deref()
210    }
211
212    /// Enable snapshotting with a content-addressed store.
213    pub fn enable_snapshot_store(&mut self, store: SnapshotStore) {
214        self.snapshot_store = Some(store);
215    }
216
217    /// Get last snapshot ID, if any.
218    pub fn last_snapshot(&self) -> Option<&HashDigest> {
219        self.last_snapshot.as_ref()
220    }
221
222    /// Access the snapshot store (if configured).
223    pub fn snapshot_store(&self) -> Option<&SnapshotStore> {
224        self.snapshot_store.as_ref()
225    }
226
227    /// Store a serializable blob in the snapshot store and return its hash.
228    pub fn store_snapshot_blob<T: Serialize>(&self, value: &T) -> Result<HashDigest> {
229        let store = self
230            .snapshot_store
231            .as_ref()
232            .ok_or_else(|| ShapeError::RuntimeError {
233                message: "Snapshot store not configured".to_string(),
234                location: None,
235            })?;
236        Ok(store.put_struct(value)?)
237    }
238
239    /// Create a snapshot of semantic/runtime state, with optional VM/bytecode hashes supplied by the executor.
240    pub fn snapshot_with_hashes(
241        &mut self,
242        vm_hash: Option<HashDigest>,
243        bytecode_hash: Option<HashDigest>,
244    ) -> Result<HashDigest> {
245        let store = self
246            .snapshot_store
247            .as_ref()
248            .ok_or_else(|| ShapeError::RuntimeError {
249                message: "Snapshot store not configured".to_string(),
250                location: None,
251            })?;
252
253        let semantic = SemanticSnapshot {
254            exported_symbols: self.exported_symbols.clone(),
255        };
256        let semantic_hash = store.put_struct(&semantic)?;
257
258        let context = if let Some(ctx) = self.runtime.persistent_context() {
259            ctx.snapshot(store)?
260        } else {
261            return Err(ShapeError::RuntimeError {
262                message: "No persistent context for snapshot".to_string(),
263                location: None,
264            });
265        };
266        let context_hash = store.put_struct(&context)?;
267
268        let snapshot = ExecutionSnapshot {
269            version: crate::snapshot::SNAPSHOT_VERSION,
270            created_at_ms: chrono::Utc::now().timestamp_millis(),
271            semantic_hash,
272            context_hash,
273            vm_hash,
274            bytecode_hash,
275            script_path: self.script_path.clone(),
276        };
277
278        let snapshot_hash = store.put_snapshot(&snapshot)?;
279        self.last_snapshot = Some(snapshot_hash.clone());
280        Ok(snapshot_hash)
281    }
282
283    /// Load a snapshot and return its components (semantic/context + optional vm/bytecode hashes).
284    pub fn load_snapshot(
285        &self,
286        snapshot_id: &HashDigest,
287    ) -> Result<(
288        SemanticSnapshot,
289        ContextSnapshot,
290        Option<HashDigest>,
291        Option<HashDigest>,
292    )> {
293        let store = self
294            .snapshot_store
295            .as_ref()
296            .ok_or_else(|| ShapeError::RuntimeError {
297                message: "Snapshot store not configured".to_string(),
298                location: None,
299            })?;
300        let snapshot = store.get_snapshot(snapshot_id)?;
301        let semantic: SemanticSnapshot =
302            store
303                .get_struct(&snapshot.semantic_hash)
304                .map_err(|e| ShapeError::RuntimeError {
305                    message: format!("failed to deserialize SemanticSnapshot: {e}"),
306                    location: None,
307                })?;
308        let context: ContextSnapshot =
309            store
310                .get_struct(&snapshot.context_hash)
311                .map_err(|e| ShapeError::RuntimeError {
312                    message: format!("failed to deserialize ContextSnapshot: {e}"),
313                    location: None,
314                })?;
315        Ok((semantic, context, snapshot.vm_hash, snapshot.bytecode_hash))
316    }
317
318    /// Apply a semantic/context snapshot to the current engine.
319    pub fn apply_snapshot(
320        &mut self,
321        semantic: SemanticSnapshot,
322        context: ContextSnapshot,
323    ) -> Result<()> {
324        // B1.5: snapshot replay can materialise TypedObjects whose layouts
325        // are resolved via the current ambient registry. Install this
326        // runtime's registry for the duration of the restore so the
327        // decoder does not fall back to the legacy global static.
328        let _scope = self.runtime.enter_schema_scope();
329
330        self.exported_symbols = semantic.exported_symbols;
331        if let Some(ctx) = self.runtime.persistent_context_mut() {
332            let store = self
333                .snapshot_store
334                .as_ref()
335                .ok_or_else(|| ShapeError::RuntimeError {
336                    message: "Snapshot store not configured".to_string(),
337                    location: None,
338                })?;
339            ctx.restore_from_snapshot(context, store)?;
340            Ok(())
341        } else {
342            Err(ShapeError::RuntimeError {
343                message: "No persistent context for snapshot".to_string(),
344                location: None,
345            })
346        }
347    }
348
349    /// Register extension module namespaces with the runtime.
350    /// Must be called before execute() so the module loader recognizes modules like `duckdb`.
351    pub fn register_extension_modules(
352        &mut self,
353        modules: &[crate::extensions::ParsedModuleSchema],
354    ) {
355        self.runtime.register_extension_module_artifacts(modules);
356    }
357
358    /// Register Shape source artifacts bundled by loaded language runtime extensions.
359    ///
360    /// Each language runtime extension (e.g. Python, TypeScript) may bundle a
361    /// `.shape` module source that defines the extension's own namespace.
362    /// The namespace is the language identifier itself (e.g. `"python"`,
363    /// `"typescript"`) -- NOT `"std::core::*"`.
364    ///
365    /// Call this after loading extensions but before execute().
366    pub fn register_language_runtime_artifacts(&mut self) {
367        let runtimes = self.language_runtimes();
368        let mut schemas = Vec::new();
369        for (_lang_id, runtime) in &runtimes {
370            match runtime.shape_source() {
371                Ok(Some((namespace, source))) => {
372                    schemas.push(crate::extensions::ParsedModuleSchema {
373                        module_name: namespace.clone(),
374                        functions: Vec::new(),
375                        artifacts: vec![crate::extensions::ParsedModuleArtifact {
376                            module_path: namespace,
377                            source: Some(source),
378                            compiled: None,
379                        }],
380                    });
381                }
382                Ok(None) => {}
383                Err(e) => {
384                    tracing::warn!(
385                        "Failed to get shape source from language runtime: {}",
386                        e
387                    );
388                }
389            }
390        }
391        if !schemas.is_empty() {
392            self.runtime.register_extension_module_artifacts(&schemas);
393        }
394    }
395
396    /// Set the current source text for error messages
397    ///
398    /// Call this before execute() to enable source-contextualized error messages.
399    /// The source is used during bytecode compilation to populate debug info.
400    pub fn set_source(&mut self, source: &str) {
401        self.current_source = Some(source.to_string());
402    }
403
404    /// Get the current source text (if set)
405    pub fn current_source(&self) -> Option<&str> {
406        self.current_source.as_deref()
407    }
408
409    /// Register a data provider (Phase 8)
410    ///
411    /// Registers a named provider for runtime data access.
412    ///
413    /// # Example
414    ///
415    /// ```ignore
416    /// let adapter = Arc::new(DataFrameAdapter::new(...));
417    /// engine.register_provider("data", adapter);
418    /// ```
419    pub fn register_provider(&mut self, name: &str, provider: crate::data::SharedAsyncProvider) {
420        if let Some(ctx) = self.runtime.persistent_context_mut() {
421            ctx.register_provider(name, provider);
422        }
423    }
424
425    /// Set default data provider (Phase 8)
426    ///
427    /// Sets which provider to use for runtime data access when no provider is specified.
428    pub fn set_default_provider(&mut self, name: &str) -> Result<()> {
429        if let Some(ctx) = self.runtime.persistent_context_mut() {
430            ctx.set_default_provider(name)
431        } else {
432            Err(ShapeError::RuntimeError {
433                message: "No execution context available".to_string(),
434                location: None,
435            })
436        }
437    }
438
439    /// Register a type mapping (Phase 8)
440    ///
441    /// Registers a type mapping that defines the expected DataFrame structure
442    /// for a given type name. Type mappings enable validation and JIT optimization.
443    ///
444    /// # Example
445    ///
446    /// ```ignore
447    /// use shape_core::runtime::type_mapping::TypeMapping;
448    ///
449    /// // Register the Candle type (from stdlib)
450    /// let candle_mapping = TypeMapping::new("Candle".to_string())
451    ///     .add_field("timestamp", "timestamp")
452    ///     .add_field("open", "open")
453    ///     .add_field("high", "high")
454    ///     .add_field("low", "low")
455    ///     .add_field("close", "close")
456    ///     .add_field("volume", "volume")
457    ///     .add_required("timestamp")
458    ///     .add_required("open")
459    ///     .add_required("high")
460    ///     .add_required("low")
461    ///     .add_required("close");
462    ///
463    /// engine.register_type_mapping("Candle", candle_mapping);
464    /// ```
465    pub fn register_type_mapping(
466        &mut self,
467        type_name: &str,
468        mapping: crate::type_mapping::TypeMapping,
469    ) {
470        if let Some(ctx) = self.runtime.persistent_context_mut() {
471            ctx.register_type_mapping(type_name, mapping);
472        }
473    }
474
475    /// Get the current runtime state (for REPL)
476    pub fn get_runtime(&self) -> &Runtime {
477        &self.runtime
478    }
479
480    /// Get mutable runtime (for REPL state updates)
481    pub fn get_runtime_mut(&mut self) -> &mut Runtime {
482        &mut self.runtime
483    }
484
485    /// Get the format hint for a variable (if any)
486    ///
487    /// Returns the format hint specified in the variable's type annotation.
488    /// Example: `let rate: Number @ Percent = 0.05` → Some("Percent")
489    pub fn get_variable_format_hint(&self, name: &str) -> Option<String> {
490        self.runtime
491            .persistent_context()
492            .and_then(|ctx| ctx.get_variable_format_hint(name))
493    }
494
495    // ========================================================================
496    // Format Execution (Shape Runtime Formats)
497    // ========================================================================
498
499    /// Format a value using Shape runtime format evaluation
500    ///
501    /// This uses the format definitions from stdlib (e.g., stdlib/core/formats.shape)
502    /// instead of Rust fallback formatters.
503    ///
504    /// # Arguments
505    ///
506    /// * `value` - The value to format (as f64 for numbers)
507    /// * `type_name` - The Shape type name ("Number", "String", etc.)
508    /// * `format_name` - Optional format name (e.g., "Percent", "Currency"). Uses default if None.
509    /// * `params` - Format parameters as JSON (e.g., {"decimals": 1})
510    ///
511    /// # Returns
512    ///
513    /// Formatted string on success
514    ///
515    /// # Example
516    ///
517    /// ```ignore
518    /// let formatted = engine.format_value_string(
519    ///     0.1234,
520    ///     "Number",
521    ///     Some("Percent"),
522    ///     &HashMap::new()
523    /// )?;
524    /// assert_eq!(formatted, "12.34%");
525    /// ```
526    pub fn format_value_string(
527        &mut self,
528        value: f64,
529        type_name: &str,
530        format_name: Option<&str>,
531        params: &std::collections::HashMap<String, serde_json::Value>,
532    ) -> Result<String> {
533        // Resolve type aliases and merge meta parameter overrides
534        let (resolved_type_name, merged_params) =
535            self.resolve_type_alias_for_formatting(type_name, params)?;
536
537        // Convert merged JSON params to wire-format values for the
538        // runtime's `format_value` placeholder. The placeholder ignores
539        // the value/params today and returns `<formatted T>`; per
540        // ADR-006 §2.7.4 the kind-threaded format integration lands in
541        // Phase 2c when the format-specs are wired through `KindedSlot`
542        // bindings rather than ad-hoc `WireValue::Number`.
543        let param_values: std::collections::HashMap<String, WireValue> = merged_params
544            .iter()
545            .map(|(k, v)| {
546                let runtime_val = match v {
547                    serde_json::Value::Number(n) => WireValue::Number(n.as_f64().unwrap_or(0.0)),
548                    serde_json::Value::String(s) => WireValue::String(s.clone()),
549                    serde_json::Value::Bool(b) => WireValue::Bool(*b),
550                    _ => WireValue::Null,
551                };
552                (k.clone(), runtime_val)
553            })
554            .collect();
555
556        let runtime_value = WireValue::Number(value);
557
558        self.runtime.format_value(
559            runtime_value,
560            resolved_type_name.as_str(),
561            format_name,
562            param_values,
563        )
564    }
565
566    /// Resolve type alias to base type and merge meta parameter overrides
567    ///
568    /// If type_name is an alias (e.g., "Percent4"), resolves to base type ("Percent")
569    /// and merges stored parameter overrides with passed params.
570    fn resolve_type_alias_for_formatting(
571        &self,
572        type_name: &str,
573        params: &std::collections::HashMap<String, serde_json::Value>,
574    ) -> Result<(String, std::collections::HashMap<String, serde_json::Value>)> {
575        // Check if type_name is a type alias through the runtime context
576        let resolved = self
577            .runtime
578            .persistent_context()
579            .map(|ctx| ctx.resolve_type_for_format(type_name));
580
581        if let Some((base_type, Some(_overrides))) = resolved {
582            if base_type != type_name {
583                // Phase 1.B (ADR-006 §2.7.4): per-alias overrides are
584                // stored as `KindedSlot`s; converting to `serde_json::Value`
585                // requires the kind-threaded slot decoder that lands in
586                // Phase 2c. Until then, the alias's stored overrides are
587                // ignored and only the caller-supplied `params` are
588                // forwarded. Type aliases without overrides still
589                // resolve correctly.
590                return Ok((base_type, params.clone()));
591            }
592        }
593
594        // Not an alias, use as-is
595        Ok((type_name.to_string(), params.clone()))
596    }
597
598    // ========================================================================
599    // Extension Management
600    // ========================================================================
601
602    /// Load a data source extension from a shared library
603    ///
604    /// # Arguments
605    ///
606    /// * `path` - Path to the extension shared library (.so, .dll, .dylib)
607    /// * `config` - Configuration value for the extension
608    ///
609    /// # Returns
610    ///
611    /// Information about the loaded extension
612    ///
613    /// # Safety
614    ///
615    /// Loading extensions executes arbitrary code. Only load from trusted sources.
616    ///
617    /// # Example
618    ///
619    /// ```ignore
620    /// let info = engine.load_extension(Path::new("./libshape_ext_csv.so"), &json!({}))?;
621    /// println!("Loaded: {} v{}", info.name, info.version);
622    /// ```
623    pub fn load_extension(
624        &mut self,
625        path: &std::path::Path,
626        config: &serde_json::Value,
627    ) -> Result<crate::extensions::LoadedExtension> {
628        if let Some(ctx) = self.runtime.persistent_context_mut() {
629            ctx.load_extension(path, config)
630        } else {
631            Err(ShapeError::RuntimeError {
632                message: "No execution context available for extension loading".to_string(),
633                location: None,
634            })
635        }
636    }
637
638    /// Unload an extension by name
639    ///
640    /// # Arguments
641    ///
642    /// * `name` - Extension name to unload
643    ///
644    /// # Returns
645    ///
646    /// true if plugin was unloaded, false if not found
647    pub fn unload_extension(&mut self, name: &str) -> bool {
648        if let Some(ctx) = self.runtime.persistent_context_mut() {
649            ctx.unload_extension(name)
650        } else {
651            false
652        }
653    }
654
655    /// List all loaded extension names
656    pub fn list_extensions(&self) -> Vec<String> {
657        if let Some(ctx) = self.runtime.persistent_context() {
658            ctx.list_extensions()
659        } else {
660            Vec::new()
661        }
662    }
663
664    /// Get query schema for an extension (for LSP autocomplete)
665    ///
666    /// # Arguments
667    ///
668    /// * `name` - Extension name
669    ///
670    /// # Returns
671    ///
672    /// The query schema if extension exists
673    pub fn get_extension_query_schema(
674        &self,
675        name: &str,
676    ) -> Option<crate::extensions::ParsedQuerySchema> {
677        if let Some(ctx) = self.runtime.persistent_context() {
678            ctx.get_extension_query_schema(name)
679        } else {
680            None
681        }
682    }
683
684    /// Get output schema for an extension (for LSP autocomplete)
685    ///
686    /// # Arguments
687    ///
688    /// * `name` - Extension name
689    ///
690    /// # Returns
691    ///
692    /// The output schema if extension exists
693    pub fn get_extension_output_schema(
694        &self,
695        name: &str,
696    ) -> Option<crate::extensions::ParsedOutputSchema> {
697        if let Some(ctx) = self.runtime.persistent_context() {
698            ctx.get_extension_output_schema(name)
699        } else {
700            None
701        }
702    }
703
704    /// Get an extension data source by name
705    pub fn get_extension(
706        &self,
707        name: &str,
708    ) -> Option<std::sync::Arc<crate::extensions::ExtensionDataSource>> {
709        if let Some(ctx) = self.runtime.persistent_context() {
710            ctx.get_extension(name)
711        } else {
712            None
713        }
714    }
715
716    /// Get extension module schema by module namespace.
717    pub fn get_extension_module_schema(
718        &self,
719        module_name: &str,
720    ) -> Option<crate::extensions::ParsedModuleSchema> {
721        if let Some(ctx) = self.runtime.persistent_context() {
722            ctx.get_extension_module_schema(module_name)
723        } else {
724            None
725        }
726    }
727
728    /// Build VM extension modules from loaded extension module capabilities.
729    pub fn module_exports_from_extensions(&self) -> Vec<crate::module_exports::ModuleExports> {
730        if let Some(ctx) = self.runtime.persistent_context() {
731            ctx.module_exports_from_extensions()
732        } else {
733            Vec::new()
734        }
735    }
736
737    /// Return all loaded language runtimes, keyed by language identifier.
738    pub fn language_runtimes(
739        &self,
740    ) -> std::collections::HashMap<
741        String,
742        std::sync::Arc<crate::plugins::language_runtime::PluginLanguageRuntime>,
743    > {
744        if let Some(ctx) = self.runtime.persistent_context() {
745            ctx.language_runtimes()
746        } else {
747            std::collections::HashMap::new()
748        }
749    }
750
751    /// Invoke one loaded module export via module namespace.
752    pub fn invoke_extension_module_wire(
753        &self,
754        module_name: &str,
755        function: &str,
756        args: &[shape_wire::WireValue],
757    ) -> Result<shape_wire::WireValue> {
758        if let Some(ctx) = self.runtime.persistent_context() {
759            ctx.invoke_extension_module_wire(module_name, function, args)
760        } else {
761            Err(shape_ast::error::ShapeError::RuntimeError {
762                message: "No runtime context available".to_string(),
763                location: None,
764            })
765        }
766    }
767
768    // ========================================================================
769    // Progress Tracking
770    // ========================================================================
771
772    /// Enable progress tracking and return the registry for subscriptions
773    ///
774    /// Call this before executing code that may report progress.
775    /// The returned registry can be used to subscribe to progress events.
776    ///
777    /// # Example
778    ///
779    /// ```ignore
780    /// let registry = engine.enable_progress_tracking();
781    /// let mut receiver = registry.subscribe();
782    ///
783    /// // In a separate task
784    /// while let Ok(event) = receiver.recv().await {
785    ///     println!("Progress: {:?}", event);
786    /// }
787    /// ```
788    pub fn enable_progress_tracking(
789        &mut self,
790    ) -> std::sync::Arc<crate::progress::ProgressRegistry> {
791        // ProgressRegistry::new() already returns Arc<Self>
792        let registry = crate::progress::ProgressRegistry::new();
793        if let Some(ctx) = self.runtime.persistent_context_mut() {
794            ctx.set_progress_registry(registry.clone());
795        }
796        registry
797    }
798
799    /// Get the current progress registry if enabled
800    pub fn progress_registry(&self) -> Option<std::sync::Arc<crate::progress::ProgressRegistry>> {
801        self.runtime
802            .persistent_context()
803            .and_then(|ctx| ctx.progress_registry())
804            .cloned()
805    }
806
807    /// Check if there are pending progress events
808    pub fn has_pending_progress(&self) -> bool {
809        if let Some(registry) = self.progress_registry() {
810            !registry.is_empty()
811        } else {
812            false
813        }
814    }
815
816    /// Poll for progress events (non-blocking)
817    ///
818    /// Returns the next progress event if available, or None if queue is empty.
819    pub fn poll_progress(&self) -> Option<crate::progress::ProgressEvent> {
820        self.progress_registry()
821            .and_then(|registry| registry.try_recv())
822    }
823}
824
825impl Default for ShapeEngine {
826    fn default() -> Self {
827        Self::new().expect("Failed to create default Shape engine")
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834    use crate::extensions::{ParsedModuleArtifact, ParsedModuleSchema};
835
836    #[test]
837    fn test_register_extension_modules_registers_module_loader_artifacts() {
838        let mut engine = ShapeEngine::new().expect("engine should create");
839
840        engine.register_extension_modules(&[ParsedModuleSchema {
841            module_name: "duckdb".to_string(),
842            functions: Vec::new(),
843            artifacts: vec![ParsedModuleArtifact {
844                module_path: "duckdb".to_string(),
845                source: Some("pub fn connect(uri) { uri }".to_string()),
846                compiled: None,
847            }],
848        }]);
849
850        let mut loader = engine.runtime.configured_module_loader();
851        let module = loader
852            .load_module("duckdb")
853            .expect("registered extension module artifact should load");
854        assert!(
855            module.exports.contains_key("connect"),
856            "expected connect export"
857        );
858    }
859}