use aethershell::builtins::{BUILTIN_LOOKUP, FALLBACK_BUILTINS};
use aethershell::safety::{effect_of, Effect};
use std::collections::{HashMap, HashSet};
fn source_files() -> Vec<String> {
fn walk(dir: &std::path::Path, out: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.filter_map(|e| e.ok()) {
let p = e.path();
if p.is_dir() {
walk(&p, out);
} else if p.extension().and_then(|s| s.to_str()) == Some("rs") {
if let Ok(text) = std::fs::read_to_string(&p) {
out.push(text);
}
}
}
}
let mut out = Vec::new();
walk(
&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
&mut out,
);
assert!(
out.len() > 20,
"expected to read the crate's modules, got {}",
out.len()
);
out
}
const EVIDENCE: &[(&str, &str)] = &[
("Command::new", "constructs an OS process"),
("fs::write", "writes a file"),
("fs::remove_file", "deletes a file"),
("fs::remove_dir", "deletes a directory"),
("fs::create_dir", "creates a directory"),
("fs::copy", "copies a file"),
("fs::rename", "renames a path"),
("File::create", "creates a file"),
("TcpStream::connect", "opens a socket"),
("reqwest::", "makes an HTTP request"),
(".append(true)", "opens a file to append"),
(".create(true)", "opens a file to create"),
(".write(true)", "opens a file to write"),
("fs::set_permissions", "changes file permissions"),
("fs::hard_link", "creates a hard link"),
("symlink_file(", "creates a file symlink"),
("symlink_dir(", "creates a directory symlink"),
("fs::symlink(", "creates a symbolic link"),
("TcpListener::bind", "binds a listening socket"),
("UdpSocket::bind", "binds a datagram socket"),
(".kill(", "signals or terminates a process"),
("libc::kill(", "signals a process by pid"),
(".truncate(true)", "truncates a file to zero length"),
];
const BASELINE: &str = include_str!("effect_ratchet_baseline.txt");
fn baseline() -> std::collections::HashSet<&'static str> {
BASELINE
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect()
}
const FOLLOW_DEPTH: usize = 4;
fn strip_comments(src: &str) -> String {
let chars: Vec<char> = src.chars().collect();
let mut out = String::with_capacity(src.len());
let mut i = 0usize;
while i < chars.len() {
let c = chars[i];
let next = chars.get(i + 1).copied();
if c == '/' && next == Some('/') {
while i < chars.len() && chars[i] != '\n' {
out.push(' ');
i += 1;
}
continue;
}
if c == '/' && next == Some('*') {
let mut depth = 1usize;
out.push(' ');
out.push(' ');
i += 2;
while i < chars.len() && depth > 0 {
if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
depth += 1;
out.push(' ');
out.push(' ');
i += 2;
} else if chars[i] == '*' && chars.get(i + 1) == Some(&'/') {
depth -= 1;
out.push(' ');
out.push(' ');
i += 2;
} else {
out.push(if chars[i] == '\n' { '\n' } else { ' ' });
i += 1;
}
}
continue;
}
if c == '\'' {
let close = if next == Some('\\') {
chars
.iter()
.skip(i + 2)
.position(|&x| x == '\'')
.map(|p| i + 2 + p)
} else if chars.get(i + 2) == Some(&'\'') {
Some(i + 2)
} else {
None
};
if let Some(close) = close {
for _ in i..=close {
out.push(' ');
}
i = close + 1;
continue;
}
out.push(c);
i += 1;
continue;
}
if c == '"' {
out.push('"');
i += 1;
let mut esc = false;
while i < chars.len() {
let ch = chars[i];
if ch == '\\' && !esc {
esc = true;
out.push(' ');
i += 1;
continue;
}
if ch == '"' && !esc {
out.push('"');
i += 1;
break;
}
esc = false;
out.push(if ch == '\n' { '\n' } else { ' ' });
i += 1;
}
continue;
}
out.push(c);
i += 1;
}
out
}
fn all_fn_bodies() -> Vec<(String, String)> {
let mut out = Vec::new();
for file in source_files() {
collect_fn_bodies(&strip_comments(&file), &mut out);
}
out
}
fn collect_fn_bodies(source: &str, out: &mut Vec<(String, String)>) {
let bytes = source.as_bytes();
let mut search = 0usize;
while let Some(rel) = source[search..].find("fn ") {
let start = search + rel;
search = start + 3;
let rest = &source[start + 3..];
let name_end = match rest.find(|c: char| !(c.is_alphanumeric() || c == '_')) {
Some(i) => i,
None => continue,
};
let fn_name = rest[..name_end].to_string();
if fn_name.is_empty() {
continue;
}
let brace = match source[start..].find('{') {
Some(i) => start + i,
None => continue,
};
let mut depth = 0i32;
let mut i = brace;
let mut in_str = false;
let mut prev_escape = false;
while i < bytes.len() {
let c = bytes[i] as char;
if in_str {
if c == '\\' && !prev_escape {
prev_escape = true;
} else {
if c == '"' && !prev_escape {
in_str = false;
}
prev_escape = false;
}
} else if c == '"' {
in_str = true;
} else if c == '{' {
depth += 1;
} else if c == '}' {
depth -= 1;
if depth == 0 {
break;
}
}
i += 1;
}
if depth == 0 && i > brace {
out.push((fn_name, source[brace..=i.min(bytes.len() - 1)].to_string()));
}
}
}
fn bodies_by_name() -> HashMap<String, String> {
let mut counts: HashMap<String, usize> = HashMap::new();
let bodies = all_fn_bodies();
for (name, _) in &bodies {
*counts.entry(name.clone()).or_insert(0) += 1;
}
bodies
.into_iter()
.filter(|(name, _)| counts.get(name) == Some(&1))
.collect()
}
fn ambiguous_name_count() -> usize {
let mut counts: HashMap<String, usize> = HashMap::new();
for (name, _) in all_fn_bodies() {
*counts.entry(name).or_insert(0) += 1;
}
counts.values().filter(|c| **c > 1).count()
}
#[test]
fn the_parser_finds_a_plausible_number_of_builtin_bodies() {
let all = bodies_by_name();
let builtins = all.keys().filter(|n| n.starts_with("bi_")).count();
assert!(
builtins > 800,
"expected to parse most builtin bodies, got {builtins}"
);
assert!(
all.len() > builtins,
"expected helper functions to be parsed too, got {} total vs {builtins} builtins",
all.len()
);
let body = all
.get("bi_aecon_decode")
.expect("a known builtin should be parsed");
assert!(
body.starts_with('{') && body.ends_with('}'),
"brace matched"
);
}
#[test]
fn the_lint_does_not_read_comments_as_code() {
let src = "\
// fn json_to_value(json: serde_json::Value) -> Value;\n\
fn deletes_things() { std::fs::remove_file(p); }\n";
let stripped = strip_comments(src);
assert!(
!stripped.contains("fn json_to_value"),
"a commented-out signature must not survive stripping: {stripped}"
);
let mut bodies = Vec::new();
collect_fn_bodies(&stripped, &mut bodies);
assert_eq!(
bodies.len(),
1,
"only the real function should be parsed, got {bodies:?}"
);
assert_eq!(bodies[0].0, "deletes_things");
let flagged: Vec<String> = current_violations()
.into_iter()
.map(|(n, _, _)| n)
.filter(|n| n == "json_parse" || n == "jq_query")
.collect();
assert!(
flagged.is_empty(),
"pure JSON builtins must not be reported as acting: {flagged:?}"
);
}
fn direct_evidence(body: &str) -> Option<(&'static str, &'static str)> {
direct_evidence_in(body, EVIDENCE)
}
fn direct_evidence_in(
body: &str,
table: &'static [(&'static str, &'static str)],
) -> Option<(&'static str, &'static str)> {
table
.iter()
.find(|(m, _)| body.contains(m))
.map(|(m, why)| (*m, *why))
}
const READ_EVIDENCE: &[(&str, &str)] = &[
("fs::read", "reads a file or directory"),
("File::open", "opens a file for reading"),
("read_dir(", "lists a directory"),
("fs::metadata", "stats a path"),
(
"fs::symlink_metadata",
"stats a path without following links",
),
("env::var", "reads the environment"),
("fs::canonicalize", "resolves a path against the filesystem"),
];
fn delegated_evidence(
body: &str,
all: &HashMap<String, String>,
depth: usize,
seen: &mut HashSet<String>,
) -> Option<(&'static str, String)> {
if depth == 0 {
return None;
}
for callee in called_names(body) {
if !seen.insert(callee.clone()) {
continue;
}
let Some(cb) = all.get(&callee) else { continue };
if let Some((marker, why)) = direct_evidence(cb) {
return Some((marker, format!("{callee}() {why}")));
}
if let Some((marker, chain)) = delegated_evidence(cb, all, depth - 1, seen) {
return Some((marker, format!("{callee}() → {chain}")));
}
}
None
}
fn called_names(body: &str) -> Vec<String> {
let mut out = Vec::new();
let mut ident_start: Option<usize> = None;
let mut prev: Option<char> = None;
let mut prev_prev: Option<char> = None;
for (i, c) in body.char_indices() {
let is_ident = c.is_alphanumeric() || c == '_';
match (ident_start, is_ident) {
(None, true) if c.is_alphabetic() || c == '_' => {
let qualified = prev == Some('.') || (prev == Some(':') && prev_prev == Some(':'));
if !qualified {
ident_start = Some(i);
}
}
(Some(start), false) => {
if c == '(' {
out.push(body[start..i].to_string());
}
ident_start = None;
}
_ => {}
}
if !c.is_whitespace() {
prev_prev = prev;
prev = Some(c);
}
}
out
}
fn dispatched_pairs() -> Vec<(String, String)> {
let mut pairs: Vec<(String, String)> = bodies_by_name()
.keys()
.filter_map(|fn_name| {
let name = fn_name.strip_prefix("bi_")?;
(!name.is_empty() && BUILTIN_LOOKUP.contains_key(name))
.then(|| (name.to_string(), fn_name.clone()))
})
.collect();
pairs.extend(
FALLBACK_BUILTINS
.iter()
.map(|(n, f)| ((*n).to_string(), (*f).to_string())),
);
pairs.sort();
pairs.dedup_by(|a, b| a.0 == b.0);
pairs
}
fn current_violations() -> Vec<(String, &'static str, String)> {
let all = bodies_by_name();
let mut out = Vec::new();
for (name, fn_name) in dispatched_pairs() {
if effect_of(&name) != Effect::Pure {
continue;
}
let Some(body) = all.get(&fn_name) else {
continue;
};
if let Some((marker, why)) = direct_evidence(body) {
out.push((name, marker, why.to_string()));
continue;
}
let mut seen = HashSet::new();
seen.insert(fn_name.clone());
if let Some((marker, chain)) = delegated_evidence(body, &all, FOLLOW_DEPTH, &mut seen) {
out.push((name, marker, format!("delegates: {chain}")));
}
}
out.sort();
out
}
#[test]
fn no_new_builtin_acts_while_classified_pure() {
let base = baseline();
let fresh: Vec<String> = current_violations()
.into_iter()
.filter(|(name, _, _)| !base.contains(name.as_str()))
.map(|(name, marker, why)| format!(" {name}: {why} (`{marker}`) but effect_of = Pure"))
.collect();
assert!(
fresh.is_empty(),
"{} builtin(s) added since the baseline act but are classified Pure.\n\
Classify them in `safety::effect_of` — do not add them to \
tests/effect_ratchet_baseline.txt, which may only shrink:\n{}",
fresh.len(),
fresh.join("\n")
);
}
#[test]
fn the_baseline_only_shrinks() {
let current: std::collections::HashSet<String> = current_violations()
.into_iter()
.map(|(n, _, _)| n)
.collect();
let fixed: Vec<&str> = baseline()
.into_iter()
.filter(|n| !current.contains(*n))
.collect();
assert!(
fixed.is_empty(),
"{} baseline entr(ies) no longer violate — delete them from \
tests/effect_ratchet_baseline.txt:\n {}",
fixed.len(),
fixed.join("\n ")
);
}
#[test]
fn report_the_outstanding_debt() {
let n = current_violations().len();
println!("effect ratchet: {n} builtin(s) act while classified Pure (baseline 0)");
println!(
"unresolvable call names (defined more than once, so not followed): {}",
ambiguous_name_count()
);
assert!(n <= baseline().len(), "the debt must never grow");
}
#[test]
fn the_scanner_still_sees_a_process_being_constructed() {
let all = bodies_by_name();
let body = all
.get("bi_platform_machine_id")
.expect("bi_platform_machine_id should be parsed");
assert!(
direct_evidence(body).is_some(),
"this builtin constructs a process; failing to see it means the scanner \
is blind, not that the code changed"
);
}
#[test]
fn the_scanner_still_sees_a_file_being_appended_to() {
let all = bodies_by_name();
let body = all
.get("bi_git_ignore")
.expect("bi_git_ignore should be parsed");
assert!(
direct_evidence(body).is_some(),
"git_ignore opens a file with .append(true)"
);
}
#[test]
fn a_char_literal_containing_a_quote_does_not_blind_the_scanner() {
let src = r#"
fn probe() {
let quote = '"';
let _ = quote;
std::process::Command::new("ls");
}
"#;
let stripped = strip_comments(src);
assert!(
stripped.contains("Command::new"),
"code after a quote character literal was blanked: {stripped}"
);
}
#[test]
fn a_string_literal_is_not_read_as_code() {
let src = r#"
fn helptext() -> &'static str {
"usage: xs | join(\"-\")"
}
"#;
let stripped = strip_comments(src);
assert!(
!stripped.contains("join("),
"documentation inside a string was read as a call: {stripped}"
);
}
fn readers_classified_pure() -> Vec<(String, String)> {
let all = bodies_by_name();
let mut out = Vec::new();
for name in BUILTIN_LOOKUP.keys() {
if effect_of(name) != Effect::Pure {
continue;
}
let Some(body) = all.get(&format!("bi_{name}")) else {
continue;
};
if let Some((_, why)) = direct_evidence_in(body, READ_EVIDENCE) {
out.push((name.to_string(), why.to_string()));
}
}
out.sort();
out
}
#[test]
fn report_builtins_that_read_while_classified_pure() {
let readers = readers_classified_pure();
println!(
"read-evidence: {} builtin(s) observe local state while classified Pure",
readers.len()
);
for (name, why) in &readers {
println!(" {name}: {why}");
}
}
#[test]
fn the_read_scanner_still_sees_a_file_being_opened() {
let all = bodies_by_name();
let body = all
.get("bi_cat")
.expect("bi_cat should be found by the body parser");
assert!(
direct_evidence_in(body, READ_EVIDENCE).is_some(),
"the read scanner no longer sees bi_cat reading a file; the 0 in the \
report is blindness, not coverage"
);
}
#[test]
fn the_read_scanner_does_not_fire_on_a_genuinely_pure_builtin() {
let all = bodies_by_name();
let body = all
.get("bi_upper")
.expect("bi_upper should be found by the body parser");
assert!(
direct_evidence_in(body, READ_EVIDENCE).is_none(),
"`upper` uppercases a string and must not read as observing local state"
);
}
fn all_definitions_by_name() -> HashMap<String, Vec<String>> {
let mut m: HashMap<String, Vec<String>> = HashMap::new();
for (name, body) in all_fn_bodies() {
m.entry(name).or_default().push(body);
}
m
}
fn optimistic_evidence(
body: &str,
all: &HashMap<String, Vec<String>>,
depth: usize,
seen: &mut HashSet<String>,
) -> Option<(&'static str, String)> {
if depth == 0 {
return None;
}
for callee in called_names(body) {
if !seen.insert(callee.clone()) {
continue;
}
let Some(defs) = all.get(&callee) else {
continue;
};
for cb in defs {
if let Some((marker, why)) = direct_evidence(cb) {
return Some((marker, format!("{callee}() {why}")));
}
}
for cb in defs {
if let Some((marker, chain)) = optimistic_evidence(cb, all, depth - 1, seen) {
return Some((marker, format!("{callee}() → {chain}")));
}
}
}
None
}
#[test]
fn report_what_the_ambiguity_blind_spot_could_be_hiding() {
let strict = bodies_by_name();
let loose = all_definitions_by_name();
let mut leads: Vec<(String, String)> = Vec::new();
for name in BUILTIN_LOOKUP.keys() {
if effect_of(name) != Effect::Pure {
continue;
}
let Some(body) = strict.get(&format!("bi_{name}")) else {
continue;
};
let mut seen = HashSet::new();
if let Some((_, chain)) = optimistic_evidence(body, &loose, FOLLOW_DEPTH, &mut seen) {
leads.push((name.to_string(), chain));
}
}
leads.sort();
println!(
"ambiguity exposure: {} builtin(s) classified Pure could reach an effect \
if an ambiguous name resolves to an acting definition",
leads.len()
);
for (name, chain) in leads.iter().take(25) {
println!(" {name}: {chain}");
}
if leads.len() > 25 {
println!(" ... and {} more", leads.len() - 25);
}
}
#[test]
fn the_optimistic_resolver_is_not_simply_blind() {
let strict = bodies_by_name();
let loose = all_definitions_by_name();
let mut found = 0usize;
for name in BUILTIN_LOOKUP.keys() {
let Some(body) = strict.get(&format!("bi_{name}")) else {
continue;
};
let mut seen = HashSet::new();
if optimistic_evidence(body, &loose, FOLLOW_DEPTH, &mut seen).is_some() {
found += 1;
}
}
println!("optimistic resolver reaches an effect from {found} builtin(s)");
assert!(
found > 100,
"the optimistic resolver found effects from only {found} builtins; it \
has gone blind, and the 0-exposure report above is meaningless"
);
}
#[test]
fn report_fallback_dispatch_coverage() {
let all = bodies_by_name();
let arms: Vec<(String, String)> = FALLBACK_BUILTINS
.iter()
.map(|(n, f)| ((*n).to_string(), (*f).to_string()))
.collect();
let mut resolved = 0usize;
let mut unresolved: Vec<String> = Vec::new();
let mut declared = 0usize;
let mut pure_no_evidence: Vec<String> = Vec::new();
for (name, fn_name) in &arms {
match all.get(fn_name) {
Some(_) => resolved += 1,
None => unresolved.push(format!("{name} -> {fn_name}")),
}
if effect_of(name) != Effect::Pure {
declared += 1;
} else if all.contains_key(fn_name) {
pure_no_evidence.push(name.clone());
}
}
unresolved.sort();
unresolved.dedup();
pure_no_evidence.sort();
println!("fallback arms: {}", arms.len());
println!("bodies resolved: {resolved}");
println!("bodies unresolved ({}): {unresolved:#?}", unresolved.len());
println!("classified non-Pure: {declared}");
println!(
"Pure with a readable body and no evidence ({}): {pure_no_evidence:#?}",
pure_no_evidence.len()
);
assert!(
resolved * 2 > arms.len(),
"the fallback widening resolves only {resolved} of {} arms to a readable \
body — the ratchet's zero is mostly blindness, not coverage",
arms.len()
);
}