Skip to content

Conversation

garlic0x1
Copy link
Collaborator

@garlic0x1 garlic0x1 commented Aug 20, 2025

This is a stream library pulled from https://github.com/garlic0x1/coalton-streams. I think it ought to be in the standard library since most basic IO will need something like this and I think the current file streams are insufficient.

TODO (maybe in later PRs?):

  • Add better error types for the base methods
  • Dedupe coalton-library/file

One cost of this PR is adding flexi-streams as a dependency to the standard library.

@garlic0x1 garlic0x1 marked this pull request as ready for review August 20, 2025 21:55
Copy link
Collaborator

@shirok shirok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some ideas to explore, but I think this is good to be merged and then we exlore later.

One major issue is whether we want parameterized types on elements; I don't think we'll ever have other element types (non-octet byte streams are mostly useless with customizable endianness support). In that case, we may expose TextualStream and BinaryStream externally, and keep the parameterized stream only for internal implementaiton. But that can come in future.

Copy link
Member

@stylewarning stylewarning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am blocking this on my review. I will try to finish my review in the next couple of days.

@garlic0x1
Copy link
Collaborator Author

Performance is not great when reading millions of bytes compared to a plain CL stream. Here are some ways to improve:

  1. Abandon peek and unread operations and get rid of flexi-streams
  2. Offer *-unchecked operations that don't cons a Result

With these changes we can get as good or better performance compared to CL

@garlic0x1 garlic0x1 marked this pull request as draft September 12, 2025 13:42
* speeeed

* fundeps are fun
@garlic0x1
Copy link
Collaborator Author

Added some functions included in #1644, better to merge that first with its included tests I think.

I need to clean up and benchmark write operations.

@garlic0x1
Copy link
Collaborator Author

Table of Contents

  1. Setup
  2. Read Functions
  3. Write Functions
  4. Read Benchmarks
  5. Write Benchmarks

Setup

