Pony — Reference

Source: https://www.ponylang.io/

Pony

  • Created: 2014 by Sylvan Clebsch (PhD work at Imperial College London); first public release 2015
  • Latest stable: ponyc 0.63.4 (2026-05-02)
  • Status: Pre-1.0. Quote from ponylang.io: “Pony hasn’t reached version 1.0. Breaking changes still happen. The pool of ready-to-use libraries is small.” Stewarded by the Pony Foundation (501(c)(3)) since 2017
  • Paradigms: Object-oriented + actor model + capabilities-secure
  • Typing: Static, strong, structural (interfaces match by shape) + nominal (traits), generics, algebraic data types, reference capabilities (the headline feature)
  • Memory: Per-actor garbage collection (no stop-the-world); inter-actor garbage tracking via ORCA (Ownership and Reference Counting for Actors); zero-copy message passing for iso types
  • Compilation: AOT via LLVM (own front-end ponyc); native code only; no VM. Targets x86_64 / arm64 on Linux / macOS / Windows / FreeBSD; new Ubuntu 26.04 builds added in 0.63.4
  • Primary domains: High-throughput concurrent servers, low-latency systems (finance, telco), exploratory data systems, anywhere data races would be expensive
  • Official docs: https://www.ponylang.io/ · Tutorial: https://tutorial.ponylang.io/ · Stdlib: https://stdlib.ponylang.io/

At a glance

Pony’s killer feature: the type system mathematically proves data-race freedom at compile time, without locks, atomics, or borrow checking. It does this via reference capabilities — six modifiers (iso, val, ref, box, tag, trn) that annotate every reference and govern who can read/write/share. Combine that with a true actor model (lightweight asynchronous actors with per-actor heaps) and you get a language where you can write a million-actor server and the compiler guarantees no races, no deadlocks (no locks exist), and no nulls. Pony came out of Sylvan Clebsch’s PhD thesis at Imperial College, was acquired by Causality (he later joined Microsoft Research), and is now stewarded by the community Pony Foundation. Pre-1.0 with small ecosystem but a deeply serious language.

Getting started

Install via ponyup (the official toolchain manager):

# Linux/macOS
sh -c "$(curl --proto '=https' --tlsv1.2 -fsSL https://github.com/ponylang/ponyup/releases/latest/download/ponyup-init.sh)"
ponyup update ponyc release        # latest stable (0.63.4)
ponyup default ponyc-release-0.63.4
ponyc --version

Windows: download MSI from https://github.com/ponylang/ponyc/releases or use ponyup via WSL.

Hello world (hello/main.pony):

actor Main
  new create(env: Env) =>
    env.out.print("Hello, world!")

Build + run:

ponyc hello/         # produces ./hello/hello binary
./hello/hello

Project layout: Directory = package. Each .pony file in a dir contributes to that dir’s package. The “main” package must define an actor Main with new create(env: Env).

myapp/
├── main.pony           # actor Main
├── widget/
│   └── widget.pony     # package widget
└── corral.json         # if using corral for deps

Build/package tool:

  • ponyc is the compiler. ponyc <dir> compiles. ponyc --debug, --release (default), --paths /extra/lib, --bin-name name, --output-bin <dir>. The Pony build is whole-program and slow (no incremental linking).
  • corral is the package manager (separate tool, install via ponyup update corral release). corral init, corral add github.com/ponylang/http_server.git --version 0.7.0, corral fetch, corral run -- ponyc. Dependencies live in _corral/.
  • ponytest is the built-in test framework — write a _test.pony file, define test classes implementing UnitTest, and run as a normal Pony program.

REPL: None. Pony has no REPL; the compile/run cycle is the workflow.

Basics

Types & literals:

  • Numeric: U8 U16 U32 U64 U128 USize ULong, I8…I128 ISize ILong, F32 F64. No implicit numeric conversionslet x: U64 = 1 works (literal coerces) but (a: U32) + (b: U64) does not
  • Bool (true/false), String (UTF-8 by convention but byte-addressable), None (the unit-like type — used where other languages use null/unit)
  • Collections in stdlib: Array[T], List[T], Map[K, V], Set[T]
  • Tuples: (U64, String). Named via (x: U64, name: String). Created with (1, "hi"). Destructured with let (a, b) = pair

