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