File 5616-Implement-map-comprehensions-in-the-debugger.patch of Package erlang
From 26266f722706164730a9f903aa05325a2c2f1063 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Gustavsson?= <bjorn@erlang.org>
Date: Thu, 30 Nov 2023 16:36:26 +0100
Subject: [PATCH] Implement map comprehensions in the debugger
Fixes #7914
---
lib/debugger/src/dbg_ieval.erl | 88 ++++++++--
lib/debugger/src/dbg_iload.erl | 42 +++--
lib/debugger/test/Makefile | 1 +
lib/debugger/test/mc_SUITE.erl | 294 +++++++++++++++++++++++++++++++++
4 files changed, 393 insertions(+), 32 deletions(-)
create mode 100644 lib/debugger/test/mc_SUITE.erl
diff --git a/lib/debugger/src/dbg_ieval.erl b/lib/debugger/src/dbg_ieval.erl
index c43d6d7331..bbd3587c00 100644
--- a/lib/debugger/src/dbg_ieval.erl
+++ b/lib/debugger/src/dbg_ieval.erl
@@ -1087,6 +1087,8 @@ expr({lc,_Line,E,Qs}, Bs, Ieval) ->
eval_lc(E, Qs, Bs, Ieval);
expr({bc,_Line,E,Qs}, Bs, Ieval) ->
eval_bc(E, Qs, Bs, Ieval);
+expr({mc,_Line,E,Qs}, Bs, Ieval) ->
+ eval_mc(E, Qs, Bs, Ieval);
%% Brutal exit on unknown expressions/clauses/values/etc.
expr(E, _Bs, _Ieval) ->
@@ -1107,16 +1109,9 @@ eval_named_fun(As, RF, {Info,Bs,Cs,FName}) ->
eval_lc(E, Qs, Bs, Ieval) ->
{value,eval_lc1(E, Qs, Bs, Ieval),Bs}.
-eval_lc1(E, [{generate,Line,P,L0}|Qs], Bs0, Ieval0) ->
- Ieval = Ieval0#ieval{line=Line},
- {value,L1,Bs1} = expr(L0, Bs0, Ieval#ieval{top=false}),
+eval_lc1(E, [{generator,G}|Qs], Bs, Ieval) ->
CompFun = fun(NewBs) -> eval_lc1(E, Qs, NewBs, Ieval) end,
- eval_generate(L1, P, Bs1, CompFun, Ieval);
-eval_lc1(E, [{b_generate,Line,P,L0}|Qs], Bs0, Ieval0) ->
- Ieval = Ieval0#ieval{line=Line},
- {value,Bin,_} = expr(L0, Bs0, Ieval#ieval{top=false}),
- CompFun = fun(NewBs) -> eval_lc1(E, Qs, NewBs, Ieval) end,
- eval_b_generate(Bin, P, Bs0, CompFun, Ieval);
+ eval_generator(G, Bs, CompFun, Ieval);
eval_lc1(E, [{guard,Q}|Qs], Bs0, Ieval) ->
case guard(Q, Bs0) of
true -> eval_lc1(E, Qs, Bs0, Ieval);
@@ -1140,16 +1135,9 @@ eval_bc(E, Qs, Bs, Ieval) ->
Val = erlang:list_to_bitstring(eval_bc1(E, Qs, Bs, Ieval)),
{value,Val,Bs}.
-eval_bc1(E, [{generate,Line,P,L0}|Qs], Bs0, Ieval0) ->
- Ieval = Ieval0#ieval{line=Line},
- {value,L1,Bs1} = expr(L0, Bs0, Ieval#ieval{top=false}),
+eval_bc1(E, [{generator,G}|Qs], Bs, Ieval) ->
CompFun = fun(NewBs) -> eval_bc1(E, Qs, NewBs, Ieval) end,
- eval_generate(L1, P, Bs1, CompFun, Ieval);
-eval_bc1(E, [{b_generate,Line,P,L0}|Qs], Bs0, Ieval0) ->
- Ieval = Ieval0#ieval{line=Line},
- {value,Bin,_} = expr(L0, Bs0, Ieval#ieval{top=false}),
- CompFun = fun(NewBs) -> eval_bc1(E, Qs, NewBs, Ieval) end,
- eval_b_generate(Bin, P, Bs0, CompFun, Ieval);
+ eval_generator(G, Bs, CompFun, Ieval);
eval_bc1(E, [{guard,Q}|Qs], Bs0, Ieval) ->
case guard(Q, Bs0) of
true -> eval_bc1(E, Qs, Bs0, Ieval);
@@ -1165,6 +1153,56 @@ eval_bc1(E, [], Bs, Ieval) ->
{value,V,_} = expr(E, Bs, Ieval#ieval{top=false}),
[V].
+eval_mc(E, Qs, Bs, Ieval) ->
+ Map = eval_mc1(E, Qs, Bs, Ieval),
+ {value,maps:from_list(Map),Bs}.
+
+eval_mc1(E, [{generator,G}|Qs], Bs, Ieval) ->
+ CompFun = fun(NewBs) -> eval_mc1(E, Qs, NewBs, Ieval) end,
+ eval_generator(G, Bs, CompFun, Ieval);
+eval_mc1(E, [{guard,Q}|Qs], Bs0, Ieval) ->
+ case guard(Q, Bs0) of
+ true -> eval_mc1(E, Qs, Bs0, Ieval);
+ false -> []
+ end;
+eval_mc1(E, [Q|Qs], Bs0, Ieval) ->
+ case expr(Q, Bs0, Ieval#ieval{top=false}) of
+ {value,true,Bs} -> eval_mc1(E, Qs, Bs, Ieval);
+ {value,false,_Bs} -> [];
+ {value,V,Bs} -> exception(error, {bad_filter,V}, Bs, Ieval)
+ end;
+eval_mc1({map_field_assoc,_,K0,V0}, [], Bs, Ieval) ->
+ {value,K,_} = expr(K0, Bs, Ieval#ieval{top=false}),
+ {value,V,_} = expr(V0, Bs, Ieval#ieval{top=false}),
+ [{K,V}].
+
+eval_generator({generate,Line,P,L0}, Bs0, CompFun, Ieval0) ->
+ Ieval = Ieval0#ieval{line=Line},
+ {value,L1,Bs1} = expr(L0, Bs0, Ieval#ieval{top=false}),
+ eval_generate(L1, P, Bs1, CompFun, Ieval);
+eval_generator({b_generate,Line,P,Bin0}, Bs0, CompFun, Ieval0) ->
+ Ieval = Ieval0#ieval{line=Line},
+ {value,Bin,Bs1} = expr(Bin0, Bs0, Ieval#ieval{top=false}),
+ eval_b_generate(Bin, P, Bs1, CompFun, Ieval);
+eval_generator({m_generate,Line,P,Map0}, Bs0, CompFun, Ieval0) ->
+ Ieval = Ieval0#ieval{line=Line},
+ {map_field_exact,_,K,V} = P,
+ {value,Map,_Bs1} = expr(Map0, Bs0, Ieval),
+ Iter = case is_map(Map) of
+ true ->
+ maps:iterator(Map);
+ false ->
+ %% Validate iterator.
+ try maps:foreach(fun(_, _) -> ok end, Map) of
+ _ ->
+ Map
+ catch
+ _:_ ->
+ exception(error, {bad_generator,Map}, Bs0, Ieval)
+ end
+ end,
+ eval_m_generate(Iter, {tuple,Line,[K,V]}, Bs0, CompFun, Ieval).
+
eval_generate([V|Rest], P, Bs0, CompFun, Ieval) ->
case catch match1(P, V, erl_eval:new_bindings(), Bs0) of
{match,Bsn} ->
@@ -1193,6 +1231,20 @@ eval_b_generate(<<_/bitstring>>=Bin, P, Bs0, CompFun, Ieval) ->
eval_b_generate(Term, _P, Bs, _CompFun, Ieval) ->
exception(error, {bad_generator,Term}, Bs, Ieval).
+eval_m_generate(Iter0, P, Bs0, CompFun, Ieval) ->
+ case maps:next(Iter0) of
+ {K,V,Iter} ->
+ case catch match1(P, {K,V}, erl_eval:new_bindings(), Bs0) of
+ {match,Bsn} ->
+ Bs2 = add_bindings(Bsn, Bs0),
+ CompFun(Bs2) ++ eval_m_generate(Iter, P, Bs0, CompFun, Ieval);
+ nomatch ->
+ eval_m_generate(Iter, P, Bs0, CompFun, Ieval)
+ end;
+ none ->
+ []
+ end.
+
safe_bif(M, F, As, Bs, Ieval0) ->
try apply(M, F, As) of
Value ->
diff --git a/lib/debugger/src/dbg_iload.erl b/lib/debugger/src/dbg_iload.erl
index 468d52ee61..d185d6be7c 100644
--- a/lib/debugger/src/dbg_iload.erl
+++ b/lib/debugger/src/dbg_iload.erl
@@ -619,9 +619,11 @@ expr({'try',Anno,Es0,CaseCs0,CatchCs0,As0}, Lc, St) ->
As = expr_list(As0, St),
{'try',ln(Anno),Es,CaseCs,CatchCs,As};
expr({lc,_,_,_}=Compr, _Lc, St) ->
- expr_lc_bc(Compr, St);
+ expr_comprehension(Compr, St);
expr({bc,_,_,_}=Compr, _Lc, St) ->
- expr_lc_bc(Compr, St);
+ expr_comprehension(Compr, St);
+expr({mc,_,_,_}=Compr, _Lc, St) ->
+ expr_comprehension(Compr, St);
expr({match,Anno,P0,E0}, _Lc, St) ->
E1 = expr(E0, false, St),
P1 = pattern(P0, St),
@@ -660,7 +662,11 @@ expr({bin_element,Anno,Expr0,Size0,Type0}, _Lc, St) ->
{Size1,Type} = make_bit_type(Anno, Size0, Type0),
Expr = expr(Expr0, false, St),
Size = expr(Size1, false, St),
- {bin_element,ln(Anno),Expr,Size,Type}.
+ {bin_element,ln(Anno),Expr,Size,Type};
+expr({map_field_assoc,L,K0,V0}, _Lc, St) ->
+ K = expr(K0, false, St),
+ V = expr(V0, false, St),
+ {map_field_assoc,L,K,V}.
consify([A|As]) ->
{cons,0,A,consify(As)};
@@ -676,19 +682,27 @@ make_bit_type(_Line, Size, Type0) -> %Integer or 'all'
{ok,Size,Bt} = erl_bits:set_bit_type(Size, Type0),
{Size,erl_bits:as_list(Bt)}.
-expr_lc_bc({Tag,Anno,E0,Gs0}, St) ->
- Gs = lists:map(fun ({generate,L,P0,Qs}) ->
- {generate,L,pattern(P0, St),expr(Qs, false, St)};
- ({b_generate,L,P0,Qs}) -> %R12.
- {b_generate,L,pattern(P0, St),expr(Qs, false, St)};
- (Expr) ->
- case is_guard_test(Expr, St) of
- true -> {guard,guard([[Expr]], St)};
- false -> expr(Expr, false, St)
- end
- end, Gs0),
+expr_comprehension({Tag,Anno,E0,Gs0}, St) ->
+ Gs = [case G of
+ ({generate,L,P0,Qs}) ->
+ {generator,{generate,L,pattern(P0, St),expr(Qs, false, St)}};
+ ({b_generate,L,P0,Qs}) -> %R12.
+ {generator,{b_generate,L,pattern(P0, St),expr(Qs, false, St)}};
+ ({m_generate,L,P0,Qs}) -> %OTP 26
+ {generator,{m_generate,L,mc_pattern(P0, St),expr(Qs, false, St)}};
+ (Expr) ->
+ case is_guard_test(Expr, St) of
+ true -> {guard,guard([[Expr]], St)};
+ false -> expr(Expr, false, St)
+ end
+ end || G <- Gs0],
{Tag,ln(Anno),expr(E0, false, St),Gs}.
+mc_pattern({map_field_exact,L,KeyP0,ValP0}, St) ->
+ KeyP1 = pattern(KeyP0, St),
+ ValP1 = pattern(ValP0, St),
+ {map_field_exact,L,KeyP1,ValP1}.
+
is_guard_test(Expr, #{ctype:=Ctypes}) ->
IsOverridden = fun(NA) ->
case maps:get(NA, Ctypes, undefined) of
diff --git a/lib/debugger/test/Makefile b/lib/debugger/test/Makefile
index 518335fe73..d2d32c5345 100644
--- a/lib/debugger/test/Makefile
+++ b/lib/debugger/test/Makefile
@@ -48,6 +48,7 @@ MODULES= \
line_number_SUITE \
map_SUITE \
maybe_SUITE \
+ mc_SUITE \
overridden_bif_SUITE \
record_SUITE \
trycatch_SUITE \
diff --git a/lib/debugger/test/mc_SUITE.erl b/lib/debugger/test/mc_SUITE.erl
new file mode 100644
index 0000000000..0a374cb51a
--- /dev/null
+++ b/lib/debugger/test/mc_SUITE.erl
@@ -0,0 +1,294 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2023. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%% Originally based on Per Gustafsson's test suite.
+%%
+
+-module(mc_SUITE).
+
+-export([all/0,suite/0,init_per_suite/1,end_per_suite/1,
+ init_per_testcase/2,end_per_testcase/2,
+ init_per_group/2,end_per_group/2,
+ basic/1,duplicate_keys/1,mixed/1,
+ shadow/1,bad_generators/1]).
+
+-include_lib("common_test/include/ct.hrl").
+
+suite() -> [{ct_hooks,[ts_install_cth]}].
+
+all() ->
+ [basic,
+ duplicate_keys,
+ mixed,
+ shadow,
+ bad_generators].
+
+init_per_suite(Config) ->
+ test_lib:interpret(?MODULE),
+ true = lists:member(?MODULE, int:interpreted()),
+ Config.
+
+end_per_suite(_Config) ->
+ ok.
+
+init_per_testcase(_Case, Config) ->
+ test_lib:interpret(?MODULE),
+ Config.
+
+end_per_testcase(_Case, _Config) ->
+ ok.
+
+init_per_group(_GroupName, Config) ->
+ Config.
+
+end_per_group(_GroupName, Config) ->
+ Config.
+
+-record(foo, {a,b}).
+
+basic(_Config) ->
+ mc_double(0),
+ mc_double(1),
+ mc_double(2),
+ mc_double(3),
+ mc_double(4),
+ mc_double(5),
+
+ mc_double(17),
+ mc_double(18),
+ mc_double(19),
+
+ mc_double(30),
+ mc_double(31),
+ mc_double(32),
+ mc_double(33),
+ mc_double(34),
+
+ mc_double(63),
+ mc_double(64),
+ mc_double(65),
+
+ mc_double(77),
+ mc_double(127),
+ mc_double(128),
+ mc_double(255),
+ mc_double(333),
+ mc_double(444),
+
+ %% Patterns that cannot possibly match.
+ #{} = #{K => V || K := [V]={V} <- id(#{a => [b]})},
+ #{} = #{K => V || [K] = 42 := V <- id(#{42 => whatever})},
+
+ %% Filtering.
+ Map = #{{a,1} => {a,b,c}, {b,42} => [1,2,3], c => [4,5,6], d => {x,y}},
+ [c, d] = lists:sort([K || K := _ <- Map, is_atom(K)]),
+ [{a,b,c}, [1,2,3]] = lists:sort([V || {_,_} := V <- Map]),
+ [1] = [H || {_,_} := [H|_] <- Map],
+ [c, {b,42}] = lists:sort([K || K := [_|_] <- Map]),
+
+ %% Filtering using literal patterns.
+ [] = [0 || a := b <- #{}],
+ [] = [0 || a := b <- #{x => y}],
+ [0] = [0 || a := b <- #{a => b}],
+
+ <<>> = << <<0>> || a := b <- #{} >>,
+ <<>> = << <<0>> || a := b <- #{x => y} >>,
+ <<0>> = << <<0>> || a := b <- #{a => b} >>,
+
+ %% Matching partial records.
+ RecordMap = id(#{#foo{a=I,b=I*I} => I*I*I || I <- [1,2,3,4]}),
+
+ EvenMap = maps:from_list([{K,V} ||
+ {#foo{a=N}=K,V} <- maps:to_list(RecordMap),
+ N rem 2 =:= 0]),
+ EvenMap = #{K => V ||
+ #foo{a=N}=K := V <- RecordMap,
+ N rem 2 =:= 0},
+
+ Odd = lists:sort([V || {#foo{a=N}, V} <- maps:to_list(RecordMap),
+ N rem 2 =:= 1]),
+ Odd = lists:sort([V || #foo{a=N} := V <- RecordMap, N rem 2 =:= 1]),
+
+ ok.
+
+mc_double(Size) ->
+ Seq = lists:seq(1, Size),
+ Map = #{{key,I} => I || I <- Seq},
+
+ MapDouble = #{K => 2 * V || K := V <- id(Map)},
+ MapDouble = maps:from_list([{{key,I}, 2 * I} || I <- Seq]),
+
+ OddKeys = lists:seq(1, Size, 2),
+ OddKeys = lists:sort([I || {key,I} := I <- Map,
+ I rem 2 =/= 0]),
+
+ OddMap = #{I => [] || {key,I} := I <- Map,
+ I rem 2 =/= 0},
+ OddMap = #{I => [] || {key,I} := I <- Map,
+ id(I) rem 2 =/= 0},
+ OddKeys = lists:sort(maps:keys(OddMap)),
+
+ %% Test that map comprehensions works on iterators.
+ test_iterator(Map, 0),
+ test_iterator(Map, map_size(Map) div 3),
+ test_iterator(Map, map_size(Map) div 2),
+ test_iterator(Map, map_size(Map)),
+
+ ok.
+
+test_iterator(Map, N) ->
+ Iter0 = maps:iterator(Map),
+ {First,Iter} = grab(N, Iter0, []),
+ All = [{K,V} || K := V <- Iter] ++ First,
+ Map = maps:from_list(All),
+ ok.
+
+grab(0, Iter, Acc) ->
+ {Acc,Iter};
+grab(N, Iter0, Acc) ->
+ case maps:next(Iter0) of
+ none ->
+ {Acc,Iter0};
+ {K,V,Iter} ->
+ grab(N - 1, Iter, [{K,V}|Acc])
+ end.
+
+duplicate_keys(_Config) ->
+ #{x := b} = #{V => K || {K,V} <- [{a, x}, {b, x}]},
+
+ #{a := 4, b := 4} =
+ #{K => V || K <- [a,b],
+ <<V>> <= <<1,2,3,4>>},
+ ok.
+
+mixed(_Config) ->
+ Map = id(#{1 => 10, 2 => 5, 3 => 88, 4 => 99, 5 => 36}),
+ Bin = << <<K:8,V:24>> || K := V <- Map >>,
+ Map = maps:from_list([{K,V} || <<K:8,V:24>> <= Bin]),
+
+ Atoms = [list_to_atom([C]) || C <- lists:seq($a, $z)],
+ Integers = lists:seq(1, 64),
+
+ mixed_1(Atoms, Integers),
+ mixed_2(Atoms, Integers),
+ mixed_3(Atoms, Integers),
+
+ sum_of_triangular_numbers(7),
+ sum_of_triangular_numbers(10),
+
+ ok.
+
+mixed_1(Atoms, Integers) ->
+ IntegerMap = #{N => [] || N <- Integers},
+ IntegerKeys = [N || N := [] <- IntegerMap],
+ Integers = lists:sort(IntegerKeys),
+ Combined = [{C,N} || C <- Atoms, N := [] <- IntegerMap],
+ Combined = [{C,N} || C <- Atoms, N := [] <- maps:iterator(IntegerMap)],
+ Combined = [{C,N} || C <- Atoms, N <- IntegerKeys],
+
+ ok.
+
+mixed_2(Atoms, Integers) ->
+ IntegerMap = #{N => [] || N <- Integers},
+ IntegerKeys = [N || N := [] <- IntegerMap],
+ Bin = << <<N:16>> || N := [] <- IntegerMap >>,
+ Integers = lists:sort(IntegerKeys),
+
+ Combined = [{C,N} || N := [] <- IntegerMap, C <- Atoms],
+ Combined = [{C,N} || N := [] <- maps:iterator(IntegerMap), C <- Atoms],
+ Combined = [{C,N} || <<N:16>> <= Bin, C <- Atoms],
+ Combined = [{C,N} || N <- IntegerKeys, C <- Atoms],
+
+ ok.
+
+mixed_3(Atoms, Integers) ->
+ Map = #{K => V || {K,V} <- lists:zip(Atoms, Integers, trim)},
+ Bin = << <<N:16>> || _ := N <- Map >>,
+ {TrimmedAtoms,TrimmedIntegers} = lists:unzip([{K,V} || K := V <- Map]),
+
+ Combined = lists:sort([{K,V} || K := _ <- Map, _ := V <- Map]),
+ Combined = lists:sort([{K,V} || K <- TrimmedAtoms, <<V:16>> <= Bin]),
+ Combined = lists:sort([{K,V} || K <- TrimmedAtoms, V <- TrimmedIntegers]),
+
+ ok.
+
+sum_of_triangular_numbers(N) ->
+ Sum = N * (N + 1) * (N + 2) div 6,
+ Maps = [#{I => I || I <- lists:seq(0, I)} || I <- lists:seq(0, N)],
+ Numbers = [I || M <- Maps, I := I <- M],
+ Numbers = lists:flatten([[I || I := I <- M] || M <- Maps]),
+ Sum = lists:sum([lists:sum([I || I := I <- M]) || M <- Maps]),
+ Sum = lists:sum(Numbers),
+ ok.
+
+shadow(_Config)->
+ Shadowed = nomatch,
+ _ = id(Shadowed), %Eliminate warning.
+ Map = #{Shadowed => Shadowed+1 || Shadowed <- lists:seq(7, 9)},
+ #{7 := 8, 8 := 9, 9 := 10} = id(Map),
+ [8,9] = lists:sort([Shadowed || _ := Shadowed <- id(Map),
+ Shadowed < 10]),
+ ok.
+
+bad_generators(_Config) ->
+ %% Make sure that line numbers point out the generator.
+ case ?MODULE of
+ mc_inline_SUITE ->
+ ok;
+ _ ->
+ {'EXIT',{{bad_generator,a},
+ [{?MODULE,_,_,_}|_]}} =
+ catch id(bad_generator(a)),
+
+ {'EXIT',{{bad_generator,a},
+ [{?MODULE,_,_,_}|_]}} =
+ catch id(bad_generator_bc(a)),
+
+ {'EXIT',{{bad_generator,a},
+ [{?MODULE,_,_,_}|_]}} =
+ catch id(bad_generator_mc(a)),
+
+ BadIterator = [16#ffff|#{}],
+
+ {'EXIT',{{bad_generator,BadIterator},
+ [{?MODULE,_,_,_}|_]}} =
+ catch id(bad_generator(BadIterator)),
+
+ {'EXIT',{{bad_generator,BadIterator},
+ [{?MODULE,_,_,_}|_]}} =
+ catch id(bad_generator_bc(BadIterator)),
+
+ {'EXIT',{{bad_generator,BadIterator},
+ [{?MODULE,_,_,_}|_]}} =
+ catch id(bad_generator_mc(BadIterator))
+ end,
+ ok.
+
+id(I) -> I.
+
+-file("bad_mc.erl", 1).
+bad_generator(Map) -> %Line 2
+ [{K,V} || %Line 3
+ K := V <- Map]. %Line 4
+bad_generator_bc(Map) -> %Line 5
+ << <<K:8,V:24>> || %Line 6
+ K := V <- Map>>. %Line 7
+bad_generator_mc(Map) -> %Line 8
+ #{V => K || %Line 9
+ K := V <- Map}. %Line 10
--
2.35.3