fix: include ignoreNoncomputable in LCNF cache key (#13384)

This PR fixes a compiler panic when a structure constructor receives a
noncomputable instance as an instance-implicit argument.

The LCNF translation first visits the instance in an irrelevant position
(type parameter) where `ignoreNoncomputable` is `true`, caches the
result, and then reuses that cached entry in a relevant position,
bypassing `checkComputable`. Adding `ignoreNoncomputable` to the cache
key ensures the two contexts do not share cache entries.

Fixes #13371
This commit is contained in:
Sebastian Ullrich 2026-04-13 11:27:25 +02:00 committed by GitHub
parent 2b8c273687
commit 0d7e76ea88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 29 additions and 3 deletions

View file

@ -213,11 +213,22 @@ structure Context where
-/
expectedType : Option Expr
/--
Key for the LCNF translation cache. `ignoreNoncomputable` is part of the key
because entries cached in irrelevant positions skip the `checkComputable`
check and must not be reused in relevant positions.
-/
structure CacheKey where
expr : Expr
expectedType? : Option Expr
ignoreNoncomputable : Bool
deriving BEq, Hashable
structure State where
/-- Local context containing the original Lean types (not LCNF ones). -/
lctx : LocalContext := {}
/-- Cache from Lean regular expression to LCNF argument. -/
cache : PHashMap (Expr × Option Expr) (Arg .pure) := {}
cache : PHashMap CacheKey (Arg .pure) := {}
/--
Determines whether caching has been disabled due to finding a use of
a constant marked with `never_extract`.
@ -473,7 +484,9 @@ partial def toLCNF (e : Expr) (eType : Expr) : CompilerM (Code .pure) := do
where
visitCore (e : Expr) : M (Arg .pure) := withIncRecDepth do
let eType? := (← read).expectedType
if let some arg := (← get).cache.find? (e, eType?) then
let ignoreNoncomputable := (← read).ignoreNoncomputable
let key : CacheKey := { expr := e, expectedType? := eType?, ignoreNoncomputable }
if let some arg := (← get).cache.find? key then
return arg
let r : Arg .pure ← match e with
| .app .. => visitApp e
@ -485,7 +498,7 @@ where
| .lit lit => visitLit lit
| .fvar fvarId => if (← get).toAny.contains fvarId then pure .erased else pure (.fvar fvarId)
| .forallE .. | .mvar .. | .bvar .. | .sort .. => unreachable!
modify fun s => if s.shouldCache then { s with cache := s.cache.insert (e, eType?) r } else s
modify fun s => if s.shouldCache then { s with cache := s.cache.insert key r } else s
return r
visit (e : Expr) : M (Arg .pure) := withIncRecDepth do

13
tests/elab/13371.lean Normal file
View file

@ -0,0 +1,13 @@
-- Regression test for https://github.com/leanprover/lean4/issues/13371
-- The LCNF cache must distinguish entries cached in irrelevant positions
-- (where noncomputable uses are tolerated) from those in relevant positions.
structure Hom [Inhabited Unit] where
toFun : Unit
noncomputable instance inst : Inhabited Unit := ⟨Classical.choice ⟨()⟩⟩
/--
error: failed to compile definition, consider marking it as 'noncomputable' because it depends on 'inst', which is 'noncomputable'
-/
#guard_msgs in
def test : Hom := ⟨default⟩