Chapter 15 — Macros

A macro is ordinary Coil that runs while your program compiles and returns code. The compiler pastes that code into your program and checks it like code you wrote by hand. There are three kinds:

KindYou writeIt can
Function-style macroname!(…)expand into an expression, statements or declarations
Attribute macro#[name(…)] above a function, method, class or enumreplace or wrap that item
Derive#[derive(Name)] above a class or enumadd code generated from its fields or cases

The built-in #[derive(Show, Eq, …)] from chapter 4 are derives. This chapter writes one macro of each kind, then shows how to look at what they generate. Reach for a macro only when a function cannot do the job; the last section helps decide.

A provider module#

Macros live in a module of their own, here src/my_macros.hy, and other modules import them with use like any other item. (A module cannot use the macros it declares.) The provider imports the macro module, which describes the code a macro receives (Expr, FnDecl, TypeDecl) and returns (Code):

use macro::{Expr, FnDecl, TypeDecl, Code, lit, raw};
use io::sync::{println};

Function-style macros#

A function-style macro is declared with macro. Each parameter receives one argument of the call as code (Expr), and the macro returns Code:

/// `square!(e)`: `e * e`.
macro square(Expr e) -> Code {
    return quote expr { ${e} * ${e} };
}

quote expr { … } is a template: Coil source with holes, and ${e} pastes the argument in. Arguments stay whole, so square!(1 + 2) becomes (1 + 2) * (1 + 2), which is 9 and not 5. The word after quote says what the template holds: expr (one expression), stmts (statements) or items (declarations).

A macro called as a statement expands to statements. This one does something no function can: it puts the condition’s source text in its error message. cond.str() is that text, and lit(…) turns a string into a string literal:

/// `check!(cond)`: panics with the condition as written when it is false.
macro check(Expr cond) -> Code {
    return quote stmts {
        if !${cond} {
            panic "check failed: " + ${lit(cond.str())};
        }
    };
}

/// `swap!(a, b)`: exchanges two variables.
macro swap(Expr a, Expr b) -> Code {
    return quote stmts {
        let tmp = ${a};
        ${a} = ${b};
        ${b} = tmp;
    };
}

swap! declares a tmp of its own. Variables a template declares are renamed behind the scenes (to tmp__m), so they never clash with the caller’s variables, even one called tmp.

A last parameter of type Vec<Expr> takes any number of arguments, and $(parts)&&* pastes a list with && between the elements. The macro body is ordinary Coil, with loops, conditions and strings:

/// `all!(a, b, …)`: true when every argument is (`true` for none).
macro all(Vec<Expr> conds) -> Code {
    if conds.len() == 0 {
        return raw("true");
    }
    let parts: Vec<Code> = Vec::new();
    for c in conds {
        parts.push(raw(c.src()));
    }
    return quote expr { $(parts)&&* };
}

Called at the top level of a file, a macro produces declarations:

/// `id_type!(Name)`: a class wrapping one `int`, with `==` and `show`.
macro id_type(Expr name) -> Code {
    return quote items {
        #[derive(Eq, Show)]
        class ${name} {
            pub value: int,
        }
    };
}

Attribute macros#

An attribute macro is declared with attr. Its first parameter is the item it sits on: a FnDecl for a function, a TypeDecl for a class or enum. Further parameters are the attribute’s arguments (string, int or bool). The output replaces the item, so to keep the original, emit it again.

#[trace(label = "…")] prints a line before and after the function runs. It renames the original function and generates a wrapper with the original name:

/// Prints a line; generated code calls it as `my_macros::say`.
fn say(string text) {
    let _ = println(text);
}

/// Prints a line before the function runs and after it returns.
attr trace(FnDecl f, string label) -> Code {
    let inner = f.name.str() + "__traced";
    return quote items {
        ${raw(f.with_name(inner))}
        fn ${raw(f.signature(f.name.str()))} {
            say(${lit("[" + label + "] enter " + f.name.str())});
            let r = ${raw(f.call(inner))};
            say(${lit("[" + label + "] leave " + f.name.str())});
            return r;
        }
    };
}

f.with_name(inner) is the whole function under a new name, f.signature(name) its parameters and return type, and f.call(inner) a call passing the same arguments. The wrapper calls say, which the code using #[trace] never imports: generated code may use its provider’s own items.

On a class, t.source is the declaration as written. Emitting it keeps the class, and the rest of the output adds to it:

/// Keeps the class and adds `T::table()`.
attr table(TypeDecl t, string name) -> Code {
    return quote items {
        ${raw(t.source)}
        impl ${t.name} {
            pub static fn table() -> string {
                return ${lit(name)};
            }
        }
    };
}

Derives#

A derive is declared with derive. It takes one TypeDecl and returns code to add after the type, which stays as written. Derives generate code from a type’s shape: t.fields() lists a class’s fields, and t.variants() an enum’s cases.

