--- title: "Nim Metaprogramming: Templates, Macros, Generics, and CTFE" description: "Verified reference for Nim's compile-time metaprogramming system: hygienic templates and their call-by-name semantics, AST-level macros with NimNode and quote do:, generic constraints and the current stability status of concepts, compile-time function execution limits, and the when-vs-if distinction that trips up code generation." language: nim framework: null category: language_best_practices tags: - nim - metaprogramming - macros - templates - generics - ctfe - compile-time - concepts - anti-patterns keywords: - nim template vs macro difference - nim quote do nimnode macro example - nim template hygiene inject gensym - nim compile time function execution ctfe compileTime pragma - nim when statement vs if statement compile time - nim concept stability status experimental - nim template multiple evaluation side effect bug last_updated: "2026-07-07" difficulty: advanced version: "Nim >= 2.0 (ARC/ORC default), stable line 2.2" related: - ./README.md - ./best_practices.md - ./common_pitfalls.md - ./concurrency.md - ./testing.md - ./interop_ffi.md - ../../core/principles/clean_code.md search_priority: high status: published --- # Nim Metaprogramming: Templates, Macros, Generics, and CTFE ## Introduction / Overview Nim's compile-time metaprogramming system - `template`, `macro`, generics, `concept`s, and compile-time function execution (CTFE) - is one of the language's most powerful features and, simultaneously, one of the areas where generated code is most likely to be confidently wrong. The core source of confusion is a category error that is easy for a language model to make: code written inside a `macro` body executes **at compile time**, manipulating an abstract syntax tree, to *produce* code that will run later, at runtime. Conflating "what the macro body does" with "what the code it generates does" is the single most common structural mistake in generated Nim metaprogramming. A second, equally load-bearing distinction is that Nim `template`s do not evaluate their arguments eagerly before substitution - an argument expression is spliced, verbatim, into every place its parameter name appears in the template body, and is evaluated exactly as many times as it is referenced. This is closer to a C preprocessor macro's substitution model than to any language's normal function-call semantics, and it produces a well-documented, easy-to-miss multiple-evaluation bug class covered in this document's pitfalls section. This document assumes the ecosystem and best-practice context in [`README.md`](./README.md) and [`best_practices.md`](./best_practices.md), and the general pitfall catalogue in [`common_pitfalls.md`](./common_pitfalls.md); it focuses exclusively on the compile-time metaprogramming layer. **When to use this guidance**: You are writing, generating, or reviewing Nim `template`s, `macro`s, generic procs/types, `concept`s, or any code relying on `when`/`static`/`{.compileTime.}` compile-time evaluation. ## Core Concepts - **`template`**: hygienic, compile-time **syntactic substitution** - the template body is spliced into the call site before type-checking, with no separate execution phase of its own. - **`macro`**: full **AST-level transformation** - the macro body is an ordinary (restricted) Nim proc that runs at compile time, receiving and/or returning `NimNode` values, and its *return value* is the AST that gets compiled and run later. - **Generics (`[T]`)**: compile-time type parameterization, resolved and instantiated once per concrete type used, optionally constrained by a type class (`SomeInteger`) or a `concept`. - **CTFE (Compile-Time Function Execution)**: the compiler's ability to execute a restricted subset of ordinary Nim code (not just macros) during compilation, powering `const` initializers and `static[T]` arguments. - **`when`**: a **compile-time** conditional - its untaken branch is not even fully semantically checked the way an `if`'s untaken runtime branch is; it looks like `if` and is trivially confused with it. - **Hygiene**: the mechanism (automatic symbol renaming, `gensym` by default, `{.inject.}` to opt out) that prevents a template's or macro's internal identifiers from accidentally capturing or clashing with identifiers at the call site. ## Best Practices ### 1. `template` - Hygienic Substitution, Not a Function Call **Rationale**: A `template` is expanded at its call site before type-checking, with its parameters substituted as expressions (or, for `untyped` parameters, as raw, unanalyzed AST) rather than evaluated once and bound like an ordinary function argument. This makes templates ideal for zero-overhead inlining, control-flow-shaped DSL sugar (a custom `unless`, a scoped resource-cleanup block), and injecting boilerplate - but it means a template does not behave like a function with respect to argument evaluation. ```nim template unless(cond: bool, body: untyped): untyped = if not cond: body let n = 5 unless(n > 10): echo "n is not greater than 10" ``` `untyped` parameters (as `body` above) receive the argument as an unanalyzed syntax tree - no type-checking is performed on them before substitution, which is exactly what allows a bare code block (not a valid standalone expression) to be passed as `body`. A parameter declared with a concrete type instead (e.g., `cond: bool` above) is type-checked at the call site before substitution, but - critically - it is still *substituted as an expression*, not evaluated once into a temporary, and can therefore still be evaluated multiple times if the template body references it more than once (see the Common Pitfalls section). **Why this works well in production**: `template` gives you macro-free, zero-runtime-overhead code reuse for control-flow-shaped patterns that a plain proc cannot express (you cannot pass "a block of statements" to an ordinary proc parameter), while remaining far easier to read, write, and debug than a full AST-manipulating `macro`. ### 2. `macro` - Compile-Time AST Transformation **Rationale**: A `macro` receives its arguments as `NimNode` AST values (for `untyped`/`typed` parameters) and must return a `NimNode` - the AST that will actually be compiled and executed at runtime. The macro body itself runs once, at compile time, for each call site; it is not present in, and has no direct relationship to, the runtime behavior of the program beyond the AST it emits. ```nim import std/macros macro double(x: untyped): untyped = result = quote do: `x` + `x` let a = 5 echo double(a) # expands, at compile time, to `echo a + a` -> prints 10 ``` `quote do:` is Nim's quasi-quotation construct: it takes an ordinary block of concrete Nim syntax and returns the `NimNode` AST that represents it, letting you build AST by writing normal-looking Nim code instead of manually constructing `newTree`/`newCall`/`newIdentNode` calls. Backtick-prefixed identifiers inside a `quote do:` block (`` `x` `` above) are interpolation points - they splice in an existing `NimNode` value (here, the macro's own `x` parameter) rather than being treated as a literal identifier named `x`. The same macro can be built without `quote do:`, directly manipulating the AST - more verbose, but useful when the shape of the generated code is itself dynamic and doesn't fit a single fixed `quote do:` template: ```nim import std/macros macro genRangeEcho(a, b: static[int]): untyped = result = newStmtList() for i in a..b: result.add newCall(ident("echo"), newIntLitNode(i)) genRangeEcho(1, 3) # expands, at compile time, into three separate `echo` calls ``` **The classic confusion this best practice exists to prevent**: the `for i in a..b` loop above executes **at compile time**, once, to build the AST - it is not a runtime loop, and it does not appear as a loop in the compiled output at all; the compiled output contains three separate, already-unrolled `echo` calls. Code inside a macro's own body (ordinary `if`/`for`/`var` used to build up `result`) runs during compilation to *decide what code to generate*; it is a categorically different thing from the runtime behavior of the AST that macro eventually returns. **Why this works well in production**: macros are the only mechanism that can inspect and transform the actual syntax tree of their arguments (branch on whether an argument is a literal versus a call, rewrite a whole statement list, implement a DSL with custom parsing-adjacent behavior) - capabilities no amount of template substitution or generic instantiation can reach. ### 3. Generics and Constraints **Rationale**: `[T]` introduces an unconstrained type parameter; `[T: SomeInteger]` (or any user type-class/`concept`) constrains it. An unconstrained `T`'s body is checked once per instantiation against whatever operations are actually used - the compiler does not require every possible operation on `T` to be pre-declared the way a Rust trait bound or a C++ concept requires. ```nim proc largest[T: SomeNumber](a, b: T): T = if a > b: a else: b echo largest(3, 7) # T = int echo largest(3.5, 2.1) # T = float ``` **`concept`s** provide structural, duck-typed constraints - a type matches a concept if the expressions in the concept's body compile for that type, with no explicit `impl`-style declaration required: ```nim type Comparable = concept x, y (x < y) is bool proc largest[T: Comparable](a, b: T): T = if a < b: b else: a ``` **Verified current status**: concepts are documented in [`manual_experimental.html`](https://nim-lang.org/docs/manual_experimental.html) rather than the stable `manual.html`, and open compiler issues report overly permissive matching and occasional crashes on nontrivial concept bodies. Treat `concept`s as usable but genuinely less mature than ordinary generic constraints - verify behavior against the current compiler version before depending on a nontrivial `concept` in production code, and prefer a plain `SomeX`-style type-class bound wherever it suffices. See [`common_pitfalls.md`](./common_pitfalls.md#pitfall-11-generic-constraints-and-concepts--not-rust-traits-or-c-templates) for worked examples of concepts being mismatched against Rust-trait intuitions. ### 4. Compile-Time Function Execution (CTFE) **Rationale**: Nim can execute ordinary procs - not just macro bodies - during compilation, whenever a value is required in a compile-time context: a `const` initializer, a `static[T]` argument, or an expression a macro needs evaluated. The `{.compileTime.}` pragma marks a proc as intended specifically for compile-time use (typically helper procs called from within macros), and such a proc's variables persist across separate compile-time invocations within the same compilation. ```nim proc factorial(n: int): int {.compileTime.} = result = 1 for i in 2..n: result *= i const Fact10 = factorial(10) # computed entirely at compile time, baked into the binary ``` **Verified limitations**: the manual explicitly prohibits several constructs inside compile-time-executed code: **methods, closure iterators, the `cast` operator, reference (`ref`/`ptr`) types, and FFI** - including standard library wrappers that themselves rely on FFI or `cast` internally. The manual notes these restrictions "are likely to be lifted over time," but as of the current stable compiler line they remain hard limits - a `const` initializer or `static[T]` argument that transitively calls into networking, threading, or most FFI-backed stdlib code will fail to compile, not silently fall back to runtime evaluation. ```nim # Wrong: assuming any proc can run at compile time, including one using `cast`/FFI internally proc readMagicByte(): byte {.compileTime.} = var p: ptr byte = cast[ptr byte](someAddress) # compile error: `cast` is disallowed in CTFE p[] const MagicByte = readMagicByte() ``` ```nim # Correct: CTFE-safe code sticks to pure computation over ordinary value types proc computeChecksum(data: openArray[byte]): int {.compileTime.} = for b in data: result += int(b) const Checksum = computeChecksum([1'u8, 2, 3]) ``` ### 5. `when` vs `if` - Compile-Time Branching, Not Runtime Branching **Rationale**: `when` reads almost identically to `if`, but its condition is evaluated at compile time and its untaken branch is not semantically checked and compiled the way an `if`'s untaken runtime branch is - the untaken branch can reference symbols, types, or platform-specific code that would not even compile on the current target, and this is not an error, because that branch is discarded before full semantic analysis. ```nim when defined(windows): proc platformPath(): string = r"C:\data" else: proc platformPath(): string = "/data" ``` Both branches above define a proc with the same name, which would be an ordinary redeclaration error under `if` - it is legal under `when` precisely because only one branch is ever compiled at all. A common, verified idiom pairs `when` with the special `nimvm` symbol to select between a CTFE-safe code path and an ordinary runtime path within the same routine: ```nim proc getValue(): int = when nimvm: 100 # taken when this proc is evaluated at compile time (CTFE) else: computeAtRuntime() # taken during ordinary runtime execution ``` **The confusion this exists to prevent**: substituting `if` for `when` (or vice versa) when translating between "this should be resolved once, at compile time" and "this should be checked every time the program runs" changes both correctness and compileability. An `if defined(windows): ...` (using runtime `if` instead of `when`) still compiles both branches unconditionally on every platform - code referencing a Windows-only symbol inside a plain `if defined(windows):` will fail to compile on Linux, exactly the failure `when` exists to avoid. ### 6. Macro Hygiene: `gensym` and `{.inject.}` **Rationale**: By default, a symbol declared inside a template or macro's generated code is renamed (`gensym`ed) to a fresh, call-site-invisible name, preventing the template/macro's internal implementation details from accidentally capturing or clashing with an identifier the caller happens to also use. This is what makes it safe to write `let temp = ...` inside a template body without fear of colliding with a caller's own `temp` variable. When the caller genuinely needs to see and use a name the template introduces - the defining use case for resource-cleanup and scoped-block templates - that specific symbol must be explicitly marked `{.inject.}` to opt out of the default hiding. ```nim template withFile(path: string, mode: FileMode, body: untyped): untyped = let f {.inject.} = open(path, mode) # {.inject.}: deliberately visible to `body` try: body finally: close(f) withFile("out.txt", fmWrite): f.writeLine("hello") # `f` resolves here only because of the {.inject.} above ``` Without `{.inject.}`, `f` inside the template would be renamed to a hygienic, caller-invisible symbol, and the `body` block's reference to `f` would fail to resolve - a compile error that looks like a missing variable, not a hygiene issue, until this mechanism is understood. ### 7. Practical Patterns **Resource-cleanup template** (the idiomatic Nim equivalent of a Python context manager or a Rust RAII guard, built from Best Practice 1 and 6 together): shown above as `withFile`. The same shape generalizes to any acquire/use/release pattern - a lock, a database transaction, a timer: ```nim template withLock(l: var Lock, body: untyped): untyped = acquire(l) try: body finally: release(l) var counterLock: Lock initLock(counterLock) withLock(counterLock): inc sharedCounter ``` **Small DSL macro** (from Best Practice 2's `quote do:` pattern, extended to a slightly more realistic case - a macro that logs an expression's source text alongside its runtime value, a common "debug print" DSL that a plain template cannot express because it needs the argument's *unevaluated source text*, not just its value): ```nim import std/macros macro dump(x: untyped): untyped = let label = x.toStrLit result = quote do: echo `label`, " = ", `x` let total = 42 dump(total) # expands, at compile time, to: echo "total", " = ", total ``` `x.toStrLit` converts the macro parameter's AST back into a string literal `NimNode` representing its *source text* ("total"), which a template's simple substitution has no mechanism to produce - this is a genuine macro-only capability, and a correct illustration of when reaching for `macro` over `template` is actually necessary. ## Common Pitfalls & Anti-Patterns ### Pitfall: Assuming a Template Parameter Is Evaluated Once **Problem**: because template arguments are substituted as expressions rather than evaluated once into a bound temporary, a parameter referenced more than once in the template body causes the caller's expression to be evaluated once *per reference* - including any side effects it carries. This is the direct Nim analogue of the classic C preprocessor macro multiple-evaluation bug, and it affects both `untyped` and ordinarily typed template parameters equally, since the substitution mechanism itself does not distinguish them. ```nim # Wrong: `a` is referenced twice in the body, so a side-effecting argument runs twice var calls = 0 proc nextId(): int = inc calls calls template maxOf(a, b: untyped): untyped = (if a > b: a else: b) echo maxOf(nextId(), 10) # `nextId()` may be evaluated once for the comparison and # again for whichever branch is taken - `calls` ends up # incremented more than once, and the returned value can be # inconsistent with a single logical call to `nextId()` ``` **Recommended approach**: bind each parameter to a local `let` at the very top of the template body when it is referenced more than once, forcing single evaluation before any branching or repeated use: ```nim template maxOf(a, b: untyped): untyped = let x = a let y = b (if x > y: x else: y) echo maxOf(nextId(), 10) # `nextId()` is now evaluated exactly once ``` ### Pitfall: Forgetting `{.inject.}`, Then Concluding the Template Is Broken **Problem**: a template that declares a variable meant to be visible inside a caller-supplied `body` block, without marking it `{.inject.}`, produces a confusing "undeclared identifier" error at the *call site*, inside code the caller wrote - which reads like a caller mistake, not a hygiene omission in the template's own definition. **Recommended approach**: any identifier a template's caller is expected to reference from within a passed `body` (or from after the template call, if the template is meant to introduce a call-site-visible binding) must be explicitly marked `{.inject.}`, as shown in Best Practice 6. When reviewing a template that mysteriously fails to resolve an identifier at its call site, check the template's own declarations for a missing `{.inject.}` before assuming the caller's code is wrong. ### Pitfall: Treating a Macro's Own Body as Runtime Code **Problem**: generated code sometimes treats loops, conditionals, or I/O written directly inside a `macro` body as if they execute when the *generated* code runs, rather than once, at compile time, while the macro is building its `result` AST. This leads to macros that `echo` debug output at the wrong time (during compilation, flooding build logs, not during program execution) or that attempt runtime-only operations (genuine file/network I/O against the running program's environment) inside macro logic that Nim's CTFE restrictions do not actually permit in the first place. **Recommended approach**: remember that everything in a macro's own body - not spliced into a `quote do:` block or built as a `NimNode` and added to `result` - runs at compile time, once per call site, as part of *deciding what code to generate*; only the AST ultimately returned in `result` becomes the program's actual runtime behavior (Best Practice 2). ### Pitfall: Reaching for `concept` Where a Simple Type-Class Bound Would Do **Problem**: pattern-matching from Rust trait bounds or C++20 concepts, generated code reaches for a Nim `concept` by default for constraints that a plain `[T: SomeInteger]`-style bound already expresses correctly and more stably. **Recommended approach**: default to built-in type classes (`SomeInteger`, `SomeFloat`, `SomeNumber`, `SomeOrdinal`) or a simple custom type class for common numeric/ordinal constraints; reserve `concept`s for genuinely structural, duck-typed requirements that cannot be expressed any other way, and verify nontrivial concept bodies against the current compiler before shipping them (Best Practice 3). ### Pitfall: Confusing `when` and `if` for Platform or Feature Branching **Problem**: using runtime `if defined(...)` instead of compile-time `when defined(...)` forces the compiler to fully check both branches on every platform, causing a build failure on any platform where the untaken branch references a symbol that doesn't exist there - a failure mode that only manifests when someone actually builds on the "other" platform, often long after the code was written and merged. **Recommended approach**: any branch selecting between platform-specific code, feature flags (`defined(ssl)`, `defined(release)`), or CTFE-vs-runtime code paths (`nimvm`) must use `when`, not `if` (Best Practice 5). ## Testing Strategies - **`expandMacros`**: wrap a call site in the `expandMacros:` block (or use the `--expandMacro` compiler switch) to print the fully expanded code a macro or template produces, making multiple-evaluation and hygiene bugs directly visible rather than inferred from confusing downstream errors. - **Unit-test the generated behavior, not the macro's internals**: treat a `macro`/`template` as a black box from the test's perspective - assert on the runtime behavior of code that uses it, exactly as you would for a hand-written equivalent, since the AST it produces is an implementation detail. - **Dedicated multiple-evaluation regression tests**: for any template whose parameter is referenced more than once in its body, add a test that passes an argument expression with an observable, countable side effect (as in this document's `maxOf`/`nextId` example) and assert the side effect fired exactly once. - **CTFE-path tests**: for any `{.compileTime.}` proc or `static[T]`-parameterized routine, add a `const` binding that forces compile-time evaluation, so a CTFE-limitation regression (Best Practice 4) is caught at compile time, not discovered at first use. - See [`testing.md`](./testing.md) for the full `std/unittest`/`testament` setup this pairs with. ## Performance, Security & Scalability Considerations - **Templates have zero runtime call overhead** (true inlining, since the body is spliced in before compilation) - prefer them over an equivalent `proc` on hot paths where the call overhead genuinely matters, but only once correctness (particularly the multiple-evaluation pitfall above) is verified. - **Macros can generate code bloat** if used to unroll large, data-driven loops at compile time (as in the `genRangeEcho` example) - weigh compile-time code generation against binary size and compile-time cost for large `a..b` ranges or similarly data-scaled expansions. - **CTFE moves computation from runtime to compile time**, which is a strict security and performance win for genuinely constant inputs (precomputed lookup tables, checksums of embedded data) but is not a substitute for runtime validation of anything that is actually only known at runtime - do not attempt to force user- or environment-supplied values through a `{.compileTime.}` proc. - **Concept-based generic constraints (Best Practice 3) carry the same maturity caveat under load as anywhere else** - a concept that compiles today is not guaranteed to compile identically after a compiler upgrade, given its documented, verified instability; pin the Nim compiler version for any codebase depending on nontrivial concepts. ## Edge Cases & Advanced Usage - **`bindSym`**: inside a macro, `bindSym("someIdent")` resolves an identifier from the macro's own definition scope at macro-expansion time, rather than the call site's scope - necessary when a macro needs to reference one of its own helper symbols regardless of what identifiers happen to be in scope at each call site. - **`getAst`**: obtains the AST a `template` would expand to, without actually expanding/splicing it inline - useful inside a `macro` that wants to programmatically combine a template's expansion with additional generated code. - **Recursive macros**: a macro can call itself (or another macro) during its own compile-time execution to build up AST recursively (e.g., unrolling a nested data structure) - but each recursive call still executes fully at compile time, and unbounded recursion here is a compile-time infinite loop, not a runtime one, and will hang or exhaust the compiler rather than the running program. - **Typed vs untyped macro/template parameters at scale**: a `typed` parameter forces full semantic analysis (type-checking, overload resolution) of the caller's expression before the macro/template ever sees it, which is required if the macro body needs to inspect the resolved type - but it also means the caller's code must already be valid, standalone, type-checkable Nim, unlike `untyped`, which accepts fragments (like a bare code block) that would not type-check in isolation. ## When *Not* to Use / Alternatives | Mechanism | Best for | Avoid when | |---|---|---| | `template` | Zero-overhead inlining, control-flow-shaped sugar, simple boilerplate elimination | The body needs to inspect or transform the *shape* of its arguments (branch on whether something is a literal, a call, an identifier) | | `macro` | Genuine AST transformation, source-text-aware DSLs, code generation from a compact spec | A `template` or a plain generic `proc` already expresses the same behavior - a macro's added power costs debuggability and readability | | Generic `proc` with a type-class bound | The overwhelming majority of "work over several types" needs | The constraint is genuinely structural/duck-typed and no built-in type class fits | | `concept` | Genuinely structural constraints with no other expression | Stability matters more than expressiveness for the constraint at hand (Pitfall above) - prefer a type-class bound | | CTFE / `{.compileTime.}` | Precomputing genuinely constant data (lookup tables, checksums of embedded literals) at zero runtime cost | The input is only known at runtime - this is not a mechanism for deferring runtime validation | **Choose a plain generic `proc`** over both `template` and `macro` by default - reach for `template` only once a proc genuinely cannot express the needed control-flow shape, and for `macro` only once a `template` genuinely cannot express the needed AST-level transformation. See [`best_practices.md`](./best_practices.md#3-proc-vs-func-vs-template-vs-macro-vs-iterator-vs-method) for the full six-way comparison across `proc`/`func`/`template`/`macro`/`iterator`/`method`. ## Related Documentation - [Nim Language Overview & Ecosystem](./README.md) - [Nim Best Practices](./best_practices.md) - [Nim Common Pitfalls & Anti-Patterns](./common_pitfalls.md) - [Nim Concurrency](./concurrency.md) - [Nim Testing](./testing.md) - [Nim FFI/Interop](./interop_ffi.md) - [Clean Code Principles](../../core/principles/clean_code.md) ## Glossary - **`NimNode`**: The compiler's AST node type; the value macros receive as arguments and must return as their result. - **`quote do:`**: A quasi-quotation construct that converts a block of concrete Nim syntax into the `NimNode` AST representing it, with backtick-prefixed identifiers marking interpolation points. - **Hygiene**: The default renaming (`gensym`) of symbols a template/macro introduces, preventing accidental capture of or clashes with call-site identifiers. - **`{.inject.}`**: A pragma that opts a template/macro-introduced symbol out of default hygiene, making it visible at the call site. - **CTFE**: Compile-Time Function Execution - the compiler's ability to run a restricted subset of ordinary Nim procs during compilation. - **`static[T]`**: A generic parameter kind requiring its argument to be known at compile time, enabling `when`-based branching on its value inside the routine body. - **`concept`**: Nim's structural/duck-typed generic constraint mechanism, documented in the experimental manual and less mature than ordinary generic constraints. - **`when`**: A compile-time conditional whose untaken branch is not fully semantically checked, distinct from the runtime `if`. --- **Maintenance note**: When updating this document, also update `last_updated` and re-verify the concept-stability status, CTFE restriction list, and `quote do:`/macro syntax against the current [Nim manual](https://nim-lang.org/docs/manual.html) and [`manual_experimental.html`](https://nim-lang.org/docs/manual_experimental.html).