Osprey — one core, two flavors, zero compromise.

Osprey is one functional language fronted by two first-class flavors: a brace-style Default flavor for systems programmers and an offside-rule ML flavor for FP devotees. One Hindley-Milner type checker, one effect system, one fiber runtime, one LLVM backend — pick your tribe, go all in.

The same program — algebraic effects handled over fibers — in both flavors.

Default .osp
effect Logger {
  log: fn(string) -> Unit
}

fn runJob(weight) !Logger = {
  perform Logger.log("weight ${weight}")
  weight * weight
}

handle Logger
  log msg => print("[LOG] ${msg}")
in {
  let fast = spawn runJob(9)
  let slow = spawn runJob(5)
  print("scores ${await(fast)} / ${await(slow)}")
}
ML .ospml
effect Logger
  log : string => Unit

runJob weight =
  perform Logger.log "weight ${weight}"
  weight * weight

handle Logger
  log msg => print "[LOG] ${msg}"
in
  fast = spawn (runJob 9)
  slow = spawn (runJob 5)
  print "scores ${await fast} / ${await slow}"

Get Osprey Now

🍎 macOS / 🐧 Linux

brew install nimblesite/tap/osprey

🪟 Windows

scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket
scoop install osprey

🌐 Try Web Compiler

No installation needed! Compile and run Osprey code in your browser.

Open Playground

WebAssembly Data Studio

Run Osprey compiled to wasm in the site, backed by a live in-browser SQLite engine.

Open WASM Demo

🧩 VS Code Extension

Syntax highlighting, diagnostics, and a bundled compiler.

Get the Extension

🔨 Build from Source

Want the latest features? Build the compiler locally from GitHub.

View on GitHub

Two Flavors, One Core

Neither flavor is the watered-down one. Both are fully implemented today — the same programs run in either syntax, proven byte-for-byte in the test suite. Both lower to the same canonical AST, so after lowering nothing (type checker, effect checker, optimiser, codegen) can tell which flavor you wrote.

Default flavor — for systems programmers

C-style braces, fn, and f(x: a, y: b) calls with named arguments. Explicit, familiar, block-structured. Fully implemented today.

// Default flavor (.osp) — braces, fn, named args
fn double(n: int) -> int = n * 2

fn analyzeNumber(n: int) -> string = match n {
  0 => "Zero"
  42 => "The answer!"
  _ => "Something else"
}

print("double 21 = ${double(21)}")
print(analyzeNumber(42))

ML flavor — for FP devotees

Offside-rule layout (indentation, no braces), curry-by-default, whitespace application f a b, and => clauses. Terse, expression-first, ML/Haskell-shaped. Fully implemented today.

// ML flavor (.ospml) — layout, currying — RUNS TODAY
inc : int -> int
inc x = x + 1

adder : int -> int -> int
adder a b = a + b

// partial application falls straight out of currying:
addTen = adder 10
print "addTen32=${toString (addTen 32)}"

It's the same language underneath. Here is the same program in both flavors — the currying twin lowers to a machine-checked identical canonical AST.

Default flavor

// Default flavor (.osp): explicit curry
fn add(x) = fn(y) => x + y

ML flavor

// ML flavor (.ospml) — identical canonical AST:
add x y = x + y

Currying is the one honest difference: ML add x y ≡ Default explicit-curry fn add(x) = fn(y) => … at the AST. A Default multi-param fn add(x, y) is deliberately a different value.

Mix flavors in one folder

Because every flavor lowers to the same canonical AST before type checking, you pick the flavor per file — the team is never forced into one tribe. Exports are canonical signatures with stable names and order, so a Default module and an ML module are designed to sit in one folder and compile into one program.

Select the ML surface three ways (precedence: flag > marker > extension > Default):

  • The .ospml file extension
  • The --flavor ml CLI flag
  • A leading // osprey: flavor=ml marker

Per-file flavor selection ships today; cross-flavor multi-file imports are the design direction this model points at.

