fed-sx-m1: Step 8a — http:listen/2 BIF wrapper in runtime.sx (BRIEFING-EXCEPTION) + 5 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 31s

This commit is contained in:
2026-05-28 07:35:48 +00:00
parent 1ea47681b2
commit 81efa1d8f0
3 changed files with 183 additions and 25 deletions

View File

@@ -1468,9 +1468,26 @@
;; entry is keyed by "Module/Name/Arity"; multi-arity BIFs register ;; entry is keyed by "Module/Name/Arity"; multi-arity BIFs register
;; once per arity. Called eagerly at the end of runtime.sx so the ;; once per arity. Called eagerly at the end of runtime.sx so the
;; registry is ready before any erlang-eval-ast call. ;; registry is ready before any erlang-eval-ast call.
(define er-register-builtin-bifs! (define
(fn () er-bif-http-listen
;; erlang module — type predicates (all pure) (fn
(vs)
(let
((port (nth vs 0)) (handler (nth vs 1)))
(cond
(not (= (type-of port) "number"))
(raise (er-mk-error-marker (er-mk-atom "badarg")))
(not (er-fun? handler))
(raise (er-mk-error-marker (er-mk-atom "badarg")))
:else (let
((sx-handler (fn (req-dict) (let ((er-req (er-of-sx req-dict))) (er-to-sx (er-apply-fun handler (list er-req)))))))
(http-listen port sx-handler))))))
;; Register everything at load time.
(define
er-register-builtin-bifs!
(fn
()
(er-register-pure-bif! "erlang" "is_integer" 1 er-bif-is-integer) (er-register-pure-bif! "erlang" "is_integer" 1 er-bif-is-integer)
(er-register-pure-bif! "erlang" "is_atom" 1 er-bif-is-atom) (er-register-pure-bif! "erlang" "is_atom" 1 er-bif-is-atom)
(er-register-pure-bif! "erlang" "is_list" 1 er-bif-is-list) (er-register-pure-bif! "erlang" "is_list" 1 er-bif-is-list)
@@ -1479,27 +1496,61 @@
(er-register-pure-bif! "erlang" "is_float" 1 er-bif-is-float) (er-register-pure-bif! "erlang" "is_float" 1 er-bif-is-float)
(er-register-pure-bif! "erlang" "is_boolean" 1 er-bif-is-boolean) (er-register-pure-bif! "erlang" "is_boolean" 1 er-bif-is-boolean)
(er-register-pure-bif! "erlang" "is_pid" 1 er-bif-is-pid) (er-register-pure-bif! "erlang" "is_pid" 1 er-bif-is-pid)
(er-register-pure-bif! "erlang" "is_reference" 1 er-bif-is-reference) (er-register-pure-bif!
"erlang"
"is_reference"
1
er-bif-is-reference)
(er-register-pure-bif! "erlang" "is_binary" 1 er-bif-is-binary) (er-register-pure-bif! "erlang" "is_binary" 1 er-bif-is-binary)
(er-register-pure-bif! "erlang" "is_function" 1 er-bif-is-function) (er-register-pure-bif!
(er-register-pure-bif! "erlang" "is_function" 2 er-bif-is-function) "erlang"
;; erlang module — pure data ops "is_function"
1
er-bif-is-function)
(er-register-pure-bif!
"erlang"
"is_function"
2
er-bif-is-function)
(er-register-pure-bif! "erlang" "length" 1 er-bif-length) (er-register-pure-bif! "erlang" "length" 1 er-bif-length)
(er-register-pure-bif! "erlang" "hd" 1 er-bif-hd) (er-register-pure-bif! "erlang" "hd" 1 er-bif-hd)
(er-register-pure-bif! "erlang" "tl" 1 er-bif-tl) (er-register-pure-bif! "erlang" "tl" 1 er-bif-tl)
(er-register-pure-bif! "erlang" "element" 2 er-bif-element) (er-register-pure-bif! "erlang" "element" 2 er-bif-element)
(er-register-pure-bif! "erlang" "tuple_size" 1 er-bif-tuple-size) (er-register-pure-bif! "erlang" "tuple_size" 1 er-bif-tuple-size)
(er-register-pure-bif! "erlang" "byte_size" 1 er-bif-byte-size) (er-register-pure-bif! "erlang" "byte_size" 1 er-bif-byte-size)
(er-register-pure-bif! "erlang" "atom_to_list" 1 er-bif-atom-to-list) (er-register-pure-bif!
(er-register-pure-bif! "erlang" "list_to_atom" 1 er-bif-list-to-atom) "erlang"
"atom_to_list"
1
er-bif-atom-to-list)
(er-register-pure-bif!
"erlang"
"list_to_atom"
1
er-bif-list-to-atom)
(er-register-pure-bif! "erlang" "abs" 1 er-bif-abs) (er-register-pure-bif! "erlang" "abs" 1 er-bif-abs)
(er-register-pure-bif! "erlang" "min" 2 er-bif-min) (er-register-pure-bif! "erlang" "min" 2 er-bif-min)
(er-register-pure-bif! "erlang" "max" 2 er-bif-max) (er-register-pure-bif! "erlang" "max" 2 er-bif-max)
(er-register-pure-bif! "erlang" "tuple_to_list" 1 er-bif-tuple-to-list) (er-register-pure-bif!
(er-register-pure-bif! "erlang" "list_to_tuple" 1 er-bif-list-to-tuple) "erlang"
(er-register-pure-bif! "erlang" "integer_to_list" 1 er-bif-integer-to-list) "tuple_to_list"
(er-register-pure-bif! "erlang" "list_to_integer" 1 er-bif-list-to-integer) 1
;; erlang module — process / runtime (side-effecting) er-bif-tuple-to-list)
(er-register-pure-bif!
"erlang"
"list_to_tuple"
1
er-bif-list-to-tuple)
(er-register-pure-bif!
"erlang"
"integer_to_list"
1
er-bif-integer-to-list)
(er-register-pure-bif!
"erlang"
"list_to_integer"
1
er-bif-list-to-integer)
(er-register-bif! "erlang" "self" 0 er-bif-self) (er-register-bif! "erlang" "self" 0 er-bif-self)
(er-register-bif! "erlang" "spawn" 1 er-bif-spawn) (er-register-bif! "erlang" "spawn" 1 er-bif-spawn)
(er-register-bif! "erlang" "spawn" 3 er-bif-spawn) (er-register-bif! "erlang" "spawn" 3 er-bif-spawn)
@@ -1515,12 +1566,16 @@
(er-register-bif! "erlang" "unregister" 1 er-bif-unregister) (er-register-bif! "erlang" "unregister" 1 er-bif-unregister)
(er-register-bif! "erlang" "whereis" 1 er-bif-whereis) (er-register-bif! "erlang" "whereis" 1 er-bif-whereis)
(er-register-bif! "erlang" "registered" 0 er-bif-registered) (er-register-bif! "erlang" "registered" 0 er-bif-registered)
;; erlang module — exception raising (modelled as side-effecting) (er-register-bif!
(er-register-bif! "erlang" "throw" 1 "erlang"
"throw"
1
(fn (vs) (raise (er-mk-throw-marker (er-bif-arg1 vs "throw"))))) (fn (vs) (raise (er-mk-throw-marker (er-bif-arg1 vs "throw")))))
(er-register-bif! "erlang" "error" 1 (er-register-bif!
"erlang"
"error"
1
(fn (vs) (raise (er-mk-error-marker (er-bif-arg1 vs "error"))))) (fn (vs) (raise (er-mk-error-marker (er-bif-arg1 vs "error")))))
;; lists module — all pure
(er-register-pure-bif! "lists" "reverse" 1 er-bif-lists-reverse) (er-register-pure-bif! "lists" "reverse" 1 er-bif-lists-reverse)
(er-register-pure-bif! "lists" "map" 2 er-bif-lists-map) (er-register-pure-bif! "lists" "map" 2 er-bif-lists-map)
(er-register-pure-bif! "lists" "foldl" 3 er-bif-lists-foldl) (er-register-pure-bif! "lists" "foldl" 3 er-bif-lists-foldl)
@@ -1534,11 +1589,13 @@
(er-register-pure-bif! "lists" "filter" 2 er-bif-lists-filter) (er-register-pure-bif! "lists" "filter" 2 er-bif-lists-filter)
(er-register-pure-bif! "lists" "any" 2 er-bif-lists-any) (er-register-pure-bif! "lists" "any" 2 er-bif-lists-any)
(er-register-pure-bif! "lists" "all" 2 er-bif-lists-all) (er-register-pure-bif! "lists" "all" 2 er-bif-lists-all)
(er-register-pure-bif! "lists" "duplicate" 2 er-bif-lists-duplicate) (er-register-pure-bif!
;; io module — side-effecting (writes to io buffer) "lists"
"duplicate"
2
er-bif-lists-duplicate)
(er-register-bif! "io" "format" 1 er-bif-io-format) (er-register-bif! "io" "format" 1 er-bif-io-format)
(er-register-bif! "io" "format" 2 er-bif-io-format) (er-register-bif! "io" "format" 2 er-bif-io-format)
;; ets module — side-effecting (mutates table state)
(er-register-bif! "ets" "new" 2 er-bif-ets-new) (er-register-bif! "ets" "new" 2 er-bif-ets-new)
(er-register-bif! "ets" "insert" 2 er-bif-ets-insert) (er-register-bif! "ets" "insert" 2 er-bif-ets-insert)
(er-register-bif! "ets" "lookup" 2 er-bif-ets-lookup) (er-register-bif! "ets" "lookup" 2 er-bif-ets-lookup)
@@ -1546,23 +1603,21 @@
(er-register-bif! "ets" "delete" 2 er-bif-ets-delete) (er-register-bif! "ets" "delete" 2 er-bif-ets-delete)
(er-register-bif! "ets" "tab2list" 1 er-bif-ets-tab2list) (er-register-bif! "ets" "tab2list" 1 er-bif-ets-tab2list)
(er-register-bif! "ets" "info" 2 er-bif-ets-info) (er-register-bif! "ets" "info" 2 er-bif-ets-info)
;; code module — side-effecting (mutates module registry, kills procs)
(er-register-bif! "code" "load_binary" 3 er-bif-code-load-binary) (er-register-bif! "code" "load_binary" 3 er-bif-code-load-binary)
(er-register-bif! "code" "purge" 1 er-bif-code-purge) (er-register-bif! "code" "purge" 1 er-bif-code-purge)
(er-register-bif! "code" "soft_purge" 1 er-bif-code-soft-purge) (er-register-bif! "code" "soft_purge" 1 er-bif-code-soft-purge)
(er-register-bif! "code" "which" 1 er-bif-code-which) (er-register-bif! "code" "which" 1 er-bif-code-which)
(er-register-bif! "code" "is_loaded" 1 er-bif-code-is-loaded) (er-register-bif! "code" "is_loaded" 1 er-bif-code-is-loaded)
(er-register-bif! "code" "all_loaded" 0 er-bif-code-all-loaded) (er-register-bif! "code" "all_loaded" 0 er-bif-code-all-loaded)
;; file module
(er-register-bif! "file" "read_file" 1 er-bif-file-read-file) (er-register-bif! "file" "read_file" 1 er-bif-file-read-file)
(er-register-bif! "file" "write_file" 2 er-bif-file-write-file) (er-register-bif! "file" "write_file" 2 er-bif-file-write-file)
(er-register-bif! "file" "delete" 1 er-bif-file-delete) (er-register-bif! "file" "delete" 1 er-bif-file-delete)
;; Phase 8 FFI — host-primitive BIFs (loops/fed-prims)
(er-register-pure-bif! "crypto" "hash" 2 er-bif-crypto-hash) (er-register-pure-bif! "crypto" "hash" 2 er-bif-crypto-hash)
(er-register-pure-bif! "cid" "from_bytes" 1 er-bif-cid-from-bytes) (er-register-pure-bif! "cid" "from_bytes" 1 er-bif-cid-from-bytes)
(er-register-pure-bif! "cid" "to_string" 1 er-bif-cid-to-string) (er-register-pure-bif! "cid" "to_string" 1 er-bif-cid-to-string)
(er-register-bif! "file" "list_dir" 1 er-bif-file-list-dir) (er-register-bif! "file" "list_dir" 1 er-bif-file-list-dir)
(er-mk-atom "ok"))) (er-mk-atom "ok")))
;; Register everything at load time. (er-register-bif! "http" "listen" 2 er-bif-http-listen)
(er-register-builtin-bifs!) (er-register-builtin-bifs!)

