-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCollect.lean
More file actions
3921 lines (3509 loc) · 204 KB
/
Copy pathCollect.lean
File metadata and controls
3921 lines (3509 loc) · 204 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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
module
public import Lean
public import Lean.DeclarationRange
public import Lean.Meta.Instances
public import Lean.Util.Sorry
public import Lake.CLI.Main
public import Lake.Load.Workspace
public import MD4Lean
public import VersoManual
public import VersoManual.Markdown
public import MeaningGraph
public import Characterization
public import ChallengeGen
public import Referee.Formalization
public import Referee.Claims
@[expose] public section
/-!
# Collecting the exposed declarations of a project
Walks a compiled project's environment and builds one `DeclInfo` per exposed declaration:
signature, docstring, source location and snippet, kind, and dependency lists.
The dependency analysis itself lives in `MeaningGraph` — a standalone module that knows nothing
about this tool's output. This file only decides *which* dependency edges Referee follows
(`meaningDeps` for what a declaration means and rests on, `closureDeps` for the wider extraction
closure) and attaches the results to `DeclInfo`.
-/
open Lake
open Lean
open Lean.Meta
open Verso.Doc
open Verso.Genre
open Manual
namespace Referee
open Verso.Output Html
open MeaningGraph
open ChallengeGen
/-- CLI options used to configure site generation. Shared across the `collect`,
`extract`, `build-site`, and `all` subcommands; each one only consults the fields relevant
to it. -/
structure Cli where
rootPrefix : Option Name := none
repoUrl : Option String := none
siteUrl : Option String := none
siteTitle : Option String := none
outputDir : Option String := none
excludeLibs : Array Name := #[]
/-- Path to the collected-data JSON file: written by `collect`, read by `extract` and
`build-site`. -/
dataPath : Option String := none
/-- Single input file, used by the `highlight-file` worker subcommand. -/
inputPath : Option String := none
/-- Maximum number of worker processes to run at once. Defaults to the CPU count. -/
jobs : Option Nat := none
/-- Packages the reader is told to take on trust (`--trust`), each standing for itself *and
everything it depends on*: trusting `mathlib` trusts `batteries` and `aesop` with it, because
Mathlib's own correctness already rests on them.
A render-time flag, like `--repo-url`: the same `data.json` can be rendered with different trust
sets, which is the point — "what if I have not audited LML" should not need re-importing the
project. -/
trustedPackages : Array Name := #[]
/-- Draw declarations from *audited* packages in the graph's upstream band as well as unaudited
ones (`--show-trusted-upstream`).
Off by default, and the default is the interesting judgement. Drawing them answers "what is this
statement about", but on a Mathlib-backed project it is a median of 21 nodes and a p90 of 53 per
page — a band several times the size of the project structure underneath it, restating something
the declaration's own code block at the top of the page already shows, with types on hover. What
the band is *for* is the part no other view has: the packages nobody has vouched for. Those are
always drawn.
Kept as a flag rather than removed because the capability is cheap to carry — the data is collected
either way — and because on a project whose upstream is small the answer could go the other way. -/
showTrustedUpstream : Bool := false
/-- An earlier `collect` output to diff the current one against (`--baseline`).
A render-time flag for the same reason `--trust` is one: the comparison is a pure function of the
two JSON files, so a reader can ask "what changed since v0.2" without re-importing anything. With
none given, the site says nothing about revisions at all. -/
baselinePath : Option String := none
/-- What to call the baseline on the page (`--baseline-label`). Defaults to the file name. -/
baselineLabel : Option String := none
/-- A `semantic_hash export` JSONL file to read declaration hashes from (`--hashes`).
A `collect`-time input rather than a render-time flag, unlike `--trust` and `--baseline`: the
hashes are a property of the compiled environment, so they belong in `data.json` beside every
other thing derived from it. Optional — without it every hash is `none` and the revision diff
compares text, exactly as it did before the field existed. -/
hashesPath : Option String := none
/-- The provenance ledger (`--provenance`): written and updated by the `provenance` subcommand,
read by `build-site`.
One file for both directions, because the ledger is append-only: each run folds the current
revision into whatever is already there. A render-time flag for `build-site`, gated exactly as
`--baseline` is — without it the site says nothing about when anything changed. -/
provenancePath : Option String := none
/-- What to call this revision in the ledger (`--ref`). Defaults to `git describe --tags
--always`, which prefers a tag and falls back to a short sha. -/
revisionRef : Option String := none
/-- Whether to re-check the decoded data's closure invariants on load (`--no-verify` turns it
off). On by default: it is the guard on the one part of the pipeline `MeaningGraph`'s proofs do
not cover, the `intern`/`resolve` round trip. Turning it off is for repeated renders of a file
already checked once — it grew 73× across a 5.5× step in library size, so at Mathlib scope it is
minutes of re-verifying a file that has not changed. -/
verifyIntegrity : Bool := true
/-- Restrict the build to the results the project puts forward and what their *statements* rest
on (`--claims-only`). See `Referee.Claims` for where the list of results comes from.
Read by `collect`, which is where the scope is decided, and recorded in `data.json` so that
`build-site` can frame the site without re-deciding it. Unlike `--trust` or `--baseline` this
cannot be varied over one collected file, because the file no longer holds what a wider build
would need — which is the point of it. -/
claimsOnly : Bool := false
/-- Build the site for one declaration (`--only DECL`).
Exactly `--claims-only` with a claim set of this one declaration: the same scope rule, and the
same site, so everything its statement rests on gets a page too. Rendering only the named
declaration was the first design and it was wrong — a claim's page is mostly the dependency graph
under it, and a graph whose nodes have no pages strands the reader on a summary panel when what
they want is the card. -/
onlyDecl : Option Name := none
/-- Declarations to treat as the project's main results (`--claim NAME`), overriding whatever
`formalization.yaml` or a Comparator setup says. Repeatable.
For a project with neither metadata document, and for the case where you disagree with the one it
has. Ignored unless `--claims-only` or `--only` asked for a scoped build. -/
claimNames : Array Name := #[]
/-- Where to look for Comparator configs (`--comparator DIR`), when they are not where the
metadata points and not in a directory the shallow scan finds. -/
comparatorDir : Option String := none
/-- Whether `build-site` renders one chapter at a time instead of the whole library in one Verso
invocation (`--per-chapter`).
Verso builds the entire document tree before streaming any page out, so a monolithic render's
peak memory tracks the library — measured at 14.95 GB for 28,251 declarations and projected past
150 GB for Mathlib. Per-chapter rendering bounds the peak by the largest chapter instead, at the
cost of a stitching pass afterwards: the global artifacts Verso derives from the whole tree
(sidebar, `xref.json`, the `find` page, hover data) are merged from the per-chapter runs. -/
perChapter : Bool := false
deriving Repr
/-- The kind label shown to a reader, which is finer-grained than `DeclKind`.
Lean records `lemma` and `theorem` as the same kind, and some declarations written with `instance`
elaborate to theorems, so `DeclKind.label` alone would show a `lemma` as "Theorem". A reader
comparing the site against the source should see the keyword the author actually wrote. -/
def displayKindLabel (kindLabel : String) (isLemma isInstanceDecl : Bool) : String :=
if isInstanceDecl then "Instance"
else if isLemma then "Lemma"
else kindLabel
/-- Source file location (path and line range) for a declaration. -/
structure SourceInfo where
relPath : String
absPath : System.FilePath
line : Nat
endLine : Nat
deriving Repr, ToJson, FromJson
/-- Data container for LinkInfo. -/
structure LinkInfo where
label : String
href? : Option String := none
deriving Repr, ToJson, FromJson
/-- Data container for DeclCardData.
No name field: the card is the only one on its page, and the page's title is the declaration's
name. -/
structure DeclCardData where
anchorId : String
kindLabel : String
isLemma : Bool := false
isInstanceDecl : Bool := false
tags : Array String := #[]
deriving Repr, ToJson, FromJson, Inhabited
/-- One row of a declaration index: the listing used for module contents, the claims and trust
pages, and a declaration's dependency closures. -/
structure DeclIndexEntry where
name : String
href : String
/-- The label shown to the reader (`displayKindLabel`). -/
kind : String
/-- `definition` / `lemma` / `theorem`, matching the sidebar's visibility toggles. -/
group : String
/-- Project declarations in its closure, where that is worth showing. -/
deps : Option Nat := none
/-- An extra fact about this row, shown after the kind and dependency count. Empty for most
listings; used by the specification page to say how many properties a definition has. -/
note : String := ""
dependsOnSorry : Bool := false
deriving Repr, ToJson, FromJson, Inhabited
/-- Data container for DeclIndexData. -/
structure DeclIndexData where
entries : Array DeclIndexEntry
deriving Repr, ToJson, FromJson, Inhabited
/-- One row of the claims listing.
The listing is rendered *here* rather than by `audit.js`, unlike everything else on that page,
because a row carries the declaration's docstring and the docstring has to be real markdown — the
same `docBlocks` the declaration's own card renders, through the same Verso pipeline, so that code
spans, emphasis and math look the way they look everywhere else. A client building rows from JSON
can only be handed text, and text is what it looked like.
What the browser still owns is the *state* on the row: coverage, verdict, and the button. Those
arrive as empty slots that `audit.js` fills and refills, which is also why the rows survive a
verdict change untouched — nothing is rebuilt, two spans are rewritten. -/
structure ClaimRow where
name : String
href : String
/-- Project declarations in its statement closure. Shown until the browser replaces it with how
many of them the reader has accepted, so a reader without JavaScript still gets the number. -/
deps : Nat := 0
dependsOnSorry : Bool := false
/-- How many of the enclosing block's contents are this row's docstring. The rows share one flat
array of contents — a block extension gets its children as a list, not as a tree — so each says
how many of them are its own, and the renderer walks the two in step. -/
docLength : Nat := 0
deriving Repr, ToJson, FromJson, Inhabited
/-- Data container for ClaimListData. The block's contents are every row's docstring blocks
concatenated, divided up by `ClaimRow.docLength`. -/
structure ClaimListData where
rows : Array ClaimRow
deriving Repr, ToJson, FromJson, Inhabited
/-- One row of a specification listing: on a definition's page a theorem offered as part of its
specification, on a theorem's page a definition the theorem specifies.
Unlike `DeclIndexEntry` this carries the statement and the author's comment, because that is the
whole point of the listing: a reader should be able to judge whether the properties pin the
definition down without following a single link. -/
structure SpecRow where
name : String
/-- Empty when the declaration at the other end is not exposed and so has no page of its own. -/
href : String := ""
/-- The label shown to the reader (`displayKindLabel`). -/
kind : String
/-- The author's note on why this belongs in the specification. Empty when they wrote none. -/
comment : String := ""
/-- The statement, source form. Empty where the row is a bare link. -/
signature : String := ""
deriving Repr, ToJson, FromJson, Inhabited
/-- Data container for SpecListData. -/
structure SpecListData where
entries : Array SpecRow
deriving Repr, ToJson, FromJson, Inhabited
/-! ## The statement, taken apart
A theorem's type is one telescope of binders, and to a reader who cannot yet parse a Mathlib-style
statement it is a wall in which the objects, the structure assumed on them, the hypotheses and the
claim all look alike. `StatementAnatomy` is that telescope split by the role each binder plays,
computed at `collect` time inside the binder's own context so that every type prints with the
author's variable names. The grouping that turns it into a reading order — each object with the
typeclass assumptions made on it — is `StatementAnatomy.grouped`, kept pure so it can be tested. -/
/-- The role one binder of a statement plays for a reader. -/
inductive BinderRole where
/-- An object the statement is about: a type, a function, an element, a measure. -/
| object
/-- Structure assumed on one of those objects: an instance binder such as `[MeasurableSpace Ω]`
or `[IsProbabilityMeasure μ]`, or any binder whose type is a class — `{mΩ : MeasurableSpace Ω}`
is structure on `Ω` however the author chose to bind it. -/
| typeclass
/-- A proposition assumed: any other binder whose type is a `Prop`. -/
| hypothesis
deriving Repr, BEq, ToJson, FromJson, Inhabited
/-- One piece of a pretty-printed type: a run of text, and the constant it names when the pretty
printer says the run is a constant token. What lets every constant in a binder's type carry its own
hover, the way each token of highlighted code does. -/
structure TypePiece where
text : String := ""
const : Name := .anonymous
deriving Repr, BEq, ToJson, FromJson, Inhabited
/-- Merges runs of plain text, so a type has as few pieces as it has constants. -/
def coalescePieces (pieces : Array TypePiece) : Array TypePiece :=
pieces.foldl (init := #[]) fun acc p =>
if p.text.isEmpty then acc
else match acc.back? with
| some last =>
if last.const.isAnonymous && p.const.isAnonymous then
acc.pop.push { last with text := last.text ++ p.text }
else acc.push p
| none => acc.push p
/-- Cuts the pieces to `limit` characters in all, ending with the ellipsis `clipTo` uses. A piece
cut in the middle loses its constant: half a name is not a hover target. -/
def clipPieces (limit : Nat) (pieces : Array TypePiece) : Array TypePiece := Id.run do
let mut out : Array TypePiece := #[]
let mut used := 0
for p in pieces do
let len := p.text.length
if used + len ≤ limit then
out := out.push p
used := used + len
else
return out.push { text := (p.text.take (limit - used)).toString ++ "…" }
return out
/-- One binder of a statement's telescope, with what a reader needs in order to place it. -/
structure StatementBinder where
/-- The binder's name as the author wrote it; empty for an anonymous one (`P → Q`, `[inst : …]`). -/
name : String := ""
role : BinderRole := .object
/-- Whether the object is itself a type — `Ω : Type u_1`, `p : Prop` — rather than an element, a
function or a measure. Types are listed first: everything else in the statement lives in one. -/
isType : Bool := false
/-- `{x}` or `⦃x⦄`: not written where the theorem is used, but inferred from the other arguments. -/
implicit : Bool := false
/-- The binder's type, pretty-printed in the context of the binders before it. -/
type : String := ""
/-- The same text split at its constants, for a hover on each; see `TypePiece`. -/
pieces : Array TypePiece := #[]
/-- The head constant of that type when it has one, looking through leading `∀`s: the class of a
typeclass binder, the predicate or relation of a hypothesis. What a gloss can be looked up for. -/
head : Name := .anonymous
/-- The earlier binders this one's type mentions, as indices into the telescope, in order of first
occurrence. What attaches `[MeasurableSpace Ω]` to `Ω`. -/
mentions : Array Nat := #[]
deriving Repr, BEq, ToJson, FromJson, Inhabited
/-- One field of a structure or class, for the parts view of its page: the body of a structure is
its fields, and a field's own docstring is what a reader wants on hover. -/
structure StatementField where
name : String := ""
/-- The field's type, in the context of the parameters and the fields before it. -/
type : String := ""
/-- The same text split at its constants; see `TypePiece`. -/
pieces : Array TypePiece := #[]
/-- The head constant of that type, for a hover when the field has no docstring of its own. -/
head : Name := .anonymous
/-- The field's docstring, as written on it. -/
doc : String := ""
deriving Repr, BEq, ToJson, FromJson, Inhabited
/-- A statement split into its binders and its conclusion — or a definition into its parameters, its
result type and its body. `isProp` says which reading applies: a theorem's conclusion is a claim, a
definition's is the type of what it produces. -/
structure StatementAnatomy where
binders : Array StatementBinder := #[]
/-- The conclusion — a theorem's claim, or a definition's result type — pretty-printed in the
context of every binder. -/
conclusion : String := ""
/-- The conclusion's head constant when it has one, looking through leading `∀`s. -/
conclusionHead : Name := .anonymous
/-- The conclusion split at its constants; see `TypePiece`. -/
conclusionPieces : Array TypePiece := #[]
/-- Whether the conclusion is a proposition. False for a definition, whose conclusion is the type
of its value, and whose `body` or `fields` then say what that value is. -/
isProp : Bool := true
/-- A definition's value, in the context of its parameters. Empty for a theorem — its proof is not
part of what it says — and for anything else without one worth showing. -/
body : String := ""
/-- The body split at its constants; see `TypePiece`. -/
bodyPieces : Array TypePiece := #[]
/-- A structure's or class's fields, in the context of its parameters. -/
fields : Array StatementField := #[]
deriving Repr, BEq, ToJson, FromJson, Inhabited
/-- An object of a statement together with the typeclass assumptions attached to it. -/
structure AnatomyObject where
/-- Its index in the telescope. -/
index : Nat
binder : StatementBinder
instances : Array StatementBinder := #[]
deriving Repr, BEq, Inhabited
/-- `StatementAnatomy.grouped`: the reading order of a statement. -/
structure AnatomyGroups where
/-- The objects that are types, in telescope order, each with the structure assumed on it. -/
types : Array AnatomyObject := #[]
/-- Every other object, in telescope order, each with the structure assumed on it. -/
objects : Array AnatomyObject := #[]
/-- Typeclass binders attached to no object: their types mention none, or only hypotheses. -/
loose : Array StatementBinder := #[]
hypotheses : Array StatementBinder := #[]
deriving Repr, BEq, Inhabited
/-- Groups a statement's binders for reading: each object with the typeclass assumptions on it, then
the hypotheses.
A typeclass binder attaches to the *last-introduced* object its type mentions. `[MeasurableSpace Ω]`
mentions only `Ω`; `[Module R M]` mentions `R` and `M` and goes under `M`, the thing it is structure
on, rather than under the ring it is structure over. One mentioning no object at all —
`[DecidableEq ℕ]`, or `[Fact p]` for a hypothesis `p` — is kept rather than dropped, since it is
still assumed. -/
def StatementAnatomy.grouped (a : StatementAnatomy) : AnatomyGroups := Id.run do
let mut groups : AnatomyGroups := {}
-- Telescope index ↦ where the object went — `true` for `types`, `false` for `objects` — and its
-- position there.
let mut rowOf : Std.HashMap Nat (Bool × Nat) := {}
for i in [:a.binders.size] do
let b := a.binders[i]!
match b.role with
| .object =>
if b.isType then
rowOf := rowOf.insert i (true, groups.types.size)
groups := { groups with types := groups.types.push { index := i, binder := b } }
else
rowOf := rowOf.insert i (false, groups.objects.size)
groups := { groups with objects := groups.objects.push { index := i, binder := b } }
| .hypothesis =>
groups := { groups with hypotheses := groups.hypotheses.push b }
| .typeclass =>
let target? := b.mentions.foldl (init := (none : Option Nat)) fun acc j =>
if rowOf.contains j then some (max (acc.getD j) j) else acc
match target?.bind (fun j => rowOf.get? j) with
| some (true, row) =>
groups := { groups with types := groups.types.modify row fun o =>
{ o with instances := o.instances.push b } }
| some (false, row) =>
groups := { groups with objects := groups.objects.modify row fun o =>
{ o with instances := o.instances.push b } }
| none =>
groups := { groups with loose := groups.loose.push b }
return groups
/-- One piece of a type as `Block.anatomy` renders it: text, and the tip it is a hover for. -/
structure AnatomyPiece where
text : String := ""
/-- The `AnatomyTip.head` this piece opens on hover, or empty for plain text. -/
head : String := ""
deriving Repr, ToJson, FromJson, Inhabited
/-- One typeclass assumption of `Block.anatomy`, with what the site could find out about its class. -/
structure AnatomyInstanceRow where
name : String := ""
type : String := ""
/-- `type` split at its constants, each a hover of its own; empty falls back to `type`. -/
pieces : Array AnatomyPiece := #[]
/-- The class, shown only when there is a gloss or a link to hang it on. -/
head : String := ""
/-- The class's page, when the project declares it. -/
href : String := ""
/-- One sentence about the class, from its docstring. -/
gloss : String := ""
deriving Repr, ToJson, FromJson, Inhabited
/-- One object or hypothesis of `Block.anatomy`, or its conclusion. -/
structure AnatomyRow where
name : String := ""
type : String := ""
/-- `type` split at its constants, each a hover of its own; empty falls back to `type`. -/
pieces : Array AnatomyPiece := #[]
implicit : Bool := false
head : String := ""
href : String := ""
gloss : String := ""
/-- For an object: the typeclass assumptions attached to it. -/
instances : Array AnatomyInstanceRow := #[]
deriving Repr, ToJson, FromJson, Inhabited
/-- What a hover on a row of `Block.anatomy` shows for its head constant: the constant, its
signature, its whole docstring, and its page when the project declares it. One per head constant
per page — rows point at it by name — so a class assumed on five objects ships its docstring once. -/
structure AnatomyTip where
head : String := ""
signature : String := ""
doc : String := ""
href : String := ""
deriving Repr, ToJson, FromJson, Inhabited
/-- The payload of `Block.anatomy`: `StatementAnatomy.grouped` with the glosses and tips resolved. -/
structure AnatomyData where
types : Array AnatomyRow := #[]
objects : Array AnatomyRow := #[]
loose : Array AnatomyInstanceRow := #[]
hypotheses : Array AnatomyRow := #[]
/-- The claim, or a definition's result type; `isProp` says which. -/
conclusion : AnatomyRow := {}
isProp : Bool := true
/-- A definition's value, shown under its result type. -/
body : String := ""
/-- `body` split at its constants; empty falls back to `body`. -/
bodyPieces : Array AnatomyPiece := #[]
/-- A structure's fields, shown under its result type; each row's hover is the field's docstring. -/
fields : Array AnatomyRow := #[]
tips : Array AnatomyTip := #[]
deriving Repr, ToJson, FromJson, Inhabited
/-- The gloss for `head`, or nothing if this page has already shown one for it. -/
def glossOnce (seen : Std.HashSet String) (head gloss : String) :
String × Std.HashSet String :=
if head.isEmpty || gloss.isEmpty then (gloss, seen)
else if seen.contains head then ("", seen)
else (gloss, seen.insert head)
/-- `glossOnce` over a list of rows and the instances nested in each, in reading order. -/
def dedupeRows (seen : Std.HashSet String) (rows : Array AnatomyRow) :
Array AnatomyRow × Std.HashSet String := Id.run do
let mut seen := seen
let mut out : Array AnatomyRow := #[]
for row in rows do
let (gloss, seen') := glossOnce seen row.head row.gloss
seen := seen'
let mut instances : Array AnatomyInstanceRow := #[]
for inst in row.instances do
let (g, s) := glossOnce seen inst.head inst.gloss
seen := s
instances := instances.push { inst with gloss := g }
out := out.push { row with gloss, instances }
return (out, seen)
/-- Keeps the first gloss for each head constant, in reading order, and blanks the rest. Under the
first of five measurable spaces "a space equipped with a σ-algebra" is an explanation; under all five
it is noise. Links survive repeats, being one word each. -/
def AnatomyData.dedupeGlosses (data : AnatomyData) : AnatomyData := Id.run do
let (types, seen₁) := dedupeRows {} data.types
let (objects, seen₂) := dedupeRows seen₁ data.objects
let mut seen := seen₂
let mut loose : Array AnatomyInstanceRow := #[]
for inst in data.loose do
let (g, s) := glossOnce seen inst.head inst.gloss
seen := s
loose := loose.push { inst with gloss := g }
let (hypotheses, seen₃) := dedupeRows seen data.hypotheses
let (g, seen₄) := glossOnce seen₃ data.conclusion.head data.conclusion.gloss
let (fields, _) := dedupeRows seen₄ data.fields
return { data with
types, objects, loose, hypotheses, fields
conclusion := { data.conclusion with gloss := g } }
/-- One of the three declarations a characterization is made of, as rendered: the predicate, a
theorem that the definition satisfies it, or a theorem that nothing else does.
Carries the statement for the same reason `SpecRow` does, and more urgently. A characterization is
checked for *shape* and not for content, so the only thing that tells a reader whether it says
anything is the property's own body and the relation the uniqueness theorem stops at — both of
which are here. -/
structure CharPartRow where
/-- `Property`, `Existence` or `Uniqueness`, as shown to the reader. -/
role : String
name : String
/-- Empty when the declaration is not exposed and so has no page of its own. -/
href : String := ""
/-- The label shown to the reader (`displayKindLabel`). -/
kind : String := ""
/-- The author's note, from the attribute. Empty when they wrote none. -/
comment : String := ""
/-- On a uniqueness row, the relation it establishes, as written in its conclusion. Empty
otherwise. -/
relation : String := ""
/-- The statement, source form. -/
signature : String := ""
deriving Repr, ToJson, FromJson, Inhabited
/-- One characterization of a definition, as rendered. -/
structure CharRow where
/-- The characterizing predicate, for the header sentence. -/
property : String
/-- The relations its uniqueness theorems establish, as written. Empty when there are none. -/
relations : Array String := #[]
/-- Whether a theorem states that the definition satisfies the property. -/
hasExistence : Bool := false
/-- Whether a theorem states that the property determines its subject.
Kept apart from `hasExistence` rather than collapsed into one "complete" flag because the two
gaps read differently and the card has to say which one it is: existence without uniqueness is a
specification wearing a characterization's clothes, and uniqueness without existence is a claim
about a property that may hold of nothing at all. -/
hasUniqueness : Bool := false
/-- The relations' own definitions, deduplicated, in the order the uniqueness theorems introduce
them.
Without these the banner names the claim's conclusion and does not say what it *is*:
`Indistinguishable μ A ⟨M⟩` reads as reassurance whether it means "agree a.s. at every time" or
something far weaker, and the reader cannot tell which without leaving the page. Since the
relation is the one part of a characterization that is reported rather than checked, showing it
is not a convenience.
Empty for a relation from the toolchain — `=` and `↔` need no introduction, and printing
`{α : Sort u} → α → α → Prop` where the point is to explain something would be noise. -/
relationDefs : Array CharPartRow := #[]
/-- The property, then the existence theorems, then the uniqueness theorems. -/
parts : Array CharPartRow := #[]
deriving Repr, ToJson, FromJson, Inhabited
/-- Data container for `Block.charList`. -/
structure CharListData where
entries : Array CharRow
deriving Repr, ToJson, FromJson, Inhabited
/-- One row of the Browse table: every exposed declaration, with the columns a reader sorts and
filters on. Deliberately light — no docstring or statement — since every row of the library rides
along in a single page. -/
structure BrowseRow where
name : String
href : String
/-- Reader-facing kind (`displayKindLabel`). -/
kind : String
/-- `definition` / `lemma` / `theorem`, matching the sidebar's visibility toggles. -/
group : String
module : String
chapter : String
/-- Project declarations in its closure. -/
deps : Nat
/-- Distinct constants outside the project its closure bottoms out in. -/
ext : Nat
dependsOnSorry : Bool
/-- Rests on an axiom beyond `Classical.choice`/`propext`/`Quot.sound`. -/
extraAxioms : Bool
/-- Theorems declared to be part of this declaration's specification. `none` for anything a
specification cannot be about (see `DeclInfo.isDefinitionLike`), which is what lets the table
distinguish "no specification" from "not the sort of thing that has one". -/
specs : Option Nat := none
/-- How it changed since the baseline (`ChangeKind.slug`), or `none` when the site was built
without `--baseline`. The column and its filter exist only in the `some` case, so a site with no
baseline is exactly the site it was before the field existed. -/
change : Option String := none
/-- What this declaration means now (`meaningKeyOf`), so the Verdict column can mark an
acceptance recorded against a different meaning. Empty on a build without semantic hashes. -/
meaning : String := ""
/-- The revision at which its meaning last changed, or `none` without a provenance ledger — which
is what makes the column and its sort disappear on a site that has none. -/
changedRef : Option String := none
/-- The date of that revision, `YYYY-MM-DD`, which is also what the column sorts on: it orders
correctly as a string, and it is what a reader scans for. -/
changedDate : String := ""
deriving Repr, ToJson, FromJson, Inhabited
/-- Data container for BrowseData. -/
structure BrowseData where
rows : Array BrowseRow
/-- The project name, which is the audit state's storage key.
Carried here because the Verdict column needs it and this page has no other audit payload: the
script that owns that state cannot look up a verdict without knowing which project's state to
read, and it is deliberately the only thing that knows. -/
project : String := ""
deriving Repr, ToJson, FromJson, Inhabited
/-- Which voice a stretch of text is in, and how to say so.
Nearly every word on this site is *derived*: the tool read the compiled library and wrote a label, a
count or a sentence about what it found. A few are not — someone wrote them, and a reader deciding
how much to believe needs to know which is which before reading, not after.
`voice` is the writer: `authors` for anything taken verbatim from the project (docstrings, module
docs, the README), `reader` for the reader's own notes. It is a string rather than an enumeration
because the list is open — an interpretation generated by a model is the next one, and it must be
addable without every consumer of this payload learning about it.
`label` is what the tool says the block is, in the tool's own voice, above the quoted text. Empty
where the block is one of many — a listing row, a hover, a one-sentence gloss — and the typeface
carries the attribution on its own. -/
structure VoiceData where
voice : String
label : String := ""
deriving Repr, ToJson, FromJson, Inhabited
/-- Data container for DetailsData. -/
structure DetailsData where
summary : String
/-- Rendered already open. The default is shut, which is what a fold is usually for; a chapter on
the claims listing is the other case — folding it is for putting a chapter you are done with out
of the way, not for hiding the page's contents until asked. -/
startsOpen : Bool := false
/-- Marks a fold whose summary is a heading rather than a control, so it can be styled as one. -/
headingLevel : Option Nat := none
deriving Repr, ToJson, FromJson, Inhabited
/-- Data container for GraphNode. -/
structure GraphNode where
id : String
label : String
kind : String
status : String
groupKey : String
moduleName : String
href : String
/-- True for the declaration whose page this graph is on, so the reader can see at a glance
which node the picture is about. False everywhere on the whole-repository graph. -/
focus : Bool := false
/-- The declaration's statement, source form, for the summary the graph shows under a clicked
node until that node's own card arrives. -/
signature : String := ""
/-- The declaration's docstring, as written. Empty when it has none. -/
doc : String := ""
/-- What this declaration means now (`meaningKeyOf`), so the graph can mark the nodes a reader
has accepted *without* marking the ones they accepted an earlier version of.
The verdict itself lives in the browser and is never collected; only the key it has to be checked
against travels with the node. Empty on a build without semantic hashes, which switches the
staleness half of the check off exactly as it is switched off everywhere else. -/
meaning : String := ""
/-- The upstream package this node comes from, or `""` for a project declaration.
Set on exactly the nodes that belong in the upstream *band* rather than in the dependency rows.
They are all sinks — nothing in the graph precedes them — so they would otherwise pile into the top
row and make it several thousand pixels wide; `graph.js` lays them out as a wrapped band grouped by
this field instead. `status` says whether the package is audited (`"trusted"`/`"untrusted"`). -/
upstream : String := ""
/-- The package's depth in the workspace dependency graph (`packageRanks`), which is the order the
band stacks in: a package sits above one that depends on it, so the band reads the same way the
dependency rows below it do. -/
upstreamRank : Nat := 0
/-- Drawn without following its own dependencies, so nothing above it in the picture is its.
A graph is normally closed downwards — every node's dependencies are drawn — and a reader is
entitled to assume it. A view that stops somewhere on purpose (a characterization stops at the
definition and its theorems) breaks that assumption, and an unmarked node whose parents happen to
be absent is indistinguishable from one that rests on nothing. So the node is marked, the key
explains the mark, and clicking it says so in as many words. -/
unexpanded : Bool := false
deriving Repr, ToJson, FromJson
/-- Data container for GraphEdge. -/
structure GraphEdge where
source : String
target : String
deriving Repr, ToJson, FromJson
/-- A second node set for the same picture, offered to the reader as a switch.
The one case there is: a definition that carries a characterization rests, *as a claim*, on far less
than it rests on as a construction — building a stochastic integral is work, and recognising one is
a property and a relation. The two views answer "what do I have to accept to believe this was built"
and "what do I have to read to know what it is", and only drawing both makes the gap visible. -/
structure GraphView where
/-- The switch's label. Short: it sits in a row of buttons. -/
label : String
/-- One sentence above the picture saying what this node set is and what it leaves out. Shown only
while the view is selected, since it describes this view rather than the graph. -/
note : String := ""
/-- What to say when the reader clicks a node this view stopped at (`GraphNode.unexpanded`). Held
on the view rather than on each node because it is one fact about where this view cuts, and
writing it into every cut node would repeat the same sentence across the payload. -/
unexpandedNote : String := ""
nodes : Array GraphNode
edges : Array GraphEdge
/-- `edges` interned against `nodes`; see `GraphData.edgeIx`. -/
edgeIx : Array Nat := #[]
/-- The graph's nodes as `SiteContext.nodeIndex` numbers, positionally, when every one of them
could be reduced to a number; `nodes` is then empty and the client rebuilds it from the shared
tables. A node that no table can restore — an upstream constant the current flags do not ship —
leaves the whole view in the `nodes` form instead, since a mixed encoding would cost more to say
than it saves.
Flat rather than an array of objects because after thinning a node *is* its number: writing
`{"ix":12043}` spends thirteen bytes to say six, and there are thousands of them on a page. -/
nodeIx : Array Nat := #[]
/-- Positions in `nodeIx` of the focus node, and of the nodes a view stopped at — the two facts a
thinned node still carries. Held as positions rather than as flags per node so that the common
case, one focus and nothing cut, costs one number and one empty array. -/
focusIx : Array Nat := #[]
unexpandedIx : Array Nat := #[]
deriving Repr, ToJson, FromJson
/-- Data container for GraphData. -/
structure GraphData where
nodes : Array GraphNode
edges : Array GraphEdge
/-- What a node stands for, singular. Used in the graph's own explanatory text, so that a graph
of modules does not describe itself as a graph of declarations. -/
unit : String := "declaration"
/-- The project's own name, which labels the first of its dependency rows the way each upstream
package labels the first row of its block. -/
projectName : String := ""
/-- The switch's label for `nodes`/`edges` themselves, which are always the first view. Empty when
`views` is, and then no switch is drawn at all: a picture with one view needs no name for it. -/
viewLabel : String := ""
/-- The note for the first view, on the same terms as `GraphView.note`. -/
viewNote : String := ""
/-- The views offered *besides* `nodes`/`edges`. Empty on every graph but a characterized
definition's, which is why the first view is left in place rather than folded into this array —
the payload rides in every page and the common case must not pay for the rare one. -/
views : Array GraphView := #[]
/-- Chapter slugs whose declaration tables this graph's project nodes are looked up in; see
`thinGraphNodes`. The page loads one `<script src>` per entry, and a node carrying only an `id`
is meaningless without them, so this is emitted even though nothing draws it. -/
tables : Array String := #[]
/-- `edges`, interned: each consecutive pair is a source and a target index into `nodes`.
An edge written out is two full declaration names — around 90 bytes for what a pair of small
integers says, and there are more edges than nodes on any graph worth drawing. Interned, the same
edge is about 6. Flat rather than an array of pairs because a JSON array of two-element arrays
spends two brackets and a comma per edge on saying "pair" again.
`edges` holds whatever could not be interned — an edge whose endpoint the graph never listed as a
node, which `transitiveReduce` already tolerates — so the two are read together and neither is
authoritative alone. -/
edgeIx : Array Nat := #[]
/-- The graph's nodes as `SiteContext.nodeIndex` numbers, positionally, when every one of them
could be reduced to a number; `nodes` is then empty and the client rebuilds it from the shared
tables. A node that no table can restore — an upstream constant the current flags do not ship —
leaves the whole view in the `nodes` form instead, since a mixed encoding would cost more to say
than it saves.
Flat rather than an array of objects because after thinning a node *is* its number: writing
`{"ix":12043}` spends thirteen bytes to say six, and there are thousands of them on a page. -/
nodeIx : Array Nat := #[]
/-- Positions in `nodeIx` of the focus node, and of the nodes a view stopped at — the two facts a
thinned node still carries. Held as positions rather than as flags per node so that the common
case, one focus and nothing cut, costs one number and one empty array. -/
focusIx : Array Nat := #[]
unexpandedIx : Array Nat := #[]
deriving Repr, ToJson, FromJson
/-- Drops the fields whose value carries no information, recursively.
Lean's derived `ToJson` writes every field, default or not, so a thinned node still spelled out
`"label":"","kind":"","status":""` and so on — around 120 bytes of nothing per node, which is most
of what thinning it saved. Every default in `GraphData`, `GraphNode`, `GraphEdge` and `GraphView` is
the empty string, `false`, `0` or the empty array, so dropping exactly those is what `FromJson` puts
back, and the round trip is unchanged.
`graph.js` reads these fields through `||` — an absent field and an empty one have always been the
same thing to it — so nothing on the other side has to know which of the two it got. -/
partial def compactJson : Json → Json
| .obj fields =>
let kept := fields.foldl (init := ([] : List (String × Json))) fun acc k v =>
let v := compactJson v
let empty :=
v == Json.str "" || v == Json.bool false || v == Json.num 0 || v == Json.arr #[]
if empty then acc else (k, v) :: acc
Json.mkObj kept.reverse
| .arr items => .arr (items.map compactJson)
| j => j
/-- One end of a specification link, as written with the `@[specifies]` attribute of the
`Characterization` package: the declaration at the other end and the author's note on why the
theorem belongs in the specification (empty when they wrote none).
Used in both directions — on a theorem the `name` is the definition being specified, on a
definition it is a theorem specifying it — because both ends want the same comment. -/
structure SpecLink where
name : Name
comment : String := ""
deriving Repr, BEq, ToJson, FromJson, Inhabited
/-- A uniqueness theorem of a characterization, with the relation it stops at.
The relation is the whole load-bearing part and the reason it is carried as text rather than as a
name: `Characterization` reads it off the theorem's conclusion as written (`x = y`, `f =ᵐ[μ] g`,
`IsoRel a b`), which is what a reader needs to see, while the head constant is only good for
grouping. A characterization up to a coarse relation says correspondingly less, and nothing but
showing the relation can convey that. -/
structure CharUniqueness where
name : Name
/-- The relating conclusion as written, with the theorem's own variable names. -/
relation : String := ""
/-- Its head constant (`Eq`, `Filter.EventuallyEq`, …), anonymous when there is none. -/
relationHead : Name := .anonymous
deriving Repr, BEq, ToJson, FromJson, Inhabited
/-- One characterization claimed for a definition, as written with the `@[characterization]`
attribute of the `Characterization` package: a predicate, the theorems saying the definition
satisfies it, and the theorems saying nothing else does.
Stronger than a `SpecLink` in exactly one way that matters to a reader, and it is worth being
precise about which. `@[specifies]` is an unchecked claim; the shapes here *are* checked — the
predicate really is a predicate on the definition's type, the existence theorem really states that
the definition satisfies it, the uniqueness theorem really relates two objects that satisfy it. What
is still not checked is whether the predicate says anything worth saying. So a complete bundle is
evidence, not proof, and the site has to print the property and the relation rather than merely
report that they exist. -/
structure CharBundle where
/-- The characterizing predicate. -/
property : Name
/-- The author's note on the property, from the attribute. Empty when they wrote none. -/
comment : String := ""
/-- The theorems stating that the definition satisfies `property`, in declaration order. -/
existence : Array Name := #[]
/-- The theorems stating that `property` determines its subject, in declaration order. -/
uniqueness : Array CharUniqueness := #[]
deriving Repr, BEq, ToJson, FromJson, Inhabited
/-- Whether both halves of the claim are present. A property with an existence theorem and no
uniqueness theorem says no more than a `@[specifies]` annotation does — the definition has the
property, and so, for all this says, might anything else. -/
def CharBundle.isComplete (b : CharBundle) : Bool :=
!b.existence.isEmpty && !b.uniqueness.isEmpty
/-- The link from one of a characterization's three declarations back to the claim it is part of.
The reverse index of `characterizedBy`, computed by `attachCharacterizes`, and the reason a reader
who lands on `IsEntropy.unique` is told what it is for rather than left with a lemma about a
predicate. -/
structure CharPartLink where
/-- The definition characterized. -/
target : Name
/-- The characterizing predicate. -/
property : Name
/-- `property`, `existence` or `uniqueness`. -/
role : String
deriving Repr, BEq, ToJson, FromJson, Inhabited
/-- Fully collected metadata for one exposed declaration. -/
structure DeclInfo where
name : Name
moduleName : Name
modulePath : String
groupKey : String
kind : DeclKind
displaySignature : String
expandedSignature : String
docBlocks : Array (Block Manual)
/-- The docstring as written, before markdown parsing. `docBlocks` is the rendered form and is
the right thing for a page; this is kept for the places that need plain text, such as the
dependency graph's node data, which is built as JSON rather than as Verso blocks. -/
docText? : Option String := none
proofText? : Option String
source? : Option SourceInfo
/-- True if the declaration was written with the `lemma` keyword (a `theorem` alias used in
Mathlib to mark less central results). -/
isLemma : Bool := false
/-- True if the declaration was written with the `instance` keyword but was not classified as
`.instance` by `declKindOf`. -/
isInstanceDecl : Bool := false
/-- True if the declaration comes from an `alias` command. Unlike a real theorem, its body is kept
verbatim during extraction (`alias … := target`) rather than replaced by `sorry`, so its transitive
closure must follow value dependencies (the alias target), not just type dependencies. -/
isAlias : Bool := false
/-- True if `sorryAx` occurs anywhere in the declaration's transitive closure — whether the
`sorry` is its own or inherited from something it rests on. Computed with `Lean.collectAxioms`,
so it sees through compiler-generated helpers and into upstream libraries alike. -/
dependsOnSorry : Bool := false
/-- True if this declaration's *own* type or body contains a `sorry`. `dependsOnSorry` without
`hasOwnSorry` means the gap is inherited, and the site has to point at where it actually is
rather than blame this declaration for it. -/
hasOwnSorry : Bool := false
/-- Every axiom the declaration's closure rests on, i.e. what `#print axioms` reports. Beyond
`Classical.choice`/`propext`/`Quot.sound` these are assumptions a reader is being asked to
grant, so they are reported rather than collapsed into a flag. -/
axioms : Array Name := #[]
/-- For a theorem carrying `@[specifies …]`: the definitions its author declared it to be part of
the specification of, in the order written.
Almost everything else on `DeclInfo` is derived from the environment; this is not. It is the
author's editorial claim about which properties pin a definition down, read back from the
environment extension the `Characterization` package writes, and there is no way to infer it.
Empty for every declaration of a project that does not use that package — which is why nothing
downstream treats its absence as an error. The named definition need not be exposed, or even
belong to this project. -/
specifies : Array SpecLink := #[]
/-- For a definition: the exposed theorems that carry `@[specifies thisDeclaration …]`, in
declaration order. The reverse index of `specifies`, computed by `attachSpecifiedBy`. -/
specifiedBy : Array SpecLink := #[]
/-- For a definition: the characterizations its author claims for it — the strictly stronger
claim that a property does not merely hold of it but *determines* it, up to a stated relation.
Read from the same `Characterization` extension as `specifies`, and empty for a project that does
not use the attribute. Both theorems of a characterization also register as `@[specifies]`
annotations, so everything here is visible in `specifiedBy` too; this field is what lets the site
tell a definition that has been pinned down from one that has merely been described. -/
characterizedBy : Array CharBundle := #[]
/-- For a declaration that is one of the three parts of a characterization: which claims it
belongs to and in what role. The reverse index of `characterizedBy`, computed by
`attachCharacterizes`. -/
characterizes : Array CharPartLink := #[]
/-- The upstream packages this declaration *directly* references, after `attachUpstreamPackages`
has propagated them along the project's own dependency edges.
"Directly" is the important word: these are the packages some constant of the declaration's
project-level closure names, not the full set it rests on. The site closes them over the Lake
dependency graph to get that — see the note on upstream packages above for why the two agree.
Follows `meaningDeps`: a theorem contributes what its *statement* mentions, not what its proof
calls. An upstream proof is not a trust dependency — the kernel rechecked it, and anything left
unproved in it surfaces as a `sorry` or an extra axiom, both of which `axioms` already reports
transitively. What cannot be checked for you is an upstream *definition* your statement is about. -/
upstreamPackages : Array Name := #[]
/-- The declaration's rename-invariant *semantic* hash, read from a `semantic_hash export` file
(`collect --hashes`). Fixed-width hex, so it reads as an identifier rather than as a number
someone might compare with `<`.
Structural over the elaborated `Expr`, and — the property everything downstream rests on —
**deep**: a referenced constant contributes *its* hash rather than its name, so this changes when
the meaning of anything in the declaration's closure changes, upstream included. Invariant to
renaming, to binder names, to `mdata`, and to how anything pretty-prints.
`none` for a project collected without `--hashes`, and for any declaration the export did not
cover; `Referee.Diff` falls back to comparing text per declaration, so absence degrades rather
than breaks. -/
semanticHash? : Option String := none
/-- The same hash with theorem bodies hidden, so a theorem hashes by its *proposition*: proof
subterms contribute the hash of the proposition they prove rather than of the term.
This is `meaningDeps` computed at the `Expr` level by an independent implementation — a theorem's
proof-irrelevant hash depends on its statement's closure and on nothing its proof merely calls —
which is why the revision diff can use it directly as "did the meaning move". The pair
(`semanticHash?` differs, this one does not) is exactly a proof-only change. -/
proofIrrelHash? : Option String := none
deps : Array Name
typeDeps : Array Name := #[]