Errors & Types
Lumina has no null and no exceptions. A value that can be missing has type T?, and an operation that can fail has type T!E. Both are in the type signature, match over them must be exhaustive, and short operators keep handling them cheap. Reading a field straight off a T? without unwrapping it is a compile error in progress, so you unwrap with ??, ?., match or if Some(x) = ....
Optional: T?
struct User {
name: String
age: Integer?
}
function findUser(id: Integer): User? {
if id == 1 {
return Some(User { name: "Ada", age: Some(36) })
}
return None
}
There are three ways to use an optional.
// 1. if-pattern: bind the value in the true branch only
if Some(user) = findUser(1) {
age = user.age ?? 0 // 2. ?? gives a default
console.println(`{user.name} is {age}`)
}
// 3. match, as an expression
name = match findUser(2) { Some(u) => u.name, None => "nobody" }
For nested optionals, ?. short-circuits: u.address?.city ?? "unknown".
Many standard operations return optionals instead of failing. For example, "42".toInteger() gives Some(42), and "abc".toInteger() gives None.
Result: T!E
Integer!ParseError reads as "an Integer, or a ParseError". You construct it with Ok(...) and Err(...). The error type is usually an enum, and variants can carry data:
enum ParseError {
Empty
NotANumber { text: String }
}
function parseAge(text: String): Integer!ParseError {
if text.length() == 0 {
return Err(ParseError.Empty)
}
return match text.toInteger() {
Some(n) => Ok(n),
None => Err(ParseError.NotANumber { text: text })
}
}
? propagates
The ? operator unwraps an Ok, or returns the Err from the current function straight away:
function doubledAge(text: String): Integer!ParseError {
age = parseAge(text)?
return Ok(age * 2)
}
This is the same operator as Rust's, and it plays the role of Go's if err != nil { return err }, without the repetition. The error stays in the signature, so a caller can always see what can fail.
Handling at the edge
function describe(e: ParseError): String = match e {
ParseError.Empty => "empty input",
ParseError.NotANumber { text } => `not a number: {text}`
}
for input in ["21", "", "abc"] {
match doubledAge(input) {
Ok(v) => console.println(`ok {v}`),
Err(e) => console.println(`error: {describe(e)}`)
}
}
ok 42
error: empty input
error: not a number: abc
Enums and exhaustive match
Enums are tagged unions, as in Rust or Swift. match must cover every variant, so adding a variant breaks every match that forgot about it, at compile time:
enum Light {
Red
Yellow
Green
}
function next(l: Light): Light = match l {
Light.Red => Light.Green,
Light.Green => Light.Yellow
}
error[E2011]: main.lumina:7:40: non-exhaustive match on `Light` — missing variants: Light.Yellow
help: Add arms for all variants or use `_` wildcard
Patterns can destructure payloads, match literals, use guards, and fall back to _:
enum Shape {
Circle { radius: Float }
Rect { w: Float, h: Float }
}
function area(s: Shape): Float = match s {
Shape.Circle { radius } => 3.14159 * radius * radius,
Shape.Rect { w, h } => w * h
}
function sign(n: Integer): String = match n {
0 => "zero",
x if x < 0 => "negative",
_ => "positive"
}
Union types and narrowing
A union String | Integer is a value of either type. To narrow it, write a typed binding in a match arm:
function show(v: String | Integer): String = match v {
s: String => `text "{s}"`,
n: Integer => `number {n}`
}
Inside the arm, s has type String. Watch out for one thing: a bare name such as String => is a binding that catches everything, not a type test. Always write name: Type => when you mean to narrow.
How this compares
| Concept | TypeScript | Go | Rust | Lumina |
|---|---|---|---|---|
| Missing value | `T \ | undefined` | nil / zero value | Option<T> |
| Failure | throw | (T, error) | Result<T, E> | T!E |
| Propagate | try/catch | if err != nil | ? | ? |
| Default | ?? | manual | .unwrap_or() | ?? |
| Exhaustive switch | partial (never trick) | no | yes | yes |
Gotchas
- Inspect an optional with
matchorif Some(x) = ...; there is noopt.isSome()method. - Dropping a
T!Eon the floor is a compile error (E2100, "unused Result ... error may be lost"). You canmatchit, propagate it with?, bind it, or discard it deliberately with_ = risky(). - Panics such as an out-of-bounds index or a strict-mode integer overflow are for bugs, not for control flow. They stop the program with a message.
Next: Generics & Interfaces