96
next/tests/http_listen_bif.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# next/tests/http_listen_bif.sh — Step 8a acceptance test.
#
# Verifies the http:listen/2 BIF wrapper is registered and
# validates its arguments. We do NOT exercise the actual listen
# loop — http-listen blocks forever, so production callers spawn
# an Erlang process to host the call. The BIF wrapper itself is
# tested for: registration, integer port enforcement, function
# handler enforcement.
#
# This BIF is the briefing's allowed-exception scope addition
# to lib/erlang/runtime.sx. 5 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")
;; BIF registered under http/listen/2
(epoch 10)
(eval "(not (= (er-lookup-bif \"http\" \"listen\" 2) nil))")
;; BIF is non-pure (side effect: opens a socket)
(epoch 11)
(eval "(get (er-lookup-bif \"http\" \"listen\" 2) :pure?)")
;; Non-integer port -> badarg
(epoch 12)
(eval "(get (erlang-eval-ast \"try http:listen(not_a_number, fun () -> ok end) catch error:badarg -> ok end\") :name)")
;; Non-fun handler -> badarg
(epoch 13)
(eval "(get (erlang-eval-ast \"try http:listen(8080, not_a_fun) catch error:badarg -> ok end\") :name)")
;; Wrong arity not registered (http/listen/1 should be nil)
(epoch 14)
(eval "(= (er-lookup-bif \"http\" \"listen\" 1) nil)")
EPOCHS
OUTPUT=$(timeout 60 "$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="<no output for epoch $epoch>"
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 10 "BIF registered under http/listen/2" "true"
check 11 "BIF marked non-pure" "false"
check 12 "non-integer port -> badarg" "ok"
check 13 "non-fun handler -> badarg" "ok"
check 14 "no /1 arity registered" "true"
TOTAL=$((PASS+FAIL))
if [ $FAIL -eq 0 ]; then
echo "ok $PASS/$TOTAL next/tests/http_listen_bif.sh passed"
else
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
echo "$ERRORS"
fi
[ $FAIL -eq 0 ]

View File

@@ -505,6 +505,12 @@ publish(ActorId, ActivityRequest) ->
## Step 8 — HTTP server + endpoints ## Step 8 — HTTP server + endpoints
**Sub-deliverables:**
- [x] **8a**`http:listen/2` BIF wrapper in `lib/erlang/runtime.sx` (the briefing's allowed exception). Validates args, bridges Erlang handler funs to SX-callable lambdas via `er-of-sx`/`er-to-sx`, delegates to the native `http-listen` primitive in `bin/sx_server.ml`. Tests verify registration + arg validation (not the blocking listen loop). `next/tests/http_listen_bif.sh` (5 cases).
- [ ] **8b**`next/kernel/http_server.erl`: `start/1(Port)` spawns an Erlang process hosting `http:listen/2`, routes requests via `route/1`. Returns the Pid for shutdown.
- [ ] **8c**`route/1`: dispatch table for GET /actors/{id}, /outbox, /artifacts/{cid}, /projections, POST /activity (Step 6e auth via bearer token), /.well-known/sx-capabilities, /.well-known/webfinger.
- [ ] **8d** — Content negotiation by Accept header: application/activity+json (default), application/cbor, application/json, application/sx.
**Deliverables:** **Deliverables:**
Core endpoints (per design §16.1): Core endpoints (per design §16.1):
@@ -977,6 +983,7 @@ A few things still under-specified; resolve as work begins.
Newest first. One line per sub-deliverable commit. Erlang conformance gate Newest first. One line per sub-deliverable commit. Erlang conformance gate
(`bash lib/erlang/conformance.sh`) must remain 729/729 on every entry. (`bash lib/erlang/conformance.sh`) must remain 729/729 on every entry.
- **2026-05-28** — Step 8a: `http:listen/2` BIF wrapper added to `lib/erlang/runtime.sx` (the briefing's single allowed scope exception). The BIF takes `(Port, Handler)`, validates Port is an integer and Handler is an Erlang fun (else `badarg`), then builds an SX-callable bridge lambda that marshals request dict↔Erlang term via `er-of-sx`/`er-to-sx` and calls `er-apply-fun` on the handler. Delegates to the native `http-listen` primitive (registered in `bin/sx_server.ml`, native-only). Tests verify registration + arg validation paths (the blocking listen loop itself is not exercised — production callers spawn an Erlang process to host the call). `next/tests/http_listen_bif.sh` 5/5; Erlang conformance preserved at 729/729 despite the runtime.sx edit. Step 8 broken into 8a8d on the plan.
- **2026-05-28** — Step 7c: `outbox:publish` now broadcasts the signed activity to every projection process named in `Context`'s `:projections` entry — fired immediately after `log:append`, via `projection:async_fold`. Missing/nil/empty list is a no-op (preserves the Step 6d-publish contract). Stage halts (replay duplicate, sig failure) suppress the broadcast — projection state stays at zero while the activity is rejected. `next/tests/outbox_broadcast.sh` 14/14 covers single + multi projection fan-out, three-publish accumulation, replay-skip, sig-skip, and the projection receiving the post-sign Signed envelope (not the pre-sign skeleton). Erlang conformance 729/729. - **2026-05-28** — Step 7c: `outbox:publish` now broadcasts the signed activity to every projection process named in `Context`'s `:projections` entry — fired immediately after `log:append`, via `projection:async_fold`. Missing/nil/empty list is a no-op (preserves the Step 6d-publish contract). Stage halts (replay duplicate, sig failure) suppress the broadcast — projection state stays at zero while the activity is rejected. `next/tests/outbox_broadcast.sh` 14/14 covers single + multi projection fan-out, three-publish accumulation, replay-skip, sig-skip, and the projection receiving the post-sign Signed envelope (not the pre-sign skeleton). Erlang conformance 729/729.
- **2026-05-28** — Step 7b: `projection.erl` extended with gen_server callbacks + per-projection named-process API. `start_link/3(Name, InitialState, FoldFn)` spawns and registers under the supplied atom; `async_fold/2(Name, Activity)` casts a fold message; `query/1(Name)` synchronously returns the current state. Same port quirks as registry gen_server (Step 5b): raw Pid return, no `?MODULE` macro, processes don't survive between separate `erlang-eval-ast` calls — tests inline start_link with operations. Two named projections are independent. Snapshot persistence deferred to a later sub-step (needs SX-source eval + on-disk state). `next/tests/projection_server.sh` 11/11. Erlang conformance 729/729. - **2026-05-28** — Step 7b: `projection.erl` extended with gen_server callbacks + per-projection named-process API. `start_link/3(Name, InitialState, FoldFn)` spawns and registers under the supplied atom; `async_fold/2(Name, Activity)` casts a fold message; `query/1(Name)` synchronously returns the current state. Same port quirks as registry gen_server (Step 5b): raw Pid return, no `?MODULE` macro, processes don't survive between separate `erlang-eval-ast` calls — tests inline start_link with operations. Two named projections are independent. Snapshot persistence deferred to a later sub-step (needs SX-source eval + on-disk state). `next/tests/projection_server.sh` 11/11. Erlang conformance 729/729.
- **2026-05-28** — Step 7a: `next/kernel/projection.erl` — pure-functional projection driver. Record shape `[{name, _}, {state, _}, {fold, fun}]`; `fold_activity/2` advances state by one activity; `replay/2` folds a whole list (mirrors `log:entries/1` semantics); `new/2` defaults to the identity fold and `new/3` accepts a custom Erlang fun. Multiple projections share no state — independent record values. Step 7 split into 7a (done) + 7b (gen_server-per-projection) + 7c (broadcast hook from outbox) + 7d (sandbox eval, needs SX-source bridge). `next/tests/projection_pure.sh` 12/12. Erlang conformance 729/729. - **2026-05-28** — Step 7a: `next/kernel/projection.erl` — pure-functional projection driver. Record shape `[{name, _}, {state, _}, {fold, fun}]`; `fold_activity/2` advances state by one activity; `replay/2` folds a whole list (mirrors `log:entries/1` semantics); `new/2` defaults to the identity fold and `new/3` accepts a custom Erlang fun. Multiple projections share no state — independent record values. Step 7 split into 7a (done) + 7b (gen_server-per-projection) + 7c (broadcast hook from outbox) + 7d (sandbox eval, needs SX-source bridge). `next/tests/projection_pure.sh` 12/12. Erlang conformance 729/729.