Skip to content

Commit 979637a

Browse files
authored
feat: lint coercions that are deprecated or banned in core (#11511)
This PR implements a linter that warns when a deprecated coercion is applied. It also warns when the `Option` coercion or the `Subarray`-to-`Array` coercion is used in `Init` or `Std`. The linter is currently limited to `Coe` instances; `CoeFun` instances etc. are not considered. The linter works by collecting the `Coe` instance declaration names that are being expanded in `expandCoe?` and storing them in the info tree. The linter itself then analyzes the info tree and checks for banned or deprecated coercions.
1 parent 26ff270 commit 979637a

File tree

11 files changed

+244
-36
lines changed

11 files changed

+244
-36
lines changed

src/Init/Data/Slice/Array/Iterator.lean

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,34 @@ The implementation of `ForIn.forIn` for `Subarray`, which allows it to be used w
130130
def Subarray.forIn {α : Type u} {β : Type v} {m : Type v → Type w} [Monad m] (s : Subarray α) (b : β) (f : α → β → m (ForInStep β)) : m β :=
131131
ForIn.forIn s b f
132132

133-
namespace Array
134-
135133
/--
136134
Allocates a new array that contains the contents of the subarray.
137135
-/
138136
@[coe]
139-
def ofSubarray (s : Subarray α) : Array α :=
137+
def Subarray.toArray (s : Subarray α) : Array α :=
140138
Slice.toArray s
141139

142-
instance : Coe (Subarray α) (Array α) := ⟨ofSubarray⟩
140+
instance instCoeSubarrayArray : Coe (Subarray α) (Array α) :=
141+
⟨Subarray.toArray⟩
142+
143+
@[inherit_doc Subarray.toArray]
144+
def Subarray.copy (s : Subarray α) : Array α :=
145+
Slice.toArray s
146+
147+
@[simp]
148+
theorem Subarray.copy_eq_toArray {s : Subarray α} :
149+
s.copy = s.toArray :=
150+
(rfl)
151+
152+
theorem Subarray.sliceToArray_eq_toArray {s : Subarray α} :
153+
Slice.toArray s = s.toArray :=
154+
(rfl)
155+
156+
namespace Array
157+
158+
@[inherit_doc Subarray.toArray]
159+
def ofSubarray (s : Subarray α) : Array α :=
160+
Slice.toArray s
143161

144162
instance : Append (Subarray α) where
145163
append x y :=
@@ -157,7 +175,3 @@ instance [ToString α] : ToString (Subarray α) where
157175
toString s := toString s.toArray
158176

159177
end Array
160-
161-
@[inherit_doc Array.ofSubarray]
162-
def Subarray.toArray (s : Subarray α) : Array α :=
163-
Array.ofSubarray s

src/Init/Data/Slice/Array/Lemmas.lean

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public instance : LawfulSliceSize (Internal.SubarrayData α) where
110110

111111
public theorem toArray_eq_sliceToArray {α : Type u} {s : Subarray α} :
112112
s.toArray = Slice.toArray s := by
113-
simp [Subarray.toArray, Array.ofSubarray]
113+
simp [Subarray.toArray]
114114

115115
@[simp]
116116
public theorem forIn_toList {α : Type u} {s : Subarray α}
@@ -206,12 +206,12 @@ public theorem Subarray.size_eq {xs : Subarray α} :
206206
@[simp]
207207
public theorem Subarray.toArray_toList {xs : Subarray α} :
208208
xs.toList.toArray = xs.toArray := by
209-
simp [Std.Slice.toList, Subarray.toArray, Array.ofSubarray, Std.Slice.toArray]
209+
simp [Std.Slice.toList, Subarray.toArray, Std.Slice.toArray]
210210

211211
@[simp]
212212
public theorem Subarray.toList_toArray {xs : Subarray α} :
213213
xs.toArray.toList = xs.toList := by
214-
simp [Std.Slice.toList, Subarray.toArray, Array.ofSubarray, Std.Slice.toArray]
214+
simp [Std.Slice.toList, Subarray.toArray, Std.Slice.toArray]
215215

216216
@[simp]
217217
public theorem Subarray.length_toList {xs : Subarray α} :

src/Lean/Elab/InfoTree/Main.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,10 @@ def withMacroExpansionInfo [MonadFinally m] [Monad m] [MonadInfoTree m] [MonadLC
489489
}
490490
withInfoContext x mkInfo
491491

492+
/--
493+
Runs `x`. The last info tree that is pushed while running `x` is assigned to `mvarId`. All other
494+
pushed info trees are silently discarded.
495+
-/
492496
@[inline] def withInfoHole [MonadFinally m] [Monad m] [MonadInfoTree m] (mvarId : MVarId) (x : m α) : m α := do
493497
if (← getInfoState).enabled then
494498
let treesSaved ← getResetInfoTrees

src/Lean/Elab/SyntheticMVars.lean

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,21 @@ private def resumePostponed (savedContext : SavedContext) (stx : Syntax) (mvarId
3838
let mvarDecl ← getMVarDecl mvarId
3939
let expectedType ← instantiateMVars mvarDecl.type
4040
withInfoHole mvarId do
41-
let result ← resumeElabTerm stx expectedType (!postponeOnError)
42-
/- We must ensure `result` has the expected type because it is the one expected by the method that postponed stx.
43-
That is, the method does not have an opportunity to check whether `result` has the expected type or not. -/
44-
let result ← withRef stx <| ensureHasType expectedType result
41+
/-
42+
NOTE: `withInfoTree` discards all but the last info tree pushed inside this `do` block.
43+
`resumeElabTerm` usually pushes the term info node and `ensureHasType` sometimes
44+
pushes a custom info node with information about the coercions that were applied.
45+
46+
In order for both trees to be preserved, we use `withTermInfoContext'` to wrap these
47+
trees into a single node. Although this results in two nested term nodes for the same
48+
syntax element, this should be unproblematic. For example, `hoverableInfoAtM?` selects
49+
the innermost info tree.
50+
-/
51+
let result ← withTermInfoContext' .anonymous stx do
52+
let result ← resumeElabTerm stx expectedType (!postponeOnError)
53+
/- We must ensure `result` has the expected type because it is the one expected by the method that postponed stx.
54+
That is, the method does not have an opportunity to check whether `result` has the expected type or not. -/
55+
withRef stx <| ensureHasType expectedType result
4556
/- We must perform `occursCheck` here since `result` may contain `mvarId` when it has synthetic `sorry`s. -/
4657
if (← occursCheck mvarId result) then
4758
mvarId.assign result
@@ -537,7 +548,11 @@ mutual
537548
if (← occursCheck mvarId e) then
538549
mvarId.assign e
539550
return true
540-
if let .some coerced ← coerce? e expectedType then
551+
if let .some (coerced, expandedCoeDecls) ← coerceCollectingNames? e expectedType then
552+
pushInfoLeaf (.ofCustomInfo {
553+
stx := mvarSyntheticDecl.stx
554+
value := Dynamic.mk <| CoeExpansionTrace.mk expandedCoeDecls
555+
})
541556
if (← occursCheck mvarId coerced) then
542557
mvarId.assign coerced
543558
return true

src/Lean/Elab/Term/TermElabM.lean

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,14 +1209,23 @@ def synthesizeInstMVarCore (instMVar : MVarId) (maxResultSize? : Option Nat := n
12091209
pure <| extraErrorMsg ++ useTraceSynthMsg
12101210
throwNamedError lean.synthInstanceFailed "failed to synthesize instance of type class{indentExpr type}{msg}"
12111211

1212+
structure CoeExpansionTrace where
1213+
expandedCoeDecls : List Name
1214+
deriving TypeName
1215+
12121216
def mkCoe (expectedType : Expr) (e : Expr) (f? : Option Expr := none) (errorMsgHeader? : Option String := none)
12131217
(mkErrorMsg? : Option (MVarId → (expectedType e : Expr) → MetaM MessageData) := none)
12141218
(mkImmedErrorMsg? : Option ((errorMsg? : Option MessageData) → (expectedType e : Expr) → MetaM MessageData) := none) : TermElabM Expr := do
12151219
withTraceNode `Elab.coe (fun _ => return m!"adding coercion for {e} : {← inferType e} =?= {expectedType}") do
12161220
try
12171221
withoutMacroStackAtErr do
1218-
match ← coerce? e expectedType with
1219-
| .some eNew => return eNew
1222+
match ← coerceCollectingNames? e expectedType with
1223+
| .some (eNew, expandedCoeDecls) =>
1224+
pushInfoLeaf (.ofCustomInfo {
1225+
stx := ← getRef
1226+
value := Dynamic.mk <| CoeExpansionTrace.mk expandedCoeDecls
1227+
})
1228+
return eNew
12201229
| .none => failure
12211230
| .undef =>
12221231
let mvarAux ← mkFreshExprMVar expectedType MetavarKind.syntheticOpaque

src/Lean/Linter.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ public import Lean.Linter.Omit
1717
public import Lean.Linter.List
1818
public import Lean.Linter.Sets
1919
public import Lean.Linter.UnusedSimpArgs
20+
public import Lean.Linter.Coe

src/Lean/Linter/Coe.lean

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/-
2+
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Paul Reichert
5+
-/
6+
module
7+
8+
prelude
9+
public import Lean.Elab.Command
10+
public import Lean.Server.InfoUtils
11+
import Lean.Linter.Basic
12+
import Lean.Linter.Deprecated
13+
import all Lean.Elab.Term.TermElabM
14+
15+
public section
16+
set_option linter.missingDocs true -- keep it documented
17+
18+
namespace Lean.Linter.Coe
19+
20+
/--
21+
`set_option linter.deprecatedCoercions true` enables a linter emitting warnings on deprecated
22+
coercions.
23+
-/
24+
register_builtin_option linter.deprecatedCoercions : Bool := {
25+
defValue := true
26+
descr := "Validate that no deprecated coercions are used."
27+
}
28+
29+
/--
30+
Checks whether the "deprecated coercions" linter is enabled.
31+
-/
32+
def shouldWarnOnDeprecatedCoercions [Monad m] [MonadOptions m] : m Bool :=
33+
return (← getOptions).get linter.deprecatedCoercions.name true
34+
35+
/-- A list of coercion names that must not be used in core. -/
36+
def coercionsBannedInCore : Array Name := #[``optionCoe, ``instCoeSubarrayArray]
37+
38+
/-- Validates that no coercions are used that are either deprecated or are banned in core. -/
39+
def coeLinter : Linter where
40+
run := fun _ => do
41+
let mainModule ← getMainModule
42+
let isCoreModule := mainModule = `lean.run.linterCoe ∨ (mainModule.getRoot ∈ [`Init, `Std])
43+
let shouldWarnOnDeprecated := getLinterValue linter.deprecatedCoercions (← getLinterOptions)
44+
let trees ← Elab.getInfoTrees
45+
for tree in trees do
46+
tree.visitM' (m := Elab.Command.CommandElabM) (fun _ info _ => do
47+
match info with
48+
| .ofCustomInfo ci =>
49+
if let some trace := ci.value.get? Elab.Term.CoeExpansionTrace then
50+
for coeDecl in trace.expandedCoeDecls do
51+
if isCoreModule && coeDecl ∈ coercionsBannedInCore then
52+
logWarningAt ci.stx m!"This term uses the coercion `{coeDecl}`, which is banned in Lean's core library."
53+
if shouldWarnOnDeprecated then
54+
let some attr := deprecatedAttr.getParam? (← getEnv) coeDecl | pure ()
55+
logLint linter.deprecatedCoercions ci.stx <| .tagged ``deprecatedAttr <|
56+
m!"This term uses the deprecated coercion `{.ofConstName coeDecl true}`."
57+
| _ => pure ()
58+
return true) (fun _ _ _ _ => return)
59+
60+
builtin_initialize addLinter coeLinter
61+
62+
end Lean.Linter.Coe

src/Lean/Meta/Coe.lean

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ private partial def recProjTarget (e : Expr) (nm : Name := e.getAppFn.constName!
4040
else
4141
return nm
4242

43-
/-- Expand coercions occurring in `e` -/
44-
partial def expandCoe (e : Expr) : MetaM Expr :=
43+
/--
44+
Expands coercions occurring in `e` and return the result together with a list of applied
45+
`Coe` instances.
46+
-/
47+
partial def expandCoe (e : Expr) : MetaM (Expr × List Name) := StateT.run (s := ([] : List Name)) do
4548
withReducibleAndInstances do
4649
transform e fun e => do
4750
let f := e.getAppFn
@@ -55,8 +58,18 @@ partial def expandCoe (e : Expr) : MetaM Expr :=
5558
should still appear after unfolding (unless there are unused variables in the instances).
5659
-/
5760
recordExtraModUseFromDecl (isMeta := false) (← recProjTarget e)
58-
if let some e ← unfoldDefinition? e then
59-
return .visit e.headBeta
61+
if let some e' ← unfoldDefinition? e then
62+
/-
63+
If the unfolded coercion is an application of `Coe.coe` and its third argument is
64+
an application of a constant, record this constant's name.
65+
-/
66+
if declName = ``Coe.coe then
67+
if let some inst := e.getAppArgs[2]? then
68+
let g := inst.getAppFn
69+
if g.isConst then
70+
let instName := g.constName!
71+
StateT.set (instName :: (← StateT.get))
72+
return .visit e'.headBeta
6073
return .continue
6174

6275
register_builtin_option autoLift : Bool := {
@@ -65,20 +78,27 @@ register_builtin_option autoLift : Bool := {
6578
}
6679

6780
/-- Coerces `expr` to `expectedType` using `CoeT`. -/
68-
def coerceSimple? (expr expectedType : Expr) : MetaM (LOption Expr) := do
81+
def coerceSimpleRecordingNames? (expr expectedType : Expr) : MetaM (LOption (Expr × List Name)) := do
6982
let eType ← inferType expr
7083
let u ← getLevel eType
7184
let v ← getLevel expectedType
7285
let coeTInstType := mkAppN (mkConst ``CoeT [u, v]) #[eType, expr, expectedType]
7386
match ← trySynthInstance coeTInstType with
7487
| .some inst =>
7588
let result ← expandCoe (mkAppN (mkConst ``CoeT.coe [u, v]) #[eType, expr, expectedType, inst])
76-
unless ← isDefEq (← inferType result) expectedType do
77-
throwError "Could not coerce{indentExpr expr}\nto{indentExpr expectedType}\ncoerced expression has wrong type:{indentExpr result}"
89+
unless ← isDefEq (← inferType result.1) expectedType do
90+
throwError "Could not coerce{indentExpr expr}\nto{indentExpr expectedType}\ncoerced expression has wrong type:{indentExpr result.1}"
7891
return .some result
7992
| .undef => return .undef
8093
| .none => return .none
8194

95+
/-- Coerces `expr` to `expectedType` using `CoeT`. -/
96+
def coerceSimple? (expr expectedType : Expr) : MetaM (LOption Expr) := do
97+
match ← coerceSimpleRecordingNames? expr expectedType with
98+
| .some (result, _) => return .some result
99+
| .none => return .none
100+
| .undef => return .undef
101+
82102
/-- Coerces `expr` to a function type. -/
83103
def coerceToFunction? (expr : Expr) : MetaM (Option Expr) := do
84104
-- constructing expression manually because mkAppM wouldn't assign universe mvars
@@ -87,7 +107,7 @@ def coerceToFunction? (expr : Expr) : MetaM (Option Expr) := do
87107
let v ← mkFreshLevelMVar
88108
let γ ← mkFreshExprMVar (← mkArrow α (mkSort v))
89109
let .some inst ← trySynthInstance (mkApp2 (.const ``CoeFun [u,v]) α γ) | return none
90-
let expanded ← expandCoe (mkApp4 (.const ``CoeFun.coe [u,v]) α γ inst expr)
110+
let (expanded, _) ← expandCoe (mkApp4 (.const ``CoeFun.coe [u,v]) α γ inst expr)
91111
unless (← whnf (← inferType expanded)).isForall do
92112
throwError m!"Failed to coerce{indentExpr expr}\nto a function: After applying `CoeFun.coe`, result is still not a function{indentExpr expanded}"
93113
++ .hint' m!"This is often due to incorrect `CoeFun` instances; the synthesized instance was{indentExpr inst}"
@@ -101,7 +121,7 @@ def coerceToSort? (expr : Expr) : MetaM (Option Expr) := do
101121
let v ← mkFreshLevelMVar
102122
let β ← mkFreshExprMVar (mkSort v)
103123
let .some inst ← trySynthInstance (mkApp2 (.const ``CoeSort [u,v]) α β) | return none
104-
let expanded ← expandCoe (mkApp4 (.const ``CoeSort.coe [u,v]) α β inst expr)
124+
let (expanded, _) ← expandCoe (mkApp4 (.const ``CoeSort.coe [u,v]) α β inst expr)
105125
unless (← whnf (← inferType expanded)).isSort do
106126
throwError m!"Failed to coerce{indentExpr expr}\nto a type: After applying `CoeSort.coe`, result is still not a type{indentExpr expanded}"
107127
++ .hint' m!"This is often due to incorrect `CoeSort` instances; the synthesized instance was{indentExpr inst}"
@@ -190,7 +210,10 @@ def coerceMonadLift? (e expectedType : Expr) : MetaM (Option Expr) := do
190210
let saved ← saveState
191211
if (← isDefEq m n) then
192212
let some monadInst ← isMonad? n | restoreState saved; return none
193-
try expandCoe (← mkAppOptM ``Lean.Internal.coeM #[m, α, β, none, monadInst, e]) catch _ => restoreState saved; return none
213+
try
214+
let (result, _) ← expandCoe (← mkAppOptM ``Lean.Internal.coeM #[m, α, β, none, monadInst, e])
215+
pure result
216+
catch _ => restoreState saved; return none
194217
else if autoLift.get (← getOptions) then
195218
try
196219
-- Construct lift from `m` to `n`
@@ -217,7 +240,7 @@ def coerceMonadLift? (e expectedType : Expr) : MetaM (Option Expr) := do
217240
let v ← getLevel β
218241
let coeTInstType := Lean.mkForall `a BinderInfo.default α <| mkAppN (mkConst ``CoeT [u, v]) #[α, mkBVar 0, β]
219242
let .some coeTInstVal ← trySynthInstance coeTInstType | return none
220-
let eNew ← expandCoe (mkAppN (Lean.mkConst ``Lean.Internal.liftCoeM [u_1, u_2, u_3]) #[m, n, α, β, monadLiftVal, coeTInstVal, monadInst, e])
243+
let (eNew, _) ← expandCoe (mkAppN (Lean.mkConst ``Lean.Internal.liftCoeM [u_1, u_2, u_3]) #[m, n, α, β, monadLiftVal, coeTInstVal, monadInst, e])
221244
let eNewType ← inferType eNew
222245
unless (← isDefEq expectedType eNewType) do return none
223246
return some eNew -- approach 3 worked
@@ -227,17 +250,34 @@ def coerceMonadLift? (e expectedType : Expr) : MetaM (Option Expr) := do
227250
else
228251
return none
229252

230-
/-- Coerces `expr` to the type `expectedType`.
231-
Returns `.some coerced` on successful coercion,
253+
/--
254+
Coerces `expr` to the type `expectedType`.
255+
Returns `.some (coerced, appliedCoeDecls)` on successful coercion,
232256
`.none` if the expression cannot by coerced to that type,
233-
or `.undef` if we need more metavariable assignments. -/
234-
def coerce? (expr expectedType : Expr) : MetaM (LOption Expr) := do
257+
or `.undef` if we need more metavariable assignments.
258+
259+
`appliedCoeDecls` is a list of names representing the names of the `Coe` instances that were
260+
applied.
261+
-/
262+
def coerceCollectingNames? (expr expectedType : Expr) : MetaM (LOption (Expr × List Name)) := do
235263
if let some lifted ← coerceMonadLift? expr expectedType then
236-
return .some lifted
264+
return .some (lifted, [])
237265
if (← whnfR expectedType).isForall then
238266
if let some fn ← coerceToFunction? expr then
239267
if ← isDefEq (← inferType fn) expectedType then
240-
return .some fn
241-
coerceSimple? expr expectedType
268+
return .some (fn, [])
269+
coerceSimpleRecordingNames? expr expectedType
270+
271+
/--
272+
Coerces `expr` to the type `expectedType`.
273+
Returns `.some coerced` on successful coercion,
274+
`.none` if the expression cannot by coerced to that type,
275+
or `.undef` if we need more metavariable assignments.
276+
-/
277+
def coerce? (expr expectedType : Expr) : MetaM (LOption Expr) := do
278+
match ← coerceCollectingNames? expr expectedType with
279+
| .some (result, _) => return .some result
280+
| .none => return .none
281+
| .undef => return .undef
242282

243283
end Lean.Meta

stage0/src/stdlib_flags.h

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

3+
// dear CI, please do a stage0 update after merging my PR so that the coercion linter becomes active
34
namespace lean {
45
options get_default_options() {
56
options opts;

tests/lean/binopInfoTree.lean.expected.out

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ fun n m l => ↑n + (↑m + ↑l) : Nat → Nat → Nat → Int
5555
===>
5656
binop% HAdd.hAdd✝ n (m +' l)
5757
• [Term] ↑n + (↑m + ↑l) : Int @ ⟨7, 29⟩†-⟨7, 41⟩† @ Lean.Elab.Term.Op.elabBinOp
58+
• [CustomInfo(Lean.Elab.Term.CoeExpansionTrace)]
59+
• [CustomInfo(Lean.Elab.Term.CoeExpansionTrace)]
60+
• [CustomInfo(Lean.Elab.Term.CoeExpansionTrace)]
5861
• [Term] ↑n + (↑m + ↑l) : Int @ ⟨7, 29⟩†-⟨7, 41⟩†
5962
• [Completion-Id] HAdd.hAdd✝ : none @ ⟨7, 29⟩†-⟨7, 41⟩†
6063
• [Term] n : Nat @ ⟨7, 29⟩-⟨7, 30⟩ @ Lean.Elab.Term.elabIdent

0 commit comments

Comments
 (0)