Zulip Chat Archive

Stream: general

Topic: syntactic unification of propositional formulas


Paige Thomas (Aug 10 2025 at 16:29):

I have been trying to find, implement and verify a syntactic unification algorithm for formulas in propositional logic. The simplest that I have found so far is an adaption of Robinson's algorithm, and below is my attempt to code it in Lean. A couple of experiments with evaluating it shows that it may be correct. I'm not sure how to prove that it terminates though, even on paper. I'm wondering if anyone might know how to do so, or may have any other advice on implementing a syntactic unification algorithm for formulas in propositional logic that is efficient enough for practical purposes and not too difficult to program and prove correct.

import Mathlib.Tactic


set_option autoImplicit false



def Function.updateITE
  {α β : Type}
  [DecidableEq α]
  (f : α  β)
  (a : α)
  (b : β)
  (c : α) :
  β :=
  if c = a then b else f c


  inductive Formula_ : Type
  | false_ : Formula_
  | true_ : Formula_
  | atom_ : String  Formula_
  | not_ : Formula_  Formula_
  | and_ : Formula_  Formula_  Formula_
  | or_ : Formula_  Formula_  Formula_
  | imp_ : Formula_  Formula_  Formula_
  | iff_ : Formula_  Formula_  Formula_
  deriving Inhabited, DecidableEq, Hashable, Repr

compile_inductive% Formula_


open Formula_


/--
  `atom_occurs_in A F` := True if and only if there is an occurrence of the atom `A` in the formula `F`.
-/
def atom_occurs_in
  (A : String) :
  Formula_  Prop
  | false_ => False
  | true_ => False
  | atom_ X => A = X
  | not_ phi => atom_occurs_in A phi
  | and_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | or_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | imp_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | iff_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi

instance
  (A : String)
  (F : Formula_) :
  Decidable (atom_occurs_in A F) :=
  by
  induction F
  all_goals
    unfold atom_occurs_in
    infer_instance


/--
  `Formula_.atom_list F` := The list of all of the atoms that have an occurrence in the formula `F`.
-/
def Formula_.atom_list :
  Formula_  List String
  | false_ => []
  | true_ => []
  | atom_ X => [X]
  | not_ phi => phi.atom_list
  | and_ phi psi => phi.atom_list ++ psi.atom_list
  | or_ phi psi => phi.atom_list ++ psi.atom_list
  | imp_ phi psi => phi.atom_list ++ psi.atom_list
  | iff_ phi psi => phi.atom_list ++ psi.atom_list


/--
  `replace_atom_all_rec τ F` := The simultaneous replacement of each occurrence of any atom `A` in the formula `F` by `τ A`.
-/
def replace_atom_all_rec
  (τ : String  Formula_) :
  Formula_  Formula_
  | false_ => false_
  | true_ => true_
  | atom_ X => τ X
  | not_ phi => not_ (replace_atom_all_rec τ phi)
  | and_ phi psi => and_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | or_ phi psi => or_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | imp_ phi psi => imp_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | iff_ phi psi => iff_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)


structure Equation : Type where
  (lhs : Formula_)
  (rhs : Formula_)
  deriving Inhabited, DecidableEq, Repr

def Equation.atom_list
  (E : Equation) :
  List String :=
  E.lhs.atom_list  E.rhs.atom_list


partial
def unify
  (E : Equation) :
  Option (String  Formula_) :=
  match E with
  | false_, false_
  | true_, true_ => Option.some atom_
  | atom_ X, F =>
    if atom_ X = F
    then Option.some atom_
    else
      if atom_occurs_in X F
      then Option.none
      else Option.some (Function.updateITE atom_ X F)
  | F, atom_ X =>
    if F = atom_ X
    then Option.some atom_
    else
      if atom_occurs_in X F
      then Option.none
      else Option.some (Function.updateITE atom_ X F)
  | not_ phi, not_ phi' => unify phi, phi'
  | and_ phi psi, and_ phi' psi'
  | or_ phi psi, or_ phi' psi'
  | imp_ phi psi, imp_ phi' psi'
  | iff_ phi psi, iff_ phi' psi' =>
    match unify phi, phi' with
    | Option.some σ_1 =>
      match unify replace_atom_all_rec σ_1 psi, replace_atom_all_rec σ_1 psi' with
      | Option.some σ_2 => Option.some ((replace_atom_all_rec σ_2)  σ_1)
      | Option.none => Option.none
    | Option.none => Option.none
  | _ => Option.none


def print_unify
  (E : Equation) :
  Option (String  Formula_)  Option (List (Formula_ × Formula_))
  | Option.none => Option.none
  | Option.some σ => Option.some (List.map (fun (X : String) => (atom_ X, σ X)) E.atom_list)


#eval! let E : Equation := atom_ "P", atom_ "Q"⟩; print_unify E (unify E)

