Skip to content

Commit edcef51

Browse files
authored
feat: improve error messages for invalid field access (#11456)
This PR refines several error error messages, mostly involving invalid use of field notation, generalized field notation, and numeric projection. Provides a new error explanation for field notation. ## Error message changes In general: - Uses a slightly different convention for expression-type pairs, where the expression is always given `indentExpr` and the type is given `inlineExpr` treatment. This is something of a workaround for the fact that the `Format` type is awkward for embedding possibly-linebreaking expressions in not-linebreaking text, which may be a separate issue worth addressing. - Tries to give slightly more "why" reasoning — the environment does not contain `String.parse`, and _therefore you can't project `.parse` from a `String`_. Some specific examples: ### No such projection function ```lean4 #check "".parse ``` before: ``` error: Invalid field `parse`: The environment does not contain `String.parse` "" has type String ``` after: ``` error: Invalid field `parse`: The environment does not contain `String.parse`, so it is not possible to project the field `parse` from an expression "" of type `String` ``` ### Type does not have the correct form ```lean4 example (x : α) := (foo x).foo ``` before: ``` error: Invalid field notation: Type is not of the form `C ...` where C is a constant foo x has type α ``` after: ``` error: Invalid field notation: Field projection operates on types of the form `C ...` where C is a constant. The expression foo x has type `α` which does not have the necessary form. ``` ## Refactoring Includes some refactoring changes as well: - factors out multiple uses of number (1, 2, 3, 212, 222) to ordinal ("first", "second", "third", "212th", "222nd") conversion into Lean.Elab.ErrorUtils - significant refactoring of `resolveLValAux` in `Lean.Elab.App` — in place of five helper functions, a special-case function case analysis, and a case analysis on the projection type and structure, there's now a single case analysis on the projection type and structure. This allows several error messages to be more explicit (there were a number of cases where index projection was being described as field projection in an error messages) and gave the opportunity to slightly improve positining for several errors: field *notation* errors should appear on `foo.bar`, but field *projection* errors should appear only on the `bar` part of `foo.bar`.
1 parent 7983883 commit edcef51

18 files changed

+351
-183
lines changed

src/Lean/Elab/App.lean

Lines changed: 63 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ prelude
99
public import Lean.Meta.Tactic.ElimInfo
1010
public import Lean.Elab.Binders
1111
public import Lean.Elab.RecAppSyntax
12+
import all Lean.Elab.ErrorUtils
1213

1314
public section
1415

@@ -1252,15 +1253,6 @@ inductive LValResolution where
12521253
The `baseName` is the base name of the type to search for in the parameter list. -/
12531254
| localRec (baseName : Name) (fvar : Expr)
12541255

1255-
private def mkLValError (e : Expr) (eType : Expr) (msg : MessageData) : MessageData :=
1256-
m!"{msg}{indentExpr e}\nhas type{indentExpr eType}"
1257-
1258-
private def throwLValErrorAt (ref : Syntax) (e : Expr) (eType : Expr) (msg : MessageData) : TermElabM α :=
1259-
throwErrorAt ref (mkLValError e eType msg)
1260-
1261-
private def throwLValError (e : Expr) (eType : Expr) (msg : MessageData) : TermElabM α := do
1262-
throwLValErrorAt (← getRef) e eType msg
1263-
12641256
/--
12651257
`findMethod? S fName` tries the for each namespace `S'` in the resolution order for `S` to resolve the name `S'.fname`.
12661258
If it resolves to `name`, returns `(S', name)`.
@@ -1291,9 +1283,6 @@ private partial def findMethod? (structName fieldName : Name) : MetaM (Option (N
12911283
return res
12921284
return none
12931285

1294-
private def throwInvalidFieldNotation (e eType : Expr) : TermElabM α :=
1295-
throwLValError e eType "Invalid field notation: Type is not of the form `C ...` where C is a constant"
1296-
12971286
/--
12981287
If it seems that the user may be attempting to project out the `n`th element of a tuple, or that the
12991288
nesting behavior of n-ary products is otherwise relevant, generates a corresponding hint; otherwise,
@@ -1304,15 +1293,13 @@ private partial def mkTupleHint (eType : Expr) (idx : Nat) (ref : Syntax) : Term
13041293
if arity > 1 then
13051294
let numComps := arity + 1
13061295
if idx ≤ numComps && ref.getHeadInfo matches .original .. then
1307-
let ordinalSuffix := match idx % 10 with
1308-
| 1 => "st" | 2 => "nd" | 3 => "rd" | _ => "th"
13091296
let mut projComps := List.replicate (idx - 1) "2"
13101297
if idx < numComps then projComps := projComps ++ ["1"]
13111298
let proj := ".".intercalate projComps
13121299
let sug := { suggestion := proj, span? := ref,
13131300
toCodeActionTitle? := some (s!"Change projection `{idx}` to `{·}`") }
13141301
MessageData.hint m!"n-tuples in Lean are actually nested pairs. To access the \
1315-
{idx}{ordinalSuffix} component of this tuple, use the projection `.{proj}` instead:" #[sug]
1302+
{idx.toOrdinal} component of this tuple, use the projection `.{proj}` instead:" #[sug]
13161303
else
13171304
return MessageData.hint' m!"n-tuples in Lean are actually nested pairs. For example, to access the \
13181305
\"third\" component of `(a, b, c)`, write `(a, b, c).2.2` instead of `(a, b, c).3`."
@@ -1325,45 +1312,15 @@ where
13251312
| some (_, p2) => prodArity p2 + 1
13261313

13271314
private def resolveLValAux (e : Expr) (eType : Expr) (lval : LVal) : TermElabM LValResolution := do
1328-
let throwInvalidFieldAt {α : Type} (ref : Syntax) (fieldName : String) (fullName : Name)
1329-
(declHint := Name.anonymous) : TermElabM α := do
1330-
let msg ←
1331-
-- ordering: put decl hint, if any, last
1332-
mkUnknownIdentifierMessage (declHint := declHint)
1333-
(mkLValError e eType
1334-
m!"Invalid field `{fieldName}`: The environment does not contain `{fullName}`")
1335-
-- HACK: Simulate previous embedding of tagged `mkUnknownIdentifierMessage`.
1336-
-- The "import unknown identifier" code action requires the tag to be present somewhere in the
1337-
-- message. But if it is at the root, the tag will also be shown to the user even though the
1338-
-- current help page does not address field notation, which should likely get its own help page
1339-
-- (if any).
1340-
throwErrorAt ref m!"{msg}{.nil}"
1341-
if eType.isForall then
1342-
match lval with
1343-
| LVal.fieldName ref fieldName suffix? _fullRef =>
1344-
let fullName := Name.str `Function fieldName
1345-
if (← getEnv).contains fullName then
1346-
return LValResolution.const `Function `Function fullName
1347-
else if suffix?.isNone || e.getAppFn.isFVar then
1348-
/- If there's no suffix, or the head is a function-typed free variable, this could only have
1349-
been a field in the `Function` namespace, so we needn't wait to check if this is actually
1350-
a constant. If `suffix?` is non-`none`, we prefer to throw the "unknown constant" error
1351-
(because of monad namespaces like `IO` and auxiliary declarations like `mutual_induct`) -/
1352-
throwInvalidFieldAt ref fieldName fullName
1353-
| .fieldIdx .. =>
1354-
throwLValError e eType "Invalid projection: Projections cannot be used on functions"
1355-
else if eType.getAppFn.isMVar then
1356-
let (kind, name) :=
1357-
match lval with
1358-
| .fieldName _ fieldName _ _ => (m!"field notation", m!"field `{fieldName}`")
1359-
| .fieldIdx _ i => (m!"projection", m!"projection `{i}`")
1360-
throwError "Invalid {kind}: Type of{indentExpr e}\nis not known; cannot resolve {name}"
1361-
match eType.getAppFn.constName?, lval with
1362-
| some structName, LVal.fieldIdx ref idx =>
1315+
match eType.getAppFn, lval with
1316+
| .const structName _, LVal.fieldIdx ref idx =>
13631317
if idx == 0 then
13641318
throwError "Invalid projection: Index must be greater than 0"
13651319
let env ← getEnv
1366-
let failK _ := throwLValError e eType "Invalid projection: Expected a value whose type is a structure"
1320+
let failK _ := throwError "Invalid projection: Projection operates on structure-like types \
1321+
with fields. The expression{indentExpr e}\nhas type{inlineExpr eType}which does not \
1322+
have fields."
1323+
13671324
matchConstStructure eType.getAppFn failK fun _ _ ctorVal => do
13681325
let numFields := ctorVal.numFields
13691326
if idx - 1 < numFields then
@@ -1376,17 +1333,15 @@ private def resolveLValAux (e : Expr) (eType : Expr) (lval : LVal) : TermElabM L
13761333
return LValResolution.projIdx structName (idx - 1)
13771334
else
13781335
if numFields == 0 then
1379-
throwLValError e eType m!"Invalid projection: Projections are not supported on this type \
1380-
because it has no fields"
1381-
let (fields, bounds) := if numFields == 1 then
1382-
(m!"field", m!"the only valid index is 1")
1383-
else
1384-
(m!"fields", m!"it must be between 1 and {numFields}")
1336+
throwError m!"Invalid projection: Projection operates on structure-like types with \
1337+
fields. The expression{indentExpr e}\nhas type{inlineExpr eType}which has no fields."
13851338
let tupleHint ← mkTupleHint eType idx ref
1386-
throwError m!"Invalid projection: Index `{idx}` is invalid for this structure; {bounds}"
1387-
++ .note m!"The expression{inlineExpr e}has type{inlineExpr eType}which has only {numFields} {fields}"
1339+
throwError m!"Invalid projection: Index `{idx}` is invalid for this structure; \
1340+
{numFields.plural "the only valid index is 1" s!"it must be between 1 and {numFields}"}"
1341+
++ MessageData.note m!"The expression{indentExpr e}\nhas type{inlineExpr eType}which has only \
1342+
{numFields} field{numFields.plural}"
13881343
++ tupleHint
1389-
| some structName, LVal.fieldName ref fieldName _ _ => withRef ref do
1344+
| .const structName _, LVal.fieldName ref fieldName _ _ => withRef ref do
13901345
let env ← getEnv
13911346
if isStructure env structName then
13921347
if let some baseStructName := findField? env structName (Name.mkSimple fieldName) then
@@ -1406,14 +1361,54 @@ private def resolveLValAux (e : Expr) (eType : Expr) (lval : LVal) : TermElabM L
14061361
-- Suggest a potential unreachable private name as hint. This does not cover structure
14071362
-- inheritance, nor `import all`.
14081363
(declHint := (mkPrivateName env structName).mkStr fieldName)
1409-
| none, LVal.fieldName ref _ (some suffix) _fullRef =>
1410-
-- This may be a function constant whose implicit arguments have already been filled in:
1411-
let c := e.getAppFn
1412-
if c.isConst then
1413-
throwUnknownConstantAt ref (c.constName! ++ suffix)
1414-
else
1415-
throwInvalidFieldNotation e eType
1416-
| _, _ => throwInvalidFieldNotation e eType
1364+
1365+
| .forallE .., LVal.fieldName ref fieldName suffix? _fullRef =>
1366+
let fullName := Name.str `Function fieldName
1367+
if (← getEnv).contains fullName then
1368+
return LValResolution.const `Function `Function fullName
1369+
match e.getAppFn, suffix? with
1370+
| Expr.const c _, some suffix =>
1371+
throwUnknownConstant (c ++ suffix)
1372+
| _, _ =>
1373+
throwInvalidFieldAt ref fieldName fullName
1374+
| .forallE .., .fieldIdx .. =>
1375+
throwError "Invalid projection: Projections cannot be used on functions, and{indentExpr e}\n\
1376+
has function type{inlineExprTrailing eType}"
1377+
1378+
| .mvar .., .fieldName _ fieldName _ _ =>
1379+
throwNamedError lean.invalidField m!"Invalid field notation: Type of{indentExpr e}\nis not \
1380+
known; cannot resolve field `{fieldName}`"
1381+
| .mvar .., .fieldIdx _ i =>
1382+
throwError m!"Invalid projection: Type of{indentExpr e}\nis not known; cannot resolve \
1383+
projection `{i}`"
1384+
1385+
| _, _ =>
1386+
match e.getAppFn, lval with
1387+
| Expr.const c _, .fieldName _ref _fieldName (some suffix) _fullRef =>
1388+
throwUnknownConstant (c ++ suffix)
1389+
| _, .fieldName .. =>
1390+
throwNamedError lean.invalidField m!"Invalid field notation: Field projection operates on \
1391+
types of the form `C ...` where C is a constant. The expression{indentExpr e}\nhas \
1392+
type{inlineExpr eType}which does not have the necessary form."
1393+
| _, .fieldIdx .. =>
1394+
throwError m!"Invalid projection: Projection operates on types of the form `C ...` where C \
1395+
is a constant. The expression{indentExpr e}\nhas type{inlineExpr eType}which does not have \
1396+
the necessary form."
1397+
1398+
where
1399+
throwInvalidFieldAt {α : Type} (ref : Syntax) (fieldName : String) (fullName : Name)
1400+
(declHint := Name.anonymous) : TermElabM α := do
1401+
let msg ←
1402+
-- ordering: put decl hint, if any, last
1403+
mkUnknownIdentifierMessage (declHint := declHint)
1404+
m!"Invalid field `{fieldName}`: The environment does not contain `{fullName}`, so it is not \
1405+
possible to project the field `{fieldName}` from an expression{indentExpr e}\nof \
1406+
type{inlineExprTrailing eType}"
1407+
-- By using `mkUnknownIdentifierMessage`, the tag `Lean.unknownIdentifierMessageTag` is
1408+
-- incorporated within the message, as required for the "import unknown identifier" code action.
1409+
-- The "outermost" lean.invalidField name is the only one that triggers an error explanation.
1410+
throwNamedErrorAt ref lean.invalidField msg
1411+
14171412

14181413
/-- whnfCore + implicit consumption.
14191414
Example: given `e` with `eType := {α : Type} → (fun β => List β) α `, it produces `(e ?m, List ?m)` where `?m` is fresh metavariable. -/

src/Lean/Elab/ErrorUtils.lean

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/-
2+
Copyright (c) 2025 Microsoft Corporation. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Rob Simmons
5+
-/
6+
module
7+
8+
prelude
9+
import Init.Notation
10+
import Init.Data.String
11+
12+
/--
13+
Translate numbers (1, 2, 3, 212, 322 ...) to ordinals with appropriate English-language names for
14+
small ordinals (0 through 10 become "zeroth" through "tenth") and postfixes for other numbers
15+
(212 becomes "212th" and 1292 becomes "1292nd").
16+
-/
17+
def _root_.Nat.toOrdinal : Nat -> String
18+
| 0 => "zeroth"
19+
| 1 => "first"
20+
| 2 => "second"
21+
| 3 => "third"
22+
| 4 => "fourth"
23+
| 5 => "fifth"
24+
| 6 => "sixth"
25+
| 7 => "seventh"
26+
| 8 => "eighth"
27+
| 9 => "ninth"
28+
| 10 => "tenth"
29+
| n => if n % 100 > 10 && n % 100 < 20 then
30+
s!"{n}th"
31+
else if n % 10 == 2 then
32+
s!"{n}nd"
33+
else if n % 10 == 3 then
34+
s!"{n}rd"
35+
else
36+
s!"{n}th"
37+
38+
/--
39+
Make an oxford-comma-separated list of strings.
40+
41+
- `["eats"].toOxford == "eats"`
42+
- `["eats", "shoots"].toOxford == "eats and shoots"`
43+
- `["eats", "shoots", "leaves"] == "eats, shoots, and leaves"`
44+
-/
45+
def _root_.List.toOxford : List String -> String
46+
| [] => ""
47+
| [a] => a
48+
| [a, b] => a ++ " and " ++ b
49+
| [a, b, c] => a ++ ", " ++ b ++ ", and " ++ c
50+
| a :: as => a ++ ", " ++ toOxford as
51+
52+
/--
53+
Give alternative forms of a string if the `count` is 1 or not.
54+
55+
- `(1).plural == ""`
56+
- `(2).plural == "s"`
57+
- `(1).plural "wug" == "wug"`
58+
- `(2).plural "wug" == "wugs"`
59+
- `(1).plural "it" "they" == "it"`
60+
- `(2).plural "it" "they" == "they"`
61+
-/
62+
def _root_.Nat.plural (count : Nat) (singular : String := "") (plural : String := singular ++ "s") :=
63+
if count = 1 then
64+
singular
65+
else
66+
plural

src/Lean/Elab/SyntheticMVars.lean

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public import Lean.Util.OccursCheck
1212
public import Lean.Elab.Tactic.Basic
1313
public import Lean.Meta.AbstractNestedProofs
1414
public import Init.Data.List.Sort.Basic
15+
import all Lean.Elab.ErrorUtils
1516

1617
public section
1718

@@ -209,33 +210,6 @@ private def synthesizeUsingDefault : TermElabM Bool := do
209210
return true
210211
return false
211212

212-
/--
213-
Translate zero-based indexes (0, 1, 2, ...) to ordinals ("first", "second",
214-
"third", ...). Not appropriate for numbers that could conceivably be larger
215-
than 19 in real examples.
216-
-/
217-
private def toOrdinalString : Nat -> String
218-
| 0 => "first"
219-
| 1 => "second"
220-
| 2 => "third"
221-
| 3 => "fourth"
222-
| 4 => "fifth"
223-
| n => s!"{n+1}th"
224-
225-
/-- Make an oxford-comma-separated list of strings. -/
226-
private def toOxford : List String -> String
227-
| [] => ""
228-
| [a] => a
229-
| [a, b] => a ++ " and " ++ b
230-
| [a, b, c] => a ++ ", " ++ b ++ ", and " ++ c
231-
| a :: as => a ++ ", " ++ toOxford as
232-
233-
/- Give alternative forms of a string if the `count` is 1 or not. -/
234-
private def _root_.Nat.plural (count : Nat) (singular : String) (plural : String) :=
235-
if count = 1 then
236-
singular
237-
else
238-
plural
239213

240214
def explainStuckTypeclassProblem (typeclassProblem : Expr) : TermElabM (Option MessageData) := do
241215

@@ -296,7 +270,7 @@ def explainStuckTypeclassProblem (typeclassProblem : Expr) : TermElabM (Option M
296270
if args.length = 1 then
297271
"the type argument"
298272
else
299-
s!"the {toOxford (stuckArguments.toList.map toOrdinalString)} type {nStuck.plural "argument" "arguments"}"
273+
s!"the {(stuckArguments.toList.map (·.succ.toOrdinal)).toOxford} type {nStuck.plural "argument" "arguments"}"
300274

301275
return .some (.note m!"Lean will not try to resolve this typeclass instance problem because {theTypeArguments} to `{.ofConstName name}` {containMVars}. {nStuck.plural "This argument" "These arguments"} must be fully determined before Lean will try to resolve the typeclass."
302276
++ .hint' m!"Adding type annotations and supplying implicit arguments to functions can give Lean more information for typeclass resolution. For example, if you have a variable `x` that you intend to be a `{MessageData.ofConstName ``Nat}`, but Lean reports it as having an unresolved type like `?m`, replacing `x` with `(x : Nat)` can get typeclass resolution un-stuck.")

src/Lean/ErrorExplanations.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public import Lean.ErrorExplanations.InductiveParamMissing
1414
public import Lean.ErrorExplanations.InferBinderTypeFailed
1515
public import Lean.ErrorExplanations.InferDefTypeFailed
1616
public import Lean.ErrorExplanations.InvalidDottedIdent
17+
public import Lean.ErrorExplanations.InvalidField
1718
public import Lean.ErrorExplanations.ProjNonPropFromProp
1819
public import Lean.ErrorExplanations.PropRecLargeElim
1920
public import Lean.ErrorExplanations.RedundantMatchAlt

0 commit comments

Comments
 (0)