Skip to content

Commit 0c5accc

Browse files
committed
chore: automated sync of local changes
1 parent fbad8e6 commit 0c5accc

9 files changed

Lines changed: 303699 additions & 81 deletions

File tree

fly.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
# Internal-only: echidna is reached via echidna-nesy.internal:8090 from
99
# proven-nesy-solver-api; no public http_service.
1010
#
11-
# NB: Container is LARGE (2-4 GB) — Julia + Idris2 + Agda + Lean toolchains.
12-
# Memory must be >=2GB; shared-cpu-2x is the cheapest size that fits.
11+
# NB: Container is LARGE (~6-8 GB) — Julia + Idris2 + Agda + Lean + Isabelle.
12+
# Memory must be >=4GB (Isabelle Main heap ~1GB); shared-cpu-2x is the
13+
# cheapest size that fits.
1314

1415
app = "echidna-nesy"
1516
primary_region = "lhr"
@@ -49,6 +50,6 @@ kill_timeout = "10s"
4950

5051
[[vm]]
5152
size = "shared-cpu-2x"
52-
memory = "2048mb" # Julia + Agda need real RAM
53+
memory = "4096mb" # Julia + Agda + Isabelle Main heap need real RAM
5354
cpu_kind = "shared"
5455
cpus = 2

scripts/a2ml_emit.jl

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
# A2ML emitter for training-corpus records.
6+
# A2ML is a TOML-flavored data language (per hyperpolymath No-JSON-Emit rule).
7+
# This module provides minimal, dependency-free serialisation for the
8+
# record shapes used by ECHIDNA's extract_*.jl scripts.
9+
#
10+
# Supported value types: String, Integer, Float, Bool, Vector{String},
11+
# Vector{<:Integer}, Vector{<:AbstractFloat}, nothing (omitted).
12+
# Nested tables are NOT supported here — records are flat.
13+
14+
module A2MLEmit
15+
16+
export escape_a2ml_string, emit_scalar, emit_kv, write_metadata_table,
17+
write_record, write_records_file
18+
19+
"""
20+
escape_a2ml_string(s) -> String
21+
22+
Escape a string for A2ML (TOML) basic-string context: backslash,
23+
double-quote, control chars.
24+
"""
25+
function escape_a2ml_string(s::AbstractString)::String
26+
io = IOBuffer()
27+
for c in s
28+
if c == '\\'
29+
print(io, "\\\\")
30+
elseif c == '"'
31+
print(io, "\\\"")
32+
elseif c == '\n'
33+
print(io, "\\n")
34+
elseif c == '\r'
35+
print(io, "\\r")
36+
elseif c == '\t'
37+
print(io, "\\t")
38+
elseif c == '\b'
39+
print(io, "\\b")
40+
elseif c == '\f'
41+
print(io, "\\f")
42+
elseif UInt32(c) < 0x20
43+
print(io, "\\u", uppercase(string(UInt32(c); base=16, pad=4)))
44+
else
45+
print(io, c)
46+
end
47+
end
48+
return String(take!(io))
49+
end
50+
51+
"""
52+
emit_scalar(v) -> String
53+
54+
Render a scalar value as an A2ML/TOML literal.
55+
"""
56+
function emit_scalar(v)::String
57+
if v === nothing
58+
return "\"\"" # empty string sentinel
59+
elseif v isa Bool
60+
return v ? "true" : "false"
61+
elseif v isa Integer
62+
return string(v)
63+
elseif v isa AbstractFloat
64+
return isfinite(v) ? string(v) : "nan"
65+
elseif v isa AbstractString
66+
return "\"" * escape_a2ml_string(v) * "\""
67+
elseif v isa AbstractVector
68+
parts = [emit_scalar(x) for x in v]
69+
return "[" * join(parts, ", ") * "]"
70+
elseif v isa AbstractDict
71+
# Inline table
72+
parts = String[]
73+
for (kk, vv) in v
74+
push!(parts, string(kk) * " = " * emit_scalar(vv))
75+
end
76+
return "{" * join(parts, ", ") * "}"
77+
else
78+
# Fallback: stringify
79+
return "\"" * escape_a2ml_string(string(v)) * "\""
80+
end
81+
end
82+
83+
"""
84+
emit_kv(io, key, value)
85+
86+
Write a single `key = value` line (omits keys whose value is `nothing`).
87+
"""
88+
function emit_kv(io::IO, key::AbstractString, value)
89+
value === nothing && return
90+
println(io, key, " = ", emit_scalar(value))
91+
end
92+
93+
"""
94+
write_metadata_table(io, metadata::AbstractDict)
95+
96+
Write a `[metadata]` table.
97+
"""
98+
function write_metadata_table(io::IO, metadata::AbstractDict)
99+
println(io, "[metadata]")
100+
for (k, v) in metadata
101+
emit_kv(io, string(k), v)
102+
end
103+
println(io)
104+
end
105+
106+
"""
107+
write_record(io, table_name, record::AbstractDict)
108+
109+
Write a `[[table_name]]` array-of-tables entry.
110+
"""
111+
function write_record(io::IO, table_name::AbstractString, record::AbstractDict)
112+
println(io, "[[", table_name, "]]")
113+
for (k, v) in record
114+
emit_kv(io, string(k), v)
115+
end
116+
println(io)
117+
end
118+
119+
"""
120+
write_records_file(path, metadata, records, table_name; header=nothing)
121+
122+
Write a complete A2ML file with SPDX header, [metadata] block, and
123+
array-of-tables `[[table_name]]` entries.
124+
"""
125+
function write_records_file(
126+
path::AbstractString,
127+
metadata::AbstractDict,
128+
records::AbstractVector,
129+
table_name::AbstractString;
130+
header::Union{AbstractString,Nothing}=nothing,
131+
)
132+
open(path, "w") do io
133+
println(io, "# SPDX-License-Identifier: PMPL-1.0-or-later")
134+
println(io, "# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath)")
135+
if header !== nothing
136+
for line in split(header, '\n')
137+
println(io, "# ", line)
138+
end
139+
end
140+
println(io)
141+
write_metadata_table(io, metadata)
142+
for rec in records
143+
write_record(io, table_name, rec)
144+
end
145+
end
146+
end
147+
148+
end # module

