5.4.15. Macro Tutorial 15: Type Macro

Previous tutorials showed macros attached to functions, structures, loops, enums, typeinfo expressions, and entire compilation passes. Type macros operate in a different domain: they let you define custom type expressions that the compiler resolves during type inference.

AstTypeMacro is the base class. It has a single method:

visit(prog : ProgramPtr; mod : Module?; td : TypeDeclPtr; passT : TypeDeclPtr) TypeDeclPtr

prog is the program being compiled. mod is the module that registered the macro. td is the TypeDecl node representing the macro invocation — its dimExpr array carries the arguments. passT is non-null only in a generic context (see below). Return the resolved TypeDeclPtr, or an empty pointer on error.

The annotation [type_macro(name="…")] registers a class as a type macro. The name string determines the identifier used in type position:

[type_macro(name="padded")]
class PaddedTypeMacro : AstTypeMacro { ... }

After registration, padded(type<float>, 4) is a valid type expression.

5.4.15.1. How the parser stores arguments

When the compiler sees a type-macro invocation like padded(type<float>, 4) it creates a TypeDecl with baseType = Type.typeMacro and populates the dimExpr array:

Index

AST node

Contains

dimExpr[0]

ExprConstString

The macro name ("padded")

dimExpr[1]

ExprTypeDecl

The first argument — the type (wraps type<float>)

dimExpr[2]

ExprConstInt

The second argument — the size (4)

Type arguments are wrapped in ExprTypeDecl; their inferred result is available via dimExpr[i]._type. Value arguments keep their expression type — ExprConstInt for integer literals, ExprConstString for strings, and so on.

5.4.15.2. Two inference contexts

The compiler calls visit() in two different contexts:

Concrete — all type arguments are already inferred.

Example: var data : padded(type<float>, 4). Here td.dimExpr[1]._type is the fully-resolved float type, and passT is null. The macro clones _type, adds a dimension, and returns float[4].

Generic — the type includes unresolved type parameters.

Example: def foo(arr : padded(type<auto(TT)>, 4)). Here td.dimExpr[1]._type is null because the type has not been inferred yet. passT is non-null and carries the actual argument type being matched. The macro must return a type the compiler can use for generic matching — typically by cloning the ExprTypeDecl.typeexpr (which preserves auto(TT)) and adding the dimension.

Important

Always check td.dimExpr[i]._type == null to distinguish the generic path from the concrete path. In the generic path, fall back to .typeexpr or create an autoinfer type.

5.4.15.3. Section 1 — The macro module

The type macro lives in its own module so that it is available via require — just like macros in previous tutorials.

tutorials/macros/type_macro_mod.das
options gen2
options no_aot

// Tutorial macro module: type macro (AstTypeMacro).
//
// AstTypeMacro lets you define custom type expressions that the compiler
// resolves during type inference.  It has a single method:
//   visit(prog : ProgramPtr; mod : Module?; td : TypeDeclPtr; passT : TypeDeclPtr) : TypeDeclPtr
//
// When the compiler sees `padded(type<float>, 4)` in a type position it:
//   1. Parses it into a TypeDecl with baseType = Type.typeMacro
//   2. Stores the arguments in `td.dimExpr`:
//        td.dimExpr[0] — ExprConstString with the macro name ("padded")
//        td.dimExpr[1] — the type argument (ExprTypeDecl wrapping type<float>)
//        td.dimExpr[2] — the size argument (ExprConstInt with value 4)
//   3. Calls visit() so the macro can return the resolved type
//
// The `passT` parameter is non-null only in a generic context — it
// carries the actual argument type being matched against the parameter.
// In a concrete declaration (`var x : padded(type<float>, 4)`) passT
// is null and td.dimExpr[1]._type is already inferred.

module type_macro_mod

require ast
require daslib/ast_boost
require daslib/templates_boost

