rsaeb 0.5.0

A no_std + alloc interpreter for A=B ordered rewrite programs.
Documentation
use crate::error::{
    LeftModifierKind, ParseErrorKind, ParseErrorLocation, PayloadKind, RightActionKind,
};
use crate::inspect::{RuleActionView, RuleAnchor, RuleCount, RuleRepeat};
use crate::test_support::{
    TestFailure, TestResult, ensure, ensure_eq, ensure_matches, expect_error_position,
    expect_parse_error, source_line_number,
};
use crate::{Program, ProgramSource};

fn expect_rule(
    program: &Program,
    index: usize,
) -> Result<crate::inspect::RuleView<'_>, TestFailure> {
    program
        .rules()
        .nth(index)
        .ok_or(TestFailure::message("expected parsed rule"))
}

#[test]
fn compacting_source_whitespace_and_comments_preserves_rule_domain() -> TestResult {
    let program = Program::parse(ProgramSource::from_str(
        "a b=bb\n\
         a = b # trailing comment\n\
         ( once ) ( start ) x = ( end ) y",
    ))?;

    ensure_eq!(program.rule_count(), RuleCount::new(3))?;
    ensure_eq!(
        expect_rule(&program, 0)?.canonical_source()?,
        b"ab=bb".as_slice(),
    )?;
    ensure_eq!(
        expect_rule(&program, 1)?.canonical_source()?,
        b"a=b".as_slice(),
    )?;
    ensure_eq!(
        expect_rule(&program, 2)?.canonical_source()?,
        b"(once)(start)x=(end)y".as_slice(),
    )?;
    Ok(())
}

#[test]
fn empty_code_lines_and_comments_do_not_become_rules() -> TestResult {
    let program = Program::parse(ProgramSource::from_str(" \t\r\n# comment\n"))?;
    ensure_eq!(program.rule_count(), RuleCount::new(0))
}

#[test]
fn comments_may_contain_non_utf8_bytes_because_source_is_byte_oriented() -> TestResult {
    let program = Program::parse(ProgramSource::from_bytes(b"a=b#\xff\xfe\n"))?;
    let rule = expect_rule(&program, 0)?;

    ensure_eq!(program.rule_count(), RuleCount::new(1))?;
    ensure_eq!(rule.canonical_source()?, b"a=b".as_slice())
}

#[test]
fn code_body_rejects_non_ascii_and_non_printable_bytes_outside_comments() -> TestResult {
    let error = expect_parse_error("a=\u{80}")?;
    ensure_eq!(error.line().get(), 1)?;
    expect_error_position(&error, 1, 3)?;
    ensure_matches(
        matches!(error.kind(), ParseErrorKind::NonAsciiInCode { .. }),
        "expected non-ASCII parse error",
    )?;

    let error = expect_parse_error("a=\0")?;
    ensure_eq!(error.line().get(), 1)?;
    expect_error_position(&error, 1, 3)?;
    ensure_matches(
        matches!(error.kind(), ParseErrorKind::NonPrintableAsciiInCode { .. }),
        "expected non-printable parse error",
    )?;

    ensure(
        Program::parse(ProgramSource::from_bytes(b"a=b#\xff")).is_ok(),
        "expected comment bytes to parse",
    )
}

#[test]
fn equals_and_missing_equals_errors_keep_original_source_locations() -> TestResult {
    let error = expect_parse_error("a=b=c")?;
    expect_error_position(&error, 1, 4)?;
    ensure_matches(
        matches!(error.kind(), ParseErrorKind::MultipleEquals),
        "expected multiple equals parse error",
    )?;

    let error = expect_parse_error("a=b =c")?;
    expect_error_position(&error, 1, 5)?;
    ensure_matches(
        matches!(error.kind(), ParseErrorKind::MultipleEquals),
        "expected multiple equals parse error",
    )?;

    let error = expect_parse_error("abc")?;
    ensure_eq!(
        error.location(),
        ParseErrorLocation::Line(source_line_number(1)?),
    )?;
    ensure_matches(
        matches!(error.kind(), ParseErrorKind::MissingEquals),
        "expected missing equals parse error",
    )
}

#[test]
fn reserved_parentheses_are_rejected_outside_supported_modifier_slots() -> TestResult {
    for source in [
        "a=b(",
        "a=b)",
        "a=b()",
        "a=()",
        "a=b(start)",
        "a=(once)b",
        "a(once)=b",
    ] {
        ensure(
            Program::parse(ProgramSource::from_str(source)).is_err(),
            "source should fail",
        )?;
    }

    ensure(
        Program::parse(ProgramSource::from_str("(once)(start)a=(end)b")).is_ok(),
        "expected valid parenthesized modifiers",
    )?;
    ensure(
        Program::parse(ProgramSource::from_str("a=(return)")).is_ok(),
        "expected empty return payload",
    )
}

#[test]
fn right_side_action_payload_cannot_start_with_another_action() -> TestResult {
    for source in [
        "a=(start)(end)b",
        "a=(start)(return)b",
        "a=(end)(start)b",
        "a=(return)(start)b",
    ] {
        let error = expect_parse_error(source)?;
        ensure_matches(
            matches!(
                error.kind(),
                ParseErrorKind::UnsupportedRightActionSyntax { .. }
            ),
            "expected nested right action syntax error",
        )?;
    }

    let error = expect_parse_error("a=(start)(return)b")?;
    expect_error_position(&error, 1, 10)?;
    ensure_matches(
        matches!(
            error.kind(),
            ParseErrorKind::UnsupportedRightActionSyntax {
                action: RightActionKind::Return,
            }
        ),
        "expected return action syntax error",
    )
}

#[test]
fn payload_and_left_modifier_errors_are_structured() -> TestResult {
    let error = expect_parse_error("a = b (")?;
    expect_error_position(&error, 1, 7)?;
    ensure_matches(
        matches!(
            error.kind(),
            ParseErrorKind::ReservedSyntaxInPayload {
                payload_kind: PayloadKind::RightSideData,
                ..
            }
        ),
        "expected reserved syntax payload error",
    )?;

    let error = expect_parse_error("(start)(once)a=b")?;
    expect_error_position(&error, 1, 8)?;
    ensure_matches(
        matches!(
            error.kind(),
            ParseErrorKind::UnsupportedLeftModifierOrder {
                modifier: LeftModifierKind::Once,
            }
        ),
        "expected left modifier order error",
    )
}

#[test]
fn spaced_source_and_compact_source_parse_to_the_same_rule_view() -> TestResult {
    let compact = Program::parse(ProgramSource::from_str("(once)(start)a=(end)b"))?;
    let spaced = Program::parse(ProgramSource::from_str(
        "( once ) ( start ) a = ( end ) b # comment",
    ))?;
    let compact_rule = expect_rule(&compact, 0)?;
    let spaced_rule = expect_rule(&spaced, 0)?;

    ensure_eq!(compact.rule_count(), RuleCount::new(1))?;
    ensure_eq!(spaced.rule_count(), RuleCount::new(1))?;
    ensure_eq!(spaced_rule.repeat(), RuleRepeat::Once)?;
    ensure_eq!(spaced_rule.anchor(), RuleAnchor::Start)?;
    ensure(spaced_rule.lhs().eq_bytes(b"a"), "expected lhs")?;
    ensure_matches(
        matches!(
            spaced_rule.action(),
            RuleActionView::MoveEnd(payload) if payload.eq_bytes(b"b")
        ),
        "expected move-end action",
    )?;
    ensure_eq!(
        compact_rule.canonical_source()?,
        spaced_rule.canonical_source()?
    )
}