API Stability Notice

Macroforge is under active development. The API is not yet stable and may change between versions. Some documentation sections may be outdated.

Declarative Macros

Declarative macros let you define compile-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

import { macroRules } from "macroforge/rules";

const $vec = macroRules`
  () => []

  ($($x:Expr),+) => {
    const __v: unknown[] = [];
    $( __v.push($x); )+
    __v
  }
`;

// Usage — expanded at compile 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:

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.
  • __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 when…
The transformation is syntactic — you can express it as pattern → templateYou need to inspect resolved types, fields, or the type registry
You want it to live next to the code that uses itYou want to publish reusable macros as a package
You don’t want a Rust toolchain in the buildYou need to derive from a type’s structure (@derive(...))

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 @derive(...), 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 instead.

Section contents