source → AST → C → binary

Classes, generics, and pattern matching —
compiled straight to C.

LLPL is a systems language that transpiles to plain C. Write with reference-counted classes, traits, and tagged enums; run anywhere from a Vector<T> down to a VGA text buffer at 0xB8000.

calc.llpl — grammar { } parser generator
1grammar Calc {
2    expr   : expr '+' term
3           | expr '-' term
4           | term
5           ;
6    term   : term '*' factor
7           | term '/' factor
8           | factor
9           ;
10    factor : '(' expr ')' | NUMBER ;
11    NUMBER : [0-9]+ ;
12}
13
14let tree: ParseNode = Calc(new String("1 + 2 * 3"))
note: direct left recursion in expr/term — resolved into a precedence-climbing parse at compile time, with ambiguous grammars rejected before they ever run.

One language, two targets

The same syntax, hosted or freestanding.

Write an ordinary program against the standard library, or drop straight onto hardware with interrupt handlers and no libc — LLPL doesn't ask you to choose a different language for either.

HostedResult<T,E> · match

Tagged enums carry their own fields per variant; match destructures them directly, and the same pattern doubles as clean error handling.

// enum Shape { Circle(radius: i64), Rectangle(width: i64, height: i64), Triangle(base: i64, height: i64) }
func area(s: Shape) -> i64 {
    match s {
        case Shape.Circle(radius) => {
            return 3 * radius * radius
        }
        case Shape.Rectangle(width, height) => {
            return width * height
        }
        case Shape.Triangle(base, height) => {
            return (base * height) / 2
        }
    }
}

Bare metalno libc · freestanding

A class is still just a struct with methods — cheap enough to wrap a raw hardware address directly, no allocator required.

class Screen {
    let buffer: u8*

    constructor() {
        self.buffer = 0xB8000 as u8*
    }

    func write(msg: char*) {
        // writes straight into video memory
    }
}

More of the syntax

Small enough to read in one pass.

Four more corners of the language — none of it needs a paragraph of explanation first.

Error handling

the ? operator
func safe_div(a: i64, b: i64) -> Result<i64, i64> {
    let r: Result<i64, i64> = new Result<i64, i64>()
    if b == 0 {
        r.set_err(-1)
        return r
    }
    r.set_ok(a / b)
    return r
}

// unwraps on Ok, or returns Err straight out of main()
let x: i64 = safe_div(10, 2)?

Closures

explicit captures
// captures `n` by value at creation time
func make_adder(n: i64) -> (i64) -> i64 {
    return func[n](x: i64) -> i64 {
        return x + n
    }
}

let add5 = make_adder(5)
let total: i64 = add5(10) // 15

String interpolation

implicit as_string
let name: String = new String("Ada")
let queued: i64 = 3

puts("hello, \(name) — \(queued) items queued")
// hello, Ada — 3 items queued

Stdlib & generics

built-in JSON parser
import stdlib.json.json_parser
using namespace std.json

let doc: JsonValue = parse(new String("{\"port\": 8080}"))
let port: i64 = doc.get_or(new String("port"), JsonValue.make_int(0)).as_int()

How a build actually runs

Every stage is a real, inspectable artifact.

Nothing is hidden inside a black-box backend — the generated C is there to read, and a build can stop at any stage.

Write
.llpl source
Parse
AST
Generate
plain C
Compile
native binary

What's in the language

Built for real programs, not toy syntax.

grammar { }

new

An ANTLR-style grammar block compiled at build time into a real recursive-descent parser — left recursion and precedence handled automatically, ambiguity caught before runtime.

Ref-counted classes

Classes are heap-allocated and reference-counted automatically; structs and unions stay plain value types with no overhead.

Generics & traits

Vector<T>, Result<T,E>, HashMap<K,V> and your own — bounded by traits like Hashable, resolved at compile time.

Tagged enums & match

Sum types with per-variant fields, destructured with match — the same mechanism doubles as ergonomic error handling.

A real standard library

Linked lists, trees, heaps, hash maps, tries and graphs; YAML and JSON parsers; string, file, and socket I/O.

Freestanding-ready

Interrupt handlers, inline assembly, volatile, and zero libc dependency — the same compiler builds a toy kernel.


Language reference

Every feature, in one pass.

Not a curated highlight reel this time — the full surface of the language, grouped by what you reach for at the same time. This is a quick tour; for the complete reference — compiler CLI, standard library API, and a guided tour of every example in the repo — see the full documentation site.

Types & values

primitives · arrays · casts

Explicitly-sized integers, a float, bool, and a text char/char* string — fixed arrays are T[N]; growable ones are Vector<T> from the standard library, not a built-in array type.

i8/16/32/64, u8/16/32/64explicitly sized integers, genuinely numeric. u8 is uint8_t, not text
charone byte of text (C char) — distinct from u8 even though both are 8 bits
int, uintnative machine word (C intptr_t/uintptr_t: 4 bytes on i386, 8 on x86_64) — not aliases of i64/u64; no implicit widening to/from a fixed-width type
float, bool32-bit float, boolean
stringalias for char* — no distinct string type at the ABI level
T[N]fixed-size array, e.g. u8[256]
expr as Texplicit cast
sizeof(T), expr.sizeofcompile-time size in bytes

Variables & functions

let · const · func

Bindings are type-inferred or explicitly annotated; a later let may shadow an earlier one in the same scope. Functions take default parameter values, a trailing variadic parameter, and overload by parameter shape.

let count = 0                     // inferred as i64
const MAX: i64 = 100
volatile let ready: bool = false    // never cached in a register

func greet(name: char*, loud: bool = false) -> char* { ... }
func total(values: i64...) -> i64 { ... }

