The slot after a defn's parameters is unconditionally a type. Parse.decl no
longer takes a set of type names, and is_type_form, qualified_type, types_in,
declared_types and prelude_types are gone with the pre-pass that fed them.
What they were for: (Option f64) and (Some 1) are the same s-expression, so the
parser decided which it had by looking the head up in a set of the file's own
type names. Sound -- one top-level namespace means a name cannot be both a type
and a value -- and brittle, because the set had to be complete. It was wrong
twice in one day, the second time parsing (defn f [] (Rune {.code 65}) (bar))
as a function returning a Rune with a one-form body, silently, in every file in
the language.
Two things fall out. A type the parser could not have known -- a struct
declared further down the file, rl/Vector2 behind an unresolved alias, a
prelude type -- never needed recognising, only placing. And a mistyped type is
a mistyped type: (defn f [] f65 0.0) reaches the resolver's near-miss check and
says did you mean f64, where it used to be read as the first form of the body
and reported as an unknown name.
Unit is written (). The old spelling is refused with a message naming the new
one, the rule the colon-to-dot change followed. Internally it is still
Tname "Unit" and Types.Unit, so the resolver, the shim and the emitter did not
change; Cimport still builds Tname "Unit" for C's void without going through
the parser. Types.to_string prints () though -- that printer prints what a
person would write for every other type it knows, [i32], {K V}, (Ptr T), and
Unit was the odd one out once the source spelling moved.
Dropping prelude_types removes one of the two reasons Macro.reduce may only
drop defns: the memoised set a bootstrap build could have poisoned is gone, so
the remaining reason is the plain one.
93 lines
4.0 KiB
Plaintext
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 (), 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)
|