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