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) → TypeDeclPtrprogis the program being compiled.modis the module that registered the macro.tdis theTypeDeclnode representing the macro invocation — itsdimExprarray carries the arguments.passTis non-null only in a generic context (see below). Return the resolvedTypeDeclPtr, 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 |
|---|---|---|
|
|
The macro name ( |
|
|
The first argument — the type (wraps |
|
|
The second argument — the size ( |
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). Heretd.dimExpr[1]._typeis the fully-resolvedfloattype, andpassTis null. The macro clones_type, adds a dimension, and returnsfloat[4].- Generic — the type includes unresolved type parameters.
Example:
def foo(arr : padded(type<auto(TT)>, 4)). Heretd.dimExpr[1]._typeis null because the type has not been inferred yet.passTis 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 theExprTypeDecl.typeexpr(which preservesauto(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.
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
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