visualproof 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +99 -2
- package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
- package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
- package/dist/assets/latex/visualproof.sty +308 -0
- package/dist/extractor/Extract.lean +1180 -0
- package/dist/extractor/Usage.lean +900 -0
- package/dist/mcp/visualproof-mcp.mjs +476 -0
- package/dist/server/access.js +95 -0
- package/dist/server/agent.js +3235 -0
- package/dist/server/agentdocs.js +35 -0
- package/dist/server/agentlibrary.js +202 -0
- package/dist/server/agentnotes.js +701 -0
- package/dist/server/agentviews.js +703 -0
- package/dist/server/askfollow.js +286 -0
- package/dist/server/boardspots.js +159 -0
- package/dist/server/dirs.js +175 -0
- package/dist/server/doctor.js +186 -0
- package/dist/server/events.js +198 -0
- package/dist/server/export.js +420 -0
- package/dist/server/extract.js +204 -0
- package/dist/server/fetchurl.js +321 -0
- package/dist/server/figures.js +158 -0
- package/dist/server/history.js +741 -0
- package/dist/server/historyapi.js +304 -0
- package/dist/server/hover.js +595 -0
- package/dist/server/identity.js +114 -0
- package/dist/server/invites.js +241 -0
- package/dist/server/layoutthread.js +51 -0
- package/dist/server/library.js +600 -0
- package/dist/server/libraryroute.js +211 -0
- package/dist/server/live.js +1101 -0
- package/dist/server/lostfound.js +199 -0
- package/dist/server/main.js +1394 -0
- package/dist/server/mcptools.js +497 -0
- package/dist/server/pastdoc.js +73 -0
- package/dist/server/pipeline.js +156 -0
- package/dist/server/poppler.js +251 -0
- package/dist/server/presence.js +252 -0
- package/dist/server/project.js +222 -0
- package/dist/server/prompt.js +53 -0
- package/dist/server/readonly.js +152 -0
- package/dist/server/readonlygate.js +123 -0
- package/dist/server/renames.js +268 -0
- package/dist/server/review.js +334 -0
- package/dist/server/reviewapi.js +188 -0
- package/dist/server/snapshot.js +49 -0
- package/dist/server/sourcestore.js +473 -0
- package/dist/server/status.js +360 -0
- package/dist/server/talk.js +476 -0
- package/dist/server/tokens.js +221 -0
- package/dist/server/usage.js +501 -0
- package/dist/server/versions.js +82 -0
- package/dist/server/view.js +157 -0
- package/dist/server/views.js +1167 -0
- package/dist/shared/askfollow.js +46 -0
- package/dist/shared/board.js +332 -0
- package/dist/shared/build.js +2 -0
- package/dist/shared/collab.js +148 -0
- package/dist/shared/comment.js +285 -0
- package/dist/shared/context.js +157 -0
- package/dist/shared/doc/authors.js +98 -0
- package/dist/shared/doc/build.js +358 -0
- package/dist/shared/doc/latex.js +455 -0
- package/dist/shared/doc/markdown.js +159 -0
- package/dist/shared/doc/model.js +37 -0
- package/dist/shared/doc/order.js +107 -0
- package/dist/shared/doc/proof.js +272 -0
- package/dist/shared/doc/sections.js +93 -0
- package/dist/shared/doc/settings.js +75 -0
- package/dist/shared/edgecodec.js +102 -0
- package/dist/shared/editor.js +64 -0
- package/dist/shared/excerpt.js +87 -0
- package/dist/shared/graph.js +16 -0
- package/dist/shared/items.js +92 -0
- package/dist/shared/keys.js +203 -0
- package/dist/shared/layout.js +926 -0
- package/dist/shared/lexer.js +130 -0
- package/dist/shared/library.js +711 -0
- package/dist/shared/lostfound.js +150 -0
- package/dist/shared/moves.js +243 -0
- package/dist/shared/notecard.js +295 -0
- package/dist/shared/noteplace.js +505 -0
- package/dist/shared/palette.js +46 -0
- package/dist/shared/pastgraph.js +95 -0
- package/dist/shared/places.js +80 -0
- package/dist/shared/projectnotes.js +244 -0
- package/dist/shared/query.js +810 -0
- package/dist/shared/review.js +642 -0
- package/dist/shared/reviewqueue.js +333 -0
- package/dist/shared/ring.js +366 -0
- package/dist/shared/scope.js +255 -0
- package/dist/shared/scopeedit.js +81 -0
- package/dist/shared/search.js +191 -0
- package/dist/shared/separate.js +178 -0
- package/dist/shared/sharesnap.js +280 -0
- package/dist/shared/sticky.js +501 -0
- package/dist/shared/timeline.js +73 -0
- package/dist/shared/titles.js +298 -0
- package/dist/shared/viewgraph.js +139 -0
- package/dist/shared/views.js +859 -0
- package/dist/web/bundle/app.js +48733 -0
- package/dist/web/bundle/edges.js +409 -0
- package/dist/web/bundle/layout.js +1010 -0
- package/dist/web/bundle/pdfworker.js +48322 -0
- package/dist/web/index.html +239 -0
- package/dist/web/src/motion.js +430 -0
- package/dist/web/src/past.js +224 -0
- package/dist/web/src/scene.js +787 -0
- package/dist/web/style.css +2312 -0
- package/docs/agent-api/README.md +91 -0
- package/docs/agent-api/advice.md +108 -0
- package/docs/agent-api/export.md +120 -0
- package/docs/agent-api/following.md +140 -0
- package/docs/agent-api/history.md +142 -0
- package/docs/agent-api/library.md +65 -0
- package/docs/agent-api/mcp.md +104 -0
- package/docs/agent-api/notes.md +93 -0
- package/docs/agent-api/reading.md +166 -0
- package/docs/agent-api/requests.md +94 -0
- package/docs/agent-api/talking.md +100 -0
- package/docs/agent-api/tcb.md +135 -0
- package/docs/agent-api/views.md +124 -0
- package/docs/agent-api/words.md +56 -0
- package/package.json +39 -4
- package/test/toolchains.json +5 -0
|
@@ -0,0 +1,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
|