1use serde::Serialize;
8use std::{
9 cmp::{Ordering, Reverse},
10 collections::{BTreeMap, BTreeSet},
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub enum CascadeLevel {
16 UserAgentNormal,
17 UserNormal,
18 AuthorNormal,
19 InlineNormal,
20 Animation,
21 AuthorImportant,
22 UserImportant,
23 UserAgentImportant,
24 Transition,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct LayerRank(pub i32);
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct Specificity {
34 pub ids: u32,
35 pub classes: u32,
36 pub elements: u32,
37}
38
39impl Specificity {
40 pub const ZERO: Self = Self {
41 ids: 0,
42 classes: 0,
43 elements: 0,
44 };
45
46 pub const fn new(ids: u32, classes: u32, elements: u32) -> Self {
47 Self {
48 ids,
49 classes,
50 elements,
51 }
52 }
53}
54
55impl Ord for Specificity {
56 fn cmp(&self, other: &Self) -> Ordering {
57 (self.ids, self.classes, self.elements).cmp(&(other.ids, other.classes, other.elements))
58 }
59}
60
61impl PartialOrd for Specificity {
62 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
63 Some(self.cmp(other))
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct CascadeKey {
70 pub level: CascadeLevel,
71 pub layer_rank: LayerRank,
72 pub scope_proximity: u32,
73 pub specificity: Specificity,
74 pub source_order: u32,
75}
76
77impl CascadeKey {
78 pub const fn new(
79 level: CascadeLevel,
80 layer_rank: LayerRank,
81 scope_proximity: u32,
82 specificity: Specificity,
83 source_order: u32,
84 ) -> Self {
85 Self {
86 level,
87 layer_rank,
88 scope_proximity,
89 specificity,
90 source_order,
91 }
92 }
93}
94
95impl Ord for CascadeKey {
96 fn cmp(&self, other: &Self) -> Ordering {
97 self.level
98 .cmp(&other.level)
99 .then_with(|| self.layer_rank.cmp(&other.layer_rank))
100 .then_with(|| other.scope_proximity.cmp(&self.scope_proximity))
101 .then_with(|| self.specificity.cmp(&other.specificity))
102 .then_with(|| self.source_order.cmp(&other.source_order))
103 }
104}
105
106impl PartialOrd for CascadeKey {
107 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
108 Some(self.cmp(other))
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
113#[serde(rename_all = "camelCase")]
114pub struct CascadeDeclaration {
115 pub id: String,
116 pub property: String,
117 pub value: CascadeValue,
118 pub key: CascadeKey,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct CascadeProof {
124 pub declaration_id: String,
125 pub property: String,
126 pub level: CascadeLevel,
127 pub layer_rank: LayerRank,
128 pub scope_proximity: u32,
129 pub specificity: Specificity,
130 pub source_order: u32,
131}
132
133impl CascadeProof {
134 pub fn from_declaration(declaration: &CascadeDeclaration) -> Self {
135 Self {
136 declaration_id: declaration.id.clone(),
137 property: declaration.property.clone(),
138 level: declaration.key.level,
139 layer_rank: declaration.key.layer_rank,
140 scope_proximity: declaration.key.scope_proximity,
141 specificity: declaration.key.specificity,
142 source_order: declaration.key.source_order,
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
148#[serde(rename_all = "camelCase")]
149pub enum CascadeOutcome {
150 Definite {
151 winner: CascadeDeclaration,
152 proof: CascadeProof,
153 also_considered: Vec<CascadeDeclaration>,
154 },
155 RankedSet(Vec<CascadeDeclaration>),
156 Inherit,
157 Top,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
161#[serde(rename_all = "camelCase")]
162pub enum CascadeValue {
163 Literal(String),
164 Var {
165 name: String,
166 fallback: Option<Box<CascadeValue>>,
167 },
168 GuaranteedInvalid,
169 Unset,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
173#[serde(rename_all = "camelCase")]
174pub enum SelectorContextMatchKind {
175 NoMatch,
176 Global,
177 Root,
178 Exact,
179 ContainsSelector,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
183#[serde(rename_all = "camelCase")]
184pub struct SelectorContextWitness {
185 pub kind: SelectorContextMatchKind,
186 pub matched: bool,
187 pub rank: usize,
188 pub declaration_selector: Option<String>,
189 pub reference_selector: Option<String>,
190}
191
192impl SelectorContextWitness {
193 pub fn no_match() -> Self {
194 Self {
195 kind: SelectorContextMatchKind::NoMatch,
196 matched: false,
197 rank: 0,
198 declaration_selector: None,
199 reference_selector: None,
200 }
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
205#[serde(rename_all = "camelCase")]
206pub struct ElementSignature {
207 pub tag: Option<String>,
208 pub id: Option<String>,
209 pub classes: BTreeSet<String>,
210 pub attributes: BTreeSet<String>,
211 pub pseudo_states: BTreeSet<String>,
212 pub classes_are_exact: bool,
213 pub attributes_are_exact: bool,
214 pub pseudo_states_are_exact: bool,
215 pub tag_is_exact: bool,
216 pub id_is_exact: bool,
217}
218
219impl ElementSignature {
220 pub fn concrete(
221 tag: Option<impl Into<String>>,
222 id: Option<impl Into<String>>,
223 classes: impl IntoIterator<Item = impl Into<String>>,
224 ) -> Self {
225 Self {
226 tag: tag.map(Into::into),
227 id: id.map(Into::into),
228 classes: classes.into_iter().map(Into::into).collect(),
229 attributes: BTreeSet::new(),
230 pseudo_states: BTreeSet::new(),
231 classes_are_exact: true,
232 attributes_are_exact: true,
233 pseudo_states_are_exact: true,
234 tag_is_exact: true,
235 id_is_exact: true,
236 }
237 }
238
239 pub fn at_least_classes(classes: impl IntoIterator<Item = impl Into<String>>) -> Self {
240 Self {
241 classes_are_exact: false,
242 ..Self::concrete(None::<String>, None::<String>, classes)
243 }
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
248#[serde(rename_all = "camelCase")]
249pub struct SelectorSignature {
250 pub selector: String,
251 pub required_tag: Option<String>,
252 pub required_id: Option<String>,
253 pub required_classes: BTreeSet<String>,
254 pub required_attributes: BTreeSet<String>,
255 pub required_pseudo_states: BTreeSet<String>,
256 pub specificity: Specificity,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
260#[serde(rename_all = "camelCase")]
261pub enum SelectorMatchVerdict {
262 No,
263 Maybe,
264 Yes,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
268#[serde(rename_all = "camelCase")]
269pub enum SelectorMatchReason {
270 Universal,
271 SimpleCompound,
272 SelectorList,
273 MissingTag,
274 MissingId,
275 MissingClass,
276 MissingAttribute,
277 MissingPseudoState,
278 UnsupportedSelector,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
282#[serde(rename_all = "camelCase")]
283pub struct SelectorMatchWitness {
284 pub selector: String,
285 pub matched_branch: Option<String>,
286 pub verdict: SelectorMatchVerdict,
287 pub reason: SelectorMatchReason,
288 pub specificity: Specificity,
289 pub missing_tag: Option<String>,
290 pub missing_id: Option<String>,
291 pub missing_classes: BTreeSet<String>,
292 pub missing_attributes: BTreeSet<String>,
293 pub missing_pseudo_states: BTreeSet<String>,
294 pub unsupported_branches: Vec<String>,
295}
296
297impl SelectorMatchWitness {
298 fn unsupported(selector: &str) -> Self {
299 Self {
300 selector: selector.to_string(),
301 matched_branch: Some(selector.to_string()),
302 verdict: SelectorMatchVerdict::Maybe,
303 reason: SelectorMatchReason::UnsupportedSelector,
304 specificity: Specificity::ZERO,
305 missing_tag: None,
306 missing_id: None,
307 missing_classes: BTreeSet::new(),
308 missing_attributes: BTreeSet::new(),
309 missing_pseudo_states: BTreeSet::new(),
310 unsupported_branches: vec![selector.to_string()],
311 }
312 }
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
316#[serde(rename_all = "camelCase")]
317pub struct CascadeBoundarySummary {
318 pub product: &'static str,
319 pub ordering_model: &'static str,
320 pub substitution_model: &'static str,
321 pub ready_surfaces: Vec<&'static str>,
322 pub not_ready_surfaces: Vec<&'static str>,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
326#[serde(rename_all = "camelCase")]
327pub struct CascadeConformanceSeedCase {
328 pub name: String,
329 pub property: &'static str,
330 pub declarations: Vec<CascadeDeclaration>,
331 pub expected_outcome: &'static str,
332 pub expected_winner_id: Option<String>,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
336#[serde(rename_all = "camelCase")]
337pub struct CascadeConformanceSeedResult {
338 pub name: String,
339 pub passed: bool,
340 pub expected_outcome: &'static str,
341 pub actual_outcome: &'static str,
342 pub expected_winner_id: Option<String>,
343 pub actual_winner_id: Option<String>,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
347#[serde(rename_all = "camelCase")]
348pub struct CascadeConformanceSeedReport {
349 pub schema_version: &'static str,
350 pub product: &'static str,
351 pub case_count: usize,
352 pub passed_count: usize,
353 pub failed_count: usize,
354 pub results: Vec<CascadeConformanceSeedResult>,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
358#[serde(rename_all = "camelCase")]
359pub struct CascadeEvaluationFuzzCaseV0 {
360 pub seed: u64,
361 pub declaration_count: usize,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
365#[serde(rename_all = "camelCase")]
366pub struct CascadeEvaluationFuzzResultV0 {
367 pub seed: u64,
368 pub declaration_count: usize,
369 pub actual_winner_id: Option<String>,
370 pub expected_winner_id: Option<String>,
371 pub ranked_count: usize,
372 pub passed: bool,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
376#[serde(rename_all = "camelCase")]
377pub struct VarSubstitutionFuzzCaseV0 {
378 pub seed: u64,
379 pub chain_len: usize,
380 pub cycle: bool,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
384#[serde(rename_all = "camelCase")]
385pub struct VarSubstitutionFuzzResultV0 {
386 pub seed: u64,
387 pub chain_len: usize,
388 pub cycle: bool,
389 pub result: CascadeValue,
390 pub expected: CascadeValue,
391 pub passed: bool,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
395#[serde(rename_all = "camelCase")]
396pub struct CascadeFuzzSeedReportV0 {
397 pub schema_version: &'static str,
398 pub product: &'static str,
399 pub case_count: usize,
400 pub passed_count: usize,
401 pub failed_count: usize,
402 pub cascade_results: Vec<CascadeEvaluationFuzzResultV0>,
403 pub var_results: Vec<VarSubstitutionFuzzResultV0>,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
407#[serde(rename_all = "camelCase")]
408pub struct BoxLonghandInputV0 {
409 pub property: String,
410 pub value: String,
411 pub important: bool,
412 pub source_order: u32,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
416#[serde(rename_all = "camelCase")]
417pub struct ShorthandCombinationProofV0 {
418 pub schema_version: &'static str,
419 pub product: &'static str,
420 pub shorthand_property: String,
421 pub accepted: bool,
422 pub blocked_reason: Option<&'static str>,
423 pub ordered_longhand_properties: Vec<String>,
424 pub provenance_preserved: bool,
425 pub cascade_safe_witness: String,
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
429#[serde(rename_all = "camelCase")]
430pub enum StaticSupportsAssumptionV0 {
431 ModernBrowser,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
435#[serde(rename_all = "camelCase")]
436pub enum StaticSupportsEvalVerdictV0 {
437 AlwaysTrue,
438 AlwaysFalse,
439 Unknown,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
443#[serde(rename_all = "camelCase")]
444pub struct StaticSupportsEvalWitnessV0 {
445 pub schema_version: &'static str,
446 pub product: &'static str,
447 pub condition: String,
448 pub assumption: StaticSupportsAssumptionV0,
449 pub verdict: StaticSupportsEvalVerdictV0,
450 pub reason: &'static str,
451 pub provenance_preserved: bool,
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
455#[serde(rename_all = "camelCase")]
456pub struct ScopeFlattenInputV0 {
457 pub root_selector: String,
458 pub limit_selector: Option<String>,
459 pub scoped_rule_count: usize,
460 pub peer_scope_count: usize,
461 pub competing_unscoped_rule_count: usize,
462 pub inside_layer: bool,
463}
464
465#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
466#[serde(rename_all = "camelCase")]
467pub struct ScopeFlattenProofV0 {
468 pub schema_version: &'static str,
469 pub product: &'static str,
470 pub accepted: bool,
471 pub blocked_reason: Option<&'static str>,
472 pub root_selector: String,
473 pub provenance_preserved: bool,
474 pub cascade_safe_witness: String,
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
478#[serde(rename_all = "camelCase")]
479pub struct LayerFlattenInputV0 {
480 pub layer_name: Option<String>,
481 pub layer_rule_count: usize,
482 pub peer_layer_count: usize,
483 pub unlayered_rule_count: usize,
484 pub important_declaration_count: usize,
485 pub closed_bundle: bool,
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
489#[serde(rename_all = "camelCase")]
490pub struct LayerFlattenProofV0 {
491 pub schema_version: &'static str,
492 pub product: &'static str,
493 pub accepted: bool,
494 pub blocked_reason: Option<&'static str>,
495 pub layer_name: Option<String>,
496 pub provenance_preserved: bool,
497 pub cascade_safe_witness: String,
498}
499
500pub type CustomPropertyEnv = BTreeMap<String, CascadeValue>;
501
502pub fn summarize_cascade_boundary() -> CascadeBoundarySummary {
503 CascadeBoundarySummary {
504 product: "omena-cascade.boundary",
505 ordering_model: "lexicographicCascadeKey",
506 substitution_model: "finiteCustomPropertyLeastFixedPoint",
507 ready_surfaces: vec![
508 "cascadeKeyOrdering",
509 "specificityOrdering",
510 "cascadeOutcomeProof",
511 "genericCascadeWinner",
512 "semanticDesignTokenRanking",
513 "queryReadCascadeAtPosition",
514 "selectorContextWitness",
515 "selectorMatchWitness",
516 "cascadeConformanceSeedCorpus",
517 "customPropertySubstitution",
518 "cycleToGuaranteedInvalid",
519 "shorthandCombinationProof",
520 "supportsStaticEvalWitness",
521 "scopeFlattenProof",
522 "layerFlattenProof",
523 "wptCascadeSeedCorpus",
524 ],
525 not_ready_surfaces: vec!["fullWptCascadeCorpus"],
526 }
527}
528
529pub fn run_cascade_conformance_seed_corpus() -> CascadeConformanceSeedReport {
530 let results = cascade_conformance_seed_cases()
531 .into_iter()
532 .map(run_cascade_conformance_seed_case)
533 .collect::<Vec<_>>();
534 let passed_count = results.iter().filter(|result| result.passed).count();
535 let case_count = results.len();
536
537 CascadeConformanceSeedReport {
538 schema_version: "0",
539 product: "omena-cascade.conformance-seed-corpus",
540 case_count,
541 passed_count,
542 failed_count: case_count.saturating_sub(passed_count),
543 results,
544 }
545}
546
547pub fn run_wpt_cascade_seed_corpus() -> CascadeConformanceSeedReport {
548 let results = wpt_cascade_seed_cases()
549 .into_iter()
550 .map(run_cascade_conformance_seed_case)
551 .collect::<Vec<_>>();
552 let passed_count = results.iter().filter(|result| result.passed).count();
553 let case_count = results.len();
554
555 CascadeConformanceSeedReport {
556 schema_version: "0",
557 product: "omena-cascade.wpt-cascade-seed-corpus",
558 case_count,
559 passed_count,
560 failed_count: case_count.saturating_sub(passed_count),
561 results,
562 }
563}
564
565fn run_cascade_conformance_seed_case(
566 case: CascadeConformanceSeedCase,
567) -> CascadeConformanceSeedResult {
568 let outcome = cascade_property(case.declarations, case.property);
569 let (actual_outcome, actual_winner_id) = match outcome {
570 CascadeOutcome::Definite { winner, .. } => ("definite", Some(winner.id)),
571 CascadeOutcome::RankedSet(_) => ("rankedSet", None),
572 CascadeOutcome::Inherit => ("inherit", None),
573 CascadeOutcome::Top => ("top", None),
574 };
575 let passed =
576 actual_outcome == case.expected_outcome && actual_winner_id == case.expected_winner_id;
577
578 CascadeConformanceSeedResult {
579 name: case.name,
580 passed,
581 expected_outcome: case.expected_outcome,
582 actual_outcome,
583 expected_winner_id: case.expected_winner_id,
584 actual_winner_id,
585 }
586}
587
588fn cascade_conformance_seed_cases() -> Vec<CascadeConformanceSeedCase> {
589 vec![
590 CascadeConformanceSeedCase {
591 name: "source-order-breaks-identical-key".to_string(),
592 property: "color",
593 declarations: vec![
594 conformance_decl(
595 "source-earlier",
596 "color",
597 "red",
598 conformance_key(
599 CascadeLevel::AuthorNormal,
600 0,
601 0,
602 Specificity::new(0, 1, 0),
603 1,
604 ),
605 ),
606 conformance_decl(
607 "source-later",
608 "color",
609 "blue",
610 conformance_key(
611 CascadeLevel::AuthorNormal,
612 0,
613 0,
614 Specificity::new(0, 1, 0),
615 2,
616 ),
617 ),
618 ],
619 expected_outcome: "definite",
620 expected_winner_id: Some("source-later".to_string()),
621 },
622 CascadeConformanceSeedCase {
623 name: "specificity-beats-source-order".to_string(),
624 property: "color",
625 declarations: vec![
626 conformance_decl(
627 "specificity-low-later",
628 "color",
629 "red",
630 conformance_key(
631 CascadeLevel::AuthorNormal,
632 0,
633 0,
634 Specificity::new(0, 1, 0),
635 2,
636 ),
637 ),
638 conformance_decl(
639 "specificity-high-earlier",
640 "color",
641 "blue",
642 conformance_key(
643 CascadeLevel::AuthorNormal,
644 0,
645 0,
646 Specificity::new(1, 0, 0),
647 1,
648 ),
649 ),
650 ],
651 expected_outcome: "definite",
652 expected_winner_id: Some("specificity-high-earlier".to_string()),
653 },
654 CascadeConformanceSeedCase {
655 name: "important-origin-beats-inline-normal".to_string(),
656 property: "color",
657 declarations: vec![
658 conformance_decl(
659 "inline-normal",
660 "color",
661 "red",
662 conformance_key(
663 CascadeLevel::InlineNormal,
664 0,
665 0,
666 Specificity::new(1, 0, 0),
667 2,
668 ),
669 ),
670 conformance_decl(
671 "author-important",
672 "color",
673 "blue",
674 conformance_key(
675 CascadeLevel::AuthorImportant,
676 0,
677 0,
678 Specificity::new(0, 1, 0),
679 1,
680 ),
681 ),
682 ],
683 expected_outcome: "definite",
684 expected_winner_id: Some("author-important".to_string()),
685 },
686 CascadeConformanceSeedCase {
687 name: "layer-rank-beats-specificity-within-level".to_string(),
688 property: "color",
689 declarations: vec![
690 conformance_decl(
691 "lower-layer-specific",
692 "color",
693 "red",
694 conformance_key(
695 CascadeLevel::AuthorNormal,
696 1,
697 0,
698 Specificity::new(1, 0, 0),
699 2,
700 ),
701 ),
702 conformance_decl(
703 "higher-layer",
704 "color",
705 "blue",
706 conformance_key(
707 CascadeLevel::AuthorNormal,
708 2,
709 0,
710 Specificity::new(0, 1, 0),
711 1,
712 ),
713 ),
714 ],
715 expected_outcome: "definite",
716 expected_winner_id: Some("higher-layer".to_string()),
717 },
718 CascadeConformanceSeedCase {
719 name: "scope-proximity-beats-specificity-tie".to_string(),
720 property: "color",
721 declarations: vec![
722 conformance_decl(
723 "far-scope",
724 "color",
725 "red",
726 conformance_key(
727 CascadeLevel::AuthorNormal,
728 0,
729 5,
730 Specificity::new(0, 1, 0),
731 2,
732 ),
733 ),
734 conformance_decl(
735 "near-scope",
736 "color",
737 "blue",
738 conformance_key(
739 CascadeLevel::AuthorNormal,
740 0,
741 1,
742 Specificity::new(0, 1, 0),
743 1,
744 ),
745 ),
746 ],
747 expected_outcome: "definite",
748 expected_winner_id: Some("near-scope".to_string()),
749 },
750 CascadeConformanceSeedCase {
751 name: "missing-property-inherits".to_string(),
752 property: "background",
753 declarations: vec![conformance_decl(
754 "color-only",
755 "color",
756 "red",
757 conformance_key(
758 CascadeLevel::AuthorNormal,
759 0,
760 0,
761 Specificity::new(0, 1, 0),
762 1,
763 ),
764 )],
765 expected_outcome: "inherit",
766 expected_winner_id: None,
767 },
768 ]
769}
770
771fn wpt_cascade_seed_cases() -> Vec<CascadeConformanceSeedCase> {
772 let levels = [
773 CascadeLevel::UserAgentNormal,
774 CascadeLevel::UserNormal,
775 CascadeLevel::AuthorNormal,
776 CascadeLevel::InlineNormal,
777 CascadeLevel::Animation,
778 CascadeLevel::AuthorImportant,
779 CascadeLevel::UserImportant,
780 CascadeLevel::UserAgentImportant,
781 CascadeLevel::Transition,
782 ];
783 let specificities = [
784 Specificity::new(0, 0, 1),
785 Specificity::new(0, 1, 0),
786 Specificity::new(1, 0, 0),
787 ];
788
789 let mut cases = Vec::new();
790
791 for left in levels {
792 for right in levels {
793 if left == right {
794 continue;
795 }
796
797 let winner = if left > right { "left" } else { "right" };
798 cases.push(CascadeConformanceSeedCase {
799 name: format!("wpt-origin-importance-order-{left:?}-vs-{right:?}"),
800 property: "color",
801 declarations: vec![
802 conformance_decl(
803 "left",
804 "color",
805 "red",
806 conformance_key(left, 0, 0, Specificity::new(0, 1, 0), 1),
807 ),
808 conformance_decl(
809 "right",
810 "color",
811 "blue",
812 conformance_key(right, 0, 0, Specificity::new(0, 1, 0), 2),
813 ),
814 ],
815 expected_outcome: "definite",
816 expected_winner_id: Some(winner.to_string()),
817 });
818 }
819 }
820
821 for layer_left in -3..=3 {
822 for layer_right in -3..=3 {
823 if layer_left == layer_right {
824 continue;
825 }
826
827 let winner = if layer_left > layer_right {
828 "left"
829 } else {
830 "right"
831 };
832 cases.push(CascadeConformanceSeedCase {
833 name: format!("wpt-layer-order-{layer_left}-vs-{layer_right}"),
834 property: "color",
835 declarations: vec![
836 conformance_decl(
837 "left",
838 "color",
839 "red",
840 conformance_key(
841 CascadeLevel::AuthorNormal,
842 layer_left,
843 0,
844 Specificity::new(0, 1, 0),
845 2,
846 ),
847 ),
848 conformance_decl(
849 "right",
850 "color",
851 "blue",
852 conformance_key(
853 CascadeLevel::AuthorNormal,
854 layer_right,
855 0,
856 Specificity::new(1, 0, 0),
857 1,
858 ),
859 ),
860 ],
861 expected_outcome: "definite",
862 expected_winner_id: Some(winner.to_string()),
863 });
864 }
865 }
866
867 for scope_left in 0..=7 {
868 for scope_right in 0..=7 {
869 if scope_left == scope_right {
870 continue;
871 }
872
873 let winner = if scope_left < scope_right {
874 "left"
875 } else {
876 "right"
877 };
878 cases.push(CascadeConformanceSeedCase {
879 name: format!("wpt-scope-proximity-{scope_left}-vs-{scope_right}"),
880 property: "color",
881 declarations: vec![
882 conformance_decl(
883 "left",
884 "color",
885 "red",
886 conformance_key(
887 CascadeLevel::AuthorNormal,
888 0,
889 scope_left,
890 Specificity::new(0, 1, 0),
891 2,
892 ),
893 ),
894 conformance_decl(
895 "right",
896 "color",
897 "blue",
898 conformance_key(
899 CascadeLevel::AuthorNormal,
900 0,
901 scope_right,
902 Specificity::new(0, 1, 0),
903 1,
904 ),
905 ),
906 ],
907 expected_outcome: "definite",
908 expected_winner_id: Some(winner.to_string()),
909 });
910 }
911 }
912
913 for left in specificities {
914 for right in specificities {
915 if left == right {
916 continue;
917 }
918
919 let winner = if left > right { "left" } else { "right" };
920 cases.push(CascadeConformanceSeedCase {
921 name: format!("wpt-specificity-order-{left:?}-vs-{right:?}"),
922 property: "color",
923 declarations: vec![
924 conformance_decl(
925 "left",
926 "color",
927 "red",
928 conformance_key(CascadeLevel::AuthorNormal, 0, 0, left, 1),
929 ),
930 conformance_decl(
931 "right",
932 "color",
933 "blue",
934 conformance_key(CascadeLevel::AuthorNormal, 0, 0, right, 2),
935 ),
936 ],
937 expected_outcome: "definite",
938 expected_winner_id: Some(winner.to_string()),
939 });
940 }
941 }
942
943 for source_left in 0..=15 {
944 for source_right in 0..=15 {
945 if source_left == source_right {
946 continue;
947 }
948
949 let winner = if source_left > source_right {
950 "left"
951 } else {
952 "right"
953 };
954 cases.push(CascadeConformanceSeedCase {
955 name: format!("wpt-source-order-{source_left}-vs-{source_right}"),
956 property: "color",
957 declarations: vec![
958 conformance_decl(
959 "left",
960 "color",
961 "red",
962 conformance_key(
963 CascadeLevel::AuthorNormal,
964 0,
965 0,
966 Specificity::new(0, 1, 0),
967 source_left,
968 ),
969 ),
970 conformance_decl(
971 "right",
972 "color",
973 "blue",
974 conformance_key(
975 CascadeLevel::AuthorNormal,
976 0,
977 0,
978 Specificity::new(0, 1, 0),
979 source_right,
980 ),
981 ),
982 ],
983 expected_outcome: "definite",
984 expected_winner_id: Some(winner.to_string()),
985 });
986 }
987 }
988
989 cases
990}
991
992fn conformance_key(
993 level: CascadeLevel,
994 layer_rank: i32,
995 scope_proximity: u32,
996 specificity: Specificity,
997 source_order: u32,
998) -> CascadeKey {
999 CascadeKey::new(
1000 level,
1001 LayerRank(layer_rank),
1002 scope_proximity,
1003 specificity,
1004 source_order,
1005 )
1006}
1007
1008fn conformance_decl(id: &str, property: &str, value: &str, key: CascadeKey) -> CascadeDeclaration {
1009 CascadeDeclaration {
1010 id: id.to_string(),
1011 property: property.to_string(),
1012 value: CascadeValue::Literal(value.to_string()),
1013 key,
1014 }
1015}
1016
1017pub fn cascade_property(
1018 declarations: impl IntoIterator<Item = CascadeDeclaration>,
1019 property: &str,
1020) -> CascadeOutcome {
1021 let mut matching: Vec<CascadeDeclaration> = declarations
1022 .into_iter()
1023 .filter(|declaration| declaration.property == property)
1024 .collect();
1025
1026 if matching.is_empty() {
1027 return CascadeOutcome::Inherit;
1028 }
1029
1030 matching.sort_by_key(|declaration| std::cmp::Reverse(declaration.key));
1031 let winner = matching.remove(0);
1032 let proof = CascadeProof::from_declaration(&winner);
1033 CascadeOutcome::Definite {
1034 winner,
1035 proof,
1036 also_considered: matching,
1037 }
1038}
1039
1040pub fn run_cascade_evaluation_fuzz_case(
1041 case: CascadeEvaluationFuzzCaseV0,
1042) -> CascadeEvaluationFuzzResultV0 {
1043 let declaration_count = case.declaration_count.clamp(1, 64);
1044 let declarations = generated_cascade_fuzz_declarations(case.seed, declaration_count);
1045 let matching = declarations
1046 .iter()
1047 .filter(|declaration| declaration.property == "color")
1048 .cloned()
1049 .collect::<Vec<_>>();
1050 let expected_winner_id = rank_cascade_items(matching.clone(), |declaration| declaration.key)
1051 .first()
1052 .map(|declaration| declaration.id.clone());
1053 let actual = cascade_property(declarations, "color");
1054 let actual_winner_id = match actual {
1055 CascadeOutcome::Definite { winner, .. } => Some(winner.id),
1056 _ => None,
1057 };
1058 let ranked_count = matching.len();
1059 let passed = actual_winner_id == expected_winner_id && ranked_count > 0;
1060
1061 CascadeEvaluationFuzzResultV0 {
1062 seed: case.seed,
1063 declaration_count,
1064 actual_winner_id,
1065 expected_winner_id,
1066 ranked_count,
1067 passed,
1068 }
1069}
1070
1071pub fn run_var_substitution_fuzz_case(
1072 case: VarSubstitutionFuzzCaseV0,
1073) -> VarSubstitutionFuzzResultV0 {
1074 let chain_len = case.chain_len.clamp(1, 32);
1075 let mut env = CustomPropertyEnv::new();
1076 let terminal = CascadeValue::Literal(format!("seed-{}", case.seed));
1077
1078 for index in 0..chain_len {
1079 let name = fuzz_var_name(index);
1080 let next_value = if index == 0 && !case.cycle {
1081 terminal.clone()
1082 } else if index == 0 {
1083 CascadeValue::Var {
1084 name: fuzz_var_name(chain_len - 1),
1085 fallback: Some(Box::new(CascadeValue::Literal(
1086 "cycle-fallback".to_string(),
1087 ))),
1088 }
1089 } else {
1090 CascadeValue::Var {
1091 name: fuzz_var_name(index - 1),
1092 fallback: None,
1093 }
1094 };
1095 env.insert(name, next_value);
1096 }
1097
1098 let input = CascadeValue::Var {
1099 name: fuzz_var_name(chain_len - 1),
1100 fallback: Some(Box::new(CascadeValue::Literal(
1101 "outer-fallback".to_string(),
1102 ))),
1103 };
1104 let result = substitute_custom_properties(&input, &env);
1105 let expected = if case.cycle {
1106 CascadeValue::GuaranteedInvalid
1107 } else {
1108 terminal
1109 };
1110 let passed = result == expected;
1111
1112 VarSubstitutionFuzzResultV0 {
1113 seed: case.seed,
1114 chain_len,
1115 cycle: case.cycle,
1116 result,
1117 expected,
1118 passed,
1119 }
1120}
1121
1122pub fn run_cascade_fuzz_seed_corpus() -> CascadeFuzzSeedReportV0 {
1123 let seeds = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144];
1124 let cascade_results = seeds
1125 .into_iter()
1126 .enumerate()
1127 .map(|(index, seed)| {
1128 run_cascade_evaluation_fuzz_case(CascadeEvaluationFuzzCaseV0 {
1129 seed,
1130 declaration_count: index + 1,
1131 })
1132 })
1133 .collect::<Vec<_>>();
1134 let var_results = seeds
1135 .into_iter()
1136 .enumerate()
1137 .map(|(index, seed)| {
1138 run_var_substitution_fuzz_case(VarSubstitutionFuzzCaseV0 {
1139 seed,
1140 chain_len: index + 1,
1141 cycle: index % 3 == 0,
1142 })
1143 })
1144 .collect::<Vec<_>>();
1145 let passed_count = cascade_results
1146 .iter()
1147 .filter(|result| result.passed)
1148 .count()
1149 + var_results.iter().filter(|result| result.passed).count();
1150 let case_count = cascade_results.len() + var_results.len();
1151
1152 CascadeFuzzSeedReportV0 {
1153 schema_version: "0",
1154 product: "omena-cascade.fuzz-seed-corpus",
1155 case_count,
1156 passed_count,
1157 failed_count: case_count - passed_count,
1158 cascade_results,
1159 var_results,
1160 }
1161}
1162
1163pub fn rank_cascade_items<T>(
1164 items: impl IntoIterator<Item = T>,
1165 key_for: impl Fn(&T) -> CascadeKey,
1166) -> Vec<T> {
1167 let mut ranked = items.into_iter().collect::<Vec<_>>();
1168 ranked.sort_by_key(|item| Reverse(key_for(item)));
1169 ranked
1170}
1171
1172pub fn select_cascade_winner<T>(
1173 items: impl IntoIterator<Item = T>,
1174 key_for: impl Fn(&T) -> CascadeKey,
1175) -> Option<(T, Vec<T>)> {
1176 let mut ranked = rank_cascade_items(items, key_for);
1177 if ranked.is_empty() {
1178 return None;
1179 }
1180
1181 let winner = ranked.remove(0);
1182 Some((winner, ranked))
1183}
1184
1185pub fn prove_box_shorthand_combination(
1186 shorthand_property: &str,
1187 longhands: &[BoxLonghandInputV0],
1188) -> ShorthandCombinationProofV0 {
1189 let expected = match box_shorthand_longhands(shorthand_property) {
1190 Some(expected) => expected,
1191 None => {
1192 return shorthand_combination_proof(
1193 shorthand_property,
1194 false,
1195 Some("unsupported shorthand property"),
1196 longhands,
1197 "",
1198 );
1199 }
1200 };
1201
1202 if longhands.len() != expected.len() {
1203 return shorthand_combination_proof(
1204 shorthand_property,
1205 false,
1206 Some("incomplete longhand quartet"),
1207 longhands,
1208 "",
1209 );
1210 }
1211
1212 if longhands
1213 .iter()
1214 .zip(expected.iter())
1215 .any(|(actual, expected)| actual.property != *expected)
1216 {
1217 return shorthand_combination_proof(
1218 shorthand_property,
1219 false,
1220 Some("longhands are not in canonical top/right/bottom/left order"),
1221 longhands,
1222 "",
1223 );
1224 }
1225
1226 if longhands.iter().any(|longhand| longhand.important) {
1227 return shorthand_combination_proof(
1228 shorthand_property,
1229 false,
1230 Some("important longhands require explicit cascade equivalence proof"),
1231 longhands,
1232 "",
1233 );
1234 }
1235
1236 if longhands.iter().any(|longhand| longhand.value.is_empty()) {
1237 return shorthand_combination_proof(
1238 shorthand_property,
1239 false,
1240 Some("empty longhand value"),
1241 longhands,
1242 "",
1243 );
1244 }
1245
1246 if longhands
1247 .windows(2)
1248 .any(|pair| pair[1].source_order != pair[0].source_order + 1)
1249 {
1250 return shorthand_combination_proof(
1251 shorthand_property,
1252 false,
1253 Some("intervening declaration may change cascade outcome"),
1254 longhands,
1255 "",
1256 );
1257 }
1258
1259 shorthand_combination_proof(
1260 shorthand_property,
1261 true,
1262 None,
1263 longhands,
1264 "all four longhands are adjacent, non-important, and in canonical order",
1265 )
1266}
1267
1268pub fn evaluate_static_supports_condition(
1269 condition: &str,
1270 assumption: StaticSupportsAssumptionV0,
1271) -> StaticSupportsEvalWitnessV0 {
1272 let normalized_condition = normalize_ascii_whitespace(condition);
1273 let (verdict, reason) = match assumption {
1274 StaticSupportsAssumptionV0::ModernBrowser => {
1275 if parse_not_simple_supports_declaration(&normalized_condition).is_some() {
1276 (
1277 StaticSupportsEvalVerdictV0::AlwaysFalse,
1278 "modern-browser assumption rejects negated simple declaration feature queries",
1279 )
1280 } else if parse_simple_supports_declaration(&normalized_condition).is_some() {
1281 (
1282 StaticSupportsEvalVerdictV0::AlwaysTrue,
1283 "modern-browser assumption accepts simple declaration feature queries",
1284 )
1285 } else {
1286 (
1287 StaticSupportsEvalVerdictV0::Unknown,
1288 "unsupported supports condition shape",
1289 )
1290 }
1291 }
1292 };
1293
1294 StaticSupportsEvalWitnessV0 {
1295 schema_version: "0",
1296 product: "omena-cascade.supports-static-eval",
1297 condition: normalized_condition,
1298 assumption,
1299 verdict,
1300 reason,
1301 provenance_preserved: verdict != StaticSupportsEvalVerdictV0::Unknown,
1302 }
1303}
1304
1305pub fn prove_scope_flatten_candidate(input: ScopeFlattenInputV0) -> ScopeFlattenProofV0 {
1306 let blocked_reason = if input.limit_selector.is_some() {
1307 Some("scope limit selector cannot be encoded by the conservative flatten predicate")
1308 } else if input.root_selector.trim() != ":root" {
1309 Some("non-root scope flattening requires selector/proximity equivalence proof")
1310 } else if input.peer_scope_count > 0 {
1311 Some("peer scopes may change scope-proximity cascade ordering")
1312 } else if input.competing_unscoped_rule_count > 0 {
1313 Some("unscoped competitors may observe changed scope-proximity ordering")
1314 } else if input.inside_layer {
1315 Some("layer plus scope composition requires product cascade proof")
1316 } else {
1317 None
1318 };
1319 let accepted = blocked_reason.is_none();
1320 ScopeFlattenProofV0 {
1321 schema_version: "0",
1322 product: "omena-cascade.scope-flatten-proof",
1323 accepted,
1324 blocked_reason,
1325 root_selector: input.root_selector,
1326 provenance_preserved: accepted,
1327 cascade_safe_witness: if accepted {
1328 "root scope without limit, peer scopes, unscoped competition, or layer context"
1329 } else {
1330 "scope proximity cannot be erased by local syntax alone"
1331 }
1332 .to_string(),
1333 }
1334}
1335
1336pub fn prove_layer_flatten_candidate(input: LayerFlattenInputV0) -> LayerFlattenProofV0 {
1337 let blocked_reason = if !input.closed_bundle {
1338 Some("layer flattening requires a closed bundle witness")
1339 } else if input.peer_layer_count > 0 {
1340 Some("peer layers may change layer-rank cascade ordering")
1341 } else if input.unlayered_rule_count > 0 {
1342 Some("unlayered rules compete differently from layered normal rules")
1343 } else if input.important_declaration_count > 0 {
1344 Some("important declarations invert layer ordering")
1345 } else {
1346 None
1347 };
1348 let accepted = blocked_reason.is_none();
1349 LayerFlattenProofV0 {
1350 schema_version: "0",
1351 product: "omena-cascade.layer-flatten-proof",
1352 accepted,
1353 blocked_reason,
1354 layer_name: input.layer_name,
1355 provenance_preserved: accepted,
1356 cascade_safe_witness: if accepted {
1357 "closed bundle with a single non-important layer and no unlayered competitors"
1358 } else {
1359 "layer rank cannot be erased by local syntax alone"
1360 }
1361 .to_string(),
1362 }
1363}
1364
1365fn parse_not_simple_supports_declaration(condition: &str) -> Option<(&str, &str)> {
1366 let inner = condition.strip_prefix("not ")?;
1367 parse_simple_supports_declaration(inner)
1368}
1369
1370fn parse_simple_supports_declaration(condition: &str) -> Option<(&str, &str)> {
1371 let inner = condition.strip_prefix('(')?.strip_suffix(')')?.trim();
1372 let (property, value) = inner.split_once(':')?;
1373 let property = property.trim();
1374 let value = value.trim();
1375 if property.is_empty()
1376 || value.is_empty()
1377 || property.contains(|ch: char| !is_supports_declaration_token_char(ch))
1378 || value.contains(['{', '}', ';', '(', ')'])
1379 {
1380 return None;
1381 }
1382 Some((property, value))
1383}
1384
1385fn is_supports_declaration_token_char(ch: char) -> bool {
1386 ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_')
1387}
1388
1389fn normalize_ascii_whitespace(text: &str) -> String {
1390 text.split_whitespace().collect::<Vec<_>>().join(" ")
1391}
1392
1393fn box_shorthand_longhands(shorthand_property: &str) -> Option<[&'static str; 4]> {
1394 match shorthand_property {
1395 "margin" => Some(["margin-top", "margin-right", "margin-bottom", "margin-left"]),
1396 "padding" => Some([
1397 "padding-top",
1398 "padding-right",
1399 "padding-bottom",
1400 "padding-left",
1401 ]),
1402 _ => None,
1403 }
1404}
1405
1406fn shorthand_combination_proof(
1407 shorthand_property: &str,
1408 accepted: bool,
1409 blocked_reason: Option<&'static str>,
1410 longhands: &[BoxLonghandInputV0],
1411 witness: &str,
1412) -> ShorthandCombinationProofV0 {
1413 ShorthandCombinationProofV0 {
1414 schema_version: "0",
1415 product: "omena-cascade.shorthand-combination-proof",
1416 shorthand_property: shorthand_property.to_string(),
1417 accepted,
1418 blocked_reason,
1419 ordered_longhand_properties: longhands
1420 .iter()
1421 .map(|longhand| longhand.property.clone())
1422 .collect(),
1423 provenance_preserved: accepted,
1424 cascade_safe_witness: witness.to_string(),
1425 }
1426}
1427
1428pub fn selector_context_witness(
1429 declaration_selectors: &[String],
1430 reference_selectors: &[String],
1431) -> SelectorContextWitness {
1432 if declaration_selectors.is_empty() {
1433 return SelectorContextWitness {
1434 kind: SelectorContextMatchKind::Global,
1435 matched: true,
1436 rank: 1,
1437 declaration_selector: None,
1438 reference_selector: None,
1439 };
1440 }
1441
1442 let mut best = SelectorContextWitness::no_match();
1443 for declaration_selector in declaration_selectors {
1444 let candidate = selector_context_witness_for_declaration(
1445 declaration_selector.as_str(),
1446 reference_selectors,
1447 );
1448 if candidate.rank > best.rank {
1449 best = candidate;
1450 }
1451 }
1452 best
1453}
1454
1455pub fn selector_context_witness_for_declaration(
1456 declaration_selector: &str,
1457 reference_selectors: &[String],
1458) -> SelectorContextWitness {
1459 if declaration_selector == ":root" {
1460 return SelectorContextWitness {
1461 kind: SelectorContextMatchKind::Root,
1462 matched: true,
1463 rank: 1,
1464 declaration_selector: Some(declaration_selector.to_string()),
1465 reference_selector: None,
1466 };
1467 }
1468
1469 for reference_selector in reference_selectors {
1470 if reference_selector == declaration_selector {
1471 return SelectorContextWitness {
1472 kind: SelectorContextMatchKind::Exact,
1473 matched: true,
1474 rank: 2,
1475 declaration_selector: Some(declaration_selector.to_string()),
1476 reference_selector: Some(reference_selector.clone()),
1477 };
1478 }
1479 }
1480
1481 for reference_selector in reference_selectors {
1482 if reference_selector.contains(declaration_selector) {
1483 return SelectorContextWitness {
1484 kind: SelectorContextMatchKind::ContainsSelector,
1485 matched: true,
1486 rank: 2,
1487 declaration_selector: Some(declaration_selector.to_string()),
1488 reference_selector: Some(reference_selector.clone()),
1489 };
1490 }
1491 }
1492
1493 SelectorContextWitness {
1494 kind: SelectorContextMatchKind::NoMatch,
1495 matched: false,
1496 rank: 0,
1497 declaration_selector: Some(declaration_selector.to_string()),
1498 reference_selector: None,
1499 }
1500}
1501
1502pub fn selector_match_witness(selector: &str, element: &ElementSignature) -> SelectorMatchWitness {
1503 let branches = split_selector_list(selector);
1504 if branches.is_empty() {
1505 return SelectorMatchWitness::unsupported(selector);
1506 }
1507
1508 let mut witnesses = branches
1509 .iter()
1510 .map(|branch| selector_match_branch_witness(branch, element))
1511 .collect::<Vec<_>>();
1512
1513 let yes = strongest_by_verdict(&witnesses, SelectorMatchVerdict::Yes);
1514 if let Some(index) = yes {
1515 let mut witness = witnesses.remove(index);
1516 witness.selector = selector.to_string();
1517 if branches.len() > 1 {
1518 witness.reason = SelectorMatchReason::SelectorList;
1519 witness.unsupported_branches = witnesses
1520 .into_iter()
1521 .flat_map(|witness| witness.unsupported_branches)
1522 .collect();
1523 }
1524 return witness;
1525 }
1526
1527 let maybe = strongest_by_verdict(&witnesses, SelectorMatchVerdict::Maybe);
1528 if let Some(index) = maybe {
1529 let mut witness = witnesses.remove(index);
1530 witness.selector = selector.to_string();
1531 if branches.len() > 1 {
1532 witness.reason = SelectorMatchReason::SelectorList;
1533 witness.unsupported_branches = witnesses
1534 .into_iter()
1535 .flat_map(|witness| witness.unsupported_branches)
1536 .collect();
1537 }
1538 return witness;
1539 }
1540
1541 let mut witness = witnesses
1542 .into_iter()
1543 .max_by(|left, right| left.specificity.cmp(&right.specificity))
1544 .unwrap_or_else(|| SelectorMatchWitness::unsupported(selector));
1545 witness.selector = selector.to_string();
1546 if branches.len() > 1 {
1547 witness.reason = SelectorMatchReason::SelectorList;
1548 }
1549 witness
1550}
1551
1552pub fn parse_simple_selector_signature(selector: &str) -> Option<SelectorSignature> {
1553 parse_simple_selector_signature_inner(selector.trim())
1554}
1555
1556fn selector_match_branch_witness(
1557 selector: &str,
1558 element: &ElementSignature,
1559) -> SelectorMatchWitness {
1560 let Some(signature) = parse_simple_selector_signature(selector) else {
1561 return SelectorMatchWitness::unsupported(selector);
1562 };
1563
1564 let mut witness = SelectorMatchWitness {
1565 selector: selector.to_string(),
1566 matched_branch: Some(selector.to_string()),
1567 verdict: SelectorMatchVerdict::Yes,
1568 reason: if signature.required_tag.is_none()
1569 && signature.required_id.is_none()
1570 && signature.required_classes.is_empty()
1571 && signature.required_attributes.is_empty()
1572 && signature.required_pseudo_states.is_empty()
1573 {
1574 SelectorMatchReason::Universal
1575 } else {
1576 SelectorMatchReason::SimpleCompound
1577 },
1578 specificity: signature.specificity,
1579 missing_tag: None,
1580 missing_id: None,
1581 missing_classes: BTreeSet::new(),
1582 missing_attributes: BTreeSet::new(),
1583 missing_pseudo_states: BTreeSet::new(),
1584 unsupported_branches: Vec::new(),
1585 };
1586
1587 if let Some(required_tag) = &signature.required_tag {
1588 match element.tag.as_deref() {
1589 Some(tag) if tag == required_tag => {}
1590 _ if !element.tag_is_exact => {
1591 witness.verdict = SelectorMatchVerdict::Maybe;
1592 witness.reason = SelectorMatchReason::MissingTag;
1593 witness.missing_tag = Some(required_tag.clone());
1594 }
1595 _ => {
1596 witness.verdict = SelectorMatchVerdict::No;
1597 witness.reason = SelectorMatchReason::MissingTag;
1598 witness.missing_tag = Some(required_tag.clone());
1599 }
1600 }
1601 }
1602
1603 if let Some(required_id) = &signature.required_id {
1604 match element.id.as_deref() {
1605 Some(id) if id == required_id => {}
1606 _ if !element.id_is_exact && witness.verdict != SelectorMatchVerdict::No => {
1607 witness.verdict = SelectorMatchVerdict::Maybe;
1608 witness.reason = SelectorMatchReason::MissingId;
1609 witness.missing_id = Some(required_id.clone());
1610 }
1611 _ => {
1612 witness.verdict = SelectorMatchVerdict::No;
1613 witness.reason = SelectorMatchReason::MissingId;
1614 witness.missing_id = Some(required_id.clone());
1615 }
1616 }
1617 }
1618
1619 for required_class in &signature.required_classes {
1620 if element.classes.contains(required_class) {
1621 continue;
1622 }
1623 if !element.classes_are_exact && witness.verdict != SelectorMatchVerdict::No {
1624 witness.verdict = SelectorMatchVerdict::Maybe;
1625 } else {
1626 witness.verdict = SelectorMatchVerdict::No;
1627 }
1628 witness.reason = SelectorMatchReason::MissingClass;
1629 witness.missing_classes.insert(required_class.clone());
1630 }
1631
1632 for required_attribute in &signature.required_attributes {
1633 if element.attributes.contains(required_attribute) {
1634 continue;
1635 }
1636 if !element.attributes_are_exact && witness.verdict != SelectorMatchVerdict::No {
1637 witness.verdict = SelectorMatchVerdict::Maybe;
1638 } else {
1639 witness.verdict = SelectorMatchVerdict::No;
1640 }
1641 witness.reason = SelectorMatchReason::MissingAttribute;
1642 witness
1643 .missing_attributes
1644 .insert(required_attribute.clone());
1645 }
1646
1647 for required_pseudo_state in &signature.required_pseudo_states {
1648 if element.pseudo_states.contains(required_pseudo_state) {
1649 continue;
1650 }
1651 if !element.pseudo_states_are_exact && witness.verdict != SelectorMatchVerdict::No {
1652 witness.verdict = SelectorMatchVerdict::Maybe;
1653 } else {
1654 witness.verdict = SelectorMatchVerdict::No;
1655 }
1656 witness.reason = SelectorMatchReason::MissingPseudoState;
1657 witness
1658 .missing_pseudo_states
1659 .insert(required_pseudo_state.clone());
1660 }
1661
1662 witness
1663}
1664
1665fn strongest_by_verdict(
1666 witnesses: &[SelectorMatchWitness],
1667 verdict: SelectorMatchVerdict,
1668) -> Option<usize> {
1669 witnesses
1670 .iter()
1671 .enumerate()
1672 .filter(|(_, witness)| witness.verdict == verdict)
1673 .max_by(|(_, left), (_, right)| left.specificity.cmp(&right.specificity))
1674 .map(|(index, _)| index)
1675}
1676
1677fn parse_simple_selector_signature_inner(selector: &str) -> Option<SelectorSignature> {
1678 if selector.is_empty() || selector_has_unsupported_top_level_syntax(selector) {
1679 return None;
1680 }
1681
1682 let mut required_tag = None;
1683 let mut required_id = None;
1684 let mut required_classes = BTreeSet::new();
1685 let mut required_attributes = BTreeSet::new();
1686 let mut required_pseudo_states = BTreeSet::new();
1687 let mut specificity = Specificity::ZERO;
1688 let chars = selector.chars().collect::<Vec<_>>();
1689 let mut index = 0;
1690
1691 while index < chars.len() {
1692 match chars[index] {
1693 '*' => index += 1,
1694 '.' => {
1695 index += 1;
1696 let (name, next) = read_identifier(&chars, index)?;
1697 specificity.classes += 1;
1698 required_classes.insert(name);
1699 index = next;
1700 }
1701 '#' => {
1702 index += 1;
1703 let (name, next) = read_identifier(&chars, index)?;
1704 specificity.ids += 1;
1705 required_id = Some(name);
1706 index = next;
1707 }
1708 '[' => {
1709 let close = find_closing_bracket(&chars, index)?;
1710 let attribute = chars[index + 1..close].iter().collect::<String>();
1711 let attribute_name = read_attribute_name(attribute.trim())?;
1712 specificity.classes += 1;
1713 required_attributes.insert(attribute_name);
1714 index = close + 1;
1715 }
1716 ':' => {
1717 if matches!(chars.get(index + 1), Some(':')) {
1718 index += 2;
1719 let (_, next) = read_identifier(&chars, index)?;
1720 specificity.elements += 1;
1721 index = next;
1722 } else {
1723 index += 1;
1724 let (name, next) = read_identifier(&chars, index)?;
1725 if matches!(chars.get(next), Some('(')) {
1726 return None;
1727 }
1728 specificity.classes += 1;
1729 required_pseudo_states.insert(name);
1730 index = next;
1731 }
1732 }
1733 ch if is_identifier_start(ch) => {
1734 let (name, next) = read_identifier(&chars, index)?;
1735 if required_tag.is_some() {
1736 return None;
1737 }
1738 specificity.elements += 1;
1739 required_tag = Some(name);
1740 index = next;
1741 }
1742 _ => return None,
1743 }
1744 }
1745
1746 Some(SelectorSignature {
1747 selector: selector.to_string(),
1748 required_tag,
1749 required_id,
1750 required_classes,
1751 required_attributes,
1752 required_pseudo_states,
1753 specificity,
1754 })
1755}
1756
1757fn split_selector_list(selector: &str) -> Vec<String> {
1758 let mut branches = Vec::new();
1759 let mut start = 0;
1760 let mut paren_depth: usize = 0;
1761 let mut bracket_depth: usize = 0;
1762 let chars = selector.char_indices().collect::<Vec<_>>();
1763
1764 for (index, ch) in &chars {
1765 match *ch {
1766 '(' => paren_depth += 1,
1767 ')' => paren_depth = paren_depth.saturating_sub(1),
1768 '[' => bracket_depth += 1,
1769 ']' => bracket_depth = bracket_depth.saturating_sub(1),
1770 ',' if paren_depth == 0 && bracket_depth == 0 => {
1771 let branch = selector[start..*index].trim();
1772 if !branch.is_empty() {
1773 branches.push(branch.to_string());
1774 }
1775 start = *index + 1;
1776 }
1777 _ => {}
1778 }
1779 }
1780
1781 let tail = selector[start..].trim();
1782 if !tail.is_empty() {
1783 branches.push(tail.to_string());
1784 }
1785 branches
1786}
1787
1788fn selector_has_unsupported_top_level_syntax(selector: &str) -> bool {
1789 let mut paren_depth: usize = 0;
1790 let mut bracket_depth: usize = 0;
1791 for ch in selector.chars() {
1792 match ch {
1793 '(' => paren_depth += 1,
1794 ')' => paren_depth = paren_depth.saturating_sub(1),
1795 '[' => bracket_depth += 1,
1796 ']' => bracket_depth = bracket_depth.saturating_sub(1),
1797 '>' | '+' | '~' if paren_depth == 0 && bracket_depth == 0 => return true,
1798 ch if ch.is_whitespace() && paren_depth == 0 && bracket_depth == 0 => return true,
1799 _ => {}
1800 }
1801 }
1802 false
1803}
1804
1805fn find_closing_bracket(chars: &[char], open_index: usize) -> Option<usize> {
1806 chars
1807 .iter()
1808 .enumerate()
1809 .skip(open_index + 1)
1810 .find_map(|(index, ch)| if *ch == ']' { Some(index) } else { None })
1811}
1812
1813fn read_attribute_name(attribute: &str) -> Option<String> {
1814 let name = attribute
1815 .split(|ch: char| ch.is_whitespace() || matches!(ch, '=' | '~' | '|' | '^' | '$' | '*'))
1816 .find(|part| !part.is_empty())?;
1817 Some(name.to_string())
1818}
1819
1820fn read_identifier(chars: &[char], start: usize) -> Option<(String, usize)> {
1821 if start >= chars.len() || !is_identifier_start(chars[start]) {
1822 return None;
1823 }
1824 let mut end = start + 1;
1825 while end < chars.len() && is_identifier_continue(chars[end]) {
1826 end += 1;
1827 }
1828 Some((chars[start..end].iter().collect(), end))
1829}
1830
1831fn is_identifier_start(ch: char) -> bool {
1832 ch.is_ascii_alphabetic() || matches!(ch, '_' | '-')
1833}
1834
1835fn is_identifier_continue(ch: char) -> bool {
1836 ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-')
1837}
1838
1839pub fn substitute_custom_properties(value: &CascadeValue, env: &CustomPropertyEnv) -> CascadeValue {
1840 let mut visiting = BTreeSet::new();
1841 substitute_custom_properties_inner(value, env, &mut visiting)
1842}
1843
1844fn substitute_custom_properties_inner(
1845 value: &CascadeValue,
1846 env: &CustomPropertyEnv,
1847 visiting: &mut BTreeSet<String>,
1848) -> CascadeValue {
1849 match value {
1850 CascadeValue::Literal(_) | CascadeValue::GuaranteedInvalid | CascadeValue::Unset => {
1851 value.clone()
1852 }
1853 CascadeValue::Var { name, fallback } => {
1854 if !visiting.insert(name.clone()) {
1855 return CascadeValue::GuaranteedInvalid;
1856 }
1857 let resolved = match env.get(name) {
1858 Some(CascadeValue::Unset) | None => fallback
1859 .as_deref()
1860 .map(|fallback| substitute_custom_properties_inner(fallback, env, visiting))
1861 .unwrap_or(CascadeValue::GuaranteedInvalid),
1862 Some(value) => substitute_custom_properties_inner(value, env, visiting),
1863 };
1864 visiting.remove(name);
1865 resolved
1866 }
1867 }
1868}
1869
1870fn generated_cascade_fuzz_declarations(
1871 seed: u64,
1872 declaration_count: usize,
1873) -> Vec<CascadeDeclaration> {
1874 let mut state = seed ^ 0x9e37_79b9_7f4a_7c15;
1875 (0..declaration_count)
1876 .map(|index| {
1877 let property = if index == 0 || fuzz_next(&mut state).is_multiple_of(3) {
1878 "color"
1879 } else {
1880 "margin"
1881 };
1882 CascadeDeclaration {
1883 id: format!("decl-{seed}-{index}"),
1884 property: property.to_string(),
1885 value: CascadeValue::Literal(format!("v{}", fuzz_next(&mut state) % 17)),
1886 key: CascadeKey::new(
1887 fuzz_cascade_level(fuzz_next(&mut state)),
1888 LayerRank((fuzz_next(&mut state) % 9) as i32 - 4),
1889 (fuzz_next(&mut state) % 12) as u32,
1890 Specificity::new(
1891 (fuzz_next(&mut state) % 4) as u32,
1892 (fuzz_next(&mut state) % 8) as u32,
1893 (fuzz_next(&mut state) % 12) as u32,
1894 ),
1895 index as u32,
1896 ),
1897 }
1898 })
1899 .collect()
1900}
1901
1902fn fuzz_cascade_level(value: u64) -> CascadeLevel {
1903 match value % 9 {
1904 0 => CascadeLevel::UserAgentNormal,
1905 1 => CascadeLevel::UserNormal,
1906 2 => CascadeLevel::AuthorNormal,
1907 3 => CascadeLevel::InlineNormal,
1908 4 => CascadeLevel::Animation,
1909 5 => CascadeLevel::AuthorImportant,
1910 6 => CascadeLevel::UserImportant,
1911 7 => CascadeLevel::UserAgentImportant,
1912 _ => CascadeLevel::Transition,
1913 }
1914}
1915
1916fn fuzz_var_name(index: usize) -> String {
1917 format!("--fuzz-{index}")
1918}
1919
1920fn fuzz_next(state: &mut u64) -> u64 {
1921 *state = state
1922 .wrapping_mul(6_364_136_223_846_793_005)
1923 .wrapping_add(1_442_695_040_888_963_407);
1924 *state
1925}
1926
1927#[cfg(test)]
1928mod tests {
1929 use super::*;
1930
1931 fn declaration(id: &str, value: &str, key: CascadeKey) -> CascadeDeclaration {
1932 CascadeDeclaration {
1933 id: id.to_string(),
1934 property: "color".to_string(),
1935 value: CascadeValue::Literal(value.to_string()),
1936 key,
1937 }
1938 }
1939
1940 fn key(
1941 level: CascadeLevel,
1942 layer_rank: i32,
1943 scope_proximity: u32,
1944 specificity: Specificity,
1945 source_order: u32,
1946 ) -> CascadeKey {
1947 CascadeKey::new(
1948 level,
1949 LayerRank(layer_rank),
1950 scope_proximity,
1951 specificity,
1952 source_order,
1953 )
1954 }
1955
1956 #[test]
1957 fn orders_specificity_lexicographically() {
1958 assert!(Specificity::new(1, 0, 0) > Specificity::new(0, 99, 99));
1959 assert!(Specificity::new(0, 2, 0) > Specificity::new(0, 1, 99));
1960 assert!(Specificity::new(0, 0, 2) > Specificity::new(0, 0, 1));
1961 }
1962
1963 #[test]
1964 fn orders_cascade_keys_by_level_layer_scope_specificity_and_source() {
1965 let base = key(
1966 CascadeLevel::AuthorNormal,
1967 0,
1968 3,
1969 Specificity::new(0, 1, 0),
1970 1,
1971 );
1972 assert!(
1973 key(
1974 CascadeLevel::AuthorImportant,
1975 0,
1976 3,
1977 Specificity::new(0, 1, 0),
1978 1,
1979 ) > base
1980 );
1981 assert!(
1982 key(
1983 CascadeLevel::AuthorNormal,
1984 1,
1985 3,
1986 Specificity::new(0, 1, 0),
1987 1,
1988 ) > base
1989 );
1990 assert!(
1991 key(
1992 CascadeLevel::AuthorNormal,
1993 0,
1994 1,
1995 Specificity::new(0, 1, 0),
1996 1,
1997 ) > base
1998 );
1999 assert!(
2000 key(
2001 CascadeLevel::AuthorNormal,
2002 0,
2003 3,
2004 Specificity::new(0, 2, 0),
2005 1,
2006 ) > base
2007 );
2008 assert!(
2009 key(
2010 CascadeLevel::AuthorNormal,
2011 0,
2012 3,
2013 Specificity::new(0, 1, 0),
2014 2,
2015 ) > base
2016 );
2017 }
2018
2019 #[test]
2020 fn selects_definite_winner_with_proof() {
2021 let earlier = declaration(
2022 "earlier",
2023 "red",
2024 key(
2025 CascadeLevel::AuthorNormal,
2026 0,
2027 1,
2028 Specificity::new(0, 1, 0),
2029 1,
2030 ),
2031 );
2032 let later = declaration(
2033 "later",
2034 "blue",
2035 key(
2036 CascadeLevel::AuthorNormal,
2037 0,
2038 1,
2039 Specificity::new(0, 1, 0),
2040 2,
2041 ),
2042 );
2043
2044 let outcome = cascade_property([earlier, later], "color");
2045
2046 assert!(matches!(outcome, CascadeOutcome::Definite { .. }));
2047 if let CascadeOutcome::Definite {
2048 winner,
2049 proof,
2050 also_considered,
2051 } = outcome
2052 {
2053 assert_eq!(winner.id, "later");
2054 assert_eq!(proof.declaration_id, "later");
2055 assert_eq!(also_considered.len(), 1);
2056 }
2057 }
2058
2059 #[test]
2060 fn selects_generic_winner_with_same_cascade_ordering() {
2061 let ranked = select_cascade_winner(["earlier", "later"], |item| match *item {
2062 "earlier" => key(
2063 CascadeLevel::AuthorNormal,
2064 0,
2065 1,
2066 Specificity::new(0, 1, 0),
2067 1,
2068 ),
2069 _ => key(
2070 CascadeLevel::AuthorNormal,
2071 0,
2072 1,
2073 Specificity::new(0, 1, 0),
2074 2,
2075 ),
2076 });
2077
2078 let Some((winner, also_considered)) = ranked else {
2079 unreachable!("test input contains candidates")
2080 };
2081 assert_eq!(winner, "later");
2082 assert_eq!(also_considered, vec!["earlier"]);
2083 }
2084
2085 #[test]
2086 fn proves_adjacent_box_longhands_can_combine_to_shorthand() {
2087 let proof = prove_box_shorthand_combination(
2088 "margin",
2089 &[
2090 BoxLonghandInputV0 {
2091 property: "margin-top".to_string(),
2092 value: "1px".to_string(),
2093 important: false,
2094 source_order: 1,
2095 },
2096 BoxLonghandInputV0 {
2097 property: "margin-right".to_string(),
2098 value: "2px".to_string(),
2099 important: false,
2100 source_order: 2,
2101 },
2102 BoxLonghandInputV0 {
2103 property: "margin-bottom".to_string(),
2104 value: "3px".to_string(),
2105 important: false,
2106 source_order: 3,
2107 },
2108 BoxLonghandInputV0 {
2109 property: "margin-left".to_string(),
2110 value: "4px".to_string(),
2111 important: false,
2112 source_order: 4,
2113 },
2114 ],
2115 );
2116
2117 assert_eq!(proof.product, "omena-cascade.shorthand-combination-proof");
2118 assert!(proof.accepted);
2119 assert_eq!(proof.blocked_reason, None);
2120 assert!(proof.provenance_preserved);
2121 assert!(proof.cascade_safe_witness.contains("canonical order"));
2122 }
2123
2124 #[test]
2125 fn blocks_box_shorthand_combination_when_intervening_order_is_possible() {
2126 let proof = prove_box_shorthand_combination(
2127 "padding",
2128 &[
2129 BoxLonghandInputV0 {
2130 property: "padding-top".to_string(),
2131 value: "1px".to_string(),
2132 important: false,
2133 source_order: 1,
2134 },
2135 BoxLonghandInputV0 {
2136 property: "padding-right".to_string(),
2137 value: "2px".to_string(),
2138 important: false,
2139 source_order: 3,
2140 },
2141 BoxLonghandInputV0 {
2142 property: "padding-bottom".to_string(),
2143 value: "3px".to_string(),
2144 important: false,
2145 source_order: 4,
2146 },
2147 BoxLonghandInputV0 {
2148 property: "padding-left".to_string(),
2149 value: "4px".to_string(),
2150 important: false,
2151 source_order: 5,
2152 },
2153 ],
2154 );
2155
2156 assert!(!proof.accepted);
2157 assert_eq!(
2158 proof.blocked_reason,
2159 Some("intervening declaration may change cascade outcome")
2160 );
2161 assert!(!proof.provenance_preserved);
2162 }
2163
2164 #[test]
2165 fn evaluates_simple_supports_conditions_under_modern_browser_assumption() {
2166 let positive = evaluate_static_supports_condition(
2167 "(display: grid)",
2168 StaticSupportsAssumptionV0::ModernBrowser,
2169 );
2170 assert_eq!(positive.product, "omena-cascade.supports-static-eval");
2171 assert_eq!(positive.verdict, StaticSupportsEvalVerdictV0::AlwaysTrue);
2172 assert!(positive.provenance_preserved);
2173
2174 let negative = evaluate_static_supports_condition(
2175 "not (display: grid)",
2176 StaticSupportsAssumptionV0::ModernBrowser,
2177 );
2178 assert_eq!(negative.verdict, StaticSupportsEvalVerdictV0::AlwaysFalse);
2179 assert!(negative.provenance_preserved);
2180
2181 let unknown = evaluate_static_supports_condition(
2182 "(display: grid) and (color: red)",
2183 StaticSupportsAssumptionV0::ModernBrowser,
2184 );
2185 assert_eq!(unknown.verdict, StaticSupportsEvalVerdictV0::Unknown);
2186 assert!(!unknown.provenance_preserved);
2187 }
2188
2189 #[test]
2190 fn proves_only_root_scope_flatten_candidates_without_competition() {
2191 let accepted = prove_scope_flatten_candidate(ScopeFlattenInputV0 {
2192 root_selector: ":root".to_string(),
2193 limit_selector: None,
2194 scoped_rule_count: 1,
2195 peer_scope_count: 0,
2196 competing_unscoped_rule_count: 0,
2197 inside_layer: false,
2198 });
2199 assert_eq!(accepted.product, "omena-cascade.scope-flatten-proof");
2200 assert!(accepted.accepted);
2201 assert!(accepted.provenance_preserved);
2202
2203 let blocked = prove_scope_flatten_candidate(ScopeFlattenInputV0 {
2204 root_selector: ".card".to_string(),
2205 limit_selector: None,
2206 scoped_rule_count: 1,
2207 peer_scope_count: 0,
2208 competing_unscoped_rule_count: 0,
2209 inside_layer: false,
2210 });
2211 assert!(!blocked.accepted);
2212 assert_eq!(
2213 blocked.blocked_reason,
2214 Some("non-root scope flattening requires selector/proximity equivalence proof")
2215 );
2216 }
2217
2218 #[test]
2219 fn proves_layer_flatten_only_for_closed_single_layer_candidates() {
2220 let accepted = prove_layer_flatten_candidate(LayerFlattenInputV0 {
2221 layer_name: Some("theme".to_string()),
2222 layer_rule_count: 1,
2223 peer_layer_count: 0,
2224 unlayered_rule_count: 0,
2225 important_declaration_count: 0,
2226 closed_bundle: true,
2227 });
2228 assert_eq!(accepted.product, "omena-cascade.layer-flatten-proof");
2229 assert!(accepted.accepted);
2230 assert!(accepted.provenance_preserved);
2231
2232 let blocked = prove_layer_flatten_candidate(LayerFlattenInputV0 {
2233 layer_name: Some("theme".to_string()),
2234 layer_rule_count: 1,
2235 peer_layer_count: 0,
2236 unlayered_rule_count: 1,
2237 important_declaration_count: 0,
2238 closed_bundle: true,
2239 });
2240 assert!(!blocked.accepted);
2241 assert_eq!(
2242 blocked.blocked_reason,
2243 Some("unlayered rules compete differently from layered normal rules")
2244 );
2245 }
2246
2247 #[test]
2248 fn reports_selector_context_witness_rank() {
2249 let root = selector_context_witness(&[":root".to_string()], &[".button".to_string()]);
2250 assert_eq!(root.kind, SelectorContextMatchKind::Root);
2251 assert!(root.matched);
2252 assert_eq!(root.rank, 1);
2253
2254 let exact = selector_context_witness(&[".button".to_string()], &[".button".to_string()]);
2255 assert_eq!(exact.kind, SelectorContextMatchKind::Exact);
2256 assert_eq!(exact.rank, 2);
2257
2258 let descendant =
2259 selector_context_witness(&[".theme".to_string()], &[".theme .button".to_string()]);
2260 assert_eq!(descendant.kind, SelectorContextMatchKind::ContainsSelector);
2261 assert_eq!(
2262 descendant.reference_selector.as_deref(),
2263 Some(".theme .button")
2264 );
2265
2266 let miss = selector_context_witness(&[".card".to_string()], &[".button".to_string()]);
2267 assert_eq!(miss.kind, SelectorContextMatchKind::NoMatch);
2268 assert!(!miss.matched);
2269 }
2270
2271 #[test]
2272 fn parses_simple_selector_specificity() {
2273 let signature = parse_simple_selector_signature("button#save.primary[data-state]:hover");
2274 assert!(signature.is_some());
2275 if let Some(signature) = signature {
2276 assert_eq!(signature.required_tag.as_deref(), Some("button"));
2277 assert_eq!(signature.required_id.as_deref(), Some("save"));
2278 assert!(signature.required_classes.contains("primary"));
2279 assert!(signature.required_attributes.contains("data-state"));
2280 assert!(signature.required_pseudo_states.contains("hover"));
2281 assert_eq!(signature.specificity, Specificity::new(1, 3, 1));
2282 }
2283 }
2284
2285 #[test]
2286 fn matches_simple_compound_selectors_against_concrete_signature() {
2287 let mut element =
2288 ElementSignature::concrete(Some("button"), Some("save"), ["primary", "active"]);
2289 element.attributes.insert("data-state".to_string());
2290 element.pseudo_states.insert("hover".to_string());
2291
2292 let witness = selector_match_witness("button#save.primary[data-state]:hover", &element);
2293
2294 assert_eq!(witness.verdict, SelectorMatchVerdict::Yes);
2295 assert_eq!(witness.reason, SelectorMatchReason::SimpleCompound);
2296 assert_eq!(witness.specificity, Specificity::new(1, 3, 1));
2297 }
2298
2299 #[test]
2300 fn reports_missing_class_and_id_as_no_for_exact_signature() {
2301 let element = ElementSignature::concrete(Some("button"), Some("save"), ["primary"]);
2302
2303 let class_miss = selector_match_witness(".missing", &element);
2304 assert_eq!(class_miss.verdict, SelectorMatchVerdict::No);
2305 assert_eq!(class_miss.reason, SelectorMatchReason::MissingClass);
2306 assert!(class_miss.missing_classes.contains("missing"));
2307
2308 let id_miss = selector_match_witness("#cancel", &element);
2309 assert_eq!(id_miss.verdict, SelectorMatchVerdict::No);
2310 assert_eq!(id_miss.reason, SelectorMatchReason::MissingId);
2311 assert_eq!(id_miss.missing_id.as_deref(), Some("cancel"));
2312 }
2313
2314 #[test]
2315 fn returns_maybe_for_inexact_abstract_class_sets() {
2316 let element = ElementSignature::at_least_classes(["button"]);
2317
2318 let witness = selector_match_witness(".button.primary", &element);
2319
2320 assert_eq!(witness.verdict, SelectorMatchVerdict::Maybe);
2321 assert_eq!(witness.reason, SelectorMatchReason::MissingClass);
2322 assert!(witness.missing_classes.contains("primary"));
2323 }
2324
2325 #[test]
2326 fn selector_lists_choose_strongest_matching_branch() {
2327 let element = ElementSignature::concrete(Some("button"), Some("save"), ["primary"]);
2328
2329 let witness = selector_match_witness(".missing, button#save.primary", &element);
2330
2331 assert_eq!(witness.verdict, SelectorMatchVerdict::Yes);
2332 assert_eq!(witness.reason, SelectorMatchReason::SelectorList);
2333 assert_eq!(
2334 witness.matched_branch.as_deref(),
2335 Some("button#save.primary")
2336 );
2337 assert_eq!(witness.specificity, Specificity::new(1, 1, 1));
2338 }
2339
2340 #[test]
2341 fn unsupported_combinators_are_reported_as_maybe() {
2342 let element = ElementSignature::concrete(Some("span"), None::<String>, ["icon"]);
2343
2344 let witness = selector_match_witness(".button > .icon", &element);
2345
2346 assert_eq!(witness.verdict, SelectorMatchVerdict::Maybe);
2347 assert_eq!(witness.reason, SelectorMatchReason::UnsupportedSelector);
2348 assert_eq!(witness.unsupported_branches, vec![".button > .icon"]);
2349 }
2350
2351 #[test]
2352 fn substitutes_custom_property_fallbacks_and_references() {
2353 let mut env = CustomPropertyEnv::new();
2354 env.insert(
2355 "--brand".to_string(),
2356 CascadeValue::Literal("red".to_string()),
2357 );
2358
2359 let resolved = substitute_custom_properties(
2360 &CascadeValue::Var {
2361 name: "--brand".to_string(),
2362 fallback: Some(Box::new(CascadeValue::Literal("blue".to_string()))),
2363 },
2364 &env,
2365 );
2366 assert_eq!(resolved, CascadeValue::Literal("red".to_string()));
2367
2368 let fallback = substitute_custom_properties(
2369 &CascadeValue::Var {
2370 name: "--missing".to_string(),
2371 fallback: Some(Box::new(CascadeValue::Literal("blue".to_string()))),
2372 },
2373 &env,
2374 );
2375 assert_eq!(fallback, CascadeValue::Literal("blue".to_string()));
2376 }
2377
2378 #[test]
2379 fn substitutes_cycles_to_guaranteed_invalid() {
2380 let mut env = CustomPropertyEnv::new();
2381 env.insert(
2382 "--a".to_string(),
2383 CascadeValue::Var {
2384 name: "--b".to_string(),
2385 fallback: None,
2386 },
2387 );
2388 env.insert(
2389 "--b".to_string(),
2390 CascadeValue::Var {
2391 name: "--a".to_string(),
2392 fallback: None,
2393 },
2394 );
2395
2396 let resolved = substitute_custom_properties(
2397 &CascadeValue::Var {
2398 name: "--a".to_string(),
2399 fallback: None,
2400 },
2401 &env,
2402 );
2403
2404 assert_eq!(resolved, CascadeValue::GuaranteedInvalid);
2405 }
2406
2407 #[test]
2408 fn fuzz_seed_corpus_preserves_cascade_and_var_invariants() {
2409 let report = run_cascade_fuzz_seed_corpus();
2410
2411 assert_eq!(report.product, "omena-cascade.fuzz-seed-corpus");
2412 assert_eq!(report.failed_count, 0);
2413 assert_eq!(report.passed_count, report.case_count);
2414 assert!(
2415 report
2416 .var_results
2417 .iter()
2418 .any(|result| result.result == CascadeValue::GuaranteedInvalid)
2419 );
2420 }
2421
2422 #[test]
2423 fn summarizes_current_boundary_status() {
2424 let summary = summarize_cascade_boundary();
2425
2426 assert_eq!(summary.product, "omena-cascade.boundary");
2427 assert_eq!(summary.ordering_model, "lexicographicCascadeKey");
2428 assert!(summary.ready_surfaces.contains(&"cascadeKeyOrdering"));
2429 assert!(summary.ready_surfaces.contains(&"genericCascadeWinner"));
2430 assert!(
2431 summary
2432 .ready_surfaces
2433 .contains(&"semanticDesignTokenRanking")
2434 );
2435 assert!(
2436 summary
2437 .ready_surfaces
2438 .contains(&"queryReadCascadeAtPosition")
2439 );
2440 assert!(summary.ready_surfaces.contains(&"selectorContextWitness"));
2441 assert!(summary.ready_surfaces.contains(&"selectorMatchWitness"));
2442 assert!(
2443 summary
2444 .ready_surfaces
2445 .contains(&"supportsStaticEvalWitness")
2446 );
2447 assert!(summary.ready_surfaces.contains(&"scopeFlattenProof"));
2448 assert!(summary.ready_surfaces.contains(&"layerFlattenProof"));
2449 assert!(summary.ready_surfaces.contains(&"wptCascadeSeedCorpus"));
2450 assert!(
2451 summary
2452 .ready_surfaces
2453 .contains(&"cascadeConformanceSeedCorpus")
2454 );
2455 assert!(!summary.not_ready_surfaces.contains(&"selectorMatchWitness"));
2456 assert!(!summary.not_ready_surfaces.contains(&"wptCascadeCorpus"));
2457 assert!(summary.not_ready_surfaces.contains(&"fullWptCascadeCorpus"));
2458 }
2459
2460 #[test]
2461 fn seed_conformance_corpus_passes_current_cascade_model() {
2462 let report = run_cascade_conformance_seed_corpus();
2463
2464 assert_eq!(report.product, "omena-cascade.conformance-seed-corpus");
2465 assert_eq!(report.case_count, 6);
2466 assert_eq!(report.passed_count, report.case_count);
2467 assert_eq!(report.failed_count, 0);
2468 assert!(report.results.iter().all(|result| result.passed));
2469 }
2470
2471 #[test]
2472 fn wpt_cascade_seed_corpus_passes_current_cascade_model() {
2473 let report = run_wpt_cascade_seed_corpus();
2474
2475 assert_eq!(report.product, "omena-cascade.wpt-cascade-seed-corpus");
2476 assert!(report.case_count >= 200);
2477 assert_eq!(report.passed_count, report.case_count);
2478 assert_eq!(report.failed_count, 0);
2479 assert!(report.results.iter().all(|result| result.passed));
2480 }
2481}