Skip to content

fix: fix vcgen frames feature for monads with multiple StateT transformers#14217

Draft
volodeyka wants to merge 13 commits into
masterfrom
vcgen-frame-nested-base
Draft

fix: fix vcgen frames feature for monads with multiple StateT transformers#14217
volodeyka wants to merge 13 commits into
masterfrom
vcgen-frame-nested-base

Conversation

@volodeyka

Copy link
Copy Markdown
Contributor

This PR builds on top of vcgen frames feature and fixes some problems. More explanations yet to come

sgraf812 and others added 13 commits June 26, 2026 12:09
Introduce `Residuated`, the join-distributivity class whose residual `Residuated.imp` and frame closure `Residuated.frameClosure` internalize the frame rule for any residuated operator, not just the lattice meet. `WP.Frames` and the frame gadgets are stated for an arbitrary residuated `conj`; the Heyting/meet case is the specialization.
Add the `@[frameproc]` attribute registering a frame inference metaprogram for `vcgen`, keyed by program type. A registered procedure supplies the inferred frame, its frame operator, and the lattice split that decomposes it; `vcgen` applies the operator's frame gadget and the split. Direct (non-pointwise) lattice splits are supported via an optional `applyLemma`.
Demonstrate a residuated frame operator that is not the lattice meet: `costConj` over `Nat → Prop`, the separating conjunction for the cost resource. `TickM.wp` bakes the frame rule in as a `frameClosure`, and a registered `@[frameproc]` lets plain `vcgen` infer and apply the cost frame end-to-end.
Add the lawful `WPMonad TickM` instance (pure/bind sound for `TickM.wp`), so `vcgen` can decompose `bind` and loops over `TickM` programs. The cost frame `d` threads through `pure` and `bind`.
`applyFrame` no longer frames the monad's structural combinators (`bind`/`pure`/`map`/`seq`), letting them decompose through their specs so frame inference applies to the leaf calls. Make `TickM`'s `WP` come solely from its `WPMonad` instance, and add a `tickList` loop verified end-to-end by plain `vcgen` with automatic cost framing.
A `@[frameproc]` now also carries the lattice split for its frame operator's residual wand (`Residuated.imp conj`). `splitLatticeOp?` dispatches a `Residuated.imp` goal on its inner operator, routing a custom frame's magic wand to that split while the meet `⇨` falls through to the built-in residual. The wand no longer surfaces in a VC, so `do tickList xs; tickList ys` verifies by plain `vcgen` with the costs framed automatically across both calls.
State `tickList`'s spec in the concrete stopwatch form `⦃· = 0⦄ tickList xs ⦃fun _ t => t = xs.length⦄` (notation `tickList xs ⏱ xs.length`) rather than a post-schematic spec, so `vcgen` frames it only where the start cost is non-zero. The schematic cost lemma becomes a non-`@[spec]` helper the stopwatch spec is derived from. The two-loop example now frames the second call by the cost the first accrued.
…it frames

`matchFrameProc?` now sources the frame operator from the `@[frameproc]` registered for the program's type whether the frame comes from an explicit `frames` clause or from the registered procedure, instead of hardcoding the lattice meet for the `frames`-clause path. An explicit `frames` clause on a `TickM` program now frames with `costConj`, not meet.

Also rewrite the `tickList` demo as an idiomatic `for` loop, and show the two-loop goal framed both explicitly and with frames omitted.
The `Residuated` class now takes a resource type `R` so the frame operator is `op : R → α → α` rather than `α → α → α`. A frame is a value of `R`, which need not be an element of the lattice: the cost monad in the demo frames by a plain `Nat` budget instead of an assertion. The lattice meet is the instance at `R = α`.

`frameClosure` frames with the consistent order `op r (Q a)`, and `frameClosure_frames` assumes only an action law `op (comp r r') = op r ∘ op r'` for a resource composition `comp`, dropping the commutativity and associativity requirements. `WP.Frames`, the frame gadgets, and `WP.Frames.of_frameClosure` carry the resource frame `F : R` and thread `comp` with its action law.

The frame-inference procedure receives the operator's resource type, so an explicit `frames` clause elaborates its term at `R`. `LatticeSplit` gains a `leadingArgs` field to account for the resource-type argument of `Residuated.imp`, and the residual dispatch reads the inner operator from its shifted position.

