Skip to content

Commit b2a9fb7

Browse files
authored
feat: tactic overview page (#353)
* feat: a page documenting all tactics * Fix: save in correct location * Link to definition/module in tactic docs * Sort tactics alphabetically in output * Don't treat Tactics as a module when generating side bar * Don't need to talk about supplement anymore. * Add tactics.html to Lake's list of generated files * Case sensitivity
1 parent 4dbbb80 commit b2a9fb7

File tree

6 files changed

+154
-6
lines changed

6 files changed

+154
-6
lines changed

DocGen4/Output.lean

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import DocGen4.Output.References
1414
import DocGen4.Output.Bibtex
1515
import DocGen4.Output.SourceLinker
1616
import DocGen4.Output.Search
17+
import DocGen4.Output.Tactics
1718
import DocGen4.Output.ToJson
1819
import DocGen4.Output.FoundationalTypes
1920

@@ -52,6 +53,7 @@ def htmlOutputSetup (config : SiteBaseContext) : IO Unit := do
5253
let navbarHtml := ReaderT.run navbar config |>.toString
5354
let searchHtml := ReaderT.run search config |>.toString
5455
let referencesHtml := ReaderT.run (references (← collectBackrefs config.buildDir)) config |>.toString
56+
let tacticsHtml := ReaderT.run (tactics (← loadTacticsJSON config.buildDir)) config |>.toString
5557
let docGenStatic := #[
5658
("style.css", styleCss),
5759
("favicon.svg", faviconSvg),
@@ -70,7 +72,8 @@ def htmlOutputSetup (config : SiteBaseContext) : IO Unit := do
7072
("foundational_types.html", foundationalTypesHtml),
7173
("404.html", notFoundHtml),
7274
("navbar.html", navbarHtml),
73-
("references.html", referencesHtml)
75+
("references.html", referencesHtml),
76+
("tactics.html", tacticsHtml),
7477
]
7578
for (fileName, content) in docGenStatic do
7679
FS.writeFile (basePath config.buildDir / fileName) content
@@ -117,18 +120,22 @@ def htmlOutputResults (baseConfig : SiteBaseContext) (result : AnalyzerResult) (
117120
currentName := some modName
118121
}
119122
let (moduleHtml, cfg) := moduleToHtml module |>.run {} config baseConfig
123+
let (tactics, cfg) := module.tactics.mapM TacticInfo.docStringToHtml |>.run cfg config baseConfig
120124
if not cfg.errors.isEmpty then
121125
throw <| IO.userError s!"There are errors when generating '{filePath}': {cfg.errors}"
122126
if let .some d := filePath.parent then
123127
FS.createDirAll d
124128
FS.writeFile filePath moduleHtml.toString
125129
FS.writeFile (declarationsBasePath baseConfig.buildDir / s!"backrefs-{module.name}.json") (toString (toJson cfg.backrefs))
130+
saveTacticsJSON (declarationsBasePath baseConfig.buildDir / s!"tactics-{module.name}.json") tactics
126131
-- The output paths need to be relative to the build directory, as they are stored in a build
127132
-- artifact.
128133
outputs := outputs.push relFilePath
134+
129135
return outputs
130136

131-
def getSimpleBaseContext (buildDir : System.FilePath) (hierarchy : Hierarchy) : IO SiteBaseContext := do
137+
def getSimpleBaseContext (buildDir : System.FilePath) (hierarchy : Hierarchy) :
138+
IO SiteBaseContext := do
132139
let contents ← FS.readFile (declarationsBasePath buildDir / "references.json") <|> (pure "[]")
133140
match Json.parse contents with
134141
| .error err =>

DocGen4/Output/Navbar.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ The main entry point to rendering the navbar on the left hand side.
5757
def navbar : BaseHtmlM Html := do
5858
/-
5959
TODO: Add these in later
60-
<div class="nav_link"><a href={s!"{← getRoot}tactics.html"}>tactics</a></div>
6160
<div class="nav_link"><a href={s!"{← getRoot}commands.html"}>commands</a></div>
6261
<div class="nav_link"><a href={s!"{← getRoot}hole_commands.html"}>hole commands</a></div>
6362
<div class="nav_link"><a href={s!"{← getRoot}attributes.html"}>attributes</a></div>
6463
<div class="nav_link"><a href={s!"{← getRoot}notes.html"}>notes</a></div>
6564
-/
6665
let mut staticPages : Array Html := #[
6766
<div class="nav_link"><a href={s!"{← getRoot}"}>index</a></div>,
68-
<div class="nav_link"><a href={s!"{← getRoot}foundational_types.html"}>foundational types</a></div>
67+
<div class="nav_link"><a href={s!"{← getRoot}foundational_types.html"}>foundational types</a></div>,
68+
<div class="nav_link"><a href={s!"{← getRoot}tactics.html"}>tactics</a></div>,
6969
]
7070
let config ← read
7171
if not config.refs.isEmpty then

