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.
// 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)).
// 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.
// 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.
See also