Macros

Macros in Zolo allow you to define code-generating constructs that are expanded at compile time, before type checking and lowering. Macros are hygienic — variables introduced inside a macro body do not leak into the caller's scope.

Defining Macros #

macro log!(msg) {
    print("[LOG] {$msg}")
}

log!("server started")
// expands to: print("[LOG] {server started}")

Macro names conventionally end with ! to distinguish them from regular function calls.

Parameters #

Macros accept one or more parameters prefixed with $:

macro assert_equal!(a, b) {
    if $a != $b {
        panic("assertion failed: {$a} != {$b}")
    }
}

assert_equal!(1 + 1, 2)
assert_equal!("hello".len(), 5)

Multiple Parameters #

macro swap!(a, b) {
    let $tmp = $a
    $a = $b
    $b = $tmp
}

let mut x = 10
let mut y = 20
swap!(x, y)
print("{x}, {y}")  // 20, 10

Hygiene #

Variables introduced inside a macro body get a unique suffix to avoid colliding with caller-scope names:

macro double_print!(val) {
    let result = $val * 2   // 'result' is renamed to 'result_m1' internally
    print("{result}")
}

let result = 100            // this 'result' is NOT overwritten by the macro
double_print!(7)            // prints 14
print("{result}")         // still prints 100

Built-in: stringify!

The stringify! macro converts any expression to its source-code string representation at compile time:

let name = stringify!(hello_world)
print("{name}")  // "hello_world"

let expr = stringify!(1 + 2 * 3)
print("{expr}")  // "1 + 2 * 3"

This is useful for debugging and assertion messages:

macro expect!(expr) {
    if !($expr) {
        panic("expected: {stringify!($expr)}")
    }
}

let x = 5
expect!(x > 3)   // OK
expect!(x > 10)  // panic: expected: x > 10

Recursive Macros #

Macros can call other macros (up to a depth limit of 64):

macro min2!(a, b) {
    if $a < $b { $a } else { $b }
}

macro min3!(a, b, c) {
    min2!($a, min2!($b, $c))
}

print(min3!(7, 2, 5))  // 2

Practical Examples #

Logging with context #

macro log_debug!(msg) {
    print("[DEBUG] {$msg}")
}

macro log_error!(msg) {
    print("[ERROR] {$msg}")
}

log_debug!("processing request")
log_error!("connection refused")

Timed block #

macro timed!(label, body) {
    let t0 = now()
    $body
    let elapsed = now() - t0
    print("{$label} took {elapsed}ms")
}

timed!("sort", {
    let data = [5, 3, 8, 1]
    let sorted = Array.sort(data)
})

Retry logic #

macro retry!(times, body) {
    let mut attempts = 0
    let mut success = false
    while attempts < $times and !success {
        attempts = attempts + 1
        $body
        success = true
    }
}

retry!(3, {
    fetch("https://api.example.com/data")
})

Limitations #

  • Macros are textual — they operate on syntax trees, not types.
  • Macros must be defined before use in the same file.
  • Macros cannot be exported across modules (they are file-scoped).
  • Variadic macros (variable number of arguments) are not yet supported.
enespt-br