texform_transform/rewrite/rule.rs
1//! Rule trait and metadata for the rewrite phase.
2//!
3//! Every transform rule implements [`RewriteRule`] and carries a static
4//! [`RuleMeta`] descriptor that the engine uses for scheduling, dependency
5//! analysis, and convergence checking.
6//!
7//! Rules are organized along three axes:
8//!
9//! - **Package** ([`PackageName`]) — the owning package namespace (base, ams,
10//! physics).
11//! - **Normalization level** ([`NormalizationLevel`]) — the first transform
12//! profile that accepts the rule output as a suitable product.
13//! - **Fidelity** ([`RuleFidelity`]) — the rule's worst-case render-fidelity
14//! guarantee over its declared input domain.
15
16pub use texform_knowledge::builtin::PackageName;
17use texform_knowledge::specs::{
18 BuiltinCharacterRecord, BuiltinCommandRecord, BuiltinEnvironmentRecord,
19};
20
21use crate::ast::NodeId;
22use crate::rewrite::RuleError;
23use crate::rewrite::rule_context::RuleContext;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum NormalizationLevel {
27 /// Rules whose output is suitable for authoring-oriented normalization.
28 Standard,
29 /// Rules that expand compact or package-specific notation while preserving
30 /// the rendered formula.
31 Expand,
32 /// Rules that remove layout-only forms for corpus-oriented normalization.
33 Drop,
34 /// Rules whose output is only suitable as an equivalence-checking
35 /// intermediate, not as a corpus label.
36 Equiv,
37}
38
39impl NormalizationLevel {
40 pub const fn as_str(self) -> &'static str {
41 match self {
42 NormalizationLevel::Standard => "standard",
43 NormalizationLevel::Expand => "expand",
44 NormalizationLevel::Drop => "drop",
45 NormalizationLevel::Equiv => "equiv",
46 }
47 }
48
49 /// Lowest fidelity a rule at this level may declare.
50 ///
51 /// `level` and `fidelity` answer different questions. `level` determines
52 /// when profiles accept the rewrite output; `fidelity` is the render
53 /// guarantee used for contract validation. Do not infer one from the
54 /// other: an `Equiv` rule may still be `Full` when its output is
55 /// pixel-identical but too expanded to serve as a corpus label, as with
56 /// fenced matrix environment expansion.
57 pub const fn min_fidelity(self) -> RuleFidelity {
58 match self {
59 NormalizationLevel::Standard | NormalizationLevel::Expand => RuleFidelity::Approximate,
60 NormalizationLevel::Drop | NormalizationLevel::Equiv => RuleFidelity::Semantic,
61 }
62 }
63}
64
65/// How faithfully a rewrite preserves the input when re-rendered.
66///
67/// Ordered least-to-most faithful. The value is the rule's worst-case
68/// guarantee over its declared input domain. It drives contract validation,
69/// but it does not choose the image comparison algorithm.
70#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
71pub enum RuleFidelity {
72 /// Mathematical meaning is preserved, but rendering may change.
73 Semantic,
74 /// Rendering is visually equivalent apart from minor spacing or placement.
75 Approximate,
76 /// Rendering is pixel-identical before and after the rewrite.
77 Full,
78}
79
80/// Unique identifier for a rule, composed of its package and a human-readable name.
81///
82/// The `Display` impl produces the slash-separated form `"package/name"` which is
83/// used in diagnostics, builder filters, and rule-selection configuration.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
85pub struct RuleKey {
86 /// The package namespace this rule belongs to.
87 pub package: PackageName,
88 /// A short, unique name within the package.
89 pub name: &'static str,
90}
91
92impl std::fmt::Display for RuleKey {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 write!(f, "{}/{}", self.package.as_str(), self.name)
95 }
96}
97
98/// A specific command, environment, or character that a rule operates on,
99/// references, or produces.
100///
101/// Targets are used in [`RuleConsumes`] and [`RuleProduces`] to declare the
102/// knowledge-base entries a rule interacts with.
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum RuleTarget {
105 /// A builtin command record from `texform-knowledge`.
106 Command(&'static BuiltinCommandRecord),
107 /// A builtin environment record from `texform-knowledge`.
108 Environment(&'static BuiltinEnvironmentRecord),
109 /// A builtin character record from `texform-knowledge`.
110 Character(&'static BuiltinCharacterRecord),
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
114pub enum RuleTargetKind {
115 Command,
116 Environment,
117 Character,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
121pub struct RuleTargetKey {
122 pub kind: RuleTargetKind,
123 pub name: &'static str,
124}
125
126impl RuleTargetKey {
127 pub const fn kind_label(self) -> &'static str {
128 match self.kind {
129 RuleTargetKind::Command => "command",
130 RuleTargetKind::Environment => "environment",
131 RuleTargetKind::Character => "character",
132 }
133 }
134}
135
136impl RuleTarget {
137 pub const fn key(self) -> RuleTargetKey {
138 match self {
139 RuleTarget::Command(record) => RuleTargetKey {
140 kind: RuleTargetKind::Command,
141 name: record.name,
142 },
143 RuleTarget::Environment(record) => RuleTargetKey {
144 kind: RuleTargetKind::Environment,
145 name: record.name,
146 },
147 RuleTarget::Character(record) => RuleTargetKey {
148 kind: RuleTargetKind::Character,
149 name: record.name,
150 },
151 }
152 }
153
154 pub const fn kind_label(self) -> &'static str {
155 match self {
156 RuleTarget::Command(_) => "command",
157 RuleTarget::Environment(_) => "environment",
158 RuleTarget::Character(_) => "character",
159 }
160 }
161
162 pub const fn name(self) -> &'static str {
163 match self {
164 RuleTarget::Command(record) => record.name,
165 RuleTarget::Environment(record) => record.name,
166 RuleTarget::Character(record) => record.name,
167 }
168 }
169}
170
171impl From<RuleTarget> for RuleTargetKey {
172 fn from(value: RuleTarget) -> Self {
173 value.key()
174 }
175}
176
177/// Declares the commands, environments, or characters that a rule removes from,
178/// reads, or may otherwise touch in the AST.
179///
180/// The distinction matters for convergence analysis:
181/// - **`eliminates`** — forms the rule actively removes or replaces. After the
182/// rule fires these forms should no longer appear in the output AST.
183/// - **`touches`** — forms that the rule may read or modify without promising
184/// to eliminate them from the output AST.
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub struct RuleConsumes {
187 /// Forms that the rule removes or replaces in the AST.
188 pub eliminates: &'static [RuleTarget],
189 /// Forms that the rule may read or modify but does not eliminate.
190 pub touches: &'static [RuleTarget],
191}
192
193/// Declares the new forms that a rule may introduce into the AST.
194///
195/// The engine uses this to verify that every produced form is either in the
196/// acceptable command set or is consumed by another rule, ensuring convergence.
197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198pub struct RuleProduces {
199 /// Commands, environments, or characters that may appear in the AST after the rule fires.
200 pub targets: &'static [RuleTarget],
201}
202
203/// Static metadata bundle that fully describes a rule's identity, scheduling,
204/// and dependency contract.
205///
206/// The rewrite phase uses `triggers` and `consumes` to decide when to attempt a
207/// rule, `produces` to verify convergence, and `fidelity` for the rule's
208/// render-fidelity contract.
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub struct RuleMeta {
211 /// Unique identifier for this rule.
212 pub key: RuleKey,
213 /// Packages that make this rule loadable when any one of them is enabled.
214 pub enabled_by_packages: &'static [PackageName],
215 /// Ordered normalization level used by transform profiles.
216 pub level: NormalizationLevel,
217 /// One-line human-readable description of what the rule does.
218 pub summary: &'static str,
219 /// Worst-case render-fidelity guarantee over the rule's declared input domain.
220 pub fidelity: RuleFidelity,
221 /// Commands, environments, or characters that decide where the engine attempts this rule.
222 ///
223 /// Triggers must be non-empty. They only affect scheduling and do not
224 /// participate in eliminated-form contracts or dependency analysis.
225 pub triggers: &'static [RuleTarget],
226 /// Commands, environments, or characters this rule removes from, reads, or modifies in the AST.
227 pub consumes: RuleConsumes,
228 /// Commands, environments, or characters this rule may introduce into the AST.
229 pub produces: RuleProduces,
230}
231
232/// Result of attempting to apply a rule to a single node.
233///
234/// The engine uses this to decide whether the ApplyRules loop made progress
235/// in the current iteration.
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237pub enum RuleEffect {
238 /// The rule matched and the AST was modified.
239 Applied,
240 /// The rule matched but the node did not require transformation.
241 Skipped,
242}
243
244/// The central trait that all rewrite rules implement.
245///
246/// Implementors provide static metadata via [`meta()`](RewriteRule::meta) and
247/// the actual tree-rewriting logic via [`apply()`](RewriteRule::apply). Rules
248/// are typically defined as unit structs with a `const` [`RuleMeta`] and
249/// registered in the builtin rule list under `rewrite/rules/mod.rs`.
250pub trait RewriteRule: Send + Sync {
251 /// Returns the static metadata descriptor for this rule.
252 fn meta(&self) -> &'static RuleMeta;
253
254 /// Attempts to transform the node identified by `node_id`.
255 ///
256 /// Returns [`RuleEffect::Applied`] if the AST was modified, or
257 /// [`RuleEffect::Skipped`] if the node did not need transformation.
258 fn apply(&self, cx: &mut RuleContext<'_>, node_id: NodeId) -> Result<RuleEffect, RuleError>;
259}
260
261impl std::fmt::Debug for dyn RewriteRule + '_ {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 f.debug_struct("RewriteRule")
264 .field("key", &self.meta().key)
265 .finish_non_exhaustive()
266 }
267}