ADR-072: A File a Build Reads Is Named Three Times, and a Build Given No List Reads Nothing

Status: Superseded by ADR-310 — its D1-D8 are D4-D11 there Date: September 14, 2026 Target Version: Nikaia 0.0.8 Answers: ADR-026 Q5 (does reading need to be enabled?) and the filesystem half of its Q6. That record stays Open: what a build-time body may compute is not decided here Related: ADR-026 D1 (I/O belongs to the compiler) and D2 (a compile-time path stays inside the project), ADR-021 D13 (the asset dimension of the cache key, which this is the producer for), Part II 10.6 (comptime X = Json.value(asset("config.json"))), ADR-116 (asset("…") is the spelling), ADR-287 D6 and ADR-287 (the build-time evaluator the read happens inside, and what bounds it)


1. Context

The spelling has been in the specification since Part II 10.2 was written:

comptime CONFIG: Json::Value = Json.value(asset("config.json"))

and ADR-026 D1 already decided that the compiler does that reading rather than the evaluated body. So a build-time body performs no I/O of its own: it is handed bytes the compiler fetched, named at the site by a literal.

What D1 left open is Q5, and Q5 is the question with teeth. D2 keeps a build out of /etc/passwd; it does not keep a dependency’s grammar out of your secrets.toml, which sits inside your project root exactly where D2 allows reading. Q5 offered three options — a manifest declaration, a per-invocation flag, or no grant at all, relying on the lockfile record to make a new read visible after the fact.

2. Decisions

D1 — A build given no list reads nothing at compile time.

The default is the strict one. With no --allow-read-from-list, an asset("…") under a comptime is refused, and the whole class of compile-time file reading is off.

This is the part worth having even for a project that never intends to read anything: “this build reads nothing while building” is not a claim somebody has to make, keep true, and be believed about. It is what happens when nothing is passed.

D2 — The switch names a file, and that file names the files.

nikaia build --allow-read-from-list=build-reads.txt

One path per line; # begins a comment. The list is a file rather than a repeated flag because a list on a command line lives in whoever’s CI configuration runs the build, and a list in a file lives in the repository, where a change to it is a diff.

D3 — A file must be named in all three places, and this is the decision.

The flag says a list is in effect. The list says which files. The asset("…") literal says which one this line reads. A read that is missing any of the three is refused, and the three are deliberately not derivable from one another:

Two of the three are committed, so a change to either is visible in review. The third is not, and that is what it is for: a build may be run with the reads switched off without editing anything.

D4 — The check is against the literal, so the literal may not be computed.

from takes a string literal under a const, never a name and never an expression that folds to one. If a path could be computed, “named in the code” would stop being decidable by looking at the line, and the property the three namings buy is exactly that it is decidable by looking.

The cost is real and is accepted: a build that wants config/linux.toml and config/wasm.toml writes both, in the code and in the list.

D5 — No patterns, no directory listings.

A glob is a read of something nobody named — the directory — and it would let a list entry stand for files that do not exist yet. Both halves defeat D3. A build that wants ten files lists ten files.

D6 — The list binds the whole build, dependencies included.

A from line inside a dependency is checked against the list of the build that is running, not against anything the dependency ships. A package cannot bring its own permission.

This is stricter than resolving paths relative to the package that wrote them, and the difference is the point: a dependency that reads a file makes the consumer write a line for it, which is where the consumer finds out. The friction is the feature — it is the case Q6 calls the build.rs attack surface, answered by making the read impossible to add quietly rather than merely recorded after the fact.

D7 — The list file is hashed into the cache key.

It is a file the build read, so it belongs in the asset dimension with the files it names (ADR-021 D13). Changing a line must invalidate, or a build that stops reading a file keeps the old answer.

D8 — An entry nothing reads is a warning.

A list that may hold names nothing uses decays into everything we ever needed, which is how an allowlist stops being read. Naming the unused entry at the end of a build keeps it the size of the truth.

3. Consequences

Three places is the cost and it is charged on purpose. Nothing here is cheaper than the alternatives; it is more visible than them. ADR-021 records every file a build read in a committed lockfile, which makes a new read visible after it happened; this makes it impossible before.

The lockfile’s asset dimension gets its producer, which is the thing it has been carrying through Key::build with a comment saying nothing reports assets yet.

const is the prerequisite and does not exist. Part II 10.2’s status note says it plainly: there is no const in the parser, at item level it is a parse error, and in a body const X = 1 parses as two statements. Until that changes there is no place for a from line to stand and nothing for this check to check. The word is also not reserved.

What is still open is what a const body may compute. ADR-026 Q4 asks it, and this record does not answer it: whether the body must be sync, what a step budget looks like, and what bounds its memory are untouched here. This decides where the bytes come from, and nothing about what happens to them afterwards.

4. What is built

All of D1 to D8, at 0.0.123. The order this record named has been walked: comptime arrived with ADR-287, Q4’s evaluation rules are ADR-287’s two ledger columns, and the evaluator that runs the initialiser is ADR-287 D6’s second stage — so the check finally had a const to hang on.

What crosses is text. asset("…") evaluates to the file’s contents and a comptime hands that to the program as the &str a const holds (ADR-079 D1) — so bytes that are not UTF-8 are refused by name rather than converted. Bytes that are not text have no crossed form yet, and that is ADR-079 D5’s blob rather than this record’s.

What this does not add is a sandbox, which is the part worth saying out loud. ADR-287’s two columns already bound what a build-time body may do; this bounds what it may read, and the two are separate questions with separate answers. ADR-026 stays open for the third: what bounds a build-time body’s memory.

This site is open source. Improve this page.