Skip to content

Hygiene, Recursion, and Block Arguments

Hygiene

Variables created with let inside a macro body receive a unique suffix at expansion time. This means a let result inside a macro never overwrites a result that already exists in the caller's scope. You can reuse obvious names like result, tmp, and i without fear of collision:

twice! and inc_tmp!/dec_tmp! show that each expansion isolates its own bindings.

05-hygiene.zolo
Playground
// Feature: Macro hygiene — internal vars do not leak to the caller
// Syntax: variables declared with `let` inside the body get a unique
// suffix on expansion, avoiding collisions with external names.
// When to use: you can reuse "obvious" names like `result`, `tmp`, `i`
// inside macros without fear of overwriting the caller's bindings.

// The macro declares `result` internally.
macro twice(x) {
  let result = $x * 2
  print("inside the macro: {result}")
}


// The caller also has `result` — it is not overwritten.
let result = 100
twice!(7)
print("outside: {result}")

// expected:
// inside the macro: 14
// outside: 100

// The same `tmp` name in three different macros — no collision.
macro inc_tmp(x) {
  let tmp = $x + 1
  print("inc: {tmp}")
}


macro dec_tmp(x) {
  let tmp = $x - 1
  print("dec: {tmp}")
}


let tmp = 999
inc_tmp!(10)
dec_tmp!(10)
print("original tmp: {tmp}")
// expected:
// inc: 11
// dec: 9
// original tmp: 999

Recursion

A macro can invoke another macro in its body. Expansion is resolved recursively (limit of ~64 levels). This lets you compose small rules instead of duplicating logic — min3! reuses min2!, and min4! reuses both:

Chain min2!min3!min4! and composition with double!(inc!(5)).

04-recursive.zolo
Playground
// Feature: Recursive macros — one macro calling another
// Syntax: the `outer` macro invokes `inner!(...)` in its body
// When to use: compose small macros to avoid duplicated code, modular
// textual rules (depth limit ~64).

// Basic block: minimum of two.
macro min2(a, b) {
  if $a < $b { $a } else { $b }
}


// Reuses min2 to build min3.
macro min3(a, b, c) {
  min2!($a, min2!($b, $c))
}


// And min4 reuses min3 + min2.
macro min4(a, b, c, d) {
  min2!(min2!($a, $b), min2!($c, $d))
}


print(min2!(7, 3))

// expected: 3

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

// expected: 2

print(min4!(9, 4, 6, 1))

// expected: 1

// Composition also works with different expressions at each level.
macro double(x) {
  $x + $x
}


macro inc(x) {
  $x + 1
}


print(double!(inc!(5)))
// expected: 12   ((5 + 1) + (5 + 1))

Blocks as arguments

Any parameter can receive a { ... } block. At expansion time, the block is pasted textually wherever $param appears, creating custom control constructs. repeat_n! implements a loop; logged! wraps the body with start and end messages:

repeat_n!(n, { ... }) and logged!(label, { ... }) as control-flow constructors.

06-block-arg.zolo
Playground
// Feature: Macros that receive a block as an argument

// Syntax: `name!(arg, { ...statements... })` — any arg can be a block

// inside `{}`, expanded textually wherever `$arg` appears.

// When to use: build custom "control-flow constructs" — retry, timed,

// with_lock, etc. — where the user passes the "body".


// Repeats a block N times, counting attempts.

macro repeat_n(times, body) {
  var __i = 0
  while __i < $times {
    __i = __i + 1
    $body
  }
}


repeat_n!(3, {
  print("tick")
})

// expected:

// tick

// tick

// tick


// Macro that wraps the body with try-style logging.

macro logged(label, body) {
  print("[start] {$label}")
  $body
  print("[done] {$label}")
}


logged!("important work", {
  let x = 10 + 20
  print("computed {x}")
})
// expected:

// [start] important work

// computed 30

// [done] important work

Challenge

Create a macro timed!(label, body) that prints "[start] label", executes body, and then prints "[end] label". Use it to wrap two different blocks and verify that the messages appear correctly interleaved.

enespt-br