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,
15};
16use winnow::error::ErrMode;
17use winnow::prelude::*;
18use winnow::stream::Stream;
19use winnow::token::any;
20
21use crate::ast::{
22    AltIdentifier, Attribute, AttributeSet, AttributeValue, Cardinality, Comparison,
23    ConceptReference, ConcreteString, ConstraintOperator, Equality, ExpressionConstraint,
24    FocusConcept, HistorySupplement, MemberOf, NumericValue, Refinement, RefsetFields,
25    RefsetOperator, Sctid, SubAttributeSet, SubExpressionConstraint, SubRefinement,
26    TypedSearchTerm,
27};
28use crate::lexer::Kind;
29use sct_scg::front::{
30    Expecting, Failure, PResult, Token, Tokens, backtrack, kind, one_of_kinds, refuse,
31};
32
33mod filter;
34
35use self::filter::{
36    concept_filter_constraint, description_filter_constraint, member_filter_constraint,
37};
38
39/// A word, compared case-insensitively (every keyword of the grammar spells
40/// each letter as `(CAP_X | X)`).
41fn keyword<'i>(
42    word: &'static str,
43) -> impl Parser<Tokens<'i, Kind>, &'i Token<'i, Kind>, ErrMode<Failure>> {
44    any.verify(move |t: &Token<'i, Kind>| {
45        t.kind == Kind::Identifier && t.text.eq_ignore_ascii_case(word)
46    })
47    .expecting(word)
48}
49
50/// Whether whitespace or a comment separates two tokens (`mws`).
51fn separated_by_space(previous: &Token<'_, Kind>, next: &Token<'_, Kind>) -> bool {
52    previous.span.end < next.span.start
53}
54
55/// Whether two tokens touch (no whitespace between them).
56fn adjacent(previous: &Token<'_, Kind>, next: &Token<'_, Kind>) -> bool {
57    previous.span.end == next.span.start
58}
59
60/// A junction word that the grammar follows with mandatory whitespace
61/// (`conjunction`, `disjunction`, `exclusion`).
62fn junction_word<'i>(word: &'static str) -> impl Parser<Tokens<'i, Kind>, (), ErrMode<Failure>> {
63    move |i: &mut Tokens<'i, Kind>| {
64        let word = keyword(word).parse_next(i)?;
65        let next = cut_err(peek(any).expecting("whitespace after the junction")).parse_next(i)?;
66        if !separated_by_space(word, next) {
67            return Err(ErrMode::Cut(Failure {
68                expected: Some("whitespace after the junction"),
69            }));
70        }
71        Ok(())
72    }
73}
74
75/// `conjunction`: `AND` or `,`.
76fn conjunction(i: &mut Tokens<'_, Kind>) -> PResult<()> {
77    alt((junction_word("AND"), kind(Kind::Comma).void())).parse_next(i)
78}
79
80/// `disjunction`: `OR`.
81fn disjunction(i: &mut Tokens<'_, Kind>) -> PResult<()> {
82    junction_word("OR").parse_next(i)
83}
84
85/// `exclusion`: `MINUS`.
86fn exclusion(i: &mut Tokens<'_, Kind>) -> PResult<()> {
87    junction_word("MINUS").parse_next(i)
88}
89
90/// The text between a token's delimiters (the pipes of a term, the quotes
91/// of a string).
92fn inner<'i>(token: &Token<'i, Kind>, delimiter: char) -> PResult<&'i str> {
93    token
94        .text
95        .strip_prefix(delimiter)
96        .and_then(|t| t.strip_suffix(delimiter))
97        .map_or_else(backtrack, Ok)
98}
99
100/// `( item (mws item)* )`, at least `min` items, each separated from the
101/// previous one by whitespace.
102fn spaced_set<'i, T>(
103    mut item: impl Parser<Tokens<'i, Kind>, T, ErrMode<Failure>>,
104    min: usize,
105    what: &'static str,
106) -> impl Parser<Tokens<'i, Kind>, Vec<T>, ErrMode<Failure>> {
107    move |i: &mut Tokens<'i, Kind>| {
108        kind(Kind::LeftParen).parse_next(i)?;
109        let mut items = vec![item.parse_next(i)?];
110        loop {
111            let start = i.checkpoint();
112            let Some(next) = i.peek_token() else { break };
113            let Some(previous) = i.previous_tokens().next() else {
114                break;
115            };
116            if !separated_by_space(previous, next) {
117                break;
118            }
119            match item.parse_next(i) {
120                Ok(value) => items.push(value),
121                Err(ErrMode::Backtrack(_)) => {
122                    i.reset(&start);
123                    break;
124                }
125                Err(e) => return Err(e),
126            }
127        }
128        if items.len() < min {
129            return Err(ErrMode::Backtrack(Failure {
130                expected: Some(what),
131            }));
132        }
133        kind(Kind::RightParen).parse_next(i)?;
134        Ok(items)
135    }
136}
137
138/// One item bare, or a set of them.
139fn bare_or_set<'i, T>(
140    mut item: impl Parser<Tokens<'i, Kind>, T, ErrMode<Failure>>,
141    what: &'static str,
142) -> impl Parser<Tokens<'i, Kind>, Vec<T>, ErrMode<Failure>> {
143    move |i: &mut Tokens<'i, Kind>| {
144        if i.peek_token().is_some_and(|t| t.kind == Kind::LeftParen) {
145            return spaced_set(item.by_ref(), 1, what).parse_next(i);
146        }
147        item.parse_next(i).map(|one| vec![one])
148    }
149}
150
151/// `sctid`: 6 to 18 digits, the first not zero.
152fn sctid(i: &mut Tokens<'_, Kind>) -> PResult<Sctid> {
153    kind(Kind::Integer)
154        .verify_map(|t: &Token<'_, Kind>| sctid_text(t.text))
155        .expecting("a SNOMED CT identifier")
156        .parse_next(i)
157}
158
159/// `digits` as an `sctId`: 6 to 18 digits with no leading zero.
160fn sctid_text(digits: &str) -> Option<Sctid> {
161    ((6..=18).contains(&digits.len()) && !digits.starts_with('0'))
162        .then(|| digits.parse().ok().map(Sctid))
163        .flatten()
164}
165
166/// `term`: the text between pipes, without its surrounding whitespace; one
167/// line, not empty.
168fn term(i: &mut Tokens<'_, Kind>) -> PResult<String> {
169    let start = i.checkpoint();
170    let token = kind(Kind::Term).parse_next(i)?;
171    let text = inner(token, '|')?.trim();
172    if text.is_empty() || text.contains(['\t', '\r', '\n']) {
173        return refuse(i, &start, "a term on one line");
174    }
175    Ok(text.to_owned())
176}
177
178/// `eclconceptreference`.
179fn concept_reference(i: &mut Tokens<'_, Kind>) -> PResult<ConceptReference> {
180    let id = sctid.parse_next(i)?;
181    let term = opt(term).parse_next(i)?;
182    Ok(ConceptReference { id, term })
183}
184
185/// `altidentifierschemealias`: `alpha (dash | alpha | integervalue)*`.
186fn is_scheme_alias(text: &str) -> bool {
187    let mut chars = text.chars();
188    chars.next().is_some_and(|c| c.is_ascii_alphabetic())
189        && chars.all(|c| c.is_ascii_alphanumeric() || c == '-')
190}
191
192/// `altidentifier`: `scheme#code` bare or in quotes, with an optional term.
193fn alt_identifier(i: &mut Tokens<'_, Kind>) -> PResult<AltIdentifier> {
194    let token = one_of_kinds([Kind::AltIdentifier, Kind::String]).parse_next(i)?;
195    let text = if token.kind == Kind::String {
196        inner(token, '"')?
197    } else {
198        token.text
199    };
200    let Some((scheme, code)) = text.split_once('#') else {
201        return backtrack();
202    };
203    if !is_scheme_alias(scheme) || code.is_empty() || code.contains('\\') {
204        return backtrack();
205    }
206    let term = opt(term).parse_next(i)?;
207    Ok(AltIdentifier {
208        scheme: scheme.to_owned(),
209        code: code.to_owned(),
210        term,
211    })
212}
213
214/// `eclfocusconcept`, or the parenthesized nested constraint.
215fn focus(i: &mut Tokens<'_, Kind>) -> PResult<FocusConcept> {
216    alt((
217        kind(Kind::Asterisk).value(FocusConcept::Wildcard),
218        concept_reference.map(FocusConcept::Reference),
219        alt_identifier.map(FocusConcept::AltIdentifier),
220        delimited(
221            kind(Kind::LeftParen),
222            expression_constraint,
223            kind(Kind::RightParen),
224        )
225        .map(|inner| FocusConcept::Nested(Box::new(inner))),
226    ))
227    .expecting("a concept reference, '*', an alternate identifier, or '('")
228    .parse_next(i)
229}
230
231/// `constraintoperator`.
232fn constraint_operator(i: &mut Tokens<'_, Kind>) -> PResult<ConstraintOperator> {
233    any.verify_map(|t: &Token<'_, Kind>| match t.kind {
234        Kind::LessThan => Some(ConstraintOperator::DescendantOf),
235        Kind::DescendantOrSelfOf => Some(ConstraintOperator::DescendantOrSelfOf),
236        Kind::ChildOf => Some(ConstraintOperator::ChildOf),
237        Kind::ChildOrSelfOf => Some(ConstraintOperator::ChildOrSelfOf),
238        Kind::GreaterThan => Some(ConstraintOperator::AncestorOf),
239        Kind::AncestorOrSelfOf => Some(ConstraintOperator::AncestorOrSelfOf),
240        Kind::ParentOf => Some(ConstraintOperator::ParentOf),
241        Kind::ParentOrSelfOf => Some(ConstraintOperator::ParentOrSelfOf),
242        Kind::Top => Some(ConstraintOperator::Top),
243        Kind::Bottom => Some(ConstraintOperator::Bottom),
244        _ => None,
245    })
246    .expecting("a constraint operator")
247    .parse_next(i)
248}
249
250/// `refsetfieldname`: letters only.
251fn refset_field_name(i: &mut Tokens<'_, Kind>) -> PResult<String> {
252    kind(Kind::Identifier)
253        .verify(|t: &Token<'_, Kind>| t.text.bytes().all(|b| b.is_ascii_alphabetic()))
254        .map(|t| t.text.to_owned())
255        .expecting("a reference set field name")
256        .parse_next(i)
257}
258
259/// `refsetOperator`: `^R` (`refsetContainingAny`, the caret touching an
260/// upper-case `R`) or `memberof`. With no whitespace before the focus, the
261/// lexer reads `^R73211009` as `^` and one word, so the identifier after the
262/// `R` comes back as the focus.
263fn refset_operator(i: &mut Tokens<'_, Kind>) -> PResult<(RefsetOperator, Option<FocusConcept>)> {
264    let caret = kind(Kind::Caret).parse_next(i)?;
265    let start = i.checkpoint();
266    if let Some(word) = i
267        .peek_token()
268        .filter(|t| t.kind == Kind::Identifier && adjacent(caret, t))
269        && let Some(rest) = word.text.strip_prefix('R')
270        && rest.bytes().all(|b| b.is_ascii_digit())
271    {
272        any.void().parse_next(i)?;
273        if rest.is_empty() {
274            return Ok((RefsetOperator::ContainingAny, None));
275        }
276        let Some(id) = sctid_text(rest) else {
277            return refuse(i, &start, "a SNOMED CT identifier");
278        };
279        let term = opt(term).parse_next(i)?;
280        let focus = FocusConcept::Reference(ConceptReference { id, term });
281        return Ok((RefsetOperator::ContainingAny, Some(focus)));
282    }
283    member_of_fields
284        .map(|member_of| (RefsetOperator::MemberOf(member_of), None))
285        .parse_next(i)
286}
287
288/// `memberof` after its `^`: optionally `[ fields ]`.
289fn member_of_fields(i: &mut Tokens<'_, Kind>) -> PResult<MemberOf> {
290    let fields = opt(delimited(
291        kind(Kind::LeftBracket),
292        cut_err(alt((
293            kind(Kind::Asterisk).value(RefsetFields::Any),
294            separated(1.., refset_field_name, kind(Kind::Comma)).map(RefsetFields::Names),
295        ))),
296        cut_err(kind(Kind::RightBracket)),
297    ))
298    .parse_next(i)?;
299    Ok(MemberOf { fields })
300}
301
302/// `subexpressionconstraint`.
303fn sub_expression_constraint(i: &mut Tokens<'_, Kind>) -> PResult<SubExpressionConstraint> {
304    let operator = opt(constraint_operator).parse_next(i)?;
305    let (refset_operator, glued) = match opt(refset_operator).parse_next(i)? {
306        Some((refset_operator, glued)) => (Some(refset_operator), glued),
307        None => (None, None),
308    };
309    let focus = match glued {
310        Some(focus) => focus,
311        None if operator.is_some() || refset_operator.is_some() => cut_err(focus).parse_next(i)?,
312        None => focus.parse_next(i)?,
313    };
314    sub_expression_rest(i, operator, refset_operator, focus)
315}
316
317/// The filters and the history supplement after a sub-expression's focus.
318fn sub_expression_rest(
319    i: &mut Tokens<'_, Kind>,
320    operator: Option<ConstraintOperator>,
321    refset_operator: Option<RefsetOperator>,
322    focus: FocusConcept,
323) -> PResult<SubExpressionConstraint> {
324    let member_filters = repeat(0.., member_filter_constraint).parse_next(i)?;
325    let filters = repeat(
326        0..,
327        alt((concept_filter_constraint, description_filter_constraint)),
328    )
329    .parse_next(i)?;
330    let history = opt(history_supplement).parse_next(i)?;
331    Ok(SubExpressionConstraint {
332        operator,
333        refset_operator,
334        focus,
335        member_filters,
336        filters,
337        history,
338    })
339}
340
341/// The operands after the first of a compound constraint, joined by
342/// `junction`.
343fn operands<'i>(
344    first: SubExpressionConstraint,
345    junction: impl Parser<Tokens<'i, Kind>, (), ErrMode<Failure>>,
346    i: &mut Tokens<'i, Kind>,
347) -> PResult<Option<Vec<SubExpressionConstraint>>> {
348    let rest: Option<Vec<SubExpressionConstraint>> = opt(repeat(
349        1..,
350        preceded(junction, cut_err(sub_expression_constraint)),
351    ))
352    .parse_next(i)?;
353    Ok(rest.map(|rest| {
354        let mut all = vec![first];
355        all.extend(rest);
356        all
357    }))
358}
359
360/// `expressionconstraint`: a sub-expression, then what follows it decides
361/// the form (`:` refined, `.` dotted, a junction compound, else bare).
362fn expression_constraint(i: &mut Tokens<'_, Kind>) -> PResult<ExpressionConstraint> {
363    let first = sub_expression_constraint.parse_next(i)?;
364    if opt(kind(Kind::Colon)).parse_next(i)?.is_some() {
365        let refinement = cut_err(refinement).parse_next(i)?;
366        return Ok(ExpressionConstraint::Refined {
367            focus: first,
368            refinement: Box::new(refinement),
369        });
370    }
371    if i.peek_token().is_some_and(|t| t.kind == Kind::Period) {
372        let attributes = repeat(
373            1..,
374            preceded(kind(Kind::Period), cut_err(sub_expression_constraint)),
375        )
376        .parse_next(i)?;
377        return Ok(ExpressionConstraint::Dotted {
378            focus: first,
379            attributes,
380        });
381    }
382    if let Some(all) = operands(first.clone(), conjunction, i)? {
383        return Ok(ExpressionConstraint::Conjunction(all));
384    }
385    if let Some(all) = operands(first.clone(), disjunction, i)? {
386        return Ok(ExpressionConstraint::Disjunction(all));
387    }
388    if let Some(right) =
389        opt(preceded(exclusion, cut_err(sub_expression_constraint))).parse_next(i)?
390    {
391        return Ok(ExpressionConstraint::Exclusion { left: first, right });
392    }
393    Ok(ExpressionConstraint::Sub(first))
394}
395
396/// `nonnegativeintegervalue`: digits without a leading zero, or `0`.
397fn non_negative_integer(token: &Token<'_, Kind>) -> Option<u32> {
398    (token.text == "0" || !token.text.starts_with('0'))
399        .then(|| token.text.parse().ok())
400        .flatten()
401}
402
403/// `cardinality` between brackets: `[min..max]`, written without spaces.
404fn cardinality(i: &mut Tokens<'_, Kind>) -> PResult<Cardinality> {
405    let start = i.checkpoint();
406    let open = kind(Kind::LeftBracket).parse_next(i)?;
407    let (min, to, max, close) = cut_err(
408        (
409            kind(Kind::Integer),
410            kind(Kind::To),
411            one_of_kinds([Kind::Integer, Kind::Asterisk]),
412            kind(Kind::RightBracket),
413        )
414            .expecting("a cardinality such as [1..*]"),
415    )
416    .parse_next(i)?;
417    let tight =
418        adjacent(open, min) && adjacent(min, to) && adjacent(to, max) && adjacent(max, close);
419    let min_value = non_negative_integer(min);
420    let max_value = if max.kind == Kind::Asterisk {
421        Some(None)
422    } else {
423        non_negative_integer(max).map(Some)
424    };
425    match (tight, min_value, max_value) {
426        (true, Some(min), Some(max)) => Ok(Cardinality { min, max }),
427        _ => refuse(i, &start, "a cardinality such as [1..*]"),
428    }
429}
430
431/// `expressioncomparisonoperator` and the other `=` / `!=` operators.
432fn equality(i: &mut Tokens<'_, Kind>) -> PResult<Equality> {
433    alt((
434        kind(Kind::Equal).value(Equality::Equal),
435        kind(Kind::NotEqual).value(Equality::NotEqual),
436    ))
437    .parse_next(i)
438}
439
440/// `numericcomparisonoperator` and `timecomparisonoperator`.
441fn comparison(i: &mut Tokens<'_, Kind>) -> PResult<Comparison> {
442    alt((
443        kind(Kind::Equal).value(Comparison::Equal),
444        kind(Kind::NotEqual).value(Comparison::NotEqual),
445        kind(Kind::LessOrEqual).value(Comparison::LessOrEqual),
446        kind(Kind::LessThan).value(Comparison::Less),
447        kind(Kind::GreaterOrEqual).value(Comparison::GreaterOrEqual),
448        kind(Kind::GreaterThan).value(Comparison::Greater),
449    ))
450    .parse_next(i)
451}
452
453/// `HASH numericvalue`: `#`, an optional sign, digits, an optional fraction,
454/// all touching.
455fn numeric_value(i: &mut Tokens<'_, Kind>) -> PResult<NumericValue> {
456    let start = i.checkpoint();
457    let hash = kind(Kind::Hash).parse_next(i)?;
458    let sign = opt(one_of_kinds([Kind::Dash, Kind::Plus])).parse_next(i)?;
459    let integer = cut_err(kind(Kind::Integer)).parse_next(i)?;
460    let fraction = opt((kind(Kind::Period), kind(Kind::Integer))).parse_next(i)?;
461    let mut previous = hash;
462    let mut text = String::new();
463    for token in sign
464        .into_iter()
465        .chain(std::iter::once(integer))
466        .chain(fraction.iter().flat_map(|(dot, digits)| [*dot, *digits]))
467    {
468        if !adjacent(previous, token) {
469            return refuse(i, &start, "a number written without spaces");
470        }
471        text.push_str(token.text);
472        previous = token;
473    }
474    if integer.text != "0" && integer.text.starts_with('0') {
475        return refuse(i, &start, "a number without a leading zero");
476    }
477    Ok(NumericValue(text))
478}
479
480/// `booleanvalue`.
481fn boolean(i: &mut Tokens<'_, Kind>) -> PResult<bool> {
482    alt((keyword("true").value(true), keyword("false").value(false))).parse_next(i)
483}
484
485/// A quoted string's content.
486fn string_content<'i>(i: &mut Tokens<'i, Kind>) -> PResult<&'i str> {
487    let token = kind(Kind::String).parse_next(i)?;
488    inner(token, '"')
489}
490
491/// Whether every backslash in `text` escapes one of `allowed`.
492fn escapes_only(text: &str, allowed: &[char]) -> bool {
493    let mut chars = text.chars();
494    while let Some(c) = chars.next() {
495        if c == '\\' && !chars.next().is_some_and(|e| allowed.contains(&e)) {
496            return false;
497        }
498    }
499    true
500}
501
502/// `matchsearchtermset` content: words separated by whitespace or comments,
503/// each with `\"` and `\\` as its only escapes.
504fn match_words(text: &str) -> Option<Vec<String>> {
505    let mut words = Vec::new();
506    let mut word = String::new();
507    let mut rest = text;
508    while let Some(c) = rest.chars().next() {
509        if let Some(after) = rest.strip_prefix("/*") {
510            let end = after.find("*/")?;
511            rest = after.get(end.saturating_add(2)..)?;
512            if !word.is_empty() {
513                words.push(std::mem::take(&mut word));
514            }
515            continue;
516        }
517        rest = rest.get(c.len_utf8()..)?;
518        if matches!(c, ' ' | '\t' | '\r' | '\n') {
519            if !word.is_empty() {
520                words.push(std::mem::take(&mut word));
521            }
522        } else if c == '\\' {
523            let escaped = rest.chars().next().filter(|e| matches!(e, '"' | '\\'))?;
524            rest = rest.get(escaped.len_utf8()..)?;
525            word.push('\\');
526            word.push(escaped);
527        } else {
528            word.push(c);
529        }
530    }
531    if !word.is_empty() {
532        words.push(word);
533    }
534    (!words.is_empty()).then_some(words)
535}
536
537/// `typedsearchterm`: `wild:"pattern"`, or `[match:]"words"`.
538fn typed_search_term(i: &mut Tokens<'_, Kind>) -> PResult<TypedSearchTerm> {
539    if opt((keyword("wild"), kind(Kind::Colon)))
540        .parse_next(i)?
541        .is_some()
542    {
543        let start = i.checkpoint();
544        let pattern = cut_err(string_content).parse_next(i)?;
545        if pattern.is_empty() || !escapes_only(pattern, &['"', '\\', '*']) {
546            return refuse(i, &start, "a wildcard search term");
547        }
548        return Ok(TypedSearchTerm::Wild(pattern.to_owned()));
549    }
550    let explicit = opt((keyword("match"), kind(Kind::Colon)))
551        .parse_next(i)?
552        .is_some();
553    let start = i.checkpoint();
554    let content = if explicit {
555        cut_err(string_content).parse_next(i)?
556    } else {
557        string_content.parse_next(i)?
558    };
559    match match_words(content) {
560        Some(words) => Ok(TypedSearchTerm::Match(words)),
561        None => refuse(i, &start, "one or more search words"),
562    }
563}
564
565/// `concretestring`: a quoted string of one or more characters whose only
566/// escapes are `\"`, `\\`, and `\*`, decoded.
567fn concrete_string(i: &mut Tokens<'_, Kind>) -> PResult<ConcreteString> {
568    let start = i.checkpoint();
569    let content = string_content.parse_next(i)?;
570    if content.is_empty() || !escapes_only(content, &['"', '\\', '*']) {
571        return refuse(i, &start, "a concrete string");
572    }
573    let mut value = String::with_capacity(content.len());
574    let mut chars = content.chars();
575    while let Some(c) = chars.next() {
576        match c {
577            '\\' => value.extend(chars.next()),
578            other => value.push(other),
579        }
580    }
581    Ok(ConcreteString(value))
582}
583
584/// `concretestring | concretestringset`.
585fn concrete_strings(i: &mut Tokens<'_, Kind>) -> PResult<Vec<ConcreteString>> {
586    bare_or_set(concrete_string, "a concrete string").parse_next(i)
587}
588
589/// `typedsearchterm | typedsearchtermset`.
590fn typed_search_terms(i: &mut Tokens<'_, Kind>) -> PResult<Vec<TypedSearchTerm>> {
591    bare_or_set(typed_search_term, "a search term").parse_next(i)
592}
593
594/// `historysupplement`.
595fn history_supplement(i: &mut Tokens<'_, Kind>) -> PResult<HistorySupplement> {
596    (kind(Kind::DoubleLeftBrace), kind(Kind::Plus)).parse_next(i)?;
597    let start = i.checkpoint();
598    let word = cut_err(kind(Kind::Identifier).expecting("HISTORY")).parse_next(i)?;
599    let profile = match word.text.to_ascii_uppercase().as_str() {
600        "HISTORY" => None,
601        "HISTORY-MIN" | "HISTORY_MIN" => Some(HistorySupplement::Minimum),
602        "HISTORY-MOD" | "HISTORY_MOD" => Some(HistorySupplement::Moderate),
603        "HISTORY-MAX" | "HISTORY_MAX" => Some(HistorySupplement::Maximum),
604        _ => {
605            return refuse(
606                i,
607                &start,
608                "HISTORY, HISTORY-MIN, HISTORY-MOD, or HISTORY-MAX",
609            );
610        }
611    };
612    let supplement = match profile {
613        Some(profile) => profile,
614        None => match opt(delimited(
615            kind(Kind::LeftParen),
616            cut_err(expression_constraint),
617            cut_err(kind(Kind::RightParen)),
618        ))
619        .parse_next(i)?
620        {
621            Some(subset) => HistorySupplement::Subset(Box::new(subset)),
622            None => HistorySupplement::Default,
623        },
624    };
625    cut_err(kind(Kind::DoubleRightBrace)).parse_next(i)?;
626    Ok(supplement)
627}
628
629/// `eclattribute`. The cardinality is not committed to: a group starts the
630/// same way.
631fn attribute(i: &mut Tokens<'_, Kind>) -> PResult<Attribute> {
632    let cardinality = opt(cardinality).parse_next(i)?;
633    let (reverse, name) = if let Some(name) = glued_reverse(i)? {
634        (true, name)
635    } else if opt(keyword("R")).parse_next(i)?.is_some() {
636        (true, cut_err(sub_expression_constraint).parse_next(i)?)
637    } else {
638        (false, sub_expression_constraint.parse_next(i)?)
639    };
640    let value = cut_err(attribute_value).parse_next(i)?;
641    Ok(Attribute {
642        cardinality,
643        reverse,
644        name,
645        value,
646    })
647}
648
649/// `reverseflag ws eclattributename` with the empty `ws`: the lexer reads
650/// `R363698007` as one word, so the identifier after the `R` (or `r`, the
651/// rule is `CAP_R | R`) comes back as the attribute name's focus.
652fn glued_reverse(i: &mut Tokens<'_, Kind>) -> PResult<Option<SubExpressionConstraint>> {
653    let Some(id) = i.peek_token().and_then(|t| {
654        let rest = t.text.strip_prefix(['R', 'r'])?;
655        (t.kind == Kind::Identifier && rest.bytes().all(|b| b.is_ascii_digit()))
656            .then(|| sctid_text(rest))
657            .flatten()
658    }) else {
659        return Ok(None);
660    };
661    any.void().parse_next(i)?;
662    let term = opt(term).parse_next(i)?;
663    let focus = FocusConcept::Reference(ConceptReference { id, term });
664    sub_expression_rest(i, None, None, focus).map(Some)
665}
666
667/// The comparison and value of `eclattribute`, in the grammar's order.
668fn attribute_value(i: &mut Tokens<'_, Kind>) -> PResult<AttributeValue> {
669    alt((
670        (equality, sub_expression_constraint)
671            .map(|(operator, value)| AttributeValue::Expression { operator, value }),
672        (comparison, numeric_value)
673            .map(|(operator, value)| AttributeValue::Numeric { operator, value }),
674        (equality, concrete_strings)
675            .map(|(operator, values)| AttributeValue::String { operator, values }),
676        (equality, boolean).map(|(operator, value)| AttributeValue::Boolean { operator, value }),
677        preceded(
678            alt((equality.void(), comparison.void())),
679            cut_err(fail.expecting("an attribute value")),
680        ),
681    ))
682    .expecting("a comparison operator")
683    .parse_next(i)
684}
685
686/// `subattributeset`.
687fn sub_attribute_set(i: &mut Tokens<'_, Kind>) -> PResult<SubAttributeSet> {
688    alt((
689        attribute.map(|attribute| SubAttributeSet::Attribute(Box::new(attribute))),
690        delimited(kind(Kind::LeftParen), attribute_set, kind(Kind::RightParen))
691            .map(|set| SubAttributeSet::Nested(Box::new(set))),
692    ))
693    .parse_next(i)
694}
695
696/// The items after the first of a junction-joined set.
697fn joined<'i, T: Clone>(
698    first: &T,
699    junction: impl Parser<Tokens<'i, Kind>, (), ErrMode<Failure>>,
700    item: impl Parser<Tokens<'i, Kind>, T, ErrMode<Failure>>,
701    i: &mut Tokens<'i, Kind>,
702) -> PResult<Option<Vec<T>>> {
703    let rest: Option<Vec<T>> = opt(repeat(1.., preceded(junction, item))).parse_next(i)?;
704    Ok(rest.map(|rest| {
705        let mut all = vec![first.clone()];
706        all.extend(rest);
707        all
708    }))
709}
710
711/// `eclattributeset`.
712fn attribute_set(i: &mut Tokens<'_, Kind>) -> PResult<AttributeSet> {
713    let first = sub_attribute_set.parse_next(i)?;
714    if let Some(all) = joined(&first, conjunction, sub_attribute_set, i)? {
715        return Ok(AttributeSet::Conjunction(all));
716    }
717    if let Some(all) = joined(&first, disjunction, sub_attribute_set, i)? {
718        return Ok(AttributeSet::Disjunction(all));
719    }
720    Ok(AttributeSet::Single(Box::new(first)))
721}
722
723/// `eclattributegroup`.
724fn attribute_group(i: &mut Tokens<'_, Kind>) -> PResult<SubRefinement> {
725    let cardinality = opt(cardinality).parse_next(i)?;
726    kind(Kind::LeftBrace).parse_next(i)?;
727    let attributes = cut_err(attribute_set).parse_next(i)?;
728    cut_err(kind(Kind::RightBrace)).parse_next(i)?;
729    Ok(SubRefinement::Group {
730        cardinality,
731        attributes,
732    })
733}
734
735/// `subrefinement`, in the grammar's order: an attribute set, a group, a
736/// parenthesized refinement.
737fn sub_refinement(i: &mut Tokens<'_, Kind>) -> PResult<SubRefinement> {
738    alt((
739        attribute_set.map(SubRefinement::AttributeSet),
740        attribute_group,
741        delimited(
742            kind(Kind::LeftParen),
743            cut_err(refinement),
744            cut_err(kind(Kind::RightParen)),
745        )
746        .map(|inner| SubRefinement::Nested(Box::new(inner))),
747    ))
748    .expecting("an attribute, an attribute group, or '('")
749    .parse_next(i)
750}
751
752/// `eclrefinement`.
753fn refinement(i: &mut Tokens<'_, Kind>) -> PResult<Refinement> {
754    let first = sub_refinement.parse_next(i)?;
755    if let Some(all) = joined(&first, conjunction, cut_err(sub_refinement), i)? {
756        return Ok(Refinement::Conjunction(all));
757    }
758    if let Some(all) = joined(&first, disjunction, cut_err(sub_refinement), i)? {
759        return Ok(Refinement::Disjunction(all));
760    }
761    Ok(Refinement::Single(Box::new(first)))
762}
763
764/// A whole input: one expression constraint and nothing after it.
765///
766/// # Errors
767///
768/// Returns the failure at the first token the grammar does not admit, with
769/// the token class expected there when a rule names one.
770pub fn whole(i: &mut Tokens<'_, Kind>) -> PResult<ExpressionConstraint> {
771    let constraint = expression_constraint
772        .expecting("an expression constraint")
773        .parse_next(i)?;
774    eof.expecting("the end of the expression").parse_next(i)?;
775    Ok(constraint)
776}