use chumsky::Parser;
use chumsky::error::Rich;
use chumsky::prelude::*;
use rust_decimal::Decimal;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
Null,
Boolean(bool),
String(String),
Number(Decimal),
Integer(i64),
Date(helios_fhir::PrecisionDate),
DateTime(helios_fhir::PrecisionDateTime),
Time(helios_fhir::PrecisionTime),
Quantity(Decimal, String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expression {
Term(Term),
Invocation(Box<Expression>, Invocation),
Indexer(Box<Expression>, Box<Expression>),
Polarity(char, Box<Expression>),
Multiplicative(Box<Expression>, String, Box<Expression>),
Additive(Box<Expression>, String, Box<Expression>),
Type(Box<Expression>, String, TypeSpecifier),
Union(Box<Expression>, Box<Expression>),
Inequality(Box<Expression>, String, Box<Expression>),
Equality(Box<Expression>, String, Box<Expression>),
Membership(Box<Expression>, String, Box<Expression>),
And(Box<Expression>, Box<Expression>),
Or(Box<Expression>, String, Box<Expression>),
Implies(Box<Expression>, Box<Expression>),
Lambda(Option<String>, Box<Expression>),
InstanceSelector(String, Vec<(String, Box<Expression>)>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum TypeSpecifier {
QualifiedIdentifier(String, Option<String>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Term {
Invocation(Invocation),
Literal(Literal),
ExternalConstant(String),
Parenthesized(Box<Expression>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Invocation {
Member(String),
Function(String, Vec<Expression>),
This,
Index,
Total,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExprSpan {
pub position: usize,
pub length: usize,
}
#[derive(Debug, Clone)]
pub struct SpannedExpression {
pub kind: SpannedExprKind,
pub span: ExprSpan,
}
#[derive(Debug, Clone)]
pub enum SpannedExprKind {
Term(SpannedTerm),
Invocation(Box<SpannedExpression>, SpannedInvocation),
Indexer(Box<SpannedExpression>, Box<SpannedExpression>),
Polarity(char, Box<SpannedExpression>),
Multiplicative(Box<SpannedExpression>, String, Box<SpannedExpression>),
Additive(Box<SpannedExpression>, String, Box<SpannedExpression>),
Type(Box<SpannedExpression>, String, TypeSpecifier),
Union(Box<SpannedExpression>, Box<SpannedExpression>),
Inequality(Box<SpannedExpression>, String, Box<SpannedExpression>),
Equality(Box<SpannedExpression>, String, Box<SpannedExpression>),
Membership(Box<SpannedExpression>, String, Box<SpannedExpression>),
And(Box<SpannedExpression>, Box<SpannedExpression>),
Or(Box<SpannedExpression>, String, Box<SpannedExpression>),
Implies(Box<SpannedExpression>, Box<SpannedExpression>),
Lambda(Option<String>, Box<SpannedExpression>),
InstanceSelector(String, Vec<(String, Box<SpannedExpression>)>),
}
#[derive(Debug, Clone)]
pub enum SpannedTerm {
Literal(Literal),
Invocation(SpannedInvocation),
ExternalConstant(String),
Parenthesized(Box<SpannedExpression>),
}
#[derive(Debug, Clone)]
pub enum SpannedInvocation {
Member(String),
Function(String, Vec<SpannedExpression>),
This,
Index,
Total,
}
impl SpannedExpression {
pub fn to_expression(&self) -> Expression {
match &self.kind {
SpannedExprKind::Term(t) => Expression::Term(t.to_term()),
SpannedExprKind::Invocation(base, inv) => {
Expression::Invocation(Box::new(base.to_expression()), inv.to_invocation())
}
SpannedExprKind::Indexer(expr, idx) => Expression::Indexer(
Box::new(expr.to_expression()),
Box::new(idx.to_expression()),
),
SpannedExprKind::Polarity(op, expr) => {
Expression::Polarity(*op, Box::new(expr.to_expression()))
}
SpannedExprKind::Multiplicative(l, op, r) => Expression::Multiplicative(
Box::new(l.to_expression()),
op.clone(),
Box::new(r.to_expression()),
),
SpannedExprKind::Additive(l, op, r) => Expression::Additive(
Box::new(l.to_expression()),
op.clone(),
Box::new(r.to_expression()),
),
SpannedExprKind::Type(expr, op, ts) => {
Expression::Type(Box::new(expr.to_expression()), op.clone(), ts.clone())
}
SpannedExprKind::Union(l, r) => {
Expression::Union(Box::new(l.to_expression()), Box::new(r.to_expression()))
}
SpannedExprKind::Inequality(l, op, r) => Expression::Inequality(
Box::new(l.to_expression()),
op.clone(),
Box::new(r.to_expression()),
),
SpannedExprKind::Equality(l, op, r) => Expression::Equality(
Box::new(l.to_expression()),
op.clone(),
Box::new(r.to_expression()),
),
SpannedExprKind::Membership(l, op, r) => Expression::Membership(
Box::new(l.to_expression()),
op.clone(),
Box::new(r.to_expression()),
),
SpannedExprKind::And(l, r) => {
Expression::And(Box::new(l.to_expression()), Box::new(r.to_expression()))
}
SpannedExprKind::Or(l, op, r) => Expression::Or(
Box::new(l.to_expression()),
op.clone(),
Box::new(r.to_expression()),
),
SpannedExprKind::Implies(l, r) => {
Expression::Implies(Box::new(l.to_expression()), Box::new(r.to_expression()))
}
SpannedExprKind::Lambda(param, expr) => {
Expression::Lambda(param.clone(), Box::new(expr.to_expression()))
}
SpannedExprKind::InstanceSelector(type_name, fields) => Expression::InstanceSelector(
type_name.clone(),
fields
.iter()
.map(|(name, expr)| (name.clone(), Box::new(expr.to_expression())))
.collect(),
),
}
}
}
impl SpannedTerm {
pub fn to_term(&self) -> Term {
match self {
SpannedTerm::Literal(l) => Term::Literal(l.clone()),
SpannedTerm::Invocation(i) => Term::Invocation(i.to_invocation()),
SpannedTerm::ExternalConstant(s) => Term::ExternalConstant(s.clone()),
SpannedTerm::Parenthesized(e) => Term::Parenthesized(Box::new(e.to_expression())),
}
}
}
impl SpannedInvocation {
pub fn to_invocation(&self) -> Invocation {
match self {
SpannedInvocation::Member(s) => Invocation::Member(s.clone()),
SpannedInvocation::Function(name, args) => Invocation::Function(
name.clone(),
args.iter().map(|a| a.to_expression()).collect(),
),
SpannedInvocation::This => Invocation::This,
SpannedInvocation::Index => Invocation::Index,
SpannedInvocation::Total => Invocation::Total,
}
}
}
impl fmt::Display for Literal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Literal::Null => write!(f, "{{}}"),
Literal::Boolean(b) => write!(f, "{}", b),
Literal::String(s) => write!(f, "'{}'", s),
Literal::Number(d) => write!(f, "{}", d), Literal::Integer(n) => write!(f, "{}", n),
Literal::Date(d) => write!(f, "@{}", d.original_string()),
Literal::DateTime(dt) => write!(f, "@{}", dt.original_string()),
Literal::Time(t) => write!(f, "@T{}", t.original_string()),
Literal::Quantity(d, u) => write!(f, "{} '{}'", d, u), }
}
}
fn clean_backtick_identifier(id: &str) -> String {
if id.starts_with('`') && id.ends_with('`') && id.len() >= 3 {
id[1..id.len() - 1].to_string()
} else {
id.to_string()
}
}
fn combine_string_code_units(codes: Vec<u32>) -> Result<String, &'static str> {
let mut out = String::with_capacity(codes.len());
let mut i = 0;
while i < codes.len() {
let c = codes[i];
if (0xD800..=0xDBFF).contains(&c) {
if let Some(&lo) = codes.get(i + 1) {
if (0xDC00..=0xDFFF).contains(&lo) {
let scalar = 0x10000 + ((c - 0xD800) << 10) + (lo - 0xDC00);
match char::from_u32(scalar) {
Some(ch) => {
out.push(ch);
i += 2;
continue;
}
None => return Err("Invalid surrogate pair"),
}
}
}
return Err("Unpaired high surrogate in \\uXXXX escape");
}
if (0xDC00..=0xDFFF).contains(&c) {
return Err("Unpaired low surrogate in \\uXXXX escape");
}
match char::from_u32(c) {
Some(ch) => out.push(ch),
None => return Err("Invalid Unicode code point"),
}
i += 1;
}
Ok(out)
}
fn custom_padded<'src, T, P>(
parser: P,
) -> impl Parser<'src, &'src str, T, extra::Err<Rich<'src, char>>> + Clone
where
P: Parser<'src, &'src str, T, extra::Err<Rich<'src, char>>> + Clone,
T: Clone,
{
let ws_or_comment = choice((
text::whitespace().at_least(1).ignored(),
just("//")
.then(any().and_is(text::newline().or(end()).not()).repeated())
.ignored(),
just("/*")
.then(any().and_is(just("*/").not()).repeated())
.then(just("*/"))
.ignored(),
))
.repeated()
.ignored();
ws_or_comment
.then(parser)
.map(|(_, result)| result)
.then_ignore(ws_or_comment)
}
pub fn parser<'src>()
-> impl Parser<'src, &'src str, Expression, extra::Err<Rich<'src, char>>> + Clone + 'src {
let esc = just('\\').ignore_then(choice((
just('`').to('`' as u32),
just('\'').to('\'' as u32),
just('\\').to('\\' as u32),
just('/').to('/' as u32),
just('f').to(0x000C_u32),
just('n').to('\n' as u32),
just('r').to('\r' as u32),
just('t').to('\t' as u32),
just('"').to('"' as u32),
just('u').ignore_then(
any()
.filter(|c: &char| c.is_ascii_hexdigit())
.repeated()
.exactly(4)
.collect::<String>()
.try_map(
|digits: String, span| match u32::from_str_radix(&digits, 16) {
Ok(code) => Ok(code),
Err(_) => Err(Rich::custom(span, "Invalid hex digits")),
},
),
),
)));
macro_rules! padded {
($p:expr) => {
custom_padded($p)
};
}
let null = just('{').then(just('}')).to(Literal::Null);
let boolean = choice((
text::keyword("true").to(Literal::Boolean(true)),
text::keyword("false").to(Literal::Boolean(false)),
))
.boxed();
let string = just('\'')
.ignore_then(
none_of("\\\'")
.map(|c: char| c as u32)
.or(esc)
.repeated()
.collect::<Vec<u32>>(),
)
.then_ignore(just('\''))
.try_map(|codes, span| combine_string_code_units(codes).map_err(|e| Rich::custom(span, e)))
.map(Literal::String)
.boxed();
let integer = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1) .collect::<String>()
.try_map(|digits: String, span| match i64::from_str(&digits) {
Ok(n) => Ok(Literal::Integer(n)),
Err(_) => Err(Rich::custom(span, format!("Invalid integer: {}", digits))),
});
let integer = padded!(integer);
let number = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1) .collect::<String>()
.then(just('.')) .then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1) .collect::<String>(),
)
.try_map(|((i, _), d), span| {
let num_str = format!("{}.{}", i, d);
match Decimal::from_str(&num_str) {
Ok(decimal) => Ok(Literal::Number(decimal)),
Err(_) => Err(Rich::custom(span, format!("Invalid number: {}", num_str))),
}
})
.padded();
let time_format = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(2) .at_most(2)
.collect::<String>()
.then(
just(':') .ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(2) .at_most(2)
.collect::<String>(),
)
.then(
just(':') .ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(2) .at_most(2)
.collect::<String>(),
)
.then(
just('.') .ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1) .at_most(3)
.collect::<String>(),
)
.or_not(),
)
.or_not(),
)
.or_not(),
)
.map(|(hours, rest_opt)| {
let mut result = hours;
if let Some((minutes, seconds_part)) = rest_opt {
result.push(':');
result.push_str(&minutes);
if let Some((seconds, milliseconds)) = seconds_part {
result.push(':');
result.push_str(&seconds);
if let Some(ms) = milliseconds {
result.push('.');
result.push_str(&ms);
}
}
}
result
});
let timezone_format = just('Z')
.to("Z".to_string()) .or(one_of("+-") .map(|c: char| c.to_string()) .then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_most(2) .at_least(2)
.collect::<String>(),
)
.then(just(':')) .then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_most(2) .at_least(2)
.collect::<String>(),
)
.map(|(((sign, hour), _), min)| format!("{}{}:{}", sign, hour, min)));
let date_format_str = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.exactly(4) .collect::<String>()
.then(
just('-') .ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.exactly(2) .collect::<String>()
.then(
just('-') .ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.exactly(2) .collect::<String>(),
)
.or_not(),
),
)
.or_not(),
)
.map(|(year, month_part)| {
let mut date_str = year;
if let Some((month_str, day_part)) = month_part {
date_str.push('-');
date_str.push_str(&month_str);
if let Some(day_str) = day_part {
date_str.push('-');
date_str.push_str(&day_str);
}
}
date_str })
.boxed();
let unit_keyword = choice((
text::keyword("year").to("year".to_string()),
text::keyword("month").to("month".to_string()),
text::keyword("week").to("week".to_string()),
text::keyword("day").to("day".to_string()),
text::keyword("hour").to("hour".to_string()),
text::keyword("minute").to("minute".to_string()),
text::keyword("second").to("second".to_string()),
text::keyword("millisecond").to("millisecond".to_string()),
text::keyword("years").to("years".to_string()),
text::keyword("months").to("months".to_string()),
text::keyword("weeks").to("weeks".to_string()),
text::keyword("days").to("days".to_string()),
text::keyword("hours").to("hours".to_string()),
text::keyword("minutes").to("minutes".to_string()),
text::keyword("seconds").to("seconds".to_string()),
text::keyword("milliseconds").to("milliseconds".to_string()),
));
let unit_string_literal = just('\'')
.ignore_then(
none_of("\\\'")
.map(|c: char| c as u32)
.or(esc)
.repeated()
.collect::<Vec<u32>>(),
)
.then_ignore(just('\''))
.try_map(|codes, span| combine_string_code_units(codes).map_err(|e| Rich::custom(span, e)));
let unit = choice((
unit_keyword, unit_string_literal, ))
.boxed() .padded();
let integer_for_quantity = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.collect::<String>()
.try_map(|digits: String, span| match i64::from_str(&digits) {
Ok(n) => Ok(n), Err(_) => Err(Rich::custom(span, format!("Invalid integer: {}", digits))),
});
let number_for_quantity = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.collect::<String>()
.then(just('.'))
.then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.collect::<String>(),
)
.try_map(|((i, _), d), span| {
let num_str = format!("{}.{}", i, d);
match Decimal::from_str(&num_str) {
Ok(decimal) => Ok(decimal), Err(_) => Err(Rich::custom(span, format!("Invalid number: {}", num_str))),
}
});
let quantity = choice((
integer_for_quantity
.then_ignore(text::whitespace().at_least(1)) .then(unit.clone()) .map(|(i, u_str)| Literal::Quantity(Decimal::from(i), u_str)), number_for_quantity
.then_ignore(text::whitespace().at_least(1)) .then(unit.clone()) .map(|(d, u_str)| Literal::Quantity(d, u_str)), ));
let datetime_literal = just('@')
.ignore_then(date_format_str.clone())
.then_ignore(just('T'))
.then(time_format)
.then(timezone_format.clone().or_not())
.try_map(|((date_str, time_str), tz_opt), span| {
let full_str = if let Some(tz) = tz_opt {
format!("{}T{}{}", date_str, time_str, tz)
} else {
format!("{}T{}", date_str, time_str)
};
helios_fhir::PrecisionDateTime::parse(&full_str)
.ok_or_else(|| Rich::custom(span, format!("Invalid datetime format: {}", full_str)))
.map(Literal::DateTime)
});
let partial_datetime_literal = just('@')
.ignore_then(date_format_str.clone())
.then_ignore(just('T'))
.try_map(|date_str, span| {
let full_str = format!("{}T", date_str);
helios_fhir::PrecisionDateTime::parse(&full_str)
.ok_or_else(|| {
Rich::custom(
span,
format!("Invalid partial datetime format: {}", full_str),
)
})
.map(Literal::DateTime)
});
let time_literal = just('@')
.ignore_then(
just('T')
.ignore_then(time_format)
.then(timezone_format.or_not()), )
.try_map(|(time_str, tz_opt), span| {
if tz_opt.is_some() {
Err(Rich::custom(
span,
"Time literal cannot have a timezone offset",
))
} else {
helios_fhir::PrecisionTime::parse(&time_str)
.ok_or_else(|| Rich::custom(span, format!("Invalid time format: {}", time_str)))
.map(Literal::Time)
}
});
let date_literal = just('@')
.ignore_then(date_format_str.clone())
.try_map(|date_str, span| {
helios_fhir::PrecisionDate::parse(&date_str)
.ok_or_else(|| Rich::custom(span, format!("Invalid date format: {}", date_str)))
.map(Literal::Date)
});
let literal = choice((
null,
boolean,
string,
quantity, number, integer, padded!(datetime_literal), padded!(partial_datetime_literal), padded!(time_literal), padded!(date_literal), ))
.map(Term::Literal);
let standard_identifier = any()
.filter(|c: &char| c.is_ascii_alphabetic() || *c == '_')
.then(
any()
.filter(|c: &char| c.is_ascii_alphanumeric() || *c == '_')
.repeated()
.collect::<Vec<_>>(),
)
.map(|(first, rest): (char, Vec<char>)| {
let mut s = first.to_string();
s.extend(rest);
s
})
.padded();
let delimited_identifier = just('`')
.ignore_then(
none_of("`")
.map(|c: char| c as u32)
.or(esc)
.repeated()
.collect::<Vec<u32>>(),
)
.then_ignore(just('`'))
.try_map(|codes, span| combine_string_code_units(codes).map_err(|e| Rich::custom(span, e)))
.padded();
let identifier = choice((
standard_identifier,
delimited_identifier,
text::keyword("as").to(String::from("as")),
text::keyword("contains").to(String::from("contains")),
text::keyword("in").to(String::from("in")),
text::keyword("is").to(String::from("is")),
text::keyword("true").to(String::from("true")), text::keyword("false").to(String::from("false")), ));
let qualified_identifier = {
let explicit_namespace_type = identifier
.clone()
.then(just('.').ignore_then(identifier.clone()))
.map(|(namespace, type_name)| {
let clean_ns = clean_backtick_identifier(&namespace);
let clean_type = clean_backtick_identifier(&type_name);
TypeSpecifier::QualifiedIdentifier(clean_ns, Some(clean_type))
});
let standalone_type = identifier.clone().map(|id| {
let clean_id = clean_backtick_identifier(&id);
if clean_id.contains('.') {
if let Some(last_dot_pos) = clean_id.rfind('.') {
let namespace = clean_id[..last_dot_pos].to_string();
let type_name = clean_id[last_dot_pos + 1..].to_string();
TypeSpecifier::QualifiedIdentifier(namespace, Some(type_name))
} else {
TypeSpecifier::QualifiedIdentifier(clean_id, None)
}
} else {
TypeSpecifier::QualifiedIdentifier(clean_id, None)
}
});
choice((explicit_namespace_type.boxed(), standalone_type.boxed())).boxed()
};
let qualified_identifier = padded!(qualified_identifier);
let string_for_external = just('\'')
.ignore_then(
none_of("\'\\")
.map(|c: char| c as u32)
.or(esc)
.repeated()
.collect::<Vec<u32>>(),
)
.then_ignore(just('\''))
.try_map(|codes, span| combine_string_code_units(codes).map_err(|e| Rich::custom(span, e)))
.padded();
let external_constant = just('%')
.ignore_then(choice((identifier.clone(), string_for_external)))
.map(Term::ExternalConstant)
.padded();
recursive(|expr| {
let atom = choice((
literal.clone().map(Expression::Term).boxed(), external_constant.clone().map(Expression::Term).boxed(),
identifier
.clone()
.then(
expr.clone()
.separated_by(just(',').padded())
.allow_trailing()
.collect::<Vec<_>>()
.delimited_by(just('(').padded(), just(')').padded()),
)
.map(|(name, params)| {
Expression::Term(Term::Invocation(Invocation::Function(name, params)))
})
.boxed(),
identifier
.clone()
.then(
identifier
.clone()
.then_ignore(just(':').padded())
.then(expr.clone().boxed())
.separated_by(just(',').padded())
.allow_trailing()
.collect::<Vec<_>>()
.delimited_by(just('{').padded(), just('}').padded()),
)
.map(|(type_name, fields)| {
Expression::InstanceSelector(
type_name,
fields.into_iter().map(|(k, v)| (k, Box::new(v))).collect(),
)
})
.boxed(),
choice((
identifier.clone().map(Invocation::Member),
just("$this").to(Invocation::This),
just("$index").to(Invocation::Index),
just("$total").to(Invocation::Total),
))
.map(Term::Invocation) .map(Expression::Term) .boxed(),
expr.clone()
.boxed()
.delimited_by(just('(').padded(), just(')').padded())
.boxed(),
))
.padded();
let postfix_op = choice((
just('.')
.ignore_then(
identifier.clone().then(
expr.clone()
.boxed()
.separated_by(just(',').padded())
.allow_trailing()
.collect::<Vec<_>>()
.delimited_by(just('(').padded(), just(')').padded())
.or_not(), ),
)
.map(|(name, params_opt)| {
let invocation = match params_opt {
Some(params) => Invocation::Function(name, params),
None => Invocation::Member(name),
};
Box::new(move |left: Expression| {
Expression::Invocation(Box::new(left), invocation.clone())
}) as Box<dyn Fn(Expression) -> Expression>
}),
expr.clone()
.delimited_by(just('[').padded(), just(']').padded())
.map(|idx| {
Box::new(move |left: Expression| {
Expression::Indexer(Box::new(left), Box::new(idx.clone()))
}) as Box<dyn Fn(Expression) -> Expression>
}),
))
.boxed();
let atom_with_postfix = atom
.clone()
.then(postfix_op.repeated().collect::<Vec<_>>())
.map(|(left, ops)| ops.into_iter().fold(left, |acc, op| op(acc)));
let prefix_op = choice((just('+').to('+'), just('-').to('-'))).padded();
let term_with_polarity = prefix_op
.repeated()
.collect::<Vec<_>>()
.then(atom_with_postfix)
.map(|(ops, right)| {
ops.into_iter()
.rev()
.fold(right, |acc, op| Expression::Polarity(op, Box::new(acc)))
});
let op_mul = choice((
just('*').to("*"),
just('/').to("/"),
text::keyword("div").to("div"),
text::keyword("mod").to("mod"),
))
.padded();
let multiplicative = term_with_polarity
.clone()
.then(
op_mul
.then(term_with_polarity)
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
Expression::Multiplicative(Box::new(acc), op_str.to_string(), Box::new(right))
})
});
let op_add = choice((just('+').to("+"), just('-').to("-"), just('&').to("&"))).padded();
let additive = multiplicative
.clone()
.then(op_add.then(multiplicative).repeated().collect::<Vec<_>>())
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
Expression::Additive(Box::new(acc), op_str.to_string(), Box::new(right))
})
});
let op_type = choice((text::keyword("is").to("is"), text::keyword("as").to("as"))).padded();
let type_expr = additive
.clone()
.then(
op_type
.then(qualified_identifier.clone())
.repeated()
.collect::<Vec<_>>(),
) .map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, type_spec)| {
Expression::Type(Box::new(acc), op_str.to_string(), type_spec)
})
});
let op_union = just('|').padded();
let union = type_expr
.clone()
.then(op_union.then(type_expr).repeated().collect::<Vec<_>>())
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (_, right)| {
Expression::Union(Box::new(acc), Box::new(right))
})
});
let op_ineq = choice((
just("<=").to("<="),
just("<").to("<"),
just(">=").to(">="),
just(">").to(">"),
))
.padded();
let inequality = union
.clone()
.then(op_ineq.then(union).repeated().collect::<Vec<_>>())
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
Expression::Inequality(Box::new(acc), op_str.to_string(), Box::new(right))
})
});
let op_eq = choice((
just("=").to("="),
just("~").to("~"),
just("!=").to("!="),
just("!~").to("!~"),
))
.padded();
let equality = inequality
.clone()
.boxed()
.then(
op_eq
.then(inequality.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
Expression::Equality(Box::new(acc), op_str.to_string(), Box::new(right))
})
});
let op_mem = choice((
text::keyword("in").to("in"),
text::keyword("contains").to("contains"),
))
.padded();
let membership = equality
.clone()
.boxed()
.then(
op_mem
.then(equality.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
Expression::Membership(Box::new(acc), op_str.to_string(), Box::new(right))
})
});
let op_and = text::keyword("and").padded();
let logical_and = membership
.clone()
.boxed()
.then(
op_and
.then(membership.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (_, right)| {
Expression::And(Box::new(acc), Box::new(right))
})
});
let op_or = choice((text::keyword("or").to("or"), text::keyword("xor").to("xor"))).padded();
let logical_or = logical_and
.clone()
.boxed()
.then(
op_or
.then(logical_and.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
Expression::Or(Box::new(acc), op_str.to_string(), Box::new(right))
})
});
let op_implies = text::keyword("implies").padded();
logical_or
.clone()
.boxed()
.then(
op_implies
.then(logical_or.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (_, right)| {
Expression::Implies(Box::new(acc), Box::new(right))
})
})
}) .then_ignore(end()) }
pub fn spanned_parser<'src>()
-> impl Parser<'src, &'src str, SpannedExpression, extra::Err<Rich<'src, char>>> + Clone + 'src {
let esc = just('\\').ignore_then(choice((
just('`').to('`' as u32),
just('\'').to('\'' as u32),
just('\\').to('\\' as u32),
just('/').to('/' as u32),
just('f').to(0x000C_u32),
just('n').to('\n' as u32),
just('r').to('\r' as u32),
just('t').to('\t' as u32),
just('"').to('"' as u32),
just('u').ignore_then(
any()
.filter(|c: &char| c.is_ascii_hexdigit())
.repeated()
.exactly(4)
.collect::<String>()
.try_map(
|digits: String, span| match u32::from_str_radix(&digits, 16) {
Ok(code) => Ok(code),
Err(_) => Err(Rich::custom(span, "Invalid hex digits")),
},
),
),
)));
let null = just('{').then(just('}')).to(Literal::Null);
let boolean = choice((
text::keyword("true").to(Literal::Boolean(true)),
text::keyword("false").to(Literal::Boolean(false)),
))
.boxed();
let string = just('\'')
.ignore_then(
none_of("\\\'")
.map(|c: char| c as u32)
.or(esc)
.repeated()
.collect::<Vec<u32>>(),
)
.then_ignore(just('\''))
.try_map(|codes, span| combine_string_code_units(codes).map_err(|e| Rich::custom(span, e)))
.map(Literal::String)
.boxed();
let integer = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.collect::<String>()
.try_map(|digits: String, span| match i64::from_str(&digits) {
Ok(n) => Ok(Literal::Integer(n)),
Err(_) => Err(Rich::custom(span, format!("Invalid integer: {}", digits))),
});
let integer = custom_padded(integer);
let number = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.collect::<String>()
.then(just('.'))
.then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.collect::<String>(),
)
.try_map(|((i, _), d), span| {
let num_str = format!("{}.{}", i, d);
match Decimal::from_str(&num_str) {
Ok(decimal) => Ok(Literal::Number(decimal)),
Err(_) => Err(Rich::custom(span, format!("Invalid number: {}", num_str))),
}
})
.padded();
let time_format = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(2)
.at_most(2)
.collect::<String>()
.then(
just(':')
.ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(2)
.at_most(2)
.collect::<String>(),
)
.then(
just(':')
.ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(2)
.at_most(2)
.collect::<String>(),
)
.then(
just('.')
.ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.at_most(3)
.collect::<String>(),
)
.or_not(),
)
.or_not(),
)
.or_not(),
)
.map(|(hours, rest_opt)| {
let mut result = hours;
if let Some((minutes, seconds_part)) = rest_opt {
result.push(':');
result.push_str(&minutes);
if let Some((seconds, milliseconds)) = seconds_part {
result.push(':');
result.push_str(&seconds);
if let Some(ms) = milliseconds {
result.push('.');
result.push_str(&ms);
}
}
}
result
});
let timezone_format = just('Z').to("Z".to_string()).or(one_of("+-")
.map(|c: char| c.to_string())
.then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_most(2)
.at_least(2)
.collect::<String>(),
)
.then(just(':'))
.then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_most(2)
.at_least(2)
.collect::<String>(),
)
.map(|(((sign, hour), _), min)| format!("{}{}:{}", sign, hour, min)));
let date_format_str = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.exactly(4)
.collect::<String>()
.then(
just('-')
.ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.exactly(2)
.collect::<String>()
.then(
just('-')
.ignore_then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.exactly(2)
.collect::<String>(),
)
.or_not(),
),
)
.or_not(),
)
.map(|(year, month_part)| {
let mut date_str = year;
if let Some((month_str, day_part)) = month_part {
date_str.push('-');
date_str.push_str(&month_str);
if let Some(day_str) = day_part {
date_str.push('-');
date_str.push_str(&day_str);
}
}
date_str
})
.boxed();
let unit_keyword = choice((
text::keyword("year").to("year".to_string()),
text::keyword("month").to("month".to_string()),
text::keyword("week").to("week".to_string()),
text::keyword("day").to("day".to_string()),
text::keyword("hour").to("hour".to_string()),
text::keyword("minute").to("minute".to_string()),
text::keyword("second").to("second".to_string()),
text::keyword("millisecond").to("millisecond".to_string()),
text::keyword("years").to("years".to_string()),
text::keyword("months").to("months".to_string()),
text::keyword("weeks").to("weeks".to_string()),
text::keyword("days").to("days".to_string()),
text::keyword("hours").to("hours".to_string()),
text::keyword("minutes").to("minutes".to_string()),
text::keyword("seconds").to("seconds".to_string()),
text::keyword("milliseconds").to("milliseconds".to_string()),
));
let unit_string_literal = just('\'')
.ignore_then(
none_of("\\\'")
.map(|c: char| c as u32)
.or(esc)
.repeated()
.collect::<Vec<u32>>(),
)
.then_ignore(just('\''))
.try_map(|codes, span| combine_string_code_units(codes).map_err(|e| Rich::custom(span, e)));
let unit = choice((unit_keyword, unit_string_literal)).boxed().padded();
let integer_for_quantity = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.collect::<String>()
.try_map(|digits: String, span| match i64::from_str(&digits) {
Ok(n) => Ok(n),
Err(_) => Err(Rich::custom(span, format!("Invalid integer: {}", digits))),
});
let number_for_quantity = any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.collect::<String>()
.then(just('.'))
.then(
any()
.filter(|c: &char| c.is_ascii_digit())
.repeated()
.at_least(1)
.collect::<String>(),
)
.try_map(|((i, _), d), span| {
let num_str = format!("{}.{}", i, d);
match Decimal::from_str(&num_str) {
Ok(decimal) => Ok(decimal),
Err(_) => Err(Rich::custom(span, format!("Invalid number: {}", num_str))),
}
});
let quantity = choice((
integer_for_quantity
.then_ignore(text::whitespace().at_least(1))
.then(unit.clone())
.map(|(i, u_str)| Literal::Quantity(Decimal::from(i), u_str)),
number_for_quantity
.then_ignore(text::whitespace().at_least(1))
.then(unit.clone())
.map(|(d, u_str)| Literal::Quantity(d, u_str)),
));
let datetime_literal = just('@')
.ignore_then(date_format_str.clone())
.then_ignore(just('T'))
.then(time_format)
.then(timezone_format.clone().or_not())
.try_map(|((date_str, time_str), tz_opt), span| {
let full_str = if let Some(tz) = tz_opt {
format!("{}T{}{}", date_str, time_str, tz)
} else {
format!("{}T{}", date_str, time_str)
};
helios_fhir::PrecisionDateTime::parse(&full_str)
.ok_or_else(|| Rich::custom(span, format!("Invalid datetime format: {}", full_str)))
.map(Literal::DateTime)
});
let partial_datetime_literal = just('@')
.ignore_then(date_format_str.clone())
.then_ignore(just('T'))
.try_map(|date_str, span| {
let full_str = format!("{}T", date_str);
helios_fhir::PrecisionDateTime::parse(&full_str)
.ok_or_else(|| {
Rich::custom(
span,
format!("Invalid partial datetime format: {}", full_str),
)
})
.map(Literal::DateTime)
});
let time_literal = just('@')
.ignore_then(
just('T')
.ignore_then(time_format)
.then(timezone_format.or_not()),
)
.try_map(|(time_str, tz_opt), span| {
if tz_opt.is_some() {
Err(Rich::custom(
span,
"Time literal cannot have a timezone offset",
))
} else {
helios_fhir::PrecisionTime::parse(&time_str)
.ok_or_else(|| Rich::custom(span, format!("Invalid time format: {}", time_str)))
.map(Literal::Time)
}
});
let date_literal = just('@')
.ignore_then(date_format_str.clone())
.try_map(|date_str, span| {
helios_fhir::PrecisionDate::parse(&date_str)
.ok_or_else(|| Rich::custom(span, format!("Invalid date format: {}", date_str)))
.map(Literal::Date)
});
let literal = choice((
null,
boolean,
string,
quantity,
number,
integer,
custom_padded(datetime_literal),
custom_padded(partial_datetime_literal),
custom_padded(time_literal),
custom_padded(date_literal),
))
.map(Term::Literal);
let standard_identifier = any()
.filter(|c: &char| c.is_ascii_alphabetic() || *c == '_')
.then(
any()
.filter(|c: &char| c.is_ascii_alphanumeric() || *c == '_')
.repeated()
.collect::<Vec<_>>(),
)
.map(|(first, rest): (char, Vec<char>)| {
let mut s = first.to_string();
s.extend(rest);
s
})
.padded();
let delimited_identifier = just('`')
.ignore_then(
none_of("`")
.map(|c: char| c as u32)
.or(esc)
.repeated()
.collect::<Vec<u32>>(),
)
.then_ignore(just('`'))
.try_map(|codes, span| combine_string_code_units(codes).map_err(|e| Rich::custom(span, e)))
.padded();
let identifier = choice((
standard_identifier,
delimited_identifier,
text::keyword("as").to(String::from("as")),
text::keyword("contains").to(String::from("contains")),
text::keyword("in").to(String::from("in")),
text::keyword("is").to(String::from("is")),
text::keyword("true").to(String::from("true")),
text::keyword("false").to(String::from("false")),
));
let qualified_identifier = {
let explicit_namespace_type = identifier
.clone()
.then(just('.').ignore_then(identifier.clone()))
.map(|(namespace, type_name)| {
let clean_ns = clean_backtick_identifier(&namespace);
let clean_type = clean_backtick_identifier(&type_name);
TypeSpecifier::QualifiedIdentifier(clean_ns, Some(clean_type))
});
let standalone_type = identifier.clone().map(|id| {
let clean_id = clean_backtick_identifier(&id);
if clean_id.contains('.') {
if let Some(last_dot_pos) = clean_id.rfind('.') {
let namespace = clean_id[..last_dot_pos].to_string();
let type_name = clean_id[last_dot_pos + 1..].to_string();
TypeSpecifier::QualifiedIdentifier(namespace, Some(type_name))
} else {
TypeSpecifier::QualifiedIdentifier(clean_id, None)
}
} else {
TypeSpecifier::QualifiedIdentifier(clean_id, None)
}
});
choice((explicit_namespace_type.boxed(), standalone_type.boxed())).boxed()
};
let qualified_identifier = custom_padded(qualified_identifier);
let string_for_external = just('\'')
.ignore_then(
none_of("\'\\")
.map(|c: char| c as u32)
.or(esc)
.repeated()
.collect::<Vec<u32>>(),
)
.then_ignore(just('\''))
.try_map(|codes, span| combine_string_code_units(codes).map_err(|e| Rich::custom(span, e)))
.padded();
let external_constant = just('%')
.ignore_then(choice((identifier.clone(), string_for_external)))
.map(Term::ExternalConstant)
.padded();
recursive(
|spanned_expr: Recursive<
dyn Parser<'src, &'src str, SpannedExpression, extra::Err<Rich<'src, char>>> + 'src,
>| {
#[inline]
fn make_spanned(kind: SpannedExprKind, start: usize, end: usize) -> SpannedExpression {
SpannedExpression {
kind,
span: ExprSpan {
position: start,
length: end - start,
},
}
}
let atom = choice((
literal
.clone()
.map_with(|term, extra| {
let s = extra.span();
let spanned_term = match term {
Term::Literal(l) => SpannedTerm::Literal(l),
_ => unreachable!(),
};
make_spanned(SpannedExprKind::Term(spanned_term), s.start, s.end)
})
.boxed(),
external_constant
.clone()
.map_with(|term, extra| {
let s = extra.span();
let spanned_term = match term {
Term::ExternalConstant(name) => SpannedTerm::ExternalConstant(name),
_ => unreachable!(),
};
make_spanned(SpannedExprKind::Term(spanned_term), s.start, s.end)
})
.boxed(),
identifier
.clone()
.then(
spanned_expr
.clone()
.separated_by(just(',').padded())
.allow_trailing()
.collect::<Vec<_>>()
.delimited_by(just('(').padded(), just(')').padded()),
)
.map_with(|(name, params), extra| {
let s = extra.span();
make_spanned(
SpannedExprKind::Term(SpannedTerm::Invocation(
SpannedInvocation::Function(name, params),
)),
s.start,
s.end,
)
})
.boxed(),
identifier
.clone()
.then(
identifier
.clone()
.then_ignore(just(':').padded())
.then(spanned_expr.clone().boxed())
.separated_by(just(',').padded())
.allow_trailing()
.collect::<Vec<_>>()
.delimited_by(just('{').padded(), just('}').padded()),
)
.map_with(|(type_name, fields), extra| {
let s = extra.span();
make_spanned(
SpannedExprKind::InstanceSelector(
type_name,
fields.into_iter().map(|(k, v)| (k, Box::new(v))).collect(),
),
s.start,
s.end,
)
})
.boxed(),
choice((
identifier.clone().map(SpannedInvocation::Member),
just("$this").to(SpannedInvocation::This),
just("$index").to(SpannedInvocation::Index),
just("$total").to(SpannedInvocation::Total),
))
.map_with(|inv, extra| {
let s = extra.span();
make_spanned(
SpannedExprKind::Term(SpannedTerm::Invocation(inv)),
s.start,
s.end,
)
})
.boxed(),
spanned_expr
.clone()
.boxed()
.delimited_by(just('(').padded(), just(')').padded())
.map_with(|inner, extra| {
let s = extra.span();
make_spanned(
SpannedExprKind::Term(SpannedTerm::Parenthesized(Box::new(inner))),
s.start,
s.end,
)
})
.boxed(),
))
.padded();
let postfix_op = choice((
just('.')
.ignore_then(
identifier.clone().then(
spanned_expr
.clone()
.boxed()
.separated_by(just(',').padded())
.allow_trailing()
.collect::<Vec<_>>()
.delimited_by(just('(').padded(), just(')').padded())
.or_not(),
),
)
.map_with(|(name, params_opt), extra| {
let op_end = extra.span().end;
let invocation = match params_opt {
Some(params) => SpannedInvocation::Function(name, params),
None => SpannedInvocation::Member(name),
};
Box::new(move |left: SpannedExpression| {
let start = left.span.position;
make_spanned(
SpannedExprKind::Invocation(Box::new(left), invocation.clone()),
start,
op_end,
)
})
as Box<dyn Fn(SpannedExpression) -> SpannedExpression>
}),
spanned_expr
.clone()
.delimited_by(just('[').padded(), just(']').padded())
.map_with(|idx, extra| {
let op_end = extra.span().end;
Box::new(move |left: SpannedExpression| {
let start = left.span.position;
make_spanned(
SpannedExprKind::Indexer(Box::new(left), Box::new(idx.clone())),
start,
op_end,
)
})
as Box<dyn Fn(SpannedExpression) -> SpannedExpression>
}),
))
.boxed();
let atom_with_postfix = atom
.clone()
.then(postfix_op.repeated().collect::<Vec<_>>())
.map(|(left, ops)| ops.into_iter().fold(left, |acc, op| op(acc)));
let prefix_op = choice((just('+').to('+'), just('-').to('-'))).padded();
let term_with_polarity = prefix_op
.repeated()
.collect::<Vec<_>>()
.then(atom_with_postfix)
.map_with(|(ops, right), extra| {
if ops.is_empty() {
right
} else {
let full_start = extra.span().start;
ops.into_iter().rev().fold(right, |acc, op| {
let end = acc.span.position + acc.span.length;
make_spanned(
SpannedExprKind::Polarity(op, Box::new(acc)),
full_start,
end,
)
})
}
});
let op_mul = choice((
just('*').to("*"),
just('/').to("/"),
text::keyword("div").to("div"),
text::keyword("mod").to("mod"),
))
.padded();
let multiplicative = term_with_polarity
.clone()
.then(
op_mul
.then(term_with_polarity)
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
let start = acc.span.position;
let end = right.span.position + right.span.length;
make_spanned(
SpannedExprKind::Multiplicative(
Box::new(acc),
op_str.to_string(),
Box::new(right),
),
start,
end,
)
})
});
let op_add = choice((just('+').to("+"), just('-').to("-"), just('&').to("&"))).padded();
let additive = multiplicative
.clone()
.then(op_add.then(multiplicative).repeated().collect::<Vec<_>>())
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
let start = acc.span.position;
let end = right.span.position + right.span.length;
make_spanned(
SpannedExprKind::Additive(
Box::new(acc),
op_str.to_string(),
Box::new(right),
),
start,
end,
)
})
});
let op_type =
choice((text::keyword("is").to("is"), text::keyword("as").to("as"))).padded();
let type_expr = additive
.clone()
.then(
op_type
.then(qualified_identifier.clone())
.repeated()
.collect::<Vec<_>>(),
)
.map_with(|(left, ops), extra| {
if ops.is_empty() {
left
} else {
let full_end = extra.span().end;
ops.into_iter().fold(left, |acc, (op_str, type_spec)| {
let start = acc.span.position;
make_spanned(
SpannedExprKind::Type(Box::new(acc), op_str.to_string(), type_spec),
start,
full_end,
)
})
}
});
let op_union = just('|').padded();
let union = type_expr
.clone()
.then(op_union.then(type_expr).repeated().collect::<Vec<_>>())
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (_, right)| {
let start = acc.span.position;
let end = right.span.position + right.span.length;
make_spanned(
SpannedExprKind::Union(Box::new(acc), Box::new(right)),
start,
end,
)
})
});
let op_ineq = choice((
just("<=").to("<="),
just("<").to("<"),
just(">=").to(">="),
just(">").to(">"),
))
.padded();
let inequality = union
.clone()
.then(op_ineq.then(union).repeated().collect::<Vec<_>>())
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
let start = acc.span.position;
let end = right.span.position + right.span.length;
make_spanned(
SpannedExprKind::Inequality(
Box::new(acc),
op_str.to_string(),
Box::new(right),
),
start,
end,
)
})
});
let op_eq = choice((
just("=").to("="),
just("~").to("~"),
just("!=").to("!="),
just("!~").to("!~"),
))
.padded();
let equality = inequality
.clone()
.boxed()
.then(
op_eq
.then(inequality.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
let start = acc.span.position;
let end = right.span.position + right.span.length;
make_spanned(
SpannedExprKind::Equality(
Box::new(acc),
op_str.to_string(),
Box::new(right),
),
start,
end,
)
})
});
let op_mem = choice((
text::keyword("in").to("in"),
text::keyword("contains").to("contains"),
))
.padded();
let membership = equality
.clone()
.boxed()
.then(
op_mem
.then(equality.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
let start = acc.span.position;
let end = right.span.position + right.span.length;
make_spanned(
SpannedExprKind::Membership(
Box::new(acc),
op_str.to_string(),
Box::new(right),
),
start,
end,
)
})
});
let op_and = text::keyword("and").padded();
let logical_and = membership
.clone()
.boxed()
.then(
op_and
.then(membership.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (_, right)| {
let start = acc.span.position;
let end = right.span.position + right.span.length;
make_spanned(
SpannedExprKind::And(Box::new(acc), Box::new(right)),
start,
end,
)
})
});
let op_or =
choice((text::keyword("or").to("or"), text::keyword("xor").to("xor"))).padded();
let logical_or = logical_and
.clone()
.boxed()
.then(
op_or
.then(logical_and.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (op_str, right)| {
let start = acc.span.position;
let end = right.span.position + right.span.length;
make_spanned(
SpannedExprKind::Or(Box::new(acc), op_str.to_string(), Box::new(right)),
start,
end,
)
})
});
let op_implies = text::keyword("implies").padded();
logical_or
.clone()
.boxed()
.then(
op_implies
.then(logical_or.clone().boxed())
.repeated()
.collect::<Vec<_>>(),
)
.map(|(left, ops)| {
ops.into_iter().fold(left, |acc, (_, right)| {
let start = acc.span.position;
let end = right.span.position + right.span.length;
make_spanned(
SpannedExprKind::Implies(Box::new(acc), Box::new(right)),
start,
end,
)
})
})
},
)
.then_ignore(end())
}