Merge branch 'worktree-agent-a617552a2c34ddc3c' into dev-loop
This commit is contained in:
commit
190fdadcb8
33
FIX.org
33
FIX.org
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
19
lib/ast.ml
19
lib/ast.ml
@ -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)
|
||||
|
||||
105
lib/check.ml
105
lib/check.ml
@ -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 ...) ───────────────────────────────────
|
||||
|
||||
|
||||
21
lib/load.ml
21
lib/load.ml
@ -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
|
||||
|
||||
24
lib/parse.ml
24
lib/parse.ml
@ -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
|
||||
|
||||
126
test/programs/dotimes-range.flan
Normal file
126
test/programs/dotimes-range.flan
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user