rsaeb 0.4.1

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

use crate::allocation::{AllocationContext, AllocationError, AllocationErrorKind};

use super::{
    AebError, FallibleTraceSnapshotRunError, InputColumn, InputError, LeftModifierKind, LimitError,
    ParseError, ParseErrorKind, ParseErrorLocation, PayloadKind, RightActionKind, RunError,
    StateLimitContext, StateSizeError, TraceSnapshotError, TraceSnapshotRunError, TracedRunError,
};

impl fmt::Display for AllocationContext {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ProgramCodeLine => f.write_str("program code line"),
            Self::ProgramPayload => f.write_str("program payload"),
            Self::ProgramRuleTable => f.write_str("program rule table"),
            Self::CanonicalSource => f.write_str("canonical source bytes"),
            Self::RuntimeInput => f.write_str("runtime input state"),
            Self::RuntimeOnceRuleState => f.write_str("runtime once rule state"),
            Self::RuntimeRewriteState => f.write_str("runtime rewrite state"),
            Self::PayloadView => f.write_str("payload view"),
            Self::RuntimeStateView => f.write_str("runtime state view"),
            Self::FinalOutput => f.write_str("final output"),
            Self::ReturnOutput => f.write_str("return output"),
            Self::TraceSnapshot => f.write_str("trace snapshot"),
        }
    }
}

impl fmt::Display for AllocationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.kind() {
            AllocationErrorKind::CapacityOverflow => {
                write!(
                    f,
                    "allocation capacity overflow while building {}",
                    self.context(),
                )
            }
            AllocationErrorKind::ReserveFailed { requested_capacity } => {
                write!(
                    f,
                    "allocation failure while building {}; requested capacity: {}",
                    self.context(),
                    requested_capacity,
                )
            }
        }
    }
}

impl fmt::Display for AebError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Parse(error) => error.fmt(f),
            Self::Input(error) => error.fmt(f),
            Self::Run(error) => error.fmt(f),
        }
    }
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.location() {
            ParseErrorLocation::Line(line) => write!(f, "parse error at line {}", line.get())?,
            ParseErrorLocation::Position(position) => write!(
                f,
                "parse error at line {}, column {}",
                position.line().get(),
                position.column().get()
            )?,
        }

        write!(f, ": {}", self.kind())
    }
}

impl fmt::Display for ParseErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Allocation(error) => error.fmt(f),
            Self::NonAsciiInCode { byte } => {
                write!(f, "non-ASCII byte 0x{:02x} in code", byte.get())
            }
            Self::NonPrintableAsciiInCode { byte } => {
                write!(f, "non-printable ASCII byte 0x{:02x} in code", byte.get())
            }
            Self::MissingEquals => f.write_str("missing '='"),
            Self::MultipleEquals => f.write_str("multiple '=' characters are not allowed"),
            Self::ReservedSyntaxInPayload { byte, payload_kind } => write!(
                f,
                "reserved syntax byte '{}' in {payload_kind}",
                printable_ascii(byte.get()),
            ),
            Self::UnsupportedLeftModifierOrder { modifier } => write!(
                f,
                "duplicated or unsupported left-side modifier order at {modifier}"
            ),
            Self::UnsupportedRightActionSyntax { action } => {
                write!(
                    f,
                    "nested or unsupported right-side action syntax at {action}"
                )
            }
        }
    }
}

impl fmt::Display for LeftModifierKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Once => f.write_str("(once)"),
            Self::Start => f.write_str("(start)"),
            Self::End => f.write_str("(end)"),
        }
    }
}

impl fmt::Display for RightActionKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Start => f.write_str("(start)"),
            Self::End => f.write_str("(end)"),
            Self::Return => f.write_str("(return)"),
        }
    }
}

impl fmt::Display for PayloadKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::LeftSideData => f.write_str("left-side data"),
            Self::RightSideData => f.write_str("right-side data"),
            Self::RightSideMoveStartPayload => f.write_str("right-side move-to-start payload"),
            Self::RightSideMoveEndPayload => f.write_str("right-side move-to-end payload"),
            Self::RightSideReturnPayload => f.write_str("right-side return payload"),
        }
    }
}

impl fmt::Display for RunError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Allocation(error) => error.fmt(f),
            Self::StateSize(error) => error.fmt(f),
            Self::Limit(error) => error.fmt(f),
        }
    }
}

impl<E> fmt::Display for TracedRunError<E>
where
    E: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Run(error) => error.fmt(f),
            Self::Trace(error) => write!(f, "trace callback failed: {error}"),
        }
    }
}

impl fmt::Display for TraceSnapshotError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Limit {
                limit,
                attempted_len,
            } => write!(
                f,
                "trace snapshot limit exceeded; attempted length: {attempted_len}, limit: {}",
                limit.get(),
            ),
            Self::Allocation(error) => error.fmt(f),
        }
    }
}

