Skip to main content

harn_vm/vm/
execution.rs

1use std::rc::Rc;
2use std::time::{Duration, Instant};
3
4use crate::chunk::{Chunk, ChunkRef};
5use crate::value::{ModuleFunctionRegistry, VmError, VmValue};
6
7use super::{CallFrame, LocalSlot, Vm};
8
9const CANCEL_GRACE_INSTRUCTIONS: usize = 1024;
10const CANCEL_GRACE_ASYNC_OP: Duration = Duration::from_millis(250);
11
12impl Vm {
13    /// Execute a compiled chunk.
14    pub async fn execute(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
15        let span_id = crate::tracing::span_start(crate::tracing::SpanKind::Pipeline, "main".into());
16        let result = self.run_chunk(chunk).await;
17        crate::tracing::span_end(span_id);
18        result
19    }
20
21    /// Convert a VmError into either a handled exception (returning Ok) or a propagated error.
22    pub(crate) fn handle_error(&mut self, error: VmError) -> Result<Option<VmValue>, VmError> {
23        let thrown_value = match &error {
24            VmError::Thrown(v) => v.clone(),
25            other => VmValue::String(Rc::from(other.to_string())),
26        };
27
28        if let Some(handler) = self.exception_handlers.pop() {
29            if !handler.error_type.is_empty() {
30                // Typed catch: only match when the thrown enum's type equals the declared type.
31                let matches = match &thrown_value {
32                    VmValue::EnumVariant { enum_name, .. } => {
33                        enum_name.as_ref() == handler.error_type
34                    }
35                    _ => false,
36                };
37                if !matches {
38                    return self.handle_error(error);
39                }
40            }
41
42            self.release_sync_guards_after_unwind(handler.frame_depth, handler.env_scope_depth);
43
44            while self.frames.len() > handler.frame_depth {
45                if let Some(frame) = self.frames.pop() {
46                    if let Some(ref dir) = frame.saved_source_dir {
47                        crate::stdlib::set_thread_source_dir(dir);
48                    }
49                    self.iterators.truncate(frame.saved_iterator_depth);
50                    self.env = frame.saved_env;
51                }
52            }
53
54            // Drop deadlines that belonged to unwound frames.
55            while self
56                .deadlines
57                .last()
58                .is_some_and(|d| d.1 > handler.frame_depth)
59            {
60                self.deadlines.pop();
61            }
62
63            self.env.truncate_scopes(handler.env_scope_depth);
64
65            self.stack.truncate(handler.stack_depth);
66            self.stack.push(thrown_value);
67
68            if let Some(frame) = self.frames.last_mut() {
69                frame.ip = handler.catch_ip;
70            }
71
72            Ok(None)
73        } else {
74            Err(error)
75        }
76    }
77
78    pub(crate) async fn run_chunk(&mut self, chunk: &Chunk) -> Result<VmValue, VmError> {
79        self.run_chunk_entry(chunk, 0, None, None, None, None).await
80    }
81
82    pub(crate) async fn run_chunk_entry(
83        &mut self,
84        chunk: &Chunk,
85        argc: usize,
86        saved_source_dir: Option<std::path::PathBuf>,
87        module_functions: Option<ModuleFunctionRegistry>,
88        module_state: Option<crate::value::ModuleState>,
89        local_slots: Option<Vec<LocalSlot>>,
90    ) -> Result<VmValue, VmError> {
91        self.run_chunk_ref(
92            Rc::new(chunk.clone()),
93            argc,
94            saved_source_dir,
95            module_functions,
96            module_state,
97            local_slots,
98        )
99        .await
100    }
101
102    pub(crate) async fn run_chunk_ref(
103        &mut self,
104        chunk: ChunkRef,
105        argc: usize,
106        saved_source_dir: Option<std::path::PathBuf>,
107        module_functions: Option<ModuleFunctionRegistry>,
108        module_state: Option<crate::value::ModuleState>,
109        local_slots: Option<Vec<LocalSlot>>,
110    ) -> Result<VmValue, VmError> {
111        let initial_env = self.env.clone();
112        let local_slots = local_slots.unwrap_or_else(|| Self::fresh_local_slots(&chunk));
113        let initial_local_slots = local_slots.clone();
114        self.frames.push(CallFrame {
115            chunk,
116            ip: 0,
117            stack_base: self.stack.len(),
118            saved_env: self.env.clone(),
119            initial_env: Some(initial_env),
120            initial_local_slots: Some(initial_local_slots),
121            saved_iterator_depth: self.iterators.len(),
122            fn_name: String::new(),
123            argc,
124            saved_source_dir,
125            module_functions,
126            module_state,
127            local_slots,
128            local_scope_base: self.env.scope_depth().saturating_sub(1),
129            local_scope_depth: 0,
130        });
131
132        loop {
133            if let Some(err) = self.pending_scope_interrupt() {
134                match self.handle_error(err) {
135                    Ok(None) => continue,
136                    Ok(Some(val)) => return Ok(val),
137                    Err(e) => return Err(e),
138                }
139            }
140
141            let frame = match self.frames.last_mut() {
142                Some(f) => f,
143                None => return Ok(self.stack.pop().unwrap_or(VmValue::Nil)),
144            };
145
146            if frame.ip >= frame.chunk.code.len() {
147                let val = self.stack.pop().unwrap_or(VmValue::Nil);
148                self.release_sync_guards_for_frame(self.frames.len());
149                let popped_frame = self.frames.pop().unwrap();
150                if let Some(ref dir) = popped_frame.saved_source_dir {
151                    crate::stdlib::set_thread_source_dir(dir);
152                }
153
154                if self.frames.is_empty() {
155                    return Ok(val);
156                } else {
157                    self.iterators.truncate(popped_frame.saved_iterator_depth);
158                    self.env = popped_frame.saved_env;
159                    self.stack.truncate(popped_frame.stack_base);
160                    self.stack.push(val);
161                    continue;
162                }
163            }
164
165            let op = frame.chunk.code[frame.ip];
166            frame.ip += 1;
167
168            match self.execute_op_with_scope_interrupts(op).await {
169                Ok(Some(val)) => return Ok(val),
170                Ok(None) => continue,
171                Err(VmError::Return(val)) => {
172                    if let Some(popped_frame) = self.frames.pop() {
173                        self.release_sync_guards_for_frame(self.frames.len() + 1);
174                        if let Some(ref dir) = popped_frame.saved_source_dir {
175                            crate::stdlib::set_thread_source_dir(dir);
176                        }
177                        let current_depth = self.frames.len();
178                        self.exception_handlers
179                            .retain(|h| h.frame_depth <= current_depth);
180
181                        if self.frames.is_empty() {
182                            return Ok(val);
183                        }
184                        self.iterators.truncate(popped_frame.saved_iterator_depth);
185                        self.env = popped_frame.saved_env;
186                        self.stack.truncate(popped_frame.stack_base);
187                        self.stack.push(val);
188                    } else {
189                        return Ok(val);
190                    }
191                }
192                Err(e) => {
193                    // Capture stack trace before error handling unwinds frames.
194                    if self.error_stack_trace.is_empty() {
195                        self.error_stack_trace = self.capture_stack_trace();
196                    }
197                    match self.handle_error(e) {
198                        Ok(None) => {
199                            self.error_stack_trace.clear();
200                            continue;
201                        }
202                        Ok(Some(val)) => return Ok(val),
203                        Err(e) => return Err(self.enrich_error_with_line(e)),
204                    }
205                }
206            }
207        }
208    }
209
210    pub(crate) async fn execute_one_cycle(&mut self) -> Result<Option<(VmValue, bool)>, VmError> {
211        if let Some(err) = self.pending_scope_interrupt() {
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        let frame = match self.frames.last_mut() {
220            Some(f) => f,
221            None => {
222                let val = self.stack.pop().unwrap_or(VmValue::Nil);
223                return Ok(Some((val, false)));
224            }
225        };
226
227        if frame.ip >= frame.chunk.code.len() {
228            let val = self.stack.pop().unwrap_or(VmValue::Nil);
229            self.release_sync_guards_for_frame(self.frames.len());
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_with_scope_interrupts(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                    self.release_sync_guards_for_frame(self.frames.len() + 1);
251                    if let Some(ref dir) = popped_frame.saved_source_dir {
252                        crate::stdlib::set_thread_source_dir(dir);
253                    }
254                    let current_depth = self.frames.len();
255                    self.exception_handlers
256                        .retain(|h| h.frame_depth <= current_depth);
257                    if self.frames.is_empty() {
258                        return Ok(Some((val, false)));
259                    }
260                    self.iterators.truncate(popped_frame.saved_iterator_depth);
261                    self.env = popped_frame.saved_env;
262                    self.stack.truncate(popped_frame.stack_base);
263                    self.stack.push(val);
264                    Ok(None)
265                } else {
266                    Ok(Some((val, false)))
267                }
268            }
269            Err(e) => {
270                if self.error_stack_trace.is_empty() {
271                    self.error_stack_trace = self.capture_stack_trace();
272                }
273                match self.handle_error(e) {
274                    Ok(None) => {
275                        self.error_stack_trace.clear();
276                        Ok(None)
277                    }
278                    Ok(Some(val)) => Ok(Some((val, false))),
279                    Err(e) => Err(self.enrich_error_with_line(e)),
280                }
281            }
282        }
283    }
284
285    fn pending_scope_interrupt(&mut self) -> Option<VmError> {
286        if self.is_cancel_requested() {
287            match self.cancel_grace_instructions_remaining.as_mut() {
288                Some(0) => {
289                    self.cancel_spawned_tasks();
290                    return Some(Self::cancelled_error());
291                }
292                Some(remaining) => *remaining -= 1,
293                None => self.cancel_grace_instructions_remaining = Some(CANCEL_GRACE_INSTRUCTIONS),
294            }
295        } else {
296            self.cancel_grace_instructions_remaining = None;
297        }
298        if let Some(&(deadline, _)) = self.deadlines.last() {
299            if Instant::now() >= deadline {
300                self.deadlines.pop();
301                return Some(Self::deadline_exceeded_error());
302            }
303        }
304        None
305    }
306
307    async fn execute_op_with_scope_interrupts(
308        &mut self,
309        op: u8,
310    ) -> Result<Option<VmValue>, VmError> {
311        enum ScopeInterruptResult {
312            Op(Result<Option<VmValue>, VmError>),
313            Deadline,
314            CancelTimedOut,
315        }
316
317        let deadline = self.deadlines.last().map(|(deadline, _)| *deadline);
318        let cancel_token = self.cancel_token.clone();
319
320        if deadline.is_none() && cancel_token.is_none() {
321            return self.execute_op(op).await;
322        }
323
324        let has_deadline = deadline.is_some();
325        let cancel_requested_at_start = cancel_token
326            .as_ref()
327            .is_some_and(|token| token.load(std::sync::atomic::Ordering::SeqCst));
328        let has_cancel = cancel_token.is_some() && !cancel_requested_at_start;
329        let deadline_sleep = async move {
330            if let Some(deadline) = deadline {
331                tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)).await;
332            } else {
333                std::future::pending::<()>().await;
334            }
335        };
336        let cancel_sleep = async move {
337            if let Some(token) = cancel_token {
338                while !token.load(std::sync::atomic::Ordering::SeqCst) {
339                    tokio::time::sleep(Duration::from_millis(10)).await;
340                }
341            } else {
342                std::future::pending::<()>().await;
343            }
344        };
345
346        let result = {
347            let op_future = self.execute_op(op);
348            tokio::pin!(op_future);
349            tokio::select! {
350                result = &mut op_future => ScopeInterruptResult::Op(result),
351                _ = deadline_sleep, if has_deadline => ScopeInterruptResult::Deadline,
352                _ = cancel_sleep, if has_cancel => {
353                    let grace = tokio::time::sleep(CANCEL_GRACE_ASYNC_OP);
354                    tokio::pin!(grace);
355                    tokio::select! {
356                        result = &mut op_future => ScopeInterruptResult::Op(result),
357                        _ = &mut grace => ScopeInterruptResult::CancelTimedOut,
358                    }
359                }
360            }
361        };
362
363        match result {
364            ScopeInterruptResult::Op(result) => result,
365            ScopeInterruptResult::Deadline => {
366                self.deadlines.pop();
367                self.cancel_spawned_tasks();
368                Err(Self::deadline_exceeded_error())
369            }
370            ScopeInterruptResult::CancelTimedOut => {
371                self.cancel_spawned_tasks();
372                Err(Self::cancelled_error())
373            }
374        }
375    }
376
377    pub(crate) fn deadline_exceeded_error() -> VmError {
378        VmError::Thrown(VmValue::String(Rc::from("Deadline exceeded")))
379    }
380
381    pub(crate) fn cancelled_error() -> VmError {
382        VmError::Thrown(VmValue::String(Rc::from(
383            "kind:cancelled:VM cancelled by host",
384        )))
385    }
386
387    /// Capture the current call stack as (fn_name, line, col, source_file) tuples.
388    pub(crate) fn capture_stack_trace(&self) -> Vec<(String, usize, usize, Option<String>)> {
389        self.frames
390            .iter()
391            .map(|f| {
392                let idx = if f.ip > 0 { f.ip - 1 } else { 0 };
393                let line = f.chunk.lines.get(idx).copied().unwrap_or(0) as usize;
394                let col = f.chunk.columns.get(idx).copied().unwrap_or(0) as usize;
395                (f.fn_name.clone(), line, col, f.chunk.source_file.clone())
396            })
397            .collect()
398    }
399
400    /// Enrich a VmError with source line information from the captured stack
401    /// trace. Appends ` (line N)` to error variants whose messages don't
402    /// already carry location context.
403    pub(crate) fn enrich_error_with_line(&self, error: VmError) -> VmError {
404        // Determine the line from the captured stack trace (innermost frame).
405        let line = self
406            .error_stack_trace
407            .last()
408            .map(|(_, l, _, _)| *l)
409            .unwrap_or_else(|| self.current_line());
410        if line == 0 {
411            return error;
412        }
413        let suffix = format!(" (line {line})");
414        match error {
415            VmError::Runtime(msg) => VmError::Runtime(format!("{msg}{suffix}")),
416            VmError::TypeError(msg) => VmError::TypeError(format!("{msg}{suffix}")),
417            VmError::DivisionByZero => VmError::Runtime(format!("Division by zero{suffix}")),
418            VmError::UndefinedVariable(name) => {
419                VmError::Runtime(format!("Undefined variable: {name}{suffix}"))
420            }
421            VmError::UndefinedBuiltin(name) => {
422                VmError::Runtime(format!("Undefined builtin: {name}{suffix}"))
423            }
424            VmError::ImmutableAssignment(name) => VmError::Runtime(format!(
425                "Cannot assign to immutable binding: {name}{suffix}"
426            )),
427            VmError::StackOverflow => {
428                VmError::Runtime(format!("Stack overflow: too many nested calls{suffix}"))
429            }
430            // Leave these untouched:
431            // - Thrown: user-thrown errors should not be silently modified
432            // - CategorizedError: structured errors for agent orchestration
433            // - Return: control flow, not a real error
434            // - StackUnderflow / InvalidInstruction: internal VM bugs
435            other => other,
436        }
437    }
438}