diff --git a/lib/erlang/tests/programs/ping_pong.sx b/lib/erlang/tests/programs/ping_pong.sx new file mode 100644 index 00000000..02b0283d --- /dev/null +++ b/lib/erlang/tests/programs/ping_pong.sx @@ -0,0 +1,127 @@ +;; Ping-pong program — two processes exchange N messages, then signal +;; main via separate `ping_done` / `pong_done` notifications. + +(define er-pp-test-count 0) +(define er-pp-test-pass 0) +(define er-pp-test-fails (list)) + +(define + er-pp-test + (fn + (name actual expected) + (set! er-pp-test-count (+ er-pp-test-count 1)) + (if + (= actual expected) + (set! er-pp-test-pass (+ er-pp-test-pass 1)) + (append! er-pp-test-fails {:actual actual :expected expected :name name})))) + +(define pp-ev erlang-eval-ast) + +;; Three rounds of ping-pong, then stop. Main receives ping_done and +;; pong_done in arrival order (Ping finishes first because Pong exits +;; only after receiving stop). +(define + er-pp-program + "Me = self(), + Pong = spawn(fun () -> + Loop = fun () -> + receive + {ping, From} -> From ! pong, Loop(); + stop -> Me ! pong_done + end + end, + Loop() + end), + Ping = fun (Target, K) -> + if K =:= 0 -> Target ! stop, Me ! ping_done; + true -> Target ! {ping, self()}, receive pong -> Ping(Target, K - 1) end + end + end, + spawn(fun () -> Ping(Pong, 3) end), + receive ping_done -> ok end, + receive pong_done -> both_done end") + +(er-pp-test + "ping-pong 3 rounds" + (get (pp-ev er-pp-program) :name) + "both_done") + +;; Count exchanges via io-buffer — each pong trip prints "p". +(er-pp-test + "ping-pong 5 rounds trace" + (do + (er-io-flush!) + (pp-ev + "Me = self(), + Pong = spawn(fun () -> + Loop = fun () -> + receive + {ping, From} -> io:format(\"p\"), From ! pong, Loop(); + stop -> Me ! pong_done + end + end, + Loop() + end), + Ping = fun (Target, K) -> + if K =:= 0 -> Target ! stop, Me ! ping_done; + true -> Target ! {ping, self()}, receive pong -> Ping(Target, K - 1) end + end + end, + spawn(fun () -> Ping(Pong, 5) end), + receive ping_done -> ok end, + receive pong_done -> ok end") + (er-io-buffer-content)) + "ppppp") + +;; Main → Pong directly (no Ping process). Main plays the ping role. +(er-pp-test + "main-as-pinger 4 rounds" + (pp-ev + "Me = self(), + Pong = spawn(fun () -> + Loop = fun () -> + receive + {ping, From} -> From ! pong, Loop(); + stop -> ok + end + end, + Loop() + end), + Go = fun (K) -> + if K =:= 0 -> Pong ! stop, K; + true -> Pong ! {ping, Me}, receive pong -> Go(K - 1) end + end + end, + Go(4)") + 0) + +;; Ensure the processes really interleave — inject an id into each +;; ping and check we get them all back via trace (the order is +;; deterministic under our sync scheduler). +(er-pp-test + "ids round-trip" + (do + (er-io-flush!) + (pp-ev + "Me = self(), + Pong = spawn(fun () -> + Loop = fun () -> + receive + {ping, From, Id} -> From ! {pong, Id}, Loop(); + stop -> ok + end + end, + Loop() + end), + Go = fun (K) -> + if K =:= 0 -> Pong ! stop, done; + true -> Pong ! {ping, Me, K}, receive {pong, RId} -> io:format(\"~p \", [RId]), Go(K - 1) end + end + end, + Go(4)") + (er-io-buffer-content)) + "4 3 2 1 ") + +(define + er-pp-test-summary + (str "ping-pong " er-pp-test-pass "/" er-pp-test-count)) diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 236ca193..c6c83295 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -71,7 +71,7 @@ Core mapping: - [x] `exit/1`, basic process termination — **9 new eval tests**; `exit/2` (signal another) deferred to Phase 4 with links - [ ] Classic programs in `lib/erlang/tests/programs/`: - [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` - - [ ] `ping_pong.erl` — two processes exchanging messages + - [x] `ping_pong.erl` — two processes exchanging messages — **4 ping-pong tests** - [ ] `bank.erl` — account server (deposit/withdraw/balance) - [ ] `echo.erl` — minimal server - [ ] `fib_server.erl` — compute fib on request @@ -99,6 +99,7 @@ Core mapping: _Newest first._ +- **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. - **2026-04-24 exit/1 + termination green** — `exit/1` BIF uses `(shift k ...)` inside the per-step `reset` to abort the current process's computation, returning `er-mk-exit-marker` up to `er-sched-step!`. Step handler records `:exit-reason`, clears `:exit-result`, marks dead. Normal fall-off-end still records reason `normal`. `exit/2` errors with "deferred to Phase 4 (links)". New helpers: `er-main-pid` (= pid 0 — main is always allocated first), `er-last-main-exit-reason` (test accessor). 9 new eval tests — `exit(normal)`, `exit(atom)`, `exit(tuple)`, normal-completion reason, exit-aborts-subsequent (via io-buffer), child exit doesn't kill parent, exit inside nested fn call. Total eval 174/174; suite 327/327. - **2026-04-24 receive...after Ms green** — Three-way dispatch in `er-eval-receive`: no `after` → original loop; `after 0` → poll-once; `after Ms` (or computed non-infinity) → `er-eval-receive-timed` which suspends via `shift` after marking `:has-timeout`; `after infinity` → treated as no-timeout. `er-sched-run-all!` now recurses into `er-sched-fire-one-timeout!` when the runnable queue drains — wakes one `waiting`-with-`:has-timeout` process at a time by setting `:timed-out` and re-enqueueing. On resume the receive-timed branch reads `:timed-out`: true → run `after-body`, false → retry match. "Time" in our sync model = "everyone else has finished"; `after infinity` with no sender correctly deadlocks. 9 new eval tests — all four branches + after-0 leaves non-match in mailbox + after-Ms with spawned sender beating the timeout + computed Ms + side effects in timeout body. Total eval 165/165; suite 318/318.