erlang: gen_server behaviour (+10 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

This commit is contained in:
2026-04-25 04:36:29 +00:00
parent 424b5ca472
commit 8717094e74
6 changed files with 153 additions and 10 deletions

View File

@@ -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)))

View File

@@ -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"},

View File

@@ -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 |

View File

@@ -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))

View File

@@ -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