Merge dbsp/zset-send into dbsp/zset-edge

# Conflicts:
#	context/test-evidence/zset.md
This commit is contained in:
Claude (dbsp-threads)
2026-10-10 21:26:54 -07:00
committed by Edward Langley
7 changed files with 142 additions and 41 deletions
+3 -1
View File
@@ -20,7 +20,9 @@
* - Not thread-safe, but movable. A ZsCircuit, a ZsRebuild and a ZsZSet may each be handed to
* another thread; use each from one thread at a time (a zs_rebuild_step call uses both its
* job and its circuit). Callbacks run on whichever thread calls zs_circuit_step or
* zs_rebuild_step, so a ctx must be usable from every thread that may step. Streams, inputs and sinks are uint32_t ids local to their circuit; a sink id is
* zs_rebuild_step, with THAT thread's thread-local state (Lisp dynamic bindings, errno,
* zs_last_error), so a ctx must be usable from every thread that may step.
* - Streams, inputs and sinks are uint32_t ids local to their circuit; a sink id is
* never a stream id (they are refused as ZS_ERR_BAD_HANDLE), but an id from another circuit
* is not detected. A ZsRebuild is stepped only on the circuit that made it (else
* ZS_ERR_BAD_HANDLE), even if another circuit now lives at the old address.
+10 -18
View File
@@ -86,7 +86,7 @@ impl<T> Slot<T> {
}
/// The index of one value in a circuit's arena, typed so a read cannot name the wrong type.
struct Id<T>(usize, PhantomData<fn() -> T>);
struct Id<T>(usize, PhantomData<fn(T) -> T>);
impl<T> Clone for Id<T> {
fn clone(&self) -> Self {
@@ -114,26 +114,18 @@ impl Arena {
self.0[id.0].downcast_mut().expect("zset: arena type")
}
/// One value to read and another to write, which are never the same entry.
/// One value to read and another, allocated after it, to write.
fn read_write<R: 'static, W: 'static>(&mut self, r: Id<R>, w: Id<W>) -> (&R, &mut W) {
assert_ne!(
r.0, w.0,
"zset: an entry cannot be read and written at once"
let (lo, hi) = self.0.split_at_mut(w.0);
assert!(
r.0 < w.0,
"zset: the read entry comes before the written one"
);
let ty = "zset: arena type";
if r.0 < w.0 {
let (lo, hi) = self.0.split_at_mut(w.0);
(
lo[r.0].downcast_ref().expect(ty),
hi[0].downcast_mut().expect(ty),
)
} else {
let (lo, hi) = self.0.split_at_mut(r.0);
(
hi[0].downcast_ref().expect(ty),
lo[w.0].downcast_mut().expect(ty),
)
}
(
lo[r.0].downcast_ref().expect(ty),
hi[0].downcast_mut().expect(ty),
)
}
}
+16
View File
@@ -82,6 +82,22 @@ fn an_integrate_stream_holds_its_integral_twice_when_the_tick_is_whole() {
assert_eq!(live(), base, "[leak] every key is dropped with the circuit");
}
#[test]
fn a_sink_adds_its_input_with_one_clone_per_key() {
let mut c = Circuit::new();
let (input, s) = c.input::<Counted>();
let sink = c.output(&s);
c.push(&input, keys(N));
let before = clones();
c.step();
assert_eq!(
clones() - before,
N as usize,
"[sink] one clone per key it sums"
);
assert_eq!(c.take(&sink).len(), N as usize);
}
type E = (Counted, u8);
#[derive(Clone, Copy)]
+73 -6
View File
@@ -95,10 +95,77 @@ fn a_transaction_begun_on_one_thread_is_rolled_back_on_another() {
assert_eq!(c.ticks(), 1);
}
#[test]
#[should_panic(expected = "handle from another circuit")]
fn a_handle_from_another_circuit_is_refused() {
let (mut a, mut b) = (Circuit::new(), Circuit::new());
let (input, _) = a.input::<u8>();
b.push(&input, Z::singleton(1, 1));
trait AmbiguousIfSync<A> {
fn some_item() {}
}
impl<T: ?Sized> AmbiguousIfSync<()> for T {}
struct IsSync;
impl<T: ?Sized + Sync> AmbiguousIfSync<IsSync> for T {}
// Fails to build (E0283) if `Circuit` becomes `Sync`.
#[test]
fn a_circuit_is_not_sync() {
let _ = <Circuit as AmbiguousIfSync<_>>::some_item;
}
type Site = fn(&mut Circuit, &Input<u8>, &Stream<Z<u8>>, &Output<u8>);
/// Two circuits of one shape, so a foreign handle indexes a live slot of the same type.
fn twin() -> (Circuit, Input<u8>, Stream<Z<u8>>, Output<u8>) {
let mut c = Circuit::new();
let (i, s) = c.input::<u8>();
let o = c.output(&s);
(c, i, s, o)
}
fn panic_text(f: impl FnOnce() + std::panic::UnwindSafe) -> Option<String> {
let e = std::panic::catch_unwind(f).err()?;
e.downcast_ref::<String>()
.cloned()
.or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string()))
}
#[test]
fn every_call_that_takes_a_handle_refuses_a_foreign_one() {
let sites: [(&str, Site); 10] = [
("current", |c, _, s, _| {
let _ = c.current(s);
}),
("take", |c, _, _, o| drop(c.take(o))),
("push", |c, i, _, _| c.push(i, Z::new())),
("unary (map)", |c, _, s, _| {
let _ = c.map(s, |k| *k);
}),
("binary, first operand", |c, _, s, _| {
let (_, own) = c.input::<u8>();
let _ = c.plus(s, &own);
}),
("binary, second operand", |c, _, s, _| {
let (_, own) = c.input::<u8>();
let _ = c.plus(&own, s);
}),
("output", |c, _, s, _| {
let _ = c.output(s);
}),
("delay", |c, _, s, _| {
let _ = c.delay(s);
}),
("integrate", |c, _, s, _| {
let _ = c.integrate(s);
}),
("distinct", |c, _, s, _| {
let _ = c.distinct(s);
}),
];
for (name, site) in sites {
let (mut mine, ..) = twin();
let (_, fi, fs, fo) = twin();
let text = panic_text(std::panic::AssertUnwindSafe(|| {
site(&mut mine, &fi, &fs, &fo)
}));
assert!(
text.is_some_and(|t| t.contains("handle from another circuit")),
"[foreign] {name} accepted a handle from another circuit"
);
}
}