visualproof 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +99 -2
- package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
- package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
- package/dist/assets/latex/visualproof.sty +308 -0
- package/dist/extractor/Extract.lean +1180 -0
- package/dist/extractor/Usage.lean +900 -0
- package/dist/mcp/visualproof-mcp.mjs +476 -0
- package/dist/server/access.js +95 -0
- package/dist/server/agent.js +3235 -0
- package/dist/server/agentdocs.js +35 -0
- package/dist/server/agentlibrary.js +202 -0
- package/dist/server/agentnotes.js +701 -0
- package/dist/server/agentviews.js +703 -0
- package/dist/server/askfollow.js +286 -0
- package/dist/server/boardspots.js +159 -0
- package/dist/server/dirs.js +175 -0
- package/dist/server/doctor.js +186 -0
- package/dist/server/events.js +198 -0
- package/dist/server/export.js +420 -0
- package/dist/server/extract.js +204 -0
- package/dist/server/fetchurl.js +321 -0
- package/dist/server/figures.js +158 -0
- package/dist/server/history.js +741 -0
- package/dist/server/historyapi.js +304 -0
- package/dist/server/hover.js +595 -0
- package/dist/server/identity.js +114 -0
- package/dist/server/invites.js +241 -0
- package/dist/server/layoutthread.js +51 -0
- package/dist/server/library.js +600 -0
- package/dist/server/libraryroute.js +211 -0
- package/dist/server/live.js +1101 -0
- package/dist/server/lostfound.js +199 -0
- package/dist/server/main.js +1394 -0
- package/dist/server/mcptools.js +497 -0
- package/dist/server/pastdoc.js +73 -0
- package/dist/server/pipeline.js +156 -0
- package/dist/server/poppler.js +251 -0
- package/dist/server/presence.js +252 -0
- package/dist/server/project.js +222 -0
- package/dist/server/prompt.js +53 -0
- package/dist/server/readonly.js +152 -0
- package/dist/server/readonlygate.js +123 -0
- package/dist/server/renames.js +268 -0
- package/dist/server/review.js +334 -0
- package/dist/server/reviewapi.js +188 -0
- package/dist/server/snapshot.js +49 -0
- package/dist/server/sourcestore.js +473 -0
- package/dist/server/status.js +360 -0
- package/dist/server/talk.js +476 -0
- package/dist/server/tokens.js +221 -0
- package/dist/server/usage.js +501 -0
- package/dist/server/versions.js +82 -0
- package/dist/server/view.js +157 -0
- package/dist/server/views.js +1167 -0
- package/dist/shared/askfollow.js +46 -0
- package/dist/shared/board.js +332 -0
- package/dist/shared/build.js +2 -0
- package/dist/shared/collab.js +148 -0
- package/dist/shared/comment.js +285 -0
- package/dist/shared/context.js +157 -0
- package/dist/shared/doc/authors.js +98 -0
- package/dist/shared/doc/build.js +358 -0
- package/dist/shared/doc/latex.js +455 -0
- package/dist/shared/doc/markdown.js +159 -0
- package/dist/shared/doc/model.js +37 -0
- package/dist/shared/doc/order.js +107 -0
- package/dist/shared/doc/proof.js +272 -0
- package/dist/shared/doc/sections.js +93 -0
- package/dist/shared/doc/settings.js +75 -0
- package/dist/shared/edgecodec.js +102 -0
- package/dist/shared/editor.js +64 -0
- package/dist/shared/excerpt.js +87 -0
- package/dist/shared/graph.js +16 -0
- package/dist/shared/items.js +92 -0
- package/dist/shared/keys.js +203 -0
- package/dist/shared/layout.js +926 -0
- package/dist/shared/lexer.js +130 -0
- package/dist/shared/library.js +711 -0
- package/dist/shared/lostfound.js +150 -0
- package/dist/shared/moves.js +243 -0
- package/dist/shared/notecard.js +295 -0
- package/dist/shared/noteplace.js +505 -0
- package/dist/shared/palette.js +46 -0
- package/dist/shared/pastgraph.js +95 -0
- package/dist/shared/places.js +80 -0
- package/dist/shared/projectnotes.js +244 -0
- package/dist/shared/query.js +810 -0
- package/dist/shared/review.js +642 -0
- package/dist/shared/reviewqueue.js +333 -0
- package/dist/shared/ring.js +366 -0
- package/dist/shared/scope.js +255 -0
- package/dist/shared/scopeedit.js +81 -0
- package/dist/shared/search.js +191 -0
- package/dist/shared/separate.js +178 -0
- package/dist/shared/sharesnap.js +280 -0
- package/dist/shared/sticky.js +501 -0
- package/dist/shared/timeline.js +73 -0
- package/dist/shared/titles.js +298 -0
- package/dist/shared/viewgraph.js +139 -0
- package/dist/shared/views.js +859 -0
- package/dist/web/bundle/app.js +48733 -0
- package/dist/web/bundle/edges.js +409 -0
- package/dist/web/bundle/layout.js +1010 -0
- package/dist/web/bundle/pdfworker.js +48322 -0
- package/dist/web/index.html +239 -0
- package/dist/web/src/motion.js +430 -0
- package/dist/web/src/past.js +224 -0
- package/dist/web/src/scene.js +787 -0
- package/dist/web/style.css +2312 -0
- package/docs/agent-api/README.md +91 -0
- package/docs/agent-api/advice.md +108 -0
- package/docs/agent-api/export.md +120 -0
- package/docs/agent-api/following.md +140 -0
- package/docs/agent-api/history.md +142 -0
- package/docs/agent-api/library.md +65 -0
- package/docs/agent-api/mcp.md +104 -0
- package/docs/agent-api/notes.md +93 -0
- package/docs/agent-api/reading.md +166 -0
- package/docs/agent-api/requests.md +94 -0
- package/docs/agent-api/talking.md +100 -0
- package/docs/agent-api/tcb.md +135 -0
- package/docs/agent-api/views.md +124 -0
- package/docs/agent-api/words.md +56 -0
- package/package.json +39 -4
- package/test/toolchains.json +5 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
/-
|
|
2
|
+
VisualProof extractor: a built Lean project → one JSON graph.
|
|
3
|
+
|
|
4
|
+
Run inside the target project's environment, so the target's own toolchain reads its own
|
|
5
|
+
`.olean` files:
|
|
6
|
+
|
|
7
|
+
cd <target> && lake env lean --run <this file> <out.json> <project-lib-dir> <root-module>...
|
|
8
|
+
|
|
9
|
+
`project-lib-dir` is the target's `.lake/build/lib/lean`: a module is part of the project when its
|
|
10
|
+
`.olean` lives there, everything else (Mathlib, core, other packages) is external.
|
|
11
|
+
|
|
12
|
+
The program only reads. It never builds and never writes anywhere but `out.json`.
|
|
13
|
+
|
|
14
|
+
Output, schema 1:
|
|
15
|
+
nodes the units of the project: the declarations written in its source (TCBSpec.md,
|
|
16
|
+
decision 1). Constants the compiler generates (constructors, recursors, matchers,
|
|
17
|
+
`proof_n`, equation lemmas, projections, …) are folded into the declaration that owns
|
|
18
|
+
them, and so are the declarations Lean generates from a command (what a declaring macro
|
|
19
|
+
writes beside the declaration it names, the lemmas of `@[simps]`): those are listed in the unit's `also`. Meta-level
|
|
20
|
+
declarations (elaborators, macros and the helpers they call) are units like any
|
|
21
|
+
other; a syntax extension command (`syntax`, `notation`, `macro`, `macro_rules`,
|
|
22
|
+
`elab`, `elab_rules`, `declare_syntax_cat`) is one unit of kind `notation`, made of
|
|
23
|
+
the parser and `_aux_` declarations it generates (decision 2), under a name of its
|
|
24
|
+
own: the syntax kind it declares, or `<kind>.macro_rules` and the like for a rule
|
|
25
|
+
(`leanName` is the Lean constant, `display` what a card calls it).
|
|
26
|
+
externals constants outside the project that a node refers to directly, folded the same way.
|
|
27
|
+
edges [src, dst, sites], `sites` a bit set: 1 = statement, 2 = definition body,
|
|
28
|
+
4 = inside a proof, 8 = on grammar: into a notation unit that only declares syntax
|
|
29
|
+
(flagged `grammar`), which no TCB follows. A macro or elaborator depends on the
|
|
30
|
+
syntax it interprets at site 8 (`interprets`). Which notation a declaration is
|
|
31
|
+
written in is not here: the usage pass (extractor/Usage.lean) finds it, and the
|
|
32
|
+
server adds those edges. A reference is "inside a proof" when the subterm it occurs in
|
|
33
|
+
has a type that is a proposition (`Meta.isProof`), or is an instance argument of a
|
|
34
|
+
class whose instances are all equal (`instArgs`); by proof irrelevance, and
|
|
35
|
+
by that equality, such a reference cannot change what a statement means.
|
|
36
|
+
axioms every axiom reachable from some node, through everything including proofs;
|
|
37
|
+
`axiomSets[i]` is a list of indices into it, and each node's `ax` points there.
|
|
38
|
+
A node's set is the one `#print axioms` gives for it (for a type, see `axiomMask`),
|
|
39
|
+
joined over its unit: the node and what it folds in `also`.
|
|
40
|
+
versions each node's `hash` is the kernel's content hash over its unit (decision 3 before
|
|
41
|
+
2026-10-09); its `shape` is the same content with every constant it names replaced by
|
|
42
|
+
its place in the node's `refs`, indices into `refNames`: a constant of the project as
|
|
43
|
+
its unit's name, then a space and what it is of that unit (`A.Point mk`), one from
|
|
44
|
+
outside by its own name. The server maps those names through the renames in force
|
|
45
|
+
and hashes them with the shape: the version an opinion is bound to.
|
|
46
|
+
-/
|
|
47
|
+
import Lean
|
|
48
|
+
|
|
49
|
+
open Lean Meta
|
|
50
|
+
|
|
51
|
+
namespace VisualProof
|
|
52
|
+
|
|
53
|
+
/-- Where a reference occurs. -/
|
|
54
|
+
abbrev Site := UInt8
|
|
55
|
+
def stmt : Site := 1
|
|
56
|
+
def body : Site := 2
|
|
57
|
+
def proof : Site := 4
|
|
58
|
+
/-- Where a statement or a body is written with a syntax that only fixes the grammar (`syntax`,
|
|
59
|
+
`declare_syntax_cat`): what the tree means is its rule's business, which a use reaches on its
|
|
60
|
+
own, so a TCB does not follow it, as it does not follow a proof (TCBSpec.md, decision 2). -/
|
|
61
|
+
def grammar : Site := 8
|
|
62
|
+
|
|
63
|
+
/-- A reference seen through a generated constant keeps its position, unless either step
|
|
64
|
+
passes through a proof. -/
|
|
65
|
+
def combine (outer inner : Site) : Site :=
|
|
66
|
+
if outer == proof || inner == proof then proof else outer
|
|
67
|
+
|
|
68
|
+
/-- Declarations that exist for the elaborator, not for the kernel's terms: parsers, macros,
|
|
69
|
+
elaborators, delaborators, simprocs, and the code they run in (`MacroM`, `MetaM`, syntax).
|
|
70
|
+
No statement refers to them, but notation is read through them (TCBSpec.md, decision 2), so
|
|
71
|
+
they are units like any other, flagged `meta`. -/
|
|
72
|
+
def metaHeads : List Name :=
|
|
73
|
+
[``Lean.ParserDescr, ``Lean.TrailingParserDescr, ``Lean.Macro, ``Lean.Parser.Parser,
|
|
74
|
+
``Lean.Parser.TrailingParser, ``Lean.Elab.Term.TermElab, ``Lean.Elab.Tactic.Tactic,
|
|
75
|
+
``Lean.Elab.Command.CommandElab, ``Lean.PrettyPrinter.Delaborator.Delab,
|
|
76
|
+
``Lean.PrettyPrinter.Unexpander, ``Lean.Meta.Simp.Simproc, ``Lean.Meta.Simp.DSimproc,
|
|
77
|
+
``Lean.Elab.Term.Do.ToCodeBlock.Catch, ``Lean.MacroM, ``Lean.Meta.MetaM, ``Lean.Core.CoreM,
|
|
78
|
+
``Lean.Syntax, ``Lean.TSyntax, ``Lean.Parser.Category]
|
|
79
|
+
|
|
80
|
+
def isMetaDecl (ci : ConstantInfo) : Bool :=
|
|
81
|
+
match ci.type.getForallBody.getAppFn.constName? with
|
|
82
|
+
| some h => metaHeads.contains h || (`Lean.Elab).isPrefixOf h || (`Lean.Parser).isPrefixOf h
|
|
83
|
+
| none => false
|
|
84
|
+
|
|
85
|
+
/-- What a syntax extension command generates: the parser of a syntax kind (`ParserDescr`), a
|
|
86
|
+
syntax category, and the `_aux_` declarations of `macro_rules`, `elab_rules` and `notation`
|
|
87
|
+
(the macro, the elaborator, the unexpander). The constants one command generates make one
|
|
88
|
+
unit of kind `notation` (TCBSpec.md, decision 2). -/
|
|
89
|
+
def isNotationConst (ci : ConstantInfo) : Bool :=
|
|
90
|
+
let u := (privateToUserName? ci.name).getD ci.name
|
|
91
|
+
let generatedAux := match u with
|
|
92
|
+
| .str _ s => s.startsWith "_aux_"
|
|
93
|
+
| _ => false
|
|
94
|
+
generatedAux || match ci.type.getForallBody.getAppFn.constName? with
|
|
95
|
+
| some h => h == ``Lean.ParserDescr || h == ``Lean.TrailingParserDescr || h == ``Lean.Parser.Category
|
|
96
|
+
| none => false
|
|
97
|
+
|
|
98
|
+
/-- Suffixes of constants the compiler generates next to a user declaration, which still carry
|
|
99
|
+
a declaration range (so the range test alone does not catch them). -/
|
|
100
|
+
def generatedSuffixes : List String :=
|
|
101
|
+
["recOn", "casesOn", "noConfusion", "noConfusionType", "ctorElim", "ctorElimType", "below",
|
|
102
|
+
"brecOn", "binductionOn", "ibelow", "ctorIdx", "toCtorIdx", "ofNat", "ofNat_ctorIdx",
|
|
103
|
+
"sizeOf_spec", "injEq", "inj", "eq_def", "induct", "mutual_induct", "fun_cases",
|
|
104
|
+
"induct_unfolding", "ext_iff", "congr_simp"]
|
|
105
|
+
|
|
106
|
+
structure St where
|
|
107
|
+
/-- `env.header.moduleNames`, read once: in v4.33 it builds a new array on every call -/
|
|
108
|
+
modNames : Array Name := #[]
|
|
109
|
+
/-- project constant → owning node (itself when it is a node) -/
|
|
110
|
+
owner : Std.HashMap Name Name := {}
|
|
111
|
+
/-- per-constant references, computed once -/
|
|
112
|
+
refs : Std.HashMap Name (Array (Name × Site)) := {}
|
|
113
|
+
/-- transitive axiom mask per constant -/
|
|
114
|
+
axMask : Std.HashMap Name Nat := {}
|
|
115
|
+
axioms : Array Name := #[]
|
|
116
|
+
/-- printed statements, per node -/
|
|
117
|
+
pp : Std.HashMap Name String := {}
|
|
118
|
+
/-- modules whose cache entry is out of date: something of theirs was computed this run -/
|
|
119
|
+
dirty : Std.HashSet Name := {}
|
|
120
|
+
|
|
121
|
+
abbrev M := StateRefT St MetaM
|
|
122
|
+
|
|
123
|
+
/-- `isProof`, conservative: a term whose type cannot be inferred counts as data, so a failure
|
|
124
|
+
can only enlarge a TCB, never hide part of it. -/
|
|
125
|
+
def isProofSafe (e : Expr) : MetaM Bool := do
|
|
126
|
+
if let .const c _ := e.getAppFn then
|
|
127
|
+
if let some (.thmInfo _) := (← getEnv).find? c then return true
|
|
128
|
+
try isProof e catch _ => return false
|
|
129
|
+
|
|
130
|
+
/-- What the walk keeps for the whole run: whether a class's instances are all equal
|
|
131
|
+
(`subsingletonClass`), by its name; and which arguments of a constant are instances of such a
|
|
132
|
+
class (`instArgs`), by the constant's name. -/
|
|
133
|
+
structure WalkCache where
|
|
134
|
+
classes : IO.Ref (Std.HashMap Name Bool)
|
|
135
|
+
instArgs : IO.Ref (Std.HashMap Name (Array Bool))
|
|
136
|
+
|
|
137
|
+
def WalkCache.new : IO WalkCache := return { classes := ← IO.mkRef {}, instArgs := ← IO.mkRef {} }
|
|
138
|
+
|
|
139
|
+
/-- Whether all instances of class `c` are equal (TCBSpec.md, decision 5): `Subsingleton (c xs)`
|
|
140
|
+
holds for arbitrary arguments `xs`, as of `Decidable p`, `Fintype α`, `Unique α`. Asked once
|
|
141
|
+
per class, with a small budget; no when it cannot be told. It is the class that is asked, not
|
|
142
|
+
each of its uses: `OrderBot α` is a subsingleton when `α` is partially ordered, not over any
|
|
143
|
+
`≤`, so its instances, and the `⊥` they carry, stay. -/
|
|
144
|
+
def subsingletonClass (cache : WalkCache) (c : Name) : MetaM Bool := do
|
|
145
|
+
if let some b := (← cache.classes.get)[c]? then return b
|
|
146
|
+
let b ← try
|
|
147
|
+
let some ci := (← getEnv).find? c | pure false
|
|
148
|
+
forallTelescope ci.type fun xs _ => do
|
|
149
|
+
let ty := mkAppN (.const c (ci.levelParams.map mkLevelParam)) xs
|
|
150
|
+
let u ← getLevel ty
|
|
151
|
+
withOptions (fun o => (o.set `synthInstance.maxHeartbeats (2000 : Nat)).set `synthInstance.maxSize (64 : Nat)) do
|
|
152
|
+
return (← synthInstance? (mkApp (.const ``Subsingleton [u]) ty)).isSome
|
|
153
|
+
catch _ => pure false
|
|
154
|
+
cache.classes.modify (·.insert c b)
|
|
155
|
+
return b
|
|
156
|
+
|
|
157
|
+
/-- Whether instances of type `ty` are all equal, by its class: the head of what it says under
|
|
158
|
+
its binders (`DecidableEq α` unfolds to `∀ a b, Decidable (a = b)`, of class `Decidable`).
|
|
159
|
+
A pi type of such instances is one too. -/
|
|
160
|
+
def subsingletonType (cache : WalkCache) (ty : Expr) : MetaM Bool := do
|
|
161
|
+
try forallTelescopeReducing ty fun _ b => do
|
|
162
|
+
match b.getAppFn with
|
|
163
|
+
| .const c _ => subsingletonClass cache c
|
|
164
|
+
| _ => return false
|
|
165
|
+
catch _ => return false
|
|
166
|
+
|
|
167
|
+
/-- Whether declaration `c` is an instance of a class whose instances are all equal
|
|
168
|
+
(`instance [DecidableEq α] : DecidableEq (List α)`). -/
|
|
169
|
+
def subsingletonDecl (cache : WalkCache) (c : Name) : MetaM Bool := do
|
|
170
|
+
let env ← getEnv
|
|
171
|
+
let some ci := env.find? c | return false
|
|
172
|
+
if !Meta.isInstanceCore env c then return false
|
|
173
|
+
subsingletonType cache ci.type
|
|
174
|
+
|
|
175
|
+
/-- Which arguments of `f` are instances of a class whose instances are all equal (TCBSpec.md,
|
|
176
|
+
decision 5): instance-implicit binders of the constant's type whose class is such a class
|
|
177
|
+
(`[DecidableEq α]`, `[Fintype α]`, `[Decidable p]`). Which instance was chosen there cannot
|
|
178
|
+
change what a statement says, and the kernel checks that there is one, so the walk leaves it
|
|
179
|
+
out as it does a proof. A class that carries data (`Add`, `Monad`, `Inhabited` and its default
|
|
180
|
+
value) is no such class, and its instances stay. Read once per constant from the binders its
|
|
181
|
+
type is written with; for anything but a constant (a local function, a projection), and past
|
|
182
|
+
those binders, none: conservative, as `isProofSafe` is, a failure keeps the instance. -/
|
|
183
|
+
def instArgs (cache : WalkCache) (f : Expr) : MetaM (Array Bool) := do
|
|
184
|
+
let .const c _ := f | return #[]
|
|
185
|
+
if let some a := (← cache.instArgs.get)[c]? then return a
|
|
186
|
+
let some ci := (← getEnv).find? c | return #[]
|
|
187
|
+
let out ← try
|
|
188
|
+
forallTelescope ci.type fun xs _ => xs.mapM fun x => do
|
|
189
|
+
if !(← x.fvarId!.getBinderInfo).isInstImplicit then return false
|
|
190
|
+
subsingletonType cache (← x.fvarId!.getType)
|
|
191
|
+
catch _ => pure #[]
|
|
192
|
+
cache.instArgs.modify (·.insert c out)
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
/-- References of one expression, each tagged with its site. A subterm met again at the same site
|
|
196
|
+
adds nothing, and is not walked again (`seen`): Lean's terms share their subterms, and a term
|
|
197
|
+
can hold one many times over, so that walking it as a tree takes time, and asks Lean the type
|
|
198
|
+
of subterms, in proportion to its size written out, which can be exponential in its size in
|
|
199
|
+
memory. -/
|
|
200
|
+
partial def walk (cache : WalkCache) (seen : IO.Ref (Std.HashSet (Expr × Site))) (e : Expr) (site : Site) (acc : IO.Ref (Array (Name × Site))) : MetaM Unit := do
|
|
201
|
+
let note (c : Name) (s : Site) : MetaM Unit := acc.modify (·.push (c, s))
|
|
202
|
+
match e with
|
|
203
|
+
| .bvar .. | .fvar .. | .mvar .. | .sort .. | .lit .. => pure ()
|
|
204
|
+
| .mdata _ b => walk cache seen b site acc
|
|
205
|
+
| _ =>
|
|
206
|
+
if (← seen.get).contains (e, site) then return
|
|
207
|
+
seen.modify (·.insert (e, site))
|
|
208
|
+
if site != proof && (← isProofSafe e) then
|
|
209
|
+
for c in e.getUsedConstants do note c proof
|
|
210
|
+
return
|
|
211
|
+
match e with
|
|
212
|
+
| .const c _ => note c site
|
|
213
|
+
| .app .. =>
|
|
214
|
+
let f := e.getAppFn
|
|
215
|
+
let args := e.getAppArgs
|
|
216
|
+
walk cache seen f site acc
|
|
217
|
+
let inst ← instArgs cache f
|
|
218
|
+
for h : k in [0:args.size] do
|
|
219
|
+
let a := args[k]
|
|
220
|
+
-- an instance of a class whose instances are all equal counts as a proof does
|
|
221
|
+
if site != proof && inst[k]?.getD false then
|
|
222
|
+
for c in a.getUsedConstants do note c proof
|
|
223
|
+
else walk cache seen a site acc
|
|
224
|
+
| .lam n t b bi | .forallE n t b bi =>
|
|
225
|
+
walk cache seen t site acc
|
|
226
|
+
withLocalDecl n bi t fun x => walk cache seen (b.instantiate1 x) site acc
|
|
227
|
+
| .letE n t v b _ =>
|
|
228
|
+
walk cache seen t site acc
|
|
229
|
+
walk cache seen v site acc
|
|
230
|
+
withLetDecl n t v fun x => walk cache seen (b.instantiate1 x) site acc
|
|
231
|
+
| .proj s _ b =>
|
|
232
|
+
note s site
|
|
233
|
+
walk cache seen b site acc
|
|
234
|
+
| _ => pure ()
|
|
235
|
+
|
|
236
|
+
def walkTop (cache : WalkCache) (e : Expr) (site : Site) : MetaM (Array (Name × Site)) := do
|
|
237
|
+
let acc ← IO.mkRef #[]
|
|
238
|
+
if site == proof then
|
|
239
|
+
return e.getUsedConstants.map (·, proof)
|
|
240
|
+
walk cache (← IO.mkRef {}) e site acc
|
|
241
|
+
acc.get
|
|
242
|
+
|
|
243
|
+
/-- The references a constant makes on its own, before any folding. -/
|
|
244
|
+
def ownRefs (cache : WalkCache) (ci : ConstantInfo) : MetaM (Array (Name × Site)) := do
|
|
245
|
+
let env ← getEnv
|
|
246
|
+
let ty ← walkTop cache ci.type stmt
|
|
247
|
+
match ci with
|
|
248
|
+
| .thmInfo v => return ty ++ (← walkTop cache v.value proof)
|
|
249
|
+
| .defnInfo v =>
|
|
250
|
+
-- auxiliary recursors are one recursor application; every constant they name is a genuine
|
|
251
|
+
-- reference. (Matchers are not: their case trees carry `HEq` casts, which are proofs.)
|
|
252
|
+
if isAuxRecursor env ci.name then
|
|
253
|
+
return ty ++ v.value.getUsedConstants.map (·, body)
|
|
254
|
+
return ty ++ (← walkTop cache v.value body)
|
|
255
|
+
| .opaqueInfo _ =>
|
|
256
|
+
-- a `partial def` is an opaque constant whose code is its `_unsafe_rec` twin: what the code
|
|
257
|
+
-- calls is what it runs (a notation's implementation, TCBSpec.md decision 2). Any other
|
|
258
|
+
-- opaque the kernel cannot unfold: its value means nothing to a statement.
|
|
259
|
+
if let some (.defnInfo r) := env.find? (ci.name ++ `_unsafe_rec) then
|
|
260
|
+
return ty ++ (← walkTop cache r.value body)
|
|
261
|
+
return ty
|
|
262
|
+
| .inductInfo v =>
|
|
263
|
+
let mut out := ty
|
|
264
|
+
for c in v.ctors do
|
|
265
|
+
if let some cci := env.find? c then out := out ++ (← walkTop cache cci.type stmt)
|
|
266
|
+
return out
|
|
267
|
+
| _ => return ty
|
|
268
|
+
|
|
269
|
+
def moduleOf (modNames : Array Name) (env : Environment) (c : Name) : Name :=
|
|
270
|
+
match env.getModuleIdxFor? c with
|
|
271
|
+
| some i => modNames[i.toNat]!
|
|
272
|
+
| none => .anonymous
|
|
273
|
+
|
|
274
|
+
def refsOf (cache : WalkCache) (c : Name) : M (Array (Name × Site)) := do
|
|
275
|
+
if let some r := (← get).refs[c]? then return r
|
|
276
|
+
let env ← getEnv
|
|
277
|
+
let some ci := env.find? c | return #[]
|
|
278
|
+
let r ← ownRefs cache ci
|
|
279
|
+
-- what Lean inferred on the way (the types of subterms, under binders opened for this constant
|
|
280
|
+
-- alone) serves no other constant: kept, it grows with every constant walked, to tens of GB on
|
|
281
|
+
-- a large project
|
|
282
|
+
modifyThe Meta.State fun s => { s with cache := {} }
|
|
283
|
+
modify fun s => { s with refs := s.refs.insert c r, dirty := s.dirty.insert (moduleOf s.modNames env c) }
|
|
284
|
+
return r
|
|
285
|
+
|
|
286
|
+
/-! ## The per-module cache
|
|
287
|
+
|
|
288
|
+
A module's references and printed statements depend on its own `.olean` and on everything it
|
|
289
|
+
imports. So a module's key mixes the hash Lake wrote next to its `.olean` with the keys of the
|
|
290
|
+
project modules it imports, and a salt for the rest (toolchain, dependencies, this extractor).
|
|
291
|
+
An unchanged leaf of a large project is then read back instead of elaborated again; an edit to a
|
|
292
|
+
base module invalidates everything above it, as Lake's own rebuild does. -/
|
|
293
|
+
|
|
294
|
+
partial def nameComps : Name → List Json → List Json
|
|
295
|
+
| .anonymous, acc => acc
|
|
296
|
+
| .str p s, acc => nameComps p (Json.str s :: acc)
|
|
297
|
+
| .num p k, acc => nameComps p (toJson k :: acc)
|
|
298
|
+
|
|
299
|
+
/-- A name as its components; numbers stay numbers (private names carry one). -/
|
|
300
|
+
def encName (n : Name) : Json := Json.arr (nameComps n []).toArray
|
|
301
|
+
|
|
302
|
+
def decName (j : Json) : Option Name := do
|
|
303
|
+
let arr ← j.getArr?.toOption
|
|
304
|
+
arr.foldlM (init := Name.anonymous) fun n c =>
|
|
305
|
+
match c with
|
|
306
|
+
| .str s => some (n.str s)
|
|
307
|
+
| _ => c.getNat?.toOption.map (n.num ·)
|
|
308
|
+
|
|
309
|
+
def cacheFile (dir : System.FilePath) (m : Name) : System.FilePath := dir / s!"{m}.json"
|
|
310
|
+
|
|
311
|
+
def writeModuleCache (dir : System.FilePath) (m : Name) (key : String) (consts : Array Name) : M Unit := do
|
|
312
|
+
let st ← get
|
|
313
|
+
let mut index : Std.HashMap Name Nat := {}
|
|
314
|
+
let mut names : Array Json := #[]
|
|
315
|
+
let mut rows : Array Json := #[]
|
|
316
|
+
for c in consts do
|
|
317
|
+
let refs? := st.refs[c]?
|
|
318
|
+
let pp? := st.pp[c]?
|
|
319
|
+
if refs?.isNone && pp?.isNone then continue
|
|
320
|
+
for n in c :: ((refs?.getD #[]).map (·.1)).toList do
|
|
321
|
+
unless index.contains n do
|
|
322
|
+
index := index.insert n names.size
|
|
323
|
+
names := names.push (encName n)
|
|
324
|
+
let refsJs := match refs? with
|
|
325
|
+
| some rs => Json.arr (rs.foldl (fun a (t, s) => a.push (toJson index[t]!) |>.push (toJson s.toNat)) #[])
|
|
326
|
+
| none => Json.null
|
|
327
|
+
rows := rows.push (Json.arr #[toJson index[c]!, refsJs, (pp?.map Json.str).getD Json.null])
|
|
328
|
+
IO.FS.writeFile (cacheFile dir m)
|
|
329
|
+
(Json.mkObj [("key", key), ("names", Json.arr names), ("consts", Json.arr rows)]).compress
|
|
330
|
+
|
|
331
|
+
/-- Load a module's cache entry if its key still matches. -/
|
|
332
|
+
def readModuleCache (dir : System.FilePath) (m : Name) (key : String) : M Bool := do
|
|
333
|
+
let some text ← (try some <$> IO.FS.readFile (cacheFile dir m) catch _ => pure none) | return false
|
|
334
|
+
let .ok j := Json.parse text | return false
|
|
335
|
+
unless (j.getObjValAs? String "key").toOption == some key do return false
|
|
336
|
+
let .ok names := j.getObjValAs? (Array Json) "names" | return false
|
|
337
|
+
let .ok rows := j.getObjValAs? (Array Json) "consts" | return false
|
|
338
|
+
let decoded := names.map decName
|
|
339
|
+
let name? (x : Json) : Option Name := do decoded[(← x.getNat?.toOption)]?.join
|
|
340
|
+
for row in rows do
|
|
341
|
+
let .ok cells := row.getArr? | return false
|
|
342
|
+
let some c := cells[0]? >>= name? | return false
|
|
343
|
+
if let some (Json.arr flat) := cells[1]? then
|
|
344
|
+
let mut refs : Array (Name × Site) := #[]
|
|
345
|
+
for k in [0:flat.size / 2] do
|
|
346
|
+
let some t := name? flat[2 * k]! | return false
|
|
347
|
+
let .ok site := flat[2 * k + 1]!.getNat? | return false
|
|
348
|
+
refs := refs.push (t, site.toUInt8)
|
|
349
|
+
modify fun s => { s with refs := s.refs.insert c refs }
|
|
350
|
+
if let some (Json.str t) := cells[2]? then
|
|
351
|
+
modify fun s => { s with pp := s.pp.insert c t }
|
|
352
|
+
return true
|
|
353
|
+
|
|
354
|
+
/-- Generated constants that stand for the type they belong to. -/
|
|
355
|
+
def isStructural (env : Environment) (c : Name) : Bool :=
|
|
356
|
+
env.find? c matches some (.ctorInfo _) || env.find? c matches some (.recInfo _)
|
|
357
|
+
|| isAuxRecursor env c || isNoConfusion env c || (env.getProjectionFnInfo? c).isSome
|
|
358
|
+
|
|
359
|
+
/-- The user-facing constant an external reference stands for: a constructor stands for its
|
|
360
|
+
type, a recursor or matcher for the declaration it was generated from. -/
|
|
361
|
+
partial def foldExternal (env : Environment) (c : Name) : Name := Id.run do
|
|
362
|
+
match env.find? c with
|
|
363
|
+
| some (.ctorInfo v) => return foldExternal env v.induct
|
|
364
|
+
| some (.recInfo _) => return foldExternal env c.getPrefix
|
|
365
|
+
| _ => pure ()
|
|
366
|
+
if isAuxRecursor env c || isNoConfusion env c || isMatcherCore env c then
|
|
367
|
+
return foldExternal env c.getPrefix
|
|
368
|
+
if let some info := env.getProjectionFnInfo? c then
|
|
369
|
+
if let some (.ctorInfo v) := env.find? info.ctorName then return foldExternal env v.induct
|
|
370
|
+
let u := (privateToUserName? c).getD c
|
|
371
|
+
-- a private one (a lemma Lean generated in the project for `PSum.casesOn`) by its name as written
|
|
372
|
+
let p := if (env.find? c.getPrefix).isSome then c.getPrefix else u.getPrefix
|
|
373
|
+
if (u.isInternalDetail || generatedSuffixes.contains (u.getString!)) && !p.isAnonymous
|
|
374
|
+
&& (env.find? p).isSome then
|
|
375
|
+
return foldExternal env p
|
|
376
|
+
return c
|
|
377
|
+
|
|
378
|
+
/-- What `#print axioms` follows out of a constant (`Lean.CollectAxioms.collect`): its type, the
|
|
379
|
+
value of a definition, a theorem or an opaque, and the constructors of an inductive type.
|
|
380
|
+
Not `ConstantInfo.value?`: it gives a theorem's value only when asked with `allowOpaque`, and
|
|
381
|
+
without it every axiom and `sorry` used only inside a proof would go unseen. -/
|
|
382
|
+
def axiomDeps (ci : ConstantInfo) : Array Name :=
|
|
383
|
+
match ci with
|
|
384
|
+
| .defnInfo v => v.type.getUsedConstants ++ v.value.getUsedConstants
|
|
385
|
+
| .thmInfo v => v.type.getUsedConstants ++ v.value.getUsedConstants
|
|
386
|
+
| .opaqueInfo v => v.type.getUsedConstants ++ v.value.getUsedConstants
|
|
387
|
+
| .inductInfo v => v.type.getUsedConstants ++ v.ctors.toArray
|
|
388
|
+
| .quotInfo _ => #[]
|
|
389
|
+
| _ => ci.type.getUsedConstants -- an axiom, a constructor, a recursor
|
|
390
|
+
|
|
391
|
+
/-- A constant whose axioms are being collected: what it refers to, how far through that it is,
|
|
392
|
+
and what it has found so far. -/
|
|
393
|
+
structure AxFrame where
|
|
394
|
+
c : Name
|
|
395
|
+
num : Nat
|
|
396
|
+
deps : Array Name
|
|
397
|
+
next : Nat := 0
|
|
398
|
+
low : Nat
|
|
399
|
+
mask : Nat
|
|
400
|
+
|
|
401
|
+
/-- Transitive axioms of a constant, as a bit set over `St.axioms`: everything `#print axioms`
|
|
402
|
+
follows, proofs included. Every constant is walked once for the whole run and its set kept.
|
|
403
|
+
The walk finds strongly connected components (Tarjan), because references go round in cycles
|
|
404
|
+
(an inductive type and its constructors, mutual types, unsafe recursion): a constant met again
|
|
405
|
+
while still open would otherwise be kept with what was found up to then, and every later
|
|
406
|
+
declaration reaching it through the kept set would miss the rest. Lean's own sets, computed
|
|
407
|
+
once per module, have exactly that gap for a few types (Mathlib's `Subgroup` shows no axioms,
|
|
408
|
+
its constructor `propext`), so for a type this can say more than `#print axioms`, never less.
|
|
409
|
+
Iterative, because chains through Mathlib's proofs are deeper than the interpreter's stack. -/
|
|
410
|
+
def axiomMask (root : Name) : M Nat := do
|
|
411
|
+
if let some m := (← get).axMask[root]? then return m
|
|
412
|
+
let env ← getEnv
|
|
413
|
+
-- taken out of the state, so the walk updates them in place rather than copying them
|
|
414
|
+
let (done, axioms) ← modifyGet fun s => ((s.axMask, s.axioms), { s with axMask := {}, axioms := #[] })
|
|
415
|
+
let mut done := done
|
|
416
|
+
let mut axioms := axioms
|
|
417
|
+
let mut nums : Std.HashMap Name Nat := {}
|
|
418
|
+
let mut open_ : Array Name := #[] -- visited, component not yet complete
|
|
419
|
+
let mut frames : Array AxFrame := #[]
|
|
420
|
+
let mut todo : Option Name := some root
|
|
421
|
+
while true do
|
|
422
|
+
if let some c := todo then
|
|
423
|
+
todo := none
|
|
424
|
+
let num := nums.size
|
|
425
|
+
nums := nums.insert c num
|
|
426
|
+
open_ := open_.push c
|
|
427
|
+
let ci? := env.find? c
|
|
428
|
+
let mut mask := 0
|
|
429
|
+
if let some (.axiomInfo _) := ci? then
|
|
430
|
+
let i := (axioms.idxOf? c).getD axioms.size
|
|
431
|
+
if i == axioms.size then axioms := axioms.push c
|
|
432
|
+
mask := 1 <<< i
|
|
433
|
+
frames := frames.push { c, num, deps := (ci?.map axiomDeps).getD #[], low := num, mask }
|
|
434
|
+
let some f := frames.back? | break
|
|
435
|
+
if h : f.next < f.deps.size then
|
|
436
|
+
let d := f.deps[f.next]
|
|
437
|
+
let mut f := { f with next := f.next + 1 }
|
|
438
|
+
if let some m := done[d]? then f := { f with mask := f.mask ||| m }
|
|
439
|
+
-- seen in this walk and not done: it is open, on a cycle through `f`
|
|
440
|
+
else if let some n := nums[d]? then f := { f with low := min f.low n }
|
|
441
|
+
else todo := some d
|
|
442
|
+
frames := frames.set! (frames.size - 1) f
|
|
443
|
+
else
|
|
444
|
+
frames := frames.pop
|
|
445
|
+
if f.low == f.num then
|
|
446
|
+
-- `f` closes a component: every member reaches every other, so all share its set
|
|
447
|
+
while h : open_.size > 0 do
|
|
448
|
+
let x := open_[open_.size - 1]
|
|
449
|
+
open_ := open_.pop
|
|
450
|
+
done := done.insert x f.mask
|
|
451
|
+
if x == f.c then break
|
|
452
|
+
if let some p := frames.back? then
|
|
453
|
+
frames := frames.set! (frames.size - 1) { p with mask := p.mask ||| f.mask, low := min p.low f.low }
|
|
454
|
+
modify fun s => { s with axMask := done, axioms }
|
|
455
|
+
return done[root]?.getD 0
|
|
456
|
+
|
|
457
|
+
def kindOf (env : Environment) (ci : ConstantInfo) : String :=
|
|
458
|
+
match ci with
|
|
459
|
+
| .thmInfo _ => "theorem"
|
|
460
|
+
| .axiomInfo _ => "axiom"
|
|
461
|
+
| .opaqueInfo _ => "opaque"
|
|
462
|
+
| .quotInfo _ => "quot"
|
|
463
|
+
| .ctorInfo _ => "constructor"
|
|
464
|
+
| .recInfo _ => "recursor"
|
|
465
|
+
| .inductInfo _ =>
|
|
466
|
+
if isClass env ci.name then "class"
|
|
467
|
+
else if isStructure env ci.name then "structure" else "inductive"
|
|
468
|
+
| .defnInfo _ =>
|
|
469
|
+
if Meta.isInstanceCore env ci.name then "instance"
|
|
470
|
+
else if getReducibilityStatusCore env ci.name matches .reducible then "abbrev"
|
|
471
|
+
else "def"
|
|
472
|
+
|
|
473
|
+
def flagsOf (env : Environment) (ci : ConstantInfo) : Array String := Id.run do
|
|
474
|
+
let mut f := #[]
|
|
475
|
+
if ci.isUnsafe then f := f.push "unsafe"
|
|
476
|
+
if ci.isPartial then f := f.push "partial"
|
|
477
|
+
if isPrivateName ci.name then f := f.push "private"
|
|
478
|
+
if isNoncomputable env ci.name then f := f.push "noncomputable"
|
|
479
|
+
if isExtern env ci.name then f := f.push "extern"
|
|
480
|
+
if (Compiler.implementedByAttr.getParam? env ci.name).isSome then f := f.push "implemented_by"
|
|
481
|
+
if Meta.isInstanceCore env ci.name && !(ci matches .defnInfo _) then f := f.push "instance"
|
|
482
|
+
if isMetaDecl ci then f := f.push "meta"
|
|
483
|
+
return f
|
|
484
|
+
|
|
485
|
+
/-- What a review label is given on: the statement, and for data also the body. A theorem's
|
|
486
|
+
proof is the kernel's business, so it is not part of the hash. -/
|
|
487
|
+
def contentHash (env : Environment) (ci : ConstantInfo) : UInt64 := Id.run do
|
|
488
|
+
let mut h := ci.type.hash
|
|
489
|
+
match ci with
|
|
490
|
+
| .defnInfo v => h := mixHash h v.value.hash
|
|
491
|
+
| .inductInfo v =>
|
|
492
|
+
for c in v.ctors do
|
|
493
|
+
if let some cci := env.find? c then h := mixHash h cci.type.hash
|
|
494
|
+
| _ => pure ()
|
|
495
|
+
return h
|
|
496
|
+
|
|
497
|
+
/-- A unit's content with the names it mentions taken out (TCBSpec.md, decision 3, as revised on
|
|
498
|
+
2026-10-09): what `contentHash` reads, hashed with every constant it names — a reference, a
|
|
499
|
+
projection's structure — replaced by its place in `refs`, the constants in the order first met.
|
|
500
|
+
The server maps each of `refs` to an identity that a rename does not change and hashes the two
|
|
501
|
+
together (server/versions.ts): that is the version an opinion is bound to, which a pure rename
|
|
502
|
+
leaves as it is and a real change does not. Binder names are left out, as the kernel's hash
|
|
503
|
+
leaves them out; a subterm shared in the term is hashed once. -/
|
|
504
|
+
structure ShapeSt where
|
|
505
|
+
idx : Std.HashMap Name UInt64 := {}
|
|
506
|
+
refs : Array Name := #[]
|
|
507
|
+
memo : Std.HashMap Expr UInt64 := {}
|
|
508
|
+
|
|
509
|
+
/-- A constant's place in `refs`. -/
|
|
510
|
+
def refIdx (c : Name) : StateM ShapeSt UInt64 := do
|
|
511
|
+
if let some i := (← get).idx[c]? then return i
|
|
512
|
+
let i := (← get).refs.size.toUInt64
|
|
513
|
+
modify fun s => { s with idx := s.idx.insert c i, refs := s.refs.push c }
|
|
514
|
+
return i
|
|
515
|
+
|
|
516
|
+
partial def shapeOf (e : Expr) : StateM ShapeSt UInt64 := do
|
|
517
|
+
match e with
|
|
518
|
+
| .bvar i => return mixHash 11 (hash i)
|
|
519
|
+
| .fvar _ | .mvar _ => return 13
|
|
520
|
+
| .sort u => return mixHash 17 (hash u)
|
|
521
|
+
| .lit l => return mixHash 19 (hash l)
|
|
522
|
+
| .const c ls => return mixHash 23 (mixHash (← refIdx c) (hash ls))
|
|
523
|
+
| .mdata _ b => shapeOf b
|
|
524
|
+
| _ =>
|
|
525
|
+
if let some h := (← get).memo[e]? then return h
|
|
526
|
+
let h ← match e with
|
|
527
|
+
| .app f a => do
|
|
528
|
+
let hf ← shapeOf f
|
|
529
|
+
let ha ← shapeOf a
|
|
530
|
+
pure (mixHash 29 (mixHash hf ha))
|
|
531
|
+
| .lam _ t b bi => do
|
|
532
|
+
let ht ← shapeOf t
|
|
533
|
+
let hb ← shapeOf b
|
|
534
|
+
pure (mixHash 31 (mixHash (mixHash ht hb) bi.toUInt64))
|
|
535
|
+
| .forallE _ t b bi => do
|
|
536
|
+
let ht ← shapeOf t
|
|
537
|
+
let hb ← shapeOf b
|
|
538
|
+
pure (mixHash 37 (mixHash (mixHash ht hb) bi.toUInt64))
|
|
539
|
+
| .letE _ t v b _ => do
|
|
540
|
+
let ht ← shapeOf t
|
|
541
|
+
let hv ← shapeOf v
|
|
542
|
+
let hb ← shapeOf b
|
|
543
|
+
pure (mixHash 41 (mixHash ht (mixHash hv hb)))
|
|
544
|
+
| .proj s i b => do
|
|
545
|
+
let hs ← refIdx s
|
|
546
|
+
let hb ← shapeOf b
|
|
547
|
+
pure (mixHash 43 (mixHash hs (mixHash (hash i) hb)))
|
|
548
|
+
| _ => pure 0
|
|
549
|
+
modify fun st => { st with memo := st.memo.insert e h }
|
|
550
|
+
return h
|
|
551
|
+
|
|
552
|
+
/-- A unit's shape and references: the content of its declaration and of what it folds (`also`),
|
|
553
|
+
in that order, under one numbering. -/
|
|
554
|
+
def unitShape (env : Environment) (cs : List Name) : UInt64 × Array Name :=
|
|
555
|
+
let go : StateM ShapeSt UInt64 := do
|
|
556
|
+
let mut h : UInt64 := 7
|
|
557
|
+
for c in cs do
|
|
558
|
+
let some ci := env.find? c | continue
|
|
559
|
+
h := mixHash h (← shapeOf ci.type)
|
|
560
|
+
match ci with
|
|
561
|
+
| .defnInfo v => h := mixHash h (← shapeOf v.value)
|
|
562
|
+
| .inductInfo v =>
|
|
563
|
+
for k in v.ctors do
|
|
564
|
+
if let some cci := env.find? k then h := mixHash h (← shapeOf cci.type)
|
|
565
|
+
| _ => pure ()
|
|
566
|
+
return h
|
|
567
|
+
let (h, st) := go.run {}
|
|
568
|
+
(h, st.refs)
|
|
569
|
+
|
|
570
|
+
/-- What a unit's declarations are compiled into: the generated constants they reach that the
|
|
571
|
+
edges look through (a recursive body's `_f`, `_unary` or `_mutual`, a matcher, a `partial def`'s
|
|
572
|
+
`_unsafe_rec`), never another unit, a constant standing for its type, or a proof. A recursive
|
|
573
|
+
definition's body is there, not in its own value, so these are part of its version (TCBSpec.md,
|
|
574
|
+
decision 3): a change of the body is a new version. In the order first met. -/
|
|
575
|
+
def compiledOf (env : Environment) (inProject : Name → Bool) (owner genOwner : Std.HashMap Name Name)
|
|
576
|
+
(cs : List Name) : Array Name := Id.run do
|
|
577
|
+
let refs (ci : ConstantInfo) : Array Name :=
|
|
578
|
+
let used := ci.type.getUsedConstants ++ (match ci with
|
|
579
|
+
| .defnInfo v => v.value.getUsedConstants
|
|
580
|
+
| _ => #[])
|
|
581
|
+
-- a `partial def` is an opaque constant whose code is its `_unsafe_rec` twin
|
|
582
|
+
if ci matches .opaqueInfo _ then used.push (ci.name ++ `_unsafe_rec) else used
|
|
583
|
+
let mut seen : Std.HashSet Name := cs.foldl (·.insert ·) {}
|
|
584
|
+
let mut out : Array Name := #[]
|
|
585
|
+
let mut work : Array Name := #[]
|
|
586
|
+
for c in cs.reverse do
|
|
587
|
+
if let some ci := env.find? c then work := work ++ (refs ci).reverse
|
|
588
|
+
while h : work.size > 0 do
|
|
589
|
+
let d := work[work.size - 1]
|
|
590
|
+
work := work.pop
|
|
591
|
+
if seen.contains d || !inProject d then continue
|
|
592
|
+
seen := seen.insert d
|
|
593
|
+
let some o := owner[d]? | continue
|
|
594
|
+
if d == o || isStructural env d || genOwner.contains d then continue
|
|
595
|
+
let some ci := env.find? d | continue
|
|
596
|
+
-- a proof Lean abstracted out of a body (`proof_n`, an equation lemma) is the kernel's business
|
|
597
|
+
if ci matches .thmInfo _ then continue
|
|
598
|
+
out := out.push d
|
|
599
|
+
work := work ++ (refs ci).reverse
|
|
600
|
+
return out
|
|
601
|
+
|
|
602
|
+
def hex (n : UInt64) : String :=
|
|
603
|
+
let s := String.ofList (Nat.toDigits 16 n.toNat)
|
|
604
|
+
"".pushn '0' (16 - s.length) ++ s
|
|
605
|
+
|
|
606
|
+
/-- The tokens of a syntax as its parser describes it, in order: its atoms, and `…` where a term,
|
|
607
|
+
an identifier or another syntax goes. -/
|
|
608
|
+
partial def parserTokens (env : Environment) (e : Expr) (acc : Array String) : Array String :=
|
|
609
|
+
let args := e.getAppArgs
|
|
610
|
+
let atom (s : String) := acc.push (s.trimAscii.toString)
|
|
611
|
+
match e.getAppFn.constName?, args with
|
|
612
|
+
| some ``Lean.ParserDescr.symbol, #[.lit (.strVal s)] => atom s
|
|
613
|
+
| some ``Lean.ParserDescr.nonReservedSymbol, #[.lit (.strVal s), _] => atom s
|
|
614
|
+
| some ``Lean.ParserDescr.unicodeSymbol, #[.lit (.strVal s), _] => atom s
|
|
615
|
+
| some ``Lean.ParserDescr.unicodeSymbol, #[.lit (.strVal s), _, _] => atom s
|
|
616
|
+
| some ``Lean.ParserDescr.cat, _ | some ``Lean.ParserDescr.const, _ | some ``Lean.ParserDescr.parser, _ => acc.push "…"
|
|
617
|
+
| some c, #[] =>
|
|
618
|
+
match (env.find? c).bind (·.type.getAppFn.constName?) with
|
|
619
|
+
| some ``Lean.ParserDescr | some ``Lean.TrailingParserDescr => acc.push "…"
|
|
620
|
+
| _ => acc
|
|
621
|
+
| _, _ => args.foldl (fun a x => parserTokens env x a) acc
|
|
622
|
+
|
|
623
|
+
/-- What a card calls a syntax kind: its atoms, `…` for what goes between them (`declare_type …
|
|
624
|
+
where`), at most 40 characters; its name's last part where its parser is not to be read. -/
|
|
625
|
+
def kindLabel (env : Environment) (k : Name) : String := Id.run do
|
|
626
|
+
let fallback := match k with | .str _ s => s | _ => k.toString
|
|
627
|
+
let some (.defnInfo v) := env.find? k | return fallback
|
|
628
|
+
let mut toks : Array String := #[]
|
|
629
|
+
for t in parserTokens env v.value #[] do
|
|
630
|
+
if t.isEmpty || (t == "…" && toks.back? == some "…") then continue
|
|
631
|
+
toks := toks.push t
|
|
632
|
+
while toks.back? == some "…" do toks := toks.pop
|
|
633
|
+
while toks[0]? == some "…" do toks := toks.extract 1 toks.size
|
|
634
|
+
let s := " ".intercalate toks.toList
|
|
635
|
+
if s.isEmpty then return fallback
|
|
636
|
+
return if s.length > 40 then (s.take 39).toString ++ "…" else s
|
|
637
|
+
|
|
638
|
+
def run (out : System.FilePath) (projectDir : String) (roots : Array Name)
|
|
639
|
+
(cache : Option System.FilePath) (salt : String) : M Unit := do
|
|
640
|
+
let env ← getEnv
|
|
641
|
+
let t0 ← IO.monoMsNow
|
|
642
|
+
let modNames := env.header.moduleNames
|
|
643
|
+
modify fun s => { s with modNames }
|
|
644
|
+
-- 1. project modules: those whose .olean lives in the project's own build directory. Both paths
|
|
645
|
+
-- are made real first: the search path may spell a folder one way and the command line another
|
|
646
|
+
-- (a temporary folder on macOS, /var → /private/var; a project reached through a symbolic link)
|
|
647
|
+
let real (p : System.FilePath) : IO String := do
|
|
648
|
+
try return (← IO.FS.realPath p).toString catch _ => return p.toString
|
|
649
|
+
let sep := System.FilePath.pathSeparator.toString
|
|
650
|
+
let dir ← real projectDir
|
|
651
|
+
let dir := if dir.endsWith sep then dir else dir ++ sep
|
|
652
|
+
let mut projMods : Array (Name × Nat) := #[]
|
|
653
|
+
for h : i in [0:modNames.size] do
|
|
654
|
+
let m := modNames[i]
|
|
655
|
+
let path ← try (some <$> findOLean m) catch _ => pure none
|
|
656
|
+
if let some p := path then
|
|
657
|
+
if (← real p).startsWith dir then projMods := projMods.push (m, i)
|
|
658
|
+
let projSet : Std.HashSet Name := projMods.foldl (·.insert ·.1) {}
|
|
659
|
+
let inProject (c : Name) : Bool :=
|
|
660
|
+
match env.getModuleIdxFor? c with
|
|
661
|
+
| some i => projSet.contains modNames[i.toNat]!
|
|
662
|
+
| none => false
|
|
663
|
+
|
|
664
|
+
-- 1b. the cache: module keys in import order (moduleNames lists imports first), then load
|
|
665
|
+
let mut keys : Std.HashMap Name String := {}
|
|
666
|
+
let mut hits := 0
|
|
667
|
+
if let some dir := cache then
|
|
668
|
+
IO.FS.createDirAll dir
|
|
669
|
+
for (m, idx) in projMods do
|
|
670
|
+
let olean ← findOLean m
|
|
671
|
+
let some own ← (try some <$> IO.FS.readFile (olean.toString ++ ".hash") catch _ => pure none)
|
|
672
|
+
| continue
|
|
673
|
+
let deps := env.header.moduleData[idx]!.imports.filter (projSet.contains ·.module)
|
|
674
|
+
let depKeys := deps.filterMap (keys[·.module]?)
|
|
675
|
+
if depKeys.size < deps.size then continue
|
|
676
|
+
let key := hex (hash (salt ++ "|" ++ own.trimAscii.toString ++ "|" ++ ",".intercalate depKeys.toList))
|
|
677
|
+
keys := keys.insert m key
|
|
678
|
+
if ← readModuleCache dir m key then hits := hits + 1
|
|
679
|
+
IO.eprintln s!"[extract] cache: {hits}/{projMods.size} modules reused ({(← IO.monoMsNow) - t0} ms)"
|
|
680
|
+
|
|
681
|
+
-- 2. classify project constants: a node, or folded into one. Meta-level declarations are
|
|
682
|
+
-- nodes too (decision 2): what a syntax extension command generates is grouped below (2c)
|
|
683
|
+
let mut nodes : Array Name := #[]
|
|
684
|
+
let mut auxs : Array Name := #[]
|
|
685
|
+
let mut notations : Std.HashSet Name := {}
|
|
686
|
+
-- a constant that two modules list: importing both keeps one, which the environment places in
|
|
687
|
+
-- one of them. It is taken once, from there. Lean realises equation lemmas so in every module
|
|
688
|
+
-- that asks for them; the same declaration written in two files that do not import each other
|
|
689
|
+
-- is said, with both places
|
|
690
|
+
let mut elsewhere : Array (Name × Name) := #[]
|
|
691
|
+
for (m, idx) in projMods do
|
|
692
|
+
for c in env.header.moduleData[idx]!.constNames do
|
|
693
|
+
if (env.getModuleIdxFor? c |>.map (·.toNat)) != some idx then
|
|
694
|
+
elsewhere := elsewhere.push (c, m)
|
|
695
|
+
continue
|
|
696
|
+
let some ci := env.find? c | continue
|
|
697
|
+
let u := (privateToUserName? c).getD c
|
|
698
|
+
let ranged := (← findDeclarationRanges? c).isSome
|
|
699
|
+
if isNotationConst ci && ranged then
|
|
700
|
+
notations := notations.insert c; nodes := nodes.push c; continue
|
|
701
|
+
let generated :=
|
|
702
|
+
ci matches .ctorInfo _ || ci matches .recInfo _ || isAuxRecursor env c
|
|
703
|
+
|| isNoConfusion env c || isMatcherCore env c || (env.getProjectionFnInfo? c).isSome
|
|
704
|
+
|| u.isInternalDetail || generatedSuffixes.contains u.getString!
|
|
705
|
+
|| (u.getString! == "elim" && (env.find? c.getPrefix matches some (.ctorInfo _)))
|
|
706
|
+
if generated || !(ranged || Meta.isInstanceCore env c) then auxs := auxs.push c
|
|
707
|
+
else nodes := nodes.push c
|
|
708
|
+
let written : Std.HashSet Name := nodes.foldl (·.insert ·) {}
|
|
709
|
+
for (c, m) in elsewhere do
|
|
710
|
+
if written.contains c then
|
|
711
|
+
IO.eprintln s!"[extract] warning: {c} is declared in {m} and in {moduleOf modNames env c}; the graph keeps the one in {moduleOf modNames env c}"
|
|
712
|
+
-- by the name as written: a private constant's own prefixes are its module's mangling
|
|
713
|
+
let hasExternalPrefix (c : Name) : Bool := Id.run do
|
|
714
|
+
let mut p := ((privateToUserName? c).getD c).getPrefix
|
|
715
|
+
while !p.isAnonymous do
|
|
716
|
+
if (env.find? p).isSome && !inProject p then return true
|
|
717
|
+
p := p.getPrefix
|
|
718
|
+
return false
|
|
719
|
+
let mut owner : Std.HashMap Name Name := nodes.foldl (fun m c => m.insert c c) {}
|
|
720
|
+
-- an aux constant belongs to the nearest enclosing node: constructors and projections to
|
|
721
|
+
-- their type, everything else to the longest name prefix that is a node (a matcher of an
|
|
722
|
+
-- `_aux_` macro to it, the code of a `partial def` to the definition)
|
|
723
|
+
let byUser : Std.HashMap Name Name :=
|
|
724
|
+
nodes.foldl (fun m c => m.insert ((privateToUserName? c).getD c) c) {}
|
|
725
|
+
for c in auxs do
|
|
726
|
+
let start := match env.find? c with
|
|
727
|
+
| some (.ctorInfo v) => v.induct
|
|
728
|
+
| _ => match env.getProjectionFnInfo? c with
|
|
729
|
+
| some info => match env.find? info.ctorName with
|
|
730
|
+
| some (.ctorInfo v) => v.induct
|
|
731
|
+
| _ => c.getPrefix
|
|
732
|
+
| none => c.getPrefix
|
|
733
|
+
let mut p := (privateToUserName? start).getD start
|
|
734
|
+
let mut found : Option Name := none
|
|
735
|
+
while !p.isAnonymous && found.isNone do
|
|
736
|
+
found := byUser[p]?
|
|
737
|
+
p := p.getPrefix
|
|
738
|
+
match found with
|
|
739
|
+
| some o => owner := owner.insert c o
|
|
740
|
+
| none =>
|
|
741
|
+
-- generated in this project for a declaration outside it (an equation lemma Lean
|
|
742
|
+
-- realised on demand): it is external, and references to it fold like external ones;
|
|
743
|
+
-- anything else that belongs to no declaration stands for itself
|
|
744
|
+
if hasExternalPrefix c then pure ()
|
|
745
|
+
else nodes := nodes.push c; owner := owner.insert c c
|
|
746
|
+
|
|
747
|
+
-- 2b. units (TCBSpec.md, decision 1). A declaration Lean generated from a command (what a declaring
|
|
748
|
+
-- command writes beside the declaration it names, the lemmas of `@[simps]`) has no name of its own in the text: its
|
|
749
|
+
-- selection range is its whole range, where a written one's is its name. It belongs to the
|
|
750
|
+
-- declaration that command writes, the smallest range of a written declaration of its module
|
|
751
|
+
-- that holds its own, and folds into it as a constructor folds into its type. One for which no
|
|
752
|
+
-- such declaration is found stays a node of its own, marked `gen`. What syntax extension
|
|
753
|
+
-- commands generate is left to 2c: such a command writes no declaration.
|
|
754
|
+
let modName (c : Name) : String :=
|
|
755
|
+
match env.getModuleIdxFor? c with
|
|
756
|
+
| some i => modNames[i.toNat]!.toString
|
|
757
|
+
| none => ""
|
|
758
|
+
let generatedAt (r : DeclarationRanges) : Bool :=
|
|
759
|
+
r.selectionRange.pos == r.range.pos && r.selectionRange.endPos == r.range.endPos
|
|
760
|
+
let posLe (a b : Position) : Bool := a.line < b.line || (a.line == b.line && a.column ≤ b.column)
|
|
761
|
+
let mut rngs : Array (Option DeclarationRanges) := #[]
|
|
762
|
+
for c in nodes do rngs := rngs.push (← findDeclarationRanges? c)
|
|
763
|
+
let mut ofModule : Std.HashMap String (Array Nat) := {}
|
|
764
|
+
for h : j in [0:nodes.size] do
|
|
765
|
+
ofModule := ofModule.insert (modName nodes[j]) ((ofModule.getD (modName nodes[j]) #[]).push j)
|
|
766
|
+
-- generated declaration → the declaration its command writes
|
|
767
|
+
let mut genOwner : Std.HashMap Name Name := {}
|
|
768
|
+
let mut genLost : Std.HashSet Name := {}
|
|
769
|
+
for h : i in [0:nodes.size] do
|
|
770
|
+
let some r := rngs[i]! | continue
|
|
771
|
+
if !generatedAt r || notations.contains nodes[i] then continue
|
|
772
|
+
let mut best : Option (Nat × Nat) := none
|
|
773
|
+
for j in ofModule.getD (modName nodes[i]) #[] do
|
|
774
|
+
let some rj := rngs[j]! | continue
|
|
775
|
+
if j == i || generatedAt rj || notations.contains nodes[j]! then continue
|
|
776
|
+
if !(posLe rj.range.pos r.range.pos && posLe r.range.endPos rj.range.endPos) then continue
|
|
777
|
+
let span := (rj.range.endPos.line - rj.range.pos.line) * 100000
|
|
778
|
+
+ (rj.range.endPos.column + 100000 - rj.range.pos.column)
|
|
779
|
+
if best.all (fun (_, s) => span < s) then best := some (j, span)
|
|
780
|
+
match best with
|
|
781
|
+
| some (j, _) => genOwner := genOwner.insert nodes[i] nodes[j]!
|
|
782
|
+
| none => genLost := genLost.insert nodes[i]
|
|
783
|
+
-- 2c. notation units (decision 2): the constants one syntax extension command generates share
|
|
784
|
+
-- its range, and make one unit of kind `notation`. It is named after the syntax kind it
|
|
785
|
+
-- declares (`syntax`, `notation`, `macro`, `elab`), else after its first `_aux_` declaration
|
|
786
|
+
-- (`macro_rules`, `elab_rules`), else after its category's quotation parser
|
|
787
|
+
-- (`declare_syntax_cat`); the rest is listed in its `also`.
|
|
788
|
+
let rank (c : Name) : Nat :=
|
|
789
|
+
match env.find? c with
|
|
790
|
+
| some ci =>
|
|
791
|
+
match ci.type.getForallBody.getAppFn.constName? with
|
|
792
|
+
| some h =>
|
|
793
|
+
if h == ``Lean.Parser.Category then 3
|
|
794
|
+
else if (h == ``Lean.ParserDescr || h == ``Lean.TrailingParserDescr) then
|
|
795
|
+
(if c.getString! == "quot" then 2 else 0)
|
|
796
|
+
else 1
|
|
797
|
+
| none => 1
|
|
798
|
+
| none => 1
|
|
799
|
+
let mut byCommand : Std.HashMap String (Array Nat) := {}
|
|
800
|
+
for h : i in [0:nodes.size] do
|
|
801
|
+
if !notations.contains nodes[i] then continue
|
|
802
|
+
let key := match rngs[i]! with
|
|
803
|
+
| some r => s!"{modName nodes[i]}:{r.range.pos.line}:{r.range.pos.column}:{r.range.endPos.line}:{r.range.endPos.column}"
|
|
804
|
+
| none => s!"{nodes[i]}"
|
|
805
|
+
byCommand := byCommand.insert key ((byCommand.getD key #[]).push i)
|
|
806
|
+
let generatedCount := genOwner.size
|
|
807
|
+
let mut notationUnits : Std.HashSet Name := {}
|
|
808
|
+
for (_, members) in byCommand do
|
|
809
|
+
let sorted := members.qsort fun a b =>
|
|
810
|
+
rank nodes[a]! < rank nodes[b]! || (rank nodes[a]! == rank nodes[b]! && nodes[a]!.toString < nodes[b]!.toString)
|
|
811
|
+
let rep := nodes[sorted[0]!]!
|
|
812
|
+
notationUnits := notationUnits.insert rep
|
|
813
|
+
for k in sorted.extract 1 sorted.size do genOwner := genOwner.insert nodes[k]! rep
|
|
814
|
+
-- what a generated declaration owned (its constructors, its equation lemmas) ends at its unit
|
|
815
|
+
owner := owner.fold (fun m c o => m.insert c (genOwner.getD o o)) {}
|
|
816
|
+
let mut also : Std.HashMap Name (Array Name) := {}
|
|
817
|
+
for (c, o) in genOwner do also := also.insert o ((also.getD o #[]).push c)
|
|
818
|
+
let userName (c : Name) : Name := (privateToUserName? c).getD c
|
|
819
|
+
also := also.fold (fun m o xs => m.insert o (xs.qsort (fun a b => (userName a).toString < (userName b).toString))) {}
|
|
820
|
+
nodes := nodes.filter (!genOwner.contains ·)
|
|
821
|
+
let nodeIdx : Std.HashMap Name Nat := nodes.size.fold (fun i _ m => m.insert nodes[i]! i) {}
|
|
822
|
+
-- 2d. what a notation unit is (decision 2, as revised on 2026-10-08). Meaning lives in the
|
|
823
|
+
-- macros and elaborators and the code they call; a command that only declares syntax (`syntax`,
|
|
824
|
+
-- `declare_syntax_cat`) fixes the grammar, whose trees the rule that ran, or a parent
|
|
825
|
+
-- elaborator, interprets. Such a unit is `grammar`: a node, but edges into it carry the grammar
|
|
826
|
+
-- site, which no TCB follows. A macro or elaborator depends on the syntax it interprets at that
|
|
827
|
+
-- site, for the reader to see them side by side; nothing depends on a rule through its syntax:
|
|
828
|
+
-- a use reaches the rule itself (server/usage.ts).
|
|
829
|
+
let headOf (c : Name) : Name := ((env.find? c).bind (·.type.getForallBody.getAppFn.constName?)).getD .anonymous
|
|
830
|
+
let lastOf (c : Name) : String := match c with | .str _ s => s | _ => ""
|
|
831
|
+
let isKindDecl (c : Name) : Bool :=
|
|
832
|
+
(headOf c == ``Lean.ParserDescr || headOf c == ``Lean.TrailingParserDescr) && lastOf c != "quot"
|
|
833
|
+
let isParserHead (h : Name) : Bool :=
|
|
834
|
+
h == ``Lean.ParserDescr || h == ``Lean.TrailingParserDescr || h == ``Lean.Parser.Category
|
|
835
|
+
-- every macro and elaborator of the project, with the kinds it is registered for
|
|
836
|
+
let mut kindsOfImpl : Std.HashMap Name (Array Name) := {}
|
|
837
|
+
let mut rulesOfKind : Std.HashMap Name (Array Name) := {}
|
|
838
|
+
for k in Parser.getSyntaxNodeKinds env do
|
|
839
|
+
let impls :=
|
|
840
|
+
(Elab.macroAttribute.getEntries env k).map (·.declName)
|
|
841
|
+
++ (Elab.Term.termElabAttribute.getEntries env k).map (·.declName)
|
|
842
|
+
++ (Elab.Command.commandElabAttribute.getEntries env k).map (·.declName)
|
|
843
|
+
++ (Elab.Tactic.tacticElabAttribute.getEntries env k).map (·.declName)
|
|
844
|
+
for r in impls do
|
|
845
|
+
if inProject r then kindsOfImpl := kindsOfImpl.insert r ((kindsOfImpl.getD r #[]).push k)
|
|
846
|
+
if inProject k then rulesOfKind := rulesOfKind.insert k impls.toArray
|
|
847
|
+
let mut grammarUnits : Std.HashSet Name := {}
|
|
848
|
+
-- unit → the kinds it interprets, the command it is, and its name and display
|
|
849
|
+
let mut interprets : Std.HashMap Name (Array Name) := {}
|
|
850
|
+
let mut command : Std.HashMap Name String := {}
|
|
851
|
+
for n in nodes do
|
|
852
|
+
let members := n :: (also.getD n #[]).toList
|
|
853
|
+
let mut ks : Array Name := #[]
|
|
854
|
+
for m in members do
|
|
855
|
+
for k in kindsOfImpl.getD m #[] do
|
|
856
|
+
if !ks.contains k && !members.contains k then ks := ks.push k
|
|
857
|
+
if !ks.isEmpty then interprets := interprets.insert n (ks.qsort (·.toString < ·.toString))
|
|
858
|
+
if !notationUnits.contains n then continue
|
|
859
|
+
let heads := members.map headOf
|
|
860
|
+
let declares := members.any isKindDecl
|
|
861
|
+
let elab_ := heads.any fun h => h == ``Lean.Elab.Term.TermElab || h == ``Lean.Elab.Command.CommandElab || h == ``Lean.Elab.Tactic.Tactic
|
|
862
|
+
let cmd :=
|
|
863
|
+
if heads.all isParserHead then
|
|
864
|
+
(if heads.contains ``Lean.Parser.Category then "syntax_cat" else "syntax")
|
|
865
|
+
else if declares then
|
|
866
|
+
(if heads.contains ``Lean.PrettyPrinter.Unexpander then "notation" else if elab_ then "elab" else "macro")
|
|
867
|
+
else if elab_ then "elab_rules"
|
|
868
|
+
else if heads.contains ``Lean.Macro then "macro_rules"
|
|
869
|
+
else "notation"
|
|
870
|
+
if cmd == "syntax" || cmd == "syntax_cat" then grammarUnits := grammarUnits.insert n
|
|
871
|
+
command := command.insert n cmd
|
|
872
|
+
-- names: a unit is called after what it declares or interprets, not after Lean's `_aux_` name,
|
|
873
|
+
-- whose counter and module prefix say nothing a reader can use. A syntax, `notation`, `macro`
|
|
874
|
+
-- or `elab` keeps its kind's name; a category is `<cat>.syntax_cat`; a rule is
|
|
875
|
+
-- `<kind>.macro_rules` (`elab_rules`), the kind it interprets (the first by name when it has
|
|
876
|
+
-- several; one outside the project under the rule's own namespace), qualified with its module's
|
|
877
|
+
-- last part when that is not the module of the kind's syntax, and numbered `_2`, `_3`, … in
|
|
878
|
+
-- source order where one module has several rules for the kind. An edit elsewhere leaves it as
|
|
879
|
+
-- it is. A kind declared `local` is private to its module, so several modules may each declare
|
|
880
|
+
-- one of the same name (`local notation "Φ" => …` in every file that wants it): its unit, and the
|
|
881
|
+
-- rules that interpret it, are qualified with its module's last part too
|
|
882
|
+
-- (`MyProject.Perm.termΦ.Frame`), which no edit in another module changes. A
|
|
883
|
+
-- public kind's name is unique already.
|
|
884
|
+
let tailOf (c : Name) : String := lastOf (modNames[(env.getModuleIdxFor? c).map (·.toNat) |>.getD 0]!)
|
|
885
|
+
let kindName (k : Name) : Name := if isPrivateName k then (userName k).str (tailOf k) else userName k
|
|
886
|
+
let mut display : Std.HashMap Name String := {}
|
|
887
|
+
let mut named : Std.HashMap Name Name := {}
|
|
888
|
+
let mut byBase : Std.HashMap Name (Array (Name × Nat)) := {}
|
|
889
|
+
let mut labelOf : Std.HashMap Name String := {}
|
|
890
|
+
for n in nodes do
|
|
891
|
+
let some cmd := command[n]? | continue
|
|
892
|
+
let members := n :: (also.getD n #[]).toList
|
|
893
|
+
let modTail := lastOf (modNames[(env.getModuleIdxFor? n).map (·.toNat) |>.getD 0]!)
|
|
894
|
+
let line := ((← findDeclarationRanges? n).map (·.range.pos.line)).getD 0
|
|
895
|
+
if cmd == "syntax_cat" then
|
|
896
|
+
let base := match members.find? (lastOf · == "quot") with
|
|
897
|
+
| some q => userName q.getPrefix
|
|
898
|
+
| none => userName n
|
|
899
|
+
named := named.insert n (base.str "syntax_cat")
|
|
900
|
+
display := display.insert n s!"syntax_cat · {lastOf base}"
|
|
901
|
+
else if let some k := members.find? isKindDecl then
|
|
902
|
+
let label := kindLabel env k
|
|
903
|
+
labelOf := labelOf.insert n label
|
|
904
|
+
if isPrivateName k then named := named.insert n (kindName k)
|
|
905
|
+
display := display.insert n s!"{cmd} · {label}{if isPrivateName k then s!" · in {tailOf k}" else ""}"
|
|
906
|
+
else if let some k := (interprets.getD n #[])[0]? then
|
|
907
|
+
let own := (env.getModuleIdxFor? k) == (env.getModuleIdxFor? n)
|
|
908
|
+
let kName := if inProject k then kindName k else (userName n).getPrefix.str (lastOf k)
|
|
909
|
+
let base := if own then kName.str cmd else (kName.str cmd).str modTail
|
|
910
|
+
byBase := byBase.insert base ((byBase.getD base #[]).push (n, line))
|
|
911
|
+
labelOf := labelOf.insert n (kindLabel env k)
|
|
912
|
+
for (base, units) in byBase do
|
|
913
|
+
let sorted := units.qsort fun a b => a.2 < b.2 || (a.2 == b.2 && a.1.toString < b.1.toString)
|
|
914
|
+
for h : j in [0:sorted.size] do
|
|
915
|
+
let (n, _) := sorted[j]
|
|
916
|
+
let name := if j == 0 then base else match base with
|
|
917
|
+
| .str p s => p.str s!"{s}_{j + 1}"
|
|
918
|
+
| b => b
|
|
919
|
+
named := named.insert n name
|
|
920
|
+
let cmd := command.getD n "notation"
|
|
921
|
+
let where_ := match base with
|
|
922
|
+
| .str (.str _ c) m => if c == cmd then s!" · in {m}" else ""
|
|
923
|
+
| _ => ""
|
|
924
|
+
display := display.insert n s!"{cmd}{if j == 0 then "" else s!" #{j + 1}"} · {labelOf.getD n ""}{where_}"
|
|
925
|
+
-- a name another unit has already is not taken: that unit keeps its own, Lean's
|
|
926
|
+
let taken : Std.HashSet String := nodes.foldl (fun s c => if named.contains c then s else s.insert (userName c).toString) {}
|
|
927
|
+
named := named.filter fun _ nm => !taken.contains nm.toString
|
|
928
|
+
-- every unit's name is its own: the server, the review and the views know a declaration by its
|
|
929
|
+
-- name alone. The rules above should leave none shared; should two units still meet on
|
|
930
|
+
-- one (two private declarations of the same name in modules that do not import each other), the
|
|
931
|
+
-- later one in module order is numbered and the extraction says so
|
|
932
|
+
let mut nameOf : Std.HashMap Name String := {}
|
|
933
|
+
let mut usedNames : Std.HashMap String Name := {}
|
|
934
|
+
for n in nodes do
|
|
935
|
+
let base := named[n]?.getD (userName n)
|
|
936
|
+
let mut nm := base.toString
|
|
937
|
+
let mut k := 2
|
|
938
|
+
while usedNames.contains nm do
|
|
939
|
+
nm := (match base with | .str p s => p.str s!"{s}_{k}" | b => b.str s!"_{k}").toString
|
|
940
|
+
k := k + 1
|
|
941
|
+
if k > 2 then
|
|
942
|
+
IO.eprintln s!"[extract] warning: {base} names a unit of {modName (usedNames.getD base.toString n)} and one of {modName n}; the second is called {nm}"
|
|
943
|
+
usedNames := usedNames.insert nm n
|
|
944
|
+
nameOf := nameOf.insert n nm
|
|
945
|
+
-- what a unit's interpreting depends on, at the grammar site; the units that interpret a kind
|
|
946
|
+
let mut interpretRefs : Std.HashMap Name (Array (Name × Site)) := {}
|
|
947
|
+
let mut rules := 0
|
|
948
|
+
for (n, ks) in interprets do
|
|
949
|
+
for k in ks do
|
|
950
|
+
if owner.contains k then
|
|
951
|
+
interpretRefs := interpretRefs.insert n ((interpretRefs.getD n #[]).push (k, grammar))
|
|
952
|
+
rules := rules + 1
|
|
953
|
+
IO.eprintln s!"[extract] {projMods.size} project modules, {nodes.size} nodes ({notationUnits.size} notation, {grammarUnits.size} of them grammar), {auxs.size} folded, {generatedCount} generated from a command folded, {genOwner.size - generatedCount} folded into notation units, {rules} syntax rules ({(← IO.monoMsNow) - t0} ms)"
|
|
954
|
+
|
|
955
|
+
-- 3. edges: a unit's own references, seen through the generated constants it owns; those of
|
|
956
|
+
-- the declarations it folds are its own, at the same sites
|
|
957
|
+
let insts ← WalkCache.new
|
|
958
|
+
let mut extIdx : Std.HashMap Name Nat := {}
|
|
959
|
+
let mut exts : Array Name := #[]
|
|
960
|
+
let mut edges : Array Json := #[]
|
|
961
|
+
let mut done := 0
|
|
962
|
+
for n in nodes do
|
|
963
|
+
let mut acc : Std.HashMap Name Site := {}
|
|
964
|
+
let mut work : Array (Name × Site) := (← refsOf insts n) ++ interpretRefs.getD n #[]
|
|
965
|
+
-- a declaration written in a `mutual` block is compiled from the whole block, so its value
|
|
966
|
+
-- reaches the others' bodies only through what Lean generates (each one's `_f` in structural
|
|
967
|
+
-- recursion, one shared `_mutual` in well-founded recursion, inlined before v4.29, the others'
|
|
968
|
+
-- `_unsafe_rec` in a `partial def`'s code) and never by their names: it rests on each of them,
|
|
969
|
+
-- in its body, a theorem in its proof
|
|
970
|
+
match env.find? n with
|
|
971
|
+
| some (.defnInfo v) => for m in v.all do if m != n then work := work.push (m, body)
|
|
972
|
+
| some (.thmInfo v) => for m in v.all do if m != n then work := work.push (m, proof)
|
|
973
|
+
| some (.opaqueInfo v) =>
|
|
974
|
+
if (env.find? (n ++ `_unsafe_rec)).isSome then
|
|
975
|
+
for m in v.all do if m != n then work := work.push (m, body)
|
|
976
|
+
| _ => pure ()
|
|
977
|
+
for a in also.getD n #[] do
|
|
978
|
+
let rs ← refsOf insts a
|
|
979
|
+
-- a derived instance of a class whose instances are all equal (a `deriving DecidableEq`)
|
|
980
|
+
-- is left out as a proof is, whatever it refers to (decision 5); so is what a notation's
|
|
981
|
+
-- unexpander or delaborator refers to: it changes how a term is shown, never what it means
|
|
982
|
+
if (← subsingletonDecl insts a) || headOf a == ``Lean.PrettyPrinter.Unexpander
|
|
983
|
+
|| headOf a == ``Lean.PrettyPrinter.Delaborator.Delab then
|
|
984
|
+
work := work ++ rs.map fun (d, _) => (d, proof)
|
|
985
|
+
else work := work ++ rs
|
|
986
|
+
let mut seen : Std.HashSet (Name × Site) := {}
|
|
987
|
+
while h : work.size > 0 do
|
|
988
|
+
let (d, s) := work[work.size - 1]
|
|
989
|
+
work := work.pop
|
|
990
|
+
if inProject d && owner.contains d then
|
|
991
|
+
match owner[d]? with
|
|
992
|
+
| some o =>
|
|
993
|
+
if d == o || isStructural env d || genOwner.contains d then
|
|
994
|
+
-- a declaration, a constructor / recursor / projection standing for its type, or a
|
|
995
|
+
-- declaration generated from the command that writes its unit
|
|
996
|
+
if o != n then acc := acc.insert o ((acc[o]?.getD 0) ||| s)
|
|
997
|
+
else if !seen.contains (d, s) then
|
|
998
|
+
-- any other generated constant is looked through, whoever it was generated for:
|
|
999
|
+
-- Lean shares a matcher or an abstracted proof between declarations of the same
|
|
1000
|
+
-- shape, so its owner by name is not who uses it
|
|
1001
|
+
seen := seen.insert (d, s)
|
|
1002
|
+
for (d', s') in ← refsOf insts d do work := work.push (d', combine s s')
|
|
1003
|
+
| none => pure ()
|
|
1004
|
+
else
|
|
1005
|
+
let e := foldExternal env d
|
|
1006
|
+
acc := acc.insert e ((acc[e]?.getD 0) ||| s)
|
|
1007
|
+
let src := nodeIdx[n]!
|
|
1008
|
+
for (t, s0) in acc do
|
|
1009
|
+
-- into a unit that only declares syntax, what is not a proof is grammar
|
|
1010
|
+
let s := if grammarUnits.contains t && s0 &&& (stmt ||| body) != 0 then (s0 &&& (proof ||| grammar)) ||| grammar else s0
|
|
1011
|
+
let dst ← match nodeIdx[t]? with
|
|
1012
|
+
| some i => pure i
|
|
1013
|
+
| none =>
|
|
1014
|
+
if inProject t then continue
|
|
1015
|
+
match extIdx[t]? with
|
|
1016
|
+
| some j => pure (nodes.size + j)
|
|
1017
|
+
| none =>
|
|
1018
|
+
extIdx := extIdx.insert t exts.size
|
|
1019
|
+
exts := exts.push t
|
|
1020
|
+
pure (nodes.size + exts.size - 1)
|
|
1021
|
+
edges := edges.push (Json.arr #[src, dst, s.toNat])
|
|
1022
|
+
done := done + 1
|
|
1023
|
+
if done % 1000 == 0 then
|
|
1024
|
+
IO.eprintln s!"[extract] edges: {done}/{nodes.size} ({(← IO.monoMsNow) - t0} ms)"
|
|
1025
|
+
IO.eprintln s!"[extract] {edges.size} edges, {exts.size} externals ({(← IO.monoMsNow) - t0} ms)"
|
|
1026
|
+
|
|
1027
|
+
-- 4. axioms, through everything, joined over each unit
|
|
1028
|
+
let mut axSetIdx : Std.HashMap Nat Nat := {}
|
|
1029
|
+
let mut axSets : Array Nat := #[]
|
|
1030
|
+
let mut nodeAx : Array Nat := #[]
|
|
1031
|
+
for c in nodes ++ exts do
|
|
1032
|
+
let mut m ← axiomMask c
|
|
1033
|
+
for a in also.getD c #[] do m := m ||| (← axiomMask a)
|
|
1034
|
+
match axSetIdx[m]? with
|
|
1035
|
+
| some i => nodeAx := nodeAx.push i
|
|
1036
|
+
| none =>
|
|
1037
|
+
axSetIdx := axSetIdx.insert m axSets.size
|
|
1038
|
+
nodeAx := nodeAx.push axSets.size
|
|
1039
|
+
axSets := axSets.push m
|
|
1040
|
+
IO.eprintln s!"[extract] axioms: {(← get).axioms.size} distinct, {axSets.size} sets, {(← get).axMask.size} constants walked ({(← IO.monoMsNow) - t0} ms)"
|
|
1041
|
+
|
|
1042
|
+
-- 5. what a card shows
|
|
1043
|
+
let mut nodeJs : Array Json := #[]
|
|
1044
|
+
-- the names the units' `refs` point at, each once: a unit refers to some thirty constants
|
|
1045
|
+
let mut refIndex : Std.HashMap String Nat := {}
|
|
1046
|
+
let mut refNames : Array String := #[]
|
|
1047
|
+
for h : i in [0:nodes.size] do
|
|
1048
|
+
let c := nodes[i]
|
|
1049
|
+
let some ci := env.find? c | continue
|
|
1050
|
+
let u := (privateToUserName? c).getD c
|
|
1051
|
+
let ty ← match (← get).pp[c]? with
|
|
1052
|
+
| some t => pure t
|
|
1053
|
+
| none =>
|
|
1054
|
+
let t ← try (toString <$> withOptions (·.set `format.width (100 : Nat)) (ppExpr ci.type))
|
|
1055
|
+
catch _ => pure "<failed to print>"
|
|
1056
|
+
-- what printing it inferred serves no other statement (as in `refsOf`)
|
|
1057
|
+
modifyThe Meta.State fun s => { s with cache := {} }
|
|
1058
|
+
modify fun s => { s with pp := s.pp.insert c t, dirty := s.dirty.insert (moduleOf s.modNames env c) }
|
|
1059
|
+
pure t
|
|
1060
|
+
let doc ← findDocString? env c
|
|
1061
|
+
let rng ← findDeclarationRanges? c
|
|
1062
|
+
-- a unit's version is the kernel's (TCBSpec.md, decision 3): its own content hash, then those
|
|
1063
|
+
-- of what it folds, in name order, so a change anywhere in what its command made is a change
|
|
1064
|
+
let folded := also.getD c #[]
|
|
1065
|
+
-- and of what its declarations are compiled into, where a recursive body is (compiledOf)
|
|
1066
|
+
let compiled := compiledOf env inProject owner genOwner (c :: folded.toList)
|
|
1067
|
+
let mut h := contentHash env ci
|
|
1068
|
+
for a in folded ++ compiled do
|
|
1069
|
+
if let some aci := env.find? a then h := mixHash h (contentHash env aci)
|
|
1070
|
+
-- the same content with the names it mentions taken out, and those names in order: what the
|
|
1071
|
+
-- server makes the version from (decision 3, as revised on 2026-10-09)
|
|
1072
|
+
let (shape, refs) := unitShape env (c :: folded.toList ++ compiled.toList)
|
|
1073
|
+
let name := nameOf.getD c u.toString
|
|
1074
|
+
-- a constant of the project is written by its unit's name, then, after a space, what it is of
|
|
1075
|
+
-- that unit (`Fixture.Point mk`; nothing for the unit itself), so that the server maps it
|
|
1076
|
+
-- through a rename of the unit whatever the constant is; one from outside by its own name
|
|
1077
|
+
let refName (r : Name) : String := match owner[r]? with
|
|
1078
|
+
| some o =>
|
|
1079
|
+
let uo := (userName o).toString
|
|
1080
|
+
let ur := (userName r).toString
|
|
1081
|
+
let rest := if r == o then "" else if ur.startsWith uo then (ur.drop uo.length).toString else ur
|
|
1082
|
+
let own := nameOf.getD o uo
|
|
1083
|
+
if rest.isEmpty then own else s!"{own} {(rest.dropWhile (· == '.')).toString}"
|
|
1084
|
+
| none => (userName r).toString
|
|
1085
|
+
let mut refIdxs : Array Nat := #[]
|
|
1086
|
+
for r in refs do
|
|
1087
|
+
let s := refName r
|
|
1088
|
+
match refIndex[s]? with
|
|
1089
|
+
| some k => refIdxs := refIdxs.push k
|
|
1090
|
+
| none =>
|
|
1091
|
+
refIndex := refIndex.insert s refNames.size
|
|
1092
|
+
refIdxs := refIdxs.push refNames.size
|
|
1093
|
+
refNames := refNames.push s
|
|
1094
|
+
let mut fields : List (String × Json) :=
|
|
1095
|
+
[("name", name), ("kind", if notationUnits.contains c then "notation" else kindOf env ci), ("module", modName c),
|
|
1096
|
+
("type", ty), ("ax", nodeAx[i]!), ("hash", hex h), ("shape", hex shape), ("refs", toJson refIdxs)]
|
|
1097
|
+
if name != u.toString then fields := fields ++ [("leanName", Json.str u.toString)]
|
|
1098
|
+
if let some d := display[c]? then fields := fields ++ [("display", Json.str d)]
|
|
1099
|
+
-- the kinds it interprets, by their units' names (an outside kind by its own); for a syntax, the
|
|
1100
|
+
-- units that interpret it
|
|
1101
|
+
let unitName (x : Name) : String := match owner[x]? with
|
|
1102
|
+
| some o => nameOf.getD o (userName o).toString
|
|
1103
|
+
| none => x.toString
|
|
1104
|
+
if let some ks := interprets[c]? then fields := fields ++ [("interprets", toJson (ks.map unitName))]
|
|
1105
|
+
let rs := (c :: folded.toList).filter isKindDecl |>.flatMap (fun k => (rulesOfKind.getD k #[]).toList)
|
|
1106
|
+
let ruleUnits := (rs.filterMap (owner[·]?)).filter (· != c) |>.eraseDups
|
|
1107
|
+
if !rs.isEmpty then
|
|
1108
|
+
fields := fields ++ [("rules", toJson (ruleUnits.map unitName))]
|
|
1109
|
+
-- a kind with rules outside the project: no rule of it is the only one
|
|
1110
|
+
if rs.any (!inProject ·) then fields := fields ++ [("foreignRules", Json.bool true)]
|
|
1111
|
+
if let some d := doc then fields := fields ++ [("doc", Json.str d)]
|
|
1112
|
+
if let some r := rng then
|
|
1113
|
+
fields := fields ++ [("range", Json.arr #[r.range.pos.line, r.range.pos.column,
|
|
1114
|
+
r.range.endPos.line, r.range.endPos.column])]
|
|
1115
|
+
if genLost.contains c then fields := fields ++ [("gen", Json.str "")]
|
|
1116
|
+
if !folded.isEmpty then fields := fields ++ [("also", toJson (folded.map fun a => (userName a).toString))]
|
|
1117
|
+
let fl := flagsOf env ci ++ (if grammarUnits.contains c then #["grammar"] else #[])
|
|
1118
|
+
if !fl.isEmpty then fields := fields ++ [("flags", toJson fl)]
|
|
1119
|
+
nodeJs := nodeJs.push (Json.mkObj fields)
|
|
1120
|
+
let mut extJs : Array Json := #[]
|
|
1121
|
+
for h : j in [0:exts.size] do
|
|
1122
|
+
let c := exts[j]
|
|
1123
|
+
let kind := match env.find? c with
|
|
1124
|
+
| some ci => kindOf env ci
|
|
1125
|
+
| none => "unknown"
|
|
1126
|
+
extJs := extJs.push (Json.mkObj [("name", c.toString), ("kind", kind),
|
|
1127
|
+
("module", modName c), ("ax", nodeAx[nodes.size + j]!)])
|
|
1128
|
+
let metaUnits : Nat := nodes.foldl (init := 0) fun k c =>
|
|
1129
|
+
if !notationUnits.contains c && ((env.find? c).map isMetaDecl).getD false then k + 1 else k
|
|
1130
|
+
let axioms := (← get).axioms
|
|
1131
|
+
let axSetJs := axSets.map fun m => toJson ((List.range axioms.size).filter (fun k => m.testBit k))
|
|
1132
|
+
let modJs := projMods.map fun (m, idx) =>
|
|
1133
|
+
Json.mkObj [("name", m.toString),
|
|
1134
|
+
("imports", toJson (env.header.moduleData[idx]!.imports.filterMap fun im =>
|
|
1135
|
+
if projSet.contains im.module then some im.module.toString else none))]
|
|
1136
|
+
let doc := Json.mkObj [
|
|
1137
|
+
("schema", 1), ("roots", toJson (roots.map toString)),
|
|
1138
|
+
("modules", Json.arr modJs), ("nodes", Json.arr nodeJs), ("externals", Json.arr extJs),
|
|
1139
|
+
("edges", Json.arr edges), ("axioms", toJson (axioms.map toString)),
|
|
1140
|
+
("axiomSets", Json.arr axSetJs), ("refNames", toJson refNames),
|
|
1141
|
+
("stats", Json.mkObj [("folded", auxs.size), ("generated", generatedCount), ("metaDropped", 0),
|
|
1142
|
+
("notation", notationUnits.size), ("grammar", grammarUnits.size),
|
|
1143
|
+
("meta", metaUnits),
|
|
1144
|
+
("ms", (← IO.monoMsNow) - t0)])]
|
|
1145
|
+
IO.FS.writeFile out doc.compress
|
|
1146
|
+
IO.eprintln s!"[extract] wrote {out} ({(← IO.monoMsNow) - t0} ms)"
|
|
1147
|
+
if let some dir := cache then
|
|
1148
|
+
let dirty := (← get).dirty
|
|
1149
|
+
let mut written := 0
|
|
1150
|
+
for (m, idx) in projMods do
|
|
1151
|
+
if let some key := keys[m]? then
|
|
1152
|
+
if dirty.contains m then
|
|
1153
|
+
writeModuleCache dir m key env.header.moduleData[idx]!.constNames
|
|
1154
|
+
written := written + 1
|
|
1155
|
+
IO.eprintln s!"[extract] cache: {written} module(s) written ({(← IO.monoMsNow) - t0} ms)"
|
|
1156
|
+
|
|
1157
|
+
end VisualProof
|
|
1158
|
+
|
|
1159
|
+
def main (args : List String) : IO UInt32 := do
|
|
1160
|
+
-- options may come anywhere: --cache <dir>, --salt <text>
|
|
1161
|
+
let rec split : List String → List String → Option String → String → List String × Option String × String
|
|
1162
|
+
| [], pos, c, sa => (pos.reverse, c, sa)
|
|
1163
|
+
| "--cache" :: d :: rest, pos, _, sa => split rest pos (some d) sa
|
|
1164
|
+
| "--salt" :: v :: rest, pos, c, _ => split rest pos c v
|
|
1165
|
+
| a :: rest, pos, c, sa => split rest (a :: pos) c sa
|
|
1166
|
+
let (pos, cache, salt) := split args [] none ""
|
|
1167
|
+
match pos with
|
|
1168
|
+
| out :: projectDir :: roots@(_ :: _) =>
|
|
1169
|
+
initSearchPath (← findSysroot)
|
|
1170
|
+
let roots := roots.toArray.map String.toName
|
|
1171
|
+
-- extension state (instances, classes, matchers) is only built when extensions load, and
|
|
1172
|
+
-- loading them may run imported initializers
|
|
1173
|
+
unsafe enableInitializersExecution
|
|
1174
|
+
let env ← importModules (roots.map ({ module := · })) {} (trustLevel := 1024) (loadExts := true)
|
|
1175
|
+
let ctx : Core.Context := { fileName := "<visualproof>", fileMap := default, maxHeartbeats := 0 }
|
|
1176
|
+
let _ ← ((VisualProof.run out projectDir roots (cache.map (⟨·⟩)) salt).run' {} : MetaM Unit).toIO ctx { env }
|
|
1177
|
+
return 0
|
|
1178
|
+
| _ =>
|
|
1179
|
+
IO.eprintln "usage: lean --run Extract.lean <out.json> <project-lib-dir> <root-module>... [--cache dir] [--salt text]"
|
|
1180
|
+
return 2
|