A small language that compiles to Rust and uses every core without being asked
No null. No mutation. No package manager. No unsafe. Errors you can't 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.
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 it. 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 handle: safe() for a fallback, danger() to accept the crash.
No unsafe to switch the rules off, no package manager to pull in code from strangers. 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's output.
// 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());
}
Everything inside c.../c starts at once and the block ends when all of it finishes; the compiler emits tokio::join!. That's a hand-built Promise.all in JavaScript, or goroutines plus a WaitGroup in Go. Past /c the values are ordinary and immutable β 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) == 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)
};
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);
}
Each statement in p.../p gets its own OS thread, joined at /p, so every value below the block is ready. No locks, because there is nothing mutable to lock. 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);
}
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(). Every error path is written down, and the compiler checks that it is.
A Rust toolchain and 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
There is no editor to choose. Nail ships one, and it runs the real compiler over your file as you type, so an error is underlined where it actually is instead of being guessed at by a plugin. F7 compiles what you are looking at into a release binary, F1 documents the function under the cursor, Ctrl+S saves, Ctrl+F finds, F6 changes the theme.
That standardisation is the feature. Nothing to install, no language server to keep in sync, no formatter to argue about, no plugin that quietly disagrees with the compiler β and the same compiler, as nailc, is what your build scripts call.
Linux is the supported platform. Programs you build are ordinary Rust, so they compile for anything Rust targets.
The language specification is the full reference, and this site's own source is the largest Nail program there is to read.
Real frames captured from a terminal β the compiler underneath them is the same one that builds your program.
βFILES [*] - Press Ctrl+S to saveββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββReady β Untitled 1 β hello_world.nail* β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ βNAIL - examples/hello_world.nailββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 1 β// Hello World - Your first Nail program ββ 2 β ββ 3 βgreeting:s = `Hello, World!`; ββ 4 βprint(greeting); ββ 5 βcount:i = 5.5 ββ 6 β ββ 7 β// Demonstrate collection operations with greetings ββ 8 βgreetings:a:s = [`Hello`, `Hola`, `Bonjour`, `Guten Tag`, `Ciao`]; β Expected ';' here, but found the name 'grβ¦ββ 9 βlanguages:a:s = [`English`, `Spanish`, `French`, `German`, `Italian`]; ββ 10 β ββ 11 β// Create formatted greetings using map ββ 12 βformatted_greetings:a:s = map greeting idx in greetings { ββ 13 β language:s = danger(array_get(languages, idx)); ββ 14 β y array_join([greeting, ` (`, language, `)`], ``); ββ 15 β}; ββ 16 β ββ 17 β// Print each greeting ββ 18 βprint(`\n=== International Greetings ===`); ββ 19 βeach formatted_greeting in formatted_greetings { ββ 20 β print(formatted_greeting); ββ 21 β} ββ 22 β ββ 23 β// Find greetings with specific letters ββ 24 βgreetings_with_o:a:s = filter greeting in greetings { ββ 25 β y string_contains(greeting, `o`); ββ 26 β}; ββ 27 βhas_long_greeting:b = any greeting in greetings { ββ 28 β y string_length(greeting) > 6; ββ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ examples/hello_world.nail [*] 5:14 37 lines 1167 bytes Tab 2/2 [LN,HL,BR] Ctrl+L: Line# | Ctrl+Shift+H: Highli
The editor runs the real lexer, parser and type checker over the buffer as you edit, and marks the problem inline on the line that caused it. There is only one implementation of the language, and this is it.
βFILES [*] - Press Ctrl+S to saveββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββReady β Untitled 1 β hello_world.nail* β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ βNAIL - examples/hello_world.nailββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 1 β// Hello World - Your first Nail program ββ 2 β ββ 3 βgreeting:s = `Hello, World!`; ββ 4 βprint(greeting); ββ 5 βcount:i = 5.5 ββ 6 βtotal:i = string_le β Expected ';' here, but found the name 'total' ββ 7 β ββ 8 β// Demonstβ Completions (F1 for docs) βββββββββββββββββββββ ββ 9 βgreetings:βΖ string_length string_length(input:s) -> i βCiao`]; ββ 10 βlanguages:βββββββββββββββββββββββββββββββββββββββββββββββββ`Italian`]; ββ 11 β ββ 12 β// Create formatted greetings using map ββ 13 βformatted_greetings:a:s = map greeting idx in greetings { ββ 14 β language:s = danger(array_get(languages, idx)); ββ 15 β y array_join([greeting, ` (`, language, `)`], ``); ββ 16 β}; ββ 17 β ββ 18 β// Print each greeting ββ 19 βprint(`\n=== International Greetings ===`); ββ 20 βeach formatted_greeting in formatted_greetings { ββ 21 β print(formatted_greeting); ββ 22 β} ββ 23 β ββ 24 β// Find greetings with specific letters ββ 25 βgreetings_with_o:a:s = filter greeting in greetings { ββ 26 β y string_contains(greeting, `o`); ββ 27 β}; ββ 28 βhas_long_greeting:b = any greeting in greetings { ββ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ examples/hello_world.nail [*] 6:20 38 lines 1187 bytes Tab 2/2 [LN,HL,BR] Ctrl+L: Line# | Ctrl+Shift+H: Highli
Every function in Nail ships with the language, so the editor can offer all of them with full types. There is no index to build and nothing to keep in sync.
βFILES [*] - Press Ctrl+S to saveββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββReady β Untitled 1 β hello_world.nail* β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ βNAIL - examples/hello_world.nailββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 1 β// Hello World - Your first Nail program ββ 2 β ββ 3 βgreeting:s = `Hello, World!`; ββ 4 βprint(greeting); ββ 5 βcount:i = 5.5 ββ 6 βtotal:i = string_le β Expected ';' here, but found the name 'total' ββ 7 β ββ 8 β// Demonstrate collection operaβ Documentation (F1 to toggle) βββββββββββββββββββ ββ 9 βgreetings:a:s = [`Hello`, `HolaβFunction: string_length β ββ 10 βlanguages:a:s = [`English`, `Spβ β ββ 11 β βSignature: string_length(input:s) -> i β ββ 12 β// Create formatted greetings uβ β ββ 13 βformatted_greetings:a:s = map gβDescription: β ββ 14 β language:s = danger(array_gβReturns the number of characters in the string. β ββ 15 β y array_join([greeting, ` (β β ββ 16 β}; βExample: β ββ 17 β βlength:i = string_length(`hello`); β ββ 18 β// Print each greeting β β ββ 19 βprint(`\n=== International Greeβ β ββ 20 βeach formatted_greeting in formβPress ESC to go back, TAB to insert β ββ 21 β print(formatted_greeting); ββββββββββββββββββββββββββββββββββββββββββββββββββ ββ 22 β} ββ 23 β ββ 24 β// Find greetings with specific letters ββ 25 βgreetings_with_o:a:s = filter greeting in greetings { ββ 26 β y string_contains(greeting, `o`); ββ 27 β}; ββ 28 βhas_long_greeting:b = any greeting in greetings { ββ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ examples/hello_world.nail [*] 6:20 38 lines 1187 bytes Tab 2/2 [LN,HL,BR] Ctrl+L: Line# | Ctrl+Shift+H: Highli
Documentation lives in the same registry the compiler type checks against, so it cannot drift out of date.
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. You write the obvious version; the compiler writes the fast one.
Uses every core, automatically:
Never: each, which exists to run side effects, and those happen in the order you wrote them; and scan, where every value depends on the ones before it.
A loop is opaque: the compiler can't tell whether order matters, so it stays sequential. sum says what you meant, so it can use every core. Clearer code and faster code are the same edit.
reduce is the interesting case, because you write the step yourself. Splitting a fold regroups it β (a+b)+c becomes a+(b+c) β fine for addition, wrong for subtraction. So Nail reads your step. Add or multiply the running total by a whole number, or keep the larger or smaller of the two, and regrouping provably cannot change the answer: it runs on every core. acc + num * num and acc + string_length(word) count too, and computing each contribution is divided up as well. Subtraction, division and floats stay in order.
There is no flag to override this and nothing for you to promise. A wrong promise buys the worst kind of failure: a number quietly wrong, and differently wrong each run.
Threads aren't free, so this only pays above a size Nail measured rather than guessed:
| 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, so we won't claim a number for an application. Most software waits on I/O and gains nothing here. The floor is the promise: with 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 open an old project and it won't run any more. Nobody touched the code β the language moved on, or something it depended on vanished. The fix: every Nail file records the compiler it was written for. You never type it; the IDE stamps it on save.
nail 0.1
Open or save, and Nail reads that stamp, fetches that version if you don't have it, and edits and builds the file with it β not the newest one, the one the file asked for. What arrives is that version of Nail itself, editor included, since the errors you see while typing must come from the compiler that builds the program; the Rust toolchain underneath is shared, so switching is usually a small download. The stamp is plain text inside the file because source gets zipped, emailed, pasted and committed, and a version kept anywhere else is lost on the way.
Upgrading is the same gesture in reverse: pick a different Nail, save, and the file reopens on it. If the newer release dislikes something you wrote, set it back and save. Nothing drags you forward, and nothing rots if you stay.
Status: a plan, not a feature. There is one version of Nail today, so there is nothing to pin yet β the version line is reserved in the specification. It lands in three steps: tagged releases, then a warning when a file's version doesn't match the compiler you're running, then the automatic fetch-and-run above.