use alloc::vec::Vec;
use core::fmt;
use vize_l0::side_table::SideTable;
use vize_l0::{Span, String, cstr};
use crate::dump::Page as L2Page;
pub mod observer;
mod walk;
pub use observer::VerifyObserver;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rigor {
Raw,
Canonical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViolationCode {
SpanOrder,
RegionNesting,
IdResolution,
EmptyIf,
LeadingElse,
BranchOrder,
}
impl ViolationCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
ViolationCode::SpanOrder => "L2V001",
ViolationCode::RegionNesting => "L2V002",
ViolationCode::IdResolution => "L2V003",
ViolationCode::EmptyIf => "L2V004",
ViolationCode::LeadingElse => "L2V005",
ViolationCode::BranchOrder => "L2V006",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Violation {
pub code: ViolationCode,
pub span: Span,
pub message: String,
}
impl fmt::Display for Violation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} @{}:{} {}",
self.code.as_str(),
self.span.start,
self.span.end,
self.message
)
}
}
const _: () = assert!(size_of::<ViolationCode>() == 1);
#[cfg(target_pointer_width = "64")]
const _: () = assert!(size_of::<Violation>() == 40);
#[must_use]
pub fn verify(folio: &L2Page, rigor: Rigor) -> Vec<Violation> {
let mut out = Vec::new();
walk::walk(folio, rigor, &mut out);
out
}
#[must_use]
pub fn verify_table<T>(folio: &L2Page, table: &SideTable<T>) -> Vec<Violation> {
let count = folio.op_count();
let mut out = Vec::new();
for (id, _) in table.sorted_entries() {
if u64::from(id.index()) >= count {
out.push(Violation {
code: ViolationCode::IdResolution,
span: Span::new(0, 0),
message: cstr!("side table references {id}, but the artifact numbers {count} ops"),
});
}
}
out
}
#[cfg(test)]
mod tests {
use super::{Rigor, Violation, ViolationCode, verify, verify_table};
use crate::dump::Page as L2Page;
use alloc::vec;
use vize_l0::id::NodeId;
use vize_l0::side_table::SideTable;
use vize_l0::{Span, cstr};
#[test]
fn codes_render_as_their_stable_strings() {
assert_eq!(ViolationCode::SpanOrder.as_str(), "L2V001");
assert_eq!(ViolationCode::RegionNesting.as_str(), "L2V002");
assert_eq!(ViolationCode::IdResolution.as_str(), "L2V003");
assert_eq!(ViolationCode::EmptyIf.as_str(), "L2V004");
assert_eq!(ViolationCode::LeadingElse.as_str(), "L2V005");
assert_eq!(ViolationCode::BranchOrder.as_str(), "L2V006");
}
#[test]
fn a_violation_renders_as_one_line() {
let violation = Violation {
code: ViolationCode::EmptyIf,
span: Span::new(3, 9),
message: cstr!("`ui.if` owns no branch"),
};
assert_eq!(
cstr!("{violation}").as_str(),
"L2V004 @3:9 `ui.if` owns no branch"
);
}
#[test]
fn an_empty_page_holds_every_invariant_at_both_rigors() {
let folio = L2Page::default();
assert_eq!(verify(&folio, Rigor::Raw), vec![]);
assert_eq!(verify(&folio, Rigor::Canonical), vec![]);
}
#[test]
fn an_empty_side_table_dangles_nothing() {
let folio = L2Page::default();
let table: SideTable<u8> = SideTable::new();
assert_eq!(verify_table(&folio, &table), vec![]);
}
#[test]
fn a_dangling_reference_names_the_id_and_the_count_exactly() {
let folio = L2Page::default();
let mut table = SideTable::new();
table.insert(
NodeId::from_index(4).expect("index 4 has an id"),
"stale fact",
);
let violations = verify_table(&folio, &table);
assert_eq!(
violations,
vec![Violation {
code: ViolationCode::IdResolution,
span: Span::new(0, 0),
message: cstr!("side table references %4, but the artifact numbers 0 ops"),
}]
);
}
}