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.
193 lines
8.3 KiB
Plaintext
193 lines
8.3 KiB
Plaintext
;;;; (edn/read bytes) over the file it was built for, plus the two 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 hand-written struct reader in programs/edn.flan is the other route and
|
|
;;;; needs a defstruct per file; this one needs nothing and reads a file whose
|
|
;;;; keys it has never heard of.
|
|
;;;;
|
|
;;;; The two properties:
|
|
;;;;
|
|
;;;; * a set holds each value once, by *structure*. #{[0 0] [0 0]} is one
|
|
;;;; element, and a dedup written with `=` would make it two — two Vec
|
|
;;;; headers over two blocks are never the same header.
|
|
;;;;
|
|
;;;; * (edn/read-file path) is the same read with the buffer owned and freed
|
|
;;;; inside the call, which is only safe because of the property below it.
|
|
;;;; Two cases: the real file by path, whose answer has to equal the embed
|
|
;;;; above byte for byte, and a missing one, where slurp's FileError has to
|
|
;;;; arrive at a handler *outside* read-file with `use-value` still armed —
|
|
;;;; the pass-through decision, run rather than asserted.
|
|
;;;;
|
|
;;;; * a Value owns its strings. The last case reads a document out of a
|
|
;;;; buffer and then overwrites every byte of that buffer in place. A
|
|
;;;; reader holding views prints the overwriting bytes; one holding copies
|
|
;;;; prints what was in the file. The buffer is written rather than freed
|
|
;;;; because a freed buffer is a read of released memory, which can pass by
|
|
;;;; luck; overwriting it cannot.
|
|
|
|
(import edn "vendor:edn")
|
|
|
|
(defvar frame Allocator)
|
|
|
|
;; 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"))
|
|
|
|
;; [a b] as a Value, so a membership test can be written against a pair this
|
|
;; program made rather than one it found. Building a Value from outside the
|
|
;; package is the same two forms as building one inside it.
|
|
(defn pair [a i64 b i64] edn/Value
|
|
(let [items (vec-new edn/Value)]
|
|
(push items (edn/Value.Int {.n a}))
|
|
(push items (edn/Value.Int {.n b}))
|
|
(edn/Value.List {.items items})))
|
|
|
|
(defn field [v edn/Value k string] edn/Value
|
|
(match v
|
|
(Table entries) (match (get entries k) (Some x) x None edn/Value.Nil)
|
|
_ edn/Value.Nil))
|
|
|
|
(defn show-tileset [] ()
|
|
(match (edn/read tileset)
|
|
(Some doc)
|
|
(do
|
|
(match (field doc "texture-path")
|
|
(Text s) (println s)
|
|
_ (println "no texture path"))
|
|
(match (field doc "selected-cells")
|
|
(Set cells)
|
|
(do
|
|
(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 both of these.
|
|
(println (edn/member? cells (pair (i64 4) (i64 3))))
|
|
(println (edn/member? cells (pair (i64 0) (i64 0))))
|
|
(println (edn/member? cells (pair (i64 3) (i64 4))))
|
|
(println (edn/member? cells (pair (i64 9) (i64 9)))))
|
|
_ (println "no cells")))
|
|
None (println "malformed")))
|
|
|
|
;; 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] ()
|
|
(match (edn/read (bytes src))
|
|
(Some v) (match v (Set items) (println (len items)) _ (println "not a set"))
|
|
None (println "malformed")))
|
|
|
|
;; 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))
|
|
(match v
|
|
(Some doc)
|
|
(match (field doc "name")
|
|
(Text s) (println s)
|
|
_ (println "no name"))
|
|
None (println "malformed")))))
|
|
|
|
;; The path-taking entry point over the same file the embed above holds, and
|
|
;; the whole reason both of this session's additions exist. What a program
|
|
;; wants to write is one form:
|
|
;;
|
|
;; (defvar game-data edn/Value
|
|
;; (with-allocator frame
|
|
;; (or-else (edn/read-file "game-data.edn") edn/Value.Nil)))
|
|
;;
|
|
;; and it is written as a defn here because the *initialiser* is still refused
|
|
;; — "a global's value must be a compile-time constant — this one is computed"
|
|
;; — which is a separate piece of work on globals and nothing to do with
|
|
;; read-file or or-else. Everything inside the with-allocator is verbatim, so
|
|
;; the day a computed initialiser is allowed, load-game-data and the `set`
|
|
;; below it collapse back into the defvar above and this comment goes with
|
|
;; them.
|
|
;;
|
|
;; 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.
|
|
(defvar game-data edn/Value)
|
|
|
|
(defn load-game-data [] edn/Value
|
|
(or-else (edn/read-file "programs/assets/edn/tileset.edn") edn/Value.Nil))
|
|
|
|
(defn by-path [] ()
|
|
(set game-data (load-game-data))
|
|
(match (field game-data "texture-path")
|
|
(Text s) (println s)
|
|
_ (println "no 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.
|
|
;;
|
|
;; `some?` is the assertion: the answer is a real document, so the restart was
|
|
;; taken rather than the read quietly answering None. Distinguishing the two is
|
|
;; the reason read-file passes the condition through instead of folding a
|
|
;; missing file into the None that means "malformed".
|
|
(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 (some? (edn/read-file "programs/assets/edn/not-here.edn"))))
|
|
(println saw-file-error))
|
|
|
|
(defn main [] i32
|
|
(set frame (arena-new 262144))
|
|
(with-allocator frame
|
|
(do
|
|
(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.
|
|
(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 tables=? 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}}")
|
|
(set-size "#{1 1.0}") ; an int and a float are two values
|
|
(set-size "#{true false true}")
|
|
(println "")
|
|
|
|
(survives-its-buffer)
|
|
;; And the refusal, which has to be distinguishable from the document
|
|
;; that is literally nil.
|
|
(match (edn/read (bytes "#{1 2")) (Some _v) (println "read") None (println "malformed"))
|
|
(match (edn/read (bytes "nil")) (Some _v) (println "read") None (println "malformed"))
|
|
(println "")
|
|
|
|
(by-path)
|
|
(by-missing-path)))
|
|
(free-all frame)
|
|
(arena-destroy frame)
|
|
0)
|