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.

Cross-File Macros & Diagnostics

Importing macros

A macro defined in one file can be used in another via an import macro JSDoc comment:

/** import macro { $vec } from "./macros" */

const xs = $vec(1, 2, 3);

Specifiers resolve in this order: the bare path, then .ts, .tsx, index.ts, index.tsx.

The project registry requirement

Cross-file resolution is not automatic. The expander only resolves imported macros when a project-wide declarative registry has been installed, which means passing declarativeRegistryJson in ExpandOptions:

import { scanProjectSync, expandSync } from "macroforge";

const scan = scanProjectSync({ root: "./src" });

const result = expandSync(source, "src/app.ts", {
  declarativeRegistryJson: scan.declarativeRegistryJson,
});

The bundled integrations (Vite plugin, CLI, preprocessor) do this for you. If you call expandSync directly and omit it, relative import macro specifiers emit an error diagnostic per import and resolve nothing.

Bare package specifiers are skipped silently rather than erroring — they fall through to the proc-macro loader.

Proc-macro fallback

$-prefixed names are a reserved namespace. When a $name(...) call’s callee is not a declarative macro, it’s dispatched as a function-like procedural macro, with the module resolved from an import macro comment or from a plain import { $foo } from "pkg" statement. The same applies to $Name<...> in type position.

Practical consequence: don’t name ordinary functions with a leading $ in a file where macros are active.

Diagnostics

Discovery and parse errors (fatal)

MessageCause
${…} interpolation in templateThe macro body is a mini-language, not a JS template literal
Spread in object formmacroRules({ ...opts }) isn’t supported
Non-static keyObject-form keys must be literal identifiers/strings
Unknown option keyLists the accepted keys
Bad mode stringMust be one of the four modes or their camelCase aliases
Bad kindMust be "value" or "type"
megamorphismThreshold not a number / out of rangeMust be a numeric literal in 0–255
runtimeName missing $__cluster__The template needs the placeholder
Missing expandRequired in the object form
Sharing mode without runtime + callBoth are required to share
kind: "type" with a sharing mode, runtime, or callTypes have no runtime representation
Macro definition has no armsEmpty body
Unbalanced delimitersCheck parens/braces in the body
Arm does not start with (Arms begin with a parenthesized pattern
Expected => after patternMissing arrow
Expected :<Kind> / unknown fragment kindLists the known kinds
Expected *, +, or ? after repetitionA repetition needs a count suffix
Unterminated repetition / macro callMissing closing delimiter
Duplicate macro nameTwo definitions with identical or overlapping scopes

Match errors

MessageCause
NoArmMatchedNo arm matched. Lists every pattern tried, and notes that backtracking was attempted.
UnsupportedFragmentKindA fragment kind that can’t appear in this position — includes a per-kind explanation and suggested alternative.

Expansion errors

UnboundName, WrongBindingShape, InconsistentSequenceLength, UnanchoredRepetition, RecursionLimit (depth 256), UnknownMacroCall, MalformedMacroCallArgs, NestedMatchFailure.

Type-position errors

  • “macro $X is value-only; cannot use it in type position”
  • “no arm of type-position macro matched”

Analyzer diagnostics

LevelMessage
WarningCluster recommendation — call sites partitioned into N clusters
WarningForceExpand — too many distinct shapes; suggests share-anyway to override
WarningA call site matched no cluster; fell back to the single helper
InfoRunning without a type registry; structural clustering disabled
InfoAnalyzer telemetry, when enabled

Post-expansion validation

In dev builds only, the expander re-parses its own output and reports “macro $foo produced invalid TypeScript: …”, attributing the failure to the originating macro. This catches template bugs that would otherwise surface as a confusing syntax error in generated code.