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:

FieldTypeMeaning
existsboolWhether the path exists
isFileboolIs a regular file
isDirboolIs a directory
sizeintSize in bytes
modifiedlongLast-modified time (epoch)
createdlongCreation time (epoch)
permissionsintPOSIX 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.stat still returns a FileInfo with exists = false and zeroed fields, rather than failing — check info.exists first.


Quick reference

CallReturnsPurpose
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)boolBlocking overwrite
fs.appendText(p, s) / appendBytes(p, b)Future<bool>Async append
fs.appendTextSync(p, s) / appendBytesSync(p, b)boolBlocking append
fs.exists(p)boolDoes the path exist
fs.stat(p)FileInfoFile metadata
fs.basename(p) / dirname(p) / extension(p)stringPath components
fs.resolve(p)stringAbsolute canonical path
fs.join(a, b)stringJoin 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> / boolRename a path
fs.copy(a, b) / copySync(a, b)Future<bool> / boolCopy a file
fs.tempFile() / tempFileSync()Future<string> / stringCreate a temporary file
fs.tempDir() / tempDirSync()Future<string> / stringCreate a temporary directory
fs.watch(p, callback)voidInvoke callback after a path change
fs.mkdir(p)boolCreate a directory
fs.delete(p)Future<bool>Async remove a file or directory
fs.deleteSync(p)boolBlocking 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) or fs.listDirFull (paths).
  • Recursive traversal → fs.walk; immediate entries plus metadata → fs.readDirInfo.
  • File moves/copies → fs.rename / fs.copy; use *Sync only when blocking is acceptable.