Nail

Rust's speed, Python's ease, zero dependencies

A small language with a huge standard library. No null, no mutation, no package manager, no unsafe. Errors you can't ignore. Iteration that runs in parallel when it is safe to.

One command turns your file into a Rust program, so it is as fast and as memory safe as Rust whether or not you know Rust. Every file pins the compiler that wrote it, so what compiled once compiles forever.

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.
// y yields one value per element, a:i is an array of integers.
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, highlighting, compiler output, all of it. Read its source β†’

The whole language on one screen

If it is not on this screen, Nail does not have it

whole_language.nail
nail latest
// Every construct Nail has, on one screen. If it is not here, the language
// does not have it.
//
// The short tokens, once: f declares a function, r returns, y yields one
// element to a collection operation, p opens a parallel block (real threads),
// c opens a concurrent block (overlapped waiting, one thread). The type
// letters: i integer, f float, s string, b boolean, a array, h hashmap,
// e error, v void.

struct Point {
    x_pos:i,
    y_pos:i
}

enum Direction {
    North,
    South
}

f add_coordinates(point:Point):i {
    r point.x_pos + point.y_pos;
}

f half_of(num:i):i!e {
    if {
        num % 2 == 1 -> { r e(`odd numbers do not halve cleanly`); },
        else -> { r num / 2; }
    }
}

f return_zero_if_error(err:e):i {
    r 0;
}

f announce(message:s):v {
    print(message);
}

f first_square_past(limit:i, candidate:i):i {
    if {
        candidate * candidate > limit -> { r candidate; },
        else -> { r first_square_past(limit, candidate + 1); }
    }
}

// Everything is immutable, and every declaration carries its type
coordinate_total:i = add_coordinates(Point { x_pos = 3, y_pos = 4 });
ratio:f = 2.5;
markup:s = html`<b>a tagged string tells highlighters its language</b>`;
is_ready:b = true && !false;
heading:Direction = Direction::North;

// The three ways out of an error type: handle it, crash on it, or insist
half:i = safe(half_of(coordinate_total), return_zero_if_error);
crash_if_odd:i = danger(half_of(2));
promised_even:i = expect(half_of(4));

// if is an expression too
heading_name:s = if {
    heading == Direction::North -> { r `north`; },
    else -> { r `south`; }
};

// Arrays and hashmaps are the collections
numbers:a:i = [1, 2, 3, 4, 5];
ages:h<s,i> = hashmap_new();
hashmap_set(ages, `grug`, 30);
grug_age:i = danger(hashmap_get(ages, `grug`));

// map, filter and reduce are the loops, and they run on every core
squares:a:i = map num in numbers { y num * num; };
evens:a:i = filter num in squares { y num % 2 == 0; };
total:i = reduce acc num in evens from 0 { y acc + num; };

// scan keeps every step, find, all and any answer questions, each is for
// side effects, and any of them can also take an index iterator
running:a:i = scan acc num in numbers from 0 { y acc + num; };
first_even:i = danger(find num index in numbers { y num % 2 == 0; });
all_positive:b = all num in numbers { y num > 0; };
any_negative:b = any num in numbers { y num < 0; };
each num in numbers { print(num); }

// a search that stops is a function that calls itself until it has the answer
print(first_square_past(50, 0));

// forever runs until the program ends: servers, watchers, heartbeats.
// Nothing runs behind the program's back, so a forever function runs in a
// c block beside whatever else lives as long as the program. This one is
// declared and not called, so the tour itself can end
f heartbeat():v {
    forever {
        announce(`still here`);
        time_sleep(60.0);
    }
}

// p runs statements on real threads at once, c overlaps their waiting
p
    left:i = array_sum(array_range_inclusive(1, 1000));
    right:i = array_sum(array_range_inclusive(1, 2000));
/p
c
    doubled:i = half * 2;
    tripled:i = half * 3;
/c

// import splices in another file, sandboxed so it can only compute, and
// import_dangerous splices one in with the sandbox off. Each needs a second
// file, so they are the one pair not shown running on this screen.

print(total + left + right + grug_age + first_even + doubled + tripled);

It compiles. This server checked it with its own compiler at startup, where it became 175 lines of Rust.

Bugs do not compile

Three everyday mistakes, refused by this server's own compiler at startup. The panes are editable: fix one and the same compiler, running in your browser, changes its verdict

Ignoring a failure
nail latest
// Reading a file can fail, and this code pretends it cannot.
// Nail should FAIL type checking here, which is the point of the page.
content:s = fs_read(`config.txt`);
print(content);
The compiler's answer
error: 'content' is declared as a string (s) but fs_read returns a result (s!e) that may contain an error
Forgetting a field
nail latest
// A Point needs both coordinates, and this literal hopes nobody notices.
// Nail should FAIL type checking here, which is the point of the page.
struct Point {
    x_pos:i,
    y_pos:i
}

somewhere:Point = Point { x_pos = 3 };
print(somewhere.x_pos);
The compiler's answer
error: Missing field 'y_pos' in instantiation of struct 'Point'
Keeping a nothing
nail latest
// announce returns nothing, and this code tries to keep the nothing.
// Nail should FAIL type checking here, which is the point of the page.
f announce():v {
    print(`hello`);
}

kept:s = announce();
print(kept);
The compiler's answer
error: 'kept' is declared as a string (s) but announce returns void (v)

Key features

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

πŸ”’

Nothing changes behind your back

A name keeps its value. 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 holds a real value from the moment it exists.

πŸ”„

Say what you want, not how to loop

map changes every item, filter keeps some, reduce folds 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 pulling in dependency trees. Your program is what you wrote.

πŸ›‘οΈ

Foreign code runs sandboxed

import sandboxes downloaded code at compile time: no network, no disk, no way to phone home.

πŸ“¦

Batteries included

A thousand functions built in: servers, databases, stats, money, geo. Nothing to install.

πŸ—£οΈ

Errors that explain themselves

Every message says what broke, where, why, and how to fix it. The wording is pinned by tests.

🧳

One file becomes one binary

Your program becomes Rust, then one static executable. Deploying is copying it.

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 what the transpiler emitted.

Two blocks run statements at the same time, against different bottlenecks:

Overlapping waits is nearly free. Multiplying work takes more hardware.

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

Nail
nail latest
// 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!, async I/O on one
// thread. Concurrency overlaps waiting: three reads cost one wait. It cannot
// speed up computation, that is what p.../p and its extra cores are for.

// If a read fails, safe() hands the error here instead of crashing.
// There is deliberately no safe(expr, `default`) shortcut: a bare default
// would hide the error, and the moment you need to see what went wrong you
// would be rewriting it as a function anyway. So it starts as one - and it
// shows what went wrong before it substitutes.
f read_fallback(err:e):s {
    print(`read failed:`, err);
    r `(file unavailable)`;
}

// Three real file reads happen concurrently, one per way of handling failure:
c
    // danger(): crash on failure. For when the program is pointless without the value.
    spec:s = danger(fs_read(`nail_language_spec.md`));
    // expect(): also crashes, but tells the reader "I checked, this can't fail".
    readme:s = expect(fs_read(`README.md`));
    // safe(): pass the error to a fallback function and carry on.
    website_source:s = safe(fs_read(`examples/website/main.nail`), read_fallback);
/c

// Past /c, every value is guaranteed loaded. Those three are the only ways to
// use a value that might fail: there is no ignoring the error and hoping.
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;

fn main() {
    let __threads = nail::threads::configure();
    tokio::runtime::Builder::new_multi_thread()
        .worker_threads(__threads)
        .enable_all()
        .build()
        .expect("the async runtime could not start")
        .block_on(nail_main());
}

async fn nail_main() {
    fn read_fallback(err: String) -> String {
        print_macro!("read failed:".to_string(), err);
        return "(file unavailable)".to_string();
    }
    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 { (match (std_lib::fs::read_file("examples/website/main.nail".to_string()).await) { Ok(v) => v, Err(e) => (read_fallback.clone())(e) }) }
    );
    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, so three waits cost one. The compiler emits tokio::join!, the same idea as Promise.all in JavaScript or goroutines plus a WaitGroup in Go. It cannot make computation faster: one thread is still doing the computing. Why not p here? An OS thread costs megabytes of stack to sit asleep on a disk read, while a c task waits for close to free, so c scales to thousands of simultaneous waits. Past /c the values are ordinary and immutable, with no await left to forget.

Parallel CPU work (p.../p)

Nail
nail latest
// p.../p runs each statement on its own OS thread (std::thread::spawn).
// All threads are joined at /p, so every value below the block is ready.
// Parallelism multiplies working: three cores compute in the same instant.
// c.../c cannot do that, it overlaps waiting on a single thread.

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;

fn main() {
    let __threads = nail::threads::configure();
    tokio::runtime::Builder::new_multi_thread()
        .worker_threads(__threads)
        .enable_all()
        .build()
        .expect("the async runtime could not start")
        .block_on(nail_main());
}

