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