use std::cell::RefCell;
use ocas_atom::Atom;
pub(crate) const MAX_CHAIN_ENTRIES: u32 = 256;
pub(crate) const MAX_RESIDUAL_ENTRIES: u32 = 128;
#[derive(Clone, Copy, PartialEq, Eq)]
struct ChainKey {
node: usize,
rule_depth: usize,
parts_depth: usize,
}
thread_local! {
static STATE: RefCell<ChainState> = const { RefCell::new(ChainState::new()) };
static RESOLVE_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
static PARTS_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
static PARTIAL_SEEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) fn note_partial() {
PARTIAL_SEEN.with(|p| p.set(true));
}
pub(crate) fn partial_seen() -> bool {
PARTIAL_SEEN.with(|p| p.get())
}
struct ChainState {
stack: Vec<ChainKey>,
entries: u32,
resolve_entries: u32,
}
impl ChainState {
const fn new() -> Self {
Self {
stack: Vec::new(),
entries: 0,
resolve_entries: 0,
}
}
}
pub(crate) fn with_resolve_scope<R>(f: impl FnOnce() -> R) -> R {
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
RESOLVE_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
}
}
RESOLVE_DEPTH.with(|d| d.set(d.get() + 1));
let _guard = Guard;
f()
}
pub(crate) fn with_parts_scope<R>(f: impl FnOnce() -> R) -> R {
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
PARTS_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
}
}
PARTS_DEPTH.with(|d| d.set(d.get() + 1));
let _guard = Guard;
f()
}
pub(crate) fn parts_active() -> bool {
PARTS_DEPTH.with(|d| d.get() > 0)
}
pub(crate) fn reset() {
STATE.with(|state| {
let mut state = state.borrow_mut();
state.stack.clear();
state.entries = 0;
state.resolve_entries = 0;
});
PARTIAL_SEEN.with(|p| p.set(false));
}
pub(crate) enum Enter {
Ok(ChainGuard),
Cycle,
Exhausted,
}
pub(crate) struct ChainGuard(());
impl Drop for ChainGuard {
fn drop(&mut self) {
STATE.with(|state| {
state.borrow_mut().stack.pop();
});
}
}
pub(crate) fn enter(expr: Atom<'_>, rule_depth: usize, parts_depth: usize) -> Enter {
let key = ChainKey {
node: expr.node() as *const _ as usize,
rule_depth,
parts_depth,
};
let resolving = RESOLVE_DEPTH.with(|d| d.get() > 0);
STATE.with(|state| {
let mut state = state.borrow_mut();
if state.stack.contains(&key) {
return Enter::Cycle;
}
if resolving {
if state.resolve_entries >= MAX_RESIDUAL_ENTRIES {
return Enter::Exhausted;
}
state.resolve_entries = state.resolve_entries.saturating_add(1);
} else {
if state.entries >= MAX_CHAIN_ENTRIES {
return Enter::Exhausted;
}
state.entries = state.entries.saturating_add(1);
}
state.stack.push(key);
Enter::Ok(ChainGuard(()))
})
}
pub(crate) fn entries() -> u32 {
STATE.with(|state| state.borrow().entries)
}
#[cfg(test)]
pub(crate) fn depth() -> usize {
STATE.with(|state| state.borrow().stack.len())
}
#[cfg(test)]
mod tests {
use ocas_atom::AtomArena;
use ocas_core::arena::Arena;
use super::*;
#[test]
fn ancestor_reentry_is_a_cycle() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
reset();
let _outer = match enter(x, 0, 0) {
Enter::Ok(guard) => guard,
_ => panic!("first entry must be granted"),
};
assert!(matches!(enter(x, 0, 0), Enter::Cycle));
drop(_outer);
assert!(matches!(enter(x, 0, 0), Enter::Ok(_)));
reset();
}
#[test]
fn budget_decreasing_reentry_is_allowed() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
reset();
let _outer = match enter(x, 0, 4) {
Enter::Ok(guard) => guard,
_ => panic!("first entry must be granted"),
};
assert!(matches!(enter(x, 0, 3), Enter::Ok(_)));
assert!(matches!(enter(x, 1, 0), Enter::Ok(_)));
reset();
}
#[test]
fn sibling_repeats_are_allowed() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let sum = ctx.add(&[x, x]);
reset();
assert!(matches!(enter(x, 0, 0), Enter::Ok(_)));
assert!(matches!(enter(sum, 0, 0), Enter::Ok(_)));
reset();
}
#[test]
fn backstop_is_per_top_level_call() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
reset();
let mut granted = 0;
for _ in 0..(MAX_CHAIN_ENTRIES + 8) {
if let Enter::Ok(guard) = enter(ctx.var("x"), 0, 0) {
granted += 1;
drop(guard);
}
}
assert_eq!(granted, MAX_CHAIN_ENTRIES as usize);
assert_eq!(entries(), MAX_CHAIN_ENTRIES);
reset();
assert_eq!(entries(), 0);
assert_eq!(depth(), 0);
}
}