The dyn runtime gets a map object and an interned keyword, alongside the
vec it already had. {:a 1 :b s} is a map literal wherever a struct
literal isn't — the parser tells the two apart by whether the first form
in the braces is a .field symbol — and a bracket literal builds the
runtime's own vec rather than a typed array wherever a dyn is wanted, which
is what lets a map literal's values nest arrays and maps freely. get, put,
len and has-key? all learn a dyn-map arm alongside the typed-map one they
already had, and (keyword s) builds the same interned value a :foo literal
does, for a name that only exists at run time. nil is now a literal, the
dyn absence value that get answers for a key a map does not hold.
On the runtime side, flan_dyn.c gets an OBJ_MAP that shares the vec's
storage arm and doubles its accounting, a linear-scan intern table for
keywords that makes equality an identity compare, and structural map
equality by lookup rather than position. The marker traces a map's
interleaved keys and values the same way it already traced a vec.
edn/read and its callers move off the old (Option Value) union entirely:
a document is plain dyn now, sets are dyn maps to true, and arena-edn.flan
is retired along with the union it demonstrated. The acceptance suite's
edn-read and json rows were recaptured against the new shape, and a new
dyn-map.flan program exercises the map and keyword operations end to end,
including a 200k-iteration churn loop against a rooted map that runs
GC for real, across the LLVM, -O0 and x86 rows, and under the sanitizer.
Keywords are dyn everywhere an enum isn't expected, which changed what a
couple of existing checker tests actually see refused; both were updated
to the sentence the checker gives now rather than the one it used to.
151 lines
6.7 KiB
Plaintext
151 lines
6.7 KiB
Plaintext
;;;; (edn/read bytes) over the file it was built for, plus the properties that
|
|
;;;; are only claims until something runs them.
|
|
;;;;
|
|
;;;; assets/edn/tileset.edn is the real thing, copied out of the editor that
|
|
;;;; writes it: a map of :texture-path to a string and :selected-cells to a set
|
|
;;;; of 54 integer pairs. Nothing here declares a type for it — the point.
|
|
;;;;
|
|
;;;; It is in a subdirectory of assets/ rather than in it, because programs/
|
|
;;;; embed.flan holds (embed-dir "assets") in a [3 EmbedFile] and a fourth file
|
|
;;;; beside the other three is a type error in a program that has nothing to do
|
|
;;;; with this one. embed-dir does not descend, which is what makes a
|
|
;;;; subdirectory the answer rather than a second assets directory.
|
|
;;;;
|
|
;;;; The document is plain dyn now — maps, vecs, keywords, texts — where it
|
|
;;;; used to be the edn/Value tagged union. What the union's hand-written
|
|
;;;; equality bought, the runtime's structural equality answers:
|
|
;;;;
|
|
;;;; * a set holds each value once, by *structure*. #{[0 0] [0 0]} is one
|
|
;;;; element. A set reads as a dyn map from element to true, and the dedup
|
|
;;;; is put's own key-replace, so a set with a duplicate never exists.
|
|
;;;;
|
|
;;;; * the document owns its strings. The buffer a document was read from is
|
|
;;;; overwritten byte by byte afterwards, and the string read out of the
|
|
;;;; document still prints what was in the file — the box copied.
|
|
;;;;
|
|
;;;; * (edn/read-file path) frees its buffer inside the call, safe because
|
|
;;;; of the property above, and passes a FileError through untouched with
|
|
;;;; both restarts still armed.
|
|
;;;;
|
|
;;;; What dyn cannot say and the old (Option Value) could: `read` answers nil
|
|
;;;; both for malformed input and for the document `nil`. The distinction did
|
|
;;;; not vanish — it moved to the cursor, where the error position has always
|
|
;;;; lived, and `malformed?` below is the three lines it costs.
|
|
|
|
(import edn "vendor:edn")
|
|
|
|
;; The document, read at compile time. An embed is bytes in the binary, so
|
|
;; there is no file open here and no path to get wrong at run time.
|
|
(defconst tileset (embed "assets/edn/tileset.edn"))
|
|
|
|
(defn show-tileset [] ()
|
|
(let [doc (edn/read tileset)
|
|
cells (get doc :selected-cells)]
|
|
(println (get doc :texture-path))
|
|
(println (len cells))
|
|
;; Two cells that are in the file and one that is not. A reader that
|
|
;; flattened the pairs into 108 integers would still have the right count
|
|
;; of *something*, and would miss all of these; [3 4] answering yes where
|
|
;; [9 9] answers no is what says the pair compare is positional.
|
|
(println (has-key? cells [4 3]))
|
|
(println (has-key? cells [0 0]))
|
|
(println (has-key? cells [3 4]))
|
|
(println (has-key? cells [9 9]))))
|
|
|
|
;; The size of a set after the dedup, which is the whole of what the dedup can
|
|
;; be asked for.
|
|
(defn set-size [src string] ()
|
|
(println (len (edn/read (bytes src)))))
|
|
|
|
;; Malformed input, told apart from the document `nil` by the cursor — the
|
|
;; return value alone cannot say it, and this is the spelling that can.
|
|
(defn malformed? [src string] bool
|
|
(let [c (edn/cursor (bytes src))
|
|
t (edn/next (addr c))
|
|
v (edn/read-value (addr c) t)] ; the value is not the question here
|
|
(not (edn/ok? (addr c)))))
|
|
|
|
;; A document in a buffer this program owns and can write to. (bytes "literal")
|
|
;; is not that — a literal is constant data behind a writable-looking slice —
|
|
;; so the source is built with append and the write goes through as-slice.
|
|
(defn survives-its-buffer [] ()
|
|
(let [buf (vec-new u8)]
|
|
(append (addr buf) (bytes "{:name \"level-1\" :xs [1 2]}"))
|
|
(let [src (as-slice buf)
|
|
v (edn/read src)]
|
|
(dotimes [i (len src)]
|
|
(set (at src i) \x))
|
|
(println (get v :name)))))
|
|
|
|
;; The path-taking entry point over the same file the embed above holds. The
|
|
;; texture path printed here has to be the one show-tileset printed, which is
|
|
;; what says read-file read the file and not merely something — and that
|
|
;; freeing the buffer inside the call took none of the document with it. The
|
|
;; defvar is a dyn global, rooted once at startup, so the document lives past
|
|
;; the frame that read it.
|
|
(defvar game-data dyn)
|
|
|
|
(defn by-path [] ()
|
|
(set game-data (edn/read-file "programs/assets/edn/tileset.edn"))
|
|
(println (get game-data :texture-path)))
|
|
|
|
;; A missing file, answered from outside read-file. Nothing in the package
|
|
;; handles FileError, so the condition walks past it to here with both restarts
|
|
;; armed, and `use-value` names a path read-file then slurps instead — the read
|
|
;; resumes as if that file had been asked for all along, which is exactly what
|
|
;; slurp.flan asserts for slurp alone.
|
|
;;
|
|
;; The map check is the assertion: the answer is a real document rather than
|
|
;; the nil a swallowed error would have to become, so the restart was taken.
|
|
(defvar saw-file-error i64)
|
|
|
|
(defn by-missing-path [] ()
|
|
(handler-bind
|
|
[(FileError [c]
|
|
(set saw-file-error (+ saw-file-error 1))
|
|
(invoke-restart 'use-value "programs/assets/edn/tileset.edn"))]
|
|
(println (has-key? (edn/read-file "programs/assets/edn/not-here.edn")
|
|
:texture-path)))
|
|
(println saw-file-error))
|
|
|
|
(defn main [] i32
|
|
(show-tileset)
|
|
(println "")
|
|
|
|
;; Dedup, on each kind of element a set can hold. The nested pair is the
|
|
;; one a structural compare is needed for; the nested *set* is the one
|
|
;; that also needs the compare to ignore order, or #{1 2} and #{2 1}
|
|
;; would be two elements — as maps-to-true they are one map, compared by
|
|
;; lookup and not by position.
|
|
(set-size "#{}")
|
|
(set-size "#{1 1 2}")
|
|
(set-size "#{[0 0] [0 0] [0 1]}")
|
|
(set-size "#{\"a\" \"a\" :a :a}")
|
|
(set-size "#{#{1 2} #{2 1}}")
|
|
;; Three map cases and not one, because #{{:a 1} {:a 1} {:a 2}} answers 2
|
|
;; whether the map compare works or does nothing at all — two merge and one
|
|
;; does not, or none merge and there were only ever three. The pair below
|
|
;; isolates it: the first must be 1, and the second must be 2 on the
|
|
;; *keys*, which a size compare alone would get wrong.
|
|
(set-size "#{{:a 1} {:a 1}}")
|
|
(set-size "#{{:a 1} {:b 1}}")
|
|
(set-size "#{{:a 1} {:a 1} {:a 2}}")
|
|
;; One, and the old Value answered two. Dyn equality is the language's own,
|
|
;; and it says (= 1 1.0) the way the operators promote — so under a
|
|
;; maps-to-true set, 1 and 1.0 are one key. A narrowing against EDN's
|
|
;; letter, taken with open eyes: the alternative was a second equality kept
|
|
;; beside the runtime's, which is the duplication this rewrite retired.
|
|
(set-size "#{1 1.0}")
|
|
(set-size "#{true false true}")
|
|
(println "")
|
|
|
|
(survives-its-buffer)
|
|
;; And the refusal, told apart from the document that is literally nil.
|
|
(println (malformed? "#{1 2"))
|
|
(println (malformed? "nil"))
|
|
(println "")
|
|
|
|
(by-path)
|
|
(by-missing-path)
|
|
0)
|