flan/test/programs/init-conditions.flan
Joseph Ferano 9ce51ba94e One slice over everything with elements, and the warning at the push
as-slice was a warning, not an operation. The input type already decides
which of the two things happens — a Vec can only be borrowed, an array or a
string can only be viewed, and no call site picks between them — so the second
name expressed no choice a reader could make. And it warned at the moment the
view is taken, which is the one moment nothing is wrong; the danger arrives
later, at the push. slice now takes a Vec at all three arities and as-slice
is gone.

(slice v lo) was free, and is the arity the Vec never had: the runtime already
reads a hi of -1 as "to the end", so the tail form passes the caller's lo and
the same -1 — no slot, no length read, no second evaluation. The merge is
entirely in the checker; the Vec path builds the flan_vec_as_slice call it
always built and neither backend has a line about any of it.

A Vec a call returned is refused at every arity, and not for the array's
reason. (slice (mk)) over an array dangles. (slice (make-vec)) does not — the
storage outlives the expression — but the header is a temporary, so nothing
can ever free the block. The refusal says that and names the let.

The name's own refusal sits in ordinary_call after every table, so a program
that defines an as-slice still reaches its own. It reads for somebody who has
never heard of the old name and writes the call back out, spelling each
argument that is a name or a number.

The warning moved to where it bites: BUILT.md gains a section beside the Vec
table and the push row points at it, spec-memory.md's Borrowing says the same.
Investigated and deliberately not built — a diagnostic for a live view at the
push. (reserve v 100) then a slice, a push and a read is correct code under
the contract the spec chose, so any flag on it is a false positive by the
language's own semantics rather than by an approximation. FIX.org has the
finding and the syntactic sketch that does not work.
2026-09-21 09:51:35 +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 (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)