8.5.16. Macro Tutorial 16: Template Type Macro
Tutorial 15 showed a type macro that
returns an array type. This tutorial goes further — the pair macro
generates a structure with type-parameterized fields, turning
pair(type<int>, type<float>) into a struct with first : int
and second : float.
The implementation uses the manual approach (without the
[template_structure] shorthand) to expose the mechanics behind
template type resolution: structure aliases, generic inference, and
template class instantiation.
8.5.16.1. Key concepts
Structure aliases — every generated struct stores its type
parameters as aliases (T1 → int, T2 → float). On the generic
path the compiler reads them back to deduce auto(T1),
auto(T2).
``[typemacro_function]`` annotation (from daslib/typemacro_boost)
— converts a regular function into an AstTypeMacro. It
auto-generates the class, registers it with add_new_type_macro, and
extracts typeMacroExpr arguments into the function parameters. The first
two parameters are always macroArgument (the TypeDecl node) and
passArgument (non-null in generic context); subsequent parameters
are the user arguments.
``TypeMacroTemplateArgument`` — a helper struct that pairs a
template parameter name with its declared type (argument_type) and,
after inference, the resolved concrete type (inferred_type).
8.5.16.2. Template definition
The template struct uses alias names (T1, T2) for its fields.
The template keyword prevents direct instantiation — these aliases
only make sense after the type macro resolves them:
struct template Pair {
first : T1
second : T2
}
8.5.16.3. Two paths — concrete and generic
The type macro function handles two compiler contexts:
Concrete (passArgument == null) — the user wrote fully-specified
types like pair(type<int>, type<float>):
1. verify_arguments — ensure no remaining auto types
2. template_structure_name — build mangled name "Pair<int,float>"
3. find_unique_structure — deduplicate (skip if already exists)
4. qmacro_template_class — clone the template struct, rename
it, clear the template flag, apply substitution rules
5. make_typemacro_template_instance — annotate the new struct so
it can be recognized as a Pair instance later
6. add_structure_aliases — store T1=int, T2=float on the struct
Generic (passArgument != null) — a function parameter uses
auto(…) patterns like pair(type<auto(T1)>, type<auto(T2)>) and
the compiler is matching a concrete argument against it:
1. is_typemacro_template_instance — verify the argument is indeed
a Pair instance
2. infer_struct_aliases — read T1, T2 aliases from the concrete
struct into template_arguments[i].inferred_type
3. infer_template_types — for each argument, call
infer_generic_type to match auto(T1) against the actual type,
then update_alias_map so the compiler resolves the names
8.5.16.4. Section 1 — The macro module
options gen2
options no_aot
// Tutorial macro module: template type macro (structure generation).
//
// This module defines a `pair` type macro that produces a structure
// with two fields — `first : T1` and `second : T2`. The macro is
// implemented manually (without the `[template_structure]` shorthand)
// to show the mechanics behind template type resolution:
//
// 1. `[typemacro_function]` annotation converts a regular function
// into an AstTypeMacro. It also auto-extracts the dimExpr
// arguments into the function parameters.
//
// 2. On the **concrete** path (`passArgument == null`) the macro
// generates a new structure via `qmacro_template_class`, gives
// it a mangled name like `Pair<int,float>`, and stores each type
// parameter as a **structure alias** — these aliases tell the
// compiler how to resolve `T1` and `T2` inside the cloned struct.
//
// 3. On the **generic** path (`passArgument != null`) the macro
// reads the aliases back from the concrete struct that was passed
// as the argument, then uses `infer_template_types` to match
// `auto(T1)` / `auto(T2)` against the actual alias values and
// update the compiler's alias map.
module template_type_macro_mod
require daslib/typemacro_boost
// ---------------------------------------------------------------------------
// Template definition.
//
// `struct template` prevents direct instantiation — fields use alias
// names (T1, T2) that only make sense after template substitution.
// ---------------------------------------------------------------------------
struct template Pair {
first : T1
second : T2
}
// ---------------------------------------------------------------------------
// Type macro function.
//
// `[typemacro_function]` generates the AstTypeMacro boilerplate:
// • Creates a class derived from AstTypeMacro
// • Registers it with add_new_type_macro(name="pair")
// • Auto-extracts dimExpr arguments into function parameters
// (macroArgument = td, passArgument = passT, then user args)
// ---------------------------------------------------------------------------
[typemacro_function, unused_argument(macroArgument)]
def pair(macroArgument, passArgument : TypeDeclPtr; T1, T2 : TypeDeclPtr) : TypeDeclPtr {
// Get the TypeDecl that points to our template struct.
var template_type = typeinfo ast_typedecl(type<Pair>)
// Describe the template arguments — names must match the alias
// names used in the struct fields (T1, T2).
var template_arguments <- [
TypeMacroTemplateArgument(name = "T1", argument_type = clone_type(T1)),
TypeMacroTemplateArgument(name = "T2", argument_type = clone_type(T2))
]
// No extra (non-type) arguments for this macro.
var inscope extra_template_arguments : array<tuple<string; string>>
// --- generic path ---
// The compiler passes a concrete type it wants to match against
// our template signature. For example, when a function parameter
// is `pair(type<auto(A)>, type<auto(B)>)` and the caller provides
// a `Pair<int,float>`, passArgument is the Pair<int,float> type.
if (passArgument != null) {
// Verify the concrete type is an instance of our Pair template,
// then read the stored aliases (T1, T2) from the concrete struct
// back into template_arguments[i].inferred_type.
if (!is_typemacro_template_instance(passArgument, template_type, extra_template_arguments)
|| !infer_struct_aliases(passArgument.structType, template_arguments)) return <- TypeDeclPtr()
// Match the inferred concrete types against the template
// parameters (which may contain auto(...) patterns) and
// update the compiler's alias map for further resolution.
return <- infer_template_types(passArgument, template_arguments)
}
// --- concrete path ---
// passArgument is null — the user wrote a concrete type like
// `pair(type<int>, type<float>)`. We need to generate (or
// look up) the corresponding structure.
// All type arguments must be fully resolved (no remaining auto).
if (!verify_arguments(template_arguments)) return <- TypeDeclPtr()
// Build a mangled name like "Pair<int,float>" for deduplication.
let struct_name = template_structure_name(template_type.structType, template_arguments, extra_template_arguments)
// If this exact instantiation already exists, reuse it.
var existing_struct = compiling_program().find_unique_structure(struct_name)
if (existing_struct != null) return <- new TypeDecl(baseType = Type.tStructure, structType = existing_struct, at = template_type.at)
// Clone the template struct, rename it, clear the template flag,
// and apply substitution rules so field types resolve correctly.
var resType = qmacro_template_class(struct_name, type<Pair>)
// Annotate the new struct so `is_typemacro_template_instance`
// can identify it as an instance of Pair later (for generics).
make_typemacro_template_instance(resType.structType, template_type.structType, extra_template_arguments)
// Store T1 and T2 as structure aliases — this is how
// `infer_struct_aliases` will read them back on the generic path.
add_structure_aliases(resType.structType, template_arguments)
return <- resType
}
The [typemacro_function] annotation on pair converts the
function into an AstTypeMacro registered under the name
"pair". The annotation also generates code that extracts
typeMacroExpr[1] and typeMacroExpr[2] into the T1 and T2
TypeDecl? parameters. pair builds its result from Pair
and those two arguments, so it never reads the incoming
macroArgument type — hence the unused_argument(macroArgument)
on the annotation.
The argument names in TypeMacroTemplateArgument ("T1",
"T2") must match the alias names used in the template struct
fields. This is how qmacro_template_class knows which alias to
substitute when cloning the structure.
8.5.16.5. Section 2 — Using the type macro
options gen2
options no_aot
// Tutorial 16 — Template Type Macro (structure generation)
//
// This tutorial uses the `pair` type macro from template_type_macro_mod
// to create parameterized structures. `pair(type<T1>, type<T2>)` expands
// to a struct with `first : T1` and `second : T2`.
//
// Three patterns are demonstrated:
// 1. Concrete instantiation — fully specified types
// 2. Generic parameter — auto-deduced type arguments
// 3. Generic return type — the macro in return-type position
require template_type_macro_mod
// --- 1. Concrete instantiation ---
// `pair(type<int>, type<float>)` generates a struct `Pair<int,float>`
// with `first : int` and `second : float`.
def test_concrete() {
var p : pair(type<int>, type<float>) // nolint:STYLE013 — tutorial demonstrates typefunction-generated struct + field assignment
p.first = 42
p.second = 3.14
print("concrete: first={p.first} second={p.second}\n")
}
// --- 2. Generic parameter ---
// The function accepts any Pair instantiation. The compiler matches
// `auto(T1)` / `auto(T2)` against the stored structure aliases.
def get_first(p : pair(type<auto(T1)>, type<auto(T2)>)) : T1 {
return p.first
}
def get_second(p : pair(type<auto(T1)>, type<auto(T2)>)) : T2 {
return p.second
}
// --- 3. Generic return type ---
// The macro can appear in return-type position as well. Here we swap
// the type arguments — `pair(type<T2>, type<T1>)` — so the returned
// struct has the types reversed.
def swap_pair(p : pair(type<auto(T1)>, type<auto(T2)>)) : pair(type<T2>, type<T1>) {
var result : pair(type<T2>, type<T1>)
result.first = p.second
result.second = p.first
return result
}
[export]
def main() {
test_concrete()
// Generic parameter: get_first / get_second.
var p : pair(type<int>, type<float>) // nolint:STYLE013 — tutorial demonstrates typefunction-generated struct + field assignment
p.first = 10
p.second = 2.5
print("get_first: {get_first(p)}\n")
print("get_second: {get_second(p)}\n")
// Generic return type: swap_pair.
let swapped = swap_pair(p)
print("swapped: first={swapped.first} second={swapped.second}\n")
}
// Expected output:
// concrete: first=42 second=3.14
// get_first: 10
// get_second: 2.5
// swapped: first=2.5 second=10
Three patterns are demonstrated:
Concrete instantiation —
var p : pair(type<int>, type<float>)triggers the concrete path. The macro generates a structPair<int,float>with fieldsfirst : intandsecond : float.Generic parameter —
get_firstandget_secondaccept anyPairinstantiation. The compiler matchesauto(T1)/auto(T2)against the stored aliases and deduces the element types.Generic return type —
swap_pairreturnspair(type<T2>, type<T1>)— the swapped types. Both generic parameter inference and concrete struct generation happen in the same call.
Running the tutorial:
$ daslang tutorials/macros/16_template_type_macro.das
concrete: first=42 second=3.14
get_first: 10
get_second: 2.5
swapped: first=2.5 second=10
See also
Full source:
16_template_type_macro.das,
template_type_macro_mod.das
Previous tutorial: Macro Tutorial 15: Type Macro
Next tutorial: Macro Tutorial 17: Quasi-quotation Reference
Standard library: daslib/typemacro_boost.das — infrastructure for
template type macros (TypeMacroTemplateArgument,
infer_template_types, add_structure_aliases, etc.)
Language reference: Macros — full macro system documentation