async fn nail_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 thread on its own core, joined at /p, so every value below the block is ready. Three computations happen in the same instant, so the block costs its slowest statement instead of the sum. Threads only pay off because these jobs burn CPU instead of waiting. No locks, because there is nothing mutable to lock. Rule of thumb: c when the bottleneck is waiting, p when it is computing.

Error handling done right

Nail
nail latest
// 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. The handler always receives the
// error, so logging it is one line β€” a bare default value never could.
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;

fn main() {
    let __threads = nail::threads::configure();
    tokio::runtime::Builder::new_multi_thread()
        .worker_threads(__threads)
        .enable_all()
        .build()
        .expect("the async runtime could not start")
        .block_on(nail_main());
}

async fn nail_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 it.

Games, compiled to your browser

Every frame below is a Nail program, built with nailc --target=wasm. The same source files open native windows on a desktop. Click a game to give it your keyboard.

Platformer

Click it, then arrows or A and D move, Space jumps, R restarts the level. On a phone, the corner pads. Five levels of pits, coins, spikes and things that walk and fly, all pure functions over one struct, with sound. Every rectangle, circle and glyph is an instanced quad your graphics card draws through WebGL2, with the CPU rasterizer as the fallback. Read its source β†’

3D model viewer

Drag to orbit, scroll to zoom, or drag and pinch on a phone. The monument is a glTF file the program fetches, projected and lit by the standard library on the CPU: no JavaScript renderer, every triangle placed by your program. Read its source β†’

Monolith field, on the graphics card

Drag to orbit, scroll to zoom, click a tower to claim it. One scene shape holding an island of towers in a rolling ocean: instanced draws, a real depth buffer, fog, mouse picking, and real-time lighting - the glowing orb is the sun, and every face relights as it circles - rendered by your graphics card through WebGL2. The sun's fire and the rolling water are WGSL shaders the program itself carries as strings and loads through game3d_shader, compiled and checked the moment they load. The same program opens a native window on Vulkan. When no card answers, the same scene falls back to the CPU. Read its source β†’

Use a stranger's code safely

import pulls a file into your program and sandboxes it: it can compute anything and touch nothing

No network No disk No environment No processes No global state Zero runtime cost
A file from the internet
nail latest
// A handy helper you found on the internet. It says it formats greetings.
// It also, quietly, tries to read your machine.

f malicious_greeting(name:s):s {
    stolen:s = danger(fs_read(`/etc/passwd`));
    r string_concat([`Hello `, name, `! `, stolen]);
}
Your program
nail latest
// Your program. The downloaded file runs inside the compile-time sandbox.
// Nail should FAIL parsing here: the download reaches for the disk.
import(`snippets/jailbreak_attempt.nail`)

greeting:s = malicious_greeting(`friend`);
print(greeting);
The compiler's answer
error: Sandboxed code brought in by import cannot access 'fs_read': the 'fs' module touches the machine

Not a mock: at startup this server handed the two files above to its own compiler and printed the refusal it got back. The sandbox holds through every function the file reaches, so nothing gets laundered through a helper.

import is the default, so a stranger's file never needs your trust. import_dangerous is for your own files and trusts them completely, which is why its name makes you say so. A package repository for Nail would be a shelf of plain files imported like this: nothing to resolve, nothing to lock.

Instant MCP server

The Model Context Protocol is how AI assistants call outside tools. In Nail it is a tool list and one function.

Nail
nail latest
// An MCP server in Nail: declare the tools, answer the calls, done.
// Point any MCP client (Claude Code, Claude Desktop) at the compiled
// binary and the model can roll dice and measure the world.

tools:a:MCP_Tool = [
    MCP_Tool {
        name = `roll_dice`,
        description = `Roll some dice and see every result`,
        input_schema = `{"type":"object","properties":{"sides":{"type":"integer"},"count":{"type":"integer"}},"required":["sides","count"]}`
    },
    MCP_Tool {
        name = `distance_km`,
        description = `Great-circle distance in kilometers between two points`,
        input_schema = `{"type":"object","properties":{"lat1":{"type":"number"},"lon1":{"type":"number"},"lat2":{"type":"number"},"lon2":{"type":"number"}},"required":["lat1","lon1","lat2","lon2"]}`
    }
];

f handle_tool(tool_name:s, arguments_json:s):s!e {
    if {
        tool_name == `roll_dice` -> {
            sides:i = danger(json_get_int(arguments_json, `sides`));
            count:i = danger(json_get_int(arguments_json, `count`));
            rolls:a:i = map roll_number in array_range(0, count) {
                y danger(rand_int(1, sides));
            };
            r string_from_array_i64(rolls);
        },
        tool_name == `distance_km` -> {
            lat1:f = danger(json_get_float(arguments_json, `lat1`));
            lon1:f = danger(json_get_float(arguments_json, `lon1`));
            lat2:f = danger(json_get_float(arguments_json, `lat2`));
            lon2:f = danger(json_get_float(arguments_json, `lon2`));
            km:f = danger(geo_distance_km(lat1, lon1, lat2, lon2));
            r string_from(danger(math_round_to(km, 1)));
        },
        else -> {
            r e(string_concat([`no tool called `, tool_name]));
        }
    }
}

