lean4-htt/src/Lean/IdentifierSuggestion.lean
Robert J. Simmons 0158172871
chore: remove NameMapExtension abbreviation (#11632)
This PR avoids a conflict with Mathlib by inlining an abbreviation,
NameMapExtension, that wasn't referred to outside the file.
2025-12-12 12:34:53 +00:00

163 lines
8.2 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
Copyright (c) 2025 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Rob Simmons
-/
module
prelude
public import Lean.Attributes
public import Lean.Exception
public import Lean.Meta.Hint
public import Lean.Elab.DeclModifiers
public import Lean.ResolveName
import all Lean.Elab.ErrorUtils
namespace Lean
open Elab.Term
set_option doc.verso true
/--
Create the extension mapping from existing identifiers to the incorrect alternatives for which we
want to provide suggestions. This is mostly equivalent to a {name}`MapDeclarationExtension` or the
extensions underlying {name}`ParametricAttribute` attributes, but it differs in allowing
{name}`suggest_for` attributes to be assigned in files other than the ones where they were defined.
-/
def mkExistingToIncorrect : IO (PersistentEnvExtension (Name × Array Name) (Name × Array Name) (NameMap NameSet)) := registerPersistentEnvExtension {
name := `Lean.identifierSuggestForAttr.existingToIncorrect
mkInitial := pure {},
addImportedFn := fun _ => pure {},
addEntryFn := fun table (trueName, altNames) =>
table.alter trueName fun old =>
altNames.foldl (β := NameSet) (init := old.getD {}) fun accum altName =>
accum.insert altName
exportEntriesFn table :=
table.toArray.map (fun (trueName, altNames) =>(trueName, altNames.toArray))
|>.qsort (lt := fun a b => Name.quickLt a.1 b.1)
}
/--
Create the extension mapping incorrect identifiers to the existing identifiers we want to suggest as
replacements. This association is the opposite of the usual mapping for {name}`ParametricAttribute`
attributes.
-/
def mkIncorrectToExisting : IO (PersistentEnvExtension (Name × Array Name) (Name × Array Name) (NameMap NameSet)) := registerPersistentEnvExtension {
name := `Lean.identifierSuggestForAttr.incorrectToExisting
mkInitial := pure {},
addImportedFn := fun _ => pure {},
addEntryFn := fun table (trueName, altNames) =>
altNames.foldl (init := table) fun accum altName =>
accum.alter altName fun old =>
old.getD {} |>.insert trueName
exportEntriesFn table :=
table.toArray.map (fun (altName, trueNames) => (altName, trueNames.toArray))
|>.qsort (lt := fun a b => Name.quickLt a.1 b.1)
}
builtin_initialize identifierSuggestionsImpl : (PersistentEnvExtension (Name × Array Name) (Name × Array Name) (NameMap NameSet)) × (PersistentEnvExtension (Name × Array Name) (Name × Array Name) (NameMap NameSet)) ←
let existingToIncorrect ← mkExistingToIncorrect
let incorrectToExisting ← mkIncorrectToExisting
registerBuiltinAttribute {
name := `suggest_for,
descr := "suggest other (incorrect, not-existing) identifiers that someone might use when they actually want this definition",
add (decl : Name) (stx : Syntax) (kind : AttributeKind) := do
unless kind == AttributeKind.global do throwAttrMustBeGlobal `suggest_for kind
let altSyntaxIds : Array Syntax ← match stx with
| -- Attributes parsed _after_ the suggest_for notation is added
.node _ ``suggest_for #[.atom _ "suggest_for", .node _ `null ids] => pure ids
| -- 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"
let altIds := altSyntaxIds.map (·.getId)
modifyEnv (existingToIncorrect.addEntry · (decl, altIds))
modifyEnv (incorrectToExisting.addEntry · (decl, altIds))
}
return (existingToIncorrect, incorrectToExisting)
/--
Given a name, find all the stored correct, existing identifiers that mention that name in a
{name}`suggest_for` annotation.
-/
public def getSuggestions [Monad m] [MonadEnv m] (incorrectName : Name) : m NameSet := do
let env ← getEnv
let { state, importedEntries } := identifierSuggestionsImpl.2.toEnvExtension.getState env
let localEntries := state.find? incorrectName |>.getD {}
return importedEntries.foldl (init := localEntries) fun results moduleEntry =>
match moduleEntry.binSearch (incorrectName, default) (fun a b => Name.quickLt a.1 b.1) with
| none => results
| some (_, extras) => extras.foldl (init := results) fun accum extra => accum.insert extra
/--
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
let env ← getEnv
let { state, importedEntries } := identifierSuggestionsImpl.1.toEnvExtension.getState env
let localEntries := state.find? trueName |>.getD {}
return importedEntries.foldl (init := localEntries) fun results moduleEntry =>
match moduleEntry.binSearch (trueName, default) (fun a b => Name.quickLt a.1 b.1) with
| none => results
| some (_, extras) => extras.foldl (init := results) fun accum extra => accum.insert extra
/--
Throw an unknown constant/identifier error message, potentially suggesting alternatives using
{name}`suggest_for` attributes.
The replacement will mimic the path structure of the original as much as possible if they share a
path prefix: if there is a suggestion for replacing `Foo.Bar.jazz` with `Foo.Bar.baz`, then
`Bar.jazz` will be replaced by `Bar.baz` unless the resulting constant is ambiguous.
-/
public def throwUnknownNameWithSuggestions (constName : Name) (idOrConst := "identifier") (declHint := constName) (ref? : Option Syntax := .none) (extraMsg : MessageData := .nil) : TermElabM α := 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 := ref.getId
let env ← getEnv
let ns ← getCurrNamespace
let openDecls ← getOpenDecls
let modifySuggestion := match constName.eraseSuffix? rawId with
| .none => id
| .some prefixName => fun (suggestion : Name) =>
let candidate := suggestion.replacePrefix prefixName .anonymous
if (ResolveName.resolveGlobalName env {} ns openDecls candidate |>.length) != 1 then
suggestion
else
candidate
let alternative := if h : suggestions.size = 1 then m!"`{.ofConstName suggestions[0]}`" else m!"one of these"
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}",
toCodeActionTitle? := .some (s!"Change to {·}"),
messageData? := .some m!"`{.ofConstName suggestion}`",
}) ref
throwUnknownIdentifierAt (declHint := declHint) ref (m!"Unknown {idOrConst} `{.ofConstName constName}`" ++ extraMsg ++ hint)
public def Elab.Term.hintAutoImplicitFailure (exp : Expr) (expected := "a function") : TermElabM MessageData := do
let autoBound := (← readThe Context).autoBoundImplicitContext
unless autoBound.isSome && exp.isFVar && autoBound.get!.boundVariables.any (· == exp) do
return .nil
let name ← exp.fvarId!.getUserName
let baseMessage := m!"The identifier `{.ofName name}` is unknown, \
and Lean's `autoImplicit` option causes an unknown identifier to be treated as an implicitly \
bound variable with an unknown type. \
However, the unknown type cannot be {expected}, and {expected} is what Lean expects here. \
This is often the result of a typo or a missing `import` or `open` statement."
let suggestionExtra : MessageData := match (← getSuggestions name).toList with
| [] => .nil
| [opt] => Format.line ++ Format.line ++ m!"Perhaps you meant `{.ofConstName opt}` in place of `{.ofName name}`?"
| opts => Format.line ++ Format.line ++ m!"Perhaps you meant one of these in place of `{.ofName name}`:" ++
.joinSep (opts.map (indentD m!"• `{.ofConstName ·}`")) .nil
return MessageData.hint' (baseMessage ++ suggestionExtra)