Skip to content

Commit cdf4362

Browse files
sgraf812volodeyka
andcommitted
refactor: lift pure preconditions as soon as they arise
The solver lifts a pure precondition unconditionally when it sees one, as in #13978: an embedded `⌜φ⌝` before state-argument introduction, a bare proposition on the `Prop` lattice right after it. The lifted hypothesis is cached in `Scope.lastLiftedPre?`, and the targeted assumption step closes subsequent handoff VCs against it with a single defeq check, so no spec-rule sensitivity is needed; the rule construction returns to producing plain backward rules and the duplication guards before splits disappear. Co-authored-by: volodeyka <vovaglad00@gmail.com>
1 parent a497d5c commit cdf4362

7 files changed

Lines changed: 77 additions & 99 deletions

File tree

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,20 @@ public structure VCGen.Scope where
136136
specs : SpecTheorems
137137
/-- `__do_jp` fvars currently in scope. -/
138138
jps : FVarIdMap JumpSiteInfo := {}
139+
/-- The most recently lifted pure precondition. `tryLiftedHyp` closes handoff VCs against
140+
it without walking the local context. -/
141+
lastLiftedPre? : Option FVarId := none
139142
/-- Index of the next local declaration to consider for local specs. -/
140143
nextDeclIdx : Nat := 0
141144
deriving Inhabited
142145

143146
public structure VCGen.State where
144147
/--
145-
A cache mapping registered SpecThms to their backward rule to apply, paired with whether
146-
the rule has a premise besides the precondition VC that mentions parts of the program (in
147-
which case the caller lifts a pure goal precondition before applying the rule). The
148-
particular rule depends on the theorem name, the `WPMonad` instance and the number of
148+
A cache mapping registered SpecThms to their backward rule to apply.
149+
The particular rule depends on the theorem name, the `WPMonad` instance and the number of
149150
excess state arguments that the weakest precondition target is applied to.
150151
-/
151-
specBackwardRuleCache : Std.HashMap (Name × Expr × Nat) (BackwardRule × Bool) := {}
152+
specBackwardRuleCache : Std.HashMap (Name × Expr × Nat) BackwardRule := {}
152153
/--
153154
A cache mapping matchers to their splitting backward rule to apply.
154155
The particular rule depends on the matcher name, the monad and the number of excess state

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,17 @@ public def introStateArgs (preIsTop : Bool) (goal : MVarId) : VCGenM MVarId := d
4242
introsExcessArgs rule goal
4343

4444
/-- Introduce a pure precondition into the local context, leaving `⊤` as the residual
45-
precondition. `ofProp` selects the embedded form `⌜p⌝ ⊑ rhs` over the bare `Prop` form. -/
46-
public def introPre (ofProp : Bool) (goal : MVarId) : VCGenM MVarId := do
45+
precondition. `ofProp` selects the embedded form `⌜p⌝ ⊑ rhs` over the bare `Prop` form.
46+
Returns the new goal and the introduced hypothesis. -/
47+
public def introPre (ofProp : Bool) (goal : MVarId) : VCGenM (MVarId × FVarId) := do
4748
let rules := (← read).introRules
4849
let rule := if ofProp then rules.ofPropPreIntro else rules.propPreIntro
4950
let .goals [goal] ← rule.applyChecked goal
5051
| throwError "Failed to apply precondition intro rule to {goal}"
51-
introsHygienic goal
52+
let goal ← introsHygienic goal
53+
let some decl := (← goal.withContext getLCtx).lastDecl
54+
| throwError "Failed to intro the lifted precondition of {goal}"
55+
return (goal, decl.fvarId)
5256

