A (Vec Value) where a Value may itself hold a (Vec Value) — the recursive dynamic value an EDN reader has to answer with when nobody hands it a target struct type — was refused five different ways, and every one of the five gave the same reason: the container runtime is type-erased, so it copies and releases slots bytewise and cannot reach inside a slot. A free would release the slots and leave every block they point at stranded. That reason is about teardown, and it does not hold for a region. free-all never releases an individual slot; it takes the whole arena, and every block the elements own is in it, because they came out of it. The refusals were over-broad, and what they were guarding was never ownership — ownership tracking is untouched here, moves are still moves, and Types.is_move_only is the same function it was. So the question moved rather than disappeared. It could not stay at the type, because can-free is a capability on an allocator value and with-allocator rebinds a dynamic variable: which tier a (vec-new) will meet is not a property of the place its type is written. What is decided at compile time is only whether to ask, which is a property of the element type; the answer is a run-time branch on the allocator, one per container and never per element, because the alternative is a walk at release and a walk at release is the registry of destructors the frame tier's reset exists to not have. It is emitted at every growth and not only at the construction, because ZII means a container can exist without ever passing through (vec-new) — a case field left out of a literal, a global that starts zeroed — and those adopt the context on their first push. free on such a container is refused rather than made quietly shallow. It cannot recurse, which is the whole premise, and releasing the outer block alone would be "I freed it" written over a program that stranded everything inside; this runtime refuses that collapse everywhere else. The message names free-all, which is reachable by construction. clone stays refused for a reason the region does not dissolve, and the old message had bundled the two failures under one sentence: what disqualifies clone is not that it copies a header — so do at and get, and they are fine, because they promise nothing — it is that clone allocates a new block and promises independence, and a bytewise copy hands back elements still pointing into the original's region. A struct or union field is admitted only where the field's container holds owning elements, because that container can only have been built against a region. A field holding a plain (Vec u8) stays refused: nothing would force that one into a region, and two copies of the aggregate would be two headers over one heap block. vec-in-struct.flan still pins that. The epoch already covered use after free-all, including the case this makes reachable — an inner header copied out of an arena-held element into a local still traps, because an Allocator is a pointer and a copied-by-value one would carry its own epoch. arena-value.flan builds the value by hand; arena-edn.flan reads a real document through the tokenizer, and its reader takes no allocator and names none, because spec-memory.md already puts the allocator in the calling convention. arena-region.flan is the branch itself: run 0 is the (Vec (Vec i32)) control that must not trap, and runs 1 and 2 are the two ways this dies.
92 lines
3.5 KiB
Plaintext
92 lines
3.5 KiB
Plaintext
;;;; The recursive dynamic value, held in an arena and released by one
|
|
;;;; free-all. A reader handed no target struct type has to answer *something*,
|
|
;;;; and the something is this: a data type naming itself through a (Vec Value)
|
|
;;;; and a (Map string Value).
|
|
;;;;
|
|
;;;; Five refusals used to stand between here and a type like this, and every
|
|
;;;; one of them gave the same reason: the type-erased runtime copies and
|
|
;;;; releases slots bytewise, so a free would release the slots and leave what
|
|
;;;; they point at stranded. That reason is about *teardown*, and an arena has
|
|
;;;; none — free-all takes the whole region, and the inner blocks are in it
|
|
;;;; because they came out of it. So the refusals moved from the type, where
|
|
;;;; the allocator is not knowable, to the construction, where it is a value.
|
|
;;;;
|
|
;;;; There is no drop, no destructor, no finalizer and no per-element teardown
|
|
;;;; anywhere below. The release at the bottom of main is one call.
|
|
|
|
(defvar frame Allocator)
|
|
|
|
(defdata Value
|
|
[(Nil [])
|
|
(Int [n i64])
|
|
(Text [s string])
|
|
(List [items (Vec Value)])
|
|
(Table [entries (Map string Value)])])
|
|
|
|
;; [0 1 .. n-1] as a dynamic list. No allocator is named: with-allocator in
|
|
;; main has rebound the context, and spec-memory.md puts the allocator in the
|
|
;; calling convention precisely so that a builder like this need not carry one
|
|
;; through its signature.
|
|
(defn number-list [n i32] Value
|
|
(let [items (vec-new Value)]
|
|
(dotimes [i n]
|
|
(push items (Value.Int {.n (i64 i)})))
|
|
(Value.List {.items items})))
|
|
|
|
;; A table whose values are themselves lists, so the graph is three levels
|
|
;; deep before it reaches a leaf: Table -> Vec -> List -> Vec -> Int.
|
|
(defn a-table [] Value
|
|
(let [entries (map-new string Value)]
|
|
(put entries "xs" (number-list 3))
|
|
(put entries "ys" (number-list 5))
|
|
(put entries "name" (Value.Text {.s "edn"}))
|
|
(Value.Table {.entries entries})))
|
|
|
|
;; Reading it back. (at v i) addresses the element in place and (get m k)
|
|
;; answers a copy of the value's bytes, and in a region both are the same
|
|
;; thing: an alias into storage nobody individually owns. That is the bargain
|
|
;; a region is, and it is why no accessor beyond the two already here is
|
|
;; needed to walk a parsed document.
|
|
(defn total [v Value] i64
|
|
(match v
|
|
(Int n) n
|
|
(List items)
|
|
(let [t (i64 0)]
|
|
(dotimes [i (len items)]
|
|
(set t (+ t (total (at items i)))))
|
|
t)
|
|
(Table entries)
|
|
(+ (match (get entries "xs") (Some x) (total x) None (i64 0))
|
|
(match (get entries "ys") (Some y) (total y) None (i64 0)))
|
|
_ (i64 0)))
|
|
|
|
(defn build [] i64
|
|
(let [outer (vec-new Value)]
|
|
(dotimes [i 3]
|
|
(push outer (number-list (+ i 2))))
|
|
(push outer (a-table))
|
|
(push outer Value.Nil)
|
|
(println (len outer))
|
|
(let [t (i64 0)]
|
|
(dotimes [i (len outer)]
|
|
(set t (+ t (total (at outer i)))))
|
|
t)))
|
|
|
|
(defn main [] i32
|
|
(set frame (arena-new 65536))
|
|
(with-allocator frame
|
|
(println (build)))
|
|
;; The whole graph, in one operation. Every Vec block, every Map block and
|
|
;; every string the values point at came out of this region, so this is all
|
|
;; of it — and the epoch moves, so anything still holding one of those
|
|
;; headers traps rather than reading released bytes.
|
|
(free-all frame)
|
|
;; And the region is reusable, which is what makes it the frame tier: the
|
|
;; pages stayed, the offset went back to zero, and a second document builds
|
|
;; in the same bytes the first one used.
|
|
(with-allocator frame
|
|
(println (build)))
|
|
(free-all frame)
|
|
(arena-destroy frame)
|
|
0)
|