Skip to main content

aver/interpreter/
mod.rs

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};
18// Re-export value types so existing `use aver::interpreter::Value` imports keep working.
19pub 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); // (line, arm_count)
56
57#[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>, // 0-based arm indices
69}
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/// Per-function memo cache with collision-safe buckets and true LRU eviction.
83#[derive(Debug, Default, Clone)]
84struct FnMemoCache {
85    /// Primary index: hash(args) -> bucket of potentially colliding entries.
86    buckets: HashMap<u64, Vec<MemoEntry>>,
87    /// Entry id -> (bucket hash, index in bucket vec).
88    positions: HashMap<u64, (u64, usize)>,
89    /// LRU links: entry id -> (prev, next).
90    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    /// NanValue-native get — converts NanValue args to Value for comparison,
223    /// returns cached Value. Arena conversion done by caller.
224    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    /// NanValue-native insert — stores args and result as Value (bridge).
230    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    /// Base index into `env` for the current function's frames.
247    /// lookup_ref sees env[0] (global) + env[env_base..] (current fn).
248    /// Caller frames in env[1..env_base] are invisible.
249    env_base: usize,
250    /// Arena for NaN-boxed value storage.
251    pub arena: Arena,
252    module_cache: HashMap<String, Value>,
253    /// Record field order schemas by type name (used to validate and
254    /// canonicalize `RecordCreate` runtime values).
255    record_schemas: HashMap<String, Vec<String>>,
256    call_stack: Vec<CallFrame>,
257    /// Active slot mapping for resolved function bodies.
258    /// Set when entering a resolved fn, cleared on exit.
259    active_local_slots: Option<Rc<HashMap<String, u16>>>,
260    /// Names of pure recursive functions eligible for auto-memoization.
261    memo_fns: HashSet<String>,
262    /// Per-function memo cache with collision-safe entries and LRU eviction.
263    memo_cache: HashMap<String, FnMemoCache>,
264    replay_state: EffectReplayState,
265    recording_sink: Option<RecordingSink>,
266    verify_match_coverage: Option<VerifyMatchCoverageTracker>,
267    /// Runtime policy from `aver.toml` — constrains Http hosts, Disk paths, etc.
268    runtime_policy: Option<crate::config::ProjectConfig>,
269    /// Command-line arguments passed to the Aver program (available via `Args.get()`).
270    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        // Touch key=11 so key=22 becomes LRU.
306        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}