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