#eval! let E : Equation := atom_ "X", not_ (atom_ "X")⟩; print_unify E (unify E)

#eval! let E : Equation := and_ (atom_ "X") (atom_ "Y"), and_ (atom_ "Y") (atom_ "Z")⟩; print_unify E (unify E)

Alfredo Moreira-Rosa (Aug 10 2025 at 19:26):

I would base my tree on an existing tree structure already implemented in lean/mathlib.

https://leanprover-community.github.io/mathlib4_docs/Mathlib/Data/Tree/Basic.html

Alexander Bentkamp (Aug 10 2025 at 19:38):

For a paper proof, have a look here: https://en.wikipedia.org/wiki/Unification_%28computer_science%29 (Section: Proof of termination)

Paige Thomas (Aug 11 2025 at 03:22):

ecyrbe said:

I would base my tree on an existing tree structure already implemented in lean/mathlib.

https://leanprover-community.github.io/mathlib4_docs/Mathlib/Data/Tree/Basic.html

I'm sorry, which tree are you referring to?

Paige Thomas (Aug 11 2025 at 05:22):

Alexander Bentkamp said:

For a paper proof, have a look here: https://en.wikipedia.org/wiki/Unification_%28computer_science%29 (Section: Proof of termination)

I guess I'm not entirely certain how to apply that proof to this particular algorithm.

Paige Thomas (Aug 11 2025 at 05:30):

Something starting along these lines?

import Mathlib.Tactic


set_option autoImplicit false



def Function.updateITE
  {α β : Type}
  [DecidableEq α]
  (f : α  β)
  (a : α)
  (b : β)
  (c : α) :
  β :=
  if c = a then b else f c


  inductive Formula_ : Type
  | false_ : Formula_
  | true_ : Formula_
  | atom_ : String  Formula_
  | not_ : Formula_  Formula_
  | and_ : Formula_  Formula_  Formula_
  | or_ : Formula_  Formula_  Formula_
  | imp_ : Formula_  Formula_  Formula_
  | iff_ : Formula_  Formula_  Formula_
  deriving Inhabited, DecidableEq, Hashable, Repr

compile_inductive% Formula_


open Formula_


/--
  `Formula_.size F` := The number of subformulas in the formula `F`.
-/
def Formula_.size :
  Formula_  Nat
  | false_ => 1
  | true_ => 1
  | atom_ _ => 1
  | not_ phi => phi.size + 1
  | and_ phi psi => phi.size + psi.size + 1
  | or_ phi psi => phi.size + psi.size + 1
  | imp_ phi psi => phi.size + psi.size + 1
  | iff_ phi psi => phi.size + psi.size + 1


/--
  `atom_occurs_in A F` := True if and only if there is an occurrence of the atom `A` in the formula `F`.
-/
def atom_occurs_in
  (A : String) :
  Formula_  Prop
  | false_ => False
  | true_ => False
  | atom_ X => A = X
  | not_ phi => atom_occurs_in A phi
  | and_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | or_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | imp_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | iff_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi

instance
  (A : String)
  (F : Formula_) :
  Decidable (atom_occurs_in A F) :=
  by
  induction F
  all_goals
    unfold atom_occurs_in
    infer_instance


/--
  `Formula_.atom_list F` := The list of all of the atoms that have an occurrence in the formula `F`.
-/
def Formula_.atom_list :
  Formula_  List String
  | false_ => []
  | true_ => []
  | atom_ X => [X]
  | not_ phi => phi.atom_list
  | and_ phi psi => phi.atom_list ++ psi.atom_list
  | or_ phi psi => phi.atom_list ++ psi.atom_list
  | imp_ phi psi => phi.atom_list ++ psi.atom_list
  | iff_ phi psi => phi.atom_list ++ psi.atom_list


/--
  `replace_atom_all_rec τ F` := The simultaneous replacement of each occurrence of any atom `A` in the formula `F` by `τ A`.
-/
def replace_atom_all_rec
  (τ : String  Formula_) :
  Formula_  Formula_
  | false_ => false_
  | true_ => true_
  | atom_ X => τ X
  | not_ phi => not_ (replace_atom_all_rec τ phi)
  | and_ phi psi => and_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | or_ phi psi => or_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | imp_ phi psi => imp_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | iff_ phi psi => iff_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)


structure Equation : Type where
  (lhs : Formula_)
  (rhs : Formula_)
  deriving Inhabited, DecidableEq, Repr

def Equation.atom_list
  (E : Equation) :
  List String :=
  E.lhs.atom_list  E.rhs.atom_list


/--
  `List.dup_count_aux acc L` := Helper function for `List.dup_count`.
-/
def List.dup_count_aux
  {α : Type}
  [DecidableEq α]
  (acc : Finset α) :
  List α  Nat
  | [] => 0
  | hd :: tl =>
    if hd  tl  hd  acc
    then 1 + List.dup_count_aux (acc  {hd}) tl
    else List.dup_count_aux acc tl

