erlang: gen_server behaviour (+10 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:
@@ -814,3 +814,58 @@
|
||||
(er-mk-atom mod)
|
||||
(er-mk-atom name)))))
|
||||
(er-apply-fun (get mod-env name) vs)))))
|
||||
|
||||
;; ── gen_server (OTP-lite) ───────────────────────────────────────
|
||||
;; A minimal gen_server behaviour — `start_link/2`, `call/2`, `cast/2`,
|
||||
;; `stop/1`, plus the receive loop dispatching `Mod:handle_call/3`,
|
||||
;; `Mod:handle_cast/2`, `Mod:handle_info/2`. Loaded into the user
|
||||
;; module registry on demand via `(er-load-gen-server!)`.
|
||||
(define
|
||||
er-gen-server-source
|
||||
"-module(gen_server).
|
||||
start_link(Mod, Args) ->
|
||||
spawn(fun () ->
|
||||
case Mod:init(Args) of
|
||||
{ok, State} -> gen_server:loop(Mod, State);
|
||||
{stop, Reason} -> exit(Reason)
|
||||
end
|
||||
end).
|
||||
call(Pid, Req) ->
|
||||
Ref = make_ref(),
|
||||
Pid ! {'$gen_call', {self(), Ref}, Req},
|
||||
receive {Ref, Reply} -> Reply end.
|
||||
cast(Pid, Msg) ->
|
||||
Pid ! {'$gen_cast', Msg},
|
||||
ok.
|
||||
stop(Pid) ->
|
||||
gen_server:call(Pid, '$gen_stop').
|
||||
loop(Mod, State) ->
|
||||
receive
|
||||
{'$gen_call', {From, Ref}, '$gen_stop'} ->
|
||||
From ! {Ref, ok};
|
||||
{'$gen_call', {From, Ref}, Req} ->
|
||||
case Mod:handle_call(Req, From, State) of
|
||||
{reply, Reply, NewState} ->
|
||||
From ! {Ref, Reply},
|
||||
gen_server:loop(Mod, NewState);
|
||||
{noreply, NewState} ->
|
||||
gen_server:loop(Mod, NewState);
|
||||
{stop, Reason, Reply, NewState} ->
|
||||
From ! {Ref, Reply},
|
||||
exit(Reason)
|
||||
end;
|
||||
{'$gen_cast', Msg} ->
|
||||
case Mod:handle_cast(Msg, State) of
|
||||
{noreply, NewState} -> gen_server:loop(Mod, NewState);
|
||||
{stop, Reason, NewState} -> exit(Reason)
|
||||
end;
|
||||
Other ->
|
||||
case Mod:handle_info(Other, State) of
|
||||
{noreply, NewState} -> gen_server:loop(Mod, NewState);
|
||||
{stop, Reason, NewState} -> exit(Reason)
|
||||
end
|
||||
end.")
|
||||
|
||||
(define
|
||||
er-load-gen-server!
|
||||
(fn () (erlang-load-module er-gen-server-source)))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"language": "erlang",
|
||||
"total_pass": 415,
|
||||
"total": 415,
|
||||
"total_pass": 425,
|
||||
"total": 425,
|
||||
"suites": [
|
||||
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
|
||||
{"name":"parse","pass":52,"total":52,"status":"ok"},
|
||||
{"name":"eval","pass":231,"total":231,"status":"ok"},
|
||||
{"name":"eval","pass":241,"total":241,"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: 415 / 415 tests passing**
|
||||
**Total: 425 / 425 tests passing**
|
||||
|
||||
| | Suite | Pass | Total |
|
||||
|---|---|---|---|
|
||||
| ✅ | tokenize | 62 | 62 |
|
||||
| ✅ | parse | 52 | 52 |
|
||||
| ✅ | eval | 231 | 231 |
|
||||
| ✅ | eval | 241 | 241 |
|
||||
| ✅ | runtime | 39 | 39 |
|
||||
| ✅ | ring | 4 | 4 |
|
||||
| ✅ | ping-pong | 4 | 4 |
|
||||
|
||||
@@ -699,6 +699,75 @@
|
||||
(ev "Me = self(), spawn(fun () -> m9:work(Me) end), receive done -> ok end"))
|
||||
(er-mk-atom "ok"))
|
||||
|
||||
;; ── gen_server (OTP-lite) ──────────────────────────────────────
|
||||
(do
|
||||
(er-load-gen-server!)
|
||||
(erlang-load-module
|
||||
"-module(ctr).
|
||||
init(N) -> {ok, N}.
|
||||
handle_call(get, _F, S) -> {reply, S, S}.
|
||||
handle_call({set, V}, _F, _S) -> {reply, ok, V}.
|
||||
handle_call({add, K}, _F, S) -> {reply, S + K, S + K}.
|
||||
handle_cast(inc, S) -> {noreply, S + 1}.
|
||||
handle_cast(dec, S) -> {noreply, S - 1}.
|
||||
handle_cast({add, K}, S) -> {noreply, S + K}.
|
||||
handle_info(_M, S) -> {noreply, S}.")
|
||||
nil)
|
||||
|
||||
(er-eval-test "gen_server start + call get"
|
||||
(ev "P = gen_server:start_link(ctr, 10), gen_server:call(P, get)")
|
||||
10)
|
||||
|
||||
(er-eval-test "gen_server cast then call"
|
||||
(ev "P = gen_server:start_link(ctr, 0), gen_server:cast(P, inc), gen_server:cast(P, inc), gen_server:cast(P, inc), gen_server:call(P, get)")
|
||||
3)
|
||||
|
||||
(er-eval-test "gen_server call returns reply"
|
||||
(ev "P = gen_server:start_link(ctr, 5), gen_server:call(P, {add, 7})")
|
||||
12)
|
||||
|
||||
(er-eval-test "gen_server state mutation"
|
||||
(ev "P = gen_server:start_link(ctr, 5), gen_server:call(P, {set, 99}), gen_server:call(P, get)")
|
||||
99)
|
||||
|
||||
(er-eval-test "gen_server stop returns ok"
|
||||
(nm (ev "P = gen_server:start_link(ctr, 0), gen_server:stop(P)"))
|
||||
"ok")
|
||||
|
||||
(er-eval-test "gen_server cast returns ok immediately"
|
||||
(nm (ev "P = gen_server:start_link(ctr, 0), gen_server:cast(P, inc)"))
|
||||
"ok")
|
||||
|
||||
(er-eval-test "gen_server multi-state mutations"
|
||||
(ev "P = gen_server:start_link(ctr, 0), gen_server:cast(P, {add, 100}), gen_server:cast(P, dec), gen_server:cast(P, dec), gen_server:call(P, get)")
|
||||
98)
|
||||
|
||||
;; Stack server — exercises a different state shape.
|
||||
(do
|
||||
(erlang-load-module
|
||||
"-module(stk).
|
||||
init(_) -> {ok, []}.
|
||||
handle_call(pop, _F, []) -> {reply, empty, []};
|
||||
handle_call(pop, _F, [H | T]) -> {reply, {ok, H}, T};
|
||||
handle_call(peek, _F, []) -> {reply, empty, []};
|
||||
handle_call(peek, _F, [H | T]) -> {reply, {ok, H}, [H | T]};
|
||||
handle_call(size, _F, S) -> {reply, length(S), S}.
|
||||
handle_cast({push, V}, S) -> {noreply, [V | S]}.
|
||||
handle_info(_M, S) -> {noreply, S}.")
|
||||
nil)
|
||||
|
||||
(er-eval-test "stack push/pop"
|
||||
(ev "P = gen_server:start_link(stk, ignored), gen_server:cast(P, {push, 1}), gen_server:cast(P, {push, 2}), gen_server:cast(P, {push, 3}), gen_server:call(P, size)")
|
||||
3)
|
||||
|
||||
(er-eval-test "stack lifo"
|
||||
(ev "P = gen_server:start_link(stk, ignored), gen_server:cast(P, {push, 1}), gen_server:cast(P, {push, 2}), gen_server:cast(P, {push, 3}), {ok, V} = gen_server:call(P, pop), V")
|
||||
3)
|
||||
|
||||
(er-eval-test "stack empty pop"
|
||||
(nm (ev "P = gen_server:start_link(stk, ignored), gen_server:call(P, pop)"))
|
||||
"empty")
|
||||
|
||||
(define
|
||||
er-eval-test-summary
|
||||
(str "eval " er-eval-test-pass "/" er-eval-test-count))
|
||||
|
||||
@@ -486,10 +486,10 @@
|
||||
(er-apply-fun (get env name) vs)
|
||||
:else (er-apply-bif name vs)))
|
||||
(= (get fun-node :type) "remote")
|
||||
(er-apply-remote-bif
|
||||
(get (get fun-node :mod) :value)
|
||||
(get (get fun-node :fun) :value)
|
||||
(er-eval-args args env))
|
||||
(let
|
||||
((mod-name (er-resolve-call-name (get fun-node :mod) env "module"))
|
||||
(fn-name (er-resolve-call-name (get fun-node :fun) env "function")))
|
||||
(er-apply-remote-bif mod-name fn-name (er-eval-args args env)))
|
||||
:else
|
||||
(let
|
||||
((fv (er-eval-expr fun-node env)))
|
||||
@@ -509,6 +509,24 @@
|
||||
(range 0 (len args)))
|
||||
out)))
|
||||
|
||||
;; Resolve a remote call's module/function reference into a string.
|
||||
;; Atom AST nodes use their `:value` directly. For any other shape
|
||||
;; (typically a var or another expression), evaluate it and require
|
||||
;; the result to be an atom.
|
||||
(define
|
||||
er-resolve-call-name
|
||||
(fn
|
||||
(node env kind)
|
||||
(cond
|
||||
(= (get node :type) "atom") (get node :value)
|
||||
:else (let
|
||||
((v (er-eval-expr node env)))
|
||||
(if
|
||||
(er-atom? v)
|
||||
(get v :name)
|
||||
(error
|
||||
(str "Erlang: call " kind " must be an atom, got " (er-format-value v))))))))
|
||||
|
||||
;; ── fun values ───────────────────────────────────────────────────
|
||||
(define
|
||||
er-mk-fun
|
||||
|
||||
@@ -85,7 +85,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
|
||||
- [ ] `gen_server` behaviour (the big OTP win)
|
||||
- [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)
|
||||
- [ ] Registered processes: `register/2`, `whereis/1`
|
||||
|
||||
@@ -99,6 +99,7 @@ Core mapping:
|
||||
|
||||
_Newest first._
|
||||
|
||||
- **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.
|
||||
- **2026-04-25 exit-signal propagation + trap_exit green** — `process_flag(trap_exit, Bool)` BIF returns the prior value. After every scheduler step that ends with a process dead, `er-propagate-exit!` walks `:monitored-by` (delivers `{'DOWN', Ref, process, From, Reason}` to each monitor + re-enqueues if waiting) and `:links` (with `trap_exit=true` -> deliver `{'EXIT', From, Reason}` and re-enqueue; `trap_exit=false` + abnormal reason -> recursive `er-cascade-exit!`; normal reason without trap_exit -> no signal). `er-sched-step!` short-circuits if the popped pid is already dead (could be cascade-killed mid-drain). 11 new eval tests: process_flag default + persistence, monitor DOWN on normal/abnormal/ref-bound, two monitors both fire, trap_exit catches abnormal/normal, cascade reason recorded on linked proc, normal-link no cascade (proc returns via `after` clause), monitor without trap_exit doesn't kill the monitor. Total suite 386/386. `kill`-as-special-reason and `exit/2` (signal to another) deferred.
|
||||
|
||||
Reference in New Issue
Block a user