visualproof 0.0.0-stage → 0.2.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.
Files changed (146) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +99 -2
  3. package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
  4. package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
  5. package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
  6. package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
  7. package/dist/assets/latex/visualproof.sty +308 -0
  8. package/dist/assets/webfonts/LICENSE-Jost.txt +93 -0
  9. package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
  10. package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
  11. package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
  12. package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
  13. package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
  14. package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
  15. package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
  16. package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
  17. package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
  18. package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  19. package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
  20. package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
  21. package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
  22. package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
  23. package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
  24. package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -0
  25. package/dist/extractor/Extract.lean +1180 -0
  26. package/dist/extractor/Usage.lean +900 -0
  27. package/dist/mcp/visualproof-mcp.mjs +476 -0
  28. package/dist/server/access.js +95 -0
  29. package/dist/server/agent.js +3270 -0
  30. package/dist/server/agentdocs.js +35 -0
  31. package/dist/server/agentlibrary.js +202 -0
  32. package/dist/server/agentnotes.js +701 -0
  33. package/dist/server/agentviews.js +703 -0
  34. package/dist/server/askfollow.js +286 -0
  35. package/dist/server/boardspots.js +159 -0
  36. package/dist/server/dirs.js +175 -0
  37. package/dist/server/doctor.js +186 -0
  38. package/dist/server/events.js +198 -0
  39. package/dist/server/export.js +420 -0
  40. package/dist/server/extract.js +204 -0
  41. package/dist/server/fetchurl.js +321 -0
  42. package/dist/server/figures.js +158 -0
  43. package/dist/server/history.js +780 -0
  44. package/dist/server/historyapi.js +304 -0
  45. package/dist/server/hover.js +595 -0
  46. package/dist/server/identity.js +114 -0
  47. package/dist/server/invites.js +241 -0
  48. package/dist/server/layoutthread.js +51 -0
  49. package/dist/server/library.js +600 -0
  50. package/dist/server/libraryroute.js +211 -0
  51. package/dist/server/live.js +1101 -0
  52. package/dist/server/lostfound.js +199 -0
  53. package/dist/server/main.js +1406 -0
  54. package/dist/server/mcptools.js +496 -0
  55. package/dist/server/pastdoc.js +73 -0
  56. package/dist/server/pipeline.js +159 -0
  57. package/dist/server/poppler.js +251 -0
  58. package/dist/server/presence.js +252 -0
  59. package/dist/server/project.js +222 -0
  60. package/dist/server/prompt.js +53 -0
  61. package/dist/server/readonly.js +152 -0
  62. package/dist/server/readonlygate.js +123 -0
  63. package/dist/server/renames.js +268 -0
  64. package/dist/server/review.js +334 -0
  65. package/dist/server/reviewapi.js +188 -0
  66. package/dist/server/snapshot.js +50 -0
  67. package/dist/server/sourcestore.js +473 -0
  68. package/dist/server/status.js +360 -0
  69. package/dist/server/talk.js +476 -0
  70. package/dist/server/tokens.js +221 -0
  71. package/dist/server/usage.js +501 -0
  72. package/dist/server/versions.js +82 -0
  73. package/dist/server/view.js +164 -0
  74. package/dist/server/views.js +1167 -0
  75. package/dist/shared/askfollow.js +46 -0
  76. package/dist/shared/board.js +332 -0
  77. package/dist/shared/build.js +2 -0
  78. package/dist/shared/collab.js +241 -0
  79. package/dist/shared/comment.js +285 -0
  80. package/dist/shared/context.js +157 -0
  81. package/dist/shared/doc/authors.js +98 -0
  82. package/dist/shared/doc/build.js +358 -0
  83. package/dist/shared/doc/latex.js +455 -0
  84. package/dist/shared/doc/markdown.js +159 -0
  85. package/dist/shared/doc/model.js +37 -0
  86. package/dist/shared/doc/order.js +124 -0
  87. package/dist/shared/doc/outline.js +45 -0
  88. package/dist/shared/doc/proof.js +272 -0
  89. package/dist/shared/doc/sections.js +93 -0
  90. package/dist/shared/doc/settings.js +75 -0
  91. package/dist/shared/edgecodec.js +102 -0
  92. package/dist/shared/editor.js +64 -0
  93. package/dist/shared/excerpt.js +87 -0
  94. package/dist/shared/graph.js +16 -0
  95. package/dist/shared/items.js +92 -0
  96. package/dist/shared/keys.js +203 -0
  97. package/dist/shared/layout.js +926 -0
  98. package/dist/shared/lexer.js +130 -0
  99. package/dist/shared/library.js +711 -0
  100. package/dist/shared/lostfound.js +150 -0
  101. package/dist/shared/moves.js +244 -0
  102. package/dist/shared/notecard.js +295 -0
  103. package/dist/shared/noteplace.js +505 -0
  104. package/dist/shared/palette.js +46 -0
  105. package/dist/shared/pastgraph.js +103 -0
  106. package/dist/shared/places.js +80 -0
  107. package/dist/shared/projectnotes.js +244 -0
  108. package/dist/shared/query.js +818 -0
  109. package/dist/shared/review.js +642 -0
  110. package/dist/shared/reviewqueue.js +333 -0
  111. package/dist/shared/ring.js +366 -0
  112. package/dist/shared/scope.js +255 -0
  113. package/dist/shared/scopeedit.js +81 -0
  114. package/dist/shared/search.js +191 -0
  115. package/dist/shared/separate.js +178 -0
  116. package/dist/shared/sharesnap.js +281 -0
  117. package/dist/shared/sticky.js +501 -0
  118. package/dist/shared/timeline.js +73 -0
  119. package/dist/shared/titles.js +300 -0
  120. package/dist/shared/viewgraph.js +157 -0
  121. package/dist/shared/views.js +859 -0
  122. package/dist/web/bundle/app.js +52671 -0
  123. package/dist/web/bundle/edges.js +407 -0
  124. package/dist/web/bundle/layout.js +1010 -0
  125. package/dist/web/bundle/pdfworker.js +48322 -0
  126. package/dist/web/index.html +257 -0
  127. package/dist/web/src/motion.js +430 -0
  128. package/dist/web/src/past.js +224 -0
  129. package/dist/web/src/scene.js +804 -0
  130. package/dist/web/style.css +2548 -0
  131. package/docs/agent-api/README.md +91 -0
  132. package/docs/agent-api/advice.md +108 -0
  133. package/docs/agent-api/export.md +120 -0
  134. package/docs/agent-api/following.md +140 -0
  135. package/docs/agent-api/history.md +142 -0
  136. package/docs/agent-api/library.md +65 -0
  137. package/docs/agent-api/mcp.md +104 -0
  138. package/docs/agent-api/notes.md +93 -0
  139. package/docs/agent-api/reading.md +166 -0
  140. package/docs/agent-api/requests.md +94 -0
  141. package/docs/agent-api/talking.md +100 -0
  142. package/docs/agent-api/tcb.md +135 -0
  143. package/docs/agent-api/views.md +125 -0
  144. package/docs/agent-api/words.md +56 -0
  145. package/package.json +39 -4
  146. 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