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 ExpressionPtr to transform the loop; return default<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:

sources

Source expressions (the in parts).

iterators

Iterator variable names.

iteratorsAt

Source locations for each iterator.

iteratorsAka

Alias names (from aka).

iteratorsTags

Tag expressions.

iteratorsTupleExpansion

uint8 flag per iterator — nonzero if the parser saw (k,v) syntax.

iteratorVariables

Resolved 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):

  1. Clone the entire ExprFor.

  2. Erase the table entry at tab_index from all parallel vectors.

  3. Add two new sources — keys(tab) and values(tab) — with the split iterator names.

  4. Clear iteratorVariables so 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