|
| 1 | +# 3tk — staging plan 022 |
| 2 | + |
| 3 | +Written 2026-09-03. |
| 4 | + |
| 5 | +**Provenance.** Follows [3tk-staging-plan-021.md](backup/3tk-staging-plan-021.md). |
| 6 | +021 declared one stage, **3TK-57**, and it has since run and closed |
| 7 | +(2026-08-31, GitHub Actions CI, built in `matryoshka-3tk`). **021 is fully |
| 8 | +spent.** This plan declares one stage: **3TK-58.** |
| 9 | + |
| 10 | +State is in [3tk-status.md](3tk-status.md). Narrative is in |
| 11 | +[3tk-log.md](3tk-log.md). Neither is duplicated here. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Why this plan exists |
| 16 | + |
| 17 | +**`Mailbox` and `Pool` are public structs today, with every field readable |
| 18 | +and writable from outside the module** — the leading underscore is |
| 19 | +convention only, not an enforced boundary; `ref/3tk-api-003.md` even states |
| 20 | +"C3 0.8.3 hides neither a field nor a method" as the rationale for the |
| 21 | +current shape. The owner wants that changed: `Mailbox` and `Pool` become |
| 22 | +**opaque handles**, following the idiom already used by C3's own stdlib |
| 23 | +(`std::thread::channel::UnboundedChannel`, in |
| 24 | +`/home/g41797/dev/langs/c3/lib/std/threads/unbounded_channel.c3`) — |
| 25 | +`typedef Mailbox = void;` as the public type, the real fields in an |
| 26 | +`@private` impl struct in the same module, every method casting the opaque |
| 27 | +pointer to the real type as its first line. Client code that only calls |
| 28 | +public methods (`mb.send(...)`, `pool.get(...)`, etc.) does not change; code |
| 29 | +that reached into `_active`/`_cv` directly no longer compiles. |
| 30 | + |
| 31 | +**Scope, ruled before this plan was written**: `Mailbox` and `Pool` only — |
| 32 | +not `PoolBucket` (also a public struct today), and not the core |
| 33 | +(`PolyNode`/`Inner`). This matches the boundary [3tk-status.md](3tk-status.md) |
| 34 | +already records from the earlier API 12 work: flatten `Mailbox`/`Pool` only, |
| 35 | +never the core. |
| 36 | + |
| 37 | +**The owner's tests are black-box by convention.** The exceptions to that — |
| 38 | +three test files reaching `Mailbox`/`Pool` fields directly today — are |
| 39 | +in scope for this stage, not a follow-up, because an opaque `Mailbox`/`Pool` |
| 40 | +makes those reads a compile error. One of the three cannot be replaced by any |
| 41 | +public call (reasoning below); it is dropped, not rewritten. |
| 42 | + |
| 43 | +## The stage |
| 44 | + |
| 45 | +``` |
| 46 | +3TK-58 Mailbox/Pool opaque handles 3tk/src/, 3tk/test/, matryoshka-3tk 3tk-reference-006.md |
| 47 | +``` |
| 48 | + |
| 49 | +## 3TK-58 — Mailbox/Pool opaque handles |
| 50 | + |
| 51 | +**1. `src/mailbox.c3`, `src/pool.c3`.** |
| 52 | + |
| 53 | +- `struct Mailbox { ... }` → `struct _Mbox @private { ... }` (same fields, same |
| 54 | + layout — `Inner node` stays the first field, since `to_handle`/`of` still |
| 55 | + compute a `Handle` from its address). |
| 56 | +- `struct Pool { ... }` → `struct _Pool @private { ... }`, same treatment. |
| 57 | +- `typedef Mailbox = void;` / `typedef Pool = void;` become the public types. |
| 58 | +- Every method signature changes from `fn T Mailbox.method(&self, ...)` to |
| 59 | + `fn T Mailbox.method(&mbox, ...)` (parameter renamed since `self` no longer |
| 60 | + names the real type), with `_Mbox* self = (_Mbox*)mbox;` as the first line |
| 61 | + of the body — body otherwise unchanged. Same for `Pool`. |
| 62 | +- `to_handle`, `of`, and the `@closed_fast` macro take the opaque pointer and |
| 63 | + cast internally, the same way. |
| 64 | +- The descriptive `<* *>` doc comment currently above `struct Mailbox` / |
| 65 | + `struct Pool` moves to above the new `typedef` line. Mechanically safe: |
| 66 | + `check-doc-loop.sh` matches every `<* *>` block's sentences against the |
| 67 | + reference text as a flat pool, regardless of what declaration follows it — |
| 68 | + confirmed by reading the script before this plan was written. c3c itself |
| 69 | + has no working docgen to have a placement rule at all (`MANUAL.md` ~line |
| 70 | + 10427: "`c3c docs` ... Not added yet!"). |
| 71 | + |
| 72 | +**2. New public API**: `Mailbox.is_quiet()` / `Pool.is_quiet()` → `bool`, |
| 73 | +returning `self._closed && self._active == 0` under the mutex, same |
| 74 | +predicate `release()` already asserts. Deliberately narrower than a raw |
| 75 | +`active_count()` accessor — the owner's call, to avoid exposing the count as |
| 76 | +a number tests could otherwise be tempted to inspect more closely than the |
| 77 | +contract promises. |
| 78 | + |
| 79 | +**3. Test fixes, same stage:** |
| 80 | + |
| 81 | +| file | today | after | |
| 82 | +|---|---|---| |
| 83 | +| `test/t_mailbox.c3:443,471,510` | `always_assert(mb._active == 0, ...)` | `always_assert(mb.is_quiet(), ...)` | |
| 84 | +| `test/t_pool.c3:672,700,733,736` | `always_assert(p._active == 0, ...)` | `always_assert(p.is_quiet(), ...)` | |
| 85 | +| `test/t_concurrency.c3` | `the_deadline_is_anchored_once` calls `mb._cv.broadcast()` directly to provoke a spurious wakeup | **dropped** — no replacement | |
| 86 | +| `test/t_identity.c3:43-44,70-71,98-99` | `.node` access, target type not yet confirmed | read first; fix only if it touches `Mailbox`/`Pool` | |
| 87 | + |
| 88 | +**Why `the_deadline_is_anchored_once` has no replacement.** The defect it |
| 89 | +guards against (Part 2.5, D7 — `wait_until`'s deadline must be anchored once, |
| 90 | +not recomputed on each spurious wakeup) only shows up when the mailbox's |
| 91 | +internal wait loop re-enters `cv.wait_until(deadline)` with *nothing* |
| 92 | +changed: no new item, `_wake_gen` unmoved, not closed. Every public way to |
| 93 | +signal the condition variable changes one of exactly those three things: |
| 94 | +`send`/`send_oob` add an item (the loop's `dequeue()` then succeeds and it |
| 95 | +returns), `wake_all()` bumps `_wake_gen` (the loop sees the generation change |
| 96 | +and returns `WOKEN`), `close()` sets `_closed` (same). There is no sequence |
| 97 | +of public calls that re-enters the wait loop while all three stay unchanged |
| 98 | +— the "spurious, keep waiting" case is structurally unreachable from outside |
| 99 | +the module by construction, which is exactly what the existing doc comment |
| 100 | +on this test already said before this stage: *"there is no public way to |
| 101 | +produce a spurious wakeup, because a spurious wakeup is not a feature."* |
| 102 | +Once `_cv` itself is no longer reachable, the test's own mechanism for |
| 103 | +provoking the case it tests goes with it. Tracked as the "Tests improvements" |
| 104 | +TODO in [3tk-status.md](3tk-status.md); the guarantee stays documented, not |
| 105 | +mechanically tested, going forward. |
| 106 | + |
| 107 | +**4. Reference doc**, written directly in the separate `matryoshka-3tk` repo |
| 108 | +(per the owner's ruling this session, [[design-docs-editable-in-3tk-repo]] in |
| 109 | +Claude's own memory — design docs there are editable directly, not deferred): |
| 110 | +a new `3tk-reference-006.md`, rewriting the `Mailbox`/`Pool` sections to |
| 111 | +describe the opaque shape and retracting "C3 0.8.3 hides neither a field nor |
| 112 | +a method," which described the shape this stage removes. |
| 113 | +`3tk-reference-005.md` moves to that repo's `backup/` with a plain `mv` |
| 114 | +(never `git mv` — git stays untouched there too). |
| 115 | + |
| 116 | +**5. Verification.** |
| 117 | + |
| 118 | +- `./3tk/run-builds.sh` after the `src/` and test changes. Compilation is the |
| 119 | + check: any leftover direct field access (anything this plan's search |
| 120 | + missed) fails to build. Fix iteratively until green, same precedent as |
| 121 | + 3TK-50's steps. |
| 122 | +- `./3tk/check-doc-loop.sh` against the new `3tk-reference-006.md` once it |
| 123 | + exists (the in-repo default `REF` path is already known stale — point it |
| 124 | + at the new file explicitly). |
| 125 | +- Confirmed before writing this plan: no `Mailbox{...}` / `Pool{...}` struct |
| 126 | + literal exists anywhere under `3tk/`, so no construction site needs |
| 127 | + updating for that reason. |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +## Rules that hold |
| 132 | + |
| 133 | +- **No stage runs `git`.** Moves in this repo are plain `mv`; the owner saves |
| 134 | + and pushes. The same now applies inside `matryoshka-3tk` for design docs, |
| 135 | + per this session's ruling. |
| 136 | +- **A change to `3tk/src` revises `ref/` in the same stage** — this stage's |
| 137 | + `ref/` update is the reference book in `matryoshka-3tk`, since that is |
| 138 | + where the live reference now lives; this repo's `ref/3tk-api-003.md` is |
| 139 | + already known stale and is not the target. |
| 140 | +- **Tests stay black-box after this stage.** The three files fixed here are |
| 141 | + brought back into that convention, not given a new sanctioned way to reach |
| 142 | + internals. |
| 143 | + |
| 144 | +## Versioning |
| 145 | + |
| 146 | +**`3tk-staging-plan-021.md` is superseded by this file** and moves to |
| 147 | +`backup/`. `3tk-reference-005.md`, in `matryoshka-3tk/design/`, is superseded |
| 148 | +by `3tk-reference-006.md` as part of this stage's own work, not before it. |
| 149 | + |
| 150 | +## What this plan leaves to the owner |
| 151 | + |
| 152 | +- **Whether the dropped `the_deadline_is_anchored_once` coverage gap is |
| 153 | + acceptable long-term**, or whether some other verification (a stress test, |
| 154 | + a manual sanitizer run, something else) should stand in for it. Tracked as |
| 155 | + the "Tests improvements" TODO, not resolved here. |
| 156 | +- **"Managed Outers — re-thinking"** — raised this session, not elaborated, |
| 157 | + entirely separate from 3TK-58 and not started by it. |
| 158 | +- **The seven questions plan 018 left and 019 carried**, and everything in |
| 159 | + `3tk-status.md`'s *Open questions* — untouched by this stage, and not |
| 160 | + reopened by it. |
0 commit comments