mod frame;
mod reference;
pub(crate) use frame::ContextFrame;
pub(crate) use reference::ContextRef;
use super::value::DataValue;
#[cfg(all(test, feature = "serde_json"))]
use bumpalo::Bump;
use smallvec::SmallVec;
pub(crate) struct ContextStack<'a> {
root: &'a DataValue<'a>,
top: Option<ContextFrame<'a>>,
parents: Vec<ContextFrame<'a>>,
track_ancestors: bool,
depth: u32,
error_path: Vec<u32>,
cse_slots: SmallVec<[Option<&'a DataValue<'a>>; 8]>,
#[cfg(feature = "error-handling")]
catch_depth: u32,
#[cfg(feature = "error-handling")]
thrown_slot: Option<&'a DataValue<'a>>,
#[cfg(feature = "budget")]
ops: u64,
#[cfg(feature = "budget")]
budget: u64,
#[cfg(feature = "trace")]
tracer: Option<Box<crate::trace::TraceCollector>>,
}
#[must_use = "hand the FrameToken back to `restore_frame`, or the displaced frame is lost"]
struct FrameToken<'a>(Option<ContextFrame<'a>>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FrameTarget {
Root,
Top,
Ancestor(usize),
}
#[inline]
pub(crate) const fn data_climb(level: usize) -> usize {
level / 2 + (level & 1)
}
#[inline]
pub(crate) const fn metadata_climb(level: usize) -> Option<usize> {
if level == 0 || level % 2 == 1 {
Some(level / 2)
} else {
None
}
}
#[inline]
pub(crate) const fn frame_at_climb(frame_count: usize, climb: usize) -> FrameTarget {
if climb >= frame_count {
return FrameTarget::Root;
}
if climb == 0 {
FrameTarget::Top
} else {
FrameTarget::Ancestor(frame_count - 1 - climb)
}
}
#[inline]
pub(crate) const fn frame_target(frame_count: usize, level: usize) -> FrameTarget {
frame_at_climb(frame_count, data_climb(level))
}
impl<'a> ContextStack<'a> {
#[inline]
pub(crate) fn new(root: &'a DataValue<'a>, track_ancestors: bool) -> Self {
Self {
root,
top: None,
parents: Vec::new(),
track_ancestors,
depth: 0,
error_path: Vec::new(),
cse_slots: SmallVec::new(),
#[cfg(feature = "error-handling")]
catch_depth: 0,
#[cfg(feature = "error-handling")]
thrown_slot: None,
#[cfg(feature = "budget")]
ops: 0,
#[cfg(feature = "budget")]
budget: u64::MAX,
#[cfg(feature = "trace")]
tracer: None,
}
}
#[cfg(all(test, feature = "serde_json"))]
#[inline]
pub(crate) fn from_value(root: &'a serde_json::Value, arena: &'a Bump) -> Self {
let av = crate::arena::value::value_to_data(root, arena);
Self::new(arena.alloc(av), true)
}
#[cfg(feature = "trace")]
#[inline]
pub(crate) fn attach_tracer(&mut self, tracer: crate::trace::TraceCollector) {
self.tracer = Some(Box::new(tracer));
}
#[cfg(feature = "trace")]
#[inline]
pub(crate) fn detach_tracer(&mut self) -> Option<crate::trace::TraceCollector> {
self.tracer.take().map(|boxed| *boxed)
}
#[cfg(feature = "trace")]
#[inline]
pub(crate) fn has_tracer(&self) -> bool {
self.tracer.is_some()
}
#[cfg(feature = "trace")]
#[inline]
fn with_tracer<F: FnOnce(&mut crate::trace::TraceCollector)>(&mut self, f: F) {
if let Some(tracer) = self.tracer.as_mut() {
f(tracer);
}
}
#[inline]
pub(crate) fn is_tracing(&self) -> bool {
#[cfg(feature = "trace")]
{
self.has_tracer()
}
#[cfg(not(feature = "trace"))]
{
false
}
}
#[cfg(all(feature = "trace", feature = "serde_json"))]
pub(crate) fn current_data_as_value(&self) -> serde_json::Value {
crate::arena::data_to_value(self.current().data())
}
#[cfg(all(feature = "trace", feature = "serde_json"))]
pub(crate) fn record_node_result(
&mut self,
node_id: u32,
ctx_data: serde_json::Value,
result: &crate::Result<&'a crate::arena::DataValue<'a>>,
) {
self.with_tracer(|collector| match result {
Ok(av) => {
let v = crate::arena::data_to_value(av);
collector.record_step(node_id, ctx_data, v);
}
Err(e) => {
collector.record_error(node_id, ctx_data, e.to_string());
}
});
}
#[cfg(feature = "trace")]
#[inline]
pub(crate) fn trace_push_iteration(&mut self, index: u32, total: u32) {
self.with_tracer(|c| c.push_iteration(index, total));
}
#[cfg(feature = "trace")]
#[inline]
pub(crate) fn trace_pop_iteration(&mut self) {
self.with_tracer(|c| c.pop_iteration());
}
#[inline]
pub(crate) fn root_input(&self) -> &'a DataValue<'a> {
self.root
}
#[inline]
pub(crate) fn depth(&self) -> usize {
debug_assert!(
!self.track_ancestors
|| self.depth as usize == self.parents.len() + usize::from(self.top.is_some()),
"explicit depth counter drifted from the frame storage"
);
self.depth as usize
}
#[inline]
pub(crate) fn cse_slot(&self, slot: u16) -> Option<&'a DataValue<'a>> {
self.cse_slots.get(slot as usize).copied().flatten()
}
#[inline]
pub(crate) fn fill_cse_slot(&mut self, slot: u16, value: &'a DataValue<'a>) {
let index = slot as usize;
if index >= self.cse_slots.len() {
self.cse_slots.resize(index + 1, None);
}
self.cse_slots[index] = Some(value);
}
#[inline]
pub(crate) fn current(&self) -> ContextRef<'a, '_> {
if let Some(frame) = self.top.as_ref() {
ContextRef::Frame(frame)
} else {
ContextRef::Root(self.root)
}
}
pub(crate) fn get_at_level(&self, level: isize) -> Option<ContextRef<'a, '_>> {
match frame_target(self.depth(), level.unsigned_abs()) {
FrameTarget::Root => Some(ContextRef::Root(self.root)),
target => self.frame_at(target).map(ContextRef::Frame),
}
}
#[inline]
fn frame_at(&self, target: FrameTarget) -> Option<&ContextFrame<'a>> {
match target {
FrameTarget::Root => None,
FrameTarget::Top => self.top.as_ref(),
FrameTarget::Ancestor(index) => {
debug_assert!(
self.track_ancestors,
"ancestor lookup on a stack built without ancestor tracking - \
`Logic::needs_ancestor_frames` under-approximated"
);
self.parents.get(index)
}
}
}
#[inline]
pub(crate) fn metadata_at_level(&self, level: isize) -> Option<&ContextFrame<'a>> {
let climb = metadata_climb(level.unsigned_abs())?;
if climb == 0 {
return self.top.as_ref();
}
self.frame_at(frame_at_climb(self.depth(), climb))
}
#[inline]
fn push_frame(&mut self, frame: ContextFrame<'a>) -> FrameToken<'a> {
let prev = self.top.replace(frame);
if self.track_ancestors
&& let Some(p) = prev
{
self.parents.push(p);
}
self.depth += 1;
FrameToken(prev)
}
#[cfg(feature = "error-handling")]
#[inline]
fn push(&mut self, data: &'a DataValue<'a>) -> FrameToken<'a> {
self.push_frame(ContextFrame::Data(data))
}
#[inline]
fn push_indexed(&mut self, data: &'a DataValue<'a>, index: usize) -> FrameToken<'a> {
self.push_frame(ContextFrame::Indexed { data, index })
}
#[inline]
fn push_with_key_index(
&mut self,
data: &'a DataValue<'a>,
index: usize,
key: &'a str,
) -> FrameToken<'a> {
self.push_frame(ContextFrame::Keyed { data, index, key })
}
#[inline]
fn push_reduce(
&mut self,
current: &'a DataValue<'a>,
accumulator: &'a DataValue<'a>,
) -> FrameToken<'a> {
self.push_frame(ContextFrame::Reduce {
current,
accumulator,
})
}
#[inline]
fn replace_top(&mut self, frame: ContextFrame<'a>) {
if let Some(top) = self.top.as_mut() {
*top = frame;
}
}
#[inline]
fn replace_top_data(&mut self, data: &'a DataValue<'a>, index: usize) {
self.replace_top(ContextFrame::Indexed { data, index });
}
#[inline]
fn replace_top_key_data(&mut self, data: &'a DataValue<'a>, index: usize, key: &'a str) {
self.replace_top(ContextFrame::Keyed { data, index, key });
}
#[inline]
fn replace_reduce_data(&mut self, current: &'a DataValue<'a>, accumulator: &'a DataValue<'a>) {
self.replace_top(ContextFrame::Reduce {
current,
accumulator,
});
}
#[inline]
fn restore_frame(&mut self, token: FrameToken<'a>) {
self.top = token.0;
if self.track_ancestors && token.0.is_some() {
self.parents.pop();
}
self.depth -= 1;
}
#[cold]
#[inline(never)]
pub(crate) fn push_error_step(&mut self, id: u32) {
self.error_path.push(id);
}
#[cfg(feature = "error-handling")]
#[inline]
pub(crate) fn error_path_len(&self) -> usize {
self.error_path.len()
}
#[cfg(feature = "error-handling")]
#[inline]
pub(crate) fn truncate_error_path(&mut self, len: usize) {
self.error_path.truncate(len);
}
#[inline]
pub(crate) fn take_error_path(&mut self) -> Vec<u32> {
std::mem::take(&mut self.error_path)
}
#[cfg(feature = "budget")]
#[inline]
pub(crate) fn charge(&mut self, n: u64) -> crate::Result<()> {
self.ops = self.ops.saturating_add(n);
if self.ops > self.budget {
return Err(crate::Error::budget_exceeded(self.budget, self.ops));
}
Ok(())
}
#[cfg(not(feature = "budget"))]
#[inline(always)]
pub(crate) fn charge(&mut self, _n: u64) -> crate::Result<()> {
Ok(())
}
#[cfg(feature = "budget")]
#[inline]
pub(crate) fn set_budget(&mut self, budget: u64) {
self.budget = budget;
}
#[cfg(feature = "budget")]
#[inline]
pub(crate) fn ops_spent(&self) -> u64 {
self.ops
}
#[cfg(feature = "error-handling")]
#[inline]
pub(crate) fn in_catch_scope(&self) -> bool {
self.catch_depth > 0
}
#[cfg(feature = "error-handling")]
#[inline]
pub(crate) fn enter_catch_scope(&mut self) {
self.catch_depth += 1;
}
#[cfg(feature = "error-handling")]
#[inline]
pub(crate) fn exit_catch_scope(&mut self) {
debug_assert!(self.catch_depth > 0, "unbalanced exit_catch_scope");
self.catch_depth -= 1;
}
#[cfg(feature = "error-handling")]
#[inline]
pub(crate) fn set_thrown_slot(&mut self, payload: &'a DataValue<'a>) {
self.thrown_slot = Some(payload);
}
#[cfg(feature = "error-handling")]
#[inline]
pub(crate) fn take_thrown_slot(&mut self) -> Option<&'a DataValue<'a>> {
self.thrown_slot.take()
}
#[cfg(feature = "error-handling")]
#[inline]
pub(crate) fn clear_thrown_slot(&mut self) {
self.thrown_slot = None;
}
}
pub(crate) struct IterGuard<'g, 'a> {
ctx: &'g mut ContextStack<'a>,
saved: Option<FrameToken<'a>>,
}
impl<'g, 'a> IterGuard<'g, 'a> {
#[inline]
pub(crate) fn new(ctx: &'g mut ContextStack<'a>) -> Self {
Self { ctx, saved: None }
}
#[cfg(feature = "error-handling")]
#[inline]
pub(crate) fn step_data(&mut self, data: &'a DataValue<'a>) {
if self.saved.is_some() {
self.ctx.replace_top(ContextFrame::Data(data));
} else {
self.saved = Some(self.ctx.push(data));
}
}
#[inline]
pub(crate) fn step_indexed(&mut self, data: &'a DataValue<'a>, index: usize) {
if self.saved.is_some() {
self.ctx.replace_top_data(data, index);
} else {
self.saved = Some(self.ctx.push_indexed(data, index));
}
}
#[inline]
pub(crate) fn step_keyed(&mut self, data: &'a DataValue<'a>, index: usize, key: &'a str) {
if self.saved.is_some() {
self.ctx.replace_top_key_data(data, index, key);
} else {
self.saved = Some(self.ctx.push_with_key_index(data, index, key));
}
}
#[inline]
pub(crate) fn step_reduce(
&mut self,
current: &'a DataValue<'a>,
accumulator: &'a DataValue<'a>,
) {
if self.saved.is_some() {
self.ctx.replace_reduce_data(current, accumulator);
} else {
self.saved = Some(self.ctx.push_reduce(current, accumulator));
}
}
#[inline]
pub(crate) fn stack(&mut self) -> &mut ContextStack<'a> {
self.ctx
}
}
impl Drop for IterGuard<'_, '_> {
#[inline]
fn drop(&mut self) {
if let Some(token) = self.saved.take() {
self.ctx.restore_frame(token);
}
}
}
#[cfg(all(test, feature = "serde_json"))]
mod tests {
use super::*;
use crate::arena::value::DataValue;
use serde_json::Value;
#[test]
fn lifecycle_indexed() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
assert_eq!(ctx.depth(), 0);
assert!(ctx.current().root_data().is_some(), "root at depth 0");
let a: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(1)));
let token = ctx.push_indexed(a, 0);
assert_eq!(ctx.depth(), 1);
assert_eq!(ctx.current().get_index(), Some(0));
let b: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(2)));
ctx.replace_top_data(b, 1);
assert_eq!(ctx.current().get_index(), Some(1));
ctx.restore_frame(token);
assert_eq!(ctx.depth(), 0);
}
#[test]
fn lifecycle_keyed() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
let a: &DataValue = arena.alloc(DataValue::Bool(true));
let _token = ctx.push_with_key_index(a, 0, "k1");
assert_eq!(ctx.current().get_key(), Some("k1"));
let b: &DataValue = arena.alloc(DataValue::Bool(false));
ctx.replace_top_key_data(b, 1, "k2");
assert_eq!(ctx.current().get_key(), Some("k2"));
assert_eq!(ctx.current().get_index(), Some(1));
}
#[test]
fn lifecycle_reduce() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
let cur: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(1)));
let acc: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(0)));
let _token = ctx.push_reduce(cur, acc);
assert_eq!(ctx.depth(), 1);
if let ContextRef::Frame(f) = ctx.current() {
assert!(f.get_reduce_current().is_some());
assert!(f.get_reduce_accumulator().is_some());
} else {
panic!("expected frame");
}
}
#[test]
fn get_at_level_walks_up() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
let a: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(10)));
let b: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(20)));
let _ta = ctx.push_indexed(a, 0);
let _tb = ctx.push_indexed(b, 1);
assert_eq!(ctx.depth(), 2);
assert_eq!(ctx.get_at_level(0).and_then(|r| r.get_index()), Some(1));
assert_eq!(ctx.get_at_level(1).and_then(|r| r.get_index()), Some(0));
assert_eq!(ctx.get_at_level(2).and_then(|r| r.get_index()), Some(0));
assert!(ctx.get_at_level(3).and_then(|r| r.root_data()).is_some());
assert!(ctx.get_at_level(9).and_then(|r| r.root_data()).is_some());
assert_eq!(
ctx.metadata_at_level(1).and_then(|f| f.get_index()),
Some(1),
"[[1]] names the innermost frame's metadata"
);
assert_eq!(
ctx.metadata_at_level(3).and_then(|f| f.get_index()),
Some(0),
"[[3]] names the enclosing frame's metadata"
);
assert!(
ctx.metadata_at_level(2).is_none(),
"an even level names an element frame, not metadata"
);
assert!(
ctx.metadata_at_level(5).is_none(),
"a metadata climb past the outermost frame has no frame"
);
}
#[test]
fn pop_restores_parent_frames() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
let a: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(1)));
let b: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(2)));
let c: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(3)));
let ta = ctx.push_indexed(a, 10);
let tb = ctx.push_indexed(b, 20);
let tc = ctx.push_indexed(c, 30);
assert_eq!(ctx.depth(), 3);
assert_eq!(ctx.current().get_index(), Some(30));
ctx.restore_frame(tc);
assert_eq!(ctx.depth(), 2);
assert_eq!(ctx.current().get_index(), Some(20), "parent restored");
ctx.restore_frame(tb);
assert_eq!(ctx.current().get_index(), Some(10));
ctx.restore_frame(ta);
assert_eq!(ctx.depth(), 0);
assert!(ctx.current().root_data().is_some(), "back to root");
}
#[test]
fn deep_nesting_spills_and_unwinds() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
let depth = 8;
let mut tokens = Vec::new();
for i in 0..depth {
let v: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(
i as i64,
)));
tokens.push(ctx.push_indexed(v, i));
}
assert_eq!(ctx.depth(), depth);
assert_eq!(ctx.current().get_index(), Some(depth - 1));
for level in 1..2 * depth - 1 {
let climb = data_climb(level);
let r = ctx.get_at_level(level as isize).expect("in range");
assert_eq!(
r.get_index(),
Some(depth - 1 - climb),
"level {level} climbs {climb}"
);
}
for level in 2 * depth - 1..2 * depth + 3 {
assert!(
ctx.get_at_level(level as isize)
.and_then(|r| r.root_data())
.is_some(),
"level {level} clamps to the root"
);
}
for level in (1..2 * depth).step_by(2) {
assert_eq!(
ctx.metadata_at_level(level as isize)
.and_then(|f| f.get_index()),
Some(depth - 1 - metadata_climb(level).expect("odd level")),
"metadata at level {level}"
);
}
for i in (0..depth).rev() {
assert_eq!(ctx.current().get_index(), Some(i));
ctx.restore_frame(tokens.pop().expect("one token per push"));
}
assert_eq!(ctx.depth(), 0);
assert!(tokens.is_empty());
}
#[test]
fn iter_guard_pushes_then_pops_indexed() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
assert_eq!(ctx.depth(), 0);
let a: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(1)));
let b: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(2)));
{
let mut g = IterGuard::new(&mut ctx);
g.step_indexed(a, 0);
assert_eq!(g.stack().depth(), 1);
assert_eq!(g.stack().current().get_index(), Some(0));
g.step_indexed(b, 1);
assert_eq!(g.stack().depth(), 1, "replace, not push");
assert_eq!(g.stack().current().get_index(), Some(1));
}
assert_eq!(ctx.depth(), 0, "drop pops");
}
#[test]
fn iter_guard_no_push_no_pop() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
assert_eq!(ctx.depth(), 0);
{
let _g = IterGuard::new(&mut ctx);
}
assert_eq!(ctx.depth(), 0, "drop without push is a no-op");
}
#[test]
fn iter_guard_keyed_and_reduce() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
let a: &DataValue = arena.alloc(DataValue::Bool(true));
let b: &DataValue = arena.alloc(DataValue::Bool(false));
{
let mut g = IterGuard::new(&mut ctx);
g.step_keyed(a, 0, "k1");
assert_eq!(g.stack().current().get_key(), Some("k1"));
g.step_keyed(b, 1, "k2");
assert_eq!(g.stack().current().get_key(), Some("k2"));
assert_eq!(g.stack().current().get_index(), Some(1));
}
assert_eq!(ctx.depth(), 0);
let cur: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(1)));
let acc: &DataValue = arena.alloc(DataValue::Number(datavalue::NumberValue::from_i64(0)));
{
let mut g = IterGuard::new(&mut ctx);
g.step_reduce(cur, acc);
assert_eq!(g.stack().depth(), 1);
g.step_reduce(acc, cur); assert_eq!(g.stack().depth(), 1);
}
assert_eq!(ctx.depth(), 0);
}
#[cfg(feature = "error-handling")]
#[test]
fn error_path_round_trip() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
ctx.push_error_step(1);
ctx.push_error_step(2);
ctx.push_error_step(3);
assert_eq!(ctx.error_path_len(), 3);
ctx.truncate_error_path(1);
let p = ctx.take_error_path();
assert_eq!(p, vec![1]);
}
#[cfg(feature = "error-handling")]
#[test]
fn thrown_slot_and_catch_scope_round_trip() {
let arena = Bump::new();
let root_val = Value::Null;
let mut ctx = ContextStack::from_value(&root_val, &arena);
assert!(!ctx.in_catch_scope());
ctx.enter_catch_scope();
ctx.enter_catch_scope();
assert!(ctx.in_catch_scope());
ctx.exit_catch_scope();
assert!(ctx.in_catch_scope(), "nested scopes count");
ctx.exit_catch_scope();
assert!(!ctx.in_catch_scope());
assert!(ctx.take_thrown_slot().is_none());
let payload: &DataValue = arena.alloc(DataValue::Bool(true));
ctx.set_thrown_slot(payload);
assert!(std::ptr::eq(
ctx.take_thrown_slot().expect("slot set"),
payload
));
assert!(ctx.take_thrown_slot().is_none(), "take consumes");
ctx.set_thrown_slot(payload);
ctx.clear_thrown_slot();
assert!(ctx.take_thrown_slot().is_none(), "clear drops");
}
#[test]
#[cfg(target_pointer_width = "64")]
fn context_stack_stays_small() {
let size = std::mem::size_of::<ContextStack<'_>>();
assert!(
size <= 256,
"ContextStack grew to {size} bytes; it is constructed per evaluation"
);
}
}