use super::budget::RuntimeBudgetState;
use super::matcher::{RuleSearch, find_next_match};
use super::once::OnceStateSet;
use super::rewrite::RewriteScratch;
use super::state::State;
use crate::bytes::Payload;
use crate::error::{
ReturnOutputLimitError, RunStepError, RuntimeInputError, RuntimeStateLimitError, StepLimitError,
};
use crate::execution::{BorrowedFailedRun, BorrowedStepTransition};
use crate::input::{RuntimeInput, RuntimeInputSource};
use crate::limits::{
DEFAULT_MAX_INPUT_LEN, DEFAULT_MAX_RETURN_LEN, DEFAULT_MAX_STATE_LEN, ReturnByteLimit,
ReturnOutputByteCount, RuntimeInputByteCount, RuntimeInputByteLimit, RuntimeStateByteCount,
RuntimeStateByteLimit, StepCount, StepLimit,
};
use crate::program::RunOutcome;
use crate::runtime::action::prepare_matched_rule;
use crate::test_support::{
TestFailure, TestResult, TestRunPolicy, ensure_eq, ensure_matches, parse_program, run_seed,
};
use crate::trace::RuntimeStateView;
use alloc::vec::Vec;
fn runtime_view_bytes(view: RuntimeStateView<'_>) -> Vec<u8> {
view.materialized_bytes().collect()
}
fn expect_runtime_byte(state: &State, index: usize) -> Result<u8, TestFailure> {
state
.view()
.materialized_bytes()
.nth(index)
.ok_or(TestFailure::message("expected runtime byte"))
}
fn expect_payload_byte(payload: &Payload, index: usize) -> Result<u8, TestFailure> {
payload
.bytes()
.nth(index)
.ok_or(TestFailure::message("expected payload byte"))
}
fn expect_step_limit(error: RunStepError) -> Result<StepLimitError, TestFailure> {
match error {
RunStepError::StepLimit(error) => Ok(error),
RunStepError::Allocation(_)
| RunStepError::RewriteSize(_)
| RunStepError::RuntimeStateLimit(_)
| RunStepError::ReturnOutputLimit(_)
| RunStepError::RuleRuntimeState(_) => {
Err(TestFailure::message("expected step limit error"))
}
}
}
fn expect_step_error<'program>(
result: BorrowedStepTransition<'program>,
) -> Result<BorrowedFailedRun<'program>, TestFailure> {
match result {
BorrowedStepTransition::Failed(failed) => Ok(failed),
BorrowedStepTransition::Applied(_)
| BorrowedStepTransition::Stable(_)
| BorrowedStepTransition::Returned(_) => Err(TestFailure::message("expected step error")),
}
}
fn expect_step_transition<'program>(
result: BorrowedStepTransition<'program>,
) -> Result<BorrowedStepTransition<'program>, TestFailure> {
match result {
BorrowedStepTransition::Failed(failed) => Err(TestFailure::from(failed.into_error())),
transition => Ok(transition),
}
}
fn state_from_input_bytes(input: &[u8], limits: TestRunPolicy) -> Result<State, TestFailure> {
let (input, _) = run_seed(input, limits)?.into_runtime_parts();
Ok(State::from_input(input))
}
struct OnceRuleFailureExpectation {
source: &'static str,
input: &'static [u8],
limits: TestRunPolicy,
error: RunStepError,
expected_match: &'static str,
expected_availability: &'static str,
}
fn ensure_once_rule_failure_does_not_commit_rule(
expectation: &OnceRuleFailureExpectation,
) -> TestResult {
let program = parse_program(expectation.source)?;
let state = state_from_input_bytes(expectation.input, expectation.limits)?;
let mut budget = RuntimeBudgetState::new(expectation.limits.execution());
let mut scratch = RewriteScratch::new();
let mut once_states = OnceStateSet::new(program.once_rule_count())?;
let matched = match find_next_match(program.rule_scan(), &mut once_states, &state)
.map_err(RunStepError::from)?
{
RuleSearch::Matched(matched) => matched,
RuleSearch::Stable => {
return Err(TestFailure::message(expectation.expected_match));
}
};
let result = prepare_matched_rule(&mut scratch, &mut budget, state.byte_count(), matched);
let Err(error) = result else {
return Err(TestFailure::message(expectation.expected_match));
};
ensure_eq!(error, expectation.error)?;
ensure_eq!(budget.completed_steps(), StepCount::ZERO)?;
ensure_eq!(
runtime_view_bytes(state.view()).as_slice(),
expectation.input
)?;
ensure_matches(
matches!(
find_next_match(program.rule_scan(), &mut once_states, &state)
.map_err(RunStepError::from)?,
RuleSearch::Matched(_)
),
expectation.expected_availability,
)
}
#[test]
fn once_rule_failure_preserves_state_before_step_commit() -> TestResult {
let program = parse_program("(once)a=(return)ok")?;
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
DEFAULT_MAX_STATE_LEN,
ReturnByteLimit::new(1),
);
let input = run_seed(b"a", limits)?;
let runtime = program.start_run(input)?;
let error = expect_step_error(runtime.step())?;
ensure_eq!(
error.error(),
&RunStepError::ReturnOutputLimit(ReturnOutputLimitError::new(
ReturnByteLimit::new(1),
ReturnOutputByteCount::new(2),
)),
)?;
ensure_eq!(error.completed_steps(), StepCount::ZERO)?;
ensure_eq!(
runtime_view_bytes(error.state()).as_slice(),
b"a".as_slice()
)
}
#[test]
fn execution_step_limit_failure_preserves_uncommitted_state() -> TestResult {
let program = parse_program("a=b")?;
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(0),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let no_match_input = run_seed(b"x", limits)?;
let no_match = program.start_run(no_match_input)?;
match expect_step_transition(no_match.step())? {
BorrowedStepTransition::Stable(stable) => {
ensure_eq!(stable.steps().get(), 0)?;
ensure_eq!(
runtime_view_bytes(stable.state()).as_slice(),
b"x".as_slice()
)?;
}
BorrowedStepTransition::Applied(_)
| BorrowedStepTransition::Returned(_)
| BorrowedStepTransition::Failed(_) => {
return Err(TestFailure::message("expected stable completion"));
}
}
let program = parse_program("a=b")?;
let would_match_input = run_seed(b"a", limits)?;
let would_match = program.start_run(would_match_input)?;
let error = expect_step_error(would_match.step())?;
ensure_eq!(
expect_step_limit(error.into_error())?,
StepLimitError::new(
StepLimit::new(0),
StepCount::ZERO,
RuntimeStateByteCount::new(1),
),
)?;
let program = parse_program("a=b")?;
let would_match = program.start_run(run_seed(b"a", limits)?)?;
let error = expect_step_error(would_match.step())?;
ensure_eq!(error.completed_steps(), StepCount::ZERO)?;
ensure_eq!(
runtime_view_bytes(error.state()).as_slice(),
b"a".as_slice(),
)?;
ensure_eq!(
expect_step_limit(error.into_error())?,
StepLimitError::new(
StepLimit::new(0),
StepCount::ZERO,
RuntimeStateByteCount::new(1),
),
)
}
#[test]
fn execution_size_limit_failures_preserve_uncommitted_state() -> TestResult {
let state_limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
RuntimeStateByteLimit::new(2),
ReturnByteLimit::new(10),
);
let state_program = parse_program("=a")?;
let state_input = run_seed(b"aa", state_limits)?;
let state_limited = state_program.start_run(state_input)?;
let state_error = expect_step_error(state_limited.step())?;
ensure_eq!(
state_error.error(),
&RunStepError::RuntimeStateLimit(RuntimeStateLimitError::new(
RuntimeStateByteLimit::new(2),
RuntimeStateByteCount::new(3),
)),
)?;
ensure_eq!(state_error.completed_steps(), StepCount::ZERO)?;
ensure_eq!(
runtime_view_bytes(state_error.state()).as_slice(),
b"aa".as_slice(),
)?;
ensure_eq!(
state_error.into_error(),
RunStepError::RuntimeStateLimit(RuntimeStateLimitError::new(
RuntimeStateByteLimit::new(2),
RuntimeStateByteCount::new(3),
)),
)?;
let return_limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
RuntimeStateByteLimit::new(10),
ReturnByteLimit::new(1),
);
let return_program = parse_program("a=(return)ok")?;
let return_input = run_seed(b"a", return_limits)?;
let return_limited = return_program.start_run(return_input)?;
let return_error = expect_step_error(return_limited.step())?;
ensure_eq!(
return_error.error(),
&RunStepError::ReturnOutputLimit(ReturnOutputLimitError::new(
ReturnByteLimit::new(1),
ReturnOutputByteCount::new(2),
)),
)?;
ensure_eq!(return_error.completed_steps(), StepCount::ZERO)?;
ensure_eq!(
runtime_view_bytes(return_error.state()).as_slice(),
b"a".as_slice(),
)?;
ensure_eq!(
return_error.into_error(),
RunStepError::ReturnOutputLimit(ReturnOutputLimitError::new(
ReturnByteLimit::new(1),
ReturnOutputByteCount::new(2),
)),
)
}
#[test]
fn return_action_bypasses_rewrite_state_mutation_path() -> TestResult {
let program = parse_program("a=(return)ok")?;
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
RuntimeStateByteLimit::new(1),
ReturnByteLimit::new(2),
);
let session = program.start_run(run_seed(b"a", limits)?)?;
match expect_step_transition(session.step())? {
BorrowedStepTransition::Returned(returned) => {
let result = returned.into_result();
ensure_eq!(result.steps().get(), 1)?;
ensure_matches(
matches!(
result.outcome(),
RunOutcome::Return(output) if output.as_slice() == b"ok"
),
"expected return output to bypass rewrite state limit",
)
}
BorrowedStepTransition::Applied(_)
| BorrowedStepTransition::Stable(_)
| BorrowedStepTransition::Failed(_) => {
Err(TestFailure::message("expected return transition"))
}
}
}
#[test]
fn once_limit_failures_do_not_commit_rule() -> TestResult {
let expectations = [
OnceRuleFailureExpectation {
source: "(once)=aa",
input: b"a",
limits: TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
RuntimeStateByteLimit::new(1),
DEFAULT_MAX_RETURN_LEN,
),
error: RunStepError::RuntimeStateLimit(RuntimeStateLimitError::new(
RuntimeStateByteLimit::new(1),
RuntimeStateByteCount::new(3),
)),
expected_match: "expected once rewrite limit failure",
expected_availability: "expected failed once rewrite to remain available",
},
OnceRuleFailureExpectation {
source: "(once)a=b",
input: b"a",
limits: TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(0),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
),
error: RunStepError::StepLimit(StepLimitError::new(
StepLimit::new(0),
StepCount::ZERO,
RuntimeStateByteCount::new(1),
)),
expected_match: "expected once step limit failure",
expected_availability: "expected failed step reservation to leave once rule available",
},
OnceRuleFailureExpectation {
source: "(once)a=(return)ok",
input: b"a",
limits: TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
DEFAULT_MAX_STATE_LEN,
ReturnByteLimit::new(1),
),
error: RunStepError::ReturnOutputLimit(ReturnOutputLimitError::new(
ReturnByteLimit::new(1),
ReturnOutputByteCount::new(2),
)),
expected_match: "expected once return limit failure",
expected_availability: "expected failed once return to remain available",
},
];
for expectation in expectations {
ensure_once_rule_failure_does_not_commit_rule(&expectation)?;
}
Ok(())
}
#[test]
fn once_state_set_is_constructed_from_parser_assigned_slots() -> TestResult {
let program = parse_program("(once)a=b")?;
let mut once_states = OnceStateSet::new(program.once_rule_count())?;
let state = state_from_input_bytes(
b"a",
TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
),
)?;
ensure_matches(
matches!(
find_next_match(program.rule_scan(), &mut once_states, &state)
.map_err(RunStepError::from)?,
RuleSearch::Matched(_)
),
"expected parser-assigned once slot state to keep the rule available",
)
}
#[test]
fn runtime_input_error_is_structured_at_the_runtime_boundary() -> TestResult {
let Err(error) = RuntimeInput::validate(
RuntimeInputSource::from_bytes(b"abc"),
TestRunPolicy::new(
RuntimeInputByteLimit::new(2),
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
)
.input(),
) else {
return Err(TestFailure::message("expected input limit error"));
};
ensure_eq!(
error,
RuntimeInputError::InputLimit {
limit: RuntimeInputByteLimit::new(2),
attempted_len: RuntimeInputByteCount::new(3),
},
)?;
let Err(error) = RuntimeInput::validate(
RuntimeInputSource::from_bytes("a\u{80}".as_bytes()),
TestRunPolicy::new(
RuntimeInputByteLimit::new(1),
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
)
.input(),
) else {
return Err(TestFailure::message(
"expected input limit before byte error",
));
};
ensure_eq!(
error,
RuntimeInputError::InputLimit {
limit: RuntimeInputByteLimit::new(1),
attempted_len: RuntimeInputByteCount::new(3),
},
)?;
let Err(error) = RuntimeInput::validate(
RuntimeInputSource::from_bytes("a\u{80}".as_bytes()),
TestRunPolicy::default().input(),
) else {
return Err(TestFailure::message("expected input error"));
};
ensure_matches(
matches!(
error,
RuntimeInputError::NonAscii { column, .. } if column.get() == 2
),
"expected non-ASCII input error at the original column",
)
}
#[test]
fn internal_code_and_runtime_bytes_are_distinct_domains() -> TestResult {
let program = parse_program("a=b")?;
let payload = program
.rule_scan()
.iter()
.next()
.ok_or(TestFailure::message("expected parsed rule"))?
.lhs();
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(10_000),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let (input, _) = run_seed(b"a=()# ", limits)?.into_runtime_parts();
let state = State::from_input(input);
ensure_eq!(expect_payload_byte(payload, 0)?, b'a')?;
ensure_eq!(expect_runtime_byte(&state, 0)?, b'a')?;
ensure_eq!(expect_runtime_byte(&state, 1)?, b'=')?;
ensure_eq!(expect_runtime_byte(&state, 2)?, b'(')?;
ensure_eq!(expect_runtime_byte(&state, 5)?, b' ')?;
let result = program.run(run_seed(b"a=()# ", limits)?)?;
ensure_matches(
matches!(
result.outcome(),
RunOutcome::Stable(output) if output.as_slice() == b"b=()# "
),
"expected rewrite to leave runtime-only input bytes materialized but unmatched",
)
}
#[test]
fn once_rule_commit_proof_allows_only_one_successful_application() -> TestResult {
let program = parse_program("(once)a=a\na=b")?;
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(10),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let result = program.run(run_seed(b"a", limits)?)?;
ensure_eq!(result.steps().get(), 2)?;
ensure_matches(
matches!(
result.outcome(),
RunOutcome::Stable(output) if output.as_slice() == b"b"
),
"expected consumed once rule to give the later rule a chance",
)
}
#[test]
fn rewrite_action_variants_preserve_runtime_placement() -> TestResult {
for (source, input, expected) in [
("a=x", b"ab".as_slice(), b"xb".as_slice()),
("b=(start)x", b"ab".as_slice(), b"xa".as_slice()),
("a=(end)x", b"ab".as_slice(), b"bx".as_slice()),
] {
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let result = parse_program(source)?.run(run_seed(input, limits)?)?;
ensure_matches(
matches!(
result.outcome(),
RunOutcome::Stable(output) if output.as_slice() == expected
),
"expected rewrite action variant to preserve placement",
)?;
}
Ok(())
}
#[test]
fn empty_payload_matches_keep_anchor_specific_span_placement() -> TestResult {
for (source, expected) in [
("=x", b"xab".as_slice()),
("(start)=x", b"xab".as_slice()),
("(end)=x", b"abx".as_slice()),
] {
let program = parse_program(source)?;
let limits = TestRunPolicy::new(
DEFAULT_MAX_INPUT_LEN,
StepLimit::new(1),
DEFAULT_MAX_STATE_LEN,
DEFAULT_MAX_RETURN_LEN,
);
let session = program.start_run(run_seed(b"ab", limits)?)?;
match expect_step_transition(session.step())? {
BorrowedStepTransition::Applied(applied) => {
ensure_eq!(runtime_view_bytes(applied.state()).as_slice(), expected)?;
}
BorrowedStepTransition::Stable(_)
| BorrowedStepTransition::Returned(_)
| BorrowedStepTransition::Failed(_) => {
return Err(TestFailure::message("expected one empty-payload rewrite"));
}
}
}
Ok(())
}