Zulip Chat Archive

Stream: mathlib4

Topic: congr tactic: first steps


Siddhartha Gadgil (Mar 21 2022 at 12:59):

I have made my first attempts at congr tactic. It does cover the two examples at https://leanprover-community.github.io/mathlib_docs/tactics.html#congr (including the congr' example). It is verbose however, and does not cover the rcasesPat part. I am sharing below for comments.

Also, can someone point me to docs/examples for the rcasesPat part (I have no idea what that is).

def congrStep? (closeOnly : Bool)(mvar: MVarId) : MetaM (Option (List MVarId)) := do
  let u  mkFreshLevelMVar
  let v  mkFreshLevelMVar
  try
    let res   Meta.apply mvar (mkConst ``Eq.refl [u])
    if res.isEmpty then return some [] else pure none
  catch _ =>
  try
    let res   Meta.apply mvar (mkConst ``Subsingleton.intro [u])
    if res.isEmpty then return some [] else pure none
  catch _ =>
  if !closeOnly then
    try
      let res   Meta.apply mvar (mkConst ``congr [u, v])
      return some res
    catch e =>
      pure none
  else
    pure none

partial def recCongr(maxDepth? : Option Nat)(mvar: MVarId) : MetaM (List MVarId) := do
  let closeOnly : Bool := (maxDepth?.map (fun n => decide (n   1))).getD false
  let res  congrStep? closeOnly mvar
  match res with
  | some [] => return []
  | some xs => do
    let depth? := maxDepth?.map (fun n => n - 1)
    let groups  xs.mapM (recCongr depth?)
    return groups.bind fun x => x
  | none => return [mvar]

def Meta.congr(maxDepth? : Option Nat)(mvar : MVarId) : MetaM (List MVarId) := do
  try
    let u  mkFreshLevelMVar
    let v  mkFreshLevelMVar
    let xs  Meta.apply mvar (mkConst ``congr [u, v])
    let groups  xs.mapM (recCongr maxDepth?)
    return groups.bind fun x => x
  catch e =>
    throwTacticEx `congr mvar m!"congr tactic failed"

open Lean.Elab.Tactic

syntax (name := congrTactic) "congr" (ppSpace (colGt num))? : tactic
@[tactic congrTactic] def congrTacticImpl : Tactic := fun stx =>
match stx with
| `(tactic|congr $(x?)?) =>
  withMainContext do
    let x? := x?.map <| fun card => (Syntax.isNatLit? card).get!
    liftMetaTactic (Meta.congr x?)
| _ => throwIllFormedSyntax

example (x y w: Nat)(f g: Nat  Nat): x * f y = g w * f z := by
  congr
  repeat (exact sorry)

example (x y : Nat)(f g: Nat  Nat): f (g (x + y)) = f (g (y + x)) := by
  congr 2 -- goal remaining : x + y = y + x
  skip
  repeat (exact sorry)

Arthur Paulino (Mar 21 2022 at 13:42):

rcasesPat is declared (and expanded) here

Siddhartha Gadgil (Mar 21 2022 at 14:51):

Thanks. What would it mean to have such a pattern in the congr tactic?

Arthur Paulino (Mar 21 2022 at 14:56):

I don't know :D
But I think Mario or Gabriel will be able to explain

Siddhartha Gadgil (Mar 21 2022 at 14:58):

@Gabriel Ebner Can I ask what it means to say congr with (x, y) or some other such pattern? Should one try to match both sides of an equality to the pattern, or the full proposition? Is it done before, after, or instead of function application?
Or is it something entirely different?

Floris van Doorn (Mar 21 2022 at 15:11):

In lean 3 it's just that rcases is being applied to the introduced variable(s) with that pattern (or rintro)

Siddhartha Gadgil (Mar 21 2022 at 15:22):

I see.
Does this mean that after each iteration of apply congr I should call rcases on the returned variables? With a failure not being an error, but just not decomposing further?

Mario Carneiro (Mar 21 2022 at 18:53):

src#tactic.interactive.congr'

Mario Carneiro (Mar 21 2022 at 18:56):

the two parts are not intermingled, and you can probably just write it as a macro tactic. It does (congr n with pats) := focus (congr n <;> ext pats)

Mario Carneiro (Mar 21 2022 at 18:57):

(For this kind of tactic implementation I do recommend reading the source of the original tactic to get the control flow right)

