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 pub(super) reverse_independent_eval: bool,
82}
83
84#[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 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 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 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 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 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 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 fn vm_effect_allowed(&self, required_id: u32, symbols: &VmSymbolTable) -> bool {
253 if self.allowed_effects.contains(&required_id) {
254 return true;
255 }
256 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 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 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 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 if self.trace_collecting
451 && is_effectful
452 && crate::types::checker::effect_classification::is_classified(builtin_name)
453 {
454 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 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); 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 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}