Filesystem I/O v2

September 10, 2026 ยท View on GitHub

Status: implemented private additive profile; HOSTED GREEN under the v0.4.0 release baseline. This document records the current filesystem-io.v2 contract. Full standard- library completion and broader physical-filesystem support remain separate.

Audience: language users, standard-library authors, compiler contributors, and host-adapter implementers.

Filesystem I/O v2 extends the frozen Filesystem I/O v1 profile with metadata, directory listing, directory creation, removal, and explicit atomic replacement. The v1 operation identities, signatures, status domain, limits, provider authority model, and projections remain frozen. A v1 program continues to select Graph v41 and the v1 facts; reaching any v2 operation selects Graph v42 and the additive v2 facts.

Closed operation set

The compiler owns these stable operation identities. They are not authored imports and cannot be redefined by name or ID:

Source operationStable identityEffectSignature
file_statcore.host.file-statfs.read(borrow Slice<u8>, usize path_length) -> usize
file_listcore.host.file-listfs.read(borrow Slice<u8>, usize path_length, usize max) -> own Bytes
file_create_dircore.host.file-create-dirfs.write(borrow Slice<u8>, usize path_length) -> usize
file_removecore.host.file-removefs.write(borrow Slice<u8>, usize path_length) -> usize
file_write_atomiccore.host.file-write-atomicfs.write(borrow Slice<u8>, usize path_length, borrow Slice<u8>, usize data_length) -> usize

std.fs composes these as metadata, list, create_dir, remove, and write_atomic. The typed metadata wrapper returns a Copy FileInfo record with kind and size fields. The compiler primitive returns a packed usize: a regular file is size * 4 + 1; an admitted directory is 2 (size is zero). Arithmetic overflow is CAPACITY_EXCEEDED. create_dir and remove return 0 on success. write_atomic returns the exact data length on success.

Paths, listing, and accounting

Paths are bounded byte prefixes with a logical path_length; bytes outside the prefix are ignored. Non-root paths use v1's relative slash-separated grammar: components are nonempty, and ., .., NUL, backslash, and colon are rejected. The logical path is at most 4,096 bytes and need not be UTF-8. An empty logical path is admitted only by file_stat and file_list, where it denotes the provider's explicitly selected root. Empty paths remain invalid for create, remove, atomic write, read, and create-new write.

file_list returns immediate entry names as raw bytes, each followed by one NUL byte. Names are unique and sorted by unsigned raw-byte order; the result is canonical and contains no path separators. There are at most 1,024 names, each at most 4,096 bytes, and the complete NUL-terminated payload is at most 65,536 bytes. The requested max is also at most 65,536 bytes. The empty listing is the empty byte sequence.

Every attempted operation reserves first and never refunds. Reads and lists reserve their requested max; writes reserve data_length; stat, create-dir, and remove reserve zero. The invocation-wide limits are 64 operations and 1,048,576 reserved bytes. The reservation precedes logical-slice extent and path validation, provider dispatch, and result publication, preserving the same failure priority across backends. A provider result is checked against the requested bounds; listing results are additionally checked for the canonical raw-byte wire form and stat results for the packed representation.

All failures use the frozen semaprax.filesystem.v1 status domain and codes: INVALID_PATH (1), NOT_FOUND (2), ALREADY_EXISTS (3), CAPACITY_EXCEEDED (4), IO_FAILURE (5), AUTHORITY_DENIED (6), and INVALID_FILE_TYPE (7). Zero is success. Failure selection is sticky and settlement occurs before result publication, as in v1.

Provider and target boundary

The caller injects a mutable FileProvider for one invocation. Its retained root or fixture state is the explicit authority; there is no current-directory, home-directory, WASI, Node fs, libc path lookup, or ambient callback. The fixture provider is deterministic in-memory state, and the denied provider returns AUTHORITY_DENIED.

The Unix ScopedFileProvider retains an opened directory descriptor, walks relative parent descriptors without following symlinks, and admits regular files and directories only. write_atomic writes a private temporary file and uses the provider's explicit atomic rename within the selected parent. This provides replacement atomicity at that provider boundary; it makes no durability, fsync, crash-recovery, or rollback claim. A failed physical write or later language failure cannot be treated as a transaction that restores prior filesystem state.

The checked interpreter, native C path, and Core Wasm lowering all retain the same operation set, validation order, status normalization, and ownership rules. Core Wasm appends the v2 imports spx_filesystem_stat_v2, spx_filesystem_list_v2, spx_filesystem_create_dir_v2, spx_filesystem_remove_v2, and spx_filesystem_write_atomic_v2 after the frozen v1 filesystem prefix. Import presence does not grant a provider or filesystem authority.

Graph v42 and Project v15

Reachable v2 calls select semaprax.graph.v42. Its filesystem fact uses semaprax.filesystem.v2 and records the retained checked calls, the v1 status domain, the 4,096/65,536/1,048,576/64 limits, root-capable stat/list operations, kind-plus-four-times-size-file1-directory2 stat encoding, sorted-unique-immediate-raw-names-nul-terminated listing encoding, and reserve-first no-refund accounting. Graph verification binds these facts to the exact retained HIR calls and rejects a v1 schema or forged fact.

The private std/fs manifest selects Project v15 with profile filesystem-io.v2, command shape fn () -> bool, no web exports, and exactly the explicit capabilities ["fs.read", "fs.write"]. It uses the existing std.io and std.path.value dependencies. Project v14 and filesystem-io.v1 remain frozen and separate.

Release evidence and remaining scope

The implemented v2 operations, projections, package composition and admitted provider/backend checks use the hosted-green v0.4.0 baseline. This status does not change the Unix provider's replacement-only atomicity or supply a new physical provider on another operating system. Streaming, stronger durability, crash recovery, general traversal and the full Everyday profile remain governed by their own implementation and support gates.