DocGen4/Output/Tactics.lean

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/-
2+
Copyright (c) 2025 Anne Baanen. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Anne Baanen
5+
-/
6+
import DocGen4.Process.Analyze
7+
import DocGen4.Output.Module
8+
9+
namespace DocGen4.Process
10+
11+
open scoped DocGen4.Jsx
12+
open DocGen4 Output Lean
13+
14+
/--
15+
Render the HTML for a single tactic.
16+
-/
17+
def TacticInfo.docStringToHtml (tac : TacticInfo MarkdownDocstring) : Output.HtmlM (TacticInfo Html) := do
18+
return {
19+
tac with
20+
docString := <p>[← Output.docStringToHtml tac.docString tac.internalName.toString]</p>
21+
}
22+
23+
/--
24+
Render the HTML for a single tactic.
25+
-/
26+
def TacticInfo.toHtml (tac : TacticInfo Html) : Output.BaseHtmlM Html := do
27+
let internalName := tac.internalName.toString
28+
let defLink := (← moduleNameToLink tac.definingModule) ++ "#" ++ internalName
29+
let tags := ", ".intercalate (tac.tags.map (·.toString)).qsort.toList
30+
return <div id={internalName}>
31+
<h2>{tac.userName}</h2>
32+
{tac.docString}
33+
<dl>
34+
<dt>Tags:</dt>
35+
<dd>{tags}</dd>
36+
<dt>Defined in module:</dt>
37+
<dd><a href={defLink}>{tac.definingModule.toString}</a></dd>
38+
</dl>
39+
</div>
40+
41+
def TacticInfo.navLink (tac : TacticInfo α) : Html :=
42+
<p><a href={"#".append tac.internalName.toString}>{tac.userName}</a></p>
43+
44+
end DocGen4.Process
45+
46+
namespace DocGen4.Output
47+
48+
open scoped DocGen4.Jsx
49+
open Lean Process
50+
51+
/--
52+
Render the HTML for the tactics listing page.
53+
-/
54+
def tactics (tacticInfo : Array (TacticInfo Html)) : BaseHtmlM Html := do
55+
let sectionsHtml ← tacticInfo.mapM (· |>.toHtml)
56+
templateLiftExtends (baseHtmlGenerator "Tactics") <| pure #[
57+
<nav class="internal_nav">
58+
<p><a href="#top">return to top</a></p>
59+
[tacticInfo.map (· |>.navLink)]
60+
</nav>,
61+
Html.element "main" false #[] (
62+
#[<p>The tactic language is a special-purpose programming language for constructing proofs, indicated using the keyword <code>by</code>.</p>] ++
63+
sectionsHtml)
64+
]
65+
66+
def loadTacticsJSON (buildDir : System.FilePath) : IO (Array (TacticInfo Html)) := do
67+
let mut result : Array (TacticInfo _) := #[]
68+
for entry in ← System.FilePath.readDir (declarationsBasePath buildDir) do
69+
if entry.fileName.startsWith "tactics-" && entry.fileName.endsWith ".json" then
70+
let fileContent ← IO.FS.readFile entry.path
71+
match Json.parse fileContent with
72+
| .error err =>
73+
throw <| IO.userError s!"failed to parse file '{entry.path}' as json: {err}"
74+
| .ok jsonContent =>
75+
match fromJson? jsonContent with
76+
| .error err =>
77+
throw <| IO.userError s!"failed to parse file '{entry.path}': {err}"
78+
| .ok (arr : Array (TacticInfo _)) => result := result ++ arr
79+
return result.qsort (lt := (·.userName < ·.userName))
80+
81+
/-- Save sections of supplementary pages declared in a specific module.
82+
83+
This `abbrev` exists as a type-checking wrapper around `toJson`, ensuring `loadTacticsJSON` gets
84+
objects in the expected format.
85+
-/
86+
abbrev saveTacticsJSON (fileName : System.FilePath) (tacticInfo : Array (TacticInfo Html)) : IO Unit := do
87+
if tacticInfo.size > 0 then
88+
IO.FS.writeFile fileName (toString (toJson tacticInfo))
89+
90+
end Output
91+
end DocGen4

DocGen4/Process/Analyze.lean

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ Released under Apache 2.0 license as described in the file LICENSE.
44
Authors: Henrik Böving
55
-/
66

