Zulip Chat Archive

Stream: mathlib4

Topic: change at


Eric Wieser (Mar 12 2023 at 17:04):

When I hover over change, I get a docstring that suggests change t at h is supported:
image.png

But the definition is

/-- `change` is a synonym for `show`,
and can be used to replace a goal with a definitionally equal one. -/
macro_rules
  | `(tactic| change $e:term) => `(tactic| show $e)

Where is this incorrect docstring coming from?

Eric Wieser (Mar 12 2023 at 17:43):

Ah, I see this came up earlier in https://leanprover.zulipchat.com/#narrow/stream/113489-new-members/topic/why.20show.2E.2Efrom.20.3F/near/340489181

Ruben Van de Velde (Mar 12 2023 at 17:53):

It's definitely not supported. You can use replace h : xxx := h

Eric Wieser (Mar 12 2023 at 18:07):

Where's the docstring coming from that claims it is supported?

Eric Wieser (Mar 12 2023 at 18:31):

Ah, it's in core as

/--
* `change tgt'` will change the goal from `tgt` to `tgt'`,
  assuming these are definitionally equal.
* `change t' at h` will change hypothesis `h : t` to have type `t'`, assuming
  assuming `t` and `t'` are definitionally equal.
-/
syntax (name := change) "change " term (location)? : tactic

Kevin Buzzard (Mar 12 2023 at 18:43):

This keeps coming up. Is it really anything more than a few lines of tactic code to just do "revert h, change [what_you_want_h_to_be] -> [old goal], intro h"? I have no clue about tactic-writing but this looks like something which could easily be added to mathlib by someone who does.

Eric Wieser (Mar 12 2023 at 18:53):

I have no idea how to write Lean4 tactics, but my initial attempt is

elab_rules : tactic
  | `(tactic| change $e:term $[$l:location]?) => do
    let l := expandOptLocation l
    match l with
    | wildcard => fail
    | targets ts include_goal => do
      for t in ts
        skip -- TODO: `have $t : $e = $t, clear $t`
        -- let hId? := (← getLCtx).findFromUserName? name |>.map fun d ↦ d.fvarId
        -- evalTactic $ ← `(tactic| have $[$n?]? $[: $t?]? := $v)

      if include_goal
        evalTactic $  `(tactic| show $e)

Eric Wieser (Mar 12 2023 at 18:55):

The actual tactic work is trivial, I just have no idea how to process a location, or write a for loop

Eric Wieser (Mar 12 2023 at 18:56):

And I have no idea what I should be reading to learn about this

Michael Stoll (Mar 12 2023 at 19:20):

Eric Wieser said:

Ah, it's in core as

/--
* `change tgt'` will change the goal from `tgt` to `tgt'`,
  assuming these are definitionally equal.
* `change t' at h` will change hypothesis `h : t` to have type `t'`, assuming
  assuming `t` and `t'` are definitionally equal.
-/
syntax (name := change) "change " term (location)? : tactic

If you copied it correctly, then the second part has a double "assuming"...

Eric Wieser (Mar 12 2023 at 19:25):

I assume you meant to say "assuming
assuming I copied it correctly"...

Adam Topaz (Mar 12 2023 at 19:42):

for the for loop, I think you're missing a do

Adam Topaz (Mar 12 2023 at 19:58):

BTW, why don't we use TSyntax ident for the targets constructor of docs4#Lean.Elab.Tactic.Location ?

Arien Malec (Mar 12 2023 at 20:08):

Ruben Van de Velde said:

It's definitely not supported. You can use replace h : xxx := h

I clarified the section on the porting wiki on this.

Adam Topaz (Mar 12 2023 at 20:26):

I don't know how to clear the old fvar, but here's something that kinda works:

import Lean

open Lean Elab Tactic

