flan/test/programs/init-conditions.flan
Joseph Ferano a4c6b996ff def re-runs its initialiser, and defvar is renamed defonce
The trio the author decided on 2026-09-20 is now all built: def is CL's
defparameter — its initialiser runs on every daemon re-run, unguarded, so
an edited initialiser repaints the same storage on C-c C-c plus re-run —
defonce (Clojure's name for CL's defvar, per the author) initialises once
behind the .init~once. flag, and defconst stays the image.

One parse arm reads both forms; the difference is Ast.reinit, carried to
Tast.global's grerun. Emit.startup_plan gives a def no guard flag, and
Check.check_global lifts every def initialiser — zero and literal
included — into global/<n>, so the host's startup reaches it through the
function cell and a re-evaluated def swaps it (Session's def_inits;
Emit.redefinition declares the cell for a non-sibling target). The old
defvar spelling is refused with the rename and both compiling spellings,
and every program, test, doc and editor list is swept — except sand.flan,
the author's live WIP, whose seven defvar lines are flagged in FIX.org
and keep its three dependent tests red on this branch.
2026-09-21 07:12:04 +07:00

152 lines
5.7 KiB
Plaintext

;;;; Conditions where the program has not started yet, and the defer that has
;;;; not registered yet.
;;;;
;;;; The report that opened this file said handler-case segfaults in a global
;;;; initialiser. It does not, and the top half here is what says so: all three
;;;; condition forms run in that position, before main, and answer the same
;;;; numbers they would answer anywhere. §5 of spec-conditions.md is not
;;;; suspended at startup, because an initialiser is a call from main and not a
;;;; constructor the loader runs.
;;;;
;;;; What did crash was underneath, and had nothing to do with startup:
;;;;
;;;; (defn read-file [path string] dyn
;;;; (let [src (slurp path (heap-allocator))]
;;;; (defer (free src))
;;;; (read (as-slice src))))
;;;;
;;;; `slurp` signals FileError and the handler further out unwinds. The
;;;; transfer leaves this frame through its defers — and `src` was never
;;;; written, because the form that would have written it is the one that
;;;; transferred. `free` then read whatever the stack held under that slot,
;;;; which at -O0 in a small program is zero and at -O2 is a pointer.
;;;;
;;;; A [return] never had this: the checker splices the defers registered
;;;; *above* it and no others, and says so. The transfer exit took the whole
;;;; list. The bottom half of this file is that distinction, pinned from both
;;;; sides — the defer below the signal must not run, the one above it must.
;;;;
;;;; [log] is a digit trace and not a running sum, for handler-case.flan's
;;;; reason: a sum commutes and would score a wrong order right.
;;;;
;;;; DO NOT DROP THE edn IMPORT. This program is in test_valgrind.ml, and only
;;;; one half of it earns that row: [game-data] below, whose initialiser goes
;;;; through edn/read-file and so through the real (defer (free src)) over a
;;;; binding slurp transferred out of. That is the free of an uninitialised
;;;; slot, and memcheck names it. The [note] half is output-only — revert the
;;;; fix and its printed trace changes, but no memcheck error is produced,
;;;; because a wrong integer written to a global is not a memory error. So a
;;;; later edit that trims the edn dependency out of here leaves the valgrind
;;;; row green and no longer looking at anything. Move it to a program that
;;;; still frees, or take the row out honestly.
(import edn "vendor:edn")
(defstruct Missing [id i32])
(defstruct Late [id i32])
(defonce log i64)
;;; A counter of its own, because two computed globals that both wrote [log]
;;; would be asserting the order the initialiser sort happened to pick between
;;; two that do not depend on each other — and that order is not this file's
;;; subject.
(defonce late i64)
(defn note [n i64] () (set log (+ (* log 10) n)))
(defn raise [n i32] i32
(error (Missing {.id n}))
0)
;;; A defer written *below* the form that transfers. Nothing reached it when
;;; the unwind starts, so it must not run: there is no v for it to run on, and
;;; in the shape this was found in the cleanup was a free.
(defn unreached [n i32] i32
(let [v (raise n)]
(defer (note 1))
(+ v 1)))
;;; And one written above it, which did register. It must still run — a fix
;;; that dropped this one would turn the crash into a leak, which is the same
;;; bug wearing a quieter coat.
(defn reached [n i32] i32
(defer (note 2))
(let [v (raise n)]
(+ v 1)))
;;; Both at once, so the trace says which of the two ran rather than how many.
(defn both [n i32] i32
(defer (note 2))
(let [v (raise n)]
(defer (note 1))
(+ v 1)))
;;; And the ordinary case, where nothing transfers: every defer registers and
;;; every one runs, innermost first.
(defn no-signal [n i32] i32
(defer (note 2))
(let [v (+ n 1)]
(defer (note 1))
(+ v 1)))
;;; ── The three forms, in a global initialiser ──────────────────────
;;;
;;; Each of these runs before main. A handler-case whose condition fires, one
;;; whose body completes and whose clause therefore never runs, a handler-bind
;;; that returns normally and lets signal carry on, and a restart-case with a
;;; handler-bind inside it — which is slurp's own shape and the reason the
;;; checker lets a condition form stand here at all.
(defonce fired i32
(handler-case (both 4)
[(Missing [c] (+ 100 (.id c)))]))
(defonce quiet i32
(handler-case (+ 1 40)
[(Missing [c] -1)]))
(defonce bound i32
(handler-bind [(Late [c] (set late 5))]
(do (signal (Late {.id 1}))
7)))
(defonce restarted i32
(restart-case
(handler-bind [(Missing [c] (invoke-restart 'use-zero 9))]
(raise 3))
(use-zero [k i32] k)))
;;; The author's own form, which is what sent anyone looking: a dyn global read
;;; out of a file that is not there, with the FileError answered by nil. The
;;; path is never present in a build directory, so the condition always fires.
(defonce game-data dyn
(handler-case (edn/read-file "no-such-file-here.edn")
[(FileError [c] nil)]))
(defn main [] i32
;; The initialisers ran above; this is what they left.
(println fired)
(println quiet)
(println bound)
(println restarted)
(println game-data)
(println late)
;; The 2 is `both`'s registered defer. The 1 is the one that never
;; registered, and its absence here is the whole fix.
(println log)
(set log 0)
;; The same three shapes from an ordinary function, where they always worked
;; and must go on working.
(println (handler-case (unreached 1) [(Missing [c] (+ 200 (.id c)))]))
(println log)
(set log 0)
(println (handler-case (reached 2) [(Missing [c] (+ 300 (.id c)))]))
(println log)
(set log 0)
(println (handler-case (both 3) [(Missing [c] (+ 400 (.id c)))]))
(println log)
(set log 0)
(println (no-signal 5))
(println log)
0)