5.4.7. Macro Tutorial 7: Block Macros
Tutorials 3–6 transformed functions and structs. Block macros
operate on block closures instead — the $ { ... } expressions
that are passed to functions as arguments.
[block_macro(name="X")] registers a class that extends
AstBlockAnnotation. When a block is annotated with [X], the
compiler calls the macro’s methods at two stages of the compilation
pipeline:
Method |
When it runs |
|---|---|
|
During parsing, before inference. Can modify the block’s statement list and finally list, validate annotation arguments, and inject code. |
|
After all inference and optimization. Read-only — argument types are fully resolved, useful for diagnostics. |
This tutorial builds a [traced(tag="X")] annotation that:
Prepends an enter-message and appends an exit-message (via
finalList) to the block body — the exit message runs even on early return (apply).Prints a compile-time summary of each block’s typed arguments and statement count (
finish).
run_block() $ [traced(tag="setup")] {
print(" initializing\n")
}
After compilation the block body gains print(">> setup\n") at the
start and print("<< setup\n") in its finally section, so every
invocation prints entry and exit markers around the user code.
5.4.7.1. Block annotation syntax
Block annotations are placed between the $ sigil and the parameter
list (or body, for parameterless blocks):
// Parameterless block
$ [annotation(args)] { body }
// Block with parameters
$ [annotation(args)] (params) { body }
Multiple annotations can be comma-separated inside the brackets, just like function annotations:
$ [traced(tag="x"), REQUIRE(hp)] (v : int) { ... }
5.4.7.2. Why only two methods?
Structure macros have three methods (apply, patch, finish)
because structs often need type-aware code generation in patch()
after inference resolves field types. Block macros skip patch()
entirely:
apply() — The block is parsed but types may not be resolved. You can modify
blk.list(the statement list) andblk.finalList(the finally section). Validation and code injection happen here.finish() — Everything is final: argument types are resolved, code is optimized. No modifications allowed. Use it for diagnostics, compile-time reporting, or verifying block structure.
Blocks are self-contained expressions — simpler than functions or structs — so two methods are sufficient. If you need post-inference code generation, use a function macro or structure macro instead.
5.4.7.3. The module: block_macro_mod.das
5.4.7.3.1. Registration
[block_macro(name="traced")]
class TracedBlockMacro : AstBlockAnnotation {
...
}
[block_macro(name="traced")] tells the compiler:
When a block is annotated with
[traced], call this class’s methods during compilation.
Block annotations are registered as function annotations under the
hood — add_new_block_annotation internally calls
add_function_annotation. This is why block annotations share the
AnnotationArgumentList parameter type with function macros.
5.4.7.3.2. Inside apply()
The method receives the block expression, annotation arguments, and an error string. It runs during parsing, before inference.
5.4.7.3.2.1. Step 1 — Validate arguments
let labelArg = find_arg(args, "tag")
if (!(labelArg is tString)) {
errors := "[traced] requires a 'tag' string argument"
return false
}
let lbl = labelArg as tString
We use find_arg (from daslib/ast_boost) to look up the
tag argument by name. It returns an RttiValue variant —
we check is tString and cast with as tString. Returning
false aborts compilation with the error message.
5.4.7.3.2.2. Step 2 — Prepend enter-print
var inscope enterExpr <- qmacro(print($v(">> {lbl}\n")))
blk.list |> emplace(enterExpr, 0)
blk.list is the block’s statement array. emplace(vec, val, 0)
inserts at position 0, pushing existing statements down. This places
the enter-print before any user code.
$v(">> {lbl}\n") splices the compile-time string (with the tag
value baked in) as a constant expression in the generated code.
5.4.7.3.2.3. Step 3 — Append exit-print to finalList
var inscope exitExpr <- qmacro(print($v("<< {lbl}\n")))
blk.finalList |> emplace(exitExpr)
blk.finalList is the block’s finally section — statements here
run when the block exits, regardless of how it exits. This guarantees
the exit message prints even if the block has an early return.
Note
finalList on loop blocks (blockFlags.inTheLoop) only runs
once at loop exit, not per iteration. For block closures passed to
functions (like in this tutorial), finalList runs on every
invoke, which is correct for enter/exit tracing.
5.4.7.3.3. Inside finish()
def override finish(var blk : smart_ptr<ExprBlock>; var group : ModuleGroup;
args, progArgs : AnnotationArgumentList;
var errors : das_string) : bool {
let labelArg = find_arg(args, "tag")
var lbl = "?"
if (labelArg is tString) {
lbl = labelArg as tString
}
// Count original statements (subtract injected enter-print)
var numStmts = 0
for (s in blk.list) {
numStmts++
}
if (numStmts > 0) {
numStmts -= 1
}
print("[traced] \"{lbl}\": {numStmts} statement(s)")
// Iterate typed arguments — types are now resolved
var numArgs = 0
for (a in blk.arguments) {
numArgs++
}
if (numArgs > 0) {
print(", args = (")
var first = true
for (arg in blk.arguments) {
if (!first) {
print(", ")
}
print("{arg.name}:{describe(arg._type)}")
first = false
}
print(")")
}
print("\n")
return true
}
finish() runs after all inference and optimization. The block is
in its final form — argument types are fully resolved. We iterate
blk.arguments (a managed vector of VariablePtr) and call
describe(arg._type) to get human-readable type names like
int const.
We count statements by iterating blk.list and subtract one to
account for the enter-print injected by apply().
5.4.7.4. The usage file
options gen2
require block_macro_mod
def run_block(blk : block) {
invoke(blk)
}
def apply_to(x : int; blk : block<(v : int) : void>) {
invoke(blk, x)
}
[export]
def main() {
print("--- simple block ---\n")
run_block() $ [traced(tag="setup")] {
print(" initializing\n")
}
print("\n--- block with argument ---\n")
apply_to(42) $ [traced(tag="process")] (v : int) {
print(" received {v}\n")
}
}
The first block is parameterless — it demonstrates the basic
[traced] syntax. The second block takes an int parameter,
showing that finish() can report its fully-typed arguments.
Both blocks are invoked by helper functions via invoke(blk) /
invoke(blk, x). Each invocation executes the modified block body
(with the injected enter/exit prints).
Compile-time output (from finish):
[traced] "setup": 1 statement(s)
[traced] "process": 1 statement(s), args = (v:int const)
Runtime output:
--- simple block ---
>> setup
initializing
<< setup
--- block with argument ---
>> process
received 42
<< process
5.4.7.5. Compilation pipeline summary
The full sequence for a [traced] block:
parse block closure
↓
apply() → validate tag, prepend enter-print, append exit-print to finalList
↓
infer types (block arguments and body are inferred)
↓
optimize
↓
finish() → compile-time diagnostic (statement count, typed args)
↓
simulate → invoke block at runtime (enter-print, user code, exit-print)
Unlike structure macros, there is no patch → re-infer cycle.
All code injection happens in apply() before the first inference
pass.
5.4.7.6. Key takeaways
Concept |
What it does |
|---|---|
|
Registers a class as a block annotation |
|
Base class with |
|
Pre-inference: modify statement list, inject code, validate args |
|
Final: read-only diagnostics and reporting with resolved types |
|
The block’s statement array — modify to inject code |
|
The block’s finally section — runs on exit, like a |
|
The block’s parameter list ( |
|
Insert at a specific position in a managed vector |
|
Look up annotation argument values by name |
|
Human-readable type name from a |
|
Block annotation syntax — annotation between |
See also
Full source:
block_macro_mod.das,
07_block_macro.das
Previous tutorial: tutorial_macro_structure_macro
Next tutorial: tutorial_macro_variant_macro
Standard library examples:
daslib/decs_boost.das (REQUIRE / REQUIRE_NOT — marker block annotations),
daslib/defer.das (finalList manipulation),
daslib/heartbeat.das (emplace(list, expr, 0) prepend pattern)
Language reference: Macros — full macro system documentation