From 394d5790ad87dde7834077fdab41ab7fde888efd Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 30 Jun 2026 13:55:57 +0000 Subject: [PATCH] erlang: lists flatmap/2 + filtermap/2 + mapfoldl/3 + search/2 (862/862) Higher-order list ops in lib/erlang/lists-ext.sx. filtermap honours true/false/{true,V}; mapfoldl returns {Mapped, Acc}; search returns {value,E}|false. lists_ext suite 83 -> 91. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/erlang/lists-ext.sx | 80 ++++++++++++++++++++++++++++++++++- lib/erlang/scoreboard.json | 6 +-- lib/erlang/scoreboard.md | 4 +- lib/erlang/tests/lists_ext.sx | 31 ++++++++++++++ plans/erlang-on-sx.md | 2 + 5 files changed, 117 insertions(+), 6 deletions(-) diff --git a/lib/erlang/lists-ext.sx b/lib/erlang/lists-ext.sx index 137903ac..e0ca5502 100644 --- a/lib/erlang/lists-ext.sx +++ b/lib/erlang/lists-ext.sx @@ -484,6 +484,80 @@ er-bif-lists-droplast (fn (vs) (er-ext-droplast (er-bif-arg1 vs "lists:droplast")))) +;; ── more higher-order (flatmap / filtermap / mapfoldl / search) ─── +(define + er-ext-flatmap + (fn (f lst) + (cond + (er-nil? lst) (er-mk-nil) + (er-cons? lst) + (er-list-append + (er-apply-fun f (list (get lst :head))) + (er-ext-flatmap f (get lst :tail))) + :else (raise (er-mk-error-marker (er-mk-atom "badarg")))))) + +(define + er-bif-lists-flatmap + (fn (vs) (er-ext-flatmap (nth vs 0) (nth vs 1)))) + +(define + er-ext-atom-true? + (fn (v) (and (er-atom? v) (= (get v :name) "true")))) + +(define + er-ext-filtermap + (fn (f lst) + (cond + (er-nil? lst) (er-mk-nil) + (er-cons? lst) + (let ((r (er-apply-fun f (list (get lst :head))))) + (cond + (er-ext-atom-true? r) + (er-mk-cons (get lst :head) (er-ext-filtermap f (get lst :tail))) + (and + (er-tuple? r) + (= (len (get r :elements)) 2) + (er-ext-atom-true? (nth (get r :elements) 0))) + (er-mk-cons (nth (get r :elements) 1) (er-ext-filtermap f (get lst :tail))) + :else (er-ext-filtermap f (get lst :tail)))) + :else (raise (er-mk-error-marker (er-mk-atom "badarg")))))) + +(define + er-bif-lists-filtermap + (fn (vs) (er-ext-filtermap (nth vs 0) (nth vs 1)))) + +(define + er-ext-mapfoldl + (fn (f acc lst mapped) + (cond + (er-nil? lst) + (er-mk-tuple (list (er-list-reverse-iter mapped (er-mk-nil)) acc)) + (er-cons? lst) + (let ((r (er-apply-fun f (list (get lst :head) acc)))) + (let ((es (get r :elements))) + (er-ext-mapfoldl f (nth es 1) (get lst :tail) + (er-mk-cons (nth es 0) mapped)))) + :else (raise (er-mk-error-marker (er-mk-atom "badarg")))))) + +(define + er-bif-lists-mapfoldl + (fn (vs) (er-ext-mapfoldl (nth vs 0) (nth vs 1) (nth vs 2) (er-mk-nil)))) + +(define + er-ext-search + (fn (pred lst) + (cond + (er-nil? lst) (er-mk-atom "false") + (er-cons? lst) + (if (er-truthy? (er-apply-fun pred (list (get lst :head)))) + (er-mk-tuple (list (er-mk-atom "value") (get lst :head))) + (er-ext-search pred (get lst :tail))) + :else (er-mk-atom "false")))) + +(define + er-bif-lists-search + (fn (vs) (er-ext-search (nth vs 0) (nth vs 1)))) + ;; ── register ────────────────────────────────────────────────────── ;; Hook into er-register-builtin-bifs! rather than registering once: ;; the registry can be reset + rebuilt mid-run (tests/runtime.sx does @@ -516,7 +590,11 @@ (er-register-pure-bif! "lists" "sublist" 3 er-bif-lists-sublist) (er-register-pure-bif! "lists" "nthtail" 2 er-bif-lists-nthtail) (er-register-pure-bif! "lists" "split" 2 er-bif-lists-split) - (er-register-pure-bif! "lists" "droplast" 1 er-bif-lists-droplast))) + (er-register-pure-bif! "lists" "droplast" 1 er-bif-lists-droplast) + (er-register-pure-bif! "lists" "flatmap" 2 er-bif-lists-flatmap) + (er-register-pure-bif! "lists" "filtermap" 2 er-bif-lists-filtermap) + (er-register-pure-bif! "lists" "mapfoldl" 3 er-bif-lists-mapfoldl) + (er-register-pure-bif! "lists" "search" 2 er-bif-lists-search))) (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 73626ba0..90d0747b 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,7 +1,7 @@ { "language": "erlang", - "total_pass": 854, - "total": 854, + "total_pass": 862, + "total": 862, "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":83,"total":83,"status":"ok"} + {"name":"lists_ext","pass":91,"total":91,"status":"ok"} ] } diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index e41f2df7..dcc21dee 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,6 +1,6 @@ # Erlang-on-SX Scoreboard -**Total: 854 / 854 tests passing** +**Total: 862 / 862 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -16,7 +16,7 @@ | ✅ | ffi | 37 | 37 | | ✅ | vm | 78 | 78 | | ✅ | send_after | 10 | 10 | -| ✅ | lists_ext | 83 | 83 | +| ✅ | lists_ext | 91 | 91 | Generated by `lib/erlang/conformance.sh`. diff --git a/lib/erlang/tests/lists_ext.sx b/lib/erlang/tests/lists_ext.sx index a5494371..8fcbb416 100644 --- a/lib/erlang/tests/lists_ext.sx +++ b/lib/erlang/tests/lists_ext.sx @@ -310,3 +310,34 @@ (er-lx-test "droplast single" (er-lx-nm "lists:droplast([9]) =:= []") "true") + +;; ── lists:flatmap/2 ─────────────────────────────────────────────── +(er-lx-test "flatmap duplicates" + (er-lx-nm "lists:flatmap(fun(X) -> [X,X] end, [1,2]) =:= [1,1,2,2]") "true") + +(er-lx-test "flatmap empty" + (er-lx-nm "lists:flatmap(fun(X) -> [X] end, []) =:= []") "true") + +;; ── lists:filtermap/2 ───────────────────────────────────────────── +(er-lx-test "filtermap transform" + (er-lx-nm + "lists:filtermap(fun(X) -> case X rem 2 of 0 -> {true, X*10}; _ -> false end end, [1,2,3,4]) =:= [20,40]") + "true") + +(er-lx-test "filtermap bool keep" + (er-lx-nm "lists:filtermap(fun(X) -> X > 2 end, [1,2,3,4]) =:= [3,4]") "true") + +;; ── lists:mapfoldl/3 ────────────────────────────────────────────── +(er-lx-test "mapfoldl map+acc" + (er-lx-nm + "lists:mapfoldl(fun(X,A) -> {X*2, A+X} end, 0, [1,2,3]) =:= {[2,4,6],6}") "true") + +(er-lx-test "mapfoldl empty" + (er-lx-nm "lists:mapfoldl(fun(X,A) -> {X,A} end, 5, []) =:= {[],5}") "true") + +;; ── lists:search/2 ──────────────────────────────────────────────── +(er-lx-test "search hit" + (er-lx-nm "lists:search(fun(X) -> X > 2 end, [1,2,3,4]) =:= {value,3}") "true") + +(er-lx-test "search miss" + (er-lx-nm "lists:search(fun(X) -> X > 9 end, [1,2,3])") "false") diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index e4f147ce..c9baca14 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` flatmap/filtermap/mapfoldl/search** — Added `flatmap/2`, `filtermap/2` (`true` keep / `false` drop / `{true, V}` transform), `mapfoldl/3` (returns `{MappedList, AccFinal}`), `search/2` (`{value, E}` | `false`) to `lib/erlang/lists-ext.sx`. `lists_ext` suite 83→**91** (+8). Conformance **854 → 862/862**. The `lists` module is now broadly covered (sort/usort/keylists/fold/partition/while/flatten/min/max/zip/slicing/flatmap/filtermap/mapfoldl/search on top of the originals). loops/erlang only. + - **2026-06-30 stdlib hardening — `lists` slicing** — Added `sublist/2`, `sublist/3`, `nthtail/2`, `split/2`, `droplast/1` to `lib/erlang/lists-ext.sx`. `sublist` is lenient (clamps to list length); `nthtail/2` and `split/2` are strict (`badarg` when the list is shorter than N, matching the stdlib); `droplast/1` raises on `[]`. `lists_ext` suite 70→**83** (+13). Conformance **841 → 854/854**. loops/erlang only. - **2026-06-30 stdlib hardening — `lists` zip family** — Added `zip/2`, `zipwith/3`, `unzip/1` to `lib/erlang/lists-ext.sx`. Length mismatch (zip/zipwith) and malformed/non-pair elements (unzip) raise `badarg` (port equivalent of Erlang's `function_clause`). `lists_ext` suite 62→**70** (+8, incl. a zip/unzip roundtrip). Conformance **833 → 841/841**. loops/erlang only.