---
title: 'Declarative Macros'
source: 'https://academia.sh/en/courses/rust-advanced/declarative-macros'
course: 'The Rust Ecosystem and Advanced Topics'
language: en
updated: '2026-09-15T04:58:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows by running it that the body a declarative macro generates passes type checking the same way a hand-written body does'
    - "Verifies that macro hygiene keeps a name inside a macro from clashing with the caller's variable of the same name"
    - 'Explains with an example that when the same macro is called with two different types, both compile, but the macro has no way of knowing which one is meaningful'
---

# Declarative Macros

The course's measurement core runs from inside the program and places twelve observations into four classes — still holds, handed to the writer, outside the language, nobody checks it; the distribution is 4/3/2/3, and this lesson deepens only the two observations that fall into the 'still holds' class: the body a macro generates passes type checking, and a name inside the macro does not clash with the caller's.

Throughout the Concurrency and Asynchronous Rust course, what a task needed in order to make
progress was measured — a channel, a lock, a wait point. Across four courses, one assumption
was never questioned: the compiler stands behind everything and checks every step. This course
opens exactly that assumption to measurement. The same question will be asked across twelve
different constructs: where does the compiler's guarantee end here?

The answer falls into one of four classes, and these four names do not change across the
course. This first lesson runs all twelve together, in a single measurement core; then it
deepens two of them — the check on the body a macro generates, and the fact that a macro's own
inner names do not clash with the caller's. The remaining ten questions fall to the course's
next eleven lessons; this lesson takes only the two that concern macros, since this topic's
subject is macros.

## The Four Classes

| Class | Definition |
|---|---|
| still holds | The compiler keeps checking |
| handed to the writer | Checking stopped; whoever wrote the condition took it over |
| outside the language | Correctness is determined by another language, tool, or target |
| nobody checks it | No one checks it |

This four-way split should not be confused with the question asked in the Standard Library
and Streams course: there, the question was who gives the guarantee for a behavior — the
interface, the implementation, or the caller — that is, the parties to a contract. Here, the
question is where the compiler's own checking stops. Declarative macros give the clearest
example of the first of these four classes, still holds: the source a macro call produces is
indistinguishable, from the compiler's point of view, from hand-written source, and it is
checked by the same rules.

To see why this is not surprising, it helps to look at when the macro actually acts. A
declarative macro's expansion happens **before** type checking: the compiler first replaces
every macro call in the source with the text it produces, then reads the whole file as ordinary
Rust source and applies typing, borrowing rules, and name resolution in turn. This ordering
explains why the source a macro produces falls into the "still holds" class: the moment the
macro expands, it disappears, leaving only ordinary Rust source behind, and the mechanism that
checks that source is the same one that checks a hand-written file.

## The Measurement Core

The core used throughout the rest of the course is `guarantee.rs`. It writes all twelve
observations into a single observation ledger and, at the end, prints the total for each class.
It uses no third-party crate: the foreign function interface is built with two functions the C
library already links in, and no outside package enters the build. The course's remaining
eleven lessons will each take only their own slice of this core; only this lesson shows all
twelve observations together.

**MM1.** The `observe` function and the `ledger` tuple (a `String`, `String`, `String` triple)
are the core's own design decision. What decides which class an observation falls into is the
shape of each line in the source; the `observe` call only records that decision into a table
so that all twelve observations can be compared in a single run.