(ql:quickload :coalton)
(ql:quickload :coalton-streams)
(defpackage #:stream-benchmark
  (:use #:coalton #:coalton-prelude)
  (:local-nicknames
   (#:file #:coalton-streams/file)
   (#:stream #:coalton-library/stream)))

Read Functions

(coalton-toplevel
  (define (read-rand-byte bytes) 
    (file:with-input-file "/dev/random"
      (fn (stream)
        (the (stream:InputStream U8) stream)
        (coalton-library/experimental:dotimes (_ bytes)
          (stream:read stream)))))

  (define (read-unchecked-rand-byte bytes) 
    (file:with-input-file "/dev/random"
      (fn (stream)
        (the (stream:InputStream U8) stream)
        (coalton-library/experimental:dotimes (_ bytes)
          (stream:read-unchecked stream)))))

  (define (read-unchecked-peekable-rand-byte bytes)
    (file:with-input-file "/dev/random"
      (fn (stream)
        (the (stream:InputStream U8) stream)
        (let stream = (stream:make-peekable stream))
        (coalton-library/experimental:dotimes (_ bytes)
          (stream:read-unchecked stream))))))

(cl:defun read-rand-byte-cl (bytes)
  (uiop:with-input-file (s "/dev/random" :element-type '(cl:unsigned-byte 8))
    (cl:dotimes (_ bytes)
      (cl:read-byte s))))

(cl:defun read-rand-byte-cl-flexi (bytes)
  (uiop:with-input-file (s "/dev/random" :element-type '(cl:unsigned-byte 8))
    (cl:let ((s (flexi-streams:make-flexi-stream s)))
      (cl:dotimes (_ bytes)
        (cl:read-byte s)))))

Write Functions

(coalton-toplevel
  (define (write-bytes bytes)
    (file:with-output-file* "/dev/null" file:Append file:DNEError
      (fn (stream)
        (the (stream:OutputStream U8) stream)
        (coalton-library/experimental:dotimes (_ bytes)
          (stream:write stream 97)))))
  (define (write-bytes-unchecked bytes)
    (file:with-output-file* "/dev/null" file:Append file:DNEError
      (fn (stream)
        (the (stream:OutputStream U8) stream)
        (coalton-library/experimental:dotimes (_ bytes)
          (stream:write-unchecked stream 97))))))

(cl:defun write-bytes-cl (bytes)
  (uiop:with-output-file (s "/dev/null" :element-type '(cl:unsigned-byte 8) :if-exists :append)
    (cl:dotimes (_ bytes)
      (cl:write-byte 97 s))))

Read Benchmarks

(cl:let ((cl:*trace-output* cl:*standard-output*))
  (cl:format cl:t "Reading 100 Million Bytes:~%")
  (cl:print :CL)
  (cl:time (read-rand-byte-cl        100000000))
  (cl:print :FLEX)
  (cl:time (read-rand-byte-cl-flexi  100000000))
  (cl:print :COAL-OPTIONAL)
  (cl:time (read-rand-byte           100000000))
  (cl:print :COAL-UNCHECKED)
  (cl:time (read-unchecked-rand-byte 100000000))
  (cl:print :COAL-UNCHECKED-PEEKABLE)
  (cl:time (read-unchecked-peekable-rand-byte 100000000)))

Reading 100 Million Bytes:

:CL 
Evaluation took:
  0.656 seconds of real time
  0.656323 seconds of total run time (0.468481 user, 0.187842 system)
  100.00% CPU
  154,112 bytes consed


:FLEX 
Evaluation took:
  3.738 seconds of real time
  3.738573 seconds of total run time (3.553288 user, 0.185285 system)
  100.03% CPU
  131,040 bytes consed


:COAL-OPTIONAL 
Evaluation took:
  1.344 seconds of real time
  1.344823 seconds of total run time (1.162907 user, 0.181916 system)
  100.07% CPU
  131,040 bytes consed


:COAL-UNCHECKED 
Evaluation took:
  0.437 seconds of real time
  0.437596 seconds of total run time (0.259091 user, 0.178505 system)
  100.23% CPU
  196,592 bytes consed


:COAL-UNCHECKED-PEEKABLE 
Evaluation took:
  1.805 seconds of real time
  1.805170 seconds of total run time (1.620884 user, 0.184286 system)
  100.00% CPU
  131,040 bytes consed

Write Benchmarks

(cl:let ((cl:*trace-output* cl:*standard-output*))
  (cl:format cl:t "Writing 100 Million Bytes:~%")
  (cl:print :CL)
  (cl:time (write-bytes-cl        100000000))
  (cl:print :COAL-BOOLEAN)
  (cl:time (write-bytes           100000000))
  (cl:print :COAL-UNCHECKED)
  (cl:time (write-bytes-unchecked 100000000)))

Reading 100 Million Bytes:

:CL 
Evaluation took:
  0.568 seconds of real time
  0.568569 seconds of total run time (0.566969 user, 0.001600 system)
  100.18% CPU
  0 bytes consed


:COAL-BOOLEAN 
Evaluation took:
  1.385 seconds of real time
  1.385121 seconds of total run time (1.381807 user, 0.003314 system)
  100.00% CPU
  0 bytes consed


:COAL-UNCHECKED 
Evaluation took:
  0.394 seconds of real time
  0.394355 seconds of total run time (0.393289 user, 0.001066 system)
  100.00% CPU
  0 bytes consed

@stylewarning
Copy link
Member

Awesome benchmarks. I'm surprised Coalton is faster than Lisp.

@garlic0x1 garlic0x1 marked this pull request as ready for review September 13, 2025 14:40
Copy link
Member

@stylewarning stylewarning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are some critical design questions to answer. Perhaps the biggest one is whether the stream types offered should be parameterized on type.

(coalton-toplevel
(repr :native cl:stream)
(define-type (InputStream :elt)
"A stream that can be read from.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to promise that these are Lisp streams so that they can be passed into lisp forms?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question remains. If so, we should document it in the type here, and also in the interop doc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't the (repr :native cl:stream) promise this?

Signals a condition on error."
(:stream :elt -> :elt)))

(define-class (IntoPeekable :stream :elt :peekablestream (:stream -> :peekablestream))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we should make this a type class; we might provide some functions to take our offered stream data types and turn them into peekable streams. This class suggests we will have all sorts of (stream, peekable-stream) pairs to extend with in the future.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any new integer-type stream might want to implement make-peekable

(define (read stream)
"Consume 1 element from a stream."
(catch (Some (read-unchecked stream))
(_ None)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we would limit to the cl:end-of-file condition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is a way to catch specific CL conditions yet

;; High Level Functions
;;

(coalton-toplevel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the below functions should be a part of this module.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the resolution on this?

Copy link
Collaborator Author

@garlic0x1 garlic0x1 Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the tokenizing read functions with the understanding that boolean predicate read-to functions were still desired


(coalton-toplevel
(declare stdout (Unit -> OutputStream :elt))
(define (stdout)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not type safe.

  1. We don't know what kind of stream *standard-output* will be bound to when this call is made.
  2. Even if we did, the function is presenting itself as polymorphic on the stream element type.

The standard is kind of bad when it comes to specifying streams, but we can be guaranteed *terminal-io* won't be re-bound, and we can basically guarantee all such streams will be character streams.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is fine now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:elt below needs to be changed.

(coalton-toplevel
(repr :native cl:stream)
(define-type (InputStream :elt)
"A stream that can be read from.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question remains. If so, we should document it in the type here, and also in the interop doc.

(stream (IOStream :elt))
(buffer (vec:Vector :elt)))

(define-class (Closable :stream :elt)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see :elt as superfluous here. Just :stream should work.

"Consume 1 element from a stream.

Signals a condition on error."
(:stream :elt -> :elt))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have :stream -> :elt, and not have :stream be higher-kinded.

"Consume `n` elements from a stream and destructively fill array."
(:stream :elt -> array:LispArray :elt -> UFix)))

(define-class (Writable :stream :elt)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as Readable.

"Flush buffered output."
(:stream :elt -> Unit)))

(define-class (Readable :stream :elt => Peekable :stream :elt)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as Readable.

(define (unread stream elt)
(vec:push! elt (.buffer stream))
Unit)
(define (peek-unchecked stream)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the resolution here?

(vec:push! elt (.buffer stream))
elt))))

(define-instances (Readable (PeekableInputStream U8) (PeekableIOStream U8))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be specialized on U8?

(vec:clear! (.buffer stream))
result))))

(define-instance (Writable PeekableIOStream U8)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question for everything below. Does it need to be specialized?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, although right now I can't figure out how to define the instances to do it correctly

;; High Level Functions
;;

(coalton-toplevel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the resolution on this?


(coalton-toplevel
(declare stdout (Unit -> OutputStream :elt))
(define (stdout)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:elt below needs to be changed.

@garlic0x1
Copy link
Collaborator Author

Some improvements made, I am struggling to figure out how to reduce the kind arity though

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants