Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: recommended_spelling command #6869

Merged
merged 6 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Lean/DocString.lean
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Authors: Leonardo de Moura
prelude
import Lean.DocString.Extension
import Lean.Parser.Tactic.Doc
import Lean.Parser.Term.Doc

set_option linter.missingDocs true

Expand All @@ -15,6 +16,7 @@ set_option linter.missingDocs true

namespace Lean
open Lean.Parser.Tactic.Doc
open Lean.Parser.Term.Doc

/--
Finds the docstring for a name, taking tactic alternate forms and documentation extensions into
Expand All @@ -26,4 +28,5 @@ including extensions.
def findDocString? (env : Environment) (declName : Name) (includeBuiltin := true) : IO (Option String) := do
let declName := alternativeOfTactic env declName |>.getD declName
let exts := getTacticExtensionString env declName
return (← findSimpleDocString? env declName (includeBuiltin := includeBuiltin)).map (· ++ exts)
let spellings := getRecommendedSpellingString env declName
return (← findSimpleDocString? env declName (includeBuiltin := includeBuiltin)).map (· ++ exts ++ spellings)
1 change: 1 addition & 0 deletions src/Lean/Elab.lean
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ import Lean.Elab.CheckTactic
import Lean.Elab.MatchExpr
import Lean.Elab.Tactic.Doc
import Lean.Elab.Time
import Lean.Elab.RecommendedSpelling
34 changes: 34 additions & 0 deletions src/Lean/Elab/RecommendedSpelling.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Markus Himmel
-/
prelude
import Lean.Parser.Term.Doc
import Lean.Parser.Command
import Lean.Elab.Command

namespace Lean.Elab.Term.Doc
open Lean.Parser.Term.Doc
open Lean.Elab.Command
open Lean.Parser.Command

