Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 a Result-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 io touch 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)

MemberBehaviour
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)

  1. The stdlib is modules. Reached with a bare import "name", indistinguishable from a file module once loaded (ADR 0019).
  2. Native for now. Each module is Rust, in crates/korrin/src/stdlib/<name>.rs, registered in stdlib::load. A stdlib-in-Korrin is possible on this mechanism later.
  3. 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.
  4. Each module has a coherent flavour. math is float-oriented numeric helpers; io is "talk to the outside world". A module is not a junk drawer.
  5. 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.
  • io failing by raising means every file operation needs a try if 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.kor can 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 nil on failure. text = io.read_file(path) then if text == nil. Easy to forget the check, and it conflates "file was empty" edge cases. Raising is Korrin's model.
  • A Result / okerr return. 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's open(), and it would need io to know which file called it. Rejected — import and io resolve 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.