flow: railway attempt combinator — fail-value short-circuit + 10 tests (Phase 6)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 55s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 55s
(attempt n1 n2 ...) threads like sequence but stops at the first node returning a (fail ...) value, returning that failure. Makes the fail/recover error model compose into validation/ETL pipelines (railway-oriented). 132/132 across 8 suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ SUITES=(
|
||||
"distributed flow-dist-tests-run! lib/flow/tests/distributed.sx"
|
||||
"api flow-api-tests-run! lib/flow/tests/api.sx"
|
||||
"combinators flow-cmb-tests-run! lib/flow/tests/combinators.sx"
|
||||
"railway flow-rail-tests-run! lib/flow/tests/railway.sx"
|
||||
)
|
||||
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"total": 122,
|
||||
"passed": 122,
|
||||
"total": 132,
|
||||
"passed": 132,
|
||||
"failed": 0,
|
||||
"suites": {
|
||||
"basic": { "passed": 18, "total": 18 },
|
||||
@@ -9,7 +9,8 @@
|
||||
"recovery": { "passed": 8, "total": 8 },
|
||||
"distributed": { "passed": 19, "total": 19 },
|
||||
"api": { "passed": 12, "total": 12 },
|
||||
"combinators": { "passed": 17, "total": 17 }
|
||||
"combinators": { "passed": 17, "total": 17 },
|
||||
"railway": { "passed": 10, "total": 10 }
|
||||
},
|
||||
"phases": { "phase1": "done", "phase2": "done", "phase3": "done", "phase4": "done", "phase5": "done" }
|
||||
"phases": { "phase1": "done", "phase2": "done", "phase3": "done", "phase4": "done", "phase5": "done", "phase6": "done" }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# flow-on-sx Scoreboard
|
||||
|
||||
**All tests pass: 122 / 122 across 7 suites. Phases 1-5 complete.**
|
||||
**All tests pass: 132 / 132 across 8 suites. Phases 1-6 complete.**
|
||||
|
||||
`bash lib/flow/conformance.sh`
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
| 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 | 17 | Phase 5: `tap`, `recover` (fail-value), `map-flow` fan-over-list, `flow-while`/`flow-until` bounded iteration |
|
||||
| railway | 10 | Phase 6: `attempt` — fail-value short-circuiting sequence + recover rejoin |
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
;; branch / fail / failed? / fail-reason / try-catch / retry / timeout / tick
|
||||
;; tap (Phase 5): side-effecting pass-through (returns input unchanged).
|
||||
;; recover (Phase 5): the fail-VALUE counterpart of try-catch.
|
||||
;; attempt (Phase 6): railway sequence — thread nodes left-to-right but stop at
|
||||
;; the first node that returns a (fail ...) value, returning that failure.
|
||||
;;
|
||||
;; Phase 3 suspend core (flow-suspend-src):
|
||||
;; The guest Scheme's call/cc is ESCAPE-ONLY (re-invoking a captured k after it
|
||||
@@ -42,7 +44,7 @@
|
||||
|
||||
(define
|
||||
flow-control-src
|
||||
"(define (branch pred then else)\n (lambda (input) (if (pred input) (then input) (else input))))\n (define (fail reason) (list (quote flow-fail) reason))\n (define (failed? x) (and (pair? x) (eq? (car x) (quote flow-fail))))\n (define (fail-reason x) (car (cdr x)))\n (define (recover node handler)\n (lambda (input)\n (let ((r (node input)))\n (if (failed? r) (handler (fail-reason r)) r))))\n (define (tap effect)\n (lambda (input) (begin (effect input) input)))\n (define (try-catch node handler)\n (lambda (input) (guard (e (#t (handler e))) (node input))))\n (define (flow-retry-step n node input)\n (guard (e (#t (if (<= n 1) (raise e) (flow-retry-step (- n 1) node input))))\n (node input)))\n (define (retry n node) (lambda (input) (flow-retry-step n node input)))\n (define flow-timeout-budget -1)\n (define (tick)\n (if (< flow-timeout-budget 0)\n 0\n (begin\n (set! flow-timeout-budget (- flow-timeout-budget 1))\n (if (< flow-timeout-budget 0)\n (raise (quote flow-timeout))\n flow-timeout-budget))))\n (define (timeout budget node)\n (lambda (input)\n (let ((saved flow-timeout-budget))\n (set! flow-timeout-budget budget)\n (guard (e (#t (begin (set! flow-timeout-budget saved) (raise e))))\n (let ((result (node input)))\n (set! flow-timeout-budget saved)\n result)))))")
|
||||
"(define (branch pred then else)\n (lambda (input) (if (pred input) (then input) (else input))))\n (define (fail reason) (list (quote flow-fail) reason))\n (define (failed? x) (and (pair? x) (eq? (car x) (quote flow-fail))))\n (define (fail-reason x) (car (cdr x)))\n (define (recover node handler)\n (lambda (input)\n (let ((r (node input)))\n (if (failed? r) (handler (fail-reason r)) r))))\n (define (tap effect)\n (lambda (input) (begin (effect input) input)))\n (define (flow-attempt-step ns v)\n (if (failed? v)\n v\n (if (null? ns) v (flow-attempt-step (cdr ns) ((car ns) v)))))\n (define attempt (lambda ns (lambda (input) (flow-attempt-step ns input))))\n (define (try-catch node handler)\n (lambda (input) (guard (e (#t (handler e))) (node input))))\n (define (flow-retry-step n node input)\n (guard (e (#t (if (<= n 1) (raise e) (flow-retry-step (- n 1) node input))))\n (node input)))\n (define (retry n node) (lambda (input) (flow-retry-step n node input)))\n (define flow-timeout-budget -1)\n (define (tick)\n (if (< flow-timeout-budget 0)\n 0\n (begin\n (set! flow-timeout-budget (- flow-timeout-budget 1))\n (if (< flow-timeout-budget 0)\n (raise (quote flow-timeout))\n flow-timeout-budget))))\n (define (timeout budget node)\n (lambda (input)\n (let ((saved flow-timeout-budget))\n (set! flow-timeout-budget budget)\n (guard (e (#t (begin (set! flow-timeout-budget saved) (raise e))))\n (let ((result (node input)))\n (set! flow-timeout-budget saved)\n result)))))")
|
||||
|
||||
(define
|
||||
flow-suspend-src
|
||||
|
||||
73
lib/flow/tests/railway.sx
Normal file
73
lib/flow/tests/railway.sx
Normal file
@@ -0,0 +1,73 @@
|
||||
;; lib/flow/tests/railway.sx — Phase 6: railway-oriented composition (attempt).
|
||||
|
||||
(define flow-rail-pass 0)
|
||||
(define flow-rail-fail 0)
|
||||
(define flow-rail-fails (list))
|
||||
|
||||
(define
|
||||
flow-rail-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! flow-rail-pass (+ flow-rail-pass 1))
|
||||
(begin
|
||||
(set! flow-rail-fail (+ flow-rail-fail 1))
|
||||
(append! flow-rail-fails {:name name :expected expected :actual actual})))))
|
||||
|
||||
(define flow-r (fn (src) (flow-run src)))
|
||||
|
||||
;; ── attempt — short-circuit on the first (fail ...) ─────────────
|
||||
(flow-rail-test
|
||||
"attempt: threads like sequence when nothing fails"
|
||||
(flow-r
|
||||
"(flow/start (attempt (lambda (x) (+ x 1)) (lambda (x) (* x 10))) 4)")
|
||||
50)
|
||||
(flow-rail-test
|
||||
"attempt: empty is identity"
|
||||
(flow-r "(flow/start (attempt) 7)")
|
||||
7)
|
||||
(flow-rail-test
|
||||
"attempt: returns the first failure"
|
||||
(flow-r
|
||||
"(failed? (flow/start (attempt (lambda (x) (fail (quote bad))) (lambda (x) (* x 10))) 4))")
|
||||
true)
|
||||
(flow-rail-test
|
||||
"attempt: the failure carries its reason"
|
||||
(flow-r
|
||||
"(fail-reason (flow/start (attempt (lambda (x) x) (lambda (x) (fail (quote rejected)))) 4))")
|
||||
"rejected")
|
||||
(flow-rail-test
|
||||
"attempt: nodes after a failure do not run"
|
||||
(flow-r
|
||||
"(define ran 0) (flow/start (attempt (lambda (x) (fail (quote stop))) (lambda (x) (begin (set! ran (+ ran 1)) x))) 0) ran")
|
||||
0)
|
||||
(flow-rail-test
|
||||
"attempt: a failed input short-circuits immediately"
|
||||
(flow-r
|
||||
"(define ran 0) (fail-reason (flow/start (attempt (lambda (x) (begin (set! ran (+ ran 1)) x))) (fail (quote pre))))")
|
||||
"pre")
|
||||
(flow-rail-test
|
||||
"attempt: middle failure halts the chain"
|
||||
(flow-r
|
||||
"(define ran 0) (flow/start (attempt (lambda (x) (+ x 1)) (lambda (x) (fail (quote mid))) (lambda (x) (begin (set! ran (+ ran 1)) x))) 5) ran")
|
||||
0)
|
||||
|
||||
;; ── attempt + recover (rejoin the happy track) ──────────────────
|
||||
(flow-rail-test
|
||||
"attempt + recover: recover turns a failure into a value"
|
||||
(flow-r
|
||||
"(flow/start (recover (attempt (lambda (x) (if (> x 0) x (fail (quote non-positive)))) (lambda (x) (* x 2))) (flow-const 0)) -5)")
|
||||
0)
|
||||
(flow-rail-test
|
||||
"attempt + recover: happy path passes recover through"
|
||||
(flow-r
|
||||
"(flow/start (recover (attempt (lambda (x) (if (> x 0) x (fail (quote non-positive)))) (lambda (x) (* x 2))) (flow-const 0)) 5)")
|
||||
10)
|
||||
(flow-rail-test
|
||||
"attempt: validation pipeline reports the failing stage"
|
||||
(flow-r
|
||||
"(defflow validate (attempt (lambda (s) (if (>= (string-length s) 3) s (fail (quote too-short)))) (lambda (s) (if (<= (string-length s) 8) s (fail (quote too-long)))) (lambda (s) (list (quote ok) (string-length s))))) (list (fail-reason (flow/start validate \"hi\")) (flow/start validate \"hello\"))")
|
||||
(list "too-short" (list "ok" 5)))
|
||||
|
||||
(define flow-rail-tests-run! (fn () {:total (+ flow-rail-pass flow-rail-fail) :passed flow-rail-pass :failed flow-rail-fail :fails flow-rail-fails}))
|
||||
@@ -16,7 +16,7 @@ federation extension via fed-sx for remote-node execution.
|
||||
|
||||
## Status (rolling)
|
||||
|
||||
`bash lib/flow/conformance.sh` → **122/122** (Phases 1-5 complete)
|
||||
`bash lib/flow/conformance.sh` → **132/132** (Phases 1-6 complete)
|
||||
|
||||
## Ground rules
|
||||
|
||||
@@ -149,6 +149,16 @@ something operators and authors actually use. Accumulation, not a rewrite.
|
||||
value while/until pred holds, capped at `max` steps (deterministic bound)
|
||||
- [x] `lib/flow/tests/api.sx` (12) + `lib/flow/tests/combinators.sx` (17)
|
||||
|
||||
## Phase 6 — Railway-oriented composition
|
||||
|
||||
Make the `(fail reason)` value channel compose into real validation/ETL pipelines.
|
||||
|
||||
- [x] `attempt` — like `sequence`, but short-circuits at the first node that returns
|
||||
a `(fail ...)` value, returning that failure (the railway track). Pairs with
|
||||
`recover` for the rejoin.
|
||||
- [x] `lib/flow/tests/railway.sx` — 10 cases: fail short-circuiting, no-run-after-
|
||||
failure, recover rejoin, validation pipeline reporting the failing stage
|
||||
|
||||
## Progress log
|
||||
|
||||
- **Phase 1 (combinators + sequential runtime).** Flow built as a Scheme prelude
|
||||
|
||||
Reference in New Issue
Block a user