texform_transform/rewrite/
plan.rs1use 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}