feat: reclaim fiber + async heap (close the closure-env / Future leaks)

Closes the documented per-spawn closure-env leak and most of the async leak,
using only the existing closure.env / closure.fn_ptr field accessors — no compiler
change. Also names the fat-pointer ABI in core.sx (ClosureRaw / SliceRaw) so the
underlying {fn_ptr, env} / {ptr, len} layout is discoverable in one place.

- Fiber body env: Scheduler.reap_fiber frees f.body.env via f.dctx.allocator (the
  spawn-time allocator snapshotted in dctx) at all three reap sites (run/poll/
  deinit). 1820's 'live after deinit' 3 -> 0.
- Async box + closure envs: sx_run_boxed_closure frees the ThunkBox, the
  completion-closure env, and the worker's env (new ThunkBox.worker_env) the
  instant the worker completes.
- Async Future: two-flag ownership — Future.worker_done (set at the end of the
  completion closure) + consumed (set at the end of await); fut_release frees the
  heap Future (via the captured Future.alloc) when BOTH are set, so the LAST of
  {worker, await} reclaims it. await now CONSUMES the future (single-use; touching
  it afterward is a use-after-free — documented). Residual for an AWAITED future
  is 0 (lock: examples/concurrency/1827); a never-awaited future (fire-and-forget /
  race loser) keeps only its Future struct — the structured-concurrency remainder.

Self-reviewed across orderings (await-after/before-complete, cancel-then-await,
cancel-while-parked, double-free via await+deinit, race residual, blocking impl,
cross-allocator reap) — all deterministic, no UAF/double-free. Suite 855/0;
byte-identical on aarch64-macOS + aarch64-linux; .ir churn is the core.sx +
Future/ThunkBox field additions.
This commit is contained in:
agra
2026-06-28 16:19:04 +03:00
parent aae7d72a66
commit 2b1307a0dc
52 changed files with 168342 additions and 160106 deletions

View File

@@ -160,3 +160,35 @@ Context :: struct {
Into :: protocol(Target: Type) {
convert :: (self: *Self) -> Target;
}
// --- Raw ABI views of the language's fat-pointer types -----------------------
//
// sx's closures and slices/strings are two-word "fat" values. These structs name
// that underlying layout in ONE place so it is discoverable and documented, and so
// owning code can reinterpret a fat value (`raw : ClosureRaw = xx c`) to reach a
// field the ergonomic accessors do not expose for a use case — e.g. freeing a
// stored closure's heap `env`. Field order/types mirror the compiler ABI
// (`types.zig`: closure / slice size = 2 words); if that ABI ever changes these
// move with it.
//
// The ergonomic accessors are the normal way in: a closure value answers
// `.fn_ptr` (the code pointer) and `.env` (the captured environment — heap,
// allocated at the literal via the then-current `context.allocator`; `null` for a
// capture-free closure), and a slice/string answers `.ptr` / `.len`. The `*Raw`
// structs are the explicit type-erased layout behind those accessors.
// A closure value: `{ fn_ptr, env }`. Reinterpret with `xx` to reach `env` for
// ownership/lifetime work (the owner of a stored closure frees `env` when the
// closure is dead). Equivalent to the `c.fn_ptr` / `c.env` field accessors.
ClosureRaw :: struct {
fn_ptr: *void;
env: *void;
}
// A slice or string value: `{ ptr, len }` (the element type is erased to bytes
// here). Equivalent to the `s.ptr` / `s.len` accessors. `len` is the element
// count (an `i64`, matching the ABI), not a byte count.
SliceRaw :: struct {
ptr: [*]u8;
len: i64;
}