7+
import Lean.Elab.Tactic.Doc
78
import Lean.Meta.Basic
9+
import Lean.Parser.Extension
810
import Std.Data.HashMap
911
import Std.Data.HashSet
1012

@@ -15,6 +17,10 @@ import DocGen4.Process.DocInfo
1517
namespace DocGen4.Process
1618

1719
open Lean Meta
20+
open Lean.Elab.Tactic.Doc
21+
22+
/-- Represents a non-verso docstring; these will be rendered using `Output.docStringToHtml`. -/
23+
abbrev MarkdownDocstring := String
1824

1925
/--
2026
Member of a module, either a declaration or some module doc string.
@@ -24,6 +30,25 @@ inductive ModuleMember where
2430
| modDoc (doc : ModuleDoc) : ModuleMember
2531
deriving Inhabited
2632

33+
/-- Information about a tactic declaration which will be rendered on the Tactics page.
34+
35+
This datastructure contains slightly different contents compared to a `TacticDoc` to make
36+
it easily serializable as JSON. It is designed to be easy to instantiate,
37+
using `{ tacticDoc with ... }`.
38+
-/
39+
structure TacticInfo (textType : Type) where
40+
/-- The name of the canonical parser for the tactic -/
41+
internalName : Name
42+
/-- The user-facing name to display (typically the first keyword token) -/
43+
userName : String
44+
/-- The tags that have been applied to the tactic -/
45+
tags : Array Name
46+
/-- The docstring for the tactic, including any extension docstrings. -/
47+
docString : textType
48+
/-- Name of the module where the tactic is declared. -/
49+
definingModule : Name
50+
deriving FromJson, ToJson
51+
2752
/--
2853
A Lean module.
2954
-/
@@ -37,6 +62,10 @@ structure Module where
3762
-/
3863
members : Array ModuleMember
3964
imports : Array Name
65+
/--
66+
Tactics declared in this module.
67+
-/
68+
tactics : Array (TacticInfo MarkdownDocstring)
4069
deriving Inhabited
4170

4271
/--
@@ -92,6 +121,24 @@ def AnalyzeTask.getLoad (task : AnalyzeTask) : Array Name :=
92121
| .analyzePrefixModules topLevel => #[topLevel]
93122
| .analyzeConcreteModules modules => modules
94123

124+
/-- Collect tactic info for pages to display in addition to the module docs. -/
125+
def collectTactics (module : Name) (env : Environment) :
126+
MetaM (Array (TacticInfo MarkdownDocstring)) := do
127+
let docs ← Elab.Tactic.Doc.allTacticDocs
128+
let mut contents := #[]
129+
for doc in docs do
130+
let some modIdx := env.getModuleIdxFor? doc.internalName | continue
131+
let definingModule := env.header.moduleNames[modIdx]!
132+
if module != definingModule then continue
133+
contents := contents.push {
134+
doc with
135+
docString := doc.docString.getD "This tactic has no documentation." ++
136+
("\n\n".intercalate doc.extensionDocs.toList)
137+
tags := doc.tags.toArray,
138+
definingModule := definingModule,
139+
}
140+
return contents
141+
95142
def getAllModuleDocs (relevantModules : Array Name) : MetaM (Std.HashMap Name Module) := do
96143
let env ← getEnv
97144
let mut res := Std.HashMap.emptyWithCapacity relevantModules.size
@@ -100,7 +147,8 @@ def getAllModuleDocs (relevantModules : Array Name) : MetaM (Std.HashMap Name Mo
100147
let some modIdx := env.getModuleIdx? module | unreachable!
101148
let moduleData := env.header.moduleData[modIdx]!
102149
let imports := moduleData.imports.map Import.module
103-
res := res.insert module <| Module.mk module modDocs imports
150+
let tactics ← collectTactics module env
151+
res := res.insert module <| Module.mk module modDocs imports tactics
104152
return res
105153

106154
def mkOptions : IO DocGenOptions := do

DocGen4/Process/Hierarchy.lean

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ def baseDirBlackList : Std.HashSet String :=
9696
"search.js",
9797
"src",
9898
"style.css",
99-
"favicon.svg"
99+
"favicon.svg",
100+
"tactics.html",
100101
]
101102

102103
partial def fromDirectoryAux (dir : System.FilePath) (previous : Name) : IO (Array Name) := do

lakefile.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ library_facet docs (lib) : Array FilePath := do
308308
basePath / "foundational_types.html",
309309
basePath / "references.html",
310310
basePath / "references.bib",
311+
basePath / "tactics.html",
311312
basePath / "find" / "index.html",
312313
basePath / "find" / "find.js"
313314
]

0 commit comments

Comments
 (0)