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

tutorials/macros/template_type_macro_mod.das
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

tutorials/macros/16_template_type_macro.das
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:

  1. Concrete instantiationvar p : pair(type<int>, type<float>) triggers the concrete path. The macro generates a struct Pair<int,float> with fields first : int and second : float.

  2. Generic parameterget_first and get_second accept any Pair instantiation. The compiler matches auto(T1) / auto(T2) against the stored aliases and deduces the element types.

  3. Generic return typeswap_pair returns pair(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