Skip to main content

FS

Constant FS 

Source
pub const FS: &str = "//! Files and directories: whole files read and written at once, files opened\n//! to read and write a piece at a time, and directories made, listed and\n//! removed.\n//!\n//! Paths are `String`s, and a relative path is resolved against the process\'s\n//! current directory. `read_to_string` and `write_string` move a whole file\n//! as text, and `read` and `write` as `Bytes`. `open` returns a `File`, which\n//! reads and writes through `std/io`\'s `Read` and `Write`. Failures are\n//! `std/io`\'s `IoError`s. Every module sees these names without an import,\n//! and a module\'s own definition of one of them shadows the one here.\n//!\n//! Every function here that touches the filesystem is an `async fn`, but none\n//! of them suspends: each blocks every task until the operation finishes.\n//!\n//! Writing a file in a directory of its own under `temp_dir()`, reading it\n//! back, listing the directory, and removing both:\n//!\n//! ```\n//! async fn main() {\n//!     let dir = \"${temp_dir()}/nova-std-fs\"\n//!     create_dir_all(dir).await.unwrap()\n//!     let path = \"${dir}/notes.txt\"\n//!     write_string(path, \"first line\\n\").await.unwrap()\n//!     assert_eq(read_to_string(path).await.unwrap(), \"first line\\n\")\n//!\n//!     let entries = read_dir(dir).await.unwrap()\n//!     assert_eq(entries.len(), 1)\n//!     assert_eq(entries[0].name, \"notes.txt\")\n//!     assert(entries[0].is_file, \"notes.txt is a file\")\n//!\n//!     remove_dir_all(dir).await.unwrap()\n//!     assert(!exists(dir).await, \"the directory and its file are gone\")\n//! }\n//! ```\n\n// Nova standard library \u{2014} filesystem.\n//\n// Compiled as an implicit module and glob-imported into every user module. Error\n// types live in `std/io`.\n//\n// Paths are `String` throughout. Contents are not, uniformly: `read`/`write`\n// take and return `Bytes` (`std/bytes`), so arbitrary file contents need no\n// UTF-8 round-trip, while `read_to_string`/`write_string` stay on `String`\n// for the common text case.\n//\n// Every function that touches the filesystem is an `async fn` per\n// `nova-spec/20-STDLIB.md` \u{a7}5, but **none of them suspends**: the operation\n// runs synchronously inside the poll. This used to say \"because there is no\n// I/O poller yet\"; amended 2026-08-16 (branch `io-poller-std-net`), because a\n// real poller now exists (`crates/nova-runtime/src/poll.rs`,\n// docs/adr/0013-io-poller.md) and `std/net` genuinely suspends on it. The\n// reason this module still does not has *changed*, not merely aged: it is no\n// longer a roadmap gap but a structural fact \u{2014} a regular file is not\n// readiness-pollable by `select`/`WSAPoll`, or anything like them, on any of\n// this project\'s three CI platforms, so making `std/fs` suspend needs a\n// completion-based interface (IOCP), which ADR 0013 declines on scope. See\n// docs/adr/0009-async-execution-model.md \u{a7}1\'s 2026-08-16 amendment. The\n// signature is what matters \u{2014} it did not change when the poller landed, and no\n// call site broke, exactly as this comment predicted. The cost is that a call\n// blocks the whole executor for its duration, so a sibling task makes no\n// progress during a large read. See docs/adr/0011-io-error-kinds.md.\n//\n// `temp_dir` is the one of them that reaches outward and is still a plain\n// `fn`: it only queries the environment and touches no filesystem, so there is\n// nothing in it that could ever suspend, and no future reason to change its\n// signature either. It is not the only plain `fn` in this module any more,\n// only the only exception -- `OpenOptions`\'s `default`/`reading`/`writing`/\n// `appending` build a record of six `Bool`s and touch nothing at all, and\n// `impl Read for File`/`impl Write for File`\'s methods are plain `fn`s\n// returning `Future`, which is how a suspending trait method is spelled in\n// this language.\n//\n// # The shape of these wrappers is load-bearing\n//\n// Each one calls its intrinsic, then reads a slot, with **no `.await` between\n// the two**. The runtime\'s payload and message slots are per-task, keyed on the\n// polling task (`crates/nova-runtime/src/fs.rs`\'s `SLOTS`/`slot_index`), so a\n// sibling task cannot clobber them. The risk that remains is narrower but still\n// real: **within one task**, a second operation overwrites that same task\'s own\n// slot, so a wrapper that awaited mid-sequence could still read a payload its\n// own later call replaced. A future function here that awaited mid-sequence\n// would break that. Keep them straight-line.\n\n// Read the whole file at `path` as a UTF-8 string.\n//\n// Non-UTF-8 contents are `InvalidData`, not a panic and not lossy replacement.\n/// Reads the whole file at `path` and returns its contents as a `String`.\n///\n/// Returns an `IoError` of kind `NotFound` if nothing exists at `path`,\n/// `PermissionDenied` if the system denies access to it, and `InvalidData` if\n/// the contents are not valid UTF-8. Reading a directory fails too, with a\n/// kind that depends on the system.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn read_to_string(path: String) -> Result<String, IoError> {\n    let status = fs_read_to_string(path)\n    if status == 0 { return Ok(fs_take_string()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\n// Write `content` to `path`, replacing any existing contents.\n/// Writes `content` to the file at `path`, creating the file if it does not\n/// exist and replacing its contents if it does.\n///\n/// Returns an `IoError` of kind `NotFound` if the directory to hold the file\n/// does not exist, and `PermissionDenied` if the system denies access.\n/// Writing to a directory fails too, with a kind that depends on the system.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn write_string(path: String, content: String) -> Result<(), IoError> {\n    let status = fs_write_string(path, content)\n    if status == 0 { return Ok(()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\n// Read the whole file at `path` as bytes.\n//\n// Unlike `read_to_string` this cannot fail with `InvalidData`: there is no\n// encoding to violate.\n/// Reads the whole file at `path` and returns its contents as `Bytes`.\n///\n/// Unlike `read_to_string`, it takes any contents. Returns an `IoError` of\n/// kind `NotFound` if nothing exists at `path`, and `PermissionDenied` if the\n/// system denies access to it. Reading a directory fails too, with a kind\n/// that depends on the system.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn read(path: String) -> Result<Bytes, IoError> {\n    let status = fs_read(path)\n    if status == 0 { return Ok(fs_take_bytes()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\n// Write `content` to `path`, replacing any existing contents.\n/// Writes `content` to the file at `path`, creating the file if it does not\n/// exist and replacing its contents if it does.\n///\n/// Returns an `IoError` of kind `NotFound` if the directory to hold the file\n/// does not exist, and `PermissionDenied` if the system denies access.\n/// Writing to a directory fails too, with a kind that depends on the system.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn write(path: String, content: Bytes) -> Result<(), IoError> {\n    let status = fs_write(path, content)\n    if status == 0 { return Ok(()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\n// The OS\'s temporary-directory path, with no trailing separator guaranteed\n// either way -- join a name onto it the same way `fs_roundtrip` does.\n/// Returns the path of the operating system\'s directory for temporary files.\n///\n/// On Linux that is the `TMPDIR` environment variable when it is set, and\n/// `/tmp` otherwise. The path may or may not end with a separator, and\n/// nothing checks that the directory exists. Unlike most of this module, it\n/// is not `async`: it touches no file.\npub fn temp_dir() -> String { fs_temp_dir() }\n\n// Whether `path` exists.\n//\n// Returns `Bool`, not `Result`, per `nova-spec/20-STDLIB.md` \u{a7}5 \u{2014} so a path that\n// exists but cannot be examined is indistinguishable from one that is absent.\n/// Returns `true` if something exists at `path`: a file, a directory, or\n/// anything else.\n///\n/// It also returns `false` when `path` cannot be examined, for lack of\n/// permission say, so `false` does not prove that nothing is there. A\n/// symbolic link counts only if what it points to exists.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn exists(path: String) -> Bool { fs_exists(path) }\n\n// Create the directory `path`. The parent must already exist; use\n// `create_dir_all` otherwise. An existing `path` is `AlreadyExists`.\n/// Creates the directory `path`.\n///\n/// Its parent must already exist; `create_dir_all` creates missing parents\n/// too. Returns an `IoError` of kind `AlreadyExists` if anything, a file\n/// included, already exists at `path`, `NotFound` if the parent does not\n/// exist, and `PermissionDenied` if the system denies access.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn create_dir(path: String) -> Result<(), IoError> {\n    let status = fs_create_dir(path)\n    if status == 0 { return Ok(()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\n// Create the directory `path`, and any missing parent directories along the\n// way. Unlike `create_dir`, an existing `path` is not an error.\n/// Creates the directory `path`, and every missing directory above it.\n///\n/// A directory already at `path` is not an error, and neither is an empty\n/// `path`: both return `Ok(())`. Returns an `IoError` of kind `AlreadyExists`\n/// if a file is at `path`, and `PermissionDenied` if the system denies\n/// access. A file in the way above `path` gives an error whose kind depends\n/// on the system.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn create_dir_all(path: String) -> Result<(), IoError> {\n    let status = fs_create_dir_all(path)\n    if status == 0 { return Ok(()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\n// Remove the file `path`.\n/// Removes the file at `path`.\n///\n/// Returns an `IoError` of kind `NotFound` if nothing exists at `path`, and\n/// `PermissionDenied` if the system denies the removal. A directory is not\n/// removed: that fails with a kind that depends on the system, and\n/// `remove_dir_all` removes one.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn remove_file(path: String) -> Result<(), IoError> {\n    let status = fs_remove_file(path)\n    if status == 0 { return Ok(()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\n// Remove the directory `path` and everything under it.\n/// Removes the directory `path` and everything in it.\n///\n/// A symbolic link inside is removed, not followed. Returns an `IoError` of\n/// kind `NotFound` if nothing exists at `path`, and `PermissionDenied` if the\n/// system denies removing something; a file at `path` fails with a kind that\n/// depends on the system. A failure part way can leave some of the contents\n/// removed.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn remove_dir_all(path: String) -> Result<(), IoError> {\n    let status = fs_remove_dir_all(path)\n    if status == 0 { return Ok(()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\n// One entry in a directory listing.\n//\n// `is_file` and `is_dir` come from a single `fs_kind` call, so they cannot\n// disagree with each other. They CAN both be false, though: `fs_kind` returns\n// `0` whenever it cannot read the entry\'s metadata at all, not only when the\n// entry is absent (see `fs_kind`\'s own doc comment), and this record does not\n// special-case that. So \"exactly one is true\" holds only for an entry whose\n// metadata `fs_kind` could actually read -- not for every entry that exists.\n// INFERRED, not measured: the clean trigger is a dangling symlink (`read_dir`\n// lists it, its metadata call fails), which this codebase has not measured\n// against on any platform -- constructing one needs admin rights on Windows,\n// where this was checked.\n/// One entry of a directory, as `read_dir` lists it.\n///\n/// A symbolic link is followed: `is_file` and `is_dir` describe what it\n/// points to. Both are `false` when the entry cannot be examined, as for a\n/// link that points to nothing.\npub record DirEntry {\n    /// The entry\'s name in its directory, such as `notes.txt`.\n    pub name: String,\n    /// The path given to `read_dir`, then `/`, then `name`, so a path given\n    /// with a trailing `/` gives `//`.\n    pub path: String,\n    /// `true` if the entry is not a directory: a file, or another kind of\n    /// entry such as a pipe.\n    pub is_file: Bool,\n    /// `true` if the entry is a directory.\n    pub is_dir: Bool,\n}\n\n// List the entries in `path`, sorted by name.\n//\n// Sorted because directory order is unspecified by the operating system; the\n// sort happens in the runtime.\n//\n// Builds each `DirEntry` here rather than in the runtime: a record built in Rust\n// would put Nova\'s field layout in two places, which is the drift this module\'s\n// boundary exists to avoid. The cost is one extra `fs_kind` call per entry.\n/// Returns the entries of the directory `path`, sorted by name.\n///\n/// Names are compared byte by byte, so `B.txt` comes before `a.txt`. Names\n/// beginning with `.` are listed, but `.` and `..` are not, and a name that\n/// is not valid Unicode has U+FFFD in place of its bad bytes. Returns an\n/// `IoError` of kind `NotFound` if nothing exists at `path`, and\n/// `PermissionDenied` if the system denies access; a file at `path` fails\n/// with a kind that depends on the system.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\npub async fn read_dir(path: String) -> Result<[DirEntry], IoError> {\n    let status = fs_read_dir(path)\n    if status != 0 {\n        return Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n    }\n    let names = fs_take_string_array()\n    let n = names.len()\n    // A repeat array, then every slot overwritten. `[init; n]` evaluates `init`\n    // exactly ONCE, so all `n` slots start as the same heap object -- harmless\n    // only because the loop below replaces each one. Do not read a slot before\n    // assigning it.\n    let mut out = [DirEntry { name: \"\", path: \"\", is_file: false, is_dir: false }; n]\n    let mut i = 0\n    while i < n {\n        let name = names[i]\n        let full = \"${path}/${name}\"\n        let k = fs_kind(full)\n        out[i] = DirEntry { name: name, path: full, is_file: k == 1, is_dir: k == 2 }\n        i = i + 1\n    }\n    Ok(out)\n}\n\n// --- File, OpenOptions and open ---\n//\n// Unlike everything above, which opens, acts and closes inside one intrinsic\n// call, a `File` is a handle that survives between calls -- Nova\'s first\n// value holding an OS resource that way. Nova has no destructors, so\n// explicit `close` is the only release mechanism; a `File` that is never\n// closed leaks its descriptor until the process exits.\n\n// Options for `open`. Each flag forwards onto `std::fs::OpenOptions`\'s own\n// method of the same name, in the order `open` below passes them to\n// `file_open`: read, write, append, truncate, create, create_new.\n//\n// No chainable builder: a receiver-mutating method cannot be called on a\n// temporary (`E0060`, measured), so `OpenOptions::reading().with_write()`\n// does not compile in this language. Use one of the three constructors\n// below, or build an exotic combination with `let mut o =\n// OpenOptions::default()` followed by field assignment.\n/// How `open` opens a file: to read, write or append, and whether to create\n/// or truncate it.\n///\n/// `reading()`, `writing()` and `appending()` give the usual sets. For any\n/// other, start from `OpenOptions::default()`, which sets every flag to\n/// `false`, in a `let mut` binding, and assign `true` to the flags wanted:\n/// `read`, `write`, `append`, `truncate`, `create`, and `create_new`, which\n/// creates the file and fails if it already exists. `open` returns an\n/// `IoError` of kind `Other` for a set it cannot honour, such as one with\n/// none of `read`, `write` and `append`, or one with `create` or `truncate`\n/// but neither `write` nor `append`.\npub record OpenOptions {\n    read: Bool,\n    write: Bool,\n    append: Bool,\n    truncate: Bool,\n    create: Bool,\n    create_new: Bool,\n}\n\n// Every flag false. Passed to `open` as-is this fails -- measured:\n// `std::fs::OpenOptions` itself requires at least one of read/write/append\n// -- so this exists as the base for field assignment building an exotic\n// combination, not as an option set meant to be used directly.\nimpl Default for OpenOptions {\n    fn default() -> OpenOptions {\n        OpenOptions {\n            read: false,\n            write: false,\n            append: false,\n            truncate: false,\n            create: false,\n            create_new: false,\n        }\n    }\n}\n\nimpl OpenOptions {\n    /// Returns options that open an existing file for reading only.\n    pub fn reading() -> OpenOptions {\n        OpenOptions {\n            read: true,\n            write: false,\n            append: false,\n            truncate: false,\n            create: false,\n            create_new: false,\n        }\n    }\n\n    // Write + create + truncate: start this file fresh, creating it if it is\n    // not there yet and discarding any content it already has.\n    /// Returns options that open a file for writing only, creating it if it\n    /// does not exist and emptying it if it does.\n    pub fn writing() -> OpenOptions {\n        OpenOptions {\n            read: false,\n            write: true,\n            append: false,\n            truncate: true,\n            create: true,\n            create_new: false,\n        }\n    }\n\n    // Append + create: add to this file, creating it if it is not there\n    // yet, and leaving any content it already has in place.\n    /// Returns options that open a file for appending only, creating it if it\n    /// does not exist and keeping what it holds: every write goes to the end.\n    pub fn appending() -> OpenOptions {\n        OpenOptions {\n            read: false,\n            write: false,\n            append: true,\n            truncate: false,\n            create: true,\n            create_new: false,\n        }\n    }\n}\n\n// A handle to an open file. `fd` keys\n// `crates/nova-runtime/src/file.rs`\'s thread-local table of open\n// `std::fs::File`s -- it is not an OS file descriptor number, and nothing\n// about its value means anything to a Nova program beyond identifying an\n// entry in that table.\n//\n// `fd` is not privacy-enforced -- no record field in this language is -- so\n// a program can build `File { fd: 9999 }` naming no file this module ever\n// opened. That is safe by design: absence from the handle table is\n// indistinguishable from closedness, so a forged, stale, or already-closed\n// handle behaves exactly like the real thing does once closed (see `close`\n// below).\n/// A file opened by `open`, to read and write a piece at a time through\n/// `std/io`\'s `Read` and `Write`.\n///\n/// Copying a `File` copies the handle: every copy refers to the same open\n/// file. Nothing closes a `File` for you: call `close` when done, or it stays\n/// open until the program exits.\npub record File { fd: Int }\n\n// Open `path` under `options`.\n/// Opens the file at `path` as `options` say, and returns it.\n///\n/// Returns an `IoError` of kind `NotFound` if the file does not exist and\n/// `options` does not create it, or if the directory to hold it does not\n/// exist; `AlreadyExists` if `options` sets `create_new` and the file exists;\n/// `PermissionDenied` if the system denies access; and `Other` for options it\n/// cannot honour. A directory cannot be read as a file: depending on the\n/// system, opening it or reading it fails.\n///\n/// An `async fn` that never suspends: it blocks every task until it\n/// finishes.\n///\n/// Appending to a file, then reading it back:\n///\n/// ```\n/// async fn main() {\n///     let dir = \"${temp_dir()}/nova-std-fs-open\"\n///     create_dir_all(dir).await.unwrap()\n///     let path = \"${dir}/log.txt\"\n///     write_string(path, \"one\\n\").await.unwrap()\n///\n///     // `appending()` keeps what the file holds and adds each write at its end.\n///     let log = open(path, OpenOptions::appending()).await.unwrap()\n///     log.write(bytes_from_string(\"two\\n\")).await.unwrap()\n///     log.close().await.unwrap() // nothing closes a File for you\n///\n///     let reader = open(path, OpenOptions::reading()).await.unwrap()\n///     let text = reader.read_to_end().await.unwrap().to_string().unwrap()\n///     reader.close().await.unwrap()\n///     assert_eq(text, \"one\\ntwo\\n\")\n///     # remove_dir_all(dir).await.unwrap()\n/// }\n/// ```\npub async fn open(path: String, options: OpenOptions) -> Result<File, IoError> {\n    let status = file_open(\n        path,\n        options.read,\n        options.write,\n        options.append,\n        options.truncate,\n        options.create,\n        options.create_new,\n    )\n    if status == 0 { return Ok(File { fd: decode_count(fs_take_bytes()) }) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\nimpl File {\n    // Closes the handle. Idempotent: a second or third call also returns\n    // `Ok(())`, because Nova has no move checking, so `self` by value cannot\n    // stop a second call the way it would in a language with real\n    // ownership. Closing a stale or forged handle is the same no-op: the\n    // table lookup a close performs simply removes nothing.\n    /// Closes the file.\n    ///\n    /// It returns `Ok(())`, also when the file is already closed: closing\n    /// reports no error. Afterwards, reading, writing or flushing the file,\n    /// through any copy, returns an `IoError` of kind `Other`.\n    ///\n    /// An `async fn` that never suspends.\n    pub async fn close(self) -> Result<(), IoError> {\n        let status = file_close(self.fd)\n        if status == 0 { return Ok(()) }\n        Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n    }\n}\n\n// Any operation other than `close` against a handle this module\'s table does\n// not recognise -- already closed, stale, or forged -- reaches this same\n// `IoError { kind: Other }` by missing the same table lookup, whether the\n// operation is this function or `write_file`/`flush_file` below.\nasync fn read_file(f: File, max: Int) -> Result<Bytes, IoError> {\n    let status = file_read(f.fd, max)\n    if status == 0 { return Ok(fs_take_bytes()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\nimpl Read for File {\n    fn read(self, max: Int) -> Future<Result<Bytes, IoError>> { read_file(self, max) }\n}\n\n// The write\'s byte count crosses the boundary the same way `std/io`\'s\n// `write_stdout` decodes its own byte count: 8 little-endian bytes in the\n// payload slot, read back here by that same `decode_count` rather than a\n// second decoder.\nasync fn write_file(f: File, buf: Bytes) -> Result<Int, IoError> {\n    let status = file_write(f.fd, buf)\n    if status == 0 { return Ok(decode_count(fs_take_bytes())) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\nasync fn flush_file(f: File) -> Result<(), IoError> {\n    let status = file_flush(f.fd)\n    if status == 0 { return Ok(()) }\n    Err(IoError { kind: io_error_kind_of(status), message: fs_last_error_message() })\n}\n\nimpl Write for File {\n    fn write(self, buf: Bytes) -> Future<Result<Int, IoError>> { write_file(self, buf) }\n    fn flush(self) -> Future<Result<(), IoError>> { flush_file(self) }\n}\n";
Expand description

std/fs/lib.nova.