rsaeb 0.9.0

A no_std + alloc interpreter for A=B ordered rewrite programs.
Documentation
use alloc::vec::Vec;

use super::*;
use crate::error::{ParseError, ParseErrorKind, PayloadKind};
use crate::source::SourceLineNumber;
use crate::test_support::{
    TestFailure, TestResult, ensure_eq, ensure_matches, expect_error_position, source_column,
    source_line_number,
};

/// Returns one classified runtime byte after crossing the validation boundary.
///
/// # Errors
///
/// Returns `TestFailure` if the byte is rejected by runtime input validation.
fn validated_runtime_byte(byte: u8) -> Result<RuntimeByte, TestFailure> {
    RuntimeInputByte::validate(byte, 0)
        .map(RuntimeInputByte::into_runtime_byte)
        .map_err(TestFailure::from)
}

/// Parses payload bytes and returns the expected parse error.
///
/// # Errors
///
/// Returns `TestFailure` if the invalid payload is accepted.
fn parse_payload_error(
    input: &[CompactByte],
    line_number: SourceLineNumber,
    payload_kind: PayloadKind,
) -> Result<ParseError, TestFailure> {
    match PayloadSyntax::new(input, line_number, payload_kind).validate() {
        Ok(_) => Err(TestFailure::message("invalid payload bytes were accepted")),
        Err(error) => Ok(error),
    }
}

/// Parses payload bytes through the validated payload syntax boundary.
///
/// # Errors
///
/// Returns `TestFailure` if the bytes are not valid payload syntax.
fn parse_payload(
    input: &[CompactByte],
    line_number: SourceLineNumber,
    payload_kind: PayloadKind,
) -> Result<Payload, TestFailure> {
    Ok(PayloadSyntax::new(input, line_number, payload_kind)
        .validate()?
        .into_payload())
}

/// # Errors
///
/// Returns `TestFailure` if reserved syntax bytes are not rejected with
/// structured payload errors.
#[test]
fn payload_rejects_every_reserved_syntax_byte_even_if_payload_parser_is_called_directly()
-> TestResult {
    for reserved in [b'=', b'#', b'(', b')'] {
        let compact = [CompactByte::new(reserved, source_column(1)?)];
        let error =
            parse_payload_error(&compact, source_line_number(1)?, PayloadKind::RightSideData)?;

        expect_error_position(&error, 1, 1)?;
        ensure_matches(
            matches!(
                error.kind(),
                ParseErrorKind::ReservedSyntaxInPayload { byte, .. }
                    if byte.get() == reserved
            ),
            "expected concrete reserved syntax byte",
        )?;
        ensure_matches(
            matches!(
                error.kind(),
                ParseErrorKind::ReservedSyntaxInPayload {
                    payload_kind: PayloadKind::RightSideData,
                    ..
                }
            ),
            "expected reserved syntax payload error",
        )?;
    }
    Ok(())
}

/// # Errors
///
/// Returns `TestFailure` if payload validation accepts invalid executable
/// bytes or reports an unexpected error location.
#[test]
fn payload_validates_compact_bytes_at_the_domain_boundary() -> TestResult {
    let non_ascii = [CompactByte::new(0xff, source_column(1)?)];
    let non_graphic = [CompactByte::new(b' ', source_column(2)?)];

    let error = parse_payload_error(
        &non_ascii,
        source_line_number(1)?,
        PayloadKind::RightSideData,
    )?;
    ensure_matches(
        matches!(error.kind(), ParseErrorKind::NonAsciiInCode { .. }),
        "expected non-ASCII parse error",
    )?;

    let error = parse_payload_error(
        &non_graphic,
        source_line_number(1)?,
        PayloadKind::RightSideData,
    )?;
    expect_error_position(&error, 1, 2)?;
    ensure_matches(
        matches!(error.kind(), ParseErrorKind::NonPrintableAsciiInCode { .. }),
        "expected non-printable parse error",
    )?;
    Ok(())
}

/// # Errors
///
/// Returns `TestFailure` if a parsed payload exposes bytes inconsistent
/// with the validated domain value.
#[test]
fn payload_exposes_validated_bytes_without_leaking_the_internal_domain_type() -> TestResult {
    let compact = [
        CompactByte::new(b'a', source_column(1)?),
        CompactByte::new(b'b', source_column(2)?),
    ];
    let payload = parse_payload(&compact, source_line_number(1)?, PayloadKind::LeftSideData)?;

    ensure_eq!(payload.bytes().collect::<Vec<_>>(), b"ab".to_vec())?;
    let PayloadNeedle::NonEmpty(needle) = payload.needle() else {
        return Err(TestFailure::message("expected non-empty payload needle"));
    };
    ensure_eq!(needle.first_byte().get(), b'a')?;
    Ok(())
}

/// # Errors
///
/// Returns `TestFailure` if runtime input byte classification or
/// materialization drifts from the ASCII domain split.
#[test]
fn runtime_input_classifies_program_constructible_and_opaque_ascii_separately() -> TestResult {
    let parsed = validated_runtime_byte(b'a')?;
    ensure_matches(
        matches!(parsed, RuntimeByte::ProgramConstructible(byte) if byte.get() == b'a'),
        "expected program-constructible input byte",
    )?;
    ensure_eq!(parsed.materialize(), b'a')?;

    for byte in [0x00, b' ', b'=', b'#', b'(', b')'] {
        let parsed = validated_runtime_byte(byte)?;
        ensure_eq!(parsed.materialize(), byte)?;
        ensure_matches(
            matches!(parsed, RuntimeByte::Opaque(_)),
            "expected opaque input byte",
        )?;
    }

    Ok(())
}

/// # Errors
///
/// Returns `TestFailure` if runtime input validation depends on reconstructing
/// executable program bytes.
#[test]
fn runtime_input_validation_has_no_reconstruction_invariant() -> TestResult {
    let parsed = validated_runtime_byte(b'a')?;
    ensure_matches(
        matches!(parsed, RuntimeByte::ProgramConstructible(byte) if byte.get() == b'a'),
        "expected ASCII runtime input to validate normally",
    )
}