|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# SPDX-FileCopyrightText: 2026 ECHIDNA Project Team |
| 3 | +# SPDX-License-Identifier: PMPL-1.0-or-later |
| 4 | + |
| 5 | +""" |
| 6 | +Extract proofs from ACL2 community books and convert to ECHIDNA training format. |
| 7 | +
|
| 8 | +Attempts to download from the ACL2 GitHub repository (books/ directory). |
| 9 | +Falls back to generating high-quality synthetic ACL2 proofs from standard |
| 10 | +patterns (defthm, defun, verify-guards). |
| 11 | +
|
| 12 | +ACL2 (A Computational Logic for Applicative Common Lisp) is an industrial- |
| 13 | +strength theorem prover used for hardware and software verification (e.g. |
| 14 | +AMD floating-point division, JVM bytecode verification). |
| 15 | +
|
| 16 | +Output: training_data/proof_states_acl2.jsonl |
| 17 | +ID range: 92000+ |
| 18 | +""" |
| 19 | + |
| 20 | +import json |
| 21 | +import os |
| 22 | +import re |
| 23 | +import urllib.request |
| 24 | +import urllib.error |
| 25 | +from typing import Dict, List, Any, Tuple |
| 26 | + |
| 27 | +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 28 | +EXTERNAL_DIR = os.path.join(REPO_ROOT, "external_corpora", "acl2") |
| 29 | +OUTPUT_DIR = os.path.join(REPO_ROOT, "training_data") |
| 30 | +OUTPUT_FILE = os.path.join(OUTPUT_DIR, "proof_states_acl2.jsonl") |
| 31 | +STATS_FILE = os.path.join(OUTPUT_DIR, "stats_acl2.json") |
| 32 | +START_ID = 92000 |
| 33 | + |
| 34 | +ACL2_RAW_BASE = "https://raw.githubusercontent.com/acl2/acl2/master" |
| 35 | +ACL2_FILES = [ |
| 36 | + "books/arithmetic/top.lisp", |
| 37 | + "books/arithmetic-5/top.lisp", |
| 38 | + "books/std/lists/top.lisp", |
| 39 | + "books/std/lists/append.lisp", |
| 40 | + "books/std/lists/rev.lisp", |
| 41 | + "books/std/lists/nth.lisp", |
| 42 | + "books/std/lists/len.lisp", |
| 43 | + "books/std/alists/top.lisp", |
| 44 | + "books/ihs/ihs-definitions.lisp", |
| 45 | + "books/ihs/logops-lemmas.lisp", |
| 46 | + "books/data-structures/list-theory.lisp", |
| 47 | + "books/sorting/msort.lisp", |
| 48 | + "books/sorting/isort.lisp", |
| 49 | + "books/arithmetic/natp-posp.lisp", |
| 50 | +] |
| 51 | + |
| 52 | + |
| 53 | +def parse_acl2_file(filepath: str) -> List[Dict[str, Any]]: |
| 54 | + """ |
| 55 | + Parse an ACL2 .lisp file and extract defthm and defun forms. |
| 56 | +
|
| 57 | + ACL2 proofs look like: |
| 58 | + (defthm theorem-name |
| 59 | + body |
| 60 | + :hints (("Goal" :in-theory (enable ...)))) |
| 61 | + """ |
| 62 | + results = [] |
| 63 | + try: |
| 64 | + with open(filepath, "r", encoding="utf-8", errors="replace") as fh: |
| 65 | + content = fh.read() |
| 66 | + except OSError: |
| 67 | + return results |
| 68 | + |
| 69 | + # Extract defthm forms |
| 70 | + # Match balanced parens is hard; use a simpler heuristic |
| 71 | + pattern_thm = re.compile( |
| 72 | + r'\(defthm\s+(\S+)\s+(.*?)(?=\n\s*\(def|\n\s*\(in-theory|\n\s*\(include-book|\Z)', |
| 73 | + re.DOTALL |
| 74 | + ) |
| 75 | + for m in pattern_thm.finditer(content): |
| 76 | + name = m.group(1).strip() |
| 77 | + body = m.group(2).strip() |
| 78 | + # Clean up body |
| 79 | + body_clean = re.sub(r'\s+', ' ', body)[:300] |
| 80 | + |
| 81 | + # Extract hints |
| 82 | + hints_match = re.search(r':hints\s*\((.*?)\)', body, re.DOTALL) |
| 83 | + hints = [] |
| 84 | + if hints_match: |
| 85 | + hint_text = hints_match.group(1) |
| 86 | + hints = re.findall(r':(\w+)', hint_text) |
| 87 | + |
| 88 | + # Extract rule classes |
| 89 | + rules_match = re.search(r':rule-classes\s+(\S+)', body) |
| 90 | + rule_class = rules_match.group(1) if rules_match else "rewrite" |
| 91 | + |
| 92 | + results.append({ |
| 93 | + "theorem": name, |
| 94 | + "goal": body_clean, |
| 95 | + "hints": hints, |
| 96 | + "rule_class": rule_class, |
| 97 | + "source": f"acl2/{os.path.basename(filepath)}", |
| 98 | + }) |
| 99 | + |
| 100 | + return results |
| 101 | + |
| 102 | + |
| 103 | +def download_acl2_files() -> int: |
| 104 | + """Attempt to download ACL2 community book files.""" |
| 105 | + downloaded = 0 |
| 106 | + for rel_path in ACL2_FILES: |
| 107 | + url = f"{ACL2_RAW_BASE}/{rel_path}" |
| 108 | + local_path = os.path.join(EXTERNAL_DIR, os.path.basename(rel_path)) |
| 109 | + if os.path.exists(local_path): |
| 110 | + downloaded += 1 |
| 111 | + continue |
| 112 | + try: |
| 113 | + req = urllib.request.Request(url, headers={"User-Agent": "ECHIDNA/1.5"}) |
| 114 | + with urllib.request.urlopen(req, timeout=15) as resp: |
| 115 | + data = resp.read() |
| 116 | + with open(local_path, "wb") as fh: |
| 117 | + fh.write(data) |
| 118 | + downloaded += 1 |
| 119 | + print(f" Downloaded: {rel_path}") |
| 120 | + except (urllib.error.URLError, OSError, TimeoutError) as exc: |
| 121 | + print(f" Skipped {rel_path}: {exc}") |
| 122 | + return downloaded |
| 123 | + |
| 124 | + |
| 125 | +def generate_synthetic_acl2() -> List[Dict[str, Any]]: |
| 126 | + """ |
| 127 | + Generate high-quality synthetic ACL2 proofs. |
| 128 | +
|
| 129 | + ACL2 proofs use a unique waterfall architecture: simplification, |
| 130 | + destructor elimination, cross-fertilization, generalization, and |
| 131 | + induction. Hints guide the prover. |
| 132 | + """ |
| 133 | + arithmetic = [ |
| 134 | + ("commutativity-of-+", "(equal (+ x y) (+ y x))", ":rule-classes :rewrite"), |
| 135 | + ("associativity-of-+", "(equal (+ (+ x y) z) (+ x (+ y z)))", ":rule-classes :rewrite"), |
| 136 | + ("commutativity-of-*", "(equal (* x y) (* y x))", ":rule-classes :rewrite"), |
| 137 | + ("associativity-of-*", "(equal (* (* x y) z) (* x (* y z)))", ":rule-classes :rewrite"), |
| 138 | + ("distributivity-of-*-over-+", "(equal (* x (+ y z)) (+ (* x y) (* x z)))", ":rule-classes :rewrite"), |
| 139 | + ("right-identity-of-+", "(equal (+ x 0) x)", ":rule-classes :rewrite"), |
| 140 | + ("left-identity-of-*", "(equal (* 1 x) x)", ":rule-classes :rewrite"), |
| 141 | + ("right-cancellation-for-+", "(implies (equal (+ a x) (+ b x)) (equal a b))", ':hints (("Goal" :use (:instance cancel-common)))'), |
| 142 | + ("natp-+", "(implies (and (natp x) (natp y)) (natp (+ x y)))", ':hints (("Goal" :in-theory (enable natp)))'), |
| 143 | + ("natp-*", "(implies (and (natp x) (natp y)) (natp (* x y)))", ':hints (("Goal" :in-theory (enable natp)))'), |
| 144 | + ("posp-implies-natp", "(implies (posp x) (natp x))", ':hints (("Goal" :in-theory (enable posp natp)))'), |
| 145 | + ("<=-reflexive", "(implies (natp x) (<= x x))", ":rule-classes :rewrite"), |
| 146 | + ("<=-transitive", "(implies (and (<= x y) (<= y z)) (<= x z))", ':hints (("Goal" :use (:instance transitivity-of-<=)))'), |
| 147 | + ("<=-antisymmetric", "(implies (and (<= x y) (<= y x)) (equal x y))", ':hints (("Goal" :use (:instance antisymmetry-of-<=)))'), |
| 148 | + ("floor-bounds", "(implies (and (natp x) (posp y)) (and (<= (* y (floor x y)) x) (< x (* y (+ 1 (floor x y))))))", ':hints (("Goal" :in-theory (enable floor)))'), |
| 149 | + ("mod-bounds", "(implies (and (natp x) (posp y)) (< (mod x y) y))", ':hints (("Goal" :in-theory (enable mod)))'), |
| 150 | + ("mod-+-cancel", "(implies (and (natp a) (natp b) (posp n)) (equal (mod (+ a (mod b n)) n) (mod (+ a b) n)))", ':hints (("Goal" :in-theory (enable mod)))'), |
| 151 | + ("expt-+", "(implies (and (natp m) (natp n)) (equal (expt b (+ m n)) (* (expt b m) (expt b n))))", ':hints (("Goal" :induct (expt b m)))'), |
| 152 | + ("evenp-+-2", "(implies (evenp x) (evenp (+ 2 x)))", ':hints (("Goal" :in-theory (enable evenp)))'), |
| 153 | + ("oddp-+-2", "(implies (oddp x) (oddp (+ 2 x)))", ':hints (("Goal" :in-theory (enable oddp)))'), |
| 154 | + ] |
| 155 | + |
| 156 | + lists = [ |
| 157 | + ("true-listp-append", "(implies (true-listp x) (true-listp (append x y)))", ':hints (("Goal" :induct (true-listp x)))'), |
| 158 | + ("append-nil", "(implies (true-listp x) (equal (append x nil) x))", ':hints (("Goal" :induct (true-listp x)))'), |
| 159 | + ("associativity-of-append", "(equal (append (append x y) z) (append x (append y z)))", ':hints (("Goal" :induct (true-listp x)))'), |
| 160 | + ("len-append", "(equal (len (append x y)) (+ (len x) (len y)))", ':hints (("Goal" :induct (true-listp x)))'), |
| 161 | + ("len-revappend", "(equal (len (revappend x y)) (+ (len x) (len y)))", ':hints (("Goal" :induct (revappend x y)))'), |
| 162 | + ("revappend-revappend", "(equal (revappend (revappend x y) z) (revappend y (append x z)))", ':hints (("Goal" :induct (revappend x y)))'), |
| 163 | + ("reverse-reverse", "(implies (true-listp x) (equal (reverse (reverse x)) x))", ':hints (("Goal" :in-theory (enable reverse)))'), |
| 164 | + ("member-append", "(iff (member a (append x y)) (or (member a x) (member a y)))", ':hints (("Goal" :induct (true-listp x)))'), |
| 165 | + ("nth-of-append", "(implies (< (nfix n) (len x)) (equal (nth n (append x y)) (nth n x)))", ':hints (("Goal" :induct (nth n x)))'), |
| 166 | + ("car-cons", "(equal (car (cons x y)) x)", ":rule-classes :rewrite"), |
| 167 | + ("cdr-cons", "(equal (cdr (cons x y)) y)", ":rule-classes :rewrite"), |
| 168 | + ("cons-car-cdr", "(implies (consp x) (equal (cons (car x) (cdr x)) x))", ":rule-classes :rewrite"), |
| 169 | + ("len-nonnegative", "(<= 0 (len x))", ":rule-classes :type-prescription"), |
| 170 | + ("true-listp-revappend", "(implies (true-listp y) (true-listp (revappend x y)))", ':hints (("Goal" :induct (revappend x y)))'), |
| 171 | + ("nthcdr-of-append", "(implies (<= (nfix n) (len x)) (equal (nthcdr n (append x y)) (append (nthcdr n x) y)))", ':hints (("Goal" :induct (nthcdr n x)))'), |
| 172 | + ] |
| 173 | + |
| 174 | + alists = [ |
| 175 | + ("alistp-acons", "(implies (alistp a) (alistp (acons key val a)))", ':hints (("Goal" :in-theory (enable acons alistp)))'), |
| 176 | + ("assoc-of-acons", "(equal (assoc-equal key (acons key val a)) (cons key val))", ':hints (("Goal" :in-theory (enable acons assoc-equal)))'), |
| 177 | + ("assoc-of-acons-diff", "(implies (not (equal k1 k2)) (equal (assoc-equal k1 (acons k2 v a)) (assoc-equal k1 a)))", ':hints (("Goal" :in-theory (enable acons assoc-equal)))'), |
| 178 | + ("strip-cars-acons", "(equal (strip-cars (acons key val a)) (cons key (strip-cars a)))", ':hints (("Goal" :in-theory (enable acons strip-cars)))'), |
| 179 | + ("strip-cdrs-acons", "(equal (strip-cdrs (acons key val a)) (cons val (strip-cdrs a)))", ':hints (("Goal" :in-theory (enable acons strip-cdrs)))'), |
| 180 | + ] |
| 181 | + |
| 182 | + bitvectors = [ |
| 183 | + ("logand-self", "(equal (logand x x) x)", ':hints (("Goal" :in-theory (enable logand)))'), |
| 184 | + ("logand-0", "(equal (logand x 0) 0)", ':hints (("Goal" :in-theory (enable logand)))'), |
| 185 | + ("logior-self", "(equal (logior x x) x)", ':hints (("Goal" :in-theory (enable logior)))'), |
| 186 | + ("logior-0", "(equal (logior x 0) x)", ':hints (("Goal" :in-theory (enable logior)))'), |
| 187 | + ("logxor-self", "(equal (logxor x x) 0)", ':hints (("Goal" :in-theory (enable logxor)))'), |
| 188 | + ("logxor-0", "(equal (logxor x 0) x)", ':hints (("Goal" :in-theory (enable logxor)))'), |
| 189 | + ("lognot-lognot", "(implies (integerp x) (equal (lognot (lognot x)) x))", ':hints (("Goal" :in-theory (enable lognot)))'), |
| 190 | + ("logand-comm", "(equal (logand x y) (logand y x))", ':hints (("Goal" :in-theory (enable logand)))'), |
| 191 | + ("logior-comm", "(equal (logior x y) (logior y x))", ':hints (("Goal" :in-theory (enable logior)))'), |
| 192 | + ("logxor-comm", "(equal (logxor x y) (logxor y x))", ':hints (("Goal" :in-theory (enable logxor)))'), |
| 193 | + ("ash-0", "(equal (ash x 0) (ifix x))", ':hints (("Goal" :in-theory (enable ash)))'), |
| 194 | + ("logbitp-of-logand", "(equal (logbitp n (logand x y)) (and (logbitp n x) (logbitp n y)))", ':hints (("Goal" :in-theory (enable logbitp logand)))'), |
| 195 | + ("unsigned-byte-p-of-logand", "(implies (or (unsigned-byte-p n x) (unsigned-byte-p n y)) (unsigned-byte-p n (logand x y)))", ':hints (("Goal" :in-theory (enable unsigned-byte-p logand)))'), |
| 196 | + ] |
| 197 | + |
| 198 | + sorting_search = [ |
| 199 | + ("orderedp-merge", "(implies (and (orderedp x) (orderedp y)) (orderedp (merge-lists x y)))", ':hints (("Goal" :induct (merge-lists x y)))'), |
| 200 | + ("perm-merge", "(perm (append x y) (merge-lists x y))", ':hints (("Goal" :induct (merge-lists x y)))'), |
| 201 | + ("orderedp-msort", "(orderedp (msort x))", ':hints (("Goal" :induct (msort x)))'), |
| 202 | + ("perm-msort", "(perm x (msort x))", ':hints (("Goal" :induct (msort x)))'), |
| 203 | + ("orderedp-isort", "(orderedp (isort x))", ':hints (("Goal" :induct (isort x)))'), |
| 204 | + ("perm-isort", "(perm x (isort x))", ':hints (("Goal" :induct (isort x)))'), |
| 205 | + ("member-of-sorted", "(implies (and (orderedp x) (member a x) (member b x) (<= a b)) (member b (cdr (member a x))))", ':hints (("Goal" :induct (member a x)))'), |
| 206 | + ("bsearch-correct", "(implies (and (orderedp arr) (member key arr)) (equal (nth (bsearch key arr) arr) key))", ':hints (("Goal" :in-theory (enable bsearch)))'), |
| 207 | + ] |
| 208 | + |
| 209 | + all_categories = [ |
| 210 | + ("arithmetic", arithmetic), |
| 211 | + ("lists", lists), |
| 212 | + ("alists", alists), |
| 213 | + ("bitvectors", bitvectors), |
| 214 | + ("sorting_search", sorting_search), |
| 215 | + ] |
| 216 | + |
| 217 | + proofs = [] |
| 218 | + for category, theorems in all_categories: |
| 219 | + for name, goal, hints in theorems: |
| 220 | + hint_keywords = re.findall(r':(\w+)', hints) |
| 221 | + proofs.append({ |
| 222 | + "theorem": name, |
| 223 | + "goal": f"(defthm {name} {goal})", |
| 224 | + "tactic_proof": f"(defthm {name} {goal} {hints})", |
| 225 | + "tactics": list(dict.fromkeys(hint_keywords))[:20], |
| 226 | + "source": f"acl2_synthetic/{category}", |
| 227 | + }) |
| 228 | + return proofs |
| 229 | + |
| 230 | + |
| 231 | +def run() -> Tuple[int, int]: |
| 232 | + """Run the full extraction pipeline.""" |
| 233 | + os.makedirs(OUTPUT_DIR, exist_ok=True) |
| 234 | + os.makedirs(EXTERNAL_DIR, exist_ok=True) |
| 235 | + |
| 236 | + all_entries: List[Dict[str, Any]] = [] |
| 237 | + extracted_count = 0 |
| 238 | + |
| 239 | + print("[ACL2] Phase 1: Attempting to download from GitHub ...") |
| 240 | + downloaded = download_acl2_files() |
| 241 | + print(f" Downloaded/cached {downloaded} files") |
| 242 | + |
| 243 | + for fname in os.listdir(EXTERNAL_DIR): |
| 244 | + if fname.endswith(".lisp"): |
| 245 | + fpath = os.path.join(EXTERNAL_DIR, fname) |
| 246 | + parsed = parse_acl2_file(fpath) |
| 247 | + for entry in parsed: |
| 248 | + all_entries.append(entry) |
| 249 | + if parsed: |
| 250 | + print(f" Parsed {len(parsed)} theorems from {fname}") |
| 251 | + extracted_count = len(all_entries) |
| 252 | + |
| 253 | + print(f"[ACL2] Phase 2: Generating synthetic proofs ...") |
| 254 | + synthetic = generate_synthetic_acl2() |
| 255 | + existing_names = {e["theorem"] for e in all_entries} |
| 256 | + added = 0 |
| 257 | + for entry in synthetic: |
| 258 | + if entry["theorem"] not in existing_names: |
| 259 | + all_entries.append(entry) |
| 260 | + existing_names.add(entry["theorem"]) |
| 261 | + added += 1 |
| 262 | + print(f" Generated {added} unique synthetic proofs") |
| 263 | + |
| 264 | + current_id = START_ID |
| 265 | + output_records = [] |
| 266 | + for entry in all_entries: |
| 267 | + record = { |
| 268 | + "id": current_id, |
| 269 | + "prover": "ACL2", |
| 270 | + "theorem": entry["theorem"], |
| 271 | + "goal": entry["goal"], |
| 272 | + "context": entry.get("tactics", entry.get("hints", [])), |
| 273 | + "tactic_proof": entry.get("tactic_proof", ""), |
| 274 | + "source": entry.get("source", "acl2"), |
| 275 | + } |
| 276 | + output_records.append(record) |
| 277 | + current_id += 1 |
| 278 | + |
| 279 | + with open(OUTPUT_FILE, "w", encoding="utf-8") as fh: |
| 280 | + for rec in output_records: |
| 281 | + fh.write(json.dumps(rec, ensure_ascii=False) + "\n") |
| 282 | + |
| 283 | + stats = { |
| 284 | + "prover": "ACL2", |
| 285 | + "total_proofs": len(output_records), |
| 286 | + "extracted_from_source": extracted_count, |
| 287 | + "synthetic_added": len(output_records) - extracted_count, |
| 288 | + "id_range": [START_ID, current_id - 1], |
| 289 | + "output_file": OUTPUT_FILE, |
| 290 | + } |
| 291 | + with open(STATS_FILE, "w", encoding="utf-8") as fh: |
| 292 | + json.dump(stats, fh, indent=2) |
| 293 | + |
| 294 | + print(f"\n[ACL2] COMPLETE: {len(output_records)} proofs written to {OUTPUT_FILE}") |
| 295 | + print(f" Extracted from source: {extracted_count}") |
| 296 | + print(f" Synthetic: {len(output_records) - extracted_count}") |
| 297 | + return extracted_count, len(output_records) - extracted_count |
| 298 | + |
| 299 | + |
| 300 | +if __name__ == "__main__": |
| 301 | + run() |
0 commit comments