Pipe operator

|>

x |> f desugars to f(x), and x |> f(a, b) to f(x, a, b) — the piped-in value always lands as the first argument. Chains read left to right, and it binds looser than every other operator, so a + b |> f is f(a + b), not a + f(b).

func double_it(x: i64) -> i64 { return x * 2 }
func add_one(x: i64) -> i64 { return x + 1 }
func add(a: i64, b: i64) -> i64 { return a + b }

let piped: i64 = 5 |> double_it |> add_one   // (5*2)+1 = 11
let with_args: i64 = 5 |> add(10)          // add(5, 10) = 15
let precedence: i64 = 2 + 3 |> double_it   // (2+3) |> double_it = 10

Classes, structs & unions

three ways to shape data

A class is always heap-allocated and reference-counted, with overloadable constructors and a destructor. A struct is a plain value type — no ref-counting, optionally packed for an exact C layout. A union overlaps its fields the same way C's does.

class Counter {
    private let value: i64

    constructor(start: i64) { self.value = start }
    destructor() {}

    func increment() { self.value = self.value + 1 }
    static func zero() -> Counter { return new Counter(0) }
}

packed struct Header {
    let magic: u32
    let version: u8
}

Generics & traits

<T: Bound>

Classes, structs, and functions all take type parameters, optionally bounded by a trait — a compile-time-only contract satisfied by an impl block elsewhere.

trait Hashable {
    func hash() -> u64
    func equals(other: Self) -> bool
}

class HashSet<T: Hashable> { ... }

impl Hashable for i64 {
    func hash() -> u64 { return self as u64 }
    func equals(other: i64) -> bool { return self == other }
}

Enums & pattern matching

tagged unions · match/case

A tagged enum variant carries its own fields and is constructed by calling it, e.g. Shape.Circle(3). match destructures a variant's fields directly into fresh bindings; a plain match on tuples and structs works the same way.

// enum Shape { Circle(radius: i64), Square(side: i64), Point }
match shape {
    case Shape.Circle(radius) => { ... }
    case Shape.Square(side) => { ... }
    default => { ... }
}

match point {
    case (0, 0) => puts("origin")
    case (x, y) => print_int("x", x)
}

Error handling

Optional<T> · Result<T,E> · try/catch

expr? unwraps an Optional<T>/Result<T,E> on success, or returns a matching empty/error value straight out of the enclosing function. Wrapped in try, that same early-exit is redirected to catch instead — plain generated control flow, not stack unwinding.

try {
    let x: i64 = safe_div(10, 0)?
} catch (e) {
    print_int("caught", e)
} finally {
    puts("always runs")
}

Closures & iteration

func[captures] · for..in

A lambda's captures are listed explicitly — copied by value at creation time, or by reference with &. for x in y works over a fixed array or anything implementing the two-method Iterator<T> protocol.

func make_adder(n: i64) -> (i64) -> i64 {
    return func[n](x: i64) -> i64 { return x + n }
}

for item in my_vector {
    puts(item)
}

Control-flow extras

unless · defer

Beyond the usual if/while/for: a trailing unless modifier reads as a guard clause, and defer schedules a statement to run when its enclosing scope exits, in reverse order for multiple defers.

log("skipped") unless is_valid   // if !is_valid { log(...) }

func read_all() {
    let f = open_file()
    defer f.close()             // runs on every exit path
    ...
}

Modules & namespaces

import · using · alias

A bare, dotted import resolves to a file path (drivers.serialdrivers/serial.llpl); a quoted one is for paths that aren't valid identifiers. using namespace brings a namespace's names into scope unqualified; $LLPL_HOME lets stdlib/... imports resolve the same way regardless of where the importing file lives.

import stdlib.collections.hashmap
import "weird-path.llpl"          // quoted, for names that aren't valid identifiers

using namespace std.collections
alias Map = EnhancedHashMap<String, i64>

Macros

quote / unquote

A hygienic macro quotes ordinary LLPL syntax and splices arguments in with unquote — the quoted body is parsed with the same grammar as everything else, not a separate template language.

macro assert_positive(x) {
    quote {
        if unquote(x) < 0 {
            llpl_panic("expected a positive value")
        }
    }
}

grammar { }

new

Covered up top — an ANTLR-style grammar block compiled into a real recursive-descent parser at build time. Direct left recursion becomes a precedence-climbing loop automatically; every decision point is checked for ambiguity at compile time, never guessed at runtime. Name(text) is sugar for constructing the generated class and calling its first rule; every match becomes a walkable ParseNode.

Memory model

ref counting · Weak<T> · raw pointers

A class instance's lifetime is reference-counted automatically. Weak<T> holds a non-owning reference for exactly the cases where two objects would otherwise keep each other alive forever. Manual allocation is still available underneath for anything managing its own memory by hand.

let a: MyClass = new MyClass()               // ref-counted
let w: Weak<MyClass> = new Weak<MyClass>(a)  // doesn't keep it alive

let raw: char* = llpl_alloc(64)                // manual buffer
llpl_free(raw)

Systems & interop

extern · interrupt · asm · attributes

Binding a C library is an extern func declaration plus a #link directive; a bare-metal target adds interrupt handlers, inline assembly, and placement attributes for linker sections and alignment.

#link "SDL3"
extern func SDL_Init(flags: u32) -> i64

interrupt func isr_divide_by_zero() { ... }

@section(".boot")
@align(16)
let boot_stack: u8[4096]

Get started

Two commands to a native binary.

$ dub build                          # builds the llpl compiler itself
$ ./llpl calc.llpl -b -o calc        # .llpl -> C -> native binary, in one step
$ ./calc
7