// One project folder, two flavors, one program:
//   project/
//     math.ospml     # ML flavor — curry-by-default module
//     app.osp        # Default flavor — braces; imports math
//
// Each file is wholly one flavor (chosen by
// extension / marker / --flavor). Both lower to the
// SAME canonical AST, so they share one type checker
// and one binary. Exports are canonical signatures,
// so a Default module and an ML module import each
// other normally.

The Vision

Osprey was born from the belief that elegance is the best defence against bugs. Too many hours are wasted debugging null pointer exceptions, handling unexpected panics, and tracking down race conditions. Osprey is different.

Osprey's type system aims to prevent entire classes of bugs at compile time, make concurrency safe by default, and keep your code maintainable for years to come.

// Effects in the type, handlers at the edge
effect Logger { log: fn(string) -> Unit }

fn greet(name: string) -> Unit !Logger =
  perform Logger.log("Hello, ${name}!")

handle Logger
  log msg => print(msg)
in greet("Alice")

Language Features

Type-Safe & Expressive

Strong static typing prevents runtime errors while keeping syntax clean and readable. Expression-bodied functions eliminate boilerplate.

  • Explicit type annotations
  • Compile-time error checking
  • Self-documenting code
  • Expression-bodied functions
fn analyzeNumber(n: int) -> string = match n {
  0 => "Zero"
  42 => "The answer!"
  _ => "Something else"
}

fn double(x: int) -> int = x * 2
fn square(x: int) -> int = x * x

// Test the functions
print("Testing functions:")
print(analyzeNumber(0))
print(analyzeNumber(42))
fn getGrade(score: int) -> string = match score {
  100 => "Perfect!"
  95 => "Excellent"
  85 => "Very Good"
  75 => "Good"
  _ => "Needs Improvement"
}

// Test the function
print("Grade for 100: ${getGrade(100)}")
print("Grade for 95: ${getGrade(95)}")

Pattern Matching

Elegant pattern matching with exhaustiveness checking ensures you handle all cases safely.

  • Exhaustive pattern checking
  • Safe value destructuring
  • Clear conditional logic
  • No forgotten edge cases

String Interpolation

Built-in string interpolation with full expression support makes output formatting clean and readable.

  • Expression interpolation
  • Type-safe formatting
  • Readable string templates
  • No manual concatenation
// String interpolation example
let name = "Alice"
let age = 25
let score = 95

print("Hello ${name}!")
print("Next year you'll be ${age + 1}")
print("Double score: ${score * 2}")
print("${name} (${age}) scored ${score}/100")
// Functional Programming Example
fn double(x: int) -> int = x * 2
fn square(x: int) -> int = x * x

// Clean data transformations
5 |> double |> square |> print

// Range operations with forEach
print("Range operations:")
range(1, 10) |> forEach(print)

Functional Programming

Pipe operators and functional iterators create elegant data processing pipelines.

  • Pipe operator for data flow
  • Functional iterators
  • Immutable by default
  • Clean transformation chains

Algebraic Effects

Osprey is the world's first language with 100% compile-time effect safety. Side effects live in a function's type, and an unhandled effect is a compilation error — never a runtime surprise.

Effects in the type, handlers at the edge

Declare an effect, perform its operations, then swap behaviour by changing the handle … in block — no globals, no dependency injection framework. The same logic runs against a production handler, a test double, or a silent one.

  • Unhandled effects fail at compile time
  • Handlers are first-class and composable
  • Nested handlers override outer ones
  • Pairs naturally with fibers, HTTP, and TUIs
effect Logger {
  log: fn(string) -> Unit
}

fn greet(name: string) -> Unit !Logger =
  perform Logger.log("Hello, ${name}!")

// Production: write to stdout
handle Logger
  log msg => print(msg)
in greet("Alice")

// Test: stay silent — same code, new handler
handle Logger
  log msg => 0
in greet("Bob")

