ADR-260: A File Belongs to Its Project, and the Single-File Commands Are Verbs

Status: Accepted Date: September 30, 2026 Answers: #93, three corpus files cannot be compiled with --input, and none of them is broken — two questions: what a single-file command means for a file inside a project, and what it is called Related: ADR-002 D1 (a project is a nikaia.toml), ADR-021 (a unit’s cache key holds every switch that reached it), ADR-286 (a Nikaia package dependency), ADR-269 and ADR-247 (nikaia test, which is what checks the examples), ADR-258 (the four weights this is weighed by), Part III 13.2

1. Context

The single-file command was nikaia --input f.nika, the compiler as it was before there were projects. It lowered one file to Rust and wrote f.rs and a ledger beside it without calling rustc; with --backend interpreter it ran the file instead, and with --explain it mapped rustc’s JSON on stdin back onto the .nika source. Three things were wrong with it.

It was neither inside a project nor outside one. It walked up to the nearest nikaia.toml and took its [build] settings - one file must not be cached under one project’s switches and compiled under another’s - but handed the lowering an empty package list. So use http in examples/hello-http/src/main.nika, with http declared in the manifest two directories up, was refused with “there’s no dependency called http. Add it to the project’s [dependencies]” - a refusal that is wrong and tells the user to do what they did.

The name said nothing. --input names an argument, not what happens to it, and the other commands are verbs (build, run, test, describe, lower-std).

Three different jobs shared one switch set. Lower this file, interpret this file and explain rustc’s errors against this file are separate questions; --backend, --output and --explain existed only because they were all --input.

2. Decisions

D1 — A file inside a project is a file of that project.

Wherever a command takes a .nika file, the nearest nikaia.toml above it, if there is one, decides everything: the [build] settings and the declared dependencies, which are read and handed to the lowering exactly as nikaia build hands them. The cache key holds the settings and the dependencies, because what a file is checked against is one of the things that decides the answer.

Weighed by ADR-258: D4 (consistent with itself) — a file is in a project or it is not, and the half-way reading is what produced the wrong refusal; D2 — the compiler finds the project, the developer does not name it twice.

D2 — A file outside any project is standalone, and says so.

With no nikaia.toml above it the file has the default settings and the command-line switches, and no dependencies. use http in such a file is refused with a message that says a single file outside a project has no dependencies and names nikaia.toml as where one is declared — not that one is missing from a manifest that does not exist.

D3 — The single-file commands are verbs.

Was Is What it does
nikaia --input f.nika nikaia lower f.nika writes f.rs and the ledger beside it; does not call rustc. The name follows lower-std
nikaia --input f.nika --backend interpreter nikaia interpret f.nika runs the file in the interpreter
nikaia --input f.nika --explain nikaia explain f.nika reads rustc --error-format=json on stdin and reports it against the .nika source

--output belongs to lower alone. --backend leaves the single-file path: the backends that are not rust and interpreter are refused by name already (ADR-021 D9), and a verb per job says which one was asked. The explaining switches (--trust, --overlaps, --sharing, --tethers, --comptime, --asserts) and --locked, --no-cache, --target, --user-parallelism stay where they are, on build, run, test and lower.

D4 — nikaia run f.nika compiles and runs one file.

With a file argument and no nikaia.toml above it, run builds a project it keeps for itself — a scratch directory under the user’s cache directory, keyed on the file’s path — through the same Cargo workspace nikaia build makes, and starts the program, so a single file behaves as a program and not as a lowering. With a nikaia.toml above it, run f.nika builds that project and runs the binary whose entry is f.nika, or is refused with the project’s binaries named. run without a file is unchanged. The interpreter is not what run uses: it handles a part of the language and would make a file mean something at run that it does not mean when built (Part I 1.2’s promise is that a program means one thing).

Weighed by ADR-258: D1 — the cost of the special case (one file with no project) is carried by the file, which needs no manifest and no mkdir; the common case, a project, is unchanged.

D5 — --input is removed, not kept as an alias.

The language is pre-alpha and its status page says syntax and diagnostics change between releases with no migration. A deprecated second name would be one more thing to remove.

D6 — Everything after the file belongs to the program.

nikaia run f.nika a -v b runs f.nika with a -v b, as go run does; a switch of nikaia run stands before the file, and -- still works. This is what lets the kernel start a script: it calls nikaia run ./f.nika a b with no --.

D7 — A #! line opens an entry point and is skipped.

A file whose first two bytes are #! has its first line skipped, and the line still counts, so a diagnostic names the line it names in the editor. Only the file that holds main may carry one; anywhere else a # is an error, as it was. With #!/usr/bin/env -S nikaia run and the execute bit, ./f.nika is a command. A file is still a whole program, so there is no REPL in this.

D8 — nikaia new name writes a project that builds, runs and tests.

A new directory with nikaia.toml holding name and version and nothing else, so every default applies without being copied; src/main.nika printing Hello, Nikaia!; tests/main.stdout holding that line, so nikaia test checks something at once; and a .gitignore naming target/. A directory that exists and is not empty is refused.

D6-D8 decided October 9, 2026 (#537).

3. Consequences

4. Considered and rejected

Keep --input and fix only the message. It would repair one wrong sentence and keep a name nobody can guess and three jobs in one switch set.

A single file is always standalone, even inside a project (no manifest search). It is consistent, but it makes nikaia lower over a project’s file disagree with nikaia build over the same file — the situation D1 removes — and it would need its own answer to the cache-key argument above.

nikaia run f.nika uses the interpreter. Cheaper to build and it skips rustc, but the interpreter runs part of the language. A program that means one thing under run and another under build is what the two settings of user_parallelism are held to never do.

This site is open source. Improve this page.