use crate::Value;
#[cfg(not(feature = "no-gc"))]
use crate::clone::{deserialize, serialize};
#[cfg(not(feature = "no-gc"))]
use crate::types::{DelayState, LazySeqState};
#[cfg(not(feature = "no-gc"))]
use crate::value::{MapValue, SetValue};
#[cfg(not(feature = "no-gc"))]
use cljrs_gc::{GcPtr, Trace};
#[cfg(not(feature = "no-gc"))]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Scan {
Clean,
Dirty,
Opaque,
}
#[cfg(not(feature = "no-gc"))]
impl Scan {
fn join(self, other: Scan) -> Scan {
use Scan::*;
match (self, other) {
(Opaque, _) | (_, Opaque) => Opaque,
(Dirty, _) | (_, Dirty) => Dirty,
_ => Clean,
}
}
}
#[cfg(not(feature = "no-gc"))]
fn tag<T: Trace + 'static>(p: &GcPtr<T>) -> Scan {
let dirty = p.is_region_alloc();
if dirty { Scan::Dirty } else { Scan::Clean }
}
#[cfg(not(feature = "no-gc"))]
fn tag_opaque<T: Trace + 'static>(p: &GcPtr<T>) -> Scan {
if p.is_region_alloc() {
Scan::Opaque
} else {
Scan::Clean
}
}
#[cfg(not(feature = "no-gc"))]
fn scan_map(m: &MapValue) -> Scan {
let mut acc = match m {
MapValue::Array(p) => tag(p),
MapValue::Hash(p) => tag(p),
MapValue::Sorted(p) => tag(p),
};
m.for_each(|k, v| {
acc = acc.join(scan(k)).join(scan(v));
});
acc
}
#[cfg(not(feature = "no-gc"))]
fn scan(v: &Value) -> Scan {
use Scan::*;
match v {
Value::Nil
| Value::Bool(_)
| Value::Long(_)
| Value::Double(_)
| Value::Char(_)
| Value::ByteBlob(_)
| Value::SharedAtom(_)
| Value::Resource(_) => Clean,
Value::BigInt(p) => tag(p),
Value::BigDecimal(p) => tag(p),
Value::Ratio(p) => tag(p),
Value::Uuid(p) => tag(p),
Value::Str(p) => tag(p),
Value::Pattern(p) => tag(p),
Value::Symbol(p) => tag(p),
Value::Keyword(p) => tag(p),
Value::List(p) => p.get().iter().fold(tag(p), |acc, e| acc.join(scan(e))),
Value::Vector(p) => p.get().iter().fold(tag(p), |acc, e| acc.join(scan(e))),
Value::Queue(p) => p.get().iter().fold(tag(p), |acc, e| acc.join(scan(e))),
Value::Map(m) => scan_map(m),
Value::Set(s) => {
let t = match s {
SetValue::Hash(p) => tag(p),
SetValue::Sorted(p) => tag(p),
};
s.iter().fold(t, |acc, e| acc.join(scan(e)))
}
Value::Cons(p) => {
let c = p.get();
tag(p).join(scan(&c.head)).join(scan(&c.tail))
}
Value::IntArray(p) => tag(p),
Value::LongArray(p) => tag(p),
Value::ShortArray(p) => tag(p),
Value::ByteArray(p) => tag(p),
Value::FloatArray(p) => tag(p),
Value::DoubleArray(p) => tag(p),
Value::BooleanArray(p) => tag(p),
Value::CharArray(p) => tag(p),
Value::ObjectArray(p) => {
let guard = p.get().0.lock().unwrap();
guard.iter().fold(tag(p), |acc, e| acc.join(scan(e)))
}
Value::WithMeta(inner, meta) => scan(inner).join(scan(meta)),
Value::Reduced(inner) => scan(inner),
Value::TypeInstance(p) => {
if p.get().mutable.is_some() {
Opaque
} else {
tag(p).join(scan_map(&p.get().fields))
}
}
Value::Error(p) => {
let mut acc = tag(p);
let e = p.get();
if let Some(data) = &e.data {
acc = acc.join(scan_map(data));
}
if let crate::error::ValueError::Thrown(t) = &e.error {
acc = acc.join(scan(t));
}
let mut cause = e.cause.clone();
while let Some(c) = cause {
acc = acc.join(tag(&c));
if let Some(data) = &c.get().data {
acc = acc.join(scan_map(data));
}
cause = c.get().cause.clone();
}
acc
}
Value::LazySeq(p) => {
let t = tag_opaque(p);
if t == Opaque {
return Opaque;
}
match &*p.get().state.lock().unwrap() {
LazySeqState::Forced(v) => t.join(scan(v)),
LazySeqState::Error(_) => t,
LazySeqState::Pending(_) | LazySeqState::Realizing { .. } => Opaque,
}
}
Value::Delay(p) => {
let t = tag_opaque(p);
if t == Opaque {
return Opaque;
}
match &*p.get().state.lock().unwrap() {
DelayState::Forced(v) => t.join(scan(v)),
DelayState::Pending(_) | DelayState::Realizing { .. } => Opaque,
DelayState::Error(_) => t,
}
}
Value::Atom(p) => tag_opaque(p),
Value::Volatile(p) => tag_opaque(p),
Value::Var(p) => tag_opaque(p),
Value::Promise(p) => tag_opaque(p),
Value::Future(p) => tag_opaque(p),
Value::Agent(p) => tag_opaque(p),
Value::Namespace(p) => tag_opaque(p),
Value::Protocol(p) => tag_opaque(p),
Value::Fn(_)
| Value::Macro(_)
| Value::BoundFn(_)
| Value::NativeFunction(_)
| Value::ProtocolFn(_)
| Value::MultiFn(_) => Opaque,
Value::TransientMap(_)
| Value::TransientSet(_)
| Value::TransientVector(_)
| Value::Matcher(_)
| Value::NativeObject(_) => Opaque,
}
}
#[cfg(not(feature = "no-gc"))]
pub fn publish_value(v: Value) -> Value {
if cljrs_gc::region::region_stack_depth() == 0
|| cljrs_gc::region::active_regions_are_poisoned()
{
return v;
}
match scan(&v) {
Scan::Clean => v,
Scan::Dirty => match serialize(&v) {
Ok(wire) => deserialize(wire),
Err(_) => {
cljrs_gc::region::poison_active_regions();
v
}
},
Scan::Opaque => {
cljrs_gc::region::poison_active_regions();
v
}
}
}
#[cfg(not(feature = "no-gc"))]
pub fn publish_for_owner(value: Value, _address: usize) -> Value {
publish_value(value)
}
#[cfg(feature = "no-gc")]
pub fn publish_value(value: Value) -> Value {
if !cljrs_gc::alloc_ctx::invocation_is_active() {
cljrs_gc::retain_regions_for(&value);
}
value
}
#[cfg(feature = "no-gc")]
pub fn publish_for_owner(value: Value, address: usize) -> Value {
publish_for_slot(value, address, 0)
}
#[cfg(feature = "no-gc")]
pub fn publish_for_slot(value: Value, address: usize, slot: usize) -> Value {
if cljrs_gc::alloc_ctx::invocation_is_active() {
return value;
}
let (value, _compact) = compact_scalar(value);
let lease = cljrs_gc::region::RegionLease::for_value(&value);
cljrs_gc::region::AllocationOwner::for_address(address).set_slot(address, slot, &lease);
value
}
#[cfg(not(feature = "no-gc"))]
pub fn publish_for_slot(value: Value, _address: usize, _slot: usize) -> Value {
publish_value(value)
}
pub fn publish_for_named_slot(value: Value, address: usize, slot: String) -> Value {
#[cfg(feature = "no-gc")]
{
if cljrs_gc::alloc_ctx::invocation_is_active() {
return value;
}
let (value, _compact) = compact_scalar(value);
let lease = cljrs_gc::region::RegionLease::for_value(&value);
cljrs_gc::region::AllocationOwner::for_address(address).set_slot(address, slot, &lease);
value
}
#[cfg(not(feature = "no-gc"))]
{
let _ = (address, slot);
publish_value(value)
}
}
pub fn read_slot(address: usize, slot: usize) {
#[cfg(feature = "no-gc")]
cljrs_gc::region::AllocationOwner::for_address(address).read_slot(address, slot);
#[cfg(not(feature = "no-gc"))]
let _ = (address, slot);
}
pub fn read_named_slot(address: usize, slot: String) {
#[cfg(feature = "no-gc")]
cljrs_gc::region::AllocationOwner::for_address(address).read_slot(address, slot);
#[cfg(not(feature = "no-gc"))]
let _ = (address, slot);
}
pub fn clear_slots(address: usize) {
#[cfg(feature = "no-gc")]
cljrs_gc::region::AllocationOwner::for_address(address).clear_slots(address);
#[cfg(not(feature = "no-gc"))]
let _ = address;
}
pub struct ValueSnapshot {
pub value: Value,
#[cfg(feature = "no-gc")]
pub(crate) lease: cljrs_gc::region::RegionLease,
}
impl ValueSnapshot {
pub fn protect_current(&self) {
#[cfg(feature = "no-gc")]
self.lease.protect_read();
}
}
#[cfg(feature = "no-gc")]
#[derive(Debug, Default)]
pub struct ValueLease {
owned: Option<cljrs_gc::region::RegionLease>,
scoped: cljrs_gc::region::RegionDependency,
}
#[cfg(feature = "no-gc")]
impl ValueLease {
pub(crate) fn for_trace(
value: &impl cljrs_gc::Trace,
owner: &cljrs_gc::region::AllocationOwner,
) -> Self {
if cljrs_gc::alloc_ctx::invocation_is_active() {
return Self::default();
}
Self::from_lease(cljrs_gc::region::RegionLease::for_value(value), owner)
}
pub fn for_values(values: &[Value], owner: &cljrs_gc::region::AllocationOwner) -> Self {
if cljrs_gc::alloc_ctx::invocation_is_active() {
return Self::default();
}
Self::from_lease(cljrs_gc::region::RegionLease::for_values(values), owner)
}
fn from_lease(
lease: cljrs_gc::region::RegionLease,
owner: &cljrs_gc::region::AllocationOwner,
) -> Self {
if owner.is_permanent() {
Self {
owned: Some(lease),
scoped: Default::default(),
}
} else {
Self {
scoped: owner.replaceable(&lease),
owned: None,
}
}
}
pub fn snapshot(&self) -> cljrs_gc::region::RegionLease {
self.owned
.as_ref()
.cloned()
.unwrap_or_else(|| self.scoped.snapshot())
}
pub(crate) fn reparent(&self, owner: &cljrs_gc::region::AllocationOwner) -> Self {
Self::from_lease(self.snapshot(), owner)
}
pub fn protect_read(&self) {
if let Some(lease) = &self.owned {
lease.protect_read();
} else {
self.scoped.protect_read();
}
}
pub fn publish_return(&self) {
if let Some(lease) = &self.owned {
lease.publish_return();
} else {
self.scoped.snapshot().publish_return();
}
}
pub fn protect_current(&self) {
if let Some(lease) = &self.owned {
lease.protect_current();
} else {
self.scoped.snapshot().protect_current();
}
}
}
#[cfg(feature = "no-gc")]
fn compact_scalar(value: Value) -> (Value, Option<cljrs_gc::alloc_ctx::ScratchGuard>) {
macro_rules! copy_leaf {
($pointer:expr, $variant:path) => {{
let pointer = $pointer;
if pointer.is_static_alloc() || pointer.is_relocated_immutable() {
return ($variant(pointer), None);
}
let arena = cljrs_gc::alloc_ctx::ScratchGuard::with_capacity(
std::mem::size_of_val(pointer.get()) + 32,
);
let copy = unsafe { pointer.clone_immutable() };
return ($variant(copy), Some(arena));
}};
}
match value {
Value::Str(p) if p.get().len() <= 4096 => copy_leaf!(p, Value::Str),
Value::Uuid(p) => copy_leaf!(p, Value::Uuid),
Value::BigInt(p) if p.get().bits() <= 32768 => copy_leaf!(p, Value::BigInt),
Value::Ratio(p)
if p.get()
.numer()
.bits()
.saturating_add(p.get().denom().bits())
<= 32768 =>
{
copy_leaf!(p, Value::Ratio)
}
other => (other, None),
}
}
#[cfg(feature = "no-gc")]
pub(crate) fn prepare_definition(
value: Value,
owner: &cljrs_gc::region::AllocationOwner,
) -> (Value, ValueLease) {
if cljrs_gc::alloc_ctx::invocation_is_active() {
return prepare_for_owner(value, owner);
}
fn worth_copying(value: &Value) -> bool {
let mut pending = vec![(value.clone(), 0usize)];
let mut remaining = 128usize;
let mut needs_copy = false;
while let Some((value, depth)) = pending.pop() {
if remaining == 0 || depth > 32 {
return false;
}
remaining -= 1;
macro_rules! leaf {
($p:expr, $bounded:expr) => {{
let p = $p;
if !p.is_static_alloc() && !p.is_relocated_immutable() {
if !$bounded {
return false;
}
needs_copy = true;
}
}};
}
macro_rules! children {
($p:expr, $iter:expr) => {{
let p = $p;
if !p.is_static_alloc() && !p.is_relocated_immutable() {
needs_copy = true;
for child in $iter(&p) {
if pending.len() >= remaining {
return false;
}
pending.push((child, depth + 1));
}
}
}};
}
match value {
Value::Str(p) => {
let bounded = p.get().len() <= 4096;
leaf!(p, bounded);
}
Value::BigInt(p) => {
let bounded = p.get().bits() <= 32768;
leaf!(p, bounded);
}
Value::Ratio(p) => {
let bounded = p
.get()
.numer()
.bits()
.saturating_add(p.get().denom().bits())
<= 32768;
leaf!(p, bounded);
}
Value::BigDecimal(p) => {
let bounded = p.get().digits() <= 9865;
leaf!(p, bounded);
}
Value::Uuid(p) => leaf!(p, true),
Value::Symbol(p) => leaf!(p, true),
Value::Keyword(p) => leaf!(p, true),
Value::Cons(p) => children!(p, |p: &cljrs_gc::GcPtr<crate::CljxCons>| [
p.get().head.clone(),
p.get().tail.clone()
]),
Value::Vector(p) => children!(p, |p: &cljrs_gc::GcPtr<crate::PersistentVector>| p
.get()
.iter()
.take(129)
.cloned()
.collect::<Vec<_>>()),
Value::List(p) => children!(p, |p: &cljrs_gc::GcPtr<crate::PersistentList>| p
.get()
.iter()
.take(129)
.cloned()
.collect::<Vec<_>>()),
Value::Queue(p) => children!(p, |p: &cljrs_gc::GcPtr<crate::PersistentQueue>| p
.get()
.iter()
.take(129)
.cloned()
.collect::<Vec<_>>()),
Value::Map(crate::MapValue::Array(p)) => {
children!(p, |p: &cljrs_gc::GcPtr<crate::PersistentArrayMap>| p
.get()
.iter()
.take(65)
.flat_map(|(k, v)| [k.clone(), v.clone()])
.collect::<Vec<_>>())
}
Value::Map(crate::MapValue::Hash(p)) => {
children!(p, |p: &cljrs_gc::GcPtr<crate::PersistentHashMap>| p
.get()
.iter()
.take(65)
.flat_map(|(k, v)| [k.clone(), v.clone()])
.collect::<Vec<_>>())
}
Value::Set(crate::SetValue::Hash(p)) => {
children!(p, |p: &cljrs_gc::GcPtr<crate::PersistentHashSet>| p
.get()
.iter()
.take(129)
.cloned()
.collect::<Vec<_>>())
}
_ => {}
}
}
needs_copy
}
if !worth_copying(&value) {
return prepare_for_owner(value, owner);
}
let _compact = cljrs_gc::alloc_ctx::ScratchGuard::with_capacity(256);
fn copy(value: Value, memo: &mut std::collections::HashMap<usize, Value>) -> Value {
macro_rules! leaf {
($pointer:expr, $variant:path) => {{
let p = $pointer;
if p.is_static_alloc() || p.is_relocated_immutable() {
return $variant(p);
}
let address = p.get() as *const _ as usize;
if let Some(value) = memo.get(&address) {
return value.clone();
}
let value = $variant(unsafe { p.clone_immutable() });
memo.insert(address, value.clone());
value
}};
}
macro_rules! collection {
($pointer:expr, $variant:expr, $data:expr) => {{
let p = $pointer;
let address = p.get() as *const _ as usize;
if p.is_static_alloc() || p.is_relocated_immutable() {
($variant)(p)
} else if let Some(value) = memo.get(&address) {
value.clone()
} else {
let data = $data(&p, memo);
let value = ($variant)(unsafe { p.relocate_immutable(data) });
memo.insert(address, value.clone());
value
}
}};
}
match value {
Value::Str(p) => leaf!(p, Value::Str),
Value::Uuid(p) => leaf!(p, Value::Uuid),
Value::BigInt(p) => leaf!(p, Value::BigInt),
Value::BigDecimal(p) => leaf!(p, Value::BigDecimal),
Value::Ratio(p) => leaf!(p, Value::Ratio),
Value::Symbol(p) => leaf!(p, Value::Symbol),
Value::Keyword(p) => leaf!(p, Value::Keyword),
Value::Queue(p) => collection!(
p,
Value::Queue,
|p: &cljrs_gc::GcPtr<crate::PersistentQueue>, memo: &mut _| {
p.get()
.iter()
.cloned()
.fold(crate::PersistentQueue::empty(), |queue, value| {
queue.conj(copy(value, memo))
})
}
),
Value::Cons(p) => collection!(
p,
Value::Cons,
|p: &cljrs_gc::GcPtr<crate::CljxCons>, memo: &mut _| crate::CljxCons {
head: copy(p.get().head.clone(), memo),
tail: copy(p.get().tail.clone(), memo)
}
),
Value::List(p) => collection!(
p,
Value::List,
|p: &cljrs_gc::GcPtr<crate::PersistentList>, memo: &mut _| {
crate::PersistentList::from_iter(p.get().iter().cloned().map(|v| copy(v, memo)))
}
),
Value::Vector(p) => collection!(
p,
Value::Vector,
|p: &cljrs_gc::GcPtr<crate::PersistentVector>, memo: &mut _| {
let values: Vec<_> = p.get().iter().cloned().map(|v| copy(v, memo)).collect();
if p.get().is_map_entry() {
crate::PersistentVector::map_entry(values[0].clone(), values[1].clone())
} else {
crate::PersistentVector::from_iter(values)
}
}
),
Value::Map(crate::MapValue::Array(p)) => collection!(
p,
|p| Value::Map(crate::MapValue::Array(p)),
|p: &cljrs_gc::GcPtr<crate::PersistentArrayMap>, memo: &mut _| {
let crate::collections::array_map::AssocResult::Array(map) =
crate::PersistentArrayMap::from_pairs(
p.get()
.iter()
.map(|(k, v)| (copy(k.clone(), memo), copy(v.clone(), memo))),
)
else {
unreachable!("compaction preserves map keys")
};
map
}
),
Value::Map(crate::MapValue::Hash(p)) => collection!(
p,
|p| Value::Map(crate::MapValue::Hash(p)),
|p: &cljrs_gc::GcPtr<crate::PersistentHashMap>, memo: &mut _| {
crate::PersistentHashMap::from_pairs(
p.get()
.iter()
.map(|(k, v)| (copy(k.clone(), memo), copy(v.clone(), memo))),
)
}
),
Value::Set(crate::SetValue::Hash(p)) => collection!(
p,
|p| Value::Set(crate::SetValue::Hash(p)),
|p: &cljrs_gc::GcPtr<crate::PersistentHashSet>, memo: &mut _| {
crate::PersistentHashSet::from_iter(
p.get().iter().cloned().map(|v| copy(v, memo)),
)
}
),
other => other,
}
}
let value = copy(value, &mut Default::default());
prepare_for_owner(value, owner)
}
#[cfg(feature = "no-gc")]
pub fn prepare_for_owner(
value: Value,
owner: &cljrs_gc::region::AllocationOwner,
) -> (Value, ValueLease) {
use cljrs_gc::region::RegionLease;
if cljrs_gc::alloc_ctx::invocation_is_active() {
return (value, Default::default());
}
let (value, _compact) = compact_scalar(value);
let lease = RegionLease::for_value(&value);
let cached = ValueLease::from_lease(lease, owner);
(value, cached)
}
#[cfg(feature = "no-gc")]
pub fn publish_return(value: Value) -> Value {
if cljrs_gc::alloc_ctx::is_static_context() && !cljrs_gc::region::ReadScope::is_active() {
publish_value(value)
} else {
cljrs_gc::region::RegionLease::for_value(&value).protect_current();
value
}
}
#[cfg(all(test, not(feature = "no-gc")))]
mod tests {
use super::*;
use crate::PersistentVector;
use cljrs_gc::region::{Region, RegionGuard};
fn region_vec(region: &mut Region, items: Vec<Value>) -> Value {
Value::Vector(region.alloc(PersistentVector::from_iter(items)))
}
#[test]
fn clean_value_passes_through_untouched() {
let mut region = Region::new();
let _guard = unsafe { RegionGuard::new(&mut region) };
let v = Value::Vector(GcPtr::new(PersistentVector::from_iter([Value::Long(1)])));
let Value::Vector(before) = &v else { panic!() };
let before = before.clone();
let out = publish_value(v);
let Value::Vector(after) = &out else { panic!() };
assert!(
GcPtr::ptr_eq(&before, after),
"heap value must not be copied"
);
}
#[test]
fn region_value_is_promoted_to_heap() {
let mut region = Region::new();
let inner = region_vec(&mut region, vec![Value::Long(1), Value::Long(2)]);
let map = Value::Map(crate::MapValue::from_pairs(vec![(Value::Long(0), inner)]));
let _guard = unsafe { RegionGuard::new(&mut region) };
let out = publish_value(map);
assert_eq!(scan(&out), Scan::Clean, "promoted copy must be heap-only");
let Value::Map(m) = &out else { panic!() };
let got = {
let mut got = None;
m.for_each(|_, v| got = Some(v.clone()));
got.unwrap()
};
assert_eq!(
got,
Value::Vector(GcPtr::new(PersistentVector::from_iter([
Value::Long(1),
Value::Long(2)
])))
);
}
#[test]
fn no_active_region_short_circuits() {
let mut region = Region::new();
let v = region_vec(&mut region, vec![Value::Long(5)]);
let out = publish_value(v.clone());
assert_eq!(out, v);
}
#[test]
fn retained_regions_skip_repeated_publication_and_keep_values_alive() {
let mut region = Box::new(Region::new());
let child = region_vec(&mut region, vec![Value::Long(42)]);
let value = Value::Vector(GcPtr::new(PersistentVector::from_iter([child])));
unsafe { cljrs_gc::region::push_region_raw(region.as_mut() as *mut Region) };
cljrs_gc::region::poison_active_regions();
let out = publish_value(value.clone());
let (Value::Vector(before), Value::Vector(after)) = (&value, &out) else {
panic!()
};
assert!(
GcPtr::ptr_eq(before, after),
"retained graphs need no scan or copy"
);
cljrs_gc::region::close_region(region);
assert_eq!(format!("{out}"), "[[42]]");
}
#[test]
fn opaque_value_poisons_active_regions() {
use std::sync::{Arc, Mutex};
#[derive(Debug)]
struct Tracked(Arc<Mutex<bool>>);
impl Drop for Tracked {
fn drop(&mut self) {
*self.0.lock().unwrap() = true;
}
}
impl Trace for Tracked {
fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
}
let dropped = Arc::new(Mutex::new(false));
let mut region = Box::new(Region::new());
region.alloc(Tracked(dropped.clone()));
let lazy = {
struct T;
impl std::fmt::Debug for T {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "T")
}
}
impl Trace for T {
fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
}
impl crate::types::Thunk for T {
fn force(&self) -> Result<Value, String> {
Ok(Value::Nil)
}
}
Value::LazySeq(GcPtr::new(crate::types::LazySeq::new(Box::new(T))))
};
unsafe { cljrs_gc::region::push_region_raw(region.as_mut() as *mut Region) };
let _ = publish_value(lazy);
cljrs_gc::region::close_region(region);
assert!(
!*dropped.lock().unwrap(),
"publishing an opaque value must retire the active region"
);
}
}