-
Notifications
You must be signed in to change notification settings - Fork 462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: binderNameHint #6947
Merged
+305
−0
Merged
feat: binderNameHint #6947
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
222dd6e
feat: binderNameHint
nomeata 82e15ff
Also test dsimp (but already works)
nomeata 4dd212d
Actually save file
nomeata f10246d
Update tests (improved simp trace formatting)
nomeata fb8a256
More test updates
nomeata 97f6ff0
set_option Elab.async false -- for stable message ordering in #guard_…
nomeata 81a27d9
undo simp tracing changes
nomeata 7c98ec1
PR splitting mistake
nomeata 4bf9b8f
More docs and tests
nomeata 548b268
Merge branch 'nightly' of https://github.com/leanprover/lean4 into jo…
nomeata 86463de
Ensure that simp and dsimp pre-rewrite with binderNameHint
nomeata 0dfb8e1
Freshen name when binder is not a binder
nomeata 2bbffb9
Test adding macro scopes
nomeata File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,3 +39,4 @@ import Init.While | |
import Init.Syntax | ||
import Init.Internal | ||
import Init.Try | ||
import Init.BinderNameHint |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/- | ||
Copyright (c) 2025 Lean FRO, LLC. All rights reserved. | ||
Released under Apache 2.0 license as described in the file LICENSE. | ||
Authors: Joachim Breitner | ||
-/ | ||
|
||
prelude | ||
import Init.Prelude | ||
import Init.Tactics | ||
|
||
set_option linter.unusedVariables false in | ||
/-- | ||
The expression `binderNameHint v binder e` defined to be `e`. | ||
|
||
If it is used on the right-hand side of an equation that is used for rewriting by `rw` or `simp`, | ||
and `v` is a local variable, and `binder` is an expression that (after beta-reduction) is a binder | ||
(`fun w => …` or `∀ w, …`), then it will rename `v` to the name used in that binder, and remove | ||
the `binderNameHint`. | ||
|
||
A typical use of this gadget would be as follows; the gadget ensures that after rewriting, the local | ||
variable is still `name`, and not `x`: | ||
``` | ||
theorem all_eq_not_any_not (l : List α) (p : α → Bool) : | ||
l.all p = !l.any fun x => binderNameHint x p (!p x) := sorry | ||
|
||
example (names : List String) : names.all (fun name => "Waldo".isPrefixOf name) = true := by | ||
rw [all_eq_not_any_not] | ||
-- ⊢ (!names.any fun name => !"Waldo".isPrefixOf name) = true | ||
``` | ||
|
||
If `binder` is not a binder, then the name of `v` attains a macro scope. This only matters when the | ||
resulting term is used in a non-hygienic way, e.g. in termination proofs for well-founded recursion. | ||
|
||
This gadget is supported by `simp`, `dsimp` and `rw` in the right-hand-side of an equation, but not | ||
in hypotheses or by other tactics. | ||
-/ | ||
@[simp ↓] | ||
def binderNameHint {α : Sort u} {β : Sort v} {γ : Sort w} (v : α) (binder : β) (e : γ) : γ := e | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/- | ||
Copyright (c) 2025 Lean FRO, LLC. All rights reserved. | ||
Released under Apache 2.0 license as described in the file LICENSE. | ||
Authors: Joachim Breitner | ||
-/ | ||
|
||
prelude | ||
|
||
import Lean.Util.FindExpr | ||
import Lean.Meta.Basic | ||
import Init.BinderNameHint | ||
|
||
namespace Lean | ||
|
||
|
||
/-- Does `e` have a binder name hint? (quick check) -/ | ||
def Expr.hasBinderNameHint (e : Expr) : Bool := | ||
Option.isSome <| e.find? fun e => e.isConstOf `binderNameHint | ||
|
||
private def enterScope (name : Name) (xs : Array Name) : Array Name := | ||
xs.push name | ||
|
||
private def exitScope (xs : Array Name) : Name × Array Name := | ||
assert! xs.size > 0 | ||
(xs.back!, xs.pop) | ||
|
||
private def rememberName (bidx : Nat) (name : Name) (xs : Array Name) : Array Name := | ||
assert! xs.size > bidx | ||
xs.set! (xs.size - bidx - 1) name | ||
|
||
private def makeFresh (bidx : Nat) (xs : Array Name) : CoreM (Array Name) := do | ||
assert! xs.size > bidx | ||
let name := xs[xs.size - bidx - 1]! | ||
let name' ← Core.mkFreshUserName name | ||
return xs.set! (xs.size - bidx - 1) name' | ||
|
||
/-- | ||
Resovles occurrences of `binderNameHint` in `e`. See docstring of `binderNameHint` for more | ||
information. | ||
-/ | ||
partial def Expr.resolveBinderNameHint (e : Expr) : CoreM Expr := | ||
(go e).run.run' #[] | ||
where | ||
/- | ||
Implementation note: | ||
|
||
We traverse the expression as an open term; we do not need a local context here. | ||
|
||
The state is the array of binder names. The length of the array is always the binder nesting depth, | ||
and the innermost binder is at the end. We update the binder names therein when encountering a | ||
`binderNameHint`, and update the binder when exiting the scope. | ||
-/ | ||
go (e : Expr) : MonadCacheT ExprStructEq Expr (StateT (Array Name) CoreM) Expr := do | ||
checkCache { val := e : ExprStructEq } fun _ => do | ||
if e.isAppOfArity `binderNameHint 6 then | ||
let v := e.appFn!.appFn!.appArg! | ||
let b := e.appFn!.appArg! | ||
let e := e.appArg! | ||
let e' ← go e | ||
match v, b.headBeta with | ||
| .bvar bidx, .lam n _ _ _ | ||
| .bvar bidx, .forallE n _ _ _ => | ||
modify (rememberName bidx n) | ||
| .bvar bidx, _ => | ||
-- If we do not have a binder to use, ensure that name has macro scope. | ||
-- This is used by the auto-attach lemmas so that the new binder `fun h =>` | ||
-- has a macro-scope, and is inaccessible in the termination proof. | ||
-- (Using `fun _ =>` causes `property†` to appear, which is bad UX) | ||
let xs ← get | ||
assert! xs.size > bidx | ||
let n := xs[xs.size - bidx - 1]! | ||
let n' ← mkFreshUserName n | ||
modify (rememberName bidx n') | ||
| _, _ => | ||
pure () | ||
pure e' | ||
else | ||
match e with | ||
| .forallE n d b bi => | ||
let d' ← go d | ||
modify (enterScope n) | ||
let b' ← go b | ||
let n' ← modifyGet exitScope | ||
return .forallE n' d' b' bi | ||
| .lam n d b bi => | ||
let d' ← go d | ||
modify (enterScope n) | ||
let b' ← go b | ||
let n' ← modifyGet exitScope | ||
return .lam n' d' b' bi | ||
| .letE n t v b nd => | ||
let t' ← go t | ||
let v' ← go v | ||
modify (enterScope n) | ||
let b' ← go b | ||
let n' ← modifyGet exitScope | ||
return .letE n' t' v' b' nd | ||
| .app f a => return e.updateApp! (← go f) (← go a) | ||
| .mdata _ b => return e.updateMData! (← go b) | ||
| .proj _ _ b => return e.updateProj! (← go b) | ||
| _ => return e | ||
|
||
end Lean |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
theorem all_eq_not_any_not (l : List α) (p : α → Bool) : | ||
l.all p = !l.any fun x => binderNameHint x p (!p x) | ||
:= List.all_eq_not_any_not l p | ||
|
||
/-- | ||
error: tactic 'fail' failed | ||
names : List String | ||
⊢ (!names.any fun name => !"Waldo".isPrefixOf name) = true | ||
-/ | ||
#guard_msgs in | ||
example (names : List String) : names.all (fun name => "Waldo".isPrefixOf name) = true := by | ||
rw [all_eq_not_any_not] | ||
fail | ||
|
||
|
||
/-- | ||
error: tactic 'fail' failed | ||
names : List String | ||
⊢ (names.any fun name => !"Waldo".isPrefixOf name) = false | ||
-/ | ||
#guard_msgs in | ||
example (names : List String) : names.all (fun name => "Waldo".isPrefixOf name) = true := by | ||
simp [all_eq_not_any_not, -List.any_eq_false] | ||
fail | ||
|
||
|
||
def List.myAll (p : α → Bool) (xs : List α) : Bool := !(xs.any fun x => !p x) | ||
|
||
theorem myAll_eq_not_any_not (l : List α) (p : α → Bool) : | ||
l.myAll p = !l.any fun x => binderNameHint x p (!p x) | ||
:= rfl | ||
|
||
/-- | ||
error: tactic 'fail' failed | ||
names : List String | ||
⊢ (!names.any fun name => !"Waldo".isPrefixOf name) = true | ||
-/ | ||
#guard_msgs in | ||
example (names : List String) : names.myAll (fun name => "Waldo".isPrefixOf name) = true := by | ||
dsimp [myAll_eq_not_any_not] | ||
fail | ||
|
||
-- Check if we can pick the second binder by providing arguments to `f` in `binderNameHint` | ||
-- (It will beta-reduce it) | ||
|
||
|
||
-- Why this not in standard lib (maybe with the more complex form of recognizing lambdas that | ||
-- ignore arguments?) | ||
@[simp] | ||
theorem List.mapIdx_eq_map (l : List α) (f : α → β) : (l.mapIdx fun _ x => f x) = l.map f := by | ||
induction l <;> simp_all | ||
|
||
set_option linter.unusedVariables false in | ||
theorem zipWith_eq_map_idx_zipWith (l1 : List α) (l2 : List β) (f : α → β → γ) : | ||
List.zipWith f l1 l2 = (List.zip l1 l2).mapIdx | ||
(fun i ⟨a, b⟩ => binderNameHint a f <| binderNameHint b (f a) <| f a b) | ||
:= by simp [List.zip_eq_zipWith, List.map_zipWith] | ||
|
||
/-- | ||
error: tactic 'fail' failed | ||
l1 l2 : List Nat | ||
⊢ (List.mapIdx | ||
(fun i x => | ||
match x with | ||
| (x, y) => x + y) | ||
(l1.zip l2)).isEmpty = | ||
true | ||
-/ | ||
#guard_msgs in | ||
example (l1 l2 : List Nat) : | ||
(List.zipWith (fun x y => x + y) l1 l2).isEmpty := by | ||
rw [zipWith_eq_map_idx_zipWith] | ||
fail | ||
|
||
-- For now, binder name hints do not work in other tactics, like `apply` | ||
-- (but at least `simp` or `dsimp` removes them) | ||
|
||
theorem myAll_eq_not_any_not_iff {l : List α} {p : α → Bool} : | ||
l.myAll p ↔ !l.any fun x => binderNameHint x p (!p x) | ||
:= by simp [myAll_eq_not_any_not] | ||
|
||
/-- | ||
error: unsolved goals | ||
names : List String | ||
⊢ (!names.any fun x => !"Waldo".isPrefixOf x) = true | ||
--- | ||
info: names : List String | ||
⊢ (!names.any fun x => binderNameHint x (fun name => "Waldo".isPrefixOf name) !"Waldo".isPrefixOf x) = true | ||
-/ | ||
#guard_msgs in | ||
example (names : List String) : names.myAll (fun name => "Waldo".isPrefixOf name) = true := by | ||
apply myAll_eq_not_any_not_iff.mpr | ||
trace_state | ||
dsimp | ||
done |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import Lean | ||
|
||
-- This checks that binderNameHint freshens up a variable.: | ||
|
||
axiom allPairs (l : List α) (p : α → α → Bool) : Bool | ||
|
||
#guard_msgs (drop warning) in | ||
theorem all_eq_allPairs (l : List α) (p : α → Bool) : | ||
l.all p ↔ ∀ x y z, binderNameHint x p <| binderNameHint y () <| p x && p y && p z:= sorry | ||
|
||
-- below we should get `a` from the user (no dagger), `y` with dagger (binderNameHint added a macro | ||
-- scope) and `z` without dagger (because intro1P is not hygienic) | ||
|
||
/-- | ||
error: tactic 'fail' failed | ||
f : String → Bool | ||
names : List String | ||
a y✝ z : String | ||
⊢ (f a && f y✝ && f z) = true | ||
-/ | ||
#guard_msgs in | ||
open Lean Elab Tactic in | ||
example {f : String → Bool} (names : List String) : names.all (fun a => f a) = true := by | ||
rw [all_eq_allPairs] | ||
run_tac liftMetaTactic1 ((some ·.2) <$> ·.intro1P) | ||
run_tac liftMetaTactic1 ((some ·.2) <$> ·.intro1P) | ||
run_tac liftMetaTactic1 ((some ·.2) <$> ·.intro1P) | ||
fail |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/-! | ||
Checks that `simp` removes the `binderNameHint` in the pre-phase, and does not spend time looking | ||
at its arguments. | ||
|
||
The following traces should show no rewriting of `x` or `y`, only `z`. | ||
-/ | ||
|
||
def x : Nat := 0 | ||
def y : Nat := 0 | ||
def z : Nat := 0 | ||
|
||
set_option trace.Meta.Tactic.simp.rewrite true | ||
|
||
/-- | ||
info: [Meta.Tactic.simp.rewrite] ↓ binderNameHint.eq_1:1000: | ||
binderNameHint x y z | ||
==> | ||
z | ||
[Meta.Tactic.simp.rewrite] unfold z, z ==> 0 | ||
[Meta.Tactic.simp.rewrite] eq_self:1000: 0 = 0 ==> True | ||
-/ | ||
#guard_msgs in | ||
example : binderNameHint x y z = 0 := by | ||
simp [x, y, z] | ||
|
||
/-- | ||
info: [Meta.Tactic.simp.rewrite] ↓ binderNameHint.eq_1:1000: | ||
binderNameHint x y z | ||
==> | ||
z | ||
[Meta.Tactic.simp.rewrite] unfold z, z ==> 0 | ||
-/ | ||
#guard_msgs in | ||
example : binderNameHint x y z = 0 := by | ||
dsimp [x, y, z] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this ought to be reducible, so that defeq matching on the types of theorems (for instance, perhaps loogle and exact?) does not get tripped up by it?
More extremely, perhaps it should be a
macro
that produces anExpr.mdata
, so that unaware tactics can easily strip it?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, making this reducible is worth a try.
I don’t think a
mdata
can store other expressions, so not sure if this would work better.