Skip to content

Commit 42de364

Browse files
authored
fix: over-specialized backward rules in vcgen's spec rule cache (#14431)
This PR fixes `vcgen` failing with `Failed to apply rule` when the same equality spec matches two different programs within one run, e.g. the equations of a recursive function registered via `vcgen [f]`: the cached backward rule was specialized to the first matched program and could not be applied to the next one. The rule built from an equality spec unified the equation's LHS with the goal's program, baking the first program's arguments and dictionaries into a rule that is cached per spec theorem, `WP` instance and excess argument count. Rule construction now treats equality specs like `⊑ wp` specs: it pins only what the cache key determines, namely the monad, the value and predicate types and the `WP` instance, and leaves the equation's remaining variables, including instance binders, as schematic rule parameters that applying the rule binds against the concrete goal program. Dictionary projections this substitution exposes in the premise program are reduced by the existing head-reduction step before the next spec lookup, which also makes the construction-time dictionary synthesis and projection reduction unnecessary.
1 parent 5e4e690 commit 42de364

2 files changed

Lines changed: 56 additions & 29 deletions

File tree

src/Lean/Elab/Tactic/Do/Internal/VCGen/RuleConstruction.lean

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ public import Lean.Elab.Tactic.Do.Internal.VCGen.Reduce
1212
public import Lean.Elab.Tactic.Do.Internal.VCGen.SpecDB
1313
public import Lean.Meta.Sym.Apply
1414
public import Lean.Meta.Sym.Util
15-
import Lean.Meta.WHNF
1615

1716
open Lean Meta Elab Tactic Sym
1817
open Lean.Elab.Tactic.Do.Internal.SpecAttr
@@ -269,45 +268,34 @@ Normalize an instantiated equality spec `lhs = rhs` (both of type `info.M α`) t
269268
270269
The schematic `Q`/`E` make the postcondition and exception-postcondition VCs collapse in
271270
`mkSpecBackwardProof`, so the resulting rule rewrites a `wp lhs` goal to a `wp rhs` premise, matching
272-
the equational behaviour of a simp spec. Leftover dictionary metavariables (abstract-monad equations
273-
such as `Spec.UnfoldLift.get`) are synthesized first, and dictionary projections in `rhs` are reduced
274-
so the RHS exposes the actual operation (e.g. `MonadState.modifyGet`'s RHS `inst.modifyGet` reduces to
275-
`MonadStateOf.modifyGet`). Reducing, rather than folding the projection, is essential: folding would
276-
turn it back into the keyed head `MonadState.modifyGet` and the rewrite would loop.
271+
the equational behaviour of a simp spec. Only the monad, the value type and the `WP` instance are
272+
pinned from the goal, mirroring how `tryMkBackwardRuleFromSpec` treats `⊑ wp` specs; the equation's
273+
remaining variables, including instance binders, stay schematic and become rule parameters that
274+
applying the rule binds against the concrete goal program. Dictionary projections this substitution
275+
exposes in the premise program (e.g. for class projection unfold equations like
276+
`MonadState.modifyGet.eq_1`) are reduced by `wpHeadReduce?` before the next spec lookup.
277277
-/
278-
private def eqSpecToWp? (info : WPApp) (xs : Array Expr) (eqPrf eqType : Expr) :
278+
private def eqSpecToWp? (info : WPApp) (eqPrf eqType : Expr) :
279279
OptionT MetaM (Expr × Expr) := do
280-
let_expr Eq eqα lhs rhs := eqType
280+
let_expr Eq eqα _lhs _rhs := eqType
281281
| throwError "simp spec is not an equation: {eqType}"
282282
-- Recover the value type `α` and confirm the equation is in the goal's monad. `α` is typed at the
283283
-- monad's domain sort so the equation's element type stays well-formed.
284284
let α ← mkFreshExprMVar (← inferType info.M).bindingDomain!
285285
guard <| ← isDefEqGuarded eqα (mkApp info.M α)
286-
-- Pin the schematic instance and state metavariables by unifying the equation's LHS with the goal's
287-
-- concrete program, so dictionary projections in `rhs` reduce against the real instance.
288-
let _ ← show MetaM Bool from commitWhen <| isDefEqGuarded lhs info.prog
289-
-- Synthesize leftover dictionary metavariables (e.g. for an abstract-monad lift equation, whose LHS
290-
-- does not unify with a concrete program) so the projections in `rhs` reduce against instances.
291-
for x in xs do
292-
if x.isMVar && !(← x.mvarId!.isAssigned) then
293-
try x.mvarId!.assign (← Meta.synthInstance (← Meta.inferType x))
294-
catch _ => pure ()
295-
-- Reduce dictionary projections to expose the actual operation VCGen continues on.
296-
let rhs ← show MetaM Expr from Meta.transform rhs (pre := fun e => do
297-
if let .proj .. := e then
298-
if let some r ← withDefault <| Meta.reduceProj? e then return .done r
299-
return .continue)
286+
-- Reusing the goal's `WP` instance below pins the program and value types it is typed at. That
287+
-- unification must happen at the current metavariable depth: `mkAppOptM` raises the depth, so the
288+
-- equation's type metavariables are read-only there.
289+
guard <| ← isDefEqGuarded (mkApp info.M α) info.Prog
300290
-- `post`/`epost` are schematic metavariables (their VCs collapse downstream).
301291
let post ← mkFreshExprMVar (userName := `Q) (← mkArrow α info.Pred)
302292
let epost ← mkFreshExprMVar (userName := `E) info.EPred
303-
-- Cast to the reduced RHS so the resulting `wp` rewrites onto the exposed operation.
304-
let eqPrf ← mkExpectedTypeHint eqPrf (← mkEq lhs rhs)
305293
-- Pin the monad and assertion instances from the goal's `wp` arguments. Inferring the monad from
306294
-- the equation type alone would leave `m β =?= info.M γ` as an underdetermined flex-rigid problem,
307-
-- so non-monadic equations like `Option.getD.eq_1` would fail to unify. With `m` fixed, the value
308-
-- type is inferred from the equation proof.
295+
-- so non-monadic equations like `Option.getD.eq_1` would fail to unify.
309296
let specProof ← mkAppOptM ``Std.Internal.Do.wp_le_wp_of_eq <|
310-
(info.args.extract 0 7).map some ++ #[none, none, some eqPrf, some post, some epost]
297+
#[some (mkApp info.M α), some α] ++ (info.args.extract 2 7).map some
298+
++ #[none, none, some eqPrf, some post, some epost]
311299
return (specProof, ← instantiateMVars (← Meta.inferType specProof))
312300

