Read this if you want to contribute a rewrite rule or audit why the transform engine produces a given output. For the user-facing view of profiles and the phase pipeline, start with Concepts: Transforms.
The rule metadata model#
Normalization is not find-and-replace. Each rewrite rule declares the metadata the engine uses to schedule rules, verify convergence, and enforce its output contract:
key— a stablepackage/nameidentifier used in transform reports and build-time filters.enabled_by_packages— the packages whose presence makes the rule loadable.level— the first profile that accepts the rule output:Authoring,Faithful,Corpus, orEquiv.summary— a one-line description.fidelity— the worst-case equivalence guarantee over the declared input domain:Math,Reading, orRender.triggers— the targets the scheduler watches.consumes— forms the rule eliminates or may touch.produces— forms the rule may introduce.
Level and fidelity are orthogonal#
A rule's level classifies the intended product, not the size of its visual change:
| Level | Intent |
|---|---|
Authoring | Author-editable canonical syntax that preserves reasonable notation and style choices. |
Faithful | Render-faithful universal forms for compact, package-specific, or legacy input. |
Corpus | Complete, stable canonical forms suitable as training labels; reading-identical layout and vocabulary variants may collapse. |
Equiv | Comparison-only intermediates that need not remain suitable as source or training labels. |
Fidelity is a separate worst-case guarantee ordered Math < Reading < Render. Render preserves rendering under the reference renderer, Reading preserves notation content, reading order, and structural roles while allowing layout changes, and Math preserves mathematical meaning over the declared domain while allowing notation and rendering to change. Authoring, Faithful, and Corpus require at least Reading; Equiv requires at least Math. A stronger fidelity does not automatically move a rule to an earlier level.
Because the levels are cumulative, the Equiv profile runs every Corpus rule and then the Equiv rules on top. Those rules drop distinctions that two spellings of the same mathematics do not need to agree on: \cfrac{a}{b} becomes \frac{a}{b}, \widehat collapses onto \hat, explicit \big / \Big delimiter sizing is dropped, and fixed horizontal spacing such as \quad disappears. That is exactly why they are comparison-only — \cfrac was chosen by an author for a reason, and an Equiv output is not a faithful replacement for the source.
Fixed-point scheduling#
Rewrite is one phase in a fixed sequence — pre-rewrite LowerAttributes, Rewrite, post-rewrite LowerAttributes, FinalizeAst, FlattenGroups, then FinalizeAst a second time. The second FinalizeAst pass is the same idempotent pass, re-run because flattening can bring nodes next to each other that were not adjacent before; it is skipped when FlattenGroups is disabled, since nothing new can become adjacent. FinalizeAst is the last phase that mutates the tree, and the eliminated-forms check that follows is read-only.
When a transform context is built, the engine filters all registered rules through the selected profile's levels, build-time filters, and enabled packages. At run time it repeatedly applies that precompiled plan until a full pass changes nothing or max_iterations is reached.
The transform report exposes this directly: rewrite.iterations is the fixed-point iteration count, including the final unchanged check, and rewrite.rules lists each recorded rule with its key, applied_count, and skipped_count. Plain normalize returns only the string. Request the report with normalize_with_report in Python or normalizeWithReport in JavaScript.
The eliminated-forms contract#
A rule that removes a form declares it in consumes.eliminates. After the complete pipeline has run, the engine checks that no declared eliminated form remains. A violation is a transform error, not a warning or a half-normalized result.
The texform-regression corpus harness runs the same contract over real-world data. Known, reasoned exceptions live in regression/contract_exceptions.yaml; a new unlisted violation means the declared contract and actual behavior no longer agree.
What callers can change#
The rule set is compiled into the engine, so there is no run-time registration hook: you cannot add a rule of your own without building TeXForm from source. What you can do from the outside is subtract. Pass disable_rules with the keys you want left out when you construct the engine — disable_rules=["physics/dv-to-frac-d"] in Python, disableRules in JavaScript — and the plan is compiled without them. Disable a rule walks through finding the key in a transform report and turning it off.
How to read rule source#
Rules live under src/rewrite/rules/ using <package>/<level>/<group>/<rule>.rs. The level directory is one of authoring, faithful, corpus, or equiv; the build script verifies it agrees with RuleMeta.level. Rule keys remain package/name regardless of the source path.
Most rules use define_rule!, which keeps metadata beside the rewrite logic:
define_rule! {
pub static OVER_TO_FRAC: OverToFracRule {
key: Base / "over-to-frac",
level: Authoring,
summary: "Rewrite infix over to an explicit frac command.",
fidelity: Render,
enabled_by_packages: [Base],
triggers: cmd_targets![&base::cmd::OVER],
consumes: RuleConsumes {
eliminates: cmd_targets![&base::cmd::OVER],
touches: &[],
},
produces: RuleProduces {
targets: cmd_targets![&base::cmd::FRAC],
},
apply(rule, cx, node_id) {
// match the \over infix and rebuild it as \frac{left}{right}
}
}
}This rule eliminates \over, produces \frac, first enters at Authoring, and guarantees Render fidelity. Other useful examples include alias rules such as physics/derivative-to-dv, which eliminates \derivative and produces \dv, and Corpus cleanup rules such as base/goodbreak-drop. The former displaylines-to-gather-env proposal was rejected and removed because it could not guarantee Reading over a reasonable declared domain.
Source map#
Paths are relative to the TeXForm repository root. They move as the code moves, so treat them as entry points rather than citations.
| Path | What it defines |
|---|---|
crates/texform-transform/src/engine.rs | The fixed phase order, including the second FinalizeAst pass and the eliminated-forms check. |
crates/texform-transform/README.md | Phase behavior, profile-to-level mapping, and the fidelity definitions. |
crates/texform-transform/src/rewrite/rules/README.md | The rule authoring guide and the metadata contract. |
crates/texform-transform/src/rewrite/rules/ | Every built-in rule, laid out as <package>/<level>/<group>/<rule>.rs. |
regression/contract_exceptions.yaml | The reasoned exceptions to the eliminated-forms contract over corpus data. |