use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use proc_macro2::{TokenStream, TokenTree};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Baseline {
pub command: String,
pub total: u64,
pub files: BTreeMap<String, u64>,
}
impl Baseline {
pub fn consistent(&self) -> Result<(), String> {
if let Some((path, _)) = self.files.iter().find(|(_, &n)| n == 0) {
return Err(format!(
"baseline lists {path} at 0; files with no kani::assume are absent"
));
}
let sum: u64 = self.files.values().sum();
if sum != self.total {
return Err(format!(
"baseline total {} is not the sum of its files ({sum})",
self.total
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Count {
pub total: u64,
pub files: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rise {
pub path: String,
pub was: u64,
pub now: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum CountError {
#[error("{path}: does not tokenize as Rust: {msg}")]
Tokenize {
path: String,
msg: String,
},
#[error("{path}: imports kani::assume by `use`; its bare assume(..) calls cannot be counted — call kani::assume(..) by path")]
AliasedImport {
path: String,
},
#[error("{path}: {source}")]
Io {
path: String,
source: std::io::Error,
},
}
pub fn count_source(path: &str, src: &str) -> Result<u64, CountError> {
let stream: TokenStream =
src.parse()
.map_err(|e: proc_macro2::LexError| CountError::Tokenize {
path: path.to_owned(),
msg: e.to_string(),
})?;
let mut flat = Vec::new();
flatten(stream, &mut flat);
if imports_assume(&flat) {
return Err(CountError::AliasedImport {
path: path.to_owned(),
});
}
Ok(flat.windows(4).filter(|w| is_path(w)).count() as u64)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Tok {
Ident(String),
Punct(char),
Open,
Close,
Other,
}
fn flatten(stream: TokenStream, out: &mut Vec<Tok>) {
for tt in stream {
match tt {
TokenTree::Ident(i) => out.push(Tok::Ident(i.to_string())),
TokenTree::Punct(p) => out.push(Tok::Punct(p.as_char())),
TokenTree::Literal(_) => out.push(Tok::Other),
TokenTree::Group(g) => {
out.push(Tok::Open);
flatten(g.stream(), out);
out.push(Tok::Close);
}
}
}
}
fn is_path(w: &[Tok]) -> bool {
matches!(w, [Tok::Ident(k), Tok::Punct(':'), Tok::Punct(':'), Tok::Ident(a)] if k == "kani" && a == "assume")
}
fn imports_assume(flat: &[Tok]) -> bool {
let mut i = 0;
while i < flat.len() {
if flat[i] == Tok::Ident("use".into()) {
let end = flat[i..]
.iter()
.position(|t| *t == Tok::Punct(';'))
.map_or(flat.len(), |p| i + p);
let item = &flat[i..end];
if let Some(k) = item.iter().position(|t| *t == Tok::Ident("kani".into())) {
if item[k..].iter().any(|t| *t == Tok::Ident("assume".into())) {
return true;
}
}
i = end;
}
i += 1;
}
false
}
pub fn count_tree(root: &Path) -> Result<Count, CountError> {
let mut files = Vec::new();
walk(root, &mut files)?;
let mut count = Count::default();
for file in files {
let key = rel_key(root, &file);
let src = std::fs::read_to_string(&file).map_err(|source| CountError::Io {
path: key.clone(),
source,
})?;
let n = count_source(&key, &src)?;
if n > 0 {
count.total += n;
count.files.insert(key, n);
}
}
Ok(count)
}
fn walk(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), CountError> {
let io = |source| CountError::Io {
path: dir.display().to_string(),
source,
};
let mut entries = std::fs::read_dir(dir)
.map_err(io)?
.collect::<Result<Vec<_>, _>>()
.map_err(io)?;
entries.sort_by_key(std::fs::DirEntry::file_name);
for entry in entries {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with('.') {
continue;
}
let kind = entry.file_type().map_err(io)?;
let path = entry.path();
if kind.is_dir() && name != "target" {
walk(&path, out)?;
} else if kind.is_file() && name.ends_with(".rs") {
out.push(path);
}
}
Ok(())
}
fn rel_key(root: &Path, file: &Path) -> String {
let rel = file.strip_prefix(root).unwrap_or(file);
rel.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/")
}
#[must_use]
pub fn rises(measured: &Count, baseline: &Baseline) -> Vec<Rise> {
measured
.files
.iter()
.filter_map(|(path, &now)| {
let was = baseline.files.get(path).copied().unwrap_or(0);
(now > was).then(|| Rise {
path: path.clone(),
was,
now,
})
})
.collect()
}
#[must_use]
pub fn baseline_of(measured: &Count, command: &str) -> Baseline {
Baseline {
command: command.to_owned(),
total: measured.total,
files: measured.files.clone(),
}
}
#[cfg(test)]
mod tests {
include!("kani_assume_tests.rs");
}