Skip to content

Commit 45e668d

Browse files
fix: parameter references in Verso docstrings (#14198)
This PR fixes a bug where references to parameters by names failed for unbracketed binders, in the presence of `_` parameters, and in macro-generated declarations. Parameters are introduced by walking the declaration's binders, matching them up to the elaborated type. Cases were missing for bare identifiers and `_`.
1 parent 6b941e7 commit 45e668d

6 files changed

Lines changed: 587 additions & 70 deletions

File tree

src/Lean/DocString/Add.lean

Lines changed: 78 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,64 @@ def reportVersoParseFailure
172172
severity := .error
173173
}
174174

175+
open Lean.Doc in
176+
/--
177+
Elaborates already-parsed Verso `blocks` for the specified declaration with interactive features
178+
disabled, reporting any elaboration messages at the current reference. When `fileMap?` is provided,
179+
message positions are interpreted against it.
180+
-/
181+
private def execVersoBlocks
182+
(declName : Name) (binders : Syntax) (blocks : Array Syntax) (fileMap? : Option FileMap) :
183+
TermElabM (Array (Doc.Block ElabInline ElabBlock) × Array (Doc.Part ElabInline ElabBlock Empty)) := do
184+
let msgs ← Core.getAndEmptyMessageLog
185+
let (val, msgs') ←
186+
try
187+
let act := (Doc.elabBlocks (blocks.map (⟨·⟩))).exec declName binders (suggestionMode := .batch)
188+
let val ←
189+
Elab.withEnableInfoTree false <|
190+
match fileMap? with
191+
| some fileMap => withTheReader Core.Context ({· with fileMap }) act
192+
| none => act
193+
pure (val, ← Core.getAndEmptyMessageLog)
194+
finally
195+
Core.setMessageLog msgs
196+
for msg in msgs'.toArray do
197+
logAt (← getRef) msg.data (severity := msg.severity) (isSilent := msg.isSilent)
198+
pure val
199+
200+
open Lean.Doc in
201+
open Parser in
202+
/--
203+
Parses a Verso docstring from its text and elaborates it for the specified declaration. Because the
204+
text carries no source positions, interactive features are disabled and any messages are reported at
205+
the current reference.
206+
207+
`binders` should be the syntax of the parameters to the constant that is being documented, as a null
208+
node that contains a sequence of bracketed binders, or an empty null node when none are available.
209+
-/
210+
def versoDocStringOfText
211+
(declName : Name) (binders : Syntax) (docComment : String) :
212+
TermElabM (Array (Doc.Block ElabInline ElabBlock) × Array (Doc.Part ElabInline ElabBlock Empty)) := do
213+
let env ← getEnv
214+
let ictx : InputContext := .mk docComment (← getFileName)
215+
let text := ictx.fileMap
216+
let pmctx : ParserModuleContext := {
217+
env,
218+
options := ← getOptions,
219+
currNamespace := (← getCurrNamespace),
220+
openDecls := (← getOpenDecls)
221+
}
222+
let s := mkParserState docComment
223+
-- TODO parse one block at a time for error recovery purposes
224+
let s := Doc.Parser.document.run ictx pmctx (getTokenTable env) s
225+
226+
if !s.allErrors.isEmpty then
227+
for (_, _, err) in s.allErrors do
228+
logError err.toString
229+
return (#[], #[])
230+
else
231+
execVersoBlocks declName binders s.stxStack.back.getArgs (fileMap? := some text)
232+
175233
open Lean.Doc in
176234
open Lean.Parser.Command in
177235
/--
@@ -187,9 +245,24 @@ are available, pass `Syntax.missing` or an empty null node.
187245
def versoDocString
188246
(declName : Name) (binders : Syntax) (docComment : TSyntax ``docComment) :
189247
TermElabM (Array (Doc.Block ElabInline ElabBlock) × Array (Doc.Part ElabInline ElabBlock Empty)) := do
190-
if let some stx ← parseVersoDocString docComment then
191-
Doc.elabBlocks (stx.getArgs.map (⟨·⟩)) |>.exec declName binders
192-
else return (#[], #[])
248+
-- A docstring already parsed as Verso, or one re-parsable from its source range, supports
249+
-- interactive features. A macro-generated docstring has neither, so fall back to its text.
250+
let body := docComment.raw[1]
251+
if (body.getPos? (canonicalOnly := true)).isSome then
252+
-- Source positions are available, so re-parse from source for interactive features.
253+
if let some stx ← parseVersoDocString docComment then
254+
Doc.elabBlocks (stx.getArgs.map (⟨·⟩)) |>.exec declName binders
255+
else return (#[], #[])
256+
else if body.isOfKind ``versoCommentBody then
257+
if body[0].isOfKind `Lean.Doc.Syntax.parseFailure then
258+
-- The markup failed to parse, so re-parse its text to report the error.
259+
versoDocStringOfText declName binders body[0][0].getAtomVal
260+
else
261+
-- A docstring parsed as Verso by a macro, with positions stripped.
262+
execVersoBlocks declName binders body[0].getArgs (fileMap? := none)
263+
else
264+
-- A plain-text doc comment without source positions; parse and elaborate from its text.
265+
versoDocStringOfText declName binders docComment.getDocString
193266

194267
open Lean.Doc Parser in
195268
open Lean.Parser.Command in
@@ -204,52 +277,15 @@ def versoModDocString
204277

205278

206279

207-
open Lean.Doc in
208-
open Parser in
209280
/--
210281
Adds a Verso docstring to the specified declaration, which should already be present in the
211282
environment. The docstring is added from a string value, rather than syntax, which means that the
212283
interactive features are disabled.
213284
-/
214285
def versoDocStringFromString
215286
(declName : Name) (docComment : String) :
216-
TermElabM (Array (Doc.Block ElabInline ElabBlock) × Array (Doc.Part ElabInline ElabBlock Empty)) := do
217-
218-
let env ← getEnv
219-
let ictx : InputContext := .mk docComment (← getFileName)
220-
let text := ictx.fileMap
221-
let pmctx : ParserModuleContext := {
222-
env,
223-
options := ← getOptions,
224-
currNamespace := (← getCurrNamespace),
225-
openDecls := (← getOpenDecls)
226-
}
227-
let s := mkParserState docComment
228-
-- TODO parse one block at a time for error recovery purposes
229-
let s := Doc.Parser.document.run ictx pmctx (getTokenTable env) s
230-
231-
if !s.allErrors.isEmpty then
232-
for (pos, _, err) in s.allErrors do
233-
logError err.toString
234-
return (#[], #[])
235-
else
236-
let stx := s.stxStack.back
237-
let stx := stx.getArgs
238-
let msgs ← Core.getAndEmptyMessageLog
239-
let (val, msgs') ←
240-
try
241-
let range? := (← getRef).getRange?
242-
let val ←
243-
Elab.withEnableInfoTree false <| withTheReader Core.Context ({· with fileMap := text}) <|
244-
(Doc.elabBlocks (stx.map (⟨·⟩))).exec declName (mkNullNode #[]) (suggestionMode := .batch)
245-
let msgs' ← Core.getAndEmptyMessageLog
246-
pure (val, msgs')
247-
finally
248-
Core.setMessageLog msgs
249-
-- Adjust messages to show them at the call site
250-
for msg in msgs'.toArray do
251-
logAt (← getRef) msg.data (severity := msg.severity)
252-
pure val
287+
TermElabM (Array (Doc.Block ElabInline ElabBlock) × Array (Doc.Part ElabInline ElabBlock Empty)) :=
288+
versoDocStringOfText declName (mkNullNode #[]) docComment
253289

254290
/--
255291
Adds a Markdown docstring to the environment, validating documentation links.

src/Lean/Elab/DocString.lean

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,12 @@ where
274274
localInstances := localInstances.push {className := c, fvar := .fvar fv}
275275

276276
if let some (some x') := x then
277-
if x'.getId == y then
277+
if x'.getKind == ``hole then
278+
-- A `_` parameter has no name, so it matches no binder. Drop it from the cursor
279+
-- and align the remaining parameters by name; each lifted binder, including
280+
-- captured variables, is introduced under its own name below.
281+
x := none
282+
else if x'.getId == y then
278283
lctx := lctx.mkLocalDecl fv y ty
279284
Meta.withLCtx lctx localInstances <|
280285
addTermInfo' x' (.fvar fv) (lctx? := some lctx) (expectedType? := ty)
@@ -301,7 +306,10 @@ where
301306
| ``instBinder =>
302307
let x := binderStx[1][0]
303308
if x.isMissing then pure #[none] else pure #[some x]
304-
| _ => throwErrorAt binderStx "Couldn't interpret binder {binderStx}"
309+
| k =>
310+
-- A parameter bound by an unbracketed identifier or `_`, as in `def f x` or `where go _`.
311+
if k == identKind || k == ``hole then pure #[some binderStx]
312+
else throwErrorAt binderStx "Couldn't interpret binder {binderStx}"
305313
getNames (ids : Syntax) : TermElabM (Array (Option Syntax)) :=
306314
ids.getArgs.mapM fun x =>
307315
if x.getKind == identKind || x.getKind == ``hole then
@@ -1209,7 +1217,11 @@ unsafe def roleExpandersForUnsafe (roleName : Ident) :
12091217
let builtins := (← builtinDocRoles.get).get? x |>.getD #[]
12101218
return (← names.mapM (fun x => do return (x, ← evalConst _ x))) ++ builtins
12111219
else
1212-
let x := roleName.getId
1220+
-- Builtin roles are not necessarily in the environment at a
1221+
-- quotation site, so they aren't in the preresolved list. They
1222+
-- must also be looked up by their plain name, erasing any macro
1223+
-- scopes a quotation introduced.
1224+
let x := roleName.getId.eraseMacroScopes
12131225
let hasBuiltin ← resolveBuiltinDocName (← builtinDocRoles.get) x
12141226
return hasBuiltin.toArray.flatten
12151227

@@ -1228,7 +1240,7 @@ unsafe def codeBlockExpandersForUnsafe (codeBlockName : Ident) :
12281240
let names' := (← builtinDocCodeBlocks.get).get? x |>.getD #[]
12291241
return (← names.mapM (fun x => do return (x, ← evalConst _ x))) ++ names'
12301242
else
1231-
let x := codeBlockName.getId
1243+
let x := codeBlockName.getId.eraseMacroScopes
12321244
let hasBuiltin ← resolveBuiltinDocName (← builtinDocCodeBlocks.get) x
12331245
return hasBuiltin.toArray.flatten
12341246

@@ -1247,7 +1259,7 @@ unsafe def directiveExpandersForUnsafe (directiveName : Ident) :
12471259
let names' := (← builtinDocDirectives.get).get? x |>.getD #[]
12481260
return (← names.mapM (fun x => do return (x, ← evalConst _ x))) ++ names'
12491261
else
1250-
let x := directiveName.getId
1262+
let x := directiveName.getId.eraseMacroScopes
12511263
let hasBuiltin ← resolveBuiltinDocName (← builtinDocDirectives.get) x
12521264
return hasBuiltin.toArray.flatten
12531265

@@ -1265,7 +1277,7 @@ unsafe def commandExpandersForUnsafe (commandName : Ident) :
12651277
let names' := (← builtinDocCommands.get).get? x |>.getD #[]
12661278
return (← names.mapM (fun x => do return (x, ← evalConst _ x))) ++ names'
12671279
else
1268-
let x := commandName.getId
1280+
let x := commandName.getId.eraseMacroScopes
12691281
let hasBuiltin :=
12701282
(← builtinDocCommands.get).get? x <|> (← builtinDocCommands.get).get? (`Lean.Doc ++ x)
12711283
return hasBuiltin.toArray.flatten

src/Lean/Elab/DocString/Builtin.lean

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,20 @@ structure Data.LeanBlock where
906906
deriving TypeName
907907

908908

909+
/--
910+
Runs `act` with info trees enabled, returning its result together with the info trees it produced.
911+
The document's prior info trees are restored afterward; the trees from `act` are added to the
912+
document only when its info trees were already enabled.
913+
-/
914+
def withHighlightingInfoTrees (act : DocM α) : DocM (α × PersistentArray InfoTree) := do
915+
let enabled := (← getInfoState).enabled
916+
let outer ← getResetInfoTrees
917+
let a ← withEnableInfoTree true <| withSaveInfoContext act
918+
let trees ← getInfoTrees
919+
modifyInfoState fun st => { st with trees := if enabled then outer ++ trees else outer }
920+
return (a, trees)
921+
922+
909923
/--
910924
Elaborates a sequence of Lean commands as examples.
911925
@@ -920,19 +934,24 @@ The flags `error` and `warning` indicate that an error or warning is expected in
920934
@[builtin_doc_code_block]
921935
def lean (name : Option Ident := none) (error warning : flag false) («show» : flag true) (code : StrLit) :
922936
DocM (Block ElabInline ElabBlock) := do
923-
let text ← getFileMap
924937
let env ← getEnv
925-
let p := andthenFn whitespace (categoryParserFnImpl `command)
926-
-- TODO fallback for non-original syntax
927-
let pos := code.raw.getPos? true |>.get!
928-
let endPos := code.raw.getTailPos? true |>.get!
929-
let endPos := if endPos ≤ text.source.rawEndPos then endPos else text.source.rawEndPos
930-
let ictx :=
931-
mkInputContext text.source (← getFileName)
932-
(endPos := endPos) (endPos_valid := by simp only [endPos]; split <;> simp [*])
938+
-- Parse from the file when source positions are available, otherwise from the literal's own
939+
-- contents (e.g. for a docstring that came from a macro).
940+
let (text, ictx, pos) ←
941+
if let some pos := code.raw.getPos? true then
942+
let text ← getFileMap
943+
let endPos := code.raw.getTailPos? true |>.getD text.source.rawEndPos
944+
let endPos := if endPos ≤ text.source.rawEndPos then endPos else text.source.rawEndPos
945+
pure (text,
946+
mkInputContext text.source (← getFileName)
947+
(endPos := endPos) (endPos_valid := by simp only [endPos]; split <;> simp [*]),
948+
pos)
949+
else
950+
let ictx := mkInputContext code.getString (← getFileName)
951+
pure (ictx.fileMap, ictx, (0 : String.Pos.Raw))
933952
let cctx : Command.Context := {fileName := ← getFileName, fileMap := text, snap? := none, cancelTk? := none}
934953
let scopes := (← get).scopes
935-
let (cmds, cmdState, trees) ← withSaveInfoContext do
954+
let ((cmds, cmdState), trees) ← withHighlightingInfoTrees do
936955
let mut cmdState : Command.State := { env, maxRecDepth := ← MonadRecDepth.getMaxRecDepth, scopes }
937956
let mut pstate : Parser.ModuleParserState := {
938957
pos
@@ -954,8 +973,7 @@ def lean (name : Option Ident := none) (error warning : flag false) («show» :
954973

955974
for t in cmdState.infoState.trees do
956975
pushInfoTree t
957-
let trees := (← getInfoTrees)
958-
pure (cmds, cmdState, trees)
976+
pure (cmds, cmdState)
959977

960978
let mut output := #[]
961979
for msg in cmdState.messages.toArray do
@@ -1098,7 +1116,7 @@ Treats the provided term as Lean syntax in the documentation's scope.
10981116
def leanRole (type : Option StrLit := none) (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
10991117
let s ← onlyCode xs
11001118
let stx ← parseStrLit leanTermContents s
1101-
withSaveInfoContext do
1119+
let (_, trees) ← withHighlightingInfoTrees do
11021120
let ty? ←
11031121
withoutErrToSorry <| do
11041122
if stx[1][1].isMissing then -- no colon
@@ -1111,7 +1129,6 @@ def leanRole (type : Option StrLit := none) (xs : TSyntaxArray `inline) : DocM (
11111129
logErrorAt t m!"Ignoring `{s.getString}` in favor of type provided after colon"
11121130
some <$> elabType stx[1][1]
11131131
withoutErrToSorry <| discard <| elabExtraTerm stx[0] ty?
1114-
let trees := (← getInfoTrees)
11151132
if h : trees.size > 0 then
11161133
let tm := Data.LeanTerm.mk (← highlightSyntax trees stx)
11171134
return .other {val := .mk tm} #[.code s.getString]
@@ -1125,15 +1142,14 @@ Treats the provided term as Lean syntax in the documentation's scope.
11251142
@[builtin_doc_code_block]
11261143
def leanTerm (code : StrLit) : DocM (Block ElabInline ElabBlock) := do
11271144
let stx ← parseStrLit leanTermContents code
1128-
withSaveInfoContext do
1145+
let (_, trees) ← withHighlightingInfoTrees do
11291146
let ty? ←
11301147
withoutErrToSorry <|
11311148
if stx[1][1].isMissing then -- no colon
11321149
pure none
11331150
else -- type after colon
11341151
some <$> elabType stx[1][1]
11351152
withoutErrToSorry <| discard <| elabExtraTerm stx[0] ty?
1136-
let trees := (← getInfoTrees)
11371153
if h : trees.size > 0 then
11381154
let tm := Data.LeanTerm.mk (← highlightSyntax trees stx)
11391155
return .other {val := .mk tm} #[.code code.getString]
@@ -1249,7 +1265,7 @@ def assert' (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
12491265
let lhsStx ← parseStrLit assertTermContents lhsCode
12501266
let rhsStx ← parseStrLit assertTermContents rhsCode
12511267
let tyStx? ← tyCode?.mapM (parseStrLit assertTermContents)
1252-
withSaveInfoContext do
1268+
let (_, trees) ← withHighlightingInfoTrees do
12531269
let ty? ← withoutErrToSorry <|
12541270
match tyStx? with
12551271
| some tyStx => some <$> elabType tyStx
@@ -1260,7 +1276,6 @@ def assert' (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
12601276
throwError m!"Expected {lhs} = {rhs}, which is {← Meta.whnf lhs} = {← Meta.whnf rhs}, reducing to {← Meta.reduceAll lhs} = {← Meta.reduceAll rhs} but they are not equal."
12611277
let str := lhsCode.getString ++ " = " ++ rhsCode.getString ++
12621278
(tyCode?.map (" : " ++ ·.getString) |>.getD "")
1263-
let trees ← getInfoTrees
12641279
if trees.size > 0 then
12651280
let mut code := (← highlightSyntax trees lhsStx) ++ " = " ++ (← highlightSyntax trees rhsStx)
12661281
if let some tyStx := tyStx? then
@@ -1277,14 +1292,13 @@ Asserts that an equality holds.
12771292
def assert (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
12781293
let s ← onlyCode xs
12791294
let stx ← parseStrLit termParser.fn s
1280-
withSaveInfoContext do
1295+
let (_, trees) ← withHighlightingInfoTrees do
12811296
let ty ← withoutErrToSorry <| elabType stx
12821297
match_expr (← Meta.whnf ty) with
12831298
| Eq _ lhs rhs =>
12841299
unless (← Meta.isDefEq lhs rhs) do
12851300
throwErrorAt stx m!"Expected {lhs} = {rhs}, but they are not definitionally equal"
12861301
| _ => throwErrorAt stx m!"Expected equality type"
1287-
let trees ← getInfoTrees
12881302
if trees.size > 0 then
12891303
let tm := Data.LeanTerm.mk (← highlightSyntax trees stx)
12901304
return .other {val := .mk tm} #[.code s.getString]

src/Lean/Elab/DocString/Builtin/Parsing.lean

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,24 @@ private def strLitRange [Monad m] [MonadFileMap m] (s : StrLit) : m Lean.Syntax.
2323
variable [Monad m] [MonadFileMap m] [MonadEnv m]
2424
variable [MonadError m] [AddMessageContext m] [MonadLog m] [MonadOptions m]
2525

26+
/--
27+
Parses a string literal's contents directly from its decoded value rather than from the source file.
28+
This is used when the literal has no source position, as in a docstring that came from a macro.
29+
-/
30+
private def parseFromContents (p : ParserFn) (contents : String) : m Syntax := do
31+
let env ← getEnv
32+
let ictx := mkInputContext contents (← getFileName)
33+
let s := p.run ictx { env, options := ← getOptions } (getTokenTable env) (mkParserState contents)
34+
if !s.allErrors.isEmpty then
35+
throwError (s.toErrorMsg ictx)
36+
else if ictx.atEnd s.pos then
37+
pure s.stxStack.back
38+
else
39+
throwError ((s.mkError "end of input").toErrorMsg ictx)
40+
2641
def parseStrLit (p : ParserFn) (s : StrLit) : m Syntax := do
42+
if (s.raw.getPos? (canonicalOnly := true)).isNone then
43+
return ← parseFromContents p s.getString
2744
let text ← getFileMap
2845
let env ← getEnv
2946
let ⟨pos, endPos⟩ ← strLitRange s
@@ -43,6 +60,8 @@ def parseStrLit (p : ParserFn) (s : StrLit) : m Syntax := do
4360
throwError ((s.mkError "end of input").toErrorMsg ictx)
4461

4562
def parseQuotedStrLit (p : ParserFn) (strLit : StrLit) : m Syntax := do
63+
if (strLit.raw.getPos? (canonicalOnly := true)).isNone then
64+
return ← parseFromContents p strLit.getString
4665
let text ← getFileMap
4766
let env ← getEnv
4867
let ⟨pos, _⟩ ← strLitRange strLit
@@ -100,6 +119,18 @@ where
100119
return n
101120

102121
def parseStrLit' (p : ParserFn) (s : StrLit) : m (Syntax × Bool) := do
122+
if (s.raw.getPos? (canonicalOnly := true)).isNone then
123+
let contents := s.getString
124+
let env ← getEnv
125+
let ictx := mkInputContext contents (← getFileName)
126+
let st := p.run ictx { env, options := ← getOptions } (getTokenTable env) (mkParserState contents)
127+
let err ←
128+
if !st.allErrors.isEmpty then
129+
logError (st.toErrorMsg ictx); pure true
130+
else if !ictx.atEnd st.pos then
131+
logError ((st.mkError "end of input").toErrorMsg ictx); pure true
132+
else pure false
133+
return (st.stxStack.back, err)
103134
let text ← getFileMap
104135
let env ← getEnv
105136
let endPos := s.raw.getTailPos? true |>.get!

0 commit comments

Comments
 (0)