Skip to content

Commit 5f9b669

Browse files
authored
feat: add until clause to mvcgen' (#13983)
This PR adds `mvcgen' until $t`, where `$t` is a conv-style pattern (holes `_` allowed); verification-condition generation stops as soon as the program matches the pattern, leaving it as a VC instead of applying a spec, similar to the existing `stepLimit` option. The pattern is elaborated lazily against the monad of the first program it is tested against, so overloaded heads resolve correctly and editor hovers work on the pattern. The out-of-fuel check and the new `until` check now share a single `SolveResult.stop`.
1 parent 0e9308e commit 5f9b669

6 files changed

Lines changed: 147 additions & 9 deletions

File tree

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@ The `VCGenM` monad: its read-only `Context` (a fixed bundle of pre-built
2525
(rule caches, accumulated invariants/VCs, simp cache).
2626
-/
2727

28+
/-- A lazily elaborated `until` pattern, stored behind an `IO.Ref` so the first `force` caches its
29+
result. The pattern is elaborated against the monad of the first program encountered in `solve`
30+
(supplied as the expected type). -/
31+
public inductive UntilPatternThunk where
32+
/-- Not yet elaborated; `elabFn m` elaborates the pattern against the program monad `m`. -/
33+
| deferred (elabFn : Expr → MetaM Sym.Pattern)
34+
/-- Already elaborated and cached. -/
35+
| elaborated (pat : Sym.Pattern)
36+
37+
/-- Force the thunk in `ref` against the program monad `m`, elaborating, tracing, and caching the
38+
pattern on first use; later calls return the cached pattern. -/
39+
public def UntilPatternThunk.force (ref : IO.Ref UntilPatternThunk) (m : Expr) : MetaM Sym.Pattern := do
40+
match ← ref.get with
41+
| .elaborated pat => return pat
42+
| .deferred elabFn =>
43+
let pat ← elabFn m
44+
trace[Elab.Tactic.Do.vcgen] "`until` pattern elaborated to {pat.pattern}"
45+
ref.set (.elaborated pat)
46+
return pat
47+
2848
public structure VCGen.Context where
2949
/-- The backward rule for `SPred.entails_cons_intro`. -/
3050
entailsConsIntroRule : BackwardRule
@@ -93,6 +113,9 @@ public structure VCGen.Context where
93113
the parsed `inv<n>` numbers (out-of-order labels are supported). Empty when no
94114
`invariants` clause is provided or in `invariants?` (suggest) mode (handled separately). -/
95115
invariantAlts : Std.HashMap Nat Syntax := {}
116+
/-- The `until` pattern: when `some ref`, VC generation stops and emits the current goal as a VC
117+
once the program in `wp⟦e⟧` matches the (lazily elaborated) pattern, before applying a spec. -/
118+
untilPat? : Option (IO.Ref UntilPatternThunk) := none
96119

97120
public structure VCGen.Scope where
98121
/-- Spec database in scope: globals plus locals from in-scope hypotheses. -/

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,9 @@ public def work (scope : Scope) (goal : Grind.Goal) : VCGenM Unit := do
108108
worklist := worklist.pop
109109
let goal := s.goal
110110
if goal.inconsistent then continue
111-
if ← outOfFuel then
112-
emitVC goal
113-
continue
114111
match ← solve s.scope goal.mvarId with
112+
| .stop =>
113+
emitVC goal
115114
| .noEntailment .. | .noProgramFoundInTarget .. =>
116115
emitVC goal
117116
| .noSpecFoundForProgram prog monad thms =>

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

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public import Lean.Meta.Sym.Simp.Rewrite
1818
public import Lean.Meta.Sym.Simp.Simproc
1919
public import Lean.Elab.Tactic.Grind.Main
2020
public import Lean.Elab.Tactic.Grind.Basic
21+
import Lean.Meta.Sym.ProofInstInfo
2122

2223
open Lean Parser Meta Elab Tactic Sym
2324
open Lean.Elab.Tactic.Do Lean.Elab.Tactic.Do.SpecAttr
@@ -277,6 +278,38 @@ private structure ParsedArgs where
277278
scope : VCGen.Scope
278279
invariantAlts? : Option (Std.HashMap Nat Syntax)
279280

281+
/-- Build a `Sym.Pattern` from `e` by abstracting the metavariables `xs` into pattern variables.
282+
`checkTypeMask?` is `none` because `until` holes appear as function arguments, whose types the
283+
enclosing application already constrains. -/
284+
private def mkUntilPattern (xs : Array Expr) (e : Expr) : MetaM Sym.Pattern := do
285+
let pattern := e.abstract xs
286+
let mut varTypes := #[]
287+
for h : i in [0:xs.size] do
288+
varTypes := varTypes.push ((← inferType xs[i]).abstractRange i xs)
289+
let mut fnInfos : AssocList Name Sym.ProofInstInfo := {}
290+
for declName in pattern.getUsedConstants do
291+
if let some info ← Sym.mkProofInstInfo? declName then
292+
fnInfos := fnInfos.insertNew declName info
293+
let varInfos? ← Sym.mkProofInstArgInfo? xs
294+
return { levelParams := [], varTypes, pattern, fnInfos, varInfos?, checkTypeMask? := none }
295+
296+
/-- Build a deferred `until` pattern (holes `_` allowed, as in `conv in $t`). The pattern term is
297+
elaborated lazily when the first program is seen in `solve`, using that program's monad `m` as the
298+
expected type (`m _`) so overloaded heads resolve; the result is cached. The holes become pattern
299+
variables. -/
300+
private def elabUntilPattern (p : Term) : TermElabM (IO.Ref UntilPatternThunk) := do
301+
let lctx ← getLCtx
302+
let localInsts ← getLocalInstances
303+
IO.mkRef <| UntilPatternThunk.deferred fun m =>
304+
withLCtx lctx localInsts <| Term.TermElabM.run' <|
305+
-- Restore the metavariable state but keep info trees, so hovers work on the pattern.
306+
Term.withoutModifyingElabMetaStateWithInfo <| withRef p <|
307+
withTheReader Term.Context ({ · with ignoreTCFailures := true }) <|
308+
Term.withoutErrToSorry do
309+
let e ← instantiateMVars (← Term.elabTerm p (some (mkApp m (← mkFreshTypeMVar))))
310+
let xs := (e.collectMVars {}).result.map Expr.mvar
311+
mkUntilPattern xs e
312+
280313
/-- Parse `mvcgen'` arguments. -/
281314
private def parseArgs (stx : Syntax) (goal : MVarId) : TermElabM ParsedArgs := goal.withContext do
282315
if mvcgen.warning.get (← getOptions) then
@@ -290,16 +323,18 @@ private def parseArgs (stx : Syntax) (goal : MVarId) : TermElabM ParsedArgs := g
290323
-- explicit `(elimLets := true)` at the syntax level (upstream `Config` can't
291324
-- distinguish "default true" from "user-set true"); not yet wired.
292325
let (ctx, scope) ← VCGen.mkContext stx[2] goal
293-
let hypSimpMethods ← elabSimplifyingAssumptions stx[4]
294-
let invariantAlts? ← parseInvariantMap stx[3]
326+
let untilPat? ← if stx[3].isNone then pure none else some <$> elabUntilPattern ⟨stx[3][1]⟩
327+
let hypSimpMethods ← elabSimplifyingAssumptions stx[5]
328+
let invariantAlts? ← parseInvariantMap stx[4]
295329
let ctx := { ctx with
296330
hypSimpMethods,
297331
trivial := config.trivial,
298332
useJP := config.jp,
299333
errorOnMissingSpec := config.errorOnMissingSpec,
300334
debug := config.debug,
301335
internalize := config.internalize,
302-
invariantAlts := invariantAlts?.getD {} }
336+
invariantAlts := invariantAlts?.getD {},
337+
untilPat? }
303338
return { config, ctx, scope, invariantAlts? }
304339

