RULER-EXTENDERS

control for ACL2's termination and induction analyses
Major Section:  SWITCHES-PARAMETERS-AND-MODES

Introduction

Consider the following recursive definition, which returns a list of threes of length one more than the length of x.

  (defun f (x)
    (cons 3
          (if (consp x)
              (f (cdr x))
            nil)))
One might expect ACL2's termination analysis to admit this function, since we know that (cdr x) is ``smaller'' than x if (consp x) is true. (By default, ACL2's notion of ``smaller'' is ordinary natural-number <, and the argument x is measured by applying function acl2-count to x.) However, that termination analysis does not consider IF tests, like (consp x) above, when they occur under calls of functions other than IF, such as CONS in the case above.

One way to overcome this problem is to ``lift'' the IF test to the top level, as follows.

  (defun f (x)
    (if (consp x)
        (cons 3 (f (cdr x)))
      (cons 3 nil)))
But another way to overcome the problem is to tell ACL2 to extend its termination (and induction) analysis through calls of cons, as follows.
  (defun f (x)
    (declare (xargs :ruler-extenders (cons)))
    (cons 3
          (if (consp x)
              (f (cdr x))
            nil)))
You may even wish to provide value :all instead of an explicit list of ruler-extenders, so that no function call blocks the termination analysis:
  (defun f (x)
    (declare (xargs :ruler-extenders :all))
    (cons 3
          (if (consp x)
              (f (cdr x))
            nil)))
