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