305340
/-- `mvcgen'` step inside `sym => …` blocks. -/
@@ -314,7 +349,7 @@ def evalSymMVCGen' : Lean.Elab.Tactic.Grind.GrindTactic := fun stx => do
314349
return result
315350
if args.invariantAlts?.isNone then
316351
runTacticM (goals := result.invariants.toList) <|
317-
elabInvariants stx[3] result.invariants (suggestInvariant (result.vcs.map (·.mvarId)))
352+
elabInvariants stx[4] result.invariants (suggestInvariant (result.vcs.map (·.mvarId)))
318353
let invariants ← result.invariants.filterM (not <$> ·.isAssigned)
319354
let newGoals ← Lean.Elab.Tactic.Grind.liftGrindM do
320355
let invGoals ← invariants.toList.mapM Grind.mkGoalCore
@@ -328,7 +363,7 @@ goals. The optional `with $g:grind` clause runs as `<;> $g` and lets the user-su
328363
grind step share an internalised E-graph with `mvcgen'`. -/
329364
@[builtin_tactic Lean.Parser.Tactic.mvcgen']
330365
public def elabMVCGen' : Tactic := fun stx => withMainContext do
331-
let `(tactic| mvcgen'%$tk $cfg:optConfig $[[$lems,*]]? $(invs)?
366+
let `(tactic| mvcgen'%$tk $cfg:optConfig $[[$lems,*]]? $[until $u:term]? $(invs)?
332367
$[simplifying_assumptions $(sa)? $[[$thms,*]]?]? $[with $g:grind]?) := stx
333368
| throwUnsupportedSyntax
334369
-- Without `with`, no downstream grind step will read the E-graph, so opt out of
@@ -338,7 +373,7 @@ public def elabMVCGen' : Tactic := fun stx => withMainContext do
338373
| none => do
339374
let off ← `(optConfig| -internalize)
340375
pure (Lean.Parser.Tactic.appendConfig off cfg)
341-
let core ← `(grind| mvcgen'%$tk $cfg:optConfig $[[$lems,*]]? $(invs)?
376+
let core ← `(grind| mvcgen'%$tk $cfg:optConfig $[[$lems,*]]? $[until $u:term]? $(invs)?
342377
$[simplifying_assumptions $(sa)? $[[$thms,*]]?]?)
343378
let step ← match g with
344379
| some g => `(grind| $core <;> $g)

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public inductive SolveResult where
4141
| noSpecFoundForProgram (e : Expr) (monad : Expr) (thms : Array SpecTheoremNew)
4242
/-- Successfully decomposed the goal. These are the subgoals, sharing `scope`. -/
4343
| goals (scope : VCGen.Scope) (subgoals : List MVarId)
44+
/-- Stop decomposing and emit the current goal as a VC (out of fuel, or `until` matched). -/
45+
| stop
4446

4547
private def isDuplicable (e : Expr) : Bool := match e with
4648
| .bvar .. | .mvar .. | .fvar .. | .const .. | .lit .. | .sort .. => true
@@ -208,6 +210,7 @@ The function performs the following steps in order:
208210
its cached backward rule.
209211
-/
210212
public def solve (scope : VCGen.Scope) (goal : MVarId) : VCGenM SolveResult := goal.withContext do
213+
if ← outOfFuel then return .stop
211214
let target ← goal.getType
212215
trace[Elab.Tactic.Do.vcgen] "🎯 Target: {target}"
213216
-- Phase 1: simplify `target` until it is of the form `H ⊢ₛ T`.
@@ -268,6 +271,16 @@ public def solve (scope : VCGen.Scope) (goal : MVarId) : VCGenM SolveResult := g
268271
VCGen.burnOne
269272
return .goals scope [g]
270273

274+
-- Stop if the program matches the `until` pattern (elaborated lazily against the program monad).
275+
let matchesUntilPattern (m : Expr) : VCGenM Bool := do
276+
let some ref := (← read).untilPat? | return false
277+
let pat ← UntilPatternThunk.force ref m
278+
if (← pat.match? e).isSome then
279+
trace[Elab.Tactic.Do.vcgen] "`until` pattern matched program {e}; stopping"
280+
return true
281+
return false
282+
if ← matchesUntilPattern m then return .stop
283+
271284
-- Apply registered specifications, or fall through to `.noStrategyForProgram`.
272285
VCGen.burnOne
273286
applySpec scope goal e excessArgs m σs ps instWP

src/Std/Tactic/Do/Syntax.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ syntax (name := mvcgenHint) "mvcgen?" optConfig
444444
@[tactic_alt Lean.Parser.Tactic.mvcgen'Macro]
445445
syntax (name := mvcgen') "mvcgen'" optConfig
446446
(" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "] ")?
447+
(&" until " term)?
447448
(invariantAlts)?
448449
(&" simplifying_assumptions" (ppSpace colGt ident)? (" [" ident,* "]")?)?
449450
(&" with " grind)? : tactic
@@ -454,6 +455,7 @@ namespace Grind
454455
steps using `<;>` instead. -/
455456
syntax (name := mvcgen') "mvcgen'" optConfig
456457
(" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "] ")?
458+
(&" until " term)?
457459
(invariantAlts)?
458460
(&" simplifying_assumptions" (ppSpace colGt ident)? (" [" ident,* "]")?)?
459461
: grind

tests/elab/mvcgenUntil.lean

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import Lean
2+
import Std
3+
import Std.Tactic.Do
4+
5+
set_option mvcgen.warning false
6+
set_option warn.sorry false
7+
8+
/-! Tests for the `mvcgen' until <pattern>` stop condition. -/
9+
10+
open Std.Do
11+
12+
namespace MVCGenUntil
13+
14+
def tick : StateM Nat Unit := modify (· + 1)
15+
16+
def useTick : StateM Nat Nat := do
17+
tick
18+
return 0
19+
20+
def step (k : Nat) : StateM Nat Unit := modify (· + k)
21+
22+
def useStep : StateM Nat Nat := do
23+
step 1
24+
return 0
25+
26+
def getThenTick : StateM Nat Nat := do
27+
let x ← get
28+
tick
29+
return x
30+
31+
-- `until tick` matches the nullary `tick` call and stops; `unfold tick` hits the leftover `wp⟦tick⟧`.
32+
theorem useTick_until : ⦃⌜True⌝⦄ useTick ⦃⇓ _ => ⌜True⌝⦄ := by
33+
mvcgen' [useTick] until tick
34+
all_goals unfold tick
35+
all_goals admit
36+
37+
-- An explicit hole matches: `until step _` stops at `step 1`, the `_` matching its argument.
38+
theorem useStep_until_hole : ⦃⌜True⌝⦄ useStep ⦃⇓ _ => ⌜True⌝⦄ := by
39+
mvcgen' [useStep] until step _
40+
all_goals unfold step
41+
all_goals admit
42+
43+
-- A concrete argument discriminates: `until step 1` matches the `step 1` call and stops.
44+
theorem useStep_until_one : ⦃⌜True⌝⦄ useStep ⦃⇓ _ => ⌜True⌝⦄ := by
45+
mvcgen' [useStep] until step 1
46+
all_goals unfold step
47+
all_goals admit
48+
49+
-- ...but `until step 2` does not match `step 1`, so VC generation reaches the un-specced call.
50+
/-- error: No spec found for program step 1. -/
51+
#guard_msgs in
52+
example : ⦃⌜True⌝⦄ useStep ⦃⇓ _ => ⌜True⌝⦄ := by
53+
mvcgen' [useStep] until step 2
54+
55+
-- A non-matching `until` (with a hole) is a no-op: VC generation runs to completion.
56+
theorem useStep_until_nomatch : ⦃⌜True⌝⦄ useStep ⦃⇓ r => ⌜r = 0⌝⦄ := by
57+
mvcgen' [useStep, step] until List.length _
58+
all_goals grind
59+
60+
-- The overloaded `get` is resolved against the program's monad and matches the `get` call, so VC
61+
-- generation stops before the un-specced `tick`. A wrong resolution would proceed to `tick` and error.
62+
theorem getThenTick_until_get : ⦃⌜True⌝⦄ getThenTick ⦃⇓ _ => ⌜True⌝⦄ := by
63+
mvcgen' [getThenTick] until get
64+
all_goals admit
65+
66+
end MVCGenUntil

0 commit comments

Comments
 (0)