proof-aware-crypto-tooling-.../src/pacta/audit.py

133 lines
4.5 KiB
Python

from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from .manifest import VerificationLayout
@dataclass(slots=True)
class HygieneIssue:
severity: str
code: str
path: str
line: int
message: str
def to_dict(self) -> dict[str, object]:
return {
"severity": self.severity,
"code": self.code,
"path": self.path,
"line": self.line,
"message": self.message,
}
SORRY_RE = re.compile(r"\bsorry\b")
AXIOM_RE = re.compile(r"^\s*axiom\s+[A-Za-z0-9_'.]+", re.MULTILINE)
TRUE_TARGET_RE = re.compile(r"\b(?:theorem|lemma)\s+[A-Za-z0-9_'.]+[\s\S]{0,240}?:\s*True\s*:=")
BY_TRIVIAL_RE = re.compile(r"\bby\s+trivial\b")
NATIVE_DECIDE_RE = re.compile(r"\bnative_decide\b")
def scan_hygiene(layout: VerificationLayout, certificates: list[str] | None = None) -> list[HygieneIssue]:
issues: list[HygieneIssue] = []
certs = certificates or []
seen_text = ""
for path in layout.lean_files:
rel = _rel(path, layout.verification_dir)
text = path.read_text(encoding="utf-8", errors="ignore")
code = strip_lean_comments(text)
seen_text += "\n" + code
issues.extend(_scan_file(path, rel, code))
for path in layout.files_not_in_manifest:
issues.append(
HygieneIssue(
severity="warning",
code="proof-file-not-in-manifest",
path=_rel(path, layout.verification_dir),
line=1,
message="Lean proof file is not listed in an explicit manifest.",
)
)
for cert in certs:
if cert and cert not in seen_text:
issues.append(
HygieneIssue(
severity="error",
code="missing-certificate-name",
path=str(layout.verification_dir),
line=1,
message=f"Configured certificate name was not found in scanned Lean files: {cert}",
)
)
return issues
def strip_lean_comments(text: str) -> str:
out: list[str] = []
i = 0
depth = 0
while i < len(text):
if depth == 0 and text.startswith("--", i):
while i < len(text) and text[i] != "\n":
i += 1
if i < len(text):
out.append("\n")
i += 1
continue
if text.startswith("/-", i):
depth += 1
i += 2
continue
if depth > 0:
if text.startswith("-/", i):
depth -= 1
i += 2
else:
if text[i] == "\n":
out.append("\n")
i += 1
continue
out.append(text[i])
i += 1
return "".join(out)
def _scan_file(path: Path, rel: str, code: str) -> list[HygieneIssue]:
issues: list[HygieneIssue] = []
for match in SORRY_RE.finditer(code):
issues.append(_issue("error", "sorry", rel, code, match.start(), "Executable Lean code contains 'sorry'."))
if _is_under_proofs(path) and AXIOM_RE.search(code):
for match in AXIOM_RE.finditer(code):
issues.append(_issue("error", "axiom-in-proofs", rel, code, match.start(), "Proofs/ contains an axiom declaration."))
for match in TRUE_TARGET_RE.finditer(code):
issues.append(_issue("warning", "trivial-true-target", rel, code, match.start(), "Theorem or lemma target appears to be trivially True."))
if _is_spec_or_certificate(path):
for match in BY_TRIVIAL_RE.finditer(code):
issues.append(_issue("warning", "by-trivial-in-spec", rel, code, match.start(), "Spec/certificate/root file contains 'by trivial'."))
for match in NATIVE_DECIDE_RE.finditer(code):
issues.append(_issue("advisory", "native-decide", rel, code, match.start(), "native_decide appears in proof code; review dependency cone."))
return issues
def _issue(severity: str, code_name: str, rel: str, code: str, offset: int, message: str) -> HygieneIssue:
return HygieneIssue(severity=severity, code=code_name, path=rel, line=code.count("\n", 0, offset) + 1, message=message)
def _is_under_proofs(path: Path) -> bool:
return "Proofs" in path.parts
def _is_spec_or_certificate(path: Path) -> bool:
lower = path.name.lower()
return any(token in lower for token in ("spec", "certificate", "main", "root"))
def _rel(path: Path, root: Path) -> str:
try:
return str(path.relative_to(root))
except ValueError:
return str(path)