5.4.1. Macro Tutorial 1: Call Macros
Call macros intercept function-call syntax at compile time and replace it with
arbitrary AST. When the compiler sees hello() or printf("...", args), it
invokes your macro’s visit method instead of looking for a function —
giving you full control over what code is generated.
This tutorial builds three progressively complex call macros:
hello()— the simplest possible macro (no arguments)greet("name")— argument validation and string builder constructionprintf(fmt, args...)— format-string parsing with argument reordering
Note
Macros cannot be used in the module that defines them. Every macro tutorial therefore has two source files: a module file containing the macro definitions and a usage file that requires the module and exercises the macros.
5.4.1.1. Prerequisites
Familiarity with daslang basics (functions, strings, control flow) is assumed. No prior macro experience is required — concepts are introduced one at a time.
Key imports used by the module:
require daslib/ast // AST node types (ExprConstString, etc.)
require daslib/ast_boost // AST helpers and ExpressionPtr
require daslib/templates_boost // qmacro, $e() reification
require daslib/strings_boost // ExprStringBuilder
require daslib/macro_boost // [call_macro] annotation, macro_verify
5.4.1.2. Section 1 — hello(): Minimal call macro
A call macro is a class that extends AstCallMacro, annotated with
[call_macro(name="...")]:
[call_macro(name="hello")]
class HelloMacro : AstCallMacro {
def override visit(prog : ProgramPtr; mod : Module?;
var expr : smart_ptr<ExprCallMacro>) : ExpressionPtr {
macro_verify(length(expr.arguments) == 0, prog, expr.at,
"hello() takes no arguments")
return <- qmacro(print("hello, call macro!\n"))
}
}
The visit method receives:
prog — the program being compiled (used for error reporting)
mod — the module where the call appears
expr — the call expression (with
.argumentsand.atfor source location)
It returns an ExpressionPtr — the AST tree that replaces the call.
qmacro(...) is a reification helper: you write normal daslang syntax
inside it and it builds the corresponding AST at compile time.
Usage:
hello() // → print("hello, call macro!\n")
5.4.1.3. Section 2 — greet(“name”): Argument validation
The greet macro validates its single argument and builds a string
interpolation expression:
[call_macro(name="greet")]
class GreetMacro : AstCallMacro {
def override visit(prog : ProgramPtr; mod : Module?;
var expr : smart_ptr<ExprCallMacro>) : ExpressionPtr {
macro_verify(length(expr.arguments) == 1, prog, expr.at,
"greet() requires exactly one string argument")
macro_verify(expr.arguments[0] is ExprConstString, prog, expr.at,
"greet() argument must be a string literal")
var inscope sbuilder <- new ExprStringBuilder(at = expr.at)
sbuilder.elements |> emplace_new <| new ExprConstString(
value := "hello, ", at = expr.at)
sbuilder.elements |> emplace_new <| clone_expression(expr.arguments[0])
sbuilder.elements |> emplace_new <| new ExprConstString(
value := "!\n", at = expr.at)
return <- qmacro(print($e(sbuilder)))
}
}
Key techniques:
``expr.arguments[0] is ExprConstString`` — compile-time type check on the AST node to verify the argument is a string literal.
``macro_verify`` — emits a compile error and returns an empty expression if the condition is false.
``ExprStringBuilder`` — the AST node for string interpolation (
"hello, {name}!\n"). Its.elementsarray holds literal strings and interpolated expressions.``clone_expression`` — duplicates an AST node. Always clone arguments before inserting them into new AST — the original may be used elsewhere.
``$e(expr)`` inside
qmacro— splices an expression node into the reified AST.
Usage:
greet("world") // → print("hello, world!\n")
greet("daslang") // → print("hello, daslang!\n")
5.4.1.4. Section 3 — printf(fmt, args…): Format-string parsing
The printf macro parses a format string at compile time, replacing
(N) placeholders with the corresponding argument expressions:
printf("player (1) scored (2) points\n", "Alice", score)
// → print("player {\"Alice\"} scored {score} points\n")
Arguments can be reordered and repeated:
printf("result: (2) from (1)\n", "source", 100)
printf("(1) and (1) and (1)\n", "echo")
The implementation iterates over the format string character by character,
looking for ( … ) pairs. For each placeholder it:
Extracts the number with
chopand converts it withto_intValidates bounds with
macro_verifyInserts a
clone_expressionof the referenced argument
[call_macro(name="printf")]
class PrintfMacro : AstCallMacro {
def override visit(prog : ProgramPtr; mod : Module?;
var expr : smart_ptr<ExprCallMacro>) : ExpressionPtr {
macro_verify(length(expr.arguments) >= 1, prog, expr.at,
"printf requires at least a format string argument")
macro_verify(expr.arguments[0] is ExprConstString, prog, expr.at,
"first argument to printf must be a constant string")
let totalArgs = length(expr.arguments)
var inscope sbuilder <- new ExprStringBuilder(at = expr.at)
let format = string((expr.arguments[0] as ExprConstString).value)
var pos = 0
while (pos < length(format)) {
var open = find(format, '(', pos)
if (open == -1) {
let tail = format.chop(pos, length(format) - pos)
sbuilder.elements |> emplace_new <| new ExprConstString(
value := tail, at = expr.at)
break
}
if (open > pos) {
let text = format.chop(pos, open - pos)
sbuilder.elements |> emplace_new <| new ExprConstString(
value := text, at = expr.at)
}
var close = find(format, ')', open + 1)
macro_verify(close != -1, prog, expr.at,
"unmatched '(' in format string")
var argNumStr = format.chop(open + 1, close - open - 1)
var argNum = to_int(argNumStr)
macro_verify(argNum >= 1, prog, expr.at,
"argument number must be >= 1")
macro_verify(argNum < totalArgs, prog, expr.at,
"argument index out of range")
sbuilder.elements |> emplace_new <| clone_expression(
expr.arguments[argNum])
pos = close + 1
}
return <- qmacro(print($e(sbuilder)))
}
}
5.4.1.5. Running the tutorial
daslang.exe tutorials/macros/01_call_macro.das
Expected output:
hello, call macro!
hello, world!
hello, daslang!
player Alice scored 42 points
result: 100 from source
echo and echo and echo
pi is approximately 3.14, or roughly 3
See also
Full source:
call_macro_mod.das,
01_call_macro.das
Next tutorial: tutorial_macro_when_expression
Language reference: Macros — full macro system documentation