Synonym-streams are always open and can't be closed twice.

Bug #1904257 reported by Richard M Kreuter
6
This bug affects 1 person
Affects Status Importance Assigned to Milestone
SBCL
Fix Released
Undecided
Douglas Katzman

Bug Description

OPEN-STREAM-P always returns T for SYNONYM-STREAMs:

--
(let ((con (make-concatenated-stream)))
  (declare (special con))
  (let ((syn (make-synonym-stream 'con)))
    (values (close syn)
            (open-stream-p con)
            (open-stream-p syn))))
=> T
=> NIL
=> T
--

It appears from a comment in src/code/stream.lisp that someone once believed that 22.1.4 justifies this behavior for the implementation of ANSI-STREAM-OPEN-STREAM-P. Here's why that's a dubious interpretation: the code above does not close CON before closing SYN. Instead, CON gets closed as part of closing SYN. It's a circularity to suppose that the fact that SBCL doesn't consider SYN closed thereafter is the reason why SBCL considers CON to have been closed before SYN (the conditions for 22.1.4 to apply).

Next, SBCL makes it an error to CLOSE a SYNONYM-STREAM twice:

--
(let ((con (make-concatenated-stream)))
  (declare (special con))
  (let ((syn (make-synonym-stream 'con)))
    (values (close syn)
            (close syn))))
--

This isn't compatible with the dictionary entry for CLOSE, which says it's permissible to close an already closed stream. (This is a useful property in programs that traffic in lots of streams and can't manage all of them using WITH forms.) It's also pathological that the logic by which the second CLOSE errors is that SBCL considers SYN to always be open while CON isn't, especially given that CON can be repeatedly closed without error.

In summary, none of the above code closes a component stream /prior/ to closing the composite stream; SBCL gives odd semantics to OPEN-STREAM-P and CLOSE on a SYNONYM-STREAM that allows 22.1.4 to be part of the consideration in the above cases.

So I propose that the definitions of OPEN-STREAM-P and CLOSE for SYNONYM-STREAM merely delegate to the target stream. (It turns out that changing OPEN-STREAM-P this way seems to suffice to change CLOSE, too.) Patch attached, and four test cases are below. Note that this change supplies non-error semantics to one case that has undefined consequences and that currently errors (the third test below), though I'd argue that this slight permissiveness is worth getting the above cases to work more reasonably.

Thanks in advance for any consideration.

--
;;; Tests for OPEN-STREAM-P and CLOSE on a SYNONYM-STREAM.

;; Prior 2.0.10.76, this returned T, NIL, T.
(sb-rt:deftest (open-stream-p synonym-to-closed-stream)
 (let ((con (make-concatenated-stream)))
   (declare (special con))
   (let ((syn (make-synonym-stream 'con)))
     (values (close syn)
             (open-stream-p con)
             (open-stream-p syn))))
 t
 nil
 nil)

;; Prior 2.0.10.76, this errored on the second CLOSE.
(sb-rt:deftest (close synonym-stream twice)
 (let ((con (make-concatenated-stream)))
   (declare (special con))
   (let ((syn (make-synonym-stream 'con)))
     (values (close syn)
             (close syn))))
 t
 t)

;; This form has undefined consequences. Prior to 2.0.10.76, the
;; second CLOSE errored because CON was closed prior to trying to
;; close SYN. After, treats SYN as closed, and so the second CLOSE has
;; no effect.
(sb-rt:deftest (close)
 (let ((con (make-concatenated-stream)))
   (declare (special con))
   (let ((syn (make-synonym-stream 'con)))
     (values (close con) ; has undefined consequences
             (close syn))))
 t
 t)

;; SYNONYM-STREAMs will always have the unique property that the nature
;; and status of the stream can change because the value of the synonym
;; variable changes. So if a SYNONYM-STREAM can go from EOF to
;; not-EOF, or from a binary stream to a character stream,
;; why not closed to open?

;; Prior to 2.0.10.76 this returned T, NIL, T, T T.
(sb-rt:deftest (close-then-frob synonym-stream)
 (let ((con1 (make-concatenated-stream))
       (con2 (make-concatenated-stream)))
   (declare (special con1 con2))
   (let ((syn (make-synonym-stream 'con1)))
     (apply #'values
            (close syn)
            (open-stream-p con1)
            (open-stream-p syn)
            (progn (setq con1 con2)
                   (list (open-stream-p con1)
                         (open-stream-p syn))))))
 t
 nil
 nil
 t
 t)
--

Bug reporting metadata:

$ uname -a
Darwin m5.local 19.6.0 Darwin Kernel Version 19.6.0: Mon Aug 31 22:12:52 PDT 2020; root:xnu-6153.141.2~1/RELEASE_X86_64 x86_64
$ sbcl --version
SBCL 2.0.0
$ sbcl --no-userinit --no-sysinit --non-interactive --eval '(format t "~{~S~%~}" *features*)'
This is SBCL 2.0.0, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
:X86-64
:64-BIT
:ALIEN-CALLBACKS
:ANSI-CL
:AVX2
:BSD
:C-STACK-IS-CONTROL-STACK
:CALL-SYMBOL
:COMMON-LISP
:COMPACT-INSTANCE-HEADER
:COMPARE-AND-SWAP-VOPS
:CYCLE-COUNTER
:DARWIN
:FP-AND-PC-STANDARD-SAVE
:GENCGC
:IEEE-FLOATING-POINT
:IMMOBILE-CODE
:IMMOBILE-SPACE
:INODE64
:INTEGER-EQL-VOP
:LINKAGE-TABLE
:LITTLE-ENDIAN
:MACH-EXCEPTION-HANDLER
:MACH-O
:OS-PROVIDES-BLKSIZE-T
:OS-PROVIDES-DLADDR
:OS-PROVIDES-DLOPEN
:OS-PROVIDES-PUTWC
:OS-PROVIDES-SUSECONDS-T
:PACKAGE-LOCAL-NICKNAMES
:SB-CORE-COMPRESSION
:SB-DOC
:SB-EVAL
:SB-LDB
:SB-PACKAGE-LOCKS
:SB-SIMD-PACK
:SB-SIMD-PACK-256
:SB-SOURCE-LOCATIONS
:SB-THREAD
:SB-UNICODE
:SBCL
:STACK-ALLOCATABLE-CLOSURES
:STACK-ALLOCATABLE-FIXED-OBJECTS
:STACK-ALLOCATABLE-LISTS
:STACK-ALLOCATABLE-VECTORS
:STACK-GROWS-DOWNWARD-NOT-UPWARD
:UNDEFINED-FUN-RESTARTS
:UNIX
:UNWIND-TO-FRAME-AND-CALL-VOP

Revision history for this message
Richard M Kreuter (kreuter) wrote :
Stas Boukarev (stassats)
Changed in sbcl:
status: New → Confirmed
Revision history for this message
Stas Boukarev (stassats) wrote :

Should CLOSE actually close the underlying stream?
As it says "The effect of close on a constructed stream is to close the argument stream only. There is no effect on the constituents of composite streams."

Revision history for this message
Richard M Kreuter (kreuter) wrote :
Download full text (3.3 KiB)

Good point. That'd be perfectly usable, and perhaps less weird than the delegation I proposed. (Patch attached, updated tests below.)

By inspection, here's what some other (random versions of arbitrarily selected) implementations do, alongside SBCL 2.0.0. It looks like CMUCL's behavior is equivalent to SBCL's, and these are the only two that error on repeated CLOSE of a SYNONYM-STREAM (presumably an inherited trait). ABCL also has CLOSE on a SYNONYM-STREAM close the underlying stream but leave the SYNONYM-STREAM reporting as being open. Allegro, Clisp, ECL, and LispWorks close the SYNONYM-STREAM and not the underlying stream. On CCL, it appears that CLOSE on a SYNONYM-STREAM does nothing.

Having SBCL close only the SYNONYM-STREAM would make it agree with Allegro, Clisp, ECL, and LispWorks among the below, modulo that the result of a repeated CLOSE is implementation-dependent non-error.

--
(with-input-from-string (str "abc")
  (declare (special str))
  (let ((syn (make-synonym-stream 'str)))
    (values (close syn)
            (open-stream-p str) ; did STR get closed?
            (open-stream-p syn) ; does SYN appear to be open?
            (multiple-value-list
             (ignore-errors (close syn))) ; can SYN be closed again?
            )))
;; ABCL 1.5.0: T, NIL, T, (T)
;; Allegro 10.1: T, T, NIL, (NIL)
;; CCL 2.0: T, T, T, (T)
;; Clisp 2.49: T, T, NIL, (T)
;; CMUCL 21D: T, NIL, T, (NIL #<SIMPLE-ERROR>)
;; ECL 20.4.24: T, T, NIL, (T)
;; LispWorks 7.1.2: T, T, NIL, (T)
;; SBCL 2.0.0: T, NIL, T, (NIL #<CLOSED-STREAM-ERROR>)
--

--
;;; Tests for OPEN-STREAM-P and CLOSE on a SYNONYM-STREAM,
;;; supposing that closing a SYNONYM-STREAM closes the
;;; composite stream and doesn't touch the component stream.

;; Prior 2.0.10.76, this returned T, NIL, T.
(sb-rt:deftest (open-stream-p synonym-to-closed-stream)
 (let ((con (make-concatenated-stream)))
   (declare (special con))
   (let ((syn (make-synonym-stream 'con)))
     (values (close syn)
             (open-stream-p con)
             (open-stream-p syn))))
 t
 t
 nil)

;; Prior 2.0.10.76, this errored on the second CLOSE.
(sb-rt:deftest (close synonym-stream twice)
 (let ((con (make-concatenated-stream)))
   (declare (special con))
   (let ((syn (make-synonym-stream 'con)))
     (values (close syn)
             (close syn))))
 t
 t)

;; This form has undefined consequences. Prior to 2.0.10.76, the
;; second CLOSE errored because CON was closed prior to trying to
;; close SYN. (To enforce the undefined-ness, any of the
;; SYNONYM-STREAM methods could check that the underlying stream was
;; open before proceeding, I guess.)
(sb-rt:deftest (close)
 (let ((con (make-concatenated-stream)))
   (declare (special con))
   (let ((syn (make-synonym-stream 'con)))
     (values (close con) ; has undefined consequences
             (close syn))))
 t
 t)

;; Prior to 2.0.10.76 this returned T, NIL, T, T T.
(sb-rt:deftest (close-then-frob synonym-stream)
 (let ((con1 (make-concatenated-stream))
       (con2 (make-concatenated-stream)))
   (declare (special con1 con2))
   (let ((syn (make-synonym-stream 'con1)))
     (apply #'values
            (close syn)
            (open-stream-p con1)
            (ope...

Read more...

Revision history for this message
Richard M Kreuter (kreuter) wrote :

And for posterity: arguably the standard is inconsistent here, in that the dictionary entry for SYNONYM-STREAM reads ``Any operations on a synonym stream will be performed on the stream that is then the value of the dynamic variable named by the synonym stream symbol.''

So any implementor needs to decide whether to privilege that sentence over the passage in CLOSE, ``The effect of CLOSE on a constructed stream is to close the argument STREAM only. There is no effect on the constituents of composite streams.''

Revision history for this message
Stas Boukarev (stassats) wrote :

I think synonym streams are rarely used on something that is supposed to be closed to matter. Hence the wild differences.

Revision history for this message
Richard M Kreuter (kreuter) wrote :

Probably so. Anyway, the more I think about it, the more I think you're right that it's better to close the synonym stream itself rather than the component stream. For example, when a synonym stream accidentally resolves to something like SB-SYS:*STDIN* at the moment you close it, it's annoying to recover.

Revision history for this message
Douglas Katzman (dougk) wrote :

If CLOSE of a synonym stream operated on anything other than the synonym stream (or unless one were to claim that CLOSE operates on both the stream itself *and* the constituent stream), then CLOSE would be somewhat though not entirely meaningless, in that you're (probably?) not supposed to close constructed streams until _after_ the constituents are closed, which implies at least that there is a way to close constructed streams. So CLOSE has to work only on the synonym stream and not also the constituent, because nobody is claiming that it would operate on both.

This is done in https://sourceforge.net/p/sbcl/sbcl/ci/1ca03bebe49e11679d1ab25044da3e7aec1493c1

Changed in sbcl:
assignee: nobody → Douglas Katzman (dougk)
status: Confirmed → Fix Committed
Revision history for this message
Richard M Kreuter (kreuter) wrote :

Thank you. I think this is the correct way to go.

Changed in sbcl:
status: Fix Committed → Fix Released
To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.