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.
Files changed (128) 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/extractor/Extract.lean +1180 -0
  9. package/dist/extractor/Usage.lean +900 -0
  10. package/dist/mcp/visualproof-mcp.mjs +476 -0
  11. package/dist/server/access.js +95 -0
  12. package/dist/server/agent.js +3235 -0
  13. package/dist/server/agentdocs.js +35 -0
  14. package/dist/server/agentlibrary.js +202 -0
  15. package/dist/server/agentnotes.js +701 -0
  16. package/dist/server/agentviews.js +703 -0
  17. package/dist/server/askfollow.js +286 -0
  18. package/dist/server/boardspots.js +159 -0
  19. package/dist/server/dirs.js +175 -0
  20. package/dist/server/doctor.js +186 -0
  21. package/dist/server/events.js +198 -0
  22. package/dist/server/export.js +420 -0
  23. package/dist/server/extract.js +204 -0
  24. package/dist/server/fetchurl.js +321 -0
  25. package/dist/server/figures.js +158 -0
  26. package/dist/server/history.js +741 -0
  27. package/dist/server/historyapi.js +304 -0
  28. package/dist/server/hover.js +595 -0
  29. package/dist/server/identity.js +114 -0
  30. package/dist/server/invites.js +241 -0
  31. package/dist/server/layoutthread.js +51 -0
  32. package/dist/server/library.js +600 -0
  33. package/dist/server/libraryroute.js +211 -0
  34. package/dist/server/live.js +1101 -0
  35. package/dist/server/lostfound.js +199 -0
  36. package/dist/server/main.js +1394 -0
  37. package/dist/server/mcptools.js +497 -0
  38. package/dist/server/pastdoc.js +73 -0
  39. package/dist/server/pipeline.js +156 -0
  40. package/dist/server/poppler.js +251 -0
  41. package/dist/server/presence.js +252 -0
  42. package/dist/server/project.js +222 -0
  43. package/dist/server/prompt.js +53 -0
  44. package/dist/server/readonly.js +152 -0
  45. package/dist/server/readonlygate.js +123 -0
  46. package/dist/server/renames.js +268 -0
  47. package/dist/server/review.js +334 -0
  48. package/dist/server/reviewapi.js +188 -0
  49. package/dist/server/snapshot.js +49 -0
  50. package/dist/server/sourcestore.js +473 -0
  51. package/dist/server/status.js +360 -0
  52. package/dist/server/talk.js +476 -0
  53. package/dist/server/tokens.js +221 -0
  54. package/dist/server/usage.js +501 -0
  55. package/dist/server/versions.js +82 -0
  56. package/dist/server/view.js +157 -0
  57. package/dist/server/views.js +1167 -0
  58. package/dist/shared/askfollow.js +46 -0
  59. package/dist/shared/board.js +332 -0
  60. package/dist/shared/build.js +2 -0
  61. package/dist/shared/collab.js +148 -0
  62. package/dist/shared/comment.js +285 -0
  63. package/dist/shared/context.js +157 -0
  64. package/dist/shared/doc/authors.js +98 -0
  65. package/dist/shared/doc/build.js +358 -0
  66. package/dist/shared/doc/latex.js +455 -0
  67. package/dist/shared/doc/markdown.js +159 -0
  68. package/dist/shared/doc/model.js +37 -0
  69. package/dist/shared/doc/order.js +107 -0
  70. package/dist/shared/doc/proof.js +272 -0
  71. package/dist/shared/doc/sections.js +93 -0
  72. package/dist/shared/doc/settings.js +75 -0
  73. package/dist/shared/edgecodec.js +102 -0
  74. package/dist/shared/editor.js +64 -0
  75. package/dist/shared/excerpt.js +87 -0
  76. package/dist/shared/graph.js +16 -0
  77. package/dist/shared/items.js +92 -0
  78. package/dist/shared/keys.js +203 -0
  79. package/dist/shared/layout.js +926 -0
  80. package/dist/shared/lexer.js +130 -0
  81. package/dist/shared/library.js +711 -0
  82. package/dist/shared/lostfound.js +150 -0
  83. package/dist/shared/moves.js +243 -0
  84. package/dist/shared/notecard.js +295 -0
  85. package/dist/shared/noteplace.js +505 -0
  86. package/dist/shared/palette.js +46 -0
  87. package/dist/shared/pastgraph.js +95 -0
  88. package/dist/shared/places.js +80 -0
  89. package/dist/shared/projectnotes.js +244 -0
  90. package/dist/shared/query.js +810 -0
  91. package/dist/shared/review.js +642 -0
  92. package/dist/shared/reviewqueue.js +333 -0
  93. package/dist/shared/ring.js +366 -0
  94. package/dist/shared/scope.js +255 -0
  95. package/dist/shared/scopeedit.js +81 -0
  96. package/dist/shared/search.js +191 -0
  97. package/dist/shared/separate.js +178 -0
  98. package/dist/shared/sharesnap.js +280 -0
  99. package/dist/shared/sticky.js +501 -0
  100. package/dist/shared/timeline.js +73 -0
  101. package/dist/shared/titles.js +298 -0
  102. package/dist/shared/viewgraph.js +139 -0
  103. package/dist/shared/views.js +859 -0
  104. package/dist/web/bundle/app.js +48733 -0
  105. package/dist/web/bundle/edges.js +409 -0
  106. package/dist/web/bundle/layout.js +1010 -0
  107. package/dist/web/bundle/pdfworker.js +48322 -0
  108. package/dist/web/index.html +239 -0
  109. package/dist/web/src/motion.js +430 -0
  110. package/dist/web/src/past.js +224 -0
  111. package/dist/web/src/scene.js +787 -0
  112. package/dist/web/style.css +2312 -0
  113. package/docs/agent-api/README.md +91 -0
  114. package/docs/agent-api/advice.md +108 -0
  115. package/docs/agent-api/export.md +120 -0
  116. package/docs/agent-api/following.md +140 -0
  117. package/docs/agent-api/history.md +142 -0
  118. package/docs/agent-api/library.md +65 -0
  119. package/docs/agent-api/mcp.md +104 -0
  120. package/docs/agent-api/notes.md +93 -0
  121. package/docs/agent-api/reading.md +166 -0
  122. package/docs/agent-api/requests.md +94 -0
  123. package/docs/agent-api/talking.md +100 -0
  124. package/docs/agent-api/tcb.md +135 -0
  125. package/docs/agent-api/views.md +124 -0
  126. package/docs/agent-api/words.md +56 -0
  127. package/package.json +39 -4
  128. package/test/toolchains.json +5 -0
