use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use vj_abi::Region;
use crate::sched::{ProcessId, RegionCallback, SchedulerCtx, SimTick};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum ReportKind {
DeferredImmediate,
ProceduralConcurrentAssertion,
PropertyPassFail,
SequenceCoverMatch,
CheckerAction,
GlobalClockDelayed,
Violation,
DeferredCall,
}
pub type Writer = fn(&[u8]);
type Key = (ReportKind, Option<ProcessId>, u64);
thread_local! {
static PENDING: RefCell<HashMap<Key, (u64, Vec<u8>)>> = RefCell::new(HashMap::new());
static GENERATION: Cell<u64> = const { Cell::new(0) };
static WRITERS: RefCell<HashMap<ReportKind, Writer>> = RefCell::new(HashMap::new());
static AWAITING: RefCell<Vec<(Key, u64, SimTick)>> = const { RefCell::new(Vec::new()) };
static SITES: RefCell<HashMap<Key, u32>> = RefCell::new(HashMap::new());
}
pub fn set_writer(kind: ReportKind, f: Writer) {
WRITERS.with(|w| w.borrow_mut().insert(kind, f));
}
pub fn report(kind: ReportKind, site: u64, region: Region, text: Vec<u8>) {
report_for_site(kind, site, region, text, None);
}
pub fn report_for_site(
kind: ReportKind,
site: u64,
region: Region,
text: Vec<u8>,
control_site: Option<u32>,
) {
let key = (kind, crate::sched::current_process(), site);
let generation = GENERATION.with(|g| {
g.set(g.get() + 1);
g.get()
});
PENDING.with(|p| p.borrow_mut().insert(key, (generation, text)));
tag_site(key, control_site);
crate::sched::with_current_ops(|ops| {
ops.register_region_callback(region, ops.now(), Box::new(Mature { key, generation }));
});
}
pub fn report_at_global_tick(text: Vec<u8>) {
crate::sched::with_current_ops(|ops| {
hold_for_global_tick(text, ops.now().ticks);
});
}
fn hold_for_global_tick(text: Vec<u8>, captured: SimTick) {
let generation = GENERATION.with(|g| {
g.set(g.get() + 1);
g.get()
});
let key = (
ReportKind::GlobalClockDelayed,
crate::sched::current_process(),
generation,
);
PENDING.with(|p| p.borrow_mut().insert(key, (generation, text)));
AWAITING.with(|a| a.borrow_mut().push((key, generation, captured)));
}
pub fn report_independent(kind: ReportKind, region: Region, text: Vec<u8>) {
report_independent_for_site(kind, region, text, None);
}
pub fn report_independent_for_site(
kind: ReportKind,
region: Region,
text: Vec<u8>,
control_site: Option<u32>,
) {
let generation = GENERATION.with(|g| {
g.set(g.get() + 1);
g.get()
});
let key = (kind, crate::sched::current_process(), generation);
PENDING.with(|p| p.borrow_mut().insert(key, (generation, text)));
tag_site(key, control_site);
crate::sched::with_current_ops(|ops| {
ops.register_region_callback(region, ops.now(), Box::new(Mature { key, generation }));
});
}
fn tag_site(key: Key, control_site: Option<u32>) {
match control_site {
Some(site) => SITES.with(|s| {
s.borrow_mut().insert(key, site);
}),
None => SITES.with(|s| {
s.borrow_mut().remove(&key);
}),
}
}
fn prune_sites() {
SITES.with(|s| {
let mut s = s.borrow_mut();
if !s.is_empty() {
PENDING.with(|p| {
let p = p.borrow();
s.retain(|k, _| p.contains_key(k));
});
}
});
}
pub fn flush_sites(sites: &[u32]) {
let doomed: Vec<Key> = SITES.with(|s| {
s.borrow()
.iter()
.filter(|(_, site)| sites.contains(site))
.map(|(k, _)| *k)
.collect()
});
PENDING.with(|p| {
let mut p = p.borrow_mut();
for k in &doomed {
p.remove(k);
}
});
prune_sites();
}
pub fn flush_process_site(proc: ProcessId, site: u32) {
let doomed: Vec<Key> = SITES.with(|s| {
s.borrow()
.iter()
.filter(|(k, v)| **v == site && k.1 == Some(proc))
.map(|(k, _)| *k)
.collect()
});
PENDING.with(|p| {
let mut p = p.borrow_mut();
for k in &doomed {
p.remove(k);
}
});
prune_sites();
}
fn take_due(now: SimTick) -> Vec<(Key, u64)> {
AWAITING.with(|a| {
let mut a = a.borrow_mut();
let mut due = Vec::new();
a.retain(|&(key, generation, captured)| {
if captured < now {
due.push((key, generation));
false
} else {
true
}
});
due
})
}
pub fn global_tick(region: Region) {
crate::sched::with_current_ops(|ops| {
for (key, generation) in take_due(ops.now().ticks) {
ops.register_region_callback(region, ops.now(), Box::new(Mature { key, generation }));
}
});
}
pub fn flush(proc: ProcessId) {
PENDING.with(|p| {
p.borrow_mut().retain(|k, _| {
k.1 != Some(proc)
|| matches!(k.0, ReportKind::GlobalClockDelayed | ReportKind::Violation)
})
});
prune_sites();
}
pub fn flush_violations(proc: ProcessId) {
PENDING.with(|p| {
p.borrow_mut()
.retain(|k, _| k.1 != Some(proc) || k.0 != ReportKind::Violation)
});
prune_sites();
}
#[cfg(test)]
pub(crate) fn pending_len() -> usize {
PENDING.with(|p| p.borrow().len())
}
pub fn call_payload(thunk: usize, handle: usize, args: &[u8]) -> Vec<u8> {
let mut p = Vec::with_capacity(16 + args.len());
p.extend_from_slice(&(thunk as u64).to_le_bytes());
p.extend_from_slice(&(handle as u64).to_le_bytes());
p.extend_from_slice(args);
p
}
fn run_call(payload: &[u8]) {
type Thunk = unsafe extern "C" fn(usize, *const u128);
if payload.len() < 16 {
return;
}
let word = |i: usize| u64::from_le_bytes(payload[i..i + 8].try_into().unwrap());
let (thunk, handle) = (word(0) as usize, word(8) as usize);
let args = &payload[16..];
let mut buf = vec![0u128; args.len().div_ceil(16).max(1)];
unsafe {
std::ptr::copy_nonoverlapping(args.as_ptr(), buf.as_mut_ptr().cast::<u8>(), args.len());
}
unsafe {
let f: Thunk = std::mem::transmute::<usize, Thunk>(thunk);
f(handle, buf.as_ptr());
}
}
struct Mature {
key: Key,
generation: u64,
}
impl RegionCallback for Mature {
fn invoke(&mut self, _ctx: &dyn SchedulerCtx) {
let text = PENDING.with(|p| {
let mut p = p.borrow_mut();
match p.get(&self.key) {
Some((g, _)) if *g == self.generation => p.remove(&self.key).map(|(_, t)| t),
_ => None,
}
});
if let Some(text) = text {
SITES.with(|s| {
s.borrow_mut().remove(&self.key);
});
let kind = self.key.0;
if kind == ReportKind::DeferredCall {
run_call(&text);
return;
}
let writer = WRITERS.with(|w| w.borrow().get(&kind).copied());
if let Some(w) = writer {
w(&text);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reset_thread_locals() {
PENDING.with(|p| p.borrow_mut().clear());
GENERATION.with(|g| g.set(0));
WRITERS.with(|w| w.borrow_mut().clear());
AWAITING.with(|a| a.borrow_mut().clear());
SITES.with(|s| s.borrow_mut().clear());
}
thread_local! {
static CAPTURED: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
fn capture_writer(bytes: &[u8]) {
CAPTURED.with(|c| c.borrow_mut().extend_from_slice(bytes));
}
#[test]
fn maturation_queue_generic_over_report_kind_still_separates_by_key() {
reset_thread_locals();
CAPTURED.with(|c| c.borrow_mut().clear());
set_writer(ReportKind::DeferredImmediate, capture_writer);
set_writer(ReportKind::ProceduralConcurrentAssertion, capture_writer);
let key_a = (ReportKind::DeferredImmediate, None, 42u64);
let key_b = (ReportKind::ProceduralConcurrentAssertion, None, 42u64);
let gen_a = GENERATION.with(|g| {
g.set(g.get() + 1);
g.get()
});
PENDING.with(|p| {
p.borrow_mut()
.insert(key_a, (gen_a, b"deferred-immediate".to_vec()))
});
let gen_b = GENERATION.with(|g| {
g.set(g.get() + 1);
g.get()
});
PENDING.with(|p| {
p.borrow_mut()
.insert(key_b, (gen_b, b"procedural-concurrent-assertion".to_vec()))
});
let mut mature_a = Mature {
key: key_a,
generation: gen_a,
};
struct NoopCtx;
impl SchedulerCtx for NoopCtx {
fn now(&self) -> crate::sched::SchedTime {
crate::sched::SchedTime { ticks: 0 }
}
fn read(&self, _cell: crate::state::StateCellId) -> crate::state::FourStateValue {
panic!("not used by this test")
}
fn rearm(&self, _region: Region, _cb: Box<dyn RegionCallback>) {
panic!("not used by this test")
}
fn take_monitor_changed(&self) -> bool {
panic!("not used by this test")
}
}
mature_a.invoke(&NoopCtx);
assert_eq!(
CAPTURED.with(|c| c.borrow().clone()),
b"deferred-immediate",
"kind A's own write must have fired"
);
assert!(
PENDING.with(|p| p.borrow().contains_key(&key_b)),
"kind B's pending entry at the identical (proc, site) must survive kind A's own \
maturation untouched -- the key's kind component is what keeps them disjoint"
);
CAPTURED.with(|c| c.borrow_mut().clear());
let mut mature_b = Mature {
key: key_b,
generation: gen_b,
};
mature_b.invoke(&NoopCtx);
assert_eq!(
CAPTURED.with(|c| c.borrow().clone()),
b"procedural-concurrent-assertion"
);
assert!(!PENDING.with(|p| p.borrow().contains_key(&key_b)));
reset_thread_locals();
}
#[test]
fn flush_discards_every_kind_for_the_flushed_process() {
reset_thread_locals();
let proc: ProcessId = 7;
PENDING.with(|p| {
let mut p = p.borrow_mut();
p.insert(
(ReportKind::DeferredImmediate, Some(proc), 1),
(1, b"a".to_vec()),
);
p.insert(
(ReportKind::CheckerAction, Some(proc), 2),
(2, b"b".to_vec()),
);
p.insert(
(ReportKind::DeferredImmediate, Some(99), 3),
(3, b"other-proc".to_vec()),
);
});
flush(proc);
PENDING.with(|p| {
let p = p.borrow();
assert!(!p.contains_key(&(ReportKind::DeferredImmediate, Some(proc), 1)));
assert!(!p.contains_key(&(ReportKind::CheckerAction, Some(proc), 2)));
assert!(
p.contains_key(&(ReportKind::DeferredImmediate, Some(99), 3)),
"a different process's pending report must survive this flush"
);
});
reset_thread_locals();
}
#[test]
fn maturing_a_kind_with_no_registered_writer_does_not_panic() {
reset_thread_locals();
let key = (ReportKind::SequenceCoverMatch, None, 5u64);
let generation = GENERATION.with(|g| {
g.set(g.get() + 1);
g.get()
});
PENDING.with(|p| p.borrow_mut().insert(key, (generation, b"x".to_vec())));
let mut mature = Mature { key, generation };
struct NoopCtx;
impl SchedulerCtx for NoopCtx {
fn now(&self) -> crate::sched::SchedTime {
crate::sched::SchedTime { ticks: 0 }
}
fn read(&self, _cell: crate::state::StateCellId) -> crate::state::FourStateValue {
panic!("not used by this test")
}
fn rearm(&self, _region: Region, _cb: Box<dyn RegionCallback>) {
panic!("not used by this test")
}
fn take_monitor_changed(&self) -> bool {
panic!("not used by this test")
}
}
mature.invoke(&NoopCtx);
assert!(
!PENDING.with(|p| p.borrow().contains_key(&key)),
"the entry is still consumed (removed) even with no writer registered"
);
reset_thread_locals();
}
#[test]
fn a_stale_generation_mature_callback_does_not_fire() {
reset_thread_locals();
let key = (ReportKind::PropertyPassFail, None, 9u64);
let stale_generation = GENERATION.with(|g| {
g.set(g.get() + 1);
g.get()
});
PENDING.with(|p| {
p.borrow_mut()
.insert(key, (stale_generation, b"old".to_vec()))
});
let fresh_generation = GENERATION.with(|g| {
g.set(g.get() + 1);
g.get()
});
PENDING.with(|p| {
p.borrow_mut()
.insert(key, (fresh_generation, b"new".to_vec()))
});
let mut stale = Mature {
key,
generation: stale_generation,
};
struct NoopCtx;
impl SchedulerCtx for NoopCtx {
fn now(&self) -> crate::sched::SchedTime {
crate::sched::SchedTime { ticks: 0 }
}
fn read(&self, _cell: crate::state::StateCellId) -> crate::state::FourStateValue {
panic!("not used by this test")
}
fn rearm(&self, _region: Region, _cb: Box<dyn RegionCallback>) {
panic!("not used by this test")
}
fn take_monitor_changed(&self) -> bool {
panic!("not used by this test")
}
}
stale.invoke(&NoopCtx);
PENDING.with(|p| {
let p = p.borrow();
let (g, text) = p.get(&key).expect("the fresh report must still be pending");
assert_eq!(*g, fresh_generation);
assert_eq!(text, b"new");
});
reset_thread_locals();
}
fn mature_due_at(now: SimTick) {
struct NoopCtx;
impl SchedulerCtx for NoopCtx {
fn now(&self) -> crate::sched::SchedTime {
crate::sched::SchedTime { ticks: 0 }
}
fn read(&self, _cell: crate::state::StateCellId) -> crate::state::FourStateValue {
panic!("not used by this test")
}
fn rearm(&self, _region: Region, _cb: Box<dyn RegionCallback>) {
panic!("not used by this test")
}
fn take_monitor_changed(&self) -> bool {
panic!("not used by this test")
}
}
for (key, generation) in take_due(now) {
Mature { key, generation }.invoke(&NoopCtx);
}
}
fn captured() -> String {
String::from_utf8(CAPTURED.with(|c| c.borrow().clone())).unwrap()
}
#[test]
fn a_global_tick_report_matures_at_the_next_tick_and_keeps_its_own_time() {
reset_thread_locals();
CAPTURED.with(|c| c.borrow_mut().clear());
set_writer(ReportKind::GlobalClockDelayed, capture_writer);
hold_for_global_tick(b"sig is not stable (attempt ended at 80)\n".to_vec(), 80);
mature_due_at(80);
assert_eq!(
captured(),
"",
"the tick of the capture time step is not the next"
);
mature_due_at(90);
assert_eq!(captured(), "sig is not stable (attempt ended at 80)\n");
reset_thread_locals();
}
#[test]
fn global_tick_reports_mature_in_capture_order_once() {
reset_thread_locals();
CAPTURED.with(|c| c.borrow_mut().clear());
set_writer(ReportKind::GlobalClockDelayed, capture_writer);
hold_for_global_tick(b"A@80\n".to_vec(), 80);
hold_for_global_tick(b"B@85\n".to_vec(), 85);
hold_for_global_tick(b"C@90\n".to_vec(), 90);
mature_due_at(90);
assert_eq!(
captured(),
"A@80\nB@85\n",
"C was captured in tick 90's own step"
);
mature_due_at(90);
assert_eq!(captured(), "A@80\nB@85\n", "a report matures once");
mature_due_at(100);
assert_eq!(captured(), "A@80\nB@85\nC@90\n");
reset_thread_locals();
}
#[test]
fn global_tick_reports_never_supersede_each_other() {
reset_thread_locals();
CAPTURED.with(|c| c.borrow_mut().clear());
set_writer(ReportKind::GlobalClockDelayed, capture_writer);
hold_for_global_tick(b"first\n".to_vec(), 71);
hold_for_global_tick(b"second\n".to_vec(), 72);
mature_due_at(80);
assert_eq!(captured(), "first\nsecond\n");
reset_thread_locals();
}
#[test]
fn a_flush_point_does_not_cancel_a_held_global_tick_report() {
reset_thread_locals();
CAPTURED.with(|c| c.borrow_mut().clear());
set_writer(ReportKind::GlobalClockDelayed, capture_writer);
set_writer(ReportKind::PropertyPassFail, capture_writer);
let proc: ProcessId = 3;
crate::sched::with_current_process(proc, || {
hold_for_global_tick(b"held\n".to_vec(), 80);
});
PENDING.with(|p| {
p.borrow_mut().insert(
(ReportKind::PropertyPassFail, Some(proc), 1),
(99, b"gone\n".to_vec()),
)
});
flush(proc);
mature_due_at(90);
assert_eq!(captured(), "held\n");
assert!(
PENDING.with(|p| p.borrow().is_empty()),
"the other kind was flushed"
);
reset_thread_locals();
}
fn mature_all_pending() {
struct NoopCtx;
impl SchedulerCtx for NoopCtx {
fn now(&self) -> crate::sched::SchedTime {
crate::sched::SchedTime { ticks: 0 }
}
fn read(&self, _cell: crate::state::StateCellId) -> crate::state::FourStateValue {
panic!("not used by this test")
}
fn rearm(&self, _region: Region, _cb: Box<dyn RegionCallback>) {
panic!("not used by this test")
}
fn take_monitor_changed(&self) -> bool {
panic!("not used by this test")
}
}
let mut due: Vec<(Key, u64)> = PENDING.with(|p| {
p.borrow()
.iter()
.map(|(k, (g, _))| (*k, *g))
.collect::<Vec<_>>()
});
due.sort_by_key(|&(_, g)| g);
for (key, generation) in due {
Mature { key, generation }.invoke(&NoopCtx);
}
}
#[test]
fn independent_reports_of_one_process_never_supersede_each_other() {
reset_thread_locals();
CAPTURED.with(|c| c.borrow_mut().clear());
set_writer(ReportKind::Violation, capture_writer);
crate::sched::with_current_process(4, || {
report_independent(ReportKind::Violation, Region::Observed, b"first\n".to_vec());
report_independent(
ReportKind::Violation,
Region::Observed,
b"second\n".to_vec(),
);
});
mature_all_pending();
assert_eq!(captured(), "first\nsecond\n");
reset_thread_locals();
}
#[test]
fn flush_violations_discards_only_that_processs_violation_reports() {
reset_thread_locals();
CAPTURED.with(|c| c.borrow_mut().clear());
set_writer(ReportKind::Violation, capture_writer);
set_writer(ReportKind::DeferredImmediate, capture_writer);
crate::sched::with_current_process(4, || {
report_independent(ReportKind::Violation, Region::Observed, b"gone\n".to_vec());
});
crate::sched::with_current_process(5, || {
report_independent(ReportKind::Violation, Region::Observed, b"kept\n".to_vec());
});
PENDING.with(|p| {
p.borrow_mut().insert(
(ReportKind::DeferredImmediate, Some(4), 1),
(99, b"other-kind\n".to_vec()),
)
});
flush_violations(4);
mature_all_pending();
assert_eq!(captured(), "kept\nother-kind\n");
reset_thread_locals();
}
#[test]
fn flush_sites_discards_only_the_reports_of_the_killed_sites() {
reset_thread_locals();
CAPTURED.with(|c| c.borrow_mut().clear());
set_writer(ReportKind::Violation, capture_writer);
set_writer(ReportKind::DeferredImmediate, capture_writer);
crate::sched::with_current_process(4, || {
report_independent_for_site(
ReportKind::Violation,
Region::Observed,
b"killed-violation\n".to_vec(),
Some(7),
);
report_independent_for_site(
ReportKind::Violation,
Region::Observed,
b"other-site\n".to_vec(),
Some(8),
);
report_independent(
ReportKind::Violation,
Region::Observed,
b"untagged\n".to_vec(),
);
});
crate::sched::with_current_process(5, || {
report_for_site(
ReportKind::DeferredImmediate,
11,
Region::Reactive,
b"killed-deferred\n".to_vec(),
Some(7),
);
});
flush_sites(&[7]);
mature_all_pending();
assert_eq!(captured(), "other-site\nuntagged\n");
assert!(
SITES.with(|s| s.borrow().is_empty()),
"no tag outlives its report"
);
reset_thread_locals();
}
#[test]
fn site_tags_do_not_outlive_their_reports() {
reset_thread_locals();
crate::sched::with_current_process(4, || {
report_independent_for_site(
ReportKind::Violation,
Region::Observed,
b"x\n".to_vec(),
Some(1),
);
});
flush_violations(4);
assert!(SITES.with(|s| s.borrow().is_empty()));
crate::sched::with_current_process(4, || {
report_for_site(
ReportKind::DeferredImmediate,
3,
Region::Reactive,
b"old\n".to_vec(),
Some(2),
);
report_for_site(
ReportKind::DeferredImmediate,
3,
Region::Reactive,
b"new\n".to_vec(),
None,
);
});
flush_sites(&[2]);
assert_eq!(
PENDING.with(|p| p.borrow().len()),
1,
"the superseding untagged report is not site 2's"
);
reset_thread_locals();
}
#[test]
fn the_generic_flush_leaves_violation_reports_pending() {
reset_thread_locals();
CAPTURED.with(|c| c.borrow_mut().clear());
set_writer(ReportKind::Violation, capture_writer);
crate::sched::with_current_process(4, || {
report_independent(
ReportKind::Violation,
Region::Observed,
b"stands\n".to_vec(),
);
});
flush(4);
mature_all_pending();
assert_eq!(captured(), "stands\n");
reset_thread_locals();
}
}