erlang: supervisor one-for-one (+7 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
@@ -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)))
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user