Skip to main content

Crate rsaeb

Crate rsaeb 

Source
Expand description

Byte-oriented interpreter for A=B ordered rewrite programs.

This page is the canonical API guide. The README is the package entry point and language overview, while the project wiki is a short use-case navigation layer. This page focuses on exact public Rust surfaces and the typed boundaries a host program should use.

rsaeb is a no_std + alloc library crate. It parses compact A=B source into an immutable program::Program, validates host bytes as input::RuntimeInput, admits that input into a one-run input::RunSeed, and executes only after limits::ExecutionLimits are attached. The interpreter core does not read files, use process arguments, access environment variables, write stdout/stderr, or perform lossy byte-to-text display conversion.

The public API is intentionally arranged as boundary types rather than root re-exports. A host should move data through the domains in this order: source bytes become a parsed program, raw input bytes become validated runtime input, validated input becomes an admitted run seed, and only then can execution or tracing start. That ordering keeps parse errors, input validation errors, run-admission errors, runtime failures, and trace-sink failures separate in both type signatures and diagnostics.

§API map

Use these public entry points according to the boundary being crossed:

§Typed boundaries

Program source and runtime input are different byte domains. Program payload bytes are printable executable syntax bytes accepted by the parser. Runtime input accepts any ASCII byte, including whitespace, control bytes, and bytes that are reserved syntax in program source. Construct both explicitly before execution so parsing, input validation, and runtime failures remain distinguishable in the type system.

input::RunSeed is the admission witness for one run. It consumes a validated input::RuntimeInput together with limits::ExecutionLimits, checks the initial runtime-state budget, and prevents a later execution API from receiving raw bytes or detached budget values. execution::RuleAttemptSeed extends that admission witness with a limits::RuleAttemptLimit when a host needs rule-line attempt stepping.

§Basic execution

Parse source::ProgramSource, validate input::RuntimeInput, then admit an input::RunSeed before running:

use rsaeb::limits::{
    DEFAULT_MAX_INPUT_LEN, DEFAULT_PARSE_LIMITS, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN, DEFAULT_MAX_STEPS,
};
use rsaeb::input::{RunSeed, RuntimeInput, RuntimeInputSource};
use rsaeb::limits::{ExecutionLimits, RuntimeInputLimits};
use rsaeb::program::{Program, RunOutcome};
use rsaeb::source::ProgramSource;

let program = Program::parse(ProgramSource::from_text("a=b"), DEFAULT_PARSE_LIMITS)?;
let input_limits = RuntimeInputLimits::new(DEFAULT_MAX_INPUT_LEN);
let execution_limits = ExecutionLimits::new(
    DEFAULT_MAX_STEPS,
    DEFAULT_MAX_STATE_LEN,
    DEFAULT_MAX_RETURN_LEN,
);
let input = RuntimeInput::validate(RuntimeInputSource::from_bytes(b"a"), input_limits)?;
let seed = RunSeed::admit(input, execution_limits)?;
let result = program.run(seed)?;

if !matches!(
    result.outcome(),
    RunOutcome::Stable(output) if output.as_slice() == b"b"
) {
    return Err("unexpected stable output".into());
}

Parse program::Program once when the same rules should be reused. The parser assigns private slots to (once) rules, and each runtime invocation owns only those per-run slot states rather than mutating the parsed program:

use rsaeb::limits::{
    DEFAULT_MAX_INPUT_LEN, DEFAULT_PARSE_LIMITS, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN,
    ExecutionLimits, RuntimeInputLimits, StepLimit,
};
use rsaeb::input::{RunSeed, RuntimeInput, RuntimeInputSource};
use rsaeb::program::{Program, RunOutcome};
use rsaeb::source::ProgramSource;

let program = Program::parse(ProgramSource::from_text("(once)a=b\na=c"), DEFAULT_PARSE_LIMITS)?;
let input_limits = RuntimeInputLimits::new(DEFAULT_MAX_INPUT_LEN);
let execution_limits = ExecutionLimits::new(
    StepLimit::new(10_000),
    DEFAULT_MAX_STATE_LEN,
    DEFAULT_MAX_RETURN_LEN,
);

