From 85dcca32998ace20c62a86a4f59022642791ca68 Mon Sep 17 00:00:00 2001 From: Joseph Rotella <7482866+jrr6@users.noreply.github.com> Date: Tue, 24 Jun 2025 05:52:11 +0200 Subject: [PATCH 01/10] feat: add error explanation elaboration and manual page --- Manual.lean | 3 + Manual/ErrorExplanations.lean | 83 ++++ Manual/Meta/ErrorExplanation.lean | 705 ++++++++++++++++++++++++++++++ static/search/domain-mappers.js | 17 + static/search/search-box.css | 1 + 5 files changed, 809 insertions(+) create mode 100644 Manual/ErrorExplanations.lean create mode 100644 Manual/Meta/ErrorExplanation.lean diff --git a/Manual.lean b/Manual.lean index e936c652..15c023b7 100644 --- a/Manual.lean +++ b/Manual.lean @@ -14,6 +14,7 @@ import Manual.Defs import Manual.Classes import Manual.Axioms import Manual.Terms +import Manual.ErrorExplanations import Manual.Tactics import Manual.Simp import Manual.BasicTypes @@ -129,6 +130,8 @@ Overview of the standard library, including types from the prelude and those tha {include 0 Manual.BuildTools} +{include 0 Manual.ErrorExplanations} + {include 0 Manual.Releases} # Index diff --git a/Manual/ErrorExplanations.lean b/Manual/ErrorExplanations.lean new file mode 100644 index 00000000..92759350 --- /dev/null +++ b/Manual/ErrorExplanations.lean @@ -0,0 +1,83 @@ +/- +Copyright (c) 2025 Lean FRO LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Author: Joseph Rotella +-/ + +import Manual.Meta.ErrorExplanation + +open Lean +open Verso Doc Elab Genre Manual + +namespace Manual + +set_option pp.rawOnError true +set_option guard_msgs.diff true + + +def Inline.errorExplanationLink (errorName : Name) : Manual.Inline where + name := `Manual.Inline.errorExplanationLink + data := toJson errorName + +@[inline_extension Inline.errorExplanationLink] +def Inline.errorExplanationLink.descr : InlineDescr where + traverse := fun _ _ _ => pure none + toTeX := none + toHtml := some fun go _ data content => + open Verso.Output.Html Verso.Doc.Html.HtmlT in do + let xref ← state + let .ok name := FromJson.fromJson? (α := String) data + | logError s!"Failed to parse error explanation link JSON: expected string, but found:\n{data}" + content.mapM go + let some obj := (← read).traverseState.getDomainObject? errorExplanationDomain name + | logError s!"Could not find explanation domain entry for name '{name}'" + content.mapM go + let some id := obj.getId + | logError s!"Could not find retrieve ID from explanation domain entry for name '{name}'" + content.mapM go + if let some { path, htmlId } := xref.externalTags.get? id then + let addr := path.link (some htmlId.toString) + pure {{{{← content.mapM go}}}} + else + logError s!"Could not find external tag for error explanation '{name}' corresponding to ID '{id}'" + content.mapM go + +@[block_role_expander error_explanation_table] +def error_explanation_table : BlockRoleExpander + | #[], #[] => do + let entries ← getErrorExplanationsSorted + let columns := 4 + let header := true + let name := "diagnostic-table" + let alignment : Option TableConfig.Alignment := none + let headers ← #["Name", "Summary", "Severity", "Since"] + |>.mapM fun s => ``(Verso.Doc.Block.para #[Doc.Inline.text $(quote s)]) + let vals ← entries.flatMapM fun (name, explan) => do + let sev := quote <| + if explan.metadata.severity == .warning then "Warning" else "Error" + let sev ← ``(Doc.Inline.text $sev) + let nameStr := toString name + let nameLink ← ``(Doc.Inline.other (Inline.errorExplanationLink $(quote name)) #[Doc.Inline.text $(quote nameStr)]) + let summary ← ``(Doc.Inline.text $(quote explan.metadata.summary)) + let since ← ``(Doc.Inline.text $(quote explan.metadata.sinceVersion)) + #[nameLink, summary, sev, since] + |>.mapM fun s => ``(Verso.Doc.Block.para #[$s]) + let blocks := (headers ++ vals).map fun c => Syntax.TSepArray.mk #[c] + pure #[← ``(Block.other (Block.table $(quote columns) $(quote header) $(quote name) $(quote alignment)) #[Block.ul #[$[Verso.Doc.ListItem.mk #[$blocks,*]],*]])] + | _, _ => throwError "unexpected syntax" + +-- Elaborating explanations can exceed the default heartbeat maximum: +set_option maxHeartbeats 1000000 + +#doc (Manual) "Error Explanations" => +%%% +number := false +htmlToc := false +%%% + +This section provides explanations of errors and warnings that may be generated +by Lean when processing a source file. + +{error_explanation_table} + +{make_explanations} diff --git a/Manual/Meta/ErrorExplanation.lean b/Manual/Meta/ErrorExplanation.lean new file mode 100644 index 00000000..d4f4926e --- /dev/null +++ b/Manual/Meta/ErrorExplanation.lean @@ -0,0 +1,705 @@ +/- +Copyright (c) 2025 Lean FRO LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Author: Joseph Rotella +-/ + +import VersoManual + +import Manual.Meta + +import Lean.ErrorExplanations + +open Lean Elab +open Verso.ArgParse +open Verso.Doc Elab +open Verso.Genre.Manual Markdown InlineLean +open SubVerso.Highlighting + +set_option pp.rawOnError true +set_option guard_msgs.diff true + +namespace Manual + +/-- +The directory in which MWE JSON files are written in the preprocessing step. +-/ +private def exampleJSONDirectory : System.FilePath := + "error_explanation_examples" + +/-- Loads the JSON data file for the preprocessed MWE code block `name`. -/ +def loadPreprocessedMWE (name : Name) (contents : String) + : MetaM (Highlighted × Array (MessageSeverity × String)) := do + let fileName : String := name.toString ++ ".json" + let path := exampleJSONDirectory / fileName + unless (← System.FilePath.pathExists path) do + throwError m!"Did not find expected preprocessed code block file `{path}`. \ + Run `lake build error_explanations`." + let fileContents ← IO.FS.readFile path + let json ← ofExcept <| Json.parse fileContents + let hls ← ofExcept <| json.getObjVal? "highlighted" + >>= FromJson.fromJson? (α := Highlighted) + let messages ← ofExcept <| json.getObjVal? "messages" + >>= FromJson.fromJson? (α := Array (MessageSeverity × String)) + let fileHash ← ofExcept <| json.getObjVal? "hash" + >>= FromJson.fromJson? (α := UInt64) + let fileVersion ← ofExcept <| json.getObjVal? "version" >>= Json.getStr? + unless fileHash == hash contents && fileVersion == Lean.versionString do + throwError m!"Preprocessed code block data file `{path}` is out of date. \ + Run `lake build error_explanations`." + return (hls, messages) + +/-- +A modified version of `Verso.Genre.Manual.InlineLean.lean` for rendering an MWE +in an error explanation. +-/ +def explanationMWE : CodeBlockExpander + | args, str => Manual.withoutAsync <| do + let config ← LeanBlockConfig.parse.run args + + let some name := config.name + | throwError "Explanation MWE is missing a name" + let (hls, msgs) ← loadPreprocessedMWE name str.getString + saveOutputs name msgs.toList + + pure #[← ``(Block.other + (Block.lean $(quote hls) (some $(quote (← getFileName))) none) + #[Block.code $str])] + +/-- +A tabbed container for MWEs in an error explanation example. Must satisfy the +invariant that `titles.size` is equal to the number of children of this block. +-/ +def Block.tabbedMWEs (titles : Array String) : Block where + name := `Manual.Block.tabbedMWEs + data := toJson titles + +@[block_extension Block.tabbedMWEs] +def Block.tabbedMWEs.descr : BlockDescr where + traverse id data _blocks := do + let name := + match FromJson.fromJson? (α := Option String) data with + | .ok (some name) => name + | _ => "error-example" + discard <| externalTag id (← read).path name + pure none + toTeX := none + extraCss := [r#" +.error-example-tabpanel-hidden { + display: none; +} +.error-example-container:not(:last-child) { + border-bottom: 1px solid gray; + padding-bottom: var(--verso--box-padding); +} +.error-example-tab-list [role="tab"] { + position: relative; + z-index: 1; + background: white; + border: 0; + padding: 0.2em; + cursor: pointer; +} +.error-example-tab-list [role="tab"]:not(:last-child) { + margin-right: 1rem; +} +.error-example-tab-list [role="tab"][aria-selected="true"] { + border-bottom: 1px solid gray; +} + "#] + extraJs := [r#" +window.addEventListener('DOMContentLoaded', () => { + const tabLists = document.querySelectorAll('.error-example-tab-list') + tabLists.forEach(tabList => { + const tabs = tabList.querySelectorAll(':scope > [role="tab"]') + + const setActiveTab = (e) => { + for (const tab of tabs) { + const controllee = document.getElementById(tab.getAttribute('aria-controls')) + if (tab === e.target) { + tab.setAttribute('aria-selected', true) + controllee.classList.remove('error-example-tabpanel-hidden') + } else { + tab.setAttribute('aria-selected', false) + controllee.classList.add('error-example-tabpanel-hidden') + } + } + } + + tabs.forEach(tab => { + tab.addEventListener('click', setActiveTab) + }) + + let focusedIdx = 0 + tabList.addEventListener('keydown', e => { + if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { + tabs[focusedIdx].setAttribute('tabindex', -1) + focusedIdx = + e.key === 'ArrowRight' + ? (focusedIdx + 1) % tabs.length + : (focusedIdx - 1 + tabs.length) % tabs.length + tabs[focusedIdx].setAttribute('tabindex', 0) + tabs[focusedIdx].focus() + } + }) + }) +}) + "#] + toHtml := some fun _goI goB id info contents => + open Verso.Doc.Html in + open Verso.Output Html in do + let .ok titles := FromJson.fromJson? (α := Array String) info + | HtmlT.logError "Invalid titles JSON for example block" + pure .empty + unless titles.size == contents.size do + HtmlT.logError s!"Mismatched number of titles and contents for example block: \ + Found {contents.size} tab panels but {titles.size} titles." + return .empty + let some { htmlId, .. } := (← HtmlT.state).externalTags[id]? + | HtmlT.logError "Could not find tag for error example" + pure .empty + let buttons ← titles.mapIdxM fun i (title : String) => do + let (tabIndex, selected) := if i == 0 then ("0", "true") else ("-1", "false") + let idxStr := toString i + return {{ + + }} + let panels ← contents.mapIdxM fun i b => do + let className := if i == 0 then "" else "error-example-tabpanel-hidden" + let idxStr := toString i + return {{ +
"Note: " "This diagnostic is no longer produced."
+{{errorName}}
}}
+ return html
+
@[block_role_expander error_explanation_table]
def error_explanation_table : BlockRoleExpander
| #[], #[] => do
@@ -49,11 +63,10 @@ def error_explanation_table : BlockRoleExpander
let headers ← #["Name", "Summary", "Severity", "Since"]
|>.mapM fun s => ``(Verso.Doc.Block.para #[Doc.Inline.text $(quote s)])
let vals ← entries.flatMapM fun (name, explan) => do
- let sev := quote <|
- if explan.metadata.severity == .warning then "Warning" else "Error"
+ let sev := quote <| if explan.metadata.severity == .warning then "Warning" else "Error"
let sev ← ``(Doc.Inline.text $sev)
- let nameStr := toString name
- let nameLink ← ``(Doc.Inline.other (Inline.errorExplanationLink $(quote name)) #[Doc.Inline.code $(quote nameStr)])
+ let nameLink ← ``(Doc.Inline.other (Inline.errorExplanationLink $(quote name))
+ #[Doc.Inline.other (Inline.errorExplanationShortName $(quote name)) #[]])
let summary ← ``(Doc.Inline.text $(quote explan.metadata.summary))
let since ← ``(Doc.Inline.text $(quote explan.metadata.sinceVersion))
#[nameLink, summary, sev, since]
@@ -72,7 +85,8 @@ htmlToc := false
%%%
This section provides explanations of errors and warnings that may be generated
-by Lean when processing a source file.
+by Lean when processing a source file. All error names listed below have the
+`lean` package prefix.
{error_explanation_table}
diff --git a/Manual/Meta/ErrorExplanation.lean b/Manual/Meta/ErrorExplanation.lean
index 32328181..c98fe2d9 100644
--- a/Manual/Meta/ErrorExplanation.lean
+++ b/Manual/Meta/ErrorExplanation.lean
@@ -678,6 +678,22 @@ def explanation : PartCommand
addExplanationBlocksFor config.name.getId
| _ => Lean.Elab.throwUnsupportedSyntax
+/--
+Returns the suffix of `name` as a string containing soft-hyphen characters at reasonable split points.
+-/
+def getBreakableSuffix (name : Name) : Option String := do
+ let suffix ← match name with
+ | .str _ s => s
+ | .num _ n => toString n
+ | .anonymous => none
+ let breakableHtml := softHyphenateIdentifiers.rwText (.text false suffix)
+ htmlText breakableHtml
+where
+ htmlText : Verso.Output.Html → String
+ | .text _ txt => txt
+ | .seq elts => elts.foldl (· ++ htmlText ·) ""
+ | .tag _nm _attrs children => htmlText children
+
open Verso Doc Elab ArgParse in
open Lean in
/-- Renders all error explanations as parts of the current page. -/
@@ -690,10 +706,12 @@ def make_explanations : PartCommand
let titleBits := #[← ``(Inline.other
(Inline.errorExplanation $(quote name) $(quote explan.metadata.summary))
#[Inline.code $(quote titleString)])]
+ let some shortTitleString := getBreakableSuffix name
+ | throwError m!"Found invalid explanation name `{name}` when generating explanations section"
PartElabM.push {
titleSyntax := quote (k := `str) titleString,
expandedTitle := some (titleString, titleBits),
- metadata := none,
+ metadata := some (← `({ shortTitle := $(quote shortTitleString) })),
blocks := #[],
priorParts := #[]
}
From 0f0d2ce2f5570a857c8b0cc09d4c2d7b72472f7a Mon Sep 17 00:00:00 2001
From: Joseph Rotella <7482866+jrr6@users.noreply.github.com>
Date: Fri, 27 Jun 2025 02:07:22 +0200
Subject: [PATCH 09/10] chore: add workaround for regression in latest nightly
---
Manual/Meta/ErrorExplanation.lean | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Manual/Meta/ErrorExplanation.lean b/Manual/Meta/ErrorExplanation.lean
index c98fe2d9..f18e2f62 100644
--- a/Manual/Meta/ErrorExplanation.lean
+++ b/Manual/Meta/ErrorExplanation.lean
@@ -435,7 +435,7 @@ def closeEnclosingSection : PartElabM Unit := do
throwError m!"Failed to close the last-opened explanation part"
/-- Adds explanation blocks until the "Examples" header is reached. -/
-def addNonExampleBlocks := do
+def addNonExampleBlocks : ExplanElabM Unit := do
repeat
let some block ← ExplanElabM.nextBlock?
| return
From d959a6ae7f0d7d10d99f05f8ddff5726b0cb0986 Mon Sep 17 00:00:00 2001
From: David Thrane Christiansen {{metadata.summary}}
{{entries}}
-