RETURN-LAST.html -- ACL2 Version 6.3

### RETURN-LAST

return the last argument, perhaps with side effects

    Major Section:  ACL2-BUILT-INS

`Return-last` is an ACL2 function that returns its last argument. It is used to implement common utilities such as `prog2$` and `time$`. For most users, this may already be more than one needs to know about `return-last`; for example, ACL2 tends to avoid printing calls of `return-last` in its output, printing calls of `prog2$` or `time$` (or other such utilities) instead.

If you encounter a call of `return-last` during a proof, then you may find it most useful to consider `return-last` simply as a function defined by the following equation.

    (equal (return-last x y z) z)

It may also be useful to know that unlike other ACL2 functions, `return-last` can take a multiple value as its last argument, in which case this multiple value is returned. The following contrived definition illustrates this point.

    ACL2 !>(defun foo (fn x y z)
             (return-last fn x (mv y z)))

    Since FOO is non-recursive, its admission is trivial.  We observe that
    the type of FOO is described by the theorem
    (AND (CONSP (FOO FN X Y Z)) (TRUE-LISTP (FOO FN X Y Z))).  We used
    primitive type reasoning.

    (FOO * * * *) => (MV * *).

    Summary
    Form:  ( DEFUN FOO ...)
    Rules: ((:FAKE-RUNE-FOR-TYPE-SET NIL))
    Time:  0.01 seconds (prove: 0.00, print: 0.00, other: 0.01)
     FOO
    ACL2 !>(foo 'bar 3 4 5)
    (4 5)
    ACL2 !>(mv-let (a b)
                   (foo 'bar 3 4 5)
                   (cons b a))
    (5 . 4)
    ACL2 !>

Most readers would be well served to avoid reading the rest of this documentation of `return-last`. For reference, however, below we document it in some detail. We include some discussion of its evaluation, in particular its behavior in raw Lisp, because we expect that most who read further are working with raw Lisp code (and trust tags).

`Return-last` is an ACL2 function that can arise from macroexpansion of certain utilities that return their last argument, which may be a multiple value. Consider for example the simplest of these, `prog2$`:

    ACL2 !>:trans1 (prog2$ (cw "Some CW printing...~%") (+ 3 4))
     (RETURN-LAST 'PROGN
                  (CW "Some CW printing...~%")
                  (+ 3 4))
    ACL2 !>

Notice that a call of `prog2$` macroexpands to a call of `return-last` in which the first argument is `(quote progn)`. Although `return-last` is a function in the ACL2 world, it is implemented ``under the hood'' as a macro in raw Lisp, as the following log (continuing the example above) illustrates.

    ACL2 !>:q

    Exiting the ACL2 read-eval-print loop.  To re-enter, execute (LP).
    ? [RAW LISP] (macroexpand-1 '(RETURN-LAST 'PROGN
                                               (CW "Some CW printing...~%")
                                               (+ 3 4)))
    (PROGN (LET ((*AOKP* T)) (CW "SOME CW PRINTING...~%")) (+ 3 4))
    T
    ? [RAW LISP]

Thus, the original `prog2$` call generates a corresponding call of `progn` in raw Lisp, which in turn causes evaluation of each argument and returns whatever is returned by evaluation of the last (second) argument.

(Remark for those who use `defattach`. The binding of `*aokp*` to `t` is always included for the second argument as shown except when the first argument is of the form `(QUOTE M)` where `M` is a macro, or (less important) when the first argument is a symbol or a cons whose car is `QUOTE`. This binding allows ACL2 to use attachments in the second argument of `return-last` (hence, in the first argument of `prog2$`), even in contexts such as proofs in which attachments are normally not allowed. Those who use the experimental HONS version of ACL2 (see hons-and-memoization) will see an additional binding in the above single-step macroexpansion, which allows the storing of memoized results even when that would otherwise be prevented because of the use of attachments.)

In general, a form `(return-last (quote F) X Y)` macroexpands to `(F X Y)`, where `F` is defined in raw Lisp to return its last argument. The case that `F` is `progn` is a bit misleading, because it is so simple. More commonly, macroexpansion produces a call of a macro defined in raw Lisp that may produce side effects. Consider for example the ACL2 utility `with-guard-checking`, which is intended to change the guard-checking mode to the indicated value (see with-guard-checking).

    ACL2 !>(with-guard-checking :none (car 3)) ; no guard violation
    NIL
    ACL2 !>:trans1 (with-guard-checking :none (car 3))
     (WITH-GUARD-CHECKING1 (CHK-WITH-GUARD-CHECKING-ARG :NONE)
                           (CAR 3))
    ACL2 !>:trans1 (WITH-GUARD-CHECKING1 (CHK-WITH-GUARD-CHECKING-ARG :NONE)
                                         (CAR 3))
     (RETURN-LAST 'WITH-GUARD-CHECKING1-RAW
                  (CHK-WITH-GUARD-CHECKING-ARG :NONE)
                  (CAR 3))
    ACL2 !>:q

    Exiting the ACL2 read-eval-print loop.  To re-enter, execute (LP).
    ? [RAW LISP] (macroexpand-1
                  '(RETURN-LAST 'WITH-GUARD-CHECKING1-RAW
                                 (CHK-WITH-GUARD-CHECKING-ARG :NONE)
                                 (CAR 3)))
    (WITH-GUARD-CHECKING1-RAW (CHK-WITH-GUARD-CHECKING-ARG :NONE) (CAR 3))
    T
    ? [RAW LISP] (pprint
                  (macroexpand-1
                   '(WITH-GUARD-CHECKING1-RAW
                     (CHK-WITH-GUARD-CHECKING-ARG :NONE)
                     (CAR 3))))

    (LET ((ACL2_GLOBAL_ACL2::GUARD-CHECKING-ON
           (CHK-WITH-GUARD-CHECKING-ARG :NONE)))
      (DECLARE (SPECIAL ACL2_GLOBAL_ACL2::GUARD-CHECKING-ON))
      (CAR 3))
    ? [RAW LISP]

