[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Issue: EXIT-EXTENT (Version 5)
- To: email@example.com
- Subject: Issue: EXIT-EXTENT (Version 5)
- From: David A. Moon <Moon@STONY-BROOK.SCRC.Symbolics.COM>
- Date: Tue, 13 Dec 88 20:59 EST
- In-reply-to: <881212-103904-4431@Xerox>
I'm sorry I got behind reading the mail on this topic. There are enough
mistakes in this released-to-X3J13 version (even though it is
considerably improved over my last version) that a line by line
commentary seems necessary. Sorry about the length. I've avoided
commenting on typos that don't affect the meaning, except to put -> in
the margin, to save space.
I'd volunteer to fix the writeup except that, as noted below, I can't
think of any implementation-independent way to say what I think the
MEDIUM proposal was intended to say, but does not actually say.
References: CATCH, THROW,
BLOCK, RETURN, RETURN-FROM,
TAGBODY, GO, UNWIND-PROTECT,
Dynamic extent (CLtL p.37),
Nested dynamic extents (CLtL p.38),
Blocks can only be exited once (CLtL p.120),
Catch is disestablished just before the values
are returned (CLtL p.139).
Related issues: UNWIND-PROTECT-NON-LOCAL-EXIT is superseded
by this one.
Edit history: ... Version 5 of UNWIND-PROTECT-NON-LOCAL-EXIT, 23-May-88 ...
Version 1, 5-Sep-88, by Moon, for discussion
Version 2, 1-Oct-88, by Masinter, minor edits
Version 3, 7-Oct-88, by Moon, wording improvements
Version 4, 7-Dec-88, by Masinter, add MEDIUM from
Version 5, 12-Dec-88, Masinter, clarify MINIMAL allows MEDIUM
CLtL does not specify precisely when the dynamic extent (lifetime)
of a nonlocal exit such as a CATCH, BLOCK, or TAGBODY ends.
For example, at what point is it no longer possible to RETURN-FROM
a particular BLOCK?
(Terminology: In this issue writeup, the noun "exit" is
-> refera to the thing that can be exited from, rather than the
act of exiting.
That would be a good idea, but in fact the writeup still uses the word
"exit" to refer both to the door and to the act of walking out the door.
I guess the sentence is technically true, since the latter use is a verb
rather than a noun. It would be better to use only "transfer of control"
to refer to the act of walking out the door.
When the extent of an exit has ended, it is
no longer legal to exit from it. This is different from
the scope of the exit. For example, a BLOCK has lexical
-> scope but dynamic extent; a the scope of a CATCH--the
visibility of the CATCH tag to corresponding THROWs--
could differ from the extent of the CATCH.)
The problem arises when there are nonlocal exits from the
"cleanup" clauses of an UNWIND-PROTECT.
There are three cases of interest:
(1) Normal exit from a CATCH, BLOCK, or TAGBODY, or equivalent such as
PROG. A normal exit occurs when the last form in the body of one of
these constructs completes its evaluation without performing a transfer
(2) Nonlocal exit from the target of a THROW or RETURN. A nonlocal exit
occurs when control is transferred by THROW, RETURN, or RETURN-FROM.
The CATCH or BLOCK named in the THROW, RETURN, or RETURN-FROM is
referred to as the target. The TAGBODY containing the tag named by a
GO is also referred to as the target, but GO differs from the other
nonlocal control transfer operators because GO does not exit its target.
-> For example,
(3) Abandonment of an exit passed over by THROW, RETURN, or GO. A
CATCH, BLOCK, or TAGBODY that is dynamically nested inside the target of
a nonlocal transfer of control is said to be passed over when control is
transferred to the target. The target itself is not said to be passed
For example, in
(when (zilched) (return-from testem nil))
(when (zorked) (throw 'uh-oh))
(format t "Neither zilched nor zorked."))
if (zilched) returns true, the block testem is exited via a
'nonlocal exit'. If (zorked) returns true, the block testem
is 'passed over'. Otherwise, the block is exited normally.
The terms "normal exit", "target", and "passed over" will be used with
these meanings for the remainder of the discussion.
CLtL is unambiguous about case 1. In case 2, the extent could end
anywhere from the time the THROW or RETURN commences, until the time the
transfer of control is completed. In case 3, the extent could end
anywhere from the time the THROW, RETURN, or GO commences, until the
time the transfer of control is completed. In case 2, it is clear that
the extent of the target ends before the transfer of control completes,
since a block cannot be exited twice, but it is not made clear whether
the extent ends before or after execution of UNWIND-PROTECT cleanup
forms. CLtL says nothing about case 3, although a note on p.38 implies
that the extent of a passed-over exit should end no later than the end
of the extent of the target exit. It would make sense for the extent
of an exit passed-over by GO to end no later than when the transfer of
control is completed, but CLtL says nothing about this.
The dynamic extent of an exit, whether target or passed-over, ends as
soon as the THROW, RETURN, or GO commences. In the language of the
implementation note on p.142, the extent ends at the beginning of the
second pass. It is an error for an UNWIND-PROTECT cleanup form executed
during a nonlocal transfer of control to attempt to use an exit whose
dynamic extent ended when the nonlocal transfer of control commenced.
Actually this should just say "It is an error to attempt a transfer of
control to an exit whose dynamic extent has ended." It doesn't really
matter when it ended nor exactly who attempts the transfer of control.
Note that this does not affect the extent of the binding of CATCH
tags; that is, under this proposal, a THROW to a CATCH which was
already in the process of being exited would be an error.
I think the word "extent" in the first line of this paragraph should
have been "scope", but I can see how reasonable people might disagree.
A couple of places later in the writeup use "scope" in that way in
connection with CATCH, though.
This proposal is called "minimal" because it gives exits the smallest
extent consistent with CLtL. A program that presumed a longer extent
would be in error. Implementations may support longer extents for
exits than is required by this proposal; in particular, an
implementation which allowed the larger extent of the MEDIUM
proposal below would still conform.
The dynamic extent of an exit, whether target or passed-over, ends
only after the exit is complete.
I doubt that that is what you intended to say. For example,
(let ((f nil))
(flet ((g () (return-from two 2)))
(setq f #'g)
(return-from one 1)))
Under MINIMAL, this is an error because the block named two is passed
over by the return-from one before the return-from two is executed.
Under MEDIUM, this is not an error because at the time of the funcall f,
the exit is not complete and so the dynamic extent of the block named
two has not yet ended. It either returns 2 or goes into an infinite
loop, depending on what it means to exit twice out of an UNWIND-PROTECT.
Probably this intended to say something about how the dynamic extent of
a passed-over exit ends when control reaches a frame that was
established before the exit was established. I don't know how to say
that in an implementation-independent way. This difficulty in defining
a clear semantics for passed-over exits is exactly why I have always
favored MINIMAL, which constrains portable programs maximally and
constrains implementations minimally (which allows us to say as little
as possible about the implementation).
I think we must only vote on what proposals actually say, not on what we
guess they might have been intended to say. We can of course amend them
to say something different and then vote on them.
A transfer of control from within an UNWIND-PROTECT cleanup form
to a point outside of the UNWIND-PROTECT causes the original control
transfer which initiated the execution of the cleanup forms to be
During the execution of the cleanup forms of an UNWIND-PROTECT a
non-local exit to a point outside of the scope of the UNWIND-PROTECT,
-> but still within the dynamic scope of of the target of the original
non-local exit succeeds, and the original pending exit is discarded.
Where an UNWIND-PROTECT cleanup form attempts a non-local exit to a
point outside the original non-local exit, control is passed to the
outer exit (and the pending original non-local exit is discarded.)
In no case will UNWIND-PROTECT cleanup forms ever be attempted more
This can't be true, since everyone agrees that
(loop (print 1)))
prints 1 more than once. Also if the UNWIND-PROTECT is entered more
than once, it cleanup forms can of course be called more than once.
I think I know what you intended to say, but that isn't what you
actually said. I'm not sure why this needs to be in the proposal at all
once the problem I pointed out above is fixed, so maybe it would be
simpler just to remove it.
-> Each of the following programs are an error under either
;; Error: BLOCK has normal exit before RETURN
(funcall (block nil #'(lambda () (return))))
;; Error: normal exit before GO
(let ((a nil))
(tagbody t (setq a #'(lambda () (go t))))
;; Error: TAGBODY is passed over, before GO
(funcall (block nil
(tagbody a (return #'(lambda () (go a))))))
-> Each of these programs are an error under MINIMAL, but
not under MEDIUM:
;;returns 2 under MEDIUM, is error under MINIMAL
(unwind-protect (return 1)
;;returns 2 under MEDIUM, is error under MINIMAL
(unwind-protect (return-from a 1)
(return-from b 2))))
;; returns 2 under MEDIUM, is error under MINIMAL
(unwind-protect (throw nil 1)
(throw nil 2)))
;; returns 2 under MEDIUM, is error under MINIMAL
(unwind-protect (throw 'a 1)
(throw 'b 2))))
;; An error under MINIMAL because the catch of b is passed over by
;; the first throw, hence portable programs must assume its dynamic extent
;; is terminated. The catch is not yet disestablished and therefore it
;; is the target of the second throw.
;; the following is an error under MINIMAL; the extent of the
;; inner catch terminates as soon as the throw commences, even
;; though it remains in scope. Thus, the throw of :second-throw
;; sees the inner catch, but its extent has ended.
;; under MEDIUM, it prints "The inner catch returns :second-throw"
;; and then returns :outer-catch.
(format t "The inner catch returns ~s.~%"
(unwind-protect (throw 'foo :first-throw)
(throw 'foo :second-throw))))
The following program is not an error. It returns 10. The inner
catch of a is passed over, but this is not case 3 because that catch
is disestablished before the throw to a is executed.
(unwind-protect (1+ (catch 'a (throw 'b 1)))
(throw 'a 10))))
The way MEDIUM is actually written, this seems to return 11 under
MEDIUM, because the throw to a goes to the inner catch. But I think you
intended for it to return 10.
The following cases are errors under MINIMAL, and have
the following interpretation under MEDIUM:
The second case is not an error under MINIMAL. It behaves
identically in MINIMAL and MEDIUM.
(UNWIND-PROTECT (THROW 'FOO 3)
(THROW 'BAR 4)
the pending exit to tag FOO is discarded by the second THROW
to BAR and the value 4 is transfered to (CATCH 'BAR ...),
which returns 4. The (CATCH 'FOO ...) then returns the 4
because its first argument has returned normally.
XXX is not printed.
(UNWIND-PROTECT (THROW 'FOO 3)
(THROW 'BAR 4)
the value 4 is returned from the (CATCH 'BAR ...); XXX is not printed.
. 3 evaluates to itself and is saved by THROW which begins
searching for tag FOO.
. 4 evaluates to iself and is saved by THROW which begins
searching for tag BAR.
. It is not an error, even though the
BAR tag is not found within the local dynamic scope of
I don't know what a "local dynamic scope" is.
the UNWIND-PROTECT cleanup form containing (THROW 'BAR 4)
but is found outside the scope of the target of the
pending THROW to FOO.
For MINIMAL: Giving exits the smallest extent consistent with CLtL
maximizes freedom for implementations; there are few applications,
if any, that require a longer extent.
-> For MEDIUM: Giving exits a longer exent has cleaner semantics.
Both implementations of Symbolics Genera (3600 and Ivory) end the extent
of a target block or catch at the moment the values are returned, and
end the extent of a passed-over exit at the moment the THROW, RETURN, or
GO commences. This choice of extent maximizes efficiency within the
particular stack structure used by these implementations, by avoiding
the need to retain the control information needed to use a passed over
exit through the transfer of control. Genera signals an error if an
attempt is made to use an exit that has been passed over.
In some implementations, the extent of a target exit lasts until the
exit has been completed; in those implementations, it is possible for a
throw or non-local exit to be effectively "stopped" by an UNWIND-PROTECT
cleanup clause that performs a nonlocal transfer of control to a
Some implementations crash or otherwise generate garbage code for
non-local exits from cleanup clauses of UNWIND-PROTECT.
Cost to Implementors:
No currently valid implementation will be made invalid by the MINIMAL
proposal. Some implementors may wish to add error checks if they
do not already have them.
MEDIUM would have a high cost for those implementations that currently
have shorter exent.
Cost to Users:
Most user programs don't do this, so there is likely little cost
of converting existing code in any case. In any case, current implementations
differ enough that this issue ostensibly does not
affect current portable programs. Some users might have code that
relies on the "unstoppable loops" that can be created with the MEDIUM
Either proposal would make Common Lisp more precisely defined.
Cost of non-adoption :
The semantics of exits will remain ambiguous.
Precisely specifying the meaning of dynamic extent improves the language.
Leaving implementations free to implement a longer extent if they choose
can be regarded as unesthetic, but consistent with Common Lisp philosophy.
Having a CATCH that is in scope even though its extent has ended may
seem unesthetic, but it is consistent with how BLOCK behaves.
This issue is controversial. It was first discussed under the issue
named UNWIND-PROTECT-CLEANUP-NON-LOCAL-EXIT. The issue was recast as
the more global one of "extent of exits" rather than the specific
one of "what happens if a cleanup in an UNWIND-PROTECT does a non-
local exit", but the problem cases for both topics are the same.
The goal of the MINIMAL proposal is to clarify the ambiguity in CLtL while
minimizing changes to the current situation. The MEDIUM proposal
defines the extent of an exit to end at the last moment possible
within some particular reference implementation. It has
a cost to implementors whose implementation is not identical to the
reference implementation. Another alternative proposal, not considered
here, would duck the issue by outlawing all nonlocal exits from UNWIND-PROTECT
cleanup forms. That alternative would have a substantial cost to some users.
Scheme is cleaner: it avoids this issue by specifying that the extent
of an exit never ends.
An argument for the MEDIUM proposal was made based on the example:
(return-from foo 'foo)
(return-from bar 'bar))))
Since there is no reason for FOO and BAR not to be treated interchangably,
calling this an error would be inappropriate.
This is no argument against the MINIMAL proposal. Suppose FOO and BAR
are to be treated interchangeably. Then the above example should be
(return-from foo 'foo)
(return-from foo 'bar)))
In fact these two examples are equivalent under both proposals. Under
MEDIUM they both return BAR. Under MINIMAL they are both errors.
It was argued that the MINIMAL proposal is equivalent to practically
outlawing non-local exits from UNWIND-PROTECT cleanup clauses, because
there is no general way to determine the target of the nonlocal exit
that caused the cleanup clause to be invoked.
CLtL never says in what dynamic environment cleanup forms of
UNWIND-PROTECT are executed. The implementation note on p.142 may have
been intended to cover this, but since it doesn't define the term
"frame" that it uses, it doesn't actually say anything. The extent of
dynamic-extent entities other than exits should be the
subject of a separate proposal. It was argued that the likely
resolution of those issues would be more consistent with the
MEDIUM proposal than MINIMAL.
The following example was offered as an argument against MINIMAL. Given:
(error "foo")) ;probably an error, under the proposal
If the ERROR handler has the same scope and extent a CATCH in the same place
would have (and that seems reasonable, though I'm not certain that the
condition system specifically requires that interpretation), then the handler
will be apparent to the call to ERROR, but will no longer be a valid target
(its extent was exited by the RETURN in the UNWIND-PROTECT body).
"exited" in the preceding line should be "ended" or "passed over".
This is true, but interchanging the first two lines of the example would fix it.
It is quite intentional that the MINIMAL proposal says this style of coding
is non-portable. In current practice it is non-portable.