flan/test/programs/edn-read.flan
Joseph Ferano f35af50f51 or-else is Java's, not Rust's, and the None it is tested against is reached
Four corrections to the prose and one to the test, none to the design.

The provenance line said "the name is Rust's or_else, the behaviour is
Rust's unwrap_or", which conflates two functions that differ in both
eagerness and return type — Rust's or_else takes a closure and answers
another Option.  Java's Optional.orElse is the exact match, and its lazy
sibling orElseGet is the one already declined a paragraph above.

The helper reaching or-else's None branch at an owning type asserted a
refusal nobody had run.  Compiled, it is "nothing here says what None is
an Option of — annotate the function's return type or the binding", so
the comment quotes that and the helper is a return type and nothing
else: its other branch was never called, in a program whose header says
every line is a claim.

read-file's comment claimed both restarts arrive unchanged and the test
runs use-value.  Narrowed to the mechanism (nothing here establishes a
handler) plus the half that is actually executed.

And edn-read.flan now says what becomes of its defn wrapper when the
computed-initialiser work lands, since that is the only thing keeping
the motivating line from being written as the defvar.
2026-09-19 04:30:57 +07:00

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)