Variables & scoping: let immutable, var mutable, embed for embedded fields (no indirection):

let x: U64 = 42       // immutable
var y: U64 = 0        // mutable
y = y + 1

All variables must be definitively assigned before use (compiler-enforced). Type annotations required where inference can’t determine; many local lets infer.

Control flow: Expression-oriented. Each branch must produce the same type (or None):

let s = if x > 0 then "positive" else "non-positive" end
match value
| 0 => "zero"
| let n: U64 if n > 100 => "big"
| let n: U64 => "small"
end
while iter.has_next() do iter.next() end
for item in collection.values() do ... end
repeat ... until cond end

Pattern matching with match is exhaustive across union types.

Functions (methods): Defined inside classes/actors/primitives:

  • fun — synchronous method (callable on object)
  • bebehavior, an asynchronous actor method (returns immediately, message-queued)
  • new — constructor
  • fun ref, fun box, fun iso — capability of this for the call
class Counter
  var n: U64 = 0
  fun ref increment() => n = n + 1
  fun current(): U64 => n

Strings: String is mutable-by-default (capability-controlled). String iso for transferable, String val for shared-immutable. Concatenation: s1 + s2 (allocates). s.size() is bytes, not codepoints. Iterate codepoints via s.runes().

Collections: Array[T] (contiguous), List[T] (linked), Map[K, V] (hash), Set[T], RingBuffer[T], Heap[T]. All in collections package: use "collections".

Intermediate

Type system depth:

  • Classes (class Foo) — heap-allocated objects with mutable state, accessed via reference capabilities
  • Actors (actor Foo) — like classes but with their own heap, mailbox, and behaviors. Only one behavior runs at a time per actor — internally race-free
  • Primitives (primitive Foo) — single, stateless instance (like a singleton/enum-tag); used for tagged unions and sentinels. primitive None is THE canonical “no value”
  • Traits (trait Foo) — nominal interface (must be is-declared); compile-time dispatch
  • Interfaces (interface Foo) — structural; types match by having matching method shapes (no declaration needed)
  • Generics: class Stack[T], fun first[A: Comparable[A]](xs: Array[A]): A?. Constraints T: SomeTrait. Reified at compile time
  • Algebraic data types via union (|) and intersection (&): (String | None) is “string or none”. (Comparable & Stringable) is “must be both”

Modules (packages): Directory = package. use "collections" imports a package. use "package_name" if windows for OS-conditional. use "lib:foo" declares a C library to link.

Error handling — partial functions: Functions/expressions that may fail are marked with ?:

fun divide(a: U64, b: U64): U64 ? =>
  if b == 0 then error end
  a / b

Calling: try divide(10, 2) else 0 end. The error keyword raises; try ... else ... end catches. Errors carry no value — they’re a single sentinel. To carry data, return a union like (U64 | DivError) and match.

Concurrency primitives — actors and behaviors:

  • An actor runs sequentially internally, but actors run in parallel
  • Behaviors (be name(...)) are async; calling actor.behavior() enqueues a message and returns immediately
  • Only tag, val, or iso references can be sent across actor boundaries (capability-enforced — anything else won’t compile)
  • No futures/promises in the language; the pattern is “send a behavior to me when done”
  • The runtime scheduler is M:N — millions of actors run on a fixed pool of OS threads

I/O: All I/O is async via behaviors. Stdlib provides files, net, time (timers), process (subprocess management), signals. The HTTP server lives in the third-party http_server package. The Env object passed to Main.create carries out, err, input, and root capabilities for ambient authority.

Stdlib highlights: builtin (Array, String, primitives), collections (Map, Set, List), files, net, net/ssl, time, random, format, strings, regex, crypto, serialise (built-in serialization), term (TTY handling), bureaucracy (Registrar pattern), cli (arg parsing), ponytest, pony_test, process, signals, ponybench.

Advanced

Memory model — per-actor heap + ORCA:

  • Each actor owns its own heap; objects are allocated in the actor that creates them
  • Per-actor GC runs only when that actor is idle (between behavior invocations) — no stop-the-world, no global pause
  • Cross-actor object lifetimes are tracked by ORCA (Ownership and Reference Counting for Actors): an asynchronous, distributed reference-counting protocol that handles cycles between actors via a separate cycle detector
  • For sent objects:
    • iso transfers ownership (zero-copy; sender loses access)
    • val shares immutably (zero-copy; refcounted by ORCA)
    • tag shares an identity-only handle (no read access)
  • The result: a language where async message passing has no data races AND no GC pause AND can be zero-copy

Concurrency deep dive:

  • The runtime uses a work-stealing scheduler with one queue per OS thread
  • Actor mailboxes are MPSC queues (lock-free)
  • Backpressure: when an actor’s mailbox grows beyond a threshold, sending actors are scheduled less, throttling producers — built in
  • Memory ordering on aarch64 was hardened in 0.63.4
  • The runtime is the same as in Sylvan Clebsch’s thesis (“The Pony Programming Language”, 2017) — peer-reviewed correctness

FFI: use "lib:foo" declares a library to link. Then declare C functions with @:

use "lib:m"
primitive Math
  fun sqrt(x: F64): F64 => @sqrt(x)

Pointers via Pointer[U8], NullablePointer[T]. The @func[ReturnType](args) syntax explicitly calls a C function. Use compile_intrinsic for hot paths.

Reflection: Very limited — Pony favors compile-time everything. Stringable interface for runtime printing. serialise package allows binary serialization but requires opt-in via Serialisable capability. No runtime type tags beyond what unions provide via match.

Performance tools: ponyc --pic --debug for debuggable builds; --llvm-args=... to pass through to LLVM. Runtime stats: --ponyminthreads, --ponymaxthreads, --ponynoblock (disable blocking detection), --ponyversion. The ponybench package is a microbenchmark framework. External: perf, samply, Tracy via custom FFI bindings.

God mode

Reference capabilities — the heart of Pony:

CapMeaningRead?Write?Aliasable?Sendable?
isoIsolated — sole referenceYesYesNO (1 ref only)YES (transfers ownership)
trnTransition — write here, read-only elsewhereYesYesRead-only aliasesNO (convert to val first)
refReference — normal mutable, single-actorYesYesYes (within actor)NO
valValue — globally immutableYesNOYes (anywhere)YES (refcounted by ORCA)
boxRead-only view (matches val OR ref)YesNOYes (within actor)NO
tagIdentity onlyNONOYes (anywhere)YES (zero-copy)

Every type/parameter/field/return/local has a capability. String alone is shorthand for String ref. Capabilities are checked at compile time; passing a ref where val is expected = error.

recover blocks for capability promotion: Inside a recover block, you build up a ref value that the compiler can prove is uniquely owned at block exit, then promotes it to iso (or val):

let s: String iso = recover String.create(20) .> append("Hello") end

Inside the recover, String.create returns ref, but because the compiler proves no external alias escapes, the result is iso-promoted. This is how you build complex isolated structures from non-isolated APIs.

Behaviors vs functions:

  • fun — synchronous; returns a value
  • be — asynchronous behavior; ALWAYS returns immediately; result type is implicit None. Only callable on actors. Sender enqueues; receiver runs later
  • Inside a behavior, calls to your own fun are synchronous; calls to another actor’s be enqueue
  • Behaviors must take only sendable parameters (iso, val, tag)

Actor model + cap system = compile-time data-race freedom: The proof: an actor can only access objects via its references. To send an object to another actor, you must give up your reference (iso) or send something globally immutable (val) or only an identity (tag). The compiler enforces these rules. Therefore: no two actors ever simultaneously have write access to the same object. Therefore: no data races. This is checked at compile time, no runtime cost.

ORCA distributed GC: The reference-counting protocol that tracks val/iso lifetimes between actors. An actor sending a val increments the destination’s local refcount-token; sending back the count adjustment is folded into normal message traffic. Cycles between actors are detected by a dedicated cycle detector that observes mailbox states. Result: cross-actor garbage is collected without any synchronous coordination.

FFI surface:

use "lib:ssl"
use @SSL_new[Pointer[_SSL] tag](ctx: Pointer[_SSLContext] tag)

The use @name form declares a C function with a Pony-typed signature; the @name(args) calls it. Use compile_error "msg" to abort compilation conditionally.

Academic backstory: Sylvan Clebsch’s PhD thesis “The Pony Programming Language” (2017, Imperial College) is the formal reference. Co-authored papers with Sophia Drossopoulou on the type system soundness (“Deny capabilities for safe, fast actors”). Real proofs, not hand-waving — Pony is one of the few languages whose memory/type model has been formally verified for data-race freedom.

