;; lib/flow/tests/combinators.sx — Phase 5: combinator library (tap, recover, map-flow, iteration). (define flow-cmb-pass 0) (define flow-cmb-fail 0) (define flow-cmb-fails (list)) (define flow-cmb-test (fn (name actual expected) (if (= actual expected) (set! flow-cmb-pass (+ flow-cmb-pass 1)) (begin (set! flow-cmb-fail (+ flow-cmb-fail 1)) (append! flow-cmb-fails {:name name :expected expected :actual actual}))))) (define flow-m (fn (src) (flow-run src))) ;; ── tap (side-effecting pass-through) ─────────────────────────── (flow-cmb-test "tap: returns input unchanged" (flow-m "(flow/start (tap (lambda (x) (* x 999))) 7)") 7) (flow-cmb-test "tap: runs the side effect" (flow-m "(define seen 0) (flow/start (tap (lambda (x) (set! seen x))) 42) seen") 42) (flow-cmb-test "tap: value flows on while the effect observes it" (flow-m "(define log 0) (flow/start (sequence (lambda (x) (+ x 1)) (tap (lambda (x) (set! log x))) (lambda (x) (* x 2))) 10) (list log (flow/result 1))") (list 11 22)) ;; ── recover (fail-value counterpart of try-catch) ─────────────── (flow-cmb-test "recover: passes a non-fail value through" (flow-m "(flow/start (recover (lambda (x) (* x 2)) (lambda (r) -1)) 5)") 10) (flow-cmb-test "recover: handles a fail value via the reason" (flow-m "(flow/start (recover (lambda (x) (fail (quote too-small))) (lambda (r) (list (quote recovered) r))) 1)") (list "recovered" "too-small")) (flow-cmb-test "recover: handler can supply a default value" (flow-m "(flow/start (sequence (recover (lambda (x) (if (> x 0) x (fail (quote neg))) ) (flow-const 0)) (lambda (x) (* x 10))) -3)") 0) (flow-cmb-test "recover: does not catch raised exceptions (those are try-catch's job)" (flow-m "(flow/start (try-catch (recover (lambda (x) (raise (quote boom))) (flow-const 0)) (lambda (e) e)) 1)") "boom") ;; ── map-flow (run a node over a list, join) ───────────────────── (flow-cmb-test "map-flow: applies the node to each item" (flow-m "(flow/start (map-flow (lambda (x) (* x x))) (list 1 2 3 4))") (list 1 4 9 16)) (flow-cmb-test "map-flow: empty list joins to empty" (flow-m "(flow/start (map-flow (lambda (x) (+ x 1))) (list))") (list)) (flow-cmb-test "map-flow: each item runs an independent sub-flow" (flow-m "(flow/start (map-flow (sequence (lambda (x) (+ x 1)) (lambda (x) (* x 2)))) (list 0 4 9))") (list 2 10 20)) (flow-cmb-test "map-flow: composes — fan over a list then reduce the join" (flow-m "(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}))