@@ -0,0 +1,900 @@
1
+ /-
2
+ VisualProof usage pass: one module of a built Lean project, elaborated → which notation each of its
3
+ commands is written in, and the context each is written in (TCBSpec.md, decisions 2 and 4).
4
+
5
+ Run with the target project's LEAN_PATH, one process per module (a process cannot import twice):
6
+
7
+ vp-usage <source.lean> <Module> <out.json> <project-modules.txt> [-Dname=value]...
8
+
9
+ `project-modules.txt` lists the project's modules, one per line: a syntax kind or an implementation
10
+ declared in one of them (or in the module itself) is the project's; notation from outside the
11
+ project is not recorded at all, whatever the review's trusted prefixes say (TCBSpec.md, decision 2
12
+ and its implementation notes). The `-D` options are those Lake compiled the module with (its trace
13
+ records them), so that the module elaborates as it did in the build.
14
+
15
+ The module is elaborated under exactly its imports, with its proofs left out (variant Bk of the
16
+ spike): the values of `theorem`, `lemma` and `example`, and `decreasing_by`, become `sorry`; a `by`
17
+ block anywhere else is kept and told apart by its goal (a proposition is a proof, anything else is
18
+ data); `#` commands are skipped; `c₁ in c₂` runs as `section c₁ c₂ end`, as Lean expands it, so that
19
+ `c₂` is recorded with its context. The program only reads. It writes `out.json` and nothing else:
20
+ no `.olean`, no `.ilean`.
21
+
22
+ Output, one JSON object:
23
+ module, ms the module, and where the time went (import, elaboration, total)
24
+ errors commands that did not elaborate: [{line, message}] (the rest is still recorded)
25
+ contexts the distinct contexts commands are written in, each in the shape of
26
+ shared/scope.ts's FileContext: namespace, blocks, opens, variables, universes,
27
+ options, local — as the elaborator's scope has them before the command, every
28
+ entry with the line of the command that put it there
29
+ cmds the commands that declare a constant or use project notation:
30
+ {line, endLine, consts, ctx, incl, uses, proofs?}
31
+ consts the constants the command declared (user names; internal ones left out, but for
32
+ the `_aux_` declarations of syntax extensions)
33
+ ctx its context, an index into `contexts`
34
+ incl constant → the indices of the `variables` of its context Lean included in it
35
+ uses [kind, impl, site, line, col, by]: a project syntax kind parsed in its text, or a
36
+ macro or elaborator that ran on it (`impl`, with the kind it ran for), "" where
37
+ there is none; `site` 1 = statement, 2 = definition body, 4 = proof; where; and
38
+ `by`, the index in `uses` of the project implementation whose expansion emitted
39
+ it, or -1 when it is written in the text
40
+ proofs where the proofs inside its definitions' bodies are, for the exported document to
41
+ cut (ExportSpec decision 8): [line, col, endLine, endCol], columns in code points,
42
+ end exclusive, sorted and merged — `by` blocks whose goal is a proposition, the
43
+ tactics of `decreasing_by`, term-mode proofs as written; none in a signature or a
44
+ quotation. Absent when there are none.
45
+ -/
46
+ import Lean
47
+
48
+ open Lean Elab Command Parser System
49
+
50
+ namespace VisualProof.Usage
51
+
52
+ /-! ## Sites, as Extract.lean has them -/
53
+
54
+ def stmt : Nat := 1
55
+ def body : Nat := 2
56
+ def proof : Nat := 4
57
+
58
+ /-! ## Syntax -/
59
+
60
+ def kindLast : Name → String
61
+ | .str _ s => s
62
+ | _ => ""
63
+
64
+ def isQuotKind (k : Name) : Bool :=
65
+ let s := kindLast k
66
+ s == "quot" || s == "quotSeq" || s == "dynamicQuot" || s == "precheckedQuot"
67
+
68
+ /-- A declaration whose value is a proof, or never a unit (`example`). -/
69
+ def isTheoremKind (k : Name) : Bool :=
70
+ k == ``Lean.Parser.Command.theorem || kindLast k == "lemma" || k == ``Lean.Parser.Command.example
71
+
72
+ def declValKinds : List Name :=
73
+ [``Lean.Parser.Command.declValSimple, ``Lean.Parser.Command.declValEqns,
74
+ ``Lean.Parser.Command.whereStructInst]
75
+
76
+ def sigKinds : List Name := [``Lean.Parser.Command.declSig, ``Lean.Parser.Command.optDeclSig]
77
+
78
+ /-- Syntax extension commands: never stripped, whatever their quotations hold. -/
79
+ def extCmdKinds : List Name :=
80
+ [``Lean.Parser.Command.syntax, ``Lean.Parser.Command.syntaxAbbrev, ``Lean.Parser.Command.syntaxCat,
81
+ ``Lean.Parser.Command.notation, ``Lean.Parser.Command.macro, ``Lean.Parser.Command.elab,
82
+ ``Lean.Parser.Command.mixfix, ``Lean.Parser.Command.macro_rules, ``Lean.Parser.Command.elab_rules]
83
+
84
+ partial def firstKind? (stx : Syntax) (k : Name) : Option Syntax :=
85
+ if stx.getKind == k then some stx else stx.getArgs.findSome? (firstKind? · k)
86
+
87
+ /-- Whether a command is of `local` or `scoped` attribute kind: `local notation`, `scoped instance`,
88
+ `attribute [local simp] …`. What it declares holds while its section or namespace is open. -/
89
+ partial def localKind? (stx : Syntax) : Option Syntax :=
90
+ if stx.getKind == ``Lean.Parser.Term.local || stx.getKind == ``Lean.Parser.Term.scoped then some stx
91
+ else if isQuotKind stx.getKind then none
92
+ else stx.getArgs.findSome? localKind?
93
+
94
+ /-- The first atom of a command, its doc comment skipped. -/
95
+ partial def firstAtom (stx : Syntax) : String :=
96
+ match stx with
97
+ | .atom _ v => v
98
+ | .node _ k args =>
99
+ if k == ``Lean.Parser.Command.docComment then "" else
100
+ args.foldl (fun acc a => if acc.isEmpty then firstAtom a else acc) ""
101
+ | _ => ""
102
+
103
+ /-- Whether a piece of syntax was written in the file, rather than made by an expansion: its own
104
+ first token (an atom of its node, or of the groups directly under it) carries the parser's
105
+ position. A notation an expansion emits has the expansion's synthetic position on its tokens,
106
+ though what it was handed (`$x`) keeps the reader's; so the node's own tokens are asked, not its
107
+ first one, which may be such an argument. -/
108
+ partial def written (stx : Syntax) : Bool :=
109
+ match ownAtom? stx with
110
+ | some (.atom (.original ..) _) => true
111
+ | some _ => false
112
+ | none => stx.getHeadInfo matches .original ..
113
+ where
114
+ ownAtom? (s : Syntax) : Option Syntax :=
115
+ s.getArgs.findSome? fun a => match a with
116
+ | .atom .. => some a
117
+ | .node _ k _ => if k == nullKind || k == groupKind then ownAtom? a else none
118
+ | _ => none
119
+
120
+ /-! ## Regions of a command, from its syntax
121
+
122
+ A position in a command is in its statement, its definition body or a proof. `by` blocks are
123
+ decided by their goal once elaborated (`ByBlock`); until then they count as proof. -/
124
+
125
+ inductive Region where
126
+ | site (s : Nat)
127
+ /-- the `k`-th `by` block of the command, inside a part of site `outer` -/
128
+ | byBlock (k : Nat) (outer : Nat)
129
+ deriving Inhabited
130
+
131
+ structure Iv where
132
+ s : Nat
133
+ e : Nat
134
+ r : Region
135
+
136
+ /-- A `by` block of a command: the `k`-th, where it is, the site of the part it is in, and whether
137
+ it is inside a quotation (syntax a macro builds, not a term of the command). -/
138
+ structure ByIv where
139
+ k : Nat
140
+ s : Nat
141
+ e : Nat
142
+ outer : Nat
143
+ quot : Bool
144
+
145
+ structure ParseOut where
146
+ /-- every syntax kind parsed, with its position -/
147
+ kinds : Array (Name × Nat) := #[]
148
+ ivs : Array Iv := #[]
149
+ byBlocks : Nat := 0
150
+ bys : Array ByIv := #[]
151
+ /-- the tactics of a definition's `decreasing_by`: a proof, cut from the exported text -/
152
+ decr : Array (Nat × Nat) := #[]
153
+
154
+ partial def walkStx (stx : Syntax) (r : Region) (inThm : Bool) (inQuot : Bool := false) : StateM ParseOut Unit := do
155
+ match stx with
156
+ | .node _ k args =>
157
+ let pos? := (stx.getPos? (canonicalOnly := false)).map (·.byteIdx)
158
+ let tail? := (stx.getTailPos? (canonicalOnly := false)).map (·.byteIdx)
159
+ let inThm := inThm || isTheoremKind k
160
+ let outer := match r with | .site s => s | .byBlock _ o => o
161
+ let isProof := match r with | .site s => s == proof | _ => false
162
+ let mut r' := r
163
+ if isProof then pure ()
164
+ else if k == ``Lean.Parser.Term.byTactic then
165
+ let n := (← get).byBlocks
166
+ r' := .byBlock n outer
167
+ modify fun s => { s with byBlocks := n + 1 }
168
+ if let (some a, some b) := (pos?, tail?) then
169
+ modify fun s => { s with bys := s.bys.push { k := n, s := a, e := b, outer, quot := inQuot } }
170
+ else if k == ``Lean.Parser.Termination.decreasingBy || k == ``Lean.Parser.Termination.terminationBy then
171
+ -- `decreasing_by tacs`: the tactics are a proof, the keyword stays
172
+ if k == ``Lean.Parser.Termination.decreasingBy && outer == body && !inQuot then
173
+ let tacs := stx[1]
174
+ if let (some a, some b) := (tacs.getPos? (canonicalOnly := false), tacs.getTailPos? (canonicalOnly := false)) then
175
+ modify fun s => { s with decr := s.decr.push (a.byteIdx, b.byteIdx) }
176
+ r' := .site proof
177
+ else if declValKinds.contains k then r' := .site (if inThm then proof else body)
178
+ else if sigKinds.contains k || k == ``Lean.Parser.Command.declaration then r' := .site stmt
179
+ let changed := match r, r' with
180
+ | .site x, .site y => x != y
181
+ | .byBlock x _, .byBlock y _ => x != y
182
+ | _, _ => true
183
+ if let (true, some a, some b) := (changed, pos?, tail?) then
184
+ modify fun s => { s with ivs := s.ivs.push ⟨a, b, r'⟩ }
185
+ if k != nullKind && !isQuotKind k then
186
+ modify fun s => { s with kinds := s.kinds.push (k, pos?.getD 0) }
187
+ let inQuot := inQuot || isQuotKind k
188
+ for a in args do walkStx a r' inThm inQuot
189
+ | _ => pure ()
190
+
191
+ /-- The site of a position, once the command's `by` blocks are known to be data (`byData`). -/
192
+ def siteAt (ivs : Array Iv) (byData : Std.HashSet Nat) (p : Nat) : Nat := Id.run do
193
+ let mut best : Region := .site stmt
194
+ let mut bestLen := 1000000000
195
+ for iv in ivs do
196
+ if iv.s ≤ p && p < iv.e && iv.e - iv.s < bestLen then
197
+ best := iv.r; bestLen := iv.e - iv.s
198
+ return match best with
199
+ | .site s => s
200
+ | .byBlock k outer => if byData.contains k then outer else proof
201
+
202
+ /-- Whether a position is in a definition's body, or in a `by` block there (data or proof: the
203
+ block's goal says which). -/
204
+ def inBody (ivs : Array Iv) (p : Nat) : Bool := Id.run do
205
+ let mut best : Region := .site stmt
206
+ let mut bestLen := 1000000000
207
+ for iv in ivs do
208
+ if iv.s ≤ p && p < iv.e && iv.e - iv.s < bestLen then
209
+ best := iv.r; bestLen := iv.e - iv.s
210
+ return match best with
211
+ | .site s => s == body
212
+ | .byBlock _ outer => outer == body
213
+
214
+ def byBlockAt (ivs : Array Iv) (p : Nat) : Option Nat := Id.run do
215
+ let mut best : Option Nat := none
216
+ let mut bestLen := 1000000000
217
+ for iv in ivs do
218
+ if let .byBlock k _ := iv.r then
219
+ if iv.s ≤ p && p < iv.e && iv.e - iv.s < bestLen then
220
+ best := some k; bestLen := iv.e - iv.s
221
+ return best
222
+
223
+ /-! ## Proofs left out -/
224
+
225
+ structure Templates where
226
+ sorryVal : Syntax
227
+ decrSorry : Syntax
228
+
229
+ partial def setAllInfo (info : SourceInfo) : Syntax → Syntax
230
+ | .node _ k args => .node info k (args.map (setAllInfo info))
231
+ | .atom _ v => .atom info v
232
+ | .ident _ raw n pre => .ident info raw n pre
233
+ | s => s
234
+
235
+ def mkTemplates (env : Environment) : IO Templates := do
236
+ let parse (s : String) : IO Syntax := match runParserCategory env `command s with
237
+ | .ok stx => pure stx
238
+ | .error e => throw (IO.userError s!"template: {e}")
239
+ let t ← parse "theorem t : True := sorry"
240
+ let d ← parse "def f (n : Nat) : Nat := n\ndecreasing_by sorry"
241
+ let some sv := firstKind? t ``Lean.Parser.Command.declValSimple | throw (IO.userError "declValSimple")
242
+ let some db := firstKind? d ``Lean.Parser.Termination.decreasingBy | throw (IO.userError "decreasingBy")
243
+ return { sorryVal := sv, decrSorry := db }
244
+
245
+ /-- The command with its proofs replaced by `sorry`: the value of a theorem, lemma or example and a
246
+ `decreasing_by` clause. `by` blocks elsewhere are kept, in a signature as in a definition's
247
+ value: whether one is a proof is told by its goal once elaborated. Quotations are left alone. -/
248
+ partial def strip (t : Templates) (stx : Syntax) (inThm : Bool) : Syntax :=
249
+ match stx with
250
+ | .node info k args =>
251
+ if isQuotKind k then stx else
252
+ let ref := SourceInfo.fromRef stx (canonical := false)
253
+ if k == ``Lean.Parser.Termination.decreasingBy then setAllInfo ref t.decrSorry else
254
+ let inThm := inThm || isTheoremKind k
255
+ if inThm && declValKinds.contains k then setAllInfo ref t.sorryVal
256
+ else .node info k (args.map (strip t · inThm))
257
+ | _ => stx
258
+
259
+ /-! ## Running commands -/
260
+
261
+ def liftCmd (ictx : InputContext) (st : Command.State) (x : CommandElabM α) (pos : Nat := 0) :
262
+ IO (α × Command.State) := do
263
+ let ctx : Command.Context := {
264
+ cmdPos := ⟨pos⟩, fileName := ictx.fileName, fileMap := ictx.fileMap, snap? := none,
265
+ cancelTk? := none }
266
+ match ← (x ctx |>.run st).toIO' with
267
+ | .error e => throw (IO.userError s!"internal: {← e.toMessageData.toString}")
268
+ | .ok r => return r
269
+
270
+ /-! ## The project -/
271
+
272
+ /-- Which names are the project's: declared in a module of the project, or in this one. -/
273
+ structure Project where
274
+ mask : Array Bool
275
+
276
+ def Project.has (p : Project) (env : Environment) (n : Name) : Bool :=
277
+ match env.getModuleIdxFor? n with
278
+ | some idx => p.mask[idx.toNat]?.getD false
279
+ | none => env.contains n
280
+
281
+ def userName (n : Name) : Name := (privateToUserName? n).getD n
282
+
283
+ def nameStr (n : Name) : String := if n.isAnonymous then "" else (userName n).toString
284
+
285
+ /-! ## Info trees: the macros and elaborators that ran -/
286
+
287
+ structure Use where
288
+ kind : Name
289
+ impl : Name
290
+ site : Nat
291
+ pos : Nat
292
+ by_ : Int
293
+
294
+ structure InfoOut where
295
+ uses : Array Use := #[]
296
+ /-- `by` blocks of the command whose goal is not a proposition -/
297
+ byData : Std.HashSet Nat := {}
298
+ /-- the same, and those nested in another block's tactics, each told apart by its own goal: which
299
+ blocks are cut from the exported text (`byData` keeps the sites the TCB has always had) -/
300
+ spanData : Std.HashSet Nat := {}
301
+ /-- term-mode proofs in a definition's body, as written: [start, end) -/
302
+ termProofs : Array (Nat × Nat) := #[]
303
+
304
+ abbrev PathElt := ContextInfo × TermInfo × IO.Ref (Option Bool)
305
+
306
+ /-- Whether some enclosing term is a proof, memoised along the path. -/
307
+ def underProof (path : Array PathElt) : IO Bool := do
308
+ for (ci, ti, r) in path do
309
+ let v ← match ← r.get with
310
+ | some v => pure v
311
+ | none => do
312
+ let v ← try ci.runMetaM ti.lctx (do Meta.isProof (← instantiateMVars ti.expr)) catch _ => pure false
313
+ r.set (some v); pure v
314
+ if v then return true
315
+ return false
316
+
317
+ structure WalkCtx where
318
+ proj : Project
319
+ ivs : Array Iv
320
+ /-- the file's text, as bytes -/
321
+ bytes : ByteArray := .empty
322
+ /-- the use of the project implementation whose expansion this is under, or -1 -/
323
+ encl : Int := -1
324
+ inTac : Bool := false
325
+ /-- the `by` block the enclosing tactic is in -/
326
+ curBy : Option Nat := none
327
+ /-- under a term-mode proof recorded already -/
328
+ inCut : Bool := false
329
+
330
+ /-- Up to two of the tokens a piece of syntax is written with. -/
331
+ partial def tokens2 (stx : Syntax) (acc : Nat := 0) : Nat :=
332
+ if acc ≥ 2 then acc else match stx with
333
+ | .atom .. | .ident .. => acc + 1
334
+ | .node _ _ args => args.foldl (fun n a => tokens2 a n) acc
335
+ | _ => acc
336
+
337
+ /-- Whether `:=` comes right before byte `p`, white space between. -/
338
+ def afterAssign (bytes : ByteArray) (p : Nat) : Bool := Id.run do
339
+ let mut i := min p bytes.size
340
+ while i > 0 && (bytes.get! (i - 1) == 32 || bytes.get! (i - 1) == 10 || bytes.get! (i - 1) == 13 || bytes.get! (i - 1) == 9) do
341
+ i := i - 1
342
+ return i ≥ 2 && bytes.get! (i - 2) == 58 && bytes.get! (i - 1) == 61
343
+
344
+ /-- Whether byte `i` of the text is `.`. -/
345
+ def dotAt (bytes : ByteArray) (i : Nat) : Bool := i < bytes.size && bytes.get! i == 46
346
+
347
+ /-- Where a term that may be a proof inside a definition's body is written: its first and last
348
+ tokens as the parser read them (a term an expansion made has no place of its own, and its
349
+ synthetic one may span what is not it), in the body. A single token — a lemma's or a hypothesis'
350
+ name, `rfl` — counts only as a whole value (`field := Nat.add_comm`): elsewhere it is a name,
351
+ not the text of a proof. Nor is a part of a name: the receiver of dot notation that Lean splits
352
+ off a written name (`DFunLike.coe_injective` of `DFunLike.coe_injective.addCommMonoid`). -/
353
+ def cutRange? (c : WalkCtx) (stx : Syntax) : Option (Nat × Nat) := do
354
+ let .original _ a _ _ := stx.getHeadInfo | none
355
+ let .original _ _ _ b := stx.getTailInfo | none
356
+ let (a, b) := (a.byteIdx, b.byteIdx)
357
+ if !(a < b && written stx && inBody c.ivs a) then none
358
+ -- parentheses are the proof's punctuation, not the proof: from v4.35 Lean records them as a
359
+ -- term of their own, whose cut would take them away; the term inside them is cut instead
360
+ if stx.isOfKind ``Lean.Parser.Term.paren then none
361
+ if stx.isIdent && ((a > 0 && dotAt c.bytes (a - 1)) || dotAt c.bytes b) then none
362
+ if tokens2 stx < 2 && !afterAssign c.bytes a then none
363
+ return (a, b)
364
+
365
+ partial def walkInfo (c : WalkCtx) (ctx? : Option ContextInfo) (t : InfoTree) (pos : Nat)
366
+ (path : Array PathElt) : StateRefT InfoOut IO Unit := do
367
+ match t with
368
+ | .context pci t => walkInfo c (pci.mergeIntoOuter? ctx?) t pos path
369
+ | .node info children =>
370
+ let some ci := ctx? | return
371
+ let env := ci.env
372
+ let pos := (info.stx.getPos? (canonicalOnly := false)).map (·.byteIdx) |>.getD pos
373
+ let mut path := path
374
+ let mut cut := false
375
+ if let .ofTermInfo ti := info then
376
+ let r ← IO.mkRef none
377
+ path := path.push (ci, ti, r)
378
+ -- a term-mode proof in a definition's body, written in the text: cut from the exported text
379
+ if !c.inCut && !ti.isBinder then
380
+ if let some (a, b) := cutRange? c info.stx then
381
+ let v ← try ci.runMetaM ti.lctx (do Meta.isProof (← instantiateMVars ti.expr)) catch _ => pure false
382
+ r.set (some v)
383
+ if v then
384
+ modify fun s => { s with termProofs := s.termProofs.push (a, b) }
385
+ cut := true
386
+ -- the outermost tactic of a `by` block: a goal that is not a proposition makes it data; so for
387
+ -- the first tactic of a block nested in another's tactics, for the cut alone
388
+ let blk := if info matches .ofTacticInfo _ then byBlockAt c.ivs pos else none
389
+ if let .ofTacticInfo ti := info then
390
+ if !c.inTac || (blk.isSome && blk != c.curBy) then
391
+ if let some g := ti.goalsBefore.head? then
392
+ let ci' : ContextInfo := { ci with mctx := ti.mctxBefore }
393
+ let isPrf ← try ci'.runMetaM {} (g.withContext do Meta.isProp (← instantiateMVars (← g.getType)))
394
+ catch _ => pure true
395
+ if !isPrf then
396
+ if let some k := blk then
397
+ if !c.inTac then modify fun s => { s with byData := s.byData.insert k }
398
+ modify fun s => { s with spanData := s.spanData.insert k }
399
+ let site : StateRefT InfoOut IO Nat := do
400
+ let s := siteAt c.ivs (← get).byData pos
401
+ if s != proof && (← underProof path) then return proof else return s
402
+ let mut c' := { c with inTac := c.inTac || info matches .ofTacticInfo _, inCut := c.inCut || cut }
403
+ if info matches .ofTacticInfo _ then c' := { c' with curBy := blk }
404
+ let record (kind impl : Name) (stx : Syntax) : StateRefT InfoOut IO Int := do
405
+ let s ← site
406
+ let by_ := if written stx then -1 else c.encl
407
+ let k := (← get).uses.size
408
+ modify fun o => { o with uses := o.uses.push { kind, impl, site := s, pos, by_ } }
409
+ return k
410
+ let elabInfo? : Option ElabInfo := match info with
411
+ | .ofTermInfo i => some i.toElabInfo
412
+ | .ofPartialTermInfo i => some i.toElabInfo
413
+ | .ofCommandInfo i => some i.toElabInfo
414
+ | .ofTacticInfo i => some i.toElabInfo
415
+ | _ => none
416
+ if let some ei := elabInfo? then
417
+ let k := ei.stx.getKind
418
+ let projImpl := c.proj.has env ei.elaborator
419
+ let projKind := c.proj.has env k
420
+ if projImpl || projKind then
421
+ let u ← record (if projKind then k else .anonymous) (if projImpl then ei.elaborator else .anonymous) ei.stx
422
+ -- what the implementation's expansion holds is its own, unless it was written in the text
423
+ if projImpl then c' := { c' with encl := u }
424
+ if let .ofMacroExpansionInfo mi := info then
425
+ let k := mi.stx.getKind
426
+ if c.proj.has env k then
427
+ -- usually the expansion of a macro whose term or command info, just above, already named it
428
+ let same := match (← get).uses[c.encl.toNat]? with
429
+ | some u => c.encl ≥ 0 && u.kind == k && u.pos == pos
430
+ | none => false
431
+ if !same then
432
+ let u ← record k .anonymous mi.stx
433
+ c' := { c' with encl := u }
434
+ for ch in children do walkInfo c' ctx? ch pos path
435
+ | .hole _ => pure ()
436
+
437
+ /-! ## Context -/
438
+
439
+ /-- A block the command sits in: what `namespace` and `section` opened, by the scopes they pushed. -/
440
+ structure Block where
441
+ kind : String
442
+ name : String
443
+ line : Nat
444
+ noncomputable_ : Bool
445
+
446
+ /-- A command that holds while its section is open: local and scoped notation, instances and
447
+ attributes, `open scoped`, `include` and `omit`. -/
448
+ structure LocalEntry where
449
+ depth : Nat
450
+ line : Nat
451
+ text : String
452
+ in_ : Bool
453
+
454
+ def lineOf (ictx : InputContext) (pos : Nat) : Nat := (ictx.fileMap.toPosition ⟨pos⟩).line
455
+
456
+ /-- The file's text from byte `a` to `b` on one line: white space, line breaks included, as one
457
+ space; at most `max` characters. -/
458
+ def textAt (ictx : InputContext) (a b : Nat) (max : Nat := 400) : String :=
459
+ let s := String.Pos.Raw.extract ictx.fileMap.source ⟨a⟩ ⟨b⟩
460
+ let (one, _) := s.foldl (init := ("", true)) fun (acc, sp) c =>
461
+ if c == ' ' || c == '\n' || c == '\t' || c == '\r' then (acc, true)
462
+ else ((if sp && !acc.isEmpty then acc.push ' ' else acc).push c, false)
463
+ if one.length > max then (one.take max).toString ++ "…" else one
464
+
465
+ /-- A piece of syntax as written, on one line; as Lean prints it when it has no place in the file. -/
466
+ def textOf (ictx : InputContext) (stx : Syntax) : String :=
467
+ match stx.getPos? (canonicalOnly := false), stx.getTailPos? (canonicalOnly := false) with
468
+ | some a, some b => textAt ictx a.byteIdx b.byteIdx
469
+ | _, _ => toString stx.prettyPrint
470
+
471
+ /-- Where a command's text starts once its doc comment is skipped. -/
472
+ partial def firstTokenPos? (stx : Syntax) : Option Nat :=
473
+ match stx with
474
+ | .atom .. | .ident .. => (stx.getPos? (canonicalOnly := false)).map (·.byteIdx)
475
+ | .node _ k args => if k == ``Lean.Parser.Command.docComment then none else args.findSome? firstTokenPos?
476
+ | _ => none
477
+
478
+ /-- A command as written, its doc comment left out, on one line. -/
479
+ def commandText (ictx : InputContext) (stx : Syntax) : String :=
480
+ match firstTokenPos? stx, stx.getTailPos? (canonicalOnly := false) with
481
+ | some a, some b => textAt ictx a b.byteIdx
482
+ | _, _ => textOf ictx stx
483
+
484
+ def openText (o : OpenDecl) : String :=
485
+ match o with
486
+ | .simple ns [] => s!"open {ns}"
487
+ | .simple ns ex => s!"open {ns} hiding {" ".intercalate (ex.map toString)}"
488
+ | .explicit id decl =>
489
+ if decl.getPrefix.isAnonymous then s!"open {decl}"
490
+ else if decl.componentsRev.head? == id.componentsRev.head? && decl.getPrefix != .anonymous then
491
+ s!"open {decl.getPrefix} ({id})"
492
+ else s!"open {decl.getPrefix} renaming {decl.componentsRev.head?.getD .anonymous} → {id}"
493
+
494
+ /-- How many variables a `variable` binder introduces: its names, or one for an instance binder. -/
495
+ def binderVars (b : Syntax) : Nat :=
496
+ if b.getKind == ``Lean.Parser.Term.instBinder then 1
497
+ else max 1 b[1].getArgs.size
498
+
499
+ /-- The variables in scope as Lean elaborates them for a declaration (`runTermElabM`, auto-bound
500
+ ones first), then which of a constant's leading binders are those: each, in order, a variable
501
+ of the same binder kind and type, and of the same name unless it is an anonymous instance.
502
+ The answer is the indices of the binders of `varDecls` included. A declaration's own binder of
503
+ the same name, kind and type as a variable it did not take would be taken for it (a reading of
504
+ names, as Lean records nothing of what it included). -/
505
+ def includedBinders (scope : Scope) (consts : Array (Name × Expr)) : CommandElabM (Array (Name × Array Nat)) := do
506
+ if scope.varDecls.isEmpty || consts.isEmpty then return #[]
507
+ -- binder index of each variable, in varUIds order
508
+ let mut owner : Array Nat := #[]
509
+ for h : i in [0:scope.varDecls.size] do
510
+ for _ in [0:binderVars scope.varDecls[i].raw] do owner := owner.push i
511
+ let norm (e : Expr) : Expr := e.replace fun
512
+ | .sort _ => some (.sort .zero)
513
+ | .const n _ => some (.const n [])
514
+ | .mdata _ b => some b
515
+ | _ => none
516
+ try
517
+ runTermElabM fun xs => do
518
+ let off := xs.size - scope.varUIds.size
519
+ let mut vars : Array (Name × BinderInfo × Expr × Option Nat) := #[]
520
+ for h : k in [0:xs.size] do
521
+ let d ← xs[k].fvarId!.getDecl
522
+ let b := if k < off then none else owner[k - off]?
523
+ vars := vars.push (d.userName.eraseMacroScopes, d.binderInfo, norm (← instantiateMVars d.type), b)
524
+ let mut out := #[]
525
+ for (c, ty) in consts do
526
+ let mut ty := ty
527
+ let mut subst : Array Expr := #[]
528
+ let mut j := 0
529
+ let mut got : Array Nat := #[]
530
+ while true do
531
+ let .forallE n dom b bi := ty | break
532
+ let dom := norm (dom.instantiateRev subst)
533
+ let n := n.eraseMacroScopes
534
+ let mut hit : Option Nat := none
535
+ for h : j' in [j:vars.size] do
536
+ let (vn, vbi, vty, _) := vars[j']
537
+ if vbi != bi then continue
538
+ let named := !(bi.isInstImplicit && (vn.isAnonymous || vn == `inst))
539
+ if named && vn != n then continue
540
+ if vty == dom then hit := some j'; break
541
+ let some j' := hit | break
542
+ if let (_, _, _, some bi) := vars[j']! then
543
+ if !got.contains bi then got := got.push bi
544
+ subst := subst.push xs[j']!
545
+ j := j' + 1
546
+ ty := b
547
+ if !got.isEmpty then out := out.push (c, got)
548
+ return out
549
+ catch _ => return #[]
550
+
551
+ /-! ## One module -/
552
+
553
+ structure RunState where
554
+ st : Command.State
555
+ proj : Project
556
+ baseOpts : Options
557
+ /-- options this program sets for itself, not the module's: never shown as its context -/
558
+ ownOpts : Options
559
+ blocks : Array Block := #[]
560
+ locals : Array LocalEntry := #[]
561
+ /-- what put each piece of the scope there: the line of the command, and whether it was `… in` -/
562
+ intro : Std.HashMap String (Nat × Bool) := {}
563
+ /-- inside the `section` a `c₁ in c₂` runs in: what is introduced holds for `c₂` alone -/
564
+ inIn : Bool := false
565
+ contexts : Array Json := #[]
566
+ ctxIdx : Std.HashMap String Nat := {}
567
+ cmds : Array Json := #[]
568
+ errors : Array Json := #[]
569
+ elabMs : Float := 0
570
+ templates : Templates
571
+ /-- the file's text, as bytes -/
572
+ bytes : ByteArray := .empty
573
+
574
+ abbrev RunM := StateRefT RunState IO
575
+
576
+ def nowMs : IO Float := return (← IO.monoNanosNow).toFloat / 1e6
577
+
578
+ def isHashCmd (stx : Syntax) : Bool := (firstAtom stx).startsWith "#"
579
+
580
+ /-- The keys of a scope's pieces, by which `RunState.intro` remembers where each was put. -/
581
+ def scopeKeys (s : Scope) (baseOpts ownOpts : Options) : Array String := Id.run do
582
+ let mut out := #[]
583
+ for o in s.openDecls do out := out.push s!"open {openText o}"
584
+ for u in s.levelNames do out := out.push s!"univ {u}"
585
+ for (k, v) in s.opts do
586
+ if baseOpts.find? k != some v && ownOpts.find? k != some v then out := out.push s!"opt {k} {v}"
587
+ return out
588
+
589
+ /-- The context a command is written in, from the scope before it (shared/scope.ts, FileContext). -/
590
+ def contextJson (ictx : InputContext) (rs : RunState) (blocks : Array Block) (locals : Array LocalEntry)
591
+ (s : Scope) : Json := Id.run do
592
+ let intro (key : String) : List (String × Json) :=
593
+ match rs.intro[key]? with
594
+ | some (l, inn) => [("line", toJson l)] ++ (if inn then [("in", Json.bool true)] else [])
595
+ | none => [("line", toJson 0)]
596
+ let cmd (key text : String) : Json := Json.mkObj (intro key ++ [("text", toJson text)])
597
+ -- `namespace A.B` opens a scope for each part, all on its line: one block, as written
598
+ let mut merged : Array Block := #[]
599
+ for b in blocks do
600
+ match merged.back? with
601
+ | some p =>
602
+ if p.kind == "namespace" && b.kind == "namespace" && p.line == b.line then
603
+ merged := merged.pop.push { p with name := p.name ++ "." ++ b.name }
604
+ else merged := merged.push b
605
+ | none => merged := merged.push b
606
+ let mut blocksJ : Array Json := #[]
607
+ for b in merged do
608
+ blocksJ := blocksJ.push <| Json.mkObj <|
609
+ [("kind", toJson b.kind), ("name", if b.name.isEmpty then Json.null else toJson b.name),
610
+ ("line", toJson b.line)] ++ (if b.noncomputable_ then [("noncomputable", Json.bool true)] else [])
611
+ let opens := s.openDecls.reverse.toArray.map fun o =>
612
+ let ns := match o with | .simple ns _ => ns | .explicit _ d => d.getPrefix
613
+ let t := openText o
614
+ Json.mkObj (intro s!"open {t}" ++ [("text", toJson t), ("namespaces", toJson #[ns.toString])])
615
+ let mut vars : Array Json := #[]
616
+ let mut uid := 0
617
+ for b in s.varDecls do
618
+ let n := binderVars b.raw
619
+ let names := (s.varUIds.extract uid (uid + n)).toList.filterMap fun x =>
620
+ let v := x.eraseMacroScopes
621
+ if v.isAnonymous then none else some (toJson v.toString)
622
+ uid := uid + n
623
+ let pos := (b.raw.getPos? (canonicalOnly := false)).map (·.byteIdx)
624
+ let line := pos.map (lineOf ictx) |>.getD 0
625
+ let inn : Bool := match pos with
626
+ | some p => (rs.intro[s!"var {p}"]?.map (·.2)).getD false
627
+ | none => false
628
+ vars := vars.push <| Json.mkObj <|
629
+ [("line", toJson line), ("text", toJson (textOf ictx b.raw)), ("binds", Json.arr names.toArray)]
630
+ ++ (if inn then [("in", Json.bool true)] else [])
631
+ let univs := s.levelNames.reverse.toArray.map fun u => cmd s!"univ {u}" u.toString
632
+ let mut opts : Array Json := #[]
633
+ for (k, v) in s.opts do
634
+ if rs.baseOpts.find? k != some v && rs.ownOpts.find? k != some v then
635
+ opts := opts.push (cmd s!"opt {k} {v}" s!"{k} {v}")
636
+ let mut local_ : Array Json := locals.map fun l =>
637
+ Json.mkObj ([("line", toJson l.line), ("text", toJson l.text)] ++ (if l.in_ then [("in", Json.bool true)] else []))
638
+ let ns := s.currNamespace
639
+ return Json.mkObj [
640
+ ("namespace", toJson (if ns.isAnonymous then "" else ns.toString)),
641
+ ("blocks", Json.arr blocksJ), ("opens", Json.arr opens), ("variables", Json.arr vars),
642
+ ("universes", Json.arr univs), ("options", Json.arr opts), ("local", Json.arr local_)]
643
+
644
+ /-- After a command: the blocks it opened or closed, and where each new piece of the scope came
645
+ from. -/
646
+ def noteScope (ictx : InputContext) (before : List Scope) (line : Nat) (stx : Syntax) : RunM Unit := do
647
+ let rs ← get
648
+ let after := rs.st.scopes
649
+ let mut blocks := rs.blocks
650
+ -- `end` pops, `namespace` and `section` push; `namespace A.B` pushes two scopes on one line
651
+ while blocks.size + 1 > after.length do blocks := blocks.pop
652
+ if after.length > before.length then
653
+ let fresh := (after.take (after.length - before.length)).reverse
654
+ let mut parent := before.head!
655
+ for s in fresh do
656
+ let isNs := s.currNamespace != parent.currNamespace
657
+ blocks := blocks.push { kind := if isNs then "namespace" else "section", name := s.header, line,
658
+ noncomputable_ := s.isNoncomputable && !parent.isNoncomputable }
659
+ parent := s
660
+ let bScope := before.head!
661
+ let aScope := after.head!
662
+ let old : Std.HashSet String := Std.HashSet.ofArray (scopeKeys bScope rs.baseOpts rs.ownOpts)
663
+ let mut intro := rs.intro
664
+ for k in scopeKeys aScope rs.baseOpts rs.ownOpts do
665
+ if !old.contains k then intro := intro.insert k (line, rs.inIn)
666
+ for b in aScope.varDecls do
667
+ if let some p := b.raw.getPos? (canonicalOnly := false) then
668
+ let k := s!"var {p.byteIdx}"
669
+ if !intro.contains k then intro := intro.insert k (line, rs.inIn)
670
+ -- local and scoped commands hold while the scope they were written in is open
671
+ let mut locals := rs.locals.filter (·.depth ≤ after.length)
672
+ let isOpenScoped := stx.getKind == ``Lean.Parser.Command.open && (firstKind? stx ``Lean.Parser.Command.openScoped).isSome
673
+ -- `include` and `omit` as written: the scope keeps only the variables' internal names, and an
674
+ -- instance binder has none
675
+ let isInclude := stx.getKind == ``Lean.Parser.Command.«include» || stx.getKind == ``Lean.Parser.Command.«omit»
676
+ if (localKind? stx).isSome || isOpenScoped || isInclude then
677
+ locals := locals.push { depth := after.length, line, text := commandText ictx stx, in_ := rs.inIn }
678
+ set { rs with blocks, intro, locals }
679
+
680
+ /-- Suffixes of constants the compiler generates next to a declaration (as Extract.lean has them):
681
+ never a unit, nor folded into one by name. -/
682
+ def generatedSuffixes : List String :=
683
+ ["recOn", "casesOn", "noConfusion", "noConfusionType", "ctorElim", "ctorElimType", "below",
684
+ "brecOn", "binductionOn", "ibelow", "ctorIdx", "toCtorIdx", "ofNat", "ofNat_ctorIdx",
685
+ "sizeOf_spec", "injEq", "inj", "eq_def", "induct", "mutual_induct", "fun_cases",
686
+ "induct_unfolding", "ext_iff", "congr_simp", "elim"]
687
+
688
+ /-- The constants a command declared that a unit can be made of: the declarations it writes and
689
+ those Lean generates from it under names of their own (what a declaring macro writes), and the
690
+ `_aux_` declarations of a syntax extension. Constructors, recursors, projections, matchers and
691
+ the like are left out: they fold into their type, as Extract.lean has it. User names. -/
692
+ def newConsts (env0 env1 : Environment) : Array Name :=
693
+ env1.constants.map₂.foldl (init := #[]) fun acc n ci =>
694
+ if env0.contains n then acc else
695
+ let u := userName n
696
+ let last := match u with | .str _ s => s | _ => ""
697
+ let keep :=
698
+ if u.hasMacroScopes then false
699
+ else if last.startsWith "_aux_" then true
700
+ else if u.isInternalDetail || generatedSuffixes.contains last then false
701
+ else !(ci matches .ctorInfo _ || ci matches .recInfo _ || isAuxRecursor env1 n || isNoConfusion env1 n
702
+ || Meta.isMatcherCore env1 n || (env1.getProjectionFnInfo? n).isSome)
703
+ if keep then acc.push n else acc
704
+
705
+ /-- Spans [start, end) sorted, those that overlap or touch made one. -/
706
+ def mergeSpans (xs : Array (Nat × Nat)) : Array (Nat × Nat) := Id.run do
707
+ let mut out : Array (Nat × Nat) := #[]
708
+ for (a, b) in xs.qsort (fun x y => x.1 < y.1 || (x.1 == y.1 && x.2 > y.2)) do
709
+ match out.back? with
710
+ | some (a', b') => if a ≤ b' then out := out.pop.push (a', max b b') else out := out.push (a, b)
711
+ | none => out := out.push (a, b)
712
+ return out
713
+
714
+ def errorsOf (msgs : MessageLog) : IO (Array String) := do
715
+ let mut out := #[]
716
+ for m in msgs.toList do
717
+ if m.severity == .error then
718
+ let s ← m.data.toString
719
+ out := out.push (s.take 300).toString
720
+ return out
721
+
722
+ partial def processCmd (ictx : InputContext) (stx : Syntax) (record : Bool := true) : RunM Unit := do
723
+ let k := stx.getKind
724
+ let pos := (stx.getPos?.map (·.byteIdx)).getD 0
725
+ let line := lineOf ictx pos
726
+ -- `c₁ in c₂`: as Lean does, `section c₁ c₂ end`, with c₂ recorded with its context
727
+ if k == ``Lean.Parser.Command.in then
728
+ let run (x : CommandElabM Unit) : RunM Unit := do
729
+ let s ← get
730
+ let (_, st) ← liftCmd ictx s.st x pos
731
+ set { s with st }
732
+ let before := (← get).st.scopes
733
+ run (do elabCommand (← `(command| section)))
734
+ -- the inner section is no block of the context: what it holds is marked `in` instead
735
+ let blocks := (← get).blocks
736
+ let wasIn := (← get).inIn
737
+ modify fun s => { s with inIn := true }
738
+ processCmd ictx stx[0] (record := false)
739
+ modify fun s => { s with inIn := wasIn }
740
+ processCmd ictx stx[2] record
741
+ run (do elabCommand (← `(command| end)))
742
+ modify fun s => { s with blocks, locals := s.locals.filter (·.depth ≤ before.length) }
743
+ return
744
+ if isHashCmd stx then return
745
+ let s ← get
746
+ let scope := s.st.scopes.head!
747
+ let before := s.st.scopes
748
+ let (blocks0, locals0) := (s.blocks, s.locals)
749
+ let isExt := extCmdKinds.contains k
750
+ let ((), po) := (walkStx stx (.site stmt) false).run {}
751
+ let cmd := if isExt then stx else strip s.templates stx false
752
+ let env0 := s.st.env
753
+ let st0 := { s.st with messages := {}, infoState := { enabled := true, trees := {} } }
754
+ let t0 ← nowMs
755
+ let (_, st1) ← liftCmd ictx st0 (elabCommandTopLevel cmd) pos
756
+ let t1 ← nowMs
757
+ let errs ← errorsOf st1.messages
758
+ -- the scope after it, the messages as they were (this program reports its own)
759
+ set { s with st := { st1 with messages := s.st.messages, infoState := { enabled := false } },
760
+ elabMs := s.elabMs + (t1 - t0) }
761
+ if !errs.isEmpty then
762
+ modify fun r => { r with errors := r.errors.push (Json.mkObj [("line", toJson line), ("message", toJson errs[0]!)]) }
763
+ noteScope ictx before line stx
764
+ if !record then return
765
+ let env := st1.env
766
+ let proj := s.proj
767
+ -- what ran, from the info trees
768
+ let wc : WalkCtx := { proj, ivs := po.ivs, bytes := s.bytes }
769
+ let ((), io) ← (st1.infoState.trees.toArray.forM fun tr => walkInfo wc none tr pos #[]).run {}
770
+ -- the kinds parsed in the text: where one also ran, its site is the one elaboration found (a
771
+ -- term that is a proof inside a definition's value)
772
+ let mut ranAt : Std.HashMap (Name × Nat) Nat := {}
773
+ for u in io.uses do
774
+ if !u.kind.isAnonymous then ranAt := ranAt.insert (u.kind, u.pos) u.site
775
+ let mut uses : Array Use := #[]
776
+ for (kd, p) in po.kinds do
777
+ if proj.has env kd then
778
+ let site := ranAt[(kd, p)]?.getD (siteAt po.ivs io.byData p)
779
+ uses := uses.push { kind := kd, impl := .anonymous, site, pos := p, by_ := -1 }
780
+ let base := uses.size
781
+ for u in io.uses do
782
+ uses := uses.push { u with by_ := if u.by_ < 0 then -1 else u.by_ + base }
783
+ let consts := newConsts env0 env
784
+ if consts.isEmpty && uses.isEmpty then return
785
+ -- the same use twice is one: by kind, implementation, site, line and emitter
786
+ let mut seen : Std.HashMap String Nat := {}
787
+ let mut keep : Array Json := #[]
788
+ let mut remap : Array Int := #[]
789
+ for u in uses do
790
+ let p := ictx.fileMap.toPosition ⟨u.pos⟩
791
+ let emitter : Int := if u.by_ < 0 then -1 else (remap[u.by_.toNat]?.getD (-1))
792
+ let key := s!"{u.kind}|{u.impl}|{u.site}|{p.line}|{p.column}|{emitter}"
793
+ match seen[key]? with
794
+ | some i => remap := remap.push i
795
+ | none =>
796
+ seen := seen.insert key keep.size
797
+ remap := remap.push keep.size
798
+ keep := keep.push (Json.arr #[toJson (nameStr u.kind), toJson (nameStr u.impl), toJson u.site,
799
+ toJson p.line, toJson p.column, toJson emitter])
800
+ -- the context, shared between the commands written in the same one
801
+ let rs ← get
802
+ let cj := contextJson ictx rs blocks0 locals0 scope
803
+ let ckey := cj.compress
804
+ let ci ← match rs.ctxIdx[ckey]? with
805
+ | some i => pure i
806
+ | none => do
807
+ modify fun r => { r with contexts := r.contexts.push cj, ctxIdx := r.ctxIdx.insert ckey r.contexts.size }
808
+ pure rs.contexts.size
809
+ -- the variables each constant took
810
+ let typed := consts.filterMap fun c => (env.find? c).map fun info => (c, info.type)
811
+ let incl ← if scope.varDecls.isEmpty then pure #[] else do
812
+ let stScope := { st0 with env, messages := {}, infoState := { enabled := false } }
813
+ let (r, _) ← (liftCmd ictx stScope (includedBinders scope typed) pos : IO _)
814
+ pure r
815
+ let tail := (stx.getTailPos?.map (·.byteIdx)).getD pos
816
+ -- the proofs inside its definitions' bodies: `by` blocks whose goal is a proposition, the tactics
817
+ -- of `decreasing_by`, term-mode proofs written in the text; never in a signature, nor in a
818
+ -- quotation; nested and overlapping ones merged
819
+ let mut spans : Array (Nat × Nat) := po.decr ++ io.termProofs
820
+ for b in po.bys do
821
+ if b.outer == body && !b.quot && !io.spanData.contains b.k then spans := spans.push (b.s, b.e)
822
+ let proofs := (mergeSpans spans).map fun (a, b) =>
823
+ let p := ictx.fileMap.toPosition ⟨a⟩
824
+ let q := ictx.fileMap.toPosition ⟨b⟩
825
+ Json.arr #[toJson p.line, toJson p.column, toJson q.line, toJson q.column]
826
+ let j := Json.mkObj <| [
827
+ ("line", toJson line), ("endLine", toJson (lineOf ictx tail)),
828
+ ("consts", toJson (consts.map nameStr)), ("ctx", toJson ci),
829
+ ("incl", Json.mkObj (incl.toList.map fun (c, bs) => (nameStr c, toJson bs))),
830
+ ("uses", Json.arr keep)] ++ (if proofs.isEmpty then [] else [("proofs", Json.arr proofs)])
831
+ modify fun r => { r with cmds := r.cmds.push j }
832
+
833
+ def run (src : FilePath) (mod : Name) (out : FilePath) (projectMods : Std.HashSet Name)
834
+ (opts : Options) : IO Unit := do
835
+ let t0 ← nowMs
836
+ let input ← IO.FS.readFile src
837
+ let ictx := Parser.mkInputContext input src.toString
838
+ let (header, pstate, msgs) ← Parser.parseHeader ictx
839
+ let (env, msgs) ← Elab.processHeader header opts msgs ictx (trustLevel := 1024) (mainModule := mod)
840
+ if msgs.hasErrors then
841
+ let es ← errorsOf msgs
842
+ throw (IO.userError s!"the imports of {mod} could not be loaded: {"; ".intercalate es.toList}")
843
+ let tImp ← nowMs
844
+ -- the options as Lake gave them, parsed now that what declares them is imported
845
+ let baseOpts ← Language.Lean.reparseOptions opts
846
+ -- this program's own: no parallel elaboration, which would leave the info trees unfinished; no
847
+ -- linters, which cost time and say nothing of what a command means
848
+ let ownOpts : Options := ({} : Options) |>.setBool `Elab.async false |>.setBool `linter.all false
849
+ let runOpts := baseOpts |>.setBool `Elab.async false |>.setBool `linter.all false
850
+ let mask := env.header.moduleNames.map (projectMods.contains ·)
851
+ let rs : RunState := {
852
+ st := Command.mkState env {} runOpts, proj := { mask }, baseOpts, ownOpts,
853
+ templates := ← mkTemplates env, bytes := input.toUTF8 }
854
+ let ((), rs) ← (show RunM Unit from do
855
+ let mut ps := pstate
856
+ repeat
857
+ let s ← get
858
+ let scope := s.st.scopes.head!
859
+ let pmctx : ParserModuleContext :=
860
+ { env := s.st.env, options := scope.opts, currNamespace := scope.currNamespace, openDecls := scope.openDecls }
861
+ let (cmd, ps', _) := Parser.parseCommand ictx pmctx ps {}
862
+ ps := ps'
863
+ if isTerminalCommand cmd then break
864
+ try processCmd ictx cmd
865
+ catch e =>
866
+ let line := lineOf ictx ((cmd.getPos?.map (·.byteIdx)).getD 0)
867
+ modify fun r => { r with errors := r.errors.push (Json.mkObj [("line", toJson line), ("message", toJson (toString e))]) }).run rs
868
+ let tEnd ← nowMs
869
+ let doc := Json.mkObj [
870
+ ("module", toJson mod.toString),
871
+ ("ms", Json.mkObj [("import", toJson (tImp - t0).round), ("elab", toJson rs.elabMs.round),
872
+ ("total", toJson (tEnd - t0).round)]),
873
+ ("errors", Json.arr rs.errors), ("contexts", Json.arr rs.contexts), ("cmds", Json.arr rs.cmds)]
874
+ IO.FS.writeFile out doc.compress
875
+
876
+ end VisualProof.Usage
877
+
878
+ open VisualProof.Usage in
879
+ unsafe def main (args : List String) : IO UInt32 := do
880
+ let (dashD, pos) := args.partition (·.startsWith "-D")
881
+ match pos with
882
+ | [src, mod, out, modsFile] =>
883
+ -- as `lean -Dname=value` gives them: strings, parsed once their declarations are imported
884
+ let mut opts : Options := {}
885
+ for d in dashD do
886
+ match (d.drop 2).toString.splitOn "=" with
887
+ | name :: rest@(_ :: _) => opts := opts.insert name.toName (.ofString ("=".intercalate rest))
888
+ | _ => IO.eprintln s!"vp-usage: ignored {d}"
889
+ initSearchPath (← findSysroot)
890
+ Lean.enableInitializersExecution
891
+ let mods := (← IO.FS.lines modsFile).filter (!·.isEmpty) |>.map String.toName
892
+ try
893
+ run src mod.toName out (Std.HashSet.ofArray mods) opts
894
+ return 0
895
+ catch e =>
896
+ IO.eprintln s!"vp-usage: {e}"
897
+ return 1
898
+ | _ =>
899
+ IO.eprintln "usage: vp-usage <source.lean> <Module> <out.json> <project-modules.txt> [-Dname=value]..."
900
+ return 2