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    /// Hostile order-axis: when `true`, the next `CALL_PAR`
158    /// (`(a, b)!` independent-product) executes its branches in
159    /// reverse source order but assigns each result back to its
160    /// source position. The verify runner flips this on for
161    /// hostile-order twin cases — a pure law's tuple must stay
162    /// invariant, so a divergence proves the "independent" claim
163    /// doesn't hold for the active stub map. Always pair with a
164    /// reset to `false` before the next case to avoid leaking the
165    /// flag into unrelated runs.
166    pub fn set_reverse_independent_eval(&mut self, value: bool) {
167        self.runtime.set_reverse_independent_eval(value);
168    }
169
170    /// Resolve an Aver top-level function name to its VM fn_id. Used by
171    /// the verify runner when wiring stubs from a `given` clause.
172    pub fn find_fn_id(&self, name: &str) -> Option<u32> {
173        self.code.find(name)
174    }
175
176    /// Oracle v1: start collecting classified-effect emissions into a
177    /// per-case trace buffer. Call before evaluating a verify-trace
178    /// case's LHS; pair with `take_trace_events` after.
179    pub fn start_trace_collection(&mut self) {
180        self.runtime.start_trace_collection();
181    }
182
183    /// Oracle v1: set (or clear) the root fn_id used by the helper-
184    /// boundary filter — only emissions whose immediate caller fn_id
185    /// matches the root count towards `.trace.*` projections. Pass
186    /// `None` (or don't call it) to disable filtering, so every
187    /// classified effect lands in the trace.
188    pub fn set_trace_root_fn_id(&mut self, fn_id: Option<u32>) {
189        self.runtime.set_trace_root_fn_id(fn_id);
190    }
191
192    /// Oracle v1: stop collection without consuming the buffer.
193    pub fn stop_trace_collection(&mut self) {
194        self.runtime.stop_trace_collection();
195    }
196
197    /// Oracle v1: take the collected trace events, stopping collection
198    /// and clearing the buffer. The returned list is what
199    /// `fn.trace.contains(...)` / `.event(k)` / `.length()` operate on.
200    pub fn take_trace_events(&mut self) -> Vec<crate::value::Value> {
201        let events = self.runtime.take_trace_events();
202        self.runtime.stop_trace_collection();
203        events
204    }
205
206    /// Oracle v1: take both events and structural coordinates
207    /// together. Used by tree-navigation projections like
208    /// `.trace.group(N).event(k)` — the coords identify which
209    /// `!`/`?!` group each event came from in source order.
210    pub fn take_trace_events_with_coords(
211        &mut self,
212    ) -> (
213        Vec<crate::value::Value>,
214        Vec<crate::vm::runtime::TraceCoord>,
215    ) {
216        let out = self.runtime.take_trace_events_with_coords();
217        self.runtime.stop_trace_collection();
218        out
219    }
220
221    pub fn set_cancelled(&mut self, flag: std::sync::Arc<std::sync::atomic::AtomicBool>) {
222        self.cancelled = Some(flag);
223    }
224
225    /// Check if this VM has been cancelled by a sibling branch.
226    fn is_cancelled(&self) -> bool {
227        self.cancelled
228            .as_ref()
229            .is_some_and(|f| f.load(std::sync::atomic::Ordering::Relaxed))
230    }
231
232    pub fn recorded_effects(&self) -> &[crate::replay::session::EffectRecord] {
233        self.runtime.recorded_effects()
234    }
235
236    pub fn replay_progress(&self) -> (usize, usize) {
237        self.runtime.replay_progress()
238    }
239
240    pub fn args_diff_count(&self) -> usize {
241        self.runtime.args_diff_count()
242    }
243
244    pub fn ensure_replay_consumed(&self) -> Result<(), VmError> {
245        self.runtime.ensure_replay_consumed()
246    }
247
248    pub fn run(&mut self) -> Result<NanValue, VmError> {
249        self.run_top_level()?;
250        // If there is no `main` function, finish silently (as expected).
251        let has_main = self
252            .code
253            .symbols
254            .find("main")
255            .and_then(|sid| self.code.symbols.resolve_function(sid))
256            .or_else(|| self.code.find("main"))
257            .is_some();
258        if has_main {
259            self.run_named_function("main", &[])
260        } else {
261            Ok(NanValue::UNIT)
262        }
263    }
264
265    pub fn run_top_level(&mut self) -> Result<(), VmError> {
266        if let Some(top_id) = self.code.find("__top_level__") {
267            let _ = self.call_function(top_id, &[])?;
268        }
269        Ok(())
270    }
271
272    pub fn run_named_function(
273        &mut self,
274        name: &str,
275        args: &[NanValue],
276    ) -> Result<NanValue, VmError> {
277        let fn_id = self
278            .code
279            .symbols
280            .find(name)
281            .and_then(|symbol_id| self.code.symbols.resolve_function(symbol_id))
282            .or_else(|| self.code.find(name))
283            .ok_or_else(|| VmError::runtime(format!("function '{}' not found", name)))?;
284        self.runtime
285            .set_allowed_effects(self.code.get(fn_id).effects.clone());
286        self.call_function(fn_id, args)
287    }
288
289    pub fn call_function(&mut self, fn_id: u32, args: &[NanValue]) -> Result<NanValue, VmError> {
290        let chunk = self.code.get(fn_id);
291        let caller_depth = self.frames.len();
292        let arena_mark = self.arena.young_len() as u32;
293        let yard_mark = self.arena.yard_len() as u32;
294        let handoff_mark = self.arena.handoff_len() as u32;
295        let bp = self.stack.len() as u32;
296        for arg in args {
297            self.stack.push(*arg);
298        }
299        for _ in args.len()..(chunk.local_count as usize) {
300            self.stack.push(NanValue::UNIT);
301        }
302        self.frames.push(CallFrame {
303            fn_id,
304            ip: 0,
305            bp,
306            local_count: chunk.local_count,
307            arena_mark,
308            yard_base: yard_mark,
309            yard_mark,
310            handoff_mark,
311            globals_dirty: false,
312            yard_dirty: false,
313            handoff_dirty: false,
314            thin: chunk.thin,
315            parent_thin: chunk.parent_thin,
316        });
317        if let Some(profile) = self.profile.as_mut() {
318            profile.record_function_entry(chunk, fn_id);
319        }
320        self.execute_until(caller_depth).map_err(|err| {
321            // Cold path: resolve source location from line_table.
322            let loc = self
323                .code
324                .resolve_source_location(self.error_fn_id, self.error_ip);
325            err.with_location(loc.map(|(file, line)| super::types::VmSourceLoc {
326                file: file.to_string(),
327                line,
328                fn_name: self.code.get(self.error_fn_id).name.clone(),
329            }))
330        })
331    }
332}