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.
Reverse Monomorphization
Inlining a macro at every call site is fast but duplicates code. Reverse monomorphization does the opposite: emit the logic once as a runtime helper, and rewrite each call site into a call to it.
const $serialize = macroRules({
mode: "auto",
expand: macroRules`($x:Expr) => JSON.stringify($x)`,
runtime: "function __serialize(value) { return JSON.stringify(value); }",
call: macroRules`($x:Expr) => __serialize($x)`,
});expand is used when the macro inlines; runtime + call are used when it shares.
Modes
| Mode | Behavior |
|---|---|
expand-only | Always inline using expand. |
share-only | Always emit the runtime helper and rewrite calls, in dev and prod alike. |
share-anyway | Same as share-only, but documented as the deliberate escape hatch when the analyzer has warned you off sharing. |
auto | Let the megamorphism analyzer choose per macro. |
Two honest notes:
share-onlyandshare-anywayare currently behaviorally identical. Neither emits a megamorphism warning, because the analyzer only examinesautomacros.share-anywayis meaningful as intent — it’s the mode theautowarning tells you to switch to.- Sharing applies in dev too. The share modes are not prod-only; they emit the helper regardless of build mode.
The default mode is inferred, not fixed: object-form macros with both runtime and call default to auto, everything else to expand-only.
The megamorphism analyzer
In auto mode the analyzer inspects every call site of the macro and recommends one of:
- Share — call sites are homogeneous enough for one helper.
- Cluster — call sites fall into distinct groups; emit one helper per cluster.
- ForceExpand — too many distinct shapes; a shared helper would be megamorphic and slow, so inline instead. Emits a warning.
How shapes are extracted
This is the part that most affects whether auto works for you. For each argument:
| Argument | Shape |
|---|---|
User (capitalized identifier) | Named("User") + field fingerprint |
new User(...) | Named("User") + field fingerprint |
"a", 1, true, null, 1n, `t` | Literal("string" / "number" / …) |
user (lowercase identifier) | Opaque |
| anything else | Opaque |
Lowercase identifiers become Opaque because a local variable name doesn’t bound the type. This is the common case in real code — $serialize(user) contributes no type information. If you want the analyzer to cluster usefully, call it with type-bearing expressions, or accept that it will see opaque shapes and fall back accordingly.
How clustering works
- Call sites are bucketed by arity first.
- Within a bucket, a tuple joins a cluster when its mean pairwise Jaccard similarity across all argument positions is ≥ 0.60 against the cluster’s existing members.
- Field fingerprints come from the project type registry. Without it, the analyzer falls back to a name-prefix heuristic and emits an
Infodiagnostic saying structural clustering is disabled. - If a cluster’s distinct-fingerprint count exceeds
megamorphismThreshold, the macro is forced to expand inline.
Cluster ids appear in the generated helper names — derived from the member type names, or empty / lit / opaque for the degenerate buckets.
Requirements
Structural clustering needs typeRegistryJson passed in ExpandOptions (produced by a project scan). Without it clustering still runs, but on the weaker name-prefix heuristic.
runtimeName and clustering
When clustering emits more than one helper, each needs a distinct name. Two ways to get that:
- Provide
runtimeNamecontaining$__cluster__, which is substituted with the cluster id. Use the same placeholder in yourcallarms. - Omit it, and the rewriter auto-suffixes
__{cluster_id}onto the name it parses out of the firstfunction NAME(in yourruntimestring. This meansruntimemust contain afunction NAME(declaration for clustering to work.
The helper is inserted once near the top of the file and deduplicated per (macro, cluster). If a call site’s shape matches no cluster, it falls back to the single helper and emits a warning.