// Everything for a person goes to stderr through log, stdout is the protocol.
log_info(`nail-tools MCP server starting`);
danger(mcp_serve(`nail-tools`, `1.0.0`, tools));

Declare the tools, write handle_tool, call mcp_serve. One command compiles this file into a static binary that plugs into Claude Code or Claude Desktop, with no runtime to install and no dependencies to resolve. Stdout carries the protocol, the log functions write to stderr so they stay out of its way, and an error you return becomes a tool error the model can act on. Elsewhere this is a Node or Python project with a package manager in front of it. Here it is a file.

Standard library

1189 functions across 84 modules, all built in. This server computed every answer below at startup, by running the expression on its left

money_loan_payment(20000000, 6.0, 360)$1,199.10
stats_ab_test(200, 1000, 250, 1000)0.0074
geo_distance_km(53.5461, -113.4937, 51.0447, -114.0719)280.9
string_best_match("edmontn", cities)Edmonton
time_cron_describe("0 3 * * *")every day at 03:00
markdown_to_html("**bold** move")<p><strong>bold</strong> move</p>
net_ip_in_cidr("10.1.2.3", "10.0.0.0/8")true
html_sanitize("<script>alert(1)</script>hello")hello
color_text_on("#2563eb")#ffffff
format_number_words(array_length(stdlib_functions()))one thousand one hundred eighty-nine
Browse standard library

Get started

One download. No Rust to install, no C compiler, nothing else on the machine.

Shell
curl -fsSL https://nail.alex-wilkinson.ca/install | sudo sh

# Opens the file in Nail's own IDE
nail hello.nail

That installs to /opt/nail and hands the directory to the user who ran it. The sudo is for that one command and nothing after it: installing a version, updating, reclaiming disk and removing versions are all yours to do without it. sudo rm -rf /opt/nail /usr/local/bin/nail is the uninstaller.

Why it needs sudo

Because a release ships with every dependency already compiled, and that only works at one fixed path. Cargo decides whether compiled code can be reused by fingerprints that contain absolute paths. A cache warmed at one path and read from another is thrown away, and the first program you build recompiles five hundred crates instead of using the ones you just downloaded. Installing everywhere means installing at the same place everywhere, and /opt is where a machine keeps software that did not come from its package manager.

What one path buys, beyond the cache being real:

Sharing a machine with someone? Every account on it can build, and they share the one already-built library, so nothing compiles twice. An account created after the install joins with sudo nail share bob.

And if giving an installer one sudo is a line you would rather not cross, that is a perfectly reasonable line and Nail is not the right tool for you. Nothing here will try to talk you round. The alternative was a home install that quietly recompiled everything the toolchain had already compiled, and shipping something that pretends to be prebuilt is worse than asking once.

nail is the only command and the only thing on your PATH. It reads which version a file was written for, downloads exactly that version if you do not have it, and opens the file. The compiler, the editor, a pinned Rust toolchain and every library already built come down in one piece, so the first program compiles in seconds and everything after that works with the network unplugged.

The editor comes with it and runs the real compiler over your file as you type, so an error is underlined where it is instead of guessed at by a plugin:

Nothing to configure: no language server to keep in sync, no formatter to argue about, no plugin that quietly disagrees with the compiler. Build scripts call that same compiler through nail build and nail check, at the version the file asked for rather than whichever is newest.

The install script is there to read, though reading it proves less than it looks like: a server can serve one thing to a pipe and another to a browser, and what it fetches is a compiled toolchain either way. The claim worth having is the one you can check afterwards, which is that all of it is in a directory you own and deleting that directory is the end of it. Or skip the script and build from source with git clone and cargo run. Linux is the supported platform for the tools. The programs you build are static binaries and run anywhere.

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 β”‚nail latest                                                                                                     β”‚β–ˆ
 2 β”‚// Hello World - Your first Nail program                                                                        β”‚β–ˆ
 3 β”‚                                                                                                                β”‚β–ˆ
 4 β”‚greeting:s = `Hello, World!`;                                                                                   β”‚β–ˆ
 5 β”‚print(greeting);                                                                                                β”‚β–ˆ
 6 β”‚count:i = 5.5                                                                                                   β”‚β–ˆ
 7 β”‚                                                                                                                β”‚β–ˆ
 8 β”‚// Demonstrate collection operations with greetings                                                             β”‚β–ˆ
 9 β”‚greetings:a:s = [`Hello`, `Hola`, `Bonjour`, `Guten Tag`, `Ciao`];  β—€ Expected ';' here, but found the name 'grβ€¦β”‚β–ˆ
