lean4-htt/tests/elab/mvcgenTutorial.lean
Joachim Breitner 5b87ab6625
feat: use explicit allowlist instead of transparency bump in whnfMatcher (#13363)
This PR replaces the transparency bump from `.reducible` to `.instances`
in `whnfMatcher` with an explicit allowlist in `canUnfoldAtMatcher`.
Previously, `whnfMatcher` would unfold all `implicitReducible`
definitions and all `fromClass` projections when reducing match
discriminants. This made it impossible to mark definitions as
`implicit_reducible` without silently affecting match reduction
behavior.

The new `canUnfoldAtMatcher` delegates to `canUnfoldDefault` first
(respecting the ambient transparency), then allows unfolding of
`match_pattern`-attributed definitions, and finally checks an explicit
allowlist:

- `OfNat.ofNat` — numeric literals in match discriminants
- `NatCast.natCast` — `↑m` coercions (pervasive in Int proofs)
- `Zero.zero`, `One.one` — `0`/`1` class projections in match
discriminants
- `Fin.ofNat`, `HMod.hMod`, `Mod.mod` — Fin literal reduction
- `decEq`, `Nat.decEq` — decidable equality
- `Char.ofNat`, `Char.ofNatAux` — character literals
- `String.decEq`, `List.hasDecEq` — string/list equality
- `UInt{8,16,32,64}.{ofNat,decEq}` — unsigned integer literals and
equality

The key change is removing the blanket `implicitReducible` and
`fromClass` checks, so that marking definitions as `implicit_reducible`
no longer silently affects match reduction.

Additionally, `reduceMatcher?` and `reduceRecMatcher?` now call
`consumeMData` on their input to handle mdata-wrapped matcher
expressions.

Mathlib adaptation: the removal of the `fromClass` projection check
means class projections like `CategoryStruct.comp`, `CategoryStruct.id`,
`Min.min` etc. are no longer auto-unfolded in match discriminants.
Affected proofs add these projections explicitly to `simp`/`dsimp`
calls.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 13:50:30 +00:00

287 lines
8.7 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Std.Tactic.Do
import Std.Tactic.BVDecide
import Std.Data.HashSet
set_option backward.do.legacy false
set_option grind.warning false
set_option mvcgen.warning false
open Std Do
namespace Nodup
-- Inspired by Markus' `pairsSumToZero`.
def nodup (l : List Int) : Bool := Id.run do
let mut seen : HashSet Int := ∅
for x in l do
if x ∈ seen then
return false
seen := seen.insert x
return true
theorem nodup_correct (l : List Int) : nodup l ↔ l.Nodup := by
generalize h : nodup l = r
apply Id.of_wp_run_eq h
mvcgen
case inv1 =>
exact Invariant.withEarlyReturnNewDo
(onReturn := fun ret seen => ⌜ret = false ∧ ¬l.Nodup⌝)
(onContinue := fun traversalState seen =>
⌜(∀ x, x ∈ seen ↔ x ∈ traversalState.prefix) ∧ traversalState.prefix.Nodup⌝)
all_goals mleave; grind
theorem nodup_correct_directly (l : List Int) : nodup l ↔ l.Nodup := by
rw [nodup]
generalize hseen : (∅ : Std.HashSet Int) = seen
change ?lhs ↔ l.Nodup
suffices h : ?lhs ↔ l.Nodup ∧ ∀ x ∈ l, x ∉ seen by grind
clear hseen
induction l generalizing seen with grind [Id.run_pure, Id.run_bind]
end Nodup
namespace Fresh
structure Supply where
counter : Nat
def mkFresh : StateM Supply Nat := do
let n ← (·.counter) <$> get
modify (fun s => {s with counter := s.counter + 1})
pure n
def mkFreshN (n : Nat) : StateM Supply (List Nat) := do
let mut acc := #[]
for _ in [:n] do
acc := acc.push (← mkFresh)
pure acc.toList
namespace Noncompositional
theorem mkFreshN_correct (n : Nat) : ((mkFreshN n).run' s).Nodup := by
generalize h : (mkFreshN n).run' s = x
apply StateM.of_wp_run'_eq h
mvcgen [mkFreshN, mkFresh]
case inv1 => exact ⇓⟨xs, acc⟩ state => ⌜(∀ x ∈ acc, x < state.counter) ∧ acc.toList.Nodup⌝
all_goals mleave; grind
theorem mkFreshN_correct_directly (n : Nat) : ((mkFreshN n).run' s).run.Nodup := by
simp [mkFreshN, mkFresh]
generalize hacc : #[] = acc
change ?prog.Nodup
suffices h : acc.toList.Nodup → (∀ x ∈ acc, x < s.counter) → ?prog.Nodup by grind
clear hacc
induction List.range' 0 n generalizing acc s with (simp_all; try grind)
end Noncompositional
namespace Compositional
@[spec]
theorem mkFresh_spec (c : Nat) : ⦃fun state => ⌜state.counter = c⌝⦄ mkFresh ⦃⇓ r state => ⌜r = c ∧ c < state.counter⌝⦄ := by
mvcgen [mkFresh]
grind
@[spec]
theorem mkFreshN_spec (n : Nat) : ⦃⌜True⌝⦄ mkFreshN n ⦃⇓ r => ⌜r.Nodup⌝⦄ := by
mvcgen [mkFreshN]
case inv1 => exact ⇓⟨xs, acc⟩ state => ⌜(∀ x ∈ acc, x < state.counter) ∧ acc.toList.Nodup⌝
all_goals mleave; grind
theorem mkFreshN_correct (n : Nat) : ((mkFreshN n).run' s).Nodup :=
mkFreshN_spec n s True.intro
end Compositional
end Fresh
namespace FreshStack
structure Supply where
counter : Nat
def mkFresh [Monad m] : StateT Supply m Nat := do
let n ← (·.counter) <$> get
modify (fun s => {s with counter := s.counter + 1})
pure n
abbrev AppM := StateT Bool (StateT Supply (StateM String))
abbrev liftCounterM : StateT Supply (StateM String) α → AppM α := liftM
def mkFreshN (n : Nat) : AppM (List Nat) := do
let mut acc := #[]
for _ in [:n] do
let n ← liftCounterM mkFresh
acc := acc.push n
return acc.toList
theorem mkFreshN_spec_noncompositional (n : Nat) : ⦃⌜True⌝⦄ mkFreshN n ⦃⇓ r => ⌜r.Nodup⌝⦄ := by
mvcgen [mkFreshN, mkFresh, liftCounterM]
case inv1 => exact ⇓⟨xs, acc⟩ _ state => ⌜(∀ n ∈ acc, n < state.counter) ∧ acc.toList.Nodup⌝
all_goals mleave; grind
@[spec]
theorem mkFresh_spec [Monad m] [WPMonad m ps] (c : Nat) :
⦃fun state => ⌜state.counter = c⌝⦄ mkFresh (m := m) ⦃⇓ r state => ⌜r = c ∧ c < state.counter⌝⦄ := by
mvcgen [mkFresh]
grind
@[spec]
theorem mkFreshN_spec (n : Nat) : ⦃⌜True⌝⦄ mkFreshN n ⦃⇓ r => ⌜r.Nodup⌝⦄ := by
-- This is a great test case for `applyRflAndAndIntro` because it requires
-- reducing `(⌜s₁.counter = ?c⌝ s).down` to `s₁ = ?c`.
mvcgen [mkFreshN, liftCounterM]
case inv1 => exact ⇓⟨xs, acc⟩ _ state => ⌜(∀ n ∈ acc, n < state.counter) ∧ acc.toList.Nodup⌝
all_goals mleave; grind
theorem mkFreshN_correct (n : Nat) : (((StateT.run' (mkFreshN n) b).run' c).run' s).Nodup :=
mkFreshN_spec n _ _ _ True.intro
end FreshStack
namespace FreshBounded
structure Supply where
counter : Nat
limit : Nat
property : counter ≤ limit
def mkFresh : EStateM String Supply Nat := do
let supply ← get
if h : supply.counter = supply.limit then
throw s!"Supply exhausted: {supply.counter} = {supply.limit}"
else
let n := supply.counter
have := supply.property
set {supply with counter := n + 1, property := by omega}
pure n
def mkFreshN (n : Nat) : EStateM String Supply (List Nat) := do
let mut acc := #[]
for _ in [:n] do
acc := acc.push (← mkFresh)
pure acc.toList
@[spec]
theorem mkFresh_spec (c : Nat) :
⦃fun state => ⌜state.counter = c⌝⦄
mkFresh
⦃post⟨fun r state => ⌜r = c ∧ c < state.counter⌝, fun _msg state => ⌜c = state.counter ∧ c = state.limit⌝⟩⦄ := by
mvcgen [mkFresh]
all_goals grind
@[spec]
theorem mkFreshN_spec (n : Nat) :
⦃⌜True⌝⦄
mkFreshN n
⦃post⟨fun r => ⌜r.Nodup⌝, fun _msg state => ⌜state.counter = state.limit⌝⟩⦄ := by
mvcgen [mkFreshN]
case inv1 => exact post⟨fun ⟨xs, acc⟩ state => ⌜(∀ n ∈ acc, n < state.counter) ∧ acc.toList.Nodup⌝,
fun _msg state => ⌜state.counter = state.limit⌝⟩
all_goals mleave; try grind
theorem mkFreshN_correct (n : Nat) :
match (mkFreshN n).run s with
| .ok l _ => l.Nodup
| .error _ s' => s'.counter = s'.limit := by
generalize h : (mkFreshN n).run s = x
apply EStateM.of_wp_run_eq h
mvcgen
end FreshBounded
namespace Aeneas
inductive Error where
| integerOverflow: Error
-- ... more error kinds ...
inductive Result (α : Type u) where
| ok (v: α): Result α
| fail (e: Error): Result α
| div
instance : Monad Result where
pure x := .ok x
bind x f := match x with
| .ok v => f v
| .fail e => .fail e
| .div => .div
instance : LawfulMonad Result :=
LawfulMonad.mk' _
(by dsimp only [Functor.map]; grind)
(by dsimp only [bind, pure]; grind)
(by dsimp only [bind, pure]; grind)
instance Result.instWP : WP Result (.except Error .pure) where
wp
| .ok v => PredTrans.pure v
| .fail e => PredTrans.throw e
| .div => PredTrans.const ⌜False⌝
theorem Result.apply_wp_pure {α} {a : α} {Q : PostCond α (.except Error .pure)} :
wp⟦pure (f := Result) a⟧ Q = Q.1 a := by rfl
theorem Result.apply_wp_bind {α β} {x : Result α} {f : α → Result β} {Q : PostCond β (.except Error .pure)} :
wp⟦do let a ← x; f a⟧ Q = wp⟦x⟧ (fun a => wp⟦f a⟧ Q, Q.2) := by
simp only [wp, bind]
grind
instance Result.instWPMonad : WPMonad Result (.except Error .pure) where
wp_pure _ := by ext Q : 1; apply Result.apply_wp_pure
wp_bind x f := by ext Q : 1; apply Result.apply_wp_bind
theorem Result.of_wp {α} {x : Result α} (P : Result α → Prop) :
(⊢ₛ wp⟦x⟧ post⟨fun a => ⌜P (.ok a)⌝, fun e => ⌜P (.fail e)⌝⟩) → P x := by
intro hspec
match x with
| .ok a => simpa [wp] using hspec
| .fail e => simpa [wp] using hspec
| .div => simp [wp] at hspec
instance : MonadExcept Error Result where
throw e := .fail e
tryCatch x h := match x with
| .ok v => pure v
| .fail e => h e
| .div => .div
def addOp (x y : UInt32) : Result UInt32 :=
if x.toNat + y.toNat ≥ UInt32.size then
throw .integerOverflow
else
pure (x + y)
@[spec]
theorem Result.throw_spec (e : Error) :
⦃Q.2.1 e⦄ throw (m := Result) (α := α) e ⦃Q⦄ := id
@[spec]
theorem addOp_noOverflow_spec (x y : UInt32) (h : x.toNat + y.toNat < UInt32.size) :
⦃⌜True⌝⦄ addOp x y ⦃⇓ r => ⌜r = x + y ∧ (x + y).toNat = x.toNat + y.toNat⌝⦄ := by
mvcgen [addOp] <;> simp_all; try grind
example :
⦃⌜True⌝⦄
do let mut x ← addOp 1 3
for _ in [:4] do
x ← addOp x 5
return x
⦃⇓ r => ⌜r.toNat = 24⌝⦄ := by
mvcgen
case inv1 => exact ⇓⟨xs, x⟩ => ⌜x.toNat = 4 + 5 * xs.prefix.length⌝
all_goals simp_all [UInt32.size]; try grind
end Aeneas
section PostCond
variable (α σ ε : Type)
example : PostCond α (.arg σ .pure) = ((ασ → ULift Prop) × PUnit) := rfl
example : PostCond α (.except ε .pure) = ((α → ULift Prop) × (ε → ULift Prop) × PUnit) := rfl
example : PostCond α (.arg σ (.except ε .pure)) = ((ασ → ULift Prop) × (ε → ULift Prop) × PUnit) := rfl
example : PostCond α (.except ε (.arg σ .pure)) = ((ασ → ULift Prop) × (ε → σ → ULift Prop) × PUnit) := rfl
end PostCond