//! I/O errors, the `Read` and `Write` traits, and the standard streams.
//!
//! The operations here and in `std/fs` and `std/net` that can fail report a
//! failure as an `IoError`: an `IoErrorKind` saying what went wrong, and a
//! message for a person to read. `Read` and `Write` move bytes a piece at a
//! time. `stdin()`, `stdout()` and `stderr()` return the process's standard
//! streams, which implement them, as `std/fs`'s `File` and `std/net`'s
//! `TcpStream` do. Every module sees these names without an import, and a
//! module's own definition of one of them shadows the one here.
//!
//! The methods of `Read` and `Write` return a `Future` for the caller to
//! await. On the standard streams they never suspend: each call blocks every
//! task until the operating system has answered.
//!
//! Writing to standard output through `Write`, and telling an `IoError` by
//! its kind:
//!
//! ```
//! async fn main() {
//! // `write` returns how many bytes it took, which can be fewer than given.
//! let out = stdout()
//! let n = out.write(bytes_from_string("hello\n")).await.unwrap()
//! assert(n >= 1 && n <= 6, "some of the six bytes were written")
//! out.flush().await.unwrap()
//!
//! // A failure is an `IoError`: match on its `kind`, not its `message`.
//! let err = match read_to_string("${temp_dir()}/nova-std-io/absent.txt").await {
//! Ok(_) => panic("nothing should be there")
//! Err(e) => e
//! }
//! let not_found = match err.kind {
//! NotFound => true
//! _ => false
//! }
//! assert(not_found, "expected NotFound, got: ${err.message}")
//! }
//! ```
// Nova standard library — I/O error types, and the `Read`/`Write` traits over
// the three standard streams.
//
// Compiled as an implicit module and glob-imported into every user module, so
// these names need no `import` (see docs/adr/0004-stdlib-compile-model.md).
// A user definition of the same name shadows the one here.
//
// `File`, `open` and `OpenOptions` live in `std/fs`, not here: they need the
// handle-lifetime design that module's own doc comment describes. `Stdin`,
// `Stdout` and `Stderr` below are process-global, always open and never
// closed, so they exercise both traits below with none of it.
//
// **The `Future<T>` spelling.** `async fn` in a trait *declaration* is
// `E0900`, so `Read`/`Write` below are declared `fn ... -> Future<T>`
// instead of `async fn ... -> T`. Calling an `async fn` without `.await`
// produces its `Future` without running it, so a plain, non-`async` `fn` can
// still return one, unawaited -- every method on both traits follows this
// same shape, including `read_to_end`'s default body, which returns the
// `Future` of the private, generic `read_all` rather than `await`ing it
// itself (an `async fn` default body could not: it would need to `await` to
// produce a plain `T`, and a non-`async` `fn` has no `await` available to it).
//
// **Every wrapper below is straight-line**: it calls its builtin and reads
// the per-task payload/message slot with no `.await` between the two. Those
// slots are per-task, keyed on the polling task
// (`crates/nova-runtime/src/fs.rs`'s `SLOTS`/`slot_index`), so a sibling task
// cannot clobber them -- but *within* one task, a second operation still
// overwrites that same task's own slot, so a wrapper that awaited
// mid-sequence could read back a payload its own later call had already
// replaced. `fs_take_bytes`/`fs_last_error_message` keep their `fs_` prefix
// for history, not scope: the slot table has been a general per-task
// boundary facility since increment 3a, not a filesystem-only one, and
// `std/io` reaches both directly because `Builtin::STD_ONLY` is seeded into
// every std module's scope, not only `std/fs`'s.
// Why an operation failed. `ConnectionRefused` is unused by `std/fs` and is
// carried so §4's network I/O needs no further addition later.
//
// `AlreadyExists` and `InvalidData` are additions to the list in
// `nova-spec/20-STDLIB.md` §4; see docs/adr/0011-io-error-kinds.md.
/// What went wrong in a failed I/O operation, as an `IoError` reports it.
///
/// Match on it to handle a failure by its cause. Failures with no variant of
/// their own are `Other`, and only the error's `message` tells them apart.
pub type IoErrorKind =
/// A file or directory the operation needs does not exist.
| NotFound
/// The operating system did not permit the operation.
| PermissionDenied
/// Something the operation was to create already exists.
| AlreadyExists
/// The data is not valid for the operation, as when `read_to_string`
/// reads a file that is not UTF-8.
| InvalidData
/// The operation was interrupted before it finished.
| Interrupted
/// A time limit passed before the operation finished, such as
/// `TcpStream::read_timeout`'s.
| TimedOut
/// The other end refused the connection: usually nothing is listening at
/// the address `connect` was given.
| ConnectionRefused
/// Any other failure, among them an address already in use, an address
/// that is not valid, a broken connection, and a `File` or `TcpStream`
/// that is closed.
| Other
// A failed I/O operation: a kind to branch on, and the operating system's own
// message for diagnosis.
//
// `message` is platform-specific text, so a test asserting output should match
// on `kind` and normalise `message` -- the same treatment
// `tests/runtime/nova_test.stdout` already applies to a Windows NTSTATUS.
/// A failed I/O operation: what kind of failure it was, and a description of
/// it.
///
/// The functions of `std/io`, `std/fs` and `std/net` that can fail return
/// one in their `Err`.
pub record IoError {
/// What kind of failure this is.
pub kind: IoErrorKind,
/// A description of the failure for a person to read, usually the
/// operating system's own, so its wording differs from one system to
/// another. Match on `kind` rather than on `message`.
pub message: String,
}
// Map a runtime status code to its kind.
//
// **This function is one half of a wire contract.** The other half is
// `fail` in `crates/nova-runtime/src/fs.rs`, which produces these codes
// from `std::io::ErrorKind`. The two are independent copies of one numbering,
// which is the shape that has already produced a miscompile in this project.
//
// **This function's own mapping (every arm below, including the unmapped-code
// fallback to `Other`) is pinned per-code by `fs_io_types.nova`.** The Rust
// side that produces these codes from a real OS condition is pinned for only
// four of the eight kinds -- `NotFound`, `AlreadyExists`, `InvalidData`, and
// (Windows only) `PermissionDenied`; the rest are either unreachable from
// `std/fs` or reachable but not yet exercised by a fixture. See `fs.rs`'s
// status-constant doc comment and `docs/adr/0011-io-error-kinds.md` for the
// full split -- this comment previously claimed all eight were "pinned
// together by a fixture per kind," which was false for five of them
// (final review, I1).
//
// An unrecognised code is `Other` rather than a panic: a status this function
// does not know is a runtime bug, and mapping it to `Other` keeps a diagnosable
// error flowing instead of ending the process.
/// Returns the `IoErrorKind` that the status code `code` stands for: 1
/// `NotFound`, 2 `PermissionDenied`, 3 `AlreadyExists`, 4 `InvalidData`, 5
/// `Interrupted`, 6 `TimedOut`, 7 `ConnectionRefused`, and `Other` for any
/// other code.
///
/// std's I/O functions use it to build their `IoError`s.
pub fn io_error_kind_of(code: Int) -> IoErrorKind {
if code == 1 { return NotFound }
if code == 2 { return PermissionDenied }
if code == 3 { return AlreadyExists }
if code == 4 { return InvalidData }
if code == 5 { return Interrupted }
if code == 6 { return TimedOut }
if code == 7 { return ConnectionRefused }
Other
}
/// A source of bytes, read a piece at a time.
///
/// `Stdin`, `std/fs`'s `File` and `std/net`'s `TcpStream` implement it. Its
/// methods return a `Future`: await it for the result.
pub trait Read {
// Up to `max` bytes. An **empty** result means end of stream. **A short
// read is not EOF** -- a pipe or a terminal may hand back far fewer bytes
// than `max` and still have more to give later, so a caller (including
// `read_to_end`'s default below) must keep reading on anything
// non-empty and stop only on a genuinely empty result.
/// Reads up to `max` bytes and returns them; empty bytes mean the end of
/// the stream.
///
/// A read can return fewer than `max` bytes even when more are coming, so
/// only an empty result marks the end. A `max` of 0 returns empty bytes
/// too. Room for `max` bytes is allocated before the read, however few
/// arrive.
///
/// Returns an `IoError` if the read fails: of kind `Other` for a `File` or
/// `TcpStream` that is closed. A `TcpStream` read suspends the calling
/// task until there is something to read; a `Stdin` or `File` read blocks
/// every task until it returns. std's implementations panic if `max` is
/// negative.
fn read(self, max: Int) -> Future<Result<Bytes, IoError>>
// Reads until end of stream and returns everything concatenated in
// order. Defaults to the private `read_all`, over this trait's own
// `read` -- see that function for the loop, and `CHUNK` for the size it
// reads at a time.
/// Reads to the end of the stream and returns every byte read, in order.
///
/// It reads until a read returns empty bytes, so on `Stdin` or a
/// `TcpStream` it waits for the other end to close. At the end already, it
/// returns empty bytes. If a read fails, it returns that read's `IoError`,
/// and the bytes read before it are lost.
fn read_to_end(self) -> Future<Result<Bytes, IoError>> { read_all(self) }
}
/// A destination for bytes, written a piece at a time.
///
/// `Stdout`, `Stderr`, `std/fs`'s `File` and `std/net`'s `TcpStream`
/// implement it. Its methods return a `Future`: await it for the result.
pub trait Write {
// Writes as much of `buf` as one underlying write accepts, and reports
// exactly how many bytes that was -- **not necessarily all of `buf`**.
// Mirrors Rust's own possibly-partial `Write::write`, deliberately
// unlike `std/fs`'s count-less, write-all `write`. A caller that needs a
// guaranteed full write must loop on the returned count itself; this
// method does not do it for you.
/// Writes as much of `buf` as one write accepts, and returns how many
/// bytes that was.
///
/// The count can be less than `buf.len()`: to write all of `buf`, call
/// `write` again with the bytes not yet written. An empty `buf` writes
/// nothing and returns 0. Returns an `IoError` if the write fails: of kind
/// `Other` for a `File` or `TcpStream` that is closed, and for a broken
/// connection. A `TcpStream` write suspends the calling task until there
/// is room to write; the others block every task until they return.
///
/// Writing all of some bytes, a partial write at a time:
///
/// ```
/// // `write` can take fewer bytes than it is given, so writing all of them
/// // means calling it again with the rest.
/// async fn write_all(f: File, data: Bytes) {
/// let mut rest = data
/// while rest.len() > 0 {
/// let n = f.write(rest).await.unwrap()
/// rest = rest.slice(n, rest.len())
/// }
/// }
///
/// async fn main() {
/// let dir = "${temp_dir()}/nova-std-io"
/// create_dir_all(dir).await.unwrap()
/// let f = open("${dir}/out.txt", OpenOptions::writing()).await.unwrap()
/// write_all(f, bytes_from_string("line\n".repeat(1000))).await
/// f.close().await.unwrap()
/// assert_eq(read("${dir}/out.txt").await.unwrap().len(), 5000)
/// # remove_dir_all(dir).await.unwrap()
/// }
/// ```
fn write(self, buf: Bytes) -> Future<Result<Int, IoError>>
/// Writes out any bytes the writer is holding in a buffer.
///
/// Of std's writers only `Stdout` holds any: on the others it does
/// nothing, and on a `File` it does not wait for the data to reach the
/// disk. Returns an `IoError` if the held bytes cannot be written, or for
/// a `File` that is closed.
fn flush(self) -> Future<Result<(), IoError>>
}
// The process's standard input. Process-global, always open, never closed --
// reading it more than once keeps reading from wherever the last read left
// off, the same as calling `std::io::stdin()` more than once in Rust.
//
// A `read` here blocks the whole executor for as long as the OS read takes,
// so a terminal with nothing typed yet stalls every other task on the thread,
// not only this one. This used to say "there is no I/O poller yet"; amended
// 2026-08-16 (branch `io-poller-std-net`), because one now exists
// (`crates/nova-runtime/src/poll.rs`, docs/adr/0013-io-poller.md). The
// consequence above is unchanged and this one really is still just unbuilt --
// unlike `std/fs`, whose reason is structural, stdin is in principle a handle
// this poller's mechanism could watch, but nothing wires `std/io` to it:
// `std/net` is the poller's only caller. See
// `docs/adr/0009-async-execution-model.md` §1's 2026-08-16 amendment, which
// records both halves.
/// The process's standard input, which `stdin()` returns.
///
/// It is always open, and every `Stdin` reads the same stream: a read
/// continues where the last one stopped. A read blocks every task until
/// input arrives or the stream ends.
pub record Stdin {}
/// Returns the process's standard input, to read with `Read`.
pub fn stdin() -> Stdin { Stdin {} }
async fn read_stdin(s: Stdin, max: Int) -> Result<Bytes, IoError> {
let status = io_stdin_read(max)
if status == 0 { return Ok(fs_take_bytes()) }
Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}
impl Read for Stdin {
fn read(self, max: Int) -> Future<Result<Bytes, IoError>> { read_stdin(self, max) }
}
// The process's standard output. Process-global, always open, never closed.
/// The process's standard output, which `stdout()` returns.
///
/// It is always open, and its writes share one buffer with `print` and
/// `println`, so their output stays in order. A newline sends out everything
/// up to it; bytes after the last newline can wait in the buffer until a
/// later newline or `flush`. They can be lost if the program panics or
/// returns from `main` before then, so flush before it ends; `exit` flushes
/// them.
pub record Stdout {}
/// Returns the process's standard output, to write with `Write`.
pub fn stdout() -> Stdout { Stdout {} }
// The write's byte count crosses the boundary as 8 little-endian bytes in the
// payload slot (`crates/nova-runtime/src/io.rs`'s `stash_count`), not in the
// status word -- decoded here by `decode_count`, which reads all eight,
// rather than by reading a single byte, which would silently truncate any
// count at or above 256.
async fn write_stdout(s: Stdout, buf: Bytes) -> Result<Int, IoError> {
let status = io_stdout_write(buf)
if status == 0 { return Ok(decode_count(fs_take_bytes())) }
Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}
async fn flush_stdout(s: Stdout) -> Result<(), IoError> {
let status = io_stdout_flush()
if status == 0 { return Ok(()) }
Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}
impl Write for Stdout {
fn write(self, buf: Bytes) -> Future<Result<Int, IoError>> { write_stdout(self, buf) }
fn flush(self) -> Future<Result<(), IoError>> { flush_stdout(self) }
}
// The process's standard error. Process-global, always open, never closed.
/// The process's standard error, which `stderr()` returns.
///
/// It is always open, and not buffered: each write goes out at once.
pub record Stderr {}
/// Returns the process's standard error, to write with `Write`.
pub fn stderr() -> Stderr { Stderr {} }
// Mirrors `write_stdout` exactly, against the other stream.
async fn write_stderr(s: Stderr, buf: Bytes) -> Result<Int, IoError> {
let status = io_stderr_write(buf)
if status == 0 { return Ok(decode_count(fs_take_bytes())) }
Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}
// Mirrors `flush_stdout` exactly, against the other stream.
async fn flush_stderr(s: Stderr) -> Result<(), IoError> {
let status = io_stderr_flush()
if status == 0 { return Ok(()) }
Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}
impl Write for Stderr {
fn write(self, buf: Bytes) -> Future<Result<Int, IoError>> { write_stderr(self, buf) }
fn flush(self) -> Future<Result<(), IoError>> { flush_stderr(self) }
}
// How many bytes `read_all` asks for per `read` call while draining a `Read`
// to end of stream. Stated once, here, and nowhere else -- a size duplicated
// between a doc comment and the code is a drift this project has repeatedly
// had to correct.
const CHUNK: Int = 4096
// `read_to_end`'s default body, over any `Read`. Loops `read(CHUNK)` and
// concatenates every non-empty result in order, stopping on the first
// genuinely empty one (`read`'s own EOF contract) -- never on a short one,
// which is not EOF (see `Read::read`'s own doc comment). Getting that
// backwards -- stopping as soon as a result is shorter than `CHUNK` rather
// than only when it is empty -- silently truncates the result, and does so
// against the *common* case, not an edge case: a real pipe or terminal
// rarely hands back a chunk exactly `CHUNK` bytes long, so nearly every read
// from one looks "short" by that broken test.
async fn read_all<T: Read>(reader: T) -> Result<Bytes, IoError> {
let mut acc = bytes_from_ints([])
while true {
let chunk = reader.read(CHUNK).await
match chunk {
Ok(bytes) => {
if bytes.len() == 0 { return Ok(acc) }
acc = acc.concat(bytes)
}
Err(e) => return Err(e)
}
}
Ok(acc)
}
// Decodes a payload integer -- 8 little-endian bytes in the payload slot --
// back into the `Int` it encodes, folding from the most significant byte
// down. Four separate places in the runtime write that encoding, and they are
// named at the end of this comment rather than cited here, because each
// produces the payloads of exactly one module. Reading only the first byte --
// `fs_take_bytes().byte_at(0)` -- would silently truncate any count at or
// above 256; a single write can exceed that.
//
// **This fold RELIES ON `Int` ARITHMETIC WRAPPING, and the reliance is not
// optional -- do not add an overflow check to this loop.** For a non-negative
// payload no intermediate exceeds the final value, which is why this comment
// used to claim the fold needs no more range than its result: true of every
// payload `std/io`, `std/fs` and `std/net` produce, and false as a general
// statement. `std/crypto`'s `random_int` is the first caller whose payload
// can be negative, and for a drawn `0 - 1` -- eight `0xFF` bytes -- the fold
// leaves `Int` range one step before it finishes: `n` is `2^56 - 1` after
// seven iterations, so the eighth multiply reaches `2^64 - 256`, the largest
// value the fold forms short of its own result, and the closing add reaches
// `2^64 - 1`, which is that result and not an intermediate at all. Both of
// those steps are out of range, and it is wrapping at both that carries the
// fold to `-1`. An overflow check here would therefore corrupt every negative
// draw while this module's own tests stayed green, because none of their
// payloads is ever negative. `tests/runtime/crypto_random.nova` is what does
// exercise it: it draws `random_int(0 - 3, 3)` two hundred times, roughly
// three in seven of which are negative, checks each against both ends, and
// its golden asserts `bounded true`.
//
// `pub` because this is the whole tree's payload-integer decoder rather than
// this module's own. Every std module that reads an `Int` back out of the
// payload slot calls this same function instead of growing a second
// Nova-level decoder: this module for the two stream writes above, `std/fs`
// for `File::open`'s new fd and its `Write::write` byte count, `std/net` for
// `connect`'s and `accept`'s stream fds, `bind`'s listener fd, `local_port`
// and its own write count, and `std/crypto` for `random_int`'s drawn value.
// All of them cross the boundary the identical way, 8 little-endian bytes in
// the payload slot, because the status word carries the error kind instead --
// `IoErrorKind` for this module, `std/fs` and `std/net`, and
// `CryptoErrorKind` for `std/crypto`, which maps the same status word through
// its own `crypto_error_kind_of`.
//
// **The sharing stops at this function.** One level down, on the runtime
// side, four separate producers write that same encoding, one per module, and
// only three of them are even a named helper:
// `crates/nova-runtime/src/io.rs`'s `stash_count` for this module's two
// stream writes, `crates/nova-runtime/src/file.rs`'s `stash_i64` for
// `std/fs`, `crates/nova-runtime/src/net.rs`'s `stash_i64` for `std/net` -- a
// second and separate definition of the same one-line body, not a call into
// `file.rs`'s -- and `crates/nova-runtime/src/crypto.rs` for `random_int`,
// which stashes `value.to_le_bytes()` inline with no helper at all. So what
// the call sites listed above actually share is the *encoding* -- 8
// little-endian bytes in the payload slot -- plus this one decoder on the
// Nova side; there is no shared encoder to point at, and citing any single
// producer here would be wrong for every module but one. `net.rs`'s and
// `file.rs`'s doc comments each record that their copy is a copy.
/// Returns the integer that `b` holds in little-endian order: its first byte
/// is the least significant.
///
/// Eight bytes can hold any `Int`, a negative one in two's complement. Fewer
/// give a number that is not negative, empty bytes give 0, and bytes after
/// the eighth do not change the result. std uses it to decode the integers,
/// such as byte counts, that its I/O operations report.
pub fn decode_count(b: Bytes) -> Int {
let ints = b.to_ints()
let mut n = 0
let mut i = ints.len() - 1
while i >= 0 {
n = n * 256 + ints[i]
i = i - 1
}
n
}