10 β”‚languages:a:s = [`English`, `Spanish`, `French`, `German`, `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:14  38 lines  1179 bytes  Tab 2/2  [LN,HL,BR]  Ctrl+L: Line# | Ctrl+Shift+H: Highli

The editor runs the real lexer, parser and type checker as you edit, and marks the problem on the line that caused it. There is 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 β”‚nail latest                                                                                                     β”‚β–ˆ
 2 β”‚// Hello World - Your first Nail program                                                                        β”‚β–ˆ
 3 β”‚                                                                                                                β”‚β–ˆ
 4 β”‚greeting:s = `Hello, World!`;                                                                                   β”‚β–ˆ
 5 β”‚print(greeting);                                                                                                β”‚β–ˆ
 6 β”‚count:i = 5.5                                                                                                   β”‚β–ˆ
 7 β”‚total:i = string_le  β—€ Expected ';' here, but found the name 'total'                                            β”‚β–ˆ
 8 β”‚                                                                                                                β”‚β–ˆ
 9 β”‚// Demonstβ”Œ Completions (F1 for docs) ────────────────────┐                                                     β”‚β–ˆ
10 β”‚greetings:β”‚Ζ’ string_length string_length(input:s) -> i    β”‚Ciao`];                                              β”‚β–ˆ
11 β”‚languages:β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜`Italian`];                                          β”‚β–ˆ
12 β”‚                                                                                                                β”‚β–ˆ
13 β”‚// Create formatted greetings using map                                                                         β”‚β”‚
14 β”‚formatted_greetings:a:s = map greeting idx in greetings {                                                       β”‚β”‚
15 β”‚    language:s = danger(array_get(languages, idx));                                                             β”‚β”‚
16 β”‚    y array_join([greeting, ` (`, language, `)`], ``);                                                          β”‚β”‚
17 β”‚};                                                                                                              β”‚β”‚
18 β”‚                                                                                                                β”‚β”‚
19 β”‚// Print each greeting                                                                                          β”‚β”‚
20 β”‚print(`\n=== International Greetings ===`);                                                                     β”‚β”‚
21 β”‚each formatted_greeting in formatted_greetings {                                                                β”‚β”‚
22 β”‚    print(formatted_greeting);                                                                                  β”‚β”‚
23 β”‚}                                                                                                               β”‚β”‚
24 β”‚                                                                                                                β”‚β”‚
25 β”‚// Find greetings with specific letters                                                                         β”‚β”‚
26 β”‚greetings_with_o:a:s = filter greeting in greetings {                                                           β”‚β”‚
27 β”‚    y string_contains(greeting, `o`);                                                                           β”‚β”‚
28 β”‚};                                                                                                              β”‚β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
 examples/hello_world.nail  [*]  7:20  39 lines  1199 bytes  Tab 2/2  [LN,HL,BR]  Ctrl+L: Line# | Ctrl+Shift+H: Highli

Every function ships with the language, so the editor offers all of them with full types. No index to build, nothing to keep in sync.

Embedded languages stay readable

Nail
page_styles:s = css`.hero { color: #86efac; }`;
banner:s = html`<section class="hero"><h1>Nail</h1></section>`;
lookup:s = sql`SELECT name FROM users WHERE active = 1;`;
settings:s = yaml`service: nail   # what runs here`;
manifest:s = toml`[package]`;
behaviour:s = js`const total = items.map(double);`;
shader:s = wgsl`@fragment fn glow() -> @location(0) vec4<f32> { return vec4<f32>(hue, 1.0); }`;
release:s = md`## Out **today**`;
plain:s = `no tag, no change`;

A web program is mostly long strings of other languages. Tag one with what it holds - html`<p>hi</p>`, or css, sql, yaml - and the editor and this page both colour it as that language. The colouring keeps its place across line breaks, so an open <div, a CSS block or a /* ... */ still reads correctly on the next line.

The tag means nothing to the compiler: same type, same escapes, same Rust out the other end. It is a note to whoever is reading. The tags both highlighters know:

Markuphtml htm xhtml svg xml rss atom
Stylesheetscss scss sass less
JavaScriptjs javascript mjs cjs jsx ts typescript tsx json jsonc
SQLsql postgres postgresql mysql sqlite
Shellsh bash zsh shell dockerfile
Languagespy rb rs go java cs c cpp php swift kt lua graphql
Shaderswgsl
Configurationyaml yml toml ini cfg conf properties
Prosemd markdown

The compiler keeps no list of languages, so a tag from outside this table still compiles, its string simply keeps one plain colour. Nothing here is a plugin: one scanner covers everything built out of words, strings and comments, and each language is a row in its table.

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 β”‚nail latest                                                                                                     β”‚β–ˆ
 2 β”‚// Hello World - Your first Nail program                                                                        β”‚β–ˆ
 3 β”‚                                                                                                                β”‚β–ˆ
 4 β”‚greeting:s = `Hello, World!`;                                                                                   β”‚β–ˆ
 5 β”‚print(greeting);                                                                                                β”‚β–ˆ
 6 β”‚count:i = 5.5                                                                                                   β”‚β–ˆ
 7 β”‚total:i = string_le  β—€ Expected ';' here, but found the name 'total'                                            β”‚β–ˆ
 8 β”‚                                                                                                                β”‚β–ˆ
 9 β”‚// Demonstrate collection operaβ”Œ Documentation (F1 to toggle) ──────────────────┐                               β”‚β–ˆ