/--
  `List.dup_count L` := The number of elements that occur more than once in the list `L`.
-/
def List.dup_count
  {α : Type}
  [DecidableEq α]
  (L : List α) :
  Nat :=
  List.dup_count_aux {} L

#eval [0].dup_count
#eval [0, 0].dup_count
#eval [0, 0, 0].dup_count
#eval [0, 0, 1].dup_count
#eval [0, 0, 1, 1].dup_count
#eval [1, 0, 0, 1].dup_count
#eval [0, 1, 0, 1].dup_count


--partial
def unify
  (E : Equation) :
  Option (String  Formula_) :=
  match E with
  | false_, false_
  | true_, true_ => Option.some atom_
  | atom_ X, F =>
    if atom_ X = F
    then Option.some atom_
    else
      if atom_occurs_in X F
      then Option.none
      else Option.some (Function.updateITE atom_ X F)
  | F, atom_ X =>
    if F = atom_ X
    then Option.some atom_
    else
      if atom_occurs_in X F
      then Option.none
      else Option.some (Function.updateITE atom_ X F)
  | not_ phi, not_ phi' => unify phi, phi'
  | and_ phi psi, and_ phi' psi'
  | or_ phi psi, or_ phi' psi'
  | imp_ phi psi, imp_ phi' psi'
  | iff_ phi psi, iff_ phi' psi' =>
    match unify phi, phi' with
    | Option.some σ_1 =>
      match unify replace_atom_all_rec σ_1 psi, replace_atom_all_rec σ_1 psi' with
      | Option.some σ_2 => Option.some ((replace_atom_all_rec σ_2)  σ_1)
      | Option.none => Option.none
    | Option.none => Option.none
  | _ => Option.none
  termination_by (E.atom_list.dup_count, E.lhs.size + E.rhs.size)
  decreasing_by
  all_goals
    sorry


def print_unify
  (E : Equation) :
  Option (String  Formula_)  Option (List (Formula_ × Formula_))
  | Option.none => Option.none
  | Option.some σ => Option.some (List.map (fun (X : String) => (atom_ X, σ X)) E.atom_list)


#eval! let E : Equation := atom_ "P", atom_ "Q"⟩; print_unify E (unify E)

#eval! let E : Equation := atom_ "X", not_ (atom_ "X")⟩; print_unify E (unify E)

#eval! let E : Equation := and_ (atom_ "X") (atom_ "Y"), and_ (atom_ "Y") (atom_ "Z")⟩; print_unify E (unify E)

Paige Thomas (Aug 11 2025 at 06:07):

This is an attempt at formalizing the algorithm in the Wikipedia article. I'm not so certain I did this right though, especially where I put in the match unify Γ with.

import Mathlib.Tactic


set_option autoImplicit false



def Function.updateITE
  {α β : Type}
  [DecidableEq α]
  (f : α  β)
  (a : α)
  (b : β)
  (c : α) :
  β :=
  if c = a then b else f c


  inductive Formula_ : Type
  | false_ : Formula_
  | true_ : Formula_
  | atom_ : String  Formula_
  | not_ : Formula_  Formula_
  | and_ : Formula_  Formula_  Formula_
  | or_ : Formula_  Formula_  Formula_
  | imp_ : Formula_  Formula_  Formula_
  | iff_ : Formula_  Formula_  Formula_
  deriving Inhabited, DecidableEq, Hashable, Repr

compile_inductive% Formula_


open Formula_


/--
  `Formula_.size F` := The number of subformulas in the formula `F`.
-/
def Formula_.size :
  Formula_  Nat
  | false_ => 1
  | true_ => 1
  | atom_ _ => 1
  | not_ phi => phi.size + 1
  | and_ phi psi => phi.size + psi.size + 1
  | or_ phi psi => phi.size + psi.size + 1
  | imp_ phi psi => phi.size + psi.size + 1
  | iff_ phi psi => phi.size + psi.size + 1


/--
  `atom_occurs_in A F` := True if and only if there is an occurrence of the atom `A` in the formula `F`.
-/
def atom_occurs_in
  (A : String) :
  Formula_  Prop
  | false_ => False
  | true_ => False
  | atom_ X => A = X
  | not_ phi => atom_occurs_in A phi
  | and_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | or_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | imp_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | iff_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi

instance
  (A : String)
  (F : Formula_) :
  Decidable (atom_occurs_in A F) :=
  by
  induction F
  all_goals
    unfold atom_occurs_in
    infer_instance


/--
  `Formula_.atom_list F` := The list of all of the atoms that have an occurrence in the formula `F`.
