summaryrefslogtreecommitdiff
path: root/lib/compiler/src/beam_ssa_alias.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/compiler/src/beam_ssa_alias.erl')
-rw-r--r--lib/compiler/src/beam_ssa_alias.erl926
1 files changed, 926 insertions, 0 deletions
diff --git a/lib/compiler/src/beam_ssa_alias.erl b/lib/compiler/src/beam_ssa_alias.erl
new file mode 100644
index 0000000000..c16011ebc6
--- /dev/null
+++ b/lib/compiler/src/beam_ssa_alias.erl
@@ -0,0 +1,926 @@
+%%
+%% %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%
+%%
+%%
+
+-module(beam_ssa_alias).
+
+-export([opt/2]).
+
+-import(lists, [foldl/3, reverse/1, zip/2]).
+
+%% The maximum number of iterations when calculating alias
+%% information.
+-define(MAX_REPETITIONS, 16).
+
+-include("beam_ssa_opt.hrl").
+-include("beam_types.hrl").
+
+%% -define(DEBUG, true).
+
+-ifdef(DEBUG).
+-define(DP(FMT, ARGS), io:format(FMT, ARGS)).
+-define(DP(FMT), io:format(FMT)).
+-else.
+-define(DP(FMT, ARGS), skip).
+-define(DP(FMT), skip).
+-endif.
+
+-type call_args_status_map() :: #{ #b_local{} => ['aliased' | 'unique'] }.
+
+%% Alias analysis state
+-record(aas, {
+ caller :: func_id() | 'undefined',
+ call_args = #{} :: call_args_status_map(),
+ alias_map = #{},
+ func_db :: func_info_db(),
+ kills :: kills_map(),
+ st_map :: st_map(),
+ orig_st_map :: st_map(),
+ repeats = sets:new([{version,2}]) :: sets:set(func_id())
+ }).
+
+%% A code location refering to either the #b_set{} defining a variable
+%% or the terminator of a block.
+-type kill_loc() :: #b_var{} | {terminator, beam_ssa:label()}.
+
+%% Map a code location to the set of variables which die at that
+%% location.
+-type kill_set() :: #{ kill_loc() => sets:set(#b_var{}) }.
+
+-type kills_map() :: #{ func_id() => kill_set() }.
+
+%% Record holding the liveness information for a code location.
+-record(liveness_st, {
+ in = sets:new([{version,2}]) :: sets:set(#b_var{}),
+ out = sets:new([{version,2}]) :: sets:set(#b_var{})
+ }).
+
+%%%
+%%% Optimization pass which calculates the alias status of values and
+%%% uses the results to transform the code.
+%%%
+-spec opt(st_map(), func_info_db()) -> {st_map(), func_info_db()}.
+
+opt(StMap0, FuncDb0) ->
+ %% Ignore functions which are not in the function db (never
+ %% called) or are stubs for nifs.
+ Funs = [ F || F <- maps:keys(StMap0),
+ is_map_key(F, FuncDb0), not is_nif(F, StMap0)],
+ Liveness = liveness(Funs, StMap0),
+ KillsMap = killsets(Liveness, StMap0),
+
+ aa(Funs, KillsMap, StMap0, FuncDb0).
+
+%%%
+%%% Calculate liveness for each function using the standard iterative
+%%% fixpoint method.
+%%%
+
+-spec liveness([func_id()], st_map()) ->
+ [{func_id(), #{func_id() => {beam_ssa:label(), #liveness_st{}}}}].
+
+liveness([F|Funs], StMap) ->
+ Liveness = liveness_fun(F, StMap),
+ [{F,Liveness}|liveness(Funs, StMap)];
+liveness([], _StMap) ->
+ [].
+
+liveness_fun(F, StMap0) ->
+ #opt_st{ssa=SSA} = map_get(F, StMap0),
+ State0 = #{Lbl => #liveness_st{} || {Lbl,_} <- SSA},
+ UseDefCache = liveness_make_cache(SSA),
+ liveness_blks_fixp(reverse(SSA), State0, false, UseDefCache).
+
+liveness_blks_fixp(_SSA, State0, State0, _UseDefCache) ->
+ State0;
+liveness_blks_fixp(SSA, State0, _Old, UseDefCache) ->
+ State = liveness_blks(SSA, State0, UseDefCache),
+ liveness_blks_fixp(SSA, State, State0, UseDefCache).
+
+liveness_blks([{Lbl,Blk}|Blocks], State0, UseDefCache) ->
+ OutOld = get_live_out(Lbl, State0),
+ #{Lbl:={Defs,Uses}} = UseDefCache,
+ In = sets:union(Uses, sets:subtract(OutOld, Defs)),
+ Out = successor_live_ins(Blk, State0),
+ liveness_blks(Blocks, set_block_liveness(Lbl, In, Out, State0),
+ UseDefCache);
+liveness_blks([], State0, _UseDefCache) ->
+ State0.
+
+get_live_in(Lbl, State) ->
+ #liveness_st{in=In} = map_get(Lbl, State),
+ In.
+
+get_live_out(Lbl, State) ->
+ #liveness_st{out=Out} = map_get(Lbl, State),
+ Out.
+
+set_block_liveness(Lbl, In, Out, State) ->
+ L = map_get(Lbl, State),
+ State#{Lbl => L#liveness_st{in=In,out=Out}}.
+
+successor_live_ins(Blk, State) ->
+ foldl(fun(Lbl, Acc) ->
+ sets:union(Acc, get_live_in(Lbl, State))
+ end, sets:new([{version,2}]), beam_ssa:successors(Blk)).
+
+blk_defs(#b_blk{is=Is}) ->
+ foldl(fun(#b_set{dst=Dst}, Acc) ->
+ sets:add_element(Dst, Acc)
+ end, sets:new([{version,2}]), Is).
+
+blk_effective_uses(#b_blk{is=Is,last=Last}) ->
+ %% We can't use beam_ssa:used/1 on the whole block as it considers
+ %% a use after a def a use and that will derail the liveness
+ %% calculation.
+ blk_effective_uses([Last|reverse(Is)], sets:new([{version,2}])).
+
+blk_effective_uses([I|Is], Uses0) ->
+ Uses = case I of
+ #b_set{dst=Dst} ->
+ %% The uses after the def do not count
+ sets:del_element(Dst, Uses0);
+ _ -> % A terminator, no defs
+ Uses0
+ end,
+ LocalUses = sets:from_list(beam_ssa:used(I), [{version,2}]),
+ blk_effective_uses(Is, sets:union(Uses, LocalUses));
+blk_effective_uses([], Uses) ->
+ Uses.
+
+liveness_make_cache(SSA) ->
+ liveness_make_cache(SSA, #{}).
+
+liveness_make_cache([{Lbl,Blk}|Blocks], Cache0) ->
+ Defs = blk_defs(Blk),
+ Uses = blk_effective_uses(Blk),
+ Cache = Cache0#{Lbl=>{Defs,Uses}},
+ liveness_make_cache(Blocks, Cache);
+liveness_make_cache([], Cache) ->
+ Cache.
+
+%%%
+%%% Predicate to check if a function is the stub for a nif.
+%%%
+-spec is_nif(func_id(), st_map()) -> boolean().
+
+is_nif(F, StMap) ->
+ #opt_st{ssa=[{0,#b_blk{is=Is}}|_]} = map_get(F, StMap),
+ case Is of
+ [#b_set{op=nif_start}|_] ->
+ true;
+ _ -> false
+ end.
+
+%%%
+%%% Calculate the killset for all functions in the liveness
+%%% information.
+%%%
+-spec killsets([{func_id(),
+ #{func_id() => {beam_ssa:label(), #liveness_st{}}}}],
+ st_map()) -> kills_map().
+
+killsets(Liveness, StMap) ->
+ #{F => kills_fun(F, StMap, Live) || {F, Live} <- Liveness}.
+
+%%%
+%%% Calculate the killset for a function. The killset allows us to
+%%% look up the variables that die at a code location.
+%%%
+kills_fun(Fun, StMap, Liveness) ->
+ #opt_st{ssa=SSA} = map_get(Fun, StMap),
+ kills_fun1(SSA, #{}, Liveness).
+
+kills_fun1([{Lbl,Blk}|Blocks], KillsMap0, Liveness) ->
+ KillsMap = kills_block(Lbl, Blk, map_get(Lbl, Liveness), KillsMap0),
+ kills_fun1(Blocks, KillsMap, Liveness);
+kills_fun1([], KillsMap, _) ->
+ KillsMap.
+
+kills_block(Lbl, #b_blk{is=Is,last=Last}, #liveness_st{out=Out}, KillsMap0) ->
+ kills_is([Last|reverse(Is)], Out, KillsMap0, Lbl).
+
+kills_is([I|Is], Live0, KillsMap0, Blk) ->
+ {Live, Key} = case I of
+ #b_set{dst=Dst} ->
+ {sets:del_element(Dst, Live0), Dst};
+ _ ->
+ {Live0, {terminator, Blk}}
+ end,
+ Uses = sets:from_list(beam_ssa:used(I), [{version,2}]),
+ RemainingUses = sets:union(Live0, Uses),
+ Killed = sets:subtract(RemainingUses, Live0),
+ KillsMap = KillsMap0#{Key => Killed},
+ kills_is(Is, sets:union(Live, Killed), KillsMap, Blk);
+kills_is([], _, KillsMap, _) ->
+ KillsMap.
+
+%%%
+%%% Perform an alias analysis of the given functions, alias
+%%% information is added as annotations on the SSA code.
+%%%
+%%% Alias analysis is done by an algorithm inspired by Kotzmann and
+%%% Mössenböck's 2005 algorithm for Escape Analysis
+%%% (https://www.usenix.org/events/vee05/full_papers/p111-kotzmann.pdf),
+%%% particularly their escape equivalent sets. But in contrast to
+%%% Kotzmann and Mössenböck, instead of just tracking escaping values
+%%% we track if a value in a variable is unique and/or aliased.
+%%%
+%%% A variable is said to be unique if it currently is the only live
+%%% variable pointing to a particular term on the heap. Literals and
+%%% non-boxed terms are considered unique.
+%%%
+%%% A variable is said to be aliased if it points to a heap term which
+%%% can be reached by other means than the boxed pointer in the
+%%% variable.
+%%%
+%%% The alias analysis is performed by traversing the functions in the
+%%% module and their code. For each operation the uniqueness and alias
+%%% status are updated. The unique/aliased status is maintained in a
+%%% map which maps a variable to a either a status or another
+%%% variable. Thus constructing equivalent sets in the same way a
+%%% Kotzmann and Mössenböck.
+%%%
+%%% When the analysis finishes each instruction is annotated with
+%%% information about which of its arguments are unique or aliased.
+%%%
+-spec aa([func_id()], kills_map(), st_map(), func_info_db()) ->
+ {st_map(), func_info_db()}.
+
+aa(Funs, KillsMap, StMap, FuncDb) ->
+ %% Set up the argument info to make all incoming arguments to
+ %% exported functions aliased and all non-exported functions
+ %% unique.
+ ArgsInfo =
+ foldl(
+ fun(F=#b_local{}, Acc) ->
+ #func_info{exported=E,arg_types=AT} = map_get(F, FuncDb),
+ S = case E of
+ true -> aliased;
+ false -> unique
+ end,
+ Acc#{F=>[S || _ <- AT]}
+ end, #{}, Funs),
+ AAS = #aas{call_args=ArgsInfo,func_db=FuncDb,kills=KillsMap,
+ st_map=StMap, orig_st_map=StMap},
+ aa_fixpoint(Funs, AAS).
+
+%%%
+%%% Alias analysis works on the whole module and uses its own fixpoint
+%%% loop instead of the fixpoint abstraction in beam_ssa_opt. The
+%%% reason for this is three-fold:
+%%%
+%%% * The termination condition is simpler: the alias map hasn't
+%%% changed.
+%%%
+%%% * Adapting the alias analysis to fit into the beam_ssa_opt
+%%% fixpoint framework would require it to be expressed as three
+%%% passes in the style of ssa_opt_type_start,
+%%% ssa_opt_type_continue, and ssa_opt_type_finish in order to
+%%% create and maintain the state which is now kept in #aas{}.
+%%%
+%%% * As the beam_ssa_opt fixpoint framework doesn't provide a way for
+%%% an optimization to be informed that the fixpoint calculation
+%%% didn't converge within the interation limit and it is unsafe to
+%%% do optimizations on incomplete unique/aliased information it is
+%%% much simpler to explicitly handle it locally instead of trying
+%%% to detect incomplete information in a hypothetical
+%%% ssa_opt_alias_finish pass.
+%%%
+aa_fixpoint(Funs, AAS=#aas{func_db=FuncDb}) ->
+ Order = aa_breadth_first(Funs, FuncDb),
+ aa_fixpoint(Order, Order, AAS#aas.alias_map, AAS, ?MAX_REPETITIONS).
+
+aa_fixpoint([F|Fs], Order, OldAliasMap, AAS0=#aas{st_map=StMap}, Limit) ->
+ #b_local{name=#b_literal{val=_N},arity=_A} = F,
+ AAS1 = AAS0#aas{caller=F},
+ ?DP("-= ~p/~p =-~n", [_N, _A]),
+ {OptSt,AAS2} = aa_fun(F, map_get(F, StMap), AAS1),
+ AAS = AAS2#aas{st_map=StMap#{F => OptSt}},
+ aa_fixpoint(Fs, Order, OldAliasMap, AAS, Limit);
+aa_fixpoint([], _Order, OldAliasMap,
+ #aas{alias_map=OldAliasMap,func_db=FuncDb,st_map=StMap}, _) ->
+ ?DP("**** End of iteration ****~n"),
+ {StMap, FuncDb};
+aa_fixpoint([], _, _, #aas{func_db=FuncDb,orig_st_map=StMap}, 0) ->
+ ?DP("**** End of iteration, too many iterations ****~n"),
+ {StMap, FuncDb};
+aa_fixpoint([], Order, _OldAliasMap,
+ AAS=#aas{alias_map=AliasMap,repeats=Repeats}, Limit) ->
+ ?DP("**** Things have changed, starting next iteration ****~n"),
+ %% Following the depth first order, select those in Repeats.
+ NewOrder = [Id || Id <- Order, sets:is_element(Id, Repeats)],
+ aa_fixpoint(NewOrder, Order, AliasMap,
+ AAS#aas{repeats=sets:new([{version,2}])}, Limit - 1).
+
+aa_fun(F, #opt_st{ssa=Linear0,args=Args}=St,
+ AAS0=#aas{alias_map=AliasMap0,func_db=FuncDb,repeats=Repeats0}) ->
+ %% Initially assume all formal parameters are unique for a
+ %% non-exported function, if we have call argument info in the
+ %% AAS, we use it. For an exported function, all arguments are
+ %% assumed to be aliased.
+ ArgsStatus = aa_get_call_args_status(Args, F, AAS0),
+ SS0 = foldl(fun({Var, Status}, Acc) ->
+ aa_new_ssa_var(Var, Status, Acc)
+ end, #{}, ArgsStatus),
+ ?DP("@@ Args: ~p~n", [ArgsStatus]),
+ {Linear1,SS,AAS1} = aa_blocks(Linear0, SS0, AAS0),
+ ?DP("SS:~n~s~n~n", [SS]),
+ AAS = aa_merge_call_args_status(SS, AAS1),
+
+ AliasMap = AliasMap0#{ F => SS },
+ PrevSS = maps:get(F, AliasMap0, #{}),
+ Repeats = case PrevSS =/= SS of
+ true ->
+ %% Alias status has changed, so schedule both
+ %% our callers and callees for renewed analysis.
+ #{ F := #func_info{in=In,out=Out} } = FuncDb,
+ foldl(fun sets:add_element/2,
+ foldl(fun sets:add_element/2, Repeats0, Out), In);
+ false ->
+ Repeats0
+ end,
+ {St#opt_st{ssa=Linear1}, AAS#aas{alias_map=AliasMap,repeats=Repeats}}.
+
+%% Main entry point for the alias analysis
+aa_blocks([{L,#b_blk{is=Is0,last=T0}=Blk}|Bs0], SS0, AAS0) ->
+ {Is,SS1,AAS1} = aa_is(Is0, SS0, [], AAS0),
+ {T,SS2} = aa_terminator(T0, SS1, AAS1),
+ {Bs,SS,AAS} = aa_blocks(Bs0, SS2, AAS1),
+ {[{L,Blk#b_blk{is=Is,last=T}}|Bs],SS,AAS};
+aa_blocks([], SS, AAS) ->
+ {[],SS, AAS}.
+
+aa_is([I=#b_set{dst=Dst,op=Op,args=Args,anno=Anno0}|Is], SS0, Acc, AAS0) ->
+ SS1 = aa_new_ssa_var(Dst, unique, SS0),
+ {SS, AAS} =
+ case Op of
+ %% Instructions changing the alias status.
+ {bif,Bif} ->
+ {aa_bif(Dst, Bif, Args, SS1, AAS0), AAS0};
+ bs_create_bin ->
+ case Args of
+ [#b_literal{val=Flag},_,Arg|_] when
+ Flag =:= private_append ; Flag =:= append ->
+ case aa_all_dies([Arg], Dst, AAS0) of
+ true ->
+ %% Inherit the status of the argument
+ {aa_join(Dst, Arg, SS1), AAS0};
+ false ->
+ %% We alias with the surviving arg
+ {aa_set_aliased([Dst|Args], SS1), AAS0}
+ end;
+ _ ->
+ %% TODO: Too conservative?
+ {aa_set_aliased([Dst|Args], SS1), AAS0}
+ end;
+ bs_extract ->
+ {aa_set_aliased([Dst|Args], SS1), AAS0};
+ bs_get_tail ->
+ {aa_set_aliased([Dst|Args], SS1), AAS0};
+ bs_match ->
+ {aa_set_aliased([Dst|Args], SS1), AAS0};
+ bs_start_match ->
+ [_,Bin] = Args,
+ {aa_set_aliased([Dst,Bin], SS1), AAS0};
+ build_stacktrace ->
+ %% build_stacktrace can potentially alias anything
+ %% live at this point in the code. We handle it by
+ %% aliasing everything known to us. Touching
+ %% variables which are dead is harmless.
+ {aa_alias_all(SS1), AAS0};
+ call ->
+ {aa_call(Dst, Args, Anno0, SS1, AAS0), AAS0};
+ 'catch_end' ->
+ [_Tag,Arg] = Args,
+ {aa_join(Dst, Arg, SS1), AAS0};
+ extract ->
+ [Arg,_] = Args,
+ {aa_join(Dst, Arg, SS1), AAS0};
+ get_hd ->
+ [Arg] = Args,
+ {aa_pair_extraction(Dst, Arg, hd, SS1), AAS0};
+ get_map_element ->
+ [Map,_Key] = Args,
+ {aa_join(Dst, Map, SS1), AAS0};
+ get_tl ->
+ [Arg] = Args,
+ {aa_pair_extraction(Dst, Arg, tl, SS1), AAS0};
+ get_tuple_element ->
+ [Arg,Idx] = Args,
+ {aa_tuple_extraction(Dst, Arg, Idx, SS1), AAS0};
+ landingpad ->
+ {aa_set_aliased(Dst, SS1), AAS0};
+ make_fun ->
+ [Callee|Env] = Args,
+ aa_make_fun(Dst, Callee, Env, SS1, AAS0);
+ old_make_fun ->
+ [Callee|Env] = Args,
+ aa_make_fun(Dst, Callee, Env, SS1, AAS0);
+ peek_message ->
+ {aa_set_aliased(Dst, SS1), AAS0};
+ phi ->
+ {aa_phi(Dst, Args, SS1), AAS0};
+ put_list ->
+ {aa_construct_term(Dst, Args, SS1, AAS0), AAS0};
+ put_map ->
+ {aa_construct_term(Dst, Args, SS1, AAS0), AAS0};
+ put_tuple ->
+ {aa_construct_term(Dst, Args, SS1, AAS0), AAS0};
+ update_tuple ->
+ {aa_construct_term(Dst, Args, SS1, AAS0), AAS0};
+ update_record ->
+ [_Hint,_Size,Src|Updates] = Args,
+ Values = [Src|aa_update_record_get_vars(Updates)],
+ {aa_construct_term(Dst, Values, SS1, AAS0), AAS0};
+
+ %% Instructions which don't change the alias status
+ {float,_} ->
+ {SS1, AAS0};
+ {succeeded,_} ->
+ {SS1, AAS0};
+ bs_init_writable ->
+ {SS1, AAS0};
+ bs_test_tail ->
+ {SS1, AAS0};
+ has_map_field ->
+ {SS1, AAS0};
+ is_nonempty_list ->
+ {SS1, AAS0};
+ is_tagged_tuple ->
+ {SS1, AAS0};
+ kill_try_tag ->
+ {SS1, AAS0};
+ match_fail ->
+ {SS1, AAS0};
+ new_try_tag ->
+ {SS1, AAS0};
+ nif_start ->
+ {SS1, AAS0};
+ raw_raise ->
+ {SS1, AAS0};
+ recv_marker_bind ->
+ {SS1, AAS0};
+ recv_marker_clear ->
+ {SS1, AAS0};
+ recv_marker_reserve ->
+ {SS1, AAS0};
+ recv_next ->
+ {SS1, AAS0};
+ remove_message ->
+ {SS1, AAS0};
+ resume ->
+ {SS1, AAS0};
+ wait_timeout ->
+ {SS1, AAS0};
+ _ ->
+ exit({unknown_instruction, I})
+ end,
+ aa_is(Is, SS, [aa_update_annotation(I, SS1, AAS)|Acc], AAS);
+aa_is([], SS, Acc, AAS) ->
+ {reverse(Acc), SS, AAS}.
+
+aa_terminator(T=#b_br{anno=Anno0}, SS0, AAS) ->
+ Anno = aa_update_annotation(Anno0, SS0, AAS),
+ {T#b_br{anno=Anno}, SS0};
+aa_terminator(T=#b_ret{arg=Arg,anno=Anno0}, SS0, AAS) ->
+ Type = maps:get(result_type, Anno0, any),
+ Status0 = aa_get_status(Arg, SS0),
+ ?DP("Returned ~p:~p:~p~n", [Arg, Status0, Type]),
+ Type2Status0 = maps:get(returns, SS0, #{}),
+ Status = case Type2Status0 of
+ #{ Type := OtherStatus } ->
+ aa_meet(Status0, OtherStatus);
+ #{ } ->
+ Status0
+ end,
+ Type2Status = Type2Status0#{ Type => Status },
+ ?DP("new status map: ~p~n", [Type2Status]),
+ SS = SS0#{ returns => Type2Status},
+ {aa_update_annotation(T, SS, AAS), SS};
+aa_terminator(T=#b_switch{anno=Anno0}, SS0, AAS) ->
+ Anno = aa_update_annotation(Anno0, SS0, AAS),
+ {T#b_switch{anno=Anno}, SS0}.
+
+%% Add a new ssa variable to the alias state and set its status.
+aa_new_ssa_var(Var, Status, State) ->
+ false = maps:get(Var, State, false), % Assertion
+ State#{Var => {status, Status}}.
+
+aa_get_representative(Var, State) ->
+ %% TODO: Consider path compression
+ case State of
+ #{ Var := {status, _} } ->
+ Var;
+ #{ Var := Parent } ->
+ aa_get_representative(Parent, State)
+ end.
+
+aa_get_status(V=#b_var{}, State) ->
+ Repr = aa_get_representative(V, State),
+ #{ Repr := {status, S} } = State,
+ S;
+aa_get_status(#b_literal{}, _State) ->
+ unique.
+
+aa_set_status(V=#b_var{}, Status, State) ->
+ Repr = aa_get_representative(V, State),
+ State#{ Repr => {status, Status} };
+aa_set_status(#b_literal{}, _Status, State) ->
+ State;
+aa_set_status([X|T], Status, State) ->
+ aa_set_status(X, Status, aa_set_status(T, Status, State));
+aa_set_status([], _, State) ->
+ State.
+
+aa_update_annotation(I=#b_set{anno=Anno0,args=Args,op=Op}, SS, AAS) ->
+ {Aliased,Unique} =
+ foldl(fun(#b_var{}=V, {As,Us}) ->
+ case aa_get_status(V, SS) of
+ aliased ->
+ {ordsets:add_element(V, As), Us};
+ unique ->
+ {As, ordsets:add_element(V, Us)}
+ end;
+ (_, A) ->
+ A
+ end, {ordsets:new(),ordsets:new()}, Args),
+ Anno1 = case Aliased of
+ [] -> maps:remove(aliased, Anno0);
+ _ -> Anno0#{aliased => Aliased}
+ end,
+ Anno2 = case Unique of
+ [] -> maps:remove(unique, Anno1);
+ _ -> Anno1#{unique => Unique}
+ end,
+ Anno = case {Op,Args} of
+ {bs_create_bin,[#b_literal{val=append},_,Var|_]} ->
+ %% Alias analysis indicate the alias status of the
+ %% instruction arguments before the instruction is
+ %% executed. For the private-append optimization we
+ %% need to know if the first fragment dies with
+ %% this instruction or not. Adding an annotation
+ %% here, during alias analysis, is more efficient
+ %% than trying to reconstruct information in the
+ %% kill map during the private-append pass.
+ #aas{caller=Caller,kills=KillsMap} = AAS,
+ #b_set{dst=Dst} = I,
+ KillMap = map_get(Caller, KillsMap),
+ Dies = sets:is_element(Var, map_get(Dst, KillMap)),
+ Anno2#{first_fragment_dies => Dies};
+ _ ->
+ Anno2
+ end,
+ I#b_set{anno=Anno};
+aa_update_annotation(I=#b_ret{arg=#b_var{}=V,anno=Anno0}, SS, _AAS) ->
+ Anno = case aa_get_status(V, SS) of
+ aliased ->
+ maps:remove(unique, Anno0#{aliased=>[V]});
+ unique ->
+ maps:remove(aliased, Anno0#{unique=>[V]})
+ end,
+ I#b_ret{anno=Anno};
+aa_update_annotation(I, _SS, _AAS) ->
+ %% For now we don't care about the other terminators.
+ I.
+
+aa_set_aliased(Args, SS) ->
+ aa_set_status(Args, aliased, SS).
+
+aa_alias_all(SS0) ->
+ maps:map(fun(#b_var{}, _) ->
+ {status,aliased};
+ (returns, Types) ->
+ #{ T => aliased || T := _ <- Types};
+ (_, V) ->
+ V
+ end, SS0).
+
+aa_join_ls(VarA, [#b_var{}=VarB|Vars], State) ->
+ aa_join_ls(VarB, Vars, aa_join(VarA, VarB, State));
+aa_join_ls(VarA, [_|Vars], State) ->
+ aa_join_ls(VarA, Vars, State);
+aa_join_ls(_, [], State) ->
+ State.
+
+aa_join(#b_var{}=VarA, #b_var{}=VarB, State) ->
+ ARepr = aa_get_representative(VarA, State),
+ BRepr = aa_get_representative(VarB, State),
+ case {ARepr, BRepr} of
+ {Repr, Repr} ->
+ State;
+ _ ->
+ {status, A} = map_get(ARepr, State),
+ {status, B} = map_get(BRepr, State),
+ State#{ ARepr => {status, aa_meet(A, B)}, BRepr => ARepr }
+ end;
+aa_join(_, _, State) ->
+ State.
+
+aa_meet(#b_var{}=Var, SetStatus, State) ->
+ Repr = aa_get_representative(Var, State),
+ {status, Status} = map_get(Repr, State),
+ State#{ Repr => {status, aa_meet(SetStatus, Status)} };
+aa_meet(#b_literal{}, _SetStatus, State) ->
+ State;
+aa_meet([Var|Vars], [Status|Statuses], State) ->
+ aa_meet(Vars, Statuses, aa_meet(Var, Status, State));
+aa_meet([], [], State) ->
+ State.
+
+aa_meet(StatusA, StatusB) ->
+ case {StatusA, StatusB} of
+ {_,aliased} -> aliased;
+ {aliased, _} -> aliased;
+ {unique, unique} -> unique
+ end.
+
+aa_meet([H|T]) ->
+ aa_meet(H, aa_meet(T));
+aa_meet([]) ->
+ unique.
+
+%%
+%% Type is always less specific or exactly the same as one of the
+%% types in StatusByType, so we need to meet all possible statuses for
+%% the call site.
+%%
+aa_get_status_by_type(Type, StatusByType) ->
+ Statuses = [Status || Candidate := Status <- StatusByType,
+ beam_types:meet(Type, Candidate) =/= none],
+ aa_meet(Statuses).
+
+%% Predicate to check if all variables in `Vars` dies at `Where`.
+-spec aa_all_dies([#b_var{}], kill_loc(), #aas{}) -> boolean().
+aa_all_dies(Vars, Where, #aas{caller=Caller,kills=Kills}) ->
+ KillMap = map_get(Caller, Kills),
+ KillSet = map_get(Where, KillMap),
+ aa_all_dies(Vars, KillSet).
+
+aa_all_dies([#b_literal{}|Vars], KillSet) ->
+ aa_all_dies(Vars, KillSet);
+aa_all_dies([#b_var{}=V|Vars], KillSet) ->
+ case sets:is_element(V, KillSet) of
+ true ->
+ aa_all_dies(Vars, KillSet);
+ false ->
+ false
+ end;
+aa_all_dies([], _) ->
+ true.
+
+aa_alias_if_args_dont_die(Args, Where, SS, AAS) ->
+ case aa_all_dies(Args, Where, AAS) of
+ true ->
+ SS;
+ false ->
+ aa_set_aliased([Where|Args], SS)
+ end.
+
+%% Check that a variable in Args only occurs once, literals are
+%% ignored.
+aa_all_vars_unique(Args) ->
+ aa_all_vars_unique(Args, #{}).
+
+aa_all_vars_unique([#b_literal{}|Args], Seen) ->
+ aa_all_vars_unique(Args, Seen);
+aa_all_vars_unique([#b_var{}=V|Args], Seen) ->
+ case Seen of
+ #{ V := _ } ->
+ false;
+ #{} ->
+ aa_all_vars_unique(Args, Seen#{V => true })
+ end;
+aa_all_vars_unique([], _) ->
+ true.
+
+aa_construct_term(Dst, Values, SS, AAS) ->
+ case aa_all_vars_unique(Values)
+ andalso aa_all_dies(Values, Dst, AAS) of
+ true ->
+ aa_join_ls(Dst, Values, SS);
+ false ->
+ aa_set_aliased([Dst|Values], SS)
+ end.
+
+aa_update_record_get_vars([#b_literal{}, Value|Updates]) ->
+ [Value|aa_update_record_get_vars(Updates)];
+aa_update_record_get_vars([]) ->
+ [].
+
+aa_bif(Dst, element, [_Idx,Tuple], SS, AAS) ->
+ %% If we extract a value and the aggregate dies and wasn't aliased,
+ %% we should not consider this an aliasing operation.
+ aa_alias_if_args_dont_die([Tuple], Dst, SS, AAS);
+aa_bif(Dst, hd, Args, SS, AAS) ->
+ %% If we extract a value and the aggregate dies and wasn't aliased,
+ %% we should not consider this an aliasing operation.
+ aa_alias_if_args_dont_die(Args, Dst, SS, AAS);
+aa_bif(Dst, tl, Args, SS, AAS) ->
+ %% If we extract a value and the aggregate dies and wasn't aliased,
+ %% we should not consider this an aliasing operation.
+ aa_alias_if_args_dont_die(Args, Dst, SS, AAS);
+%% TODO: Ignored for now, as we don't track what's inside maps.
+%% aa_bif(_Dst, map_get, _Args, SS, _AAS) ->
+%% SS;
+aa_bif(Dst, Bif, Args, SS, _AAS) ->
+ Arity = length(Args),
+ case erl_internal:guard_bif(Bif, Arity)
+ orelse erl_internal:bool_op(Bif, Arity)
+ orelse erl_internal:comp_op(Bif, Arity)
+ orelse erl_internal:arith_op(Bif, Arity)
+ orelse erl_internal:new_type_test(Bif, Arity) of
+ true ->
+ SS;
+ false ->
+ %% Assume anything else shares the arguments and returns an
+ %% aliased result.
+ aa_set_aliased([Dst|Args], SS)
+ end.
+
+aa_phi(Dst, Args0, SS) ->
+ Args = [V || {V,_} <- Args0],
+ aa_join_ls(Dst, Args, SS).
+
+aa_call(Dst, [#b_local{}=Callee|Args], Anno, SS0,
+ #aas{alias_map=AliasMap,st_map=StMap}) ->
+ #b_local{name=#b_literal{val=_N},arity=_A} = Callee,
+ ?DP("A Call~n callee: ~p/~p~n args: ~p~n", [_N, _A, Args]),
+ IsNif = is_nif(Callee, StMap),
+ case AliasMap of
+ #{ Callee := CalleeSS } when not IsNif ->
+ ?DP(" The callee is known~n"),
+ #opt_st{args=CalleeArgs} = map_get(Callee, StMap),
+ ?DP(" args in caller: ~p~n",
+ [[{Arg, aa_get_status(Arg, SS0)} || Arg <- Args]]),
+ ArgStates = [ aa_get_status(Arg, CalleeSS) || Arg <- CalleeArgs],
+ ?DP(" callee arg states: ~p~n", [ArgStates]),
+ SS1 = aa_add_call_info(Callee, Args, SS0),
+ SS = aa_meet(Args, ArgStates, SS1),
+ ?DP(" meet: ~p~n",
+ [[{Arg, aa_get_status(Arg, SS1)} || Arg <- Args]]),
+ ReturnStatusByType = maps:get(returns, CalleeSS, #{}),
+ ?DP(" status by type: ~p~n", [ReturnStatusByType]),
+ ReturnedType = case Anno of
+ #{ result_type := ResultType } ->
+ ResultType;
+ #{} ->
+ any
+ end,
+ %% ReturnedType is always less specific or exactly the
+ %% same as one of the types in ReturnStatusByType.
+ ?DP(" returned type: ~s~n",
+ [beam_ssa_pp:format_type(ReturnedType)]),
+ ResultStatus = aa_get_status_by_type(ReturnedType,
+ ReturnStatusByType),
+ ?DP(" result status: ~p~n", [ResultStatus]),
+ aa_set_status(Dst, ResultStatus, SS);
+ _ when IsNif ->
+ %% This is a nif, assume that all arguments will be
+ %% aliased and that the result is aliased.
+ aa_set_aliased([Dst|Args], SS0);
+ #{} ->
+ %% We don't know anything about the function, don't change
+ %% the status of any variables
+ SS0
+ end;
+aa_call(Dst, [_Callee|Args], _Anno, SS, _AAS) ->
+ %% This is either a call to a fun or to an external function,
+ %% assume that all arguments and the result escape.
+ aa_set_aliased([Dst|Args], SS).
+
+%% Add info about the aliasing status of the arguments to the call
+aa_add_call_info(Callee, Args, SS0) ->
+ ArgStats = [aa_get_status(Arg, SS0) || Arg <- Args],
+ NewStats = case SS0 of
+ #{{call_info, Callee} := Stats} ->
+ [aa_meet(A, B) || {A,B} <- zip(Stats, ArgStats)];
+ #{} ->
+ ArgStats
+ end,
+ SS0#{{call_info, Callee} => NewStats}.
+
+%% Incorporate aliasing information derived when analysing the body of
+%% a function into the module-global state.
+aa_merge_call_args_status(SS, AAS=#aas{call_args=Info0}) ->
+ Info =
+ maps:fold(fun({call_info,Callee}, NewArgs, Acc) ->
+ #{ Callee := OldArgs } = Acc,
+ Args = [aa_meet(A, B)
+ || {A,B} <- zip(NewArgs, OldArgs)],
+ Acc#{Callee => Args};
+ (_, _, Acc) ->
+ Acc
+ end, Info0, SS),
+ AAS#aas{call_args=Info}.
+
+aa_get_call_args_status(Args, Callee, #aas{call_args=Info}) ->
+ #{ Callee := Status } = Info,
+ zip(Args, Status).
+
+aa_pair_extraction(Dst, Pair, Element, SS0) ->
+ case SS0 of
+ #{{pair,Pair}:=both} ->
+ %% Both elements have already been extracted
+ aa_set_aliased([Dst,Pair], SS0);
+ #{{pair,Pair}:=Element} ->
+ %% This element has already been extracted
+ aa_set_aliased([Dst,Pair], SS0);
+ #{{pair,Pair}:=_Other} ->
+ %% Both elements have now been extracted
+ aa_join(Dst, Pair, SS0#{{pair,Pair}=>both});
+ _ ->
+ %% Nothing has been extracted from this pair
+ aa_join(Dst, Pair, SS0#{{pair,Pair}=>Element})
+ end.
+
+aa_tuple_extraction(Dst, Tuple, #b_literal{val=I}, SS1) ->
+ case SS1 of
+ #{{tuple_element,Tuple}:=OrdSet0} ->
+ case ordsets:is_element(I, OrdSet0) of
+ true ->
+ aa_set_aliased([Dst,Tuple], SS1);
+ false ->
+ OrdSet = ordsets:add_element(I, OrdSet0),
+ aa_join(Dst, Tuple, SS1#{{tuple_element,Tuple}=>OrdSet})
+ end;
+ _ ->
+ %% There are no aliases yet.
+ aa_join(Dst, Tuple, SS1#{{tuple_element,Tuple}=>[I]})
+ end.
+
+aa_make_fun(Dst, Callee=#b_local{name=#b_literal{}},
+ Env0, SS0,
+ AAS0=#aas{call_args=Info0,repeats=Repeats0}) ->
+ %% When a value is copied into the environment of a fun we assume
+ %% that it has been aliased as there is no obvious way to track
+ %% and ensure that the value is only used once, even if the
+ %% argument dies at this location.
+ %%
+ %% We also assume that all arguments are aliased because someone
+ %% could have stolen the function through tracing and called it
+ %% with unexpected arguments, which may be aliased.
+ SS = aa_set_aliased([Dst|Env0], SS0),
+ #{ Callee := Status0 } = Info0,
+ Status = [aliased || _ <- Status0],
+ #{ Callee := PrevStatus } = Info0,
+ Info = Info0#{ Callee := Status },
+ Repeats = case PrevStatus =/= Status of
+ true ->
+ %% We have new information for the callee, we
+ %% have to revisit it.
+ sets:add_element(Callee, Repeats0);
+ false ->
+ Repeats0
+ end,
+ AAS = AAS0#aas{call_args=Info,repeats=Repeats},
+ {SS, AAS}.
+
+aa_breadth_first(Funs, FuncDb) ->
+ IsExported = fun (F) ->
+ #{ F := #func_info{exported=E} } = FuncDb,
+ E
+ end,
+ Exported = [ F || F <- Funs, IsExported(F)],
+ aa_breadth_first(Exported, [], sets:new([{version,2}]), FuncDb).
+
+aa_breadth_first([F|Work], Next, Seen, FuncDb) ->
+ case sets:is_element(F, Seen) of
+ true ->
+ aa_breadth_first(Work, Next, Seen, FuncDb);
+ false ->
+ case FuncDb of
+ #{ F := #func_info{out=Children} } ->
+ [F|aa_breadth_first(Work, Children ++ Next,
+ sets:add_element(F, Seen), FuncDb)];
+ #{} ->
+ %% Other optimization steps can have determined
+ %% that the function is not called and removed it
+ %% from the funcdb, but it still remains in the
+ %% #func_info{} of the (at the syntax-level)
+ %% caller.
+ aa_breadth_first(Work, Next, Seen, FuncDb)
+ end
+ end;
+aa_breadth_first([], [], _Seen, _FuncDb) ->
+ [];
+aa_breadth_first([], Next, Seen, FuncDb) ->
+ aa_breadth_first(Next, [], Seen, FuncDb).
+