ADR-389: immutable Binds a Value Nothing Reachable From It Ever Changes; at Item Level It Is Evaluated Once, on First Use

Status: Accepted Date: November 3, 2026 (owner’s decision) Answers: #1174 point 3: a value the whole program reads, computed once at run time (the compiler’s own accepted_dump_dir() reads an environment variable once and keeps the answer, a Rust OnceLock), and a way to say inside a function that a value never changes, deeply Related: ADR-351 D8 (no global state), Part II 10.2 (comptime), Part I 2.1 (let, mut), Part I 6.2 (Shared, SharedMut, Locked), Part II 11.2 (what a task may be handed)

1. Question

let is shallow: the name cannot be assigned again, but a SharedMut it holds can still be changed through another owner. comptime X = f() at item level is evaluated at build time and cannot read what only the running program knows (an environment variable, the path of the program). A Rust static with a OnceLock is global state, which ADR-351 D8 refuses. How does a program say “this value never changes”, and how does it keep a value computed at run time, once, for every part of the program to read?

2. Decision

D1 - immutable is a reserved word and a binding

immutable x = expr binds a name like let, inside a function body and where an item stands. A type may be written, immutable x: T = expr. immutable mut does not exist.

D2 - Immutability is deep (transitive)

Neither the name nor anything reachable from its value ever changes. The value’s type may not reach, through fields, elements, variants or captures, a SharedMut, a Locked, a channel end, or another handle to storage shared and changeable through another owner. Such a binding is refused with NK1331, labelled at the initializer, naming the path to the first such part (config.cache: SharedMut[Map[String, Int]]). A Shared[T] is allowed where T itself passes the rule. Methods taking ref mut self cannot be called on it, and it cannot be moved out of: it is read through views.

D3 - Inside a function, it is handed to tasks without a copy or a count

A task may read an immutable binding of the function that starts it, by capture or as an argument, with no copy and no owner count: nothing can change it, and the compiler keeps it alive until the last task that reads it has ended (the scope rule of Part I 8.3). It never meets NK2101.

D4 - At item level, it is evaluated lazily, exactly once, thread-safe

immutable X = f() where an item stands is evaluated on first access, exactly once, even when several tasks reach it at the same time; the others wait for the one that evaluates it. Every later access reads the same value. It is not global state in the sense of ADR-351 D8: nobody can change it, so no part of the program can affect another through it. An item-level immutable may be pub. Its initializer may read other item-level immutables; a cycle among them is refused at build time with NK1331, naming the cycle.

D5 - The initializer may not throw

An item-level initializer whose call can fail is refused (NK1331, the help writes a catch or a T?). A value that may be missing is written as T?: immutable DUMP_DIR: String? = env::var("NIKAIA_DUMP_ACCEPTED"). A crash in the initializer is a crash of the program at first access.

D6 - comptime and let keep their meaning

comptime X = f() stays the form for values known at build time (Part II 10.2); let stays shallowly immutable. A value that is known at build time and deeply immutable is written with comptime.

3. Why

4. To build

  1. Lexer: immutable on the reserved list (Part I 2.1).
  2. Parser: the binding inside a function body and at item level, with an optional type and pub.
  3. Checker: the deep rule of D2 (NK1331), no ref mut self calls and no moves out; the no-throw rule of D5 and the cycle rule of D4.
  4. Tasks: D3, no NK2101 and no count for an immutable capture.
  5. Code generation: item level as a once-evaluated cell (LazyLock in the emitted Rust), function level as a plain binding.
  6. Tests in tests/compiler/ for each rule and each refusal.
  7. Use it for accepted_dump_dir() in the move of check_walked (#125).
This site is open source. Improve this page.