@@ -331,7 +331,7 @@ var registry_mutex: std.Io.Mutex = .init;
331331
332332/// Live registered-worker count, mirroring the non-null slots in
333333/// `thread_registry`. Maintained under `registry_mutex` (register/unregister)
334- /// but exposed lock-free via `registeredCountRelaxed ` so `safepoint.stopWorld`
334+ /// but exposed lock-free via `registeredCountLockFree ` so `safepoint.stopWorld`
335335/// can re-read the rendezvous target while holding `sp_mutex` — taking
336336/// `registry_mutex` under `sp_mutex` would invert the lock order against the
337337/// unregister path (registry_mutex → sp_mutex wake). The array scan in
@@ -351,20 +351,69 @@ var registered_count: std.atomic.Value(u32) = .init(0);
351351/// dormant D-244 #4 path, validated separately under user awareness.
352352pub threadlocal var is_registered_worker : bool = false ;
353353
354+ /// Build the CALLING thread's standard worker context: all four TLS root
355+ /// slots plus the caller-supplied opaque `tx_slot` (opaque because root_set
356+ /// must not import lock_tx — see `ThreadGcContext.tx_slot`). Must run ON the
357+ /// worker thread (the addresses taken are its threadlocals). One shared
358+ /// builder so a future worker family cannot silently drop a slot (the
359+ /// slot-drift hazard the ADR-0175 review named).
360+ pub fn workerContext (tx_slot : * const ? * anyopaque ) ThreadGcContext {
361+ return .{
362+ .frame_slot = & env_mod .current_frame ,
363+ .analysis_frame_slot = & analysis_frame_head ,
364+ .eval_frame_slot = & eval_frame_head ,
365+ .self_guard_slot = & gc_self_guard ,
366+ .tx_slot = tx_slot ,
367+ };
368+ }
369+
354370/// Register a worker thread's published roots. Locked (workers register
355371/// at `Thread.spawn`). Returns `error.TooManyThreads` past the cap.
372+ ///
373+ /// Registration is itself a GC SAFEPOINT (ADR-0175). A worker spawned while a
374+ /// stop-the-world collect was arming is invisible to that collect's rendezvous
375+ /// target (it was not yet in the registry), so `stopWorld` can return with
376+ /// target 0 and start walking roots while this thread comes to life. Without
377+ /// the post-publish park below, the newborn would run mutator code (push
378+ /// binding/eval frames, pop an agent action) CONCURRENTLY with the collector's
379+ /// `nextThreadRoots` walk — the spawn-to-register window (the
380+ /// phase16_gc_torture agent-drain GP fault on the 4-vCPU Linux runner).
381+ ///
382+ /// The seq_cst pairing makes the handshake airtight (Dekker shape): the
383+ /// collector stores `gc_requested` then reads `registered_count`; this thread
384+ /// adds `registered_count` then loads `gc_requested`. Under seq_cst it is
385+ /// impossible that BOTH the collector misses this thread's count AND this
386+ /// thread misses the collector's flag — so either the collector waits for our
387+ /// park, or we park here before touching the heap. The park runs AFTER
388+ /// `registry_mutex` is released (a parked thread must never hold it — peers
389+ /// registering and the next walk would block behind a thread the collector
390+ /// will not wake). A freshly registered ctx publishes only empty TLS roots, so
391+ /// a collector already mid-walk reading our slot sees an empty contribution.
392+ ///
393+ /// CONTRACT for callers: (a) no lock may be held at the call site (the park
394+ /// must not strand a lock the collector's world could need); (b) code run
395+ /// BEFORE this call must touch only spawner-pinned objects and pure bit-ops
396+ /// (an unregistered thread's roots are invisible to the collector); (c) on
397+ /// `TooManyThreads` the worker must NOT run its mutator body — it reports its
398+ /// domain failure without GC allocation and exits (an unregistered mutator is
399+ /// exactly the corruption this safepoint closes).
356400pub fn registerThread (ctx : * ThreadGcContext ) error {TooManyThreads }! void {
357- io_default .lockMutex (& registry_mutex );
358- defer io_default .unlockMutex (& registry_mutex );
359- for (& thread_registry ) | * slot | {
360- if (slot .* == null ) {
361- slot .* = ctx ;
362- is_registered_worker = true ;
363- _ = registered_count .fetchAdd (1 , .release );
364- return ;
365- }
401+ {
402+ io_default .lockMutex (& registry_mutex );
403+ defer io_default .unlockMutex (& registry_mutex );
404+ const idx : usize = blk : {
405+ for (& thread_registry , 0.. ) | * slot , i | {
406+ if (slot .* == null ) break :blk i ;
407+ }
408+ return error .TooManyThreads ;
409+ };
410+ // Release-store so a lock-free walker (`threadContextAt`) that sees the
411+ // slot also sees the fully-initialised ctx it points at.
412+ @atomicStore (? * ThreadGcContext , & thread_registry [idx ], ctx , .release );
413+ is_registered_worker = true ;
414+ _ = registered_count .fetchAdd (1 , .seq_cst );
366415 }
367- return error . TooManyThreads ;
416+ if ( safepoint . gc_requested . load ( .seq_cst )) safepoint . park () ;
368417}
369418
370419/// Deregister a worker thread's context (at `Thread.join`). No-op if absent.
@@ -376,8 +425,8 @@ pub fn unregisterThread(ctx: *ThreadGcContext) void {
376425 defer io_default .unlockMutex (& registry_mutex );
377426 for (& thread_registry ) | * slot | {
378427 if (slot .* == ctx ) {
379- slot .* = null ;
380- _ = registered_count .fetchSub (1 , .release );
428+ @atomicStore ( ? * ThreadGcContext , slot , null , .release ) ;
429+ _ = registered_count .fetchSub (1 , .seq_cst );
381430 removed = true ;
382431 break ;
383432 }
@@ -394,9 +443,13 @@ pub fn unregisterThread(ctx: *ThreadGcContext) void {
394443}
395444
396445/// Lock-free snapshot of the registered-worker count for `safepoint.stopWorld`'s
397- /// rendezvous-target recompute (see `registered_count`).
398- pub fn registeredCountRelaxed () u32 {
399- return registered_count .load (.acquire );
446+ /// rendezvous-target recompute (see `registered_count` — taking `registry_mutex`
447+ /// under `sp_mutex` would invert the unregister path's lock order). Seq_cst: the
448+ /// collector reads this AFTER its seq_cst `gc_requested` store, pairing with
449+ /// `registerThread`'s seq_cst count-add + flag-load so a registering newborn is
450+ /// either counted here or parks there (ADR-0175 spawn-window handshake).
451+ pub fn registeredCountLockFree () u32 {
452+ return registered_count .load (.seq_cst );
400453}
401454
402455/// Count of currently-registered worker contexts (test/introspection).
@@ -417,8 +470,11 @@ pub fn registeredThreadCount() usize {
417470/// single-thread collect sees an empty registry → no-op. `markTxFn` receives the
418471/// opaque `current_tx` value (the worker's `?*LockingTransaction`, reinterpreted).
419472pub fn markRegisteredTxs (context : * anyopaque , markTxFn : * const fn (* anyopaque , ? * anyopaque ) void ) void {
420- for (thread_registry ) | slot | {
421- if (slot ) | ctx | markTxFn (context , ctx .tx_slot .* );
473+ for (& thread_registry ) | * slot | {
474+ // Acquire-load: a newborn may register mid-collect (it then parks at
475+ // its registration safepoint with all-empty roots — ADR-0175), so the
476+ // slot read must be atomic against that concurrent release-store.
477+ if (@atomicLoad (? * ThreadGcContext , slot , .acquire )) | ctx | markTxFn (context , ctx .tx_slot .* );
422478 }
423479}
424480
@@ -428,9 +484,12 @@ pub fn markRegisteredTxs(context: *anyopaque, markTxFn: *const fn (*anyopaque, ?
428484// worker), read through its published TLS pointers. `.end` terminates the walk
429485// past the registry cap. A sparse (null) registry slot yields an all-empty
430486// contribution and the cursor advances. Reading the registry array during the
431- // walk is safe because (a) it is empty until Phase-B real threads (#3a/#3b are
432- // runtime-inert), and (b) the #3b-step2 safepoint guarantees no concurrent
433- // register/unregister during collect.
487+ // walk is safe because (a) slot reads are atomic acquire-loads paired with the
488+ // register/unregister release-stores, (b) a newborn registering mid-collect
489+ // parks at its registration safepoint BEFORE running any mutator code, so a
490+ // mid-walk registration contributes only its empty TLS roots (ADR-0175), and
491+ // (c) unregistration cannot happen mid-collect — the rendezvous completed, so
492+ // every already-registered worker is parked (or blocked-counted).
434493const ThreadRoots = struct {
435494 frame_head : ? * env_mod.BindingFrame ,
436495 eval_head : ? * EvalFrame ,
@@ -447,7 +506,7 @@ fn threadContextAt(idx: usize) ThreadSource {
447506 } };
448507 const ri = idx - 1 ;
449508 if (ri >= MAX_GC_THREADS ) return .end ;
450- if (thread_registry [ri ]) | ctx | return .{ .roots = .{
509+ if (@atomicLoad ( ? * ThreadGcContext , & thread_registry [ri ], .acquire ) ) | ctx | return .{ .roots = .{
451510 .frame_head = ctx .frame_slot .* ,
452511 .eval_head = ctx .eval_frame_slot .* ,
453512 .self_guard = ctx .self_guard_slot .* ,
0 commit comments