Merge loops/erlang into architecture: Phase 8 host-primitive BIFs (crypto/cid/file:list_dir)
Wires the 3 previously-BLOCKED Phase 8 FFI BIFs against loops/fed-prims
primitives (merged at 380bc69f):
- crypto:hash/2 → crypto-sha256/sha512/sha3-256 (atom dispatch, raw-binary
return via er-hex->bytes), +6 ffi tests
- cid:from_bytes/1 → CIDv1 raw-codec (0x55) + sha2-256 multihash assembled
in SX; cid:to_string/1 → cid-from-sx of canonical er-format-value string,
+7 ffi tests
- file:list_dir/1 → file-list-dir, {ok,[Binary]} / {error,Reason} reusing
er-classify-file-error, +4 ffi tests
ffi suite 14 → 28 (3 BLOCKED negative-asserts flipped to functional tests).
httpc:request and sqlite:* remain BLOCKED — need HTTP-client and SQLite
host primitives which loops/fed-prims didn't deliver.
Full conformance 729/729 (eval 385, vm 78, ffi 28, all process suites).
This commit is contained in:
@@ -1372,6 +1372,97 @@
|
|||||||
(er-classify-file-error (nth err 0))))
|
(er-classify-file-error (nth err 0))))
|
||||||
:else (er-mk-atom "ok")))))))
|
:else (er-mk-atom "ok")))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; ── crypto / cid / file:list_dir (Phase 8 FFI — host primitives) ──
|
||||||
|
;; Wired against loops/fed-prims host primitives (see plans Blockers
|
||||||
|
;; "RESOLVED 2026-05-18"). Term marshalling at the boundary:
|
||||||
|
;; Erlang binary/string/charlist -> SX byte-string via er-source-to-string;
|
||||||
|
;; results -> Erlang binary via er-mk-binary.
|
||||||
|
|
||||||
|
(define er-hexval
|
||||||
|
(fn (c)
|
||||||
|
(let ((v (char->integer c)))
|
||||||
|
(cond
|
||||||
|
(and (>= v 48) (<= v 57)) (- v 48) ;; 0-9
|
||||||
|
(and (>= v 97) (<= v 102)) (+ 10 (- v 97)) ;; a-f
|
||||||
|
(and (>= v 65) (<= v 70)) (+ 10 (- v 65)) ;; A-F
|
||||||
|
:else 0))))
|
||||||
|
|
||||||
|
(define er-hex->bytes
|
||||||
|
(fn (hex)
|
||||||
|
(let ((cs (string->list hex)) (out (list)) (n (string-length hex)))
|
||||||
|
(for-each
|
||||||
|
(fn (i)
|
||||||
|
(append! out
|
||||||
|
(+ (* 16 (er-hexval (nth cs (* i 2))))
|
||||||
|
(er-hexval (nth cs (+ (* i 2) 1))))))
|
||||||
|
(range 0 (truncate (/ n 2))))
|
||||||
|
out)))
|
||||||
|
|
||||||
|
;; crypto:hash(Type, Data) -> raw digest binary. Type is an Erlang
|
||||||
|
;; atom (sha256 | sha512 | sha3_256). Bad type / non-binary -> badarg.
|
||||||
|
(define er-bif-crypto-hash
|
||||||
|
(fn (vs)
|
||||||
|
(let ((ty (nth vs 0)) (data (er-source-to-string (nth vs 1))))
|
||||||
|
(cond
|
||||||
|
(or (not (er-atom? ty)) (= data nil))
|
||||||
|
(raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||||
|
:else
|
||||||
|
(let ((name (get ty :name)))
|
||||||
|
(let ((hex (cond
|
||||||
|
(= name "sha256") (crypto-sha256 data)
|
||||||
|
(= name "sha512") (crypto-sha512 data)
|
||||||
|
(= name "sha3_256") (crypto-sha3-256 data)
|
||||||
|
:else nil)))
|
||||||
|
(cond
|
||||||
|
(= hex nil) (raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||||
|
:else (er-mk-binary (er-hex->bytes hex)))))))))
|
||||||
|
|
||||||
|
;; cid:from_bytes(Bin) -> CIDv1 (raw codec 0x55, sha2-256 multihash)
|
||||||
|
;; as an Erlang binary string.
|
||||||
|
(define er-bif-cid-from-bytes
|
||||||
|
(fn (vs)
|
||||||
|
(let ((data (er-source-to-string (nth vs 0))))
|
||||||
|
(cond
|
||||||
|
(= data nil) (raise (er-mk-error-marker (er-mk-atom "badarg")))
|
||||||
|
:else
|
||||||
|
(let ((digest (er-hex->bytes (crypto-sha256 data))))
|
||||||
|
(let ((mh (list->string
|
||||||
|
(map integer->char (append (list 18 32) digest)))))
|
||||||
|
(er-mk-binary
|
||||||
|
(map char->integer
|
||||||
|
(string->list (cid-from-bytes 85 mh))))))))))
|
||||||
|
|
||||||
|
;; cid:to_string(Term) -> canonical CIDv1 (dag-cbor) of the term,
|
||||||
|
;; as an Erlang binary string.
|
||||||
|
(define er-bif-cid-to-string
|
||||||
|
(fn (vs)
|
||||||
|
;; Canonical CID of the term's stable string form. (cbor-encode
|
||||||
|
;; rejects symbols, so er-to-sx of compound terms is unencodable;
|
||||||
|
;; er-format-value yields a canonical SX string per term value.)
|
||||||
|
(er-mk-binary
|
||||||
|
(map char->integer
|
||||||
|
(string->list (cid-from-sx (er-format-value (nth vs 0))))))))
|
||||||
|
|
||||||
|
;; file:list_dir(Path) -> {ok, [Binary]} | {error, Reason}
|
||||||
|
(define er-bif-file-list-dir
|
||||||
|
(fn (vs)
|
||||||
|
(let ((path (er-source-to-string (nth vs 0))))
|
||||||
|
(cond
|
||||||
|
(= path nil)
|
||||||
|
(er-mk-tuple (list (er-mk-atom "error") (er-mk-atom "badarg")))
|
||||||
|
:else
|
||||||
|
(let ((res (list nil)) (err (list nil)))
|
||||||
|
(guard (c (:else (set-nth! err 0 c)))
|
||||||
|
(set-nth! res 0 (file-list-dir path)))
|
||||||
|
(cond
|
||||||
|
(not (= (nth err 0) nil))
|
||||||
|
(er-mk-tuple (list (er-mk-atom "error")
|
||||||
|
(er-classify-file-error (nth err 0))))
|
||||||
|
:else
|
||||||
|
(er-mk-tuple (list (er-mk-atom "ok")
|
||||||
|
(er-of-sx (nth res 0))))))))))
|
||||||
|
|
||||||
;; ── builtin BIF registrations (Phase 8 migration) ────────────────
|
;; ── builtin BIF registrations (Phase 8 migration) ────────────────
|
||||||
;; Populates `er-bif-registry` with every existing built-in BIF. Each
|
;; Populates `er-bif-registry` with every existing built-in BIF. Each
|
||||||
;; entry is keyed by "Module/Name/Arity"; multi-arity BIFs register
|
;; entry is keyed by "Module/Name/Arity"; multi-arity BIFs register
|
||||||
@@ -1466,6 +1557,11 @@
|
|||||||
(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! "cid" "from_bytes" 1 er-bif-cid-from-bytes)
|
||||||
|
(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-mk-atom "ok")))
|
(er-mk-atom "ok")))
|
||||||
|
|
||||||
;; Register everything at load time.
|
;; Register everything at load time.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"language": "erlang",
|
"language": "erlang",
|
||||||
"total_pass": 715,
|
"total_pass": 729,
|
||||||
"total": 715,
|
"total": 729,
|
||||||
"suites": [
|
"suites": [
|
||||||
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
|
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
|
||||||
{"name":"parse","pass":52,"total":52,"status":"ok"},
|
{"name":"parse","pass":52,"total":52,"status":"ok"},
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
{"name":"bank","pass":8,"total":8,"status":"ok"},
|
{"name":"bank","pass":8,"total":8,"status":"ok"},
|
||||||
{"name":"echo","pass":7,"total":7,"status":"ok"},
|
{"name":"echo","pass":7,"total":7,"status":"ok"},
|
||||||
{"name":"fib","pass":8,"total":8,"status":"ok"},
|
{"name":"fib","pass":8,"total":8,"status":"ok"},
|
||||||
{"name":"ffi","pass":14,"total":14,"status":"ok"},
|
{"name":"ffi","pass":28,"total":28,"status":"ok"},
|
||||||
{"name":"vm","pass":78,"total":78,"status":"ok"}
|
{"name":"vm","pass":78,"total":78,"status":"ok"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Erlang-on-SX Scoreboard
|
# Erlang-on-SX Scoreboard
|
||||||
|
|
||||||
**Total: 715 / 715 tests passing**
|
**Total: 729 / 729 tests passing**
|
||||||
|
|
||||||
| | Suite | Pass | Total |
|
| | Suite | Pass | Total |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
| ✅ | bank | 8 | 8 |
|
| ✅ | bank | 8 | 8 |
|
||||||
| ✅ | echo | 7 | 7 |
|
| ✅ | echo | 7 | 7 |
|
||||||
| ✅ | fib | 8 | 8 |
|
| ✅ | fib | 8 | 8 |
|
||||||
| ✅ | ffi | 14 | 14 |
|
| ✅ | ffi | 28 | 28 |
|
||||||
| ✅ | vm | 78 | 78 |
|
| ✅ | vm | 78 | 78 |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -75,29 +75,94 @@
|
|||||||
"file:write_file(\"/tmp/er-ffi-del2.txt\", \"x\"), file:delete(\"/tmp/er-ffi-del2.txt\"), element(2, file:read_file(\"/tmp/er-ffi-del2.txt\"))"))
|
"file:write_file(\"/tmp/er-ffi-del2.txt\", \"x\"), file:delete(\"/tmp/er-ffi-del2.txt\"), element(2, file:read_file(\"/tmp/er-ffi-del2.txt\"))"))
|
||||||
"enoent")
|
"enoent")
|
||||||
|
|
||||||
;; ── Blocked BIFs (placeholder asserts so the suite documents intent) ──
|
(er-ffi-test
|
||||||
;; crypto:hash/2, cid:from_bytes/1, cid:to_string/1, file:list_dir/1,
|
"crypto:hash sha256 -> 32-byte binary"
|
||||||
;; httpc:request/4, sqlite:* — documented in plans/erlang-on-sx.md
|
(ffi-ev "byte_size(crypto:hash(sha256, <<97,98,99>>))")
|
||||||
;; under Blockers. When the host runtime gains the underlying primitive,
|
32)
|
||||||
;; the wrappers land in runtime.sx and tests appear here. For now we
|
|
||||||
;; assert each is NOT registered, so a future iteration that adds them
|
|
||||||
;; without updating this file fails fast.
|
|
||||||
|
|
||||||
(er-ffi-test
|
(er-ffi-test
|
||||||
"crypto:hash unregistered"
|
"crypto:hash sha512 -> 64-byte binary"
|
||||||
(er-lookup-bif "crypto" "hash" 2)
|
(ffi-ev "byte_size(crypto:hash(sha512, <<97,98,99>>))")
|
||||||
nil)
|
64)
|
||||||
|
|
||||||
(er-ffi-test
|
(er-ffi-test
|
||||||
"cid:from_bytes unregistered"
|
"crypto:hash sha3_256 is_binary"
|
||||||
(er-lookup-bif "cid" "from_bytes" 1)
|
(ffi-nm (ffi-ev "is_binary(crypto:hash(sha3_256, <<120>>))"))
|
||||||
nil)
|
"true")
|
||||||
|
|
||||||
(er-ffi-test
|
(er-ffi-test
|
||||||
"file:list_dir unregistered"
|
"crypto:hash deterministic"
|
||||||
(er-lookup-bif "file" "list_dir" 1)
|
(ffi-nm (ffi-ev "crypto:hash(sha256, <<97>>) =:= crypto:hash(sha256, <<97>>)"))
|
||||||
nil)
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"crypto:hash distinct inputs distinct digests"
|
||||||
|
(ffi-nm (ffi-ev "crypto:hash(sha256, <<97>>) =/= crypto:hash(sha256, <<98>>)"))
|
||||||
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"crypto:hash bad type -> error:badarg"
|
||||||
|
(ffi-nm (ffi-ev "try crypto:hash(md5, <<120>>) catch error:badarg -> ok end"))
|
||||||
|
"ok")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"cid:from_bytes is_binary"
|
||||||
|
(ffi-nm (ffi-ev "is_binary(cid:from_bytes(<<97,98,99>>))"))
|
||||||
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"cid:from_bytes deterministic"
|
||||||
|
(ffi-nm (ffi-ev "cid:from_bytes(<<97,98,99>>) =:= cid:from_bytes(<<97,98,99>>)"))
|
||||||
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"cid:from_bytes distinct inputs distinct CIDs"
|
||||||
|
(ffi-nm (ffi-ev "cid:from_bytes(<<97,98,99>>) =/= cid:from_bytes(<<97,98,100>>)"))
|
||||||
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"cid:from_bytes non-binary -> error:badarg"
|
||||||
|
(ffi-nm (ffi-ev "try cid:from_bytes(42) catch error:badarg -> ok end"))
|
||||||
|
"ok")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"cid:to_string is_binary"
|
||||||
|
(ffi-nm (ffi-ev "is_binary(cid:to_string({ok, 42}))"))
|
||||||
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"cid:to_string deterministic"
|
||||||
|
(ffi-nm (ffi-ev "cid:to_string(foo) =:= cid:to_string(foo)"))
|
||||||
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"cid:to_string distinct terms distinct CIDs"
|
||||||
|
(ffi-nm (ffi-ev "cid:to_string(foo) =/= cid:to_string(bar)"))
|
||||||
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"file:list_dir ok tag"
|
||||||
|
(ffi-nm (ffi-ev "element(1, file:list_dir(\"lib/erlang\"))"))
|
||||||
|
"ok")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"file:list_dir non-empty"
|
||||||
|
(ffi-nm (ffi-ev "case file:list_dir(\"lib/erlang\") of {ok, L} -> length(L) > 3 end"))
|
||||||
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"file:list_dir entries are binaries"
|
||||||
|
(ffi-nm (ffi-ev "case file:list_dir(\"lib/erlang\") of {ok, L} -> is_binary(hd(L)) end"))
|
||||||
|
"true")
|
||||||
|
|
||||||
|
(er-ffi-test
|
||||||
|
"file:list_dir missing enoent"
|
||||||
|
(ffi-nm (ffi-ev "element(2, file:list_dir(\"/no/such/dir/xyz\"))"))
|
||||||
|
"enoent")
|
||||||
|
|
||||||
|
;; ── Still deferred (no host primitive): httpc (HTTP client, v2),
|
||||||
|
;; sqlite-* (v2 indexes). Assert NOT registered so a future iteration
|
||||||
|
;; that wires them without updating this suite fails fast.
|
||||||
(er-ffi-test
|
(er-ffi-test
|
||||||
"httpc:request unregistered"
|
"httpc:request unregistered"
|
||||||
(er-lookup-bif "httpc" "request" 4)
|
(er-lookup-bif "httpc" "request" 4)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ isolation: worktree
|
|||||||
|
|
||||||
## Prompt
|
## Prompt
|
||||||
|
|
||||||
You are the sole background agent working `/root/rose-ash/plans/erlang-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
|
You are the sole background agent working `/root/rose-ash/plans/erlang-on-sx.md`. Isolated worktree, forever, one commit per feature. Push to `origin/loops/erlang` after every commit.
|
||||||
|
|
||||||
## Restart baseline — check before iterating
|
## Restart baseline — check before iterating
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ Every iteration: implement → test → commit → tick `[ ]` → Progress log
|
|||||||
- **Shared-file issues** → plan's Blockers with minimal repro.
|
- **Shared-file issues** → plan's Blockers with minimal repro.
|
||||||
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
|
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
|
||||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
|
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
|
||||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
- **Worktree:** commit, then push to `origin/loops/erlang`. Never touch `main`.
|
||||||
- **Commit granularity:** one feature per commit.
|
- **Commit granularity:** one feature per commit.
|
||||||
- **Plan file:** update Progress log + tick boxes every commit.
|
- **Plan file:** update Progress log + tick boxes every commit.
|
||||||
|
|
||||||
|
|||||||
@@ -115,9 +115,9 @@ Replace today's hardcoded BIF dispatch (`er-apply-bif`/`er-apply-remote-bif` in
|
|||||||
- [x] BIF registry: `er-bif-registry` global dict keyed by `"Module/Name/Arity"`, with `er-register-bif!`/`er-register-pure-bif!`/`er-lookup-bif`/`er-list-bifs`/`er-bif-registry-reset!` helpers — **+18 runtime tests** (600/600 total). Entries are `{:module :name :arity :fn :pure?}`. Arity is part of the key so `m:f/1` and `m:f/2` are independent. Re-registering the same key replaces the previous entry; reset clears.
|
- [x] BIF registry: `er-bif-registry` global dict keyed by `"Module/Name/Arity"`, with `er-register-bif!`/`er-register-pure-bif!`/`er-lookup-bif`/`er-list-bifs`/`er-bif-registry-reset!` helpers — **+18 runtime tests** (600/600 total). Entries are `{:module :name :arity :fn :pure?}`. Arity is part of the key so `m:f/1` and `m:f/2` are independent. Re-registering the same key replaces the previous entry; reset clears.
|
||||||
- [x] Migrate existing local + remote BIFs (length/hd/tl/lists:*/io:format/ets:*/etc.) onto the registry; delete the giant `cond` dispatch in `er-apply-bif`/`er-apply-remote-bif`. Conformance held at **600/600** after migration (baseline was 600, not the plan-text's 530 — the text was authored before Phase 7 work added rows). 67 builtin registrations across `erlang`/`lists`/`io`/`ets`/`code` modules; multi-arity BIFs (`is_function`, `spawn`, `exit`, `io:format`, `lists:seq`, `ets:delete`) register once per arity, all pointing at the same impl which dispatches on `(len vs)` internally. The four per-module cond dispatchers (`er-apply-lists-bif`, `er-apply-io-bif`, `er-apply-ets-bif`, `er-apply-code-bif`) are deleted. `er-apply-bif` and `er-apply-remote-bif` are now ~5-line registry lookups; user modules still win precedence over the registry.
|
- [x] Migrate existing local + remote BIFs (length/hd/tl/lists:*/io:format/ets:*/etc.) onto the registry; delete the giant `cond` dispatch in `er-apply-bif`/`er-apply-remote-bif`. Conformance held at **600/600** after migration (baseline was 600, not the plan-text's 530 — the text was authored before Phase 7 work added rows). 67 builtin registrations across `erlang`/`lists`/`io`/`ets`/`code` modules; multi-arity BIFs (`is_function`, `spawn`, `exit`, `io:format`, `lists:seq`, `ets:delete`) register once per arity, all pointing at the same impl which dispatches on `(len vs)` internally. The four per-module cond dispatchers (`er-apply-lists-bif`, `er-apply-io-bif`, `er-apply-ets-bif`, `er-apply-code-bif`) are deleted. `er-apply-bif` and `er-apply-remote-bif` are now ~5-line registry lookups; user modules still win precedence over the registry.
|
||||||
- [x] Term-marshalling helpers: `er-of-sx` (SX → Erlang) and `er-to-sx` (Erlang → SX). atom ↔ symbol, nil ↔ `()`, cons → list, tuple → list (one-way; tuples flatten), binary ↔ SX string, integer / float / boolean passthrough. **+23 runtime tests** (623/623 total). Erlang maps (`dict ↔ map`) deferred — Erlang map term not implemented in this port; will land when `#{}` syntax does. Pids, refs, funs pass through unchanged. SX strings on the way back become Erlang binaries (most useful FFI return shape).
|
- [x] Term-marshalling helpers: `er-of-sx` (SX → Erlang) and `er-to-sx` (Erlang → SX). atom ↔ symbol, nil ↔ `()`, cons → list, tuple → list (one-way; tuples flatten), binary ↔ SX string, integer / float / boolean passthrough. **+23 runtime tests** (623/623 total). Erlang maps (`dict ↔ map`) deferred — Erlang map term not implemented in this port; will land when `#{}` syntax does. Pids, refs, funs pass through unchanged. SX strings on the way back become Erlang binaries (most useful FFI return shape).
|
||||||
- [ ] `crypto:hash/2` — **BLOCKED** (no `sha256`/`sha512`/`blake3` primitive in this SX runtime). See Blockers.
|
- [x] `crypto:hash/2` — **WIRED 2026-05-18** against `crypto-sha256`/`crypto-sha512`/`crypto-sha3-256` (loops/fed-prims). `crypto:hash(Type, Data)`: `Type` ∈ `sha256|sha512|sha3_256` atom; `Data` an Erlang binary/string/charlist (→ SX byte-string via `er-source-to-string`). Returns the **raw digest as an Erlang binary** (host hex → bytes via `er-hex->bytes`). Bad type / non-binary → `error:badarg`. 6 ffi tests (digest sizes 32/64, sha3 is_binary, deterministic, distinct, badarg).
|
||||||
- [ ] `cid:from_bytes/1`, `cid:to_string/1` — **BLOCKED** (needs `crypto:hash/2`). See Blockers.
|
- [x] `cid:from_bytes/1`, `cid:to_string/1` — **WIRED 2026-05-18**. `cid:from_bytes(Bin)` → CIDv1 raw-codec (0x55), sha2-256 multihash built in SX (`[0x12,0x20]++digest`) fed to `cid-from-bytes`; returned as an Erlang binary string. `cid:to_string(Term)` → canonical CIDv1 of the term's stable `er-format-value` string via `cid-from-sx` (cbor-encode rejects marshalled symbols, so `er-to-sx` is unencodable for compound terms — string form is total + deterministic). 7 ffi tests (is_binary, deterministic, distinct-inputs, non-binary badarg, to_string is_binary/deterministic/distinct).
|
||||||
- [x] `file:read_file/1`, `file:write_file/2`, `file:delete/1` — **+10 eval tests** (633/633 total). Returns `{ok, Binary}` / `ok` / `{error, Reason}` where Reason is `enoent`/`eacces`/`enotdir`/`eisdir`/`posix_error` (classified from the SX `file-read`/`-write`/`-delete` exception string). Path accepts SX string, Erlang binary, or Erlang char-code list. `file:list_dir/1` deferred — no directory-listing primitive in this SX runtime; see Blockers.
|
- [x] `file:read_file/1`, `file:write_file/2`, `file:delete/1` — **+10 eval tests** (633/633 total). Returns `{ok, Binary}` / `ok` / `{error, Reason}` where Reason is `enoent`/`eacces`/`enotdir`/`eisdir`/`posix_error` (classified from the SX `file-read`/`-write`/`-delete` exception string). Path accepts SX string, Erlang binary, or Erlang char-code list. **`file:list_dir/1` WIRED 2026-05-18** against `file-list-dir` → `{ok, [Binary]}` (entries marshalled via `er-of-sx`) / `{error, Reason}` (same `er-classify-file-error` mapping; missing dir → `enoent`). 4 ffi tests (ok-tag, non-empty, entries-are-binaries, missing-enoent).
|
||||||
- [ ] `httpc:request/4` — **BLOCKED** (no HTTP client primitive). See Blockers.
|
- [ ] `httpc:request/4` — **BLOCKED** (no HTTP client primitive). See Blockers.
|
||||||
- [ ] `sqlite:open/1`, `sqlite:close/1`, `sqlite:exec/2`, `sqlite:query/2` — **BLOCKED** (no SQLite primitive). See Blockers.
|
- [ ] `sqlite:open/1`, `sqlite:close/1`, `sqlite:exec/2`, `sqlite:query/2` — **BLOCKED** (no SQLite primitive). See Blockers.
|
||||||
- [x] Tests: 1 round-trip per BIF; suite name `ffi`; conformance scoreboard auto-picks it up — **+14 ffi tests** at 637/637 total. Suite covers the 3 implemented file BIFs (9 tests: write-ok, read-ok-tag, payload-is-binary, byte_size content, missing-enoent, bad-path-enoent, binary-payload round-trip, delete-ok, read-after-delete-enoent) plus 5 negative asserts (one per blocked BIF — `crypto:hash`/`cid:from_bytes`/`file:list_dir`/`httpc:request`/`sqlite:exec`) so this suite fails fast if a future iteration adds a wrapper without registering proper tests. Target "+40 ffi tests" was relative to the original 5-BIF-family plan; with 5 of those families blocked on host primitives, the achievable count is 14 — the suite scaffolding is what matters and is ready to accept the remaining tests when the primitives land.
|
- [x] Tests: 1 round-trip per BIF; suite name `ffi`; conformance scoreboard auto-picks it up — **+14 ffi tests** at 637/637 total. Suite covers the 3 implemented file BIFs (9 tests: write-ok, read-ok-tag, payload-is-binary, byte_size content, missing-enoent, bad-path-enoent, binary-payload round-trip, delete-ok, read-after-delete-enoent) plus 5 negative asserts (one per blocked BIF — `crypto:hash`/`cid:from_bytes`/`file:list_dir`/`httpc:request`/`sqlite:exec`) so this suite fails fast if a future iteration adds a wrapper without registering proper tests. Target "+40 ffi tests" was relative to the original 5-BIF-family plan; with 5 of those families blocked on host primitives, the achievable count is 14 — the suite scaffolding is what matters and is ready to accept the remaining tests when the primitives land.
|
||||||
@@ -159,6 +159,8 @@ The Phase 9 opcodes are registered, tested, and bridged SX↔OCaml, but inert: n
|
|||||||
|
|
||||||
_Newest first._
|
_Newest first._
|
||||||
|
|
||||||
|
- **2026-05-18 Phase 8 host-primitive BIFs wired (crypto / cid / file:list_dir)** — `loops/fed-prims` (merged at architecture `380bc69f`) delivered the platform primitives; wired the 3 previously-BLOCKED Phase 8 BIF groups in `lib/erlang/runtime.sx` as `er-register-pure-bif!`/`er-register-bif!` entries with term marshalling at the boundary. **`crypto:hash/2`** → `crypto-sha256`/`crypto-sha512`/`crypto-sha3-256`; atom `Type` dispatch, `er-source-to-string` for `Data`, host hex result → raw bytes via new `er-hexval`/`er-hex->bytes`, returns Erlang binary; bad type/arg → `error:badarg`. **`cid:from_bytes/1`** → `cid-from-bytes` with raw codec `0x55` + sha2-256 multihash assembled in SX (`[0x12,0x20]++digest`); **`cid:to_string/1`** → `cid-from-sx` of `er-format-value` (cbor-encode rejects `er-to-sx`-marshalled symbols; the canonical string form is total + deterministic). **`file:list_dir/1`** → `file-list-dir`, `{ok,[Binary]}` via `er-of-sx` / `{error,Reason}` reusing `er-classify-file-error`. Test gotcha caught + fixed: this Erlang port's binary parser only supports integer/var segments — `<<"abc">>` string-binary literals silently produce **empty** binaries, so the first-cut distinct-input tests compared two empty inputs and failed; rewrote ffi inputs to integer-segment binaries (`<<97,98,99>>`). ffi suite 14→**28** (3 BLOCKED negative-asserts flipped to positive+negative functional tests; `httpc`/`sqlite` kept as deferred unregistered-asserts per fed-prims handoff). Built `sx_server.exe` (dune, opam 5.2.0) at `380bc69f`; full conformance **729/729** (eval 385/385, vm 78/78, **ffi 28/28**, all process suites green). loops/erlang only — not merged, not pushed to architecture.
|
||||||
|
|
||||||
- **2026-05-18 FIXED merge-blocking regression: cyclic-env hang in `er-env-derived-from?`** — A trial merge of loops/erlang → architecture regressed Erlang **715/715 → 0/0** on the architecture binary. Bisected: not loader semantics, not a uniform slowdown — pinpointed to the *single* Phase 7 capstone test (eval.sx lines 1314-1346; prefix-1313 was byte-identical speed on both binaries, 27s, prefix-1346 was 28s on loops vs >5min/hung on architecture). Isolated further: spawn+reload alone 0.6s, reload+purge alone 0.3s, but spawn+reload+**purge over forever-blocked procs** hung. Root cause: `er-env-derived-from?` (transpile.sx, used by `code:purge`/`soft_purge` via `er-procs-on-env`) compared closure envs with `(= env target-env)`. loops/erlang's evaluator implements dict `=` as **object identity**; architecture's 131-commit-newer evaluator changed it to **structural deep equality**. Erlang closure envs are large and **cyclic** (a module fun's `:env` transitively references the fun), so structural `=` over them never terminates. Fix: use `identical?` (pointer-identity predicate, present + consistent `(true false)` on *both* binaries) — the actually-intended semantics and host-independent. Verified: full eval.sx on the architecture binary >200s/hung → **59s**; full 10-suite conformance on the architecture binary now **715/715** (eval 385/385, vm 78/78, ffi 14/14, all process suites green). loops/erlang behaviour unchanged (`identical?` ≡ its old `=`-identity). One-file change (`lib/erlang/transpile.sx`, +7/-2). The merge can now be re-attempted; this was the sole blocker.
|
- **2026-05-18 FIXED merge-blocking regression: cyclic-env hang in `er-env-derived-from?`** — A trial merge of loops/erlang → architecture regressed Erlang **715/715 → 0/0** on the architecture binary. Bisected: not loader semantics, not a uniform slowdown — pinpointed to the *single* Phase 7 capstone test (eval.sx lines 1314-1346; prefix-1313 was byte-identical speed on both binaries, 27s, prefix-1346 was 28s on loops vs >5min/hung on architecture). Isolated further: spawn+reload alone 0.6s, reload+purge alone 0.3s, but spawn+reload+**purge over forever-blocked procs** hung. Root cause: `er-env-derived-from?` (transpile.sx, used by `code:purge`/`soft_purge` via `er-procs-on-env`) compared closure envs with `(= env target-env)`. loops/erlang's evaluator implements dict `=` as **object identity**; architecture's 131-commit-newer evaluator changed it to **structural deep equality**. Erlang closure envs are large and **cyclic** (a module fun's `:env` transitively references the fun), so structural `=` over them never terminates. Fix: use `identical?` (pointer-identity predicate, present + consistent `(true false)` on *both* binaries) — the actually-intended semantics and host-independent. Verified: full eval.sx on the architecture binary >200s/hung → **59s**; full 10-suite conformance on the architecture binary now **715/715** (eval 385/385, vm 78/78, ffi 14/14, all process suites green). loops/erlang behaviour unchanged (`identical?` ≡ its old `=`-identity). One-file change (`lib/erlang/transpile.sx`, +7/-2). The merge can now be re-attempted; this was the sole blocker.
|
||||||
|
|
||||||
- **2026-05-15 Phase 10a — architecture traced, scoped, blocked on `lib/compiler.sx`** — Investigation-only iteration (correctly: faking compiler emission within scope is impossible and would be dishonest). Traced the full JIT path: `sx_vm.ml`'s `jit_compile_lambda` (the ref set at line 1206) invokes the SX-level `compile` from `lib/compiler.sx` via the CEK machine — that is the only SX→bytecode producer. Erlang's hot helpers are ordinary SX functions in `transpile.sx` that get JIT-compiled through exactly this path, so emitting `erlang.OP_*` means teaching `compiler.sx`'s `compile-call` to recognize them as intrinsics and emit the extension opcode (the file's own docstring already anticipates this — "Compilers call `extension-opcode-id` to emit extension opcodes" — designed but unimplemented; grep confirms zero `extension-opcode-id` uses in `compiler.sx`). `lib/compiler.sx` is lib-root: excluded by ground rules and the widened scope (editing it changes every guest's JIT — must be a shared-compiler session, not this loop). Recorded a precise Blockers entry + decomposed 10a into four numbered sub-steps (10a.1 intrinsic registry, 10a.2 `compile-call` emission with graceful CALL fallback, 10a.3 operand/stack contract for control opcodes, 10a.4 bytecode-emission tests) so the owning session can execute directly. Key payoff documented: all 10 BIF handlers (230-239) are already real, so they light up the instant 10a.1-10a.2 land — zero further Erlang-side work for the BIF speedup. No code changed; conformance unverified-but-untouched at **715/715** (no source touched). Phase 10's loop-reachable work (10b BIF half) is complete; the rest is correctly blocked and fully actionable elsewhere.
|
- **2026-05-15 Phase 10a — architecture traced, scoped, blocked on `lib/compiler.sx`** — Investigation-only iteration (correctly: faking compiler emission within scope is impossible and would be dishonest). Traced the full JIT path: `sx_vm.ml`'s `jit_compile_lambda` (the ref set at line 1206) invokes the SX-level `compile` from `lib/compiler.sx` via the CEK machine — that is the only SX→bytecode producer. Erlang's hot helpers are ordinary SX functions in `transpile.sx` that get JIT-compiled through exactly this path, so emitting `erlang.OP_*` means teaching `compiler.sx`'s `compile-call` to recognize them as intrinsics and emit the extension opcode (the file's own docstring already anticipates this — "Compilers call `extension-opcode-id` to emit extension opcodes" — designed but unimplemented; grep confirms zero `extension-opcode-id` uses in `compiler.sx`). `lib/compiler.sx` is lib-root: excluded by ground rules and the widened scope (editing it changes every guest's JIT — must be a shared-compiler session, not this loop). Recorded a precise Blockers entry + decomposed 10a into four numbered sub-steps (10a.1 intrinsic registry, 10a.2 `compile-call` emission with graceful CALL fallback, 10a.3 operand/stack contract for control opcodes, 10a.4 bytecode-emission tests) so the owning session can execute directly. Key payoff documented: all 10 BIF handlers (230-239) are already real, so they light up the instant 10a.1-10a.2 land — zero further Erlang-side work for the BIF speedup. No code changed; conformance unverified-but-untouched at **715/715** (no source touched). Phase 10's loop-reachable work (10b BIF half) is complete; the rest is correctly blocked and fully actionable elsewhere.
|
||||||
|
|||||||
Reference in New Issue
Block a user