;;;; (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)