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.

Fibers and channels

Spawn lightweight fibers and coordinate them over typed channels. Receive with a bound select — 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 literals, bindings, guards, and tuple patterns — including nested tuples. The checker enforces exhaustiveness.

pattern_tuple_literals.li
// Tuple patterns with literal sub-patterns must emit equality guards.
// Regression test: previously every tuple input matched the first arm.
// @expect: origin
// @expect: other
// @expect: yaxis
// @expect: xaxis
// @expect: all-zero
// @expect: origin-z
// @expect: pair-zero
// @expect: general
// @expect: mid-zero
// @expect: mid-other

fn quadrant(p: (int, int)) -> string {
    return match p {
        (0, 0) => "origin",
        (0, y) => "yaxis",
        (x, 0) => "xaxis",
        (x, y) => "other"
    }
}

fn classify(p: ((int, int), int)) -> string {
    return match p {
        ((0, 0), 0) => "all-zero",
        ((0, 0), z) => "origin-z",
        ((x, y), 0) => "pair-zero",
        ((x, y), z) => "general"
    }
}

fn mid(t: (int, int, int)) -> string {
    return match t {
        (x, 0, z) => "mid-zero",
        (x, y, z) => "mid-other"
    }
}

fn main() {
    print(quadrant((0, 0)))
    print(quadrant((3, 4)))
    print(quadrant((0, 5)))
    print(quadrant((3, 0)))
    print(classify(((0, 0), 0)))
    print(classify(((0, 0), 5)))
    print(classify(((1, 2), 0)))
    print(classify(((1, 2), 3)))
    print(mid((9, 0, 9)))
    print(mid((9, 9, 9)))
}

Generics and generic enums

Write generic functions, structs, and enums. Build your own Option- or Result-shaped types with full type checking.

generic_enum.li
// Test generic enum declarations (type erasure model)
// @expect: 42
// @expect: none
// @expect: both 1 hello
// @expect: neither
// @expect: 5

enum Opt<T> {
    Some(T),
    None
}

enum Pair<A, B> {
    Both(A, B),
    Neither
}

fn main() {
    // Single-param generic enum
    let some_val = Opt::Some(42)
    let none_val = Opt::None

    match some_val {
        Opt::Some(x) => println(x)
        Opt::None => println("none")
    }

    match none_val {
        Opt::Some(x) => println(x)
        Opt::None => println("none")
    }

    // Two-param generic enum
    let both = Pair::Both(1, "hello")
    let neither = Pair::Neither

    match both {
        Pair::Both(a, b) => println("both " + a + " " + b)
        Pair::Neither => println("neither")
    }

    match neither {
        Pair::Both(a, b) => println("both " + a + " " + b)
        Pair::Neither => println("neither")
    }

    // Generic enum with concrete annotation
    let o: Opt<int> = Opt::Some(5)
    match o {
        Opt::Some(n) => println(n)
        Opt::None => println("none")
    }
}

main()

Errors as values

Return Result, propagate failures with ?, and handle them by matching. No exceptions, no surprises.

result_propagation.li
// Test Result type with ? operator
// @expect: 100
// @expect: error: division by zero

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()

Honest, located diagnostics

The checker reports type and binding errors with a line:column location, before any bytecode is generated. These are the compiler's actual messages — not mockups.

Tune in.

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