Siddhartha Gadgil (Mar 22 2022 at 06:17):

Thanks. Indeed I should look at lean 3 mathlib and the already implemented tactics more.
In this case:

So it seems the best I can do now is do what congr and congr' did in mathlib for lean 3. I will try to streamline my code, and any suggestions to improve it are very welcome.

Siddhartha Gadgil (Mar 22 2022 at 08:30):

Apologies @Mario Carneiro : I see that lean 3 docs clearly say ext is used with given pattern. I will read these with care in the future.

Mario Carneiro (Mar 22 2022 at 08:31):

I mean the source specifically, not just the docs. Sometimes the docs are vague about what happens in such and such edge case, but you can find out by reading the source and/or doing some unit testing of the tactic

Mario Carneiro (Mar 22 2022 at 08:33):

I believe that congr should exist in lean 4 for the same reason it existed in lean 3: it was originally written as an internal mechanism for simp to be able to traverse inside terms, and it was later exposed as a standalone tactic

Siddhartha Gadgil (Mar 22 2022 at 08:41):

I realized that, but it is more egregious to miss something in the docs. As I started using lean 4 without using lean 3 more than a tiny amount, I have not gotten the habit of looking at the lean 3 sources (while learning most things by trawling the lean 4 source).

I see meanwhile that ext now supports the rintro syntax. I will try to cover that case too.

Siddhartha Gadgil (Mar 22 2022 at 08:57):

Specific question: @Gabriel Ebner and @Mario Carneiro : the (unimplemented) syntax of congr uses rcasesPat while the syntax of extuses rintroPat. On the other hand a TODO in ext mentioned rcasesPat as intended syntax.

My question: is this a result of everything being in a transient state, or is the intention that ext is called after relating the two types of patterns (I can see that they are related)?

Mario Carneiro (Mar 22 2022 at 08:59):

The lean 4 tactics are intended to be an exact match (or possibly a superset) for the corresponding lean 3 tactics. So the source of truth here is whatever is in mathlib

Mario Carneiro (Mar 22 2022 at 09:04):

For tactics that do a list of name introductions with pattern matching, rintroPat makes more sense. Note that rintroPat is a superset of rcasesPat, which supports binders like (a b c : Nat) that would not make sense as a cases/obtain pattern e.g. obtain (a b c: Nat) := foo

Mario Carneiro (Mar 22 2022 at 09:04):

So IMO both congr and ext should use rintroPat

Siddhartha Gadgil (Mar 22 2022 at 12:14):

I have made a pull request at https://github.com/leanprover-community/mathlib4/pull/247. As far as I can tell this is correct in the basic cases. I could not test the case with patterns.

If there is something I should change to fit in with the porting plans (or because it is wrong) please let me know. I plan to try implementing symm, trans and rfl' next (modeled on the ext implementation).

Siddhartha Gadgil (Mar 22 2022 at 12:16):

Incidentally, ext gives a confusing error message when the tactic fails, saying incorrect syntax and including a whole block (maybe it is trying to parse the whole block as a tactic).

Mario Carneiro (Mar 22 2022 at 12:48):

#mwe?

Mario Carneiro (Mar 22 2022 at 12:49):

That sounds like a bug in the parser or the input, so what does the input look like?

Siddhartha Gadgil (Mar 22 2022 at 13:06):

Modified from the test file:

example (a b : C n) : a = b := by
  ext x

Siddhartha Gadgil (Mar 22 2022 at 13:10):

Since ext x is perfectly valid syntax which works elsewhere, I would expect a failed tactic, but we get incorrect syntax.
I get a similar error elsewhere, but that could be my mistake.

Siddhartha Gadgil (Mar 22 2022 at 13:12):

I guess the above was not self-contained, so here is a fuller example (the above is ext.lean modified).

import Mathlib.Tactic.Ext
import Mathlib.Init.Logic

structure A (n : Nat) where
  a : Nat

structure B (n) extends A n where
  b : Nat
  h : b > 0
  i : Fin b

@[ext] structure C (n) extends B n where
  c : Nat

example (a b : C n) : a = b := by
  ext x

Mario Carneiro (Mar 22 2022 at 13:13):

The ext attribute before the example is throwing several index out of bounds errors

Siddhartha Gadgil (Mar 22 2022 at 13:14):

I see that too. Had not noticed.

Mario Carneiro (Mar 22 2022 at 13:15):

