euph 0.2.0

A functional stack-based programming language
mod base;
mod bind;

pub use base::euphrates;
use dashu_int::IBig;
use hipstr::LocalHipStr;
use ordered_float::OrderedFloat;
use winnow::{
    ascii::digit1,
    combinator::{
        alt,
        cut_err,
        delimited,
        dispatch,
        empty,
        fail,
        not,
        opt,
        peek,
        preceded,
        repeat,
    },
    error::{
        StrContext,
        StrContextValue,
    },
    prelude::*,
    token::{
        any,
        one_of,
        take_while,
    },
};

fn eu_str_raw_inner<'eu>(input: &mut &str) -> ModalResult<LocalHipStr<'eu>> {
    delimited('`', take_while(0.., |c| c != '`'), opt('`'))
        .output_into()
        .parse_next(input)
}

fn eu_str_inner<'eu>(input: &mut &str) -> ModalResult<LocalHipStr<'eu>> {
    delimited(
        '"',
        repeat(0.., dispatch!(peek(any); '"' => fail, _ => eu_char_atom)).fold(
            LocalHipStr::new,
            |mut s, co| {
                if let Some(c) = co {
                    s.push(c);
                }
                s
            },
        ),
        opt('"'),
    )
    .parse_next(input)
}

fn eu_char_inner(input: &mut &str) -> ModalResult<Option<char>> {
    preceded('\'', eu_char_atom).parse_next(input)
}

fn eu_char_atom(input: &mut &str) -> ModalResult<Option<char>> {
    dispatch!(any;
        '\\' => dispatch!(cut_err(any).context(StrContext::Label("escape"));
            '\n' => empty.value(None),
            '0' => empty.value(Some('\0')),
            'n' => empty.value(Some('\n')),
            'r' => empty.value(Some('\r')),
            't' => empty.value(Some('\t')),
            'x' => eu_char_hex.map(Some),
            'u' => eu_char_uni.map(Some),
            c => empty.value(Some(c)),
        ),
        c => empty.value(Some(c)),
    )
    .parse_next(input)
}

fn eu_char_hex(input: &mut &str) -> ModalResult<char> {
    cut_err(
        take_while(2, |c: char| c.is_ascii_hexdigit())
            .try_map(|hex| u8::from_str_radix(hex, 16))
            .output_into(),
    )
    .context(StrContext::Label("hex pair escape"))
    .context(StrContext::Expected(StrContextValue::Description(
        "`\\xHH` where `H` is a hexadecimal digit",
    )))
    .parse_next(input)
}

fn eu_char_uni(input: &mut &str) -> ModalResult<char> {
    cut_err(
        delimited(
            '{',
            take_while(1..=6, |c: char| c.is_ascii_hexdigit()),
            opt('}'),
        )
        .context(StrContext::Label("unicode escape"))
        .context(StrContext::Expected(
            "`\\u{H...}` where `H...` is 1-6 hexadecimal digits".into(),
        ))
        .try_map(|hex| u32::from_str_radix(hex, 16))
        .verify_map(char::from_u32),
    )
    .context(StrContext::Label("unicode escape"))
    .context(StrContext::Expected(
        winnow::error::StrContextValue::Description("valid codepoint"),
    ))
    .parse_next(input)
}

fn int_suffix<T>(
    ns: &str,
    i32: impl FnOnce(i32) -> T + Copy,
    i64: impl FnOnce(i64) -> T + Copy,
    ibig: impl FnOnce(IBig) -> T + Copy,
    f64: impl FnOnce(OrderedFloat<f64>) -> T + Copy,
) -> impl FnMut(&mut &str) -> ModalResult<T> {
    move |input| {
        cut_err(alt((
            "i32"
                .try_map(|_| ns.parse().map(i32))
                .context(StrContext::Label("i32")),
            "i64"
                .try_map(|_| ns.parse().map(i64))
                .context(StrContext::Label("i64")),
            "ibig"
                .try_map(|_| ns.parse().map(ibig))
                .context(StrContext::Label("ibig")),
            "f64"
                .try_map(|_| ns.parse().map(f64))
                .context(StrContext::Label("f64")),
            empty
                .try_map(|()| ns.parse().map(ibig))
                .context(StrContext::Label("int")),
        )))
        .parse_next(input)
    }
}

fn float_suffix<T>(
    ns: &str,
    f64: impl FnOnce(OrderedFloat<f64>) -> T + Copy,
) -> impl FnMut(&mut &str) -> ModalResult<T> {
    move |input| {
        not_int_suffix(input)?;
        cut_err(alt((
            "f64"
                .try_map(|_| ns.parse().map(f64))
                .context(StrContext::Label("f64")),
            empty
                .try_map(|()| ns.parse().map(f64))
                .context(StrContext::Label("float")),
        )))
        .parse_next(input)
    }
}

fn not_int_suffix(input: &mut &str) -> ModalResult<()> {
    cut_err(not(alt(("i32", "i64", "ibig"))))
        .context(StrContext::Label("float suffix"))
        .context(StrContext::Expected(StrContextValue::Description(
            "`f64` or no suffix",
        )))
        .parse_next(input)
}

fn eu_num_inner<'i>(input: &mut &'i str) -> ModalResult<(bool, &'i str)> {
    let ((_, dec, exp), ns) = (
        digit1,
        opt(preceded('.', digit1)),
        opt((one_of(('e', 'E')), opt(one_of(('+', '-'))), digit1)),
    )
        .verify_map(|res @ (pre, dec, _): (&str, _, _)| {
            (!pre.is_empty() || dec.is_some_and(|s: &str| !s.is_empty())).then_some(res)
        })
        .with_taken()
        .parse_next(input)?;
    Ok((dec.is_some() || exp.is_some(), ns))
}

fn eu_word_inner<'eu>(input: &mut &str) -> ModalResult<LocalHipStr<'eu>> {
    (
        take_while(1, |c: char| {
            !c.is_dec_digit() && !matches!(c, '$' | '\\') && is_word_char(c)
        }),
        take_while(0.., is_word_char),
    )
        .map(|(a, b)| LocalHipStr::concat([a, b]))
        .parse_next(input)
}

fn is_word_char(c: char) -> bool {
    !matches!(
        c,
        '`' | '"' | '\'' | '(' | ')' | '[' | ']' | '{' | '}' | '.' | '\\'
    ) && !c.is_whitespace()
}

fn ws<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
    take_while(0.., char::is_whitespace).parse_next(input)
}

#[cfg(test)]
mod tests;