Nail

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.

Get Started See Examples
Nail
// 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 β†’

Our Philosophy

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 best code is not the code that handles every edge case with clever abstractions. It's the code that doesn't have edge cases to begin with."

Three bugs Nail cannot express

NULL

The billion dollar mistake. There is no null to return, so nothing can be missing.

LOOPS

Off-by-one, iterator invalidation, infinite loops. There are no counters to get wrong.

RACES

Data races and deadlocks. Nothing is mutable, so threads have nothing to fight over.

Key Features

Every feature here exists to make a whole family of bugs impossible to write

πŸ”’

Nothing Changes Behind Your Back

Once a name has a value it keeps it. Nothing elsewhere can overwrite it, so there is never a hunt for what did.

πŸ‘»

Nothing Is Ever Empty

No null, no undefined, no declared-but-unset. Every name has a real value from the moment it exists.

πŸ”„

Say What You Want, Not How to Loop

map to change every item, filter to keep some, reduce to fold them into one. No counters, so no off-by-one.

⚑

Many Things at Once, Safely

A c block waits on files and networks together; a p block spreads CPU work over every core. You never touch a thread.

πŸ”€

Errors Can't Be Ignored

Anything that can fail returns something you must handle: safe() for a fallback, danger() to accept the crash.

🚫

No Escape Hatch, No Supply Chain

No unsafe to switch the rules off, no package manager to pull in code from strangers. Your program is what you wrote.

Code Examples

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.

Concurrent I/O Operations (c.../c)

Nail
// 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!`);
πŸ¦€ View the generated Rust (tokio::join! under the hood)
Generated Rust
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.

Parallel CPU Work (p.../p)

Nail
// 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);
πŸ¦€ View the generated Rust (thread spawn + join barrier)
Generated Rust
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.

Error Handling Done Right

Nail
// 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);
πŸ¦€ View the generated Rust (Result types, no exceptions)
Generated Rust
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.

Get Started

A Rust toolchain and one clone. The editor comes with the language.

Shell
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.

The Editor Is Part of the Language

Real frames captured from a terminal β€” the compiler underneath them is the same one that builds your program.

Errors appear where they are, while you type

β”Œ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.

Completions come from the standard library, with signatures

β”Œ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.

F1 documents whatever is under the cursor

β”Œ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.

Performance, Honestly

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:

  • map and filter β€” spread across all cores
  • find, all, any β€” every core searches, everyone stops at the answer
  • sum, min, max β€” split across cores once the array is big enough to be worth it
  • p blocks β€” one real thread per statement, for CPU work
  • c blocks β€” three file reads take as long as the slowest one, instead of all three added together
  • reduce β€” but only when the compiler can prove it is safe, below

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.

Why naming the operation is what makes it fast

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.

Planned: Programs That Don't Rot

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
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.

Made By

Alex Wilkinson

Alex Wilkinson

Most bugs I have shipped were simple problems buried under features I did not need: a null, a variable something else changed, an off-by-one, two threads on the same value. Nail drops those features instead of giving me better tools for surviving them.

This page is a Nail program, compiled by that compiler. I also made Simple Universal Language, a spoken and written language built to still reach people who cannot see or hear it.