From 47a59343a140ffaeb29ad9d996a821ae6ac54f89 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 25 Apr 2026 05:09:41 +0000 Subject: [PATCH] erlang: supervisor one-for-one (+7 tests) --- lib/erlang/runtime.sx | 55 ++++++++++++++++++++++++++++ lib/erlang/scoreboard.json | 6 +-- lib/erlang/scoreboard.md | 4 +- lib/erlang/tests/eval.sx | 75 ++++++++++++++++++++++++++++++++++++++ plans/erlang-on-sx.md | 3 +- 5 files changed, 137 insertions(+), 6 deletions(-) diff --git a/lib/erlang/runtime.sx b/lib/erlang/runtime.sx index b1f258f2..362a2a84 100644 --- a/lib/erlang/runtime.sx +++ b/lib/erlang/runtime.sx @@ -869,3 +869,58 @@ (define er-load-gen-server! (fn () (erlang-load-module er-gen-server-source))) + +;; ── supervisor (OTP-lite, one-for-one) ────────────────────────── +;; Each child spec is `{Id, StartFn}` — `StartFn/0` returns the +;; child's pid. The supervisor `process_flag(trap_exit, true)`, +;; links to every child, and on `{'EXIT', DeadPid, _}` calls the +;; matching `StartFn` to bring up a fresh replacement. Strategy is +;; one-for-one: only the dead child restarts; siblings keep running. +(define + er-supervisor-source + "-module(supervisor). + start_link(Mod, Args) -> + spawn(fun () -> + process_flag(trap_exit, true), + case Mod:init(Args) of + {ok, ChildSpecs} -> + Children = lists:map( + fun (Spec) -> supervisor:start_child(Spec) end, + ChildSpecs), + supervisor:loop(Children) + end + end). + start_child({Id, StartFn}) -> + P = StartFn(), + link(P), + {Id, StartFn, P}. + which_children(Sup) -> + Sup ! {'$sup_which', self()}, + receive {'$sup_children', Cs} -> Cs end. + stop(Sup) -> + Sup ! '$sup_stop', + ok. + loop(Children) -> + receive + {'EXIT', Dead, _Reason} -> + supervisor:loop(supervisor:restart(Children, Dead)); + {'$sup_which', From} -> + From ! {'$sup_children', Children}, + supervisor:loop(Children); + '$sup_stop' -> + ok + end. + restart([], _) -> []; + restart([{Id, SF, P} | T], Dead) -> + case P =:= Dead of + true -> + NewP = SF(), + link(NewP), + [{Id, SF, NewP} | T]; + false -> + [{Id, SF, P} | supervisor:restart(T, Dead)] + end.") + +(define + er-load-supervisor! + (fn () (erlang-load-module er-supervisor-source))) diff --git a/lib/erlang/scoreboard.json b/lib/erlang/scoreboard.json index b60173dc..28db7ad5 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,11 +1,11 @@ { "language": "erlang", - "total_pass": 425, - "total": 425, + "total_pass": 432, + "total": 432, "suites": [ {"name":"tokenize","pass":62,"total":62,"status":"ok"}, {"name":"parse","pass":52,"total":52,"status":"ok"}, - {"name":"eval","pass":241,"total":241,"status":"ok"}, + {"name":"eval","pass":248,"total":248,"status":"ok"}, {"name":"runtime","pass":39,"total":39,"status":"ok"}, {"name":"ring","pass":4,"total":4,"status":"ok"}, {"name":"ping-pong","pass":4,"total":4,"status":"ok"}, diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index ee7e06fd..54747a40 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,12 +1,12 @@ # Erlang-on-SX Scoreboard -**Total: 425 / 425 tests passing** +**Total: 432 / 432 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | tokenize | 62 | 62 | | ✅ | parse | 52 | 52 | -| ✅ | eval | 241 | 241 | +| ✅ | eval | 248 | 248 | | ✅ | runtime | 39 | 39 | | ✅ | ring | 4 | 4 | | ✅ | ping-pong | 4 | 4 | diff --git a/lib/erlang/tests/eval.sx b/lib/erlang/tests/eval.sx index 93563c28..dc0a8260 100644 --- a/lib/erlang/tests/eval.sx +++ b/lib/erlang/tests/eval.sx @@ -768,6 +768,81 @@ (nm (ev "P = gen_server:start_link(stk, ignored), gen_server:call(P, pop)")) "empty") +;; ── supervisor (one-for-one) ──────────────────────────────────── +(do + (er-load-supervisor!) + (erlang-load-module + "-module(echoer). + start() -> spawn(fun () -> echoer:loop() end). + loop() -> + receive + {ping, From} -> From ! pong, echoer:loop(); + die -> exit(killed) + end.") + nil) + +(er-eval-test "sup starts children" + (do + (erlang-load-module + "-module(sup1). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.") + (ev "Sup = supervisor:start_link(sup1, []), receive after 5 -> ok end, length(supervisor:which_children(Sup))")) + 1) + +(er-eval-test "sup multiple children" + (do + (erlang-load-module + "-module(sup2). + init(_) -> {ok, [ + {w1, fun () -> echoer:start() end}, + {w2, fun () -> echoer:start() end}, + {w3, fun () -> echoer:start() end} + ]}.") + (ev "Sup = supervisor:start_link(sup2, []), receive after 5 -> ok end, length(supervisor:which_children(Sup))")) + 3) + +(er-eval-test "sup child responds" + (do + (erlang-load-module + "-module(sup3). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.") + (nm (ev "Sup = supervisor:start_link(sup3, []), receive after 5 -> ok end, [{_, _, P1} | _] = supervisor:which_children(Sup), P1 ! {ping, self()}, receive pong -> ok end"))) + "ok") + +(er-eval-test "sup restarts on exit" + (do + (erlang-load-module + "-module(sup4). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.") + (nm + (ev "Sup = supervisor:start_link(sup4, []), receive after 5 -> ok end, [{_, _, P1} | _] = supervisor:which_children(Sup), P1 ! die, receive after 5 -> ok end, [{_, _, P2} | _] = supervisor:which_children(Sup), P1 =/= P2"))) + "true") + +(er-eval-test "sup restarted child works" + (do + (erlang-load-module + "-module(sup5). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.") + (nm + (ev "Sup = supervisor:start_link(sup5, []), receive after 5 -> ok end, [{_, _, P1} | _] = supervisor:which_children(Sup), P1 ! die, receive after 5 -> ok end, [{_, _, P2} | _] = supervisor:which_children(Sup), P2 ! {ping, self()}, receive pong -> ok end"))) + "ok") + +(er-eval-test "sup one-for-one isolates failures" + (do + (erlang-load-module + "-module(sup6). + init(_) -> {ok, [ + {w1, fun () -> echoer:start() end}, + {w2, fun () -> echoer:start() end} + ]}.") + (nm + (ev "Sup = supervisor:start_link(sup6, []), receive after 5 -> ok end, [{_, _, P1}, {_, _, P2}] = supervisor:which_children(Sup), P1 ! die, receive after 5 -> ok end, [{_, _, _NewP1}, {_, _, P2Again}] = supervisor:which_children(Sup), P2 =:= P2Again"))) + "true") + +(er-eval-test "sup stop" + (nm + (do + (erlang-load-module + "-module(sup7). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.") + (ev "Sup = supervisor:start_link(sup7, []), receive after 5 -> ok end, supervisor:stop(Sup)"))) + "ok") + (define er-eval-test-summary (str "eval " er-eval-test-pass "/" er-eval-test-count)) diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index ace6a470..9f8db41a 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -86,7 +86,7 @@ Core mapping: ### Phase 5 — modules + OTP-lite - [x] `-module(M).` loading, `M:F(...)` calls across modules — **10 new eval tests**; multi-arity, sibling calls, cross-module dispatch via `er-modules` registry - [x] `gen_server` behaviour (the big OTP win) — **10 new eval tests**; counter + LIFO stack callback modules driven via `gen_server:start_link/call/cast/stop` -- [ ] `supervisor` (simple one-for-one) +- [x] `supervisor` (simple one-for-one) — **7 new eval tests**; trap_exit-based restart loop; child specs are `{Id, StartFn}` pairs - [ ] Registered processes: `register/2`, `whereis/1` ### Phase 6 — the rest @@ -99,6 +99,7 @@ Core mapping: _Newest first._ +- **2026-04-25 supervisor (one-for-one) green** — `er-supervisor-source` in `lib/erlang/runtime.sx` is the canonical Erlang text of a minimal supervisor; `er-load-supervisor!` registers it. Implements `start_link(Mod, Args)` (sup process traps exits, calls `Mod:init/1` to get child-spec list, runs `start_child/1` for each which links the spawned pid back to itself), `which_children/1`, `stop/1`. Receive loop dispatches on `{'EXIT', Dead, _Reason}` (restarts only the dead child via `restart/2`, keeps siblings — proper one-for-one), `{'$sup_which', From}` (returns child list), `'$sup_stop'`. Child specs are `{Id, StartFn}` where `StartFn/0` returns the new child's pid. 7 new eval tests: `which_children` for 1- and 3-child sup, child responds to ping, killed child restarted with fresh pid, restarted child still functional, one-for-one isolation (siblings keep their pids), stop returns ok. Total suite 432/432. - **2026-04-25 gen_server (OTP-lite) green** — `er-gen-server-source` in `lib/erlang/runtime.sx` is the canonical Erlang text of the behaviour; `er-load-gen-server!` registers it in the user-module table. Implements `start_link/2`, `call/2` (sync via `make_ref` + selective `receive {Ref, Reply}`), `cast/2` (async fire-and-forget returning `ok`), `stop/1`, and the receive loop dispatching `{'$gen_call', {From, Ref}, Req}` → `Mod:handle_call/3`, `{'$gen_cast', Msg}` → `Mod:handle_cast/2`, anything else → `Mod:handle_info/2`. handle_call reply tuples supported: `{reply, R, S}`, `{noreply, S}`, `{stop, R, Reply, S}`. handle_cast/info: `{noreply, S}`, `{stop, R, S}`. `Mod:F` and `M:F` where `M` is a runtime variable now work via new `er-resolve-call-name` (was bug: passed unevaluated AST node `:value` to remote dispatch). 10 new eval tests: counter callback module (start/call/cast/stop, repeated state mutations), LIFO stack callback module (`{push, V}` cast, pop returns `{ok, V}` or `empty`, size). Total suite 425/425. - **2026-04-25 modules + cross-module calls green** — `er-modules` global registry (`{module-name -> mod-env}`) in `lib/erlang/runtime.sx`. `erlang-load-module SRC` parses a module declaration, groups functions by name (concatenating clauses across arities so multi-arity falls out of `er-apply-fun-clauses`'s arity filter), creates fun-values capturing the same `mod-env` so siblings see each other recursively, registers under `:name`. `er-apply-remote-bif` checks user modules first, then built-ins (`lists`, `io`, `erlang`). `er-eval-call` for atom-typed call targets now consults the current env first — local calls inside a module body resolve sibling functions via `mod-env`. Undefined cross-module call raises `error({undef, Mod, Fun})`. 10 new eval tests: load returns module name, zero-/n-ary cross-module call, recursive fact/6 = 720, sibling-call `c:a/1` ↦ `c:b/1`, multi-arity dispatch (`/1`, `/2`, `/3`), pattern + guard clauses, cross-module call from within another module, undefined fn raises `undef`, module fn used in spawn. Total suite 415/415. - **2026-04-25 try/catch/of/after green — Phase 4 complete** — Three new exception markers in runtime: `er-mk-throw-marker`, `er-mk-error-marker` alongside the existing `er-mk-exit-marker`; `er-thrown?`, `er-errored?` predicates. `throw/1` and `error/1` BIFs raise their respective markers. Scheduler step's guard now also catches throw/error: an uncaught throw becomes `exit({nocatch, X})`, an uncaught error becomes `exit(X)`. `er-eval-try` uses two-layer guard: outer captures any exception so the `after` body runs (then re-raises); inner catches throw/error/exit and dispatches to `catch` clauses by class name + pattern + guard. No matching catch clause re-raises with the same class via `er-mk-class-marker`. `of` clauses run on success; no-match raises `error({try_clause, V})`. 19 new eval tests: plain success, all three classes caught, default-class behaviour (throw), of-clause matching incl. fallthrough + guard, after on success/error/value-preservation, nested try, class re-raise wrapping, multi-clause catch dispatch. Total suite 405/405. **Phase 4 complete — Phase 5 (modules + OTP-lite) is next.** Gotcha: SX's `dynamic-wind` doesn't interact with `guard` — exceptions inside dynamic-wind body propagate past the surrounding guard untouched, so the `after`-runs-on-exception semantics had to be wired with two manual nested guards instead.