flan/test/programs/exhausted.flan
Joseph Ferano af8d291154 (Vec T) over a type-erased runtime, with StorageExhausted going in beside it
Two element types, one runtime, and the element type appears nowhere below
the call site: size_of and align_of are produced where the concrete type is
known, which without generics is simply the concrete call site. That is
Odin's arrangement and it is what spec-memory.md specifies. `at` and `len`
were already the names for a fixed array and a slice, so a Vec extends them
rather than adding a parallel pair — the asymmetry `nth` was removed for —
and the value form and the place form go through one helper so they cannot
drift apart.

StorageExhausted lands with step 2 rather than after it, because the
signatures depend on it: `push` and `reserve` are Unit, `clone` is the
container, and nothing grows a Result. It is built out of nodes that already
existed — a while, a restart-case and an error — so the backend learned
nothing about allocation. The restart is established at the failing
allocation, which spec-memory.md names as the exception to "restarts go at
the resync point, once", and the element a push was given is bound to a slot
before the loop so a retry re-attempts the allocation and not the expression.

Move-only is a dead set on the checker context, and it is flow-sensitive at
an `if`: both arms start from the same set and the union survives the join,
so `(if c (free v) (free v))` is legal and a one-armed free still kills the
binding. The case a dead set cannot answer is a move inside a loop — merged
once at the end of the body it counts one move, not two — so that is a rule,
refused with its reason.

Four decisions the spec did not settle:

The Vec header is six words in every build, not four in release. A layout
that changes with a build flag can disagree across the reload boundary
silently: a redefinition module is built by llc and ld against a host built
separately, and nothing makes the two agree on a struct size. The 32-byte
release layout is deferred on that.

A zeroed Vec has a null allocator, and the first operation needing storage
adopts the context allocator. Odin's behaviour. The alternative was refusing a
Vec-typed struct field until drop lands; shipping the null was a null deref on
the first push.

A Vec's length and index are i32, like every other length here. Widening
indices is one change across all the containers, not a Vec question.

`let` has no type annotation, so a local Vec has nowhere to say what it holds
and the element type is written at the call: `(vec-new i32)`. This is not the
explicit instantiation syntax the generics section rules out — nothing here is
generic and the name resolves as an ordinary type. Where the context says, it
may be left out.

The allocator grew a budget: a ceiling on live bytes, 0 for none. The retry
restart is only answerable by a handler that can make the *same* request
succeed, and for a fixed backing store the handler that works is the one that
raises the ceiling — releasing the region a container lives in invalidates
the container, which is what the epoch check catches. The spec's "grows the
arena and then invokes retry" needed something to grow.

The generation word is bumped on every reallocation and read by nothing. The
stale-slice trap it is for needs a slice that can carry the Vec's identity,
and a slice is ptr+len. Said plainly rather than implied by the word's
presence.
2026-09-12 11:07:57 +07:00

93 lines
4.0 KiB
Plaintext

;;;; StorageExhausted and retry — spec-memory.md, "Allocation failure".
;;;;
;;;; No allocating operation returns an error and none can fail silently. The
;;;; operation signals StorageExhausted with `error`, whose type is Never,
;;;; inside a restart-case offering `retry` — so push stays Unit, clone stays
;;;; the container, and no signature anywhere grows a Result. Odin's append
;;;; returns an ignorable Allocator_Error; an append that appends nothing and
;;;; says nothing is the outcome this rule exists to make impossible.
;;;;
;;;; This is also the named exception to plan.org's "restarts go at the resync
;;;; point, once": the restart is established *at the failing allocation*,
;;;; because a restart at an outer loop cannot re-attempt an allocation and
;;;; only the allocation site can.
;;;;
;;;; The handler that works is the one that raises the ceiling and retries.
;;;; Releasing the region the container lives in does not work and must not be
;;;; written: it invalidates the container, which the epoch check then catches
;;;; — and that case is its own program, stale-region.flan.
;; Globals, because a handler cannot see the locals of the function that
;; established it: check.ml's `captured` refuses one by name and says to use a
;; global. That refusal is the accumulation pattern, and it is not built.
(defvar tight Allocator)
(defvar failures i64)
(defvar last-bytes i64)
(defvar last-align i64)
(defvar same-allocator bool)
(defn main [] i32
;; The general-purpose tier, with a ceiling on it. 32 bytes is four i32 and
;; the doubling past it is not.
(set tight (heap-allocator))
(set-alloc-budget tight 32)
(handler-bind
[(StorageExhausted [c]
(set failures (+ failures 1))
;; The condition is a value struct with fixed numeric fields and no
;; rendered message: formatting would allocate, and this is the one path
;; that must not. Rendering happens here, where a working allocator is
;; known.
(set last-bytes (.bytes c))
(set last-align (.align c))
;; It names which region ran out, so a handler holding several can tell
;; them apart.
(set same-allocator (= (.allocator c) (alloc-id tight)))
;; Grow it, then re-attempt the same request. The Vec is untouched and
;; its allocator is unchanged, which is why this retry can succeed.
(set-alloc-budget tight (* 4 (alloc-budget tight)))
(invoke-restart 'retry))]
(let [v (vec-new i32 tight)]
;; Somewhere in here the ceiling is hit, the handler raises it, and the
;; push that failed is re-attempted. No push is lost: a failed push
;; appends nothing and the retry appends exactly once.
(dotimes [i 64] (push v (* i 2)))
(println (len v)) ; 64
(println (at v 0)) ; 0
(println (at v 63)) ; 126
(free v)))
;; The handler ran, more than once, and what it saw were the numbers of the
;; request that did not fit.
(println (> failures 1)) ; true
(println (> last-bytes 0)) ; true
(println last-align) ; 4 — align-of i32, from the call site
(println same-allocator) ; true
;; Every allocating operation, not only push. reserve asks for the whole
;; block at once, and clone asks the new allocator for the source's length.
(set-alloc-budget tight 32)
(set failures 0)
(handler-bind
[(StorageExhausted [c]
(set failures (+ failures 1))
(set-alloc-budget tight 4096)
(invoke-restart 'retry))]
(let [v (vec-new i32 tight)]
(reserve v 256)
(println (len v)) ; 0
(dotimes [i 8] (push v i))
(set-alloc-budget tight 4128)
(let [w (clone v)]
(println (len w)) ; 8
(println (at w 7)) ; 7
(free w))
(free v)))
(println (> failures 0)) ; true
;; And the restart is not once-per-program: it is established at each
;; allocation, so a later one offers it again.
(set-alloc-budget tight 0)
0)