5357
/--
5458
Reduce an `EPost.Cons.head` projection on the RHS of `pre ⊑ rhs` to the underlying component:

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ specs must check that their equation type is definitionally equal to `m α`.
3535
Cache key: `(proof key, instWP, excessArgs.size)`.
3636
-/
3737
public def mkBackwardRuleFromSpecCached (specThm : SpecTheorem) (info : WPInfo) :
38-
OptionT VCGenM (BackwardRule × Bool) := do
38+
OptionT VCGenM BackwardRule := do
3939
let key := (specThm.proof.key, info.instWP, info.excessArgs.size)
4040
let s := (← get).specBackwardRuleCache
4141
if let some rule := s[key]? then return rule
4242
let some rule ← withNewMCtxDepth <| match specThm.kind with
4343
| .triple => tryMkBackwardRuleFromSpec specThm info |>.run
44-
| .simp _ => ((·, false)) <$> tryMkBackwardRuleFromSimp specThm info |>.run
44+
| .simp _ => tryMkBackwardRuleFromSimp specThm info |>.run
4545
| failure
4646
modify fun st => { st with specBackwardRuleCache := st.specBackwardRuleCache.insert key rule }
4747
return rule

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

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ prf : ∀ (pre : Prop) (α : Type) (x : StateT Nat Id α) (β : Type)
368368
-/
369369
private def mkSpecBackwardProof
370370
(pre prog postSpec epostSpec specProof EPred : Expr) (ss ssTypes : Array Expr)
371-
(stateArgNames : Array Name := #[]) : SymM (AbstractMVarsResult × Bool) := do
371+
(stateArgNames : Array Name := #[]) : SymM AbstractMVarsResult := do
372372
/- we start with `pre ⊑ wp prog post epost` where
373373
1. `pre` represents the Lean expression for `pre`
374374
2. `prog`, `postSpec`, and `epostSpec` are the selected arguments of the spec's `wp` RHS
@@ -378,11 +378,9 @@ private def mkSpecBackwardProof
378378
let mut postAbstract := postSpec.consumeMData
379379
let mut epostAbstract := epostSpec.consumeMData
380380
let mut specApplied := specProof
381-
let mut hasContinuationVC := false
382381

383382
/- abstract concrete `post` if it is not already abstract -/
384383
unless postAbstract.isMVar do
385-
hasContinuationVC := true
386384
/- `α → Pred`: type of `post` -/
387385
let postTy ← Sym.inferType postSpec
388386
/- mvar `postAbstract` for new abstract `post` -/
@@ -421,7 +419,6 @@ private def mkSpecBackwardProof
421419
This proof DOES NOT have a `?epostImpl` premise -/
422420
specApplied ← mkAppM ``WPMonad.wp_econs_bot_le #[prog, postAbstract, epostAbstract, specApplied]
423421
else
424-
hasContinuationVC := true
425422
/- Decompose `epostSpec ⊑ epostAbstract` into per-component proofs
426423
using `EPost.Cons.mk_le` and `EPost.Nil.le` -/
427424
let hepost ← decomposeEPostRel EPred epostSpec epostAbstract stateArgNames
@@ -450,7 +447,7 @@ private def mkSpecBackwardProof
450447
/- use `PartialOrder.rel_trans` to compose the abstracted `pre` and the proof of the original theorem -/
451448
specApplied ← mkAppM ``PartialOrder.rel_trans #[specAbstract, specApplied]
452449

453-
return (← abstractMVars specApplied, hasContinuationVC)
450+
abstractMVars specApplied
454451

455452
/--
456453
Try to build a backward rule from a single spec theorem in `⊑` form.
@@ -464,7 +461,7 @@ Given a spec `pre ⊑ wp prog post epost` where the lattice type is
464461
`info.Pred = σ1 → ... → σn → Prop`
465462
-/
466463
public def tryMkBackwardRuleFromSpec (specThm : SpecTheorem) (info : WPInfo)
467-
(stateArgNames : Array Name := #[]) : OptionT SymM (BackwardRule × Bool) := do
464+
(stateArgNames : Array Name := #[]) : OptionT SymM BackwardRule := do
468465
-- Instantiate the spec theorem, creating metavars for all universally quantified params
469466
let (_xs, _bs, specProof, specType) ← specThm.instantiate
470467
let_expr PartialOrder.rel Pred' _cl' pre rhs := specType
@@ -481,12 +478,8 @@ public def tryMkBackwardRuleFromSpec (specThm : SpecTheorem) (info : WPInfo)
481478
let ty ← Sym.inferType info.excessArgs[i]
482479
ssTypes := ssTypes.push ty
483480
ss := ss.push <| ← mkFreshExprMVar (userName := stateArgNames[i]?.getD `s) ty
484-
let (res, hasContinuationVC) ← mkSpecBackwardProof pre prog postSpec epostSpec specProof info.EPred ss ssTypes stateArgNames
485-
let rule ← mkBackwardRuleFromExpr res.expr res.paramNames.toList
486-
-- The post and epost VCs never see the goal's precondition. When the rule has one, the
487-
-- caller lifts a pure goal precondition into the local context first; `tryLiftedHyp`
488-
-- closes the resulting handoff VCs against the lifted hypothesis.
489-
return (rule, hasContinuationVC)
481+
let res ← mkSpecBackwardProof pre prog postSpec epostSpec specProof info.EPred ss ssTypes stateArgNames
482+
mkBackwardRuleFromExpr res.expr res.paramNames.toList
490483

491484
/-! ## Equality spec rules
492485

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

Lines changed: 48 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,18 @@ private def tryRfl (goal : MVarId) (target pre rhs : Expr) : VCGenM (Option (Lis
122122
goal.assign (mkApp3 refl relArgs[0]! relArgs[1]! pre)
123123
return some []
124124

125-
/-- Strategy 5: close `pre ⊑ rhs` against the newest pure hypothesis in the local context,
126-
which is typically the precondition lifted just before the current spec rule was applied.
127-
This is one defeq check against one hypothesis, not an assumption search.
125+
/-- Strategy 5: close `pre ⊑ rhs` against the most recently lifted pure precondition,
126+
cached in `Scope.lastLiftedPre?`. This is one defeq check against one hypothesis, not an
127+
assumption search.
128128
129129
Goals whose RHS contains metavariables are skipped: the defeq check could otherwise assign
130130
a post abstraction from an unrelated hypothesis. -/
131-
private def tryLiftedHyp (goal : MVarId) (α pre rhs : Expr) : VCGenM (Option (List MVarId)) :=
131+
private def tryLiftedHyp (scope : VCGen.Scope) (goal : MVarId) (α pre rhs : Expr) :
132+
VCGenM (Option (List MVarId)) :=
132133
goal.withContext do
133134
if rhs.hasExprMVar then return none
134-
let some hyp ← (← getLCtx).findDeclRevM? (fun decl => do
135-
if decl.isImplementationDetail then return none
136-
else if ← Meta.isProp decl.type then return some decl
137-
else return none)
138-
| return none
135+
let some fvarId := scope.lastLiftedPre? | return none
136+
let some hyp := (← getLCtx).find? fvarId | return none
139137
let proof? ← match_expr rhs with
140138
| CompleteLattice.ofProp _l _inst φ => do
141139
if ← isDefEqS φ hyp.type then
@@ -155,25 +153,17 @@ private def tryLiftedHyp (goal : MVarId) (α pre rhs : Expr) : VCGenM (Option (L
155153
goal.assign proof
156154
return some []
157155

158-
/--
159-
Lift a non-trivial embedded pure precondition `⌜φ⌝` into the local context, leaving `⊤`
160-
as the residual precondition. Used before a decomposition step that would duplicate the
161-
precondition into several subgoals (a `⊓` or match split), and before applying a spec whose own
162-
precondition is `⊤` (whose precondition VC would otherwise discard the fact). Returns `none` when
163-
the precondition is not such a `⌜φ⌝`.
164-
-/
165-
private def liftPurePre? (goal : MVarId) (pre : Expr) : VCGenM (Option MVarId) := do
156+
/-- Strategy 6: lift an embedded pure precondition `⌜φ⌝` into the local context, leaving `⊤`
157+
as the residual precondition. Runs before state-argument introduction, which would otherwise
158+
leave `⌜φ⌝` applied to the introduced arguments. Returns the new goal and the hypothesis. -/
159+
private def tryOfPropPreIntro (goal : MVarId) (pre : Expr) : VCGenM (Option (MVarId × FVarId)) := do
166160
let_expr CompleteLattice.ofProp _l _inst φ := pre | return none
167161
if φ.isTrue then return none
168162
return some (← introPre (ofProp := true) goal)
169163

170-
/-- Strategy 6: decompose a supported lattice connective on the RHS, lifting a pure
171-
precondition first when the connective is `⊓` (which duplicates the precondition). -/
172-
private def tryLattice (goal : MVarId) (target pre rhs : Expr) (preIsTop : Bool) :
164+
/-- Strategy 8: decompose a supported lattice connective on the RHS. -/
165+
private def tryLattice (goal : MVarId) (target rhs : Expr) (preIsTop : Bool) :
173166
VCGenM (Option (List MVarId)) := do
174-
if rhs.isAppOf ``meet then
175-
if let some g ← liftPurePre? goal pre then
176-
return some [g]
177167
solveLatticeConnective? goal target rhs preIsTop
178168

179169
/-- Replace the program in `goal`'s target with `prog` (which must be definitionally equal). -/
@@ -213,16 +203,14 @@ private def tryWPLet (goal : MVarId) (target : Expr) (info : WPInfo) : VCGenM (O
213203
return some (← substOrHoistLet goal target info name type val body nondep
214204
info.prog.getAppRevArgs)
215205

216-
/-- Strategy 7b: split an `ite`/`dite`/match program (or iota-reduce a matcher with a concrete
217-
discriminant), lifting a pure precondition first since the split duplicates it into the alts. -/
218-
private def tryWPMatch (goal : MVarId) (target pre : Expr) (info : WPInfo) :
206+
/-- Strategy 7b: split an `ite`/`dite`/match program, or iota-reduce a matcher with a concrete
207+
discriminant. -/
208+
private def tryWPMatch (goal : MVarId) (target : Expr) (info : WPInfo) :
219209
VCGenM (Option (List MVarId)) := do
220210
let some splitInfo ← liftMetaM <| Lean.Elab.Tactic.Do.getSplitInfo? info.prog | return none
221211
if splitInfo matches .matcher .. then
222212
if let some prog ← liftMetaM <| withReducible <| reduceRecMatcher? info.prog then
223213
return some [← replaceProgDefEq goal target info (← shareCommonInc prog)]
224-
if let some g ← liftPurePre? goal pre then
225-
return some [g]
226214
let rule ← mkBackwardRuleForSplitCached splitInfo info
227215
let .goals goals ← rule.applyChecked goal m!"split rule for{indentExpr info.prog}"
228216
| throwError "Failed to apply split rule for {indentExpr info.prog}"
@@ -255,19 +243,15 @@ private def tryWPHeadReduce (goal : MVarId) (target : Expr) (info : WPInfo) :
255243
return some (← replaceProgDefEq goal target info prog)
256244

257245
/-- Strategy 7e: look up a registered `@[spec]` theorem (triple or simp) for the program head
258-
and apply its cached backward rule.
259-
260-
When the rule has a premise besides the precondition VC that mentions parts of the program,
261-
a pure goal precondition is lifted into the local context first. Such premises never see the
262-
goal's precondition, so the lift is what keeps the fact available in them. -/
263-
private def applySpec (scope : VCGen.Scope) (goal : MVarId) (target pre α : Expr)
264-
(preIsTop : Bool) (info : WPInfo) : VCGenM SolveResult := do
246+
and apply its cached backward rule. -/
247+
private def applySpec (scope : VCGen.Scope) (goal : MVarId) (target : Expr) (info : WPInfo) :
248+
VCGenM SolveResult := do
265249
trace[Elab.Tactic.Do.vcgen] "Applying a spec for {info.prog}. Excess args: {info.excessArgs}"
266250
match ← SpecTheorems.findSpecs scope.specs info.prog with
267251
| .error thms => return .noSpecFoundForProgram info.prog info.m thms
268252
| .ok thm =>
269253
trace[Elab.Tactic.Do.vcgen] "Spec for {info.prog}: {thm.proof}"
270-
let some (rule, needsPureLift)
254+
let some rule ←
271255
try
272256
mkBackwardRuleFromSpecCached thm info |>.run
273257
catch ex =>
@@ -277,11 +261,6 @@ private def applySpec (scope : VCGen.Scope) (goal : MVarId) (target pre α : Exp
277261
Pred:{indentExpr info.Pred}\n\
278262
excessArgs: {info.excessArgs}"
279263
| return .noSpecFoundForProgram info.prog info.m #[thm]
280-
if needsPureLift && !preIsTop then
281-
if let some g ← liftPurePre? goal pre then
282-
return .goals scope [g]
283-
if α.isProp then
284-
return .goals scope [← introPre (ofProp := false) goal]
285264
let .goals goals ← rule.applyChecked goal m!"spec rule for{indentExpr info.prog}"
286265
| do
287266
let ruleType ← Meta.inferType rule.expr
@@ -314,23 +293,23 @@ The function performs the following steps in order:
314293
2. **Target-let handling**: zeta-substitute duplicable top-level lets, otherwise introduce them.
315294
3. **Triple unfolding**: If the target is `⦃P⦄ x ⦃Q; E⦄`, unfold into `P ⊑ wp x Q E`.
316295
4. **Syntactic rfl**: close `pre ⊑ rhs` by `PartialOrder.rel_refl` when both sides unify.
317-
5. **Lifted-hypothesis discharge**: close `pre ⊑ rhs` with the newest pure hypothesis,
318-
typically the precondition lifted before the previous spec application.
319-
6. **State-argument introduction**: If the lattice carrier is a function type
296+
5. **Lifted-hypothesis discharge**: close `pre ⊑ rhs` against the most recently lifted
297+
precondition, cached in `Scope.lastLiftedPre?`.
298+
6. **Embedded pure precondition introduction**: lift a `⌜φ⌝` precondition into the local
299+
context, before state-argument introduction would apply it to the introduced arguments.
300+
7. **State-argument introduction**: If the lattice carrier is a function type
320301
`σ₁ → ... → σₙ → Base`, introduce all excess state arguments.
321-
7. **EPost projection reduction**: reduce an `EPost.Cons.head` RHS to the projected component.
322-
8. **Lattice decomposition**: decompose `⊓`, `⇨`, `⌜p⌝` and `⊤` RHS connectives.
323-
9. **WP decomposition**: when the RHS is `wp e post epost s₁ ... sₙ`, in order:
324-
hoist/zeta program-head lets, split `ite`/`dite`/match, zeta-unfold fvar program heads,
325-
reduce projection heads, and finally apply a registered `@[spec]` theorem.
326-
10. **Pure precondition introduction** (last resort): introduce a bare pure precondition (on
327-
the `Prop` lattice) so the residual VC has the form `⊤ ⊑ φ`.
328-
329-
Pure preconditions are deliberately introduced *late* (step 10, and inside steps 8/9 right
330-
before a `⊓` or match split duplicates them, or before a spec rule with a post or epost VC
331-
loses them): introducing them eagerly would turn reflexivity-closable verification conditions
332-
such as `Q x ⊑ wp (pure x) Q E` into goals that need an assumption search. The lifted fact is
333-
handed back to the handoff VCs by step 5.
302+
8. **Bare pure precondition introduction**: on the `Prop` lattice, lift a non-`⊤`
303+
precondition into the local context.
304+
9. **EPost projection reduction**: reduce an `EPost.Cons.head` RHS to the projected component.
305+
10. **Lattice decomposition**: decompose `⊓`, `⇨`, `⌜p⌝` and `⊤` RHS connectives.
306+
11. **WP decomposition**: when the RHS is `wp e post epost s₁ ... sₙ`, in order:
307+
hoist/zeta program-head lets, split `ite`/`dite`/match, zeta-unfold fvar program heads,
308+
reduce projection heads, and finally apply a registered `@[spec]` theorem.
309+
310+
Pure preconditions are lifted as soon as they arise (steps 6 and 8). The lifted hypothesis is
311+
cached in `Scope.lastLiftedPre?`, and step 5 closes the handoff VCs of subsequent spec
312+
applications against it with a single defeq check.
334313
-/
335314
public def solve (scope : VCGen.Scope) (goal : MVarId) : VCGenM SolveResult := goal.withContext do
336315
if ← outOfFuel then return .stop
@@ -354,19 +333,25 @@ public def solve (scope : VCGen.Scope) (goal : MVarId) : VCGenM SolveResult := g
354333
let preIsTop := pre.isAppOf ``Lean.Order.top && pre.getAppNumArgs == 2
355334

356335
if let some gs ← tryRfl goal target pre rhs then return .goals scope gs
357-
if let some gs ← tryLiftedHyp goal α pre rhs then return .goals scope gs
336+
if let some gs ← tryLiftedHyp scope goal α pre rhs then return .goals scope gs
358337

359-
-- Phase 2: introduce excess state arguments and reduce EPost projections, so that the RHS
360-
-- exposes either a lattice connective or a `wp` application.
338+
-- Phase 2: lift pure preconditions as they arise, introduce excess state arguments, and
339+
-- reduce EPost projections, so that the RHS exposes a lattice connective or a `wp`
340+
-- application over a `⊤` precondition.
341+
if let some (g, h) ← tryOfPropPreIntro goal pre then
342+
return .goals { scope with lastLiftedPre? := some h } [g]
361343
if α.isForall then return .goals scope [← introStateArgs preIsTop goal]
344+
if !preIsTop && α.isProp then
345+
let (g, h) ← introPre (ofProp := false) goal
346+
return .goals { scope with lastLiftedPre? := some h } [g]
362347
if let some g ← reduceEPostHead? goal target α inst pre rhs then return .goals scope [g]
363348

364349
-- Collect new local specs before any strategy that may emit multiple subgoals
365350
-- (`tryWPMatch`, `tryLattice`) or apply a registered spec (`applySpec`).
366351
let scope ← scope.collectLocalSpecs goal
367352

368353
-- Phase 3: lattice connective decomposition.
369-
if let some gs ← tryLattice goal target pre rhs preIsTop then return .goals scope gs
354+
if let some gs ← tryLattice goal target rhs preIsTop then return .goals scope gs
370355

371356
-- Phase 4: wp decomposition. The program-shape steps below all consume one unit of fuel
372357
-- (the `stepLimit` config option) when they make progress.
@@ -375,7 +360,7 @@ public def solve (scope : VCGen.Scope) (goal : MVarId) : VCGenM SolveResult := g
375360
if let some g ← tryWPLet goal target info then
376361
VCGen.burnOne
377362
return .goals scope [g]
378-
if let some gs ← tryWPMatch goal target pre info then
363+
if let some gs ← tryWPMatch goal target info then
379364
VCGen.burnOne
380365
return .goals scope gs
381366
if let some g ← tryWPFVarZeta goal target info then
@@ -389,14 +374,9 @@ public def solve (scope : VCGen.Scope) (goal : MVarId) : VCGenM SolveResult := g
389374
let f := info.prog.getAppFn
390375
if f.isConst || f.isFVar then
391376
VCGen.burnOne
392-
return ← applySpec scope goal target pre α preIsTop info
377+
return ← applySpec scope goal target info
393378
return .noStrategyForProgram info.prog
394379

395-
-- Phase 5 (last resort): introduce a bare pure precondition so the residual VC
396-
-- has the form `⊤ ⊑ φ`.
397-
if !preIsTop && α.isProp then
398-
return .goals scope [← introPre (ofProp := false) goal]
399-
400380
return .noProgramOrLatticeFoundInTarget rhs
401381

402382
end VCGen

0 commit comments

Comments
 (0)