-
Notifications
You must be signed in to change notification settings - Fork 446
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: let dot notation see through
CoeFun
instances (#5692)
Projects like mathlib like to define projection functions with extra structure, for example one could imagine defining `Multiset.card : Multiset α →+ Nat`, which bundles the fact that `Multiset.card (m1 + m2) = Multiset.card m1 + Multiset.card m2` for all `m1 m2 : Multiset α`. A problem though is that so far this has prevented dot notation from working: you can't write `(m1 + m2).card = m1.card + m2.card`. With this PR, now you can. The way it works is that "LValue resolution" will apply CoeFun instances when trying to resolve which argument should receive the object of dot notation. A contrived-yet-representative example: ```lean structure Equiv (α β : Sort _) where toFun : α → β invFun : β → α infixl:25 " ≃ " => Equiv instance: CoeFun (α ≃ β) fun _ => α → β where coe := Equiv.toFun structure Foo where n : Nat def Foo.n' : Foo ≃ Nat := ⟨Foo.n, Foo.mk⟩ variable (f : Foo) #check f.n' -- Foo.n'.toFun f : Nat ``` Design note 1: While LValue resolution attempts to make use of named arguments when positional arguments cannot be used, when we apply CoeFun instances we disallow making use of named arguments. The rationale is that argument names for CoeFun instances tend to be random, which could lead dot notation randomly succeeding or failing. It is better to be uniform, and so it uniformly fails in this case. Design note 2: There is a limitation in that this will *not* make use of the values of any of the provided arguments when synthesizing the CoeFun instances (see the tests for an example), since argument elaboration takes place after LValue resolution. However, we make sure that synthesis will fail rather than choose the wrong CoeFun instance. Performance note: Such instances will be synthesized twice, once during LValue resolution, and again when applying arguments. This also adds in a small optimization to the parameter list computation in LValue resolution so that it lazily reduces when a relevant parameter hasn't been found yet, rather than using `forallTelescopeReducing`. It also switches to using `forallMetaTelescope` to make sure the CoeFun synthesis will fail if multiple instances could apply. Getting this to pretty print will be deferred to future work. Closes #1910
- Loading branch information
Showing
2 changed files
with
164 additions
and
38 deletions.
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
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 @@ | ||
/-! | ||
# Dot notation and CoeFun | ||
https://github.com/leanprover/lean4/issues/1910 | ||
-/ | ||
|
||
set_option pp.mvars false | ||
|
||
/-! | ||
Test that dot notation resolution can see through CoeFun instances. | ||
-/ | ||
|
||
structure Equiv (α β : Sort _) where | ||
toFun : α → β | ||
invFun : β → α | ||
|
||
infixl:25 " ≃ " => Equiv | ||
|
||
instance : CoeFun (α ≃ β) fun _ => α → β where | ||
coe := Equiv.toFun | ||
|
||
structure Foo where | ||
n : Nat | ||
|
||
def Foo.n' : Foo ≃ Nat := ⟨Foo.n, Foo.mk⟩ | ||
|
||
variable (f : Foo) | ||
/-- info: Foo.n'.toFun f : Nat -/ | ||
#guard_msgs in #check f.n' | ||
|
||
example (f : Foo) : f.n' = f.n := rfl | ||
|
||
/-! | ||
Fail dot notation if it requires using a named argument from the CoeFun instance. | ||
-/ | ||
structure F where | ||
f : Bool → Nat → Nat | ||
|
||
instance : CoeFun F (fun _ => (x : Bool) → (y : Nat) → Nat) where | ||
coe x := fun (a : Bool) (b : Nat) => x.f a b | ||
|
||
-- Recall CoeFun oddity: it uses the unfolded *value* to figure out parameter names. | ||
-- That's why this is `a` and `b` rather than `x` and `y`. | ||
/-- info: fun x => (fun a b => x.f a b) true 2 : F → Nat -/ | ||
#guard_msgs in #check fun (x : F) => x (a := true) (b := 2) | ||
|
||
def Nat.foo : F := { f := fun _ b => b } | ||
|
||
-- Ok: | ||
/-- info: fun n x => (fun a b => Nat.foo.f a b) x n : Nat → Bool → Nat -/ | ||
#guard_msgs in #check fun (n : Nat) => (Nat.foo · n) | ||
|
||
-- Intentionally fails: | ||
/-- | ||
error: invalid field notation, function 'Nat.foo' has argument with the expected type | ||
Nat | ||
but it cannot be used | ||
--- | ||
info: fun n => sorryAx (?_ n) true : (n : Nat) → ?_ n | ||
-/ | ||
#guard_msgs in #check fun (n : Nat) => n.foo | ||
|
||
/-! | ||
Make sure that dot notation does not use the wrong CoeFun instance. | ||
The following instances rely on the second one having higher priority, | ||
so we need to fail completely when the instances would depend on argument values. | ||
-/ | ||
|
||
structure Bar (b : Bool) where | ||
|
||
instance : CoeFun (Bar b) (fun _ => Bar b → Bool) where | ||
coe := fun _ _ => b | ||
|
||
instance : CoeFun (Bar true) (fun _ => (b : Bool) → Bar b) where | ||
coe := fun _ _ => {} | ||
|
||
def Bar.bar : Bar true := {} | ||
|
||
/-- info: fun f => (fun x => false) f : Bar false → Bool -/ | ||
#guard_msgs in #check fun (f : Bar false) => Bar.bar false f | ||
/-- | ||
error: invalid field notation, function 'Bar.bar' does not have argument with type (Bar ...) that can be used, it must be explicit or implicit with a unique name | ||
--- | ||
info: fun f => sorryAx (?_ f) true : (f : Bar false) → ?_ f | ||
-/ | ||
#guard_msgs in #check fun (f : Bar false) => f.bar false | ||
|
||
/-- info: fun f => (fun x => false) f : Bar false → Bool -/ | ||
#guard_msgs in #check fun (f : Bar false) => Bar.bar true false f | ||
/-- | ||
error: invalid field notation, function 'Bar.bar' does not have argument with type (Bar ...) that can be used, it must be explicit or implicit with a unique name | ||
--- | ||
info: fun f => sorryAx (?_ f) true : (f : Bar false) → ?_ f | ||
-/ | ||
#guard_msgs in #check fun (f : Bar false) => f.bar true false |