ChallengeGen
One declaration of a compiled Lean project, turned into a file that compiles on its own: its
transitive dependencies inlined, its proofs replaced by sorry, its imports cut down to the
external frontier. One such file is a self-contained statement of one problem — which is what makes
it usable as a challenge for something that has to produce the proof.
Depends on Lean core and MeaningGraph, and
nothing else. It was extracted from the
exposition repository, whose referee tool
is its first consumer, and is a package of its own so that generating challenges does not drag in
that tool's build (Verso, SubVerso, MD4Lean, …).
Two tiers
Both take the same input and differ only in how they render a declaration.
writeAllExtractions— the readable tier. Copies the verbatim source text of each declaration and replays the surroundingnamespace/open/variable/notation context, so notation survives and the file reads the way a mathematician wrote it. Needs the project's source files on disk.Flat.writeAllFlatExtractions— the robust tier. Renders each declaration from itsConstantInfo: fully qualified,@-explicit, no notation, no instance search, no context to replay. Never opens a source file. Gives up readability and, with it, the entire class of context-replay failures.
The intended use is both: prefer the readable file, fall back to the flat one for the declarations whose readable version does not compile.
What you have to supply
structure ChallengeDecl where
name : Name
kind : DeclKind
moduleName : Name
transDeps : Array Name := #[]
Four fields, and that is the whole interface. This package does not decide which declarations
are worth extracting, and it does not compute or choose the dependency closure — a closure that
dropped a lemma some kept tactic block calls would produce a file that does not compile, so the
edge policy belongs to whoever knows what the files are for. Take the closure over edges that keep
proofs; MeaningGraph.transitiveDeps is what computes it.
Both entry points also need a live Environment with the project imported, so a tool built on this
runs inside the target project's lake env, the way referee extract does.
import ChallengeGen
open Lean ChallengeGen
def writeChallenges (env : Environment) (root : Name) (decls : Array ChallengeDecl)
(projectDir out : System.FilePath) : IO Nat :=
writeAllExtractions env root decls projectDir out
Each file is named <anchorIdOf decl.name>.lean. anchorIdOf is exported for exactly that reason:
a tool that links to these files has to compute the stem the same way the writer does, so there is
one definition of it and both sides import it.
Checks
lake build ChallengeGenTest runs the #guards: the pure string and syntax helpers, and the name
mapping that decides what a file is called. They are elaboration-time, so building the target is
running them.
The bulk of the extraction is exercised end to end against real projects instead — constructing a
synthetic Environment for those paths is impractical, and what actually matters is whether the
files compile. The consumer this was extracted from measures that with a script that runs
lake env lean over every generated file.
The test module is ChallengeGen.Test, not Test: module roots are shared across a whole Lake
workspace, so a package that claims the top-level name Test takes it away from every project that
requires it.