std.fs
std.fs is the blocking, capability-anchored filesystem module. Operations on fs.File and fs.Dir may block the calling OS thread. Code running on a cooperative executor uses the distinct std.async.fs file contract or moves a deliberately coarse blocking job through async.blocking.
std.io provides synchronous byte interfaces and nominal standard streams. Raw platform resource interop is outside the portable target.
Migration status: RFC 0022 removes
fs.cwdand establishes that safe filesystem behavior cannot depend on a live mutable process-global CWD. RFC 0023 fixes the replacement described below: lossless path roles, strict relative/absolute refinements andDir-anchored safe operations. RFC 0025 adds capability-anchored streaming directory traversal and deletes glob policy from corestd. RFC 0063 completes the file-I/O direction:std.fsis blocking,std.async.fsis cooperative-safe, and their nominalFiletypes expose only offset-based short operations. Existing helpers that accept unanchoredstrpaths or return whole-file futures are transitional source, not target API. RFC 0066 removesfs.open_root,MissingRoot, and the global named-root registry; executable roots instead receive exact private@hostloans. RFC 0068 supplies each loan through one explicitly selected per-instancezynx-launchdocument;pathandprocess_startare explicit sources and no binding name is special. RFC 0069 adds one closed allow-only rights set to every directory grant. RFC 0070 adds the sole capability-relative namespace mutations and extends only directory policy throughzynx-launchv2. RFC 0071 fixes the previously unspecifiedOpenModeas the explicit product of file access and one of five complete open dispositions; the old.Readshorthand and option-style modes are not target API. RFC 0072 adds explicit full-integrity barriers for exact file objects and directory namespaces, with cold cooperative-safe counterparts.zynx-launchv3 adds only the independentopen_file.syncandsync_directoryrights; unsupported targets fail closed. RFC 0073 keeps append out ofOpenMode: it adds separateAppendAccess,AppendMode, and nominal blocking/asynchronousAppendFileowners.zynx-launchv4 adds onlyopen_file.append; targets that cannot certify atomic EOF placement reject the grant or reportIOError.Unsupportedrather than emulate it. RFC 0074 closes durable single-file publication without adding a public abstraction: there is noPublication,StagedFile,PreparedFile,write_atomic, generated staging-name policy, cleanup-on-drop, recovery transaction, new right, orzynx-launchv5. Programs compose the existing explicitCreateNew, short-write loop,File.sync,drop,Dir.rename, and exact-parentDir.syncoperations.
Contract
- Portable safe operations obtain authority from
fs.Dirand accept checkedfs.RelativePath; they never resolve an ambient relativestr. fs.Fileis a blocking affine resource.async.fs.Fileis a separate affine resource whose operations do not block a cooperative worker.- Both ordinary
Filecontracts are offset-based. They have no hidden current position,seek,tell, or directio.Reader/io.Writerimplementation. - One
read_atorwrite_atmay transfer fewer bytes than requested. Offsets areu64; byte-buffer extents and returned transfer lengths areusize. - Every ordinary
Fileopen states both retainedFileAccessand exactOpenDisposition. All fifteen combinations are valid; there are no Boolean create/truncate flags, implicit defaults, or access aliases. - Append is a separate nominal resource, not a
FileAccesscase. EveryAppendFile.writeis one possibly short, indivisible append at the EOF chosen by the host for that call; it has no caller-provided offset and no hidden complete-write loop. - Core
stdhas no target whole-file read/write/copy helper and noread_text. Programs loop explicitly and cross bytes into text throughunicode.utf8.text. - There is no
existsquery. Attempt the authoritative open or metadata operation and handleio.IOError.NotFound. - Rename is rename only. It never hides a cross-filesystem copy-and-remove fallback or promises rollback. Cross-filesystem failure is the exact
io.IOError.CrossDevicecase. - Crash durability is explicit: synchronize completed file data before rename, then synchronize every affected directory namespace. Drop, close, rename, and ordinary buffered-I/O flush are not durability barriers.
- Directory traversal remains the synchronous lending contract from RFC 0025. There is no async traversal or
for awaitfilesystem cursor.
Accepted Path And Directory Target
RFC 0023 gives filesystem spellings and filesystem authority separate types:
Path
^Path
RelativePath
^RelativePath
AbsolutePath
^AbsolutePath
Dir
^DirPath is a borrowed, target-canonical path view and ^Path owns the same representation. Construction is checked, never implicit from str, rejects an empty path and embedded NUL, and does not normalize spelling. POSIX preserves the exact non-NUL path bytes, Windows uses lossless WTF-8, and WASI requires strict UTF-8. bytes() exposes the lossless borrowed representation without allocation; text() is a checked, allocation-free strict-UTF-8 view; clone() explicitly allocates an independent ^Path.
Path and text views are counted; they do not promise a trailing NUL at their own end. A size-aware host boundary passes pointer plus byte size. A platform call that requires terminated path text uses an explicit trusted adapter backed by a complete owner; it never forwards a borrowed str.ptr as though the view were a C string. When the native boundary is genuinely pointer-only, its wrapper may lend a std.ffi.CStr from proven terminated owner storage. POSIX path bytes are not assumed to be UTF-8, and Windows path conversion remains a separate platform adapter rather than a universal CStr cast.
let path: fs.Path in _ = try fs.Path(input);
let raw = path.bytes();
let text = try path.text();
let owned: ^fs.Path = try path.clone();RelativePath is a checked refinement used beneath a directory capability. It is non-empty and rejects rooted paths, Windows drive-relative/UNC/device prefixes, empty components, . and ... Its constructor validates without allocation or cleanup; POSIX uses / as the separator, while Windows checks both / and \ while preserving the original bytes.
AbsolutePath is a checked, allocation-free refinement for paths that need no ambient base. POSIX requires a leading /; Windows requires a complete drive, UNC or supported extended path and rejects drive-relative and rooted-current- drive forms such as C:tmp and \tmp. It preserves spelling and does not prove filesystem-object identity: namespace mutation can retarget a later lookup. Consuming an owned ^Path may produce ^AbsolutePath without copying its buffer.
The path role family deliberately has no implicit text conversion, mutable byte access, normalization, canonicalization, separator conversion or ordinary Eq/Hash/ordering contract. Comparing bytes() compares spelling, not file identity. PathError.Invalid reports failed path/refinement construction.
Dir capabilities
^Dir is an affine owner of an open directory capability. Moving it transfers the host resource; dropping it closes exactly once. The opaque target type has an audited Send + Sync bridge, so a borrowed Dir supports shared concurrent operations on every supported backend. It is not Copy, does not expose path(), and has no ordinary Clone, Eq, Hash or ordering. Creating another independently owned handle is explicit and fallible:
let worker_root: ^fs.Dir = try project.duplicate();Every portable safe relative operation is anchored in a Dir and accepts a RelativePath; there is no target free function that resolves an unanchored relative path:
let source_path: fs.Path in _ = try fs.Path("src/main.zx");
let source: fs.RelativePath in _ = try fs.RelativePath(source_path);
let file: ^fs.File = try project.open_file(
source,
fs.OpenMode {
access: .Read,
disposition: .Existing,
},
);The Dir boundary is enforced across every component, including symlinks, junctions, reparse points, renames and concurrent replacement. A lookup may not escape through a link even when the target would remain inside some wider ancestor capability. Opening a nested Dir creates a new, narrower boundary. Implementations must resolve relative to open handles and fail closed; joining a remembered textual prefix is not conforming.
The capability does not freeze namespace contents. Code must open the desired object atomically and continue through its returned handle; a separate exists() followed by open() is not a security check.
Host-loan acquisition
The executable composition root declares every initial directory authority:
// RFC 0066/0068 target; implementation pending.
import std.fs;
@host let project: fs.Dir in _;
@host let assets: fs.Dir in _;
fn main() {
build(project, assets);
}The host satisfies the complete typed binding set atomically before main and retains every backing owner until entry quiescence. Only the direct body of main may resolve these names; libraries and helpers receive ordinary explicit Dir parameters. Knowing the source name does not authorize lookup, and there is no fs.open_root, MissingRoot, registry, enumeration, default, or first root. Code that needs an independent owner calls project.duplicate().
RFC 0068 removes every magic binding-name convention. A launch grant for any declared Dir may select either an explicit path or the explicit process_start source. For example, project may receive the stable directory captured at process start only when its zynx-launch entry says so. A binding named start receives no special treatment, and WASI never treats its first preopen as an implicit grant.
RFC 0069 requires a policy.rights list. The version-1 registry is exactly:
duplicate
open_directory
entries
walk
metadata
open_file.read_at
open_file.write_at
open_file.create
open_file.truncateThe list is allow-only. Omission, an unknown right, or a duplicate is invalid; an explicit empty list is a valid deny-all capability. There is no all, read, write, mutate, or preset whose meaning could grow with the API. Operations not listed above—including remove, rename, link, watch, execute, append, and atomic replacement—gain no authority from a version-1 grant.
Every requested file-open property checks its own right. Read/write access requires open_file.read_at/open_file.write_at; create requires open_file.create; truncate requires both open_file.truncate and open_file.write_at. The complete sum is checked before the first path lookup. An open-or-create request therefore needs open_file.create even if the entry happens to exist, and a create-or-truncate request needs create, truncate and write authority even if the entry happens to be absent. entries and walk are independent rights: the private descent needed by walk does not publish a nested Dir or imply public entries/open_directory authority.
duplicate produces an owner with exactly the same policy. A nested directory inherits exactly the same rights over its narrower namespace. Neither operation can widen authority. Symlink, junction, mount, and reparse-point containment is still an unconditional RFC 0023 invariant, not a launch-policy switch.
Exact file-open modes
RFC 0071 gives OpenMode exactly two orthogonal, explicit axes:
// RFC 0071 target; implementation pending.
export enum FileAccess {
Read,
Write,
ReadWrite,
}
export enum OpenDisposition {
Existing,
OpenOrCreate,
CreateNew,
TruncateExisting,
CreateOrTruncate,
}
export struct OpenMode {
export let access: FileAccess,
export let disposition: OpenDisposition,
}The disposition says exactly what one open operation does. This matrix assumes the final entry is either absent or an existing regular file:
OpenDisposition | Final entry absent | Existing regular file |
|---|---|---|
Existing | io.IOError.NotFound | Open without changing its length. |
OpenOrCreate | Create and open. | Open without changing its length. |
CreateNew | Create and open. | io.IOError.FileExists. |
TruncateExisting | io.IOError.NotFound | Truncate to zero and open. |
CreateOrTruncate | Create and open. | Truncate to zero and open. |
Every combination of the three access cases and five dispositions is valid. Access describes only the authority retained by the returned file:
.Readretainsread_ataccess;.Writeretainswrite_ataccess;.ReadWriteretains both.
A later data operation not selected by access returns io.IOError.Permission without performing host I/O. Creation and truncation are effects of the open itself, not access retained by the file. Consequently this is valid and intentionally distinct from a writable file:
// RFC 0071 target; implementation pending.
let emptied: ^fs.File = try project.open_file(
path,
fs.OpenMode {
access: .Read,
disposition: .TruncateExisting,
},
);The open requires read, truncate and write-at directory rights, but emptied retains only read access. Creation likewise does not imply later write access. The exact policy sum is:
| Requested property | Required directory right |
|---|---|
FileAccess.Read | open_file.read_at |
FileAccess.Write | open_file.write_at |
FileAccess.ReadWrite | both data rights |
OpenDisposition.OpenOrCreate or .CreateNew | additionally open_file.create |
OpenDisposition.TruncateExisting | additionally open_file.truncate and open_file.write_at |
OpenDisposition.CreateOrTruncate | additionally create, truncate and write-at rights |
CreateNew is one atomic create-if-absent operation. Every existing final entry, including a directory, symbolic link, or dangling symbolic link, yields io.IOError.FileExists; the final link is never followed. Of two concurrent CreateNew operations for the same entry, exactly one may succeed. There is no exists preflight, retry loop, hidden random name, or replace behavior.
The other four dispositions may follow a final symbolic link only when the backend proves that the resolved object remains beneath the same source Dir boundary. If it cannot preserve that invariant across the complete lookup, it fails closed. No mode weakens containment or turns a remembered textual path into authority.
The previous shorthand and option-family spellings are removed target forms:
// Rejected target spellings.
project.open_file(path, .Read);
project.open_file(path, fs.OpenOptions { create: true, truncate: false });There is no default mode, builder, Boolean flag set, bitmask, alias, or append case. Append, temporary-file naming and publication policy remain separate contracts rather than additional OpenMode state. RFC 0072's exact sync operations are independent capabilities, not mode fields.
Exact atomic append files
RFC 0073 represents append as a separate nominal resource rather than a sixth OpenDisposition, another FileAccess case, or a Boolean open option:
// RFC 0073 target; implementation pending.
export enum AppendAccess {
Append,
ReadAppend,
}
export struct AppendMode {
export let access: AppendAccess,
export let disposition: OpenDisposition,
}
fn fs.Dir.open_append(
path: fs.RelativePath,
mode: fs.AppendMode,
) throws(io.IOError) -> ^fs.AppendFile;
fn fs.AppendFile.read_at(
&self,
offset: u64,
buffer: &[u8],
) throws(io.IOError) -> usize;
fn fs.AppendFile.write(
&self,
data: bytes.Bytes,
) throws(io.IOError) -> usize;
fn fs.AppendFile.sync(&self) throws(io.IOError);All ten products of two access cases and RFC 0071's five dispositions are valid. Append retains only atomic append. ReadAppend additionally retains read_at on the same underlying file object; reads use explicit offsets, do not consume a cursor, and do not snapshot or change append placement. The disposition still performs its exact existing/create/truncate action during open.
One successful write(data) appends exactly the returned prefix data[0..<n], contiguously, at the EOF selected atomically for that host operation. Another conforming append writer to the same underlying file object cannot overwrite that prefix or insert bytes inside it. The operation may be short: atomic EOF placement applies to that one returned prefix, not to the caller's whole logical record. A retry loop is explicit and consists of multiple append operations that may be separated by concurrent appends. There is no offset argument, hidden position, write_at, seek, tell, write_all, record framing, reservation, locking, or transaction API on AppendFile.
The complete policy sum is checked before lookup:
| Requested property | Required directory right |
|---|---|
Every AppendMode | open_file.append |
AppendAccess.ReadAppend | additionally open_file.read_at |
OpenDisposition.OpenOrCreate or .CreateNew | additionally open_file.create |
OpenDisposition.TruncateExisting | additionally open_file.truncate |
OpenDisposition.CreateOrTruncate | additionally create and truncate rights |
Unlike an RFC 0071 writable File open, append truncation does not require or retain open_file.write_at: open_file.append is the independently authorized mutation capability and AppendFile never exposes arbitrary-offset writes. Calling read_at through append-only access returns IOError.Permission before host I/O. open_file.sync is also independent; when carried from the source Dir, it authorizes AppendFile.sync without changing append/read access. Drop and close do not synchronize.
The cooperative-safe surface uses a distinct nominal owner and ordinary cold Future producers:
// RFC 0073 target; implementation pending.
fn async.fs.open_append<region L>(
root: fs.Dir in L,
path: fs.RelativePath in L,
mode: fs.AppendMode,
) -> ^Future<^async.fs.AppendFile throws(io.IOError)> in L;
fn async.fs.AppendFile.read_at<region L>(
&self in L,
offset: u64,
output: ^OutputSpan<u8>,
) -> ^Future<^OutputSpan<u8> throws(io.IOError)> in L;
fn async.fs.AppendFile.write<region L>(
&self in L,
data: bytes.Bytes in L,
) -> ^Future<usize throws(io.IOError)> in L;
fn async.fs.AppendFile.sync<region L>(
&self in L,
) -> ^Future<void throws(io.IOError)> in L;Construction performs no check, submission, allocation, or filesystem effect. After start, blocking and asynchronous forms have identical short-write, atomic-placement, identity, access, disposition, right, and sync semantics. Cancellation retains every owner and loan until actual completion or proven quiescence; it neither rolls an append back nor reports whether it happened.
zynx-launch v4 copies all sixteen v3 directory rights and the Network schema unchanged, then adds exactly open_file.append. Versions 1 through 3 reject that string. A target may accept it only when it can certify atomic EOF placement for every resulting AppendFile: for example through POSIX O_APPEND, Windows FILE_APPEND_DATA, or a WASI append stream whose host contract preserves the same property. It must not emulate append as metadata size plus write_at. A statically unrepresentable v4 grant fails before main; a concrete weak, remote, or otherwise unsupported filesystem discovered only after open reports IOError.Unsupported = 18, never silent best effort.
Namespace mutations
RFC 0070 adds exactly three capability-relative mutation primitives. The blocking surface is:
// RFC 0070 target; implementation pending.
enum RemoveKind {
NonDirectory,
EmptyDirectory,
}
fn fs.Dir.create_directory(
path: fs.RelativePath,
) throws(io.IOError);
fn fs.Dir.remove(
path: fs.RelativePath,
kind: fs.RemoveKind,
) throws(io.IOError);
fn fs.Dir.rename(
source_path: fs.RelativePath,
destination_root: fs.Dir,
destination_path: fs.RelativePath,
) throws(io.IOError);create_directory creates only the final directory entry. Missing parents and an existing destination are errors. It returns void; opening the new directory is a separate open_directory operation with its own right.
remove(..., .NonDirectory) removes one non-directory entry, including a final symbolic link itself, without following that final link. remove(..., .EmptyDirectory) removes only an empty directory. The caller writes the kind; there is no hidden stat, remove-any dispatch, recursive mode, or exists preflight.
rename performs one namespace rename from the receiver Dir into the explicit destination Dir. A compatible existing destination is replaced. Both final components are operated on as entries rather than followed links. A cross-filesystem operation returns io.IOError.CrossDevice; it never becomes copy plus remove. Atomic namespace visibility does not imply crash durability.
The cooperative-safe spellings are ordinary cold Future producers, never async fn declarations:
// RFC 0070 target; implementation pending.
fn async.fs.create_directory<region L>(
root: fs.Dir in L,
path: fs.RelativePath in L,
) -> ^Future<void throws(io.IOError)> in L;
fn async.fs.remove<region L>(
root: fs.Dir in L,
path: fs.RelativePath in L,
kind: fs.RemoveKind,
) -> ^Future<void throws(io.IOError)> in L;
fn async.fs.rename<region L>(
source_root: fs.Dir in L,
source_path: fs.RelativePath in L,
destination_root: fs.Dir in L,
destination_path: fs.RelativePath in L,
) -> ^Future<void throws(io.IOError)> in L;Construction performs no policy check or host work. Start checks authority before the first host effect or blocking-pool submission. Cancellation after start does not roll the mutation back, and terminal completion waits for true quiescence while retaining every directory and path loan.
zynx-launch v2 copies the complete v1 directory and Network schemas unchanged and adds exactly:
create_directory
remove.non_directory
remove.empty_directory
rename.source
rename.destination_replaceCreate and each explicit remove kind require their corresponding right. Rename requires rename.source on the source root and rename.destination_replace on the destination root, even when no destination exists at the time of the call. Version 1 remains frozen and grants none of these operations. There is still no mutate, all, or future-expanding preset.
Explicit durability barriers and publication recipe
RFC 0072 adds exactly two blocking durability primitives; RFC 0073 reuses the same file-integrity operation on its separate append owner:
// RFC 0072 target; implementation pending.
fn fs.File.sync(
&self,
) throws(io.IOError);
fn fs.AppendFile.sync(&self) throws(io.IOError);
fn fs.Dir.sync() throws(io.IOError);File.sync and AppendFile.sync establish a full file-integrity barrier for changes to that exact underlying file object which completed and happen-before the barrier through Zynx operations or explicit caller/host coordination. They cover data, length, and metadata needed to recover those data. The exclusive loan orders operations through the same Zynx owner; it does not order another handle, process, mapping, or external actor. A file barrier does not persist a directory entry, file name, another hard link, or any containing directory. Later, concurrent, or uncoordinated external writes are outside it.
Dir.sync establishes a namespace-persistence barrier for mutations completed earlier on that exact directory object, including creation, removal, rename, and destination replacement. It does not synchronize child-file contents, descendant directories, or ancestors. A same-directory rename needs one directory barrier. A cross-directory rename needs a barrier for every affected directory; successful completion of one does not imply completion of the other.
RFC 0074 accepts no nominal publication owner or helper. Its sole portable single-file publication recipe remains visible in program order. First open the exact directory whose entries will change; staging_name and destination_name below are direct-child names relative to that same parent, not multi-component paths through a deeper directory:
// RFC 0070-0074 target; schematic because core std has no write-all helper.
let staging: ^fs.File = try parent.open_file(
staging_name,
fs.OpenMode {
access: .Write,
disposition: .CreateNew,
},
);
// Perform the explicit short-write retry loop with (&staging).write_at(...).
try (&staging).sync();
drop(<-staging);
try parent.rename(
staging_name,
parent,
destination_name,
);
try parent.sync();The write step is an explicit retry loop: each possibly short write_at advances the caller-maintained u64 offset until all intended bytes have been accepted. A zero-progress success must not become a busy loop. drop closes the exact staging handle only; it neither synchronizes nor removes its name.
Dir.sync covers entries of that exact Dir, not entries in descendants. If the destination is root/cache/result, the recipe first opens the cache directory, opens and renames direct-child names beneath it, and synchronizes that cache capability. Synchronizing only root is insufficient. A rename between different exact parent directories requires synchronizing both; that is outside the single-directory recipe above.
Capability authority also does not provide isolation or a namespace lock. After drop(<-staging), rename selects its source by path rather than by the closed file identity. The caller must coordinate every external actor able to remove or replace either name, or use an application protocol that validates identity and version. Core std does not make this coordination implicit.
Every failure is an ordinary value and no step rolls an earlier step back. In particular:
- before rename succeeds, the destination is unchanged, while the staging entry may remain and reflects only the barriers already completed;
- after rename succeeds but before
Dir.syncsucceeds, the destination names the new file but namespace durability is unconfirmed and the old content is not restored automatically; - cancellation, process failure, or restart between phases has no universal recovery classification. The application must inspect its own names, content, and version markers and decide whether to resume, retain, or remove an abandoned staging entry.
A retry therefore cannot in general infer or repair the prior persistence state. Successful sync means that the adapter executed its certified stable-storage barrier; it does not promise survival against lying firmware, lost remote acknowledgements, or physical media failure.
There is only full sync: no flush, fsync, fdatasync, sync_data, durability Boolean or enum, open-mode durability flag, implicit sync on drop, or high-level publish_file/write_atomic target helper. RFC 0074 also rejects Publication, StagedFile, PreparedFile, generated temporary names, cleanup-on-drop, rollback, and universal recovery policy. Generated names would hide entropy, allocation, collision retry, cleanup, and recovery decisions; the caller therefore chooses the CreateNew staging name and owns every such decision explicitly.
The recipe needs only existing launch rights: open_file.write_at, open_file.create, open_file.sync, rename.source, rename.destination_replace, and sync_directory. Explicit application cleanup additionally needs remove.non_directory, but cleanup is not a publication step. RFC 0074 adds no right or launch version: version 3 already expresses the recipe, and version 4 is the compatible append-capable superset.
The cooperative-safe forms are cold Future producers:
// RFC 0072 target; implementation pending.
fn async.fs.File.sync<region L>(
&self in L,
) -> ^Future<void throws(io.IOError)> in L;
fn async.fs.AppendFile.sync<region L>(
&self in L,
) -> ^Future<void throws(io.IOError)> in L;
fn async.fs.sync_directory<region L>(
root: fs.Dir in L,
) -> ^Future<void throws(io.IOError)> in L;Construction performs no right check, barrier, or blocking-pool submission. Cancellation before start performs no barrier. Once submitted, cancellation must retain the handle and wait for real completion or proven quiescence; it does not undo writes or rename and does not itself reveal whether storage made them durable.
zynx-launch v3 copies all fourteen v2 directory rights and its complete Network schema unchanged, then adds exactly:
open_file.sync
sync_directoryopen_file.sync neither implies nor is implied by open_file.read_at, open_file.write_at, create, or truncate. When present, it is retained by an opened file independently of FileAccess; this permits, for example, a read-retained handle to synchronize truncation performed by its open operation without gaining write_at. sync_directory neither implies nor is implied by create, remove, or rename rights. Missing authority yields io.IOError.Permission before host work (at first start for the async forms). RFC 0073 carries the same independent open_file.sync proof into an AppendFile; append authority alone never grants synchronization.
Versions 1 and 2 are frozen and reject the new rights. A launch plan requiring a barrier that its target adapter cannot represent is rejected before main; the implementation never substitutes a no-op or best effort. File sync uses a certified full-integrity mapping: Darwin/macOS requires fcntl(F_FULLFSYNC) with no fallback to ordinary fsync, Windows uses FlushFileBuffers, and other POSIX/WASI adapters must prove an equally strong contract. Directory sync is exposed only after verifying exact directory-handle semantics; standard Windows and uncertified portable WASI adapters reject sync_directory. io.IOError.Unsupported = 18 covers a concrete mounted filesystem or resource whose inability can only be discovered at operation time.
Directory traversal
RFC 0025 fixes two explicit streaming operations on Dir:
fn entries<region Root>(self in Root)
throws(io.IOError | AllocError)
-> ^DirectoryEntries in Root;
fn walk<region Root>(self in Root)
throws(io.IOError | AllocError)
-> ^DirectoryWalk in Root;entries() enumerates direct children. walk() enumerates every descendant in depth-first pre-order, excluding the starting Dir itself. A subtree is selected by first opening a narrower Dir; neither operation accepts a root path:
let source: ^fs.Dir = try root.open_directory(relative);
let walk: ^fs.DirectoryWalk in _ = try source.walk();Both results are concrete fallible lending cursors rather than Generator instances:
struct DirectoryEntries {
fn next<region Step>(&self in Step)
throws(io.IOError | AllocError)
-> DirectoryEntry? in Step;
}
struct DirectoryWalk {
fn next<region Step>(&self in Step)
throws(io.IOError | AllocError)
-> WalkEntry? in Step;
}Each successful next() lends a small Copy entry valid only for that step. The loan must end before the cursor advances, so one internal path buffer can be reused without allocating an owner for every entry. The required explicit loan binding makes that lifetime visible:
struct DirectoryEntry: Copy {
fn name<region Loan>(self in Loan) -> RelativePath in Loan;
fn kind(self) -> DirectoryEntryKind;
}
struct WalkEntry: Copy {
fn path<region Loan>(self in Loan) -> RelativePath in Loan;
fn kind(self) -> DirectoryEntryKind;
}let entries = try root.entries();
for try entry: fs.DirectoryEntry in _ in &entries {
inspect(entry.name(), entry.kind());
}
let walk = try root.walk();
for try entry: fs.WalkEntry in _ in &walk {
if entry.kind() == .File {
inspect(entry.path());
}
}DirectoryEntry.name() lends exactly one child component as a RelativePath. WalkEntry.path() lends the descendant path relative to the Dir on which walk() was called. A path that must outlive the current step is cloned explicitly:
let walk = try root.walk();
var saved: ^fs.RelativePath? = null;
for try entry: fs.WalkEntry in _ in &walk {
if should_keep(entry) {
saved = try entry.path().clone();
break;
}
}Both entries report one closed kind:
enum DirectoryEntryKind {
File,
Directory,
Link,
Other,
}There is no Unknown. When a host directory record omits the kind, the cursor performs a non-following metadata lookup. kind() remains an observation made during traversal, not a stable identity guarantee; code that needs stable authority opens a File or nested Dir immediately and continues through the handle.
Traversal never follows symbolic links, junctions or reparse points. Such objects are yielded as .Link, but cannot cross the capability boundary. There is no follow flag, maximum-depth policy, callback, filter or implicit parallelism. Sibling order is host-defined; callers that need deterministic order collect and sort explicitly.
The first cursor or metadata error terminates traversal and is propagated by for try before another loop-body entry. It is never skipped or stored as a hidden final error. Exhaustion and cursor drop close every open directory handle exactly once. A loop over a fresh traversal temporary drops it on break or propagated error and therefore closes its handles. A loop over a named traversal cursor only borrows it exclusively: break preserves its open state for an explicit later resume, and its handles close at exhaustion or its later owner drop. walk() is iterative and stack-safe, retaining an explicit handle stack proportional to traversal depth. All descent remains handle-relative and fails closed across concurrent namespace changes.
These cursors use the structurally fallible lending-loop rule documented in std.iter; they do not add a generic fallible iterator interface. Core std has no replacement glob or path-pattern matcher. A future matcher, if justified, belongs in a separate checked policy type rather than filesystem traversal.
Blocking Files
fs.File is an affine owner of a blocking host file handle. The synchronous open is anchored in a Dir and retains exactly the access written in its OpenMode:
// RFC 0063/0071 target; implementation pending.
let path: fs.RelativePath in _ = try fs.RelativePath(
try fs.Path("records.bin"),
);
let mode = fs.OpenMode {
access: .ReadWrite,
disposition: .Existing,
};
let file: ^fs.File = try root.open_file(path, mode);The primitive data operations use explicit absolute file offsets:
// RFC 0063 target; implementation pending.
let read: usize = try (&file).read_at(
offset: 0 as u64,
buffer: buffer,
);
let written: usize = try (&file).write_at(
offset: 4096 as u64,
data: bytes.Bytes("record"),
);read_at and write_at each perform one possibly short operation. A result of 0 from read_at with a nonempty destination means EOF. Callers that require an exact transfer write the retry loop and offset arithmetic explicitly. write_at likewise reports the bytes written rather than silently becoming a complete-write helper.
When the file carries the independent open_file.sync capability, sync performs the RFC 0072 full file-integrity barrier:
// RFC 0072 target; implementation pending.
try (&file).sync();This right and operation are independent of retained data access. Calling sync without it returns io.IOError.Permission before host work.
Offsets are always u64, independent of pointer width. A file has no hidden cursor, so scheduling two offset operations cannot change their addresses by changing execution order. File therefore has no seek or tell and does not directly implement the cursor-shaped std.io.Reader, Writer, or Stream interfaces. A later sequential adapter, if justified, must be a distinct nominal owner such as FileCursor with its own visible u64 position.
Dropping ^fs.File closes the handle exactly once. It has no safe conversion to or from async.fs.File; code must choose the execution contract while opening the resource.
fs.AppendFile is likewise affine but has no arbitrary-offset write. It is opened explicitly through AppendMode and each call reports its one atomic append prefix:
// RFC 0073 target; implementation pending.
let log: ^fs.AppendFile = try root.open_append(
path,
fs.AppendMode {
access: .ReadAppend,
disposition: .OpenOrCreate,
},
);
let data = bytes.Bytes("one record\n");
let appended: usize = try (&log).write(data);
// `appended` may be smaller than data.length. Retrying is a new append.
let observed: usize = try (&log).read_at(
offset: 0 as u64,
buffer: buffer,
);The read and append operations address the same open file identity. Reading does not reserve EOF, move a cursor, or weaken the atomic placement of a later write. If the logical record must be indivisible, it must fit in one host append operation and the caller must handle a short result as a partial record; core std does not conceal that protocol behind write_all.
std.async.fs
std.async.fs owns cooperative-safe file operations. It reuses the path, directory, metadata, exact FileAccess/OpenDisposition/OpenMode, AppendAccess/AppendMode, and error vocabulary from std.fs, but its File and AppendFile owners are nominally distinct from their blocking counterparts:
// RFC 0063/0071 target; implementation pending.
import std.fs;
import std.async.fs as afs;
let path: fs.RelativePath in _ = try fs.RelativePath(
try fs.Path("records.bin"),
);
let mode = fs.OpenMode {
access: .Write,
disposition: .CreateNew,
};
let file: ^afs.File = try await afs.open_file(
root: root,
path: path,
mode: mode,
);Opening and file access return cold Future values. Creating the Future does not check the path or perform the open; after start, the same disposition, atomicity, containment, rights and retained-access rules apply as to blocking Dir.open_file. The data contract remains offset-based and short. Initialization-aware reads move the output session into the future because the runtime or kernel may retain its pointer while the operation is pending:
// RFC 0054 and RFC 0063 target; implementation pending.
var output = try (<-storage).output(4096);
output = try await (&file).read_at(
offset: 0 as u64,
output: <-output,
);
storage = (<-output).commit();Atomic append is also a cold operation and remains visibly short:
// RFC 0073 target; implementation pending.
let log: ^afs.AppendFile = try await afs.open_append(
root: root,
path: path,
mode: fs.AppendMode {
access: .Append,
disposition: .OpenOrCreate,
},
);
let appended: usize = try await (&log).write(
data: bytes.Bytes("one record\n"),
);Full-integrity synchronization uses the same cold contract:
// RFC 0072 target; implementation pending.
try await (&file).sync();
try await afs.sync_directory(root: root);The async module guarantees that a filesystem operation does not block a cooperative worker. A backend may satisfy that contract with native completion I/O (io_uring, IOCP, or host WASI async) or with a lazily created blocking pool. The fallback pool has bounded workers and a bounded admission queue and uses the runtime's ordinary registration/completion path. Backend selection, queue layout, and worker count are not public semantics.
Cancellation and true completion
A cold filesystem future that never starts makes no host call. Cancellation before admission removes the queued job without a filesystem effect. Once a host operation has started, cancellation requests backend cancellation where available but does not detach the work.
The future remains cancelling until the host operation reaches real completion or otherwise proves quiescence. Only then may the runtime release its file handle or buffer, end its loans, run cleanup, and report the task cancelled. Consequently a Scope close, async.race, or async.timeout waits for that quiescence. A timeout deadline is not a promise that the wrapper returns at the same instant when an uninterruptible host call is already in flight.
Cancellation is lifecycle state rather than io.IOError, and it does not roll back an operation. Some bytes may already have been read, written at an offset, or appended atomically, and a rename may already have taken effect. There is no detached filesystem job and no early reuse of a buffer still reachable by the host.
For a submitted synchronization barrier, cancellation also does not imply that storage did or did not make the preceding operations durable. The Future must still wait for completion or proven quiescence before releasing its exact file or directory loan.
Deliberate blocking bridge
RFC 0025 directory traversal remains synchronous and lending. It is not duplicated as async.fs.walk, wrapped in Future<WalkEntry in Step>, or given for await syntax. When a program consciously wants a whole blocking operation off the cooperative workers, it moves one coarse closure through async.blocking:
// RFC 0063 target; implementation pending.
let summary = try await async.blocking(<-job);Only the owned result crosses the Future boundary. Step-bounded traversal loans remain inside the blocking closure.
No Whole-File Or Existence Shortcuts
Target core std deliberately has no free or ambient forms such as:
// Rejected target spellings.
fs.read("report.txt");
fs.read_bytes("report.bin");
fs.write("report.bin", data);
fs.copy("a", "b");
fs.move("a", "b");
fs.exists("report.txt");
fs.stat("report.txt");Open through a Dir, then loop over offset operations. Text decoding is a separate checked unicode.utf8.text(Bytes) boundary; there is no read_text or UTF-8-reading file primitive. There is currently no target read_all, write_all, copy, or other whole-file convenience operation. A future helper must remain capability-anchored and expose bounds such as a maximum allocation.
Code does not use exists followed by open: that pair is racy even when it is not a security boundary. It attempts the authoritative operation and handles io.IOError.NotFound. Rename is only a single filesystem rename. In particular, a cross-filesystem failure is never changed into an implicit copy-and-remove sequence with different atomicity and partial-failure rules.
Temporary-resource naming and cleanup, filesystem watching, and whole-file bounds require separate contracts. RFC 0063 does not preserve their current ambient helpers as target aliases, and RFC 0071 does not encode any of those policies into OpenMode. RFC 0072 supplies only explicit File.sync and Dir.sync; it does not accept a publish_file, write_atomic, or hidden staging/cleanup policy. RFC 0073 accepts append only through capability-relative open_append and nominal AppendFile; ambient fs.append(path, data) remains deleted. RFC 0074 closes publication without a new public surface: the only accepted portable composition is caller-chosen CreateNew, an explicit short write loop, File.sync, drop, path-based same-parent rename, and sync of that exact parent directory.
Accepted File API Reference
The shared Path, RelativePath, AbsolutePath, Dir, Metadata, FileAccess, OpenDisposition, OpenMode, AppendAccess, and AppendMode declarations live in std.fs. The two modules deliberately define different nominal File and AppendFile owners.
| Module and operation | Signature | Contract |
|---|---|---|
fs.Dir.open_file | open_file(path: fs.RelativePath, mode: fs.OpenMode) throws(io.IOError) -> ^fs.File | Blocking open with the exact access and disposition beneath this directory capability. |
fs.File.read_at | read_at(offset: u64, buffer: &[u8]) throws(io.IOError) -> usize | One blocking, possibly short initialized-overwrite read. |
fs.File.write_at | write_at(offset: u64, data: bytes.Bytes) throws(io.IOError) -> usize | One blocking, possibly short write. |
fs.File.sync | sync(&self) throws(io.IOError) | Full blocking file-integrity barrier for earlier completed operations on this exact file object. |
fs.Dir.open_append | open_append(path: fs.RelativePath, mode: fs.AppendMode) throws(io.IOError) -> ^fs.AppendFile | Blocking append open with exact access and disposition beneath this directory capability. |
fs.AppendFile.read_at | read_at(&self, offset: u64, buffer: &[u8]) throws(io.IOError) -> usize | One possibly short read of the same underlying identity; available only under ReadAppend. |
fs.AppendFile.write | write(&self, data: bytes.Bytes) throws(io.IOError) -> usize | One possibly short prefix placed contiguously at atomically selected EOF. |
fs.AppendFile.sync | sync(&self) throws(io.IOError) | The same independent full-integrity barrier when open_file.sync was carried. |
fs.Dir.create_directory | create_directory(path: fs.RelativePath) throws(io.IOError) | Creates exactly the final directory entry beneath this capability. |
fs.Dir.remove | remove(path: fs.RelativePath, kind: fs.RemoveKind) throws(io.IOError) | Removes exactly one written non-directory or empty-directory entry without hidden type discovery. |
fs.Dir.rename | rename(source_path: fs.RelativePath, destination_root: fs.Dir, destination_path: fs.RelativePath) throws(io.IOError) | Performs one replace-capable dual-directory namespace rename. |
fs.Dir.sync | sync() throws(io.IOError) | Blocking namespace-persistence barrier for earlier completed mutations of this exact directory object. |
async.fs.open_file | open_file<region L>(root: fs.Dir in L, path: fs.RelativePath in L, mode: fs.OpenMode) -> ^Future<^async.fs.File throws(io.IOError)> in L | Cold cooperative-safe form of the same exact open, retaining the source loans through completion. |
async.fs.File.read_at | read_at<region L>(&self in L, offset: u64, output: ^OutputSpan<u8>) -> ^Future<^OutputSpan<u8> throws(io.IOError)> in L | Consumes the output session; success advances it by the actual short read. |
async.fs.File.write_at | write_at<region L>(&self in L, offset: u64, data: bytes.Bytes in L) -> ^Future<usize throws(io.IOError)> in L | Cold cooperative-safe, possibly short write retaining the byte loan. |
async.fs.File.sync | sync<region L>(&self in L) -> ^Future<void throws(io.IOError)> in L | Cold full-integrity barrier retaining the exact file loan through true completion. |
async.fs.open_append | open_append<region L>(root: fs.Dir in L, path: fs.RelativePath in L, mode: fs.AppendMode) -> ^Future<^async.fs.AppendFile throws(io.IOError)> in L | Cold append open with the same exact access, disposition, containment, and rights checks. |
async.fs.AppendFile.read_at | read_at<region L>(&self in L, offset: u64, output: ^OutputSpan<u8>) -> ^Future<^OutputSpan<u8> throws(io.IOError)> in L | Cold possibly short same-identity read, retaining the output session through completion. |
async.fs.AppendFile.write | write<region L>(&self in L, data: bytes.Bytes in L) -> ^Future<usize throws(io.IOError)> in L | Cold one-operation atomic EOF append; the returned prefix may be short. |
async.fs.AppendFile.sync | sync<region L>(&self in L) -> ^Future<void throws(io.IOError)> in L | Cold full-integrity barrier retaining the append-file loan through true completion. |
async.fs.create_directory | create_directory<region L>(root: fs.Dir in L, path: fs.RelativePath in L) -> ^Future<void throws(io.IOError)> in L | Cold cooperative-safe one-directory creation. |
async.fs.remove | remove<region L>(root: fs.Dir in L, path: fs.RelativePath in L, kind: fs.RemoveKind) -> ^Future<void throws(io.IOError)> in L | Cold cooperative-safe exact-kind removal. |
async.fs.rename | rename<region L>(source_root: fs.Dir in L, source_path: fs.RelativePath in L, destination_root: fs.Dir in L, destination_path: fs.RelativePath in L) -> ^Future<void throws(io.IOError)> in L | Cold cooperative-safe dual-directory rename retaining all loans to true completion. |
async.fs.sync_directory | sync_directory<region L>(root: fs.Dir in L) -> ^Future<void throws(io.IOError)> in L | Cold namespace-persistence barrier retaining the exact directory loan through true completion. |
Neither File nor AppendFile has a position, cursor operation, direct stream interface, whole-file helper, or safe sync/async conversion. AppendFile also has no offset write or complete-write helper.
Transitional File Data Helpers
The remainder of this page records checked-in implementation state. These unanchored
strhelpers are not target API and must not be used to infer the accepted blocking or async filesystem contracts. In particular, themovecopy/remove fallback and temp-backedwrite_atomicprotocol below are rejected target behavior. RFC 0070 accepts only oneDir.renamenamespace operation and does not accept temporary naming, synchronization, durability, recovery, or cleanup policy. RFC 0071 additionally rejects reading the checked-in helpers' flags or shorthand modes as the targetOpenModecontract. RFC 0072 accepts only the explicit barriers above; it does not rescue the ambientwrite_atomichelper or itsAtomicDurabilitypolicy. RFC 0073 replaces the ambient whole-callappend(path, data)contract with explicitDir.open_appendand one possibly shortAppendFile.write; the legacy helper below is not a compatibility spelling. RFC 0074 rejects a targetwrite_atomicalias, nominal staging owner, generated temporary name, cleanup-on-drop, or universal recovery transaction. The accepted recipe remains the explicit primitive composition documented above.
import std.fs;
import std.io;
import std.mem.{ AllocError };
fn main() throws(AllocError) {
let text = try await fs.read("docs/std/fs.md");
assert text.size > 0 as usize;
io.println("ok");
}read(path) and write(path, data) are the core file primitives:
read(path: str) -> ^Future<^str throws(io.IOError | utf8.UnicodeError | AllocError)>reads the entire file and validates UTF-8.read_bytes(path: str) -> ^Future<^[u8] throws(io.IOError | AllocError)>reads the entire file without UTF-8 conversion.write(path: str, data: str|Bytes) -> ^Future<void throws(io.IOError | AllocError)>writes from the beginning, truncating existing files.append(path: str, data: str|Bytes) -> ^Future<void throws(io.IOError | AllocError)>is the legacy ambient, Boolean/whole-write append experiment. It is removed from target corestd; use explicitAppendModeandAppendFile.write.write_atomic(path: str, data: str|Bytes, durability: AtomicDurability = .None) throws(io.IOError | AllocError)writes the full content to a sibling temp file, syncs it, then renames it overpath, so readers never observe a partially written file.
import std.fs;
import std.io;
import std.mem.{ AllocError };
fn main() throws(AllocError) {
let source = "/tmp/zynx_std_fs_doc.txt";
try await fs.write(source, "abc");
try await fs.append(source, "def");
let text = try await fs.read(source);
io.println(text);
}The checked-in transitional write_atomic implementation attempts to replace a file through a same-directory temporary name. It does not define the target language contract and must not be read as a universal safety, identity, cleanup, cancellation, or recovery guarantee. By default, the temp file is synced with fsync and the parent directory is not synced. Pass AtomicDurability.Fsync to sync both temp file and parent directory, and AtomicDurability.Fdatasync to use fdatasync for the temp file plus parent directory fsync (where available). RFC 0074 rejects this helper and policy enum from target core std.
Additional file operations:
copy(src: str, dst: str)asynchronously duplicates file bytes without UTF-8 conversion.move(src: str, dst: str)asynchronously renames, falling back to copy+remove across filesystems (EXDEV); the fallback is not atomic and preserves metadata on a best-effort basis.remove(path: str)asynchronously deletes a single file.symlink(target: str, path: str)creates a symbolic link.readlink(path: str) -> ^strreads symbolic-link destination text.canonicalize(path: str) -> ^strresolves symlinks and returns an absolute path. The path must exist.exists(path: str)asynchronously returns whether the path resolves successfully.tmpfile()creates a unique temporary file and returns its path.tmpfile(directory: str)creates a unique temporary file indirectory.write_atomicacceptsAtomicDurability.FsyncorAtomicDurability.Fdatasyncto ensure parent-directory metadata is synced when replacing files in crash-prone environments.stat(path: str) -> ^Future<FileInfo throws(io.IOError | AllocError)>returns metadata after following symlinks.
exists is intentionally boolean and returns false for failed path lookup. Allocation failures and invalid path/runtime setup errors still surface through its future error set.
Path Helpers
Import std.fs.path directly for join, normalize, basename, dirname, and extension; importing std.fs does not import the child module.
import std.fs.path;
import std.io;
import std.mem.{ AllocError };
fn main() throws(AllocError) {
let joined = try path.join("src", "main.zx");
let clean = try path.normalize("src//./main.zx");
io.println(joined);
io.println(clean);
}path.normalize(path) is purely lexical:
- collapses repeated
/and removes.segments; - resolves
..segments without touching the filesystem; - preserves absolute versus relative paths;
- does not resolve symlinks.
canonicalize uses host realpath behavior instead, so it requires an existing path and resolves symlinks.
Directory Helpers
mkdir(path: str)creates one directory component.mkdirs(path: str)creates parent components as needed.rmdir(path: str)removes one empty directory.rmtree(path: str)recursively removes a directory tree.list(path: str) -> ^[^str]returns immediate entry names, excluding.and...tmpdir() -> ^strreturns a temporary directory path.
The accepted target anchors filesystem operations in an explicit Dir capability injected as a private executable-root @host loan and then passed through ordinary parameters. The current str helpers and fs.cwd source experiment are transitional implementation state, not compatibility spellings for Path, RelativePath, AbsolutePath or Dir.
Transitional Walk + Glob
The currently shipped source still provides a pure-Zynx recursive directory walk and glob matching built on list and symlink-aware metadata. RFC 0025 deletes all three spellings below without compatibility aliases; this section records implementation state, not the accepted contract.
walk(root: str) -> ^[^str]enumerates every entry beneathrootdepth-first. Each directory path is yielded before its contents (pre-order), so callers can inspect a directory before its children appear. Paths are returned joined ontoroot. Symlink entries are yielded, but symlinked directories are not traversed.glob(root: str, pattern: str) -> ^[^str]walksrootand returns the full paths whose entry name (basename) matchespattern. This behaves like a recursivefind -name.matches(pattern: str, text: str) -> boolis the underlying glob matcher.
import std.fs;
import std.fs.path as path;
import std.io;
import std.mem.{ AllocError };
fn main() throws(AllocError) {
let root = "/tmp/zynx_std_fs_glob_doc";
if try await fs.exists(root) {
try fs.rmtree(root);
}
try fs.mkdirs(try path.join(root, "sub"));
try await fs.write(try path.join(root, "a.txt"), "a");
try await fs.write(try path.join(root, "sub", "b.txt"), "b");
let txt = try fs.glob(root, "*.txt");
io.println(try ^"matches={txt.length}");
try fs.rmtree(root);
}Matcher semantics (byte-oriented):
*matches any run of bytes within a single path segment; it never crosses/.?matches exactly one byte other than/.[...]is a character class witha-zranges and!/^negation; a leading]is a literal member, and a malformed class with no closing]makes the[a literal byte.\escapes the next byte so glob metacharacters can match literally.
? and character classes operate on single bytes, not whole UTF-8 codepoints.
Watch
The public facade exposes fs.watch(...). The implementation reports Created, Modified, and Deleted changes through native platform watch backends when available, with a snapshot-diff fallback on unsupported platforms.
import std.fs;
import std.io;
import std.mem.{ AllocError };
fn main() throws(AllocError) {
let root = "/tmp/zynx_std_fs_watch_doc";
if try await fs.exists(root) {
try fs.rmtree(root);
}
try fs.mkdir(root);
var watcher = try fs.watch(root, true);
try await fs.write("/tmp/zynx_std_fs_watch_doc/sample.txt", "sample");
let result = try watcher.poll();
io.println(try ^"events={result.events.length}");
try fs.rmtree(root);
}watch(path: str, recursive: bool) -> Watcherbuilds an initial watcher forpath, preferring the native backend when available and falling back to snapshot tracking otherwise.poll()drains pending events from the native watch stream and, on fallback mode, compares a snapshot against the current state to produce aPollResultwitheventsplus an updatednextwatcher.WatchEventKindis one ofCreated,Modified,Deleted.WatchEventincludeskindand changedpath.
Metadata
import std.fs;
fn main() {
let info = try await fs.stat("docs/std/fs.md");
if info.is_file() {
_ = info.size;
}
}FileInfo exposes:
let size: u64let kind: FileKindmodified() -> time.Durationcreated() -> time.Durationmode() -> u32permissions() -> u32is_file() -> boolis_dir() -> boolis_readable() -> boolis_writable() -> boolis_executable() -> boolis_owner_readable() / is_owner_writable() / is_owner_executable() -> boolis_group_readable() / is_group_writable() / is_group_executable() -> boolis_other_readable() / is_other_writable() / is_other_executable() -> boolis_setuid() -> boolis_setgid() -> boolis_sticky() -> bool
created maps to st_ctime on Unix-like platforms (inode metadata time), not birth time.
Ownership, Allocation, Errors, And Unspecified Behavior
Filesystem functions borrow path and data arguments for the duration of the call or returned future. In the current transitional source, read, read_bytes, list, walk, glob, canonicalize, readlink, tmpdir, and tmpfile allocate owned return values. In the accepted target, Dir.entries() and Dir.walk() allocate cursor state but lend every entry; retaining a path requires an explicit clone(). watch owns snapshot or native watcher state until the watcher is advanced or dropped.
Host filesystem failures are reported as io.IOError; allocation failures are reported as AllocError; text-decoding helpers can also throw utf8.UnicodeError. exists is intentionally boolean for lookup failure, but allocation and runtime setup errors still surface through its future error set.
Directory sibling order, watch event ordering/coalescing, native watcher backend selection, timestamp precision, metadata fields beyond those documented here, and copy fallback metadata preservation are host-dependent or unspecified.
API Reference
This table records the currently shipped transitional source surface. It does not override the accepted target contracts above; specifically, target std.fs has no free append, walk, glob, matches, write_atomic, or AtomicDurability policy surface.
| Symbol | Signature | Parameters | Return | Notes |
|---|---|---|---|---|
append | append(path: str, data: Bytes) -> ^Future<void throws(io.IOError | AllocError)>append(path: str, data: str) -> ^Future<void throws(io.IOError | AllocError)> | path: host path, data: bytes/text to append | async void | Transitional ambient whole-write helper, deleted by RFC 0073. It is not an alias for capability-relative, possibly short AppendFile.write. |
canonicalize | canonicalize(path: str) -> ^str | path: existing host path | ^str throws(io.IOError | AllocError) | Resolves symlinks and returns an absolute filesystem-backed path. |
copy | copy(src: str, dst: str) -> ^Future<void throws(io.IOError | AllocError)> | src: existing path, dst: destination path | async void | Copies file data in chunks. |
exists | exists(path: str) -> ^Future<bool throws(io.IOError | AllocError)> | path: host path | async bool | Returns false when lookup fails. |
glob | glob(root: str, pattern: str) -> ^[^str] | root: directory to walk, pattern: glob pattern | ^[^str] throws(io.IOError | AllocError) | Walks root and returns paths whose basename matches pattern. |
matches | matches(pattern: str, text: str) -> bool | pattern: glob pattern, text: candidate | bool | Byte-oriented glob matcher (*, ?, [...], \); */? never cross /. |
mkdir | mkdir(path: str) -> void | path: directory path | void throws(io.IOError | AllocError) | One-segment directory creation, not recursive. |
mkdirs | mkdirs(path: str) -> void | path: directory path | void throws(io.IOError | AllocError) | Creates parent components with EEXIST ignored. |
move | move(src: str, dst: str) -> ^Future<void throws(io.IOError | AllocError)> | src: source path, dst: destination | async void | Native rename; falls back to copy+metadata+remove across filesystems (EXDEV). |
write_atomic | write_atomic(path: str, data: Bytes) -> voidwrite_atomic(path: str, data: str) -> voidwrite_atomic(path: str, data: Bytes, durability: AtomicDurability) -> voidwrite_atomic(path: str, data: str, durability: AtomicDurability) -> void | path: host path, data: bytes/text to write, durability: sync policy | void throws(io.IOError | AllocError) | Writes to a sibling temp file, syncs it, optionally syncs the parent directory on rename according to durability. |
read | read(path: str) -> ^Future<^str throws(io.IOError | utf8.UnicodeError | AllocError)> | path: host path | async ^str | Reads entire file and validates UTF-8. |
read_bytes | read_bytes(path: str) -> ^Future<^[u8] throws(io.IOError | AllocError)> | path: host path | async ^[u8] | Reads entire file as bytes. |
remove | remove(path: str) -> ^Future<void throws(io.IOError | AllocError)> | path: file path | async void | Removes a single file entry. |
rmdir | rmdir(path: str) -> void | path: directory path | void throws(io.IOError | AllocError) | Removes one empty directory. |
rmtree | rmtree(path: str) -> void | path: directory path | void throws(io.IOError | AllocError) | Recursively removes tree contents then directory itself. |
stat | stat(path: str) -> ^Future<FileInfo throws(io.IOError | AllocError)> | path: host path | async FileInfo | Returns scalar metadata after following symlinks. |
tmpfile | tmpfile() -> ^strtmpfile(directory: str) -> ^str | none, or directory | ^str throws(io.IOError | AllocError) | Creates and opens a unique temporary file, returning its path on success. The descriptor is closed before returning. |
tmpdir | tmpdir() -> ^str | none | str | Returns TMPDIR or /tmp. |
walk | walk(root: str) -> ^[^str] | root: directory path | ^[^str] throws(io.IOError | AllocError) | Depth-first pre-order enumeration of all descendant paths without recursing into symlinked directories. |
watch | watch(path: str) -> Watcherwatch(path: str, recursive: bool) -> Watcher | path: directory/file to observe, recursive: include descendants | Watcher throws(io.IOError | AllocError) | Creates a watcher backed by native notifications where available, with snapshot polling fallback. Use watcher.poll() to read events and advance watcher state. |
PollResult | struct | none | none | Returned by Watcher.poll(). Contains events and next. |
WatchEvent | struct | none | none | Report row with kind and changed path. |
WatchEventKind | enum | none | none | Created, Modified, or Deleted. |
Watcher | struct | none | none | Holds snapshot state and exposes poll(). |
write | write(path: str, data: Bytes) -> ^Future<void throws(io.IOError | AllocError)>write(path: str, data: str) -> ^Future<void throws(io.IOError | AllocError)> | path: host path, data: bytes/text to write | async void | Truncates and writes complete content. |
list | list(path: str) -> ^[^str] | path: directory path | ^[^str] throws(io.IOError | AllocError) | Excludes . and ..; order unspecified. |
readlink | readlink(path: str) -> ^str | path: link path | ^str throws(io.IOError | AllocError | utf8.UnicodeError) | Reads the stored symbolic-link destination bytes and validates them as UTF-8 text. |
AtomicDurability | enum | none | none | None (default behavior), Fsync (sync temp file and parent directory), Fdatasync (fdatasync temp file + parent directory fsync). |
FileInfo | struct | none | none | Metadata view with size/kind/time bits. |
symlink | symlink(target: str, path: str) -> void | target: destination target, path: link path | void throws(io.IOError | AllocError) | Creates a symbolic link at path that points to target. |
FileKind | enum | none | none | One of File, Dir, Symlink, Other. |
std.fs.path
The std.fs.path submodule provides lexical Unix-style helpers:
| Symbol | Signature | Notes |
|---|---|---|
basename | basename(path: str) -> ^str | Last lexical component. |
dirname | dirname(path: str) -> ^str | Path prefix before final component. |
extension | extension(path: str) -> ^str? | Final extension without dot. |
normalize | normalize(path: str) throws(AllocError) -> ^str | Pure lexical cleanup; does not touch the filesystem or resolve symlinks. |
join | join(a: str, b: str, rest: str...) throws(AllocError) -> ^str | Joins lexical fragments with a single / boundary. |
Status
std.fs checked-in source is still the transitional ambient facade recorded in the table above. The accepted target is RFC 0023 path/directory capabilities, RFC 0025 synchronous lending traversal, and RFC 0063's split between blocking fs.File and cooperative-safe async.fs.File, plus RFC 0070's exact capability-relative namespace mutations, RFC 0071's exact open modes, and RFC 0072's explicit file/directory durability barriers, plus RFC 0073's distinct atomic-EOF AppendFile owners. RFC 0074 adds no publication API: callers use the explicit same-parent primitive recipe, choose and own staging names, and handle cleanup, coordination, and phase recovery themselves. RFC 0074 adds no publication-specific launch schema change; RFC 0076 separately assigns zynx-launch v5 to MMIO grants and changes no filesystem right or operation. There is no portable safe process-current-directory API, core glob policy, ambient append, hidden file cursor, whole-file or publication target helper, exists, or async traversal. Use std.io for synchronous byte streams and std.async.io for generic async streams. Raw OS-resource interop is deferred to target-specific APIs. For the exact shipped surface during migration, see Standard Library Inventory.