nova-std 0.3.1

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
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
//! Files and directories: whole files read and written at once, files opened
//! to read and write a piece at a time, and directories made, listed and
//! removed.
//!
//! Paths are `String`s, and a relative path is resolved against the process's
//! current directory. `read_to_string` and `write_string` move a whole file
//! as text, and `read` and `write` as `Bytes`. `open` returns a `File`, which
//! reads and writes through `std/io`'s `Read` and `Write`. Failures are
//! `std/io`'s `IoError`s. Every module sees these names without an import,
//! and a module's own definition of one of them shadows the one here.
//!
//! Every function here that touches the filesystem is an `async fn`, but none
//! of them suspends: each blocks every task until the operation finishes.
//!
//! Writing a file in a directory of its own under `temp_dir()`, reading it
//! back, listing the directory, and removing both:
//!
//! ```
//! async fn main() {
//!     let dir = "${temp_dir()}/nova-std-fs"
//!     create_dir_all(dir).await.unwrap()
//!     let path = "${dir}/notes.txt"
//!     write_string(path, "first line\n").await.unwrap()
//!     assert_eq(read_to_string(path).await.unwrap(), "first line\n")
//!
//!     let entries = read_dir(dir).await.unwrap()
//!     assert_eq(entries.len(), 1)
//!     assert_eq(entries[0].name, "notes.txt")
//!     assert(entries[0].is_file, "notes.txt is a file")
//!
//!     remove_dir_all(dir).await.unwrap()
//!     assert(!exists(dir).await, "the directory and its file are gone")
//! }
//! ```

// Nova standard library — filesystem.
//
// Compiled as an implicit module and glob-imported into every user module. Error
// types live in `std/io`.
//
// Paths are `String` throughout. Contents are not, uniformly: `read`/`write`
// take and return `Bytes` (`std/bytes`), so arbitrary file contents need no
// UTF-8 round-trip, while `read_to_string`/`write_string` stay on `String`
// for the common text case.
//
// Every function that touches the filesystem is an `async fn` per
// `nova-spec/20-STDLIB.md` §5, but **none of them suspends**: the operation
// runs synchronously inside the poll. This used to say "because there is no
// I/O poller yet"; amended 2026-08-16 (branch `io-poller-std-net`), because a
// real poller now exists (`crates/nova-runtime/src/poll.rs`,
// docs/adr/0013-io-poller.md) and `std/net` genuinely suspends on it. The
// reason this module still does not has *changed*, not merely aged: it is no
// longer a roadmap gap but a structural fact — a regular file is not
// readiness-pollable by `select`/`WSAPoll`, or anything like them, on any of
// this project's three CI platforms, so making `std/fs` suspend needs a
// completion-based interface (IOCP), which ADR 0013 declines on scope. See
// docs/adr/0009-async-execution-model.md §1's 2026-08-16 amendment. The
// signature is what matters — it did not change when the poller landed, and no
// call site broke, exactly as this comment predicted. The cost is that a call
// blocks the whole executor for its duration, so a sibling task makes no
// progress during a large read. See docs/adr/0011-io-error-kinds.md.
//
// `temp_dir` is the one of them that reaches outward and is still a plain
// `fn`: it only queries the environment and touches no filesystem, so there is
// nothing in it that could ever suspend, and no future reason to change its
// signature either. It is not the only plain `fn` in this module any more,
// only the only exception -- `OpenOptions`'s `default`/`reading`/`writing`/
// `appending` build a record of six `Bool`s and touch nothing at all, and
// `impl Read for File`/`impl Write for File`'s methods are plain `fn`s
// returning `Future`, which is how a suspending trait method is spelled in
// this language.
//
// # The shape of these wrappers is load-bearing
//
// Each one calls its intrinsic, then reads a slot, with **no `.await` between
// the two**. The runtime's payload and message 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. The risk that remains is narrower but still
// real: **within one task**, a second operation overwrites that same task's own
// slot, so a wrapper that awaited mid-sequence could still read a payload its
// own later call replaced. A future function here that awaited mid-sequence
// would break that. Keep them straight-line.