The cost demo now frames by a `Nat`: `costConj r b = fun n => r ≤ n ∧ b (n - r)` with residual `fun m => b (m + r)`, a plain cost shift, so the two-loop example reads `frames | tickList xs => 0 | tickList ys => xs.length`.
Name the class for what it requires rather than an opaque property of its operators: `SupPreserving R α op` says each `op r ·` preserves arbitrary joins. Its right adjoint is `SupPreserving.upperAdjoint` (was `Residuated.imp`), with the unit/counit renamed to `le_upperAdjoint`/`upperAdjoint_le`.

Reintroduce `himp` as the meet upper adjoint, `himp a b = SupPreserving.upperAdjoint (· ⊓ ·) a b`, with `⇨` as its notation, and give `vcgen` a dedicated `himp` lattice split so the meet residual decomposes through it.
Rename `SupPreserving R α op` to `PreservesSup (f : α → α)`, a predicate on a single join-preserving map following the Lean-core `Preserves*` convention (`PreservesLE`, `PreservesLeast?`). A frame operator is instantiated at its slice `op r`: the lattice meet at `meet a`, the cost combinator at `costConj r`. `upperAdjoint`/`le_upperAdjoint`/`upperAdjoint_le`/`map_mono` and `frameClosure` follow, with `frameClosure_frames` taking `[∀ r, PreservesSup (op r)]`.

Make `himp` a `def` with its own API derived from the upper-adjoint properties (`le_himp`, `meet_himp_le`), `⇨` its notation. `vcgen` decomposes `⇨` through a dedicated `himp` split keyed on `le_himp`; the general residual split is gone, since custom operators register their own and the meet case is `himp`.
Drop the meet-specific `meet_wp_imp_le_wp_skipFrame`; `vcgen` now frames every operator, the lattice meet included, through the single gadget `op_wp_upperAdjoint_le_wp_skipFrame` (renamed from `wp_imp_le_wp_skipFrame`, built on `WP.Frames.op_wp_upperAdjoint_le_wp`). The meet residual `upperAdjoint (meet F) b` is folded to `F ⇨ b` (definitionally equal: `himp` is the meet upper adjoint) by `foldUpperAdjointMeet?`, exposing the meet operand so the `himp` split decomposes it. `applyFrame` no longer branches on the operator.
Extend `@[frameproc]` frame inference to a *nested-base* assertion
lattice (e.g. `ComplexiT (StateM σ)`, lattice `Tick → σ → Prop`). There
the frame operator is applied to more coordinates than the registered
direct split lemma handles (`pre ⊑ cCostConj c R n s`, not `… n`), so
`splitLatticeOp?` returns `none` and the raw goal — with an un-specced
inner `wp` — is dumped on `finish`.

Add two fallback steps in `solve`, run after the direct split and the wp
phase (so flat lattices are untouched): for a registered frame `conj` or
its residual wand, `mkReduceRule` builds a backward rule from a
registered reduce-equation (`FrameProc.conjReduce` / `impReduce`) whose
conclusion matches the goal head, and `applyChecked`s it. The single
premise is the reduced subgoal — the built-in connective for the conj
(the meet split then peels it over all coordinates), or the shifted body
for the wand (exposing the inner `wp` for the spec step).

`mkReduceRule` reuses `mkBackwardRuleForLattice`'s composition tail
(`liftEqByArgs` + `Eq.mp` + `mkBackwardRuleFromExpr`), minus the
`relLemma`. `tests/bench/vcgen/mwe_nested_frame.lean` documents the
2-level setup. Flat `TickM` is unchanged (the fallbacks never fire).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN label Jun 29, 2026
@mathlib-lean-pr-testing

Copy link
Copy Markdown

Mathlib CI status (docs):

  • ❗ Batteries/Mathlib CI will not be attempted unless your PR branches off the nightly-with-mathlib branch. Try git rebase 8f37bc8b0a305a55285825b3535e4da5ed2a2d31 --onto 84b251f7390c20a0a00a221050ab4a6e6c46a191. You can force Mathlib CI using the force-mathlib-ci label. (2026-06-29 13:59:33)

@leanprover-bot

Copy link
Copy Markdown
Collaborator

Reference manual CI status:

  • ❗ Reference manual CI will not be attempted unless your PR branches off the nightly-with-manual branch. Try git rebase 8f37bc8b0a305a55285825b3535e4da5ed2a2d31 --onto 84b251f7390c20a0a00a221050ab4a6e6c46a191. You can force reference manual CI using the force-manual-ci label. (2026-06-29 13:59:34)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants