Macros

Macros are one of Oxide's most powerful features. They allow you to write code that writes other code, enabling abstractions that would be difficult or impossible with functions alone.

What Are Macros?

Macros are programs that generate code at compile time. They're different from functions in important ways:

AspectFunctionsMacros
Type checkingCheckedGenerated code is checked
ParametersMust match typesCan accept variable number of arguments
MonomorphizationFunctions are sharedCode can be duplicated
SpeedRuntimeCompile time

There are two kinds of macros in Oxide:

  1. Declarative macros - Define rules for pattern matching and code generation
  2. Procedural macros - Functions that manipulate token streams

Declarative Macros

Declarative macros, also called "macros by example," let you define a syntax pattern and the code to generate when that pattern matches.

The vec! Macro

The vec! macro is a classic example:

// Calling the macro
let v = vec![1, 2, 3]

// Equivalent to
let v = {
    var v = Vec.new()
    v.push(1)
    v.push(2)
    v.push(3)
    v
}

Defining Declarative Macros

The syntax is:

Note: macro_rules! blocks use Rust's macro syntax (including => for rule arms). This is a deliberate compatibility island; outside macros, Oxide uses ->.

macro_rules! name {
    (pattern1) => { expansion1 };
    (pattern2) => { expansion2 };
}

Here's a simple example:

macro_rules! shout {
    ($e:expr) => {
        println!("\(($e).toUppercase())!")
    };
}

fn main() {
    shout!("hello")  // Prints: HELLO!
    shout!("world")  // Prints: WORLD!
}

Macro Patterns and Fragments

The part in parentheses after the macro name defines what patterns it accepts. Common fragment specifiers:

SpecifierMatches
exprExpressions
stmtStatements
itemItems (functions, structs, etc.)
tyTypes
identIdentifiers
pathPaths
ttToken trees
blockBlock expressions

Here's a macro that accepts multiple expressions:

macro_rules! printAll {
    ($($e:expr),*) => {
        $(
            println!("\($e)")
        )*
    };
}

fn main() {
    printAll!(1, 2, 3, "hello", true)
}

The $(...)* syntax means "repeat this pattern zero or more times."

Building the vec! Macro

Let's build our own version of vec!:

macro_rules! myVec {
    // Empty vec
    () => {
        Vec.new()
    };

    // Single element
    ($e:expr) => {
        {
            var v = Vec.new()
            v.push($e)
            v
        }
    };

    // Multiple elements
    ($($e:expr),+) => {
        {
            var v = Vec.new()
            $(
                v.push($e)
            )*
            v
        }
    };

    // Comma-separated list with trailing comma
    ($($e:expr),+ ,) => {
        myVec!($($e),*)
    };
}

fn main() {
    let v1 = myVec!()
    let v2 = myVec!(1)
    let v3 = myVec!(1, 2, 3)
    let v4 = myVec!(1, 2, 3,)

    println!("\(v1:?)")  // []
    println!("\(v2:?)")  // [1]
    println!("\(v3:?)")  // [1, 2, 3]
    println!("\(v4:?)")  // [1, 2, 3]
}

Repetition Patterns

The repetition syntax uses several operators:

// * means 0 or more times
($($e:expr),*)

// + means 1 or more times
($($e:expr),+)

// ? means 0 or 1 times
($e:expr)?

// You can separate with any token
($($e:expr);*)  // semicolon-separated
($($e:expr) |*)  // pipe-separated

Advanced Example: DSL for Assertions

Here's a declarative macro that creates a domain-specific language (DSL) for assertions:

macro_rules! assertEqWithMsg {
    ($left:expr, $right:expr) => {
        assertEqWithMsg!($left, $right, "")
    };

    ($left:expr, $right:expr, $msg:expr) => {
        {
            let left = &($left)
            let right = &($right)
            if left != right {
                panic!(
                    "assertion failed: {} == {}, message: {}",
                    left, right, $msg
                )
            }
        }
    };
}

fn main() {
    assertEqWithMsg!(5, 5)
    assertEqWithMsg!(10, 5 * 2, "Basic math")
    // assertEqWithMsg!(5, 6)  // Would panic
}

Procedural Macros

Procedural macros are more powerful but also more complex. They're functions that take a token stream as input and produce a token stream as output.

Derive Macros

The most common procedural macros are derive macros, which automatically implement traits:

#[derive(Clone, Copy, Debug)]
struct Point {
    x: Int,
    y: Int,
}

fn main() {
    let p = Point { x: 5, y: 10 }
    let p2 = p.clone()

    println!("\(p:?)")   // Point { x: 5, y: 10 }
    println!("\(p2:?)")  // Point { x: 5, y: 10 }
}

In this example, #[derive(Debug)] automatically implements the Debug trait, so we can print the struct with the :? format specifier.

