flan/test/programs/loops.flan
Joseph Ferano 3f097de522 Pin the latch to the continue path, and the clause barrier to its reason
A dotimes whose every iteration continues still counts to its trip count. The
existing case fails by hanging if the latch is wrong; this one fails by
counting wrong, which is the off-by-one the four-block layout could have.

A restart-case clause was made a barrier on reasoning alone and nothing
observed it. Now something does.

And say what a labelled continue means, which is the half that is not obvious:
it advances the named loop's counter and skips the rest of its body, not just
the rest of the innermost one.
2026-09-12 22:00:13 +07:00

95 lines
3.5 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
;; Every iteration continues, and the count is still the trip count: the
;; latch is on the continue path and not merely reachable from the body. The
;; case above would hang if it were not; this one would count wrong.
(let [c 0]
(dotimes [m 3]
(set c (+ c 1))
(continue))
(print c) (println "")) ; 3
;; 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)