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

@@ -37,9 +37,7 @@ append :: (sh: *Shared, v: i64) {
}
main :: () -> i64 {
sh : Shared = ---;
sh.n = 0;
sh.done[0] = 0; sh.done[1] = 0; sh.done[2] = 0;
sh : Shared = .{ n = 0 }; // seq[] + done[] zero-filled
s := sched.Scheduler.init();
ps := @s;

View File

@@ -32,8 +32,7 @@ Log :: struct { seq: [16]i64; n: i64; }
rec :: (l: *Log, v: i64) { l.seq[l.n] = v; l.n = l.n + 1; }
main :: () -> i64 {
lg : Log = ---;
lg.n = 0;
lg : Log = .{ n = 0 }; // seq[] zero-filled
s := sched.Scheduler.init();
ps := @s;

View File

@@ -43,8 +43,7 @@ Log :: struct { ids: [16]i64; ts: [16]i64; n: i64; }
rec :: (l: *Log, id: i64, t: i64) { l.ids[l.n] = id; l.ts[l.n] = t; l.n = l.n + 1; }
main :: () -> i64 {
lg : Log = ---;
lg.n = 0;
lg : Log = .{ n = 0 }; // ids[] + ts[] zero-filled
s := sched.Scheduler.init();
ps := @s;

View File

@@ -41,10 +41,7 @@ S :: struct {
}
main :: () -> i64 {
st : S = ---;
st.wrote = false;
st.read_n = 0;
st.read_done = false;
st : S = .{ wrote = false, read_n = 0, read_done = false }; // bytes[] zero-filled
fds : [2]i32 = ---;
if pipe(@fds[0]) != 0 {

View File

@@ -0,0 +1,124 @@
// Stream B1 (fibers) — `Scheduler.deinit` releases the scheduler's owned heap
// + fd resources, closing the documented bounded leaks (kq fd / heap Tasks /
// List backings). Verified by a tracking `GPA`: deinit drives the live
// allocation count DOWN, and resets the kqueue fd to -1.
//
// Scenario (one run that touches every freed resource):
// - a SLEEPER fiber `sleep(5)`s → exercises the `timers` List
// - a READER fiber `block_on_fd`s a pipe → exercises the kqueue fd + the
// `io_waiters` List
// - a WRITER fiber writes 3 bytes → makes the pipe readable
// - two `go` tasks compute 42 / 7 → exercise the heap `Task`s +
// the `task_allocs` List
// After `run()` drains all of it, `deinit()` frees: the 2 heap Tasks, the
// `timers` / `io_waiters` / `task_allocs` List backings, and CLOSES the kqueue
// fd (resetting `kq` to -1). The Fibers were already reaped during `run()`.
//
// WHAT IT PROVES (the contract; numbers below are the snapshot):
// - `freed by deinit: N` — live allocations reclaimed by `deinit` (> 0).
// - `live after deinit` — the RESIDUAL. This is NOT zero and NOT a bug: it is
// exactly the documented closure-env leak — one heap env per `spawn`/`go`
// that sx cannot free (the runtime has no name for the env pointer). deinit
// reclaims everything it CAN; the env residual is a language limitation.
// - `kq open after run: 1` then `kq after deinit: -1` — the lazily-opened
// kqueue fd was genuinely open after the fd round and is closed by deinit.
// - `read: 3 [97 98 99]` — the fd path actually ran (reader blocked, woke via
// kqueue, read 'a' 'b' 'c'), so the kq we close is a real, used fd.
//
// Counts are captured into locals BEFORE any `print` — `print` itself allocates
// format temporaries through the same GPA, which would otherwise pollute the
// reading.
//
// aarch64-macOS-pinned (`.build {"target":"macos"}`, matches host → runs
// end-to-end): sched.sx's switch asm + the kqueue path are per-arch/Apple.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
// Raw libc fd primitives — canonical signatures (the extern dedupe rejects a
// divergent re-binding of the same C symbol). `close` matches sched.sx's own.
pipe :: (fds: *i32) -> i32 extern libc "pipe";
read :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "read";
write :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "write";
close :: (fd: i32) -> i32 extern libc "close";
S :: struct {
read_n: i64;
bytes: [8]u8;
read_done: bool;
}
main :: () -> i64 {
st : S = .{ read_n = 0, read_done = false }; // bytes[] zero-filled; read() fills it
fds : [2]i32 = ---;
if pipe(@fds[0]) != 0 {
print("1820: pipe() failed\n");
return 1;
}
read_fd := fds[0];
write_fd := fds[1];
// Captured under the GPA scope; printed after it closes.
after_run : i64 = 0;
after_deinit : i64 = 0;
kq_open_run : bool = false;
kq_after : i32 = 0;
gpa := mem.GPA.init();
push Context.{ allocator = xx gpa, data = null } {
s := sched.Scheduler.init();
ps := @s; pst := @st;
// SLEEPER — arms a virtual-time timer, then parks.
ps.spawn(() => { ps.sleep(5); });
// READER — blocks on the empty pipe until kqueue reports it readable.
mk_reader :: (ps: *sched.Scheduler, pst: *S, rfd: i32) {
ps.spawn(() => {
ps.block_on_fd(rfd, true);
n := read(rfd, xx @pst.bytes[0], xx 3);
pst.read_n = xx n;
pst.read_done = true;
});
}
// WRITER — writes 'a' 'b' 'c', making the pipe readable.
mk_writer :: (ps: *sched.Scheduler, wfd: i32) {
ps.spawn(() => {
buf : [3]u8 = ---;
buf[0] = xx 97; buf[1] = xx 98; buf[2] = xx 99;
write(wfd, xx @buf[0], xx 3);
});
}
mk_reader(ps, pst, read_fd);
mk_writer(ps, write_fd);
// Two async tasks — heap Tasks tracked for deinit to free.
ps.go(() -> i64 => 42);
ps.go(() -> i64 => 7);
ps.run();
after_run = gpa.alloc_count;
kq_open_run = s.kq >= 0;
ps.deinit();
after_deinit = gpa.alloc_count;
kq_after = s.kq;
}
print("read: {} [", st.read_n);
i := 0;
while i < st.read_n {
if i > 0 { print(" "); }
print("{}", st.bytes[i]);
i = i + 1;
}
print("]\n");
print("freed by deinit: {}\n", after_run - after_deinit);
print("live after deinit: {}\n", after_deinit);
print("kq open after run: {}\n", kq_open_run);
print("kq after deinit: {}\n", kq_after);
close(read_fd);
close(write_fd);
return 0;
}

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,5 @@
read: 3 [97 98 99]
freed by deinit: 5
live after deinit: 5
kq open after run: true
kq after deinit: -1