A small, statically-typed, expression-oriented programming language.
curl -fsSL al.alistair.sh/install.sh | bashal run <file.al> Run a program
al check <file.al> Type-check without running
al build <file.al> Print the AST
AL compiles to bytecode and runs on a stack-based virtual machine. The compiler is written in V, producing a single native binary with no dependencies.
Statically typed with inference. Every expression has a type known at compile time. The type checker catches errors before your code runs, while inference keeps the syntax clean.
Expression-oriented. No statements—if/else, match, and blocks all return values.
Unified error handling. Both optional values (?T) and errors (T!E) use the same or syntax.
The parser and type checker recover from errors to report multiple issues at once:
error: Unexpected ')'
--> example.al:3:8
|
3 | x = )
| ^
error: Unexpected ']'
--> example.al:7:12
|
7 | value = ]
| ^
Found 2 errors
Type errors are caught at compile time:
error: Type mismatch: expected Int, got String
--> example.al:5:12
|
5 | return 'oops'
| ^^^^^^
error: Unknown variable: 'undefined_var'
--> example.al:8:5
|
8 | undefined_var + 1
| ^^^^^^^^^^^^^
Found 2 errors
No statements. If/else, match, and blocks all return values.
result = if x > 0 { 'positive' } else { 'negative' }
grade = match score {
90..100 -> 'A',
80..90 -> 'B',
else -> 'C',
}
Functions that might not return a value use ? in their return type. Handle with or.
fn find_user(id Int) ?User {
if id == 0 { none } else { User{ id: id, name: 'found' } }
}
user = find_user(0) or User{ id: 0, name: 'guest' }
Functions that can fail use ! with an error type. Handle with or.
fn divide(a Int, b Int) Int!DivisionError {
if b == 0 {
error DivisionError{ message: 'divide by zero' }
} else {
a / b
}
}
safe = divide(10, 0) or 0
result = divide(10, 2) or err -> {
println('Error: ${err.message}')
0
}
Match on values, enums, and literal payloads.
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',
}
}
struct Person {
name String,
age Int,
}
enum Status {
Active,
Inactive,
Banned(String),
}
person = Person{ name: 'alice', age: 30 }
status = Status.Banned('spam')
fn apply(x Int, f fn(Int) Int) Int {
f(x)
}
double = fn(n Int) Int { n * 2 }
result = apply(5, double)
name = 'world'
greeting = 'Hello, $name!'
math = 'Result: ${1 + 2 * 3}'
Types are inferred from context. Annotate when needed, skip when obvious.
// Types inferred
count = 42
name = 'alice'
numbers = [1, 2, 3]
// Explicit annotations
fn add(a Int, b Int) Int {
a + b
}
Use lowercase type variables for polymorphic functions.
fn identity(x a) a {
x
}
fn first(arr []a) a {
arr[0]
}
fn map(arr []a, f fn(a) b) []b {
result = []
for item in arr {
result = result + [f(item)]
}
result
}
// Works with any type
x = identity(42)
y = identity('hello')
head = first([1, 2, 3])
MIT