Zulip Chat Archive

Stream: metaprogramming / tactics

Topic: Code review for small tactic


Heather Macbeth (Jan 11 2022 at 04:47):

I wrote my first small tactic, basically a wrapper for fin_cases, which case splits according to the remainder of a variable n modulo a variable p. I'd be really glad to get feedback on doing things more efficiently, or more according to convention.

import tactic.interactive
import tactic.fin_cases
import tactic.norm_cast
import data.int.modeq

lemma int.exists_unique_modeq_mem_range (a : ) (b : ) (hb : 0 < b) :
   z  finset.range b, a  z [ZMOD b] :=
begin
  obtain a₀, ha₀, ha₀n := int.exists_unique_equiv_nat a (by exact_mod_cast hb : (0:) < b),
  norm_cast at ha₀,
  refine a₀, finset.mem_range.mpr ha₀, _⟩,
  apply int.modeq.symm,
  exact ha₀n
end

namespace tactic

meta def mod_cases (n p : expr) : tactic unit :=
do z  mk_app `nat.zero [],
   pos  mk_app `has_lt.lt [z, p],
   assert `h_pos pos, tactic.exact_dec_trivial, -- make a proof that `p` is positive
   hh  get_local `h_pos,
   w  mk_app `int.exists_unique_modeq_mem_range [n, p, hh],
   [(p, [w_name, hw_aux₂])]  tactic.cases w [`k, `hk_aux],
   [(p', [hk_name, H_name])]  tactic.cases hw_aux₂ [`hk, `H],
   tactic.fin_cases_at none none hk_name,
   all_goals (do H'  get_local `H,
                  junk  replace_at norm_cast.derive [H'] ff,
                  pure ()),
   pure ()


namespace interactive
setup_tactic_parser

meta def mod_cases (n p : parse parser.pexpr) : tactic unit :=
do p'  to_expr p,
   n'  to_expr n,
   tactic.mod_cases n' p'

end interactive
end tactic

example (n : ) :  ¬ (4  n ^ 2 - 3) :=
begin
  mod_cases n 4,

end

Heather Macbeth (Jan 11 2022 at 04:49):

Some questions:

  • how can I make the syntax mod_cases n 4 using hn, to name the hypothesis produced?
  • how can I protect against bad variable inputs (n not an integer, p not a nat, p not positive)?
  • can I do the tactic.cases application more efficiently?

Mario Carneiro (Jan 11 2022 at 05:07):

Since p needs to be a numeral known to the tactic, you can use small_nat to parse it instead of parse pexpr

Mario Carneiro (Jan 11 2022 at 05:08):

that will restrict users of the tactic to write numerals, i.e. 4 not 2+2 and certainly not k, but that should be fine for this application

Mario Carneiro (Jan 11 2022 at 05:10):

n on the other hand can be a non-numeral expression, so you want to use parse pexpr there. If you put some kind of punctuation or keyword between n and p then you can parse n at low precedence which is probably nicer if n is a composite expression like a + b

Mario Carneiro (Jan 11 2022 at 05:11):

