module io
module lib/io.mu
import "io"
io is the file and descriptor API.
Everything here is a COMPLETE operation: io.write writes all of its data or returns an error, io.read_all reads to end of file, io.readln returns one whole line. io.read is the exception and says so — it is one read(2), and a short result is normal.
io also owns the platform knowledge underneath those operations: which open syscall this kernel wants, what its O_* flags are numbered, and how its directory records are laid out. The layer below that is the syscall builtin itself; a program that wants one raw unlooped write calls syscall("write", fd, text, len(text)) rather than reaching for a module.
Imports
Values
stderr#
stderr is descriptor 2, the process's standard error.
Source lib/io.mu:26
stderr := 2stdin#
stdin is descriptor 0, the process's standard input.
Source lib/io.mu:20
stdin := 0stdout#
stdout is descriptor 1, the process's standard output.
Source lib/io.mu:23
stdout := 1Functions
append_file#
append_file appends data to path, creating it if it does not exist.
Source lib/io.mu:232
fn append_file(path, data) {
return _write_whole_file("append_file", path, data, "a")
}close#
close closes a file descriptor.
Source lib/io.mu:61
fn close(fd) {
if !typing.is_int(fd) {
return error("io.close expects integer fd, got " + type(fd))
}
result := syscall("close", fd)
if is_error(result) {
return result
}
// Drop any readln carry-over for this descriptor. The kernel recycles fd
// numbers, so a buffer that outlived its close would hand the next file
// opened onto the same number another file's leftover bytes.
del(_readln_bufs, str(fd))
return nil
}exists#
exists reports whether path names anything at all (file, directory, or otherwise). It follows symlinks, so a dangling link answers false.
Source lib/io.mu:404
fn exists(path) {
return !is_error(stat(path))
}is_dir#
is_dir reports whether path names a directory. A path that does not exist answers false rather than an error, so it can gate a mkdir directly.
Source lib/io.mu:411
fn is_dir(path) {
info := stat(path)
if is_error(info) {
return false
}
return info["is_dir"]
}list_dir#
list_dir returns the entry names in a directory, excluding "." and "..".
This is the one platform-specific thing io does, because there is no portable half to lift out: linux and darwin disagree on both the syscall and the layout of the records it returns.
Source lib/io.mu:493
fn list_dir(path) {
if !typing.is_str(path) {
return error("io.list_dir expects string path, got " + type(path))
}
flags := _open_flags("d")
if is_error(flags) {
return flags
}
// O_DIRECTORY fails on a non-directory, so this open is also the
// is-it-a-directory check.
fd := _open_fd(path, flags)
if is_error(fd) {
return _path_error("list_dir", path, fd)
}
if fd < 0 {
return error("io.list_dir: cannot open " + path)
}
p := platform()
names := nil
if p == "darwin/amd64" || p == "darwin/arm64" {
names = _read_dents_darwin(fd)
} else {
if p == "linux/amd64" || p == "linux/arm64" || p == "linux/riscv64" {
names = _read_dents_linux(fd)
} else {
names = error("io.list_dir: unsupported platform " + p)
}
}
close(fd)
return names
}mkdir#
mkdir creates a directory with mode 0755. The parent must already exist; creating a path that already exists is an error, as it is everywhere else.
Source lib/io.mu:424
fn mkdir(path) {
if !typing.is_str(path) {
return error("io.mkdir expects string path, got " + type(path))
}
result := syscall("mkdirat", 0 - 100, path + chr(0), 493)
if is_error(result) {
return _path_error("mkdir", path, result)
}
return nil
}open#
open opens a file path in mode "r" (read), "w" (truncate) or "a" (append), and returns a file descriptor.
Source lib/io.mu:31
fn open(path, mode) {
if !typing.is_str(path) {
return error("io.open expects string path, got " + type(path))
}
if !typing.is_str(mode) {
return error("io.open expects string mode, got " + type(mode))
}
if mode != "r" && mode != "w" && mode != "a" {
return error("io.open expects mode \"r\", \"w\" or \"a\", got \"" + mode + "\"")
}
flags := _open_flags(mode)
if is_error(flags) {
return flags
}
fd := _open_fd(path, flags)
if is_error(fd) {
return _path_error("open", path, fd)
}
return fd
}read#
read issues ONE read(2) of up to size bytes from fd and returns them as a string. A short read is normal and does not mean end of file; only "" does. Use read_all to read a descriptor to exhaustion.
Source lib/io.mu:83
fn read(fd, size) {
if !typing.is_int(fd) {
return error("io.read expects integer fd, got " + type(fd))
}
if !typing.is_int(size) {
return error("io.read expects integer size, got " + type(size))
}
if size < 0 {
return error("io.read expects non-negative size, got " + str(size))
}
return _read_chunk(fd, size)
}read_all#
read_all reads fd to end of file and returns everything it produced.
Source lib/io.mu:101
fn read_all(fd) {
if !typing.is_int(fd) {
return error("io.read_all expects integer fd, got " + type(fd))
}
parts := []
while true {
chunk := _read_chunk(fd, 65536)
if is_error(chunk) {
return chunk
}
if len(chunk) == 0 {
return strings.join(parts, "")
}
parts = append(parts, chunk)
}
return strings.join(parts, "")
}read_all_each#
read_all_each reads every fd in fds to end of file AT THE SAME TIME and returns one result per fd, in order (each a string or an error value).
Each read runs in its own task, and the scheduler parks a task whose fd is quiet, so one blocked pipe cannot stall the others. process.run needs exactly this: a child that fills one pipe buffer blocks in write(2) until the parent drains it, so reading a child's stdout and stderr one after the other can deadlock against a child interleaving both. The helper lives here rather than in process because that module's own wait() shadows the task builtin of the same name.
Source lib/io.mu:136
fn read_all_each(fds) {
tasks := []
i := 0
while i < len(fds) {
tasks = append(tasks, task(read_all, fds[i]))
i = i + 1
}
out := []
i = 0
while i < len(tasks) {
out = append(out, wait(tasks[i]))
i = i + 1
}
return out
}read_file#
read_file opens path, reads the whole file, closes it, and returns its text.
Source lib/io.mu:158
fn read_file(path) {
if !typing.is_str(path) {
return error("io.read_file expects string path, got " + type(path))
}
fd := open(path, "r")
if is_error(fd) {
return fd
}
data := read_all(fd)
if is_error(data) {
close(fd)
return _path_error("read", path, data)
}
closed := close(fd)
if is_error(closed) {
return _path_error("close", path, closed)
}
return data
}readln#
readln reads a single line from stdin, without its line ending. It answers nil when there is nothing left to read.
Source lib/io.mu:270
fn readln() {
return readln_from(stdin)
}readln_from#
readln_from reads a single line from fd, without its line ending.
It reads one chunk at a time so an interactive terminal is not held waiting for end of file, and keeps whatever followed the line ending for the next call. A final line with no trailing newline is still returned; nil means there is nothing left.
Source lib/io.mu:285
fn readln_from(fd) {
if !typing.is_int(fd) {
return error("io.readln_from expects integer fd, got " + type(fd))
}
key := str(fd)
buffer := _readln_bufs[key]
if buffer == nil {
buffer = ""
}
while true {
ending := strings.find_newline(buffer)
if ending[0] >= 0 {
idx := ending[0]
skip := ending[1]
line := strings.substring(buffer, 0, idx)
_readln_bufs[key] = strings.substring(buffer, idx + skip, len(buffer))
return line
}
chunk := _read_chunk(fd, 1024)
if is_error(chunk) {
return chunk
}
if len(chunk) == 0 {
_readln_bufs[key] = ""
if len(buffer) > 0 {
return buffer
}
return nil
}
buffer = buffer + chunk
}
}remove#
remove deletes a file (not a directory). unlinkat on linux, because the asm-generic ABI (arm64, riscv64) has no unlink(2); unlink on darwin.
Source lib/io.mu:466
fn remove(path) {
if !typing.is_str(path) {
return error("io.remove expects string path, got " + type(path))
}
platform_key := platform()
result := nil
if platform_key == "darwin/amd64" || platform_key == "darwin/arm64" {
result = syscall("unlink", path + chr(0))
} else {
result = syscall("unlinkat", 0 - 100, path + chr(0), 0)
}
if is_error(result) {
return _path_error("remove", path, result)
}
return nil
}rename#
rename atomically renames (moves) old to new within a filesystem, replacing new if it exists. renameat2 on linux -- the only rename the riscv64 kernel has -- and renameat on darwin, which has no renameat2.
Source lib/io.mu:442
fn rename(old, new) {
if !typing.is_str(old) || !typing.is_str(new) {
return error("io.rename expects string paths, got " + type(old) + " and " + type(new))
}
platform_key := platform()
result := nil
if platform_key == "darwin/amd64" || platform_key == "darwin/arm64" {
result = syscall("renameat", 0 - 100, old + chr(0), 0 - 100, new + chr(0))
} else {
result = syscall("renameat2", 0 - 100, old + chr(0), 0 - 100, new + chr(0), 0)
}
if is_error(result) {
return _path_error("rename", old, result)
}
return nil
}stat#
stat returns a file's metadata: a map with "size" (bytes), "mode" (the raw st_mode), "is_dir"/"is_file", "mtime" (Unix seconds) and "mtime_ns" (the same instant in nanoseconds). Symlinks are followed. An error value answers a path that does not exist -- use exists() for the boolean question.
One syscall serves every platform -- fstatat(AT_FDCWD, path, buf, 0) -- but the struct it fills is laid out three ways, and this module owns that knowledge the same way it owns dirent layouts: mode is a 32-bit word at 24 on linux/amd64 and at 16 on asm-generic linux (arm64, riscv64), and a 16-bit word at 4 on darwin; size sits at 48 on linux and 96 on darwin; the modification timespec sits at 88/96 on linux and 48/56 on darwin.
Source lib/io.mu:340
fn stat(path) {
if !typing.is_str(path) {
return error("io.stat expects string path, got " + type(path))
}
b := buf(144)
result := syscall("fstatat", 0 - 100, path + chr(0), b, 0)
if is_error(result) {
return _path_error("stat", path, result)
}
platform_key := platform()
mode := 0
size := 0
sec := 0
nsec := 0
if platform_key == "darwin/amd64" || platform_key == "darwin/arm64" {
mode = _stat_word(b, 4, 2)
size = _stat_word(b, 96, 8)
sec = _stat_word(b, 48, 8)
nsec = _stat_word(b, 56, 8)
} else {
if platform_key == "linux/amd64" {
mode = _stat_word(b, 24, 4)
size = _stat_word(b, 48, 8)
sec = _stat_word(b, 88, 8)
nsec = _stat_word(b, 96, 8)
} else {
if platform_key == "linux/arm64" || platform_key == "linux/riscv64" {
mode = _stat_word(b, 16, 4)
size = _stat_word(b, 48, 8)
sec = _stat_word(b, 88, 8)
nsec = _stat_word(b, 96, 8)
} else {
return error("io.stat: unsupported platform " + platform_key)
}
}
}
kind := mode & 61440
return {"size": size, "mode": mode, "is_dir": kind == 16384, "is_file": kind == 32768, "mtime": sec, "mtime_ns": sec * 1000000000 + nsec}
}write#
write writes ALL of data to fd and returns the byte count. A single write(2) may accept less than it was offered — it returns how much it took — so this loops until nothing is left.
Source lib/io.mu:189
fn write(fd, data) {
if !typing.is_int(fd) {
return error("io.write expects integer fd, got " + type(fd))
}
if !typing.is_str(data) {
return error("io.write expects string data, got " + type(data))
}
total := 0
length := len(data)
while total < length {
chunk := data
if total > 0 {
chunk = strings.substring(data, total, length)
}
wrote := syscall("write", fd, chunk, len(chunk))
if is_error(wrote) {
return wrote
}
if wrote <= 0 {
return error("io.write made no progress with " + str(length - total) + " bytes remaining")
}
total = total + wrote
}
return total
}write_file#
write_file writes data to path, replacing any existing file.
Source lib/io.mu:226
fn write_file(path, data) {
return _write_whole_file("write_file", path, data, "w")
}Internal helpers
Underscore-prefixed names are implementation detail. They are listed so the module's source reads without surprises, not as API — they may change at any time.
| _open_fd(path, flags) | — |
| _open_flags(mode) | — |
| _path_error(action, path, err) | — |
| _read_chunk(fd, size) | — |
| _read_dents_darwin(fd) | — |
| _read_dents_linux(fd) | — |
| _readln_bufs | Per-fd carry-over buffers: bytes read past a line's ending are kept here for the next readln instead of being discarded. |
| _stat_word(b, off, width) | — |
| _write_whole_file(caller, path, data, mode) | — |