```rust
// guarantee.rs — where does the compiler's guarantee end: does it still hold, was it handed
// to the writer, is it outside the language, or does nobody check it at all.

use std::ffi::CString;
use std::os::raw::{c_char, c_int};

// Two functions outside the language: the compiler never saw their bodies.
extern "C" {
    fn abs(x: c_int) -> c_int;
    fn strlen(s: *const c_char) -> usize;
}

// Declarative macro: the body it generates also passes type checking.
macro_rules! double_it {
    ($name:ident) => {
        fn $name(x: i32) -> i32 {
            x * 2
        }
    };
}

double_it!(generated_fn);

// Macro hygiene: the name inside does not clash with the caller's name.
macro_rules! uses_own_name {
    () => {{
        let x = 99;
        x
    }};
}

/// This doc line says the function returns the **largest** value.
/// The function returns the smallest one. No tool checks this contradiction.
fn wrong_doc(a: i32, b: i32) -> i32 {
    if a < b {
        a
    } else {
        b
    }
}

// Body excluded by cfg: the compiler never sees it at all.
#[cfg(feature = "unopened")]
fn never_checked() {
    this_function_was_never_defined();
    let _x: i32 = "text";
}

const VERSION_STRING: &str = "0.1.0";

// Function whose contract is handed to the writer: the caller guarantees the index is valid.
unsafe fn get_second_item(v: &[i32]) -> i32 {
    *v.get_unchecked(1)
}

fn observe(ledger: &mut Vec<(String, String, String)>, name: &str, class: &str, evidence: String) {
    ledger.push((name.to_string(), class.to_string(), evidence));
}

fn main() {
    let mut ledger: Vec<(String, String, String)> = Vec::new();
    let v = vec![10, 20, 30, 40];

    // --- the compiler still holds ---

    let generated = generated_fn(21);
    observe(
        &mut ledger,
        "macro-generated body",
        "still holds",
        format!("call result {}", generated),
    );

    let x = 1;
    let macro_value = uses_own_name!();
    observe(
        &mut ledger,
        "macro hygiene",
        "still holds",
        format!("outer x={} macro {}", x, macro_value),
    );

    let at_boundary = v.get(10);
    observe(
        &mut ledger,
        "slice bounds check",
        "still holds",
        format!("tenth item {:?}", at_boundary),
    );

    let pointer = v.as_ptr();
    let (raw_first, in_block) = unsafe {
        // Two operations in the same block: one is truly unchecked, the other is still checked.
        (*pointer, v.get(10))
    };
    observe(
        &mut ledger,
        "check inside an unsafe block",
        "still holds",
        format!("raw read {} but tenth item {:?}", raw_first, in_block),
    );

    // --- handed to the writer ---

    let raw = unsafe { *pointer.add(2) };
    observe(
        &mut ledger,
        "raw pointer read",
        "handed to the writer",
        format!("read {} safe equivalent {}", raw, v[2]),
    );

    let unchecked = unsafe { *v.get_unchecked(1) };
    observe(
        &mut ledger,
        "unchecked access",
        "handed to the writer",
        format!("read {} safe equivalent {}", unchecked, v[1]),
    );

    let contracted = unsafe { get_second_item(&v) };
    observe(
        &mut ledger,
        "handed-off contract function",
        "handed to the writer",
        format!("read {}", contracted),
    );

    // --- outside the language ---

    let absolute = unsafe { abs(-7) };
    observe(
        &mut ledger,
        "foreign function call",
        "outside the language",
        format!("result {}", absolute),
    );

    let text = CString::new("welcome").unwrap();
    let length = unsafe { strlen(text.as_ptr()) };
    observe(
        &mut ledger,
        "foreign length measurement",
        "outside the language",
        format!("length {} Rust equivalent {}", length, "welcome".len()),
    );

    // --- nobody checks it ---

    let doc_result = wrong_doc(3, 9);
    observe(
        &mut ledger,
        "doc comment accuracy",
        "nobody checks it",
        format!("doc says largest, result {}", doc_result),
    );

    observe(
        &mut ledger,
        "version string meaning",
        "nobody checks it",
        format!("written value {}", VERSION_STRING),
    );

    observe(
        &mut ledger,
        "body excluded from compilation",
        "nobody checks it",
        String::from("carries an undefined call and a type error, the program still compiled"),
    );

    // --- report ---

    println!("{:<34}{:<23}{}", "observation", "class", "evidence");
    for (name, class, evidence) in &ledger {
        println!("{:<34}{:<23}{}", name, class, evidence);
    }

    println!();
    for s in ["still holds", "handed to the writer", "outside the language", "nobody checks it"] {
        let count = ledger.iter().filter(|(_, k, _)| k == s).count();
        println!("{:<23}{}", s, count);
    }
    println!("total observations: {}", ledger.len());
}
```

