Syntax

If you can read TypeScript, you can read Lumina. The keywords are words (function, struct, match), types follow a colon, and lambdas use =>. There are no semicolons, no let/const noise and no lifetimes.

A complete program

struct Point {
    x: Integer
    y: Integer

    public function sum(self): Integer = self.x + self.y

    public function scaled(self, k: Integer): Point = Point { x: self.x * k, y: self.y * k }
}

function greet(name: String, greeting: String = "Hello"): String = `{greeting}, {name}!`

function classify(n: Integer): String =
    if n < 0 { "negative" } else if n == 0 { "zero" } else { "positive" }

function main(console: Console): Integer {
    p = Point { x: 3, y: 4 }
    console.println(`sum = {p.scaled(2).sum()}`)
    console.println(greet("Ada"))
    console.println(greet("Linus", "Hi"))

    numbers = [1, 2, 3, 4, 5, 6]
    evens = numbers.filter(n => n % 2 == 0)
    squares = evens.map(n => n * n)
    console.println(`squares = {squares}`)

    mut total = 0
    for i in 1..=5 {
        total += i
    }
    console.println(`total = {total}, {classify(-2)}`)
    return 0
}
sum = 14
Hello, Ada!
Hi, Linus!
squares = [4, 16, 36]
total = 15, negative

Here is what is going on.

Variables

name = "Ada"          // immutable, type inferred as String
mut counter = 0       // mutable
age: Integer = 36     // explicit type annotation

A binding without mut cannot be reassigned. This is the default, so the common case needs no keyword. Mutability covers the details.

Types

The primitive types have full names: Integer (64-bit), Float, String, Boolean, Char and Void. Collections are generic: Array<Integer>, written as a literal [1, 2, 3]. A tuple type is written (Integer, Integer). A ? after a type makes it optional (User?), and ! attaches an error type (Integer!ParseError). See Errors & Types.

Char is its own type. It does not silently convert to Integer.

Functions

function add(a: Integer, b: Integer): Integer {
    return a + b
}

function add2(a: Integer, b: Integer): Integer = a + b   // expression body

Structs and methods

Methods live inside the struct body. They take self (read-only) or mut self (may modify fields):

struct Counter {
    public count: Integer

    public function increment(mut self): Void {
        self.count = self.count + 1
    }
}

Fields and methods are private by default. If you read c.count from outside a struct whose field is not public, you get error E1301, and the message tells you to write public count: .... You build a struct with a literal, Point { x: 3, y: 4 }.

Strings

Backtick strings interpolate any expression in {}:

greeting = `Hello {name}, you have {items.length()} items`

Ordinary "double quoted" strings are plain literals. Common methods include length(), split(), substring() and toInteger(). The last one returns an Integer?, because parsing can fail.

Control flow

Lambdas and collections

evens = numbers.filter(n => n % 2 == 0)
squares = evens.map(n => n * n)

Lambdas use =>, as in TypeScript. Arrays have map, filter, push, length and indexing (xs[0]). Indexing is bounds-checked, and an out-of-range index stops the program with an error message instead of reading garbage.

Tuples and destructuring

function minMax(xs: Array<Integer>): (Integer, Integer) {
    mut lo = xs[0]
    mut hi = xs[0]
    for x in xs {
        if x < lo { lo = x }
        if x > hi { hi = x }
    }
    return (lo, hi)
}

function norm1({ x, y }: Point): Integer = x + y

(lo, hi) = minMax([4, 9, 1, 7])     // 1..9

A function can return several values as a tuple, and you can destructure a struct right in the parameter list.

Optional chaining

city = u.address?.city ?? "unknown"

?. walks through optional fields, and ?? supplies a default. Both are explained in Errors & Types.

Next: Mutability