let first_input = RuntimeInput::validate(RuntimeInputSource::from_bytes(b"aa"), input_limits)?;
let second_input = RuntimeInput::validate(RuntimeInputSource::from_bytes(b"aa"), input_limits)?;

let first = program.run(RunSeed::admit(first_input, execution_limits)?)?;
let second = program.run(RunSeed::admit(second_input, execution_limits)?)?;

if !matches!(
    first.outcome(),
    RunOutcome::Stable(output) if output.as_slice() == b"bc"
) {
    return Err("unexpected first output".into());
}
if !matches!(
    second.outcome(),
    RunOutcome::Stable(output) if output.as_slice() == b"bc"
) {
    return Err("unexpected second output".into());
}

§Stepwise execution

Use program::Program::start_run when a host wants to wait after each applied rule while keeping the parsed program reusable:

use rsaeb::execution::BorrowedStepTransition;
use rsaeb::limits::{
    DEFAULT_MAX_INPUT_LEN, DEFAULT_PARSE_LIMITS, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN, StepLimit,
};
use rsaeb::input::{RuntimeInput, RuntimeInputSource};
use rsaeb::input::RunSeed;
use rsaeb::limits::{ExecutionLimits, RuntimeInputLimits};
use rsaeb::program::Program;
use rsaeb::source::ProgramSource;

let program = Program::parse(ProgramSource::from_text("a=b\nb=c"), DEFAULT_PARSE_LIMITS)?;
let input_limits = RuntimeInputLimits::new(DEFAULT_MAX_INPUT_LEN);
let execution_limits = ExecutionLimits::new(
    StepLimit::new(10),
    DEFAULT_MAX_STATE_LEN,
    DEFAULT_MAX_RETURN_LEN,
);
let input = RuntimeInput::validate(RuntimeInputSource::from_bytes(b"a"), input_limits)?;
let seed = RunSeed::admit(input, execution_limits)?;
let execution = program.start_run(seed)?;

let execution = match execution.step() {
    BorrowedStepTransition::Applied(applied) => {
        if applied.rule().position().number().get() != 1 {
            return Err("unexpected first applied rule".into());
        }
        if applied.state().materialize()?.as_slice() != b"b" {
            return Err("unexpected first applied state".into());
        }
        applied.into_session()
    }
    BorrowedStepTransition::Stable(_) | BorrowedStepTransition::Returned(_) | BorrowedStepTransition::Failed(_) => {
        return Err("expected first applied step".into());
    }
};

let execution = match execution.step() {
    BorrowedStepTransition::Applied(applied) => {
        if applied.rule().position().number().get() != 2 {
            return Err("unexpected second applied rule".into());
        }
        if applied.state().materialize()?.as_slice() != b"c" {
            return Err("unexpected second applied state".into());
        }
        applied.into_session()
    }
    BorrowedStepTransition::Stable(_) | BorrowedStepTransition::Returned(_) | BorrowedStepTransition::Failed(_) => {
        return Err("expected second applied step".into());
    }
};

match execution.step() {
    BorrowedStepTransition::Stable(stable) => {
        if stable.steps().get() != 2 {
            return Err("unexpected stable step count".into());
        }
        if stable.state().materialize()?.as_slice() != b"c" {
            return Err("unexpected stable state".into());
        }
    }
    BorrowedStepTransition::Applied(_) | BorrowedStepTransition::Returned(_) | BorrowedStepTransition::Failed(_) => {
        return Err("expected stable completion".into());
    }
}

A execution::BorrowedStepTransition::Failed value is terminal. It exposes the uncommitted state for diagnostics, then lets callers discard the failed run into its error::RunError; it does not expose a retryable session. execution::OwnedStepTransition::Failed carries the same error and uncommitted-state diagnostics for owned sessions, and it can split into the runtime error plus the parsed program when ownership matters. Failed transitions are terminal; recovering the program never recovers a retryable session. Borrowed applied and returned transitions carry inspect::RuleView witnesses; owned transitions retain execution::OwnedRuleWitness values so rule metadata remains available after ownership moves. Owned non-terminal applied and missed transitions also expose into_parts methods so callers can keep the owned witness and the continuation session together.

Use program::Program::start_rule_attempt_run when the host needs to observe every executable rule line, including lines that do not apply to the current runtime state:

use rsaeb::execution::{BorrowedRuleAttemptTransition, RuleAttemptSeed, RuleMissReason};
use rsaeb::limits::{
    DEFAULT_MAX_INPUT_LEN, DEFAULT_PARSE_LIMITS, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN,
    RuleAttemptLimit, StepLimit,
};
use rsaeb::input::{RuntimeInput, RuntimeInputSource};
use rsaeb::input::RunSeed;
use rsaeb::limits::{ExecutionLimits, RuntimeInputLimits};
use rsaeb::program::Program;
use rsaeb::source::ProgramSource;

let program = Program::parse(ProgramSource::from_text("z=x\na=b"), DEFAULT_PARSE_LIMITS)?;
let input_limits = RuntimeInputLimits::new(DEFAULT_MAX_INPUT_LEN);
let execution_limits = ExecutionLimits::new(
    StepLimit::new(10),
    DEFAULT_MAX_STATE_LEN,
    DEFAULT_MAX_RETURN_LEN,
);
let input = RuntimeInput::validate(RuntimeInputSource::from_bytes(b"a"), input_limits)?;
let seed = RunSeed::admit(input, execution_limits)?;
let attempt_seed = RuleAttemptSeed::new(seed, RuleAttemptLimit::new(10));
let execution = program.start_rule_attempt_run(attempt_seed)?;

let execution = match execution.step() {
    BorrowedRuleAttemptTransition::Missed(missed) => {
        if missed.miss().reason() != RuleMissReason::StateMismatch {
            return Err("unexpected miss reason".into());
        }
        if missed.miss().rule().position().number().get() != 1 {
            return Err("unexpected missed rule".into());
        }
        missed.into_session()
    }
    BorrowedRuleAttemptTransition::Applied(_)
    | BorrowedRuleAttemptTransition::Stable(_)
    | BorrowedRuleAttemptTransition::Returned(_)
    | BorrowedRuleAttemptTransition::Failed(_) => return Err("expected first rule to miss".into()),
};

match execution.step() {
    BorrowedRuleAttemptTransition::Applied(applied) => {
        if applied.step().get() != 1 || applied.rule().position().number().get() != 2 {
            return Err("unexpected applied rule attempt".into());
        }
    }
    BorrowedRuleAttemptTransition::Missed(_)
    | BorrowedRuleAttemptTransition::Stable(_)
    | BorrowedRuleAttemptTransition::Returned(_)
    | BorrowedRuleAttemptTransition::Failed(_) => return Err("expected second rule to apply".into()),
}

§Limits

limits::RuntimeInputLimits carries input-byte validation policy. limits::ExecutionLimits carries initial runtime-state admission, the step budget, and byte budgets for rewrite states and (return) outputs. execution::RuleAttemptSeed binds one run seed to the limits::RuleAttemptLimit for the separate rule-line attempt mode. Trace snapshot materialization uses an explicit limits::TraceSnapshotByteLimit. Step limits are reserved before rewrite or return-output materialization when another matching rule would apply after the configured number of completed steps:

use rsaeb::error::{RunError, RunFinishError, RunStepError};
use rsaeb::limits::{
    DEFAULT_MAX_INPUT_LEN, DEFAULT_PARSE_LIMITS, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN, StepLimit,
};
use rsaeb::input::{RuntimeInput, RuntimeInputSource};
use rsaeb::input::RunSeed;
use rsaeb::limits::{ExecutionLimits, RuntimeInputLimits};
use rsaeb::program::Program;
use rsaeb::source::ProgramSource;

