Docs
Write, check, ship.
The short version of Neo: install it, write a program, read what the compiler proves, and hand your agent skills.md.
Install
One command installs Neo in ~/.neo and runs neo doctor, which names anything missing and the command that installs it. Then add ~/.neo/bin to your PATH, as the installer says.
$ curl -fsSL https://neolang.dev/install.sh | sh
Building Neo needs Python 3.11 or newer, Clang/LLVM and binutils. It targets x86-64 Linux, ARM64 Linux and ARM64 macOS. Linux binaries are static: no libc, loader or shared libraries.
$ neo init my-app
$ cd my-app
$ neo run
neo init creates a project with a manifest that names the entry file; neo build and neo run use it when no file is given.
For agents: skills.md
skills.md at its root: one file that teaches a model the write → check → fix → build loop, how to read every proof obligation, the syntax that matters and the usual fixes. Give it to your agent before it writes its first line.It is written for models, not people: short rules, exact commands and real compiler output, with nothing to skim past.
Your first program
A main without effects returns its output; the runtime prints it.
fn main () -> Arr u8 _ = "Hello, World!\n"
$ neo build hello.neo -o hello
$ ./hello
Hello, World!The check loop
neo check proves every array index, division and range, and prints one JSON entry per obligation. Anything it cannot prove is a compile error with a counterexample and suggested fixes; it never inserts a silent runtime check. A file without main is a library: add --library.
fn at (a : Arr i64 n, i : i64) -> i64 = a[i]
$ neo check --library at.neo
{ "fn": "at", "node": "0d1e7a0fe59c", "kind": "bounds",
"goal": "0 <= i && i < len(a)", "status": "unproven",
"counterexample": { "i": -1, "n": 0 },
"fixes": ["guard: if 0 <= i && i < len(a) then … else …",
"check 0 <= i && i < len(a) in …",
"refine the index parameter, e.g. i : i64{0 <= i, i < n}"] }
Refine the parameter and the obligation is proven; every caller now has to prove its own precondition:
fn at (a : Arr i64 n, i : i64{0 <= i, i < n}) -> i64 = a[i]
- bounds
- An array index is in range.
- division
- A divisor is not zero.
- precondition
- A call satisfies the callee's refinements.
- invariant
- A loop state's refinement holds initially and is preserved.
- check
- An explicit
check, kept as a runtime test.
The node hash names the expression by its structure, so it survives renames and edits elsewhere: use it to follow one obligation across iterations.
Syntax basics
Everything is an expression; there are no statements or early returns. Comments start with ;.
fn square (x : i64) -> i64 = x * x
fn clamp (v : i64, lo : i64, hi : i64) -> i64
= if v < lo then lo else if v > hi then hi else v
fn sum_to (n : i64{n >= 0}) -> i64
= let total = sum(i in 0..n) i in total
- Types:
i64,i32,u8,f64,bool,unit. Arrays areArr T n, wherennames the length;_means unknown. Strings areArr u8 _. - Refinements:
i : i64{0 <= i, i < n}. Commas mean "and"; every caller must prove them. - Records:
type Body = { x : f64, m : f64 }, built withBody { x = 1.0, m = 2.0 }and updated withb with { x = 0.0 }. - Comprehensions:
arr(i in 0..n) ebuilds an array andsum(i in 0..n) ereduces. Both run across cores.
Loops carry explicit state. next gives every state variable its new value, in order; yield is the result:
fn starts_with (a : Arr u8 n, b : Arr u8 m) -> bool
= m <= n && loop (i : i64{i >= 0} = 0, ok = true)
while ok && i < m
next (i + 1, a[i] == b[i])
yield okEffects
A plain fn is pure, and pure calls with literal arguments run at compile time. Side effects live in effect functions that name their capabilities, checked through every call: fs, net, time, env, process and unsafe.
import "std:file"
effect(fs) fn main() -> unit
= using file=open_file("/dev/null",0) in
let status=file_status(file) in check status.ok in
let result=read_file(file,32) in check result.ok in
let output=print_out(concat("read ",int_text(len(result.value))," bytes\n")) in noop()
Refinements and comprehension bodies can never perform effects.
An HTTP server
A server is one call. The handler is a pure function from request bytes to response bytes; the runtime handles sockets, framing, pipelining and one worker per core.
fn handle (req : Arr u8 n) -> Arr u8 _
= "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!"
effect fn main (port : i64{port > 0, port < 65536}) -> i64 = serve(port, handle)
$ neo run server.neo -- 8080Standard library
Import a relative file with import "helpers.neo" and the standard library with import "std:json". Declarations are public unless marked private.
| Module | For |
|---|---|
std:strings | Slicing, search, trimming, integer parsing and formatting, UTF-8 |
std:json | Strict parsing, accessors and typed field decoding |
std:io, std:file | Files, arguments, environment, clocks |
std:http, std:http_client | Request and response helpers, HTTP client |
std:vec, std:hashmap | Growable vectors and maps |
std:option, std:result | Option<T>, Result<T,E> and ? binds |
Command line
| Command | Does |
|---|---|
neo check file.neo | Type-check and prove; JSON on stdout |
neo build file.neo -o app | Native binary, LLVM -O3 (--opt-level 0 for quick builds) |
neo run file.neo -- args | Build and run |
neo test | Project tests |
neo fmt | Format source files |
neo init dir | Create a project |
neo lsp | Language server for editors |