Skip to content

Commit d0762eb

Browse files
hyperpolymathclaude
andcommitted
feat(corpus): Agda extractor — 1 → 5,529 proofs from agda-stdlib
Agda was the single worst-represented prover: 1 proof out of 209,517 (0.0005%). The stdlib is vendored under external_corpora/agda-stdlib with 1,182 .agda source files, so the gap was extraction coverage, not source availability. scripts/extract_agda.jl walks agda-stdlib/src/**/*.agda, splits top-level `name : Type` signatures (with multi-line continuations), and keeps declarations that reference proof-relevant constructs: propositional equality, Dec, relations, ∀/Π/Σ, All/Any, ¬/⊥/⊤, algebraic-structure bundles, and homomorphisms. Output: training_data/proof_states_agda.jsonl (IDs 210000–215528, 5,529 records from 602 files). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ce6528f commit d0762eb

2 files changed

Lines changed: 5868 additions & 0 deletions

File tree

scripts/extract_agda.jl

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
#!/usr/bin/env julia
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
4+
#
5+
# extract_agda.jl — Mine the locally-vendored Agda standard library
6+
# (external_corpora/agda-stdlib) for proof-shaped declarations and emit
7+
# ECHIDNA-format JSONL.
8+
#
9+
# Agda was the worst-represented prover in the corpus (1 proof of
10+
# 209,517). The stdlib vendored under external_corpora/ carries ~1,260
11+
# .agda files — more than enough to lift Agda out of single-digit
12+
# territory.
13+
#
14+
# Extraction strategy
15+
# -------------------
16+
# Agda top-level declarations follow the shape
17+
#
18+
# name : Type
19+
#
20+
# where `Type` may span multiple lines. We:
21+
#
22+
# 1. Walk agda-stdlib/src/**/*.agda.
23+
# 2. Skip blank lines, line/block comments, pragmas, module / open /
24+
# import / private / record / data / syntax / instance directives.
25+
# 3. Accumulate a signature line + its indented continuations.
26+
# 4. Keep a declaration when the signature references proof-relevant
27+
# Agda constructs: propositional equality `_≡_`, decidable `Dec`,
28+
# relations (`Rel`, `_≤_`, `_<_`), universally-quantified
29+
# statements (`∀`, `Π`), `All`, `Any`, `¬`, `⊥`, `⊤`.
30+
# 5. Emit one JSONL record per kept declaration.
31+
#
32+
# Output
33+
# ------
34+
# training_data/proof_states_agda.jsonl
35+
# training_data/stats_agda.json
36+
#
37+
# ID range: 210000+
38+
#
39+
# Run: julia scripts/extract_agda.jl
40+
41+
using JSON3
42+
using Dates
43+
44+
const REPO_ROOT = dirname(dirname(abspath(@__FILE__)))
45+
const AGDA_ROOT = joinpath(REPO_ROOT, "external_corpora", "agda-stdlib")
46+
const AGDA_SRC = joinpath(AGDA_ROOT, "src")
47+
const OUTPUT_DIR = joinpath(REPO_ROOT, "training_data")
48+
const OUTPUT_FILE = joinpath(OUTPUT_DIR, "proof_states_agda.jsonl")
49+
const STATS_FILE = joinpath(OUTPUT_DIR, "stats_agda.json")
50+
const START_ID = 210000
51+
52+
# Signature-level filters.
53+
const PROOFY = [
54+
"_≡_", "", "_≢_", "",
55+
"Dec ", "Dec.", "Dec(", "Dec{",
56+
"Rel ", "_≤_", "_<_", "_≥_", "_>_",
57+
"", "Π", "Σ",
58+
"All ", "Any ",
59+
"¬", "", "",
60+
"Reflexive", "Symmetric", "Transitive", "Total",
61+
"IsCommutative", "IsAssociative", "Injective", "Surjective", "Bijective",
62+
"Monoid", "Group", "Ring", "Semiring", "Lattice",
63+
"Homomorphism", "Isomorphism",
64+
]
65+
66+
# Lines we never open a declaration on.
67+
const SKIP_PREFIXES = [
68+
"module", "open", "import", "private", "abstract", "mutual",
69+
"syntax", "infix", "infixl", "infixr",
70+
"record", "data", "postulate", "instance",
71+
"pattern", "field", "variable", "let", "where",
72+
"{-", "---", "-- ",
73+
"{-#", "primitive",
74+
]
75+
76+
const KW_AS_NAME = Set([
77+
"module", "open", "import", "private", "where", "let", "in",
78+
"data", "record", "field", "instance", "postulate", "primitive",
79+
"with", "rewrite", "do", "λ", "forall", "syntax",
80+
"infix", "infixl", "infixr", "abstract", "mutual",
81+
"pattern", "variable", "constructor", "hiding", "renaming",
82+
])
83+
84+
# ---------------------------------------------------------------------------
85+
# Parsing
86+
# ---------------------------------------------------------------------------
87+
88+
"""
89+
line_indent(line) -> Int
90+
91+
Number of leading spaces on `line` (tabs count as 2).
92+
"""
93+
function line_indent(line::AbstractString)::Int
94+
n = 0
95+
for c in line
96+
c == ' ' ? (n += 1) : c == '\t' ? (n += 2) : break
97+
end
98+
return n
99+
end
100+
101+
"""
102+
is_skippable(stripped) -> Bool
103+
104+
True if this trimmed line cannot begin a top-level signature.
105+
"""
106+
function is_skippable(stripped::AbstractString)::Bool
107+
isempty(stripped) && return true
108+
for p in SKIP_PREFIXES
109+
startswith(stripped, p) && return true
110+
end
111+
return false
112+
end
113+
114+
"""
115+
split_signature(line) -> Union{Nothing, Tuple{String, String}}
116+
117+
If `line` parses as `name : body`, return `(name, body)`. Otherwise
118+
return `nothing`. Only accepts a non-indented line.
119+
120+
Colon parsing: take the first ` : ` that is NOT inside parens / braces
121+
and NOT adjacent to `::`.
122+
"""
123+
function split_signature(line::AbstractString)
124+
depth = 0
125+
i = firstindex(line)
126+
while i < lastindex(line)
127+
c = line[i]
128+
if c == '(' || c == '{' || c == '['
129+
depth += 1
130+
elseif c == ')' || c == '}' || c == ']'
131+
depth -= 1
132+
elseif depth == 0 && c == ':' && i > firstindex(line)
133+
prev = prevind(line, i)
134+
nxt = nextind(line, i)
135+
if line[prev] == ' ' && nxt <= lastindex(line) && line[nxt] == ' '
136+
# Exclude `::` just in case (Agda uses `:` but be safe).
137+
nxt2 = nxt <= lastindex(line) ? nxt : lastindex(line)
138+
if nxt2 <= lastindex(line) && line[nxt2] != ':'
139+
name = strip(line[firstindex(line):prev])
140+
body = strip(line[nxt:lastindex(line)])
141+
return (String(name), String(body))
142+
end
143+
end
144+
end
145+
i = nextind(line, i)
146+
end
147+
return nothing
148+
end
149+
150+
"""
151+
looks_proofy(sig) -> Bool
152+
153+
True if the signature body references a proof-relevant construct.
154+
"""
155+
function looks_proofy(sig::AbstractString)::Bool
156+
for kw in PROOFY
157+
occursin(kw, sig) && return true
158+
end
159+
return false
160+
end
161+
162+
"""
163+
parse_file(path) -> Vector{Dict{String,Any}}
164+
165+
Pull keepable declarations out of one Agda source file.
166+
"""
167+
function parse_file(path::String)::Vector{Dict{String,Any}}
168+
out = Dict{String,Any}[]
169+
local lines::Vector{String}
170+
try
171+
lines = readlines(path)
172+
catch
173+
return out
174+
end
175+
176+
i = 1
177+
n = length(lines)
178+
seen = Set{String}()
179+
rel = relpath(path, REPO_ROOT)
180+
181+
while i <= n
182+
raw = lines[i]
183+
stripped = lstrip(raw)
184+
185+
# Top-level only: skip anything indented or skippable.
186+
if line_indent(raw) != 0 || is_skippable(stripped)
187+
i += 1
188+
continue
189+
end
190+
191+
sigparts = split_signature(stripped)
192+
if sigparts === nothing
193+
i += 1
194+
continue
195+
end
196+
name, body = sigparts
197+
198+
# Guard against single-char or keyword "names".
199+
if isempty(name) || name in KW_AS_NAME || length(name) > 120
200+
i += 1
201+
continue
202+
end
203+
# Multi-word names (e.g. "f g h") are not proper declarations.
204+
occursin(' ', name) && (i += 1; continue)
205+
206+
# Accumulate continuation lines (indented or blank in between).
207+
buf = [body]
208+
j = i + 1
209+
while j <= n
210+
nxt = lines[j]
211+
nstripped = lstrip(nxt)
212+
if isempty(nstripped)
213+
j += 1; continue
214+
end
215+
if line_indent(nxt) > 0
216+
push!(buf, strip(nstripped))
217+
j += 1
218+
else
219+
break
220+
end
221+
end
222+
223+
sig_body = replace(join(buf, " "), r"\s+" => " ")
224+
# Bound the recorded signature length to keep JSONL rows compact.
225+
if length(sig_body) > 400
226+
sig_body = sig_body[1:400] * ""
227+
end
228+
229+
if looks_proofy(sig_body)
230+
key = name * "@" * rel
231+
if !(key in seen)
232+
push!(seen, key)
233+
ctx = collect_context(sig_body)
234+
push!(out, Dict{String,Any}(
235+
"theorem" => name,
236+
"goal" => sig_body,
237+
"context" => ctx,
238+
"source" => "agda-stdlib/" * relpath(path, AGDA_ROOT),
239+
))
240+
end
241+
end
242+
243+
i = max(j, i + 1)
244+
end
245+
246+
return out
247+
end
248+
249+
"""
250+
collect_context(sig) -> Vector{String}
251+
252+
Pick up to ten distinctive keywords from the signature so the dataloader
253+
has a non-empty context field to work with.
254+
"""
255+
function collect_context(sig::AbstractString)::Vector{String}
256+
ctx = String[]
257+
for kw in (
258+
"_≡_", "Dec", "Rel", "_≤_", "_<_", "", "Π", "Σ",
259+
"All", "Any", "¬", "", "",
260+
"Reflexive", "Symmetric", "Transitive",
261+
"IsCommutative", "Injective", "Surjective",
262+
"Monoid", "Group", "Ring", "Semiring",
263+
"Homomorphism", "Isomorphism",
264+
)
265+
occursin(kw, sig) && push!(ctx, kw)
266+
length(ctx) >= 10 && break
267+
end
268+
return ctx
269+
end
270+
271+
# ---------------------------------------------------------------------------
272+
# Main
273+
# ---------------------------------------------------------------------------
274+
275+
function main()
276+
println("EXTRACT AGDA")
277+
println("=" ^ 60)
278+
279+
isdir(AGDA_SRC) || error("Missing $AGDA_SRC — vendor agda-stdlib first.")
280+
281+
files = String[]
282+
for (root, _, names) in walkdir(AGDA_SRC)
283+
for n in names
284+
endswith(n, ".agda") && push!(files, joinpath(root, n))
285+
end
286+
end
287+
println("Scanning $(length(files)) .agda files under $AGDA_SRC")
288+
289+
all_records = Dict{String,Any}[]
290+
parsed_ok = 0
291+
for f in files
292+
recs = parse_file(f)
293+
if !isempty(recs)
294+
parsed_ok += 1
295+
append!(all_records, recs)
296+
end
297+
end
298+
println("Files with keepable decls: $parsed_ok")
299+
println("Raw declarations: $(length(all_records))")
300+
301+
# Assign IDs and write JSONL.
302+
mkpath(OUTPUT_DIR)
303+
nid = START_ID
304+
open(OUTPUT_FILE, "w") do fh
305+
for rec in all_records
306+
rec["id"] = nid
307+
rec["prover"] = "Agda"
308+
rec["tactic_proof"] = "" # Agda stdlib uses equational proofs;
309+
# the body lives in the source file
310+
# referenced in rec["source"].
311+
JSON3.write(fh, rec)
312+
println(fh)
313+
nid += 1
314+
end
315+
end
316+
total = nid - START_ID
317+
println("Wrote $total records to $OUTPUT_FILE")
318+
319+
# Stats.
320+
stats = Dict{String,Any}(
321+
"prover" => "Agda",
322+
"total_proofs" => total,
323+
"files_scanned" => length(files),
324+
"files_with_proofs" => parsed_ok,
325+
"id_range" => [START_ID, nid - 1],
326+
"source" => "external_corpora/agda-stdlib",
327+
"extraction_date" => string(today()),
328+
"extractor" => "scripts/extract_agda.jl",
329+
)
330+
open(STATS_FILE, "w") do fh
331+
JSON3.pretty(fh, stats)
332+
end
333+
println("Wrote $STATS_FILE")
334+
335+
println("=" ^ 60)
336+
println("DONE")
337+
end
338+
339+
main()

0 commit comments

Comments
 (0)