A small language that compiles to Rust and uses every core without being asked
No null. No mutation. No package manager. No unsafe. Errors the compiler will not let you ignore, and iteration that runs in parallel because you named what you meant.
// Nothing here is mutable, so nothing here can race. // map and filter are language operations, so they run on every core. evens:a:i = filter num in array_range(1, 1000000) { y num % 2 == 0; }; squares:a:i = map num in evens { y num * num; }; total:i = array_sum(squares);
This page is a Nail program — server, syntax highlighting and compiler output, all of it. Read its source →
Simplicity is not about doing less. It's about doing only what matters.
Languages compete on features. Every new abstraction is another way to do the same thing, and another thing to understand when it breaks. Nail removes instead of adding: one good way to solve a problem, not ten.
The billion dollar mistake. There is no null to return, so nothing can be missing.
Off-by-one, iterator invalidation, infinite loops. There are no counters to get wrong.
Data races and deadlocks. Nothing is mutable, so threads have nothing to fight over.
Every feature here exists to make a whole family of bugs impossible to write
Once a name has a value, it keeps that value. Nothing elsewhere can overwrite it, so there is never a hunt for what did.
No null, no undefined, no declared-but-unset. Every name has a real value from the moment it exists.
map to change every item, filter to keep some, reduce to fold them into one. No counters, so no off-by-one.
A c block waits on files and networks together; a p block spreads CPU work over every core. You never touch a thread.
Anything that can fail returns something you must deal with: safe() for a fallback, danger() to accept the crash. Silence isn't an option.
No unsafe to switch the rules off, and no package manager to pull in code from people you've never met. Your program is what you wrote.
Nothing here is a mockup. ▶ Run serves output this server really computed, the highlighting is Nail's own lexer, and every Rust panel is the compiler transpiling the example at boot.
// c.../c starts every statement at the same time and waits for all of them. // The compiler turns this block into Rust's tokio::join! — real async I/O. // Three real file reads happen concurrently: c spec:s = danger(fs_read(`nail_language_spec.md`)); readme:s = danger(fs_read(`README.md`)); website_source:s = danger(fs_read(`examples/nail_website.nail`)); /c // Past /c, every value is guaranteed loaded. // No callbacks, no .then() chains, no await keywords to forget. print(`Language spec chars:`, string_length(spec)); print(`README chars:`, string_length(readme)); print(`Website source chars:`, string_length(website_source)); print(`All three files loaded concurrently!`);
use tokio;
use nail::std_lib;
use nail::print_macro;
use std::boxed::Box;
use rayon::prelude::*;
use rayon::iter::IntoParallelIterator;
use futures::future;
#[tokio::main]
async fn main() {
let (spec, readme, website_source) = tokio::join!(
async { std_lib::fs::read_file("nail_language_spec.md".to_string()).await.unwrap_or_else(|nail_error| panic!("🔨 Nail Error: {}", nail_error)) },
async { std_lib::fs::read_file("README.md".to_string()).await.unwrap_or_else(|nail_error| panic!("🔨 Nail Error: {}", nail_error)) },
async { std_lib::fs::read_file("examples/nail_website.nail".to_string()).await.unwrap_or_else(|nail_error| panic!("🔨 Nail Error: {}", nail_error)) }
);
print_macro!("Language spec chars:".to_string(), std_lib::string::len(&spec));
print_macro!("README chars:".to_string(), std_lib::string::len(&readme));
print_macro!("Website source chars:".to_string(), std_lib::string::len(&website_source));
print_macro!("All three files loaded concurrently!".to_string());
}
How it works: everything inside c.../c starts at once and the block ends when all of it has finished. The compiler emits tokio::join!. Why it matters: in JavaScript that's a hand-built Promise.all, in Go goroutines plus a WaitGroup. Here it's where you put the lines — and past /c they are ordinary immutable values, with no await left to forget.
// p.../p runs each statement on its own OS thread (std::thread::spawn). // All threads are joined at /p — every value below the block is guaranteed ready. f factorial(num:i):i { if { num <= 1 => { r 1; }, else => { r num * factorial(num - 1); } } } f is_prime(num:i):b { if { num < 2 => { r false; }, else => { has_divisor:b = any div in array_range(2, num) { y num % div == 0; }; r !has_divisor; } } } f count_primes_below(limit:i):i { primes:a:i = filter num in array_range(2, limit) { y is_prime(num); }; r array_length(primes); } // Three CPU-heavy jobs run simultaneously on separate cores. // No locks, no mutexes: values are immutable, so threads cannot collide. p fact_12:i = factorial(12); sum_to_million:i = array_sum(array_range_inclusive(1, 1000000)); prime_count:i = count_primes_below(10000); /p print(`12! =`, fact_12); print(`Sum of 1 to 1,000,000 =`, sum_to_million); print(`Primes below 10,000 =`, prime_count);
use tokio;
use nail::std_lib;
use nail::print_macro;
use std::boxed::Box;
use rayon::prelude::*;
use rayon::iter::IntoParallelIterator;
use futures::future;
#[tokio::main]
async fn main() {
fn factorial(num: i64) -> i64 {
if num <= 1i64 {
return 1i64;
} else {
return num * factorial(num - 1i64);
}
}
fn is_prime(num: i64) -> bool {
if num < 2i64 {
return false;
} else {
let has_divisor: bool = {
let __iter = (2i64..num);
let num = num.clone();
let __search_result = __iter.into_par_iter().any(|div| {
let num = num.clone();
let condition_result = {
(num % div.clone()) == 0i64
};
condition_result
});
__search_result
};
return !has_divisor;
}
}
fn count_primes_below(limit: i64) -> i64 {
let primes: Vec<i64> = {
let __iter = (2i64..limit);
let __result: Vec<_> = __iter.into_par_iter().filter_map(|num| {
let condition_result = {
is_prime(num.clone())
};
if condition_result {
Some(num.clone())
} else {
None
}
}).collect();
__result
};
return std_lib::array::len(&primes);
}
let (fact_12, sum_to_million, prime_count) = {
let handle0 = std::thread::spawn({ let __rt_handle = tokio::runtime::Handle::current(); move || { __rt_handle.block_on(async move { factorial(12i64) }) } });
let handle1 = std::thread::spawn({ let __rt_handle = tokio::runtime::Handle::current(); move || { __rt_handle.block_on(async move { std_lib::array::sum(&std_lib::array::array_range_inclusive(1i64, 1000000i64)) }) } });
let handle2 = std::thread::spawn({ let __rt_handle = tokio::runtime::Handle::current(); move || { __rt_handle.block_on(async move { count_primes_below(10000i64) }) } });
(handle0.join().unwrap(), handle1.join().unwrap(), handle2.join().unwrap())
};
print_macro!("12! =".to_string(), fact_12);
print_macro!("Sum of 1 to 1,000,000 =".to_string(), sum_to_million);
print_macro!("Primes below 10,000 =".to_string(), prime_count);
}
How it works: each statement in p.../p gets its own OS thread, joined at /p, so every value below the block is ready. Why it matters: there are no locks because there is nothing mutable to lock — threads cannot write over each other. Rule of thumb: c blocks to wait on the outside world, p blocks to burn CPU.
// Nail forces you to handle errors - no silent failures! f divide(numerator:i, denominator:i):i!e { if { denominator == 0 => { r e(`Cannot divide by zero!`); }, else => { r numerator / denominator; } } } // Must explicitly handle the error case result:i = danger(divide(10, 2)); print(`10 / 2 = `); print(result); // Safe handling with fallback function f handle_div_error(err:e):i { print(`Error occurred: `); print(err); r 0; // Return default value } safe_result:i = safe(divide(10, 0), handle_div_error); print(`Result with error handling: `); print(safe_result);
use tokio;
use nail::std_lib;
use nail::print_macro;
use std::boxed::Box;
use rayon::prelude::*;
use rayon::iter::IntoParallelIterator;
use futures::future;
#[tokio::main]
async fn main() {
async fn divide(numerator: i64, denominator: i64) -> Result<i64, String> {
if denominator == 0i64 {
return Err(format!("divide: {}", "Cannot divide by zero!".to_string()));
} else {
return Ok(numerator / denominator);
}
}
let result: i64 = Box::pin(divide(10i64, 2i64)).await.unwrap_or_else(|nail_error| panic!("🔨 Nail Error: {}", nail_error));
print_macro!("10 / 2 = ".to_string());
print_macro!(result);
fn handle_div_error(err: String) -> i64 {
print_macro!("Error occurred: ".to_string());
print_macro!(err);
return 0i64;
}
let safe_result: i64 = match Box::pin(divide(10i64, 0i64)).await { Ok(v) => v, Err(e) => (handle_div_error.clone())(e) };
print_macro!("Result with error handling: ".to_string());
print_macro!(safe_result);
}
How it works: divide returns i!e — an integer or an error — and Nail will not compile code that ignores the error half. Handle it with safe() and a fallback, or accept the crash with danger(). Why it matters: no null, no unchecked exception, nothing swallowed in silence. Every error path is written down and the compiler checks that it is.
Linux, a Rust toolchain, one clone. The editor comes with the language.
git clone https://github.com/AlexTDWilkinson/Nail.git cd Nail # Opens the file in Nail's own IDE cargo run -- examples/hello_world.nail
No editor to configure, no extension to hunt down. Nail ships a terminal IDE that runs the real compiler against your file as you type, so type errors are underlined where they are, not guessed at by a plugin. F7 builds and runs what you're looking at, F1 shows documentation for the function under the cursor, Ctrl+S saves, Ctrl+F finds, F6 changes the theme.
Prefer your own editor? nailc is the compiler on its own: --transpile writes readable Rust beside your source, and --cargo-toml generates that program's Cargo.toml from the functions it actually calls, so nothing you didn't use is compiled in.
The language specification is the full reference, and this site's own source is the largest Nail program there is to read.
The optimizations that actually matter are the ones you never have to write
Most code is sequential because parallelism is painful: threads, locks, queues, races. Nail applies those optimizations for you. You write the obvious version; the compiler writes the fast one.
Uses every core, automatically:
Deliberately left alone: reduce and each. Your fold might depend on the order it runs in, and your side effects definitely do. Speeding those up by reordering them would be changing your answer, so Nail doesn't.
Write a loop and the compiler just sees a loop. It can't tell whether the order matters, so it plays safe and leaves it slow. Write sum and it knows exactly what you meant. That's the whole trade: every loop you replace with a named operation is one the compiler is free to speed up. Clearer code and faster code turn out to be the same edit.
Threads aren't free, so this only pays off above a certain size. Nail measured where that line is rather than guessing, and stays sequential below it:
| Elements | min / max | sum |
|---|---|---|
| 200,000 | 0.42x — slower | slower |
| 1,000,000 | 1.23x | 0.63x — slower |
| 4,000,000 | 2.64x | 1.98x |
24-core machine, i64 elements — the cheapest case, so the hardest to win. Anything costlier to compare crosses over sooner: minimum of a million strings is 2.69x at a fifth of that size. On one core Nail skips the parallel path entirely.
These are microbenchmarks of single operations, not whole programs — we haven't written the same non-trivial application in both languages, so we won't claim a number for one. Most software waits on I/O and gains nothing here. What we'll promise is the floor: when there's nothing to parallelize, Nail compiles to the sequential Rust you'd have written anyway.
| Workload | Hand-written sequential Rust | Nail |
|---|---|---|
| Primes below 50,000 by trial division (CPU-heavy filter) | 132 ms | 12 ms — 11x faster |
| Doubling 1,000,000 integers (trivial per-element work) | <1 ms | 1 ms — same |
24-core Linux machine, both --release. Unless you parallelize by hand, your code runs on one core; Nail uses the other 23 for free where the work justifies it.
Not built yet — this is what we're building next, and why
You have opened an old project and found it won't run any more. Nobody touched the code — the language moved on, or something it depended on vanished. That is normal, and it shouldn't be. The fix: the first line of every Nail file says which Nail it was written for.
nail 0.1
The toolchain reads that line, fetches that exact compiler if you don't have it, and runs it — not the newest one, the one the file asked for. Moving forward stays your choice: change the number whenever you like, and change it back if the new release dislikes something you wrote. Nothing drags you forward, and nothing rots if you stay.
Status, honestly: this is a plan, not a feature. Today there is one version of Nail, so there is nothing to pin yet — the version line is reserved in the specification so it will mean the right thing when it arrives. It lands in three steps: tagged releases first, then a warning when a file's version doesn't match the compiler you're running, then the automatic fetch-and-run described above.