```
observation                       class                  evidence
macro-generated body              still holds            call result 42
macro hygiene                     still holds            outer x=1 macro 99
slice bounds check                still holds            tenth item None
check inside an unsafe block      still holds            raw read 10 but tenth item None
raw pointer read                  handed to the writer   read 30 safe equivalent 30
unchecked access                  handed to the writer   read 20 safe equivalent 20
handed-off contract function      handed to the writer   read 20
foreign function call             outside the language   result 7
foreign length measurement        outside the language   length 7 Rust equivalent 7
doc comment accuracy              nobody checks it       doc says largest, result 3
version string meaning            nobody checks it       written value 0.1.0
body excluded from compilation    nobody checks it       carries an undefined call and a type error, the program still compiled

still holds            4
handed to the writer   3
outside the language   2
nobody checks it       3
total observations: 12
```

**MM2.** Four of the twelve observations fall into the still holds class, and two of those four
are the subject of this topic: the call to a generated body (`call result 42`) and a name inside
a macro not clashing with the caller's variable of the same name (`outer x=1 macro 99`). This
lesson runs the remaining ten once and sets them aside, deepening only these two.

## Type Checking the Generated Body

The generating macro's call in the core brings a function named `generated_fn` into existence
without a single hand-written `fn` line. That this function returns `42` when called as
`generated_fn(21)` shows that a body stands in the source, but it does not show how deep that
goes. The example below sets up the same generation once more and puts the generated function
inside a constraint the macro knows nothing about — a generic bound.

```rust
// body_generalization.rs — a macro-generated body is put inside a generic bound.
macro_rules! generate_body {
    ($name:ident) => {
        fn $name(x: i32) -> i32 {
            x * 3
        }
    };
}

generate_body!(triple);

fn apply<F: Fn(i32) -> i32>(func: F, value: i32) -> i32 {
    func(value)
}

fn main() {
    let result = apply(triple, 7);
    println!("triple inside the generic bound: {}", result);
}
```

```
triple inside the generic bound: 21
```

**MM3.** This call to the generating macro brought only a function named `triple` into being;
the macro itself never tested whether the function satisfies the `F: Fn(i32) -> i32` bound
inside `apply` — that test was carried out by the compiler itself, exactly as it would be for
an ordinary function, when the generated source was passed into `apply`. The generated body is
checked not only when it is called directly but also when it is put inside a generic bound: the
macro has no advance knowledge of which context it will be used in, but the source it produces
is subject to the same rules in every context.

This distinction looks small, but its consequence is large: the place where the generating
macro is defined has no knowledge of which generic bounds `triple` will enter — that bound is
written much later, in an entirely separate place, when `apply` is defined. The two still work
together consistently, because the only link between them is the generated source's
**signature**: `fn triple(x: i32) -> i32`. The macro is responsible for producing this
signature, not for where it will be used; everything else, exactly as with a hand-written
function, is worked out jointly by the caller and the compiler.

## Macro Hygiene

The hygiene macro's call in the core binds its own `x` inside itself and returns it; the
caller's own `x` (`1`) is unaffected by this call (`outer x=1 macro 99`). That result was shown
with a single call. The example below calls the same macro twice in the same scope, testing
whether it clashes with the outer name, with each other, or with neither.

```rust
// hygiene_second.rs — the same macro is called twice; its inner names clash neither with the outside nor with each other.
macro_rules! uses_own_name {
    () => {{
        let x = 99;
        x
    }};
}

fn main() {
    let x = 7;
    let first = uses_own_name!();
    let second = uses_own_name!() + 1;
    println!("outer x={} first={} second={}", x, first, second);
}
```

