From 65cbdb838757d2a60d5d4aa7faadbc85d02bcee6 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 6 Jun 2026 16:32:37 +0000 Subject: [PATCH] flow: branch combinator (conditional) + 6 tests Phase 2 control flow. (branch pred then else) selects then/else node by running pred on the threaded input; named 'branch' since 'cond' is a Scheme special form. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/flow/conformance.sh | 1 + lib/flow/scoreboard.json | 10 +++++--- lib/flow/scoreboard.md | 8 +++--- lib/flow/spec.sx | 11 +++++++- lib/flow/tests/control.sx | 53 +++++++++++++++++++++++++++++++++++++++ plans/flow-on-sx.md | 5 ++-- 6 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 lib/flow/tests/control.sx diff --git a/lib/flow/conformance.sh b/lib/flow/conformance.sh index b0fbab42..007e7bf2 100755 --- a/lib/flow/conformance.sh +++ b/lib/flow/conformance.sh @@ -22,6 +22,7 @@ VERBOSE="${1:-}" # Suites: NAME RUNNER-FN PATH SUITES=( "basic flow-basic-tests-run! lib/flow/tests/basic.sx" + "control flow-ctl-tests-run! lib/flow/tests/control.sx" ) TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT diff --git a/lib/flow/scoreboard.json b/lib/flow/scoreboard.json index 7fef1c7f..1b51b7e7 100644 --- a/lib/flow/scoreboard.json +++ b/lib/flow/scoreboard.json @@ -1,11 +1,13 @@ { - "total": 18, - "passed": 18, + "total": 24, + "passed": 24, "failed": 0, "suites": { - "basic": { "passed": 18, "total": 18 } + "basic": { "passed": 18, "total": 18 }, + "control": { "passed": 6, "total": 6 } }, "phases": { - "phase1": "in-progress" + "phase1": "done", + "phase2": "in-progress" } } diff --git a/lib/flow/scoreboard.md b/lib/flow/scoreboard.md index 6bf2efeb..19baeaa0 100644 --- a/lib/flow/scoreboard.md +++ b/lib/flow/scoreboard.md @@ -1,6 +1,6 @@ # flow-on-sx Scoreboard -**All tests pass: 18 / 18 across 1 suite.** +**All tests pass: 24 / 24 across 2 suites.** `bash lib/flow/conformance.sh` @@ -9,6 +9,7 @@ | Suite | Passing | Covers | |-------|--------:|--------| | basic | 18 | Phase 1: single nodes, linear sequence, data-flow threading, defflow, parallel fan/join, nested composition, publish-shaped flow | +| control | 6 | Phase 2: `branch` conditional — true/false select, threaded predicate, full-node branches, nested 3-way, approval gate | ## Architecture @@ -34,8 +35,7 @@ capture the flow continuation directly. ## Phases -- [~] Phase 1 — Declarative DAG + sequential execution (combinators + 18 tests done; - `flow/start` done) -- [ ] Phase 2 — Control flow + error handling +- [x] Phase 1 — Declarative DAG + sequential execution (combinators + 18 tests, `flow/start`) +- [~] Phase 2 — Control flow + error handling (`branch` done) - [ ] Phase 3 — Suspend / resume (the showcase) - [ ] Phase 4 — Distributed nodes via fed-sx diff --git a/lib/flow/spec.sx b/lib/flow/spec.sx index 35e75150..324ad4cf 100644 --- a/lib/flow/spec.sx +++ b/lib/flow/spec.sx @@ -7,7 +7,7 @@ ;; Scheme interpreter so that Phase 3's `suspend` (call/cc) can capture the ;; flow continuation directly. ;; -;; Phase 1 combinators: +;; Phase 1 combinators (flow-combinators-src): ;; (flow-node f) — wrap a 1-arg procedure as a node (identity) ;; (flow-id input) — pass the upstream value through unchanged ;; (flow-const v) — node that ignores input and yields v @@ -15,15 +15,24 @@ ;; (parallel n ...) — fan input to every child, join results into a list ;; (SEQUENTIAL evaluation; true concurrency is Phase 3) ;; (defflow name body)— bind a named flow +;; +;; Phase 2 combinators (flow-control-src): +;; (branch pred then else) — pred on input selects then/else node +;; (`cond` is a Scheme special form, so the combinator is named `branch`) (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-syntax defflow\n (syntax-rules ()\n ((defflow nm body) (define nm body))))") +(define + flow-control-src + "(define (branch pred then else)\n (lambda (input) (if (pred input) (then input) (else input))))") + (define flow-load-combinators! (fn (env) (begin (scheme-eval-program (scheme-parse-all flow-combinators-src) env) + (scheme-eval-program (scheme-parse-all flow-control-src) env) env))) diff --git a/lib/flow/tests/control.sx b/lib/flow/tests/control.sx new file mode 100644 index 00000000..b4a7a046 --- /dev/null +++ b/lib/flow/tests/control.sx @@ -0,0 +1,53 @@ +;; lib/flow/tests/control.sx — Phase 2: control flow + error handling. + +(define flow-ctl-pass 0) +(define flow-ctl-fail 0) +(define flow-ctl-fails (list)) + +(define + flow-ctl-test + (fn + (name actual expected) + (if + (= actual expected) + (set! flow-ctl-pass (+ flow-ctl-pass 1)) + (begin + (set! flow-ctl-fail (+ flow-ctl-fail 1)) + (append! flow-ctl-fails {:name name :expected expected :actual actual}))))) + +(define flow-c (fn (src) (flow-run src))) +(define flow-cs (fn (src) (get (flow-run src) :scm-string))) + +;; ── branch ────────────────────────────────────────────────────── +(flow-ctl-test + "branch: true selects then" + (flow-c + "(flow/start (branch (lambda (x) (> x 0)) (lambda (x) (* x 100)) (lambda (x) (- 0 x))) 5)") + 500) +(flow-ctl-test + "branch: false selects else" + (flow-c + "(flow/start (branch (lambda (x) (> x 0)) (lambda (x) (* x 100)) (lambda (x) (- 0 x))) -3)") + 3) +(flow-ctl-test + "branch: predicate sees the threaded input" + (flow-c + "(flow/start (sequence (lambda (x) (+ x 1)) (branch (lambda (x) (> x 3)) (flow-const 100) (flow-const 0))) 3)") + 100) +(flow-ctl-test + "branch: branches are full nodes (sequence inside)" + (flow-c + "(flow/start (branch (lambda (x) (< x 10)) (sequence (lambda (x) (+ x 1)) (lambda (x) (* x 2))) (flow-const 0)) 4)") + 10) +(flow-ctl-test + "branch: nested branch (3-way sign)" + (flow-c + "(defflow sign (branch (lambda (x) (> x 0)) (flow-const 1) (branch (lambda (x) (< x 0)) (flow-const -1) (flow-const 0)))) (list (flow/start sign 7) (flow/start sign -7) (flow/start sign 0))") + (list 1 -1 0)) +(flow-ctl-test + "branch: publish-shaped approval gate" + (flow-cs + "(defflow publish (branch (lambda (post) (>= (string-length post) 3)) (lambda (post) (string-append post \" [published]\")) (lambda (post) (string-append post \" [rejected]\")))) (flow/start publish \"ok\")") + "ok [rejected]") + +(define flow-ctl-tests-run! (fn () {:total (+ flow-ctl-pass flow-ctl-fail) :passed flow-ctl-pass :failed flow-ctl-fail :fails flow-ctl-fails})) diff --git a/plans/flow-on-sx.md b/plans/flow-on-sx.md index d0dbf392..c2407f95 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` → **18/18** (Phase 1 in progress) +`bash lib/flow/conformance.sh` → **24/24** (Phase 1 done; Phase 2 in progress) ## Ground rules @@ -75,7 +75,8 @@ lib/flow/spec.sx lib/flow/runtime.sx lib/flow/store.sx ## Phase 2 — Control flow + error handling -- [ ] `cond` combinator — predicate selects branch +- [x] `cond` combinator — predicate selects branch (named `branch`; `cond` is a + Scheme special form). `(branch pred then else)` — 6 tests. - [ ] `retry n [backoff]` — re-runs node up to n times on exception - [ ] `timeout ms` — bounds node execution - [ ] `try-catch` — exception handler with reified error