let input_limits = RuntimeInputLimits::new(DEFAULT_MAX_INPUT_LEN);
let execution_limits = ExecutionLimits::new(
    StepLimit::new(0),
    DEFAULT_MAX_STATE_LEN,
    DEFAULT_MAX_RETURN_LEN,
);
let input = RuntimeInput::validate(RuntimeInputSource::from_bytes(b"a"), input_limits)?;
let seed = RunSeed::admit(input, execution_limits)?;
let result = Program::parse(ProgramSource::from_text("a=b"), DEFAULT_PARSE_LIMITS)?.run(seed);

if !matches!(
    result,
    Err(RunError::Finish(RunFinishError::Step(RunStepError::StepLimit(error))))
        if error.completed_steps().get() == 0
) {
    return Err("unexpected step-limit error".into());
}

§Rule inspection

Parsed rules are exposed as borrowed structured views, not as stored source strings:

use rsaeb::limits::DEFAULT_PARSE_LIMITS;
use rsaeb::inspect::{RuleAction, RuleAnchor, RuleRepeat};
use rsaeb::program::Program;
use rsaeb::source::ProgramSource;

let program = Program::parse(ProgramSource::from_text("( once ) ( start ) a = ( end ) b # comment"), DEFAULT_PARSE_LIMITS)?;
let rule = program.rules().next().ok_or("missing parsed rule")?;

if rule.repeat() != RuleRepeat::Once {
    return Err("unexpected repeat".into());
}
if rule.anchor() != RuleAnchor::Start {
    return Err("unexpected anchor".into());
}
if rule.lhs().materialize()?.as_slice() != b"a" {
    return Err("unexpected left side".into());
}
match rule.action() {
    RuleAction::MoveEnd(payload) => {
        if payload.materialize()?.as_slice() != b"b" {
            return Err("unexpected moved payload".into());
        }
    }
    RuleAction::Replace(_) | RuleAction::MoveStart(_) | RuleAction::Return(_) => {
        return Err("expected move-end action".into());
    }
}
if rule.canonical_source()?.as_slice() != b"(once)(start)a=(end)b" {
    return Err("unexpected canonical source".into());
}

§Tracing

Borrowed trace events allocate no snapshots. Snapshot tracing is layered on top when a caller needs owned event bytes:

use core::convert::Infallible;
use rsaeb::limits::{
    DEFAULT_MAX_INPUT_LEN, DEFAULT_PARSE_LIMITS, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN, StepLimit,
};
use rsaeb::trace::BorrowedTraceEvent;
use rsaeb::input::{RuntimeInput, RuntimeInputSource};
use rsaeb::input::RunSeed;
use rsaeb::limits::{ExecutionLimits, RuntimeInputLimits};
use rsaeb::program::Program;
use rsaeb::source::ProgramSource;

let program = Program::parse(ProgramSource::from_text("a=b\nb=(return)ok"), DEFAULT_PARSE_LIMITS)?;
let mut byte_counts = Vec::new();
let input_limits = RuntimeInputLimits::new(DEFAULT_MAX_INPUT_LEN);
let execution_limits = ExecutionLimits::new(
    StepLimit::new(10),
    DEFAULT_MAX_STATE_LEN,
    DEFAULT_MAX_RETURN_LEN,
);
let input = RuntimeInput::validate(RuntimeInputSource::from_bytes(b"a"), input_limits)?;
let seed = RunSeed::admit(input, execution_limits)?;

program.run_with_borrowed_trace(
    seed,
    |event| {
        byte_counts.push(event.byte_count().get());
        if let BorrowedTraceEvent::Step { rule, .. } = event {
            let _line = rule.line_number();
        }
        Ok::<(), Infallible>(())
    },
)?;

if byte_counts != [1, 1, 2] {
    return Err("unexpected trace byte counts".into());
}