```
outer x=7 first=99 second=100
```

The outer `x` stays seven, both calls produce their own `99`, and the second call can use that
value in its own computation (`+ 1`) — the name inside the macro belongs to its own scope on
every expansion, clashing neither with the outer `x` nor with the previous expansion's `x`.
Hygiene here is not a rule that prevents a name clash; it is a mechanism in which every macro
expansion carries its own naming space: at the text level, the same word `x` appears three times
side by side, but all three are separate bindings as far as the compiler is concerned.

The practical payoff of this mechanism is this: whoever defines a macro can use as many helper
names as they want inside its body and never has to know which names the caller uses. Without
hygiene, it would be necessary to separately make sure that every helper name inside the macro
does not clash with names in the caller's scope — which would make the macro risky to use
anywhere the names cannot be predicted in advance. Rust's macro system closes this risk inside
the language itself, without writing a contract between the caller and whoever wrote the macro.

## The Limiting Measurement

The previous two sections showed that the source a macro produces is checked just like real
Rust source. This checking carries a boundary: the macro operates only at the syntax level and
has no idea what the piece it substitutes actually means. The same macro is called with two
separate types to show this boundary.

```rust
// larger_picker.rs — the same macro is called with two types; both pass type checking.
macro_rules! generate_larger_picker_body {
    ($name:ident, $t:ty) => {
        fn $name(a: $t, b: $t) -> $t {
            if a > b { a } else { b }
        }
    };
}

generate_larger_picker_body!(larger_int, i32);
generate_larger_picker_body!(larger_bool, bool);

fn main() {
    println!("larger_int(3, 9)={}", larger_int(3, 9));
    println!("larger_bool(false, true)={}", larger_bool(false, true));
}
```

```
larger_int(3, 9)=9
larger_bool(false, true)=true
```

No type bound is written in the macro itself; `$t:ty` only says "a type name will come here",
without asking which types make this operation meaningful. Picking the larger of two `i32`
values is an ordinary comparison; the same syntax is just as valid for `bool` — `false` counts
as smaller than `true`, so the second function returns `true`. The compiler accepts both for the
same reason: both are types that support the comparison operator. Neither the macro nor the
compiler can say which call actually means something to the caller and which one compiles only
because the type happens to fit — only whoever reads the source can make that distinction.

This boundary does not invalidate the checking shown in the previous two sections; it
**narrows** it. Whether the generated body satisfies a generic bound and whether the name
inside it clashes with the caller's are both precise questions, and the compiler answers both
of them precisely. But "does this operation make sense for this type" is an entirely different
question, and the only tool the compiler has is type fitness: if a type supports the comparison
operator, it accepts that comparison without checking whether it matches the caller's intent.
For a tool that operates at the syntax level, this is not an unexpected limit but the definition
itself — a macro transforms one piece of text into another and has no say in what that text
means to whoever reads it.

## Summary

- The four classes do not change throughout the course: still holds, handed to the writer,
  outside the language, nobody checks it; the distribution of the twelve observations is 4/3/2/3.
- This lesson deepened the two observations that fall into the still holds class: the body a
  macro generates is checked like ordinary source even inside a generic bound, and the name
  inside a macro carries its own scope on every expansion, never clashing with the caller's.
- Checking the generated body is not limited to a direct call: a generated function is tested
  by the same rules when it is put inside a trait bound as well.
- A macro operates at the syntax level: when the same macro is called with two types that are
  semantically far apart, both compile, because the compiler only checks whether the types fit.

## Next Step

This lesson showed that generation happens within the same compilation step — the same file:
the macro definition and the macro's call could stand together, and the compiler could do the
defining, the expanding, and the type checking all in a single pass. The next lesson measures
the same still holds class from an entirely different setup: generation no longer happens in
the same file but in a separate compilation step, and the consuming source can only see the
generated source once the compiler is invoked a second time.
