7.1.48. Compile-Time Field Iteration with apply

This tutorial covers daslib/apply — a call macro that iterates struct, tuple, and variant fields at compile time. Unlike runtime RTTI walkers, apply generates specialized code per field with zero reflection overhead.

Prerequisites: basic daslang knowledge (structs, tuples, variants).

options gen2
options rtti

require daslib/apply
require daslib/strings_boost

7.1.48.1. Basic struct iteration

apply(value) $(name, field) { ... } visits every field of a struct. name is a compile-time string constant with the field name, and field is the field value with its concrete type:

struct Hero {
    name   : string
    health : int
    speed  : float
}

let hero = Hero(name = "Archer", health = 100, speed = 3.5)

apply(hero) $(name, field) {
    print("  {name} = {field}\n")
}

Output:

name = Archer
health = 100
speed = 3.5

7.1.48.2. Compile-time dispatch with static_if

Because name is known at compile time, static_if can branch on it. Only the matching branch compiles for each field — the others are discarded entirely:

struct Config {
    width      : int
    height     : int
    title      : string
    fullscreen : bool
}

let cfg = Config(width = 1920, height = 1080,
                 title = "My Game", fullscreen = true)

apply(cfg) $(name, field) {
    static_if (name == "title") {
        print("  Title (special handling): \"{field}\"\n")
    } else {
        print("  {name} = {field}\n")
    }
}

You can also dispatch on the type with typeinfo stripped_typename(field).

7.1.48.3. Mutating fields

Pass a var (mutable) variable and apply gives mutable references to each field:

struct Stats {
    attack  : int
    defense : int
    magic   : int
}

var stats = Stats(attack = 10, defense = 5, magic = 8)

apply(stats) $(name, field) {
    field *= 2
}
// stats is now Stats(attack=20, defense=10, magic=16)

7.1.48.4. Tuples

apply works on tuples too. Unnamed tuple fields are named _0, _1, etc. Named tuples use their declared names:

let pair : tuple<int; string> = (42, "hello")
apply(pair) $(name, field) {
    print("  {name} = {field}\n")
}
// _0 = 42
// _1 = hello

let point : tuple<x : float; y : float> = (1.0, 2.0)
apply(point) $(name, field) {
    print("  {name} = {field}\n")
}
// x = 1
// y = 2

7.1.48.5. Variants

For variants, apply visits only the currently active alternative. The block fires for the one alternative that is set:

variant Shape {
    circle   : float
    rect     : float2
    triangle : float3
}

let s = Shape(circle = 5.0)
apply(s) $(name, field) {
    print("  Shape is {name}: {field}\n")
}
// Shape is circle: 5

7.1.48.6. Generic describe function

apply is ideal for building generic utilities that work on any struct without knowing its fields in advance:

def describe(value) {
    var first = true
    print("\{")
    apply(value) $(name, field) {
        if (!first) {
            print(", ")
        }
        first = false
        static_if (typeinfo stripped_typename(field) == "string") {
            print("{name}=\"{field}\"")
        } else {
            print("{name}={field}")
        }
    }
    print("\}")
}

This prints any struct in {field=value, ...} format without writing type-specific code.

7.1.48.7. Field annotations (3-argument form)

Struct fields can carry metadata via @ annotations:

struct DbRecord {
    @column="user_name"  name  : string
    @column="user_email" email : string
    @skip                id    : int
    @column="age"        age   : int
}

Annotation syntax:

  • @name — boolean (defaults to true)

  • @name=value — integer, float, or bare identifier (string)

  • @name="text" — quoted string

The 3-argument form apply(value) $(name, field, annotations) receives annotations as array<tuple<name:string; data:RttiValue>> for each field. RttiValue is a variant with alternatives tBool, tInt, tFloat, tString, etc.

apply(record) $(name : string; field; annotations) {
    var column_name = name
    var skip = false
    for (ann in annotations) {
        if (ann.name == "skip") {
            skip = true
        } elif (ann.name == "column") {
            column_name = ann.data as tString
        }
    }
    if (!skip) {
        // use column_name and field ...
    }
}

This pattern powers daslib/json_boost’s @rename, @optional, @enum_as_int, @unescape, and @embed field annotations.

7.1.48.8. Skipping a field with return

A block may use return to skip the rest of the current field and move on to the next — handy in a serializer that drops some fields:

apply(record) $(name : string; field; annotations) {
    for (ann in annotations) {
        if (ann.name == "skip") {
            return            // skip this field entirely
        }
    }
    // ... serialize field ...
}

Internally this is the one case where apply does not inline: a block with an escaping return falls back to a generated per-field helper that the macro invokes (so return stays a block-local “skip this field”, not a function exit). The fallback is transparent — nothing you write changes — but it is why apply_imm (below) cannot accept such a block.

7.1.48.9. apply vs apply_imm

apply inlines the block once per field — no helper function and no per-field block invoke — so it is already cheap enough for hot paths like serialization. (The sole exception is a block that uses return to skip a field, which transparently falls back to the per-field invoke codegen, as above.) For a struct-only hot field walk there is a slightly faster sibling, apply_imm: it aliases the block parameters instead of binding reference locals, which is about 25% faster under the interpreter (identical under JIT). Use apply_imm for structs in a hot loop, and apply for tuples, variants, a side-effecting source value, or a block that uses return — because apply_imm always inlines, it has no invoke fallback and rejects a return block at compile time. The block shape is otherwise the same:

apply_imm(record) $(name : string; var field) {
    // same $(name, field[, annotations]) block as apply
}

7.1.48.10. Full source

The complete tutorial source is in tutorials/language/48_apply.das.

Run it with:

daslang.exe tutorials/language/48_apply.das

See also

Full source: tutorials/language/48_apply.das

Apply reflection patternapply call macro reference.

Runtime type information libraryRttiValue variant type used by the 3-argument annotation form.

Data Walking with DapiDataWalker — runtime data walking with DapiDataWalker (Tutorial 47).

Previous tutorial: Data Walking with DapiDataWalker

Next tutorial: Async / Await