Snapshot tracing materializes owned event bytes under an explicit snapshot byte budget, which lets the caller retain events after each callback returns:

use rsaeb::limits::{
    DEFAULT_MAX_INPUT_LEN, DEFAULT_PARSE_LIMITS, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN,
    DEFAULT_MAX_TRACE_SNAPSHOT_LEN, StepLimit,
};
use rsaeb::trace::{TraceSnapshotEffect, TraceSnapshotEvent};
use rsaeb::input::{RuntimeInput, RuntimeInputSource};
use rsaeb::input::RunSeed;
use rsaeb::limits::{ExecutionLimits, RuntimeInputLimits};
use rsaeb::program::Program;
use rsaeb::source::ProgramSource;

let program = Program::parse(ProgramSource::from_text("a=b\nb=(return)ok"), DEFAULT_PARSE_LIMITS)?;
let input_limits = RuntimeInputLimits::new(DEFAULT_MAX_INPUT_LEN);
let execution_limits = ExecutionLimits::new(
    StepLimit::new(10),
    DEFAULT_MAX_STATE_LEN,
    DEFAULT_MAX_RETURN_LEN,
);
let input = RuntimeInput::validate(RuntimeInputSource::from_bytes(b"a"), input_limits)?;
let seed = RunSeed::admit(input, execution_limits)?;
let mut states = Vec::new();
let mut returns = Vec::new();

program.run_with_trace_snapshots(seed, DEFAULT_MAX_TRACE_SNAPSHOT_LEN, |event| {
    match event {
        TraceSnapshotEvent::Initial { state } => states.push(state.into_raw_bytes()),
        TraceSnapshotEvent::Step {
            effect: TraceSnapshotEffect::Continue { state },
            ..
        } => states.push(state.into_raw_bytes()),
        TraceSnapshotEvent::Step {
            effect: TraceSnapshotEffect::Return { output },
            ..
        } => returns.push(output.into_raw_bytes()),
    }
    Ok::<(), core::convert::Infallible>(())
})?;

if states != [b"a".to_vec(), b"b".to_vec()] {
    return Err("unexpected trace states".into());
}
if returns != [b"ok".to_vec()] {
    return Err("unexpected trace returns".into());
}

§Error model

Source parsing, runtime input validation, runtime execution, trace snapshot materialization, and user trace-sink failures are reported with structured error types such as error::ParseError, error::RuntimeInputError, error::RunError, error::AllocationError, error::TraceSnapshotError, error::TraceSnapshotRunError, and error::TracedRunError. Allocation reservation failures include a typed error::RequestedCapacity instead of only a formatted string. Representation failures are distinct from allocation failures, and runtime contradictions that public construction paths cannot express are eliminated by typed witnesses instead of becoming hidden panics or display-only errors.

use rsaeb::error::RuntimeInputError;
use rsaeb::limits::{RuntimeInputByteLimit, RuntimeInputLimits};
use rsaeb::input::{RuntimeInput, RuntimeInputSource};

fn validate_host_input(bytes: &[u8]) -> Result<RuntimeInput, RuntimeInputError> {
    let limits = RuntimeInputLimits::new(RuntimeInputByteLimit::new(4));
    RuntimeInput::validate(RuntimeInputSource::from_bytes(bytes), limits)
}

let Err(error) = validate_host_input(&[0xff]) else {
    return Err("expected non-ASCII input to fail".into());
};

if !matches!(
    error,
    RuntimeInputError::NonAscii { column, byte }
        if column.get() == 1 && byte.get() == 0xff
) {
    return Err("unexpected input error".into());
}

Modules§

error
Structured error types for parsing, input validation, running, and tracing.
execution
Public stepwise run typestates.
input
Runtime-input boundary types.
inspect
Borrowed inspection views for parsed rules and payloads.
limits
Runtime budgets and public byte-count value types.
program
Parsed program and run-to-completion result types.
source
Program-source boundary and source-position value types.
trace
Borrowed and snapshot trace event types.