use alloc::vec::Vec;
use crate::allocation::{AllocationContext, AllocationError, try_push, try_reserve_total_exact};
use crate::bytes::{RuntimeByte, RuntimeStateByteCount, TraceSnapshotByteCount};
use crate::error::TraceSnapshotError;
use crate::program::{ReturnOutput, RuntimeStateSnapshot, StepCount, TraceSnapshotByteLimit};
use crate::rule::{PayloadView, RuleView};
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct RuntimeStateView<'run> {
bytes: &'run [RuntimeByte],
}
impl<'run> RuntimeStateView<'run> {
pub(crate) const fn new(bytes: &'run [RuntimeByte]) -> Self {
Self { bytes }
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.bytes.is_empty()
}
pub fn bytes(self) -> impl Iterator<Item = u8> + 'run {
self.bytes.iter().copied().map(RuntimeByte::materialize)
}
#[must_use]
pub const fn byte_count(self) -> RuntimeStateByteCount {
RuntimeStateByteCount::new(self.bytes.len())
}
pub fn to_vec(self) -> Result<Vec<u8>, AllocationError> {
self.to_vec_with_context(AllocationContext::RuntimeStateView)
}
pub(crate) fn to_vec_with_context(
self,
context: AllocationContext,
) -> Result<Vec<u8>, AllocationError> {
let mut output = Vec::new();
try_reserve_total_exact(&mut output, self.bytes.len(), context)?;
for byte in self.bytes() {
try_push(&mut output, byte, context)?;
}
Ok(output)
}
}
impl core::fmt::Debug for RuntimeStateView<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_list().entries((*self).bytes()).finish()
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TraceSnapshotEffect {
Continue {
state: RuntimeStateSnapshot,
},
Return {
output: ReturnOutput,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BorrowedTraceEffect<'program, 'run> {
Continue {
state: RuntimeStateView<'run>,
},
Return {
output: PayloadView<'program>,
},
}
impl BorrowedTraceEffect<'_, '_> {
#[must_use]
pub fn byte_count(self) -> TraceSnapshotByteCount {
match self {
Self::Continue { state } => TraceSnapshotByteCount::new(state.byte_count().get()),
Self::Return { output } => TraceSnapshotByteCount::new(output.byte_count().get()),
}
}
#[must_use]
pub fn is_empty(self) -> bool {
match self {
Self::Continue { state } => state.is_empty(),
Self::Return { output } => output.is_empty(),
}
}
fn to_snapshot(
self,
limit: TraceSnapshotByteLimit,
) -> Result<TraceSnapshotEffect, TraceSnapshotError> {
ensure_trace_len(self.byte_count(), limit)?;
match self {
Self::Continue { state } => Ok(TraceSnapshotEffect::Continue {
state: RuntimeStateSnapshot::from_vec(
state.to_vec_with_context(AllocationContext::TraceSnapshot)?,
),
}),
Self::Return { output } => Ok(TraceSnapshotEffect::Return {
output: ReturnOutput::from_vec(
output
.to_vec_with_context(AllocationContext::TraceSnapshot)
.map_err(TraceSnapshotError::from)?,
),
}),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TraceSnapshotEvent<'program> {
Initial {
state: RuntimeStateSnapshot,
},
Step {
step: StepCount,
rule: RuleView<'program>,
effect: TraceSnapshotEffect,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BorrowedTraceEvent<'program, 'run> {
Initial {
state: RuntimeStateView<'run>,
},
Step {
step: StepCount,
rule: RuleView<'program>,
effect: BorrowedTraceEffect<'program, 'run>,
},
}
impl<'program> BorrowedTraceEvent<'program, '_> {
#[must_use]
pub fn byte_count(self) -> TraceSnapshotByteCount {
match self {
Self::Initial { state } => TraceSnapshotByteCount::new(state.byte_count().get()),
Self::Step { effect, .. } => effect.byte_count(),
}
}
#[must_use]
pub fn is_empty(self) -> bool {
match self {
Self::Initial { state } => state.is_empty(),
Self::Step { effect, .. } => effect.is_empty(),
}
}
pub fn to_snapshot(
self,
limit: TraceSnapshotByteLimit,
) -> Result<TraceSnapshotEvent<'program>, TraceSnapshotError> {
ensure_trace_len(self.byte_count(), limit)?;
match self {
Self::Initial { state } => Ok(TraceSnapshotEvent::Initial {
state: RuntimeStateSnapshot::from_vec(
state.to_vec_with_context(AllocationContext::TraceSnapshot)?,
),
}),
Self::Step { step, rule, effect } => Ok(TraceSnapshotEvent::Step {
step,
rule,
effect: effect.to_snapshot(limit)?,
}),
}
}
}
fn ensure_trace_len(
len: TraceSnapshotByteCount,
limit: TraceSnapshotByteLimit,
) -> Result<(), TraceSnapshotError> {
if len.get() > limit.get() {
return Err(TraceSnapshotError::Limit {
limit,
attempted_len: len,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::test_support::{
TestFailure, TestResult, ensure, ensure_eq, ensure_matches, expect_event,
expect_return_output, result_bytes, runtime_input, trace_event_bytes,
};
use crate::{
BorrowedTraceEffect, BorrowedTraceEvent, FallibleTraceSnapshotRunError, Program,
RuleActionView, RunError, RunLimits, RuntimeStateByteCount, StepLimit,
TraceSnapshotByteCount, TraceSnapshotByteLimit, TraceSnapshotEffect, TraceSnapshotError,
TraceSnapshotEvent, TraceSnapshotRunError,
};
use std::vec::Vec;
#[test]
fn borrowed_trace_events_are_emitted_without_snapshots() -> TestResult {
let program = Program::parse_str("a=b\nb=(return)ok")?;
let mut seen = Vec::new();
let limits = RunLimits::new(StepLimit::new(10_000));
let result = program.run_with_borrowed_trace(runtime_input(b"a")?, limits, |event| {
let bytes = match event {
BorrowedTraceEvent::Initial { state }
| BorrowedTraceEvent::Step {
effect: BorrowedTraceEffect::Continue { state },
..
} => state.bytes().collect::<Vec<_>>(),
BorrowedTraceEvent::Step {
effect: BorrowedTraceEffect::Return { output },
..
} => output.bytes().collect::<Vec<_>>(),
};
seen.push((event.byte_count().get(), bytes));
})?;
expect_return_output(&result, b"ok")?;
ensure_eq(
seen.as_slice(),
&[(1, b"a".to_vec()), (1, b"b".to_vec()), (2, b"ok".to_vec())],
)?;
Ok(())
}
#[test]
fn trace_snapshot_events_are_emitted_without_core_stderr() -> TestResult {
let program = Program::parse_str("a=b\nb=(return)ok")?;
let mut events = Vec::new();
let limits = RunLimits::new(StepLimit::new(10_000));
let result = program.run_with_trace_snapshots(
runtime_input(b"a")?,
limits,
crate::DEFAULT_MAX_TRACE_SNAPSHOT_LEN,
|event| {
events.push(event);
},
)?;
expect_return_output(&result, b"ok")?;
ensure_eq(events.len(), 3)?;
let initial = expect_event(&events, 0)?;
let first_step = expect_event(&events, 1)?;
let second_step = expect_event(&events, 2)?;
ensure_matches(
matches!(initial, TraceSnapshotEvent::Initial { .. }),
"expected initial trace event",
)?;
ensure_eq(trace_event_bytes(initial), b"a".as_slice())?;
ensure_eq(trace_event_bytes(first_step), b"b".as_slice())?;
ensure_eq(trace_event_bytes(second_step), b"ok".as_slice())?;
ensure_matches(
matches!(
first_step,
TraceSnapshotEvent::Step {
effect: TraceSnapshotEffect::Continue { .. },
..
}
),
"expected continue step",
)?;
ensure_matches(
matches!(
second_step,
TraceSnapshotEvent::Step {
effect: TraceSnapshotEffect::Return { .. },
..
}
),
"expected return step",
)?;
match first_step {
TraceSnapshotEvent::Step {
rule,
effect: TraceSnapshotEffect::Continue { state },
..
} => {
ensure_eq(state.as_bytes(), b"b".as_slice())?;
ensure_eq(state.byte_count(), RuntimeStateByteCount::new(1))?;
ensure_eq(rule.position().number().get(), 1)?;
ensure_eq(rule.line_number().get(), 1)?;
ensure(rule.lhs().eq_bytes(b"a"), "expected lhs")?;
ensure_matches(
matches!(
rule.action(),
RuleActionView::Replace(payload) if payload.eq_bytes(b"b")
),
"expected replace action",
)?;
ensure_eq(rule.canonical_source()?, b"a=b".as_slice())?;
}
TraceSnapshotEvent::Initial { .. } | TraceSnapshotEvent::Step { .. } => {
return Err(TestFailure::message("expected continuing step event"));
}
}
Ok(())
}
#[test]
fn borrowed_trace_to_snapshot_uses_only_snapshot_limit() -> TestResult {
let program = Program::parse_str("a=b")?;
let mut materialization = None;
program.run_with_borrowed_trace(
runtime_input(b"a")?,
RunLimits::new(StepLimit::new(10)),
|event| {
if materialization.is_none() {
materialization = Some(event.to_snapshot(TraceSnapshotByteLimit::new(0)));
}
},
)?;
ensure_eq(
materialization.ok_or(TestFailure::message("expected trace event"))?,
Err(TraceSnapshotError::Limit {
limit: TraceSnapshotByteLimit::new(0),
attempted_len: TraceSnapshotByteCount::new(1),
}),
)
}
#[test]
fn trace_snapshot_api_splits_runtime_snapshot_and_sink_failures() -> TestResult {
let program = Program::parse_str("a=b")?;
let runtime_error = program.run_with_trace_snapshots(
runtime_input(b"a")?,
RunLimits::new(StepLimit::new(0)),
TraceSnapshotByteLimit::new(10),
|_event| {},
);
ensure_matches(
matches!(
runtime_error,
Err(TraceSnapshotRunError::Run(RunError::Limit(_)))
),
"expected runtime failure variant",
)?;
let snapshot_error = program.run_with_trace_snapshots(
runtime_input(b"a")?,
RunLimits::bounded(
StepLimit::new(10),
crate::StateByteLimit::new(10),
crate::ReturnByteLimit::new(10),
),
TraceSnapshotByteLimit::new(0),
|_event| {},
);
ensure_eq(
snapshot_error,
Err(TraceSnapshotRunError::Snapshot(TraceSnapshotError::Limit {
limit: TraceSnapshotByteLimit::new(0),
attempted_len: TraceSnapshotByteCount::new(1),
})),
)?;
let sink_error = program.try_run_with_trace_snapshots(
runtime_input(b"a")?,
RunLimits::new(StepLimit::new(10)),
TraceSnapshotByteLimit::new(10),
|_event| Err::<(), _>("trace sink full"),
);
ensure_eq(
sink_error,
Err(FallibleTraceSnapshotRunError::Trace("trace sink full")),
)
}
#[test]
fn fallible_trace_callback_can_abort_execution() -> TestResult {
let program = Program::parse_str("a=b\nb=c")?;
let limits = RunLimits::new(StepLimit::new(10_000));
let result = program.try_run_with_trace_snapshots(
runtime_input(b"a")?,
limits,
crate::DEFAULT_MAX_TRACE_SNAPSHOT_LEN,
|_event| Err::<(), _>("trace sink full"),
);
ensure_eq(
result,
Err(FallibleTraceSnapshotRunError::Trace("trace sink full")),
)?;
Ok(())
}
#[test]
fn traced_final_event_matches_run_result() -> TestResult {
let program = Program::parse_str("a=b\nb=(return)c")?;
let mut events = Vec::new();
let limits = RunLimits::new(StepLimit::new(10));
let result = program.run_with_trace_snapshots(
runtime_input(b"a")?,
limits,
crate::DEFAULT_MAX_TRACE_SNAPSHOT_LEN,
|event| {
events.push(event);
},
)?;
let last = events
.last()
.ok_or(TestFailure::message("expected final trace event"))?;
ensure_eq(trace_event_bytes(last), result_bytes(&result))?;
let expected_events = result
.steps()
.checked_next()
.ok_or(TestFailure::message("expected trace event count"))?;
ensure_eq(events.len(), expected_events.get())?;
ensure_matches(
matches!(
last,
TraceSnapshotEvent::Step {
effect: TraceSnapshotEffect::Return { .. },
..
}
),
"expected final return step",
)?;
Ok(())
}
}