-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, []).