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.
Drop the interpreter, the runtime overhead, and the boilerplate — SQLite, graphics, audio, timers, threads, and an HTTP/WebSocket server, built directly into the language.
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]))
$ 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.
Every Festina program compiles through LLVM to a real executable. No interpreter, no JIT warmup, no runtime shipped alongside your code.
Declare a table, and festina.sqlite is created, migrated, and kept in sync automatically — no ORM, no migration scripts.
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.
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] { ... }.
Values are reclaimed for you — and free spritesheet / delete map.key exist for the moments you know the lifetime better than the compiler does.
Template strings, ternaries, familiar control flow — but every condition is a real bool, and everything is statically typed and checked before it runs.
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.
The fewest dependencies that get the job done, failing loudly and clearly the moment one is actually missing.
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).
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.
// 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
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.
| hello | Run time | Build time | Binary size |
|---|---|---|---|
| Festina | 1.2 ms | 66.3 ms | 1.48 MB |
| Rust | 1.2 ms | 70.0 ms | 3.77 MB |
| Go | 1.2 ms | 128.0 ms | 2.11 MB |
| Bun | 7.7 ms | n/a | n/a |
| fib(32) | Run time | Build time | Binary size |
|---|---|---|---|
| Festina | 6.2 ms | 71.6 ms | 1.48 MB |
| Rust | 6.9 ms | 70.6 ms | 3.77 MB |
| Go | 9.7 ms | 117.4 ms | 2.11 MB |
| Bun | 20.2 ms | n/a | n/a |
| loop_sum (100M) | Run time | Build time | Binary size |
|---|---|---|---|
| Festina | 370.8 ms | 89.0 ms | 1.48 MB |
| Rust | 444.0 ms | 70.2 ms | 3.77 MB |
| Go | 368.9 ms | 122.6 ms | 2.11 MB |
| Bun | 7438.6 ms | n/a | n/a |
| array_sum (2M) | Run time | Build time | Binary size |
|---|---|---|---|
| Festina | 72.3 ms | 90.1 ms | 1.48 MB |
| Rust | 69.5 ms | 103.8 ms | 3.77 MB |
| Go | 68.7 ms | 120.4 ms | 2.11 MB |
| Bun | 1866.8 ms | n/a | n/a |
| string_concat (15K) | Run time | Build time | Binary size |
|---|---|---|---|
| Festina | 2.9 ms | 71.9 ms | 1.48 MB |
| Rust | 1.1 ms | 94.5 ms | 3.77 MB |
| Go | 29.7 ms | 128.0 ms | 2.11 MB |
| Bun | 10.0 ms | n/a | n/a |
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.
| Canvas | Frame (min) | Frame (median) | First frame |
|---|---|---|---|
| Festina (Cairo) | 37 ms | 39 ms | 24 ms |
| HTML <canvas> (Chromium/Skia) | 97 ms | 110 ms | 429 ms |
| MonoGame (software GL) | 252 ms | 270 ms | 181 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.
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/sec | Avg latency | Transfer/sec |
|---|---|---|---|
| Festina | 43,652 | 1.05 ms | 4.25 MB/s |
| Rust | 63,513 | 0.69 ms | 5.88 MB/s |
| Go | 35,457 | 1.30 ms | 3.28 MB/s |
| Bun | 41,798 | 1.09 ms | 5.34 MB/s |
| json (/json) | Requests/sec | Avg latency | Transfer/sec |
|---|---|---|---|
| Festina | 41,482 | 1.33 ms | 4.83 MB/s |
| Rust | 59,877 | 0.73 ms | 6.68 MB/s |
| Go | 35,441 | 1.27 ms | 3.95 MB/s |
| Bun | 39,580 | 1.15 ms | 5.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.
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.
| Language specification | The normative spec: syntax, semantics and built-ins, ECMAScript-style |
| API reference | Full browsable language & standard library docs |
| Code examples | A proxy, SQLite over WebSocket, a CAPTCHA endpoint, a weather app |
| setup.md | Setup & dependencies |
| macos.md | macOS port |
| windows.md | Windows port |
| wasm.md | WebAssembly (wasm32-wasi) |
| Browser client | Running a compiled .wasm in a browser tab |
| security.md | Security posture |
| benchmark.md | Benchmarks vs. Rust/Go/Bun |
| todo.md | Roadmap |
| CHANGELOG.md | Version history |
| tests/CONTRACT.md | Full spec-compliance test suite |
| decisions.md | Design decision log |
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.