Skip to main content

omena_abstract_value/
value_grammar.rs

1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2use std::sync::{Arc, OnceLock, RwLock};
3
4use omena_cascade::{CascadeStandardValueValidatorV0, CascadeStandardValueVerdictV0};
5use omena_evidence_graph::{
6    EvidenceNodeKeyV0, EvidenceNodeSeedV0, ExternalToolRunWitnessV0, FamilyStampV0, GuaranteeKindV0,
7};
8use omena_spec_audit::{
9    SpecGrammarBoundaryClassificationV0, SpecGrammarRegistryV0, spec_grammar_registry,
10};
11use omena_syntax::ident::{
12    CanonicalStandardPropertyNameV0, PropertyNameV0, is_custom_property_name,
13};
14use omena_value_lattice::{
15    CssValueComponentKindV0, CssValueComponentV0, DeclarationValueLensV0, ValueNodeV0,
16    css_value_component_stream, declaration_value_lens, parse_numeric_value_with_unit,
17};
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20
21const CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE: &str =
22    include_str!("../data/closed-world-keyword-closure-certificate.json");
23const CLOSED_WORLD_BUILTIN_TOKEN_PROFILES_SOURCE: &str =
24    include_str!("../data/closed-world-builtin-token-profiles.json");
25
26use crate::{
27    AbstractCssTypedScalarValueV0, AbstractCssTypedValueV0, AbstractCssValueV0,
28    DeclaredNumericTypeV0, DeclaredValueKindV0, abstract_css_typed_scalar_from_text,
29    classify_registered_property_declared_value_v0,
30};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct CssValueGrammarBudgetV0 {
35    pub max_match_steps: usize,
36    pub max_reference_depth: usize,
37    pub max_states: usize,
38}
39
40impl Default for CssValueGrammarBudgetV0 {
41    fn default() -> Self {
42        Self {
43            max_match_steps: 50_000,
44            max_reference_depth: 64,
45            max_states: 4_096,
46        }
47    }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub enum CssValueGrammarBudgetKindV0 {
53    MatchSteps,
54    ReferenceDepth,
55    CandidateStates,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct CssValueGrammarLocusV0 {
61    pub start: usize,
62    pub end: usize,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
66#[serde(
67    tag = "kind",
68    rename_all = "camelCase",
69    rename_all_fields = "camelCase"
70)]
71pub enum CssValueGrammarVerdictV0 {
72    Matched {
73        grammar: String,
74        consumed_components: usize,
75    },
76    Unmatched {
77        grammar: String,
78        locus: CssValueGrammarLocusV0,
79    },
80    NotMatchedWithinBudget {
81        grammar: String,
82        locus: CssValueGrammarLocusV0,
83        budget: CssValueGrammarBudgetKindV0,
84        limit: usize,
85        reference: Option<String>,
86    },
87    GrammarDefect {
88        grammar: String,
89        offset: usize,
90        code: String,
91        detail: String,
92    },
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub enum CssValueValidationClassV0 {
98    Valid,
99    Invalid,
100    NotValidatable,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
104#[serde(rename_all = "camelCase")]
105pub enum CssValueValidationReasonV0 {
106    GrammarMatched,
107    GrammarUnmatched,
108    GrammarDefect,
109    MatchBudgetExhausted,
110    DeferredSubstitution,
111    VendorExtension,
112    ForwardTierGrammar,
113    UnvalidatedStandardFunction,
114    MatcherCoverageIncomplete,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub struct CssValueValidationV0 {
120    pub class: CssValueValidationClassV0,
121    pub reason: CssValueValidationReasonV0,
122    pub verdict: CssValueGrammarVerdictV0,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
126#[serde(rename_all = "camelCase")]
127pub struct CssValueValidationConsumerPolicyV0 {
128    pub consumer: &'static str,
129    pub matched: &'static str,
130    pub unmatched: &'static str,
131    pub forward_tier_unmatched: &'static str,
132    pub grammar_defect: &'static str,
133    pub budget_exhausted: &'static str,
134}
135
136pub const CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0: [CssValueValidationConsumerPolicyV0; 5] = [
137    CssValueValidationConsumerPolicyV0 {
138        consumer: "checker.registeredPropertyTypeMismatch",
139        matched: "accept",
140        unmatched: "diagnostic",
141        forward_tier_unmatched: "not-applicable",
142        grammar_defect: "silent",
143        budget_exhausted: "silent",
144    },
145    CssValueValidationConsumerPolicyV0 {
146        consumer: "checker.invalidPropertyValue",
147        matched: "accept",
148        unmatched: "diagnostic",
149        forward_tier_unmatched: "not-validatable",
150        grammar_defect: "silent",
151        budget_exhausted: "silent",
152    },
153    CssValueValidationConsumerPolicyV0 {
154        consumer: "cascade.postSubstitutionStandardProperty",
155        matched: "accept",
156        unmatched: "reject",
157        forward_tier_unmatched: "not-validatable",
158        grammar_defect: "unknown",
159        budget_exhausted: "unknown",
160    },
161    CssValueValidationConsumerPolicyV0 {
162        consumer: "scss.nativeCssFunctionParameter",
163        matched: "accept",
164        unmatched: "reject",
165        forward_tier_unmatched: "not-applicable",
166        grammar_defect: "unknown",
167        budget_exhausted: "unknown",
168    },
169    CssValueValidationConsumerPolicyV0 {
170        consumer: "scss.nativeCssFunctionReturn",
171        matched: "accept",
172        unmatched: "reject",
173        forward_tier_unmatched: "not-applicable",
174        grammar_defect: "unknown",
175        budget_exhausted: "unknown",
176    },
177];
178
179/// Records invocation facts for a development-time value grammar oracle.
180/// The external tool remains a witness; it does not determine matcher truth.
181pub fn css_value_grammar_external_tool_evidence_v0(
182    tool_name: &str,
183    tool_version: &str,
184    input_digest: &str,
185    exit_status: i32,
186) -> EvidenceNodeSeedV0 {
187    let witness = ExternalToolRunWitnessV0 {
188        tool_name: tool_name.to_string(),
189        tool_version: tool_version.to_string(),
190        input_digest: input_digest.to_string(),
191        exit_status,
192    };
193    EvidenceNodeSeedV0::with_family(
194        EvidenceNodeKeyV0::new(
195            "omena-abstract-value.value-grammar-differential",
196            input_digest,
197        ),
198        vec![
199            format!("externalTool:{tool_name}"),
200            format!("toolVersion:{tool_version}"),
201            format!("exitStatus:{exit_status}"),
202        ],
203        GuaranteeKindV0::for_label_less_family(),
204        FamilyStampV0::external_tool(&witness),
205    )
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
209#[serde(rename_all = "camelCase")]
210pub struct CssValueGrammarRegistryAuditV0 {
211    pub total_entry_count: usize,
212    pub parsed_entry_count: usize,
213    pub missing_syntax_count: usize,
214    pub grammar_defect_count: usize,
215    pub categories: Vec<CssValueGrammarCategoryAuditV0>,
216    pub defects: Vec<CssValueGrammarDefectV0>,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
220#[serde(rename_all = "camelCase")]
221pub struct CssValueGrammarCategoryAuditV0 {
222    pub category: String,
223    pub entry_count: usize,
224    pub parsed_entry_count: usize,
225    pub missing_syntax_count: usize,
226    pub grammar_defect_count: usize,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
230#[serde(rename_all = "camelCase")]
231pub struct CssValueGrammarDefectV0 {
232    pub category: String,
233    pub name: String,
234    pub offset: usize,
235    pub code: String,
236    pub detail: String,
237}
238
239#[derive(Debug, Clone, PartialEq)]
240pub struct CssValueGrammarTypedMatchV0<'a> {
241    pub verdict: CssValueGrammarVerdictV0,
242    pub abstract_value: AbstractCssValueV0,
243    pub projection: Option<CssValueTypedProjectionV0<'a>>,
244}
245
246#[derive(Debug, Clone, PartialEq)]
247pub struct CssValueTypedProjectionV0<'a> {
248    pub lattice: DeclarationValueLensV0<'a>,
249    pub scalar_leaves: Vec<AbstractCssTypedScalarValueV0>,
250}
251
252impl CssValueGrammarVerdictV0 {
253    pub const fn is_matched(&self) -> bool {
254        matches!(self, Self::Matched { .. })
255    }
256
257    pub const fn is_definite_mismatch(&self) -> bool {
258        matches!(self, Self::Unmatched { .. })
259    }
260
261    pub const fn is_validatable(&self) -> bool {
262        matches!(self, Self::Matched { .. } | Self::Unmatched { .. })
263    }
264}
265
266/// Parses every grammar supplied by the pinned registry and accounts for every
267/// row. Missing source syntax and unsupported grammar shapes remain explicit
268/// data instead of disappearing from a coverage percentage.
269pub fn audit_css_value_grammar_registry_v0(
270    registry: &SpecGrammarRegistryV0,
271) -> CssValueGrammarRegistryAuditV0 {
272    let mut categories = Vec::new();
273    let mut defects = Vec::new();
274    let mut parsed_entry_count = 0usize;
275    let mut missing_syntax_count = 0usize;
276    for category in ["atrules", "functions", "properties", "selectors", "types"] {
277        let entries = registry.entries(category);
278        let mut category_parsed = 0usize;
279        let mut category_missing = 0usize;
280        let defect_start = defects.len();
281        for entry in entries {
282            let Some(grammar) = entry.syntax.as_deref() else {
283                category_missing += 1;
284                missing_syntax_count += 1;
285                continue;
286            };
287            match VdsParser::new(strip_matching_quotes(grammar.trim())).parse() {
288                Ok(_) => {
289                    category_parsed += 1;
290                    parsed_entry_count += 1;
291                }
292                Err(error) => defects.push(CssValueGrammarDefectV0 {
293                    category: category.to_string(),
294                    name: entry.name.clone(),
295                    offset: error.offset,
296                    code: error.code.to_string(),
297                    detail: error.detail,
298                }),
299            }
300        }
301        categories.push(CssValueGrammarCategoryAuditV0 {
302            category: category.to_string(),
303            entry_count: entries.len(),
304            parsed_entry_count: category_parsed,
305            missing_syntax_count: category_missing,
306            grammar_defect_count: defects.len() - defect_start,
307        });
308    }
309    CssValueGrammarRegistryAuditV0 {
310        total_entry_count: registry.total_entry_count(),
311        parsed_entry_count,
312        missing_syntax_count,
313        grammar_defect_count: defects.len(),
314        categories,
315        defects,
316    }
317}
318
319/// Matches a standard property's value against the grammar supplied by the
320/// pinned specification registry.
321pub fn match_standard_property_value_v0(property: &str, value: &str) -> CssValueGrammarVerdictV0 {
322    let property = PropertyNameV0::from_authored(property);
323    match_standard_property_value_with_coverage_v0(&property, value).0
324}
325
326fn match_standard_property_value_with_coverage_v0(
327    property: &PropertyNameV0,
328    value: &str,
329) -> (CssValueGrammarVerdictV0, bool) {
330    let canonical_property = property.canonical_name();
331    let registry = spec_grammar_registry();
332    let Some(entry) = registry.entry("properties", canonical_property) else {
333        return (
334            grammar_defect(
335                "",
336                0,
337                "unknownProperty",
338                format!("property {canonical_property:?} is absent from the pinned registry"),
339            ),
340            false,
341        );
342    };
343    let Some(grammar) = entry.syntax.as_deref() else {
344        return (
345            grammar_defect(
346                "",
347                0,
348                "missingPropertyGrammar",
349                format!("property {canonical_property:?} has no syntax in the pinned registry"),
350            ),
351            false,
352        );
353    };
354    let matcher_coverage_complete = standard_property_matcher_coverage_complete(property, registry);
355    if matches!(
356        classify_registered_property_declared_value_v0(value),
357        DeclaredValueKindV0::CssWide
358    ) {
359        return (
360            CssValueGrammarVerdictV0::Matched {
361                grammar: grammar.to_string(),
362                consumed_components: 1,
363            },
364            matcher_coverage_complete,
365        );
366    }
367    let components = match css_value_component_stream(value, 0) {
368        Ok(components) => components,
369        Err(error) => {
370            return (
371                grammar_defect(
372                    grammar,
373                    error.span.start,
374                    "invalidValueTokenStream",
375                    error.message,
376                ),
377                false,
378            );
379        }
380    };
381    let normalized = strip_matching_quotes(grammar.trim());
382    let expression = match cached_pinned_vds_expression(normalized) {
383        Ok(expression) => expression,
384        Err(error) => {
385            return (
386                grammar_defect(grammar, error.offset, error.code, error.detail),
387                false,
388            );
389        }
390    };
391    (
392        match_css_value_grammar_components_with_expression_v0(
393            grammar,
394            &components,
395            registry,
396            CssValueGrammarBudgetV0::default(),
397            expression.as_ref(),
398            true,
399        ),
400        matcher_coverage_complete,
401    )
402}
403
404/// Matches a registered custom-property or native-CSS function descriptor.
405pub fn match_registered_property_value_v0(syntax: &str, value: &str) -> CssValueGrammarVerdictV0 {
406    let grammar = strip_matching_quotes(syntax.trim()).trim();
407    if grammar == "*" {
408        return match css_value_component_stream(value, 0) {
409            Ok(components) => CssValueGrammarVerdictV0::Matched {
410                grammar: syntax.to_string(),
411                consumed_components: components.len(),
412            },
413            Err(error) => grammar_defect(
414                syntax,
415                error.span.start,
416                "invalidValueTokenStream",
417                error.message,
418            ),
419        };
420    }
421    if matches!(
422        classify_registered_property_declared_value_v0(value),
423        DeclaredValueKindV0::CssWide
424    ) {
425        return CssValueGrammarVerdictV0::Matched {
426            grammar: syntax.to_string(),
427            consumed_components: 1,
428        };
429    }
430    match_css_value_grammar_v0(
431        grammar,
432        value,
433        spec_grammar_registry(),
434        CssValueGrammarBudgetV0::default(),
435    )
436}
437
438pub fn validate_standard_property_value_v0(property: &str, value: &str) -> CssValueValidationV0 {
439    let property = PropertyNameV0::from_authored(property);
440    let canonical_property = property.canonical_name();
441    let registry = spec_grammar_registry();
442    let classification = registry
443        .entry("properties", canonical_property)
444        .map(|entry| entry.boundary.classification)
445        .unwrap_or(SpecGrammarBoundaryClassificationV0::InBoundary);
446    let (verdict, matcher_coverage_complete) =
447        match_standard_property_value_with_coverage_v0(&property, value);
448    let matcher_coverage_complete = matcher_coverage_complete
449        && standard_property_value_token_kinds_have_closure_authority(&property, value, registry);
450    let closed_world_token_kind_mismatch =
451        matches!(verdict, CssValueGrammarVerdictV0::Unmatched { .. })
452            && standard_property_closed_world_token_kind_mismatch(&property, value, registry);
453    adjudicate_css_value_validation_with_boundary(
454        value,
455        verdict,
456        classification,
457        matcher_coverage_complete,
458        closed_world_token_kind_mismatch,
459    )
460}
461
462/// Product adapter from the spec-derived grammar registry to the cascade
463/// computed-value validation port.
464#[derive(Debug, Clone, Copy, Default)]
465pub struct SpecStandardPropertyValueValidatorV0;
466
467impl CascadeStandardValueValidatorV0 for SpecStandardPropertyValueValidatorV0 {
468    fn validate_standard_property_value(
469        &self,
470        property: &PropertyNameV0,
471        value: &str,
472    ) -> CascadeStandardValueVerdictV0 {
473        match validate_standard_property_value_v0(property.canonical_name(), value).class {
474            CssValueValidationClassV0::Valid => CascadeStandardValueVerdictV0::Matched,
475            CssValueValidationClassV0::Invalid => CascadeStandardValueVerdictV0::Unmatched,
476            CssValueValidationClassV0::NotValidatable => CascadeStandardValueVerdictV0::Unknown,
477        }
478    }
479}
480
481pub fn validate_registered_property_value_v0(syntax: &str, value: &str) -> CssValueValidationV0 {
482    adjudicate_css_value_validation(value, match_registered_property_value_v0(syntax, value))
483}
484
485fn adjudicate_css_value_validation(
486    value: &str,
487    verdict: CssValueGrammarVerdictV0,
488) -> CssValueValidationV0 {
489    adjudicate_css_value_validation_with_boundary(
490        value,
491        verdict,
492        SpecGrammarBoundaryClassificationV0::InBoundary,
493        true,
494        false,
495    )
496}
497
498fn adjudicate_css_value_validation_with_boundary(
499    value: &str,
500    verdict: CssValueGrammarVerdictV0,
501    classification: SpecGrammarBoundaryClassificationV0,
502    matcher_coverage_complete: bool,
503    closed_world_token_kind_mismatch: bool,
504) -> CssValueValidationV0 {
505    let components = css_value_component_stream(value, 0).ok();
506    let has_unvalidated_standard_function = matches!(
507        &verdict,
508        CssValueGrammarVerdictV0::Unmatched { grammar, locus }
509            if classification == SpecGrammarBoundaryClassificationV0::InBoundary
510                && components.as_deref().is_some_and(|components| {
511                    recognized_standard_functions_explain_unmatched_value(
512                        grammar,
513                        components,
514                        *locus,
515                    )
516                })
517    );
518    let (class, reason) = if components
519        .as_deref()
520        .is_some_and(contains_deferred_css_value)
521    {
522        (
523            CssValueValidationClassV0::NotValidatable,
524            CssValueValidationReasonV0::DeferredSubstitution,
525        )
526    } else if components
527        .as_deref()
528        .is_some_and(has_leading_vendor_identifier)
529    {
530        (
531            CssValueValidationClassV0::NotValidatable,
532            CssValueValidationReasonV0::VendorExtension,
533        )
534    } else if has_unvalidated_standard_function {
535        (
536            CssValueValidationClassV0::NotValidatable,
537            CssValueValidationReasonV0::UnvalidatedStandardFunction,
538        )
539    } else {
540        match verdict {
541            CssValueGrammarVerdictV0::Matched { .. } => (
542                CssValueValidationClassV0::Valid,
543                CssValueValidationReasonV0::GrammarMatched,
544            ),
545            CssValueGrammarVerdictV0::Unmatched { .. }
546                if classification == SpecGrammarBoundaryClassificationV0::ForwardTier =>
547            {
548                (
549                    CssValueValidationClassV0::NotValidatable,
550                    CssValueValidationReasonV0::ForwardTierGrammar,
551                )
552            }
553            CssValueGrammarVerdictV0::Unmatched { .. }
554                if !matcher_coverage_complete && !closed_world_token_kind_mismatch =>
555            {
556                (
557                    CssValueValidationClassV0::NotValidatable,
558                    CssValueValidationReasonV0::MatcherCoverageIncomplete,
559                )
560            }
561            CssValueGrammarVerdictV0::Unmatched { .. } => (
562                CssValueValidationClassV0::Invalid,
563                CssValueValidationReasonV0::GrammarUnmatched,
564            ),
565            CssValueGrammarVerdictV0::NotMatchedWithinBudget { .. } => (
566                CssValueValidationClassV0::NotValidatable,
567                CssValueValidationReasonV0::MatchBudgetExhausted,
568            ),
569            CssValueGrammarVerdictV0::GrammarDefect { .. } => (
570                CssValueValidationClassV0::NotValidatable,
571                CssValueValidationReasonV0::GrammarDefect,
572            ),
573        }
574    };
575    CssValueValidationV0 {
576        class,
577        reason,
578        verdict,
579    }
580}
581
582fn recognized_standard_functions_explain_unmatched_value(
583    grammar: &str,
584    components: &[CssValueComponentV0],
585    locus: CssValueGrammarLocusV0,
586) -> bool {
587    let has_locus_function = components.iter().any(|component| {
588        component.span.start < locus.end
589            && locus.start < component.span.end
590            && component_is_recognized_standard_function(component)
591    });
592    if !has_locus_function {
593        return false;
594    }
595
596    let normalized = strip_matching_quotes(grammar.trim());
597    let Ok(expression) = cached_pinned_vds_expression(normalized) else {
598        return false;
599    };
600    let mut context = MatchContext {
601        registry: spec_grammar_registry(),
602        budget: CssValueGrammarBudgetV0::default(),
603        match_steps: 0,
604        first_stop: None,
605        grammar_cache: HashMap::new(),
606        cache_registered_grammars: true,
607        allow_unvalidated_standard_function_references: true,
608    };
609    context
610        .match_expression(expression.as_ref(), components, 0, 0)
611        .contains(&components.len())
612}
613
614fn component_is_recognized_standard_function(component: &CssValueComponentV0) -> bool {
615    matches!(
616        &component.kind,
617        CssValueComponentKindV0::Function { name, .. }
618            if recognized_standard_function_names().contains(name)
619    )
620}
621
622fn recognized_standard_function_names() -> &'static BTreeSet<String> {
623    static NAMES: OnceLock<BTreeSet<String>> = OnceLock::new();
624    NAMES.get_or_init(|| {
625        spec_grammar_registry()
626            .entries("functions")
627            .iter()
628            .filter(|entry| {
629                entry.boundary.classification == SpecGrammarBoundaryClassificationV0::InBoundary
630            })
631            .filter_map(|entry| entry.name.strip_suffix("()"))
632            .filter(|name| !name.starts_with('-') && !is_deferred_css_function_name(name))
633            .map(str::to_string)
634            .collect()
635    })
636}
637
638fn is_deferred_css_function_name(name: &str) -> bool {
639    matches!(name, "var" | "env" | "attr")
640}
641
642fn contains_deferred_css_value(components: &[CssValueComponentV0]) -> bool {
643    components.iter().any(|component| match &component.kind {
644        CssValueComponentKindV0::Function { name, arguments } => {
645            is_deferred_css_function_name(name) || contains_deferred_css_value(arguments)
646        }
647        CssValueComponentKindV0::Parenthesized { values }
648        | CssValueComponentKindV0::Bracketed { values }
649        | CssValueComponentKindV0::Braced { values } => contains_deferred_css_value(values),
650        CssValueComponentKindV0::Ident
651        | CssValueComponentKindV0::Number
652        | CssValueComponentKindV0::Percentage
653        | CssValueComponentKindV0::Dimension
654        | CssValueComponentKindV0::Hash
655        | CssValueComponentKindV0::String
656        | CssValueComponentKindV0::Url
657        | CssValueComponentKindV0::Comma
658        | CssValueComponentKindV0::Slash
659        | CssValueComponentKindV0::Delimiter => false,
660    })
661}
662
663fn has_leading_vendor_identifier(components: &[CssValueComponentV0]) -> bool {
664    components.first().is_some_and(|component| {
665        matches!(component.kind, CssValueComponentKindV0::Ident) && component.text.starts_with('-')
666    })
667}
668
669/// Matches and projects a standard property value into the existing scalar
670/// typed domain plus the existing value-lattice list/function topology.
671pub fn match_and_type_standard_property_value_v0<'a>(
672    property: &str,
673    value: &'a str,
674) -> CssValueGrammarTypedMatchV0<'a> {
675    typed_match_result(match_standard_property_value_v0(property, value), value)
676}
677
678/// Property-independent typed projection for custom grammar consumers.
679pub fn match_and_type_css_value_grammar_v0<'a>(
680    grammar: &str,
681    value: &'a str,
682    registry: &SpecGrammarRegistryV0,
683    budget: CssValueGrammarBudgetV0,
684) -> CssValueGrammarTypedMatchV0<'a> {
685    typed_match_result(
686        match_css_value_grammar_v0(grammar, value, registry, budget),
687        value,
688    )
689}
690
691fn typed_match_result<'a>(
692    verdict: CssValueGrammarVerdictV0,
693    value: &'a str,
694) -> CssValueGrammarTypedMatchV0<'a> {
695    if !verdict.is_matched() {
696        return CssValueGrammarTypedMatchV0 {
697            verdict,
698            abstract_value: AbstractCssValueV0::Raw {
699                value: value.to_string(),
700            },
701            projection: None,
702        };
703    }
704    let components = match css_value_component_stream(value, 0) {
705        Ok(components) => components,
706        Err(error) => {
707            return CssValueGrammarTypedMatchV0 {
708                verdict: grammar_defect(
709                    verdict_grammar(&verdict),
710                    error.span.start,
711                    "typedProjectionTokenStreamDrift",
712                    error.message,
713                ),
714                abstract_value: AbstractCssValueV0::Raw {
715                    value: value.to_string(),
716                },
717                projection: None,
718            };
719        }
720    };
721    let mut scalar_leaves = Vec::new();
722    collect_typed_scalar_leaves(&components, &mut scalar_leaves);
723    let lattice = declaration_value_lens(value, 0);
724    let typed = typed_value_from_projection(&lattice, &scalar_leaves).map(Box::new);
725    CssValueGrammarTypedMatchV0 {
726        verdict,
727        abstract_value: AbstractCssValueV0::Exact {
728            value: value.to_string(),
729            typed,
730        },
731        projection: Some(CssValueTypedProjectionV0 {
732            lattice,
733            scalar_leaves,
734        }),
735    }
736}
737
738fn verdict_grammar(verdict: &CssValueGrammarVerdictV0) -> &str {
739    match verdict {
740        CssValueGrammarVerdictV0::Matched { grammar, .. }
741        | CssValueGrammarVerdictV0::Unmatched { grammar, .. }
742        | CssValueGrammarVerdictV0::NotMatchedWithinBudget { grammar, .. }
743        | CssValueGrammarVerdictV0::GrammarDefect { grammar, .. } => grammar,
744    }
745}
746
747fn collect_typed_scalar_leaves(
748    components: &[CssValueComponentV0],
749    leaves: &mut Vec<AbstractCssTypedScalarValueV0>,
750) {
751    for component in components {
752        if let Some(value) = abstract_css_typed_scalar_from_text(component.text.as_str()) {
753            leaves.push(value);
754            continue;
755        }
756        match &component.kind {
757            CssValueComponentKindV0::Function { arguments, .. }
758            | CssValueComponentKindV0::Parenthesized { values: arguments }
759            | CssValueComponentKindV0::Bracketed { values: arguments }
760            | CssValueComponentKindV0::Braced { values: arguments } => {
761                collect_typed_scalar_leaves(arguments, leaves);
762            }
763            CssValueComponentKindV0::Ident
764            | CssValueComponentKindV0::Number
765            | CssValueComponentKindV0::Percentage
766            | CssValueComponentKindV0::Dimension
767            | CssValueComponentKindV0::Hash
768            | CssValueComponentKindV0::String
769            | CssValueComponentKindV0::Url
770            | CssValueComponentKindV0::Comma
771            | CssValueComponentKindV0::Slash
772            | CssValueComponentKindV0::Delimiter => {}
773        }
774    }
775}
776
777fn typed_value_from_projection(
778    lattice: &DeclarationValueLensV0<'_>,
779    scalar_leaves: &[AbstractCssTypedScalarValueV0],
780) -> Option<AbstractCssTypedValueV0> {
781    match (lattice.root(), scalar_leaves) {
782        (ValueNodeV0::List { .. } | ValueNodeV0::Function { .. }, [_, ..]) | (_, [_, _, ..]) => {
783            Some(AbstractCssTypedValueV0::Compound {
784                leaves: scalar_leaves.to_vec(),
785            })
786        }
787        (_, [value]) => Some(AbstractCssTypedValueV0::Exact {
788            value: value.clone(),
789        }),
790        (_, []) => None,
791    }
792}
793
794/// Matches a value against one CSS Value Definition Syntax expression.
795pub fn match_css_value_grammar_v0(
796    grammar: &str,
797    value: &str,
798    registry: &SpecGrammarRegistryV0,
799    budget: CssValueGrammarBudgetV0,
800) -> CssValueGrammarVerdictV0 {
801    let components = match css_value_component_stream(value, 0) {
802        Ok(components) => components,
803        Err(error) => {
804            return grammar_defect(
805                grammar,
806                error.span.start,
807                "invalidValueTokenStream",
808                error.message,
809            );
810        }
811    };
812    match_css_value_grammar_components_v0(grammar, &components, registry, budget)
813}
814
815/// Property-independent matcher entry point over an already tokenized value.
816pub fn match_css_value_grammar_components_v0(
817    grammar: &str,
818    components: &[CssValueComponentV0],
819    registry: &SpecGrammarRegistryV0,
820    budget: CssValueGrammarBudgetV0,
821) -> CssValueGrammarVerdictV0 {
822    let normalized = strip_matching_quotes(grammar.trim());
823    let expression = match VdsParser::new(normalized).parse() {
824        Ok(expression) => expression,
825        Err(error) => {
826            return grammar_defect(grammar, error.offset, error.code, error.detail);
827        }
828    };
829    match_css_value_grammar_components_with_expression_v0(
830        grammar,
831        components,
832        registry,
833        budget,
834        &expression,
835        false,
836    )
837}
838
839fn match_css_value_grammar_components_with_expression_v0(
840    grammar: &str,
841    components: &[CssValueComponentV0],
842    registry: &SpecGrammarRegistryV0,
843    budget: CssValueGrammarBudgetV0,
844    expression: &VdsExpression,
845    cache_registered_grammars: bool,
846) -> CssValueGrammarVerdictV0 {
847    let locus = component_locus(components);
848    let mut context = MatchContext {
849        registry,
850        budget,
851        match_steps: 0,
852        first_stop: None,
853        grammar_cache: HashMap::new(),
854        cache_registered_grammars,
855        allow_unvalidated_standard_function_references: false,
856    };
857    let ends = context.match_expression(expression, components, 0, 0);
858    if ends.contains(&components.len()) {
859        return CssValueGrammarVerdictV0::Matched {
860            grammar: grammar.to_string(),
861            consumed_components: components.len(),
862        };
863    }
864    if let Some(stop) = context.first_stop {
865        return match stop {
866            MatchStop::Budget {
867                kind,
868                limit,
869                reference,
870            } => CssValueGrammarVerdictV0::NotMatchedWithinBudget {
871                grammar: grammar.to_string(),
872                locus,
873                budget: kind,
874                limit,
875                reference,
876            },
877            MatchStop::GrammarDefect {
878                offset,
879                code,
880                detail,
881            } => grammar_defect(grammar, offset, code, detail),
882        };
883    }
884    CssValueGrammarVerdictV0::Unmatched {
885        grammar: grammar.to_string(),
886        locus,
887    }
888}
889
890#[derive(Debug, Clone, PartialEq, Eq)]
891enum VdsExpression {
892    Literal(String),
893    Reference(VdsReference),
894    Function {
895        name: String,
896        arguments: Box<VdsExpression>,
897    },
898    Sequence(Vec<VdsExpression>),
899    AllInAnyOrder(Vec<VdsExpression>),
900    OneOrMoreInAnyOrder(Vec<VdsExpression>),
901    Choice(Vec<VdsExpression>),
902    Repeat {
903        expression: Box<VdsExpression>,
904        min: usize,
905        max: Option<usize>,
906        comma_separated: bool,
907    },
908    Required(Box<VdsExpression>),
909}
910
911fn standard_property_matcher_coverage_complete(
912    property: &PropertyNameV0,
913    registry: &SpecGrammarRegistryV0,
914) -> bool {
915    let Some(grammar) = registry.syntax("properties", property.canonical_name()) else {
916        return false;
917    };
918    let Ok(expression) = cached_pinned_vds_expression(strip_matching_quotes(grammar.trim())) else {
919        return false;
920    };
921    let mut visiting = HashSet::new();
922    let mut memo = HashMap::new();
923    expression_matcher_coverage_complete(expression.as_ref(), registry, &mut visiting, &mut memo)
924}
925
926fn expression_matcher_coverage_complete(
927    expression: &VdsExpression,
928    registry: &SpecGrammarRegistryV0,
929    visiting: &mut HashSet<(ReferenceCategory, String)>,
930    memo: &mut HashMap<(ReferenceCategory, String), bool>,
931) -> bool {
932    match expression {
933        VdsExpression::Literal(_) => true,
934        VdsExpression::Reference(reference) => {
935            if is_builtin_reference_name(reference.name.as_str()) {
936                return builtin_reference_matcher_coverage_complete(reference.name.as_str());
937            }
938            let key = (reference.category, reference.name.clone());
939            if let Some(complete) = memo.get(&key) {
940                return *complete;
941            }
942            if !visiting.insert(key.clone()) {
943                return true;
944            }
945            let category = match reference.category {
946                ReferenceCategory::Type => "types",
947                ReferenceCategory::Property => "properties",
948                ReferenceCategory::Function => "functions",
949            };
950            let complete = registry
951                .syntax(category, reference.name.as_str())
952                .and_then(|source| cached_pinned_vds_expression(source).ok())
953                .is_some_and(|expression| {
954                    expression_matcher_coverage_complete(
955                        expression.as_ref(),
956                        registry,
957                        visiting,
958                        memo,
959                    )
960                });
961            visiting.remove(&key);
962            memo.insert(key, complete);
963            complete
964        }
965        VdsExpression::Function { arguments, .. }
966        | VdsExpression::Repeat {
967            expression: arguments,
968            ..
969        }
970        | VdsExpression::Required(arguments) => {
971            expression_matcher_coverage_complete(arguments, registry, visiting, memo)
972        }
973        VdsExpression::Sequence(expressions)
974        | VdsExpression::AllInAnyOrder(expressions)
975        | VdsExpression::OneOrMoreInAnyOrder(expressions)
976        | VdsExpression::Choice(expressions) => expressions.iter().all(|expression| {
977            expression_matcher_coverage_complete(expression, registry, visiting, memo)
978        }),
979    }
980}
981
982fn builtin_reference_matcher_coverage_complete(name: &str) -> bool {
983    // Only lexical token families and structurally unrestricted values are
984    // certified here. Numeric dimensions admit a wider CSS math language than
985    // the bounded positive matcher below, while semantic families such as
986    // images, colors, custom identifiers, and transforms have extra validity
987    // rules. Their positive matches are useful, but their negative matches
988    // cannot certify a standards-level rejection.
989    matches!(
990        name,
991        "declaration-value"
992            | "any-value"
993            | "whole-value"
994            | "number-token"
995            | "percentage-token"
996            | "ident"
997            | "ident-token"
998            | "dashed-ident"
999            | "custom-property-name"
1000            | "string"
1001            | "string-token"
1002            | "url"
1003            | "url-token"
1004            | "hex-color"
1005            | "zero"
1006            | "dimension-token"
1007            | "hash-token"
1008            | "function-token"
1009            | "comma-token"
1010    )
1011}
1012
1013#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1014enum ClosedWorldTokenKindV0 {
1015    Ident,
1016    Hash,
1017    Dimension,
1018    Number,
1019    Percentage,
1020    FunctionName,
1021    String,
1022    Url,
1023}
1024
1025#[derive(Debug, Clone, Default, PartialEq, Eq)]
1026struct ClosedWorldTokenDomainV0 {
1027    open: bool,
1028    allowed: BTreeSet<String>,
1029}
1030
1031#[derive(Debug, Clone, Default, PartialEq, Eq)]
1032struct ClosedWorldTokenProfileV0 {
1033    ident: ClosedWorldTokenDomainV0,
1034    hash: ClosedWorldTokenDomainV0,
1035    dimension: ClosedWorldTokenDomainV0,
1036    number: ClosedWorldTokenDomainV0,
1037    percentage: ClosedWorldTokenDomainV0,
1038    function_name: ClosedWorldTokenDomainV0,
1039    string: ClosedWorldTokenDomainV0,
1040    url: ClosedWorldTokenDomainV0,
1041}
1042
1043#[derive(Debug, Deserialize)]
1044#[serde(rename_all = "camelCase")]
1045struct ClosedWorldKeywordClosureCertificateV0 {
1046    schema_version: String,
1047    product: String,
1048    oracle: ClosedWorldKeywordClosureOracleV0,
1049    source: String,
1050    maximum_reference_depth: usize,
1051    property_count: usize,
1052    candidate_pair_count: usize,
1053    accepted_pair_count: usize,
1054    matched_pair_count: usize,
1055    matcher_gap_count: usize,
1056    accepted_pair_digest: String,
1057    certified_properties: BTreeSet<CanonicalStandardPropertyNameV0>,
1058    property_tests: Vec<ClosedWorldKeywordClosurePropertyTestV0>,
1059}
1060
1061#[derive(Debug, Deserialize)]
1062#[serde(rename_all = "camelCase")]
1063struct ClosedWorldKeywordClosurePropertyTestV0 {
1064    property: CanonicalStandardPropertyNameV0,
1065    candidate_pair_count: usize,
1066    tested_pair_count: usize,
1067    matched_pair_count: usize,
1068    matcher_gap_count: usize,
1069    accepted_keywords: Vec<String>,
1070}
1071
1072#[derive(Debug, Default, PartialEq, Eq)]
1073struct ClosedWorldKeywordAuthorityV0 {
1074    certified_properties: BTreeSet<CanonicalStandardPropertyNameV0>,
1075    accepted_keywords_by_property: BTreeMap<CanonicalStandardPropertyNameV0, BTreeSet<String>>,
1076}
1077
1078#[derive(Debug, Deserialize)]
1079struct ClosedWorldKeywordClosureOracleV0 {
1080    name: ClosedWorldKeywordClosureOracleNameV0,
1081    version: String,
1082}
1083
1084#[derive(Debug, Deserialize, PartialEq, Eq)]
1085enum ClosedWorldKeywordClosureOracleNameV0 {
1086    #[serde(rename = "css-tree")]
1087    CssTree,
1088}
1089
1090#[derive(Debug, Deserialize)]
1091#[serde(rename_all = "camelCase")]
1092struct ClosedWorldBuiltinTokenProfilesV0 {
1093    schema_version: String,
1094    product: String,
1095    oracle: ClosedWorldKeywordClosureOracleV0,
1096    profile_count: usize,
1097    profiles: Vec<ClosedWorldBuiltinTokenProfileV0>,
1098}
1099
1100#[derive(Debug, Deserialize)]
1101#[serde(rename_all = "camelCase")]
1102struct ClosedWorldBuiltinTokenProfileV0 {
1103    name: String,
1104    authority: String,
1105    open_token_kinds: BTreeSet<String>,
1106    allowed_values: HashMap<String, Vec<String>>,
1107}
1108
1109impl ClosedWorldTokenProfileV0 {
1110    fn domain(&self, kind: ClosedWorldTokenKindV0) -> &ClosedWorldTokenDomainV0 {
1111        match kind {
1112            ClosedWorldTokenKindV0::Ident => &self.ident,
1113            ClosedWorldTokenKindV0::Hash => &self.hash,
1114            ClosedWorldTokenKindV0::Dimension => &self.dimension,
1115            ClosedWorldTokenKindV0::Number => &self.number,
1116            ClosedWorldTokenKindV0::Percentage => &self.percentage,
1117            ClosedWorldTokenKindV0::FunctionName => &self.function_name,
1118            ClosedWorldTokenKindV0::String => &self.string,
1119            ClosedWorldTokenKindV0::Url => &self.url,
1120        }
1121    }
1122
1123    fn domain_mut(&mut self, kind: ClosedWorldTokenKindV0) -> &mut ClosedWorldTokenDomainV0 {
1124        match kind {
1125            ClosedWorldTokenKindV0::Ident => &mut self.ident,
1126            ClosedWorldTokenKindV0::Hash => &mut self.hash,
1127            ClosedWorldTokenKindV0::Dimension => &mut self.dimension,
1128            ClosedWorldTokenKindV0::Number => &mut self.number,
1129            ClosedWorldTokenKindV0::Percentage => &mut self.percentage,
1130            ClosedWorldTokenKindV0::FunctionName => &mut self.function_name,
1131            ClosedWorldTokenKindV0::String => &mut self.string,
1132            ClosedWorldTokenKindV0::Url => &mut self.url,
1133        }
1134    }
1135
1136    fn allow(&mut self, kind: ClosedWorldTokenKindV0, value: &str) {
1137        self.domain_mut(kind)
1138            .allowed
1139            .insert(value.to_ascii_lowercase());
1140    }
1141
1142    fn mark_open(&mut self, kind: ClosedWorldTokenKindV0) {
1143        self.domain_mut(kind).open = true;
1144    }
1145
1146    fn mark_all_open(&mut self) {
1147        for kind in CLOSED_WORLD_TOKEN_KINDS {
1148            self.mark_open(kind);
1149        }
1150    }
1151
1152    fn merge(&mut self, other: Self) {
1153        for kind in CLOSED_WORLD_TOKEN_KINDS {
1154            let other_domain = other.domain(kind);
1155            let domain = self.domain_mut(kind);
1156            domain.open |= other_domain.open;
1157            domain.allowed.extend(other_domain.allowed.iter().cloned());
1158        }
1159    }
1160}
1161
1162const CLOSED_WORLD_TOKEN_KINDS: [ClosedWorldTokenKindV0; 8] = [
1163    ClosedWorldTokenKindV0::Ident,
1164    ClosedWorldTokenKindV0::Hash,
1165    ClosedWorldTokenKindV0::Dimension,
1166    ClosedWorldTokenKindV0::Number,
1167    ClosedWorldTokenKindV0::Percentage,
1168    ClosedWorldTokenKindV0::FunctionName,
1169    ClosedWorldTokenKindV0::String,
1170    ClosedWorldTokenKindV0::Url,
1171];
1172
1173fn certified_keyword_properties() -> &'static BTreeSet<CanonicalStandardPropertyNameV0> {
1174    static EMPTY: OnceLock<BTreeSet<CanonicalStandardPropertyNameV0>> = OnceLock::new();
1175    closed_world_keyword_authority()
1176        .map(|authority| &authority.certified_properties)
1177        .unwrap_or_else(|| EMPTY.get_or_init(BTreeSet::new))
1178}
1179
1180fn closed_world_keyword_authority() -> Option<&'static ClosedWorldKeywordAuthorityV0> {
1181    static AUTHORITY: OnceLock<Option<ClosedWorldKeywordAuthorityV0>> = OnceLock::new();
1182    AUTHORITY
1183        .get_or_init(|| {
1184            parse_closed_world_keyword_closure_certificate(
1185                CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE,
1186            )
1187        })
1188        .as_ref()
1189}
1190
1191fn parse_closed_world_keyword_closure_certificate(
1192    source: &str,
1193) -> Option<ClosedWorldKeywordAuthorityV0> {
1194    let certificate =
1195        serde_json::from_str::<ClosedWorldKeywordClosureCertificateV0>(source).ok()?;
1196    if certificate.schema_version != "0"
1197        || certificate.product != "omena-abstract-value.closed-world-keyword-closure-certificate"
1198        || certificate.oracle.name != ClosedWorldKeywordClosureOracleNameV0::CssTree
1199        || certificate.oracle.version != "3.2.1"
1200        || certificate.source != "cssTree.lexer.properties.typeAndPropertyReferenceClosure"
1201        || certificate.maximum_reference_depth != 12
1202        || certificate.property_count != 704
1203        || certificate.candidate_pair_count != 23_178
1204        || certificate.accepted_pair_count != 16_445
1205        || certificate.property_count != certificate.property_tests.len()
1206    {
1207        return None;
1208    }
1209
1210    let mut previous_property: Option<&CanonicalStandardPropertyNameV0> = None;
1211    let mut candidate_pair_count = 0usize;
1212    let mut accepted_pair_count = 0usize;
1213    let mut matched_pair_count = 0usize;
1214    let mut matcher_gap_count = 0usize;
1215    let mut derived_certified_properties = BTreeSet::new();
1216    let mut accepted_keywords_by_property = BTreeMap::new();
1217    let mut digest = Sha256::new();
1218
1219    for property_test in &certificate.property_tests {
1220        if property_test.property.as_str().is_empty()
1221            || previous_property.is_some_and(|previous| previous >= &property_test.property)
1222            || property_test.tested_pair_count != property_test.accepted_keywords.len()
1223            || property_test
1224                .matched_pair_count
1225                .checked_add(property_test.matcher_gap_count)
1226                != Some(property_test.tested_pair_count)
1227            || property_test.candidate_pair_count < property_test.tested_pair_count
1228            || property_test
1229                .accepted_keywords
1230                .windows(2)
1231                .any(|pair| pair[0] >= pair[1])
1232            || property_test
1233                .accepted_keywords
1234                .iter()
1235                .any(|keyword| keyword.is_empty())
1236        {
1237            return None;
1238        }
1239        previous_property = Some(&property_test.property);
1240        candidate_pair_count =
1241            candidate_pair_count.checked_add(property_test.candidate_pair_count)?;
1242        accepted_pair_count = accepted_pair_count.checked_add(property_test.tested_pair_count)?;
1243        matched_pair_count = matched_pair_count.checked_add(property_test.matched_pair_count)?;
1244        matcher_gap_count = matcher_gap_count.checked_add(property_test.matcher_gap_count)?;
1245
1246        for keyword in &property_test.accepted_keywords {
1247            digest.update(property_test.property.as_str().as_bytes());
1248            digest.update([0]);
1249            digest.update(keyword.as_bytes());
1250            digest.update([b'\n']);
1251        }
1252        if accepted_keywords_by_property
1253            .insert(
1254                property_test.property.clone(),
1255                property_test.accepted_keywords.iter().cloned().collect(),
1256            )
1257            .is_some()
1258        {
1259            return None;
1260        }
1261        if property_test.tested_pair_count > 0 && property_test.matcher_gap_count == 0 {
1262            derived_certified_properties.insert(property_test.property.clone());
1263        }
1264    }
1265
1266    let accepted_pair_digest = digest
1267        .finalize()
1268        .iter()
1269        .map(|byte| format!("{byte:02x}"))
1270        .collect::<String>();
1271    if certificate.candidate_pair_count != candidate_pair_count
1272        || certificate.accepted_pair_count != accepted_pair_count
1273        || certificate.matched_pair_count != matched_pair_count
1274        || certificate.matcher_gap_count != matcher_gap_count
1275        || certificate.accepted_pair_digest != accepted_pair_digest
1276        || certificate.certified_properties != derived_certified_properties
1277    {
1278        return None;
1279    }
1280
1281    Some(ClosedWorldKeywordAuthorityV0 {
1282        certified_properties: certificate.certified_properties,
1283        accepted_keywords_by_property,
1284    })
1285}
1286
1287fn standard_property_value_token_kinds_have_closure_authority(
1288    property: &PropertyNameV0,
1289    value: &str,
1290    registry: &SpecGrammarRegistryV0,
1291) -> bool {
1292    let Ok(components) = css_value_component_stream(value, 0) else {
1293        return false;
1294    };
1295    if components
1296        .iter()
1297        .any(|component| matches!(component.kind, CssValueComponentKindV0::Ident))
1298    {
1299        let Some(property_key) = property.as_standard_key() else {
1300            return false;
1301        };
1302        return certified_keyword_properties().contains(property_key);
1303    }
1304    standard_property_grammar_is_machine_imported_without_overrides(property, registry)
1305}
1306
1307fn standard_property_grammar_is_machine_imported_without_overrides(
1308    property: &PropertyNameV0,
1309    registry: &SpecGrammarRegistryV0,
1310) -> bool {
1311    let Some(entry) = registry.entry("properties", property.canonical_name()) else {
1312        return false;
1313    };
1314    let Some(source) = entry.syntax.as_deref() else {
1315        return false;
1316    };
1317    if entry.override_provenance.is_some() {
1318        return false;
1319    }
1320    let Ok(expression) = cached_pinned_vds_expression(strip_matching_quotes(source.trim())) else {
1321        return false;
1322    };
1323    let mut visiting = HashSet::new();
1324    expression_sources_are_machine_imported_without_overrides(
1325        expression.as_ref(),
1326        registry,
1327        &mut visiting,
1328    )
1329}
1330
1331fn expression_sources_are_machine_imported_without_overrides(
1332    expression: &VdsExpression,
1333    registry: &SpecGrammarRegistryV0,
1334    visiting: &mut HashSet<(ReferenceCategory, String)>,
1335) -> bool {
1336    match expression {
1337        VdsExpression::Literal(_) => true,
1338        VdsExpression::Reference(reference) => {
1339            if is_builtin_reference_name(reference.name.as_str()) {
1340                return closed_world_builtin_token_profiles().is_some_and(|manifest| {
1341                    manifest
1342                        .profiles
1343                        .iter()
1344                        .any(|profile| profile.name == reference.name)
1345                });
1346            }
1347            let key = (reference.category, reference.name.clone());
1348            if !visiting.insert(key.clone()) {
1349                return true;
1350            }
1351            let category = match reference.category {
1352                ReferenceCategory::Type => "types",
1353                ReferenceCategory::Property => "properties",
1354                ReferenceCategory::Function => "functions",
1355            };
1356            let imported = registry
1357                .entry(category, reference.name.as_str())
1358                .filter(|entry| entry.override_provenance.is_none())
1359                .and_then(|entry| entry.syntax.as_deref())
1360                .and_then(|source| cached_pinned_vds_expression(source).ok())
1361                .is_some_and(|expression| {
1362                    expression_sources_are_machine_imported_without_overrides(
1363                        expression.as_ref(),
1364                        registry,
1365                        visiting,
1366                    )
1367                });
1368            visiting.remove(&key);
1369            imported
1370        }
1371        VdsExpression::Function { arguments, .. }
1372        | VdsExpression::Repeat {
1373            expression: arguments,
1374            ..
1375        }
1376        | VdsExpression::Required(arguments) => {
1377            expression_sources_are_machine_imported_without_overrides(arguments, registry, visiting)
1378        }
1379        VdsExpression::Sequence(expressions)
1380        | VdsExpression::AllInAnyOrder(expressions)
1381        | VdsExpression::OneOrMoreInAnyOrder(expressions)
1382        | VdsExpression::Choice(expressions) => expressions.iter().all(|expression| {
1383            expression_sources_are_machine_imported_without_overrides(
1384                expression, registry, visiting,
1385            )
1386        }),
1387    }
1388}
1389
1390fn standard_property_closed_world_token_kind_mismatch(
1391    property: &PropertyNameV0,
1392    value: &str,
1393    registry: &SpecGrammarRegistryV0,
1394) -> bool {
1395    let Ok(components) = css_value_component_stream(value, 0) else {
1396        return false;
1397    };
1398    let Some(profile) = cached_standard_property_closed_world_token_profile(property, registry)
1399    else {
1400        return false;
1401    };
1402    if components.iter().any(|component| {
1403        closed_world_component_identity(component).is_some_and(|(kind, value)| {
1404            let domain = profile.domain(kind);
1405            kind == ClosedWorldTokenKindV0::FunctionName
1406                && domain.open
1407                && !domain.allowed.contains(value.as_str())
1408                && !recognized_standard_function_names().contains(value.as_str())
1409        })
1410    }) {
1411        return false;
1412    }
1413    components.iter().any(|component| {
1414        let Some((kind, value)) = closed_world_component_identity(component) else {
1415            return false;
1416        };
1417        if kind == ClosedWorldTokenKindV0::Ident {
1418            let Some(property_key) = property.as_standard_key() else {
1419                return false;
1420            };
1421            let Some(accepted_keywords) = closed_world_keyword_authority()
1422                .and_then(|authority| authority.accepted_keywords_by_property.get(property_key))
1423            else {
1424                return false;
1425            };
1426            let domain = profile.domain(kind);
1427            return !domain.open && !accepted_keywords.contains(value.as_str());
1428        }
1429        let domain = profile.domain(kind);
1430        !domain.open && !domain.allowed.contains(value.as_str())
1431    })
1432}
1433
1434fn cached_standard_property_closed_world_token_profile(
1435    property: &PropertyNameV0,
1436    registry: &SpecGrammarRegistryV0,
1437) -> Option<ClosedWorldTokenProfileV0> {
1438    static CACHE: OnceLock<RwLock<HashMap<String, Option<ClosedWorldTokenProfileV0>>>> =
1439        OnceLock::new();
1440    let cache = CACHE.get_or_init(|| RwLock::new(HashMap::new()));
1441    let key = property.canonical_name().to_string();
1442    if let Some(profile) = cache
1443        .read()
1444        .unwrap_or_else(std::sync::PoisonError::into_inner)
1445        .get(&key)
1446    {
1447        return profile.clone();
1448    }
1449    let profile = registry
1450        .entry("properties", property.canonical_name())
1451        .and_then(|entry| entry.syntax.as_deref().map(|grammar| (entry, grammar)))
1452        .and_then(|(entry, grammar)| {
1453            cached_pinned_vds_expression(strip_matching_quotes(grammar.trim()))
1454                .ok()
1455                .map(|expression| (entry, expression))
1456        })
1457        .map(|(entry, expression)| {
1458            let mut ident_visiting = HashSet::new();
1459            let mut ident_memo = HashMap::new();
1460            let ident_open = expression_has_open_ident_production(
1461                expression.as_ref(),
1462                registry,
1463                &mut ident_visiting,
1464                &mut ident_memo,
1465            );
1466            let mut visiting = HashSet::new();
1467            let mut memo = HashMap::new();
1468            let mut profile =
1469                closed_world_token_profile(expression.as_ref(), registry, &mut visiting, &mut memo);
1470            if entry.override_provenance.is_some() {
1471                profile.mark_all_open();
1472            }
1473            // A reviewed syntax replacement is conservative for the generic
1474            // token-kind profile, but it does not itself introduce an open
1475            // identifier production. Identifier rejection uses the expanded
1476            // grammar plus the independently authenticated accepted-keyword
1477            // table, so preserve that narrower fact after the broad fallback.
1478            profile.ident.open = ident_open;
1479            profile
1480        });
1481    cache
1482        .write()
1483        .unwrap_or_else(std::sync::PoisonError::into_inner)
1484        .entry(key)
1485        .or_insert_with(|| profile.clone());
1486    profile
1487}
1488
1489fn expression_has_open_ident_production(
1490    expression: &VdsExpression,
1491    registry: &SpecGrammarRegistryV0,
1492    visiting: &mut HashSet<(ReferenceCategory, String)>,
1493    memo: &mut HashMap<(ReferenceCategory, String), bool>,
1494) -> bool {
1495    match expression {
1496        VdsExpression::Literal(_) | VdsExpression::Function { .. } => false,
1497        VdsExpression::Reference(reference) => {
1498            reference_has_open_ident_production(reference, registry, visiting, memo)
1499        }
1500        VdsExpression::Sequence(expressions)
1501        | VdsExpression::AllInAnyOrder(expressions)
1502        | VdsExpression::OneOrMoreInAnyOrder(expressions)
1503        | VdsExpression::Choice(expressions) => expressions.iter().any(|expression| {
1504            expression_has_open_ident_production(expression, registry, visiting, memo)
1505        }),
1506        VdsExpression::Repeat { expression, .. } | VdsExpression::Required(expression) => {
1507            expression_has_open_ident_production(expression, registry, visiting, memo)
1508        }
1509    }
1510}
1511
1512fn reference_has_open_ident_production(
1513    reference: &VdsReference,
1514    registry: &SpecGrammarRegistryV0,
1515    visiting: &mut HashSet<(ReferenceCategory, String)>,
1516    memo: &mut HashMap<(ReferenceCategory, String), bool>,
1517) -> bool {
1518    if reference.category == ReferenceCategory::Function {
1519        return false;
1520    }
1521    if reference.category == ReferenceCategory::Type
1522        && let Some(profile) = closed_world_builtin_profile(reference.name.as_str())
1523    {
1524        return profile.ident.open;
1525    }
1526
1527    let key = (reference.category, reference.name.clone());
1528    if let Some(open) = memo.get(&key) {
1529        return *open;
1530    }
1531    if !visiting.insert(key.clone()) {
1532        return true;
1533    }
1534    let category = match reference.category {
1535        ReferenceCategory::Type => "types",
1536        ReferenceCategory::Property => "properties",
1537        ReferenceCategory::Function => unreachable!("function references return above"),
1538    };
1539    let open = registry
1540        .entry(category, reference.name.as_str())
1541        .and_then(|entry| entry.syntax.as_deref())
1542        .and_then(|source| cached_pinned_vds_expression(source).ok())
1543        .map(|expression| {
1544            expression_has_open_ident_production(expression.as_ref(), registry, visiting, memo)
1545        })
1546        .unwrap_or(true);
1547    visiting.remove(&key);
1548    memo.insert(key, open);
1549    open
1550}
1551
1552fn closed_world_token_profile(
1553    expression: &VdsExpression,
1554    registry: &SpecGrammarRegistryV0,
1555    visiting: &mut HashSet<(ReferenceCategory, String)>,
1556    memo: &mut HashMap<(ReferenceCategory, String), ClosedWorldTokenProfileV0>,
1557) -> ClosedWorldTokenProfileV0 {
1558    match expression {
1559        VdsExpression::Literal(literal) => closed_world_literal_profile(literal),
1560        VdsExpression::Reference(reference) => {
1561            closed_world_reference_profile(reference, registry, visiting, memo)
1562        }
1563        VdsExpression::Function { name, .. } => {
1564            let mut profile = ClosedWorldTokenProfileV0::default();
1565            profile.allow(ClosedWorldTokenKindV0::FunctionName, name);
1566            profile
1567        }
1568        VdsExpression::Sequence(expressions)
1569        | VdsExpression::AllInAnyOrder(expressions)
1570        | VdsExpression::OneOrMoreInAnyOrder(expressions)
1571        | VdsExpression::Choice(expressions) => {
1572            let mut profile = ClosedWorldTokenProfileV0::default();
1573            for expression in expressions {
1574                profile.merge(closed_world_token_profile(
1575                    expression, registry, visiting, memo,
1576                ));
1577            }
1578            profile
1579        }
1580        VdsExpression::Repeat { expression, .. } | VdsExpression::Required(expression) => {
1581            closed_world_token_profile(expression, registry, visiting, memo)
1582        }
1583    }
1584}
1585
1586fn closed_world_reference_profile(
1587    reference: &VdsReference,
1588    registry: &SpecGrammarRegistryV0,
1589    visiting: &mut HashSet<(ReferenceCategory, String)>,
1590    memo: &mut HashMap<(ReferenceCategory, String), ClosedWorldTokenProfileV0>,
1591) -> ClosedWorldTokenProfileV0 {
1592    if reference.category == ReferenceCategory::Function {
1593        let mut profile = ClosedWorldTokenProfileV0::default();
1594        profile.allow(
1595            ClosedWorldTokenKindV0::FunctionName,
1596            reference.name.trim_end_matches("()"),
1597        );
1598        return profile;
1599    }
1600    if reference.category == ReferenceCategory::Type
1601        && let Some(profile) = closed_world_builtin_profile(reference.name.as_str())
1602    {
1603        return profile;
1604    }
1605
1606    let key = (reference.category, reference.name.clone());
1607    if let Some(profile) = memo.get(&key) {
1608        return profile.clone();
1609    }
1610    if !visiting.insert(key.clone()) {
1611        let mut profile = ClosedWorldTokenProfileV0::default();
1612        profile.mark_all_open();
1613        return profile;
1614    }
1615    let category = match reference.category {
1616        ReferenceCategory::Type => "types",
1617        ReferenceCategory::Property => "properties",
1618        ReferenceCategory::Function => unreachable!("function references return above"),
1619    };
1620    let mut profile = registry
1621        .entry(category, reference.name.as_str())
1622        .and_then(|entry| entry.syntax.as_deref().map(|source| (entry, source)))
1623        .and_then(|(entry, source)| {
1624            cached_pinned_vds_expression(source)
1625                .ok()
1626                .map(|expression| (entry, expression))
1627        })
1628        .map(|(entry, expression)| {
1629            let mut profile =
1630                closed_world_token_profile(expression.as_ref(), registry, visiting, memo);
1631            if entry.override_provenance.is_some() {
1632                profile.mark_all_open();
1633            }
1634            profile
1635        })
1636        .unwrap_or_else(|| {
1637            let mut unknown = ClosedWorldTokenProfileV0::default();
1638            unknown.mark_all_open();
1639            unknown
1640        });
1641    visiting.remove(&key);
1642    if reference.category == ReferenceCategory::Type
1643        && matches!(
1644            reference.name.as_str(),
1645            "number" | "integer" | "length" | "percentage" | "length-percentage" | "angle" | "time"
1646        )
1647    {
1648        profile.mark_open(ClosedWorldTokenKindV0::FunctionName);
1649    }
1650    memo.insert(key, profile.clone());
1651    profile
1652}
1653
1654fn closed_world_builtin_profile(name: &str) -> Option<ClosedWorldTokenProfileV0> {
1655    let known_builtin = is_builtin_reference_name(name)
1656        || matches!(name, "declaration-value" | "any-value" | "whole-value");
1657    if !known_builtin {
1658        return None;
1659    }
1660
1661    let Some(manifest) = closed_world_builtin_token_profiles() else {
1662        return Some(all_open_closed_world_token_profile());
1663    };
1664    let Some(witnessed) = manifest
1665        .profiles
1666        .iter()
1667        .find(|profile| profile.name == name)
1668    else {
1669        return Some(all_open_closed_world_token_profile());
1670    };
1671    if witnessed.authority == "registryDerived" {
1672        return None;
1673    }
1674    if !matches!(
1675        witnessed.authority.as_str(),
1676        "cssTreeWitness" | "defaultOpen"
1677    ) {
1678        return Some(all_open_closed_world_token_profile());
1679    }
1680
1681    let mut profile = ClosedWorldTokenProfileV0::default();
1682    for kind in &witnessed.open_token_kinds {
1683        let Some(kind) = closed_world_token_kind_from_data_name(kind) else {
1684            return Some(all_open_closed_world_token_profile());
1685        };
1686        profile.mark_open(kind);
1687    }
1688    for (kind, values) in &witnessed.allowed_values {
1689        let Some(kind) = closed_world_token_kind_from_data_name(kind) else {
1690            return Some(all_open_closed_world_token_profile());
1691        };
1692        for value in values {
1693            profile.allow(kind, value);
1694        }
1695    }
1696    if witnessed.authority == "defaultOpen"
1697        && CLOSED_WORLD_TOKEN_KINDS
1698            .iter()
1699            .any(|kind| !profile.domain(*kind).open)
1700    {
1701        return Some(all_open_closed_world_token_profile());
1702    }
1703    Some(profile)
1704}
1705
1706fn closed_world_builtin_token_profiles() -> Option<&'static ClosedWorldBuiltinTokenProfilesV0> {
1707    static PROFILES: OnceLock<Option<ClosedWorldBuiltinTokenProfilesV0>> = OnceLock::new();
1708    PROFILES
1709        .get_or_init(|| {
1710            let profiles = serde_json::from_str::<ClosedWorldBuiltinTokenProfilesV0>(
1711                CLOSED_WORLD_BUILTIN_TOKEN_PROFILES_SOURCE,
1712            )
1713            .ok()?;
1714            (profiles.schema_version == "0"
1715                && profiles.product == "omena-abstract-value.closed-world-builtin-token-profiles"
1716                && profiles.oracle.name == ClosedWorldKeywordClosureOracleNameV0::CssTree
1717                && profiles.oracle.version == "3.2.1"
1718                && profiles.profile_count == profiles.profiles.len())
1719            .then_some(profiles)
1720        })
1721        .as_ref()
1722}
1723
1724fn closed_world_token_kind_from_data_name(name: &str) -> Option<ClosedWorldTokenKindV0> {
1725    match name {
1726        "ident" => Some(ClosedWorldTokenKindV0::Ident),
1727        "hash" => Some(ClosedWorldTokenKindV0::Hash),
1728        "dimension" => Some(ClosedWorldTokenKindV0::Dimension),
1729        "number" => Some(ClosedWorldTokenKindV0::Number),
1730        "percentage" => Some(ClosedWorldTokenKindV0::Percentage),
1731        "functionName" => Some(ClosedWorldTokenKindV0::FunctionName),
1732        "string" => Some(ClosedWorldTokenKindV0::String),
1733        "url" => Some(ClosedWorldTokenKindV0::Url),
1734        _ => None,
1735    }
1736}
1737
1738fn all_open_closed_world_token_profile() -> ClosedWorldTokenProfileV0 {
1739    let mut profile = ClosedWorldTokenProfileV0::default();
1740    profile.mark_all_open();
1741    profile
1742}
1743
1744fn closed_world_literal_profile(literal: &str) -> ClosedWorldTokenProfileV0 {
1745    let mut profile = ClosedWorldTokenProfileV0::default();
1746    let Ok(components) = css_value_component_stream(literal, 0) else {
1747        return profile;
1748    };
1749    if let [component] = components.as_slice()
1750        && let Some((kind, value)) = closed_world_component_identity(component)
1751    {
1752        profile.allow(kind, value.as_str());
1753    }
1754    profile
1755}
1756
1757fn closed_world_component_identity(
1758    component: &CssValueComponentV0,
1759) -> Option<(ClosedWorldTokenKindV0, String)> {
1760    let kind = match &component.kind {
1761        CssValueComponentKindV0::Ident => ClosedWorldTokenKindV0::Ident,
1762        CssValueComponentKindV0::Hash => ClosedWorldTokenKindV0::Hash,
1763        CssValueComponentKindV0::Dimension => ClosedWorldTokenKindV0::Dimension,
1764        CssValueComponentKindV0::Number => ClosedWorldTokenKindV0::Number,
1765        CssValueComponentKindV0::Percentage => ClosedWorldTokenKindV0::Percentage,
1766        CssValueComponentKindV0::Function { name, .. } => {
1767            return Some((
1768                ClosedWorldTokenKindV0::FunctionName,
1769                name.to_ascii_lowercase(),
1770            ));
1771        }
1772        CssValueComponentKindV0::String => ClosedWorldTokenKindV0::String,
1773        CssValueComponentKindV0::Url => ClosedWorldTokenKindV0::Url,
1774        CssValueComponentKindV0::Parenthesized { .. }
1775        | CssValueComponentKindV0::Bracketed { .. }
1776        | CssValueComponentKindV0::Braced { .. }
1777        | CssValueComponentKindV0::Comma
1778        | CssValueComponentKindV0::Slash
1779        | CssValueComponentKindV0::Delimiter => return None,
1780    };
1781    Some((kind, component.text.to_ascii_lowercase()))
1782}
1783
1784#[derive(Debug, Clone, PartialEq, Eq)]
1785struct VdsReference {
1786    category: ReferenceCategory,
1787    name: String,
1788    range: Option<NumericRange>,
1789}
1790
1791#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1792enum ReferenceCategory {
1793    Type,
1794    Property,
1795    Function,
1796}
1797
1798#[derive(Debug, Clone, PartialEq, Eq)]
1799struct NumericRange {
1800    min: Option<String>,
1801    max: Option<String>,
1802}
1803
1804#[derive(Debug, Clone, PartialEq, Eq)]
1805struct VdsParseError {
1806    offset: usize,
1807    code: &'static str,
1808    detail: String,
1809}
1810
1811type CachedVdsExpression = Result<Arc<VdsExpression>, VdsParseError>;
1812
1813fn cached_pinned_vds_expression(source: &str) -> CachedVdsExpression {
1814    static CACHE: OnceLock<RwLock<HashMap<String, CachedVdsExpression>>> = OnceLock::new();
1815    let cache = CACHE.get_or_init(|| RwLock::new(HashMap::new()));
1816    if let Some(parsed) = cache
1817        .read()
1818        .unwrap_or_else(std::sync::PoisonError::into_inner)
1819        .get(source)
1820    {
1821        return parsed.clone();
1822    }
1823
1824    let parsed = VdsParser::new(source).parse().map(Arc::new);
1825    cache
1826        .write()
1827        .unwrap_or_else(std::sync::PoisonError::into_inner)
1828        .entry(source.to_string())
1829        .or_insert(parsed)
1830        .clone()
1831}
1832
1833#[derive(Debug, Clone, PartialEq, Eq)]
1834struct VdsToken {
1835    kind: VdsTokenKind,
1836    offset: usize,
1837}
1838
1839#[derive(Debug, Clone, PartialEq, Eq)]
1840enum VdsTokenKind {
1841    Word(String),
1842    Reference(String),
1843    Literal(String),
1844    OpenBracket,
1845    CloseBracket,
1846    OpenParen,
1847    CloseParen,
1848    Or,
1849    OrOr,
1850    AndAnd,
1851    Question,
1852    Star,
1853    Plus,
1854    Hash,
1855    Range(usize, Option<usize>),
1856    Bang,
1857    End,
1858}
1859
1860struct VdsParser<'a> {
1861    source: &'a str,
1862    tokens: Vec<VdsToken>,
1863    cursor: usize,
1864}
1865
1866impl<'a> VdsParser<'a> {
1867    fn new(source: &'a str) -> Self {
1868        Self {
1869            source,
1870            tokens: Vec::new(),
1871            cursor: 0,
1872        }
1873    }
1874
1875    fn parse(mut self) -> Result<VdsExpression, VdsParseError> {
1876        self.tokens = lex_vds(self.source)?;
1877        let expression = self.parse_choice()?;
1878        if !matches!(self.peek(), VdsTokenKind::End) {
1879            return Err(self.error(
1880                "unexpectedGrammarToken",
1881                "unexpected trailing grammar token",
1882            ));
1883        }
1884        Ok(expression)
1885    }
1886
1887    fn parse_choice(&mut self) -> Result<VdsExpression, VdsParseError> {
1888        let mut values = vec![self.parse_one_or_more_in_any_order()?];
1889        while matches!(self.peek(), VdsTokenKind::Or) {
1890            self.cursor += 1;
1891            values.push(self.parse_one_or_more_in_any_order()?);
1892        }
1893        Ok(flatten_expression(values, VdsExpression::Choice))
1894    }
1895
1896    fn parse_one_or_more_in_any_order(&mut self) -> Result<VdsExpression, VdsParseError> {
1897        let mut values = vec![self.parse_all_in_any_order()?];
1898        while matches!(self.peek(), VdsTokenKind::OrOr) {
1899            self.cursor += 1;
1900            values.push(self.parse_all_in_any_order()?);
1901        }
1902        Ok(flatten_expression(
1903            values,
1904            VdsExpression::OneOrMoreInAnyOrder,
1905        ))
1906    }
1907
1908    fn parse_all_in_any_order(&mut self) -> Result<VdsExpression, VdsParseError> {
1909        let mut values = vec![self.parse_sequence()?];
1910        while matches!(self.peek(), VdsTokenKind::AndAnd) {
1911            self.cursor += 1;
1912            values.push(self.parse_sequence()?);
1913        }
1914        Ok(flatten_expression(values, VdsExpression::AllInAnyOrder))
1915    }
1916
1917    fn parse_sequence(&mut self) -> Result<VdsExpression, VdsParseError> {
1918        let mut values = Vec::new();
1919        while self.starts_primary() {
1920            values.push(self.parse_postfix()?);
1921        }
1922        if values.is_empty() {
1923            return Err(self.error("missingGrammarTerm", "expected a grammar term"));
1924        }
1925        Ok(flatten_expression(values, VdsExpression::Sequence))
1926    }
1927
1928    fn parse_postfix(&mut self) -> Result<VdsExpression, VdsParseError> {
1929        let mut expression = self.parse_primary()?;
1930        loop {
1931            expression = match self.peek() {
1932                VdsTokenKind::Question => {
1933                    self.cursor += 1;
1934                    repeat(expression, 0, Some(1), false)
1935                }
1936                VdsTokenKind::Star => {
1937                    self.cursor += 1;
1938                    repeat(expression, 0, None, false)
1939                }
1940                VdsTokenKind::Plus => {
1941                    self.cursor += 1;
1942                    repeat(expression, 1, None, false)
1943                }
1944                VdsTokenKind::Hash => {
1945                    self.cursor += 1;
1946                    let (min, max) = match self.peek().clone() {
1947                        VdsTokenKind::Range(min, max) => {
1948                            self.cursor += 1;
1949                            (min, max)
1950                        }
1951                        _ => (1, None),
1952                    };
1953                    repeat(expression, min, max, true)
1954                }
1955                VdsTokenKind::Range(min, max) => {
1956                    let min = *min;
1957                    let max = *max;
1958                    self.cursor += 1;
1959                    repeat(expression, min, max, false)
1960                }
1961                VdsTokenKind::Bang => {
1962                    self.cursor += 1;
1963                    VdsExpression::Required(Box::new(expression))
1964                }
1965                _ => break,
1966            };
1967        }
1968        Ok(expression)
1969    }
1970
1971    fn parse_primary(&mut self) -> Result<VdsExpression, VdsParseError> {
1972        let token = self.tokens[self.cursor].clone();
1973        self.cursor += 1;
1974        match token.kind {
1975            VdsTokenKind::Reference(source) => Ok(VdsExpression::Reference(parse_reference(
1976                source.as_str(),
1977                token.offset,
1978            )?)),
1979            VdsTokenKind::Word(word) => {
1980                if matches!(self.peek(), VdsTokenKind::OpenParen) {
1981                    self.cursor += 1;
1982                    if matches!(self.peek(), VdsTokenKind::CloseParen) {
1983                        self.cursor += 1;
1984                        return Ok(VdsExpression::Function {
1985                            name: word,
1986                            arguments: Box::new(VdsExpression::Sequence(Vec::new())),
1987                        });
1988                    }
1989                    let arguments = self.parse_choice()?;
1990                    self.expect_close_paren()?;
1991                    Ok(VdsExpression::Function {
1992                        name: word,
1993                        arguments: Box::new(arguments),
1994                    })
1995                } else {
1996                    Ok(VdsExpression::Literal(word))
1997                }
1998            }
1999            VdsTokenKind::Literal(literal) => Ok(VdsExpression::Literal(literal)),
2000            VdsTokenKind::OpenBracket => {
2001                let expression = self.parse_choice()?;
2002                if !matches!(self.peek(), VdsTokenKind::CloseBracket) {
2003                    return Err(self.error("unclosedGrammarGroup", "missing closing ]"));
2004                }
2005                self.cursor += 1;
2006                Ok(expression)
2007            }
2008            VdsTokenKind::OpenParen => {
2009                let expression = self.parse_choice()?;
2010                self.expect_close_paren()?;
2011                Ok(expression)
2012            }
2013            _ => Err(VdsParseError {
2014                offset: token.offset,
2015                code: "unexpectedGrammarPrimary",
2016                detail: "expected a literal, reference, function, or group".to_string(),
2017            }),
2018        }
2019    }
2020
2021    fn expect_close_paren(&mut self) -> Result<(), VdsParseError> {
2022        if !matches!(self.peek(), VdsTokenKind::CloseParen) {
2023            return Err(self.error("unclosedGrammarFunction", "missing closing )"));
2024        }
2025        self.cursor += 1;
2026        Ok(())
2027    }
2028
2029    fn starts_primary(&self) -> bool {
2030        matches!(
2031            self.peek(),
2032            VdsTokenKind::Reference(_)
2033                | VdsTokenKind::Word(_)
2034                | VdsTokenKind::Literal(_)
2035                | VdsTokenKind::OpenBracket
2036                | VdsTokenKind::OpenParen
2037        )
2038    }
2039
2040    fn peek(&self) -> &VdsTokenKind {
2041        &self.tokens[self.cursor].kind
2042    }
2043
2044    fn error(&self, code: &'static str, detail: &str) -> VdsParseError {
2045        VdsParseError {
2046            offset: self.tokens[self.cursor].offset,
2047            code,
2048            detail: detail.to_string(),
2049        }
2050    }
2051}
2052
2053fn flatten_expression(
2054    mut values: Vec<VdsExpression>,
2055    wrap: impl FnOnce(Vec<VdsExpression>) -> VdsExpression,
2056) -> VdsExpression {
2057    if values.len() == 1 {
2058        values.pop().unwrap_or(VdsExpression::Sequence(Vec::new()))
2059    } else {
2060        wrap(values)
2061    }
2062}
2063
2064fn repeat(
2065    expression: VdsExpression,
2066    min: usize,
2067    max: Option<usize>,
2068    comma_separated: bool,
2069) -> VdsExpression {
2070    VdsExpression::Repeat {
2071        expression: Box::new(expression),
2072        min,
2073        max,
2074        comma_separated,
2075    }
2076}
2077
2078fn lex_vds(source: &str) -> Result<Vec<VdsToken>, VdsParseError> {
2079    let mut tokens = Vec::new();
2080    let mut cursor = 0usize;
2081    while cursor < source.len() {
2082        let Some(character) = source[cursor..].chars().next() else {
2083            break;
2084        };
2085        if character.is_whitespace() {
2086            cursor += character.len_utf8();
2087            continue;
2088        }
2089        let offset = cursor;
2090        let rest = &source[cursor..];
2091        if rest.starts_with("||") {
2092            tokens.push(token(VdsTokenKind::OrOr, offset));
2093            cursor += 2;
2094            continue;
2095        }
2096        if rest.starts_with("&&") {
2097            tokens.push(token(VdsTokenKind::AndAnd, offset));
2098            cursor += 2;
2099            continue;
2100        }
2101        if character == '<' {
2102            let Some(relative_end) = rest.find('>') else {
2103                return Err(VdsParseError {
2104                    offset,
2105                    code: "unclosedGrammarReference",
2106                    detail: "missing closing >".to_string(),
2107                });
2108            };
2109            let end = cursor + relative_end;
2110            tokens.push(token(
2111                VdsTokenKind::Reference(source[cursor + 1..end].trim().to_string()),
2112                offset,
2113            ));
2114            cursor = end + 1;
2115            continue;
2116        }
2117        if character == '{' {
2118            let Some(relative_end) =
2119                rest.char_indices()
2120                    .find_map(|(relative_offset, candidate)| {
2121                        (candidate == '}').then_some(relative_offset)
2122                    })
2123            else {
2124                return Err(VdsParseError {
2125                    offset,
2126                    code: "unclosedGrammarRange",
2127                    detail: "missing closing }".to_string(),
2128                });
2129            };
2130            let end = cursor + relative_end;
2131            let range = parse_repeat_range(&source[cursor + 1..end], offset)?;
2132            tokens.push(token(VdsTokenKind::Range(range.0, range.1), offset));
2133            cursor = end + 1;
2134            continue;
2135        }
2136        let simple = match character {
2137            '[' => Some(VdsTokenKind::OpenBracket),
2138            ']' => Some(VdsTokenKind::CloseBracket),
2139            '(' => Some(VdsTokenKind::OpenParen),
2140            ')' => Some(VdsTokenKind::CloseParen),
2141            '|' => Some(VdsTokenKind::Or),
2142            '?' => Some(VdsTokenKind::Question),
2143            '*' => Some(VdsTokenKind::Star),
2144            '+' => Some(VdsTokenKind::Plus),
2145            '#' => Some(VdsTokenKind::Hash),
2146            '!' => Some(VdsTokenKind::Bang),
2147            ',' | '/' | ':' | ';' | '=' | '@' | '~' | '^' | '$' | '&' => {
2148                Some(VdsTokenKind::Literal(character.to_string()))
2149            }
2150            _ => None,
2151        };
2152        if let Some(kind) = simple {
2153            tokens.push(token(kind, offset));
2154            cursor += character.len_utf8();
2155            continue;
2156        }
2157        if character == '\'' || character == '"' {
2158            let quote = character;
2159            cursor += character.len_utf8();
2160            let content_start = cursor;
2161            let mut escaped = false;
2162            let mut found_end = None;
2163            while cursor < source.len() {
2164                let Some(current) = source[cursor..].chars().next() else {
2165                    break;
2166                };
2167                if escaped {
2168                    escaped = false;
2169                } else if current == '\\' {
2170                    escaped = true;
2171                } else if current == quote {
2172                    found_end = Some(cursor);
2173                    break;
2174                }
2175                cursor += current.len_utf8();
2176            }
2177            let Some(end) = found_end else {
2178                return Err(VdsParseError {
2179                    offset,
2180                    code: "unclosedGrammarString",
2181                    detail: "missing closing quote".to_string(),
2182                });
2183            };
2184            tokens.push(token(
2185                VdsTokenKind::Literal(source[content_start..end].to_string()),
2186                offset,
2187            ));
2188            cursor = end + quote.len_utf8();
2189            continue;
2190        }
2191        let start = cursor;
2192        while cursor < source.len() {
2193            let Some(current) = source[cursor..].chars().next() else {
2194                break;
2195            };
2196            if current.is_whitespace()
2197                || matches!(
2198                    current,
2199                    '<' | '>'
2200                        | '['
2201                        | ']'
2202                        | '('
2203                        | ')'
2204                        | '{'
2205                        | '}'
2206                        | '|'
2207                        | '&'
2208                        | '?'
2209                        | '*'
2210                        | '+'
2211                        | '#'
2212                        | '!'
2213                        | ','
2214                        | '/'
2215                        | ':'
2216                        | ';'
2217                        | '='
2218                        | '@'
2219                        | '~'
2220                        | '^'
2221                        | '$'
2222                        | '\''
2223                        | '"'
2224                )
2225            {
2226                break;
2227            }
2228            cursor += current.len_utf8();
2229        }
2230        if start == cursor {
2231            return Err(VdsParseError {
2232                offset,
2233                code: "unsupportedGrammarCharacter",
2234                detail: format!("unsupported grammar character {character:?}"),
2235            });
2236        }
2237        tokens.push(token(
2238            VdsTokenKind::Word(source[start..cursor].to_string()),
2239            offset,
2240        ));
2241    }
2242    tokens.push(token(VdsTokenKind::End, source.len()));
2243    Ok(tokens)
2244}
2245
2246fn token(kind: VdsTokenKind, offset: usize) -> VdsToken {
2247    VdsToken { kind, offset }
2248}
2249
2250fn parse_repeat_range(
2251    source: &str,
2252    offset: usize,
2253) -> Result<(usize, Option<usize>), VdsParseError> {
2254    let mut parts = source.split(',').map(str::trim);
2255    let first = parts.next().unwrap_or_default();
2256    let second = parts.next();
2257    if parts.next().is_some() || first.is_empty() {
2258        return Err(VdsParseError {
2259            offset,
2260            code: "invalidGrammarRange",
2261            detail: format!("invalid repeat range {{{source}}}"),
2262        });
2263    }
2264    let min = first.parse::<usize>().map_err(|_| VdsParseError {
2265        offset,
2266        code: "invalidGrammarRange",
2267        detail: format!("invalid repeat range minimum {first:?}"),
2268    })?;
2269    let max = match second {
2270        None => Some(min),
2271        Some("") => None,
2272        Some(value) => Some(value.parse::<usize>().map_err(|_| VdsParseError {
2273            offset,
2274            code: "invalidGrammarRange",
2275            detail: format!("invalid repeat range maximum {value:?}"),
2276        })?),
2277    };
2278    if max.is_some_and(|max| max < min) {
2279        return Err(VdsParseError {
2280            offset,
2281            code: "invalidGrammarRange",
2282            detail: format!("repeat range maximum precedes minimum in {{{source}}}"),
2283        });
2284    }
2285    Ok((min, max))
2286}
2287
2288fn parse_reference(source: &str, offset: usize) -> Result<VdsReference, VdsParseError> {
2289    let source = source.trim();
2290    if source.is_empty() {
2291        return Err(VdsParseError {
2292            offset,
2293            code: "emptyGrammarReference",
2294            detail: "empty grammar reference".to_string(),
2295        });
2296    }
2297    if let Some(property) = source
2298        .strip_prefix('\'')
2299        .and_then(|value| value.strip_suffix('\''))
2300    {
2301        return Ok(VdsReference {
2302            category: ReferenceCategory::Property,
2303            name: PropertyNameV0::from_authored(property)
2304                .canonical_name()
2305                .to_string(),
2306            range: None,
2307        });
2308    }
2309    let (name, range) = split_reference_range(source, offset)?;
2310    let category = if name.ends_with("()") {
2311        ReferenceCategory::Function
2312    } else {
2313        ReferenceCategory::Type
2314    };
2315    Ok(VdsReference {
2316        category,
2317        name: name.to_ascii_lowercase(),
2318        range,
2319    })
2320}
2321
2322fn split_reference_range(
2323    source: &str,
2324    offset: usize,
2325) -> Result<(&str, Option<NumericRange>), VdsParseError> {
2326    let Some(open) = source.find('[') else {
2327        return Ok((source.trim(), None));
2328    };
2329    let Some(close) = source.rfind(']') else {
2330        return Err(VdsParseError {
2331            offset,
2332            code: "unclosedReferenceRange",
2333            detail: format!("missing ] in reference <{source}>"),
2334        });
2335    };
2336    if close + 1 != source.len() {
2337        return Err(VdsParseError {
2338            offset,
2339            code: "trailingReferenceRangeContent",
2340            detail: format!("unexpected content after range in <{source}>"),
2341        });
2342    }
2343    let name = source[..open].trim();
2344    let mut bounds = source[open + 1..close].split(',').map(str::trim);
2345    let min = bounds.next().unwrap_or_default();
2346    let max = bounds.next();
2347    if name.is_empty() || min.is_empty() || max.is_none() || bounds.next().is_some() {
2348        return Err(VdsParseError {
2349            offset,
2350            code: "invalidReferenceRange",
2351            detail: format!("invalid numeric range in <{source}>"),
2352        });
2353    }
2354    let max = max.unwrap_or_default();
2355    Ok((
2356        name,
2357        Some(NumericRange {
2358            min: finite_range_bound(min),
2359            max: finite_range_bound(max),
2360        }),
2361    ))
2362}
2363
2364fn finite_range_bound(source: &str) -> Option<String> {
2365    (!matches!(source, "∞" | "+∞" | "-∞")).then(|| source.to_string())
2366}
2367
2368#[derive(Debug, Clone, PartialEq, Eq)]
2369enum MatchStop {
2370    Budget {
2371        kind: CssValueGrammarBudgetKindV0,
2372        limit: usize,
2373        reference: Option<String>,
2374    },
2375    GrammarDefect {
2376        offset: usize,
2377        code: &'static str,
2378        detail: String,
2379    },
2380}
2381
2382struct MatchContext<'a> {
2383    registry: &'a SpecGrammarRegistryV0,
2384    budget: CssValueGrammarBudgetV0,
2385    match_steps: usize,
2386    first_stop: Option<MatchStop>,
2387    grammar_cache: HashMap<(ReferenceCategory, String), CachedVdsExpression>,
2388    cache_registered_grammars: bool,
2389    allow_unvalidated_standard_function_references: bool,
2390}
2391
2392#[derive(Debug, Clone, Copy)]
2393struct RepeatMatchPlan<'a> {
2394    expression: &'a VdsExpression,
2395    min: usize,
2396    max: Option<usize>,
2397    comma_separated: bool,
2398}
2399
2400impl MatchContext<'_> {
2401    fn match_expression(
2402        &mut self,
2403        expression: &VdsExpression,
2404        components: &[CssValueComponentV0],
2405        position: usize,
2406        reference_depth: usize,
2407    ) -> BTreeSet<usize> {
2408        if !self.consume_step(None) {
2409            return BTreeSet::new();
2410        }
2411        let positions = match expression {
2412            VdsExpression::Literal(literal) => match_literal(literal, components, position),
2413            VdsExpression::Reference(reference) => {
2414                self.match_reference(reference, components, position, reference_depth)
2415            }
2416            VdsExpression::Function { name, arguments } => {
2417                self.match_function(name, arguments, components, position, reference_depth)
2418            }
2419            VdsExpression::Sequence(expressions) => {
2420                self.match_sequence(expressions, components, position, reference_depth)
2421            }
2422            VdsExpression::AllInAnyOrder(expressions) => {
2423                self.match_any_order(expressions, components, position, reference_depth, true)
2424            }
2425            VdsExpression::OneOrMoreInAnyOrder(expressions) => {
2426                self.match_any_order(expressions, components, position, reference_depth, false)
2427            }
2428            VdsExpression::Choice(expressions) => expressions
2429                .iter()
2430                .flat_map(|expression| {
2431                    self.match_expression(expression, components, position, reference_depth)
2432                })
2433                .collect(),
2434            VdsExpression::Repeat {
2435                expression,
2436                min,
2437                max,
2438                comma_separated,
2439            } => self.match_repeat(
2440                RepeatMatchPlan {
2441                    expression,
2442                    min: *min,
2443                    max: *max,
2444                    comma_separated: *comma_separated,
2445                },
2446                components,
2447                position,
2448                reference_depth,
2449            ),
2450            VdsExpression::Required(expression) => self
2451                .match_expression(expression, components, position, reference_depth)
2452                .into_iter()
2453                .filter(|end| *end > position)
2454                .collect(),
2455        };
2456        self.cap_states(positions, None)
2457    }
2458
2459    fn match_sequence(
2460        &mut self,
2461        expressions: &[VdsExpression],
2462        components: &[CssValueComponentV0],
2463        position: usize,
2464        reference_depth: usize,
2465    ) -> BTreeSet<usize> {
2466        let mut states = BTreeSet::from([(position, false)]);
2467        for expression in expressions {
2468            let mut next = BTreeSet::new();
2469            for (position, previous_was_omitted) in states {
2470                if previous_was_omitted && is_comma_literal(expression) {
2471                    next.insert((position, false));
2472                    continue;
2473                }
2474                for end in self.match_expression(expression, components, position, reference_depth)
2475                {
2476                    next.insert((end, end == position));
2477                }
2478            }
2479            if next.len() > self.budget.max_states {
2480                self.record_stop(MatchStop::Budget {
2481                    kind: CssValueGrammarBudgetKindV0::CandidateStates,
2482                    limit: self.budget.max_states,
2483                    reference: None,
2484                });
2485                next.clear();
2486            }
2487            states = next;
2488            if states.is_empty() {
2489                break;
2490            }
2491        }
2492        states.into_iter().map(|(position, _)| position).collect()
2493    }
2494
2495    fn match_repeat(
2496        &mut self,
2497        plan: RepeatMatchPlan<'_>,
2498        components: &[CssValueComponentV0],
2499        position: usize,
2500        reference_depth: usize,
2501    ) -> BTreeSet<usize> {
2502        let effective_max = plan
2503            .max
2504            .unwrap_or_else(|| components.len().saturating_add(1));
2505        let mut accepted = BTreeSet::new();
2506        let mut frontier = BTreeSet::from([position]);
2507        if plan.min == 0 {
2508            accepted.insert(position);
2509        }
2510        for count in 1..=effective_max {
2511            let mut next = BTreeSet::new();
2512            for current in &frontier {
2513                let item_start = if plan.comma_separated && count > 1 {
2514                    if components.get(*current).is_some_and(|component| {
2515                        matches!(component.kind, CssValueComponentKindV0::Comma)
2516                    }) {
2517                        *current + 1
2518                    } else {
2519                        continue;
2520                    }
2521                } else {
2522                    *current
2523                };
2524                for end in
2525                    self.match_expression(plan.expression, components, item_start, reference_depth)
2526                {
2527                    if end > item_start {
2528                        next.insert(end);
2529                    }
2530                }
2531            }
2532            frontier = self.cap_states(next, None);
2533            if frontier.is_empty() {
2534                break;
2535            }
2536            if count >= plan.min {
2537                accepted.extend(frontier.iter().copied());
2538            }
2539        }
2540        accepted
2541    }
2542
2543    fn match_any_order(
2544        &mut self,
2545        expressions: &[VdsExpression],
2546        components: &[CssValueComponentV0],
2547        position: usize,
2548        reference_depth: usize,
2549        require_all: bool,
2550    ) -> BTreeSet<usize> {
2551        if expressions.len() > 63 {
2552            self.record_stop(MatchStop::Budget {
2553                kind: CssValueGrammarBudgetKindV0::CandidateStates,
2554                limit: self.budget.max_states,
2555                reference: None,
2556            });
2557            return BTreeSet::new();
2558        }
2559        let required_mask = (1u64 << expressions.len()) - 1;
2560        let mut accepted = BTreeSet::new();
2561        let mut stack = vec![(position, 0u64)];
2562        let mut visited = BTreeSet::new();
2563        while let Some((current, mask)) = stack.pop() {
2564            if !visited.insert((current, mask)) {
2565                continue;
2566            }
2567            if visited.len() > self.budget.max_states {
2568                self.record_stop(MatchStop::Budget {
2569                    kind: CssValueGrammarBudgetKindV0::CandidateStates,
2570                    limit: self.budget.max_states,
2571                    reference: None,
2572                });
2573                break;
2574            }
2575            if (require_all && mask == required_mask) || (!require_all && mask != 0) {
2576                accepted.insert(current);
2577            }
2578            for (index, expression) in expressions.iter().enumerate() {
2579                let bit = 1u64 << index;
2580                if mask & bit != 0 {
2581                    continue;
2582                }
2583                for end in self.match_expression(expression, components, current, reference_depth) {
2584                    if end > current || (require_all && end == current) {
2585                        stack.push((end, mask | bit));
2586                    }
2587                }
2588            }
2589        }
2590        accepted
2591    }
2592
2593    fn match_function(
2594        &mut self,
2595        name: &str,
2596        arguments: &VdsExpression,
2597        components: &[CssValueComponentV0],
2598        position: usize,
2599        reference_depth: usize,
2600    ) -> BTreeSet<usize> {
2601        let Some(component) = components.get(position) else {
2602            return BTreeSet::new();
2603        };
2604        let CssValueComponentKindV0::Function {
2605            name: actual,
2606            arguments: actual_arguments,
2607        } = &component.kind
2608        else {
2609            return BTreeSet::new();
2610        };
2611        if !actual.eq_ignore_ascii_case(name) {
2612            return BTreeSet::new();
2613        }
2614        self.match_expression(arguments, actual_arguments, 0, reference_depth)
2615            .contains(&actual_arguments.len())
2616            .then_some(position + 1)
2617            .into_iter()
2618            .collect()
2619    }
2620
2621    fn match_reference(
2622        &mut self,
2623        reference: &VdsReference,
2624        components: &[CssValueComponentV0],
2625        position: usize,
2626        reference_depth: usize,
2627    ) -> BTreeSet<usize> {
2628        if self.allow_unvalidated_standard_function_references
2629            && reference.category != ReferenceCategory::Function
2630            && components
2631                .get(position)
2632                .is_some_and(component_is_recognized_standard_function)
2633        {
2634            return BTreeSet::from([position + 1]);
2635        }
2636        if let Some(positions) =
2637            match_builtin_reference(reference, components, position, self.registry)
2638        {
2639            return positions;
2640        }
2641        if reference_depth >= self.budget.max_reference_depth {
2642            self.record_stop(MatchStop::Budget {
2643                kind: CssValueGrammarBudgetKindV0::ReferenceDepth,
2644                limit: self.budget.max_reference_depth,
2645                reference: Some(reference.name.clone()),
2646            });
2647            return BTreeSet::new();
2648        }
2649        let category = match reference.category {
2650            ReferenceCategory::Type => "types",
2651            ReferenceCategory::Property => "properties",
2652            ReferenceCategory::Function => "functions",
2653        };
2654        let Some(entry) = self.registry.entry(category, reference.name.as_str()) else {
2655            self.record_stop(MatchStop::GrammarDefect {
2656                offset: 0,
2657                code: "unknownGrammarReference",
2658                detail: format!("unknown {category} reference <{}>", reference.name),
2659            });
2660            return BTreeSet::new();
2661        };
2662        let Some(source) = entry.syntax.as_deref() else {
2663            self.record_stop(MatchStop::GrammarDefect {
2664                offset: 0,
2665                code: "missingReferencedGrammar",
2666                detail: format!("{category} reference <{}> has no syntax", reference.name),
2667            });
2668            return BTreeSet::new();
2669        };
2670        let key = (reference.category, reference.name.clone());
2671        let expression = match self
2672            .grammar_cache
2673            .entry(key)
2674            .or_insert_with(|| {
2675                if self.cache_registered_grammars {
2676                    cached_pinned_vds_expression(source)
2677                } else {
2678                    VdsParser::new(source).parse().map(Arc::new)
2679                }
2680            })
2681            .clone()
2682        {
2683            Ok(expression) => expression,
2684            Err(error) => {
2685                self.record_stop(MatchStop::GrammarDefect {
2686                    offset: error.offset,
2687                    code: error.code,
2688                    detail: format!("referenced grammar <{}>: {}", reference.name, error.detail),
2689                });
2690                return BTreeSet::new();
2691            }
2692        };
2693        if reference.category == ReferenceCategory::Function {
2694            return self.match_function_reference(
2695                reference,
2696                expression.as_ref(),
2697                components,
2698                position,
2699                reference_depth + 1,
2700            );
2701        }
2702        self.match_expression(
2703            expression.as_ref(),
2704            components,
2705            position,
2706            reference_depth + 1,
2707        )
2708    }
2709
2710    fn match_function_reference(
2711        &mut self,
2712        reference: &VdsReference,
2713        expression: &VdsExpression,
2714        components: &[CssValueComponentV0],
2715        position: usize,
2716        reference_depth: usize,
2717    ) -> BTreeSet<usize> {
2718        let name = reference.name.trim_end_matches("()");
2719        let whole_component =
2720            self.match_expression(expression, components, position, reference_depth);
2721        if !whole_component.is_empty() {
2722            return whole_component;
2723        }
2724        self.match_function(name, expression, components, position, reference_depth)
2725    }
2726
2727    fn consume_step(&mut self, reference: Option<String>) -> bool {
2728        self.match_steps += 1;
2729        if self.match_steps <= self.budget.max_match_steps {
2730            return true;
2731        }
2732        self.record_stop(MatchStop::Budget {
2733            kind: CssValueGrammarBudgetKindV0::MatchSteps,
2734            limit: self.budget.max_match_steps,
2735            reference,
2736        });
2737        false
2738    }
2739
2740    fn cap_states(
2741        &mut self,
2742        mut states: BTreeSet<usize>,
2743        reference: Option<String>,
2744    ) -> BTreeSet<usize> {
2745        if states.len() <= self.budget.max_states {
2746            return states;
2747        }
2748        self.record_stop(MatchStop::Budget {
2749            kind: CssValueGrammarBudgetKindV0::CandidateStates,
2750            limit: self.budget.max_states,
2751            reference,
2752        });
2753        states.clear();
2754        states
2755    }
2756
2757    fn record_stop(&mut self, stop: MatchStop) {
2758        if self.first_stop.is_none() {
2759            self.first_stop = Some(stop);
2760        }
2761    }
2762}
2763
2764fn match_literal(
2765    literal: &str,
2766    components: &[CssValueComponentV0],
2767    position: usize,
2768) -> BTreeSet<usize> {
2769    components
2770        .get(position)
2771        .filter(|component| component.text.eq_ignore_ascii_case(literal))
2772        .map(|_| BTreeSet::from([position + 1]))
2773        .unwrap_or_default()
2774}
2775
2776fn is_comma_literal(expression: &VdsExpression) -> bool {
2777    matches!(expression, VdsExpression::Literal(literal) if literal == ",")
2778}
2779
2780fn is_unitless_zero(component: &CssValueComponentV0) -> bool {
2781    matches!(component.kind, CssValueComponentKindV0::Number)
2782        && parse_numeric_value_with_unit(component.text.as_str())
2783            .is_some_and(|numeric| numeric.value == 0.0 && numeric.unit.is_empty())
2784}
2785
2786fn match_builtin_reference(
2787    reference: &VdsReference,
2788    components: &[CssValueComponentV0],
2789    position: usize,
2790    registry: &SpecGrammarRegistryV0,
2791) -> Option<BTreeSet<usize>> {
2792    if reference.category != ReferenceCategory::Type {
2793        return None;
2794    }
2795    if matches!(
2796        reference.name.as_str(),
2797        "declaration-value" | "any-value" | "whole-value"
2798    ) {
2799        return Some(((position + 1)..=components.len()).collect());
2800    }
2801    if !is_builtin_reference_name(reference.name.as_str()) {
2802        return None;
2803    }
2804    let Some(component) = components.get(position) else {
2805        return Some(BTreeSet::new());
2806    };
2807    if math_function_matches_reference(reference, component, registry) {
2808        return Some(BTreeSet::from([position + 1]));
2809    }
2810    let kind = classify_registered_property_declared_value_v0(component.text.as_str());
2811    let accepted = match reference.name.as_str() {
2812        "number" | "number-token" => {
2813            matches!(
2814                kind,
2815                DeclaredValueKindV0::Number | DeclaredValueKindV0::Integer
2816            )
2817        }
2818        "integer" => matches!(kind, DeclaredValueKindV0::Integer),
2819        "length" => {
2820            matches!(
2821                kind,
2822                DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Length)
2823            ) || is_unitless_zero(component)
2824        }
2825        "percentage" | "percentage-token" => matches!(
2826            kind,
2827            DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage)
2828        ),
2829        "length-percentage" => {
2830            matches!(
2831                kind,
2832                DeclaredValueKindV0::Dimension(
2833                    DeclaredNumericTypeV0::Length | DeclaredNumericTypeV0::Percentage
2834                )
2835            ) || is_unitless_zero(component)
2836        }
2837        "angle" => matches!(
2838            kind,
2839            DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Angle)
2840        ),
2841        "time" => matches!(
2842            kind,
2843            DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Time)
2844        ),
2845        "resolution" => matches!(
2846            kind,
2847            DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Resolution)
2848        ),
2849        "flex" => parse_numeric_value_with_unit(component.text.as_str())
2850            .is_some_and(|numeric| numeric.unit.eq_ignore_ascii_case("fr")),
2851        "hex-color" => matches!(kind, DeclaredValueKindV0::HexColor),
2852        "named-color" => matches!(kind, DeclaredValueKindV0::ColorKeyword(_)),
2853        "custom-ident" => {
2854            matches!(component.kind, CssValueComponentKindV0::Ident)
2855                && !matches!(kind, DeclaredValueKindV0::CssWide)
2856        }
2857        "ident" | "ident-token" => matches!(component.kind, CssValueComponentKindV0::Ident),
2858        "dashed-ident" | "custom-property-name" => {
2859            matches!(component.kind, CssValueComponentKindV0::Ident)
2860                && is_custom_property_name(&component.text)
2861        }
2862        "string" | "string-token" => matches!(kind, DeclaredValueKindV0::QuotedString),
2863        "url" | "url-token" => matches!(kind, DeclaredValueKindV0::Url),
2864        "image" => matches!(
2865            kind,
2866            DeclaredValueKindV0::ImageFunction | DeclaredValueKindV0::Url
2867        ),
2868        "transform-function" => matches!(kind, DeclaredValueKindV0::TransformFunction),
2869        "alpha-value" => matches!(
2870            kind,
2871            DeclaredValueKindV0::Number
2872                | DeclaredValueKindV0::Integer
2873                | DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage)
2874        ),
2875        "zero" => parse_numeric_value_with_unit(component.text.as_str())
2876            .is_some_and(|numeric| numeric.value == 0.0),
2877        "dimension-token" => matches!(component.kind, CssValueComponentKindV0::Dimension),
2878        "hash-token" => matches!(component.kind, CssValueComponentKindV0::Hash),
2879        "function-token" => matches!(component.kind, CssValueComponentKindV0::Function { .. }),
2880        "comma-token" => matches!(component.kind, CssValueComponentKindV0::Comma),
2881        _ => false,
2882    };
2883    let accepted =
2884        accepted && numeric_range_accepts(reference.range.as_ref(), component.text.as_str());
2885    Some(accepted.then_some(position + 1).into_iter().collect())
2886}
2887
2888fn is_builtin_reference_name(name: &str) -> bool {
2889    matches!(
2890        name,
2891        "number"
2892            | "number-token"
2893            | "integer"
2894            | "length"
2895            | "percentage"
2896            | "percentage-token"
2897            | "length-percentage"
2898            | "angle"
2899            | "time"
2900            | "resolution"
2901            | "flex"
2902            | "hex-color"
2903            | "named-color"
2904            | "custom-ident"
2905            | "ident"
2906            | "ident-token"
2907            | "dashed-ident"
2908            | "custom-property-name"
2909            | "string"
2910            | "string-token"
2911            | "url"
2912            | "url-token"
2913            | "image"
2914            | "transform-function"
2915            | "alpha-value"
2916            | "zero"
2917            | "dimension-token"
2918            | "hash-token"
2919            | "function-token"
2920            | "comma-token"
2921    )
2922}
2923
2924fn math_function_matches_reference(
2925    reference: &VdsReference,
2926    component: &CssValueComponentV0,
2927    registry: &SpecGrammarRegistryV0,
2928) -> bool {
2929    if reference.category != ReferenceCategory::Type
2930        || !matches!(
2931            reference.name.as_str(),
2932            "number" | "length" | "percentage" | "length-percentage" | "time" | "angle"
2933        )
2934    {
2935        return false;
2936    }
2937    let CssValueComponentKindV0::Function { name, arguments } = &component.kind else {
2938        return false;
2939    };
2940    if !matches!(name.as_str(), "calc" | "min" | "max" | "clamp") {
2941        return false;
2942    }
2943    let registry_name = format!("{name}()");
2944    if !registry
2945        .entry("functions", registry_name.as_str())
2946        .is_some_and(|entry| {
2947            entry.boundary.classification == SpecGrammarBoundaryClassificationV0::InBoundary
2948                && entry.syntax.is_some()
2949        })
2950    {
2951        return false;
2952    }
2953    math_function_result_kind(name, arguments, registry)
2954        .is_some_and(|kind| math_kind_matches_reference(kind, reference.name.as_str()))
2955        && math_range_is_provably_accepted(reference.range.as_ref(), arguments)
2956}
2957
2958fn split_math_argument_groups(arguments: &[CssValueComponentV0]) -> Vec<&[CssValueComponentV0]> {
2959    let mut groups = Vec::new();
2960    let mut start = 0;
2961    for (index, component) in arguments.iter().enumerate() {
2962        if matches!(component.kind, CssValueComponentKindV0::Comma) {
2963            groups.push(&arguments[start..index]);
2964            start = index + 1;
2965        }
2966    }
2967    groups.push(&arguments[start..]);
2968    groups
2969}
2970
2971#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2972enum MathValueKind {
2973    Number,
2974    Length,
2975    Percentage,
2976    LengthPercentage,
2977    Time,
2978    Angle,
2979}
2980
2981fn math_kind_matches_reference(kind: MathValueKind, reference: &str) -> bool {
2982    matches!(
2983        (kind, reference),
2984        (MathValueKind::Number, "number")
2985            | (MathValueKind::Length, "length" | "length-percentage")
2986            | (
2987                MathValueKind::Percentage,
2988                "percentage" | "length-percentage"
2989            )
2990            | (MathValueKind::LengthPercentage, "length-percentage")
2991            | (MathValueKind::Time, "time")
2992            | (MathValueKind::Angle, "angle")
2993    )
2994}
2995
2996fn math_function_result_kind(
2997    name: &str,
2998    arguments: &[CssValueComponentV0],
2999    registry: &SpecGrammarRegistryV0,
3000) -> Option<MathValueKind> {
3001    if !matches!(name, "calc" | "min" | "max" | "clamp") {
3002        return None;
3003    }
3004    let registry_name = format!("{name}()");
3005    if !registry
3006        .entry("functions", registry_name.as_str())
3007        .is_some_and(|entry| {
3008            entry.boundary.classification == SpecGrammarBoundaryClassificationV0::InBoundary
3009                && entry.syntax.is_some()
3010        })
3011    {
3012        return None;
3013    }
3014    let groups = split_math_argument_groups(arguments);
3015    let arity_matches = match name {
3016        "calc" => groups.len() == 1,
3017        "min" | "max" => !groups.is_empty(),
3018        "clamp" => groups.len() == 3,
3019        _ => false,
3020    };
3021    if !arity_matches {
3022        return None;
3023    }
3024    let mut kinds = groups
3025        .iter()
3026        .map(|group| MathExpressionParser::new(group, registry).parse())
3027        .collect::<Option<Vec<_>>>()?
3028        .into_iter();
3029    let first = kinds.next()?;
3030    kinds.try_fold(first, unify_additive_math_kinds)
3031}
3032
3033fn unify_additive_math_kinds(left: MathValueKind, right: MathValueKind) -> Option<MathValueKind> {
3034    if left == right {
3035        return Some(left);
3036    }
3037    if matches!(
3038        (left, right),
3039        (MathValueKind::Length, MathValueKind::Percentage)
3040            | (MathValueKind::Percentage, MathValueKind::Length)
3041            | (MathValueKind::LengthPercentage, MathValueKind::Length)
3042            | (MathValueKind::Length, MathValueKind::LengthPercentage)
3043            | (MathValueKind::LengthPercentage, MathValueKind::Percentage)
3044            | (MathValueKind::Percentage, MathValueKind::LengthPercentage)
3045    ) {
3046        return Some(MathValueKind::LengthPercentage);
3047    }
3048    None
3049}
3050
3051struct MathExpressionParser<'a> {
3052    components: &'a [CssValueComponentV0],
3053    cursor: usize,
3054    registry: &'a SpecGrammarRegistryV0,
3055}
3056
3057impl<'a> MathExpressionParser<'a> {
3058    fn new(components: &'a [CssValueComponentV0], registry: &'a SpecGrammarRegistryV0) -> Self {
3059        Self {
3060            components,
3061            cursor: 0,
3062            registry,
3063        }
3064    }
3065
3066    fn parse(mut self) -> Option<MathValueKind> {
3067        let kind = self.parse_sum()?;
3068        (self.cursor == self.components.len()).then_some(kind)
3069    }
3070
3071    fn parse_sum(&mut self) -> Option<MathValueKind> {
3072        let mut left = self.parse_product()?;
3073        loop {
3074            if self.peek_binary_additive_operator().is_none() {
3075                break;
3076            }
3077            self.cursor += 1;
3078            let right = self.parse_product()?;
3079            left = unify_additive_math_kinds(left, right)?;
3080        }
3081        Some(left)
3082    }
3083
3084    fn parse_product(&mut self) -> Option<MathValueKind> {
3085        let mut left = self.parse_unary()?;
3086        while let Some(operator) = self.peek_operator(&["*", "/"]).map(str::to_owned) {
3087            self.cursor += 1;
3088            let right = self.parse_unary()?;
3089            left = match operator.as_str() {
3090                "*" if left == MathValueKind::Number => right,
3091                "*" if right == MathValueKind::Number => left,
3092                "/" if right == MathValueKind::Number => left,
3093                _ => return None,
3094            };
3095        }
3096        Some(left)
3097    }
3098
3099    fn parse_unary(&mut self) -> Option<MathValueKind> {
3100        if self.peek_operator(&["+", "-"]).is_some() {
3101            self.cursor += 1;
3102            return self.parse_unary();
3103        }
3104        self.parse_operand()
3105    }
3106
3107    fn parse_operand(&mut self) -> Option<MathValueKind> {
3108        let component = self.components.get(self.cursor)?;
3109        self.cursor += 1;
3110        match &component.kind {
3111            CssValueComponentKindV0::Number => Some(MathValueKind::Number),
3112            CssValueComponentKindV0::Percentage => Some(MathValueKind::Percentage),
3113            CssValueComponentKindV0::Dimension => {
3114                match classify_registered_property_declared_value_v0(component.text.as_str()) {
3115                    DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Length) => {
3116                        Some(MathValueKind::Length)
3117                    }
3118                    DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage) => {
3119                        Some(MathValueKind::Percentage)
3120                    }
3121                    DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Time) => {
3122                        Some(MathValueKind::Time)
3123                    }
3124                    DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Angle) => {
3125                        Some(MathValueKind::Angle)
3126                    }
3127                    _ => None,
3128                }
3129            }
3130            CssValueComponentKindV0::Function { name, arguments } => {
3131                math_function_result_kind(name, arguments, self.registry)
3132            }
3133            CssValueComponentKindV0::Parenthesized { values } => {
3134                MathExpressionParser::new(values, self.registry).parse()
3135            }
3136            CssValueComponentKindV0::Ident
3137            | CssValueComponentKindV0::Hash
3138            | CssValueComponentKindV0::String
3139            | CssValueComponentKindV0::Url
3140            | CssValueComponentKindV0::Bracketed { .. }
3141            | CssValueComponentKindV0::Braced { .. }
3142            | CssValueComponentKindV0::Comma
3143            | CssValueComponentKindV0::Slash
3144            | CssValueComponentKindV0::Delimiter => None,
3145        }
3146    }
3147
3148    fn peek_operator(&self, expected: &[&str]) -> Option<&str> {
3149        let component = self.components.get(self.cursor)?;
3150        (matches!(
3151            component.kind,
3152            CssValueComponentKindV0::Delimiter | CssValueComponentKindV0::Slash
3153        ) || (matches!(component.kind, CssValueComponentKindV0::Ident)
3154            && matches!(component.text.as_str(), "+" | "-")))
3155        .then_some(component.text.as_str())
3156        .filter(|operator| expected.contains(operator))
3157    }
3158
3159    fn peek_binary_additive_operator(&self) -> Option<&str> {
3160        let previous = self
3161            .cursor
3162            .checked_sub(1)
3163            .and_then(|index| self.components.get(index))?;
3164        let operator_component = self.components.get(self.cursor)?;
3165        let next = self.components.get(self.cursor + 1)?;
3166        self.peek_operator(&["+", "-"]).filter(|_| {
3167            previous.span.end < operator_component.span.start
3168                && operator_component.span.end < next.span.start
3169        })
3170    }
3171}
3172
3173fn math_range_is_provably_accepted(
3174    range: Option<&NumericRange>,
3175    components: &[CssValueComponentV0],
3176) -> bool {
3177    let Some(range) = range else {
3178        return true;
3179    };
3180    if !range
3181        .min
3182        .as_deref()
3183        .and_then(parse_numeric_value_with_unit)
3184        .is_some_and(|numeric| numeric.value == 0.0)
3185        || range.max.is_some()
3186    {
3187        return false;
3188    }
3189    components.iter().all(|component| match &component.kind {
3190        CssValueComponentKindV0::Number
3191        | CssValueComponentKindV0::Percentage
3192        | CssValueComponentKindV0::Dimension => parse_numeric_value_with_unit(&component.text)
3193            .is_some_and(|numeric| numeric.value >= 0.0),
3194        CssValueComponentKindV0::Function { arguments, .. }
3195        | CssValueComponentKindV0::Parenthesized { values: arguments } => {
3196            math_range_is_provably_accepted(Some(range), arguments)
3197        }
3198        CssValueComponentKindV0::Delimiter => matches!(component.text.as_str(), "+" | "-"),
3199        CssValueComponentKindV0::Comma | CssValueComponentKindV0::Slash => true,
3200        CssValueComponentKindV0::Ident => matches!(component.text.as_str(), "+" | "-"),
3201        CssValueComponentKindV0::Hash
3202        | CssValueComponentKindV0::String
3203        | CssValueComponentKindV0::Url
3204        | CssValueComponentKindV0::Bracketed { .. }
3205        | CssValueComponentKindV0::Braced { .. } => false,
3206    })
3207}
3208
3209fn numeric_range_accepts(range: Option<&NumericRange>, source: &str) -> bool {
3210    let Some(range) = range else {
3211        return true;
3212    };
3213    let Some(numeric) = parse_numeric_value_with_unit(source) else {
3214        return false;
3215    };
3216    let above_min = range
3217        .min
3218        .as_deref()
3219        .and_then(|value| value.parse::<f64>().ok())
3220        .is_none_or(|minimum| numeric.value >= minimum);
3221    let below_max = range
3222        .max
3223        .as_deref()
3224        .and_then(|value| value.parse::<f64>().ok())
3225        .is_none_or(|maximum| numeric.value <= maximum);
3226    above_min && below_max
3227}
3228
3229fn component_locus(components: &[CssValueComponentV0]) -> CssValueGrammarLocusV0 {
3230    match (components.first(), components.last()) {
3231        (Some(first), Some(last)) => CssValueGrammarLocusV0 {
3232            start: first.span.start,
3233            end: last.span.end,
3234        },
3235        _ => CssValueGrammarLocusV0 { start: 0, end: 0 },
3236    }
3237}
3238
3239fn grammar_defect(
3240    grammar: &str,
3241    offset: usize,
3242    code: impl Into<String>,
3243    detail: impl Into<String>,
3244) -> CssValueGrammarVerdictV0 {
3245    CssValueGrammarVerdictV0::GrammarDefect {
3246        grammar: grammar.to_string(),
3247        offset,
3248        code: code.into(),
3249        detail: detail.into(),
3250    }
3251}
3252
3253fn strip_matching_quotes(source: &str) -> &str {
3254    if source.len() >= 2 {
3255        let bytes = source.as_bytes();
3256        if matches!(
3257            (bytes[0], bytes[source.len() - 1]),
3258            (b'\'', b'\'') | (b'"', b'"')
3259        ) {
3260            return &source[1..source.len() - 1];
3261        }
3262    }
3263    source
3264}
3265
3266#[cfg(test)]
3267mod tests {
3268    use std::{collections::BTreeSet, sync::Arc};
3269
3270    use omena_cascade::{CascadeStandardValueValidatorV0, CascadeStandardValueVerdictV0};
3271    use omena_spec_audit::{SpecGrammarBoundaryClassificationV0, spec_grammar_registry};
3272    use omena_syntax::ident::PropertyNameV0;
3273    use omena_value_lattice::ValueNodeV0;
3274
3275    use super::{
3276        CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE, CLOSED_WORLD_TOKEN_KINDS,
3277        CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0, CssValueGrammarBudgetKindV0,
3278        CssValueGrammarBudgetV0, CssValueGrammarLocusV0, CssValueGrammarVerdictV0,
3279        CssValueValidationClassV0, CssValueValidationReasonV0,
3280        SpecStandardPropertyValueValidatorV0, adjudicate_css_value_validation,
3281        adjudicate_css_value_validation_with_boundary, audit_css_value_grammar_registry_v0,
3282        cached_pinned_vds_expression, cached_standard_property_closed_world_token_profile,
3283        closed_world_builtin_profile, closed_world_builtin_token_profiles,
3284        closed_world_keyword_authority, match_and_type_css_value_grammar_v0,
3285        match_and_type_standard_property_value_v0, match_css_value_grammar_v0,
3286        match_standard_property_value_v0, parse_closed_world_keyword_closure_certificate,
3287        standard_property_closed_world_token_kind_mismatch, validate_registered_property_value_v0,
3288        validate_standard_property_value_v0,
3289    };
3290    use crate::{
3291        AbstractCssTypedValueV0, AbstractCssValueV0, DeclaredValueKindV0,
3292        classify_registered_property_declared_value_v0,
3293    };
3294
3295    fn assert_matches(grammar: &str, value: &str) {
3296        let verdict = match_css_value_grammar_v0(
3297            grammar,
3298            value,
3299            spec_grammar_registry(),
3300            CssValueGrammarBudgetV0::default(),
3301        );
3302        assert!(
3303            verdict.is_matched(),
3304            "{grammar:?} should match {value:?}: {verdict:?}"
3305        );
3306    }
3307
3308    fn assert_unmatched(grammar: &str, value: &str) {
3309        let verdict = match_css_value_grammar_v0(
3310            grammar,
3311            value,
3312            spec_grammar_registry(),
3313            CssValueGrammarBudgetV0::default(),
3314        );
3315        assert!(
3316            verdict.is_definite_mismatch(),
3317            "{grammar:?} should reject {value:?}: {verdict:?}"
3318        );
3319    }
3320
3321    #[test]
3322    fn parsed_grammar_cache_reuses_the_immutable_expression() {
3323        let grammar = "<length> | cache-sentinel";
3324        let first = cached_pinned_vds_expression(grammar);
3325        let second = cached_pinned_vds_expression(grammar);
3326
3327        assert!(matches!(
3328            (&first, &second),
3329            (Ok(first), Ok(second)) if Arc::ptr_eq(first, second)
3330        ));
3331    }
3332
3333    #[test]
3334    fn grammar_conformance_covers_all_combinators_and_multipliers() {
3335        for (grammar, value) in [
3336            ("<length> <color>", "1px red"),
3337            ("<length> && <color>", "red 1px"),
3338            ("<length> || <color>", "red"),
3339            ("auto | <length>", "auto"),
3340            ("[ auto | <length> ]?", ""),
3341            ("<length>*", "1px 2px"),
3342            ("<length>+", "1px 2px"),
3343            ("<length>#", "1px, 2px"),
3344            ("<length>{2,3}", "1px 2px 3px"),
3345            ("<length>#{2}", "1px, 2px"),
3346            ("[ <length>? <color>? ]!", "red"),
3347            ("rgb( <number>#{3} )", "rgb(1, 2, 3)"),
3348        ] {
3349            assert_matches(grammar, value);
3350        }
3351        for (grammar, value) in [
3352            ("<length> <color>", "red 1px"),
3353            ("<length> && <color>", "1px"),
3354            ("<length> || <color>", "auto"),
3355            ("<length>+", ""),
3356            ("<length>#", "1px 2px"),
3357            ("<length>{2,3}", "1px"),
3358            ("[ <length>? <color>? ]!", ""),
3359            ("rgb( <number>#{3} )", "rgb(1, 2)"),
3360        ] {
3361            assert_unmatched(grammar, value);
3362        }
3363    }
3364
3365    #[test]
3366    fn combinator_precedence_is_juxtaposition_then_and_then_double_or_then_or() {
3367        let grammar = "a b && c || d | e";
3368        for value in ["c a b", "a b c", "d", "e"] {
3369            assert_matches(grammar, value);
3370        }
3371        for value in ["a c", "b c", "a b d"] {
3372            assert_unmatched(grammar, value);
3373        }
3374    }
3375
3376    #[test]
3377    fn reference_depth_exhaustion_is_typed_and_provenanced() {
3378        let verdict = match_css_value_grammar_v0(
3379            "<calc-sum>",
3380            "calc(1px + 2px)",
3381            spec_grammar_registry(),
3382            CssValueGrammarBudgetV0 {
3383                max_reference_depth: 0,
3384                ..CssValueGrammarBudgetV0::default()
3385            },
3386        );
3387        assert!(matches!(
3388            verdict,
3389            CssValueGrammarVerdictV0::NotMatchedWithinBudget {
3390                budget: CssValueGrammarBudgetKindV0::ReferenceDepth,
3391                limit: 0,
3392                reference: Some(reference),
3393                ..
3394            } if reference == "calc-sum"
3395        ));
3396    }
3397
3398    #[test]
3399    fn malformed_grammar_is_a_defect_not_a_mismatch() {
3400        let verdict = match_css_value_grammar_v0(
3401            "[ <length> | <color>",
3402            "1px",
3403            spec_grammar_registry(),
3404            CssValueGrammarBudgetV0::default(),
3405        );
3406        assert!(matches!(
3407            verdict,
3408            CssValueGrammarVerdictV0::GrammarDefect { .. }
3409        ));
3410    }
3411
3412    #[test]
3413    fn property_and_type_references_use_the_pinned_registry() {
3414        assert_matches("<'box-sizing'>", "border-box");
3415        assert_matches("<color>", "rebeccapurple");
3416        assert_matches("<rgb()>", "rgb(1 2 3)");
3417        assert!(match_standard_property_value_v0("box-sizing", "content-box").is_matched());
3418        assert!(
3419            match_standard_property_value_v0("box-sizing", "inline-box").is_definite_mismatch()
3420        );
3421    }
3422
3423    #[test]
3424    fn numeric_reference_ranges_are_enforced() {
3425        assert_matches("<number [0,1]>", "0.5");
3426        assert_unmatched("<number [0,1]>", "2");
3427        assert_matches("<length [0,∞]>", "0px");
3428        assert_unmatched("<length [0,∞]>", "-1px");
3429    }
3430
3431    #[test]
3432    fn unitless_zero_matches_length_references_without_widening_other_dimensions() {
3433        for grammar in ["<length>", "<length-percentage>"] {
3434            assert_matches(grammar, "0");
3435        }
3436        for grammar in ["<percentage>", "<angle>", "<time>", "<resolution>"] {
3437            assert_unmatched(grammar, "0");
3438        }
3439        for value in ["1", "-1", "0.5"] {
3440            assert_unmatched("<length>", value);
3441            assert_unmatched("<length-percentage>", value);
3442        }
3443        let calc_verdict = match_css_value_grammar_v0(
3444            "<length> | <calc-sum>",
3445            "calc(0 + 2px)",
3446            spec_grammar_registry(),
3447            CssValueGrammarBudgetV0::default(),
3448        );
3449        assert!(
3450            !calc_verdict.is_matched(),
3451            "unitless zero cannot be added to a dimension inside calc(): {calc_verdict:?}"
3452        );
3453        assert!(
3454            match_css_value_grammar_v0(
3455                "<length> | <calc-sum>",
3456                "calc(0px + 2px)",
3457                spec_grammar_registry(),
3458                CssValueGrammarBudgetV0::default(),
3459            )
3460            .is_matched()
3461        );
3462    }
3463
3464    #[test]
3465    fn standard_properties_accept_unitless_zero_without_retyping_integer_consumers() {
3466        for (property, value) in [
3467            ("padding", "0"),
3468            ("margin", "0 auto"),
3469            ("border-width", "0 4px 6px"),
3470            ("width", "0"),
3471            ("z-index", "0"),
3472            ("opacity", "0"),
3473        ] {
3474            let verdict = validate_standard_property_value_v0(property, value);
3475            assert_eq!(
3476                verdict.class,
3477                CssValueValidationClassV0::Valid,
3478                "{property}: {value} should be valid: {verdict:?}"
3479            );
3480        }
3481        assert_eq!(
3482            classify_registered_property_declared_value_v0("0"),
3483            DeclaredValueKindV0::Integer
3484        );
3485    }
3486
3487    #[test]
3488    fn nullable_all_in_any_order_operands_can_satisfy_their_slots() {
3489        let grammar = "[ alpha? && [ none | beta ] && gamma? ]";
3490        for value in ["none", "alpha none", "none gamma", "gamma none alpha"] {
3491            assert_matches(grammar, value);
3492        }
3493        assert_unmatched(grammar, "none unexpected");
3494    }
3495
3496    #[test]
3497    fn nested_property_references_keep_inner_comma_repetition_reachable() {
3498        assert_matches("<'box-shadow-color'>", "red, blue");
3499        assert_unmatched("<'box-shadow-color'>", "red blue");
3500
3501        let grammar = "[ <'box-shadow-color'>? && [ none | <length>{2} ] [ <'box-shadow-blur'> <'box-shadow-spread'>? ]? && <'box-shadow-position'>? ]";
3502        assert_matches(grammar, "red none");
3503        assert_unmatched(grammar, "red none unexpected");
3504    }
3505
3506    #[test]
3507    fn sequence_omits_a_comma_only_with_an_omitted_adjacent_component() {
3508        let grammar = "<length>? , <color>";
3509        assert_matches(grammar, "red");
3510        assert_matches(grammar, "1px, red");
3511        assert_unmatched(grammar, ", red");
3512        assert_unmatched(grammar, "1px red");
3513    }
3514
3515    #[test]
3516    fn standard_keyword_grammars_remain_precise_through_nested_expansion() {
3517        for (property, value) in [("box-shadow", "none"), ("background", "transparent")] {
3518            let verdict = validate_standard_property_value_v0(property, value);
3519            assert_eq!(
3520                verdict.class,
3521                CssValueValidationClassV0::Valid,
3522                "{property}: {value} should be valid: {verdict:?}"
3523            );
3524        }
3525        for (property, value) in [
3526            ("box-shadow", "1px nonsense"),
3527            ("background", ", transparent"),
3528        ] {
3529            let verdict = validate_standard_property_value_v0(property, value);
3530            assert_ne!(
3531                verdict.class,
3532                CssValueValidationClassV0::Valid,
3533                "{property}: {value} must not be accepted: {verdict:?}"
3534            );
3535        }
3536    }
3537
3538    #[test]
3539    fn reviewed_compatibility_syntax_and_boundary_policy_are_applied_independently() {
3540        let compatibility_value =
3541            validate_standard_property_value_v0("-webkit-background-clip", "text");
3542        assert_eq!(compatibility_value.class, CssValueValidationClassV0::Valid);
3543        assert_eq!(
3544            compatibility_value.reason,
3545            CssValueValidationReasonV0::GrammarMatched
3546        );
3547        let registry = spec_grammar_registry();
3548        let compatibility_entry = registry.entry("properties", "-webkit-background-clip");
3549        assert!(
3550            compatibility_entry.is_some(),
3551            "compatibility property must remain registered"
3552        );
3553        let Some(compatibility_entry) = compatibility_entry else {
3554            return;
3555        };
3556        assert!(compatibility_entry.override_provenance.is_some());
3557
3558        let forward_tier =
3559            validate_standard_property_value_v0("background", "definitely-not-a-background");
3560        assert_eq!(
3561            forward_tier.class,
3562            CssValueValidationClassV0::NotValidatable
3563        );
3564        assert_eq!(
3565            forward_tier.reason,
3566            CssValueValidationReasonV0::ForwardTierGrammar
3567        );
3568        assert!(forward_tier.verdict.is_definite_mismatch());
3569
3570        let in_boundary = validate_standard_property_value_v0("box-sizing", "inline-box");
3571        assert_eq!(in_boundary.class, CssValueValidationClassV0::Invalid);
3572        assert_eq!(
3573            in_boundary.reason,
3574            CssValueValidationReasonV0::GrammarUnmatched
3575        );
3576    }
3577
3578    #[test]
3579    fn pinned_registry_rows_are_all_accounted_for_by_the_grammar_parser() {
3580        const MIN_PINNED_REGISTRY_ENTRY_COUNT: usize = 1_700;
3581
3582        let registry = spec_grammar_registry();
3583        let audit = audit_css_value_grammar_registry_v0(registry);
3584        assert_eq!(audit.total_entry_count, registry.total_entry_count());
3585        assert!(
3586            audit.total_entry_count >= MIN_PINNED_REGISTRY_ENTRY_COUNT,
3587            "pinned registry unexpectedly shrank below the audited coverage floor"
3588        );
3589        assert_eq!(audit.categories.len(), 5);
3590        assert_eq!(
3591            audit.parsed_entry_count + audit.missing_syntax_count + audit.grammar_defect_count,
3592            audit.total_entry_count
3593        );
3594        let properties = audit
3595            .categories
3596            .iter()
3597            .find(|category| category.category == "properties");
3598        assert_eq!(
3599            properties.map(|category| (
3600                category.entry_count,
3601                category.parsed_entry_count,
3602                category.missing_syntax_count,
3603                category.grammar_defect_count,
3604            )),
3605            Some((817, 812, 5, 0))
3606        );
3607        assert_eq!(
3608            (
3609                audit.parsed_entry_count,
3610                audit.missing_syntax_count,
3611                audit.grammar_defect_count,
3612            ),
3613            (1_529, 131, 57)
3614        );
3615    }
3616
3617    #[test]
3618    fn matched_compounds_project_through_existing_typed_and_lattice_domains() {
3619        let border = match_and_type_standard_property_value_v0("border-top", "1px solid red");
3620        assert!(border.verdict.is_matched(), "{:?}", border.verdict);
3621        assert!(matches!(
3622            &border.abstract_value,
3623            AbstractCssValueV0::Exact {
3624                typed: Some(typed), ..
3625            } if matches!(
3626                typed.as_ref(),
3627                AbstractCssTypedValueV0::Compound { leaves } if leaves.len() == 3
3628            )
3629        ));
3630        assert!(matches!(
3631            border.projection.as_ref().map(|projection| projection.lattice.root()),
3632            Some(ValueNodeV0::List { items, .. }) if items.len() == 3
3633        ));
3634
3635        let calc = match_and_type_css_value_grammar_v0(
3636            "calc( <length> '+' <length> )",
3637            "calc(1px + 2px)",
3638            spec_grammar_registry(),
3639            CssValueGrammarBudgetV0::default(),
3640        );
3641        assert!(calc.verdict.is_matched(), "{:?}", calc.verdict);
3642        assert!(matches!(
3643            calc.projection.as_ref().map(|projection| projection.lattice.root()),
3644            Some(ValueNodeV0::Function { name, arguments, .. })
3645                if *name == "calc" && arguments.len() == 3
3646        ));
3647
3648        let font_families =
3649            match_and_type_standard_property_value_v0("font-family", "serif, sans-serif");
3650        assert!(
3651            font_families.verdict.is_matched(),
3652            "{:?}",
3653            font_families.verdict
3654        );
3655        assert!(matches!(
3656            font_families
3657                .projection
3658                .as_ref()
3659                .map(|projection| projection.lattice.root()),
3660            Some(ValueNodeV0::List { .. })
3661        ));
3662    }
3663
3664    #[test]
3665    fn rejected_value_preserves_raw_bytes_and_carries_the_match_locus() {
3666        let source = "  1px nonsense red  ";
3667        let result = match_and_type_standard_property_value_v0("border-top", source);
3668        assert!(matches!(
3669            result.verdict,
3670            CssValueGrammarVerdictV0::Unmatched {
3671                grammar,
3672                locus,
3673            } if grammar == "<line-width> || <line-style> || <color>"
3674                && locus.start == 2
3675                && locus.end == source.len() - 2
3676        ));
3677        assert_eq!(
3678            result.abstract_value,
3679            AbstractCssValueV0::Raw {
3680                value: source.to_string(),
3681            }
3682        );
3683        assert!(result.projection.is_none());
3684    }
3685
3686    #[test]
3687    fn validation_keeps_invalid_and_not_validatable_outcomes_distinct() {
3688        let invalid = validate_standard_property_value_v0("box-sizing", "inline-box");
3689        assert_eq!(invalid.class, CssValueValidationClassV0::Invalid);
3690        assert_eq!(invalid.reason, CssValueValidationReasonV0::GrammarUnmatched);
3691
3692        let closed_world_mismatch = validate_standard_property_value_v0("z-index", "banana");
3693        assert_eq!(
3694            closed_world_mismatch.class,
3695            CssValueValidationClassV0::Invalid
3696        );
3697        assert_eq!(
3698            closed_world_mismatch.reason,
3699            CssValueValidationReasonV0::GrammarUnmatched
3700        );
3701
3702        let defect = validate_registered_property_value_v0("<future-value>", "1px");
3703        assert_eq!(defect.class, CssValueValidationClassV0::NotValidatable);
3704        assert_eq!(defect.reason, CssValueValidationReasonV0::GrammarDefect);
3705
3706        let budget_verdict = match_css_value_grammar_v0(
3707            "<calc-sum>",
3708            "calc(1px + 2px)",
3709            spec_grammar_registry(),
3710            CssValueGrammarBudgetV0 {
3711                max_reference_depth: 0,
3712                ..CssValueGrammarBudgetV0::default()
3713            },
3714        );
3715        let budget = adjudicate_css_value_validation("1px", budget_verdict);
3716        assert_eq!(budget.class, CssValueValidationClassV0::NotValidatable);
3717        assert_eq!(
3718            budget.reason,
3719            CssValueValidationReasonV0::MatchBudgetExhausted
3720        );
3721
3722        let deferred = validate_standard_property_value_v0("width", "var(--width)");
3723        assert_eq!(deferred.class, CssValueValidationClassV0::NotValidatable);
3724        assert_eq!(
3725            deferred.reason,
3726            CssValueValidationReasonV0::DeferredSubstitution
3727        );
3728    }
3729
3730    #[test]
3731    fn validation_distinguishes_negative_dimensions_from_vendor_identifiers() {
3732        let valid_negative = validate_standard_property_value_v0("margin", "-10px");
3733        assert_eq!(valid_negative.class, CssValueValidationClassV0::Valid);
3734        assert_eq!(
3735            valid_negative.reason,
3736            CssValueValidationReasonV0::GrammarMatched
3737        );
3738        assert!(valid_negative.verdict.is_matched());
3739
3740        let invalid_negative = validate_standard_property_value_v0("margin", "-10px totally-bogus");
3741        assert_eq!(invalid_negative.class, CssValueValidationClassV0::Invalid);
3742        assert_eq!(
3743            invalid_negative.reason,
3744            CssValueValidationReasonV0::GrammarUnmatched
3745        );
3746        assert!(invalid_negative.verdict.is_definite_mismatch());
3747
3748        let vendor_identifier =
3749            validate_standard_property_value_v0("box-sizing", "-webkit-border-box");
3750        assert_eq!(
3751            vendor_identifier.class,
3752            CssValueValidationClassV0::NotValidatable
3753        );
3754        assert_eq!(
3755            vendor_identifier.reason,
3756            CssValueValidationReasonV0::VendorExtension
3757        );
3758        assert!(vendor_identifier.verdict.is_definite_mismatch());
3759    }
3760
3761    #[test]
3762    fn function_tokens_preserve_validation_boundaries() {
3763        let math_function = validate_standard_property_value_v0("width", "round(up, 101px, 10px)");
3764        assert_eq!(
3765            math_function.class,
3766            CssValueValidationClassV0::NotValidatable
3767        );
3768        assert_eq!(
3769            math_function.reason,
3770            CssValueValidationReasonV0::UnvalidatedStandardFunction
3771        );
3772        assert!(math_function.verdict.is_definite_mismatch());
3773
3774        let quoted_text = validate_standard_property_value_v0("content", "\"var(\"");
3775        assert_eq!(quoted_text.class, CssValueValidationClassV0::Valid);
3776        assert_eq!(
3777            quoted_text.reason,
3778            CssValueValidationReasonV0::GrammarMatched
3779        );
3780        assert!(quoted_text.verdict.is_matched());
3781
3782        let grid_function =
3783            validate_standard_property_value_v0("grid-template-columns", "minmax(101px, 1fr)");
3784        assert_eq!(grid_function.class, CssValueValidationClassV0::Valid);
3785        assert_eq!(
3786            grid_function.reason,
3787            CssValueValidationReasonV0::GrammarMatched
3788        );
3789    }
3790
3791    #[test]
3792    fn recognized_functions_do_not_mask_adjacent_invalid_components() {
3793        for value in [
3794            "round(up, 101px, 10px)",
3795            "mod(10px, 3px)",
3796            "rem(10px, 3px)",
3797            "sin(45deg)",
3798            "pow(2, 3)",
3799            "sqrt(4)",
3800            "hypot(3px, 4px)",
3801            "abs(-10px)",
3802        ] {
3803            let validation = validate_standard_property_value_v0("width", value);
3804            assert_eq!(
3805                validation.class,
3806                CssValueValidationClassV0::NotValidatable,
3807                "{value} must remain non-definite until its function semantics are modeled"
3808            );
3809            assert_eq!(
3810                validation.reason,
3811                CssValueValidationReasonV0::UnvalidatedStandardFunction,
3812                "{value} must be attributed to the unvalidated standard-function channel"
3813            );
3814        }
3815
3816        let adjacent_scalar =
3817            validate_standard_property_value_v0("margin", "round(1, 2) totally-bogus");
3818        assert_eq!(adjacent_scalar.class, CssValueValidationClassV0::Invalid);
3819        assert_eq!(
3820            adjacent_scalar.reason,
3821            CssValueValidationReasonV0::GrammarUnmatched
3822        );
3823
3824        let unregistered_function =
3825            validate_standard_property_value_v0("width", "totally-unknown(1px)");
3826        assert_eq!(
3827            unregistered_function.class,
3828            CssValueValidationClassV0::NotValidatable
3829        );
3830        assert_eq!(
3831            unregistered_function.reason,
3832            CssValueValidationReasonV0::MatcherCoverageIncomplete
3833        );
3834
3835        let compound_value =
3836            validate_standard_property_value_v0("margin", "round(up, 10px, 1px) auto");
3837        assert_eq!(
3838            compound_value.class,
3839            CssValueValidationClassV0::NotValidatable
3840        );
3841        assert_eq!(
3842            compound_value.reason,
3843            CssValueValidationReasonV0::UnvalidatedStandardFunction
3844        );
3845    }
3846
3847    #[test]
3848    fn deferred_validation_uses_parsed_function_names() {
3849        for value in [
3850            "var(--width)",
3851            "env(safe-area-inset-top)",
3852            "attr(data-width type(<length>))",
3853        ] {
3854            let validation = validate_standard_property_value_v0("width", value);
3855            assert_eq!(
3856                validation.class,
3857                CssValueValidationClassV0::NotValidatable,
3858                "{value} must remain deferred"
3859            );
3860            assert_eq!(
3861                validation.reason,
3862                CssValueValidationReasonV0::DeferredSubstitution,
3863                "{value} must be attributed to an actual deferred function component"
3864            );
3865        }
3866
3867        let unvalidated_outer_function =
3868            validate_standard_property_value_v0("width", "round(up, calc(101px), 10px)");
3869        assert_eq!(
3870            unvalidated_outer_function.class,
3871            CssValueValidationClassV0::NotValidatable
3872        );
3873        assert_eq!(
3874            unvalidated_outer_function.reason,
3875            CssValueValidationReasonV0::UnvalidatedStandardFunction
3876        );
3877
3878        let similarly_named =
3879            validate_standard_property_value_v0("grid-template-columns", "minmax(101px, 1fr)");
3880        assert_eq!(
3881            similarly_named.reason,
3882            CssValueValidationReasonV0::GrammarMatched
3883        );
3884    }
3885
3886    #[test]
3887    fn pinned_matcher_resolves_paint_math_and_grid_function_shapes() {
3888        for (property, value) in [
3889            ("fill", "#ff00aa"),
3890            ("stroke", "rgb(10 20 30)"),
3891            ("fill", "context-fill"),
3892            ("fill", "context-stroke"),
3893            ("stroke", "context-fill"),
3894            ("stroke", "context-stroke"),
3895            ("width", "calc(1px + 2px)"),
3896            ("width", "calc(100% - 8px)"),
3897            ("width", "calc(8px - 4px)"),
3898            ("width", "min(1px, 2px)"),
3899            ("width", "max(10%, 20%)"),
3900            ("width", "clamp(1px, 2px, 3px)"),
3901            ("opacity", "calc(0.4 + 0.1)"),
3902            ("animation-duration", "max(1s, 2s)"),
3903            ("rotate", "calc(10deg + 5deg)"),
3904            ("grid-template-columns", "minmax(101px, 1fr)"),
3905            ("grid-template-columns", "repeat(3, 1fr)"),
3906            ("grid-template-columns", "repeat(2, minmax(0, 1fr))"),
3907        ] {
3908            let validation = validate_standard_property_value_v0(property, value);
3909            assert_eq!(
3910                validation.class,
3911                CssValueValidationClassV0::Valid,
3912                "{property}: {value}: {validation:?}"
3913            );
3914        }
3915
3916        for (property, value) in [
3917            ("width", "calc(1px + 2)"),
3918            ("width", "calc(1px * 2px)"),
3919            ("width", "calc(100% -8px)"),
3920            ("width", "calc(100%- 8px)"),
3921            ("opacity", "calc(1 + 1px)"),
3922            ("animation-duration", "min(1s, 2px)"),
3923        ] {
3924            let validation = validate_standard_property_value_v0(property, value);
3925            assert_ne!(
3926                validation.class,
3927                CssValueValidationClassV0::Valid,
3928                "dimensionally invalid math was accepted: {property}: {value}: {validation:?}"
3929            );
3930        }
3931    }
3932
3933    #[test]
3934    fn css_tree_keyword_closure_regressions_are_valid_across_the_validator_adapter() {
3935        let validator = SpecStandardPropertyValueValidatorV0;
3936        let regressions = [
3937            ("fill", "context-fill"),
3938            ("fill", "context-stroke"),
3939            ("stroke", "context-fill"),
3940            ("stroke", "context-stroke"),
3941            ("zoom", "normal"),
3942            ("zoom", "reset"),
3943            ("baseline-shift", "baseline"),
3944            ("-webkit-mask", "border"),
3945            ("-webkit-mask", "content"),
3946            ("-webkit-mask", "padding"),
3947            ("-webkit-mask", "text"),
3948        ];
3949        assert_eq!(regressions.len(), 11);
3950        assert_eq!(
3951            regressions
3952                .iter()
3953                .map(|(_, value)| *value)
3954                .collect::<BTreeSet<_>>()
3955                .len(),
3956            9
3957        );
3958        for (property, value) in regressions {
3959            let validation = validate_standard_property_value_v0(property, value);
3960            assert_eq!(
3961                validation.class,
3962                CssValueValidationClassV0::Valid,
3963                "{property}: {value}: {validation:?}"
3964            );
3965            assert_eq!(
3966                validator
3967                    .validate_standard_property_value(&PropertyNameV0::standard(property), value,),
3968                CascadeStandardValueVerdictV0::Matched,
3969                "{property}: {value} must cross the cascade validator adapter as matched"
3970            );
3971        }
3972    }
3973
3974    #[test]
3975    fn keyword_closure_certificate_binds_the_tested_pairs_and_nonempty_certification() {
3976        let authority = parse_closed_world_keyword_closure_certificate(
3977            CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE,
3978        );
3979        assert!(
3980            authority.is_some(),
3981            "the embedded keyword-closure certificate must pass its in-binary integrity checks"
3982        );
3983        let Some(authority) = authority else {
3984            return;
3985        };
3986        assert_eq!(authority.certified_properties.len(), 388);
3987        assert_eq!(authority.accepted_keywords_by_property.len(), 704);
3988        assert_eq!(
3989            authority
3990                .accepted_keywords_by_property
3991                .values()
3992                .map(BTreeSet::len)
3993                .sum::<usize>(),
3994            16_445
3995        );
3996        assert!(
3997            authority
3998                .accepted_keywords_by_property
3999                .get(&PropertyNameV0::canonical_standard_key("content"))
4000                .is_some_and(|keywords| keywords.contains("open-quote"))
4001        );
4002
4003        let wrong_digest = serde_json::from_str::<serde_json::Value>(
4004            CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE,
4005        );
4006        assert!(wrong_digest.is_ok(), "embedded certificate JSON must parse");
4007        let Ok(mut wrong_digest) = wrong_digest else {
4008            return;
4009        };
4010        wrong_digest["acceptedPairDigest"] = serde_json::Value::String("0".repeat(64));
4011        let wrong_digest_source = serde_json::to_string(&wrong_digest);
4012        assert!(
4013            wrong_digest_source.is_ok(),
4014            "mutated certificate JSON must serialize"
4015        );
4016        let Ok(wrong_digest_source) = wrong_digest_source else {
4017            return;
4018        };
4019        assert!(
4020            parse_closed_world_keyword_closure_certificate(&wrong_digest_source).is_none(),
4021            "the accepted-pair digest must be checked by the in-binary loader"
4022        );
4023
4024        let zero_sample = serde_json::from_str::<serde_json::Value>(
4025            CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE,
4026        );
4027        assert!(zero_sample.is_ok(), "embedded certificate JSON must parse");
4028        let Ok(mut zero_sample) = zero_sample else {
4029            return;
4030        };
4031        let zero_sample_property = zero_sample["propertyTests"].as_array().and_then(|entries| {
4032            entries.iter().find_map(|entry| {
4033                if entry["testedPairCount"].as_u64() == Some(0) {
4034                    entry["property"].as_str().map(str::to_owned)
4035                } else {
4036                    None
4037                }
4038            })
4039        });
4040        assert!(
4041            zero_sample_property.is_some(),
4042            "the exhaustive property table must include a zero-accepted property"
4043        );
4044        let Some(zero_sample_property) = zero_sample_property else {
4045            return;
4046        };
4047        let certified_properties = zero_sample["certifiedProperties"].as_array_mut();
4048        assert!(
4049            certified_properties.is_some(),
4050            "certified property list must be an array"
4051        );
4052        let Some(certified_properties) = certified_properties else {
4053            return;
4054        };
4055        certified_properties.push(serde_json::Value::String(zero_sample_property));
4056        let zero_sample_source = serde_json::to_string(&zero_sample);
4057        assert!(
4058            zero_sample_source.is_ok(),
4059            "mutated certificate JSON must serialize"
4060        );
4061        let Ok(zero_sample_source) = zero_sample_source else {
4062            return;
4063        };
4064        assert!(
4065            parse_closed_world_keyword_closure_certificate(&zero_sample_source).is_none(),
4066            "a property with zero tested pairs must never be certified"
4067        );
4068    }
4069
4070    #[test]
4071    fn closed_world_builtin_domains_are_bound_to_the_css_tree_witness_manifest() {
4072        let manifest = closed_world_builtin_token_profiles();
4073        assert!(
4074            manifest.is_some(),
4075            "the css-tree builtin token witness manifest must parse"
4076        );
4077        let Some(manifest) = manifest else {
4078            return;
4079        };
4080        assert_eq!(manifest.profile_count, 33);
4081
4082        let length = closed_world_builtin_profile("length");
4083        assert!(
4084            length.is_some(),
4085            "length must have a witnessed builtin profile"
4086        );
4087        let Some(length) = length else {
4088            return;
4089        };
4090        assert!(length.dimension.open);
4091        assert!(length.function_name.open);
4092        assert!(!length.number.open);
4093        assert_eq!(length.number.allowed, BTreeSet::from(["0".to_string()]));
4094
4095        let unknown_css_tree_type = closed_world_builtin_profile("whole-value");
4096        assert!(
4097            unknown_css_tree_type.is_some(),
4098            "an unknown css-tree type must receive a default-open profile"
4099        );
4100        let Some(unknown_css_tree_type) = unknown_css_tree_type else {
4101            return;
4102        };
4103        assert!(
4104            CLOSED_WORLD_TOKEN_KINDS
4105                .iter()
4106                .all(|kind| unknown_css_tree_type.domain(*kind).open)
4107        );
4108        assert!(closed_world_builtin_profile("named-color").is_none());
4109    }
4110
4111    #[test]
4112    fn incomplete_matcher_coverage_cannot_promote_unmatched_to_invalid() {
4113        let validation = adjudicate_css_value_validation_with_boundary(
4114            "fixture-value",
4115            CssValueGrammarVerdictV0::Unmatched {
4116                grammar: "<partially-modeled-type>".to_string(),
4117                locus: CssValueGrammarLocusV0 { start: 0, end: 13 },
4118            },
4119            SpecGrammarBoundaryClassificationV0::InBoundary,
4120            false,
4121            false,
4122        );
4123        assert_eq!(
4124            validation.class,
4125            CssValueValidationClassV0::NotValidatable,
4126            "MatcherCoverageIncomplete must remain non-definite"
4127        );
4128        assert_eq!(
4129            validation.reason,
4130            CssValueValidationReasonV0::MatcherCoverageIncomplete
4131        );
4132    }
4133
4134    #[test]
4135    fn accepted_keyword_authority_prevents_oracle_valid_ident_rejection() {
4136        let validation = validate_standard_property_value_v0("content", "open-quote");
4137        assert_eq!(
4138            validation.class,
4139            CssValueValidationClassV0::NotValidatable,
4140            "an oracle-accepted matcher gap cannot become a definite rejection: {validation:?}"
4141        );
4142        assert_eq!(
4143            validation.reason,
4144            CssValueValidationReasonV0::MatcherCoverageIncomplete
4145        );
4146    }
4147
4148    #[test]
4149    fn identifier_rejection_authority_property_surface_is_pinned() {
4150        let registry = spec_grammar_registry();
4151        let property_count = registry
4152            .entries("properties")
4153            .iter()
4154            .filter(|entry| {
4155                let property = PropertyNameV0::standard(entry.name.as_str());
4156                let property_key = PropertyNameV0::canonical_standard_key(entry.name.as_str());
4157                closed_world_keyword_authority().is_some_and(|authority| {
4158                    authority
4159                        .accepted_keywords_by_property
4160                        .contains_key(&property_key)
4161                }) && cached_standard_property_closed_world_token_profile(&property, registry)
4162                    .is_some_and(|profile| !profile.ident.open)
4163            })
4164            .count();
4165
4166        println!("identifierRejectionAuthorityPropertyCount={property_count}");
4167        assert_eq!(property_count, 505);
4168    }
4169
4170    #[test]
4171    fn open_preprocessor_function_keeps_compound_ident_rejection_non_definite() {
4172        let validation =
4173            validate_standard_property_value_v0("box-shadow", "inset 0 0 0 2px fade(#0ea5e9, 24%)");
4174        assert_eq!(
4175            validation.class,
4176            CssValueValidationClassV0::NotValidatable,
4177            "an unevaluated preprocessor function must preserve uncertainty: {validation:?}"
4178        );
4179        assert_eq!(
4180            validation.reason,
4181            CssValueValidationReasonV0::MatcherCoverageIncomplete
4182        );
4183    }
4184
4185    #[test]
4186    fn closed_world_token_kinds_certify_impossible_standard_values() {
4187        for (property, value) in [
4188            ("color", "12px"),
4189            ("color", "definitely-not-a-color"),
4190            ("width", "red"),
4191            ("border-top", "1px nonsense red"),
4192            ("fill", "bogusvalue"),
4193            ("z-index", "banana"),
4194            ("margin", "-10px totally-bogus"),
4195        ] {
4196            let validation = validate_standard_property_value_v0(property, value);
4197            assert_eq!(
4198                validation.class,
4199                CssValueValidationClassV0::Invalid,
4200                "{property}: {value}: {validation:?}"
4201            );
4202            assert_eq!(
4203                validation.reason,
4204                CssValueValidationReasonV0::GrammarUnmatched,
4205                "{property}: {value}: {validation:?}"
4206            );
4207        }
4208    }
4209
4210    #[test]
4211    fn valid_declaration_corpus_covers_closed_ident_edges_without_definite_rejection() {
4212        let declarations = [
4213            ("color", "red"),
4214            ("color", "#ff00aa"),
4215            ("fill", "#f0f"),
4216            ("stroke", "#ff00ff"),
4217            ("width", "calc(10px + 2px)"),
4218            ("width", "min(10px, 20px)"),
4219            ("width", "max(10%, 20%)"),
4220            ("width", "clamp(1px, 2px, 3px)"),
4221            ("height", "calc(50% + 2px)"),
4222            ("margin", "calc(1rem + 2px)"),
4223            ("padding", "min(1rem, 2rem)"),
4224            ("row-gap", "clamp(1px, 2px, 3px)"),
4225            ("column-gap", "max(1%, 2%)"),
4226            ("gap", "clamp(1px, 2px, 3px)"),
4227            ("opacity", "calc(0.5 + 0.1)"),
4228            ("line-height", "min(1.2, 1.5)"),
4229            ("animation-duration", "calc(1s + 200ms)"),
4230            ("transition-duration", "max(1s, 2s)"),
4231            ("rotate", "calc(10deg + 5deg)"),
4232            ("grid-template-columns", "minmax(101px, 1fr)"),
4233            ("grid-template-columns", "repeat(3, 1fr)"),
4234            ("grid-template-columns", "repeat(2, minmax(0, 1fr))"),
4235            ("grid-template-columns", "1fr 2fr"),
4236            ("border-top", "1px solid red"),
4237            ("margin", "0 auto"),
4238            ("padding", "1px 2px"),
4239            ("display", "grid"),
4240            ("position", "absolute"),
4241            ("inset", "0"),
4242            ("top", "1px"),
4243            ("z-index", "2"),
4244            ("font-weight", "700"),
4245            ("font-size", "16px"),
4246            ("background-color", "rebeccapurple"),
4247            ("border-radius", "4px"),
4248            ("flex-grow", "1"),
4249            ("flex-shrink", "0"),
4250            ("order", "-1"),
4251            ("transform", "rotate(45deg)"),
4252            ("background-image", "linear-gradient(red, blue)"),
4253            ("color", "CanvasText"),
4254            ("fill", "context-fill"),
4255            ("stroke", "context-stroke"),
4256            ("zoom", "normal"),
4257            ("baseline-shift", "baseline"),
4258            ("-webkit-mask", "border"),
4259            ("-webkit-mask", "text"),
4260            ("content", "open-quote"),
4261        ];
4262        assert_eq!(declarations.len(), 48);
4263        let definite_rejections = declarations
4264            .iter()
4265            .filter_map(|(property, value)| {
4266                let property_name = PropertyNameV0::from_authored(*property);
4267                assert!(
4268                    !standard_property_closed_world_token_kind_mismatch(
4269                        &property_name,
4270                        value,
4271                        spec_grammar_registry(),
4272                    ),
4273                    "a valid declaration was outside the derived open/closed token profile: {property}: {value}"
4274                );
4275                let validation = validate_standard_property_value_v0(property, value);
4276                (validation.class == CssValueValidationClassV0::Invalid)
4277                    .then_some((*property, *value, validation))
4278            })
4279            .collect::<Vec<_>>();
4280        assert!(
4281            definite_rejections.is_empty(),
4282            "valid declarations were definitely rejected: {definite_rejections:?}"
4283        );
4284    }
4285
4286    #[test]
4287    fn validation_consumer_policy_table_covers_every_live_consumer() {
4288        assert_eq!(CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0.len(), 5);
4289        assert_eq!(
4290            CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0
4291                .iter()
4292                .map(|policy| policy.consumer)
4293                .collect::<Vec<_>>(),
4294            vec![
4295                "checker.registeredPropertyTypeMismatch",
4296                "checker.invalidPropertyValue",
4297                "cascade.postSubstitutionStandardProperty",
4298                "scss.nativeCssFunctionParameter",
4299                "scss.nativeCssFunctionReturn",
4300            ]
4301        );
4302        for policy in CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0 {
4303            assert_eq!(policy.matched, "accept");
4304            assert!(matches!(policy.unmatched, "diagnostic" | "reject"));
4305            assert!(matches!(
4306                policy.forward_tier_unmatched,
4307                "not-applicable" | "not-validatable"
4308            ));
4309            assert!(matches!(policy.grammar_defect, "silent" | "unknown"));
4310            assert!(matches!(policy.budget_exhausted, "silent" | "unknown"));
4311        }
4312    }
4313
4314    #[test]
4315    fn cascade_validator_adapter_preserves_spec_grammar_outcomes() {
4316        let validator = SpecStandardPropertyValueValidatorV0;
4317
4318        assert_eq!(
4319            validator.validate_standard_property_value(&PropertyNameV0::standard("color"), "red"),
4320            CascadeStandardValueVerdictV0::Matched
4321        );
4322        assert_eq!(
4323            validator.validate_standard_property_value(&PropertyNameV0::standard("color"), "12px"),
4324            CascadeStandardValueVerdictV0::Unmatched
4325        );
4326        assert_eq!(
4327            validator.validate_standard_property_value(
4328                &PropertyNameV0::standard("box-sizing"),
4329                "inline-box",
4330            ),
4331            CascadeStandardValueVerdictV0::Unmatched
4332        );
4333        assert_eq!(
4334            validator.validate_standard_property_value(
4335                &PropertyNameV0::standard("color"),
4336                "var(--tone)",
4337            ),
4338            CascadeStandardValueVerdictV0::Unknown
4339        );
4340    }
4341}