In Greek, Nostos (νόστος) means a hero's homecoming or return from a long journey.
After wandering through callback hell, fighting with async/await, and battling race conditions—Nostos welcomes you home to a place where concurrent code is simple, readable, and just works.
📚 Full Tutorial: heynostos.tech
brew tap pegesund/nostos
brew install nostos# Apple Silicon (M1/M2/M3/M4)
curl -LO https://github.com/pegesund/nostos/releases/latest/download/nostos-aarch64-apple-darwin.tar.gz
tar -xzf nostos-aarch64-apple-darwin.tar.gz
# Intel Mac
curl -LO https://github.com/pegesund/nostos/releases/latest/download/nostos-x86_64-apple-darwin.tar.gz
tar -xzf nostos-x86_64-apple-darwin.tar.gz
chmod +x nostos nostos-lsp
sudo mv nostos nostos-lsp /usr/local/bin/
# If macOS blocks it: xattr -d com.apple.quarantine nostos nostos-lspcurl -LO https://github.com/pegesund/nostos/releases/latest/download/nostos-x86_64-unknown-linux-gnu.tar.gz
tar -xzf nostos-x86_64-unknown-linux-gnu.tar.gz
chmod +x nostos nostos-lsp
sudo mv nostos nostos-lsp /usr/local/bin/Download from GitHub Releases or:
Invoke-WebRequest -Uri "https://github.com/pegesund/nostos/releases/latest/download/nostos-x86_64-pc-windows-msvc.zip" -OutFile "nostos.zip"
Expand-Archive -Path "nostos.zip" -DestinationPath "C:\nostos"
# Add C:\nostos to your PATHecho 'main() = println("Hello, Nostos!")' > hello.nos
nostos hello.nosFirst run extracts stdlib and builds cache (~1s). Subsequent runs: ~0.1s.
Download the .vsix from GitHub Releases and install:
curl -LO https://github.com/pegesund/nostos/releases/latest/download/nostos-vscode.vsix
code --install-extension nostos-vscode.vsixNote: The extension requires nostos-lsp to be in your PATH. Both nostos and nostos-lsp binaries are included in the release packages—make sure both are installed (e.g., in /usr/local/bin/).
Features: Syntax highlighting, LSP integration (errors, autocomplete), code navigation.
# Pattern matching flows naturally
fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(n) = fibonacci(n - 1) + fibonacci(n - 2)
# Lists destructure elegantly
sum([]) = 0
sum([head | tail]) = head + sum(tail)
# Quicksort in 3 lines
quicksort([]) = []
quicksort([pivot | rest]) =
quicksort(rest.filter(x => x < pivot)) ++ [pivot] ++ quicksort(rest.filter(x => x >= pivot))
# Pipes chain beautifully
result = users
.filter(u => u.active)
.map(u => u.name)
.join(", ")
Describe the shape you expect, not the steps to extract it. Pattern matching handles recursive data structures elegantly:
# A binary tree: either empty or a node with value and children
type Tree[T] = Empty | Node(T, Tree[T], Tree[T])
# Count nodes - pattern matching makes it trivial
size(Empty) = 0
size(Node(_, left, right)) = 1 + size(left) + size(right)
# Sum all values
sum(Empty) = 0
sum(Node(val, left, right)) = val + sum(left) + sum(right)
# Search with guards
contains(Empty, _) = false
contains(Node(val, _, _), target) when val == target = true
contains(Node(_, left, right), target) =
contains(left, target) || contains(right, target)
# Or-patterns for multiple alternatives
classify(1 | 2 | 3) = "small"
classify(4 | 5 | 6) = "medium"
classify(_) = "large"
# Build and query a tree
main() = {
tree = Node(5,
Node(3, Node(1, Empty, Empty), Empty),
Node(8, Node(6, Empty, Empty), Node(9, Empty, Empty))
)
println("Size: " ++ show(size(tree))) # 6
println("Sum: " ++ show(sum(tree))) # 32
println("Has 6? " ++ show(contains(tree, 6))) # true
}
The compiler ensures you handle every case. Add a new tree variant? The compiler reminds you to update every function. No forgotten cases, no runtime surprises.
Nostos is designed to save you time. That's the only metric that matters. We prefer functional patterns because they're often clearer and safer, but we won't force you into contortions when a mutable variable just makes sense.
Immutable by Default — Data structures are immutable by default. Updates create new versions with structural sharing—fast and safe:
# Immutable list operations
numbers = [1, 2, 3]
doubled = numbers.map(x => x * 2) # Creates new list
filtered = numbers.filter(x => x > 1) # Original unchanged
# Immutable maps with convenient index syntax
config = %{"port": 8080, "host": "localhost"}
port = config["port"] # Get value: 8080
config["debug"] = true # Set value (updates variable with new map)
# Sets with membership checking via index syntax
roles = #{"admin", "editor", "viewer"}
if roles["admin"] then println("Has admin access") # true/false
This prevents entire classes of bugs. No surprise mutations, no defensive copying needed.
Mutable Variables When You Need Them — Sometimes a loop with a mutable accumulator is clearer than a fold:
# Functional style - elegant for simple cases
sumFunctional(numbers) = numbers.fold(0, (acc, x) => acc + x)
# Imperative style - clearer for complex logic
averagePositive(numbers) = {
var total = 0
var count = 0
numbers.each(n => {
if n > 0 then {
total = total + n
count = count + 1
} else ()
})
if count > 0 then total / count else 0
}
Use var when it makes your intent clearer. The data structures themselves remain immutable—only local variables can be reassigned.
Thread-Safe Globals with mvars — Need shared state across processes? Use mvar for thread-safe global variables:
use stdlib.server.{serve, respondText}
# Declare a thread-safe global counter
mvar requestCount: Int = 0
handleRequest(req) = {
requestCount = requestCount + 1 # Atomic update
respondText(req, "Request #" ++ show(requestCount))
}
main() = serve(8080, handleRequest)
Each HTTP request spawns a new process. All processes safely update requestCount because mvar uses locks internally. Use mvar sparingly—message passing is usually better—but it's perfect for simple shared counters or caches.
Traditional Loops — All the imperative control flow you'd expect:
# While loops with condition
factorial(n) = {
var result = 1
var i = 1
while i <= n {
result = result * i
i = i + 1
}
result
}
# For loops with ranges (1 to n is exclusive of n)
sumSquares(n) = {
var sum = 0
for i = 1 to (n + 1) {
sum = sum + (i * i)
}
sum
}
# Break to exit early
findFirst(arr, target) = {
var result = -1
for i = 0 to arr.length() {
if arr[i] == target then {
result = i
break # Exit loop when found
} else ()
}
result
}
# Continue to skip iterations
sumPositive(arr) = {
var total = 0
for i = 0 to arr.length() {
if arr[i] < 0 then continue # Skip negatives
total = total + arr[i]
}
total
}
# Early return from functions
searchMatrix(matrix, value) = {
for row = 0 to matrix.length() {
for col = 0 to matrix[row].length() {
if matrix[row][col] == value then
return (row, col) # Found it, return immediately
}
}
(-1, -1) # Not found
}
Both functional and imperative styles are valid. Use whichever makes the code easier to understand. Your time matters more than purity.
Every I/O operation yields automatically. No async, no await, no colored functions. Your code looks synchronous but runs concurrently.
# These HTTP requests run in parallel—no special syntax needed
fetchAll(urls) = urls.map(url => Http.get(url))
# Spawn 100,000 processes without breaking a sweat
main() = {
pids = range(1, 100001).map(i => spawn(() => worker(i)))
println("Spawned " ++ show(pids.length()) ++ " processes")
}
The REPL isn't just for experiments—it's your development cockpit:
- Live reload: Change code, see results instantly
- Autocomplete: Context-aware suggestions as you type
- Inline errors: Know what's wrong before you run
- State inspection: Peek inside running processes
$ nostos
Nostos REPL v0.1.0
>>> users = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }]
>>> users.filter(u => u.age > 28).map(u => u.name)
["Alice"]Not just syntax highlighting—true understanding:
- Real-time error checking as you type
- Go to definition across modules
- Smart autocomplete with type inference
- Integrated REPL in your editor
- File status badges showing compile state at a glance
Query your database with minimal friction:
main() = {
conn = Pg.connect("host=localhost dbname=mydb user=postgres password=secret")
# Parameterized queries prevent SQL injection
# Params: () for none, scalar for one, tuple/list for multiple
rows = Pg.query(conn, "SELECT name, email FROM users WHERE age > $1 AND active = $2", (18, true))
rows.map(row => println(row.0 ++ ": " ++ row.1))
Pg.close(conn)
}
No slow, complex, or scary ORM. Just plain queries and safe types, powered by introspection.
Typed Results — Map query results to typed records:
use stdlib.db.{query}
type User = { name: String, email: String }
main() = {
conn = Pg.connect("host=localhost dbname=mydb user=postgres password=secret")
# Column order in SELECT must match field order in type
users: List[User] = query[User](conn, "SELECT name, email FROM users", ())
# Now use field names instead of positional access!
users.map(u => println(u.name ++ ": " ++ u.email))
Pg.close(conn)
}
Features:
- Transactions -
Pg.begin(),Pg.commit(),Pg.rollback() - Connection pooling - Automatic per-connection-string pooling
- Prepared statements -
Pg.prepare(),Pg.queryPrepared(),Pg.executePrepared() - Vector search (pgvector) - Native Float32Array support for embeddings
- JSON/JSONB - Direct JSON value support in queries
- LISTEN/NOTIFY - Real-time change notifications with
Pg.listen(),Pg.notify() - Binary types - Int, Float, Bool, String, arrays, and custom types
- TLS/SSL - Secure connections to cloud providers (Supabase, Neon, etc.)
Full-stack reactive web apps with server-side rendering and automatic DOM diffing. State changes propagate automatically—change a reactive record and the UI updates:
use stdlib.rweb.*
reactive Todo = { text: String, done: Bool }
reactive State = { todos: List[Todo] }
sessionSetup(writerId) = {
state = State([])
renderPage = () => RHtml(div([
h1("Todo App"),
component("list", () => RHtml(
ul(state.todos.map(t => li(t.text)))
)),
input(type: "text", dataAction: "add")
]))
onAction = (action, params) => match action {
"add" -> { state.todos = state.todos ++ [Todo(params.text, false)] }
_ -> ()
}
(renderPage, onAction)
}
main() = startRWeb(8080, "Todos", sessionSetup)
Production-ready networking:
# Server
handle(req) = match req.path {
"/api/users" -> jsonResponse(getUsers()),
"/health" -> textResponse("OK"),
_ -> notFound()
}
main() = Server.start(8080, handle, workers: 8)
# Client
response = Http.get("https://api.example.com/data")
data = json.parse(response.body)
Low-level when you need it:
server = Tcp.listen(9000)
client = Tcp.accept(server)
Tcp.send(client, "Hello!")
message = Tcp.receive(client)
Real-time bidirectional communication:
# Server
handle(req) = {
if WebSocket.isUpgrade(req) then {
ws = WebSocket.accept(req.id)
WebSocket.send(ws, "Welcome!")
message = WebSocket.recv(ws)
WebSocket.close(ws)
} else respond404(req)
}
# Client
ws = WebSocket.connect("wss://echo.websocket.org")
WebSocket.send(ws, "Hello!")
response = WebSocket.recv(ws)
# Client with custom HTTP headers on the upgrade (e.g. X-API-Key, Authorization)
ws = WebSocket.connectWithHeaders("wss://api.example.com/feed", [
("X-API-Key", "secret"),
("Authorization", "Bearer abc123"),
])
Secure hashing and password storage:
# Hashing
sha = Crypto.sha256("password123") # Hex string
sha512 = Crypto.sha512("data")
# Password hashing (bcrypt)
hash = Crypto.bcryptHash("password", 12) # Cost factor 12
valid = Crypto.bcryptVerify("password", hash) # true
# Random bytes for tokens/keys
token = Crypto.randomBytes(32) # 32 random bytes as hex
Pattern matching and text processing:
# Match and find
if Regex.matches("hello123", "\\d+") then println("Has numbers")
Regex.find("Price: $42.99", "\\$[0-9.]+") # Some("$42.99")
# Replace
Regex.replace("hello world", "world", "Nostos") # "hello Nostos"
Regex.replaceAll("a1b2c3", "\\d", "X") # "aXbXcX"
# Split and capture
words = Regex.split("one,two,three", ",") # ["one", "two", "three"]
Read and write files with ease:
# Simple read/write
content = File.readAll("config.txt")
File.writeAll("output.txt", "Hello, world!")
File.append("log.txt", "Error: connection failed\n")
# Streaming for large files
handle = File.open("large.dat", "r")
line = File.readLine(handle)
File.close(handle)
Parse, generate, and transform:
data = json.parse('{"name": "Alice", "scores": [95, 87, 92]}')
name = data.get("name") # "Alice"
output = json.stringify(#{ "status": "ok", "count": 42 })
Type-safe templates with built-in parameter names for common attributes:
use stdlib.html.{Html, render}
# Common attributes like class, id, style are built-in parameters
page(title, content) = Html(
div(class: "container", id: "main", [
header([
h1(class: "title", title),
nav(class: "nav", [
a(href: "/home", "Home"),
a(href: "/about", "About")
])
]),
div(class: "content", content),
button(
"Submit",
btnType: "submit",
class: "btn-primary",
dataAction: "submit-form"
),
input(
inputType: "email",
placeholder: "Enter email",
class: "input",
name: "email"
),
footer(class: "footer", [ p("© 2024") ])
])
)
html = render(page("Welcome", [p("Hello, world!")]))
Built-in parameters include class, id, style, href, inputType, btnType, dataAction, and more. Use attrs: [("custom", "value")] for non-standard attributes.
Structured logging with levels:
import logging
log.info("Server started", port: 8080)
log.error("Connection failed", error: err, retry: 3)
When you need raw performance or existing libraries, Nostos extensions bridge to native code seamlessly:
# Load a native extension
import glam # Linear algebra via Rust's glam crate
main() = {
v1 = glam.vec3(1.0, 2.0, 3.0)
v2 = glam.vec3(4.0, 5.0, 6.0)
dot = v1.dot(v2)
cross = v1.cross(v2)
normalized = v1.normalize()
}
Extensions are Rust crates that expose functions to Nostos. The type system ensures safe interop:
# nostos.toml
[extensions]
glam = { git = "https://github.com/pegesund/nostos-glam" }
nalgebra = { git = "https://github.com/pegesund/nostos-nalgebra" }| Extension | Description | Repository |
|---|---|---|
| glam | Fast linear algebra (vectors, matrices, quaternions) | nostos-glam |
| nalgebra | Dynamic vectors and matrices for scientific computing | nostos-nalgebra |
| redis | Redis client for caching and pub/sub | nostos-redis |
| candle | Machine learning with Hugging Face's Candle | nostos-candle |
Extend the REPL with custom panels:
| Nostlet | Description | Repository |
|---|---|---|
| runtime-stats | Live CPU, memory, and process statistics | nostos-runtime-stats |
| community | Community-contributed panels | nostos-nostlets |
Organize code into modules and import what you need:
# Import everything from a module
use math.*
# Import specific items
use stdlib.server.{serve, respondText}
# Import with aliases to avoid conflicts
use graphics.{draw as graphicsDraw}
use text.{draw as textDraw}
The compiler catches name conflicts at compile time. If two modules export the same name:
# a.nos
pub helper(x: Int) = x + 1
# b.nos
pub helper(x: Int) = x * 2
# main.nos
use a.*
use b.* # Error: import conflict: `helper` is imported from both `a` and `b`
The compiler tells you exactly what's wrong and how to fix it:
Error: import conflict: `helper` is imported from both `a` and `b`
Help: use qualified name `b.helper` or selective import to resolve the conflict
Solutions:
# Option 1: Use aliases
use a.{helper as aHelper}
use b.{helper as bHelper}
# Option 2: Import one, qualify the other
use a.*
result = helper(5) + b.helper(5)
Strong static typing with inference that actually works:
# Types are inferred...
numbers = [1, 2, 3] # List[Int]
doubled = numbers.map(x => x * 2) # List[Int]
# ...but you can be explicit when it helps
parseConfig(path: String) -> Result[Config, String] = {
content = readFile(path)
json.parse(content).mapErr(e => "Parse error: " ++ e)
}
trait Drawable
draw(self) -> String
end
type Circle = { radius: Float }
type Square = { side: Float }
Circle: Drawable
draw(self) = "●"
end
Square: Drawable
draw(self) = "■"
end
# Works with any Drawable
render[T: Drawable](shapes: List[T]) = shapes.map(s => s.draw()).join(" ")
trait Printable
toString(self) -> String
end
trait Serializable: Printable # Requires Printable
toJson(self) -> String
end
# Clone and build
git clone https://github.com/pegesund/nostos.git
cd nostos
cargo build --release
# Run the REPL
./target/release/nostos
# Run a program
./target/release/nostos examples/hello_server.nos
# Install VS Code extension
cd editors/vscode && npm install && npm run package
code --install-extension nostos-*.vsixImportant
To build Nostos from source, you need Rust 1.85 or newer.
Built in Rust for reliability and performance:
| Crate | Purpose |
|---|---|
compiler |
Lexer, parser, type inference |
vm |
Register-based bytecode interpreter |
jit |
Cranelift-powered JIT compilation |
scheduler |
Lightweight process runtime |
repl |
Interactive environment & LSP |
lsp |
Language server for editors |
-
Tokio runtime — All async I/O is powered by Tokio, the industry-standard async runtime for Rust. File operations, networking, timers, and process scheduling all run on Tokio's work-stealing thread pool.
-
imbl persistent data structures — Lists, maps, and sets are immutable by default, using structural sharing for efficient updates. No defensive copying, no surprise mutations. Functional programming without the performance penalty.
-
Hindley-Milner type inference — Full type inference means you rarely write type annotations, but the compiler catches errors at compile time. Generics, traits, and higher-order functions all work seamlessly.
-
Register-based VM — Unlike stack-based VMs, our register-based design reduces instruction count and enables better JIT optimization. Hot paths are compiled to native code via Cranelift.
-
tokio-postgres + deadpool — Native async PostgreSQL driver with connection pooling. No ORM overhead, just fast queries with automatic pooling.
-
Axum + Reqwest — HTTP server and client built on Tokio. Production-ready with automatic keep-alive, connection pooling, and TLS support via native-tls.
-
tokio-tungstenite — WebSocket client and server with TLS support. Real-time bidirectional communication with automatic frame handling.
-
regex — Fast, safe regular expressions. Guaranteed linear time performance with no catastrophic backtracking.
-
bcrypt + sha2 — Cryptographic hashing for passwords and data integrity. Industry-standard algorithms with safe defaults.
-
thirtyfour — Browser automation via Selenium WebDriver. End-to-end testing and web scraping with a clean async API.
-
Lightweight processes — Each Nostos process is ~2KB of overhead. Spawn millions of them. They're scheduled cooperatively on the Tokio runtime, yielding at I/O boundaries.
-
Tail call optimization — Recursive functions that return a function call directly are optimized to loops, enabling elegant recursive algorithms without stack overflow.
-
Tracing garbage collector — Automatic memory management with a concurrent GC that minimizes pause times.
-
Built-in profiler — Measure execution time with
:profile exprin the REPL. Compare implementations, find bottlenecks, and watch JIT warmup effects in real time. -
Interactive debugger — Set breakpoints with
:debug fn, step through calls, inspect arguments. Debug without leaving your REPL flow.
nostos> :profile fib(35)
Result: 9227465
Time: 42.3ms
nostos> :debug factorial
nostos> factorial(5)
[BREAK] factorial(5)
Press Enter to continue, 'c' to skip remaining...Releases are automated via GitHub Actions. To create a new release:
-
Update version in
Cargo.toml(workspace root):[workspace.package] version = "0.2.10" # Bump this
-
Verify clippy passes (CI will fail otherwise):
cargo clippy -- -D warnings
-
Commit the version bump:
git add Cargo.toml git commit -m "Bump version to 0.2.10" -
Push to master:
git push origin master
-
Create and push a tag:
git tag v0.2.10 git push origin v0.2.10
-
GitHub Actions builds automatically:
- Linux x86_64
- macOS x86_64 (Intel)
- macOS aarch64 (Apple Silicon)
- Windows x86_64
- VS Code extension (.vsix)
-
Monitor the build:
gh run list --limit 1 gh run view <run-id>
The release appears at https://github.com/pegesund/nostos/releases once all builds complete.
Nostos believes that:
- Concurrency should be simple. Message passing between lightweight processes beats shared memory and locks.
- I/O should never block. Every operation yields, letting thousands of processes share a thread pool.
- Types should help, not hinder. Inference handles the common case; annotations clarify intent.
- Development should be interactive. The REPL and live reload keep you in flow.
- Batteries should be included. HTTP, PostgreSQL, JSON, WebSockets—ready when you are.
Welcome home.