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}