Source
tests/purrc/thread_sys_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
// System-level e2e for the `thread` module (suite StdlibE2E, like every module's *_sys_test): one4
// program driving the whole surface — spawn with mixed argument types, sequential join chains,5
// the `with` guard, joinable(), and a worker `raise` recovered in-language with try/except (the6
// per-venue retry shape a real multi-feed collector uses). Deterministic by construction: a7
// worker's output is only observed behind its join.8
//9
// ThreadOwnerParamLowering asserts the GENERATED C++ (not just behavior): a worker parameter10
// typed `memory.Owner<int>` must lower to a concrete `Owner<long long>&` — the same type11
// `memory.own(0)` deduces — so a pinned Owner travels into a thread by reference. It only12
// asserts the signature (the program never exercises the Owner engine, which is TDD elsewhere).13
#include <string>15
#include "e2e_harness.hpp"17
TEST(StdlibE2E, Thread) {18
e2e::expect_e2e("thread_sys", R"PURR(import io19
import thread21
fn double_and_report(n : int, tag : str) {22
io.print(tag, n * 2)23
}25
fn flaky(attempt : int) {26
if attempt < 2 {27
raise "feed dropped"28
}29
io.print("feed up on attempt", attempt)30
}32
fn main() {33
# spawn -> join, one at a time (deterministic output ordering)34
let t1 = thread.spawn(double_and_report, 21, "first:")35
t1.join()36
io.print(t1.joinable())38
# the `with` guard joins on scope exit39
with thread.spawn(double_and_report, 100, "second:") {40
}42
# per-worker retry: a raise re-surfaces at join(); the supervisor retries (collector shape)43
let attempt = 144
let up = false45
while not up {46
let w = thread.spawn(flaky, attempt)47
try {48
w.join()49
up = true50
} except e {51
io.print("retrying after:", e)52
attempt = attempt + 153
}54
}55
io.print("collector done")56
}58
main()59
)PURR",60
"first: 42\nFalse\nsecond: 200\n"61
"retrying after: feed dropped\nfeed up on attempt 2\ncollector done\n");62
}64
TEST(StdlibE2E, ThreadOwnerParamLowering) {65
const std::string gen = e2e::expect_e2e_source("thread_owner_param", R"PURR(import io66
import memory67
import thread69
fn worker(o : memory.Owner<int>, n : int) {70
io.print("worker sees", n)71
}73
fn main() {74
let o = memory.own(0)75
let t = thread.spawn(worker, o, 7)76
t.join()77
}79
main()80
)PURR",81
"worker sees 7\n");82
// The typed Owner parameter lowers to the CONCRETE reference memory.own(0) deduces —83
// `int` inside the template argument maps like every other cheatah int.84
EXPECT_NE(gen.find("memory::Owner<long long>& o"), std::string::npos)85
<< "expected the Owner<int> parameter to lower to memory::Owner<long long>&:\n"86
<< gen;87
EXPECT_EQ(gen.find("Owner<int>"), std::string::npos)88
<< "raw C++ `Owner<int>` must not appear (cheatah int is long long):\n"89
<< gen;90
}92
TEST(StdlibE2E, ThreadSharedOwnerSum) {93
// The two-module design, end to end: N spawned workers share ONE pinned Owner (by reference)94
// and increment through exclusive write leases — the drain-before-write engine makes the total95
// exact regardless of interleaving. This is the .purr the thread module exists for.96
e2e::expect_e2e("thread_owner_sum", R"PURR(import io97
import memory98
import thread100
fn worker(o : memory.Owner<int>, quota : int) {101
let done = 0102
while done < quota {103
with o.rwrite().acquire() as w {104
w.write(w.read() + 1)105
}106
done = done + 1107
}108
}110
fn main() {111
let o = memory.own(0)112
with thread.spawn(worker, o, 2000) {113
with thread.spawn(worker, o, 2000) {114
with thread.spawn(worker, o, 2000) {115
}116
}117
}118
io.print(o.rread().acquire().read())119
}121
main()122
)PURR",123
"6000\n");124
}