|
| 1 | +/- |
| 2 | +Copyright (c) 2026 Lean FRO LLC. All rights reserved. |
| 3 | +Released under Apache 2.0 license as described in the file LICENSE. |
| 4 | +Authors: Vladimir Gladshtein, Sebastian Graf |
| 5 | +-/ |
| 6 | +import Lean |
| 7 | +import Std.Internal |
| 8 | +import Std.Tactic.Do |
| 9 | + |
| 10 | +set_option mvcgen.warning false |
| 11 | +set_option grind.warning false |
| 12 | + |
| 13 | +/-! |
| 14 | +# MWE: `vcgen` cost-framing over a *2-level* (stateful-base) assertion lattice |
| 15 | +
|
| 16 | +`tests/elab/vcgenTickFrames.lean` shows `vcgen` cost-framing working for `TickM := StateM Nat`, |
| 17 | +whose assertion lattice is the **flat** `Nat → Prop` (the single cost coordinate). The *same* |
| 18 | +construction over a **stateful base monad** — e.g. `ComplexiT (StateM σ)` in the `loom-complexity` |
| 19 | +project, assertion lattice `Nat → (σ → Prop)` (a cost coordinate **and** a base-state coordinate) — |
| 20 | +*used to* fail (`vcgen [foo] with finish` reported `finish failed`). It is **now fixed** by the |
| 21 | +`reduceFrameConj?` / `reduceFrameImp?` fallbacks in `VCGen/Solve.lean` (see "The fix" below). This |
| 22 | +file kept the diagnosis that pinned the root cause down. |
| 23 | +
|
| 24 | +Concretely, with `foo : ComplexiT (StateM Unit) Unit := do withTick ()` and |
| 25 | +`fooSpec : ⦃fun acc _ => acc = 0⦄ foo ⦃fun _ acc _ => acc ≤ 1⦄`, after the registered frameproc |
| 26 | +fires the raw stuck goal is (`s✝¹` = cost, `s✝` = base `Unit` state): |
| 27 | +
|
| 28 | + (s✝¹ = 0) ⊑ costConj s✝¹ (wp (Gadget.skipFrame foo) (fun a => (costConj s✝¹) -⋆ Q) ⊥) s✝¹ s✝ |
| 29 | + └──────────────────── costConj applied to TWO coords ───────────┘ |
| 30 | +
|
| 31 | +The registered *direct* split `le_costConj_point_apply : pre ⊑ costConj c R n` matches `costConj` |
| 32 | +applied to **one** coordinate. Over the 2-level lattice `vcgen` has introduced the extra base-state |
| 33 | +arg, so the goal is `costConj c R n s` (one application deeper); the split can't unify, returns |
| 34 | +`none`, and the raw `costConj` goal — with the un-specced inner `wp` still inside it — is dumped on |
| 35 | +`finish`, which cannot evaluate it. |
| 36 | +
|
| 37 | +## What this file pins down (each `example` below verifies a claim) |
| 38 | +
|
| 39 | +* **§A — the built-in stateful `meet` split is NOT the culprit.** `vcgen` decomposes |
| 40 | + `pre ⊑ (guard ⊓ shift) coords` over a *2-level* lattice for every operand shape we tried (opaque, |
| 41 | + λ, cost-shifted, `⌜·⌝`-guarded — i.e. the exact `costConj` shape). In `loom-complexity` it was |
| 42 | + further verified to split even when an operand is `wp (Gadget.skipFrame prog) …` (reaching the |
| 43 | + inner `wp`, i.e. getting *past* the meet). So once the meet is exposed, the split fires fine, |
| 44 | + over both coordinates. |
| 45 | +
|
| 46 | +* **§B — `costConj` is NOT defeq to its `meet` form.** The pi-lattice meet is a genuinely different |
| 47 | + term (`meet_apply` is a *lemma*, not `rfl`). So a fix cannot reduce the *current* `costConj` with |
| 48 | + `Meta.unfoldDefinition?` + `MVarId.replaceTargetDefEq` (the `rfl` it relies on does not hold); the |
| 49 | + meet form must be exposed either by *defining* `costConj` meet-first, or by a propositional |
| 50 | + `replaceTargetEq`. |
| 51 | +
|
| 52 | +## The fix (implemented in `VCGen/Solve.lean`) |
| 53 | +
|
| 54 | +Two fallback steps in `solve`, run *after* `splitLatticeOp?` so flat lattices keep their direct split: |
| 55 | +
|
| 56 | +* `reduceFrameConj?` — for a registered frameproc `conj` the direct split couldn't peel, delta-unfold |
| 57 | + the `conj` (defined **meet-first**, §B) to expose its `meet`, then **`unfoldReducible`-normalize**. |
| 58 | + That last step was the real blocker: the meet split's `applyChecked` only unifies against the |
| 59 | + reducible-normal form (state types `StateM σ`, `Tick` sit behind reducible defs). `applyChecked`'s |
| 60 | + `+debug` retry diagnoses exactly this ("succeeded after `unfoldReducible`-normalization"). The next |
| 61 | + `solve` iteration's meet split then peels both coordinates and specs the inner `wp`. Fires every |
| 62 | + iteration, so it recurses at each framing level. |
| 63 | +
|
| 64 | +* `reduceFrameImp?` — for ≥2 cost calls, the residual wand `upperAdjoint (conj c) R` wraps the *next* |
| 65 | + call's un-specced `wp` and the direct `impSplit` can't peel the extra coord. Rewrite it via a |
| 66 | + registered `FrameProc.impReduce` equation (here `costConj_imp : upperAdjoint (costConj c) R = |
| 67 | + fun m => R (m+c)`), then beta + `unfoldReducible`, exposing the inner `wp` for the spec step. |
| 68 | +
|
| 69 | +The leftover VCs (budget guard `c ≤ n`, `WP.Frames`) close under `with finish`. |
| 70 | +-/ |
| 71 | + |
| 72 | +open Lean Order Std Internal.Do |
| 73 | + |
| 74 | +/-- The 2-level cost-frame operator: a guard on the cost coordinate `n`, met — on the base-state |
| 75 | +lattice `σ → Prop` — with the body run on the remaining cost `n - r`. The analog of the flat |
| 76 | +`costConj` of `vcgenTickFrames.lean`, but over `Nat → (σ → Prop)`. -/ |
| 77 | +def costConj {σ : Type} (r : Nat) (b : Nat → (σ → Prop)) : Nat → (σ → Prop) := |
| 78 | + fun n => ⌜r ≤ n⌝ ⊓ b (n - r) |
| 79 | + |
| 80 | +/-! ## §A — the built-in stateful `meet` split decomposes 2-level meets |
| 81 | +
|
| 82 | +Each goal is a bare lattice entailment whose RHS is a `meet` of two `Nat → (Unit → Prop)` functions; |
| 83 | +`vcgen` introduces the cost arg *and* the base-state arg and fires the meet split across **both** |
| 84 | +coordinates, leaving one obligation per operand, which `finish` discharges. -/ |
| 85 | + |
| 86 | +/-- Two `⊤` operands over the 2-level lattice `Nat → Unit → Prop`: the split introduces the cost arg |
| 87 | +`n` *and* the base-state arg `s`, then closes `pre ⊑ ⊤` on each side. -/ |
| 88 | +example (pre : Nat → Unit → Prop) : |
| 89 | + pre ⊑ (fun _ => (⊤ : Unit → Prop)) ⊓ (fun _ => (⊤ : Unit → Prop)) := by |
| 90 | + vcgen with finish [top_apply] |
| 91 | + |
| 92 | +-- The exact `costConj` shape — `⌜·⌝`-guard ⊓ cost-shift — over the 2-level lattice. The split peels |
| 93 | +-- **both** coordinates (`s✝¹` the cost, `s✝` the base state), leaving the guard obligation (`vc1`) |
| 94 | +-- and the shifted-body obligation (`vc2`), each trivially true. (In `vcgenTickFrames.lean` the |
| 95 | +-- analogous split has only the single cost coordinate; here it must — and does — recurse into base.) |
| 96 | +/-- |
| 97 | +error: unsolved goals |
| 98 | +case vc1 |
| 99 | +b : Nat → PUnit → Prop |
| 100 | +c s✝¹ : Nat |
| 101 | +s✝ : PUnit |
| 102 | +⊢ (c ≤ s✝¹ ∧ b (s✝¹ - c) s✝) ⊑ ⌜c ≤ s✝¹⌝ s✝ |
| 103 | +
|
| 104 | +case vc2 |
| 105 | +b : Nat → PUnit → Prop |
| 106 | +c s✝¹ : Nat |
| 107 | +s✝ : PUnit |
| 108 | +⊢ (c ≤ s✝¹ ∧ b (s✝¹ - c) s✝) ⊑ b (s✝¹ - c) s✝ |
| 109 | +-/ |
| 110 | +#guard_msgs in |
| 111 | +example (b : Nat → Unit → Prop) (c : Nat) : |
| 112 | + (fun n s => c ≤ n ∧ b (n - c) s) ⊑ (fun n => ⌜c ≤ n⌝) ⊓ (fun n => b (n - c)) := by |
| 113 | + vcgen |
| 114 | + |
| 115 | +/-! ## §B — `costConj` is not definitionally its `meet` form -/ |
| 116 | + |
| 117 | +/-- The `rfl` that an `unfoldDefinition?` + `replaceTargetDefEq` reduction would rely on **fails**; |
| 118 | +the equation holds only *propositionally* (`meet_apply` is a lemma, not `rfl`). -/ |
| 119 | +example {σ : Type} (c : Nat) (b : Nat → σ → Prop) : |
| 120 | + costConj c b = (fun n => ⌜c ≤ n⌝) ⊓ (fun n => b (n - c)) := by |
| 121 | + fail_if_success rfl |
| 122 | + funext n; simp only [costConj, meet_apply] |
0 commit comments