Read and write streams¶
Import system.io for file handles, memory streams, buffering, and the Reader,
Writer, AsyncReader, and AsyncWriter interfaces.
File handles and cleanup¶
Open a file with FileHandle.open(path, mode) or FileHandle.open_async(path, mode, token):
| Mode | Access | If the file exists | If it is missing |
|---|---|---|---|
FileMode.Read |
read | Opens at the start | ENOENT |
FileMode.Write |
write | Truncates it | Creates it |
FileMode.Append |
append | Writes go to the end | Creates it |
FileMode.ReadWrite |
read and write | Opens at the start | ENOENT |
FileMode.WriteRead |
read and write | Truncates it | Creates it |
FileMode.AppendRead |
read and append | Writes go to the end | Creates it |
The mode is a FileMode enum. Reads, writes, seeking, length queries, and
explicit closure return Result. A successful empty read at a positive requested
size means EOF; a read failure returns IoError.
import system.io;
fun main(): Result<Unit, IoError> {
let file = FileHandle.open("input.txt", FileMode.Read)?;
while true {
let line = file.read_line()?;
if line.is_none() {
break;
}
}
return file.close();
}
FileHandle member |
Returns | Effect |
|---|---|---|
read(n) |
Result<byte[], IoError> |
Reads up to n bytes. |
read_line(max_bytes) |
Result<Option<string>, IoError> |
Reads one line; None at EOF. |
write(data) |
Result<int, IoError> |
Writes bytes; returns the count written. Pass Encoding.utf8_encode(text) to write text. |
write_all(data) |
Result<Unit, IoError> |
Retries until every byte is written. |
read_exact(count) |
Result<byte[], IoError> |
Reads exactly count bytes or fails with EEOF. |
seek(pos) |
Result<Unit, IoError> |
Moves to an absolute byte offset. |
seek_from_end(offset) |
Result<Unit, IoError> |
Moves relative to the end; negative moves backward. |
tell() |
Result<long, IoError> |
Current byte offset. |
size() |
Result<long, IoError> |
Current file size in bytes. |
flush() |
Result<Unit, IoError> |
Checks the handle is open; writes are unbuffered. |
close() |
Result<Unit, IoError> |
Closes the handle. |
is_closed |
bool |
Whether the handle is closed. |
path |
string |
Path used to open the file. |
The async forms read_async(n, token), write_async(data, token),
seek_async(pos, token), and flush_async(token) return the same results.
Neither flush() nor flush_async() forces data to durable storage.
Use Streams.read_text(file) to read the remaining bytes as UTF-8 text, or file.read_line()
for line-by-line input. Byte reads use file.read(n). For chunked Unicode input use an
incremental decoder so a chunk boundary cannot split a character.
close() is idempotent. Operations after closure return ECLOSED. Dropping the
last reference also closes the handle, but only explicit closure can report an
error. A failed close retires the handle: retrying an OS descriptor could close a
different file after the descriptor has been reused.
File positions and sizes use long. tell() includes buffered read-ahead, so
mixing line and byte reads preserves the logical position. Failed seeks preserve
the position and any unread bytes. FileHandle.tell() and .size() return
Result<long, IoError>; use seek() to change position.
Async opens, reads, writes, and seeks use a bounded native executor or Node's
callback-based filesystem APIs. Native execution uses four workers and a queue
of at most 128 jobs; a saturated executor reports EBUSY. Browser memory-file
operations remain immediately ready. No guest buffers are accessed by workers.
A handle permits one pending operation at a time. Concurrent I/O or explicit
closure returns EBUSY, leaving the handle open. After a cancelled operation,
wait for underlying work to finish before reusing or closing that handle. Dropping
the handle is safe while work continues: the host retains the necessary resources.
Tokens are checked before submission and register event subscriptions while pending. Completion and cancellation wake the scheduler without periodic checks. Cancellation does not undo completed writes or restore consumed input. A cancelled open closes its late result automatically. A submitted seek completes and updates read-ahead state before reporting cancellation; this prevents a cancelled seek from leaving the logical cursor inconsistent.
Streaming copies¶
import system.io;
fun main(): Result<Unit, IoError> {
let source = FileHandle.open("input.bin", FileMode.Read)?;
let destination = FileHandle.open("output.bin", FileMode.Write)?;
Streams.copy(source, destination, buffer_size: 65536)?;
destination.close()?;
return source.close();
}
| Function | Returns | Effect |
|---|---|---|
Streams.copy(source, destination, buffer_size) |
Result<long, IoError> |
Copies a Reader to a Writer. |
Streams.copy_async(source, destination, buffer_size, token) |
async Result<long, IoError> |
Copies an AsyncReader to an AsyncWriter. |
Both copy to EOF in chunks of at most buffer_size bytes (default 8192), flush the
destination, and return the byte count. Neither closes either stream. A
nonpositive buffer_size returns EINVAL. Completed writes stay in place
after a failure or cancellation.
Stream interfaces¶
Implement these interfaces to plug your own types into the copy helpers and buffered wrappers:
| Interface | Members | Notes |
|---|---|---|
Reader |
read(max_bytes), seek(position), tell(), read_exact(count) |
Only read is required; default seek/tell return ENOTSUP. |
Writer |
write(data), flush(), write_all(data) |
write_all is provided. |
AsyncReader |
read_async(max_bytes, token) |
Same EOF and size rules as Reader. |
AsyncWriter |
write_async(data, token), flush_async(token) |
Writes may accept fewer bytes than given. |
Reader.read_exact reports EEOF if the stream ends early. Writer.write_all
retries short writes and reports an error if the writer makes no progress.
Interface implementations must return no more bytes than requested.
Buffered and memory streams¶
import system.io;
import system.encoding;
fun main(): Result<Unit, IoError> {
let source = MemoryStream(Encoding.utf8_encode("first\r\nsecond\n"));
let reader = BufferedReader(source, buffer_size: 8192);
let first = reader.read_line()?;
reader.seek(0L)?;
let destination = MemoryStream();
let writer = BufferedWriter(destination, buffer_size: 8192);
writer.write_all(Encoding.utf8_encode("hello"))?;
writer.finish()?;
return destination.close();
}
| Member | Returns | Effect |
|---|---|---|
BufferedReader(source, buffer_size) |
BufferedReader |
Wraps a Reader; buffer_size defaults to 8192. |
BufferedReader.read(max_bytes) |
Result<byte[], IoError> |
Reads from the buffer, refilling as needed. |
BufferedReader.read_line(max_bytes) |
Result<Option<string>, IoError> |
Reads one line; None at EOF. |
BufferedReader.seek(position) / tell() |
Result<Unit, IoError> / Result<long, IoError> |
Repositions the source / reports the logical position. |
BufferedReader.take_buffered() |
byte[] |
Removes and returns unread buffered bytes. |
BufferedWriter(destination, buffer_size) |
BufferedWriter |
Wraps a Writer; buffer_size defaults to 8192. |
BufferedWriter.write(data) |
Result<int, IoError> |
Buffers bytes; may accept only part. |
BufferedWriter.flush() |
Result<Unit, IoError> |
Writes pending bytes and flushes the destination. |
BufferedWriter.finish() |
Result<Unit, IoError> |
Flushes and rejects later writes with ECLOSED. |
Buffer wrappers retain their underlying streams without closing them. Finish a
buffered writer explicitly so failures are visible; dropping it does not flush.
After a partial flush failure, the unwritten bytes remain available for retry.
finish() is idempotent after success and rejects subsequent writes.
Use a buffered reader's seek() when repositioning its seekable source. Do not
read, write, or reposition that source directly while unread bytes are held by the
wrapper. take_buffered() transfers those unread bytes to the caller.
Line reads support LF and CRLF, preserve standalone carriage returns, and return
an unterminated final line before EOF. The default line limit is 1 MiB, configurable
with read_line(max_bytes: ...). A limit error consumes the prefix and the byte
that exceeded the limit. Buffer sizes must be positive; invalid sizes return
EINVAL when used.
Memory streams support seeking, overwriting, and zero-filled gaps. Their
to_bytes() method returns an independent copy. Memory stream async methods are
immediately ready and do not block on host I/O.
MemoryStream member |
Returns | Effect |
|---|---|---|
MemoryStream() |
MemoryStream |
Creates an empty stream. |
MemoryStream(bytes) |
MemoryStream |
Copies bytes and starts at byte zero. |
read(max_bytes) |
Result<byte[], IoError> |
Reads and advances the cursor. |
write(bytes) |
Result<int, IoError> |
Overwrites or appends at the cursor. |
seek(position) |
Result<Unit, IoError> |
Moves the cursor, including past the end. |
tell() |
Result<long, IoError> |
Current position; fails after closure. |
position / length |
long |
Cursor and stored size; readable after closure. |
to_bytes() |
Result<byte[], IoError> |
Copy of all stored bytes. |
flush() |
Result<Unit, IoError> |
Checks the stream is open. |
close() |
Result<Unit, IoError> |
Closes idempotently; data is retained. |
MemoryStream also implements read_async, write_async, and flush_async.
Positions are limited to the int range.
Error codes¶
Stream operations fail with IoError, which has code(), message(), and a
path field that is empty when no file is involved.
| Code | Meaning |
|---|---|
ENOENT |
The file does not exist. |
EACCES |
Permission was denied. |
EEXIST |
The path already exists. |
EINVAL |
Invalid argument, such as a negative size or nonpositive buffer size. |
ECLOSED |
The stream was already closed or finished. |
EBUSY |
Another operation is pending on the handle, or the file executor is saturated. |
ECANCELLED |
A cancellation token fired. |
ENOTSUP |
The stream does not support seeking or positions. |
EEOF |
read_exact reached the end early. |
ELIMIT |
A line exceeded the read_line byte limit. |
EIO |
Any other I/O failure. |
Asynchronous file copies¶
import system.io;
import system.encoding;
async fun main(): Result<Unit, IoError> {
let source = MemoryStream(Encoding.utf8_encode("hello"));
let destination = FileHandle.open_async("async-copy.txt", FileMode.WriteRead).await?;
Streams.copy_async(source, destination).await?;
destination.seek_async(0L).await?;
let bytes = destination.read_async(5).await?;
destination.close()?;
source.close()?;
File.delete("async-copy.txt");
return Result.Ok(bytes.length == 5);
}
Standard input and output¶
Import system.io to use System.stdin as a Reader and System.stdout or
System.stderr as a Writer. Byte reads and writes report IoError; they leave the
process descriptors open. Standard streams do not support seeking.
import system;
import system.io;
import system.encoding;
fun main(): void {
let writer = BufferedWriter(System.stdout);
writer.write_all(Encoding.utf8_encode("Ready\n")).unwrap();
writer.finish().unwrap();
let reader = BufferedReader(System.stdin);
switch (reader.read_line().unwrap()) {
Some(line) => System.println(line),
None => {},
}
}
Streams.copy(System.stdin, destination) copies incoming bytes into a writer.
Streams.copy(source, System.stdout) copies from a reader to standard output;
flush the output when the copy finishes. Reading standard input can block.
System.print and System.println are console text shortcuts for writing to
standard output. They flush their output, preserving order when mixed with
System.stdout.write. System.read_line and System.read_key remain shortcuts
for simple console input. Use BufferedReader and Encoding for text over a byte stream.