Download code/scripts/p2/supply/elab_hash.py from viai957/align: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/viai957/align/resolve/main/code/scripts/p2/supply/elab_hash.py
- Command line
-
hf download hf://viai957/align/code/scripts/p2/supply/elab_hash.py
-
curl -L -o elab_hash.py https://huggingface.co/viai957/align/resolve/main/code/scripts/p2/supply/elab_hash.py
10.3 kB
| #!/usr/bin/env python | |
| """Elaborated-type hash of formal statements (plan v3 Phase 0a item 5, first bullet). | |
| Why: token/alpha hashes (p2_common.statement_sha / alpha_sha) are blind to hypothesis order, notation | |
| (`x β Finset.range n` vs `x < n`-style rewrites that elaborate to the same Expr), implicit coercion spelling, and | |
| `open` differences. The elaborated type of `theorem _align_ref <sig> := by sorry` in the pinned Lean env is | |
| not. Expr uses de Bruijn indices for bound variables, so binder names never reach the hash. | |
| What is hashed (all decided in Lean, `LEAN_DUMP`, then canonicalised and hashed in pure Python): | |
| * the statement type is split with `forallTelescope` into binders and a conclusion; | |
| * instance-implicit binders are DROPPED, and so are instance-implicit ARGUMENTS of every application | |
| (`getFunInfo` paramInfo), so `@HAdd.hAdd β β β inst a b` is `HAdd.hAdd β β β a b`; | |
| * data binders (non-Prop) keep their order (later types may depend on earlier ones) and are named by that | |
| ordinal `v0, v1, ...`; Prop binders (hypotheses) are serialised with the variables as `v_k`, and the | |
| hypothesis list is SORTED, so reordering hypotheses does not change the hash; | |
| * universe levels are dropped; binder names/infos inside terms are dropped; `mdata` is stripped; numerals | |
| keep their type argument (`OfNat.ofNat β 2` differs from `OfNat.ofNat β 2`). | |
| Known blind spots (by design, documented): permuting the order of data variables that are interchangeable is not | |
| canonicalised; hypotheses that mention other hypotheses (proof-dependent types) are serialised with `?h`. | |
| # unit-testable pure parts: parse_dump, canonical_form, elab_sha, build_lean_prelude | |
| # <=50 statements validation (2 Lean workers max): | |
| .venv/bin/python scripts/p2/supply/elab_hash.py --in .tmp/p2/problems/minif2f.jsonl --out .tmp/p2/supply/elab_minif2f.jsonl --limit 50 --workers 2 | |
| # FULL RUN (GPU-free window only; ~0.5-3 s/statement/worker after header warm-up; resumable by --out): | |
| .venv/bin/python scripts/p2/supply/elab_hash.py --in <set>.clean.jsonl --out <set>.elab.jsonl --workers 2 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import importlib.util | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from typing import Any, Iterable, Optional | |
| REPO = Path(__file__).resolve().parents[3] | |
| sys.path.insert(0, str(REPO)) | |
| MARK = "ALIGN_ELAB_HASH " | |
| NS = "AlignElabHash" | |
| REF = "_align_elab_ref" | |
| #: Trusted Lean prelude, elaborated once per worker BEFORE the header's `open`s (pristine namespace). | |
| LEAN_DUMP = r''' | |
| namespace AlignElabHash | |
| open Lean Meta | |
| partial def ser (nm : FVarId β Option String) (e : Expr) : MetaM String := do | |
| match e with | |
| | .bvar i => return s!"#{i}" | |
| | .fvar id => return (nm id).getD "?fv" | |
| | .mvar _ => return "?m" | |
| | .sort l => return s!"Sort({toString l})" | |
| | .const n _ => return n.toString | |
| | .lit (.natVal n) => return toString n | |
| | .lit (.strVal s) => return s!"\"{s}\"" | |
| | .mdata _ b => ser nm b | |
| | .proj s i b => return s!"{s}.{i}({β ser nm b})" | |
| | .lam _ t b _ => return s!"(fun {β ser nm t} => {β ser nm b})" | |
| | .forallE _ t b _ => return s!"(all {β ser nm t} , {β ser nm b})" | |
| | .letE _ t v b _ => return s!"(let {β ser nm t} := {β ser nm v} in {β ser nm b})" | |
| | .app .. => | |
| let f := e.getAppFn | |
| let args := e.getAppArgs | |
| let mut keep : Array Bool := args.map fun _ => true | |
| try | |
| let info β getFunInfoNArgs f args.size | |
| for i in [0:args.size] do | |
| if i < info.paramInfo.size && info.paramInfo[i]!.isInstImplicit then | |
| keep := keep.set! i false | |
| catch _ => pure () | |
| let mut out := #[β ser nm f] | |
| for i in [0:args.size] do | |
| if keep[i]! then out := out.push (β ser nm args[i]!) | |
| return "(" ++ " ".intercalate out.toList ++ ")" | |
| def dump (n : Name) : Elab.Command.CommandElabM Unit := do | |
| let some ci := (β getEnv).find? n | throwError "no such declaration {n}" | |
| let js β Elab.Command.liftTermElabM do | |
| forallTelescope ci.type fun xs concl => do | |
| let mut names : List (FVarId Γ String) := [] | |
| let mut vars : Array String := #[] | |
| let mut hyps : Array String := #[] | |
| let mut k := 0 | |
| for x in xs do | |
| let d β x.fvarId!.getDecl | |
| if d.binderInfo.isInstImplicit then continue | |
| let nmf := fun (id : FVarId) => (names.find? (fun p => p.1 == id)).map (Β·.2) | |
| let ty β ser nmf d.type | |
| if (β isProp d.type) then | |
| hyps := hyps.push ty | |
| else | |
| vars := vars.push ty | |
| names := (x.fvarId!, s!"v{k}") :: names | |
| k := k + 1 | |
| let nmf := fun (id : FVarId) => (names.find? (fun p => p.1 == id)).map (Β·.2) | |
| let c β ser nmf concl | |
| let hs := hyps.toList.map fun h => Json.str h | |
| return Json.compress (Json.mkObj [("vars", Json.arr (vars.toList.map Json.str).toArray), ("hyps", Json.arr hs.toArray), ("concl", Json.str c)]) | |
| logInfo m!"ALIGN_ELAB_HASH {js}" | |
| end AlignElabHash | |
| ''' | |
| # --------------------------------------------------------------------------- pure parts | |
| def build_lean_prelude() -> str: | |
| return LEAN_DUMP | |
| def build_ref_command(sig_text: str) -> str: | |
| return f"theorem {REF} {sig_text} := by\n sorry" | |
| DUMP_COMMAND = f"#eval {NS}.dump `{REF}" | |
| def parse_dump(messages: Iterable[dict]) -> dict: | |
| """Extract the JSON the Lean dump logged. Raises ValueError with the Lean error otherwise.""" | |
| errs = [] | |
| for m in messages: | |
| if not isinstance(m, dict): | |
| continue | |
| data = str(m.get("data", "")) | |
| if m.get("severity") == "error": | |
| errs.append(data) | |
| elif MARK in data: | |
| return json.loads(data.split(MARK, 1)[1].strip()) | |
| raise ValueError("no dump in output" + (": " + errs[0][:200] if errs else "")) | |
| def canonical_form(dump: dict) -> str: | |
| """Hypotheses sorted (multiset order-invariance); data binders and conclusion kept in order.""" | |
| if not {"vars", "hyps", "concl"} <= set(dump): | |
| raise ValueError(f"dump lacks fields: {sorted(dump)}") | |
| return json.dumps({"vars": list(dump["vars"]), "hyps": sorted(dump["hyps"]), "concl": dump["concl"]}, | |
| ensure_ascii=False, separators=(",", ":"), sort_keys=True) | |
| def elab_sha(dump: dict) -> str: | |
| return hashlib.sha256(canonical_form(dump).encode("utf-8")).hexdigest() | |
| # --------------------------------------------------------------------------- Lean side | |
| def _load_common(): | |
| if "p2_common" in sys.modules: | |
| return sys.modules["p2_common"] | |
| spec = importlib.util.spec_from_file_location("p2_common", REPO / "scripts" / "p2" / "p2_common.py") | |
| mod = importlib.util.module_from_spec(spec) | |
| sys.modules["p2_common"] = mod | |
| spec.loader.exec_module(mod) | |
| return mod | |
| def hash_statement(worker, statement: str, timeout_s: float = 120.0) -> dict: | |
| """Elaborate one statement in the pinned env and return {elab_sha, dump} or {error}.""" | |
| from align.verify.lean.repl_pool import ReplError, ReplProtocolError | |
| from align.verify.lean.strict import parse_statement | |
| ref = parse_statement(statement) | |
| if not ref.ok: | |
| return {"error": f"unsupported_statement: {ref.problem}"} | |
| try: | |
| env = worker.header_env(ref.header_text, LEAN_DUMP) | |
| except ReplProtocolError as e: | |
| return {"error": "header_error: " + " ".join(str(e).split())[:200]} | |
| except ReplError as e: | |
| return {"error": e.code if hasattr(e, "code") else type(e).__name__} | |
| try: | |
| r1 = worker.command(build_ref_command(ref.sig_text), env, timeout_s) | |
| errs = [m for m in r1.get("messages", []) if isinstance(m, dict) and m.get("severity") == "error"] | |
| if errs or "env" not in r1: | |
| return {"error": "elab_error: " + " ".join(str(errs[0].get("data", r1) if errs else r1).split())[:200]} | |
| r2 = worker.command(DUMP_COMMAND, r1["env"], timeout_s) | |
| dump = parse_dump(r2.get("messages", [])) | |
| except ReplError as e: | |
| return {"error": e.code if hasattr(e, "code") else type(e).__name__} | |
| except ValueError as e: | |
| return {"error": f"dump_error: {e}"} | |
| return {"elab_sha": elab_sha(dump), "dump": dump} | |
| def main(argv=None) -> int: | |
| ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) | |
| ap.add_argument("--in", dest="inp", required=True) | |
| ap.add_argument("--out", required=True, help="JSONL {problem_id, elab_sha | error}; existing ids are skipped (resume)") | |
| ap.add_argument("--limit", type=int, default=0) | |
| ap.add_argument("--workers", type=int, default=2) | |
| ap.add_argument("--timeout", type=float, default=120.0) | |
| a = ap.parse_args(argv) | |
| if not 1 <= a.workers <= 2: | |
| raise SystemExit("--workers must be 1 or 2 (>2 needs the heavy-job lock; do the full run in a GPU-free window)") | |
| C = _load_common() | |
| from align.utils import guard | |
| from align.verify.lean import ReplPool, find_toolchain | |
| from align.verify.lean.repl_pool import ReplLimits | |
| guard.preflight_host_check(strict=True) | |
| done = set() | |
| if os.path.exists(a.out): | |
| done = {r["problem_id"] for r in C.iter_jsonl(a.out)} | |
| rows = [] | |
| for r in C.iter_jsonl(a.inp): | |
| if r["problem_id"] not in done: | |
| rows.append(r) | |
| if a.limit and len(rows) >= a.limit: | |
| break | |
| tc = find_toolchain() | |
| pool = ReplPool.from_toolchain(tc, n_workers=a.workers, limits=ReplLimits(), preflight=True, | |
| scratch_dir=str(Path(os.environ.get("TMPDIR", str(REPO / ".tmp"))) / "p2_elab_pool")) | |
| t0 = time.time() | |
| ok = 0 | |
| with C.AppendJsonl(a.out) as out: | |
| for i in range(0, len(rows), 16): | |
| chunk = rows[i : i + 16] | |
| res = pool.map(lambda w, r: hash_statement(w, r["formal_statement"], a.timeout), chunk) | |
| for r, x in zip(chunk, res): | |
| if isinstance(x, BaseException): | |
| raise x | |
| out.append({"problem_id": r["problem_id"], **({"elab_sha": x["elab_sha"]} if "elab_sha" in x else {"error": x["error"]})}) | |
| ok += "elab_sha" in x | |
| pool.close() | |
| print(json.dumps({"statements": len(rows), "hashed": ok, "errors": len(rows) - ok, "seconds": round(time.time() - t0, 1)})) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |