The !-means-mutates convention distinguished nothing — there is no immutable counterpart to contrast with — so every mutating name drops the mark: sort, sort-by, sort-bytes, swap, reverse, append, append-i64, append-f64, encode-rune, split-next, map-remove, map-next, and the test helpers beside them. Two could not simply shed it: map! is map-in-place, because map is the into transform's word and means the non-mutating thing; put! is put-at, because put is the Map builtin. The ?-means-asks convention stays. Dated records keep the old spellings; watch.clj's reset-spies! and the other Clojure names are not ours to rename.
128 lines
5.2 KiB
Plaintext
128 lines
5.2 KiB
Plaintext
;;;; An EDN document read into a dynamic value, against an arena.
|
|
;;;;
|
|
;;;; This is the other half of programs/edn.flan. That one reads a document
|
|
;;;; whose shape is known into a struct, by hand, which is what the compiler's
|
|
;;;; (read-edn Enemy bytes) will emit. This one is what a reader handed *no*
|
|
;;;; target type has to answer with: a data type naming itself through a
|
|
;;;; (Vec Value) and a (Map string Value), holding whatever was in the file.
|
|
;;;;
|
|
;;;; ── The allocator story, which is the point of the program ───────────
|
|
;;;;
|
|
;;;; edn/read takes no allocator and names none. It does not need to:
|
|
;;;; spec-memory.md puts the allocator in the calling convention, so every
|
|
;;;; (vec-new) and (map-new) inside it takes the *context*, and the caller
|
|
;;;; chooses the tier with (with-allocator ...) around the call. An explicit
|
|
;;;; allocator at a construction site overrides that, which is how a reader
|
|
;;;; would take one as a parameter if it wanted to — but the existing idiom
|
|
;;;; already does the job, so there is no new machinery here and none needed.
|
|
;;;;
|
|
;;;; The tier has to be a region and not the heap, and that is enforced rather
|
|
;;;; than documented: a (Vec Value) whose elements own storage traps at its
|
|
;;;; construction against any allocator that can free one block. See
|
|
;;;; programs/arena-region.flan.
|
|
;;;;
|
|
;;;; ── What the region buys, said plainly ───────────────────────────────
|
|
;;;;
|
|
;;;; The document below is four levels deep and every level allocates. There is
|
|
;;;; no teardown anywhere in this file: no drop, no destructor, no recursive
|
|
;;;; free, not even a (free) call. One (free-all frame) at the bottom of main
|
|
;;;; releases every Vec block, every Map block and every entry in them, because
|
|
;;;; they all came out of the same region.
|
|
;;;;
|
|
;;;; Odin's core:encoding/json ships a hand-written recursive destroy_value in
|
|
;;;; the *library* for the heap case, and names parsing against temp_allocator
|
|
;;;; and calling free_all as the idiomatic alternative. This is that
|
|
;;;; alternative, and it needs nothing from the language that was not already
|
|
;;;; there.
|
|
;;;;
|
|
;;;; ── The reader moved, and what is left here ──────────────────────────
|
|
;;;;
|
|
;;;; read-value used to be written out in this file. It is vendor/edn's now, as
|
|
;;;; (edn/read bytes), and what this program keeps is the half that was always
|
|
;;;; the demonstration: the walk back over a document nobody declared a type
|
|
;;;; for, and the one free-all that ends it. The strings are the package's own
|
|
;;;; copies in the region now rather than views into `doc`, which is why there
|
|
;;;; is nothing left here saying which parts of the value outlive the release.
|
|
|
|
(import edn "vendor:edn")
|
|
|
|
(defvar frame Allocator)
|
|
|
|
(defn count-leaves [v edn/Value] i32
|
|
(match v
|
|
(List items)
|
|
(let [n 0]
|
|
(dotimes [i (len items)]
|
|
(set n (+ n (count-leaves (at items i)))))
|
|
n)
|
|
(Set items)
|
|
(let [n 0]
|
|
(dotimes [i (len items)]
|
|
(set n (+ n (count-leaves (at items i)))))
|
|
n)
|
|
;; map-next fills an out-parameter with a copy of the value's bytes,
|
|
;; which for a Value holding a container is a second header over the same
|
|
;; block. In a region that is an alias and not a second owner — nothing
|
|
;; here owns anything, the arena does — so walking a map is the ordinary
|
|
;; iteration and needs no accessor of its own.
|
|
(Table entries)
|
|
(let [n 0
|
|
cur (i64 0)
|
|
k ""
|
|
v edn/Value.Nil]
|
|
(while (map-next entries (addr cur) (addr k) (addr v))
|
|
(set n (+ n (count-leaves v))))
|
|
n)
|
|
_ 1))
|
|
|
|
(defn sum-ints [v edn/Value] i64
|
|
(match v
|
|
(Int n) n
|
|
(List items)
|
|
(let [t (i64 0)]
|
|
(dotimes [i (len items)]
|
|
(set t (+ t (sum-ints (at items i)))))
|
|
t)
|
|
(Table entries)
|
|
(match (get entries "xs") (Some x) (sum-ints x) None (i64 0))
|
|
_ (i64 0)))
|
|
|
|
(defn describe [v edn/Value] string
|
|
(match v
|
|
Nil "nil" (Bool _b) "bool" (Int _n) "int" (Float _x) "float"
|
|
(Text _s) "string" (Key _s) "keyword" (List _i) "vector"
|
|
(Set _i) "set" (Table _e) "map"))
|
|
|
|
(defconst doc
|
|
"{:name \"level-1\"
|
|
:xs [1 2 3]
|
|
:spawns [{:kind :grunt :at [10 20]}
|
|
{:kind :boss :at [30 40]}]
|
|
:gravity 9.8
|
|
:looping true}")
|
|
|
|
(defn main [] i32
|
|
(set frame (arena-new 65536))
|
|
(with-allocator frame
|
|
;; None is the malformed document, and it cannot happen for a literal that
|
|
;; is right here — but reading it back out of the Option is what makes the
|
|
;; refusal visible at the call site instead of arriving as a Nil that looks
|
|
;; like data.
|
|
(match (edn/read (bytes doc))
|
|
(Some v)
|
|
(do
|
|
(println (describe v))
|
|
(println (count-leaves v))
|
|
(println (sum-ints v))
|
|
(match v
|
|
(Table entries)
|
|
(match (get entries "name")
|
|
(Some n) (println (describe n))
|
|
None (println "missing"))
|
|
_ (println "not a map")))
|
|
None (println "malformed")))
|
|
;; The whole document, in one operation and with no per-element teardown.
|
|
(free-all frame)
|
|
(arena-destroy frame)
|
|
0)
|