SUBVERSIVE-RECURSIONS

why we restrict encapsulated recursive functions
Major Section:  MISCELLANEOUS

Subtleties arise when one of the ``constrained'' functions, f, introduced in the signature of an encapsulate event, is involved in the termination argument for a non-local recursively defined function, g, in that encapsulate. During the processing of the encapsulated events, f is locally defined to be some witness function, f'. Properties of f' are explicitly proved and exported from the encapsulate as the constraints on the undefined function f. But if f is used in a recursive g defined within the encapsulate, then the termination proof for g may use properties of f' -- the witness -- that are not explicitly set forth in the constraints stated for f.

Such recursive g are said be ``subversive'' because if naively treated they give rise to unsound induction schemes or (via functional instantiation) recurrence equations that are impossible to satisfy. We illustrate what could go wrong below.

Subversive recursions are not banned outright. Instead, they are treated as part of the constraint. That is, in the case above, the definitional equation for g becomes one of the constraints on f. This is generally a severe restriction on future functional instantiations of f. In addition, ACL2 removes from its knowledge of g any suggestions about legal inductions to ``unwind'' its recursion.

What should you do? Often, the simplest response is to move the offending recursive definition, e.g., g, out of the encapsulate. That is, introduce f by constraint and then define g as an ``independent'' event. You may need to constrain ``additional'' properties of f in order to admit g, e.g., constrain it to reduce some ordinal measure. However, by separating the introduction of f from the admission of g you will clearly identify the necessary constraints on f, functional instantiations of f will be simpler, and g will be a useful function which suggests inductions to the theorem prover.

Note that the functions introduced in the signature should not even occur ancestrally in the termination proofs for non-local recursive functions in the encapsulate. That is, the constrained functions of an encapsulate should not be reachable in the dependency graph of the functions used in the termination arguments of recursive functions in encapsulate. If they are reachable, their definitions become part of the constraints.

The following event illustrates the problem posed by subversive recursions.

(encapsulate (((f *) => *))
  (local (defun f (x) (cdr x)))
  (defun g (x)
    (if (consp x) (not (g (f x))) t)))
Suppose, contrary to how ACL2 works, that the encapsulate above were to introduce no constraints on f on the bogus grounds that the only use of f in the encapsulate is in an admissible function. We discuss the plausibility of this bogus argument in a moment.

Then it would be possible to prove the theorem:

(defthm f-not-identity
  (not (equal (f '(a . b)) '(a . b)))
  :rule-classes nil
  :hints (("Goal" :use (:instance g (x '(a . b))))))
simply by observing that if (f '(a . b)) were '(a . b), then (g '(a . b)) would be (not (g '(a . b))), which is impossible.

But then we could functionally instantiate f-not-identity, replacing f by the identity function, to prove nil! This is bad.

(defthm bad
  nil
  :rule-classes nil
  :hints
  (("Goal" :use (:functional-instance f-not-identity (f identity)))))

This sequence of events was legal in versions of ACL2 prior to Version 1.5. When we realized the problem we took steps to make it illegal. However, our steps were insufficient and it was possible to sneak in a subversive function (via mutual recursion) as late as Version 2.3.

We now turn to the plausibility of the bogus argument above. Why might one even be tempted to think that the definition of g above poses no constraint on f? Here is a very similar encapsulate.

(encapsulate (((f *) => *))
  (local (defun f (x) (cdr x)))
  (defun map (x)
    (if (consp x)
        (cons (f x) (map (cdr x)))
      nil)))
Here map plays the role of g above. Like g, map calls the constrained function f. But map truly does not constrain f. In particular, the definition of map could be moved ``out'' of the encapsulate so that map is introduced afterwards. The difference between map and g is that the constrained function plays no role in the termination argument for the one but does for the other.

As a ``user-friendly'' gesture, ACL2 implicitly moves map-like functions out of encapsulations; logically speaking, they are introduced after the encapsulation. This simplifies the constraint. When the constraint cannot be thus simplfied the user is advised, via the ``infected'' warning, to phrase the encapsulation in the simplest way possible.

The lingering bug between Versions 1.5 and 2.3 mentioned above was due to our failure to detect the g-like nature of some functions when they were defined in mutually recursively cliques with other functions. The singly recursive case was recognized. The bug arose because our detection ``algorithm'' was based on the ``suggested inductions'' left behind by successful definitions. We failed to recall that mutually-recursive definitions do not, as of this writing, make any suggestions about inductions and so did not leave any traces of their subversive natures.

We conclude by elaborating on the criterion ``involved in the termination argument'' mentioned at the outset. Suppose that function f is recursively defined in an encapsulate, where the body of f has no ``ancestor'' among the functions introduced in the signature of that encapsulate, i.e.: the body contains no call of a signature function, no call of a function whose body calls a signature function, and so on. Then ACL2 treats f as though it is defined in front of that encapsulate form; so f is not constrained by the encapsulate, and its definition is hence certainly not subversive in the sense discussed above. But suppose to the contrary that some function introduced in the signature is an ancestor of the body of f. Then the definition of f is subversive if moreover, its termination proof obligation has an ancestor among the signature functions. Now, that proof obligation involves terms from the first argument of selected calls of IF, as well as recursive calls; for a detailed discussion, see ruler-extenders. The important point here is that for the recursive calls, only the arguments in ``measured'' positions are relevant to the termination proof obligation. Consider the following example.

  (encapsulate
   (((h *) => *))
   (local (defun h (n) n))
   (defun f (i n)
     (if (zp i)
         n
       (f (1- i) (h n)))))
ACL2 heuristically picks the measure (acl2-count i) for the definition of f; thus, i is the only ``measured formal'' of f. Since i is the first formal of f, then for the recursive call of f, only the first argument contributes to the termination proof obligation: in this case, (1- i) but not (h n). Thus, even though h is a signature function, the definition of f is not considered to be subversive; an induction scheme is thus stored for f. (This restriction to measured formal positions of recursive calls, for determining subversive definitions, is new in Version_3.5 of ACL2.)