silk/filesystem
Profiles: aarch64-apple-darwin, aarch64-unknown-linux-gnu, aarch64-unknown-linux-gnu-no-libc, wasm32-unknown-unknown, x86_64-unknown-linux-gnu, x86_64-unknown-linux-gnu-no-libc.
Portable normalized paths, whole-file operations, directory traversal, and explicit temp scopes.
When to use
Build provider-absolute Path values with make or fromBytes, then run operations
through a supplied FileSystem. Use rawBytes for platform values that must round-trip even
when they are not UTF-8, and resolve for lexical relative-path resolution.
Details
Paths are absolute and normalized: they reject NUL, empty components, trailing separators, and
embedded . or ... Resolution handles relative dot components but rejects escape above root.
Directory listings return independently owned child paths in deterministic path-byte order.
Portable FileError data names both the operation and a closed recovery reason, with an
optional provider code for diagnostics.
Temporary directories have an explicit lifecycle because removal can fail and needs services.
Use release when cleanup failure matters, or releaseIgnored as an infallible finalizer
only after deliberately accepting that loss.
Gotchas
A path created from arbitrary bytes may not have a valid text view. Keep using rawBytes unless
the bytes were validated as UTF-8; view and name rely on that caller knowledge.
Examples
Construct and inspect a portable path
import silk.allocator { Allocator, OutOfMemoryError }
import silk.effect { Effect }
import silk.filesystem { FileSystem, FileError, Path }
effect fn example() -> i32
! FileError | OutOfMemoryError {
let mut allocator = Allocator.systemAllocatorProvider()
let path = run Path.make("/workspace")
|> Effect.provideMut(&mut allocator)
if Path.name(&path) == "workspace" {
return 42
}
return 0
}
effect fn recover(error: FileError | OutOfMemoryError) -> i32 {
return 0
}
pub fn main() -> i32 {
return run Effect.catchAll(example(), recover)
}Import as FileSystem with import silk.filesystem { FileSystem }.
Public declarations: 10.
Path
pub struct PathAn owned, normalized absolute path in a FileSystem provider's portable namespace.
Details
Portable / means the selected provider's root, not necessarily the host operating system's
root. Construct paths through make, fromBytes, root, join, or resolve; the
representation is private so every Path satisfies the normalization rules.
Associated function Path.make
pub effect<'life0> fn make<'life0>(value: string<'life0>) -> Path ! FileError | OutOfMemoryError ? &mut AllocatorCopies UTF-8 text into an owned, normalized provider-absolute Path.
Details
The text must begin with /. Root is valid; every other path must have nonempty components and
no trailing slash, NUL, . component, or .. component. Invalid input fails with
FileError(FileSystem.pathOperation(), FileSystem.invalidPath()); copying can fail with OutOfMemoryError.
Associated function Path.fromBytes
pub effect<'life0> fn fromBytes<'life0>(values: &'life0 [u8]) -> Path ! FileError | OutOfMemoryError ? &mut AllocatorConstructs an owned normalized provider-absolute Path from exact platform bytes.
Details
Platform paths are byte sequences, and a caller that received one from the platform — a
directory entry, an argument, an environment value — must be able to hand it back unchanged.
The same normalization applies as for textual construction: the value is absolute, rejects NUL,
and rejects ., .., empty components, and trailing separators. Well-formed text is not
required, so a Path built this way may have no string view.
Associated function Path.root
pub effect<'static> fn root() -> Path ! OutOfMemoryError ? &mut AllocatorAllocates the portable root path / in the selected allocator.
Method Path.rawBytes
pub fn rawBytes<'life0>(self: &'life0 Path) -> &'life0 [u8]Borrows the complete normalized path as exact platform bytes.
Details
This is the lossless view. It round-trips a Path built from platform bytes even when those
bytes are not well-formed text, which the string view cannot promise.
Method Path.view
pub fn view<'life0>(self: &'life0 Path) -> string<'life0>Borrows the complete path as text when its bytes are known to be valid UTF-8.
Details
Paths from make, join, joinUtf8, and resolve satisfy that precondition. A path
created with fromBytes may not; use rawBytes unless the source bytes were validated.
Method Path.isRoot
pub fn isRoot<'life0>(self: &'life0 Path) -> boolReturns true exactly when this path is the portable root /.
Method Path.name
pub fn name<'life0>(self: &'life0 Path) -> string<'life0>Borrows the final component as text; root returns empty text.
Details
This has the same UTF-8 precondition as view. It does not allocate or include a separator.
Associated function Path.join
pub effect<'env> fn join<'life0: 'env, 'life1: 'env, 'env>(base: &'life0 silk/filesystem.Path, fragment: string<'life1>) -> Path ! FileError | OutOfMemoryError ? &mut AllocatorAppends one normalized relative text fragment to an absolute base path.
Details
fragment must be nonempty and relative, with no NUL, empty, ., or .. component and no
trailing slash. Use resolve when dot components should be interpreted instead of rejected.
Associated function Path.joinUtf8
pub effect<'env> fn joinUtf8<'life0: 'env, 'life1: 'env, 'env>(base: &'life0 silk/filesystem.Path, fragment: &'life1 [u8]) -> Path ! FileError | OutOfMemoryError ? &mut AllocatorValidates UTF-8 bytes as one normalized relative fragment and appends them to base.
Details
This is useful for a child name returned as bytes by another portable API. Invalid UTF-8 and the
same malformed components rejected by join fail with the InvalidPath reason.
Associated function Path.joinBytes
pub effect<'env> fn joinBytes<'life0: 'env, 'life1: 'env, 'env>(base: &'life0 silk/filesystem.Path, fragment: &'life1 [u8]) -> Path ! FileError | OutOfMemoryError ? &mut AllocatorAppends one normalized relative byte fragment while preserving non-UTF-8 names.
Details
The fragment must be nonempty and contain no NUL, empty, dot or dot-dot component.
This is the byte-preserving operation for native directory names; use joinUtf8
when the caller also requires valid UTF-8.
Associated function Path.resolve
pub effect<'env> fn resolve<'life0: 'env, 'life1: 'env, 'env>(base: &'life0 silk/filesystem.Path, relativeText: string<'life1>) -> Path ! FileError | OutOfMemoryError ? &mut AllocatorResolves relative text lexically against an explicit absolute base.
Details
Empty text and . keep the base; .. removes components; ordinary components append. An
absolute relative value, an empty interior component, NUL, or any attempt to escape above root
fails with the InvalidPath reason. Resolution is lexical and never accesses the filesystem.
Method Path.parent
pub effect<'life0> fn parent<'life0>(self: &'life0 Path) -> silk/option.Option<silk/filesystem.Path> ! OutOfMemoryError ? &mut AllocatorAllocates an independently owned parent path, or None when self is root.
Details
The result does not borrow self. A direct child of root has root as its parent.
FileInfo
pub struct FileInfoMinimal portable metadata for one regular file.
Field byteLength
pub byteLength: usizeComplete file length in bytes.
DirectoryInfo
pub struct DirectoryInfoPortable metadata identifying a directory; no platform-specific fields are exposed.
DirectoryEntryKind
pub struct DirectoryEntryKindThe closed portable kind of one directory entry.
Field code
pub code: i32Stable portable kind code selected by file or directory.
DirectoryEntry
pub struct DirectoryEntryOne immediate directory child with an independently owned complete Path.
Field path
pub path: PathIndependently owned complete path to the child.
Field kind
pub kind: DirectoryEntryKindPortable kind reported for the child.
FileOperation
pub struct FileOperationThe stable portable operation category stored in a FileError.
Field code
pub code: i32Stable code identifying the attempted portable operation.
FileReason
pub struct FileReasonA stable portable recovery category stored in a FileError.
Field code
pub code: i32Stable code identifying the portable recovery reason.
FileError
pub struct FileErrorAn allocation-free portable failure naming the attempted operation and recovery reason.
Details
Match or compare operationCode and reasonCode for portable recovery. providerCode
may retain an OS or provider-specific numeric detail for diagnostics, but portable decisions
must not depend on it.
Field operation
pub operation: FileOperationThe operation that failed.
Field reason
pub reason: FileReasonThe portable reason callers can recover by.
FileSystem
pub service FileSystemPortable mutable service for normalized paths and whole-file operations.
Details
Application code supplies one provider lexically with Effect.provideMut; tests can implement
this service in memory, while native applications can use silk.os_filesystem. The service owns
platform policy, but every implementation must preserve the portable error categories,
create-or-truncate writes, and deterministic listing order described here.
writeFileWithParents composes parent creation and the final write using the same lexical
provider. Supply both &mut FileSystem and &mut Allocator for this operation. Its directory
creation is not transactional: a failed final write may leave newly created parents behind.
Operation readFile
effect<'life0> fn readFile<'life0>(path: &'life0 silk/filesystem.Path) -> Bytes ! FileError | OutOfMemoryError ? &mut FileSystem | &mut AllocatorReads one complete regular file into independently owned bytes.
Details
Reading a directory fails with WrongType. Allocation of the returned Bytes may fail
independently of the provider read.
Operation writeFile
effect<'env> fn writeFile<'life0: 'env, 'life1: 'env, 'env>(path: &'life0 silk/filesystem.Path, bytes: &'life1 [u8]) -> () ! FileError ? &mut FileSystemWrites one complete byte view with create-or-truncate semantics.
Details
A missing file is created; an existing regular file is replaced by exactly bytes. The call
does not create missing parent directories—use writeFileWithParents for that workflow.
Operation stat
effect<'life0> fn stat<'life0>(path: &'life0 silk/filesystem.Path) -> silk/filesystem.DirectoryInfo | silk/filesystem.FileInfo ! FileError ? &mut FileSystemReturns FileInfo or DirectoryInfo for the path without opening file contents.
Details
Missing paths fail with NotFound; providers use WrongType only when an operation requires a
particular kind, not for this discriminating query.
Operation listDirectory
effect<'life0> fn listDirectory<'life0>(path: &'life0 silk/filesystem.Path) -> silk/vector.Vector<silk/filesystem.DirectoryEntry> ! FileError | OutOfMemoryError ? &mut FileSystem | &mut AllocatorReturns immediate owned children in deterministic complete-path byte order.
Details
The result is not recursive. Each DirectoryEntry.path is independently owned and may be
retained after the listing vector is released.
Operation createDirectory
effect<'life0> fn createDirectory<'life0>(path: &'life0 silk/filesystem.Path) -> () ! FileError ? &mut FileSystemCreates exactly one missing directory whose parent already exists.
Details
Existing paths fail with AlreadyExists; use createDirectoriesRecursively to ensure every
missing component.
Operation removeFile
effect<'life0> fn removeFile<'life0>(path: &'life0 silk/filesystem.Path) -> () ! FileError ? &mut FileSystemRemoves exactly one regular file and fails with WrongType for a directory.
Operation removeDirectory
effect<'life0> fn removeDirectory<'life0>(path: &'life0 silk/filesystem.Path) -> () ! FileError ? &mut FileSystemRemoves exactly one empty directory.
Details
A nonempty directory fails with NotEmpty; use removeDirectoryRecursively only when all
descendants are intentionally in scope for removal.
Operation createTemporaryDirectory
effect<'env> fn createTemporaryDirectory<'life0: 'env, 'life1: 'env, 'env>(parent: &'life0 silk/filesystem.Path, prefix: &'life1 [u8]) -> Path ! FileError | OutOfMemoryError ? &mut FileSystem | &mut AllocatorCreates one directory under an existing parent under a name no other caller holds.
Details
The provider chooses the name's unique part and returns the complete Path, because only the
provider can create and claim a name in one step. A caller that supplied the name would have
to check-then-create, and the gap between those two is exactly the race this avoids.
prefix is a byte prefix for the provider-chosen child name, not a complete path. The returned
directory already exists and is an immediate child of parent.
Associated function FileSystem.file
pub fn file() -> DirectoryEntryKindConstructs the regular-file DirectoryEntryKind.
Associated function FileSystem.directory
pub fn directory() -> DirectoryEntryKindConstructs the directory DirectoryEntryKind.
Associated function FileSystem.entryKindCode
pub fn entryKindCode(kind: DirectoryEntryKind) -> i32Returns the stable code for a consumed DirectoryEntryKind: 0 for file, 1 for directory.
Associated function FileSystem.fileInfo
pub fn fileInfo(byteLength: usize) -> FileInfoConstructs regular-file metadata with the complete length in bytes.
Associated function FileSystem.directoryInfo
pub fn directoryInfo() -> DirectoryInfoConstructs the fieldless portable directory metadata value.
Associated function FileSystem.directoryEntry
pub fn directoryEntry(path: Path, kind: DirectoryEntryKind) -> DirectoryEntryConstructs a directory entry by taking ownership of its complete child path and kind.
Associated function FileSystem.readFileOperation
pub fn readFileOperation() -> FileOperationSelects the read-file operation.
Associated function FileSystem.writeFileOperation
pub fn writeFileOperation() -> FileOperationSelects the write-file operation.
Associated function FileSystem.statOperation
pub fn statOperation() -> FileOperationSelects the stat operation.
Associated function FileSystem.listDirectoryOperation
pub fn listDirectoryOperation() -> FileOperationSelects the list-directory operation.
Associated function FileSystem.createDirectoryOperation
pub fn createDirectoryOperation() -> FileOperationSelects the create-directory operation.
Associated function FileSystem.removeFileOperation
pub fn removeFileOperation() -> FileOperationSelects the remove-file operation.
Associated function FileSystem.removeDirectoryOperation
pub fn removeDirectoryOperation() -> FileOperationSelects the remove-directory operation.
Associated function FileSystem.pathOperation
pub fn pathOperation() -> FileOperationSelects path construction and resolution.
Associated function FileSystem.createTemporaryDirectoryOperation
pub fn createTemporaryDirectoryOperation() -> FileOperationSelects the create-temporary-directory operation.
Associated function FileSystem.operationCode
pub fn operationCode(operation: FileOperation) -> i32Returns the stable numeric code of a consumed FileOperation.
Associated function FileSystem.notFound
pub fn notFound() -> FileReasonConstructs the NotFound recovery reason.
Associated function FileSystem.alreadyExists
pub fn alreadyExists() -> FileReasonConstructs the AlreadyExists recovery reason.
Associated function FileSystem.permissionDenied
pub fn permissionDenied() -> FileReasonConstructs the PermissionDenied recovery reason.
Associated function FileSystem.invalidPath
pub fn invalidPath() -> FileReasonConstructs the InvalidPath recovery reason.
Associated function FileSystem.wrongType
pub fn wrongType() -> FileReasonConstructs the WrongType recovery reason.
Associated function FileSystem.notEmpty
pub fn notEmpty() -> FileReasonConstructs the NotEmpty recovery reason.
Associated function FileSystem.noSpace
pub fn noSpace() -> FileReasonConstructs the NoSpace recovery reason.
Associated function FileSystem.tooLarge
pub fn tooLarge() -> FileReasonConstructs the TooLarge recovery reason.
Associated function FileSystem.unsupported
pub fn unsupported() -> FileReasonConstructs the Unsupported recovery reason.
Associated function FileSystem.other
pub fn other() -> FileReasonConstructs the catch-all Other recovery reason.
Associated function FileSystem.reasonCode
pub fn reasonCode(reason: FileReason) -> i32Returns the stable numeric code of a consumed FileReason.
Associated function FileSystem.error
pub fn error(operation: FileOperation, reason: FileReason) -> FileErrorConstructs a portable FileError without a provider-specific numeric detail.
Associated function FileSystem.errorWithCode
pub fn errorWithCode(operation: FileOperation, reason: FileReason, code: i32) -> FileErrorConstructs a portable FileError while retaining one provider-specific diagnostic code.
Details
The numeric code is opaque outside that provider. The portable operation and reason remain
the fields callers should use for recovery.
Associated function FileSystem.providerCode
pub fn providerCode<'life0>(error: &'life0 silk/filesystem.FileError) -> silk/option.Option<i32>Borrows an error and returns its provider-specific numeric detail, if one was retained.
Associated function FileSystem.temporaryDirectory
pub effect<'env> fn temporaryDirectory<'life0: 'env, 'life1: 'env, 'env>(parent: &'life0 silk/filesystem.Path, prefix: string<'life1>) -> TemporaryDirectory ! FileError | OutOfMemoryError ? &mut FileSystem | &mut AllocatorCreates an explicitly owned temporary directory under parent with a name beginning in prefix.
Details
The result is owned. Nothing removes it until a caller runs release or releaseIgnored.
The prefix is encoded as UTF-8 and the provider chooses and claims the remaining unique name in
one operation.
Associated function FileSystem.release
pub effect<'static> fn release(self: TemporaryDirectory) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut AllocatorConsumes one TemporaryDirectory and removes it together with everything inside it.
Details
Both rows are stated rather than hidden. Removal reaches the provider, so it can fail; walking the tree to find what to remove allocates, so it can exhaust memory. A caller that must observe a failed cleanup uses this operation and handles the failure. The owner is consumed even when removal fails, so copy any diagnostic path information needed before calling.
Associated function FileSystem.releaseIgnored
pub effect<'static> fn releaseIgnored(self: TemporaryDirectory) -> () ? &mut FileSystem | &mut AllocatorConsumes one TemporaryDirectory, removes it, and discards a failed removal.
Details
This exists because Effect.ensuring types its finalizer ! never, so a fallible release has
to be recovered before it can be a finalizer. The recovery is deliberate and it is named: a
caller reading releaseIgnored at the call site can see that a failed removal is being
dropped, which a hook doing the same thing invisibly could not show. What is lost is bounded —
a directory the host will reap — and what is kept is the protected Effect's own outcome, which
is the answer the program was computing.
A caller who needs the failure uses release instead and does not compose it with ensuring.
The finalizer consumes the directory. The protected Effect cannot borrow it when the finalizer starts. Derive the required paths before you give the owner to the finalizer.
Associated function FileSystem.removeDirectoryRecursively
pub effect<'life0> fn removeDirectoryRecursively<'life0>(path: &'life0 silk/filesystem.Path) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut AllocatorRemoves a directory, every descendant file, and every descendant directory.
Details
Two passes, because the portable primitive removes exactly one empty directory. The first pass walks the tree front to back, unlinking every file it meets and recording every directory it meets; the second removes the recorded directories back to front. That order is child-before-parent for free: a directory is always recorded before the children found inside it, so reversing the record reverses the containment. Neither pass recurses, so depth costs vector capacity rather than stack.
This operation is destructive and not transactional. If a provider or allocation failure occurs, removals already completed remain completed and the remaining tree is left in place.
Associated function FileSystem.createDirectoriesRecursively
pub effect<'life0> fn createDirectoriesRecursively<'life0>(path: &'life0 silk/filesystem.Path) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut AllocatorEnsures that path and every missing ancestor exist as directories.
Details
Existing directories are kept. An existing regular file at any component fails with
WrongType; failures other than NotFound propagate. This is ordinary stat-then-create
composition, so concurrent namespace changes may still race according to provider policy.
Associated function FileSystem.writeFileWithParents
pub effect<'env> fn writeFileWithParents<'life0: 'env, 'life1: 'env, 'env>(path: &'life0 silk/filesystem.Path, bytes: &'life1 [u8]) -> () ! FileError | OutOfMemoryError ? &mut FileSystem | &mut AllocatorEnsures every parent directory exists, then writes the complete byte view to path.
Details
The final write uses FileSystem.writeFile create-or-truncate semantics. Passing root delegates
directly to the provider and normally fails with WrongType. Directory creation and writing are
not transactional, so a later failure may leave newly created parents behind.
Associated function FileSystem.exists
pub effect<'life0> fn exists<'life0>(path: &'life0 silk/filesystem.Path) -> bool ! FileError ? &mut FileSystemReturns whether a file or directory exists at path.
Details
Only the portable NotFound reason becomes false. Permission, I/O, and every other provider
failure propagate so callers cannot mistake an inaccessible path for an absent one.
TemporaryDirectory
pub struct TemporaryDirectoryA directory a caller owns outright, together with everything written inside it.
Details
Ownership is affine: TemporaryDirectory holds an owned Path, so exactly one binding holds
it and the compiler rejects a second use of a moved one. Ownership is not, however, a Drop
hook. Removing a directory is a fallible operation that requires the FileSystem capability,
and a Drop hook may carry neither a failure row nor a requirement row, so a hook here could
only be written by inventing an infallible intrinsic over a fallible syscall. Release is
therefore explicit and honest about both rows — see release.
Scope ownership comes from composition rather than from a hook: Effect.ensuring(release)
runs the release whatever the protected Effect's outcome. Because ensuring types its
finalizer ! never, that composition has to say what a failed removal means; releaseIgnored
is the stdlib's answer and names the loss at the call site.
Field path
pub path: PathThe complete owned path callers use while the scope remains live.