use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use rune_ast::Noun;
#[derive(Clone)]
pub struct CompiledArtifact {
pub source_hash: u64,
pub formula: Noun,
pub via_trident: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Tier {
Interpret,
Compiling,
Compiled,
}
pub struct CallCounter {
counts: HashMap<u64, u64>,
threshold: u64,
}
impl CallCounter {
pub fn new(threshold: u64) -> Self {
CallCounter { counts: HashMap::new(), threshold }
}
pub fn record(&mut self, formula_hash: u64) -> bool {
let count = self.counts.entry(formula_hash).or_insert(0);
*count += 1;
*count == self.threshold
}
pub fn count(&self, formula_hash: u64) -> u64 {
self.counts.get(&formula_hash).copied().unwrap_or(0)
}
}
pub struct ArtifactCache {
inner: Arc<Mutex<HashMap<u64, CompiledArtifact>>>,
}
impl ArtifactCache {
pub fn new() -> Self {
ArtifactCache { inner: Arc::new(Mutex::new(HashMap::new())) }
}
pub fn get(&self, source_hash: u64) -> Option<CompiledArtifact> {
self.inner.lock().unwrap().get(&source_hash).cloned()
}
pub fn store(&self, artifact: CompiledArtifact) {
self.inner.lock().unwrap().insert(artifact.source_hash, artifact.clone());
}
}
impl Default for ArtifactCache {
fn default() -> Self { Self::new() }
}
pub fn compile_formula(formula: &Noun, source_hash: u64) -> CompiledArtifact {
CompiledArtifact {
source_hash,
formula: formula.clone(),
via_trident: false,
}
}
pub fn try_trident_compile(formula: &Noun, source_hash: u64) -> Option<CompiledArtifact> {
if contains_dynamic(formula) {
return None;
}
Some(CompiledArtifact {
source_hash,
formula: formula.clone(),
via_trident: false, })
}
fn contains_dynamic(noun: &Noun) -> bool {
match noun {
Noun::Atom(_) => false,
Noun::Cell(h, t) => {
if let Noun::Atom(op) = h.as_ref() {
if *op == 16 || *op == 17 { return true; }
}
contains_dynamic(h) || contains_dynamic(t)
}
}
}
pub fn formula_hash(noun: &Noun) -> u64 {
match noun {
Noun::Atom(n) => n.wrapping_mul(0x517cc1b727220a95),
Noun::Cell(h, t) => {
let hh = formula_hash(h);
let th = formula_hash(t);
hh.wrapping_mul(31).wrapping_add(th)
}
}
}
struct ResultCache {
inner: HashMap<(u64, u64), Noun>,
max_entries: usize,
}
impl ResultCache {
fn new(max_entries: usize) -> Self {
ResultCache { inner: HashMap::new(), max_entries }
}
fn get(&self, formula_hash: u64, subject_hash: u64) -> Option<&Noun> {
self.inner.get(&(formula_hash, subject_hash))
}
fn store(&mut self, formula_hash: u64, subject_hash: u64, result: Noun) {
if self.inner.len() < self.max_entries {
self.inner.insert((formula_hash, subject_hash), result);
}
}
fn len(&self) -> usize { self.inner.len() }
}
pub struct TierStats {
pub result_cache_entries: usize,
}
pub struct TierRouter {
counter: CallCounter,
cache: ArtifactCache,
results: ResultCache,
hot_threshold: u64,
}
impl TierRouter {
pub fn new(compile_threshold: u64) -> Self {
TierRouter {
counter: CallCounter::new(compile_threshold),
cache: ArtifactCache::new(),
results: ResultCache::new(1024),
hot_threshold: compile_threshold,
}
}
pub fn eval(&mut self, subject: &Noun, formula: &Noun) -> Result<Noun, rune_interp::InterpError> {
let fhash = formula_hash(formula);
let is_pure = !contains_dynamic(formula);
if is_pure {
let shash = formula_hash(subject);
if let Some(cached) = self.results.get(fhash, shash) {
return Ok(cached.clone());
}
}
let formula_to_run = if let Some(artifact) = self.cache.get(fhash) {
artifact.formula
} else {
formula.clone()
};
let result = rune_interp::eval(subject, &formula_to_run)?;
if self.counter.record(fhash) {
if let Some(artifact) = try_trident_compile(formula, fhash) {
self.cache.store(artifact);
}
}
if is_pure && self.counter.count(fhash) >= self.hot_threshold {
let shash = formula_hash(subject);
self.results.store(fhash, shash, result.clone());
}
Ok(result)
}
pub fn stats(&self) -> TierStats {
TierStats {
result_cache_entries: self.results.len(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rune_ast::Noun;
#[test]
fn call_counter_threshold() {
let mut c = CallCounter::new(3);
assert!(!c.record(42));
assert!(!c.record(42));
assert!(c.record(42)); assert!(!c.record(42)); }
#[test]
fn pure_formula_detection() {
let pure = Noun::cell(Noun::Atom(1), Noun::Atom(42));
assert!(!contains_dynamic(&pure));
let dynamic = Noun::cell(Noun::Atom(16), Noun::cell(Noun::Atom(0), Noun::Atom(0)));
assert!(contains_dynamic(&dynamic));
}
#[test]
fn tier_router_interprets() {
let mut router = TierRouter::new(100);
let subj = Noun::Atom(0);
let formula = Noun::cell(Noun::Atom(1), Noun::Atom(42));
let result = router.eval(&subj, &formula).unwrap();
assert_eq!(result, Noun::Atom(42));
}
#[test]
fn result_cache_hit_after_threshold() {
let mut router = TierRouter::new(3);
let subj = Noun::Atom(0);
let formula = Noun::cell(Noun::Atom(1), Noun::Atom(42));
for _ in 0..3 {
let r = router.eval(&subj, &formula).unwrap();
assert_eq!(r, Noun::Atom(42));
}
let r = router.eval(&subj, &formula).unwrap();
assert_eq!(r, Noun::Atom(42));
assert_eq!(router.stats().result_cache_entries, 1);
}
#[test]
fn dynamic_formula_not_memoized() {
let mut router = TierRouter::new(2);
let subj = Noun::Atom(0);
let hint_meta = Noun::cell(Noun::cell(Noun::Atom(1), Noun::Atom(0)), Noun::cell(Noun::Atom(1), Noun::Atom(0)));
let body = Noun::cell(Noun::Atom(1), Noun::Atom(42));
let dynamic = Noun::cell(Noun::Atom(16), Noun::cell(hint_meta, body));
for _ in 0..5 {
router.eval(&subj, &dynamic).unwrap();
}
assert_eq!(router.stats().result_cache_entries, 0);
}
#[test]
fn different_subjects_cached_separately() {
let mut router = TierRouter::new(2);
let formula = Noun::cell(Noun::Atom(1), Noun::Atom(99));
for _ in 0..3 {
router.eval(&Noun::Atom(0), &formula).unwrap();
router.eval(&Noun::Atom(1), &formula).unwrap();
}
assert_eq!(router.stats().result_cache_entries, 2);
}
}