Common derive macros:

  • Debug - Printable representation
  • Clone - Cloning
  • Copy - Copy semantics
  • Default - Default values
  • Hash - Hashing
  • PartialEq, Eq - Equality
  • PartialOrd, Ord - Ordering

Attribute Macros

Attribute macros modify items:

#[route(GET, "/")]
fn index(): String {
    "Hello, world!".toString()
}

The #[route(...)] macro is called with the attribute parameters and the item it decorates.

Function-like Macros

Function-like procedural macros look like function calls but process tokens:

let sql = sql!(
    SELECT * FROM users
    WHERE age > 18
)

Macro Rules and Macros Defined in Terms of Other Macros

Macros can call other macros:

macro_rules! fiveTimes {
    ($e:expr) => {
        $e + $e + $e + $e + $e
    };
}

macro_rules! timesFifteen {
    ($e:expr) => {
        fiveTimes!(5 * $e) + fiveTimes!(2 * $e)
    };
}

fn main() {
    println!("\(timesFifteen!(2))")  // (5 * 2) * 5 + (2 * 2) * 5 = 70
}

Common Macro Patterns

Variadic Functions

Implement function-like behavior with variable arguments:

macro_rules! sum {
    ($e:expr) => { $e };
    ($e:expr, $($rest:expr),+) => {
        $e + sum!($($rest),+)
    };
}

fn main() {
    println!("\(sum!(1))")           // 1
    println!("\(sum!(1, 2))")        // 3
    println!("\(sum!(1, 2, 3, 4))")  // 10
}

Type Repetition

Generate code for multiple types:

macro_rules! implForTypes {
    ($trait:ident, $method:ident, $($ty:ty),*) => {
        $(
            extension $ty: $trait {
                fn $method(): String {
                    stringify!($ty).toString()
                }
            }
        )*
    };
}

Debug Printing

Create convenient debugging utilities:

macro_rules! dbg {
    ($($e:expr),*) => {
        $(
            eprintln!("{} = {:?}", stringify!($e), &$e)
        )*
    };
}

fn main() {
    let x = 5
    let y = 10
    dbg!(x, y)  // Prints debug info
}

Hygiene

Macros are "hygienic," meaning they don't accidentally capture or shadow variables:

macro_rules! setup {
    ($n:expr) => {
        let x = $n  // This x is local to the macro
    };
}

fn main() {
    let x = "outer"
    setup!(5)
    println!("\(x)")  // Prints: outer, not 5
}

This is different from macros in C, where they're simple text substitution.

Useful Built-in Macros

Oxide provides several useful built-in macros:

// Print debugging
println!("Hello, \(name)!")

// Panic with a message
panic!("Something went wrong")

// Assert conditions
assert!(condition)
assertEq!(left, right)

// Create vectors
vec![1, 2, 3]

// Stringify expressions
let s = stringify!(x + y)

// Get file and line info
file!()
line!()
column!()

// Access environment variables at compile time
env!("HOME")

When to Use Macros

Use macros when:

  1. Repetitive code generation - Avoid boilerplate
  2. Domain-specific languages - Create natural syntax
  3. Metaprogramming - Generate code based on input
  4. Zero-cost abstractions - Generate specialized code
  5. Syntax extensions - Extend the language

Don't use macros when:

  1. A function would work - Functions are simpler and more testable
  2. You need dynamic behavior - Macros run at compile time
  3. Code clarity matters more - Macros can hide what's happening
  4. You need debugging - Macro expansion can be hard to debug

Debugging Macros

To see what a macro expands to, use cargo expand (requires the nightly compiler):

cargo expand

This will show the expanded code, helping you understand what the macro is generating.

Best Practices

Document Your Macros

/// Creates a vector with the given elements.
///
/// # Examples
///
/// ```
/// let v = myVec![1, 2, 3]
/// assertEq!(v.len(), 3)
/// ```
macro_rules! myVec {
    // ...
}

Keep Them Simple

// Good: simple, focused
macro_rules! triple {
    ($e:expr) => { $e + $e + $e };
}

// Avoid: complex, hard to understand
macro_rules! complex {
    // ... 50 lines of rules
}

Use Descriptive Names

// Good
macro_rules! assertError {
    // ...
}

// Avoid
macro_rules! ae {
    // ...
}

Test Your Macros

#[test]
fn testMyVecMacro() {
    let v = myVec![1, 2, 3]
    assertEq!(v.len(), 3)
    assertEq!(v[0], 1)
}

Summary

Macros in Oxide:

  • Declarative macros - Pattern matching and code generation
  • Procedural macros - More powerful, work with token streams
  • Derive macros - Automatically implement traits
  • Hygiene - Safe variable scoping
  • Powerful abstractions - Can eliminate boilerplate and create DSLs

Macros are advanced because they're powerful but require careful design. Use them judiciously, and they can make your code cleaner and more expressive. When in doubt, prefer functions—they're easier to understand and debug.