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

16. Operations on built-in values are methods (amends 0014)

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

Context

ADR 0014 made every operation on a str / list / map a free function — append(xs, v), keys(m), len(s) — and said so explicitly for Milestone 1, whose goal was a complete but small language. It named the follow-up: "Adding methods to built-in types later is a compatible change… decided in a follow-up ADR." This is that ADR.

The free-function design has three costs that show up as soon as real code is written:

  • No chaining. " a, b ".trim().split(",") has to be written split(trim(" a, b "), ",") — inside-out, and the reader parses it backwards.
  • A crowded global scope. append, pop, keys, values, has, get, remove are all top-level names a program cannot use for its own bindings, and none of them says which type it is for without reading the docs.
  • It fights the language it is part of. Korrin already has instance.method() for user classes. Having list operations be append(xs, v) while a user's own collection type uses xs.add(v) is an inconsistency with no upside.

Milestone 2 also adds a real string and collection vocabulary (upper, split, sort, join, slice, …). Introducing a dozen more free functions would make all three costs worse.

Decision

Operations on str, list, and map are methods, reached with . like any instance method. The . operator resolves on these three built-in types as well as on class instances.

  • The M1 collection free functions are removed, not kept as aliases: appendlist.push, poplist.pop, keys / values / has / get / remove → the same-named map methods. len(x)x.len(). There is one way, per the project's core goal; an alias would mean two.
  • print, type, str, int, float, bool, range, input stay free functions. They construct a value, convert between types, or do I/O — they are not operations on a receiver. type(x) and str(x) in particular must work uniformly on every value, including nil and int, which carry no methods.
  • A method read (xs.push) evaluates to a callable bound to its receiver, the same shape as a bound instance method. It is normally called immediately.
  • Mutating list methods (push, insert, remove_at, reverse, sort) change the receiver in place and return nil; pop and remove_at return the element removed. This matches how a mutable, shared list already behaves and avoids the xs = xs.push(v) misreading 0014 warned about.
  • Implementation: a NativeMethod table per type in crates/korrin/src/interpreter/methods.rs, a Value::BoundNative for the bound-but-not-yet-called value, and one new arm each in the interpreter's attribute-read and call paths. No lexer, parser, resolver, or grammar change — the syntax is the existing attribute and call productions.
  • The method set for M2 is specified in §8.2–8.4.
  • crates/korrin/tests/builtins.rs now checks method coverage against the spec the same way it checks functions: a method missing from §8, or documented but not implemented, fails the build.

Consequences

  • Code reads left to right: text.trim().lower().split(" ").
  • The global scope drops seven names; the remaining eight builtins are all constructors or I/O, which is a describable rule rather than a list.
  • Breaking, on top of removing the free functions: this is the second M2 change (after interpolation) that invalidates M1 code. Every doc example, spec block, golden program, and the guide were swept; the CHANGELOG lists the rename table.
  • The interpreter's . path is no longer instance-only, which is the surface 0014 was deferring. It stays small: three static tables, a linear name lookup, no per-type Value machinery beyond BoundNative.
  • len is now x.len() everywhere, including inside interpolation ("{items.len()} left"). Slightly more to type than len(items); consistent with every other operation, and len was the only builtin that read as an operation on its argument.
  • Future built-in types (a set, a bytes) get methods by adding a table, with no new global names.

Alternatives considered

  • Keep 0014 as-is. Rejected: the costs above are real and compound as the standard vocabulary grows. 0014 itself scoped the free-function choice to M1.
  • Methods, but keep the free functions as aliases. Two ways to do the same thing, forever — the exact thing Korrin's design goal rules out. A short deprecation window was considered unnecessary for a pre-1.0 language with no external users yet.
  • Keep len(x) as a free function, make everything else a method. Tempting — len reads well and Python does this. Rejected for consistency: len is an operation on its argument (unlike type or str), so it belongs with the other operations. One rule ("operations are methods, constructors are functions") beats one rule plus an exception.
  • A hybrid — methods on list / map, functions for str. str gains the most from chaining (.trim().lower()); excluding it makes no sense.