@[builtin_command_elab «recommended_spelling»] def elabRecommendedSpelling : CommandElab
| `(«recommended_spelling»|$[$docs:docComment]? recommended_spelling $spelling:str for $«notation»:str in [$[$decls:ident],*]) => do
let declNames ← liftTermElabM <| decls.mapM (fun decl => realizeGlobalConstNoOverloadWithInfo decl)
let recommendedSpelling : RecommendedSpelling := {
«notation» := «notation».getString
recommendedSpelling := spelling.getString
additionalInformation? := docs.map (·.getDocString)
}
modifyEnv (addRecommendedSpelling · recommendedSpelling declNames)
| _ => throwError "Malformed recommended spelling command"

/-- Returns an array containing all recommended spellings. -/
def allRecommendedSpellings : MetaM (Array RecommendedSpelling) := do
let all := recommendedSpellingExt.toEnvExtension.getState (← getEnv)
|>.importedEntries
|>.push (recommendedSpellingExt.exportEntriesFn (recommendedSpellingExt.getState (← getEnv)))
return all.flatMap id

end Lean.Elab.Term.Doc
45 changes: 43 additions & 2 deletions src/Lean/Parser/Command.lean
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ Documentation can only be added to declarations in the same module.
docComment >> "add_decl_doc " >> ident

/--
Register a tactic tag, saving its user-facing name and docstring.
Registers a tactic tag, saving its user-facing name and docstring.

Tactic tags can be used by documentation generation tools to classify related tactics.
-/
Expand All @@ -745,7 +745,7 @@ Tactic tags can be used by documentation generation tools to classify related ta
"register_tactic_tag " >> ident >> strLit

/--
Add more documentation as an extension of the documentation for a given tactic.
Adds more documentation as an extension of the documentation for a given tactic.

The extended documentation is placed in the command's docstring. It is shown as part of a bulleted
list, so it should be brief.
Expand All @@ -754,6 +754,47 @@ list, so it should be brief.
optional (docComment >> ppLine) >>
"tactic_extension " >> ident

/--
Documents a recommended spelling for a notation in identifiers.

Theorems should generally be systematically named after their statement, rather than creatively.
Non-identifier notations should be referred to consistently by their recommended spelling.

TwoFX marked this conversation as resolved.
Show resolved Hide resolved
```
/-- some additional info -/
recommended_spelling "and" for "∧" in [And, «term_∧_»]
```

will do the following:
* Adds the sentence "The recommended spelling of `∧` in identifiers is `and` (some additional info)."
to the end of the docstring for `And` and for `∧`. If the additional info is more than a single
line, it will be placed below the sentence instead of in parentheses.
* Registers this information in an environment extension, so that it will later be possible to
generate a table with all recommended spellings.

You can add attach the recommended spelling to as many declarations as you want. It is recommended
to attach the recommended spelling to all relevant parsers as well as the declaration the parsers
TwoFX marked this conversation as resolved.
Show resolved Hide resolved
refer to (if such a declaration exists). Note that the `inherit_doc` attribute does *not* copy
recommended spellings, so even though the parser for `∧` uses `@[inherit_doc And]`, we have to
TwoFX marked this conversation as resolved.
Show resolved Hide resolved
attach the recommended spelling to both `And` and `«term_∧_»`.

The `syntax`, `macro`, `elab` and `notation` commands accept a `(name := parserName)` option to
assign a name to the created parser so that you do not have to guess the automatically generated
name. The `synax`, `macro` and `elab` commands can be hovered to see the name of the parser.

For complex notations which enclose identifiers, the convention is to use example identifiers rather
than other placeholders. This is an example following the convention:
```
recommended_spelling "singleton" for "[a]" in [List.cons, «term[_]»]
```
Using `[·]` or `[⋯]` or `[…]` instead of `[a]` would be against the convention. When attaching a
recommended spelling to a notation whose docstring already has an example, try to reuse the
identifier names chosen in the docstring for consistency.
-/
@[builtin_command_parser] def «recommended_spelling» := leading_parser
optional (docComment >> ppLine) >>
"recommended_spelling " >> strLit >> " for " >> strLit >> " in " >>
"[" >> sepBy1 ident ", " >> "]"

/--
This is an auxiliary command for generation constructor injectivity theorems for
Expand Down
1 change: 1 addition & 0 deletions src/Lean/Parser/Term.lean
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Authors: Leonardo de Moura, Sebastian Ullrich, Mario Carneiro
prelude
import Lean.Parser.Attr
import Lean.Parser.Level
import Lean.Parser.Term.Doc

namespace Lean
namespace Parser
Expand Down
83 changes: 83 additions & 0 deletions src/Lean/Parser/Term/Doc.lean
Original file line number Diff line number Diff line change
@@ -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.
Authors: Markus Himmel
-/
prelude
import Lean.Parser.Extension

/-! Environment extension to register preferred spellings of notations in identifiers. -/

set_option linter.missingDocs true

namespace Lean.Parser.Term.Doc

/-- Information about how to spell a certain notation for an identifier in declaration names. -/
structure RecommendedSpelling where
/-- The notation in question. -/
«notation» : String
/-- The recommended spelling of the notation in identifiers. -/
recommendedSpelling : String
/-- Additional information. -/
additionalInformation? : Option String

/--
Recommended spellings for notations, stored in a way so that the recommended spellings for a given
declaration are easily accessible.
-/
builtin_initialize recommendedSpellingByNameExt
: PersistentEnvExtension (Name × Array RecommendedSpelling) (RecommendedSpelling × Array Name)
(NameMap (Array RecommendedSpelling)) ←
registerPersistentEnvExtension {
mkInitial := pure {},
addImportedFn := fun _ => pure {},
addEntryFn := fun es (rec, xs) => xs.foldl (init := es) fun es x => es.insert x (es.findD x #[] |>.push rec),
exportEntriesFn := fun es =>
es.fold (fun a src tgt => a.push (src, tgt)) #[] |>.qsort (Name.quickLt ·.1 ·.1)
}

/-- Recommended spellings for notations, stored in such a way that it is easy to generate a table
containing every recommended spelling exactly once. -/
builtin_initialize recommendedSpellingExt
: PersistentEnvExtension RecommendedSpelling RecommendedSpelling (Array RecommendedSpelling) ←
registerPersistentEnvExtension {
mkInitial := pure {},
addImportedFn := fun _ => pure {},
addEntryFn := Array.push,
exportEntriesFn := id
}

/-- Adds a recommended spelling to the environment. -/
def addRecommendedSpelling (env : Environment) (rec : RecommendedSpelling) (names : Array Name) : Environment :=
let env := recommendedSpellingExt.addEntry env rec
recommendedSpellingByNameExt.addEntry env (rec, names)

/-- Returns the recommended spellings associated with the given declaration name. -/
def getRecommendedSpellingsForName (env : Environment) (declName : Name) :
Array RecommendedSpelling := Id.run do
let mut spellings := #[]
for modArr in recommendedSpellingByNameExt.toEnvExtension.getState env |>.importedEntries do
if let some (_, strs) := modArr.binSearch (declName, #[]) (Name.quickLt ·.1 ·.1) then
spellings := spellings ++ strs
if let some strs := recommendedSpellingByNameExt.getState env |>.find? declName then
spellings := spellings ++ strs
return spellings

/-- Renders the recommended spellings for the given declaration into a string for appending to
the docstring. -/
def getRecommendedSpellingString (env : Environment) (declName : Name) : String := Id.run do
let spellings := getRecommendedSpellingsForName env declName
if spellings.size == 0 then ""
else "\n\nConventions for notations in identifiers:\n\n" ++ String.join (spellings.toList.map bullet) |>.trimRight
where
indentLine (str : String) : String :=
(if str.all (·.isWhitespace) then str else " " ++ str) ++ "\n"
bullet (spelling : RecommendedSpelling) : String :=
let firstLine := s!" * The recommended spelling of `{spelling.«notation»}` in identifiers is `{spelling.recommendedSpelling}`"
let additionalInfoLines := spelling.additionalInformation?.map (·.splitOn "\n")
match additionalInfoLines with
| none | some [] => firstLine ++ ".\n\n"
| some [l] => firstLine ++ s!" ({l.trimRight}).\n\n"
| some ls => firstLine ++ ".\n\n" ++ String.join (ls.map indentLine) ++ "\n\n"

end Lean.Parser.Term.Doc
64 changes: 64 additions & 0 deletions tests/lean/run/recommendedSpelling.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Lean

/-!
Test the `recommended_spelling` command.

TODO: once we use this command in Init, we can test that recommended spellings from imported
modules are reported correctly.
-/

/--
Conjuction

Second line
-/
inductive MyAnd (P Q : Prop) : Prop where
| intro : P → Q → MyAnd P Q

@[inherit_doc] infixr:35 " ☃ " => MyAnd
@[inherit_doc] infixr:35 " ☋ " => MyAnd

recommended_spelling "snowmand" for "☃" in [MyAnd, «term_☃_»]
/-- the preferred notation is `∧`.

more stuff

even more stuff

hello

-/
recommended_spelling "orbitalAnd" for "☋" in [MyAnd, «term_☋_»]
/-- Docstring -/
recommended_spelling "and" for "something" in [«term_☃_», «term_☋_»]

def findDocString? (n : Lean.Name) : Lean.MetaM (Option String) := do
Lean.findDocString? (← Lean.getEnv) n

/--
info: some
"Conjuction\n\nSecond line\n\n\nConventions for notations in identifiers:\n\n * The recommended spelling of `☃` in identifiers is `snowmand`.\n\n * The recommended spelling of `☋` in identifiers is `orbitalAnd`.\n\n the preferred notation is `∧`.\n\n more stuff\n\n even more stuff\n\n hello"
-/
#guard_msgs in
#eval findDocString? `MyAnd

/--
info: some
"Conjuction\n\nSecond line\n\n\nConventions for notations in identifiers:\n\n * The recommended spelling of `☃` in identifiers is `snowmand`.\n\n * The recommended spelling of `something` in identifiers is `and` (Docstring)."
-/
#guard_msgs in
#eval findDocString? `«term_☃_»

/--
info: some
"Conjuction\n\nSecond line\n\n\nConventions for notations in identifiers:\n\n * The recommended spelling of `☋` in identifiers is `orbitalAnd`.\n\n the preferred notation is `∧`.\n\n more stuff\n\n even more stuff\n\n hello\n\n\n\n\n * The recommended spelling of `something` in identifiers is `and` (Docstring)."
-/
#guard_msgs in
#eval findDocString? `«term_☋_»

/-- info: 1 -/
#guard_msgs in
#eval do
return (← Lean.Elab.Term.Doc.allRecommendedSpellings)
|>.map Lean.Parser.Term.Doc.RecommendedSpelling.«notation»
|>.count "☋"
Loading