The Lira programming language

Systems programming, in harmony.

Lira pairs Go-style fiber concurrency with pattern matching and generics. Programs are statically type-checked, compiled to bytecode, and run on a compact VM.

pipeline.li
// Two fibers compute in parallel and send results down a channel.
// main collects both with select, binding each value as it arrives.

fn square(n: int, out: Channel<int>) {
    send(out, n * n)
}

fn main() {
    let results = chan(2)

    spawn square(10, results)
    spawn square(21, results)

    var total = 0
    var received = 0
    while received < 2 {
        select {
            v = <-results => {
                println("got ${v}")
                total = total + v
                received = received + 1
            }
        }
    }
    println("sum ${total}")
}

Two fibers compute in parallel; main collects both results with a bound select.

Built for programs that do several things at once

Every snippet below is a real .li file in this repository, verified to compile and run before the site is built.

Concurrency

Fibers and channels

Spawn lightweight fibers and coordinate them over typed channels. Receive with a bound select — the value lands in a variable, with no shared-memory guesswork.

concurrency.li
// Spawn a producer; receive its values with a bound select.
fn produce(out: Channel<int>) {
    send(out, 1)
    send(out, 2)
    send(out, 3)
}

fn main() {
    let ch = chan(3)
    spawn produce(ch)

    var seen = 0
    while seen < 3 {
        select {
            n = <-ch => {
                println("recv ${n}")
                seen = seen + 1
            }
        }
    }
}

Pattern matching

Match on shape, exhaustively

Destructure with literals, bindings, and nested tuples. The checker enforces exhaustiveness, so an unhandled case is a compile error — not a runtime surprise.

patterns.li
// Tuple patterns destructure positionally — and the elements can be
// literals, bindings, or nested tuples. Classifying a point by which
// axis it sits on falls out naturally.
fn quadrant(p: (int, int)) -> string {
    return match p {
        (0, 0) => "origin",
        (0, y) => "on the y-axis",
        (x, 0) => "on the x-axis",
        (x, y) => "at (${x}, ${y})"
    }
}

// Nesting works too: a pattern can reach into an inner tuple.
fn label(p: ((int, int), int)) -> string {
    return match p {
        ((0, 0), z) => "axis origin, depth ${z}",
        ((x, y), 0) => "flat at (${x}, ${y})",
        ((x, y), z) => "point in space"
    }
}

println(quadrant((0, 0)))
println(quadrant((0, 5)))
println(quadrant((3, 4)))
println(label(((0, 0), 9)))
println(label(((1, 2), 0)))

Generics

Build your own Option and Result

Write generic functions, structs, and enums. Roll Option- or Result-shaped types yourself — fully type-checked, then erased at runtime.

option.li
// A generic enum makes its own Option type. Matching binds the
// payload of `Some`, and the checker forces you to handle `None`.
enum Opt<T> {
    Some(T),
    None
}

fn unwrap_or(o: Opt<int>, fallback: int) -> int {
    return match o {
        Opt::Some(v) => v,
        Opt::None => fallback
    }
}

println(unwrap_or(Opt::Some(42), 0))
println(unwrap_or(Opt::None, -1))

Error handling

Errors as values

Return Result, propagate failures with the ? operator, and handle them by matching. No exceptions, no hidden control flow.

result.li
fn divide(a: int, b: int) -> Result<int, string> {
    if b == 0 {
        return Result::Err("division by zero")
    }
    return Result::Ok(a / b)
}

fn calculate(x: int, y: int) -> Result<int, string> {
    let result = divide(x, y)?
    return Result::Ok(result * 10)
}

fn main() {
    // Test successful case
    let r1 = calculate(100, 10)
    match r1 {
        Result::Ok(v) => println(v)
        Result::Err(e) => println("error: " + e)
    }

    // Test error propagation
    let r2 = calculate(100, 0)
    match r2 {
        Result::Ok(v) => println(v)
        Result::Err(e) => println("error: " + e)
    }
}

main()

Located diagnostics

The checker reports type and binding errors with a line:column location, before any bytecode is generated.

Tune in.

Start with the guide, or read the source. Lira is early, and the guide is clear about what works today.