Skip to content

Commit 00b74e0

Browse files
feat: docstring role for module names, plus improved suggestions (#10533)
This PR adds a docstring role for module names, called `module`. It also improves the suggestions provided for code elements, making them more relevant and proposing `lit`.
1 parent 90db9ef commit 00b74e0

File tree

4 files changed

+156
-29
lines changed

4 files changed

+156
-29
lines changed

src/Lean/Elab/DocString.lean

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,7 +1198,10 @@ private def sortSuggestions (ss : Array Meta.Hint.Suggestion) : Array Meta.Hint.
11981198
ss.qsort (cmp ·.suggestion ·.suggestion)
11991199

12001200
open Diff in
1201-
private def mkSuggestion (ref : Syntax) (hintTitle : MessageData) (newStrings : Array (String × Option String × Option String)) : DocM MessageData := do
1201+
private def mkSuggestion
1202+
(ref : Syntax) (hintTitle : MessageData)
1203+
(newStrings : Array (String × Option String × Option String)) :
1204+
DocM MessageData := do
12021205
match (← read).suggestionMode with
12031206
| .interactive =>
12041207
hintTitle.hint (newStrings.map fun (s, preInfo?, postInfo?) => { suggestion := s, preInfo?, postInfo? }) (ref? := some ref)
@@ -1268,6 +1271,11 @@ public partial def elabInline (stx : TSyntax `inline) : DocM (Inline ElabInline)
12681271
snd.snd := moreInfo.map withSpace
12691272
}
12701273
let ss := ss.qsort (fun x y => x.1 < y.1)
1274+
let litSuggestion :=
1275+
( "{lit}" ++ str,
1276+
some "Use the `lit` role:\n",
1277+
some "\nto mark the code as literal text and disable suggestions" )
1278+
let ss := ss.push litSuggestion
12711279
let hint ← mkSuggestion stx m!"Insert a role to document it:" ss
12721280
logWarning m!"Code element could be more specific.{hint}"
12731281
return .code s.getString
@@ -1293,7 +1301,7 @@ public partial def elabInline (stx : TSyntax `inline) : DocM (Inline ElabInline)
12931301
continue
12941302
else throw e
12951303
| e => throw e
1296-
throwErrorAt name "No expander for `{name}`"
1304+
throwErrorAt name "Unkown role `{name}`"
12971305
| other =>
12981306
throwErrorAt other "Unsupported syntax {other}"
12991307
where
@@ -1359,7 +1367,7 @@ public partial def elabBlock (stx : TSyntax `block) : DocM (Block ElabInline Ela
13591367
continue
13601368
else throw e
13611369
| e => throw e
1362-
throwErrorAt name "No directive expander for `{name}`"
1370+
throwErrorAt name "Unknown directive `{name}`"
13631371
| `(block| ```%$opener | $s ```) =>
13641372
if doc.verso.suggestions.get (← getOptions) then
13651373
if let some ⟨b, e⟩ := opener.getRange? then
@@ -1402,7 +1410,7 @@ public partial def elabBlock (stx : TSyntax `block) : DocM (Block ElabInline Ela
14021410
continue
14031411
else throw e
14041412
| e => throw e
1405-
throwErrorAt name "No code block expander for `{name}`"
1413+
throwErrorAt name "Unknown code block `{name}`"
14061414
| `(block| command{$name $args*}) =>
14071415
let expanders ← commandExpandersFor name
14081416
for (exName, ex) in expanders do
@@ -1421,7 +1429,7 @@ public partial def elabBlock (stx : TSyntax `block) : DocM (Block ElabInline Ela
14211429
continue
14221430
else throw e
14231431
| e => throw e
1424-
throwErrorAt name "No document command elaborator for `{name}`"
1432+
throwErrorAt name "Unknown document command `{name}`"
14251433
| `(block|%%%$_*%%%) =>
14261434
let h ←
14271435
if stx.raw.getRange?.isSome then m!"Remove it".hint #[""] (ref? := stx)

src/Lean/Elab/DocString/Builtin.lean

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ structure Data.Syntax where
106106
stx : Lean.Syntax
107107
deriving TypeName
108108

109+
/-- The code represents a module name. -/
110+
structure Data.ModuleName where
111+
/-- The module. -/
112+
«module» : Name
113+
deriving TypeName
114+
115+
109116
private def onlyCode [Monad m] [MonadError m] (xs : TSyntaxArray `inline) : m StrLit := do
110117
if h : xs.size = 1 then
111118
match xs[0] with
@@ -177,6 +184,61 @@ def name (full : Option Ident := none) (scope : DocScope := .local) (xs : TSynta
177184
}
178185
return .other { name := ``PostponedCheck, val := .mk val } #[.code s.getString]
179186

187+
private def similarNames (x : Name) (xs : Array Name) : Array Name := Id.run do
188+
let s := x.toString
189+
let mut threshold := if s.length < 5 then 1 else if s.length < 8 then 2 else 3
190+
let mut candidates := #[]
191+
for x in xs do
192+
if let some d ← levenshtein s x.toString threshold then
193+
if d < threshold then threshold := d
194+
if d ≤ threshold then candidates := candidates.push (x, d)
195+
-- Only keep the smallest distance
196+
return candidates.filterMap fun (x, d) => do
197+
guard (d ≤ threshold)
198+
pure x
199+
200+
/--
201+
Displays a name, without attempting to elaborate implicit arguments.
202+
-/
203+
@[builtin_doc_role]
204+
def module (checked : flag true) (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
205+
let s ← onlyCode xs
206+
let x := s.getString.toName
207+
let n := mkIdentFrom' s x
208+
if checked then
209+
let env ← getEnv
210+
if x ∉ env.header.moduleNames then
211+
let ss := similarNames x env.header.moduleNames
212+
let ref ← getRef
213+
let unchecked : Option Meta.Hint.Suggestion ←
214+
match ref with
215+
| `(inline|role{$x +checked}%$tk2[$_]) =>
216+
let some b := x.raw.getTailPos?
217+
| pure none
218+
let some e := tk2.getPos?
219+
| pure none
220+
pure <| some {
221+
span? := some (Syntax.mkStrLit ((← getFileMap).source.extract b e) (info := .synthetic b e)),
222+
previewSpan? := some ref,
223+
suggestion := "" : Meta.Hint.Suggestion
224+
}
225+
| `(inline|role{$_}%$tk2[$_]) =>
226+
pure <| some {
227+
span? := some tk2
228+
previewSpan? := some ref,
229+
suggestion := " -checked}": Meta.Hint.Suggestion
230+
}
231+
| _ => pure none
232+
let ss := unchecked.toArray ++ ss.map fun x =>
233+
{ suggestion := x.toString, span? := some n, previewSpan? := some ref }
234+
let h ←
235+
if ss.isEmpty then pure m!""
236+
else m!"Either disable the existence check or use an imported module:".hint ss (ref? := some ref)
237+
logErrorAt n m!"Module is not transitively imported by the current module.{h}"
238+
239+
return .other {name := ``Data.ModuleName, val := .mk (Data.ModuleName.mk x)} #[.code s.getString]
240+
241+
180242
private def introduceAntiquotes (stx : Syntax) : DocM Unit :=
181243
discard <| stx.rewriteBottomUpM fun stx' =>
182244
match stx' with
@@ -439,15 +501,15 @@ def syntaxCat (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
439501

440502
private partial def onlyIdent : Syntax → Bool
441503
| .node _ _ args =>
442-
let nonEmpty := args.filter isEmpty
504+
let nonEmpty := args.filter (!isEmpty ·)
443505
if h : nonEmpty.size = 1 then onlyIdent nonEmpty[0]
444506
else false
445507
| .ident .. => true
446508
| _ => false
447509
where
448510
isEmpty : Syntax → Bool
449511
| .node _ _ xs =>
450-
xs.size = 0 || xs.all isEmpty
512+
xs.all isEmpty
451513
| _ => false
452514

453515
/--
@@ -884,7 +946,7 @@ def suggestName (code : StrLit) : DocM (Array CodeSuggestion) := do
884946
| _ =>
885947
if let some (_, []) := (← resolveLocalName stx.getId) then
886948
suggestions := suggestions.push <| .mk ``name none none
887-
else
949+
else if stx.getId.components.length == 1 then
888950
suggestions := suggestions.push <| .mk ``given none none
889951
return suggestions
890952

@@ -912,8 +974,9 @@ def suggestLean (code : StrLit) : DocM (Array CodeSuggestion) := do
912974
withoutErrToSorry <|
913975
if stx[1][1].isMissing then pure none
914976
else some <$> elabType stx[1][1]
915-
discard <| withoutErrToSorry <| elabTerm stx[0] ty?
977+
let tm ← withoutErrToSorry <| elabTerm stx[0] ty?
916978
return #[.mk ``lean none none]
979+
917980
catch | _ => return #[]
918981

919982
/--
@@ -1040,3 +1103,15 @@ def suggestSyntax (code : StrLit) : DocM (Array CodeSuggestion) := do
10401103

10411104
candidates.mapM fun cat => do
10421105
return .mk ``«syntax» (some s!"{cat}") none
1106+
1107+
/--
1108+
Suggests the `module` role, if applicable.
1109+
-/
1110+
@[builtin_doc_code_suggestions]
1111+
def suggestModule (code : StrLit) : DocM (Array CodeSuggestion) := do
1112+
let env ← getEnv
1113+
let moduleNames := env.header.moduleNames
1114+
let s := code.getString
1115+
if moduleNames.any (·.toString == s) then
1116+
return #[.mk ``module none none]
1117+
else return #[]

stage0/src/stdlib_flags.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "util/options.h"
22

3+
// CI, please update stage0
4+
35
namespace lean {
46
options get_default_options() {
57
options opts;

tests/lean/run/versoDocs.lean

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -368,37 +368,26 @@ Hint: Insert a role to document it:
368368
• {̲a̲t̲t̲r̲}̲`instance`
369369
• {̲k̲w̲ ̲(̲o̲f̲ ̲:̲=̲ ̲L̲e̲a̲n̲.̲P̲a̲r̲s̲e̲r̲.̲A̲t̲t̲r̲.̲i̲n̲s̲t̲a̲n̲c̲e̲)̲}̲`instance` (in `attr`)
370370
• {̲s̲y̲n̲t̲a̲x̲ ̲a̲t̲t̲r̲}̲`instance`
371+
• Use the `lit` role:
372+
{̲l̲i̲t̲}̲`instance`
373+
to mark the code as literal text and disable suggestions
371374
---
372375
warning: Code element could be more specific.
373376
374377
Hint: Insert a role to document it:
375378
• {̲a̲t̲t̲r̲}̲`term_elab`
376379
• {̲g̲i̲v̲e̲n̲}̲`term_elab`
377-
• {̲l̲e̲a̲n̲}̲`term_elab`
378-
• {̲s̲y̲n̲t̲a̲x̲ ̲a̲t̲t̲r̲}̲`term_elab`
379-
• {̲s̲y̲n̲t̲a̲x̲ ̲d̲o̲E̲l̲e̲m̲}̲`term_elab`
380-
• {̲s̲y̲n̲t̲a̲x̲ ̲m̲c̲a̲s̲e̲s̲P̲a̲t̲}̲`term_elab`
381-
• {̲s̲y̲n̲t̲a̲x̲ ̲m̲i̲n̲t̲r̲o̲P̲a̲t̲}̲`term_elab`
382-
• {̲s̲y̲n̲t̲a̲x̲ ̲m̲r̲e̲f̲i̲n̲e̲P̲a̲t̲}̲`term_elab`
383-
• {̲s̲y̲n̲t̲a̲x̲ ̲m̲r̲e̲v̲e̲r̲t̲P̲a̲t̲}̲`term_elab`
384-
• {̲s̲y̲n̲t̲a̲x̲ ̲r̲c̲a̲s̲e̲s̲P̲a̲t̲}̲`term_elab`
385-
• {̲s̲y̲n̲t̲a̲x̲ ̲r̲i̲n̲t̲r̲o̲P̲a̲t̲}̲`term_elab`
386-
• {̲s̲y̲n̲t̲a̲x̲ ̲s̲t̲x̲}̲`term_elab`
380+
• Use the `lit` role:
381+
{̲l̲i̲t̲}̲`term_elab`
382+
to mark the code as literal text and disable suggestions
387383
---
388384
warning: Code element could be more specific.
389385
390386
Hint: Insert a role to document it:
391387
• {̲g̲i̲v̲e̲n̲}̲`instantiation`
392-
• {̲l̲e̲a̲n̲}̲`instantiation`
393-
• {̲s̲y̲n̲t̲a̲x̲ ̲a̲t̲t̲r̲}̲`instantiation`
394-
• {̲s̲y̲n̲t̲a̲x̲ ̲d̲o̲E̲l̲e̲m̲}̲`instantiation`
395-
• {̲s̲y̲n̲t̲a̲x̲ ̲m̲c̲a̲s̲e̲s̲P̲a̲t̲}̲`instantiation`
396-
• {̲s̲y̲n̲t̲a̲x̲ ̲m̲i̲n̲t̲r̲o̲P̲a̲t̲}̲`instantiation`
397-
• {̲s̲y̲n̲t̲a̲x̲ ̲m̲r̲e̲f̲i̲n̲e̲P̲a̲t̲}̲`instantiation`
398-
• {̲s̲y̲n̲t̲a̲x̲ ̲m̲r̲e̲v̲e̲r̲t̲P̲a̲t̲}̲`instantiation`
399-
• {̲s̲y̲n̲t̲a̲x̲ ̲r̲c̲a̲s̲e̲s̲P̲a̲t̲}̲`instantiation`
400-
• {̲s̲y̲n̲t̲a̲x̲ ̲r̲i̲n̲t̲r̲o̲P̲a̲t̲}̲`instantiation`
401-
• {̲s̲y̲n̲t̲a̲x̲ ̲s̲t̲x̲}̲`instantiation`
388+
• Use the `lit` role:
389+
{̲l̲i̲t̲}̲`instantiation`
390+
to mark the code as literal text and disable suggestions
402391
-/
403392
#guard_msgs in
404393
/--
@@ -408,6 +397,59 @@ This is not an attribute: `instantiation`
408397
-/
409398
def attrSuggestionTest := ()
410399

400+
/--
401+
error: Module is not transitively imported by the current module.
402+
403+
Hint: Either disable the existence check or use an imported module:
404+
{module ̲-̲c̲h̲e̲c̲k̲e̲d̲}`NonExistent`
405+
---
406+
error: Module is not transitively imported by the current module.
407+
408+
Hint: Either disable the existence check or use an imported module:
409+
• {module ̲-̲c̲h̲e̲c̲k̲e̲d̲}`Laen.Data.Jsn`
410+
• {module}`L̵a̵e̵n̵.̵D̵a̵t̵a̵.̵J̵s̵n̵L̲e̲a̲n̲.̲D̲a̲t̲a̲.̲J̲s̲o̲n̲`
411+
---
412+
error: Module is not transitively imported by the current module.
413+
414+
Hint: Either disable the existence check or use an imported module:
415+
• {module ̲-̲c̲h̲e̲c̲k̲e̲d̲}`Lean.Data.jso`
416+
• {module}`L̵e̵a̵n̵.̵D̵a̵t̵a̵.̵j̵s̵o̵L̲e̲a̲n̲.̲D̲a̲t̲a̲.̲J̲s̲o̲n̲`
417+
• {module}`L̵e̵a̵n̵.̵D̵a̵t̵a̵.̵j̵s̵o̵L̲e̲a̲n̲.̲D̲a̲t̲a̲.̲L̲s̲p̲`
418+
-/
419+
#guard_msgs in
420+
/--
421+
Error, no suggestions:
422+
{module}`NonExistent`
423+
424+
Error, one suggestions:
425+
{module}`Laen.Data.Jsn`
426+
427+
No error:
428+
{module -checked}`NonExistent`
429+
430+
Error, two suggestions:
431+
{module}`Lean.Data.jso`
432+
433+
No error:
434+
{module}`Lean.Data.Json`
435+
-/
436+
def talksAboutModules := ()
437+
438+
/--
439+
warning: Code element could be more specific.
440+
441+
Hint: Insert a role to document it:
442+
• {̲m̲o̲d̲u̲l̲e̲}̲`Lean.Data.Json.Basic`
443+
• Use the `lit` role:
444+
{̲l̲i̲t̲}̲`Lean.Data.Json.Basic`
445+
to mark the code as literal text and disable suggestions
446+
-/
447+
#guard_msgs in
448+
/--
449+
`Lean.Data.Json.Basic`
450+
-/
451+
def moduleSuggestionTest := ()
452+
411453
/-
412454
TODO test:
413455
* Scope rules for all operators

0 commit comments

Comments
 (0)