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")]
15pub 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")]
23pub 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")]
35pub enum EmissionCycleClassV0 {
37 Import,
38 Composition,
39 Mixed,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub enum EmissionCyclePolicyV0 {
46 ModuleIdentity,
47}
48
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub enum EmissionOrderingPolicyV0 {
53 #[default]
54 ModuleIdLegacy,
55 ImportOrderPreserving,
56}
57
58impl EmissionOrderingPolicyV0 {
59 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")]
70pub 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")]
80pub 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}