Three notes, all of them about a comment that was true as far as it went. The cost sentence on defer_slot said one i64 and one compare on a path that is already unwinding, which is the unwind's share and not the whole bill: every function with a defer also pays a store of zero at entry and a store of an ordinal at each defer, on the ordinary path, whether anything transfers or not. Small, correct, and now written down as what it is. init-conditions.flan is in test_valgrind.ml, and only the game-data half earns that row -- it is the one that reaches a real (free src) over a slot slurp transferred out of. The note half is output-only: revert the fix and its trace changes, but a wrong integer in a global is not a memory error and memcheck stays quiet. A later edit trimming the edn dependency would leave the row green and blind, so the header says so. And register_defer saves in_defer rather than clearing it. Unreachable today, because defer_ok is false inside a defer and nothing can nest one; written so that the flag comes back rather than being dropped on the day that changes.
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])
|
|
|
|
(defvar 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.
|
|
(defvar 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.
|
|
|
|
(defvar fired i32
|
|
(handler-case (both 4)
|
|
[(Missing [c] (+ 100 (.id c)))]))
|
|
|
|
(defvar quiet i32
|
|
(handler-case (+ 1 40)
|
|
[(Missing [c] -1)]))
|
|
|
|
(defvar bound i32
|
|
(handler-bind [(Late [c] (set late 5))]
|
|
(do (signal (Late {.id 1}))
|
|
7)))
|
|
|
|
(defvar 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.
|
|
(defvar 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)
|