fed-sx-types Phase 5: flow-on-erlang engine core (next/flow/)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 49s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 49s
A native Erlang-on-SX durable workflow engine, so the fed-sx kernel can
fan activities out into business flows in its own runtime — no cross-
guest FFI to the Scheme lib/flow, no marshalling, no Scheme dependency.
The seed of a real engine (chosen over bridging Scheme flow) that can
later supersede it for substrate use.
- flow.erl — the deterministic-replay driver. Same durability model as
the Scheme engine (re-run from the top; effects go through suspend;
the replay log is plain [{Tag,Value}] data, restart-ready), but
adapted to three hard runtime constraints: no re-enterable
continuation, no process dictionary, and a blocking receive inside a
`try` deadlocks the cooperative scheduler. Resolution: thread the log
through a railway-style context and make suspend SHORT-CIRCUIT (like a
fail value) instead of throwing — purely functional, sidesteps all
three. Ctx = {flow_cont,V,Log} | {flow_susp,Tag,Log}.
- flow_spec.erl — combinator algebra mirrored from lib/flow/spec.sx:
leaves, sequence/parallel/map_flow, flow_while/flow_until, branch,
railway fail/recover/attempt, tap, try_catch/retry.
- flow_store.erl — durable gen_server: named-flow registry + instance
table + start/resume/status. Drives the pure flow from handle_call,
so no gen_server:call is ever inside the replay try-path.
Gate: next/flow/conformance.sh — 34/34. lib/erlang untouched (771/771).
See next/flow/README.md for the model + why railway threading.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
84
next/flow/flow.erl
Normal file
84
next/flow/flow.erl
Normal file
@@ -0,0 +1,84 @@
|
||||
-module(flow).
|
||||
-export([drive/3, run/2,
|
||||
cont/2, susp/2, is_susp/1, ctx_value/1, ctx_log/1,
|
||||
suspend/1, log_lookup/2]).
|
||||
|
||||
%% flow-on-erlang — the deterministic-replay core. A native Erlang port
|
||||
%% of the Scheme flow engine (lib/flow), so the fed-sx kernel can fan
|
||||
%% activities out into durable business flows in its own runtime (no
|
||||
%% cross-guest FFI).
|
||||
%%
|
||||
%% Durability model — identical semantics to the Scheme engine, but
|
||||
%% adapted to this Erlang-on-SX runtime, which has three hard
|
||||
%% constraints the Scheme host doesn't: no escape continuation that can
|
||||
%% be re-entered, no process dictionary, and (critically) a blocking
|
||||
%% `receive` / `gen_server:call` inside a `try` deadlocks the
|
||||
%% cooperative scheduler. So instead of the Scheme engine's
|
||||
%% mutable-global + call/cc-escape, the replay log is THREADED through a
|
||||
%% railway-style context and `suspend` SHORT-CIRCUITS (like a fail
|
||||
%% value) rather than throwing. No ambient state, no throw, no
|
||||
%% gen_server — purely functional, which sidesteps every constraint.
|
||||
%%
|
||||
%% A node is `fun(Ctx) -> Ctx`. A Ctx is one of:
|
||||
%% {flow_cont, Value, Log} — running; Value is the current value
|
||||
%% {flow_susp, Tag, Log} — short-circuited at suspend Tag
|
||||
%% Log is the replay log: [{Tag, ResolvedValue}, ...]. Combinators
|
||||
%% (flow_spec) thread Ctx and pass {flow_susp,...} straight through, so
|
||||
%% once a flow suspends nothing downstream runs.
|
||||
%%
|
||||
%% suspend/1 is the load-bearing primitive: a node that, given the
|
||||
%% running Ctx, looks Tag up in the replay log. A hit replaces the
|
||||
%% current value with the logged value and continues; a miss
|
||||
%% short-circuits to {flow_susp, Tag, Log}. ALL effects/non-determinism
|
||||
%% go through a suspend node so they run once — in the driver, between
|
||||
%% drives — and their results are logged, never re-run on replay. Tags
|
||||
%% must be unique and deterministic across replays.
|
||||
|
||||
%% ── context constructors / accessors ────────────────────────────
|
||||
|
||||
cont(Value, Log) -> {flow_cont, Value, Log}.
|
||||
susp(Tag, Log) -> {flow_susp, Tag, Log}.
|
||||
|
||||
is_susp({flow_susp, _, _}) -> true;
|
||||
is_susp(_) -> false.
|
||||
|
||||
ctx_value({flow_cont, Value, _}) -> Value;
|
||||
ctx_value({flow_susp, _, _}) -> undefined.
|
||||
|
||||
ctx_log({flow_cont, _, Log}) -> Log;
|
||||
ctx_log({flow_susp, _, Log}) -> Log.
|
||||
|
||||
%% ── suspend node ────────────────────────────────────────────────
|
||||
|
||||
suspend(Tag) ->
|
||||
fun (Ctx) ->
|
||||
case Ctx of
|
||||
{flow_susp, _, _} -> Ctx;
|
||||
{flow_cont, _Value, Log} ->
|
||||
case log_lookup(Tag, Log) of
|
||||
{ok, Resolved} -> {flow_cont, Resolved, Log};
|
||||
miss -> {flow_susp, Tag, Log}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
log_lookup(_, []) -> miss;
|
||||
log_lookup(Tag, [{Tag, Value} | _]) -> {ok, Value};
|
||||
log_lookup(Tag, [_ | Rest]) -> log_lookup(Tag, Rest).
|
||||
|
||||
%% ── driver ──────────────────────────────────────────────────────
|
||||
|
||||
%% drive(Flow, Input, Log) — run Flow under the replay Log.
|
||||
%% {flow_done, Result} — flow completed
|
||||
%% {flow_suspended, Tag} — flow short-circuited at an unresolved
|
||||
%% suspend; the driver resolves Tag, appends
|
||||
%% {Tag, Value} to Log, and re-drives.
|
||||
drive(Flow, Input, Log) ->
|
||||
case Flow({flow_cont, Input, Log}) of
|
||||
{flow_cont, Result, _} -> {flow_done, Result};
|
||||
{flow_susp, Tag, _} -> {flow_suspended, Tag}
|
||||
end.
|
||||
|
||||
%% run(Flow, Input) — drive with an empty replay log.
|
||||
run(Flow, Input) ->
|
||||
drive(Flow, Input, []).
|
||||
Reference in New Issue
Block a user