noko 0.2.0

A tool for viewing logs
use chumsky::error::SimpleReason;
use chumsky::prelude::*;
use miette::Diagnostic;
use miette::LabeledSpan;
use miette::SourceSpan;
use thiserror::Error;

#[derive(Debug)]
pub enum Expr {
    Var(String),
    Call(Vec<Expr>),
    StringLiteral(String),
    ColorLiteral(String),
}

const fn assci_char_set(set: &'static str) -> [bool; 256] {
    let mut out = [false; 256];
    let mut idx = 0;
    let bset = set.as_bytes();
    while idx < bset.len() {
        out[bset[idx] as usize] = true;
        idx += 1;
    }
    out
}

fn in_assci_set(c: char, set: &[bool; 256]) -> bool {
    let true = c.is_ascii()
    else {
      return false;
    };
    set[u32::from(c) as usize]
}

fn is_leading_var_char(c: &char) -> bool {
    const CSET: [bool; 256] = assci_char_set("!$%&*+-./:<=>?@^_~");
    c.is_alphabetic() || in_assci_set(*c, &CSET)
}

fn is_var_char(c: &char) -> bool {
    is_leading_var_char(c) || c.is_ascii_digit()
}

fn varible() -> impl Parser<char, Expr, Error = Simple<char>> {
    filter(is_leading_var_char)
        .chain(filter(is_var_char).repeated())
        .collect()
        .map(Expr::Var)
        .padded()
        .padded_by(comment())
}

fn comment() -> impl Parser<char, (), Error = Simple<char>> + Clone {
    just("//")
        .then(take_until(text::newline()))
        .padded()
        .repeated()
        .ignored()
}

fn string_literal() -> impl Parser<char, Expr, Error = Simple<char>> {
    just("\"")
        .ignore_then(take_until(just("\"").ignored()))
        .map(|t| t.0)
        .collect()
        .map(Expr::StringLiteral)
        .padded()
        .padded_by(comment())
}

fn is_hex_digit(c: &char) -> bool {
    c.is_ascii_hexdigit()
}

fn hex_color_literal() -> impl Parser<char, Expr, Error = Simple<char>> {
    just("#")
        .ignore_then(filter(is_hex_digit).repeated().exactly(6))
        .collect()
        .map(Expr::ColorLiteral)
        .padded()
        .padded_by(comment())
}

pub fn expr() -> impl Parser<char, Vec<Expr>, Error = Simple<char>> {
    recursive(|e| {
        choice((
            varible(),
            string_literal(),
            hex_color_literal(),
            e.delimited_by(just('('), just(')')).map(Expr::Call),
        ))
        .repeated()
        .at_least(1)
    })
    .padded()
    .padded_by(comment())
}

pub fn to_parse_error(source: &str, err: &Simple<char>) -> ParseError {
    match err.reason() {
        SimpleReason::Unexpected => {
            let mut lbls: Vec<_> = err.label().into_iter().map(|_| err.span().into()).collect();
            if lbls.is_empty() && err.found().is_none() {
                lbls = vec![(source.len() - 1..source.len()).into()];
            }
            ParseError::Unexpected {
                found: err.found().copied(),
                expected: err.expected().flatten().cloned().collect(),
                label: lbls.into_iter().next(),
            }
        }
        SimpleReason::Unclosed { span, .. } => ParseError::Unmatched {
            begin: LabeledSpan::at(span.clone(), "Opening paren here"),
            end: LabeledSpan::at(err.span(), "mismatch here"),
        },
        _ => {
            ParseError::Generic {
                msg: err.to_string(),
            }
        }
    }
}

#[derive(Error, Debug, Diagnostic)]
pub enum ParseError {
    #[error("Unexpected Syntax: expected one of {expected:?}, found {found:?}")]
    #[diagnostic(code(parser::unexpected_input))]
    Unexpected {
        found: Option<char>,
        expected: Vec<char>,
        #[label]
        label: Option<SourceSpan>,
    },
    #[error("Unmatched Paren")]
    #[diagnostic(code(parser::unmatched_paren))]
    Unmatched {
        begin: LabeledSpan,
        end: LabeledSpan,
    },

    #[error("Parse Error: {msg}")]
    #[diagnostic(code(parser::generic_error))]
    Generic {
        msg: String,
    },
}

#[derive(Debug, Error, Diagnostic)]
#[error("Parsing Errors")]
#[diagnostic(code(parse_error))]
pub struct ParseErrors {
    #[source_code]
    pub source_code: String,

    #[related]
    pub all: Vec<ParseError>,
}

pub fn parse(source: &str) -> Result<Vec<Expr>, ParseErrors> {
    expr()
        .parse(source)
        .map_err(|errs| ParseErrors {
            source_code: source.into(),
            all: errs.iter().map(|err| to_parse_error(source, err)).collect()})
}