radlang — File System (fs)
This document covers the fs namespace: reading and writing files, inspecting
paths, listing directories, and querying file metadata.
For language fundamentals see the Overview.
Overview
fs is a compiler-intrinsic namespace. The name is reserved and dispatched
directly to the runtime — it needs no import:
fn main() {
if sys.blockOn(fs.writeText("hello.txt", "hi there")) {
string? contents = sys.blockOn(fs.readText("hello.txt"))
sys.output(contents ?? "(unreadable)")
}
} Content reads and writes are asynchronous and return futures. Use await inside
an async fn or sys.blockOn from synchronous code. The *Sync variants keep
the blocking behavior and return their result directly. Write, append, mkdir,
and delete report success rather than throwing. readText returns a nullable string so a missing or unreadable file is distinguishable from an
empty one.
Reading files
Text
fs.readText(path: string) : Future<string?>
fs.readTextSync(path: string) : string? Reads an entire file as text. Returns null if the file cannot be read
(missing, permission denied, etc.), so use ?? or a null check.
fn main() {
string? data = sys.blockOn(fs.readText("notes.txt"))
if data == null {
sys.error("could not read notes.txt")
return
}
sys.output(data)
} Bytes
fs.readBytes(path: string) : Future<byte[]>
fs.readBytesSync(path: string) : byte[] Reads a file as a list of raw bytes — use this for binary data or when you need byte-level access.
fn main() {
byte[] bytes = sys.blockOn(fs.readBytes("image.png"))
sys.output("read " + bytes.length().toString() + " bytes")
} Writing files
fs.writeText(path: string, text: string) : Future<bool>
fs.writeTextSync(path: string, text: string) : bool
fs.writeBytes(path: string, data: byte[]) : Future<bool>
fs.writeBytesSync(path: string, data: byte[]) : bool
fs.appendText(path: string, text: string) : Future<bool>
fs.appendTextSync(path: string, text: string) : bool
fs.appendBytes(path: string, data: byte[]) : Future<bool>
fs.appendBytesSync(path: string, data: byte[]) : bool Each returns true on success, false on failure. write* truncates and
replaces any existing file; append* adds to the end (creating the file if
needed).
fn main() {
sys.blockOn(fs.writeText("log.txt", "first line\n"))
sys.blockOn(fs.appendText("log.txt", "second line\n"))
byte[] header = [0x89, 0x50, 0x4E, 0x47]
sys.blockOn(fs.writeBytes("out.bin", header))
} For blocking code, use writeTextSync, writeBytesSync, appendTextSync,
and appendBytesSync instead.
Paths
fs.basename(path) : string // final component
fs.dirname(path) : string // parent directory
fs.extension(path) : string // file extension
fs.resolve(path) : string // absolute, canonical path
fs.join(a, b) : string // join two path segments fn main() {
sys.output(fs.basename("/usr/local/bin/rad")) // rad
sys.output(fs.dirname("/usr/local/bin/rad")) // /usr/local/bin
sys.output(fs.extension("archive.tar.gz")) // gz
sys.output(fs.join("src", "main.rad")) // src/main.rad
sys.output(fs.resolve("./config.toml")) // /abs/path/config.toml
} Directories
fs.mkdir(path: string) : bool // create a directory
fs.delete(path: string) : Future<bool> // async remove
fs.deleteSync(path: string) : bool // blocking remove
fs.listDir(path: string) : string[] // entry names only
fs.listDirFull(path: string) : string[] // full paths of entries
fs.walk(path: string) : Future<string[]> // recursive paths
fs.walkSync(path: string) : string[] fn main() {
fs.mkdir("build")
string[] names = fs.listDir(".") // ["main.rad", "config.toml", ...]
for (string name in names) {
sys.output(name)
}
string[] full = fs.listDirFull("src") // ["src/main.rad", "src/lib.rad", ...]
sys.output(full.length().toString() + " entries")
} walk recursively returns the paths below a directory. walkSync performs the
same traversal on the calling thread.
Directory metadata
type DirEntry {
string path
FileInfo info
}
fs.readDirInfo(path: string) : Future<DirEntry[]>
fs.readDirInfoSync(path: string) : DirEntry[] These return immediate children (not recursive), with each child’s full path
and metadata. The asynchronous form performs directory enumeration and stat calls on the worker pool.
File operations
fs.rename(from: string, to: string) : Future<bool>
fs.renameSync(from: string, to: string) : bool
fs.copy(from: string, to: string) : Future<bool>
fs.copySync(from: string, to: string) : bool
fs.tempFile() : Future<string>
fs.tempFileSync() : string
fs.tempDir() : Future<string>
fs.tempDirSync() : string rename and copy report success or failure. The temporary path helpers
create and return a unique file or directory path.
Watching a path
fs.watch(path: string, fn(string changedPath): void callback): void Registers a callback that runs on the microtask queue after the watched path’s existence, size, or modification time changes. Watching is asynchronous and does not block the RadLang main thread. Callbacks should be short-lived; an active watch uses one shared async worker while it waits for a change.
File metadata
fs.exists(path: string) : bool
fs.stat(path: string) : FileInfo fs.exists is a quick existence check. fs.stat returns a FileInfo struct
with full metadata:
| Field | Type | Meaning |
|---|---|---|
exists | bool | Whether the path exists |
isFile | bool | Is a regular file |
isDir | bool | Is a directory |
size | int | Size in bytes |
modified | long | Last-modified time (epoch) |
created | long | Creation time (epoch) |
permissions | int | POSIX permission bits |
fn main() {
if fs.exists("main.rad") {
FileInfo info = fs.stat("main.rad")
sys.output("is file: " + info.isFile.toString())
sys.output("size: " + info.size.toString() + " bytes")
sys.output("modified: " + info.modified.toString())
}
} Note. When a path does not exist,
fs.statstill returns aFileInfowithexists = falseand zeroed fields, rather than failing — checkinfo.existsfirst.
Quick reference
| Call | Returns | Purpose |
|---|---|---|
fs.readText(p) | Future<string?> | Async text read (null on failure) |
fs.readTextSync(p) | string? | Blocking text read |
fs.readBytes(p) | Future<byte[]> | Async byte read |
fs.readBytesSync(p) | byte[] | Blocking byte read |
fs.writeText(p, s) / writeBytes(p, b) | Future<bool> | Async overwrite |
fs.writeTextSync(p, s) / writeBytesSync(p, b) | bool | Blocking overwrite |
fs.appendText(p, s) / appendBytes(p, b) | Future<bool> | Async append |
fs.appendTextSync(p, s) / appendBytesSync(p, b) | bool | Blocking append |
fs.exists(p) | bool | Does the path exist |
fs.stat(p) | FileInfo | File metadata |
fs.basename(p) / dirname(p) / extension(p) | string | Path components |
fs.resolve(p) | string | Absolute canonical path |
fs.join(a, b) | string | Join path segments |
fs.listDir(p) | string[] | Directory entry names |
fs.listDirFull(p) | string[] | Directory entries as full paths |
fs.walk(p) / walkSync(p) | Future<string[]> / string[] | Recursive directory paths |
fs.readDirInfo(p) / readDirInfoSync(p) | Future<DirEntry[]> / DirEntry[] | Immediate entries with metadata |
fs.rename(a, b) / renameSync(a, b) | Future<bool> / bool | Rename a path |
fs.copy(a, b) / copySync(a, b) | Future<bool> / bool | Copy a file |
fs.tempFile() / tempFileSync() | Future<string> / string | Create a temporary file |
fs.tempDir() / tempDirSync() | Future<string> / string | Create a temporary directory |
fs.watch(p, callback) | void | Invoke callback after a path change |
fs.mkdir(p) | bool | Create a directory |
fs.delete(p) | Future<bool> | Async remove a file or directory |
fs.deleteSync(p) | bool | Blocking remove a file or directory |
Picking the right tool
- Text config/logs →
fs.readText/fs.writeText/fs.appendText. - Binary files →
fs.readBytes/fs.writeBytes. - Just checking presence →
fs.exists; need size/times →fs.stat. - Manipulating path strings →
basename/dirname/extension/join/resolve. - Walking a folder →
fs.listDir(names) orfs.listDirFull(paths). - Recursive traversal →
fs.walk; immediate entries plus metadata →fs.readDirInfo. - File moves/copies →
fs.rename/fs.copy; use*Synconly when blocking is acceptable.