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.
// Arithmetic
sum = 1 + 2
diff = 5 - 3
prod = 4 * 2
quot = 10 / 3
rem = 10 % 3
// 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. Add explicit types when needed for clarity or error types.
// Types fully inferred
fn double(x) { x * 2 }
fn add(a, b) { a + b }
fn greet(name) { 'Hello, ' + name }
// Explicit types when needed
fn divide(a Int, b Int) Int!Error {
if b == 0 { error Error{ message: 'divide by zero' } }
else { a / b }
}
// Call functions
println(double(21)) // 42
println(add(10, 5)) // 15
println(greet('world')) // Hello, worldGenerics
Use lowercase type variables for polymorphic functions. The type checker infers concrete types at each call site.
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),
}
}
// Works with any type
n = identity(42) // Int
s = identity('hello') // String
head = first([1, 2, 3]) or 0First-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)
double_then_add(5) // 11
// Higher-order patterns
fn twice(x, f) { f(f(x)) }
twice(3, fn(n) { n + 1 }) // 5Recursion
Functions can call themselves. AL optimizes tail recursion.
fn factorial(n Int) Int {
if n <= 1 { 1 }
else { n * factorial(n - 1) }
}
fn fibonacci(n Int) Int {
match n {
0 -> 0,
1 -> 1,
else -> fibonacci(n - 1) + fibonacci(n - 2),
}
}
println(factorial(5)) // 120
println(fibonacci(10)) // 55Everything is an expression
No statements. If/else, match, and blocks all return values. The last expression in a block is its value.
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 {
Ok('special') -> 'matched literal!',
Ok(value) -> 'got: $value',
Err(e) -> 'error: $e',
}
}
// Use it
handle(Ok('special')) // 'matched literal!'
handle(Ok('hello')) // 'got: hello'
handle(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 = Some(42) // Short formGeneric enums
Enums can have type parameters for flexible data modeling.
enum Maybe(t) {
Just(t)
Nothing
}
enum Result(ok, err) {
Ok(ok)
Err(err)
}
// Type inferred from usage
x Maybe = Just(42)
y Maybe = Nothing
result Result = Ok('success')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'
// In function returns
fn divide(a Int, b Int) (Int, Int) {
(a / b, a % b)
}
quotient, remainder = divide(10, 3)Arrays
Ordered collections of values. Access by index, concatenate with +.
numbers = [1, 2, 3, 4, 5]
names = ['alice', 'bob', 'charlie']
// Access by index
first = numbers[0] // 1
second = names[1] // 'bob'
// Concatenate
combined = [1, 2] + [3, 4] // [1, 2, 3, 4]
// 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.
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'.
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: -1, 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}')
-1
}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(Person{ name: 'alice', age: 30 })
// 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
content = read_file('data.txt')
write_file('output.txt', 'hello world')
// TCP networking
listener = tcp_listen(8080)
client = tcp_accept(listener)
data = tcp_read(client)
tcp_write(client, 'HTTP/1.1 200 OK\r\n\r\nHello')
tcp_close(client)