Zulip Chat Archive
Stream: new members
Topic: repeat
Bolton Bailey (Sep 06 2021 at 05:26):
The documentation for repeat says: "... the tactic is applied recursively to all the generated subgoals until it eventually fails. The recursion stops in a subgoal when the tactic has failed to make progress."
Which is it: Is it applied recursively until it fails, or until it fails to make progress?
Notification Bot (Sep 18 2021 at 21:26):
Bolton Bailey has marked this topic as unresolved.
Bolton Bailey (Sep 18 2021 at 21:39):
I'll reopen this thread and ask again because I just don't understand what I'm seeing.
The code I'm working on is a bit too complicated to post an mwe, so hopefully you can bear with me. In the code below, I get an output with two different goals. It reports the error error: solve1 tactic failed, focused goal has not been solved
on the final closing bracket. This suggests to me that at the very least , lines 4-6 below are not causing an error. But when I comment those lines out and rerun the program, I get a different output, which only has one goal, (but still says the same error that the goal is not solved by the final bracket).
This makes no sense to me, because, based om my reading of docs#repeat, if the middle three lines don't cause a failure, the output of t, repeat { t, },
should be the same as repeat { t, },
. What gives?
{
simp only [*] with integral_domain_simp at *,
try {simp only [] with integral_domain_simp at *},
try {clear found_zero}, -- clear "found_zero" if present
done <|> begin cases ‹_ ∨ _› with found_zero found_zero; rw found_zero at *, end,
repeat {
try {simp only [] with integral_domain_simp at *},
try {clear found_zero}, -- clear "found_zero" if present
done <|> begin cases ‹_ ∨ _› with found_zero found_zero; rw found_zero at *, end },
}
Mario Carneiro (Sep 18 2021 at 21:51):
I would suggest replacing repeat
with iterate n
for different values of n
and see for which values it succeeds
Mario Carneiro (Sep 18 2021 at 21:52):
It is worth noting that begin ... end
in tactic mode is the same as { ... }
. In particular, it will fail if you haven't closed the goal at the end of the block
Mario Carneiro (Sep 18 2021 at 21:55):
This looks like an error reporting bug:
theorem foo : true ∧ true :=
begin
done <|> {
refine ⟨trivial, _⟩,
}, -- the error is actually here
-- uncommenting this doesn't work
-- refine trivial,
end -- but it is reported here
Mario Carneiro (Sep 18 2021 at 21:56):
So what is really happening is that your first begin cases
line is failing and the repeat never runs
Bolton Bailey (Sep 18 2021 at 21:57):
Mario Carneiro said:
It is worth noting that
begin ... end
in tactic mode is the same as{ ... }
. In particular, it will fail if you haven't closed the goal at the end of the block
This is certainly good to know.
Mario Carneiro (Sep 18 2021 at 21:57):
You should use id { ... }
instead to group tactics without the implicit done
at the end
Bolton Bailey (Sep 18 2021 at 21:59):
Sorry, what does id
do?
Mario Carneiro (Sep 18 2021 at 22:00):
it's a tactic combinator like try
Mario Carneiro (Sep 18 2021 at 22:00):
it does nothing, it's the identity function
Mario Carneiro (Sep 18 2021 at 22:01):
but the { ... }
is part of the syntax, so it doesn't count as regular focusing brackets
Bolton Bailey (Sep 18 2021 at 22:01):
So you mean I should use
iterate 2 id {
try {simp only [] with integral_domain_simp at *},
try {clear found_zero}, -- clear "found_zero" if present
id {cases ‹_ ∨ _› with found_zero found_zero; rw found_zero at *}, },
And this will cause it to not fail if the goal is completed by the second line?
Mario Carneiro (Sep 18 2021 at 22:02):
The braces after iterate 2 { ... }
are also part of the syntax
Mario Carneiro (Sep 18 2021 at 22:02):
so that's not well formed
Mario Carneiro (Sep 18 2021 at 22:02):
Immediately after iterate 2
though, you already have a tactic block argument, so you don't need the id
Mario Carneiro (Sep 18 2021 at 22:03):
It's useful in things like done <|> id { ... }
Bolton Bailey (Sep 18 2021 at 22:03):
iterate 2 {
try {simp only [] with integral_domain_simp at *},
try {clear found_zero}, -- clear "found_zero" if present
done <|> id {cases ‹_ ∨ _› with found_zero found_zero; rw found_zero at *}, },
Mario Carneiro (Sep 18 2021 at 22:04):
you can probably replace done <|> id ...
with try ...
in this case
Bolton Bailey (Sep 18 2021 at 22:04):
Wouldn't that loop forever with repeat?
Mario Carneiro (Sep 18 2021 at 22:04):
yes it would
Mario Carneiro (Sep 18 2021 at 22:05):
I think done
would as well though
Mario Carneiro (Sep 18 2021 at 22:05):
if there are no goals then it will just spin with done
succeeding every time
Bolton Bailey (Sep 18 2021 at 22:06):
See the very first comment in this thread. The docs for repeat should probably be changed to not say "when the tactic has failed to make progress."
Mario Carneiro (Sep 18 2021 at 22:06):
"make progress" isn't something lean knows how to detect
Bolton Bailey (Sep 18 2021 at 22:06):
Exactly my point
Mario Carneiro (Sep 18 2021 at 22:06):
it assumes that tactics fail when they fail to make progress
Mario Carneiro (Sep 18 2021 at 22:06):
which is generally true of front end tactics
Bolton Bailey (Sep 18 2021 at 22:08):
But not of all tactics, as I realized to my confusion a few weeks ago.
Mario Carneiro (Sep 18 2021 at 22:09):
If you use the more explicit structural tactics like done
and try
that's not quite true anymore
Bolton Bailey (Sep 18 2021 at 23:30):
Ok, I am confused again. iterate { fail_if_success { done }, ... }
succeeds in producing 5 new simpler goals in 2 minutes or so. But repeat { fail_if_success { done }, ... }
and repeat { ... }
both seem to hang, taking at least 5 minutes. Why would these behave differently?
Notification Bot (Sep 18 2021 at 23:30):
Bolton Bailey has marked this topic as unresolved.
Mario Carneiro (Sep 18 2021 at 23:37):
It would really help if you made #mwe's for this stuff
Mario Carneiro (Sep 18 2021 at 23:38):
Why are these tactics taking minutes? You aren't supposed to exhaust the recursion limit
Bolton Bailey (Sep 18 2021 at 23:38):
Yes, sorry about that. Let me try to make a simpler mwe.
Bolton Bailey (Sep 18 2021 at 23:40):
Part of the problem is there is a lot of code earlier on in the file that takes a while to run which generates these equations, and it's hard to separate that time from the time taken by the tactics we are talking about.
Mario Carneiro (Sep 18 2021 at 23:41):
You seem to think that the actual content of the blocks doesn't matter, so make a MWE with a simpler block
Mario Carneiro (Sep 18 2021 at 23:42):
my examples use split
and trivial
which is usually good enough for interesting branching patterns
Mario Carneiro (Sep 18 2021 at 23:43):
If you need an expensive tactic, use sleep
Mario Carneiro (Sep 18 2021 at 23:48):
I wonder if I should #xy your problem though. This all looks very case-bashy and you haven't even shown what the goal is, but it looks like you are factoring a polynomial? I'm like 80% sure there is a better proof approach that involves specifying the factorization
Bolton Bailey (Sep 18 2021 at 23:49):
The problem is I keep making MWEs with simpler blocks and the error doesn't manifest.
Mario Carneiro (Sep 18 2021 at 23:51):
Your block doesn't seem to reference any lemmas except whatever is in integral_domain_simp
, so why not just have a big list of sorry lemmas and then your theorem statement? That will at least skip the top of the file
Mario Carneiro (Sep 18 2021 at 23:52):
You can also use trace_state
to see how the evaluation is progressing btw. How deep is this recursion?
Mario Carneiro (Sep 18 2021 at 23:52):
Isn't it simpler to just use iterate 3
or something?
Bolton Bailey (Sep 18 2021 at 23:53):
Let me try to answer all these questions:
Bolton Bailey (Sep 18 2021 at 23:54):
What I am trying to do is to prove that a collection of ~50 equations over an integral domain implies a goal equation.
Mario Carneiro (Sep 18 2021 at 23:55):
how in the world did that happen
Bolton Bailey (Sep 18 2021 at 23:55):
This problem arises in the context of the proof of soundness of a cryptographic construction called a SNARK.
Mario Carneiro (Sep 18 2021 at 23:56):
ah, well that explains it
Mario Carneiro (Sep 18 2021 at 23:56):
I hope you have done everything possible to simplify the problem before the main proof
Mario Carneiro (Sep 18 2021 at 23:57):
for example, if two of the equations imply a third equation that is more useful
Mario Carneiro (Sep 18 2021 at 23:57):
breaking big proofs into small pieces is good for a bunch of reasons
Bolton Bailey (Sep 18 2021 at 23:57):
Indeed, I have been working on this in parallel: I could try to look at the paper proof carefully and see if I could reduce the number of equations. Unfortunately, this doesn't bode well for being able to generalize to other constructions.
Mario Carneiro (Sep 18 2021 at 23:58):
in what way do you want to generalize?
Bolton Bailey (Sep 18 2021 at 23:58):
I want to generalize to other SNARK constructions, which have different sets of equations.
Bolton Bailey (Sep 18 2021 at 23:59):
If I hard-code in "these 30 equations are not necessary" then I have to do that every time in the future.
Mario Carneiro (Sep 18 2021 at 23:59):
I don't mean to reduce the number of equations, I mean to factor the 50 lemmas -> 1 conclusion proof into a tree like 10 x (5 equations -> 1 concl) -> 2 equations -> 1 concl
Mario Carneiro (Sep 19 2021 at 00:00):
It sounds like you want a general mechanism to prove theorems in this language
Mario Carneiro (Sep 19 2021 at 00:00):
in which case you should write a custom tactic for it
Bolton Bailey (Sep 19 2021 at 00:00):
I considered this.
Bolton Bailey (Sep 19 2021 at 00:01):
The nsatz
tactic from coq seems to do what I would want.
Bolton Bailey (Sep 19 2021 at 00:02):
I expect writing a whole tactic from scratch to resolve these equations would be harder to do though.
Mario Carneiro (Sep 19 2021 at 00:03):
I don't think you need to be so ambitious as that
Mario Carneiro (Sep 19 2021 at 00:03):
I mean a tactic to coordinate calls to simp only
and rw
Mario Carneiro (Sep 19 2021 at 00:03):
i.e. what you are doing already
Mario Carneiro (Sep 19 2021 at 00:04):
you just handle the goal manipulation and control flow
Bolton Bailey (Sep 19 2021 at 00:05):
I guess I don't understand how what you are suggesting is different from what I'm doing.
Mario Carneiro (Sep 19 2021 at 00:07):
You will have much better control over the equations and recursion ordering. It's hard to demonstrate because you still haven't shown a #mwe
Bolton Bailey (Sep 19 2021 at 00:08):
Ok, here's an mwe I'm working on where iterate and repeat behave differently
import data.nat.basic
section my_section
lemma zero_eq_zero : 0 = 0 ↔ true :=
begin
simp only [eq_self_iff_true],
end
example (a b c d e f : ℕ) (h1 : a = 0 ∨ b = 0) (h2 : d * e = a) : b * d * f * e = 0
:=
begin
iterate {
fail_if_success { done },
try {simp only [mul_eq_zero, zero_mul, mul_zero, zero_eq_zero, or_true, true_or] at *},
try {clear found_zero}, -- clear "found_zero" if present
done <|> id {cases ‹_ ∨ _› with found_zero found_zero; rw found_zero at *}, },
end
end my_section
Mario Carneiro (Sep 19 2021 at 00:08):
But it sounds like you just haven't tried writing tactics before, it's not as hard as you think and they can be as simple or complicated as you like. You should watch Rob's metaprogramming tutorial
Mario Carneiro (Sep 19 2021 at 00:09):
Note that iterate
and repeat
are different tactics that do different things, as I mentioned. So it's not that much of a surprise that they act differently
Mario Carneiro (Sep 19 2021 at 00:10):
the difference is basically iterate {tac} = tac, iterate {tac}
while repeat {tac} = tac; repeat {tac}
Mario Carneiro (Sep 19 2021 at 00:11):
The all_goals
behavior of repeat
is important for this example. This works:
begin
iterate {
fail_if_success { done },
all_goals {
try {simp only [mul_eq_zero, zero_mul, mul_zero, zero_eq_zero, or_true, true_or] at *},
try {clear found_zero}, -- clear "found_zero" if present
done <|> id {cases ‹_ ∨ _› with found_zero found_zero; rw found_zero at *} } },
end
Bolton Bailey (Sep 19 2021 at 00:12):
Mario Carneiro said:
But it sounds like you just haven't tried writing tactics before, it's not as hard as you think and they can be as simple or complicated as you like. You should watch Rob's metaprogramming tutorial
I have watched it through twice. It seems like every time I encounter a pitfall, I have to watch it again.
Mario Carneiro (Sep 19 2021 at 00:12):
If you hit any issues when writing tactics feel free to ask here
Mario Carneiro (Sep 19 2021 at 00:13):
Most tactics prefer to be called on one goal, and repeat
does that, but iterate
just calls tac
repeatedly and doesn't care if the first invocation produced many subgoals
Bolton Bailey (Sep 19 2021 at 00:20):
Mario Carneiro said:
the difference is basically
iterate {tac} = tac, iterate {tac}
whilerepeat {tac} = tac; repeat {tac}
I have never read any formal documentation for the difference between ,
and ;
. I honestly don't understand the difference between these definitions. Shouldn't they be the same?
Bolton Bailey (Sep 19 2021 at 00:23):
It seems like both of those definitions are just "recursively apply tac on all subgoals, creating a branching tree of subgoals until you reach failure".
Mario Carneiro (Sep 19 2021 at 00:23):
That's what ;
does
Mario Carneiro (Sep 19 2021 at 00:23):
,
means do tac1, then do tac2
Mario Carneiro (Sep 19 2021 at 00:24):
tac1; tac2
means do tac1, then focus on each goal produced by tac1 and do tac2 in that state, then join all the goals that result
Bolton Bailey (Sep 19 2021 at 00:27):
Ok, I understand the difference between ,
and ;
I think. But that doesn't explain why iterate fails in a situation where repeat closes all goals.
Bolton Bailey (Sep 19 2021 at 00:28):
Here's my logic: If repeat closes all goals, then essentially what it did to solve those goals is just to apply tac
over and over, to whatever goal was on top of the stack at the time. If that succeeds, shouldn't iterate do the same thing?
Bolton Bailey (Sep 19 2021 at 00:38):
I'm trying to get a better understanding by reading lean's definition of iterate
. I reached this auxiliary function
/--
`iterate_at_most' n t` repeats `t` `n` times or until `t` fails.
-/
meta def iterate_at_most' : nat → tactic unit → tactic unit
| 0 t := skip
| (succ n) t := do
(some _) ← try_core t | skip,
iterate_at_most' n t
What does the |
between try_core t
and skip
do? This is one of those things that might have been covered in Rob Lewis course, but I can't remember all the details.
Mario Carneiro (Sep 19 2021 at 00:40):
If the pattern doesn't match, it will run the thing on the other side of the |
and skip the rest of the block
Mario Carneiro (Sep 19 2021 at 00:41):
so here that means that if try_core t
returns none
(i.e. t
fails) then the tactic terminates with success
Mario Carneiro (Sep 19 2021 at 00:42):
Bolton Bailey said:
Here's my logic: If repeat closes all goals, then essentially what it did to solve those goals is just to apply
tac
over and over, to whatever goal was on top of the stack at the time. If that succeeds, shouldn't iterate do the same thing?
Tactics are called on the entire tactic state, which includes the full list of goals, not just the one on top
Mario Carneiro (Sep 19 2021 at 00:42):
Most tactics by convention only modify the top goal and leave the others alone, but not all
Mario Carneiro (Sep 19 2021 at 00:43):
in particular, done
acts differently if there are "side goals" after the main goal
Mario Carneiro (Sep 19 2021 at 00:43):
because if the main goal is done then done
will still fail because the side goals aren't
Bolton Bailey (Sep 19 2021 at 00:45):
Ok, so then does a goal disappear when it is finished?
Bolton Bailey (Sep 19 2021 at 00:46):
If I have two goals in my state that can both by finished by tac, and I call tac, tac
, does that finish all goals?
Bolton Bailey (Sep 19 2021 at 00:49):
Ok, I finally see why my mwe fails with iterate: the try {simp only [mul_eq_zero, zero_mul, mul_zero, zero_eq_zero, or_true, true_or] at *},
closes a goal, and then done <|> id {cases ‹_ ∨ _› with found_zero found_zero; rw found_zero at *}, },
is applied to a different goal
Bolton Bailey (Sep 19 2021 at 00:49):
This is what was confusing.
Mario Carneiro (Sep 19 2021 at 00:51):
Bolton Bailey said:
If I have two goals in my state that can both by finished by tac, and I call
tac, tac
, does that finish all goals?
Yes, assuming tac
only acts on the first goal
Bolton Bailey (Sep 19 2021 at 00:52):
Is there a way to say "apply this list of tactics as if the top goal was the only goal in the environment"
Bolton Bailey (Sep 19 2021 at 00:53):
I thought this was accomplished with { ... }
or id { ... }
, but neither of those make the iterate mwe succeed.
Mario Carneiro (Sep 19 2021 at 00:54):
here's a tactic that I think represents the control flow you are going for:
open tactic
meta def mysatz : tactic unit := do
`[simp only [mul_eq_zero, zero_mul, mul_zero, zero_eq_zero, or_true, true_or]
at * {fail_if_unchanged := ff}],
_::_ ← get_goals | skip,
`[cases ‹_ ∨ _› with found_zero found_zero]; (do
`[rw found_zero at *; clear found_zero],
mysatz)
example (a b c d e f : ℕ) (h1 : a = 0 ∨ b = 0) (h2 : d * e = a) : b * d * f * e = 0 :=
by mysatz
Mario Carneiro (Sep 19 2021 at 00:56):
Bolton Bailey said:
Is there a way to say "apply this list of tactics as if the top goal was the only goal in the environment"
focus { tac }
Bolton Bailey (Sep 19 2021 at 00:57):
Yep, that works.
Bolton Bailey (Sep 19 2021 at 00:58):
Ok, in the code you just posted, there are at least 3 things I am unfamiliar with.
Bolton Bailey (Sep 19 2021 at 00:58):
I guess "fail_if_unchaged" is pretty self-explanatory.
Mario Carneiro (Sep 19 2021 at 00:59):
It's basically equivalent to the try { simp only ... }
you wrote, but this gives better errors
Bolton Bailey (Sep 19 2021 at 00:59):
What is _::_ <- get_goals | skip
?
Mario Carneiro (Sep 19 2021 at 00:59):
same thing as the |
from before
Mario Carneiro (Sep 19 2021 at 01:00):
it's fail_if_success done
Mario Carneiro (Sep 19 2021 at 01:00):
actually scratch that
Mario Carneiro (Sep 19 2021 at 01:00):
if there are no goals, then it succeeds and terminates mysatz
Mario Carneiro (Sep 19 2021 at 01:01):
If we let the cases
run, then it would cause our tactic to fail
Mario Carneiro (Sep 19 2021 at 01:01):
in other words, this covers the case where the simp
closes the goal
Bolton Bailey (Sep 19 2021 at 01:02):
Right, ok
Mario Carneiro (Sep 19 2021 at 01:02):
Regarding goal handling, this is a "regular" tactic, it assumes there is one goal to start and closes the goal or fails
Mario Carneiro (Sep 19 2021 at 01:03):
we can assume the simp only
doesn't create goals so there is only one goal at the cases
Mario Carneiro (Sep 19 2021 at 01:04):
and we use ;
after cases
to run the succeeding block twice, on the two subgoals, and ensure we have only one goal
Mario Carneiro (Sep 19 2021 at 01:05):
crucially, after the rw, clear
we call mysatz
recursively
Mario Carneiro (Sep 19 2021 at 01:05):
this causes a branching tree of invocations, like what repeat
gives you
Mario Carneiro (Sep 19 2021 at 01:07):
actually, we should probably use ;
instead of ,
before the recursive call, because the rw
might also close the goal
Bolton Bailey (Sep 19 2021 at 01:10):
Ok, I understand. One more question, for my own edification. In my version of mysatz
(creatively named integral_domain_tactic
, and which was bugges for other reasons), instead of using the first ;
I was doing
...
do `[cases_type or],
trace "cases_type or succeeds"
-- If cases_type or succeeds, call tactic recursively
`[integral_domain_tactic],
`[integral_domain_tactic])
-- If cases_type or fails, rewrite using more powerful simp
Does this also work in principle, because cases_type or
always returns two subgoals, or is it flawed somehow?
Bolton Bailey (Sep 19 2021 at 01:12):
I expect the answer is "it would probably work, but ;
to do recursive subcalls is more elegant".
Mario Carneiro (Sep 19 2021 at 01:12):
It's a little riskier, because if the first integral_domain_tactic
doesn't close the goal because of bugs or otherwise, then the second integral_domain_tactic
will get a weird state
Bolton Bailey (Sep 19 2021 at 01:13):
Right, I see.
I'm taking a break, thanks so much for all your help!
Bolton Bailey (Sep 21 2021 at 01:18):
Unfortunately, I'm realizing the mysatz
tactic above is not doing quite what I want it to. After the _::_ ← get_goals | skip,
line, it might be the case that there are no ∨
present in the hypotheses. In this case, I would like the tactic to gracefully just return the state at that point as a goal, rather than fail. I'm looking at the try_core
and match
syntax to try to resolve this, but I'm not having much luck. Anyone able to help? This is what I have
meta def integral_domain_tactic : tactic unit := do
`[simp only [] with integral_domain_simp
at * {fail_if_unchanged := ff}],
try `[cases_type* true false],
_::_ ← get_goals | skip, -- If no goals left (get goals is an empty list) then skip
-- Otherwise try to recurse on an ∨.
-- I still think this may be wrong. If there is no or, we ought to stop at this point
cases_success <- try_core `[cases ‹_ ∨ _› with found_zero found_zero],
match cases_success with
| some _ := do all_goals { `[rw found_zero at *, clear found_zero], integral_domain_tactic }
| none := skip
Bolton Bailey (Sep 21 2021 at 01:21):
It doesn't work to put ;
after the try core either. Lean complains that it doesn't know the identifier cases_success
Bolton Bailey (Sep 21 2021 at 01:29):
Actually nevermind, after playing with it a few more minutes, I found
meta def integral_domain_tactic : tactic unit := do
`[simp only [] with integral_domain_simp
at * {fail_if_unchanged := ff}],
try `[cases_type* true false],
_::_ ← get_goals | skip, -- If no goals left (get goals is an empty list) then skip
-- Otherwise try to recurse on an ∨.
-- I still think this may be wrong. If there is no or, we ought to stop at this point
cases_success <- try_core `[cases ‹_ ∨ _› with found_zero found_zero],
match cases_success with
| some _ := do all_goals' `[rw found_zero at *, clear found_zero, integral_domain_tactic]
| none := skip
end
Seems to work. I'll let you all know if anything else comes up.
Last updated: Dec 20 2023 at 11:08 UTC