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.
$ lira run hello.li # type-check, compile, and execute $ lira check hello.li # type-check only, no output $ lira compile hello.li # emit bytecode (.lic) $ lira ast hello.li # print the parsed AST $ lira disasm hello.li # disassemble the bytecode
Hello, Lira
Execution starts at fn main(). println is a
VM built-in — no import needed — and strings interpolate with
$${…}.
// 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:
$ lira run hello.li
Hello, world!
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 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 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 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:
$ lira check program.li 7:5: Cannot assign to immutable variable: total 12:13: Undefined variable: y
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.