Pekoscript
A statically typed language, and the compiler and tooling that make it run.
What it is
01Pekoscript is a statically typed language I designed and wrote the compiler for. It is the language Peko apps are written in, so it exists because I needed it to, but the compiler is the part I actually care about.
The toolchain is four Rust crates and comes to roughly 89,000 lines. peko-core is the front end and static analyzer at about 34,000 lines. peko-cli is the driver at about 31,000. peko-llvm is codegen and linking at about 18,000. peko-lsp is the language server at about 6,000.
If someone asks what I can actually do, this is what I point at.
/// A DOM target: a selector plus an anchor point given as
/// percentages of the element box.
[public] class Target {
[private] selector: string
[private] x: number
[private] y: number
}The front end
02peko-core takes source text and turns it into typed abstract syntax trees, then runs type checking and reachability over them and produces diagnostics describing whatever it found. It generates no code and has no CLI of its own. Those live in sibling crates, which keeps the analysis usable by more than one consumer.
That separation is why the language server works the way it does. peko-lsp reuses the same analysis engine, so the errors an editor shows you are the exact errors the compiler produces. There is no second, approximate implementation drifting away from the real one.
The crate also holds the formatter, the package resolver and lockfile handling, target configuration, and a simulator.
.peko source
│
▼
peko-core lexing, parsing, types, static analysis
│
├──▶ diagnostics ──▶ peko-lsp same engine, so the editor
│ and the compiler agree
└──▶ typed AST
│
▼
peko-llvm LLVM 18 IR ──▶ object files ──▶ lld
│
▼
peko-cli the peko commandCodegen
03peko-llvm takes the typed ASTs and lowers them through LLVM 18 IR into object files. AST nodes implement a PekoValueBuilder trait whose single build_value method drives codegen for that node, with the implementations split across literals, expressions, statements, and declarations.
The part I would point at is how the builder API is organized. It is ten traits arranged in dependency layers, and each layer only calls into itself or something below it. Layer 0 is LLVM types and constants. Layer 1 is instructions and memory. Layer 2 is arithmetic, function definition, and globals. Layer 3 is higher level operations and scope management. Layer 4 is cross-module orchestration sitting on top of everything.
The reason for that is boring and it mattered a lot. Without the layers, every new high-level operation ends up on one enormous context type, and after a few months nobody can tell what depends on what. With them, a new operation goes in the right trait and inherits everything underneath it for free.
Linking
04The linker drives lld in process through a small C++ shim compiled into a static archive. One entry point takes a target description plus a list of object files and produces an executable.
It picks the driver per platform, ld.lld for Linux and Android, ld64.lld for macOS and iOS, lld-link for Windows, then assembles the argument string, search paths, runtime objects, and system libraries for that target before handing everything to LLD.
Getting this right across five platforms was less interesting than it was tedious, and it is the kind of thing that has to work perfectly or nothing ships.
What was hard
05Designing a language forces you to be exact about things you can stay vague on forever if you are only ever using one. What does a cast mean when it cannot be proven safe. What happens when a switch is not exhaustive. Where a pointer type sits in the type system. Every one of those is a decision, and getting it wrong is expensive later because the whole toolchain is built on top of it.
The V2 work I was partway through when I stopped is a decent picture of that. Renaming Pointer<T> to pointer<T>, adding f16, f32, and f64 and dropping double, splitting casts into a static-safe as and a forced danger_cast<T>, making switch exhaustive, adding serialize and deserialize. Those are all corrections to earlier decisions.
Where it stands
06On hold with Peko. The compiler works and it builds and runs real programs, but I am not developing it further while Peko is paused.