Alternatively, you can omit the XARGS :RULER-EXTENDERS form, instead modifying the global default set of ruler-extenders:
  (set-ruler-extenders :all)

  ; or, for example:
  (set-ruler-extenders '(cons return-last))
You can call the function default-ruler-extenders as follows to see the current global default set of ruler-extenders:
(default-ruler-extenders (w state))

We conclude this introduction by considering the handling of LET expressions by termination analysis. Consider the following example.

  (defun fact (n)
    (the (integer 1 *)
         (if (posp n)
             (* n (fact (1- n)))
           1)))
ACL2 treats the call of THE in the body of this definition as follows.
  (let ((var (if (posp n)
                 (* n (fact (1- n)))
               1)))
    (if (and (integerp var) (<= 1 var))
        var
      <some_error>))
A LET expression, in turn, is treated as a LAMBDA application:
  ((lambda (var)
     (if (if (integerp var)
             (not (< var 1))
           nil)
         var
       <some_error>))
   (if (posp n)
       (* n (fact (1- n)))
     1))
Notice that the posp test, which governs the recursive call of fact, is inside an argument of a function application, namely the application of the LAMBDA expression. So by default, ACL2 will not consider this posp test in its termination analysis. The keyword :LAMBDAS in the list of ruler-extenders denotes all calls of lambda expressions, much as the inclusion of CONS in the ruler-extenders denotes all calls of CONS. The following definition is thus accepted by ACL2.
  (defun fact (n)
    (declare (xargs :ruler-extenders (:lambdas)))
    (the (integer 1 *)
         (if (posp n)
             (* n (fact (1- n)))
           1)))
As a convenience, ACL2 allows the symbol :lambdas in place of (:lambdas), and in fact the former will also include the default ruler-extenders: RETURN-LAST (which comes from macroexpansion of calls of PROG2$, EC-CALL, and others) and MV-LIST.

IMPORTANT REMARKS. (1) Notice that the argument to set-ruler-extenders is evaluated, but the argument to :RULER-EXTENDERS in XARGS is not evaluated. (2) Do not put macro names in your list of ruler-extenders. For example, if you intend that + should not block the termination analysis, in analogy to cons in the example above, then the list of ruler-extenders should include binary-+, not +. Of course, if you use :all then this is not an issue, but see the next remark. (3) Also please note that by taking advantage of the ruler-extenders, you may be complicating the induction scheme stored for the function, whose computation takes similar advantage of the additional IF structure that you are specifying.

Below we describe the notion of ruler-extenders in detail, as well as how to set its default using set-ruler-extenders.

Details

We begin by discussing how to set the ruler-extenders by using the macro set-ruler-extenders; below we will discuss the use of keyword :ruler-extenders in XARGS declare forms.

Examples:
(set-ruler-extenders :basic) ; return to default
(set-ruler-extenders *basic-ruler-extenders*) ; same as immediately above
(set-ruler-extenders :all) ; every governing IF test rules a recursive call
(set-ruler-extenders :lambdas) ; LET does not block termination analysis
(set-ruler-extenders (cons :lambdas *basic-ruler-extenders*))
                               ; same as immediately above
(set-ruler-extenders '(f g)) ; termination analysis goes past calls of f, g

General Form:
(set-ruler-extenders val)
where val evaluates to one of :basic, :all, :lambdas, or a true list of symbols containing no keyword other than, optionally, :lambdas.

When a recursive definition is submitted to ACL2 (in :logic mode), the recursion must be proved to terminate; see defun. More precisely, ACL2 explores the IF structure of the body of the definition to accumulate the tests that ``rule'' any given recursive call. The following example reviews how this works. Suppose that f has already been defined.

  (defun g (x y)
    (declare (xargs :measure (+ (acl2-count x) (acl2-count y))))
    (if (consp x)
        (g (cdr x) y)
      (if (consp y)
          (f (g x (cdr y)))
        (f (list x y)))))
ACL2 makes the following response to this proposed definition. Notice that the :measure proposed above must be proved to be an ACL2 ordinal -- that is, to satisfy O-P -- and that the arguments to each recursive call must be smaller (in the sense of that measure and O<, which here reduces to the ordinary < relation) than the formals under the assumption of the ruling IF tests. The first IMPLIES term below thus corresponds to the recursive call (g (cdr x) y), while the second corresponds to the recursive call (g x (cdr y)).
  For the admission of G we will use the relation O< (which is known
  to be well-founded on the domain recognized by O-P) and the measure
  (+ (ACL2-COUNT X) (ACL2-COUNT Y)).  The non-trivial part of the measure
  conjecture is

  Goal
  (AND (O-P (+ (ACL2-COUNT X) (ACL2-COUNT Y)))
       (IMPLIES (CONSP X)
                (O< (+ (ACL2-COUNT (CDR X)) (ACL2-COUNT Y))
                    (+ (ACL2-COUNT X) (ACL2-COUNT Y))))
       (IMPLIES (AND (NOT (CONSP X)) (CONSP Y))
                (O< (+ (ACL2-COUNT X) (ACL2-COUNT (CDR Y)))
                    (+ (ACL2-COUNT X) (ACL2-COUNT Y))))).
Now consider the following alternate version of the above definition.
  (defun g (x y)
    (declare (xargs :measure (+ (acl2-count x) (acl2-count y))))
    (if (consp x)
        (g (cdr x) y)
      (f (if (consp y)
             (g x (cdr y))
           (list x y)))))
The first test, (consp x), still rules the first recursive call, (g (cdr x) y). And the negation of that test, namely (not (consp x)), still rules the second recursive call (g x (cdr y)). But the call of f blocks the top-down exploration of the IF structure of the body of g, so (consp y) does not rule that second recursive call, which (again) is (g x (cdr y)). As a result, ACL2 fails to admit the above definition.

Set-ruler-extenders is provided to overcome the sort of blocking described above. Suppose for example that the following event is submitted:

  (set-ruler-extenders '(f))
Then the alternate definition of g above is admissible, because the call of f no longer blocks the top-down exploration of the IF structure of the body of g: that is, (consp y) becomes a ruler of the recursive call (g x (cdr y)). In this case, we say that f is a ``ruler-extender''. The same result obtains if we first submit
  (set-ruler-extenders :all)
as this removes all function calls as blockers of the top-down analysis. In other words, with :all it is the case that for every recursive call, every test argument of a superior call of IF contributes a ruler of that recursive call.

ACL2 handles LET (and LET*) expressions by translating them to LAMBDA expressions (see term). The next examples illustrates termination analysis involving such expressions. First consider the following (admittedly inefficient) definition.

  (defun fact (n)
    (let ((k (if (natp n) n 0)))
      (if (equal k 0)
          1
        (* k (fact (+ -1 k))))))
ACL2 translates the body of this definition to a LAMBDA application, essentially:
  ((lambda (k)
     (if (equal k 0)
         1
       (* k (fact (+ -1 k)))))
   (if (natp n) n 0))
As with the application of any function other than IF, the top-down termination analysis does not dive into arguments: the LAMBDA blocks the continuation of the analysis into its argument. But here, the argument of the LAMBDA is (if (natp n) n 0), which has no recursive calls to consider anyhow. What is more interesting: ACL2 does continue its termination analysis into the body of the LAMBDA, in an environment binding the LAMBDA formals to its actuals. In this case, the termination analysis thus continues into the term
  (if (equal k 0)
      1
    (* k (fact (+ -1 k))))
in the environment that binds k to the term (if (natp n) n 0). Thus, the proof obligation is successfully discharged, as reported by ACL2:
  For the admission of FACT we will use the relation O< (which is known
  to be well-founded on the domain recognized by O-P) and the measure
  (ACL2-COUNT N).  The non-trivial part of the measure conjecture is

  Goal
  (IMPLIES (NOT (EQUAL (IF (NATP N) N 0) 0))
           (O< (ACL2-COUNT (+ -1 (IF (NATP N) N 0)))
               (ACL2-COUNT N))).
  .....
  Q.E.D.

  That completes the proof of the measure theorem for FACT.

But now consider the following definition, in which the recursion takes place inside the argument of the LAMBDA rather than inside the LAMBDA body.

  (defun app (x y)
    (let ((result (if (endp x)
                      y
                    (cons (car x)
                          (app (cdr x) y)))))
      (if (our-test result)
          result
        0)))
Writing the body in LAMBDA notation:
  ((lambda (result)
     (if (our-test result) 
         result
       0))
   (if (endp x)
       y
     (cons (car x)
           (app (cdr x) y))))
By default, the LAMBDA call blocks the top-down termination analysis from proceeding into the term (if (endp x) ...). To solve this, one can submit the event:
  (set-ruler-extenders :lambdas)
The above definition of app is then admitted by ACL2, because the termination analysis is no longer blocked by the LAMBDA call.

The example just above illustrates that the heuristically-chosen measure is suitably sensitive to the ruler-extenders. Specifically: that measure is the application of acl2-count to the first formal parameter of the function that is tested along every branch of the relevant IF structure (as determined by the rulers) and occurs as a proper subterm at the same argument position in every recursive call. The heuristics for choosing the controller-alist for a definition rule are similarly sensitive to the ruler-extenders (see definition).

The remarks above for defun events are equally applicable when a definition sits inside a mutual-recursion event, except of course that in this case, a ``recursive call'' is a call of any function being defined by that mutual-recursion event.

Rules of class :definition are sensitive to set-ruler-extenders in analogy to the case of defun events.

This macro generates a call (table acl2-defaults-table :ruler-extenders val) and hence is local to any books and encapsulate events in which it occurs. See acl2-defaults-table. The current list of ruler-extenders may be obtained as

  (cdr (assoc-eq :ruler-extenders 
       (table-alist 'acl2-defaults-table (w state))))
or more conveniently, as:
  (default-ruler-extenders (w state))

Note that evaluation of (set-ruler-extenders lst), where lst evaluates to a list, does not necessarily include the default ruler-extenders -- i.e., those included for the argument, :basic -- which are the elements of the list constant *basic-ruler-extenders*, namely return-last and mv-list. You may, of course, include these explicitly in your list argument.

We conclude our discussion by noting that the set of ruler-extenders can affect the induction scheme that is stored with a recursive definition. The community book books/misc/misc2/ruler-extenders-tests.lisp explains how induction schemes are derived in this case. Consider the following example.

  (defun tree-of-nils-p (x)
    (if (consp x)
        (and (tree-of-nils-p (car x))
             (tree-of-nils-p (cdr x)))
      (null x)))
The above definition generates the following induction scheme. Note that (and u v) expands to (if u v nil), which explains why the term (tree-of-nils-p (car x)) rules the recursive call (tree-of-nils-p (cdr x)), resulting in the hypothesis (tree-of-nils-p (car x)) in the final conjunct below.
  (AND (IMPLIES (NOT (CONSP X)) (:P X))
       (IMPLIES (AND (CONSP X)
                     (NOT (TREE-OF-NILS-P (CAR X)))
                     (:P (CAR X)))
                (:P X))
       (IMPLIES (AND (CONSP X)
                     (TREE-OF-NILS-P (CAR X))
                     (:P (CAR X))
                     (:P (CDR X)))
                (:P X)))
Now consider the following variant of the above definition, in which a call of the function identity blocks the termination analysis.
  (defun tree-of-nils-p (x)
    (if (consp x)
        (identity (and (tree-of-nils-p (car x))
                       (tree-of-nils-p (cdr x))))
      (null x)))
This time the induction scheme is as follows, since only the top-level IF test contributes rulers to the termination analysis.
  (AND (IMPLIES (NOT (CONSP X)) (:P X))
       (IMPLIES (AND (CONSP X)
                     (:P (CAR X))
                     (:P (CDR X)))
                (:P X)))
But now suppose we first designate identity as a ruler-extender.
(set-ruler-extenders '(identity))
Then the induction scheme generated for the both of the above variants of tree-of-nils-p is the one shown for the first variant, which is reasonable because both definitions now produce essentially the same termination analysis.