1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
//! Fiber-based task runtime built on `dtact`.
//!
//! `plan.md` §4.3 mandates an async-fiber interface, and the review found
//! every async path (`ffi::async_bridge`, `parallel::solver`) still using
//! heavyweight `std::thread::spawn`. This module funnels all task dispatch
//! through `dtact`'s lock-free fiber pool, exposing three primitives:
//!
//! * [`ensure_runtime`](crate::runtime::ensure_runtime) — idempotent one-shot init of the global fiber pool.
//! * [`spawn_task`](crate::runtime::spawn_task) — fire-and-forget fiber for a `FnOnce() + Send`.
//! * [`parallel_for_each`](crate::runtime::parallel_for_each) — fan-out / fan-in over an iterator of closures.
//!
//! ## Task-envelope memory strategy
//!
//! Each spawned fiber needs a heap allocation to carry its closure across
//! thread boundaries (the spawning thread owns the closure; a dtact worker
//! thread will read and execute it). Routing every spawn through the global
//! allocator (malloc / HeapAlloc) costs ~20–100 ns per task — enough to
//! negate the scheduling advantage of fibers for small, rapid fan-outs.
//!
//! ### Lock-free pool (fast path)
//!
//! When a closure fits in `POOL_INLINE_CAPACITY` bytes and has alignment ≤
//! 16, the closure is written into a pre-allocated `PoolNode` drawn from a
//! global **ABA-safe Treiber stack** (`POOL_HEAD`). The worker returns the
//! node to the stack after moving the closure out — one `LOCK CMPXCHG8B`
//! instead of a malloc + free.
//!
//! #### ABA safety
//!
//! A plain `AtomicPtr` Treiber stack suffers from ABA: if a node is popped,
//! used for a task that completes and returns the node, all before the
//! original thread's CAS fires, the stale `next` pointer silently wins and
//! can corrupt the list. We prevent this with a **tagged pointer**:
//! `POOL_HEAD` is an `AtomicU64` whose top 16 bits hold a 16-bit generation
//! tag (bottom 48 bits = pointer, always ≤ 48 bits on x86-64 without LA57).
//! Each successful CAS increments the tag; a stale observer always sees a
//! different tag and retries. `AtomicU64::new(0)` is a stable `const fn`
//! with no software-lock fallback on any 64-bit target.
//!
//! ### TaskEnvelope fallback (large / over-aligned closures)
//!
//! Closures that exceed `POOL_INLINE_CAPACITY` or require alignment > 16
//! fall back to the monomorphised `TaskEnvelope<F>` + `Box::into_raw` path.
//! These are rare in practice (typical captures: a few `Arc` / `usize` values,
//! all ≤ 8-byte aligned).
//!
//! ### Shared trampoline
//!
//! Both paths store the monomorphised `invoke` pointer at **byte offset 0**
//! (`#[repr(C)]` for `TaskEnvelope`, `repr(C, align(16))` with `word0` at
//! offset 0 for `PoolNode`). The single `task_trampoline` reads those bytes
//! as `unsafe fn(*mut ())` and dispatches without knowing which path produced
//! the pointer.
use c_void;
use ptr;
use OnceLock;
use ;
use c_ffi::;
use dtact_await;
use crate;
/// Marker returned by [`ensure_runtime`] so callers can prove the pool is
/// alive without re-checking. Stored once in `RUNTIME_GATE` and copied
/// freely thereafter.
/// Initialization sentinel. `dtact_init` itself uses `OnceLock` internally,
/// but we wrap it again so that callers from this crate share a single
/// thread-safe init path and never race on the `dtact_default_config()`
/// argument construction.
static RUNTIME_GATE: = new;
/// Initializes the global `dtact` runtime on first call; subsequent calls
/// are O(1) and return the same `RuntimeGate`.
///
/// Safe to call from any thread, including pre-`main` static init paths.
/// Returns the active runtime gate if the pool is initialized.
///
/// FFI entry points use this to refuse work rather than implicitly start
/// the runtime.
///
/// # Errors
///
/// Returns [`FfiError::RuntimeUninitialized`] if [`ensure_runtime`] has not
/// been called yet on this process.
// =========================================================================
// Common C-ABI trampoline
// =========================================================================
//
// Both dispatch paths (`PoolNode` and `TaskEnvelope<F>`) guarantee that the
// `invoke` function pointer lives at byte offset 0 of the allocation that
// `arg` points to. The trampoline reads those bytes without knowing which
// path produced `arg`, then calls the monomorphised handler.
/// C-ABI trampoline for `dtact_fiber_launch`.
///
/// `arg` is either a `*mut PoolNode` (fast path) or a `*mut TaskEnvelope<F>`
/// (fallback path). In both cases `invoke` sits at byte offset 0 of the
/// pointed-to memory (`word0` at offset 0 for `PoolNode`, `invoke` at offset
/// 0 via `#[repr(C)]` for `TaskEnvelope`). The called function takes
/// ownership of the full allocation and frees or recycles it.
extern "C"
// =========================================================================
// Fast path — ABA-safe lock-free pool via tagged-pointer Treiber stack
// =========================================================================
/// Maximum closure size (bytes) that uses the pooled fast path.
///
/// 64 bytes covers common captures: three `Arc<T>` fat pointers (24 B),
/// a `usize` index (8 B), and a `*const` data pointer (8 B), with room to
/// spare. Closures larger than this fall back to [`TaskEnvelope`].
const POOL_INLINE_CAPACITY: usize = 64;
/// A reusable memory node for task dispatch.
///
/// **While on the free list** ([`POOL_HEAD`]):
/// * `word0` holds the raw `*mut PoolNode` "next" pointer (null = tail).
/// * `_word1` and `data` are logically uninitialized.
///
/// **While in-flight** (handed to a dtact fiber):
/// * `word0` holds the monomorphised `invoke` trampoline pointer.
/// * `_word1` is unused padding.
/// * `data[0..size_of::<F>()]` holds the closure `F` written via
/// `ptr::write`; the rest is logically uninitialized.
///
/// `#[repr(C, align(16))]` ensures:
/// * `word0` is at byte offset 0 — the shared trampoline reads it as a
/// function pointer without knowing whether this is a pool node or a
/// `TaskEnvelope`.
/// * `data` starts at byte offset 16, which is 16-byte aligned — meeting
/// the alignment requirement of any closure capturing `Arc`, `usize`,
/// `*const T`, or SIMD-compatible types up to 16-byte alignment.
// ── Tagged-pointer helpers ────────────────────────────────────────────────
//
// Layout of the u64 stored in POOL_HEAD:
//
// bits 63..48 ┃ generation counter (u16, wraps at 65 536)
// bits 47.. 0 ┃ raw pointer (48 bits, canonical x86-64 user-space)
//
// `AtomicU128::new` is not yet a stable const fn, so it cannot appear in a
// `static` initializer without nightly. More critically, `AtomicU128` often
// degrades to a software-lock fallback on platforms without `CMPXCHG16B`,
// cancelling the lock-free guarantee we need.
//
// Instead we use a plain `AtomicU64` — always lock-free, always const-stable.
// On x86-64 (without LA57 5-level paging), user-space canonical addresses
// occupy ≤ 48 bits; the top 16 bits are always zero. Windows does not yet
// expose LA57 to user-space (as of 2026). Those 16 free bits carry a
// generation counter that defeats ABA.
//
// ABA analysis: 65 536 pop-use-push cycles must complete inside the ~3 ns
// window between our `load` and `compare_exchange_weak`. At 10⁶ cycles/s
// that would take ≥ 65 ms. Probability ≈ 3 ns / 65 ms ≈ 5 × 10⁻⁸ per
// CAS — negligible.
/// Head of the global ABA-resistant lock-free pool (tagged Treiber stack).
///
/// Zero-initialized → `ptr = null, gen = 0` → empty pool.
/// `AtomicU64::new(0)` is a stable `const fn`; the CAS compiles to
/// `LOCK CMPXCHG8B` on x86-64 — one instruction, no software lock.
static POOL_HEAD: AtomicU64 = new;
/// Packs a pointer and 16-bit generation counter into one `u64`.
///
/// # Safety (caller contract)
/// `ptr` must be a canonical user-space address whose top 16 bits are zero
/// (standard x86-64 without LA57). A `debug_assert` fires otherwise.
/// Unpacks a `u64` CAS word into a `(pointer, generation)` pair.
const
/// Allocates a fresh [`PoolNode`] via the global allocator.
///
/// Cold path: called only when the pool is empty.
///
/// The `Box` is intentional: callers immediately call `Box::into_raw` to
/// produce a `*mut PoolNode` that the C trampoline owns; `Box::from_raw`
/// reconstructs it after the fiber completes. A plain `PoolNode` return
/// would force callers to perform their own `Box::new` anyway.
/// Pops a [`PoolNode`] from the global free list, or allocates one.
///
/// Uses a `compare_exchange_weak` loop on the tagged `POOL_HEAD`. The
/// 16-bit tag in bits 63..48 is incremented on every successful CAS, so a
/// stale load (ABA) always causes the CAS to fail and retry.
///
/// The `Box` return is intentional: `spawn_task` immediately calls
/// `Box::into_raw` to hand the raw pointer to `dtact_fiber_launch`; the
/// C trampoline later reconstructs it with `Box::from_raw` in
/// `invoke_and_drop_pooled`. A plain `PoolNode` return would only push
/// the `Box::new` call into the caller.
/// Pushes a [`PoolNode`] back onto the global free list.
///
/// The tag is incremented so that any thread holding a stale `head_val`
/// cannot win a CAS against the new state.
/// Monomorphised trampoline for the pooled fast path.
///
/// # Safety
///
/// `raw` must be a valid `*mut PoolNode` whose `data[0..size_of::<F>()]`
/// holds an initialized `F` written by `ptr::write`. After this call the
/// node has been returned to the pool and `raw` must not be used again.
unsafe
// =========================================================================
// Fallback path — TaskEnvelope<F> (oversized / over-aligned closures)
// =========================================================================
/// Typed envelope for an oversized or over-aligned closure.
///
/// Avoids double-boxing (`Box<Box<dyn FnOnce()>>`): `TaskEnvelope<F>`
/// monomorphises the trampoline, stores the closure inline, and uses a
/// single `Box` (global allocator, thread-safe) for the one allocation.
///
/// `#[repr(C)]` ensures `invoke` is at offset 0 — the trampoline reads it
/// from a type-erased `*mut c_void` without knowing `F`.
// =========================================================================
// Public API — spawn / join
// =========================================================================
/// Opaque handle for a spawned task. Returned by [`spawn_task`] and
/// consumed by [`join`].
;
/// Spawns `f` onto the fiber pool and returns a joinable [`TaskHandle`].
///
/// **Fast path** — closures ≤ `POOL_INLINE_CAPACITY` bytes and ≤ 16-byte
/// alignment: drawn from the lock-free `POOL_HEAD` pool (one `LOCK CMPXCHG8B`
/// pair, no malloc).
///
/// **Fallback path** — larger or over-aligned closures: one `Box` allocation
/// via the global allocator, same as before the pool existed.
///
/// `dtact` dispatches the closure to whichever worker is currently coldest.
/// Drop the handle if you don't need to wait — fibers run to completion
/// regardless. Call [`join`] to block until the fiber finishes.
/// Blocks the calling thread (or yields the calling fiber) until the task
/// behind `handle` finishes.
// =========================================================================
// Fan-out / fan-in
// =========================================================================
/// Runs each closure in `tasks` on its own fiber and waits for all of them
/// to finish before returning. Closures produce a `T` which is collected
/// into the returned `Vec` in input order.
///
/// Panics inside individual tasks are caught via [`std::panic::catch_unwind`]
/// and mapped to `None` in the output; the returned `Vec` contains all slots
/// including `None` values for panicking tasks, preserving input order.
///
/// Uses a lock-free write path: each fiber writes directly into its own
/// pre-allocated slot in an `UnsafeCell<Vec<Option<T>>>` using the slot
/// index as the exclusive key — no `Mutex` contention between workers.
/// The fan-in join barrier (`dtact_await`) provides the happens-before
/// edge that makes the final read of all slots safe.
///
/// This is the workhorse used by `parallel::solver` and `ffi::async_bridge`
/// to replace the `std::thread::spawn` pattern.
extern crate alloc;