Skip to content

Commit a497d5c

Browse files
sgraf812volodeyka
andcommitted
refactor: discharge handoff VCs against the lifted hypothesis
The solver gains a targeted assumption step: after reflexivity, it tries to close `pre ⊑ rhs` against the newest pure hypothesis in the local context, which is typically the precondition lifted just before the previous spec application. This is one defeq check against one hypothesis, not an assumption search. With it, the lift trigger returns to its simple form: lift a pure precondition whenever the spec rule has a post or epost VC, since those premises never see the goal's precondition. Chains of specs with concrete pre/postconditions close under plain mvcgen' at their previous cost, loop invariants need not restate pure facts, and goals whose RHS contains metavariables are skipped so the check cannot assign post abstractions. Also renames repeatAndRfl to solveTrivialConjuncts. Co-authored-by: volodeyka <vovaglad00@gmail.com>
1 parent dc6864b commit a497d5c

7 files changed

Lines changed: 69 additions & 40 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ The `mvcgen'` tactic, split across the modules above.
2626
- `VCGen.RuleConstruction` — SymM rule constructors from spec/simp/split info.
2727
- `VCGen.Context` — `VCGenM`, its `Context`/`State`, the bundle of pre-built rules.
2828
- `VCGen.EPost` — exception-postcondition decomposition helpers.
29-
- `VCGen.Util` — generic VCGenM helpers (`introsHygienic`, `simpGoalTelescope`, `repeatAndRfl`).
29+
- `VCGen.Util` — generic VCGenM helpers (`introsHygienic`, `simpGoalTelescope`, `solveTrivialConjuncts`).
3030
- `VCGen.RuleCache` — VCGenM cache wrappers around the SymM rule constructors.
3131
- `VCGen.Entails` — entailment-shaped goal decomposition (Triple unfolding, state/precondition intro, EPost and lattice steps).
3232
- `VCGen.Solve` — the main `solve` step / `SolveResult`.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public structure VCGen.Context where
100100
/-- User-customizable simp methods used to pre-simplify hypotheses. -/
101101
hypSimpMethods : Option Sym.Simp.Methods := none
102102
/-- The `trivial` config option: when `true` (default), `Driver.emitVC` runs
103-
`repeatAndRfl` to collapse trivial `And.intro` chains; when `false`, the goal is
103+
`solveTrivialConjuncts` to collapse trivial `And.intro` chains; when `false`, the goal is
104104
emitted as-is. -/
105105
trivial : Bool := true
106106
/-- The `jp` config option: when `true`, `tryLetIntro` recognises `__do_jp` lets

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,11 @@ public def emitVC (goal : Grind.Goal) : VCGenM Unit := do
9393
goal := { goal with mvarId }
9494
goal ← processHypotheses goal
9595
if goal.inconsistent then return
96-
-- `trivial`: when false, skip `repeatAndRfl` (which collapses And-chains via rfl);
96+
-- `trivial`: when false, skip `solveTrivialConjuncts` (which collapses And-chains via rfl);
9797
-- emit the goal as-is.
9898
let mvarId ←
9999
if (← read).trivial then
100-
let some mvarId ← repeatAndRfl goal.mvarId | return
100+
let some mvarId ← solveTrivialConjuncts goal.mvarId | return
101101
pure mvarId
102102
else
103103
pure goal.mvarId

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

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -378,13 +378,11 @@ private def mkSpecBackwardProof
378378
let mut postAbstract := postSpec.consumeMData
379379
let mut epostAbstract := epostSpec.consumeMData
380380
let mut specApplied := specProof
381-
let progMVars ← getMVars prog
382-
let mut premiseHasProg := false
381+
let mut hasContinuationVC := false
383382

384383
/- abstract concrete `post` if it is not already abstract -/
385384
unless postAbstract.isMVar do
386-
unless premiseHasProg do
387-
premiseHasProg := (← getMVars postSpec).any progMVars.contains
385+
hasContinuationVC := true
388386
/- `α → Pred`: type of `post` -/
389387
let postTy ← Sym.inferType postSpec
390388
/- mvar `postAbstract` for new abstract `post` -/
@@ -423,8 +421,7 @@ private def mkSpecBackwardProof
423421
This proof DOES NOT have a `?epostImpl` premise -/
424422
specApplied ← mkAppM ``WPMonad.wp_econs_bot_le #[prog, postAbstract, epostAbstract, specApplied]
425423
else
426-
unless premiseHasProg do
427-
premiseHasProg := (← getMVars epostSpec).any progMVars.contains
424+
hasContinuationVC := true
428425
/- Decompose `epostSpec ⊑ epostAbstract` into per-component proofs
429426
using `EPost.Cons.mk_le` and `EPost.Nil.le` -/
430427
let hepost ← decomposeEPostRel EPred epostSpec epostAbstract stateArgNames
@@ -453,7 +450,7 @@ private def mkSpecBackwardProof
453450
/- use `PartialOrder.rel_trans` to compose the abstracted `pre` and the proof of the original theorem -/
454451
specApplied ← mkAppM ``PartialOrder.rel_trans #[specAbstract, specApplied]
455452

