#!/usr/bin/env python3 """ Anti-smuggle CI gate: detect vacuous theorems that pass `lake build`. Catches patterns like: - theorem body is `:= rfl` with no prior computation - equality where both sides are syntactically identical (bound var rename) - `:=` followed by a reference to a trivially-true definition - `by` block with only `rfl` or `simp` that does no work Usage: python3 scripts/anti_smuggle_check.py # scan formal/ python3 scripts/anti_smuggle_check.py --ci # strict mode (exit 1 on any finding) python3 scripts/anti_smuggle_check.py --file foo.lean # single file """ import argparse import os import re import sys from pathlib import Path SUSPICIOUS_RFL = re.compile( r'(theorem|lemma)\s+(\w+)\s*[^:]*:\s*([^=]+)=\s*([^=]+)\s*:=\s*rfl\b' ) SUSPICIOUS_BY_RFL = re.compile( r'(theorem|lemma)\s+(\w+)\s*[^:]*:\s*([^=]+)=\s*([^=]+)\s*:=\s*by\s+rfl\b' ) SUSPICIOUS_RENAME = re.compile( r'(theorem|lemma)\s+(\w+)\s*[^:]*:\s*' r'∀[^,]+,([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)\.\s*' r'\(∑[^:]+\s*=\s*∑[^:]+\)' ) BARE_RFL_LINE = re.compile(r'^\s*(:=|=>)\s*rfl\s*$') LAMBDA_RFL = re.compile( r'^\s*:=\s*(fun\s+(\w+\s+)+\w+\s*=>\s*rfl|λ\s+(\w+\s+)+\w+\s*=>\s*rfl)\s*$' ) EMPTY_SORRY = re.compile(r'^\s*(:=|=>)\s*(by\s+)?sorry\s*$') def check_file(path: str, ci_mode: bool) -> int: """Check a single .lean file for smuggle patterns. Returns finding count.""" findings = 0 try: with open(path, 'r') as f: content = f.read() except Exception as e: print(f" ERROR reading {path}: {e}") return 0 rel = os.path.relpath(path) # 1. Check for `:= rfl` after an equality for m in SUSPICIOUS_RFL.finditer(content): lhs = m.group(3).strip() rhs = m.group(4).strip() if lhs == rhs: findings += 1 print(f" [SMUGGLE] {rel}:{line_of(content, m.start())} " f"theorem '{m.group(2)}' is `:= rfl` with identical LHS/RHS '{lhs}'") # 2. Check for `:= by rfl` for m in SUSPICIOUS_BY_RFL.finditer(content): lhs = m.group(3).strip() rhs = m.group(4).strip() if lhs == rhs: findings += 1 print(f" [SMUGGLE] {rel}:{line_of(content, m.start())} " f"theorem '{m.group(2)}' is `:= by rfl` with identical LHS/RHS") # 3. Check for the YB pattern: sums with bound variable rename for m in SUSPICIOUS_RENAME.finditer(content): # This is a specific pattern: ∀ ... (sum ... r ... = sum ... s ...) # It might still be non-trivial if the body does work; flag for review findings += 1 print(f" [SMUGGLE] {rel}:{line_of(content, m.start())} " f"theorem '{m.group(2)}' has ∀-quantified equation with sums " f"(possible bound-variable rename)") # 4. Check for trivial theorem bodies: `:= rfl`, `:= fun ... => rfl`, `:= by rfl` lines = content.split('\n') for i, line in enumerate(lines): stripped = line.strip() # Check single-line `:= rfl` if re.match(r'^\s*:=\s*rfl\s*$', stripped): surrounding = '\n'.join(lines[max(0, i - 3):i + 1]) if re.search(r'(theorem|lemma)\s+\w+', surrounding): findings += 1 print(f" [SMUGGLE] {rel}:{i + 1} bare `:= rfl` as theorem body") # Check `:= fun ... => rfl` (possibly multi-line: `:=` on prev line, `fun ... => rfl` on this line) if re.match(r'^\s*fun\s+(\w+\s+)+\w+\s*=>\s*rfl\s*$', stripped) or \ re.match(r'^\s*λ\s+(\w+\s+)+\w+\s*=>\s*rfl\s*$', stripped): surrounding = '\n'.join(lines[max(0, i - 2):i + 1]) if re.search(r'(theorem|lemma)\s+\w+|:=\s*$', surrounding, re.MULTILINE): findings += 1 print(f" [SMUGGLE] {rel}:{i + 1} `fun ... => rfl` lambda as theorem body") if EMPTY_SORRY.match(stripped) and ci_mode: findings += 1 print(f" [WARN] {rel}:{i + 1} bare sorry (not an axiom with TODO)") return findings def line_of(content: str, pos: int) -> int: """Return 1-indexed line number for a character position.""" return content[:pos].count('\n') + 1 def check_dir(root: str, ci_mode: bool) -> int: """Recursively check all .lean files under root.""" total = 0 for dirpath, _, filenames in os.walk(root): for fn in filenames: if fn.endswith('.lean'): total += check_file(os.path.join(dirpath, fn), ci_mode) return total def main(): parser = argparse.ArgumentParser( description="Anti-smuggle CI gate for Lean theorem vacuity" ) parser.add_argument('paths', nargs='*', default=['formal'], help='Files or directories to check') parser.add_argument('--ci', action='store_true', help='Strict mode: exit 1 on any finding') parser.add_argument('--file', type=str, help='Check a single file') args = parser.parse_args() total = 0 if args.file: total += check_file(args.file, args.ci) for path in args.paths: if os.path.isfile(path): total += check_file(path, args.ci) elif os.path.isdir(path): total += check_dir(path, args.ci) elif path != 'formal': print(f"WARNING: {path} not found", file=sys.stderr) if total == 0: print("Anti-smuggle check: PASSED (no vacuities detected)") return 0 else: msg = f"Anti-smuggle check: {total} potential vacuit{'y' if total == 1 else 'ies'} found" if args.ci: print(f"FAIL: {msg}", file=sys.stderr) return 1 else: print(f"WARNING: {msg}") return 0 if __name__ == '__main__': sys.exit(main())