#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges) — not the application (#1694)"
)]
use std::collections::BTreeMap;
use crate::ids::CaptureName;
use crate::refgrammar::TimeExpr;
#[derive(Debug, Clone, PartialEq)]
pub enum Captured {
Scalar(String),
List(Vec<String>),
Body(serde_json::Value),
InstantMs {
lo: i64,
hi: i64,
},
}
#[derive(Debug, Clone, Default)]
pub struct VarStore {
values: BTreeMap<CaptureName, Captured>,
}
impl VarStore {
pub fn set(&mut self, name: CaptureName, value: Captured) {
self.values.insert(name, value);
}
#[must_use]
pub fn get(&self, name: &CaptureName) -> Option<&Captured> {
self.values.get(name)
}
#[must_use]
pub fn scalar(&self, name: &CaptureName) -> Option<&str> {
match self.values.get(name) {
Some(Captured::Scalar(s)) => Some(s),
_ => None,
}
}
#[must_use]
pub fn latest_instant_hi(&self) -> Option<i64> {
self.values
.values()
.filter_map(|c| match c {
Captured::InstantMs { hi, .. } => Some(*hi),
_ => None,
})
.max()
}
pub fn resolve_time(&self, expr: &TimeExpr) -> Result<i64, String> {
let instant = |name: &CaptureName| -> Result<(i64, i64), String> {
match self.values.get(name) {
Some(Captured::InstantMs { lo, hi }) => Ok((*lo, *hi)),
Some(_) => Err(format!("capture {name} is not a commit instant")),
None => Err(format!("capture {name} is not bound")),
}
};
match expr {
TimeExpr::Before(t) => instant(t)?
.0
.checked_sub(1)
.ok_or_else(|| "instant arithmetic underflow".to_owned()),
TimeExpr::After(t) => instant(t)?
.1
.checked_add(1)
.ok_or_else(|| "instant arithmetic overflow".to_owned()),
TimeExpr::Between(t1, t2) => {
let (a, b) = (instant(t1)?.1, instant(t2)?.0);
#[expect(
clippy::integer_division,
reason = "midpoint of a millisecond gap: the truncated half is \
deliberate, an instant is a whole millisecond"
)]
b.checked_sub(a)
.and_then(|d| a.checked_add(d / 2))
.ok_or_else(|| "instant arithmetic overflow".to_owned())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn temporal_law_d_is_fixed() {
let mut store = VarStore::default();
let t1 = CaptureName::parse("t1").unwrap();
let t2 = CaptureName::parse("t2").unwrap();
store.set(
t1.clone(),
Captured::InstantMs {
lo: 1_000,
hi: 1_000,
},
);
store.set(
t2.clone(),
Captured::InstantMs {
lo: 2_001,
hi: 2_001,
},
);
assert_eq!(
store.resolve_time(&TimeExpr::Before(t1.clone())).unwrap(),
999
);
assert_eq!(
store.resolve_time(&TimeExpr::After(t1.clone())).unwrap(),
1_001
);
assert_eq!(
store
.resolve_time(&TimeExpr::Between(t1.clone(), t2))
.unwrap(),
1_500 );
assert!(
store
.resolve_time(&TimeExpr::Before(CaptureName::parse("ghost").unwrap()))
.is_err()
);
}
}