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;
279pub(crate) mod lowered;
280mod ops;
281mod patterns;
282
283#[cfg(test)]
284mod memo_cache_tests {
285    use super::*;
286
287    #[test]
288    fn collision_bucket_is_exact_match_on_args() {
289        let mut cache = FnMemoCache::default();
290        cache.insert(1, vec![Value::Int(1)], Value::Int(10), 8);
291        cache.insert(1, vec![Value::Int(2)], Value::Int(20), 8);
292
293        assert_eq!(cache.get(1, &[Value::Int(1)]), Some(Value::Int(10)));
294        assert_eq!(cache.get(1, &[Value::Int(2)]), Some(Value::Int(20)));
295        assert_eq!(cache.get(1, &[Value::Int(3)]), None);
296    }
297
298    #[test]
299    fn lru_evicts_least_recently_used() {
300        let mut cache = FnMemoCache::default();
301        cache.insert(11, vec![Value::Int(1)], Value::Int(10), 2);
302        cache.insert(22, vec![Value::Int(2)], Value::Int(20), 2);
303
304        // Touch key=11 so key=22 becomes LRU.
305        assert_eq!(cache.get(11, &[Value::Int(1)]), Some(Value::Int(10)));
306        cache.insert(33, vec![Value::Int(3)], Value::Int(30), 2);
307
308        assert_eq!(cache.get(11, &[Value::Int(1)]), Some(Value::Int(10)));
309        assert_eq!(cache.get(22, &[Value::Int(2)]), None);
310        assert_eq!(cache.get(33, &[Value::Int(3)]), Some(Value::Int(30)));
311    }
312}