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.
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)}")
}
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 PlaygroundWebAssembly Data Studio
Run Osprey compiled to wasm in the site, backed by a live in-browser SQLite engine.
Open WASM Demo🔨 Build from Source
Want the latest features? Build the compiler locally from GitHub.
View on GitHubTwo 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
.ospmlfile extension - The
--flavor mlCLI flag - A leading
// osprey: flavor=mlmarker
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 tolistConcatfor lists andmapMerge(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.