456-
return (← abstractMVars specApplied, premiseHasProg)
453+
return (← abstractMVars specApplied, hasContinuationVC)
457454

458455
/--
459456
Try to build a backward rule from a single spec theorem in `⊑` form.
@@ -469,7 +466,7 @@ Given a spec `pre ⊑ wp prog post epost` where the lattice type is
469466
public def tryMkBackwardRuleFromSpec (specThm : SpecTheorem) (info : WPInfo)
470467
(stateArgNames : Array Name := #[]) : OptionT SymM (BackwardRule × Bool) := do
471468
-- Instantiate the spec theorem, creating metavars for all universally quantified params
472-
let (xs, _bs, specProof, specType) ← specThm.instantiate
469+
let (_xs, _bs, specProof, specType) ← specThm.instantiate
473470
let_expr PartialOrder.rel Pred' _cl' pre rhs := specType
474471
| throwError "target not a partial order ⊑ application {specType}"
475472
guard <| ← isDefEqGuarded info.Pred Pred'
@@ -484,20 +481,12 @@ public def tryMkBackwardRuleFromSpec (specThm : SpecTheorem) (info : WPInfo)
484481
let ty ← Sym.inferType info.excessArgs[i]
485482
ssTypes := ssTypes.push ty
486483
ss := ss.push <| ← mkFreshExprMVar (userName := stateArgNames[i]?.getD `s) ty
487-
let (res, postVCHasProg) ← mkSpecBackwardProof pre prog postSpec epostSpec specProof info.EPred ss ssTypes stateArgNames
484+
let (res, hasContinuationVC) ← mkSpecBackwardProof pre prog postSpec epostSpec specProof info.EPred ss ssTypes stateArgNames
488485
let rule ← mkBackwardRuleFromExpr res.expr res.paramNames.toList
489-
-- The goal's precondition reaches exactly one premise: the precondition VC. Every other
490-
-- premise that mentions parts of the program verifies program fragments without it. When
491-
-- such a premise exists, the caller lifts a pure goal precondition into the local context
492-
-- first, so its fact stays available everywhere.
493-
let progMVars ← getMVars prog
494-
let mut hypHasProg := false
495-
for x in xs do
496-
if x.isMVar && !hypHasProg then
497-
let xTy ← Meta.inferType x
498-
if ← Meta.isProp xTy then
499-
hypHasProg := (← getMVars xTy).any progMVars.contains
500-
return (rule, hypHasProg || postVCHasProg)
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)
501490

502491
/-! ## Equality spec rules
503492

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

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,39 @@ 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.
128+
129+
Goals whose RHS contains metavariables are skipped: the defeq check could otherwise assign
130+
a post abstraction from an unrelated hypothesis. -/
131+
private def tryLiftedHyp (goal : MVarId) (α pre rhs : Expr) : VCGenM (Option (List MVarId)) :=
132+
goal.withContext do
133+
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
139+
let proof? ← match_expr rhs with
140+
| CompleteLattice.ofProp _l _inst φ => do
141+
if ← isDefEqS φ hyp.type then
142+
pure (some (← mkAppM ``Lean.Order.le_ofProp #[pre, φ, hyp.toExpr]))
143+
else
144+
pure none
145+
| _ => do
146+
if α.isProp then
147+
if ← isDefEqS rhs hyp.type then
148+
pure (some (← mkAppM ``Lean.Order.le_prop #[pre, rhs, hyp.toExpr]))
149+
else
150+
pure none
151+
else
152+
pure none
153+
let some proof := proof? | return none
154+
trace[Elab.Tactic.Do.vcgen] "Solved by lifted hypothesis {hyp.userName}"
155+
goal.assign proof
156+
return some []
157+
125158
/--
126159
Lift a non-trivial embedded pure precondition `⌜φ⌝` into the local context, leaving `⊤`
127160
as the residual precondition. Used before a decomposition step that would duplicate the
@@ -281,20 +314,23 @@ The function performs the following steps in order:
281314
2. **Target-let handling**: zeta-substitute duplicable top-level lets, otherwise introduce them.
282315
3. **Triple unfolding**: If the target is `⦃P⦄ x ⦃Q; E⦄`, unfold into `P ⊑ wp x Q E`.
283316
4. **Syntactic rfl**: close `pre ⊑ rhs` by `PartialOrder.rel_refl` when both sides unify.
284-
5. **State-argument introduction**: If the lattice carrier is a function type
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
285320
`σ₁ → ... → σₙ → Base`, introduce all excess state arguments.
286-
6. **EPost projection reduction**: reduce an `EPost.Cons.head` RHS to the projected component.
287-
7. **Lattice decomposition**: decompose `⊓`, `⇨`, `⌜p⌝` and `⊤` RHS connectives.
288-
8. **WP decomposition**: when the RHS is `wp e post epost s₁ ... sₙ`, in order:
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:
289324
hoist/zeta program-head lets, split `ite`/`dite`/match, zeta-unfold fvar program heads,
290325
reduce projection heads, and finally apply a registered `@[spec]` theorem.
291-
9. **Pure precondition introduction** (last resort): introduce a bare pure precondition (on
292-
the `Prop` lattice) so the residual VC has the form `⊤ ⊑ φ`.
293-
294-
Pure preconditions are deliberately introduced *late* (step 9, and inside steps 7/8
295-
right before a `⊓` or match split duplicates them): introducing them eagerly would turn
296-
reflexivity-closable verification conditions such as `Q x ⊑ wp (pure x) Q E` into goals that
297-
need an assumption search.
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.
298334
-/
299335
public def solve (scope : VCGen.Scope) (goal : MVarId) : VCGenM SolveResult := goal.withContext do
300336
if ← outOfFuel then return .stop
@@ -318,6 +354,7 @@ public def solve (scope : VCGen.Scope) (goal : MVarId) : VCGenM SolveResult := g
318354
let preIsTop := pre.isAppOf ``Lean.Order.top && pre.getAppNumArgs == 2
319355

320356
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
321358

322359
-- Phase 2: introduce excess state arguments and reduce EPost projections, so that the RHS
323360
-- exposes either a lattice connective or a `wp` application.

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ open Lean Meta Sym Lean.Order
2020
/-!
2121
Generic `VCGenM` helpers: checked backward-rule application, telescope-aware `simp` driver,
2222
hygienic binder introduction, hypothesis-internalization for grind, and the trivial-conjunct
23-
collapser `repeatAndRfl`. None of these know anything about the entailment shapes `solve`
23+
collapser `solveTrivialConjuncts`. None of these know anything about the entailment shapes `solve`
2424
decomposes.
2525
-/
2626

@@ -132,7 +132,7 @@ exactly the conjuncts that could not be solved.
132132
This procedure may assign metavariables in `e₁`/`e₂`, for example for `e = ?m` it will assign
133133
`?m := e`.
134134
-/
135-
public partial def repeatAndRfl (goal : MVarId) : VCGenM (Option MVarId) :=
135+
public partial def solveTrivialConjuncts (goal : MVarId) : VCGenM (Option MVarId) :=
136136
goal.withContext do
137137
let ctx ← read
138138
let ty ← instantiateMVars (← goal.getType)
@@ -141,8 +141,8 @@ public partial def repeatAndRfl (goal : MVarId) : VCGenM (Option MVarId) :=
141141
return none
142142
else if ty.isAppOf ``And then
143143
let .goals [g₁, g₂] ← ctx.andIntroRule.applyChecked goal
144-
| throwError "repeatAndRfl: failed to apply {.ofConstName ``And.intro} to{indentExpr ty}"
145-
matchrepeatAndRfl g₁, ← repeatAndRfl g₂ with
144+
| throwError "solveTrivialConjuncts: failed to apply {.ofConstName ``And.intro} to{indentExpr ty}"
145+
matchsolveTrivialConjuncts g₁, ← solveTrivialConjuncts g₂ with
146146
| none, none => return none
147147
| some g, none => return some g
148148
| none, some g => return some g

src/Std/Internal/Do/Order/Basic.lean

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ theorem prop_le (x y : Prop) : (x → (⊤ : Prop) ⊑ y) → x ⊑ y :=
267267
theorem top_le_prop (x : Prop) : x → (⊤ : Prop) ⊑ x :=
268268
fun hx _ => hx
269269

270+
theorem le_prop (x y : Prop) : y → x ⊑ y :=
271+
fun hy _ => hy
272+
270273
theorem of_top_le_prop {x : Prop} : (⊤ : Prop) ⊑ x → x :=
271274
fun h => h (le_top True trivial)
272275

0 commit comments

Comments
 (0)