The above raw Lisp code binds the state global variable `guard-checking-on` to `:none`, as `chk-with-guard-checking-arg` is just the identity function except for causing a hard error for an illegal input.

The intended use of `return-last` is that the second argument is evaluated first in a normal manner, and then the third argument is evaluated in an environment that may depend on the value of the second argument. (For example, the macro `with-prover-time-limit` macroexpands to a call of `return-last` with a first argument of `'WITH-PROVER-TIME-LIMIT1-RAW`, a second argument that evaluates to a numeric time limit, and a third argument that is evaluated in an environment where the theorem prover is restricted to avoid running longer than that time limit.) Although this intended usage model is not strictly enforced, it is useful to keep in mind in the following description of how calls of `return-last` are handled by the ACL2 evaluator.

When a form is submitted in the top-level loop, it is handled by ACL2's custom evaluator. That evaluator is specified to respect the semantics of the expression supplied to it: briefly put, if an expression `E` evaluates to a value `V`, then the equality `(equal E (quote V))` should be a theorem. Notice that this specification does not discuss the side-effects that may occur when evaluating a call of `return-last`, so we discuss that now. Suppose that the ACL2 evaluator encounters the call `(return-last 'fn expr1 expr2)`. First it evaluates `expr1`. If this evaluation succeeds without error, then it constructs an expression of the form `(fn *x* ev-form)`, where *x* is a Lisp variable bound to the result of evaluating `expr1` and `ev-form` is a call of the evaluator for `expr2`. (Those who want implementation details are invited to look at function `ev-rec-return-last` in ACL2 source file `translate.lisp`.) There are exceptions if `fn` is `progn`, `ec-call1-raw`, `with-guard-checking1-raw`, or `mbe1-raw`, but the main idea is the same: do a reasonable job emulating the behavior of a raw-Lisp call of `return-last`.

The following log shows how a `time$` call can generate a call of the evaluator for the last argument of `return-last` (arguent `expr2`, above). We use `:``trans1` to show single-step macroexpansions, which indicate how a call of `time$` expands to a call of `return-last`. The implementation actually binds the Lisp variable `*RETURN-LAST-ARG3*` to `expr2` before calling the ACL2 evaluator, `ev-rec`.

    ACL2 !>:trans1 (time$ (+ 3 4))
     (TIME$1 (LIST 0 NIL NIL NIL NIL)
             (+ 3 4))
    ACL2 !>:trans1 (TIME$1 (LIST 0 NIL NIL NIL NIL)
                           (+ 3 4))
     (RETURN-LAST 'TIME$1-RAW
                  (LIST 0 NIL NIL NIL NIL)
                  (+ 3 4))
    ACL2 !>(time$ (+ 3 4))
    ; (EV-REC *RETURN-LAST-ARG3* ...) took
    ; 0.00 seconds realtime, 0.00 seconds runtime
    ; (1,120 bytes allocated).
    7
    ACL2 !>

We now show how things can go wrong in other than the ``intended use'' case described above. In the example below, the macro `mac-raw` is operating directly on the syntactic representation of its first argument, which it obtains of course as the second argument of a `return-last` call. Again this ``intended use'' of `return-last` requires that argument to be evaluated and then only its result is relevant; its syntax is not supposed to matter. We emphasize that only top-level evaluation depends on this ``intended use''; once evaluation is passed to Lisp, the issue disappears. We illustrate below how to use the `top-level` utility to avoid this issue; see top-level. The example uses the utility `defmacro-last` to ``install'' special handling of the raw-Lisp macro `mac-raw` by `return-last`; later below we discuss `defmacro-last`.

    ACL2 !>(defttag t)

    TTAG NOTE: Adding ttag :T from the top level loop.
     T
    ACL2 !>(progn!
             (set-raw-mode t)
             (defmacro mac-raw (x y)
               `(progn (print (quote ,(cadr x)))
                       (terpri) ; newline
                       ,y)))

    Summary
    Form:  ( PROGN! (SET-RAW-MODE T) ...)
    Rules: NIL
    Time:  0.01 seconds (prove: 0.00, print: 0.00, other: 0.01)
     NIL
    ACL2 !>(defmacro-last mac)
    [[ ... output omitted ... ]]
     RETURN-LAST-TABLE
    ACL2 !>(return-last 'mac-raw '3 nil)

    ***********************************************
    ************ ABORTING from raw Lisp ***********
    Error:  Fault during read of memory address #x120000300006
    ***********************************************

    If you didn't cause an explicit interrupt (Control-C),
    then the root cause may be call of a :program mode
    function that has the wrong guard specified, or even no
    guard specified (i.e., an implicit guard of t).
    See :DOC guards.

    To enable breaks into the debugger (also see :DOC acl2-customization):
    (SET-DEBUGGER-ENABLE T)
    ACL2 !>(top-level (return-last 'mac-raw '3 nil))

    3
    NIL
    ACL2 !>

