lume

03 — The Lume Language Specification, v0.1 (draft)

Status: draft, edition 2026. This document is normative for Stage 0/1 implementations. Sections marked [Stage N] describe features whose syntax is reserved now and implemented in that roadmap stage; Stage 0 implements §1–§9 (the “core subset”). Grammar: grammar/lume.ebnf.

Implementation note. The [Stage N] tags mark the stage at which the spec intends a feature to land; they do not all mean it is implemented in the self-hosted lumec today. As of #239, traits/impl, closures, refinement T where ..., async/.await, shard, and let-polymorphism are implemented and tested; AArch64/RISC-V ELF targets run under QEMU and Mach-O/PE/Intel-HEX are emitted (#238, see docs/26). Still not real as language types: rc[T]/arc[T]/weak[T] ownership tiers and promise[T]/signal[T] (today only hand-written library mocks - reopened #197/#215). See docs/10-stage1-status.md and docs/PROJECT_STATE.md for what is real.

Notation: code for syntax; italics for metavariables; “MUST/SHOULD” as in RFC 2119.


1. Source text and lexical structure

1.1 Encoding and files

1.2 Comments

1.3 Identifiers and keywords

1.4 Literals

1.5 Layout: indentation

1.6 Operators and punctuation

+ - * / % ** == != < <= > >= = += -= *= /= %= .. ..= -> => | || & && ^ ~ << >> . ?. ?? ? ! : , ; ( ) [ ] { } @ |> plus keyword operators and or not in is as. |> pipes the left value as the first argument of the right call.


2. Modules and visibility

mod payroll                 # optional; defaults to the file name
use std.io                  # brings `io` into scope
use std.data (table, query) # selective import
use std.collections as col  # alias
pub use self.types.*        # re-export

3. Types

3.1 Primitive types

| Type | Meaning | |—|—| | bool | true/false | | i8 i16 i32 i64 i128 isize, u8 u16 u32 u64 u128 usize | fixed-width integers; int = i64, uint = u64 | | f16 f32 f64 | IEEE 754 floats; float = f64 | | decimal(p, s) | exact decimal, p digits, s after the point; decimal = decimal(34, 6) | | char | Unicode scalar value | | str | owned UTF-8 string; &str-like views are str borrows (see §6) | | () | unit | | never | the type of expressions that do not return (fail, panic, return, infinite loop) |

Integer arithmetic is checked (panics on overflow) in debug and test builds and wrapping is never the silent default: release builds keep checks unless the module opts into @overflow(wrap) or uses the explicit operators +% (wrapping), +| (saturating), +? (checked, returns T?). Division by zero is an error value for checked_div and a panic for /.

3.2 Compound types

| Syntax | Type | |—|—| | (A, B, C) | tuple; fields .0, .1 | | [T] | growable list (owned, heap) | | [T; n] | fixed array, n a comptime integer | | T[] | slice view (borrowed, contiguous) — see §6 | | map[K, V] | hash map; omap[K, V] ordered map | | set[T] | hash set; bitset[E] for enums (Pascal set of) | | T? | optional: some(x) or none; literals auto-wrap in optional context | | T ! E | error union: a T or an error from error set E; T ! infers E | | fn(A, B) -> R / {effs} | function type (effects optional, see §8) | | rc[T], arc[T], cell[T], weak[T] | shared ownership tiers (§6) | | ptr[T], rawptr | raw pointers, usable only in unsafe | | dyn Trait | trait object (dynamic dispatch) | | impl Trait | some type implementing Trait (static, inferred) | | T in lo..hi | range type (subtype of T) | | T where pred(self) | refinement type [Stage 3]; checked at the boundary | | distinct T | newtype with no implicit conversion to T | | tensor[T, d1, d2, …] | tensor with static or symbolic dims [Stage 4] |

3.3 Type declarations

type Celsius = distinct f64
type Age     = int in 0..150
type Point   = record
    x: f64
    y: f64 = 0.0                 # default value

type Dept = enum Sales | IT | Finance             # unit variants
type Shape = enum                                  # payload variants
    Circle(radius: f64)
    Rect(w: f64, h: f64)
    Empty

