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.
43 lines
2.1 KiB
Plaintext
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)
|