The INSERTIONSORT crash, all three rulings (FIX.org 2026-09-20): - (bytes s) allocates a writable copy through the allocator surface — context or (bytes s a), StorageExhausted with retry, a registry note in dev builds (flan_bytes_dup, lowered like vec-new). (bytes-view s) is the old zero-cost reinterpret, renamed, read-only by convention; every in-repo reader swept over to it. (string b) unchanged. - String constants were already read-only on both backends at -O0; now pinned — bytes-copy.flan rows on LLVM/-O0/--x86, and dies_segv rows asserting the write-through-view trap on both backends. - A dev build installs a SIGSEGV/SIGBUS handler by the same dev-only constructor slot that arms the registry: one line naming the address and the innermost frame, then the trap-hook park — stopped, not dead, the daemon serving. No agent: message and re-raise. Release builds untouched. Pinned by trap_park over dev-segv.flan.
110 lines
5.0 KiB
Plaintext
110 lines
5.0 KiB
Plaintext
;;;; spec-memory.md's arena rule, which is the run-time half of what replaced
|
|
;;;; the three type-level refusals a container of owning elements used to meet.
|
|
;;;;
|
|
;;;; "Constructing a container whose element type owns storage, against an
|
|
;;;; allocator that lacks can-free, is refused at the point of construction:
|
|
;;;; one branch per container, not per element." One branch and not a walk,
|
|
;;;; because the alternative is something that inspects the graph at release,
|
|
;;;; and that is the registry of destructors the frame tier's reset exists to
|
|
;;;; not have.
|
|
;;;;
|
|
;;;; It is a run-time branch and not a compile-time refusal because there is
|
|
;;;; nothing static to refuse against: with-allocator rebinds a dynamic
|
|
;;;; variable, so which tier a (vec-new) meets is not knowable where it is
|
|
;;;; written. The compiler decides only whether to *ask*.
|
|
;;;;
|
|
;;;; Argument 0 is everything that must work, argument 1 and argument 2 are the
|
|
;;;; two ways this dies. Each death is the whole test of its case, so they are
|
|
;;;; separate runs rather than one program that could pass by dying early.
|
|
|
|
(defvar frame Allocator)
|
|
|
|
(defalias Row (Vec i32))
|
|
|
|
(defdata Value [Nil (Int [n i64]) (List [items (Vec Value)])])
|
|
|
|
(defn main [args [string]] i32
|
|
(set frame (arena-new 4096))
|
|
(let [which (if (> (len args) 1) (i32 (bytes->i64 (bytes-view (at args 1)))) 0)]
|
|
(cond
|
|
(= which 1)
|
|
;; The refusal. The context here is the heap, which can free one
|
|
;; block, and a (Vec Value) against it is a free that would release
|
|
;; the slots and strand every inner Vec — so the construction dies
|
|
;; rather than the free three hundred lines later.
|
|
(let [bad (vec-new Value)]
|
|
(println (len bad)))
|
|
|
|
(= which 2)
|
|
;; Use after free-all, which is a different mechanism and worth
|
|
;; pinning separately: the allocator's epoch moves on every free-all
|
|
;; and every container records the epoch it was made at. The header
|
|
;; below was copied *out* of the arena container into a local before
|
|
;; the release, which is the case the check has to cover and the
|
|
;; reason spec-memory.md makes an Allocator a pointer rather than a
|
|
;; copied value — a copied allocator would carry its own epoch and
|
|
;; the copy would never notice.
|
|
(let [outer (vec-new Value frame)]
|
|
(let [inner (vec-new Value frame)]
|
|
(push inner (Value.Int {.n (i64 7)}))
|
|
(push outer (Value.List {.items inner})))
|
|
(match (at outer 0)
|
|
(List items)
|
|
(do (println (len items))
|
|
(free-all frame)
|
|
(println (len items)))
|
|
_ (println 0)))
|
|
|
|
(= which 3)
|
|
;; ZII, which is the hole a guard only at the construction would have
|
|
;; left. The items field is omitted from the literal, so it is a zeroed
|
|
;; Vec with no allocator at all — it never went near (vec-new) — and
|
|
;; the first push is what adopts the context. So the branch is emitted
|
|
;; at every growth too, and there it asks the container, which answers
|
|
;; from the allocator it will adopt when it has none of its own.
|
|
(let [v (Value.List {})]
|
|
(match v
|
|
(List items)
|
|
(do (push items (Value.Int {.n (i64 1)}))
|
|
(println (len items)))
|
|
_ (println 0)))
|
|
|
|
:else
|
|
(do
|
|
;; The control, and it is the case the frame tier exists for: a
|
|
;; (Vec (Vec i32)) owns storage at two levels and is perfectly happy
|
|
;; in a region, because free-all releases every block the region
|
|
;; handed out and the inner ones are among them. The rule asks about
|
|
;; the *allocator*, never "does this element own anything", so this
|
|
;; must be built without complaint.
|
|
(with-allocator frame
|
|
(let [rows (vec-new Row)]
|
|
(let [row (vec-new i32)]
|
|
(push row 1)
|
|
(push row 2)
|
|
(push rows row))
|
|
(println (len rows))
|
|
(println (len (at rows 0)))))
|
|
(free-all frame)
|
|
;; And the same container against the heap dies — asserted from the
|
|
;; other side in run 1 above; here the point is only that the region
|
|
;; run above got no complaint.
|
|
(with-allocator frame
|
|
(let [vs (vec-new Value)]
|
|
(push vs (Value.Int {.n (i64 41)}))
|
|
(println (len vs))))
|
|
(free-all frame)
|
|
;; And the zeroed field of run 3, this time in the region: the growth
|
|
;; guard has to pass here as surely as it has to fail there, or every
|
|
;; ZII container in an arena would be unusable.
|
|
(with-allocator frame
|
|
(let [v (Value.List {})]
|
|
(match v
|
|
(List items)
|
|
(do (push items (Value.Int {.n (i64 1)}))
|
|
(println (len items)))
|
|
_ (println 0))))
|
|
(free-all frame))))
|
|
(arena-destroy frame)
|
|
0)
|