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

openSUSE Build Service is sponsored by