1use crate::nan_value::{Arena, NanValue};
2use std::collections::{HashMap, HashSet};
3use std::path::{Path, PathBuf};
4use std::rc::Rc;
5
6use crate::ast::*;
7use crate::replay::{
8 EffectRecord, EffectReplayMode, EffectReplayState, JsonValue, RecordedOutcome,
9 SessionRecording, session_recording_to_string_pretty, value_to_json, values_to_json_lossy,
10};
11#[cfg(feature = "terminal")]
12use crate::services::terminal;
13use crate::services::{args, console, disk, env, http, http_server, random, tcp, time};
14use crate::source::{
15 canonicalize_path, find_module_file, parse_source, require_module_declaration,
16};
17use crate::types::{bool, byte, char, float, int, list, map, option, result, string, vector};
18pub use crate::value::{Env, EnvFrame, RuntimeError, Value, aver_display, aver_repr};
20use crate::value::{list_len, list_view};
21
22#[derive(Debug, Clone)]
23struct CallFrame {
24 name: Rc<String>,
25 effects: Rc<Vec<String>>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ExecutionMode {
30 Normal,
31 Record,
32 Replay,
33}
34
35const MEMO_CACHE_CAP_PER_FN: usize = 4096;
36
37#[derive(Debug, Clone)]
38struct MemoEntry {
39 id: u64,
40 args: Vec<Value>,
41 result: Value,
42}
43
44#[derive(Debug, Clone)]
45struct RecordingSink {
46 path: PathBuf,
47 request_id: String,
48 timestamp: String,
49 program_file: String,
50 module_root: String,
51 entry_fn: String,
52 input: JsonValue,
53}
54
55type MatchSiteKey = (usize, usize); #[derive(Debug, Clone)]
58struct VerifyMatchCoverageTracker {
59 target_fn: String,
60 expected_arms: std::collections::BTreeMap<MatchSiteKey, usize>,
61 visited_arms: HashMap<MatchSiteKey, HashSet<usize>>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct VerifyMatchCoverageMiss {
66 pub line: usize,
67 pub total_arms: usize,
68 pub missing_arms: Vec<usize>, }
70
71#[derive(Debug, Clone)]
72pub struct RecordingConfig {
73 pub path: PathBuf,
74 pub request_id: String,
75 pub timestamp: String,
76 pub program_file: String,
77 pub module_root: String,
78 pub entry_fn: String,
79 pub input: JsonValue,
80}
81
82#[derive(Debug, Default, Clone)]
84struct FnMemoCache {
85 buckets: HashMap<u64, Vec<MemoEntry>>,
87 positions: HashMap<u64, (u64, usize)>,
89 links: HashMap<u64, (Option<u64>, Option<u64>)>,
91 lru_head: Option<u64>,
92 lru_tail: Option<u64>,
93 next_id: u64,
94 len: usize,
95}
96
97impl FnMemoCache {
98 fn get(&mut self, hash: u64, args: &[Value]) -> Option<Value> {
99 let found = self
100 .buckets
101 .get_mut(&hash)
102 .and_then(|entries| entries.iter_mut().find(|entry| entry.args == args))
103 .map(|entry| (entry.id, entry.result.clone()));
104
105 if let Some((id, value)) = found {
106 self.touch(id);
107 Some(value)
108 } else {
109 None
110 }
111 }
112
113 fn insert(&mut self, hash: u64, args: Vec<Value>, result: Value, cap: usize) {
114 let update_hit = self
115 .buckets
116 .get_mut(&hash)
117 .and_then(|entries| entries.iter_mut().find(|entry| entry.args == args))
118 .map(|entry| {
119 entry.result = result.clone();
120 entry.id
121 });
122
123 if let Some(id) = update_hit {
124 self.touch(id);
125 return;
126 }
127
128 if self.len >= cap {
129 self.evict_lru();
130 }
131
132 let id = self.alloc_id();
133 let entry = MemoEntry { id, args, result };
134 let idx = self.buckets.entry(hash).or_default().len();
135 self.buckets.entry(hash).or_default().push(entry);
136 self.positions.insert(id, (hash, idx));
137 self.append_tail(id);
138 self.len += 1;
139 }
140
141 fn alloc_id(&mut self) -> u64 {
142 let id = self.next_id;
143 self.next_id = self.next_id.wrapping_add(1);
144 id
145 }
146
147 fn evict_lru(&mut self) {
148 if let Some(id) = self.lru_head {
149 self.remove_entry(id);
150 }
151 }
152
153 fn touch(&mut self, id: u64) {
154 if self.lru_tail == Some(id) {
155 return;
156 }
157 self.detach(id);
158 self.append_tail(id);
159 }
160
161 fn append_tail(&mut self, id: u64) {
162 let prev = self.lru_tail;
163 self.links.insert(id, (prev, None));
164 if let Some(tail) = prev {
165 if let Some((_, next)) = self.links.get_mut(&tail) {
166 *next = Some(id);
167 }
168 } else {
169 self.lru_head = Some(id);
170 }
171 self.lru_tail = Some(id);
172 }
173
174 fn detach(&mut self, id: u64) {
175 let Some((prev, next)) = self.links.get(&id).copied() else {
176 return;
177 };
178
179 if let Some(p) = prev {
180 if let Some((_, p_next)) = self.links.get_mut(&p) {
181 *p_next = next;
182 }
183 } else {
184 self.lru_head = next;
185 }
186
187 if let Some(n) = next {
188 if let Some((n_prev, _)) = self.links.get_mut(&n) {
189 *n_prev = prev;
190 }
191 } else {
192 self.lru_tail = prev;
193 }
194
195 if let Some(link) = self.links.get_mut(&id) {
196 *link = (None, None);
197 }
198 }
199
200 fn remove_entry(&mut self, id: u64) {
201 let Some((hash, idx)) = self.positions.remove(&id) else {
202 return;
203 };
204 self.detach(id);
205 self.links.remove(&id);
206
207 let mut remove_bucket = false;
208 if let Some(entries) = self.buckets.get_mut(&hash) {
209 entries.swap_remove(idx);
210 if idx < entries.len() {
211 let moved_id = entries[idx].id;
212 self.positions.insert(moved_id, (hash, idx));
213 }
214 remove_bucket = entries.is_empty();
215 }
216 if remove_bucket {
217 self.buckets.remove(&hash);
218 }
219 self.len = self.len.saturating_sub(1);
220 }
221
222 fn get_nv_as_value(&mut self, hash: u64, nv_args: &[NanValue], arena: &Arena) -> Option<Value> {
225 let args: Vec<Value> = nv_args.iter().map(|nv| nv.to_value(arena)).collect();
226 self.get(hash, &args)
227 }
228
229 fn insert_nv(
231 &mut self,
232 hash: u64,
233 nv_args: Vec<NanValue>,
234 nv_result: NanValue,
235 arena: &Arena,
236 cap: usize,
237 ) {
238 let args: Vec<Value> = nv_args.iter().map(|nv| nv.to_value(arena)).collect();
239 let result = nv_result.to_value(arena);
240 self.insert(hash, args, result, cap);
241 }
242}
243
244pub struct Interpreter {
245 pub env: Env,
246 env_base: usize,
250 pub arena: Arena,
252 module_cache: HashMap<String, Value>,
253 record_schemas: HashMap<String, Vec<String>>,
256 call_stack: Vec<CallFrame>,
257 active_local_slots: Option<Rc<HashMap<String, u16>>>,
260 memo_fns: HashSet<String>,
262 memo_cache: HashMap<String, FnMemoCache>,
264 replay_state: EffectReplayState,
265 recording_sink: Option<RecordingSink>,
266 verify_match_coverage: Option<VerifyMatchCoverageTracker>,
267 runtime_policy: Option<crate::config::ProjectConfig>,
269 cli_args: Vec<String>,
271}
272
273mod api;
274mod builtins;
275mod core;
276mod effects;
277mod eval;
278mod exec;
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 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}