-
Notifications
You must be signed in to change notification settings - Fork 3
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.
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 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)
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")
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.
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 ... — 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.
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 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"
Kairo provides a set of compiler-intrinsic macros for common tasks.
Builtin names are reserved: defining a macro with a builtin's name (macro first!(...),
macro paste!(...)) is a compile error. Qualified macros (mymod::first!) do not collide —
builtins only claim the unqualified names.
| Macro | Description |
|---|---|
paste!(a, b) |
Paste identifier tokens into a single identifier |
stringify!(a) |
Convert a token to a string literal |
unstringify!(s) |
Convert a string literal back to tokens |
var name = paste!(my, _var) // becomes: my_var
var s = stringify!(some_ident) // becomes: "some_ident"
paste! joins the spelling of every argument token and produces one identifier; the result must
be a valid identifier.
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.
| Macro | Description |
|---|---|
count!(...args) |
Number of arguments |
first!(...args) |
First argument |
last!(...args) |
Last argument |
rest!(...args) |
All arguments except the first |
butlast!(...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
butlast!(a, b, c) // a, b
These builtins expand macro invocations in their arguments first, then split on top-level commas, so they compose:
count!(rest!(a, b, c)) // 2
first!(rest!(a, b, c)) // b
stringify! and defined! are the exceptions: they operate on their arguments' raw tokens
(quoting and name-probing would be destroyed by expansion).
| Macro | Description |
|---|---|
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!}")
| Macro | Description |
|---|---|
compile_error!(msg) |
Emit a compile error with the given message |
compile_warn!(msg) |
Emit a compile warning |
compile_note!(msg) |
Emit a compile note |
eval if platform == "wasm" {
compile_error!("WebAssembly is not supported yet")
}
The compile_ prefix keeps these distinct from runtime logging: compile_error! aborts the
build, it does not log.
| Macro | Description |
|---|---|
include!(path) |
Paste file contents as tokens |
embed_str!(path) |
File contents as a string literal |
embed!(path) |
File contents as a byte array ([byte]) |
unique_id! / unique_id!(tag)
|
Generate a unique identifier |
todo!(msg) |
Runtime panic placeholder with optional message |
unreachable!(msg) |
Assert a code path is unreachable (panics if reached) |
unreachable_unchecked! |
Optimizer hint: path is dead (UB if reached) |
// Embed a shader as a string
eval VERTEX_SHADER = embed_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!() and unreachable!() are SAFE: both panic at runtime if reached, with unreachable!()
signaling a logic error rather than missing code. unreachable_unchecked!() is the unsafe
variant: it is a hard assertion to the optimizer that the path is dead, and reaching it at
runtime is undefined behavior. Reach for unreachable_unchecked! only when the panic branch of
unreachable! shows up in a profile.
Untagged, unique_id! produces a fresh identifier on every invocation. The tagged form
unique_id!(tag) is stable within one dynamic macro expansion: every unique_id!(guard) inside
the same expansion instance yields the SAME identifier, letting a macro body refer to one
generated name in several places, while separate invocations of the macro still get distinct
names.
| Macro | Description |
|---|---|
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) { }
}
Some macros are compiler intrinsics that perform operations beyond token substitution:
| Macro | Description | Details |
|---|---|---|
label!(name) |
Declare a jump target | Control Flow |
jump!(name) |
Unconditional jump to a label | Control Flow |
forget!(ptr) |
Drop a pointer from AMT tracking | Unsafe |
unwrap!(expr) |
Force-unwrap a nullable, panic on null | Variables |
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.
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 | Attributes | |
|---|---|---|
| Operates on | Raw tokens | AST nodes |
| Type awareness | No | Yes |
| Expansion time | Before parsing | After parsing |
| Can modify structure | Token substitution only | Can transform the AST |
| Syntax | name!(args) |
@name on declarations |
| Hygiene | Scoped, balanced delimiters | Full 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.
// 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 sizeof usize < 8 {
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!
}
This wiki mirrors the language reference at kairolang.org/docs. To edit a page, edit the source at kairo-web/src/content/docs/language changes sync automatically.
Start here: Primitives
1. Fundamentals
2. Functions & Control Flow
3. Types
4. Modules & Metaprogramming
5. Memory & Safety
6. Interop & Concurrency