From 4129e608cac36fb2df336b8053558ec8e0022cfa Mon Sep 17 00:00:00 2001 From: Peter M Date: Wed, 27 May 2026 22:25:04 +0200 Subject: [PATCH 1/4] 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/4] 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/4] 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 From 00b26a279d11bb5aedba7f811bfa3eb034c20193 Mon Sep 17 00:00:00 2001 From: Peter M Date: Fri, 10 Jul 2026 15:09:46 +0200 Subject: [PATCH 4/4] maps: align helpers with OTP behavior Align map helper error precedence and iterator handling with current OTP semantics, including ordered and key-value iterator forms. Harden maps:next/1 against invalid numeric positions and malformed ordered iterators, and expand estdlib regression coverage for callback order, iterator validation, and badarg/badmap behavior. Signed-off-by: Peter M --- CHANGELOG.md | 7 +- libs/estdlib/src/maps.erl | 197 ++++++++++++++++------------ src/libAtomVM/nifs.c | 31 ++++- tests/libs/estdlib/test_maps.erl | 215 +++++++++++++++++++++++++++---- 4 files changed, 329 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c429575ef8..eb43a93cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,9 +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 `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 +- 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 36e12fab09..9557d5222b 100644 --- a/libs/estdlib/src/maps.erl +++ b/libs/estdlib/src/maps.erl @@ -137,21 +137,18 @@ is_key(Key, Map) -> erlang:is_map_key(Key, Map). %%----------------------------------------------------------------------------- -%% @param Iterator the iterator to validate -%% @returns `true' if the iterator is valid, `false' otherwise -%% @doc Check if an iterator is valid. +%% @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 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. +%% 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(Iterator :: iterator()) -> boolean(). -is_iterator_valid(Iterator) -> +-spec is_iterator_valid(MaybeIterator :: iterator() | term()) -> boolean(). +is_iterator_valid(MaybeIterator) -> try - is_iterator_valid_1(Iterator) + is_iterator_valid_1(MaybeIterator) catch error:badarg -> false end. @@ -270,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 @@ -348,13 +348,14 @@ 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). @@ -380,13 +381,14 @@ filter(_Pred, _Map) -> 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}); + 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). @@ -411,13 +413,14 @@ filtermap(_Fun, _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). @@ -438,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). @@ -553,8 +557,9 @@ intersect(_Map1, Map2) when not is_map(Map2) -> %% `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. +%% 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( @@ -572,9 +577,13 @@ intersect_with(Combiner, Map1, Map2) when 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) -> +intersect_with(Combiner, Map1, _Map2) when + is_function(Combiner, 3) andalso not is_map(Map1) +-> error({badmap, Map1}); -intersect_with(_Combiner, _Map1, Map2) when not is_map(Map2) -> +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). @@ -592,13 +601,14 @@ intersect_with(_Combiner, _Map1, _Map2) -> -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). @@ -662,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 @@ -829,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) -> @@ -888,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) -> @@ -904,9 +917,9 @@ iterate_from_list(_List, _Accum) -> error(badarg). %% @private -iterate_filtermap(_Fun, none, Accum) -> +iterate_filtermap(_Fun, none, Accum, _ErrorTag) -> lists:reverse(Accum); -iterate_filtermap(Fun, {Key, Value, Iterator}, Accum) -> +iterate_filtermap(Fun, {Key, Value, Iterator}, Accum, ErrorTag) -> NewAccum = case Fun(Key, Value) of true -> @@ -916,7 +929,7 @@ iterate_filtermap(Fun, {Key, Value, Iterator}, Accum) -> false -> Accum end, - iterate_filtermap(Fun, maps:next(Iterator), NewAccum). + iterate_filtermap(Fun, try_next(Iterator, ErrorTag), NewAccum, ErrorTag). %% @private groups_from_list_1(_KeyFun, _ValueFun, [], Acc) -> @@ -951,6 +964,20 @@ intersect_with_iterate({K, V1, Iterator}, Keep, BigMap, Combiner) -> 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; diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index b726a0c3df..b7ab44d1ef 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -6786,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); @@ -6799,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); } } diff --git a/tests/libs/estdlib/test_maps.erl b/tests/libs/estdlib/test_maps.erl index 6d38f70808..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(), @@ -128,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() -> @@ -210,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() -> @@ -240,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. @@ -282,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 -> @@ -294,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. @@ -356,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. @@ -532,9 +618,38 @@ test_filtermap() -> 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() -> @@ -592,6 +707,8 @@ test_intersect_with() -> 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() -> @@ -634,16 +751,72 @@ test_is_iterator_valid() -> %% 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), + %% 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. check_bad_map(F) -> @@ -654,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(),