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 (like #if, #for, #struct) comments out the entire block that follows, not just one line:

#if debug_mode
    println("debugging")
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)
let (a, b): i32 = (1, 2)          # Shared type
var (p, 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

Only def is allowed at the global scope. All let and var declarations must be inside functions:

def max_size = 1024

fun main()
    let local_let = 10
    var local_var = 20
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 (string is an alias for s8). Zero also supports s16 and s32 for other encodings.

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

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 While / Until

Any statement can be repeated with a while or until suffix. The statement executes first, then the condition is checked (like a do-while):

var x = 0
var sum = 0
sum += x++ while x < 5
# 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

Union Type Matching

Match on the type of a union 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

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.

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

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: f32
        self.celsius = (fahrenheit - 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?              # Nullable 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 and non-reassignable — they cannot be assigned to another variable or used in union types.

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

Define inside the struct with mut:

extend vec2
    mut +=(other: vec2)
        self.x += other.x
        self.y += other.y
    end
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]: 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)

Union 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

Nullable Types

T? is shorthand for null | T. A nullable 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

Nullable values are truthy when non-null:

if x
    println("has value")
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 union 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"

Symbols as Errors

Declare error symbols and return them as union values. Use : to check which type a union 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

Error Optional 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 nullables):

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"

Implicit Templates

Omit type annotations and the compiler infers types from usage:

fun add(a, b) => a + b
fun max(a, b) => a > b then a else b

assert add(1, 2) == 3            # Instantiates for i32
assert add(1.5, 2.5) == 4.0      # Instantiates for f32

Generic Structs

struct box(t)
    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)

Type Specialization

Generic structs can be specialized for specific types:

struct holder(def t: type)
    var value: t
end

struct holder(i32)               # Specialized for i32
    var value: i32
    fun doubled() => self.value * 2
end

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(a, b)
    where type(a) == type(b)
    return a + b
end

same_add(10, 20)                 # OK: both i32
# same_add(10, 3.14)            # Compile error: i32 != f32

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()

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

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:

var data: i32[] = [1, 2, 3, 4, 5]

parallel for h in @data
    sync
        h@ = h@ * 2
    end
end

# With reduction
var sum = 0
parallel for h in @data
    sum += h@                    # Automatic reduction
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

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