a small language that compiles to native code and is written in itself. python-shaped syntax, result types instead of exceptions, reference counting instead of a garbage collector. the compiler is about 23,000 lines of pith; the standard library is another 26,000, and it goes deeper than you'd guess — the TLS 1.3 client, the regex engine, and gzip compression are all pith source you can read.
enum Shape:
Circle(Float)
Rect(Float, Float)
fn area(s: Shape) -> Float:
match s:
Shape.Circle(r) => 3.14159 * r * r
Shape.Rect(w, h) => w * h
fn parse_radius(text: String) -> Float!:
n := parse_int(text)!
if n <= 0:
fail "radius must be positive"
return n.to_float()
fn main() -> Int!:
shapes := [Shape.Circle(2.0), Shape.Rect(3.0, 4.0)]
mut total := 0.0
for s in shapes:
total = total + area(s)
print("total area: {total:.2}")
r := parse_radius("3")!
print("one more: {area(Shape.Circle(r)):.2}")
fallback := parse_radius("") catch 1.0
print("fallback radius: {fallback}")
return 0
that's most of the flavor in one file: enums carry payloads and match
destructures them, errors are values that propagate with ! and get
absorbed with catch, and interpolation takes format specs
({total:.2}). no null, no exceptions, no panics in safe code.
mostly to answer a question: how much of a real toolchain can one small language carry on its own back? the answer so far:
- it compiles itself. the lexer, parser, type checker, formatter, linter, and ir emitter are pith. the build reaches a fixed point — the compiler compiles a compiler that compiles an identical compiler — and ci verifies that on every merge.
- errors are values.
T!is a result,T?is an optional,failproduces an error,!propagates it,catchgives a fallback. functions that can't fail can't lie about it. - memory is reference-counted by the compiler. retain/release pairs are emitted at compile time following a borrowed-by-default discipline (docs/ownership.md). no gc pauses; also no cycle collector yet — cycles leak, and the limitations page says so.
- the stdlib doesn't shell out. TLS 1.3, http, websockets, json, toml, csv, tar, zip, gzip (interops with system gzip in both directions), sha-256, a linear-time regex engine — written in pith. when the language couldn't express something well, that became a language feature to build rather than a binding to hide behind.
- tooling is machine-readable. check, lint, and doc all take
--json; every diagnostic has a stable code (docs/errors.md);fmthas one canonical style. scripts and editors get the same interface people do. - builds are quick. the benchmark app compiles in about 2 seconds, the entire self-hosted compiler in under 7.
the honest list lives in docs/limitations.md and stays current. highlights you should know before diving in: reference cycles and closure environments leak (no cycle collector), removed container elements aren't reclaimed until the container dies, tls is 1.3-only with no 1.2 fallback, and performance sits between go and rust on service-shaped work but behind go on heavy string churn — measured numbers, not vibes, in docs/performance.md.
you need rust/cargo for the native backend.
cargo build --release
./target/release/pith run examples/hello.pith
make self-host
./self-host/pith_main check examples/hello.pith
the first build compiles the ir driver from the tracked seed at
self-host/bootstrap/ir_driver.ir — the compiler is written in pith,
so the seed breaks the circular dependency. after that everything
rebuilds from source.
string errors are fine until callers need to inspect the failure.
T!E carries a typed payload:
struct ParseError:
message: String
fn parse_port(text: String) -> Int!ParseError:
if text == "":
fail ParseError{"empty"}
return 8080
fn main() -> Int!:
print((parse_port("8080") catch 9000).to_string())
print((parse_port("") catch 9000).to_string())
return 0
structs (heap references with generated destructors), enums with
payloads, interfaces with generic bounds, impl blocks with
per-instantiation specialization, closures with capture, tuples,
type aliases. mut is required for reassignment. for drives
collections, strings, ranges (0..n, 0..=n), and anything with a
fn next() -> T? method. match checks exhaustiveness. generics
monomorphize. concurrency is structured: spawn/await with Task[T],
channels, select, mutexes, wait groups, contexts, timers.
the full grammar is docs/grammar.ebnf; the tour with idioms is docs/idiomatic_pith.md.
62 modules, ~26,000 lines, all pith. the areas: io and filesystems (fs, glob, path, process), networking (tcp, dns, url, http, websocket, tls, sse), data (json, toml, csv, config, table), bytes and crypto (hash, checksum, encoding, crypto, bits, binary), compression and archives (gzip/zlib, tar, zip), text (regex, scanner, fmt), app plumbing (log, metrics, cli, testing, diagnostic, time, datetime, rand, uuid, math), and lazy iterators (std.iter).
four real programs live in tools/ and run against golden output in
ci: a static site generator (toml config, markdown subset, layouts,
feeds), a web log analyzer (combined format, gzip input, csv export),
a json api client that talks to a pith http server — both ends of the
conversation in pith — and a worker pool running a jobs file through
spawn and channels. they are the honest answer to "what does
non-trivial pith look like."
pith run <file> compile and run
pith build <file> compile to a native binary
pith test <file> run test declarations
pith check <file> type check (--json for machine output)
pith fmt <file> format (--check to verify)
pith lint <file> conventions (--json available)
pith doc <file> docs (--check verifies pub items documented)
pith lex / parse token stream / ast, for the curious
pith package <cmd> check/test/lint/doc/deps/lock/install/inspect
local packages resolve through pith.toml:
[dependencies]
greeter = { path = "../greeter" }from greeter import greet
make build # native backend
make self-host # the compiler, compiled by itself
make test # the full suite
make run-examples # 89 example programs against snapshots
make status-audit # current corpus and size numbers
self-host/ the compiler, in pith (~23,000 lines): lexer, parser,
checker, formatter, linter, docgen, ir emitter
cranelift/ the native backend, in rust (~12,500 lines): ir
consumer, codegen, runtime (arc, collections, net)
std/ the standard library (62 modules)
examples/ 89 runnable programs with expected output
tests/ regression, invalid-program, and golden fixtures
tools/ the four real programs
docs/ architecture, ownership, errors, grammar, limitations
start with docs/architecture.md if you want to change the compiler, and docs/contributing.md for the development loop.
.pith files map to python highlighting via .gitattributes as a
stopgap. the textmate grammar and the linguist submission checklist
live in tooling/highlighting/.
mit