diff --git a/lib/erlang/lists-ext.sx b/lib/erlang/lists-ext.sx index 832f0fac..a3e24577 100644 --- a/lib/erlang/lists-ext.sx +++ b/lib/erlang/lists-ext.sx @@ -249,6 +249,76 @@ (er-bool (not (er-ext-lt? (er-ext-tup-elem b n) (er-ext-tup-elem a n)))))))))) +;; ── higher-order traversal (foldr / partition / *while) ─────────── +(define + er-ext-foldr + (fn (f acc lst) + (cond + (er-nil? lst) acc + (er-cons? lst) + (er-apply-fun f (list (get lst :head) (er-ext-foldr f acc (get lst :tail)))) + :else (raise (er-mk-error-marker (er-mk-atom "badarg")))))) + +(define + er-bif-lists-foldr + (fn (vs) (er-ext-foldr (nth vs 0) (nth vs 1) (nth vs 2)))) + +(define + er-ext-partition + (fn (pred lst yes no) + (cond + (er-nil? lst) + (er-mk-tuple + (list + (er-list-reverse-iter yes (er-mk-nil)) + (er-list-reverse-iter no (er-mk-nil)))) + (er-cons? lst) + (if (er-truthy? (er-apply-fun pred (list (get lst :head)))) + (er-ext-partition pred (get lst :tail) (er-mk-cons (get lst :head) yes) no) + (er-ext-partition pred (get lst :tail) yes (er-mk-cons (get lst :head) no))) + :else (raise (er-mk-error-marker (er-mk-atom "badarg")))))) + +(define + er-bif-lists-partition + (fn (vs) (er-ext-partition (nth vs 0) (nth vs 1) (er-mk-nil) (er-mk-nil)))) + +(define + er-ext-takewhile + (fn (pred lst) + (cond + (er-nil? lst) (er-mk-nil) + (er-cons? lst) + (if (er-truthy? (er-apply-fun pred (list (get lst :head)))) + (er-mk-cons (get lst :head) (er-ext-takewhile pred (get lst :tail))) + (er-mk-nil)) + :else (er-mk-nil)))) + +(define + er-bif-lists-takewhile + (fn (vs) (er-ext-takewhile (nth vs 0) (nth vs 1)))) + +(define + er-ext-dropwhile + (fn (pred lst) + (cond + (er-nil? lst) (er-mk-nil) + (er-cons? lst) + (if (er-truthy? (er-apply-fun pred (list (get lst :head)))) + (er-ext-dropwhile pred (get lst :tail)) + lst) + :else lst))) + +(define + er-bif-lists-dropwhile + (fn (vs) (er-ext-dropwhile (nth vs 0) (nth vs 1)))) + +(define + er-bif-lists-splitwith + (fn (vs) + (let ((pred (nth vs 0)) (lst (nth vs 1))) + (er-mk-tuple + (list (er-ext-takewhile pred lst) (er-ext-dropwhile pred lst)))))) + ;; ── register ────────────────────────────────────────────────────── ;; Hook into er-register-builtin-bifs! rather than registering once: ;; the registry can be reset + rebuilt mid-run (tests/runtime.sx does @@ -265,7 +335,12 @@ (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))) + (er-register-pure-bif! "lists" "keysort" 2 er-bif-lists-keysort) + (er-register-pure-bif! "lists" "foldr" 3 er-bif-lists-foldr) + (er-register-pure-bif! "lists" "partition" 2 er-bif-lists-partition) + (er-register-pure-bif! "lists" "takewhile" 2 er-bif-lists-takewhile) + (er-register-pure-bif! "lists" "dropwhile" 2 er-bif-lists-dropwhile) + (er-register-pure-bif! "lists" "splitwith" 2 er-bif-lists-splitwith))) (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 0619ca53..35eff24f 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,7 +1,7 @@ { "language": "erlang", - "total_pass": 809, - "total": 809, + "total_pass": 823, + "total": 823, "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":38,"total":38,"status":"ok"} + {"name":"lists_ext","pass":52,"total":52,"status":"ok"} ] } diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index f534dcbf..b802f506 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,6 +1,6 @@ # Erlang-on-SX Scoreboard -**Total: 809 / 809 tests passing** +**Total: 823 / 823 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -16,7 +16,7 @@ | ✅ | ffi | 37 | 37 | | ✅ | vm | 78 | 78 | | ✅ | send_after | 10 | 10 | -| ✅ | lists_ext | 38 | 38 | +| ✅ | lists_ext | 52 | 52 | Generated by `lib/erlang/conformance.sh`. diff --git a/lib/erlang/tests/lists_ext.sx b/lib/erlang/tests/lists_ext.sx index 690a2ce2..b1b73cd9 100644 --- a/lib/erlang/tests/lists_ext.sx +++ b/lib/erlang/tests/lists_ext.sx @@ -154,3 +154,54 @@ (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") + +;; ── lists:foldr/3 ───────────────────────────────────────────────── +(er-lx-test "foldr preserves order" + (er-lx-nm + "lists:foldr(fun(X,Acc) -> [X|Acc] end, [], [1,2,3]) =:= [1,2,3]") "true") + +(er-lx-test "foldr sum" + (erlang-eval-ast "lists:foldr(fun(X,A) -> X+A end, 0, [1,2,3,4])") 10) + +(er-lx-test "foldr empty returns acc" + (erlang-eval-ast "lists:foldr(fun(X,A) -> X+A end, 42, [])") 42) + +;; ── lists:partition/2 ───────────────────────────────────────────── +(er-lx-test "partition evens/odds" + (er-lx-nm + "lists:partition(fun(X) -> X rem 2 =:= 0 end, [1,2,3,4,5]) =:= {[2,4],[1,3,5]}") + "true") + +(er-lx-test "partition all satisfy" + (er-lx-nm "lists:partition(fun(_) -> true end, [1,2]) =:= {[1,2],[]}") "true") + +(er-lx-test "partition empty" + (er-lx-nm "lists:partition(fun(_) -> true end, []) =:= {[],[]}") "true") + +;; ── lists:takewhile/2 ───────────────────────────────────────────── +(er-lx-test "takewhile prefix" + (er-lx-nm "lists:takewhile(fun(X) -> X < 3 end, [1,2,3,4,1]) =:= [1,2]") "true") + +(er-lx-test "takewhile none" + (er-lx-nm "lists:takewhile(fun(X) -> X < 0 end, [1,2]) =:= []") "true") + +(er-lx-test "takewhile all" + (er-lx-nm "lists:takewhile(fun(X) -> X < 9 end, [1,2,3]) =:= [1,2,3]") "true") + +;; ── lists:dropwhile/2 ───────────────────────────────────────────── +(er-lx-test "dropwhile prefix" + (er-lx-nm "lists:dropwhile(fun(X) -> X < 3 end, [1,2,3,4,1]) =:= [3,4,1]") "true") + +(er-lx-test "dropwhile all" + (er-lx-nm "lists:dropwhile(fun(X) -> X < 9 end, [1,2,3]) =:= []") "true") + +(er-lx-test "dropwhile none" + (er-lx-nm "lists:dropwhile(fun(X) -> X < 0 end, [1,2]) =:= [1,2]") "true") + +;; ── lists:splitwith/2 ───────────────────────────────────────────── +(er-lx-test "splitwith" + (er-lx-nm + "lists:splitwith(fun(X) -> X < 3 end, [1,2,3,4,1]) =:= {[1,2],[3,4,1]}") "true") + +(er-lx-test "splitwith empty" + (er-lx-nm "lists:splitwith(fun(_) -> true end, []) =:= {[],[]}") "true") diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 23e5280c..b15c4181 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` higher-order traversal** — Added `foldr/3`, `partition/2`, `takewhile/2`, `dropwhile/2`, `splitwith/2` to `lib/erlang/lists-ext.sx`, registered pure through the `er-register-builtin-bifs!` wrapper (consistent with the existing pure `map`/`filter`/`foldl`). `foldr` right-folds (order-preserving when consing); `partition` returns `{Satisfying, NotSatisfying}` order-preserved via `er-list-reverse-iter`; `splitwith` = `{takewhile, dropwhile}`. `lists_ext` suite 38→**52** (+14). Conformance **809 → 823/823**. loops/erlang only. + - **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.