|
| 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 F* (Project Everest) and convert to ECHIDNA training format. |
| 7 | +
|
| 8 | +Attempts to download from the F* GitHub repository (examples/ dir). Falls back |
| 9 | +to generating high-quality synthetic F* proofs. |
| 10 | +
|
| 11 | +F* is a general-purpose ML-like functional programming language with effects |
| 12 | +aimed at program verification. It is used in Project Everest for verified |
| 13 | +cryptographic libraries (HACL*, EverCrypt, miTLS). |
| 14 | +
|
| 15 | +Output: training_data/proof_states_fstar.jsonl |
| 16 | +ID range: 97000+ |
| 17 | +""" |
| 18 | + |
| 19 | +import json |
| 20 | +import os |
| 21 | +import re |
| 22 | +import urllib.request |
| 23 | +import urllib.error |
| 24 | +from typing import Dict, List, Any, Tuple |
| 25 | + |
| 26 | +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 27 | +EXTERNAL_DIR = os.path.join(REPO_ROOT, "external_corpora", "fstar") |
| 28 | +OUTPUT_DIR = os.path.join(REPO_ROOT, "training_data") |
| 29 | +OUTPUT_FILE = os.path.join(OUTPUT_DIR, "proof_states_fstar.jsonl") |
| 30 | +STATS_FILE = os.path.join(OUTPUT_DIR, "stats_fstar.json") |
| 31 | +START_ID = 97000 |
| 32 | + |
| 33 | +FSTAR_RAW = "https://raw.githubusercontent.com/FStarLang/FStar/master" |
| 34 | +FSTAR_FILES = [ |
| 35 | + "examples/algorithms/BinarySearch.fst", |
| 36 | + "examples/algorithms/InsertionSort.fst", |
| 37 | + "examples/algorithms/QuickSort.fst", |
| 38 | + "examples/algorithms/MergeSort.fst", |
| 39 | + "examples/data_structures/BinomialQueue.fst", |
| 40 | + "examples/data_structures/RBTree.fst", |
| 41 | + "examples/crypto/Hacl.fst", |
| 42 | + "examples/micro-benchmarks/Arith.fst", |
| 43 | + "examples/micro-benchmarks/Nat.fst", |
| 44 | + "examples/termination/Ackermann.fst", |
| 45 | + "examples/termination/Fibonacci.fst", |
| 46 | + "ulib/FStar.List.Tot.fst", |
| 47 | + "ulib/FStar.Seq.Base.fst", |
| 48 | + "ulib/FStar.Math.Lemmas.fst", |
| 49 | +] |
| 50 | + |
| 51 | + |
| 52 | +def parse_fstar_file(filepath: str) -> List[Dict[str, Any]]: |
| 53 | + """Parse an F* .fst file and extract val/let definitions with specs.""" |
| 54 | + results = [] |
| 55 | + try: |
| 56 | + with open(filepath, "r", encoding="utf-8", errors="replace") as fh: |
| 57 | + content = fh.read() |
| 58 | + except OSError: |
| 59 | + return results |
| 60 | + |
| 61 | + # val declarations with refinement types |
| 62 | + val_pattern = re.compile( |
| 63 | + r'val\s+(\w+)\s*:\s*(.*?)(?=\nval\s|\nlet\s|\ntype\s|\nopen\s|\nmodule\s|\Z)', |
| 64 | + re.DOTALL |
| 65 | + ) |
| 66 | + for m in val_pattern.finditer(content): |
| 67 | + name = m.group(1).strip() |
| 68 | + sig = re.sub(r'\s+', ' ', m.group(2).strip())[:400] |
| 69 | + if not sig or len(sig) < 5: |
| 70 | + continue |
| 71 | + keywords = re.findall(r'\b(Tot|Lemma|Pure|ST|Stack|GTot|requires|ensures|decreases|modifies)\b', sig) |
| 72 | + results.append({ |
| 73 | + "theorem": name, |
| 74 | + "goal": sig, |
| 75 | + "tactics": list(dict.fromkeys(keywords))[:20], |
| 76 | + "source": f"fstar/{os.path.basename(filepath)}", |
| 77 | + }) |
| 78 | + |
| 79 | + # let with refinement types or Lemma |
| 80 | + let_pattern = re.compile( |
| 81 | + r'let\s+(?:rec\s+)?(\w+)\s*(?::.*?)?\s*=\s*(.*?)(?=\nlet\s|\nval\s|\ntype\s|\Z)', |
| 82 | + re.DOTALL |
| 83 | + ) |
| 84 | + for m in let_pattern.finditer(content): |
| 85 | + name = m.group(1).strip() |
| 86 | + body = re.sub(r'\s+', ' ', m.group(2).strip())[:300] |
| 87 | + if 'Lemma' in body or 'assert' in body or 'calc' in body: |
| 88 | + keywords = re.findall(r'\b(Lemma|assert|calc|assume|admit|Classical)\b', body) |
| 89 | + results.append({ |
| 90 | + "theorem": f"{name}_impl", |
| 91 | + "goal": body[:200], |
| 92 | + "tactics": list(dict.fromkeys(keywords))[:20], |
| 93 | + "source": f"fstar/{os.path.basename(filepath)}", |
| 94 | + }) |
| 95 | + |
| 96 | + return results |
| 97 | + |
| 98 | + |
| 99 | +def download_fstar_files() -> int: |
| 100 | + """Attempt to download F* source files.""" |
| 101 | + downloaded = 0 |
| 102 | + for rel_path in FSTAR_FILES: |
| 103 | + url = f"{FSTAR_RAW}/{rel_path}" |
| 104 | + local_path = os.path.join(EXTERNAL_DIR, os.path.basename(rel_path)) |
| 105 | + if os.path.exists(local_path): |
| 106 | + downloaded += 1 |
| 107 | + continue |
| 108 | + try: |
| 109 | + req = urllib.request.Request(url, headers={"User-Agent": "ECHIDNA/1.5"}) |
| 110 | + with urllib.request.urlopen(req, timeout=15) as resp: |
| 111 | + data = resp.read() |
| 112 | + with open(local_path, "wb") as fh: |
| 113 | + fh.write(data) |
| 114 | + downloaded += 1 |
| 115 | + print(f" Downloaded: {rel_path}") |
| 116 | + except (urllib.error.URLError, OSError, TimeoutError) as exc: |
| 117 | + print(f" Skipped {rel_path}: {exc}") |
| 118 | + return downloaded |
| 119 | + |
| 120 | + |
| 121 | +def generate_synthetic_fstar() -> List[Dict[str, Any]]: |
| 122 | + """Generate high-quality synthetic F* proofs.""" |
| 123 | + |
| 124 | + arithmetic = [ |
| 125 | + ("add_comm", "val add_comm: a:int -> b:int -> Lemma (a + b == b + a)", "let add_comm a b = ()"), |
| 126 | + ("add_assoc", "val add_assoc: a:int -> b:int -> c:int -> Lemma ((a + b) + c == a + (b + c))", "let add_assoc a b c = ()"), |
| 127 | + ("mul_comm", "val mul_comm: a:int -> b:int -> Lemma (a * b == b * a)", "let mul_comm a b = ()"), |
| 128 | + ("mul_assoc", "val mul_assoc: a:int -> b:int -> c:int -> Lemma ((a * b) * c == a * (b * c))", "let mul_assoc a b c = ()"), |
| 129 | + ("distributive", "val distributive: a:int -> b:int -> c:int -> Lemma (a * (b + c) == a * b + a * c)", "let distributive a b c = ()"), |
| 130 | + ("add_zero_r", "val add_zero_r: a:int -> Lemma (a + 0 == a)", "let add_zero_r a = ()"), |
| 131 | + ("mul_one_r", "val mul_one_r: a:int -> Lemma (a * 1 == a)", "let mul_one_r a = ()"), |
| 132 | + ("mul_zero_r", "val mul_zero_r: a:int -> Lemma (a * 0 == 0)", "let mul_zero_r a = ()"), |
| 133 | + ("pow_add", "val pow_add: b:nat -> m:nat -> n:nat -> Lemma (ensures pow b (m + n) == pow b m * pow b n) (decreases m)", "let rec pow_add b m n = if m = 0 then () else pow_add b (m - 1) n"), |
| 134 | + ("pow_mul", "val pow_mul: b:nat -> m:nat -> n:nat -> Lemma (ensures pow b (m * n) == pow (pow b m) n) (decreases n)", "let rec pow_mul b m n = if n = 0 then () else (pow_add b m (m * (n - 1)); pow_mul b m (n - 1))"), |
| 135 | + ("mod_add", "val mod_add: a:int -> b:int -> n:pos -> Lemma ((a + b) % n == ((a % n) + (b % n)) % n)", "let mod_add a b n = FStar.Math.Lemmas.lemma_mod_plus_distr_l a b n"), |
| 136 | + ("div_exact", "val div_exact: a:nat -> b:pos -> Lemma (requires a % b == 0) (ensures a / b * b == a)", "let div_exact a b = FStar.Math.Lemmas.lemma_div_exact a b"), |
| 137 | + ] |
| 138 | + |
| 139 | + lists = [ |
| 140 | + ("append_nil", "val append_nil: #a:Type -> l:list a -> Lemma (l @ [] == l)", "let rec append_nil #a l = match l with | [] -> () | _::tl -> append_nil tl"), |
| 141 | + ("append_assoc", "val append_assoc: #a:Type -> l1:list a -> l2:list a -> l3:list a -> Lemma ((l1 @ l2) @ l3 == l1 @ (l2 @ l3))", "let rec append_assoc #a l1 l2 l3 = match l1 with | [] -> () | _::tl -> append_assoc tl l2 l3"), |
| 142 | + ("length_append", "val length_append: #a:Type -> l1:list a -> l2:list a -> Lemma (length (l1 @ l2) == length l1 + length l2)", "let rec length_append #a l1 l2 = match l1 with | [] -> () | _::tl -> length_append tl l2"), |
| 143 | + ("rev_rev", "val rev_rev: #a:Type -> l:list a -> Lemma (rev (rev l) == l)", "let rec rev_rev #a l = match l with | [] -> () | hd::tl -> rev_append_rev tl [hd]; rev_rev tl"), |
| 144 | + ("map_append", "val map_append: #a:Type -> #b:Type -> f:(a -> b) -> l1:list a -> l2:list a -> Lemma (map f (l1 @ l2) == map f l1 @ map f l2)", "let rec map_append #a #b f l1 l2 = match l1 with | [] -> () | _::tl -> map_append f tl l2"), |
| 145 | + ("length_map", "val length_map: #a:Type -> #b:Type -> f:(a -> b) -> l:list a -> Lemma (length (map f l) == length l)", "let rec length_map #a #b f l = match l with | [] -> () | _::tl -> length_map f tl"), |
| 146 | + ("mem_append", "val mem_append: #a:eqtype -> x:a -> l1:list a -> l2:list a -> Lemma (mem x (l1 @ l2) <==> mem x l1 || mem x l2)", "let rec mem_append #a x l1 l2 = match l1 with | [] -> () | _::tl -> mem_append x tl l2"), |
| 147 | + ("filter_append", "val filter_append: #a:Type -> f:(a -> bool) -> l1:list a -> l2:list a -> Lemma (filter f (l1 @ l2) == filter f l1 @ filter f l2)", "let rec filter_append #a f l1 l2 = match l1 with | [] -> () | hd::tl -> filter_append f tl l2"), |
| 148 | + ("fold_left_append", "val fold_left_append: #a:Type -> #b:Type -> f:(b -> a -> b) -> init:b -> l1:list a -> l2:list a -> Lemma (fold_left f init (l1 @ l2) == fold_left f (fold_left f init l1) l2)", "let rec fold_left_append #a #b f init l1 l2 = match l1 with | [] -> () | hd::tl -> fold_left_append f (f init hd) tl l2"), |
| 149 | + ("for_all_append", "val for_all_append: #a:Type -> f:(a -> bool) -> l1:list a -> l2:list a -> Lemma (for_all f (l1 @ l2) <==> for_all f l1 && for_all f l2)", "let rec for_all_append #a f l1 l2 = match l1 with | [] -> () | _::tl -> for_all_append f tl l2"), |
| 150 | + ] |
| 151 | + |
| 152 | + crypto_verification = [ |
| 153 | + ("aead_encrypt_decrypt", "val aead_encrypt_decrypt: k:key -> n:nonce -> p:plaintext -> ad:aad -> Lemma (ensures (let c = aead_encrypt k n p ad in aead_decrypt k n c ad == Some p))", ""), |
| 154 | + ("hmac_verify", "val hmac_verify: k:key -> m:msg -> t:tag -> Lemma (requires t == hmac k m) (ensures verify_hmac k m t == true)", ""), |
| 155 | + ("hash_collision_resistance", "val hash_collision_resistance: m1:msg -> m2:msg -> Lemma (requires m1 <> m2) (ensures hash m1 <> hash m2) [SMTPat (hash m1); SMTPat (hash m2)]", ""), |
| 156 | + ("kdf_extract_length", "val kdf_extract_length: salt:bytes -> ikm:bytes -> Lemma (ensures length (kdf_extract salt ikm) == hash_length)", ""), |
| 157 | + ("chacha20_involutive", "val chacha20_involutive: k:key -> n:nonce -> ctr:nat -> p:bytes -> Lemma (ensures chacha20 k n ctr (chacha20 k n ctr p) == p)", ""), |
| 158 | + ] |
| 159 | + |
| 160 | + effects_and_state = [ |
| 161 | + ("incr_spec", "val incr: r:ref int -> ST unit (requires fun h -> True) (ensures fun h0 _ h1 -> sel h1 r == sel h0 r + 1)", "let incr r = r := !r + 1"), |
| 162 | + ("swap_spec", "val swap: r1:ref int -> r2:ref int -> ST unit (requires fun h -> True) (ensures fun h0 _ h1 -> sel h1 r1 == sel h0 r2 /\\ sel h1 r2 == sel h0 r1)", "let swap r1 r2 = let t = !r1 in r1 := !r2; r2 := t"), |
| 163 | + ("factorial_spec", "val factorial: n:nat -> Tot (r:nat{r >= 1})", "let rec factorial n = if n = 0 then 1 else n * factorial (n - 1)"), |
| 164 | + ("fibonacci_spec", "val fibonacci: n:nat -> Tot nat (decreases n)", "let rec fibonacci n = if n <= 1 then n else fibonacci (n - 1) + fibonacci (n - 2)"), |
| 165 | + ("alloc_and_free", "val alloc_and_free: n:nat -> ST unit (requires fun h -> True) (ensures fun h0 _ h1 -> modifies Set.empty h0 h1)", "let alloc_and_free n = let r = alloc n in free r"), |
| 166 | + ] |
| 167 | + |
| 168 | + refinement_types = [ |
| 169 | + ("nat_positive", "val nat_positive: n:nat{n > 0} -> Tot (r:nat{r >= 0})", "let nat_positive n = n - 1"), |
| 170 | + ("bounded_add", "val bounded_add: a:nat{a < 100} -> b:nat{b < 100} -> Tot (r:nat{r < 200})", "let bounded_add a b = a + b"), |
| 171 | + ("safe_div", "val safe_div: a:int -> b:int{b <> 0} -> Tot int", "let safe_div a b = a / b"), |
| 172 | + ("vector_index", "val vector_index: #n:nat -> v:vector n -> i:nat{i < n} -> Tot elem", "let vector_index #n v i = Seq.index v i"), |
| 173 | + ("sorted_insert", "val sorted_insert: x:int -> l:list int{sorted l} -> Tot (r:list int{sorted r /\\ length r == length l + 1})", ""), |
| 174 | + ("non_empty_head", "val non_empty_head: #a:Type -> l:list a{Cons? l} -> Tot a", "let non_empty_head #a l = match l with | hd::_ -> hd"), |
| 175 | + ("matrix_mult_dims", "val matrix_mult: #m:nat -> #n:nat -> #p:nat -> matrix m n -> matrix n p -> Tot (matrix m p)", ""), |
| 176 | + ("well_typed_eval", "val well_typed_eval: #t:typ -> e:exp{has_type e t} -> Tot (v:value{value_type v t})", ""), |
| 177 | + ] |
| 178 | + |
| 179 | + termination = [ |
| 180 | + ("ackermann", "val ackermann: m:nat -> n:nat -> Tot nat (decreases %[m; n])", "let rec ackermann m n = if m = 0 then n + 1 else if n = 0 then ackermann (m - 1) 1 else ackermann (m - 1) (ackermann m (n - 1))"), |
| 181 | + ("gcd_termination", "val gcd: a:nat -> b:nat{a > 0 || b > 0} -> Tot (r:pos) (decreases b)", "let rec gcd a b = if b = 0 then a else gcd b (a % b)"), |
| 182 | + ("collatz_steps", "val collatz_steps: n:pos -> Tot nat (decreases %[n; if n = 1 then 0 else 1])", ""), |
| 183 | + ("mutual_even", "val mutual_even: n:nat -> Tot bool (decreases n)\nval mutual_odd: n:nat -> Tot bool (decreases n)", "let rec mutual_even n = if n = 0 then true else mutual_odd (n - 1)\nand mutual_odd n = if n = 0 then false else mutual_even (n - 1)"), |
| 184 | + ] |
| 185 | + |
| 186 | + all_categories = [ |
| 187 | + ("arithmetic", arithmetic), |
| 188 | + ("lists", lists), |
| 189 | + ("crypto", crypto_verification), |
| 190 | + ("effects", effects_and_state), |
| 191 | + ("refinement", refinement_types), |
| 192 | + ("termination", termination), |
| 193 | + ] |
| 194 | + |
| 195 | + proofs = [] |
| 196 | + for category, entries in all_categories: |
| 197 | + for entry in entries: |
| 198 | + name = entry[0] |
| 199 | + sig = entry[1] |
| 200 | + impl = entry[2] if len(entry) > 2 else "" |
| 201 | + |
| 202 | + keywords = re.findall(r'\b(Lemma|Tot|Pure|ST|GTot|requires|ensures|decreases|modifies|SMTPat)\b', sig) |
| 203 | + proofs.append({ |
| 204 | + "theorem": name, |
| 205 | + "goal": sig, |
| 206 | + "tactic_proof": impl, |
| 207 | + "tactics": list(dict.fromkeys(keywords))[:20], |
| 208 | + "source": f"fstar_synthetic/{category}", |
| 209 | + }) |
| 210 | + return proofs |
| 211 | + |
| 212 | + |
| 213 | +def run() -> Tuple[int, int]: |
| 214 | + """Run the full extraction pipeline.""" |
| 215 | + os.makedirs(OUTPUT_DIR, exist_ok=True) |
| 216 | + os.makedirs(EXTERNAL_DIR, exist_ok=True) |
| 217 | + |
| 218 | + all_entries: List[Dict[str, Any]] = [] |
| 219 | + extracted_count = 0 |
| 220 | + |
| 221 | + print("[F*] Phase 1: Attempting to download from GitHub ...") |
| 222 | + downloaded = download_fstar_files() |
| 223 | + print(f" Downloaded/cached {downloaded} files") |
| 224 | + |
| 225 | + for fname in os.listdir(EXTERNAL_DIR): |
| 226 | + if fname.endswith(".fst"): |
| 227 | + fpath = os.path.join(EXTERNAL_DIR, fname) |
| 228 | + parsed = parse_fstar_file(fpath) |
| 229 | + for entry in parsed: |
| 230 | + all_entries.append(entry) |
| 231 | + if parsed: |
| 232 | + print(f" Parsed {len(parsed)} from {fname}") |
| 233 | + extracted_count = len(all_entries) |
| 234 | + |
| 235 | + print(f"[F*] Phase 2: Generating synthetic proofs ...") |
| 236 | + synthetic = generate_synthetic_fstar() |
| 237 | + existing_names = {e["theorem"] for e in all_entries} |
| 238 | + added = 0 |
| 239 | + for entry in synthetic: |
| 240 | + if entry["theorem"] not in existing_names: |
| 241 | + all_entries.append(entry) |
| 242 | + existing_names.add(entry["theorem"]) |
| 243 | + added += 1 |
| 244 | + print(f" Generated {added} unique synthetic proofs") |
| 245 | + |
| 246 | + current_id = START_ID |
| 247 | + output_records = [] |
| 248 | + for entry in all_entries: |
| 249 | + record = { |
| 250 | + "id": current_id, |
| 251 | + "prover": "FStar", |
| 252 | + "theorem": entry["theorem"], |
| 253 | + "goal": entry["goal"], |
| 254 | + "context": entry.get("tactics", []), |
| 255 | + "tactic_proof": entry.get("tactic_proof", ""), |
| 256 | + "source": entry.get("source", "fstar"), |
| 257 | + } |
| 258 | + output_records.append(record) |
| 259 | + current_id += 1 |
| 260 | + |
| 261 | + with open(OUTPUT_FILE, "w", encoding="utf-8") as fh: |
| 262 | + for rec in output_records: |
| 263 | + fh.write(json.dumps(rec, ensure_ascii=False) + "\n") |
| 264 | + |
| 265 | + stats = { |
| 266 | + "prover": "FStar", |
| 267 | + "total_proofs": len(output_records), |
| 268 | + "extracted_from_source": extracted_count, |
| 269 | + "synthetic_added": len(output_records) - extracted_count, |
| 270 | + "id_range": [START_ID, current_id - 1], |
| 271 | + } |
| 272 | + with open(STATS_FILE, "w", encoding="utf-8") as fh: |
| 273 | + json.dump(stats, fh, indent=2) |
| 274 | + |
| 275 | + print(f"\n[F*] COMPLETE: {len(output_records)} proofs written to {OUTPUT_FILE}") |
| 276 | + return extracted_count, len(output_records) - extracted_count |
| 277 | + |
| 278 | + |
| 279 | +if __name__ == "__main__": |
| 280 | + run() |
0 commit comments