From 4129e608cac36fb2df336b8053558ec8e0022cfa Mon Sep 17 00:00:00 2001 From: Peter M Date: Wed, 27 May 2026 22:25:04 +0200 Subject: [PATCH 1/3] maps: Add take, update_with, with, without OTP-compatible implementations of maps:take/2, maps:update_with/3, maps:update_with/4, maps:with/2 and maps:without/2 to the estdlib maps module. Each function preserves OTP error semantics: - {badmap, Map} when the map argument is not a map (taking precedence over badarg when multiple arguments are wrong) - badarg when Keys is not a list (with/2, without/2) or Fun is not a function of arity 1 (update_with/3,4) - {badkey, Key} from update_with/3 only when Map is a valid map, Fun is arity-1 and Key is missing - update_with/4 inserts Init verbatim without invoking Fun when the key is absent Tests cover the happy paths plus error precedence, duplicate keys in with/without, a value of the atom 'error' in take/2 (must not be confused with the missing-key result), and the guarantee that update_with/4 does not call Fun on insert. Signed-off-by: Peter M --- CHANGELOG.md | 2 + libs/estdlib/src/maps.erl | 131 ++++++++++++++++++++++++++++++- tests/libs/estdlib/test_maps.erl | 75 ++++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6fbef48c..69fffe9722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for configuring pins and width for sdmmc on ESP32 - Added support for map comprehensions - Added USB CDC port drivers for ESP32, RP2, and STM32 platforms +- Added `maps:take/2`, `maps:update_with/3`, `maps:update_with/4`, `maps:with/2` and + `maps:without/2` to the estdlib `maps` module ### Changed - Updated network type db() to dbm() to reflect the actual representation of the type diff --git a/libs/estdlib/src/maps.erl b/libs/estdlib/src/maps.erl index 2b50d5d524..329a973965 100644 --- a/libs/estdlib/src/maps.erl +++ b/libs/estdlib/src/maps.erl @@ -57,7 +57,12 @@ merge/2, merge_with/3, remove/2, - update/3 + take/2, + update/3, + update_with/3, + update_with/4, + with/2, + without/2 ]). -export_type([ @@ -506,10 +511,134 @@ update(Key, Value, Map) -> _ = ?MODULE:get(Key, Map), Map#{Key => Value}. +%%----------------------------------------------------------------------------- +%% @param Key the key to take +%% @param Map the map from which to take the key +%% @returns `{Value, Map2}' if `Key' exists in `Map', where `Value' is the +%% value associated with `Key' and `Map2' is the map without `Key'. +%% Returns `error' if `Key' is not present in `Map'. +%% @doc Removes the `Key' from `Map' and returns the associated value together +%% with the updated map. +%% +%% This function raises a `{badmap, Map}' error if `Map' is not a map. +%% @end +%%----------------------------------------------------------------------------- +-spec take(Key, Map1 :: #{Key => Value, _ => _}) -> {Value, Map2 :: #{_ => _}} | error. +take(Key, Map) when is_map(Map) -> + case Map of + #{Key := Value} -> {Value, maps:remove(Key, Map)}; + _ -> error + end; +take(_Key, Map) -> + error({badmap, Map}). + +%%----------------------------------------------------------------------------- +%% @param Key the key to update +%% @param Fun the function to apply to the existing value +%% @param Map the map to update +%% @returns a new map with `Key' updated by applying `Fun' to its existing value. +%% @doc Updates the value in `Map' for `Key' by calling `Fun' with the old value. +%% +%% This function raises a `{badmap, Map}' error if `Map' is not a map, +%% a `{badkey, Key}' error if `Key' is not present in `Map', and a `badarg' +%% error if `Fun' is not a function of arity 1. +%% @end +%%----------------------------------------------------------------------------- +-spec update_with(Key, Fun :: fun((Value1) -> Value2), Map1 :: #{Key := Value1, _ => _}) -> + #{Key := Value2, _ => _}. +update_with(Key, Fun, Map) when is_function(Fun, 1) andalso is_map(Map) -> + case Map of + #{Key := Value} -> Map#{Key := Fun(Value)}; + #{} -> error({badkey, Key}) + end; +update_with(_Key, _Fun, Map) when not is_map(Map) -> + error({badmap, Map}); +update_with(_Key, _Fun, _Map) -> + error(badarg). + +%%----------------------------------------------------------------------------- +%% @param Key the key to update +%% @param Fun the function to apply to the existing value +%% @param Init the default value to insert if `Key' is not present +%% @param Map the map to update +%% @returns a new map with `Key' updated by `Fun', or inserted with `Init' +%% if `Key' was not present. +%% @doc Updates the value in `Map' for `Key' by calling `Fun' on the old value, +%% or inserts `Init' if `Key' was not previously present. +%% +%% This function raises a `{badmap, Map}' error if `Map' is not a map and a +%% `badarg' error if `Fun' is not a function of arity 1. +%% @end +%%----------------------------------------------------------------------------- +-spec update_with( + Key, + Fun :: fun((Value1) -> Value2), + Init, + Map1 :: #{Key => Value1, _ => _} +) -> #{Key := Value2 | Init, _ => _}. +update_with(Key, Fun, Init, Map) when is_function(Fun, 1) andalso is_map(Map) -> + case Map of + #{Key := Value} -> Map#{Key := Fun(Value)}; + #{} -> Map#{Key => Init} + end; +update_with(_Key, _Fun, _Init, Map) when not is_map(Map) -> + error({badmap, Map}); +update_with(_Key, _Fun, _Init, _Map) -> + error(badarg). + +%%----------------------------------------------------------------------------- +%% @param Keys the list of keys to keep +%% @param Map1 the map from which to select entries +%% @returns a new map containing only those entries from `Map1' whose keys +%% appear in `Keys'. +%% @doc Returns a new map containing only the entries from `Map1' whose key +%% is present in `Keys'. +%% +%% This function raises a `{badmap, Map}' error if `Map1' is not a map, and a +%% `badarg' error if `Keys' is not a list. +%% @end +%%----------------------------------------------------------------------------- +-spec with(Keys :: [K], Map1 :: #{K => V, _ => _}) -> #{K => V}. +with(Keys, Map) when is_list(Keys) andalso is_map(Map) -> + with_1(Keys, Map, ?MODULE:new()); +with(_Keys, Map) when not is_map(Map) -> + error({badmap, Map}); +with(_Keys, _Map) -> + error(badarg). + +%%----------------------------------------------------------------------------- +%% @param Keys the list of keys to drop +%% @param Map1 the map from which to drop entries +%% @returns a new map containing the entries from `Map1' whose keys are not +%% in `Keys'. +%% @doc Returns a new map containing the entries of `Map1' with the keys in +%% `Keys' removed. +%% +%% This function raises a `{badmap, Map}' error if `Map1' is not a map, and a +%% `badarg' error if `Keys' is not a list. +%% @end +%%----------------------------------------------------------------------------- +-spec without(Keys :: [K], Map1 :: #{K => _, _ => _}) -> #{_ => _}. +without(Keys, Map) when is_list(Keys) andalso is_map(Map) -> + lists:foldl(fun maps:remove/2, Map, Keys); +without(_Keys, Map) when not is_map(Map) -> + error({badmap, Map}); +without(_Keys, _Map) -> + error(badarg). + %% %% Internal functions %% +%% @private +with_1([], _Map, Acc) -> + Acc; +with_1([K | Ks], Map, Acc) -> + case Map of + #{K := V} -> with_1(Ks, Map, Acc#{K => V}); + #{} -> with_1(Ks, Map, Acc) + end. + %% @private iterate_keys(none, undefined, Accum) -> lists:reverse(Accum); diff --git a/tests/libs/estdlib/test_maps.erl b/tests/libs/estdlib/test_maps.erl index 7fe1ac3dbc..5fde8b5722 100644 --- a/tests/libs/estdlib/test_maps.erl +++ b/tests/libs/estdlib/test_maps.erl @@ -77,6 +77,11 @@ test() -> ok = test_remove(), ok = test_update(), ok = test_comprehension(), + ok = test_take(), + ok = test_update_with_3(), + ok = test_update_with_4(), + ok = test_with(), + ok = test_without(), ok. test_get() -> @@ -402,6 +407,76 @@ test_comprehension() -> test_comprehension() -> ok. -endif. +test_take() -> + ?ASSERT_EQUALS(maps:take(foo, maps:new()), error), + ?ASSERT_EQUALS(maps:take(a, #{a => 1, b => 2, c => 3}), {1, #{b => 2, c => 3}}), + ?ASSERT_EQUALS(maps:take(b, #{a => 1, b => 2, c => 3}), {2, #{a => 1, c => 3}}), + ?ASSERT_EQUALS(maps:take(c, #{a => 1, b => 2, c => 3}), {3, #{a => 1, b => 2}}), + ?ASSERT_EQUALS(maps:take(d, #{a => 1, b => 2, c => 3}), error), + %% value happens to be the atom 'error' - must not be confused with missing-key result + ?ASSERT_EQUALS(maps:take(a, #{a => error, b => 2}), {error, #{b => 2}}), + %% taking the only key leaves an empty map + ?ASSERT_EQUALS(maps:take(a, #{a => 1}), {1, #{}}), + ok = check_bad_map(fun() -> maps:take(foo, id(not_a_map)) end), + ok. + +test_update_with_3() -> + Inc = fun(V) -> V + 1 end, + ?ASSERT_EQUALS(maps:update_with(a, Inc, #{a => 1, b => 2}), #{a => 2, b => 2}), + ?ASSERT_EQUALS(maps:update_with(b, Inc, #{a => 1, b => 2}), #{a => 1, b => 3}), + ?ASSERT_ERROR(maps:update_with(c, Inc, #{a => 1, b => 2}), {badkey, c}), + ok = check_bad_map(fun() -> maps:update_with(a, Inc, id(not_a_map)) end), + ?ASSERT_ERROR(maps:update_with(a, not_a_function, maps:new()), badarg), + %% wrong-arity fun also yields badarg + ?ASSERT_ERROR(maps:update_with(a, fun(_, _) -> ok end, maps:new()), badarg), + %% badmap takes precedence over badarg when both args are wrong + ok = check_bad_map(fun() -> maps:update_with(a, not_a_function, id(not_a_map)) end), + ok. + +test_update_with_4() -> + Inc = fun(V) -> V + 1 end, + ?ASSERT_EQUALS(maps:update_with(a, Inc, 0, #{a => 1, b => 2}), #{a => 2, b => 2}), + ?ASSERT_EQUALS(maps:update_with(b, Inc, 0, #{a => 1, b => 2}), #{a => 1, b => 3}), + ?ASSERT_EQUALS(maps:update_with(c, Inc, 42, #{a => 1, b => 2}), #{a => 1, b => 2, c => 42}), + ?ASSERT_EQUALS(maps:update_with(c, Inc, 42, maps:new()), #{c => 42}), + ok = check_bad_map(fun() -> maps:update_with(a, Inc, 0, id(not_a_map)) end), + ?ASSERT_ERROR(maps:update_with(a, not_a_function, 0, maps:new()), badarg), + %% Fun must NOT be invoked when inserting Init for a missing key + Crash = fun(_) -> error(should_not_be_called) end, + ?ASSERT_EQUALS(maps:update_with(new_key, Crash, init, #{}), #{new_key => init}), + %% wrong-arity fun also yields badarg + ?ASSERT_ERROR(maps:update_with(a, fun(_, _) -> ok end, 0, maps:new()), badarg), + %% badmap takes precedence over badarg when both args are wrong + ok = check_bad_map(fun() -> maps:update_with(a, not_a_function, 0, id(not_a_map)) end), + ok. + +test_with() -> + ?ASSERT_EQUALS(maps:with([], #{a => 1, b => 2, c => 3}), #{}), + ?ASSERT_EQUALS(maps:with([a, c], #{a => 1, b => 2, c => 3}), #{a => 1, c => 3}), + ?ASSERT_EQUALS(maps:with([a, missing], #{a => 1, b => 2, c => 3}), #{a => 1}), + ?ASSERT_EQUALS(maps:with([missing], #{a => 1, b => 2, c => 3}), #{}), + ?ASSERT_EQUALS(maps:with([a, b, c], maps:new()), #{}), + %% duplicate keys are tolerated + ?ASSERT_EQUALS(maps:with([a, a, c], #{a => 1, b => 2, c => 3}), #{a => 1, c => 3}), + ok = check_bad_map(fun() -> maps:with([a], id(not_a_map)) end), + ?ASSERT_ERROR(maps:with(id(not_a_list), maps:new()), badarg), + %% badmap takes precedence over badarg when both args are wrong + ok = check_bad_map(fun() -> maps:with(id(not_a_list), id(not_a_map)) end), + ok. + +test_without() -> + ?ASSERT_EQUALS(maps:without([], #{a => 1, b => 2, c => 3}), #{a => 1, b => 2, c => 3}), + ?ASSERT_EQUALS(maps:without([a, c], #{a => 1, b => 2, c => 3}), #{b => 2}), + ?ASSERT_EQUALS(maps:without([missing], #{a => 1, b => 2, c => 3}), #{a => 1, b => 2, c => 3}), + ?ASSERT_EQUALS(maps:without([a, b, c], #{a => 1, b => 2, c => 3}), #{}), + ?ASSERT_EQUALS(maps:without([a], maps:new()), #{}), + %% duplicate keys are tolerated + ?ASSERT_EQUALS(maps:without([a, a, c], #{a => 1, b => 2, c => 3}), #{b => 2}), + ok = check_bad_map(fun() -> maps:without([a], id(not_a_map)) end), + ?ASSERT_ERROR(maps:without(id(not_a_list), maps:new()), badarg), + %% badmap takes precedence over badarg when both args are wrong + ok = check_bad_map(fun() -> maps:without(id(not_a_list), id(not_a_map)) end), + ok. id(X) -> X. From 28f232b21586e24a949c5d4f603bed60b50bc803 Mon Sep 17 00:00:00 2001 From: Peter M Date: Wed, 3 Jun 2026 12:38:42 +0200 Subject: [PATCH 2/3] maps: Add filtermap, intersect, intersect_with, groups_from_list, is_iterator_valid Add six pure Erlang functions to the estdlib maps module for improved OTP compatibility: - filtermap/2: Combined filter and map operation - intersect/2: Map intersection (values from second map) - intersect_with/3: Map intersection with value combiner - groups_from_list/2: Group list elements by key function - groups_from_list/3: Group with value transformation - is_iterator_valid/1: Iterator validation (internal, exported) All functions follow OTP semantics exactly, include comprehensive error handling ({badmap, Map}, badarg), work with both maps and iterators where applicable, and have full test coverage. Signed-off-by: Peter M --- CHANGELOG.md | 5 +- libs/estdlib/src/maps.erl | 243 ++++++++++++++++++++++++++++++- tests/libs/estdlib/test_maps.erl | 153 +++++++++++++++++++ 3 files changed, 397 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69fffe9722..c429575ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for configuring pins and width for sdmmc on ESP32 - Added support for map comprehensions - Added USB CDC port drivers for ESP32, RP2, and STM32 platforms -- Added `maps:take/2`, `maps:update_with/3`, `maps:update_with/4`, `maps:with/2` and - `maps:without/2` to the estdlib `maps` module +- Added `maps:take/2`, `maps:update_with/3`, `maps:update_with/4`, `maps:with/2`, + `maps:without/2`, `maps:filtermap/2`, `maps:intersect/2`, `maps:intersect_with/3`, + `maps:groups_from_list/2` and `maps:groups_from_list/3` to the estdlib `maps` module ### Changed - Updated network type db() to dbm() to reflect the actual representation of the type diff --git a/libs/estdlib/src/maps.erl b/libs/estdlib/src/maps.erl index 329a973965..4327f5718a 100644 --- a/libs/estdlib/src/maps.erl +++ b/libs/estdlib/src/maps.erl @@ -38,6 +38,7 @@ -export([ get/2, get/3, is_key/2, + is_iterator_valid/1, put/3, iterator/1, iterator/2, @@ -50,9 +51,14 @@ size/1, find/2, filter/2, + filtermap/2, fold/3, foreach/2, from_keys/2, + groups_from_list/2, + groups_from_list/3, + intersect/2, + intersect_with/3, map/2, merge/2, merge_with/3, @@ -131,8 +137,25 @@ is_key(Key, Map) -> erlang:is_map_key(Key, Map). %%----------------------------------------------------------------------------- -%% @param Key the key -%% @param Value the value +%% @param Iterator the iterator to validate +%% @returns `true' if the iterator is valid, `false' otherwise +%% @doc Check if an iterator is valid. +%% +%% This function checks if an iterator can still be used with `maps:next/1'. +%% An iterator becomes invalid if it has been exhausted or if the underlying +%% map has been modified. +%% +%% This is an internal function, primarily used by other functions in this module. +%% @end +%%----------------------------------------------------------------------------- +-spec is_iterator_valid(Iterator :: iterator()) -> boolean(). +is_iterator_valid(Iterator) -> + try is_iterator_valid_1(Iterator) + catch + error:badarg -> false + end. + +%%----------------------------------------------------------------------------- %% @param Map the map %% @returns A copy of `Map' containing the `{Key, Value}' association. %% @doc Return the map containing the `{Key, Value}' association. @@ -334,6 +357,38 @@ filter(_Pred, Map) when not is_map(Map) -> filter(_Pred, _Map) -> error(badarg). +%%----------------------------------------------------------------------------- +%% @param Fun a function that maps and filters entries from the map +%% @param MapOrIterator the map or map iterator to filter and map +%% @returns a map containing all elements in `MapOrIterator' that satisfy `Fun' +%% @doc Return a map whose entries are filtered and mapped by the supplied function. +%% +%% This function returns a new map containing all elements from the input +%% `MapOrIterator' that satisfy the input `Fun'. +%% +%% The supplied function is a function from key-value inputs to either `true' +%% (keep the entry), `false' (drop the entry), or `{true, NewValue}' (keep the +%% entry with a new value). +%% +%% This function raises a `{badmap, Map}' error if `Map' is not a map or map +%% iterator, and a `badarg' error if the input function is not a function. +%% @end +%%----------------------------------------------------------------------------- +-spec filtermap( + Fun :: fun((Key, Value) -> boolean() | {true, NewValue}), + MapOrIterator :: map_or_iterator(Key, Value) +) -> #{Key => Value | NewValue}. +filtermap(Fun, Map) when is_function(Fun, 2) andalso is_map(Map) -> + maps:from_list(iterate_filtermap(Fun, maps:next(maps:iterator(Map)), [])); +filtermap(Fun, [Pos | Map] = Iterator) when + is_function(Fun, 2) andalso is_integer(Pos) andalso is_map(Map) +-> + maps:from_list(iterate_filtermap(Fun, maps:next(Iterator), [])); +filtermap(_Fun, Map) when not is_map(Map) -> + error({badmap, Map}); +filtermap(_Fun, _Map) -> + error(badarg). + %%----------------------------------------------------------------------------- %% @param Fun function over which to fold values %% @param Init the initial value of the fold accumulator @@ -403,6 +458,126 @@ foreach(_Fun, _Map) -> from_keys(List, _Value) when is_list(List) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Fun a function that returns the key for each element +%% @param List the list to group +%% @returns a map where keys are the results of applying `Fun' to elements +%% and values are lists of elements that produced that key +%% @doc Group elements of a list by a key function. +%% +%% This function groups elements of `List' into a map. The key for each element +%% is computed by applying `Fun' to the element. All elements with the same key +%% are collected into a list, preserving the order from the original list. +%% +%% This function raises a `badarg' error if `Fun' is not a function of arity 1 +%% or if `List' is not a proper list. +%% @end +%%----------------------------------------------------------------------------- +-spec groups_from_list(Fun :: fun((Elem) -> Key), List :: [Elem]) -> #{Key => [Elem]}. +groups_from_list(Fun, List) when is_function(Fun, 1) -> + groups_from_list(Fun, fun(X) -> X end, List); +groups_from_list(_Fun, _List) -> + error(badarg). + +%%----------------------------------------------------------------------------- +%% @param KeyFun a function that returns the key for each element +%% @param ValueFun a function that returns the value for each element +%% @param List the list to group +%% @returns a map where keys are the results of applying `KeyFun' to elements +%% and values are lists of results of applying `ValueFun' to elements +%% that produced that key +%% @doc Group elements of a list by a key function, with value transformation. +%% +%% This function groups elements of `List' into a map. The key for each element +%% is computed by applying `KeyFun' to the element, and the value is computed +%% by applying `ValueFun' to the element. All elements with the same key +%% are collected into a list, preserving the order from the original list. +%% +%% This function raises a `badarg' error if `KeyFun' or `ValueFun' are not +%% functions of arity 1 or if `List' is not a proper list. +%% @end +%%----------------------------------------------------------------------------- +-spec groups_from_list( + KeyFun :: fun((Elem) -> Key), + ValueFun :: fun((Elem) -> Value), + List :: [Elem] +) -> #{Key => [Value]}. +groups_from_list(KeyFun, ValueFun, List) when + is_function(KeyFun, 1) andalso is_function(ValueFun, 1) +-> + try lists:reverse(List) of + RevList -> + groups_from_list_1(KeyFun, ValueFun, RevList, #{}) + catch + error:_ -> + error(badarg) + end; +groups_from_list(_KeyFun, _ValueFun, _List) -> + error(badarg). + +%%----------------------------------------------------------------------------- +%% @param Map1 a map +%% @param Map2 a map +%% @returns a map containing the intersection of `Map1' and `Map2' +%% @doc Return the intersection of two maps. +%% +%% This function returns a new map containing only those keys that exist in +%% both `Map1' and `Map2'. The values are taken from `Map2'. +%% +%% This function raises a `badmap' error if either `Map1' or `Map2' is not a map. +%% @end +%%----------------------------------------------------------------------------- +-spec intersect(Map1 :: #{Key => Value}, Map2 :: #{Key => Value}) -> #{Key => Value}. +intersect(Map1, Map2) when is_map(Map1) andalso is_map(Map2) -> + case map_size(Map1) =< map_size(Map2) of + true -> + intersect_with_small_map_first(fun(_K, _V1, V2) -> V2 end, Map1, Map2); + false -> + intersect_with_small_map_first(fun(_K, V1, _V2) -> V1 end, Map2, Map1) + end; +intersect(Map1, _Map2) when not is_map(Map1) -> + error({badmap, Map1}); +intersect(_Map1, Map2) when not is_map(Map2) -> + error({badmap, Map2}). + +%%----------------------------------------------------------------------------- +%% @param Combiner a function to combine values from Map1 and Map2 +%% @param Map1 a map +%% @param Map2 a map +%% @returns a map containing the intersection of `Map1' and `Map2' with combined values +%% @doc Return the intersection of two maps with combined values. +%% +%% This function returns a new map containing only those keys that exist in +%% both `Map1' and `Map2'. For each such key, the value is computed by calling +%% `Combiner(Key, Value1, Value2)' where `Value1' is from `Map1' and `Value2' +%% is from `Map2'. +%% +%% This function raises a `badmap' error if either `Map1' or `Map2' is not a map, +%% and a `badarg' error if `Combiner' is not a function of arity 3. +%% @end +%%----------------------------------------------------------------------------- +-spec intersect_with( + Combiner :: fun((Key, Value, Value) -> Value), + Map1 :: #{Key => Value}, + Map2 :: #{Key => Value} +) -> #{Key => Value}. +intersect_with(Combiner, Map1, Map2) when + is_map(Map1) andalso is_map(Map2) andalso is_function(Combiner, 3) +-> + case map_size(Map1) =< map_size(Map2) of + true -> + intersect_with_small_map_first(Combiner, Map1, Map2); + false -> + RCombiner = fun(K, V1, V2) -> Combiner(K, V2, V1) end, + intersect_with_small_map_first(RCombiner, Map2, Map1) + end; +intersect_with(_Combiner, Map1, _Map2) when not is_map(Map1) -> + error({badmap, Map1}); +intersect_with(_Combiner, _Map1, Map2) when not is_map(Map2) -> + error({badmap, Map2}); +intersect_with(_Combiner, _Map1, _Map2) -> + error(badarg). + %%----------------------------------------------------------------------------- %% @param Fun the function to apply to every entry in the map %% @param Map the map to which to apply the map function @@ -731,3 +906,67 @@ iterate_from_list([{Key, Value} | T], Accum) -> iterate_from_list(T, Accum#{Key => Value}); iterate_from_list(_List, _Accum) -> error(badarg). + +%% @private +iterate_filtermap(_Fun, none, Accum) -> + lists:reverse(Accum); +iterate_filtermap(Fun, {Key, Value, Iterator}, Accum) -> + NewAccum = + case Fun(Key, Value) of + true -> + [{Key, Value} | Accum]; + {true, NewValue} -> + [{Key, NewValue} | Accum]; + false -> + Accum + end, + iterate_filtermap(Fun, maps:next(Iterator), NewAccum). + +%% @private +groups_from_list_1(_KeyFun, _ValueFun, [], Acc) -> + Acc; +groups_from_list_1(KeyFun, ValueFun, [Elem | Rest], Acc) -> + Key = KeyFun(Elem), + Value = ValueFun(Elem), + NewAcc = + case Acc of + #{Key := Values} -> + Acc#{Key := [Value | Values]}; + #{} -> + Acc#{Key => [Value]} + end, + groups_from_list_1(KeyFun, ValueFun, Rest, NewAcc). + +%% @private +intersect_with_small_map_first(Combiner, SmallMap, BigMap) -> + Next = maps:next(maps:iterator(SmallMap)), + intersect_with_iterate(Next, [], BigMap, Combiner). + +%% @private +intersect_with_iterate({K, V1, Iterator}, Keep, BigMap, Combiner) -> + Next = maps:next(Iterator), + case BigMap of + #{K := V2} -> + V = Combiner(K, V1, V2), + intersect_with_iterate(Next, [{K, V} | Keep], BigMap, Combiner); + #{} -> + intersect_with_iterate(Next, Keep, BigMap, Combiner) + end; +intersect_with_iterate(none, Keep, _BigMap, _Combiner) -> + maps:from_list(Keep). + +%% @private +is_iterator_valid_1(none) -> + true; +is_iterator_valid_1({_, _, Iter}) -> + is_iterator_valid_1(Iter); +is_iterator_valid_1([Pos | Map]) when is_integer(Pos), is_map(Map) -> + %% Default iterator - try to use it + _ = maps:next([Pos | Map]), + true; +is_iterator_valid_1([Keys | Map]) when is_list(Keys), is_map(Map) -> + %% Ordered iterator - try to use it + _ = maps:next([Keys | Map]), + true; +is_iterator_valid_1(_) -> + false. diff --git a/tests/libs/estdlib/test_maps.erl b/tests/libs/estdlib/test_maps.erl index 5fde8b5722..1de522b8cd 100644 --- a/tests/libs/estdlib/test_maps.erl +++ b/tests/libs/estdlib/test_maps.erl @@ -82,6 +82,11 @@ test() -> ok = test_update_with_4(), ok = test_with(), ok = test_without(), + ok = test_filtermap(), + ok = test_intersect(), + ok = test_intersect_with(), + ok = test_groups_from_list(), + ok = test_is_iterator_valid(), ok. test_get() -> @@ -478,6 +483,154 @@ test_without() -> ok = check_bad_map(fun() -> maps:without(id(not_a_list), id(not_a_map)) end), ok. +test_filtermap() -> + %% Empty map yields empty map + ?ASSERT_EQUALS(maps:filtermap(fun(_K, _V) -> true end, maps:new()), #{}), + %% Keep all entries + ?ASSERT_EQUALS( + maps:filtermap(fun(_K, _V) -> true end, #{a => 1, b => 2}), + #{a => 1, b => 2} + ), + %% Drop all entries + ?ASSERT_EQUALS( + maps:filtermap(fun(_K, _V) -> false end, #{a => 1, b => 2}), + #{} + ), + %% Filter with predicate + ?ASSERT_EQUALS( + maps:filtermap(fun(_K, V) -> V > 1 end, #{a => 1, b => 2, c => 3}), + #{b => 2, c => 3} + ), + %% Transform values + ?ASSERT_EQUALS( + maps:filtermap(fun(_K, V) -> {true, V * 10} end, #{a => 1, b => 2}), + #{a => 10, b => 20} + ), + %% Filter and transform + ?ASSERT_EQUALS( + maps:filtermap(fun(_K, V) -> case V rem 2 of 0 -> {true, V * 10}; _ -> false end end, #{ + a => 1, b => 2, c => 3, d => 4 + }), + #{b => 20, d => 40} + ), + %% Works with iterators + Iter = maps:iterator(#{a => 1, b => 2, c => 3}), + ?ASSERT_EQUALS( + maps:filtermap(fun(_K, V) -> V > 1 end, Iter), + #{b => 2, c => 3} + ), + %% Error cases + ok = check_bad_map(fun() -> maps:filtermap(fun(_K, _V) -> true end, id(not_a_map)) end), + ?ASSERT_ERROR(maps:filtermap(not_a_function, maps:new()), badarg), + ok. + +test_intersect() -> + %% Empty maps + ?ASSERT_EQUALS(maps:intersect(maps:new(), maps:new()), #{}), + ?ASSERT_EQUALS(maps:intersect(#{a => 1}, maps:new()), #{}), + ?ASSERT_EQUALS(maps:intersect(maps:new(), #{a => 1}), #{}), + %% No common keys + ?ASSERT_EQUALS(maps:intersect(#{a => 1}, #{b => 2}), #{}), + %% Some common keys - values from second map + ?ASSERT_EQUALS( + maps:intersect(#{a => 1, b => 2, c => 3}, #{a => 10, b => 20, d => 40}), + #{a => 10, b => 20} + ), + %% All keys common + ?ASSERT_EQUALS( + maps:intersect(#{a => 1, b => 2}, #{a => 10, b => 20}), + #{a => 10, b => 20} + ), + %% Error cases + ok = check_bad_map(fun() -> maps:intersect(id(not_a_map), #{}) end), + ok = check_bad_map(fun() -> maps:intersect(#{}, id(not_a_map)) end), + ok. + +test_intersect_with() -> + %% Empty maps + Combiner = fun(_K, V1, V2) -> V1 + V2 end, + ?ASSERT_EQUALS(maps:intersect_with(Combiner, maps:new(), maps:new()), #{}), + ?ASSERT_EQUALS(maps:intersect_with(Combiner, #{a => 1}, maps:new()), #{}), + ?ASSERT_EQUALS(maps:intersect_with(Combiner, maps:new(), #{a => 1}), #{}), + %% No common keys + ?ASSERT_EQUALS(maps:intersect_with(Combiner, #{a => 1}, #{b => 2}), #{}), + %% Some common keys - combine values + ?ASSERT_EQUALS( + maps:intersect_with(Combiner, #{a => 1, b => 2, c => 3}, #{a => 10, b => 20, d => 40}), + #{a => 11, b => 22} + ), + %% All keys common + ?ASSERT_EQUALS( + maps:intersect_with(Combiner, #{a => 1, b => 2}, #{a => 10, b => 20}), + #{a => 11, b => 22} + ), + %% Combiner receives key and both values + KeyCombiner = fun(K, V1, V2) -> {K, V1, V2} end, + ?ASSERT_EQUALS( + maps:intersect_with(KeyCombiner, #{a => 1}, #{a => 2}), + #{a => {a, 1, 2}} + ), + %% Map1 larger than Map2 still passes values to Combiner in Map1, Map2 order + ?ASSERT_EQUALS( + maps:intersect_with(KeyCombiner, #{a => 1, b => 2, c => 3}, #{b => 20}), + #{b => {b, 2, 20}} + ), + %% Error cases + ok = check_bad_map(fun() -> maps:intersect_with(Combiner, id(not_a_map), #{}) end), + ok = check_bad_map(fun() -> maps:intersect_with(Combiner, #{}, id(not_a_map)) end), + ?ASSERT_ERROR(maps:intersect_with(not_a_function, #{}, #{}), badarg), + ok. + +test_groups_from_list() -> + %% Empty list + ?ASSERT_EQUALS(maps:groups_from_list(fun(X) -> X end, []), #{}), + %% Group by identity + ?ASSERT_EQUALS( + maps:groups_from_list(fun(X) -> X end, [a, b, a, c, b, a]), + #{a => [a, a, a], b => [b, b], c => [c]} + ), + %% Group by length + ?ASSERT_EQUALS( + maps:groups_from_list(fun length/1, ["ant", "buffalo", "cat", "dingo"]), + #{3 => ["ant", "cat"], 5 => ["dingo"], 7 => ["buffalo"]} + ), + %% With value transformation + ?ASSERT_EQUALS( + maps:groups_from_list(fun(X) -> X rem 2 end, fun(X) -> X * 10 end, [1, 2, 3, 4, 5]), + #{0 => [20, 40], 1 => [10, 30, 50]} + ), + %% Preserves order within groups + ?ASSERT_EQUALS( + maps:groups_from_list(fun(X) -> X rem 3 end, [1, 2, 3, 4, 5, 6]), + #{0 => [3, 6], 1 => [1, 4], 2 => [2, 5]} + ), + %% Error cases + ?ASSERT_ERROR(maps:groups_from_list(not_a_function, []), badarg), + ?ASSERT_ERROR(maps:groups_from_list(fun(X) -> X end, not_a_list), badarg), + ok. + +test_is_iterator_valid() -> + %% Valid iterator from empty map + Iter1 = maps:iterator(maps:new()), + ?ASSERT_EQUALS(maps:is_iterator_valid(Iter1), true), + %% Valid iterator from non-empty map + Iter2 = maps:iterator(#{a => 1, b => 2}), + ?ASSERT_EQUALS(maps:is_iterator_valid(Iter2), true), + %% Exhausted iterator (none) is valid + ?ASSERT_EQUALS(maps:is_iterator_valid(none), true), + %% Partially consumed iterator + {_, _, Iter3} = maps:next(maps:iterator(#{a => 1, b => 2})), + ?ASSERT_EQUALS(maps:is_iterator_valid(Iter3), true), + %% Ordered iterator + Iter4 = maps:iterator(#{a => 1, b => 2}, ordered), + ?ASSERT_EQUALS(maps:is_iterator_valid(Iter4), true), + %% Invalid iterators + ?ASSERT_EQUALS(maps:is_iterator_valid(not_an_iterator), false), + ?ASSERT_EQUALS(maps:is_iterator_valid(42), false), + ?ASSERT_EQUALS(maps:is_iterator_valid([]), false), + ?ASSERT_EQUALS(maps:is_iterator_valid([not_int | #{}]), false), + ok. + id(X) -> X. check_bad_map(F) -> From 9306ac023f91186f8ea707b2f51fc88d742247a3 Mon Sep 17 00:00:00 2001 From: Peter M Date: Wed, 3 Jun 2026 13:24:30 +0200 Subject: [PATCH 3/3] maps:take/2 nif Signed-off-by: Peter M --- libs/estdlib/src/maps.erl | 12 ++---- src/libAtomVM/nifs.c | 65 ++++++++++++++++++++++++++++++++ src/libAtomVM/nifs.gperf | 1 + tests/libs/estdlib/test_maps.erl | 19 ++++++++-- 4 files changed, 86 insertions(+), 11 deletions(-) diff --git a/libs/estdlib/src/maps.erl b/libs/estdlib/src/maps.erl index 4327f5718a..36e12fab09 100644 --- a/libs/estdlib/src/maps.erl +++ b/libs/estdlib/src/maps.erl @@ -150,7 +150,8 @@ is_key(Key, Map) -> %%----------------------------------------------------------------------------- -spec is_iterator_valid(Iterator :: iterator()) -> boolean(). is_iterator_valid(Iterator) -> - try is_iterator_valid_1(Iterator) + try + is_iterator_valid_1(Iterator) catch error:badarg -> false end. @@ -699,13 +700,8 @@ update(Key, Value, Map) -> %% @end %%----------------------------------------------------------------------------- -spec take(Key, Map1 :: #{Key => Value, _ => _}) -> {Value, Map2 :: #{_ => _}} | error. -take(Key, Map) when is_map(Map) -> - case Map of - #{Key := Value} -> {Value, maps:remove(Key, Map)}; - _ -> error - end; -take(_Key, Map) -> - error({badmap, Map}). +take(_, _) -> + erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param Key the key to update diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 2e3f70f7eb..b726a0c3df 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -291,6 +291,7 @@ static term nif_lists_keymember(Context *ctx, int argc, term argv[]); static term nif_lists_member(Context *ctx, int argc, term argv[]); static term nif_maps_from_keys(Context *ctx, int argc, term argv[]); static term nif_maps_next(Context *ctx, int argc, term argv[]); +static term nif_maps_take(Context *ctx, int argc, term argv[]); static term nif_unicode_characters_to_list(Context *ctx, int argc, term argv[]); static term nif_unicode_characters_to_binary(Context *ctx, int argc, term argv[]); static term nif_erlang_lists_subtract(Context *ctx, int argc, term argv[]); @@ -943,6 +944,10 @@ static const struct Nif maps_next_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_maps_next }; +static const struct Nif maps_take_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_maps_take +}; static const struct Nif unicode_characters_to_list_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_unicode_characters_to_list @@ -6832,6 +6837,66 @@ static term nif_maps_next(Context *ctx, int argc, term argv[]) return ret; } +static term nif_maps_take(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term key = argv[0]; + term map = argv[1]; + + if (UNLIKELY(!term_is_map(map))) { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &map, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term err = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(err, 0, BADMAP_ATOM); + term_put_tuple_element(err, 1, map); + RAISE_ERROR(err); + } + + int pos = term_find_map_pos(map, key, ctx->global); + if (pos == TERM_MAP_MEMORY_ALLOC_FAIL) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + if (pos == TERM_MAP_NOT_FOUND) { + return ERROR_ATOM; + } + + int old_size = term_get_map_size(map); + int new_size = old_size - 1; + + // Allocate space for new map + result tuple {Value, NewMap} + size_t heap_needed = term_map_size_in_terms(new_size) + TUPLE_SIZE(2); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, heap_needed, 2, argv, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + // Recompute after possible GC + key = argv[0]; + map = argv[1]; + pos = term_find_map_pos(map, key, ctx->global); + if (pos == TERM_MAP_MEMORY_ALLOC_FAIL) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term value = term_get_map_value(map, pos); + + // Create new map without the key + term new_map = term_alloc_map(new_size, &ctx->heap); + for (int i = 0, j = 0; i < old_size; i++) { + if (i != pos) { + term_set_map_assoc(new_map, j, term_get_map_key(map, i), term_get_map_value(map, i)); + j++; + } + } + + // Return {Value, NewMap} + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, value); + term_put_tuple_element(result, 1, new_map); + + return result; +} + static bool encoding_from_atom(term encoding_atom, enum CharDataEncoding *encoding) { switch (encoding_atom) { diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index c7ce64cf7e..d4bd925a4e 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -243,6 +243,7 @@ lists:reverse/1, &lists_reverse_nif lists:reverse/2, &lists_reverse_nif maps:from_keys/2, &maps_from_keys_nif maps:next/1, &maps_next_nif +maps:take/2, &maps_take_nif unicode:characters_to_list/1, &unicode_characters_to_list_nif unicode:characters_to_list/2, &unicode_characters_to_list_nif unicode:characters_to_binary/1, &unicode_characters_to_binary_nif diff --git a/tests/libs/estdlib/test_maps.erl b/tests/libs/estdlib/test_maps.erl index 1de522b8cd..6d38f70808 100644 --- a/tests/libs/estdlib/test_maps.erl +++ b/tests/libs/estdlib/test_maps.erl @@ -423,6 +423,11 @@ test_take() -> %% taking the only key leaves an empty map ?ASSERT_EQUALS(maps:take(a, #{a => 1}), {1, #{}}), ok = check_bad_map(fun() -> maps:take(foo, id(not_a_map)) end), + %% heap-term keys must survive GC + ?ASSERT_EQUALS(maps:take({complex, key}, #{{complex, key} => v}), {v, #{}}), + %% heap-term badmap arguments must not cause stale pointers + ok = check_bad_map(fun() -> maps:take(foo, id({this_is, not_a, map})) end), + ok = check_bad_map(fun() -> maps:take(foo, id([this_is, not_a, map])) end), ok. test_update_with_3() -> @@ -508,9 +513,17 @@ test_filtermap() -> ), %% Filter and transform ?ASSERT_EQUALS( - maps:filtermap(fun(_K, V) -> case V rem 2 of 0 -> {true, V * 10}; _ -> false end end, #{ - a => 1, b => 2, c => 3, d => 4 - }), + maps:filtermap( + fun(_K, V) -> + case V rem 2 of + 0 -> {true, V * 10}; + _ -> false + end + end, + #{ + a => 1, b => 2, c => 3, d => 4 + } + ), #{b => 20, d => 40} ), %% Works with iterators