forked from leanprover-community/mathlib4
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCrossRefAttribute.lean
More file actions
334 lines (271 loc) · 12.5 KB
/
Copy pathCrossRefAttribute.lean
File metadata and controls
334 lines (271 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
/-
Copyright (c) 2024 Damiano Testa. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Damiano Testa
-/
module
public meta import Lean.Elab.Command
public import Mathlib.Init
public meta import Mathlib.Tactic.CrossRef.Fetch
public meta import Mathlib.Tactic.Widget.CrossRefHover
/-!
# Cross-reference attributes
This file provides attributes for tagging Mathlib results with cross-references
to entries in external mathematical databases:
* `@[stacks TAG]` — [Stacks Project](https://stacks.math.columbia.edu/tags)
* `@[kerodon TAG]` — [Kerodon](https://kerodon.net/tag/)
* `@[wikidata QID]` — [Wikidata](https://www.wikidata.org)
Each attribute records the cross-reference in an environment extension, appends
a link to the declaration's docstring, and attaches the `CrossRefHoverPanel`
widget to the attribute syntax so the info view shows the upstream label /
description when the cursor sits on the tag.
The `Database` enum and the `databaseURL` / `databaseLabel` projections live in
`Mathlib.Tactic.CrossRef.Fetch`; the widget itself in
`Mathlib.Tactic.Widget.CrossRefHover`.
The remaining shared infrastructure (`Tag`, `tagExt`, `addCrossRefDoc`,
`traceCrossRefs`) is database-agnostic; per-database code defines a parser, the
attribute syntax, and the trace command.
-/
public meta section
open Lean Elab Server
namespace Mathlib.CrossRef
/-- A cross-reference from a Mathlib declaration to an entry in an external database. -/
structure Tag where
/-- The name of the declaration carrying the cross-reference. -/
declName : Name
/-- The external database the entry belongs to. -/
database : Database
/-- The database identifier. -/
tag : String
/-- An optional comment supplied with the attribute. -/
comment : String
deriving BEq, Hashable
/-- The environment extension storing all cross-references.
`addImportedFn` is a constant function to avoid a performance overhead during initialization. -/
initialize tagExt : SimplePersistentEnvExtension Tag (Array (Array Tag)) ←
registerSimplePersistentEnvExtension {
addImportedFn tags := tags
addEntryFn tags _ := tags
}
/-- `addTagEntry declName db tag comment` records a cross-reference for `declName` in `tagExt`. -/
def addTagEntry {m : Type → Type} [MonadEnv m]
(declName : Name) (db : Database) (tag comment : String) : m Unit :=
modifyEnv (tagExt.addEntry ·
{ declName := declName, database := db, tag := tag, comment := comment })
/-- Append a cross-reference link to the docstring of `decl` and record it in `tagExt`.
This is the database-agnostic core of every cross-reference attribute's `add` handler. -/
def addCrossRefDoc (db : Database) (decl : Name) (idStr comment : String) : CoreM Unit := do
let oldDoc := (← findDocString? (← getEnv) decl).getD ""
let commentInDoc := if comment.isEmpty then "" else s!" ({comment})"
let link := s!"[{databaseLabel db} {idStr}]({databaseURL db}{idStr}){commentInDoc}"
addDocStringCore decl <| "\n\n".intercalate ([oldDoc, link].filter (· != ""))
addTagEntry decl db idStr comment
/-- Attach the `CrossRefHoverPanel` info-view widget to the attribute syntax
`attrStx`. This is what makes the info view show the upstream snippet when the
cursor lands on a cross-reference tag. -/
def attachCrossRefWidget (db : Database) (tag comment : String) (attrStx : Syntax) :
CoreM Unit :=
Widget.savePanelWidgetInfo
CrossRefHoverPanel.javascriptHash
(rpcEncode { database := db.name, tag := tag, comment := comment : CrossRefHoverProps })
attrStx
open Parser
/-! ### Stacks (and Kerodon) parser -/
/-- `stacksTag` is the node kind of Stacks Project Tags: a sequence of digits and
uppercase letters. -/
abbrev stacksTagKind : SyntaxNodeKind := `stacksTag
/-- The main parser for Stacks Project Tags: it accepts any sequence of 4 digits or
uppercase letters. -/
def stacksTagFn : ParserFn := fun c s =>
let i := s.pos
let s := takeWhileFn (fun c => c.isAlphanum) c s
if s.hasError then
s
else if s.pos == i then
ParserState.mkError s "stacks tag"
else
let tag := c.extract i s.pos
if !tag.all fun (c : Char) => c.isDigit || c.isUpper then
ParserState.mkUnexpectedError s
"Stacks tags must consist only of digits and uppercase letters."
else if tag.length != 4 then
ParserState.mkUnexpectedError s "Stacks tags must be exactly 4 characters"
else
mkNodeToken stacksTagKind i true c s
@[inherit_doc stacksTagFn]
def stacksTagNoAntiquot : Parser := {
fn := stacksTagFn
info := mkAtomicInfo "stacksTag"
}
@[inherit_doc stacksTagFn]
def stacksTagParser : Parser :=
withAntiquot (mkAntiquot "stacksTag" stacksTagKind) stacksTagNoAntiquot
/-! ### Wikidata parser -/
/-- `wikidataId` is the node kind of Wikidata identifiers: the letter `Q` followed by digits. -/
abbrev wikidataIdKind : SyntaxNodeKind := `wikidataId
/-- The main parser for Wikidata identifiers: it accepts `Q` followed by one or more digits. -/
def wikidataIdFn : ParserFn := fun c s =>
let i := s.pos
let s := takeWhileFn (fun c => c.isAlphanum) c s
if s.hasError then
s
else if s.pos == i then
ParserState.mkError s "wikidata id"
else
let id := c.extract i s.pos
match id.toList with
| 'Q' :: rest@(_ :: _) =>
if rest.all Char.isDigit then
mkNodeToken wikidataIdKind i true c s
else
ParserState.mkUnexpectedError s
"Wikidata ids must consist of the letter Q followed by digits."
| _ =>
ParserState.mkUnexpectedError s
"Wikidata ids must start with the letter Q followed by one or more digits."
@[inherit_doc wikidataIdFn]
def wikidataIdNoAntiquot : Parser := {
fn := wikidataIdFn
info := mkAtomicInfo "wikidataId"
}
@[inherit_doc wikidataIdFn]
def wikidataIdParser : Parser :=
withAntiquot (mkAntiquot "wikidataId" wikidataIdKind) wikidataIdNoAntiquot
end Mathlib.CrossRef
open Mathlib.CrossRef
/-- Extract the underlying tag as a string from a `stacksTag` node. -/
def Lean.TSyntax.getStacksTag (stx : TSyntax stacksTagKind) : CoreM String := do
let some val := Syntax.isLit? stacksTagKind stx | throwError "Malformed Stacks tag"
return val
/-- Extract the underlying identifier as a string from a `wikidataId` node. -/
def Lean.TSyntax.getWikidataId (stx : TSyntax wikidataIdKind) : CoreM String := do
let some val := Syntax.isLit? wikidataIdKind stx | throwError "Malformed Wikidata id"
return val
namespace Lean.PrettyPrinter
namespace Formatter
/-- The formatter for Stacks Project Tags syntax. -/
@[combinator_formatter stacksTagNoAntiquot] def stacksTagNoAntiquot.formatter :=
visitAtom stacksTagKind
/-- The formatter for Wikidata identifier syntax. -/
@[combinator_formatter wikidataIdNoAntiquot] def wikidataIdNoAntiquot.formatter :=
visitAtom wikidataIdKind
end Formatter
namespace Parenthesizer
/-- The parenthesizer for Stacks Project Tags syntax. -/
@[combinator_parenthesizer stacksTagNoAntiquot] def stacksTagAntiquot.parenthesizer := visitToken
/-- The parenthesizer for Wikidata identifier syntax. -/
@[combinator_parenthesizer wikidataIdNoAntiquot] def wikidataIdAntiquot.parenthesizer := visitToken
end Lean.PrettyPrinter.Parenthesizer
namespace Mathlib.CrossRef
/-! ### Stacks / Kerodon attribute -/
/-- The syntax category for the database name. -/
declare_syntax_cat stacksTagDB
/-- The syntax for a "kerodon" database identifier in a `@[kerodon]` attribute. -/
syntax "kerodon" : stacksTagDB
/-- The syntax for a "stacks" database identifier in a `@[stacks]` attribute. -/
syntax "stacks" : stacksTagDB
/-- The `stacksTag` attribute.
Use it as `@[kerodon TAG "Optional comment"]` or `@[stacks TAG "Optional comment"]`
depending on the database you are referencing.
The `TAG` is mandatory and should be a sequence of 4 digits or uppercase letters.
See the [Tags page](https://stacks.math.columbia.edu/tags) in the Stacks project or
[Tags page](https://kerodon.net/tag/) in the Kerodon project for more details.
-/
syntax (name := stacksTag) stacksTagDB stacksTagParser (ppSpace str)? : attr
initialize Lean.registerBuiltinAttribute {
name := `stacksTag
descr := "Apply a Stacks or Kerodon project tag to a theorem."
add := fun decl stx _attrKind => do
let (db, tag, comment) := ← match stx with
| `(attr| stacks $tag $[$comment]?) => return (Database.stacks, tag, comment)
| `(attr| kerodon $tag $[$comment]?) => return (Database.kerodon, tag, comment)
| _ => throwUnsupportedSyntax
let tagStr ← tag.getStacksTag
let commentStr := (comment.map (·.getString)).getD ""
addCrossRefDoc db decl tagStr commentStr
attachCrossRefWidget db tagStr commentStr stx
-- docstrings are immutable once an asynchronous elaboration task has been started
applicationTime := .beforeElaboration
}
/-! ### Wikidata attribute -/
/-- The `wikidata` attribute.
Use it as `@[wikidata Q12345 "Optional comment"]` to associate a Mathlib declaration with
the corresponding [Wikidata](https://www.wikidata.org) item.
The identifier must be the letter `Q` followed by one or more digits.
-/
syntax (name := wikidataTag) "wikidata" wikidataIdParser (ppSpace str)? : attr
initialize Lean.registerBuiltinAttribute {
name := `wikidataTag
descr := "Apply a Wikidata identifier to a declaration."
add := fun decl stx _attrKind => do
let (id, comment) := ← match stx with
| `(attr| wikidata $id $[$comment]?) => return (id, comment)
| _ => throwUnsupportedSyntax
let tagStr ← id.getWikidataId
let commentStr := (comment.map (·.getString)).getD ""
addCrossRefDoc .wikidata decl tagStr commentStr
attachCrossRefWidget .wikidata tagStr commentStr stx
-- docstrings are immutable once an asynchronous elaboration task has been started
applicationTime := .beforeElaboration
}
end Mathlib.CrossRef
/-- Returns the array of `Tag`s in the environment, sorted alphabetically by tag. -/
private def Lean.Environment.getSortedCrossRefs (env : Environment) : Array Tag :=
let tags := PersistentEnvExtension.getState tagExt env
tags.2.flatten.appendList tags.1 |>.qsort (·.tag < ·.tag)
/-- Returns the declaration names of results carrying the cross-reference `tag`. -/
private def Lean.Environment.getCrossRefDeclNames (env : Environment) (tag : String) :
Array Name :=
env.getSortedCrossRefs.filterMap fun d => if d.tag == tag then some d.declName else none
namespace Mathlib.CrossRef
/-- `traceCrossRefs db verbose` prints the cross-references of database `db` and
inlines the declaration types if `verbose` is `true`. -/
def traceCrossRefs (db : Database) (verbose : Bool := false) :
Command.CommandElabM Unit := do
let env ← getEnv
let entries := env.getSortedCrossRefs |>.filter (·.database == db)
if entries.isEmpty then logInfo "No tags found." else
let mut msgs := #[m!""]
for d in entries do
let (parL, parR) := if d.comment.isEmpty then ("", "") else (" (", ")")
let cmt := parL ++ d.comment ++ parR
msgs := msgs.push
m!"[{databaseLabel db} {d.tag}]({databaseURL db ++ d.tag}) \
corresponds to declaration '{.ofConstName d.declName}'.{cmt}"
if verbose then
let dType := ((env.find? d.declName).getD default).type
msgs := (msgs.push m!"{dType}").push ""
let msg := MessageData.joinSep msgs.toList "\n"
logInfo msg
/--
`#stacks_tags` retrieves all declarations that have the `stacks` attribute.
For each found declaration, it prints a line
```
'declaration_name' corresponds to tag 'declaration_tag'.
```
The variant `#stacks_tags!` also adds the theorem statement (for theorems)
or declaration type (for definitions, structures, instances, etc.) after each summary line.
-/
elab (name := stacksTags) "#stacks_tags" tk:("!")? : command =>
traceCrossRefs .stacks (tk.isSome)
/-- The `#kerodon_tags` command retrieves all declarations that have the `kerodon` attribute.
For each found declaration, it prints a line
```
'declaration_name' corresponds to tag 'declaration_tag'.
```
The variant `#kerodon_tags!` also adds the theorem statement (for theorems)
or declaration type (for definitions, structures, instances, etc.) after each summary line.
-/
elab (name := kerodonTags) "#kerodon_tags" tk:("!")? : command =>
traceCrossRefs .kerodon (tk.isSome)
/-- The `#wikidata_tags` command retrieves all declarations that have the `wikidata` attribute.
For each found declaration, it prints a line
```
'declaration_name' corresponds to tag 'declaration_tag'.
```
The variant `#wikidata_tags!` also adds the theorem statement (for theorems)
or declaration type (for definitions, structures, instances, etc.) after each summary line.
-/
elab (name := wikidataTags) "#wikidata_tags" tk:("!")? : command =>
traceCrossRefs .wikidata (tk.isSome)
end Mathlib.CrossRef