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 args: &[NanValue],
370 arena: &mut Arena,
371 owned_mask: u8,
372 ) -> Result<NanValue, VmError> {
373 if owned_mask & 1 != 0 {
376 let owned_result = match builtin {
377 VmBuiltin::MapSet => Some(crate::types::map::set_nv_owned(args, arena)),
378 VmBuiltin::VectorSet => Some(crate::types::vector::vec_set_nv_owned(args, arena)),
379 _ => None,
380 };
381 if let Some(result) = owned_result {
382 return result.map_err(|err| match err {
383 crate::value::RuntimeError::Error(msg)
384 | crate::value::RuntimeError::ErrorAt { msg, .. } => VmError::runtime(msg),
385 other => VmError::runtime(format!("{:?}", other)),
386 });
387 }
388 }
389 self.invoke_builtin(symbols, builtin, args, arena)
390 }
391
392 pub(super) fn invoke_builtin(
393 &mut self,
394 symbols: &VmSymbolTable,
395 builtin: VmBuiltin,
396 args: &[NanValue],
397 arena: &mut Arena,
398 ) -> Result<NanValue, VmError> {
399 debug_assert!(
400 !builtin.is_http_server(),
401 "HttpServer builtins require VM callback handling outside VmRuntime"
402 );
403 self.ensure_builtin_effects_allowed(symbols, builtin)?;
404 self.check_runtime_policy(builtin.name(), args, arena)?;
405
406 let builtin_name = builtin.name();
407 let required_effects = symbols
408 .find(builtin_name)
409 .and_then(|symbol_id| symbols.get(symbol_id))
410 .map(|info| info.required_effects.as_slice())
411 .unwrap_or(&[]);
412 let is_effectful = !required_effects.is_empty();
413 if self.trace_collecting
425 && is_effectful
426 && crate::types::checker::effect_classification::is_classified(builtin_name)
427 {
428 let arg_vals: Vec<crate::value::Value> =
434 args.iter().map(|a| a.to_value(arena)).collect();
435 self.record_trace_event(builtin_name, &arg_vals);
436 if let Some(classification) =
437 crate::types::checker::effect_classification::classify(builtin_name)
438 {
439 use crate::types::checker::effect_classification::EffectDimension;
440 if matches!(classification.dimension, EffectDimension::Output) {
441 return Ok(NanValue::UNIT);
442 }
443 }
444 }
445 match (is_effectful, self.execution_mode()) {
446 (_, VmExecutionMode::Normal) | (false, _) => builtin
447 .invoke_nv(args, arena, &self.cli_args, self.silent_console)
448 .map_err(|err| match err {
449 crate::value::RuntimeError::Error(msg)
450 | crate::value::RuntimeError::ErrorAt { msg, .. } => VmError::runtime(msg),
451 other => VmError::runtime(format!("{:?}", other)),
452 }),
453 (true, VmExecutionMode::Record) => {
454 if self.replay_state.record_full() {
461 return Err(VmError::runtime(format!(
462 "record cap reached (kept {} effects so far) while calling {} — program was still running. Recording below is a prefix.",
463 self.replay_state.recorded_effects().len(),
464 builtin_name
465 )));
466 }
467 let args_json = {
468 let vals: Vec<_> = args.iter().map(|a| a.to_value(arena)).collect();
469 values_to_json_lossy(&vals)
470 };
471 let nv_result = builtin
472 .invoke_nv(args, arena, &self.cli_args, self.silent_console)
473 .map_err(|err| match err {
474 crate::value::RuntimeError::Error(msg)
475 | crate::value::RuntimeError::ErrorAt { msg, .. } => VmError::runtime(msg),
476 other => VmError::runtime(format!("{:?}", other)),
477 })?;
478 let result_val = nv_result.to_value(arena);
479 let outcome = match value_to_json(&result_val) {
480 Ok(json) => RecordedOutcome::Value(json),
481 Err(e) => RecordedOutcome::RuntimeError(e),
482 };
483 self.replay_state
484 .record_effect(builtin_name, args_json, outcome, "", 0); Ok(nv_result)
486 }
487 (true, VmExecutionMode::Replay) => self.replay_builtin(builtin_name, args, arena),
488 }
489 }
490
491 fn replay_builtin(
492 &mut self,
493 builtin_name: &str,
494 args: &[NanValue],
495 arena: &mut Arena,
496 ) -> Result<NanValue, VmError> {
497 let got_args = {
498 let vals: Vec<_> = args.iter().map(|a| a.to_value(arena)).collect();
499 values_to_json_lossy(&vals)
500 };
501 let record = self
502 .replay_state
503 .replay_effect(builtin_name, Some(got_args))
504 .map_err(|err| match err {
505 ReplayFailure::Exhausted { effect_type, .. } => VmError::runtime(format!(
506 "Replay exhausted: no more recorded effects for '{}'",
507 effect_type
508 )),
509 ReplayFailure::Mismatch { seq, expected, got } => VmError::runtime(format!(
510 "Replay mismatch at #{}: expected '{}', got '{}'",
511 seq, expected, got
512 )),
513 ReplayFailure::ArgsMismatch {
514 seq, effect_type, ..
515 } => VmError::runtime(format!(
516 "Replay args mismatch at #{} for '{}'",
517 seq, effect_type
518 )),
519 ReplayFailure::Unconsumed { remaining } => VmError::runtime(format!(
520 "Replay finished with {} unconsumed recorded effect(s)",
521 remaining
522 )),
523 })?;
524 let result = match &record {
525 RecordedOutcome::Value(json) => {
526 let val = json_to_value(json).map_err(VmError::runtime)?;
527 NanValue::from_value(&val, arena)
528 }
529 RecordedOutcome::RuntimeError(msg) => return Err(VmError::runtime(msg.clone())),
530 };
531 Ok(result)
532 }
533
534 pub(super) fn ensure_effects_allowed(
535 &self,
536 symbols: &VmSymbolTable,
537 callable_name: &str,
538 required_effects: &[u32],
539 ) -> Result<(), VmError> {
540 if required_effects.is_empty() {
541 return Ok(());
542 }
543 for effect_id in required_effects {
544 if !self.vm_effect_allowed(*effect_id, symbols) {
545 if let Some(info) = symbols.get(*effect_id) {
558 let classified =
559 crate::types::checker::effect_classification::is_classified(&info.name);
560 if self.oracle_stubs.contains_key(&info.name) {
561 continue;
562 }
563 if classified && self.trace_collecting {
564 continue;
565 }
566 }
567 let effect_name = symbols
568 .get(*effect_id)
569 .map(|info| info.name.as_str())
570 .unwrap_or("<unknown>");
571 return Err(VmError::runtime(format!(
572 "Runtime effect violation: cannot call '{}' (missing effect: {})",
573 callable_name, effect_name
574 )));
575 }
576 }
577 Ok(())
578 }
579
580 pub(super) fn ensure_builtin_effects_allowed(
581 &self,
582 symbols: &VmSymbolTable,
583 builtin: VmBuiltin,
584 ) -> Result<(), VmError> {
585 let builtin_name = builtin.name();
586 let required_effects = symbols
587 .find(builtin_name)
588 .and_then(|symbol_id| symbols.get(symbol_id))
589 .map(|info| info.required_effects.as_slice())
590 .unwrap_or(&[]);
591 self.ensure_effects_allowed(symbols, builtin_name, required_effects)
592 }
593
594 fn check_runtime_policy(
595 &self,
596 builtin_name: &str,
597 args: &[NanValue],
598 arena: &Arena,
599 ) -> Result<(), VmError> {
600 if self.execution_mode() == VmExecutionMode::Replay {
601 return Ok(());
602 }
603 let Some(policy) = &self.runtime_policy else {
604 return Ok(());
605 };
606
607 match (builtin_name.split('.').next(), args.first()) {
608 (Some("Http"), Some(arg)) => {
609 if let Value::Str(url) = arg.to_value(arena) {
610 policy
611 .check_http_host(builtin_name, &url)
612 .map_err(VmError::runtime)?;
613 }
614 }
615 (Some("Disk"), Some(arg)) => {
616 if let Value::Str(path) = arg.to_value(arena) {
617 policy
618 .check_disk_path(builtin_name, &path)
619 .map_err(VmError::runtime)?;
620 }
621 }
622 (Some("Env"), Some(arg)) => {
623 if let Value::Str(key) = arg.to_value(arena) {
624 policy
625 .check_env_key(builtin_name, &key)
626 .map_err(VmError::runtime)?;
627 }
628 }
629 _ => {}
630 }
631
632 Ok(())
633 }
634}