Skip to content

Files, processes, and system APIs

Operating-system capabilities are standard modules rather than magical globals. Importing them makes both dependency and cost visible:

import console from "std/console";
import fs from "std/fs";
import process from "std/process";
import { delay, sleep } from "std/time";

Filesystem failures use normal typed Z errors:

function load(in path: String): String throws String {
return try fs.readText(path);
}

fs.readText returns an owned, validated UTF-8 String. Use fs.readBytes when arbitrary bytes are valid; it returns an owned Array<u8> without text decoding.

fs.writeText(path, contents) and fs.writeBytes(path, bytes) borrow their arguments for the call, create or truncate the file, and copy the contents into it once; neither allocates beyond the C stdio stream that carries the OS write. fs.writeBytes takes an Array<u8> and writes it exactly, with no text encoding or added newline, so it is the write side of fs.readBytes. Both throw a typed String OS failure.

function stage(in sourcePath: String): void throws String {
const source = try fs.readText(sourcePath);
try fs.createDirectories(".z-cache");
try fs.writeText(".z-cache/output.pending", source);
try fs.replace(
".z-cache/output.pending",
".z-cache/output.zs"
);
}

fs.replace performs an atomic same-filesystem directory-entry replacement. It prevents readers from observing a partially written output; it does not by itself promise persistence after sudden power loss.

Useful queries include:

API Result
fs.exists(path) Non-throwing existence check
fs.isDirectory(path) Whether an existing path is a directory
fs.realPath(path) Owned canonical absolute UTF-8 path or typed failure
fs.createDirectories(path) Creates every missing parent

An interprocess file lock is a move-only owner:

function publish(): void throws String {
const lock: fs.Lock = try fs.lock(".z-cache/build.lock");
try fs.writeText(".z-cache/result.txt", "ready");
// lock is released on scope exit
}

Early release is consuming:

lock.unlock();
// lock is no longer usable

The file may remain on disk after release; the operating system lock itself is released deterministically on scope exit, error propagation, early return, or explicit unlock().

Use std/sync’s Mutex<T> for synchronous in-process shared state. A file lock exists for coordination across processes and protects a different boundary.

import process from "std/process";
function clangVersion(): i32 throws String {
const arguments = Array<String>("--version");
return try process.run("clang", arguments);
}

process.run launches the executable directly, inherits the current standard streams, waits for completion, and returns the child exit code. It does not invoke a shell, interpolate a command string, or treat a nonzero child exit as a launch failure. Failure to launch or wait throws a typed String OS error.

Program arguments are owned values:

const arguments = process.args();
const executable = try process.executablePath();

process.args() excludes the executable name. process.executablePath() returns the running Z binary’s canonical path, which is useful for locating resources or helpers shipped beside it.

From the CLI, separate compiler and program arguments with --:

Terminal window
z run . -- input.zs --verbose
sleep(100); // blocks the current OS thread
await delay(100); // suspends the current task

Both durations are milliseconds. Use sleep only when occupying the current thread is intentional, such as a dedicated native worker. delay requires an async context and lets the executor run other work while the task waits.

Runnable examples: source stager, source inspector, and blocking sleep.

Next: Testing, projects, and the CLI.