From e762cc2e327524ab3f03b2e045f6502a81bb4d79 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 6 Jun 2026 16:42:16 +0000 Subject: [PATCH] =?UTF-8?q?flow:=20timeout=20combinator=20=E2=80=94=20coop?= =?UTF-8?q?erative=20step=20budget=20+=207=20tests=20(Phase=202=20complete?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (timeout budget node) bounds a node deterministically: nodes opt in via (tick), budget ticks are allowed, the next raises flow-timeout. No scheduler/clock in pure SX so the budget is a step count, not wall-clock. Budgets nest and are per-run. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/flow/scoreboard.json | 8 ++++---- lib/flow/scoreboard.md | 6 +++--- lib/flow/spec.sx | 9 ++++++++- lib/flow/tests/control.sx | 36 ++++++++++++++++++++++++++++++++++++ plans/flow-on-sx.md | 10 +++++++--- 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/lib/flow/scoreboard.json b/lib/flow/scoreboard.json index 181eeb3a..8ed50f27 100644 --- a/lib/flow/scoreboard.json +++ b/lib/flow/scoreboard.json @@ -1,10 +1,10 @@ { - "total": 42, - "passed": 42, + "total": 49, + "passed": 49, "failed": 0, "suites": { "basic": { "passed": 18, "total": 18 }, - "control": { "passed": 24, "total": 24 } + "control": { "passed": 31, "total": 31 } }, - "phases": { "phase1": "done", "phase2": "in-progress" } + "phases": { "phase1": "done", "phase2": "done", "phase3": "pending" } } diff --git a/lib/flow/scoreboard.md b/lib/flow/scoreboard.md index dbaa9fb4..4432b438 100644 --- a/lib/flow/scoreboard.md +++ b/lib/flow/scoreboard.md @@ -1,6 +1,6 @@ # flow-on-sx Scoreboard -**All tests pass: 42 / 42 across 2 suites.** +**All tests pass: 49 / 49 across 2 suites.** `bash lib/flow/conformance.sh` @@ -9,7 +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 | 24 | Phase 2: `branch` (6); error model `fail`/`failed?`/`fail-reason` (6); `try-catch` (6); `retry n` re-running on raised exceptions (6) | +| control | 31 | Phase 2: `branch` (6); error model `fail`/`failed?`/`fail-reason` (6); `try-catch` (6); `retry n` (6); `timeout` cooperative step budget (7) | ## Architecture @@ -36,6 +36,6 @@ capture the flow continuation directly. ## Phases - [x] Phase 1 — Declarative DAG + sequential execution (combinators + 18 tests, `flow/start`) -- [~] Phase 2 — Control flow + error handling (`branch`, error model, `try-catch`, `retry` done) +- [x] Phase 2 — Control flow + error handling (branch, error model, try-catch, retry, timeout) - [ ] 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 3ff6a542..72e78ebe 100644 --- a/lib/flow/spec.sx +++ b/lib/flow/spec.sx @@ -28,6 +28,13 @@ ;; exception; the last attempt's exception propagates. Only RAISED exceptions ;; are retried — explicit (fail ...) values pass through unchanged. (Once a ;; node has suspended in Phase 3, retry does not re-run it; resume continues.) +;; (timeout budget node) — bound node by a COOPERATIVE STEP BUDGET. There is no +;; scheduler or wall clock in pure SX, so timeout is deterministic: a node opts +;; in by calling (tick) at safe points. `budget` ticks are allowed; the next +;; tick raises (quote flow-timeout) (catchable by try-catch). A node that never +;; ticks is unbounded. Budgets nest (save/restore) and are isolated per flow +;; run (fresh env per flow-make-env). +;; (tick) — consume one unit of the active timeout budget (define flow-combinators-src @@ -35,7 +42,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 (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)))") + "(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 (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-load-combinators! diff --git a/lib/flow/tests/control.sx b/lib/flow/tests/control.sx index 0e28c3e9..e4fc086a 100644 --- a/lib/flow/tests/control.sx +++ b/lib/flow/tests/control.sx @@ -140,4 +140,40 @@ "(define ctr 0) (flow/start (sequence (retry 5 (lambda (x) (set! ctr (+ ctr 1)) (fail (quote bad)))) (lambda (f) ctr)) 0)") 1) +;; ── timeout — cooperative step budget ─────────────────────────── +(flow-ctl-test + "timeout: work within budget completes" + (flow-c + "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 10 (lambda (x) (cd x))) (flow-const (quote timed-out))) 5)") + 99) +(flow-ctl-test + "timeout: work exceeding budget raises flow-timeout" + (flow-c + "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 10 (lambda (x) (cd x))) (flow-const (quote timed-out))) 20)") + "timed-out") +(flow-ctl-test + "timeout: exact budget boundary completes" + (flow-c + "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 5 (lambda (x) (cd x))) (flow-const (quote timed-out))) 5)") + 99) +(flow-ctl-test + "timeout: one tick over the budget raises" + (flow-c + "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 5 (lambda (x) (cd x))) (flow-const (quote timed-out))) 6)") + "timed-out") +(flow-ctl-test + "timeout: the raised error is identifiable" + (flow-c + "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 2 (lambda (x) (cd x))) (lambda (e) e)) 9)") + "flow-timeout") +(flow-ctl-test + "timeout: a node that never ticks is unbounded" + (flow-c "(flow/start (timeout 0 (lambda (x) (* x 2))) 5)") + 10) +(flow-ctl-test + "timeout: budget is restored across sequential timeouts" + (flow-c + "(define (cd n) (if (<= n 0) 1 (begin (tick) (cd (- n 1))))) (flow/start (sequence (timeout 4 (lambda (x) (cd x))) (timeout 4 (lambda (x) (cd 3))) (lambda (x) (begin (tick) (+ x 100)))) 3)") + 101) + (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 b913051f..ba10374f 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` → **42/42** (Phase 1 done; Phase 2 in progress) +`bash lib/flow/conformance.sh` → **49/49** (Phases 1-2 done; Phase 3 next) ## Ground rules @@ -80,13 +80,17 @@ lib/flow/spec.sx lib/flow/runtime.sx lib/flow/store.sx - [x] `retry n` — re-runs node up to n attempts on a raised exception; last exception propagates. Only raised exceptions are retried — `(fail ...)` values pass through. 6 tests. (Backoff deferred: no wall clock in pure SX.) -- [ ] `timeout ms` — bounds node execution +- [x] `timeout budget` — bounds node execution via a **cooperative step budget** + (deterministic; no scheduler/clock in pure SX). Nodes opt in via `(tick)`; + `budget` ticks allowed, the next raises `flow-timeout`. Non-ticking nodes are + unbounded; budgets nest. 7 tests. - [x] `try-catch` — exception handler with reified error: `(try-catch node handler)` runs node; on raise, calls `(handler error)` and returns its value. 6 tests. - [x] error model — exceptions vs explicit `(fail reason)` results: `fail`/`failed?`/ `fail-reason` produce/inspect failure values that flow downstream as data (distinct from raised exceptions caught by retry/try-catch). 6 tests. -- [ ] `lib/flow/tests/control.sx` — 25+ cases: each combinator + composition +- [x] `lib/flow/tests/control.sx` — 31 cases: branch, error model, try-catch, + retry, timeout + compositions ## Phase 3 — Suspend / resume (the showcase)