← Back to AILP Home

Macro Hygiene

Nitpick macros are hygienic: variables introduced inside a macro body do not conflict with variables at the call site, and call-site variables do not accidentally shadow macro-internal names.

The Problem Without Hygiene

Without hygiene, a macro that declares a temporary variable could clash with a variable of the same name at the call site:

// hypothetical unhygienic macro (NOT what Nitpick does)
macro:swap_bad = (a, b) {
    int32:tmp = a;   // if caller also has 'tmp', this would clash
    a = b;
    b = tmp;
};

How Nitpick Handles It

The compiler performs AST cloning with gensym before substituting the macro body at each call site. Each internal binding gets a fresh unique name (e.g., tmp__macro_tmp_42). This happens automatically — you do not need to do anything special.

macro:swap = (a, b) {
    int32:tmp = a;   // internally renamed to a fresh name at each call site
    a = b;
    b = tmp;
};

func:main = int32() {
    int32:x = 1i32;
    int32:y = 2i32;
    int32:tmp = 99i32;   // NOT affected by macro's internal 'tmp'
    swap!(x, y);
    println(`x=&{x} y=&{y} tmp=&{tmp}`);  // x=2 y=1 tmp=99
    exit(0);
};
func:failsafe = int32(tbb32:e) { exit(1); };

What Hygiene Covers

What Hygiene Does Not Cover

Inspecting Hygienic Expansion

Use --expand-macros to see the gensym-renamed variables:

$ npkc myfile.npk --expand-macros
// macro expansion at myfile.npk:10:5
// call:     swap!(Ident(x), Ident(y))
// expanded: Block([VarDecl(__macro_tmp_1, ...)])

See debug.md for full --expand-macros documentation.