Zulip Chat Archive

Stream: new members

Topic: Creating a case analysis lemma for a function


pandaman (Apr 29 2025 at 05:24):

Hi! I'd like to create a lemma to abstract the case analysis of a (non-recursive) function so that I can easily perform the same case analysis in different results (ideally using the cases tactic). For recursive functions, we now have auto-generated .induct, so mimicking the signature allowed me to use the induction tactic with my own induction principle. However, I'm lost how to do something like this for a non-recursive function. Any advice are welcome!

set_option autoImplicit false

inductive Node where
  | done
  | epsilon (next : Nat)

def pushNext (n : Node) (stack : List Nat) : List Nat :=
  match n with
  | .done => stack
  | .epsilon next =>
    if next < 10 then
      next :: stack
    else
      stack

-- A case analysis lemma
theorem pushNext.cases (n : Node) (stack : List Nat)
  (motive : List Nat  Prop)
  (done : n = .done  motive stack)
  (epsilon_pos :  (next : Nat), n = .epsilon next  next < 10  motive (next :: stack))
  (epsilon_neg :  (next : Nat), n = .epsilon next  ¬next < 10  motive stack) :
  motive (pushNext n stack) := by
  unfold pushNext
  split
  next n => exact done rfl
  next n next =>
    split
    next lt => exact epsilon_pos next rfl lt
    next nlt => exact epsilon_neg next rfl nlt

example (n : Node) (stack : List Nat) (h :  i  stack, i < 10) :  i  pushNext n stack, i < 10 := by
  apply pushNext.cases (motive := fun stack =>  i  stack, i < 10)
  -- Cannot introduce variables due to "too many variable names provided"
  case done /-h-/ => sorry
  case epsilon_pos /-next hn lt-/ => sorry
  case epsilon_neg /-next hn nlt-/ => sorry

example (n : Node) (stack : List Nat) (h :  i  stack, i < 10) :  i  pushNext n stack, i < 10 := by
  -- How can I use `cases` here?
  -- cases n, stack using pushNext.cases
  sorry

Yaël Dillies (Apr 29 2025 at 06:14):

You should do induction on n : Node, not on pushNext n stack

pandaman (Apr 29 2025 at 06:30):

That means I need to add if h : next < 10 to the epsilon case every time, which I want to avoid.

pandaman (Apr 29 2025 at 06:34):

Functional induction does case analysis (along the branching of a function) + induction. I want the former for non-recursive functions.

Markus Himmel (Apr 29 2025 at 06:44):

Are you aware of the fun_cases tactic, i.e., fun_cases pushNext n stack?

Markus Himmel (Apr 29 2025 at 06:46):

Your pushNext.cases lemma is quite similar to the autogenerated pushNext.fun_cases (which is created by the same machinery as the .induct lemmas for recursive functions). The fun_cases tactic applies that lemma for you (just like the fun_induction tactic applies the .induct lemma for you).

pandaman (Apr 29 2025 at 06:46):

I'll try it. Thank you!

pandaman (Apr 29 2025 at 07:48):

