#!/usr/bin/env python3
"""Classify every external the extraction declares.
For each gen/
/_Template.lean, Aeneas states what the extracted Rust
needs from outside. Each such name must be provided by exactly one of:
MODEL — declared in the hand-written sibling gen//.lean: an
assumption, which the axiom gate and the per-certificate cones
then govern;
PROVEN — resolved to a real definition in the proven corpus, because a
module of this repository declares it (namespace-aware). This is
the valuable case and the one the docs claim for the tier-A/B
curve calls; nothing has ever checked it.
Anything else is drift: the extraction asks for something this repository does
not provide.
"""
import re, sys, os, glob
DECL = re.compile(
r'^[ \t]*(?:@\[[^\]]*\]\s*)?(?:private |protected |noncomputable |unsafe )*'
r'(axiom|def|abbrev|opaque|structure|inductive)[ \t]+([A-Za-z_][A-Za-z0-9_.\'!?]*)')
NS = re.compile(r'^[ \t]*(namespace|end)[ \t]+([A-Za-z_][A-Za-z0-9_.\']*)')
def declared(path):
"""Fully-qualified names declared in one file, honouring namespaces."""
names, stack = set(), []
for line in open(path, encoding='utf-8', errors='replace'):
m = NS.match(line)
if m:
if m.group(1) == 'namespace':
stack.append(m.group(2))
elif stack and stack[-1] == m.group(2):
stack.pop()
continue
d = DECL.match(line)
if d:
names.add('.'.join(stack + [d.group(2)]) if stack else d.group(2))
return names
def main(root):
gen = os.path.join(root, 'gen')
templates = sorted(glob.glob(os.path.join(gen, '*', '*_Template.lean')))
# The proven corpus: every generated module that is neither a template nor
# a hand-written model. These are the files Aeneas produced from Rust.
models = {t.replace('_Template', '') for t in templates}
corpus = set()
for f in sorted(glob.glob(os.path.join(gen, '*', '*.lean'))):
if f in models or f.endswith('_Template.lean'):
continue
corpus |= declared(f)
rows, unresolved = [], []
for t in templates:
model = t.replace('_Template', '')
rel = os.path.relpath(t, gen).replace('_Template.lean', '')
tnames = declared(t)
mnames = declared(model) if os.path.exists(model) else set()
for n in sorted(tnames):
if n in mnames:
rows.append(f'{rel}|{n}|MODEL')
elif n in corpus:
rows.append(f'{rel}|{n}|PROVEN')
else:
rows.append(f'{rel}|{n}|UNRESOLVED')
unresolved.append(f'{rel}|{n}')
for n in sorted(mnames - tnames):
rows.append(f'{rel}|{n}|EXTRA')
print('\n'.join(rows))
print(f'CORRESPONDENCE-COUNT|{len(rows)}')
return 1 if unresolved else 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1]))