From 0a1b89c97589e11fe958a85c02debe6c59cb28f7 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 6 Jun 2026 18:02:59 +0000 Subject: [PATCH] flow: bounded iteration combinators flow-while/flow-until + 6 tests (flow-while pred body max) / (flow-until pred body max) re-run body threading the value while/until pred holds, capped at max steps for a deterministic bound (no unbounded loops in pure SX). 122/122 across 7 suites. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/flow/scoreboard.json | 6 +++--- lib/flow/scoreboard.md | 4 ++-- lib/flow/spec.sx | 5 ++++- lib/flow/tests/combinators.sx | 33 ++++++++++++++++++++++++++++++++- plans/flow-on-sx.md | 6 ++++-- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/lib/flow/scoreboard.json b/lib/flow/scoreboard.json index 0b50b43b..0c32983f 100644 --- a/lib/flow/scoreboard.json +++ b/lib/flow/scoreboard.json @@ -1,6 +1,6 @@ { - "total": 116, - "passed": 116, + "total": 122, + "passed": 122, "failed": 0, "suites": { "basic": { "passed": 18, "total": 18 }, @@ -9,7 +9,7 @@ "recovery": { "passed": 8, "total": 8 }, "distributed": { "passed": 19, "total": 19 }, "api": { "passed": 12, "total": 12 }, - "combinators": { "passed": 11, "total": 11 } + "combinators": { "passed": 17, "total": 17 } }, "phases": { "phase1": "done", "phase2": "done", "phase3": "done", "phase4": "done", "phase5": "done" } } diff --git a/lib/flow/scoreboard.md b/lib/flow/scoreboard.md index a35e93f9..988adb5f 100644 --- a/lib/flow/scoreboard.md +++ b/lib/flow/scoreboard.md @@ -1,6 +1,6 @@ # flow-on-sx Scoreboard -**All tests pass: 116 / 116 across 7 suites. Phases 1-5 complete.** +**All tests pass: 122 / 122 across 7 suites. Phases 1-5 complete.** `bash lib/flow/conformance.sh` @@ -14,7 +14,7 @@ | recovery | 8 | Phase 3: crash recovery — store export/import, resumable scan, restart-at-every-step, replay-log survival | | distributed | 19 | Phase 4: `remote-node` (7); `remote-failover` (6); replication + handoff across instances (6) | | api | 12 | Phase 5: introspection — `flow/status`, `flow/result`, `flow/list`, `flow/pending` | -| combinators | 11 | Phase 5: `tap` side-effect, `recover` (fail-value), `map-flow` fan-over-list | +| combinators | 17 | Phase 5: `tap`, `recover` (fail-value), `map-flow` fan-over-list, `flow-while`/`flow-until` bounded iteration | ## Architecture diff --git a/lib/flow/spec.sx b/lib/flow/spec.sx index c15e9cb3..933c571c 100644 --- a/lib/flow/spec.sx +++ b/lib/flow/spec.sx @@ -11,6 +11,9 @@ ;; defflow both binds the flow and registers it by name (flow-register!, in ;; store.sx) so it can be re-resolved after a process restart. ;; map-flow (Phase 5): run a node over each item of a list input, join results. +;; flow-while / flow-until (Phase 5): bounded iteration — re-run body, threading +;; the value, while/until pred holds, up to `max` steps (deterministic bound; no +;; unbounded loops in pure SX). ;; ;; Phase 2 combinators (flow-control-src): ;; branch / fail / failed? / fail-reason / try-catch / retry / timeout / tick @@ -35,7 +38,7 @@ (define flow-combinators-src - "(define (flow-node f) f)\n (define (flow-id input) input)\n (define (flow-const v) (lambda (input) v))\n (define (flow-seq-step ns v)\n (if (null? ns) v (flow-seq-step (cdr ns) ((car ns) v))))\n (define sequence (lambda ns (lambda (input) (flow-seq-step ns input))))\n (define parallel (lambda ns (lambda (input) (map (lambda (n) (n input)) ns))))\n (define (map-flow node) (lambda (items) (map node items)))\n (define-syntax defflow\n (syntax-rules ()\n ((defflow nm body)\n (begin (define nm body) (flow-register! (quote nm) nm)))))") + "(define (flow-node f) f)\n (define (flow-id input) input)\n (define (flow-const v) (lambda (input) v))\n (define (flow-seq-step ns v)\n (if (null? ns) v (flow-seq-step (cdr ns) ((car ns) v))))\n (define sequence (lambda ns (lambda (input) (flow-seq-step ns input))))\n (define parallel (lambda ns (lambda (input) (map (lambda (n) (n input)) ns))))\n (define (map-flow node) (lambda (items) (map node items)))\n (define (flow-while-step pred body input n)\n (if (<= n 0)\n input\n (if (pred input) (flow-while-step pred body (body input) (- n 1)) input)))\n (define (flow-while pred body max) (lambda (input) (flow-while-step pred body input max)))\n (define (flow-until-step pred body input n)\n (if (<= n 0)\n input\n (if (pred input) input (flow-until-step pred body (body input) (- n 1)))))\n (define (flow-until pred body max) (lambda (input) (flow-until-step pred body input max)))\n (define-syntax defflow\n (syntax-rules ()\n ((defflow nm body)\n (begin (define nm body) (flow-register! (quote nm) nm)))))") (define flow-control-src diff --git a/lib/flow/tests/combinators.sx b/lib/flow/tests/combinators.sx index 467e010c..7931bfeb 100644 --- a/lib/flow/tests/combinators.sx +++ b/lib/flow/tests/combinators.sx @@ -1,4 +1,4 @@ -;; lib/flow/tests/combinators.sx — Phase 5: combinator library (tap, recover, map-flow). +;; lib/flow/tests/combinators.sx — Phase 5: combinator library (tap, recover, map-flow, iteration). (define flow-cmb-pass 0) (define flow-cmb-fail 0) @@ -74,4 +74,35 @@ "(flow/start (sequence (map-flow (lambda (x) (* x 10))) (lambda (xs) (apply + xs))) (list 1 2 3))") 60) +;; ── flow-while / flow-until (bounded iteration) ───────────────── +(flow-cmb-test + "flow-while: iterates while the predicate holds" + (flow-m + "(flow/start (flow-while (lambda (x) (< x 10)) (lambda (x) (+ x 1)) 100) 0)") + 10) +(flow-cmb-test + "flow-while: a false predicate leaves input unchanged" + (flow-m + "(flow/start (flow-while (lambda (x) (< x 0)) (lambda (x) (+ x 1)) 100) 5)") + 5) +(flow-cmb-test + "flow-while: respects the max-iteration bound" + (flow-m "(flow/start (flow-while (lambda (x) #t) (lambda (x) (+ x 1)) 3) 0)") + 3) +(flow-cmb-test + "flow-while: doubles until past a threshold" + (flow-m + "(flow/start (flow-while (lambda (x) (< x 50)) (lambda (x) (* x 2)) 100) 3)") + 96) +(flow-cmb-test + "flow-until: iterates until the predicate becomes true" + (flow-m + "(flow/start (flow-until (lambda (x) (>= x 10)) (lambda (x) (+ x 3)) 100) 0)") + 12) +(flow-cmb-test + "flow-until: composes inside a sequence" + (flow-m + "(flow/start (sequence (flow-until (lambda (x) (> x 100)) (lambda (x) (* x 3)) 100) (lambda (x) (- x 100))) 5)") + 35) + (define flow-cmb-tests-run! (fn () {:total (+ flow-cmb-pass flow-cmb-fail) :passed flow-cmb-pass :failed flow-cmb-fail :fails flow-cmb-fails})) diff --git a/plans/flow-on-sx.md b/plans/flow-on-sx.md index 0deaaac6..86b9a957 100644 --- a/plans/flow-on-sx.md +++ b/plans/flow-on-sx.md @@ -16,7 +16,7 @@ federation extension via fed-sx for remote-node execution. ## Status (rolling) -`bash lib/flow/conformance.sh` → **116/116** (Phases 1-5 complete) +`bash lib/flow/conformance.sh` → **122/122** (Phases 1-5 complete) ## Ground rules @@ -145,7 +145,9 @@ something operators and authors actually use. Accumulation, not a rewrite. - [x] `recover` — complement to try-catch for the fail-VALUE channel: run node; if it yields `(fail ...)`, run a recovery node on the reason - [x] `map-flow` — run a flow per item of a list, join results (sequential) -- [x] `lib/flow/tests/api.sx` (12) + `lib/flow/tests/combinators.sx` (11) +- [x] `flow-while` / `flow-until` — bounded iteration: re-run body threading the + value while/until pred holds, capped at `max` steps (deterministic bound) +- [x] `lib/flow/tests/api.sx` (12) + `lib/flow/tests/combinators.sx` (17) ## Progress log