The macro expander is invoked with % by default (change with --special).
Syntax basics
Argument whitespace
Leading whitespace in every argument is stripped, so you can align calls without spaces leaking into the output:
%def(tag, name, value, %{<%(name)>%(value)</%(name)>%})
%tag( div,
Hello world)
Output: <div>Hello world</div>
To include a literal leading space, wrap in a block:
%tag(%{ div%}, %{ Hello world%})
Named arguments
Any argument may be given a name with identifier = value. Named args bind by
name regardless of position and must come after all positional args:
%def(greet, name, msg, %{Hello, %(name)! %(msg)%})
%greet(name = Alice, msg = %{Good morning%})
Comments
| Syntax | Scope |
|---|---|
|
to end of line |
|
to end of line |
|
to end of line |
|
block, nestable |
Multi-line blocks
%{ … %} delimits a block that may span lines and contain commas and
parentheses without triggering argument splitting. An optional tag makes
matching pairs easier to spot:
%def(page, title, body, %page{
<!DOCTYPE html>
<html><head><title>%(title)</title></head>
<body>%(body)</body></html>
%page})
Built-in macros
%def — define a macro
%def(name, param1, param2, ..., body)
%def(greet, name, %{Hello, %(name)!%})
%greet(World)
Output: Hello, World!
Macro bodies may call other macros and contain nested %def calls.
Nested definitions are scoped to the invocation; use %export to promote them.
Calling conventions
These rules apply to all macro kinds:
-
Positional args fill declared params left-to-right; extras are silently ignored.
-
Named args (
param = value) bind by name; an unknown name is an error. -
Positional args must come before named args.
-
Binding the same param both ways is an error.
-
Missing args default to empty string.
%def(endpoint, method, path, handler, %{%(method) %(path) → %(handler)%})
%endpoint(GET, path = /users, handler = list_users)
Output: GET /users → list_users
%set — set a variable
%set(version, 1.0.0) Version: %(version)
Output: Version: 1.0.0
%if — conditional
Empty string is falsy; any non-empty string is truthy.
%set(debug, yes) %if(%(debug), [DEBUG MODE], )
Output: [DEBUG MODE]
%equal — equality test
Returns its first argument if both arguments are equal, otherwise empty string.
%equal(%(mode), release)
%include / %import
%include expands the file inline. %import expands it but discards the
output (for loading macro definitions only).
%import(macros/common.txt) %my_macro(arg)
%env — read an environment variable
Requires --allow-env; raises an error without it.
Prefix: %env(MY_PREFIX)_
Case conversion
%capitalize, %decapitalize, %to_snake_case, %to_camel_case,
%to_pascal_case, %to_screaming_case
%to_snake_case(MyFancyName)
Output: my_fancy_name
%eval — indirect macro call
%eval(%(macro_name), arg1, arg2)
%export — promote to parent scope
%def(init, %{
%set(x, 10)
%export(x)
%})
%init()
x is: %(x)
%here — in-place expansion
Evaluates its argument and writes the result back into the source file at the call site. Useful for one-time code generation.
%rhaidef — Rhai-scripted macros
%rhaidef(name, param1, ..., body)
The body is a Rhai script evaluated at call time. All
visible weaveback scope variables are injected as Rhai string variables. Wrap
the body in %{ … %} when it contains parentheses.
Registered helpers:
| Function | Signature | Description |
|---|---|---|
|
|
Parse string to integer (0 on error) |
|
|
Parse string to float (0.0 on error) |
|
|
Format as |
%rhaidef(double, x, %{(parse_int(x) * 2).to_string()%})
%double(21)
Output: 42
%rhaidef(as_hex, n, %{to_hex(parse_int(n))%})
%as_hex(255)
Output: 0xFF
%rhaiset / %rhaiget / %rhaiexpr — typed Rhai store
A persistent store that survives across all %rhaidef calls in a run. Every
entry is injected into every %rhaidef script with its native Rhai type. Store
keys modified by a script are automatically written back.
%rhaiset(key, value) — store a string or number (auto-parsed to i64/f64) %rhaiget(key) — read a store value as string %rhaiexpr(key, rhai_expr) — store the result of a Rhai expression
Counter with auto write-back:
%rhaiset(counter, 0)
%rhaidef(tick, %{
counter += 1;
counter.to_string()
%})
%tick() %tick() %tick()
Output: 1 2 3
%pydef — Python-scripted macros
%pydef(name, param1, ..., body)
The body is evaluated by monty, a
pure-Rust sandboxed Python interpreter compiled into the binary. No Python
runtime required. Only declared parameters and %pyset store entries are
available inside the script.
|
Note
|
monty supports a subset of Python — arithmetic, string ops, |
%pydef(double, x, %{str(int(x) * 2)%})
%double(21)
Output: 42
%pyset / %pyget — Python store
%pyset(key, value) — write a string into the store %pyget(key) — read from the store (empty string if absent)
Write-back is explicit — capture the return value with %pyset:
%pyset(total, 0)
%pydef(add, n, %{str(int(total) + int(n))%})
%pyset(total, %add(10))
%pyset(total, %add(20))
Total: %pyget(total)
Output: Total: 30
Macro redefinition and the X macro pattern
A %def with the same name silently replaces the previous definition. This
enables the X macro idiom: define a
list macro that calls a configurable inner macro X for each entry, then
redefine X before each use.
%def(Colors,
%X(Red)
%X(Green)
%X(Blue)
)
%def(X, value, %{%(value),%})
typedef enum { %Colors() } Color;
%def(X, value, %{[%(value)] = "%(value)",%})
const char *color_names[] = { %Colors() };
Semantic tracing
Code generated by macros is fully traceable. Weaveback maintains a two-level
source map (generated line → intermediate noweb line → original literate token).
This allows tools like weaveback trace and the MCP server to pinpoint the
exact macro definition or call-site argument that produced a given line of code.
See architecture.adoc for more on how this integrates with language servers.
Output:
typedef enum Color;
const char *color_names = ;
X need not be defined before Colors is defined — only before %Colors() is
called. Adding an entry to Colors automatically propagates to every projection.