Note that one way tactics can "throw an error" is throwUnsupportedSyntax which is usually used to allow overlapping syntaxes to take a shot at interpreting the input. I guess this is what it looks like if nothing is found

Siddhartha Gadgil (Mar 22 2022 at 13:19):

I checked in mathlib4 and all cases I see of throwUnsupportedSyntax are the fallback case in a pattern match.

Siddhartha Gadgil (Mar 22 2022 at 13:20):

I am puzzled by this being triggered in a context dependent way, i.e., not just the syntactic context.

Mario Carneiro (Mar 22 2022 at 13:20):

it's also used by things like macro_rules and elab_rules as the implicit fallback

Mario Carneiro (Mar 22 2022 at 13:20):

I don't see evidence that this is not simply a syntactic issue

Siddhartha Gadgil (Mar 22 2022 at 13:25):

In the same file, the following is valid

example (f g : Nat × Nat  Nat) : f = g := by
  ext x

but the following is not

example (a b : C n) : a = b := by
  ext x

Mario Carneiro (Mar 22 2022 at 13:25):

I think rintro is throwing the exception

Mario Carneiro (Mar 22 2022 at 13:26):

because ext x expands to apply_ext_lemma; rintro x; skip

Siddhartha Gadgil (Mar 22 2022 at 13:27):

I see. My main concern is somewhere else where I have seemingly identical code which works if entered by hand but not by macros. I feel this may be related.

Mario Carneiro (Mar 22 2022 at 13:28):

I think it's a precedence issue in rintroPat

Siddhartha Gadgil (Mar 22 2022 at 13:28):

Here is the full code (the macro is based on what you said).

import Lean.Meta
import Mathlib.Mathport.Syntax
import Mathlib.Tactic.Ext

namespace Mathlib.Tactic.Congr
open Lean Meta Elab

/-- try to close goal using reflexivity and subsingletons -/
def tryCloseGoal (mvar: MVarId) : MetaM Bool := do
  try
    let res   Meta.apply mvar ( mkConstWithFreshMVarLevels ``Eq.refl)
    unless res.isEmpty do
      throwError "failed to close goal"
    pure true
  catch _ =>
  try
    let res   Meta.apply mvar ( mkConstWithFreshMVarLevels ``Subsingleton.intro)
    unless res.isEmpty do
      throwError "failed to close goal"
    pure true
  catch _ =>
    pure false

/-- apply `congr` after trying to close goal, optionally return result if successful -/
def congrStep? (mvar: MVarId) : MetaM (Option (List MVarId)) := do
  let closed   tryCloseGoal mvar
  if !closed then
    try
      let res   Meta.apply mvar ( mkConstWithFreshMVarLevels ``congr)
      return some res
    catch e =>
      pure none
  else
    pure none

/-- recursively apply congr and try to close goals, not an error if tactics fail -/
partial def recCongr(maxDepth? : Option Nat)(mvar: MVarId) : MetaM (List MVarId) := do
  let closeOnly : Bool := (maxDepth?.map (fun n => decide (n   1))).getD false
  if closeOnly then
    let  chk  tryCloseGoal mvar
    if chk then return [] else return [mvar]
  let res  congrStep? mvar
  match res with
  | some [] => return []
  | some xs => do
    let depth? := maxDepth?.map (fun n => n - 1)
    let groups  xs.mapM (recCongr depth?)
    return groups.bind id
  | none => return [mvar]

/-- apply `congr` and then continue recursively; error if first application fails -/
def Meta.congr(maxDepth? : Option Nat)(mvar : MVarId) : MetaM (List MVarId) := do
  try
    let u  mkFreshLevelMVar
    let v  mkFreshLevelMVar
    let xs  Meta.apply mvar (mkConst ``congr [u, v])
    let groups  xs.mapM (recCongr maxDepth?)
    return groups.bind id
  catch e =>
    throwTacticEx `congr mvar e.toMessageData

open Lean.Elab.Tactic

syntax (name := congrBase) "congr_base" (ppSpace (colGt num))? : tactic

/--
The `congr` tactic attempts to identify both sides of an equality goal `A = B`, leaving as new goals the subterms of `A` and `B` which are not definitionally equal. Example: suppose the goal is `x * f y = g w * f z`. Then congr will produce two goals: `x = g w` and `y = z`.

If `x y : t`, and an instance subsingleton t is in scope, then any goals of the form `x = y` are solved automatically.

The `congr` tactic, but takes an optional argument which gives
the depth of recursive applications.
* This is useful when `congr` without a depth bound is too aggressive in breaking down the goal.
* For example, given `⊢ f (g (x + y)) = f (g (y + x))`, `congr'` produces the goals `⊢ x = y`
  and `⊢ y = x`, while `congr' 2` produces the intended `⊢ x + y = y + x`.