#check Location
elab_rules : tactic
  | `(tactic| change $e:term $[$l:location]?) => do
    match l with
    | some l =>
        match expandLocation l with
        | .wildcard => throwError "Wildcards are not supported."
        | .targets ts incl =>
          let allowed : Bool := (ts.size == 0 && incl) || (ts.size == 1 && !incl)
          if !allowed then throwError "Changing in more than one location is not supported"
          if incl then evalTactic <|  `(tactic| show $e)
          else
            let t := ts[0]!
            evalTactic <|  `(tactic| have $(TSyntax.mk t) : $e := $(TSyntax.mk t))
    | none => evalTactic <|  `(tactic| show $e)


example (n m : Nat) (h : n+2 = m) : n+1+1 = m := by
  change n+1+1 = m at h
  sorry

Adam Topaz (Mar 12 2023 at 20:27):

well change at \goal doesn't work.

Adam Topaz (Mar 12 2023 at 20:30):

I think I'm just misunderstanding what expandLocation actually does

Adam Topaz (Mar 12 2023 at 21:18):

Adam Topaz said:

I don't know how to clear the old fvar, but here's something that kinda works:

I forgot about replace... here's a (hacky) approximation:

import Mathlib.Tactic.Replace
import Lean

open Lean Elab Tactic

#check Location
elab_rules : tactic
  | `(tactic| change $e:term $[$l:location]?) => do
    match l with
    | some l =>
        match expandLocation l with
        | .wildcard => throwError "Wildcards are not supported."
        | .targets ts incl =>
          let allowed : Bool := (ts.size == 0 && incl) || (ts.size == 1 && !incl)
          if !allowed then throwError "Changing in more than one location is not supported"
          if incl then evalTactic <|  `(tactic| show $e)
          else
            let t := ts[0]!
            evalTactic <|  `(tactic| replace $(TSyntax.mk t) : $e := $(TSyntax.mk t))
    | none => evalTactic <|  `(tactic| show $e)

example (n m : Nat) (h : n+2 = m) : n+1+1 = m := by
  change n+1+1 = m at h
  change n+2 = m
  sorry

Kyle Miller (Mar 13 2023 at 01:02):

@Kevin Buzzard It's a little tricker than just revert/change/intro because the revert might revert multiple hypotheses. It's also a bit tricky because change needs to be able to handle _'s (Edit: in the implementation a few messages down, the pre-existing function docs4#Lean.MVarId.changeLocalDecl handles the whole revert/change/intro for us, which is great)

Kyle Miller (Mar 13 2023 at 01:02):

@Adam Topaz One problem with using replace is that when there are dependencies it creates a new local variable that shadows the old one.

Kyle Miller (Mar 13 2023 at 01:02):

I think I got an implementation, but it's hacky in a different way:

import Lean

open Lean Elab Tactic Meta

elab_rules : tactic
  | `(tactic| change $newType:term $[$loc:location]?) => do
    let ofNewType : Syntax  `(term| (_ : $newType))
    withLocation (expandOptLocation (Lean.mkOptionalNode loc))
      (atLocal := fun h  do
        let (e, gs)  elabTermWithHoles ofNewType ( h.getType) ( getMainTag)
          (allowNaturalHoles := true)
        liftMetaTactic fun mvarId => do
          guard ( isDefEq e (.fvar h))
          if not ( e.mvarId!.isAssigned) then
            e.mvarId!.assign (.fvar h)
          return ( mvarId.changeLocalDecl h ( inferType e)) :: gs)
      (atTarget := withMainContext do
        let (e, gs)  elabTermWithHoles ofNewType ( getMainTarget) ( getMainTag)
          (allowNaturalHoles := true)
        liftMetaTactic fun mvarId => do
          guard ( isDefEq e (.mvar mvarId))
          if not ( e.mvarId!.isAssigned) then
            e.mvarId!.assign (.mvar mvarId)
          return ( mvarId.change ( inferType e)) :: gs)
      (failed := fun _  throwError "change failed")

example  (n m : Nat)  (p : n + 2 = m  Type) (h : n+2 = m) (x : p h) : n + 2 = m := by
  change n+1+1 = _ at h
  change n+1+1 = _
  exact h

