use chumsky::DefaultExpected;
use chumsky::error::{Error, LabelError};
use chumsky::prelude::*;
use chumsky::util::MaybeRef;
use indexmap::IndexMap;
use super::{OdinInterval, OdinKey, OdinValue};
use crate::v1_1::lexer::{Spanned, Token};
#[expect(
unused_qualifications,
reason = "the local alias shadows the name being qualified — dropping the path makes the definition self-referential"
)]
type Err<'a> = chumsky::extra::Err<Failure>;
#[derive(Debug, Clone)]
pub(super) enum Failure {
Syntax(SimpleSpan),
DuplicateAttribute {
name: String,
span: SimpleSpan,
},
DuplicateKey {
key: String,
span: SimpleSpan,
},
}
impl Failure {
fn span(&self) -> SimpleSpan {
match self {
Self::Syntax(s)
| Self::DuplicateAttribute { span: s, .. }
| Self::DuplicateKey { span: s, .. } => *s,
}
}
fn preferred(self, other: Self) -> Self {
match (&self, &other) {
(Self::DuplicateAttribute { .. } | Self::DuplicateKey { .. }, _) => self,
(_, Self::DuplicateAttribute { .. } | Self::DuplicateKey { .. }) => other,
_ => self,
}
}
}
impl<'a> Error<'a, &'a [Token]> for Failure {
fn merge(self, other: Self) -> Self {
self.preferred(other)
}
}
impl<'a> LabelError<'a, &'a [Token], DefaultExpected<'a, Token>> for Failure {
fn expected_found<E: IntoIterator<Item = DefaultExpected<'a, Token>>>(
_expected: E,
_found: Option<MaybeRef<'a, Token>>,
span: SimpleSpan,
) -> Self {
Self::Syntax(span)
}
fn merge_expected_found<E: IntoIterator<Item = DefaultExpected<'a, Token>>>(
self,
_expected: E,
_found: Option<MaybeRef<'a, Token>>,
span: SimpleSpan,
) -> Self {
self.preferred(Self::Syntax(span))
}
fn replace_expected_found<E: IntoIterator<Item = DefaultExpected<'a, Token>>>(
self,
_expected: E,
_found: Option<MaybeRef<'a, Token>>,
span: SimpleSpan,
) -> Self {
match self {
Self::Syntax(_) => Self::Syntax(span),
typed => typed,
}
}
}
pub(super) struct Located {
pub(super) failure: Failure,
pub(super) offset: usize,
}
pub(super) fn parse_tokens(spanned: &[Spanned]) -> Result<super::OdinDocument, Located> {
let tokens: Vec<Token> = spanned.iter().map(|s| s.token.clone()).collect();
odin_text().parse(&tokens).into_result().map_err(|errs| {
let failure = errs
.into_iter()
.reduce(Failure::preferred)
.unwrap_or(Failure::Syntax(SimpleSpan::new(
(),
spanned.len()..spanned.len(),
)));
let idx = failure.span().start;
let offset = spanned
.get(idx)
.map(|s| s.span.start)
.or_else(|| spanned.last().map(|s| s.span.end))
.unwrap_or(0);
Located { failure, offset }
})
}
fn odin_text<'a>() -> impl Parser<'a, &'a [Token], super::OdinDocument, Err<'a>> {
let block = object_block();
let attrs = attr_vals(block.clone());
let keyed = keyed_objects(block.clone());
schema_identifier()
.or_not()
.then(choice((attrs, keyed, block)))
.map(|(schema, root)| super::OdinDocument { schema, root })
.then_ignore(end())
}
fn schema_identifier<'a>() -> impl Parser<'a, &'a [Token], super::OdinSchemaId, Err<'a>> + Clone {
let name = select! {
Token::AlphaUcId(s) => s,
Token::AlphaLcId(s) => s,
Token::AlphaUnderscoreId(s) => s,
};
just(Token::SymAt)
.ignore_then(name)
.then_ignore(just(Token::SymEq))
.then(select! { Token::EmbeddedUri(s) => s })
.map(|(name, uri)| super::OdinSchemaId { name, uri })
}
fn attr_vals<'a>(
block: impl Parser<'a, &'a [Token], OdinValue, Err<'a>> + Clone + 'a,
) -> impl Parser<'a, &'a [Token], OdinValue, Err<'a>> + Clone {
let key = select! {
Token::AlphaUcId(s) => s,
Token::AlphaLcId(s) => s,
Token::AlphaUnderscoreId(s) => s,
};
key.then_ignore(just(Token::SymEq))
.then(block)
.then_ignore(just(Token::SymSemiColon).or_not())
.map_with(|pair, e| (pair, e.span()))
.repeated()
.at_least(1)
.collect::<Vec<((String, OdinValue), SimpleSpan)>>()
.try_map(|pairs, _| {
let mut map: IndexMap<String, OdinValue> = IndexMap::with_capacity(pairs.len());
for ((name, value), span) in pairs {
if map.insert(name.clone(), value).is_some() {
return Err(Failure::DuplicateAttribute { name, span });
}
}
Ok(OdinValue::Object(map))
})
}
fn keyed_objects<'a>(
block: impl Parser<'a, &'a [Token], OdinValue, Err<'a>> + Clone + 'a,
) -> impl Parser<'a, &'a [Token], OdinValue, Err<'a>> + Clone {
let key_id = key_id();
just(Token::LBracket)
.ignore_then(key_id)
.then_ignore(just(Token::RBracket))
.then_ignore(just(Token::SymEq))
.then(block)
.then_ignore(just(Token::SymSemiColon).or_not())
.map_with(|pair, e| (pair, e.span()))
.repeated()
.at_least(1)
.collect::<Vec<((OdinKey, OdinValue), SimpleSpan)>>()
.try_map(|entries, _| {
let mut seen: std::collections::HashSet<OdinKey> =
std::collections::HashSet::with_capacity(entries.len());
let mut list = Vec::with_capacity(entries.len());
for ((key, value), span) in entries {
if !seen.insert(key.clone()) {
return Err(Failure::DuplicateKey {
key: key.path_text(),
span,
});
}
list.push((key, value));
}
Ok(OdinValue::KeyedList(list))
})
}
fn key_id<'a>() -> impl Parser<'a, &'a [Token], OdinKey, Err<'a>> + Clone {
custom(|inp| {
let before = inp.cursor();
let sign = match inp.peek() {
Some(Token::SymPlus) => {
inp.next();
Some(1i64)
}
Some(Token::SymMinus) => {
inp.next();
Some(-1i64)
}
_ => None,
};
let token = inp.next();
let span = inp.span_since(&before);
match (sign, token) {
(sign, Some(Token::Integer(s))) => {
let mag = integer_lexeme(&s).ok_or(Failure::Syntax(span))?;
sign.unwrap_or(1)
.checked_mul(mag)
.map(OdinKey::Integer)
.ok_or(Failure::Syntax(span))
}
(None, Some(Token::String(s))) => Ok(OdinKey::String(decode_string(&s))),
(None, Some(Token::Iso8601Date(s))) => Ok(OdinKey::Date(s)),
(None, Some(Token::Iso8601Time(s))) => Ok(OdinKey::Time(s)),
(None, Some(Token::Iso8601DateTime(s))) => Ok(OdinKey::DateTime(s)),
_ => Err(Failure::Syntax(span)),
}
})
}
fn object_block<'a>() -> impl Parser<'a, &'a [Token], OdinValue, Err<'a>> + Clone {
recursive(|block| {
let keyed_list = keyed_objects(block.clone());
let bare_path =
select! { Token::AdlPath(s) => s }.or(just(Token::SymSlash).to("/".to_owned()));
let path = key_id()
.delimited_by(just(Token::LBracket), just(Token::RBracket))
.or_not()
.then(bare_path)
.map(|(root, p)| match root {
Some(key) => format!("[{}]{p}", key.path_text()),
None => p,
});
let ref_list = path
.clone()
.then(
choice((
just(Token::SymComma)
.ignore_then(path)
.repeated()
.at_least(1)
.collect::<Vec<String>>(),
just(Token::SymListContinue).to(Vec::new()),
))
.or_not(),
)
.map(|(first, rest)| {
let mut v = vec![first];
if let Some(mut more) = rest {
v.append(&mut more);
}
OdinValue::PathList(v)
});
let void = just(Token::SymListContinue).to(OdinValue::Empty);
let inner = choice((
void,
keyed_list,
ref_list,
attr_vals(block.clone()),
primitive_object(),
));
let value_block = rm_type_id()
.delimited_by(just(Token::LParen), just(Token::RParen))
.or_not()
.then(
inner
.or_not()
.delimited_by(just(Token::SymLt), just(Token::SymGt)),
)
.map(|(cast, inner_opt)| {
let val = inner_opt.unwrap_or(OdinValue::Empty);
match cast {
Some(rm_type) => OdinValue::Typed {
rm_type,
value: Box::new(val),
},
None => val,
}
});
let uri = select! { Token::EmbeddedUri(s) => OdinValue::Uri(s) };
let plug_in_tag = select! {
Token::AlphaLcId(s) => s,
Token::AlphaUcId(s) => s,
};
let plug_in = plug_in_tag
.delimited_by(just(Token::LParen), just(Token::RParen))
.then(select! { Token::PlugInBlock(s) => s })
.map(|(syntax, raw)| OdinValue::PlugIn {
syntax,
text: raw
.strip_prefix("<#")
.and_then(|t| t.strip_suffix("#>"))
.unwrap_or(&raw)
.to_owned(),
});
choice((uri, plug_in, value_block))
})
}
fn rm_type_id<'a>() -> impl Parser<'a, &'a [Token], String, Err<'a>> + Clone {
recursive(|t| {
let package_segment = select! {
Token::AlphaUcId(s) => s,
Token::AlphaLcId(s) => s,
};
package_segment
.then_ignore(just(Token::SymDot))
.repeated()
.collect::<Vec<String>>()
.then(select! { Token::AlphaUcId(s) => s })
.map(|(namespace, name)| {
if namespace.is_empty() {
name
} else {
format!("{}.{name}", namespace.join("."))
}
})
.then(
t.separated_by(just(Token::SymComma))
.at_least(1)
.collect::<Vec<String>>()
.delimited_by(just(Token::SymLt), just(Token::SymGt))
.or_not(),
)
.map(|(name, generics)| match generics {
Some(gs) => format!("{name}<{}>", gs.join(",")),
None => name,
})
})
}
fn primitive_object<'a>() -> impl Parser<'a, &'a [Token], OdinValue, Err<'a>> + Clone {
let leaf = leaf_value();
let item = choice((interval_value(leaf.clone()), leaf));
item.clone()
.map_with(|value, e| (value, e.span()))
.then(
just(Token::SymComma)
.ignore_then(item.map_with(|value, e| (value, e.span())))
.repeated()
.collect::<Vec<(OdinValue, SimpleSpan)>>(),
)
.then(
just(Token::SymComma)
.ignore_then(just(Token::SymListContinue))
.or_not(),
)
.try_map(|(((first, _), more), open), _| {
if more.is_empty() && open.is_none() {
return Ok(first);
}
let kind = std::mem::discriminant(&first);
let mut values = Vec::with_capacity(more.len() + 2);
values.push(first);
for (value, span) in more {
if std::mem::discriminant(&value) != kind {
return Err(Failure::Syntax(span));
}
values.push(value);
}
if open.is_some() {
values.push(OdinValue::ListContinue);
}
Ok(OdinValue::List(values))
})
}
fn interval_value<'a>(
leaf: impl Parser<'a, &'a [Token], OdinValue, Err<'a>> + Clone + 'a,
) -> impl Parser<'a, &'a [Token], OdinValue, Err<'a>> + Clone {
let unbounded = choice((
just(Token::SymMinus)
.or_not()
.ignore_then(just(Token::SymInfinity))
.ignored(),
just(Token::SymStar).ignored(),
))
.to(None);
let bound = choice((unbounded, leaf.clone().map(Some)));
let range = just(Token::SymGt)
.or_not()
.then(bound.clone())
.then_ignore(just(Token::SymIvlSep))
.then(just(Token::SymLt).or_not())
.then(bound.clone())
.map(|(((gt, lo), lt), hi)| OdinInterval::Range {
lower_included: gt.is_none() && lo.is_some(),
upper_included: lt.is_none() && hi.is_some(),
lower: lo.map(Box::new),
upper: hi.map(Box::new),
});
let plus_minus = leaf
.clone()
.then_ignore(just(Token::SymPlusOrMinus))
.then(leaf.clone())
.map(|(centre, delta)| OdinInterval::PlusMinus {
centre: Box::new(centre),
delta: Box::new(delta),
});
let relop = choice((
just(Token::SymGe).to(RelBound::Lower(true)),
just(Token::SymGt).to(RelBound::Lower(false)),
just(Token::SymLe).to(RelBound::Upper(true)),
just(Token::SymLt).to(RelBound::Upper(false)),
));
let single = relop.or_not().then(bound).map(|(op, v)| match (op, v) {
(None, Some(v)) => OdinInterval::Range {
lower: Some(Box::new(v.clone())),
lower_included: true,
upper: Some(Box::new(v)),
upper_included: true,
},
(Some(RelBound::Lower(incl)), Some(v)) => OdinInterval::Range {
lower: Some(Box::new(v)),
lower_included: incl,
upper: None,
upper_included: false,
},
(Some(RelBound::Upper(incl)), Some(v)) => OdinInterval::Range {
lower: None,
lower_included: false,
upper: Some(Box::new(v)),
upper_included: incl,
},
(_, None) => OdinInterval::Range {
lower: None,
lower_included: false,
upper: None,
upper_included: false,
},
});
just(Token::SymIvlDelim)
.ignore_then(choice((range, plus_minus, single)))
.then_ignore(just(Token::SymIvlDelim))
.map(OdinValue::Interval)
}
#[derive(Clone, Copy)]
enum RelBound {
Lower(bool),
Upper(bool),
}
fn leaf_value<'a>() -> impl Parser<'a, &'a [Token], OdinValue, Err<'a>> + Clone {
let int_sign = choice((
just(Token::SymPlus).to(1i64),
just(Token::SymMinus).to(-1i64),
))
.or_not();
let integer = int_sign
.then(select! { Token::Integer(s) => s })
.try_map(|(sign, s), span| {
let mag = integer_lexeme(&s).ok_or(Failure::Syntax(span))?;
sign.unwrap_or(1)
.checked_mul(mag)
.map(OdinValue::Integer)
.ok_or(Failure::Syntax(span))
});
let real_sign = choice((
just(Token::SymPlus).to(1f64),
just(Token::SymMinus).to(-1f64),
))
.or_not();
let real = real_sign
.then(select! { Token::Real(s) => s })
.try_map(|(sign, s), span| {
let Ok(mag) = s.parse::<f64>() else {
return Err(Failure::Syntax(span));
};
Ok(OdinValue::Real(sign.unwrap_or(1.0) * mag))
});
let string = select! { Token::String(s) => OdinValue::String(decode_string(&s)) };
let boolean = select! {
Token::SymTrue => OdinValue::Boolean(true),
Token::SymFalse => OdinValue::Boolean(false),
};
let character =
select! { Token::Character(s) => s }.map(|s| OdinValue::Character(decode_char(&s)));
let term_code = select! {
Token::TermCodeRef(s) => OdinValue::TermCode(s),
Token::LocalTermCodeRef(s) => OdinValue::TermCode(s),
};
let date = select! { Token::Iso8601Date(s) => OdinValue::Date(s) };
let time = select! { Token::Iso8601Time(s) => OdinValue::Time(s) };
let date_time = select! { Token::Iso8601DateTime(s) => OdinValue::DateTime(s) };
let duration = select! { Token::Iso8601Duration(s) => OdinValue::Duration(s) };
choice((
real, integer, string, boolean, character, term_code, date_time, date, time, duration,
))
}
#[expect(
clippy::integer_division,
reason = "the `mantissa % scale == 0` guard proves the division is exact — a negative exponent that does not divide evenly returns None instead"
)]
fn integer_lexeme(s: &str) -> Option<i64> {
let Some(e) = s.find(['e', 'E']) else {
return s.parse::<i64>().ok();
};
let mantissa = s.get(..e)?.parse::<i64>().ok()?;
let exponent = s.get(e + 1..)?.parse::<i32>().ok()?;
let scale = 10i64.checked_pow(exponent.unsigned_abs())?;
if exponent >= 0 {
mantissa.checked_mul(scale)
} else if mantissa % scale == 0 {
Some(mantissa / scale)
} else {
None
}
}
#[expect(
clippy::expect_used,
reason = "`Token::String` only exists when the lexer's validate_string ran crate::v1_1::escape::validate over the same body and it succeeded, so this decode of that body cannot fail"
)]
fn decode_string(raw: &str) -> String {
crate::v1_1::escape::decode_string_literal(raw)
.expect("a lexer-validated string literal should decode")
}
#[expect(
clippy::expect_used,
reason = "`Token::Character` only exists when the lexer's validate_char admitted the body, which restricts an escape to the six quoted forms none of which can fail to decode; the same token regex admits one body character or one two-character escape, each decoding to exactly one char, so the literal is never empty"
)]
fn decode_char(raw: &str) -> char {
crate::v1_1::escape::decode_character_literal(raw)
.expect("a lexer-validated character literal should decode")
.chars()
.next()
.expect("a lexer-validated character literal should decode to one character")
}