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.
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 } } }
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 } }
Four more corners of the language — none of it needs a paragraph of explanation first.
? operatorfunc 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)?
// 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
as_stringlet name: String = new String("Ada") let queued: i64 = 3 puts("hello, \(name) — \(queued) items queued") // hello, Ada — 3 items queued
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()
Nothing is hidden inside a black-box backend — the generated C is there to read, and a build can stop at any stage.
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.
Classes are heap-allocated and reference-counted automatically; structs and unions stay plain value types with no overhead.
Vector<T>, Result<T,E>, HashMap<K,V> and your own — bounded by traits like Hashable, resolved at compile time.
Sum types with per-variant fields, destructured with match — the same mechanism doubles as ergonomic error handling.
Linked lists, trees, heaps, hash maps, tries and graphs; YAML and JSON parsers; string, file, and socket I/O.
Interrupt handlers, inline assembly, volatile, and zero libc dependency — the same compiler builds a toy kernel.
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.
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 textcharone byte of text (C char) — distinct from u8 even though both are 8 bitsint, 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 typefloat, bool32-bit float, booleanstringalias for char* — no distinct string type at the ABI levelT[N]fixed-size array, e.g. u8[256]expr as Texplicit castsizeof(T), expr.sizeofcompile-time size in bytesBindings 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 { ... }
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
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 }
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 } }
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) }
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") }
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) }
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 ... }
A bare, dotted import resolves to a file path (drivers.serial → drivers/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>
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") } } }
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.
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)
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]
$ dub build # builds the llpl compiler itself $ ./llpl calc.llpl -b -o calc # .llpl -> C -> native binary, in one step $ ./calc 7