The trio the author decided on 2026-09-20 is now all built: def is CL's defparameter — its initialiser runs on every daemon re-run, unguarded, so an edited initialiser repaints the same storage on C-c C-c plus re-run — defonce (Clojure's name for CL's defvar, per the author) initialises once behind the .init~once. flag, and defconst stays the image. One parse arm reads both forms; the difference is Ast.reinit, carried to Tast.global's grerun. Emit.startup_plan gives a def no guard flag, and Check.check_global lifts every def initialiser — zero and literal included — into global/<n>, so the host's startup reaches it through the function cell and a re-evaluated def swaps it (Session's def_inits; Emit.redefinition declares the cell for a non-sibling target). The old defvar spelling is refused with the rename and both compiling spellings, and every program, test, doc and editor list is swept — except sand.flan, the author's live WIP, whose seven defvar lines are flagged in FIX.org and keep its three dependent tests red on this branch.
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.
|
|
|
|
(defonce 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)
|