-/
def Formula_.atom_list :
  Formula_  List String
  | false_ => []
  | true_ => []
  | atom_ X => [X]
  | not_ phi => phi.atom_list
  | and_ phi psi => phi.atom_list ++ psi.atom_list
  | or_ phi psi => phi.atom_list ++ psi.atom_list
  | imp_ phi psi => phi.atom_list ++ psi.atom_list
  | iff_ phi psi => phi.atom_list ++ psi.atom_list


/--
  `replace_atom_one_rec A F P` :=

  `A → F` in `P` for each occurrence of the atom `A` in the formula `P`

  The result of simultaneously replacing each occurrence of the atom `A` in the formula `P` by an occurrence of the formula `F`.
-/
def replace_atom_one_rec
  (A : String)
  (F : Formula_ ) :
  Formula_  Formula_
  | false_ => false_
  | true_ => true_
  | atom_ X => if A = X then F else atom_ X
  | not_ phi => not_ (replace_atom_one_rec A F phi)
  | and_ phi psi => and_ (replace_atom_one_rec A F phi) (replace_atom_one_rec A F psi)
  | or_ phi psi => or_ (replace_atom_one_rec A F phi) (replace_atom_one_rec A F psi)
  | imp_ phi psi => imp_ (replace_atom_one_rec A F phi) (replace_atom_one_rec A F psi)
  | iff_ phi psi => iff_ (replace_atom_one_rec A F phi) (replace_atom_one_rec A F psi)


/--
  `replace_atom_all_rec τ F` := The simultaneous replacement of each occurrence of any atom `A` in the formula `F` by `τ A`.
-/
def replace_atom_all_rec
  (τ : String  Formula_) :
  Formula_  Formula_
  | false_ => false_
  | true_ => true_
  | atom_ X => τ X
  | not_ phi => not_ (replace_atom_all_rec τ phi)
  | and_ phi psi => and_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | or_ phi psi => or_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | imp_ phi psi => imp_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | iff_ phi psi => iff_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)


structure Equation : Type where
  (lhs : Formula_)
  (rhs : Formula_)
  deriving Inhabited, DecidableEq, Repr

def Equation.atom_list
  (E : Equation) :
  List String :=
  E.lhs.atom_list  E.rhs.atom_list


/--
  `List.dup_count_aux acc L` := Helper function for `List.dup_count`.
-/
def List.dup_count_aux
  {α : Type}
  [DecidableEq α]
  (acc : Finset α) :
  List α  Nat
  | [] => 0
  | hd :: tl =>
    if hd  tl  hd  acc
    then 1 + List.dup_count_aux (acc  {hd}) tl
    else List.dup_count_aux acc tl

/--
  `List.dup_count L` := The number of elements that occur more than once in the list `L`.
-/
def List.dup_count
  {α : Type}
  [DecidableEq α]
  (L : List α) :
  Nat :=
  List.dup_count_aux {} L

#eval [0].dup_count
#eval [0, 0].dup_count
#eval [0, 0, 0].dup_count
#eval [0, 0, 1].dup_count
#eval [0, 0, 1, 1].dup_count
#eval [1, 0, 0, 1].dup_count
#eval [0, 1, 0, 1].dup_count


def var_elim
  (X : String)
  (F : Formula_)
  (L : List Equation) :
  List Equation :=
  L.map (fun (E : Equation) => replace_atom_one_rec X F E.lhs, replace_atom_one_rec X F E.rhs)


partial
def unify :
  List Equation  Option (List Equation)
  | [] => Option.some []
  | false_, false_ :: Γ => unify Γ
  | true_, true_ :: Γ => unify Γ
  | atom_ X, F :: Γ =>
    if atom_ X = F
    then unify Γ
    else
      if atom_occurs_in X F
      then Option.none
      else
        if  (E : Equation), E  Γ  (atom_occurs_in X E.lhs  atom_occurs_in X E.rhs)
        then unify (atom_ X, F :: var_elim X F Γ)
        else
          match unify Γ with
          | Option.none => Option.none
          | Option.some Γ' => Option.some (atom_ X, F :: Γ')
  | F, atom_ X :: Γ => unify (atom_ X, F :: Γ)
  | not_ phi, not_ phi' :: Γ =>
      unify (phi, phi' :: Γ)
  | and_ phi psi, and_ phi' psi' :: Γ
  | or_ phi psi, or_ phi' psi' :: Γ
  | imp_ phi psi, imp_ phi' psi' :: Γ
  | iff_ phi psi, iff_ phi' psi' :: Γ =>
      unify (phi, phi' :: psi, psi' :: Γ)
  | _ => Option.none


#eval! unify [atom_ "P", atom_ "Q"]

#eval! unify [atom_ "X", not_ (atom_ "X")]

#eval! unify [and_ (atom_ "X") (atom_ "Y"), and_ (atom_ "Y") (atom_ "Z")]

Alexander Bentkamp (Aug 11 2025 at 16:55):

Maybe you should deviate a bit from the wikipedia article and handle the atom case like this:

  | atom_ X, F :: Γ =>
    if atom_ X = F
    then unify Γ
    else
      if atom_occurs_in X F
      then Option.none
      else
        match unify (var_elim X F Γ) with
        | Option.none => Option.none
        | Option.some Γ' => Option.some (atom_ X, F :: Γ')