10 β”‚greetings:a:s = [`Hello`, `Holaβ”‚Function: string_length                         β”‚                               β”‚β–ˆ
11 β”‚languages:a:s = [`English`, `Spβ”‚                                                β”‚                               β”‚β–ˆ
12 β”‚                               β”‚Signature: string_length(input:s) -> i          β”‚                               β”‚β–ˆ
13 β”‚// Create formatted greetings uβ”‚                                                β”‚                               β”‚β”‚
14 β”‚formatted_greetings:a:s = map gβ”‚Description:                                    β”‚                               β”‚β”‚
15 β”‚    language:s = danger(array_gβ”‚Returns the number of characters in the string. β”‚                               β”‚β”‚
16 β”‚    y array_join([greeting, ` (β”‚                                                β”‚                               β”‚β”‚
17 β”‚};                             β”‚Example:                                        β”‚                               β”‚β”‚
18 β”‚                               β”‚length:i = string_length(`hello`);              β”‚                               β”‚β”‚
19 β”‚// Print each greeting         β”‚                                                β”‚                               β”‚β”‚
20 β”‚print(`\n=== International Greeβ”‚                                                β”‚                               β”‚β”‚
21 β”‚each formatted_greeting in formβ”‚Press ESC to go back, TAB to insert             β”‚                               β”‚β”‚
22 β”‚    print(formatted_greeting); β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                               β”‚β”‚
23 β”‚}                                                                                                               β”‚β”‚
24 β”‚                                                                                                                β”‚β”‚
25 β”‚// Find greetings with specific letters                                                                         β”‚β”‚
26 β”‚greetings_with_o:a:s = filter greeting in greetings {                                                           β”‚β”‚
27 β”‚    y string_contains(greeting, `o`);                                                                           β”‚β”‚
28 β”‚};                                                                                                              β”‚β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
 examples/hello_world.nail  [*]  7:20  39 lines  1199 bytes  Tab 2/2  [LN,HL,BR]  Ctrl+L: Line# | Ctrl+Shift+H: Highli

Documentation lives in the registry the compiler type checks against, so it cannot drift out of date.

Performance overview

What runs in parallel automatically, and what it measures out to

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: only when the compiler can prove it is safe, below

Never: each, whose side effects happen in the order you wrote them, and scan, where every value depends on the ones before it.

A hand-written loop is opaque, so it runs in order. A named operation carries its own rules: sum says order does not matter, which is what lets it spread across cores.

reduce parallelizes only where regrouping cannot change the answer: integer addition and multiplication, min, max. Subtraction, division and floats stay in order, with no flag to override it, because a wrong promise is a silently wrong number.

Threads are not free, so parallelism switches on above a measured size:

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. Costlier comparisons cross 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.

Microbenchmarks of single operations, not whole programs. Most software waits on I/O and gains nothing here, where Nail emits the sequential Rust you would 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 built with --release. Hand-written Rust runs on one core unless you parallelize it yourself. Nail spreads across the rest when the work justifies it.

The server profiles itself

Every build times every function, with nothing turned on

Not a benchmark: the live profiler dump of the Nail program serving you this page, every function it has run since it started, counted and timed, refreshed as you watch. Recording a call costs about 30 nanoseconds, and nailc --no-profile builds without it.

Reading the profiler dump...

The same numbers, pinned in the IDE

β”ŒFILES───────────────────────────────────────────────────────────────────────────────────────────────────────────Ready
β”‚ Untitled 1 β”‚ profiling_demo.nail                                                                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β”ŒNAIL - profiling_demo.nailβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β–ˆ
 1 β”‚nail latest                                                                                                     β”‚β–ˆ
 2 β”‚// Every build is profiled. Run this program, then open it in the IDE:                                          β”‚β–ˆ
 3 β”‚// each function wears the timings the program just measured for itself.                                        β”‚β–ˆ
 4 β”‚                                                                                                                β”‚β–ˆ
 5 β”‚f factorial(num:i):i {  β—€ 53.7Β΅s total (0.0%)  4.5Β΅s avg Γ— 12  4.9Β΅s max                                        β”‚β–ˆ
 6 β”‚    if {                                                                                                        β”‚β–ˆ
 7 β”‚        num <= 1 -> { r 1; },                                                                                   β”‚β–ˆ
 8 β”‚        else -> { r num * factorial(num - 1); }                                                                 β”‚β–ˆ
 9 β”‚    }                                                                                                           β”‚β–ˆ
10 β”‚}                                                                                                               β”‚β–ˆ
11 β”‚                                                                                                                β”‚β–ˆ
12 β”‚f is_prime(num:i):b {  β—€ 506.4ms total (92.1%)  50.6Β΅s avg Γ— 9998  12.2ms max                                   β”‚β–ˆ
13 β”‚    if {                                                                                                        β”‚β–ˆ
14 β”‚        num < 2 -> { r false; },                                                                                β”‚β–ˆ
15 β”‚        else -> {                                                                                               β”‚β–ˆ
16 β”‚            has_divisor:b = any div in array_range(2, num) { y num % div == 0; };                               β”‚β”‚
17 β”‚            r !has_divisor;                                                                                     β”‚β”‚
18 β”‚        }                                                                                                       β”‚β”‚
19 β”‚    }                                                                                                           β”‚β”‚
20 β”‚}                                                                                                               β”‚β”‚
21 β”‚                                                                                                                β”‚β”‚
22 β”‚f count_primes_below(limit:i):i {  β—€ 523.0ms total (95.1%)  523.0ms avg Γ— 1  523.0ms max                        β”‚β”‚
23 β”‚    primes:a:i = filter num in array_range(2, limit) { y is_prime(num); };                                      β”‚β”‚
24 β”‚    r array_length(primes);                                                                                     β”‚β”‚
25 β”‚}                                                                                                               β”‚β”‚
26 β”‚                                                                                                                β”‚β”‚
27 β”‚primes_found:i = count_primes_below(10000);                                                                     β”‚β”‚
28 β”‚twelve_factorial:i = factorial(12);                                                                             β”‚β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
 profiling_demo.nail   1:1  30 lines  786 bytes  Tab 2/2  [LN,HL,BR]  Ctrl+L: Line# | Ctrl+Shift+H: Highlight | Ctrl+S

A real frame from the editor. Each function wears its last run's timings: total, share of wall time, average with call count, max. Edit the file and they dim as stale until the next run.

And every run answers on the way out

a program closing, printed by itself
── nail timing sheet ──
function               calls      total        avg        max       %
is_prime                9998    419.0ms     41.9Β΅s     13.9ms  2304.3%
count_primes_below         1     18.2ms     18.2ms     18.2ms   99.8%
factorial                 12      5.2Β΅s      437ns      781ns    0.0%
program wall time 18.2ms, it could run 55.0 times a second. Times are cumulative, a caller includes its callees. A share over 100% means that function ran on several cores at once.

Real output from examples/profiling_demo.nail. The last line is the whole program as one number: over 60 runs a second reads as instant, single digits as sluggish. The 2304% share is is_prime running across every core at once, parallelized by the compiler. Games close with one more line, like 56 fps shown, 513 fps possible, 1.95ms of real work per frame.

Programs that don't rot

A file says which Nail wrote it, and that is the Nail that compiles it. Forever

Line one of every Nail file names the compiler that wrote it. It is part of the file, so it travels wherever the file goes: into git, into a zip, onto a laptop five years from now.

my_app.nail
nail 0.1.0

total:i = array_sum([3, 4, 5]);
print(string_from(total));

The command that runs it never mentions a version. nail reads line one, makes sure that exact compiler is on the machine, downloads it if it is not, and hands the file over.

Terminal, the first time this machine sees 0.1.0
$ nail run my_app.nail
nail: 0.1.0 is not installed, fetching it
  Nail 0.1.0 [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100%  774.2 MB / 774.2 MB  22.4 MB/s
unpacking
Nail 0.1.0 installed
12

The 12 on the last line is the program's own output: array_sum([3, 4, 5]) is 12. The install happened on the way to running the file, not as a step of its own.

The nail on your PATH is not the compiler. It is a small program that owns the set of installed versions and picks one per file. Versions sit side by side in a directory you own and never see each other, so opening an old file installs the compiler that file asks for and changes nothing about any other file on the machine. There is no global default to move and no project to migrate first.

Why the version number is the whole lock file

Elsewhere a compiler version is the start of the story, and a lock file carries the hundreds of package versions it does not pin. Nail has no package manager and a closed set of libraries, all shipped inside the release, already vendored and built. Pin the compiler and every byte underneath it is pinned too. Nothing is resolved at build time, so there is nothing left to write down.

The three states of line one

Line oneWhat happensWho writes it
nail 0.1.0 Compiled by 0.1.0 and nothing else, today and in ten years The editor, on save. nail new, on create
nail latest Not settled yet. Every open asks what the newest release is, and fetches it if it is new You, by hand, for code you are still writing
Missing An error. The compiler refuses a file that has not said what built it Nobody

No ranges, no carets, no "any 0.3.x". A range means the same file compiling differently next year, which is the drift this exists to prevent, so the grammar cannot express one. The missing line is an error for the same reason: any default would quietly mean "whatever is newest".

nail which: what compiles this file, and why

Resolution is never a guess you have to make. Ask, and you get the version, the reason it was chosen, and the exact binary that would run.

Terminal
$ nail which my_app.nail
my_app.nail pins 0.1.0
/opt/nail/versions/0.1.0/bin/nail

$ nail which work_in_progress.nail
work_in_progress.nail says `nail latest`, newest installed is 0.1.1
/opt/nail/versions/0.1.1/bin/nail

nail fetch: take a whole project offline

A checkout can pin several versions at once, because one file was frozen last year and another moved last week. nail fetch walks a directory, reads line one of every Nail file under it, and installs every version it finds. When it finishes, every file in that tree opens with the network unplugged.

Terminal
$ nail fetch .
41 Nail files, pinning 2 version(s), 1 missing
  Nail 0.1.1 [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100%  800.2 MB / 800.2 MB  22.4 MB/s
unpacking
Nail 0.1.1 installed

$ nail fetch .
41 Nail files, pinning 2 version(s), 0 missing
nothing to fetch

nail update: moving a file forward

Moving a file to a newer Nail means exactly one thing: rewriting line one. The rest of the file is untouched, and no other file is affected. Nothing does this on its own. A pinned file is compiled by its pin and by nothing else, even when a newer compiler would accept it, because compiling cleanly is not proof the program still behaves exactly as it was written. There is no auto upgrade path, only an edit you asked for.

nail update installs the version being moved to, compiles each file against it, and rewrites line one only for the files that pass. The check is a filter, not a guarantee: it stops the files the new version rejects outright. A file that fails keeps the line it had and keeps working, exactly as it did before the command ran. The files that do move are yours to test under their new pin.

Without --yes nothing is written. It is a list of what would be attempted.

Terminal
$ nail update .
3 file(s) would move to 0.1.1
  ./main.nail
  ./my_app.nail
  ./my_old_app.nail

nothing changed. Add --yes to check each file and restamp the ones that pass

$ nail update . --yes
3 file(s) would move to 0.1.1
2 file(s) moved to 0.1.1
1 file(s) do not compile under 0.1.1 and were left alone:
  ./my_old_app.nail

And on disk, that is the whole of it:

FileLine one beforeLine one afterWhy
main.nail nail 0.1.0 nail 0.1.1 Compiles under 0.1.1, so the move you asked for went ahead
my_app.nail nail 0.1.0 nail 0.1.1 Compiles under 0.1.1, so the move you asked for went ahead
my_old_app.nail nail 0.1.0 nail 0.1.0 0.1.1 rejects it, so the move was refused. Line one untouched, and it opens with 0.1.0 exactly as before
work_in_progress.nail nail latest nail latest Tracks the newest release already, so there is nothing to move

my_old_app.nail is not a problem to be solved before the others can move. Fix it and run the command again, or leave it on 0.1.0 for as long as it is useful. Migration is a per-file choice, and it is always a choice. --to=0.1.4 moves to a named version rather than the newest installed one.

A release is one file on one box, served over HTTPS, and a version number names exactly one of them. No index to go stale, no registry to resolve against.

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.

One command turned this page into the server you are reading it from. I also made Simple Universal Language, a spoken and written language built to still reach people who cannot see or hear it.