Skip to main content

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}