191 lines
8.0 KiB
Plaintext
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 (docs/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 docs/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)
|