scripts/extract_mathcomp.jl

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
#!/usr/bin/env julia
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath)
4+
#
5+
# Mathematical Components (mathcomp) extractor for ECHIDNA training data.
6+
# Reads .v Coq/ssreflect source files and extracts Lemma/Theorem statements
7+
# together with their proofs (ssreflect tactics).
8+
#
9+
# Input: external_corpora/mathcomp/ (git clone of math-comp/math-comp)
10+
# Output: training_data/proof_states_mathcomp.a2ml
11+
# training_data/tactics_mathcomp.a2ml
12+
# training_data/stats_mathcomp.a2ml
13+
14+
using Dates
15+
include("a2ml_emit.jl")
16+
using .A2MLEmit
17+
18+
const ID_BASE = 200000
19+
const PROVER = "Coq" # mathcomp targets Rocq/Coq
20+
21+
# Match Lemma/Theorem/Corollary/Fact/Proposition/Remark <name> <binders>? : <stmt>.
22+
# We keep statement capture short-circuited at the first top-level period followed
23+
# by whitespace or newline — imperfect but fast.
24+
const STMT_PAT = r"(Lemma|Theorem|Corollary|Fact|Proposition|Remark)\s+([A-Za-z_][A-Za-z0-9_']*)\s*([^:.]*?):\s*(.*?)\.\s*\n"s
25+
26+
const PROOF_PAT = r"Proof\.(.*?)(Qed|Defined|Admitted)\s*\."s
27+
28+
"""
29+
split_tactics(proof_body::AbstractString) -> Vector{String}
30+
31+
Split ssreflect proof body into tactic steps at top-level periods.
32+
Balanced brackets are respected so `case: (foo x).` stays together.
33+
"""
34+
function split_tactics(proof_body::AbstractString)::Vector{String}
35+
tactics = String[]
36+
buf = IOBuffer()
37+
depth = 0
38+
for c in proof_body
39+
if c == '(' || c == '[' || c == '{'
40+
depth += 1; print(buf, c)
41+
elseif c == ')' || c == ']' || c == '}'
42+
depth = max(0, depth - 1); print(buf, c)
43+
elseif c == '.' && depth == 0
44+
s = strip(String(take!(buf)))
45+
isempty(s) || push!(tactics, s)
46+
else
47+
print(buf, c)
48+
end
49+
end
50+
s = strip(String(take!(buf)))
51+
isempty(s) || push!(tactics, s)
52+
return tactics
53+
end
54+
55+
function find_v_files(base_dir::String)::Vector{String}
56+
files = String[]
57+
for (root, _dirs, fs) in walkdir(base_dir)
58+
for f in fs
59+
endswith(f, ".v") && push!(files, joinpath(root, f))
60+
end
61+
end
62+
sort!(files)
63+
return files
64+
end
65+
66+
function extract_all(base_dir::String)
67+
files = find_v_files(base_dir)
68+
println("Found $(length(files)) .v files in $base_dir")
69+
70+
proof_states = Dict{String,Any}[]
71+
tactics = Dict{String,Any}[]
72+
skipped_noproof = 0
73+
errors = 0
74+
75+
for (idx, fpath) in enumerate(files)
76+
content = try
77+
read(fpath, String)
78+
catch
79+
errors += 1
80+
continue
81+
end
82+
83+
theorem_matches = try
84+
collect(eachmatch(STMT_PAT, content))
85+
catch
86+
errors += 1
87+
continue
88+
end
89+
90+
for tm in theorem_matches
91+
kind, name, binders, stmt = tm.captures
92+
record_id = ID_BASE + length(proof_states)
93+
94+
# Look for Proof block immediately after the theorem statement
95+
proof_start = tm.offset + length(tm.match)
96+
rest = content[min(proof_start, lastindex(content)):end]
97+
pm = try
98+
match(PROOF_PAT, rest)
99+
catch
100+
nothing
101+
end
102+
103+
if pm === nothing
104+
skipped_noproof += 1
105+
continue
106+
end
107+
108+
proof_body = pm.captures[1]
109+
terminator = pm.captures[2]
110+
steps = split_tactics(proof_body)
111+
isempty(steps) && continue
112+
113+
theorem = String(strip(name))
114+
goal = String(strip(stmt))
115+
# Join binders into goal if present
116+
bstr = String(strip(binders))
117+
isempty(bstr) || (goal = bstr * " : " * goal)
118+
119+
rel = relpath(fpath, base_dir)
120+
push!(proof_states, Dict{String,Any}(
121+
"id" => record_id,
122+
"prover" => PROVER,
123+
"theorem" => theorem,
124+
"goal" => goal,
125+
"context" => String[],
126+
"source" => "mathcomp",
127+
"kind" => String(kind),
128+
"terminator" => String(terminator),
129+
"file" => rel,
130+
"proof_steps" => length(steps),
131+
))
132+
133+
for (step_idx, tac) in enumerate(steps)
134+
push!(tactics, Dict{String,Any}(
135+
"proof_id" => record_id,
136+
"step" => step_idx,
137+
"tactic" => tac,
138+
"prover" => PROVER,
139+
))
140+
end
141+
end
142+
143+
idx % 200 == 0 && println(" processed $idx/$(length(files)) files ...")
144+
end
145+
146+
id_range = isempty(proof_states) ? "none" : "$(ID_BASE)-$(ID_BASE + length(proof_states) - 1)"
147+
stats = Dict{String,Any}(
148+
"version" => "v2.0-mathcomp",
149+
"extraction_date" => Dates.format(now(), dateformat"yyyy-mm-ddTHH:MM:SS"),
150+
"total_files_scanned" => length(files),
151+
"total_proofs" => length(proof_states),
152+
"total_tactics" => length(tactics),
153+
"skipped_no_proof" => skipped_noproof,
154+
"read_errors" => errors,
155+
"source" => "Mathematical Components (math-comp/math-comp)",
156+
"id_range" => id_range,
157+
)
158+
return proof_states, tactics, stats
159+
end
160+
161+
function save_results(proof_states, tactics, stats; output_dir="training_data")
162+
mkpath(output_dir)
163+
write_records_file(
164+
joinpath(output_dir, "proof_states_mathcomp.a2ml"),
165+
stats, proof_states, "proof-state";
166+
header="mathcomp proof-state records (Coq/ssreflect training data)")
167+
write_records_file(
168+
joinpath(output_dir, "tactics_mathcomp.a2ml"),
169+
stats, tactics, "tactic";
170+
header="mathcomp tactic records (ssreflect tactics per step)")
171+
open(joinpath(output_dir, "stats_mathcomp.a2ml"), "w") do fh
172+
println(fh, "# SPDX-License-Identifier: PMPL-1.0-or-later")
173+
println(fh, "# mathcomp extraction statistics"); println(fh)
174+
A2MLEmit.write_metadata_table(fh, stats)
175+
end
176+
println("\nSaved $(length(proof_states)) proof states, $(length(tactics)) tactics -> $output_dir/*_mathcomp.a2ml")
177+
end
178+
179+
function main()::Int
180+
println("=" ^ 60)
181+
println("ECHIDNA Mathcomp Extractor")
182+
println("=" ^ 60)
183+
base_dir = "external_corpora/mathcomp"
184+
if !isdir(base_dir)
185+
println("ERROR: Corpus directory not found: $base_dir")
186+
return 1
187+
end
188+
proof_states, tactics, stats = extract_all(base_dir)
189+
if isempty(proof_states)
190+
println("\nWARNING: No proof states extracted.")
191+
return 1
192+
end
193+
save_results(proof_states, tactics, stats)
194+
println("\nTotal: $(stats["total_proofs"]) proofs / $(stats["total_tactics"]) tactics (IDs $(stats["id_range"]))")
195+
return 0
196+
end
197+
198+
if abspath(PROGRAM_FILE) == @__FILE__
199+
exit(main())
200+
end

0 commit comments

Comments
 (0)