* If, at any point, a subgoal matches a hypothesis then the subgoal will be closed.
-/
@[tactic congrBase] def congrTacticImpl : Tactic := fun stx =>
match stx with
| `(tactic|congr_base $(x?)?) =>
  withMainContext do
    let x? := x?.map <| fun card => (Syntax.isNatLit? card).get!
    liftMetaTactic (Meta.congr x?)
| _ => throwIllFormedSyntax

macro_rules
| `(tactic| congr $(x?)?) => do
    `(tactic|congr_base $(x?)?)
| `(tactic| congr $(x?)? with $xs) => do
    `(tactic| congr_base $(x?)?; ext $xs)

example (x y w: Nat)(f g: Nat  Nat): x * f y = g w * f z := by
  congr
  have : x = g w := sorry
  assumption
  have : y = z := sorry
  assumption

example (x y : Nat)(f g: Nat  Nat): f (g (x + y)) = f (g (y + x)) := by
  congr 2
  have : x + y = y + x := sorry
  assumption

example (x y : Nat)(f g: Nat  Nat): f (x + y) = (g (y + x)) := by
  -- congr 2 with x
  congr_base 2; ext x
  admit
  admit
  admit

Siddhartha Gadgil (Mar 22 2022 at 13:31):

If I uncomment the commented line close to the end and comment the next, I get the error unexpected syntax with the block just like that inserted by hand giving an error.
I don't understand macros well, so assumed it was my error. But I see the behaviour that I at least do not expect elsewhere, and suspect these are related.

Mario Carneiro (Mar 22 2022 at 14:05):

I minimized the issue to the following:

import Lean

elab "fails" : tactic => throwError "nope"

syntax "foo" (ppSpace ident)? : tactic
macro_rules | `(tactic| foo) => `(tactic| skip)
macro_rules | `(tactic| foo $x:ident) => `(tactic| fails; skip)

example : True := by foo x

Mario Carneiro (Mar 22 2022 at 14:07):

The problem seems to be that when a macro has multiple macro_rules definitions, if all of the definitions fail (in any way, not just by throwUnsupportedSyntax) then it throws a syntax error instead of the result of the (first? last?) tactic

Mario Carneiro (Mar 22 2022 at 14:08):

which makes me think that it is best not used as a control flow mechanism in nontrivial cases

Mario Carneiro (Mar 22 2022 at 14:10):

the macro in question is ext_or_simp btw

Mario Carneiro (Mar 22 2022 at 14:10):

and this syntax error is going to bubble up to ext and congr that call this tactic internally

Siddhartha Gadgil (Mar 22 2022 at 14:28):

A lot of the lean 4 tactics seem to be implemented at Meta/Elab level and chained that way.

Siddhartha Gadgil (Mar 22 2022 at 14:28):

Perhaps that is the most robust approach.

Siddhartha Gadgil (Mar 22 2022 at 14:28):

Call things as functions, rather than manipulate syntax.

Siddhartha Gadgil (Mar 22 2022 at 14:29):

I am very biased by being much more comfortable with the non-syntactic style.

Mario Carneiro (Mar 22 2022 at 15:14):

I also think that elab tactics are superior: you have more control, less overhead, and a reasonable programming language to work in

Mario Carneiro (Mar 22 2022 at 15:15):

I would only prefer macro tactics for trivial syntactic manipulations. Even something like repeat is a bit of a stretch to do as a recursive macro expansion

Siddhartha Gadgil (Mar 22 2022 at 15:22):

What has happened here is that I had to go to a syntactic level as the tactics to use are at macro level. Best may be to move to something else for now without this problem (e.g. an attempt at symm).

Arthur Paulino (Mar 22 2022 at 15:24):

Mario Carneiro said:

less overhead

Why do elab tactics have less overhead? I thought it was more expensive to lift a TacticM monad

Mario Carneiro (Mar 22 2022 at 15:27):

lifting is usually negative work: there is some small overhead to prepare the input and output but most of the execution is now happening in a smaller monad which means less things to pass around on every step

Mario Carneiro (Mar 22 2022 at 15:27):

the thing that makes macro tactics expensive is that you are constantly parsing and reinterpreting syntax into expressions

Mario Carneiro (Mar 22 2022 at 15:28):

if you use repeat to execute exact foo a bunch of times it has to parse and elaborate the syntax foo into an expression many times

Mario Carneiro (Mar 22 2022 at 15:29):

whereas if you have a MetaM tactic foo will usually be an Expr already

Arthur Paulino (Mar 22 2022 at 15:29):

Got it. And then calling evalTactic ⋯ inside TacticM is even more expensive I presume?

Mario Carneiro (Mar 22 2022 at 15:29):

it's about the same cost as a macro tactic

Mario Carneiro (Mar 22 2022 at 15:30):

probably a bit less since you aren't calling expandMacros too

Mario Carneiro (Mar 22 2022 at 15:31):

On the other hand, interactive tactics do a lot of things for you and when you take it into your own hands you have to make sure to do all the side stuff

Mario Carneiro (Mar 22 2022 at 15:32):

for example this is needed for getting go-to-definition on a binder to work

Mario Carneiro (Mar 22 2022 at 15:33):

eventually we will reimplement these things enough times that the common utilities will naturally be discovered

Siddhartha Gadgil (Mar 23 2022 at 04:38):

Some updates: now I cover the ext case as well at least in a basic example. I had to struggle a bit to pass around the syntax, and things worked when - as @Mario Carneiro said should be the case - I changed rcasesPat to rintroPatin the syntax for congr (otherwise when the syntax was passed around it was no longer recognized).

Can I request a review @Mario Carneiro @Gabriel Ebner ? This looks to me like a largely correct implementation, and before trying more implementations I want to understand how this matches with the maintainers expectations.

Mario Carneiro (Mar 23 2022 at 04:46):

I suspect that just calling apply congr isn't going to work in dependent situations. Did you take a look at how simp is currently doing congruence and whether you can reuse that backend?

Mario Carneiro (Mar 23 2022 at 04:48):

You should try porting the lean 3 tests for congr if there are any; you need more dependent function and subsingleton tests, for example congruence of if expressions

Mario Carneiro (Mar 23 2022 at 04:57):

I think you should look into src4#Lean.Meta.CongrTheorem and src#tactic.congr

Siddhartha Gadgil (Mar 23 2022 at 05:09):

Thanks @Mario Carneiro - I will read the sources you mention and look for tests.

Mario Carneiro (Mar 23 2022 at 05:10):

the main logic is in src4#Lean.Meta.Simp.simp which clearly has a congruence component, but it's not clear how extractable it is from the simp core

Mario Carneiro (Mar 23 2022 at 05:14):

I think you want to adapt tryAutoCongrTheorem? to do something similar but call congr instead of simp recursively

Siddhartha Gadgil (Mar 23 2022 at 05:24):

Yes, it does look like much of the work is done there. On the other hand, I believe simp will not resolve f x = g y to goals f = g and x = y, as congr should. I will digest the code and then get back to trying to implement a better congr.

I should also follow the same congr_core pattern as the lean implementation (with updated syntax).

Mario Carneiro (Mar 23 2022 at 05:55):

We aren't using simp directly here, so the question is closer to whether simp is capable of performing simplifications when they are available in both f and x. And it looks like it is:

example (h1 : (f : α  β) = g) (h2 : x = y) : f x = g y := by simp [h1, h2]

Mario Carneiro (Mar 23 2022 at 05:56):

congr uses the same steps as simp, it just does the subgoal management differently

Siddhartha Gadgil (Mar 23 2022 at 06:40):

Shouldn't congr break up f x = g y into f = g and x = y, as apply congr will? I agree with your point that most (perhaps all) of the non-trivial stuff done will overlap with simp, and indeed the code for simp shows this.

Siddhartha Gadgil (Mar 23 2022 at 06:50):

Okay, I see from the lean 3 code that one matches for a (heterogeneous) equality, then looks if the lhs is a function application and if it is uses the same stuff as in simplification.

Siddhartha Gadgil (Mar 23 2022 at 06:52):

In lean 4 there are methods Expr.eq? and Expr.heq? which do the matching (I have used the former a fair bit). So one can glue these with appropriately extracted code from simp (which I have to digest further to know what is the appropriate code).


Last updated: Dec 20 2023 at 11:08 UTC