type IoError = error NotFound | Permission(path: str) | Other(msg: str)   # error set
type Pair[T] = record                               # generic
    a: T
    b: T

3.4 Generics

fn max[T: Ord](a: T, b: T) -> T. Type parameters in square brackets; bounds with :; multiple bounds T: Ord + Show; where clauses for complex bounds. Generics are monomorphised. Comptime values are parameters too: fn zeros[T, n: usize]() -> [T; n].

3.5 Type inference

Local inference everywhere (let x = 3 ⇒ int); bidirectional checking lets literals adopt context types. Function signatures of pub items MUST be fully annotated; private functions may omit the return type and effects (inferred). Generic parameters are inferred at call sites.

3.6 Conversions

No implicit conversions. as converts between numeric types (checked: x as u8 panics on overflow in debug; x as! u8 truncates; x as? u8 returns u8?). from/into traits for user-defined conversions. str(x) ≡ x.show().


4. Expressions

Precedence, highest first (same row = same level, left-associative unless noted):

  1. Postfix: call f(x), index a[i], field a.b, method/UFCS a.f(x), ? (propagate), ?. (optional chain), .await [Stage 3]
  2. Unary: -x, not x, ~x, own x, mut x, &x (explicit borrow, rarely needed)
  3. ** (right-assoc)
  4. * / %
  5. + -
  6. << >>
  7. &
  8. ^
  9. |
  10. .. ..= (ranges; non-associative)
  11. == != < <= > >= in is as
  12. ?? (right-assoc)
  13. and / &&
  14. or / ||
  15. |> (pipe)
  16. = += -= … (assignment; statement-level, yields ())

4.1 Blocks as expressions

A block’s value is its last expression. if c then_block elif c2 block else block is an expression; all branches must have compatible types (or the if is used as a statement with type ()).

4.2 match

match shape
    Circle(radius)          => 3.14159 * radius ** 2
    Rect(w, h) if w == h    => w * w              # guard
    Rect(w, h)              => w * h
    Empty                   => 0.0

Patterns: literals, ranges 0..=9, bindings x, wildcard _, variant patterns, record patterns Point(x, y: 0.0), tuples, lists [first, ..rest], some(x)/none, ok(x)/err(e) for error unions, x @ pattern (bind and match), alternatives A | B, is Type for dyn downcast. An arm body is an expression or an indented block. Matches are checked for exhaustiveness (T0040) and redundancy (warning W0041).

4.3 Closures

|x| x * 2 · |x: int, y: int| -> int x + y · multi-line:

let f = |x|
    let y = x * 2
    y + 1

Closures capture by borrow by default, by move with own |x| …. Code blocks (Clipper) are closures. A function expecting a closure can take it as a trailing block:

items.each |item|
    io.println(item)

4.4 Collections and comprehensions

[1, 2, 3] · [0; 10] · {"a": 1, "b": 2} (map) · {1, 2, 3} (set) · (1, "a") (tuple). Comprehensions: [x * 2 for x in items if x > 0], {k: v for (k, v) in pairs}. Slicing: a[1..3], a[..2], a[2..], a[-1] (negative from end; checked).

4.5 String interpolation

"Hello, {name}! You are {age:>3} years old. Total {total:.2}". Format spec after : follows std.fmt (width, alignment, precision, radix). `` are literal braces. Interpolation calls Show or Format.

4.6 Ranges

a..b half-open, a..=b inclusive, a.. open, ..b. Ranges are iterable values of type range[T]. x in 1..=10 tests membership.

4.7 ?, ??, ?.

4.8 Holes

todo and todo "message" are expressions of type never that panic when reached and that the compiler reports (W0100) with the expected type and in-scope bindings. ? as a bare expression (a typed hole) is an error in release builds and a reported hole in debug/agent mode.


5. Statements and control flow

let x = 1                 # immutable binding (shadowing allowed)
let y: f64 = 2            # annotated; literal adopts f64
var count = 0             # mutable binding
count += 1
let (a, b) = pair         # destructuring (also records, enums with one variant)
const MAX: int = 100      # comptime constant (module level)

6. Memory model: ownership, borrowing, sharing

6.1 Values and ownership

