Skip to main content

texform_transform/rewrite/
mod.rs

1//! AST rewrite phase: scheduling, rule application, and eliminated-form checks.
2
3mod contract;
4pub mod helpers;
5pub mod level_set;
6pub(crate) mod macro_support;
7mod macros;
8pub mod plan;
9mod registry;
10pub mod rule;
11pub mod rule_context;
12pub mod rules;
13mod scheduler;
14
15#[cfg(test)]
16#[allow(unused_imports)]
17pub(crate) use macros::transform_examples;
18#[allow(unused_imports)]
19pub(crate) use macros::{alias_rule, char_targets, cmd_targets, define_rule, env_targets};
20
21pub use contract::{ContractViolation, collect_eliminated_violations};
22pub use level_set::NormalizationLevelSet;
23pub use plan::{Plan, PlanBuildError, RuleAvailabilityFailure};
24pub use registry::all_rules;
25pub use rule::{
26    NormalizationLevel, PackageName, RewriteRule, RuleConsumes, RuleEffect, RuleFidelity, RuleKey,
27    RuleMeta, RuleProduces, RuleTarget, RuleTargetKey, RuleTargetKind,
28};
29pub use rule_context::{CommandView, DeclarativeView, EnvironmentView, InfixView, RuleContext};
30
31/// Accumulates statistics across an entire rewrite pass.
32#[derive(Clone, Debug, Default, PartialEq, Eq)]
33pub struct RewriteReport {
34    /// Per-rule execution counts for rules that were attempted at least once.
35    pub rules: Vec<RewriteRuleStat>,
36    /// The number of fixed-point iterations the Rewrite phase completed.
37    pub iterations: usize,
38}
39
40/// Tracks how often a specific rule changed the AST or skipped after a scheduling target match.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct RewriteRuleStat {
43    /// The identity of the rule.
44    pub key: RuleKey,
45    /// The total number of times this rule fired.
46    pub applied_count: usize,
47    /// The total number of times this rule's scheduling target matched but `apply()` returned `Skipped`.
48    pub skipped_count: usize,
49}
50
51impl RewriteReport {
52    pub(crate) fn stat_mut(&mut self, key: RuleKey) -> &mut RewriteRuleStat {
53        if let Some(index) = self.rules.iter().position(|entry| entry.key == key) {
54            return &mut self.rules[index];
55        }
56
57        self.rules.push(RewriteRuleStat {
58            key,
59            applied_count: 0,
60            skipped_count: 0,
61        });
62        self.rules
63            .last_mut()
64            .expect("newly inserted rule stat must exist")
65    }
66
67    pub fn mark_rule_applied(&mut self, key: RuleKey) {
68        self.stat_mut(key).applied_count += 1;
69    }
70
71    pub fn mark_rule_skipped(&mut self, key: RuleKey) {
72        self.stat_mut(key).skipped_count += 1;
73    }
74
75    pub fn record_iteration(&mut self, iterations: usize) {
76        self.iterations = iterations;
77    }
78}
79
80/// Top-level errors produced by the Rewrite phase.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub enum RewriteError {
83    /// An individual rule returned an error during application.
84    Rule { rule: RuleKey, kind: RuleError },
85    /// The output AST still contains a form that the rewrite contract requires to be eliminated.
86    ContractViolation {
87        target: RuleTargetKey,
88        node_name: Option<String>,
89    },
90    /// The Rewrite phase did not converge within the allowed iteration budget.
91    MaxIterationsExceeded { max_iterations: usize },
92}
93
94/// Errors reported by individual rules during application.
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub enum RuleError {
97    /// The rule encountered a node whose structure does not match its expectations.
98    InvalidNodeShape { message: String },
99    /// The rule requires knowledge-base metadata that is not present.
100    MissingMetadata { name: String },
101}
102
103impl std::fmt::Display for RewriteError {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        match self {
106            RewriteError::Rule { rule, kind } => match kind {
107                RuleError::InvalidNodeShape { message } => write!(f, "{rule}: {message}"),
108                RuleError::MissingMetadata { name } => {
109                    write!(f, "{rule}: missing metadata for {name}")
110                }
111            },
112            RewriteError::ContractViolation { target, node_name } => write!(
113                f,
114                "rewrite contract violated for {} `{}` (node {:?})",
115                target.kind_label(),
116                target.name,
117                node_name
118            ),
119            RewriteError::MaxIterationsExceeded { max_iterations } => {
120                write!(f, "rewrite exceeded max iterations: {max_iterations}")
121            }
122        }
123    }
124}
125
126impl std::error::Error for RewriteError {}
127
128use crate::ast::Ast;
129use crate::parse::ParseContext;
130
131/// Applies rewrite rules to an AST and records what changed.
132pub fn run(
133    ast: &mut Ast,
134    parse_ctx: &ParseContext,
135    plan: &Plan,
136    max_iterations: usize,
137    report: &mut RewriteReport,
138) -> Result<(), RewriteError> {
139    scheduler::drive_fixed_point(ast, parse_ctx, plan, max_iterations, report)
140}
141
142#[cfg(test)]
143pub(crate) fn run_one_rule_for_test(
144    ast: &mut Ast,
145    parse_ctx: &ParseContext,
146    rule: &'static dyn RewriteRule,
147    level: NormalizationLevel,
148) -> Result<crate::TransformReport, crate::TransformError> {
149    let build_config = crate::BuildConfig::profile(crate::Profile::Authoring)
150        .rewrite_levels(NormalizationLevelSet::from(level))
151        .only_rule_for_tests(rule.meta().key);
152    let context = crate::TransformContext::from_build_config(build_config, parse_ctx)
153        .map_err(crate::TransformError::Build)?;
154    context.run_with(
155        ast,
156        parse_ctx,
157        &crate::TransformConfig {
158            rewrite_enabled: true,
159            lower_attributes_enabled: false,
160            finalize_ast: crate::FinalizeAstConfig::DISABLED,
161            flatten_groups: crate::FlattenGroupsConfig::DISABLED,
162            max_iterations: 100,
163        },
164    )
165}