Skip to main content

harn_vm/vm/
execution.rs

1use std::rc::Rc;
2use std::time::Instant;
3
4use crate::chunk::{Chunk, ChunkRef};
5use crate::value::{ModuleFunctionRegistry, VmError, VmValue};
6
7use super::{CallFrame, LocalSlot, Vm};
8
9impl Vm {
10    /// Execute a compiled chunk.
11    pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
12        let span_id = crate::tracing::span_start(crate::tracing::SpanKind::Pipeline, "main".into());
13        let result = self.run_chunk(chunk).await;
14        crate::tracing::span_end(span_id);
15        result
16    }
17
18    /// Convert a VmError into either a handled exception (returning Ok) or a propagated error.
19    pub(crate) fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
20        let thrown_value = match &error {
21            VmError::Thrown(v) => v.clone(),
22            other => VmValue::String(Rc::from(other.to_string())),
23        };
24
25        if let Some(handler) = self.exception_handlers.pop() {
26            if !handler.error_type.is_empty() {
27                // Typed catch: only match when the thrown enum's type equals the declared type.
28                let matches = match &thrown_value {
29                    VmValue::EnumVariant { enum_name, .. } => {
30                        enum_name.as_ref() == handler.error_type
31                    }
32                    _ => false,
33                };
34                if !matches {
35                    return self.handle_error(error);
36                }
37            }
38
39            while self.frames.len() > handler.frame_depth {
40                if let Some(frame) = self.frames.pop() {
41                    if let Some(ref dir) = frame.saved_source_dir {
42                        crate::stdlib::set_thread_source_dir(dir);
43                    }
44                    self.iterators.truncate(frame.saved_iterator_depth);
45                    self.env = frame.saved_env;
46                }
47            }
48
49            // Drop deadlines that belonged to unwound frames.
50            while self
51                .deadlines
52                .last()
53                .is_some_and(|d| d.1 > handler.frame_depth)
54            {
55                self.deadlines.pop();
56            }
57
58            self.env.truncate_scopes(handler.env_scope_depth);
59
60            self.stack.truncate(handler.stack_depth);
61            self.stack.push(thrown_value);
62
63            if let Some(frame) = self.frames.last_mut() {
64                frame.ip = handler.catch_ip;
65            }
66
67            Ok(None)
68        } else {
69            Err(error)
70        }
71    }
72
73    pub(crate) async fn run_chunk(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
74        self.run_chunk_entry(chunk, 0, None, None, None, None).await
75    }
76
77    pub(crate) async fn run_chunk_entry(
78        &mut self,
79        chunk: &Chunk,
80        argc: usize,
81        saved_source_dir: Option<std::path::PathBuf>,
82        module_functions: Option<ModuleFunctionRegistry>,
83        module_state: Option<crate::value::ModuleState>,
84        local_slots: Option<Vec<LocalSlot>>,
85    ) -> Result<VmValue, VmError> {
86        self.run_chunk_ref(
87            Rc::new(chunk.clone()),
88            argc,
89            saved_source_dir,
90            module_functions,
91            module_state,
92            local_slots,
93        )
94        .await
95    }
96
97    pub(crate) async fn run_chunk_ref(
98        &mut self,
99        chunk: ChunkRef,
100        argc: usize,
101        saved_source_dir: Option<std::path::PathBuf>,
102        module_functions: Option<ModuleFunctionRegistry>,
103        module_state: Option<crate::value::ModuleState>,
104        local_slots: Option<Vec<LocalSlot>>,
105    ) -> Result<VmValue, VmError> {
106        let initial_env = self.env.clone();
107        let local_slots = local_slots.unwrap_or_else(|| Self::fresh_local_slots(&chunk));
108        let initial_local_slots = local_slots.clone();
109        self.frames.push(CallFrame {
110            chunk,
111            ip: 0,
112            stack_base: self.stack.len(),
113            saved_env: self.env.clone(),
114            initial_env: Some(initial_env),
115            initial_local_slots: Some(initial_local_slots),
116            saved_iterator_depth: self.iterators.len(),
117            fn_name: String::new(),
118            argc,
119            saved_source_dir,
120            module_functions,
121            module_state,
122            local_slots,
123            local_scope_base: self.env.scope_depth().saturating_sub(1),
124            local_scope_depth: 0,
125        });
126
127        loop {
128            if let Some(&(deadline, _)) = self.deadlines.last() {
129                if Instant::now() > deadline {
130                    self.deadlines.pop();
131                    let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
132                    match self.handle_error(err) {
133                        Ok(None) => continue,
134                        Ok(Some(val)) => return Ok(val),
135                        Err(e) => return Err(e),
136                    }
137                }
138            }
139
140            let frame = match self.frames.last_mut() {
141                Some(f) => f,
142                None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
143            };
144
145            if frame.ip >= frame.chunk.code.len() {
146                let val = self.stack.pop().unwrap_or(VmValue::Nil);
147                let popped_frame = self.frames.pop().unwrap();
148                if let Some(ref dir) = popped_frame.saved_source_dir {
149                    crate::stdlib::set_thread_source_dir(dir);
150                }
151
152                if self.frames.is_empty() {
153                    return Ok(val);
154                } else {
155                    self.iterators.truncate(popped_frame.saved_iterator_depth);
156                    self.env = popped_frame.saved_env;
157                    self.stack.truncate(popped_frame.stack_base);
158                    self.stack.push(val);
159                    continue;
160                }
161            }
162
163            let op = frame.chunk.code[frame.ip];
164            frame.ip += 1;
165
166            match self.execute_op(op).await {
167                Ok(Some(val)) => return Ok(val),
168                Ok(None) => continue,
169                Err(VmError::Return(val)) => {
170                    if let Some(popped_frame) = self.frames.pop() {
171                        if let Some(ref dir) = popped_frame.saved_source_dir {
172                            crate::stdlib::set_thread_source_dir(dir);
173                        }
174                        let current_depth = self.frames.len();
175                        self.exception_handlers
176                            .retain(|h| h.frame_depth <= current_depth);
177
178                        if self.frames.is_empty() {
179                            return Ok(val);
180                        }
181                        self.iterators.truncate(popped_frame.saved_iterator_depth);
182                        self.env = popped_frame.saved_env;
183                        self.stack.truncate(popped_frame.stack_base);
184                        self.stack.push(val);
185                    } else {
186                        return Ok(val);
187                    }
188                }
189                Err(e) => {
190                    // Capture stack trace before error handling unwinds frames.
191                    if self.error_stack_trace.is_empty() {
192                        self.error_stack_trace = self.capture_stack_trace();
193                    }
194                    match self.handle_error(e) {
195                        Ok(None) => {
196                            self.error_stack_trace.clear();
197                            continue;
198                        }
199                        Ok(Some(val)) => return Ok(val),
200                        Err(e) => return Err(self.enrich_error_with_line(e)),
201                    }
202                }
203            }
204        }
205    }
206
207    pub(crate) async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
208        if let Some(&(deadline, _)) = self.deadlines.last() {
209            if Instant::now() > deadline {
210                self.deadlines.pop();
211                let err = VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")));
212                match self.handle_error(err) {
213                    Ok(None) => return Ok(None),
214                    Ok(Some(val)) => return Ok(Some((val, false))),
215                    Err(e) => return Err(e),
216                }
217            }
218        }
219
220        let frame = match self.frames.last_mut() {
221            Some(f) => f,
222            None => {
223                let val = self.stack.pop().unwrap_or(VmValue::Nil);
224                return Ok(Some((val, false)));
225            }
226        };
227
228        if frame.ip >= frame.chunk.code.len() {
229            let val = self.stack.pop().unwrap_or(VmValue::Nil);
230            let popped_frame = self.frames.pop().unwrap();
231            if self.frames.is_empty() {
232                return Ok(Some((val, false)));
233            } else {
234                self.iterators.truncate(popped_frame.saved_iterator_depth);
235                self.env = popped_frame.saved_env;
236                self.stack.truncate(popped_frame.stack_base);
237                self.stack.push(val);
238                return Ok(None);
239            }
240        }
241
242        let op = frame.chunk.code[frame.ip];
243        frame.ip += 1;
244
245        match self.execute_op(op).await {
246            Ok(Some(val)) => Ok(Some((val, false))),
247            Ok(None) => Ok(None),
248            Err(VmError::Return(val)) => {
249                if let Some(popped_frame) = self.frames.pop() {
250                    if let Some(ref dir) = popped_frame.saved_source_dir {
251                        crate::stdlib::set_thread_source_dir(dir);
252                    }
253                    let current_depth = self.frames.len();
254                    self.exception_handlers
255                        .retain(|h| h.frame_depth <= current_depth);
256                    if self.frames.is_empty() {
257                        return Ok(Some((val, false)));
258                    }
259                    self.iterators.truncate(popped_frame.saved_iterator_depth);
260                    self.env = popped_frame.saved_env;
261                    self.stack.truncate(popped_frame.stack_base);
262                    self.stack.push(val);
263                    Ok(None)
264                } else {
265                    Ok(Some((val, false)))
266                }
267            }
268            Err(e) => {
269                if self.error_stack_trace.is_empty() {
270                    self.error_stack_trace = self.capture_stack_trace();
271                }
272                match self.handle_error(e) {
273                    Ok(None) => {
274                        self.error_stack_trace.clear();
275                        Ok(None)
276                    }
277                    Ok(Some(val)) => Ok(Some((val, false))),
278                    Err(e) => Err(self.enrich_error_with_line(e)),
279                }
280            }
281        }
282    }
283
284    /// Capture the current call stack as (fn_name, line, col, source_file) tuples.
285    pub(crate) fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
286        self.frames
287            .iter()
288            .map(|f| {
289                let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
290                let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
291                let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
292                (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
293            })
294            .collect()
295    }
296
297    /// Enrich a VmError with source line information from the captured stack
298    /// trace. Appends ` (line N)` to error variants whose messages don't
299    /// already carry location context.
300    pub(crate) fn enrich_error_with_line(&self, error: VmError) -> VmError {
301        // Determine the line from the captured stack trace (innermost frame).
302        let line = self
303            .error_stack_trace
304            .last()
305            .map(|(_, l, _, _)| *l)
306            .unwrap_or_else(|| self.current_line());
307        if line == 0 {
308            return error;
309        }
310        let suffix = format!(" (line {line})");
311        match error {
312            VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
313            VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
314            VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
315            VmError::UndefinedVariable(name) => {
316                VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
317            }
318            VmError::UndefinedBuiltin(name) => {
319                VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
320            }
321            VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
322                "Cannot assign to immutable binding: {name}{suffix}"
323            )),
324            VmError::StackOverflow => {
325                VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
326            }
327            // Leave these untouched:
328            // - Thrown: user-thrown errors should not be silently modified
329            // - CategorizedError: structured errors for agent orchestration
330            // - Return: control flow, not a real error
331            // - StackUnderflow / InvalidInstruction: internal VM bugs
332            other => other,
333        }
334    }
335}