Compare commits

...

8 Commits

Author SHA1 Message Date
Kim Morrison
189cea9f80 chore: check for empty PRs in CI (#12956)
This PR adds a CI check that fails when a PR introduces no changes
compared to its base branch. This catches cases where a duplicate PR is
queued for merge after an identical PR has already landed (as happened
with https://github.com/leanprover/lean4/pull/12876 and
https://github.com/leanprover/lean4/pull/12877).

The check is added as a second job in the existing `check-stage0.yml`
workflow, which already has the same trigger conditions and git setup
pattern. On `pull_request` events it diffs against the merge base; on
`merge_group` events it diffs `HEAD^1..HEAD` (the PR's contribution to
the synthetic merge commit). Note that batched merge groups are treated
as a unit — if the entire group is non-empty the check passes, which is
the right behaviour for lean4's typical single-PR queuing.

🤖 Prepared with Claude Code

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 03:09:52 +00:00
Kim Morrison
b9028fa6e9 fix: handle lean4-nightly toolchain prefix in release checklist (#12865)
This PR fixes a crash in release_checklist.py when a repository uses the
`leanprover/lean4-nightly:` toolchain prefix (e.g. leansqlite). The
`is_version_gte` function only checked for `leanprover/lean4:nightly-`
but
not `leanprover/lean4-nightly:`, causing a `ValueError: invalid literal
for
int() with base 10: 'nightly'` when trying to parse the version.

🤖 Prepared with Claude Code

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 03:01:58 +00:00
Leonardo de Moura
0c0edcc96c feat: add control and arrow_telescope simproc DSL primitives (#13048)
This PR adds two new `sym_simproc` DSL primitives and helper grind-mode
tactics.

Simproc primitives:
- `control` — simplifies control-flow expressions (`if-then-else`,
  `match`, `cond`, `dite`), visiting only conditions and discriminants.
  Intended as a `pre` simproc.
- `arrow_telescope` — simplifies arrow telescopes
  (`p₁ → p₂ → ... → q`) without entering binders. Intended as a `pre`
  simproc.

Grind-mode tactics:
- `show_goals` — displays pending goals (non-terminal `trace_state` for
  grind mode)
- `exact e` — macro delegating to `tactic => exact e`

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 02:19:13 +00:00
Leonardo de Moura
9f4db470c4 feat: add permutation theorem support to Sym.simp (#13046)
This PR prevents `Sym.simp` from looping on permutation theorems like
`∀ x y, x + y = y + x`.

- Add `perm : Bool` field to `Theorem`
- Add `isPerm` that checks if LHS and RHS have the same structure with
  pattern variables (de Bruijn indices) rearranged via a consistent
  bijection. Uses `ReaderT` (offset for binder entry), `StateT`
  (forward/backward maps), `ExceptT` (failure).
- Compute `perm` in `mkTheoremFromDecl` / `mkTheoremFromExpr`
- In `Theorem.rewrite`, when `perm` is true, only apply the rewrite if
  the result is strictly less than the input (using `acLt`)
- Tests include the classic AC normalization stress test with
  `add_comm`, `add_assoc`, `add_left_comm`

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 00:22:36 +00:00
Kim Morrison
8ae39633d1 fix: mark auxiliary definitions from normalizeInstance as meta (#13043)
This PR fixes a bug where `inferInstanceAs` and the default `deriving`
handler, when used inside a `meta section`, would create auxiliary
definitions (via `normalizeInstance`) that were not marked as `meta`.
This caused the compiler to reject the parent `meta` definition with:

```
Invalid `meta` definition `instEmptyCollectionNamePrefixRel`, `instEmptyCollectionNamePrefixRel._aux_1` not marked `meta`
```

The fix adds an `isMeta` parameter to `normalizeInstance` that is
propagated from the elaboration context (`isMarkedMeta` for
`inferInstanceAs`, `Scope.isMeta` for the deriving handler), and marks
each auxiliary definition created by `mkAuxDefinition` as `meta` when
appropriate.

Found while adapting Mathlib to
https://github.com/leanprover/lean4/pull/12897.

🤖 Prepared with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:41:57 +00:00
Leonardo de Moura
cffacf1b10 feat: support local hypotheses in simp [h] for sym => mode (#13042)
This PR extends the `simp` tactic in `sym =>` mode to support local
hypotheses in the extra theorem list.

`simp myVariant [h]` now resolves `h` against the local context first,
falling back to global constants. Local hypotheses are converted to
rewrite rules via `mkTheoremFromExpr`, which applies the `eq_true`/
`eq_false`/`propext` adapter from #13041.

- Add `ExtraTheorem` inductive (`.const` / `.fvar`) for cache keying
- Add `resolveExtraTheorems` that checks the local context before
globals
- Update `addExtraTheorems`, `mkDefaultMethods`, `elabVariant`
signatures

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 21:50:31 +00:00
Leonardo de Moura
b858d0fbf2 feat: adapt non-equality theorems in mkTheoremFromDecl (#13041)
This PR extends `mkTheoremFromDecl` and `mkTheoremFromExpr` to handle
theorems whose conclusion is not an equality, enabling `Sym.simp` to use
a broader class of lemmas as rewrite rules.

Adaptations:
- `¬ p` → `p = False` via `eq_false`
- `p ↔ q` → `p = q` via `propext`
- `p` (proposition) → `p = True` via `eq_true`

Conjunctions (`p ∧ q`) are not handled here since the `SymM` E-graph
aggressively splits them via case analysis.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:34:37 +00:00
Leonardo de Moura
9a3678935d feat: add sanity checks to register_sym_simp (#13040)
This PR adds validation to the `register_sym_simp` command:

- Reject duplicate variant names
- Validate `pre`/`post` syntax by elaborating them via `elabSymSimproc`
  in a minimal `GrindTacticM` context, catching unknown theorem names
  and unknown theorem set references at registration time

Adds `withGrindTacticM` helper for running `GrindTacticM` from
`CommandElabM`,
and `validateOptionSimprocSyntax` for optional syntax validation.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:41:30 +00:00
22 changed files with 591 additions and 119 deletions

29
.github/workflows/check-empty-pr.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Check for empty PR
on:
merge_group:
pull_request:
jobs:
check-empty-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
filter: tree:0
- name: Check for empty diff
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
base=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
else
base=$(git rev-parse HEAD^1)
fi
if git diff --quiet "$base" HEAD --; then
echo "This PR introduces no changes compared to its base branch." | tee "$GITHUB_STEP_SUMMARY"
echo "It may be a duplicate of an already-merged PR." | tee -a "$GITHUB_STEP_SUMMARY"
exit 1
fi
shell: bash

View File

@@ -236,7 +236,7 @@ def parse_version(version_str):
def is_version_gte(version1, version2):
"""Check if version1 >= version2, including proper handling of release candidates."""
# Check if version1 is a nightly toolchain
if version1.startswith("leanprover/lean4:nightly-"):
if version1.startswith("leanprover/lean4:nightly-") or version1.startswith("leanprover/lean4-nightly:"):
return False
return parse_version(version1) >= parse_version(version2)

View File

@@ -107,6 +107,9 @@ syntax (name := showLocalThms) "show_local_thms" : grind
-/
syntax (name := showTerm) "show_term " grindSeq : grind
/-- Shows the pending goals. -/
syntax (name := showGoals) "show_goals" : grind
declare_syntax_cat grind_ref (behavior := both)
syntax:max anchor : grind_ref
@@ -315,5 +318,8 @@ Only available in `sym =>` mode.
-/
syntax (name := symSimp) "simp" (ppSpace colGt ident)? (" [" ident,* "]")? : grind
/-- `exact e` closes the main goal if its target type matches that of `e`. -/
macro "exact " e:term : grind => `(grind| tactic => exact $e:term)
end Grind
end Lean.Parser.Tactic

View File

@@ -49,6 +49,14 @@ syntax (name := ground) "ground" : sym_simproc
/-- Simplify telescope binders but not the final body. -/
syntax (name := telescope) "telescope" : sym_simproc
/-- Simplify control-flow expressions (`if-then-else`, `match`, `cond`, `dite`).
Visits only conditions and discriminants. Intended as a `pre` simproc. -/
syntax (name := control) "control" : sym_simproc
/-- Simplify arrow telescopes (`p₁ → p₂ → ... → q`) without entering binders.
Simplifies each `pᵢ` and `q` individually. Intended as a `pre` simproc. -/
syntax (name := arrowTelescope) "arrow_telescope" : sym_simproc
/-- Rewrite using a named theorem set. Optionally specify a discharger for conditional rewrites. -/
syntax (name := rewriteSet) "rewrite" ident (" with " sym_discharger)? : sym_simproc

View File

@@ -329,7 +329,8 @@ private def mkSilentAnnotationIfHole (e : Expr) : TermElabM Expr := do
let inst if backward.inferInstanceAs.wrap.get ( getOptions) then
-- Normalize to instance normal form.
let logCompileErrors := !( read).isNoncomputableSection && !( read).declName?.any (Lean.isNoncomputable ( getEnv))
withNewMCtxDepth <| normalizeInstance inst expectedType (logCompileErrors := logCompileErrors)
let isMeta := ( read).isMetaSection
withNewMCtxDepth <| normalizeInstance inst expectedType (logCompileErrors := logCompileErrors) (isMeta := isMeta)
else
pure inst
ensureHasType expectedType? inst

View File

@@ -666,7 +666,8 @@ private def mkTermContext (ctx : Context) (s : State) : CommandElabM Term.Contex
return {
macroStack := ctx.macroStack
sectionVars := sectionVars
isNoncomputableSection := scope.isNoncomputable }
isNoncomputableSection := scope.isNoncomputable
isMetaSection := scope.isMeta }
/--
Lift the `TermElabM` monadic action `x` into a `CommandElabM` monadic action.

View File

@@ -220,10 +220,12 @@ def processDefDeriving (view : DerivingClassView) (decl : Expr) (isNoncomputable
instName liftMacroM <| mkUnusedBaseName instName
if isPrivateName declName then
instName := mkPrivateName env instName
let isMeta := ( read).isMetaSection
let inst if backward.inferInstanceAs.wrap.get ( getOptions) then
withDeclNameForAuxNaming instName <| withNewMCtxDepth <|
normalizeInstance result.instVal result.instType
(logCompileErrors := false) -- covered by noncomputable check below
(isMeta := isMeta)
else
pure result.instVal
let closure Closure.mkValueTypeClosure result.instType inst (zetaDelta := true)

View File

@@ -25,10 +25,16 @@ structure Context extends Tactic.Context where
open Meta.Grind (Goal)
/-- Cache key for `Sym.simp` variant invocations: variant name + ordered extra theorem names. -/
/-- An extra theorem passed to `simp` in `sym =>` mode. -/
inductive ExtraTheorem where
| const (declName : Name)
| fvar (fvarId : FVarId)
deriving BEq, Hashable
/-- Cache key for `Sym.simp` variant invocations. -/
structure SimpCacheKey where
variant : Name
extras : List Name
extras : Array ExtraTheorem
deriving BEq, Hashable
structure Cache where

View File

@@ -76,6 +76,10 @@ def evalGrindSeq : GrindTactic := fun stx =>
@[builtin_grind_tactic skip] def evalSkip : GrindTactic := fun _ =>
return ()
@[builtin_grind_tactic showGoals] def evalShowGoals : GrindTactic := fun _ => do
let goals getUnsolvedGoalMVarIds
addRawTrace (goalsToMessageData goals)
@[builtin_grind_tactic paren] def evalParen : GrindTactic := fun stx =>
evalGrindTactic stx[1]

View File

@@ -7,14 +7,42 @@ module
prelude
import Init.Sym.Simp.SimprocDSL
import Lean.Meta.Sym.Simp.Variant
import Lean.Elab.Tactic.Grind.SimprocDSL
import Lean.Elab.Command
namespace Lean.Elab.Command
open Meta Sym.Simp
/--
Runs a `GrindTacticM` computation in a minimal context for validation.
-/
def withGrindTacticM (k : Tactic.Grind.GrindTacticM α) : CommandElabM α := do
liftTermElabM do
let params Grind.mkDefaultParams {}
let (ctx, state) Grind.GrindM.run (params := params) do
let methods Grind.getMethods
let grindCtx readThe Meta.Grind.Context
let symCtx readThe Sym.Context
let grindState get
let symState getThe Sym.State
let ctx := {
elaborator := `registerSymSimp,
ctx := grindCtx, sctx := symCtx, methods, params
}
return (ctx, { grindState, symState, goals := [] })
let (a, _) Tactic.Grind.GrindTacticM.run k ctx state
return a
def validateOptionSimprocSyntax (proc? : Option Syntax) : CommandElabM Unit := do
let some proc := proc? | return ()
discard <| withGrindTacticM <| Tactic.Grind.elabSymSimproc proc
@[builtin_command_elab Lean.Parser.Command.registerSymSimp]
def elabRegisterSymSimp : CommandElab := fun stx => do
let id := stx[1]
let name := id.getId
-- Check for duplicate variant
if (getSymSimpVariant? ( getEnv) name).isSome then
throwErrorAt id "Sym.simp variant `{name}` is already registered"
let fields := stx[3].getArgs
let mut pre? : Option Syntax := none
let mut post? : Option Syntax := none
@@ -35,7 +63,10 @@ def elabRegisterSymSimp : CommandElab := fun stx => do
unless post?.isNone do throwErrorAt field "duplicate `post` field"
post? := some proc
| _ => throwErrorAt field "unexpected field"
let config := { maxSteps := maxSteps?.getD 100_000, maxDischargeDepth := maxDischargeDepth?.getD 2 }
-- Validate pre/post by elaborating them
validateOptionSimprocSyntax pre?
validateOptionSimprocSyntax post?
let config := { maxSteps := maxSteps?.getD 100_000, maxDischargeDepth := maxDischargeDepth?.getD 2 }
let variant : SymSimpVariant := { pre?, post?, config }
modifyEnv fun env => symSimpVariantExtension.addEntry env { name, variant }

View File

@@ -9,6 +9,8 @@ import Lean.Elab.Tactic.Grind.SimprocDSL
import Init.Sym.Simp.SimprocDSL
import Lean.Meta.Sym.Simp.EvalGround
import Lean.Meta.Sym.Simp.Telescope
import Lean.Meta.Sym.Simp.ControlFlow
import Lean.Meta.Sym.Simp.Forall
import Lean.Meta.Sym.Simp.Rewrite
namespace Lean.Elab.Tactic.Grind
open Meta Sym.Simp
@@ -23,6 +25,14 @@ def elabSimprocGround : SymSimprocElab := fun _ =>
def elabSimprocTelescope : SymSimprocElab := fun _ =>
return simpTelescope
@[builtin_sym_simproc Lean.Parser.Sym.Simp.control]
def elabSimprocControl : SymSimprocElab := fun _ =>
return simpControl
@[builtin_sym_simproc Lean.Parser.Sym.Simp.arrowTelescope]
def elabSimprocArrowTelescope : SymSimprocElab := fun _ =>
return simpArrowTelescope
@[builtin_sym_simproc self]
def elabSimprocSelf : SymSimprocElab := fun _ =>
return simp

View File

@@ -153,39 +153,54 @@ def elabOptSimproc (stx? : Option Syntax) : GrindTacticM Simproc := do
let some stx := stx? | return trivialSimproc
elabSymSimproc stx
def addExtraTheorems (post : Simproc) (extraNames : Array Name) : GrindTacticM Simproc := do
if extraNames.isEmpty then return post
def resolveExtraTheorems (ids? : Option (Array (TSyntax `ident))) : GrindTacticM (Array ExtraTheorem × Array Theorem) := do
let some ids := ids? | return (#[], #[])
let mut extras := #[]
let mut thms := #[]
let lctx getLCtx
for id in ids do
if let some decl := lctx.findFromUserName? id.getId then
extras := extras.push <| .fvar decl.fvarId
thms := thms.push ( mkTheoremFromExpr decl.toExpr)
else
let declName realizeGlobalConstNoOverload id
extras := extras.push <| .const declName
thms := thms.push ( mkTheoremFromDecl declName)
return (extras, thms)
def addExtraTheorems (post : Simproc) (extraThms : Array Theorem) : GrindTacticM Simproc := do
if extraThms.isEmpty then return post
let mut thms : Theorems := {}
for name in extraNames do
thms := thms.insert ( mkTheoremFromDecl name)
for thm in extraThms do
thms := thms.insert thm
return post >> thms.rewrite
def mkDefaultMethods (extraNames : Array Name) : GrindTacticM Sym.Simp.Methods := do
def mkDefaultMethods (extraThms : Array Theorem) : GrindTacticM Sym.Simp.Methods := do
let thms getSymSimpTheorems
let pre := simpControl >> simpArrowTelescope
let post addExtraTheorems (evalGround >> thms.rewrite) extraNames
let post addExtraTheorems (evalGround >> thms.rewrite) extraThms
return { pre, post }
def elabVariant (variantName : Name) (extraNames : Array Name) : GrindTacticM (Sym.Simp.Methods × Sym.Simp.Config) := do
def elabVariant (variantName : Name) (extraThms : Array Theorem) : GrindTacticM (Sym.Simp.Methods × Sym.Simp.Config) := do
if variantName.isAnonymous then
return ( mkDefaultMethods extraNames, {})
return ( mkDefaultMethods extraThms, {})
let some v := getSymSimpVariant? ( getEnv) variantName
| throwError "unknown Sym.simp variant `{variantName}`"
let pre elabOptSimproc v.pre?
let post addExtraTheorems ( elabOptSimproc v.post?) extraNames
let post addExtraTheorems ( elabOptSimproc v.post?) extraThms
return ({ pre, post}, v.config)
@[builtin_grind_tactic Parser.Tactic.Grind.symSimp] def evalSymSimp : GrindTactic := fun stx => do
@[builtin_grind_tactic Parser.Tactic.Grind.symSimp] def evalSymSimp : GrindTactic := fun stx => withMainContext do
ensureSym
let `(grind| simp $[$variantId?]? $[[ $[$extraIds],* ]]?) := stx | throwUnsupportedSyntax
-- Resolve variant
let variantName := variantId?.map (·.getId) |>.getD .anonymous
-- Compose extra theorems into post
let extraNames (extraIds.getD #[]).mapM fun id => realizeGlobalConstNoOverload id
-- Resolve extra theorems (local hypotheses first, then global constants)
let (extras, thms) resolveExtraTheorems extraIds
-- Cache lookup/creation
let cacheKey : SimpCacheKey := { variant := variantName, extras := extraNames.toList }
let cacheKey : SimpCacheKey := { variant := variantName, extras }
let simpState := ( get).cache.simpState[cacheKey]?.getD {}
let (methods, config) elabVariant variantName extraNames
let (methods, config) elabVariant variantName thms
let goal getMainGoal
let (simpResult, simpState) liftGrindM <| goal.withContext do
Sym.Simp.SimpM.run (Sym.Simp.simp ( goal.mvarId.getType)) methods config simpState

View File

@@ -309,6 +309,8 @@ structure Context where
heedElabAsElim : Bool := true
/-- Noncomputable sections automatically add the `noncomputable` modifier to any declaration we cannot generate code for. -/
isNoncomputableSection : Bool := false
/-- `true` when inside a `meta section`. -/
isMetaSection : Bool := false
/-- When `true` we skip TC failures. We use this option when processing patterns. -/
ignoreTCFailures : Bool := false
/-- `true` when elaborating patterns. It affects how we elaborate named holes. -/

View File

@@ -99,7 +99,7 @@ Normalize an instance value to "instance normal form".
See the module docstring for the full algorithm specification.
-/
partial def normalizeInstance (inst expectedType : Expr) (compile : Bool := true)
(logCompileErrors : Bool := true) : MetaM Expr := withTransparency .instances do
(logCompileErrors : Bool := true) (isMeta : Bool := false) : MetaM Expr := withTransparency .instances do
withTraceNode `Meta.instanceNormalForm
(fun _ => return m!"type: {expectedType}") do
let some className isClass? expectedType
@@ -124,9 +124,11 @@ partial def normalizeInstance (inst expectedType : Expr) (compile : Bool := true
return inst
else
let name mkAuxDeclName
let wrapped mkAuxDefinition name expectedType inst (compile := compile)
(logCompileErrors := logCompileErrors)
let wrapped mkAuxDefinition name expectedType inst (compile := false)
setReducibilityStatus name .implicitReducible
if isMeta then modifyEnv (markMeta · name)
if compile then
compileDecls (logErrors := logCompileErrors) #[name]
enableRealizationsForConst name
return wrapped
else
@@ -169,7 +171,7 @@ partial def normalizeInstance (inst expectedType : Expr) (compile : Bool := true
catch _ => pure ()
mvarId.assign ( normalizeInstance arg argExpectedType (compile := compile)
(logCompileErrors := logCompileErrors))
(logCompileErrors := logCompileErrors) (isMeta := isMeta))
else
-- For data fields, assign directly or wrap in aux def to fix types.
if backward.inferInstanceAs.wrap.data.get ( getOptions) then
@@ -180,6 +182,7 @@ partial def normalizeInstance (inst expectedType : Expr) (compile : Bool := true
let name mkAuxDeclName
mvarId.assign ( mkAuxDefinition name argExpectedType arg (compile := false))
setInlineAttribute name
if isMeta then modifyEnv (markMeta · name)
if compile then
compileDecls (logErrors := logCompileErrors) #[name]
enableRealizationsForConst name

View File

@@ -9,6 +9,7 @@ public import Lean.Meta.Sym.Simp.Simproc
public import Lean.Meta.Sym.Simp.Theorems
public import Lean.Meta.Sym.Simp.App
public import Lean.Meta.Sym.Simp.Discharger
import Lean.Meta.ACLt
import Lean.Meta.Sym.InstantiateS
import Lean.Meta.Sym.InstantiateMVarsS
import Init.Data.Range.Polymorphic.Iterators
@@ -71,10 +72,16 @@ public def Theorem.rewrite (thm : Theorem) (e : Expr) (d : Discharger := dischar
let expr instantiateRevBetaS rhs args.toArray
if isSameExpr e expr then
return mkRflResultCD isCD
else if !( checkPerm thm.perm e expr) then
return mkRflResultCD isCD
else
return .step expr proof (contextDependent := isCD)
else
return .rfl
where
checkPerm (perm : Bool) (e result : Expr) : MetaM Bool := do
if !perm then return true
acLt result e
public def Theorems.rewrite (thms : Theorems) (d : Discharger := dischargeNone) : Simproc := fun e => do
-- Track `cd` across all attempted theorems. If theorem A fails with cd=true

View File

@@ -8,7 +8,9 @@ prelude
public import Lean.Meta.Sym.Pattern
public import Lean.Meta.DiscrTree
import Lean.Meta.Sym.Simp.DiscrTree
import Lean.Meta.AppBuilder
import Lean.ExtraModUses
import Init.Omega
public section
namespace Lean.Meta.Sym.Simp
@@ -25,6 +27,10 @@ structure Theorem where
pattern : Pattern
/-- Right-hand side of the equation. -/
rhs : Expr
/-- If `true`, the theorem is a permutation rule (e.g., `x + y = y + x`).
Rewriting is only applied when the result is strictly less than the input
(using `acLt`), preventing infinite loops. -/
perm : Bool := false
deriving Inhabited
instance : BEq Theorem where
@@ -44,9 +50,112 @@ def Theorems.getMatch (thms : Theorems) (e : Expr) : Array Theorem :=
def Theorems.getMatchWithExtra (thms : Theorems) (e : Expr) : Array (Theorem × Nat) :=
Sym.getMatchWithExtra thms.thms e
/--
Check whether `lhs` and `rhs` (with `numVars` pattern variables represented as `.bvar` indices
`≥ 0` before any binder entry) are permutations of each other — same structure with only
pattern variable indices rearranged via a consistent bijection.
Bvars with index `< offset` are "local" (introduced by binders inside the pattern) and must
match exactly. Bvars with index `≥ offset` are pattern variables and may be permuted,
but the mapping must be a bijection.
Simplified compared to `Meta.simp`'s `isPerm`:
- Uses de Bruijn indices instead of metavariables
- No `.proj` (folded into applications) or `.letE` (zeta-expanded) cases
-/
private abbrev IsPermM := ReaderT Nat $ StateT (Array (Option Nat)) $ Except Unit
private partial def isPermAux (a b : Expr) : IsPermM Unit := do
match a, b with
| .bvar i, .bvar j =>
let offset read
if i < offset && j < offset then
unless i == j do throw ()
else if i >= offset && j >= offset then
let pi := i - offset
let pj := j - offset
let fwd get
if h : pi >= fwd.size then throw () else
match fwd[pi] with
| none =>
-- Check injectivity: pj must not already be a target of another mapping
if fwd.contains (some pj) then throw ()
set (fwd.set pi (some pj))
| some pj' => unless pj == pj' do throw ()
else throw ()
| .app f₁ a₁, .app f₂ a₂ => isPermAux f₁ f₂; isPermAux a₁ a₂
| .mdata _ s, t => isPermAux s t
| s, .mdata _ t => isPermAux s t
| .forallE _ d₁ b₁ _, .forallE _ d₂ b₂ _ => isPermAux d₁ d₂; withReader (· + 1) (isPermAux b₁ b₂)
| .lam _ d₁ b₁ _, .lam _ d₂ b₂ _ => isPermAux d₁ d₂; withReader (· + 1) (isPermAux b₁ b₂)
| s, t => unless s == t do throw ()
def isPerm (numVars : Nat) (lhs rhs : Expr) : Bool :=
((isPermAux lhs rhs).run 0 |>.run (Array.replicate numVars none)) matches .ok _
/-- Describes how a theorem's conclusion was adapted to an equality for use in `Sym.simp`. -/
private inductive EqAdaptation where
/-- Already an equality `lhs = rhs`. Proof is used as-is. -/
| eq
/-- Was `¬ p`. Proof `h` adapted to `eq_false h : p = False`. -/
| eqFalse
/-- Was `p ↔ q`. Proof `h` adapted to `propext h : p = q`. -/
| iff
/-- Was a proposition `p`. Proof `h` adapted to `eq_true h : p = True`. -/
| eqTrue
/--
Analyze the conclusion of a theorem type and extract `(lhs, rhs)` for use as a
rewrite rule in `Sym.simp`. Handles:
- `lhs = rhs` — used as-is
- `¬ p` — adapted to `p = False`
- `p ↔ q` — adapted to `p = q`
- `p` (proposition) — adapted to `p = True`
-/
private def selectEqKey (type : Expr) : MetaM (Expr × Expr × EqAdaptation) := do
match_expr type with
| Eq _ lhs rhs => return (lhs, rhs, .eq)
| Not p => return (p, mkConst ``False, .eqFalse)
| Iff lhs rhs => return (lhs, rhs, .iff)
| _ =>
unless ( isProp type) do
throwError "cannot use as a simp theorem, conclusion is not a proposition{indentExpr type}"
return (type, mkConst ``True, .eqTrue)
/--
Wrap a proof expression according to the adaptation applied to its type.
Given a proof `h : <original type>`, returns a proof of the adapted equality.
This wrapping must be applied AFTER the proof has been applied to its quantified arguments.
-/
private def wrapProof (numVars : Nat) (expr : Expr) (adaptation : EqAdaptation) : MetaM Expr :=
match adaptation with
| .eq => return expr
| .eqFalse =>
wrapInner numVars expr fun h => mkAppM ``eq_false #[h]
| .iff =>
wrapInner numVars expr fun h => mkAppM ``propext #[h]
| .eqTrue =>
wrapInner numVars expr fun h => mkAppM ``eq_true #[h]
where
/-- Wraps the innermost application of `expr` (after `numVars` arguments) with `wrap`. -/
wrapInner (numVars : Nat) (expr : Expr) (wrap : Expr MetaM Expr) : MetaM Expr := do
let type inferType expr
forallBoundedTelescope type numVars fun xs _ => do
let h := mkAppN expr xs
mkLambdaFVars xs ( wrap h)
def mkTheoremFromDecl (declName : Name) : MetaM Theorem := do
let (pattern, rhs) mkEqPatternFromDecl declName
return { expr := mkConst declName, pattern, rhs }
let (pattern, (rhs, adaptation)) mkPatternFromDeclWithKey declName selectEqKey
let expr wrapProof pattern.varTypes.size (mkConst declName) adaptation
let perm := isPerm pattern.varTypes.size pattern.pattern rhs
return { expr, pattern, rhs, perm }
/-- Create a `Theorem` from a proof expression. Handles equalities, `¬`, `↔`, and propositions. -/
def mkTheoremFromExpr (e : Expr) : MetaM Theorem := do
let (pattern, (rhs, adaptation)) mkPatternFromExprWithKey e [] selectEqKey
let expr wrapProof pattern.varTypes.size e adaptation
let perm := isPerm pattern.varTypes.size pattern.pattern rhs
return { expr, pattern, rhs, perm }
/--
Environment extension storing a set of `Sym.Simp` theorems.

12
tests/elab/12897.lean Normal file
View File

@@ -0,0 +1,12 @@
-- Tests that `inferInstanceAs` auxiliary definitions are properly marked `meta`
-- when used inside a `meta` section.
module
meta section
def Foo := List Nat
instance : EmptyCollection Foo := inferInstanceAs (EmptyCollection (List Nat))
end

View File

@@ -0,0 +1,99 @@
import Lean
set_option linter.unusedVariables false
set_option warn.sorry false
/-! Tests for `mkTheoremFromDecl` adaptation of non-equality theorems -/
opaque p : Nat Prop
opaque q : Nat Prop
-- Equality theorem (no adaptation needed)
theorem eq_thm : p x = q x := sorry
-- Negation theorem: `¬ p x` adapted to `p x = False`
theorem neg_thm : ¬ p x := sorry
-- Iff theorem: `p x ↔ q x` adapted to `p x = q x`
theorem iff_thm : p x q x := sorry
-- Proposition theorem: `p x` adapted to `p x = True`
theorem prop_thm : p x := sorry
-- Quantified negation: `∀ x, ¬ p x` adapted to `∀ x, p x = False`
theorem quant_neg : x, ¬ p x := sorry
-- Quantified prop: `∀ x, p x` adapted to `∀ x, p x = True`
theorem quant_prop : x, p x := sorry
-- Test: `simp` with a proposition theorem (`h : p x`) rewrites `p x` to `True`
register_sym_simp propSimp where
post := ground >> rewrite [prop_thm]
example : p x = True := by
sym => simp propSimp
-- Test: `simp` with a negation theorem (`h : ¬ p x`) rewrites `p x` to `False`
register_sym_simp negSimp where
post := ground >> rewrite [neg_thm]
example : p x = False := by
sym => simp negSimp
-- Test: `simp` with an iff theorem (`h : p x ↔ q x`) rewrites `p x` to `q x`
register_sym_simp iffSimp where
post := ground >> rewrite [iff_thm]
example : p x = q x := by
sym => simp iffSimp
-- Test: quantified prop still works
register_sym_simp quantPropSimp where
post := ground >> rewrite [quant_prop]
example (y : Nat) : p y = True := by
sym => simp quantPropSimp
-- Test: quantified negation still works
register_sym_simp quantNegSimp where
post := ground >> rewrite [quant_neg]
example (y : Nat) : p y = False := by
sym => simp quantNegSimp
register_sym_simp simple where
post := ground
example (x : Nat) : p x := by
sym => simp simple [quant_prop]
example (x : Nat) : ¬ p x := by
sym => simp simple [quant_neg]
example (x : Nat) : p x = q x := by
sym => simp simple [iff_thm]
-- Tests for local hypothesis support in `simp [h]`
-- Local hypothesis `h : p x` rewrites `p x` to `True`
example (x : Nat) (h : p x) : p x = True := by
sym => simp simple [h]
-- Local hypothesis `h : ¬ p x` rewrites `p x` to `False`
example (x : Nat) (h : ¬ p x) : p x = False := by
sym => simp simple [h]
-- Local hypothesis `h : p x ↔ q x` rewrites `p x` to `q x`
example (x : Nat) (h : p x q x) : p x = q x := by
sym => simp simple [h]
-- Local hypothesis `h : p x = q x` (already an equality)
example (x : Nat) (h : p x = q x) : p x = q x := by
sym => simp simple [h]
-- Local hypothesis with intro
example (x : Nat) : p x p x = True := by
sym =>
intro h
simp simple [h]
example (h : x, p x = q x) : p a = q a p b = q b := by
sym => simp simple [h, and_true]

View File

@@ -42,153 +42,120 @@ example : 2 + 3 = 5 := by
-- and lands in the transient cache. On the second invocation, the transient cache is
-- cleared, so there should be NO persistent cache hit for the overall expression.
-- Only context-independent sub-expressions (literals, fvars) get persistent cache hits.
theorem Nat.add_comm_of_pos (a b : Nat) (_h : 0 < a) : a + b = b + a := Nat.add_comm a b
opaque f : Nat Nat
axiom f_idem (a : Nat) (_h : 0 < a) : f (f a) = f a
set_option trace.sym.simp.debug.cache true in
/--
trace: [sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: 2 + n
trace: [sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] transient cache hit: f (f n)
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] second traversal
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: 2 + n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] transient cache hit: f (f n)
[sym.simp.debug.cache] persistent cache hit: f n
-/
#guard_msgs in
example (n : Nat) (h : 0 < n) : n + 2 = 2 + n := by
sym_simp_twice [Nat.add_comm_of_pos]
example (n : Nat) (h : 0 < n) : f (f n) = f (f (f n)) := by
sym_simp_twice [f_idem]
-- Test 3: Congruence — cd propagates through function application.
-- `n + 2` rewrites context-dependently (cd=true), `3 + 4` evaluates ground (cd=false).
-- The congruence combines both, so the overall result is cd=true.
-- On second traversal: ground sub-expressions (`3 + 4`, `7`) hit persistent cache,
-- but cd-tainted expressions (`2 + n`, `2 + n + 7`) are only in transient.
set_option trace.sym.simp.debug.cache true in
/--
trace: [sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: 2 + n
[sym.simp.debug.cache] transient cache hit: (2 + n) * 7
trace: [sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n + 7
[sym.simp.debug.cache] second traversal
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: 3 + 4
[sym.simp.debug.cache] transient cache hit: 2 + n
[sym.simp.debug.cache] persistent cache hit: 7
[sym.simp.debug.cache] transient cache hit: (2 + n) * 7
[sym.simp.debug.cache] persistent cache hit: f n + 7
[sym.simp.debug.cache] persistent cache hit: f n + 7
-/
#guard_msgs in
example (n : Nat) (h : 0 < n) : (n + 2) * (3 + 4) = (2 + n) * 7 := by
sym_simp_twice [Nat.add_comm_of_pos]
example (n : Nat) (h : 0 < n) : f (f n) + (3 + 4) = f n + 7 := by
sym_simp_twice [f_idem]
-- Similar to previous test, but `Nat.add_comm_of_pos` is not applicable, but discharger must return `cd := true`.
-- Similar to previous test, but `f_idem` is not applicable (no hypothesis), but discharger must return `cd := true`.
set_option trace.sym.simp.debug.cache true in
/--
trace: [sym.simp.debug.cache] transient cache hit: n + 2
[sym.simp.debug.cache] transient cache hit: (n + 2) * 7
trace: [sym.simp.debug.cache] transient cache hit: f (f n)
[sym.simp.debug.cache] transient cache hit: f (f n) + 7
[sym.simp.debug.cache] second traversal
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: 3 + 4
[sym.simp.debug.cache] transient cache hit: n + 2
[sym.simp.debug.cache] transient cache hit: f (f n)
[sym.simp.debug.cache] persistent cache hit: 7
[sym.simp.debug.cache] transient cache hit: (n + 2) * 7
[sym.simp.debug.cache] transient cache hit: f (f n) + 7
-/
#guard_msgs in
example (n : Nat) : (n + 2) * (3 + 4) = (n + 2) * 7 := by
sym_simp_twice [Nat.add_comm_of_pos]
example (n : Nat) : f (f n) + (3 + 4) = f (f n) + 7 := by
sym_simp_twice [f_idem]
-- Test 4: Arrow — cd propagates through implication.
-- The hypothesis `n + 2 = 2 + n` is simplified context-dependently to `True`.
-- `True → True` simplifies to `True`. The whole result is cd=true.
-- `True` hits persistent cache; `2 + n` is only in transient.
set_option trace.sym.simp.debug.cache true in
/--
trace: [sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: 2 + n
trace: [sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] second traversal
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: 2 + n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: True
-/
#guard_msgs in
set_option linter.unusedVariables false in
example (n : Nat) (h : 0 < n) : (n + 2 = 2 + n) True := by
sym_simp_twice [Nat.add_comm_of_pos]
example (n : Nat) (h : 0 < n) : (f (f n) = f n) True := by
sym_simp_twice [f_idem]
-- Test 5: Lambda — cd propagates through funext.
-- Body `n + 2` is simplified context-dependently inside the binder.
-- `withFreshTransientCache` clears the transient cache on binder entry.
-- The lambda result `fun x => 2 + n` is only in transient.
set_option trace.sym.simp.debug.cache true in
/--
trace: [sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: fun x => 2 + n
trace: [sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: fun x => f n
[sym.simp.debug.cache] second traversal
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: fun x => 2 + n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: fun x => f n
[sym.simp.debug.cache] persistent cache hit: fun x => f n
-/
#guard_msgs in
example (n : Nat) (_h : 0 < n) : (fun _ : Nat => n + 2) = (fun _ : Nat => 2 + n) := by
sym_simp_twice [Nat.add_comm_of_pos]
example (n : Nat) (_h : 0 < n) : (fun _ : Nat => f (f n)) = (fun _ : Nat => f n) := by
sym_simp_twice [f_idem]
-- Test 6: Control flow — cd propagates through `ite` condition.
-- The condition `n + 2 = 2 + n` is simplified context-dependently.
-- The `ite` result inherits cd, and `1` (ground) is in persistent cache.
set_option trace.sym.simp.debug.cache true in
/--
trace: [sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: 2 + n
trace: [sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: 1
[sym.simp.debug.cache] second traversal
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: 2 + n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: 1
[sym.simp.debug.cache] persistent cache hit: 1
-/
#guard_msgs in
example (n : Nat) (h : 0 < n) : (if n + 2 = 2 + n then 1 else 0) = 1 := by
sym_simp_twice [Nat.add_comm_of_pos]
example (n : Nat) (h : 0 < n) : (if f (f n) = f n then 1 else 0) = 1 := by
sym_simp_twice [f_idem]
-- Test 7: Dependent forall — body cd under binder with `withFreshTransientCache`.
-- Simplifying `∀ (m : Nat), n + 2 = 2 + n` enters a binder (for `m`).
-- The transient cache is cleared on binder entry (`withFreshTransientCache`).
-- The body uses a cd rewrite, so the overall result is cd=true.
-- After "second traversal": `Nat` (the binder type) hits persistent cache.
set_option trace.sym.simp.debug.cache true in
/--
trace: [sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: 2 + n
trace: [sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] transient cache hit: f (f n)
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] second traversal
[sym.simp.debug.cache] persistent cache hit: Nat
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: 2
[sym.simp.debug.cache] persistent cache hit: n
[sym.simp.debug.cache] transient cache hit: 2 + n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] persistent cache hit: f n
[sym.simp.debug.cache] transient cache hit: f (f n)
[sym.simp.debug.cache] persistent cache hit: f n
-/
#guard_msgs in
set_option linter.unusedVariables false in
example (n : Nat) (h : 0 < n) : (_ : Nat), n + 2 = 2 + n := by
sym_simp_twice [Nat.add_comm_of_pos]
example (n : Nat) (h : 0 < n) : (_ : Nat), f (f n) = f (f (f n)) := by
sym_simp_twice [f_idem]

View File

@@ -0,0 +1,97 @@
/-! Tests for `control` and `arrow_telescope` simproc DSL primitives.
Based on `sym_simp_4.lean` but using `register_sym_simp` + DSL instead of custom Lean tactics. -/
register_sym_simp mySimp where
pre := control >> arrow_telescope
post := ground >> rewrite [and_true] with self
example : (if true then a else b) = a := by
sym => simp mySimp
example : (if True then a else b) = a := by
sym => simp mySimp
example : (if False then a else b) = b := by
sym => simp mySimp
/--
trace: case grind
α✝ : Sort u_1
x : α✝
p q : Prop
h : p → q
⊢ p → q
-/
#guard_msgs in
example (p q : Prop) (h : p q) : True p x = x q := by
sym =>
simp mySimp
show_goals
exact h
example (p q : Prop) : q p True := by
sym => simp mySimp
example (p q : Prop) : q p x = x := by
sym => simp mySimp
example (q : Prop) : q x x True := by
sym => simp mySimp
example (α : Type) (p : Prop) : α p x = x := by
sym => simp mySimp
example (q : Prop) (α : Type) (p : Prop) : q α p x = x := by
sym => simp mySimp
example (α β : Type) (p q : Prop) : q β p α True := by
sym => simp mySimp
/--
trace: case grind
α : Type u
x : α
p : Prop
h : α → p → True → α
α → p → True → α
-/
#guard_msgs in
example (α : Type u) (x : α) (p : Prop) (h : α p True α) : α p x = x α := by
sym =>
simp mySimp
show_goals
exact h
set_option linter.unusedVariables false
def F := False
/--
trace: case grind
α : Type
x : α
q : Prop
h : F
⊢ ∀ (a b : α), q
-/
#guard_msgs in
example (α : Type) (x : α) (q : Prop) (h : F) : (a : α) x = x (b : α) True q := by
sym =>
simp mySimp
show_goals
exact False.elim h
/--
trace: case grind
α : Sort u
x : α
p q : Prop
h : F
⊢ ∀ (a : α) {b : α}, q
-/
#guard_msgs in
example (α : Sort u) (x : α) (p q : Prop) (h : F) : (a : α) x = x {b : α} True (q True) := by
sym =>
simp mySimp
show_goals
exact False.elim h

View File

@@ -111,3 +111,29 @@ example (f : Nat → Nat) (x y : Nat) (h : f (f (f (f x))) = y) : 0 + x = x ∧
simp simple
try simp simple
apply h
/-! ## Test 11: duplicate variant name is rejected -/
/--
error: Sym.simp variant `simple` is already registered
-/
#guard_msgs in
register_sym_simp simple where
/-! ## Test 12: unknown theorem in `rewrite [...]` is rejected -/
/--
error: Unknown constant `bla`
-/
#guard_msgs in
register_sym_simp simple₁ where
pre := rewrite [bla]
/-! ## Test 13: unknown theorem set in `rewrite setName` is rejected -/
/--
error: unknown Sym.simp theorem set `boo`
-/
#guard_msgs in
register_sym_simp simple₃ where
pre := rewrite boo

View File

@@ -0,0 +1,37 @@
import Lean
/-! Tests for permutation theorem support in `Sym.simp` -/
-- Nat.add_comm is a permutation theorem: x + y = y + x
-- Without perm support, `simp` with this theorem would loop.
register_sym_simp commSimp where
post := ground >> rewrite [Nat.add_comm]
-- This should terminate: Nat.add_comm is detected as perm,
-- and only applied when result < input.
example (x y : Nat) : x + y = y + x := by
sym =>
simp commSimp
-- Combining perm with non-perm theorems
register_sym_simp commZeroSimp where
post := ground >> rewrite [Nat.add_comm, Nat.zero_add, Nat.add_zero]
example (x y : Nat) : 0 + (x + y) = y + x := by
sym =>
simp commZeroSimp
-- Verify perm doesn't interfere with non-perm theorems
register_sym_simp nonPermSimp where
post := ground >> rewrite [Nat.zero_add]
example (x : Nat) : 0 + x = x := by
sym =>
simp nonPermSimp
register_sym_simp simple where
post := ground
example (x y z w : Nat) : x + y + z + w = w + (z + y) + x := by
sym => simp simple [Nat.add_comm, Nat.add_assoc, Nat.add_left_comm]