; -*- mode: markdown -*- # loops/erlang — `erlang:send_after` substrate primitive Scoped briefing for a single focused iteration loop on `loops/erlang`. Not a replacement for the general `erlang-loop.md`; this is the load-bearing-blocker task that, when done, unblocks `plans/fed-sx-milestone-2.md` Step **8b-timer** (delivery retry loop) and the standard `gen_server` `timeout` return. ``` description: loops/erlang — send_after primitive subagent_type: general-purpose run_in_background: true isolation: worktree # worktree at /root/rose-ash-loops/erlang ``` ## The goal Implement the standard Erlang timer primitives so a process can schedule a message-to-self (or to another pid) after N milliseconds: ```erlang Ref = erlang:send_after(Time, Dest, Msg). %% Time: int millis. Dest: pid or atom. ok | TimeLeft = erlang:cancel_timer(Ref). %% returns remaining ms, or false if fired/expired. ``` These are the same primitives `gen_server` uses internally to emit `{noreply, S, Timeout}` returns — when the gen_server's `handle_*` callback returns `{noreply, NewState, T}`, the gen_server schedules `{timeout}` to itself after T ms via `send_after`, and the next `handle_info({timeout}, S)` fires when no other message arrives first. Without `send_after`, anything that wants a delayed self-cast has to busy-loop or block — neither acceptable for the kernel. ## Acceptance — single SX file `lib/erlang/tests/send_after.sx` The standard test suite plus this one. Run the conformance gate after each commit (`bash lib/erlang/conformance.sh`). - `T1` — `erlang:send_after(50, self(), hello)` returns a Ref; after 60ms a receive picks up `hello`. Round-trip latency under 100ms in steady state. - `T2` — `Ref = erlang:send_after(1000, self(), late), erlang:cancel_timer(Ref)` returns an integer ~1000 (remaining ms) AND a subsequent `receive late -> got after 50 -> none end` returns `none` — the cancelled message never arrives. - `T3` — multiple in-flight timers fire in deadline order, not schedule order: `send_after(80, self(), b)`, then `send_after(20, self(), a)` — selective receive on `a` first. - `T4` — cancel_timer on an already-fired timer returns `false`. - `T5` — `send_after` to a registered atom: register a process, queue a delayed message to its name, it lands in that process's mailbox. - `T6` — `gen_server` `{noreply, S, 100}` → `handle_info({timeout}, S)` fires after 100ms when no other message arrives. (Sanity check that the gen_server library, currently shipped, hooks up correctly.) Conformance gate stays green (currently 725/725 on loops/erlang per `lib/erlang/scoreboard.json`, 761/761 after the m2 BIFs are in — both numbers will move when this lands). ## Implementation shape (suggested, not prescribed) The OCaml host has no event loop today — `bin/sx_server.ml` is a single-thread epoch protocol. But the Erlang scheduler in `lib/erlang/runtime.sx` IS a SX-level event loop already: `er-sched` holds the global dict; `er-sched-step-alive!` advances one runnable process; `er-sched-run-all!` drains the runnable queue until all processes are waiting / exiting / dead. Two options: **Option A — pure-SX timer wheel.** Extend the scheduler dict with `:timers (sorted-list-of {:deadline-ms _ :pid _ :msg _ :ref _ :alive true})`. `er-sched-run-all!` already loops; when it finds the runnable queue empty but processes still alive in `receive` blocks, walk the timer list and check whether any deadline has passed (need a monotonic clock — see below). For deadlines in the future, sleep until the next one or until the runnable queue gets work. Cancel = set `:alive false` on the timer entry; cull on next scan. **Option B — OCaml-side `Unix.setitimer` / `Unix.select` wakeup.** Heavier, lets the SX scheduler park properly on `Unix.select` against a self-pipe. Right shape for a long-running kernel that may sit idle for minutes between events. Probably the production answer, but significantly more work than A. Recommend A first for the unblock; B as a follow-up if the kernel needs to idle gracefully. ### Clock primitive `erlang:monotonic_time/0,1` and `erlang:system_time/0,1` ought to exist for completeness — `send_after` needs at least a monotonic ms clock. If the substrate doesn't have one, add `erlang:monotonic_time(millisecond)` as a small native BIF in `bin/sx_server.ml` (Unix.gettimeofday) and expose it via `er-bif-erlang-monotonic-time`. Time travel safety: DO NOT use wall-clock for timer deadlines. ## Files in play (loops/erlang scope) - `lib/erlang/runtime.sx` — scheduler state, BIFs, primitive registration. Scheduler dict at `er-scheduler` (single-element list). Step function: `er-sched-step-alive!` (line ~708 on m2; check line numbers freshly — recent commits moved code). - `lib/erlang/tests/send_after.sx` — new file, the test suite above. - `lib/erlang/scoreboard.json` + `scoreboard.md` — bump counts. - (If Option B is taken) `bin/sx_server.ml` — out of `loops/erlang` scope per the loop's standing rules. Surface a separate `loops/fed-prims` task for the native bits and keep `send_after`'s SX wrapper minimal. ## Base branch / rebase note The substrate change for the kernel mutex (Blockers #4 in fed-sx-milestone-2.md) lands on architecture via the m2 merge. The key change is in `lib/erlang/runtime.sx`: - `er-sched-step-alive!` reads `:pending-args` from the process record when first invoked (line 736 on m2; was hardcoded to `(list)`). - `er-bif-http-listen` spawns the user's handler as a real er-process with `:pending-args (list req-pl)` instead of `er-apply-fun`-inline. If `loops/erlang` rebases onto `origin/architecture` and that merge hasn't landed yet (still sitting on `local /root/rose-ash` at `089ed88f`-or-later), fetch `origin/loops/fed-sx-m2` and cherry-pick or rebase against `29e4234b` to pick up the `:pending-args` field shape. The work area (`er-sched-step-alive!`) is the same surface this loop will touch — without the m2 change, the timer wakeup path won't dovetail cleanly with the spawn-from-BIF pattern. Conflict risk: medium. Conflict surface: small (~5 lines around the `:pending-args` read). ## Test discipline - `bash lib/erlang/conformance.sh` green before and after every commit. - Commits scoped to `lib/erlang/**`. No edits to `next/**`, `bin/sx_server.ml`, `spec/**`, or other `lib//**` from this loop. - One commit per acceptance test cluster — `T1+T2`, `T3+T4`, `T5+T6` is a reasonable cadence. - Push to `origin/loops/erlang` after each commit. The fed-sx-m2 loop is dormant waiting on this. ## Done when - 6/6 new `send_after` tests green. - conformance gate green at the new total. - single line ticked in `plans/erlang-on-sx.md` Progress log + a tick on whatever roadmap entry covers timers. - Then update the m2 plan (`plans/fed-sx-milestone-2.md` Blockers #3) to **RESOLVED** and unblock Step 8b-timer; that update belongs to the fed-sx-m2 loop, not this one.