Skip to content

pegesund/nostos

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2,689 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nostos

MSRV

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.

⚠️ Early Stage Software: It is early days and version 1.0 is not ready. For brave developers and early adopters only.

📚 Full Tutorial: heynostos.tech


Installation

macOS (Homebrew)

brew tap pegesund/nostos
brew install nostos

macOS (Manual)

# 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-lsp

Linux

curl -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/

Windows

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 PATH

Hello World

echo 'main() = println("Hello, Nostos!")' > hello.nos
nostos hello.nos

First run extracts stdlib and builds cache (~1s). Subsequent runs: ~0.1s.

VS Code Extension

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

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


What Makes Nostos Different

Code That Reads Like Poetry

# 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(", ")

Pattern Matching Rocks

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.

Functional First, Pragmatism Always

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.

Non-Blocking by Default

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")
}

Living Development Environment

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"]

VS Code That Understands Your Code

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

Some Batteries Included

PostgreSQL

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

Reactive Web (RWeb)

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)

HTTP Server & Client

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)

TCP Sockets

Low-level when you need it:

server = Tcp.listen(9000)
client = Tcp.accept(server)
Tcp.send(client, "Hello!")
message = Tcp.receive(client)

WebSockets

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"),
])

Cryptography

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

Regular Expressions

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"]

File I/O

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)

JSON

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 })

HTML Templating

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.

Logging

Structured logging with levels:

import logging

log.info("Server started", port: 8080)
log.error("Connection failed", error: err, retry: 3)

FFI: Extend With Native Code

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" }

Available Extensions

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

Nostlets (TUI Plugins)

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

Modules and Imports

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}

Import Conflict Detection

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)

The Type System Stays Out of Your Way

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)
}

Traits for Polymorphism

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(" ")

Supertraits Build Hierarchies

trait Printable
    toString(self) -> String
end

trait Serializable: Printable  # Requires Printable
    toJson(self) -> String
end

Getting Started

# 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-*.vsix

Important

To build Nostos from source, you need Rust 1.85 or newer.


Architecture

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

Under the Hood

  • 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 expr in 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...

Creating a Release

Releases are automated via GitHub Actions. To create a new release:

  1. Update version in Cargo.toml (workspace root):

    [workspace.package]
    version = "0.2.10"  # Bump this
  2. Verify clippy passes (CI will fail otherwise):

    cargo clippy -- -D warnings
  3. Commit the version bump:

    git add Cargo.toml
    git commit -m "Bump version to 0.2.10"
  4. Push to master:

    git push origin master
  5. Create and push a tag:

    git tag v0.2.10
    git push origin v0.2.10
  6. GitHub Actions builds automatically:

    • Linux x86_64
    • macOS x86_64 (Intel)
    • macOS aarch64 (Apple Silicon)
    • Windows x86_64
    • VS Code extension (.vsix)
  7. 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.


Philosophy

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.

About

Typed, flexible programming language with a living repl

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors