File 0644-beam_ssa_opt-Fix-block-tracking-in-bsm_shortcut.patch of Package erlang

From b7f18fb91a196cb288ee1fe5b143589ac1c4d9c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?John=20H=C3=B6gberg?= <john@erlang.org>
Date: Tue, 15 Jul 2025 10:11:07 +0200
Subject: [PATCH] beam_ssa_opt: Fix block tracking in bsm_shortcut

Success blocks were not adequately tracked, causing `bs_test_tail`
instructions to be removed when they shouldn't have been.

Fixes GH-10047
---
 lib/compiler/src/beam_ssa_opt.erl    | 177 +++++++++++++++------------
 lib/compiler/src/beam_validator.erl  |   9 +-
 lib/compiler/test/bs_match_SUITE.erl |  71 +++++++++++
 3 files changed, 174 insertions(+), 83 deletions(-)

diff --git a/lib/compiler/src/beam_ssa_opt.erl b/lib/compiler/src/beam_ssa_opt.erl
index a6dfc0ddb3..0a54b8c311 100644
--- a/lib/compiler/src/beam_ssa_opt.erl
+++ b/lib/compiler/src/beam_ssa_opt.erl
@@ -2002,8 +2002,7 @@ bsm_skip([], _) -> [].
 
 bsm_skip_is([I0|Is], Extracted) ->
     case I0 of
