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
// Reassignment shadows the previous binding
x = 10
x = x + 1 // x is now 11
// Arrays — indexing returns Option(t)
numbers = [1, 2, 3, 4, 5]
first = numbers[0] or 0
println('${name}: ${count}') // alice: 42
println(x) // 11
println(first) // 1Constants
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}!'
println(greeting) // Welcome to my app!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 = !True
println('${sum} ${diff} ${prod} ${quot} ${rem}') // 8 2 15 1 2
println('${eq} ${neq} ${lt} ${lte}') // False True False False
println('${and_result} ${or_result} ${not_result}') // False True FalseFunctions with type inference
Function parameter and return types are inferred from usage. Named function bodies are always blocks. Add explicit types when needed.
// Types fully inferred
fn double(x) { x * 2 }
fn add(a, b) { a + b }
fn greet(name) { 'Hello, ' + name }
// With explicit types
fn abs(n Int) Int { if n < 0 { -n } else { n } }
type DivError { message String }
fn divide(a Int, b Int) Result(Int, DivError) {
if b == 0 { Err(DivError(message: 'divide by zero')) }
else { Ok(a / b) }
}
// Call functions
println(double(21)) // 42
println(add(10, 5)) // 15
println(greet('world')) // Hello, worldGenerics
Type variables are lowercase identifiers; concrete types start with an uppercase letter. The type checker infers concrete types at each call site, so the same function can be called with different types.
fn identity(x a) a { x }
fn first(arr Array(a)) Option(a) {
match arr {
[] -> None
[head, ..] -> Some(head)
}
}
fn map_array(arr Array(a), f fn(a) b) Array(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]
println('${n} ${s}') // 42 hello
println(head) // 1
println(doubled) // [2, 4, 6]First-class functions and lambdas
Functions are values. Anonymous functions (fn(...) body) infer their return type and don't need braces for a single-expression body.
fn apply(x, f) { f(x) }
fn compose(f, g) { fn(x) f(g(x)) }
// Braceless lambda — body starts right after )
double = fn(n) n * 2
add_one = fn(n) n + 1
result = apply(5, double) // 10
println(result) // 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 Array(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
If, match, and blocks all return values. Every if requires an else. The last expression in a block is its value.
x = 5
result = if x > 0 {
'positive'
} else {
'non-positive'
}
// Blocks are expressions — and double as grouping
total = {
a = 10
b = 20
a + b
}
nine = {1 + 2} * 3
// Use in function bodies
fn abs(n Int) Int {
if n < 0 { -n } else { n }
}
println(result) // positive
println(total) // 30
println(nine) // 9Pattern matching
Match on values, or-patterns, constructors, 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 Array(Int)) Int {
match arr {
[] -> 0
[head, ..tail] -> head + sum(tail)
}
}
// Guards: pattern matches first, then condition filters
fn classify(n Int) String {
match n {
x if x < 0 -> 'negative'
0 -> 'zero'
else -> 'positive'
}
}Constructor pattern matching
Match on type constructors and bind payload values. Match literal payloads for specific cases.
type Reply {
Ack(value String)
Nack(value String)
}
fn handle(r Reply) String {
match r {
Ack('special') -> 'matched literal!'
Ack(value) -> 'got: ${value}'
Nack(e) -> 'failed: ${e}'
}
}
println(handle(Ack('special'))) // 'matched literal!'
println(handle(Ack('hello'))) // 'got: hello'
println(handle(Nack('oops'))) // 'failed: oops'Types
Define data with the 'type' keyword. Every field has a label. Single-constructor types may list fields directly; access with dot notation.
type Person {
name String
age Int
}
type Point { x Int y Int }
// Create instances — constructors are function calls
person = Person(name: 'alice', age: 30)
origin = Point(x: 0, y: 0)
// Access fields
println(person.name) // alice
println(origin.x) // 0
// Spread to copy with overrides
older = Person(..person, age: 31)
println(older.age) // 31Generic types
Types can have type parameters. Type arguments are inferred from field values.
type Box(t) { value t }
type Pair(a, b) {
first a
second b
}
// Type args inferred from values
int_box = Box(value: 42)
pair = Pair(first: 'hello', second: 123)
println(int_box.value) // 42
println(pair.first) // helloMultiple constructors
A type with multiple constructors models alternatives. Constructors are first-class — they're just functions.
type Status {
Active
Inactive
Banned(reason String)
}
type Shape {
Circle(r Float)
Rect(w Float h Float)
}
// Create values
status = Active
_banned = Banned('spam')
_c = Circle(r: 1.0)
println(status) // Active
// Constructors are functions — pass them around
fn map3(a, b, c, f) { [f(a), f(b), f(c)] }
xs = map3(1.0, 2.0, 3.0, Circle)
println(xs)Generic constructors
Constructor types can have type parameters for flexible data modeling.
type Maybe(t) {
Just(value t)
Nothing
}
type Either(l, r) {
Left(value l)
Right(value r)
}
// Type inferred from usage
x = Just(42)
y = Nothing
result = Left('failed')
println(x) // Just(42)
println(y) // Nothing
println(result) // Left(failed)Type aliases
An alias is a transparent second name for a type — they unify freely. Use a single-constructor type instead for a distinct newtype.
type Pair(a, b) { first a second b }
type Id = Int
type StringPair = Pair(String, String)
// Alias: Email IS String
type Email = String
// Newtype: UserId is distinct from Int
type UserId { UserId(value Int) }Tuples
Fixed-size collections of mixed types with two or more elements. Access elements by index.
// Create tuples (always 2+ elements; (e) is a syntax error)
pair = (1, 'hello')
_triple = (True, 42, 'world')
// Access by index
first = pair.0 // 1
second = pair.1 // 'hello'
println('${first} ${second}') // 1 hello
// Return from functions
fn divide(a, b) { (a / b, a % b) }
// Destructure with parentheses
(quotient, remainder) = divide(10, 3)
println('${quotient} r${remainder}') // 3 r1Arrays
Ordered collections of values. Indexing returns Option(t). Build and concatenate with spread.
numbers = [1, 2, 3, 4, 5]
names = ['alice', 'bob', 'charlie']
// Indexing returns Option — unwrap with 'or'
first = numbers[0] or 0 // 1
second = names[1] or 'anon' // 'bob'
println('${first} ${second}') // 1 bob
// Build with spread
combined = [..[1, 2], ..[3, 4]] // [1, 2, 3, 4]
more = [0, ..numbers, 6] // [0, 1, 2, 3, 4, 5, 6]
println(combined) // [1, 2, 3, 4]
println(more) // [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
println(r[3] or -1) // 3
// Ranges in expressions
fn in_range(n Int, start Int, end Int) Bool {
n >= start && n < end
}
println(in_range(5, 0, 10)) // TrueBinaries
The Binary type holds raw bits. Construct with '<<>>' — each bare segment is one byte. Match the same way; a length mismatch falls through, so an 'else' or ':binary' tail is required.
import al/binary
import al/string
// Three bytes
header = <<1, 2, 3>>
println(binary.byte_size(header)) // 3
// Bit-sized segments — two 4-bit nibbles pack into one byte
packed = <<1:4, 2:4>>
println(string.inspect(packed)) // <<18>>
// Match on structure: a and b each bind one byte
sum = match binary.from_string('AB') {
<<a, b>> -> a + b
else -> 0
}
println(sum) // 131 (65 + 66)
// ':binary' binds the variable-length remainder
fn drop_one(b Binary) Binary {
match b {
<<_, rest:binary>> -> rest
else -> b
}
}Strings and interpolation
Strings use single quotes. Embed expressions with $ for variables or ${} for complex expressions.
type Person { name String age Int }
name = 'world'
greeting = 'Hello, $name!'
math = 'Result: ${1 + 2 * 3}'
println(greeting) // Hello, world!
println(math) // Result: 7
// Multi-part interpolation
person = Person(name: 'Alice', age: 30)
bio = '${person.name} is ${person.age} years old'
println(bio) // Alice is 30 years old
// Escape sequences
_quote = 'She said \'hello\''Optional values
Option(t) models a value that might be absent. The constructors Some and None are in the prelude. Unwrap with 'or'.
type User { id Int name String }
fn find_user(id Int) Option(User) {
if id == 0 { None }
else { Some(User(id: id, name: 'found')) }
}
// Provide a default with 'or'
user = find_user(0) or User(id: 0, name: 'guest')
println(user.name) // guest
// Array indexing returns Option too
names = ['alice', 'bob']
who = names[5] or 'nobody'
println(who) // nobodyResult and error handling
Result(t, e) models success or failure. The constructors Ok and Err are in the prelude. Unwrap with 'or', optionally binding the error value.
type DivisionError { message String }
fn divide(a Int, b Int) Result(Int, DivisionError) {
if b == 0 {
Err(DivisionError(message: 'divide by zero'))
} else {
Ok(a / b)
}
}
// Provide default on failure
safe = divide(10, 0) or 0
println(safe) // 0
// Bind the error value
result = divide(10, 0) or err -> {
println('Failed: ${err.message}')
0
}
println(result) // 0Modules and imports
One file is one module. Mark exports with 'pub'. Import by path; access via the qualifier or bring names in directly.
// === main.al ===
import ./util
import ./util as u
import ./util.{quote, Id}
import al/net/socket.{Socket} // standard library
println(util.quote('hi')) // qualified
println(u.quote('hi')) // module alias
println(quote('hi')) // selective import
// === util.al ===
// pub fn quote(s String) String { '"' + s + '"' }
// pub type Id { Id(value Int) }Tuple and range patterns
Match on tuple structure and integer ranges. 'else' is the catch-all arm; '_' is for nested wildcards.
fn classify(p (Int, String)) String {
match p {
(0, msg) -> 'zero: ${msg}'
(1..10, msg) -> 'small: ${msg}'
(_, msg) -> 'other: ${msg}'
}
}
fn label(x Int) String {
match x {
0..10 -> 'digit'
10..100 -> 'tens'
else -> 'big'
}
}
println(classify((5, 'hi')))
println(label(42))Mutual recursion
Top-level 'fn' declarations see one another regardless of order.
fn is_even(n Int) Bool {
if n == 0 { True } else { is_odd(n - 1) }
}
fn is_odd(n Int) Bool {
if n == 0 { False } else { is_even(n - 1) }
}
println(is_even(10))
println(is_odd(7))Field-exhaustive constructor patterns
Positional patterns must match arity; labeled patterns may use '..' to ignore the rest.
type User { name String age Int email String }
fn name_of(u User) String {
match u {
User(name: n, ..) -> n
}
}
u = User(name: 'alice', age: 30, email: 'a@b.c')
println(name_of(u))Discards and unused variables
Unused let-bindings and parameters are an error. Prefix a name with '_' to mark it intentional, or write 'TypeName = expr' to assert a type and drop the value. Single-constructor types can be destructured at binding position.
// '_' prefix marks a binding as intentionally unused
fn handle(_conn, msg) { println(msg) }
// Typed discard — checks the type, then drops the value
Nil = println('side effect') // println returns Nil
Int = 1 + 2 // asserts the rhs is Int
// Single-constructor types destructure irrefutably
type Point { x Int y Int }
Point(x: a, y: b) = Point(x: 3, y: 4)
println(a + b) // 7Built-in functions
Core functions available without imports.
import al/string
// Print any value
println(42)
println('hello')
println([1, 2, 3])
// Convert to string representation
s = string.inspect([1, 2, 3]) // '[1, 2, 3]'
println(s)
// Strings module
parts = string.split('a,b,c', ',') // ['a', 'b', 'c']
n = string.length('hello') // 5
has = string.contains('hello', 'ell') // True
println(parts) // [a, b, c]
println('${n} ${has}') // 5 TrueFloats
Float arithmetic uses the same operators as Int. The 'al/float' module has conversions and helpers.
import al/float
// Float to Int
println(float.round(2.7)) // 3
println(float.floor(2.7)) // 2
println(float.ceil(2.1)) // 3
println(float.truncate(2.9)) // 2
// Int to Float
half = float.from_int(1) / 2.0
// Helpers
diff = 1.0 - 3.5
println(float.abs(diff)) // 2.5
println(float.max(1.2, 3.4)) // 3.4
println(float.to_string(half))I/O operations (experimental)
File and network I/O requires the --experimental-shitty-io flag.
// Run with: al run --experimental-shitty-io file.al
import al/io
import al/net
import al/net/socket.{Socket}
import al/net/address
import al/binary
// Text helpers wrap the Binary I/O
content = io.read_text('data.txt') or ''
println(content)
// TCP — accept gives you a Socket that knows its peer address
fn respond(sock Socket) Nil {
body = 'Hello from AL!'
socket.write(sock, binary.from_string('HTTP/1.1 200 OK\r\n\r\n${body}')) or Nil
socket.close(sock) or Nil
}
match net.listen('0.0.0.0', 8080) {
Err(e) -> println('listen failed: ${e}')
Ok(server) -> match net.accept(server) {
Ok(sock) -> {
println('connection from ${address.to_string(sock.peer)}')
respond(sock)
}
Err(e) -> println('accept failed: ${e}')
}
}