[This is from the ACL2 source file other-events.lisp, as modified for support of nested stobjs. I've removed the leading "; " at the beginning of each line.] ..... Essay on Nested Stobjs After Version_6.1 we introduced a new capability: allowing fields of stobjs to themselves be stobjs or arrays of stobjs. Initially we resisted this idea because of an aliasing problem, which we review now, as it is fundamental to understanding our implementation. Consider the following events. (defstobj st fld) (defstobj st2 (fld2 :type st)) Now suppose we could evaluate the following code, to be run immediately after admitting the two defstobj events above. (let* ((st (fld2 st2)) (st (update-fld 3 st))) (mv st st2)) A reasonable raw-Lisp implementation of nested stobjs, using destructive updates, could be expected to have the property that for the returned st and st2, st = (fld2 st2) and thus (fld (fld2 st2)) = (fld st) = 3. However, under an applicative semantics, st2 has not changed and thus, logically, it follows that (fld (fld2 st2)) has its original value of nil, not 3. In summary, a change to st can cause a logically-inexplicable change to st2. But this problem can also happen in reverse: a change to st2 can cause a logically-inexplicable change to st. Consider evaluation of the following code, to be run immediately after admitting the two defstobj events above. (let ((st2 (let* ((st (fld2 st2)) (st (update-fld 3 st))) (update-fld2 st st2)))) (mv st st2)) With destructive updates in raw Lisp, we expect that st = (fld2 st2) for the returned st and st2, and thus (fld st) = (fld (fld2 st2)) = 3. But logically, the returned st is as initially created, and hence (fld st) = nil. One can imagine other kinds of aliasing problems; imagine putting a single stobj into two different slots of a parent stobj. Therefore, we carefully control access to stobj fields of stobjs by introducing a new construct, stobj-let. Consider for example the following events. (defstobj st1 ...) (defstobj st2 ...) (defstobj st3 ...) (defstobj st+ (fld1 :type st1) (fld2 :type st2) (fld3 :type (array st3 (8)))) If producer and consumer are functions, then we can write the following form. Note that stobj-let takes four "arguments": bindings, producer variables, a producer form, and a consumer form. (stobj-let ((st1 (fld1 st+)) (st2 (fld2 st+)) (st3 (fld3i 4 st+))) (x st1 y st3 ...) ; producer variables (producer st1 st2 st3 ...) (consumer st+ x y ...)) Updater names need to be supplied if not the default. Thus, the form above is equivalent to the following. (stobj-let ((st1 (fld1 st+) update-fld1) (st2 (fld2 st+) update-fld2) (st3 (fld3i 4 st+) update-fld3i)) (x st1 y st3 ...) ; producer variables (producer st1 st2 st3 ...) (consumer st+ x y ...)) The form above expands as follows in the logic (or at least, essentially so). The point is that we avoid the aliasing problem: there is no direct access to the parent stobj when running the producer, which is updated to stay in sync with updates to the child stobjs; and there is no direct access to the child stobjs when running the consumer. Note that since st2 is not among the producer variables, fld2 is not updated. (let ((st1 (fld1 st+)) (st2 (fld2 st+)) (st3 (fld3i 4 st+))) (declare (ignorable st1 st2 st3)) ; since user has no way to do this (mv-let (x st1 y st3 ...) ; producer variables (check-vars-not-free (st+) (producer st1 st2 st3 ...)) (let* ((st+ (update-fld1 st1 st+)) (st+ (update-fld3i 4 st3 st+))) (check-vars-not-free (st1 st2 st3) (consumer st+ x y ...))))) We consider next whether the use of check-vars-not-free truly prevents access to a stobj named in its variable list (first argument). For example, if