flan/test/programs/bounds-condition.flan

191 lines
8.0 KiB
Plaintext

;;;; An index out of range is a condition, not the end of the process.
;;;;
;;;; Until now a bad index printed its source location and called exit(134).
;;;; That was defensible when `flan dev` was two processes; it is not now that
;;;; the compiler runs inside the program, because the trap takes the session
;;;; with it and the session is the thing the project is built around never
;;;; having to restart. And the route in is the most ordinary one there is: a
;;;; grid indexed from a mouse position is out of bounds the first time the
;;;; pointer leaves the window (PORTING.md, §3).
;;;;
;;;; So a failed bounds check signals BoundsError with `error`, the same way a
;;;; failed allocation signals StorageExhausted, and dies with the old message
;;;; only if nothing answered. This program is the "something answered" half —
;;;; the unhandled half is bounds.flan, which still exits 134 with the same
;;;; text it always did.
;;;;
;;;; **No restart is established at the failing index**, and that is the
;;;; decision worth reading this file for. StorageExhausted offers `retry`
;;;; because its attempt is repeatable: free something and the allocation
;;;; succeeds. Nothing a handler can do makes index 7 valid for a length-4
;;;; array. `use-value` for the index would cost every indexing operation a
;;;; restart frame and buy a *different element*, silently. What answers a bad
;;;; index is the restart the program already had — the frame loop's
;;;; `continue`, which is sand.flan's shape and is what a game wants: abandon
;;;; this frame, keep the window open.
;;;;
;;;; Four things are asserted, and the first two are the ones that matter:
;;;;
;;;; 1. The frame is abandoned and the program carries on. `frames` counts
;;;; the ones that finished and `skipped` the ones that did not.
;;;; 2. **Defers run.** A trap ran none, which BUILT.md recorded as following
;;;; from the noreturn-then-unreachable shape rather than as a decision.
;;;; The shape changed, so the question had to be answered rather than
;;;; inherited: an *answered* bounds failure leaves through the same
;;;; unwind path a `return` uses, and therefore runs the function's
;;;; defers, innermost first. An unanswered one still runs none, because
;;;; it is still a die inside C.
;;;; 3. The condition carries the numbers. `low` and `high` are the same
;;;; index for an `at` and the two ends of the range for a `slice`, which
;;;; is why there is one condition type and not two.
;;;; 4. Every route to a bad index signals: reading a fixed array, writing
;;;; one (a different lowering — place/Pindex, not At), a slice, a Vec
;;;; element, and a Vec's as-slice. A Vec's check lives inside the
;;;; runtime rather than in emitted IR, so those two are plumbed
;;;; separately and are the ones most likely to be left behind.
(defvar grid [4 i32])
;;; Handlers cannot see the locals of the function that established them —
;;; check.ml refuses a capture by name and says to use a global — so
;;; everything this program counts lives up here.
(defvar frames i64)
(defvar skipped i64)
(defvar cleaned i64)
(defvar low i64)
(defvar high i64)
(defvar length i64)
;;; Two frames deep, with a defer on the way, so the transfer has something to
;;; cross and something to run on its way out.
(defn show [name string n i64] ()
(print name) (print " ") (print n) (println ""))
(defn read-cell [i i32] i32
(defer (set cleaned (+ cleaned 1)))
(at grid i))
(defn write-cell [i i32] ()
(defer (set cleaned (+ cleaned 1)))
(set (at grid i) 99))
;;; The frame loop's shape: one restart-case around the work, offering
;;; `continue`, which abandons this frame and nothing else. sand.flan's main
;;; loop is this.
(defn read-frame [i i32] ()
(restart-case
(do (show "read" (i64 (read-cell i)))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1)))))
(defn write-frame [i i32] ()
(restart-case
(do (write-cell i)
(show "wrote" (i64 (at grid i)))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1)))))
(defn slice-frame [s [u8] lo i32 hi i32] ()
(restart-case
(do (show "slice" (i64 (len (slice s lo hi))))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1)))))
;;; The two Vec frames are written inline in main rather than as functions,
;;; because a (Vec T) is move-only: passing one to a helper would hand
;;; ownership over and the caller's binding would be dead afterwards. A
;;; restart-case does not have to be in a different function from the
;;; handler-bind that answers into it — the transfer is by frame address and
;;; the frames here are simply both this one.
(defn main [] i32
(set (at grid 0) 10)
(set (at grid 1) 11)
(set (at grid 2) 12)
(set (at grid 3) 13)
(let [s (bytes "hello") ; len 5
v (vec-new i32)]
(push v 100)
(push v 200)
(handler-bind
[(BoundsError [c]
;; The numbers are here rather than in a message, for the same reason
;; StorageExhausted has none: formatting allocates, and this is a path
;; that must be able to run when allocation is what failed.
(set low (.low c))
(set high (.high c))
(set length (.length c))
;; Abandon the frame. The transfer crosses read-cell (running its
;; defer) and lands in the clause of the restart-case two frames out.
(invoke-restart 'continue))]
;; In bounds: the frame finishes, the handler never runs, and the defer
;; runs on the ordinary return path.
(read-frame 2)
;; Past the end, then negative. A negative index sign-extends to a huge
;; unsigned and is caught by the same single comparison, but the number
;; the condition carries is the signed one.
(read-frame 7)
(show "low" low)
(show "length" length)
(read-frame -1)
(show "low" low)
;; The write path lowers through place/Pindex rather than through At, so
;; it would be perfectly possible to convert one and not the other.
(write-frame 1)
(write-frame 4)
(show "low" low)
;; A slice reports both ends, which is the whole reason low and high are
;; two fields: [2 9) against a length of 5.
(slice-frame s 1 4)
(slice-frame s 2 9)
(show "low" low)
(show "high" high)
(show "length" length)
;; A reversed range, which the lo <= hi test is what catches: without it
;; this builds a slice of length hi - lo as a huge unsigned.
(slice-frame s 3 1)
(show "low" low)
(show "high" high)
;; And the Vec pair, whose checks are inside the runtime rather than in
;; emitted IR — a different code path entirely, and the one most likely
;; to be left behind by a change made in emit.ml.
(restart-case
(do (show "vec" (i64 (at v 1)))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1))))
(restart-case
(do (show "vec" (i64 (at v 5)))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1))))
(show "low" low)
(show "length" length)
(restart-case
(do (show "vec-slice" (i64 (len (as-slice v 0 2))))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1))))
(restart-case
(do (show "vec-slice" (i64 (len (as-slice v 0 9))))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1))))
(show "high" high))
(free v))
;; Six frames finished, six were abandoned, and every one of the twelve ran
;; its defer — which is the claim about defer that the old shape could not
;; make.
(show "frames" frames)
(show "skipped" skipped)
(show "cleaned" cleaned)
;; The write that did land, and the one that did not: grid[1] is 99 and
;; nothing else moved.
(print (at grid 0)) (print " ") (print (at grid 1)) (print " ")
(print (at grid 2)) (print " ") (print (at grid 3)) (println "")
0)