From: Steven M. Haflich
Subject: pprint-logical-block and non-local exits
Date: 
Message-ID: <3B8B5B42.C061ADE8@pacbell.net>
This is a matter probably of interest only to fellow language lawyers.

Consider the following ANS CL code.  There is little question what
it prints, ignoring the possibility of odd values of various other
*print-mumbl* special variables:

> (let ((*print-right-margin* 79)
	(l (loop for i below 100 collect i)))
    (block foo
      (pprint-logical-block (*standard-output* l :prefix "(" :suffix ")")
	(pprint-exit-if-list-exhausted)
	(loop as x = (pprint-pop)
	    do (princ x *standard-output*)
	       (pprint-exit-if-list-exhausted)
	       (write-char #\space *standard-output*)
	       (pprint-newline :fill *standard-output*)))))
(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99)
nil

But now consider the following, which adds the one flagged line:

> (let ((*print-right-margin* 79)
	(l (loop for i below 100 collect i)))
    (block foo
      (pprint-logical-block (*standard-output* l :prefix "(" :suffix ")")
	(pprint-exit-if-list-exhausted)
	(loop as x = (pprint-pop)
	    do (princ x *standard-output*)
	       (pprint-exit-if-list-exhausted)
	       (when (eql x 50) (return-from foo))	; <<<<<<<<<<<<<
	       (write-char #\space *standard-output*)
	       (pprint-newline :fill *standard-output*)))))
(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
nil

I operates identically in the two implementations I checked, and that
result doesn't surprise me, being the author of the pretty printer code
in one of them (which code was heavily dependent on the XP prototype code
contriuted to X3J13 by Dick Waters).  The ANS does not specifically address
what happens if nonlocal exit is made from the body of a pprint-logical-block.

However, the reason the issue occurred to me is that I had innocently
written code like the following:

(defun foo (x s)
  ...
     (pprint-logical-block (...)
       (when (frumblep x)
         (format s ...)
         (return-from foo nil)))
     ...))

In other words, even being exquisitely familiar with the pretty printer,
I nonetheless naively wrote code that assumed that these unrelated
components of the language (pp-l-b and nonlocal exit) would cooperate to
DWIM.  Silly me.  No language is perfect.  In any case like this it would
be straighforward to recast the bogus code as a then-else conditional
without the return-from.

But next I wondered whether this requirement in the ANS would have been a
good thing (that pp-l-b would catch nonlocal exits, presumably using
unwind-protect, in order to guarantee that necessary whitespace and
logical-block suffix were printed).  In other words, should the expansion
of pp-l-b have an unwind-protect, similar to with-open-stream?

There is an obvious efficiency issue, of course, but I would first prefer
to consider the question abstractly, as a language aesthetic issue.  (This
isn't the same thing as considering whether the ANS does or does not
specify one behavior or the other.  That's interesting too, of course, but
it isn't the question I'm asking right now.)  In a proper implementation
unwind-protect is not as expensive as one might think, but there is also a
valid consideration how error recovery would ever gracefully exit a
nested pprint if, say, the output stream has developed some recalcitrant
error such as a broken socket.

Naturally, I'd likely (but perhaps reluctantly) side with the efficiency
twerps who would not want to slow down the pretty printer with the required
wrapping of all these unwind-protects just to ensure orthogonality of
orthogonal language features.  The corrent treatment of nonlocal exit
is not guaranteed for pp-l-b by the ANS, therefore programmers have no
right to expect it.

But then I happened to look at the original Dick Waters XP code:

       (block logical-block
	 (start-block ,var ,prefix ,per-line? ,suffix)
!!>>>	 (unwind-protect
	   (macrolet ((pprint-pop () `(pprint-pop+ ,',args ,',var))
		      (pprint-exit-if-list-exhausted ()
			`(if (null ,',args) (return-from logical-block nil))))
	     ,@ body)
	   (end-block ,var ,suffix)))

The ACL code is identical except for the flagged unwind-protect.  Probably
I was the one who removed it ("for efficiency reasons" although I have no
recollection of doing so) more than a decade ago.  But other implementations
apparently did the same.

There is little question in my mind that the ANS does not require the
unwind-protect.  (On the other hand, it is equally hard to argue that it
prohibits it.  So arguably the behavior is undefined.)  But it is just as
clear that the original Waters prototype intended that pp-l-b be robust
in face of non-local exit from the body.

I'm interested if any other language lawyers have opinions on this,
preferably from the perspective of proper language design, not (for now)
merely trying to discern what the ANS requires.

======
"The ANS for CL is the Talmud for the Third Millenium!"
Steven M. Haflich
···@alum.mit.edu
From: Kent M Pitman
Subject: Re: pprint-logical-block and non-local exits
Date: 
Message-ID: <sfwzo8klcmz.fsf@world.std.com>
"Steven M. Haflich" <·······@pacbell.net> writes:

> This is a matter probably of interest only to fellow language lawyers.
> 
> Consider the following ANS CL code.  There is little question what
> it prints, ignoring the possibility of odd values of various other
> *print-mumbl* special variables:
> 
> > (let ((*print-right-margin* 79)
> 	(l (loop for i below 100 collect i)))
>     (block foo
>       (pprint-logical-block (*standard-output* l :prefix "(" :suffix ")")
> 	(pprint-exit-if-list-exhausted)
> 	(loop as x = (pprint-pop)
> 	    do (princ x *standard-output*)
> 	       (pprint-exit-if-list-exhausted)
> 	       (write-char #\space *standard-output*)
> 	       (pprint-newline :fill *standard-output*)))))
> (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
>  29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
>  55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
>  81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99)
> nil
> 
> But now consider the following, which adds the one flagged line:
> 
> > (let ((*print-right-margin* 79)
> 	(l (loop for i below 100 collect i)))
>     (block foo
>       (pprint-logical-block (*standard-output* l :prefix "(" :suffix ")")
> 	(pprint-exit-if-list-exhausted)
> 	(loop as x = (pprint-pop)
> 	    do (princ x *standard-output*)
> 	       (pprint-exit-if-list-exhausted)
> 	       (when (eql x 50) (return-from foo))	; <<<<<<<<<<<<<
> 	       (write-char #\space *standard-output*)
> 	       (pprint-newline :fill *standard-output*)))))
> (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
> nil
> [...]
> However, the reason the issue occurred to me is that I had innocently
> written code like the following:
> 
> (defun foo (x s)
>   ...
>      (pprint-logical-block (...)
>        (when (frumblep x)
>          (format s ...)
>          (return-from foo nil)))
>      ...))
>
> But then I happened to look at the original Dick Waters XP code:
> 
>        (block logical-block
> 	 (start-block ,var ,prefix ,per-line? ,suffix)
> !!>>>	 (unwind-protect
> 	   (macrolet ((pprint-pop () `(pprint-pop+ ,',args ,',var))
> 		      (pprint-exit-if-list-exhausted ()
> 			`(if (null ,',args) (return-from logical-block nil))))
> 	     ,@ body)
> 	   (end-block ,var ,suffix)))
> 
> The ACL code is identical except for the flagged unwind-protect.  Probably
> I was the one who removed it ("for efficiency reasons" although I have no
> recollection of doing so) more than a decade ago.  But other implementations
> apparently did the same.
> 
> There is little question in my mind that the ANS does not require the
> unwind-protect.  (On the other hand, it is equally hard to argue that it
> prohibits it.  So arguably the behavior is undefined.)  But it is just as
> clear that the original Waters prototype intended that pp-l-b be robust
> in face of non-local exit from the body.
> 
> I'm interested if any other language lawyers have opinions on this,
> preferably from the perspective of proper language design, not (for now)
> merely trying to discern what the ANS requires.

My thoughts are these:

(1) I don't use the pretty printer, and it's pretty complicated, so I don't
    have a seat-of-the-pants opinion.  I'm willing to trust that the analysis
    you've made of the conformance details is right.

(2) If I were designing this and were going to use unwind-protect, I'd make
    the thing which did the end-block more sensitive to the question of 
    whether normal completion had finished.  That is, it would know whether
    the entire stream of foo's had been printed [analogous to an abort-close
    of a stream] and if the foo's hadn't been marked "done", I'd force not
    only the suffix but also the abbreviatory text, "...", before it.

(3) I don't see what the efficiency issue is for unwind-protect.  Are they
    hugely expensive?  Especially compared to I/O, which is often 
    comparatively slow enough that I'd think unwind-protect would be "in
    the noise".   But then, you guys seem to do a lot more metering than 
    I usually ahve occasion to do.

(4) Probably from a language design point of view, what is needed is 
    something analogous to "loop-finish" here, which says "stop doing the
    thing and do your pending cleanups"; and perhaps there could even be a
    variant version that did (logical-block-finish-then (return-from foo))
    which send #'(lambda () (return-from foo)) to the logical-block-finish
    tag and then funcalled the thunk's action after finalizing.  Then again,
    consing that obviously-not-downward closure might be no faster than the
    unwind-protect you want to get rid of.  The nice thing about a loop-finish
    kind of cleanup is that in the case of

     (#?FOO #?BAR
     >> Error: "BAZ" is an illegal #? name.
     Restart Option: ABORT
 +-> )
 |
 |
 +------ This paren seems to me stupid and inappropriate once an error dialog
     has occurred on the stream.  The problem with UNWIND-PROTECT is that it
     can't tell if the unwind-protect is due to error or just "normal" 
     non-local return (because it seemed convenient).