Skip to main content

omena_bundler/
emission_order.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use omena_cross_file_summary::EdgeOrderRelevanceV0;
4use omena_parser::ModuleInstanceKeyV0;
5use serde::Serialize;
6
7use crate::{
8    BundleResolutionAuthorityV0, GlobalRuleOrderV0, LinkedStylesheetRuleV0, LinkerInputV0,
9    StyleDialect, TransformBundleEdgeKind, TransformBundleLinkErrorV0,
10    TransformBundleResolvedDependencyV0, module_instances_by_linker_path,
11    resolve_imported_module_instance_for_edge, selector_kind_label,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
15#[serde(rename_all = "camelCase")]
16/// Identifies one emitted rule by module instance and source order within that module.
17pub struct EmissionOrderKeyV0 {
18    pub module_instance: ModuleInstanceKeyV0,
19    pub intra_module_ordinal: u32,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "camelCase")]
24/// Records an order-bearing dependency used to construct a bundle emission plan.
25pub struct EmissionDependencyFactV0 {
26    pub from_module: ModuleInstanceKeyV0,
27    pub to_module: ModuleInstanceKeyV0,
28    pub edge_kind: TransformBundleEdgeKind,
29    pub import_ordinal: u32,
30    pub order_relevance: EdgeOrderRelevanceV0,
31    pub order_relevance_reason: &'static str,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
35#[serde(rename_all = "camelCase")]
36/// Classifies a dependency cycle by the edge kinds that participate in it.
37pub enum EmissionCycleClassV0 {
38    Import,
39    Composition,
40    Mixed,
41}
42
43#[non_exhaustive]
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
45#[serde(rename_all = "camelCase")]
46/// Identifies the source-language family represented by an emission cycle.
47pub enum EmissionCycleDialectV0 {
48    Css,
49    Scss,
50    Sass,
51    Less,
52    Mixed,
53}
54
55fn emission_cycle_dialect(dialect: StyleDialect) -> EmissionCycleDialectV0 {
56    match dialect {
57        StyleDialect::Css => EmissionCycleDialectV0::Css,
58        StyleDialect::Scss => EmissionCycleDialectV0::Scss,
59        StyleDialect::Sass => EmissionCycleDialectV0::Sass,
60        StyleDialect::Less => EmissionCycleDialectV0::Less,
61    }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "camelCase")]
66/// Selects the deterministic tie-break policy used inside a dependency cycle.
67pub enum EmissionCyclePolicyV0 {
68    ModuleIdentity,
69}
70
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
72#[serde(rename_all = "camelCase")]
73/// Selects how linked modules are ordered before their rules are emitted.
74pub enum EmissionOrderingPolicyV0 {
75    #[deprecated(
76        since = "0.5.0",
77        note = "legacy module-id emission ordering is scheduled for removal before 1.0"
78    )]
79    ModuleIdLegacy,
80    #[default]
81    ImportOrderPreserving,
82}
83
84#[allow(deprecated)]
85impl EmissionOrderingPolicyV0 {
86    /// Returns the stable label serialized by command and adapter surfaces.
87    pub const fn as_wire_label(self) -> &'static str {
88        match self {
89            Self::ModuleIdLegacy => "moduleIdLegacy",
90            Self::ImportOrderPreserving => "importOrderPreserving",
91        }
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
96#[serde(rename_all = "camelCase")]
97/// Describes a strongly connected dependency group and its deterministic member order.
98pub struct EmissionCycleGroupV0 {
99    pub members: Vec<ModuleInstanceKeyV0>,
100    pub chosen_order: Vec<ModuleInstanceKeyV0>,
101    pub class: EmissionCycleClassV0,
102    pub dialect: EmissionCycleDialectV0,
103    pub policy: EmissionCyclePolicyV0,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
107#[serde(rename_all = "camelCase")]
108/// Contains the complete rule order and supporting dependency evidence for a linked bundle.
109pub struct EmissionPlanV0 {
110    pub policy: EmissionOrderingPolicyV0,
111    pub entries: Vec<EmissionOrderKeyV0>,
112    pub dependency_facts: Vec<EmissionDependencyFactV0>,
113    pub cycle_groups: Vec<EmissionCycleGroupV0>,
114}
115
116pub(crate) struct EmissionModulePlanV0 {
117    pub(crate) policy: EmissionOrderingPolicyV0,
118    pub(crate) module_order: Vec<ModuleInstanceKeyV0>,
119    pub(crate) dependency_facts: Vec<EmissionDependencyFactV0>,
120    pub(crate) cycle_groups: Vec<EmissionCycleGroupV0>,
121}
122
123pub(crate) fn build_emission_plan(
124    inputs: &[LinkerInputV0],
125    linked_modules: &[ModuleInstanceKeyV0],
126    entrypoints: &[ModuleInstanceKeyV0],
127    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
128    policy: EmissionOrderingPolicyV0,
129    resolution_authority: BundleResolutionAuthorityV0,
130) -> Result<EmissionPlanV0, TransformBundleLinkErrorV0> {
131    let module_plan = build_emission_module_plan(
132        inputs,
133        linked_modules,
134        entrypoints,
135        resolved_dependencies,
136        policy,
137        resolution_authority,
138    )?;
139    build_emission_plan_from_module_plan(inputs, &module_plan)
140}
141
142#[allow(deprecated)]
143pub(crate) fn build_emission_module_plan(
144    inputs: &[LinkerInputV0],
145    linked_modules: &[ModuleInstanceKeyV0],
146    entrypoints: &[ModuleInstanceKeyV0],
147    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
148    policy: EmissionOrderingPolicyV0,
149    resolution_authority: BundleResolutionAuthorityV0,
150) -> Result<EmissionModulePlanV0, TransformBundleLinkErrorV0> {
151    let dependency_facts = collect_emission_dependency_facts(
152        inputs,
153        linked_modules,
154        resolved_dependencies,
155        resolution_authority,
156    )?;
157    let cycle_groups = build_cycle_groups(inputs, linked_modules, &dependency_facts)?;
158    let module_order = match policy {
159        EmissionOrderingPolicyV0::ModuleIdLegacy => linked_modules.to_vec(),
160        EmissionOrderingPolicyV0::ImportOrderPreserving => import_ordered_modules(
161            entrypoints,
162            linked_modules,
163            &dependency_facts,
164            &cycle_groups,
165        )?,
166    };
167    Ok(EmissionModulePlanV0 {
168        policy,
169        module_order,
170        dependency_facts,
171        cycle_groups,
172    })
173}
174
175pub(crate) fn build_emission_plan_from_module_plan(
176    inputs: &[LinkerInputV0],
177    module_plan: &EmissionModulePlanV0,
178) -> Result<EmissionPlanV0, TransformBundleLinkErrorV0> {
179    let inputs_by_instance = inputs
180        .iter()
181        .map(|input| (input.instance.clone(), input))
182        .collect::<BTreeMap<_, _>>();
183    let mut entries = Vec::new();
184    for instance in &module_plan.module_order {
185        let input = inputs_by_instance.get(instance).ok_or_else(|| {
186            TransformBundleLinkErrorV0::InvalidEmissionPlan {
187                reason: format!(
188                    "reachable module {} has no linker input",
189                    instance.module().as_str()
190                ),
191            }
192        })?;
193        for intra_module_ordinal in 0..input.ordered_rules.len() {
194            entries.push(EmissionOrderKeyV0 {
195                module_instance: instance.clone(),
196                intra_module_ordinal: u32::try_from(intra_module_ordinal).map_err(|_| {
197                    TransformBundleLinkErrorV0::InvalidEmissionPlan {
198                        reason: format!(
199                            "module {} has more rules than the emission key can represent",
200                            instance.module().as_str()
201                        ),
202                    }
203                })?,
204            });
205        }
206    }
207    Ok(EmissionPlanV0 {
208        policy: module_plan.policy,
209        entries,
210        dependency_facts: module_plan.dependency_facts.clone(),
211        cycle_groups: module_plan.cycle_groups.clone(),
212    })
213}
214
215fn import_ordered_modules(
216    entrypoints: &[ModuleInstanceKeyV0],
217    linked_modules: &[ModuleInstanceKeyV0],
218    dependency_facts: &[EmissionDependencyFactV0],
219    cycle_groups: &[EmissionCycleGroupV0],
220) -> Result<Vec<ModuleInstanceKeyV0>, TransformBundleLinkErrorV0> {
221    let mut components = cycle_groups
222        .iter()
223        .map(|group| group.chosen_order.clone())
224        .collect::<Vec<_>>();
225    let cycle_members = components
226        .iter()
227        .flatten()
228        .cloned()
229        .collect::<BTreeSet<_>>();
230    components.extend(
231        linked_modules
232            .iter()
233            .filter(|module| !cycle_members.contains(*module))
234            .cloned()
235            .map(|module| vec![module]),
236    );
237    components.sort_by(|left, right| left[0].cmp(&right[0]));
238
239    let mut component_by_module = BTreeMap::new();
240    for (component_index, members) in components.iter().enumerate() {
241        for member in members {
242            component_by_module.insert(member.clone(), component_index);
243        }
244    }
245    if component_by_module.len() != linked_modules.len() {
246        return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
247            reason: "emission components do not cover closed-world membership".to_string(),
248        });
249    }
250
251    let mut adjacency = (0..components.len())
252        .map(|index| (index, Vec::<(u32, usize)>::new()))
253        .collect::<BTreeMap<_, _>>();
254    for fact in dependency_facts {
255        let Some(from_component) = component_by_module.get(&fact.from_module).copied() else {
256            return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
257                reason: "dependency source is absent from emission components".to_string(),
258            });
259        };
260        let Some(to_component) = component_by_module.get(&fact.to_module).copied() else {
261            return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
262                reason: "dependency target is absent from emission components".to_string(),
263            });
264        };
265        if from_component != to_component {
266            adjacency
267                .entry(from_component)
268                .or_default()
269                .push((fact.import_ordinal, to_component));
270        }
271    }
272    for targets in adjacency.values_mut() {
273        targets.sort_by_key(|(ordinal, target)| (*ordinal, *target));
274        targets.dedup_by_key(|(_, target)| *target);
275    }
276
277    let mut roots = entrypoints
278        .iter()
279        .filter_map(|entrypoint| component_by_module.get(entrypoint).copied())
280        .collect::<Vec<_>>();
281    roots.extend(0..components.len());
282    let mut visited = BTreeSet::new();
283    let mut component_order = Vec::new();
284    for root in roots {
285        if visited.contains(&root) {
286            continue;
287        }
288        let mut stack = vec![(root, false)];
289        while let Some((component, expanded)) = stack.pop() {
290            if expanded {
291                component_order.push(component);
292                continue;
293            }
294            if !visited.insert(component) {
295                continue;
296            }
297            stack.push((component, true));
298            if let Some(targets) = adjacency.get(&component) {
299                stack.extend(targets.iter().rev().map(|(_, target)| (*target, false)));
300            }
301        }
302    }
303
304    Ok(component_order
305        .into_iter()
306        .flat_map(|component| components[component].iter().cloned())
307        .collect())
308}
309
310fn collect_emission_dependency_facts(
311    inputs: &[LinkerInputV0],
312    linked_modules: &[ModuleInstanceKeyV0],
313    resolved_dependencies: &[TransformBundleResolvedDependencyV0],
314    resolution_authority: BundleResolutionAuthorityV0,
315) -> Result<Vec<EmissionDependencyFactV0>, TransformBundleLinkErrorV0> {
316    let reachable = linked_modules.iter().cloned().collect::<BTreeSet<_>>();
317    let inputs_by_instance = inputs
318        .iter()
319        .map(|input| (input.instance.clone(), input))
320        .collect::<BTreeMap<_, _>>();
321    let instances_by_path = module_instances_by_linker_path(inputs);
322    let mut facts = Vec::new();
323    for from_module in linked_modules {
324        let input = inputs_by_instance.get(from_module).ok_or_else(|| {
325            TransformBundleLinkErrorV0::InvalidEmissionPlan {
326                reason: format!(
327                    "reachable module {} has no dependency projection",
328                    from_module.module().as_str()
329                ),
330            }
331        })?;
332        for edge in &input.dependency_edges {
333            let order_relevance = edge.kind.order_relevance();
334            if order_relevance == EdgeOrderRelevanceV0::OrderNeutral {
335                continue;
336            }
337            let import_ordinal = edge.import_ordinal.ok_or_else(|| {
338                TransformBundleLinkErrorV0::InvalidEmissionPlan {
339                    reason: format!(
340                        "order-bearing dependency {} in {} has no parser-origin ordinal",
341                        edge.import_source,
342                        from_module.module().as_str()
343                    ),
344                }
345            })?;
346            let to_module = resolve_imported_module_instance_for_edge(
347                input,
348                edge,
349                resolved_dependencies,
350                &instances_by_path,
351                resolution_authority,
352            )?
353            .target_instance
354            .ok_or_else(|| TransformBundleLinkErrorV0::MissingDependency {
355                source_path: input.source_path.clone(),
356                import_source: edge.import_source.clone(),
357            })?;
358            if !reachable.contains(&to_module) {
359                return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
360                    reason: format!(
361                        "dependency {} from {} is absent from the closed-world membership",
362                        to_module.module().as_str(),
363                        from_module.module().as_str()
364                    ),
365                });
366            }
367            facts.push(EmissionDependencyFactV0 {
368                from_module: from_module.clone(),
369                to_module,
370                edge_kind: edge.kind,
371                import_ordinal,
372                order_relevance,
373                order_relevance_reason: edge.kind.order_relevance_reason(),
374            });
375        }
376    }
377    facts.sort_by(|left, right| {
378        left.from_module
379            .cmp(&right.from_module)
380            .then_with(|| left.import_ordinal.cmp(&right.import_ordinal))
381            .then_with(|| left.to_module.cmp(&right.to_module))
382    });
383    Ok(facts)
384}
385
386fn build_cycle_groups(
387    inputs: &[LinkerInputV0],
388    linked_modules: &[ModuleInstanceKeyV0],
389    dependency_facts: &[EmissionDependencyFactV0],
390) -> Result<Vec<EmissionCycleGroupV0>, TransformBundleLinkErrorV0> {
391    let dialects_by_instance = inputs
392        .iter()
393        .map(|input| (input.instance.clone(), input.dialect))
394        .collect::<BTreeMap<_, _>>();
395    let mut adjacency = linked_modules
396        .iter()
397        .cloned()
398        .map(|module| (module, BTreeSet::new()))
399        .collect::<BTreeMap<_, _>>();
400    let mut reverse = adjacency.clone();
401    for fact in dependency_facts {
402        adjacency
403            .entry(fact.from_module.clone())
404            .or_default()
405            .insert(fact.to_module.clone());
406        reverse
407            .entry(fact.to_module.clone())
408            .or_default()
409            .insert(fact.from_module.clone());
410    }
411
412    let finish_order = graph_finish_order(linked_modules, &adjacency);
413    let mut assigned = BTreeSet::new();
414    let mut groups = Vec::new();
415    for root in finish_order.into_iter().rev() {
416        if assigned.contains(&root) {
417            continue;
418        }
419        let mut stack = vec![root];
420        let mut members = Vec::new();
421        while let Some(module) = stack.pop() {
422            if !assigned.insert(module.clone()) {
423                continue;
424            }
425            members.push(module.clone());
426            if let Some(predecessors) = reverse.get(&module) {
427                stack.extend(predecessors.iter().rev().cloned());
428            }
429        }
430        members.sort();
431        let has_self_loop = members.len() == 1
432            && adjacency
433                .get(&members[0])
434                .is_some_and(|targets| targets.contains(&members[0]));
435        if members.len() < 2 && !has_self_loop {
436            continue;
437        }
438
439        let member_set = members.iter().cloned().collect::<BTreeSet<_>>();
440        let mut has_import = false;
441        let mut has_composition = false;
442        let mut edge_kinds = Vec::new();
443        for fact in dependency_facts.iter().filter(|fact| {
444            member_set.contains(&fact.from_module) && member_set.contains(&fact.to_module)
445        }) {
446            if !edge_kinds.contains(&fact.edge_kind) {
447                edge_kinds.push(fact.edge_kind);
448            }
449            match fact.edge_kind {
450                TransformBundleEdgeKind::CssModuleComposesExternal => has_composition = true,
451                TransformBundleEdgeKind::CssModuleComposesLocal => {
452                    return Err(TransformBundleLinkErrorV0::UnsupportedEmissionCycle {
453                        edge_kind: fact.edge_kind,
454                    });
455                }
456                TransformBundleEdgeKind::SassUse
457                | TransformBundleEdgeKind::SassForward
458                | TransformBundleEdgeKind::SassImport
459                | TransformBundleEdgeKind::CssImport
460                | TransformBundleEdgeKind::LessImport
461                | TransformBundleEdgeKind::CssModuleValueImport
462                | TransformBundleEdgeKind::IcssImport => has_import = true,
463            }
464        }
465        let class = match (has_import, has_composition) {
466            (true, true) => EmissionCycleClassV0::Mixed,
467            (false, true) => EmissionCycleClassV0::Composition,
468            (true, false) => EmissionCycleClassV0::Import,
469            (false, false) => {
470                return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
471                    reason: "cycle group has no classified order-bearing edge".to_string(),
472                });
473            }
474        };
475        edge_kinds.sort_by_key(|edge_kind| edge_kind.as_wire_label());
476        let cycle_dialects = members
477            .iter()
478            .map(|member| {
479                dialects_by_instance.get(member).copied().ok_or_else(|| {
480                    TransformBundleLinkErrorV0::InvalidEmissionPlan {
481                        reason: format!(
482                            "cycle member {} has no source dialect",
483                            member.module().as_str()
484                        ),
485                    }
486                })
487            })
488            .collect::<Result<BTreeSet<_>, _>>()?;
489        let dialect = if cycle_dialects.len() == 1 {
490            let source_dialect = cycle_dialects.first().copied().ok_or_else(|| {
491                TransformBundleLinkErrorV0::InvalidEmissionPlan {
492                    reason: "cycle group has no source dialect".to_string(),
493                }
494            })?;
495            emission_cycle_dialect(source_dialect)
496        } else {
497            EmissionCycleDialectV0::Mixed
498        };
499        if matches!(
500            class,
501            EmissionCycleClassV0::Import | EmissionCycleClassV0::Mixed
502        ) {
503            return Err(
504                TransformBundleLinkErrorV0::UnsupportedDialectEmissionCycle {
505                    dialect,
506                    class,
507                    edge_kinds,
508                },
509            );
510        }
511        groups.push(EmissionCycleGroupV0 {
512            chosen_order: members.clone(),
513            members,
514            class,
515            dialect,
516            policy: EmissionCyclePolicyV0::ModuleIdentity,
517        });
518    }
519    groups.sort_by(|left, right| left.members.cmp(&right.members));
520    Ok(groups)
521}
522
523fn graph_finish_order(
524    nodes: &[ModuleInstanceKeyV0],
525    adjacency: &BTreeMap<ModuleInstanceKeyV0, BTreeSet<ModuleInstanceKeyV0>>,
526) -> Vec<ModuleInstanceKeyV0> {
527    let mut visited = BTreeSet::new();
528    let mut finished = Vec::new();
529    for root in nodes {
530        if visited.contains(root) {
531            continue;
532        }
533        let mut stack = vec![(root.clone(), false)];
534        while let Some((module, expanded)) = stack.pop() {
535            if expanded {
536                finished.push(module);
537                continue;
538            }
539            if !visited.insert(module.clone()) {
540                continue;
541            }
542            stack.push((module.clone(), true));
543            if let Some(targets) = adjacency.get(&module) {
544                stack.extend(targets.iter().rev().cloned().map(|target| (target, false)));
545            }
546        }
547    }
548    finished
549}
550
551pub(crate) fn build_global_rule_order_from_plan(
552    inputs: &[LinkerInputV0],
553    plan: &EmissionPlanV0,
554) -> Result<GlobalRuleOrderV0, TransformBundleLinkErrorV0> {
555    let inputs_by_instance = inputs
556        .iter()
557        .map(|input| (input.instance.clone(), input))
558        .collect::<BTreeMap<_, _>>();
559    let mut rules = Vec::with_capacity(plan.entries.len());
560    for (global_order_index, key) in plan.entries.iter().enumerate() {
561        let input = inputs_by_instance
562            .get(&key.module_instance)
563            .ok_or_else(|| TransformBundleLinkErrorV0::InvalidEmissionPlan {
564                reason: format!(
565                    "emission key refers to unknown module {}",
566                    key.module_instance.module().as_str()
567                ),
568            })?;
569        let selector = input
570            .ordered_rules
571            .get(key.intra_module_ordinal as usize)
572            .ok_or_else(|| TransformBundleLinkErrorV0::InvalidEmissionPlan {
573                reason: format!(
574                    "emission key refers to missing rule {} in {}",
575                    key.intra_module_ordinal,
576                    key.module_instance.module().as_str()
577                ),
578            })?;
579        rules.push(LinkedStylesheetRuleV0 {
580            global_order_index: u32::try_from(global_order_index).map_err(|_| {
581                TransformBundleLinkErrorV0::InvalidEmissionPlan {
582                    reason: "emission plan has more rules than the output index can represent"
583                        .to_string(),
584                }
585            })?,
586            module_instance: key.module_instance.clone(),
587            selector_name: selector.selector_name.clone(),
588            selector_kind: selector_kind_label(selector.selector_kind),
589            range_start: selector.range_start,
590            range_end: selector.range_end,
591        });
592    }
593    Ok(GlobalRuleOrderV0 { rules })
594}