diff --git a/lib/erlang/tests/programs/echo.sx b/lib/erlang/tests/programs/echo.sx new file mode 100644 index 00000000..d8afb71e --- /dev/null +++ b/lib/erlang/tests/programs/echo.sx @@ -0,0 +1,140 @@ +;; Echo server — minimal classic Erlang server. Receives {From, Msg} +;; and sends Msg back to From, then loops. `stop` ends the server. + +(define er-echo-test-count 0) +(define er-echo-test-pass 0) +(define er-echo-test-fails (list)) + +(define + er-echo-test + (fn + (name actual expected) + (set! er-echo-test-count (+ er-echo-test-count 1)) + (if + (= actual expected) + (set! er-echo-test-pass (+ er-echo-test-pass 1)) + (append! er-echo-test-fails {:actual actual :expected expected :name name})))) + +(define echo-ev erlang-eval-ast) + +(define + er-echo-server-src + "EchoSrv = fun () -> + Loop = fun () -> + receive + {From, Msg} -> From ! Msg, Loop(); + stop -> ok + end + end, + Loop() + end") + +;; Single round-trip with an atom. +(er-echo-test + "atom round-trip" + (get + (echo-ev + (str + er-echo-server-src + ", Me = self(), + Echo = spawn(EchoSrv), + Echo ! {Me, hello}, + receive R -> Echo ! stop, R end")) + :name) + "hello") + +;; Number round-trip. +(er-echo-test + "number round-trip" + (echo-ev + (str + er-echo-server-src + ", Me = self(), + Echo = spawn(EchoSrv), + Echo ! {Me, 42}, + receive R -> Echo ! stop, R end")) + 42) + +;; Tuple round-trip — pattern-match the reply to extract V. +(er-echo-test + "tuple round-trip" + (echo-ev + (str + er-echo-server-src + ", Me = self(), + Echo = spawn(EchoSrv), + Echo ! {Me, {ok, 7}}, + receive {ok, V} -> Echo ! stop, V end")) + 7) + +;; List round-trip. +(er-echo-test + "list round-trip" + (echo-ev + (str + er-echo-server-src + ", Me = self(), + Echo = spawn(EchoSrv), + Echo ! {Me, [1, 2, 3]}, + receive [H | _] -> Echo ! stop, H end")) + 1) + +;; Multiple sequential round-trips. +(er-echo-test + "three round-trips" + (echo-ev + (str + er-echo-server-src + ", Me = self(), + Echo = spawn(EchoSrv), + Echo ! {Me, 10}, A = receive Ra -> Ra end, + Echo ! {Me, 20}, B = receive Rb -> Rb end, + Echo ! {Me, 30}, C = receive Rc -> Rc end, + Echo ! stop, + A + B + C")) + 60) + +;; Two clients sharing one echo server. Each gets its own reply. +(er-echo-test + "two clients" + (get + (echo-ev + (str + er-echo-server-src + ", Me = self(), + Echo = spawn(EchoSrv), + Client = fun (Tag) -> + spawn(fun () -> + Echo ! {self(), Tag}, + receive R -> Me ! {got, R} end + end) + end, + Client(a), + Client(b), + receive {got, _} -> ok end, + receive {got, _} -> ok end, + Echo ! stop, + finished")) + :name) + "finished") + +;; Echo via io trace — verify each message round-trips through. +(er-echo-test + "trace 4 messages" + (do + (er-io-flush!) + (echo-ev + (str + er-echo-server-src + ", Me = self(), + Echo = spawn(EchoSrv), + Send = fun (V) -> Echo ! {Me, V}, receive R -> io:format(\"~p \", [R]) end end, + Send(1), Send(2), Send(3), Send(4), + Echo ! stop, + done")) + (er-io-buffer-content)) + "1 2 3 4 ") + +(define + er-echo-test-summary + (str "echo " er-echo-test-pass "/" er-echo-test-count)) diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 79a9e250..f72194e2 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -73,7 +73,7 @@ Core mapping: - [x] `ring.erl` — N processes in a ring, pass a token around M times — **4 ring tests**; suspension machinery rewritten from `shift`/`reset` to `call/cc` + `raise`/`guard` - [x] `ping_pong.erl` — two processes exchanging messages — **4 ping-pong tests** - [x] `bank.erl` — account server (deposit/withdraw/balance) — **8 bank tests** - - [ ] `echo.erl` — minimal server + - [x] `echo.erl` — minimal server — **7 echo tests** - [ ] `fib_server.erl` — compute fib on request - [ ] `lib/erlang/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md` - [ ] Target: 5/5 classic programs + 1M-process ring benchmark runs @@ -99,6 +99,7 @@ Core mapping: _Newest first._ +- **2026-04-25 echo.erl green** — `lib/erlang/tests/programs/echo.sx` with 7 tests. Server: `receive {From, Msg} -> From ! Msg, Loop(); stop -> ok end`. Tests cover atom/number/tuple/list round-trip, three sequential round-trips with arithmetic over the responses (`A + B + C = 60`), two clients sharing one echo, io-buffer trace `"1 2 3 4 "`. Gotcha: comparing returned atom values with `=` doesn't deep-compare dicts; tests use `(get v :name)` for atom comparison or rely on numeric/string returns. Total suite 350/350. - **2026-04-24 bank.erl green** — `lib/erlang/tests/programs/bank.sx` with 8 tests. Stateful server pattern: `Server = fun (Balance) -> receive ... Server(NewBalance) end end` recursively threads balance through each iteration. Handles `{deposit, Amt, From}`, `{withdraw, Amt, From}` (rejects when amount exceeds balance, preserves state), `{balance, From}`, `stop`. Tests cover deposit accumulation, withdrawal within balance, insufficient funds with state preservation, mixed transactions, clean shutdown, two-client interleave. Total suite 343/343. - **2026-04-24 ping_pong.erl green** — `lib/erlang/tests/programs/ping_pong.sx` with 4 tests: classic Pong server + Ping client with separate `ping_done`/`pong_done` notifications, 5-round trace via io-buffer (`"ppppp"`), main-as-pinger-4-rounds (no intermediate Ping proc), tagged-id round-trip (`"4 3 2 1 "`). All driven by `Ping = fun (Target, K) -> ... Ping(Target, K-1) ... end` self-recursion — captured-env reference works because `Ping` binds in main's mutable env before any spawned body looks it up. Total suite 335/335. - **2026-04-24 ring.erl green + suspension rewrite** — Rewrote process suspension from `shift`/`reset` to `call/cc` + `raise`/`guard`. **Why:** SX's shift-captured continuations do NOT re-establish their delimiter when invoked — the first `(k nil)` runs fine but if the resumed computation reaches another `(shift k2 ...)` it raises "shift without enclosing reset". Ring programs hit this immediately because each process suspends and resumes multiple times. `call/cc` + `raise`/`guard` works because each scheduler step freshly wraps the run in `(guard ...)`, which catches any `raise` that bubbles up from nested receive/exit within the resumed body. Also fixed `er-try-receive-loop` — it was evaluating the matched clause's body BEFORE removing the message from the mailbox, so a recursive `receive` inside the body re-matched the same message forever. Added `lib/erlang/tests/programs/ring.sx` with 4 tests (N=3 M=6, N=2 M=4, N=1 M=5 self-loop, N=3 M=9 hop-count via io-buffer). All process-communication eval tests still pass. Total suite 331/331.