Skip to content

Instantly share code, notes, and snippets.

@kini
Last active August 29, 2015 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kini/79db4500e029c0007025 to your computer and use it in GitHub Desktop.
Save kini/79db4500e029c0007025 to your computer and use it in GitHub Desktop.
ReverseLike in ACL2 with constrained functions

ReverseLike in ACL2 with constrained functions

This document describes a toy example of how ACL2’s “constrained functions” feature can be used to verify some higher-order statements, despite ACL2 being a first-order theorem prover.

Suppose we have a function called $reverse$ which when applied to a list returns a list containing the same elements in reverse order. Consider the following proposition.

Let $f$ be a function that satisfies the following three properties:

  • The empty list is unchanged by $f$.
  • A single-element list is unchanged by $f$.
  • Appending two lists and passing the result through $f$ is equivalent to passing each of the two lists through $f$ first and then appending them in the opposite order.

Then $f$ is identically equal to the $reverse$ function on all input lists.

How can this proposition be proved (or disproved)?

In a higher-order theorem prover like Coq, Agda, Idris, Isabelle/HOL, HOL4, etc., one might state something like “for all $f$, if the three properties hold of $f$, then for all lists $x$, it must be that $f(x) = reverse(x)$”. (In fact, see @copumpkin’s Agda version and @puffnfresh’s Idris version which inspired this document.)

However, in ACL2, functions are not terms in the logic, and so we cannot have propositions parametrized over functions in this way. But we do have a handy mechanism called encapsulate which allows us to “fake it” to some extent.

Let’s begin. First, let’s define our basic $reverse$ function. We’ll also need a function that appends two lists.

(defun app (x y)
  (if (atom x)
      y
    (cons (car x) (app (cdr x) y))))

(defun rev (x)
  (if (atom x)
      nil
    (app (rev (cdr x)) (list (car x)))))

If functions are not terms in the logic, what exactly does submitting a defun form mean? It introduces a new axiom into the logic, which equates applications of the function name to its arguments on the left hand side, to the expanded body on the right hand side. So the above defun forms introduced the following axioms into the logic:

(equal (app x y)
       (if (atom x) y (cons (car x) (app (cdr x) y))))

(equal (rev x)
       (if (atom x) nil (app (rev (cdr x)) (list (car x)))))

This is one way to introduce information about a function into the logic. Another way, of course, is to prove theorems about the function. For example, I might prove that if x is an atom, then (rev x) is nil:

(defthm dumb-theorem
  (implies (atom x)
           (equal (rev x) nil))

Once ACL2 is satisfied that the statement is derivable from the current axioms, it adds it as a new axiom.

A perhaps unsurprising fact about ACL2 is that in order to prove theorems about a function, you must first have defined the function. After all, you can’t pass the function in as a parameter to the theorem you’re trying to prove, because ACL2 is a first order theorem prover, as mentioned above.

But if you think about a function definition merely as a way to introduce an axiom about the function’s name, one might wonder why we couldn’t introduce (implies (atom x) (equal (rev x) nil)) into the logic without having to introduce (equal (rev x) ...) first.

What encapsulate allows us to do is exactly that… sort of. Basically it lets us create an environment in which rev is defined and some theorems are proved about it, but then the definitional axiom of rev is removed when exiting the environment, with the other theorems about rev remaining. At this point, rev has become a constrained function.

(Note: Why is it OK to simply remove an axiom from the logic? Well, we certainly won’t introduce any inconsistency by doing so, so why not?)

So what’s the point of doing this? Well, think back to the statement we wanted to formalize. “Let $f$ be a function that satisfies […]” What we propose to do here is to literally let $f$ be a function that satisfies […], without any further knowledge about $f$; at the end of our encapsulate form, we will essentially have three pieces of information and no more, just as in the original English formulation – and we have done so without appealing to quantification over functions in the logic.

(Isabelle users might gain some intuition for what is going on here by thinking about the difference between $\bigwedge$ and $∀$.)

We begin the encapsulate form, as follows:

(encapsulate
 (((f *) => *))

Here we have declared that we are going to define a constrained function called f, which will accept one argument and return one result (in ACL2, functions can of course take multiple arguments, but they can also “return multiple results” in some cases, so this is important to specify).

Normally it’s not important to declare this kind of stuff before defining a function, but as the definitional axiom is going to be rescinded later, the system needs to at least retain knowledge of what ways of applying the function are valid. For example, it needs to be able to cry foul if we later say something involving, say, (f x y).

We define our function f as follows.

(local
 (defun f (x)
   (rev x)))

The local wrapper indicates that the axiom introduced by this defun will be rescinded once we exit the encapsulate.

Why define f as equal to rev? Remember that f is supposed to represent an arbitrary function that satisfies three properties. But in order to add that knowledge to the logic, we need to provide a particular f as a witness that it is even possible to satisfy those three properties. Thankfully, rev does satisfy the three properties (as we would expect!).

This is a disadvantage of our approach – as we encode our properties of f as axioms in the logic, they had better be satisfiable, otherwise we’re making the logic inconsistent. In a higher-order logic, we could easily prove a vacuous theorem like “if 3 is an even number then 5 = 7” without introducing inconsistency into the logic itself, as the contradiction is contained within the hypotheses of a particular proposition.

Now that we’ve defined our witness f, namely rev, we proceed to show that f satisfies the three properties we desire:

(local
 (defthm true-listp-rev
   (true-listp (rev x))))

(local
 (defthm app-nil
   (implies (true-listp x)
            (equal (app x nil) x))))

(defthm f-0
  (equal (f nil)
         nil))

(defthm f-1
  (equal (f (list x))
         (list x)))

(defthm f-app
  (implies (and (true-listp x)
                (true-listp y))
           (equal (app (f x) (f y))
                  (f (app y x)))))

Note that we needed a couple small lemmas first, in order to show that rev indeed satisfied the three properties. Since they don’t involve f, it doesn’t really matter whether we put them in a local wrapper or not (in the sense that it won’t affect what we remember about f after leaving the encapsulate), but we won’t need them again so we might as well make them local here.

At this point we know four things about f (by which I mean the logic contains four axioms involving f):

(equal (f x) (rev x))
(equal (f nil) nil)
(equal (f (list x)) (list x))
(implies (and (true-listp x) (true-listp y))
         (equal (app (f x) (f y)) (f (app x y))))

Now we close the encapsulate

)

… and only the latter three axioms remain.

Now, can we prove the statement we were originally considering? Are those three properties sufficient for us to determine that f is identical to rev, at least when considering lists as input?

We can answer this question by attempting to re-prove the axiom we lost, (equal (f x) (rev x)), from the axioms we have remaining. Here’s how I did it:

(defthm f-cons
  (implies (true-listp y)
           (equal (f (cons x y))
                  (app (f y) (list x))))
  :hints (("Goal"
           :use (:instance f-app
                           (x y)
                           (y (list x))))))

(defthm f-rev-equivalent
  (implies (true-listp x)
           (equal (f x)
                  (rev x))))

(Note that I had to add the hypothesis (true-listp x), otherwise the statement isn’t quite true – one could imagine a function f that acted like rev when the input was a true list, but returned the string “fnord” on any other input, for example. ACL2 is an untyped logic, folks!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment