#![warn(missing_docs)]
use dashmap::DashMap;
use rustc_hash::FxHasher;
use std::any::TypeId;
use std::fmt::Debug;
use std::hash::{BuildHasherDefault, Hash};
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Revision(u64);
impl Revision {
pub const INITIAL: Self = Self(0);
#[must_use]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn next(self) -> Self {
Self(self.0 + 1)
}
#[must_use]
pub const fn as_u64(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct QueryId {
pub query_type: TypeId,
pub key_hash: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QueryState {
NotComputed,
InProgress,
Memoized(Revision),
Stale,
}
#[derive(Debug, Clone)]
pub enum QueryError {
Cycle(Vec<QueryId>),
Panic(String),
Cancelled,
}
impl std::fmt::Display for QueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Cycle(ids) => write!(f, "query cycle detected: {} queries involved", ids.len()),
Self::Panic(msg) => write!(f, "query panicked: {msg}"),
Self::Cancelled => write!(f, "query was cancelled"),
}
}
}
impl std::error::Error for QueryError {}
pub type QueryResult<T> = Result<T, QueryError>;
#[derive(Debug)]
pub struct MemoizedResult<V> {
pub value: V,
pub computed_at: Revision,
pub verified_at: Revision,
pub dependencies: Vec<QueryId>,
}
pub trait QueryKey: Clone + Eq + Hash + Debug + Send + Sync + 'static {}
impl<T: Clone + Eq + Hash + Debug + Send + Sync + 'static> QueryKey for T {}
pub trait QueryValue: Clone + Debug + Send + Sync + 'static {}
impl<T: Clone + Debug + Send + Sync + 'static> QueryValue for T {}
pub trait Query: Send + Sync + 'static {
type Key: QueryKey;
type Value: QueryValue;
fn compute(&self, db: &dyn QueryDatabase, key: &Self::Key) -> Self::Value;
fn name(&self) -> &'static str;
}
pub trait QueryDatabase: Send + Sync {
fn current_revision(&self) -> Revision;
fn increment_revision(&self) -> Revision;
fn mark_in_progress(&self, id: QueryId);
fn unmark_in_progress(&self, id: QueryId);
fn is_in_progress(&self, id: QueryId) -> bool;
fn record_dependency(&self, from: QueryId, to: QueryId);
}
pub struct QueryStorage<Q: Query> {
query: Q,
results: DashMap<Q::Key, MemoizedResult<Q::Value>, BuildHasherDefault<FxHasher>>,
}
impl<Q: Query> QueryStorage<Q> {
pub fn new(query: Q) -> Self {
Self {
query,
results: DashMap::with_hasher(BuildHasherDefault::default()),
}
}
pub fn get(&self, db: &dyn QueryDatabase, key: &Q::Key) -> QueryResult<Q::Value> {
if let Some(entry) = self.results.get(key) {
if entry.computed_at >= db.current_revision() {
tracing::trace!(query = Q::name(&self.query), "cache hit");
return Ok(entry.value.clone());
}
}
let query_id = self.make_query_id(key);
if db.is_in_progress(query_id) {
return Err(QueryError::Cycle(vec![query_id]));
}
db.mark_in_progress(query_id);
let value = self.query.compute(db, key);
db.unmark_in_progress(query_id);
let current_rev = db.current_revision();
self.results.insert(
key.clone(),
MemoizedResult {
value: value.clone(),
computed_at: current_rev,
verified_at: current_rev,
dependencies: Vec::new(),
},
);
tracing::trace!(query = Q::name(&self.query), "computed and cached");
Ok(value)
}
pub fn invalidate_all(&self) {
self.results.clear();
}
pub fn invalidate(&self, key: &Q::Key) {
self.results.remove(key);
}
fn make_query_id(&self, key: &Q::Key) -> QueryId {
use std::hash::Hasher;
let mut hasher = FxHasher::default();
key.hash(&mut hasher);
QueryId {
query_type: TypeId::of::<Q>(),
key_hash: hasher.finish(),
}
}
}
pub struct SimpleDatabase {
revision: AtomicU64,
in_progress: DashMap<QueryId, (), BuildHasherDefault<FxHasher>>,
dependencies: DashMap<QueryId, Vec<QueryId>, BuildHasherDefault<FxHasher>>,
}
impl SimpleDatabase {
#[must_use]
pub fn new() -> Self {
Self {
revision: AtomicU64::new(0),
in_progress: DashMap::with_hasher(BuildHasherDefault::default()),
dependencies: DashMap::with_hasher(BuildHasherDefault::default()),
}
}
}
impl Default for SimpleDatabase {
fn default() -> Self {
Self::new()
}
}
impl QueryDatabase for SimpleDatabase {
fn current_revision(&self) -> Revision {
Revision(self.revision.load(Ordering::Acquire))
}
fn increment_revision(&self) -> Revision {
Revision(self.revision.fetch_add(1, Ordering::AcqRel) + 1)
}
fn mark_in_progress(&self, id: QueryId) {
self.in_progress.insert(id, ());
}
fn unmark_in_progress(&self, id: QueryId) {
self.in_progress.remove(&id);
}
fn is_in_progress(&self, id: QueryId) -> bool {
self.in_progress.contains_key(&id)
}
fn record_dependency(&self, from: QueryId, to: QueryId) {
self.dependencies.entry(from).or_default().push(to);
}
}
pub struct QueryRuntime<DB> {
pub db: DB,
}
impl<DB: QueryDatabase> QueryRuntime<DB> {
pub fn new(db: DB) -> Self {
Self { db }
}
pub fn query<Q: Query>(
&self,
storage: &QueryStorage<Q>,
key: &Q::Key,
) -> QueryResult<Q::Value> {
storage.get(&self.db, key)
}
}
pub trait InputQuery: Query + Sized {
fn set(storage: &QueryStorage<Self>, key: Self::Key, value: Self::Value);
}
#[cfg(test)]
mod tests {
use super::*;
struct TestQuery;
impl Query for TestQuery {
type Key = u32;
type Value = u64;
fn compute(&self, _db: &dyn QueryDatabase, key: &u32) -> u64 {
(*key as u64) * 2
}
fn name(&self) -> &'static str {
"test_query"
}
}
#[test]
fn test_basic_query() {
let db = SimpleDatabase::new();
let storage = QueryStorage::new(TestQuery);
let runtime = QueryRuntime::new(db);
let result = runtime.query(&storage, &21).unwrap();
assert_eq!(result, 42);
let result2 = runtime.query(&storage, &21).unwrap();
assert_eq!(result2, 42);
}
#[test]
fn test_revision_tracking() {
let db = SimpleDatabase::new();
assert_eq!(db.current_revision(), Revision::INITIAL);
let rev1 = db.increment_revision();
assert_eq!(rev1, Revision::new(1));
let rev2 = db.increment_revision();
assert_eq!(rev2, Revision::new(2));
}
}