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.
This commit is contained in:
Joseph Ferano 2026-09-21 07:52:13 +07:00
parent 0a8dc1d82b
commit 2d8d9cd5a8
10 changed files with 424 additions and 29 deletions

33
FIX.org
View File

@ -4462,3 +4462,36 @@ sand-dependent tests (test_flan's parse pin, test_session's create,
test_acceptance's "a package's main is not visible") are red here and turn
green when those seven lines say ~defonce~ (or ~def~, where the author wants
the initialiser to follow the source — ~colors~ was the motivating one).
* dotimes counts, 2026-09-21
"Is there a way to do dotimes or a loop in reverse?" — the answer was a
hand-written let plus set, which is the wrong answer for the commonest loop
there is after counting up.
Ruled: dotimes grows the start/stop/step arities, the way CL's loop and
Clojure's range have them.
(dotimes [i n]) ; 0 .. n-1, unchanged
(dotimes [i start stop]) ; start .. stop-1
(dotimes [i start stop step]) ; start, start+step, ... while short of stop
stop is exclusive in every arity, so (dotimes [i 0 n]) is (dotimes [i n]) —
one rule, not two — and a negative step counts down, testing with > instead
of <. (dotimes [i 9 -1 -1]) is 9 down to 0.
The two edges, decided: a literal step of 0 is refused at compile time, being
an infinite loop spelled as an accident; a step that is only a value cannot
be refused there, and the sign test that picks the loop's direction leaves 0
with neither direction, so it runs no times at all. Terminating and
deterministic, and it costs nothing — a literal step still emits the one
comparison it always did.
Still a special form desugaring in check.ml to a Let and a While, so neither
backend learned anything. test/programs/dotimes-range.flan is the corpus
program; docs/BUILT.md carries the convention.
Four sites, all of them this feature and none of them a pre-existing bug:
parse.ml takes a vector of two to four, check.ml desugars it, and load.ml's
two walks — the Ast rename and the Form-level one at load.ml:503 — learn to
walk more than one bound. The Form walk matched Vec [n; count] exactly, so
it had to grow; before this, a three-bound dotimes was a parse error long
before that walk could see it, so nothing was ever miscompiled by it.

View File

@ -12,7 +12,8 @@ first. Come here when you need to know why something is the shape it is.
**`dotimes`** desugars in `check.ml` to a `Let` plus a `While` — no new IR node. The bound is evaluated once into a
hidden slot before the loop, so a body that changes it cannot change the trip count, and the loop variable is not
assignable, which makes the generated step its only writer.
assignable, which makes the generated step its only writer. **Amended** by the start/stop/step arities; see
"`dotimes` counts from where you say" at the foot of this file.
**`defer`** is recognised in `check_fn` and nowhere else, because that is the only place that knows a form is at the top
level of a function body. Each one is checked in place, then registered on the context; it emits nothing where it
@ -6239,3 +6240,54 @@ One shared plan feeds both backends, so x86 and LLVM cannot disagree; `programs/
every `def` spelling's startup on both, at `-O0` and `-O2`, and `programs/dev-rerun.flan` pins the live loop — a
`(def c 3)` printing 4 on every run beside a defonce that climbs, and the edited `(def c 9)` printing 10 after the next
re-run.
## `dotimes` counts from where you say
The author asked whether there was a way to run a `dotimes` in reverse, and the answer was a hand-written `let` plus
`set` — which is the wrong answer for the commonest loop there is after counting up. So `dotimes` grew the arities
Common Lisp's `loop` and Clojure's `range` have:
```flan
(dotimes [i n]) ; 0 .. n-1, exactly as before
(dotimes [i start stop]) ; start .. stop-1
(dotimes [i start stop step]) ; start, start+step, ... while it is still short of stop
```
**`stop` is exclusive in every arity, and a negative step counts down and tests with `>` instead of `<`** — so
`(dotimes [i 0 n] ...)` is the same loop as `(dotimes [i n] ...)`, one rule rather than two, and
`(dotimes [i 9 -1 -1] ...)` counts 9 down to 0.
Still a special form, still `check_dotimes`, still a `Let` and a `While` with the step in the latch. Nothing new
reaches either backend: an `x86` build of `test/programs/dotimes-range.flan` prints what the LLVM build prints, at
`-O2` and at `-O0`.
### Each bound once, and in the order written
`start` is the counter's initial value in the `Let`, `stop` gets the hidden slot it always had, and a `step` gets one
too when it is not a literal. All three are checked — and therefore evaluated — left to right and outside the
counter's scope, so a body that assigns to whatever they were computed from cannot move them. `dotimes-range.flan`
proves it with a function that prints a marker per call: the three markers appear once each, in order, ahead of the
counter's own output. A literal `step` needs no slot to be evaluated once, which is why the one-bound form emits
exactly the frame and the comparison it emitted before.
### The sign of the step, and the step of zero
The comparison direction is the step's sign, and when the step is a literal the direction is known while checking: one
`Lt` or one `Gt`, no test, no cost. **A literal step of `0` is refused where it is written** — it is an infinite loop
spelled as an accident, and the message says to write a step that moves or leave it out for 1.
A step that is only known at run time cannot be refused, so the condition asks the sign first: `step > 0` and the
counter short of `stop`, else `step < 0` and the counter past it, else stop. Written as nested `If`s, which is what
`and` and `or` already become. That shape decides the run-time zero for free — neither arm holds, so **a run-time step
of `0` runs the loop no times at all**. It is the one answer that is both deterministic and terminating; a trap would
need a check the literal case does not want, and looping for ever is the failure the refusal above exists to prevent.
### Width, and overflow
Every bound is an index, so every bound is `i32``check ~want:index_ty`, the same as the single bound has always
had. There is no width to join here and the repo's join rules do not come into it: a bound of another width is the
ordinary `expected i32, found i64`.
Arithmetic wraps in Flan (`emit.ml`, no `nsw`/`nuw`), and the counter is arithmetic like any other. A step that
carries `i` past `i32`'s range therefore wraps to the far end instead of trapping — defined, but the loop then runs
far longer than it was meant to, and can fail to reach `stop` at all. Keep `stop` within one `step` of the width's
limit; nothing checks it for you.

View File

@ -116,7 +116,12 @@ and expr_kind =
| ArrayGen of len list * expr
(* These bind names or alter control flow, so none of them can be a call. *)
| Fn of string list * expr list (* (fn [x y] ...) — non-escaping *)
| Dotimes of string option * string * expr * expr list (* (dotimes :o [i n] ...) *)
(* (dotimes :o [i n] ...), (dotimes [i start stop] ...) and
(dotimes [i start stop step] ...). The bounds are a record rather than
three positional fields because the one-bound form is the common one and
"which of these is the stop" should not be a counting exercise at every
site that walks them. *)
| Dotimes of string option * string * bounds * expr list
| Defer of expr list (* runs on scope exit *)
| Unwrap of unwrap * expr (* (some x) / (try x) *)
(* (handler-bind [(Type [c] body ...) ...] body ...) — spec-conditions.md.
@ -136,6 +141,11 @@ and expr_kind =
| RestartCase of expr * rclause list
| InvokeRestart of string * expr list
(* A [dotimes]'s counting. [dstop] is always written; the other two have
defaults 0 and 1 and are [None] when the source left them out, which is
what lets the one-bound form desugar to exactly what it always did. *)
and bounds = { dstart : expr option; dstop : expr; dstep : expr option }
(* Two ways to signal, because they are two different things — §1 and §2.
[signal] returns Unit whatever it finds; [error] has type Never and, with
nothing transferring, the program stops. *)
@ -408,7 +418,12 @@ let map_children f (e : expr) : expr =
| ArrayFill (ds, v) -> ArrayFill (ds, ex v)
| ArrayGen (ds, f) -> ArrayGen (ds, ex f)
| Fn (ps, es) -> Fn (ps, List.map ex es)
| Dotimes (l, n, c, es) -> Dotimes (l, n, ex c, List.map ex es)
| Dotimes (l, n, b, es) ->
Dotimes (l, n,
{ dstart = Option.map ex b.dstart;
dstop = ex b.dstop;
dstep = Option.map ex b.dstep },
List.map ex es)
| Defer es -> Defer (List.map ex es)
| Unwrap (u, x) -> Unwrap (u, ex x)
| HandlerBind (cs, es) -> HandlerBind (List.map hcl cs, List.map ex es)

View File

@ -3144,8 +3144,8 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
Option; this one returns %s" (Types.to_string other))
| Ast.Unwrap (Ast.Utry, _) -> unimplemented loc "try (Result)" 6
| Ast.Fn (params, body) -> check_fn ctx ~want loc params body
| Ast.Dotimes (label, name, count, body) ->
check_dotimes ctx ~want loc label name count body
| Ast.Dotimes (label, name, bounds, body) ->
check_dotimes ctx ~want loc label name bounds body
(* (signal c) : Unit, always — spec-conditions.md §1. A handler that returns
normally leaves the signalling function to carry on, and with nothing
matching this is a no-op, so nothing about it alters control flow. That is
@ -4106,31 +4106,108 @@ and loop_target ctx loc verb label =
in
go 0 ctx.loops
and check_dotimes ctx ~want loc label name count body =
let count = check ctx ~want:index_ty count in
(* [(dotimes [i n] ...)], [(dotimes [i start stop] ...)] and
[(dotimes [i start stop step] ...)].
**The convention.** [stop] is exclusive, so [(dotimes [i 0 n] ...)] is the
same loop as [(dotimes [i n] ...)] one rule rather than two, and the
shorter form stays the longer one with its defaults left off. A negative
step counts down and tests with [>] instead of [<], which is what makes
[(dotimes [i 9 -1 -1] ...)] run 9 down to 0.
**Each bound once, before the loop.** [start] is the counter's initial
value, [stop] and a non-literal [step] each get a hidden slot, and all three
are checked and therefore evaluated left to right, outside the counter's
scope. A body that assigns to what they were computed from cannot change the
trip count.
**The sign of the step.** When it is a literal the direction is known here
and the condition is the one comparison it always was, so nothing changed
for every loop anyone has written. A literal 0 is refused: it is an infinite
loop spelled as an accident. A step that is only known at run time gets a
condition that asks the sign first, and the cost lands on exactly the loops
that need it. A run-time 0 falls out of that test as a loop that runs no
times at all neither arm of the sign test holds which is deterministic
and terminating, the two things an accidental hang is not. *)
and check_dotimes ctx ~want loc label name (b : Ast.bounds) body =
(* Left to right, and all three before the counter is bound: they are
evaluated before it exists, so [(dotimes [i i (* outer i *)] ...)] reads
the outer name and the order a counter function sees is the written one. *)
let start = Option.map (fun e -> check ctx ~want:index_ty e) b.Ast.dstart in
let stop = check ctx ~want:index_ty b.Ast.dstop in
let step = Option.map (fun e -> check ctx ~want:index_ty e) b.Ast.dstep in
let int k = mk loc index_ty (Tast.Int (k, Types.I32)) in
(* A literal step, if that is what was written. The default is 1, which is a
literal too, so the one-bound form takes this path and emits exactly what
it has always emitted. *)
let literal =
match step with
| None -> Some 1L
| Some { Tast.e = Tast.Int (k, _); _ } -> Some k
| Some _ -> None
in
(match literal, step with
| Some 0L, Some s ->
fail s.Tast.loc
"a step of 0 never moves the counter, so this loop would never end. \
Give it a step that moves, as in (dotimes [i 0 10 2] (print i)). \
Left out, the step is 1"
| _ -> ());
scoped ctx (fun () ->
let i = bind ctx name index_ty ~assignable:false in
let limit = fresh_slot ctx index_ty in
(* A slot only when the step is not a literal: a literal needs no slot to
be evaluated once, and the one-bound form's frame keeps the shape it
had. *)
let stepslot =
match literal with None -> Some (fresh_slot ctx index_ty) | Some _ -> None
in
let body = in_loop ctx ?label (fun () -> map_lr (fun b -> check ctx b) body) in
let iv = mk loc index_ty (Tast.Local i) in
let one = mk loc index_ty (Tast.Int (1L, Types.I32)) in
let cond =
mk loc Types.Bool
(Tast.Prim (Tast.Lt, [ iv; mk loc index_ty (Tast.Local limit) ]))
let limitv = mk loc index_ty (Tast.Local limit) in
let stepv =
match literal, stepslot with
| Some k, _ -> int k
| None, Some s -> mk loc index_ty (Tast.Local s)
| None, None -> assert false
in
let step =
let cmp op = mk loc Types.Bool (Tast.Prim (op, [ iv; limitv ])) in
let cond =
match literal with
| Some k when k > 0L -> cmp Tast.Lt
| Some _ -> cmp Tast.Gt
| None ->
(* Both directions, asked in the order that leaves 0 with neither: the
counter has not passed the stop *and* the step is going that way.
Written as nested [If]s because that is what [and] and [or] already
become, so nothing new reaches a backend. *)
let sign op =
mk loc Types.Bool (Tast.Prim (op, [ stepv; int 0L ]))
in
mk loc Types.Bool
(Tast.If (sign Tast.Gt, cmp Tast.Lt,
mk loc Types.Bool
(Tast.If (sign Tast.Lt, cmp Tast.Gt,
mk loc Types.Bool (Tast.Bool false)))))
in
let advance =
mk loc Types.Unit
(Tast.Set (Tast.Plocal i,
mk loc index_ty (Tast.Prim (Tast.Add, [ iv; one ]))))
mk loc index_ty (Tast.Prim (Tast.Add, [ iv; stepv ]))))
in
let zero = mk loc index_ty (Tast.Int (0L, Types.I32)) in
(* The step is the *latch* and not the last form of the body. Folded onto
the body it would be skipped by a [continue], which branches past the
rest of the body so [i] would never advance and the loop would hang.
That is the whole reason [Tast.While] carries a third list. *)
let loop = mk loc Types.Unit (Tast.While (cond, body, [ step ])) in
expect ctx loc ~want
(mk loc Types.Unit (Tast.Let ([ (i, zero); (limit, count) ], [ loop ]))))
let loop = mk loc Types.Unit (Tast.While (cond, body, [ advance ])) in
let binds =
(i, match start with Some s -> s | None -> int 0L)
:: (limit, stop)
:: (match stepslot, step with
| Some s, Some v -> [ (s, v) ]
| _ -> [])
in
expect ctx loc ~want (mk loc Types.Unit (Tast.Let (binds, [ loop ]))))
(* ── (loop [...] ...) and (recur ...) ───────────────────────────────────

View File

@ -314,8 +314,14 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr =
| Ast.ArrayGen (ds, v) -> Ast.ArrayGen (List.map (rename_len owned alias) ds, go v)
| Ast.Fn (ps, body) ->
Ast.Fn (ps, List.map (rename_expr owned alias (ps @ bound)) body)
| Ast.Dotimes (l, i, n, body) ->
Ast.Dotimes (l, i, go n,
| Ast.Dotimes (l, i, b, body) ->
(* The bounds are outside the counter's scope — they are evaluated before
it exists so they rename against [bound], and only the body gets
[i] added to it. *)
Ast.Dotimes (l, i,
{ Ast.dstart = Option.map go b.Ast.dstart;
dstop = go b.Ast.dstop;
dstep = Option.map go b.Ast.dstep },
List.map (rename_expr owned alias (i :: bound)) body)
| Ast.Defer body -> Ast.Defer (gos body)
| Ast.Unwrap (u, v) -> Ast.Unwrap (u, go v)
@ -616,10 +622,13 @@ let rec rename_form owned alias bound (f : Form.t) : Form.t =
keep (Form.List (hd :: pv :: List.map (go bound) body))
| Form.List (({ Form.v = Form.Sym "dotimes"; _ } as hd) :: rest) ->
(match peel_label rest with
| lbl, ({ Form.v = Form.Vec [ n; count ]; loc = bloc } :: body) ->
(* One, two or three bounds. They are evaluated before the counter
exists, so they walk under [bound]; only the body sees [n]. *)
| lbl, ({ Form.v = Form.Vec (n :: ((_ :: _) as bs)); loc = bloc } :: body)
when List.length bs <= 3 ->
keep (Form.List
(hd :: lbl
@ Form.make (Form.Vec [ n; go bound count ]) bloc
@ Form.make (Form.Vec (n :: List.map (go bound) bs)) bloc
:: List.map (go (form_syms n bound)) body))
| _ -> keep (Form.List (hd :: List.map (go bound) rest)))
| Form.List xs -> keep (Form.List (List.map (go bound) xs))
@ -789,7 +798,9 @@ let rec expr_uses acc (e : Ast.expr) =
ds;
go v
| Ast.Fn (_, body) -> gos body
| Ast.Dotimes (_, _, n, body) -> go n; gos body
| Ast.Dotimes (_, _, b, body) ->
Option.iter go b.Ast.dstart; go b.Ast.dstop; Option.iter go b.Ast.dstep;
gos body
| Ast.Defer body -> gos body
| Ast.Unwrap (_, v) -> go v
| Ast.Signal (_, c) -> go c

View File

@ -496,12 +496,30 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
mk (Ast.Fn (List.map sym ps, body_of body))
| _ -> fail f "fn is (fn [param ...] body ...)")
(* One, two or three bounds. The stop is always the last one written, so the
shorter forms are the longer one with its defaults left off: [start] is 0
and [step] is 1. *)
| Sym "dotimes" ->
(match label args with
| lbl, ({ v = Vec [ n; count ]; _ } :: body) ->
| lbl, ({ v = Vec (n :: ((_ :: _) as bs)); _ } :: body)
when List.length bs <= 3 ->
no_pattern n;
mk (Ast.Dotimes (lbl, sym n, expr count, body_of body))
| _ -> fail f "dotimes is (dotimes [name count] body ...)")
let b =
match List.map expr bs with
| [ stop ] -> { Ast.dstart = None; dstop = stop; dstep = None }
| [ start; stop ] ->
{ Ast.dstart = Some start; dstop = stop; dstep = None }
| [ start; stop; step ] ->
{ Ast.dstart = Some start; dstop = stop; dstep = Some step }
| _ -> assert false
in
mk (Ast.Dotimes (lbl, sym n, b, body_of body))
| _ ->
fail f
"dotimes is (dotimes [name stop] body ...), \
(dotimes [name start stop] body ...) or \
(dotimes [name start stop step] body ...) stop is exclusive, and a \
negative step counts down")
(* [(loop [x 0 acc 1] body ...)]. No label: [break] and [continue] may not
leave a loop a loop answers with the value of its body, and a jump out

View File

@ -0,0 +1,126 @@
;;;; 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)

View File

@ -421,6 +421,19 @@ let () =
watchdog above is what turns that failure back into a report. *)
outputs "break and continue" "programs/loops.flan"
"4\n9\n8\n3\n0\n1\n0\n0\n0\n3\n6\nhit\nhit\n2\n";
(* dotimes with a start, a stop and a step. Three things here fail by
hanging rather than by printing wrongly, which is again the watchdog's
job: a continue in a down-counting loop (the latch must subtract on the
skipped iteration too), a step of 0 that is only a value (no direction,
so no trips), and any bound read per iteration instead of once. The
bump lines are the evaluation count and order one marker per bound,
ahead of the counter's own output. *)
outputs "dotimes with a range" "programs/dotimes-range.flan"
"012\n234\n0369\n0123\n9876543210\n10741\n\
-2147483645\n-2147483646\n-2147483647\nnone\n\
78901\n701\n012\n012\n\
012\n0134\n234\n235\n036\n039\n54\n5421\n4\n21\n222\n\
0246\n6420\nzero\n456024\n";
(* loop and recur. The ten-million line is the one that matters: a recur is
a jump to the top of a [While] and not a call, so the program returns
rather than running out of stack. The swap line is the other recur

View File

@ -401,8 +401,19 @@ let () =
(* This is the class that silently misparses: it reads fine as a call and
means something entirely different. *)
(match (parse1 "(dotimes [i 10] (f i))").e with
| Dotimes (None, "i", { e = Int 10L; _ }, [ _ ]) -> ()
| Dotimes (None, "i",
{ dstart = None; dstop = { e = Int 10L; _ }; dstep = None },
[ _ ]) -> ()
| _ -> check "dotimes binds" false);
(* The written bound is always the *stop*, so the shorter forms are the
longer one with its defaults left off. *)
(match (parse1 "(dotimes [i 9 -1 -1] (f i))").e with
| Dotimes (None, "i",
{ dstart = Some { e = Int 9L; _ };
dstop = { e = Int (-1L); _ };
dstep = Some { e = Int (-1L); _ } },
[ _ ]) -> ()
| _ -> check "dotimes takes start, stop and step" false);
(match (parse1 "(fn [x y] x)").e with
| Fn ([ "x"; "y" ], [ _ ]) -> ()
| _ -> check "fn binds" false);
@ -2986,6 +2997,36 @@ let () =
"(defn f [] () (while :o true (while true (break :o))))";
accepts "continue in a dotimes"
"(defn f [] () (dotimes [i 3] (continue)))";
(* And in the other two arities, counting either way. The output side of
this is test/programs/dotimes-range.flan; what is checked here is that
nothing about the longer forms disturbs the loop stack. *)
accepts "break and continue in a start/stop dotimes"
"(defn f [] () (dotimes [i 2 5] (when (= i 3) (continue)) (break)))";
accepts "break and continue in a down-counting dotimes"
"(defn f [] () (dotimes :o [i 9 -1 -1] (when (= i 3) (continue :o)) \
(break :o)))";
(* A step of 0 written as a literal is an infinite loop spelled as an
accident, so it is refused where it is written. A step that is only a
value cannot be refused here and runs no times at all the sign test
that picks the direction leaves it with neither. *)
rejects_check "a literal step of 0"
"(defn f [] () (dotimes [i 0 10 0] (print i)))"
~needle:"a step of 0 never moves the counter";
accepts "a step whose sign is not known until run time"
"(defn f [] () (let [s 0] (dotimes [i 0 10 s] (print i))))";
(* Three bounds is the most there are. A fourth is refused by the parser,
which names all three arities. *)
rejects_check "a dotimes with four bounds"
"(defn f [] () (dotimes [i 0 10 2 1] (print i)))"
~needle:"(dotimes [name start stop step] body ...)";
rejects_check "a dotimes with no bound at all"
"(defn f [] () (dotimes [i] (print i)))"
~needle:"(dotimes [name stop] body ...)";
(* Every bound is an index, so it is i32 like the one bound always was.
There is no width to join: a wider one is the ordinary type error. *)
rejects_check "a dotimes bound of another width"
"(defn f [] () (let [n (i64 10)] (dotimes [i 0 n] (print i))))"
~needle:"expected i32, found i64";
rejects_check "break outside a loop"
"(defn f [] () (break))" ~needle:"only allowed inside a loop";
rejects_check "continue outside a loop"

View File

@ -715,9 +715,18 @@ whose type matters is named at the top level rather than written inline.</p>
unless runs when the test is false
8</code></pre>
<p><code>dotimes</code> evaluates its bound once into a hidden slot before the loop, so
a body that changes it cannot change the trip count, and the loop variable is not
assignable.</p>
<p><code>dotimes</code> takes one, two or three bounds:
<code>(dotimes [i n])</code> counts 0 to n-1, <code>(dotimes [i start stop])</code> counts
start to stop-1, and <code>(dotimes [i start stop step])</code> steps by step. The stop is
exclusive in every form — so <code>(dotimes [i 0 n])</code> is <code>(dotimes [i n])</code>
and a negative step counts down: <code>(dotimes [i 9 -1 -1])</code> is 9 down to 0. A step
of 0 written as a literal is a compile error; one that is only known at run time runs the
loop no times.</p>
<p>Each bound is evaluated exactly once, before the loop and left to right — the start
into the counter itself, the stop into a hidden slot, and the step into one too unless it
is a literal. So a body that changes what a bound came from cannot change the trip count,
and the loop variable is not assignable.</p>
<p><code>break</code> and <code>continue</code> leave or restart the innermost loop, and
take a <strong>label</strong> when that is not the one meant. <code>loop</code> and