A Nyx program is a file. There is no project to create, no build file to write and no runtime to ship: one command turns the file into a native binary and runs it.
hello.nx
Every program starts at fn main(). Put this in a file called hello.nx:
fn main() { print("Hello, world!") }
And run it:
$ nyx hello.nx Hello, world!
What just happened
The nyx command is a wrapper around the whole toolchain. Given a file, it does four things, in a temporary directory that it deletes on the way out:
- The compiler reads
hello.nxand emits LLVM IR. clangcompiles that IR together with the C runtime — the garbage collector, strings, arrays, maps, sockets, threads — into one executable.- The executable runs, with whatever arguments you passed after the file name.
- The temporary directory goes away.
Nothing is interpreted and nothing is left behind. The output is a real native binary; the only reason you do not see it is that this command throws it away. Step 03 shows how to keep it.
If the compilation fails, the whole build log is dumped to standard error — the compiler's diagnostics and, when NYX_DIAG=json is set, its machine-readable form. A failure is never a single mute line.
Check before you run
Compiling and linking takes seconds. Type-checking takes a fraction of that, and most mistakes are caught there:
$ nyx check hello.nx
nyx check parses and type-checks and stops — it does not link and it does not run. On success it prints a machine-readable index of what it found (DEF: for definitions, SYM: for symbols in scope) and exits 0; on failure it prints the diagnostic and exits 1. The exit code is the point: nyx check && nyx hello.nx is safe to chain.
Reading an error
Suppose greet.nx calls a function that does not exist:
fn greet(name: String) -> String { return "Hello, " + name + "!" } fn main() { print(greet("world")) print(greeting("Nyx")) }
$ nyx greet.nx nyx v0.31.0 lex 1538 tokens parse OK ✗ error [NYX1002] in 'main' (line 7): 'greeting' not declared (did you mean 'greet'?) check FAILED error: compilation failed for greet.nx
Three things to take from that line. The code — NYX1002 — is stable and searchable. The location is the enclosing function and the line inside the file. And the suggestion in parentheses is the compiler comparing your identifier against the ones it actually knows; when it offers one, it is usually right.
The phase names above the error say how far it got: lexing produced tokens, parsing succeeded, and the failure came from the checker. A parse error instead of a check error means the shape of the code is wrong, not the meaning.
Two more tools
nyx vet reads the file for things the compiler accepts but you probably did not mean: unused variables, dead code, unused imports. It also names the language's known traps instead of letting them turn into a confusing parse error later — each one as a warning with a code, the file, the line, and where the long explanation lives:
$ nyx vet shapes.nx warning[W101] shapes.nx:2: Enum variants use `.`, not `::`: `Shape.Circle(5)`, never `Shape::Circle(5)`. (docs/nyx/LLM.md §5 enum-dot-not-colons)
nyx fmt prints the file in canonical form on standard output. It does not edit anything, so nothing is lost if the result surprises you:
$ nyx fmt hello.nx fn main() { print("Hello, world!") }
Both default to src/main.nx when you give them no file — which is the entry point of a project, and that is the next step.