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#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum VmExecutionMode {
16 Normal,
17 Record,
18 Replay,
19}
20
21pub(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 pub(super) oracle_stubs: std::collections::HashMap<String, u32>,
39 pub(super) oracle_counter: u32,
40 pub(super) collected_trace_events: Vec<crate::value::Value>,
47 pub(super) collected_trace_coords: Vec<TraceCoord>,
56 pub(super) trace_collecting: bool,
61 pub(super) trace_root_fn_id: Option<u32>,
68 pub(super) trace_caller_fn_id: u32,
72}
73
74#[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 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 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 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 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 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 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 fn vm_effect_allowed(&self, required_id: u32, symbols: &VmSymbolTable) -> bool {
234 if self.allowed_effects.contains(&required_id) {
235 return true;
236 }
237 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 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 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 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 if self.trace_collecting
432 && is_effectful
433 && crate::types::checker::effect_classification::is_classified(builtin_name)
434 {
435 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 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); 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 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}