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