Macros

Macros

Macros in Kairo are token-level substitutions they operate on raw tokens before parsing, similar to C/C++ #define but with scoping and balanced-delimiter requirements. Macros are identified by the ! suffix on their name.

For AST-level transformations with type awareness, see Attributes.

In this section


Defining Macros

A macro is defined with the macro keyword, a name ending in !, optional parameters, and a body. The body must have balanced delimiters:

macro double!(x) {
    x + x
}

macro greeting! {
    "hello, world"
}

At every use site, the preprocessor replaces the macro invocation with the body, substituting parameters:

var a = double!(5)     // replaced with: 5 + 5
var b = greeting!      // replaced with: "hello, world"

Parameters

Parameters are typeless they accept any sequence of tokens. Multiple parameters are comma-separated:

macro clamp!(value, lo, hi) {
    if value < lo => lo else if value > hi => hi else => value
}

var x = clamp!(temperature, 0, 100)

Default values

A parameter may declare a default token run with =. When the caller omits the argument, the default tokens substitute instead. Defaulted parameters must come after required ones:

macro log!(msg, level = "info") {
    std::println(f"[{level}] {msg}")
}

log!("started")            // level defaults to "info"
log!("failed", "error")

Ignoring extra arguments

A bare ... (no name) as the last parameter accepts any number of extra arguments and discards them. Useful for macros that mirror a wider signature:

macro trace!(msg, ...) { std::println(msg) }

trace!("hit", state, depth)   // state, depth are consumed and dropped

A ... parameter (named or bare) must be last, and cannot follow defaulted parameters.

Variadic macros and pack expansion

The ... prefix on the last parameter declares a token pack: it captures any number of comma-separated arguments. Inside the body, the pack expands with a postfix .... This is the same declaration-prefix / expansion-postfix split used by variadic functions and pack expansion expressions:

macro list!(...xs) { [ xs... ] }

list!()          // [ ]
list!(1)         // [ 1 ]
list!(1, 2, 3)   // [ 1, 2, 3 ]

A bare splat xs... emits the pack's elements joined by commas. An empty pack consumes the splat and emits nothing.

Pattern expansion

A parenthesized token run followed by ... replicates the whole pattern once per pack element, substituting the pack name with that element each time. Instances are comma-joined:

macro wrap_all!(...xs) { [ (box(xs))... ] }

wrap_all!(1, 2, 3)   // [ box(1), box(2), box(3) ]

Ordinary parameters substitute inside patterns as usual:

macro call_each!(f, ...xs) { (f(xs))... }

call_each!(log, a, b)   // log(a), log(b)

The outer parentheses are grouping syntax and are not emitted. To emit parentheses around each instance, double them:

macro pairs!(k, ...xs) { [ ((k, xs))... ] }

pairs!(id, 7, 8)   // [ (id, 7), (id, 8) ]

Two rules keep macro expansion from colliding with expression-level pack expansion in the output: a postfix ... after an identifier that is not the macro's pack, or after a parenthesized run that never mentions the pack, passes through verbatim as target-language code. Expansion is single-level: a nested ... inside a pattern is emitted verbatim.

Macros as arguments

Macros can be passed to other macros. The inner macro is expanded at the final substitution site:

macro tag! { "debug" }
macro repeat!(m) { m! m! m! }

var labels = repeat!(tag)   // becomes: "debug" "debug" "debug"

Built-in macros are covered in Built-in Macros.


Scoping

Unlike C/C++ #define, Kairo macros respect scope. A macro defined inside a module or block is only visible within that scope:

module internal {
    macro BUFFER_SIZE! { 4096 }
    var buf: [u8; BUFFER_SIZE!]
}

// BUFFER_SIZE! is not visible here

Top-level macros follow the same visibility rules as other declarations:

pub macro MAX_RETRIES! { 3 }          // visible to importers
priv macro INTERNAL_FLAG! { true }     // file-scoped

Macros vs Attributes

MacrosAttributes
Operates onRaw tokensAST nodes
Type awarenessNoYes
Expansion timeBefore parsingAfter parsing
Can modify structureToken substitution onlyCan transform the AST
Syntaxname!(args)@name on declarations
HygieneScoped, balanced delimitersFull AST hygiene

Use macros for simple substitutions, constants, and conditional compilation. Use attributes for code transformations that need type information or structural awareness.

See Attributes for the AST-level transformation system.


Summary

// Define a macro
macro square!(x) { x * x }
var s = square!(5)   // 25

// No-parameter macro
macro VERSION! { "1.0.0" }
std::println(f"version: {VERSION!}")

// Built-in macros
std::println(f"file: {file!}, line: {line!}")
eval SHADER = embed_str!("shader.glsl")
eval ICON = embed!("icon.png")

// Variadic helpers
var n = count!(a, b, c, d)   // 4

// Diagnostics
eval if !defined!(__LP64__) {
    compile_error!("64-bit platform required")
}

// Conditional compilation
macro DEBUG! { true }
eval if defined!(DEBUG) {
    fn trace(msg: string) { std::println(f"[TRACE] {msg}") }
}

// Scoped macros
module config {
    priv macro DEFAULT_PORT! { 8080 }
    pub eval PORT = DEFAULT_PORT!
}

See also