flan/test/programs/registry.flan
Joseph Ferano c897526e47 Following a pointer was never a type question; it was a permission question
(Ptr Enemy) already says Enemy, at compile time, in the walk. What the renderer
lacked was any way to know whether the storage at the far end is still there —
and an allocation registry is exactly a record of which addresses it is still
true to read. So the inspector follows a live one and renders the pointee by the
same walk as anything else, and names what died at a dead one.

println does not, and the split is not squeamishness: spec-memory.md fixes what
a printed Ptr prints, a printed line belongs to the program and has to read the
same in a release build, and a release build has no registry to ask. The two
callers already differ in an emitter record; they differ in one more.

No address appears in the text. An address is not stable across two runs, so
printing one would make a rendering depend on where the heap landed — the rule
Render already follows for an allocator. What a reader wants from a dangling
pointer is what died.

registry.flan is one program read twice: a dev build answers for an address at
the heap, arena and pool tiers, and a release build answers 0 to all of it. The
arena row is the free-all Valgrind cannot see — this does not make memcheck
report it, it makes the same read answerable.
2026-09-13 09:17:47 +07:00

67 lines
2.9 KiB
Plaintext

;;;; The allocation registry — NEXT.md, "a dev-build allocation registry".
;;;;
;;;; A Flan struct is exactly its C layout with no header and no tag word, so
;;;; nothing at run time can say what is at an address. The registry sidesteps
;;;; that: the allocator's *caller* knew the type, and a dev build writes it
;;;; down. What is asserted here is the consequence a program can see without
;;;; an inspector — whether an address is still live — and the three ways
;;;; storage dies underneath one.
;;;;
;;;; This program is deliberately readable in a release build too, and prints
;;;; a different and equally correct answer there: nothing is recorded, so
;;;; every question about an address comes back 0. The two expectations sit
;;;; side by side in the acceptance table, which is the honest way to assert
;;;; "a release build carries none of it".
(declare-c reg-on [] i32 "flan_dev_reg_enabled")
(declare-c reg-live [p (Ptr i32)] i32 "flan_dev_reg_live")
(declare-c reg-count [live i32] i64 "flan_dev_reg_count")
(defvar frame Allocator)
(defn main [] i32
;; Armed by a constructor in a dev build and never in a release one.
(println (reg-on))
;; 1. The heap tier. A pointer into a Vec's storage is live while the Vec is,
;; and the free that releases it is seen — which is the whole of "use
;; after free that names what died", minus the naming, which needs the
;; inspector to read it back.
(let [v (vec-new i32)]
(push v 7)
(push v 8)
(let [p (addr (at v 1))]
(println (reg-live p)) ; dev: 1
(free v)
(println (reg-live p)))) ; 0 either way
;; 2. The arena tier, and the hole test_valgrind.ml measures. free-all is
;; retain-capacity: the pages stay mapped and the bytes stay readable, so
;; memcheck is never told anything died and a later read of stale bytes
;; goes unnoticed. This does not tell memcheck. It tells the registry, so
;; that the same read is at least *answerable*.
(set frame (arena-new 4096))
(let [w (vec-new i32 frame)]
(push w 3)
(let [q (addr (at w 0))]
(println (reg-live q)) ; dev: 1
(free-all frame)
(println (reg-live q)))) ; 0 either way
;; 3. And the pool, whose storage is the one place a (Ptr T) is handed to a
;; program by name: (resolve p h) points into the middle of the items
;; array, never at its base. Nothing but a containment lookup can answer
;; for it.
(let [pool (pool-new i32)]
(let [h (insert pool 5)]
(match (resolve pool h)
(Some ip) (println (reg-live ip)) ; dev: 1
None (println -1))
(free pool)))
;; Nothing is live by now except whatever the arena's own destroy leaves, so
;; the count is a statement about the table rather than about one address.
(arena-destroy frame)
(println (reg-count 1)) ; 0 either way
0)