1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//! The tracking frame must be popped on the unwind path (`#lzspecedgeindex`).
//!
//! Reading a disposed node panics — that is this library's expression of the
//! spec's `read_after_dispose`. Such a read happens *inside* a compute closure
//! whenever a surviving dependent is recomputed after its dependency was
//! disposed, which is exactly the survivor shape
//! `disposal_does_not_run_surviving_effects.json` describes. A bare
//! `push_tracking_frame` / `pop_tracking_frame` pair skips the pop on unwind,
//! leaving the dead node as the current frame so every *later* top-level read
//! silently registers a dependency edge against it. The corruption surfaces
//! arbitrarily far from the throw, which is what makes it worth pinning.
use std::panic::{self, AssertUnwindSafe};
use lazily::Context;
/// Swallow a panic and its message.
fn quiet<R>(f: impl FnOnce() -> R) -> Result<R, ()> {
let prev = panic::take_hook();
panic::set_hook(Box::new(|_| {}));
let out = panic::catch_unwind(AssertUnwindSafe(f));
panic::set_hook(prev);
out.map_err(|_| ())
}
#[test]
fn a_panic_out_of_a_slot_compute_does_not_strand_the_frame() {
let ctx = Context::new();
let src = ctx.source(1i64);
let mid = ctx.computed(move |c| c.get(&src) + 1);
let survivor = ctx.computed(move |c| c.get(&mid) * 10);
assert_eq!(ctx.get(&survivor), 20);
ctx.dispose_slot(&mid);
assert!(
quiet(|| ctx.get(&survivor)).is_err(),
"reading through a disposed dependency must error"
);
// A top-level read is not inside any compute, so it must register nothing.
let fresh = ctx.source(100i64);
assert_eq!(ctx.get(&fresh), 100);
assert_eq!(
ctx.dependent_count(&fresh),
0,
"stranded tracking frame: a top-level read registered a dependent"
);
}
#[test]
fn a_panic_out_of_an_effect_body_does_not_strand_the_frame() {
let ctx = Context::new();
let src = ctx.source(1i64);
let mid = ctx.computed(move |c| c.get(&src) + 1);
// The effect reads `src` directly as well as through `mid`. The direct edge
// is what survives the disposal and gives the write below something to
// invalidate, so the effect actually reruns and reaches the disposed `mid`.
// Reading only `mid` would not do: disposal detaches that edge, the write
// would reach nothing, and the effect body would never run again.
let _watch = ctx.effect(move |c| {
let _ = c.get(&src);
let _ = c.get(&mid);
});
ctx.dispose_slot(&mid);
// Disposal must not run the survivor, so force the rerun explicitly to
// reach the effect-body unwind path this test is about.
assert!(
quiet(|| ctx.set(&src, 2)).is_err(),
"rerunning an effect over a disposed dependency must error"
);
let fresh = ctx.source(100i64);
assert_eq!(ctx.get(&fresh), 100);
assert_eq!(
ctx.dependent_count(&fresh),
0,
"stranded tracking frame: a top-level read registered a dependent"
);
}