Laila Lang
Started as documentation. Became a language. A reading pseudo-language assembled from Rust, C, TypeScript and Lua, now a real compiler with ownership, a borrow checker, and a native back-end on the way.
// La³ - ownership, pattern matching, and the ? operator.
struct Config {
port: u16,
hosts: List<str>,
}
fn load(path: str) -> Result<Config> {
let text = fs.read(path)?
let cfg = json.decode::<Config>(text)?
Ok(cfg)
}
fn main() {
match load("la3.json") {
Ok(cfg) => io.println(f"listening on :{cfg.port}"),
Err(e) => io.println(f"config error: {e}"),
}
}Code showcase
Read it once, recognize it everywhere
Every construct is borrowed from a language you already know. Here is the real syntax, the same code the interpreter runs today.
struct Buffer {
bytes: List<u8>,
}
fn consume(buf: Buffer) -> usize {
buf.bytes.len() // takes ownership of buf
}
fn main() {
let buf = Buffer { bytes: [1, 2, 3] }
let n = consume(buf) // buf is moved here
// Using buf again is a compile-time use-after-move error:
// io.println(buf.bytes.len()) // rejected by the borrow checker
io.println(f"consumed {n} bytes")
// Borrow instead of move - &buf reads without taking ownership.
let live = Buffer { bytes: [9, 9] }
let view = &live
io.println(f"still alive: {view.bytes.len()}")
}Move semantics and a borrow checker, checked statically, no garbage collector.
Philosophy
A language designed to be read
Every technical blog hits the same wall: a Python example reads strangely to a systems programmer; a C example needs translation for anyone living in managed languages. The friction lands at the exact moment the text needs full attention.
La³ began as the answer: a pseudo-language for code examples, assembled from the clearest parts of Rust, C, TypeScript and Lua. No piece of its syntax is arbitrary; each choice has a source and a reason.
Then the idea outgrew the page. A lexer became a parser, a checker, an interpreter, and the pseudo-language quietly became a real one. It is now a compiler project pursued for exploration and the love of how machines actually work.
When two designs are equally expressive, the more readable one wins; when readability ties, the one that prevents a silent mistake wins.
Immutability by default
let binds an immutable value; mut is the marked, visible exception. A reader holds a value in mind for the whole scope without rechecking.
Expressions over statements
if and match are expressions; a block evaluates to its last expression. Less syntactic noise, more composability.
Absence is explicit
Exactly one null-like value, nil. Absence lives in the type, as a bare T | nil or an advertised Option<T>, never as a silent surprise.
One obvious way
Where two notations would mean the same thing, La³ keeps the one most readers recognize and drops the other. Redundancy is noise.
Architecture
From source to native binary
The interpreter stays the correctness oracle while a real LLVM back-end is built on top of the front-end. MIR is where the hard lowerings live, so codegen stays a thin translation.
- Source Code .la3shipped
- Lexer tokensshipped
- Parser ASTshipped
- Type Checker sound typesshipped
- Borrow Checker ownershipshipped
- HIR desugaredshipped
- MIR CFG · loweringbuilding
- LLVM IR codegenplanned
- Native Binary linkedplanned
Features
Systems-grade, by design
Exact-width integers, ownership, raw pointers and C-style layout: the same front-end serves bare metal and full applications. Nothing is wasted.
Ownership
Rust-style move semantics. Values have a single owner; passing one by value moves it, and using it afterwards is a compile-time error.
Borrow Checking
Aliasing xor mutability, enforced statically. A live &mut x forbids any other access; &x forbids writes. Dangling references are rejected.
Pattern Matching
Exhaustive match over sum types with ranges, guards, bindings and or-patterns. Add a variant and every site that must change lights up.
Interfaces
TypeScript-style declarations, Rust-style nominal conformance. A type satisfies an interface only with an explicit impl block.
Generics
Type parameters bounded by interfaces, inferred at call sites or supplied with turbofish. Monomorphized to concrete layouts for native code.
Concurrency
spawn / join for real threads and channels for hand-off, plus async / await with all and race for I/O-bound work: two honest tools.
LLVM Compilation
A native back-end on top of the front-end: AST to a sound type and borrow check, then HIR, MIR, LLVM IR, an object file and a linked binary.
À-la-carte stdlib
No no_std split. The standard library is many independent modules; a granular build pulls in only what main actually reaches.
Roadmap
A compiler, phase by phase
Taken straight from the project's COMPILER_PLAN. A phase is only done when every subpart builds, tests and verifies against the interpreter oracle.
- Phase 0 Done
Foundations & decisions
Toolchain pinned, build subcommand stubbed, runtime crate skeleton, and a differential test harness that runs the interpreter against the future binary.
- Phase 1 Done
Sound type checker
Every AST node annotated with a concrete type, full field and method resolution, by-value layout computation, exact as-cast semantics, and sound inference with i32 / f64 defaults.
- Phase 1.6 Done
Ownership & borrow checker
Move semantics, argument and receiver moves, move-closure captures, lexical borrow regions with field granularity, and a drop / ownership codegen contract, replacing the earlier ARC plan.
- Phase 2 Done
HIR & desugaring
Name resolution to unique BindingIds, lowering to a typed HIR, all surface sugar desugared (f-strings, ??, ?., ?, while let), and explicit closure captures.
- Phase 3 In progress
MIR, the enabling layer
A Rust-MIR-like CFG where the hard lowerings live. The data model, HIR-to-MIR lowering and the la3 mir command land; match decision trees, closure conversion and ownership lowering are next.
- Phase 4 Planned
Runtime library
str, List, Map and Set in the native runtime with drop glue, f-string formatting with specs, and an extern "C" stdlib subset (io, fs, os, math, bytes, json).
- Phase 5 Planned
LLVM codegen
MIR lowered to LLVM IR: the first binary that runs. Milestone: fizzbuzz, fib and shapes compile and match the interpreter exactly.
- Phase 6–8 Planned
Memory, generics & closures
Codegen for references and raw pointers, monomorphization with static and dynamic dispatch, and converted closures driving the higher-order collection methods.
- Phase 9–11 Planned
Errors, concurrency & driver
Result / Option / ? as early returns over enums, threads and async over the executor, and a mode-aware driver linking the runtime into an optimized executable.
- Phase 12 Planned
À-la-carte dynamic stdlib
The standard library as independent modules with reachability-driven dead-code elimination and opportunistic sharing: small binaries for the Pico, full builds for PC and web.
Documentation
Docs first, prose later
This section is intentionally an outline for contributors: enough structure to navigate the repo, not a finished reference.
Getting started
Install, run, and verify the local setup.
Project structure
What lives in src/routes, src/lib, and static.
Routing and pages
Layouts, metadata, and page composition.
Components and data
Shared sections, helpers, and content sources.
Styling and UI
Tokens, fonts, motion, and responsive behavior.
Build and deploy
Checks, type generation, build, preview, and Cloudflare deploy.
Playground
Run La³ in the browser
A preview of what's coming: an online playground backed by the real interpreter, with the same subcommands the CLI exposes today.
fn main() {
let nums = [1, 2, 3, 4, 5]
let squares = nums.map(|x| x * x)
let total = squares.reduce(0, |a, b| a + b)
for (i, n) in squares.enumerate() {
io.println(f"{i}: {n}")
}
io.println(f"sum of squares = {total}")
}0: 1 1: 4 2: 9 3: 16 4: 25 sum of squares = 55
Mockup: the interactive playground is on the roadmap.
Future vision
One language, all the way down
The same front-end that type-checks a web app (exact-width integers, ownership, raw pointers, C-style layout) is exactly what bare metal needs. The real split is the runtime and the target, not the language.
So the north star is audacious and deliberately for the love of it: run La³ on a Raspberry Pi Pico 2 W, from the first instruction after reset up to the applications: a bootloader, a kernel, a runtime and apps, all in one language.
Applications
Desktop and bare-metal programs written in La³, sharing one language across the whole stack.
Runtime
A granular, à-la-carte standard library that links only what main reaches, small enough for a microcontroller.
Kernel
A minimal kernel in La³, leaning on ownership, exact-width integers and raw pointers for memory it controls.
Bootloader
A bootloader bringing the board up from reset: the first La³ code the silicon ever runs.