flan/test/programs/edn-read.flan
Joseph Ferano 2e203f64b8 bytes copies, bytes-view aliases, and a dev-session segfault parks
The INSERTIONSORT crash, all three rulings (FIX.org 2026-09-20):

- (bytes s) allocates a writable copy through the allocator surface —
  context or (bytes s a), StorageExhausted with retry, a registry note in
  dev builds (flan_bytes_dup, lowered like vec-new). (bytes-view s) is the
  old zero-cost reinterpret, renamed, read-only by convention; every
  in-repo reader swept over to it. (string b) unchanged.
- String constants were already read-only on both backends at -O0; now
  pinned — bytes-copy.flan rows on LLVM/-O0/--x86, and dies_segv rows
  asserting the write-through-view trap on both backends.
- A dev build installs a SIGSEGV/SIGBUS handler by the same dev-only
  constructor slot that arms the registry: one line naming the address and
  the innermost frame, then the trap-hook park — stopped, not dead, the
  daemon serving. No agent: message and re-raise. Release builds untouched.
  Pinned by trap_park over dev-segv.flan.
2026-09-20 23:12:42 +07:00

151 lines
6.8 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-view 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-view 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-view "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-view "{: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)