cheatah
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): one
4// 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 (the
6// per-venue retry shape a real multi-feed collector uses). Deterministic by construction: a
7// worker's output is only observed behind its join.
8//
9// ThreadOwnerParamLowering asserts the GENERATED C++ (not just behavior): a worker parameter
10// typed `memory.Owner<int>` must lower to a concrete `Owner<long long>&` — the same type
11// `memory.own(0)` deduces — so a pinned Owner travels into a thread by reference. It only
12// asserts the signature (the program never exercises the Owner engine, which is TDD elsewhere).
13#include <string>
15#include "e2e_harness.hpp"
17TEST(StdlibE2E, Thread) {
18 e2e::expect_e2e("thread_sys", R"PURR(import io
19import thread
21fn double_and_report(n : int, tag : str) {
22 io.print(tag, n * 2)
25fn flaky(attempt : int) {
26 if attempt < 2 {
27 raise "feed dropped"
28 }
29 io.print("feed up on attempt", attempt)
32fn 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 exit
39 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 = 1
44 let up = false
45 while not up {
46 let w = thread.spawn(flaky, attempt)
47 try {
48 w.join()
49 up = true
50 } except e {
51 io.print("retrying after:", e)
52 attempt = attempt + 1
53 }
54 }
55 io.print("collector done")
58main()
59)PURR",
60 "first: 42\nFalse\nsecond: 200\n"
61 "retrying after: feed dropped\nfeed up on attempt 2\ncollector done\n");
64TEST(StdlibE2E, ThreadOwnerParamLowering) {
65 const std::string gen = e2e::expect_e2e_source("thread_owner_param", R"PURR(import io
66import memory
67import thread
69fn worker(o : memory.Owner<int>, n : int) {
70 io.print("worker sees", n)
73fn main() {
74 let o = memory.own(0)
75 let t = thread.spawn(worker, o, 7)
76 t.join()
79main()
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;
92TEST(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 total
95 // exact regardless of interleaving. This is the .purr the thread module exists for.
96 e2e::expect_e2e("thread_owner_sum", R"PURR(import io
97import memory
98import thread
100fn worker(o : memory.Owner<int>, quota : int) {
101 let done = 0
102 while done < quota {
103 with o.rwrite().acquire() as w {
104 w.write(w.read() + 1)
105 }
106 done = done + 1
107 }
110fn 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())
121main()
122)PURR",
123 "6000\n");