Skip to main content

omena_transform_passes/runtime/
planner.rs

1//! Transform pass registry, DAG planner, and public boundary summary.
2//!
3//! Planner code maps `omena-transform-cst` pass contracts into executable
4//! registry entries, enforces default DAG ordering, and reports the mutation
5//! passes that are implemented by the runtime executor.
6
7use omena_transform_cst::{
8    TRANSFORM_PASS_CATALOG_LEN, TransformDagEdgeV0, TransformLayer, TransformPassClassV0,
9    TransformPassContractV0, TransformPassDescriptorV0, TransformPassKind,
10    all_transform_pass_kinds, default_transform_dag_edges, default_transform_pass_contracts,
11    default_transform_pass_descriptors, transform_build_profile_from_passes,
12};
13
14use crate::{
15    TransformPassDispatchKindV0, TransformPassExecutionStatus, TransformPassPlanV0,
16    TransformPassPlanningErrorV0, TransformPassRegistryEntryV0, TransformPassRegistryV0,
17    TransformPassesBoundarySummaryV0, TransformPlanDependencyCycleV0,
18    TransformPlanDependencyEdgeV0, TransformPlanPassConflictV0,
19};
20
21pub fn summarize_omena_transform_passes_boundary() -> TransformPassesBoundarySummaryV0 {
22    let registry = default_transform_pass_registry();
23    let registry_entries = registry.entries.clone();
24    let pass_count = registry_entries.len();
25    let semantic_aware_pass_count = registry_entries
26        .iter()
27        .filter(|entry| entry.contract.layer == TransformLayer::SemanticAware)
28        .count();
29    let cascade_aware_pass_count = registry_entries
30        .iter()
31        .filter(|entry| entry.contract.reads_cascade_model)
32        .count();
33    let structural_pass_count = registry_entries
34        .iter()
35        .filter(|entry| entry.descriptor.pass_class == TransformPassClassV0::Structural)
36        .count();
37    let text_local_pass_count = registry_entries
38        .iter()
39        .filter(|entry| entry.descriptor.pass_class == TransformPassClassV0::TextLocal)
40        .count();
41    let module_evaluation_pass_count = registry_entries
42        .iter()
43        .filter(|entry| entry.descriptor.pass_class == TransformPassClassV0::ModuleEvaluation)
44        .count();
45
46    TransformPassesBoundarySummaryV0 {
47        schema_version: "0",
48        product: "omena-transform-passes.boundary",
49        registry_entries,
50        dag_edges: default_transform_dag_edges(),
51        pass_count,
52        full_catalog_registered: pass_count == TRANSFORM_PASS_CATALOG_LEN,
53        semantic_aware_pass_count,
54        cascade_aware_pass_count,
55        structural_pass_count,
56        text_local_pass_count,
57        module_evaluation_pass_count,
58        planner_enforces_dag_edges: true,
59        planner_uses_pass_descriptors: true,
60        ordinal_has_execution_semantics: false,
61        execution_runtime_ready: true,
62        incremental_execution_runtime_ready: true,
63        module_evaluation_native_output_marker: "nativeEditOutput",
64        module_evaluation_requires_native_product_output: true,
65        module_evaluation_requires_oracle_readiness: true,
66        module_evaluation_legacy_output_is_oracle_only: true,
67        module_evaluation_preserves_source_without_native_output: true,
68        implemented_mutation_pass_ids: implemented_mutation_pass_ids(),
69        next_surfaces: Vec::new(),
70    }
71}
72
73// This compatibility wrapper predates the checked API. The default registry is repository-owned
74// and separately exhaustively probed; an impossible cycle must hard-stop instead of fabricating a
75// plan for legacy callers that cannot receive `TransformPassPlanningErrorV0`.
76#[allow(clippy::panic)]
77pub fn plan_transform_passes(requested: &[TransformPassKind]) -> TransformPassPlanV0 {
78    let registry = default_transform_pass_registry();
79    let dag_edges = default_transform_dag_edges();
80    plan_transform_passes_with_registry(requested, &registry, dag_edges.as_slice()).unwrap_or_else(
81        |cycle| panic!("default transform pass registry contains a dependency cycle: {cycle:?}"),
82    )
83}
84
85fn plan_transform_passes_with_registry(
86    requested: &[TransformPassKind],
87    registry: &TransformPassRegistryV0,
88    dag_edges: &[TransformDagEdgeV0],
89) -> Result<TransformPassPlanV0, TransformPlanDependencyCycleV0> {
90    let requested_pass_ids = requested.iter().map(|pass| pass.id()).collect::<Vec<_>>();
91    let requested_unique = dedupe_requested_passes(requested);
92    let conflicting_unordered_pass_pairs = conflicting_unordered_pass_pairs(
93        requested_unique.as_slice(),
94        registry.entries.as_slice(),
95        dag_edges,
96    );
97    let ordered_passes = order_passes_by_registry(requested, registry.entries.as_slice())?;
98    let ordered_pass_ids = ordered_passes
99        .iter()
100        .map(|pass| pass.id())
101        .collect::<Vec<_>>();
102    let satisfied_dag_edge_count = dag_edges
103        .iter()
104        .filter(|edge| {
105            edge_applies(edge, &ordered_pass_ids) && edge_is_satisfied(edge, &ordered_pass_ids)
106        })
107        .count();
108    let violated_dag_edge_count = dag_edges
109        .iter()
110        .filter(|edge| {
111            edge_applies(edge, &ordered_pass_ids) && !edge_is_satisfied(edge, &ordered_pass_ids)
112        })
113        .count();
114
115    Ok(TransformPassPlanV0 {
116        schema_version: "0",
117        product: "omena-transform-passes.plan",
118        build_profile: transform_build_profile_from_passes(
119            "descriptor-ordered-transform-plan",
120            ordered_passes.as_slice(),
121        ),
122        requested_pass_ids,
123        ordered_pass_ids,
124        satisfied_dag_edge_count,
125        violated_dag_edge_count,
126        all_requested_registered: requested
127            .iter()
128            .all(|pass| descriptor_for_pass(*pass, registry.entries.as_slice()).is_some()),
129        conflicting_unordered_pass_pairs,
130    })
131}
132
133pub fn plan_transform_passes_checked(
134    requested: &[TransformPassKind],
135) -> Result<TransformPassPlanV0, TransformPassPlanningErrorV0> {
136    let registry = default_transform_pass_registry();
137    let dag_edges = default_transform_dag_edges();
138    plan_transform_passes_checked_with_registry(requested, &registry, dag_edges.as_slice())
139}
140
141fn plan_transform_passes_checked_with_registry(
142    requested: &[TransformPassKind],
143    registry: &TransformPassRegistryV0,
144    dag_edges: &[TransformDagEdgeV0],
145) -> Result<TransformPassPlanV0, TransformPassPlanningErrorV0> {
146    let plan = plan_transform_passes_with_registry(requested, registry, dag_edges)
147        .map_err(|cycle| TransformPassPlanningErrorV0::DependencyCycle { cycle })?;
148    if let Some(conflict) = plan.conflicting_unordered_pass_pairs.first().cloned() {
149        Err(TransformPassPlanningErrorV0::UnorderedPassConflict { conflict })
150    } else {
151        Ok(plan)
152    }
153}
154
155#[cfg(feature = "transform-catalog-trace")]
156pub fn plan_transform_passes_parallel_transform_catalog_layers(
157    requested: &[TransformPassKind],
158) -> omena_lawvere::TransformCatalogTransformPassParallelPlanV0 {
159    omena_lawvere::plan_transform_catalog_parallel_layers_v0(requested)
160}
161
162#[cfg(feature = "transform-catalog-trace")]
163#[allow(deprecated)]
164#[deprecated(
165    since = "0.4.0",
166    note = "use plan_transform_passes_parallel_transform_catalog_layers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
167)]
168pub fn plan_transform_passes_parallel_lawvere_layers(
169    requested: &[TransformPassKind],
170) -> omena_lawvere::TransformPassParallelPlanV0 {
171    omena_lawvere::plan_transform_pass_parallel_layers_v0(requested)
172}
173
174pub fn implemented_mutation_pass_ids() -> Vec<&'static str> {
175    default_transform_pass_registry()
176        .entries
177        .into_iter()
178        .filter(|entry| entry.contract.executes_mutation)
179        .map(|entry| entry.contract.id)
180        .collect()
181}
182
183pub fn default_transform_pass_registry() -> TransformPassRegistryV0 {
184    let contracts = default_transform_pass_contracts();
185    let entries = default_transform_pass_descriptors()
186        .into_iter()
187        .filter_map(|descriptor| {
188            contract_for_pass(descriptor.kind, contracts.as_slice())
189                .cloned()
190                .map(|contract| registry_entry_for_descriptor(contract, descriptor))
191        })
192        .collect::<Vec<_>>();
193    TransformPassRegistryV0 {
194        schema_version: "0",
195        product: "omena-transform-passes.pass-registry",
196        entries,
197    }
198}
199
200fn registry_entry_for_descriptor(
201    contract: TransformPassContractV0,
202    descriptor: TransformPassDescriptorV0,
203) -> TransformPassRegistryEntryV0 {
204    let module_family = contract.family;
205    let dispatch_kind = dispatch_kind_for_descriptor(&descriptor);
206    TransformPassRegistryEntryV0 {
207        module_family,
208        query_family: query_family_for_pass(contract.kind),
209        dispatch_kind,
210        execution_status: TransformPassExecutionStatus::RegistryAndPlannerReady,
211        contract,
212        descriptor,
213    }
214}
215
216fn dispatch_kind_for_descriptor(
217    descriptor: &TransformPassDescriptorV0,
218) -> TransformPassDispatchKindV0 {
219    match descriptor.pass_class {
220        TransformPassClassV0::TextLocal => TransformPassDispatchKindV0::TextLocalSliceRewrite,
221        TransformPassClassV0::Structural => TransformPassDispatchKindV0::StructuralIrTransaction,
222        TransformPassClassV0::ModuleEvaluation => {
223            TransformPassDispatchKindV0::ModuleEvaluationHandler
224        }
225        TransformPassClassV0::Emission => TransformPassDispatchKindV0::EmissionBoundary,
226    }
227}
228
229fn query_family_for_pass(kind: TransformPassKind) -> &'static str {
230    match kind.layer() {
231        TransformLayer::SemanticAware => "semantic-aware-transform-query",
232        TransformLayer::Commodity => "commodity-transform-query",
233        TransformLayer::Emission => "emission-transform-query",
234        TransformLayer::SemanticReadOnly => "semantic-read-only-query",
235    }
236}
237
238// Kahn no-progress with a non-empty remainder entails a cycle. The witness walk covers exactly
239// those remaining registry dependencies, so absence would be an internal invariant violation.
240#[allow(clippy::expect_used)]
241fn order_passes_by_registry(
242    requested: &[TransformPassKind],
243    registry_entries: &[TransformPassRegistryEntryV0],
244) -> Result<Vec<TransformPassKind>, TransformPlanDependencyCycleV0> {
245    let mut remaining = dedupe_requested_passes(requested);
246    remaining.sort_by_key(|kind| {
247        descriptor_for_pass(*kind, registry_entries)
248            .map(|descriptor| (descriptor.phase, descriptor.phase_order, descriptor.id))
249            .unwrap_or((u8::MAX, u16::MAX, ""))
250    });
251
252    let mut ordered = Vec::with_capacity(remaining.len());
253    while !remaining.is_empty() {
254        let Some(next_index) = remaining.iter().position(|candidate| {
255            !has_incoming_edge_from_remaining(*candidate, &remaining, registry_entries)
256        }) else {
257            let cycle = dependency_cycle_witness(remaining.as_slice(), registry_entries)
258                .expect("Kahn planner no-progress state must contain a dependency cycle");
259            return Err(cycle);
260        };
261        ordered.push(remaining.remove(next_index));
262    }
263
264    Ok(ordered)
265}
266
267#[cfg(test)]
268#[allow(clippy::expect_used, clippy::items_after_test_module, clippy::panic)]
269mod planner_cycle_tests {
270    use super::*;
271
272    #[test]
273    fn constructed_dependency_cycle_is_not_silently_ordered() {
274        let mut registry = default_transform_pass_registry();
275        registry
276            .entries
277            .iter_mut()
278            .find(|entry| entry.descriptor.kind == TransformPassKind::ImportInline)
279            .expect("import-inline descriptor")
280            .descriptor
281            .depends_on = vec![TransformPassKind::PrintCss.id()];
282        registry
283            .entries
284            .iter_mut()
285            .find(|entry| entry.descriptor.kind == TransformPassKind::PrintCss)
286            .expect("print-css descriptor")
287            .descriptor
288            .depends_on = vec![TransformPassKind::ImportInline.id()];
289
290        let error = plan_transform_passes_checked_with_registry(
291            &[TransformPassKind::ImportInline, TransformPassKind::PrintCss],
292            &registry,
293            default_transform_dag_edges().as_slice(),
294        )
295        .expect_err("a cyclic transform registry must not produce an arbitrary order");
296        let serialized = serde_json::to_value(&error).expect("serialize typed planner error");
297        eprintln!("TRANSFORM_PLANNER_CYCLE_ERROR={serialized}");
298        assert_eq!(serialized["kind"], "dependencyCycle");
299        assert_eq!(
300            serialized["cycle"]["dependencyEdges"]
301                .as_array()
302                .map(Vec::len),
303            Some(2)
304        );
305        let TransformPassPlanningErrorV0::DependencyCycle { cycle: error } = error else {
306            panic!("constructed dependency cycle returned the wrong typed planning error");
307        };
308
309        assert_eq!(
310            error.cycle_path,
311            vec!["import-inline", "print-css", "import-inline"]
312        );
313        assert_eq!(
314            error.dependency_edges,
315            vec![
316                TransformPlanDependencyEdgeV0 {
317                    prerequisite_pass_id: "print-css",
318                    dependent_pass_id: "import-inline",
319                },
320                TransformPlanDependencyEdgeV0 {
321                    prerequisite_pass_id: "import-inline",
322                    dependent_pass_id: "print-css",
323                },
324            ]
325        );
326    }
327
328    #[test]
329    fn default_catalog_has_no_dependency_cycle_across_300k_deterministic_requests() {
330        const PROBE_INPUT_COUNT: usize = 300_000;
331        const MAX_REQUEST_WIDTH: usize = 8;
332
333        let registry = default_transform_pass_registry();
334        let dag_edges = default_transform_dag_edges();
335        let catalog = all_transform_pass_kinds();
336        let mut state = 0x9e37_79b9_7f4a_7c15_u64;
337        let mut observed_pass_ids = std::collections::BTreeSet::new();
338        let mut cycle_error_count = 0_usize;
339        let mut violated_dag_edge_plan_count = 0_usize;
340
341        for _ in 0..PROBE_INPUT_COUNT {
342            state ^= state << 13;
343            state ^= state >> 7;
344            state ^= state << 17;
345            let width = 1 + (state as usize % MAX_REQUEST_WIDTH);
346            let mut requested = Vec::with_capacity(width);
347            for _ in 0..width {
348                state ^= state << 13;
349                state ^= state >> 7;
350                state ^= state << 17;
351                let pass = catalog[state as usize % catalog.len()];
352                observed_pass_ids.insert(pass.id());
353                requested.push(pass);
354            }
355            match plan_transform_passes_with_registry(
356                requested.as_slice(),
357                &registry,
358                dag_edges.as_slice(),
359            ) {
360                Ok(plan) => {
361                    violated_dag_edge_plan_count += usize::from(plan.violated_dag_edge_count > 0);
362                }
363                Err(_) => cycle_error_count += 1,
364            }
365        }
366
367        eprintln!(
368            "TRANSFORM_PLANNER_PROBE={{\"inputCount\":{PROBE_INPUT_COUNT},\"maximumRequestWidth\":{MAX_REQUEST_WIDTH},\"observedPassKindCount\":{},\"cycleErrorCount\":{cycle_error_count},\"violatedDagEdgePlanCount\":{violated_dag_edge_plan_count}}}",
369            observed_pass_ids.len(),
370        );
371        assert_eq!(observed_pass_ids.len(), catalog.len());
372        assert_eq!(cycle_error_count, 0);
373        assert_eq!(violated_dag_edge_plan_count, 0);
374    }
375}
376
377fn dependency_cycle_witness(
378    remaining: &[TransformPassKind],
379    registry_entries: &[TransformPassRegistryEntryV0],
380) -> Option<TransformPlanDependencyCycleV0> {
381    let remaining_ids = remaining.iter().map(|pass| pass.id()).collect::<Vec<_>>();
382    let mut visit_state = std::collections::BTreeMap::<&'static str, u8>::new();
383    let mut stack = Vec::new();
384    for pass_id in &remaining_ids {
385        if visit_state.get(pass_id).copied().unwrap_or_default() == 0
386            && let Some(cycle) = visit_dependency_cycle(
387                pass_id,
388                remaining_ids.as_slice(),
389                registry_entries,
390                &mut visit_state,
391                &mut stack,
392            )
393        {
394            return Some(cycle);
395        }
396    }
397    None
398}
399
400fn visit_dependency_cycle(
401    pass_id: &'static str,
402    remaining_ids: &[&'static str],
403    registry_entries: &[TransformPassRegistryEntryV0],
404    visit_state: &mut std::collections::BTreeMap<&'static str, u8>,
405    stack: &mut Vec<&'static str>,
406) -> Option<TransformPlanDependencyCycleV0> {
407    visit_state.insert(pass_id, 1);
408    stack.push(pass_id);
409    let mut dependencies = registry_entries
410        .iter()
411        .find(|entry| entry.descriptor.id == pass_id)
412        .map(|entry| entry.descriptor.depends_on.clone())
413        .unwrap_or_default();
414    dependencies.sort_unstable();
415    dependencies.dedup();
416    for dependency in dependencies
417        .into_iter()
418        .filter(|dependency| remaining_ids.contains(dependency))
419    {
420        match visit_state.get(dependency).copied().unwrap_or_default() {
421            0 => {
422                if let Some(cycle) = visit_dependency_cycle(
423                    dependency,
424                    remaining_ids,
425                    registry_entries,
426                    visit_state,
427                    stack,
428                ) {
429                    return Some(cycle);
430                }
431            }
432            1 => {
433                let cycle_start = stack
434                    .iter()
435                    .position(|candidate| *candidate == dependency)?;
436                let mut cycle_path = stack[cycle_start..].to_vec();
437                cycle_path.push(dependency);
438                let dependency_edges = cycle_path
439                    .windows(2)
440                    .map(|pair| TransformPlanDependencyEdgeV0 {
441                        prerequisite_pass_id: pair[1],
442                        dependent_pass_id: pair[0],
443                    })
444                    .collect();
445                return Some(TransformPlanDependencyCycleV0 {
446                    cycle_path,
447                    dependency_edges,
448                });
449            }
450            _ => {}
451        }
452    }
453    stack.pop();
454    visit_state.insert(pass_id, 2);
455    None
456}
457
458fn dedupe_requested_passes(requested: &[TransformPassKind]) -> Vec<TransformPassKind> {
459    let mut unique = Vec::new();
460    for pass in requested {
461        if !unique.contains(pass) {
462            unique.push(*pass);
463        }
464    }
465    unique
466}
467
468fn conflicting_unordered_pass_pairs(
469    requested: &[TransformPassKind],
470    registry_entries: &[TransformPassRegistryEntryV0],
471    dag_edges: &[TransformDagEdgeV0],
472) -> Vec<TransformPlanPassConflictV0> {
473    let mut conflicts = Vec::new();
474    for (left_index, left) in requested.iter().enumerate() {
475        for right in requested.iter().skip(left_index + 1) {
476            let Some(left_descriptor) = descriptor_for_pass(*left, registry_entries) else {
477                continue;
478            };
479            let Some(right_descriptor) = descriptor_for_pass(*right, registry_entries) else {
480                continue;
481            };
482            let declared = left_descriptor
483                .conflicts_with
484                .contains(&right_descriptor.id)
485                || right_descriptor
486                    .conflicts_with
487                    .contains(&left_descriptor.id);
488            if declared
489                && !dag_path_exists(left_descriptor.id, right_descriptor.id, dag_edges)
490                && !dag_path_exists(right_descriptor.id, left_descriptor.id, dag_edges)
491            {
492                conflicts.push(TransformPlanPassConflictV0 {
493                    pass_a: left_descriptor.id,
494                    pass_b: right_descriptor.id,
495                });
496            }
497        }
498    }
499    conflicts
500}
501
502fn has_incoming_edge_from_remaining(
503    candidate: TransformPassKind,
504    remaining: &[TransformPassKind],
505    registry_entries: &[TransformPassRegistryEntryV0],
506) -> bool {
507    descriptor_for_pass(candidate, registry_entries).is_some_and(|descriptor| {
508        descriptor
509            .depends_on
510            .iter()
511            .any(|dependency| remaining.iter().any(|other| other.id() == *dependency))
512    })
513}
514
515fn edge_applies(edge: &TransformDagEdgeV0, ordered_pass_ids: &[&'static str]) -> bool {
516    ordered_pass_ids.contains(&edge.from) && ordered_pass_ids.contains(&edge.to)
517}
518
519fn edge_is_satisfied(edge: &TransformDagEdgeV0, ordered_pass_ids: &[&'static str]) -> bool {
520    let from = position_of_pass_id(edge.from, ordered_pass_ids);
521    let to = position_of_pass_id(edge.to, ordered_pass_ids);
522    match (from, to) {
523        (Some(from), Some(to)) => from < to,
524        _ => false,
525    }
526}
527
528fn dag_path_exists(from: &'static str, to: &'static str, dag_edges: &[TransformDagEdgeV0]) -> bool {
529    let mut stack = vec![from];
530    let mut visited = Vec::new();
531    while let Some(current) = stack.pop() {
532        if current == to {
533            return true;
534        }
535        if visited.contains(&current) {
536            continue;
537        }
538        visited.push(current);
539        for edge in dag_edges.iter().filter(|edge| edge.from == current) {
540            stack.push(edge.to);
541        }
542    }
543    false
544}
545
546fn position_of_pass_id(pass_id: &'static str, ordered_pass_ids: &[&'static str]) -> Option<usize> {
547    ordered_pass_ids
548        .iter()
549        .position(|ordered_pass_id| *ordered_pass_id == pass_id)
550}
551
552fn contract_for_pass(
553    pass: TransformPassKind,
554    contracts: &[TransformPassContractV0],
555) -> Option<&TransformPassContractV0> {
556    contracts.iter().find(|contract| contract.kind == pass)
557}
558
559fn descriptor_for_pass(
560    pass: TransformPassKind,
561    registry_entries: &[TransformPassRegistryEntryV0],
562) -> Option<&TransformPassDescriptorV0> {
563    registry_entries
564        .iter()
565        .find(|entry| entry.descriptor.kind == pass)
566        .map(|entry| &entry.descriptor)
567}
568
569pub(crate) fn transform_pass_kind_from_id(pass_id: &str) -> Option<TransformPassKind> {
570    all_transform_pass_kinds()
571        .into_iter()
572        .find(|kind| kind.id() == pass_id)
573}