flan/test/programs/two-numbers.flan
Joseph Ferano 81b807f544 A rendered number goes in the caller's frame, not in one buffer for the process
Every number-to-text conversion wrote into one file-static in the runtime and
answered a slice over it, and nothing copied. Two of them in one expression
printed the second number twice — no crash, no diagnostic, and nothing a
sanitizer could find, because every byte read was inside an object that was
alive. The wrong object.

The buffer is now the caller's, one frame slot per call site. The slot is
allocated in the checker rather than in either backend: a slot is a
function-lifetime location in both of them, where an x86 backend temporary is
bump-allocated and reclaimed at the end of the expression that made it — which
is the one lifetime a returned slice must outlive. Each backend gains one
pointer argument and no reasoning of its own, which is what keeps them
symmetric.

The static is gone rather than left unused, since a buffer with nothing but a
comment beside it is a loaded gun. What remains is the ordinary lifetime a
pointer into a frame has: storing one of these slices in a container that
outlives the frame, or returning it, is still a copy the caller has to make.
NEXT.md's sharp edge now says that instead of what it used to say.
2026-09-17 22:55:30 +07:00

43 lines
2.1 KiB
Plaintext

;;;; Two rendered numbers, held at once.
;;;;
;;;; i64->bytes and its two siblings render into a buffer and answer a slice
;;;; over it. That buffer used to be one file-static in the runtime, shared by
;;;; every call in the process, so the program below printed "22 22": the
;;;; second conversion overwrote the first, and the first slice — still a
;;;; perfectly valid pointer into a perfectly live buffer — was read after it.
;;;; No crash, no diagnostic, and nothing for a sanitizer to catch, because
;;;; every byte read was inside an object that was alive. The wrong bytes.
;;;;
;;;; The buffer is the caller's now, one frame slot per call site, which is why
;;;; the two conversions below do not collide and why the f64 held across an
;;;; i64 conversion — a different shim, and the same buffer before — survives
;;;; it. What the slice still does not outlive is its frame: storing one in a
;;;; container that lives longer, or returning it, hands back a view of storage
;;;; that has been reused. That is copying's job and is said in check.ml.
(defn main [] i32
;; Two i64 conversions alive at the same time.
(let [a (string (i64->bytes 11))
b (string (i64->bytes 22))]
(print a) (print " ") (println b)) ; 11 22
;; Three, and read in the order they were made rather than in reverse, so a
;; version that rotated among two buffers would still be caught.
(let [a (string (i64->bytes 1))
b (string (i64->bytes 2))
c (string (i64->bytes 3))]
(print a) (print b) (println c)) ; 123
;; Across the two shims: the f64's text is made first and read last.
(let [x (string (f64->bytes 2.5))
n (string (i64->bytes 7))]
(print x) (print " ") (println n)) ; 2.5 7
;; Inside a loop, where the slot is reused per iteration: each turn's text is
;; read before the next turn writes it, which is the contract a frame slot
;; gives. Printed on one line so the loop's shape is visible in the output.
(dotimes [i 4]
(let [s (string (i64->bytes (i64 (* i 11))))]
(print s) (print " ")))
(println "") ; 0 11 22 33
0)