diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/compiler/src/compile.erl | 29 | ||||
-rw-r--r-- | lib/compiler/src/sys_core_fold.erl | 87 | ||||
-rw-r--r-- | lib/compiler/test/compile_SUITE.erl | 21 | ||||
-rw-r--r-- | lib/compiler/test/core_SUITE.erl | 5 | ||||
-rw-r--r-- | lib/compiler/test/core_SUITE_data/fun_letrec_effect.core | 25 | ||||
-rw-r--r-- | lib/compiler/test/core_fold_SUITE.erl | 24 |
6 files changed, 177 insertions, 14 deletions
diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl index 5ef9611504..e1c1f7338e 100644 --- a/lib/compiler/src/compile.erl +++ b/lib/compiler/src/compile.erl @@ -1432,16 +1432,30 @@ encrypt_debug_info(DebugInfo, Key, Opts) -> end. cleanup_compile_options(Opts) -> - lists:filter(fun keep_compile_option/1, Opts). - + IsDeterministic = lists:member(deterministic, Opts), + lists:filter(fun(Opt) -> + keep_compile_option(Opt, IsDeterministic) + end, Opts). + +%% Include paths and current directory don't affect compilation, but they might +%% be helpful so we include them unless we're doing a deterministic build. +keep_compile_option({i, _}, Deterministic) -> + not Deterministic; +keep_compile_option({cwd, _}, Deterministic) -> + not Deterministic; %% We are storing abstract, not asm or core. -keep_compile_option(from_asm) -> false; -keep_compile_option(from_core) -> false; +keep_compile_option(from_asm, _Deterministic) -> + false; +keep_compile_option(from_core, _Deterministic) -> + false; %% Parse transform and macros have already been applied. -keep_compile_option({parse_transform, _}) -> false; -keep_compile_option({d, _, _}) -> false; +keep_compile_option({parse_transform, _}, _Deterministic) -> + false; +keep_compile_option({d, _, _}, _Deterministic) -> + false; %% Do not affect compilation result on future calls. -keep_compile_option(Option) -> effects_code_generation(Option). +keep_compile_option(Option, _Deterministic) -> + effects_code_generation(Option). start_crypto() -> try crypto:start() of @@ -1589,6 +1603,7 @@ effects_code_generation(Option) -> binary -> false; verbose -> false; {cwd,_} -> false; + {i,_} -> false; {outdir, _} -> false; _ -> true end. diff --git a/lib/compiler/src/sys_core_fold.erl b/lib/compiler/src/sys_core_fold.erl index 617411ed56..3a65b40fa5 100644 --- a/lib/compiler/src/sys_core_fold.erl +++ b/lib/compiler/src/sys_core_fold.erl @@ -345,7 +345,12 @@ expr(#c_letrec{body=#c_var{}}=Letrec, effect, _Sub) -> void(); expr(#c_letrec{defs=Fs0,body=B0}=Letrec, Ctxt, Sub) -> Fs1 = map(fun ({Name,Fb}) -> - {Name,expr(Fb, {letrec,Ctxt}, Sub)} + case Ctxt =:= effect andalso is_fun_effect_safe(Name, B0) of + true -> + {Name,expr(Fb, {letrec, effect}, Sub)}; + false -> + {Name,expr(Fb, {letrec, value}, Sub)} + end end, Fs0), B1 = body(B0, Ctxt, Sub), Letrec#c_letrec{defs=Fs1,body=B1}; @@ -476,6 +481,86 @@ expr(#c_try{anno=A,arg=E0,vars=Vs0,body=B0,evars=Evs0,handler=H0}=Try, _, Sub0) Try#c_try{arg=E1,vars=Vs1,body=B1,evars=Evs1,handler=H1} end. + +%% If a fun or its application is used as an argument, then it's unsafe to +%% handle it in effect context as the side-effects may rely on its return +%% value. The following is a minimal example of where it can go wrong: +%% +%% do letrec 'f'/0 = fun () -> ... whatever ... +%% in call 'side':'effect'(apply 'f'/0()) +%% 'ok' +%% +%% This function returns 'true' if Body definitely does not rely on a +%% value produced by FVar, or 'false' if Body depends on or might depend on +%% a value produced by FVar. + +is_fun_effect_safe(#c_var{}=FVar, Body) -> + ifes_1(FVar, Body, true). + +ifes_1(FVar, #c_alias{pat=Pat}, _Safe) -> + ifes_1(FVar, Pat, false); +ifes_1(FVar, #c_apply{op=Op,args=Args}, Safe) -> + %% FVar(...) is safe as long its return value is ignored, but it's never + %% okay to pass FVar as an argument. + ifes_list(FVar, Args, false) andalso ifes_1(FVar, Op, Safe); +ifes_1(FVar, #c_binary{segments=Segments}, _Safe) -> + ifes_list(FVar, Segments, false); +ifes_1(FVar, #c_bitstr{val=Val,size=Size,unit=Unit}, _Safe) -> + ifes_list(FVar, [Val, Size, Unit], false); +ifes_1(FVar, #c_call{args=Args}, _Safe) -> + ifes_list(FVar, Args, false); +ifes_1(FVar, #c_case{arg=Arg,clauses=Clauses}, Safe) -> + ifes_1(FVar, Arg, false) andalso ifes_list(FVar, Clauses, Safe); +ifes_1(FVar, #c_catch{body=Body}, _Safe) -> + ifes_1(FVar, Body, false); +ifes_1(FVar, #c_clause{pats=Pats,guard=Guard,body=Body}, Safe) -> + ifes_list(FVar, Pats, false) andalso + ifes_1(FVar, Guard, false) andalso + ifes_1(FVar, Body, Safe); +ifes_1(FVar, #c_cons{hd=Hd,tl=Tl}, _Safe) -> + ifes_1(FVar, Hd, false) andalso ifes_1(FVar, Tl, false); +ifes_1(FVar, #c_fun{body=Body}, _Safe) -> + ifes_1(FVar, Body, false); +ifes_1(FVar, #c_let{arg=Arg,body=Body}, Safe) -> + ifes_1(FVar, Arg, false) andalso ifes_1(FVar, Body, Safe); +ifes_1(FVar, #c_letrec{defs=Defs,body=Body}, Safe) -> + Funs = [Fun || {_,Fun} <- Defs], + ifes_list(FVar, Funs, false) andalso ifes_1(FVar, Body, Safe); +ifes_1(_FVar, #c_literal{}, _Safe) -> + true; +ifes_1(FVar, #c_map{arg=Arg,es=Elements}, _Safe) -> + ifes_1(FVar, Arg, false) andalso ifes_list(FVar, Elements, false); +ifes_1(FVar, #c_map_pair{key=Key,val=Val}, _Safe) -> + ifes_1(FVar, Key, false) andalso ifes_1(FVar, Val, false); +ifes_1(FVar, #c_primop{args=Args}, _Safe) -> + ifes_list(FVar, Args, false); +ifes_1(FVar, #c_receive{timeout=Timeout,action=Action,clauses=Clauses}, Safe) -> + ifes_1(FVar, Timeout, false) andalso + ifes_1(FVar, Action, Safe) andalso + ifes_list(FVar, Clauses, Safe); +ifes_1(FVar, #c_seq{arg=Arg,body=Body}, Safe) -> + %% Arg of a #c_seq{} has no effect so it's okay to use FVar there even if + %% Safe=false. + ifes_1(FVar, Arg, true) andalso ifes_1(FVar, Body, Safe); +ifes_1(FVar, #c_try{arg=Arg,handler=Handler,body=Body}, Safe) -> + ifes_1(FVar, Arg, false) andalso + ifes_1(FVar, Handler, Safe) andalso + ifes_1(FVar, Body, Safe); +ifes_1(FVar, #c_tuple{es=Elements}, _Safe) -> + ifes_list(FVar, Elements, false); +ifes_1(FVar, #c_values{es=Elements}, _Safe) -> + ifes_list(FVar, Elements, false); +ifes_1(#c_var{name=Name}, #c_var{name=Name}, Safe) -> + %% It's safe to return FVar if it's unused. + Safe; +ifes_1(_FVar, #c_var{}, _Safe) -> + true. + +ifes_list(FVar, [E|Es], Safe) -> + ifes_1(FVar, E, Safe) andalso ifes_list(FVar, Es, Safe); +ifes_list(_FVar, [], _Safe) -> + true. + expr_list(Es, Ctxt, Sub) -> [expr(E, Ctxt, Sub) || E <- Es]. diff --git a/lib/compiler/test/compile_SUITE.erl b/lib/compiler/test/compile_SUITE.erl index 2206927ed1..1ecae06128 100644 --- a/lib/compiler/test/compile_SUITE.erl +++ b/lib/compiler/test/compile_SUITE.erl @@ -36,7 +36,7 @@ core_roundtrip/1, asm/1, optimized_guards/1, sys_pre_attributes/1, dialyzer/1, warnings/1, pre_load_check/1, env_compiler_options/1, - bc_options/1 + bc_options/1, deterministic_include/1 ]). suite() -> [{ct_hooks,[ts_install_cth]}]. @@ -53,7 +53,7 @@ all() -> cover, env, core_pp, core_roundtrip, asm, optimized_guards, sys_pre_attributes, dialyzer, warnings, pre_load_check, env_compiler_options, custom_debug_info, bc_options, - custom_compile_info]. + custom_compile_info, deterministic_include]. groups() -> []. @@ -1514,6 +1514,23 @@ highest_opcode(DataDir, Mod, Opt) -> <<16:32,0:32,HighestOpcode:32,_/binary>> = Code, HighestOpcode. +deterministic_include(Config) when is_list(Config) -> + DataDir = proplists:get_value(data_dir, Config), + Simple = filename:join(DataDir, "simple"), + + %% Files without +deterministic should differ if their include paths do, + %% as their debug info will be different. + {ok,_,NonDetA} = compile:file(Simple, [binary, {i,"gurka"}]), + {ok,_,NonDetB} = compile:file(Simple, [binary, {i,"gaffel"}]), + true = NonDetA =/= NonDetB, + + %% ... but files with +deterministic shouldn't. + {ok,_,DetC} = compile:file(Simple, [binary, deterministic, {i,"gurka"}]), + {ok,_,DetD} = compile:file(Simple, [binary, deterministic, {i,"gaffel"}]), + true = DetC =:= DetD, + + ok. + %%% %%% Utilities. %%% diff --git a/lib/compiler/test/core_SUITE.erl b/lib/compiler/test/core_SUITE.erl index 23f420f5f2..e5611e99d1 100644 --- a/lib/compiler/test/core_SUITE.erl +++ b/lib/compiler/test/core_SUITE.erl @@ -29,7 +29,7 @@ bs_shadowed_size_var/1, cover_v3_kernel_1/1,cover_v3_kernel_2/1,cover_v3_kernel_3/1, cover_v3_kernel_4/1,cover_v3_kernel_5/1, - non_variable_apply/1,name_capture/1]). + non_variable_apply/1,name_capture/1,fun_letrec_effect/1]). -include_lib("common_test/include/ct.hrl"). @@ -57,7 +57,7 @@ groups() -> bs_shadowed_size_var, cover_v3_kernel_1,cover_v3_kernel_2,cover_v3_kernel_3, cover_v3_kernel_4,cover_v3_kernel_5, - non_variable_apply,name_capture + non_variable_apply,name_capture,fun_letrec_effect ]}]. @@ -94,6 +94,7 @@ end_per_group(_GroupName, Config) -> ?comp(cover_v3_kernel_5). ?comp(non_variable_apply). ?comp(name_capture). +?comp(fun_letrec_effect). try_it(Mod, Conf) -> Src = filename:join(proplists:get_value(data_dir, Conf), diff --git a/lib/compiler/test/core_SUITE_data/fun_letrec_effect.core b/lib/compiler/test/core_SUITE_data/fun_letrec_effect.core new file mode 100644 index 0000000000..ab6f5b7940 --- /dev/null +++ b/lib/compiler/test/core_SUITE_data/fun_letrec_effect.core @@ -0,0 +1,25 @@ +module 'fun_letrec_effect' ['fun_letrec_effect'/0, 'ok'/0, 'wat'/0] +attributes [] + +'fun_letrec_effect'/0 = + fun () -> + do apply 'wat'/0() + receive + <'bar'> when 'true' -> 'ok' + <_0> when 'true' -> 'failed' + after 'infinity' -> + 'true' + +%% The return value (bar) of the fun was optimized away because the result of +%% the `letrec ... in` was unused, despite the fun's return value being +%% relevant for the side-effect of the expression. +'wat'/0 = + fun () -> + let <Self> = call 'erlang':'self'() in + do letrec 'f'/0 = fun () -> + do call 'maps':'put'('foo', 'bar', ~{}~) + 'bar' + in call 'erlang':'send'(Self, apply 'f'/0()) + 'undefined' + +end diff --git a/lib/compiler/test/core_fold_SUITE.erl b/lib/compiler/test/core_fold_SUITE.erl index 2a2369fff9..47606014c3 100644 --- a/lib/compiler/test/core_fold_SUITE.erl +++ b/lib/compiler/test/core_fold_SUITE.erl @@ -28,7 +28,7 @@ mixed_matching_clauses/1,unnecessary_building/1, no_no_file/1,configuration/1,supplies/1, redundant_stack_frame/1,export_from_case/1, - empty_values/1]). + empty_values/1,cover_letrec_effect/1]). -export([foo/0,foo/1,foo/2,foo/3]). @@ -48,7 +48,7 @@ groups() -> mixed_matching_clauses,unnecessary_building, no_no_file,configuration,supplies, redundant_stack_frame,export_from_case, - empty_values]}]. + empty_values,cover_letrec_effect]}]. init_per_suite(Config) -> @@ -598,5 +598,25 @@ empty_values(_Config) -> do_empty_values() when (#{})#{} -> c. +cover_letrec_effect(_Config) -> + self() ! {tag,42}, + _ = try + try + ignore + after + receive + {tag,Int}=Term -> + Res = #{k => {Term,<<Int:16>>}}, + self() ! Res + end + end + after + ok + end, + receive + Any -> + #{k := {{tag,42},<<42:16>>}} = Any + end, + ok. id(I) -> I. |