The call to mk_app `int.exists_unique_modeq_mem_range will automatically check that n is an integer, but another way to do it is to use i_to_expr ``(%%p : int) instead of to_expr p

Mario Carneiro (Jan 11 2022 at 05:19):

To parse an optional using hn, you can use parse (tk "using" *> ident)?

Heather Macbeth (Jan 11 2022 at 05:21):

Thanks Mario! What should I do to protect against the ugly fail if p = 0?

exact tactic failed, type mismatch, given expression has type
  true
but is expected to have type
  as_true (0 < 0)

Mario Carneiro (Jan 11 2022 at 05:23):

altogether it looks like this:

namespace tactic

meta def mod_cases (n : expr) (p' : ) (H : name) : tactic unit :=
do z  mk_app `nat.zero [],
  p  expr.of_nat `() p',
  pos  mk_app `has_lt.lt [z, p],
  assert `h_pos pos, tactic.exact_dec_trivial, -- make a proof that `p` is positive
  hh  get_local `h_pos,
  w  mk_app `int.exists_unique_modeq_mem_range [n, p, hh],
  [(p, [w_name, hw_aux₂])]  tactic.cases w [`k, `hk_aux],
  [(p', [hk_name, H_name])]  tactic.cases hw_aux₂ [`hk, H],
  tactic.fin_cases_at none none hk_name,
  () <$ all_goals (do
    H'  get_local H,
    () <$ replace_at norm_cast.derive [H'] ff)


namespace interactive
setup_tactic_parser

meta def mod_cases
  (n : parse parser.pexpr)
  (p : parse small_nat)
  (h : parse (tk "using" *> ident)?) : tactic unit :=
do n'  to_expr ``(%%n : ),
   tactic.mod_cases n' p (h.get_or_else `H)

end interactive
end tactic

example (n : ) :  ¬ (4  n ^ 2 - 3) :=
begin
  mod_cases n 4 using a,
end

Mario Carneiro (Jan 11 2022 at 05:24):

The tactic also puts an unnecessary h_pos in the context; here you only need that expression in order to construct w so there is no need to have it

Mario Carneiro (Jan 11 2022 at 05:25):

You should use norm_num (or rather norm_num.prove_pos) instead of exact_dec_trivial to prove p is positive

Mario Carneiro (Jan 11 2022 at 05:26):

Since p' is just a nat here, you can provide a nicer error message with e.g. when (p = 0) (fail "nope")

Mario Carneiro (Jan 11 2022 at 05:27):

you can even make it a parse error with parse (do n <- small_nat, guard (n != 0), pure n) for the p parameter

Mario Carneiro (Jan 11 2022 at 05:32):

Here's how you would use norm_num to prove 0 < p:

meta def mod_cases (n : expr) (p' : ) (H : name) : tactic unit :=
do z  mk_app `nat.zero [],
  p  expr.of_nat `() p',
  pos  mk_app `has_lt.lt [z, p],
  ic  mk_instance_cache `(),
  (_, hh)  norm_num.prove_pos_nat ic p,
  w  mk_app `int.exists_unique_modeq_mem_range [n, p, hh],
  [(p, [w_name, hw_aux₂])]  tactic.cases w [`k, `hk_aux],
  [(p', [hk_name, H_name])]  tactic.cases hw_aux₂ [`hk, H],
  tactic.fin_cases_at none none hk_name,
  () <$ all_goals (do
    H'  get_local H,
    () <$ replace_at norm_cast.derive [H'] ff)

Heather Macbeth (Jan 11 2022 at 06:12):

Thank you! That _ trick is nice, I see I can do

  [(_, [_, hw_aux₂])]  tactic.cases w [`k, `hk_aux],
  [(_, [hk_name, _])]  tactic.cases hw_aux₂ [`hk, H],

later as well. I also didn't know how about norm_num.prove_pos_nat to extract a proof of positivity.

Is there a way to get (0:ℕ) faster than z ← mk_app nat.zero []`?

Is there a way to combine the two tactic.cases?

Heather Macbeth (Jan 11 2022 at 06:12):

And I also couldn't see where to slot in the

parse (do n <- small_nat, guard (n != 0), pure n)

Mario Carneiro (Jan 11 2022 at 06:22):

For (0:nat) specifically (i.e. you know the type in advance so no typeclass search is needed), you can just use let zero := `(0:nat)

Mario Carneiro (Jan 11 2022 at 06:24):

Re: combining cases, not really, although you can craft your lemma as an induction principle so that you don't need the cases

Heather Macbeth (Jan 11 2022 at 06:24):

Also, do I understand correctly that I ought to use get_unused_name to ensure that the cases don't accidentally receive names that already exist in context?

  A₁  get_unused_name,
  A₂  get_unused_name,
  [(_, [_, hw])]  tactic.cases w,
  [(_, [hk, _])]  tactic.cases hw [A₁, A₂],

Mario Carneiro (Jan 11 2022 at 06:24):

rcases just calls cases repeatedly

Mario Carneiro (Jan 11 2022 at 06:25):

You usually shouldn't have to worry about that. You only need to provide names if you are routing some names provided by the user

Mario Carneiro (Jan 11 2022 at 06:26):

you can just use `_ for everything you don't want to name

Mario Carneiro (Jan 11 2022 at 07:24):

It's a shame that fin_cases doesn't have a lower level interface to save some computation here. I didn't take a deep look at what fin_cases is doing, but here's a version that performs significantly better and skips cases and such:

import tactic.interactive
import tactic.fin_cases
import tactic.norm_cast
import data.int.modeq

namespace int

-- these should exist in data.int.modeq (in preference to the exists version?)
def sigma_unique_equiv (a : ) {b : } (hb : 0 < b) : {z :  // 0  z  z < b  z  a [ZMOD b]} :=
 a % b, mod_nonneg _ (ne_of_gt hb),
  have a % b < |b|, from mod_lt _ (ne_of_gt hb),
  by rwa abs_of_pos hb at this,
  by simp [modeq] 

def sigma_unique_equiv_nat (a : ) {b : } (hb : 0 < b) : {z :  // z < b  z  a [ZMOD b]} :=
let z, hz1, hz2, hz3 := sigma_unique_equiv a hb in
z.nat_abs, by split; rw [of_nat_eq_coe, of_nat_nat_abs_eq_of_nonneg hz1]; assumption

end int

namespace tactic

def on_mod_cases (n : ) (a lb : ) (p : Sort*) :=
 z, lb  z  z < n  a  z [ZMOD n]  p

lemma on_mod_cases_start (p : Sort*) (a : ) (n : ) (hn : 0 < n)
  (H : on_mod_cases n a 0 p) : p :=
begin
  obtain a₀, ha₀, ha₀n := int.sigma_unique_equiv_nat a (by exact_mod_cast hn : (0:) < n),
  exact H a₀ (by simp) (by simp [ha₀]) ha₀n.symm
end

lemma on_mod_cases_stop {p : Sort*} {n : } {a n' : } (h : n' = n) :
  on_mod_cases n a n' p :=
λ z h₁ h₂, (not_lt_of_le h₁ $ h.symm  h₂).elim

lemma on_mod_cases_succ {p : Sort*} {n : } {a b c : } (bc : b + 1 = c)
  (h : a  b [ZMOD n]  p) (H : on_mod_cases n a c p) :
  on_mod_cases n a b p :=
λ z h₁ h₂ hm, if he : b = z then h (he.symm  hm) else
  H z (bc  lt_of_le_of_ne h₁ he) h₂ hm

meta def prove_mod_cases (u : level) (p n a : expr) (nn : ) :
  instance_cache  instance_cache  expr    tactic (list expr × expr)
| zc nc b nb := if nn  nb then do
    (_, _, _, pr)  norm_num.prove_nat_uncast zc nc b,
    pure ([], pr)
  else do
    (zc, c, bc)  norm_num.prove_succ' zc b,
    g  to_expr_strict ``(%%a  %%b [ZMOD (%%n : )]  %%p) >>= mk_meta_var,
    (gs, pr)  prove_mod_cases zc nc c (nb+1),
    pure (g::gs, (expr.const ``on_mod_cases_succ [u]).mk_app [p, n, a, b, c, bc, g, pr])

meta def mod_cases (a : expr) (nn : ) (H : name) : tactic unit :=
focus1 $ do
  p  target,
  zc  mk_instance_cache `(),
  nc  mk_instance_cache `(),
  (nc, n)  nc.of_nat nn,
  (nc, hn)  norm_num.prove_pos_nat nc n,
  refine ``(on_mod_cases_start _ %%a %%n %%hn _),
  expr.sort u  infer_type p | failure,
  (gs, pr)  try_for 1000 (prove_mod_cases u p n a nn zc nc `(0:) 0),
  exact pr,
  set_goals gs,
  () <$ all_goals (intro H)

namespace interactive
setup_tactic_parser

meta def mod_cases
  (n : parse parser.pexpr)
  (p : parse (do n  small_nat, guard (n  0), pure n))
  (h : parse (tk "using" *> ident)?) : tactic unit :=
do n'  to_expr ``(%%n : ),
   tactic.mod_cases n' p (h.get_or_else `H)

end interactive
end tactic

example (n : ) :  ¬ (4  n ^ 2 - 3) :=
begin
  mod_cases n 100 using a,
end

Mario Carneiro (Jan 11 2022 at 07:25):

@Heather Macbeth

Heather Macbeth (Jan 11 2022 at 16:20):

@Mario Carneiro This is very interesting, I will study it! But also, perhaps you could PR it? :). Much better than I came up with.

Mario Carneiro (Jan 11 2022 at 16:22):

Also, I was going to mention that using h is not a conventional way to ask for a name in a tactic; I would suggest h : instead, or perhaps with h

Mario Carneiro (Jan 11 2022 at 16:23):

there aren't too many tactics using using, but those that do expect an expression after it

Heather Macbeth (Jan 11 2022 at 20:03):

Mario Carneiro said:

there aren't too many tactics using using, but those that do expect an expression after it

Indeed, I realised later and changed to with in my local version!


Last updated: Dec 20 2023 at 11:08 UTC