LA3 Loading La³, please wait
Skip to content
The La³ metallic 8-pointed star logo

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.

la3.json · main.la3
// 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.

ownership.la3
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.

  1. Source Code .la3
  2. Lexer tokens
  3. Parser AST
  4. Type Checker sound types
  5. Borrow Checker ownership
  6. HIR desugared
  7. MIR CFG · lowering
  8. LLVM IR codegen
  9. Native Binary linked

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.

Shipping

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.

Shipping

Borrow Checking

Aliasing xor mutability, enforced statically. A live &mut x forbids any other access; &x forbids writes. Dangling references are rejected.

Shipping

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.

Shipping

Interfaces

TypeScript-style declarations, Rust-style nominal conformance. A type satisfies an interface only with an explicit impl block.

Shipping

Generics

Type parameters bounded by interfaces, inferred at call sites or supplied with turbofish. Monomorphized to concrete layouts for native code.

Shipping

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.

Building

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.

Planned

À-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.

4/10 shipped
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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).

  7. 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.

  8. 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.

  9. 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.

  10. 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.

Open the outline Developer-focused skeleton. Fill later.
01

Getting started

Install, run, and verify the local setup.

02

Project structure

What lives in src/routes, src/lib, and static.

03

Routing and pages

Layouts, metadata, and page composition.

04

Components and data

Shared sections, helpers, and content sources.

05

Styling and UI

Tokens, fonts, motion, and responsive behavior.

06

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.

Preview
main.la3
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}")
}
output
0: 1
1: 4
2: 9
3: 16
4: 25
sum of squares = 55
exited 0 · checked & borrow-checked

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.

Target board: Raspberry Pi Pico 2 W · RP2350

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.