diff --git a/lib/compiler/src/Makefile b/lib/compiler/src/Makefile index 0ceba13ebe26..9b50016bb559 100644 --- a/lib/compiler/src/Makefile +++ b/lib/compiler/src/Makefile @@ -112,6 +112,7 @@ BEAM_H = $(wildcard ../priv/beam_h/*.h) HRL_FILES= \ beam_asm.hrl \ beam_disasm.hrl \ + beam_ssa_alias_debug.hrl \ beam_ssa_opt.hrl \ beam_ssa.hrl \ beam_types.hrl \ @@ -216,7 +217,8 @@ $(EBIN)/beam_jump.beam: beam_asm.hrl $(EBIN)/beam_listing.beam: core_parse.hrl beam_ssa.hrl \ beam_asm.hrl beam_types.hrl $(EBIN)/beam_ssa.beam: beam_ssa.hrl -$(EBIN)/beam_ssa_alias.beam: beam_ssa_opt.hrl beam_types.hrl +$(EBIN)/beam_ssa_alias.beam: beam_ssa_opt.hrl beam_ssa_alias_debug.hrl \ + beam_types.hrl $(EBIN)/beam_ssa_bsm.beam: beam_ssa.hrl $(EBIN)/beam_ssa_bool.beam: beam_ssa.hrl $(EBIN)/beam_ssa_check.beam: beam_ssa.hrl beam_types.hrl @@ -229,7 +231,7 @@ $(EBIN)/beam_ssa_pp.beam: beam_ssa.hrl beam_types.hrl $(EBIN)/beam_ssa_pre_codegen.beam: beam_ssa.hrl beam_asm.hrl $(EBIN)/beam_ssa_recv.beam: beam_ssa.hrl $(EBIN)/beam_ssa_share.beam: beam_ssa.hrl -$(EBIN)/beam_ssa_ss.beam: beam_ssa.hrl beam_types.hrl +$(EBIN)/beam_ssa_ss.beam: beam_ssa.hrl beam_ssa_alias_debug.hrl beam_types.hrl $(EBIN)/beam_ssa_throw.beam: beam_ssa.hrl beam_types.hrl $(EBIN)/beam_ssa_type.beam: beam_ssa.hrl beam_types.hrl $(EBIN)/beam_trim.beam: beam_asm.hrl diff --git a/lib/compiler/src/beam_digraph.erl b/lib/compiler/src/beam_digraph.erl index 5cf4b23d11fe..f89d39bb2b56 100644 --- a/lib/compiler/src/beam_digraph.erl +++ b/lib/compiler/src/beam_digraph.erl @@ -30,13 +30,15 @@ -export([new/0, add_vertex/2, add_vertex/3, add_edge/3, add_edge/4, del_edge/2, del_edges/2, + del_vertex/2, + edges/1, foldv/3, has_vertex/2, is_path/3, in_degree/2, in_edges/2, in_neighbours/2, no_vertices/1, out_degree/2, out_edges/2, out_neighbours/2, - vertex/2, vertices/1, + vertex/2, vertex/3, vertices/1, reverse_postorder/2, roots/1, topsort/1, @@ -76,6 +78,18 @@ add_vertex(Dg, V, Label) -> Vs = Vs0#{V=>Label}, Dg#dg{vs=Vs}. +-spec del_vertex(graph(), vertex()) -> graph(). +del_vertex(Dg, V) -> + #dg{vs=Vs0,in_es=InEsMap0,out_es=OutEsMap0} = Dg, + InEs = maps:get(V, InEsMap0, []), + OutEsMap = foldl(fun({From,_,_}=E, A) -> edge_map_del(From, E, A) end, + maps:remove(V, OutEsMap0), InEs), + OutEs = maps:get(V, OutEsMap0, []), + InEsMap = foldl(fun({_,To,_}=E, A) -> edge_map_del(To, E, A) end, + maps:remove(V, InEsMap0), OutEs), + Vs = maps:remove(V, Vs0), + Dg#dg{vs=Vs,in_es=InEsMap,out_es=OutEsMap}. + -spec add_edge(graph(), vertex(), vertex()) -> graph(). add_edge(Dg, From, To) -> add_edge(Dg, From, To, edge). @@ -176,6 +190,12 @@ no_vertices(#dg{vs=Vs}) -> vertex(#dg{vs=Vs}, V) -> map_get(V, Vs). +%% As vertex/2 but if the vertex does not exist a default value is +%% returned. +-spec vertex(graph(), vertex(), label()) -> label(). +vertex(#dg{vs=Vs}, V, Default) -> + maps:get(V, Vs, Default). + -spec vertices(graph()) -> [{vertex(), label()}]. vertices(#dg{vs=Vs}) -> maps:to_list(Vs). @@ -217,6 +237,12 @@ topsort(G) -> Seen = roots(G), reverse_postorder(G, Seen). +-spec edges(graph()) -> [edge()]. +edges(#dg{out_es=OutEsMap}) -> + maps:fold(fun(_, Es, Acc) -> + Es ++ Acc + end, [], OutEsMap). + %% %% Kosaraju's algorithm %% diff --git a/lib/compiler/src/beam_ssa_alias.erl b/lib/compiler/src/beam_ssa_alias.erl index 19c438f61fde..9d3433855f64 100644 --- a/lib/compiler/src/beam_ssa_alias.erl +++ b/lib/compiler/src/beam_ssa_alias.erl @@ -32,15 +32,16 @@ -include("beam_ssa_opt.hrl"). -include("beam_types.hrl"). +-include("beam_ssa_alias_debug.hrl"). -%% Uncomment the following to get trace printouts. - -%% -define(DEBUG, true). - --ifdef(DEBUG). +-ifdef(DEBUG_ALIAS). -define(DP(FMT, ARGS), io:format(FMT, ARGS)). -define(DP(FMT), io:format(FMT)). -define(DBG(STMT), STMT). + +fn(#b_local{name=#b_literal{val=N},arity=A}) -> + io_lib:format("~p/~p", [N, A]). + -else. -define(DP(FMT, ARGS), skip). -define(DP(FMT), skip). @@ -58,20 +59,38 @@ orig_st_map :: st_map(), repeats = sets:new() :: sets:set(func_id()), %% The next unused variable name in caller - cnt = 0 :: non_neg_integer() + cnt = 0 :: non_neg_integer(), + %% Functions which have been analyzed at least once. + analyzed = sets:new() :: sets:set(func_id()), + run_count = #{} :: #{ func_id() => non_neg_integer() }, + prune_strategy = #{} :: #{ func_id() => + #{beam_ssa:label() => + 'add' | 'del'} } }). %% 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()} - | {live_outs, beam_ssa:label()}. + | {live_outs, beam_ssa:label()} + | {killed_in_block, beam_ssa:label()}. + +-type var_set() :: sets:set(#b_var{}). %% Map a code location to the set of variables which die at that %% location. --type kill_set() :: #{ kill_loc() => sets:set(#b_var{}) }. +-type kill_set() :: #{ kill_loc() => var_set() }. + +%% Map a label to the set of variables which are live in to that block. +-type live_in_set() :: #{ beam_ssa:label() => var_set() }. --type kills_map() :: #{ func_id() => kill_set() }. +%% Map a flow-control edge to the set of variables which are live in +%% to the destination block due to phis. +-type phi_live_set() :: #{ {beam_ssa:label(), beam_ssa:label()} => var_set() }. + +-type kills_map() :: #{ func_id() => {live_in_set(), + kill_set(), + phi_live_set()} }. -type alias_map() :: #{ func_id() => lbl2ss() }. @@ -135,19 +154,19 @@ killsets_fun(Blocks) -> killsets_blks([{Lbl,Blk}|Blocks], LiveIns0, Kills0, PhiLiveIns) -> {LiveIns,Kills} = killsets_blk(Lbl, Blk, LiveIns0, Kills0, PhiLiveIns), killsets_blks(Blocks, LiveIns, Kills, PhiLiveIns); -killsets_blks([], _LiveIns0, Kills, _PhiLiveIns) -> - Kills. +killsets_blks([], LiveIns, Kills, PhiLiveIns) -> + {LiveIns,Kills,PhiLiveIns}. killsets_blk(Lbl, #b_blk{is=Is0,last=L}=Blk, LiveIns0, Kills0, PhiLiveIns) -> Successors = beam_ssa:successors(Blk), Live1 = killsets_blk_live_outs(Successors, Lbl, LiveIns0, PhiLiveIns), Kills1 = Kills0#{{live_outs,Lbl}=>Live1}, Is = [L|reverse(Is0)], - {Live,Kills} = killsets_is(Is, Live1, Kills1, Lbl), + {Live,Kills} = killsets_is(Is, Live1, Kills1, Lbl, sets:new()), LiveIns = LiveIns0#{Lbl=>Live}, {LiveIns, Kills}. -killsets_is([#b_set{op=phi,dst=Dst}=I|Is], Live, Kills0, Lbl) -> +killsets_is([#b_set{op=phi,dst=Dst}=I|Is], Live, Kills0, Lbl, KilledInBlock0) -> %% The Phi uses are logically located in the predecessors, so we %% don't want them live in to this block. But to correctly %% calculate the aliasing of the arguments to the Phi in this @@ -156,21 +175,24 @@ killsets_is([#b_set{op=phi,dst=Dst}=I|Is], Live, Kills0, Lbl) -> Uses = beam_ssa:used(I), {_,LastUses} = killsets_update_live_and_last_use(Live, Uses), Kills = killsets_add_kills({phi,Dst}, LastUses, Kills0), - killsets_is(Is, sets:del_element(Dst, Live), Kills, Lbl); -killsets_is([I|Is], Live0, Kills0, Lbl) -> + KilledInBlock = sets:union(LastUses, KilledInBlock0), + killsets_is(Is, sets:del_element(Dst, Live), Kills, Lbl, KilledInBlock); +killsets_is([I|Is], Live0, Kills0, Lbl, KilledInBlock0) -> Uses = beam_ssa:used(I), {Live,LastUses} = killsets_update_live_and_last_use(Live0, Uses), + KilledInBlock = sets:union(LastUses, KilledInBlock0), case I of #b_set{dst=Dst} -> killsets_is(Is, sets:del_element(Dst, Live), - killsets_add_kills(Dst, LastUses, Kills0), Lbl); + killsets_add_kills(Dst, LastUses, Kills0), + Lbl, KilledInBlock); _ -> killsets_is(Is, Live, killsets_add_kills({terminator,Lbl}, LastUses, Kills0), - Lbl) + Lbl, KilledInBlock) end; -killsets_is([], Live, Kills, _) -> - {Live,Kills}. +killsets_is([], Live, Kills, Lbl, KilledInBlock) -> + {Live,killsets_add_kills({killed_in_block,Lbl}, KilledInBlock, Kills)}. killsets_update_live_and_last_use(Live0, Uses) -> foldl(fun(Use, {LiveAcc,LastAcc}=Acc) -> @@ -331,90 +353,131 @@ aa(Funs, KillsMap, StMap, FuncDb) -> %%% to detect incomplete information in a hypothetical %%% ssa_opt_alias_finish pass. %%% -aa_fixpoint(Funs, AAS=#aas{alias_map=AliasMap,call_args=CallArgs, - func_db=FuncDb}) -> - Order = aa_reverse_post_order(Funs, FuncDb), - aa_fixpoint(Order, Order, AliasMap, CallArgs, AAS, ?MAX_REPETITIONS). - -aa_fixpoint([F|Fs], Order, OldAliasMap, OldCallArgs, 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]), +aa_fixpoint(Funs, AAS=#aas{func_db=FuncDb}) -> + Order = aa_order(Funs, FuncDb), + ?DP("Traversal order:~n ~s~n", + [string:join([fn(F) || F <- Order], ",\n ")]), + aa_fixpoint(Order, reverse(Order), AAS, 1). + +aa_fixpoint([F|Fs], Order, + AAS0=#aas{func_db=FuncDb,st_map=StMap, + repeats=Repeats,run_count=RC}, NoofIters) -> + + ?DP("-= ~s =-~n", [fn(F)]), + %% If, when analysing another function, this function has been + %% scheduled for revisiting, we can remove it, as we otherwise + %% revisit it again in the next iteration. + AAS1 = AAS0#aas{caller=F,repeats=sets:del_element(F, Repeats)}, + St = #opt_st{ssa=_Is} = map_get(F, StMap), ?DP("code:~n~p.~n", [_Is]), AAS = aa_fun(F, St, AAS1), - ?DP("Done ~p/~p~n", [_N, _A]), - aa_fixpoint(Fs, Order, OldAliasMap, OldCallArgs, AAS, Limit); -aa_fixpoint([], Order, OldAliasMap, OldCallArgs, - #aas{alias_map=OldAliasMap,call_args=OldCallArgs, - func_db=FuncDb}=AAS, _Limit) -> - ?DP("**** End of iteration ~p ****~n", [_Limit]), - {StMap,_} = aa_update_annotations(Order, AAS), - {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, _OldCallArgs, - #aas{alias_map=AliasMap,call_args=CallArgs,repeats=Repeats}=AAS, - Limit) -> - ?DP("**** Things have changed, starting next iteration ****~n"), + ?DP("Done ~s~n", [fn(F)]), + case maps:get(F, RC, ?MAX_REPETITIONS) of + 0 -> + ?DP("**** End of iteration, too many iterations ****~n"), + {StMap, FuncDb}; + N -> + aa_fixpoint(Fs, Order, + AAS#aas{run_count=RC#{F => N - 1}}, NoofIters) + end; +aa_fixpoint([], Order, #aas{func_db=FuncDb,repeats=Repeats}=AAS, NoofIters) -> %% Following the depth first order, select those in Repeats. - NewOrder = [Id || Id <- Order, sets:is_element(Id, Repeats)], - aa_fixpoint(NewOrder, Order, AliasMap, CallArgs, - AAS#aas{repeats=sets:new()}, Limit - 1). + case [Id || Id <- Order, sets:is_element(Id, Repeats)] of + [] -> + ?DP("**** Fixpoint reached after ~p traversals ****~n", + [NoofIters]), + {StMap,_} = aa_update_annotations(Order, AAS), + {StMap, FuncDb}; + NewOrder -> + ?DP("**** Starting traversal ~p ****~n", [NoofIters + 1]), + aa_fixpoint(NewOrder, reverse(Order), + AAS#aas{repeats=sets:new()}, NoofIters + 1) + end. aa_fun(F, #opt_st{ssa=Linear0,args=Args}, - AAS0=#aas{alias_map=AliasMap0,call_args=CallArgs0, - func_db=FuncDb,kills=KillsMap,repeats=Repeats0}) -> + AAS0=#aas{alias_map=AliasMap0,analyzed=Analyzed,kills=KillsMap, + prune_strategy=StrategyMap0}) -> %% 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. {SS0,Cnt} = aa_init_fun_ss(Args, F, AAS0), - #{F:=Kills} = KillsMap, - {SS,#aas{call_args=CallArgs}=AAS} = - aa_blocks(Linear0, Kills, #{0=>SS0}, AAS0#aas{cnt=Cnt}), + #{F:={LiveIns,Kills,PhiLiveIns}} = KillsMap, + Strategy0 = maps:get(F, StrategyMap0, #{}), + {SS,Strategy,AAS1} = + aa_blocks(Linear0, LiveIns, PhiLiveIns, + Kills, #{0=>SS0}, Strategy0, AAS0#aas{cnt=Cnt}), + Lbl2SS0 = maps:get(F, AliasMap0, #{}), + Type2Status0 = maps:get(returns, Lbl2SS0, #{}), + Type2Status = maps:get(returns, SS, #{}), + AAS = case Type2Status0 =/= Type2Status of + true -> + aa_schedule_revisit_callers(F, AAS1); + false -> + AAS1 + end, AliasMap = AliasMap0#{ F => SS }, - PrevSS = maps:get(F, AliasMap0, #{}), - Repeats = case PrevSS =/= SS orelse CallArgs0 =/= CallArgs 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, - AAS#aas{alias_map=AliasMap,repeats=Repeats}. + StrategyMap = StrategyMap0#{F => Strategy}, + AAS#aas{alias_map=AliasMap,analyzed=sets:add_element(F, Analyzed), + prune_strategy=StrategyMap}. %% Main entry point for the alias analysis -aa_blocks([{?EXCEPTION_BLOCK,_}|Bs], Kills, Lbl2SS, AAS) -> +aa_blocks([{?EXCEPTION_BLOCK,_}|Bs], + LiveIns, PhiLiveIns, Kills, Lbl2SS, Strategy, AAS) -> %% Nothing happening in the exception block can propagate to the %% other block. - aa_blocks(Bs, Kills, Lbl2SS, AAS); -aa_blocks([{L,#b_blk{is=Is0,last=T}}|Bs0], Kills, Lbl2SS0, AAS0) -> + aa_blocks(Bs, LiveIns, PhiLiveIns, Kills, Lbl2SS, Strategy, AAS); +aa_blocks([{L,#b_blk{is=Is0,last=T}}|Bs0], + LiveIns, PhiLiveIns, Kills, Lbl2SS0, Strategy0, AAS0) -> #{L:=SS0} = Lbl2SS0, - ?DP("Block: ~p~nSS: ~p~n", [L, SS0]), + ?DP("Block: ~p~nSS:~n~s~n", [L, beam_ssa_ss:dump(SS0)]), {FullSS,AAS1} = aa_is(Is0, SS0, AAS0), #{{live_outs,L}:=LiveOut} = Kills, {Lbl2SS1,Successors} = aa_terminator(T, FullSS, Lbl2SS0), - PrunedSS = beam_ssa_ss:prune(LiveOut, FullSS), - Lbl2SS2 = aa_add_block_entry_ss(Successors, PrunedSS, Lbl2SS1), + %% In around 80% of the cases when prune is called, more than half + %% of the nodes in the sharing state database survive. Therefore + %% we default to a pruning strategy which removes nodes from the + %% database. But if only a few nodes survive it is faster to + %% recreate the pruned state from scratch. We therefore track the + %% result of a previous prune for the current basic block and + %% select the, hopefully, best pruning strategy. + Before = beam_ssa_ss:size(FullSS), + S = maps:get(L, Strategy0, del), + PrunedSS = + case S of + del -> + #{{killed_in_block,L}:=KilledInBlock} = Kills, + beam_ssa_ss:prune(LiveOut, KilledInBlock, FullSS); + add -> + beam_ssa_ss:prune_by_add(LiveOut, FullSS) + end, + After = beam_ssa_ss:size(PrunedSS), + Strategy = case After < (Before div 2) of + true when S =:= add -> + Strategy0; + false when S =:= del -> + Strategy0; + true -> + Strategy0#{L => add}; + false -> + Strategy0#{L => del} + end, + ?DP("Live out from ~p: ~p~n", [L, sets:to_list(LiveOut)]), + Lbl2SS2 = aa_add_block_entry_ss(Successors, L, PrunedSS, + LiveOut, LiveIns, PhiLiveIns, Lbl2SS1), Lbl2SS = aa_set_block_exit_ss(L, FullSS, Lbl2SS2), - aa_blocks(Bs0, Kills, Lbl2SS, AAS1); -aa_blocks([], _Kills, Lbl2SS, AAS) -> - {Lbl2SS,AAS}. + aa_blocks(Bs0, LiveIns, PhiLiveIns, Kills, Lbl2SS, Strategy, AAS1); +aa_blocks([], _LiveIns, _PhiLiveIns, _Kills, Lbl2SS, Strategy, AAS) -> + {Lbl2SS, Strategy, AAS}. aa_is([_I=#b_set{dst=Dst,op=Op,args=Args,anno=Anno0}|Is], SS0, AAS0) -> ?DP("I: ~p~n", [_I]), - SS1 = beam_ssa_ss:add_var(Dst, unique, SS0), {SS, AAS} = case Op of %% Instructions changing the alias status. {bif,Bif} -> - {aa_bif(Dst, Bif, Args, SS1, AAS0), AAS0}; + {aa_bif(Dst, Bif, Args, SS0, AAS0), AAS0}; bs_create_bin -> case Args of [#b_literal{val=Flag},_,Arg|_] when @@ -422,76 +485,82 @@ aa_is([_I=#b_set{dst=Dst,op=Op,args=Args,anno=Anno0}|Is], SS0, AAS0) -> case aa_all_dies([Arg], Dst, AAS0) of true -> %% Inherit the status of the argument - {aa_derive_from(Dst, Arg, SS1), AAS0}; + {aa_derive_from(Dst, Arg, SS0), AAS0}; false -> %% We alias with the surviving arg - {aa_set_aliased([Dst|Args], SS1), AAS0} + {aa_set_aliased([Dst|Args], SS0), AAS0} end; _ -> %% TODO: Too conservative? - {aa_set_aliased([Dst|Args], SS1), AAS0} + {aa_set_aliased([Dst|Args], SS0), AAS0} end; bs_extract -> - {aa_set_aliased([Dst|Args], SS1), AAS0}; + {aa_set_aliased([Dst|Args], SS0), AAS0}; bs_get_tail -> - {aa_set_aliased([Dst|Args], SS1), AAS0}; + {aa_set_aliased([Dst|Args], SS0), AAS0}; bs_match -> - {aa_set_aliased([Dst|Args], SS1), AAS0}; + {aa_set_aliased([Dst|Args], SS0), AAS0}; bs_start_match -> [_,Bin] = Args, - {aa_set_aliased([Dst,Bin], SS1), AAS0}; + {aa_set_aliased([Dst,Bin], SS0), AAS0}; build_stacktrace -> + SS1 = beam_ssa_ss:add_var(Dst, unique, SS0), %% 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 -> + SS1 = beam_ssa_ss:add_var(Dst, unique, SS0), aa_call(Dst, Args, Anno0, SS1, AAS0); 'catch_end' -> [_Tag,Arg] = Args, - {aa_derive_from(Dst, Arg, SS1), AAS0}; + {aa_derive_from(Dst, Arg, SS0), AAS0}; extract -> [Arg,_] = Args, - {aa_derive_from(Dst, Arg, SS1), AAS0}; + {aa_derive_from(Dst, Arg, SS0), AAS0}; get_hd -> [Arg] = Args, Type = maps:get(0, maps:get(arg_types, Anno0, #{0=>any}), any), - {aa_pair_extraction(Dst, Arg, hd, Type, SS1), AAS0}; + {aa_pair_extraction(Dst, Arg, hd, Type, SS0), AAS0}; get_map_element -> [Map,_Key] = Args, - {aa_map_extraction(Dst, Map, SS1, AAS0), AAS0}; + {aa_map_extraction(Dst, Map, SS0, AAS0), AAS0}; get_tl -> [Arg] = Args, Type = maps:get(0, maps:get(arg_types, Anno0, #{0=>any}), any), - {aa_pair_extraction(Dst, Arg, tl, Type, SS1), AAS0}; + {aa_pair_extraction(Dst, Arg, tl, Type, SS0), AAS0}; get_tuple_element -> [Arg,Idx] = Args, Types = maps:get(arg_types, Anno0, #{}), - {aa_tuple_extraction(Dst, Arg, Idx, Types, SS1), AAS0}; + {aa_tuple_extraction(Dst, Arg, Idx, Types, SS0), AAS0}; landingpad -> - {aa_set_aliased(Dst, SS1), AAS0}; + {aa_set_aliased(Dst, SS0), AAS0}; make_fun -> + SS1 = beam_ssa_ss:add_var(Dst, unique, SS0), [Callee|Env] = Args, aa_make_fun(Dst, Callee, Env, SS1, AAS0); peek_message -> - {aa_set_aliased(Dst, SS1), AAS0}; + {aa_set_aliased(Dst, SS0), AAS0}; phi -> - {aa_phi(Dst, Args, SS1, AAS0), AAS0}; + aa_phi(Dst, Args, SS0, AAS0); put_list -> + SS1 = beam_ssa_ss:add_var(Dst, unique, SS0), Types = aa_map_arg_to_type(Args, maps:get(arg_types, Anno0, #{})), {aa_construct_pair(Dst, Args, Types, SS1, AAS0), AAS0}; put_map -> - {aa_construct_term(Dst, Args, SS1, AAS0), AAS0}; + {aa_construct_term(Dst, Args, SS0, AAS0), AAS0}; put_tuple -> + SS1 = beam_ssa_ss:add_var(Dst, unique, SS0), Types = aa_map_arg_to_type(Args, maps:get(arg_types, Anno0, #{})), Values = lists:enumerate(0, Args), {aa_construct_tuple(Dst, Values, Types, SS1, AAS0), AAS0}; update_tuple -> - {aa_construct_term(Dst, Args, SS1, AAS0), AAS0}; + {aa_construct_term(Dst, Args, SS0, AAS0), AAS0}; update_record -> + SS1 = beam_ssa_ss:add_var(Dst, unique, SS0), [#b_literal{val=Hint},_Size,Src|Updates] = Args, RecordType = maps:get(arg_types, Anno0, #{}), ?DP("UPDATE RECORD dst: ~p, src: ~p, type:~p~n", @@ -521,47 +590,47 @@ aa_is([_I=#b_set{dst=Dst,op=Op,args=Args,anno=Anno0}|Is], SS0, AAS0) -> %% Instructions which don't change the alias status {float,_} -> - {SS1, AAS0}; + {SS0, AAS0}; {succeeded,_} -> - {SS1, AAS0}; + {SS0, AAS0}; bs_init_writable -> - {SS1, AAS0}; + {SS0, AAS0}; bs_test_tail -> - {SS1, AAS0}; + {SS0, AAS0}; executable_line -> - {SS1, AAS0}; + {SS0, AAS0}; has_map_field -> - {SS1, AAS0}; + {SS0, AAS0}; is_nonempty_list -> - {SS1, AAS0}; + {SS0, AAS0}; is_tagged_tuple -> - {SS1, AAS0}; + {SS0, AAS0}; kill_try_tag -> - {SS1, AAS0}; + {SS0, AAS0}; match_fail -> - {SS1, AAS0}; + {SS0, AAS0}; new_try_tag -> - {SS1, AAS0}; + {SS0, AAS0}; nif_start -> - {SS1, AAS0}; + {SS0, AAS0}; raw_raise -> - {SS1, AAS0}; + {SS0, AAS0}; recv_marker_bind -> - {SS1, AAS0}; + {SS0, AAS0}; recv_marker_clear -> - {SS1, AAS0}; + {SS0, AAS0}; recv_marker_reserve -> - {SS1, AAS0}; + {SS0, AAS0}; recv_next -> - {SS1, AAS0}; + {SS0, AAS0}; remove_message -> - {SS1, AAS0}; + {SS0, AAS0}; resume -> - {SS1, AAS0}; + {SS0, AAS0}; wait_timeout -> - {SS1, AAS0} + {SS0, AAS0} end, - ?DP("Post I: ~p.~n ~p~n", [_I, SS]), + ?DP("Post I: ~p.~n~s~n", [_I, beam_ssa_ss:dump(SS)]), aa_is(Is, SS, AAS); aa_is([], SS, AAS) -> {SS, AAS}. @@ -594,11 +663,25 @@ aa_set_block_exit_ss(ThisBlockLbl, SS, Lbl2SS) -> Lbl2SS#{ThisBlockLbl=>SS}. %% Extend the SS valid on entry to the blocks in the list with NewSS. -aa_add_block_entry_ss([?EXCEPTION_BLOCK|BlockLabels], NewSS, Lbl2SS) -> - aa_add_block_entry_ss(BlockLabels, NewSS, Lbl2SS); -aa_add_block_entry_ss([L|BlockLabels], NewSS, Lbl2SS) -> - aa_add_block_entry_ss(BlockLabels, NewSS, aa_merge_ss(L, NewSS, Lbl2SS)); -aa_add_block_entry_ss([], _, Lbl2SS) -> +aa_add_block_entry_ss([?EXCEPTION_BLOCK|BlockLabels], From, NewSS, + LiveOut, LiveIns, PhiLiveIns, Lbl2SS) -> + aa_add_block_entry_ss(BlockLabels, From, NewSS, LiveOut, + LiveIns, PhiLiveIns, Lbl2SS); +aa_add_block_entry_ss([L|BlockLabels], From, + NewSS, LiveOut, LiveIns, PhiLiveIns, Lbl2SS) -> + #{L:=LiveIn} = LiveIns, + PhiLiveIn = case PhiLiveIns of + #{{From,L}:=Vs} -> Vs; + #{} -> sets:new() + end, + AllLiveIn = sets:union(LiveIn, PhiLiveIn), + KilledOnEdge = sets:subtract(LiveOut, AllLiveIn), + ?DP("Killed on edge to ~p: ~p~n", [L, sets:to_list(KilledOnEdge)]), + ?DP("Live on edge to ~p: ~p~n", [L, sets:to_list(AllLiveIn)]), + Pruned = beam_ssa_ss:prune(AllLiveIn, KilledOnEdge, NewSS), + aa_add_block_entry_ss(BlockLabels, From, NewSS, LiveOut, LiveIns, + PhiLiveIns, aa_merge_ss(L, Pruned, Lbl2SS)); +aa_add_block_entry_ss([], _, _, _, _, _, Lbl2SS) -> Lbl2SS. %% Merge two sharing states when traversing the execution graph @@ -670,8 +753,7 @@ aa_update_annotations(Funs, #aas{alias_map=AliasMap0,st_map=StMap0}=AAS) -> foldl(fun(F, {StMapAcc,AliasMapAcc}) -> #{F:=Lbl2SS0} = AliasMapAcc, #{F:=OptSt0} = StMapAcc, - #b_local{name=#b_literal{val=_N},arity=_A} = F, - ?DP("Updating annotations for ~p/~p~n", [_N,_A]), + ?DP("Updating annotations for ~s~n", [fn(F)]), {OptSt,Lbl2SS} = aa_update_fun_annotation(OptSt0, Lbl2SS0, AAS#aas{caller=F}), @@ -786,7 +868,10 @@ aa_update_annotation_for_var(Var, Status, Anno0) -> ordsets:del_element(Var, Unique0)}; unique -> {ordsets:del_element(Var, Aliased0), - ordsets:add_element(Var, Unique0)} + ordsets:add_element(Var, Unique0)}; + no_info -> + {ordsets:del_element(Var, Aliased0), + ordsets:del_element(Var, Unique0)} end, Anno1 = case Aliased of [] -> @@ -805,7 +890,7 @@ aa_update_annotation_for_var(Var, Status, Anno0) -> %% instruction. dies_at(Var, #b_set{dst=Dst}, AAS) -> #aas{caller=Caller,kills=KillsMap} = AAS, - KillMap = map_get(Caller, KillsMap), + {_LiveIns,KillMap,_PhiLiveIns} = map_get(Caller, KillsMap), sets:is_element(Var, map_get(Dst, KillMap)). aa_set_aliased(Args, SS) -> @@ -853,7 +938,7 @@ aa_alias_surviving_args1([], SS, _KillSet) -> %% Return the kill-set for the instruction defining Dst. aa_killset_for_instr(Dst, #aas{caller=Caller,kills=Kills}) -> - KillMap = map_get(Caller, Kills), + {_LiveIns,KillMap,_PhiLiveIns} = map_get(Caller, Kills), map_get(Dst, KillMap). %% Predicate to check if all variables in `Vars` dies at `Where`. @@ -985,7 +1070,7 @@ aa_construct_tuple(Dst, IdxValues, Types, SS, AAS) -> killed=>aa_dies(V, Types, KillSet), plain=>aa_is_plain_value(V, Types)} || {Idx,V} <- IdxValues]]), - ?DP("~p~n", [SS]), + ?DP("~s~n", [beam_ssa_ss:dump(SS)]), aa_build_tuple_or_pair(Dst, IdxValues, Types, KillSet, SS, []). aa_build_tuple_or_pair(Dst, [{Idx,#b_literal{val=Lit}}|IdxValues], Types, @@ -1019,7 +1104,8 @@ aa_build_tuple_or_pair(Dst, [], _Types, _KillSet, SS, Sources) -> aa_construct_pair(Dst, Args0, Types, SS, AAS) -> KillSet = aa_killset_for_instr(Dst, AAS), [Hd,Tl] = Args0, - ?DP("Constructing pair in ~p~n from ~p and ~p~n~p~n", [Dst,Hd,Tl,SS]), + ?DP("Constructing pair in ~p~n from ~p and ~p~n~s~n", + [Dst, Hd, Tl, beam_ssa_ss:dump(SS)]), Args = [{hd,Hd},{tl,Tl}], aa_build_tuple_or_pair(Dst, Args, Types, KillSet, SS, []). @@ -1033,8 +1119,9 @@ aa_bif(Dst, element, [#b_literal{val=Idx},Tuple], SS, _AAS) %% The element bif is always rewritten to a get_tuple_element %% instruction when the index is an integer and the second %% argument is a known to be a tuple. Therefore this code is only - %% reached when the type of is unknown, thus there is no point in - %% trying to provide aa_tuple_extraction/5 with type information. + %% reached when the type of Tuple is unknown, thus there is no + %% point in trying to provide aa_tuple_extraction/5 with type + %% information. aa_tuple_extraction(Dst, Tuple, #b_literal{val=Idx-1}, #{}, SS); aa_bif(Dst, element, [#b_literal{},Tuple], SS, _AAS) -> %% This BIF will fail, but in order to avoid any later transforms @@ -1045,21 +1132,24 @@ aa_bif(Dst, element, [#b_var{},Tuple], SS, _AAS) -> aa_bif(Dst, hd, [Pair], SS, _AAS) -> %% The hd bif is always rewritten to a get_hd instruction when the %% argument is known to be a pair. Therefore this code is only - %% reached when the type of is unknown, thus there is no point in - %% trying to provide aa_pair_extraction/5 with type information. + %% reached when the type of Pair is unknown, thus there is no + %% point in trying to provide aa_pair_extraction/5 with type + %% information. aa_pair_extraction(Dst, Pair, hd, SS); aa_bif(Dst, tl, [Pair], SS, _AAS) -> %% The tl bif is always rewritten to a get_tl instruction when the %% argument is known to be a pair. Therefore this code is only - %% reached when the type of is unknown, thus there is no point in - %% trying to provide aa_pair_extraction/5 with type information. + %% reached when the type of Pair is unknown, thus there is no + %% point in trying to provide aa_pair_extraction/5 with type + %% information. aa_pair_extraction(Dst, Pair, tl, SS); aa_bif(Dst, map_get, [_Key,Map], SS, AAS) -> aa_map_extraction(Dst, Map, SS, AAS); -aa_bif(Dst, binary_part, Args, SS, _AAS) -> +aa_bif(Dst, binary_part, Args, SS0, _AAS) -> %% bif:binary_part/{2,3} is the only guard bif which could lead to %% aliasing, it extracts a sub-binary with a reference to its %% argument. + SS = beam_ssa_ss:add_var(Dst, unique, SS0), aa_set_aliased([Dst|Args], SS); aa_bif(Dst, Bif, Args, SS, _AAS) -> Arity = length(Args), @@ -1076,28 +1166,32 @@ aa_bif(Dst, Bif, Args, SS, _AAS) -> aa_set_aliased([Dst|Args], SS) end. -aa_phi(Dst, Args0, SS0, AAS) -> +aa_phi(Dst, Args0, SS0, #aas{cnt=Cnt0}=AAS) -> + %% TODO: Use type info? Args = [V || {V,_} <- Args0], - SS = aa_alias_surviving_args(Args, {phi,Dst}, SS0, AAS), - aa_derive_from(Dst, Args, SS). + ?DP("Phi~n"), + SS1 = aa_alias_surviving_args(Args, {phi,Dst}, SS0, AAS), + ?DP(" after aa_alias_surviving_args:~n~s.~n", [beam_ssa_ss:dump(SS1)]), + {SS,Cnt} = beam_ssa_ss:phi(Dst, Args, SS1, Cnt0), + {SS,AAS#aas{cnt=Cnt}}. aa_call(Dst, [#b_local{}=Callee|Args], Anno, SS0, - #aas{alias_map=AliasMap,st_map=StMap,cnt=Cnt0}=AAS0) -> - #b_local{name=#b_literal{val=_N},arity=_A} = Callee, - ?DP("A Call~n callee: ~p/~p~n args: ~p~n", [_N, _A, Args]), - case is_map_key(Callee, AliasMap) of + #aas{alias_map=AliasMap,analyzed=Analyzed, + st_map=StMap,cnt=Cnt0}=AAS0) -> + ?DP("A Call~n callee: ~s~n args: ~p~n", [fn(Callee), Args]), + ?DP(" caller args: ~p~n", [Args]), + SS1 = aa_alias_surviving_args(Args, Dst, SS0, AAS0), + ?DP(" caller ss before call:~n~s.~n", [beam_ssa_ss:dump(SS1)]), + #aas{alias_map=AliasMap} = AAS = + aa_add_call_info(Callee, Args, SS1, AAS0), + case sets:is_element(Callee, Analyzed) of true -> - ?DP(" The callee is known~n"), + ?DP(" The callee has been analyzed~n"), #opt_st{args=_CalleeArgs} = map_get(Callee, StMap), ?DP(" callee args: ~p~n", [_CalleeArgs]), - ?DP(" caller args: ~p~n", [Args]), - SS1 = aa_alias_surviving_args(Args, Dst, SS0, AAS0), - ?DP(" caller ss before call:~n ~p.~n", [SS1]), - #aas{alias_map=AliasMap} = AAS = - aa_add_call_info(Callee, Args, SS1, AAS0), #{Callee:=#{0:=_CalleeSS}=Lbl2SS} = AliasMap, - ?DP(" callee ss: ~p~n", [_CalleeSS]), - ?DP(" caller ss after call: ~p~n", [SS1]), + ?DP(" callee ss:~n~s~n", [beam_ssa_ss:dump(_CalleeSS)]), + ?DP(" caller ss after call:~n~s~n", [beam_ssa_ss:dump(SS1)]), ReturnStatusByType = maps:get(returns, Lbl2SS, #{}), ?DP(" status by type: ~p~n", [ReturnStatusByType]), @@ -1111,12 +1205,14 @@ aa_call(Dst, [#b_local{}=Callee|Args], Anno, SS0, ?DP(" result status: ~p~n", [ResultStatus]), {SS,Cnt} = beam_ssa_ss:set_call_result(Dst, ResultStatus, SS1, Cnt0), - ?DP("~p~n", [SS]), + ?DP("~s~n", [beam_ssa_ss:dump(SS)]), {SS, AAS#aas{cnt=Cnt}}; false -> - %% We don't know anything about the function, don't change - %% the status of any variables - {SS0, AAS0} + ?DP(" The callee has not been analyzed~n"), + %% We don't know anything about the function, so + %% explicitly mark that we don't know anything about the + %% result. + {beam_ssa_ss:set_status(Dst, no_info, SS0), AAS} end; aa_call(_Dst, [#b_remote{mod=#b_literal{val=erlang}, name=#b_literal{val=exit}, @@ -1136,21 +1232,25 @@ aa_call(Dst, [_Callee|Args], _Anno, SS0, AAS) -> aa_add_call_info(Callee, Args, SS0, #aas{call_args=InInfo0,caller=_Caller}=AAS) -> #{Callee := InStatus0} = InInfo0, - ?DBG(#b_local{name=#b_literal{val=_CN},arity=_CA} = _Caller), - ?DBG(#b_local{name=#b_literal{val=_N},arity=_A} = Callee), - ?DP("Adding call info for ~p/~p when called by ~p/~p~n" - " args: ~p.~n ss:~p.~n", [_N,_A,_CN,_CA,Args,SS0]), + ?DP("Adding call info for ~s when called by ~s~n" + " args: ~p.~n ss:~n~s.~n", + [fn(Callee), fn(_Caller), Args, beam_ssa_ss:dump(SS0)]), InStatus = beam_ssa_ss:merge_in_args(Args, InStatus0, SS0), - ?DP(" orig in-info: ~p.~n", [InStatus0]), - ?DP(" updated in-info for ~p/~p:~n ~p.~n", [_N,_A,InStatus]), - InInfo = InInfo0#{Callee => InStatus}, - AAS#aas{call_args=InInfo}. + ?DP(" orig in-info:~n ~p.~n", [InStatus0]), + ?DP(" updated in-info for ~s:~n ~p.~n", [fn(Callee), InStatus]), + case InStatus0 =/= InStatus of + true -> + InInfo = InInfo0#{Callee => InStatus}, + aa_schedule_revisit(Callee, AAS#aas{call_args=InInfo}); + false -> + AAS + end. aa_init_fun_ss(Args, FunId, #aas{call_args=Info,st_map=StMap}) -> #{FunId:=ArgsStatus} = Info, #{FunId:=#opt_st{cnt=Cnt}} = StMap, - ?DP("aa_init_fun_ss: ~p~n args: ~p~n status: ~p~n cnt: ~p~n", - [FunId,Args,ArgsStatus,Cnt]), + ?DP("aa_init_fun_ss: ~s~n args: ~p~n status: ~p~n cnt: ~p~n", + [fn(FunId), Args, ArgsStatus, Cnt]), beam_ssa_ss:new(Args, ArgsStatus, Cnt). %% Pair extraction. @@ -1159,10 +1259,10 @@ aa_pair_extraction(Dst, Pair, Element, SS) -> aa_pair_extraction(Dst, #b_var{}=Pair, Element, Type, SS) -> IsPlainValue = case {Type,Element} of - {#t_cons{type=Ty},hd} -> - aa_is_plain_type(Ty); {#t_cons{terminator=Ty},tl} -> aa_is_plain_type(Ty); + {#t_cons{type=Ty},hd} -> + aa_is_plain_type(Ty); _ -> %% There is no type information, %% conservatively assume this isn't a plain @@ -1190,7 +1290,7 @@ aa_tuple_extraction(Dst, #b_var{}=Tuple, #b_literal{val=I}, Types, SS) -> TupleType = maps:get(0, Types, any), TypeIdx = I+1, %% In types tuple indices starting at zero. IsPlainValue = case TupleType of - #t_tuple{elements=#{TypeIdx:=T}} -> + #t_tuple{elements=#{TypeIdx:=T}} when T =/= any -> aa_is_plain_type(T); _ -> %% There is no type information, @@ -1201,19 +1301,19 @@ aa_tuple_extraction(Dst, #b_var{}=Tuple, #b_literal{val=I}, Types, SS) -> ?DP("tuple-extraction dst:~p, tuple: ~p, idx: ~p,~n" " type: ~p,~n plain: ~p~n", [Dst, Tuple, I, TupleType, IsPlainValue]), - if IsPlainValue -> + case IsPlainValue of + true -> %% A plain value was extracted, it doesn't change the %% alias status of Dst nor the tuple. SS; - true -> + false -> beam_ssa_ss:extract(Dst, Tuple, I, SS) end; aa_tuple_extraction(_, #b_literal{}, _, _, SS) -> SS. aa_make_fun(Dst, Callee=#b_local{name=#b_literal{}}, - Env0, SS0, - AAS0=#aas{call_args=Info0,repeats=Repeats0}) -> + Env0, SS0, AAS0=#aas{call_args=Info0}) -> %% 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 @@ -1227,21 +1327,24 @@ aa_make_fun(Dst, Callee=#b_local{name=#b_literal{}}, 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 + AAS1 = case PrevStatus =/= Status of + true -> + %% We have new information for the callee, we + %% have to revisit it. + aa_schedule_revisit(Callee, AAS0); + false -> + AAS0 end, - AAS = AAS0#aas{call_args=Info,repeats=Repeats}, - {SS, AAS}. - -aa_reverse_post_order(Funs, FuncDb) -> - %% In order to produce a reverse post order of the call graph, we - %% have to make sure all exported functions without local callers - %% are visited before exported functions with local callers. + {SS, AAS1#aas{call_args=Info}}. + +aa_order(Funs, FuncDb) -> + %% Information about the alias status flows from callers to + %% callees and then back through the status of the result. In + %% order to avoid the most pessimistic estimate of the aliasing + %% status we want to process callers before callees with one + %% exception: zero-arity functions are always processed before + %% their callers as they are likely to give the most precise alias + %% information to their callers. IsExportedNoLocalCallers = fun (F) -> #{ F := #func_info{exported=E,in=In} } = FuncDb, @@ -1254,9 +1357,12 @@ aa_reverse_post_order(Funs, FuncDb) -> #{ F := #func_info{exported=E,in=In} } = FuncDb, E andalso In =/= [] end, + + ZeroArityFunctions = lists:sort([ F || #b_local{arity=0}=F <- Funs]), ExportedLocalCallers = lists:sort([ F || F <- Funs, IsExportedLocalCallers(F)]), - aa_reverse_post_order(ExportedNoLocalCallers, ExportedLocalCallers, + aa_reverse_post_order(ZeroArityFunctions ++ ExportedNoLocalCallers, + ExportedLocalCallers, sets:new(), FuncDb). aa_reverse_post_order([F|Work], Next, Seen, FuncDb) -> @@ -1283,6 +1389,22 @@ aa_reverse_post_order([], [], _Seen, _FuncDb) -> aa_reverse_post_order([], Next, Seen, FuncDb) -> aa_reverse_post_order(Next, [], Seen, FuncDb). +%% Schedule a function for revisting. +aa_schedule_revisit(FuncId, + #aas{func_db=FuncDb,st_map=StMap,repeats=Repeats}=AAS) + when is_map_key(FuncId, FuncDb), is_map_key(FuncId, StMap) -> + ?DP("Scheduling ~s for revisit~n", [fn(FuncId)]), + AAS#aas{repeats=sets:add_element(FuncId, Repeats)}; +aa_schedule_revisit(_, AAS) -> + AAS. + +%% Schedule the callers of a function for revisting. +aa_schedule_revisit_callers(FuncId, #aas{func_db=FuncDb}=AAS) -> + #{FuncId:=#func_info{in=In}} = FuncDb, + ?DP("Scheduling callers of ~s for revisit: ~s.~n", + [fn(FuncId), string:join([fn(I) || I <- In], ", ")]), + foldl(fun aa_schedule_revisit/2, AAS, In). + expand_record_update(#opt_st{ssa=Linear0,cnt=First,anno=Anno0}=OptSt) -> {Linear,Cnt} = eru_blocks(Linear0, First), Anno = Anno0#{orig_cnt=>First}, diff --git a/lib/compiler/src/beam_ssa_alias_debug.hrl b/lib/compiler/src/beam_ssa_alias_debug.hrl new file mode 100644 index 000000000000..1e3fc43002e0 --- /dev/null +++ b/lib/compiler/src/beam_ssa_alias_debug.hrl @@ -0,0 +1,43 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2024. 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% +%% +%% The beam_ssa_alias and beam_ssa_ss modules are both used by the +%% alias analysis pass of the compiler, where beam_ssa_ss provides the +%% underlying database manipulated by the beam_ssa_alias +%% module. Debugging beam_ssa_alias is simplified when the module can +%% print out the internal state of beam_ssa_ss, but as that code is +%% redundant otherwise it is ifdefed-out, this header exist in order +%% to avoid having to modify multiple modules when toggling debugging +%% traces for beam_ssa_alias and beam_ssa_ss. + +%% Uncomment the following to get trace printouts. + +%% -define(DEBUG_ALIAS, true). % For beam_ssa_alias + +%% -define(DEBUG_SS, true). % For beam_ssa_ss + +%% Uncomment the following to check that all invariants for the state +%% hold. These checks are expensive and not enabled by default. + +%% -define(SS_EXTRA_ASSERTS, true). + +-if(defined(DEBUG_ALIAS) orelse defined(DEBUG_SS) + orelse defined(SS_EXTRA_ASSERTS)). +-define(PROVIDE_DUMP, true). +-endif. diff --git a/lib/compiler/src/beam_ssa_check.erl b/lib/compiler/src/beam_ssa_check.erl index 29a1264daf6c..0bc4ed9cb65e 100644 --- a/lib/compiler/src/beam_ssa_check.erl +++ b/lib/compiler/src/beam_ssa_check.erl @@ -159,6 +159,12 @@ op_check([set,Result,{{atom,_,bif},{atom,_,Op}}|PArgs], PAnno, [Op, Result, Dst, PArgs, AArgs, _I]), Env = op_check_call(Op, Result, Dst, PArgs, AArgs, Env0), check_annos(PAnno, AAnno, Env); +op_check([set,Result,{{atom,_,succeeded},{atom,_,Kind}}|PArgs], PAnno, + #b_set{dst=Dst,args=AArgs,op={succeeded,Kind},anno=AAnno}=_I, Env0) -> + ?DP("trying succeed ~p:~n res: ~p <-> ~p~n args: ~p <-> ~p~n i: ~p~n", + [Kind, Result, Dst, PArgs, AArgs, _I]), + Env = op_check_call(dont_care, Result, Dst, PArgs, AArgs, Env0), + check_annos(PAnno, AAnno, Env); op_check([none,{atom,_,ret}|PArgs], PAnno, #b_ret{arg=AArg,anno=AAnno}=_I, Env) -> ?DP("trying return:, arg: ~p <-> ~p~n i: ~p~n", diff --git a/lib/compiler/src/beam_ssa_destructive_update.erl b/lib/compiler/src/beam_ssa_destructive_update.erl index c6e9d2875acf..1cb92440f866 100644 --- a/lib/compiler/src/beam_ssa_destructive_update.erl +++ b/lib/compiler/src/beam_ssa_destructive_update.erl @@ -92,7 +92,7 @@ -export([opt/2]). --import(lists, [foldl/3, foldr/3, keysort/2, reverse/1]). +-import(lists, [foldl/3, foldr/3, keysort/2, splitwith/2, reverse/1]). -include("beam_ssa_opt.hrl"). -include("beam_types.hrl"). @@ -830,19 +830,18 @@ patch_is([I0=#b_set{dst=Dst}|Rest], PD0, Cnt0, Acc, BlockAdditions0) PD = maps:remove(Dst, PD0), case Patches of [{opargs,Dst,_,_,_}|_] -> + Splitter = fun({opargs,D,_Idx,_Lit,_Element}) -> + Dst =:= D; + (_) -> + false + end, + {OpArgs0, Other} = splitwith(Splitter, Patches), OpArgs = [{Idx,Lit,Element} - || {opargs,D,Idx,Lit,Element} <- Patches, Dst =:= D], - Forced = [ F || {force_copy,_}=F <- Patches], - I1 = case Forced of - [] -> - I0; - _ -> - no_reuse(I0) - end, - 0 = length(Patches) - length(Forced) - length(OpArgs), + || {opargs,_D,Idx,Lit,Element} <- OpArgs0], Ps = keysort(1, OpArgs), - {Is,Cnt} = patch_opargs(I1, Ps, Cnt0), - patch_is(Rest, PD, Cnt, Is++Acc, BlockAdditions0); + {Is,Cnt} = patch_opargs(I0, Ps, Cnt0), + patch_is([hd(Is)|Rest], PD#{Dst=>Other}, Cnt, + tl(Is)++Acc, BlockAdditions0); [{appendable_binary,Dst,#b_literal{val= <<>>}=Lit}] -> %% Special case for when the first fragment is a literal %% <<>> and it has to be replaced with a bs_init_writable. diff --git a/lib/compiler/src/beam_ssa_ss.erl b/lib/compiler/src/beam_ssa_ss.erl index ae4fc32d35ea..bf5fd649e040 100644 --- a/lib/compiler/src/beam_ssa_ss.erl +++ b/lib/compiler/src/beam_ssa_ss.erl @@ -43,38 +43,36 @@ merge_in_args/3, new/0, new/3, - prune/2, + phi/4, + prune/3, + prune_by_add/2, set_call_result/4, set_status/3, + size/1, variables/1]). -include("beam_ssa.hrl"). -include("beam_types.hrl"). +-include("beam_ssa_alias_debug.hrl"). + +-ifdef(PROVIDE_DUMP). +-export([dump/1]). +-endif. -import(lists, [foldl/3]). -define(ARGS_DEPTH_LIMIT, 4). -define(SS_DEPTH_LIMIT, 30). -%% -define(DEBUG, true). - --ifdef(DEBUG). +-ifdef(DEBUG_SS). -define(DP(FMT, ARGS), io:format(FMT, ARGS)). -define(DP(FMT), io:format(FMT)). --define(DBG(STMT), STMT). -else. -define(DP(FMT, ARGS), skip). -define(DP(FMT), skip). --define(DBG(STMT), skip). -endif. - -%% Uncomment the following to check that all invariants for the state -%% hold. These checks are expensive and not enabled by default. - -%% -define(EXTRA_ASSERTS, true). - --ifdef(EXTRA_ASSERTS). +-ifdef(SS_EXTRA_ASSERTS). -define(assert_state(State), assert_state(State)). -define(ASSERT(Assert), Assert). -else. @@ -83,7 +81,7 @@ -endif. -type sharing_state() :: any(). % A beam_digraph graph. --type sharing_status() :: 'unique' | 'aliased'. +-type sharing_status() :: 'unique' | 'aliased' | 'no_info'. -type element() :: 'hd' | 'tl' | non_neg_integer(). -spec add_var(beam_ssa:b_var(), sharing_status(), sharing_state()) -> @@ -124,15 +122,14 @@ add_edge(State, Src, Dst, Lbl) -> -spec derive_from(beam_ssa:b_var(), beam_ssa:b_var(), sharing_state()) -> sharing_state(). derive_from(Dst, Src, State) -> - ?DP("Deriving ~p from ~p~nSS:~p~n", [Dst,Src,State]), + ?DP("Deriving ~p from ~p~nSS:~n~s~n", [Dst, Src, dump(State)]), ?assert_state(State), - ?ASSERT(assert_variable_exists(Dst, State)), - ?ASSERT(assert_variable_exists(Src, State)), - case {beam_digraph:vertex(State, Dst),beam_digraph:vertex(State, Src)} of + case {beam_digraph:vertex(State, Dst, unique), + beam_digraph:vertex(State, Src, plain)} of + {_,plain} -> + State; {aliased,_} -> - %% Nothing to do, already aliased. This can happen when - %% handling Phis, no propagation to the source should be - %% done. + %% Nothing to do, already aliased. State; {_,aliased} -> %% The source is aliased, the destination will become aliased. @@ -147,33 +144,43 @@ derive_from(Dst, Src, State) -> false -> %% Source is not aliased and has not been embedded %% in a term, record that it now is. - ?assert_state(add_edge(State, Src, Dst, embed)) + State1 = case beam_digraph:has_vertex(State, Dst) of + true -> + State; + false -> + beam_ssa_ss:add_var(Dst, unique, State) + end, + ?assert_state(add_edge(State1, Src, Dst, embed)) end end. -spec embed_in(beam_ssa:b_var(), [{element(),beam_ssa:b_var()}], sharing_state()) -> sharing_state(). embed_in(Dst, Elements, State0) -> - ?DP("Embedding ~p into ~p~nSS:~p~n", [Elements,Dst,State0]), + ?DP("Embedding ~p into ~p~nSS:~n~s~n", [Elements, Dst, dump(State0)]), ?assert_state(State0), ?ASSERT(assert_variable_exists(Dst, State0)), - ?ASSERT([assert_variable_exists(Src, State0) - || {#b_var{},Src} <- Elements]), foldl(fun({Element,Src}, Acc) -> add_embedding(Dst, Src, Element, Acc) end, State0, Elements). add_embedding(Dst, Src, Element, State0) -> ?DP("add_embedding(~p, ~p, ~p, ...)~n", [Dst,Src,Element]), - - %% Create a node for literals as it isn't in the graph. + %% Create a node for literals and plain values as they are not in + %% the graph. The node is needed to record the status of the + %% element. State1 = case Src of plain -> beam_digraph:add_vertex(State0, Src, unique); #b_literal{} -> beam_digraph:add_vertex(State0, Src, unique); _ -> - State0 + case beam_digraph:has_vertex(State0, Src) of + true -> + State0; + false -> + beam_digraph:add_vertex(State0, Src, unique) + end end, %% Create the edge, this is done regardless of the aliasing status @@ -206,10 +213,7 @@ add_embedding(Dst, Src, Element, State0) -> extract(Dst, Src, Element, State) -> ?DP("Extracting ~p[~p] into ~p~n", [Src,Element,Dst]), ?assert_state(State), - ?ASSERT(assert_variable_exists(Dst, State)), - ?ASSERT(assert_variable_exists(Src, State)), - - case beam_digraph:vertex(State, Src) of + case beam_digraph:vertex(State, Src, plain) of aliased -> %% The pair/tuple is aliased, so what is extracted will be aliased. ?assert_state(set_status(Dst, aliased, State)); @@ -218,7 +222,17 @@ extract(Dst, Src, Element, State) -> OutEdges = beam_digraph:out_edges(State, Src), ?ASSERT(true = is_integer(Element) orelse (Element =:= hd) orelse (Element =:= tl)), - extract_element(Dst, Src, Element, OutEdges, State) + ?DP("dst: ~p, src: ~p, e: ~p, out-edges: ~p~n", + [Dst, Src, Element, OutEdges]), + extract_element(Dst, Src, Element, OutEdges, State); + no_info -> + ?assert_state(set_status(Dst, no_info, State)); + plain -> + %% Extracting from a plain value is not possible, but the + %% alias analysis pass can sometimes encounter it when no + %% type information is available. Conservatively set the + %% result to aliased. + ?assert_state(set_status(Dst, aliased, State)) end. %% Note that extract_element/5 will never be given an out edge with a @@ -236,42 +250,66 @@ extract_element(Dst, Src, Element, [], State0) -> %% aliased (checked in extract/4). It could be that we're about to %% extract an element which is known to be aliased. ?DP(" the element has not been extracted so far~n"), - State = ?assert_state(add_edge(State0, Src, Dst, {extract,Element})), + State1 = beam_ssa_ss:add_var(Dst, unique, State0), + State = ?assert_state(add_edge(State1, Src, Dst, {extract,Element})), extract_status_for_element(Element, Src, Dst, State). -extract_status_for_element(Element, Src, Dst, State) -> +extract_status_for_element(Element, Src, Dst, State0) -> ?DP(" extract_status_for_element(~p, ~p)~n", [Element, Src]), - InEdges = beam_digraph:in_edges(State, Src), - extract_status_for_element(InEdges, Element, Src, Dst, State). - -extract_status_for_element([{N,_,{embed,Element}}|_InEdges], - Element, _Src, Dst, State0) -> - ?DP(" found new source ~p~n", [N]), - ?DP(" SS ~p~n", [State0]), - ?DP(" status ~p~n", [beam_digraph:vertex(State0, N)]), - State = set_status(Dst, beam_digraph:vertex(State0, N), State0), - ?DP(" Returned SS ~p~n", [State]), - ?assert_state(State); -extract_status_for_element([{N,_,{extract,SrcElement}}|InEdges], - Element, Src, Dst, State0) -> - ?DP(" found source: ~p[~p]~n", [N,SrcElement]), - Origs = [Var || {Var,_,{embed,SE}} <- beam_digraph:in_edges(State0, N), - SrcElement =:= SE], - ?DP(" original var: ~p~n", [Origs]), - case Origs of - [] -> + InEdges = beam_digraph:in_edges(State0, Src), + ?DP(" in-edges: ~p~n", [InEdges]), + Embeddings = [Var || {Var,_,{embed,SE}} <- InEdges, Element =:= SE], + Extracts = [Ex || {_,_,{extract,_}}=Ex <- InEdges], + case {Embeddings,Extracts} of + {[],[]} -> + %% Nothing found, the status will be aliased. ?DP(" no known source~n"), - extract_status_for_element(InEdges, Element, Src, Dst, State0); - [Orig] -> - extract_status_for_element(Element, Orig, Dst, State0) + ?DP(" status of ~p will be aliased~n", [Dst]), + ?assert_state(set_status(Dst, aliased, State0)); + {[Var],[]} -> + %% The element which is looked up is an embedding. + ?DP(" found embedding~n"), + ?DP(" the source is ~p~n", [Var]), + ?DP(" SS~n~s~n", [dump(State0)]), + ?DP(" status ~p~n", [beam_digraph:vertex(State0, Var, unique)]), + State = set_status(Dst, beam_digraph:vertex(State0, Var, unique), + State0), + ?DP(" Returned SS~n~s~n", [dump(State)]), + ?assert_state(State); + {[], [{Aggregate,_Dst,{extract,E}}]} -> + %% We are trying extract from an extraction. + S = get_status_of_extracted_element(Aggregate, [E,Element], State0), + ?DP(" status of ~p will be ~p~n", [Dst, S]), + ?assert_state(set_status(Dst, S, State0)) + end. + + +get_status_of_extracted_element(Aggregate, [First|Rest]=Elements, State) -> + ?DP(" ~s(~p, ~p, ...)~n", [?FUNCTION_NAME, Aggregate, Elements]), + %% This clause will only be called when there is a chain of + %% extracts from unique aggregates. This implies that when the + %% chain is traced backwards, no aliased aggregates will be found, + %% but in case that invariant is ever broken, assert. + ?ASSERT(unique = beam_digraph:vertex(State, Aggregate, unique)), + ?DP(" aggregate is unique~n"), + InEdges = beam_digraph:in_edges(State, Aggregate), + Embeddings = [Src || {Src,_,{embed,E}} <- InEdges, First =:= E], + Extracts = [{Src,E} || {Src,_,{extract,E}} <- InEdges], + ?DP(" embeddings ~p~n", [Embeddings]), + ?DP(" extracts ~p~n", [Extracts]), + case {Embeddings,Extracts} of + {[Embedding],[]} -> + get_status_of_extracted_element(Embedding, Rest, State); + {[],[{Extract,E}]} -> + get_status_of_extracted_element(Extract, [E|Elements], State); + {[],[]} -> + aliased end; -extract_status_for_element([_Edge|InEdges], Element, Src, Dst, State) -> - ?DP(" ignoring in-edge ~p~n", [_Edge]), - extract_status_for_element(InEdges, Element, Src, Dst, State); -extract_status_for_element([], _Element, _Src, Dst, State) -> - %% Nothing found, the status will be aliased. - ?DP(" status of ~p will be aliased~n", [Dst]), - ?assert_state(set_status(Dst, aliased, State)). +get_status_of_extracted_element(Aggregate, [], State) -> + ?DP(" ~s(~p, [], ...)~n", [?FUNCTION_NAME, Aggregate]), + S = beam_digraph:vertex(State, Aggregate, unique), + ?DP(" bottomed out, status is ~p~n", [S]), + S. %% A cut-down version of merge/2 which only considers variables in %% Main and whether they have been aliased in Other. @@ -300,7 +338,7 @@ forward_status(Main, Other) -> -spec get_status(beam_ssa:b_var(), sharing_state()) -> sharing_status(). get_status(V=#b_var{}, State) -> - beam_digraph:vertex(State, V). + beam_digraph:vertex(State, V, unique). -spec merge(sharing_state(), sharing_state()) -> sharing_state(). merge(StateA, StateB) -> @@ -317,9 +355,9 @@ merge(StateA, StateB) -> end, ?DP("Merging Small into Large~nLarge:~n"), ?DP("Small:~n"), - ?DBG(dump(Small)), + ?DP(dump(Small)), ?DP("Large:~n"), - ?DBG(dump(Large)), + ?DP(dump(Large)), R = merge(Large, Small, beam_digraph:vertices(Small), sets:new(), sets:new()), ?assert_state(R). @@ -327,12 +365,7 @@ merge(StateA, StateB) -> merge(Dest, Source, [{V,VStatus}|Vertices], Edges0, Forced) -> Edges = accumulate_edges(V, Source, Edges0), - DestStatus = case beam_digraph:has_vertex(Dest, V) of - true -> - beam_digraph:vertex(Dest, V); - false -> - false - end, + DestStatus = beam_digraph:vertex(Dest, V, false), case {DestStatus,VStatus} of {Status,Status} -> %% Same status in both states. @@ -348,7 +381,14 @@ merge(Dest, Source, [{V,VStatus}|Vertices], Edges0, Forced) -> {aliased,unique} -> %% V has to be revisited and non-aliased copied parts will %% be aliased. - merge(Dest, Source, Vertices, Edges, sets:add_element(V, Forced)) + merge(Dest, Source, Vertices, Edges, sets:add_element(V, Forced)); + {aliased,no_info} -> + %% Nothing to do. + merge(Dest, Source, Vertices, Edges, Forced); + {no_info,aliased} -> + %% Alias in Dest. + merge(set_status(V, aliased, Dest), Source, + Vertices, Edges, Forced) end; merge(Dest0, _Source, [], Edges, Forced) -> merge1(Dest0, _Source, sets:to_list(Edges), @@ -413,25 +453,103 @@ accumulate_edges(V, State, Edges0) -> new() -> beam_digraph:new(). +-spec phi(beam_ssa:b_var(), [beam_ssa:b_var()], + sharing_state(), non_neg_integer()) + -> sharing_state(). +phi(Dst, Args, State0, Cnt) -> + ?assert_state(State0), + ?DP("** phi **~n~s~n", [dump(State0)]), + ?DP(" dst: ~p~n", [Dst]), + ?DP(" args: ~p~n", [Args]), + Structure = foldl(fun(Arg, Acc) -> + merge_in_arg(Arg, Acc, ?ARGS_DEPTH_LIMIT, State0) + end, no_info, Args), + ?DP(" structure: ~p~n", [Structure]), + new([Dst], [Structure], Cnt, State0). + %%% %%% Throws `too_deep` if the depth of sharing state value chains %%% exceeds SS_DEPTH_LIMIT. %%% --spec prune(sets:set(beam_ssa:b_var()), sharing_state()) -> sharing_state(). -prune(LiveVars, State) -> +-spec prune(sets:set(beam_ssa:b_var()), + sets:set(beam_ssa:b_var()), + sharing_state()) -> sharing_state(). +prune(LiveVars, Killed, State) -> + ?assert_state(State), + ?DP("** pruning **~n~s~n", [dump(State)]), + ?DP("pruning to: ~p~n", [sets:to_list(LiveVars)]), + ?DP("killed: ~p~n", [sets:to_list(Killed)]), + case sets:is_empty(LiveVars) of + false -> + Work = [{?SS_DEPTH_LIMIT,K} || K <- sets:to_list(Killed)], + R = prune(Work, Killed, LiveVars, State), + ?DP("Result:~n~s~n", [dump(R)]), + ?assert_state(R); + true -> + R = new(), + ?DP("Result (nothing killed):~n~s~n", [dump(R)]), + R + end. + +prune([{0,_}|_], _, _, _) -> + throw(too_deep); +prune([{Depth,V}|Work], Killed, LiveVars, State0) -> + case is_safe_to_prune(V, LiveVars, State0) of + true -> + State = beam_digraph:del_vertex(State0, V), + Ins = [{Depth - 1, I} + || I <- beam_digraph:in_neighbours(State0, V)], + prune(Ins++Work, Killed, LiveVars, State); + false -> + prune(Work, Killed, LiveVars, State0) + end; +prune([], _Killed, _LiveVars, State) -> + State. + +is_safe_to_prune(V, LiveVars, State) -> + case sets:is_element(V, LiveVars) of + true -> + false; + false -> + %% Safe to prune if all out-neighbours are safe-to-prune. + case beam_digraph:out_neighbours(State, V) of + [] -> + true; + Outs -> + lists:all(fun(X) -> + is_safe_to_prune(X, LiveVars, State) + end, Outs) + end + end. + +%%% +%%% As prune/3, but doing the pruning by rebuilding the surviving +%%% state from scratch. +%%% +%%% Throws `too_deep` if the depth of sharing state value chains +%%% exceeds SS_DEPTH_LIMIT. +%%% +-spec prune_by_add(sets:set(beam_ssa:b_var()), sharing_state()) + -> sharing_state(). +prune_by_add(LiveVars, State) -> ?assert_state(State), ?DP("Pruning to ~p~n", [sets:to_list(LiveVars)]), - ?DBG(dump(State)), - R = prune([{0,V} || V <- sets:to_list(LiveVars)], [], new(), State), - ?DP("Pruned result~n"), - ?DBG(dump(R)), + ?DP("~s~n", [dump(State)]), + ?DP("Vertices: ~p~n", [beam_digraph:vertices(State)]), + R = prune_by_add([{0,V} || V <- sets:to_list(LiveVars), + beam_digraph:has_vertex(State, V)], + [], new(), State), + ?DP("Pruned result~n~s~n", [dump(R)]), ?assert_state(R). -prune([{Depth0,V}|Wanted], Edges, New0, Old) -> +prune_by_add([{Depth0,V}|Wanted], Edges, New0, Old) -> + ?DP("Looking at ~p~n", [V]), + ?DP("Curr:~n~s~n", [dump(New0)]), + ?DP("Wanted: ~p~n", [Wanted]), case beam_digraph:has_vertex(New0, V) of true -> %% This variable is already added. - prune(Wanted, Edges, New0, Old); + prune_by_add(Wanted, Edges, New0, Old); false when Depth0 < ?SS_DEPTH_LIMIT -> %% This variable has to be kept. Add it to the new graph. New = add_vertex(New0, V, beam_digraph:vertex(Old, V)), @@ -439,12 +557,19 @@ prune([{Depth0,V}|Wanted], Edges, New0, Old) -> InEdges = beam_digraph:in_edges(Old, V), Depth = Depth0 + 1, InNodes = [{Depth, From} || {From,_,_} <- InEdges], - prune(InNodes ++ Wanted, InEdges ++ Edges, New, Old); + prune_by_add(InNodes ++ Wanted, InEdges ++ Edges, New, Old); false -> - %% We're in too deep, give up. + %% We're in too deep, give up. This case will probably + %% never be hit as it would require a previous prune/3 + %% application which doesn't hit the depth limit and for + %% it to remove more than half of the nodes to trigger the + %% use of prune_by_add/2, and in a later iteration trigger + %% the depth limit. As it cannot be definitely ruled out, + %% take the hit against the coverage statistics, as the + %% handling code in beam_ssa_alias is tested. throw(too_deep) end; -prune([], Edges, New0, _Old) -> +prune_by_add([], Edges, New0, _Old) -> foldl(fun({Src,Dst,Lbl}, New) -> add_edge(New, Src, Dst, Lbl) end, New0, Edges). @@ -491,19 +616,23 @@ set_status(#b_var{}=V, Status, State0) -> %% is embedded remains unchanged. ?DP("Setting status of ~p to ~p~n", [V,Status]), - ?ASSERT(assert_variable_exists(V, State0)), - case beam_digraph:vertex(State0, V) of + case beam_digraph:vertex(State0, V, unique) of Status -> %% Status is unchanged. State0; unique when Status =:= aliased -> State = add_vertex(State0, V, Status), - set_alias(get_alias_edges(V, State), State) + set_alias(get_alias_edges(V, State), State); + no_info when Status =/= no_info -> + add_vertex(State0, V, Status); + unique when Status =:= no_info -> + %% Can only be used safely for newly created variables. + add_vertex(State0, V, Status) end. set_alias([#b_var{}=V|Vars], State0) -> %% TODO: fold into the above - case beam_digraph:vertex(State0, V) of + case beam_digraph:vertex(State0, V, unique) of aliased -> set_alias(Vars, State0); _ -> @@ -529,9 +658,12 @@ get_alias_edges(V, State) -> end], EmbedEdges ++ OutEdges. +-spec size(sharing_state()) -> non_neg_integer(). +size(State) -> + beam_digraph:no_vertices(State). + -spec variables(sharing_state()) -> [beam_ssa:b_var()]. variables(State) -> - %% TODO: Sink this beam_digraph to avoid splitting the list? [V || {V,_Lbl} <- beam_digraph:vertices(State)]. -type call_in_arg_status() :: no_info @@ -601,8 +733,8 @@ meet_in_args_elems1([], Large, Result) -> -spec merge_in_args([beam_ssa:b_var()], call_in_arg_info(), sharing_state()) -> call_in_arg_info(). merge_in_args([Arg|Args], [ArgStatus|Status], State) -> - ?DP(" merge_in_arg: ~p~n current: ~p~n SS: ~p.~n", - [Arg,ArgStatus,State]), + ?DP(" merge_in_arg: ~p~n current: ~p~n SS:~n~s.~n", + [Arg, ArgStatus, dump(State)]), Info = merge_in_arg(Arg, ArgStatus, ?ARGS_DEPTH_LIMIT, State), [Info|merge_in_args(Args, Status, State)]; merge_in_args([], [], _State) -> @@ -612,28 +744,45 @@ merge_in_arg(_, aliased, _, _State) -> aliased; merge_in_arg(plain, _, _, _State) -> unique; -merge_in_arg(#b_var{}=V, _Status, 0, State) -> +merge_in_arg(#b_var{}=V, Status, 0, State) -> %% We will not traverse this argument further, this means that no %% element-level aliasing info will be kept for this element. - get_status(V, State); + case {Status, get_status(V, State)} of + {S,S} -> + S; + {no_info,S} -> + S; + {S,no_info} -> + S; + {_,aliased} -> + aliased + end; +merge_in_arg(#b_var{}=V, unique, _Cutoff, State) -> + case beam_digraph:vertex(State, V, unique) of + no_info -> + unique; + S -> + S + end; merge_in_arg(#b_var{}=V, Status, Cutoff, State) -> - case beam_digraph:vertex(State, V) of + case beam_digraph:vertex(State, V, unique) of aliased -> aliased; unique -> InEdges = beam_digraph:in_edges(State, V), Elements = case Status of - unique -> #{}; {unique,Es} -> Es; no_info -> #{} end, - merge_elements(InEdges, Elements, Cutoff, State) + merge_elements(InEdges, Elements, Cutoff, State); + no_info -> + Status end; -merge_in_arg(#b_literal{}, _, 0, _State) -> +merge_in_arg(#b_literal{}, Status, 0, _State) -> %% We have reached the cutoff while traversing a larger construct, - %% as we're not looking deeper down into the structure we indicate - %% that we have no information. - no_info; + %% as we're not looking deeper down into the structure we stop + %% constructing detailed information. + Status; merge_in_arg(#b_literal{val=[Hd|Tl]}, Status, Cutoff, State) -> {HdS,TlS,Elements0} = case Status of {unique,#{hd:=HdS0,tl:=TlS0}=All} -> @@ -649,12 +798,15 @@ merge_in_arg(#b_literal{val=[Hd|Tl]}, Status, Cutoff, State) -> {unique,Elements}; merge_in_arg(#b_literal{val=[]}, Status, _, _State) -> Status; +merge_in_arg(#b_literal{val=T}, unique, _Cutoff, _State) when is_tuple(T) -> + %% The uniqe status cannot be safely upgraded to a more detailed + %% info. + unique; merge_in_arg(#b_literal{val=T}, Status, Cutoff, State) when is_tuple(T) -> SrcElements = tuple_to_list(T), OrigElements = case Status of {unique,TupleElems} -> TupleElems; - unique -> #{}; no_info -> #{} end, Elements = merge_tuple_elems(SrcElements, OrigElements, Cutoff, State), @@ -705,15 +857,21 @@ merge_elements([{Src,_,{embed,Idx}}|Rest], Elements0, Cutoff, State) when merge_elements(Rest, Elements, Cutoff, State); merge_elements([{_Src,_,embed}|Rest], _Elements0, Cutoff, State) -> %% We don't know where this element is embedded. Src will always - %% be unique as otherwise erge_in_arg/4 will not bother merging + %% be unique as otherwise merge_in_arg/4 will not bother merging %% the in-edges. - ?ASSERT(unique = get_status(_Src, State)), + ?ASSERT(true = get_status(_Src, State) =:= unique + orelse get_status(_Src, State) =:= no_info), merge_elements(Rest, no_info, Cutoff, State); -merge_elements([{_,V,{extract,_}}|_Rest], _Elements0, _, State) -> - %% For now we don't try to derive the structure of this argument - %% further. - %% TODO: Revisit the decision above. - get_status(V, State). +merge_elements([{Src,V,{extract,E}}], Elements, Cutoff, State) -> + ?DP("Looking for an embedding of the ~p element from ~p~n", [E, Src]), + InEdges = beam_digraph:in_edges(State, Src), + case [Other || {Other,_,{embed,EdgeOp}} <- InEdges, EdgeOp =:= E] of + [Next] -> + ?DP("Found: ~p~n", [Next]), + merge_in_arg(Next, {unique,Elements}, Cutoff, State); + [] -> + get_status(V, State) + end. -spec new([beam_ssa:b_var()], call_in_arg_info(), non_neg_integer()) -> sharing_state(). @@ -723,12 +881,8 @@ new(Args, ArgsInfo, Cnt) -> ?assert_state(SS), R. -new([A|As], [S0|Stats], Cnt, SS) - when S0 =:= aliased; S0 =:= unique; S0 =:= no_info -> - S = case S0 of - no_info -> unique; - _ -> S0 - end, +new([A|As], [S|Stats], Cnt, SS) + when S =:= aliased; S =:= unique; S =:= no_info -> new(As, Stats, Cnt, add_var(A, S, SS)); new([A|As], [{unique,Elements}|Stats], Cnt0, SS0) -> SS1 = add_var(A, unique, SS0), @@ -761,17 +915,24 @@ has_out_edges(V, State) -> %% Debug support below --ifdef(EXTRA_ASSERTS). +-ifdef(SS_EXTRA_ASSERTS). -spec assert_state(sharing_state()) -> sharing_state(). assert_state(State) -> + assert_bad_edges(State), assert_aliased_parent_implies_aliased(State), assert_embedded_in_aliased_implies_aliased(State), assert_multiple_embeddings_force_aliasing(State), assert_multiple_extractions_force_aliasing(State), State. +%% Check that we don't have edges between non-existing nodes +assert_bad_edges(State) -> + [{assert_variable_exists(F, State), assert_variable_exists(T, State)} + || {F,T,_} <- beam_digraph:edges(State)]. + + %% Check that extracted and embedded elements of an aliased variable %% are aliased. assert_aliased_parent_implies_aliased(State) -> @@ -785,12 +946,12 @@ assert_apia(Parent, State) -> embed -> true; {embed,_} -> false end], - [case beam_digraph:vertex(State, Child) of + [case beam_digraph:vertex(State, Child, unique) of aliased -> ok; _ -> io:format("Expected ~p to be aliased as is derived from ~p.~n" - "state: ~p", [Child, Parent, State]), + "state: ~s", [Child, Parent, dump(State)]), throw(assertion_failure) end || Child <- Children]. @@ -802,14 +963,15 @@ assert_embedded_in_aliased_implies_aliased(State) -> assert_eiaia(Embedder, State) -> NotAliased = [ Src || {Src,_,embed} <- beam_digraph:in_edges(State, Embedder), - beam_digraph:vertex(State, Src) =/= aliased], + beam_digraph:vertex(State, Src, unique) =/= aliased, + beam_digraph:vertex(State, Src, unique) =/= no_info], case NotAliased of [] -> State; _ -> io:format("Expected ~p to be aliased as" - " they are embedded in aliased values.~n~p.~n", - [NotAliased, State]), + " they are embedded in aliased values.~n~s.~n", + [NotAliased, dump(State)]), throw(assertion_failure) end. @@ -820,10 +982,10 @@ assert_multiple_embeddings_force_aliasing(State) -> assert_mefa(V, State) -> NotAliased = [ B || {B,_,embed} <- beam_digraph:out_edges(State, V), - beam_digraph:vertex(State, B) =/= aliased], + beam_digraph:vertex(State, B, unique) =/= aliased], case NotAliased of [_,_|_] -> - io:format("Expected ~p in ~p to be aliased.~n", [V,State]), + io:format("Expected ~p to be aliased in:~n~s.~n", [V, dump(State)]), throw(assertion_failure); _ -> State @@ -846,7 +1008,8 @@ assert_mxfa(V, State) -> end, #{}, beam_digraph:out_edges(State, V)), Bad = maps:fold( fun(_Elem, [_,_|_]=Vars, Acc) -> - [X || X <- Vars, beam_digraph:vertex(State, X) =/= aliased] + [X || X <- Vars, + beam_digraph:vertex(State, X, unique) =/= aliased] ++ Acc; (_, _, Acc) -> Acc @@ -855,21 +1018,21 @@ assert_mxfa(V, State) -> [] -> State; _ -> - io:format("~p should be aliased~nstate:~p.~n", [V,State]), + io:format("~p should be aliased~nstate:~n~s.~n", [V, dump(State)]), throw(assertion_failure) end. assert_variable_exists(plain, State) -> case beam_digraph:has_vertex(State, plain) of false -> - io:format("Expected ~p in ~p.~n", [plain,State]), + io:format("Expected ~p in~n ~s.~n", [plain, dump(State)]), throw(assertion_failure); _ -> - case beam_digraph:vertex(State, plain) of + case beam_digraph:vertex(State, plain, unique) of unique -> State; Other -> - io:format("Expected plain in ~p to be unique," - " was ~p.~n", [State,Other]), + io:format("Expected plain to be unique, was ~p, in:~n~p~n", + [Other, dump(State)]), throw(assertion_failure) end end; @@ -878,16 +1041,28 @@ assert_variable_exists(#b_literal{}, State) -> assert_variable_exists(#b_var{}=V, State) -> case beam_digraph:has_vertex(State, V) of false -> - io:format("Expected ~p in ~p.~n", [V,State]), + io:format("Expected ~p in~n~s.~n", [V, dump(State)]), throw(assertion_failure); _ -> State end. -endif. --ifdef(DEBUG). +-ifdef(PROVIDE_DUMP). +-spec dump(sharing_state()) -> iolist(). dump(State) -> - io:format("~p~n", [State]). - + Ls = lists:enumerate(0, beam_digraph:vertices(State)), + V2Id = #{ V=>Id || {Id,{V,_}} <- Ls }, + [ + "digraph G {\n", + [ io_lib:format(" ~p [label=\"~p:~p\",shape=\"box\"]~n", + [Id,V,beam_digraph:vertex(State, V)]) + || V:=Id <- V2Id], + [ io_lib:format(" ~p -> ~p [label=\"~p\"]~n", + [maps:get(From, V2Id, plain), + maps:get(To, V2Id), + Lbl]) + || {From,To,Lbl} <- beam_digraph:edges(State)], + "}\n"]. -endif. diff --git a/lib/compiler/src/beam_ssa_type.erl b/lib/compiler/src/beam_ssa_type.erl index fdc5d06dd9b9..f34dd1cb51f4 100644 --- a/lib/compiler/src/beam_ssa_type.erl +++ b/lib/compiler/src/beam_ssa_type.erl @@ -1561,12 +1561,26 @@ will_succeed_1(#b_set{op=wait_timeout}, _Src, _Ts) -> will_succeed_1(#b_set{}, _Src, _Ts) -> 'maybe'. +%% Take care to not produce a reuse hint when more than one update +%% exists. There is no point in attempting the reuse optimization when +%% more than one element is updated, as checking more than one element +%% at runtime is known to be slower than just copying the tuple in +%% most cases. Additionally, using a copy hint occasionally allows the +%% alias analysis pass to do a better job. simplify_update_record(Src, Hint0, Updates, Ts) -> case sur_1(Updates, concrete_type(Src, Ts), Ts, Hint0, []) of + {#b_literal{val=reuse}, []} when length(Updates) > 2 -> + {changed, #b_literal{val=copy}, Updates}; {Hint0, []} -> unchanged; - {Hint, Skipped} -> - {changed, Hint, sur_skip(Updates, Skipped)} + {Hint1, Skipped} -> + Updates1 = sur_skip(Updates, Skipped), + Hint = if length(Updates1) > 2 -> + #b_literal{val=copy}; + true -> + Hint1 + end, + {changed, Hint, Updates1} end. sur_1([Index, Arg | Updates], RecordType, Ts, Hint, Skipped) -> diff --git a/lib/compiler/test/beam_ssa_check_SUITE.erl b/lib/compiler/test/beam_ssa_check_SUITE.erl index 54baeb07f286..b1ddcb93492a 100644 --- a/lib/compiler/test/beam_ssa_check_SUITE.erl +++ b/lib/compiler/test/beam_ssa_check_SUITE.erl @@ -33,6 +33,7 @@ appendable_checks/1, bs_size_unit_checks/1, no_reuse_hint_checks/1, + no_type_info_checks/1, private_append_checks/1, ret_annotation_checks/1, sanity_checks/1, @@ -49,6 +50,7 @@ groups() -> annotation_checks, appendable_checks, no_reuse_hint_checks, + no_type_info_checks, private_append_checks, ret_annotation_checks, sanity_checks, @@ -104,6 +106,9 @@ bs_size_unit_checks(Config) when is_list(Config) -> no_reuse_hint_checks(Config) when is_list(Config) -> run_post_ssa_opt(no_reuse_hint, Config). +no_type_info_checks(Config) when is_list(Config) -> + run_post_ssa_opt(no_type_info, Config). + private_append_checks(Config) when is_list(Config) -> run_post_ssa_opt(private_append, Config). diff --git a/lib/compiler/test/beam_ssa_check_SUITE_data/alias.erl b/lib/compiler/test/beam_ssa_check_SUITE_data/alias.erl index 197bc0d7410d..55400ccd9d27 100644 --- a/lib/compiler/test/beam_ssa_check_SUITE_data/alias.erl +++ b/lib/compiler/test/beam_ssa_check_SUITE_data/alias.erl @@ -106,7 +106,13 @@ fuzz0/0, fuzz0/1, alias_after_phi/0, - check_identifier_type/0]). + check_identifier_type/0, + + nested_tuple/0, + nested_cons/0, + nested_mixed/0, + + see_through/0]). %% Trivial smoke test transformable0(L) -> @@ -1153,3 +1159,57 @@ should_return_unique({X}) -> %ssa% (_) when post_ssa_opt -> %ssa% ret(R) { unique => [R] }. X. + +%% Check that the alias analysis handles a chain of extracts from +%% tuples. +nested_tuple_inner() -> + {{{{<<>>, e:x()}}}}. + +nested_tuple() -> +%ssa% () when post_ssa_opt -> +%ssa% U = bs_create_bin(append, _, T, ...) { unique => [T] }, +%ssa% R = put_tuple(U, A) { aliased => [A], unique => [U] }, +%ssa% ret(R). + {{{{Z,X}}}} = nested_tuple_inner(), + {<>,X}. + +%% Check that the alias analysis handles a chain of extracts from +%% pairs. +nested_cons_inner() -> + [[[[<<>>, e:x()]]]]. + +nested_cons() -> +%ssa% () when post_ssa_opt -> +%ssa% U = bs_create_bin(append, _, T, ...) { unique => [T] }, +%ssa% R = put_tuple(U, A) { aliased => [A], unique => [U] }, +%ssa% ret(R). + [[[[Z,X]]]] = nested_cons_inner(), + {<>,X}. + +nested_mixed_inner() -> + [{[{<<>>, e:x()}]}]. + +nested_mixed() -> +%ssa% () when post_ssa_opt -> +%ssa% U = bs_create_bin(append, _, T, ...) { unique => [T] }, +%ssa% R = put_tuple(U, A) { aliased => [A], unique => [U] }, +%ssa% ret(R). + [{[{Z,X}]}] = nested_mixed_inner(), + {<>,X}. + +%% +%% Check that the analysis can see through embed-extract chains. +%% +-record(see_through, {a,b}). + +see_through() -> + [R] = see_through0(), + see_through1(R). + +see_through1({_,R}) -> +%ssa% (_) when post_ssa_opt -> +%ssa% _ = update_record(reuse, 3, Rec, _, _) {unique => [Rec], source_dies => true}. + R#see_through{a=e:f()}. + +see_through0() -> + [{foo, #see_through{a={bar, [foo]}}}]. diff --git a/lib/compiler/test/beam_ssa_check_SUITE_data/no_info0.erl b/lib/compiler/test/beam_ssa_check_SUITE_data/no_info0.erl new file mode 100644 index 000000000000..53b3b25344c1 --- /dev/null +++ b/lib/compiler/test/beam_ssa_check_SUITE_data/no_info0.erl @@ -0,0 +1,60 @@ +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2024. 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% +%% +%% Extracts from beam_clean.erl to test cases when special handling of +%% the no_info sharing state is required for correct functioning. +%% +-module(no_info0). +-moduledoc false. + +-export([clean_labels/1]). + +-import(lists, [reverse/1]). + +-type label() :: beam_asm:label(). + +-record(st, {lmap :: [{label(),label()}], %Translation tables for labels. + entry :: beam_asm:label(), %Number of entry label. + lc :: non_neg_integer() %Label counter + }). + +clean_labels(Fs0) -> + St0 = #st{lmap=[],entry=1,lc=1}, + function_renumber(Fs0, St0, []). + +function_renumber([{function,Name,Arity,_Entry,Asm0}|Fs], St0, Acc) -> + {Asm,St} = renumber_labels(Asm0, [], St0), + function_renumber(Fs, St, [{function,Name,Arity,St#st.entry,Asm}|Acc]); +function_renumber([], St, Acc) -> {Acc,St}. + +renumber_labels([{label,Old}|Is], [{label,New}|_]=Acc, #st{lmap=D0}=St) -> +%ssa% (_, _, Rec) when post_ssa_opt -> +%ssa% _ = update_record(inplace, 4, Rec, ...), +%ssa% _ = update_record(inplace, 4, Rec, ...), +%ssa% _ = update_record(inplace, 4, Rec, ...). + D = [{Old,New}|D0], + renumber_labels(Is, Acc, St#st{lmap=D}); +renumber_labels([{label,Old}|Is], Acc, St0) -> + New = St0#st.lc, + D = [{Old,New}|St0#st.lmap], + renumber_labels(Is, [{label,New}|Acc], St0#st{lmap=D,lc=New+1}); +renumber_labels([{func_info,_,_,_}=Fi|Is], Acc, St0) -> + renumber_labels(Is, [Fi|Acc], St0#st{entry=St0#st.lc}); +renumber_labels([I|Is], Acc, St0) -> + renumber_labels(Is, [I|Acc], St0); +renumber_labels([], Acc, St) -> {Acc,St}. diff --git a/lib/compiler/test/beam_ssa_check_SUITE_data/no_type_info.erl b/lib/compiler/test/beam_ssa_check_SUITE_data/no_type_info.erl new file mode 100644 index 000000000000..22505ee8eda6 --- /dev/null +++ b/lib/compiler/test/beam_ssa_check_SUITE_data/no_type_info.erl @@ -0,0 +1,51 @@ +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2024. 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 module tests functions which have previously crashed the +%% compiler when the `no_type_opt` option was used. +%% + +-module(no_type_info). +-export([bug/0]). + +-compile(no_type_opt). + +bug() -> +%ssa% () when post_ssa_opt -> +%ssa% X = bif:hd(L) { unique => [L] }, +%ssa% _ = succeeded:body(X) { aliased => [X] }. + begin + + <<42 || + $s <- + try + something + catch + error:false -> + [] + end + >> + end:(hd(not girl))( + try home of + _ when 34 -> + 8 + catch + _:_ -> + whatever + after + ok + end). diff --git a/lib/compiler/test/beam_ssa_check_SUITE_data/phis.erl b/lib/compiler/test/beam_ssa_check_SUITE_data/phis.erl new file mode 100644 index 000000000000..cff2b64338e2 --- /dev/null +++ b/lib/compiler/test/beam_ssa_check_SUITE_data/phis.erl @@ -0,0 +1,57 @@ +%% Extracted from lib/syntax_tools/src/erl_recomment.erl to test +%% omissions in handling of Phi instructions. + +%% ===================================================================== +%% 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 +%% +%% 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. +%% +%% Alternatively, you may use this file under the terms of the GNU Lesser +%% General Public License (the "LGPL") as published by the Free Software +%% Foundation; either version 2.1, or (at your option) any later version. +%% If you wish to allow use of your version of this file only under the +%% terms of the LGPL, you should delete the provisions above and replace +%% them with the notice and other provisions required by the LGPL; see +%% . If you do not delete the provisions +%% above, a recipient may use your version of this file under the terms of +%% either the Apache License or the LGPL. +%% +%% @copyright 1997-2006 Richard Carlsson +%% @author Richard Carlsson +%% @end +%% ===================================================================== + +-module(phis). + +-export([filter_forms/1]). + +-record(filter, {file = undefined :: file:filename() | 'undefined', + line = 0 :: integer()}). + +filter_forms(Fs) -> + filter_forms(Fs, #filter{}). + +filter_forms([{A1, A2} | Fs], S) -> +%ssa% (_, Rec0) when post_ssa_opt -> +%ssa% Rec = update_record(inplace, 3, Rec0, ...), +%ssa% Phi = phi({Rec0, _}, {Rec, _}), +%ssa% _ = update_record(inplace, 3, Phi, ...). + S1 = case ex:f() of + undefined -> + S#filter{file = A1, line = A2}; + _ -> + S + end, + if S1#filter.file =:= A1 -> + filter_forms(Fs, S1#filter{line = A2}); + true -> + filter_forms(Fs, S1) + end; +filter_forms([], _) -> + []. diff --git a/lib/compiler/test/beam_ssa_check_SUITE_data/sanity_checks.erl b/lib/compiler/test/beam_ssa_check_SUITE_data/sanity_checks.erl index a47665597916..9624f6000940 100644 --- a/lib/compiler/test/beam_ssa_check_SUITE_data/sanity_checks.erl +++ b/lib/compiler/test/beam_ssa_check_SUITE_data/sanity_checks.erl @@ -37,7 +37,8 @@ t35/1, t36/0, t37/0, t38/0, t39/1, t40/0, t41/0, t42/0, t43/0, t44/0, - check_env/0]). + check_env/0, + check_succeeded/1]). %% Check that we do not trigger on the wrong pass check_wrong_pass() -> @@ -339,3 +340,11 @@ check_env() -> A = <<>>, B = ex:f(), <>. + +%% Check that succeeded-instructions can be matched. +check_succeeded(L) -> +%ssa% (L) when post_ssa_opt -> +%ssa% X = bif:hd(L), +%ssa% Y = succeeded:body(X), +%ssa% ret(X). + hd(L). diff --git a/lib/compiler/test/beam_ssa_check_SUITE_data/ss_depth_limit.erl b/lib/compiler/test/beam_ssa_check_SUITE_data/ss_depth_limit.erl index ec91b57f26fa..c0c714523890 100644 --- a/lib/compiler/test/beam_ssa_check_SUITE_data/ss_depth_limit.erl +++ b/lib/compiler/test/beam_ssa_check_SUITE_data/ss_depth_limit.erl @@ -16,18 +16,21 @@ do(N) -> "-export([f/0]).\n\n", "f() ->\n", "%ssa% fail () when post_ssa_opt ->\n" - "%ssa% ret(X) { unique => [X] }.\n" + "%ssa% ret(Y) { aliased => [Y] }.\n" + " Y = e:f(),\n", " X0 = e:f(),\n", [io_lib:format(" X~p = {X~p,e:f()},~n", [X, X-1]) || X<- lists:seq(1, N)], - io_lib:format(" X~p.~n", [N]) + io_lib:format(" e:f(X~p),~n", [N]), + " Y.\n" ], file:write_file("ss_depth_limit.erl", Data). -endif. f() -> %ssa% fail () when post_ssa_opt -> -%ssa% ret(X) { unique => [X] }. +%ssa% ret(Y) { aliased => [Y] }. + Y = e:f(), X0 = e:f(), X1 = {X0,e:f()}, X2 = {X1,e:f()}, @@ -60,4 +63,5 @@ f() -> X29 = {X28,e:f()}, X30 = {X29,e:f()}, X31 = {X30,e:f()}, - X31. + e:f(X31), + Y.