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} while repeat {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_coreand 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