Skip to main content

sct_ecl/
parser.rs

1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-License-Identifier: BUSL-1.1
3
4//! The parser: one function per rule of `ECL.g4`, over the token stream of
5//! [`crate::lexer`], with `winnow`.
6//!
7//! Every rule keeps the grammar's alternative order, so an input that two
8//! alternatives admit is read the way the reference grammar reads it. The
9//! grammar's mandatory whitespace (`mws`) and its adjacency (a number, a
10//! cardinality) are checked from the token spans, since the lexer skips
11//! whitespace.
12
13use winnow::combinator::{
14    alt, cut_err, delimited, eof, fail, opt, peek, preceded, repeat, separated, terminated,
15};
16use winnow::error::{ErrMode, ParserError};
17use winnow::prelude::*;
18use winnow::stream::{ContainsToken, Stream, TokenSlice};
19use winnow::token::{any, one_of};
20
21use crate::ast::{
22    Acceptability, AcceptabilitySet, AltIdentifier, Attribute, AttributeSet, AttributeValue,
23    Cardinality, Comparison, ConceptFilter, ConceptReference, ConceptSet, ConstraintOperator,
24    DefinitionStatus, DescriptionFilter, DialectAlias, DialectIdValue, Equality,
25    ExpressionConstraint, FieldValue, FilterConstraint, FocusConcept, HistorySupplement,
26    MemberFilter, MemberOf, NumericValue, Refinement, RefsetFields, Sctid, SubAttributeSet,
27    SubExpressionConstraint, SubRefinement, TimeValue, TypeToken, TypedSearchTerm,
28};
29use crate::lexer::{Kind, Token};
30
31/// The token stream.
32pub type Tokens<'i> = TokenSlice<'i, Token<'i>>;
33/// A parser result.
34type PResult<T> = ModalResult<T, Failure>;
35
36/// Why a rule did not match: the token class it expected, when a rule said.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub struct Failure {
39    /// The token class expected where the parser stopped.
40    pub expected: Option<&'static str>,
41}
42
43impl<I: Stream> ParserError<I> for Failure {
44    type Inner = Self;
45
46    fn from_input(_: &I) -> Self {
47        Self::default()
48    }
49
50    fn into_inner(self) -> Result<Self::Inner, Self> {
51        Ok(self)
52    }
53}
54
55impl ContainsToken<&'_ Token<'_>> for Kind {
56    fn contains_token(&self, token: &Token<'_>) -> bool {
57        *self == token.kind
58    }
59}
60
61impl<const LEN: usize> ContainsToken<&'_ Token<'_>> for [Kind; LEN] {
62    fn contains_token(&self, token: &Token<'_>) -> bool {
63        self.contains(&token.kind)
64    }
65}
66
67/// Names what a parser expected when it backtracks; a cut error keeps the
68/// deeper rule's own expectation.
69trait Expecting<'i, O>: Parser<Tokens<'i>, O, ErrMode<Failure>> + Sized {
70    fn expecting(self, what: &'static str) -> impl Parser<Tokens<'i>, O, ErrMode<Failure>> {
71        let mut parser = self;
72        move |i: &mut Tokens<'i>| {
73            parser.parse_next(i).map_err(|error| match error {
74                ErrMode::Backtrack(_) => ErrMode::Backtrack(Failure {
75                    expected: Some(what),
76                }),
77                other => other,
78            })
79        }
80    }
81}
82
83impl<'i, O, P: Parser<Tokens<'i>, O, ErrMode<Failure>>> Expecting<'i, O> for P {}
84
85fn backtrack<T>() -> PResult<T> {
86    Err(ErrMode::Backtrack(Failure::default()))
87}
88
89/// Rewinds to `start` and refuses with `what`, so the error points at the
90/// token the rule read, not past it.
91fn refuse<'i, T>(
92    i: &mut Tokens<'i>,
93    start: &<Tokens<'i> as Stream>::Checkpoint,
94    what: &'static str,
95) -> PResult<T> {
96    i.reset(start);
97    Err(ErrMode::Cut(Failure {
98        expected: Some(what),
99    }))
100}
101
102/// One token of `kind`.
103fn kind<'i>(kind: Kind) -> impl Parser<Tokens<'i>, &'i Token<'i>, ErrMode<Failure>> {
104    one_of(kind).expecting(kind.describe())
105}
106
107/// A word, compared case-insensitively (every keyword of the grammar spells
108/// each letter as `(CAP_X | X)`).
109fn keyword<'i>(word: &'static str) -> impl Parser<Tokens<'i>, &'i Token<'i>, ErrMode<Failure>> {
110    any.verify(move |t: &Token<'i>| t.kind == Kind::Identifier && t.text.eq_ignore_ascii_case(word))
111        .expecting(word)
112}
113
114/// Whether whitespace or a comment separates two tokens (`mws`).
115fn separated_by_space(previous: &Token<'_>, next: &Token<'_>) -> bool {
116    previous.span.end < next.span.start
117}
118
119/// Whether two tokens touch (no whitespace between them).
120fn adjacent(previous: &Token<'_>, next: &Token<'_>) -> bool {
121    previous.span.end == next.span.start
122}
123
124/// A junction word that the grammar follows with mandatory whitespace
125/// (`conjunction`, `disjunction`, `exclusion`).
126fn junction_word<'i>(word: &'static str) -> impl Parser<Tokens<'i>, (), ErrMode<Failure>> {
127    move |i: &mut Tokens<'i>| {
128        let word = keyword(word).parse_next(i)?;
129        let next = cut_err(peek(any).expecting("whitespace after the junction")).parse_next(i)?;
130        if !separated_by_space(word, next) {
131            return Err(ErrMode::Cut(Failure {
132                expected: Some("whitespace after the junction"),
133            }));
134        }
135        Ok(())
136    }
137}
138
139/// `conjunction`: `AND` or `,`.
140fn conjunction(i: &mut Tokens<'_>) -> PResult<()> {
141    alt((junction_word("AND"), kind(Kind::Comma).void())).parse_next(i)
142}
143
144/// `disjunction`: `OR`.
145fn disjunction(i: &mut Tokens<'_>) -> PResult<()> {
146    junction_word("OR").parse_next(i)
147}
148
149/// `exclusion`: `MINUS`.
150fn exclusion(i: &mut Tokens<'_>) -> PResult<()> {
151    junction_word("MINUS").parse_next(i)
152}
153
154/// The text between a token's delimiters (the pipes of a term, the quotes
155/// of a string).
156fn inner<'i>(token: &Token<'i>, delimiter: char) -> PResult<&'i str> {
157    token
158        .text
159        .strip_prefix(delimiter)
160        .and_then(|t| t.strip_suffix(delimiter))
161        .map_or_else(backtrack, Ok)
162}
163
164/// `( item (mws item)* )`, at least `min` items, each separated from the
165/// previous one by whitespace.
166fn spaced_set<'i, T>(
167    mut item: impl Parser<Tokens<'i>, T, ErrMode<Failure>>,
168    min: usize,
169    what: &'static str,
170) -> impl Parser<Tokens<'i>, Vec<T>, ErrMode<Failure>> {
171    move |i: &mut Tokens<'i>| {
172        kind(Kind::LeftParen).parse_next(i)?;
173        let mut items = vec![item.parse_next(i)?];
174        loop {
175            let start = i.checkpoint();
176            let Some(next) = i.peek_token() else { break };
177            let Some(previous) = i.previous_tokens().next() else {
178                break;
179            };
180            if !separated_by_space(previous, next) {
181                break;
182            }
183            match item.parse_next(i) {
184                Ok(value) => items.push(value),
185                Err(ErrMode::Backtrack(_)) => {
186                    i.reset(&start);
187                    break;
188                }
189                Err(e) => return Err(e),
190            }
191        }
192        if items.len() < min {
193            return Err(ErrMode::Backtrack(Failure {
194                expected: Some(what),
195            }));
196        }
197        kind(Kind::RightParen).parse_next(i)?;
198        Ok(items)
199    }
200}
201
202/// One item bare, or a set of them.
203fn bare_or_set<'i, T>(
204    mut item: impl Parser<Tokens<'i>, T, ErrMode<Failure>>,
205    what: &'static str,
206) -> impl Parser<Tokens<'i>, Vec<T>, ErrMode<Failure>> {
207    move |i: &mut Tokens<'i>| {
208        if i.peek_token().is_some_and(|t| t.kind == Kind::LeftParen) {
209            return spaced_set(item.by_ref(), 1, what).parse_next(i);
210        }
211        item.parse_next(i).map(|one| vec![one])
212    }
213}
214
215/// `sctid`: 6 to 18 digits, the first not zero.
216fn sctid(i: &mut Tokens<'_>) -> PResult<Sctid> {
217    kind(Kind::Integer)
218        .verify_map(|t: &Token<'_>| {
219            let digits = t.text.len();
220            ((6..=18).contains(&digits) && !t.text.starts_with('0'))
221                .then(|| t.text.parse().ok().map(Sctid))
222                .flatten()
223        })
224        .expecting("a SNOMED CT identifier")
225        .parse_next(i)
226}
227
228/// `term`: the text between pipes, without its surrounding whitespace; one
229/// line, not empty.
230fn term(i: &mut Tokens<'_>) -> PResult<String> {
231    let start = i.checkpoint();
232    let token = kind(Kind::Term).parse_next(i)?;
233    let text = inner(token, '|')?.trim();
234    if text.is_empty() || text.contains(['\t', '\r', '\n']) {
235        return refuse(i, &start, "a term on one line");
236    }
237    Ok(text.to_owned())
238}
239
240/// `eclconceptreference`.
241fn concept_reference(i: &mut Tokens<'_>) -> PResult<ConceptReference> {
242    let id = sctid.parse_next(i)?;
243    let term = opt(term).parse_next(i)?;
244    Ok(ConceptReference { id, term })
245}
246
247/// `altidentifierschemealias`: `alpha (dash | alpha | integervalue)*`.
248fn is_scheme_alias(text: &str) -> bool {
249    let mut chars = text.chars();
250    chars.next().is_some_and(|c| c.is_ascii_alphabetic())
251        && chars.all(|c| c.is_ascii_alphanumeric() || c == '-')
252}
253
254/// `altidentifier`: `scheme#code` bare or in quotes, with an optional term.
255fn alt_identifier(i: &mut Tokens<'_>) -> PResult<AltIdentifier> {
256    let token = one_of([Kind::AltIdentifier, Kind::String]).parse_next(i)?;
257    let text = if token.kind == Kind::String {
258        inner(token, '"')?
259    } else {
260        token.text
261    };
262    let Some((scheme, code)) = text.split_once('#') else {
263        return backtrack();
264    };
265    if !is_scheme_alias(scheme) || code.is_empty() || code.contains('\\') {
266        return backtrack();
267    }
268    let term = opt(term).parse_next(i)?;
269    Ok(AltIdentifier {
270        scheme: scheme.to_owned(),
271        code: code.to_owned(),
272        term,
273    })
274}
275
276/// `eclfocusconcept`, or the parenthesized nested constraint.
277fn focus(i: &mut Tokens<'_>) -> PResult<FocusConcept> {
278    alt((
279        kind(Kind::Asterisk).value(FocusConcept::Wildcard),
280        concept_reference.map(FocusConcept::Reference),
281        alt_identifier.map(FocusConcept::AltIdentifier),
282        delimited(
283            kind(Kind::LeftParen),
284            expression_constraint,
285            kind(Kind::RightParen),
286        )
287        .map(|inner| FocusConcept::Nested(Box::new(inner))),
288    ))
289    .expecting("a concept reference, '*', an alternate identifier, or '('")
290    .parse_next(i)
291}
292
293/// `constraintoperator`.
294fn constraint_operator(i: &mut Tokens<'_>) -> PResult<ConstraintOperator> {
295    any.verify_map(|t: &Token<'_>| match t.kind {
296        Kind::LessThan => Some(ConstraintOperator::DescendantOf),
297        Kind::DescendantOrSelfOf => Some(ConstraintOperator::DescendantOrSelfOf),
298        Kind::ChildOf => Some(ConstraintOperator::ChildOf),
299        Kind::ChildOrSelfOf => Some(ConstraintOperator::ChildOrSelfOf),
300        Kind::GreaterThan => Some(ConstraintOperator::AncestorOf),
301        Kind::AncestorOrSelfOf => Some(ConstraintOperator::AncestorOrSelfOf),
302        Kind::ParentOf => Some(ConstraintOperator::ParentOf),
303        Kind::ParentOrSelfOf => Some(ConstraintOperator::ParentOrSelfOf),
304        Kind::Top => Some(ConstraintOperator::Top),
305        Kind::Bottom => Some(ConstraintOperator::Bottom),
306        _ => None,
307    })
308    .expecting("a constraint operator")
309    .parse_next(i)
310}
311
312/// `refsetfieldname`: letters only.
313fn refset_field_name(i: &mut Tokens<'_>) -> PResult<String> {
314    kind(Kind::Identifier)
315        .verify(|t: &Token<'_>| t.text.bytes().all(|b| b.is_ascii_alphabetic()))
316        .map(|t| t.text.to_owned())
317        .expecting("a reference set field name")
318        .parse_next(i)
319}
320
321/// `memberof`: `^`, optionally `[ fields ]`.
322fn member_of(i: &mut Tokens<'_>) -> PResult<MemberOf> {
323    kind(Kind::Caret).parse_next(i)?;
324    let fields = opt(delimited(
325        kind(Kind::LeftBracket),
326        cut_err(alt((
327            kind(Kind::Asterisk).value(RefsetFields::Any),
328            separated(1.., refset_field_name, kind(Kind::Comma)).map(RefsetFields::Names),
329        ))),
330        cut_err(kind(Kind::RightBracket)),
331    ))
332    .parse_next(i)?;
333    Ok(MemberOf { fields })
334}
335
336/// `subexpressionconstraint`.
337fn sub_expression_constraint(i: &mut Tokens<'_>) -> PResult<SubExpressionConstraint> {
338    let operator = opt(constraint_operator).parse_next(i)?;
339    let member_of = opt(member_of).parse_next(i)?;
340    let focus = if operator.is_some() || member_of.is_some() {
341        cut_err(focus).parse_next(i)?
342    } else {
343        focus.parse_next(i)?
344    };
345    let member_filters = repeat(0.., member_filter_constraint).parse_next(i)?;
346    let filters = repeat(
347        0..,
348        alt((concept_filter_constraint, description_filter_constraint)),
349    )
350    .parse_next(i)?;
351    let history = opt(history_supplement).parse_next(i)?;
352    Ok(SubExpressionConstraint {
353        operator,
354        member_of,
355        focus,
356        member_filters,
357        filters,
358        history,
359    })
360}
361
362/// The operands after the first of a compound constraint, joined by
363/// `junction`.
364fn operands<'i>(
365    first: SubExpressionConstraint,
366    junction: impl Parser<Tokens<'i>, (), ErrMode<Failure>>,
367    i: &mut Tokens<'i>,
368) -> PResult<Option<Vec<SubExpressionConstraint>>> {
369    let rest: Option<Vec<SubExpressionConstraint>> = opt(repeat(
370        1..,
371        preceded(junction, cut_err(sub_expression_constraint)),
372    ))
373    .parse_next(i)?;
374    Ok(rest.map(|rest| {
375        let mut all = vec![first];
376        all.extend(rest);
377        all
378    }))
379}
380
381/// `expressionconstraint`: a sub-expression, then what follows it decides
382/// the form (`:` refined, `.` dotted, a junction compound, else bare).
383fn expression_constraint(i: &mut Tokens<'_>) -> PResult<ExpressionConstraint> {
384    let first = sub_expression_constraint.parse_next(i)?;
385    if opt(kind(Kind::Colon)).parse_next(i)?.is_some() {
386        let refinement = cut_err(refinement).parse_next(i)?;
387        return Ok(ExpressionConstraint::Refined {
388            focus: first,
389            refinement: Box::new(refinement),
390        });
391    }
392    if i.peek_token().is_some_and(|t| t.kind == Kind::Period) {
393        let attributes = repeat(
394            1..,
395            preceded(kind(Kind::Period), cut_err(sub_expression_constraint)),
396        )
397        .parse_next(i)?;
398        return Ok(ExpressionConstraint::Dotted {
399            focus: first,
400            attributes,
401        });
402    }
403    if let Some(all) = operands(first.clone(), conjunction, i)? {
404        return Ok(ExpressionConstraint::Conjunction(all));
405    }
406    if let Some(all) = operands(first.clone(), disjunction, i)? {
407        return Ok(ExpressionConstraint::Disjunction(all));
408    }
409    if let Some(right) =
410        opt(preceded(exclusion, cut_err(sub_expression_constraint))).parse_next(i)?
411    {
412        return Ok(ExpressionConstraint::Exclusion { left: first, right });
413    }
414    Ok(ExpressionConstraint::Sub(first))
415}
416
417/// `nonnegativeintegervalue`: digits without a leading zero, or `0`.
418fn non_negative_integer(token: &Token<'_>) -> Option<u32> {
419    (token.text == "0" || !token.text.starts_with('0'))
420        .then(|| token.text.parse().ok())
421        .flatten()
422}
423
424/// `cardinality` between brackets: `[min..max]`, written without spaces.
425fn cardinality(i: &mut Tokens<'_>) -> PResult<Cardinality> {
426    let start = i.checkpoint();
427    let open = kind(Kind::LeftBracket).parse_next(i)?;
428    let (min, to, max, close) = cut_err(
429        (
430            kind(Kind::Integer),
431            kind(Kind::To),
432            one_of([Kind::Integer, Kind::Asterisk]),
433            kind(Kind::RightBracket),
434        )
435            .expecting("a cardinality such as [1..*]"),
436    )
437    .parse_next(i)?;
438    let tight =
439        adjacent(open, min) && adjacent(min, to) && adjacent(to, max) && adjacent(max, close);
440    let min_value = non_negative_integer(min);
441    let max_value = if max.kind == Kind::Asterisk {
442        Some(None)
443    } else {
444        non_negative_integer(max).map(Some)
445    };
446    match (tight, min_value, max_value) {
447        (true, Some(min), Some(max)) => Ok(Cardinality { min, max }),
448        _ => refuse(i, &start, "a cardinality such as [1..*]"),
449    }
450}
451
452/// `expressioncomparisonoperator` and the other `=` / `!=` operators.
453fn equality(i: &mut Tokens<'_>) -> PResult<Equality> {
454    alt((
455        kind(Kind::Equal).value(Equality::Equal),
456        kind(Kind::NotEqual).value(Equality::NotEqual),
457    ))
458    .parse_next(i)
459}
460
461/// `numericcomparisonoperator` and `timecomparisonoperator`.
462fn comparison(i: &mut Tokens<'_>) -> PResult<Comparison> {
463    alt((
464        kind(Kind::Equal).value(Comparison::Equal),
465        kind(Kind::NotEqual).value(Comparison::NotEqual),
466        kind(Kind::LessOrEqual).value(Comparison::LessOrEqual),
467        kind(Kind::LessThan).value(Comparison::Less),
468        kind(Kind::GreaterOrEqual).value(Comparison::GreaterOrEqual),
469        kind(Kind::GreaterThan).value(Comparison::Greater),
470    ))
471    .parse_next(i)
472}
473
474/// `HASH numericvalue`: `#`, an optional sign, digits, an optional fraction,
475/// all touching.
476fn numeric_value(i: &mut Tokens<'_>) -> PResult<NumericValue> {
477    let start = i.checkpoint();
478    let hash = kind(Kind::Hash).parse_next(i)?;
479    let sign = opt(one_of([Kind::Dash, Kind::Plus])).parse_next(i)?;
480    let integer = cut_err(kind(Kind::Integer)).parse_next(i)?;
481    let fraction = opt((kind(Kind::Period), kind(Kind::Integer))).parse_next(i)?;
482    let mut previous = hash;
483    let mut text = String::new();
484    for token in sign
485        .into_iter()
486        .chain(std::iter::once(integer))
487        .chain(fraction.iter().flat_map(|(dot, digits)| [*dot, *digits]))
488    {
489        if !adjacent(previous, token) {
490            return refuse(i, &start, "a number written without spaces");
491        }
492        text.push_str(token.text);
493        previous = token;
494    }
495    if integer.text != "0" && integer.text.starts_with('0') {
496        return refuse(i, &start, "a number without a leading zero");
497    }
498    Ok(NumericValue(text))
499}
500
501/// `booleanvalue`.
502fn boolean(i: &mut Tokens<'_>) -> PResult<bool> {
503    alt((keyword("true").value(true), keyword("false").value(false))).parse_next(i)
504}
505
506/// A quoted string's content.
507fn string_content<'i>(i: &mut Tokens<'i>) -> PResult<&'i str> {
508    let token = kind(Kind::String).parse_next(i)?;
509    inner(token, '"')
510}
511
512/// Whether every backslash in `text` escapes one of `allowed`.
513fn escapes_only(text: &str, allowed: &[char]) -> bool {
514    let mut chars = text.chars();
515    while let Some(c) = chars.next() {
516        if c == '\\' && !chars.next().is_some_and(|e| allowed.contains(&e)) {
517            return false;
518        }
519    }
520    true
521}
522
523/// `matchsearchtermset` content: words separated by whitespace or comments,
524/// each with `\"` and `\\` as its only escapes.
525fn match_words(text: &str) -> Option<Vec<String>> {
526    let mut words = Vec::new();
527    let mut word = String::new();
528    let mut rest = text;
529    while let Some(c) = rest.chars().next() {
530        if let Some(after) = rest.strip_prefix("/*") {
531            let end = after.find("*/")?;
532            rest = after.get(end.saturating_add(2)..)?;
533            if !word.is_empty() {
534                words.push(std::mem::take(&mut word));
535            }
536            continue;
537        }
538        rest = rest.get(c.len_utf8()..)?;
539        if matches!(c, ' ' | '\t' | '\r' | '\n') {
540            if !word.is_empty() {
541                words.push(std::mem::take(&mut word));
542            }
543        } else if c == '\\' {
544            let escaped = rest.chars().next().filter(|e| matches!(e, '"' | '\\'))?;
545            rest = rest.get(escaped.len_utf8()..)?;
546            word.push('\\');
547            word.push(escaped);
548        } else {
549            word.push(c);
550        }
551    }
552    if !word.is_empty() {
553        words.push(word);
554    }
555    (!words.is_empty()).then_some(words)
556}
557
558/// `typedsearchterm`: `wild:"pattern"`, or `[match:]"words"`.
559fn typed_search_term(i: &mut Tokens<'_>) -> PResult<TypedSearchTerm> {
560    if opt((keyword("wild"), kind(Kind::Colon)))
561        .parse_next(i)?
562        .is_some()
563    {
564        let start = i.checkpoint();
565        let pattern = cut_err(string_content).parse_next(i)?;
566        if pattern.is_empty() || !escapes_only(pattern, &['"', '\\', '*']) {
567            return refuse(i, &start, "a wildcard search term");
568        }
569        return Ok(TypedSearchTerm::Wild(pattern.to_owned()));
570    }
571    let explicit = opt((keyword("match"), kind(Kind::Colon)))
572        .parse_next(i)?
573        .is_some();
574    let start = i.checkpoint();
575    let content = if explicit {
576        cut_err(string_content).parse_next(i)?
577    } else {
578        string_content.parse_next(i)?
579    };
580    match match_words(content) {
581        Some(words) => Ok(TypedSearchTerm::Match(words)),
582        None => refuse(i, &start, "one or more search words"),
583    }
584}
585
586/// `typedsearchterm | typedsearchtermset`.
587fn typed_search_terms(i: &mut Tokens<'_>) -> PResult<Vec<TypedSearchTerm>> {
588    bare_or_set(typed_search_term, "a search term").parse_next(i)
589}
590
591/// `timevalue`: `""` or `"YYYYMMDD"`.
592fn time_value(i: &mut Tokens<'_>) -> PResult<TimeValue> {
593    let start = i.checkpoint();
594    let text = string_content.parse_next(i)?;
595    let in_range = |range: std::ops::Range<usize>, max: u8| {
596        text.get(range)
597            .and_then(|part| part.parse::<u8>().ok())
598            .is_some_and(|value| (1..=max).contains(&value))
599    };
600    let valid = text.is_empty()
601        || (text.len() == 8
602            && text.bytes().all(|b| b.is_ascii_digit())
603            && !text.starts_with('0')
604            && in_range(4..6, 12)
605            && in_range(6..8, 31));
606    if !valid {
607        return refuse(i, &start, "a time as \"YYYYMMDD\" or \"\"");
608    }
609    Ok(TimeValue(text.to_owned()))
610}
611
612/// `activevalue`: `1`, `0`, `true`, or `false`.
613fn active_value(i: &mut Tokens<'_>) -> PResult<bool> {
614    alt((
615        kind(Kind::Integer).verify_map(|t: &Token<'_>| match t.text {
616            "1" => Some(true),
617            "0" => Some(false),
618            _ => None,
619        }),
620        boolean,
621    ))
622    .expecting("1, 0, true, or false")
623    .parse_next(i)
624}
625
626/// `eclconceptreferenceset` (two or more) or a sub-expression constraint.
627///
628/// The grammar lists the constraint first; a set of two or more references
629/// never parses as one, so trying the set first reads every valid input the
630/// same way and keeps the constraint's errors for the invalid ones.
631fn concept_set(i: &mut Tokens<'_>) -> PResult<ConceptSet> {
632    alt((
633        spaced_set(concept_reference, 2, "two or more concept references").map(ConceptSet::Set),
634        sub_expression_constraint.map(ConceptSet::Expression),
635    ))
636    .parse_next(i)
637}
638
639/// `acceptabilityset`.
640fn acceptability_set(i: &mut Tokens<'_>) -> PResult<AcceptabilitySet> {
641    alt((
642        spaced_set(concept_reference, 1, "acceptability concepts").map(AcceptabilitySet::Concepts),
643        spaced_set(
644            alt((
645                keyword("accept").value(Acceptability::Acceptable),
646                keyword("prefer").value(Acceptability::Preferred),
647            )),
648            1,
649            "accept or prefer",
650        )
651        .map(AcceptabilitySet::Tokens),
652    ))
653    .parse_next(i)
654}
655
656/// `dialectidset`, kept only when it cannot also be read as a nested
657/// constraint (the grammar's first alternative for `( reference )`).
658fn dialect_id_set(
659    i: &mut Tokens<'_>,
660) -> PResult<Vec<(ConceptReference, Option<AcceptabilitySet>)>> {
661    spaced_set(
662        (concept_reference, opt(acceptability_set)),
663        1,
664        "dialect reference sets",
665    )
666    .verify(
667        |items: &Vec<(ConceptReference, Option<AcceptabilitySet>)>| {
668            items.len() >= 2 || items.iter().any(|(_, a)| a.is_some())
669        },
670    )
671    .parse_next(i)
672}
673
674/// `dialectalias`: a letter, then letters, digits, and dashes.
675fn dialect_alias(i: &mut Tokens<'_>) -> PResult<String> {
676    kind(Kind::Identifier)
677        .verify(|t: &Token<'_>| is_scheme_alias(t.text))
678        .map(|t| t.text.to_owned())
679        .expecting("a dialect alias such as en-gb")
680        .parse_next(i)
681}
682
683/// `languagecode`: two letters.
684fn language_code(i: &mut Tokens<'_>) -> PResult<String> {
685    kind(Kind::Identifier)
686        .verify(|t: &Token<'_>| {
687            t.text.len() == 2 && t.text.bytes().all(|b| b.is_ascii_alphabetic())
688        })
689        .map(|t| t.text.to_owned())
690        .expecting("a two-letter language code")
691        .parse_next(i)
692}
693
694/// `typetoken`.
695fn type_token(i: &mut Tokens<'_>) -> PResult<TypeToken> {
696    alt((
697        keyword("syn").value(TypeToken::Synonym),
698        keyword("fsn").value(TypeToken::FullySpecifiedName),
699        keyword("def").value(TypeToken::Definition),
700    ))
701    .parse_next(i)
702}
703
704/// `definitionstatustoken`.
705fn definition_status(i: &mut Tokens<'_>) -> PResult<DefinitionStatus> {
706    alt((
707        keyword("primitive").value(DefinitionStatus::Primitive),
708        keyword("defined").value(DefinitionStatus::Defined),
709    ))
710    .parse_next(i)
711}
712
713/// `dialectfilter`.
714fn dialect_filter(i: &mut Tokens<'_>) -> PResult<DescriptionFilter> {
715    if opt(keyword("dialectId")).parse_next(i)?.is_some() {
716        let (operator, value, acceptability) = cut_err((
717            equality,
718            alt((
719                dialect_id_set.map(DialectIdValue::Set),
720                sub_expression_constraint.map(DialectIdValue::Expression),
721            )),
722            opt(acceptability_set),
723        ))
724        .parse_next(i)?;
725        return Ok(DescriptionFilter::DialectId {
726            operator,
727            value,
728            acceptability,
729        });
730    }
731    keyword("dialect").parse_next(i)?;
732    let (operator, aliases, acceptability) = cut_err((
733        equality,
734        alt((
735            spaced_set(
736                (dialect_alias, opt(acceptability_set)).map(|(alias, acceptability)| {
737                    DialectAlias {
738                        alias,
739                        acceptability,
740                    }
741                }),
742                1,
743                "dialect aliases",
744            ),
745            dialect_alias.map(|alias| {
746                vec![DialectAlias {
747                    alias,
748                    acceptability: None,
749                }]
750            }),
751        )),
752        opt(acceptability_set),
753    ))
754    .parse_next(i)?;
755    Ok(DescriptionFilter::Dialect {
756        operator,
757        aliases,
758        acceptability,
759    })
760}
761
762/// `descriptionfilter`.
763fn description_filter(i: &mut Tokens<'_>) -> PResult<DescriptionFilter> {
764    alt((
765        preceded(keyword("term"), cut_err((equality, typed_search_terms)))
766            .map(|(operator, terms)| DescriptionFilter::Term { operator, terms }),
767        preceded(
768            keyword("language"),
769            cut_err((equality, bare_or_set(language_code, "language codes"))),
770        )
771        .map(|(operator, codes)| DescriptionFilter::Language { operator, codes }),
772        preceded(keyword("typeId"), cut_err((equality, concept_set)))
773            .map(|(operator, value)| DescriptionFilter::TypeId { operator, value }),
774        preceded(
775            keyword("type"),
776            cut_err((equality, bare_or_set(type_token, "type tokens"))),
777        )
778        .map(|(operator, tokens)| DescriptionFilter::Type { operator, tokens }),
779        dialect_filter,
780        preceded(keyword("moduleId"), cut_err((equality, concept_set)))
781            .map(|(operator, value)| DescriptionFilter::Module { operator, value }),
782        preceded(
783            keyword("effectiveTime"),
784            cut_err((comparison, bare_or_set(time_value, "times"))),
785        )
786        .map(|(operator, values)| DescriptionFilter::EffectiveTime { operator, values }),
787        preceded(keyword("active"), cut_err((equality, active_value)))
788            .map(|(operator, value)| DescriptionFilter::Active { operator, value }),
789        preceded(
790            keyword("id"),
791            cut_err((equality, bare_or_set(sctid, "description identifiers"))),
792        )
793        .map(|(operator, ids)| DescriptionFilter::Id { operator, ids }),
794    ))
795    .expecting("a description filter")
796    .parse_next(i)
797}
798
799/// `conceptfilter`.
800fn concept_filter(i: &mut Tokens<'_>) -> PResult<ConceptFilter> {
801    alt((
802        preceded(
803            keyword("definitionStatusId"),
804            cut_err((equality, concept_set)),
805        )
806        .map(|(operator, value)| ConceptFilter::DefinitionStatusId { operator, value }),
807        preceded(
808            keyword("definitionStatus"),
809            cut_err((
810                equality,
811                bare_or_set(definition_status, "definition status tokens"),
812            )),
813        )
814        .map(|(operator, tokens)| ConceptFilter::DefinitionStatus { operator, tokens }),
815        preceded(keyword("moduleId"), cut_err((equality, concept_set)))
816            .map(|(operator, value)| ConceptFilter::Module { operator, value }),
817        preceded(
818            keyword("effectiveTime"),
819            cut_err((comparison, bare_or_set(time_value, "times"))),
820        )
821        .map(|(operator, values)| ConceptFilter::EffectiveTime { operator, values }),
822        preceded(keyword("active"), cut_err((equality, active_value)))
823            .map(|(operator, value)| ConceptFilter::Active { operator, value }),
824    ))
825    .expecting("a concept filter")
826    .parse_next(i)
827}
828
829/// The value part of `memberfieldfilter`, in the grammar's alternative order.
830fn field_value(i: &mut Tokens<'_>) -> PResult<FieldValue> {
831    alt((
832        (equality, sub_expression_constraint)
833            .map(|(operator, value)| FieldValue::Expression { operator, value }),
834        (comparison, numeric_value)
835            .map(|(operator, value)| FieldValue::Numeric { operator, value }),
836        (equality, typed_search_terms)
837            .map(|(operator, terms)| FieldValue::String { operator, terms }),
838        (equality, boolean).map(|(operator, value)| FieldValue::Boolean { operator, value }),
839        (comparison, bare_or_set(time_value, "times"))
840            .map(|(operator, values)| FieldValue::Time { operator, values }),
841        preceded(
842            alt((equality.void(), comparison.void())),
843            cut_err(fail.expecting("a member field value")),
844        ),
845    ))
846    .expecting("a comparison operator")
847    .parse_next(i)
848}
849
850/// `memberfilter`.
851fn member_filter(i: &mut Tokens<'_>) -> PResult<MemberFilter> {
852    alt((
853        preceded(keyword("moduleId"), cut_err((equality, concept_set)))
854            .map(|(operator, value)| MemberFilter::Module { operator, value }),
855        preceded(
856            keyword("effectiveTime"),
857            cut_err((comparison, bare_or_set(time_value, "times"))),
858        )
859        .map(|(operator, values)| MemberFilter::EffectiveTime { operator, values }),
860        preceded(keyword("active"), cut_err((equality, active_value)))
861            .map(|(operator, value)| MemberFilter::Active { operator, value }),
862        (refset_field_name, cut_err(field_value))
863            .map(|(name, value)| MemberFilter::Field { name, value }),
864    ))
865    .expecting("a member filter")
866    .parse_next(i)
867}
868
869/// `{{ M filter, filter }}`.
870fn member_filter_constraint(i: &mut Tokens<'_>) -> PResult<Vec<MemberFilter>> {
871    preceded(
872        (kind(Kind::DoubleLeftBrace), keyword("M")),
873        cut_err(terminated(
874            separated(1.., member_filter, kind(Kind::Comma)),
875            kind(Kind::DoubleRightBrace),
876        )),
877    )
878    .parse_next(i)
879}
880
881/// `{{ C filter, filter }}`.
882fn concept_filter_constraint(i: &mut Tokens<'_>) -> PResult<FilterConstraint> {
883    preceded(
884        (kind(Kind::DoubleLeftBrace), keyword("C")),
885        cut_err(terminated(
886            separated(1.., concept_filter, kind(Kind::Comma)),
887            kind(Kind::DoubleRightBrace),
888        )),
889    )
890    .map(FilterConstraint::Concept)
891    .parse_next(i)
892}
893
894/// `{{ [D] filter, filter }}`; a `{{` that opens a member filter, a concept
895/// filter, or a history supplement is left to those rules.
896fn description_filter_constraint(i: &mut Tokens<'_>) -> PResult<FilterConstraint> {
897    kind(Kind::DoubleLeftBrace).parse_next(i)?;
898    let starts_other = i.peek_token().is_some_and(|t| {
899        t.kind == Kind::Plus
900            || (t.kind == Kind::Identifier
901                && (t.text.eq_ignore_ascii_case("M") || t.text.eq_ignore_ascii_case("C")))
902    });
903    if starts_other {
904        return backtrack();
905    }
906    opt(keyword("D")).parse_next(i)?;
907    cut_err(terminated(
908        separated(1.., description_filter, kind(Kind::Comma)),
909        kind(Kind::DoubleRightBrace),
910    ))
911    .map(FilterConstraint::Description)
912    .parse_next(i)
913}
914
915/// `historysupplement`.
916fn history_supplement(i: &mut Tokens<'_>) -> PResult<HistorySupplement> {
917    (kind(Kind::DoubleLeftBrace), kind(Kind::Plus)).parse_next(i)?;
918    let start = i.checkpoint();
919    let word = cut_err(kind(Kind::Identifier).expecting("HISTORY")).parse_next(i)?;
920    let profile = match word.text.to_ascii_uppercase().as_str() {
921        "HISTORY" => None,
922        "HISTORY-MIN" | "HISTORY_MIN" => Some(HistorySupplement::Minimum),
923        "HISTORY-MOD" | "HISTORY_MOD" => Some(HistorySupplement::Moderate),
924        "HISTORY-MAX" | "HISTORY_MAX" => Some(HistorySupplement::Maximum),
925        _ => {
926            return refuse(
927                i,
928                &start,
929                "HISTORY, HISTORY-MIN, HISTORY-MOD, or HISTORY-MAX",
930            );
931        }
932    };
933    let supplement = match profile {
934        Some(profile) => profile,
935        None => match opt(delimited(
936            kind(Kind::LeftParen),
937            cut_err(expression_constraint),
938            cut_err(kind(Kind::RightParen)),
939        ))
940        .parse_next(i)?
941        {
942            Some(subset) => HistorySupplement::Subset(Box::new(subset)),
943            None => HistorySupplement::Default,
944        },
945    };
946    cut_err(kind(Kind::DoubleRightBrace)).parse_next(i)?;
947    Ok(supplement)
948}
949
950/// `eclattribute`. The cardinality is not committed to: a group starts the
951/// same way.
952fn attribute(i: &mut Tokens<'_>) -> PResult<Attribute> {
953    let cardinality = opt(cardinality).parse_next(i)?;
954    let reverse = opt(keyword("R")).parse_next(i)?.is_some();
955    let name = if reverse {
956        cut_err(sub_expression_constraint).parse_next(i)?
957    } else {
958        sub_expression_constraint.parse_next(i)?
959    };
960    let value = cut_err(attribute_value).parse_next(i)?;
961    Ok(Attribute {
962        cardinality,
963        reverse,
964        name,
965        value,
966    })
967}
968
969/// The comparison and value of `eclattribute`, in the grammar's order.
970fn attribute_value(i: &mut Tokens<'_>) -> PResult<AttributeValue> {
971    alt((
972        (equality, sub_expression_constraint)
973            .map(|(operator, value)| AttributeValue::Expression { operator, value }),
974        (comparison, numeric_value)
975            .map(|(operator, value)| AttributeValue::Numeric { operator, value }),
976        (equality, typed_search_terms)
977            .map(|(operator, terms)| AttributeValue::String { operator, terms }),
978        (equality, boolean).map(|(operator, value)| AttributeValue::Boolean { operator, value }),
979        preceded(
980            alt((equality.void(), comparison.void())),
981            cut_err(fail.expecting("an attribute value")),
982        ),
983    ))
984    .expecting("a comparison operator")
985    .parse_next(i)
986}
987
988/// `subattributeset`.
989fn sub_attribute_set(i: &mut Tokens<'_>) -> PResult<SubAttributeSet> {
990    alt((
991        attribute.map(|attribute| SubAttributeSet::Attribute(Box::new(attribute))),
992        delimited(kind(Kind::LeftParen), attribute_set, kind(Kind::RightParen))
993            .map(|set| SubAttributeSet::Nested(Box::new(set))),
994    ))
995    .parse_next(i)
996}
997
998/// The items after the first of a junction-joined set.
999fn joined<'i, T: Clone>(
1000    first: &T,
1001    junction: impl Parser<Tokens<'i>, (), ErrMode<Failure>>,
1002    item: impl Parser<Tokens<'i>, T, ErrMode<Failure>>,
1003    i: &mut Tokens<'i>,
1004) -> PResult<Option<Vec<T>>> {
1005    let rest: Option<Vec<T>> = opt(repeat(1.., preceded(junction, item))).parse_next(i)?;
1006    Ok(rest.map(|rest| {
1007        let mut all = vec![first.clone()];
1008        all.extend(rest);
1009        all
1010    }))
1011}
1012
1013/// `eclattributeset`.
1014fn attribute_set(i: &mut Tokens<'_>) -> PResult<AttributeSet> {
1015    let first = sub_attribute_set.parse_next(i)?;
1016    if let Some(all) = joined(&first, conjunction, sub_attribute_set, i)? {
1017        return Ok(AttributeSet::Conjunction(all));
1018    }
1019    if let Some(all) = joined(&first, disjunction, sub_attribute_set, i)? {
1020        return Ok(AttributeSet::Disjunction(all));
1021    }
1022    Ok(AttributeSet::Single(Box::new(first)))
1023}
1024
1025/// `eclattributegroup`.
1026fn attribute_group(i: &mut Tokens<'_>) -> PResult<SubRefinement> {
1027    let cardinality = opt(cardinality).parse_next(i)?;
1028    kind(Kind::LeftBrace).parse_next(i)?;
1029    let attributes = cut_err(attribute_set).parse_next(i)?;
1030    cut_err(kind(Kind::RightBrace)).parse_next(i)?;
1031    Ok(SubRefinement::Group {
1032        cardinality,
1033        attributes,
1034    })
1035}
1036
1037/// `subrefinement`, in the grammar's order: an attribute set, a group, a
1038/// parenthesized refinement.
1039fn sub_refinement(i: &mut Tokens<'_>) -> PResult<SubRefinement> {
1040    alt((
1041        attribute_set.map(SubRefinement::AttributeSet),
1042        attribute_group,
1043        delimited(
1044            kind(Kind::LeftParen),
1045            cut_err(refinement),
1046            cut_err(kind(Kind::RightParen)),
1047        )
1048        .map(|inner| SubRefinement::Nested(Box::new(inner))),
1049    ))
1050    .expecting("an attribute, an attribute group, or '('")
1051    .parse_next(i)
1052}
1053
1054/// `eclrefinement`.
1055fn refinement(i: &mut Tokens<'_>) -> PResult<Refinement> {
1056    let first = sub_refinement.parse_next(i)?;
1057    if let Some(all) = joined(&first, conjunction, cut_err(sub_refinement), i)? {
1058        return Ok(Refinement::Conjunction(all));
1059    }
1060    if let Some(all) = joined(&first, disjunction, cut_err(sub_refinement), i)? {
1061        return Ok(Refinement::Disjunction(all));
1062    }
1063    Ok(Refinement::Single(Box::new(first)))
1064}
1065
1066/// A whole input: one expression constraint and nothing after it.
1067///
1068/// # Errors
1069///
1070/// Returns the failure at the first token the grammar does not admit, with
1071/// the token class expected there when a rule names one.
1072pub fn whole(i: &mut Tokens<'_>) -> PResult<ExpressionConstraint> {
1073    let constraint = expression_constraint
1074        .expecting("an expression constraint")
1075        .parse_next(i)?;
1076    eof.expecting("the end of the expression").parse_next(i)?;
1077    Ok(constraint)
1078}