sync and Touches at Most the Build’s ParametersStatus: Superseded by ADR-287 — its D1-D6 are D13-D18 there
Date: September 14, 2026
Target Version: Nikaia 0.0.8
Answers: ADR-026 Q4, and with it Q1, Q2 and Q3. That record stays
Open for one thing only: what bounds a build-time body’s memory
Related: ADR-067 D1 (sync means one thing: it never pauses)
and D2 (touches is inferred over the call graph), ADR-073 D5
(which staged this and did not answer it), ADR-072 (where the bytes
come from), ADR-005 D8 (byte-determinism, enforced by CI
double-builds), ADR-021 D5 (what reaches the cache key)
ADR-026 Q4 has been the question everything else in that record was
downstream of: what is a program allowed to do in const? It asked for pure
functions or anything, named allocation as a requirement rather than an option,
and asked for a step budget and for nothing that observes time, environment or
randomness.
Two of those are no longer questions, because they became columns. Since
ADR-067, sync means exactly one thing — it never pauses — and
touches is inferred over the call graph for every function a .nika file
declares. The restriction Q4 asked somebody to invent already exists twice over
and is maintained by the compiler rather than by a list.
What the ledger says when you count it. Of std’s 74 sync entries, ten
touch something:
cli::args, Args::nth "args read"
print, println "stdout write"
eprint, eprintln "stderr write"
Locked::get/set "lock read" / "lock write"
SharedMut::get/set "lock read" / "lock write"
So “anything sync” alone would let a build-time body read the environment it
was invoked in and write to the compiler’s own output. That is what makes the
second column necessary, and it is also what makes the exact boundary a decision
rather than a derivation.
sync.The first half of the rule, and it costs nothing to enforce: the column is
inferred already, for std and for the program alike.
What sync buys here is narrower than it sounds and is worth stating, because
ADR-067 D1 deliberately made the word mean one thing: a build-time
body may not suspend. It may still block a thread — the panic hook is the
proof, it must be sync and may block. At build time that is slow rather than
wrong, which is why the word is enough for this purpose and would not be for
another.
args read.Not touches = []. cli::args passes, deliberately: same parameters, same
code. A build’s own parameters are an input like its source is, and a build-time
body that shapes a table differently for two declared parameters is doing the
thing const is for.
Everything else in the list above is refused: a build-time body does not write to
the compiler’s output and does not take locks. That refusal is a consequence of
the boundary rather than a judgement about println — a build-time print is a
reasonable thing to want, build.rs has one and Zig has @compileLog, and it is
a later decision with its own reasons rather than something this record settled
against.
ADR-005 D8’s byte-determinism is a hard guarantee, enforced by CI double-builds, and D2 changes what it is a guarantee about:
same source and same toolchain and same parameters produce the same bytes.
That is not a weakening as long as the parameters are carried. They are not
today: Choices holds exactly two fields, the build shape and the backend, and
its own doc comment says what to do about that — “Adding a field here means
adding a line to Key::build, which is the point of keeping them in a struct of
their own”. So this costs one field and one line, and skipping it would make the
cache serve a build made with other parameters, which is the failure mode
ADR-021 exists to prevent.
And cli::args means the same thing in both modes, which is why it is not
two functions: the arguments of whoever is running me. At program time that is
the program; at build time it is the build.
Q4 asked for one and this record does not add one. The cost is not hypothetical
and ADR-026 Q6 already wrote it down: “a build that hangs with no
diagnostic is worse for the local developer than one that stops and names the
call path”. A const that does not terminate hangs the compiler silently, and
sync does not rule that out — it says a body never pauses, not that it ends.
Deferred rather than decided against, and the direction is safe: a budget added later refuses only programs that were already hanging. Nothing built now has to anticipate it.
Neither column says anything about allocation, and Q4 named allocation as a requirement rather than an option — a build-time body that builds a table must allocate. So there is no bound on how much, and ADR-026 stays Open for exactly this.
It is the mildest of the three worries it arrived with: a build that exhausts memory fails loudly, where one that reads a file quietly does not and one that hangs says nothing at all.
ADR-026 Q2 asked restrict-and-interpret, or permit-and-sandbox. D1 and D2 are the restriction, so: restrict and interpret. Q3 asked what to sandbox in, and its own table says AST interpretation is the smallest and is viable only if Q2 restricts — which it now does. Q1, about a grammar being a closed world making a sandbox affordable, is moot when there is no sandbox.
What goes with the sandbox is worth naming, because each was a real cost: no
WebAssembly runtime in the dependency tree, no wasm32 target required of
every contributor and every CI job, and no asterisk on determinism for NaN bit
patterns, which Q3 flagged as able to make two builds both correct and different.
Twenty-four of std’s 74 sync entries have no touch answer at all, and the
polarity is fail-closed, so each of them reads “touches everything”. Among
them:
HashMap::new HashMap::get HashMap::keys HashMap::len
HashMap::entry f64::sqrt f64::abs fs::Mapped::deref
Under D2 a build-time body could not use a map — which is the thing this feature
is for. That is not a design problem: it is twenty-four missing lines in the
shipped ledger. ADR-067 D2’s inference does not reach them, because
it walks Nikaia bodies and these entries are backed by Rust, so they are written
by hand. HashMap::get touches nothing, and saying so is one line.
It is on [#205](https://github.com/Nikaia-Language/Nikaia/issues/205) rather than here.
docs/open-decisions.md loses its oldest entry, and what is left of
ADR-026 is one bound rather than a design space.
The two columns get a third consumer, after the ordering pass and the
lock-inside-a-lock refusal, and it is the first that uses both at once: sync
says a body does not wait, touches says it does not reach.
The file half and the evaluation half now meet. ADR-072 says where the bytes come from, this says what may be done with them, and ADR-073 says where the declaration stands. What is missing is the declaration itself.
D1 and D2, and they are what let ADR-073 D5’s second stage be
written at all: comptime N = double(21) evaluates, because the interpreter
asks the two columns this record names and nothing else.
The rule is read off the ledger and not off a list, which is the sentence
that decided the shape of the work. sync says the body never pauses and the
touch set says it reaches nothing but the build’s own parameters; both are
derived for every function the compiler sees, so crates/nikaia/src/build_time.rs
decides nothing about safety — it reads what was decided here. A callee this
unit does not declare is neither allowed nor forbidden but unevaluable:
there is no body to run, and saying you may not about a function whose body is
somewhere else is a claim this cannot make.
NK1152, and it is deliberately not NK1127. That one says this compiler
cannot evaluate it — the staging talking — and this one says the rule says
no. A reader does two different things about them: wait for a stage, or change
the callee.
And D4’s cost needed one neighbour. That decision has no step budget and writes down what it costs: a body that does not terminate hangs the build. A recursion that does not terminate is a different failure — it overflows this compiler’s own stack, and a compiler that falls over is not the hang D4 accepted. So the call depth is bounded, at a limit no terminating program meets, and the message says that is what it is. The work is still unbounded.
What is left of this record is D2’s second half in the other direction: a build-time print. §3 calls it a reasonable thing to want and a later decision with its own reasons; today it is refused as a consequence of the boundary, which is what that paragraph said it would be.
The order for the rest is unchanged: ADR-072’s allowlist and
dsl … from "…" with it — plus the twenty-four ledger lines of §3, which are
needed before the first useful const and are not needed before the first one.