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