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        self.runtime.load_program(&program, &self.default_data)?;
180
181        // Store source text for error messages during execution
182        self.set_source(source);
183
184        // Execute
185        let runtime_start = std::time::Instant::now();
186        let result = executor.execute_program(self, &program)?;
187        let runtime_time_ms = runtime_start.elapsed().as_millis() as u64;
188
189        let total_time_ms = start_time.elapsed().as_millis() as u64;
190        let memory_used_bytes = self.estimate_memory_usage();
191        let rows_processed = Some(self.default_data.row_count());
192        let messages = self.collect_messages();
193
194        Ok(ExecutionResult {
195            value: result.wire_value,
196            type_info: result.type_info,
197            execution_type: ExecutionType::Repl,
198            metrics: ExecutionMetrics {
199                execution_time_ms: total_time_ms,
200                parse_time_ms,
201                analysis_time_ms,
202                runtime_time_ms,
203                memory_used_bytes,
204                rows_processed,
205            },
206            messages,
207            content_json: result.content_json,
208            content_html: result.content_html,
209            content_terminal: result.content_terminal,
210        })
211    }
212
213    /// Parse and analyze source code without executing it.
214    ///
215    /// Returns the analyzed AST `Program`, ready for compilation.
216    /// Used by the recompile-and-resume flow.
217    pub fn parse_and_analyze(&mut self, source: &str) -> Result<shape_ast::Program> {
218        // Parse/desugar may touch ambient schema state (e.g. comptime
219        // builtins). Install this runtime's registry for the call.
220        let _scope = self.runtime.enter_schema_scope();
221
222        if let Some(ctx) = self.runtime.persistent_context_mut() {
223            ctx.reset_for_new_execution();
224        }
225        let mut program = parser::parse_program(source)?;
226        shape_ast::transform::desugar_program(&mut program);
227        self.set_source(source);
228        Ok(program)
229    }
230
231    /// Execute a Shape program with options
232    pub(super) fn execute_with_options(
233        &mut self,
234        executor: &mut impl super::ProgramExecutor,
235        source: &str,
236        _is_stdlib: bool,
237    ) -> Result<ExecutionResult> {
238        // Install this runtime's TypeSchemaRegistry as the thread-local
239        // ambient for the duration of synchronous execution.
240        let _scope = self.runtime.enter_schema_scope();
241
242        let start_time = std::time::Instant::now();
243
244        // Always reset variable scopes before each execution
245        if let Some(ctx) = self.runtime.persistent_context_mut() {
246            ctx.reset_for_new_execution();
247        }
248
249        // Check for deprecated APIs before parsing (the deprecated syntax may not parse cleanly)
250        Self::check_deprecated_apis(source)?;
251
252        // Parse the source
253        let parse_start = std::time::Instant::now();
254        let mut program = parser::parse_program(source)?;
255        let parse_time_ms = parse_start.elapsed().as_millis() as u64;
256
257        // Desugar high-level syntax (e.g., from-queries to method chains) before analysis
258        shape_ast::transform::desugar_program(&mut program);
259
260        let analysis_start = std::time::Instant::now();
261        let analysis_time_ms = analysis_start.elapsed().as_millis() as u64;
262
263        // Store source text for error messages during execution
264        self.set_source(source);
265
266        // Execute the program
267        let runtime_start = std::time::Instant::now();
268        let result = executor.execute_program(self, &program)?;
269        let runtime_time_ms = runtime_start.elapsed().as_millis() as u64;
270
271        let total_time_ms = start_time.elapsed().as_millis() as u64;
272
273        // Get memory usage estimate (heap allocation approximation)
274        let memory_used_bytes = self.estimate_memory_usage();
275
276        // Get rows processed count from market data
277        let rows_processed = Some(self.default_data.row_count());
278
279        // Collect any messages from the runtime
280        let messages = self.collect_messages();
281
282        Ok(ExecutionResult {
283            value: result.wire_value,
284            type_info: result.type_info,
285            execution_type: result.execution_type,
286            metrics: ExecutionMetrics {
287                execution_time_ms: total_time_ms,
288                parse_time_ms,
289                analysis_time_ms,
290                runtime_time_ms,
291                memory_used_bytes,
292                rows_processed,
293            },
294            messages,
295            content_json: result.content_json,
296            content_html: result.content_html,
297            content_terminal: result.content_terminal,
298        })
299    }
300
301    /// Execute a REPL command
302    pub fn execute_repl_command(
303        &mut self,
304        executor: &mut impl super::ProgramExecutor,
305        command: &str,
306    ) -> Result<ExecutionResult> {
307        let mut result = self.execute(executor, command)?;
308        result.execution_type = ExecutionType::Repl;
309        Ok(result)
310    }
311
312    /// Check for deprecated APIs before parsing. Some deprecated call syntax may
313    /// not parse cleanly (e.g. escaped quotes in raw strings), so we detect them
314    /// via pattern matching on the source text and produce helpful diagnostics.
315    fn check_deprecated_apis(source: &str) -> Result<()> {
316        let trimmed = source.trim();
317        if trimmed.starts_with("csv.load") || trimmed.contains("csv.load(") {
318            return Err(shape_ast::error::ShapeError::SemanticError {
319                message: "csv.load has been removed. Use the csv package instead: import { read } from \"csv\""
320                    .to_string(),
321                location: None,
322            });
323        }
324        // Check for bare load(provider, params) — the global load function was removed
325        if trimmed.starts_with("load(") || trimmed.starts_with("load (") {
326            return Err(shape_ast::error::ShapeError::SemanticError {
327                message: "load(provider, params) has been removed. Use typed data access instead: data(\"source\", { ... })"
328                    .to_string(),
329                location: None,
330            });
331        }
332        Ok(())
333    }
334
335    /// Estimate memory usage based on runtime state
336    pub(super) fn estimate_memory_usage(&self) -> Option<usize> {
337        // Estimate based on known allocations
338        let mut total = 0usize;
339
340        // Market data rows (each row ~48 bytes for 6 f64 values)
341        total += self.default_data.row_count() * 48;
342
343        // Variable storage estimate (rough approximation)
344        // This is a simplified estimate - real tracking would require custom allocator
345        total += 1024; // Base overhead for runtime structures
346
347        Some(total)
348    }
349
350    /// Collect messages from runtime execution
351    pub(super) fn collect_messages(&self) -> Vec<super::types::Message> {
352        // Currently the runtime doesn't track messages, but this provides the interface
353        // for future implementation
354        vec![]
355    }
356}