Every value has exactly one owner (a binding, a field, a collection slot). When the owner goes out of scope the value is dropped (destructor Drop.drop runs, LIFO). Assignment and passing own move the value; the source is no longer usable (O0001 use after move). Types that are plain data (integers, floats, bool, char, tuples/records/enums of them) are Copy: moves are copies. Non-Copy values are duplicated only by explicit .clone().

6.2 Borrowing — the default for parameters

fn total(items: [Item]) -> decimal          # `items` is a shared borrow
fn bump(counter: mut Counter)                # exclusive (mutable) borrow
fn consume(file: own File)                   # ownership moves in

6.3 Shared ownership tiers

6.4 Allocators

Heap-allocating types and functions take an allocator implicitly from the current context (std.alloc.current()), which defaults to the global allocator and can be rebound for a scope: with alloc = arena … . freestanding builds have no default allocator; the compiler requires an explicit with alloc (O0030) — the Zig discipline where it matters.

6.5 unsafe

Inside unsafe: ptr[T] deref, pointer arithmetic, rawptr as ptr[T], asm, FFI calls, @mmio registers, transmute. The compiler never relaxes borrow rules for safe references inside unsafe; unsafe only unlocks the operations above. lume audit unsafe lists them.


7. Functions, traits, implementations

7.1 Functions

## Divide, failing on zero.
pub fn divide(a: int, b: int) -> int ! DivError
    requires b != 0 or fail_ok      # contract (§9)
    ensures result * b == a
    if b == 0
        fail DivError.ByZero
    a / b

fn sq(x: int) -> int = x * x                       # expression form
fn greet(name: str, greeting: str = "Hello") -> str  # default argument
    "{greeting}, {name}"
greet("Ana", greeting: "Olá")                      # named argument at call
fn log(items: ...str)                               # variadic (a slice)

7.2 Traits

trait Shape
    fn area(self) -> f64
    fn perimeter(self) -> f64
    fn describe(self) -> str = "area {self.area():.2}"   # default method
    type Unit                                            # associated type
    const SIDES: int                                     # associated const

impl Shape for Circle
    fn area(self) -> f64 = 3.14159 * self.radius ** 2
    fn perimeter(self) -> f64 = 2 * 3.14159 * self.radius
    type Unit = Meter
    const SIDES = 0

fn print_all(shapes: [dyn Shape])          # dynamic dispatch
fn biggest[S: Shape](shapes: [S]) -> S     # static, monomorphised

7.3 Properties (Delphi)

type Account = record
    _balance: decimal(12, 2)
    pub prop balance: decimal(12, 2)
        get = self._balance
        set(v) requires v >= 0 = self._balance = v

8. Errors, effects and panics

8.1 Error unions

8.2 Effects

fn load(path: str) -> Config ! IoError / {io, alloc}
fn pure_hash(data: u8[]) -> u64                     # no effects: pure
fn fetch(url: str) -> str ! NetError / {net, async}

8.3 Panics

panic is for bugs. It unwinds the current task, running defers and Drops, and is reported to the task’s scope as a failure. Processes with one task exit non-zero. @panic(abort) builds abort immediately (kernels).


9. Contracts and invariants

fn withdraw(acct: mut Account, amount: decimal(12, 2)) ! InsufficientFunds
    requires amount > 0
    requires acct.balance >= amount or fail InsufficientFunds   # contract that fails softly
    ensures acct.balance == old(acct.balance) - amount
    acct.balance -= amount

type SortedList[T: Ord] = record
    items: [T]
    invariant items.is_sorted()

10. Concurrency [Stage 3]

fn fetch_all(urls: [str]) -> [str] ! NetError / {net, async}
    scope s                                  # structured: all tasks end before `scope` ends
        let handles = [s.spawn fetch(u) for u in urls]
        [h.await? for h in handles]

let (tx, rx) = chan[Msg](capacity: 100)
s.spawn
    for msg in rx
        handle(msg)
select
    msg = rx.recv()      => …
    _   = timer.after(1.s) => …

par for i in 0..n                             # data parallel
    out[i] = f(inp[i])

11. Data: records, tables, queries, persistence [Stage 2]

use std.data (table, query, index)

let customers = table[Customer].read_csv("customers.csv")?
index customers by id                       # unique index
index customers by (city, name)

