5.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 rtti
require daslib/apply
require daslib/strings_boost

5.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

5.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).

5.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)

5.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

5.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

5.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.

5.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.

5.1.48.8. 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