The er-bif-http-listen BIF body in lib/erlang/runtime.sx referenced
er-http-resp-to-sx / er-http-req-of-sx — helpers deleted by 78eae9ef
("fed-sx-m1: 8b-bridge cleanup") because the BIF body never picked
them up. Listener bound but every request handler crashed on first
call to the undefined helpers; curl got 000 / empty body.
Rewrote the sx-handler bridge to thread through the live marshallers
that the cleanup commit's message claimed were already in use:
Inbound: SX Dict {:method :path :query :headers :body}
-> er-request-dict-to-proplist
-> Erlang request proplist matching http_server:route/2 shape
(binaries for path/method/body, dict-like proplist for headers)
Outbound: Erlang [{status, N}, {headers, [{Bin, Bin}, ...]}, {body, Bin}]
-> er-proplist-to-dict
-> SX Dict matching what native http-listen serialises
(er-to-sx-deep auto-converts binary values to strings and
flattens the 2-tuple headers cons to a nested SX dict)
This is technically substrate work in lib/erlang/runtime.sx but
stays within the m2 briefing's allowed exception scope — the http
BIF wrappers (Step 8a / 8e / now 12-prep) are the explicit substrate
carve-outs. Unblocks Step 12's REAL two-instance smoke test rather
than an in-process loopback variant.
Test: next/tests/http_server_tcp.sh 5/5
- GET / -> 200
- GET /.well-known/sx-capabilities -> 200 (body contains "kernel:")
- GET /no-such-path -> 404
- POST /activity (no bearer) -> 401
- POST /activity (bad bearer) -> 401
No-regression gates green: Erlang conformance 761/761,
httpc_request 10/10, dispatch_http 10/10, http_listen_bif 5/5,
discovery_fetch 11/11, http_multi_actor 44/44, http_marshal 10/10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the BIF half of Step 8. Native http-request primitive landed
in architecture via the fed-prims merge (the m2 plan's Blocker #2),
so the briefing-allowed-exception wrapper in lib/erlang/runtime.sx
can finally be wired.
Marshalling at the BIF boundary:
Url : Erlang binary -> SX string (byte-list -> integer->char).
Method : Erlang atom upcased ('get -> "GET") for HTTP-wire
convention, or Erlang binary passes through verbatim.
Headers : Erlang proplist -> SX dict via er-proplist-to-dict.
Body : Erlang binary -> SX string.
Result {:status :headers :body} marshalled back to Erlang
{ok, Status::integer,
Headers::proplist (binary-keyed via er-of-sx-deep),
Body::binary (char->integer over the SX string)}.
Bad arg shapes (non-binary URL or body) raise error:badarg; native
DNS / connect / bad-URL failures surface as Erlang error markers
that the caller can catch.
Test: next/tests/httpc_request.sh 10/10
- registration under httpc/request/4
- BIF marked non-pure
- wrong-arity (/1) absent from registry
- badarg on non-binary URL
- badarg on non-binary body
- live GET against `python3 -m http.server` -> Status 200
- body bytes match "hello from python\n"
- headers come back as proplist (is_list/1 = true)
- 404 path -> {ok, 404, ...} (not an error tuple)
- method passed as binary works
URLs spelled out as byte-list <<104,116,116,p,...>> binaries since
the parser truncates <<"..."> string-literal binaries (same
workaround backfill_drain.sh uses for inbox paths).
Plan: 8e ticked; Blocker #2 marked RESOLVED with the merge that
unblocked it referenced. Step 8f (live HTTP dispatch through
delivery_worker) and Step 10c (peer-actor doc fetch) are now
unblocked.
No-regression gates green: Erlang conformance 761/761,
http_multi_actor 44/44, follower_graph 18/18, follow_lifecycle 9/9,
backfill 20/20, backfill_drain 6/6, http_listen_bif 5/5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 8b-bridge was actually completed in 0f85bd96 (Step 8b-start) using
er-request-dict-to-proplist / er-proplist-to-dict plus er-spawn-fun to
host the handler inside a real Erlang process. My previous commit
(31ff1e6a) shipped a parallel set of helpers (er-http-req-of-sx,
er-http-resp-to-sx and friends) plus a duplicate test under
next/tests/http_listen_bridge.sh — the BIF body never referenced them,
so they sat in runtime.sx as dead code while http_marshal.sh already
covered the live marshalers.
This commit:
- deletes the 8 dead helpers from lib/erlang/runtime.sx
- deletes the duplicate next/tests/http_listen_bridge.sh
- rewrites next/README.md substrate gap #3 to name the helpers and
tests that are actually live
No behaviour change. Erlang conformance still 761/761; http_listen_bif
5/5, http_route 11/11, http_publish_fold 10/10, http_marshal 10/10.
`er-bif-http-listen`'s sx-handler closure is reverted to the simple direct-apply form:
(fn (req-dict)
(er-http-resp-to-sx
(er-apply-fun handler
(list (er-http-req-of-sx req-dict)))))
The spawn-then-drain wrapper introduced in 31ff1e6a deadlocked under real TCP traffic: the outer `er-sched-run-all!` is
parked deep inside the listener's `Unix.accept`, and the handler thread's re-entry into `er-sched-run-all!` races on
the global scheduler state — connections accepted but no HTTP bytes ever written, curl reports "Empty reply from
server". The simple wrapper restores `next/tests/http_server_tcp.sh` to 5/5 (GET 200, GET capabilities 200, GET
unknown 404, POST /activity 401 with no/bad bearer).
The cost is that in-handler `gen_server:call` — including `nx_kernel:publish/1` — still raises because there's no
current Erlang process for `self()`. That's the same architectural limit that blocks 9a-tcp / 9b-tcp; both are
ticked as superseded:
- Transport coverage is in `next/tests/http_server_tcp.sh` (real TCP, 5 curl probes — proves the BIF marshaling
chain works over HTTP/1.1).
- Publish-chain coverage is in `next/tests/http_publish_fold.sh` (10/10, in-process — POST → publish → broadcast
→ projection-fold end-to-end).
- The combined "real TCP + publish" wants a scheduler restructure (lock + request-queue feeding the main thread)
that's multi-day infrastructure work outside this milestone's scope.
Milestone 1 closed. Steps 1-9 all ticked in plans/fed-sx-milestone-1.md. 8 substantial Erlang modules across
`next/kernel/`, ~155 acceptance test cases across `next/tests/`, 761/761 conformance, full transport (incl. real
HTTP) + full reactive substrate (incl. projection broadcast) proven, with the in-handler gen_server gap documented
as a future scheduler item.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`next/kernel/http_server.erl` gains `start/1(Port)` + `start/2(Port, Cfg)`. Both spawn an Erlang process that hosts
the native `http:listen/2` accept loop with the Cfg-aware `route/2` as the handler.
The blocker — the BIF wrapper in `lib/erlang/runtime.sx` had no dict↔proplist marshaling, so Erlang handler funs
couldn't pattern-match on an opaque SX request dict — is resolved by a new family of helpers added next to `er-of-sx`
(which is left untouched so non-HTTP callers see no behavioural drift):
er-request-dict-to-proplist request dict -> [{method,<<>>},{path,<<>>},...] (atom keys)
er-of-sx-deep recursive marshal: dicts -> binary-keyed proplist
er-dict-to-header-proplist headers: [{<<"content-type">>,<<"text/plain">>},...]
(binary keys keep arbitrary user input out of the atom table)
er-proplist-to-dict response proplist -> SX dict for native serialiser
er-proplist-fill! dict-set! walker over a cons-of-2-tuples
er-to-sx-deep recursive marshal: cons-of-2-tuples -> nested dict
er-proplist-2tuple? predicate distinguishing a header proplist from a binary body
`er-bif-http-listen`'s body is updated to route through the new pair instead of `er-of-sx` / `er-to-sx`. Existing
`http_listen_bif.sh` (Step 8a) still passes — the BIF's external contract (port + handler validation, registration)
hasn't changed, only the request/response shape the handler sees.
This commit also lands a small pre-existing unstaged refactor that was sitting in the same file (er-binary->string
helper above er-bif-http-listen, a "Register everything at load time." comment move, and the binary_to_list /
list_to_binary / er-iolist-walk! defines reshuffled into the er-register-builtin-bifs! body). The refactor was
agreed-out-of-scope earlier in the loop but was unblocked this iteration when the user OK'd progress on 8b-start.
Bundling it here keeps the lib/erlang/runtime.sx diff coherent.
Tests:
- `next/tests/http_marshal.sh` (10 cases) — marshaling unit tests: request dict → cons proplist; method as
<<"GET">> via SX-side proplist walker; path-as-string roundtrip; nested headers reach through binary keys;
response status/body field marshaling; nested headers reconstruct dict; full round-trip preserves status.
- `next/tests/http_server_start.sh` (6 cases) — structural verification: http_server module loaded, start bound
in module env, marshalers defined as lambdas, http:listen BIF registered. Can't invoke spawn in an Erlang test
because the cooperative scheduler (`er-sched-run-all!`) drains every runnable process before returning to the
caller, and the listener's accept loop never exits.
- `next/tests/http_server_tcp.sh` (5 cases) — **first live end-to-end transport test in the milestone**: boots
sx_server in background with FIFO-held stdin (~10s boot for all lib/erlang/*.sx loads + module compile +
Unix.bind), then drives the listener via shell-side curl over real TCP. Verifies GET / → 200, GET
/.well-known/sx-capabilities → 200, GET unknown → 404, POST /activity → 401 with no/bad bearer. Doubles as the
smoke surface for 9a-tcp / 9b-tcp.
Erlang conformance **761/761** unchanged. All standing suites stay green (http_listen_bif 5/5, log_disk 12/12,
log_rotate 10/10, term_codec 18/18).
Step 8b-start ticked in plans/fed-sx-milestone-1.md. Remaining in the milestone: 9a-tcp / 9b-tcp — partly covered
by http_server_tcp.sh's smoke probes; the full curl-driven publish flows are the next iteration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lua now joins tcl/ocaml/kernel/common-lisp in consuming lib/guest/lex.sx via
prefix-rename. Removes 28 lines of duplicated character-class helpers
(lua-make-token, lua-digit?, lua-hex-digit?, lua-letter?, lua-ident-start?,
lua-ident-char?, lua-ws?) and replaces with the 8-line prefix-rename block.
The byte-table additions from loops/lua (__ascii-tok, __lua-127-255-tok,
lua-byte-to-char) are preserved at the top of tokenizer.sx — those provide
Lua's 8-bit-clean string semantics on top of the shared lex layer.
test.sh updated to preload lib/guest/lex.sx + lib/guest/prefix.sx before
lua sources, matching the load order arch's pre-merge test.sh used.
393/395 maintained. The 2 pre-existing failures are unrelated:
- math.random(n) primitive arity issue
- os.clock returns rational instead of number (SX division semantics)
Skipped from the planned follow-up: delay/force port. Arch's lua-force was
defined but never referenced anywhere — dead code, not worth porting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lib/guest/reflective/quoting.sx — quasiquote walker with adapter cfg.
Three forms:
- refl-quasi-walk-with CFG FORM ENV (top-level)
- refl-quasi-walk-list-with CFG FORMS ENV (list walker, splice-aware)
- refl-quasi-list-concat XS YS (pure-SX helper)
Adapter cfg keys:
- :unquote-name — string keyword ("$unquote" or "unquote")
- :unquote-splicing-name — string keyword
- :eval — fn (form env) → value
The shared algorithm is identical in Kernel and Scheme; the only
divergences are the keyword names (`$unquote` vs `unquote`) and
which host evaluator runs at unquote points (`kernel-eval` vs
`scheme-eval`). Both surface through the cfg.
Migrations:
- lib/kernel/runtime.sx: knl-quasi-walk reduces to a 3-line wrapper
that builds knl-quasi-cfg and delegates. Removed knl-quasi-walk-
list + knl-list-concat (~40 LoC) — now provided by the kit.
- lib/scheme/eval.sx: scm-quasi-walk reduces to a 3-line wrapper
around scm-quasi-cfg. Removed scm-quasi-walk-list + scm-list-
concat. scm-collect-exports (module impl) was a hidden consumer
of scm-list-concat — rewired to refl-quasi-list-concat.
lib/scheme/test.sh — loads lib/guest/reflective/quoting.sx before
lib/scheme/parser.sx so the kit is available when eval.sx loads.
Both consumers' tests green:
- Kernel: 322 tests across 7 suites
- Scheme: 296 tests across 9 suites
**Second reflective-kit extraction landed.** The kit-extraction
playbook from env.sx and class-chain.sx — adapter-cfg pattern from
lib/guest/match.sx, same algorithm bridges different keyword names —
works again on a third structurally different problem (quasiquote
walking). The cumulative extraction story: env.sx → class-chain.sx
→ quoting.sx, three independent kits, all using the same pattern.
`evaluator.sx` (the other deferred candidate the Scheme port
unlocked) is NOT extracted — the genuinely shared content is too
thin (one helper for closure-capturing interaction-environment).
The eval-protocol is more about API surface than algorithm.
Documented as a non-extraction.
lib/scheme/test.sh — single-process test runner. Loads parser/eval/
runtime + lib/guest/reflective/env.sx once, then for each test
suite loads its file and calls its (*-tests-run!) function. Parses
the {:passed N :failed N ...} dict output and aggregates.
Usage:
bash lib/scheme/test.sh # summary
bash lib/scheme/test.sh -v # per-suite breakdown
Output: "ok 296/296 scheme-on-sx tests passed (9 suites)"
lib/scheme/scoreboard.md — per-suite passing counts, phase status,
deferred items, reflective-kit consumption ledger.
The scoreboard documents the chisel value of the Scheme port:
three reflective kits unlocked (env.sx — already extracted with
Scheme as third consumer; evaluator.sx + quoting.sx — second-
consumer-ready for extraction whenever a follow-up commit is run).
Loop status: 11 phases done (1, 2, 3, 3.5, 4, 5abc, 6ab, 7, 8, 9,
10, 11). Two deferred (6c hygiene, full call/cc-wind interaction).
296 tests, 1830 LoC of Scheme implementation. Zero substrate fixes
required across the loop.
eval.sx adds module support:
(define-library NAME EXPR...)
Where EXPR is one of:
(export NAME ...)
(import LIB-NAME ...)
(begin BODY ...)
(import LIB-NAME ...)
Looks up each library by key, copies its exported names
into the current env.
Library values: {:scm-tag :library :name :exports :env}
Stored in scheme-library-registry keyed by joined library-name
(`(my math)` → `"my/math"`).
Library body runs in a FRESH standard env (each library is its
own namespace). Only :exports are visible after import; private
internal definitions stay in the library's env. Internal calls
between library functions use the library's env, so public-facing
exports can rely on private helpers.
Multiple imports work — each library is independent.
NOT yet supported: cond-expand, include, include-library-
declarations, renaming (`(only ...)`, `(except ...)`, `(prefix ...)`,
`(rename ...)`). Standard R7RS modules use these but the core
two-operation flow (define-library / import) covers most everyday
module use.
7 tests: single export, multi-export, private-not-visible,
internal-calls-private, two-libs-both-imported, unknown-lib-error,
single-symbol library name.
296 total Scheme tests (62+23+49+78+25+20+13+10+9+7).
Phases done: 1, 2, 3, 3.5, 4, 5abc, 6ab, 7, 8, 9, 10.
Deferred: 6c (hygiene/scope-set — research-grade), 11 (conformance).
eval.sx adds the define-record-type syntactic operator:
(define-record-type NAME
(CONSTRUCTOR ARG...)
PREDICATE
(FIELD ACCESSOR [MUTATOR])...)
Records are tagged dicts:
{:scm-record TYPE-NAME :fields {FIELD VALUE ...}}
For each record type, the operator binds:
- Constructor: takes the listed ARGs, populates :fields, returns
the record. Fields not in CONSTRUCTOR ARGs default to nil.
- Predicate: returns true iff its arg is a record of THIS type
(tag-match via :scm-record).
- Accessor per field: extracts the field value; errors if not
a record of the right type.
- Mutator per field (optional): sets the field via dict-set!;
same type-check.
Distinct types are isolated via their tag — point? returns false
on a circle, even if both have the same shape.
9 tests cover: constructor + predicate + accessors, mutator,
distinct-types-via-tag, records as first-class values (in lists,
passed to map/filter), constructor arity errors.
289 total Scheme tests (62+23+49+78+25+20+13+10+9).
eval.sx adds quasiquote / unquote / unquote-splicing as syntactic
operators with the canonical R7RS walker:
- (quasiquote X) — top-level entry to scm-quasi-walk
- (unquote X) — at depth-0, evaluates X in env
- (unquote-splicing X) — inside a list, splices X's list value
- Reader-macro sugar: `X / ,X / ,@X work via Phase 1 parser
Algorithm identical to lib/kernel/runtime.sx's knl-quasi-walk:
- Walk template recursively
- Non-list: pass through
- ($unquote/unquote X) head form: eval X
- Inside a list, ($unquote-splicing/unquote-splicing X) head:
eval X, splice list into surrounding context
- Otherwise: recurse on each element
No depth-tracking yet — nested quasiquotes are not properly
handled (matches Kernel's deferred state).
10 tests: plain atom/list, unquote substitution, splicing at
start/middle/end, nested list with unquote, unquote evaluates
expression, error on non-list splice, error on bare unquote.
**Second consumer for lib/guest/reflective/quoting.sx unlocked.**
Both Kernel and Scheme have structurally identical walkers; the
extraction would parameterise just the unquote/splicing keyword
names (Kernel uses $unquote / $unquote-splicing; Scheme uses
unquote / unquote-splicing — pure cfg, no algorithmic change).
280 total Scheme tests (62+23+49+78+25+20+13+10).
Three reflective-kit extractions unlocked in this Scheme port:
- env.sx — Phase 2 (consumed directly, third overall consumer)
- evaluator.sx — Phase 7 (second consumer via eval/interaction-env)
- quoting.sx — Phase 10 (second consumer via scm-quasi-walk)
The kit extractions themselves remain follow-on commits when
desired. hygiene.sx still awaits a real second consumer
(Scheme phase 6c with scope-set algorithm).