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.


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)

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

Kairo provides a set of compiler-intrinsic macros for common tasks.

Token manipulation

MacroDescription
concat!(a, b)Join tokens into a single token
stringify!(a)Convert a token to a string literal
unstringify!(s)Convert a string literal back to tokens
var name = concat!(my, _var)       // becomes: my_var
var s = stringify!(some_ident)     // becomes: "some_ident"
Warning

unstringify! converts a string into raw tokens that are injected into the source. This is a potential injection risk only use with trusted, compile-time-known strings.

Variadic helpers

MacroDescription
count!(...args)Number of arguments
first!(...args)First argument
last!(...args)Last argument
rest!(...args)All arguments except the first
init!(...args)All arguments except the last
count!(a, b, c)     // 3
first!(a, b, c)     // a
last!(a, b, c)      // c
rest!(a, b, c)      // b, c
init!(a, b, c)      // a, b

Source location

MacroDescription
file!Current file path as a string literal
line!Current line number as an integer literal
column!Current column number as an integer literal
module_path!Current module path as a string literal
std::println(f"logged from {file!}:{line!}")

Diagnostics

MacroDescription
error!(msg)Emit a compile error with the given message
warning!(msg)Emit a compile warning
note!(msg)Emit a compile note
eval if platform == "wasm" {
    error!("WebAssembly is not supported yet")
}

An optional error code can be passed as a second argument:

warning!("deprecated API", "W0042")

Code generation

MacroDescription
include!(path)Paste file contents as tokens
include_str!(path)File contents as a string literal
embed!(path)File contents as a byte array ([byte])
unique_id!Generate a unique identifier
todo!(msg)Runtime panic placeholder with optional message
unreachable!(msg)Assert a code path is unreachable
// Embed a shader as a string
eval VERTEX_SHADER = include_str!("shaders/vertex.glsl")

// Embed a binary resource
eval ICON_DATA = embed!("assets/icon.png")

// Mark unfinished code
fn process() -> Config {
    todo!("implement config parsing")
}

todo!() panics at runtime with the given message. unreachable!() is undefined behavior if reached the optimizer assumes the code path is dead.

Conditional

MacroDescription
defined!(name)Check if a macro with the given name exists
eval if defined!(DEBUG_MODE) {
    fn log(msg: string) { std::println(f"[DEBUG] {msg}") }
} else {
    fn log(msg: string) { }
}

Hygiene

MacroDescription
undef!(name)Remove a macro definition
macro TEMP! { 42 }
var x = TEMP!        // 42
undef!(TEMP)
// var y = TEMP!     // compile error: TEMP is not defined

Compiler Intrinsic Macros

Some macros are compiler intrinsics that perform operations beyond token substitution:

MacroDescriptionDetails
label!(name)Declare a jump targetControl Flow
jump!(name)Unconditional jump to a labelControl Flow
forget!(ptr)Drop a pointer from AMT trackingUnsafe
unwrap!(expr)Force-unwrap a nullable, panic on nullVariables
mref!(T)Produce an rvalue reference type (&&T)Classes

These use macro syntax (name!) to make their usage explicit and searchable in a codebase, but they are not user-definable they are built into the compiler.


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 = include_str!("shader.glsl")
eval ICON = embed!("icon.png")

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

// Diagnostics
eval if sizeof(usize) < 8 {
    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!
}