flan/test/programs/dotimes-range.flan
Joseph Ferano 2d8d9cd5a8 dotimes counts from where you say, and can count down
"Is there a way to do dotimes or a loop in reverse?" — the answer was a
hand-written let plus set. Now it is (dotimes [i 9 -1 -1]).

Three arities: [i n], [i start stop], [i start stop step]. The stop is
exclusive in all of them, so [i 0 n] is [i n] — one rule, not two — and a
negative step counts down, testing with > instead of <.

A literal step of 0 is refused where it is written. One that is only a value
cannot be, so the condition asks the sign first and 0 falls out of it as a
loop that runs no times: terminating and deterministic, and free, because a
literal step still emits the single comparison it always did.

Each bound is evaluated once, left to right, before the counter exists: the
start into the counter, the stop into the hidden slot it always had, the
step into one of its own unless it is a literal.

Still a special form, still a Let and a While with the step in the latch, so
neither backend learned anything — the new program prints the same thing
under --x86 and at -O0. load.ml's Form-level walk had to learn more than one
bound for the same reason parse.ml did; it is part of this feature and not a
bug that was sitting there, because before this a three-bound dotimes was a
parse error long before that walk could reach it.
2026-09-21 08:09:53 +07:00

127 lines
5.8 KiB
Plaintext

;;;; dotimes with start, stop and step.
;;;;
;;;; stop is exclusive in every arity, so (dotimes [i 0 n]) is (dotimes [i n]),
;;;; and a negative step counts down and tests with > instead of <. What is
;;;; asserted here rather than in a unit test is what comes out:
;;;;
;;;; 1. Each bound is evaluated exactly once, before the loop, and left to
;;;; right. The counter function prints a marker per call, so a bound read
;;;; twice shows up as an extra marker and a bound read per iteration shows
;;;; up as many.
;;;;
;;;; 2. A body that assigns to what a bound was computed from cannot change the
;;;; trip count — the bounds are in hidden slots by then.
;;;;
;;;; 3. (continue) advances a *down*-counting loop too. The step is the latch
;;;; whichever way it goes, and folded onto the body this hangs rather than
;;;; printing the wrong thing, which is what the watchdog is for.
;;;;
;;;; 4. A step whose sign is only known at run time picks its direction at the
;;;; test, and a run-time step of 0 runs the loop no times at all.
;; Prints its tag and answers with its value, so the output says how many times
;; and in what order each bound was evaluated.
(defn bump [tag i32 v i32] i32
(print tag)
v)
(defn main [] i32
;; ── the three arities, counting up ────────────────────────────────
(dotimes [i 3] (print i)) (println "") ; 012
(dotimes [i 2 5] (print i)) (println "") ; 234
(dotimes [i 0 10 3] (print i)) (println "") ; 0369 — uneven, stops short
;; (dotimes [i 0 n]) is (dotimes [i n]). One rule, not two.
(dotimes [i 0 4] (print i)) (println "") ; 0123
;; ── counting down ─────────────────────────────────────────────────
(dotimes [i 9 -1 -1] (print i)) (println "") ; 9876543210
(dotimes [i 10 0 -3] (print i)) (println "") ; 10 7 4 1
;; The last representable i32 is reachable as a stop: it is exclusive, so
;; the counter reaches -2147483647 and the test ends it there.
(dotimes [i -2147483645 -2147483648 -1] (print i) (println "")) ; three lines
;; ── zero-trip loops ───────────────────────────────────────────────
(dotimes [i 5 5] (print i)) ; nothing
(dotimes [i 0 10 -1] (print i)) ; nothing: already past
(dotimes [i 10 0] (print i)) ; nothing: already past
(println "none")
;; ── each bound once, in the order written ─────────────────────────
;; 7 8 9 for the three bounds, then the two iterations. A bound evaluated
;; per iteration would interleave; one evaluated twice would repeat.
(dotimes [i (bump 7 0) (bump 8 2) (bump 9 1)] (print i))
(println "")
;; The same for the one-bound form, which is the one that always worked.
(dotimes [i (bump 7 2)] (print i))
(println "")
;; ── a body cannot move the bounds ─────────────────────────────────
(let [n 3]
(dotimes [i 0 n] (set n 0) (print i))) ; 012, not 0
(println "")
(let [s 1]
(dotimes [i 0 3 s] (set s 5) (print i))) ; 012, not 0
(println "")
;; ── break and continue, in every arity ────────────────────────────
(dotimes [i 5] (when (= i 3) (break)) (print i))
(println "") ; 012
(dotimes [i 5] (when (= i 2) (continue)) (print i))
(println "") ; 0134
(dotimes [i 2 8] (when (= i 5) (break)) (print i))
(println "") ; 234
(dotimes [i 2 6] (when (= i 4) (continue)) (print i))
(println "") ; 235
(dotimes [i 0 12 3] (when (= i 9) (break)) (print i))
(println "") ; 036
(dotimes [i 0 12 3] (when (= i 6) (continue)) (print i))
(println "") ; 039
;; Counting down, which is the new path for the latch: the skipped
;; iteration still subtracts, or this never finishes.
(dotimes [i 5 0 -1] (when (= i 3) (break)) (print i))
(println "") ; 54
(dotimes [i 5 0 -1] (when (= i 3) (continue)) (print i))
(println "") ; 5421
;; Every iteration continues and the trip count is still the trip count.
(let [c 0]
(dotimes [i 4 0 -1] (set c (+ c 1)) (continue))
(print c))
(println "") ; 4
;; Labels, on a down-counting loop.
(dotimes :outer [a 2 -1 -1]
(dotimes [b 2 -1 -1]
(when (= b 0) (break :outer))
(print b)))
(println "") ; 21
(dotimes :rows [r 2 -1 -1]
(dotimes [c 2 -1 -1]
(when (= c 1) (continue :rows))
(print c))
(println "tail"))
(println "") ; 222, no tail
;; ── a step the compiler cannot see the sign of ────────────────────
(let [up 2 down -2 flat 0]
(dotimes [i 0 7 up] (print i)) ; 0246
(println "")
(dotimes [i 6 -1 down] (print i)) ; 6420
(println "")
;; A step of 0 cannot be refused here — it is a value, not a literal — so
;; the sign test leaves it with no direction and the loop runs no times.
(dotimes [i 0 7 flat] (print i))
(println "zero")
;; And it is evaluated once like the others, so a body that changes it
;; changes nothing.
(dotimes [i (bump 4 0) (bump 5 6) (bump 6 up)] (print i))
(println ""))
0)