#![warn(missing_docs)]
#![deny(missing_debug_implementations)]
mod arena;
mod context;
mod effect;
mod iter;
mod memo;
mod signal;
use std::any::{Any, TypeId};
use std::cell::RefCell;
use std::marker::PhantomData;
use std::mem;
use std::rc::{Rc, Weak};
use ahash::AHashMap;
use arena::*;
pub use context::*;
pub use effect::*;
use indexmap::IndexMap;
pub use iter::*;
pub use memo::*;
pub use signal::*;
use slotmap::{DefaultKey, SlotMap};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct InvariantLifetime<'id>(PhantomData<&'id mut &'id ()>);
#[derive(Default)]
struct ScopeInner<'a> {
cleanups: Vec<Box<dyn FnOnce() + 'a>>,
child_scopes: SlotMap<DefaultKey, *mut ScopeRaw<'a>>,
#[allow(clippy::box_collection)]
contexts: Option<Box<AHashMap<TypeId, &'a dyn Any>>>,
_phantom: InvariantLifetime<'a>,
}
struct ScopeRaw<'a> {
inner: RefCell<ScopeInner<'a>>,
arena: ScopeArena<'a>,
parent: Option<*const ScopeRaw<'a>>,
}
#[derive(Clone, Copy)]
pub struct BoundedScope<'a, 'b: 'a> {
raw: &'a ScopeRaw<'a>,
_phantom: PhantomData<&'b ()>,
}
impl<'a, 'b: 'a> std::fmt::Debug for BoundedScope<'a, 'b> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BoundedScope").finish()
}
}
impl<'a, 'b: 'a> BoundedScope<'a, 'b> {
fn new(raw: &'a ScopeRaw<'a>) -> Self {
Self {
raw,
_phantom: PhantomData,
}
}
fn alloc<T>(&self, value: T) -> &'a mut T {
self.raw.arena.alloc(value)
}
}
pub type Scope<'a> = BoundedScope<'a, 'a>;
impl<'a> ScopeRaw<'a> {
pub(crate) fn new() -> Self {
Self {
inner: RefCell::new(ScopeInner {
cleanups: Default::default(),
child_scopes: Default::default(),
contexts: None,
_phantom: Default::default(),
}),
arena: Default::default(),
parent: None,
}
}
}
pub struct ScopeDisposer<'a> {
f: Box<dyn FnOnce() + 'a>,
}
impl<'a> std::fmt::Debug for ScopeDisposer<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScopeDisposer").finish()
}
}
impl<'a> ScopeDisposer<'a> {
fn new(f: impl FnOnce() + 'a) -> Self {
Self { f: Box::new(f) }
}
pub unsafe fn dispose(self) {
(self.f)();
}
}
#[must_use = "not calling the disposer function will result in a memory leak"]
pub fn create_scope<'disposer>(f: impl for<'a> FnOnce(Scope<'a>)) -> ScopeDisposer<'disposer> {
let cx = ScopeRaw::new();
let boxed = Box::new(cx);
let ptr = Box::into_raw(boxed);
untrack(|| f(unsafe { Scope::new(&*ptr) }));
ScopeDisposer::new(move || unsafe {
let boxed = Box::from_raw(ptr);
boxed.dispose();
})
}
pub fn create_child_scope<'a, F>(cx: Scope<'a>, f: F) -> ScopeDisposer<'a>
where
F: for<'child_lifetime> FnOnce(BoundedScope<'child_lifetime, 'a>),
{
let mut child = ScopeRaw::new();
child.parent = Some(unsafe { std::mem::transmute(cx.raw as *const _) });
let boxed = Box::new(child);
let ptr = Box::into_raw(boxed);
let key = cx.raw.inner.borrow_mut().child_scopes.insert(ptr);
f(unsafe { Scope::new(&*ptr) });
ScopeDisposer::new(move || unsafe {
let cx = cx.raw.inner.borrow_mut().child_scopes.remove(key).unwrap();
let cx = Box::from_raw(cx);
cx.dispose();
})
}
pub fn create_scope_immediate(f: impl for<'a> FnOnce(Scope<'a>)) {
let disposer = create_scope(f);
unsafe {
disposer.dispose();
}
}
pub fn create_ref<T>(cx: Scope, value: T) -> &T {
cx.raw.arena.alloc(value)
}
pub fn on_cleanup<'a>(cx: Scope<'a>, f: impl FnOnce() + 'a) {
cx.raw.inner.borrow_mut().cleanups.push(Box::new(f));
}
pub fn use_scope_status(cx: Scope) -> RcSignal<bool> {
let status = create_rc_signal(true);
on_cleanup(cx, {
let status = status.clone();
move || status.set(false)
});
status
}
impl<'a> ScopeRaw<'a> {
pub(crate) unsafe fn dispose(&self) {
let mut inner = self.inner.borrow_mut();
for &child in mem::take(&mut inner.child_scopes).values() {
let cx = Box::from_raw(child);
cx.dispose();
}
untrack(|| {
for cb in mem::take(&mut inner.cleanups) {
cb();
}
});
self.arena.dispose();
}
}
impl Drop for ScopeRaw<'_> {
fn drop(&mut self) {
unsafe { self.dispose() };
}
}
pub fn on<'a, U, const N: usize>(
dependencies: [&'a (dyn AnyReadSignal<'a> + 'a); N],
mut f: impl FnMut() -> U + 'a,
) -> impl FnMut() -> U + 'a {
move || {
for i in dependencies {
i.track();
}
untrack(&mut f)
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use super::*;
#[test]
fn refs() {
let disposer = create_scope(|cx| {
let r = create_ref(cx, 0);
on_cleanup(cx, move || {
let _ = r; dbg!(r);
})
});
unsafe {
disposer.dispose();
}
}
#[test]
fn cleanup() {
create_scope_immediate(|cx| {
let cleanup_called = create_signal(cx, false);
let disposer = create_child_scope(cx, |cx| {
on_cleanup(cx, || {
cleanup_called.set(true);
});
});
assert!(!*cleanup_called.get());
unsafe {
disposer.dispose();
}
assert!(*cleanup_called.get());
});
}
#[test]
fn cleanup_in_effect() {
create_scope_immediate(|cx| {
let trigger = create_signal(cx, ());
let counter = create_signal(cx, 0);
create_effect_scoped(cx, |cx| {
trigger.track();
on_cleanup(cx, || {
counter.set(*counter.get() + 1);
});
});
assert_eq!(*counter.get(), 0);
trigger.set(());
assert_eq!(*counter.get(), 1);
trigger.set(());
assert_eq!(*counter.get(), 2);
});
}
#[test]
fn cleanup_is_untracked() {
create_scope_immediate(|cx| {
let trigger = create_signal(cx, ());
let counter = create_signal(cx, 0);
create_effect_scoped(cx, |cx| {
counter.set(*counter.get_untracked() + 1);
on_cleanup(cx, || {
trigger.track(); });
});
assert_eq!(*counter.get(), 1);
trigger.set(());
assert_eq!(*counter.get(), 1);
});
}
#[test]
fn can_store_disposer_in_own_signal() {
create_scope_immediate(|cx| {
let signal = create_signal(cx, None);
let disposer = create_child_scope(cx, |_cx| {});
signal.set(Some(disposer));
});
}
#[test]
fn refs_are_dropped_on_dispose() {
thread_local! {
static COUNTER: Cell<u32> = Cell::new(0);
}
struct IncOnDrop;
impl Drop for IncOnDrop {
fn drop(&mut self) {
COUNTER.with(|c| c.set(c.get() + 1));
}
}
struct AssertDropCount {
count: u32,
}
impl Drop for AssertDropCount {
fn drop(&mut self) {
assert_eq!(COUNTER.with(Cell::get), self.count);
}
}
assert_eq!(COUNTER.with(Cell::get), 0);
let disposer = create_scope(|cx| {
create_ref(cx, IncOnDrop);
});
assert_eq!(COUNTER.with(Cell::get), 0);
unsafe { disposer.dispose() };
assert_eq!(COUNTER.with(Cell::get), 1);
let disposer = create_scope(|cx| {
create_ref(cx, AssertDropCount { count: 2 }); create_ref(cx, IncOnDrop);
create_ref(cx, AssertDropCount { count: 1 }); });
unsafe { disposer.dispose() };
}
#[test]
fn access_previous_ref_in_drop() {
struct ReadRefOnDrop<'a> {
r: &'a i32,
expect: i32,
}
impl<'a> Drop for ReadRefOnDrop<'a> {
fn drop(&mut self) {
assert_eq!(*self.r, self.expect);
}
}
let disposer = create_scope(|cx| {
let r = create_ref(cx, 123);
create_ref(cx, ReadRefOnDrop { r, expect: 123 });
});
unsafe { disposer.dispose() };
}
}