1use crate::nan_value::{Arena, NanValue};
2use std::collections::{HashMap, HashSet};
3use std::path::{Path, PathBuf};
4use std::sync::Arc as 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 pub(crate) last_call_line: usize,
274}
275
276mod api;
277mod builtins;
278mod core;
279mod effects;
280mod eval;
281mod exec;
282mod ir_bridge;
283pub(crate) mod lowered;
284mod ops;
285mod patterns;
286
287#[cfg(test)]
288mod memo_cache_tests {
289 use super::*;
290
291 #[test]
292 fn collision_bucket_is_exact_match_on_args() {
293 let mut cache = FnMemoCache::default();
294 cache.insert(1, vec![Value::Int(1)], Value::Int(10), 8);
295 cache.insert(1, vec![Value::Int(2)], Value::Int(20), 8);
296
297 assert_eq!(cache.get(1, &[Value::Int(1)]), Some(Value::Int(10)));
298 assert_eq!(cache.get(1, &[Value::Int(2)]), Some(Value::Int(20)));
299 assert_eq!(cache.get(1, &[Value::Int(3)]), None);
300 }
301
302 #[test]
303 fn lru_evicts_least_recently_used() {
304 let mut cache = FnMemoCache::default();
305 cache.insert(11, vec![Value::Int(1)], Value::Int(10), 2);
306 cache.insert(22, vec![Value::Int(2)], Value::Int(20), 2);
307
308 assert_eq!(cache.get(11, &[Value::Int(1)]), Some(Value::Int(10)));
310 cache.insert(33, vec![Value::Int(3)], Value::Int(30), 2);
311
312 assert_eq!(cache.get(11, &[Value::Int(1)]), Some(Value::Int(10)));
313 assert_eq!(cache.get(22, &[Value::Int(2)]), None);
314 assert_eq!(cache.get(33, &[Value::Int(3)]), Some(Value::Int(30)));
315 }
316}
317
318#[cfg(test)]
319mod ir_bridge_tests {
320 use aver_rt::AverVector;
321
322 use super::ir_bridge::InterpreterLowerCtx;
323 use super::lowered::{
324 self, ExprId, LoweredDirectCallTarget, LoweredExpr, LoweredForwardArg, LoweredLeafOp,
325 LoweredMatchArm, LoweredTailCallTarget,
326 };
327 use super::*;
328
329 fn sb(expr: Expr) -> Spanned<Expr> {
331 Spanned::bare(expr)
332 }
333
334 fn sbb(expr: Expr) -> Box<Spanned<Expr>> {
336 Box::new(Spanned::bare(expr))
337 }
338
339 fn register_task_event_type(interpreter: &mut Interpreter) {
340 interpreter.register_type_def(&TypeDef::Sum {
341 name: "TaskEvent".to_string(),
342 variants: vec![
343 TypeVariant {
344 name: "TaskCreated".to_string(),
345 fields: vec!["String".to_string()],
346 },
347 TypeVariant {
348 name: "TaskMoved".to_string(),
349 fields: vec!["String".to_string(), "Int".to_string()],
350 },
351 ],
352 line: 1,
353 });
354
355 let mut members = HashMap::new();
356 members.insert(
357 "TaskEvent".to_string(),
358 interpreter
359 .lookup("TaskEvent")
360 .expect("TaskEvent namespace should be defined"),
361 );
362 interpreter
363 .define_module_path(
364 "Domain.Types",
365 Value::Namespace {
366 name: "Types".to_string(),
367 members,
368 },
369 )
370 .expect("module path should be mountable");
371 }
372
373 #[test]
374 fn eval_constructor_uses_shared_semantics_for_wrappers_and_qualified_variants() {
375 let mut interpreter = Interpreter::new();
376 register_task_event_type(&mut interpreter);
377
378 let ok_expr = sb(Expr::Constructor(
379 "Ok".to_string(),
380 Some(sbb(Expr::Literal(Literal::Int(7)))),
381 ));
382 let created_expr = sb(Expr::Constructor(
383 "Domain.Types.TaskEvent.TaskCreated".to_string(),
384 Some(sbb(Expr::Literal(Literal::Str("now".to_string())))),
385 ));
386
387 assert_eq!(
388 interpreter
389 .eval_expr(&ok_expr)
390 .expect("Ok constructor should evaluate"),
391 Value::Ok(Box::new(Value::Int(7)))
392 );
393
394 match interpreter
395 .eval_expr(&created_expr)
396 .expect("qualified constructor should build a variant")
397 {
398 Value::Variant {
399 type_name,
400 variant,
401 fields,
402 } => {
403 assert_eq!(type_name, "TaskEvent");
404 assert_eq!(variant, "TaskCreated");
405 assert_eq!(fields.as_ref(), &[Value::Str("now".to_string())]);
406 }
407 other => panic!("expected variant, got {other:?}"),
408 }
409 }
410
411 #[test]
412 fn qualified_constructor_patterns_use_shared_semantics_in_both_match_paths() {
413 let mut interpreter = Interpreter::new();
414 register_task_event_type(&mut interpreter);
415
416 let pattern = Pattern::Constructor(
417 "Domain.Types.TaskEvent.TaskCreated".to_string(),
418 vec!["at".to_string()],
419 );
420 let value = Value::Variant {
421 type_name: "TaskEvent".to_string(),
422 variant: "TaskCreated".to_string(),
423 fields: vec![Value::Str("now".to_string())].into(),
424 };
425
426 assert_eq!(
427 interpreter.match_pattern(&pattern, &value),
428 Some(vec![("at".to_string(), Value::Str("now".to_string()))])
429 );
430
431 let nv = NanValue::from_value(&value, &mut interpreter.arena);
432 let bindings = interpreter
433 .match_pattern_nv(&pattern, nv)
434 .expect("nan-value pattern path should match");
435 assert_eq!(bindings.len(), 1);
436 assert_eq!(bindings[0].0, "at");
437 assert_eq!(
438 bindings[0].1.to_value(&interpreter.arena),
439 Value::Str("now".to_string())
440 );
441 }
442
443 #[test]
444 fn runtime_match_dispatch_plan_selects_bool_list_and_wrapper_arms() {
445 let mut interpreter = Interpreter::new();
446
447 let bool_arms = vec![
448 LoweredMatchArm {
449 pattern: Pattern::Literal(Literal::Bool(true)),
450 body: ExprId(0),
451 },
452 LoweredMatchArm {
453 pattern: Pattern::Ident("other".to_string()),
454 body: ExprId(1),
455 },
456 ];
457 let (bool_arm, bool_bindings) = interpreter
458 .try_dispatch_match_plan_nv(NanValue::FALSE, &bool_arms)
459 .expect("bool match plan should dispatch");
460 assert_eq!(bool_arm, 1);
461 assert_eq!(bool_bindings.len(), 1);
462 assert_eq!(bool_bindings[0].0, "other");
463 assert_eq!(bool_bindings[0].1.bits(), NanValue::FALSE.bits());
464
465 let non_empty_list = NanValue::new_list(interpreter.arena.push_list(vec![NanValue::TRUE]));
466 let list_arms = vec![
467 LoweredMatchArm {
468 pattern: Pattern::EmptyList,
469 body: ExprId(0),
470 },
471 LoweredMatchArm {
472 pattern: Pattern::Cons("head".to_string(), "tail".to_string()),
473 body: ExprId(1),
474 },
475 ];
476 let (list_arm, list_bindings) = interpreter
477 .try_dispatch_match_plan_nv(non_empty_list, &list_arms)
478 .expect("list match plan should dispatch");
479 assert_eq!(list_arm, 1);
480 assert_eq!(list_bindings.len(), 2);
481 assert_eq!(list_bindings[0].0, "head");
482 assert_eq!(list_bindings[0].1.bits(), NanValue::TRUE.bits());
483
484 let wrapper_arms = vec![
485 LoweredMatchArm {
486 pattern: Pattern::Constructor("Option.None".to_string(), vec![]),
487 body: ExprId(0),
488 },
489 LoweredMatchArm {
490 pattern: Pattern::Constructor("Option.Some".to_string(), vec!["x".to_string()]),
491 body: ExprId(1),
492 },
493 LoweredMatchArm {
494 pattern: Pattern::Ident("fallback".to_string()),
495 body: ExprId(2),
496 },
497 ];
498 let some_subject = NanValue::new_some_value(
499 NanValue::new_int(7, &mut interpreter.arena),
500 &mut interpreter.arena,
501 );
502 let (wrapper_arm, wrapper_bindings) = interpreter
503 .try_dispatch_match_plan_nv(some_subject, &wrapper_arms)
504 .expect("wrapper match plan should dispatch");
505 assert_eq!(wrapper_arm, 1);
506 assert_eq!(wrapper_bindings.len(), 1);
507 assert_eq!(wrapper_bindings[0].0, "x");
508 assert_eq!(wrapper_bindings[0].1.as_int(&interpreter.arena), 7);
509
510 let (default_arm, default_bindings) = interpreter
511 .try_dispatch_match_plan_nv(NanValue::TRUE, &wrapper_arms)
512 .expect("dispatch table default arm should match");
513 assert_eq!(default_arm, 2);
514 assert_eq!(default_bindings.len(), 1);
515 assert_eq!(default_bindings[0].0, "fallback");
516 assert_eq!(default_bindings[0].1.bits(), NanValue::TRUE.bits());
517 }
518
519 #[test]
520 fn lowered_roots_classify_shared_builtin_leaf_ops() {
521 let interpreter = Interpreter::new();
522 let ctx = InterpreterLowerCtx::new(&interpreter);
523
524 let map_get = sb(Expr::FnCall(
525 sbb(Expr::Attr(
526 sbb(Expr::Ident("Map".to_string())),
527 "get".to_string(),
528 )),
529 vec![
530 sb(Expr::Ident("m".to_string())),
531 sb(Expr::Literal(Literal::Str("k".to_string()))),
532 ],
533 ));
534 let (lowered_map_get, map_get_root) = lowered::lower_expr_root(&map_get, &ctx);
535 assert!(matches!(
536 lowered_map_get.expr(map_get_root),
537 LoweredExpr::Leaf(LoweredLeafOp::MapGet { .. })
538 ));
539
540 let vec_default = sb(Expr::FnCall(
541 sbb(Expr::Attr(
542 sbb(Expr::Ident("Option".to_string())),
543 "withDefault".to_string(),
544 )),
545 vec![
546 sb(Expr::FnCall(
547 sbb(Expr::Attr(
548 sbb(Expr::Ident("Vector".to_string())),
549 "get".to_string(),
550 )),
551 vec![
552 sb(Expr::Ident("v".to_string())),
553 sb(Expr::Ident("idx".to_string())),
554 ],
555 )),
556 sb(Expr::Literal(Literal::Int(0))),
557 ],
558 ));
559 let (lowered_vec_default, vec_default_root) = lowered::lower_expr_root(&vec_default, &ctx);
560 assert!(matches!(
561 lowered_vec_default.expr(vec_default_root),
562 LoweredExpr::Leaf(LoweredLeafOp::VectorGetOrDefaultLiteral {
563 default_literal: Literal::Int(0),
564 ..
565 })
566 ));
567 }
568
569 #[test]
570 fn runtime_executes_shared_leaf_ops_in_host_interpreter() {
571 let mut interpreter = Interpreter::new();
572
573 let mut map_value = HashMap::new();
574 map_value.insert(Value::Str("k".to_string()), Value::Int(7));
575 interpreter.define("m".to_string(), Value::Map(map_value));
576 interpreter.define(
577 "v".to_string(),
578 Value::Vector(AverVector::from_vec(vec![Value::Int(10), Value::Int(20)])),
579 );
580 interpreter.define("idx".to_string(), Value::Int(1));
581 interpreter.define("miss".to_string(), Value::Int(5));
582
583 let map_get = sb(Expr::FnCall(
584 sbb(Expr::Attr(
585 sbb(Expr::Ident("Map".to_string())),
586 "get".to_string(),
587 )),
588 vec![
589 sb(Expr::Ident("m".to_string())),
590 sb(Expr::Literal(Literal::Str("k".to_string()))),
591 ],
592 ));
593 assert_eq!(
594 interpreter
595 .eval_expr(&map_get)
596 .expect("Map.get leaf should run"),
597 Value::Some(Box::new(Value::Int(7)))
598 );
599
600 let vec_hit = sb(Expr::FnCall(
601 sbb(Expr::Attr(
602 sbb(Expr::Ident("Option".to_string())),
603 "withDefault".to_string(),
604 )),
605 vec![
606 sb(Expr::FnCall(
607 sbb(Expr::Attr(
608 sbb(Expr::Ident("Vector".to_string())),
609 "get".to_string(),
610 )),
611 vec![
612 sb(Expr::Ident("v".to_string())),
613 sb(Expr::Ident("idx".to_string())),
614 ],
615 )),
616 sb(Expr::Literal(Literal::Int(0))),
617 ],
618 ));
619 assert_eq!(
620 interpreter
621 .eval_expr(&vec_hit)
622 .expect("Vector.get default leaf should return hit"),
623 Value::Int(20)
624 );
625
626 let vec_miss = sb(Expr::FnCall(
627 sbb(Expr::Attr(
628 sbb(Expr::Ident("Option".to_string())),
629 "withDefault".to_string(),
630 )),
631 vec![
632 sb(Expr::FnCall(
633 sbb(Expr::Attr(
634 sbb(Expr::Ident("Vector".to_string())),
635 "get".to_string(),
636 )),
637 vec![
638 sb(Expr::Ident("v".to_string())),
639 sb(Expr::Ident("miss".to_string())),
640 ],
641 )),
642 sb(Expr::Literal(Literal::Int(0))),
643 ],
644 ));
645 assert_eq!(
646 interpreter
647 .eval_expr(&vec_miss)
648 .expect("Vector.get default leaf should return fallback"),
649 Value::Int(0)
650 );
651 }
652
653 #[test]
654 fn lowered_roots_classify_shared_call_plans_for_builtin_and_function_calls() {
655 let mut interpreter = Interpreter::new();
656 register_task_event_type(&mut interpreter);
657 let ctx = InterpreterLowerCtx::new(&interpreter);
658
659 let list_len = sb(Expr::FnCall(
660 sbb(Expr::Attr(
661 sbb(Expr::Ident("List".to_string())),
662 "len".to_string(),
663 )),
664 vec![sb(Expr::Ident("xs".to_string()))],
665 ));
666 let (lowered_builtin, builtin_root) = lowered::lower_expr_root(&list_len, &ctx);
667 assert!(matches!(
668 lowered_builtin.expr(builtin_root),
669 LoweredExpr::DirectCall {
670 target: LoweredDirectCallTarget::Builtin(name),
671 ..
672 } if name == "List.len"
673 ));
674
675 let identity_call = sb(Expr::FnCall(
676 sbb(Expr::Ident("identity".to_string())),
677 vec![sb(Expr::Literal(Literal::Int(7)))],
678 ));
679 let (lowered_fn, fn_root) = lowered::lower_expr_root(&identity_call, &ctx);
680 assert!(matches!(
681 lowered_fn.expr(fn_root),
682 LoweredExpr::DirectCall {
683 target: LoweredDirectCallTarget::Function(name),
684 ..
685 } if name == "identity"
686 ));
687
688 let wrapper_call = sb(Expr::FnCall(
689 sbb(Expr::Attr(
690 sbb(Expr::Ident("Result".to_string())),
691 "Ok".to_string(),
692 )),
693 vec![sb(Expr::Literal(Literal::Int(1)))],
694 ));
695 let (lowered_wrapper, wrapper_root) = lowered::lower_expr_root(&wrapper_call, &ctx);
696 assert!(matches!(
697 lowered_wrapper.expr(wrapper_root),
698 LoweredExpr::DirectCall {
699 target: LoweredDirectCallTarget::Wrapper(crate::ir::WrapperKind::ResultOk),
700 ..
701 }
702 ));
703
704 let none_call = sb(Expr::FnCall(
705 sbb(Expr::Attr(
706 sbb(Expr::Ident("Option".to_string())),
707 "None".to_string(),
708 )),
709 vec![],
710 ));
711 let (lowered_none, none_root) = lowered::lower_expr_root(&none_call, &ctx);
712 assert!(matches!(
713 lowered_none.expr(none_root),
714 LoweredExpr::DirectCall {
715 target: LoweredDirectCallTarget::NoneValue,
716 ..
717 }
718 ));
719
720 let ctor_call = sb(Expr::FnCall(
721 sbb(Expr::Attr(
722 sbb(Expr::Attr(
723 sbb(Expr::Attr(
724 sbb(Expr::Ident("Domain".to_string())),
725 "Types".to_string(),
726 )),
727 "TaskEvent".to_string(),
728 )),
729 "TaskCreated".to_string(),
730 )),
731 vec![sb(Expr::Literal(Literal::Str("now".to_string())))],
732 ));
733 let (lowered_ctor, ctor_root) = lowered::lower_expr_root(&ctor_call, &ctx);
734 assert!(matches!(
735 lowered_ctor.expr(ctor_root),
736 LoweredExpr::DirectCall {
737 target: LoweredDirectCallTarget::TypeConstructor {
738 qualified_type_name,
739 variant_name,
740 },
741 ..
742 } if qualified_type_name == "Domain.Types.TaskEvent" && variant_name == "TaskCreated"
743 ));
744 }
745
746 #[test]
747 fn runtime_executes_shared_direct_calls_in_host_interpreter() {
748 let mut interpreter = Interpreter::new();
749 register_task_event_type(&mut interpreter);
750
751 let identity = FnDef {
752 name: "identity".to_string(),
753 line: 1,
754 params: vec![("x".to_string(), "Int".to_string())],
755 return_type: "Int".to_string(),
756 effects: vec![],
757 desc: None,
758 body: Rc::new(FnBody::from_expr(sb(Expr::Resolved(0)))),
759 resolution: Some(FnResolution {
760 local_slots: Rc::new(HashMap::from([(String::from("x"), 0u16)])),
761 local_count: 1,
762 }),
763 };
764 interpreter
765 .exec_fn_def(&identity)
766 .expect("identity function should register");
767
768 let list_len = sb(Expr::FnCall(
769 sbb(Expr::Attr(
770 sbb(Expr::Ident("List".to_string())),
771 "len".to_string(),
772 )),
773 vec![sb(Expr::List(vec![
774 sb(Expr::Literal(Literal::Int(1))),
775 sb(Expr::Literal(Literal::Int(2))),
776 ]))],
777 ));
778 assert_eq!(
779 interpreter
780 .eval_expr(&list_len)
781 .expect("direct builtin call should run"),
782 Value::Int(2)
783 );
784
785 let identity_call = sb(Expr::FnCall(
786 sbb(Expr::Ident("identity".to_string())),
787 vec![sb(Expr::Literal(Literal::Int(9)))],
788 ));
789 assert_eq!(
790 interpreter
791 .eval_expr(&identity_call)
792 .expect("direct function call should run"),
793 Value::Int(9)
794 );
795
796 let wrapper_call = sb(Expr::FnCall(
797 sbb(Expr::Attr(
798 sbb(Expr::Ident("Result".to_string())),
799 "Ok".to_string(),
800 )),
801 vec![sb(Expr::Literal(Literal::Int(5)))],
802 ));
803 assert_eq!(
804 interpreter
805 .eval_expr(&wrapper_call)
806 .expect("direct wrapper call should run"),
807 Value::Ok(Box::new(Value::Int(5)))
808 );
809
810 let none_call = sb(Expr::FnCall(
811 sbb(Expr::Attr(
812 sbb(Expr::Ident("Option".to_string())),
813 "None".to_string(),
814 )),
815 vec![],
816 ));
817 assert_eq!(
818 interpreter
819 .eval_expr(&none_call)
820 .expect("direct none call should run"),
821 Value::None
822 );
823
824 let ctor_call = sb(Expr::FnCall(
825 sbb(Expr::Attr(
826 sbb(Expr::Attr(
827 sbb(Expr::Attr(
828 sbb(Expr::Ident("Domain".to_string())),
829 "Types".to_string(),
830 )),
831 "TaskEvent".to_string(),
832 )),
833 "TaskCreated".to_string(),
834 )),
835 vec![sb(Expr::Literal(Literal::Str("now".to_string())))],
836 ));
837 match interpreter
838 .eval_expr(&ctor_call)
839 .expect("direct qualified ctor call should run")
840 {
841 Value::Variant {
842 type_name,
843 variant,
844 fields,
845 } => {
846 assert_eq!(type_name, "TaskEvent");
847 assert_eq!(variant, "TaskCreated");
848 assert_eq!(fields.as_ref(), &[Value::Str("now".to_string())]);
849 }
850 other => panic!("expected variant, got {other:?}"),
851 }
852
853 let multi_ctor_call = sb(Expr::FnCall(
854 sbb(Expr::Attr(
855 sbb(Expr::Attr(
856 sbb(Expr::Attr(
857 sbb(Expr::Ident("Domain".to_string())),
858 "Types".to_string(),
859 )),
860 "TaskEvent".to_string(),
861 )),
862 "TaskMoved".to_string(),
863 )),
864 vec![
865 sb(Expr::Literal(Literal::Str("later".to_string()))),
866 sb(Expr::Literal(Literal::Int(3))),
867 ],
868 ));
869 match interpreter
870 .eval_expr(&multi_ctor_call)
871 .expect("direct qualified multi-field ctor call should run")
872 {
873 Value::Variant {
874 type_name,
875 variant,
876 fields,
877 } => {
878 assert_eq!(type_name, "TaskEvent");
879 assert_eq!(variant, "TaskMoved");
880 assert_eq!(
881 fields.as_ref(),
882 &[Value::Str("later".to_string()), Value::Int(3)]
883 );
884 }
885 other => panic!("expected variant, got {other:?}"),
886 }
887 }
888
889 #[test]
890 fn lowered_fn_bodies_classify_forward_calls_through_shared_ir() {
891 let interpreter = Interpreter::new();
892 let ctx = InterpreterLowerCtx::new(&interpreter);
893 let body = FnBody::from_expr(sb(Expr::FnCall(
894 sbb(Expr::Ident("first".to_string())),
895 vec![sb(Expr::Resolved(1)), sb(Expr::Resolved(0))],
896 )));
897 let lowered = lowered::lower_fn_body(&body, &ctx, "swap");
898
899 assert!(matches!(
900 lowered.expr(ExprId(0)),
901 LoweredExpr::ForwardCall {
902 target: LoweredDirectCallTarget::Function(name),
903 args,
904 ..
905 } if name == "first"
906 && matches!(
907 args.as_ref(),
908 [LoweredForwardArg::Slot(1), LoweredForwardArg::Slot(0)]
909 )
910 ));
911 }
912
913 #[test]
914 fn runtime_executes_forward_calls_without_evaling_arg_exprs() {
915 let mut interpreter = Interpreter::new();
916
917 let first = FnDef {
918 name: "first".to_string(),
919 line: 1,
920 params: vec![
921 ("x".to_string(), "Int".to_string()),
922 ("y".to_string(), "Int".to_string()),
923 ],
924 return_type: "Int".to_string(),
925 effects: vec![],
926 desc: None,
927 body: Rc::new(FnBody::from_expr(sb(Expr::Resolved(0)))),
928 resolution: Some(FnResolution {
929 local_slots: Rc::new(HashMap::from([
930 (String::from("x"), 0u16),
931 (String::from("y"), 1u16),
932 ])),
933 local_count: 2,
934 }),
935 };
936 interpreter
937 .exec_fn_def(&first)
938 .expect("first function should register");
939
940 let swap = FnDef {
941 name: "swap".to_string(),
942 line: 2,
943 params: vec![
944 ("a".to_string(), "Int".to_string()),
945 ("b".to_string(), "Int".to_string()),
946 ],
947 return_type: "Int".to_string(),
948 effects: vec![],
949 desc: None,
950 body: Rc::new(FnBody::from_expr(sb(Expr::FnCall(
951 sbb(Expr::Ident("first".to_string())),
952 vec![sb(Expr::Resolved(1)), sb(Expr::Resolved(0))],
953 )))),
954 resolution: Some(FnResolution {
955 local_slots: Rc::new(HashMap::from([
956 (String::from("a"), 0u16),
957 (String::from("b"), 1u16),
958 ])),
959 local_count: 2,
960 }),
961 };
962 interpreter
963 .exec_fn_def(&swap)
964 .expect("swap function should register");
965
966 let swap_call = sb(Expr::FnCall(
967 sbb(Expr::Ident("swap".to_string())),
968 vec![
969 sb(Expr::Literal(Literal::Int(3))),
970 sb(Expr::Literal(Literal::Int(7))),
971 ],
972 ));
973 assert_eq!(
974 interpreter
975 .eval_expr(&swap_call)
976 .expect("forward call should run"),
977 Value::Int(7)
978 );
979 }
980
981 #[test]
982 fn lowered_fn_bodies_classify_tail_calls_through_shared_ir() {
983 let interpreter = Interpreter::new();
984 let ctx = InterpreterLowerCtx::new(&interpreter);
985
986 let self_body = FnBody::from_expr(sb(Expr::TailCall(Box::new((
987 "loop".to_string(),
988 vec![sb(Expr::Literal(Literal::Int(1)))],
989 )))));
990 let lowered_self = lowered::lower_fn_body(&self_body, &ctx, "loop");
991 assert!(matches!(
992 lowered_self.expr(ExprId(1)),
993 LoweredExpr::TailCall {
994 target: LoweredTailCallTarget::SelfCall,
995 ..
996 }
997 ));
998
999 let known_body = FnBody::from_expr(sb(Expr::TailCall(Box::new((
1000 "other".to_string(),
1001 vec![sb(Expr::Literal(Literal::Int(2)))],
1002 )))));
1003 let lowered_known = lowered::lower_fn_body(&known_body, &ctx, "loop");
1004 assert!(matches!(
1005 lowered_known.expr(ExprId(1)),
1006 LoweredExpr::TailCall {
1007 target: LoweredTailCallTarget::KnownFunction(name),
1008 ..
1009 } if name == "other"
1010 ));
1011
1012 let unknown_body = FnBody::from_expr(sb(Expr::TailCall(Box::new((
1013 "Result.Ok".to_string(),
1014 vec![sb(Expr::Literal(Literal::Int(3)))],
1015 )))));
1016 let lowered_unknown = lowered::lower_fn_body(&unknown_body, &ctx, "loop");
1017 assert!(matches!(
1018 lowered_unknown.expr(ExprId(1)),
1019 LoweredExpr::TailCall {
1020 target: LoweredTailCallTarget::Unknown(name),
1021 ..
1022 } if name == "Result.Ok"
1023 ));
1024 }
1025}