// Read the whole file at `path` as a UTF-8 string.
//
// Non-UTF-8 contents are `InvalidData`, not a panic and not lossy replacement.
/// Reads the whole file at `path` and returns its contents as a `String`.
///
/// Returns an `IoError` of kind `NotFound` if nothing exists at `path`,
/// `PermissionDenied` if the system denies access to it, and `InvalidData` if
/// the contents are not valid UTF-8. Reading a directory fails too, with a
/// kind that depends on the system.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn read_to_string(path: String) -> Result<String, IoError> {
    let status = fs_read_to_string(path)
    if status == 0 { return Ok(fs_take_string()) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

// Write `content` to `path`, replacing any existing contents.
/// Writes `content` to the file at `path`, creating the file if it does not
/// exist and replacing its contents if it does.
///
/// Returns an `IoError` of kind `NotFound` if the directory to hold the file
/// does not exist, and `PermissionDenied` if the system denies access.
/// Writing to a directory fails too, with a kind that depends on the system.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn write_string(path: String, content: String) -> Result<(), IoError> {
    let status = fs_write_string(path, content)
    if status == 0 { return Ok(()) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

// Read the whole file at `path` as bytes.
//
// Unlike `read_to_string` this cannot fail with `InvalidData`: there is no
// encoding to violate.
/// Reads the whole file at `path` and returns its contents as `Bytes`.
///
/// Unlike `read_to_string`, it takes any contents. Returns an `IoError` of
/// kind `NotFound` if nothing exists at `path`, and `PermissionDenied` if the
/// system denies access to it. Reading a directory fails too, with a kind
/// that depends on the system.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn read(path: String) -> Result<Bytes, IoError> {
    let status = fs_read(path)
    if status == 0 { return Ok(fs_take_bytes()) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

// Write `content` to `path`, replacing any existing contents.
/// Writes `content` to the file at `path`, creating the file if it does not
/// exist and replacing its contents if it does.
///
/// Returns an `IoError` of kind `NotFound` if the directory to hold the file
/// does not exist, and `PermissionDenied` if the system denies access.
/// Writing to a directory fails too, with a kind that depends on the system.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn write(path: String, content: Bytes) -> Result<(), IoError> {
    let status = fs_write(path, content)
    if status == 0 { return Ok(()) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

// The OS's temporary-directory path, with no trailing separator guaranteed
// either way -- join a name onto it the same way `fs_roundtrip` does.
/// Returns the path of the operating system's directory for temporary files.
///
/// On Linux that is the `TMPDIR` environment variable when it is set, and
/// `/tmp` otherwise. The path may or may not end with a separator, and
/// nothing checks that the directory exists. Unlike most of this module, it
/// is not `async`: it touches no file.
pub fn temp_dir() -> String { fs_temp_dir() }

// Whether `path` exists.
//
// Returns `Bool`, not `Result`, per `nova-spec/20-STDLIB.md` §5 — so a path that
// exists but cannot be examined is indistinguishable from one that is absent.
/// Returns `true` if something exists at `path`: a file, a directory, or
/// anything else.
///
/// It also returns `false` when `path` cannot be examined, for lack of
/// permission say, so `false` does not prove that nothing is there. A
/// symbolic link counts only if what it points to exists.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn exists(path: String) -> Bool { fs_exists(path) }

// Create the directory `path`. The parent must already exist; use
// `create_dir_all` otherwise. An existing `path` is `AlreadyExists`.
/// Creates the directory `path`.
///
/// Its parent must already exist; `create_dir_all` creates missing parents
/// too. Returns an `IoError` of kind `AlreadyExists` if anything, a file
/// included, already exists at `path`, `NotFound` if the parent does not
/// exist, and `PermissionDenied` if the system denies access.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn create_dir(path: String) -> Result<(), IoError> {
    let status = fs_create_dir(path)
    if status == 0 { return Ok(()) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

// Create the directory `path`, and any missing parent directories along the
// way. Unlike `create_dir`, an existing `path` is not an error.
/// Creates the directory `path`, and every missing directory above it.
///
/// A directory already at `path` is not an error, and neither is an empty
/// `path`: both return `Ok(())`. Returns an `IoError` of kind `AlreadyExists`
/// if a file is at `path`, and `PermissionDenied` if the system denies
/// access. A file in the way above `path` gives an error whose kind depends
/// on the system.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn create_dir_all(path: String) -> Result<(), IoError> {
    let status = fs_create_dir_all(path)
    if status == 0 { return Ok(()) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

// Remove the file `path`.
/// Removes the file at `path`.
///
/// Returns an `IoError` of kind `NotFound` if nothing exists at `path`, and
/// `PermissionDenied` if the system denies the removal. A directory is not
/// removed: that fails with a kind that depends on the system, and
/// `remove_dir_all` removes one.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn remove_file(path: String) -> Result<(), IoError> {
    let status = fs_remove_file(path)
    if status == 0 { return Ok(()) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

// Remove the directory `path` and everything under it.
/// Removes the directory `path` and everything in it.
///
/// A symbolic link inside is removed, not followed. Returns an `IoError` of
/// kind `NotFound` if nothing exists at `path`, and `PermissionDenied` if the
/// system denies removing something; a file at `path` fails with a kind that
/// depends on the system. A failure part way can leave some of the contents
/// removed.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn remove_dir_all(path: String) -> Result<(), IoError> {
    let status = fs_remove_dir_all(path)
    if status == 0 { return Ok(()) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

// One entry in a directory listing.
//
// `is_file` and `is_dir` come from a single `fs_kind` call, so they cannot
// disagree with each other. They CAN both be false, though: `fs_kind` returns
// `0` whenever it cannot read the entry's metadata at all, not only when the
// entry is absent (see `fs_kind`'s own doc comment), and this record does not
// special-case that. So "exactly one is true" holds only for an entry whose
// metadata `fs_kind` could actually read -- not for every entry that exists.
// INFERRED, not measured: the clean trigger is a dangling symlink (`read_dir`
// lists it, its metadata call fails), which this codebase has not measured
// against on any platform -- constructing one needs admin rights on Windows,
// where this was checked.
/// One entry of a directory, as `read_dir` lists it.
///
/// A symbolic link is followed: `is_file` and `is_dir` describe what it
/// points to. Both are `false` when the entry cannot be examined, as for a
/// link that points to nothing.
pub record DirEntry {
    /// The entry's name in its directory, such as `notes.txt`.
    pub name: String,
    /// The path given to `read_dir`, then `/`, then `name`, so a path given
    /// with a trailing `/` gives `//`.
    pub path: String,
    /// `true` if the entry is not a directory: a file, or another kind of
    /// entry such as a pipe.
    pub is_file: Bool,
    /// `true` if the entry is a directory.
    pub is_dir: Bool,
}

// List the entries in `path`, sorted by name.
//
// Sorted because directory order is unspecified by the operating system; the
// sort happens in the runtime.
//
// Builds each `DirEntry` here rather than in the runtime: a record built in Rust
// would put Nova's field layout in two places, which is the drift this module's
// boundary exists to avoid. The cost is one extra `fs_kind` call per entry.
/// Returns the entries of the directory `path`, sorted by name.
///
/// Names are compared byte by byte, so `B.txt` comes before `a.txt`. Names
/// beginning with `.` are listed, but `.` and `..` are not, and a name that
/// is not valid Unicode has U+FFFD in place of its bad bytes. Returns an
/// `IoError` of kind `NotFound` if nothing exists at `path`, and
/// `PermissionDenied` if the system denies access; a file at `path` fails
/// with a kind that depends on the system.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
pub async fn read_dir(path: String) -> Result<[DirEntry], IoError> {
    let status = fs_read_dir(path)
    if status != 0 {
        return Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
    }
    let names = fs_take_string_array()
    let n = names.len()
    // A repeat array, then every slot overwritten. `[init; n]` evaluates `init`
    // exactly ONCE, so all `n` slots start as the same heap object -- harmless
    // only because the loop below replaces each one. Do not read a slot before
    // assigning it.
    let mut out = [DirEntry { name: "", path: "", is_file: false, is_dir: false }; n]
    let mut i = 0
    while i < n {
        let name = names[i]
        let full = "${path}/${name}"
        let k = fs_kind(full)
        out[i] = DirEntry { name: name, path: full, is_file: k == 1, is_dir: k == 2 }
        i = i + 1
    }
    Ok(out)
}

// --- File, OpenOptions and open ---
//
// Unlike everything above, which opens, acts and closes inside one intrinsic
// call, a `File` is a handle that survives between calls -- Nova's first
// value holding an OS resource that way. Nova has no destructors, so
// explicit `close` is the only release mechanism; a `File` that is never
// closed leaks its descriptor until the process exits.

// Options for `open`. Each flag forwards onto `std::fs::OpenOptions`'s own
// method of the same name, in the order `open` below passes them to
// `file_open`: read, write, append, truncate, create, create_new.
//
// No chainable builder: a receiver-mutating method cannot be called on a
// temporary (`E0060`, measured), so `OpenOptions::reading().with_write()`
// does not compile in this language. Use one of the three constructors
// below, or build an exotic combination with `let mut o =
// OpenOptions::default()` followed by field assignment.
/// How `open` opens a file: to read, write or append, and whether to create
/// or truncate it.
///
/// `reading()`, `writing()` and `appending()` give the usual sets. For any
/// other, start from `OpenOptions::default()`, which sets every flag to
/// `false`, in a `let mut` binding, and assign `true` to the flags wanted:
/// `read`, `write`, `append`, `truncate`, `create`, and `create_new`, which
/// creates the file and fails if it already exists. `open` returns an
/// `IoError` of kind `Other` for a set it cannot honour, such as one with
/// none of `read`, `write` and `append`, or one with `create` or `truncate`
/// but neither `write` nor `append`.
pub record OpenOptions {
    read: Bool,
    write: Bool,
    append: Bool,
    truncate: Bool,
    create: Bool,
    create_new: Bool,
}

// Every flag false. Passed to `open` as-is this fails -- measured:
// `std::fs::OpenOptions` itself requires at least one of read/write/append
// -- so this exists as the base for field assignment building an exotic
// combination, not as an option set meant to be used directly.
impl Default for OpenOptions {
    fn default() -> OpenOptions {
        OpenOptions {
            read: false,
            write: false,
            append: false,
            truncate: false,
            create: false,
            create_new: false,
        }
    }
}

impl OpenOptions {
    /// Returns options that open an existing file for reading only.
    pub fn reading() -> OpenOptions {
        OpenOptions {
            read: true,
            write: false,
            append: false,
            truncate: false,
            create: false,
            create_new: false,
        }
    }

    // Write + create + truncate: start this file fresh, creating it if it is
    // not there yet and discarding any content it already has.
    /// Returns options that open a file for writing only, creating it if it
    /// does not exist and emptying it if it does.
    pub fn writing() -> OpenOptions {
        OpenOptions {
            read: false,
            write: true,
            append: false,
            truncate: true,
            create: true,
            create_new: false,
        }
    }

    // Append + create: add to this file, creating it if it is not there
    // yet, and leaving any content it already has in place.
    /// Returns options that open a file for appending only, creating it if it
    /// does not exist and keeping what it holds: every write goes to the end.
    pub fn appending() -> OpenOptions {
        OpenOptions {
            read: false,
            write: false,
            append: true,
            truncate: false,
            create: true,
            create_new: false,
        }
    }
}

// A handle to an open file. `fd` keys
// `crates/nova-runtime/src/file.rs`'s thread-local table of open
// `std::fs::File`s -- it is not an OS file descriptor number, and nothing
// about its value means anything to a Nova program beyond identifying an
// entry in that table.
//
// `fd` is not privacy-enforced -- no record field in this language is -- so
// a program can build `File { fd: 9999 }` naming no file this module ever
// opened. That is safe by design: absence from the handle table is
// indistinguishable from closedness, so a forged, stale, or already-closed
// handle behaves exactly like the real thing does once closed (see `close`
// below).
/// A file opened by `open`, to read and write a piece at a time through
/// `std/io`'s `Read` and `Write`.
///
/// Copying a `File` copies the handle: every copy refers to the same open
/// file. Nothing closes a `File` for you: call `close` when done, or it stays
/// open until the program exits.
pub record File { fd: Int }

// Open `path` under `options`.
/// Opens the file at `path` as `options` say, and returns it.
///
/// Returns an `IoError` of kind `NotFound` if the file does not exist and
/// `options` does not create it, or if the directory to hold it does not
/// exist; `AlreadyExists` if `options` sets `create_new` and the file exists;
/// `PermissionDenied` if the system denies access; and `Other` for options it
/// cannot honour. A directory cannot be read as a file: depending on the
/// system, opening it or reading it fails.
///
/// An `async fn` that never suspends: it blocks every task until it
/// finishes.
///
/// Appending to a file, then reading it back:
///
/// ```
/// async fn main() {
///     let dir = "${temp_dir()}/nova-std-fs-open"
///     create_dir_all(dir).await.unwrap()
///     let path = "${dir}/log.txt"
///     write_string(path, "one\n").await.unwrap()
///
///     // `appending()` keeps what the file holds and adds each write at its end.
///     let log = open(path, OpenOptions::appending()).await.unwrap()
///     log.write(bytes_from_string("two\n")).await.unwrap()
///     log.close().await.unwrap() // nothing closes a File for you
///
///     let reader = open(path, OpenOptions::reading()).await.unwrap()
///     let text = reader.read_to_end().await.unwrap().to_string().unwrap()
///     reader.close().await.unwrap()
///     assert_eq(text, "one\ntwo\n")
///     # remove_dir_all(dir).await.unwrap()
/// }
/// ```
pub async fn open(path: String, options: OpenOptions) -> Result<File, IoError> {
    let status = file_open(
        path,
        options.read,
        options.write,
        options.append,
        options.truncate,
        options.create,
        options.create_new,
    )
    if status == 0 { return Ok(File { fd: decode_count(fs_take_bytes()) }) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

impl File {
    // Closes the handle. Idempotent: a second or third call also returns
    // `Ok(())`, because Nova has no move checking, so `self` by value cannot
    // stop a second call the way it would in a language with real
    // ownership. Closing a stale or forged handle is the same no-op: the
    // table lookup a close performs simply removes nothing.
    /// Closes the file.
    ///
    /// It returns `Ok(())`, also when the file is already closed: closing
    /// reports no error. Afterwards, reading, writing or flushing the file,
    /// through any copy, returns an `IoError` of kind `Other`.
    ///
    /// An `async fn` that never suspends.
    pub async fn close(self) -> Result<(), IoError> {
        let status = file_close(self.fd)
        if status == 0 { return Ok(()) }
        Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
    }
}

// Any operation other than `close` against a handle this module's table does
// not recognise -- already closed, stale, or forged -- reaches this same
// `IoError { kind: Other }` by missing the same table lookup, whether the
// operation is this function or `write_file`/`flush_file` below.
async fn read_file(f: File, max: Int) -> Result<Bytes, IoError> {
    let status = file_read(f.fd, 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 File {
    fn read(self, max: Int) -> Future<Result<Bytes, IoError>> { read_file(self, max) }
}

// The write's byte count crosses the boundary the same way `std/io`'s
// `write_stdout` decodes its own byte count: 8 little-endian bytes in the
// payload slot, read back here by that same `decode_count` rather than a
// second decoder.
async fn write_file(f: File, buf: Bytes) -> Result<Int, IoError> {
    let status = file_write(f.fd, 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_file(f: File) -> Result<(), IoError> {
    let status = file_flush(f.fd)
    if status == 0 { return Ok(()) }
    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })
}

impl Write for File {
    fn write(self, buf: Bytes) -> Future<Result<Int, IoError>> { write_file(self, buf) }
    fn flush(self) -> Future<Result<(), IoError>> { flush_file(self) }
}