abyss-core 0.5.0

Core language primitives for the AbySS scripting language: AST, chumsky-based parser, static analysis, and formatter. Shared between the CLI interpreter and editor tooling.
Documentation
pub use helpers::{LineMap, abyss_whitespace, attach_line_info, scrub_comments_preserve_layout};
pub use span::SimpleSpan;
pub use tokens::{SpannedToken, Token, lexer};
mod diagnostics;
mod grammar;
mod helpers;
mod span;
mod tokens;

pub use diagnostics::{ParserDiagnostic, emit_diagnostics};

use std::sync::Arc;

use chumsky::{Parser, input::IterInput};
use ordered_float::OrderedFloat;

use crate::ast::AST;

use diagnostics::convert_rich_error;
use grammar::build_parser;
pub struct ParseOutcome {
    pub ast: Vec<AST>,
    pub diagnostics: Vec<ParserDiagnostic>,
}

pub fn parse(source: &str) -> ParseOutcome {
    let map = Arc::new(LineMap::new(source));

    let scrubbed_source = scrub_comments_preserve_layout(source);

    let (maybe_tokens, lex_errors) = lexer().parse(scrubbed_source.as_str()).into_output_errors();

    let mut diagnostics: Vec<ParserDiagnostic> = lex_errors
        .into_iter()
        .map(|err| convert_rich_error(err, &map, "Incantation unravelled at lexing stage"))
        .collect();

    let tokens = match maybe_tokens {
        Some(tokens) => normalize_negative_literals(tokens),
        None => {
            return ParseOutcome {
                ast: Vec::new(),
                diagnostics,
            };
        }
    };

    let len = source.len();
    let token_input = IterInput::new(tokens.into_iter(), SimpleSpan::new(len, len));

    let parser = build_parser(map.clone());
    let (maybe_ast, parse_errors) = parser.parse(token_input).into_output_errors();

    diagnostics.extend(
        parse_errors
            .into_iter()
            .map(|err| convert_rich_error(err, &map, "Spell error: Incantation failed")),
    );

    let ast = maybe_ast.unwrap_or_default();

    ParseOutcome { ast, diagnostics }
}

fn normalize_negative_literals(tokens: Vec<SpannedToken>) -> Vec<SpannedToken> {
    use Token::*;

    fn allows_unary(prev: Option<&Token>) -> bool {
        match prev {
            None => true,
            Some(token) => matches!(
                token,
                Assign
                    | AddAssign
                    | SubAssign
                    | MulAssign
                    | DivAssign
                    | ModAssign
                    | PowArcanaAssign
                    | PowAetherAssign
                    | Plus
                    | Minus
                    | Star
                    | Slash
                    | Percent
                    | Caret
                    | DoubleStar
                    | DoublePipe
                    | DoubleAmpersand
                    | Equal
                    | NotEqual
                    | LessThan
                    | LessThanOrEqual
                    | GreaterThan
                    | GreaterThanOrEqual
                    | OpenParen
                    | OpenBrace
                    | Comma
                    | Colon
                    | Semicolon
                    | Arrow
                    | FatArrow
                    | Bang
            ),
        }
    }

    let mut result = Vec::with_capacity(tokens.len() + 4);
    let mut prev_token: Option<Token> = None;

    for (token, span) in tokens {
        match token {
            Arcana(value) if value < 0 && !allows_unary(prev_token.as_ref()) => {
                let abs_val = -value;
                let minus_span = SimpleSpan::new(span.start(), span.start() + 1);
                let literal_span = SimpleSpan::new(span.start() + 1, span.end());
                result.push((Minus, minus_span));
                result.push((Arcana(abs_val), literal_span));
                prev_token = Some(Arcana(abs_val));
            }
            Aether(value)
                if value < OrderedFloat::from(0.0) && !allows_unary(prev_token.as_ref()) =>
            {
                let abs_val = OrderedFloat::from(value.into_inner().abs());
                let minus_span = SimpleSpan::new(span.start(), span.start() + 1);
                let literal_span = SimpleSpan::new(span.start() + 1, span.end());
                result.push((Minus, minus_span));
                result.push((Aether(abs_val), literal_span));
                prev_token = Some(Aether(abs_val));
            }
            other => {
                prev_token = Some(other.clone());
                result.push((other, span));
            }
        }
    }

    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use ordered_float::OrderedFloat;

    fn span(start: usize, end: usize) -> SimpleSpan<usize> {
        SimpleSpan::new(start, end)
    }

    #[test]
    fn splits_negative_arcana_after_value_tokens() {
        let tokens = vec![
            (Token::Arcana(1), span(0, 1)),
            (Token::Arcana(-2), span(1, 3)),
        ];

        let normalized = normalize_negative_literals(tokens);

        let expected = vec![
            (Token::Arcana(1), span(0, 1)),
            (Token::Minus, span(1, 2)),
            (Token::Arcana(2), span(2, 3)),
        ];

        assert_eq!(normalized, expected);
    }

    #[test]
    fn keeps_unary_negative_literals_intact_after_operators() {
        let tokens = vec![(Token::Minus, span(0, 1)), (Token::Arcana(-2), span(1, 3))];

        let normalized = normalize_negative_literals(tokens.clone());

        assert_eq!(normalized, tokens);
    }

    #[test]
    fn splits_negative_aether_after_closing_delimiters() {
        let tokens = vec![
            (Token::CloseParen, span(0, 1)),
            (Token::Aether(OrderedFloat::from(-1.25)), span(1, 5)),
        ];

        let normalized = normalize_negative_literals(tokens);

        let expected = vec![
            (Token::CloseParen, span(0, 1)),
            (Token::Minus, span(1, 2)),
            (Token::Aether(OrderedFloat::from(1.25)), span(2, 5)),
        ];

        assert_eq!(normalized, expected);
    }
}