fibers: Scheduler.deinit + struct-literal init cleanup

Scheduler.deinit closes the bounded leaks B1 documented: it reaps any leftover
ready fibers, frees every heap Task from go (now tracked via a task_allocs
field), frees the timers/io_waiters/task_allocs List backings, and closes the
lazily-opened kqueue fd. Terminal + idempotent; the per-spawn/go closure env
remains unfreeable (language limitation). Locked by
examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx, which exercises
every freed resource under a tracking GPA (freed by deinit: 5, kq reset to -1).

Also converts plain-struct '= ---'+field-assign init to '.{ ... }' literal init
where '---' carries no meaning: Scheduler.init, Dock.make, and the fiber
examples 1811/1813/1814/1816 (partial literals zero-fill the index-filled array
fields). Unions, '---'-feature tests, the 0154 regression, documented
generic-pack gaps, and loop/conditional inits are intentionally left on '---'.
This commit is contained in:
agra
2026-06-22 09:45:33 +03:00
parent 1e0015d6b4
commit 55ed9a248e
12 changed files with 299 additions and 53 deletions

View File

@@ -4,7 +4,35 @@ Companion to [PLAN-FIBERS.md](PLAN-FIBERS.md). Update after every step (one step
per the cadence rule). New corpus category: `18xx` concurrency.
## Last completed step
**B1.5 — END-TO-END M:1 validation — STREAM B1 COMPLETE.** A single capstone exercises the whole
**B1 follow-up — `Scheduler.deinit` (close the bounded leaks).** Post-B1 non-blocking cleanup: a
terminal `deinit` on `library/modules/std/sched.sx`'s `Scheduler` releases the resources B1 documented
as leaked. Frees, in order: (1) any fibers still enqueued ready (leak-safety net for `spawn`/`go`
without `run()``munmap` stack + free struct; a suspended off-queue fiber is unreachable, but a clean
`run()` aborts on orphans so none survive it); (2) every heap `*Task` from `go` — newly tracked via a
`task_allocs: List(*void)` field appended in `go` (the scheduler otherwise has no handle on its generic
`Task($R)`s); (3) the three `List` backings (`task_allocs`/`timers`/`io_waiters`, all grown through
`own_allocator`); (4) the lazily-opened kqueue fd (`close`, reset to `-1`). NOT freed (unchanged
language limitation): the per-`spawn`/`go` closure env (sx exposes no env-free). Idempotent (rests on
`List.deinit` nulling `items` + the `kq`/`ready_head` resets); TERMINAL contract — no scheduler-owned
handle (`*Task`, `*Fiber`, the scheduler) is usable after `deinit`.
- Added a canonical `close :: (i32) -> i32 extern libc` (matches the dedupe-canonical signature 1816
already uses) + the `task_allocs` field.
- Locked by `examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx` (aarch64-macOS `.build
{"target":"macos"}`, runs end-to-end): one run touches every freed resource — a SLEEPER (`timers`), a
pipe READER `block_on_fd` + WRITER (kqueue fd + `io_waiters`), two `go` tasks (`Task`s + `task_allocs`)
— then `deinit`. Verified by a tracking `GPA`: `freed by deinit: 5`, `live after deinit: 5` (the
RESIDUAL = the 5 documented closure envs, not a bug), `kq open after run: true` → `kq after deinit:
-1` (the genuinely-open kqueue fd is closed), `read: 3 [97 98 99]` (the fd path actually ran). Counts
captured into locals BEFORE printing (`print` allocates format temporaries through the same GPA).
- **Adversarially reviewed (worker):** no real memory-safety bug in the supported (deinit-after-`run`)
path — reap-loop reads `f.next` before freeing `f`, the three freed List backings + Tasks + kq are all
disjoint + scheduler-owned, no over-free, idempotent. The one CRITICAL it raised was a DOC contradiction
(step-(1) defensive reap vs step-(2) "post-run only"), reconciled by spelling out the terminal contract.
Its 0154-over-store concern (`.{}`→`List` writes in `init` could clobber `kq`) was PROBED and cleared:
`kq == -1` immediately after `init`, all fields clean. Suite GREEN **759/0**.
### Earlier — B1.5 — END-TO-END M:1 validation — STREAM B1 COMPLETE
A single capstone exercises the whole
colorblind pure-sx async runtime together: the M:1 scheduler (B1.5a) + suspending fiber-task async
`go`/`wait` (B1.4a) + deterministic virtual-time `sleep`/`now_ms` (B1.4b), over the `abi(.naked)`
`swap_context` on guarded `mmap` stacks (B1.0B1.3). `examples/concurrency/1817-concurrency-fiber-m1-end-to-end.sx`:
@@ -366,11 +394,14 @@ async (B1.4a: `Task($R)`/`go`/`wait`/`cancel`), deterministic virtual-time timer
`clock_ms`/`now_ms`/`sleep`, timer-driven `run`), AND real fd readiness via kqueue (B1.4c: lazy `kq`,
`io_waiters`, `block_on_fd`, run-loop Mode 2) — all over the `abi(.naked)` `swap_context` on guarded
`mmap` stacks (B1.0B1.3), reusing `std/net/kqueue.sx`. Every park path (timer sleep, fd block, raw
suspend) is balanced through `wake` (which evicts stale timer + fd waiters — the UAF guards). Locked
by `18xx` 18001817 (naked-asm, context-snapshot, blocking async, the switch + §10.7 stress gate +
guarded stacks + Win64 sibling, scheduler round-robin, suspend/wake, async go/wait/cancel, sim-timer
ordering, timer early-wake eviction, kqueue pipe I/O, and the **1817 end-to-end capstone**). Suite
GREEN **755/0**, master committed.
suspend) is balanced through `wake` (which evicts stale timer + fd waiters — the UAF guards). A terminal
`deinit` (B1 follow-up) closes the previously-documented leaks: heap `Task`s (tracked via `task_allocs`),
the `timers`/`io_waiters`/`task_allocs` List backings, and the kqueue fd; the per-`spawn`/`go` closure
env remains unfreeable (language limitation). Locked by `18xx` 18001820 (naked-asm, context-snapshot,
blocking async, the switch + §10.7 stress gate + guarded stacks + Win64 sibling, scheduler round-robin,
suspend/wake, async go/wait/cancel, sim-timer ordering, timer early-wake eviction, kqueue pipe I/O, the
**1817 end-to-end capstone**, sleep-negative/double-wait guards, and **1820 scheduler-deinit**). Suite
GREEN **759/0**, committed.
Future work (none blocking B1): a **linux epoll twin** of `block_on_fd` (mirror via `std/net/epoll`;
OS-neutral facade `std.event`) — B1.4c wired macOS kqueue only; routing the suspending async through
@@ -471,11 +502,13 @@ fibers/Io/scheduler code yet. Grounded floor facts:
## Next step
**Stream B1 is COMPLETE — no next step in this stream.** The pure-sx M:1 async runtime is feature-
complete and committed (18001817 green, 755/0). Pick up **Stream B2** (channels / structured cancel /
async stdlib) as a fresh carve (PLAN-CHANNELS.md), OR one of the documented non-blocking follow-ups:
the linux `epoll` twin of `block_on_fd`, a `Scheduler.deinit` (free the kq fd / heap Tasks / drain
leaks), `Future(void)`/`timeout` (needs issue 0150), or routing the suspending async through the
erased `context.io` for the M:N model. None of these block B1.
complete and committed (18001820 green, 759/0), now WITH a `Scheduler.deinit` closing the bounded
leaks. Pick up **Stream B2** (channels / structured cancel / async stdlib) as a fresh carve
(PLAN-CHANNELS.md), OR one of the remaining non-blocking follow-ups: the linux `epoll` twin of
`block_on_fd`, `Future(void)`/`timeout` (needs issue 0150), or routing the suspending async through the
erased `context.io` for the M:N model. (`Scheduler.deinit` — DONE, see Last completed step.) None of
these block B1. The closure-env leak survives `deinit` (no language affordance to free a closure env);
revisit if/when sx grows closure-env ownership.
**Deferred (future B1.4c sibling): the linux epoll twin of `block_on_fd`.** B1.4c wired the **macOS
kqueue** path only (the host is aarch64-macOS). The linux mirror would register interest via
@@ -521,6 +554,14 @@ incomplete); a dedicated effort; lambda workers are the idiom meanwhile.
codegen (`.unresolved` reaching LLVM emission, `src/backend/llvm/types.zig:196`). Found in the
B1.5a review; the scheduler doesn't use it (array-field index + `.*` only). Filed for its own
session: `issues/0155-scalar-pointer-index-llvm-panic.{md,sx}`.
- **✅ issue 0158 — FIXED** — a plain `union` struct-literal (`b : Overlay = .{ f = 3.14 }`) fell
through the generic struct-literal path (`getStructFields` empty for a union → malformed
`structInit`, overlapping zero-fill clobbered the member → silent `0.0`). Fix: `lowerStructLiteral`
detects a plain-union target → new `lowerUnionLiteral` (`src/ir/lower/stmt.zig`) writes each named
member into a union-sized slot via the assignment-path lvalue resolver, then loads it back.
Single-arm only (one direct member, or same-arm promoted members); overlapping/different-arm/
positional literals are diagnosed. specs.md updated. Regressions: `examples/types/0194` +
`examples/diagnostics/1191`.
- **✅ issue 0157 — FIXED** (B1.4a) — a user generic `ufcs` method whose name collides with a
stdlib re-export resolved via last-wins `fn_ast_map` with no receiver filtering → wrong overload →
`$R` unbound → LLVM panic. Fix: `selectUfcsGenericByReceiver` (`src/ir/lower/call.zig`) — most
@@ -593,6 +634,17 @@ incomplete); a dedicated effort; lambda workers are the idiom meanwhile.
trusted. `18xx` asserts program-emitted ordering contracts, not raw interleaving.
## Log
- **B1 follow-up — `Scheduler.deinit`.** Closes the bounded leaks B1 documented. Added a `task_allocs:
List(*void)` field (appended in `go` so the scheduler can reach its generic `Task($R)`s) + a canonical
`close` extern, then a terminal idempotent `deinit`: reap leftover ready fibers (`munmap` + free) →
free tracked Tasks → `List.deinit` the 3 backings → `close` the lazy kqueue fd (reset `-1`). Closure
envs stay unfreeable (documented). Probe-observed the accounting under a tracking GPA (deinit drives
live allocs 7→3 in a spawn+sleep+2×go run; residual = envs). Locked by
`1820-concurrency-fiber-scheduler-deinit.sx` (one run hits timers + kqueue fd + Tasks; `freed by
deinit: 5`, `live after deinit: 5` (env residual), `kq open after run: true`→`kq after deinit: -1`,
`read: 3 [97 98 99]`), `.build {"target":"macos"}`. Adversarial review: no real UAF/over-free in the
supported deinit-after-`run` path; reconciled a doc contradiction (terminal-contract wording); 0154
over-store concern probed + cleared (`kq == -1` right after `init`). Suite GREEN **759/0**.
- **B1.4c — real fd-readiness blocking via kqueue (macOS).** De-risked first with a no-scheduler probe
(confirmed `size_of(Kevent)==32` and the pipe→kevent roundtrip: `kq_wait` returned 1, `out.ident ==
read_fd`, `out.filter == -1`, `out.data == 1` — the struct layout reads the fd back correctly). Then