Structural typing — interfaces match by shape:

interface Stringable
  fun string(): String
 
class Foo
  fun string(): String => "foo"
 
let things: Array[Stringable] = [Foo]      // Foo automatically matches

No implements Stringable required — the compiler checks shape. Traits (trait) are nominal alternatives when you want explicit declarations.

Idioms & style

  • Naming: lower_snake_case for fields/locals/methods/packages, UpperCamelCase for classes/actors/primitives/traits/interfaces, _leading_underscore is private (package-local)
  • Capability defaults: types default to ref when unspecified (mutable, actor-local). Annotate when you want to share or transfer
  • Formatter/linter: No official formatter (this is a noted gap). Community uses tab indentation per the style guide. Linter: ponyc --pass=ir --output=/dev/null for full type-check; ponyc --verify for additional checks
  • Idiomatic patterns:
    • Return iso from constructors when the caller will send the object across actors
    • Use val for shared-immutable data (config, lookup tables)
    • Use tag actor refs as IDs to communicate “send to this actor without reading its state”
    • Build complex objects inside recover blocks to get an iso out
    • Prefer primitive None over null/option types
    • Use partial functions (?) for “this should never fail given preconditions”; use union types for “this regularly fails with a reason”
  • Expert review focus: correct caps on every public method (especially fun ref vs fun box), no needless recover (compiler will reject if it can’t prove uniqueness, but unnecessary recoveries clutter code), correct sendability of behavior args, proper ? propagation, no actor leaks (an actor with no references is GC’d; one held only by tag keeps running)

Ecosystem

Small but high-quality. Most production Pony code is private/internal.

DomainLibrary
HTTP serverhttp_server, http_client (ponylang official)
TLSnet/ssl (built-in via OpenSSL FFI)
JSONjson (built-in package)
Logginglogger (ponylang official)
CLIcli (built-in, getopt-style)
Configini (built-in for INI files)
Testponytest / pony_test (built-in framework)
Benchponybench (built-in microbenchmarks)
Buildponyc + corral (deps) + ponyup (toolchain)
LSPpony-lsp (in active development; new features in 0.63.4 — type hierarchy, signature help, inlay hints)
EditorVS Code extension, Sublime, Vim — built on pony-lsp

Notable users: Causality (Sylvan Clebsch’s company, acquired the IP), WallarooLabs (Wallaroo stream processor, large open-source Pony codebase, now archived but historically the canonical large Pony app), Microsoft (Sylvan worked on Verona partially inspired by Pony), various financial firms running internal services.

Gotchas

  • Pre-1.0: Breaking changes between minor versions. Pin a ponyc version with ponyup and a corral.json lockfile
  • The cap system has a steep learning curve — expect days/weeks to internalize iso/val/ref/box/tag/trn. The compiler error messages have improved but can still cite “incompatible capabilities” without explaining why intuitively
  • recover blocks are restrictive — only certain operations are allowed inside (no calls to outside-scope ref methods that take/return non-sendable). Refactor when stuck
  • No null, but no exceptions either — partial functions (?) carry NO error data. To carry an error reason, use a union type (Result | Error) and match
  • Compile times are slow — whole-program LLVM compilation. Incremental compilation does not exist
  • No REPL, no scripting mode — the compile/run cycle is the workflow
  • No formatter — community gap. Style guide is hand-enforced via review
  • Actor leaks are subtle — an actor only held by tag references stays alive. To stop an actor, you typically have it process a “shutdown” message and stop scheduling new work
  • String.size() is bytes, not codepoints — same gotcha as Rust/Go
  • Numeric coercion is strict — explicit .u64() conversions everywhere; compiler errors on mixed-width arithmetic
  • embed vs reference fields: embed lays the field out inline (no indirection, but requires the type be sized). Subtle perf and lifetime implications
  • Behaviors can’t return values — pattern is “send a callback actor reference and have me invoke a behavior on you when done”
  • Backpressure can starve actors — if an actor’s mailbox grows huge, the scheduler de-prioritizes its senders. Good in steady state, can cause unexpected latency in bursts
  • Distributed Pony is research, not production — there’s no built-in cross-machine actor protocol; multi-machine deployments roll their own (often via stdlib net)

Citations