5.4.9. Macro Tutorial 9: For-Loop Macros
Previous tutorials transformed calls, functions, structures, blocks, and
variants. For-loop macros operate on for-loop expressions — they
intercept for (... in ...) at compile time and can rewrite the entire
loop before type inference finalises.
[for_loop_macro(name="X")] registers a class that extends
AstForLoopMacro. The compiler calls the macro’s single method
after each for-loop’s sources have been type-checked:
visitExprFor(prog, mod, expr)Called after visiting the for-loop body, during type inference. Source types are resolved. Return a replacement
ExpressionPtrto transform the loop; returndefault<ExpressionPtr>to skip.
5.4.9.1. Motivation
daslang tables (table<K;V>) are not directly iterable. The standard
idiom to iterate a table requires the verbose keys() and values()
built-in functions:
for (k, v in keys(tab), values(tab)) {
print("{k} => {v}\n")
}
This tutorial builds a for-loop macro that supports a much more natural tuple-destructuring syntax:
for ((k,v) in tab) { // rewrites to keys/values automatically
print("{k} => {v}\n")
}
The (k,v) syntax is already supported by the parser — it
produces a backtick-joined iterator name (k`v) and sets a
tuple-expansion flag. All the macro needs to do is detect a table
source, split the name, and rewrite the loop.
Note
Macros cannot be used in the module that defines them. This tutorial has two source files: a module file containing the macro definition and a usage file that requires the module and exercises it.
5.4.9.2. The macro module
5.4.9.2.1. Prerequisites
require ast // AST node types (ExprFor, ExprCall, etc.)
require daslib/ast_boost // AstForLoopMacro base class, [for_loop_macro]
require strings // find, slice — for splitting the iterator name
5.4.9.2.2. Step 1 — Registration
[for_loop_macro(name=table_kv)]
class TableKVForLoop : AstForLoopMacro {
//! Transforms for ((k,v) in tab) into for (k, v in keys(tab), values(tab)).
def override visitExprFor(prog : ProgramPtr; mod : Module?; expr : smart_ptr<ExprFor>) : ExpressionPtr {
[for_loop_macro(name=table_kv)] registers TableKVForLoop so the
compiler calls visitExprFor for every for-loop in modules that
require this one.
5.4.9.2.3. Step 2 — Detect a table source with tuple expansion
Each for-loop’s ExprFor node has several parallel vectors:
sourcesSource expressions (the
inparts).iteratorsIterator variable names.
iteratorsAtSource locations for each iterator.
iteratorsAkaAlias names (from
aka).iteratorsTagsTag expressions.
iteratorsTupleExpansionuint8flag per iterator — nonzero if the parser saw(k,v)syntax.iteratorVariablesResolved variables (populated by inference, cleared on rewrite).
The macro scans for a source where the tuple-expansion flag is set and the source type is a table:
var tab_index = -1
for (index, src in count(), expr.sources) {
if (index < int(expr.iteratorsTupleExpansion |> length)) {
if (int(expr.iteratorsTupleExpansion[index]) != 0 && src._type != null && src._type.isGoodTableType) {
tab_index = index
break
}
}
}
5.4.9.2.4. Step 3 — Split the backtick-joined name
When the user writes (k,v), the parser stores the iterator name as
"k`v" (joined with a backtick). The macro splits this into
key_name and val_name:
let joined_name = string(expr.iterators[tab_index])
let bt = find(joined_name, "`")
if (bt < 0 || find(joined_name, "`", bt + 1) >= 0) {
return <- default<ExpressionPtr> // need exactly 2 parts
}
let key_name = slice(joined_name, 0, bt)
let val_name = slice(joined_name, bt + 1)
5.4.9.2.5. Step 4 — Clone and rewrite
The transformation follows the same pattern as the standard library’s
SoaForLoop (in daslib/soa.das):
Clone the entire
ExprFor.Erase the table entry at
tab_indexfrom all parallel vectors.Add two new sources —
keys(tab)andvalues(tab)— with the split iterator names.Clear
iteratorVariablesso inference rebuilds them on the next pass.
// Clone the for expression
var inscope new_for_e <- clone_expression(expr)
var new_for = new_for_e as ExprFor
// Erase the table entry from all parallel vectors
let source_at = expr.sources[tab_index].at
new_for.sources |> erase(tab_index)
new_for.iterators |> erase(tab_index)
new_for.iteratorsAt |> erase(tab_index)
new_for.iteratorsAka |> erase(tab_index)
new_for.iteratorsTags |> erase(tab_index)
new_for.iteratorsTupleExpansion |> erase(tab_index)
// Add keys(tab) and values(tab) as new sources
new_for.sources |> emplace_new <| make_kv_call("keys", expr.sources[tab_index], source_at)
new_for.sources |> emplace_new <| make_kv_call("values", expr.sources[tab_index], source_at)
// Add key and value iterator names
let si = new_for.iterators |> length
new_for.iterators |> resize(si + 2)
new_for.iterators[si] := key_name
new_for.iterators[si + 1] := val_name
new_for.iteratorsAka |> resize(si + 2)
new_for.iteratorsAka[si] := ""
new_for.iteratorsAka[si + 1] := ""
new_for.iteratorsAt |> push(expr.iteratorsAt[tab_index])
new_for.iteratorsAt |> push(expr.iteratorsAt[tab_index])
new_for.iteratorsTags |> emplace_new <| clone_expression(expr.iteratorsTags[tab_index])
new_for.iteratorsTags |> emplace_new <| clone_expression(expr.iteratorsTags[tab_index])
new_for.iteratorsTupleExpansion |> push(0u8)
new_for.iteratorsTupleExpansion |> push(0u8)
// Clear iterator variables for re-inference
new_for.iteratorVariables |> clear()
The helper make_kv_call builds an ExprCall node for keys()
or values():
def make_kv_call(fn_name : string; src_expr : ExpressionPtr; at : LineInfo) : ExpressionPtr {
var inscope call <- new ExprCall(at = at, name := fn_name)
call.arguments |> emplace_new <| clone_expression(src_expr)
return <- call
5.4.9.3. Using the macro
5.4.9.3.1. Section 1 — Verbose (without macro)
print("--- Section 1: table iteration without the macro ---\n")
var tab <- { "one" => 1, "two" => 2, "three" => 3 }
for (k, v in keys(tab), values(tab)) {
print(" {k} => {v}\n")
}
}
This is the standard idiom: keys() and values() return separate
iterators that advance in lock-step.
5.4.9.3.2. Section 2 — Tuple destructuring
var tab <- { "one" => 1, "two" => 2, "three" => 3 }
for ((k, v) in tab) {
print(" {k} => {v}\n")
}
Exactly the same output, but the syntax is more natural. The macro rewrites this to the verbose form transparently.
5.4.9.3.3. Section 3 — Mixed sources
The macro handles the table source independently; other sources in the same loop are preserved:
var tab <- { "apple" => 10, "banana" => 20, "cherry" => 30 }
for ((k, v), idx in tab, range(100)) {
print(" [{idx}] {k} => {v}\n")
}
Here (k,v) iterates over the table while idx counts from
range(100). All three advance in parallel.
5.4.9.4. Running the tutorial
daslang.exe tutorials/macros/09_for_loop_macro.das
Expected output:
--- Section 1: table iteration without the macro ---
one => 1
three => 3
two => 2
--- Section 2: for ((k,v) in tab) with the macro ---
one => 1
three => 3
two => 2
--- Section 3: mixed sources ---
[0] apple => 10
[1] banana => 20
[2] cherry => 30
(Table iteration order may vary.)
5.4.9.5. Real-world example
The standard library module daslib/soa.das uses the same mechanism.
Its SoaForLoop macro rewrites for (it in soa_struct) to iterate
over the individual arrays of a structure-of-arrays layout — a more
complex transformation but the same AstForLoopMacro pattern.
See also
Full source:
09_for_loop_macro.das,
for_loop_macro_mod.das
Previous tutorial: tutorial_macro_variant_macro
Next tutorial: tutorial_macro_capture_macro
Standard library: daslib/soa.das (SoaForLoop macro)
Language reference: Macros — full macro system documentation