Zulip Chat Archive

Stream: general

Topic: termination of conversion to negation normal form


Paige Thomas (Jan 23 2025 at 06:52):

I was wondering if someone might have a suggestion for how to go about proving that this terminates:

import Mathlib.Util.CompileInductive

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_
  | forall_ : String  Formula_  Formula_
  | exists_ : String  Formula_  Formula_
  deriving Inhabited, DecidableEq, Repr

compile_inductive% Formula_

open Formula_

def to_nnf :
  Formula_  Formula_
  | and_ phi psi => and_ (to_nnf phi) (to_nnf psi)
  | or_ phi psi => or_ (to_nnf phi) (to_nnf psi)
  | imp_ phi psi => or_ (to_nnf (not_ phi)) (to_nnf psi)
  | iff_ phi psi => or_ (and_ (to_nnf phi) (to_nnf psi)) (and_ (to_nnf (not_ phi)) (to_nnf (not_ psi)))
  | not_ (not_ phi) => to_nnf phi
  | not_ (and_ phi psi) => or_ (to_nnf (not_ phi)) (to_nnf (not_ psi))
  | not_ (or_ phi psi) => and_ (to_nnf (not_ phi)) (to_nnf (not_ psi))
  | not_ (imp_ phi psi) => and_ (to_nnf phi) (to_nnf (not_ psi))
  | not_ (iff_ phi psi) => or_ (and_ (to_nnf phi) (to_nnf (not_ psi))) (and_ (to_nnf (not_ phi)) (to_nnf psi))
  | phi => phi

Paige Thomas (Jan 23 2025 at 06:53):

The issue seems to be the calls of the form to_nnf (not_ phi) and to_nnf (not_ psi) on the rhs.

Joachim Breitner (Jan 23 2025 at 08:24):

Right, these aren't obviously smaller, are they?
If you specify termination_by f => f then you should get better error messagea about what could not be solved here. Maybe you need a custom measure that really is decreasing.

Joachim Breitner (Jan 23 2025 at 08:26):

Alternatively and maybe cleaner: write two mutually recursive functions, to_nnf and to_nnf_neg. Then you might even get away with structural recursion!

Joachim Breitner (Jan 23 2025 at 08:27):

Oh, yes, the imp case will clearly make it hard to get your current definition through, at least without a very carefully crafted measure that makes not cheap and imp expensive. Try the mutual variant!

Joachim Breitner (Jan 23 2025 at 08:28):

And read https://lean-lang.org/blog/2024-1-11-recursive-definitions-in-lean/ if you haven't already!

Paige Thomas (Jan 23 2025 at 08:29):

Do you mean one that translates a formula F and another that translates not F?

Paige Thomas (Jan 23 2025 at 08:29):

I see. I'll give it a try. Thank you!

Joachim Breitner (Jan 23 2025 at 08:29):

Right. This way you should never have to make bigger arguments in the recursive calls

Paige Thomas (Jan 23 2025 at 09:09):

This seems to type check. I'll have to prove some properties to make sure it means the same thing.

import Mathlib.Util.CompileInductive

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_
  | forall_ : String  Formula_  Formula_
  | exists_ : String  Formula_  Formula_
  deriving Inhabited, DecidableEq, Repr

compile_inductive% Formula_

open Formula_

mutual
def to_nnf :
  Formula_  Formula_
  | and_ phi psi => and_ (to_nnf phi) (to_nnf psi)
  | or_ phi psi => or_ (to_nnf phi) (to_nnf psi)
  | imp_ phi psi => or_ (to_nnf_neg phi) (to_nnf psi)
  | iff_ phi psi => or_ (and_ (to_nnf phi) (to_nnf psi)) (and_ (to_nnf_neg phi) (to_nnf_neg psi))
  | not_ (not_ phi) => to_nnf phi
  | not_ (and_ phi psi) => or_ (to_nnf_neg phi) (to_nnf_neg psi)
  | not_ (or_ phi psi) => and_ (to_nnf_neg phi) (to_nnf_neg psi)
  | not_ (imp_ phi psi) => and_ (to_nnf phi) (to_nnf_neg psi)
  | not_ (iff_ phi psi) => or_ (and_ (to_nnf phi) (to_nnf_neg psi)) (and_ (to_nnf_neg phi) (to_nnf psi))
  | phi => phi

def to_nnf_neg :
  Formula_  Formula_
  | (not_ phi) => to_nnf phi
  | (and_ phi psi) => or_ (to_nnf_neg phi) (to_nnf_neg psi)
  | (or_ phi psi) => and_ (to_nnf_neg phi) (to_nnf_neg psi)
  | (imp_ phi psi) => and_ (to_nnf phi) (to_nnf_neg psi)
  | (iff_ phi psi) => or_ (and_ (to_nnf phi) (to_nnf_neg psi)) (and_ (to_nnf_neg phi) (to_nnf psi))
  | phi => not_ phi
end

#eval to_nnf false_
#eval to_nnf (not_ false_)
#eval to_nnf (not_ (not_ false_))
#eval to_nnf (not_ (not_ (not_ false_)))
#eval to_nnf (not_ (not_ (not_ (not_ false_))))

Joachim Breitner (Jan 23 2025 at 09:10):

I’d replace

  | not_ (not_ phi) => to_nnf phi
  | not_ (and_ phi psi) => or_ (to_nnf_neg phi) (to_nnf_neg psi)
  | not_ (or_ phi psi) => and_ (to_nnf_neg phi) (to_nnf_neg psi)
  | not_ (imp_ phi psi) => and_ (to_nnf phi) (to_nnf_neg psi)
  | not_ (iff_ phi psi) => or_ (and_ (to_nnf phi) (to_nnf_neg psi)) (and_ (to_nnf_neg phi) (to_nnf psi))

with

  | not_ phi => to_nnf_not phi

no need to duplicate that logic!

Paige Thomas (Jan 23 2025 at 09:11):

That did seem to be odd to me.

Joachim Breitner (Jan 23 2025 at 09:14):

Hmm, why? Seems natural to me. After all, to_nnf_not phi is meant to be the helper to implement to_nnf (not phi)

Paige Thomas (Jan 23 2025 at 09:14):

No, I mean it seemed odd to have the same logic twice.

Paige Thomas (Jan 23 2025 at 09:15):

Cool. Thank you!


Last updated: May 02 2025 at 03:31 UTC