20. The io module and standard-library principles
- Status: Accepted
- Date: 2026-09-03
Context
ADR 0019 built the built-in-module mechanism and
shipped math. It named io — file reading and writing, command-line
arguments — as the next module, needed before "write a real tool in Korrin" is
true. This ADR settles io's surface and the questions a filesystem module
raises, and writes down the standard-library principles that were implicit in
0019.
Questions specific to io:
- How does a failing operation report? Return
nil/ a sentinel, return aResult-like value, or raise? - What are paths relative to? The process's working directory, or the script
that called
io? - Is there a sandbox? Can
iotouch any file, or only some? - A file-handle object (
io.open(path)→ something with.read()/.close()), or whole-file helpers?
Decision
io's surface (Milestone 2)
| Member | Behaviour |
|---|---|
io.read_file(path) | the file's contents as a str |
io.write_file(path, text) | write text, creating or truncating; returns nil |
io.args() | the command-line arguments after the script name, a list of str |
There is no io.read_line(): the input builtin
already reads a line from standard input (its optional prompt is the only extra),
and a second way to do the same thing is exactly what Korrin avoids.
Failures raise
Every io operation that can fail raises E0316 on
failure — a catchable exception (ADR 0017), not a nil
return or an error value. This matches how the rest of Korrin fails (a bad index,
a divide by zero): loudly, at the point of failure, with a message. A program
that wants to handle a missing file wraps the call in try.
Paths are OS-relative
io passes paths to the operating system unchanged. A relative path is relative
to the process's working directory — the same as open() in Python, fs in
Node, File::open in Rust. This is deliberately different from import, whose
"./x" is relative to the importing file (ADR 0018):
import wires a program together at load time and must not depend on where it is
run from; io is a runtime data operation and should behave like every other
language's file I/O.
No sandbox
io can read and write anything the user running korrin can. A Korrin script
is trusted code, like a shell script or a Python script. A capability/permission
system (an allow-list of paths, a --deny-io flag) is a plausible future
addition and is not foreclosed, but it is not Milestone 2.
Whole-file helpers, not handles
read_file / write_file read or write the whole thing. There is no file-handle
object, no streaming, no seek. Most scripts want the whole file; streaming can
be added later (io.open, or an iterator of lines) without changing these.
Standard-library principles (making 0019 explicit)
- The stdlib is modules. Reached with a bare
import "name", indistinguishable from a file module once loaded (ADR 0019). - Native for now. Each module is Rust, in
crates/korrin/src/stdlib/<name>.rs, registered instdlib::load. A stdlib-in-Korrin is possible on this mechanism later. - Small, and grows per module. A new module needs a file, a spec section in §11, a guide mention, and a coverage test — no language change. So modules can be added one at a time as a real need appears, not bundled speculatively.
- Each module has a coherent flavour.
mathis float-oriented numeric helpers;iois "talk to the outside world". A module is not a junk drawer. - Failures raise, conversions are explicit, no hidden state. The same rules as the core language.
Consequences
- A Korrin program can now read its input, write its output, and see its arguments — the last missing piece for command-line tools.
iofailing by raising means every file operation needs atryif the program wants to recover — more verbose than checking a return value, but consistent and impossible to forget silently.- OS-relative paths can surprise someone who expects
io.read_file("data.txt")to find a file next to the script. It is the universal convention, and it is documented; a script that wants script-relative behaviour can build the path from a known location. - No sandbox means
korrin run untrusted.korcan do anything. This is stated plainly; it is the same trust model as every scripting language and the CLI docs will say so.
Alternatives considered
- Return
nilon failure.text = io.read_file(path)thenif text == nil. Easy to forget the check, and it conflates "file was empty" edge cases. Raising is Korrin's model. - A
Result/ok–errreturn. Korrin has no such type and ADR 0017 chose exceptions over that model language-wide. - Script-relative paths. Consistent with
import, but against every other language'sopen(), and it would needioto know which file called it. Rejected —importandioresolve paths for different reasons. - A sandbox by default. Safer for running untrusted scripts, but Korrin is not positioned as a sandbox runtime, and it would block the common case (a tool that legitimately writes files) behind configuration. Left for later.
io.open(path)returning a handle. More capable (streaming, partial reads). More surface, and a.close()to forget. Whole-file helpers cover the common case; handles can come when a real need does.