313301
/--
@@ -326,11 +314,11 @@ same way.
326314
public def tryMkBackwardRuleFromSpec (specThm : SpecTheorem) (info : WPApp)
327315
(stateArgNames : Array Name := #[]) : OptionT MetaM BackwardRule := do
328316
-- Instantiate the spec theorem, creating metavars for all universally quantified params
329-
let (xs, _bs, specProof, specType) ← specThm.instantiate
317+
let (_xs, _bs, specProof, specType) ← specThm.instantiate
330318
-- Equality specs (the simp side of `@[spec]`) are normalized to `⊑ wp` form, then handled like
331319
-- any ordinary `⊑ wp` spec.
332320
let (specProof, specType) ←
333-
if specType.isAppOfArity ``Eq 3 then eqSpecToWp? info xs specProof specType
321+
if specType.isAppOfArity ``Eq 3 then eqSpecToWp? info specProof specType
334322
else pure (specProof, specType)
335323
let_expr PartialOrder.rel Pred' _cl' pre rhs := specType
336324
| throwError "target not a partial order ⊑ application {specType}"

tests/elab/vcgenSpecRuleCache.lean

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Std.Tactic.Do
2+
import Std.Internal.Do
3+
4+
/-!
5+
Regression test for `vcgen`'s spec backward-rule cache. The rule built from an equality spec must
6+
stay generic in the spec's value variables: the cache reuses the rule for every program matching
7+
the same spec theorem, so a rule specialized to the first matched program (e.g. `recf 3` below,
8+
whose unfolding produces the recursive call `recf 2` handled by the same equation) fails on the
9+
next one.
10+
-/
11+
12+
set_option mvcgen.warning false
13+
14+
def recf (n : Nat) : Id Nat :=
15+
match n with | 0 => pure 1 | n + 1 => recf n
16+
17+
def myid (a : Nat) : Id Nat :=
18+
pure a
19+
20+
def myid2 (a : α) : Id α :=
21+
pure a
22+
23+
section
24+
open Lean.Order Std.Internal.Do
25+
26+
-- The same equation `recf.eq_2` matches both `recf 3` and the recursive call `recf 2`.
27+
example : ⦃ True ⦄ recf 3fun r => r > 0 ⦄ := by
28+
vcgen [recf] with finish
29+
30+
-- The same equation `myid.eq_1` matches two calls with different arguments.
31+
example : ⦃ True ⦄ (do let x ← myid 3; let y ← myid 5; pure (x + y) : Id Nat) ⦃ fun r => r > 0 ⦄ := by
32+
vcgen [myid] with finish
33+
34+
-- The same equation `myid2.eq_1` matches calls at two different value types.
35+
example :
36+
⦃ True ⦄ (do let x ← myid2 3; let y ← myid2 "st"; pure (x + y.length) : Id Nat)
37+
fun r => r > 0 ⦄ := by
38+
vcgen [myid2] with finish
39+
end

0 commit comments

Comments
 (0)