and then use the following measure:

  • number of distinct variables occurring in the given equations
  • sum of the sizes of the given equations

Alexander Bentkamp (Aug 11 2025 at 17:04):

But the result you get is not a standard (simultaneous) substitution. You would have to convert if you want one.

Alexander Bentkamp (Aug 11 2025 at 17:05):

The result substitution you get must be applied in sequence left to right.

Paige Thomas (Aug 13 2025 at 02:29):

Hmm. Does changing the code make it easier to prove termination?

Paige Thomas (Aug 13 2025 at 05:01):

Actually, it looks like that code may be wrong as I have it:

#eval! unify [or_ (and_ (atom_ "X") (atom_ "Y")) (atom_ "Z"), or_ (and_ (atom_ "Y") (atom_ "Z")) (atom_ "X")]

gives

some [{ lhs := Formula_.atom_ "X", rhs := Formula_.atom_ "Y" },
 { lhs := Formula_.atom_ "Y", rhs := Formula_.atom_ "Z" }]

which isn't right.

Paige Thomas (Aug 13 2025 at 05:24):

at least not for a simultaneous substitution. hmm.

Paige Thomas (Aug 13 2025 at 05:26):

I see, that is what you already pointed out. I was assuming that was just the case for the changed code.

Paige Thomas (Aug 13 2025 at 05:29):

It would probably be preferable to prove the single equation one:

partial
def unify
  (E : Equation) :
  Option (String  Formula_) :=
  match E with
  | false_, false_
  | true_, true_ => Option.some atom_
  | atom_ X, F =>
    if atom_ X = F
    then Option.some atom_
    else
      if atom_occurs_in X F
      then Option.none
      else Option.some (Function.updateITE atom_ X F)
  | F, atom_ X =>
    if F = atom_ X
    then Option.some atom_
    else
      if atom_occurs_in X F
      then Option.none
      else Option.some (Function.updateITE atom_ X F)
  | not_ phi, not_ phi' => unify phi, phi'
  | and_ phi psi, and_ phi' psi'
  | or_ phi psi, or_ phi' psi'
  | imp_ phi psi, imp_ phi' psi'
  | iff_ phi psi, iff_ phi' psi' => do
    let σ_1  unify phi, phi'
    let σ_2  unify replace_atom_all_rec σ_1 psi, replace_atom_all_rec σ_1 psi'
    (replace_atom_all_rec σ_2)  σ_1
  | _ => Option.none

Alexander Bentkamp (Aug 13 2025 at 08:45):

Yes, I think my suggestion would make it easier to prove termination.

If you insist on your original implementation, you can probably prove termination using this measure:

  • number of distinct variables in E
  • syntactic size of E

But you would have to simultaneously prove that the substitutions that unify E returns have the property that all nonidentity values of the substitution contain only variables from E and do not contain the mapped variable itself.

I am not sure how you can prove such a property in Lean while you still have to prove termination. Maybe dependent types are the only option here.

Alexander Bentkamp (Aug 13 2025 at 13:12):

Actually, here is another idea, similar to what I proposed, but returning simultaneous substitutions:

