Memory Model
Zero uses value semantics throughout. Every variable owns its data, and there are no pointers, no garbage collector, and no reference counting.
- Stack-based — All values live on the stack and are freed automatically when they go out of scope.
- Deep copy on assignment — Assigning one variable to another creates an independent copy. Modifying the copy never affects the original.
- Single-owner mutation — At any point during execution, there is only one mutable path to each piece of data. This eliminates aliasing bugs.
reffor borrowing — When a function needs to modify a caller's variable, it takes arefparameter. The caller explicitly passesref x, making mutation visible at the call site.- Handles for indirection — Since there are no pointers, handles (
@) serve as type-safe indices into arrays. - Destructors for cleanup — Types with destructors (
~TypeName()) run cleanup code when they go out of scope. Destructible types cannot be copied, enforcing unique ownership. They can be reassigned (old value is destroyed first) and used in variant types.
Getting Started
Every Zero program begins with a main function — the entry point that runs when the program starts. Use println() to print a line of text to standard output.
fun main()
println("Hello, World!")
end
Zero is a compiled language that combines the performance of systems languages with the expressiveness of modern high-level languages. It supports type inference, pattern matching, built-in concurrency, automatic differentiation, and much more.
Variables
Zero has three storage classes for variables, each offering a different level of mutability:
| Keyword | Description |
|---|---|
def | Compile-time constant — evaluated before the program runs |
let | Runtime immutable — set once, cannot be reassigned |
var | Runtime mutable — can be reassigned freely |
def pi = 3.14159 # Evaluated at compile time
let x = 42 # Immutable at runtime
var y = 10 # Mutable
y = 20 # OK
Type Inference
Types are inferred automatically from the initial value. The default integer type is i32 and the default float type is f32:
let a = 100 # i32
let b = 3.14 # f32
let c = "hello" # string
let d = true # bool
You can also specify the type explicitly with a colon:
let x: i64 = 100
var y: f64 = 3.14
Mutable Variables
var variables don't require initialization — they default to the zero value of their type (0 for numbers, "" for strings, false for booleans):
var count: i32 # Defaults to 0
var name: string # Defaults to ""
Multiple Declarations
Declare multiple variables at once using tuple syntax:
let (x: i32, y: i32) = (10, 20)
var (p: f32, q: f32) # Both default to 0.0
Chained Assignment
Assignments can be chained; the rightmost value flows left:
var x: i32
var y: i32
x = y = 100 # Both become 100
Shadowing
Variables can be redeclared in the same scope, shadowing the previous binding. The new variable can even have a different type:
var x: i32 = 10
let x = "now a string" # Shadows the previous x
Compile-Time Constants
def variables are evaluated at compile time. They can call pure functions:
fun compute() -> i32
return 100
end
def size = compute()
def doubled = size * 2
Global Variables
def, let, and var are allowed at global scope. All global initializers are evaluated at compile time:
def MAX_SIZE = 1024 ## compile-time constant
let name = "zero" ## immutable global
var counter = 0 ## mutable global
fun main()
assert counter == 0
counter = counter + 1
assert counter == 1
end
Primitive Types
Integers
| Type | Description |
|---|---|
i8, i16, i32, i64 | Signed integers |
u8, u16, u32, u64 | Unsigned integers |
bool | Boolean (true or false) |
The default integer type is i32. Use type suffixes to force a specific type:
let a = 100 # i32
let b = 100i8 # i8
let c = 100u64 # u64
Arbitrary-Width Integers
Zero supports integers of any bit width from 1 to 64 using iX (signed) and uX (unsigned). Values wrap on overflow:
var x: u5 = 31 # 5-bit unsigned (0..31)
var y: i5 = -16 # 5-bit signed (-16..15)
# Overflow wraps
assert 31u5 + 1u5 == 0u5 # Wraps around
assert ~0u5 == 31u5 # All bits set in 5-bit field
Floating Point
| Type | Description |
|---|---|
f16 | Half precision (storage only) |
f32 | Single precision (default) |
f64 | Double precision |
let x = 3.14 # f32
let y = 3.14f64 # f64
Fixed Point
Fixed-point types use iW.F or uW.F syntax where W is whole bits and F is fractional bits:
let x: i8.8 = 25.5_i8.8
let y: i16.16 = x # Implicit widening
Range-Bounded Integers
Constrain integer types to a specific range. Assigning out-of-range values causes an error:
var x: 0..100 = 50 # Exclusive upper bound
var y: 0..=100 = 50 # Inclusive upper bound
Number Literals
Numbers can be written in several bases, and underscores can be inserted anywhere for readability:
let hex = 0xFF # Hexadecimal
let bin = 0b1010 # Binary
let oct = 077 # Leading zero for octal
let big = 1_000_000 # Underscores for readability
let sci = 1.5e10 # Scientific notation
Explicit Type Casting
Zero supports implicit widening conversions (e.g., i32 to i64), but narrowing or lossy conversions require an explicit cast. Use the target type name as a function:
# Float to integer (truncates toward zero)
let pi: f64 = 3.14
let n: i32 = i32(pi) # 3
# Integer narrowing
let big: i64 = 42
let small: i8 = i8(big)
# String to number
let x: i32 = i32("123")
# Any value to string
let s: string = string(42) # "42"
Strings
Strings use double quotes and are UTF-8 encoded.
let greeting = "Hello, World!"
String Interpolation
Use \(expression) inside a string to embed any value directly:
let name = "World"
let message = "Hello, \(name)!"
let result = "Sum: \(x + y)"
Escape Characters
Standard escape sequences are supported:
let newline = "line1\nline2"
let tab = "col1\tcol2"
let quote = "She said \"hello\""
let backslash = "C:\\Users\\Name"
let escaped_interp = "\\(not interpolated)"
Raw Strings
Triple-quoted strings preserve literal content — backslashes are not treated as escapes. Interpolation with \(expr) still works:
let raw = """
This is a raw string.
Backslashes are literal: \n is not a newline.
But interpolation works: \(name)
"""
String Operations
Strings support indexing, concatenation, removal, and replacement:
let len = greeting.count # Number of characters
let char = greeting[0] # Single character by index
let concat = "Hello" + ", " + "World" # Concatenation with +
let converted = string(42) # Convert any value to string
var s = "hello"
s += " world" # Append
s -= "o" # Remove all occurrences of "o"
s["world"] = "there" # Replace all occurrences
Operators
Arithmetic
Standard arithmetic operators, plus ^ for exponentiation. Precedence (highest to lowest): ^, * / %, + -.
let x = 1 + 2 - 3 * 4 / 5 % 6
let pow = 2.0 ^ 3.0 # 8.0
Comparison
All comparison operators return bool:
x == y x != y
x < y x > y
x <= y x >= y
Logical
Logical operators work on bool values. Both symbol and keyword forms are supported:
true && false # Logical AND
true || false # Logical OR
!true # Logical NOT
a ~~ b # Logical XOR
# Keyword aliases
a and b # Same as &&
a or b # Same as ||
not a # Same as !
a xor b # Same as ~~
Bitwise
Bitwise operators work on integer types. Note that XOR uses ~ (tilde), not ^ (which is exponentiation):
a & b # AND
a | b # OR
a ~ b # XOR (tilde, not caret)
~a # NOT (bitwise complement)
a << 2 # Left shift
a >> 1 # Right shift
Compound Assignment
Every binary operator has a compound assignment form:
x += 1 x -= 1 x *= 2 x /= 2
x %= 3 x ^= 2 x &= 0xFF
x |= 1 x ~= mask x <<= 2 x >>= 1
Increment / Decrement
Both prefix and postfix forms are supported. Prefix returns the new value; postfix returns the old value:
x++ ++x
x-- --x
Swap
The <-> operator swaps the values of two mutable locations:
var a = 10
var b = 20
a <-> b ## a is now 20, b is now 10
p.x <-> p.y ## swap struct fields
arr[i] <-> arr[j] ## swap array elements
Both sides must be mutable (var) and have compatible types. Swap is safe to use with non-copyable types since both sides always hold valid values.
Ternary
Zero uses then / else instead of ? : for conditional expressions. They can be chained:
let max = a > b then a else b
let grade = score >= 90 then "A" else score >= 80 then "B" else "C"
Conditionals
If / Elif / Else
Conditions don't need parentheses. Blocks are terminated with end (or braces):
if x > 0
println("positive")
elif x < 0
println("negative")
else
println("zero")
end
Inline If
Any statement can have a trailing if condition — the statement only executes if the condition is true:
println("found!") if x > 0
return -1 if error
Loops
For Loops
Zero has several forms of for loop:
# Count form
for 10
println("ten times")
end
# Range (exclusive upper bound)
for i in 0..10
println(i) # 0 through 9
end
# Range (inclusive)
for i in 0..=10
println(i) # 0 through 10
end
# With step
for i in 0..10 by 2
println(i) # 0, 2, 4, 6, 8
end
# Array iteration
for item in array
println(item)
end
# Mutable iteration
for var item in array
item *= 2
end
C-Style For
A more traditional for loop with initializer, condition, and update:
for var i = 0 in i < 10 by i++
println(i)
end
# Multiple variables
for var i = 0; var j = 10 in i < 5 by i++; j--
println(i + j)
end
While Loop
var i = 0
while i < 10
i++
end
Inline Until
Any statement can be repeated with an until suffix. The statement executes first, then the condition is checked (like a do-while):
var x = 0
var sum = 0
sum += x++ until x > 4
# x is now 5, sum is 0+1+2+3+4 = 10
Loop (Infinite)
loop repeats forever until a break statement is reached:
loop
break if done
end
Loop-Until
A loop can end with until instead of end — the body runs at least once, then repeats until the condition is true:
var x = 0
loop
x++
until x >= 10
Break and Continue
break exits a loop and continue skips to the next iteration. Both support inline if:
for i in 0..100
continue if i % 2 == 0 # Skip even numbers
break if i > 50 # Stop at 50
end
In nested loops, repeat break to exit multiple levels:
# Break two levels
for x in 0..10
for y in 0..10
break break if x + y > 15
end
end
# Break outer loop, continue its next iteration
for x in 0..5
for y in 0..5
break continue if y == 3
end
end
Pattern Matching
match provides pattern matching against values, ranges, types, enums, tuples, and structs. Cases are tested in order and else handles anything unmatched:
match value
case 0
println("zero")
case 1
println("one")
case 2..10
println("small")
else
println("other")
end
OR Patterns
Match multiple values with |:
match status_code
case 200 | 201 | 202
println("success")
case 404 | 410
println("not found")
end
Enum Matching
match color
case color.red
println("red")
case color.green
println("green")
end
Variant Type Matching
Match on the type of a variant value:
match x
case i32
println("integer")
case string
println("string")
end
Tuple Patterns
match pair
case (0, "zero")
println("zero pair")
case (1, _) # Wildcard matches anything
println("one with something")
end
Struct Patterns
match pt
case point() with (x = 0, y = 0)
println("origin")
case point() with (x = 1)
println("unit x")
else
println("elsewhere")
end
Functions
Functions are declared with fun. Parameters require type annotations, and the return type follows ->. If the function returns nothing, the return type can be omitted.
fun add(x: i32, y: i32) -> i32
return x + y
end
Inline Functions
Single-expression functions can use the compact => form. The return type is inferred automatically:
fun square(x: i32) => x * x
fun greet(name: string) => "Hello, \(name)!"
Implicit Return Type
The return type can be omitted when it can be inferred from the function body:
fun multiply(x: i32, y: i32)
return x * y
end
Default Parameters
fun greet(name: string, greeting: string = "Hello") -> string
return greeting + ", " + name
end
greet("World") # "Hello, World"
greet("World", "Hi") # "Hi, World"
Function Overloading
Functions can be overloaded by parameter count or type:
fun process(x: i32) => x * 2
fun process(s: string) => 100
Function Name Suffixes (? and !)
Function, method, and getter names can end with ? or ! to indicate optional or exceptional return types. The ? suffix requires an optional return type (T?), and ! requires an exceptional return type (T!):
## ? suffix — must return optional type
fun find?(items: i32[], target: i32) -> i32?
for i in 0..items.length
if items[i] == target
return i
end
end
return null
end
## ! suffix — must return exceptional type
sym parse_error
fun parse!(s: string) -> i32!
if s == ""
return parse_error!
end
return 42
end
let index = find?(items, 5) ## i32?
let value = parse!("hello") ## i32!
Methods and getters follow the same pattern:
extend MyType
fun get?(key: string) -> i32?
...
end
get safe_value?: i32?
...
end
end
Parameter Storage Classes
Parameters can use the same storage classes as variables. The default is let (immutable):
fun read_only(let x: i32) # Default: cannot modify
return x + 1
end
fun mutable_copy(var x: i32) # Gets a mutable copy
x *= 2
return x
end
fun by_reference(ref x: i32) # Modifies original
x += 10
end
fun compile_time(def x: i32) # Must be compile-time value
return x * 10
end
Function Storage Classes
Functions use fun, mut, or ref as their storage class. mut and ref are only allowed inside types or as nested functions:
fun outer() -> i32
var count = 0
# mut: can modify variables in outer scope
mut increment()
count += 1
end
increment()
increment()
return count # Returns 2
end
Inside structs, mut indicates a method that modifies self, and ref returns a reference.
Nested Functions
Nested functions declared with mut can read and modify variables from all enclosing scopes. They can be nested multiple levels deep:
fun outer() -> i32
var x = 1
mut middle() -> i32
var y = 10
mut inner() -> i32
var z = 100
return x + y + z # Access all three levels
end
return inner()
end
return middle() # Returns 111
end
Unified Call Syntax (UCS)
Any function can be called with dot syntax, where the first argument goes before the dot. This allows chaining free functions as if they were methods:
fun square(x: i32) => x * x
fun add(x: i32, y: i32) => x + y
let result = 5.square() # Same as square(5)
let sum = 10.add(5) # Same as add(10, 5)
let chained = 5.square().add(3) # add(square(5), 3) = 28
If the function takes a ref parameter, UCS automatically passes by reference:
fun increment(ref x: i32)
x += 1
end
var n = 10
n.increment() # Same as increment(ref n)
Lambdas
Function Types
Function types use parameter-list-arrow-return syntax:
var f: (x: i32, y: i32) -> i32
Function Variables
Assign named functions to variables:
fun add(x: i32, y: i32) => x + y
fun sub(x: i32, y: i32) => x - y
var f: (x: i32, y: i32) -> i32 = add
assert f(1, 2) == 3
f = sub
assert f(1, 2) == -1
Inline Lambdas
Use => for single-expression lambdas:
fun apply(list: i32[], f: (a: i32) -> i32) -> i32[]
for i in list
yield f(i)
end
end
let result = apply([1, 2, 3], (x: i32) => x * 2)
assert result == [2, 4, 6]
Multi-Line Lambdas
Use fun for pure multi-line lambdas, or mut for lambdas that capture and mutate outer variables:
# Pure multi-line lambda
let added = apply([1, 2, 3], fun (x: i32) -> i32
return x + 1
end)
# Mutating lambda (captures outer variable)
var count = 0
let result = apply([1, 2, 3], mut (x: i32) -> i32
count += 1
return x * count
end)
Passing Named Functions
Named functions can be passed directly:
fun square(x: i32) => x * x
let result = apply([1, 2, 3, 4, 5], square)
assert result == [1, 4, 9, 16, 25]
Generators & Yield
Functions that use yield become generators. The return type is a dynamic array, and each yield adds one element to the result:
fun fibonacci() -> i32[]
var a = 0
var b = 1
loop
yield a
let temp = a + b
a = b
b = temp
end
end
for val in fibonacci()
break if val > 100
println(val)
end
Conditional Yield
Combine yield with inline if to conditionally emit values:
fun evens(limit: i32) -> i32[]
for i in 0..limit
yield i if i % 2 == 0
end
end
let result = evens(10) # [0, 2, 4, 6, 8]
Generator Methods
Generators work inside structs too:
struct inventory
var items: item[]
fun low_stock(threshold: i32) -> item[]
for it in self.items
yield it if it.quantity <= threshold
end
end
end
Tuples
Tuples are ordered collections of values with potentially different types. Access elements by index:
var t = (1, 2, 3)
let x = t.0
let y = t.1
Named Tuples
Tuple fields can be given names for readability:
var point = (x = 10, y = 20)
let px = point.x
let py = point.y
Grouped Type Annotation
When all fields share a type, use the grouped form:
let v: ((x, y, z): i32) = (1, 2, 3)
assert v.x == 1
Unpacking
Destructure a tuple into individual variables:
let (a, b, c) = (1, 2, 3)
# Swap values without a temporary
var (x, y) = (1, 2)
(x, y) = (y, x)
Concatenation
Join tuples with :::
let t1 = (1, 2)
let t2 = t1 :: (3, 4) # (1, 2, 3, 4)
Arrays
Fixed-Size Arrays
Specify the size in the type annotation. For multidimensional arrays, use comma-separated sizes:
let arr: i32[5] = [1, 2, 3, 4, 5]
let matrix: i32[3, 3] = [1,2,3; 4,5,6; 7,8,9]
Dynamic Arrays
Dynamic arrays use [] with no size. Append elements with ::=:
var arr: i32[] = []
arr ::= 10 # Append element
arr ::= [20, 30] # Append array
Array Constructor
Create an array with a specified size and fill value using constructor syntax:
var zeros = i32[](10) # Dynamic: 10 default-initialized elements
var filled = i32[](10, 42) # Dynamic: 10 elements, all set to 42
var fixed = i32[5](99) # Fixed-size: 5 elements, all 99
let n = 100
var sized = i32[n](0) # Let-sized: n elements, all 0
For dynamic arrays, the first argument is the size and the optional second is the fill value. For fixed-size and let-sized arrays, the single argument is the fill value. Works with any type, including structs.
Access
Access elements by index (0-based). Use .count to get the number of elements:
let first = arr[0]
arr[1] = 99
let len = arr.count
Multidimensional Arrays
Use semicolons to separate rows:
let m = [1, 2; 3, 4] # 2x2 matrix
let val = m[1, 0] # Row 1, Col 0 = 3
let cube = [1, 2; 3, 4;; 5, 6; 7, 8] # 2x2x2
Slicing
Extract or replace a range of elements using range syntax:
var a = [1, 2, 3, 4, 5]
let slice = a[1..3] # [2, 3]
a[0..2] = [10, 20] # Assign to slice
Broadcasting
Apply an operation to every element using [] (empty brackets):
fun double(x: i32) => x * 2
var arr = [1, 2, 3, 4, 5]
let doubled = arr[].double() # [2, 4, 6, 8, 10]
Filtering
Use a lambda inside [] to filter elements:
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let evens = arr[x => x % 2 == 0] # [2, 4, 6, 8, 10]
let big = arr[x => x > 5] # [6, 7, 8, 9, 10]
Array/Tuple Conversion
Arrays and tuples of matching types and sizes convert implicitly:
var a: i32[3] = (1, 2, 3) # Tuple to array
var t: (i32, i32, i32) = [1, 2, 3] # Array to tuple
Let-Sized Arrays
Fixed-size arrays can use let variables for their size:
let size = 10
var data: i32[size] # Size determined at runtime, fixed after
Structs
Structs define custom data types with fields and methods.
Basic Struct
Fields can be var (mutable), let (set once in constructor), or def (compile-time constant):
struct point
var x: f32
var y: f32
end
Structs get a default constructor automatically:
var p = point() # All fields zeroed
var q = point(1.0, 2.0) # All-fields constructor
Custom Constructors
Define custom constructors with mut:
struct person
let name: string
var age: i32
mut person(n: string, a: i32)
self.name = n
self.age = a
end
end
var p = person("Alice", 30)
Named Constructors
struct vec2
var x: f32
var y: f32
mut vec2.zero()
self.x = 0.0
self.y = 0.0
end
mut vec2.unit_x()
self.x = 1.0
self.y = 0.0
end
end
let v = vec2.zero()
let u = vec2.unit_x()
Methods
Methods use fun for non-mutating and mut for mutating. Access fields through self:
struct counter
var count: i32
fun get_count() -> i32
return self.count
end
mut increment()
self.count += 1
end
end
var c = counter(0)
c.increment()
assert c.get_count() == 1
Ref Methods
A ref method returns a reference, allowing the caller to read and write through the returned value:
struct pair
var first: i32
var second: i32
ref larger() -> i32
if self.first >= self.second
return self.first
end
return self.second
end
end
var p = pair(10, 20)
assert p.larger() == 20
p.larger() = 99 # Writes through to p.second
assert p.second == 99
Field Defaults
struct config
def version = 1 # Compile-time constant
let name: string # Set once in constructor
var value: i32 # Mutable
var count: i32 = 10 # Field with default value
end
Properties (Getters and Setters)
struct temperature
var celsius: f32
get fahrenheit -> f32 => self.celsius * 1.8 + 32.0
set fahrenheit
self.celsius = (value - 32.0) / 1.8
end
end
var t = temperature(100.0)
assert t.fahrenheit == 212.0
t.fahrenheit = 32.0
assert t.celsius == 0.0
With Expression
Create a modified copy of a struct:
let p1 = point(1.0, 2.0)
let p2 = p1 with (x = 10.0)
let p3 = p1 with
x = 3.0
y = 4.0
end
Nested Types
struct outer
var value: i32
struct inner
var data: i32
end
var nested: inner
end
Recursive Structs
struct node
var value: i32
var next: node? # Optional self-reference
end
String Conversion
Define a free string function to convert a type to a string:
fun string(p: point) -> string
return "(\(p.x), \(p.y))"
end
println(string(point(1.0, 2.0))) # "(1, 2)"
Destructors
Define cleanup logic that runs when a value goes out of scope:
struct resource
var name: string
mut ~resource()
println("Cleaning up \(self.name)")
end
end
Destructors are called automatically in reverse order of creation. Types with destructors are non-copyable — they cannot be assigned from another variable. However, they can be reassigned from a constructor or function call (the old value's destructor runs first), and they can be used in variant types:
var a = resource("file1")
a = resource("file2") ## destroys "file1", then assigns "file2"
var x: resource | null = null
x = resource("conn") ## assign destructible into variant
x = null ## destroys "conn"
Enums
Enums define a set of named variants. Variants are accessed with dot syntax.
Basic Enum
enum color
red
green
blue
end
let c = color.red
assert c == color.red
assert c != color.blue
Explicit Values
enum priority
low = 1
medium = 5
high = 10
critical # Auto-increments to 11
end
assert priority.high.value == 10
assert priority.critical.value == 11
Custom Backing Type
Enums can have a custom backing type specified with : after the name:
enum color_rgb: i32[3]
red = [255, 0, 0]
green = [0, 255, 0]
blue = [0, 0, 255]
end
let r = color_rgb.red
let rgb = r.value # [255, 0, 0]
Properties and Iteration
assert priority.count == 4 # Number of variants
for variant in priority
println(variant.value)
end
Implicit Resolution
When a function parameter is an enum type, you can omit the enum name:
fun set_color(c: color)
# ...
end
set_color(red) # Inferred as color.red
This also works for variable declarations with explicit types:
let v: color = red
var w: color = green
w = blue
Comma Syntax
Commas between variants are optional:
enum color { red, green, blue }
Extending Enums
extend color
fun to_string() -> string
match self
case color.red then return "Red"
case color.green then return "Green"
case color.blue then return "Blue"
end
end
end
Extend Blocks
Add methods, properties, or trait implementations to existing types — including built-in types — without modifying the original definition:
struct point
var x: f32
var y: f32
end
extend point
fun distance() -> f32
return (self.x ^ 2.0 + self.y ^ 2.0) ^ 0.5
end
end
var p = point(3.0, 4.0)
assert p.distance() == 5.0
Extending Built-In Types
extend string
fun is_empty() -> bool
return self.count == 0
end
end
Operator Overloading
Binary Operators
Define operators as free functions:
struct vec2
var x: f32
var y: f32
end
fun +(a: vec2, b: vec2) -> vec2
return vec2(a.x + b.x, a.y + b.y)
end
fun -(a: vec2, b: vec2) -> vec2
return vec2(a.x - b.x, a.y - b.y)
end
Compound Assignment
Compound operators are synthesized automatically from matching binary operators. If +(a: T, b: U) -> T exists, a += b is available automatically:
fun +(a: vec2, b: vec2) -> vec2
return vec2(a.x + b.x, a.y + b.y)
end
Index Operator
Use get, set, or ref to define index access:
struct grid
var data: i32[]
var width: i32
get [row: i32, col: i32] -> i32
return self.data[row * self.width + col]
end
set [row: i32, col: i32]
self.data[row * self.width + col] = value
end
end
var g = grid([1, 2, 3, 4], 2)
assert g[0, 1] == 2
g[1, 0] = 99
Iterator Operator
Define a get [] -> property to make a type iterable:
struct counter
var limit: i32
get [] -> i32[]
for i in 0..self.limit
yield i
end
end
end
let c = counter(5)
for val in c
println(val) # 0, 1, 2, 3, 4
end
Unary Operators
fun -(v: vec2) -> vec2 => vec2(-v.x, -v.y)
Traits
Traits define interfaces that types can implement. Use abst for abstract methods (must be implemented) and virt for virtual methods (have a default):
trait hashable
fun hash() -> i32 abst
end
trait printable
fun to_string() -> string virt
return "unknown"
end
end
Implementing Traits
Use extend type: trait to add an implementation:
extend point: hashable
fun hash() => self.x * 31 + self.y
end
# Or implement directly in the struct declaration
struct circle: hashable
var radius: i32
fun hash() => self.radius
end
Multiple Traits
extend point: hashable & printable
fun hash() => self.x * 31 + self.y
fun to_string() => "(\(self.x), \(self.y))"
end
Trait Inheritance
trait drawable
fun draw() abst
end
trait widget: drawable
fun resize(w: i32, h: i32) abst
end
Trait Variables
A variable typed as a trait can hold any value that implements it:
var obj: hashable = point(1, 2)
let h = obj.hash()
Type Checking
Use : to check if a value implements a trait:
assert obj : hashable
assert !(other : hashable)
Variant Types
A variable can hold one of several types, separated by |:
var x: i32 | f32 | string = 42
x = 3.14f32
x = "hello"
Type Narrowing
Use : to check and narrow the type. Inside the if block, the variable is known to be that specific type:
if x : i32
let n: i32 = x # Safe: x is narrowed to i32
end
Common Members and Methods
If all variants share a field, getter, or method with the same name and compatible types, you can access it directly without narrowing. The correct variant is dispatched at runtime:
fun display(x: car | person)
println(x.to_string()) ## OK — both variants have to_string()
end
Methods that only exist on some variants require type narrowing first:
if x : person
x.grow() ## OK — narrowed to person
end
Optional Types
T? is shorthand for null | T. An optional variable can hold either a value of type T or null:
var x: i32? = 42
x = null
Null Coalescing
Provide a default value when null with ??:
let val = x ?? 0 # 0 if x is null
Null Conditional
Access members safely — returns null if the value is null:
var str: string? = "hello"
let len = str?.count
Null Check
Optional values are truthy when non-null:
if x
println("has value")
end
If Let / If Var Binding
Combine a truthiness check with variable binding. The variable is bound to the unwrapped value and scoped to the branch body:
var maybe: i32? = 42
if let val = maybe
println(val + 1) # val is i32, guaranteed non-null
end
# Mutable binding
if var val = maybe
val = val + 10
end
# Works with exceptional and elif
if let a = first_attempt()
use(a)
elif let b = second_attempt()
use(b)
else
handle_failure()
end
Symbols
Symbols are unique named values, useful for error types and sentinel values. They act like lightweight enums with a single variant:
sym not_found
sym invalid_input
# Multiple symbols on one line
sym error_a, error_b
Symbols are most commonly used in variant return types for error handling:
fun lookup(key: string) -> i32 | not_found
return not_found if key == ""
return 42
end
Error Handling
Zero handles errors without exceptions. Errors are values that flow through the type system.
Assert
Debug-only checks that halt the program if the condition is false:
assert x == 42
assert x > 0
Raise
Immediately halt with an error message:
raise "Something went wrong"
Catch
Define a global catch function to intercept runtime errors from raise. It receives the error message, source line number, and a stack trace:
fun catch(error: string, line: i32, trace: string)
println("Error: " + error + " at line " + string(line))
end
fun main()
raise "something broke" # Calls catch instead of crashing
end
If catch is defined, it replaces the default error handler. You can re-raise from inside catch to propagate with a modified message.
Symbols as Errors
Declare error symbols and return them as variant values. Use : to check which type a variant holds:
sym divide_by_zero
fun safe_divide(a: i32, b: i32) -> i32 | divide_by_zero
return divide_by_zero if b == 0
return a / b
end
let result = safe_divide(10, 0)
if result : i32
println(result)
else
println("Error!")
end
Exceptional Types
T! is shorthand for an error-or-value type:
sym bad_input
fun parse(s: string) -> i32!
return bad_input! if s == ""
return 42
end
Error Coalescing
Use !! to provide a default value when an error occurs (similar to ?? for optionals):
let val = parse("") !! -1 # -1 on error
Error Conditional
Use !. to access members only when the value is not an error:
let doubled = result!.get_doubled() !! -1
let field = result!.value !! 0
Error Truthiness
Error-or-value types are truthy when they hold a success value:
if result
println("success")
end
if !result
println("failed")
end
Generics
Zero supports generics through two approaches: explicit type parameters and implicit templates.
Explicit Type Parameters
Pass types explicitly using def t: type:
fun identity(def t: type, x: t) -> t
return x
end
assert identity(i32, 42) == 42
assert identity(string, "hello") == "hello"
Def Parameter Inference
Leading def parameters are automatically inferred from call-site arguments:
fun add(def t: type, a: t, b: t) -> t => a + b
fun max(def t: type, a: t, b: t) -> t => a > b then a else b
assert add(1, 2) == 3 # t inferred as i32
assert add(1.5, 2.5) == 4.0 # t inferred as f32
Generic Structs
struct box(def t: type)
var value: t
end
var int_box: box(i32)
int_box.value = 42
var str_box: box(string)
str_box.value = "hello"
Default Type Parameters
struct container(def t: type = i32)
var value: t
end
var c1: container() # Uses default: i32
var c2: container(string) # Explicit type
Value Parameters
struct fixed_array(t, def n: i32)
var data: t[n]
end
var arr: fixed_array(f32, 10)
Specialization
Generic structs and functions can be specialized for specific types or values. When multiple overloads match, the most specific specialization wins.
Type Specialization
Provide a concrete type in place of a def t: type parameter:
struct holder(def t: type)
var value: t
get describe -> string => "generic holder"
end
struct holder(i32) # Specialized for i32
var value: i32
get describe -> string => "i32 holder"
end
Functions can be specialized the same way:
fun add(def t: type, a: t, b: t) -> t
return a + b
end
fun add(i32[3], a: i32[3], b: i32[3]) -> i32[3]
var r: i32[3]
for i in 0..3
r[i] = a[i] + b[i]
end
return r
end
Def Value Specialization
Compile-time def parameters can be specialized with literal values:
fun foo(def a: i32, def b: i32, c: i32) -> i32
return a * b + c
end
fun foo(0, def b: i32, c: i32) -> i32 # a is 0
return c
end
fun foo(0, 0, c: i32) -> i32 # both 0 (most specific)
return c
end
The compiler selects the most specific match. Equally-specific matches are a compile error.
Aliases
Create alternate names for functions and types.
Function Aliases
fun double(x: i32) => x * 2
alias dbl = double
assert dbl(5) == 10
Type Aliases
struct vec(def t: type, def n: i32)
var data: t[n]
end
alias vec2f = vec(f32, 2)
alias vec3i = vec(i32, 3)
var v: vec2f
v.data[0] = 1.0
Parameterized Aliases
# Fixes size but keeps type as a parameter
alias vec4(def t: type) = vec(t, 4)
# With default parameter
alias vec2(def t: type = f32) = vec(t, 2)
var v1: vec4(f32)
var v2: vec2() # Uses default: f32
var v3: vec2(i32) # Explicit type
Where Clauses
Where clauses are compile-time constraints on def parameters and types. They are checked at compile time, not at runtime:
Value Constraints
struct bounded_array(def t: type, def n: i32)
where n > 0
var data: t[n]
end
var arr: bounded_array(i32, 5) # OK
# var bad: bounded_array(i32, 0) # Compile error
Trait Constraints
fun process(def t: type, item: t)
where t: printable && t: hashable
let s = item.to_string()
let h = item.hash()
end
Type Equality Constraints
fun same_add(def t: type, a: t, b: t) -> t
return a + b
end
same_add(10, 20) # OK: t inferred as i32
Has Expression
The has keyword checks whether a type has a specific field, method, or property. It uses the same syntax as declarations:
# Field checks
T has var name: string
T has let id: i32
# Method checks (full signature)
T has fun toString() -> string
T has mut reset()
# Property checks
T has get count -> i32
T has set count
Use has in where clauses to require specific members on generic types:
fun greet(def t: type, item: t) -> string
where t has fun speak() -> string
return item.speak()
end
Global functions matching via Unified Call Syntax are also considered.
Namespaces
Spaces group related declarations under a name. Access members with dot syntax, or use to import them into the current scope:
space math
fun add(a: i32, b: i32) => a + b
fun sub(a: i32, b: i32) => a - b
end
let result = math.add(1, 2)
# Import into current scope
use math
let sum = add(3, 4)
# Scoped import
use math in
let x = add(1, 2)
end
Nested Spaces
Use dotted names for nested namespaces:
space a.b.c
fun greet() => "hello from a.b.c"
end
let msg = a.b.c.greet()
Inline Space Declarations
Instead of wrapping declarations in a space block, you can place them directly into a space using dotted names on any declaration:
struct utils.point
var x: i32
var y: i32
end
fun utils.add(a: i32, b: i32) -> i32 => a + b
enum types.color
red, green, blue
end
var config.debug = false
This is syntactic sugar — struct utils.point is equivalent to space utils { struct point ... end }. All declaration types support this: struct, fun/mut/ref, trait, enum, alias, and var/let/def.
Field Spaces (Organizing Struct Fields)
Zero does not have explicit access modifiers like private or public. Instead, you can use dotted field names inside structs to group related fields, providing logical organization and a naming convention for signaling internal state:
struct entity
var x: i32
var y: i32
var internal.hp: i32
var internal.alive: bool
use internal
mut damage(amount: i32)
hp -= amount
alive = hp > 0
end
end
fun main()
var e = entity(10, 20, 100, true)
e.damage(30)
assert e.internal.hp == 70
end
The use internal directive imports the nested fields so methods can access them directly. Deeply nested paths like var db.host: string are also supported.
Handles
Handles are type-safe array indices. Create one with array@(index) and dereference with @h:
var data = [10, 20, 30]
var h = data@(0) # Create handle
assert @h == 10 # Dereference
@h = 99 # Modify through handle
assert data[0] == 99
Handle Iteration
for h in data@
@h = @h * 2 # Double every element
end
Handle Types
var h2: data@ = data@(1) # Type annotation
# Handles as struct fields
struct container
var items: i32[]
var refs: items@[] # Array of handles
end
Auto-Dereference
Handles auto-dereference for member access on struct arrays:
var points: point[] = [point(1.0, 2.0)]
var h = points@(0)
assert h.x == 1.0 # Same as (@h).x
h.x = 5.0 # Auto-deref for write too
Operators on Handles
Comparison operators (==, !=, <, >, <=, >=) work directly on handles and compare their indices. Both handles must reference the same array:
var data = [10, 20, 30]
var a = data@(0)
var b = data@(2)
assert a < b # Compares indices: 0 < 2
All other operators (arithmetic, bitwise, unary, compound assignment) require explicit dereference with @:
## let sum = a + b # Error: use explicit dereference
let sum = @a + @b # Correct: 10 + 30 = 40
Optional Handles
Handles can be optional using @?, which is useful for linked structures:
struct list
struct node
var value: i32
var next: data@? # Optional handle
end
var data: node[]
var head: data@? # Starts as null
mut add(value: i32)
data ::= node(value, head)
head = data@(data.count - 1)
end
fun collect_values() -> i32[]
var result: i32[] = []
var current = head
while current # Truthy when non-null
result ::= current.value
current = current.next
end
return result
end
end
Allocators
Custom memory management using new and free:
struct pool
var items: i32[]
mut new(value: i32) -> items@
items ::= value
return items@(items.count - 1)
end
mut free(handle: items@)
# Cleanup logic
end
end
var mem: pool
var a = mem new 10
var b = mem new 20
assert @a == 10
free a
Concurrency
Parallel For
Parallelize loops across threads. Use sync for thread-safe mutations, or reduce for reduction operations:
var data: i32[] = [1, 2, 3, 4, 5]
parallel for h in data@
sync
@h = @h * 2
end
end
# With reduction (requires 'reduce' keyword)
var sum = 0
parallel for h in data@
sum += @h reduce
end
Writes to external var variables require either reduce or sync. Only one reduce per variable per sync region is allowed.
Custom Reducers
Supply a named function to control how partial results are combined:
fun reducer(a: i32, b: i32) -> i32
return a + b
end
var total: i32 = 0
parallel for h in data@
total = reducer(total, @h) reduce
end
BSP (Bulk Synchronous Parallel)
Multiple sync blocks inside a single parallel for create distinct phases. All threads complete the work before a sync, then all execute the sync block together, then proceed to the next phase:
var data: i32[] = [1, 2, 3]
var sum: i32 = 0
parallel for h in data@
sum += @h reduce # Phase 1: parallel reduction
sync
println(sum) # All threads see the same sum
end
sum += @h * 10 reduce # Phase 2: more parallel work
sync
println(sum) # All threads see the final sum
end
end
Async / Await
Use async to create tasks and wait to block until complete:
let task = async
return 42
end
let result: i32 = wait task
Inside an async block, use await for non-blocking waits:
let outer = async
let inner = async
return 42
end
return await inner
end
Awaiting Multiple Tasks
let t1 = async return 10 end
let t2 = async return 20 end
let t3 = async return 30 end
let combined = async
let (a, b, c) = await (t1, t2, t3)
return a + b + c
end
Select
React to the first task that completes:
select
case r = await t1
println("Task 1: \(r)")
case r = await t2
println("Task 2: \(r)")
end
Scope Blocks
Use scope for structured concurrency — all tasks created inside are joined at the end:
var result: i32 = 0
scope
let t1 = async
return 10
end
let t2 = async
return 20
end
result = wait t1 + wait t2
end
assert result == 30
Async Yield
Inside an async block, async yield voluntarily yields control to the scheduler, allowing other tasks to run:
let t = async
async yield # Give other tasks a chance to run
return 7
end
let result: i32 = wait t
Inline Async
A single expression can be made async with a trailing async keyword:
let t = 10 + 5 async # Expression runs as a task
let result: i32 = wait t # 15
Async Capture Rules
Async blocks are isolated from mutable state in outer scopes. The compiler enforces these rules at compile time to prevent data races:
No mutable variable capture: An async block cannot directly reference a var from an outer scope. Copy the value into a let first:
var x = 10
let snapshot = x # Explicit copy
let t = async
# return x # Error: cannot capture var
return snapshot + 1 # OK: snapshot is let
end
No calling functions that capture mutable state: An async block cannot call a nested function or method whose transitive capture set includes a var from outside the async boundary:
fun foo()
var x = 0
fun read() -> i32
return x # read() captures outer var x
end
let t = async
# read() # Error: read() captures mutable 'x'
return 0
end
end
This rule applies transitively — if wrapper() calls inc(), and inc() captures a var, then wrapper() also cannot be called from async. It also applies to methods on locally-defined struct types that close over outer mutable variables.
Local state is fine: Functions and methods that only operate on state local to the async block are unrestricted.
Determinism and Thread Safety
Zero is designed to be deterministic and thread-safe by construction. Rather than relying on the programmer to avoid data races, the language's access rules and parallel constructs make race conditions structurally impossible.
Thread safety: All parallel execution flows through parallel for and async, both of which enforce strict rules about shared state. Parallel for requires that any write to an external variable use either sync or reduce — unprotected writes are compile-time errors, not runtime bugs. Async tasks cannot access mutable variables from outer scopes, either directly or through nested functions and methods that capture them. The compiler checks the transitive capture set of every function called from an async block and rejects calls that would create shared mutable state. There is no unsafe escape hatch, no raw threads, no shared memory, and no locks to forget.
Determinism: Given the same inputs, a Zero program will produce the same outputs on any supported platform or hardware configuration. This includes parallel for — reductions always combine results in a well-defined order, and BSP phases execute identically regardless of thread count or scheduling. The one exception is select, which returns whichever async task finishes first and is intentionally non-deterministic.
Boundaries: These guarantees hold for pure Zero code. Multi-sender/receiver channels introduce scheduling-dependent ordering, and FFI calls (extern functions) execute outside Zero's control. Within the language itself, thread safety and determinism are not best practices to follow — they are invariants the compiler enforces.
Differentiation
Zero can automatically compute derivatives of functions symbolically at compile time.
Partial Derivatives
Use f'x(args) to compute the partial derivative with respect to x:
fun f(x: f32) => x ^ 2.0 + 3.0 * x + 1.0
# f'(x) = 2x + 3
assert f'x(1.0) == 5.0
assert f'x(0.0) == 3.0
Multi-Variable
fun g(x: f32, y: f32) => x ^ 2.0 * y + x * y ^ 2.0
# Partial with respect to x: 2xy + y^2
assert g'x(1.0, 2.0) == 8.0
# Partial with respect to y: x^2 + 2xy
assert g'y(1.0, 2.0) == 5.0
Gradient
Use f'(args) to get a tuple of all partial derivatives:
fun f(x: f32, y: f32) => x ^ 2.0 + y ^ 2.0
let grad = f'(1.0, 2.0) # (2.0, 4.0)
assert grad.0 == 2.0
assert grad.1 == 4.0
Hessian
Use f''(args) for the matrix of second partial derivatives:
let h = f''(1.0, 2.0)
assert h[0, 0] == 2.0 # d2f/dxdx
Chained Derivatives
assert f'x'x(1.0) == 2.0 # Second derivative with respect to x
Custom Derivative Override
Manually specify the derivative when needed:
fun my_func(x: f32) => some_complex_expression(x)
fun my_func'x(x: f32) => manual_derivative(x)
Vector-Valued Functions
Functions returning structs can also be differentiated:
struct vec3
var x: f32
var y: f32
var z: f32
end
fun position(t: f32) => vec3(t ^ 2.0, 2.0 * t, 3.0)
# Velocity = dp/dt = (2t, 2, 0)
let vel = position't(1.0)
assert vel.x == 2.0
assert vel.y == 2.0
assert vel.z == 0.0
Modifiers
Apply compile-time transformations with $. Modifiers can mark declarations with attributes, conditionally include code, or generate code from strings.
Attribute Markers
sym pure
sym inline
$pure
$inline
fun compute(x: i32) -> i32
return x * x
end
Conditional Compilation
fun `if`(condition: bool, node: ast.node) -> ast.node?
return condition then node else null
end
$if(true)
fun included() -> i32
return 42
end
$if(false)
fun excluded() -> i32 # This function is removed
return 0
end
Backtick syntax (`if`) allows using keywords as function names.
String-Generating Modifiers
Modifiers can return code as strings that get parsed and injected:
fun generate_getter() -> string
return "fun get_value() -> i32\n return 42\nend"
end
$generate_getter()
FFI
Zero can call functions from native shared libraries and expose its own functions for use by other languages.
Import
Use import "library_name" to call functions from a shared library:
fun get_answer() -> i32 import "my_lib"
fun add(a: i32, b: i32) -> i32 import "my_lib"
# Struct parameters passed as pointers
fun point_sum(p: point) -> i32 import "my_lib"
# Function pointer callbacks
fun apply_binop(a: i32, b: i32, op: (i32, i32) -> i32) -> i32 import "my_lib"
Extern
Declare functions that are provided by the host environment:
fun sqrt(x: f64) -> f64 extern
# Extern block for multiple declarations
extern
fun host_log(msg: string)
fun host_time() -> f64
end
Export
Mark functions with export to make them callable from other languages:
fun my_function(x: i32) -> i32 export
return x * 2
end
Range Parameters
Functions can accept range arguments directly:
fun range_len(from..to: i32) => to - from
assert range_len(1..10) == 9
fun inclusive_len(from..=to: i32) => to - from + 1
assert inclusive_len(1..=10) == 10
# Unbound ranges
fun suffix(..end: i32) => end
fun prefix(start..: i32) => start
Syntax Flexibility
Zero supports C-style syntax as an alternative to end blocks. Both styles can be freely mixed within the same file.
Braces
fun factorial(n: i32) -> i32 {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
struct point {
var x: i32 = 0
var y: i32 = 0
fun sum() -> i32 {
return self.x + self.y
}
}
Mixing Styles
Ruby-style end blocks and C-style braces can be mixed within the same file:
fun ruby_func(x: i32) -> i32
if x > 0 {
return x * 2
} else {
return -x
}
end
fun c_func(x: i32) -> i32 {
if x > 0
return x + 10
else
return x - 10
end
}
Semicolons
Multiple statements can be placed on one line with semicolons:
let x = 1; let y = 2; let z = x + y
Comments
Zero supports several comment styles. Line comments begin with
#and extend to the end of the line:Block Comments
Block comments use
#[and]#. They can span multiple lines and nest inside each other:Documentation Comments
Documentation comments begin with
##and attach to the declaration that follows them:C-Style Comments
For familiarity, C-style comments also work:
Semantic Comments
A line comment followed immediately by a keyword comments out the entire block that follows, including any nested content. This works with any block-level keyword:
#fun,#struct,#trait,#extend,#enum,#space,#if,#while,#loop,#for,#match,#begin.The commented-out block is completely ignored by the compiler, so it can even contain references to undefined symbols: