Gray Streams (user-defined streams)
rontolisp ships its own small Gray-stream extension, mirroring how real
implementations expose their native Gray support: a user class extends one of
the rontolisp:fundamental-*-stream base classes and defines methods on the
rontolisp:stream-* generics, and the stream-taking built-ins dispatch to
those methods when handed such an instance instead of a stream handle. This
works on every backend (interpreter, JVM, both WASM variants).
The base classes form the CL-shaped hierarchy: fundamental-stream at the
root, fundamental-input-stream / fundamental-output-stream below it, and
fundamental-character-input-stream / fundamental-character-output-stream /
fundamental-binary-input-stream / fundamental-binary-output-stream as the
leaves (all in the rontolisp package).
| built-in | dispatches to |
|---|---|
write-char | rontolisp:stream-write-char |
write-string, format (stream destination) | rontolisp:stream-write-string |
princ, prin1, print | rontolisp:stream-write-string of the rendered text (print then stream-terpri) |
terpri | rontolisp:stream-terpri (default method writes a newline through stream-write-char) |
fresh-line | rontolisp:stream-fresh-line (default method: stream-terpri unless stream-start-line-p) |
write-line | rontolisp:stream-write-string then rontolisp:stream-terpri |
force-output / finish-output / clear-output | rontolisp:stream-force-output / -finish-output / -clear-output (default methods answer nil) |
close | answers t -- see below |
write-byte | rontolisp:stream-write-byte |
read-byte | rontolisp:stream-read-byte |
read-char | rontolisp:stream-read-char |
read-char-no-hang | rontolisp:stream-read-char-no-hang (default method IS stream-read-char) |
peek-char | rontolisp:stream-peek-char (default method: read one, hand it back through stream-unread-char); the peek-type skipping forms loop over it |
unread-char | rontolisp:stream-unread-char (default method parks the character in the protocol's one-slot pushback) |
read-line | rontolisp:stream-read-line (default method loops stream-read-char) |
listen | rontolisp:stream-listen (default method answers nil) |
open-stream-p | answers t -- like close, a name a program may own |
stream-element-type | character, or (unsigned-byte 8) for a binary base class -- a class subclassing BOTH (a bivalent stream) answers character, because the answer is which buffer to allocate. A name a program may own |
read-sequence / write-sequence | rontolisp:stream-read-sequence / -write-sequence (default methods loop the element generics) |
file-position | rontolisp:stream-file-position; the two-argument form calls the (setf rontolisp:stream-file-position) writer generic |
A character output stream defines stream-write-char or stream-write-string
-- either one is enough. Each has a default method written in terms of the
other, so the rest of the output protocol composes out of whichever you wrote.
(Defining neither is the one broken shape: the two defaults then call each
other.)
Two more generics have no built-in of their own but are what the line-oriented
operators consult: rontolisp:stream-line-column answers the stream's current
column, or nil (the default) for a stream that tracks none, and
rontolisp:stream-start-line-p answers from it. A stream with no column cannot
tell whether it is at the start of a line, so fresh-line on it always writes
the newline. rontolisp:stream-advance-to-column rounds out the protocol for a
program that calls it directly.
Closing a Gray stream answers t and does nothing else -- there is nothing to
release. A stream that DOES hold something writes CL's own spelling, a method on
close itself:
Such a method dispatches on every backend. A program that defines one owns
close outright: the Gray default steps aside for it.
A character INPUT stream defines stream-read-char -- that one method is
enough (a binary one defines stream-read-byte). Everything else on the read
side is written over it: stream-read-line and stream-read-sequence loop it,
stream-read-char-no-hang is it, and stream-peek-char reads one character and
hands it back through stream-unread-char, whose own default parks the
character in the protocol's one-slot pushback. A class that can rewind its own
source defines stream-unread-char and owns the pushback instead — the pushback
cell is then never written.
On the read side the methods answer the keyword :eof at end of stream; the
built-ins translate that through the usual eof-error-p / eof-value
contract. stream-read-line answers a partial last line as that line — :eof
means "no characters left at all".
A stream-write-char-only stream that tracks its column, so fresh-line can
tell whether it has to break the line:
A binary input stream with the file-position protocol:
A character input stream defining only stream-read-char, driven through the
rest of the read protocol:
The trivial-gray-streams shim
Portable libraries are written against
trivial-gray-streams
rather than an implementation's own protocol. rontolisp bundles a built-in
trivial-gray-streams ASDF system adapting the portable API onto the protocol
above (see Systems): the
trivial-gray-streams package mirrors every base class (plus
trivial-gray-stream-mixin) and every generic, including
stream-read-sequence / stream-write-sequence (stream sequence start end &key), stream-file-position with its (setf ...) writer, and the output
family stream-line-column / stream-start-line-p / stream-terpri /
stream-fresh-line / stream-advance-to-column / stream-force-output /
stream-finish-output / stream-clear-output — this is how
jzon's :stream writer API runs, and the class shape fast-io and
circular-streams define loads unchanged. The defaults are the same ones the
rontolisp protocol has, so a portable class that defines only
trivial-gray-streams:stream-write-char still answers every operator above.
Limits
rontolisp:stream-advance-to-columnexists as a protocol generic but no built-in dispatches to it (format's~Tdoes not consult the column).- The protocol's pushback holds ONE character for ONE stream at a time, which is
what CL promises for
unread-char. It is drained by every read that goes through the protocol's own defaults; a class that overridesstream-read-lineorstream-read-sequenceoutright reads past it, so such a class should definestream-unread-chartoo. input-stream-p/output-stream-panswer the DIRECTION base class the instance extends, not a predicate method per class: afundamental-input-streamdescendant answerstto the first andnilto the second, and a subclass of the barefundamental-streamanswersnilto both. A class may still define a method on either name and own the answer.unread-charon a stream HANDLE — a file, a string input stream, a socket — parks the character in a handle-side pushback of its own, whichread-char,peek-charandread-linedrain. It holds one character for one stream, like the protocol's; a secondunread-charwith the cell still full signals.read-byte,read-sequenceandreaddo not consult it.- The read generics return primary values only:
stream-read-linehas no(values line missing-newline-p)pair —:eofis the whole EOF signal. listenon a Gray instance works on the interpreter and the JVM; the Preview 1 WASM backend rejects anylistencall at compile time (a pre-existing platform limit, Gray or not).- A
(write-string s instance :start ... :end ...)call with bounding keywords does not dispatch the bounds to the instance. - Dispatch happens at the built-in call sites: a first-class
(funcall #'read-byte instance)does not dispatch on the compiled backends.