use winnow::combinator::{
alt, cut_err, delimited, eof, fail, opt, peek, preceded, repeat, separated,
};
use winnow::error::ErrMode;
use winnow::prelude::*;
use winnow::stream::Stream;
use winnow::token::any;
use crate::ast::{
AltIdentifier, Attribute, AttributeSet, AttributeValue, Cardinality, Comparison,
ConceptReference, ConcreteString, ConstraintOperator, Equality, ExpressionConstraint,
FocusConcept, HistorySupplement, MemberOf, NumericValue, Refinement, RefsetFields,
RefsetOperator, Sctid, SubAttributeSet, SubExpressionConstraint, SubRefinement,
TypedSearchTerm,
};
use crate::lexer::Kind;
use sct_scg::front::{
Expecting, Failure, PResult, Token, Tokens, backtrack, kind, one_of_kinds, refuse,
};
mod filter;
use self::filter::{
concept_filter_constraint, description_filter_constraint, member_filter_constraint,
};
fn keyword<'i>(
word: &'static str,
) -> impl Parser<Tokens<'i, Kind>, &'i Token<'i, Kind>, ErrMode<Failure>> {
any.verify(move |t: &Token<'i, Kind>| {
t.kind == Kind::Identifier && t.text.eq_ignore_ascii_case(word)
})
.expecting(word)
}
fn separated_by_space(previous: &Token<'_, Kind>, next: &Token<'_, Kind>) -> bool {
previous.span.end < next.span.start
}
fn adjacent(previous: &Token<'_, Kind>, next: &Token<'_, Kind>) -> bool {
previous.span.end == next.span.start
}
fn junction_word<'i>(word: &'static str) -> impl Parser<Tokens<'i, Kind>, (), ErrMode<Failure>> {
move |i: &mut Tokens<'i, Kind>| {
let word = keyword(word).parse_next(i)?;
let next = cut_err(peek(any).expecting("whitespace after the junction")).parse_next(i)?;
if !separated_by_space(word, next) {
return Err(ErrMode::Cut(Failure {
expected: Some("whitespace after the junction"),
}));
}
Ok(())
}
}
fn conjunction(i: &mut Tokens<'_, Kind>) -> PResult<()> {
alt((junction_word("AND"), kind(Kind::Comma).void())).parse_next(i)
}
fn disjunction(i: &mut Tokens<'_, Kind>) -> PResult<()> {
junction_word("OR").parse_next(i)
}
fn exclusion(i: &mut Tokens<'_, Kind>) -> PResult<()> {
junction_word("MINUS").parse_next(i)
}
fn inner<'i>(token: &Token<'i, Kind>, delimiter: char) -> PResult<&'i str> {
token
.text
.strip_prefix(delimiter)
.and_then(|t| t.strip_suffix(delimiter))
.map_or_else(backtrack, Ok)
}
fn spaced_set<'i, T>(
mut item: impl Parser<Tokens<'i, Kind>, T, ErrMode<Failure>>,
min: usize,
what: &'static str,
) -> impl Parser<Tokens<'i, Kind>, Vec<T>, ErrMode<Failure>> {
move |i: &mut Tokens<'i, Kind>| {
kind(Kind::LeftParen).parse_next(i)?;
let mut items = vec![item.parse_next(i)?];
loop {
let start = i.checkpoint();
let Some(next) = i.peek_token() else { break };
let Some(previous) = i.previous_tokens().next() else {
break;
};
if !separated_by_space(previous, next) {
break;
}
match item.parse_next(i) {
Ok(value) => items.push(value),
Err(ErrMode::Backtrack(_)) => {
i.reset(&start);
break;
}
Err(e) => return Err(e),
}
}
if items.len() < min {
return Err(ErrMode::Backtrack(Failure {
expected: Some(what),
}));
}
kind(Kind::RightParen).parse_next(i)?;
Ok(items)
}
}
fn bare_or_set<'i, T>(
mut item: impl Parser<Tokens<'i, Kind>, T, ErrMode<Failure>>,
what: &'static str,
) -> impl Parser<Tokens<'i, Kind>, Vec<T>, ErrMode<Failure>> {
move |i: &mut Tokens<'i, Kind>| {
if i.peek_token().is_some_and(|t| t.kind == Kind::LeftParen) {
return spaced_set(item.by_ref(), 1, what).parse_next(i);
}
item.parse_next(i).map(|one| vec![one])
}
}
fn sctid(i: &mut Tokens<'_, Kind>) -> PResult<Sctid> {
kind(Kind::Integer)
.verify_map(|t: &Token<'_, Kind>| sctid_text(t.text))
.expecting("a SNOMED CT identifier")
.parse_next(i)
}
fn sctid_text(digits: &str) -> Option<Sctid> {
((6..=18).contains(&digits.len()) && !digits.starts_with('0'))
.then(|| digits.parse().ok().map(Sctid))
.flatten()
}
fn term(i: &mut Tokens<'_, Kind>) -> PResult<String> {
let start = i.checkpoint();
let token = kind(Kind::Term).parse_next(i)?;
let text = inner(token, '|')?.trim();
if text.is_empty() || text.contains(['\t', '\r', '\n']) {
return refuse(i, &start, "a term on one line");
}
Ok(text.to_owned())
}
fn concept_reference(i: &mut Tokens<'_, Kind>) -> PResult<ConceptReference> {
let id = sctid.parse_next(i)?;
let term = opt(term).parse_next(i)?;
Ok(ConceptReference { id, term })
}
fn is_scheme_alias(text: &str) -> bool {
let mut chars = text.chars();
chars.next().is_some_and(|c| c.is_ascii_alphabetic())
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '-')
}
fn alt_identifier(i: &mut Tokens<'_, Kind>) -> PResult<AltIdentifier> {
let token = one_of_kinds([Kind::AltIdentifier, Kind::String]).parse_next(i)?;
let text = if token.kind == Kind::String {
inner(token, '"')?
} else {
token.text
};
let Some((scheme, code)) = text.split_once('#') else {
return backtrack();
};
if !is_scheme_alias(scheme) || code.is_empty() || code.contains('\\') {
return backtrack();
}
let term = opt(term).parse_next(i)?;
Ok(AltIdentifier {
scheme: scheme.to_owned(),
code: code.to_owned(),
term,
})
}
fn focus(i: &mut Tokens<'_, Kind>) -> PResult<FocusConcept> {
alt((
kind(Kind::Asterisk).value(FocusConcept::Wildcard),
concept_reference.map(FocusConcept::Reference),
alt_identifier.map(FocusConcept::AltIdentifier),
delimited(
kind(Kind::LeftParen),
expression_constraint,
kind(Kind::RightParen),
)
.map(|inner| FocusConcept::Nested(Box::new(inner))),
))
.expecting("a concept reference, '*', an alternate identifier, or '('")
.parse_next(i)
}
fn constraint_operator(i: &mut Tokens<'_, Kind>) -> PResult<ConstraintOperator> {
any.verify_map(|t: &Token<'_, Kind>| match t.kind {
Kind::LessThan => Some(ConstraintOperator::DescendantOf),
Kind::DescendantOrSelfOf => Some(ConstraintOperator::DescendantOrSelfOf),
Kind::ChildOf => Some(ConstraintOperator::ChildOf),
Kind::ChildOrSelfOf => Some(ConstraintOperator::ChildOrSelfOf),
Kind::GreaterThan => Some(ConstraintOperator::AncestorOf),
Kind::AncestorOrSelfOf => Some(ConstraintOperator::AncestorOrSelfOf),
Kind::ParentOf => Some(ConstraintOperator::ParentOf),
Kind::ParentOrSelfOf => Some(ConstraintOperator::ParentOrSelfOf),
Kind::Top => Some(ConstraintOperator::Top),
Kind::Bottom => Some(ConstraintOperator::Bottom),
_ => None,
})
.expecting("a constraint operator")
.parse_next(i)
}
fn refset_field_name(i: &mut Tokens<'_, Kind>) -> PResult<String> {
kind(Kind::Identifier)
.verify(|t: &Token<'_, Kind>| t.text.bytes().all(|b| b.is_ascii_alphabetic()))
.map(|t| t.text.to_owned())
.expecting("a reference set field name")
.parse_next(i)
}
fn refset_operator(i: &mut Tokens<'_, Kind>) -> PResult<(RefsetOperator, Option<FocusConcept>)> {
let caret = kind(Kind::Caret).parse_next(i)?;
let start = i.checkpoint();
if let Some(word) = i
.peek_token()
.filter(|t| t.kind == Kind::Identifier && adjacent(caret, t))
&& let Some(rest) = word.text.strip_prefix('R')
&& rest.bytes().all(|b| b.is_ascii_digit())
{
any.void().parse_next(i)?;
if rest.is_empty() {
return Ok((RefsetOperator::ContainingAny, None));
}
let Some(id) = sctid_text(rest) else {
return refuse(i, &start, "a SNOMED CT identifier");
};
let term = opt(term).parse_next(i)?;
let focus = FocusConcept::Reference(ConceptReference { id, term });
return Ok((RefsetOperator::ContainingAny, Some(focus)));
}
member_of_fields
.map(|member_of| (RefsetOperator::MemberOf(member_of), None))
.parse_next(i)
}
fn member_of_fields(i: &mut Tokens<'_, Kind>) -> PResult<MemberOf> {
let fields = opt(delimited(
kind(Kind::LeftBracket),
cut_err(alt((
kind(Kind::Asterisk).value(RefsetFields::Any),
separated(1.., refset_field_name, kind(Kind::Comma)).map(RefsetFields::Names),
))),
cut_err(kind(Kind::RightBracket)),
))
.parse_next(i)?;
Ok(MemberOf { fields })
}
fn sub_expression_constraint(i: &mut Tokens<'_, Kind>) -> PResult<SubExpressionConstraint> {
let operator = opt(constraint_operator).parse_next(i)?;
let (refset_operator, glued) = match opt(refset_operator).parse_next(i)? {
Some((refset_operator, glued)) => (Some(refset_operator), glued),
None => (None, None),
};
let focus = match glued {
Some(focus) => focus,
None if operator.is_some() || refset_operator.is_some() => cut_err(focus).parse_next(i)?,
None => focus.parse_next(i)?,
};
sub_expression_rest(i, operator, refset_operator, focus)
}
fn sub_expression_rest(
i: &mut Tokens<'_, Kind>,
operator: Option<ConstraintOperator>,
refset_operator: Option<RefsetOperator>,
focus: FocusConcept,
) -> PResult<SubExpressionConstraint> {
let member_filters = repeat(0.., member_filter_constraint).parse_next(i)?;
let filters = repeat(
0..,
alt((concept_filter_constraint, description_filter_constraint)),
)
.parse_next(i)?;
let history = opt(history_supplement).parse_next(i)?;
Ok(SubExpressionConstraint {
operator,
refset_operator,
focus,
member_filters,
filters,
history,
})
}
fn operands<'i>(
first: SubExpressionConstraint,
junction: impl Parser<Tokens<'i, Kind>, (), ErrMode<Failure>>,
i: &mut Tokens<'i, Kind>,
) -> PResult<Option<Vec<SubExpressionConstraint>>> {
let rest: Option<Vec<SubExpressionConstraint>> = opt(repeat(
1..,
preceded(junction, cut_err(sub_expression_constraint)),
))
.parse_next(i)?;
Ok(rest.map(|rest| {
let mut all = vec![first];
all.extend(rest);
all
}))
}
fn expression_constraint(i: &mut Tokens<'_, Kind>) -> PResult<ExpressionConstraint> {
let first = sub_expression_constraint.parse_next(i)?;
if opt(kind(Kind::Colon)).parse_next(i)?.is_some() {
let refinement = cut_err(refinement).parse_next(i)?;
return Ok(ExpressionConstraint::Refined {
focus: first,
refinement: Box::new(refinement),
});
}
if i.peek_token().is_some_and(|t| t.kind == Kind::Period) {
let attributes = repeat(
1..,
preceded(kind(Kind::Period), cut_err(sub_expression_constraint)),
)
.parse_next(i)?;
return Ok(ExpressionConstraint::Dotted {
focus: first,
attributes,
});
}
if let Some(all) = operands(first.clone(), conjunction, i)? {
return Ok(ExpressionConstraint::Conjunction(all));
}
if let Some(all) = operands(first.clone(), disjunction, i)? {
return Ok(ExpressionConstraint::Disjunction(all));
}
if let Some(right) =
opt(preceded(exclusion, cut_err(sub_expression_constraint))).parse_next(i)?
{
return Ok(ExpressionConstraint::Exclusion { left: first, right });
}
Ok(ExpressionConstraint::Sub(first))
}
fn non_negative_integer(token: &Token<'_, Kind>) -> Option<u32> {
(token.text == "0" || !token.text.starts_with('0'))
.then(|| token.text.parse().ok())
.flatten()
}
fn cardinality(i: &mut Tokens<'_, Kind>) -> PResult<Cardinality> {
let start = i.checkpoint();
let open = kind(Kind::LeftBracket).parse_next(i)?;
let (min, to, max, close) = cut_err(
(
kind(Kind::Integer),
kind(Kind::To),
one_of_kinds([Kind::Integer, Kind::Asterisk]),
kind(Kind::RightBracket),
)
.expecting("a cardinality such as [1..*]"),
)
.parse_next(i)?;
let tight =
adjacent(open, min) && adjacent(min, to) && adjacent(to, max) && adjacent(max, close);
let min_value = non_negative_integer(min);
let max_value = if max.kind == Kind::Asterisk {
Some(None)
} else {
non_negative_integer(max).map(Some)
};
match (tight, min_value, max_value) {
(true, Some(min), Some(max)) => Ok(Cardinality { min, max }),
_ => refuse(i, &start, "a cardinality such as [1..*]"),
}
}
fn equality(i: &mut Tokens<'_, Kind>) -> PResult<Equality> {
alt((
kind(Kind::Equal).value(Equality::Equal),
kind(Kind::NotEqual).value(Equality::NotEqual),
))
.parse_next(i)
}
fn comparison(i: &mut Tokens<'_, Kind>) -> PResult<Comparison> {
alt((
kind(Kind::Equal).value(Comparison::Equal),
kind(Kind::NotEqual).value(Comparison::NotEqual),
kind(Kind::LessOrEqual).value(Comparison::LessOrEqual),
kind(Kind::LessThan).value(Comparison::Less),
kind(Kind::GreaterOrEqual).value(Comparison::GreaterOrEqual),
kind(Kind::GreaterThan).value(Comparison::Greater),
))
.parse_next(i)
}
fn numeric_value(i: &mut Tokens<'_, Kind>) -> PResult<NumericValue> {
let start = i.checkpoint();
let hash = kind(Kind::Hash).parse_next(i)?;
let sign = opt(one_of_kinds([Kind::Dash, Kind::Plus])).parse_next(i)?;
let integer = cut_err(kind(Kind::Integer)).parse_next(i)?;
let fraction = opt((kind(Kind::Period), kind(Kind::Integer))).parse_next(i)?;
let mut previous = hash;
let mut text = String::new();
for token in sign
.into_iter()
.chain(std::iter::once(integer))
.chain(fraction.iter().flat_map(|(dot, digits)| [*dot, *digits]))
{
if !adjacent(previous, token) {
return refuse(i, &start, "a number written without spaces");
}
text.push_str(token.text);
previous = token;
}
if integer.text != "0" && integer.text.starts_with('0') {
return refuse(i, &start, "a number without a leading zero");
}
Ok(NumericValue(text))
}
fn boolean(i: &mut Tokens<'_, Kind>) -> PResult<bool> {
alt((keyword("true").value(true), keyword("false").value(false))).parse_next(i)
}
fn string_content<'i>(i: &mut Tokens<'i, Kind>) -> PResult<&'i str> {
let token = kind(Kind::String).parse_next(i)?;
inner(token, '"')
}
fn escapes_only(text: &str, allowed: &[char]) -> bool {
let mut chars = text.chars();
while let Some(c) = chars.next() {
if c == '\\' && !chars.next().is_some_and(|e| allowed.contains(&e)) {
return false;
}
}
true
}
fn match_words(text: &str) -> Option<Vec<String>> {
let mut words = Vec::new();
let mut word = String::new();
let mut rest = text;
while let Some(c) = rest.chars().next() {
if let Some(after) = rest.strip_prefix("/*") {
let end = after.find("*/")?;
rest = after.get(end.saturating_add(2)..)?;
if !word.is_empty() {
words.push(std::mem::take(&mut word));
}
continue;
}
rest = rest.get(c.len_utf8()..)?;
if matches!(c, ' ' | '\t' | '\r' | '\n') {
if !word.is_empty() {
words.push(std::mem::take(&mut word));
}
} else if c == '\\' {
let escaped = rest.chars().next().filter(|e| matches!(e, '"' | '\\'))?;
rest = rest.get(escaped.len_utf8()..)?;
word.push('\\');
word.push(escaped);
} else {
word.push(c);
}
}
if !word.is_empty() {
words.push(word);
}
(!words.is_empty()).then_some(words)
}
fn typed_search_term(i: &mut Tokens<'_, Kind>) -> PResult<TypedSearchTerm> {
if opt((keyword("wild"), kind(Kind::Colon)))
.parse_next(i)?
.is_some()
{
let start = i.checkpoint();
let pattern = cut_err(string_content).parse_next(i)?;
if pattern.is_empty() || !escapes_only(pattern, &['"', '\\', '*']) {
return refuse(i, &start, "a wildcard search term");
}
return Ok(TypedSearchTerm::Wild(pattern.to_owned()));
}
let explicit = opt((keyword("match"), kind(Kind::Colon)))
.parse_next(i)?
.is_some();
let start = i.checkpoint();
let content = if explicit {
cut_err(string_content).parse_next(i)?
} else {
string_content.parse_next(i)?
};
match match_words(content) {
Some(words) => Ok(TypedSearchTerm::Match(words)),
None => refuse(i, &start, "one or more search words"),
}
}
fn concrete_string(i: &mut Tokens<'_, Kind>) -> PResult<ConcreteString> {
let start = i.checkpoint();
let content = string_content.parse_next(i)?;
if content.is_empty() || !escapes_only(content, &['"', '\\', '*']) {
return refuse(i, &start, "a concrete string");
}
let mut value = String::with_capacity(content.len());
let mut chars = content.chars();
while let Some(c) = chars.next() {
match c {
'\\' => value.extend(chars.next()),
other => value.push(other),
}
}
Ok(ConcreteString(value))
}
fn concrete_strings(i: &mut Tokens<'_, Kind>) -> PResult<Vec<ConcreteString>> {
bare_or_set(concrete_string, "a concrete string").parse_next(i)
}
fn typed_search_terms(i: &mut Tokens<'_, Kind>) -> PResult<Vec<TypedSearchTerm>> {
bare_or_set(typed_search_term, "a search term").parse_next(i)
}
fn history_supplement(i: &mut Tokens<'_, Kind>) -> PResult<HistorySupplement> {
(kind(Kind::DoubleLeftBrace), kind(Kind::Plus)).parse_next(i)?;
let start = i.checkpoint();
let word = cut_err(kind(Kind::Identifier).expecting("HISTORY")).parse_next(i)?;
let profile = match word.text.to_ascii_uppercase().as_str() {
"HISTORY" => None,
"HISTORY-MIN" | "HISTORY_MIN" => Some(HistorySupplement::Minimum),
"HISTORY-MOD" | "HISTORY_MOD" => Some(HistorySupplement::Moderate),
"HISTORY-MAX" | "HISTORY_MAX" => Some(HistorySupplement::Maximum),
_ => {
return refuse(
i,
&start,
"HISTORY, HISTORY-MIN, HISTORY-MOD, or HISTORY-MAX",
);
}
};
let supplement = match profile {
Some(profile) => profile,
None => match opt(delimited(
kind(Kind::LeftParen),
cut_err(expression_constraint),
cut_err(kind(Kind::RightParen)),
))
.parse_next(i)?
{
Some(subset) => HistorySupplement::Subset(Box::new(subset)),
None => HistorySupplement::Default,
},
};
cut_err(kind(Kind::DoubleRightBrace)).parse_next(i)?;
Ok(supplement)
}
fn attribute(i: &mut Tokens<'_, Kind>) -> PResult<Attribute> {
let cardinality = opt(cardinality).parse_next(i)?;
let (reverse, name) = if let Some(name) = glued_reverse(i)? {
(true, name)
} else if opt(keyword("R")).parse_next(i)?.is_some() {
(true, cut_err(sub_expression_constraint).parse_next(i)?)
} else {
(false, sub_expression_constraint.parse_next(i)?)
};
let value = cut_err(attribute_value).parse_next(i)?;
Ok(Attribute {
cardinality,
reverse,
name,
value,
})
}
fn glued_reverse(i: &mut Tokens<'_, Kind>) -> PResult<Option<SubExpressionConstraint>> {
let Some(id) = i.peek_token().and_then(|t| {
let rest = t.text.strip_prefix(['R', 'r'])?;
(t.kind == Kind::Identifier && rest.bytes().all(|b| b.is_ascii_digit()))
.then(|| sctid_text(rest))
.flatten()
}) else {
return Ok(None);
};
any.void().parse_next(i)?;
let term = opt(term).parse_next(i)?;
let focus = FocusConcept::Reference(ConceptReference { id, term });
sub_expression_rest(i, None, None, focus).map(Some)
}
fn attribute_value(i: &mut Tokens<'_, Kind>) -> PResult<AttributeValue> {
alt((
(equality, sub_expression_constraint)
.map(|(operator, value)| AttributeValue::Expression { operator, value }),
(comparison, numeric_value)
.map(|(operator, value)| AttributeValue::Numeric { operator, value }),
(equality, concrete_strings)
.map(|(operator, values)| AttributeValue::String { operator, values }),
(equality, boolean).map(|(operator, value)| AttributeValue::Boolean { operator, value }),
preceded(
alt((equality.void(), comparison.void())),
cut_err(fail.expecting("an attribute value")),
),
))
.expecting("a comparison operator")
.parse_next(i)
}
fn sub_attribute_set(i: &mut Tokens<'_, Kind>) -> PResult<SubAttributeSet> {
alt((
attribute.map(|attribute| SubAttributeSet::Attribute(Box::new(attribute))),
delimited(kind(Kind::LeftParen), attribute_set, kind(Kind::RightParen))
.map(|set| SubAttributeSet::Nested(Box::new(set))),
))
.parse_next(i)
}
fn joined<'i, T: Clone>(
first: &T,
junction: impl Parser<Tokens<'i, Kind>, (), ErrMode<Failure>>,
item: impl Parser<Tokens<'i, Kind>, T, ErrMode<Failure>>,
i: &mut Tokens<'i, Kind>,
) -> PResult<Option<Vec<T>>> {
let rest: Option<Vec<T>> = opt(repeat(1.., preceded(junction, item))).parse_next(i)?;
Ok(rest.map(|rest| {
let mut all = vec![first.clone()];
all.extend(rest);
all
}))
}
fn attribute_set(i: &mut Tokens<'_, Kind>) -> PResult<AttributeSet> {
let first = sub_attribute_set.parse_next(i)?;
if let Some(all) = joined(&first, conjunction, sub_attribute_set, i)? {
return Ok(AttributeSet::Conjunction(all));
}
if let Some(all) = joined(&first, disjunction, sub_attribute_set, i)? {
return Ok(AttributeSet::Disjunction(all));
}
Ok(AttributeSet::Single(Box::new(first)))
}
fn attribute_group(i: &mut Tokens<'_, Kind>) -> PResult<SubRefinement> {
let cardinality = opt(cardinality).parse_next(i)?;
kind(Kind::LeftBrace).parse_next(i)?;
let attributes = cut_err(attribute_set).parse_next(i)?;
cut_err(kind(Kind::RightBrace)).parse_next(i)?;
Ok(SubRefinement::Group {
cardinality,
attributes,
})
}
fn sub_refinement(i: &mut Tokens<'_, Kind>) -> PResult<SubRefinement> {
alt((
attribute_set.map(SubRefinement::AttributeSet),
attribute_group,
delimited(
kind(Kind::LeftParen),
cut_err(refinement),
cut_err(kind(Kind::RightParen)),
)
.map(|inner| SubRefinement::Nested(Box::new(inner))),
))
.expecting("an attribute, an attribute group, or '('")
.parse_next(i)
}
fn refinement(i: &mut Tokens<'_, Kind>) -> PResult<Refinement> {
let first = sub_refinement.parse_next(i)?;
if let Some(all) = joined(&first, conjunction, cut_err(sub_refinement), i)? {
return Ok(Refinement::Conjunction(all));
}
if let Some(all) = joined(&first, disjunction, cut_err(sub_refinement), i)? {
return Ok(Refinement::Disjunction(all));
}
Ok(Refinement::Single(Box::new(first)))
}
pub fn whole(i: &mut Tokens<'_, Kind>) -> PResult<ExpressionConstraint> {
let constraint = expression_constraint
.expecting("an expression constraint")
.parse_next(i)?;
eof.expecting("the end of the expression").parse_next(i)?;
Ok(constraint)
}