Language reference
Variables and types
Variables are declared with assignment. Types are inferred from values. Reassignment shadows the previous binding.
// Types inferred from values
count = 42 // Int
name = 'alice' // String
active = true // Bool
nothing = none // None
// Reassignment shadows the previous binding
x = 10
x = x + 1 // x is now 11
// Arrays
numbers = [1, 2, 3, 4, 5]
first = numbers[0]Constants
Top-level constants are declared with 'const'. They cannot be reassigned.
const pi = 314
const app_name = 'my app'
const max_retries = 3
greeting = 'Welcome to ${app_name}!'Basic operators
Standard arithmetic, comparison, and logical operators.
a = 5
b = 3
// Arithmetic
sum = a + b
diff = a - b
prod = a * b
quot = a / b
rem = a % b
// Comparison
eq = a == b
neq = a != b
lt = a < b
lte = a <= b
// Logical
and_result = true && false
or_result = true || false
not_result = !trueFunctions with type inference
Function parameter and return types are inferred from usage. Single-expression bodies don't need braces. Add explicit types when needed.
// Types fully inferred — single-expression body
fn double(x) x * 2
fn add(a, b) a + b
fn greet(name) 'Hello, ' + name
// Block body when you need multiple statements
fn abs(n) {
if n < 0 { -n } else { n }
}
// Explicit types when needed
struct DivError { message String }
fn divide(a Int, b Int) Int!DivError {
if b == 0 { error DivError{ message: 'divide by zero' } }
else { a / b }
}
// Call functions
println(double(21)) // 42
println(add(10, 5)) // 15
println(greet('world')) // Hello, worldGenerics
Single uppercase letters are type variables. The type checker infers concrete types at each call site — the same function can be called with different types.
fn identity(x A) A { x }
fn first(arr []A) ?A {
match arr {
[] -> none,
[head, ..] -> head,
}
}
fn map_array(arr []A, f fn(A) B) []B {
match arr {
[] -> [],
[head, ..rest] -> [f(head), ..map_array(rest, f)],
}
}
// Same function, different types
n = identity(42) // Int
s = identity('hello') // String
head = first([1, 2, 3]) or 0
doubled = map_array([1, 2, 3], fn(x) x * 2) // [2, 4, 6]First-class functions
Functions are values. Pass them around, store them in variables, return them from functions.
fn apply(x, f) f(x)
fn compose(f, g) fn(x) f(g(x))
double = fn(n) n * 2
add_one = fn(n) n + 1
result = apply(5, double) // 10
// Compose functions
double_then_add = compose(add_one, double)
println(double_then_add(5)) // 11
// Higher-order patterns
fn twice(x, f) f(f(x))
println(twice(3, fn(n) n + 1)) // 5Recursion
Functions can call themselves. When the recursive call is the last thing a function does, AL reuses the stack frame — no stack overflow on deep recursion.
fn factorial(n Int) Int {
if n <= 1 { 1 }
else { n * factorial(n - 1) } // not tail recursive (n * ...)
}
// Tail-recursive with accumulator — constant stack space
fn factorial_iter(n Int, acc Int) Int {
if n <= 1 { acc }
else { factorial_iter(n - 1, n * acc) } // tail call
}
fn sum(arr []Int, acc Int) Int {
match arr {
[] -> acc,
[head, ..rest] -> sum(rest, acc + head), // tail call
}
}
println(factorial(5)) // 120
println(factorial_iter(5, 1)) // 120
println(sum([1, 2, 3, 4, 5], 0)) // 15Everything is an expression
No statements. If/else, match, and blocks all return values. The last expression in a block is its value.
x = 5
result = if x > 0 {
'positive'
} else {
'non-positive'
}
// Blocks are expressions
total = {
a = 10
b = 20
a + b
}
// Use in function bodies
fn abs(n Int) Int {
if n < 0 { -n } else { n }
}Pattern matching
Match on values, or-patterns, enums, literal payloads, and arrays. Exhaustive checking ensures you handle all cases.
fn describe(x Int) String {
match x {
0 -> 'zero',
1 | 2 | 3 -> 'small',
else -> 'other',
}
}
// Match on arrays
fn sum(arr []Int) Int {
match arr {
[] -> 0,
[head, ..tail] -> head + sum(tail),
}
}
// Wildcard pattern
fn ignore_second(pair) {
match pair {
[a, _] -> a,
else -> none,
}
}Enum pattern matching
Match on enum variants and bind payload values. Match literal payloads for specific cases.
enum Result {
Ok(String)
Err(String)
}
fn handle(r Result) String {
match r {
Result.Ok('special') -> 'matched literal!',
Result.Ok(value) -> 'got: ${value}',
Result.Err(e) -> 'error: ${e}',
}
}
println(handle(Result.Ok('special'))) // 'matched literal!'
println(handle(Result.Ok('hello'))) // 'got: hello'
println(handle(Result.Err('oops'))) // 'error: oops'Structs
Define data structures with named fields. Access fields with dot notation.
struct Person {
name String
age Int
}
struct Point {
x Int
y Int
}
// Create instances
person = Person{ name: 'alice', age: 30 }
origin = Point{ x: 0, y: 0 }
// Access fields
println(person.name) // alice
println(person.age) // 30Generic structs
Structs can have type parameters. Type arguments are inferred from field values.
struct Box(T) {
value T
}
struct Pair(A, B) {
first A
second B
}
// Type args inferred from values
int_box = Box{ value: 42 }
pair = Pair{ first: 'hello', second: 123 }
// Or specify explicitly
Box(String){ value: 'world' }Enums
Model variants with enums. Variants can carry payloads of any type.
enum Status {
Active
Inactive
Banned(String)
}
enum Option {
Some(Int)
Empty
}
// Create enum values
status = Status.Active
banned = Status.Banned('spam')
some_value = Option.Some(42)Generic enums
Enums can have type parameters for flexible data modeling.
enum Maybe(T) {
Just(T)
Nothing
}
enum Either(L, R) {
Left(L)
Right(R)
}
// Type inferred from usage
x Maybe = Maybe.Just(42)
y Maybe = Maybe.Nothing
result Either = Either.Left('error')Tuples
Fixed-size collections of mixed types. Access elements by index.
// Create tuples
pair = (1, 'hello')
triple = (true, 42, 'world')
// Access by index
first = pair.0 // 1
second = pair.1 // 'hello'
// Return from functions (type inferred)
fn divide(a, b) (a / b, a % b)
// Destructure with parentheses
(quotient, remainder) = divide(10, 3)Arrays
Ordered collections of values. Access by index, concatenate with spread.
numbers = [1, 2, 3, 4, 5]
names = ['alice', 'bob', 'charlie']
// Access by index
first = numbers[0] // 1
second = names[1] // 'bob'
// Concatenate with spread
combined = [..[1, 2], ..[3, 4]] // [1, 2, 3, 4]
more = [0, ..numbers, 6] // [0, 1, 2, 3, 4, 5, 6]
// Nested arrays
matrix = [[1, 2], [3, 4]]Ranges
Create ranges with the '..' operator. Useful for iteration patterns.
// Create a range
r = 0..10
// Ranges in expressions
fn in_range(n Int, start Int, end Int) Bool {
n >= start && n < end
}Strings and interpolation
Strings use single quotes. Embed expressions with $ for variables or ${} for complex expressions.
struct Person {
name String
age Int
}
name = 'world'
greeting = 'Hello, $name!'
math = 'Result: ${1 + 2 * 3}'
// Multi-part interpolation
person = Person{ name: 'Alice', age: 30 }
bio = '${person.name} is ${person.age} years old'
// Escape sequences
quote = 'She said \'hello\''Optional values
Functions that might not return a value use ? in their return type. Handle missing values with 'or'.
struct User {
id Int
name String
}
fn find_user(id Int) ?User {
if id == 0 { none }
else { User{ id: id, name: 'found' } }
}
// Provide a default with 'or'
user = find_user(0) or User{ id: 0, name: 'guest' }
// Handle with receiver
result = find_user(0) or missing -> {
println('User not found')
User{ id: 0, name: 'default' }
}Error handling
Functions that can fail use ! with an error type. Handle errors with 'or', optionally binding the error.
struct DivisionError {
message String
}
fn divide(a Int, b Int) Int!DivisionError {
if b == 0 {
error DivisionError{ message: 'divide by zero' }
} else {
a / b
}
}
// Provide default on error
safe = divide(10, 0) or 0
// Handle error with receiver
result = divide(10, 0) or err -> {
println('Error: ${err.message}')
0
}Built-in functions
Core functions available without imports.
// Print any value
println(42)
println('hello')
println([1, 2, 3])
// Convert to string representation
s = inspect([1, 2, 3]) // '[1, 2, 3]'
// Split strings
parts = str_split('a,b,c', ',') // ['a', 'b', 'c']I/O operations (experimental)
File and network I/O requires the --experimental-shitty-io flag.
// Run with: al run --experimental-shitty-io file.al
// File operations — return Result, handle with 'or'
content = read_file('data.txt') or ''
None = write_file('output.txt', 'hello world') or none
// TCP networking — wrap in a failable function to propagate errors
fn serve() None!String {
listener = tcp_listen(8080) or err -> { error err }
client = tcp_accept(listener) or err -> { error err }
data = tcp_read(client) or ''
None = tcp_write(client, 'HTTP/1.1 200 OK\r\n\r\nHello') or none
tcp_close(client) or none
}