diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6fbef48c..eb43a93cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ 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 more OTP-compatible functions to the estdlib `maps` module, including + `maps:take/2`, `maps:update_with/3,4`, `maps:with/2`, `maps:without/2`, + `maps:filtermap/2`, `maps:intersect/2`, `maps:intersect_with/3`, + `maps:groups_from_list/2,3`, and `maps:is_iterator_valid/1` ### 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..9557d5222b 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,14 +51,24 @@ 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, remove/2, - update/3 + take/2, + update/3, + update_with/3, + update_with/4, + with/2, + without/2 ]). -export_type([ @@ -126,8 +137,23 @@ is_key(Key, Map) -> erlang:is_map_key(Key, Map). %%----------------------------------------------------------------------------- -%% @param Key the key -%% @param Value the value +%% @param MaybeIterator the term to inspect +%% @returns `true' if `MaybeIterator' is a valid map iterator; `false' otherwise +%% @doc Check if a term is a valid map iterator. +%% +%% This function recognizes iterators returned by `maps:iterator/1,2', +%% `{Key, Value, Iterator}' triples returned by `maps:next/1', and `none'. +%% @end +%%----------------------------------------------------------------------------- +-spec is_iterator_valid(MaybeIterator :: iterator() | term()) -> boolean(). +is_iterator_valid(MaybeIterator) -> + try + is_iterator_valid_1(MaybeIterator) + 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. @@ -241,13 +267,16 @@ values(Map) -> %% an iterator. %% @end %%----------------------------------------------------------------------------- --spec to_list(Map :: #{Key => Value}) -> [{Key, Value}]. +-spec to_list(MapOrIterator :: map_or_iterator(Key, Value)) -> [{Key, Value}]. to_list(Map) when is_map(Map) -> - to_list(maps:iterator(Map)); -to_list(Iterator) when is_list(Iterator) andalso is_map(tl(Iterator)) -> - iterate_entries(maps:next(Iterator), []); -to_list(Map) -> - error({badmap, Map}). + iterate_entries(maps:next(maps:iterator(Map)), [], undefined); +to_list(Iterator) -> + ErrorTag = make_ref(), + try + iterate_entries(try_next(Iterator, ErrorTag), [], ErrorTag) + catch + error:ErrorTag -> error({badmap, Iterator}) + end. %%----------------------------------------------------------------------------- %% @param List a list of `[{Key, Value}]' pairs @@ -319,16 +348,50 @@ find(Key, Map) -> MapOrIterator :: map_or_iterator(Key, Value) ) -> #{Key => Value}. filter(Pred, Map) when is_function(Pred, 2) andalso is_map(Map) -> - iterate_filter(Pred, maps:next(maps:iterator(Map)), ?MODULE:new()); -filter(Pred, [Pos | Map] = Iterator) when - is_function(Pred, 2) andalso is_integer(Pos) andalso is_map(Map) --> - iterate_filter(Pred, maps:next(Iterator), ?MODULE:new()); -filter(_Pred, Map) when not is_map(Map) -> - error({badmap, Map}); + iterate_filter(Pred, maps:next(maps:iterator(Map)), ?MODULE:new(), undefined); +filter(Pred, Iterator) when is_function(Pred, 2) -> + ErrorTag = make_ref(), + try + iterate_filter(Pred, try_next(Iterator, ErrorTag), ?MODULE:new(), ErrorTag) + catch + error:ErrorTag -> error({badmap, Iterator}) + end; 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)), [], undefined)); +filtermap(Fun, Iterator) when is_function(Fun, 2) -> + ErrorTag = make_ref(), + try + maps:from_list(iterate_filtermap(Fun, try_next(Iterator, ErrorTag), [], ErrorTag)) + catch + error:ErrorTag -> error({badmap, Iterator}) + end; +filtermap(_Fun, _Map) -> + error(badarg). + %%----------------------------------------------------------------------------- %% @param Fun function over which to fold values %% @param Init the initial value of the fold accumulator @@ -350,13 +413,14 @@ filter(_Pred, _Map) -> MapOrIterator :: map_or_iterator(Key, Value) ) -> Accum. fold(Fun, Init, Map) when is_function(Fun, 3) andalso is_map(Map) -> - iterate_fold(Fun, maps:next(maps:iterator(Map)), Init); -fold(Fun, Init, [Pos | Map] = Iterator) when - is_function(Fun, 3) andalso is_integer(Pos) andalso is_map(Map) --> - iterate_fold(Fun, maps:next(Iterator), Init); -fold(_Fun, _Init, Map) when not is_map(Map) -> - error({badmap, Map}); + iterate_fold(Fun, maps:next(maps:iterator(Map)), Init, undefined); +fold(Fun, Init, Iterator) when is_function(Fun, 3) -> + ErrorTag = make_ref(), + try + iterate_fold(Fun, try_next(Iterator, ErrorTag), Init, ErrorTag) + catch + error:ErrorTag -> error({badmap, Iterator}) + end; fold(_Fun, _Init, _Map) -> error(badarg). @@ -377,13 +441,14 @@ fold(_Fun, _Init, _Map) -> MapOrIterator :: map_or_iterator(Key, Value) ) -> ok. foreach(Fun, Map) when is_function(Fun, 2) andalso is_map(Map) -> - iterate_foreach(Fun, maps:next(maps:iterator(Map))); -foreach(Fun, [Pos | Map] = Iterator) when - is_function(Fun, 2) andalso is_integer(Pos) andalso is_map(Map) --> - iterate_foreach(Fun, maps:next(Iterator)); -foreach(_Fun, Map) when not is_map(Map) -> - error({badmap, Map}); + iterate_foreach(Fun, maps:next(maps:iterator(Map)), undefined); +foreach(Fun, Iterator) when is_function(Fun, 2) -> + ErrorTag = make_ref(), + try + iterate_foreach(Fun, try_next(Iterator, ErrorTag), ErrorTag) + catch + error:ErrorTag -> error({badmap, Iterator}) + end; foreach(_Fun, _Map) -> error(badarg). @@ -398,6 +463,131 @@ 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 `badarg' error if `Combiner' is not a function of +%% arity 3. Otherwise, it raises a `badmap' error if either `Map1' or `Map2' is +%% not a map. +%% @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 + is_function(Combiner, 3) andalso not is_map(Map1) +-> + error({badmap, Map1}); +intersect_with(Combiner, _Map1, Map2) when + is_function(Combiner, 3) andalso 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 @@ -411,13 +601,14 @@ from_keys(List, _Value) when is_list(List) -> -spec map(Fun :: fun((Key, Value) -> MappedValue), Map :: map_or_iterator(Key, Value)) -> #{Key => MappedValue}. map(Fun, Map) when is_function(Fun, 2) andalso is_map(Map) -> - iterate_map(Fun, maps:next(maps:iterator(Map)), ?MODULE:new()); -map(Fun, [Pos | Map] = Iterator) when - is_function(Fun, 2) andalso is_integer(Pos) andalso is_map(Map) --> - iterate_map(Fun, maps:next(Iterator), ?MODULE:new()); -map(_Fun, Map) when not is_map(Map) -> - error({badmap, Map}); + iterate_map(Fun, maps:next(maps:iterator(Map)), ?MODULE:new(), undefined); +map(Fun, Iterator) when is_function(Fun, 2) -> + ErrorTag = make_ref(), + try + iterate_map(Fun, try_next(Iterator, ErrorTag), ?MODULE:new(), ErrorTag) + catch + error:ErrorTag -> error({badmap, Iterator}) + end; map(_Fun, _Map) -> error(badarg). @@ -481,14 +672,17 @@ merge_with(_Combiner, _Map1, Map2) when not is_map(Map2) -> remove(Key, Map) when is_map(Map) -> case ?MODULE:is_key(Key, Map) of true -> - iterate_remove(Key, maps:next(maps:iterator(Map)), ?MODULE:new()); + iterate_remove(Key, maps:next(maps:iterator(Map)), ?MODULE:new(), undefined); _ -> Map end; -remove(Key, [Pos | Map] = Iterator) when is_integer(Pos) andalso is_map(Map) -> - iterate_remove(Key, maps:next(Iterator), ?MODULE:new()); -remove(_Key, Map) when not is_map(Map) -> - error({badmap, Map}). +remove(Key, Iterator) -> + ErrorTag = make_ref(), + try + iterate_remove(Key, try_next(Iterator, ErrorTag), ?MODULE:new(), ErrorTag) + catch + error:ErrorTag -> error({badmap, Iterator}) + end. %%----------------------------------------------------------------------------- %% @param Key the key to update @@ -506,10 +700,129 @@ 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(_, _) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @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); @@ -529,44 +842,44 @@ iterate_values({_Key, Value, Iterator}, Accum) -> iterate_values(maps:next(Iterator), [Value | Accum]). %% @private -iterate_entries(none, Accum) -> +iterate_entries(none, Accum, _ErrorTag) -> lists:reverse(Accum); -iterate_entries({Key, Value, Iterator}, Accum) -> - iterate_entries(maps:next(Iterator), [{Key, Value} | Accum]). +iterate_entries({Key, Value, Iterator}, Accum, ErrorTag) -> + iterate_entries(try_next(Iterator, ErrorTag), [{Key, Value} | Accum], ErrorTag). %% @private -iterate_filter(_Pred, none, Accum) -> +iterate_filter(_Pred, none, Accum, _ErrorTag) -> Accum; -iterate_filter(Pred, {Key, Value, Iterator}, Accum) -> +iterate_filter(Pred, {Key, Value, Iterator}, Accum, ErrorTag) -> NewAccum = case Pred(Key, Value) of true -> Accum#{Key => Value}; - _ -> + false -> Accum end, - iterate_filter(Pred, maps:next(Iterator), NewAccum). + iterate_filter(Pred, try_next(Iterator, ErrorTag), NewAccum, ErrorTag). %% @private -iterate_fold(_Fun, none, Accum) -> +iterate_fold(_Fun, none, Accum, _ErrorTag) -> Accum; -iterate_fold(Fun, {Key, Value, Iterator}, Accum) -> +iterate_fold(Fun, {Key, Value, Iterator}, Accum, ErrorTag) -> NewAccum = Fun(Key, Value, Accum), - iterate_fold(Fun, maps:next(Iterator), NewAccum). + iterate_fold(Fun, try_next(Iterator, ErrorTag), NewAccum, ErrorTag). %% @private -iterate_foreach(_Fun, none) -> +iterate_foreach(_Fun, none, _ErrorTag) -> ok; -iterate_foreach(Fun, {Key, Value, Iterator}) -> +iterate_foreach(Fun, {Key, Value, Iterator}, ErrorTag) -> _ = Fun(Key, Value), - iterate_foreach(Fun, maps:next(Iterator)). + iterate_foreach(Fun, try_next(Iterator, ErrorTag), ErrorTag). %% @private -iterate_map(_Fun, none, Accum) -> +iterate_map(_Fun, none, Accum, _ErrorTag) -> Accum; -iterate_map(Fun, {Key, Value, Iterator}, Accum) -> +iterate_map(Fun, {Key, Value, Iterator}, Accum, ErrorTag) -> NewAccum = Accum#{Key => Fun(Key, Value)}, - iterate_map(Fun, maps:next(Iterator), NewAccum). + iterate_map(Fun, try_next(Iterator, ErrorTag), NewAccum, ErrorTag). %% @private iterate_merge_with(_Combiner, none, Accum) -> @@ -588,12 +901,12 @@ iterate_merge({Key, Value, Iterator}, Accum) -> iterate_merge(maps:next(Iterator), Accum#{Key => Value}). %% @private -iterate_remove(_Key, none, Accum) -> +iterate_remove(_Key, none, Accum, _ErrorTag) -> Accum; -iterate_remove(Key, {Key, _Value, Iterator}, Accum) -> - iterate_remove(Key, maps:next(Iterator), Accum); -iterate_remove(Key, {OtherKey, Value, Iterator}, Accum) -> - iterate_remove(Key, maps:next(Iterator), Accum#{OtherKey => Value}). +iterate_remove(Key, {Key, _Value, Iterator}, Accum, ErrorTag) -> + iterate_remove(Key, try_next(Iterator, ErrorTag), Accum, ErrorTag); +iterate_remove(Key, {OtherKey, Value, Iterator}, Accum, ErrorTag) -> + iterate_remove(Key, try_next(Iterator, ErrorTag), Accum#{OtherKey => Value}, ErrorTag). %% @private iterate_from_list([], Accum) -> @@ -602,3 +915,81 @@ 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, _ErrorTag) -> + lists:reverse(Accum); +iterate_filtermap(Fun, {Key, Value, Iterator}, Accum, ErrorTag) -> + NewAccum = + case Fun(Key, Value) of + true -> + [{Key, Value} | Accum]; + {true, NewValue} -> + [{Key, NewValue} | Accum]; + false -> + Accum + end, + iterate_filtermap(Fun, try_next(Iterator, ErrorTag), NewAccum, ErrorTag). + +%% @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 +try_next({_, _, _} = KeyValueIterator, _ErrorTag) -> + KeyValueIterator; +try_next(none, _ErrorTag) -> + none; +try_next(Iterator, undefined) -> + maps:next(Iterator); +try_next(Iterator, ErrorTag) -> + try + maps:next(Iterator) + catch + error:badarg -> error(ErrorTag) + end. + +%% @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/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 2e3f70f7eb..b7ab44d1ef 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 @@ -6781,6 +6786,9 @@ static term nif_maps_next(Context *ctx, int argc, term argv[]) } term iterator = argv[0]; + if (term_is_tuple(iterator) && term_get_tuple_arity(iterator) == 3) { + return iterator; + } VALIDATE_VALUE(iterator, term_is_nonempty_list); term post = term_get_list_head(iterator); @@ -6794,16 +6802,34 @@ static term nif_maps_next(Context *ctx, int argc, term argv[]) int pos; if (term_is_integer(post)) { int size = term_get_map_size(map); - pos = term_to_int(post); - if (pos >= size) { + avm_int_t requested_pos = term_to_int(post); + if (UNLIKELY(requested_pos < 0 || requested_pos > size)) { + RAISE_ERROR(BADARG_ATOM); + } + if (requested_pos == size) { return NONE_ATOM; } + pos = (int) requested_pos; } else if (term_is_nil(post)) { return NONE_ATOM; } else { - pos = term_find_map_pos(map, term_get_list_head(post), ctx->global); - if (pos == TERM_MAP_NOT_FOUND) { - return NONE_ATOM; + term remaining_keys = post; + pos = TERM_MAP_NOT_FOUND; + while (term_is_nonempty_list(remaining_keys)) { + int key_pos = term_find_map_pos(map, term_get_list_head(remaining_keys), ctx->global); + if (UNLIKELY(key_pos == TERM_MAP_MEMORY_ALLOC_FAIL)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + if (UNLIKELY(key_pos == TERM_MAP_NOT_FOUND)) { + RAISE_ERROR(BADARG_ATOM); + } + if (pos == TERM_MAP_NOT_FOUND) { + pos = key_pos; + } + remaining_keys = term_get_list_tail(remaining_keys); + } + if (UNLIKELY(!term_is_nil(remaining_keys))) { + RAISE_ERROR(BADARG_ATOM); } } @@ -6832,6 +6858,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 7fe1ac3dbc..841efe7f30 100644 --- a/tests/libs/estdlib/test_maps.erl +++ b/tests/libs/estdlib/test_maps.erl @@ -34,13 +34,7 @@ test() -> ok = test_is_key(), ok = test_put(), ok = test_iterator(), - HasIterator2 = - case erlang:system_info(machine) of - "BEAM" -> - erlang:system_info(version) >= "14."; - "ATOM" -> - true - end, + HasIterator2 = has_iterator_2(), case HasIterator2 of true -> ok = test_iterator_2_undefined(), @@ -77,6 +71,16 @@ 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_filtermap(), + ok = test_intersect(), + ok = test_intersect_with(), + ok = test_groups_from_list(), + ok = test_is_iterator_valid(), ok. test_get() -> @@ -118,6 +122,13 @@ test_iterator() -> EmptyIterator = maps:iterator(EmptyMap), none = maps:next(EmptyIterator), + %% Key-value iterator triples are iterators too. + ?ASSERT_EQUALS(maps:next(id({a, 1, none})), {a, 1, none}), + %% Invalid positions and malformed ordered iterators must not reach map storage. + ?ASSERT_ERROR(maps:next([-1 | #{a => 1}]), badarg), + ?ASSERT_ERROR(maps:next([2 | #{a => 1}]), badarg), + ?ASSERT_ERROR(maps:next([[a | malformed] | #{a => 1}]), badarg), + ok. test_iterator_2_undefined() -> @@ -200,7 +211,14 @@ test_values() -> test_to_list() -> ?ASSERT_MATCH(maps:to_list(maps:new()), []), ?ASSERT_MATCH(lists:sort(maps:to_list(#{a => 1, b => 2, c => 3})), [{a, 1}, {b, 2}, {c, 3}]), + ?ASSERT_EQUALS(maps:to_list(id(none)), []), + ?ASSERT_EQUALS(maps:to_list(id({a, 1, {b, 2, none}})), [{a, 1}, {b, 2}]), + ok = maybe_with_ordered_iterator(fun() -> + OrderedIter = maps:iterator(#{b => 2, a => 1, c => 3}, ordered), + ?ASSERT_EQUALS(maps:to_list(OrderedIter), [{a, 1}, {b, 2}, {c, 3}]) + end), ok = check_bad_map(fun() -> maps:to_list(id(not_a_map)) end), + ok = check_bad_map(fun() -> maps:to_list(id([-1 | #{a => 1}])) end), ok. test_from_list() -> @@ -230,17 +248,59 @@ test_filter() -> Filter = fun(_Key, Value) -> Value rem 2 == 0 end, ?ASSERT_EQUALS(maps:filter(Filter, maps:new()), #{}), ?ASSERT_EQUALS(maps:filter(Filter, #{a => 1, b => 2, c => 3}), #{b => 2}), + ?ASSERT_EQUALS( + maps:filter(fun(_K, _V) -> true end, id({a, 1, {b, 2, none}})), + #{a => 1, b => 2} + ), + ok = maybe_with_ordered_iterator(fun() -> + Self = self(), + OrderedIter = maps:iterator(#{b => 2, a => 1, c => 3}, ordered), + ?ASSERT_EQUALS( + maps:filter( + fun(K, V) -> + Self ! {Self, filter_order, K}, + V > 1 + end, + OrderedIter + ), + #{b => 2, c => 3} + ), + ?ASSERT_EQUALS(collect_order(filter_order, []), [a, b, c]) + end), ok = check_bad_map(fun() -> maps:filter(Filter, id(not_a_map)) end), - ok = check_bad_map_or_badarg(fun() -> maps:filter(not_a_function, id(not_a_map)) end), + ok = check_bad_map(fun() -> maps:filter(Filter, id([-1 | #{a => 1}])) end), + ?ASSERT_ERROR(maps:filter(not_a_function, id(not_a_map)), badarg), ?ASSERT_ERROR(maps:filter(not_a_function, maps:new()), badarg), + ?ASSERT_ERROR( + maps:filter(fun(_K, _V) -> unexpected_return end, #{a => 1}), + {case_clause, unexpected_return} + ), ok. test_fold() -> Fun = fun(_Key, Value, Sum) -> Sum + Value end, ?ASSERT_EQUALS(maps:fold(Fun, 0, maps:new()), 0), ?ASSERT_EQUALS(maps:fold(Fun, 0, #{a => 1, b => 2, c => 3}), 6), + ?ASSERT_EQUALS(maps:fold(Fun, 0, id({a, 1, {b, 2, none}})), 3), + ok = maybe_with_ordered_iterator(fun() -> + Self = self(), + OrderedIter = maps:iterator(#{b => 2, a => 1, c => 3}, ordered), + ?ASSERT_EQUALS( + maps:fold( + fun(K, V, Sum) -> + Self ! {Self, fold_order, K}, + Sum + V + end, + 0, + OrderedIter + ), + 6 + ), + ?ASSERT_EQUALS(collect_order(fold_order, []), [a, b, c]) + end), ok = check_bad_map(fun() -> maps:fold(Fun, any, id(not_a_map)) end), - ok = check_bad_map_or_badarg(fun() -> maps:fold(not_a_function, any, id(not_a_map)) end), + ok = check_bad_map(fun() -> maps:fold(Fun, 0, id([-1 | #{a => 1}])) end), + ?ASSERT_ERROR(maps:fold(not_a_function, any, id(not_a_map)), badarg), ?ASSERT_ERROR(maps:fold(not_a_function, any, maps:new()), badarg), ok. @@ -272,8 +332,21 @@ test_foreach() -> end, ok = maps:foreach(Fun, #{a => 1, b => 2, c => 3}), ?ASSERT_EQUALS(lists:sort(collect_foreach([])), [{a, 1}, {b, 2}, {c, 3}]), + ok = maps:foreach(Fun, id({a, 1, {b, 2, none}})), + ?ASSERT_EQUALS(lists:sort(collect_foreach([])), [{a, 1}, {b, 2}]), + ok = maybe_with_ordered_iterator(fun() -> + OrderedIter = maps:iterator(#{b => 2, a => 1, c => 3}, ordered), + ok = maps:foreach( + fun(K, _V) -> + Self ! {Self, foreach_order, K} + end, + OrderedIter + ), + ?ASSERT_EQUALS(collect_order(foreach_order, []), [a, b, c]) + end), ok = check_bad_map(fun() -> maps:foreach(Fun, id(not_a_map)) end), - ok = check_bad_map_or_badarg(fun() -> maps:foreach(not_a_function, id(not_a_map)) end), + ok = check_bad_map(fun() -> maps:foreach(Fun, id([-1 | #{a => 1}])) end), + ?ASSERT_ERROR(maps:foreach(not_a_function, id(not_a_map)), badarg), ?ASSERT_ERROR(maps:foreach(not_a_function, maps:new()), badarg), ok; true -> @@ -284,8 +357,25 @@ test_map() -> Fun = fun(_Key, Value) -> 2 * Value end, ?ASSERT_EQUALS(maps:map(Fun, maps:new()), #{}), ?ASSERT_EQUALS(maps:map(Fun, #{a => 1, b => 2, c => 3}), #{a => 2, b => 4, c => 6}), + ?ASSERT_EQUALS(maps:map(Fun, id({a, 1, {b, 2, none}})), #{a => 2, b => 4}), + ok = maybe_with_ordered_iterator(fun() -> + Self = self(), + OrderedIter = maps:iterator(#{b => 2, a => 1, c => 3}, ordered), + ?ASSERT_EQUALS( + maps:map( + fun(K, V) -> + Self ! {Self, map_order, K}, + V * 2 + end, + OrderedIter + ), + #{a => 2, b => 4, c => 6} + ), + ?ASSERT_EQUALS(collect_order(map_order, []), [a, b, c]) + end), ok = check_bad_map(fun() -> maps:map(Fun, id(not_a_map)) end), - ok = check_bad_map_or_badarg(fun() -> maps:map(not_a_function, id(not_a_map)) end), + ok = check_bad_map(fun() -> maps:map(Fun, id([-1 | #{a => 1}])) end), + ?ASSERT_ERROR(maps:map(not_a_function, id(not_a_map)), badarg), ?ASSERT_ERROR(maps:map(not_a_function, maps:new()), badarg), ok. @@ -346,6 +436,12 @@ test_remove() -> ?ASSERT_EQUALS(maps:remove(b, #{a => 1, b => 2, c => 3}), #{a => 1, c => 3}), ?ASSERT_EQUALS(maps:remove(c, #{a => 1, b => 2, c => 3}), #{a => 1, b => 2}), ?ASSERT_EQUALS(maps:remove(d, #{a => 1, b => 2, c => 3}), #{a => 1, b => 2, c => 3}), + ok = maybe_with_atomvm_ordered_iterator(fun() -> + OrderedIter = maps:iterator(#{b => 2, a => 1, c => 3}, ordered), + ?ASSERT_EQUALS(maps:remove(b, OrderedIter), #{a => 1, c => 3}), + ?ASSERT_EQUALS(maps:remove(b, id({a, 1, {b, 2, none}})), #{a => 1}), + ok = check_bad_map(fun() -> maps:remove(a, id([-1 | #{a => 1}])) end) + end), ok = check_bad_map(fun() -> maps:remove(foo, id(not_a_map)) end), ok. @@ -402,6 +498,324 @@ 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), + %% 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() -> + 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. + +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} + ), + %% Key-value iterator triples, including makeshift chains, are valid iterators. + ?ASSERT_EQUALS( + maps:filtermap(fun(_K, _V) -> true end, id({a, 1, {b, 2, none}})), + #{a => 1, b => 2} + ), + %% Ordered iterators keep the requested callback order + ok = maybe_with_ordered_iterator(fun() -> + Self = self(), + OrderedIter = maps:iterator(#{b => 2, a => 1, c => 3}, ordered), + ?ASSERT_EQUALS( + maps:filtermap( + fun(K, V) -> + Self ! {Self, filtermap_order, K}, + {true, V} + end, + OrderedIter + ), + #{a => 1, b => 2, c => 3} + ), + ?ASSERT_EQUALS(collect_order(filtermap_order, []), [a, b, c]) + end), + %% Error cases + ok = check_bad_map(fun() -> maps:filtermap(fun(_K, _V) -> true end, id(not_a_map)) end), + ok = check_bad_map(fun() -> + maps:filtermap(fun(_K, _V) -> true end, id([-1 | #{a => 1}])) + end), + ?ASSERT_ERROR(maps:filtermap(not_a_function, maps:new()), badarg), + ?ASSERT_ERROR(maps:filtermap(not_a_function, id(not_a_map)), badarg), + ?ASSERT_ERROR( + maps:filtermap(fun(_K, _V) -> unexpected_return end, #{a => 1}), + {case_clause, unexpected_return} + ), + 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), + ?ASSERT_ERROR(maps:intersect_with(not_a_function, id(not_a_map), #{}), badarg), + ?ASSERT_ERROR(maps:intersect_with(not_a_function, #{}, id(not_a_map)), 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 iterators + ok = maybe_with_ordered_iterator(fun() -> + ?ASSERT_EQUALS(maps:is_iterator_valid(maps:iterator(#{}, ordered)), true), + ?ASSERT_EQUALS(maps:is_iterator_valid(maps:iterator(#{a => 1, b => 2}, ordered)), true), + ?ASSERT_EQUALS(maps:is_iterator_valid(maps:iterator(#{}, reversed)), true), + ?ASSERT_EQUALS(maps:is_iterator_valid(maps:iterator(#{a => 1, b => 2}, reversed)), true), + ?ASSERT_EQUALS(maps:is_iterator_valid(maps:iterator(#{}, fun erlang:'=<'/2)), true), + ?ASSERT_EQUALS( + maps:is_iterator_valid(maps:iterator(#{a => 1, b => 2}, fun erlang:'=<'/2)), true + ) + end), + %% Makeshift iterators matching maps:next/1 return values + ?ASSERT_EQUALS(maps:is_iterator_valid({a, 1, none}), true), + ?ASSERT_EQUALS(maps:is_iterator_valid({a, 1, {b, 2, none}}), true), + ?ASSERT_EQUALS(maps:is_iterator_valid({a, 1, maps:iterator(#{b => 2})}), 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(1.0), false), + ?ASSERT_EQUALS(maps:is_iterator_valid([]), false), + ?ASSERT_EQUALS(maps:is_iterator_valid(<<"not an iterator">>), false), + ?ASSERT_EQUALS(maps:is_iterator_valid(fun() -> ok end), false), + ?ASSERT_EQUALS(maps:is_iterator_valid(#{}), false), + ?ASSERT_EQUALS(maps:is_iterator_valid({}), false), + ?ASSERT_EQUALS(maps:is_iterator_valid({a}), false), + ?ASSERT_EQUALS(maps:is_iterator_valid({a, b}), false), + ?ASSERT_EQUALS(maps:is_iterator_valid({a, b, c, d}), false), + ?ASSERT_EQUALS(maps:is_iterator_valid({a, b, c}), false), + ?ASSERT_EQUALS(maps:is_iterator_valid({a, b, {c, d, e}}), false), + ?ASSERT_EQUALS(maps:is_iterator_valid([not_int | #{}]), false), + %% Default iterator positions must be within the map, and never negative. + ?ASSERT_EQUALS(maps:is_iterator_valid([-1 | #{a => 1}]), false), + ?ASSERT_EQUALS(maps:is_iterator_valid([2 | #{a => 1}]), false), + %% Validation must inspect the complete ordered-key list, not only its head. + ?ASSERT_EQUALS(maps:is_iterator_valid([[a | malformed] | #{a => 1}]), false), + ?ASSERT_EQUALS(maps:is_iterator_valid([[a, missing] | #{a => 1}]), false), + ok. + +collect_order(Tag, Acc) -> + Self = self(), + receive + {Self, Tag, Key} -> + collect_order(Tag, [Key | Acc]) + after 0 -> lists:reverse(Acc) + end. + +maybe_with_ordered_iterator(Fun) -> + case has_iterator_2() of + true -> Fun(); + false -> ok + end. + +maybe_with_atomvm_ordered_iterator(Fun) -> + case erlang:system_info(machine) of + "ATOM" -> Fun(); + "BEAM" -> ok + end. + +has_iterator_2() -> + case erlang:system_info(machine) of + "BEAM" -> + erlang:system_info(version) >= "14."; + "ATOM" -> + true + end. id(X) -> X. @@ -413,20 +827,6 @@ check_bad_map(F) -> error:{badmap, _} -> ok end. -check_bad_map_or_badarg(F) -> - BadargFirst = - case erlang:system_info(machine) of - "BEAM" -> erlang:system_info(version) >= "12."; - "ATOM" -> false - end, - try - F(), - fail - catch - error:{badmap, _} when not BadargFirst -> ok; - error:badarg when BadargFirst -> ok - end. - check_bad_key(F, _Key) -> try F(),