Adam Topaz (Mar 13 2023 at 01:03):

Yeah I know that was a problem with using replace.

Kyle Miller (Mar 13 2023 at 01:03):

The main part of the implementation is mvarId.changeLocalDecl and mvarId.change (and using withLocation to drive things)

Kyle Miller (Mar 13 2023 at 01:04):

I'm using elabTermWithHoles but I found I kept getting defeq errors when there were placeholders if I tried to work with the types directly

Kyle Miller (Mar 13 2023 at 01:05):

The workaround I'm using is to run elabTermWithHoles on (_ : $newType) instead, because somehow this creates the right kind of placeholders(?), but I also need to assign the metavariable directly since isDefEq won't do that when the terms are Props (at least I think that's the issue)

Kyle Miller (Mar 13 2023 at 01:36):

There's a function withAssignableSyntheticOpaque that changes isDefEq to be able to assign to the metavariables that elabTermWithHoles seems to produce, and that appears to be the right thing to do here, giving a cleaner implementation.

import Lean

open Lean Elab Tactic Meta

elab_rules : tactic
  | `(tactic| change $newType:term $[$loc:location]?) => do
    withLocation (expandOptLocation (Lean.mkOptionalNode loc))
      (atLocal := fun h  do
        let hTy  h.getType
        let (e, gs)  elabTermWithHoles newType ( inferType hTy) ( getMainTag)
                        (allowNaturalHoles := true)
        liftMetaTactic fun mvarId  do
          unless  withAssignableSyntheticOpaque (isDefEq e hTy) do
            let h'  h.getUserName
            throwTacticEx `change mvarId
              m!"given type{indentExpr e}\nis not definitionally equal at {h'} to{indentExpr hTy}"
          return ( mvarId.changeLocalDecl h e) :: gs)
      (atTarget := withMainContext do
        let tgt  getMainTarget
        let (e, gs)  elabTermWithHoles newType ( inferType tgt) ( getMainTag)
                        (allowNaturalHoles := true)
        liftMetaTactic fun mvarId  do
          unless  withAssignableSyntheticOpaque (isDefEq e tgt) do
            throwTacticEx `change mvarId
              m!"given type{indentExpr e}\nis not definitionally equal to{indentExpr tgt}"
          return ( mvarId.change e) :: gs)
      (failed := fun _  throwError "change failed")

Kyle Miller (Mar 13 2023 at 01:53):

mathlib4#2836

Kyle Miller (Mar 13 2023 at 23:07):

It turns out elaborating expressions as well as show does is trickier than I'm able to pull off (and show does a very good job at dealing with placeholders). So, for the main goal, it seems you can't do better than evalTactic <| ← `(tactic| show $newType). And then for hypotheses, there's this hack you can use where you elaborate a show expression, copying how the show (and refine) tactics work, but then you throw this expression away and just use some metavariables you get from this. It seems to work fine for creating side-goals with ?_'s too (as unlikely as the use of those may be).

A downside compared to before is that the error messages are a bit confusing since they refer to details of how show ... from ... elaborates.

Here's the code now for reference:

elab_rules : tactic
  | `(tactic| change $newType:term $[$loc:location]?) => do
    withLocation (expandOptLocation (Lean.mkOptionalNode loc))
      (atLocal := fun h  do
        let hTy  h.getType
        -- This is a hack to get the new type to elaborate in the same sort of way that
        -- it would for a `show` expression for the goal.
        let mvar  mkFreshExprMVar none
        let (_, mvars)  elabTermWithHoles
                          ( `(term | show $newType from $( Term.exprToSyntax mvar))) hTy `change
        liftMetaTactic fun mvarId  do
          return ( mvarId.changeLocalDecl h ( inferType mvar)) :: mvars)
      (atTarget := evalTactic <|  `(tactic| show $newType))
      (failed := fun _  throwError "change tactic failed")

Last updated: Dec 20 2023 at 11:08 UTC