From 3bab6ea6c04f12e9ff510639815d62c8207c40af Mon Sep 17 00:00:00 2001 From: Peter Ullrich Date: Wed, 13 May 2026 20:29:38 +0100 Subject: [PATCH 1/8] Update Keyword.new/2 --- lib/elixir/lib/keyword.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/keyword.ex b/lib/elixir/lib/keyword.ex index f6f57e8b7c..775421080c 100644 --- a/lib/elixir/lib/keyword.ex +++ b/lib/elixir/lib/keyword.ex @@ -214,12 +214,17 @@ defmodule Keyword do """ @spec new(Enumerable.t(), (term -> {key, value})) :: t def new(pairs, transform) when is_function(transform, 1) do - fun = fn el, acc -> - {k, v} = transform.(el) - put_new(acc, k, v) + fun = fn element, {acc, seen} -> + {key, value} = transform.(element) + + case seen do + %{^key => _} -> {acc, seen} + _ -> {[{key, value} | acc], Map.put(seen, key, true)} + end end - :lists.foldl(fun, [], Enum.reverse(pairs)) + {result, _seen} = :lists.foldl(fun, {[], %{}}, Enum.reverse(pairs)) + result end @doc """ From 4a5ea1e72ef22a4724b58b6d09d9c81ff606bc75 Mon Sep 17 00:00:00 2001 From: Peter Ullrich Date: Wed, 13 May 2026 20:57:03 +0100 Subject: [PATCH 2/8] Update `Keyword.merge/2` --- lib/elixir/lib/keyword.ex | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/keyword.ex b/lib/elixir/lib/keyword.ex index 775421080c..db74bfad93 100644 --- a/lib/elixir/lib/keyword.ex +++ b/lib/elixir/lib/keyword.ex @@ -1039,21 +1039,18 @@ defmodule Keyword do def merge([], keywords2) when is_list(keywords2), do: keywords2 def merge(keywords1, keywords2) when is_list(keywords1) and is_list(keywords2) do - if keyword?(keywords2) do - fun = fn - {key, _value} when is_atom(key) -> - not has_key?(keywords2, key) - - _ -> - raise ArgumentError, - "expected a keyword list as the first argument, got: #{inspect(keywords1)}" - end + keys2 = collect_keys!(keywords2) - :lists.filter(fun, keywords1) ++ keywords2 - else - raise ArgumentError, - "expected a keyword list as the second argument, got: #{inspect(keywords2)}" + fun = fn + {key, _value} when is_atom(key) -> + not Map.has_key?(keys2, key) + + _ -> + raise ArgumentError, + "expected a keyword list as the first argument, got: #{inspect(keywords1)}" end + + :lists.filter(fun, keywords1) ++ keywords2 end @doc """ @@ -1117,9 +1114,21 @@ defmodule Keyword do rest ++ :lists.reverse(acc) end - defp do_merge(_other, _acc, _rest, _original, _fun, keywords2) do + defp emit_right([], emitted, _queue_map, _fun), do: emitted + + # Validates a keyword list while collecting its keys into a `%{key => []}` + # lookup map. Raises with the full original list if a non-keyword element + # is encountered. Used by merge/2 and merge/3. + defp collect_keys!(list), do: collect_keys!(list, %{}, list) + + defp collect_keys!([{key, _} | rest], acc, original) when is_atom(key), + do: collect_keys!(rest, Map.put(acc, key, []), original) + + defp collect_keys!([], acc, _original), do: acc + + defp collect_keys!(_other, _acc, original) do raise ArgumentError, - "expected a keyword list as the second argument, got: #{inspect(keywords2)}" + "expected a keyword list as the second argument, got: #{inspect(original)}" end @doc """ From c14f57a5e63b1a2fcce3b4af1e0e7a62fb00e803 Mon Sep 17 00:00:00 2001 From: Peter Ullrich Date: Wed, 13 May 2026 21:45:22 +0100 Subject: [PATCH 3/8] Update `Keyword.merge/3` --- lib/elixir/lib/keyword.ex | 59 +++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/lib/elixir/lib/keyword.ex b/lib/elixir/lib/keyword.ex index db74bfad93..2adef60736 100644 --- a/lib/elixir/lib/keyword.ex +++ b/lib/elixir/lib/keyword.ex @@ -1090,31 +1090,62 @@ defmodule Keyword do @spec merge(t, t, (key, value, value -> value)) :: t def merge(keywords1, keywords2, fun) when is_list(keywords1) and is_list(keywords2) and is_function(fun, 3) do - if keyword?(keywords1) do - do_merge(keywords2, [], keywords1, keywords1, fun, keywords2) - else + if not keyword?(keywords1) do raise ArgumentError, "expected a keyword list as the first argument, got: #{inspect(keywords1)}" end + + keys2 = collect_keys!(keywords2) + + {non_matching_rev, keys2, duplicate_keys} = + partition_left(keywords1, [], keys2, MapSet.new()) + + keys2 = + Enum.reduce(duplicate_keys, keys2, fn key, acc -> + Map.update!(acc, key, &:lists.reverse/1) + end) + + emitted_rev = emit_right(keywords2, [], keys2, fun) + :lists.reverse(non_matching_rev) ++ :lists.reverse(emitted_rev) end - defp do_merge([{key, value2} | tail], acc, rest, original, fun, keywords2) when is_atom(key) do - case :lists.keyfind(key, 1, original) do - {^key, value1} -> - acc = [{key, fun.(key, value1, value2)} | acc] - original = :lists.keydelete(key, 1, original) - do_merge(tail, acc, delete(rest, key), original, fun, keywords2) + defp partition_left([{key, value} | rest], non_matching, keys2, duplicate_keys) do + case keys2 do + %{^key => []} -> + partition_left(rest, non_matching, Map.put(keys2, key, [value]), duplicate_keys) - false -> - do_merge(tail, [{key, value2} | acc], rest, original, fun, keywords2) + %{^key => current} -> + partition_left( + rest, + non_matching, + Map.put(keys2, key, [value | current]), + MapSet.put(duplicate_keys, key) + ) + + _ -> + partition_left(rest, [{key, value} | non_matching], keys2, duplicate_keys) end end - defp do_merge([], acc, rest, _original, _fun, _keywords2) do - rest ++ :lists.reverse(acc) + defp partition_left([], non_matching, keys2, duplicate_keys), + do: {non_matching, keys2, duplicate_keys} + + defp emit_right([{key, value2} | rest], emitted, keys2, fun) do + case keys2 do + %{^key => [value1 | remaining]} -> + emit_right( + rest, + [{key, fun.(key, value1, value2)} | emitted], + Map.put(keys2, key, remaining), + fun + ) + + _ -> + emit_right(rest, [{key, value2} | emitted], keys2, fun) + end end - defp emit_right([], emitted, _queue_map, _fun), do: emitted + defp emit_right([], emitted, _keys2, _fun), do: emitted # Validates a keyword list while collecting its keys into a `%{key => []}` # lookup map. Raises with the full original list if a non-keyword element From 8885411de36743891a06719a5efea0f102cd8b02 Mon Sep 17 00:00:00 2001 From: Peter Ullrich Date: Thu, 14 May 2026 11:57:14 +0100 Subject: [PATCH 4/8] Update `Keyword.split/2`, `Keyword.take/2`, and `Keyword.drop/2` --- lib/elixir/lib/keyword.ex | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/keyword.ex b/lib/elixir/lib/keyword.ex index 2adef60736..e14a09a5c9 100644 --- a/lib/elixir/lib/keyword.ex +++ b/lib/elixir/lib/keyword.ex @@ -1273,18 +1273,21 @@ defmodule Keyword do """ @spec split(t, [key]) :: {t, t} def split(keywords, keys) when is_list(keywords) and is_list(keys) do - fun = fn {k, v}, {take, drop} -> - case k in keys do - true -> {[{k, v} | take], drop} - false -> {take, [{k, v} | drop]} - end + predicate = in_keys_check(keys) + + fun = fn pair, {take, drop} -> + if predicate.(pair), do: {[pair | take], drop}, else: {take, [pair | drop]} end - acc = {[], []} - {take, drop} = :lists.foldl(fun, acc, keywords) + {take, drop} = :lists.foldl(fun, {[], []}, keywords) {:lists.reverse(take), :lists.reverse(drop)} end + defp in_keys_check(keys) do + keys_set = MapSet.new(keys) + fn {key, _} -> MapSet.member?(keys_set, key) end + end + @doc """ Splits the `keywords` into two keyword lists according to the given function `fun`. @@ -1340,7 +1343,7 @@ defmodule Keyword do """ @spec take(t, [key]) :: t def take(keywords, keys) when is_list(keywords) and is_list(keys) do - :lists.filter(fn {k, _} -> :lists.member(k, keys) end, keywords) + :lists.filter(in_keys_check(keys), keywords) end @doc """ @@ -1360,7 +1363,8 @@ defmodule Keyword do """ @spec drop(t, [key]) :: t def drop(keywords, keys) when is_list(keywords) and is_list(keys) do - :lists.filter(fn {k, _} -> k not in keys end, keywords) + predicate = in_keys_check(keys) + :lists.filter(fn pair -> not predicate.(pair) end, keywords) end @doc """ From a6c7c100a95a8510223f3755bce7f558b22a44ec Mon Sep 17 00:00:00 2001 From: Peter Ullrich Date: Thu, 14 May 2026 12:03:01 +0100 Subject: [PATCH 5/8] Update `Keyword.pop/3` --- lib/elixir/lib/keyword.ex | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/keyword.ex b/lib/elixir/lib/keyword.ex index e14a09a5c9..30a1764efc 100644 --- a/lib/elixir/lib/keyword.ex +++ b/lib/elixir/lib/keyword.ex @@ -1391,12 +1391,18 @@ defmodule Keyword do """ @spec pop(t, key, default) :: {value | default, t} def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do - case fetch(keywords, key) do - {:ok, value} -> {value, delete(keywords, key)} - :error -> {default, keywords} - end + do_pop(keywords, key, default, []) end + defp do_pop([{key, value} | tail], key, _default, acc), + do: {value, :lists.reverse(acc, delete_key(tail, key))} + + defp do_pop([{_, _} = pair | tail], key, default, acc), + do: do_pop(tail, key, default, [pair | acc]) + + defp do_pop([], _key, default, acc), + do: {default, :lists.reverse(acc)} + @doc """ Returns the first value for `key` and removes all associated entries in the keyword list, raising if `key` is not present. From 2e6359515a32ae5b5dd72ea1dd058987b298233e Mon Sep 17 00:00:00 2001 From: Peter Ullrich Date: Thu, 14 May 2026 12:43:02 +0100 Subject: [PATCH 6/8] Add `Keyword.new/2` doctests to test correct order of interleaved keys --- lib/elixir/lib/keyword.ex | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/elixir/lib/keyword.ex b/lib/elixir/lib/keyword.ex index 30a1764efc..092fef4dfa 100644 --- a/lib/elixir/lib/keyword.ex +++ b/lib/elixir/lib/keyword.ex @@ -193,6 +193,15 @@ defmodule Keyword do iex> Keyword.new([{:a, 1}, {:a, 2}, {:a, 3}]) [a: 3] + iex> Keyword.new([{:a, 1}, {:b, 2}, {:a, 3}]) + [b: 2, a: 3] + + iex> Keyword.new([{:a, 1}, {:b, 2}, {:a, 3}, {:c, 4}, {:b, 5}, {:a, 6}]) + [c: 4, b: 5, a: 6] + + iex> Keyword.new([]) + [] + """ @spec new(Enumerable.t()) :: t def new(pairs) do From 4862d810a99bb3ea6c2d54b7ba228e41c03a7fbc Mon Sep 17 00:00:00 2001 From: Peter Ullrich Date: Thu, 14 May 2026 12:47:59 +0100 Subject: [PATCH 7/8] Remove comment --- lib/elixir/lib/keyword.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/elixir/lib/keyword.ex b/lib/elixir/lib/keyword.ex index 092fef4dfa..5a05989f02 100644 --- a/lib/elixir/lib/keyword.ex +++ b/lib/elixir/lib/keyword.ex @@ -1156,9 +1156,6 @@ defmodule Keyword do defp emit_right([], emitted, _keys2, _fun), do: emitted - # Validates a keyword list while collecting its keys into a `%{key => []}` - # lookup map. Raises with the full original list if a non-keyword element - # is encountered. Used by merge/2 and merge/3. defp collect_keys!(list), do: collect_keys!(list, %{}, list) defp collect_keys!([{key, _} | rest], acc, original) when is_atom(key), From 16aaf71a2eca165d1f88c6562d6290c646c38016 Mon Sep 17 00:00:00 2001 From: Peter Ullrich Date: Thu, 14 May 2026 13:52:03 +0100 Subject: [PATCH 8/8] Replace MapSet with Map --- lib/elixir/lib/keyword.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/keyword.ex b/lib/elixir/lib/keyword.ex index 5a05989f02..d6c8c24128 100644 --- a/lib/elixir/lib/keyword.ex +++ b/lib/elixir/lib/keyword.ex @@ -1107,10 +1107,10 @@ defmodule Keyword do keys2 = collect_keys!(keywords2) {non_matching_rev, keys2, duplicate_keys} = - partition_left(keywords1, [], keys2, MapSet.new()) + partition_left(keywords1, [], keys2, %{}) keys2 = - Enum.reduce(duplicate_keys, keys2, fn key, acc -> + Enum.reduce(duplicate_keys, keys2, fn {key, _true}, acc -> Map.update!(acc, key, &:lists.reverse/1) end) @@ -1128,7 +1128,7 @@ defmodule Keyword do rest, non_matching, Map.put(keys2, key, [value | current]), - MapSet.put(duplicate_keys, key) + Map.put(duplicate_keys, key, true) ) _ -> @@ -1290,8 +1290,8 @@ defmodule Keyword do end defp in_keys_check(keys) do - keys_set = MapSet.new(keys) - fn {key, _} -> MapSet.member?(keys_set, key) end + keys_set = :lists.foldl(fn key, acc -> Map.put(acc, key, true) end, %{}, keys) + fn {key, _} -> Map.has_key?(keys_set, key) end end @doc """