Skip to main content

texform_transform/rewrite/
plan.rs

1//! Compiled rewrite plan: filtered rules and eliminated forms.
2
3use std::collections::VecDeque;
4
5use crate::config::BuildConfig;
6use crate::parse::{MutationSummary, ParseContext};
7use crate::rewrite::registry;
8use crate::rewrite::rule::{
9    PackageName, RewriteRule, RuleKey, RuleTarget, RuleTargetKey, RuleTargetKind,
10};
11
12#[derive(Clone, Debug)]
13pub struct Plan {
14    rules: Vec<&'static dyn RewriteRule>,
15    eliminated_forms: Vec<RuleTargetKey>,
16}
17
18impl Plan {
19    pub fn build(config: &BuildConfig, parse_ctx: &ParseContext) -> Result<Self, PlanBuildError> {
20        let enabled = filter_rules(registry::all_rules(), config, parse_ctx)?;
21        let ordered = topological_sort(enabled.as_slice())?;
22        let eliminated_forms = derive_eliminated_forms(ordered.as_slice());
23        Ok(Self {
24            rules: ordered,
25            eliminated_forms,
26        })
27    }
28
29    pub fn rules(&self) -> &[&'static dyn RewriteRule] {
30        self.rules.as_slice()
31    }
32
33    pub fn eliminated_forms(&self) -> &[RuleTargetKey] {
34        self.eliminated_forms.as_slice()
35    }
36
37    #[cfg(test)]
38    pub(crate) fn from_rules_for_tests(rules: Vec<&'static dyn RewriteRule>) -> Self {
39        let eliminated_forms = derive_eliminated_forms(rules.as_slice());
40        Self {
41            rules,
42            eliminated_forms,
43        }
44    }
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub(crate) enum RuleSelection {
49    #[default]
50    All,
51    Only(Vec<RuleKey>),
52    Except(Vec<RuleKey>),
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub enum PlanBuildError {
57    SelectedRuleUnavailable {
58        rule: RuleKey,
59        reason: RuleAvailabilityFailure,
60    },
61    InvalidRuleMetadata {
62        rule: RuleKey,
63        message: &'static str,
64    },
65    DependencyCycle {
66        chain: Vec<RuleKey>,
67    },
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub enum RuleAvailabilityFailure {
72    DisabledByPackage {
73        required: Vec<PackageName>,
74        active: Vec<PackageName>,
75    },
76    ProducedTargetUnavailable {
77        target: RuleTargetKey,
78        active: Vec<PackageName>,
79    },
80}
81
82impl std::fmt::Display for PlanBuildError {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match self {
85            PlanBuildError::SelectedRuleUnavailable { rule, reason } => {
86                write!(f, "selected transform rule {rule} is unavailable: {reason}")
87            }
88            PlanBuildError::InvalidRuleMetadata { rule, message } => {
89                write!(f, "transform rule {rule} has invalid metadata: {message}")
90            }
91            PlanBuildError::DependencyCycle { chain } => {
92                let chain = chain
93                    .iter()
94                    .map(ToString::to_string)
95                    .collect::<Vec<_>>()
96                    .join(" -> ");
97                write!(f, "transform dependency cycle detected: {chain}")
98            }
99        }
100    }
101}
102
103impl std::fmt::Display for RuleAvailabilityFailure {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        match self {
106            RuleAvailabilityFailure::DisabledByPackage { required, active } => write!(
107                f,
108                "enabled_by_packages {:?} does not intersect active packages {:?}",
109                package_names_for_message(required.as_slice()),
110                package_names_for_message(active.as_slice())
111            ),
112            RuleAvailabilityFailure::ProducedTargetUnavailable { target, active } => write!(
113                f,
114                "produced {} `{}` is unavailable in active packages {:?}",
115                target.kind_label(),
116                target.name,
117                package_names_for_message(active.as_slice())
118            ),
119        }
120    }
121}
122
123impl std::error::Error for PlanBuildError {}
124
125fn package_names_for_message(packages: &[PackageName]) -> Vec<&'static str> {
126    packages.iter().map(|package| package.as_str()).collect()
127}
128
129fn filter_rules(
130    rules: &[&'static dyn RewriteRule],
131    config: &BuildConfig,
132    parse_ctx: &ParseContext,
133) -> Result<Vec<&'static dyn RewriteRule>, PlanBuildError> {
134    let mut enabled = Vec::new();
135
136    for rule in rules.iter().copied() {
137        let key = rule.meta().key;
138        let in_selection = match &config.selection {
139            RuleSelection::All => true,
140            RuleSelection::Only(keys) => keys.contains(&key),
141            RuleSelection::Except(keys) => !keys.contains(&key),
142        };
143        let explicitly_selected =
144            matches!(&config.selection, RuleSelection::Only(keys) if keys.contains(&key));
145
146        if !in_selection {
147            continue;
148        }
149        if !config.levels.contains(rule.meta().level) {
150            continue;
151        }
152        if rule_touched_by_mutations(rule, parse_ctx.mutation_summary()) {
153            continue;
154        }
155
156        validate_rule_metadata(rule)?;
157
158        if let Some(reason) = package_availability_failure(rule, parse_ctx) {
159            if explicitly_selected {
160                return Err(PlanBuildError::SelectedRuleUnavailable { rule: key, reason });
161            }
162            continue;
163        }
164
165        if let Some(reason) = produced_target_availability_failure(rule, parse_ctx) {
166            if explicitly_selected {
167                return Err(PlanBuildError::SelectedRuleUnavailable { rule: key, reason });
168            }
169            continue;
170        }
171
172        enabled.push(rule);
173    }
174
175    Ok(enabled)
176}
177
178fn validate_rule_metadata(rule: &'static dyn RewriteRule) -> Result<(), PlanBuildError> {
179    let meta = rule.meta();
180    if meta.triggers.is_empty() {
181        return Err(PlanBuildError::InvalidRuleMetadata {
182            rule: meta.key,
183            message: "triggers must be non-empty",
184        });
185    }
186    if meta.fidelity < meta.level.min_fidelity() {
187        return Err(PlanBuildError::InvalidRuleMetadata {
188            rule: meta.key,
189            message: "fidelity must not be below the normalization level floor",
190        });
191    }
192
193    let consumes = meta
194        .consumes
195        .eliminates
196        .iter()
197        .chain(meta.consumes.touches.iter())
198        .copied()
199        .map(RuleTarget::key)
200        .collect::<Vec<_>>();
201    if meta
202        .triggers
203        .iter()
204        .copied()
205        .map(RuleTarget::key)
206        .any(|trigger| !consumes.contains(&trigger))
207    {
208        return Err(PlanBuildError::InvalidRuleMetadata {
209            rule: meta.key,
210            message: "triggers must be a subset of consumes",
211        });
212    }
213
214    Ok(())
215}
216
217fn package_availability_failure(
218    rule: &'static dyn RewriteRule,
219    parse_ctx: &ParseContext,
220) -> Option<RuleAvailabilityFailure> {
221    let active = parse_ctx.enabled_packages();
222    if rule
223        .meta()
224        .enabled_by_packages
225        .iter()
226        .any(|package| active.contains(package))
227    {
228        return None;
229    }
230
231    Some(RuleAvailabilityFailure::DisabledByPackage {
232        required: rule.meta().enabled_by_packages.to_vec(),
233        active: active.to_vec(),
234    })
235}
236
237fn produced_target_availability_failure(
238    rule: &'static dyn RewriteRule,
239    parse_ctx: &ParseContext,
240) -> Option<RuleAvailabilityFailure> {
241    rule.meta()
242        .produces
243        .targets
244        .iter()
245        .copied()
246        .map(RuleTarget::key)
247        .find(|target| !parse_context_knows_target(parse_ctx, *target))
248        .map(
249            |target| RuleAvailabilityFailure::ProducedTargetUnavailable {
250                target,
251                active: parse_ctx.enabled_packages().to_vec(),
252            },
253        )
254}
255
256fn parse_context_knows_target(parse_ctx: &ParseContext, target: RuleTargetKey) -> bool {
257    match target.kind {
258        RuleTargetKind::Command => parse_ctx.knows_command_name(target.name),
259        RuleTargetKind::Environment => parse_ctx.knows_env_name(target.name),
260        RuleTargetKind::Character => parse_ctx.knows_character_name(target.name),
261    }
262}
263
264fn rule_touched_by_mutations(rule: &'static dyn RewriteRule, summary: &MutationSummary) -> bool {
265    rule.meta()
266        .consumes
267        .eliminates
268        .iter()
269        .chain(rule.meta().consumes.touches.iter())
270        .chain(rule.meta().produces.targets.iter())
271        .copied()
272        .map(RuleTarget::key)
273        .any(|target| match target.kind {
274            RuleTargetKind::Command | RuleTargetKind::Character => {
275                summary.touched_commands.contains(target.name)
276            }
277            RuleTargetKind::Environment => summary.touched_environments.contains(target.name),
278        })
279}
280
281fn derive_eliminated_forms(rules: &[&'static dyn RewriteRule]) -> Vec<RuleTargetKey> {
282    let mut forms = Vec::new();
283    for rule in rules {
284        for target in rule
285            .meta()
286            .consumes
287            .eliminates
288            .iter()
289            .copied()
290            .map(RuleTarget::key)
291        {
292            if !forms.contains(&target) {
293                forms.push(target);
294            }
295        }
296    }
297    forms
298}
299
300fn topological_sort(
301    rules: &[&'static dyn RewriteRule],
302) -> Result<Vec<&'static dyn RewriteRule>, PlanBuildError> {
303    let mut incoming = vec![0usize; rules.len()];
304    let mut edges = vec![Vec::<usize>::new(); rules.len()];
305
306    for (from_index, from_rule) in rules.iter().enumerate() {
307        for (to_index, to_rule) in rules.iter().enumerate() {
308            if from_index == to_index {
309                continue;
310            }
311            if rules_intersect(*from_rule, *to_rule) {
312                edges[from_index].push(to_index);
313                incoming[to_index] += 1;
314            }
315        }
316    }
317
318    let mut queue = VecDeque::new();
319    for (index, &count) in incoming.iter().enumerate() {
320        if count == 0 {
321            queue.push_back(index);
322        }
323    }
324
325    let mut ordered = Vec::with_capacity(rules.len());
326    while let Some(index) = queue.pop_front() {
327        ordered.push(rules[index]);
328        for next in &edges[index] {
329            incoming[*next] -= 1;
330            if incoming[*next] == 0 {
331                queue.push_back(*next);
332            }
333        }
334    }
335
336    if ordered.len() == rules.len() {
337        return Ok(ordered);
338    }
339
340    Err(PlanBuildError::DependencyCycle {
341        chain: detect_cycle(rules, edges.as_slice()),
342    })
343}
344
345fn rules_intersect(from_rule: &'static dyn RewriteRule, to_rule: &'static dyn RewriteRule) -> bool {
346    from_rule
347        .meta()
348        .produces
349        .targets
350        .iter()
351        .copied()
352        .map(RuleTarget::key)
353        .any(|produced| {
354            to_rule
355                .meta()
356                .consumes
357                .eliminates
358                .iter()
359                .chain(to_rule.meta().consumes.touches.iter())
360                .copied()
361                .map(RuleTarget::key)
362                .any(|consumed| consumed == produced)
363        })
364}
365
366fn detect_cycle(rules: &[&'static dyn RewriteRule], edges: &[Vec<usize>]) -> Vec<RuleKey> {
367    let mut stack = Vec::new();
368    let mut state = vec![0u8; rules.len()];
369
370    for index in 0..rules.len() {
371        if let Some(chain) = visit_cycle(index, rules, edges, &mut state, &mut stack) {
372            return chain;
373        }
374    }
375
376    rules.iter().map(|rule| rule.meta().key).collect()
377}
378
379fn visit_cycle(
380    index: usize,
381    rules: &[&'static dyn RewriteRule],
382    edges: &[Vec<usize>],
383    state: &mut [u8],
384    stack: &mut Vec<usize>,
385) -> Option<Vec<RuleKey>> {
386    if state[index] == 1 {
387        let cycle_start = stack.iter().position(|node| *node == index).unwrap_or(0);
388        let mut chain = stack[cycle_start..]
389            .iter()
390            .map(|node| rules[*node].meta().key)
391            .collect::<Vec<_>>();
392        chain.push(rules[index].meta().key);
393        return Some(chain);
394    }
395
396    if state[index] == 2 {
397        return None;
398    }
399
400    state[index] = 1;
401    stack.push(index);
402    for &next in &edges[index] {
403        if let Some(chain) = visit_cycle(next, rules, edges, state, stack) {
404            return Some(chain);
405        }
406    }
407    stack.pop();
408    state[index] = 2;
409    None
410}
411
412#[cfg(test)]
413mod tests {
414    use texform_knowledge::builtin::base;
415
416    use super::*;
417    use crate::ast::NodeId;
418    use crate::rewrite::rule::{
419        NormalizationLevel, RuleConsumes, RuleEffect, RuleFidelity, RuleMeta, RuleProduces,
420    };
421    use crate::rewrite::rule_context::RuleContext;
422    use crate::rewrite::{RuleError, cmd_targets};
423
424    #[test]
425    fn metadata_validation_rejects_fidelity_below_level_floor() {
426        let err = validate_rule_metadata(&SEMANTIC_STANDARD_RULE)
427            .expect_err("standard rules must not declare semantic fidelity");
428
429        assert_eq!(
430            err,
431            PlanBuildError::InvalidRuleMetadata {
432                rule: SemanticStandardRule::KEY,
433                message: "fidelity must not be below the normalization level floor",
434            }
435        );
436    }
437
438    struct SemanticStandardRule;
439
440    static SEMANTIC_STANDARD_RULE: SemanticStandardRule = SemanticStandardRule;
441
442    impl SemanticStandardRule {
443        const KEY: RuleKey = RuleKey {
444            package: PackageName::Base,
445            name: "semantic-standard-test",
446        };
447    }
448
449    impl RewriteRule for SemanticStandardRule {
450        fn meta(&self) -> &'static RuleMeta {
451            static META: RuleMeta = RuleMeta {
452                key: SemanticStandardRule::KEY,
453                enabled_by_packages: &[PackageName::Base],
454                level: NormalizationLevel::Standard,
455                summary: "Test-only invalid metadata.",
456                fidelity: RuleFidelity::Semantic,
457                triggers: cmd_targets![&base::cmd::BREAK],
458                consumes: RuleConsumes {
459                    eliminates: cmd_targets![&base::cmd::BREAK],
460                    touches: &[],
461                },
462                produces: RuleProduces { targets: &[] },
463            };
464            &META
465        }
466
467        fn apply(
468            &self,
469            _cx: &mut RuleContext<'_>,
470            _node_id: NodeId,
471        ) -> Result<RuleEffect, RuleError> {
472            Ok(RuleEffect::Skipped)
473        }
474    }
475}