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")]
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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
44#[serde(rename_all = "camelCase")]
45pub enum EmissionCyclePolicyV0 {
47 ModuleIdentity,
48}
49
50#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub enum EmissionOrderingPolicyV0 {
54 #[default]
55 ModuleIdLegacy,
56 ImportOrderPreserving,
57}
58
59impl EmissionOrderingPolicyV0 {
60 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")]
71pub 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")]
81pub 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}