Zulip Chat Archive
Stream: lean4
Topic: Implementation of exact
Filippo A. E. Nuccio (Sep 19 2024 at 19:52):
Until stumbling upon this unexpected behaviour
example (h : 0 < -1) : False := by
exact (Int.negSucc_not_nonneg (0 + 0).succ).mp h -- fails
exact (Int.negSucc_not_nonneg (0 + 0).succ).mp (by exact h) -- is OK
I was convinced that exact
was really not doing much. Then I tried to look inside its definition in docs#Lean.Elab.Tactic.evalExact and I cannot understand what, in that definition, is responsible for the behaviour in the example above.
Damiano Testa (Sep 19 2024 at 19:57):
Could it be the EnsuringType
kicking in?
Filippo A. E. Nuccio (Sep 19 2024 at 19:58):
On line 76, you mean?
Damiano Testa (Sep 19 2024 at 19:59):
Indeed (I'm on mobile, sorry for being cryptic).
Damiano Testa (Sep 19 2024 at 19:59):
I think that there you pass extra information to Lean.
Mario Carneiro (Sep 19 2024 at 19:59):
also by
delays elaboration
Filippo A. E. Nuccio (Sep 19 2024 at 20:00):
(deleted)
Filippo A. E. Nuccio (Sep 19 2024 at 20:00):
Damiano Testa said:
Indeed (I'm on mobile, sorry for being cryptic).
But then why does it succeed in the second call? After
apply (Int.negSucc_not_nonneg (0 + 0).succ).mp
is expects a term of the same type.
Filippo A. E. Nuccio (Sep 19 2024 at 20:00):
Mario Carneiro said:
also
by
delays elaboration
Can you expand a bit on this?
Damiano Testa (Sep 19 2024 at 20:30):
I think that there are a few things at play that end up having similar effects. I hope that if I say something wrong, someone will correct me!
When you write a proof (especially in tactic mode), you often leave work to lean to do to figure out what lots of implicit terms are. The two mechanism for addressing incomplete information that are at play here are
- using information from the goal to squeeze out some more information,
- wait out until some other missing terms have been figured out and use that information to fill in more blanks.
The former is what exact
does (possibly with the EnsuringType
).
The latter is probably what Mario is referring to: when Lean sees by
it decides to try harder to figure out the rest, before coming back to this specific hole.
apply
creates a new metavariable. In this context it probably means that it decides to delay figuring out what the hole is until you give it more information after apply
is done.
Often, information is redundant enough that these three methods work interchangeably. Sometimes, the information is involved enough that some of these methods work, while others do not.
Damiano Testa (Sep 19 2024 at 20:31):
I honestly do not know the details of what is going on here, but I'm guessing that some combination of the above plays a role! :smile:
Filippo A. E. Nuccio (Sep 19 2024 at 20:33):
Thanks! What you say agrees (about exact
and apply
, I had no idea that by
really do have some effect) with my rough picture. But if someone is willing to fill in more details I'd be happy to read them! :nerd:
Mario Carneiro (Sep 19 2024 at 20:43):
I believe the issue here is the expression (0 + 0).succ
. Lean doesn't know what function .succ
is referring to, because it doesn't know the type of 0 + 0
, because it's waiting for the type of 0
to be resolved. It will be default-resolved to Nat
, but this is supposed to happen "last", and in particular it tries to resolve the application involving h
before type defaulting. But because the whole expression (0+0).succ
is stuck, all it knows is that the type of (Int.negSucc_not_nonneg ?).mp
is 0 ≤ Int.negSucc ? → False
so applying that to h
means solving 0 < -1 =?= 0 ≤ Int.negSucc ?
, which is wrong on a few levels.
Mario Carneiro (Sep 19 2024 at 20:44):
note that even the version that works is pretty sus, you have h : 0 < -1
and the function is expecting a proof of 0 <= -2
Filippo A. E. Nuccio (Sep 19 2024 at 20:47):
Mario Carneiro said:
I believe the issue here is the expression
(0 + 0).succ
. Lean doesn't know what function.succ
is referring to, because it doesn't know the type of0 + 0
, because it's waiting for the type of0
to be resolved. It will be default-resolved toNat
, but this is supposed to happen "last", and in particular it tries to resolve the application involvingh
before type defaulting. But because the whole expression(0+0).succ
is stuck, all it knows is that the type of(Int.negSucc_not_nonneg ?).mp
is0 ≤ Int.negSucc ? → False
so applying that toh
means solving0 < -1 =?= 0 ≤ Int.negSucc ?
, which is wrong on a few levels.
Oh, I see. You mean that at the point Int.negSucc_not_nonneg
it is just waiting for a Nat, and there might be a function .succ
from some type T
to Nat
that maps ((0 : T) + (0 : T)).succ
to some Nat
, and Lean is waiting to undestand what T
is?
Mario Carneiro (Sep 19 2024 at 20:47):
exactly
Mario Carneiro (Sep 19 2024 at 20:48):
I think if you did (.succ (0 + 0))
instead it would work since the type deduction goes the other way
Filippo A. E. Nuccio (Sep 19 2024 at 20:49):
Oh, nice. But then why all this disappears in the second call? When you say that by
delays elaboration, do you mean that after the by
Lean creates a metavariable precisely there T
should land?
Mario Carneiro (Sep 19 2024 at 20:50):
because by
comes after the default types are resolved
Mario Carneiro (Sep 19 2024 at 20:50):
by
has even lower priority than default types
Mario Carneiro (Sep 19 2024 at 20:50):
(everyone wants to come last)
Filippo A. E. Nuccio (Sep 19 2024 at 20:50):
Mario Carneiro said:
I think if you did
(.succ (0 + 0))
instead it would work since the type deduction goes the other way
Indeed, it works as (you) expected!
Matthew Ballard (Sep 19 2024 at 20:51):
Related https://leanprover.zulipchat.com/#narrow/stream/116395-maths/topic/Heavy.20rfl/near/469781815
Filippo A. E. Nuccio (Sep 19 2024 at 20:51):
Mario Carneiro said:
because
by
comes after the default types are resolved
But is it " coming" somewhere? Or do you mean that every term constructed with a by
comes last?
Mario Carneiro (Sep 19 2024 at 20:52):
every by
block is delayed until every other elaboration problem and available trick is used
Mario Carneiro (Sep 19 2024 at 20:52):
that way the goal state doesn't have confusing metavariables in it which are already resolved later in the term
Filippo A. E. Nuccio (Sep 19 2024 at 20:53):
Oh, so this really have little to do with exact
in the end, it is really the priority ofby
.
Filippo A. E. Nuccio (Sep 19 2024 at 20:53):
Wow.
Mario Carneiro (Sep 19 2024 at 20:54):
one of the design criteria of tactic mode is that elaboration should be predictable and in-order top down
Filippo A. E. Nuccio (Sep 19 2024 at 20:54):
And just to be sure: the (.succ (0+0))
trick works because Lean parses right to left and first sees the 0
, it defaults it to 0 : Nat
, so .succ
must be Nat.succ
and everything goes smoothly?
Mario Carneiro (Sep 19 2024 at 20:54):
but term mode is a crazy dance of priorities and out of order execution
Mario Carneiro (Sep 19 2024 at 20:55):
It's not because of parsing order, it's because (.succ ...)
elaborates to T.succ
where T
is the expected type, while (...).succ
elaborates to T.succ
where T
is the type of (...)
Mario Carneiro (Sep 19 2024 at 20:56):
we know the expected type is Nat
, we don't know the type of 0 + 0
Kyle Miller (Sep 19 2024 at 20:56):
This works too:
example (h : 0 < -1) : False :=
(Int.negSucc_not_nonneg (0 + 0 : Nat).succ).mp h
Filippo A. E. Nuccio (Sep 19 2024 at 20:56):
Mario Carneiro said:
It's not because of parsing order, it's because
(.succ ...)
elaborates toT.succ
whereT
is the expected type, while(...).succ
elaborates toT.succ
whereT
is the type of(...)
Crystal clear, thanks! :crystal: (although dotted identifier notation is still somewhat arcane to me in its implementation details).
Mario Carneiro (Sep 19 2024 at 20:58):
don't worry, it's arcane even to the people that wrote it
Last updated: May 02 2025 at 03:31 UTC