return is refused inside handler-bind and restart-case blanketly, and rightly: a return always crosses the frames they pushed. A break does not. A loop written wholly inside a restart-case body has a perfectly good local break, so the rule is a barrier on the loop stack rather than a flag — a jump is refused exactly when a barrier stands between it and the loop it names, and the message says which construct. handler-bind and restart-case bodies are barriers, so is a restart clause, so are a defer's forms; a handler clause is lifted into its own function and needs no rule at all. in_frames is untouched: a return is the special case where the target is always outside every barrier. continue wanted the other blocker. check_dotimes folded its step onto the end of the body, which a continue would jump past, so the counter would never advance and the loop would hang. Tast.While carries a latch now — condition, body, latch — the step goes there, and emit_while emits four blocks. A while's latch is empty and folds away. Labels are Odin's, in the head position: (while :outer c ...) and (break :outer). A keyword there is unambiguous because a loop condition is never one, so one label function serves while, until, dotimes, break and continue. It is not a goto — the checker resolves a label against the loops the form is lexically inside, so control can only leave a loop it is already in. Break and Continue carry a relative depth rather than a name, because that is what a backend already has: emit keeps one entry per While the way it keeps one pad per frame, and indexes it. Nothing in the prelude wants either. Every early exit there is a return from the function, which break cannot replace; the sentinel-flag loop break exists to remove does not appear in it. The two the compiler emits are that shape and are the one place it cannot help — their sentinel is set inside a restart-case. reach.ml and render.ml take the While arity change and nothing else.
86 lines
3.2 KiB
Plaintext
86 lines
3.2 KiB
Plaintext
;;;; break and continue, with loop labels.
|
|
;;;;
|
|
;;;; The two things worth asserting here rather than in a unit test, because
|
|
;;;; they are about the code that comes out and not about the checker:
|
|
;;;;
|
|
;;;; 1. A (continue) in a dotimes still advances the counter. The step is the
|
|
;;;; loop's *latch* and not the last form of the body — folded onto the body
|
|
;;;; it would be jumped over and the program would hang, which is a test
|
|
;;;; that fails by never finishing rather than by printing the wrong thing.
|
|
;;;; The watchdog is what turns that back into a failure.
|
|
;;;;
|
|
;;;; 2. A labelled break leaves the loop it names and no other.
|
|
|
|
(defstruct Hit [n i32])
|
|
|
|
(defn main [] i32
|
|
;; break, unlabelled: the innermost loop.
|
|
(let [i 0]
|
|
(while (< i 100)
|
|
(set i (+ i 1))
|
|
(when (= i 4) (break)))
|
|
(print i) (println "")) ; 4
|
|
|
|
;; continue in a while. The advance is written before it, because a while
|
|
;; has no latch of its own — that is the loop's own business and not the
|
|
;; compiler's.
|
|
(let [j 0 seen 0]
|
|
(while (< j 6)
|
|
(set j (+ j 1))
|
|
(when (= (% j 2) 0) (continue))
|
|
(set seen (+ seen j)))
|
|
(print seen) (println "")) ; 1 + 3 + 5 = 9
|
|
|
|
;; continue in a dotimes, which is the one the latch exists for: the counter
|
|
;; must advance on the skipped iteration too, or this never returns.
|
|
(let [sum 0]
|
|
(dotimes [k 5]
|
|
(when (= k 2) (continue))
|
|
(set sum (+ sum k)))
|
|
(print sum) (println "")) ; 0 + 1 + 3 + 4 = 8
|
|
|
|
;; A labelled break leaves the named loop. Without the label it would leave
|
|
;; the inner one and the outer would run all three times: 0 1 0 1 0 1.
|
|
(dotimes :outer [a 3]
|
|
(dotimes [b 3]
|
|
(when (= b 2) (break :outer))
|
|
(print b) (println ""))) ; 0 1
|
|
|
|
;; A labelled continue starts the *outer* loop's next iteration, so the rest
|
|
;; of the outer body is skipped as well as the rest of the inner one.
|
|
(dotimes :rows [r 3]
|
|
(dotimes [c 3]
|
|
(when (= c 1) (continue :rows))
|
|
(print c) (println ""))
|
|
(println "tail")) ; 0 0 0, and no tail
|
|
|
|
;; until takes a label on the same rule, being a while with a negated test.
|
|
(let [n 0]
|
|
(until :count (> n 10)
|
|
(set n (+ n 1))
|
|
(when (= n 3) (break :count)))
|
|
(print n) (println "")) ; 3
|
|
|
|
;; A loop wholly inside a restart-case body has a perfectly good local
|
|
;; break: nothing the restart-case established is crossed by leaving a loop
|
|
;; that is inside it. This is the case the blanket refusal on return would
|
|
;; have caught and the relative rule does not.
|
|
(let [t 0]
|
|
(restart-case
|
|
(while true
|
|
(set t (+ t 1))
|
|
(when (> t 5) (break)))
|
|
(carry-on [] (println "not reached")))
|
|
(print t) (println "")) ; 6
|
|
|
|
;; The same the other way round: a handler-bind written inside the loop body
|
|
;; is entered and left before the break runs, so the break crosses nothing.
|
|
(let [u 0]
|
|
(while true
|
|
(handler-bind [(Hit [c] (println "hit"))]
|
|
(signal (Hit {.n 1})))
|
|
(set u (+ u 1))
|
|
(when (= u 2) (break)))
|
|
(print u) (println "")) ; hit hit 2
|
|
0)
|