8.5.20. Macro Tutorial 20: Template struct/class instances
A family of similar classes — same shape, different element type, a couple of
small behavior switches — usually ends up as copy-paste. daslang has a
class template / struct template grammar for the shared body, and
[template_struct_instance] from daslib/typemacro_boost turns that body
into concrete classes. Each stamped copy gets its own types, its own constant
values, and its own method set, with nothing shared at runtime — this stamping
is called reification.
An instance is just a class that inherits from the template:
a
typedefinside the instance binds a type parameter;an
overrideof a@template_constantfield binds a constant;an instance method with
def overridereplaces the template’s method;an
overrideof a@template_callfield redirects a free-function call.
8.5.20.1. The template
Full source: template_struct_instance_mod.das
[ |> template_struct_instance]
class template public TopKeeperT {
best : KT
n : int = 0
@template_constant KEEP_LATEST : bool = false
def feed(v : KT) {
if (n == 0) {
best = v
} else {
static_if (KEEP_LATEST) {
best = v
} else {
if (v > best) {
best = v
}
}
}
n++
}
def report : string {
return "kept {best} out of {n}"
}
}
Three things to notice:
KTis not declared anywhere. An unresolved name in type position is a type parameter — every instance must bind it with atypedef, and the macro reports the missing name if one forgets.@template_constantmarksKEEP_LATESTas a constant parameter. Instances pick its value withoverride; the field itself is erased from every stamped class, and each use site gets the value as a literal.[|> template_struct_instance]— the|>prefix marks the annotation as inherited: the parser copies it onto every class that derives from the template, ahead of that class’s own annotations. Instances write no macro boilerplate at all, and any annotation an instance does carry (a dispatch or codegen macro, for example) runs after the class is already stamped and concrete.
8.5.20.2. The four instance patterns
Full source: 20_template_struct_instance.das
class FloatTop : TopKeeperT { // 1. type parameter
typedef KT = float
}
class IntLatest : TopKeeperT { // 2. constant parameter
typedef KT = int
override KEEP_LATEST = true
}
class IntShouty : TopKeeperT { // 3. method override
typedef KT = int
def override report : string {
return "BEST {best} OF {n}!"
}
}
FloatTop takes everything from the template: KEEP_LATEST stays
false and feed keeps the maximum. IntLatest flips the constant —
the static_if folds during compilation, so its stamped feed carries
the “keep the latest value” body and no branch. IntShouty replaces
report; the template’s other methods call the replacement, resolved at
stamp time with no virtual dispatch left over.
Constants also fold inside field initializers and inside methods the instance
writes itself — a @template_constant behaves like a per-instance
compile-time value everywhere in the class body.
The fourth pattern parameterizes a free-function call. The template calls
a function by name; @template_call marks that name as rebindable — the
field name is what the body spells, the init is where the call goes:
[ |> template_struct_instance]
class template public MixT {
acc : int = 0
@template_call dot_i = @@dot_i
def feed(a, b : int) {
acc += dot_i(a, b)
}
}
class MixPlain : MixT { // dot_i calls stay on the real dot_i
}
class MixLoud : MixT {
override dot_i = @@dot_i_scaled
}
MixLoud redirects every dot_i(...) call — and every @@dot_i
address — to dot_i_scaled. The value may also be a string
(override dot_i = "dot_i_scaled"). Like a constant, the field is erased:
the stamped class makes a plain direct call, so there is no function pointer
in the object and nothing blocks inlining. Compare this with the
static_if route: a constant switch picks between branches the template
already spells out, while a call parameter is open — a new instance can
route to a function the template has never heard of.
Only the bare spelling rebinds. _::dot_i(...) and __::dot_i(...)
keep their normal _ / __ resolution rules, so a template body that
must reach the real dot_i no matter what an instance rebinds spells the
call qualified.
8.5.20.3. What the reifier does
The macro runs at parse time, when the instance class is declared. It:
binds every template alias to the instance’s
typedefdeclarations (module-level aliases stay untouched — only a genuinely unbound name is an error;[ |> template_struct_instance(late_bind = true)]waives the check for templates whose parameters another macro supplies later in the compile);harvests every
@template_constantvalue, replaces its reads with the literal, and erases the field;harvests every
@template_calltarget and renames the matching calls and@@addresses, erasing the field;clones each template method onto the instance, retyping signatures and bodies, skipping methods the instance defines itself;
cuts the template out of the instance’s ancestry. If the template itself derives from a concrete base class, the instance keeps that base — normal inheritance, upcasts, and virtual dispatch through the base still work.
After step 5 the instance is an ordinary class. Anything that inspects it later — other structure annotations, RTTI, AOT — sees plain concrete code.
8.5.20.4. Output
FloatTop: kept 3.25 out of 3
IntLatest: kept 7 out of 3
IntShouty: BEST 10 OF 3!
MixPlain: acc=6
MixLoud: acc=600
See also
Full source:
20_template_struct_instance.das,
template_struct_instance_mod.das
Previous tutorial: Macro Tutorial 19: Custom module options
Related: Macro Tutorial 16 —
[template_structure] generates parameterized structs from type-position
macros; this tutorial’s annotation serves the inheritance-spelled,
annotation-driven case.
Language reference: Macros — full macro system documentation