From 5a1412515abe66cd6b9b1985414aba4d5f017cb9 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 30 Jun 2026 13:46:46 +0000 Subject: [PATCH] erlang: lists zip/2 + zipwith/3 + unzip/1 (841/841) Zip family in lib/erlang/lists-ext.sx; length mismatch and malformed pairs raise badarg. lists_ext suite 62 -> 70. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/erlang/lists-ext.sx | 61 ++++++++++++++++++++++++++++++++++- lib/erlang/scoreboard.json | 6 ++-- lib/erlang/scoreboard.md | 4 +-- lib/erlang/tests/lists_ext.sx | 29 +++++++++++++++++ plans/erlang-on-sx.md | 2 ++ 5 files changed, 96 insertions(+), 6 deletions(-) diff --git a/lib/erlang/lists-ext.sx b/lib/erlang/lists-ext.sx index 4aaa6616..25852fd1 100644 --- a/lib/erlang/lists-ext.sx +++ b/lib/erlang/lists-ext.sx @@ -366,6 +366,62 @@ (fn (a b) (er-ext-lt? b a))) (raise (er-mk-error-marker (er-mk-atom "badarg"))))))) +;; ── zip family (zip / zipwith / unzip) ──────────────────────────── +;; Length mismatch raises badarg (real Erlang raises function_clause; +;; badarg is the closest in-port equivalent). +(define + er-ext-zip + (fn (a b) + (cond + (and (er-nil? a) (er-nil? b)) (er-mk-nil) + (and (er-cons? a) (er-cons? b)) + (er-mk-cons + (er-mk-tuple (list (get a :head) (get b :head))) + (er-ext-zip (get a :tail) (get b :tail))) + :else (raise (er-mk-error-marker (er-mk-atom "badarg")))))) + +(define + er-bif-lists-zip + (fn (vs) (er-ext-zip (nth vs 0) (nth vs 1)))) + +(define + er-ext-zipwith + (fn (f a b) + (cond + (and (er-nil? a) (er-nil? b)) (er-mk-nil) + (and (er-cons? a) (er-cons? b)) + (er-mk-cons + (er-apply-fun f (list (get a :head) (get b :head))) + (er-ext-zipwith f (get a :tail) (get b :tail))) + :else (raise (er-mk-error-marker (er-mk-atom "badarg")))))) + +(define + er-bif-lists-zipwith + (fn (vs) (er-ext-zipwith (nth vs 0) (nth vs 1) (nth vs 2)))) + +(define + er-ext-unzip + (fn (lst as bs) + (cond + (er-nil? lst) + (er-mk-tuple + (list + (er-list-reverse-iter as (er-mk-nil)) + (er-list-reverse-iter bs (er-mk-nil)))) + (and (er-cons? lst) (er-tuple? (get lst :head))) + (let ((es (get (get lst :head) :elements))) + (if (= (len es) 2) + (er-ext-unzip (get lst :tail) + (er-mk-cons (nth es 0) as) + (er-mk-cons (nth es 1) bs)) + (raise (er-mk-error-marker (er-mk-atom "badarg"))))) + :else (raise (er-mk-error-marker (er-mk-atom "badarg")))))) + +(define + er-bif-lists-unzip + (fn (vs) + (er-ext-unzip (er-bif-arg1 vs "lists:unzip") (er-mk-nil) (er-mk-nil)))) + ;; ── register ────────────────────────────────────────────────────── ;; Hook into er-register-builtin-bifs! rather than registering once: ;; the registry can be reset + rebuilt mid-run (tests/runtime.sx does @@ -390,7 +446,10 @@ (er-register-pure-bif! "lists" "splitwith" 2 er-bif-lists-splitwith) (er-register-pure-bif! "lists" "flatten" 1 er-bif-lists-flatten) (er-register-pure-bif! "lists" "max" 1 er-bif-lists-max) - (er-register-pure-bif! "lists" "min" 1 er-bif-lists-min))) + (er-register-pure-bif! "lists" "min" 1 er-bif-lists-min) + (er-register-pure-bif! "lists" "zip" 2 er-bif-lists-zip) + (er-register-pure-bif! "lists" "zipwith" 3 er-bif-lists-zipwith) + (er-register-pure-bif! "lists" "unzip" 1 er-bif-lists-unzip))) (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 4429b21c..09730bbf 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,7 +1,7 @@ { "language": "erlang", - "total_pass": 833, - "total": 833, + "total_pass": 841, + "total": 841, "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":62,"total":62,"status":"ok"} + {"name":"lists_ext","pass":70,"total":70,"status":"ok"} ] } diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index c3bd1749..10d9b11f 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,6 +1,6 @@ # Erlang-on-SX Scoreboard -**Total: 833 / 833 tests passing** +**Total: 841 / 841 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -16,7 +16,7 @@ | ✅ | ffi | 37 | 37 | | ✅ | vm | 78 | 78 | | ✅ | send_after | 10 | 10 | -| ✅ | lists_ext | 62 | 62 | +| ✅ | lists_ext | 70 | 70 | Generated by `lib/erlang/conformance.sh`. diff --git a/lib/erlang/tests/lists_ext.sx b/lib/erlang/tests/lists_ext.sx index b02b89ec..7d4a9ab6 100644 --- a/lib/erlang/tests/lists_ext.sx +++ b/lib/erlang/tests/lists_ext.sx @@ -238,3 +238,32 @@ (er-lx-test "min mixed term order" (er-lx-nm "lists:min([a,1,b]) =:= 1") "true") + +;; ── lists:zip/2 ─────────────────────────────────────────────────── +(er-lx-test "zip pairs" + (er-lx-nm "lists:zip([a,b,c],[1,2,3]) =:= [{a,1},{b,2},{c,3}]") "true") + +(er-lx-test "zip empty" + (er-lx-nm "lists:zip([],[]) =:= []") "true") + +(er-lx-test "zip length" + (erlang-eval-ast "length(lists:zip([1,2],[3,4]))") 2) + +;; ── lists:zipwith/3 ─────────────────────────────────────────────── +(er-lx-test "zipwith sum" + (er-lx-nm + "lists:zipwith(fun(X,Y) -> X+Y end, [1,2,3], [10,20,30]) =:= [11,22,33]") + "true") + +(er-lx-test "zipwith tuple" + (er-lx-nm "lists:zipwith(fun(X,Y) -> {X,Y} end, [a], [1]) =:= [{a,1}]") "true") + +;; ── lists:unzip/1 ───────────────────────────────────────────────── +(er-lx-test "unzip" + (er-lx-nm "lists:unzip([{a,1},{b,2},{c,3}]) =:= {[a,b,c],[1,2,3]}") "true") + +(er-lx-test "unzip empty" + (er-lx-nm "lists:unzip([]) =:= {[],[]}") "true") + +(er-lx-test "zip/unzip roundtrip" + (er-lx-nm "lists:unzip(lists:zip([1,2],[3,4])) =:= {[1,2],[3,4]}") "true") diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 0b83014f..f2610b33 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` 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. + - **2026-06-30 stdlib hardening — `lists` flatten/max/min** — Added `flatten/1` (deep recursive flatten via `er-list-append`), `max/1`, `min/1` (full term order via `er-ext-lt?`, `badarg` on empty) to `lib/erlang/lists-ext.sx`. Gotcha caught: `er-ext-lt?` returns a raw SX boolean, so the extreme-finder uses it directly in `if` rather than wrapping in `er-truthy?` (which only recognises Erlang bool atoms, not SX booleans — the first cut wrapped it and silently never updated the running best). `lists_ext` suite 52→**62** (+10). Conformance **823 → 833/833**. loops/erlang only. - **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.