chore: switch the association of stored suggestions (#11590)

This PR switches the way olean files store identifier suggestions. The
ordering introduced in #11367 and #11529 made sense if we were only
storing incorrect -> correct mappings, but for the reference manual we
want to store the correct -> incorrect mappings as well, and so it is
more sensible to store just the correct -> incorrect mapping that mimics
the author-generated data better.

Also tweaks error messages further in preparation for public-facing
@[suggest_for] annotations and forbids suggestions on non-public names.

Does not make generally-visible changes as there are no introduced uses
of @[suggest_for] annotations yet.
This commit is contained in:
Robert J. Simmons 2025-12-10 16:42:05 -05:00 committed by GitHub
parent 466a24893b
commit 9e87560376
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 109 additions and 38 deletions

View file

@ -1375,13 +1375,13 @@ private def resolveLValAux (e : Expr) (eType : Expr) (lval : LVal) : TermElabM L
-- inheritance, nor `import all`.
(declHint := (mkPrivateName env structName).mkStr fieldName)
| .forallE .., LVal.fieldName ref fieldName suffix? _fullRef =>
| .forallE .., LVal.fieldName ref fieldName suffix? fullRef =>
let fullName := Name.str `Function fieldName
if (← getEnv).contains fullName then
return LValResolution.const `Function `Function fullName
match e.getAppFn, suffix? with
| Expr.const c _, some suffix =>
throwUnknownConstantWithSuggestions (c ++ suffix)
throwUnknownConstantWithSuggestions (ref? := fullRef) (c ++ suffix)
| _, _ =>
throwInvalidFieldAt ref fieldName fullName
| .forallE .., .fieldIdx .. =>
@ -1402,8 +1402,8 @@ private def resolveLValAux (e : Expr) (eType : Expr) (lval : LVal) : TermElabM L
| _, _ =>
match e.getAppFn, lval with
| Expr.const c _, .fieldName _ref _fieldName (some suffix) _fullRef =>
throwUnknownConstantWithSuggestions (c ++ suffix)
| Expr.const c _, .fieldName _ref _fieldName (some suffix) fullRef =>
throwUnknownConstantWithSuggestions (ref? := fullRef) (c ++ suffix)
| _, .fieldName .. =>
throwNamedError lean.invalidField m!"Invalid field notation: Field projection operates on \
types of the form `C ...` where C is a constant. The expression{indentExpr e}\nhas \
@ -1424,7 +1424,7 @@ where
type{inlineExprTrailing eType}"
-- Possible alternatives provided with `@[suggest_for]` annotations
let suggestions := (← Lean.getSuggestions fullName).filter (·.getPrefix = fullName.getPrefix)
let suggestions := (← Lean.getSuggestions fullName).filter (·.getPrefix = fullName.getPrefix) |>.toArray
let suggestForHint ←
if h : suggestions.size = 0 then
pure .nil
@ -1432,9 +1432,9 @@ where
MessageData.hint (ref? := ref)
m!"Perhaps you meant `{.ofConstName suggestions[0]}` in place of `{fullName}`:"
(suggestions.map fun suggestion => {
preInfo? := .some s!"{e}.",
preInfo? := .some s!".",
suggestion := suggestion.getString!,
toCodeActionTitle? := .some (s!"Change to {e}.{·}"),
toCodeActionTitle? := .some (s!"Change to .{·}"),
diffGranularity := .all,
})
else
@ -1442,8 +1442,8 @@ where
m!"Perhaps you meant one of these in place of `{fullName}`:"
(suggestions.map fun suggestion => {
suggestion := suggestion.getString!,
toCodeActionTitle? := .some (s!"Change to {e}.{·}"),
messageData? := .some m!"`{.ofConstName suggestion}`: {e}.{suggestion.getString!}",
toCodeActionTitle? := .some (s!"Change to .{·}"),
messageData? := .some m!"`{.ofConstName suggestion}`",
})
-- By using `mkUnknownIdentifierMessage`, the tag `Lean.unknownIdentifierMessageTag` is

View file

@ -17,23 +17,35 @@ namespace Lean
set_option doc.verso true
public structure IdentSuggestion where
existingToIncorrect : NameMap NameSet := {}
incorrectToExisting : NameMap NameSet := {}
deriving Inhabited
def IdentSuggestion.add (table : IdentSuggestion) (trueName : Name) (altNames : Array Name) : IdentSuggestion :=
{ existingToIncorrect :=
table.existingToIncorrect.alter trueName fun old =>
altNames.foldl (β := NameSet) (init := old.getD {}) fun accum altName =>
accum.insert altName
incorrectToExisting :=
altNames.foldl (init := table.incorrectToExisting) fun accum altName =>
accum.alter altName fun old =>
old.getD {} |>.insert trueName
}
builtin_initialize identifierSuggestionForAttr : PersistentEnvExtension
(Name × Array Name) /- *Store* mappings from incorrect names to corrections (identifiers that exist) -/
(Name × Array Name) /- *Add* mappings from existing functions to possible incorrect namings -/
(NameMap NameSet) /- *Use* mapping from incorrect names to corrections (identifiers that exist) -/ ←
(Name × Array Name) /- Mapping from existing constant to potential incorrect alternative names -/
(Name × Array Name) /- Mapping from existing constant to potential incorrect alternative names -/
IdentSuggestion /- Two directional mapping between existing identifier and alternative incorrect mappings -/ ←
let ext ← registerPersistentEnvExtension {
mkInitial := pure {},
addImportedFn := fun modules => pure <|
modules.foldl (init := {}) fun accum entries =>
entries.foldl (init := accum) fun accum (altName, trueNames) =>
accum.alter altName (fun old => trueNames.foldl (β := NameSet) (init := old.getD ∅) fun accum trueName =>
accum.insert trueName),
addEntryFn := fun table (trueName, altNames) =>
altNames.foldl (init := table) fun accum alt =>
accum.alter alt (·.getD {} |>.insert trueName)
exportEntriesFn table := table.toArray.map fun (altName, trueNames) =>
(altName, trueNames.toArray)
(modules.foldl (init := {}) fun accum entries =>
entries.foldl (init := accum) fun accum (trueName, altNames) =>
accum.add trueName altNames),
addEntryFn := fun table (trueName, altNames) => table.add trueName altNames
exportEntriesFn table := table.existingToIncorrect.toArray.map fun (trueName, altNames) =>
(trueName, altNames.toArray)
}
registerBuiltinAttribute {
@ -47,16 +59,32 @@ builtin_initialize identifierSuggestionForAttr : PersistentEnvExtension
| -- Attributes parsed _before the suggest_for notation is added
.node _ ``Parser.Attr.simple #[.ident _ _ `suggest_for [], .node _ `null #[id]] => pure #[id]
| _ => throwError "Invalid `[suggest_for]` attribute syntax {repr stx}"
if isPrivateName decl then
throwError "Cannot make suggestions for private names"
modifyEnv (ext.addEntry · (decl, altSyntaxIds.map (·.getId)))
}
return ext
public def getSuggestions [Monad m] [MonadEnv m] (fullName : Name) : m (Array Name) := do
/--
Given a name, find all the stored correct, existing identifiers that mention that name in a
{lit}`suggest_for` annotation.
-/
public def getSuggestions [Monad m] [MonadEnv m] (incorrectName : Name) : m NameSet := do
return identifierSuggestionForAttr.getState (← getEnv)
|>.find? fullName
|>.incorrectToExisting
|>.find? incorrectName
|>.getD {}
/--
Given a (presumably existing) identifier, find all the {lit}`suggest_for` annotations that were given
for that identifier.
-/
public def getStoredSuggestions [Monad m] [MonadEnv m] (trueName : Name) : m NameSet := do
return identifierSuggestionForAttr.getState (← getEnv)
|>.existingToIncorrect
|>.find? trueName
|>.getD {}
|>.toArray
/--
Throw an unknown constant error message, potentially suggesting alternatives using
@ -64,15 +92,15 @@ Throw an unknown constant error message, potentially suggesting alternatives usi
The "Unknown constant `<id>`" message will fully qualify the name, whereas the
-/
public def throwUnknownConstantWithSuggestions (constName : Name) : CoreM α := do
let suggestions ← getSuggestions constName
let ref ← getRef
public def throwUnknownConstantWithSuggestions (constName : Name) (ref? : Option Syntax := .none) : CoreM α := do
let suggestions := (← getSuggestions constName).toArray
let ref := ref?.getD (← getRef)
let hint ← if suggestions.size = 0 then
pure MessageData.nil
else
-- Modify suggestions to have the same structure as the user-provided identifier, but only
-- if that doesn't cause ambiguity.
let rawId := (← getRef).getId
let rawId := ref.getId
let env ← getEnv
let ns ← getCurrNamespace
let openDecls ← getOpenDecls
@ -86,7 +114,8 @@ public def throwUnknownConstantWithSuggestions (constName : Name) : CoreM α :=
candidate
let alternative := if h : suggestions.size = 1 then m!"`{.ofConstName suggestions[0]}`" else m!"one of these"
m!"Perhaps you meant {alternative} in place of `{.ofName rawId}`:".hint (suggestions.map fun suggestion =>
let inPlaceOfThis := if rawId.isAnonymous then .nil else m!" in place of `{.ofName rawId}`"
m!"Perhaps you meant {alternative}{inPlaceOfThis}:".hint (suggestions.map fun suggestion =>
let modified := modifySuggestion suggestion
{
suggestion := s!"{modified}",

View file

@ -23,7 +23,7 @@ error: Invalid field `test0`: The environment does not contain `String.test0`, s
of type `String`
Hint: Perhaps you meant `String.foo` in place of `String.test0`:
"abc".t̵e̵s̵t̵0̵f̲o̲o̲
.t̵e̵s̵t̵0̵f̲o̲o̲
-/
#guard_msgs in
#check "abc".test0
@ -44,8 +44,8 @@ error: Invalid field `test1`: The environment does not contain `String.test1`, s
of type `String`
Hint: Perhaps you meant one of these in place of `String.test1`:
[apply] `String.foo`: "abc".foo
[apply] `String.baz`: "abc".baz
[apply] `String.foo`
[apply] `String.baz`
-/
#guard_msgs in
#check "abc".test1
@ -67,9 +67,9 @@ error: Invalid field `test2`: The environment does not contain `String.test2`, s
of type `String`
Hint: Perhaps you meant one of these in place of `String.test2`:
[apply] `String.foo`: "abc".foo
[apply] `String.baz`: "abc".baz
[apply] `String.bar`: "abc".bar
[apply] `String.foo`
[apply] `String.baz`
[apply] `String.bar`
-/
#guard_msgs in
#check "abc".test2
@ -119,7 +119,7 @@ error: Invalid field `toNum`: The environment does not contain `Foo.Bar.toNum`,
of type `Foo.Bar`
Hint: Perhaps you meant `Foo.Bar.toNat` in place of `Foo.Bar.toNum`:
Foo.Bar.three.t̵o̵N̵u̵m̵t̲o̲N̲a̲t̲
.t̵o̵N̵u̵m̵t̲o̲N̲a̲t̲
-/
#guard_msgs in
#eval Foo.Bar.three.toNum
@ -130,7 +130,7 @@ error: Invalid field `toStr`: The environment does not contain `Foo.Bar.toStr`,
of type `Foo.Bar`
Hint: Perhaps you meant `Foo.Bar.toString` in place of `Foo.Bar.toStr`:
Foo.Bar.two.t̵o̵S̵t̵r̵t̲o̲S̲t̲r̲i̲n̲g̲
.t̵o̵S̵t̵r̵t̲o̲S̲t̲r̲i̲n̲g̲
-/
#guard_msgs in
#eval Foo.Bar.two.toStr
@ -274,7 +274,35 @@ error: Invalid field `not`: The environment does not contain `MyBool.not`, so it
of type `MyBool`
Hint: Perhaps you meant `MyBool.swap` in place of `MyBool.not`:
MyBool.tt.n̵o̵t̵s̲w̲a̲p̲
.n̵o̵t̵s̲w̲a̲p̲
-/
#guard_msgs in
example := MyBool.tt.not
/--
error: Invalid field `not`: The environment does not contain `MyBool.not`, so it is not possible to project the field `not` from an expression
(fun x => if x < 3 then MyBool.tt else MyBool.ff) 4
of type `MyBool`
Hint: Perhaps you meant `MyBool.swap` in place of `MyBool.not`:
.n̵o̵t̵s̲w̲a̲p̲
-/
#guard_msgs in
example := ((fun x => if x < 3 then MyBool.tt else .ff) 4).not
@[suggest_for MyBool.not]
def MyBool.justFalse : MyBool → MyBool
| _ => ff
/--
error: Invalid field `not`: The environment does not contain `MyBool.not`, so it is not possible to project the field `not` from an expression
(fun x => if x < 3 then MyBool.tt else MyBool.ff) 4
of type `MyBool`
Hint: Perhaps you meant one of these in place of `MyBool.not`:
[apply] `MyBool.justFalse`
[apply] `MyBool.swap`
-/
#guard_msgs in
example := ((fun x => if x < 3 then MyBool.tt else .ff) 4).not

View file

@ -510,3 +510,17 @@ public meta def delab : Lean.PrettyPrinter.Delaborator.Delab :=
public def noMetaDelab : Lean.PrettyPrinter.Delaborator.Delab :=
default
/-- error: Cannot make suggestions for private names -/
#guard_msgs in
@[suggest_for Bar1]
def FooBar1 := 4
/-- error: Cannot make suggestions for private names -/
#guard_msgs in
@[suggest_for Bar2]
meta def FooBar2 := 4
#guard_msgs in
@[suggest_for Bar3 FooBar1 FooBar2]
public def FooBar3 := 4