Files
sx/examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx
agra 55ed9a248e 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 '---'.
2026-06-22 09:45:33 +03:00

125 lines
4.8 KiB
Plaintext

// 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;
}