use crate::term::Term;
use super::{ProcessContext, RootedTerms};
pub struct TermAccumulator {
backing: Backing,
}
enum Backing {
Rooted(RootedTerms),
Owned(Vec<Term>),
}
impl ProcessContext<'_> {
pub fn with_accumulator<R>(
&mut self,
body: impl FnOnce(&mut Self, &mut TermAccumulator) -> Result<R, Term>,
) -> Result<R, Term> {
if self.process.is_none() {
let mut accumulator = TermAccumulator {
backing: Backing::Owned(Vec::new()),
};
return body(self, &mut accumulator);
}
self.with_rooted(&[], |context, roots| {
let mut accumulator = TermAccumulator {
backing: Backing::Rooted(*roots),
};
body(context, &mut accumulator)
})
}
}
impl TermAccumulator {
#[must_use]
pub fn len(&self) -> usize {
match &self.backing {
Backing::Rooted(handle) => handle.len,
Backing::Owned(terms) => terms.len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn push(&mut self, context: &mut ProcessContext<'_>, term: Term) -> Result<(), Term> {
match &mut self.backing {
Backing::Rooted(handle) => context.rooted_push(handle, term),
Backing::Owned(terms) => {
terms.push(term);
Ok(())
}
}
}
pub fn get(&self, context: &ProcessContext<'_>, index: usize) -> Result<Term, Term> {
match &self.backing {
Backing::Rooted(handle) => context.rooted(handle, index),
Backing::Owned(terms) => terms
.get(index)
.copied()
.ok_or_else(|| Term::atom(crate::atom::Atom::BADARG)),
}
}
pub fn set(
&mut self,
context: &mut ProcessContext<'_>,
index: usize,
term: Term,
) -> Result<(), Term> {
match &mut self.backing {
Backing::Rooted(handle) => context.set_rooted(handle, index, term),
Backing::Owned(terms) => {
let slot = terms
.get_mut(index)
.ok_or_else(|| Term::atom(crate::atom::Atom::BADARG))?;
*slot = term;
Ok(())
}
}
}
fn snapshot(&self, context: &ProcessContext<'_>) -> Result<Vec<Term>, Term> {
match &self.backing {
Backing::Rooted(handle) => {
let mut terms = Vec::with_capacity(handle.len);
for index in 0..handle.len {
terms.push(context.rooted(handle, index)?);
}
Ok(terms)
}
Backing::Owned(terms) => Ok(terms.clone()),
}
}
pub fn to_list(&self, context: &mut ProcessContext<'_>) -> Result<Term, Term> {
let elements = self.snapshot(context)?;
context.alloc_list(&elements)
}
pub fn to_list_with_tail(
&self,
context: &mut ProcessContext<'_>,
tail: Term,
) -> Result<Term, Term> {
let elements = self.snapshot(context)?;
context.alloc_list_with_tail(&elements, tail)
}
pub fn to_tuple(&self, context: &mut ProcessContext<'_>) -> Result<Term, Term> {
let elements = self.snapshot(context)?;
context.alloc_tuple(&elements)
}
pub fn to_map_pairs(&self, context: &mut ProcessContext<'_>) -> Result<Term, Term> {
let flat = self.snapshot(context)?;
if flat.len() % 2 != 0 {
return Err(Term::atom(crate::atom::Atom::BADARG));
}
let mut keys = Vec::with_capacity(flat.len() / 2);
let mut values = Vec::with_capacity(flat.len() / 2);
for pair in flat.chunks_exact(2) {
keys.push(pair[0]);
values.push(pair[1]);
}
context.alloc_map(&keys, &values)
}
pub fn sort_pairs_by_key(&mut self, context: &mut ProcessContext<'_>) -> Result<(), Term> {
let flat = self.snapshot(context)?;
if flat.len() % 2 != 0 {
return Err(Term::atom(crate::atom::Atom::BADARG));
}
let mut pairs: Vec<(Term, Term)> = flat
.chunks_exact(2)
.map(|pair| (pair[0], pair[1]))
.collect();
pairs.sort_by_key(|(key, _)| *key);
for (index, (key, value)) in pairs.into_iter().enumerate() {
self.set(context, index * 2, key)?;
self.set(context, index * 2 + 1, value)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::process::Process;
use crate::term::binary::Binary;
use crate::term::boxed::Cons;
const WIDTH: usize = 12;
fn payload(index: usize) -> String {
format!("a{index:0WIDTH$}")
}
fn check_list(list: Term, count: usize) -> Result<(), String> {
let cap = count * 2 + 16;
let mut seen = 0usize;
let mut tail = list;
while !tail.is_nil() {
if seen > cap {
return Err(format!(
"list did not terminate within {cap} cells — cyclic tail"
));
}
let cons = Cons::new(tail)
.ok_or_else(|| format!("entry {seen}: tail is not a cons — carrier went stale"))?;
let binary = Binary::new(cons.head()).ok_or_else(|| {
format!("entry {seen}: head is not a binary — carrier went stale")
})?;
let want = payload(seen);
if binary.as_bytes() != want.as_bytes() {
return Err(format!(
"entry {seen}: contents {:?} != {want:?}",
String::from_utf8_lossy(binary.as_bytes())
));
}
seen += 1;
tail = cons.tail();
}
if seen != count {
return Err(format!("recovered {seen} entries, put {count}"));
}
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
enum Cell {
Clean,
Refused,
Corrupt(String),
}
fn pre_fill(context: &mut ProcessContext<'_>, margin: usize) -> usize {
let mut filler = Vec::new();
let mut last_available = usize::MAX;
loop {
let available = context
.process_heap()
.map(|heap| heap.available())
.unwrap_or(0);
if available <= margin || available >= last_available {
break available;
}
last_available = available;
match context.alloc_binary(&[0xA1; 32]) {
Ok(term) => filler.push(term),
Err(_) => break available,
}
}
}
fn unrooted_arm(count: usize, heap: usize, margin: usize) -> (usize, Cell) {
let mut process = Process::new(11, heap);
let mut context = ProcessContext::new();
context.attach_process(&mut process, 0);
let achieved = pre_fill(&mut context, margin);
let mut terms: Vec<Term> = Vec::with_capacity(count);
for index in 0..count {
match context.alloc_binary(payload(index).as_bytes()) {
Ok(term) => terms.push(term),
Err(_) => return (achieved, Cell::Refused),
}
}
let Ok(list) = context.alloc_list(&terms) else {
return (achieved, Cell::Refused);
};
(achieved, cell_of(check_list(list, count)))
}
fn rooted_arm(count: usize, heap: usize, margin: usize) -> (usize, Cell) {
let mut process = Process::new(11, heap);
let mut context = ProcessContext::new();
context.attach_process(&mut process, 0);
let achieved = pre_fill(&mut context, margin);
let outcome = context.with_accumulator(|context, accumulator| {
for index in 0..count {
let term = context.alloc_binary(payload(index).as_bytes())?;
accumulator.push(context, term)?;
}
accumulator.to_list(context)
});
let Ok(list) = outcome else {
return (achieved, Cell::Refused);
};
(achieved, cell_of(check_list(list, count)))
}
fn cell_of(result: Result<(), String>) -> Cell {
match result {
Ok(()) => Cell::Clean,
Err(reason) => Cell::Corrupt(reason),
}
}
#[test]
fn accumulator_survives_collections_that_break_a_bare_vec() {
const HEAP: usize = 4096;
let mut corrupt_control = 0usize;
let mut clean_control = 0usize;
let mut rooted_failures = Vec::new();
for count in [20usize, 60, 120] {
for margin in [0usize, 1, 2, 4, 8, 16, 32, 64, 128] {
let (bare_at, bare) = unrooted_arm(count, HEAP, margin);
let (acc_at, accumulated) = rooted_arm(count, HEAP, margin);
eprintln!(
"accumulator len {count:>4} margin req {margin:>4}: \
bare Vec @{bare_at:>5} {bare:?} | accumulator @{acc_at:>5} {accumulated:?}"
);
match bare {
Cell::Corrupt(_) => corrupt_control += 1,
Cell::Clean => clean_control += 1,
Cell::Refused => {}
}
if let Cell::Corrupt(reason) = accumulated {
rooted_failures.push(format!("len {count} margin {margin}: {reason}"));
}
}
}
assert!(
corrupt_control > 0,
"POSITIVE CONTROL DEAD: the bare-Vec arm was never CORRUPTED (clean {clean_control}), \
so this sweep applies no usable heap pressure and the rooted arm's green means \
nothing. Repair the pressure regime — do NOT weaken this assertion, and do NOT \
count refusals as reds."
);
assert!(
clean_control > 0,
"NEGATIVE CONTROL DEAD: no bare-Vec cell was clean, so the reader may be broken \
rather than the carrier stale."
);
assert!(
rooted_failures.is_empty(),
"TermAccumulator failed to keep its elements alive: {rooted_failures:#?}"
);
assert_eq!(
(corrupt_control, clean_control),
(17, 10),
"the bare-Vec control's surface drifted; re-derive the pressure regime before \
trusting any green in this module"
);
}
#[test]
fn accumulator_works_on_a_detached_context() {
let mut context = ProcessContext::new();
let list = context
.with_accumulator(|context, accumulator| {
for index in 0..8 {
let term = context.alloc_binary(payload(index).as_bytes())?;
accumulator.push(context, term)?;
}
assert_eq!(accumulator.len(), 8);
accumulator.to_list(context)
})
.expect("detached accumulation should succeed");
check_list(list, 8).expect("detached list should round-trip");
}
#[test]
fn odd_pair_runs_are_refused_not_truncated() {
let mut process = Process::new(11, 4096);
let mut context = ProcessContext::new();
context.attach_process(&mut process, 0);
let map = context.with_accumulator(|context, accumulator| {
let key = context.alloc_binary(b"k")?;
accumulator.push(context, key)?;
accumulator.to_map_pairs(context)
});
assert_eq!(map, Err(Term::atom(crate::atom::Atom::BADARG)));
let sorted = context.with_accumulator(|context, accumulator| {
let key = context.alloc_binary(b"k")?;
accumulator.push(context, key)?;
accumulator.sort_pairs_by_key(context)
});
assert_eq!(sorted, Err(Term::atom(crate::atom::Atom::BADARG)));
}
}