A derive can also own field attributes. attrs(column) lets users write #[column(…)] on a field, and f.attr_str(…) reads it:

/// `T::columns()`: the field names; `#[column(name = "…")]` renames one.
derive Columns(TypeDecl t) -> Code attrs(column) {
    let names: Vec<Code> = Vec::new();
    for f in t.fields() {
        names.push(lit(f.attr_str("column", "name", f.name.str())));
    }
    return quote items {
        impl ${t.name} {
            pub static fn columns() -> Vec<string> {
                let out: Vec<string> = Vec::from([$(names),*]);
                return out;
            }
        }
    };
}

Most derives implement a trait. The trait can live next to its derive under the same name:

/// Implemented by `#[derive(Describe)]`.
trait Describe<T> {
    fn describe(T value) -> string {}
}

/// `impl Describe for T`: `Name(field=value, …)`.
derive Describe(TypeDecl t) -> Code {
    let parts: Vec<Code> = Vec::new();
    let sep = "";
    for f in t.fields() {
        parts.push(quote expr { ${lit(sep + f.name.str() + "=")} + show(value.${f.name}) });
        sep = ", ";
    }
    return quote items {
        impl Describe for ${t.name} {
            pub fn describe(${t.name} value) -> string {
                return ${lit(t.name.str() + "(")} + $(parts)+* + ")";
            }
        }
    };
}

A macro sees types as written, not their meaning, so Describe cannot ask whether a field is printable. It emits show(…) anyway; a field without Show is reported at the #[derive] line.

Using them#

use io::sync::{println};
use string::{format};
use my_macros::{square, check, swap, all, id_type, trace, table, Columns, Describe};

id_type!(UserId);

#[trace(label = "math")]
fn add(int a, int b) -> int {
    return a + b;
}

#[table(name = "accounts")]
class Account {
    pub id: int,
}

#[derive(Columns, Describe)]
class User {
    pub id: int,
    #[column(name = "user_name")]
    pub name: string,
    pub active: bool,
}

fn main() {
    let x = square!(1 + 2);
    check!(x == 9);

    let a = 1;
    let tmp = 2;
    swap!(a, tmp);                    // the macro's own `tmp` does not clash

    let ok = all!(x > 0, a == 2, tmp == 1);
    let same = new UserId(7) == new UserId(7);
    println(format("%i %i %i %z %z", x, a, tmp, ok, same))?;

    let n = add(2, 3);
    let cols = User::columns();
    println(format("%i %s %s", n, Account::table(), cols[1]))?;
    println(new User(1, "ada", true).describe())?;
}

Output:

9 2 1 true true
[math] enter add
[math] leave add
5 accounts user_name
User(id=1, name=ada, active=true)

Changing the check to check!(x < 5) stops the program with panic: check failed: x < 5. A field attribute that no derive on the type owns is an error, so a typo such as #[colum(…)] is caught.

Seeing the generated code#

coil dissect --expand FILE prints a file with every macro expanded:

coil dissect --expand --root ~/.coil/stdlib/src src/main.hy
fn add__traced(int a, int b) -> int {
    return a + b;
}

fn add(int a, int b) -> int {
    my_macros::say("[math] enter add");
    let r__m = add__traced(a, b);
    my_macros::say("[math] leave add");
    return r__m;
}

In an editor, the language server offers Expand macros in this file on attribute and name!(…) lines.

Generated code is checked like your own, and errors in it point at the place the macro was used:

Error: [E0102] cannot apply `*` to operands of type `string`
 3 │     let s = square!("a");

A macro has no standard output; to see what it received, panic with it: panic "got: " + e.str();. Macros run with no access to files, network or environment unless the build grants it, and with a step budget, so a runaway loop fails the build instead of hanging it. See compile-time code for those grants and for comptime blocks, which run code at compile time without a macro.

Which one to use#

You want toUse
Generate code from a type’s fields or cases (trait implementations, serializers)a derive
Let users tune that per field (#[column(name = …)])a derive with attrs(…)
Wrap or change a function or type (logging, timing, registration)an attribute macro
Use an expression’s source text, declare variables at the call site, or take any number of argumentsa function-style macro
Compute a value from valuesa plain function

Two cautions. A macro’s arguments are code, so square!(next_id()) calls next_id twice; bind an argument with let in the output if it may have side effects. And an attribute macro that forgets to emit t.source or the renamed function deletes the item it was meant to decorate.

Exercises#

  1. Write check_eq!(a, b) that evaluates each side once and panics with both sides as written and their values, such as check failed: x + 1 == 3 (2 vs 3).
  2. Write #[derive(FieldCount)] adding static fn field_count() -> int. lit_int turns an int into code.
  3. Make Columns leave out fields marked #[column(skip = true)].

The macros reference lists every declaration form, template hole and macro module function.

Next: Chapter 16 — Calling C.

↑ ↓ navigate ↵ open