From 343c5089391920e8323c9da98179c051b06d2013 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 30 Jun 2026 13:28:57 +0000 Subject: [PATCH] erlang: lists keylist BIFs (keyfind/keymember/keydelete/keyreplace/keystore/keytake/keysort) (809/809) Adds the tuple-keyed list family to lib/erlang/lists-ext.sx: act on first match, key compare via == (er-equal?), non-tuples/short tuples pass through. keysort/2 reuses the stable merge sort + full term order. keytake/3 returns {value, Tuple, Rest} | false. All seven registered through the er-register-builtin-bifs! wrapper so they survive mid-run registry resets. lists_ext suite 17 -> 38. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/erlang/lists-ext.sx | 117 +++++++++++++++++++++++++++++++++- lib/erlang/scoreboard.json | 6 +- lib/erlang/scoreboard.md | 4 +- lib/erlang/tests/lists_ext.sx | 80 +++++++++++++++++++++++ plans/erlang-on-sx.md | 2 + 5 files changed, 203 insertions(+), 6 deletions(-) diff --git a/lib/erlang/lists-ext.sx b/lib/erlang/lists-ext.sx index e187203c..832f0fac 100644 --- a/lib/erlang/lists-ext.sx +++ b/lib/erlang/lists-ext.sx @@ -141,6 +141,114 @@ (er-ext-dedup (er-ext-msort (er-cons->sxlist lst) er-ext-term-le)))))) +;; ── keylists (lists of tuples keyed on element N, 1-indexed) ────── +;; keyfind/keymember/keydelete/keyreplace/keystore/keytake/keysort. +;; Key comparison is == (er-equal?), matching the standard lib. Only +;; the FIRST matching tuple is acted on. Non-tuples / tuples shorter +;; than N never match and are passed through unchanged. +(define + er-ext-tup-elem + (fn (tup n) + (if (er-tuple? tup) + (let ((es (get tup :elements))) + (if (and (>= n 1) (<= n (len es))) (nth es (- n 1)) nil)) + nil))) + +(define + er-ext-key-match? + (fn (key n tup) + (and + (er-tuple? tup) + (>= n 1) + (<= n (len (get tup :elements))) + (er-equal? key (nth (get tup :elements) (- n 1)))))) + +(define + er-ext-keyfind + (fn (key n lst) + (cond + (er-nil? lst) (er-mk-atom "false") + (er-cons? lst) + (if (er-ext-key-match? key n (get lst :head)) + (get lst :head) + (er-ext-keyfind key n (get lst :tail))) + :else (er-mk-atom "false")))) + +(define + er-ext-keydelete + (fn (key n lst) + (cond + (er-nil? lst) (er-mk-nil) + (er-cons? lst) + (if (er-ext-key-match? key n (get lst :head)) + (get lst :tail) + (er-mk-cons (get lst :head) (er-ext-keydelete key n (get lst :tail)))) + :else lst))) + +(define + er-ext-keyreplace + (fn (key n lst new) + (cond + (er-nil? lst) (er-mk-nil) + (er-cons? lst) + (if (er-ext-key-match? key n (get lst :head)) + (er-mk-cons new (get lst :tail)) + (er-mk-cons (get lst :head) (er-ext-keyreplace key n (get lst :tail) new))) + :else lst))) + +(define + er-ext-keystore + (fn (key n lst new) + (cond + (er-nil? lst) (er-mk-cons new (er-mk-nil)) + (er-cons? lst) + (if (er-ext-key-match? key n (get lst :head)) + (er-mk-cons new (get lst :tail)) + (er-mk-cons (get lst :head) (er-ext-keystore key n (get lst :tail) new))) + :else lst))) + +(define + er-bif-lists-keyfind + (fn (vs) (er-ext-keyfind (nth vs 0) (nth vs 1) (nth vs 2)))) + +(define + er-bif-lists-keymember + (fn (vs) + (er-bool (not (er-atom? (er-ext-keyfind (nth vs 0) (nth vs 1) (nth vs 2))))))) + +(define + er-bif-lists-keydelete + (fn (vs) (er-ext-keydelete (nth vs 0) (nth vs 1) (nth vs 2)))) + +(define + er-bif-lists-keyreplace + (fn (vs) (er-ext-keyreplace (nth vs 0) (nth vs 1) (nth vs 2) (nth vs 3)))) + +(define + er-bif-lists-keystore + (fn (vs) (er-ext-keystore (nth vs 0) (nth vs 1) (nth vs 2) (nth vs 3)))) + +(define + er-bif-lists-keytake + (fn (vs) + (let ((key (nth vs 0)) (n (nth vs 1)) (lst (nth vs 2))) + (let ((hit (er-ext-keyfind key n lst))) + (if (er-atom? hit) + (er-mk-atom "false") + (er-mk-tuple + (list (er-mk-atom "value") hit (er-ext-keydelete key n lst)))))))) + +(define + er-bif-lists-keysort + (fn (vs) + (let ((n (nth vs 0)) (lst (nth vs 1))) + (er-sxlist->cons + (er-ext-msort + (er-cons->sxlist lst) + (fn (a b) + (er-bool + (not (er-ext-lt? (er-ext-tup-elem b n) (er-ext-tup-elem a n)))))))))) + ;; ── register ────────────────────────────────────────────────────── ;; Hook into er-register-builtin-bifs! rather than registering once: ;; the registry can be reset + rebuilt mid-run (tests/runtime.sx does @@ -150,7 +258,14 @@ (fn () (er-register-pure-bif! "lists" "sort" 1 er-bif-lists-sort) (er-register-pure-bif! "lists" "sort" 2 er-bif-lists-sort) - (er-register-pure-bif! "lists" "usort" 1 er-bif-lists-usort))) + (er-register-pure-bif! "lists" "usort" 1 er-bif-lists-usort) + (er-register-pure-bif! "lists" "keyfind" 3 er-bif-lists-keyfind) + (er-register-pure-bif! "lists" "keymember" 3 er-bif-lists-keymember) + (er-register-pure-bif! "lists" "keydelete" 3 er-bif-lists-keydelete) + (er-register-pure-bif! "lists" "keyreplace" 4 er-bif-lists-keyreplace) + (er-register-pure-bif! "lists" "keystore" 4 er-bif-lists-keystore) + (er-register-pure-bif! "lists" "keytake" 3 er-bif-lists-keytake) + (er-register-pure-bif! "lists" "keysort" 2 er-bif-lists-keysort))) (define er-ext-prev-register-builtins er-register-builtin-bifs!) (define er-register-builtin-bifs! diff --git a/lib/erlang/scoreboard.json b/lib/erlang/scoreboard.json index 1d893b91..0619ca53 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,7 +1,7 @@ { "language": "erlang", - "total_pass": 788, - "total": 788, + "total_pass": 809, + "total": 809, "suites": [ {"name":"tokenize","pass":62,"total":62,"status":"ok"}, {"name":"parse","pass":52,"total":52,"status":"ok"}, @@ -15,6 +15,6 @@ {"name":"ffi","pass":37,"total":37,"status":"ok"}, {"name":"vm","pass":78,"total":78,"status":"ok"}, {"name":"send_after","pass":10,"total":10,"status":"ok"}, - {"name":"lists_ext","pass":17,"total":17,"status":"ok"} + {"name":"lists_ext","pass":38,"total":38,"status":"ok"} ] } diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index 01a3b69c..f534dcbf 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,6 +1,6 @@ # Erlang-on-SX Scoreboard -**Total: 788 / 788 tests passing** +**Total: 809 / 809 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -16,7 +16,7 @@ | ✅ | ffi | 37 | 37 | | ✅ | vm | 78 | 78 | | ✅ | send_after | 10 | 10 | -| ✅ | lists_ext | 17 | 17 | +| ✅ | lists_ext | 38 | 38 | Generated by `lib/erlang/conformance.sh`. diff --git a/lib/erlang/tests/lists_ext.sx b/lib/erlang/tests/lists_ext.sx index f0b5b691..690a2ce2 100644 --- a/lib/erlang/tests/lists_ext.sx +++ b/lib/erlang/tests/lists_ext.sx @@ -74,3 +74,83 @@ (er-lx-test "usort/1 length after dedup" (erlang-eval-ast "length(lists:usort([4,4,2,2,1,1,4]))") 3) + +;; ── lists:keyfind/3 ─────────────────────────────────────────────── +(er-lx-test "keyfind hit" + (erlang-eval-ast "element(2, lists:keyfind(b, 1, [{a,1},{b,2},{c,3}]))") 2) + +(er-lx-test "keyfind first match only" + (erlang-eval-ast "element(2, lists:keyfind(a, 1, [{a,1},{a,9}]))") 1) + +(er-lx-test "keyfind miss returns false" + (er-lx-nm "lists:keyfind(z, 1, [{a,1},{b,2}])") "false") + +(er-lx-test "keyfind on second element" + (er-lx-nm "element(1, lists:keyfind(2, 2, [{a,1},{b,2}]))") "b") + +(er-lx-test "keyfind skips short tuples" + (er-lx-nm "lists:keyfind(x, 2, [{x},{y,x}]) =:= {y,x}") "true") + +;; ── lists:keymember/3 ───────────────────────────────────────────── +(er-lx-test "keymember true" + (er-lx-nm "lists:keymember(b, 1, [{a,1},{b,2}])") "true") + +(er-lx-test "keymember false" + (er-lx-nm "lists:keymember(z, 1, [{a,1},{b,2}])") "false") + +;; ── lists:keydelete/3 ───────────────────────────────────────────── +(er-lx-test "keydelete removes first match" + (er-lx-nm "lists:keydelete(b, 1, [{a,1},{b,2},{c,3}]) =:= [{a,1},{c,3}]") "true") + +(er-lx-test "keydelete only first" + (er-lx-nm "lists:keydelete(a, 1, [{a,1},{a,2},{b,3}]) =:= [{a,2},{b,3}]") "true") + +(er-lx-test "keydelete miss unchanged" + (er-lx-nm "lists:keydelete(z, 1, [{a,1},{b,2}]) =:= [{a,1},{b,2}]") "true") + +;; ── lists:keyreplace/4 ──────────────────────────────────────────── +(er-lx-test "keyreplace hit" + (er-lx-nm + "lists:keyreplace(b, 1, [{a,1},{b,2},{c,3}], {b,99}) =:= [{a,1},{b,99},{c,3}]") + "true") + +(er-lx-test "keyreplace miss unchanged" + (er-lx-nm + "lists:keyreplace(z, 1, [{a,1}], {z,0}) =:= [{a,1}]") "true") + +;; ── lists:keystore/4 ────────────────────────────────────────────── +(er-lx-test "keystore replaces existing" + (er-lx-nm + "lists:keystore(b, 1, [{a,1},{b,2}], {b,99}) =:= [{a,1},{b,99}]") "true") + +(er-lx-test "keystore appends when absent" + (er-lx-nm + "lists:keystore(z, 1, [{a,1},{b,2}], {z,0}) =:= [{a,1},{b,2},{z,0}]") "true") + +;; ── lists:keytake/3 ─────────────────────────────────────────────── +(er-lx-test "keytake hit value tag" + (er-lx-nm "element(1, lists:keytake(b, 1, [{a,1},{b,2},{c,3}]))") "value") + +(er-lx-test "keytake hit tuple" + (er-lx-nm + "element(2, lists:keytake(b, 1, [{a,1},{b,2},{c,3}])) =:= {b,2}") "true") + +(er-lx-test "keytake hit rest" + (er-lx-nm + "element(3, lists:keytake(b, 1, [{a,1},{b,2},{c,3}])) =:= [{a,1},{c,3}]") "true") + +(er-lx-test "keytake miss false" + (er-lx-nm "lists:keytake(z, 1, [{a,1}])") "false") + +;; ── lists:keysort/2 ─────────────────────────────────────────────── +(er-lx-test "keysort by element 1" + (er-lx-nm + "lists:keysort(1, [{c,3},{a,1},{b,2}]) =:= [{a,1},{b,2},{c,3}]") "true") + +(er-lx-test "keysort by element 2" + (er-lx-nm + "lists:keysort(2, [{a,3},{b,1},{c,2}]) =:= [{b,1},{c,2},{a,3}]") "true") + +(er-lx-test "keysort stable on equal keys" + (er-lx-nm + "lists:keysort(1, [{a,1},{a,2},{a,3}]) =:= [{a,1},{a,2},{a,3}]") "true") diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index cec03f19..23e5280c 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -159,6 +159,8 @@ The Phase 9 opcodes are registered, tested, and bridged SX↔OCaml, but inert: n _Newest first._ +- **2026-06-30 stdlib hardening — `lists` keylists** — Added the keylist family to `lib/erlang/lists-ext.sx`: `keyfind/3`, `keymember/3`, `keydelete/3`, `keyreplace/4`, `keystore/4`, `keytake/3`, `keysort/2`. All operate on lists of tuples keyed on element N (1-indexed), act on the first match only, and pass through non-tuples / tuples shorter than N. Key comparison is `==` (`er-equal?`) per the stdlib; `keysort/2` reuses the stable `er-ext-msort` + `er-ext-lt?` from the sort commit, comparing extracted keys. `keytake/3` returns `{value, Tuple, Rest}` / `false`. Registered through the same `er-register-builtin-bifs!` wrapper so they survive registry resets. `lists_ext` suite 17→**38** (+21: hit/miss/first-match-only/short-tuple-skip across all seven, keysort by elem 1 and 2 + stability). Conformance **788 → 809/809**. Test-harness note: `element(2, T)` returns an integer (no `:name`), so those two cases compare the raw number via `erlang-eval-ast` rather than `er-lx-nm`. loops/erlang only. + - **2026-06-30 stdlib hardening — `lists:sort/1,2` + `lists:usort/1`** — Roadmap is saturated within this loop's scope (every remaining `[ ]` is blocked: `httpc`/`sqlite` on absent host primitives, 10a/10c on out-of-scope `lib/compiler.sx`). Continued as forever-loop hardening by filling idiomatic-Erlang stdlib gaps. Added the `lists` sort family in a **new file `lib/erlang/lists-ext.sx`** (loaded after `runtime.sx`): stable merge sort over an SX-list bridge, registered via `er-register-pure-bif!`. `lists:sort/1` and `usort/1` use full Erlang term order; `sort/2` takes a `fun(A,B)->bool` comparator. **Two notable findings:** (1) the shared `er-lt?` (transpile.sx) only deep-compares numbers/atoms/strings and treats *any two tuples (or lists) as order-equal* — so `lists:sort` (and, latently, `min/2`/`max/2`) would not order compound terms. Fixed locally with a self-contained `er-ext-lt?` that compares tuples by arity-then-elementwise and lists elementwise (shorter proper prefix first), delegating cross-type cases to `er-lt?`. `er-lt?` itself left untouched (shared by the `<` operator; can't edit transpile.sx — see Blockers). (2) `tests/runtime.sx` resets the BIF registry mid-run via `er-register-builtin-bifs!`, which would wipe a one-shot registration; so `lists-ext.sx` **wraps** `er-register-builtin-bifs!` to re-add its BIFs on every rebuild. New `lists_ext` suite (17 tests: term order, dup-keeping, stability, descending comparator, usort dedup). Conformance **771 → 788/788** (12→13 suites). New-file workaround forced because every sx-tree write tool (incl. `sx_write_file`) raises yojson "Expected string, got null" in this worktree — authored via the `Write` fallback + `sx_validate`, the same pattern other loops use. loops/erlang only. - **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.