fed-sx-m1: Step 4c — bootstrap:read_genesis/0,1 + 5 helpers + 15 read tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 14m10s

This commit is contained in:
2026-05-27 23:50:45 +00:00
parent ae5df5cfa1
commit 73a1a55572
3 changed files with 221 additions and 1 deletions

96
next/kernel/bootstrap.erl Normal file
View File

@@ -0,0 +1,96 @@
-module(bootstrap).
-export([read_genesis/0, read_genesis/1,
read_section/2, sections/0, section_subdir/1,
default_base/0, ends_with_sx/1]).
%% Genesis bundle reader per design §12.2.
%%
%% read_genesis/0,1 walks the seven canonical section subdirectories
%% under `next/genesis/`, filters .sx files, reads each file into a
%% binary, and returns a structured snapshot:
%%
%% {ok, [{Section :: atom,
%% [{FileName :: binary, FileBytes :: binary}, ...]},
%% ...]}
%%
%% Step 4d will compute the bundle CID by hashing the assembled
%% byte string across all entries; Step 4e will register the parsed
%% definitions in the kernel registry.
%%
%% Port note: this module does NOT parse the .sx contents. The
%% Erlang-on-SX port has no in-Erlang path from binary bytes to SX
%% structured terms (same substrate gap that parked Step 3b); the
%% bundle CID needs only the raw bytes, and registry registration
%% will happen via an SX-side helper that the kernel hands the
%% binary contents to. read_genesis/1 ignores its arg in v1 except
%% to swap the BasePath — `default_base/0` is "next/genesis".
%%
%% Port note 2: string-literal binary segments `<<"abc">>` truncate
%% to one byte in this port, so all path constants are hand-spelled
%% as integer-segment binaries.
%% ── Public API ──────────────────────────────────────────────────
%% "next/genesis"
default_base() ->
<<110,101,120,116,47,103,101,110,101,115,105,115>>.
read_genesis() ->
read_genesis(default_base()).
read_genesis(BasePath) ->
{ok, lists:map(
fun (S) -> {S, read_section(BasePath, S)} end,
sections())}.
sections() ->
[activity_types, object_types, projections,
validators, codecs, sig_suites, audience].
%% "activity-types"
section_subdir(activity_types) ->
<<97,99,116,105,118,105,116,121,45,116,121,112,101,115>>;
%% "object-types"
section_subdir(object_types) ->
<<111,98,106,101,99,116,45,116,121,112,101,115>>;
%% "projections"
section_subdir(projections) ->
<<112,114,111,106,101,99,116,105,111,110,115>>;
%% "validators"
section_subdir(validators) ->
<<118,97,108,105,100,97,116,111,114,115>>;
%% "codecs"
section_subdir(codecs) ->
<<99,111,100,101,99,115>>;
%% "sig-suites"
section_subdir(sig_suites) ->
<<115,105,103,45,115,117,105,116,101,115>>;
%% "audience"
section_subdir(audience) ->
<<97,117,100,105,101,110,99,101>>.
read_section(BasePath, Section) ->
SubDir = section_subdir(Section),
%% 47 = '/'
Path = <<BasePath/binary, 47, SubDir/binary>>,
case file:list_dir(Path) of
{ok, Names} ->
SxNames = lists:filter(fun (N) -> ends_with_sx(N) end, Names),
lists:map(fun (Name) -> read_one(Path, Name) end, SxNames);
{error, _} ->
[]
end.
%% Suffix check on the .sx extension. 46='.' 115='s' 120='x'.
ends_with_sx(<<46, 115, 120>>) -> true;
ends_with_sx(<<>>) -> false;
ends_with_sx(<<_, Rest/binary>>) -> ends_with_sx(Rest).
%% ── Internal ────────────────────────────────────────────────────
read_one(DirPath, Name) ->
Full = <<DirPath/binary, 47, Name/binary>>,
case file:read_file(Full) of
{ok, Bytes} -> {Name, Bytes};
{error, R} -> {Name, {error, R}}
end.

123
next/tests/bootstrap_read.sh Executable file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# next/tests/bootstrap_read.sh — Step 4c acceptance test.
#
# Exercises bootstrap:read_genesis/0, read_section/2, sections/0,
# section_subdir/1, ends_with_sx/1. Verifies per-section file
# counts match the manifest authored in Steps 4a/4b. 14 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 "(get (erlang-load-module (file-read \"next/kernel/bootstrap.erl\")) :name)")
;; sections/0 returns 7 atoms
(epoch 10)
(eval "(erlang-eval-ast \"length(bootstrap:sections())\")")
;; ends_with_sx — positive on "create.sx", negative on "hello"
(epoch 11)
(eval "(get (erlang-eval-ast \"bootstrap:ends_with_sx(<<99,114,101,97,116,101,46,115,120>>)\") :name)")
(epoch 12)
(eval "(get (erlang-eval-ast \"bootstrap:ends_with_sx(<<104,101,108,108,111>>)\") :name)")
(epoch 13)
(eval "(get (erlang-eval-ast \"bootstrap:ends_with_sx(<<>>)\") :name)")
;; Per-section file counts match the manifest (3/10/7/3/3/2/3)
(epoch 20)
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), activity_types))\")")
(epoch 21)
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), object_types))\")")
(epoch 22)
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), projections))\")")
(epoch 23)
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), validators))\")")
(epoch 24)
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), codecs))\")")
(epoch 25)
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), sig_suites))\")")
(epoch 26)
(eval "(erlang-eval-ast \"length(bootstrap:read_section(bootstrap:default_base(), audience))\")")
;; read_genesis/0 returns {ok, [{Section, Entries}, ...]} with 7 entries
(epoch 30)
(eval "(erlang-eval-ast \"{ok, G} = bootstrap:read_genesis(), length(G)\")")
;; First entry is {activity_types, [_,_,_]}
(epoch 31)
(eval "(get (erlang-eval-ast \"{ok, G} = bootstrap:read_genesis(), {S, Entries} = hd(G), S\") :name)")
;; Each entry has the right number of files
(epoch 32)
(eval "(erlang-eval-ast \"{ok, G} = bootstrap:read_genesis(), {_, E} = hd(G), length(E)\")")
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="<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 2 "module load name" "bootstrap"
check 10 "sections/0 length" "7"
check 11 "ends_with_sx create.sx" "true"
check 12 "ends_with_sx hello" "false"
check 13 "ends_with_sx empty" "false"
check 20 "section activity_types count" "3"
check 21 "section object_types count" "10"
check 22 "section projections count" "7"
check 23 "section validators count" "3"
check 24 "section codecs count" "3"
check 25 "section sig_suites count" "2"
check 26 "section audience count" "3"
check 30 "read_genesis returns 7 sections" "7"
check 31 "first section name" "activity_types"
check 32 "first section entry count" "3"
TOTAL=$((PASS+FAIL))
if [ $FAIL -eq 0 ]; then
echo "ok $PASS/$TOTAL next/tests/bootstrap_read.sh passed"
else
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
echo "$ERRORS"
fi
[ $FAIL -eq 0 ]