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