let vip = query customers
    where balance > 10_000 and city == "Curitiba"
    order by balance desc
    select (name, balance)                  # projection → table[(str, decimal)]
    limit 50

let by_city = query customers
    group by city
    select (city, count(), sum(balance) as total)

let orders_with_names = query orders
    join customers on orders.customer_id == customers.id
    select (orders.id, customers.name, orders.total)

for c in vip
    io.println("{c.name}: {c.balance}")

persist table[Customer] customers at "data/customers"     # ACID, crash-safe, concurrent
customers.insert(Customer(id: 1, name: "Ana", …))?
customers.update(where: |c| c.id == 1, set: |c| c.balance += 10)?

12. Compile-time programming [Stage 2]

comptime
    const TABLE = [i * i for i in 0..256]           # computed at compile time

fn vec[T, n: usize]() -> [T; n] = [T.default(); n]

macro unless(cond, body)                             # hygienic AST macro
    quote
        if not $cond
            $body

unless x > 0
    io.println("non-positive")

@derive(Show) type P = record …                      # derive = macro over the type AST
type Row = schema "orders.sql"                       # type provider: types from a schema at comptime

13. Machine level [Stage 5]

@repr(c) type Header = record { magic: u32, len: u16, flags: u8, _pad: u8 }
@packed  type Pixel  = record { r: u8, g: u8, b: u8 }
@bits    type Status = record { ready: 1, error: 1, mode: 2, _: 4 }     # bit fields
@mmio(0x4000_0000) var UART: UartRegs

unsafe
    let p: ptr[u8] = buf.as_ptr()
    p[3] = 0xFF
    asm "mov {out}, cr3" out = own cr3: u64

kernel fn saxpy(a: f32, x: tensor[f32, n], y: mut tensor[f32, n])
    for i in par 0..n
        y[i] = a * x[i] + y[i]

@target(freestanding) @no_std
pub fn _start() -> never / {interrupt} …

14. AI constructs [Stage 4]

type Mlp = model
    w1: tensor[f32, 784, 128]
    b1: tensor[f32, 128]
    w2: tensor[f32, 128, 10]
    b2: tensor[f32, 10]
    fn forward(self, x: tensor[f32, batch, 784]) -> tensor[f32, batch, 10]
        (x @ self.w1 + self.b1).relu() @ self.w2 + self.b2

fn loss(m: Mlp, x, y) -> f32 = m.forward(x).cross_entropy(y)
let g = grad(loss)                               # ∇ wrt the first argument, compiled
var m = Mlp.init(rand)
for (x, y) in batches
    m -= lr * g(m, x, y)

fn coin(prior: Beta) -> f64 / {prob}
    let p = sample prior
    observe flips ~ Bernoulli(p)
    p
let post = infer coin(Beta(1, 1)) with mcmc(steps: 10_000)

extern llm fn extract(text: str) -> Invoice ! LlmError / {llm}
    """Extract invoice fields. Dates ISO-8601, amounts decimal."""
    ensures result.total == result.items.sum(.amount)

15. Standard library map (names are normative; contents grow by stage)

std.core (prelude: Option, Result, Iter, Show, Eq, Ord, Clone, Drop, Default), std.io, std.fs, std.fmt, std.str, std.collections, std.math, std.decimal, std.time, std.rand, std.sync, std.task, std.actor, std.chan, std.net, std.http, std.json, std.data (table, query, index, persist, adapters), std.report, std.ui, std.sh, std.regex, std.alloc, std.mem, std.os, std.os.cap, std.ffi, std.simd, std.tensor, std.nn, std.prob, std.llm, std.rules, std.neuro, std.test, std.bench, std.prove, std.build.


16. The lume-core IR (normative kernel) [Stage 0]

Everything above desugars to a small typed core:


17. Diagnostics (normative format)

Every diagnostic: code (L lexical, P parse, T type, O ownership, E effect, M module, C contract, W warning), severity, message (one sentence, plain English, names the concept), span (file, line, col, len), labels (secondary spans), expected/actual (when applicable), help (a suggested fix, ideally as a text edit), principle (optional P#), doc (URL/anchor into the spec). --json emits one JSON object per diagnostic; lume agent streams them.


18. Editions and conformance