Guide

Getting started

Install the CLI, write your first program, and learn the core building blocks — values, functions, structs, and control flow.

Lira is a statically-checked language that compiles to bytecode and runs on a small virtual machine. You write .li source, the checker proves it sound, and the unified lira CLI runs it. Every snippet on this page is a real file in the repository, verified to run before this site is built.

The CLI

A single binary, lira, drives the whole toolchain. The two commands you reach for first are run and check.

Hello, Lira

Execution starts at fn main(). println is a VM built-in — no import needed — and strings interpolate with $${…}.

hello.li
// Your first Lira program. `fn main()` is the entry point;
// `println` is a built-in, so no import is needed.
fn main() {
    let name = "world"
    println("Hello, ${name}!")
}

Run it:

Values and bindings

Bind a value with let for an immutable binding, or var when you need to reassign it later. Local bindings infer their type from the initializer; you only annotate where there is nothing to infer from — function parameters and return types.

Functions

Functions annotate each parameter and, if they return a value, the return type after ->. A function with no -> T returns nothing.

functions.li
// Functions annotate their parameters and return type.
// Inside a body, `let` bindings infer their type from the value.
fn add(a: int, b: int) -> int {
    return a + b
}

// A function with no `-> T` returns nothing.
fn announce(label: string, value: int) {
    println("${label} = ${value}")
}

fn main() {
    let sum = add(2, 3)        // inferred int
    announce("sum", sum)
    announce("doubled", add(sum, sum))
}

Structs

Structs group named fields. Construct one with Name { field: value } and read fields back with dot access. abs here is a free function; many small numeric helpers are built in.

structs.li
// Structs group named fields. Construct them with field: value,
// and read fields back with dot access.
struct Point {
    x: int,
    y: int,
}

fn manhattan(p: Point) -> int {
    return abs(p.x) + abs(p.y)
}

fn main() {
    let p = Point { x: 3, y: -4 }
    println("x=${p.x} y=${p.y}")
    println("distance=${manhattan(p)}")
}

Control flow

if/else and while work as you would expect. The one worth highlighting is match: it is an expression, so each arm yields a value and the whole match evaluates to the arm that fits.

control.li
// Control flow: if/else as a statement, while loops, and `match`
// as an expression that yields a value.
fn classify(n: int) -> string {
    return match n {
        0 => "zero",
        _ => if n > 0 { "positive" } else { "negative" }
    }
}

fn main() {
    var i = 0
    while i < 3 {
        println("tick ${i}")
        i = i + 1
    }

    println(classify(7))
    println(classify(0))
    println(classify(-2))
}

When something is wrong

The checker runs before any bytecode is generated and reports errors with a line:column location. These are the compiler's real messages — for example, assigning to a let binding or using a name that was never defined:

Where to go next

From here, the language fans out into its three signature areas: concurrency with fibers, channels, and select; the type system with generics, optionals, and Result; and pattern matching with enforced exhaustiveness. The standard library covers the everyday work in between.