E0005 · E0201 · E0230
Errors are instructions
Every error has a stable code, an exact position and a hint that says what to write. --json is for agents, nyra explain E0201 is for the why.
MIT license · Nyra v0.5
Small and strict, with errors that tell an agent exactly what to write. One source file compiles to native code through C, or to JavaScript, Python, TypeScript, Rust and Go, and prints the same thing on every one.
first six Fibonacci numbers
fn fib(n: int) -> int {
if n < 2 { ret n }
ret fib(n - 1) + fib(n - 2)
}
fn main() {
for i in 0..6 {
print("fib({i}) = {fib(i)}")
}
}nativeC · gcc
$ nyra run fib.nyra
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5JavaScriptNode.js
$ nyra run fib.nyra --js
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5The compiler is Rust with no crates. It builds anywhere Rust does and writes plain, readable C and JavaScript.
Native code through C, and JavaScript for Node and the browser. Same output, same evaluation order, same runtime errors.
The whole language is a short spec an agent reads in one go. The compiler corrects what is left.
Design
Nyra is shaped by how language models write code, not by what humans like to read. Fewer choices, fewer wrong ones.
E0005 · E0201 · E0230
Every error has a stable code, an exact position and a hint that says what to write. --json is for agents, nyra explain E0201 is for the why.
one spelling
No implicit conversions, no shadowing, no null: 1 + 2.0 is error E0210. A model that half-knows Rust, Go or TypeScript already knows the syntax.
newline ends it
Braces are required and stay on the line that opens them. A stray ; is error E0005, and the hint says: delete it.
copy on write
Assigning, passing and returning always make independent values. Copies share storage until one side writes, so nothing changes behind an agent's back.
free · keep · arena
Reference counting frees memory when the last owner is gone: no garbage collector, no borrow checker. free, keep and arena say exactly when.
byte for byte
Both backends come from one intermediate representation, so evaluation order, number formatting and runtime errors match.
Errors are instructions
Every error carries a stable code, the exact line and column, and a hint that says what to write. The agent reads it as JSON, applies the fix and checks again. No guessing, no scraping text.
$ nyra check main.nyra --json {"ok":false,"errors":[{"code":"E0230","message":"cannot infer the type of the empty array `[]`","file":"main.nyra","line":2,"col":14,"hint":"write its type where it is declared, e.g. `var xs: [int] = []`"}]} $ nyra check main.nyra --json {"ok":true,"errors":[]} $ nyra run main.nyra [1]
Codes never change meaning. nyra explain E0230 prints what it means, why the rule exists, the usual causes, and a wrong and a fixed program.
File, line and column, the same on every backend. Runtime errors carry them too, with "runtime":true in the JSON.
Not "syntax error", but the line to type: a closest-name suggestion, the missing type, the keyword you meant.
Compiler
A small Rust compiler with zero dependencies. Both backends read the same typed intermediate representation, so a program prints the same thing whether it runs natively or on JavaScript. Scroll to fly through it.
Sourcesq.nyra
Plain text, written by an agent. Newlines end statements, so there are no semicolons.
fn sq(x: int) -> int = x * x
fn main() {
print(sq(5))
}
Tokenslexer
The lexer cuts the text into keywords, names, types and symbols.
Treeparser
The parser builds a syntax tree. It recovers after an error, so one mistake does not hide the next.
Typeschecker
The checker gives every node a type and collects every error in one pass.
IRnyra's real dump
One typed intermediate representation. Expressions are pure and every effect is its own statement, so the evaluation order is fixed once, for every backend.
fn sq(x: int) -> int {
return (x * x)
}
fn main() {
%0: int = call sq(5)
print(%0)
}
C99→ native executable
static int64_t ny_sq(int64_t x) {
return x * x;
}
gcc or clang turn it into a native binary.
JavaScript→ Node, browser
function sq(x) {
return x * x + 0;
}
The + 0 turns an int -0 into 0: C has no -0 for ints, so JavaScript may not print one either.
Outputrun it either way
native
$ nyra run sq.nyra
25JavaScript
$ nyra run sq.nyra --js
25identical output, byte for byte
The IR above is the compiler's real dump for this program. Every example in the repository runs on all six backends in CI and must match byte for byte.
Memory
Copy a value and you get an independent value. Under the hood the copy is free until one side writes: then, and only then, the memory splits.
Reference counting frees memory the moment the last owner is gone, with no garbage collector and no pauses. When you want to be exact, three words say so, and the compiler checks them.
free(x)Give the memory back now. Using x afterwards is a compile error (E0239), until you assign it a new value.
keep(x)Mark a value as living for the whole program: it is never freed early.
arena { }A block whose values are freed together at the closing brace. Values leave only through ret, and outer values are read-only inside. Version 0.3 checks these rules; a faster bulk allocator comes later.
Language · v0.5
Version 0.3 adds real data: strings and chars with methods, arrays, structs, inout parameters, break and continue, and explicit memory control. Every program below was compiled and run; the output is what the compiler printed, identical on native and JavaScript.
Functions with typed signatures, one-line functions, let and var, compound assignment, string interpolation and if as a value.
fn add(a: int, b: int) -> int = a + b
fn gcd(a: int, b: int) -> int {
if b == 0 { ret a }
ret gcd(b, a % b)
}
fn main() {
let x = 5
var total = 0
total += add(x, 2)
let size = if total > 6 { "big" } else { "small" }
print("total {total} is {size}")
print("gcd(48, 18) = {gcd(48, 18)}")
}total 7 is big gcd(48, 18) = 6
Text is UTF-8 and counted in characters. str has methods, char is one code point, and conversions are always explicit.
fn main() {
let s = " Hello, Nyra ".trim()
print(s.upper())
print(s.replace("Nyra", "agents"))
print(s.slice(0, 5))
print("a-b-c".split("-"))
let c = 'A'
print(c.code())
print(char(c.code() + 1))
}HELLO, NYRA Hello, agents Hello ["a", "b", "c"] 65 B
Arrays are typed, grow with push, and loop with for x in xs. They are values: copies never change each other.
fn main() {
var xs = [3, 1, 2]
xs.push(0)
xs.sort()
print(xs)
print(xs.slice(1, 3))
print(xs.contains(2))
var total = 0
for x in xs {
total += x
}
print(total)
}[0, 1, 2, 3] [1, 2] true 6
Structs have named fields and are built like a call: User(name: "ann", age: 30). They are values too.
struct User {
name: str
age: int
}
fn older(u: User) -> User = User(name: u.name, age: u.age + 1)
fn main() {
var ann = User(name: "ann", age: 30)
let next = older(ann)
ann.name = "Ann"
print(ann)
print(next)
}User(name: "Ann", age: 30) User(name: "ann", age: 31)
A function can change the caller's variable, and the call says so with inout. Two inout arguments are never the same variable.
fn swap(inout a: int, inout b: int) {
let t = a
a = b
b = t
}
fn main() {
var x = 1
var y = 2
swap(inout x, inout y)
print("{x} {y}")
}2 1
for, while, break and continue. The end of a range is exclusive.
fn main() {
var odd = ""
for i in 0..10 {
if i % 2 == 0 { continue }
odd += str(i)
}
print(odd)
var n = 0
while true {
if n * n > 50 { break }
n += 1
}
print(n)
}13579 8
Memory is automatic. free, keep and arena change only when it is returned, never what the program prints.
fn longest_word(text: str) -> str {
arena {
let words = text.split(" ")
var best = ""
for w in words {
if w.len() > best.len() { best = w }
}
ret best
}
}
fn main() {
var big = [0].repeat(100000)
print(big.len())
free(big)
big = [1, 2, 3]
let table = [1, 4, 9, 16]
keep(table)
print(longest_word("the quick brown fox"))
print(big)
print(table)
}100000 quick [1, 2, 3] [1, 4, 9, 16]
For agents
Nyra is brand new, so no model has seen it in training. It does not need to: the docs are short enough to read in one go, and the compiler corrects what is left.
Read https://github.com/SpAkXD/nyra, starting with llms.txt and docs/AI_GUIDE.md, then write me a Nyra program that prints the first 20 prime numbers.
.out file.Get started
One binary: nyra. Native programs need a C compiler (gcc, clang or tcc, found automatically). The JavaScript backend needs Node.js instead.
AInstall, option one
Linux x86_64, Windows x86_64 and macOS Apple Silicon. Download the archive from the latest release, unpack it and put nyra on your PATH.
On macOS run xattr -d com.apple.quarantine nyra once: the binary is not signed.
BInstall, option two
Any platform with Rust.
git clone https://github.com/SpAkXD/nyra cd nyra cargo build --release
The binary is target/release/nyra. Or: cargo install --git https://github.com/SpAkXD/nyra
CFirst run
fn main() {
print("hello from nyra")
}Save it as hello.nyra, then:
nyra run hello.nyra nyra run hello.nyra --js
nyra run hello.nyraCompile and run natively.nyra run hello.nyra --jsThe same program on Node.js.nyra build app.nyraA native executable next to the source. Unchanged programs skip the C compiler.nyra check app.nyra --jsonOnly report errors, as JSON for agents. Exit code 0 means no errors.nyra explain E0230What an error means, why, the usual causes, a wrong and a fixed program.flags
status
Benchmark · first results
83 tasks, four languages, three Claude models. Strong models write Nyra about as reliably as Python, small models are not there yet. Nyra programs run about 50× faster than Python; the code is about 1.25× as long. Every number is published, the bad ones too.
After up to 3 repairs with the compiler's feedback, Opus and Sonnet reach 83/83 in every language. Speed tasks: Nyra ~5 ms, Python ~300 ms. October 2026, Nyra v0.5.
Roadmap
Nyra is 0.x, so the syntax may still change before 1.0.
v0.1
Functions, int float bool str, if, while, for, the C and JavaScript backends, JSON errors.
v0.2
One-line functions, +=, string interpolation, if as a value, a build cache, runtime errors and a typed IR.
v0.3
Strings and chars, arrays, structs, inout, break and continue, reference counting with free, keep and arena.
v0.4
Scripts without fn main, nyra check --fix, an MCP server for agents, Python, TypeScript, Rust and Go backends, and a harder benchmark tier.
v0.5
Lambdas and comprehensions, ex examples checked while compiling, a standard library (input, files, JSON, math, time, random), maps, script variables in functions, native code near C speed, and the first benchmark results.
v0.6
Checked integer overflow, a WebAssembly backend and a browser playground, modules of your own and a C foreign function interface.
v1.0
Packages and addons, a published VS Code extension, a docs site.
Open source
Nyra is MIT licensed. Issues and pull requests are welcome, and cargo test must stay green. If an AI could not write a program you expected it to, open an issue with the program and the nyra check --json output: that is the most useful bug report.