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")]
16pub 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")]
24pub 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")]
36pub 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")]
46pub 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")]
66pub enum EmissionCyclePolicyV0 {
68 ModuleIdentity,
69}
70
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub 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 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")]
97pub 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")]
108pub 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}