[type_macro(name="padded")]
class PaddedTypeMacro : AstTypeMacro {
    //! Type macro that resolves `padded(type<T>, N)` to `T[N]`.
    def override visit(prog : ProgramPtr; mod : Module?; td : TypeDeclPtr; passT : TypeDeclPtr) : TypeDeclPtr {
        // --- argument validation ---
        // dimExpr must have exactly 3 entries: name + type + size.
        if (length(td.dimExpr) != 3) {
            macro_error(compiling_program(), td.at, "padded expects 2 arguments: type and size")
            return <- TypeDeclPtr()
        }
        // The size argument must be a constant integer.
        if (!(td.dimExpr[2] is ExprConstInt)) {
            macro_error(compiling_program(), td.at, "padded: second argument must be a constant integer")
            return <- TypeDeclPtr()
        }
        let count = (td.dimExpr[2] as ExprConstInt).value

        // --- generic path ---
        // When the type argument is not yet inferred (e.g. in a generic
        // parameter like `padded(type<auto(TT)>, 4)`) we return a type
        // with autoinfer so the compiler can match and deduce TT.
        if (td.dimExpr[1]._type == null) {
            var inscope auto_type : TypeDeclPtr
            if (td.dimExpr[1] is ExprTypeDecl) {
                // Clone the unresolved type expression (e.g. auto(TT)).
                auto_type |> move_new <| clone_type((td.dimExpr[1] as ExprTypeDecl).typeexpr)
            } else {
                // Fallback: pure auto-infer.
                auto_type |> move_new <| new TypeDecl(baseType = Type.autoinfer)
            }
            auto_type.dim |> push(count)
            return <- auto_type
        }

        // --- concrete path ---
        // The type argument is fully inferred.  Clone it and add the
        // dimension to produce the final array type (e.g. float[4]).
        var inscope final_type <- clone_type(td.dimExpr[1]._type)
        final_type.dim |> push(count)
        return <- final_type
    }
}

The visit method first validates that exactly two user arguments were provided (dimExpr length 3 — the name plus two arguments) and that the size argument is a constant integer.

For the generic path (_type == null): if the first argument is an ExprTypeDecl (e.g. wrapping auto(TT)), clone its .typeexpr; otherwise create a pure autoinfer type. Add the requested dimension and return.

For the concrete path: clone _type (the already-inferred type), add the dimension, and return the final type — e.g. float[4].

5.4.15.4. Section 2 — Using the type macro

tutorials/macros/15_type_macro.das
options gen2
options no_aot

// Tutorial 15 — Type Macro (AstTypeMacro)
//
// This tutorial demonstrates how to create and use a custom type macro.
// The `padded` macro defined in type_macro_mod.das resolves the type
// expression `padded(type<T>, N)` into `T[N]`.
//
// Two usage patterns are shown:
//   1. Concrete — fully specified types (`padded(type<float>, 4)`)
//   2. Generic  — auto-deduced types (`padded(type<auto(TT)>, 4)`)

require type_macro_mod

// --- Concrete usage ---
// The compiler sees `padded(type<float>, 4)` and calls PaddedTypeMacro.visit().
// The macro returns float[4], so `data` is a fixed-size array of 4 floats.

def test_concrete() {
    var data : padded(type<float>, 4)
    data[0] = 1.0
    data[1] = 2.0
    data[2] = 3.0
    data[3] = 4.0
    print("concrete: data = {data}\n")
}

// --- Generic usage ---
// When the type parameter uses `auto(TT)` the compiler resolves TT from
// the call site.  The macro returns auto(TT)[4] so the compiler can match
// and deduce the element type.

def sum_padded(arr : padded(type<auto(TT)>, 4)) : TT {
    var total : TT
    for (i in range(4)) {
        total += arr[i]
    }
    return total
}

[export]
def main() {
    test_concrete()

    // sum_padded deduces TT = float from the argument type.
    var floats : padded(type<float>, 4)
    floats[0] = 10.0
    floats[1] = 20.0
    floats[2] = 30.0
    floats[3] = 40.0
    let float_sum = sum_padded(floats)
    print("generic: float sum = {float_sum}\n")

    // sum_padded deduces TT = int from the argument type.
    var ints : padded(type<int>, 4)
    ints[0] = 1
    ints[1] = 2
    ints[2] = 3
    ints[3] = 4
    let int_sum = sum_padded(ints)
    print("generic: int sum = {int_sum}\n")
}

// Expected output:
//   concrete: data = [[ 1; 2; 3; 4]]
//   generic: float sum = 100
//   generic: int sum = 10

test_concrete declares var data : padded(type<float>, 4). The compiler invokes the macro in concrete mode — _type is float, so the macro returns float[4]. data is a fixed-size array of 4 floats.

sum_padded declares its parameter as padded(type<auto(TT)>, 4). The compiler invokes the macro in generic mode. When called with floats (a float[4]), TT is deduced as float; when called with ints (an int[4]), TT is deduced as int.

Running the tutorial:

$ daslang tutorials/macros/15_type_macro.das
concrete: data = [[ 1; 2; 3; 4]]
generic: float sum = 100
generic: int sum = 10

See also

Full source: 15_type_macro.das, type_macro_mod.das

Previous tutorial: tutorial_macro_pass_macro

Next tutorial: tutorial_macro_template_type_macro

Language reference: Macros — full macro system documentation