#![allow(clippy::double_parens)]
use chumsky::{
Parser,
input::{Stream, ValueInput},
prelude::*,
};
use logos::Logos;
use crate::{
ext::{business::Money, math::Natural},
runtime::model::ActorKind,
};
use super::{
ast::{Arg, Args, Command, Script, Split, Stmt, Value},
lex::Token,
};
pub type Error<'tok, 'src> = Rich<'tok, Token<'src>, SimpleSpan>;
pub type Ctx<'tok, 'src> = extra::Err<Error<'tok, 'src>>;
impl<'tok> Script<'tok> {
#[must_use]
pub fn parse<'src: 'tok>(source: &'src str) -> ParseResult<Self, Error<'tok, 'src>> {
let iter = Token::lexer(source).spanned().map(|(tok, span)| match tok {
Ok(tok) => (tok, span.into()),
Err(()) => (Token::Error, span.into()),
});
let end_span = (source.len()..source.len()).into();
let stream = Stream::from_iter(iter).map(end_span, |(t, s): (_, _)| (t, s));
parser().parse(stream)
}
}
macro_rules! from_str {
($parser:expr) => {
($parser).try_map(|src, span| src.parse().map_err(|err| Error::custom(span, err)))
};
}
pub fn parser<'tok, 'src: 'tok, I>() -> impl Parser<'tok, I, Script<'tok>, Ctx<'tok, 'src>>
where
I: ValueInput<'tok, Token = Token<'src>, Span = SimpleSpan>,
{
use Token as T;
const DOT_SHIFT: u8 = 10u8.pow(2);
let optional_space = just(T::Whitespace).repeated();
let hard_space = optional_space.at_least(1);
let statement_delimiter = one_of([T::Semicolon, T::Newline]).padded_by(optional_space);
let ident = select! { T::Ident(id) => id };
let decimal = select! { T::Decimal(src) => src }.map(|src| {
let (whole, fraction) = src
.rsplit_once('.')
.expect("lexer to emit decimal token only with a dot");
let nat = |src: &str| src.parse::<Natural>().unwrap();
nat(whole) * DOT_SHIFT + nat(fraction)
});
let natural = select! { T::Natural(src) => src }.from_str().unwrapped();
let cents = natural.then_ignore(optional_space.then(just(T::SignCent)));
let euros = choice((decimal, natural.map(|num| num * DOT_SHIFT)))
.then_ignore(optional_space.then(just(T::SignEuro)));
let money = choice((euros, cents)).map(Money);
let split = group((natural, just(T::Colon).padded_by(optional_space), natural))
.map(|(from, _, to)| Split { from, to });
let gtin = from_str!(select! { T::Natural(src) => src });
let name = ident;
let value = choice((
money.map(Value::Money),
split.map(Value::Split),
gtin.map(Value::Gtin),
name.map(Value::Name),
));
let named = group((ident, just(T::Equals).padded_by(optional_space), value))
.map(|(key, _, value)| Arg::Named { key, value });
let positional = value.map(Arg::Pos);
let arguments = choice((named, positional))
.separated_by(hard_space)
.collect::<Vec<_>>()
.map(Args);
let actor = select! {
T::Entity => ActorKind::Entity,
T::Concept => ActorKind::Concept,
T::Object => ActorKind::Object,
};
let command = choice((
just(T::Create)
.then(hard_space)
.ignore_then(actor)
.map(Command::Create),
just(T::Pay).to(Command::Pay),
just(T::Deliver).to(Command::Deliver),
just(T::Balance).to(Command::Balance),
));
let statement =
group((command, hard_space, arguments)).map(|(cmd, (), args)| Stmt { cmd, args });
statement
.separated_by(statement_delimiter.repeated().at_least(1))
.allow_leading()
.allow_trailing()
.collect::<Vec<_>>()
.map(Script)
}