File 2172-compiler-Suppress-stack-traces-for-throw-1-when-they.patch of Package erlang
From 1067ef391c492683e91f7d4d7a4a96fbd2a26b05 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?John=20H=C3=B6gberg?= <john@erlang.org>
Date: Mon, 28 Sep 2020 14:33:31 +0200
Subject: [PATCH 2/2] compiler: Suppress stack traces for throw/1 when they're
not used
---
lib/compiler/src/Makefile | 2 +
lib/compiler/src/beam_ssa.erl | 2 +-
lib/compiler/src/beam_ssa_throw.erl | 495 +++++++++++++++++++++++++++
lib/compiler/src/beam_ssa_type.erl | 30 +-
lib/compiler/src/compile.erl | 5 +
lib/compiler/src/compiler.app.src | 1 +
lib/compiler/test/trycatch_SUITE.erl | 73 ++++
7 files changed, 596 insertions(+), 12 deletions(-)
create mode 100644 lib/compiler/src/beam_ssa_throw.erl
diff --git a/lib/compiler/src/Makefile b/lib/compiler/src/Makefile
index b1531ac985..d4eacfa154 100644
--- a/lib/compiler/src/Makefile
+++ b/lib/compiler/src/Makefile
@@ -71,6 +71,7 @@ MODULES = \
beam_ssa_pre_codegen \
beam_ssa_recv \
beam_ssa_share \
+ beam_ssa_throw \
beam_ssa_type \
beam_kernel_to_ssa \
beam_trim \
@@ -210,6 +211,7 @@ $(EBIN)/beam_ssa_pp.beam: beam_ssa.hrl
$(EBIN)/beam_ssa_pre_codegen.beam: beam_ssa.hrl
$(EBIN)/beam_ssa_recv.beam: beam_ssa.hrl
$(EBIN)/beam_ssa_share.beam: beam_ssa.hrl
+$(EBIN)/beam_ssa_throw.beam: beam_ssa.hrl
$(EBIN)/beam_ssa_type.beam: beam_ssa.hrl beam_types.hrl
$(EBIN)/beam_types.beam: beam_types.hrl
$(EBIN)/beam_validator.beam: beam_types.hrl
diff --git a/lib/compiler/src/beam_ssa.erl b/lib/compiler/src/beam_ssa.erl
index aa971b5f57..be251d0c78 100644
--- a/lib/compiler/src/beam_ssa.erl
+++ b/lib/compiler/src/beam_ssa.erl
@@ -50,7 +50,7 @@
var_name/0,var_base/0,literal_value/0,
op/0,anno/0,block_map/0,dominator_map/0,
rename_map/0,rename_proplist/0,usage_map/0,
- definition_map/0]).
+ definition_map/0,predecessor_map/0]).
-include("beam_ssa.hrl").
diff --git a/lib/compiler/src/beam_ssa_throw.erl b/lib/compiler/src/beam_ssa_throw.erl
new file mode 100644
index 0000000000..3e0ce49056
--- /dev/null
+++ b/lib/compiler/src/beam_ssa_throw.erl
@@ -0,0 +1,495 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2020-2020. 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%
+%%
+
+%%%
+%%% This pass skips stack trace generation on erlang:throw/1 when they're
+%%% guaranteed not to be inspected.
+%%%
+%%% It does so by finding all exception handlers in the module, determining
+%%% which functions they cover, and then simulating the result of every throw/1
+%%% in the module; if a particular throw/1 never reaches an instruction that
+%%% creates a stack trace, then it's safe to rewrite it.
+%%%
+
+-module(beam_ssa_throw).
+
+-export([module/2]).
+
+-import(lists, [foldl/3]).
+
+-include("beam_ssa.hrl").
+-include("beam_types.hrl").
+
+-type exception_var() :: none | #b_var{}.
+-type exception_vars() :: {Class :: exception_var(),
+ Reason :: exception_var(),
+ Stacktrace :: exception_var()}.
+-type tentative() :: {tentative,
+ Start :: beam_ssa:label(),
+ Vars :: exception_vars(),
+ Blocks :: beam_ssa:block_map()}.
+
+-type handler() :: tentative() | unsuitable | suitable.
+-type handler_id() :: {#b_local{}, beam_ssa:label()}.
+
+-type suitability() :: {tentative, beam_ssa:label()} |
+ unsuitable |
+ suitable.
+
+%% Per-module scan state
+-record(gst, {tlh_roots :: gb_trees:tree(#b_local{}, gb_sets:set(handler())),
+ tlh_edges=#{} :: #{ #b_local{} => gb_sets:set(#b_local{}) },
+ throws=cerl_sets:new() :: cerl_sets:set(#b_local{})}).
+
+%% Per-function scan state
+-record(lst, {suitability=#{} :: #{ #b_var{} => suitability() },
+ handlers=#{} :: #{ handler_id() => handler() },
+ blocks :: beam_ssa:block_map(),
+ predecessors :: beam_ssa:predecessor_map()}).
+
+-spec module(beam_ssa:b_module(), [compile:option()]) ->
+ {'ok',beam_ssa:b_module()}.
+
+module(#b_module{body=Fs0}=Module, _Opts) ->
+ case scan(Module) of
+ {Throws, TLHs} ->
+ Fs = opt(Fs0, Throws, TLHs),
+ {ok, Module#b_module{body=Fs}};
+ no_throws ->
+ {ok, Module}
+ end.
+
+%% Builds a map with the top-level exception handlers (TLHs) for each function,
+%% as well as the set of functions that call throw/1.
+scan(#b_module{body=Fs0}=Module) ->
+ scan_1(Fs0, init_gst(Module)).
+
+init_gst(#b_module{exports=Exports}) ->
+ %% We can't optimize any throw that leaves the module, so we'll add an
+ %% unsuitable top-level handler to all exported functions.
+ Unsuitable = gb_sets:singleton(unsuitable),
+ Roots = foldl(fun({Name, Arity}, Acc) ->
+ Id = #b_local{name=#b_literal{val=Name},arity=Arity},
+ gb_trees:insert(Id, Unsuitable, Acc)
+ end, gb_trees:empty(), Exports),
+ #gst{tlh_roots=Roots}.
+
+scan_1([#b_function{bs=Blocks}=F | Fs], Gst0) ->
+ Id = get_func_id(F),
+
+ Preds = beam_ssa:predecessors(Blocks),
+ Linear = beam_ssa:linearize(Blocks),
+
+ Lst = #lst{blocks=Blocks,predecessors=Preds},
+ {_, Gst} = scan_bs(Linear, Id, Lst, Gst0),
+
+ scan_1(Fs, Gst);
+scan_1([], Gst) ->
+ #gst{tlh_roots=Roots,
+ tlh_edges=Edges,
+ throws=Throws} = Gst,
+
+ case cerl_sets:size(Throws) of
+ 0 ->
+ no_throws;
+ _ ->
+ TLHs = propagate_tlhs(gb_trees:to_list(Roots), Edges, #{}),
+ {Throws, TLHs}
+ end.
+
+%% Propagates all top-level handlers to the functions they cover. See
+%% inherit_tlh/3 and add_tlh/3.
+propagate_tlhs([{Id, HandlersA} | Roots], Edges, Acc0) ->
+ HandlersB = maps:get(Id, Acc0, gb_sets:empty()),
+ case gb_sets:is_subset(HandlersA, HandlersB) of
+ true ->
+ propagate_tlhs(Roots, Edges, Acc0);
+ false ->
+ Merged = gb_sets:union(HandlersA, HandlersB),
+ Callees = pt_callees(Id, Merged, Edges),
+
+ Acc = propagate_tlhs(Callees, Edges, Acc0#{ Id => Merged }),
+ propagate_tlhs(Roots, Edges, Acc)
+ end;
+propagate_tlhs([], _Edges, Acc) ->
+ Acc.
+
+pt_callees(Id, Merged, Edges) ->
+ case Edges of
+ #{ Id := Callees } ->
+ gb_sets:fold(fun(Callee, Acc) ->
+ [{Callee, Merged} | Acc]
+ end, [], Callees);
+ #{} ->
+ []
+ end.
+
+%% We scan the blocks in post-order so we can finish analyzing handlers before
+%% reaching their affected calls. This lets us avoid unnecessary indirection
+%% when adding or inspecting top-level handlers.
+scan_bs([{?EXCEPTION_BLOCK, _} | Bs], Id, Lst, Gst) ->
+ scan_bs(Bs, Id, Lst, Gst);
+scan_bs([{Lbl, #b_blk{last=Last,is=Is}} | Bs], Id, Lst0, Gst0) ->
+ {Lst, Gst} = scan_bs(Bs, Id, Lst0, Gst0),
+ si_is(Is, Id, Lbl, Last, Lst, Gst);
+scan_bs([], _Id, Lst, Gst) ->
+ {Lst, Gst}.
+
+si_is([#b_set{op=landingpad,args=[_Kind, _Tag]} | Is],
+ Id, Lbl, Last, Lst, Gst) ->
+ %% Class:Reason:Stacktrace
+ Vars = {none, none, none},
+ si_handler_start(Is, Id, Lbl, Last, Vars, Lst, Gst);
+si_is([#b_set{op=resume,args=[Stacktrace, _Reason]} | Is],
+ Id, Lbl, Last, Lst, Gst) ->
+ si_handler_end(Is, Id, Lbl, Last, Stacktrace, Lst, Gst);
+si_is([#b_set{op=raw_raise,args=[_,_,Stacktrace]} | Is],
+ Id, Lbl, Last, Lst, Gst) ->
+ si_handler_end(Is, Id, Lbl, Last, Stacktrace, Lst, Gst);
+si_is([#b_set{op=build_stacktrace,args=[Stacktrace]} | Is],
+ Id, Lbl, Last, Lst, Gst) ->
+ si_handler_end(Is, Id, Lbl, Last, Stacktrace, Lst, Gst);
+si_is([#b_set{op=call,
+ dst=Dst,
+ args=[#b_remote{mod=#b_literal{val=erlang},
+ name=#b_literal{val=throw},
+ arity=1}, _Term]}],
+ Id, _Lbl, #b_ret{arg=Dst}, Lst, Gst) ->
+ %% Tail throw, handled by caller. We'll need to visit this function again.
+ #gst{throws=Throws0} = Gst,
+ Throws = cerl_sets:add_element(Id, Throws0),
+ {Lst, Gst#gst{throws=Throws}};
+si_is([#b_set{op=call,dst=Dst,args=[#b_local{}=Callee | _]}],
+ Id, _Lbl, #b_ret{arg=Dst}, Lst, Gst) ->
+ %% Tail call, inherit our caller's handler.
+ {Lst, inherit_tlh(Id, Callee, Gst)};
+si_is([#b_set{op=call,dst=Dst,args=[#b_local{}=Callee | _]},
+ #b_set{op={succeeded,body},args=[Dst]}],
+ Id, _Lbl, #b_br{fail=?EXCEPTION_BLOCK}, Lst, Gst) ->
+ %% No local handler, inherit our caller's top-level handlers.
+ {Lst, inherit_tlh(Id, Callee, Gst)};
+si_is([#b_set{op=call,dst=Dst,args=[#b_local{}=Callee | _]},
+ #b_set{op={succeeded,body},args=[Dst]}],
+ Id, _Lbl, #b_br{fail=Fail}, Lst, Gst) ->
+ %% Local handler, register it as a top-level handler with our callee.
+ HandlerId = {Id, Fail},
+ {Lst, add_tlh(HandlerId, Callee, Lst, Gst)};
+si_is([#b_set{} | Is], Id, Lbl, Last, Lst, Gst) ->
+ si_is(Is, Id, Lbl, Last, Lst, Gst);
+si_is([], _Id, _Lbl, _Last, Lst, Gst) ->
+ {Lst, Gst}.
+
+%% Marks the end of a handler. Note that we encounter this _before_ the start
+%% since we iterate the blocks in post-order.
+si_handler_end(Is, Id, EndLbl, Last, Stacktrace, Lst0, Gst) ->
+ #lst{suitability=Suitability0} = Lst0,
+
+ Marker = case Suitability0 of
+ #{ Stacktrace := {tentative, _} } ->
+ %% Multiple uses make partitioning more difficult, so
+ %% we'll disqualify this handler to save us the hassle.
+ unsuitable;
+ #{} ->
+ %% This handler may or may not be suitable depending on
+ %% what's thrown, so we keep it for later analysis.
+ {tentative, EndLbl}
+ end,
+
+ Suitability = Suitability0#{ Stacktrace => Marker },
+
+ Lst = Lst0#lst{suitability=Suitability},
+ si_is(Is, Id, EndLbl, Last, Lst, Gst).
+
+si_handler_start([#b_set{op=extract,
+ dst=Dst,
+ args=[_, #b_literal{val=Idx}]} | Is],
+ Id, StartLbl, Last, Vars0, Lst, Gst) ->
+ none = element(1 + Idx, Vars0), %Assertion.
+ Vars = setelement(1 + Idx, Vars0, Dst),
+ si_handler_start(Is, Id, StartLbl, Last, Vars, Lst, Gst);
+si_handler_start(Is, Id, StartLbl, Last, Vars, Lst0, Gst) ->
+ HandlerId = {Id, StartLbl},
+
+ #lst{blocks=Blocks,
+ predecessors=Preds,
+ suitability=Suitability,
+ handlers=Handlers0} = Lst0,
+
+ {_, _, Stacktrace} = Vars,
+
+ Handlers = case Suitability of
+ #{ Stacktrace := {tentative, EndLbl} } ->
+ %% This handler's suitability depends on what we throw,
+ %% so we'll extract the blocks between handler start and
+ %% the first use of the stack trace for later analysis.
+ Path = beam_ssa:between(StartLbl, EndLbl, Preds, Blocks),
+ Partition = maps:with(Path, Blocks),
+
+ Handler = {tentative, StartLbl, Vars, Partition},
+ Handlers0#{ HandlerId => Handler };
+ #{} when Stacktrace =/= none ->
+ %% Stack trace is used in an unsupported manner.
+ Handlers0#{ HandlerId => unsuitable };
+ #{} when Stacktrace =:= none ->
+ %% Stack trace is never used, so it's always safe to
+ %% skip trace generation.
+ Handlers0#{ HandlerId => suitable }
+ end,
+
+ Lst = Lst0#lst{handlers=Handlers},
+
+ si_is(Is, Id, StartLbl, Last, Lst, Gst).
+
+%% Makes the callee inherit its callers top-level handlers
+inherit_tlh(Caller, Callee, #gst{tlh_edges=Edges0}=Gst) ->
+ Callees = case Edges0 of
+ #{ Caller := Callees0 } ->
+ gb_sets:add_element(Callee, Callees0);
+ #{} ->
+ gb_sets:singleton(Callee)
+ end,
+ Edges = Edges0#{ Caller => Callees },
+ Gst#gst{tlh_edges=Edges}.
+
+%% Registers the given handler as a top-level handler for Callee
+add_tlh(Id, Callee, Lst, Gst) ->
+ #lst{handlers=Handlers} = Lst,
+ #gst{tlh_roots=Roots0} = Gst,
+
+ #{ Id := Handler } = Handlers,
+
+ TLHs0 = case gb_trees:lookup(Callee, Roots0) of
+ none -> gb_sets:singleton(Handler);
+ {value, V} -> V
+ end,
+
+ TLHs = case gb_sets:is_element(unsuitable, TLHs0) of
+ true ->
+ %% One bad apple spoils the bunch.
+ TLHs0;
+ false ->
+ gb_sets:add_element(Handler, TLHs0)
+ end,
+
+ Roots = gb_trees:enter(Callee, TLHs, Roots0),
+ Gst#gst{tlh_roots=Roots}.
+
+%% Goes through all functions that call throw/1, checking whether the stack
+%% trace is unused in all of its top-level exception handlers (TLHs), and
+%% rewriting such calls to `raise(throw, Term, [])`.
+opt([#b_function{bs=Blocks0}=F | Fs], Throws, TLHs) ->
+ Id = get_func_id(F),
+
+ Blocks = case {cerl_sets:is_element(Id, Throws), TLHs} of
+ {true, #{ Id := Handlers } } ->
+ opt_function(Handlers, Blocks0);
+ {_, _} ->
+ %% We're either unreachable or never throw.
+ Blocks0
+ end,
+
+ [F#b_function{bs=Blocks} | opt(Fs, Throws, TLHs)];
+opt([], _Throws, _TLHs) ->
+ [].
+
+get_func_id(#b_function{anno=Anno}) ->
+ {_,Name,Arity} = maps:get(func_info, Anno),
+ #b_local{name=#b_literal{val=Name},arity=Arity}.
+
+opt_function(Handlers, Blocks) ->
+ case gb_sets:is_member(unsuitable, Handlers) of
+ true ->
+ Blocks;
+ false ->
+ Linear0 = beam_ssa:linearize(Blocks),
+ Linear = opt_bs(Linear0, gb_sets:to_list(Handlers)),
+ maps:from_list(Linear)
+ end.
+
+opt_bs([{Lbl, #b_blk{last=Last,is=Is0}=Blk} | Bs], Hs) ->
+ Is = opt_is(Is0, Last, Hs),
+ [{Lbl, Blk#b_blk{is=Is}} | opt_bs(Bs, Hs)];
+opt_bs([], _Hs) ->
+ [].
+
+opt_is([#b_set{op=call,
+ dst=Dst,
+ args=[#b_remote{mod=#b_literal{val=erlang},
+ name=#b_literal{val=throw},
+ arity=1}, _Term]}=I0],
+ #b_ret{arg=Dst}, Hs) ->
+ ThrownType = beam_ssa:get_anno(thrown_type, I0, any),
+ I = opt_throw(Hs, ThrownType, I0),
+ [I];
+opt_is([I | Is], Last, Hs) ->
+ [I | opt_is(Is, Last, Hs)];
+opt_is([], _Last, _Hs) ->
+ [].
+
+opt_throw([suitable | Hs], ThrownType, I) ->
+ opt_throw(Hs, ThrownType, I);
+opt_throw([{tentative, Start, Vars, Blocks} | Hs], ThrownType, I) ->
+ case opt_is_suitable(Start, Blocks, Vars, ThrownType) of
+ true -> opt_throw(Hs, ThrownType, I);
+ false -> I
+ end;
+opt_throw([], _ThrownType, #b_set{args=[_, Reason]}=I) ->
+ %% All handlers caught us and ignored our stack trace. Rewrite ourselves to
+ %% erlang:raise/3 with an empty stack trace.
+ MFA = #b_remote{mod=#b_literal{val=erlang},
+ name=#b_literal{val=raise},
+ arity=3},
+ Stacktrace = #b_literal{val=[]},
+ I#b_set{args=[MFA, #b_literal{val=throw}, Reason, Stacktrace]}.
+
+%% Checks whether we avoid building a stack trace when entering the handler
+%% with the given throw type ("reason").
+%%
+%% The analysis is similar to that of the type pass, and I'd liked to have used
+%% that more directly but it would require much more work for little benefit;
+%% the watered-down version below is good enough to handle the most common
+%% cases.
+opt_is_suitable(Start, Blocks, Vars, ThrownType) ->
+ Ts = ois_init_ts(Vars, ThrownType),
+ ois_1([Start], Blocks, Ts).
+
+ois_1([Lbl | Lbls], Blocks, Ts0) ->
+ case Blocks of
+ #{ Lbl := #b_blk{last=Last,is=Is} } ->
+ case ois_is(Is, Ts0) of
+ {ok, Ts} ->
+ Next = ois_successors(Last, Ts),
+ ois_1(Next ++ Lbls, Blocks, Ts);
+ error ->
+ false
+ end;
+ #{} ->
+ ois_1(Lbls, Blocks, Ts0)
+ end;
+ois_1([], _Blocks, _Ts) ->
+ true.
+
+ois_successors(#b_switch{fail=Fail,list=List}, _Ts) ->
+ Lbls = [Lbl || {_, Lbl} <- List],
+ [Fail | Lbls];
+ois_successors(#b_br{bool=Bool,succ=Succ,fail=Fail}, Ts) ->
+ case beam_types:get_singleton_value(ois_get_type(Bool, Ts)) of
+ {ok, true} -> [Succ];
+ {ok, false} -> [Fail];
+ error -> [Succ, Fail]
+ end.
+
+ois_init_ts({Class, Reason, Stacktrace}, ThrownType) ->
+ Ts = #{ Class => beam_types:make_atom(throw),
+ Reason => ThrownType,
+ Stacktrace => any },
+ maps:remove(none, Ts).
+
+ois_is([#b_set{op=build_stacktrace} | _], _Ts) ->
+ error;
+ois_is([#b_set{op=raw_raise} | _], _Ts) ->
+ error;
+ois_is([#b_set{op=resume} | _], _Ts) ->
+ error;
+ois_is([#b_set{op=get_hd,dst=Dst,args=[Src]} | Is], Ts0) ->
+ SrcType = ois_get_type(Src, Ts0),
+ {Type, _, _} = beam_call_types:types(erlang, hd, [SrcType]),
+ Ts = Ts0#{ Dst => Type },
+ ois_is(Is, Ts);
+ois_is([#b_set{op=get_tl,dst=Dst,args=[Src]} | Is], Ts0) ->
+ SrcType = ois_get_type(Src, Ts0),
+ {Type, _, _} = beam_call_types:types(erlang, tl, [SrcType]),
+ Ts = Ts0#{ Dst => Type },
+ ois_is(Is, Ts);
+ois_is([#b_set{op=get_tuple_element,dst=Dst,args=[Src, Offset]} | Is], Ts0) ->
+ Type = case Ts0 of
+ #{ Src := #t_tuple{size=Size,elements=Es} } ->
+ #b_literal{val=N} = Offset,
+ true = Size > N, %Assertion.
+ beam_types:get_tuple_element(N + 1, Es);
+ #{} ->
+ any
+ end,
+ Ts = Ts0#{ Dst => Type },
+ ois_is(Is, Ts);
+ois_is([#b_set{op={bif,is_atom},dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, #t_atom{}, Is, Ts);
+ois_is([#b_set{op={bif,is_bitstring},dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, #t_bitstring{}, Is, Ts);
+ois_is([#b_set{op={bif,is_binary},dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, #t_bitstring{size_unit=8}, Is, Ts);
+ois_is([#b_set{op={bif,is_float},dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, #t_float{}, Is, Ts);
+ois_is([#b_set{op={bif,is_integer},dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, #t_integer{}, Is, Ts);
+ois_is([#b_set{op={bif,is_list},dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, #t_list{}, Is, Ts);
+ois_is([#b_set{op={bif,is_map},dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, #t_map{}, Is, Ts);
+ois_is([#b_set{op={bif,is_number},dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, number, Is, Ts);
+ois_is([#b_set{op={bif,is_tuple},dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, #t_tuple{}, Is, Ts);
+ois_is([#b_set{op=is_nonempty_list,dst=Dst,args=[Src]} | Is], Ts) ->
+ ois_type_test(Src, Dst, #t_cons{}, Is, Ts);
+ois_is([#b_set{op=is_tagged_tuple,dst=Dst,args=[Src, Size, Tag]} | Is], Ts) ->
+ Es = beam_types:set_tuple_element(1, ois_get_type(Tag, Ts), #{}),
+ #b_literal{val=N} = Size,
+
+ Type = #t_tuple{exact=true,size=N,elements=Es},
+
+ ois_type_test(Src, Dst, Type, Is, Ts);
+ois_is([#b_set{op={bif,tuple_size},dst=Dst,args=[Src]} | Is], Ts0) ->
+ SrcType = ois_get_type(Src, Ts0),
+ {Type, _, _} = beam_call_types:types(erlang, tuple_size, [SrcType]),
+ Ts = Ts0#{ Dst => Type },
+ ois_is(Is, Ts);
+ois_is([#b_set{op={bif,'=:='},
+ dst=Dst,
+ args=[LHS,#b_literal{val=RHS}]} | Is], Ts0) ->
+ Type = case beam_types:get_singleton_value(ois_get_type(LHS, Ts0)) of
+ {ok, RHS} -> beam_types:make_atom(true);
+ {ok, _Other} -> beam_types:make_atom(false);
+ error -> beam_types:make_boolean()
+ end,
+ Ts = Ts0#{ Dst => Type },
+ ois_is(Is, Ts);
+ois_is([#b_set{} | Is], Ts) ->
+ ois_is(Is, Ts);
+ois_is([], Ts) ->
+ {ok, Ts}.
+
+ois_type_test(Src, Dst, RequiredType, Is, Ts0) ->
+ GivenType = ois_get_type(Src, Ts0),
+ Type = case beam_types:meet(GivenType, RequiredType) of
+ GivenType -> beam_types:make_atom(true);
+ none -> beam_types:make_atom(false);
+ _Other -> beam_types:make_boolean()
+ end,
+ Ts = Ts0#{ Dst => Type },
+ ois_is(Is, Ts).
+
+ois_get_type(#b_literal{val=Value}, _Ts) ->
+ beam_types:make_type_from_value(Value);
+ois_get_type(#b_var{}=Arg, Ts) ->
+ case Ts of
+ #{ Arg := Type } -> Type;
+ #{} -> any
+ end.
diff --git a/lib/compiler/src/beam_ssa_type.erl b/lib/compiler/src/beam_ssa_type.erl
index 1e4c9ba5a0..232a5daf16 100644
--- a/lib/compiler/src/beam_ssa_type.erl
+++ b/lib/compiler/src/beam_ssa_type.erl
@@ -931,16 +931,11 @@ simplify(#b_set{op=put_tuple,args=Args}=I, _Ts) ->
end;
simplify(#b_set{op=wait_timeout,args=[#b_literal{val=0}]}, _Ts) ->
#b_literal{val=true};
-simplify(#b_set{op=call,args=[#b_remote{}=Rem|Args]}=I, _Ts) ->
+simplify(#b_set{op=call,args=[#b_remote{}=Rem|Args]}=I, Ts) ->
case Rem of
#b_remote{mod=#b_literal{val=Mod},
name=#b_literal{val=Name}} ->
- case erl_bifs:is_pure(Mod, Name, length(Args)) of
- true ->
- simplify_remote_call(Mod, Name, Args, I);
- false ->
- I
- end;
+ simplify_remote_call(Mod, Name, Args, Ts, I);
#b_remote{} ->
I
end;
@@ -1154,13 +1149,17 @@ phi_all_same_1([], _Arg) ->
phi_all_same_1(_Phis, _Arg) ->
false.
-%% Simplify a remote call to a pure BIF.
-simplify_remote_call(erlang, '++', [#b_literal{val=[]},Tl], _I) ->
+simplify_remote_call(erlang, throw, [Term], Ts, I) ->
+ %% Annotate `throw` instructions with the type of their thrown term,
+ %% helping `beam_ssa_throw` optimize non-local returns.
+ Type = normalized_type(Term, Ts),
+ beam_ssa:add_anno(thrown_type, Type, I);
+simplify_remote_call(erlang, '++', [#b_literal{val=[]},Tl], _Ts, _I) ->
Tl;
simplify_remote_call(erlang, setelement,
[#b_literal{val=Pos},
#b_literal{val=Tuple},
- #b_var{}=Value], I)
+ #b_var{}=Value], _Ts, I)
when is_integer(Pos), 1 =< Pos, Pos =< tuple_size(Tuple) ->
%% Position is a literal integer and the shape of the
%% tuple is known.
@@ -1168,7 +1167,16 @@ simplify_remote_call(erlang, setelement,
{Bef,[_|Aft]} = split(Pos - 1, Els0),
Els = Bef ++ [Value|Aft],
I#b_set{op=put_tuple,args=Els};
-simplify_remote_call(Mod, Name, Args0, I) ->
+simplify_remote_call(Mod, Name, Args, _Ts, I) ->
+ case erl_bifs:is_pure(Mod, Name, length(Args)) of
+ true ->
+ simplify_pure_call(Mod, Name, Args, I);
+ false ->
+ I
+ end.
+
+%% Simplify a remote call to a pure BIF.
+simplify_pure_call(Mod, Name, Args0, I) ->
case make_literal_list(Args0) of
none ->
I;
diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl
index 8a3e1c6cd3..e08b444a13 100644
--- a/lib/compiler/src/compile.erl
+++ b/lib/compiler/src/compile.erl
@@ -874,6 +874,10 @@ kernel_passes() ->
{iff,dssaopt,{listing,"ssaopt"}},
{unless,no_ssa_opt,{iff,ssalint,{pass,beam_ssa_lint}}},
+ {unless,no_throw_opt,{pass,beam_ssa_throw}},
+ {iff,dthrow,{listing,"throw"}},
+ {unless,no_throw_opt,{iff,ssalint,{pass,beam_ssa_lint}}},
+
{unless,no_recv_opt,{pass,beam_ssa_recv}},
{iff,drecv,{listing,"recv"}},
{unless,no_recv_opt,{iff,ssalint,{pass,beam_ssa_lint}}}]},
@@ -2173,6 +2177,7 @@ pre_load() ->
beam_ssa_pre_codegen,
beam_ssa_recv,
beam_ssa_share,
+ beam_ssa_throw,
beam_ssa_type,
beam_trim,
beam_types,
diff --git a/lib/compiler/src/compiler.app.src b/lib/compiler/src/compiler.app.src
index 92e9fa74e5..9ab9553283 100644
--- a/lib/compiler/src/compiler.app.src
+++ b/lib/compiler/src/compiler.app.src
@@ -47,6 +47,7 @@
beam_ssa_pre_codegen,
beam_ssa_recv,
beam_ssa_share,
+ beam_ssa_throw,
beam_ssa_type,
beam_trim,
beam_types,
diff --git a/lib/compiler/test/trycatch_SUITE.erl b/lib/compiler/test/trycatch_SUITE.erl
index 9b4a7754b3..8695946409 100644
--- a/lib/compiler/test/trycatch_SUITE.erl
+++ b/lib/compiler/test/trycatch_SUITE.erl
@@ -1526,6 +1526,7 @@ expr_export_5() ->
coverage(_Config) ->
{'EXIT',{{badfun,true},[_|_]}} = (catch coverage_1()),
+ ok = coverage_ssa_throw(),
ok.
%% Cover some code in beam_trim.
@@ -1543,5 +1544,77 @@ coverage_1() ->
true
end.
+%% Cover some code in beam_ssa_throw.
+coverage_ssa_throw() ->
+ cst_trivial(),
+ cst_raw(),
+ cst_stacktrace(),
+ cst_types(),
+
+ ok.
+
+cst_trivial() ->
+ %% never inspects stacktrace
+ try
+ cst_trivial_1()
+ catch
+ _C:_R:_S ->
+ ok
+ end.
+
+cst_trivial_1() -> throw(id(gurka)).
+
+cst_types() ->
+ %% type tests
+ try
+ cst_types_1()
+ catch
+ throw:Val when is_atom(Val);
+ is_bitstring(Val);
+ is_binary(Val);
+ is_float(Val);
+ is_integer(Val);
+ is_list(Val);
+ is_map(Val);
+ is_number(Val);
+ is_tuple(Val) ->
+ ok;
+ throw:[_|_]=Cons when hd(Cons) =/= gurka;
+ tl(Cons) =/= gaffel ->
+ %% is_nonempty_list, get_hd, get_tl
+ ok;
+ throw:Tuple when tuple_size(Tuple) < 5 ->
+ %% tuple_size
+ ok
+ end.
+
+cst_types_1() -> throw(id(gurka)).
+
+cst_stacktrace() ->
+ %% build_stacktrace
+ try
+ cst_stacktrace_1()
+ catch
+ throw:gurka ->
+ ok;
+ _C:_R:Stack ->
+ id(Stack),
+ ok
+ end.
+
+cst_stacktrace_1() -> throw(id(gurka)).
+
+cst_raw() ->
+ %% raw_raise
+ try
+ cst_raw_1()
+ catch
+ throw:gurka ->
+ ok;
+ _C:_R:Stack ->
+ erlang:raise(error, dummy, Stack)
+ end.
+
+cst_raw_1() -> throw(id(gurka)).
id(I) -> I.
--
2.26.2