Skip to main content

aver/vm/
runtime.rs

1use crate::nan_value::{Arena, NanValue, NanValueConvert};
2use crate::replay::session::RecordedOutcome;
3use crate::replay::{
4    EffectRecord, EffectReplayMode, EffectReplayState, ReplayFailure, json_to_value, value_to_json,
5    values_to_json_lossy,
6};
7use crate::value::Value;
8
9use super::builtin::VmBuiltin;
10use super::symbol::VmSymbolTable;
11use super::types::VmError;
12
13/// VM execution mode for record/replay.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum VmExecutionMode {
16    Normal,
17    Record,
18    Replay,
19}
20
21/// Host/runtime bridge for builtin dispatch, effects, and record/replay.
22///
23/// This is intentionally separate from the core execute loop so the VM stays
24/// focused on bytecode mechanics rather than service plumbing.
25pub(super) struct VmRuntime {
26    allowed_effects: Vec<u32>,
27    cli_args: Vec<String>,
28    silent_console: bool,
29    replay_state: EffectReplayState,
30    runtime_policy: Option<crate::config::ProjectConfig>,
31    /// Oracle v1: during `aver verify` for an effectful law, install a map
32    /// from effect method name (`"Random.int"`) to the fn_id of a stub
33    /// function supplied via `given name: Effect.method = [stub]`. When
34    /// the VM dispatches a classified effect that has a stub installed,
35    /// it calls the stub with `(BranchPath.root, counter, orig_args...)`
36    /// instead of invoking the real effect. Counter increments per call;
37    /// reset when stubs are installed or cleared. Empty map ⇒ no hook.
38    pub(super) oracle_stubs: std::collections::HashMap<String, u32>,
39    pub(super) oracle_counter: u32,
40    /// Oracle v1: during a verify-trace case, the VM collects every
41    /// effect emission the LHS impl makes — effect method name + argument
42    /// snapshot as a JSON-ish value list. The verify runner reads this
43    /// after LHS eval and wraps it into a `Trace` record for the
44    /// `.trace.contains(...)` / `.trace.event(k)` / `.trace.length()`
45    /// projections. Empty when not in verify-trace mode.
46    pub(super) collected_trace_events: Vec<crate::value::Value>,
47    /// Oracle v1: per-event structural coordinates captured at record
48    /// time. `group_id` is the `!`/`?!` group index in source order
49    /// (None when the emission is outside any group). `branch_idx` is
50    /// the current branch inside that group (None at the sequential
51    /// level). Parallel to `collected_trace_events` — same length.
52    /// Used to project `.trace.group(N).branch(idx).event(k)` chains
53    /// without embedding tree metadata inside the Value-level
54    /// `EffectEvent` record.
55    pub(super) collected_trace_coords: Vec<TraceCoord>,
56    /// When true, every dispatched effect is recorded into
57    /// `collected_trace_events` regardless of whether an oracle stub
58    /// handled it. The verify runner flips this on before LHS eval and
59    /// off after RHS eval, so only LHS emissions land in the trace.
60    pub(super) trace_collecting: bool,
61    /// Oracle v1: the fn under `verify <fn> trace` — set by the verify
62    /// runner before the LHS helper runs. Trace events whose immediate
63    /// caller fn_id != this root are helper emissions and get filtered
64    /// out of `.trace.*` projections. When `None`, no filter applies
65    /// (every classified effect is collected — used by non-verify
66    /// tests that drive trace collection directly).
67    pub(super) trace_root_fn_id: Option<u32>,
68    /// Updated by VmExecute on entry to every effect dispatch with the
69    /// `fn_id` of the frame that issued the call. Used by
70    /// `record_trace_event` to apply the helper-boundary filter.
71    pub(super) trace_caller_fn_id: u32,
72    /// Hostile order-axis: when true, `CALL_PAR` (the `(a, b)!`
73    /// independent-product opcode) executes its branches in reverse
74    /// source order but assigns results to the same positional slots,
75    /// so a pure law's output stays identical. A divergence between
76    /// forward and reverse runs proves the law's "independent" claim
77    /// doesn't actually hold — either the stub has hidden state, or
78    /// the compiler is reordering through side-effecting glue. Default
79    /// false; flipped on by the verify runner for hostile-order twin
80    /// cases.
81    pub(super) reverse_independent_eval: bool,
82}
83
84/// Oracle v1: structural coordinates for a recorded trace event.
85/// `group_id` / `branch_idx` are `None` for sequential code outside
86/// any `!`/`?!` group. `dewey` is the dewey-decimal path string (same
87/// form `BranchPath.parse(s)` consumes), empty at the sequential
88/// level — used by the `.path()` bridge to produce an Aver-level
89/// `BranchPath` value from a recorded event.
90#[derive(Debug, Clone, Default)]
91pub struct TraceCoord {
92    pub group_id: Option<u32>,
93    pub branch_idx: Option<u32>,
94    pub dewey: String,
95}
96
97impl Default for VmRuntime {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl VmRuntime {
104    pub(super) fn new() -> Self {
105        Self {
106            allowed_effects: Vec::new(),
107            cli_args: Vec::new(),
108            silent_console: false,
109            replay_state: EffectReplayState::default(),
110            runtime_policy: None,
111            oracle_stubs: std::collections::HashMap::new(),
112            oracle_counter: 0,
113            collected_trace_events: Vec::new(),
114            collected_trace_coords: Vec::new(),
115            trace_collecting: false,
116            trace_root_fn_id: None,
117            trace_caller_fn_id: 0,
118            reverse_independent_eval: false,
119        }
120    }
121
122    pub(super) fn set_reverse_independent_eval(&mut self, value: bool) {
123        self.reverse_independent_eval = value;
124    }
125
126    pub(super) fn reverse_independent_eval(&self) -> bool {
127        self.reverse_independent_eval
128    }
129
130    pub(super) fn start_trace_collection(&mut self) {
131        self.collected_trace_events.clear();
132        self.collected_trace_coords.clear();
133        // Reset the replay scope so `!`/`?!` group ids start at 1 for
134        // each verify-trace case. Without this, the ids accumulate
135        // across cases and user-visible indices like `.trace.group(0)`
136        // stop matching after the first case.
137        self.replay_state.reset_scope();
138        self.trace_collecting = true;
139    }
140
141    pub(super) fn stop_trace_collection(&mut self) {
142        self.trace_collecting = false;
143        self.trace_root_fn_id = None;
144    }
145
146    pub(super) fn set_trace_root_fn_id(&mut self, fn_id: Option<u32>) {
147        self.trace_root_fn_id = fn_id;
148    }
149
150    pub(super) fn sync_caller_fn_id(&mut self, fn_id: u32) {
151        self.trace_caller_fn_id = fn_id;
152    }
153
154    /// Oracle v1: should this effect emission land in the user-visible
155    /// trace? Filters out helper-boundary emissions — if a root fn is
156    /// set, only direct emissions by that fn count. When no root is
157    /// set (tests driving trace directly), every event counts.
158    fn trace_event_is_direct(&self) -> bool {
159        match self.trace_root_fn_id {
160            Some(root) => self.trace_caller_fn_id == root,
161            None => true,
162        }
163    }
164
165    pub(super) fn take_trace_events(&mut self) -> Vec<crate::value::Value> {
166        self.collected_trace_coords.clear();
167        std::mem::take(&mut self.collected_trace_events)
168    }
169
170    /// Oracle v1: take both the events and their structural coordinates
171    /// together. Coords are parallel to events (same length, same
172    /// ordering). Used by `.trace.group(N).*` projections.
173    pub(super) fn take_trace_events_with_coords(
174        &mut self,
175    ) -> (Vec<crate::value::Value>, Vec<TraceCoord>) {
176        let events = std::mem::take(&mut self.collected_trace_events);
177        let coords = std::mem::take(&mut self.collected_trace_coords);
178        (events, coords)
179    }
180
181    pub(super) fn record_trace_event(&mut self, effect_name: &str, args: &[crate::value::Value]) {
182        if !self.trace_collecting || !self.trace_event_is_direct() {
183            return;
184        }
185        let dewey = self.replay_state.oracle_path_string();
186        let event = crate::value::Value::Record {
187            type_name: crate::types::effect_event::TYPE_NAME.to_string(),
188            fields: vec![
189                (
190                    crate::types::effect_event::FIELD_METHOD.to_string(),
191                    crate::value::Value::Str(effect_name.to_string()),
192                ),
193                (
194                    crate::types::effect_event::FIELD_ARGS.to_string(),
195                    crate::value::list_from_vec(args.to_vec()),
196                ),
197                (
198                    crate::types::effect_event::FIELD_PATH.to_string(),
199                    crate::value::Value::Str(dewey.clone()),
200                ),
201            ]
202            .into(),
203        };
204        // Oracle v1: capture structural coordinates alongside the
205        // event — group_id / branch_idx read from the replay state's
206        // live stacks. At the sequential level (outside any group),
207        // both are None and the dewey is the empty string (which is
208        // the canonical `BranchPath.root` representation).
209        let coord = TraceCoord {
210            group_id: self.replay_state.current_group_id(),
211            branch_idx: self.replay_state.current_branch_idx(),
212            dewey,
213        };
214        self.collected_trace_events.push(event);
215        self.collected_trace_coords.push(coord);
216    }
217
218    /// Install the oracle-stub map for the current scope (typically a
219    /// single verify-law case). `stubs` maps classified effect method
220    /// names (e.g. `"Random.int"`) to the fn_id of an Aver stub function
221    /// with signature `(BranchPath, Int, orig_args...) -> T`.
222    pub(super) fn install_oracle_stubs(&mut self, stubs: std::collections::HashMap<String, u32>) {
223        self.oracle_stubs = stubs;
224        self.oracle_counter = 0;
225    }
226
227    /// Clear the oracle-stub map and reset the counter. Called at the end
228    /// of each verify-law case and on any mode transition.
229    pub(super) fn clear_oracle_stubs(&mut self) {
230        self.oracle_stubs.clear();
231        self.oracle_counter = 0;
232    }
233
234    pub(super) fn oracle_stub_for(&self, effect_name: &str) -> Option<u32> {
235        self.oracle_stubs.get(effect_name).copied()
236    }
237
238    pub(super) fn allowed_effects(&self) -> &[u32] {
239        &self.allowed_effects
240    }
241
242    pub(super) fn set_allowed_effects(&mut self, effects: Vec<u32>) {
243        self.allowed_effects = effects;
244    }
245
246    pub(super) fn swap_allowed_effects(&mut self, effects: Vec<u32>) -> Vec<u32> {
247        std::mem::replace(&mut self.allowed_effects, effects)
248    }
249
250    /// Check if a required effect is allowed, supporting namespace shorthand.
251    /// E.g., allowed "Disk" (id=X) covers required "Disk.readText" (id=Y).
252    fn vm_effect_allowed(&self, required_id: u32, symbols: &VmSymbolTable) -> bool {
253        if self.allowed_effects.contains(&required_id) {
254            return true;
255        }
256        // Namespace shorthand: check if any allowed effect is a prefix
257        let required_name = match symbols.get(required_id) {
258            Some(info) => &info.name,
259            None => return false,
260        };
261        for allowed_id in &self.allowed_effects {
262            if let Some(info) = symbols.get(*allowed_id)
263                && crate::effects::effect_satisfies(&info.name, required_name)
264            {
265                return true;
266            }
267        }
268        false
269    }
270
271    pub(super) fn set_cli_args(&mut self, args: Vec<String>) {
272        self.cli_args = args;
273    }
274
275    pub(super) fn cli_args(&self) -> &[String] {
276        &self.cli_args
277    }
278
279    pub(super) fn set_silent_console(&mut self, silent: bool) {
280        self.silent_console = silent;
281    }
282
283    pub(super) fn silent_console(&self) -> bool {
284        self.silent_console
285    }
286
287    pub(super) fn set_runtime_policy(&mut self, config: crate::config::ProjectConfig) {
288        self.runtime_policy = Some(config);
289    }
290
291    pub(super) fn runtime_policy(&self) -> Option<&crate::config::ProjectConfig> {
292        self.runtime_policy.as_ref()
293    }
294
295    pub(super) fn independence_mode(&self) -> crate::config::IndependenceMode {
296        self.runtime_policy
297            .as_ref()
298            .map_or(crate::config::IndependenceMode::default(), |c| {
299                c.independence_mode
300            })
301    }
302
303    pub(super) fn start_recording(&mut self) {
304        self.replay_state.start_recording();
305    }
306
307    pub(super) fn set_record_cap(&mut self, cap: Option<usize>) {
308        self.replay_state.set_record_cap(cap);
309    }
310
311    pub(super) fn start_replay(&mut self, effects: Vec<EffectRecord>, validate_args: bool) {
312        self.replay_state.start_replay(effects, validate_args);
313    }
314
315    pub(super) fn execution_mode(&self) -> VmExecutionMode {
316        match self.replay_state.mode() {
317            EffectReplayMode::Normal => VmExecutionMode::Normal,
318            EffectReplayMode::Record => VmExecutionMode::Record,
319            EffectReplayMode::Replay => VmExecutionMode::Replay,
320        }
321    }
322
323    pub fn recorded_effects(&self) -> &[EffectRecord] {
324        self.replay_state.recorded_effects()
325    }
326
327    pub(super) fn replay_progress(&self) -> (usize, usize) {
328        self.replay_state.replay_progress()
329    }
330
331    pub(super) fn args_diff_count(&self) -> usize {
332        self.replay_state.args_diff_count()
333    }
334
335    pub(super) fn is_effect_tracking(&self) -> bool {
336        matches!(
337            self.replay_state.mode(),
338            EffectReplayMode::Record | EffectReplayMode::Replay
339        )
340    }
341
342    pub(super) fn replay_enter_group(&mut self) {
343        self.replay_state.enter_group();
344    }
345
346    pub(super) fn replay_exit_group(&mut self) {
347        self.replay_state.exit_group();
348    }
349
350    pub(super) fn replay_set_branch(&mut self, index: u32) {
351        self.replay_state.set_branch(index);
352    }
353
354    /// Oracle v1: grab the current (path, counter) pair for an oracle-
355    /// stub dispatch and advance the counter. If we're inside a `!`/`?!`
356    /// group, use the replay state's branch-aware tracking; otherwise
357    /// fall back to the VM-level `oracle_counter` that covers flat
358    /// (root-level) effect calls.
359    pub(super) fn take_oracle_coordinates(&mut self) -> (String, u32) {
360        if self.replay_state.is_inside_group() {
361            let path = self.replay_state.oracle_path_string();
362            let counter = self.replay_state.oracle_branch_counter().unwrap_or(0);
363            self.replay_state.bump_oracle_branch_counter();
364            (path, counter)
365        } else {
366            let c = self.oracle_counter;
367            self.oracle_counter += 1;
368            (String::new(), c)
369        }
370    }
371
372    pub(super) fn ensure_replay_consumed(&self) -> Result<(), VmError> {
373        self.replay_state
374            .ensure_replay_consumed()
375            .map_err(|err| match err {
376                ReplayFailure::Unconsumed { remaining } => VmError::runtime(format!(
377                    "Replay finished with {} unconsumed recorded effect(s)",
378                    remaining
379                )),
380                other => VmError::runtime(format!("invalid replay state: {:?}", other)),
381            })
382    }
383
384    pub(super) fn invoke_builtin_with_owned(
385        &mut self,
386        symbols: &VmSymbolTable,
387        builtin: VmBuiltin,
388        symbol_id: u32,
389        args: &[NanValue],
390        arena: &mut Arena,
391        owned_mask: u8,
392    ) -> Result<NanValue, VmError> {
393        // Fast path: if arg 0 is owned and this is a collection mutator,
394        // call the owned variant that takes instead of cloning.
395        if owned_mask & 1 != 0 {
396            let owned_result = match builtin {
397                VmBuiltin::MapSet => Some(crate::types::map::set_nv_owned(args, arena)),
398                VmBuiltin::VectorSet => Some(crate::types::vector::vec_set_nv_owned(args, arena)),
399                _ => None,
400            };
401            if let Some(result) = owned_result {
402                return result.map_err(|err| match err {
403                    crate::value::RuntimeError::Error(msg)
404                    | crate::value::RuntimeError::ErrorAt { msg, .. } => VmError::runtime(msg),
405                    other => VmError::runtime(format!("{:?}", other)),
406                });
407            }
408        }
409        self.invoke_builtin(symbols, builtin, symbol_id, args, arena)
410    }
411
412    pub(super) fn invoke_builtin(
413        &mut self,
414        symbols: &VmSymbolTable,
415        builtin: VmBuiltin,
416        symbol_id: u32,
417        args: &[NanValue],
418        arena: &mut Arena,
419    ) -> Result<NanValue, VmError> {
420        debug_assert!(
421            !builtin.is_http_server(),
422            "HttpServer builtins require VM callback handling outside VmRuntime"
423        );
424        self.ensure_builtin_effects_allowed(symbols, builtin, symbol_id)?;
425        self.check_runtime_policy(builtin.name(), args, arena)?;
426
427        let builtin_name = builtin.name();
428        // Direct `get(symbol_id)` instead of `find(name)` — the
429        // bytecode already encodes `symbol_id`, so the hash lookup
430        // by name is pure overhead. Profile shows the hashing path
431        // (`Hasher::write` + `BuildHasher::hash_one`) accounts for
432        // ~2.4% self-time on fractal_seahorse, all from the two
433        // `find` callsites in this fn + `ensure_builtin_effects_allowed`.
434        let required_effects = symbols
435            .get(symbol_id)
436            .map(|info| info.required_effects.as_slice())
437            .unwrap_or(&[]);
438        let is_effectful = !required_effects.is_empty();
439        // Oracle v1: when verify-trace collection is active, record every
440        // classified effect dispatched by the LHS impl (pre-invocation
441        // snapshot of args) so `.trace.contains(...)` / `.trace.event(k)`
442        // can query them after LHS returns.
443        //
444        // Output-dimension effects (Console.print / .error / .warn) are
445        // suppressed — they return Unit, and letting the real host
446        // handler fire would pollute the terminal of `aver verify`.
447        // Generative / snapshot effects still dispatch for real unless
448        // the user supplied a stub via `given` (oracle dispatch is
449        // handled earlier in the call site).
450        if self.trace_collecting
451            && is_effectful
452            && crate::types::checker::effect_classification::is_classified(builtin_name)
453        {
454            // Recording is filtered by helper-boundary
455            // (record_trace_event checks trace_event_is_direct);
456            // suppression of output effects is NOT filtered, so a
457            // helper's `Console.print` call doesn't leak to the
458            // terminal of `aver verify` either.
459            let arg_vals: Vec<crate::value::Value> =
460                args.iter().map(|a| a.to_value(arena)).collect();
461            self.record_trace_event(builtin_name, &arg_vals);
462            if let Some(classification) =
463                crate::types::checker::effect_classification::classify(builtin_name)
464            {
465                use crate::types::checker::effect_classification::EffectDimension;
466                if matches!(classification.dimension, EffectDimension::Output) {
467                    return Ok(NanValue::UNIT);
468                }
469            }
470        }
471        match (is_effectful, self.execution_mode()) {
472            (_, VmExecutionMode::Normal) | (false, _) => builtin
473                .invoke_nv(args, arena, &self.cli_args, self.silent_console)
474                .map_err(|err| match err {
475                    crate::value::RuntimeError::Error(msg)
476                    | crate::value::RuntimeError::ErrorAt { msg, .. } => VmError::runtime(msg),
477                    other => VmError::runtime(format!("{:?}", other)),
478                }),
479            (true, VmExecutionMode::Record) => {
480                // Record-cap safety net: caller (e.g. the browser
481                // playground) can set a ceiling via set_record_cap so
482                // runaway loops (game on an input-starved stub) stop
483                // cleanly instead of hanging the wasm main thread.
484                // The partial recording stays intact — callers can
485                // still replay everything captured up to the cap.
486                if self.replay_state.record_full() {
487                    return Err(VmError::runtime(format!(
488                        "record cap reached (kept {} effects so far) while calling {} — program was still running. Recording below is a prefix.",
489                        self.replay_state.recorded_effects().len(),
490                        builtin_name
491                    )));
492                }
493                let args_json = {
494                    let vals: Vec<_> = args.iter().map(|a| a.to_value(arena)).collect();
495                    values_to_json_lossy(&vals)
496                };
497                let nv_result = builtin
498                    .invoke_nv(args, arena, &self.cli_args, self.silent_console)
499                    .map_err(|err| match err {
500                        crate::value::RuntimeError::Error(msg)
501                        | crate::value::RuntimeError::ErrorAt { msg, .. } => VmError::runtime(msg),
502                        other => VmError::runtime(format!("{:?}", other)),
503                    })?;
504                let result_val = nv_result.to_value(arena);
505                let outcome = match value_to_json(&result_val) {
506                    Ok(json) => RecordedOutcome::Value(json),
507                    Err(e) => RecordedOutcome::RuntimeError(e),
508                };
509                self.replay_state
510                    .record_effect(builtin_name, args_json, outcome, "", 0); // VM: no caller fn/line yet
511                Ok(nv_result)
512            }
513            (true, VmExecutionMode::Replay) => self.replay_builtin(builtin_name, args, arena),
514        }
515    }
516
517    fn replay_builtin(
518        &mut self,
519        builtin_name: &str,
520        args: &[NanValue],
521        arena: &mut Arena,
522    ) -> Result<NanValue, VmError> {
523        let got_args = {
524            let vals: Vec<_> = args.iter().map(|a| a.to_value(arena)).collect();
525            values_to_json_lossy(&vals)
526        };
527        let record = self
528            .replay_state
529            .replay_effect(builtin_name, Some(got_args))
530            .map_err(|err| match err {
531                ReplayFailure::Exhausted { effect_type, .. } => VmError::runtime(format!(
532                    "Replay exhausted: no more recorded effects for '{}'",
533                    effect_type
534                )),
535                ReplayFailure::Mismatch { seq, expected, got } => VmError::runtime(format!(
536                    "Replay mismatch at #{}: expected '{}', got '{}'",
537                    seq, expected, got
538                )),
539                ReplayFailure::ArgsMismatch {
540                    seq, effect_type, ..
541                } => VmError::runtime(format!(
542                    "Replay args mismatch at #{} for '{}'",
543                    seq, effect_type
544                )),
545                ReplayFailure::Unconsumed { remaining } => VmError::runtime(format!(
546                    "Replay finished with {} unconsumed recorded effect(s)",
547                    remaining
548                )),
549            })?;
550        let result = match &record {
551            RecordedOutcome::Value(json) => {
552                let val = json_to_value(json).map_err(VmError::runtime)?;
553                NanValue::from_value(&val, arena)
554            }
555            RecordedOutcome::RuntimeError(msg) => return Err(VmError::runtime(msg.clone())),
556        };
557        Ok(result)
558    }
559
560    pub(super) fn ensure_effects_allowed(
561        &self,
562        symbols: &VmSymbolTable,
563        callable_name: &str,
564        required_effects: &[u32],
565    ) -> Result<(), VmError> {
566        if required_effects.is_empty() {
567            return Ok(());
568        }
569        for effect_id in required_effects {
570            if !self.vm_effect_allowed(*effect_id, symbols) {
571                // Oracle v1: during a verify-law case, a classified
572                // effect counts as satisfied at this call edge when
573                // either:
574                //
575                //   (a) it has an installed oracle stub (the stub
576                //       replaces the effect when the dispatcher gets
577                //       to it), or
578                //   (b) trace collection is active — we're running the
579                //       effectful impl under verify, so classified
580                //       output effects (Console.print etc.) are still
581                //       legal even without a stub; they get executed
582                //       and recorded in the trace buffer.
583                if let Some(info) = symbols.get(*effect_id) {
584                    let classified =
585                        crate::types::checker::effect_classification::is_classified(&info.name);
586                    if self.oracle_stubs.contains_key(&info.name) {
587                        continue;
588                    }
589                    if classified && self.trace_collecting {
590                        continue;
591                    }
592                }
593                let effect_name = symbols
594                    .get(*effect_id)
595                    .map(|info| info.name.as_str())
596                    .unwrap_or("<unknown>");
597                return Err(VmError::runtime(format!(
598                    "Runtime effect violation: cannot call '{}' (missing effect: {})",
599                    callable_name, effect_name
600                )));
601            }
602        }
603        Ok(())
604    }
605
606    pub(super) fn ensure_builtin_effects_allowed(
607        &self,
608        symbols: &VmSymbolTable,
609        builtin: VmBuiltin,
610        symbol_id: u32,
611    ) -> Result<(), VmError> {
612        let builtin_name = builtin.name();
613        let required_effects = symbols
614            .get(symbol_id)
615            .map(|info| info.required_effects.as_slice())
616            .unwrap_or(&[]);
617        self.ensure_effects_allowed(symbols, builtin_name, required_effects)
618    }
619
620    fn check_runtime_policy(
621        &self,
622        builtin_name: &str,
623        args: &[NanValue],
624        arena: &Arena,
625    ) -> Result<(), VmError> {
626        if self.execution_mode() == VmExecutionMode::Replay {
627            return Ok(());
628        }
629        let Some(policy) = &self.runtime_policy else {
630            return Ok(());
631        };
632
633        match (builtin_name.split('.').next(), args.first()) {
634            (Some("Http"), Some(arg)) => {
635                if let Value::Str(url) = arg.to_value(arena) {
636                    policy
637                        .check_http_host(builtin_name, &url)
638                        .map_err(VmError::runtime)?;
639                }
640            }
641            (Some("Disk"), Some(arg)) => {
642                if let Value::Str(path) = arg.to_value(arena) {
643                    policy
644                        .check_disk_path(builtin_name, &path)
645                        .map_err(VmError::runtime)?;
646                }
647            }
648            (Some("Env"), Some(arg)) => {
649                if let Value::Str(key) = arg.to_value(arena) {
650                    policy
651                        .check_env_key(builtin_name, &key)
652                        .map_err(VmError::runtime)?;
653                }
654            }
655            _ => {}
656        }
657
658        Ok(())
659    }
660}