;;;; 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)