from __future__ import annotations
import re
import sys
from pathlib import Path
DEFAULT_SRC = Path(__file__).resolve().parent.parent / "src"
def strip_comments(text: str) -> str:
return "\n".join(re.sub(r"//.*$", "", line) for line in text.split("\n"))
def split_top_level(text: str, sep: str = ",") -> list[str]:
parts, depth, cur = [], 0, []
for ch in text:
if ch in "([{<":
depth += 1
elif ch in ")]}>":
depth -= 1
if ch == sep and depth == 0:
parts.append("".join(cur))
cur = []
else:
cur.append(ch)
parts.append("".join(cur))
return [p.strip() for p in parts if p.strip()]
def normalise(clause: str) -> str:
return re.sub(r"\s+", "", clause)
def find_body_start(text: str, from_idx: int) -> int:
depth = 0
for i in range(from_idx, len(text)):
ch = text[i]
if ch in "([":
depth += 1
elif ch in ")]":
depth -= 1
elif ch == "{" and depth == 0:
return i
return -1
def parse_spec_fns(text: str) -> dict[str, list[str]]:
out: dict[str, list[str]] = {}
for m in re.finditer(r"\bspec\s+fn\s+(\w+)\s*\(", text):
name = m.group(1)
brace = find_body_start(text, m.end() - 1)
if brace < 0:
continue
depth, end = 0, -1
for i in range(brace, len(text)):
if text[i] == "{":
depth += 1
elif text[i] == "}":
depth -= 1
if depth == 0:
end = i
break
if end < 0:
continue
body = text[brace + 1 : end].strip()
if "match" in body or body.startswith("if"):
continue
if "&&&" in body:
conj = [c.strip() for c in body.split("&&&") if c.strip()]
elif "&&" in body:
conj = split_top_level(body, "&") if False else [
c.strip() for c in body.split("&&") if c.strip()
]
else:
continue
out[name] = conj
return out
def parse_proof_fns(text: str):
for m in re.finditer(r"\bproof\s+fn\s+(\w+)\s*\(", text):
name = m.group(1)
brace = find_body_start(text, m.end() - 1)
if brace < 0:
continue
sig = text[m.end() : brace]
line = text[: m.start()].count("\n") + 1
def section(kw: str) -> list[str]:
km = re.search(rf"\b{kw}\b", sig)
if not km:
return []
rest = sig[km.end() :]
nxt = re.search(r"\b(requires|ensures|decreases|opens_invariants)\b", rest)
if nxt:
rest = rest[: nxt.start()]
return split_top_level(rest)
yield name, line, section("requires"), section("ensures")
def self_test() -> int:
fixture = Path(__file__).resolve().parent / "fixtures" / "vacuous"
text = strip_comments((fixture / "fixture.rs").read_text())
spec_fns = parse_spec_fns(text)
flagged = set()
for name, _line, req, ens in parse_proof_fns(text):
if _is_vacuous(text, spec_fns, req, ens):
flagged.add(name)
must_flag = {"vacuous_restatement", "vacuous_by_definition"}
must_not = {"contentful_antisymmetry", "contentful_transitivity"}
missed = must_flag - flagged
false_pos = must_not & flagged
if missed or false_pos:
if missed:
print(f"SELF-TEST FAILED: did not flag {sorted(missed)}")
if false_pos:
print(f"SELF-TEST FAILED: wrongly flagged {sorted(false_pos)}")
return 1
print("Self-test passed: both vacuous shapes caught, neither contentful one flagged.")
return 0
def _is_vacuous(text, spec_fns, req, ens) -> bool:
req_norm = {normalise(r) for r in req}
if not req_norm or not ens:
return False
for clause in ens:
if normalise(clause) in req_norm:
return True
call = re.match(r"^([\w:]+)\s*\((.*)\)$", clause.strip())
if not call:
continue
fn = call.group(1).split("::")[-1]
conj = spec_fns.get(fn)
if not conj or len(conj) < 2:
continue
args = split_top_level(call.group(2))
params = _param_names(text, fn)
if params is None or len(params) != len(args):
continue
subst = dict(zip(params, args))
if all(normalise(_substitute(c, subst)) in req_norm for c in conj):
return True
return False
def main(argv: list[str]) -> int:
if len(argv) > 1 and argv[1] == "--self-test":
return self_test()
src = Path(argv[1]) if len(argv) > 1 else DEFAULT_SRC
findings = []
spec_fns: dict[str, list[str]] = {}
sources = sorted(src.glob("*.rs"))
for path in sources:
spec_fns.update(parse_spec_fns(strip_comments(path.read_text())))
for path in sources:
text = strip_comments(path.read_text())
for name, line, req, ens in parse_proof_fns(text):
if not ens:
continue
req_norm = {normalise(r) for r in req}
if not req_norm:
continue
for clause in ens:
cn = normalise(clause)
if cn in req_norm:
findings.append(
(path.name, line, name, clause, "restates a `requires` clause verbatim")
)
continue
call = re.match(r"^([\w:]+)\s*\((.*)\)$", clause.strip())
if not call:
continue
fn = call.group(1).split("::")[-1]
conj = spec_fns.get(fn)
if not conj or len(conj) < 2:
continue
args = split_top_level(call.group(2))
params = _param_names(text, fn)
if params is None or len(params) != len(args):
continue
subst = dict(zip(params, args))
unfolded = [_substitute(c, subst) for c in conj]
if all(normalise(u) in req_norm for u in unfolded):
findings.append(
(
path.name,
line,
name,
clause,
f"`{fn}` unfolds to a conjunction of the `requires` clauses",
)
)
if findings:
print("VACUOUS THEOREMS — the conclusion follows from the hypotheses alone:\n")
for fname, line, name, clause, why in findings:
print(f" {fname}:{line} {name}")
print(f" ensures: {clause}")
print(f" reason: {why}\n")
print(
f"{len(findings)} finding(s). Either strengthen the conclusion into "
"something with content, or delete the theorem."
)
return 1
print("No vacuous theorems found.")
return 0
def _param_names(text: str, fn: str) -> list[str] | None:
m = re.search(rf"\bspec\s+fn\s+{re.escape(fn)}\s*\(([^)]*)\)", text)
if not m:
return None
out = []
for p in split_top_level(m.group(1)):
p = p.strip()
if not p:
continue
out.append(p.split(":")[0].strip())
return out
def _substitute(body: str, subst: dict[str, str]) -> str:
def repl(m: re.Match) -> str:
return subst.get(m.group(0), m.group(0))
return re.sub(r"\b\w+\b", repl, body)
if __name__ == "__main__":
sys.exit(main(sys.argv))