Persistent Collections

// List — 32-way bitmapped vector trie
let xs = listAppend(listAppend(List(), 10), 20)
let ys = listAppend(xs, 99)

listLength(xs)              // 2  — xs untouched
listLength(ys)              // 3
xs + ys                     // O(n+m) concat

// Map — Hash Array Mapped Trie (HAMT)
let m = mapSet(Map(), "alice", 25)
let m2 = mapSet(m, "bob", 30)

mapContains(m,  "bob")      // false
mapContains(m2, "bob")      // true
m + m2                      // right-biased union

Built on FP foundations

Immutable List<T> and Map<K, V> backed by structural sharing, so "modifying" a collection never copies the whole thing — old versions stay valid in O(1) extra space relative to the change.

  • List: 32-way bitmapped vector trie (Bagwell 2000; Clojure's PersistentVector). O(log₃₂ n) point ops.
  • Map: Hash Array Mapped Trie with bitmap-packed children. O(log₃₂ n) expected for lookup, insert and remove.
  • + operator dispatches to listConcat for lists and mapMerge (right-biased) for maps.
  • Backed by 33 C-level assertions and 12 byte-exact end-to-end test programs.

Compile-Time Effect Safety

Every side effect is tracked in the type system. Unhandled effects are caught by the compiler.

Memory Safe

Strong static typing and immutable data prevent buffer overflows and data races.

Compiles to LLVM

Stream fusion and structural sharing lower high-level code to efficient native binaries.

Real-World Examples

Fiber Concurrency

fn work(n: int) -> int = n * n

// Spawn lightweight fibers, await out of order
let a = spawn work(6)
let b = spawn work(7)

let rb = await(b)
let ra = await(a)
print("a=${ra}, b=${rb}")

// Message passing over a channel
let ch = Channel(1)
send(ch, 42)
print("got ${recv(ch)}")

C Interop & SQLite

// @link: sqlite3

// Bind C functions with typed signatures
extern fn sqlite3_open(path: string, ppDb: Ptr) -> int
extern fn osprey_ffi_cell() -> Ptr
extern fn osprey_ffi_deref(cell: Ptr) -> Ptr

// Ptr carries the opaque C handle — no
// arithmetic, no dereference, handles only
let cell = osprey_ffi_cell()
let rc = sqlite3_open("app.db", cell)
let db = osprey_ffi_deref(cell)
print("sqlite open rc = ${rc}")

HTTPS Client

// TLS via OpenSSL — https just works
let client = httpCreateClient("https://httpbin.org", 5000)

let status = httpGet(client, "/get", "")
print("status: ${status}")

let closed = httpCloseClient(client)
print("client closed")

Core Philosophy

Referential Transparency

Functions return the same output for the same input. Side effects are explicit and tracked by the type system.

Immutability by Default

Data is immutable unless explicitly marked mutable, making it safe to share across fibers.

Explicit Error Handling

No hidden exceptions or panics. All failures return Result types you must handle.

Zero-Cost Abstractions

High-level features — effects, iterators, persistent collections — compile to efficient native code.

How Osprey Is Different

Feature Traditional Osprey
Side Effects Untracked, implicit In the type; unhandled effects are a compile error
Error Handling Exceptions, panics Result types
Null Safety Null pointer exceptions Option types only
Concurrency Shared mutable state Fiber-isolated, message-passing
Type Safety Runtime type errors possible Compile-time prevention via Hindley-Milner inference
Memory Management Manual memory or garbage collection Memory-safe, no GC pauses
Syntax One syntax, take it or leave it Two flavors — braces or layout — sharing one core; mix them per file in one folder

…and it performs: Osprey matches C and Rust on CPU, uses C-level memory, and beats OCaml and Haskell across an 18-case cross-language benchmark suite →

Help Build the Future of Programming

Anyone can contribute. AI assistants like Claude + Cursor make compiler development accessible to regular developers. No CS degree required.