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::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};
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    /// Source line of the current call expression (set before dispatch, used for
272    /// effect recording and error decoration). 0 = unknown.
273    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        // Touch key=11 so key=22 becomes LRU.
309        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    /// Shorthand: wrap an Expr in Spanned::bare (line=0).
330    fn sb(expr: Expr) -> Spanned<Expr> {
331        Spanned::bare(expr)
332    }
333
334    /// Shorthand: wrap an Expr in Box<Spanned::bare(...)>.
335    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}