Skip to main content

mf2_syntax/
validate.rs

1//! Validation: the six Data Model Errors (`spec/syntax.md`, `spec/errors.md`).
2//!
3//! Names and keys are compared under NFC ([`crate::norm`]); the model is not
4//! changed. [`validate`] works on any model, including one built in code
5//! (spans are then `None`); `parse_model` runs the same checks and maps each
6//! error's location to a source span.
7//!
8//! * **Duplicate Declaration** — a declaration binds a variable that is bound
9//!   by, or appears in, a previous declaration (code 104); or its own
10//!   expression uses it — for `.input`, within its function (code 105).
11//! * **Duplicate Option Name** — two options of one function or markup.
12//! * **Missing Selector Annotation** — a selector that does not, directly or
13//!   through `.local $x = {$y}` chains, reach a declaration with a function.
14//! * **Variant Key Mismatch** — a variant's key count differs from the
15//!   selector count (one error per such variant).
16//! * **Missing Fallback Variant** — no variant whose keys are all `*`
17//!   (whatever their number: `.match $x * * {{…}}` has a fallback and a key
18//!   mismatch, as the WG suite expects).
19//! * **Duplicate Variant** — a variant whose keys equal an earlier one's
20//!   (`*` equals `*`; literals under NFC; `|*|` is a literal, not `*`).
21//!
22//! Cost: each check compares pairwise, without allocating, while it has at
23//! most [`SMALL`] items (every real message); beyond that it sorts or uses an
24//! ordered set — O(n log n) — so no input can make validation quadratic.
25
26use alloc::borrow::Cow;
27use alloc::collections::{BTreeMap, BTreeSet};
28use alloc::vec::Vec;
29
30use mf2_model::{
31    Declaration, Diagnostic, Diagnostics, ErrorKind, Expression, Key, Message, OptionValue,
32    Options, Pattern, PatternPart, Variant,
33};
34
35use crate::code;
36use crate::norm::{nfc, nfc_eq};
37
38/// Up to this many items, checks compare pairwise and allocate nothing.
39pub(crate) const SMALL: usize = 16;
40
41/// Where a data-model error is, in terms of the model.
42#[derive(Clone, Copy, PartialEq, Eq, Debug)]
43pub(crate) enum Loc {
44    /// The variable bound by declaration `i`.
45    Declaration(usize),
46    /// Selector `i`.
47    Selector(usize),
48    /// The keys of variant `i`.
49    Variant(usize),
50    /// The `.match` keyword.
51    Matcher,
52    /// Option `index` of the function or markup of an expression.
53    Option { expr: ExprLoc, index: usize },
54}
55
56/// Which expression or markup.
57#[derive(Clone, Copy, PartialEq, Eq, Debug)]
58pub(crate) enum ExprLoc {
59    /// The value of declaration `i`.
60    Declaration(usize),
61    /// Part `part` of the message's pattern (`variant: None`) or of a
62    /// variant's pattern.
63    Part { variant: Option<usize>, part: usize },
64}
65
66/// Checks a data model for the six Data Model Errors. Spans are `None`.
67pub fn validate(message: &Message<'_>) -> Diagnostics {
68    let mut out = Diagnostics::new();
69    check(message, &mut |kind, code, _| {
70        out.push(Diagnostic::new(kind, code, None));
71    });
72    out
73}
74
75/// Runs every check, reporting `(kind, code, location)` roughly in source
76/// order.
77pub(crate) fn check(message: &Message<'_>, report: &mut impl FnMut(ErrorKind, u16, Loc)) {
78    let declarations = message.declarations();
79    let duplicates = duplicate_declarations(declarations);
80    for (i, d) in declarations.iter().enumerate() {
81        let duplicate = if let Some(set) = &duplicates {
82            set.binary_search(&i).is_ok()
83        } else {
84            earlier_declarations_use(declarations, i, d.name())
85        };
86        if duplicate {
87            report(
88                ErrorKind::DuplicateDeclaration,
89                code::DUPLICATE_DECLARATION,
90                Loc::Declaration(i),
91            );
92        } else if uses_itself(d) {
93            report(
94                ErrorKind::DuplicateDeclaration,
95                code::SELF_REFERENCING_DECLARATION,
96                Loc::Declaration(i),
97            );
98        }
99        let function = match d {
100            Declaration::Input(x) => x.value.function.as_ref(),
101            Declaration::Local(x) => x.value.function(),
102            _ => None,
103        };
104        if let Some(f) = function {
105            check_options(&f.options, ExprLoc::Declaration(i), report);
106        }
107    }
108    match message {
109        Message::Pattern(p) => check_pattern(&p.pattern, None, report),
110        Message::Select(s) => {
111            let annotated = Annotations::new(declarations);
112            for (i, selector) in s.selectors.iter().enumerate() {
113                if !annotated.is_annotated(declarations, &selector.name) {
114                    report(
115                        ErrorKind::MissingSelectorAnnotation,
116                        code::MISSING_SELECTOR_ANNOTATION,
117                        Loc::Selector(i),
118                    );
119                }
120            }
121            let has_fallback = s
122                .variants
123                .iter()
124                .any(|v| v.keys.iter().all(|k| matches!(k, Key::CatchAll(_))));
125            if !has_fallback {
126                report(
127                    ErrorKind::MissingFallbackVariant,
128                    code::MISSING_FALLBACK_VARIANT,
129                    Loc::Matcher,
130                );
131            }
132            let duplicates = duplicate_variants(&s.variants);
133            for (i, v) in s.variants.iter().enumerate() {
134                if v.keys.len() != s.selectors.len() {
135                    report(
136                        ErrorKind::VariantKeyMismatch,
137                        code::VARIANT_KEY_MISMATCH,
138                        Loc::Variant(i),
139                    );
140                }
141                let duplicate = if let Some(set) = &duplicates {
142                    set.binary_search(&i).is_ok()
143                } else {
144                    let earlier = s.variants.get(..i).unwrap_or_default();
145                    earlier.iter().any(|w| keys_equal(&w.keys, &v.keys))
146                };
147                if duplicate {
148                    report(
149                        ErrorKind::DuplicateVariant,
150                        code::DUPLICATE_VARIANT,
151                        Loc::Variant(i),
152                    );
153                }
154                check_pattern(&v.value, Some(i), report);
155            }
156        }
157        _ => {}
158    }
159}
160
161// ── declarations ────────────────────────────────────────────────────────────
162
163/// Whether variable `name` is bound by, or appears in, a declaration before
164/// `i` (the pairwise form).
165fn earlier_declarations_use(declarations: &[Declaration<'_>], i: usize, name: &str) -> bool {
166    let earlier = declarations.get(..i).unwrap_or_default();
167    earlier
168        .iter()
169        .any(|p| nfc_eq(p.name(), name) || variables_used(p).any(|v| nfc_eq(v, name)))
170}
171
172/// For more than [`SMALL`] declarations: the sorted indices of those that
173/// bind a variable bound by, or appearing in, an earlier declaration (one
174/// pass with an ordered set of NFC names). `None` below the threshold.
175fn duplicate_declarations(declarations: &[Declaration<'_>]) -> Option<Vec<usize>> {
176    if declarations.len() <= SMALL {
177        return None;
178    }
179    let mut seen: BTreeSet<Cow<'_, str>> = BTreeSet::new();
180    let mut out = Vec::new();
181    for (i, d) in declarations.iter().enumerate() {
182        let name = nfc(d.name());
183        if seen.contains(&name) {
184            out.push(i);
185        }
186        seen.insert(name);
187        for v in variables_used(d) {
188            seen.insert(nfc(v));
189        }
190    }
191    Some(out)
192}
193
194/// The variables a declaration uses other than the one it binds: for
195/// `.input`, those in its function's options; for `.local`, its operand and
196/// its options.
197fn variables_used<'d>(d: &'d Declaration<'_>) -> impl Iterator<Item = &'d str> {
198    let (operand, function) = match d {
199        Declaration::Input(x) => (None, x.value.function.as_ref()),
200        Declaration::Local(x) => (
201            match &x.value {
202                Expression::Variable(v) => Some(&*v.arg.name),
203                _ => None,
204            },
205            x.value.function(),
206        ),
207        _ => (None, None),
208    };
209    let options = function.into_iter().flat_map(|f| {
210        f.options.iter().filter_map(|(_, v)| match v {
211            OptionValue::Variable(v) => Some(&*v.name),
212            _ => None,
213        })
214    });
215    operand.into_iter().chain(options)
216}
217
218/// Whether a declaration's own expression uses the variable it binds.
219fn uses_itself(d: &Declaration<'_>) -> bool {
220    let name = d.name();
221    variables_used(d).any(|v| nfc_eq(v, name))
222}
223
224/// Which declarations directly or indirectly reference a declaration with a
225/// function.
226enum Annotations<'d> {
227    /// Few declarations: walk the chain back for each selector.
228    Walk,
229    /// Many: computed in one forward pass. `last` maps an NFC name to the
230    /// index of the last declaration binding it.
231    Table {
232        annotated: Vec<bool>,
233        last: BTreeMap<Cow<'d, str>, usize>,
234    },
235}
236
237impl<'d> Annotations<'d> {
238    fn new(declarations: &'d [Declaration<'_>]) -> Self {
239        if declarations.len() <= SMALL {
240            return Annotations::Walk;
241        }
242        let mut annotated = Vec::with_capacity(declarations.len());
243        let mut last: BTreeMap<Cow<'d, str>, usize> = BTreeMap::new();
244        for (i, d) in declarations.iter().enumerate() {
245            let a = match d {
246                Declaration::Input(x) => x.value.function.is_some(),
247                Declaration::Local(x) => {
248                    x.value.function().is_some()
249                        || match &x.value {
250                            Expression::Variable(v) => last
251                                .get(&nfc(&v.arg.name))
252                                .is_some_and(|&j| annotated.get(j).copied().unwrap_or(false)),
253                            _ => false,
254                        }
255                }
256                _ => false,
257            };
258            annotated.push(a);
259            last.insert(nfc(d.name()), i);
260        }
261        Annotations::Table { annotated, last }
262    }
263
264    fn is_annotated(&self, declarations: &[Declaration<'_>], name: &str) -> bool {
265        match self {
266            Annotations::Walk => walk_annotation(declarations, name),
267            Annotations::Table { annotated, last } => last
268                .get(&nfc(name))
269                .is_some_and(|&j| annotated.get(j).copied().unwrap_or(false)),
270        }
271    }
272}
273
274/// The pairwise form: follow `.local $x = {$y}` back to a declaration with a
275/// function. Each step looks only at declarations before the current one, so
276/// the walk ends even in a message with duplicate declarations.
277fn walk_annotation(declarations: &[Declaration<'_>], name: &str) -> bool {
278    let mut name = name;
279    let mut upto = declarations.len();
280    loop {
281        let candidates = declarations.get(..upto).unwrap_or_default();
282        let Some(i) = candidates.iter().rposition(|d| nfc_eq(d.name(), name)) else {
283            return false;
284        };
285        match candidates.get(i) {
286            Some(Declaration::Input(d)) => return d.value.function.is_some(),
287            Some(Declaration::Local(d)) => {
288                if d.value.function().is_some() {
289                    return true;
290                }
291                match &d.value {
292                    Expression::Variable(v) => {
293                        name = &v.arg.name;
294                        upto = i;
295                    }
296                    _ => return false,
297                }
298            }
299            _ => return false,
300        }
301    }
302}
303
304// ── variants ────────────────────────────────────────────────────────────────
305
306fn keys_equal(a: &[Key<'_>], b: &[Key<'_>]) -> bool {
307    a.len() == b.len()
308        && a.iter().zip(b).all(|(x, y)| match (x, y) {
309            (Key::CatchAll(_), Key::CatchAll(_)) => true,
310            (Key::Literal(x), Key::Literal(y)) => nfc_eq(&x.value, &y.value),
311            _ => false,
312        })
313}
314
315/// For more than [`SMALL`] variants: the sorted indices of variants whose
316/// keys equal an earlier variant's (sorting the key lists, `*` as `None`,
317/// literals in NFC). `None` below the threshold.
318fn duplicate_variants(variants: &[Variant<'_>]) -> Option<Vec<usize>> {
319    if variants.len() <= SMALL {
320        return None;
321    }
322    let mut keyed: Vec<(Vec<Option<Cow<'_, str>>>, usize)> = variants
323        .iter()
324        .enumerate()
325        .map(|(i, v)| {
326            let keys = v
327                .keys
328                .iter()
329                .map(|k| match k {
330                    Key::Literal(l) => Some(nfc(&l.value)),
331                    _ => None,
332                })
333                .collect();
334            (keys, i)
335        })
336        .collect();
337    keyed.sort();
338    Some(later_duplicates(&keyed))
339}
340
341/// Given `(key, index)` pairs sorted by key then index, the indices that
342/// repeat an earlier key, in ascending order.
343fn later_duplicates<K: PartialEq>(sorted: &[(K, usize)]) -> Vec<usize> {
344    let mut out: Vec<usize> = sorted
345        .windows(2)
346        .filter(|w| w[0].0 == w[1].0)
347        .map(|w| w[1].1)
348        .collect();
349    out.sort_unstable();
350    out
351}
352
353// ── options ─────────────────────────────────────────────────────────────────
354
355fn check_pattern(
356    pattern: &Pattern<'_>,
357    variant: Option<usize>,
358    report: &mut impl FnMut(ErrorKind, u16, Loc),
359) {
360    for (part, p) in pattern.parts().iter().enumerate() {
361        let options = match p {
362            PatternPart::Expression(e) => e.function().map(|f| &f.options),
363            PatternPart::Markup(m) => Some(&m.options),
364            _ => None,
365        };
366        if let Some(options) = options {
367            check_options(options, ExprLoc::Part { variant, part }, report);
368        }
369    }
370}
371
372fn check_options(
373    options: &Options<'_>,
374    expr: ExprLoc,
375    report: &mut impl FnMut(ErrorKind, u16, Loc),
376) {
377    let mut emit = |index| {
378        report(
379            ErrorKind::DuplicateOptionName,
380            code::DUPLICATE_OPTION_NAME,
381            Loc::Option { expr, index },
382        );
383    };
384    if options.len() <= SMALL {
385        for (index, (name, _)) in options.iter().enumerate() {
386            if options.iter().take(index).any(|(p, _)| nfc_eq(p, name)) {
387                emit(index);
388            }
389        }
390    } else {
391        let mut keyed: Vec<(Cow<'_, str>, usize)> = options
392            .iter()
393            .enumerate()
394            .map(|(i, (name, _))| (nfc(name), i))
395            .collect();
396        keyed.sort();
397        for index in later_duplicates(&keyed) {
398            emit(index);
399        }
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use alloc::format;
406    use alloc::string::String;
407    use alloc::vec::Vec;
408    use core::fmt::Write as _;
409
410    use mf2_model::{Diagnostics, ErrorKind, Message};
411
412    use super::{SMALL, validate};
413
414    fn kinds(d: &Diagnostics) -> Vec<ErrorKind> {
415        d.iter().map(|x| x.kind).collect()
416    }
417
418    fn parse(src: &str) -> Message<'_> {
419        crate::parse_model(src).message.expect("parses")
420    }
421
422    /// The pairwise (≤ SMALL) and the sorting (> SMALL) forms agree: the same
423    /// errors, at the same indices, whichever side of the threshold a message
424    /// falls on.
425    #[test]
426    fn both_forms_of_every_check_agree() {
427        for n in [SMALL - 1, SMALL, SMALL + 1, 3 * SMALL] {
428            // Options: every third one repeats an earlier name (one written in
429            // NFD).
430            let mut src = String::from("{:f");
431            for i in 0..n {
432                let name = if i % 3 == 2 {
433                    format!("o{}", i - 2)
434                } else {
435                    format!("o{i}")
436                };
437                let _ = write!(src, " {name}=1");
438            }
439            src.push_str(" \u{e9}=1 e\u{301}=2}");
440            let d = validate(&parse(&src));
441            assert_eq!(d.len(), n / 3 + 1, "options, n = {n}");
442            assert!(
443                kinds(&d)
444                    .iter()
445                    .all(|k| *k == ErrorKind::DuplicateOptionName)
446            );
447
448            // Declarations: redeclarations and uses of earlier names.
449            let mut src = String::new();
450            for i in 0..n {
451                let _ = write!(src, ".local $v{i} = {{$e{i}}} ");
452            }
453            src.push_str(".local $v1 = {1} .local $e2 = {2} .local $w = {$w} {{}}");
454            let d = validate(&parse(&src));
455            assert_eq!(
456                kinds(&d),
457                [ErrorKind::DuplicateDeclaration; 3],
458                "declarations, n = {n}"
459            );
460
461            // Variants: duplicates under NFC, and `*` vs `|*|`.
462            let mut src = String::from(".input {$x :f} .match $x ");
463            for i in 0..n {
464                let _ = write!(src, "k{i} {{{{}}}} ");
465            }
466            src.push_str("k1 {{}} |*| {{}} \u{e9} {{}} e\u{301} {{}} * {{}} * {{}}");
467            let d = validate(&parse(&src));
468            assert_eq!(
469                kinds(&d),
470                [ErrorKind::DuplicateVariant; 3],
471                "variants, n = {n}"
472            );
473
474            // Selector annotation through a long chain of locals.
475            let mut src = String::from(".input {$a0 :f} ");
476            for i in 1..n {
477                let _ = write!(src, ".local $a{i} = {{$a{}}} ", i - 1);
478            }
479            let _ = write!(src, ".local $z = {{1}} .match $a{} $z * * {{{{}}}}", n - 1);
480            let d = validate(&parse(&src));
481            assert_eq!(
482                kinds(&d),
483                [ErrorKind::MissingSelectorAnnotation],
484                "selectors, n = {n}"
485            );
486        }
487    }
488}