flan/vendor/edn/read.flan
Joseph Ferano 51df451e7b The map dedup is asserted by the case that can only pass if it works
#{{:a 1} {:a 1} {:a 2}} answers 2 whether tables=? compares anything or
compares nothing, so it was proving the count and not the compare. The
pair beside it isolates both halves: one map twice must collapse to 1,
and two maps of one entry each with different keys must stay 2, which is
what a size-only compare would get wrong.

And read's comment stops implying a property it does not have: empty
input answers (Some Value.Nil), indistinguishable from the document that
is nil. Empty is not malformed and the reader is not the thing that gets
to decide it is.
2026-09-18 23:03:56 +07:00

236 lines
11 KiB
Plaintext

;;;; The dynamic reader: an EDN document, and no type to read it into.
;;;;
;;;; `edn.flan` answers "what is the next token". This answers "what is in the
;;;; file", for a caller that has no struct to hand — a config file whose keys
;;;; are not known until it is read, a tileset, a save. `(read-edn Enemy bytes)`
;;;; is the other direction and is not here: it wants a compile-time walk over
;;;; a struct's fields, there is no run-time type information in this language,
;;;; and it is its own project (NEXT.md item 9).
;;;;
;;;; ── Where the storage comes from ─────────────────────────────────────
;;;;
;;;; `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) below takes the *context*, and the caller chooses
;;;; the tier by writing (with-allocator frame (edn/read bytes)). An explicit
;;;; allocator at a construction site overrides that, which is how this would
;;;; take one as a parameter if the idiom could not say it — and the idiom
;;;; says it, so there is no allocator parameter here and nothing lost.
;;;;
;;;; The tier has to be a region, 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. Calling `read` with the heap in the
;;;; context dies at the first collection in the document, naming the line.
;;;; See test/programs/arena-region.flan.
;;;;
;;;; There is no teardown in this file — no drop, no destructor, no recursive
;;;; free. One (free-all frame) releases the whole document, because every part
;;;; of it came out of the one region.
;;;;
;;;; ── A Value owns its strings, and a Token does not ───────────────────
;;;;
;;;; This is the one place the two layers of this package disagree, and it is
;;;; deliberate. A Token's text is a slice INTO the source buffer; a Value's
;;;; strings are copies, in the allocator, and the document does not point at
;;;; the source at all once `read` has returned.
;;;;
;;;; A view would be cheaper and would be a trap. `read` hands its answer back
;;;; out of the function that owns the buffer, which is exactly the case
;;;; edn.flan's lifetime contract says a view cannot survive: the caller frees
;;;; the bytes it slurped, or reads the next file into them, and every string
;;;; in the document is garbage with nothing to say so. A free-all on the arena
;;;; would not even take them, because they were never in it.
;;;;
;;;; Odin settles the same question the same way: core/encoding/json's parser
;;;; clones a string even when it holds no escapes (parser.odin:388), clones
;;;; keys (:254), and its destroy_value frees them (types.odin:96). A reader
;;;; whose result is self-contained is the only kind that can be a library.
;;;;
;;;; ── Sets are a Vec, and why they are not a Map ───────────────────────
;;;;
;;;; `Value.Set` holds a (Vec Value), deduplicated on insert by a structural
;;;; `value=?`. The obvious shape — a (Map Value bool) — does not typecheck and
;;;; cannot be made to: lib/types.ml `keyable` refuses a key type holding a Vec
;;;; or a Map, and Value holds both. Restricting set elements to the Values
;;;; that *are* keyable was the other way out and is worse: `#{[0 0] [1 0]}` is
;;;; legal EDN and is the exact shape this was built for, so the restriction
;;;; would refuse the motivating file to buy a faster insert.
;;;;
;;;; The cost is stated rather than hidden: insert is O(n) and building a set
;;;; of n elements is O(n²). For the file this was written for — 54 integer
;;;; pairs — that is 1458 comparisons, once, at load. A set large enough for
;;;; the quadratic to matter is one this shape is wrong for, and the reader
;;;; will know before this comment does.
(defdata Value
[(Nil [])
(Bool [b bool])
(Int [n i64])
(Float [x f64])
(Text [s string])
(Key [s string])
(List [items (Vec Value)])
(Set [items (Vec Value)])
(Table [entries (Map string Value)])])
;; ── Copying a token's text ──────────────────────────────────────────
;; The (Vec u8) is the copy; the string is a view of it, and the Vec header is
;; dropped here on purpose. Nothing individually owns a block in a region —
;; free-all owns all of them — so keeping the header around to free through
;; would be keeping a handle for an operation that never happens.
(defn copy-text [s [u8]] string
(let [b (vec-new u8)]
(append! (addr b) s)
(string (as-slice b))))
;; ── Structural equality ─────────────────────────────────────────────
;; What the set's dedup is written against. Recursive, because a set element
;; may be a vector or a map or another set, and `=` on a Value would compare a
;; Vec header against a Vec header — two copies of one document would never be
;; equal and two aliases of one block always would.
(defn value=? [a Value b Value] bool
(match a
Nil (match b Nil true _ false)
;; `=` on two bools is refused by the language (plan.org, Types), so the
;; comparison is written as the thing it means.
(Bool x) (match b (Bool y) (if x y (not y)) _ false)
(Int x) (match b (Int y) (= x y) _ false)
(Float x) (match b (Float y) (= x y) _ false)
;; Text and Key are compared by their bytes and never to each other:
;; "a" and :a are two values in EDN and stay two here.
(Text x) (match b (Text y) (bytes=? (bytes x) (bytes y)) _ false)
(Key x) (match b (Key y) (bytes=? (bytes x) (bytes y)) _ false)
(List xs) (match b (List ys) (items=? xs ys) _ false)
(Set xs) (match b (Set ys) (sets=? xs ys) _ false)
(Table e) (match b (Table f) (tables=? e f) _ false)))
;; A vector is equal element by element, in order.
(defn items=? [xs (Vec Value) ys (Vec Value)] bool
(when (!= (len xs) (len ys))
(return false))
(dotimes [i (len xs)]
(when (not (value=? (at xs i) (at ys i)))
(return false)))
true)
;; A set is not. #{1 2} and #{2 1} are one value written two ways, and a
;; positional compare would make `#{#{1 2} #{2 1}}` a two-element set — which
;; is the case that decides whether this function is worth having separately
;; from items=?.
(defn sets=? [xs (Vec Value) ys (Vec Value)] bool
(when (!= (len xs) (len ys))
(return false))
(dotimes [i (len xs)]
(when (not (member? ys (at xs i)))
(return false)))
true)
(defn member? [xs (Vec Value) v Value] bool
(dotimes [i (len xs)]
(when (value=? (at xs i) v)
(return true)))
false)
;; Maps compare by size and then by lookup, which is what makes the walk
;; order-independent — two maps built by inserting the same pairs in different
;; orders iterate differently and are the same map.
(defn tables=? [a (Map string Value) b (Map string Value)] bool
(when (!= (len a) (len b))
(return false))
(let [cur (i64 0)
k ""
v Value.Nil]
(while (map-next! a (addr cur) (addr k) (addr v))
(match (get b k)
(Some w) (when (not (value=? v w)) (return false))
None (return false))))
true)
;; ── Reading ─────────────────────────────────────────────────────────
;; One token in hand, and the cursor for whatever that token opens. Public
;; because it is the entry point for a caller who wants the error *position*:
;; a caller driving its own Cursor can ask (edn/error-pos c) afterwards, and
;; `read` below cannot, because the cursor it made is gone.
(defn read-value [c (Ptr Cursor) t Token] Value
(cond
(= (.kind t) tok-bool)
(Value.Bool {.b (match (bool-of t) (Some v) v None false)})
(= (.kind t) tok-int)
(Value.Int {.n (match (int-of t) (Some v) v None (i64 0))})
(= (.kind t) tok-float)
(Value.Float {.x (match (float-of t) (Some v) v None 0.0)})
(= (.kind t) tok-string) (Value.Text {.s (copy-text (.text t))})
(= (.kind t) tok-keyword) (Value.Key {.s (copy-text (.text t))})
;; A symbol becomes a Key. There is no Symbol case, because nothing that
;; reads a document this way tells the two apart — and a case nobody can
;; act on differently is a case that only makes matches longer.
(= (.kind t) tok-symbol) (Value.Key {.s (copy-text (.text t))})
(= (.kind t) tok-vec-open)
(let [items (vec-new Value)
u (next c)]
(while (and (ok? c)
(!= (.kind u) tok-vec-close)
(!= (.kind u) tok-eof))
(push items (read-value c u))
(set u (next c)))
(Value.List {.items items}))
;; A set ends on tok-map-close, because `}` is the byte that ends it. The
;; dedup is here and not at the end: a set with a duplicate in it never
;; exists, so nothing downstream has to know that one might.
(= (.kind t) tok-set-open)
(let [items (vec-new Value)
u (next c)]
(while (and (ok? c)
(!= (.kind u) tok-map-close)
(!= (.kind u) tok-eof))
(let [v (read-value c u)]
(when (not (member? items v))
(push items v)))
(set u (next c)))
(Value.Set {.items items}))
;; A map's key is whatever token is there, and its text is the key —
;; copied, so :a and "a" collide as keys here where EDN keeps them apart.
;; That is a real narrowing and it is the price of a (Map string Value):
;; the alternative is a (Map Value Value), which `keyable` refuses for the
;; reason the set's comment above gives.
(= (.kind t) tok-map-open)
(let [entries (map-new string Value)
k (next c)]
(while (and (ok? c)
(!= (.kind k) tok-map-close)
(!= (.kind k) tok-eof))
(let [v (next c)]
(put entries (copy-text (.text k)) (read-value c v)))
(set k (next c)))
(Value.Table {.entries entries}))
:else Value.Nil))
;; The whole document, from a byte slice, in the calling convention's
;; allocator.
;;
;; (Option Value) and not Value, which is the one place this departs from
;; edn.flan's "errors live on the cursor, not in the return type". The cursor
;; is made inside this function and dies with it, so there is nothing left for
;; a caller to ask — and a Value.Nil answer would be indistinguishable from the
;; document that is literally `nil`, which is the class of quiet wrongness the
;; package's refusals exist to avoid. A caller who needs the byte offset builds
;; the Cursor itself and calls read-value; that is the three lines below.
;;
;; One collision the Option does NOT resolve, said here rather than discovered:
;; empty input answers (Some Value.Nil), the same as the document `nil`. Empty
;; is not malformed — a tokenizer over no bytes reports no error, correctly —
;; and the alternative is this function deciding that an empty file is a
;; failure, which is the caller's question and not the reader's.
(defn read [src [u8]] (Option Value)
(let [c (cursor src)
t (next (addr c))
v (read-value (addr c) t)]
(if (ok? (addr c)) (Some v) None)))