partial
def unify :
  List Equation  Option (String  Formula_)
  | [] => Option.some atom_
  | false_, false_ :: Γ => unify Γ
  | true_, true_ :: Γ => unify Γ
  | atom_ X, F :: Γ =>
    if atom_ X = F
    then unify Γ
    else
      if atom_occurs_in X F
      then Option.none
      else
        match unify (var_elim X F Γ) with
        | Option.none => Option.none
        | Option.some τ => Option.some (Function.updateITE τ X (replace_atom_all_rec τ F))
  | F, atom_ X :: Γ => unify (atom_ X, F :: Γ)
  | not_ phi, not_ phi' :: Γ =>
      unify (phi, phi' :: Γ)
  | and_ phi psi, and_ phi' psi' :: Γ
  | or_ phi psi, or_ phi' psi' :: Γ
  | imp_ phi psi, imp_ phi' psi' :: Γ
  | iff_ phi psi, iff_ phi' psi' :: Γ =>
      unify (phi, phi' :: psi, psi' :: Γ)
  | _ => Option.none

Alexander Bentkamp (Aug 13 2025 at 13:13):

You should be able to prove this terminating using:

  • number of distinct variables occurring in the given equations
  • sum of the sizes of the given equations

Paige Thomas (Aug 14 2025 at 03:50):

Do you mean something like

def equation_list_atom_list
  (L : List Equation) :
  List String :=
    List.foldr (fun (next : Equation) (prev : List String) => next.lhs.atom_list ++ next.rhs.atom_list ++ prev) [] L

#eval equation_list_atom_list []
#eval equation_list_atom_list [atom_ "P", atom_ "Q"]
#eval equation_list_atom_list [atom_ "P", atom_ "Q", atom_ "Q", atom_ "R"]
#eval equation_list_atom_list [atom_ "P", atom_ "Q", atom_ "R", atom_ "S"]


def equation_list_size
  (L : List Equation) :
  Nat :=
  List.foldr (fun (next : Equation) (prev : Nat) => next.lhs.size + next.rhs.size + prev) 0 L


def unify :
  List Equation  Option (String  Formula_)
  | [] => Option.some atom_
  | false_, false_ :: Γ
  | true_, true_ :: Γ => unify Γ
  | atom_ X, F :: Γ =>
    if atom_ X = F
    then unify Γ
    else
      if atom_occurs_in X F
      then Option.none
      else
        match unify (var_elim X F Γ) with
        | Option.none => Option.none
        | Option.some τ => Option.some (Function.updateITE τ X (replace_atom_all_rec τ F))
  | F, atom_ X :: Γ => unify (atom_ X, F :: Γ)
  | not_ phi, not_ phi' :: Γ =>
      unify (phi, phi' :: Γ)
  | and_ phi psi, and_ phi' psi' :: Γ
  | or_ phi psi, or_ phi' psi' :: Γ
  | imp_ phi psi, imp_ phi' psi' :: Γ
  | iff_ phi psi, iff_ phi' psi' :: Γ =>
      unify (phi, phi' :: psi, psi' :: Γ)
  | _ => Option.none
  termination_by L => ((equation_list_atom_list L).dedup.length, equation_list_size L)
  decreasing_by
  all_goals
    sorry

?

Alexander Bentkamp (Aug 14 2025 at 06:11):

Yes! However, instead of creating a list of atoms and then calling dedup, it will probably easier to prove things if you use a Finset instead of a List to collect the atoms. Then dedup will happen automatically.

Paige Thomas (Aug 15 2025 at 04:05):

It seems that it is only looking at the first of the pair of terms being used to show termination? At least, it won't let me apply Prod.Lex.right in the first goal of the decreasing_by. Is there another theorem that would allow me to use the second term? Or another approach?

import Mathlib.Tactic


set_option autoImplicit false



def Function.updateITE
  {α β : Type}
  [DecidableEq α]
  (f : α  β)
  (a : α)
  (b : β)
  (c : α) :
  β :=
  if c = a then b else f c


  inductive Formula_ : Type
  | false_ : Formula_
  | true_ : Formula_
  | atom_ : String  Formula_
  | not_ : Formula_  Formula_
  | and_ : Formula_  Formula_  Formula_
  | or_ : Formula_  Formula_  Formula_
  | imp_ : Formula_  Formula_  Formula_
  | iff_ : Formula_  Formula_  Formula_
  deriving Inhabited, DecidableEq, Hashable, Repr

compile_inductive% Formula_


open Formula_


/--
  `Formula_.size F` := The number of subformulas in the formula `F`.
-/
def Formula_.size :
  Formula_  Nat
  | false_ => 1
  | true_ => 1
  | atom_ _ => 1
  | not_ phi => phi.size + 1
  | and_ phi psi => phi.size + psi.size + 1
  | or_ phi psi => phi.size + psi.size + 1
  | imp_ phi psi => phi.size + psi.size + 1
  | iff_ phi psi => phi.size + psi.size + 1


/--
  `atom_occurs_in A F` := True if and only if there is an occurrence of the atom `A` in the formula `F`.
-/
def atom_occurs_in
  (A : String) :
  Formula_  Prop
  | false_ => False
  | true_ => False
  | atom_ X => A = X
  | not_ phi => atom_occurs_in A phi
  | and_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | or_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | imp_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi
  | iff_ phi psi => atom_occurs_in A phi  atom_occurs_in A psi

instance
  (A : String)
  (F : Formula_) :
  Decidable (atom_occurs_in A F) :=
  by
  induction F
  all_goals
    unfold atom_occurs_in
    infer_instance


/--
  `Formula_.atom_set F` := The set of all of the atoms that have an occurrence in the formula `F`.
-/
def Formula_.atom_set :
  Formula_  Finset String
  | false_ => 
  | true_ => 
  | atom_ X => {X}
  | not_ phi => phi.atom_set
  | and_ phi psi => phi.atom_set  psi.atom_set
  | or_ phi psi => phi.atom_set  psi.atom_set
  | imp_ phi psi => phi.atom_set  psi.atom_set
  | iff_ phi psi => phi.atom_set  psi.atom_set


/--
  `replace_atom_one_rec A F P` :=

  `A → F` in `P` for each occurrence of the atom `A` in the formula `P`

  The result of simultaneously replacing each occurrence of the atom `A` in the formula `P` by an occurrence of the formula `F`.
-/
def replace_atom_one_rec
  (A : String)
  (F : Formula_ ) :
  Formula_  Formula_
  | false_ => false_
  | true_ => true_
  | atom_ X => if A = X then F else atom_ X
  | not_ phi => not_ (replace_atom_one_rec A F phi)
  | and_ phi psi => and_ (replace_atom_one_rec A F phi) (replace_atom_one_rec A F psi)
  | or_ phi psi => or_ (replace_atom_one_rec A F phi) (replace_atom_one_rec A F psi)
  | imp_ phi psi => imp_ (replace_atom_one_rec A F phi) (replace_atom_one_rec A F psi)
  | iff_ phi psi => iff_ (replace_atom_one_rec A F phi) (replace_atom_one_rec A F psi)


/--
  `replace_atom_all_rec τ F` := The simultaneous replacement of each occurrence of any atom `A` in the formula `F` by `τ A`.
-/
def replace_atom_all_rec
  (τ : String  Formula_) :
  Formula_  Formula_
  | false_ => false_
  | true_ => true_
  | atom_ X => τ X
  | not_ phi => not_ (replace_atom_all_rec τ phi)
  | and_ phi psi => and_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | or_ phi psi => or_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | imp_ phi psi => imp_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)
  | iff_ phi psi => iff_ (replace_atom_all_rec τ phi) (replace_atom_all_rec τ psi)


structure Equation : Type where
  (lhs : Formula_)
  (rhs : Formula_)
  deriving Inhabited, DecidableEq, Repr


def var_elim
  (X : String)
  (F : Formula_)
  (L : List Equation) :
  List Equation :=
  L.map (fun (E : Equation) => replace_atom_one_rec X F E.lhs, replace_atom_one_rec X F E.rhs)


def equation_list_atom_set
  (L : List Equation) :
  Finset String :=
    List.foldr (fun (next : Equation) (prev : Finset String) => next.lhs.atom_set  next.rhs.atom_set  prev) {} L

#eval equation_list_atom_set {}
#eval equation_list_atom_set [atom_ "P", atom_ "Q"]
#eval equation_list_atom_set [atom_ "P", atom_ "Q", atom_ "Q", atom_ "R"]
#eval equation_list_atom_set [atom_ "P", atom_ "Q", atom_ "R", atom_ "S"]


def equation_list_size
  (L : List Equation) :
  Nat :=
  List.foldr (fun (next : Equation) (prev : Nat) => next.lhs.size + next.rhs.size + prev) 0 L


def unify :
  List Equation  Option (String  Formula_)
  | [] => Option.some atom_
  | false_, false_ :: Γ
  | true_, true_ :: Γ => unify Γ
  | atom_ X, F :: Γ =>
    if atom_ X = F
    then unify Γ
    else
      if atom_occurs_in X F
      then Option.none
      else
        match unify (var_elim X F Γ) with
        | Option.none => Option.none
        | Option.some τ => Option.some (Function.updateITE τ X (replace_atom_all_rec τ F))
  | F, atom_ X :: Γ => unify (atom_ X, F :: Γ)
  | not_ phi, not_ phi' :: Γ =>
      unify (phi, phi' :: Γ)
  | and_ phi psi, and_ phi' psi' :: Γ
  | or_ phi psi, or_ phi' psi' :: Γ
  | imp_ phi psi, imp_ phi' psi' :: Γ
  | iff_ phi psi, iff_ phi' psi' :: Γ =>
      unify (phi, phi' :: psi, psi' :: Γ)
  | _ => Option.none
  termination_by L => ((equation_list_atom_set L).card, equation_list_size L)
  decreasing_by
  all_goals
    sorry
Γ : List Equation
 Prod.Lex (fun x1 x2 => x1 < x2) (fun x1 x2 => x1 < x2) ((equation_list_atom_set Γ).card, equation_list_size Γ)
  ((equation_list_atom_set ({ lhs := false_, rhs := false_ } :: Γ)).card,
    equation_list_size ({ lhs := false_, rhs := false_ } :: Γ))

Paige Thomas (Aug 15 2025 at 04:22):

If I swap the order I can make progress, but then I get stuck on

  termination_by L => (equation_list_size L, (equation_list_atom_set L).card)
  decreasing_by
  · apply Prod.Lex.left
    simp only [equation_list_size]
    simp only [List.foldr_cons]
    simp only [Formula_.size]
    linarith
  · apply Prod.Lex.left
    simp only [equation_list_size]
    simp only [List.foldr_cons]
    simp only [Formula_.size]
    linarith
  · apply Prod.Lex.left
    simp only [equation_list_size]
    simp only [List.foldr_cons]
    simp only [Formula_.size]
    linarith
  · simp only [Prod.lex_def]
    sorry
  all_goals
    sorry
X : String
F : Formula_
Γ : List Equation
h✝¹ : ¬atom_ X = F
h : ¬atom_occurs_in X F
 equation_list_size (var_elim X F Γ) < equation_list_size ({ lhs := atom_ X, rhs := F } :: Γ) 
  equation_list_size (var_elim X F Γ) = equation_list_size ({ lhs := atom_ X, rhs := F } :: Γ) 
    (equation_list_atom_set (var_elim X F Γ)).card < (equation_list_atom_set ({ lhs := atom_ X, rhs := F } :: Γ)).card

Paige Thomas (Aug 15 2025 at 04:44):

If I change it to this, I make further progress:

def unify :
  List Equation  Option (String  Formula_)
  | [] => Option.some atom_
  | false_, false_ :: Γ
  | true_, true_ :: Γ => unify Γ
  | atom_ X, F :: Γ =>
    if atom_ X = F
    then unify Γ
    else
      if atom_occurs_in X F
      then Option.none
      else
        have : (var_elim X F Γ).length < Γ.length + 1 :=
        by
          unfold var_elim
          simp only [List.length_map]
          apply lt_add_one
        match unify (var_elim X F Γ) with
        | Option.none => Option.none
        | Option.some τ => Option.some (Function.updateITE τ X (replace_atom_all_rec τ F))
  | F, atom_ X :: Γ => unify (atom_ X, F :: Γ)
  | not_ phi, not_ phi' :: Γ =>
      unify (phi, phi' :: Γ)
  | and_ phi psi, and_ phi' psi' :: Γ
  | or_ phi psi, or_ phi' psi' :: Γ
  | imp_ phi psi, imp_ phi' psi' :: Γ
  | iff_ phi psi, iff_ phi' psi' :: Γ =>
      unify (phi, phi' :: psi, psi' :: Γ)
  | _ => Option.none
  termination_by L => (L.length, (equation_list_atom_set L).card)

I then get to the case | ⟨F, atom_ X⟩ :: Γ => unify (⟨atom_ X, F⟩ :: Γ). It would seem like I should be able to use an induction hypothesis here, but there isn't one?

Paige Thomas (Aug 15 2025 at 07:08):

Ok, I think I see a way forward now.

Alexander Bentkamp (Aug 15 2025 at 13:41):

Try Prod.Lex.right' instead. I've made a proposal where to use left and where to use right' below.

And definitely use the measure ((equation_list_atom_set L).card, equation_list_size L), in this order. The other measures you are proposing will not work in my opinion.

One thing I missed is the case | ⟨F, atom_ X⟩ :: Γ => unify (⟨atom_ X, F⟩ :: Γ). My measure works only if you merge it with the previous case as follows.

def unify :
  List Equation  Option (String  Formula_)
  | [] => Option.some atom_
  | false_, false_ :: Γ
  | true_, true_ :: Γ => unify Γ
  | atom_ X, F :: Γ
  | F, atom_ X :: Γ =>
    if atom_ X = F
    then unify Γ
    else
      if atom_occurs_in X F
      then Option.none
      else
        match unify (var_elim X F Γ) with
        | Option.none => Option.none
        | Option.some τ => Option.some (Function.updateITE τ X (replace_atom_all_rec τ F))
  | not_ phi, not_ phi' :: Γ =>
      unify (⟨phi, phi' :: Γ)
  | and_ phi psi, and_ phi' psi' :: Γ
  | or_ phi psi, or_ phi' psi' :: Γ
  | imp_ phi psi, imp_ phi' psi' :: Γ
  | iff_ phi psi, iff_ phi' psi' :: Γ =>
      unify (⟨phi, phi' :: psi, psi' :: Γ)
  | _ => Option.none
  termination_by L => ((equation_list_atom_set L).card, equation_list_size L)
  decreasing_by
  · apply Prod.Lex.right'
    all_goals sorry
  · apply Prod.Lex.right'
    all_goals sorry
  · apply Prod.Lex.right'
    all_goals sorry
  · apply Prod.Lex.left
    sorry
  · apply Prod.Lex.right'
    all_goals sorry
  · apply Prod.Lex.left
    sorry
  · apply Prod.Lex.right'
    all_goals sorry
  · apply Prod.Lex.right'
    all_goals sorry
  · apply Prod.Lex.right'
    all_goals sorry
  · apply Prod.Lex.right'
    all_goals sorry
  · apply Prod.Lex.right'
    all_goals sorry

Full code here:

Full code

Paige Thomas (Aug 16 2025 at 06:07):

Thank you! I'm working my way through the proof.


Last updated: Dec 20 2025 at 21:32 UTC