Skip to main content

shape_runtime/engine/
execution.rs

1//! Main execution methods for Shape engine
2
3use super::types::{ExecutionMetrics, ExecutionResult, ExecutionType};
4use shape_ast::error::Result;
5use shape_ast::parser;
6
7impl super::ShapeEngine {
8    /// Execute a Shape program from source code (sync mode)
9    ///
10    /// This method allows blocking data loads (REPL mode).
11    /// For scripts/backtests, use execute_async() instead.
12    pub fn execute(
13        &mut self,
14        executor: &impl super::ProgramExecutor,
15        source: &str,
16    ) -> Result<ExecutionResult> {
17        // Set REPL/sync mode - allow blocking loads
18        if let Some(ctx) = self.runtime.persistent_context_mut() {
19            ctx.set_data_load_mode(crate::context::DataLoadMode::Sync);
20        }
21
22        self.execute_with_options(executor, source, false)
23    }
24
25    /// Execute a Shape program with async prefetching (Phase 6/8)
26    ///
27    /// This method:
28    /// 1. Sets Async data mode (runtime data requests use cache, not blocking)
29    /// 2. Parses and analyzes the program
30    /// 3. Determines required data (symbols/timeframes)
31    /// 4. Prefetches data concurrently
32    /// 5. Executes synchronously using cached data
33    ///
34    /// # Example
35    ///
36    /// ```ignore
37    /// let provider = DataFrameAdapter::new(...);
38    /// let mut engine = ShapeEngine::with_async_provider(provider)?;
39    /// let result = engine.execute_async(&interpreter, "let sma = close.sma(20)").await?;
40    /// ```
41    pub async fn execute_async(
42        &mut self,
43        executor: &impl super::ProgramExecutor,
44        source: &str,
45    ) -> Result<ExecutionResult> {
46        // Set async mode - runtime data requests must use cache
47        if let Some(ctx) = self.runtime.persistent_context_mut() {
48            ctx.set_data_load_mode(crate::context::DataLoadMode::Async);
49        }
50
51        let start_time = std::time::Instant::now();
52
53        // Parse the source
54        let parse_start = std::time::Instant::now();
55        let mut program = parser::parse_program(source)?;
56        let parse_time_ms = parse_start.elapsed().as_millis() as u64;
57
58        // Desugar high-level syntax (e.g., from-queries to method chains) before analysis
59        shape_ast::transform::desugar_program(&mut program);
60
61        // Analyze the program to understand data requirements
62        let analysis_start = std::time::Instant::now();
63        self.analyzer.set_source(source);
64        self.analyzer.analyze(&program)?;
65        let analysis_time_ms = analysis_start.elapsed().as_millis() as u64;
66
67        // Prefetch data if using async provider
68        let has_cache = self
69            .runtime
70            .persistent_context()
71            .map(|ctx| ctx.has_data_cache())
72            .unwrap_or(false);
73
74        if has_cache {
75            // Extract symbols/timeframes from program
76            let queries = self.extract_data_queries(&program)?;
77
78            // Prefetch all required data concurrently
79            if let Some(ctx) = self.runtime.persistent_context_mut() {
80                ctx.prefetch_data(queries).await?;
81            }
82        }
83
84        // Store source text for error messages during execution
85        self.set_source(source);
86
87        // Execute synchronously using cached data
88        let runtime_start = std::time::Instant::now();
89        let result = executor.execute_program(self, &program)?;
90        let runtime_time_ms = runtime_start.elapsed().as_millis() as u64;
91
92        let total_time_ms = start_time.elapsed().as_millis() as u64;
93        let memory_used_bytes = self.estimate_memory_usage();
94        let rows_processed = Some(self.default_data.row_count());
95        let messages = self.collect_messages();
96
97        Ok(ExecutionResult {
98            value: result.wire_value,
99            type_info: result.type_info,
100            execution_type: result.execution_type,
101            metrics: ExecutionMetrics {
102                execution_time_ms: total_time_ms,
103                parse_time_ms,
104                analysis_time_ms,
105                runtime_time_ms,
106                memory_used_bytes,
107                rows_processed,
108            },
109            messages,
110            content_json: result.content_json,
111            content_html: result.content_html,
112            content_terminal: result.content_terminal,
113        })
114    }
115
116    /// Execute a REPL command with persistent state
117    ///
118    /// Unlike `execute_async`, this uses incremental analysis where variables
119    /// and functions persist across commands. Call `init_repl()` once before
120    /// the first call to this method.
121    pub async fn execute_repl(
122        &mut self,
123        executor: &impl super::ProgramExecutor,
124        source: &str,
125    ) -> Result<ExecutionResult> {
126        // Set async mode - runtime data requests must use cache
127        if let Some(ctx) = self.runtime.persistent_context_mut() {
128            ctx.set_data_load_mode(crate::context::DataLoadMode::Async);
129        }
130
131        let start_time = std::time::Instant::now();
132
133        // Parse the source
134        let parse_start = std::time::Instant::now();
135        let mut program = parser::parse_program(source)?;
136        let parse_time_ms = parse_start.elapsed().as_millis() as u64;
137
138        // Desugar high-level syntax (e.g., from-queries to method chains) before analysis
139        shape_ast::transform::desugar_program(&mut program);
140
141        // Analyze incrementally - state persists across REPL commands
142        let analysis_start = std::time::Instant::now();
143        self.analyzer.set_source(source);
144        self.analyzer.analyze_incremental(&program)?;
145        let analysis_time_ms = analysis_start.elapsed().as_millis() as u64;
146
147        // Prefetch data if using async provider
148        let has_cache = self
149            .runtime
150            .persistent_context()
151            .map(|ctx| ctx.has_data_cache())
152            .unwrap_or(false);
153
154        if has_cache {
155            let queries = self.extract_data_queries(&program)?;
156            if let Some(ctx) = self.runtime.persistent_context_mut() {
157                ctx.prefetch_data(queries).await?;
158            }
159        }
160
161        // Process imports and declarations before execution
162        self.runtime.load_program(&program, &self.default_data)?;
163
164        // Store source text for error messages during execution
165        self.set_source(source);
166
167        // Execute
168        let runtime_start = std::time::Instant::now();
169        let result = executor.execute_program(self, &program)?;
170        let runtime_time_ms = runtime_start.elapsed().as_millis() as u64;
171
172        let total_time_ms = start_time.elapsed().as_millis() as u64;
173        let memory_used_bytes = self.estimate_memory_usage();
174        let rows_processed = Some(self.default_data.row_count());
175        let messages = self.collect_messages();
176
177        Ok(ExecutionResult {
178            value: result.wire_value,
179            type_info: result.type_info,
180            execution_type: ExecutionType::Repl,
181            metrics: ExecutionMetrics {
182                execution_time_ms: total_time_ms,
183                parse_time_ms,
184                analysis_time_ms,
185                runtime_time_ms,
186                memory_used_bytes,
187                rows_processed,
188            },
189            messages,
190            content_json: result.content_json,
191            content_html: result.content_html,
192            content_terminal: result.content_terminal,
193        })
194    }
195
196    /// Parse and analyze source code without executing it.
197    ///
198    /// Returns the analyzed AST `Program`, ready for compilation.
199    /// Used by the recompile-and-resume flow.
200    pub fn parse_and_analyze(&mut self, source: &str) -> Result<shape_ast::Program> {
201        if let Some(ctx) = self.runtime.persistent_context_mut() {
202            ctx.reset_for_new_execution();
203        }
204        let mut program = parser::parse_program(source)?;
205        shape_ast::transform::desugar_program(&mut program);
206        self.analyzer.set_source(source);
207        self.analyzer.analyze(&program)?;
208        self.set_source(source);
209        Ok(program)
210    }
211
212    /// Execute a Shape program with options
213    pub(super) fn execute_with_options(
214        &mut self,
215        executor: &impl super::ProgramExecutor,
216        source: &str,
217        _is_stdlib: bool,
218    ) -> Result<ExecutionResult> {
219        let start_time = std::time::Instant::now();
220
221        // Always reset variable scopes before each execution
222        if let Some(ctx) = self.runtime.persistent_context_mut() {
223            ctx.reset_for_new_execution();
224        }
225
226        // Check for deprecated APIs before parsing (the deprecated syntax may not parse cleanly)
227        Self::check_deprecated_apis(source)?;
228
229        // Parse the source
230        let parse_start = std::time::Instant::now();
231        let mut program = parser::parse_program(source)?;
232        let parse_time_ms = parse_start.elapsed().as_millis() as u64;
233
234        // Desugar high-level syntax (e.g., from-queries to method chains) before analysis
235        shape_ast::transform::desugar_program(&mut program);
236
237        // Analyze the program
238        let analysis_start = std::time::Instant::now();
239        // Note: Semantic analyzer handles function parameters in map/filter via closure type inference
240        self.analyzer.set_source(source);
241        self.analyzer.analyze(&program)?;
242        let analysis_time_ms = analysis_start.elapsed().as_millis() as u64;
243
244        // Store source text for error messages during execution
245        self.set_source(source);
246
247        // Execute the program
248        let runtime_start = std::time::Instant::now();
249        let result = executor.execute_program(self, &program)?;
250        let runtime_time_ms = runtime_start.elapsed().as_millis() as u64;
251
252        let total_time_ms = start_time.elapsed().as_millis() as u64;
253
254        // Get memory usage estimate (heap allocation approximation)
255        let memory_used_bytes = self.estimate_memory_usage();
256
257        // Get rows processed count from market data
258        let rows_processed = Some(self.default_data.row_count());
259
260        // Collect any messages from the runtime
261        let messages = self.collect_messages();
262
263        Ok(ExecutionResult {
264            value: result.wire_value,
265            type_info: result.type_info,
266            execution_type: result.execution_type,
267            metrics: ExecutionMetrics {
268                execution_time_ms: total_time_ms,
269                parse_time_ms,
270                analysis_time_ms,
271                runtime_time_ms,
272                memory_used_bytes,
273                rows_processed,
274            },
275            messages,
276            content_json: result.content_json,
277            content_html: result.content_html,
278            content_terminal: result.content_terminal,
279        })
280    }
281
282    /// Execute a REPL command
283    pub fn execute_repl_command(
284        &mut self,
285        executor: &impl super::ProgramExecutor,
286        command: &str,
287    ) -> Result<ExecutionResult> {
288        let mut result = self.execute(executor, command)?;
289        result.execution_type = ExecutionType::Repl;
290        Ok(result)
291    }
292
293    /// Check for deprecated APIs before parsing. Some deprecated call syntax may
294    /// not parse cleanly (e.g. escaped quotes in raw strings), so we detect them
295    /// via pattern matching on the source text and produce helpful diagnostics.
296    fn check_deprecated_apis(source: &str) -> Result<()> {
297        let trimmed = source.trim();
298        if trimmed.starts_with("csv.load") || trimmed.contains("csv.load(") {
299            return Err(shape_ast::error::ShapeError::SemanticError {
300                message: "csv.load has been removed. Use the csv package instead: import { read } from \"csv\""
301                    .to_string(),
302                location: None,
303            });
304        }
305        // Check for bare load(provider, params) — the global load function was removed
306        if trimmed.starts_with("load(") || trimmed.starts_with("load (") {
307            return Err(shape_ast::error::ShapeError::SemanticError {
308                message: "load(provider, params) has been removed. Use typed data access instead: data(\"source\", { ... })"
309                    .to_string(),
310                location: None,
311            });
312        }
313        Ok(())
314    }
315
316    /// Estimate memory usage based on runtime state
317    pub(super) fn estimate_memory_usage(&self) -> Option<usize> {
318        // Estimate based on known allocations
319        let mut total = 0usize;
320
321        // Market data rows (each row ~48 bytes for 6 f64 values)
322        total += self.default_data.row_count() * 48;
323
324        // Variable storage estimate (rough approximation)
325        // This is a simplified estimate - real tracking would require custom allocator
326        total += 1024; // Base overhead for runtime structures
327
328        Some(total)
329    }
330
331    /// Collect messages from runtime execution
332    pub(super) fn collect_messages(&self) -> Vec<super::types::Message> {
333        // Currently the runtime doesn't track messages, but this provides the interface
334        // for future implementation
335        vec![]
336    }
337}