It worked perfectly. Thank you! I missed we have .fun_cases (I tried .cases and .casesOn and assumed it's not implemented :sweat_smile: )

pandaman (Apr 29 2025 at 07:56):

Now I have something like this

set_option autoImplicit false

inductive Node where
  | done
  | epsilon (next : Nat)

def pushNext (n : Node) (stack : List Nat) : List Nat :=
  match n with
  | .done => stack
  | .epsilon next =>
    if next < 10 then
      next :: stack
    else
      stack

-- Renaming branches
theorem pushNext.fun_cases' (motive : Node  List Nat  Prop)
  (done :  (stack : List Nat), motive Node.done stack)
  (epsilon_pos :  (stack : List Nat) (next : Nat), next < 10  motive (Node.epsilon next) stack)
  (epsilon_neg :  (stack : List Nat) (next : Nat), ¬next < 10  motive (Node.epsilon next) stack) :
   (n : Node) (stack : List Nat), motive n stack :=
  pushNext.fun_cases motive done epsilon_pos epsilon_neg

example (n : Node) (stack : List Nat) (h :  i  stack, i < 10) :  i  pushNext n stack, i < 10 := by
  cases n, stack using pushNext.fun_cases'
  case done =>
    simpa [pushNext] using h
  case epsilon_pos next lt =>
    simpa [pushNext, lt] using h
  case epsilon_neg next nlt =>
    simpa [pushNext, nlt] using h

pandaman (May 02 2025 at 11:36):

In my real code, I have a few fixed parameters I want to exclude from the motive, but it seems the auto-generated .fun_cases treat all arguments as indices. I tried to create a custom .fun_cases, but I couldn't use it with the cases tactic. Is my lemma malformed somehow?

set_option autoImplicit false

inductive Node where
  | done
  | epsilon (next : Nat)

def pushNext (param : Nat) (n : Node) (stack : List Nat) : List Nat :=
  match n with
  | .done => stack
  | .epsilon next =>
    if next < param then
      next :: stack
    else
      stack

#check pushNext.fun_cases -- gives `motive : Nat → Node → List Nat → Prop`

theorem pushNext.fun_cases' (param : Nat) (motive : Node  List Nat  Prop)
  (done :  (stack : List Nat), motive Node.done stack)
  (epsilon_pos :  (stack : List Nat) (next : Nat), next < param  motive (Node.epsilon next) stack)
  (epsilon_neg :  (stack : List Nat) (next : Nat), ¬next < param  motive (Node.epsilon next) stack) :
   (n : Node) (stack : List Nat), motive n stack := by
  intro n stack
  match n with
  | .done => exact done stack
  | .epsilon next =>
    if lt : next < param then
      exact epsilon_pos stack next lt
    else
      exact epsilon_neg stack next lt

example (n : Node) (stack : List Nat) (h :  i  stack, i < 10) :  i  pushNext 10 n stack, i < 10 := by
  -- invalid number of targets #2, motive expects #0
  cases n, stack using pushNext.fun_cases' 10
  case done =>
    simpa [pushNext] using h
  case epsilon_pos next lt =>
    simpa [pushNext, lt] using h
  case epsilon_neg next nlt =>
    simpa [pushNext, nlt] using h

example (n : Node) (stack : List Nat) (h :  i  stack, i < 10) :  i  pushNext 10 n stack, i < 10 := by
  -- Need to manually revert
  revert n stack
  apply pushNext.fun_cases' 10
  -- Cannot introduce variables in `case done =>` due to "too many variable names provided"
  case done =>
    intro stack h i mem
    simp [pushNext] at mem
    exact h i mem
  case epsilon_pos =>
    intro stack next lt h i mem
    simp [pushNext, lt] at mem
    cases mem with
    | inl eq => simp [eq, lt]
    | inr mem => exact h i mem
  case epsilon_neg =>
    intro stack next nlt h i mem
    simp [pushNext, nlt] at mem
    exact h i mem

pandaman (May 02 2025 at 11:37):

Hmm. I could do this. The real example would have 5 parameters so it might look ugly, but it may work.

example (n : Node) (stack : List Nat) (h :  i  stack, i < 10) :  i  pushNext 10 n stack, i < 10 := by
  cases h : 10, n, stack using pushNext.fun_cases
  case case1 param =>
    -- h: 10 = param
    sorry
  case case2 param next lt =>
    sorry
  case case3 param next nlt =>
    sorry

pandaman (May 02 2025 at 12:03):

It couldn't type check in more involved cases like this :cry:

set_option autoImplicit false

structure Params where
  T : Type
  limit : Nat
  ofNat : Nat  T

inductive Node where
  | done
  | epsilon (next : Nat)

def pushNext (params : Params) (n : Node) (stack : List (params.T × Nat)) : List (params.T × Nat) :=
  match n with
  | .done => stack
  | .epsilon next =>
    if next < params.limit then
      (params.ofNat next, next) :: stack
    else
      stack

#check pushNext.fun_cases -- gives `motive : Nat → Node → List Nat → Prop`

def P1 : Params := {
  T := Nat
  limit := 10
  ofNat := id
}

example (n : Node) (stack : List (Nat × Nat)) (h :  p  stack, p.2 < 10) :  p  pushNext P1 n stack, p.2 < 10 := by
  --tactic 'generalize' failed, result is not type correct
  -- ∀ (x : Params) (p : x.T × Nat), p ∈ pushNext x n stack → p.snd < 10
  cases h : P1, n, stack using pushNext.fun_cases
  case case1 param =>
    -- h: 10 = param
    sorry
  case case2 param next lt =>
    sorry
  case case3 param next nlt =>
    sorry

pandaman (May 02 2025 at 12:06):

We can make it work if we treat Params as a parameter, so it would be really great if we can make the following fun_cases' work with cases.

-- push params to parameter
theorem pushNext.fun_cases' (params : Params) (motive : Node  List (params.T × Nat)  Prop)
  (done :  (stack : List (params.T × Nat)), motive Node.done stack)
  (epsilon_pos :  (stack : List (params.T × Nat)) (next : Nat), next < params.limit  motive (Node.epsilon next) stack)
  (epsilon_neg :  (stack : List (params.T × Nat)) (next : Nat), ¬next < params.limit  motive (Node.epsilon next) stack) :
   (n : Node) (stack : List (params.T × Nat)), motive n stack := by
  intro n stack
  match n with
  | .done => exact done stack
  | .epsilon next =>
    if lt : next < params.limit then
      exact epsilon_pos stack next lt
    else
      exact epsilon_neg stack next lt

example (n : Node) (stack : List (P1.T × Nat)) (h :  p  stack, p.2 < 10) :  p  pushNext P1 n stack, p.2 < 10 := by
  -- invalid number of targets #2, motive expects #0
  cases n, stack using pushNext.fun_cases' P1
  sorry

example (n : Node) (stack : List (P1.T × Nat)) (h :  p  stack, p.2 < 10) :  p  pushNext P1 n stack, p.2 < 10 := by
  -- Need to manually revert
  revert n stack
  apply pushNext.fun_cases' P1
  -- Cannot introduce variables in `case done =>` due to "too many variable names provided"
  case done =>
    intro stack h i mem
    simp [pushNext] at mem
    exact h i mem
  case epsilon_pos =>
    intro stack next lt h i mem
    simp [pushNext, lt] at mem
    cases mem with
    | inl eq =>
      simp [P1] at lt
      simp [eq, lt]
    | inr mem => exact h i mem
  case epsilon_neg =>
    intro stack next nlt h i mem
    simp [pushNext, nlt] at mem
    exact h i mem

Robin Arnez (May 02 2025 at 12:23):

induction seems to work in your original example, not sure what's wrong with cases. This seems like a bug though.

Robin Arnez (May 02 2025 at 12:25):

Actually, you need to make motive implicit and then cases works as well

pandaman (May 02 2025 at 12:28):

Actually, you need to make motive implicit and then cases works as well

Interesting, thanks!


Last updated: Dec 20 2025 at 21:32 UTC