fFestina
Version 0.44 · MIT Licensed · Linux · macOS · Windows

A fast, simple language that compiles to native code.

Drop the interpreter, the runtime overhead, and the boilerplate — SQLite, graphics, audio, timers, threads, and an HTTP/WebSocket server, built directly into the language.

Get started View on GitHub
table Visits {
    id:int
    place:text
}

sqlite('INSERT INTO Visits (id, place) VALUES (?, ?)', [1, 'the summit'])
arr[Visits] visits = sqlite('SELECT * FROM Visits')

text func describe(v:Visits) {
    return `Visit #${v.id}: ${v.place}`
}

log(describe(visits[0]))
shell
$ bin/festina compile visits.f -o visits && ./visits
Visit #1: the summit

No imports. No config. No main(), no ORM, no migrations. Run this today — festina.sqlite and the Visits table are created and kept in sync automatically, a real parameterized query runs against it, the whole festina runtime is one native binary, and it needs nothing Python or JIT-shaped to execute.

Why Festina


I

Native compilation, not a VM

Every Festina program compiles through LLVM to a real executable. No interpreter, no JIT warmup, no runtime shipped alongside your code.

II

A database that's just... there

Declare a table, and festina.sqlite is created, migrated, and kept in sync automatically — no ORM, no migration scripts.

III

Graphics and audio, wired in

A real canvas (drawRect, on mouseDown, paths, gradients, typed color/font values, …) and layered audio playback with channels (aud music = 'music.mp3', then music.play()), backed by Cairo/X11 and ALSA.

IV

Real threads, without the footguns

thread worker { ... } runs on its own OS thread and can't touch a global or reach into another thread at all — every message across the boundary is a deep copy, so there is no shared mutable state to race on. Send with worker.postMessage(x), answer with t.reply(y), get the answer back with .callback(fn), and scale out with thread pool[N] { ... }.

V

Automatic memory, manual override

Values are reclaimed for you — and free spritesheet / delete map.key exist for the moments you know the lifetime better than the compiler does.

VI

Readable syntax, real guarantees

Template strings, ternaries, familiar control flow — but every condition is a real bool, and everything is statically typed and checked before it runs.

VII

Slim by default

A compiled program only links what it actually uses — skip graphics and audio in your source, and the binary skips Cairo, X11, and ALSA too.

VIII

Minimal setup, by design

The fewest dependencies that get the job done, failing loudly and clearly the moment one is actually missing.

Get started

One line, on Linux/macOS/MSYS2 UCRT64 bash on Windows — it clones the source and checks/installs build dependencies for you:

$ curl -fsSL https://raw.githubusercontent.com/uraikus/festina/main/install.sh | sh

Or from an existing checkout:

$ sudo apt install clang libsqlite3-dev libcairo2-dev libx11-dev libasound2-dev pkg-config
$ bin/festina compile examples/hello.f -o hello
$ ./hello

Or skip the intermediate binary and just run it:

$ bin/festina run examples/hello.f

