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.
  • ref for borrowing — When a function needs to modify a caller's variable, it takes a ref parameter. The caller explicitly passes ref 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.

Comments

Zero supports several comment styles. Line comments begin with # and extend to the end of the line:

# This is a line comment
println("hello")  # Inline comment

Block Comments

Block comments use #[ and ]#. They can span multiple lines and nest inside each other:

#[ This is a block comment.
   It can span multiple lines.
   #[ And they can be nested. ]#
]#

Documentation Comments

Documentation comments begin with ## and attach to the declaration that follows them:

## Adds two integers and returns the result.
fun add(x: i32, y: i32) -> i32
    return x + y
end

C-Style Comments

For familiarity, C-style comments also work:

// Single-line comment
/* Block comment */
/* Nested /* block */ comments */

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.

#if debug_mode
    println("debugging")
end

The commented-out block is completely ignored by the compiler, so it can even contain references to undefined symbols:

#fun old_version() -> i32
    return missing_symbol        # No error — this is commented out
end

fun old_version() -> i32
    return 42
end

Variables

Zero has three storage classes for variables, each offering a different level of mutability:

KeywordDescription
defCompile-time constant — evaluated before the program runs
letRuntime immutable — set once, cannot be reassigned
varRuntime 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

TypeDescription
i8, i16, i32, i64Signed integers
u8, u16, u32, u64Unsigned integers
boolBoolean (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

TypeDescription
f16Half precision (storage only)
f32Single precision (default)
f64Double 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