1use std::collections::HashMap;
13use std::sync::{Arc, Mutex};
14use rune_ast::Noun;
15
16#[derive(Clone)]
20pub struct CompiledArtifact {
21 pub source_hash: u64,
23 pub formula: Noun,
25 pub via_trident: bool,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
31pub enum Tier {
32 Interpret,
34 Compiling,
36 Compiled,
38}
39
40pub struct CallCounter {
42 counts: HashMap<u64, u64>,
43 threshold: u64,
44}
45
46impl CallCounter {
47 pub fn new(threshold: u64) -> Self {
48 CallCounter { counts: HashMap::new(), threshold }
49 }
50
51 pub fn record(&mut self, formula_hash: u64) -> bool {
53 let count = self.counts.entry(formula_hash).or_insert(0);
54 *count += 1;
55 *count == self.threshold
56 }
57
58 pub fn count(&self, formula_hash: u64) -> u64 {
59 self.counts.get(&formula_hash).copied().unwrap_or(0)
60 }
61}
62
63pub struct ArtifactCache {
65 inner: Arc<Mutex<HashMap<u64, CompiledArtifact>>>,
66}
67
68impl ArtifactCache {
69 pub fn new() -> Self {
70 ArtifactCache { inner: Arc::new(Mutex::new(HashMap::new())) }
71 }
72
73 pub fn get(&self, source_hash: u64) -> Option<CompiledArtifact> {
74 self.inner.lock().unwrap().get(&source_hash).cloned()
75 }
76
77 pub fn store(&self, artifact: CompiledArtifact) {
78 self.inner.lock().unwrap().insert(artifact.source_hash, artifact.clone());
79 }
80}
81
82impl Default for ArtifactCache {
83 fn default() -> Self { Self::new() }
84}
85
86pub fn compile_formula(formula: &Noun, source_hash: u64) -> CompiledArtifact {
89 CompiledArtifact {
93 source_hash,
94 formula: formula.clone(),
95 via_trident: false,
96 }
97}
98
99pub fn try_trident_compile(formula: &Noun, source_hash: u64) -> Option<CompiledArtifact> {
103 if contains_dynamic(formula) {
105 return None;
106 }
107 Some(CompiledArtifact {
110 source_hash,
111 formula: formula.clone(),
112 via_trident: false, })
114}
115
116fn contains_dynamic(noun: &Noun) -> bool {
119 match noun {
120 Noun::Atom(_) => false,
121 Noun::Cell(h, t) => {
122 if let Noun::Atom(op) = h.as_ref() {
124 if *op == 16 || *op == 17 { return true; }
125 }
126 contains_dynamic(h) || contains_dynamic(t)
127 }
128 }
129}
130
131pub fn formula_hash(noun: &Noun) -> u64 {
133 match noun {
134 Noun::Atom(n) => n.wrapping_mul(0x517cc1b727220a95),
135 Noun::Cell(h, t) => {
136 let hh = formula_hash(h);
137 let th = formula_hash(t);
138 hh.wrapping_mul(31).wrapping_add(th)
139 }
140 }
141}
142
143struct ResultCache {
146 inner: HashMap<(u64, u64), Noun>,
147 max_entries: usize,
148}
149
150impl ResultCache {
151 fn new(max_entries: usize) -> Self {
152 ResultCache { inner: HashMap::new(), max_entries }
153 }
154
155 fn get(&self, formula_hash: u64, subject_hash: u64) -> Option<&Noun> {
156 self.inner.get(&(formula_hash, subject_hash))
157 }
158
159 fn store(&mut self, formula_hash: u64, subject_hash: u64, result: Noun) {
160 if self.inner.len() < self.max_entries {
161 self.inner.insert((formula_hash, subject_hash), result);
162 }
163 }
166
167 fn len(&self) -> usize { self.inner.len() }
168}
169
170pub struct TierStats {
172 pub result_cache_entries: usize,
173}
174
175pub struct TierRouter {
177 counter: CallCounter,
178 cache: ArtifactCache,
179 results: ResultCache,
180 hot_threshold: u64,
181}
182
183impl TierRouter {
184 pub fn new(compile_threshold: u64) -> Self {
185 TierRouter {
186 counter: CallCounter::new(compile_threshold),
187 cache: ArtifactCache::new(),
188 results: ResultCache::new(1024),
189 hot_threshold: compile_threshold,
190 }
191 }
192
193 pub fn eval(&mut self, subject: &Noun, formula: &Noun) -> Result<Noun, rune_interp::InterpError> {
195 let fhash = formula_hash(formula);
196 let is_pure = !contains_dynamic(formula);
197
198 if is_pure {
200 let shash = formula_hash(subject);
201 if let Some(cached) = self.results.get(fhash, shash) {
202 return Ok(cached.clone());
203 }
204 }
205
206 let formula_to_run = if let Some(artifact) = self.cache.get(fhash) {
208 artifact.formula
209 } else {
210 formula.clone()
211 };
212
213 let result = rune_interp::eval(subject, &formula_to_run)?;
215
216 if self.counter.record(fhash) {
218 if let Some(artifact) = try_trident_compile(formula, fhash) {
219 self.cache.store(artifact);
220 }
221 }
222
223 if is_pure && self.counter.count(fhash) >= self.hot_threshold {
225 let shash = formula_hash(subject);
226 self.results.store(fhash, shash, result.clone());
227 }
228
229 Ok(result)
230 }
231
232 pub fn stats(&self) -> TierStats {
234 TierStats {
235 result_cache_entries: self.results.len(),
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use rune_ast::Noun;
244
245 #[test]
246 fn call_counter_threshold() {
247 let mut c = CallCounter::new(3);
248 assert!(!c.record(42));
249 assert!(!c.record(42));
250 assert!(c.record(42)); assert!(!c.record(42)); }
253
254 #[test]
255 fn pure_formula_detection() {
256 let pure = Noun::cell(Noun::Atom(1), Noun::Atom(42));
258 assert!(!contains_dynamic(&pure));
259
260 let dynamic = Noun::cell(Noun::Atom(16), Noun::cell(Noun::Atom(0), Noun::Atom(0)));
262 assert!(contains_dynamic(&dynamic));
263 }
264
265 #[test]
266 fn tier_router_interprets() {
267 let mut router = TierRouter::new(100);
268 let subj = Noun::Atom(0);
269 let formula = Noun::cell(Noun::Atom(1), Noun::Atom(42));
270 let result = router.eval(&subj, &formula).unwrap();
271 assert_eq!(result, Noun::Atom(42));
272 }
273
274 #[test]
275 fn result_cache_hit_after_threshold() {
276 let mut router = TierRouter::new(3);
277 let subj = Noun::Atom(0);
278 let formula = Noun::cell(Noun::Atom(1), Noun::Atom(42));
280
281 for _ in 0..3 {
283 let r = router.eval(&subj, &formula).unwrap();
284 assert_eq!(r, Noun::Atom(42));
285 }
286
287 let r = router.eval(&subj, &formula).unwrap();
289 assert_eq!(r, Noun::Atom(42));
290 assert_eq!(router.stats().result_cache_entries, 1);
291 }
292
293 #[test]
294 fn dynamic_formula_not_memoized() {
295 let mut router = TierRouter::new(2);
296 let subj = Noun::Atom(0);
297 let hint_meta = Noun::cell(Noun::cell(Noun::Atom(1), Noun::Atom(0)), Noun::cell(Noun::Atom(1), Noun::Atom(0)));
299 let body = Noun::cell(Noun::Atom(1), Noun::Atom(42));
300 let dynamic = Noun::cell(Noun::Atom(16), Noun::cell(hint_meta, body));
301
302 for _ in 0..5 {
303 router.eval(&subj, &dynamic).unwrap();
304 }
305 assert_eq!(router.stats().result_cache_entries, 0);
307 }
308
309 #[test]
310 fn different_subjects_cached_separately() {
311 let mut router = TierRouter::new(2);
312 let formula = Noun::cell(Noun::Atom(1), Noun::Atom(99));
313
314 for _ in 0..3 {
316 router.eval(&Noun::Atom(0), &formula).unwrap();
317 router.eval(&Noun::Atom(1), &formula).unwrap();
318 }
319
320 assert_eq!(router.stats().result_cache_entries, 2);
322 }
323}