-        #b_set{anno=Anno0,
-               op=bs_match,
+        #b_set{op=bs_match,
                dst=Ctx,
                args=[#b_literal{val=T}=Type,PrevCtx|Args0]}
           when T =/= float, T =/= string, T =/= skip ->
@@ -2014,9 +2013,7 @@ bsm_skip_is([I0|Is], Extracted) ->
                         I0;
                     false ->
                         %% The value is never extracted.
-                        Args = [#b_literal{val=skip},PrevCtx,Type|Args0],
-                        Anno = maps:remove(arg_types, Anno0),
-                        I0#b_set{anno=Anno,args=Args}
+                        I0#b_set{args=[#b_literal{val=skip},PrevCtx,Type|Args0]}
                 end,
             [I|Is];
         #b_set{} ->
@@ -2117,7 +2114,7 @@ ssa_opt_bsm_shortcut({#opt_st{ssa=Linear0}=St, FuncDb}) ->
             {St, FuncDb};
         _ ->
             Linear1 = bsm_shortcut(Linear0, Positions),
-            Linear = bsm_tail(Linear1, #{}),
+            Linear = bsm_tail(Linear1, #{ 0 => any }),
             ssa_opt_live({St#opt_st{ssa=Linear}, FuncDb})
     end.
 
@@ -2200,86 +2197,106 @@ bsm_shortcut([], _PosMap) -> [].
 %%     m1(<<_, Rest/binary>>) -> m1(Rest);
 %%     m1(<<>>) -> ok.
 %%
-%% The second clause of `m1/1` does not need to check for an empty
-%% binary.
-
-bsm_tail([{L,#b_blk{is=Is0,last=Last0}=Blk0}|Bs], Map0) ->
-    {Is,Last,Map} = bsm_tail_is(Is0, Last0, L, Map0, []),
-    Blk = Blk0#b_blk{is=Is,last=Last},
-    [{L,Blk}|bsm_tail(Bs, Map)];
-bsm_tail([], _Map) ->
+%% The second clause of `m1/1` does not need to check for an empty bitstring.
+%%
+%% This is done by keeping track of which blocks are reachable solely because
+%% of `bs_match` instructions that can only fail because the end has been
+%% reached, and then eliminating the related `bs_match` and `bs_test_tail`
+%% instructions in those blocks.
+
+bsm_tail([{L, #b_blk{is=Is0}=Blk0} | Bs], Tags0) when is_map_key(L, Tags0) ->
+    {Blk, Tags} = bsm_tail_is_1(Is0, Blk0, L, Tags0),
+    [{L, Blk} | bsm_tail(Bs, Tags)];
+bsm_tail([_ | Bs], Tags) ->
+    bsm_tail(Bs, Tags);
+bsm_tail([], _Tags) ->
     [].
 
-bsm_tail_is([#b_set{op=bs_start_match,anno=Anno,dst=Dst}=I|Is], Last, L, Map0, Acc) ->
-    case Anno of
-        #{arg_types := #{1 := Type}} ->
-            case beam_types:get_bs_matchable_unit(Type) of
-                error ->
-                    bsm_tail_is(Is, Last, L, Map0, [I|Acc]);
-                Unit when is_integer(Unit) ->
-                    Map = Map0#{Dst => Unit},
-                    bsm_tail_is(Is, Last, L, Map, [I|Acc])
-            end;
-        #{} ->
-            bsm_tail_is(Is, Last, L, Map0, [I|Acc])
-    end;
-bsm_tail_is([#b_set{op=bs_match,dst=Dst,args=Args},
-             #b_set{op={succeeded,guard},dst=SuccDst,args=[Dst]}|_]=Is,
-            #b_br{bool=SuccDst,fail=Fail}=Last,
-            _L, Map0, Acc) ->
-    case bsm_tail_num_matched(Args, Map0) of
-        unknown ->
-            %% Unknown number of bits or the match operation will fail
-            %% to match certain values.
-            Map = Map0#{Fail => unknown},
-            {reverse(Acc, Is),Last,Map};
-        Bits when is_integer(Bits) ->
-            case Map0 of
-                #{Fail := Bits} ->
-                    {reverse(Acc, Is),Last,Map0};
-                #{Fail := _} ->
-                    Map = Map0#{Fail => unknown},
-                    {reverse(Acc, Is),Last,Map};
-                #{} ->
-                    Map = Map0#{Fail => Bits},
-                    {reverse(Acc, Is),Last,Map}
-            end
-    end;
-bsm_tail_is([#b_set{op=bs_test_tail,args=[_,#b_literal{val=0}],dst=Dst}]=Is,
-            #b_br{bool=Dst,succ=Succ}=Last0, L, Map0, Acc) ->
-    case Map0 of
-        #{L := Bits} when is_integer(Bits) ->
-            %% The `bs_match` instruction targeting this block on failure
-            %% will only fail when the end of the binary has been reached.
-            %% There is no need for the test.
-            Last = beam_ssa:normalize(Last0#b_br{fail=Succ}),
-            {reverse(Acc, Is),Last,Map0};
-        #{} ->
-            {reverse(Acc, Is),Last0,Map0}
-    end;
-bsm_tail_is([#b_set{}=I|Is], Last, L, Map, Acc) ->
-    bsm_tail_is(Is, Last, L, Map, [I|Acc]);
-bsm_tail_is([], Last, _L, Map0, Acc) ->
-    Map = foldl(fun(F, A) ->
-                        A#{F => unknown}
-                end, Map0, beam_ssa:successors(#b_blk{is=[],last=Last})),
-    {reverse(Acc),Last,Map}.
-
-bsm_tail_num_matched([#b_literal{val=skip},Ctx,Type,Flags,Size,Unit], Map) ->
-    bsm_tail_num_matched([Type,Ctx,Flags,Size,Unit], Map);
-bsm_tail_num_matched([#b_literal{val=Type},Ctx,#b_literal{},
-                      #b_literal{val=Size},#b_literal{val=Unit}], Map)
+bsm_tail_is_1([#b_set{op=bs_match,anno=Anno,dst=Dst,args=[_, Ctx | _]=Args},
+               #b_set{op={succeeded,guard},dst=SuccDst,args=[Dst]}],
+              #b_blk{last=#b_br{bool=SuccDst,succ=Succ,fail=Fail}=Last}=Blk0,
+              L, Tags) ->
+    case {Tags, bsm_tail_match_tag(Args, Anno)} of
+        {#{ L := Ctx }, Ctx} ->
+            %% This block can only be reached through matches that fail because
+            %% the context is empty, and the current match will likewise only
+            %% fail because the context is empty, so we KNOW that this cannot
+            %% succeed.
+            %%
+            %% Kill the instruction and propagate the condition.
+            Blk = Blk0#b_blk{last=beam_ssa:normalize(Last#b_br{succ=Fail})},
+            {Blk, bsm_tail_update_target(Fail, Fail, Ctx, Tags)};
+        {#{ L := _ }, Tag} ->
+            %% `any` or different context. Mark the fail block with whether
+            %% it's reachable solely because the context is empty.
+            {Blk0, bsm_tail_update_target(Succ, Fail, Tag, Tags)}
+    end;
+bsm_tail_is_1([#b_set{op=bs_test_tail,args=[Ctx,#b_literal{val=Size}],dst=Dst}],
+              #b_blk{last=#b_br{bool=Dst,succ=Succ,fail=Fail}=Last0}=Blk0,
+              L, Tags) ->
+    true = is_integer(Size) andalso Size >= 0,  %Assertion.
+    case Tags of
+        #{ L := Ctx } ->
+            %% This block can only be reached through matches that fail because
+            %% the end of the context has been reached.
+            %%
+            %% Kill the instruction and propagate the condition.
+            Next = case Size of
+                       0 -> Succ;
+                       _ -> Fail
+                   end,
+            Last = beam_ssa:normalize(Last0#b_br{succ=Next,fail=Next}),
+            Blk = Blk0#b_blk{last=Last},
+            {Blk, bsm_tail_update_target(Next, Next, Ctx, Tags)};
+        #{ L := _ } ->
+            %% `any` or different context. We cannot optimize this, but it's
+            %% safe to mark the success block as only being reachable when the
+            %% context is empty.
+            Tag = case Size of
+                      0 -> Ctx;
+                      _ -> any
+                  end,
+            {Blk0, bsm_tail_update_target(Fail, Succ, Tag, Tags)}
+    end;
+bsm_tail_is_1([#b_set{} | Is], Blk, L, Tags) ->
+    bsm_tail_is_1(Is, Blk, L, Tags);
+bsm_tail_is_1([], Blk, _L, Tags0) ->
+    Tags = foldl(fun(Lbl, Acc) ->
+                         Acc#{ Lbl => any }
+                 end, Tags0, beam_ssa:successors(Blk)),
+    {Blk, Tags}.
+
+bsm_tail_match_tag([#b_literal{val=skip}, Ctx, Type | Rest], Anno) ->
+    bsm_tail_match_tag([Type, Ctx | Rest], Anno);
+bsm_tail_match_tag([#b_literal{val=Type},
+                    #b_var{}=Ctx,
+                    #b_literal{},
+                    #b_literal{val=Size},
+                    #b_literal{val=Unit}],
+                   Anno)
   when (Type =:= integer orelse Type =:= binary),
        is_integer(Size), is_integer(Unit) ->
     Bits = Size * Unit,
-    case Map of
-        #{Ctx := Bits} when is_integer(Bits) ->
-            Bits;
+    case Anno of
+        #{ arg_types := #{ 1 := CtxType } } ->
+            case beam_types:get_bs_matchable_unit(CtxType) of
+                Bits -> Ctx;
+                _ -> any
+            end;
         #{} ->
-            unknown
-    end;
-bsm_tail_num_matched(_Args, _Map) ->
-    unknown.
+            any
+    end;
+bsm_tail_match_tag(_Args, _Anno) ->
+    any.
+
+bsm_tail_update_target(Succ, Fail, Tag, Tags) when Succ =/= Fail ->
+    bsm_tail_update_target(Fail, Fail, Tag, Tags#{ Succ => any });
+bsm_tail_update_target(Same, Same, Tag, Tags) ->
+    case Tags of
+        #{ Same := Tag } -> Tags;
+        #{ Same := _ } -> Tags#{ Same => any };
+        #{} -> Tags#{ Same => Tag }
+    end.
 
 %%%
 %%% Optimize binary construction.
diff --git a/lib/compiler/src/beam_validator.erl b/lib/compiler/src/beam_validator.erl
index aa7211f715..d4ae896cbe 100644
--- a/lib/compiler/src/beam_validator.erl
+++ b/lib/compiler/src/beam_validator.erl
@@ -915,11 +915,13 @@ vi({test,bs_skip_bits2,{f,Fail},[Ctx,Size0,Unit,_Flags]}, Vst) ->
              end,
 
     validate_bs_skip(Fail, Ctx, Stride, Vst);
-vi({test,bs_test_tail2,{f,Fail},[Ctx,_Size]}, Vst) ->
+vi({test,bs_test_tail2,{f,Fail},[Ctx0,_Size]}, Vst) ->
+    Ctx = unpack_typed_arg(Ctx0, Vst),
     assert_no_exception(Fail),
     assert_type(#t_bs_context{}, Ctx, Vst),
     branch(Fail, Vst);
-vi({test,bs_test_unit,{f,Fail},[Ctx,Unit]}, Vst) ->
+vi({test,bs_test_unit,{f,Fail},[Ctx0,Unit]}, Vst) ->
+    Ctx = unpack_typed_arg(Ctx0, Vst),
     assert_type(#t_bs_context{}, Ctx, Vst),
 
     Type = #t_bs_context{tail_unit=Unit},
@@ -1849,7 +1851,8 @@ validate_bs_get_1(Fail, Ctx, Live, Vst, SuccFun) ->
 validate_bs_skip(Fail, Ctx, Stride, Vst) ->
     validate_bs_skip(Fail, Ctx, Stride, no_live, Vst).
 
-validate_bs_skip(Fail, Ctx, Stride, Live, Vst) ->
+validate_bs_skip(Fail, Ctx0, Stride, Live, Vst) ->
+    Ctx = unpack_typed_arg(Ctx0, Vst),
     assert_no_exception(Fail),
 
     assert_type(#t_bs_context{}, Ctx, Vst),
diff --git a/lib/compiler/test/bs_match_SUITE.erl b/lib/compiler/test/bs_match_SUITE.erl
index 2b06ec0a8d..27bcda248b 100644
--- a/lib/compiler/test/bs_match_SUITE.erl
+++ b/lib/compiler/test/bs_match_SUITE.erl
@@ -2533,6 +2533,8 @@ empty_matches(Config) when is_list(Config) ->
     {'EXIT',{function_clause,[_|_]}} = catch em_4(<<0:1>>, <<>>),
     {'EXIT',{function_clause,[_|_]}} = catch em_4(<<0:1>>, <<0:1>>),
 
+    ok = em_5(),
+
     ok.
 
 em_1(Bytes) ->
@@ -2560,6 +2562,75 @@ em_3_1(I) -> I.
 em_4(<<X:0, _:X>>, <<Y:0, _:Y>>) ->
     ok.
 
+%% GH-10047
+
+em_5() ->
+    A = id(<<1>>),
+    Empty = id(<<>>),
+    <<Zero>> = id(<<0>>),
+
+    true = is_bitstring(A),
+    true = is_bitstring(Empty),
+
+    <<1,128>> = em_5_1(A),
+    <<0>> = em_5_1(id(Empty)),
+
+    true = is_binary(A),
+    true = is_binary(Empty),
+
+    ok = em_5_coverage_1(A),
+    ok = em_5_coverage_1(Empty),
+
+    ok = em_5_coverage_2(A),
+    ok = em_5_coverage_2(Empty),
+
+    ok = em_5_coverage_3(A),
+    ok = em_5_coverage_3(Empty),
+
+    ok = em_5_coverage_4(A, Zero),
+    ok = em_5_coverage_4(Empty, Zero),
+
+    ok.
+
+em_5_1(<<Chunk:7/bits>>) ->
+    <<Chunk:7/bits, 0:1>>;
+em_5_1(<<Chunk:1/bits>>) ->
+    <<Chunk:1/bits, 0:7>>;
+em_5_1(<<>>) ->
+    <<0:8>>;
+em_5_1(<<Chunk:7/bits, Rest/bits>>) ->
+    <<Chunk:7/bits, 1:1, (em_5_1(Rest))/bits>>.
+
+%% Improves coverage.
+em_5_coverage_1(<<_:8/bits, Rest/binary>>) ->
+    em_5_coverage_1(Rest);
+em_5_coverage_1(<<_:8/integer, _/binary>>) ->
+    unreachable;
+em_5_coverage_1(<<>>) ->
+    ok.
+
+em_5_coverage_2(<<_:8/bits, Rest/binary>>) ->
+    em_5_coverage_2(Rest);
+em_5_coverage_2(<<_:8/integer, _/binary>>) ->
+    unreachable;
+em_5_coverage_2(<<>>) ->
+    ok.
+
+em_5_coverage_3(<<_:8/bits, Rest/binary>>) ->
+    em_5_coverage_3(Rest);
+em_5_coverage_3(<<_>>) ->
+    unreachable;
+em_5_coverage_3(<<>>) ->
+    ok.
+
+em_5_coverage_4(<<_:8/bits, Rest/binary>>, TailBits) ->
+    em_5_coverage_4(Rest, TailBits);
+em_5_coverage_4(Rest, TailBits) ->
+    case Rest of
+        <<_:TailBits/bits>> -> ok;
+        <<>> -> error
+    end.
+
 %% beam_trim would sometimes crash when bs_start_match4 had {atom,resume} as
 %% its fail label.
 trim_bs_start_match_resume(Config) when is_list(Config) ->
-- 
2.43.0

openSUSE Build Service is sponsored by