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

19. Built-in modules and the first standard library

  • Status: Accepted
  • Date: 2026-09-03

Context

ADR 0018 built the module system and split specifiers two ways: "./x" is a file, anything else is a "built-in module". This ADR decides what a built-in module is, how import tells the two apart, and what the first one (math) contains.

Constraints:

  • Korrin's builtins (print, range, …) are a deliberately tiny set of constructors and I/O (ADR 0016). Numeric helpers like sqrt do not belong there — they are a library, and a program that never does maths should not carry them.
  • The module system already gives us namespacing (math.sqrt) and lazy loading (a module builds on first import). A standard library should use it, not a second mechanism.

Decision

"name" vs "./name"

The specifier's first characters decide, with no filesystem probe:

  • starts with ./ or ../ → a file (ADR 0018);
  • anything else → a built-in module, looked up by exact name in a fixed table.

So import "math" is always the standard library's math, even if a math.kor sits next to the importing file. To import that file, write import "./math". This keeps the two namespaces from ever shadowing each other, in either direction, and makes an import line's meaning obvious without knowing what files exist.

What a built-in module is

A built-in module is a scope populated in Rust and handed to the loader, which wraps it in the same Value::Module a file import produces and caches it under builtin:<name>. From Korrin it is indistinguishable from a file module: type() is "module", members are module.x, _-names are private, loading happens once.

Built-in modules live in crates/korrin/src/stdlib/, one file each, registered in stdlib::load. crates/korrin/tests/builtins.rs checks each module's exported names against its specification section, both ways.

math (Milestone 2)

math is float-flavoured: operations that are naturally real-valued return float; the rounding operations return int.

NameResult
math.sqrt(x)√x as a float; x must be ≥ 0 (E0304)
math.pow(base, exp)base ** exp as a float
math.floor(x) / math.ceil(x) / math.round(x)nearest int in that direction (round is half-away-from-zero); E0304 if it will not fit int
math.abs(x)absolute value, keeping x's type (intint, floatfloat)
math.min(a, …) / math.max(a, …)the smallest / largest argument (one or more numbers), returned unchanged
math.pi / math.ethe constants, as float

io (file reading and writing, CLI arguments) is delivered the same way — see ADR 0020, which also writes down the standard-library principles this ADR started.

Consequences

  • The global scope stays tiny; numeric code opts in with one import "math".
  • A standard library is "just modules" — no new runtime concept, and a third-party could ship a module that is imported identically (import "./their/lib").
  • import "math" meaning the stdlib unconditionally is a small foot-gun for someone who wants to shadow it with their own math.kor — but that is rare, and "./math" is right there. The predictability is worth it.
  • Native modules cannot (yet) be written in Korrin — math is Rust. A future "stdlib written in Korrin, bundled as source" is possible on this same mechanism and is not foreclosed.
  • Rounding functions returning int (not float) is a small inconsistency with sqrt/pow, chosen because int(math.floor(x)) everywhere would be noise and a floored value is conceptually a whole number.

Alternatives considered

  • A filesystem probe — try ./math.kor, fall back to built-in. Rejected: an import line's meaning would depend on which files exist, and adding a file could silently change behaviour.
  • A std: or @ prefix for built-ins (import "std:math"). Explicit, but ugly for the common case and a new bit of syntax. "No ./ means built-in" is a rule you learn once.
  • Putting sqrt etc. in the global builtins. Against ADR 0016's "builtins construct or convert, they are not a library". Every program would carry them.
  • A bigger first cut (json, time, random, …). Deferred. math and io are what "write a real tool" needs first; the rest can land per-module with no design cost now that the mechanism exists.