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.
152 lines
5.7 KiB
Plaintext
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)
|