Skip to main content

shape_runtime/engine/
execution.rs

1//! Main execution methods for Shape engine
2
3use super::types::{ExecutionMetrics, ExecutionResult, ExecutionType};
4use crate::type_schema::with_async_scope;
5use shape_ast::error::Result;
6use shape_ast::parser;
7
8impl super::ShapeEngine {
9    /// Execute a Shape program from source code (sync mode)
10    ///
11    /// This method allows blocking data loads (REPL mode).
12    /// For scripts/backtests, use execute_async() instead.
13    pub fn execute(
14        &mut self,
15        executor: &mut impl super::ProgramExecutor,
16        source: &str,
17    ) -> Result<ExecutionResult> {
18        // Set REPL/sync mode - allow blocking loads
19        if let Some(ctx) = self.runtime.persistent_context_mut() {
20            ctx.set_data_load_mode(crate::context::DataLoadMode::Sync);
21        }
22
23        self.execute_with_options(executor, source, false)
24    }
25
26    /// Execute a Shape program with async prefetching (Phase 6/8)
27    ///
28    /// This method:
29    /// 1. Sets Async data mode (runtime data requests use cache, not blocking)
30    /// 2. Parses and analyzes the program
31    /// 3. Determines required data (symbols/timeframes)
32    /// 4. Prefetches data concurrently
33    /// 5. Executes synchronously using cached data
34    ///
35    /// # Example
36    ///
37    /// ```ignore
38    /// let provider = DataFrameAdapter::new(...);
39    /// let mut engine = ShapeEngine::with_async_provider(provider)?;
40    /// let result = engine.execute_async(&interpreter, "let sma = close.sma(20)").await?;
41    /// ```
42    pub async fn execute_async(
43        &mut self,
44        executor: &mut impl super::ProgramExecutor,
45        source: &str,
46    ) -> Result<ExecutionResult> {
47        // Set async mode - runtime data requests must use cache
48        if let Some(ctx) = self.runtime.persistent_context_mut() {
49            ctx.set_data_load_mode(crate::context::DataLoadMode::Async);
50        }
51
52        // Install this runtime's TypeSchemaRegistry as the task-local
53        // ambient for the duration of this execution.
54        let schema_registry = self.runtime.schema_registry_arc();
55        with_async_scope(schema_registry, self.execute_async_inner(executor, source)).await
56    }
57
58    async fn execute_async_inner(
59        &mut self,
60        executor: &mut impl super::ProgramExecutor,
61        source: &str,
62    ) -> Result<ExecutionResult> {
63        let start_time = std::time::Instant::now();
64
65        // Parse the source
66        let parse_start = std::time::Instant::now();
67        let mut program = parser::parse_program(source)?;
68        let parse_time_ms = parse_start.elapsed().as_millis() as u64;
69
70        // Desugar high-level syntax (e.g., from-queries to method chains) before analysis
71        shape_ast::transform::desugar_program(&mut program);
72
73        let analysis_start = std::time::Instant::now();
74        let analysis_time_ms = analysis_start.elapsed().as_millis() as u64;
75
76        // Prefetch data if using async provider
77        let has_cache = self
78            .runtime
79            .persistent_context()
80            .map(|ctx| ctx.has_data_cache())
81            .unwrap_or(false);
82
83        if has_cache {
84            // Extract symbols/timeframes from program
85            let queries = self.extract_data_queries(&program)?;
86
87            // Prefetch all required data concurrently
88            if let Some(ctx) = self.runtime.persistent_context_mut() {
89                ctx.prefetch_data(queries).await?;
90            }
91        }
92
93        // Store source text for error messages during execution
94        self.set_source(source);
95
96        // Execute synchronously using cached data
97        let runtime_start = std::time::Instant::now();
98        let result = executor.execute_program(self, &program)?;
99        let runtime_time_ms = runtime_start.elapsed().as_millis() as u64;
100
101        let total_time_ms = start_time.elapsed().as_millis() as u64;
102        let memory_used_bytes = self.estimate_memory_usage();
103        let rows_processed = Some(self.default_data.row_count());
104        let messages = self.collect_messages();
105
106        Ok(ExecutionResult {
107            value: result.wire_value,
108            type_info: result.type_info,
109            execution_type: result.execution_type,
110            metrics: ExecutionMetrics {
111                execution_time_ms: total_time_ms,
112                parse_time_ms,
113                analysis_time_ms,
114                runtime_time_ms,
115                memory_used_bytes,
116                rows_processed,
117            },
118            messages,
119            content_json: result.content_json,
120            content_html: result.content_html,
121            content_terminal: result.content_terminal,
122        })
123    }
124
125    /// Execute a REPL command with persistent state
126    ///
127    /// Unlike `execute_async`, this uses incremental analysis where variables
128    /// and functions persist across commands. Call `init_repl()` once before
129    /// the first call to this method.
130    pub async fn execute_repl(
131        &mut self,
132        executor: &mut impl super::ProgramExecutor,
133        source: &str,
134    ) -> Result<ExecutionResult> {
135        // Set async mode - runtime data requests must use cache
136        if let Some(ctx) = self.runtime.persistent_context_mut() {
137            ctx.set_data_load_mode(crate::context::DataLoadMode::Async);
138        }
139
140        // Install this runtime's TypeSchemaRegistry as the task-local
141        // ambient for the duration of this REPL command.
142        let schema_registry = self.runtime.schema_registry_arc();
143        with_async_scope(schema_registry, self.execute_repl_inner(executor, source)).await
144    }
145
146    async fn execute_repl_inner(
147        &mut self,
148        executor: &mut impl super::ProgramExecutor,
149        source: &str,
150    ) -> Result<ExecutionResult> {
151        let start_time = std::time::Instant::now();
152
153        // Parse the source
154        let parse_start = std::time::Instant::now();
155        let mut program = parser::parse_program(source)?;
156        let parse_time_ms = parse_start.elapsed().as_millis() as u64;
157
158        // Desugar high-level syntax (e.g., from-queries to method chains) before analysis
159        shape_ast::transform::desugar_program(&mut program);
160
161        let analysis_start = std::time::Instant::now();
162        let analysis_time_ms = analysis_start.elapsed().as_millis() as u64;
163
164        // Prefetch data if using async provider
165        let has_cache = self
166            .runtime
167            .persistent_context()
168            .map(|ctx| ctx.has_data_cache())
169            .unwrap_or(false);
170
171        if has_cache {
172            let queries = self.extract_data_queries(&program)?;
173            if let Some(ctx) = self.runtime.persistent_context_mut() {
174                ctx.prefetch_data(queries).await?;
175            }
176        }
177
178        // Process imports and declarations before execution.
179        //
180        // REPL cross-cell persistence (WS-11): definition-item injection
181        // and module-binding round-trip both happen inside
182        // `ProgramExecutor::execute_program` (it owns the only
183        // cross-cell-stable handle — `ShapeEngine` — and is the single
184        // path shared by every executor caller). `load_program` only
185        // needs the cell's own items; the executor re-injects prior
186        // definitions before compilation.
187        self.runtime.load_program(&program, &self.default_data)?;
188
189        // Store source text for error messages during execution
190        self.set_source(source);
191
192        // Execute
193        let runtime_start = std::time::Instant::now();
194        let result = executor.execute_program(self, &program)?;
195        let runtime_time_ms = runtime_start.elapsed().as_millis() as u64;
196
197        let total_time_ms = start_time.elapsed().as_millis() as u64;
198        let memory_used_bytes = self.estimate_memory_usage();
199        let rows_processed = Some(self.default_data.row_count());
200        let messages = self.collect_messages();
201
202        Ok(ExecutionResult {
203            value: result.wire_value,
204            type_info: result.type_info,
205            execution_type: ExecutionType::Repl,
206            metrics: ExecutionMetrics {
207                execution_time_ms: total_time_ms,
208                parse_time_ms,
209                analysis_time_ms,
210                runtime_time_ms,
211                memory_used_bytes,
212                rows_processed,
213            },
214            messages,
215            content_json: result.content_json,
216            content_html: result.content_html,
217            content_terminal: result.content_terminal,
218        })
219    }
220
221    /// Parse and analyze source code without executing it.
222    ///
223    /// Returns the analyzed AST `Program`, ready for compilation.
224    /// Used by the recompile-and-resume flow.
225    pub fn parse_and_analyze(&mut self, source: &str) -> Result<shape_ast::Program> {
226        // Parse/desugar may touch ambient schema state (e.g. comptime
227        // builtins). Install this runtime's registry for the call.
228        let _scope = self.runtime.enter_schema_scope();
229
230        if let Some(ctx) = self.runtime.persistent_context_mut() {
231            ctx.reset_for_new_execution();
232        }
233        let mut program = parser::parse_program(source)?;
234        shape_ast::transform::desugar_program(&mut program);
235        self.set_source(source);
236        Ok(program)
237    }
238
239    /// Execute a Shape program with options
240    pub(super) fn execute_with_options(
241        &mut self,
242        executor: &mut impl super::ProgramExecutor,
243        source: &str,
244        _is_stdlib: bool,
245    ) -> Result<ExecutionResult> {
246        // Install this runtime's TypeSchemaRegistry as the thread-local
247        // ambient for the duration of synchronous execution.
248        let _scope = self.runtime.enter_schema_scope();
249
250        let start_time = std::time::Instant::now();
251
252        // Always reset variable scopes before each execution
253        if let Some(ctx) = self.runtime.persistent_context_mut() {
254            ctx.reset_for_new_execution();
255        }
256
257        // Check for deprecated APIs before parsing (the deprecated syntax may not parse cleanly)
258        Self::check_deprecated_apis(source)?;
259
260        // Parse the source
261        let parse_start = std::time::Instant::now();
262        let mut program = parser::parse_program(source)?;
263        let parse_time_ms = parse_start.elapsed().as_millis() as u64;
264
265        // Desugar high-level syntax (e.g., from-queries to method chains) before analysis
266        shape_ast::transform::desugar_program(&mut program);
267
268        let analysis_start = std::time::Instant::now();
269        let analysis_time_ms = analysis_start.elapsed().as_millis() as u64;
270
271        // Store source text for error messages during execution
272        self.set_source(source);
273
274        // Execute the program
275        let runtime_start = std::time::Instant::now();
276        let result = executor.execute_program(self, &program)?;
277        let runtime_time_ms = runtime_start.elapsed().as_millis() as u64;
278
279        let total_time_ms = start_time.elapsed().as_millis() as u64;
280
281        // Get memory usage estimate (heap allocation approximation)
282        let memory_used_bytes = self.estimate_memory_usage();
283
284        // Get rows processed count from market data
285        let rows_processed = Some(self.default_data.row_count());
286
287        // Collect any messages from the runtime
288        let messages = self.collect_messages();
289
290        Ok(ExecutionResult {
291            value: result.wire_value,
292            type_info: result.type_info,
293            execution_type: result.execution_type,
294            metrics: ExecutionMetrics {
295                execution_time_ms: total_time_ms,
296                parse_time_ms,
297                analysis_time_ms,
298                runtime_time_ms,
299                memory_used_bytes,
300                rows_processed,
301            },
302            messages,
303            content_json: result.content_json,
304            content_html: result.content_html,
305            content_terminal: result.content_terminal,
306        })
307    }
308
309    /// Execute a REPL command
310    pub fn execute_repl_command(
311        &mut self,
312        executor: &mut impl super::ProgramExecutor,
313        command: &str,
314    ) -> Result<ExecutionResult> {
315        let mut result = self.execute(executor, command)?;
316        result.execution_type = ExecutionType::Repl;
317        Ok(result)
318    }
319
320    /// Check for deprecated APIs before parsing. Some deprecated call syntax may
321    /// not parse cleanly (e.g. escaped quotes in raw strings), so we detect them
322    /// via pattern matching on the source text and produce helpful diagnostics.
323    fn check_deprecated_apis(source: &str) -> Result<()> {
324        let trimmed = source.trim();
325        if trimmed.starts_with("csv.load") || trimmed.contains("csv.load(") {
326            return Err(shape_ast::error::ShapeError::SemanticError {
327                message: "csv.load has been removed. Use the csv package instead: import { read } from \"csv\""
328                    .to_string(),
329                location: None,
330            });
331        }
332        // Check for bare load(provider, params) — the global load function was removed
333        if trimmed.starts_with("load(") || trimmed.starts_with("load (") {
334            return Err(shape_ast::error::ShapeError::SemanticError {
335                message: "load(provider, params) has been removed. Use typed data access instead: data(\"source\", { ... })"
336                    .to_string(),
337                location: None,
338            });
339        }
340        Ok(())
341    }
342
343    /// Estimate memory usage based on runtime state
344    pub(super) fn estimate_memory_usage(&self) -> Option<usize> {
345        // Estimate based on known allocations
346        let mut total = 0usize;
347
348        // Market data rows (each row ~48 bytes for 6 f64 values)
349        total += self.default_data.row_count() * 48;
350
351        // Variable storage estimate (rough approximation)
352        // This is a simplified estimate - real tracking would require custom allocator
353        total += 1024; // Base overhead for runtime structures
354
355        Some(total)
356    }
357
358    /// Collect messages from runtime execution
359    pub(super) fn collect_messages(&self) -> Vec<super::types::Message> {
360        // Currently the runtime doesn't track messages, but this provides the interface
361        // for future implementation
362        vec![]
363    }
364
365    /// Definition items accumulated from prior REPL cells (WS-11).
366    ///
367    /// The `ProgramExecutor` re-prepends these to each new cell's
368    /// program before bytecode compilation so that `fn` / `type` /
369    /// `enum` / `trait` / `impl` / type-alias / annotation declarations
370    /// from earlier lines remain resolvable. Empty unless [`init_repl`]
371    /// enabled cross-cell persistence.
372    pub fn repl_definitions(&self) -> &[shape_ast::ast::Item] {
373        &self.repl_definitions
374    }
375
376    /// Whether cross-cell persistence is active AND there is at least
377    /// one accumulated definition to inject.
378    pub fn has_repl_definitions(&self) -> bool {
379        self.repl_persistence && !self.repl_definitions.is_empty()
380    }
381
382    /// Harvest a successfully-executed cell's definition items into the
383    /// cross-cell accumulator (WS-11).
384    ///
385    /// Call this only after the cell ran without error, so a failed cell
386    /// does not poison the accumulated state. No-op when cross-cell
387    /// persistence is disabled.
388    pub fn absorb_repl_cell_definitions(&mut self, program: &shape_ast::Program) {
389        if !self.repl_persistence {
390            return;
391        }
392        let cell = Self::collect_definition_items(program);
393        if !cell.is_empty() {
394            self.absorb_repl_definitions(cell);
395        }
396    }
397
398    /// User type schemas persisted across REPL cells, keyed by name
399    /// (WS-11). The `ProgramExecutor` seeds each cell's compiler with
400    /// these so a `type` keeps a stable `SchemaId` for the whole
401    /// session.
402    pub fn repl_user_schemas(
403        &self,
404    ) -> &std::collections::HashMap<String, crate::type_schema::TypeSchema> {
405        &self.repl_user_schemas
406    }
407
408    /// Record a user type schema under its first-assigned id (WS-11).
409    ///
410    /// Idempotent and first-write-wins: once a `type` has a session id,
411    /// later cells must not overwrite it — that id is what every already
412    /// persisted instance of the type carries.
413    pub fn remember_repl_user_schema(&mut self, schema: crate::type_schema::TypeSchema) {
414        if !self.repl_persistence {
415            return;
416        }
417        self.repl_user_schemas
418            .entry(schema.name.clone())
419            .or_insert(schema);
420    }
421
422    /// Names of the struct / enum types this cell's definition items
423    /// introduce — used to pull the matching schemas out of the
424    /// compiled program's registry for cross-cell id stabilization.
425    pub fn repl_user_type_names(program: &shape_ast::Program) -> Vec<String> {
426        use shape_ast::ast::Item;
427        let mut names = Vec::new();
428        for item in &program.items {
429            match item {
430                Item::StructType(s, _) => names.push(s.name.clone()),
431                Item::Enum(e, _) => names.push(e.name.clone()),
432                _ => {}
433            }
434        }
435        names
436    }
437
438    /// Collect the definition-class items from a parsed REPL cell.
439    ///
440    /// "Definition" here means an item whose effect is to introduce a
441    /// reusable name (a function, type, enum, trait, impl, type alias,
442    /// annotation, or declaration-only builtin) rather than to compute a
443    /// value. These are the items that must survive into subsequent
444    /// cells so that later lines can reference them. Statement-class
445    /// items (`let`, expressions, assignments) are intentionally
446    /// excluded — their *values* persist via the module-binding
447    /// round-trip in the program executor, not via AST re-injection.
448    fn collect_definition_items(program: &shape_ast::Program) -> Vec<shape_ast::ast::Item> {
449        use shape_ast::ast::Item;
450        program
451            .items
452            .iter()
453            .filter(|item| {
454                matches!(
455                    item,
456                    Item::Function(..)
457                        | Item::StructType(..)
458                        | Item::Enum(..)
459                        | Item::Trait(..)
460                        | Item::Impl(..)
461                        | Item::Extend(..)
462                        | Item::TypeAlias(..)
463                        | Item::AnnotationDef(..)
464                        | Item::ForeignFunction(..)
465                        | Item::BuiltinTypeDecl(..)
466                        | Item::BuiltinFunctionDecl(..)
467                )
468            })
469            .cloned()
470            .collect()
471    }
472
473    /// Fold a cell's definition items into the cross-cell accumulator.
474    ///
475    /// A later redefinition of a name (`fn foo` after an earlier
476    /// `fn foo`, `type Point` after an earlier `type Point`) replaces
477    /// the earlier definition rather than accumulating a duplicate that
478    /// would make the next cell's compilation ambiguous. Items keyed by
479    /// the same identity are matched by name; `impl` / `extend` blocks
480    /// are keyed by their target (and trait/impl selector) so a
481    /// re-`impl` of the same trait-for-type pair supersedes the prior
482    /// one.
483    fn absorb_repl_definitions(&mut self, cell: Vec<shape_ast::ast::Item>) {
484        for item in cell {
485            let key = Self::definition_identity(&item);
486            if let Some(pos) = self
487                .repl_definitions
488                .iter()
489                .position(|existing| Self::definition_identity(existing) == key)
490            {
491                self.repl_definitions[pos] = item;
492            } else {
493                self.repl_definitions.push(item);
494            }
495        }
496    }
497
498    /// A stable identity string for a definition item, used to dedup the
499    /// cross-cell accumulator. Two items with the same identity are the
500    /// "same declaration" for redefinition purposes.
501    fn definition_identity(item: &shape_ast::ast::Item) -> String {
502        use shape_ast::ast::Item;
503        match item {
504            Item::Function(f, _) => format!("fn:{}", f.name),
505            Item::StructType(s, _) => format!("type:{}", s.name),
506            Item::Enum(e, _) => format!("enum:{}", e.name),
507            Item::Trait(t, _) => format!("trait:{}", t.name),
508            Item::TypeAlias(a, _) => format!("alias:{}", a.name),
509            Item::AnnotationDef(a, _) => format!("ann:{}", a.name),
510            Item::ForeignFunction(f, _) => format!("fn:{}", f.name),
511            Item::BuiltinTypeDecl(d, _) => format!("type:{}", d.name),
512            Item::BuiltinFunctionDecl(d, _) => format!("fn:{}", d.name),
513            Item::Impl(i, _) => format!(
514                "impl:{}:{}:{}",
515                Self::type_name_key(&i.trait_name),
516                Self::type_name_key(&i.target_type),
517                i.impl_name.as_deref().unwrap_or(""),
518            ),
519            Item::Extend(e, _) => {
520                format!("extend:{}", Self::type_name_key(&e.type_name))
521            }
522            // Non-definition items are never absorbed; give each a unique
523            // identity so they would never collide if one slipped through.
524            other => format!("other:{:p}", other as *const _),
525        }
526    }
527
528    /// Base path name of a `TypeName`, used as a dedup key component for
529    /// `impl` / `extend` blocks in the cross-cell definition accumulator.
530    fn type_name_key(tn: &shape_ast::ast::TypeName) -> &str {
531        match tn {
532            shape_ast::ast::TypeName::Simple(path) => path.name(),
533            shape_ast::ast::TypeName::Generic { name, .. } => name.name(),
534        }
535    }
536}