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