@macroforge/mcp-server
Version:
MCP server for Macroforge documentation and code analysis
85 lines (66 loc) • 3.68 kB
Markdown
# Declarative Macros
Declarative macros let you define build-time code transformations entirely in TypeScript. They use
pattern-matching syntax inspired by Rust's `macro_rules!`, adapted to TypeScript's grammar. No Rust
knowledge and no native build step are required — you define the macro in the same `.ts` file where
you use it.
## Quick Example
```typescript
import { macroRules } from '@macroforge/core/rules';
const $vec = macroRules`
() => []
($($x:Expr),+) => {
const __v: unknown[] = [];
$( __v.push($x); )+
__v
}
`;
// Usage — expanded at build time:
const empty = $vec(); // → []
const xs = $vec(1, 2, 3);
```
The `$vec` definition is erased from the output, and every `$vec(...)` call site is replaced with
the matched arm's expansion. Here `xs` becomes:
```typescript
const xs = (() => {
const __v$1 = [];
__v$1.push(1);
__v$1.push(2);
__v$1.push(3);
return __v$1;
})();
```
Two things are happening in that output that are worth understanding early:
- The block body is wrapped in an **IIFE** because the call sits in expression position, and the
trailing expression gets an injected `return`. See
[Expansion & Hygiene](/docs/declarative-macros/expansion).
- `__v` was renamed to `__v$1` by **hygiene**, so the macro can't collide with a `__v` at the call
site.
## When to use declarative macros
| Use a declarative macro when… | Use a [Rust macro](/docs/custom-macros) when… |
| -------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| The transformation is syntactic — you can express it as pattern → template | You need to inspect resolved types, fields, or the type registry |
| You want it to live next to the code that uses it | You want to publish reusable macros as a package |
| You don't want a Rust toolchain in the build | You need to derive from a type's structure (`(...)`) |
Declarative macros run as a **pre-pass**, before derive and attribute macros. The full order is:
declarative expansion → attribute macros → derive macros → codegen. That means a declarative macro
can expand _into_ a class carrying `(...)`, and the derive will still see it.
## The `$` prefix is required
A macro binding's name **must** start with `$`. This is not a convention — discovery skips any
binding that doesn't start with `$`, so `const vec = macroRules\`…\`` is simply not registered as a
macro.
The `$` prefix also marks a reserved namespace: a `$`-prefixed call whose callee isn't a declarative
macro is dispatched to the [proc-macro fallback](/docs/declarative-macros/cross-file) instead.
## Section contents
- **[Defining Macros](/docs/declarative-macros/defining)** — tag form, object form, erasure, scoping
and shadowing
- **[Patterns & Fragments](/docs/declarative-macros/patterns)** — all 11 fragment kinds and what
each accepts
- **[Repetitions & Backtracking](/docs/declarative-macros/repetitions)** — `$( … )sep*` on both
sides of the arrow
- **[Expansion & Hygiene](/docs/declarative-macros/expansion)** — contexts, IIFE wrapping, `__`
renaming, composition
- **[Type-Position Macros](/docs/declarative-macros/type-macros)** — `$Macro<T>` in type position
- **[Reverse Monomorphization](/docs/declarative-macros/sharing)** — sharing a runtime helper
instead of inlining
- **[Cross-File Macros & Diagnostics](/docs/declarative-macros/cross-file)** — importing macros, and
every error you can hit