Not sure your machine has everything Festina needs? bin/festina doctor checks every dependency above and tells you exactly what's missing and how to install it — including whether festina itself is on PATH yet. Add --fix and it installs whatever's missing for you (via apt/Homebrew/MSYS2's pacman, whichever this machine has) and adds festina to PATH too.

That's the whole loop — see setup.md for the full dependency breakdown, packaged-binary installs, and running the test suite. macOS and Windows are supported too, each with its own CI-verified toolchain section in setup.md.

Editing .f files in Vim or Neovim? See editors/vim for syntax highlighting (Vim's own bundled Fortran filetype otherwise claims the .f extension).

A familiar language, statically typed

int count = 10
text message = 'Hello'
bool active = true

if active {
    log(`${message}, ${count} times`)
}

int func add(a:int, b:int) {
    return a + b
}

for int i = 0, i < 10, i++ {
    log(i)
}

int and float mix freely — the int side promotes to float automatically, and / always returns float. The only way back to int is Math.floor/ceil/round/trunc. Division and modulo by zero return null instead of crashing. See the API reference for the full language and standard library documentation.

Built in, not bolted on

// SQLite -- no setup code, no ORM
table People { id:int  name:text }
arr[People] people = sqlite('SELECT * FROM People')

// Graphics -- a real window, opened on first use
color brand = '#4a90d9'
font  title = 'bold 24px sans-serif'
fillStyle(brand)
changeFont(title)
drawRect(0, 0, 100, 100)
on mouseDown(x:int, y:int, button:int) { log(`pressed ${x}, ${y}`) }
on mouseUp(x:int, y:int, button:int)   { log(`released ${x}, ${y}`) }

// Audio
aud music = 'music.wav'
music.play()

// Files -- a blob is a file's bytes, and knows its own path
blob notes = 'notes.txt'
notes.write('hello')
log(notes.toText())
notes.saveCopy('notes.bak')           // img and aud save the same way

// Timers
setTimeout(showMessage, 1000)
setInterval(tick, 500)

// Regex, literal syntax
'room 42'.replace(/[0-9]+/, 'N')

// Maps -- text-keyed, object-literal-flavored
map[int] npcHealths = { 'npc1': 10, 'npc2': 15 }
npcHealths['npc1'] = 30

// Threads -- isolated, message-passing, no shared mutable state
thread doubler {
    on message(sender:thread, msg:int) { sender.reply(msg * 2) }
}
doubler.postMessage(21).callback(void (answer:int) => log(answer))

// Config straight from the environment, no extra library
text apiKey = environment.API_KEY

How it compares

Festina, Rust, Go, and Bun on the same small equivalent-logic benchmarks. Not a claim that Festina is faster in general — see benchmark.md for methodology and the full, regularly-refreshed results.

helloRun timeBuild timeBinary size
Festina1.2 ms66.3 ms1.48 MB
Rust1.2 ms70.0 ms3.77 MB
Go1.2 ms128.0 ms2.11 MB
Bun7.7 msn/an/a

fib(32)Run timeBuild timeBinary size
Festina6.2 ms71.6 ms1.48 MB
Rust6.9 ms70.6 ms3.77 MB
Go9.7 ms117.4 ms2.11 MB
Bun20.2 msn/an/a

loop_sum (100M)Run timeBuild timeBinary size
Festina370.8 ms89.0 ms1.48 MB
Rust444.0 ms70.2 ms3.77 MB
Go368.9 ms122.6 ms2.11 MB
Bun7438.6 msn/an/a

array_sum (2M)Run timeBuild timeBinary size
Festina72.3 ms90.1 ms1.48 MB
Rust69.5 ms103.8 ms3.77 MB
Go68.7 ms120.4 ms2.11 MB
Bun1866.8 msn/an/a

string_concat (15K)Run timeBuild timeBinary size
Festina2.9 ms71.9 ms1.48 MB
Rust1.1 ms94.5 ms3.77 MB
Go29.7 ms128.0 ms2.11 MB
Bun10.0 msn/an/a
2.6x faster

than an HTML <canvas>, drawing the same scene

The one benchmark here that isn't against another language — it's against the thing a 2D game would otherwise most likely be written on. 20,000 filled rectangles and 20,000 filled circles, both sides drawing offscreen into an 800×600 surface.

CanvasFrame (min)Frame (median)First frame
Festina (Cairo)37 ms39 ms24 ms
HTML <canvas> (Chromium/Skia)97 ms110 ms429 ms
MonoGame (software GL)252 ms270 ms181 ms

Circles are drawn from a cached alpha mask per radius, stamped rather than re-tessellated on every call — the reason Festina stays ahead of the browser on this workload. Festina also wins startup by more than an order of magnitude and wins on variance — the browser's frame time swings between runs, while Festina's two numbers sit right on top of each other, and for a frame budget that's not a footnote.

Read the MonoGame row with its caveat: MonoGame is a GPU framework, measured here with no GPU present — its graphics context is a software rasterizer doing in CPU cycles what real hardware does for free. On actual GPU hardware these 40,000 sprites batch into a couple of draw calls and finish in well under a millisecond. The row measures the headless, no-GPU case (CI, a build server, a container), not GPU-accelerated performance.

HTTP: Festina vs Rust vs Go vs Bun

Four servers answering the same two routes — / (plain text) and /json — load-tested with wrk. All four are held to the same connection-handling logic: single-threaded, one connection at a time. Festina's HTTP server works this way natively (see HTTP Limitations), and the Rust/Go servers here are hand-rolled raw sockets matching it rather than hyper's or net/http's own multi-threaded defaults. Every response closes the connection, matched uniformly across all four: Rust's and Go's raw-socket servers close by default, Bun's server sets Connection: close explicitly to opt out of its own native keep-alive, and since Festina supports HTTP/1.1 keep-alive by default (see Keep-alive), the load generator itself sends an explicit Connection: close request header against all four servers uniformly, which Festina honors per-request the same way any other client's explicit close request would be.

plaintext (/)Requests/secAvg latencyTransfer/sec
Festina43,6521.05 ms4.25 MB/s
Rust63,5130.69 ms5.88 MB/s
Go35,4571.30 ms3.28 MB/s
Bun41,7981.09 ms5.34 MB/s

json (/json)Requests/secAvg latencyTransfer/sec
Festina41,4821.33 ms4.83 MB/s
Rust59,8770.73 ms6.68 MB/s
Go35,4411.27 ms3.95 MB/s
Bun39,5801.15 ms5.81 MB/s

Festina clears Go on both routes and lands right around Bun's own number, with Rust ahead of all three — read that as "at matching connection-handling logic," not as a claim about what hyper/axum or net/http would report with their own concurrency and keep-alive defaults turned on, or about Bun's own native Bun.serve(). The /json route runs Festina's request struct through the same JSON-via-.toText() path every other container response already uses, so the gap between the two routes reflects that serialization cost rather than connection handling. Numbers are one client machine talking to one server process on the same machine — no network hop, no TLS — the same disclaimer every other benchmark on this page carries.

Short version: Festina holds its own against Rust and Go on compute-bound native code and lands right at them on allocation-heavy work too, comfortably outruns a JIT on cold single-shot execution, sits second on string_concat — ahead of both Go and Bun and within ~2.4x of Rust's spare-capacity-reusing String, the one gap here that's genuinely algorithmic, not an allocator artifact — and clears Go on HTTP throughput too, landing right around Bun's own number, with Rust's raw-socket implementation ahead of all three.

Project status

2411 tests, 0 failures. Under active development, but not vaporware.

The compiler frontend, LLVM codegen backend, and native C runtime are real and tested. Every specification.md language construct this project has committed to is implemented end to end, not just parsed. That includes a leak stress suite — scripts/leak_stress.sh runs mixed churn programs plus one isolation program per data type under AddressSanitizer and LeakSanitizer, because "the answers are right" and "nothing accumulates while producing them" are different claims. Native builds on Linux, macOS, and Windows, plus cross-compiling to wasm32-wasi, are all supported today. See tests/CONTRACT.md for exactly what's covered, and todo.md for what's next.

Documentation

Language specificationThe normative spec: syntax, semantics and built-ins, ECMAScript-style
API referenceFull browsable language & standard library docs
Code examplesA proxy, SQLite over WebSocket, a CAPTCHA endpoint, a weather app
setup.mdSetup & dependencies
macos.mdmacOS port
windows.mdWindows port
wasm.mdWebAssembly (wasm32-wasi)
Browser clientRunning a compiled .wasm in a browser tab
security.mdSecurity posture
benchmark.mdBenchmarks vs. Rust/Go/Bun
todo.mdRoadmap
CHANGELOG.mdVersion history
tests/CONTRACT.mdFull spec-compliance test suite
decisions.mdDesign decision log

Design philosophy

Festina intentionally favors:

Festina is not a scripting language with a compiler bolted on — it's a compiled, statically typed language designed from the start for native performance, that happens to keep its syntax approachable.