diff --git a/NEXT.md b/NEXT.md index d1ad386..0ff527a 100644 --- a/NEXT.md +++ b/NEXT.md @@ -362,6 +362,49 @@ Already banked, and it means classes are less work than plan.org implies: **a ge whose body is a dispatch table, which a reload extends. That is the expensive half of method dispatch, and it is built and tested. +**Resource cleanup: `defer` stays the answer. `drop` is not built, and `with-cleanup` is not either.** Reached by +working the case through rather than by preference, so the reasoning is worth keeping. + +`drop` was specified in `spec-memory.md` this morning. **This amends it: the hook is deferred, not built.** Three things +decided against it. It runs code somewhere the reader is not looking, which is the C++ behaviour the author explicitly +does not want. It would not even cover the motivating case — `Image` and `Texture2D` are *raylib's* types, and attaching +a hook to a foreign type is its own unsolved design question. And its one real advantage, cascading through a container, +is the case `Handle` is about to make rare: entities holding handles hold numbers, not resources. + +`with-cleanup` / `unwind-protect` was also put and rejected: awkward with several resources, and it reads worse than +what already exists. The raylib begin/end pairs that seemed to motivate it are a macro problem, not a primitive one — +`with-drawing` and `with-mode-2d` are three-line macros once the expander lands. + +**What to build instead is small: relax where `defer` may be written.** It is refused today inside a `let`, a loop or a +branch. The loop and branch refusals are right — `defer` is a *compile-time* construct, the cleanup copied into every +exit path, so "maybe registered" is not expressible and a loop body would fire once at function exit instead of once per +iteration. But **a `let` at the top level of a function body has exactly the function's extent** and always registers, +so it is as safe as function scope and is refused for a reason that does not apply to it. Relaxing it gives: + +``` +(defn load-brush [] + (let [sheet (rl/load-image-from-memory ".png" brush-bytes)] + (defer (rl/unload-image sheet)) + (set brush (rl/load-texture-from-image sheet)) + (rl/image-flip-horizontal (addr sheet)) + (set brush-mirrored (rl/load-texture-from-image sheet)))) +``` + +Several resources are several defers, released in reverse, visible in acquisition order. A container of resources is an +ordinary loop inside the defer body — the manual cascade, three lines, at one level of nesting. + +**Odin, for the record**, has no destructors, no drop and no finalizers: `delete` frees container memory and nothing +else, and resource release is `defer` at the acquisition site. That idiom does not transfer directly only because +Odin's `defer` is block-scoped; the relaxation above recovers most of it. + +**The safety net, and the better use of effort: a debug tracking allocator.** ASan's leak detection covers memory +*instrumented* code allocated — the Flan allocator, and it is already wired up and clean. It does **not** cover a leaked +texture, because that memory belongs to uninstrumented raylib, which is the same reason the sanitizer sweep treats the +windowed examples as noise. But every raylib call goes through a *generated* wrapper, so a dev build can count +acquisitions against releases at that boundary and report what is still held at exit, by name. No hook, no type +annotation, nothing running at a distance — it does not change how code is written, it reports when something was +forgotten. + ## Blocked and unfinished Everything below was found, decided or half-built and then stopped. Each says what blocks it. Nothing here is a