1use crate::nan_value::{Arena, NanValue};
2use std::collections::{HashMap, HashSet};
3use std::path::{Path, PathBuf};
4use std::rc::Rc;
5
6use crate::ast::*;
7use crate::replay::{
8 EffectRecord, EffectReplayMode, EffectReplayState, JsonValue, RecordedOutcome,
9 SessionRecording, session_recording_to_string_pretty, value_to_json, values_to_json_lossy,
10};
11#[cfg(feature = "terminal")]
12use crate::services::terminal;
13use crate::services::{args, console, disk, env, http, http_server, random, tcp, time};
14use crate::source::{
15 canonicalize_path, find_module_file, parse_source, require_module_declaration,
16};
17use crate::types::{bool, byte, char, float, int, list, map, option, result, string, vector};
18pub use crate::value::{Env, EnvFrame, RuntimeError, Value, aver_display, aver_repr};
20use crate::value::{list_len, list_view};
21
22#[derive(Debug, Clone)]
23struct CallFrame {
24 name: Rc<String>,
25 effects: Rc<Vec<String>>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ExecutionMode {
30 Normal,
31 Record,
32 Replay,
33}
34
35const MEMO_CACHE_CAP_PER_FN: usize = 4096;
36
37#[derive(Debug, Clone)]
38struct MemoEntry {
39 id: u64,
40 args: Vec<Value>,
41 result: Value,
42}
43
44#[derive(Debug, Clone)]
45struct RecordingSink {
46 path: PathBuf,
47 request_id: String,
48 timestamp: String,
49 program_file: String,
50 module_root: String,
51 entry_fn: String,
52 input: JsonValue,
53}
54
55type MatchSiteKey = (usize, usize); #[derive(Debug, Clone)]
58struct VerifyMatchCoverageTracker {
59 target_fn: String,
60 expected_arms: std::collections::BTreeMap<MatchSiteKey, usize>,
61 visited_arms: HashMap<MatchSiteKey, HashSet<usize>>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct VerifyMatchCoverageMiss {
66 pub line: usize,
67 pub total_arms: usize,
68 pub missing_arms: Vec<usize>, }
70
71#[derive(Debug, Clone)]
72pub struct RecordingConfig {
73 pub path: PathBuf,
74 pub request_id: String,
75 pub timestamp: String,
76 pub program_file: String,
77 pub module_root: String,
78 pub entry_fn: String,
79 pub input: JsonValue,
80}
81
82#[derive(Debug, Default, Clone)]
84struct FnMemoCache {
85 buckets: HashMap<u64, Vec<MemoEntry>>,
87 positions: HashMap<u64, (u64, usize)>,
89 links: HashMap<u64, (Option<u64>, Option<u64>)>,
91 lru_head: Option<u64>,
92 lru_tail: Option<u64>,
93 next_id: u64,
94 len: usize,
95}
96
97impl FnMemoCache {
98 fn get(&mut self, hash: u64, args: &[Value]) -> Option<Value> {
99 let found = self
100 .buckets
101 .get_mut(&hash)
102 .and_then(|entries| entries.iter_mut().find(|entry| entry.args == args))
103 .map(|entry| (entry.id, entry.result.clone()));
104
105 if let Some((id, value)) = found {
106 self.touch(id);
107 Some(value)
108 } else {
109 None
110 }
111 }
112
113 fn insert(&mut self, hash: u64, args: Vec<Value>, result: Value, cap: usize) {
114 let update_hit = self
115 .buckets
116 .get_mut(&hash)
117 .and_then(|entries| entries.iter_mut().find(|entry| entry.args == args))
118 .map(|entry| {
119 entry.result = result.clone();
120 entry.id
121 });
122
123 if let Some(id) = update_hit {
124 self.touch(id);
125 return;
126 }
127
128 if self.len >= cap {
129 self.evict_lru();
130 }
131
132 let id = self.alloc_id();
133 let entry = MemoEntry { id, args, result };
134 let idx = self.buckets.entry(hash).or_default().len();
135 self.buckets.entry(hash).or_default().push(entry);
136 self.positions.insert(id, (hash, idx));
137 self.append_tail(id);
138 self.len += 1;
139 }
140
141 fn alloc_id(&mut self) -> u64 {
142 let id = self.next_id;
143 self.next_id = self.next_id.wrapping_add(1);
144 id
145 }
146
147 fn evict_lru(&mut self) {
148 if let Some(id) = self.lru_head {
149 self.remove_entry(id);
150 }
151 }
152
153 fn touch(&mut self, id: u64) {
154 if self.lru_tail == Some(id) {
155 return;
156 }
157 self.detach(id);
158 self.append_tail(id);
159 }
160
161 fn append_tail(&mut self, id: u64) {
162 let prev = self.lru_tail;
163 self.links.insert(id, (prev, None));
164 if let Some(tail) = prev {
165 if let Some((_, next)) = self.links.get_mut(&tail) {
166 *next = Some(id);
167 }
168 } else {
169 self.lru_head = Some(id);
170 }
171 self.lru_tail = Some(id);
172 }
173
174 fn detach(&mut self, id: u64) {
175 let Some((prev, next)) = self.links.get(&id).copied() else {
176 return;
177 };
178
179 if let Some(p) = prev {
180 if let Some((_, p_next)) = self.links.get_mut(&p) {
181 *p_next = next;
182 }
183 } else {
184 self.lru_head = next;
185 }
186
187 if let Some(n) = next {
188 if let Some((n_prev, _)) = self.links.get_mut(&n) {
189 *n_prev = prev;
190 }
191 } else {
192 self.lru_tail = prev;
193 }
194
195 if let Some(link) = self.links.get_mut(&id) {
196 *link = (None, None);
197 }
198 }
199
200 fn remove_entry(&mut self, id: u64) {
201 let Some((hash, idx)) = self.positions.remove(&id) else {
202 return;
203 };
204 self.detach(id);
205 self.links.remove(&id);
206
207 let mut remove_bucket = false;
208 if let Some(entries) = self.buckets.get_mut(&hash) {
209 entries.swap_remove(idx);
210 if idx < entries.len() {
211 let moved_id = entries[idx].id;
212 self.positions.insert(moved_id, (hash, idx));
213 }
214 remove_bucket = entries.is_empty();
215 }
216 if remove_bucket {
217 self.buckets.remove(&hash);
218 }
219 self.len = self.len.saturating_sub(1);
220 }
221
222 fn get_nv_as_value(&mut self, hash: u64, nv_args: &[NanValue], arena: &Arena) -> Option<Value> {
225 let args: Vec<Value> = nv_args.iter().map(|nv| nv.to_value(arena)).collect();
226 self.get(hash, &args)
227 }
228
229 fn insert_nv(
231 &mut self,
232 hash: u64,
233 nv_args: Vec<NanValue>,
234 nv_result: NanValue,
235 arena: &Arena,
236 cap: usize,
237 ) {
238 let args: Vec<Value> = nv_args.iter().map(|nv| nv.to_value(arena)).collect();
239 let result = nv_result.to_value(arena);
240 self.insert(hash, args, result, cap);
241 }
242}
243
244pub struct Interpreter {
245 pub env: Env,
246 env_base: usize,
250 pub arena: Arena,
252 module_cache: HashMap<String, Value>,
253 record_schemas: HashMap<String, Vec<String>>,
256 call_stack: Vec<CallFrame>,
257 active_local_slots: Option<Rc<HashMap<String, u16>>>,
260 memo_fns: HashSet<String>,
262 memo_cache: HashMap<String, FnMemoCache>,
264 replay_state: EffectReplayState,
265 recording_sink: Option<RecordingSink>,
266 verify_match_coverage: Option<VerifyMatchCoverageTracker>,
267 runtime_policy: Option<crate::config::ProjectConfig>,
269 cli_args: Vec<String>,
271}
272
273mod api;
274mod builtins;
275mod core;
276mod effects;
277mod eval;
278mod exec;
279mod ir_bridge;
280pub(crate) mod lowered;
281mod ops;
282mod patterns;
283
284#[cfg(test)]
285mod memo_cache_tests {
286 use super::*;
287
288 #[test]
289 fn collision_bucket_is_exact_match_on_args() {
290 let mut cache = FnMemoCache::default();
291 cache.insert(1, vec![Value::Int(1)], Value::Int(10), 8);
292 cache.insert(1, vec![Value::Int(2)], Value::Int(20), 8);
293
294 assert_eq!(cache.get(1, &[Value::Int(1)]), Some(Value::Int(10)));
295 assert_eq!(cache.get(1, &[Value::Int(2)]), Some(Value::Int(20)));
296 assert_eq!(cache.get(1, &[Value::Int(3)]), None);
297 }
298
299 #[test]
300 fn lru_evicts_least_recently_used() {
301 let mut cache = FnMemoCache::default();
302 cache.insert(11, vec![Value::Int(1)], Value::Int(10), 2);
303 cache.insert(22, vec![Value::Int(2)], Value::Int(20), 2);
304
305 assert_eq!(cache.get(11, &[Value::Int(1)]), Some(Value::Int(10)));
307 cache.insert(33, vec![Value::Int(3)], Value::Int(30), 2);
308
309 assert_eq!(cache.get(11, &[Value::Int(1)]), Some(Value::Int(10)));
310 assert_eq!(cache.get(22, &[Value::Int(2)]), None);
311 assert_eq!(cache.get(33, &[Value::Int(3)]), Some(Value::Int(30)));
312 }
313}
314
315#[cfg(test)]
316mod ir_bridge_tests {
317 use aver_rt::AverVector;
318
319 use super::ir_bridge::InterpreterLowerCtx;
320 use super::lowered::{
321 self, ExprId, LoweredDirectCallTarget, LoweredExpr, LoweredForwardArg, LoweredLeafOp,
322 LoweredMatchArm, LoweredTailCallTarget,
323 };
324 use super::*;
325
326 fn register_task_event_type(interpreter: &mut Interpreter) {
327 interpreter.register_type_def(&TypeDef::Sum {
328 name: "TaskEvent".to_string(),
329 variants: vec![
330 TypeVariant {
331 name: "TaskCreated".to_string(),
332 fields: vec!["String".to_string()],
333 },
334 TypeVariant {
335 name: "TaskMoved".to_string(),
336 fields: vec!["String".to_string(), "Int".to_string()],
337 },
338 ],
339 line: 1,
340 });
341
342 let mut members = HashMap::new();
343 members.insert(
344 "TaskEvent".to_string(),
345 interpreter
346 .lookup("TaskEvent")
347 .expect("TaskEvent namespace should be defined"),
348 );
349 interpreter
350 .define_module_path(
351 "Domain.Types",
352 Value::Namespace {
353 name: "Types".to_string(),
354 members,
355 },
356 )
357 .expect("module path should be mountable");
358 }
359
360 #[test]
361 fn eval_constructor_uses_shared_semantics_for_wrappers_and_qualified_variants() {
362 let mut interpreter = Interpreter::new();
363 register_task_event_type(&mut interpreter);
364
365 let ok_expr = Expr::Constructor(
366 "Ok".to_string(),
367 Some(Box::new(Expr::Literal(Literal::Int(7)))),
368 );
369 let created_expr = Expr::Constructor(
370 "Domain.Types.TaskEvent.TaskCreated".to_string(),
371 Some(Box::new(Expr::Literal(Literal::Str("now".to_string())))),
372 );
373
374 assert_eq!(
375 interpreter
376 .eval_expr(&ok_expr)
377 .expect("Ok constructor should evaluate"),
378 Value::Ok(Box::new(Value::Int(7)))
379 );
380
381 match interpreter
382 .eval_expr(&created_expr)
383 .expect("qualified constructor should build a variant")
384 {
385 Value::Variant {
386 type_name,
387 variant,
388 fields,
389 } => {
390 assert_eq!(type_name, "TaskEvent");
391 assert_eq!(variant, "TaskCreated");
392 assert_eq!(fields.as_ref(), &[Value::Str("now".to_string())]);
393 }
394 other => panic!("expected variant, got {other:?}"),
395 }
396 }
397
398 #[test]
399 fn qualified_constructor_patterns_use_shared_semantics_in_both_match_paths() {
400 let mut interpreter = Interpreter::new();
401 register_task_event_type(&mut interpreter);
402
403 let pattern = Pattern::Constructor(
404 "Domain.Types.TaskEvent.TaskCreated".to_string(),
405 vec!["at".to_string()],
406 );
407 let value = Value::Variant {
408 type_name: "TaskEvent".to_string(),
409 variant: "TaskCreated".to_string(),
410 fields: vec![Value::Str("now".to_string())].into(),
411 };
412
413 assert_eq!(
414 interpreter.match_pattern(&pattern, &value),
415 Some(vec![("at".to_string(), Value::Str("now".to_string()))])
416 );
417
418 let nv = NanValue::from_value(&value, &mut interpreter.arena);
419 let bindings = interpreter
420 .match_pattern_nv(&pattern, nv)
421 .expect("nan-value pattern path should match");
422 assert_eq!(bindings.len(), 1);
423 assert_eq!(bindings[0].0, "at");
424 assert_eq!(
425 bindings[0].1.to_value(&interpreter.arena),
426 Value::Str("now".to_string())
427 );
428 }
429
430 #[test]
431 fn runtime_match_dispatch_plan_selects_bool_list_and_wrapper_arms() {
432 let mut interpreter = Interpreter::new();
433
434 let bool_arms = vec![
435 LoweredMatchArm {
436 pattern: Pattern::Literal(Literal::Bool(true)),
437 body: ExprId(0),
438 },
439 LoweredMatchArm {
440 pattern: Pattern::Ident("other".to_string()),
441 body: ExprId(1),
442 },
443 ];
444 let (bool_arm, bool_bindings) = interpreter
445 .try_dispatch_match_plan_nv(NanValue::FALSE, &bool_arms)
446 .expect("bool match plan should dispatch");
447 assert_eq!(bool_arm, 1);
448 assert_eq!(bool_bindings.len(), 1);
449 assert_eq!(bool_bindings[0].0, "other");
450 assert_eq!(bool_bindings[0].1.bits(), NanValue::FALSE.bits());
451
452 let non_empty_list = NanValue::new_list(interpreter.arena.push_list(vec![NanValue::TRUE]));
453 let list_arms = vec![
454 LoweredMatchArm {
455 pattern: Pattern::EmptyList,
456 body: ExprId(0),
457 },
458 LoweredMatchArm {
459 pattern: Pattern::Cons("head".to_string(), "tail".to_string()),
460 body: ExprId(1),
461 },
462 ];
463 let (list_arm, list_bindings) = interpreter
464 .try_dispatch_match_plan_nv(non_empty_list, &list_arms)
465 .expect("list match plan should dispatch");
466 assert_eq!(list_arm, 1);
467 assert_eq!(list_bindings.len(), 2);
468 assert_eq!(list_bindings[0].0, "head");
469 assert_eq!(list_bindings[0].1.bits(), NanValue::TRUE.bits());
470
471 let wrapper_arms = vec![
472 LoweredMatchArm {
473 pattern: Pattern::Constructor("Option.None".to_string(), vec![]),
474 body: ExprId(0),
475 },
476 LoweredMatchArm {
477 pattern: Pattern::Constructor("Option.Some".to_string(), vec!["x".to_string()]),
478 body: ExprId(1),
479 },
480 LoweredMatchArm {
481 pattern: Pattern::Ident("fallback".to_string()),
482 body: ExprId(2),
483 },
484 ];
485 let some_subject = NanValue::new_some_value(
486 NanValue::new_int(7, &mut interpreter.arena),
487 &mut interpreter.arena,
488 );
489 let (wrapper_arm, wrapper_bindings) = interpreter
490 .try_dispatch_match_plan_nv(some_subject, &wrapper_arms)
491 .expect("wrapper match plan should dispatch");
492 assert_eq!(wrapper_arm, 1);
493 assert_eq!(wrapper_bindings.len(), 1);
494 assert_eq!(wrapper_bindings[0].0, "x");
495 assert_eq!(wrapper_bindings[0].1.as_int(&interpreter.arena), 7);
496
497 let (default_arm, default_bindings) = interpreter
498 .try_dispatch_match_plan_nv(NanValue::TRUE, &wrapper_arms)
499 .expect("dispatch table default arm should match");
500 assert_eq!(default_arm, 2);
501 assert_eq!(default_bindings.len(), 1);
502 assert_eq!(default_bindings[0].0, "fallback");
503 assert_eq!(default_bindings[0].1.bits(), NanValue::TRUE.bits());
504 }
505
506 #[test]
507 fn lowered_roots_classify_shared_builtin_leaf_ops() {
508 let interpreter = Interpreter::new();
509 let ctx = InterpreterLowerCtx::new(&interpreter);
510
511 let map_get = Expr::FnCall(
512 Box::new(Expr::Attr(
513 Box::new(Expr::Ident("Map".to_string())),
514 "get".to_string(),
515 )),
516 vec![
517 Expr::Ident("m".to_string()),
518 Expr::Literal(Literal::Str("k".to_string())),
519 ],
520 );
521 let (lowered_map_get, map_get_root) = lowered::lower_expr_root(&map_get, &ctx);
522 assert!(matches!(
523 lowered_map_get.expr(map_get_root),
524 LoweredExpr::Leaf(LoweredLeafOp::MapGet { .. })
525 ));
526
527 let vec_default = Expr::FnCall(
528 Box::new(Expr::Attr(
529 Box::new(Expr::Ident("Option".to_string())),
530 "withDefault".to_string(),
531 )),
532 vec![
533 Expr::FnCall(
534 Box::new(Expr::Attr(
535 Box::new(Expr::Ident("Vector".to_string())),
536 "get".to_string(),
537 )),
538 vec![Expr::Ident("v".to_string()), Expr::Ident("idx".to_string())],
539 ),
540 Expr::Literal(Literal::Int(0)),
541 ],
542 );
543 let (lowered_vec_default, vec_default_root) = lowered::lower_expr_root(&vec_default, &ctx);
544 assert!(matches!(
545 lowered_vec_default.expr(vec_default_root),
546 LoweredExpr::Leaf(LoweredLeafOp::VectorGetOrDefaultLiteral {
547 default_literal: Literal::Int(0),
548 ..
549 })
550 ));
551 }
552
553 #[test]
554 fn runtime_executes_shared_leaf_ops_in_host_interpreter() {
555 let mut interpreter = Interpreter::new();
556
557 let mut map_value = HashMap::new();
558 map_value.insert(Value::Str("k".to_string()), Value::Int(7));
559 interpreter.define("m".to_string(), Value::Map(map_value));
560 interpreter.define(
561 "v".to_string(),
562 Value::Vector(AverVector::from_vec(vec![Value::Int(10), Value::Int(20)])),
563 );
564 interpreter.define("idx".to_string(), Value::Int(1));
565 interpreter.define("miss".to_string(), Value::Int(5));
566
567 let map_get = Expr::FnCall(
568 Box::new(Expr::Attr(
569 Box::new(Expr::Ident("Map".to_string())),
570 "get".to_string(),
571 )),
572 vec![
573 Expr::Ident("m".to_string()),
574 Expr::Literal(Literal::Str("k".to_string())),
575 ],
576 );
577 assert_eq!(
578 interpreter
579 .eval_expr(&map_get)
580 .expect("Map.get leaf should run"),
581 Value::Some(Box::new(Value::Int(7)))
582 );
583
584 let vec_hit = Expr::FnCall(
585 Box::new(Expr::Attr(
586 Box::new(Expr::Ident("Option".to_string())),
587 "withDefault".to_string(),
588 )),
589 vec![
590 Expr::FnCall(
591 Box::new(Expr::Attr(
592 Box::new(Expr::Ident("Vector".to_string())),
593 "get".to_string(),
594 )),
595 vec![Expr::Ident("v".to_string()), Expr::Ident("idx".to_string())],
596 ),
597 Expr::Literal(Literal::Int(0)),
598 ],
599 );
600 assert_eq!(
601 interpreter
602 .eval_expr(&vec_hit)
603 .expect("Vector.get default leaf should return hit"),
604 Value::Int(20)
605 );
606
607 let vec_miss = Expr::FnCall(
608 Box::new(Expr::Attr(
609 Box::new(Expr::Ident("Option".to_string())),
610 "withDefault".to_string(),
611 )),
612 vec![
613 Expr::FnCall(
614 Box::new(Expr::Attr(
615 Box::new(Expr::Ident("Vector".to_string())),
616 "get".to_string(),
617 )),
618 vec![
619 Expr::Ident("v".to_string()),
620 Expr::Ident("miss".to_string()),
621 ],
622 ),
623 Expr::Literal(Literal::Int(0)),
624 ],
625 );
626 assert_eq!(
627 interpreter
628 .eval_expr(&vec_miss)
629 .expect("Vector.get default leaf should return fallback"),
630 Value::Int(0)
631 );
632 }
633
634 #[test]
635 fn lowered_roots_classify_shared_call_plans_for_builtin_and_function_calls() {
636 let mut interpreter = Interpreter::new();
637 register_task_event_type(&mut interpreter);
638 let ctx = InterpreterLowerCtx::new(&interpreter);
639
640 let list_len = Expr::FnCall(
641 Box::new(Expr::Attr(
642 Box::new(Expr::Ident("List".to_string())),
643 "len".to_string(),
644 )),
645 vec![Expr::Ident("xs".to_string())],
646 );
647 let (lowered_builtin, builtin_root) = lowered::lower_expr_root(&list_len, &ctx);
648 assert!(matches!(
649 lowered_builtin.expr(builtin_root),
650 LoweredExpr::DirectCall {
651 target: LoweredDirectCallTarget::Builtin(name),
652 ..
653 } if name == "List.len"
654 ));
655
656 let identity_call = Expr::FnCall(
657 Box::new(Expr::Ident("identity".to_string())),
658 vec![Expr::Literal(Literal::Int(7))],
659 );
660 let (lowered_fn, fn_root) = lowered::lower_expr_root(&identity_call, &ctx);
661 assert!(matches!(
662 lowered_fn.expr(fn_root),
663 LoweredExpr::DirectCall {
664 target: LoweredDirectCallTarget::Function(name),
665 ..
666 } if name == "identity"
667 ));
668
669 let wrapper_call = Expr::FnCall(
670 Box::new(Expr::Attr(
671 Box::new(Expr::Ident("Result".to_string())),
672 "Ok".to_string(),
673 )),
674 vec![Expr::Literal(Literal::Int(1))],
675 );
676 let (lowered_wrapper, wrapper_root) = lowered::lower_expr_root(&wrapper_call, &ctx);
677 assert!(matches!(
678 lowered_wrapper.expr(wrapper_root),
679 LoweredExpr::DirectCall {
680 target: LoweredDirectCallTarget::Wrapper(crate::ir::WrapperKind::ResultOk),
681 ..
682 }
683 ));
684
685 let none_call = Expr::FnCall(
686 Box::new(Expr::Attr(
687 Box::new(Expr::Ident("Option".to_string())),
688 "None".to_string(),
689 )),
690 vec![],
691 );
692 let (lowered_none, none_root) = lowered::lower_expr_root(&none_call, &ctx);
693 assert!(matches!(
694 lowered_none.expr(none_root),
695 LoweredExpr::DirectCall {
696 target: LoweredDirectCallTarget::NoneValue,
697 ..
698 }
699 ));
700
701 let ctor_call = Expr::FnCall(
702 Box::new(Expr::Attr(
703 Box::new(Expr::Attr(
704 Box::new(Expr::Attr(
705 Box::new(Expr::Ident("Domain".to_string())),
706 "Types".to_string(),
707 )),
708 "TaskEvent".to_string(),
709 )),
710 "TaskCreated".to_string(),
711 )),
712 vec![Expr::Literal(Literal::Str("now".to_string()))],
713 );
714 let (lowered_ctor, ctor_root) = lowered::lower_expr_root(&ctor_call, &ctx);
715 assert!(matches!(
716 lowered_ctor.expr(ctor_root),
717 LoweredExpr::DirectCall {
718 target: LoweredDirectCallTarget::TypeConstructor {
719 qualified_type_name,
720 variant_name,
721 },
722 ..
723 } if qualified_type_name == "Domain.Types.TaskEvent" && variant_name == "TaskCreated"
724 ));
725 }
726
727 #[test]
728 fn runtime_executes_shared_direct_calls_in_host_interpreter() {
729 let mut interpreter = Interpreter::new();
730 register_task_event_type(&mut interpreter);
731
732 let identity = FnDef {
733 name: "identity".to_string(),
734 line: 1,
735 params: vec![("x".to_string(), "Int".to_string())],
736 return_type: "Int".to_string(),
737 effects: vec![],
738 desc: None,
739 body: Rc::new(FnBody::from_expr(Expr::Resolved(0))),
740 resolution: Some(FnResolution {
741 local_slots: Rc::new(HashMap::from([(String::from("x"), 0u16)])),
742 local_count: 1,
743 }),
744 };
745 interpreter
746 .exec_fn_def(&identity)
747 .expect("identity function should register");
748
749 let list_len = Expr::FnCall(
750 Box::new(Expr::Attr(
751 Box::new(Expr::Ident("List".to_string())),
752 "len".to_string(),
753 )),
754 vec![Expr::List(vec![
755 Expr::Literal(Literal::Int(1)),
756 Expr::Literal(Literal::Int(2)),
757 ])],
758 );
759 assert_eq!(
760 interpreter
761 .eval_expr(&list_len)
762 .expect("direct builtin call should run"),
763 Value::Int(2)
764 );
765
766 let identity_call = Expr::FnCall(
767 Box::new(Expr::Ident("identity".to_string())),
768 vec![Expr::Literal(Literal::Int(9))],
769 );
770 assert_eq!(
771 interpreter
772 .eval_expr(&identity_call)
773 .expect("direct function call should run"),
774 Value::Int(9)
775 );
776
777 let wrapper_call = Expr::FnCall(
778 Box::new(Expr::Attr(
779 Box::new(Expr::Ident("Result".to_string())),
780 "Ok".to_string(),
781 )),
782 vec![Expr::Literal(Literal::Int(5))],
783 );
784 assert_eq!(
785 interpreter
786 .eval_expr(&wrapper_call)
787 .expect("direct wrapper call should run"),
788 Value::Ok(Box::new(Value::Int(5)))
789 );
790
791 let none_call = Expr::FnCall(
792 Box::new(Expr::Attr(
793 Box::new(Expr::Ident("Option".to_string())),
794 "None".to_string(),
795 )),
796 vec![],
797 );
798 assert_eq!(
799 interpreter
800 .eval_expr(&none_call)
801 .expect("direct none call should run"),
802 Value::None
803 );
804
805 let ctor_call = Expr::FnCall(
806 Box::new(Expr::Attr(
807 Box::new(Expr::Attr(
808 Box::new(Expr::Attr(
809 Box::new(Expr::Ident("Domain".to_string())),
810 "Types".to_string(),
811 )),
812 "TaskEvent".to_string(),
813 )),
814 "TaskCreated".to_string(),
815 )),
816 vec![Expr::Literal(Literal::Str("now".to_string()))],
817 );
818 match interpreter
819 .eval_expr(&ctor_call)
820 .expect("direct qualified ctor call should run")
821 {
822 Value::Variant {
823 type_name,
824 variant,
825 fields,
826 } => {
827 assert_eq!(type_name, "TaskEvent");
828 assert_eq!(variant, "TaskCreated");
829 assert_eq!(fields.as_ref(), &[Value::Str("now".to_string())]);
830 }
831 other => panic!("expected variant, got {other:?}"),
832 }
833
834 let multi_ctor_call = Expr::FnCall(
835 Box::new(Expr::Attr(
836 Box::new(Expr::Attr(
837 Box::new(Expr::Attr(
838 Box::new(Expr::Ident("Domain".to_string())),
839 "Types".to_string(),
840 )),
841 "TaskEvent".to_string(),
842 )),
843 "TaskMoved".to_string(),
844 )),
845 vec![
846 Expr::Literal(Literal::Str("later".to_string())),
847 Expr::Literal(Literal::Int(3)),
848 ],
849 );
850 match interpreter
851 .eval_expr(&multi_ctor_call)
852 .expect("direct qualified multi-field ctor call should run")
853 {
854 Value::Variant {
855 type_name,
856 variant,
857 fields,
858 } => {
859 assert_eq!(type_name, "TaskEvent");
860 assert_eq!(variant, "TaskMoved");
861 assert_eq!(
862 fields.as_ref(),
863 &[Value::Str("later".to_string()), Value::Int(3)]
864 );
865 }
866 other => panic!("expected variant, got {other:?}"),
867 }
868 }
869
870 #[test]
871 fn lowered_fn_bodies_classify_forward_calls_through_shared_ir() {
872 let interpreter = Interpreter::new();
873 let ctx = InterpreterLowerCtx::new(&interpreter);
874 let body = FnBody::from_expr(Expr::FnCall(
875 Box::new(Expr::Ident("first".to_string())),
876 vec![Expr::Resolved(1), Expr::Resolved(0)],
877 ));
878 let lowered = lowered::lower_fn_body(&body, &ctx, "swap");
879
880 assert!(matches!(
881 lowered.expr(ExprId(0)),
882 LoweredExpr::ForwardCall {
883 target: LoweredDirectCallTarget::Function(name),
884 args,
885 } if name == "first"
886 && matches!(
887 args.as_ref(),
888 [LoweredForwardArg::Slot(1), LoweredForwardArg::Slot(0)]
889 )
890 ));
891 }
892
893 #[test]
894 fn runtime_executes_forward_calls_without_evaling_arg_exprs() {
895 let mut interpreter = Interpreter::new();
896
897 let first = FnDef {
898 name: "first".to_string(),
899 line: 1,
900 params: vec![
901 ("x".to_string(), "Int".to_string()),
902 ("y".to_string(), "Int".to_string()),
903 ],
904 return_type: "Int".to_string(),
905 effects: vec![],
906 desc: None,
907 body: Rc::new(FnBody::from_expr(Expr::Resolved(0))),
908 resolution: Some(FnResolution {
909 local_slots: Rc::new(HashMap::from([
910 (String::from("x"), 0u16),
911 (String::from("y"), 1u16),
912 ])),
913 local_count: 2,
914 }),
915 };
916 interpreter
917 .exec_fn_def(&first)
918 .expect("first function should register");
919
920 let swap = FnDef {
921 name: "swap".to_string(),
922 line: 2,
923 params: vec![
924 ("a".to_string(), "Int".to_string()),
925 ("b".to_string(), "Int".to_string()),
926 ],
927 return_type: "Int".to_string(),
928 effects: vec![],
929 desc: None,
930 body: Rc::new(FnBody::from_expr(Expr::FnCall(
931 Box::new(Expr::Ident("first".to_string())),
932 vec![Expr::Resolved(1), Expr::Resolved(0)],
933 ))),
934 resolution: Some(FnResolution {
935 local_slots: Rc::new(HashMap::from([
936 (String::from("a"), 0u16),
937 (String::from("b"), 1u16),
938 ])),
939 local_count: 2,
940 }),
941 };
942 interpreter
943 .exec_fn_def(&swap)
944 .expect("swap function should register");
945
946 let swap_call = Expr::FnCall(
947 Box::new(Expr::Ident("swap".to_string())),
948 vec![
949 Expr::Literal(Literal::Int(3)),
950 Expr::Literal(Literal::Int(7)),
951 ],
952 );
953 assert_eq!(
954 interpreter
955 .eval_expr(&swap_call)
956 .expect("forward call should run"),
957 Value::Int(7)
958 );
959 }
960
961 #[test]
962 fn lowered_fn_bodies_classify_tail_calls_through_shared_ir() {
963 let interpreter = Interpreter::new();
964 let ctx = InterpreterLowerCtx::new(&interpreter);
965
966 let self_body = FnBody::from_expr(Expr::TailCall(Box::new((
967 "loop".to_string(),
968 vec![Expr::Literal(Literal::Int(1))],
969 ))));
970 let lowered_self = lowered::lower_fn_body(&self_body, &ctx, "loop");
971 assert!(matches!(
972 lowered_self.expr(ExprId(1)),
973 LoweredExpr::TailCall {
974 target: LoweredTailCallTarget::SelfCall,
975 ..
976 }
977 ));
978
979 let known_body = FnBody::from_expr(Expr::TailCall(Box::new((
980 "other".to_string(),
981 vec![Expr::Literal(Literal::Int(2))],
982 ))));
983 let lowered_known = lowered::lower_fn_body(&known_body, &ctx, "loop");
984 assert!(matches!(
985 lowered_known.expr(ExprId(1)),
986 LoweredExpr::TailCall {
987 target: LoweredTailCallTarget::KnownFunction(name),
988 ..
989 } if name == "other"
990 ));
991
992 let unknown_body = FnBody::from_expr(Expr::TailCall(Box::new((
993 "Result.Ok".to_string(),
994 vec![Expr::Literal(Literal::Int(3))],
995 ))));
996 let lowered_unknown = lowered::lower_fn_body(&unknown_body, &ctx, "loop");
997 assert!(matches!(
998 lowered_unknown.expr(ExprId(1)),
999 LoweredExpr::TailCall {
1000 target: LoweredTailCallTarget::Unknown(name),
1001 ..
1002 } if name == "Result.Ok"
1003 ));
1004 }
1005}