impl fmt::Display for TraceSnapshotRunError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Run(error) => error.fmt(f),
            Self::Snapshot(error) => error.fmt(f),
        }
    }
}

impl<E> fmt::Display for FallibleTraceSnapshotRunError<E>
where
    E: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Run(error) => error.fmt(f),
            Self::Snapshot(error) => error.fmt(f),
            Self::Trace(error) => write!(f, "trace callback failed: {error}"),
        }
    }
}

impl fmt::Display for InputError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NonAscii { column, byte } => write!(
                f,
                "input error: non-ASCII byte 0x{:02x} at column {column}",
                byte.get(),
            ),
            Self::Allocation(error) => error.fmt(f),
        }
    }
}

impl fmt::Display for InputColumn {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.get().fmt(f)
    }
}

impl fmt::Display for StateSizeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "state size failure: replacing {} bytes in a {} byte state with {} bytes",
            self.lhs_len(),
            self.state_len(),
            self.rhs_len(),
        )
    }
}

impl fmt::Display for StateLimitContext {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Input => f.write_str("runtime input"),
            Self::Rewrite => f.write_str("rewrite result"),
        }
    }
}

impl fmt::Display for LimitError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::State {
                context,
                limit,
                attempted_len,
            } => write!(
                f,
                "state limit exceeded by {context}; attempted length: {attempted_len}, limit: {}",
                limit.get(),
            ),
            Self::Return {
                limit,
                attempted_len,
            } => write!(
                f,
                "return output limit exceeded; attempted length: {attempted_len}, limit: {}",
                limit.get(),
            ),
            Self::Step {
                max_steps,
                completed_steps,
                state_len,
            } => write!(
                f,
                "step limit exceeded after {} steps; max steps: {}, state length: {state_len} bytes",
                completed_steps.get(),
                max_steps.get(),
            ),
        }
    }
}

fn printable_ascii(byte: u8) -> char {
    if byte.is_ascii() {
        byte as char
    } else {
        '\u{fffd}'
    }
}

#[cfg(test)]
mod tests {
    use alloc::string::ToString;

    use crate::test_support::{
        TestResult, ensure_eq, expect_input_error, expect_parse_error, expect_run_error,
        expect_state_limit, expect_step_limit, runtime_input,
    };
    use crate::{
        AllocationContext, AllocationError, Program, ReturnByteLimit, RunLimits, RuntimeInput,
        StateByteLimit, StepLimit,
    };

    #[test]
    fn allocation_display_names_the_failed_context_and_capacity() -> TestResult {
        let error = AllocationError::reserve_failed(AllocationContext::TraceSnapshot, 123);

        ensure_eq(
            error.to_string(),
            "allocation failure while building trace snapshot; requested capacity: 123",
        )?;

        let error = AllocationError::reserve_failed(AllocationContext::RuntimeStateView, 456);

        ensure_eq(
            error.to_string(),
            "allocation failure while building runtime state view; requested capacity: 456",
        )?;

        let error = AllocationError::capacity_overflow(AllocationContext::CanonicalSource);

        ensure_eq(
            error.to_string(),
            "allocation capacity overflow while building canonical source bytes",
        )?;
        Ok(())
    }

    #[test]
    fn parse_error_display_includes_line_column_and_structured_reason() -> TestResult {
        let error = expect_parse_error("a=b=c")?;

        ensure_eq(
            error.to_string(),
            "parse error at line 1, column 4: multiple '=' characters are not allowed",
        )?;
        Ok(())
    }

    #[test]
    fn input_error_display_keeps_byte_and_original_column() -> TestResult {
        let error = expect_input_error(RuntimeInput::parse(&[0xff]))?;

        ensure_eq(
            error.to_string(),
            "input error: non-ASCII byte 0xff at column 1",
        )?;
        Ok(())
    }

    #[test]
    fn state_limit_display_names_context_attempted_length_and_limit() -> TestResult {
        let limits = RunLimits::bounded(
            StepLimit::new(10),
            StateByteLimit::new(1),
            ReturnByteLimit::new(10),
        );
        let error = expect_run_error(
            Program::parse_str("# no executable rules")?.run(RuntimeInput::parse(b"aa")?, limits),
        )?;
        let error = expect_state_limit(error)?;

        ensure_eq(
            error.to_string(),
            "state limit exceeded by runtime input; attempted length: 2, limit: 1",
        )?;
        Ok(())
    }

    #[test]
    fn step_limit_display_reports_limit_and_preserved_state_len() -> TestResult {
        let program = Program::parse_str("a=b")?;
        let limits = RunLimits::new(StepLimit::new(0));
        let error = expect_run_error(program.run(runtime_input(b"a")?, limits))?;
        let error = expect_step_limit(error)?;

        ensure_eq(
            error.to_string(),
            "step limit exceeded after 0 steps; max steps: 0, state length: 1 bytes",
        )?;
        Ok(())
    }
}