silk/filesystem
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 }
import silk.effect { Effect }
import silk.filesystem as FileSystem
effect fn example() -> i32
! FileSystem.FileError | Allocator.OutOfMemoryError {
let mut allocator = Allocator.systemAllocatorProvider()
let path = run FileSystem.make("/workspace")
|> Effect.provideMut(&mut allocator)
if FileSystem.name(&path) == "workspace" {
return 42
}
return 0
}
effect fn recover(error: FileSystem.FileError | Allocator.OutOfMemoryError) -> i32 {
return 0
}
pub fn main() -> i32 {
return run Effect.catchAll(example(), recover)
}Import as FileSystem with import silk.filesystem.
Public declarations: 58.
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.
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.
file
pub fn file() -> DirectoryEntryKindConstructs the regular-file DirectoryEntryKind.
directory
pub fn directory() -> DirectoryEntryKindConstructs the directory DirectoryEntryKind.
entryKindCode
pub fn entryKindCode(kind: DirectoryEntryKind) -> i32Returns the stable code for a consumed DirectoryEntryKind: 0 for file, 1 for directory.
fileInfo
pub fn fileInfo(byteLength: usize) -> FileInfoConstructs regular-file metadata with the complete length in bytes.
directoryInfo
pub fn directoryInfo() -> DirectoryInfoConstructs the fieldless portable directory metadata value.
directoryEntry
pub fn directoryEntry(path: Path, kind: DirectoryEntryKind) -> DirectoryEntryConstructs a directory entry by taking ownership of its complete child path and kind.
readFileOperation
pub fn readFileOperation() -> FileOperationSelects the read-file operation.
writeFileOperation
pub fn writeFileOperation() -> FileOperationSelects the write-file operation.
statOperation
pub fn statOperation() -> FileOperationSelects the stat operation.
listDirectoryOperation
pub fn listDirectoryOperation() -> FileOperationSelects the list-directory operation.
createDirectoryOperation
pub fn createDirectoryOperation() -> FileOperationSelects the create-directory operation.
removeFileOperation
pub fn removeFileOperation() -> FileOperationSelects the remove-file operation.
removeDirectoryOperation
pub fn removeDirectoryOperation() -> FileOperationSelects the remove-directory operation.
pathOperation
pub fn pathOperation() -> FileOperationSelects path construction and resolution.
createTemporaryDirectoryOperation
pub fn createTemporaryDirectoryOperation() -> FileOperationSelects the create-temporary-directory operation.
operationCode
pub fn operationCode(operation: FileOperation) -> i32Returns the stable numeric code of a consumed FileOperation.
notFound
pub fn notFound() -> FileReasonConstructs the NotFound recovery reason.
alreadyExists
pub fn alreadyExists() -> FileReasonConstructs the AlreadyExists recovery reason.
permissionDenied
pub fn permissionDenied() -> FileReasonConstructs the PermissionDenied recovery reason.
invalidPath
pub fn invalidPath() -> FileReasonConstructs the InvalidPath recovery reason.
wrongType
pub fn wrongType() -> FileReasonConstructs the WrongType recovery reason.
notEmpty
pub fn notEmpty() -> FileReasonConstructs the NotEmpty recovery reason.
noSpace
pub fn noSpace() -> FileReasonConstructs the NoSpace recovery reason.
tooLarge
pub fn tooLarge() -> FileReasonConstructs the TooLarge recovery reason.
unsupported
pub fn unsupported() -> FileReasonConstructs the Unsupported recovery reason.
other
pub fn other() -> FileReasonConstructs the catch-all Other recovery reason.
reasonCode
pub fn reasonCode(reason: FileReason) -> i32Returns the stable numeric code of a consumed FileReason.
error
pub fn error(operation: FileOperation, reason: FileReason) -> FileErrorConstructs a portable FileError without a provider-specific numeric detail.
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.
providerCode
pub fn providerCode(error: &silk/filesystem.FileError) -> Option<i32>Borrows an error and returns its provider-specific numeric detail, if one was retained.
make
pub effect fn make(value: string) -> 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(pathOperation(), invalidPath()); copying can fail with OutOfMemoryError.
fromBytes
pub effect fn fromBytes(values: &[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.
root
pub effect fn root() -> Path ! OutOfMemoryError ? &mut AllocatorAllocates the portable root path / in the selected allocator.
rawBytes
pub fn rawBytes(self: &silk/filesystem.Path) -> &[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.
view
pub fn view(self: &silk/filesystem.Path) -> stringBorrows 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.
isRoot
pub fn isRoot(self: &silk/filesystem.Path) -> boolReturns true exactly when this path is the portable root /.
name
pub fn name(self: &silk/filesystem.Path) -> stringBorrows 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.
join
pub effect fn join(base: &silk/filesystem.Path, fragment: string) -> 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.
joinUtf8
pub effect fn joinUtf8(base: &silk/filesystem.Path, fragment: &[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.
resolve
pub effect fn resolve(base: &silk/filesystem.Path, relativeText: string) -> 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.
parent
pub effect fn parent(self: &silk/filesystem.Path) -> 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.
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.
Examples
Write a file after creating its parents
import silk.allocator { Allocator }
import silk.filesystem as FileSystem
import silk.usize as usize
pub effect fn store(path: &FileSystem.Path, contents: &[u8]) -> usize
! FileSystem.FileError | Allocator.OutOfMemoryError
? &mut FileSystem.FileSystem | &mut Allocator {
let written = run FileSystem.writeFileWithParents(path, contents)
return contents.length
}Operation readFile
effect fn readFile(path: &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 fn writeFile(path: &silk/filesystem.Path, bytes: &[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 fn stat(path: &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 fn listDirectory(path: &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 fn createDirectory(path: &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 fn removeFile(path: &silk/filesystem.Path) -> () ! FileError ? &mut FileSystemRemoves exactly one regular file and fails with WrongType for a directory.
Operation removeDirectory
effect fn removeDirectory(path: &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 fn createTemporaryDirectory(parent: &silk/filesystem.Path, prefix: &[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.
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.
temporaryDirectory
pub effect fn temporaryDirectory(parent: &silk/filesystem.Path, prefix: string) -> 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.
release
pub effect 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.
releaseIgnored
pub effect 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.
removeDirectoryRecursively
pub effect fn removeDirectoryRecursively(path: &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.
createDirectoriesRecursively
pub effect fn createDirectoriesRecursively(path: &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.
writeFileWithParents
pub effect fn writeFileWithParents(path: &silk/filesystem.Path, bytes: &[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.
exists
pub effect fn exists(path: &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.