From 5bb8262bd4ae8dbcc7438e80de5cedac7ccee1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gustavsson?= Date: Tue, 31 Mar 2015 08:47:57 +0200 Subject: Raise more descriptive error messages for failed map operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit According to EEP-43 for maps, a 'badmap' exception should be generated when an attempt is made to update non-map term such as: <<>>#{a=>42} That was not implemented in the OTP 17. José Valim suggested that we should take the opportunity to improve the errors coming from map operations: http://erlang.org/pipermail/erlang-questions/2015-February/083588.html This commit implement better errors from map operations similar to his suggestion. When a map update operation (Map#{...}) or a BIF that expects a map is given a non-map term, the exception will be: {badmap,Term} This kind of exception is similar to the {badfun,Term} exception from operations that expect a fun. When a map operation requires a key that is not present in a map, the following exception will be raised: {badkey,Key} José Valim suggested that the exception should be {badkey,Key,Map}. We decided not to do that because the map could potentially be huge and cause problems if the error propagated through links to other processes. For BIFs, it could be argued that the exceptions could be simply 'badmap' and 'badkey', because the bad map and bad key can be found in the argument list for the BIF in the stack backtrace. However, for the map update operation (Map#{...}), the bad map or bad key will not be included in the stack backtrace, so that information must be included in the exception reason itself. For consistency, the BIFs should raise the same exceptions as update operation. If more than one key is missing, it is undefined which of keys that will be reported in the {badkey,Key} exception. --- lib/compiler/src/sys_core_fold.erl | 10 ++--- lib/compiler/src/v3_core.erl | 38 +++++++++-------- lib/compiler/test/map_SUITE.erl | 83 ++++++++++++++++++++++-------------- lib/compiler/test/warnings_SUITE.erl | 4 +- 4 files changed, 77 insertions(+), 58 deletions(-) (limited to 'lib/compiler') diff --git a/lib/compiler/src/sys_core_fold.erl b/lib/compiler/src/sys_core_fold.erl index 0d020578f5..beab2ce897 100644 --- a/lib/compiler/src/sys_core_fold.erl +++ b/lib/compiler/src/sys_core_fold.erl @@ -70,7 +70,8 @@ -export([module/2,format_error/1]). -import(lists, [map/2,foldl/3,foldr/3,mapfoldl/3,all/2,any/2, - reverse/1,reverse/2,member/2,nth/2,flatten/1,unzip/1]). + reverse/1,reverse/2,member/2,nth/2,flatten/1, + unzip/1,keyfind/3]). -import(cerl, [ann_c_cons/3,ann_c_map/3,ann_c_tuple/2]). @@ -1624,12 +1625,11 @@ eval_case(Case, _) -> Case. eval_case_warn(#c_primop{anno=Anno, name=#c_literal{val=match_fail}, - args=[#c_literal{val=Reason}]}=Core) - when is_atom(Reason) -> - case member(eval_failure, Anno) of + args=[_]}=Core) -> + case keyfind(eval_failure, 1, Anno) of false -> ok; - true -> + {eval_failure,Reason} -> %% Example: M = not_map, M#{k:=v} add_warning(Core, {eval_failure,Reason}) end; diff --git a/lib/compiler/src/v3_core.erl b/lib/compiler/src/v3_core.erl index c954d21e59..ed7b55df07 100644 --- a/lib/compiler/src/v3_core.erl +++ b/lib/compiler/src/v3_core.erl @@ -538,19 +538,8 @@ expr({tuple,L,Es0}, St0) -> {annotate_tuple(A, Es1, St1),Eps,St1}; expr({map,L,Es0}, St0) -> map_build_pairs(#c_literal{val=#{}}, Es0, full_anno(L, St0), St0); -expr({map,L,M0,Es0}, St0) -> - try expr_map(M0,Es0,lineno_anno(L, St0),St0) of - {_,_,_}=Res -> Res - catch - throw:{bad_map,Warning} -> - St = add_warning(L, Warning, St0), - LineAnno = lineno_anno(L, St), - As = [#c_literal{anno=LineAnno,val=badarg}], - {#icall{anno=#a{anno=LineAnno}, %Must have an #a{} - module=#c_literal{anno=LineAnno,val=erlang}, - name=#c_literal{anno=LineAnno,val=error}, - args=As},[],St} - end; +expr({map,L,M,Es}, St) -> + expr_map(M, Es, L, St); expr({bin,L,Es0}, St0) -> try expr_bin(Es0, full_anno(L, St0), St0) of {_,_,_}=Res -> Res @@ -758,11 +747,14 @@ make_bool_switch_guard(L, E, V, T, F) -> {clause,NegL,[V],[],[V]} ]}. -expr_map(M0, Es0, A, St0) -> +expr_map(M0, Es0, L, St0) -> {M1,Eps0,St1} = safe(M0, St0), + Badmap = badmap_term(M1, St1), + A = lineno_anno(L, St1), + Fc = fail_clause([], [{eval_failure,badmap}|A], Badmap), case is_valid_map_src(M1) of true -> - {M2,Eps1,St2} = map_build_pairs(M1, Es0, A, St1), + {M2,Eps1,St2} = map_build_pairs(M1, Es0, full_anno(L, St1), St1), M3 = case Es0 of [] -> M1; [_|_] -> M2 @@ -775,13 +767,23 @@ expr_map(M0, Es0, A, St0) -> name=#c_literal{anno=A,val=is_map}, args=[M1]}], body=[M3]}], - Fc = fail_clause([], [eval_failure|A], #c_literal{val=badarg}), Eps = Eps0 ++ Eps1, {#icase{anno=#a{anno=A},args=[],clauses=Cs,fc=Fc},Eps,St2}; false -> - throw({bad_map,bad_map}) + %% Not a map source. The update will always fail. + St2 = add_warning(L, badmap, St1), + #iclause{body=[Fail]} = Fc, + {Fail,Eps0,St2} end. +badmap_term(_Map, #core{in_guard=true}) -> + %% The code generator cannot handle complex error reasons + %% in guards. But the exact error reason does not matter anyway + %% since it is not user-visible. + #c_literal{val=badmap}; +badmap_term(Map, #core{in_guard=false}) -> + #c_tuple{es=[#c_literal{val=badmap},Map]}. + map_build_pairs(Map, Es0, Ann, St0) -> {Es,Pre,St1} = map_build_pairs_1(Es0, St0), {ann_c_map(Ann, Map, Es),Pre,St1}. @@ -2395,7 +2397,7 @@ format_error(nomatch) -> "pattern cannot possibly match"; format_error(bad_binary) -> "binary construction will fail because of a type mismatch"; -format_error(bad_map) -> +format_error(badmap) -> "map construction will fail because of a type mismatch". add_warning(Line, Term, #core{ws=Ws,file=[{file,File}]}=St) when Line >= 0 -> diff --git a/lib/compiler/test/map_SUITE.erl b/lib/compiler/test/map_SUITE.erl index 7db467a0d2..8768e47b65 100644 --- a/lib/compiler/test/map_SUITE.erl +++ b/lib/compiler/test/map_SUITE.erl @@ -669,9 +669,9 @@ t_map_size(Config) when is_list(Config) -> false = map_is_size(M#{ "c" => 2}, 2), %% Error cases. - {'EXIT',{badarg,_}} = (catch map_size([])), - {'EXIT',{badarg,_}} = (catch map_size(<<1,2,3>>)), - {'EXIT',{badarg,_}} = (catch map_size(1)), + {'EXIT',{{badmap,[]},_}} = (catch map_size([])), + {'EXIT',{{badmap,<<1,2,3>>},_}} = (catch map_size(<<1,2,3>>)), + {'EXIT',{{badmap,1},_}} = (catch map_size(1)), ok. map_is_size(M,N) when map_size(M) =:= N -> true; @@ -874,9 +874,9 @@ t_update_map_expressions(Config) when is_list(Config) -> #{ "a" := b } = F(), - %% Error cases, FIXME: should be 'badmap'? - {'EXIT',{badarg,_}} = (catch (id(<<>>))#{ a := 42, b => 2 }), - {'EXIT',{badarg,_}} = (catch (id([]))#{ a := 42, b => 2 }), + %% Error cases. + {'EXIT',{{badmap,<<>>},_}} = (catch (id(<<>>))#{ a := 42, b => 2 }), + {'EXIT',{{badmap,[]},_}} = (catch (id([]))#{ a := 42, b => 2 }), ok. @@ -897,8 +897,14 @@ t_update_assoc(Config) when is_list(Config) -> %% Errors cases. BadMap = id(badmap), - {'EXIT',{badarg,_}} = (catch BadMap#{nonexisting=>val}), - {'EXIT',{badarg,_}} = (catch <<>>#{nonexisting=>val}), + {'EXIT',{{badmap,BadMap},_}} = (catch BadMap#{nonexisting=>val}), + {'EXIT',{{badmap,<<>>},_}} = (catch <<>>#{nonexisting=>val}), + + %% Evaluation order. + {'EXIT',{blurf,_}} = + (catch BadMap#{whatever=>id(error(blurf))}), + {'EXIT',{blurf,_}} = + (catch BadMap#{id(error(blurf))=>whatever}), ok. t_update_assoc_large(Config) when is_list(Config) -> @@ -965,8 +971,8 @@ t_update_assoc_large(Config) when is_list(Config) -> M2 = M0#{13.0:=wrong,13.0=>new}, %% Errors cases. - BadMap = id(badmap), - {'EXIT',{badarg,_}} = (catch BadMap#{nonexisting=>M0}), + BadMap = id({no,map}), + {'EXIT',{{badmap,BadMap},_}} = (catch BadMap#{nonexisting=>M0}), ok. @@ -991,19 +997,29 @@ t_update_exact(Config) when is_list(Config) -> 1.0 => new_val4 }, %% Errors cases. - {'EXIT',{badarg,_}} = (catch ((id(nil))#{ a := b })), - {'EXIT',{badarg,_}} = (catch M0#{nonexisting:=val}), - {'EXIT',{badarg,_}} = (catch M0#{1.0:=v,1.0=>v2}), - {'EXIT',{badarg,_}} = (catch M0#{42.0:=v,42:=v2}), - {'EXIT',{badarg,_}} = (catch M0#{42=>v1,42.0:=v2,42:=v3}), - {'EXIT',{badarg,_}} = (catch <<>>#{nonexisting:=val}), - {'EXIT',{badarg,_}} = (catch M0#{<<0:257>> := val}), %% limitation + {'EXIT',{{badmap,nil},_}} = (catch ((id(nil))#{ a := b })), + {'EXIT',{{badkey,nonexisting},_}} = (catch M0#{nonexisting:=val}), + {'EXIT',{{badkey,1.0},_}} = (catch M0#{1.0:=v,1.0=>v2}), + {'EXIT',{{badkey,42},_}} = (catch M0#{42.0:=v,42:=v2}), + {'EXIT',{{badkey,42.0},_}} = (catch M0#{42=>v1,42.0:=v2,42:=v3}), + {'EXIT',{{badmap,<<>>},_}} = (catch <<>>#{nonexisting:=val}), + {'EXIT',{{badkey,<<0:257>>},_}} = + (catch M0#{<<0:257>> := val}), %limitation %% A workaround for a bug allowed an empty map to be updated. - {'EXIT',{badarg,_}} = (catch (id(#{}))#{a:=1}), - {'EXIT',{badarg,_}} = (catch #{}#{a:=1}), + {'EXIT',{{badkey,a},_}} = (catch (id(#{}))#{a:=1}), + {'EXIT',{{badkey,a},_}} = (catch #{}#{a:=1}), Empty = #{}, - {'EXIT',{badarg,_}} = (catch Empty#{a:=1}), + {'EXIT',{{badkey,a},_}} = (catch Empty#{a:=1}), + + %% Evaluation order. + BadMap = id([no,map]), + {'EXIT',{blurf,_}} = + (catch BadMap#{whatever:=id(error(blurf))}), + {'EXIT',{blurf,_}} = + (catch BadMap#{id(error(blurf)):=whatever}), + {'EXIT',{{badmap,BadMap},_}} = + (catch BadMap#{id(nonexisting):=whatever}), ok. t_update_exact_large(Config) when is_list(Config) -> @@ -1081,10 +1097,10 @@ t_update_exact_large(Config) when is_list(Config) -> M2 = M0#{13.0=>wrong,13.0:=new}, %% Errors cases. - {'EXIT',{badarg,_}} = (catch M0#{nonexisting:=val}), - {'EXIT',{badarg,_}} = (catch M0#{1.0:=v,1.0=>v2}), - {'EXIT',{badarg,_}} = (catch M0#{42.0:=v,42:=v2}), - {'EXIT',{badarg,_}} = (catch M0#{42=>v1,42.0:=v2,42:=v3}), + {'EXIT',{{badkey,nonexisting},_}} = (catch M0#{nonexisting:=val}), + {'EXIT',{{badkey,1.0},_}} = (catch M0#{1.0:=v,1.0=>v2}), + {'EXIT',{{badkey,42},_}} = (catch M0#{42.0:=v,42:=v2}), + {'EXIT',{{badkey,42.0},_}} = (catch M0#{42=>v1,42.0:=v2,42:=v3}), ok. @@ -1666,8 +1682,8 @@ t_update_assoc_variables(Config) when is_list(Config) -> %% Errors cases. BadMap = id(badmap), - {'EXIT',{badarg,_}} = (catch BadMap#{nonexisting=>val}), - {'EXIT',{badarg,_}} = (catch <<>>#{nonexisting=>val}), + {'EXIT',{{badmap,BadMap},_}} = (catch BadMap#{nonexisting=>val}), + {'EXIT',{{badmap,<<>>},_}} = (catch <<>>#{nonexisting=>val}), ok. t_update_exact_variables(Config) when is_list(Config) -> @@ -1697,13 +1713,14 @@ t_update_exact_variables(Config) when is_list(Config) -> #{ "wat" := 3, 2 := a } = id(#{ "wat" => 1, K2 => 2 }#{ K2 := a, "wat" := 3 }), %% Errors cases. - {'EXIT',{badarg,_}} = (catch ((id(nil))#{ a := b })), - {'EXIT',{badarg,_}} = (catch M0#{nonexisting:=val}), - {'EXIT',{badarg,_}} = (catch M0#{1.0:=v,1.0=>v2}), - {'EXIT',{badarg,_}} = (catch M0#{42.0:=v,42:=v2}), - {'EXIT',{badarg,_}} = (catch M0#{42=>v1,42.0:=v2,42:=v3}), - {'EXIT',{badarg,_}} = (catch <<>>#{nonexisting:=val}), - {'EXIT',{badarg,_}} = (catch M0#{<<0:257>> := val}), %% limitation + {'EXIT',{{badmap,nil},_}} = (catch ((id(nil))#{ a := b })), + {'EXIT',{{badkey,nonexisting},_}} = (catch M0#{nonexisting:=val}), + {'EXIT',{{badkey,1.0},_}} = (catch M0#{1.0:=v,1.0=>v2}), + {'EXIT',{{badkey,42},_}} = (catch M0#{42.0:=v,42:=v2}), + {'EXIT',{{badkey,42.0},_}} = (catch M0#{42=>v1,42.0:=v2,42:=v3}), + {'EXIT',{{badmap,<<>>},_}} = (catch <<>>#{nonexisting:=val}), + {'EXIT',{{badkey,<<0:257>>},_}} = + (catch M0#{<<0:257>> := val}), %limitation ok. t_nested_pattern_expressions(Config) when is_list(Config) -> diff --git a/lib/compiler/test/warnings_SUITE.erl b/lib/compiler/test/warnings_SUITE.erl index d0b7c71be8..e996a55db6 100644 --- a/lib/compiler/test/warnings_SUITE.erl +++ b/lib/compiler/test/warnings_SUITE.erl @@ -583,7 +583,7 @@ maps(Config) when is_list(Config) -> ok. ">>, [], - {warnings,[{4,sys_core_fold,{eval_failure,badarg}}]}}, + {warnings,[{4,sys_core_fold,{eval_failure,badmap}}]}}, {bad_map_src2, <<" t() -> @@ -601,7 +601,7 @@ maps(Config) when is_list(Config) -> ok. ">>, [], - {warnings,[{3,v3_core,bad_map}]}}, + {warnings,[{3,v3_core,badmap}]}}, {ok_map_literal_key, <<" t() -> -- cgit v1.2.3