Skip to main content

omena_abstract_value/
value_grammar.rs

1use std::collections::{BTreeSet, HashMap};
2use std::sync::{Arc, OnceLock, RwLock};
3
4use omena_evidence_graph::{
5    EvidenceNodeKeyV0, EvidenceNodeSeedV0, ExternalToolRunWitnessV0, FamilyStampV0, GuaranteeKindV0,
6};
7use omena_spec_audit::{
8    SpecGrammarBoundaryClassificationV0, SpecGrammarRegistryV0, spec_grammar_registry,
9};
10use omena_value_lattice::{
11    CssValueComponentKindV0, CssValueComponentV0, DeclarationValueLensV0, ValueNodeV0,
12    css_value_component_stream, declaration_value_lens, parse_numeric_value_with_unit,
13};
14use serde::{Deserialize, Serialize};
15
16use crate::{
17    AbstractCssTypedScalarValueV0, AbstractCssTypedValueV0, AbstractCssValueV0,
18    DeclaredNumericTypeV0, DeclaredValueKindV0, abstract_css_typed_scalar_from_text,
19    classify_registered_property_declared_value_v0,
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct CssValueGrammarBudgetV0 {
25    pub max_match_steps: usize,
26    pub max_reference_depth: usize,
27    pub max_states: usize,
28}
29
30impl Default for CssValueGrammarBudgetV0 {
31    fn default() -> Self {
32        Self {
33            max_match_steps: 50_000,
34            max_reference_depth: 64,
35            max_states: 4_096,
36        }
37    }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub enum CssValueGrammarBudgetKindV0 {
43    MatchSteps,
44    ReferenceDepth,
45    CandidateStates,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct CssValueGrammarLocusV0 {
51    pub start: usize,
52    pub end: usize,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
56#[serde(
57    tag = "kind",
58    rename_all = "camelCase",
59    rename_all_fields = "camelCase"
60)]
61pub enum CssValueGrammarVerdictV0 {
62    Matched {
63        grammar: String,
64        consumed_components: usize,
65    },
66    Unmatched {
67        grammar: String,
68        locus: CssValueGrammarLocusV0,
69    },
70    NotMatchedWithinBudget {
71        grammar: String,
72        locus: CssValueGrammarLocusV0,
73        budget: CssValueGrammarBudgetKindV0,
74        limit: usize,
75        reference: Option<String>,
76    },
77    GrammarDefect {
78        grammar: String,
79        offset: usize,
80        code: String,
81        detail: String,
82    },
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
86#[serde(rename_all = "camelCase")]
87pub enum CssValueValidationClassV0 {
88    Valid,
89    Invalid,
90    NotValidatable,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
94#[serde(rename_all = "camelCase")]
95pub enum CssValueValidationReasonV0 {
96    GrammarMatched,
97    GrammarUnmatched,
98    GrammarDefect,
99    MatchBudgetExhausted,
100    DeferredSubstitution,
101    VendorExtension,
102    ForwardTierGrammar,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
106#[serde(rename_all = "camelCase")]
107pub struct CssValueValidationV0 {
108    pub class: CssValueValidationClassV0,
109    pub reason: CssValueValidationReasonV0,
110    pub verdict: CssValueGrammarVerdictV0,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
114#[serde(rename_all = "camelCase")]
115pub struct CssValueValidationConsumerPolicyV0 {
116    pub consumer: &'static str,
117    pub matched: &'static str,
118    pub unmatched: &'static str,
119    pub forward_tier_unmatched: &'static str,
120    pub grammar_defect: &'static str,
121    pub budget_exhausted: &'static str,
122}
123
124pub const CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0: [CssValueValidationConsumerPolicyV0; 4] = [
125    CssValueValidationConsumerPolicyV0 {
126        consumer: "checker.registeredPropertyTypeMismatch",
127        matched: "accept",
128        unmatched: "diagnostic",
129        forward_tier_unmatched: "not-applicable",
130        grammar_defect: "silent",
131        budget_exhausted: "silent",
132    },
133    CssValueValidationConsumerPolicyV0 {
134        consumer: "checker.invalidPropertyValue",
135        matched: "accept",
136        unmatched: "diagnostic",
137        forward_tier_unmatched: "not-validatable",
138        grammar_defect: "silent",
139        budget_exhausted: "silent",
140    },
141    CssValueValidationConsumerPolicyV0 {
142        consumer: "scss.nativeCssFunctionParameter",
143        matched: "accept",
144        unmatched: "reject",
145        forward_tier_unmatched: "not-applicable",
146        grammar_defect: "unknown",
147        budget_exhausted: "unknown",
148    },
149    CssValueValidationConsumerPolicyV0 {
150        consumer: "scss.nativeCssFunctionReturn",
151        matched: "accept",
152        unmatched: "reject",
153        forward_tier_unmatched: "not-applicable",
154        grammar_defect: "unknown",
155        budget_exhausted: "unknown",
156    },
157];
158
159/// Records invocation facts for a development-time value grammar oracle.
160/// The external tool remains a witness; it does not determine matcher truth.
161pub fn css_value_grammar_external_tool_evidence_v0(
162    tool_name: &str,
163    tool_version: &str,
164    input_digest: &str,
165    exit_status: i32,
166) -> EvidenceNodeSeedV0 {
167    let witness = ExternalToolRunWitnessV0 {
168        tool_name: tool_name.to_string(),
169        tool_version: tool_version.to_string(),
170        input_digest: input_digest.to_string(),
171        exit_status,
172    };
173    EvidenceNodeSeedV0::with_family(
174        EvidenceNodeKeyV0::new(
175            "omena-abstract-value.value-grammar-differential",
176            input_digest,
177        ),
178        vec![
179            format!("externalTool:{tool_name}"),
180            format!("toolVersion:{tool_version}"),
181            format!("exitStatus:{exit_status}"),
182        ],
183        GuaranteeKindV0::for_label_less_family(),
184        FamilyStampV0::external_tool(&witness),
185    )
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
189#[serde(rename_all = "camelCase")]
190pub struct CssValueGrammarRegistryAuditV0 {
191    pub total_entry_count: usize,
192    pub parsed_entry_count: usize,
193    pub missing_syntax_count: usize,
194    pub grammar_defect_count: usize,
195    pub categories: Vec<CssValueGrammarCategoryAuditV0>,
196    pub defects: Vec<CssValueGrammarDefectV0>,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
200#[serde(rename_all = "camelCase")]
201pub struct CssValueGrammarCategoryAuditV0 {
202    pub category: String,
203    pub entry_count: usize,
204    pub parsed_entry_count: usize,
205    pub missing_syntax_count: usize,
206    pub grammar_defect_count: usize,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
210#[serde(rename_all = "camelCase")]
211pub struct CssValueGrammarDefectV0 {
212    pub category: String,
213    pub name: String,
214    pub offset: usize,
215    pub code: String,
216    pub detail: String,
217}
218
219#[derive(Debug, Clone, PartialEq)]
220pub struct CssValueGrammarTypedMatchV0<'a> {
221    pub verdict: CssValueGrammarVerdictV0,
222    pub abstract_value: AbstractCssValueV0,
223    pub projection: Option<CssValueTypedProjectionV0<'a>>,
224}
225
226#[derive(Debug, Clone, PartialEq)]
227pub struct CssValueTypedProjectionV0<'a> {
228    pub lattice: DeclarationValueLensV0<'a>,
229    pub scalar_leaves: Vec<AbstractCssTypedScalarValueV0>,
230}
231
232impl CssValueGrammarVerdictV0 {
233    pub const fn is_matched(&self) -> bool {
234        matches!(self, Self::Matched { .. })
235    }
236
237    pub const fn is_definite_mismatch(&self) -> bool {
238        matches!(self, Self::Unmatched { .. })
239    }
240
241    pub const fn is_validatable(&self) -> bool {
242        matches!(self, Self::Matched { .. } | Self::Unmatched { .. })
243    }
244}
245
246/// Parses every grammar supplied by the pinned registry and accounts for every
247/// row. Missing source syntax and unsupported grammar shapes remain explicit
248/// data instead of disappearing from a coverage percentage.
249pub fn audit_css_value_grammar_registry_v0(
250    registry: &SpecGrammarRegistryV0,
251) -> CssValueGrammarRegistryAuditV0 {
252    let mut categories = Vec::new();
253    let mut defects = Vec::new();
254    let mut parsed_entry_count = 0usize;
255    let mut missing_syntax_count = 0usize;
256    for category in ["atrules", "functions", "properties", "selectors", "types"] {
257        let entries = registry.entries(category);
258        let mut category_parsed = 0usize;
259        let mut category_missing = 0usize;
260        let defect_start = defects.len();
261        for entry in entries {
262            let Some(grammar) = entry.syntax.as_deref() else {
263                category_missing += 1;
264                missing_syntax_count += 1;
265                continue;
266            };
267            match VdsParser::new(strip_matching_quotes(grammar.trim())).parse() {
268                Ok(_) => {
269                    category_parsed += 1;
270                    parsed_entry_count += 1;
271                }
272                Err(error) => defects.push(CssValueGrammarDefectV0 {
273                    category: category.to_string(),
274                    name: entry.name.clone(),
275                    offset: error.offset,
276                    code: error.code.to_string(),
277                    detail: error.detail,
278                }),
279            }
280        }
281        categories.push(CssValueGrammarCategoryAuditV0 {
282            category: category.to_string(),
283            entry_count: entries.len(),
284            parsed_entry_count: category_parsed,
285            missing_syntax_count: category_missing,
286            grammar_defect_count: defects.len() - defect_start,
287        });
288    }
289    CssValueGrammarRegistryAuditV0 {
290        total_entry_count: registry.total_entry_count(),
291        parsed_entry_count,
292        missing_syntax_count,
293        grammar_defect_count: defects.len(),
294        categories,
295        defects,
296    }
297}
298
299/// Matches a standard property's value against the grammar supplied by the
300/// pinned specification registry.
301pub fn match_standard_property_value_v0(property: &str, value: &str) -> CssValueGrammarVerdictV0 {
302    let registry = spec_grammar_registry();
303    let Some(entry) = registry.entry("properties", property) else {
304        return grammar_defect(
305            "",
306            0,
307            "unknownProperty",
308            format!("property {property:?} is absent from the pinned registry"),
309        );
310    };
311    let Some(grammar) = entry.syntax.as_deref() else {
312        return grammar_defect(
313            "",
314            0,
315            "missingPropertyGrammar",
316            format!("property {property:?} has no syntax in the pinned registry"),
317        );
318    };
319    if matches!(
320        classify_registered_property_declared_value_v0(value),
321        DeclaredValueKindV0::CssWide
322    ) {
323        return CssValueGrammarVerdictV0::Matched {
324            grammar: grammar.to_string(),
325            consumed_components: 1,
326        };
327    }
328    let components = match css_value_component_stream(value, 0) {
329        Ok(components) => components,
330        Err(error) => {
331            return grammar_defect(
332                grammar,
333                error.span.start,
334                "invalidValueTokenStream",
335                error.message,
336            );
337        }
338    };
339    let normalized = strip_matching_quotes(grammar.trim());
340    let expression = match cached_pinned_vds_expression(normalized) {
341        Ok(expression) => expression,
342        Err(error) => {
343            return grammar_defect(grammar, error.offset, error.code, error.detail);
344        }
345    };
346    match_css_value_grammar_components_with_expression_v0(
347        grammar,
348        &components,
349        registry,
350        CssValueGrammarBudgetV0::default(),
351        expression.as_ref(),
352        true,
353    )
354}
355
356/// Matches a registered custom-property or native-CSS function descriptor.
357pub fn match_registered_property_value_v0(syntax: &str, value: &str) -> CssValueGrammarVerdictV0 {
358    let grammar = strip_matching_quotes(syntax.trim()).trim();
359    if grammar == "*" {
360        return match css_value_component_stream(value, 0) {
361            Ok(components) => CssValueGrammarVerdictV0::Matched {
362                grammar: syntax.to_string(),
363                consumed_components: components.len(),
364            },
365            Err(error) => grammar_defect(
366                syntax,
367                error.span.start,
368                "invalidValueTokenStream",
369                error.message,
370            ),
371        };
372    }
373    if matches!(
374        classify_registered_property_declared_value_v0(value),
375        DeclaredValueKindV0::CssWide
376    ) {
377        return CssValueGrammarVerdictV0::Matched {
378            grammar: syntax.to_string(),
379            consumed_components: 1,
380        };
381    }
382    match_css_value_grammar_v0(
383        grammar,
384        value,
385        spec_grammar_registry(),
386        CssValueGrammarBudgetV0::default(),
387    )
388}
389
390pub fn validate_standard_property_value_v0(property: &str, value: &str) -> CssValueValidationV0 {
391    let classification = spec_grammar_registry()
392        .entry("properties", property)
393        .map(|entry| entry.boundary.classification)
394        .unwrap_or(SpecGrammarBoundaryClassificationV0::InBoundary);
395    adjudicate_css_value_validation_with_boundary(
396        value,
397        match_standard_property_value_v0(property, value),
398        classification,
399    )
400}
401
402pub fn validate_registered_property_value_v0(syntax: &str, value: &str) -> CssValueValidationV0 {
403    adjudicate_css_value_validation(value, match_registered_property_value_v0(syntax, value))
404}
405
406fn adjudicate_css_value_validation(
407    value: &str,
408    verdict: CssValueGrammarVerdictV0,
409) -> CssValueValidationV0 {
410    adjudicate_css_value_validation_with_boundary(
411        value,
412        verdict,
413        SpecGrammarBoundaryClassificationV0::InBoundary,
414    )
415}
416
417fn adjudicate_css_value_validation_with_boundary(
418    value: &str,
419    verdict: CssValueGrammarVerdictV0,
420    classification: SpecGrammarBoundaryClassificationV0,
421) -> CssValueValidationV0 {
422    let (class, reason) = if contains_deferred_css_value(value) {
423        (
424            CssValueValidationClassV0::NotValidatable,
425            CssValueValidationReasonV0::DeferredSubstitution,
426        )
427    } else if has_leading_vendor_identifier(value) {
428        (
429            CssValueValidationClassV0::NotValidatable,
430            CssValueValidationReasonV0::VendorExtension,
431        )
432    } else {
433        match verdict {
434            CssValueGrammarVerdictV0::Matched { .. } => (
435                CssValueValidationClassV0::Valid,
436                CssValueValidationReasonV0::GrammarMatched,
437            ),
438            CssValueGrammarVerdictV0::Unmatched { .. }
439                if classification == SpecGrammarBoundaryClassificationV0::ForwardTier =>
440            {
441                (
442                    CssValueValidationClassV0::NotValidatable,
443                    CssValueValidationReasonV0::ForwardTierGrammar,
444                )
445            }
446            CssValueGrammarVerdictV0::Unmatched { .. } => (
447                CssValueValidationClassV0::Invalid,
448                CssValueValidationReasonV0::GrammarUnmatched,
449            ),
450            CssValueGrammarVerdictV0::NotMatchedWithinBudget { .. } => (
451                CssValueValidationClassV0::NotValidatable,
452                CssValueValidationReasonV0::MatchBudgetExhausted,
453            ),
454            CssValueGrammarVerdictV0::GrammarDefect { .. } => (
455                CssValueValidationClassV0::NotValidatable,
456                CssValueValidationReasonV0::GrammarDefect,
457            ),
458        }
459    };
460    CssValueValidationV0 {
461        class,
462        reason,
463        verdict,
464    }
465}
466
467fn contains_deferred_css_value(value: &str) -> bool {
468    let compact = value
469        .chars()
470        .filter(|character| !character.is_ascii_whitespace())
471        .flat_map(char::to_lowercase)
472        .collect::<String>();
473    ["var(", "env(", "attr(", "calc(", "min(", "max(", "clamp("]
474        .iter()
475        .any(|function| compact.contains(function))
476}
477
478fn has_leading_vendor_identifier(value: &str) -> bool {
479    css_value_component_stream(value, 0)
480        .ok()
481        .and_then(|components| components.into_iter().next())
482        .is_some_and(|component| {
483            matches!(component.kind, CssValueComponentKindV0::Ident)
484                && component.text.starts_with('-')
485        })
486}
487
488/// Matches and projects a standard property value into the existing scalar
489/// typed domain plus the existing value-lattice list/function topology.
490pub fn match_and_type_standard_property_value_v0<'a>(
491    property: &str,
492    value: &'a str,
493) -> CssValueGrammarTypedMatchV0<'a> {
494    typed_match_result(match_standard_property_value_v0(property, value), value)
495}
496
497/// Property-independent typed projection for custom grammar consumers.
498pub fn match_and_type_css_value_grammar_v0<'a>(
499    grammar: &str,
500    value: &'a str,
501    registry: &SpecGrammarRegistryV0,
502    budget: CssValueGrammarBudgetV0,
503) -> CssValueGrammarTypedMatchV0<'a> {
504    typed_match_result(
505        match_css_value_grammar_v0(grammar, value, registry, budget),
506        value,
507    )
508}
509
510fn typed_match_result<'a>(
511    verdict: CssValueGrammarVerdictV0,
512    value: &'a str,
513) -> CssValueGrammarTypedMatchV0<'a> {
514    if !verdict.is_matched() {
515        return CssValueGrammarTypedMatchV0 {
516            verdict,
517            abstract_value: AbstractCssValueV0::Raw {
518                value: value.to_string(),
519            },
520            projection: None,
521        };
522    }
523    let components = match css_value_component_stream(value, 0) {
524        Ok(components) => components,
525        Err(error) => {
526            return CssValueGrammarTypedMatchV0 {
527                verdict: grammar_defect(
528                    verdict_grammar(&verdict),
529                    error.span.start,
530                    "typedProjectionTokenStreamDrift",
531                    error.message,
532                ),
533                abstract_value: AbstractCssValueV0::Raw {
534                    value: value.to_string(),
535                },
536                projection: None,
537            };
538        }
539    };
540    let mut scalar_leaves = Vec::new();
541    collect_typed_scalar_leaves(&components, &mut scalar_leaves);
542    let lattice = declaration_value_lens(value, 0);
543    let typed = typed_value_from_projection(&lattice, &scalar_leaves).map(Box::new);
544    CssValueGrammarTypedMatchV0 {
545        verdict,
546        abstract_value: AbstractCssValueV0::Exact {
547            value: value.to_string(),
548            typed,
549        },
550        projection: Some(CssValueTypedProjectionV0 {
551            lattice,
552            scalar_leaves,
553        }),
554    }
555}
556
557fn verdict_grammar(verdict: &CssValueGrammarVerdictV0) -> &str {
558    match verdict {
559        CssValueGrammarVerdictV0::Matched { grammar, .. }
560        | CssValueGrammarVerdictV0::Unmatched { grammar, .. }
561        | CssValueGrammarVerdictV0::NotMatchedWithinBudget { grammar, .. }
562        | CssValueGrammarVerdictV0::GrammarDefect { grammar, .. } => grammar,
563    }
564}
565
566fn collect_typed_scalar_leaves(
567    components: &[CssValueComponentV0],
568    leaves: &mut Vec<AbstractCssTypedScalarValueV0>,
569) {
570    for component in components {
571        if let Some(value) = abstract_css_typed_scalar_from_text(component.text.as_str()) {
572            leaves.push(value);
573            continue;
574        }
575        match &component.kind {
576            CssValueComponentKindV0::Function { arguments, .. }
577            | CssValueComponentKindV0::Parenthesized { values: arguments }
578            | CssValueComponentKindV0::Bracketed { values: arguments }
579            | CssValueComponentKindV0::Braced { values: arguments } => {
580                collect_typed_scalar_leaves(arguments, leaves);
581            }
582            CssValueComponentKindV0::Ident
583            | CssValueComponentKindV0::Number
584            | CssValueComponentKindV0::Percentage
585            | CssValueComponentKindV0::Dimension
586            | CssValueComponentKindV0::Hash
587            | CssValueComponentKindV0::String
588            | CssValueComponentKindV0::Url
589            | CssValueComponentKindV0::Comma
590            | CssValueComponentKindV0::Slash
591            | CssValueComponentKindV0::Delimiter => {}
592        }
593    }
594}
595
596fn typed_value_from_projection(
597    lattice: &DeclarationValueLensV0<'_>,
598    scalar_leaves: &[AbstractCssTypedScalarValueV0],
599) -> Option<AbstractCssTypedValueV0> {
600    match (lattice.root(), scalar_leaves) {
601        (ValueNodeV0::List { .. } | ValueNodeV0::Function { .. }, [_, ..]) | (_, [_, _, ..]) => {
602            Some(AbstractCssTypedValueV0::Compound {
603                leaves: scalar_leaves.to_vec(),
604            })
605        }
606        (_, [value]) => Some(AbstractCssTypedValueV0::Exact {
607            value: value.clone(),
608        }),
609        (_, []) => None,
610    }
611}
612
613/// Matches a value against one CSS Value Definition Syntax expression.
614pub fn match_css_value_grammar_v0(
615    grammar: &str,
616    value: &str,
617    registry: &SpecGrammarRegistryV0,
618    budget: CssValueGrammarBudgetV0,
619) -> CssValueGrammarVerdictV0 {
620    let components = match css_value_component_stream(value, 0) {
621        Ok(components) => components,
622        Err(error) => {
623            return grammar_defect(
624                grammar,
625                error.span.start,
626                "invalidValueTokenStream",
627                error.message,
628            );
629        }
630    };
631    match_css_value_grammar_components_v0(grammar, &components, registry, budget)
632}
633
634/// Property-independent matcher entry point over an already tokenized value.
635pub fn match_css_value_grammar_components_v0(
636    grammar: &str,
637    components: &[CssValueComponentV0],
638    registry: &SpecGrammarRegistryV0,
639    budget: CssValueGrammarBudgetV0,
640) -> CssValueGrammarVerdictV0 {
641    let normalized = strip_matching_quotes(grammar.trim());
642    let expression = match VdsParser::new(normalized).parse() {
643        Ok(expression) => expression,
644        Err(error) => {
645            return grammar_defect(grammar, error.offset, error.code, error.detail);
646        }
647    };
648    match_css_value_grammar_components_with_expression_v0(
649        grammar,
650        components,
651        registry,
652        budget,
653        &expression,
654        false,
655    )
656}
657
658fn match_css_value_grammar_components_with_expression_v0(
659    grammar: &str,
660    components: &[CssValueComponentV0],
661    registry: &SpecGrammarRegistryV0,
662    budget: CssValueGrammarBudgetV0,
663    expression: &VdsExpression,
664    cache_registered_grammars: bool,
665) -> CssValueGrammarVerdictV0 {
666    let locus = component_locus(components);
667    let mut context = MatchContext {
668        registry,
669        budget,
670        match_steps: 0,
671        first_stop: None,
672        grammar_cache: HashMap::new(),
673        cache_registered_grammars,
674    };
675    let ends = context.match_expression(expression, components, 0, 0);
676    if ends.contains(&components.len()) {
677        return CssValueGrammarVerdictV0::Matched {
678            grammar: grammar.to_string(),
679            consumed_components: components.len(),
680        };
681    }
682    if let Some(stop) = context.first_stop {
683        return match stop {
684            MatchStop::Budget {
685                kind,
686                limit,
687                reference,
688            } => CssValueGrammarVerdictV0::NotMatchedWithinBudget {
689                grammar: grammar.to_string(),
690                locus,
691                budget: kind,
692                limit,
693                reference,
694            },
695            MatchStop::GrammarDefect {
696                offset,
697                code,
698                detail,
699            } => grammar_defect(grammar, offset, code, detail),
700        };
701    }
702    CssValueGrammarVerdictV0::Unmatched {
703        grammar: grammar.to_string(),
704        locus,
705    }
706}
707
708#[derive(Debug, Clone, PartialEq, Eq)]
709enum VdsExpression {
710    Literal(String),
711    Reference(VdsReference),
712    Function {
713        name: String,
714        arguments: Box<VdsExpression>,
715    },
716    Sequence(Vec<VdsExpression>),
717    AllInAnyOrder(Vec<VdsExpression>),
718    OneOrMoreInAnyOrder(Vec<VdsExpression>),
719    Choice(Vec<VdsExpression>),
720    Repeat {
721        expression: Box<VdsExpression>,
722        min: usize,
723        max: Option<usize>,
724        comma_separated: bool,
725    },
726    Required(Box<VdsExpression>),
727}
728
729#[derive(Debug, Clone, PartialEq, Eq)]
730struct VdsReference {
731    category: ReferenceCategory,
732    name: String,
733    range: Option<NumericRange>,
734}
735
736#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
737enum ReferenceCategory {
738    Type,
739    Property,
740    Function,
741}
742
743#[derive(Debug, Clone, PartialEq, Eq)]
744struct NumericRange {
745    min: Option<String>,
746    max: Option<String>,
747}
748
749#[derive(Debug, Clone, PartialEq, Eq)]
750struct VdsParseError {
751    offset: usize,
752    code: &'static str,
753    detail: String,
754}
755
756type CachedVdsExpression = Result<Arc<VdsExpression>, VdsParseError>;
757
758fn cached_pinned_vds_expression(source: &str) -> CachedVdsExpression {
759    static CACHE: OnceLock<RwLock<HashMap<String, CachedVdsExpression>>> = OnceLock::new();
760    let cache = CACHE.get_or_init(|| RwLock::new(HashMap::new()));
761    if let Some(parsed) = cache
762        .read()
763        .unwrap_or_else(std::sync::PoisonError::into_inner)
764        .get(source)
765    {
766        return parsed.clone();
767    }
768
769    let parsed = VdsParser::new(source).parse().map(Arc::new);
770    cache
771        .write()
772        .unwrap_or_else(std::sync::PoisonError::into_inner)
773        .entry(source.to_string())
774        .or_insert(parsed)
775        .clone()
776}
777
778#[derive(Debug, Clone, PartialEq, Eq)]
779struct VdsToken {
780    kind: VdsTokenKind,
781    offset: usize,
782}
783
784#[derive(Debug, Clone, PartialEq, Eq)]
785enum VdsTokenKind {
786    Word(String),
787    Reference(String),
788    Literal(String),
789    OpenBracket,
790    CloseBracket,
791    OpenParen,
792    CloseParen,
793    Or,
794    OrOr,
795    AndAnd,
796    Question,
797    Star,
798    Plus,
799    Hash,
800    Range(usize, Option<usize>),
801    Bang,
802    End,
803}
804
805struct VdsParser<'a> {
806    source: &'a str,
807    tokens: Vec<VdsToken>,
808    cursor: usize,
809}
810
811impl<'a> VdsParser<'a> {
812    fn new(source: &'a str) -> Self {
813        Self {
814            source,
815            tokens: Vec::new(),
816            cursor: 0,
817        }
818    }
819
820    fn parse(mut self) -> Result<VdsExpression, VdsParseError> {
821        self.tokens = lex_vds(self.source)?;
822        let expression = self.parse_choice()?;
823        if !matches!(self.peek(), VdsTokenKind::End) {
824            return Err(self.error(
825                "unexpectedGrammarToken",
826                "unexpected trailing grammar token",
827            ));
828        }
829        Ok(expression)
830    }
831
832    fn parse_choice(&mut self) -> Result<VdsExpression, VdsParseError> {
833        let mut values = vec![self.parse_one_or_more_in_any_order()?];
834        while matches!(self.peek(), VdsTokenKind::Or) {
835            self.cursor += 1;
836            values.push(self.parse_one_or_more_in_any_order()?);
837        }
838        Ok(flatten_expression(values, VdsExpression::Choice))
839    }
840
841    fn parse_one_or_more_in_any_order(&mut self) -> Result<VdsExpression, VdsParseError> {
842        let mut values = vec![self.parse_all_in_any_order()?];
843        while matches!(self.peek(), VdsTokenKind::OrOr) {
844            self.cursor += 1;
845            values.push(self.parse_all_in_any_order()?);
846        }
847        Ok(flatten_expression(
848            values,
849            VdsExpression::OneOrMoreInAnyOrder,
850        ))
851    }
852
853    fn parse_all_in_any_order(&mut self) -> Result<VdsExpression, VdsParseError> {
854        let mut values = vec![self.parse_sequence()?];
855        while matches!(self.peek(), VdsTokenKind::AndAnd) {
856            self.cursor += 1;
857            values.push(self.parse_sequence()?);
858        }
859        Ok(flatten_expression(values, VdsExpression::AllInAnyOrder))
860    }
861
862    fn parse_sequence(&mut self) -> Result<VdsExpression, VdsParseError> {
863        let mut values = Vec::new();
864        while self.starts_primary() {
865            values.push(self.parse_postfix()?);
866        }
867        if values.is_empty() {
868            return Err(self.error("missingGrammarTerm", "expected a grammar term"));
869        }
870        Ok(flatten_expression(values, VdsExpression::Sequence))
871    }
872
873    fn parse_postfix(&mut self) -> Result<VdsExpression, VdsParseError> {
874        let mut expression = self.parse_primary()?;
875        loop {
876            expression = match self.peek() {
877                VdsTokenKind::Question => {
878                    self.cursor += 1;
879                    repeat(expression, 0, Some(1), false)
880                }
881                VdsTokenKind::Star => {
882                    self.cursor += 1;
883                    repeat(expression, 0, None, false)
884                }
885                VdsTokenKind::Plus => {
886                    self.cursor += 1;
887                    repeat(expression, 1, None, false)
888                }
889                VdsTokenKind::Hash => {
890                    self.cursor += 1;
891                    let (min, max) = match self.peek().clone() {
892                        VdsTokenKind::Range(min, max) => {
893                            self.cursor += 1;
894                            (min, max)
895                        }
896                        _ => (1, None),
897                    };
898                    repeat(expression, min, max, true)
899                }
900                VdsTokenKind::Range(min, max) => {
901                    let min = *min;
902                    let max = *max;
903                    self.cursor += 1;
904                    repeat(expression, min, max, false)
905                }
906                VdsTokenKind::Bang => {
907                    self.cursor += 1;
908                    VdsExpression::Required(Box::new(expression))
909                }
910                _ => break,
911            };
912        }
913        Ok(expression)
914    }
915
916    fn parse_primary(&mut self) -> Result<VdsExpression, VdsParseError> {
917        let token = self.tokens[self.cursor].clone();
918        self.cursor += 1;
919        match token.kind {
920            VdsTokenKind::Reference(source) => Ok(VdsExpression::Reference(parse_reference(
921                source.as_str(),
922                token.offset,
923            )?)),
924            VdsTokenKind::Word(word) => {
925                if matches!(self.peek(), VdsTokenKind::OpenParen) {
926                    self.cursor += 1;
927                    if matches!(self.peek(), VdsTokenKind::CloseParen) {
928                        self.cursor += 1;
929                        return Ok(VdsExpression::Function {
930                            name: word,
931                            arguments: Box::new(VdsExpression::Sequence(Vec::new())),
932                        });
933                    }
934                    let arguments = self.parse_choice()?;
935                    self.expect_close_paren()?;
936                    Ok(VdsExpression::Function {
937                        name: word,
938                        arguments: Box::new(arguments),
939                    })
940                } else {
941                    Ok(VdsExpression::Literal(word))
942                }
943            }
944            VdsTokenKind::Literal(literal) => Ok(VdsExpression::Literal(literal)),
945            VdsTokenKind::OpenBracket => {
946                let expression = self.parse_choice()?;
947                if !matches!(self.peek(), VdsTokenKind::CloseBracket) {
948                    return Err(self.error("unclosedGrammarGroup", "missing closing ]"));
949                }
950                self.cursor += 1;
951                Ok(expression)
952            }
953            VdsTokenKind::OpenParen => {
954                let expression = self.parse_choice()?;
955                self.expect_close_paren()?;
956                Ok(expression)
957            }
958            _ => Err(VdsParseError {
959                offset: token.offset,
960                code: "unexpectedGrammarPrimary",
961                detail: "expected a literal, reference, function, or group".to_string(),
962            }),
963        }
964    }
965
966    fn expect_close_paren(&mut self) -> Result<(), VdsParseError> {
967        if !matches!(self.peek(), VdsTokenKind::CloseParen) {
968            return Err(self.error("unclosedGrammarFunction", "missing closing )"));
969        }
970        self.cursor += 1;
971        Ok(())
972    }
973
974    fn starts_primary(&self) -> bool {
975        matches!(
976            self.peek(),
977            VdsTokenKind::Reference(_)
978                | VdsTokenKind::Word(_)
979                | VdsTokenKind::Literal(_)
980                | VdsTokenKind::OpenBracket
981                | VdsTokenKind::OpenParen
982        )
983    }
984
985    fn peek(&self) -> &VdsTokenKind {
986        &self.tokens[self.cursor].kind
987    }
988
989    fn error(&self, code: &'static str, detail: &str) -> VdsParseError {
990        VdsParseError {
991            offset: self.tokens[self.cursor].offset,
992            code,
993            detail: detail.to_string(),
994        }
995    }
996}
997
998fn flatten_expression(
999    mut values: Vec<VdsExpression>,
1000    wrap: impl FnOnce(Vec<VdsExpression>) -> VdsExpression,
1001) -> VdsExpression {
1002    if values.len() == 1 {
1003        values.pop().unwrap_or(VdsExpression::Sequence(Vec::new()))
1004    } else {
1005        wrap(values)
1006    }
1007}
1008
1009fn repeat(
1010    expression: VdsExpression,
1011    min: usize,
1012    max: Option<usize>,
1013    comma_separated: bool,
1014) -> VdsExpression {
1015    VdsExpression::Repeat {
1016        expression: Box::new(expression),
1017        min,
1018        max,
1019        comma_separated,
1020    }
1021}
1022
1023fn lex_vds(source: &str) -> Result<Vec<VdsToken>, VdsParseError> {
1024    let mut tokens = Vec::new();
1025    let mut cursor = 0usize;
1026    while cursor < source.len() {
1027        let Some(character) = source[cursor..].chars().next() else {
1028            break;
1029        };
1030        if character.is_whitespace() {
1031            cursor += character.len_utf8();
1032            continue;
1033        }
1034        let offset = cursor;
1035        let rest = &source[cursor..];
1036        if rest.starts_with("||") {
1037            tokens.push(token(VdsTokenKind::OrOr, offset));
1038            cursor += 2;
1039            continue;
1040        }
1041        if rest.starts_with("&&") {
1042            tokens.push(token(VdsTokenKind::AndAnd, offset));
1043            cursor += 2;
1044            continue;
1045        }
1046        if character == '<' {
1047            let Some(relative_end) = rest.find('>') else {
1048                return Err(VdsParseError {
1049                    offset,
1050                    code: "unclosedGrammarReference",
1051                    detail: "missing closing >".to_string(),
1052                });
1053            };
1054            let end = cursor + relative_end;
1055            tokens.push(token(
1056                VdsTokenKind::Reference(source[cursor + 1..end].trim().to_string()),
1057                offset,
1058            ));
1059            cursor = end + 1;
1060            continue;
1061        }
1062        if character == '{' {
1063            let Some(relative_end) =
1064                rest.char_indices()
1065                    .find_map(|(relative_offset, candidate)| {
1066                        (candidate == '}').then_some(relative_offset)
1067                    })
1068            else {
1069                return Err(VdsParseError {
1070                    offset,
1071                    code: "unclosedGrammarRange",
1072                    detail: "missing closing }".to_string(),
1073                });
1074            };
1075            let end = cursor + relative_end;
1076            let range = parse_repeat_range(&source[cursor + 1..end], offset)?;
1077            tokens.push(token(VdsTokenKind::Range(range.0, range.1), offset));
1078            cursor = end + 1;
1079            continue;
1080        }
1081        let simple = match character {
1082            '[' => Some(VdsTokenKind::OpenBracket),
1083            ']' => Some(VdsTokenKind::CloseBracket),
1084            '(' => Some(VdsTokenKind::OpenParen),
1085            ')' => Some(VdsTokenKind::CloseParen),
1086            '|' => Some(VdsTokenKind::Or),
1087            '?' => Some(VdsTokenKind::Question),
1088            '*' => Some(VdsTokenKind::Star),
1089            '+' => Some(VdsTokenKind::Plus),
1090            '#' => Some(VdsTokenKind::Hash),
1091            '!' => Some(VdsTokenKind::Bang),
1092            ',' | '/' | ':' | ';' | '=' | '@' | '~' | '^' | '$' | '&' => {
1093                Some(VdsTokenKind::Literal(character.to_string()))
1094            }
1095            _ => None,
1096        };
1097        if let Some(kind) = simple {
1098            tokens.push(token(kind, offset));
1099            cursor += character.len_utf8();
1100            continue;
1101        }
1102        if character == '\'' || character == '"' {
1103            let quote = character;
1104            cursor += character.len_utf8();
1105            let content_start = cursor;
1106            let mut escaped = false;
1107            let mut found_end = None;
1108            while cursor < source.len() {
1109                let Some(current) = source[cursor..].chars().next() else {
1110                    break;
1111                };
1112                if escaped {
1113                    escaped = false;
1114                } else if current == '\\' {
1115                    escaped = true;
1116                } else if current == quote {
1117                    found_end = Some(cursor);
1118                    break;
1119                }
1120                cursor += current.len_utf8();
1121            }
1122            let Some(end) = found_end else {
1123                return Err(VdsParseError {
1124                    offset,
1125                    code: "unclosedGrammarString",
1126                    detail: "missing closing quote".to_string(),
1127                });
1128            };
1129            tokens.push(token(
1130                VdsTokenKind::Literal(source[content_start..end].to_string()),
1131                offset,
1132            ));
1133            cursor = end + quote.len_utf8();
1134            continue;
1135        }
1136        let start = cursor;
1137        while cursor < source.len() {
1138            let Some(current) = source[cursor..].chars().next() else {
1139                break;
1140            };
1141            if current.is_whitespace()
1142                || matches!(
1143                    current,
1144                    '<' | '>'
1145                        | '['
1146                        | ']'
1147                        | '('
1148                        | ')'
1149                        | '{'
1150                        | '}'
1151                        | '|'
1152                        | '&'
1153                        | '?'
1154                        | '*'
1155                        | '+'
1156                        | '#'
1157                        | '!'
1158                        | ','
1159                        | '/'
1160                        | ':'
1161                        | ';'
1162                        | '='
1163                        | '@'
1164                        | '~'
1165                        | '^'
1166                        | '$'
1167                        | '\''
1168                        | '"'
1169                )
1170            {
1171                break;
1172            }
1173            cursor += current.len_utf8();
1174        }
1175        if start == cursor {
1176            return Err(VdsParseError {
1177                offset,
1178                code: "unsupportedGrammarCharacter",
1179                detail: format!("unsupported grammar character {character:?}"),
1180            });
1181        }
1182        tokens.push(token(
1183            VdsTokenKind::Word(source[start..cursor].to_string()),
1184            offset,
1185        ));
1186    }
1187    tokens.push(token(VdsTokenKind::End, source.len()));
1188    Ok(tokens)
1189}
1190
1191fn token(kind: VdsTokenKind, offset: usize) -> VdsToken {
1192    VdsToken { kind, offset }
1193}
1194
1195fn parse_repeat_range(
1196    source: &str,
1197    offset: usize,
1198) -> Result<(usize, Option<usize>), VdsParseError> {
1199    let mut parts = source.split(',').map(str::trim);
1200    let first = parts.next().unwrap_or_default();
1201    let second = parts.next();
1202    if parts.next().is_some() || first.is_empty() {
1203        return Err(VdsParseError {
1204            offset,
1205            code: "invalidGrammarRange",
1206            detail: format!("invalid repeat range {{{source}}}"),
1207        });
1208    }
1209    let min = first.parse::<usize>().map_err(|_| VdsParseError {
1210        offset,
1211        code: "invalidGrammarRange",
1212        detail: format!("invalid repeat range minimum {first:?}"),
1213    })?;
1214    let max = match second {
1215        None => Some(min),
1216        Some("") => None,
1217        Some(value) => Some(value.parse::<usize>().map_err(|_| VdsParseError {
1218            offset,
1219            code: "invalidGrammarRange",
1220            detail: format!("invalid repeat range maximum {value:?}"),
1221        })?),
1222    };
1223    if max.is_some_and(|max| max < min) {
1224        return Err(VdsParseError {
1225            offset,
1226            code: "invalidGrammarRange",
1227            detail: format!("repeat range maximum precedes minimum in {{{source}}}"),
1228        });
1229    }
1230    Ok((min, max))
1231}
1232
1233fn parse_reference(source: &str, offset: usize) -> Result<VdsReference, VdsParseError> {
1234    let source = source.trim();
1235    if source.is_empty() {
1236        return Err(VdsParseError {
1237            offset,
1238            code: "emptyGrammarReference",
1239            detail: "empty grammar reference".to_string(),
1240        });
1241    }
1242    if let Some(property) = source
1243        .strip_prefix('\'')
1244        .and_then(|value| value.strip_suffix('\''))
1245    {
1246        return Ok(VdsReference {
1247            category: ReferenceCategory::Property,
1248            name: property.to_ascii_lowercase(),
1249            range: None,
1250        });
1251    }
1252    let (name, range) = split_reference_range(source, offset)?;
1253    let category = if name.ends_with("()") {
1254        ReferenceCategory::Function
1255    } else {
1256        ReferenceCategory::Type
1257    };
1258    Ok(VdsReference {
1259        category,
1260        name: name.to_ascii_lowercase(),
1261        range,
1262    })
1263}
1264
1265fn split_reference_range(
1266    source: &str,
1267    offset: usize,
1268) -> Result<(&str, Option<NumericRange>), VdsParseError> {
1269    let Some(open) = source.find('[') else {
1270        return Ok((source.trim(), None));
1271    };
1272    let Some(close) = source.rfind(']') else {
1273        return Err(VdsParseError {
1274            offset,
1275            code: "unclosedReferenceRange",
1276            detail: format!("missing ] in reference <{source}>"),
1277        });
1278    };
1279    if close + 1 != source.len() {
1280        return Err(VdsParseError {
1281            offset,
1282            code: "trailingReferenceRangeContent",
1283            detail: format!("unexpected content after range in <{source}>"),
1284        });
1285    }
1286    let name = source[..open].trim();
1287    let mut bounds = source[open + 1..close].split(',').map(str::trim);
1288    let min = bounds.next().unwrap_or_default();
1289    let max = bounds.next();
1290    if name.is_empty() || min.is_empty() || max.is_none() || bounds.next().is_some() {
1291        return Err(VdsParseError {
1292            offset,
1293            code: "invalidReferenceRange",
1294            detail: format!("invalid numeric range in <{source}>"),
1295        });
1296    }
1297    let max = max.unwrap_or_default();
1298    Ok((
1299        name,
1300        Some(NumericRange {
1301            min: finite_range_bound(min),
1302            max: finite_range_bound(max),
1303        }),
1304    ))
1305}
1306
1307fn finite_range_bound(source: &str) -> Option<String> {
1308    (!matches!(source, "∞" | "+∞" | "-∞")).then(|| source.to_string())
1309}
1310
1311#[derive(Debug, Clone, PartialEq, Eq)]
1312enum MatchStop {
1313    Budget {
1314        kind: CssValueGrammarBudgetKindV0,
1315        limit: usize,
1316        reference: Option<String>,
1317    },
1318    GrammarDefect {
1319        offset: usize,
1320        code: &'static str,
1321        detail: String,
1322    },
1323}
1324
1325struct MatchContext<'a> {
1326    registry: &'a SpecGrammarRegistryV0,
1327    budget: CssValueGrammarBudgetV0,
1328    match_steps: usize,
1329    first_stop: Option<MatchStop>,
1330    grammar_cache: HashMap<(ReferenceCategory, String), CachedVdsExpression>,
1331    cache_registered_grammars: bool,
1332}
1333
1334#[derive(Debug, Clone, Copy)]
1335struct RepeatMatchPlan<'a> {
1336    expression: &'a VdsExpression,
1337    min: usize,
1338    max: Option<usize>,
1339    comma_separated: bool,
1340}
1341
1342impl MatchContext<'_> {
1343    fn match_expression(
1344        &mut self,
1345        expression: &VdsExpression,
1346        components: &[CssValueComponentV0],
1347        position: usize,
1348        reference_depth: usize,
1349    ) -> BTreeSet<usize> {
1350        if !self.consume_step(None) {
1351            return BTreeSet::new();
1352        }
1353        let positions = match expression {
1354            VdsExpression::Literal(literal) => match_literal(literal, components, position),
1355            VdsExpression::Reference(reference) => {
1356                self.match_reference(reference, components, position, reference_depth)
1357            }
1358            VdsExpression::Function { name, arguments } => {
1359                self.match_function(name, arguments, components, position, reference_depth)
1360            }
1361            VdsExpression::Sequence(expressions) => {
1362                self.match_sequence(expressions, components, position, reference_depth)
1363            }
1364            VdsExpression::AllInAnyOrder(expressions) => {
1365                self.match_any_order(expressions, components, position, reference_depth, true)
1366            }
1367            VdsExpression::OneOrMoreInAnyOrder(expressions) => {
1368                self.match_any_order(expressions, components, position, reference_depth, false)
1369            }
1370            VdsExpression::Choice(expressions) => expressions
1371                .iter()
1372                .flat_map(|expression| {
1373                    self.match_expression(expression, components, position, reference_depth)
1374                })
1375                .collect(),
1376            VdsExpression::Repeat {
1377                expression,
1378                min,
1379                max,
1380                comma_separated,
1381            } => self.match_repeat(
1382                RepeatMatchPlan {
1383                    expression,
1384                    min: *min,
1385                    max: *max,
1386                    comma_separated: *comma_separated,
1387                },
1388                components,
1389                position,
1390                reference_depth,
1391            ),
1392            VdsExpression::Required(expression) => self
1393                .match_expression(expression, components, position, reference_depth)
1394                .into_iter()
1395                .filter(|end| *end > position)
1396                .collect(),
1397        };
1398        self.cap_states(positions, None)
1399    }
1400
1401    fn match_sequence(
1402        &mut self,
1403        expressions: &[VdsExpression],
1404        components: &[CssValueComponentV0],
1405        position: usize,
1406        reference_depth: usize,
1407    ) -> BTreeSet<usize> {
1408        let mut states = BTreeSet::from([(position, false)]);
1409        for expression in expressions {
1410            let mut next = BTreeSet::new();
1411            for (position, previous_was_omitted) in states {
1412                if previous_was_omitted && is_comma_literal(expression) {
1413                    next.insert((position, false));
1414                    continue;
1415                }
1416                for end in self.match_expression(expression, components, position, reference_depth)
1417                {
1418                    next.insert((end, end == position));
1419                }
1420            }
1421            if next.len() > self.budget.max_states {
1422                self.record_stop(MatchStop::Budget {
1423                    kind: CssValueGrammarBudgetKindV0::CandidateStates,
1424                    limit: self.budget.max_states,
1425                    reference: None,
1426                });
1427                next.clear();
1428            }
1429            states = next;
1430            if states.is_empty() {
1431                break;
1432            }
1433        }
1434        states.into_iter().map(|(position, _)| position).collect()
1435    }
1436
1437    fn match_repeat(
1438        &mut self,
1439        plan: RepeatMatchPlan<'_>,
1440        components: &[CssValueComponentV0],
1441        position: usize,
1442        reference_depth: usize,
1443    ) -> BTreeSet<usize> {
1444        let effective_max = plan
1445            .max
1446            .unwrap_or_else(|| components.len().saturating_add(1));
1447        let mut accepted = BTreeSet::new();
1448        let mut frontier = BTreeSet::from([position]);
1449        if plan.min == 0 {
1450            accepted.insert(position);
1451        }
1452        for count in 1..=effective_max {
1453            let mut next = BTreeSet::new();
1454            for current in &frontier {
1455                let item_start = if plan.comma_separated && count > 1 {
1456                    if components.get(*current).is_some_and(|component| {
1457                        matches!(component.kind, CssValueComponentKindV0::Comma)
1458                    }) {
1459                        *current + 1
1460                    } else {
1461                        continue;
1462                    }
1463                } else {
1464                    *current
1465                };
1466                for end in
1467                    self.match_expression(plan.expression, components, item_start, reference_depth)
1468                {
1469                    if end > item_start {
1470                        next.insert(end);
1471                    }
1472                }
1473            }
1474            frontier = self.cap_states(next, None);
1475            if frontier.is_empty() {
1476                break;
1477            }
1478            if count >= plan.min {
1479                accepted.extend(frontier.iter().copied());
1480            }
1481        }
1482        accepted
1483    }
1484
1485    fn match_any_order(
1486        &mut self,
1487        expressions: &[VdsExpression],
1488        components: &[CssValueComponentV0],
1489        position: usize,
1490        reference_depth: usize,
1491        require_all: bool,
1492    ) -> BTreeSet<usize> {
1493        if expressions.len() > 63 {
1494            self.record_stop(MatchStop::Budget {
1495                kind: CssValueGrammarBudgetKindV0::CandidateStates,
1496                limit: self.budget.max_states,
1497                reference: None,
1498            });
1499            return BTreeSet::new();
1500        }
1501        let required_mask = (1u64 << expressions.len()) - 1;
1502        let mut accepted = BTreeSet::new();
1503        let mut stack = vec![(position, 0u64)];
1504        let mut visited = BTreeSet::new();
1505        while let Some((current, mask)) = stack.pop() {
1506            if !visited.insert((current, mask)) {
1507                continue;
1508            }
1509            if visited.len() > self.budget.max_states {
1510                self.record_stop(MatchStop::Budget {
1511                    kind: CssValueGrammarBudgetKindV0::CandidateStates,
1512                    limit: self.budget.max_states,
1513                    reference: None,
1514                });
1515                break;
1516            }
1517            if (require_all && mask == required_mask) || (!require_all && mask != 0) {
1518                accepted.insert(current);
1519            }
1520            for (index, expression) in expressions.iter().enumerate() {
1521                let bit = 1u64 << index;
1522                if mask & bit != 0 {
1523                    continue;
1524                }
1525                for end in self.match_expression(expression, components, current, reference_depth) {
1526                    if end > current || (require_all && end == current) {
1527                        stack.push((end, mask | bit));
1528                    }
1529                }
1530            }
1531        }
1532        accepted
1533    }
1534
1535    fn match_function(
1536        &mut self,
1537        name: &str,
1538        arguments: &VdsExpression,
1539        components: &[CssValueComponentV0],
1540        position: usize,
1541        reference_depth: usize,
1542    ) -> BTreeSet<usize> {
1543        let Some(component) = components.get(position) else {
1544            return BTreeSet::new();
1545        };
1546        let CssValueComponentKindV0::Function {
1547            name: actual,
1548            arguments: actual_arguments,
1549        } = &component.kind
1550        else {
1551            return BTreeSet::new();
1552        };
1553        if !actual.eq_ignore_ascii_case(name) {
1554            return BTreeSet::new();
1555        }
1556        self.match_expression(arguments, actual_arguments, 0, reference_depth)
1557            .contains(&actual_arguments.len())
1558            .then_some(position + 1)
1559            .into_iter()
1560            .collect()
1561    }
1562
1563    fn match_reference(
1564        &mut self,
1565        reference: &VdsReference,
1566        components: &[CssValueComponentV0],
1567        position: usize,
1568        reference_depth: usize,
1569    ) -> BTreeSet<usize> {
1570        if let Some(positions) = match_builtin_reference(reference, components, position) {
1571            return positions;
1572        }
1573        if reference_depth >= self.budget.max_reference_depth {
1574            self.record_stop(MatchStop::Budget {
1575                kind: CssValueGrammarBudgetKindV0::ReferenceDepth,
1576                limit: self.budget.max_reference_depth,
1577                reference: Some(reference.name.clone()),
1578            });
1579            return BTreeSet::new();
1580        }
1581        let category = match reference.category {
1582            ReferenceCategory::Type => "types",
1583            ReferenceCategory::Property => "properties",
1584            ReferenceCategory::Function => "functions",
1585        };
1586        let Some(entry) = self.registry.entry(category, reference.name.as_str()) else {
1587            self.record_stop(MatchStop::GrammarDefect {
1588                offset: 0,
1589                code: "unknownGrammarReference",
1590                detail: format!("unknown {category} reference <{}>", reference.name),
1591            });
1592            return BTreeSet::new();
1593        };
1594        let Some(source) = entry.syntax.as_deref() else {
1595            self.record_stop(MatchStop::GrammarDefect {
1596                offset: 0,
1597                code: "missingReferencedGrammar",
1598                detail: format!("{category} reference <{}> has no syntax", reference.name),
1599            });
1600            return BTreeSet::new();
1601        };
1602        let key = (reference.category, reference.name.clone());
1603        let expression = match self
1604            .grammar_cache
1605            .entry(key)
1606            .or_insert_with(|| {
1607                if self.cache_registered_grammars {
1608                    cached_pinned_vds_expression(source)
1609                } else {
1610                    VdsParser::new(source).parse().map(Arc::new)
1611                }
1612            })
1613            .clone()
1614        {
1615            Ok(expression) => expression,
1616            Err(error) => {
1617                self.record_stop(MatchStop::GrammarDefect {
1618                    offset: error.offset,
1619                    code: error.code,
1620                    detail: format!("referenced grammar <{}>: {}", reference.name, error.detail),
1621                });
1622                return BTreeSet::new();
1623            }
1624        };
1625        if reference.category == ReferenceCategory::Function {
1626            return self.match_function_reference(
1627                reference,
1628                expression.as_ref(),
1629                components,
1630                position,
1631                reference_depth + 1,
1632            );
1633        }
1634        self.match_expression(
1635            expression.as_ref(),
1636            components,
1637            position,
1638            reference_depth + 1,
1639        )
1640    }
1641
1642    fn match_function_reference(
1643        &mut self,
1644        reference: &VdsReference,
1645        expression: &VdsExpression,
1646        components: &[CssValueComponentV0],
1647        position: usize,
1648        reference_depth: usize,
1649    ) -> BTreeSet<usize> {
1650        let name = reference.name.trim_end_matches("()");
1651        let whole_component =
1652            self.match_expression(expression, components, position, reference_depth);
1653        if !whole_component.is_empty() {
1654            return whole_component;
1655        }
1656        self.match_function(name, expression, components, position, reference_depth)
1657    }
1658
1659    fn consume_step(&mut self, reference: Option<String>) -> bool {
1660        self.match_steps += 1;
1661        if self.match_steps <= self.budget.max_match_steps {
1662            return true;
1663        }
1664        self.record_stop(MatchStop::Budget {
1665            kind: CssValueGrammarBudgetKindV0::MatchSteps,
1666            limit: self.budget.max_match_steps,
1667            reference,
1668        });
1669        false
1670    }
1671
1672    fn cap_states(
1673        &mut self,
1674        mut states: BTreeSet<usize>,
1675        reference: Option<String>,
1676    ) -> BTreeSet<usize> {
1677        if states.len() <= self.budget.max_states {
1678            return states;
1679        }
1680        self.record_stop(MatchStop::Budget {
1681            kind: CssValueGrammarBudgetKindV0::CandidateStates,
1682            limit: self.budget.max_states,
1683            reference,
1684        });
1685        states.clear();
1686        states
1687    }
1688
1689    fn record_stop(&mut self, stop: MatchStop) {
1690        if self.first_stop.is_none() {
1691            self.first_stop = Some(stop);
1692        }
1693    }
1694}
1695
1696fn match_literal(
1697    literal: &str,
1698    components: &[CssValueComponentV0],
1699    position: usize,
1700) -> BTreeSet<usize> {
1701    components
1702        .get(position)
1703        .filter(|component| component.text.eq_ignore_ascii_case(literal))
1704        .map(|_| BTreeSet::from([position + 1]))
1705        .unwrap_or_default()
1706}
1707
1708fn is_comma_literal(expression: &VdsExpression) -> bool {
1709    matches!(expression, VdsExpression::Literal(literal) if literal == ",")
1710}
1711
1712fn is_unitless_zero(component: &CssValueComponentV0) -> bool {
1713    matches!(component.kind, CssValueComponentKindV0::Number)
1714        && parse_numeric_value_with_unit(component.text.as_str())
1715            .is_some_and(|numeric| numeric.value == 0.0 && numeric.unit.is_empty())
1716}
1717
1718fn match_builtin_reference(
1719    reference: &VdsReference,
1720    components: &[CssValueComponentV0],
1721    position: usize,
1722) -> Option<BTreeSet<usize>> {
1723    if reference.category != ReferenceCategory::Type {
1724        return None;
1725    }
1726    if matches!(
1727        reference.name.as_str(),
1728        "declaration-value" | "any-value" | "whole-value"
1729    ) {
1730        return Some(((position + 1)..=components.len()).collect());
1731    }
1732    if !is_builtin_reference_name(reference.name.as_str()) {
1733        return None;
1734    }
1735    let Some(component) = components.get(position) else {
1736        return Some(BTreeSet::new());
1737    };
1738    let kind = classify_registered_property_declared_value_v0(component.text.as_str());
1739    let accepted = match reference.name.as_str() {
1740        "number" | "number-token" => {
1741            matches!(
1742                kind,
1743                DeclaredValueKindV0::Number | DeclaredValueKindV0::Integer
1744            )
1745        }
1746        "integer" => matches!(kind, DeclaredValueKindV0::Integer),
1747        "length" => {
1748            matches!(
1749                kind,
1750                DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Length)
1751            ) || is_unitless_zero(component)
1752        }
1753        "percentage" | "percentage-token" => matches!(
1754            kind,
1755            DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage)
1756        ),
1757        "length-percentage" => {
1758            matches!(
1759                kind,
1760                DeclaredValueKindV0::Dimension(
1761                    DeclaredNumericTypeV0::Length | DeclaredNumericTypeV0::Percentage
1762                )
1763            ) || is_unitless_zero(component)
1764        }
1765        "angle" => matches!(
1766            kind,
1767            DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Angle)
1768        ),
1769        "time" => matches!(
1770            kind,
1771            DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Time)
1772        ),
1773        "resolution" => matches!(
1774            kind,
1775            DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Resolution)
1776        ),
1777        "hex-color" => matches!(kind, DeclaredValueKindV0::HexColor),
1778        "named-color" => matches!(kind, DeclaredValueKindV0::ColorKeyword(_)),
1779        "custom-ident" => {
1780            matches!(component.kind, CssValueComponentKindV0::Ident)
1781                && !matches!(kind, DeclaredValueKindV0::CssWide)
1782        }
1783        "ident" | "ident-token" => matches!(component.kind, CssValueComponentKindV0::Ident),
1784        "dashed-ident" | "custom-property-name" => {
1785            matches!(component.kind, CssValueComponentKindV0::Ident)
1786                && component.text.starts_with("--")
1787        }
1788        "string" | "string-token" => matches!(kind, DeclaredValueKindV0::QuotedString),
1789        "url" | "url-token" => matches!(kind, DeclaredValueKindV0::Url),
1790        "image" => matches!(
1791            kind,
1792            DeclaredValueKindV0::ImageFunction | DeclaredValueKindV0::Url
1793        ),
1794        "transform-function" => matches!(kind, DeclaredValueKindV0::TransformFunction),
1795        "alpha-value" => matches!(
1796            kind,
1797            DeclaredValueKindV0::Number
1798                | DeclaredValueKindV0::Integer
1799                | DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage)
1800        ),
1801        "zero" => parse_numeric_value_with_unit(component.text.as_str())
1802            .is_some_and(|numeric| numeric.value == 0.0),
1803        "dimension-token" => matches!(component.kind, CssValueComponentKindV0::Dimension),
1804        "hash-token" => matches!(component.kind, CssValueComponentKindV0::Hash),
1805        "function-token" => matches!(component.kind, CssValueComponentKindV0::Function { .. }),
1806        "comma-token" => matches!(component.kind, CssValueComponentKindV0::Comma),
1807        _ => false,
1808    };
1809    let accepted =
1810        accepted && numeric_range_accepts(reference.range.as_ref(), component.text.as_str());
1811    Some(accepted.then_some(position + 1).into_iter().collect())
1812}
1813
1814fn is_builtin_reference_name(name: &str) -> bool {
1815    matches!(
1816        name,
1817        "number"
1818            | "number-token"
1819            | "integer"
1820            | "length"
1821            | "percentage"
1822            | "percentage-token"
1823            | "length-percentage"
1824            | "angle"
1825            | "time"
1826            | "resolution"
1827            | "hex-color"
1828            | "named-color"
1829            | "custom-ident"
1830            | "ident"
1831            | "ident-token"
1832            | "dashed-ident"
1833            | "custom-property-name"
1834            | "string"
1835            | "string-token"
1836            | "url"
1837            | "url-token"
1838            | "image"
1839            | "transform-function"
1840            | "alpha-value"
1841            | "zero"
1842            | "dimension-token"
1843            | "hash-token"
1844            | "function-token"
1845            | "comma-token"
1846    )
1847}
1848
1849fn numeric_range_accepts(range: Option<&NumericRange>, source: &str) -> bool {
1850    let Some(range) = range else {
1851        return true;
1852    };
1853    let Some(numeric) = parse_numeric_value_with_unit(source) else {
1854        return false;
1855    };
1856    let above_min = range
1857        .min
1858        .as_deref()
1859        .and_then(|value| value.parse::<f64>().ok())
1860        .is_none_or(|minimum| numeric.value >= minimum);
1861    let below_max = range
1862        .max
1863        .as_deref()
1864        .and_then(|value| value.parse::<f64>().ok())
1865        .is_none_or(|maximum| numeric.value <= maximum);
1866    above_min && below_max
1867}
1868
1869fn component_locus(components: &[CssValueComponentV0]) -> CssValueGrammarLocusV0 {
1870    match (components.first(), components.last()) {
1871        (Some(first), Some(last)) => CssValueGrammarLocusV0 {
1872            start: first.span.start,
1873            end: last.span.end,
1874        },
1875        _ => CssValueGrammarLocusV0 { start: 0, end: 0 },
1876    }
1877}
1878
1879fn grammar_defect(
1880    grammar: &str,
1881    offset: usize,
1882    code: impl Into<String>,
1883    detail: impl Into<String>,
1884) -> CssValueGrammarVerdictV0 {
1885    CssValueGrammarVerdictV0::GrammarDefect {
1886        grammar: grammar.to_string(),
1887        offset,
1888        code: code.into(),
1889        detail: detail.into(),
1890    }
1891}
1892
1893fn strip_matching_quotes(source: &str) -> &str {
1894    if source.len() >= 2 {
1895        let bytes = source.as_bytes();
1896        if matches!(
1897            (bytes[0], bytes[source.len() - 1]),
1898            (b'\'', b'\'') | (b'"', b'"')
1899        ) {
1900            return &source[1..source.len() - 1];
1901        }
1902    }
1903    source
1904}
1905
1906#[cfg(test)]
1907mod tests {
1908    use std::sync::Arc;
1909
1910    use omena_spec_audit::spec_grammar_registry;
1911    use omena_value_lattice::ValueNodeV0;
1912
1913    use super::{
1914        CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0, CssValueGrammarBudgetKindV0,
1915        CssValueGrammarBudgetV0, CssValueGrammarVerdictV0, CssValueValidationClassV0,
1916        CssValueValidationReasonV0, adjudicate_css_value_validation,
1917        audit_css_value_grammar_registry_v0, cached_pinned_vds_expression,
1918        match_and_type_css_value_grammar_v0, match_and_type_standard_property_value_v0,
1919        match_css_value_grammar_v0, match_standard_property_value_v0,
1920        validate_registered_property_value_v0, validate_standard_property_value_v0,
1921    };
1922    use crate::{
1923        AbstractCssTypedValueV0, AbstractCssValueV0, DeclaredValueKindV0,
1924        classify_registered_property_declared_value_v0,
1925    };
1926
1927    fn assert_matches(grammar: &str, value: &str) {
1928        let verdict = match_css_value_grammar_v0(
1929            grammar,
1930            value,
1931            spec_grammar_registry(),
1932            CssValueGrammarBudgetV0::default(),
1933        );
1934        assert!(
1935            verdict.is_matched(),
1936            "{grammar:?} should match {value:?}: {verdict:?}"
1937        );
1938    }
1939
1940    fn assert_unmatched(grammar: &str, value: &str) {
1941        let verdict = match_css_value_grammar_v0(
1942            grammar,
1943            value,
1944            spec_grammar_registry(),
1945            CssValueGrammarBudgetV0::default(),
1946        );
1947        assert!(
1948            verdict.is_definite_mismatch(),
1949            "{grammar:?} should reject {value:?}: {verdict:?}"
1950        );
1951    }
1952
1953    #[test]
1954    fn parsed_grammar_cache_reuses_the_immutable_expression() {
1955        let grammar = "<length> | cache-sentinel";
1956        let first = cached_pinned_vds_expression(grammar);
1957        let second = cached_pinned_vds_expression(grammar);
1958
1959        assert!(matches!(
1960            (&first, &second),
1961            (Ok(first), Ok(second)) if Arc::ptr_eq(first, second)
1962        ));
1963    }
1964
1965    #[test]
1966    fn grammar_conformance_covers_all_combinators_and_multipliers() {
1967        for (grammar, value) in [
1968            ("<length> <color>", "1px red"),
1969            ("<length> && <color>", "red 1px"),
1970            ("<length> || <color>", "red"),
1971            ("auto | <length>", "auto"),
1972            ("[ auto | <length> ]?", ""),
1973            ("<length>*", "1px 2px"),
1974            ("<length>+", "1px 2px"),
1975            ("<length>#", "1px, 2px"),
1976            ("<length>{2,3}", "1px 2px 3px"),
1977            ("<length>#{2}", "1px, 2px"),
1978            ("[ <length>? <color>? ]!", "red"),
1979            ("rgb( <number>#{3} )", "rgb(1, 2, 3)"),
1980        ] {
1981            assert_matches(grammar, value);
1982        }
1983        for (grammar, value) in [
1984            ("<length> <color>", "red 1px"),
1985            ("<length> && <color>", "1px"),
1986            ("<length> || <color>", "auto"),
1987            ("<length>+", ""),
1988            ("<length>#", "1px 2px"),
1989            ("<length>{2,3}", "1px"),
1990            ("[ <length>? <color>? ]!", ""),
1991            ("rgb( <number>#{3} )", "rgb(1, 2)"),
1992        ] {
1993            assert_unmatched(grammar, value);
1994        }
1995    }
1996
1997    #[test]
1998    fn combinator_precedence_is_juxtaposition_then_and_then_double_or_then_or() {
1999        let grammar = "a b && c || d | e";
2000        for value in ["c a b", "a b c", "d", "e"] {
2001            assert_matches(grammar, value);
2002        }
2003        for value in ["a c", "b c", "a b d"] {
2004            assert_unmatched(grammar, value);
2005        }
2006    }
2007
2008    #[test]
2009    fn reference_depth_exhaustion_is_typed_and_provenanced() {
2010        let verdict = match_css_value_grammar_v0(
2011            "<calc-sum>",
2012            "calc(1px + 2px)",
2013            spec_grammar_registry(),
2014            CssValueGrammarBudgetV0 {
2015                max_reference_depth: 0,
2016                ..CssValueGrammarBudgetV0::default()
2017            },
2018        );
2019        assert!(matches!(
2020            verdict,
2021            CssValueGrammarVerdictV0::NotMatchedWithinBudget {
2022                budget: CssValueGrammarBudgetKindV0::ReferenceDepth,
2023                limit: 0,
2024                reference: Some(reference),
2025                ..
2026            } if reference == "calc-sum"
2027        ));
2028    }
2029
2030    #[test]
2031    fn malformed_grammar_is_a_defect_not_a_mismatch() {
2032        let verdict = match_css_value_grammar_v0(
2033            "[ <length> | <color>",
2034            "1px",
2035            spec_grammar_registry(),
2036            CssValueGrammarBudgetV0::default(),
2037        );
2038        assert!(matches!(
2039            verdict,
2040            CssValueGrammarVerdictV0::GrammarDefect { .. }
2041        ));
2042    }
2043
2044    #[test]
2045    fn property_and_type_references_use_the_pinned_registry() {
2046        assert_matches("<'box-sizing'>", "border-box");
2047        assert_matches("<color>", "rebeccapurple");
2048        assert_matches("<rgb()>", "rgb(1 2 3)");
2049        assert!(match_standard_property_value_v0("box-sizing", "content-box").is_matched());
2050        assert!(
2051            match_standard_property_value_v0("box-sizing", "inline-box").is_definite_mismatch()
2052        );
2053    }
2054
2055    #[test]
2056    fn numeric_reference_ranges_are_enforced() {
2057        assert_matches("<number [0,1]>", "0.5");
2058        assert_unmatched("<number [0,1]>", "2");
2059        assert_matches("<length [0,∞]>", "0px");
2060        assert_unmatched("<length [0,∞]>", "-1px");
2061    }
2062
2063    #[test]
2064    fn unitless_zero_matches_length_references_without_widening_other_dimensions() {
2065        for grammar in ["<length>", "<length-percentage>"] {
2066            assert_matches(grammar, "0");
2067        }
2068        for grammar in ["<percentage>", "<angle>", "<time>", "<resolution>"] {
2069            assert_unmatched(grammar, "0");
2070        }
2071        for value in ["1", "-1", "0.5"] {
2072            assert_unmatched("<length>", value);
2073            assert_unmatched("<length-percentage>", value);
2074        }
2075        let calc_verdict = match_css_value_grammar_v0(
2076            "<length> | <calc-sum>",
2077            "calc(0 + 2px)",
2078            spec_grammar_registry(),
2079            CssValueGrammarBudgetV0::default(),
2080        );
2081        let calc_bytes = serde_json::to_vec(&calc_verdict);
2082        assert!(
2083            calc_bytes.is_ok(),
2084            "calc matcher verdict must remain serializable: {calc_bytes:?}"
2085        );
2086        let Ok(calc_bytes) = calc_bytes else {
2087            return;
2088        };
2089        assert_eq!(
2090            calc_bytes,
2091            br#"{"kind":"grammarDefect","grammar":"<length> | <calc-sum>","offset":0,"code":"missingReferencedGrammar","detail":"types reference <dimension> has no syntax"}"#
2092        );
2093    }
2094
2095    #[test]
2096    fn standard_properties_accept_unitless_zero_without_retyping_integer_consumers() {
2097        for (property, value) in [
2098            ("padding", "0"),
2099            ("margin", "0 auto"),
2100            ("border-width", "0 4px 6px"),
2101            ("width", "0"),
2102            ("z-index", "0"),
2103            ("opacity", "0"),
2104        ] {
2105            let verdict = validate_standard_property_value_v0(property, value);
2106            assert_eq!(
2107                verdict.class,
2108                CssValueValidationClassV0::Valid,
2109                "{property}: {value} should be valid: {verdict:?}"
2110            );
2111        }
2112        assert_eq!(
2113            classify_registered_property_declared_value_v0("0"),
2114            DeclaredValueKindV0::Integer
2115        );
2116    }
2117
2118    #[test]
2119    fn nullable_all_in_any_order_operands_can_satisfy_their_slots() {
2120        let grammar = "[ alpha? && [ none | beta ] && gamma? ]";
2121        for value in ["none", "alpha none", "none gamma", "gamma none alpha"] {
2122            assert_matches(grammar, value);
2123        }
2124        assert_unmatched(grammar, "none unexpected");
2125    }
2126
2127    #[test]
2128    fn nested_property_references_keep_inner_comma_repetition_reachable() {
2129        assert_matches("<'box-shadow-color'>", "red, blue");
2130        assert_unmatched("<'box-shadow-color'>", "red blue");
2131
2132        let grammar = "[ <'box-shadow-color'>? && [ none | <length>{2} ] [ <'box-shadow-blur'> <'box-shadow-spread'>? ]? && <'box-shadow-position'>? ]";
2133        assert_matches(grammar, "red none");
2134        assert_unmatched(grammar, "red none unexpected");
2135    }
2136
2137    #[test]
2138    fn sequence_omits_a_comma_only_with_an_omitted_adjacent_component() {
2139        let grammar = "<length>? , <color>";
2140        assert_matches(grammar, "red");
2141        assert_matches(grammar, "1px, red");
2142        assert_unmatched(grammar, ", red");
2143        assert_unmatched(grammar, "1px red");
2144    }
2145
2146    #[test]
2147    fn standard_keyword_grammars_remain_precise_through_nested_expansion() {
2148        for (property, value) in [("box-shadow", "none"), ("background", "transparent")] {
2149            let verdict = validate_standard_property_value_v0(property, value);
2150            assert_eq!(
2151                verdict.class,
2152                CssValueValidationClassV0::Valid,
2153                "{property}: {value} should be valid: {verdict:?}"
2154            );
2155        }
2156        for (property, value) in [
2157            ("box-shadow", "1px nonsense"),
2158            ("background", ", transparent"),
2159        ] {
2160            let verdict = validate_standard_property_value_v0(property, value);
2161            assert_ne!(
2162                verdict.class,
2163                CssValueValidationClassV0::Valid,
2164                "{property}: {value} must not be accepted: {verdict:?}"
2165            );
2166        }
2167    }
2168
2169    #[test]
2170    fn reviewed_compatibility_syntax_and_boundary_policy_are_applied_independently() {
2171        let compatibility_value =
2172            validate_standard_property_value_v0("-webkit-background-clip", "text");
2173        assert_eq!(compatibility_value.class, CssValueValidationClassV0::Valid);
2174        assert_eq!(
2175            compatibility_value.reason,
2176            CssValueValidationReasonV0::GrammarMatched
2177        );
2178        let registry = spec_grammar_registry();
2179        let compatibility_entry = registry.entry("properties", "-webkit-background-clip");
2180        assert!(
2181            compatibility_entry.is_some(),
2182            "compatibility property must remain registered"
2183        );
2184        let Some(compatibility_entry) = compatibility_entry else {
2185            return;
2186        };
2187        assert!(compatibility_entry.override_provenance.is_some());
2188
2189        let forward_tier =
2190            validate_standard_property_value_v0("background", "definitely-not-a-background");
2191        assert_eq!(
2192            forward_tier.class,
2193            CssValueValidationClassV0::NotValidatable
2194        );
2195        assert_eq!(
2196            forward_tier.reason,
2197            CssValueValidationReasonV0::ForwardTierGrammar
2198        );
2199        assert!(forward_tier.verdict.is_definite_mismatch());
2200
2201        let in_boundary = validate_standard_property_value_v0("border-top", "1px nonsense red");
2202        assert_eq!(in_boundary.class, CssValueValidationClassV0::Invalid);
2203        assert_eq!(
2204            in_boundary.reason,
2205            CssValueValidationReasonV0::GrammarUnmatched
2206        );
2207    }
2208
2209    #[test]
2210    fn pinned_registry_rows_are_all_accounted_for_by_the_grammar_parser() {
2211        const MIN_PINNED_REGISTRY_ENTRY_COUNT: usize = 1_700;
2212
2213        let registry = spec_grammar_registry();
2214        let audit = audit_css_value_grammar_registry_v0(registry);
2215        assert_eq!(audit.total_entry_count, registry.total_entry_count());
2216        assert!(
2217            audit.total_entry_count >= MIN_PINNED_REGISTRY_ENTRY_COUNT,
2218            "pinned registry unexpectedly shrank below the audited coverage floor"
2219        );
2220        assert_eq!(audit.categories.len(), 5);
2221        assert_eq!(
2222            audit.parsed_entry_count + audit.missing_syntax_count + audit.grammar_defect_count,
2223            audit.total_entry_count
2224        );
2225        let properties = audit
2226            .categories
2227            .iter()
2228            .find(|category| category.category == "properties");
2229        assert_eq!(
2230            properties.map(|category| (
2231                category.entry_count,
2232                category.parsed_entry_count,
2233                category.missing_syntax_count,
2234                category.grammar_defect_count,
2235            )),
2236            Some((815, 810, 5, 0))
2237        );
2238        assert_eq!(
2239            (
2240                audit.parsed_entry_count,
2241                audit.missing_syntax_count,
2242                audit.grammar_defect_count,
2243            ),
2244            (1_526, 132, 57)
2245        );
2246    }
2247
2248    #[test]
2249    fn matched_compounds_project_through_existing_typed_and_lattice_domains() {
2250        let border = match_and_type_standard_property_value_v0("border-top", "1px solid red");
2251        assert!(border.verdict.is_matched(), "{:?}", border.verdict);
2252        assert!(matches!(
2253            &border.abstract_value,
2254            AbstractCssValueV0::Exact {
2255                typed: Some(typed), ..
2256            } if matches!(
2257                typed.as_ref(),
2258                AbstractCssTypedValueV0::Compound { leaves } if leaves.len() == 3
2259            )
2260        ));
2261        assert!(matches!(
2262            border.projection.as_ref().map(|projection| projection.lattice.root()),
2263            Some(ValueNodeV0::List { items, .. }) if items.len() == 3
2264        ));
2265
2266        let calc = match_and_type_css_value_grammar_v0(
2267            "calc( <length> '+' <length> )",
2268            "calc(1px + 2px)",
2269            spec_grammar_registry(),
2270            CssValueGrammarBudgetV0::default(),
2271        );
2272        assert!(calc.verdict.is_matched(), "{:?}", calc.verdict);
2273        assert!(matches!(
2274            calc.projection.as_ref().map(|projection| projection.lattice.root()),
2275            Some(ValueNodeV0::Function { name, arguments, .. })
2276                if *name == "calc" && arguments.len() == 3
2277        ));
2278
2279        let font_families =
2280            match_and_type_standard_property_value_v0("font-family", "serif, sans-serif");
2281        assert!(
2282            font_families.verdict.is_matched(),
2283            "{:?}",
2284            font_families.verdict
2285        );
2286        assert!(matches!(
2287            font_families
2288                .projection
2289                .as_ref()
2290                .map(|projection| projection.lattice.root()),
2291            Some(ValueNodeV0::List { .. })
2292        ));
2293    }
2294
2295    #[test]
2296    fn rejected_value_preserves_raw_bytes_and_carries_the_match_locus() {
2297        let source = "  1px nonsense red  ";
2298        let result = match_and_type_standard_property_value_v0("border-top", source);
2299        assert!(matches!(
2300            result.verdict,
2301            CssValueGrammarVerdictV0::Unmatched {
2302                grammar,
2303                locus,
2304            } if grammar == "<line-width> || <line-style> || <color>"
2305                && locus.start == 2
2306                && locus.end == source.len() - 2
2307        ));
2308        assert_eq!(
2309            result.abstract_value,
2310            AbstractCssValueV0::Raw {
2311                value: source.to_string(),
2312            }
2313        );
2314        assert!(result.projection.is_none());
2315    }
2316
2317    #[test]
2318    fn validation_keeps_invalid_and_not_validatable_outcomes_distinct() {
2319        let invalid = validate_standard_property_value_v0("border-top", "1px nonsense red");
2320        assert_eq!(invalid.class, CssValueValidationClassV0::Invalid);
2321        assert_eq!(invalid.reason, CssValueValidationReasonV0::GrammarUnmatched);
2322
2323        let defect = validate_registered_property_value_v0("<future-value>", "1px");
2324        assert_eq!(defect.class, CssValueValidationClassV0::NotValidatable);
2325        assert_eq!(defect.reason, CssValueValidationReasonV0::GrammarDefect);
2326
2327        let budget_verdict = match_css_value_grammar_v0(
2328            "<calc-sum>",
2329            "calc(1px + 2px)",
2330            spec_grammar_registry(),
2331            CssValueGrammarBudgetV0 {
2332                max_reference_depth: 0,
2333                ..CssValueGrammarBudgetV0::default()
2334            },
2335        );
2336        let budget = adjudicate_css_value_validation("1px", budget_verdict);
2337        assert_eq!(budget.class, CssValueValidationClassV0::NotValidatable);
2338        assert_eq!(
2339            budget.reason,
2340            CssValueValidationReasonV0::MatchBudgetExhausted
2341        );
2342
2343        let deferred = validate_standard_property_value_v0("width", "var(--width)");
2344        assert_eq!(deferred.class, CssValueValidationClassV0::NotValidatable);
2345        assert_eq!(
2346            deferred.reason,
2347            CssValueValidationReasonV0::DeferredSubstitution
2348        );
2349    }
2350
2351    #[test]
2352    fn validation_distinguishes_negative_dimensions_from_vendor_identifiers() {
2353        let valid_negative = validate_standard_property_value_v0("margin", "-10px");
2354        assert_eq!(valid_negative.class, CssValueValidationClassV0::Valid);
2355        assert_eq!(
2356            valid_negative.reason,
2357            CssValueValidationReasonV0::GrammarMatched
2358        );
2359        assert!(valid_negative.verdict.is_matched());
2360
2361        let invalid_negative = validate_standard_property_value_v0("margin", "-10px totally-bogus");
2362        assert_eq!(invalid_negative.class, CssValueValidationClassV0::Invalid);
2363        assert_eq!(
2364            invalid_negative.reason,
2365            CssValueValidationReasonV0::GrammarUnmatched
2366        );
2367        assert!(invalid_negative.verdict.is_definite_mismatch());
2368
2369        let vendor_identifier =
2370            validate_standard_property_value_v0("box-sizing", "-webkit-border-box");
2371        assert_eq!(
2372            vendor_identifier.class,
2373            CssValueValidationClassV0::NotValidatable
2374        );
2375        assert_eq!(
2376            vendor_identifier.reason,
2377            CssValueValidationReasonV0::VendorExtension
2378        );
2379        assert!(vendor_identifier.verdict.is_definite_mismatch());
2380    }
2381
2382    #[test]
2383    fn validation_consumer_policy_table_covers_every_live_consumer() {
2384        assert_eq!(CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0.len(), 4);
2385        assert_eq!(
2386            CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0
2387                .iter()
2388                .map(|policy| policy.consumer)
2389                .collect::<Vec<_>>(),
2390            vec![
2391                "checker.registeredPropertyTypeMismatch",
2392                "checker.invalidPropertyValue",
2393                "scss.nativeCssFunctionParameter",
2394                "scss.nativeCssFunctionReturn",
2395            ]
2396        );
2397        for policy in CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0 {
2398            assert_eq!(policy.matched, "accept");
2399            assert!(matches!(policy.unmatched, "diagnostic" | "reject"));
2400            assert!(matches!(
2401                policy.forward_tier_unmatched,
2402                "not-applicable" | "not-validatable"
2403            ));
2404            assert!(matches!(policy.grammar_defect, "silent" | "unknown"));
2405            assert!(matches!(policy.budget_exhausted, "silent" | "unknown"));
2406        }
2407    }
2408}