Skip to content

Commit 0002ea8

Browse files
authored
feat: pre-stage0 groundwork for named error messages (#8649)
This PR adds the pre-stage0-update infrastructure for named error messages. It adds macro syntax for registering and throwing named errors (without elaborators), mechanisms for displaying error names in the Infoview and at the command line, and the ability to link to error explanations in the manual (once they are added).
1 parent 7bd82b1 commit 0002ea8

File tree

11 files changed

+289
-33
lines changed

11 files changed

+289
-33
lines changed

src/Lean/DocString/Links.lean

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ If the environment variable `LEAN_MANUAL_ROOT` is set, it is used as the root. I
2323
root is pre-configured for the current Lean executable (typically true for releases), then it is
2424
used. If neither are true, then `https://lean-lang.org/doc/reference/latest/` is used.
2525
-/
26-
def manualRoot : BaseIO String := do
26+
builtin_initialize manualRoot : String
2727
let r ←
2828
if let some root := (← IO.getEnv "LEAN_MANUAL_ROOT") then
2929
pure root
@@ -35,6 +35,22 @@ def manualRoot : BaseIO String := do
3535
pure root
3636
return if r.endsWith "/" then r else r ++ "/"
3737

38+
/--
39+
The manual domain for error explanations.
40+
41+
We expose this because it is used to populate the URL of the error message description widget.
42+
-/
43+
def errorExplanationManualDomain :=
44+
"Manual.errorExplanation"
45+
46+
-- TODO: we may wish to make this more general for domains that require additional arguments
47+
/-- Maps `lean-manual` URL paths to their corresponding manual domains. -/
48+
private def domainMap : Std.HashMap String String :=
49+
Std.HashMap.ofList [
50+
("section", "Verso.Genre.Manual.section"),
51+
("errorExplanation", errorExplanationManualDomain)
52+
]
53+
3854
/--
3955
Rewrites links from the internal Lean manual syntax to the correct URL. This rewriting is an
4056
overapproximation: any parentheses containing the internal syntax of a Lean manual URL is rewritten.
@@ -74,7 +90,7 @@ def rewriteManualLinksCore (s : String) : BaseIO (Array (String.Range × String)
7490
out := out.push c
7591
break
7692
| .ok path =>
77-
out := out ++ (← manualRoot) ++ path
93+
out := out ++ manualRoot ++ path
7894
out := out.push c'
7995
iter := iter'
8096
break
@@ -104,17 +120,19 @@ where
104120

105121
rw (path : String) : Except String String := do
106122
match path.splitOn "/" with
107-
| "section" :: args =>
108-
if let [s] := args then
109-
if s.isEmpty then
110-
throw s!"Empty section ID"
111-
return s!"find/?domain=Verso.Genre.Manual.section&name={s}"
112-
else
113-
throw s!"Expected one item after 'section', but got {args}"
114123
| [] | [""] =>
115124
throw "Missing documentation type"
116-
| other :: _ =>
117-
throw s!"Unknown documentation type '{other}'. Expected 'section'."
125+
| kind :: args =>
126+
if let some domain := domainMap.get? kind then
127+
if let [s] := args then
128+
if s.isEmpty then
129+
throw s!"Empty {kind} ID"
130+
return s!"find/?domain={domain}&name={s}"
131+
else
132+
throw s!"Expected one item after `{kind}`, but got {args}"
133+
else
134+
let acceptableKinds := ", ".intercalate <| domainMap.toList.map fun (k, _) => s!"`{k}`"
135+
throw s!"Unknown documentation type `{kind}`. Expected one of the following: {acceptableKinds}"
118136

119137

120138
/--

src/Lean/Exception.lean

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,27 @@ def unknownIdentifierMessageTag : Name := `unknownIdentifier
8080
protected def throwErrorAt [Monad m] [MonadError m] (ref : Syntax) (msg : MessageData) : m α := do
8181
withRef ref <| Lean.throwError msg
8282

83+
/--
84+
Throw an error exception with the specified name, with position information from `getRef`.
85+
86+
Note: Use the macro `throwNamedError`, which validates error names, instead of calling this function
87+
directly.
88+
-/
89+
protected def «throwNamedError» [Monad m] [MonadError m] (name : Name) (msg : MessageData) : m α := do
90+
let ref ← getRef
91+
let msg := msg.tagWithErrorName name
92+
let (ref, msg) ← AddErrorMessageContext.add ref msg
93+
throw <| Exception.error ref msg
94+
95+
/--
96+
Throw an error exception with the specified name at the position `ref`.
97+
98+
Note: Use the macro `throwNamedErrorAt`, which validates error names, instead of calling this
99+
function directly.
100+
-/
101+
protected def «throwNamedErrorAt» [Monad m] [MonadError m] (ref : Syntax) (name : Name) (msg : MessageData) : m α :=
102+
withRef ref <| Lean.throwNamedError name msg
103+
83104
/--
84105
Creates a `MessageData` that is tagged with `unknownIdentifierMessageTag`.
85106
This tag is used by the 'import unknown identifier' code action to detect messages that should

src/Lean/Log.lean

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ Authors: Leonardo de Moura
55
-/
66
prelude
77
import Lean.Util.Sorry
8+
import Lean.Widget.Types
89
import Lean.Message
10+
import Lean.DocString.Links
911

1012
namespace Lean
1113

@@ -53,6 +55,48 @@ register_builtin_option warningAsError : Bool := {
5355
descr := "treat warnings as errors"
5456
}
5557

58+
/--
59+
A widget for displaying error names and explanation links.
60+
-/
61+
-- Note that we cannot tag this as a `builtin_widget_module` in this module because doing so would
62+
-- create circular imports. Instead, we add this attribute post-hoc in `Lean.ErrorExplanation`.
63+
def errorDescriptionWidget : Widget.Module where
64+
javascript := "
65+
import { createElement } from 'react';
66+
export default function ({ code, explanationUrl }) {
67+
const sansText = { fontFamily: 'var(--vscode-font-family)' }
68+
69+
const codeSpan = createElement('span', {}, [
70+
createElement('span', { style: sansText }, 'Error code: '), code])
71+
const brSpan = createElement('span', {}, '\\n')
72+
const linkSpan = createElement('span', { style: sansText },
73+
createElement('a', { href: explanationUrl }, 'View explanation'))
74+
75+
const all = createElement('div', { style: { marginTop: '1em' } }, [codeSpan, brSpan, linkSpan])
76+
return all
77+
}"
78+
79+
/--
80+
If `msg` is tagged as a named error, appends the error description widget displaying the
81+
corresponding error name and explanation link. Otherwise, returns `msg` unaltered.
82+
-/
83+
private def MessageData.appendDescriptionWidgetIfNamed (msg : MessageData) : MessageData :=
84+
match errorNameOfKind? msg.kind with
85+
| some errorName =>
86+
let url := manualRoot ++ s!"find/?domain={errorExplanationManualDomain}&name={errorName}"
87+
let inst := {
88+
id := ``errorDescriptionWidget
89+
javascriptHash := errorDescriptionWidget.javascriptHash
90+
props := return json% {
91+
code: $(toString errorName),
92+
explanationUrl: $url
93+
}
94+
}
95+
-- Note: we do not generate corresponding message data for the widget because it pollutes
96+
-- console output
97+
msg.composePreservingKind <| .ofWidget inst .nil
98+
| none => msg
99+
56100
/--
57101
Log the message `msgData` at the position provided by `ref` with the given `severity`.
58102
If `getRef` has position information but `ref` does not, we use `getRef`.
@@ -80,26 +124,63 @@ def logAt (ref : Syntax) (msgData : MessageData)
80124
def logErrorAt (ref : Syntax) (msgData : MessageData) : m Unit :=
81125
logAt ref msgData MessageSeverity.error
82126

127+
/--
128+
Log a named error message using the given message data. The position is provided by `ref`.
129+
130+
Note: Use the macro `logNamedErrorAt`, which validates error names, instead of calling this function
131+
directly.
132+
-/
133+
protected def «logNamedErrorAt» (ref : Syntax) (name : Name) (msgData : MessageData) : m Unit :=
134+
logAt ref (msgData.tagWithErrorName name) MessageSeverity.error
135+
83136
/-- Log a new warning message using the given message data. The position is provided by `ref`. -/
84137
def logWarningAt [MonadOptions m] (ref : Syntax) (msgData : MessageData) : m Unit := do
85138
logAt ref msgData .warning
86139

140+
/--
141+
Log a named error warning using the given message data. The position is provided by `ref`.
142+
143+
Note: Use the macro `logNamedWarningAt`, which validates error names, instead of calling this function
144+
directly.
145+
-/
146+
protected def «logNamedWarningAt» (ref : Syntax) (name : Name) (msgData : MessageData) : m Unit :=
147+
logAt ref (msgData.tagWithErrorName name) MessageSeverity.warning
148+
87149
/-- Log a new information message using the given message data. The position is provided by `ref`. -/
88150
def logInfoAt (ref : Syntax) (msgData : MessageData) : m Unit :=
89151
logAt ref msgData MessageSeverity.information
90152

91153
/-- Log a new error/warning/information message using the given message data and `severity`. The position is provided by `getRef`. -/
92-
def log (msgData : MessageData) (severity : MessageSeverity := MessageSeverity.error): m Unit := do
154+
def log (msgData : MessageData) (severity : MessageSeverity := MessageSeverity.error)
155+
(isSilent : Bool := false) : m Unit := do
93156
let ref ← MonadLog.getRef
94-
logAt ref msgData severity
157+
logAt ref msgData severity isSilent
95158

96159
/-- Log a new error message using the given message data. The position is provided by `getRef`. -/
97160
def logError (msgData : MessageData) : m Unit :=
98161
log msgData MessageSeverity.error
99162

163+
/--
164+
Log a named error message using the given message data. The position is provided by `getRef`.
165+
166+
Note: Use the macro `logNamedError`, which validates error names, instead of calling this function
167+
directly.
168+
-/
169+
protected def «logNamedError» (name : Name) (msgData : MessageData) : m Unit :=
170+
log (msgData.tagWithErrorName name) MessageSeverity.error
171+
100172
/-- Log a new warning message using the given message data. The position is provided by `getRef`. -/
101173
def logWarning [MonadOptions m] (msgData : MessageData) : m Unit := do
102-
log msgData (if warningAsError.get (← getOptions) then .error else .warning)
174+
log msgData .warning
175+
176+
/--
177+
Log a named warning using the given message data. The position is provided by `getRef`.
178+
179+
Note: Use the macro `logNamedWarning`, which validates error names, instead of calling this function
180+
directly.
181+
-/
182+
protected def «logNamedWarning» (name : Name) (msgData : MessageData) : m Unit :=
183+
log (msgData.tagWithErrorName name) MessageSeverity.warning
103184

104185
/-- Log a new information message using the given message data. The position is provided by `getRef`. -/
105186
def logInfo (msgData : MessageData) : m Unit :=

src/Lean/Message.lean

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,26 @@ import Lean.Util.Sorry
1515

1616
namespace Lean
1717

18-
def mkErrorStringWithPos (fileName : String) (pos : Position) (msg : String) (endPos : Option Position := none) : String :=
18+
/--
19+
Creates a string describing an error message `msg` produced at `pos`, optionally ending at `endPos`,
20+
in `fileName`.
21+
22+
Additional optional arguments can be used to prepend a label `kind` describing the severity of
23+
the error (e.g., `"warning"` or `"error"`) and a bracketed `name` label displaying the name of the
24+
error if it has one.
25+
-/
26+
def mkErrorStringWithPos (fileName : String) (pos : Position) (msg : String)
27+
(endPos : Option Position := none) (kind : Option String := none) (name : Option Name := none)
28+
: String :=
1929
let endPos := match endPos with
2030
| some endPos => s!"-{endPos.line}:{endPos.column}"
2131
| none => ""
22-
s!"{fileName}:{pos.line}:{pos.column}{endPos}: {msg}"
32+
let label := if name.isSome || kind.isSome then
33+
let name := name.map (s!"({·})")
34+
s!" {kind.getD ""}{name.getD ""}:"
35+
else
36+
""
37+
s!"{fileName}:{pos.line}:{pos.column}{endPos}:{label} {msg}"
2338

2439
inductive MessageSeverity where
2540
| information | warning | error
@@ -154,6 +169,16 @@ def isTrace : MessageData → Bool
154169
| .trace _ _ _ => true
155170
| _ => false
156171

172+
/--
173+
`composePreservingKind msg msg'` appends the contents of `msg'` to the end of `msg` but ensures that
174+
the resulting message preserves the kind (as given by `MessageData.kind`) of `msg`.
175+
-/
176+
def composePreservingKind : MessageData → MessageData → MessageData
177+
| withContext ctx msg , msg' => withContext ctx (composePreservingKind msg msg')
178+
| withNamingContext nc msg, msg' => withNamingContext nc (composePreservingKind msg msg')
179+
| tagged t msg , msg' => tagged t (compose msg msg')
180+
| msg , msg' => compose msg msg'
181+
157182
/-- An empty message. -/
158183
def nil : MessageData :=
159184
ofFormat Format.nil
@@ -401,6 +426,45 @@ structure SerialMessage extends BaseMessage String where
401426
kind : Name
402427
deriving ToJson, FromJson
403428

429+
/--
430+
A suffix added to diagnostic name-containing tags to indicate that they should be used as an error
431+
code.
432+
-/
433+
def errorNameSuffix := "_namedError"
434+
435+
/--
436+
Produces a `MessageData` tagged with an identifier for error `name`.
437+
438+
Note: this function generally should not be called directly; instead, use the macros `logNamedError`
439+
and `throwNamedError`.
440+
-/
441+
def MessageData.tagWithErrorName (msg : MessageData) (name : Name) : MessageData :=
442+
.tagged (.str name errorNameSuffix) msg
443+
444+
/--
445+
If the provided name is labeled as a diagnostic name, removes the label and returns the
446+
corresponding diagnostic name.
447+
448+
Note: we use this labeling mechanism so that we can have error kinds that are not intended to be
449+
shown to the user, without having to validate the presence of an error explanation at runtime.
450+
-/
451+
def errorNameOfKind? : Name → Option Name
452+
| .str p last => if last == errorNameSuffix then some p else none
453+
| _ => none
454+
455+
/--
456+
Returns the error name with which `msg` is tagged, if one exists.
457+
458+
Note that this is distinct from `msg.kind`: the `kind` of a named-error message is not equal to its
459+
name, and there exist message kinds that are not error-name kinds.
460+
-/
461+
def MessageData.errorName? (msg : MessageData) : Option Name :=
462+
errorNameOfKind? msg.kind
463+
464+
@[inherit_doc MessageData.errorName?]
465+
def Message.errorName? (msg : Message) : Option Name :=
466+
msg.data.errorName?
467+
404468
namespace SerialMessage
405469

406470
@[inline] def toMessage (msg : SerialMessage) : Message :=
@@ -413,8 +477,10 @@ protected def toString (msg : SerialMessage) (includeEndPos := false) : String :
413477
str := msg.caption ++ ":\n" ++ str
414478
match msg.severity with
415479
| .information => pure ()
416-
| .warning => str := mkErrorStringWithPos msg.fileName msg.pos (endPos := endPos) "warning: " ++ str
417-
| .error => str := mkErrorStringWithPos msg.fileName msg.pos (endPos := endPos) "error: " ++ str
480+
| .warning =>
481+
str := mkErrorStringWithPos msg.fileName msg.pos str endPos "warning" (errorNameOfKind? msg.kind)
482+
| .error =>
483+
str := mkErrorStringWithPos msg.fileName msg.pos str endPos "error" (errorNameOfKind? msg.kind)
418484
if str.isEmpty || str.back != '\n' then
419485
str := str ++ "\n"
420486
return str

src/Lean/Meta/Hint.lean

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ The props to this widget are of the following form:
3030
{"type": "unchanged", "text": "h"},
3131
{"type": "deletion", "text": "ello"},
3232
{"type": "insertion", "text": "i"}
33-
]
33+
],
34+
"suggestion": "hi",
35+
"range": {
36+
"start": {"line": 100, "character": 0},
37+
"end": {"line": 100, "character": 5}
38+
}
3439
}
3540
```
3641

src/Lean/Meta/Tactic/TryThis.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export default function ({ suggestions, range, header, isInline, style }) {
8686
inner)
8787
}"
8888

89-
-- Because we can't reference `builtin_widget_module` in `Lean.Meta.Hint`, we add the attribute here
89+
-- Because we can't use `builtin_widget_module` in `Lean.Meta.Hint`, we add the attribute here
9090
attribute [builtin_widget_module] Hint.tryThisDiffWidget
9191

9292
/-! # Code action -/

src/Lean/Parser/Command.lean

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,14 @@ builtin_initialize
858858
register_parser_alias openDecl
859859
register_parser_alias docComment
860860

861+
/--
862+
Registers an error explanation.
863+
864+
Note that the error name is not relativized to the current namespace.
865+
-/
866+
@[builtin_command_parser] def registerErrorExplanationStx := leading_parser
867+
docComment >> "register_error_explanation " >> ident >> termParser
868+
861869
end Command
862870

863871
namespace Term

0 commit comments

Comments
 (0)