From c013d3ad41bd4d44ef5467f8ad2f09a3901f5126 Mon Sep 17 00:00:00 2001 From: Sverker Eriksson Date: Fri, 16 Dec 2022 00:08:57 +0100 Subject: erts: Optimize flatmap internal ordering of atoms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The internal ordering of flatmap keys are changed to let atoms be ordered according to atom index. This improves lookup of smalll maps with atom keys as it avoids memory access of the atom table entries. This internal flatmap key order is implemented by new function erts_cmp_flatmap_keys(). Only top level atoms are affected by the new order. The ordering of other term types is unchanged. The two documented term orders are of course also unchanged; arithmetic and "map-key" order. This commit: * Add and use erts_cmp_flatmap_keys(). * Adjust term_to_binary(_, [deterministic]) for flatmaps. * Reimplement flatmap comparison (Map1 < Map2) to be correct, but more complex and expensive as it cannot rely on atoms to be "map-key" ordered. * Remove erts_map_from_sorted_ks_and_vs() and replace all uses with erts_map_from_ks_and_vs() as we cannot assume atom keys sort order in compile time. ToDo: * Optimize erts_cmp_compound() to not be recursive on C stack for flatmaps. It's unusual with deep maps-in-maps (I guess) but we have limited C stack and should avoid possibilities for Erlang code to exhaust it. * Comment and explain the flatmap comparison algorithm. Upcoming PR-6718 will add features to * print maps sorted (~k). * iterate over maps in a ordered way. Credit to José Valim for the initial idea, commit and benchmarks in https://github.com/erlang/otp/pull/6151. --- erts/emulator/beam/beam_common.c | 2 +- erts/emulator/beam/beam_transform_helpers.c | 2 +- erts/emulator/beam/dist.c | 9 +- erts/emulator/beam/erl_map.c | 27 +--- erts/emulator/beam/erl_map.h | 2 - erts/emulator/beam/erl_utils.h | 12 ++ erts/emulator/beam/external.c | 18 ++- erts/emulator/beam/utils.c | 213 ++++++++++++++++++++++++---- erts/emulator/test/map_SUITE.erl | 4 + lib/kernel/test/logger_formatter_SUITE.erl | 2 +- lib/stdlib/test/io_SUITE.erl | 11 +- 11 files changed, 233 insertions(+), 69 deletions(-) diff --git a/erts/emulator/beam/beam_common.c b/erts/emulator/beam/beam_common.c index 6194644869..4fb7e55c85 100644 --- a/erts/emulator/beam/beam_common.c +++ b/erts/emulator/beam/beam_common.c @@ -2174,7 +2174,7 @@ erts_gc_update_map_assoc(Process* p, Eterm* reg, Uint live, ASSERT(kp < (Eterm *)mp); key = *old_keys; - if ((c = (key == new_key) ? 0 : CMP_TERM(key, new_key)) < 0) { + if ((c = (key == new_key) ? 0 : erts_cmp_flatmap_keys(key, new_key)) < 0) { /* Copy old key and value */ *kp++ = key; *hp++ = *old_vals; diff --git a/erts/emulator/beam/beam_transform_helpers.c b/erts/emulator/beam/beam_transform_helpers.c index 3a4ed5cd0b..dcc3a2fdc0 100644 --- a/erts/emulator/beam/beam_transform_helpers.c +++ b/erts/emulator/beam/beam_transform_helpers.c @@ -150,7 +150,7 @@ oparg_compare(BeamOpArg* a, BeamOpArg* b) static int oparg_term_compare(SortBeamOpArg* a, SortBeamOpArg* b) { - Sint res = CMP_TERM(a->term, b->term); + Sint res = erts_cmp_flatmap_keys(a->term, b->term); if (res < 0) { return -1; diff --git a/erts/emulator/beam/dist.c b/erts/emulator/beam/dist.c index c35b8ef08b..3813998dd7 100644 --- a/erts/emulator/beam/dist.c +++ b/erts/emulator/beam/dist.c @@ -6054,7 +6054,7 @@ nodes(Process *c_p, Eterm node_types, Eterm options) } } else { - Eterm ks[2], *hp, keys_tuple = THE_NON_VALUE; + Eterm ks[2], *hp; Uint map_size = 0, el_xtra, xtra; ErtsHeapFactory hfact; @@ -6089,8 +6089,8 @@ nodes(Process *c_p, Eterm node_types, Eterm options) vs[map_size++] = eni->type; } - info_map = erts_map_from_sorted_ks_and_vs(&hfact, ks, vs, - map_size, &keys_tuple); + info_map = erts_map_from_ks_and_vs(&hfact, ks, vs, map_size); + ASSERT(is_value(info_map)); hp = erts_produce_heap(&hfact, 3+2, xtra); @@ -6676,8 +6676,7 @@ send_nodes_mon_msgs(Process *c_p, Eterm what, Eterm node, map_size++; } - info = erts_map_from_sorted_ks_and_vs(&hfact, ks, vs, - map_size, NULL); + info = erts_map_from_ks_and_vs(&hfact, ks, vs, map_size); ASSERT(is_value(info)); } else { /* Info list */ diff --git a/erts/emulator/beam/erl_map.c b/erts/emulator/beam/erl_map.c index 8cdc3353d0..09c47e57df 100644 --- a/erts/emulator/beam/erl_map.c +++ b/erts/emulator/beam/erl_map.c @@ -438,7 +438,9 @@ static Eterm flatmap_from_validated_list(Process *p, Eterm list, Eterm fill_valu idx = size; - while(idx > 0 && (c = CMP_TERM(key,ks[idx-1])) < 0) { idx--; } + while(idx > 0 && (c = erts_cmp_flatmap_keys(key,ks[idx-1])) < 0) { + idx--; + } if (c == 0) { /* last compare was equal, @@ -748,21 +750,6 @@ Eterm erts_map_from_ks_and_vs(ErtsHeapFactory *factory, Eterm *ks, Eterm *vs, Ui return res; } -Eterm erts_map_from_sorted_ks_and_vs(ErtsHeapFactory *factory, Eterm *ks, Eterm *vs, - Uint n, Eterm *key_tuple) -{ -#ifdef DEBUG - Uint i; - /* verify that key array contains unique and sorted keys... */ - for (i = 1; i < n; i++) { - ASSERT(CMP_TERM(ks[i-1], ks[i]) < 0); - } -#endif - - return from_ks_and_vs(factory, ks, vs, n, key_tuple, NULL); -} - - Eterm erts_hashmap_from_ks_and_vs_extra(ErtsHeapFactory *factory, Eterm *ks, Eterm *vs, Uint n, Eterm key, Eterm value) { @@ -1383,7 +1370,7 @@ static Eterm flatmap_merge(Process *p, Eterm nodeA, Eterm nodeB) { vs2 = flatmap_get_values(mp2); while(i1 < n1 && i2 < n2) { - c = (ks1[i1] == ks2[i2]) ? 0 : CMP_TERM(ks1[i1],ks2[i2]); + c = (ks1[i1] == ks2[i2]) ? 0 : erts_cmp_flatmap_keys(ks1[i1],ks2[i2]); if (c == 0) { /* use righthand side arguments map value, * but advance both maps */ @@ -2161,7 +2148,7 @@ Eterm erts_maps_put(Process *p, Eterm key, Eterm value, Eterm map) { ASSERT(n >= 0); /* copy map in order */ - while (n && ((c = CMP_TERM(*ks, key)) < 0)) { + while (n && ((c = erts_cmp_flatmap_keys(*ks, key)) < 0)) { *shp++ = *ks++; *hp++ = *vs++; n--; @@ -3059,7 +3046,7 @@ int erts_validate_and_sort_flatmap(flatmap_t* mp) for (ix = 1; ix < sz; ix++) { jx = ix; - while( jx > 0 && (c = CMP_TERM(ks[jx],ks[jx-1])) <= 0 ) { + while( jx > 0 && (c = erts_cmp_flatmap_keys(ks[jx],ks[jx-1])) <= 0 ) { /* identical key -> error */ if (c == 0) return 0; @@ -3090,7 +3077,7 @@ void erts_usort_flatmap(flatmap_t* mp) for (ix = 1; ix < sz; ix++) { jx = ix; - while( jx > 0 && (c = CMP_TERM(ks[jx],ks[jx-1])) <= 0 ) { + while( jx > 0 && (c = erts_cmp_flatmap_keys(ks[jx],ks[jx-1])) <= 0 ) { /* identical key -> remove it */ if (c == 0) { sys_memmove(ks+jx-1,ks+jx,(sz-ix)*sizeof(Eterm)); diff --git a/erts/emulator/beam/erl_map.h b/erts/emulator/beam/erl_map.h index 3430ec6d7a..d3a023bc07 100644 --- a/erts/emulator/beam/erl_map.h +++ b/erts/emulator/beam/erl_map.h @@ -106,8 +106,6 @@ Eterm erts_hashmap_from_array(ErtsHeapFactory*, Eterm *leafs, Uint n, int rejec erts_hashmap_from_ks_and_vs_extra((F), (KS), (VS), (N), THE_NON_VALUE, THE_NON_VALUE); Eterm erts_map_from_ks_and_vs(ErtsHeapFactory *factory, Eterm *ks, Eterm *vs, Uint n); -Eterm erts_map_from_sorted_ks_and_vs(ErtsHeapFactory *factory, Eterm *ks0, Eterm *vs0, - Uint n, Eterm *key_tuple); Eterm erts_hashmap_from_ks_and_vs_extra(ErtsHeapFactory *factory, Eterm *ks, Eterm *vs, Uint n, Eterm k, Eterm v); diff --git a/erts/emulator/beam/erl_utils.h b/erts/emulator/beam/erl_utils.h index 2142e8d757..dce3665a8d 100644 --- a/erts/emulator/beam/erl_utils.h +++ b/erts/emulator/beam/erl_utils.h @@ -112,6 +112,7 @@ int eq(Eterm, Eterm); ERTS_GLB_INLINE Sint erts_cmp(Eterm, Eterm, int, int); ERTS_GLB_INLINE int erts_cmp_atoms(Eterm a, Eterm b); +ERTS_GLB_INLINE Sint erts_cmp_flatmap_keys(Eterm, Eterm); Sint erts_cmp_compound(Eterm, Eterm, int, int); @@ -231,6 +232,17 @@ ERTS_GLB_INLINE Sint erts_cmp(Eterm a, Eterm b, int exact, int eq_only) { return erts_cmp_compound(a,b,exact,eq_only); } +/* + * Only to be used for the *internal* sort order of flatmap keys. + */ +ERTS_GLB_INLINE Sint erts_cmp_flatmap_keys(Eterm key_a, Eterm key_b) { + if (is_atom(key_a) && is_atom(key_b)) { + return key_a - key_b; + } + return erts_cmp(key_a, key_b, 1, 0); +} + + #endif /* ERTS_GLB_INLINE_INCL_FUNC_DEF */ #endif diff --git a/erts/emulator/beam/external.c b/erts/emulator/beam/external.c index d2bbaf607a..6abd68aeb7 100644 --- a/erts/emulator/beam/external.c +++ b/erts/emulator/beam/external.c @@ -3197,7 +3197,6 @@ enc_term_int(TTBEncodeContext* ctx, ErtsAtomCacheMap *acmp, Eterm obj, byte* ep, long num_reductions = r; n = next_map_element - map_array; - ASSERT(n > MAP_SMALL_MAP_LIMIT); if (ctx == NULL) { /* No context means that the external representation of term * being encoded will fit in a heap binary (64 bytes). This @@ -3468,9 +3467,20 @@ enc_term_int(TTBEncodeContext* ctx, ErtsAtomCacheMap *acmp, Eterm obj, byte* ep, Eterm *kptr = flatmap_get_keys(mp); Eterm *vptr = flatmap_get_values(mp); - WSTACK_PUSH4(s, (UWord)kptr, (UWord)vptr, - ENC_MAP_PAIR, size); - } + if (dflags & DFLAG_DETERMINISTIC) { + ASSERT(map_array == NULL); + next_map_element = map_array = alloc_map_array(size); + while (size--) { + *next_map_element++ = *kptr++; + *next_map_element++ = *vptr++; + } + WSTACK_PUSH2(s, ENC_START_SORTING_MAP, THE_NON_VALUE); + } + else { + WSTACK_PUSH4(s, (UWord)kptr, (UWord)vptr, + ENC_MAP_PAIR, size); + } + } } else { Eterm hdr; Uint node_sz; diff --git a/erts/emulator/beam/utils.c b/erts/emulator/beam/utils.c index 2d1cca9c41..1b4e1ca385 100644 --- a/erts/emulator/beam/utils.c +++ b/erts/emulator/beam/utils.c @@ -108,9 +108,11 @@ void *ycf_debug_get_stack_start(void) { #endif #if defined(DEBUG) +# define IF_DEBUG(X) X # define DBG_RANDOM_REDS(REDS, SEED) \ ((REDS) * 0.01 * erts_sched_local_random_float(SEED)) #else +# define IF_DEBUG(X) # define DBG_RANDOM_REDS(REDS, SEED) (REDS) #endif @@ -1500,6 +1502,88 @@ not_equal: return 0; } +static +Sint compare_flatmap_atom_keys(const Eterm* a_keys, + const Eterm* b_keys, + int nkeys) +{ + Eterm min_key = THE_NON_VALUE; + Eterm a, b; + int ai, bi; + Sint res; + + ASSERT(nkeys > 0); + ai = nkeys; + while (*a_keys == *b_keys) { + ASSERT(is_atom(*a_keys)); + a_keys++; + b_keys++; + if (--ai == 0) + return 0; + } + + /* Found atom key diff. Find the smallest atom. */ + bi = ai; + a = *a_keys; + b = *b_keys; + IF_DEBUG(res = 0); + do { + ASSERT((ai && is_atom(a)) || (!ai && a == ERTS_UINT_MAX)); + ASSERT((bi && is_atom(b)) || (!bi && b == ERTS_UINT_MAX)); + + if (a < b) { + ASSERT(ai && is_atom(a)); + if (is_non_value(min_key) || erts_cmp_atoms(a, min_key) < 0) { + min_key = a; + res = -1; + } + a = --ai ? *(++a_keys) : ERTS_UINT_MAX; + } + else if (a > b) { + ASSERT(bi && is_atom(b)); + if (is_non_value(min_key) || erts_cmp_atoms(b, min_key) < 0) { + min_key = b; + res = 1; + } + b = --bi ? *(++b_keys) : ERTS_UINT_MAX; + } + else { + ASSERT(ai && bi && is_atom(a) && is_atom(b)); + a = --ai ? *(++a_keys) : ERTS_UINT_MAX; + b = --bi ? *(++b_keys) : ERTS_UINT_MAX; + } + } while (ai|bi); + ASSERT(is_atom(min_key)); + ASSERT(res != 0); + return res; +} + +static +Sint compare_flatmap_atom_key_values(const Eterm* keys, + const Eterm* a_vals, + const Eterm* b_vals, + int nkeys, + int exact) +{ + Eterm min_key = THE_NON_VALUE; + Sint res = 0; + + ASSERT(nkeys > 0); + do { + ASSERT(is_atom(*keys)); + if (is_non_value(min_key) || erts_cmp_atoms(*keys, min_key) < 0) { + Sint valcmp = erts_cmp(*a_vals, *b_vals, exact, 0); + if (valcmp) { + min_key = *keys; + res = valcmp; + } + } + keys++; + a_vals++; + b_vals++; + } while (--nkeys); + return res; +} /* @@ -1556,10 +1640,13 @@ Sint erts_cmp_compound(Eterm a, Eterm b, int exact, int eq_only) #define HASHMAP_PHASE2_ARE_KEYS_EQUAL 5 #define HASHMAP_PHASE2_IS_MIN_KEY_A 6 #define HASHMAP_PHASE2_IS_MIN_KEY_B 7 - +#define FLATMAP_ATOM_KEYS_OP 8 +#define FLATMAP_ATOM_VALUES_OP 9 #define OP_WORD(OP) (((OP) << _TAG_PRIMARY_SIZE) | TAG_PRIMARY_HEADER) -#define TERM_ARRAY_OP_WORD(SZ) OP_WORD(((SZ) << OP_BITS) | TERM_ARRAY_OP) +#define TERM_ARRAY_OP_WORD(SZ) OP_WORD(((SZ) << OP_BITS) | TERM_ARRAY_OP) +#define FLATMAP_ATOM_KEYS_OP_WORD(SZ) OP_WORD(((SZ) << OP_BITS) | FLATMAP_ATOM_KEYS_OP) +#define FLATMAP_ATOM_VALUES_OP_WORD(SZ) OP_WORD(((SZ) << OP_BITS) | FLATMAP_ATOM_VALUES_OP) #define GET_OP(WORD) (ASSERT(is_header(WORD)), ((WORD) >> _TAG_PRIMARY_SIZE) & OP_MASK) #define GET_OP_ARG(WORD) (ASSERT(is_header(WORD)), ((WORD) >> (OP_BITS + _TAG_PRIMARY_SIZE))) @@ -1735,50 +1822,99 @@ tailrecur_ne: { struct erts_cmp_hashmap_state* sp; if (is_flatmap_header(ahdr)) { + flatmap_t* afm = (flatmap_t*)flatmap_val(a); + flatmap_t* bfm; if (!is_flatmap(b)) { if (is_hashmap(b)) { - aa = (Eterm *)flatmap_val(a); - i = flatmap_get_size((flatmap_t*)aa) - hashmap_size(b); - ASSERT(i != 0); - RETURN_NEQ(i); + ASSERT(flatmap_get_size(afm) < hashmap_size(b)); + RETURN_NEQ(-1); } a_tag = MAP_DEF; goto mixed_types; } - aa = (Eterm *)flatmap_val(a); - bb = (Eterm *)flatmap_val(b); - - i = flatmap_get_size((flatmap_t*)aa); - if (i != flatmap_get_size((flatmap_t*)bb)) { - RETURN_NEQ((int)(i - flatmap_get_size((flatmap_t*)bb))); + bfm = (flatmap_t*)flatmap_val(b); + i = flatmap_get_size(afm); + if (i != flatmap_get_size(bfm)) { + RETURN_NEQ((int)(i - flatmap_get_size(bfm))); } if (i == 0) { goto pop_next; } - aa += 2; - bb += 2; if (exact) { + aa = &afm->keys; + bb = &bfm->keys; i += 1; /* increment for tuple-keys */ goto term_array; } else { - /* Value array */ - WSTACK_PUSH3(stack,(UWord)(bb+1),(UWord)(aa+1),TERM_ARRAY_OP_WORD(i)); - /* Switch back from 'exact' key compare */ - WSTACK_PUSH(stack,OP_WORD(SWITCH_EXACT_OFF_OP)); - /* Now do 'exact' compare of key tuples */ - a = *aa; - b = *bb; - exact = 1; - goto bodyrecur; + Eterm* a_keys = flatmap_get_keys(afm); + Eterm* b_keys = flatmap_get_keys(bfm); + Eterm* a_vals = flatmap_get_values(afm); + Eterm* b_vals = flatmap_get_values(bfm); + int n_numbers; /* sorted before atoms */ + int n_atoms; + int n_rest; /* sorted after atoms */ + int n = 0; + + while (n < i && !(is_atom(a_keys[n]) && + is_atom(b_keys[n]))) { + ++n; + } + n_numbers = n; + while (n < i && (is_atom(a_keys[n]) && + is_atom(b_keys[n]))) { + ++n; + } + n_atoms = n - n_numbers; + n_rest = i - n; + + ASSERT(n_numbers + n_atoms + n_rest == i); + ASSERT(n_atoms || !n_rest); + + if (n_rest) { + WSTACK_PUSH3(stack, + (UWord)&b_vals[n_numbers+n_atoms], + (UWord)&a_vals[n_numbers+n_atoms], + TERM_ARRAY_OP_WORD(n_rest)); + } + if (n_atoms) { + WSTACK_PUSH4(stack, + (UWord)&b_vals[n_numbers], + (UWord)&a_vals[n_numbers], + (UWord)&a_keys[n_numbers], + FLATMAP_ATOM_VALUES_OP_WORD(n_atoms)); + } + if (n_numbers) { + WSTACK_PUSH3(stack, (UWord)b_vals, (UWord)a_vals, + TERM_ARRAY_OP_WORD(n_numbers)); + } + if (!exact) { + WSTACK_PUSH(stack, OP_WORD(SWITCH_EXACT_OFF_OP)); + exact = 1; + } + if (n_rest) { + WSTACK_PUSH3(stack, + (UWord)&b_keys[n_numbers+n_atoms], + (UWord)&a_keys[n_numbers+n_atoms], + TERM_ARRAY_OP_WORD(n_rest)); + } + if (n_atoms) { + WSTACK_PUSH3(stack, + (UWord)&b_keys[n_numbers], + (UWord)&a_keys[n_numbers], + FLATMAP_ATOM_KEYS_OP_WORD(n_atoms)); + } + if (n_numbers) { + WSTACK_PUSH3(stack, (UWord)b_keys, (UWord)a_keys, + TERM_ARRAY_OP_WORD(n_numbers)); + } } + goto pop_next; } if (!is_hashmap(b)) { if (is_flatmap(b)) { - bb = (Eterm *)flatmap_val(b); - i = hashmap_size(a) - flatmap_get_size((flatmap_t*)bb); - ASSERT(i != 0); - RETURN_NEQ(i); + ASSERT(hashmap_size(a) > flatmap_get_size(flatmap_val(b))); + RETURN_NEQ(1); } a_tag = MAP_DEF; goto mixed_types; @@ -2343,6 +2479,29 @@ pop_next: sp->bp = hashmap_iterator_next(&b_stack); goto case_HASHMAP_PHASE2_LOOP; + case FLATMAP_ATOM_KEYS_OP: + i = GET_OP_ARG(something); + aa = (Eterm*) WSTACK_POP(stack); + bb = (Eterm*) WSTACK_POP(stack); + j = compare_flatmap_atom_keys(aa, bb, i); + if (j) + goto not_equal; + else + goto pop_next; + + case FLATMAP_ATOM_VALUES_OP: { + const Eterm* keys; + i = GET_OP_ARG(something); + keys = (const Eterm*) WSTACK_POP(stack); + aa = (Eterm*) WSTACK_POP(stack); + bb = (Eterm*) WSTACK_POP(stack); + j = compare_flatmap_atom_key_values(keys, aa, bb, i, exact); + if (j) + goto not_equal; + else + goto pop_next; + } + default: ASSERT(!"Invalid cmp op"); } /* switch */ diff --git a/erts/emulator/test/map_SUITE.erl b/erts/emulator/test/map_SUITE.erl index 7fb2961cad..628756b29d 100644 --- a/erts/emulator/test/map_SUITE.erl +++ b/erts/emulator/test/map_SUITE.erl @@ -2923,6 +2923,10 @@ t_erts_internal_order(_Config) when is_list(_Config) -> 0 = erts_internal:cmp_term(2147483648,2147483648), 1 = erts_internal:cmp_term(2147483648,0), + %% Make sure it's not the internal flatmap order + %% where low indexed 'true' < 'a'. + -1 = erts_internal:cmp_term(a,true), + M = #{0 => 0,2147483648 => 0}, true = M =:= binary_to_term(term_to_binary(M)), true = M =:= binary_to_term(term_to_binary(M, [deterministic])), diff --git a/lib/kernel/test/logger_formatter_SUITE.erl b/lib/kernel/test/logger_formatter_SUITE.erl index d160dce32f..bd91d92bbf 100644 --- a/lib/kernel/test/logger_formatter_SUITE.erl +++ b/lib/kernel/test/logger_formatter_SUITE.erl @@ -183,7 +183,7 @@ single_line(_Config) -> ct:log(String3), match = re:run(String3,"\\[1,2,3,4,5,6,7,8,9,10\\]",[{capture,none}]), match = re:run(String3, - "#{a => map,few => accociations,with => a}", + "#{((a => map|with => a|few => accociations)[,}]){3}", [{capture,none}]), %% This part is added to make sure that the previous test made diff --git a/lib/stdlib/test/io_SUITE.erl b/lib/stdlib/test/io_SUITE.erl index 17fd6d41fd..4882f220a3 100644 --- a/lib/stdlib/test/io_SUITE.erl +++ b/lib/stdlib/test/io_SUITE.erl @@ -2271,20 +2271,15 @@ format_string(_Config) -> ok. maps(_Config) -> - %% Note that order in which a map is printed is arbitrary. In - %% practice, small maps (non-HAMT) are printed in key order, but - %% the breakpoint for creating big maps (HAMT) is lower in the - %% debug-compiled run-time system than in the optimized run-time - %% system. - %% + %% Note that order in which a map is printed is arbitrary. %% Therefore, play it completely safe by not assuming any order %% in a map with more than one element. "#{}" = fmt("~w", [#{}]), "#{a => b}" = fmt("~w", [#{a=>b}]), - re_fmt(<<"#\\{(a => b),[.][.][.]\\}">>, + re_fmt(<<"#\\{(a => b|c => d),[.][.][.]\\}">>, "~W", [#{a => b,c => d},2]), - re_fmt(<<"#\\{(a => b),[.][.][.]\\}">>, + re_fmt(<<"#\\{(a => b|c => d|e => f),[.][.][.]\\}">>, "~W", [#{a => b,c => d,e => f},2]), "#{}" = fmt("~p", [#{}]), -- cgit v1.2.1