ce6e36112d
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
172 lines
5.0 KiB
Rust
172 lines
5.0 KiB
Rust
//! A circuit moves between threads, one at a time (`context/code/zset.md`, "Threads").
|
|
//! Evidence: `context/test-evidence/zset.md`.
|
|
|
|
use std::thread;
|
|
use zset::{Circuit, Input, Output, Rebuild, Stream, ZSet};
|
|
|
|
type Z<K> = ZSet<K>;
|
|
|
|
fn is_send<T: Send>() {}
|
|
|
|
#[test]
|
|
fn a_circuit_and_every_handle_it_hands_out_is_send() {
|
|
is_send::<Circuit>();
|
|
is_send::<Stream<Z<u8>>>();
|
|
is_send::<Input<u8>>();
|
|
is_send::<Output<u8>>();
|
|
is_send::<Rebuild<u8>>();
|
|
}
|
|
|
|
type Outs = (Output<(u8, i64)>, Output<(u8, u8)>);
|
|
|
|
fn delta(n: u8) -> Z<(u8, u8)> {
|
|
[((n % 5, n), 1), (((n + 1) % 5, n / 2), -1)]
|
|
.into_iter()
|
|
.collect()
|
|
}
|
|
|
|
/// The pipeline's outputs after `ticks` steps, collecting what `take` returns each time.
|
|
fn collect(c: &mut Circuit, i: &Input<(u8, u8)>, outs: &Outs, n: u8) -> String {
|
|
c.push(i, delta(n));
|
|
c.step();
|
|
format!("{:?} {:?}", c.take(&outs.0), c.take(&outs.1))
|
|
}
|
|
|
|
#[test]
|
|
fn a_circuit_built_on_one_thread_steps_on_another_and_returns() {
|
|
let build = || {
|
|
let mut c = Circuit::new();
|
|
let (input, s) = c.input::<(u8, u8)>();
|
|
let i = c.integrate(&s);
|
|
let d = c.distinct(&i);
|
|
let g = c.aggregate(&d, |_, vals| {
|
|
Some(vals.iter().map(|(v, w)| i64::from(*v) * w).sum::<i64>())
|
|
});
|
|
let f = c.filter(&s, |(k, _)| k % 2 == 0);
|
|
let m = c.delay(&f);
|
|
let outs = (c.output(&g), c.output(&m));
|
|
(c, input, outs)
|
|
};
|
|
let (mut one, i1, o1) = build();
|
|
let want: Vec<String> = (0..9).map(|n| collect(&mut one, &i1, &o1, n)).collect();
|
|
|
|
let (mut c, input, outs) = build();
|
|
let mut got = Vec::new();
|
|
for n in 0..9 {
|
|
// Each tick runs on a fresh thread: the circuit is moved in and moved out again.
|
|
let (back, line) = thread::spawn(move || {
|
|
let line = collect(&mut c, &input, &outs, n);
|
|
(c, line)
|
|
})
|
|
.join()
|
|
.unwrap();
|
|
c = back;
|
|
got.push(line);
|
|
}
|
|
assert_eq!(got, want, "[threads] each tick on its own thread");
|
|
}
|
|
|
|
#[test]
|
|
fn a_transaction_begun_on_one_thread_is_rolled_back_on_another() {
|
|
let mut c = Circuit::new();
|
|
let (input, s) = c.input::<u8>();
|
|
let i = c.integrate(&s);
|
|
let out = c.output(&i);
|
|
c.push(&input, Z::singleton(1, 1));
|
|
c.step();
|
|
let before = (c.current(&i), c.take(&out));
|
|
c.begin();
|
|
c.push(&input, Z::singleton(2, 1));
|
|
c.step();
|
|
let c = thread::spawn(move || {
|
|
let mut c = c;
|
|
c.rollback();
|
|
c
|
|
})
|
|
.join()
|
|
.unwrap();
|
|
let mut c = c;
|
|
assert_eq!(c.current(&i), before.0, "[txn] the stream is as it was");
|
|
assert_eq!(
|
|
c.take(&out),
|
|
Z::new(),
|
|
"[txn] the sink holds nothing of the step"
|
|
);
|
|
assert_eq!(c.ticks(), 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"
|
|
);
|
|
}
|
|
}
|