Skip to main content

aver/vm/execute/
mod.rs

1mod boundary;
2mod dispatch;
3mod host;
4mod ops;
5
6#[cfg(test)]
7mod tests;
8
9use super::runtime::VmRuntime;
10use super::types::{CallFrame, CodeStore, VmError};
11use super::{VmProfileReport, profile::VmProfileState};
12use crate::nan_value::{Arena, NanValue};
13
14/// The Aver bytecode virtual machine.
15pub struct VM {
16    stack: Vec<NanValue>,
17    frames: Vec<CallFrame>,
18    globals: Vec<NanValue>,
19    code: CodeStore,
20    pub arena: Arena,
21    runtime: VmRuntime,
22    profile: Option<VmProfileState>,
23    /// Last executing (fn_id, ip) — updated at top of dispatch loop for error reporting.
24    error_fn_id: u32,
25    error_ip: u32,
26    /// Cooperative cancellation flag — set by sibling threads on error.
27    cancelled: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
28    /// Optional cap on dispatched opcodes per `run_named_function` /
29    /// `run` call. `None` (default) = unlimited, the production `aver run`
30    /// path. `Some(n)` is set by the verify runner so a single case can't
31    /// pin the host on a tail-recursive fn that never converges — AFL
32    /// nightly's hang corpus is full of those shapes (e.g. `fn id(x) =
33    /// id(-7)` where TCO turns infinite recursion into a goto-loop with
34    /// no stack growth). Counter resets at the top of each
35    /// `run_named_function` so consecutive cases don't share budget.
36    step_limit: Option<u64>,
37    /// Mutable scratch buffers for the deforestation lowering's `__buf_*`
38    /// intrinsics (0.15 Traversal). Slots are `Option<String>` so finalize
39    /// can take ownership and leave a tombstone; `BUFFER_NEW` reuses freed
40    /// slots before extending. The pool lives on the host heap, opaque to
41    /// the arena GC — buffer handles travel as `Int(idx)` NanValues.
42    buffer_pool: Vec<Option<String>>,
43}
44
45enum ReturnControl {
46    Done(NanValue),
47    Resume {
48        result: NanValue,
49        fn_id: u32,
50        ip: usize,
51        bp: usize,
52    },
53}
54
55impl VM {
56    pub fn new(code: CodeStore, globals: Vec<NanValue>, arena: Arena) -> Self {
57        VM {
58            stack: Vec::with_capacity(1024),
59            frames: Vec::with_capacity(64),
60            globals,
61            code,
62            arena,
63            runtime: VmRuntime::new(),
64            profile: None,
65            error_fn_id: 0,
66            error_ip: 0,
67            cancelled: None,
68            buffer_pool: Vec::new(),
69            step_limit: None,
70        }
71    }
72
73    /// Cap the number of dispatched opcodes per `run_named_function` /
74    /// `run` call. The verify runner uses this so a tail-recursive case
75    /// without a base case (very easy for AFL byte-havoc to produce by
76    /// dropping a terminating arm) bails as a `Failure` instead of
77    /// pinning the host. `None` removes the cap.
78    pub fn set_step_limit(&mut self, limit: Option<u64>) {
79        self.step_limit = limit;
80    }
81
82    pub fn start_profiling(&mut self) {
83        self.profile = Some(VmProfileState::new(self.code.functions.len()));
84    }
85
86    pub fn clear_profile(&mut self) {
87        self.profile = None;
88    }
89
90    pub fn profile_report(&self) -> Option<VmProfileReport> {
91        self.profile
92            .as_ref()
93            .map(|profile| profile.report(&self.code))
94    }
95
96    pub fn profile_top_bigrams(&self, n: usize) -> Vec<((u8, u8), u64)> {
97        self.profile
98            .as_ref()
99            .map(|p| p.top_bigrams(n))
100            .unwrap_or_default()
101    }
102
103    /// Set CLI arguments for Args.get().
104    pub fn set_cli_args(&mut self, args: Vec<String>) {
105        self.runtime.set_cli_args(args);
106    }
107
108    pub fn set_silent_console(&mut self, silent: bool) {
109        self.runtime.set_silent_console(silent);
110    }
111
112    /// Set the runtime policy loaded from `aver.toml`.
113    pub fn set_runtime_policy(&mut self, config: crate::config::ProjectConfig) {
114        self.runtime.set_runtime_policy(config);
115    }
116
117    /// Start recording effectful calls.
118    pub fn start_recording(&mut self) {
119        self.runtime.start_recording();
120    }
121
122    /// Cap the recorder at `cap` events. Useful for browser record
123    /// runs where a game with no quit path would otherwise hang the
124    /// wasm main thread. `None` (default) = unlimited, matching CLI.
125    pub fn set_record_cap(&mut self, cap: Option<usize>) {
126        self.runtime.set_record_cap(cap);
127    }
128
129    /// Start replaying from recorded effects.
130    pub fn start_replay(
131        &mut self,
132        effects: Vec<crate::replay::session::EffectRecord>,
133        validate_args: bool,
134    ) {
135        self.runtime.start_replay(effects, validate_args);
136    }
137
138    pub fn set_allowed_effects(&mut self, effects: Vec<u32>) {
139        self.runtime.set_allowed_effects(effects);
140    }
141
142    /// Oracle v1: install the oracle-stub map for a verify-law case.
143    /// Maps classified effect method names (e.g. `"Random.int"`) to the
144    /// fn_id of an Aver stub function with signature
145    /// `(BranchPath, Int, orig_args...) -> T`. Counter resets to 0.
146    pub fn install_oracle_stubs(&mut self, stubs: std::collections::HashMap<String, u32>) {
147        self.runtime.install_oracle_stubs(stubs);
148    }
149
150    /// Clear the oracle-stub map and reset the counter. Always call this
151    /// at the end of the verify-law case so the next case (or normal
152    /// evaluation) doesn't see stale substitutions.
153    pub fn clear_oracle_stubs(&mut self) {
154        self.runtime.clear_oracle_stubs();
155    }
156
157    /// Resolve an Aver top-level function name to its VM fn_id. Used by
158    /// the verify runner when wiring stubs from a `given` clause.
159    pub fn find_fn_id(&self, name: &str) -> Option<u32> {
160        self.code.find(name)
161    }
162
163    /// Oracle v1: start collecting classified-effect emissions into a
164    /// per-case trace buffer. Call before evaluating a verify-trace
165    /// case's LHS; pair with `take_trace_events` after.
166    pub fn start_trace_collection(&mut self) {
167        self.runtime.start_trace_collection();
168    }
169
170    /// Oracle v1: set (or clear) the root fn_id used by the helper-
171    /// boundary filter — only emissions whose immediate caller fn_id
172    /// matches the root count towards `.trace.*` projections. Pass
173    /// `None` (or don't call it) to disable filtering, so every
174    /// classified effect lands in the trace.
175    pub fn set_trace_root_fn_id(&mut self, fn_id: Option<u32>) {
176        self.runtime.set_trace_root_fn_id(fn_id);
177    }
178
179    /// Oracle v1: stop collection without consuming the buffer.
180    pub fn stop_trace_collection(&mut self) {
181        self.runtime.stop_trace_collection();
182    }
183
184    /// Oracle v1: take the collected trace events, stopping collection
185    /// and clearing the buffer. The returned list is what
186    /// `fn.trace.contains(...)` / `.event(k)` / `.length()` operate on.
187    pub fn take_trace_events(&mut self) -> Vec<crate::value::Value> {
188        let events = self.runtime.take_trace_events();
189        self.runtime.stop_trace_collection();
190        events
191    }
192
193    /// Oracle v1: take both events and structural coordinates
194    /// together. Used by tree-navigation projections like
195    /// `.trace.group(N).event(k)` — the coords identify which
196    /// `!`/`?!` group each event came from in source order.
197    pub fn take_trace_events_with_coords(
198        &mut self,
199    ) -> (
200        Vec<crate::value::Value>,
201        Vec<crate::vm::runtime::TraceCoord>,
202    ) {
203        let out = self.runtime.take_trace_events_with_coords();
204        self.runtime.stop_trace_collection();
205        out
206    }
207
208    pub fn set_cancelled(&mut self, flag: std::sync::Arc<std::sync::atomic::AtomicBool>) {
209        self.cancelled = Some(flag);
210    }
211
212    /// Check if this VM has been cancelled by a sibling branch.
213    fn is_cancelled(&self) -> bool {
214        self.cancelled
215            .as_ref()
216            .is_some_and(|f| f.load(std::sync::atomic::Ordering::Relaxed))
217    }
218
219    pub fn recorded_effects(&self) -> &[crate::replay::session::EffectRecord] {
220        self.runtime.recorded_effects()
221    }
222
223    pub fn replay_progress(&self) -> (usize, usize) {
224        self.runtime.replay_progress()
225    }
226
227    pub fn args_diff_count(&self) -> usize {
228        self.runtime.args_diff_count()
229    }
230
231    pub fn ensure_replay_consumed(&self) -> Result<(), VmError> {
232        self.runtime.ensure_replay_consumed()
233    }
234
235    pub fn run(&mut self) -> Result<NanValue, VmError> {
236        self.run_top_level()?;
237        // If there is no `main` function, finish silently (as expected).
238        let has_main = self
239            .code
240            .symbols
241            .find("main")
242            .and_then(|sid| self.code.symbols.resolve_function(sid))
243            .or_else(|| self.code.find("main"))
244            .is_some();
245        if has_main {
246            self.run_named_function("main", &[])
247        } else {
248            Ok(NanValue::UNIT)
249        }
250    }
251
252    pub fn run_top_level(&mut self) -> Result<(), VmError> {
253        if let Some(top_id) = self.code.find("__top_level__") {
254            let _ = self.call_function(top_id, &[])?;
255        }
256        Ok(())
257    }
258
259    pub fn run_named_function(
260        &mut self,
261        name: &str,
262        args: &[NanValue],
263    ) -> Result<NanValue, VmError> {
264        let fn_id = self
265            .code
266            .symbols
267            .find(name)
268            .and_then(|symbol_id| self.code.symbols.resolve_function(symbol_id))
269            .or_else(|| self.code.find(name))
270            .ok_or_else(|| VmError::runtime(format!("function '{}' not found", name)))?;
271        self.runtime
272            .set_allowed_effects(self.code.get(fn_id).effects.clone());
273        self.call_function(fn_id, args)
274    }
275
276    pub fn call_function(&mut self, fn_id: u32, args: &[NanValue]) -> Result<NanValue, VmError> {
277        let chunk = self.code.get(fn_id);
278        let caller_depth = self.frames.len();
279        let arena_mark = self.arena.young_len() as u32;
280        let yard_mark = self.arena.yard_len() as u32;
281        let handoff_mark = self.arena.handoff_len() as u32;
282        let bp = self.stack.len() as u32;
283        for arg in args {
284            self.stack.push(*arg);
285        }
286        for _ in args.len()..(chunk.local_count as usize) {
287            self.stack.push(NanValue::UNIT);
288        }
289        self.frames.push(CallFrame {
290            fn_id,
291            ip: 0,
292            bp,
293            local_count: chunk.local_count,
294            arena_mark,
295            yard_base: yard_mark,
296            yard_mark,
297            handoff_mark,
298            globals_dirty: false,
299            yard_dirty: false,
300            handoff_dirty: false,
301            thin: chunk.thin,
302            parent_thin: chunk.parent_thin,
303        });
304        if let Some(profile) = self.profile.as_mut() {
305            profile.record_function_entry(chunk, fn_id);
306        }
307        self.execute_until(caller_depth).map_err(|err| {
308            // Cold path: resolve source location from line_table.
309            let loc = self
310                .code
311                .resolve_source_location(self.error_fn_id, self.error_ip);
312            err.with_location(loc.map(|(file, line)| super::types::VmSourceLoc {
313                file: file.to_string(),
314                line,
315                fn_name: self.code.get(self.error_fn_id).name.clone(),
316            }))
317        })
318    }
319}