1use super::*;
2
3impl Default for Interpreter {
4 fn default() -> Self {
5 Self::new()
6 }
7}
8
9impl Interpreter {
10 pub fn new() -> Self {
11 let mut global = HashMap::new();
12
13 args::register(&mut global);
14 console::register(&mut global);
15 http::register(&mut global);
16 http_server::register(&mut global);
17 disk::register(&mut global);
18 env::register(&mut global);
19 random::register(&mut global);
20 tcp::register(&mut global);
21 time::register(&mut global);
22 int::register(&mut global);
23 float::register(&mut global);
24 string::register(&mut global);
25 list::register(&mut global);
26 map::register(&mut global);
27 char::register(&mut global);
28 byte::register(&mut global);
29
30 {
32 let mut members = HashMap::new();
33 members.insert(
34 "Ok".to_string(),
35 Value::Builtin("__ctor:Result.Ok".to_string()),
36 );
37 members.insert(
38 "Err".to_string(),
39 Value::Builtin("__ctor:Result.Err".to_string()),
40 );
41 for (name, builtin_name) in result::extra_members() {
42 members.insert(name.to_string(), Value::Builtin(builtin_name));
43 }
44 global.insert(
45 "Result".to_string(),
46 Value::Namespace {
47 name: "Result".to_string(),
48 members,
49 },
50 );
51 }
52 {
53 let mut members = HashMap::new();
54 members.insert(
55 "Some".to_string(),
56 Value::Builtin("__ctor:Option.Some".to_string()),
57 );
58 members.insert("None".to_string(), Value::None);
59 for (name, builtin_name) in option::extra_members() {
60 members.insert(name.to_string(), Value::Builtin(builtin_name));
61 }
62 global.insert(
63 "Option".to_string(),
64 Value::Namespace {
65 name: "Option".to_string(),
66 members,
67 },
68 );
69 }
70
71 let mut record_schemas = HashMap::new();
72 record_schemas.insert(
73 "HttpResponse".to_string(),
74 vec![
75 "status".to_string(),
76 "body".to_string(),
77 "headers".to_string(),
78 ],
79 );
80 record_schemas.insert(
81 "HttpRequest".to_string(),
82 vec![
83 "method".to_string(),
84 "path".to_string(),
85 "body".to_string(),
86 "headers".to_string(),
87 ],
88 );
89 record_schemas.insert(
90 "Header".to_string(),
91 vec!["name".to_string(), "value".to_string()],
92 );
93 record_schemas.insert(
94 "Tcp.Connection".to_string(),
95 vec!["id".to_string(), "host".to_string(), "port".to_string()],
96 );
97
98 Interpreter {
99 env: vec![EnvFrame::Owned(global)],
100 module_cache: HashMap::new(),
101 record_schemas,
102 call_stack: Vec::new(),
103 active_local_slots: None,
104 memo_fns: HashSet::new(),
105 memo_cache: HashMap::new(),
106 execution_mode: ExecutionMode::Normal,
107 recorded_effects: Vec::new(),
108 replay_effects: Vec::new(),
109 replay_pos: 0,
110 validate_replay_args: false,
111 recording_sink: None,
112 verify_match_coverage: None,
113 runtime_policy: None,
114 cli_args: Vec::new(),
115 }
116 }
117
118 pub fn execution_mode(&self) -> ExecutionMode {
119 self.execution_mode
120 }
121
122 pub fn set_execution_mode_normal(&mut self) {
123 self.execution_mode = ExecutionMode::Normal;
124 self.recorded_effects.clear();
125 self.replay_effects.clear();
126 self.replay_pos = 0;
127 self.validate_replay_args = false;
128 }
129
130 pub fn start_recording(&mut self) {
131 self.execution_mode = ExecutionMode::Record;
132 self.recorded_effects.clear();
133 self.replay_effects.clear();
134 self.replay_pos = 0;
135 self.validate_replay_args = false;
136 }
137
138 pub fn start_replay(&mut self, effects: Vec<EffectRecord>, validate_args: bool) {
139 self.execution_mode = ExecutionMode::Replay;
140 self.replay_effects = effects;
141 self.replay_pos = 0;
142 self.validate_replay_args = validate_args;
143 self.recorded_effects.clear();
144 }
145
146 pub fn take_recorded_effects(&mut self) -> Vec<EffectRecord> {
147 std::mem::take(&mut self.recorded_effects)
148 }
149
150 pub fn replay_progress(&self) -> (usize, usize) {
151 (self.replay_pos, self.replay_effects.len())
152 }
153
154 pub fn ensure_replay_consumed(&self) -> Result<(), RuntimeError> {
155 if self.execution_mode == ExecutionMode::Replay
156 && self.replay_pos < self.replay_effects.len()
157 {
158 return Err(RuntimeError::ReplayUnconsumed {
159 remaining: self.replay_effects.len() - self.replay_pos,
160 });
161 }
162 Ok(())
163 }
164
165 pub fn configure_recording_sink(&mut self, cfg: RecordingConfig) {
166 self.recording_sink = Some(RecordingSink {
167 path: cfg.path,
168 request_id: cfg.request_id,
169 timestamp: cfg.timestamp,
170 program_file: cfg.program_file,
171 module_root: cfg.module_root,
172 entry_fn: cfg.entry_fn,
173 input: cfg.input,
174 });
175 }
176
177 pub fn recording_sink_path(&self) -> Option<std::path::PathBuf> {
178 self.recording_sink.as_ref().map(|s| s.path.clone())
179 }
180
181 pub fn persist_recording_snapshot(&self, output: RecordedOutcome) -> Result<(), RuntimeError> {
182 let Some(sink) = &self.recording_sink else {
183 return Ok(());
184 };
185
186 let recording = SessionRecording {
187 schema_version: 1,
188 request_id: sink.request_id.clone(),
189 timestamp: sink.timestamp.clone(),
190 program_file: sink.program_file.clone(),
191 module_root: sink.module_root.clone(),
192 entry_fn: sink.entry_fn.clone(),
193 input: sink.input.clone(),
194 effects: self.recorded_effects.clone(),
195 output,
196 };
197
198 let json = session_recording_to_string_pretty(&recording);
199 std::fs::write(&sink.path, json).map_err(|e| {
200 RuntimeError::Error(format!(
201 "Cannot write recording '{}': {}",
202 sink.path.display(),
203 e
204 ))
205 })?;
206 Ok(())
207 }
208
209 pub fn enable_memo(&mut self, fns: HashSet<String>) {
211 self.memo_fns = fns;
212 }
213
214 pub fn set_runtime_policy(&mut self, config: crate::config::ProjectConfig) {
217 self.runtime_policy = Some(config);
218 }
219
220 pub fn set_cli_args(&mut self, args: Vec<String>) {
222 self.cli_args = args;
223 }
224
225 pub(super) fn check_runtime_policy(
228 &self,
229 name: &str,
230 args: &[Value],
231 ) -> Result<(), RuntimeError> {
232 if self.execution_mode == ExecutionMode::Replay {
233 return Ok(());
234 }
235 let Some(policy) = &self.runtime_policy else {
236 return Ok(());
237 };
238
239 if name.starts_with("Http.") {
240 if let Some(Value::Str(url)) = args.first() {
241 policy
242 .check_http_host(name, url)
243 .map_err(RuntimeError::Error)?;
244 }
245 } else if name.starts_with("Disk.") {
246 if let Some(Value::Str(path)) = args.first() {
247 policy
248 .check_disk_path(name, path)
249 .map_err(RuntimeError::Error)?;
250 }
251 } else if name.starts_with("Env.")
252 && let Some(Value::Str(key)) = args.first()
253 {
254 policy
255 .check_env_key(name, key)
256 .map_err(RuntimeError::Error)?;
257 }
258
259 Ok(())
260 }
261
262 pub fn start_verify_match_coverage(&mut self, fn_name: &str) {
263 let Ok(fn_val) = self.lookup(fn_name) else {
264 self.verify_match_coverage = None;
265 return;
266 };
267 let Value::Fn(function) = fn_val else {
268 self.verify_match_coverage = None;
269 return;
270 };
271
272 let mut expected = std::collections::BTreeMap::new();
273 Self::collect_match_sites_from_fn_body(function.body.as_ref(), &mut expected);
274 if expected.is_empty() {
275 self.verify_match_coverage = None;
276 return;
277 }
278
279 self.verify_match_coverage = Some(VerifyMatchCoverageTracker {
280 target_fn: fn_name.to_string(),
281 expected_arms: expected,
282 visited_arms: HashMap::new(),
283 });
284 }
285
286 pub fn finish_verify_match_coverage(&mut self) -> Vec<VerifyMatchCoverageMiss> {
287 let Some(tracker) = self.verify_match_coverage.take() else {
288 return vec![];
289 };
290
291 let mut misses = Vec::new();
292 for ((line, arm_count), expected_total) in tracker.expected_arms {
293 let visited = tracker.visited_arms.get(&(line, arm_count));
294 let mut missing = Vec::new();
295 for arm_idx in 0..expected_total {
296 let covered = visited.is_some_and(|set| set.contains(&arm_idx));
297 if !covered {
298 missing.push(arm_idx);
299 }
300 }
301 if !missing.is_empty() {
302 misses.push(VerifyMatchCoverageMiss {
303 line,
304 total_arms: expected_total,
305 missing_arms: missing,
306 });
307 }
308 }
309 misses
310 }
311
312 pub(super) fn note_verify_match_arm(&mut self, line: usize, arm_count: usize, arm_idx: usize) {
313 let Some(tracker) = self.verify_match_coverage.as_mut() else {
314 return;
315 };
316 let Some(frame) = self.call_stack.last() else {
317 return;
318 };
319 if frame.name.as_str() != tracker.target_fn {
320 return;
321 }
322 let key = (line, arm_count);
323 if !tracker.expected_arms.contains_key(&key) {
324 return;
325 }
326 tracker.visited_arms.entry(key).or_default().insert(arm_idx);
327 }
328
329 fn collect_match_sites_from_fn_body(
330 body: &FnBody,
331 out: &mut std::collections::BTreeMap<MatchSiteKey, usize>,
332 ) {
333 for stmt in body.stmts() {
334 Self::collect_match_sites_from_stmt(stmt, out);
335 }
336 }
337
338 fn collect_match_sites_from_stmt(
339 stmt: &Stmt,
340 out: &mut std::collections::BTreeMap<MatchSiteKey, usize>,
341 ) {
342 match stmt {
343 Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
344 Self::collect_match_sites_from_expr(expr, out);
345 }
346 }
347 }
348
349 fn collect_match_sites_from_expr(
350 expr: &Expr,
351 out: &mut std::collections::BTreeMap<MatchSiteKey, usize>,
352 ) {
353 match expr {
354 Expr::Match {
355 subject,
356 arms,
357 line,
358 } => {
359 out.insert((*line, arms.len()), arms.len());
360 Self::collect_match_sites_from_expr(subject, out);
361 for arm in arms {
362 Self::collect_match_sites_from_expr(&arm.body, out);
363 }
364 }
365 Expr::FnCall(fn_expr, args) => {
366 Self::collect_match_sites_from_expr(fn_expr, out);
367 for arg in args {
368 Self::collect_match_sites_from_expr(arg, out);
369 }
370 }
371 Expr::BinOp(_, left, right) => {
372 Self::collect_match_sites_from_expr(left, out);
373 Self::collect_match_sites_from_expr(right, out);
374 }
375 Expr::Attr(obj, _) | Expr::ErrorProp(obj) => {
376 Self::collect_match_sites_from_expr(obj, out);
377 }
378 Expr::Constructor(_, maybe_arg) => {
379 if let Some(arg) = maybe_arg {
380 Self::collect_match_sites_from_expr(arg, out);
381 }
382 }
383 Expr::InterpolatedStr(parts) => {
384 for part in parts {
385 if let StrPart::Parsed(expr) = part {
386 Self::collect_match_sites_from_expr(expr, out);
387 }
388 }
389 }
390 Expr::List(items) | Expr::Tuple(items) => {
391 for item in items {
392 Self::collect_match_sites_from_expr(item, out);
393 }
394 }
395 Expr::MapLiteral(entries) => {
396 for (key, value) in entries {
397 Self::collect_match_sites_from_expr(key, out);
398 Self::collect_match_sites_from_expr(value, out);
399 }
400 }
401 Expr::RecordCreate { fields, .. } => {
402 for (_, expr) in fields {
403 Self::collect_match_sites_from_expr(expr, out);
404 }
405 }
406 Expr::RecordUpdate { base, updates, .. } => {
407 Self::collect_match_sites_from_expr(base, out);
408 for (_, expr) in updates {
409 Self::collect_match_sites_from_expr(expr, out);
410 }
411 }
412 Expr::TailCall(boxed) => {
413 for arg in &boxed.1 {
414 Self::collect_match_sites_from_expr(arg, out);
415 }
416 }
417 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved(_) => {}
418 }
419 }
420
421 pub(super) fn push_env(&mut self, frame: EnvFrame) {
425 self.env.push(frame);
426 }
427
428 pub(super) fn pop_env(&mut self) {
429 if self.env.len() > 1 {
430 self.env.pop();
431 }
432 }
433
434 pub(super) fn last_owned_scope_mut(
435 &mut self,
436 ) -> Result<&mut HashMap<String, Value>, RuntimeError> {
437 let frame = self
438 .env
439 .last_mut()
440 .ok_or_else(|| RuntimeError::Error("No active scope".to_string()))?;
441 match frame {
442 EnvFrame::Owned(scope) => Ok(scope),
443 EnvFrame::Shared(_) | EnvFrame::Slots(_) => Err(RuntimeError::Error(
444 "Cannot define name in non-owned frame".to_string(),
445 )),
446 }
447 }
448
449 pub(super) fn lookup_ref(&self, name: &str) -> Result<&Value, RuntimeError> {
450 for frame in self.env.iter().rev() {
451 let found = match frame {
452 EnvFrame::Owned(scope) => scope.get(name),
453 EnvFrame::Shared(scope) => scope.get(name),
454 EnvFrame::Slots(_) => None,
456 };
457 if let Some(v) = found {
458 return Ok(v);
459 }
460 }
461 Err(RuntimeError::Error(format!(
462 "Undefined variable: '{}'",
463 name
464 )))
465 }
466
467 pub(super) fn global_scope_clone(&self) -> Result<HashMap<String, Value>, RuntimeError> {
468 let frame = self
469 .env
470 .first()
471 .ok_or_else(|| RuntimeError::Error("No global scope".to_string()))?;
472 match frame {
473 EnvFrame::Owned(scope) => Ok(scope.clone()),
474 EnvFrame::Shared(scope) => Ok((**scope).clone()),
475 EnvFrame::Slots(_) => Err(RuntimeError::Error(
476 "Invalid global scope frame: Slots".to_string(),
477 )),
478 }
479 }
480
481 pub fn lookup(&self, name: &str) -> Result<Value, RuntimeError> {
482 self.lookup_ref(name).cloned()
483 }
484
485 pub fn define(&mut self, name: String, val: Value) {
486 if let Ok(scope) = self.last_owned_scope_mut() {
487 scope.insert(name, val);
488 }
489 }
490
491 fn alias_exposed_type_namespaces(&mut self, module_val: &Value) {
492 let Value::Namespace { members, .. } = module_val else {
493 return;
494 };
495 for (name, member) in members {
496 if matches!(member, Value::Namespace { .. }) {
497 self.define(name.clone(), member.clone());
498 }
499 }
500 }
501
502 pub(super) fn lookup_slot(&self, slot: u16) -> Result<Value, RuntimeError> {
504 let idx = self.env.len() - 1;
505 match &self.env[idx] {
506 EnvFrame::Slots(v) => Ok(v[slot as usize].clone()),
507 _ => {
508 Err(RuntimeError::Error(
510 "Resolved lookup on non-Slots frame".to_string(),
511 ))
512 }
513 }
514 }
515
516 pub(super) fn define_slot(&mut self, slot: u16, val: Value) {
518 let idx = self.env.len() - 1;
519 if let EnvFrame::Slots(v) = &mut self.env[idx] {
520 v[slot as usize] = val;
521 }
522 }
523
524 pub fn define_module_path(&mut self, path: &str, val: Value) -> Result<(), RuntimeError> {
525 let alias_source = val.clone();
526 let parts: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
527 if parts.is_empty() {
528 return Err(RuntimeError::Error("Empty module path".to_string()));
529 }
530 if parts.len() == 1 {
531 self.define(parts[0].to_string(), val);
532 self.alias_exposed_type_namespaces(&alias_source);
533 return Ok(());
534 }
535
536 let head = parts[0];
537 let tail = &parts[1..];
538
539 let result = {
540 let scope = self.last_owned_scope_mut()?;
541 if let Some(existing) = scope.remove(head) {
542 match existing {
543 Value::Namespace { name, mut members } => {
544 Self::insert_namespace_path(&mut members, tail, val)?;
545 scope.insert(head.to_string(), Value::Namespace { name, members });
546 Ok(())
547 }
548 _ => Err(RuntimeError::Error(format!(
549 "Cannot mount module '{}': '{}' is not a namespace",
550 parts.join("."),
551 head
552 ))),
553 }
554 } else {
555 let mut members = HashMap::new();
556 Self::insert_namespace_path(&mut members, tail, val)?;
557 scope.insert(
558 head.to_string(),
559 Value::Namespace {
560 name: head.to_string(),
561 members,
562 },
563 );
564 Ok(())
565 }
566 };
567
568 if result.is_ok() {
569 self.alias_exposed_type_namespaces(&alias_source);
570 }
571 result
572 }
573
574 pub(super) fn insert_namespace_path(
575 scope: &mut HashMap<String, Value>,
576 parts: &[&str],
577 val: Value,
578 ) -> Result<(), RuntimeError> {
579 if parts.len() == 1 {
580 scope.insert(parts[0].to_string(), val);
581 return Ok(());
582 }
583
584 let head = parts[0];
585 let tail = &parts[1..];
586
587 if let Some(existing) = scope.remove(head) {
588 match existing {
589 Value::Namespace { name, mut members } => {
590 Self::insert_namespace_path(&mut members, tail, val)?;
591 scope.insert(head.to_string(), Value::Namespace { name, members });
592 Ok(())
593 }
594 _ => Err(RuntimeError::Error(format!(
595 "Cannot mount module '{}': '{}' is not a namespace",
596 parts.join("."),
597 head
598 ))),
599 }
600 } else {
601 let mut members = HashMap::new();
602 Self::insert_namespace_path(&mut members, tail, val)?;
603 scope.insert(
604 head.to_string(),
605 Value::Namespace {
606 name: head.to_string(),
607 members,
608 },
609 );
610 Ok(())
611 }
612 }
613
614 pub(super) fn module_cache_key(path: &Path) -> String {
615 canonicalize_path(path).to_string_lossy().to_string()
616 }
617
618 pub(super) fn module_decl(items: &[TopLevel]) -> Option<&Module> {
619 items.iter().find_map(|item| {
620 if let TopLevel::Module(m) = item {
621 Some(m)
622 } else {
623 None
624 }
625 })
626 }
627
628 pub(super) fn exposed_set(items: &[TopLevel]) -> Option<HashSet<String>> {
629 Self::module_decl(items).and_then(|m| {
630 if m.exposes.is_empty() {
631 None
632 } else {
633 Some(m.exposes.iter().cloned().collect())
634 }
635 })
636 }
637
638 pub(super) fn cycle_display(loading: &[String], next: &str) -> String {
639 let mut chain = loading
640 .iter()
641 .map(|key| {
642 Path::new(key)
643 .file_stem()
644 .and_then(|s| s.to_str())
645 .unwrap_or(key)
646 .to_string()
647 })
648 .collect::<Vec<_>>();
649 chain.push(
650 Path::new(next)
651 .file_stem()
652 .and_then(|s| s.to_str())
653 .unwrap_or(next)
654 .to_string(),
655 );
656 chain.join(" -> ")
657 }
658
659 pub fn load_module(
660 &mut self,
661 name: &str,
662 base_dir: &str,
663 loading: &mut Vec<String>,
664 loading_set: &mut HashSet<String>,
665 ) -> Result<Value, RuntimeError> {
666 let path = find_module_file(name, base_dir).ok_or_else(|| {
667 RuntimeError::Error(format!("Module '{}' not found in '{}'", name, base_dir))
668 })?;
669 let cache_key = Self::module_cache_key(&path);
670
671 if let Some(cached) = self.module_cache.get(&cache_key) {
672 return Ok(cached.clone());
673 }
674
675 if loading_set.contains(&cache_key) {
676 return Err(RuntimeError::Error(format!(
677 "Circular import: {}",
678 Self::cycle_display(loading, &cache_key)
679 )));
680 }
681
682 loading.push(cache_key.clone());
683 loading_set.insert(cache_key.clone());
684 let result = (|| -> Result<Value, RuntimeError> {
685 let src = std::fs::read_to_string(&path).map_err(|e| {
686 RuntimeError::Error(format!("Cannot read '{}': {}", path.display(), e))
687 })?;
688 let mut items = parse_source(&src).map_err(|e| {
689 RuntimeError::Error(format!("Parse error in '{}': {}", path.display(), e))
690 })?;
691 require_module_declaration(&items, &path.to_string_lossy())
692 .map_err(RuntimeError::Error)?;
693 crate::resolver::resolve_program(&mut items);
694
695 if let Some(module) = Self::module_decl(&items) {
696 let expected = name.rsplit('.').next().unwrap_or(name);
697 if module.name != expected {
698 return Err(RuntimeError::Error(format!(
699 "Module name mismatch: expected '{}' (from '{}'), found '{}' in '{}'",
700 expected,
701 name,
702 module.name,
703 path.display()
704 )));
705 }
706 }
707
708 let mut sub = Interpreter::new();
709
710 if let Some(module) = Self::module_decl(&items) {
711 for dep_name in &module.depends {
712 let dep_ns = self.load_module(dep_name, base_dir, loading, loading_set)?;
713 sub.define_module_path(dep_name, dep_ns)?;
714 }
715 }
716
717 for item in &items {
718 if let TopLevel::TypeDef(td) = item {
719 sub.register_type_def(td);
720 }
721 }
722 for item in &items {
723 if let TopLevel::FnDef(fd) = item {
724 sub.exec_fn_def(fd)?;
725 }
726 }
727 let module_globals = Rc::new(sub.global_scope_clone()?);
728
729 let exposed = Self::exposed_set(&items);
730 let opaque_set: HashSet<String> = Self::module_decl(&items)
731 .map(|m| m.exposes_opaque.iter().cloned().collect())
732 .unwrap_or_default();
733 let mut members = HashMap::new();
734 for item in &items {
735 match item {
736 TopLevel::FnDef(fd) => {
737 let include = match &exposed {
738 Some(set) => set.contains(&fd.name),
739 None => !fd.name.starts_with('_'),
740 };
741 if include {
742 let mut val = sub.lookup(&fd.name).map_err(|_| {
743 RuntimeError::Error(format!(
744 "Failed to export '{}.{}'",
745 name, fd.name
746 ))
747 })?;
748 if let Value::Fn(function) = &mut val {
749 Rc::make_mut(function).home_globals =
750 Some(Rc::clone(&module_globals));
751 }
752 members.insert(fd.name.clone(), val);
753 }
754 }
755 TopLevel::TypeDef(TypeDef::Sum {
756 name: type_name, ..
757 }) => {
758 if opaque_set.contains(type_name) {
760 continue;
761 }
762 let include = match &exposed {
763 Some(set) => set.contains(type_name),
764 None => !type_name.starts_with('_'),
765 };
766 if include {
767 let val = sub.lookup(type_name).map_err(|_| {
768 RuntimeError::Error(format!(
769 "Failed to export '{}.{}'",
770 name, type_name
771 ))
772 })?;
773 members.insert(type_name.clone(), val);
774 }
775 }
776 _ => {}
777 }
778 }
779
780 Ok(Value::Namespace {
781 name: name.to_string(),
782 members,
783 })
784 })();
785 loading.pop();
786 loading_set.remove(&cache_key);
787
788 match result {
789 Ok(ns) => {
790 self.module_cache.insert(cache_key, ns.clone());
791 Ok(ns)
792 }
793 Err(e) => Err(e),
794 }
795 }
796}