(rontolisp) docs

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-indispatches to
write-charrontolisp:stream-write-char
write-string, format (stream destination)rontolisp:stream-write-string
princ, prin1, printrontolisp:stream-write-string of the rendered text (print then stream-terpri)
terprirontolisp:stream-terpri (default method writes a newline through stream-write-char)
fresh-linerontolisp:stream-fresh-line (default method: stream-terpri unless stream-start-line-p)
write-linerontolisp:stream-write-string then rontolisp:stream-terpri
force-output / finish-output / clear-outputrontolisp:stream-force-output / -finish-output / -clear-output (default methods answer nil)
closeanswers t -- see below
write-byterontolisp:stream-write-byte
read-byterontolisp:stream-read-byte
read-charrontolisp:stream-read-char
read-char-no-hangrontolisp:stream-read-char-no-hang (default method IS stream-read-char)
peek-charrontolisp:stream-peek-char (default method: read one, hand it back through stream-unread-char); the peek-type skipping forms loop over it
unread-charrontolisp:stream-unread-char (default method parks the character in the protocol's one-slot pushback)
read-linerontolisp:stream-read-line (default method loops stream-read-char)
listenrontolisp:stream-listen (default method answers nil)
open-stream-panswers t -- like close, a name a program may own
stream-element-typecharacter, 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-sequencerontolisp:stream-read-sequence / -write-sequence (default methods loop the element generics)
file-positionrontolisp: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-column exists as a protocol generic but no built-in dispatches to it (format's ~T does 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 overrides stream-read-line or stream-read-sequence outright reads past it, so such a class should define stream-unread-char too.
  • input-stream-p / output-stream-p answer the DIRECTION base class the instance extends, not a predicate method per class: a fundamental-input-stream descendant answers t to the first and nil to the second, and a subclass of the bare fundamental-stream answers nil to both. A class may still define a method on either name and own the answer.
  • unread-char on a stream HANDLE — a file, a string input stream, a socket — parks the character in a handle-side pushback of its own, which read-char, peek-char and read-line drain. It holds one character for one stream, like the protocol's; a second unread-char with the cell still full signals. read-byte, read-sequence and read do not consult it.
  • The read generics return primary values only: stream-read-line has no (values line missing-newline-p) pair — :eof is the whole EOF signal.
  • listen on a Gray instance works on the interpreter and the JVM; the Preview 1 WASM backend rejects any listen call 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.