• Top
    • Documentation
    • Books
    • Boolean-reasoning
    • Projects
    • Debugging
    • Std
    • Proof-automation
    • Macro-libraries
    • ACL2
      • Theories
      • Rule-classes
      • Proof-builder
      • Recursion-and-induction
      • Hons-and-memoization
      • Events
      • Parallelism
      • History
      • Programming
        • Defun
        • Declare
        • System-utilities
        • Stobj
        • State
        • Mutual-recursion
        • Memoize
        • Mbe
        • Io
        • Defpkg
        • Apply$
        • Loop$
          • Loop$-primer
          • Do-loop$
          • For-loop$
            • Loop$-recursion
            • Stating-and-proving-lemmas-about-loop$s
            • Loop$-recursion-induction
            • Do$
          • Programming-with-state
          • Arrays
          • Characters
          • Time$
          • Defmacro
          • Loop$-primer
          • Fast-alists
          • Defconst
          • Evaluation
          • Guard
          • Equality-variants
          • Compilation
          • Hons
          • ACL2-built-ins
          • Developers-guide
          • System-attachments
          • Advanced-features
          • Set-check-invariant-risk
          • Numbers
          • Efficiency
          • Irrelevant-formals
          • Introduction-to-programming-in-ACL2-for-those-who-know-lisp
          • Redefining-programs
          • Lists
          • Invariant-risk
          • Errors
          • Defabbrev
          • Conses
          • Alists
          • Set-register-invariant-risk
          • Strings
          • Program-wrapper
          • Get-internal-time
          • Basics
          • Packages
          • Oracle-eval
          • Defmacro-untouchable
          • <<
          • Primitive
          • Revert-world
          • Unmemoize
          • Set-duplicate-keys-action
          • Symbols
          • Def-list-constructor
          • Easy-simplify-term
          • Defiteration
          • Fake-oracle-eval
          • Defopen
          • Sleep
        • Operational-semantics
        • Real
        • Start-here
        • Debugging
        • Miscellaneous
        • Output-controls
        • Macros
        • Interfacing-tools
      • Interfacing-tools
      • Hardware-verification
      • Software-verification
      • Math
      • Testing-utilities
    • Loop$

    For-loop$

    Iteration with loop$ over an interval of integers or a list

    This topic assumes that you have read the introduction to loop$ expressions in ACL2; see loop$. Here we give more complete documentation on FOR loop$ expressions, beginning with informal discussion and then continuing with detailed syntax (General Form) and semantics. For a discussion of proofs about loop$s, see stating-and-proving-lemmas-about-loop$s.

    Examples of loop$ expressions, including FOR loop$s, may be found in community-book projects/apply/loop-tests.lisp.

    The only allowed iteration clauses are IN, where the variable ranges over the elements of the given true list; ON, where the variable ranges over the tails of the given true-list; and FROM/TO/BY, where the variable ranges over the integers between two bounds, stepping by a positive integer increment (or by 1 if no BY clause is provided).

    You may have as many iteration clauses as you wish, connected with AS. Each must introduce a unique iteration variable and that variable may be optionally followed by an OF-TYPE type-spec specification. OF-TYPE is a Common Lisp feature that allows the compiler to optimize operations on the variable in question. Here is an example.

    (loop$ for v of-type (and integer (not (satisfies zerop)))
                 from 1 to 100
           sum (/ 1 v))

    Here is that same example with a more concise type specification.

    (loop$ for v of-type (integer 1 *)
                 from 1 to 100
           sum (/ 1 v))

    After all of the iteration clauses, you may have a termination test, signaled by UNTIL, and/or a conditional test, signaled by WHEN. If both are provided, the UNTIL test must come first. Iteration stops when the UNTIL test is satisfied. The conditional test determines whether the loop body is executed for the current value of the iteration variables.

    Between the UNTIL symbol and the expression to be tested, and between the WHEN symbol and its expression, you may include a :GUARD clause. This is useful if guard verification requires an invariant relating multiple iteration variables. An example of a guarded UNTIL clause is

    (loop$ for u in lst1 as v in lst2
           until :guard (invariantp u v) (test u v)
           collect (body u v))

    ACL2 supports only five operators in FOR loop$s: SUM, COLLECT, ALWAYS, THEREIS and APPEND. We anticipate adding other Common Lisp operators eventually.

    The special symbols noted above, sometimes called ``FOR loop$ keywords'' or just ``loop$ keywords'' may be in any package. These are FOR, IN, ON, FROM, TO, BY, OF-TYPE, WHEN, UNTIL, SUM, COLLECT, ALWAYS, THEREIS, and APPEND.

    Between the operator, e.g., SUM or COLLECT, and the loop$ body you may include a :GUARD clause as in

    (loop$ for u in lst1 as v in lst2
           collect :guard (invariantp u v) (body u v))

    This is sometimes necessary in the verification of the guards for the loop$ body because Common Lisp's OF-TYPE clauses do not permit you to relate one variable to another.

    See lp-section-6 of the Loop$ Primer for some exercises in writing FOR Loop$s (with answers in a Community Book). But remember to come back here when you get to the end of that section.

    General Form

    The syntax of Common Lisp loop expressions is extremely complicated. Rather than try to write the abstract syntax of ACL2's loop$ expressions in the same formal style, we take a different approach, which is workable because loop$ allows fewer options.

    First we introduce the syntax of a ``target clause,'' a ``type-spec,'' and the ``operators.'' Then we describe the most elaborate form of a loop$ expression in terms of these elements and ordinary ACL2 terms. Every legal loop$ expression can be produced by omitting certain optional elements from the most elaborate loop$ form. So we conclude the syntactic description of loop$ by listing the elements that can be omitted.

    A target clause has one of four forms

    • IN list-expr
    • ON expr
    • FROM lo-expr TO hi-expr
    • FROM lo-expr TO hi-expr BY step-expr

    where list-expr is a term (which is expected to evaluate to a true list), expr is a term, lo-expr and hi-expr are terms (which are expected to evaluate to integers), and step-expr is a term (which is expected to evaluate to a positive integer).

    The legal type-specs are listed in type-spec.

    The legal operators are SUM, COLLECT, ALWAYS, THEREIS, and APPEND.

    The most elaborate loop$ expression is of the form

            (LOOP$ FOR v1 OF-TYPE spec1 target1
                            AS   v2 OF-TYPE spec2 target2
                            ...
                            AS   vn OF-TYPE specn targetn
                            UNTIL :GUARD guard1 until-expr
                            WHEN   :GUARD guard2 when-expr
                            ; Note the ALWAYS/THEREIS Exceptions below!
                            op :GUARD guard3 body-expr)

    where each vi   is a legal variable symbol and they are all distinct, each type-speci   is a type-spec, each targeti   is a target clause, each guardi, until-expr, and when-expr   is a term, op   is an operator, and body-expr   is a term. Furthermore, until-expr, when-expr, and body-expr   must be tame!

    The ALWAYS/THEREIS Exception: Common Lisp prohibits loops with both a WHEN clause and either an ALWAYS or a THEREIS operator. For example, if you are tempted to use WHEN p with ALWAYS q you can instead write ALWAYS (implies p q) or, if you want to evaluate q only when p is true, you can write ALWAYS (if p q t).

    The following elements may be omitted.

    • any line beginning with AS, UNTIL or WHEN,
    • any OF-TYPE speci, and
    • any :GUARD guardi.

    As noted above, the FOR loop$ keywords (as used above) may be in any package. These are FOR, IN, ON, FROM, TO, BY, OF-TYPE, WHEN, UNTIL, SUM, COLLECT, ALWAYS, THEREIS, and APPEND.

    We give names to certain classes of the syntactic entities above. The v1, ..., vn are called the iteration variables. The spec1, ..., specn are called type specs, each corresponds to a certain iteration variable, and each gives rise to a type term about its variable in the sense that ``X OF-TYPE (SATISFIES NATP)'' gives rise to the type term (NATP X) and ``I OF-TYPE INTEGER'' gives rise to the type term (INTEGERP I). The terms involved in the target expressions, e.g., the list-expr and expr in ``IN list-expr'' and ``ON expr'' and the lo-expr, hi-expr and optional step-expr in the ``FROM lo-expr TO hi-expr BY step-expr'' targets are called target terms. Finally, the until-expr, when-expr, and body-expr are called iterative forms.

    We distinguish the target terms from the iterative forms because they are handled very differently at evaluation time. When a loop$ is evaluated, the target terms are evaluated just once. But the iterative forms are evaluated multiple times as the iteration variables range over the values of the targets.

    A FOR loop$ expression with just one iteration variable and in which the iterative forms mention no free variable other than the iteration variable is called a simple loop$ (or, sometimes, a simple loop). An example of a simple loop is

    (loop$ for x in lst when (evenp x) collect (+ 1 (sq x)))

    A FOR loop$ expression is called a fancy loop$ if it is not simple. Both of the following loop$s are fancy.

    (loop$ for x in xlst as y on ylst collect (expr x y))
    
    (loop$ for x in xlst collect (expr x z))

    The first is fancy because it has two iteration variables. The second is fancy because the body freely uses the variable z which is not the iteration variable. The free variables cannot be stobjs or dfs, since the translation of the loop$ will put them into a list; see discussion of the semantics, below. In such cases you may wish to use DO loop$ expressions (see do-loop$) instead.

    Semantics

    FOR loop$ expressions are translated into calls of scions, with the UNTIL and WHEN clauses translated into preprocessors of the targets. But which scions are used depend on whether the loop is simple or fancy. Recall that a fancy loop is one that has either or both of the following characteristics: (a) there is one or more as clauses, and/or (b) one of the iterative forms (the UNTIL, WHEN or loop body expression) refers to variables other than an iteration variable. If the loop$ expression is simple, the simple scions are used; otherwise the fancy scions are used.

    loop$               simple          fancy
    keyword             scion           scion
    ______________________________________________
    SUM                 sum$            sum$+
    COLLECT             collect$        collect$+
    ALWAYS              always$         always$+
    THEREIS             thereis$        thereis$+
    APPEND              append$         append$+
    UNTIL               until$          until$+
    WHEN                when$           when$+

    We deal with simple loop$s first.

    Semantics of Simple Loop$s

    For example, the simple loop$

    (loop$ for x in lst collect (+ 1 (sq x)))

    translates to (a term equivalent to)

    (collect$ (lambda$ (x)
                       (declare (ignorable x))
                       (+ 1 (sq x)))
              lst).

    Note: The actual translation is tagged with various markers that play a role in evaluation but which are logically irrelevant and which are removed during proof. In this discussion we will not display the marked-up translations but logically equivalent terms instead. You can see the actual translations for yourself with trans.

    In the translation the target term, lst, appears as an ordinary subterm of the translation. But the iterative form, (+ 1 (sq x)), becomes the body of a lambda$ expression, which means its translation becomes a component of a quoted LAMBDA object. When the collect$ is evaluated, the target term is evaluated once but the iterative form is evaluated once for each element of the value of the target.

    UNTIL and WHEN clauses are handled by preprocessing the target. E.g.,

    (loop$ for x in lst
           until (> x 100)
           when (evenp x)
           collect (+ 1 (sq x)))

    becomes

    (collect$ (lambda$ (x)
                       (declare (ignorable x))
                       (+ 1 (sq x)))
              (when$ (lambda$ (x)
                              (declare (ignorable x))
                              (evenp x))
                     (until$ (lambda$ (x)
                                      (declare (ignorable x))
                                      (> x 100))
                             lst)))

    So from a logical perspective, the presence of an UNTIL and/or WHEN clause in a collect iteration over lst ``copies'' the target value. The until$ copies lst until encountering the first element on which its functional argument is true. The when$ then copies that (shortened?) target, keeping only the elements that satisfy its functional argument. Finally, the collect$ then applies its functional argument and collects all the values.

    ON and FROM/TO/BY targets are handled by listing all the elements in the given target. For example,

    (loop$ for x on lst collect (expr x))

    which maps x over successive non-empty tails of lst and collects the value of expr has the logical meaning

    (collect$ (lambda$ (x) (expr x))
              (tails lst))

    where, for example, (tails '(1 2 3)) is ((1 2 3) (2 3) (3)).

    Spiritually similarly,

    (loop$ for i from 1 to max by step collect (expr x))

    logically becomes

    (collect$ (lambda$ (x) (expr x))
              (from-to-by 1 max step))

    where, for example, (from-to-by 1 10 2) is (1 3 5 7 9).

    Similar translations are done for the other operators, e.g., SUM and ALWAYS. The advantage of this translation style is that it allows compositional reasoning. We discuss this further below.

    The following example illustrates basic guard proof obligations, in particular showing that WHEN clauses do not help with verifying guards for the loop$ bodies. (Similarly, UNTIL clauses do not help either.) The basic problem is that ACL2 requires that lambda objects be guard verifiable in isolation, not confined to the context in which a particular lambda object appears. Consider the following.

    (include-book "projects/apply/top" :dir :system)
    (defun$ sq (n)
      (declare (xargs :guard (natp n)))
      (* n n))
    (defun foo (lst)
      (declare (xargs :guard (nat-listp lst)))
      (loop$ for x of-type (satisfies nat-listp) on lst
             when (consp x)
             sum (sq (car x))))

    Guard verification fails for foo. The summary says that a goal of NIL was generated. Using :pso we can see that the NIL goal came from:

    Subgoal 1.2
    (IMPLIES (NAT-LISTP LOOP$-IVAR)
             (INTEGERP (CAR LOOP$-IVAR))).

    Let's see what is going on by looking at the following simplified translation of the loop$ expression.

    (sum$ '(lambda (loop$-ivar)
             (declare (type (satisfies nat-listp) loop$-ivar))
             (sq (car loop$-ivar)))
          (when$ '(lambda ...) (tails lst)))

    Notice that the lambda object supplied to sum$ cannot be guard verified in isolation: nil satisfies the :guard and (car nil) violates the guard of sq. The following modification, which adds a :guard directive after the SUM op keyword, solves the problem.

    (defun foo (lst)
      (declare (xargs :guard (nat-listp lst)))
      (loop$ for x of-type (satisfies nat-listp) on lst
             when (consp x)
             sum :guard (consp x)  ; note new :guard
             (sq (car x))))

    This new :guard may feel redundant, coming as it does after the when (consp x) clause. But it is necessary given the compositional semantics.

    A simplified translation of the defun above shows that the application (sq (car x)) is protected by a suitable guard in the lambda object.

    (sum$ '(lambda (loop$-ivar)
             (declare (type (satisfies nat-listp) loop$-ivar)
                      (xargs :guard (consp loop$-ivar)))
             (sq (car loop$-ivar)))
          (when$ '(lambda ...) (tails lst)))

    Naively we might have expected that the guard proof obligation for the loop$ body (sq (car x)) could assume the WHEN clause, but that expectation would be wrong because of the compositional semantics we use and the fact that lambda objects must be guard-verifiable on their own. The reason for the latter requirement is that our implementation caches guard-verified lambda objects for evaluation in raw Lisp, which may take place in other contexts different from that in which the lambda first appeared. See print-cl-cache.

    Semantics of Fancy Loop$s

    An example of a fancy loop$ is

    (loop$ for x in xlst as y in ylst collect (expr x y z))

    This loop exhibits both characteristics (a) and (b): it has an AS clause and the variable z appears in the loop body. Either characteristic is sufficient to classify the loop as fancy. So fancy scions are used. Here is its semantic counterpart, i.e., its simplified translation.

    (collect$+
     (lambda$ (loop$-gvars loop$-ivars)
              (declare (xargs :guard (and (true-listp loop$-gvars)
                                          (equal (len loop$-gvars) 1)
                                          (true-listp loop$-ivars)
                                          (equal (len loop$-ivars) 2))))
              (let ((z (car loop$-gvars))
                    (x (car loop$-ivars))
                    (y (car (cdr loop$-ivars))))
                (declare (ignorable x y))
                (expr x y z)))
     (list z)
     (loop$-as (list xlst ylst)))

    Before we show the definition of collect$+, note that the arguments above to collect$+ are (i) a lambda$ expression that handles the evaluation of the iterative form, in this case (expr x y z), where x and y are iteration variables and z is a ``global'' variable not among the iteration variables; (ii) the list of values of the ``global'' variables, in this case the list containing z; and (iii) a target list constructed by loop$-as from the various targets provided in the loop$, in this case xlst and ylst, supplying values for iteration variables x and y respectively. For example, (loop$-as (list '(a b c d e) '(1 2 3))) is ((a 1) (b 2) (c 3)). These tuples contain successive corresponding values of x and y.

    The definition of collect$+ is essentially

    (defun collect$+ (fn loop$-gvars lst)
      (if (endp lst)
          nil
          (cons (apply$ fn (list loop$-gvars (car lst)))
                (collect$+ fn loop$-gvars (cdr lst)))))

    We have omitted the guard and an mbe form that make it run more efficiently. All the fancy loop$ scions are defined analogously.

    Inspection of the lambda$ expression above reveals that it takes a list of global variable values and a list of iteration variable values, unpacks them with a let that binds the global variables, here just z, to their values and binds the iteration variables, here x and y, to the corresponding pair of values from the target, and then evaluates the loop$ body, (expr x y z).

    The names of the formals for the lambda$ expressions generated by fancy loop$ expressions are always loop$-gvars and loop$-ivars, for ``loop$ global variables'' and ``loop$ iteration variables.''

    UNTIL and WHEN clauses in a fancy loop$ are handled exactly as they are in simple loop$s, except that the fancy scions are used since the target list is a list of tuples of iteration variable values and the UNTIL and WHEN forms may refer to global variables.

    See lp-section-10 of the Loop$ Primer for a step-by-step discussion of the evaluation of the formal semantics of an example of a fancy Loop$. But remember to come back here when you get to the end of that section.

    Special Guard Conjectures for FOR loop$s

    Since every loop$ expands to a call of a loop$ scion on a lambda object and a target, one would expect that guard verification would generate the guard conjectures for that scion and target. Indeed, it does. In particular, the lambda object must have the correct number of formals (which is guaranteed by translation) and the target must satisfy true-listp.

    But in addition to the expected guard conjectures, we generate some special ones for the terms produced by translating FOR loop$ expressions. We discuss the reasons in the next section, but here we just state what the special conjectures are. We limit ourselves to a simple loop$. Fancy loop$s generalize in the obvious way. The two classes of ``special guard conjectures'' for FOR loop$ expressions are as follows.

    • First, every element (or tail, in the case of ON loop$s)) satisfies the type-spec, if any, and the loop$ body's :GUARD, if any.
    • Second, the loop$ body produces a value acceptable to the loop$ operator, e.g., the body of a SUM loop$ produces a number and the body of an APPEND loop$ produces a true list.

    See lp-section-8 of the Loop$ Primer for some exercises in converting recursive functions to guard verified functions using FOR loop$s rather than recursion (with answers in a Community Book). But remember to come back here when you get to the end of that section.

    Discussion of Why LOOP$s Have Special Guards

    All of the simple loop$ scions have the same guard, namely

    (AND (APPLY$-GUARD FN '(NIL))
         (TRUE-LISTP LST)),

    and all the fancy loop$ scions have the same guard, namely

    (AND (APPLY$-GUARD FN '(NIL NIL))
         (TRUE-LISTP LOOP$-GVARS)
         (TRUE-LIST-LISTP LST)).

    In addition to the normal guard conjectures that would be generated by calls of these scions, ACL2 generates some special guard conjectures because the normal guard conjectures are insufficient to guarantee the error-free execution of the corresponding Common Lisp loop expressions.

    For example, the simplified translation of the body of

    (defun foo (lst)
      (declare (xargs :guard (foo-guardp lst)))
      (loop$ for x of-type (satisfies spec) on lst sum (expr x)))

    is as follows. (One might expect the second argument of the sum$ call to be (tails lst), and of course that is what it equals, logically; the change is to support guards, as discussed in the Technical Note below.)

    (sum$ (lambda$ (loop$-ivar)
                   (declare (type (satisfies spec) loop$-ivar))
                   (expr loop$-ivar))
          (tails (prog2$ (let ((loop$-last-cdr (last-cdr lst)))
                           (declare (type (satisfies spec) loop$-last-cdr))
                           loop$-last-cdr)
                         lst)))

    Prior to the provision for special guards, the normal guard conjectures generated for foo would be

    (and (implies (foo-guardp lst)                                 ; [1]
                  (apply$-guard
                   (lambda$ (loop$-ivar)
                     (expr loop$-ivar))
                   '(nil)))
         (implies (foo-guardp lst)                                 ; [2]
                  (true-listp (tails lst)))
         (implies (spec loop$-ivar) (expr-guardp loop$-ivar)))     ; [3]
         (implies (foo-guardp lst)                                 ; [4]
                  (let ((loop$-last-cdr (last-cdr lst)))
                       (spec loop$-last-cdr)))

    Conjectures [1] and [2] stem from the guard for sum$ and establish that the guard for foo implies that sum$ is passed a function object of one argument and a true-list. Conjecture [3] establishes that the guard on the lambda$ implies the guard of its body. Conjecture [4] says that the final tail of lst (which is nil if lst is a true list; see last-cdr) satisfies spec. At first [4] may be surprising, but inspection of Common Lisp reveals that even though (expr x) is never called on the final tail of lst, implementations running with high safety settings check that the final tail satisfies spec. If you see a runtime guard violation involving variable loop$-last-cdr, you can reasonably assume that you are seeing case [4] above. A guard proof failure involving a call of last-cdr may also be from [4].

    (Technical Note. The production of [4] is accomplished as follows for an expression (loop for var of-type type-spec on lst ...): for the translation to a scion call, the target term, lst, is replaced by the following expression (simplifying slightly).

    (prog2$ (let ((loop$-last-cdr (last-cdr lst)))
              (declare (type type-spec loop$-last-cdr))
              loop$-last-cdr)
            lst)

    The declare form causes formula [4] above to be generated as a guard proof obligation. End of Technical Note.)

    The guard proof obligation shown as [4] above, for an expression (loop$ for ... on ...), has a variant for an expression (loop$ for n from i to j by k ...), where “by k” may be implicit if k is 1. Instead of the requirement in [4] above that the bound variable loop$-last-cdr satisfy the type-spec, variables loop$-lo, loop$-hi, and loop$-by, which are bound respectively to i, j, and k, are required to satisfy the type-spec. A fourth requirement is that the last value tested must also satisfy the type-spec. That last value tested is (+ loop$-lo loop$-by (* loop$-by (floor (- loop$-hi loop$-lo) loop$-by))). If you see a runtime guard violation involving variable loop$-lo, loop$-hi, loop$-by, or loop$-final, you can reasonably assume that you are seeing this variant of [4].

    But consider the raw Lisp loop generated by the loop$ in the raw Lisp definition of foo,

    (loop for x of-type (satisfies spec) on lst sum (expr x)).

    For this loop to execute without error we need to know that [5] every non-empty tail of lst satisfies spec, and [6] every non-empty tail x of lst is such that (expr x) returns a number.

    So when ACL2's guard verification process encounters a sum$ like that in the logical defun of foo, it generates two additional guard conjectures as described above. These are as mentioned above: [5] every element (or tail) satisfies the type-spec, and [6] the value of the loop$ body is acceptable to the loop$ operator.

    (implies (and (warrant ...) ; see below                   ; [5]
                  (foo-guardp lst)
                  (member-equal newv (tails lst)))
             (spec newv))
    
    (implies (and (warrant ...) ; see below                   ; [6]
                  (foo-guardp lst)
                  (member-equal newv (tails lst)))
             (acl2-numberp
              (apply$ (lambda$ (loop$-ivar) (expr loop$-ivar))
                      (list newv))))

    Notice the addition of hypotheses above of the form (warrant ...). ACL2 adds such warrant hypotheses for function symbols that might be apply$'d during evaluation of a scion call (in this case, sum$).

    In general, you may notice that ACL2 generates such ``special'' guard conjectures for all calls of FOR loop$ scions, whether or not they stemmed from uses of loop$. FROM/TO/BY targets require that the bounds and step all satisfy the OF-TYPE specification, and the APPEND operator requires that the loop body generate a true-listp (instead of an ACL2-numberp as required by the SUM operator).

    The Compromise Between Reasoning and Efficiency

    The translation of loop$ expressions into formal terms reflects a compromise between facilitating compositional reasoning and efficient execution.

    One sign of that compromise is our use of scions to handle UNTIL and WHEN clauses. As noted above, by translating

    (loop$ for x in lst until ... when ... collect ...)

    into

    (collect$ ... (when$ ... (until$ ... lst)))

    we're forcing the evaluation of the formal semantics to copy the target twice before collecting. But it gives us the ability to reason compositionally about collect$, when$, and until$. We could have defined a version of collect$ that took three lambda$ expressions, one to terminate the collection, one to filter for the elements we're interested in, and one to transform those elements into the values we wish to collect. This would avoid copying upon evaluation but make it more difficult to reason.

    Another example of compositionality is to consider a simple loop$ over the IN target (append a b). There are 8 different ways you can do a simple COLLECT over an (append a b) target,

    (loop$ for x in (append a b) collect (expr x))
    (loop$ for x on (append a b) collect (expr x))
    (loop$ for x in (append a b) until (stop x) collect (expr x))
    (loop$ for x on (append a b) until (stop x) collect (expr x))
    (loop$ for x in (append a b) when (test x) collect (expr x))
    (loop$ for x on (append a b) when (test x) collect (expr x))
    (loop$ for x in (append a b) until (stop x) when (test x)
           collect (expr x))
    (loop$ for x on (append a b) until (stop x) when (test x)
           collect (expr x))

    Similarly, there are 8 ways to SUM over an (append a b) target, 8 ways to APPEND over an (append a b), and 4 ways each to ALWAYS or THEREIS over an (append a b) target. Thus, there are 32 different simple loop$s over (append a b). And you can arrange to distribute the simple loop$ over the (append a b) with just seven rewrite rules.

    (equal (collect$ fn (append a b))
           (append (collect$ fn a)
                   (collect$ fn b)))
    
    (equal (sum$ fn (append a b))
           (+ (sum$ fn a)
              (sum$ fn b)))
    
    (equal (always$ fn (append a b))
           (and (always$ fn a)
                (always$ fn b)))
    
    (equal (thereis$ fn (append a b))
           (or (thereis$ fn a)
               (thereis$ fn b)))
    
    (equal (append$ fn (append a b))
           (append (append$ fn a)
                   (append$ fn b)))
    
    (equal (until$ fn (append a b))
           (if (exists$ fn a)
               (until$ fn a)
               (append a (until$ fn b))))
    
    (equal (when$ fn (append a b))
           (append (when$ fn a)
                   (when$ fn b)))

    Thus, you can reason about WHEN and UNTIL clauses without having to consider how they are used in the superior loop$ expression.

    To deal with fancy loop$ you need seven more rewrite rules, one for each fancy loop$ scion. But since every simple loop$ can be expressed by an appropriate use of fancy scions, we could have translated every FOR loop$ to fancy scions. We chose to break compositionality here because we think simple loop$s are most common and wanted to keep their semantics simple. I.e., we compromised.

    By the way, if you want the prover to convert every simple scion to its fancy counterpart you could prove rewrite rules like that below.

    (defthm convert-collect$-to-collect$+
      (implies (ok-fnp fn)
               (equal (collect$ fn lst)
                      (collect$+ `(lambda (loop$-gvars loop$-ivars)
                                    (,fn (car loop$-ivars)))
                                 nil
                                 (loop$-as (list lst)))))
      :hints (("[1]Goal"
               :expand ((tamep (cons fn '(x)))
                        (tamep (cons fn '((car loop$-ivars))))))))