diff --git a/next/kernel/registry.erl b/next/kernel/registry.erl index 5205325e..e762bce0 100644 --- a/next/kernel/registry.erl +++ b/next/kernel/registry.erl @@ -1,5 +1,8 @@ -module(registry). +-behaviour(gen_server). -export([new/0, kinds/0, register/4, lookup/3, list/2]). +-export([start_link/0, register/3, lookup/2, list/1, stop/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2]). %% Pure-functional registry for the seven bootstrap kinds. %% @@ -67,3 +70,51 @@ put_pair(K, V, [P | Rest]) -> [P | put_pair(K, V, Rest)]. find_pair(_, []) -> not_found; find_pair(K, [{K, V} | _]) -> {ok, V}; find_pair(K, [_ | Rest]) -> find_pair(K, Rest). + +%% ── Step 5b: gen_server wrapper ───────────────────────────────── +%% +%% The named process owns the registry state; concurrent readers +%% and writers serialize through gen_server:call. The pure /3 and +%% /4 functions remain available for offline projection-replay and +%% for tests that don't need a process at all. +%% +%% Port notes: gen_server:start_link returns the raw Pid (not +%% `{ok, Pid}` as in OTP). `?MODULE` macro is unsupported here, so +%% the registered name is the literal `registry` atom in every call. + +start_link() -> + Pid = gen_server:start_link(registry, []), + erlang:register(registry, Pid), + Pid. + +stop() -> + R = gen_server:call(registry, '$gen_stop'), + erlang:unregister(registry), + R. + +register(Kind, Name, Entry) -> + gen_server:call(registry, {register, Kind, Name, Entry}). + +lookup(Kind, Name) -> + gen_server:call(registry, {lookup, Kind, Name}). + +list(Kind) -> + gen_server:call(registry, {list, Kind}). + +%% gen_server callbacks + +init(_) -> {ok, new()}. + +handle_call({register, Kind, Name, Entry}, _From, State) -> + case register(Kind, Name, Entry, State) of + {ok, NewState} -> {reply, ok, NewState}; + {error, R} -> {reply, {error, R}, State} + end; +handle_call({lookup, Kind, Name}, _From, State) -> + {reply, lookup(Kind, Name, State), State}; +handle_call({list, Kind}, _From, State) -> + {reply, list(Kind, State), State}. + +handle_cast(_, S) -> {noreply, S}. + +handle_info(_, S) -> {noreply, S}. diff --git a/next/tests/registry_server.sh b/next/tests/registry_server.sh new file mode 100755 index 00000000..fea294ad --- /dev/null +++ b/next/tests/registry_server.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# next/tests/registry_server.sh — Step 5b acceptance test. +# +# Exercises the gen_server-wrapped registry. Each test combines +# start_link + operations + assertion into a single +# erlang-eval-ast expression because the Erlang-on-SX scheduler +# does not preserve spawned processes across separate eval +# invocations. 10 cases. + +set -uo pipefail +cd "$(git rev-parse --show-toplevel)" + +SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" +if [ ! -x "$SX_SERVER" ]; then + SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe" +fi +if [ ! -x "$SX_SERVER" ]; then + echo "ERROR: sx_server.exe not found." >&2 + exit 1 +fi + +VERBOSE="${1:-}" +PASS=0; FAIL=0; ERRORS="" +TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT + +cat > "$TMPFILE" <<'EPOCHS' +(epoch 1) +(load "lib/erlang/tokenizer.sx") +(load "lib/erlang/parser.sx") +(load "lib/erlang/parser-core.sx") +(load "lib/erlang/parser-expr.sx") +(load "lib/erlang/parser-module.sx") +(load "lib/erlang/transpile.sx") +(load "lib/erlang/runtime.sx") +(load "lib/erlang/vm/dispatcher.sx") +(epoch 2) +(eval "(er-load-gen-server!)") +(epoch 3) +(eval "(get (erlang-load-module (file-read \"next/kernel/registry.erl\")) :name)") + +;; start_link returns a Pid +(epoch 10) +(eval "(get (erlang-eval-ast \"is_pid(registry:start_link())\") :name)") + +;; register + lookup round-trip +(epoch 11) +(eval "(get (erlang-eval-ast \"registry:start_link(), registry:register(activity_types, create, e1), registry:lookup(activity_types, create) =:= {ok, e1}\") :name)") + +;; lookup unknown name returns not_found +(epoch 12) +(eval "(get (erlang-eval-ast \"registry:start_link(), registry:lookup(activity_types, missing) =:= not_found\") :name)") + +;; register returns the atom 'ok' +(epoch 13) +(eval "(get (erlang-eval-ast \"registry:start_link(), registry:register(object_types, note, e_n) =:= ok\") :name)") + +;; list returns all pairs in a kind +(epoch 14) +(eval "(erlang-eval-ast \"registry:start_link(), registry:register(activity_types, a, 1), registry:register(activity_types, b, 2), registry:register(activity_types, c, 3), length(registry:list(activity_types))\")") + +;; Re-register overrides without growing the list +(epoch 15) +(eval "(erlang-eval-ast \"registry:start_link(), registry:register(activity_types, a, v1), registry:register(activity_types, a, v2), length(registry:list(activity_types))\")") +(epoch 16) +(eval "(get (erlang-eval-ast \"registry:start_link(), registry:register(activity_types, a, v1), registry:register(activity_types, a, v2), registry:lookup(activity_types, a) =:= {ok, v2}\") :name)") + +;; State persists across multiple calls in the same expression +(epoch 17) +(eval "(erlang-eval-ast \"registry:start_link(), registry:register(activity_types, x, 1), registry:register(object_types, x, 2), {registry:lookup(activity_types, x), registry:lookup(object_types, x)} =:= {{ok, 1}, {ok, 2}}\")") + +;; Unknown kind rejected via gen_server too +(epoch 18) +(eval "(get (erlang-eval-ast \"registry:start_link(), case registry:lookup(bogus_kind, foo) of {error, unknown_kind} -> ok; _ -> bad end\") :name)") + +;; Empty kind list returns [] +(epoch 19) +(eval "(get (erlang-eval-ast \"registry:start_link(), registry:list(validators) =:= []\") :name)") +EPOCHS + +OUTPUT=$(timeout 120 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) + +check() { + local epoch="$1" desc="$2" expected="$3" + local actual + actual=$(echo "$OUTPUT" | awk -v e="$epoch" ' + $0 ~ "^\\(ok-len " e " " { getline; print; exit } + $0 ~ "^\\(ok " e " " { print; exit } + $0 ~ "^\\(error " e " " { print; exit } + ') + [ -z "$actual" ] && actual="" + if echo "$actual" | grep -qF -- "$expected"; then + PASS=$((PASS+1)) + [ "$VERBOSE" = "-v" ] && echo " ok $desc" + else + FAIL=$((FAIL+1)) + ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual +" + fi +} + +check 2 "gen_server loaded" "gen_server" +check 3 "registry module loaded" "registry" +check 10 "start_link returns Pid" "true" +check 11 "register + lookup round-trip" "true" +check 12 "lookup missing -> not_found" "true" +check 13 "register returns ok atom" "true" +check 14 "three registers, list = 3" "3" +check 15 "re-register doesn't grow list" "1" +check 16 "re-register overrides value" "true" +check 17 "different kinds independent" "true" +check 18 "lookup bogus kind" "ok" +check 19 "empty kind list = []" "true" + +# 12 cases total (epoch 2 + 3 are setup, but counted for honesty) +TOTAL=$((PASS+FAIL)) +if [ $FAIL -eq 0 ]; then + echo "ok $PASS/$TOTAL next/tests/registry_server.sh passed" +else + echo "FAIL $PASS/$TOTAL passed, $FAIL failed:" + echo "$ERRORS" +fi +[ $FAIL -eq 0 ] diff --git a/plans/fed-sx-milestone-1.md b/plans/fed-sx-milestone-1.md index d40ee7dc..90b1f950 100644 --- a/plans/fed-sx-milestone-1.md +++ b/plans/fed-sx-milestone-1.md @@ -339,7 +339,7 @@ created with a known stable CID. **Sub-deliverables:** - [x] **5a** — Pure-functional `next/kernel/registry.erl`: `new/0`, `kinds/0`, `register/4`, `lookup/3`, `list/2`. State is a property list keyed by kind atom; per-kind storage is a property list of `{Name, Entry}`. Unknown kinds rejected with `{error, unknown_kind}`. `next/tests/registry_pure.sh` (14 cases). -- [ ] **5b** — gen_server wrapper around the pure registry: `start_link/0`, registered name `registry`, the pure API delegated through `gen_server:call`. +- [x] **5b** — gen_server wrapper around the pure registry: `start_link/0`, registered name `registry`, `register/3 lookup/2 list/1 stop/0` API delegating through `gen_server:call`. `next/tests/registry_server.sh` (12 cases). Port note: each test combines start_link + ops in a single expression because spawned processes don't survive across separate `erlang-eval-ast` invocations. - [ ] **5c** — `bootstrap:load_genesis/1` (Step 4e) populates the registry from `read_genesis` output. Dispatches by section atom → kind. - [ ] **5d** — define-registry projection fold integration: incoming `Create{Define*}` activities are routed through the projection scheduler (Step 7) and update the registry. @@ -961,6 +961,7 @@ A few things still under-specified; resolve as work begins. Newest first. One line per sub-deliverable commit. Erlang conformance gate (`bash lib/erlang/conformance.sh`) must remain 729/729 on every entry. +- **2026-05-28** — Step 5b: `registry.erl` extended with gen_server callbacks + named-process API. `start_link/0` spawns the worker, registers it under the literal `registry` atom, returns the Pid (port returns raw Pid not `{ok, Pid}` — diverges from OTP). 3-arity `register`, 2-arity `lookup`, 1-arity `list` delegate to the pure /4 and /3 functions inside handle_call. Port note documented: `?MODULE` macro unsupported; tests must inline start_link with operations since spawned processes don't persist across separate `erlang-eval-ast` calls. `next/tests/registry_server.sh` 12/12. Erlang conformance 729/729. - **2026-05-28** — Step 4e: `bootstrap:load_genesis/1` + `strip_sx_suffix/1` in `next/kernel/bootstrap.erl`. Walks `read_genesis` output and threads each entry through `registry:register/4`, using the section atom as the kind and the filename-minus-`.sx` as the entry name. Per-kind counts match the seven bootstrap sections exactly (3/10/7/3/3/2/3 = 31 entries total). `next/tests/bootstrap_load.sh` 15/15. Determinism verified by comparing `cid:to_string` of the loaded state across calls (faster than deep-equality on the nested-binary state). Step 4 is now complete end-to-end except for SX-source parsing of the loaded entries. Erlang conformance 729/729. - **2026-05-28** — Step 5a: `next/kernel/registry.erl` — pure-functional registry. State is `[{Kind, [{Name, Entry}, ...]}, ...]` keyed by the same seven section atoms as Step 4c (activity_types, object_types, projections, validators, codecs, sig_suites, audience). API: `new/0`, `kinds/0`, `register/4`, `lookup/3`, `list/2`. Unknown kinds rejected with `{error, unknown_kind}`; missing names return `not_found`; re-registering the same name overrides without growing the list. `next/tests/registry_pure.sh` 14/14. Step 5 broken into 5a–5d on the plan. Erlang conformance 729/729. - **2026-05-28** — Step 4d: `bootstrap:build_genesis/1` + `verify_genesis/2` + `.cidhash` helpers in `next/kernel/bootstrap.erl`. Bundle CID delegated to host `cid:to_string` over `{genesis_bundle, Sections}` — deterministic, ~59 byte CIDv1 binary. `verify_genesis/2` returns `ok` on match, `{error, {cid_mismatch, Got, Expected}}` on drift. `write_cidhash`/`read_cidhash` persist the CID to a `.cidhash` sibling file (path hand-spelled `<<...,47,46,99,...>>` per the string-literal-in-binary substrate quirk). `next/tests/bootstrap_build.sh` 12/12. Erlang conformance 729/729.