rsaeb 0.5.0

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

/// Interpreter allocation site reported by [`AllocationError`].
///
/// The value identifies the domain boundary that was allocating, so callers can
/// distinguish parser storage, runtime state growth, final output
/// materialization, and trace snapshot materialization without parsing strings.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocationContext {
    /// Building a compact code-line byte table.
    ProgramCodeLine,
    /// Storing a parsed program payload.
    ProgramPayload,
    /// Building the parsed program rule table.
    ProgramRuleTable,
    /// Building canonical source bytes from structured rule data.
    CanonicalSource,
    /// Materializing validated runtime input into mutable runtime state.
    RuntimeInput,
    /// Storing `(once)` execution state.
    RuntimeOnceRuleState,
    /// Building the next runtime state after a rewrite.
    RuntimeRewriteState,
    /// Materializing a payload view outside parser/runtime execution.
    PayloadView,
    /// Materializing a borrowed runtime-state view outside trace snapshot APIs.
    RuntimeStateView,
    /// Materializing a stable final runtime state as public output bytes.
    FinalOutput,
    /// Materializing `(return)` output bytes.
    ReturnOutput,
    /// Materializing a trace snapshot.
    TraceSnapshot,
}

/// Fallible allocation failure reported instead of silently relying on
/// allocation side effects.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllocationError {
    context: AllocationContext,
    kind: AllocationErrorKind,
}

/// Reason an allocation boundary failed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocationErrorKind {
    /// The required capacity could not be represented as `usize`.
    CapacityOverflow,
    /// The allocator rejected the requested capacity.
    ReserveFailed {
        /// Vector capacity requested at the failing site.
        requested_capacity: usize,
    },
}

impl AllocationError {
    pub(crate) const fn capacity_overflow(context: AllocationContext) -> Self {
        Self {
            context,
            kind: AllocationErrorKind::CapacityOverflow,
        }
    }

    pub(crate) const fn reserve_failed(
        context: AllocationContext,
        requested_capacity: usize,
    ) -> Self {
        Self {
            context,
            kind: AllocationErrorKind::ReserveFailed { requested_capacity },
        }
    }

    /// Allocation site that failed.
    #[must_use]
    pub const fn context(&self) -> AllocationContext {
        self.context
    }

    /// Structured allocation failure reason.
    #[must_use]
    pub const fn kind(&self) -> AllocationErrorKind {
        self.kind
    }

    /// Requested vector capacity, when allocation reached the allocator.
    #[must_use]
    pub const fn requested_capacity(&self) -> Option<usize> {
        match self.kind {
            AllocationErrorKind::CapacityOverflow => None,
            AllocationErrorKind::ReserveFailed { requested_capacity } => Some(requested_capacity),
        }
    }
}

impl Error for AllocationError {}

pub(crate) fn try_reserve_total_exact<T>(
    vec: &mut Vec<T>,
    total_capacity: usize,
    context: AllocationContext,
) -> Result<(), AllocationError> {
    if vec.capacity() >= total_capacity {
        return Ok(());
    }

    let additional = total_capacity
        .checked_sub(vec.len())
        .ok_or_else(|| AllocationError::capacity_overflow(context))?;

    vec.try_reserve_exact(additional)
        .map_err(|_| AllocationError::reserve_failed(context, total_capacity))
}

pub(crate) fn try_push<T>(
    vec: &mut Vec<T>,
    value: T,
    context: AllocationContext,
) -> Result<(), AllocationError> {
    if vec.len() == vec.capacity() {
        let minimum_capacity = vec
            .len()
            .checked_add(1)
            .ok_or_else(|| AllocationError::capacity_overflow(context))?;
        let doubled_capacity = vec.capacity().checked_mul(2).unwrap_or(minimum_capacity);
        let requested_capacity =
            core::cmp::max(minimum_capacity, core::cmp::max(4, doubled_capacity));
        try_reserve_total_exact(vec, requested_capacity, context)?;
    }

    vec.push(value);
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_support::{TestResult, ensure_eq};
    use alloc::string::ToString;

    #[test]
    fn allocation_contexts_are_publicly_inspectable() -> TestResult {
        let error = AllocationError::reserve_failed(AllocationContext::TraceSnapshot, 123);
        ensure_eq!(error.context(), AllocationContext::TraceSnapshot)?;
        ensure_eq!(
            error.kind(),
            AllocationErrorKind::ReserveFailed {
                requested_capacity: 123,
            },
        )?;
        ensure_eq!(error.requested_capacity(), Some(123))?;

        let error = AllocationError::capacity_overflow(AllocationContext::CanonicalSource);
        ensure_eq!(error.context(), AllocationContext::CanonicalSource)?;
        ensure_eq!(error.kind(), AllocationErrorKind::CapacityOverflow)?;
        ensure_eq!(error.requested_capacity(), None)?;
        Ok(())
    }

    #[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",
        )
    }
}