nova-std 0.3.2

The Nova language's standard library sources (internal to the nova toolchain)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
//! 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
}