We next describe how to extend the behavior of `return-last`. This requires an active trust tag (see defttag), and is accomplished by extending a table provided by ACL2, see return-last-table. Rather than using `table` events directly for this purpose, it is probably more convenient to use a macro, `defmacro-last`. We describe the community book `books/misc/profiling.lisp` in order to illustrate how this works. The events in that book include the following, which are described below.

    (defttag :profiling)

    (progn!
     (set-raw-mode t)
     (load (concatenate 'string (cbd) "profiling-raw.lsp")))

    (defmacro-last with-profiling)

The first event introduces a trust tag. The second loads a file into raw Lisp that defines a macro, `with-profiling-raw`, which can do profiling for the form to be evaluated. The third introduces an ACL2 macro `with-profiling`, whose calls expand into calls of the form `(return-last (quote with-profiling-raw) & &)`. The third event also extends `return-last-table` so that these calls will expand in raw Lisp to calls of `with-profiling-raw`.

The example above illustrates the following methodology for introducing a macro that returns its last argument but produces useful side-effects with raw Lisp code.

> (1) Introduce a trust tag (see defttag).

> (2) Using `progn!`, load into raw Lisp a file defining a macro, `RAW-NAME`, that takes two arguments, returning its last (second) argument but using the first argument to produce desired side effects during evaluation of that last argument.

> (3) Evaluate the form `(defmacro-last NAME :raw RAW-NAME)`. This will introduce `NAME` as an ACL2 macro that expands to a corresponding call of `RAW-NAME` (see (2) above) in raw Lisp. The specification of keyword value of `:raw` as `RAW-NAME` may be omitted if `RAW-NAME` is the result of modifying the symbol `NAME` by suffixing the string `"-RAW"` to the `symbol-name` of `NAME`.

WARNING: Not every use of `return-last` can be soundly evaluated outside a function body. The reason is that ACL2's evaluator, `ev-rec`, recurs through terms that are presented in the top-level loop, and handles `return-last` calls in a special manner: basically, the call of `ev-rec` on the form `(return-last 'mac-raw x y)` leads to evaluation of a macro call of the form `(mac-raw *return-last-arg2* (ev-rec ...))`, where *return-last-arg2* is a global variable bound to the result of evaluating `x` with `ev-rec`. Consider the following example.

    (defttag t)
    (set-raw-mode-on state)
    (defmacro mac-raw (str y) ; print message is an atom
     `(let ((result (consp ,y))
            (str ,str))
        (or result
            (prog2$ (fmx ,str ',y)
                    nil))))
    (set-raw-mode-off state)
    (defmacro-last mac)
    ; Horrible error:
    (mac "Not a cons: ~x0~%" 17)
    ; Works, but probably many would consider it awkward to use top-level:
    (top-level (mac "Not a cons: ~x0~%" 17))

In such cases we suggest supplying keyword `:top-level-ok nil` to the call of `defmacro-last`, for example:

    (defmacro-last mac :top-level-ok nil)

Then any attempt to call `mac` at the top level, as opposed to inside a function body, will cause a clean error before evaluation begins.

It is useful to explore what is done by `defmacro-last`.

    ACL2 !>:trans1 (defmacro-last with-profiling)
     (PROGN (DEFMACRO WITH-PROFILING (X Y)
                      (LIST 'RETURN-LAST
                            (LIST 'QUOTE 'WITH-PROFILING-RAW)
                            X Y))
            (TABLE RETURN-LAST-TABLE 'WITH-PROFILING-RAW
                   'WITH-PROFILING))
    ACL2 !>:trans1 (with-profiling '(assoc-eq fgetprop rewrite) (mini-proveall))
     (RETURN-LAST 'WITH-PROFILING-RAW
                  '(ASSOC-EQ FGETPROP REWRITE)
                  (MINI-PROVEALL))
    ACL2 !>:q

    Exiting the ACL2 read-eval-print loop.  To re-enter, execute (LP).
    ? [RAW LISP] (macroexpand-1
                  '(RETURN-LAST 'WITH-PROFILING-RAW
                                 '(ASSOC-EQ FGETPROP REWRITE)
                                 (MINI-PROVEALL)))
    (WITH-PROFILING-RAW '(ASSOC-EQ FGETPROP REWRITE) (MINI-PROVEALL))
    T
    ? [RAW LISP]

To understand the macro `with-profiling-raw` you could look at the community book loaded above: `books/misc/profiling-raw.lsp`.

We mentioned above that ACL2 tends to print calls of `prog2$` or `time$` (or other such utilities) instead of calls of `return-last`. Here we elaborate that point. ACL2's ``untranslate`' utility treats `(return-last (quote F) X Y)` as `(G X Y)` if `F` corresponds to the symbol `G` in `return-last-table`. However, it is generally rare to encounter such a term during a proof, since calls of `return-last` are generally expanded away early during a proof.

Calls of `return-last` that occur in code -- forms submitted in the top-level ACL2 loop, and definition bodies other than those marked as `non-executable` (see defun-nx) -- have the following restriction: if the first argument is of the form `(quote F)`, then `F` must be an entry in `return-last-table`. There are however four exceptions: the following symbols are considered to be keys of `return-last-table` even if they are no longer associated with non-`nil` values, say because of a `table` event with keyword `:clear`.

> * `progn`, associated with `prog2$`
> * `mbe1-raw`, associated with `mbe1`, a version of `mbe`
> * `ec-call1-raw`, associated with `ec-call1` (a variant of `ec-call`)
> * `with-guard-checking1-raw`, associated with `with-guard-checking1` (a variant of `with-guard-checking`)

Note that because of its special status, it is illegal to trace `return-last`.

We conclude by warning that as a user, you take responsibility for not compromising the soundness or error handling of ACL2 when you define a macro in raw Lisp and especially when you install it as a key of `return-last-table`, either directly or (more likely) using `defmacro-last`. In particular, be sure that you are defining a macro of two arguments that always returns the value of its last argument, returning the complete multiple value if that last argument evaluates to a multiple value.

The following is correct, and illustrates care taken to return multiple values.

    :q
    (defmacro my-time1-raw (val form)
      (declare (ignore val))
      `(let  ((start-time (get-internal-run-time))
              (result (multiple-value-list ,form))
              (end-time (get-internal-run-time)))
         (format t "Total time: ~s~%"
                 (float (/ (- end-time start-time)
                           internal-time-units-per-second)))
         (values-list result)))
    (lp)
    (defttag t)
    (defmacro-last my-time1)
    (defmacro my-time (form)
      `(my-time1 nil ,form))

Then for example:

    ACL2 !>(my-time (equal (make-list 1000000) (make-list 1000000)))
    Total time: 0.12
    T
    ACL2 !>

But if instead we provide the following more naive implementation, of the above raw Lisp macro, the above evaluation can produce an error, for example if the host Lisp is CCL.

    (defmacro my-time1-raw (val form)
        (declare (ignore val))
        `(let  ((start-time (get-internal-run-time))
                (result ,form)
                (end-time (get-internal-run-time)))
           (format t "Total time: ~s~%"
                   (float (/ (- end-time start-time)
                             internal-time-units-per-second)))
           result)) ; WRONG -- need multiple values returned!

Here is a second, similar example. This time we'll start with the error; can you spot it?

    (defttag t)
    (progn!
     (set-raw-mode t)
     (defmacro foo-raw (x y)
       `(prog1
            ,y
          (cw "Message showing argument 1: ~x0~%" ,x))))
    (defmacro-last foo)

We then can wind up with a hard Lisp error:

    ACL2 !>(foo 3 (mv 4 5))
    Message showing argument 1: 3

    ***********************************************
    ************ ABORTING from raw Lisp ***********
    Error:  value NIL is not of the expected type REAL.
    ***********************************************

    If you didn't cause an explicit interrupt (Control-C),
    then the root cause may be call of a :program mode
    function that has the wrong guard specified, or even no
    guard specified (i.e., an implicit guard of t).
    See :DOC guards.

    To enable breaks into the debugger (also see :DOC acl2-customization):
    (SET-DEBUGGER-ENABLE T)
    ACL2 !>

Here is a corrected version of the above macro. The point here is that `prog1` returns a single value, while `our-multiple-value-prog1` returns all the values that are returned by its first argument.

    (progn!
     (set-raw-mode t)
     (defmacro foo-raw (x y)
       `(our-multiple-value-prog1 ;; better
         ,y
         (cw "Message showing argument 1: ~x0~%" ,x))))

To see the ACL2 definition of this function, see pf.