Zulip Chat Archive
Stream: lean4
Topic: lean4#6544 : induction does not generalize instance argument
Edward van de Meent (Apr 15 2025 at 10:39):
in trying to write tests for my fix for lean4#4246, i've been repeatedly running into lean4#6544, so i decided to investigate, and it turns out that this is caused by the fact that docs#Lean.Meta.getFVarSetToGeneralize purposefully does not generalize instimplicit arguments. Can someone explain why this is?
Edward van de Meent (Apr 15 2025 at 10:41):
CC: @Leonardo de Moura who wrote this (4 years ago)?
Edward van de Meent (Apr 15 2025 at 10:51):
i imagine a fix might look like adding a second option argument for getFVarSetToGeneralize
saying wether to ignore instance-implicit fvars, which is set to false in Lean.Elab.Tactic.generalizeVars
(it's a private def) but true in docs#Lean.Meta.getFVarsToGeneralize
Edward van de Meent (Apr 15 2025 at 14:08):
update: this does run into issues...
Edward van de Meent (Apr 15 2025 at 14:10):
in particular, for some reason this causes the following to generate a goal Decidable P
:
def casesTFOn {motive : Prop → Sort _} (P : Prop) [inst : Decidable P] : (T : motive True) → (F : motive False) → motive P :=
λ ht hf => match inst with
| isTrue H => eq_true H ▸ ht
| isFalse H => eq_false H ▸ hf
example (P : Prop) [Decidable P] : ¬¬P → P := by
induction P using casesTFOn
admit
admit
Eric Wieser (Apr 15 2025 at 14:11):
That's a bad induction principle
Edward van de Meent (Apr 15 2025 at 14:11):
(it's in the lean test suite)
Eric Wieser (Apr 15 2025 at 14:11):
I think it should be changed to
def casesTFOn {motive : (P : Prop) → [Decidable P] → Sort _} (P : Prop) [Decidable P] : (T : motive True) → (F : motive False) → motive P :=
λ ht hf => match inst with
| isTrue H => eq_true H ▸ ht
| isFalse H => eq_false H ▸ hf
Eric Wieser (Apr 15 2025 at 14:12):
The test couldn't have been written that way before because it wasn't supported
Edward van de Meent (Apr 15 2025 at 14:14):
Eric Wieser (Apr 15 2025 at 14:14):
Yes, I'm saying that what you're proposing is a behavior change, and when you make such changes sometimes tests have to change
Edward van de Meent (Apr 15 2025 at 14:15):
the behaviour change i'm proposing shouldn't change this behaviour though? i think?
Eric Wieser (Apr 15 2025 at 14:16):
I would recommend you change the test as above and see if anything else breaks
Eric Wieser (Apr 15 2025 at 14:16):
Whether changing the test is ok can wait till review
Edward van de Meent (Apr 15 2025 at 14:19):
btw, i think the principle you're proposing here won't work, since the fix for lean4#6544 is separate from the fix for lean4#4246 , so lean will complain it can't figure out the implicit target
Eric Wieser (Apr 15 2025 at 14:21):
I think probably I'm claiming that the two things are morally the same bug
Edward van de Meent (Apr 15 2025 at 14:21):
(they're not)
Eric Wieser (Apr 15 2025 at 14:22):
My claim is that instance binders are not currently generalized because if they were, induction
would not know how to populate them
Edward van de Meent (Apr 15 2025 at 14:24):
i think one of us is missing a key part here, in that as far as i can tell, lean4#4246 is about instance targets, not all instance arguments to induction
Edward van de Meent (Apr 15 2025 at 14:25):
also, the goal Decidable P
strangely does not contain the instance that existed before
Edward van de Meent (Apr 15 2025 at 14:25):
so there is something really strange going on there
Eric Wieser (Apr 15 2025 at 14:28):
Edward van de Meent said:
also, the goal
Decidable P
strangely does not contain the instance that existed before
What's the full goal state after the induction
line?
Edward van de Meent (Apr 15 2025 at 14:29):
with the following snippet:
def casesTFOn {motive : Prop → Sort _} (P : Prop) [inst : Decidable P] : (T : motive True) → (F : motive False) → motive P :=
λ ht hf => match inst with
| isTrue H => eq_true H ▸ ht
| isFalse H => eq_false H ▸ hf
example (P : Prop) [Decidable P] : ¬¬P → P := by
induction P using casesTFOn with
| T => admit
| F => admit
admit
at the third admit
:
case inst
P : Prop
⊢ Decidable P
Edward van de Meent (Apr 15 2025 at 14:30):
which is really weird to me
Edward van de Meent (Apr 15 2025 at 14:30):
time to dive into the induction implementation again, i guess
Eric Wieser (Apr 15 2025 at 14:39):
Just to check, does the alternative test I suggested pass?
Edward van de Meent (Apr 15 2025 at 14:39):
or actually, it makes sense? since the goal gets changed to [Decidable P] -> ¬¬P → P
(because we're generalizing the instance), if you then try to use induction P using casesTFOn
, you're indeed missing the needed instance
Aaron Liu (Apr 15 2025 at 14:41):
Edward van de Meent said:
with the following snippet:
def casesTFOn {motive : Prop → Sort _} (P : Prop) [inst : Decidable P] : (T : motive True) → (F : motive False) → motive P := λ ht hf => match inst with | isTrue H => eq_true H ▸ ht | isFalse H => eq_false H ▸ hf example (P : Prop) [Decidable P] : ¬¬P → P := by induction P using casesTFOn with | T => admit | F => admit admit
at the third
admit
:case inst P : Prop ⊢ Decidable P
On the web editor this gives me "no goals"
Edward van de Meent (Apr 15 2025 at 14:41):
yea, like i said, this is an issue on my branch where i try to fix lean4#6544
Aaron Liu (Apr 15 2025 at 14:41):
oh sorry didn't see
Edward van de Meent (Apr 15 2025 at 14:43):
Eric Wieser said:
Just to check, does the alternative test I suggested pass?
the alternative test you wrote doesn't compile the induction principle even on current lean
Eric Wieser (Apr 15 2025 at 14:47):
Oh, you should have told me sooner!
Eric Wieser (Apr 15 2025 at 14:47):
-- TODO: replace `(_ : Decidable P)` with `[Decidable P]`
def casesTFOn {motive : (P : Prop) → [Decidable P] → Sort _} (P : Prop) (_ : Decidable P) :
(T : motive True) → (F : motive False) → motive P :=
λ ht hf => match ‹Decidable P› with
| isTrue H => cast (by simp [H]) ht
| isFalse H => cast (by simp [H]) hf
example (P : Prop) [Decidable P] : ¬¬P → P := by
induction P, ‹_› using casesTFOn
admit
admit
Edward van de Meent (Apr 15 2025 at 14:48):
yea, that works
Eric Wieser (Apr 15 2025 at 14:48):
Does it work on your branch with []
instead of (_ :)
?
Edward van de Meent (Apr 15 2025 at 14:48):
no, failed to infer implicit target
Eric Wieser (Apr 15 2025 at 14:49):
Ok, same failure as head then
Edward van de Meent (Apr 15 2025 at 14:50):
anywho, i think that actually lean ideally should decide to not revert Decidable P
here...
Edward van de Meent (Apr 15 2025 at 14:51):
the question is, what is a good decision procedure to decide if one should or shouldnt revert this? (btw, i'm guessing lean would have a similar issue with normal implicit arguments on current lean)
Edward van de Meent (Apr 15 2025 at 14:52):
let me see if i can throw together an example
Edward van de Meent (Apr 15 2025 at 14:54):
structure Foo ( P : Prop) [Decidable P] where
def casesTFOn {motive : (P : Prop) → Sort _} (P : Prop) {inst:Decidable P} (x: Foo P): (T : motive True) → (F : motive False) → motive P :=
λ ht hf => match inst with
| isTrue H => eq_true H ▸ ht
| isFalse H => eq_false H ▸ hf
example (P) [Decidable P] : ¬¬P → P := by
induction P using casesTFOn
admit
admit
sorry
Edward van de Meent (Apr 15 2025 at 14:56):
i guess this is a result of the fact that these kinds of arguments are "extra": they're not major premises/targets, they're not minor premises, and they're not the motive...
Jz Pan (Apr 15 2025 at 15:20):
Edward van de Meent said:
in particular, for some reason this causes the following to generate a goal
Decidable P
:
I think this is not the intended behavior because the motive
does not mention Decidable P
...
Aaron Liu (Apr 15 2025 at 15:29):
I would say this is induction on Decidable P
Aaron Liu (Apr 15 2025 at 15:29):
But it's not mentioned in the motive
Aaron Liu (Apr 15 2025 at 15:29):
What's the criteria for determining the premises/targets?
Edward van de Meent (Apr 15 2025 at 15:57):
if you have an induction principle, match on the type it eliminates into; the head symbol is the motive; all other arguments to the motive in this type are the targets
Edward van de Meent (Apr 15 2025 at 15:58):
the minor premises are the arguments to the induction principle which eliminate into motive
Edward van de Meent (Apr 15 2025 at 15:58):
the major premises are the targets
Aaron Liu (Apr 15 2025 at 16:00):
Edward van de Meent said:
the minor premises are the arguments to the induction principle which eliminate into
motive
Explain?
Aaron Liu (Apr 15 2025 at 16:01):
I've decided to read the code, so I can figure out this confusing behavior
Edward van de Meent (Apr 15 2025 at 16:01):
take the induction principle for Nat
. it looks like natinduction : (motive : Nat -> Sort u) -> (zero : motive 0) -> (succ : ((n' : Nat) -> motive n' -> motive (n' + 1)) -> (n : Nat) -> motive n
Aaron Liu (Apr 15 2025 at 16:02):
So the motive is motive
, and the targets are [n]
Aaron Liu (Apr 15 2025 at 16:03):
What are the major and minor premises?
Edward van de Meent (Apr 15 2025 at 16:03):
the principle eliminates into motive n
. the head symbol is called the motive, motive
. its arguments (just n
in this case) are the major premises, which is another word for targets. the arguments zero
and succ
are the minor premises, since they eliminate into motive 0
and motive (n' + 1)
respectively.
Aaron Liu (Apr 15 2025 at 16:03):
oh i get it now
Aaron Liu (Apr 15 2025 at 16:04):
The major premise is what you're inducting on and the minor premise is the induction cases
Edward van de Meent (Apr 15 2025 at 16:08):
so the question is: given some context + some goal, how do you determine what variables should become part of the motive (i.e. be revert
ed or generalize
d), and which variables shouldn't?
Aaron Liu (Apr 15 2025 at 16:11):
What's wrong with the way it's being done right now?
Aaron Liu (Apr 15 2025 at 16:13):
Edward van de Meent said:
anywho, i think that actually lean ideally should decide to not revert
Decidable P
here...
Can you give a before and after of what you want induction
to do?
Edward van de Meent (Apr 15 2025 at 16:13):
the same as what it does on current lean, in this case
Edward van de Meent (Apr 15 2025 at 16:16):
but there are other cases where i do want lean to generalize a free variable, and i'm not sure yet what criterion to use to decide between these
Aaron Liu (Apr 15 2025 at 16:24):
Here's what I got on the current Lean.
Before:
case goal
P : Prop
inst✝ : Decidable P
⊢ ¬¬P → P
After:
case goal.T
P : Prop
inst✝ : Decidable P
⊢ ¬¬True → True
case goal.F
P : Prop
inst✝ : Decidable P
⊢ ¬¬False → False
case goal.inst
P : Prop
⊢ Decidable P
assignment
?goal :=
@casesTFOn (fun P => ∀ {inst : Decidable P}, ¬¬P → P) P ?goal.inst
(fun {inst} => ?goal.T) (fun {inst} => ?goal.F) inst✝
Edward van de Meent (Apr 15 2025 at 16:24):
ah wait, that's not right
Edward van de Meent (Apr 15 2025 at 16:25):
Edward van de Meent said:
in particular, for some reason this causes the following to generate a goal
Decidable P
:def casesTFOn {motive : Prop → Sort _} (P : Prop) [inst : Decidable P] : (T : motive True) → (F : motive False) → motive P := λ ht hf => match inst with | isTrue H => eq_true H ▸ ht | isFalse H => eq_false H ▸ hf example (P : Prop) [Decidable P] : ¬¬P → P := by induction P using casesTFOn admit admit
for this, it should behave the same as on current lean
Eric Wieser (Apr 15 2025 at 16:35):
Maybe it helps to note that this fails on current lean:
def casesTFOn {motive : Prop → Sort _} (P : Prop) {inst : Decidable P} : (T : motive True) → (F : motive False) → motive P :=
λ ht hf => match inst with
| isTrue H => eq_true H ▸ ht
| isFalse H => eq_false H ▸ hf
example (P : Prop) [Decidable P] : ¬¬P → P := by
induction P using casesTFOn
admit
admit
Eric Wieser (Apr 15 2025 at 16:37):
So I really think casesTFOn
is garbage
Eric Wieser (Apr 15 2025 at 16:37):
This also fails:
def casesTFOn {motive : Prop → Sort _} (P : Prop) [inst : Decidable P] : (T : motive True) → (F : motive False) → motive P :=
λ ht hf => match inst with
| isTrue H => eq_true H ▸ ht
| isFalse H => eq_false H ▸ hf
example (P : Prop) [Decidable P] : (if P then 1 else 0) < 2 := by
induction P using casesTFOn
admit
admit
Aaron Liu (Apr 15 2025 at 16:52):
It works if you add the Decidable P
as a target
Aaron Liu (Apr 15 2025 at 16:53):
You might still get a failed to infer implicit target
though
Eric Wieser (Apr 15 2025 at 18:06):
Do you mean like this?
Edward van de Meent (Apr 15 2025 at 20:10):
I think I have a solution: call arguments to an induction principle semitargets if their type depends on a target. Then, have all explicit targets and semitargets passed to induction
. At the point where these would be revert
ed, instead do something which behaves like refine (?_ : semitargettype -> ?_) semitarget
(but in the language of metaprogramming) such that the semitargets stay available to the current scope, but they also become available in the appropriate form when proving the minor premises.
Edward van de Meent (Apr 15 2025 at 20:10):
I'll try implementing this tomorrow
Edward van de Meent (Apr 15 2025 at 20:11):
(then generalizing any non-(semi)target will be blocked)
Kyle Miller (Apr 15 2025 at 20:41):
Maybe it's worth mentioning how @[elab_as_elim]
works. It has similar considerations.
Here's how it handles things (see docs#Lean.Elab.Term.getElabElimExprInfo):
- The "motive" is the head variable of the return type.
- The "motive fvars" consist of all the variables that appear in the return type and (transitively) every variable that appears in the type of a motive fvar.
- A "major argument" is any variable that's not the motive and either (1) it's a motive fvar or (2) its type contains a motive fvar and every application in the type is a constant application (it's a "first-order argument")
- Then, every other argument is a "minor argument".
Then the elaborator takes the following steps:
- The major arguments are all elaborated.
- The motive is computed from the expected type.
- The minor arguments are elaborated.
The purpose of all this is to be able to elaborate enough in (1) to be able to get enough information to successfully compute the motive in (2), before elaborating all the other arguments, which likely need a motive to be successful in (3).
Possibly it would have worked to simply say that the minor arguments are those whose types contain the motive variable.
The terminology doesn't line up with induction
(for example every type parameter is a "major argument" according to @[elab_as_elim]
), but maybe there's something useful to think about here
It's not clear to me that getFVarSetToGeneralize
itself is the culprit (though, disclosure, I haven't investigated the issue). It might be that the way using
clauses are elaborated needs rethinking. The handling of eliminators has undergone evolution over a number of years.
Edward van de Meent (Apr 15 2025 at 20:44):
how can a motive fvar not occur as argument to the motive though??
Edward van de Meent (Apr 15 2025 at 20:45):
i don't see what kind of motive could have that...
Kyle Miller (Apr 15 2025 at 21:03):
When there's an argument that appears in the type of an argument that appears in the return type.
Edward van de Meent (Apr 15 2025 at 21:05):
so the principle can be something like (a:A) -> (motive : b a -> Sort) -> (x: b a) -> motive x
?
Edward van de Meent (Apr 15 2025 at 21:06):
i guess that makes sense
Kyle Miller (Apr 15 2025 at 21:07):
Sure, that would make a
be a "major argument" for @[elab_as_elim]
.
This is also possible: (a:A) -> (motive : (a' : A) -> b a' -> Sort) -> (x: b a) -> motive x
. It too will include a
as something that needs to be elaborated before computing the motive.
Kyle Miller (Apr 15 2025 at 21:08):
The motivation is that to be able to find all x
's in the expected type you should elaborate x
, but to elaborate x
, you should know its type, so you should elaborate a
.
Edward van de Meent (Apr 15 2025 at 21:10):
the a
from my example would be a "motive fvar" too, right?
Kyle Miller (Apr 15 2025 at 21:14):
Yes, both in your example and mine.
Edward van de Meent (Apr 15 2025 at 21:15):
the key difference is that in my example it is only because of the transitive considerations
Kyle Miller (Apr 15 2025 at 21:15):
In yours its for two reasons: it appears in the type of x
which appears in the return type, and because it appears in the motive
argument's type.
Kyle Miller (Apr 15 2025 at 21:15):
In mine it's only for the first reason. (Both reasons are due to transitivity.)
Edward van de Meent (Apr 15 2025 at 21:16):
your example seems to be missing an argument to the final motive
Kyle Miller (Apr 15 2025 at 21:19):
Oh yeah, you're right. That's a simple case then, no transitivity.
Jz Pan (Apr 16 2025 at 02:48):
Edward van de Meent said:
so the principle can be something like
(a:A) -> (motive : b a -> Sort) -> (x: b a) -> motive x
?
I think for this case you can only induction on x
and a
is always fixed. If user want to do induction on a
it should throws an error.
Aaron Liu (Apr 16 2025 at 11:04):
Jz Pan said:
Edward van de Meent said:
so the principle can be something like
(a:A) -> (motive : b a -> Sort) -> (x: b a) -> motive x
?I think for this case you can only induction on
x
anda
is always fixed. If user want to do induction ona
it should throws an error.
This is actually really common. Take for example docs#Finset.induction, that starts with
{α : Type*} {motive : Finset α → Prop}
which fits the pattern described above.
Edward van de Meent (Apr 16 2025 at 11:05):
yea, that's fair. i also realised the same for List.rec
and similar
Jz Pan (Apr 16 2025 at 13:38):
Aaron Liu said:
Jz Pan said:
Edward van de Meent said:
so the principle can be something like
(a:A) -> (motive : b a -> Sort) -> (x: b a) -> motive x
?I think for this case you can only induction on
x
anda
is always fixed. If user want to do induction ona
it should throws an error.This is actually really common. Take for example docs#Finset.induction, that starts with
{α : Type*} {motive : Finset α → Prop}
which fits the pattern described above.
In that case the α
is fixed and cannot be changed during the proof of Finset.induction
, since the α
is not appeared as an input of motive
. Like (a:A) -> (motive : b a -> Sort) -> (x: b a) -> motive x
you cannot vary a
during the proof, since a
is not appeared as an input of motive
. If you want to vary a
you must use (a:A) -> (motive : (a' : A) -> b a' -> Sort) -> (x: b a) -> motive x
. The compiler should not implicitly prepend an a' : A
to the motive
for the former case.
Edward van de Meent (Apr 16 2025 at 13:47):
you mean (a:A) -> (motive : (a' : A) -> b a' -> Sort) -> (x: b a) -> motive a x
in that second case, right?
Jz Pan (Apr 16 2025 at 14:11):
Edward van de Meent said:
you mean
(a:A) -> (motive : (a' : A) -> b a' -> Sort) -> (x: b a) -> motive a x
in that second case, right?
Yes, I mean only in this case you can vary a
in the motive. For (a:A) -> (motive : b a -> Sort) -> (x: b a) -> motive x
the induction
should always generate goals for the fixed a
.
Edward van de Meent (Apr 16 2025 at 14:12):
or, well, as much as the value of motive
depends on it
Jz Pan (Apr 16 2025 at 14:15):
Although the value b a
of motive
depends on a
, but a
does not appear as an input of motive
which means that the a
in motive
is fixed and can never be changed... Just like thinking motive
as a closure (a
is in the closure) function which has only one argument, instead of a function taking two arguments.
Jz Pan (Apr 16 2025 at 14:17):
As an example here M
depends on A
, but when I write motive
in that way I mean the A
is always fixed as the input A
in the induction principle theorem, and can never be changed; I don't want induction
principle generates goals with varying A
.
Last updated: May 02 2025 at 03:31 UTC