flan/test/programs/loops.flan
Joseph Ferano 008165335d break crosses only sometimes, so the refusal is relative now
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.
2026-09-12 21:58:53 +07:00

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)