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