From edea6ed4dfc27e0836da737c7611628ba6f3056f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gustavsson?= Date: Mon, 5 Aug 2024 13:49:13 +0200 Subject: [PATCH] compiler: Add support debug information in BEAM files --- lib/compiler/src/beam_asm.erl | 134 ++++- lib/compiler/src/beam_block.erl | 7 +- lib/compiler/src/beam_core_to_ssa.erl | 62 ++- lib/compiler/src/beam_dict.erl | 33 +- lib/compiler/src/beam_flatten.erl | 1 + lib/compiler/src/beam_ssa_codegen.erl | 245 +++++++- lib/compiler/src/beam_ssa_pre_codegen.erl | 128 ++++- lib/compiler/src/beam_trim.erl | 12 + lib/compiler/src/beam_validator.erl | 32 +- lib/compiler/src/compile.erl | 3 + lib/compiler/src/sys_coverage.erl | 2 +- lib/compiler/src/v3_core.erl | 14 +- lib/compiler/test/Makefile | 4 + lib/compiler/test/beam_debug_info_SUITE.erl | 524 ++++++++++++++++++ lib/compiler/test/compile_SUITE.erl | 23 +- .../test/compile_SUITE_data/small.erl | 4 +- lib/compiler/test/test_lib.erl | 52 +- 17 files changed, 1193 insertions(+), 87 deletions(-) create mode 100644 lib/compiler/test/beam_debug_info_SUITE.erl diff --git a/lib/compiler/src/beam_asm.erl b/lib/compiler/src/beam_asm.erl index 817d740b0206..5d79df4247a4 100644 --- a/lib/compiler/src/beam_asm.erl +++ b/lib/compiler/src/beam_asm.erl @@ -27,11 +27,14 @@ -export_type([fail/0,label/0,src/0,module_code/0,function_name/0]). --import(lists, [append/1,duplicate/2,map/2,member/2,keymember/3,splitwith/2]). +-import(lists, [append/1,duplicate/2,keymember/3,last/1,map/2, + member/2,splitwith/2]). -include("beam_opcodes.hrl"). -include("beam_asm.hrl"). +-define(BEAM_DEBUG_INFO_VERSION, 0). + %% Common types for describing operands for BEAM instructions. -type src() :: beam_reg() | {'literal',term()} | @@ -60,23 +63,24 @@ -define(BEAMFILE_EXECUTABLE_LINE, 1). -define(BEAMFILE_FORCE_LINE_COUNTERS, 2). --spec module(module_code(), [{binary(), binary()}], [{atom(),term()}], [compile:option()]) -> - {'ok',binary()}. - -module(Code, ExtraChunks, CompileInfo, CompilerOpts) -> - {ok,assemble(Code, ExtraChunks, CompileInfo, CompilerOpts)}. +-spec module(module_code(), [{binary(), binary()}], + [{atom(),term()}], [compile:option()]) -> + {'ok',binary()}. -assemble({Mod,Exp0,Attr0,Asm0,NumLabels}, ExtraChunks, CompileInfo, CompilerOpts) -> +module(Code0, ExtraChunks, CompileInfo, CompilerOpts) -> + {Mod,Exp0,Attr0,Asm0,NumLabels} = Code0, {1,Dict0} = beam_dict:atom(Mod, beam_dict:new()), {0,Dict1} = beam_dict:fname(atom_to_list(Mod) ++ ".erl", Dict0), {0,Dict2} = beam_dict:type(any, Dict1), Dict3 = reject_unsupported_versions(Dict2), + NumFuncs = length(Asm0), {Asm,Attr} = on_load(Asm0, Attr0), Exp = sets:from_list(Exp0), - {Code,Dict} = assemble_1(Asm, Exp, Dict3, []), - build_file(Code, Attr, Dict, NumLabels, NumFuncs, - ExtraChunks, CompileInfo, CompilerOpts). + {Code,Dict} = assemble(Asm, Exp, Dict3, []), + Beam = build_file(Code, Attr, Dict, NumLabels, NumFuncs, + ExtraChunks, CompileInfo, CompilerOpts), + {ok,Beam}. reject_unsupported_versions(Dict) -> %% Emit an instruction that was added in our lowest supported @@ -106,7 +110,7 @@ insert_on_load_instruction(Is0, Entry) -> end, Is0), Bef ++ [El,on_load|Is]. -assemble_1([{function,Name,Arity,Entry,Asm}|T], Exp, Dict0, Acc) -> +assemble([{function,Name,Arity,Entry,Asm}|T], Exp, Dict0, Acc) -> Dict1 = case sets:is_element({Name,Arity}, Exp) of true -> beam_dict:export(Name, Arity, Entry, Dict0); @@ -114,8 +118,8 @@ assemble_1([{function,Name,Arity,Entry,Asm}|T], Exp, Dict0, Acc) -> beam_dict:local(Name, Arity, Entry, Dict0) end, {Code, Dict2} = assemble_function(Asm, Acc, Dict1), - assemble_1(T, Exp, Dict2, Code); -assemble_1([], _Exp, Dict0, Acc) -> + assemble(T, Exp, Dict2, Code); +assemble([], _Exp, Dict0, Acc) -> {IntCodeEnd,Dict1} = make_op(int_code_end, Dict0), {list_to_binary(lists:reverse(Acc, [IntCodeEnd])),Dict1}. @@ -125,17 +129,23 @@ assemble_function([H|T], Acc, Dict0) -> assemble_function([], Code, Dict) -> {Code, Dict}. -build_file(Code, Attr, Dict, NumLabels, NumFuncs, ExtraChunks0, CompileInfo, CompilerOpts) -> +build_file(Code, Attr, Dict0, NumLabels, NumFuncs, ExtraChunks0, + CompileInfo, CompilerOpts) -> %% Create the code chunk. CodeChunk = chunk(<<"Code">>, <<16:32, (beam_opcodes:format_number()):32, - (beam_dict:highest_opcode(Dict)):32, + (beam_dict:highest_opcode(Dict0)):32, NumLabels:32, NumFuncs:32>>, Code), + %% Build the BEAM debug information chunk. It is important + %% to build it early, because it will add entries to the + %% atom and literal tables. + {ExtraChunks1,Dict} = build_beam_debug_info(ExtraChunks0, CompilerOpts, Dict0), + %% Create the atom table chunk. {NumAtoms, AtomTab} = beam_dict:atom_table(Dict), AtomChunk = chunk(<<"AtU8">>, <>, AtomTab), @@ -194,13 +204,14 @@ build_file(Code, Attr, Dict, NumLabels, NumFuncs, ExtraChunks0, CompileInfo, Com TypeTab), %% Create the meta chunk - Meta = proplists:get_value(<<"Meta">>, ExtraChunks0, empty), + Meta = proplists:get_value(<<"Meta">>, ExtraChunks1, empty), MetaChunk = case Meta of empty -> []; Meta -> chunk(<<"Meta">>, Meta) end, + %% Remove Meta chunk from ExtraChunks since it is essential - ExtraChunks = ExtraChunks0 -- [{<<"Meta">>, Meta}], + ExtraChunks = ExtraChunks1 -- [{<<"Meta">>, Meta}], %% Create the attributes and compile info chunks. @@ -352,6 +363,91 @@ filter_essentials([<<>>|T]) -> filter_essentials(T); filter_essentials([]) -> []. +%%% +%%% Build the BEAM debug information chunk. +%%% + +build_beam_debug_info(ExtraChunks, CompilerOpts, Dict) -> + case member(beam_debug_info, CompilerOpts) of + true -> + build_beam_debug_info_1(ExtraChunks, Dict); + false -> + {ExtraChunks,Dict} + end. + +build_beam_debug_info_1(ExtraChunks0, Dict0) -> + DebugTab0 = beam_dict:debug_table(Dict0), + DebugTab1 = [{Index,Info} || + Index := Info <- maps:iterator(DebugTab0, ordered)], + DebugTab = build_bdi_fill_holes(DebugTab1), + {Contents0,Dict} = build_bdi(DebugTab, Dict0), + Contents1 = iolist_to_binary(Contents0), + Contents = <>, + ExtraChunks = [{~"DbgB",Contents}|ExtraChunks0], + {ExtraChunks,Dict}. + +build_bdi_fill_holes([{_,Item}]) -> + [Item]; +build_bdi_fill_holes([{I0,Item}|[{I1,_}|_]=T]) -> + case I0 + 1 of + I1 -> + [Item|build_bdi_fill_holes(T)]; + Next -> + NewPair = {Next,{none,[]}}, + [Item|build_bdi_fill_holes([NewPair|T])] + end. + +build_bdi([{FrameSize0,Vars0}|Items], Dict0) -> + %% The debug information utilizes the encoding machinery for BEAM + %% instructions. The debug information for `executable_line` + %% instruction is translated to: + %% + %% {call,FrameSize,{list,[VariableName,Where,...]}} + %% + %% Where: + %% + %% FrameSize := 'none' | 0..1023 + %% VariableName := binary() + %% Where := {x,0..1023} | {y,0..1023} | {literal,_} | + %% {integer,_} | {atom,_} | {float,_} | nil + %% + %% The only reason the `call` instruction is used is because it + %% has two operands. + %% + %% Example: + %% + %% {executable_line,[...],1, + %% {4, [{'Args',[{y,3}]}, + %% {'Line',[{y,2}]}, + %% {'Live',[{x,0},{y,1}]}]}}. + %% + %% As an instruction: + %% + %% {call,4,{list,[{literal,<<"Args">>},{y,3}, + %% {literal,<<"Line">>},{y,2}, + %% {literal,<<"Live">>},{y,1}]}} + %% + %% Note that only one register is given for each variable. It + %% is always the last register listed. + + FrameSize = case FrameSize0 of + none -> nil; + _ -> FrameSize0 + end, + Vars1 = [[{literal,atom_to_binary(Name)},last(Regs)] || + {Name,[_|_]=Regs} <- Vars0], + Vars = append(Vars1), + Instr0 = {call,FrameSize,{list,Vars}}, + {Instr,Dict1} = make_op(Instr0, Dict0), + {Tail,Dict2} = build_bdi(Items, Dict1), + {[Instr|Tail],Dict2}; +build_bdi([], Dict) -> + {[],Dict}. + +%%% +%%% Functions for assembling BEAM instruction. +%%% + bif_type(fnegate, 1) -> {op,fnegate}; bif_type(fadd, 2) -> {op,fadd}; bif_type(fsub, 2) -> {op,fsub}; @@ -368,6 +464,10 @@ make_op({line=Op,Location}, Dict0) -> make_op({executable_line=Op,Location,Index}, Dict0) -> {LocationIndex,Dict} = beam_dict:line(Location, Dict0, Op), encode_op(executable_line, [LocationIndex,Index], Dict); +make_op({executable_line=Op,Location,Index,DebugInfo}, Dict0) -> + {LocationIndex,Dict1} = beam_dict:line(Location, Dict0, Op), + Dict = beam_dict:debug_info(Index, DebugInfo, Dict1), + encode_op(executable_line, [LocationIndex,Index], Dict); make_op({bif, Bif, {f,_}, [], Dest}, Dict) -> %% BIFs without arguments cannot fail. encode_op(bif0, [{extfunc, erlang, Bif, 0}, Dest], Dict); diff --git a/lib/compiler/src/beam_block.erl b/lib/compiler/src/beam_block.erl index 4ddc88874a32..f3c888737229 100644 --- a/lib/compiler/src/beam_block.erl +++ b/lib/compiler/src/beam_block.erl @@ -25,7 +25,7 @@ -include("beam_asm.hrl"). -export([module/2]). --import(lists, [keysort/2,member/2,reverse/1,reverse/2, +-import(lists, [flatmap/2,keysort/2,member/2,reverse/1,reverse/2, splitwith/2,usort/1]). -spec module(beam_utils:module_code(), [compile:option()]) -> @@ -172,12 +172,17 @@ collect({put_map,{f,0},Op,S,D,R,{list,Puts}}) -> collect({fmove,S,D}) -> {set,[D],[S],fmove}; collect({fconv,S,D}) -> {set,[D],[S],fconv}; collect({executable_line,_,_}=Line) -> {set,[],[],Line}; +collect({executable_line,_,_,_}=Line) -> collect_executable_line(Line); collect({swap,D1,D2}) -> Regs = [D1,D2], {set,Regs,Regs,swap}; collect({make_fun3,F,I,U,D,{list,Ss}}) -> {set,[D],Ss,{make_fun3,F,I,U}}; collect(_) -> error. +collect_executable_line({executable_line,_Loc,_Index,{_,Vars}}=I) -> + Ss = flatmap(fun({_Name,Regs}) -> Regs end, Vars), + {set,[],Ss,I}. + %% embed_lines([Instruction]) -> [Instruction] %% Combine blocks that would be split by line/1 instructions. %% Also move a line instruction before a block into the block, diff --git a/lib/compiler/src/beam_core_to_ssa.erl b/lib/compiler/src/beam_core_to_ssa.erl index 8a87c49ad0c2..8f4799dec9af 100644 --- a/lib/compiler/src/beam_core_to_ssa.erl +++ b/lib/compiler/src/beam_core_to_ssa.erl @@ -67,7 +67,7 @@ map/2,mapfoldl/3,member/2, keyfind/3,keysort/2,last/1, partition/2,reverse/1,reverse/2, - sort/1,sort/2,splitwith/2, + sort/1,sort/2,usort/1,splitwith/2, zip/2]). -import(ordsets, [add_element/2,del_element/2,intersection/2, subtract/2,union/2,union/1]). @@ -94,7 +94,7 @@ %% matching. (Construction of those term types is translated directly %% to SSA instructions.) --record(cg_tuple, {es}). +-record(cg_tuple, {es,keep=[]}). -record(cg_map, {var=#b_literal{val=#{}},op,es}). -record(cg_map_pair, {key,val}). -record(cg_cons, {hd,tl}). @@ -149,7 +149,8 @@ get_anno(#cg_select{anno=Anno}) -> Anno. funs=[], %Fun functions free=#{}, %Free variables ws=[] :: [warning()], %Warnings. - no_min_max_bifs=false :: boolean() + no_min_max_bifs=false :: boolean(), + beam_debug_info=false :: boolean() }). -spec module(cerl:c_module(), [compile:option()]) -> @@ -159,8 +160,10 @@ module(#c_module{name=#c_literal{val=Mod},exports=Es,attrs=As,defs=Fs}, Options) Kas = attributes(As), Kes = map(fun (#c_var{name={_,_}=Fname}) -> Fname end, Es), NoMinMaxBifs = proplists:get_bool(no_min_max_bifs, Options), + DebugInfo = proplists:get_bool(beam_debug_info, Options), St0 = #kern{module=Mod, - no_min_max_bifs=NoMinMaxBifs}, + no_min_max_bifs=NoMinMaxBifs, + beam_debug_info=DebugInfo}, {Kfs,St} = mapfoldl(fun function/2, St0, Fs), Body = Kfs ++ St#kern.funs, Code = #b_module{name=Mod,exports=Kes,attributes=Kas,body=Body}, @@ -369,6 +372,14 @@ expr(#c_call{anno=A,module=M0,name=F0,args=Cargs}, Sub, St0) -> args=[M0,F0,cerl:make_list(Cargs)]}, expr(Call, Sub, St) end; +expr(#c_primop{anno=A0,name=#c_literal{val=executable_line=Op}, + args=Cargs}, Sub, #kern{beam_debug_info=true}=St0) -> + {Args,Ap,St1} = atomic_list(Cargs, Sub, St0), + #b_set{anno=A1} = I0 = primop(Op, A0, Args), + {_,Alias} = Sub, + A = A1#{alias => Alias}, + I = I0#b_set{anno=A}, + {I,Ap,St1}; expr(#c_primop{anno=A,name=#c_literal{val=match_fail},args=[Arg]}, Sub, St) -> translate_match_fail(Arg, Sub, A, St); expr(#c_primop{anno=A,name=#c_literal{val=Op},args=Cargs}, Sub, St0) -> @@ -1666,9 +1677,20 @@ get_match(#cg_bin_seg{}=Seg, St0) -> get_match(#cg_bin_int{}=BinInt, St0) -> {N,St1} = new_var(St0), {BinInt#cg_bin_int{next=N},[N],St1}; -get_match(#cg_tuple{es=Es}, St0) -> +get_match(#cg_tuple{es=Es}, #kern{beam_debug_info=DebugInfo}=St0) -> {Mes,St1} = new_vars(length(Es), St0), - {#cg_tuple{es=Mes},Mes,St1}; + Keep = + case DebugInfo of + true -> + %% Force extraction of all variables mentioned in the original source. + Keep0 = [New || + {#b_var{name=Old},#b_var{name=New}} <- lists:zip(Es, Mes), + beam_ssa_codegen:is_original_variable(Old)], + ordsets:from_list(Keep0); + false -> + [] + end, + {#cg_tuple{es=Mes,keep=Keep},Mes,St1}; get_match(#cg_map{op=exact,es=Es0}, St0) -> {Mes,St1} = new_vars(length(Es0), St0), {Es,_} = mapfoldl(fun(#cg_map_pair{}=Pair, [V|Vs]) -> @@ -2252,12 +2274,13 @@ umatch_list(Ms0, Br, St) -> {[M1|Ms1],union(Mu, Us),Stb} end, {[],[],St}, Ms0). -pat_mark_unused(#cg_tuple{es=Es0}=P, Used0, Ps) -> +pat_mark_unused(#cg_tuple{es=Es0,keep=Keep}=P, Used0, Ps) -> %% Not extracting unused tuple elements is an optimization for %% compile time and memory use during compilation. It is probably %% worthwhile because it is common to extract only a few elements %% from a huge record. - Used = intersection(Used0, Ps), + Used1 = ordsets:union(Used0, Keep), + Used = intersection(Used1, Ps), Es = [case member(V, Used) of true -> Var; false -> #b_literal{val=unused} @@ -2374,6 +2397,29 @@ cg(#b_set{op=copy,dst=#b_var{name=Dst},args=[Arg0]}, St0) -> Arg = ssa_arg(Arg0, St0), St = set_ssa_var(Dst, Arg, St0), {[],St}; +cg(#b_set{anno=Anno0,op=executable_line,args=Args0}=Set0, St) -> + Args = ssa_args(Args0, St), + Literals = [{Val,From} || From := #b_literal{val=Val} <- St#cg.vars], + Anno1 = Anno0#{literals => Literals}, + NewAlias = [{To,From} || From := #b_var{name=To} <- St#cg.vars], + case NewAlias of + [_|_] -> + Alias0 = maps:get(alias, Anno0, #{}), + Alias1 = foldl(fun({To,From}, A) -> + case A of + #{To := Vars0} -> + Vars1 = usort([From|Vars0]), + A#{To := Vars1}; + #{} -> + A#{To => [From]} + end + end, Alias0, NewAlias), + Anno = Anno1#{alias => Alias1}, + Set = Set0#b_set{anno=Anno,args=Args}, + {[Set],St}; + [] -> + {[Set0#b_set{anno=Anno1,args=Args}],St} + end; cg(#b_set{args=Args0}=Set0, St) -> Args = ssa_args(Args0, St), Set = Set0#b_set{args=Args}, diff --git a/lib/compiler/src/beam_dict.erl b/lib/compiler/src/beam_dict.erl index b9e1d0d142ef..ebd8cfab5ca0 100644 --- a/lib/compiler/src/beam_dict.erl +++ b/lib/compiler/src/beam_dict.erl @@ -23,11 +23,12 @@ -moduledoc false. -export([new/0,opcode/2,highest_opcode/1, - atom/2,local/4,export/4,import/4, - string/2,lambda/3,literal/2,line/3,fname/2,type/2, - atom_table/1,local_table/1,export_table/1,import_table/1, - string_table/1,lambda_table/1,literal_table/1, - line_table/1,type_table/1]). + atom/2,local/4,export/4,import/4, + string/2,lambda/3,literal/2,line/3, + fname/2,type/2,debug_info/3, + atom_table/1,local_table/1,export_table/1,import_table/1, + string_table/1,lambda_table/1,literal_table/1, + line_table/1,type_table/1,debug_table/1]). -include("beam_types.hrl"). @@ -35,13 +36,16 @@ -type index() :: non_neg_integer(). +-type frame_size() :: 'none' | non_neg_integer(). +-type debug_info() :: {frame_size(), list()}. + -type atom_tab() :: #{atom() => index()}. -type import_tab() :: gb_trees:tree(mfa(), index()). -type fname_tab() :: #{Name :: term() => index()}. -type line_tab() :: #{{Fname :: index(), Line :: term()} => index()}. -type literal_tab() :: #{Literal :: term() => index()}. --type type_tab() :: #{ Type :: binary() => index()}. - +-type type_tab() :: #{Type :: binary() => index()}. +-type debug_tab() :: #{index() => debug_info()}. -type lambda_info() :: {label(),{index(),label(),non_neg_integer()}}. -type lambda_tab() :: {non_neg_integer(),[lambda_info()]}. -type wrapper() :: #{label() => index()}. @@ -58,6 +62,7 @@ literals = #{} :: literal_tab(), fnames = #{} :: fname_tab(), lines = #{} :: line_tab(), + debug = #{} :: debug_tab(), num_lines = 0 :: non_neg_integer(), %Number of line instructions exec_line = false :: boolean(), next_import = 0 :: non_neg_integer(), @@ -251,6 +256,14 @@ type(Type, #asm{types=Types0}=Dict) -> {Index, Dict#asm{types=Types}} end. +-spec debug_info(index(), debug_info(), bdict()) -> bdict(). + +debug_info(Index, DebugInfo, #asm{debug=DebugTab0}=Dict) + when is_integer(Index) -> + false = is_map_key(Index, DebugTab0), %Assertion. + DebugTab = DebugTab0#{Index => DebugInfo}, + Dict#asm{debug=DebugTab}. + %% Returns the atom table. %% atom_table(Dict, Encoding) -> {LastIndex,[Length,AtomString...]} -spec atom_table(bdict()) -> {non_neg_integer(), [[non_neg_integer(),...]]}. @@ -358,6 +371,12 @@ line_table(#asm{fnames=Fnames0,lines=Lines0, Lines = [L || {L,_} <- Lines1], {NumLineInstrs,NumFnames,Fnames,NumLines,Lines,ExecLine}. + +-spec debug_table(bdict()) -> debug_tab(). + +debug_table(#asm{debug=Debug}) -> + Debug. + %% Search for binary string Str in the binary string pool Pool. %% old_string(Str, Pool) -> none | Index -spec old_string(binary(), binary()) -> 'none' | pos_integer(). diff --git a/lib/compiler/src/beam_flatten.erl b/lib/compiler/src/beam_flatten.erl index a2011b8752b2..24315f93929b 100644 --- a/lib/compiler/src/beam_flatten.erl +++ b/lib/compiler/src/beam_flatten.erl @@ -65,6 +65,7 @@ norm({set,[D],[S|Puts],{alloc,R,{put_map,Op,F}}}) -> norm({set,[],[],remove_message}) -> remove_message; norm({set,[],[],{line,_}=Line}) -> Line; norm({set,[],[],{executable_line,_,_}=Line}) -> Line; +norm({set,[],_,{executable_line,_,_,_}=Line}) -> Line; norm({set,[D1,D2],[D1,D2],swap}) -> {swap,D1,D2}. norm_allocate({_Zero,nostack,Nh,[]}, Regs) -> diff --git a/lib/compiler/src/beam_ssa_codegen.erl b/lib/compiler/src/beam_ssa_codegen.erl index 2ed93d5e182c..682db7efc1ff 100644 --- a/lib/compiler/src/beam_ssa_codegen.erl +++ b/lib/compiler/src/beam_ssa_codegen.erl @@ -23,6 +23,7 @@ -moduledoc false. -export([module/2]). +-export([is_original_variable/1]). %Called from beam_core_to_ssa. -export([classify_heap_need/2]). %Called from beam_ssa_pre_codegen. -export_type([ssa_register/0]). @@ -41,14 +42,16 @@ regs=#{} :: #{beam_ssa:b_var() => ssa_register()}, ultimate_fail=1 :: beam_label(), catches=gb_sets:empty() :: gb_sets:set(ssa_label()), - fc_label=1 :: beam_label() + fc_label=1 :: beam_label(), + debug_info=false :: boolean() }). -spec module(beam_ssa:b_module(), [compile:option()]) -> - {'ok',beam_asm:module_code()}. + {'ok',beam_asm:module_code()}. -module(#b_module{name=Mod,exports=Es,attributes=Attrs,body=Fs}, _Opts) -> - {Asm,St} = functions(Fs, {atom,Mod}), +module(#b_module{name=Mod,exports=Es,attributes=Attrs,body=Fs}, Opts) -> + DebugInfo = member(beam_debug_info, Opts), + {Asm,St} = functions(Fs, {atom,Mod}, DebugInfo), {ok,{Mod,Es,Attrs,Asm,St#cg.lcount}}. -record(need, {h=0 :: non_neg_integer(), % heap words @@ -109,12 +112,12 @@ module(#b_module{name=Mod,exports=Es,attributes=Attrs,body=Fs}, _Opts) -> -type ssa_register() :: xreg() | yreg() | freg() | zreg(). -functions(Forms, AtomMod) -> +functions(Forms, AtomMod, DebugInfo) -> mapfoldl(fun (F, St) -> function(F, AtomMod, St) end, - #cg{lcount=1}, Forms). + #cg{lcount=1,debug_info=DebugInfo}, Forms). -function(#b_function{anno=Anno,bs=Blocks}, AtomMod, St0) -> - #{func_info:={_,Name,Arity}} = Anno, +function(#b_function{anno=Anno,bs=Blocks,args=Args}, AtomMod, St0) -> + #{func_info := {_,Name,Arity}} = Anno, NoBsMatch = not maps:get(bs_ensure_opt, Anno, false), try assert_exception_block(Blocks), %Assertion. @@ -127,7 +130,7 @@ function(#b_function{anno=Anno,bs=Blocks}, AtomMod, St0) -> Labels = (St4#cg.labels)#{0=>Entry,?EXCEPTION_BLOCK=>0}, St5 = St4#cg{labels=Labels,used_labels=gb_sets:singleton(Entry), ultimate_fail=Ult}, - {Body,St} = cg_fun(Blocks, NoBsMatch, St5#cg{fc_label=Fi}), + {Body,St} = cg_fun(Blocks, Args, NoBsMatch, St5#cg{fc_label=Fi}), Asm = [{label,Fi},line(Anno), {func_info,AtomMod,{atom,Name},Arity}] ++ add_parameter_annos(Body, Anno) ++ @@ -166,7 +169,7 @@ add_parameter_annos([{label, _}=Entry | Body], Anno) -> [Entry | sort(Annos)] ++ Body. -cg_fun(Blocks, NoBsMatch, St0) -> +cg_fun(Blocks, Args, NoBsMatch, St0) -> Linear0 = linearize(Blocks), St1 = collect_catch_labels(Linear0, St0), Linear1 = need_heap(Linear0), @@ -174,7 +177,8 @@ cg_fun(Blocks, NoBsMatch, St0) -> Linear3 = liveness(Linear2, St1), Linear4 = defined(Linear3, St1), Linear5 = opt_allocate(Linear4, St1), - Linear = fix_wait_timeout(Linear5), + Linear6 = fix_wait_timeout(Linear5), + Linear = add_debug_info(Linear6, Args, St1), {Asm,St} = cg_linear(Linear, St1), case NoBsMatch of true -> {Asm,St}; @@ -245,12 +249,17 @@ need_heap_never(_) -> need_heap_blks([{L,#cg_blk{is=Is0}=Blk0}|Bs], H0, Acc) -> {Is1,H1} = need_heap_is(reverse(Is0), H0, []), {Ns,H} = need_heap_terminator(Bs, L, H1), - Is = Ns ++ Is1, + Is = delay_alloc(Ns ++ Is1), Blk = Blk0#cg_blk{is=Is}, need_heap_blks(Bs, H, [{L,Blk}|Acc]); need_heap_blks([], H, Acc) -> {Acc,H}. +delay_alloc([#cg_alloc{}=AI, + #cg_set{op=executable_line}=ELI|Is2]) -> + [ELI|delay_alloc([AI|Is2])]; +delay_alloc(Is) -> Is. + need_heap_is([#cg_alloc{words=Words}=Alloc0|Is], N, Acc) -> Alloc = Alloc0#cg_alloc{words=add_heap_words(N, Words)}, need_heap_is(Is, #need{}, [Alloc|Acc]); @@ -955,6 +964,212 @@ fix_wait_timeout_is([I|Is], Acc) -> fix_wait_timeout_is(Is, [I|Acc]); fix_wait_timeout_is([], _Acc) -> no. +%%% +%%% Collect debug information; that is the mapping from variable names +%%% to registers. +%%% + +add_debug_info(Linear0, Args, #cg{regs=Regs,debug_info=true}) -> + Def0 = ordsets:from_list(Args), + Linear = anno_defined_regs(Linear0, Def0, Regs), + FrameSzMap = #{0 => none}, + VarMap = #{}, + add_debug_info_blk(Linear, Regs, FrameSzMap, VarMap); +add_debug_info(Linear, _Args, #cg{debug_info=false}) -> + Linear. + +add_debug_info_blk([{L,#cg_blk{is=Is0,last=Last}=Blk0}|Bs], + Regs, FrameSzMap0, VarMap0) -> + FrameSize0 = map_get(L, FrameSzMap0), + {Is,VarMap,FrameSize} = + add_debug_info_is(Is0, Regs, FrameSize0, VarMap0, []), + Successors = successors(Last), + FrameSzMap = foldl(fun(Succ, Acc) -> + Acc#{Succ => FrameSize} + end, FrameSzMap0, Successors), + Blk = Blk0#cg_blk{is=Is}, + [{L,Blk}|add_debug_info_blk(Bs, Regs, FrameSzMap, VarMap)]; +add_debug_info_blk([], _Regs, _FrameSzMap, _VarMap) -> + []. + +add_debug_info_is([#cg_alloc{stack=FrameSize}=I|Is], + Regs, _FrameSize, VarMap, Acc) + when is_integer(FrameSize) -> + add_debug_info_is(Is, Regs, FrameSize, VarMap, [I|Acc]); +add_debug_info_is([#cg_set{anno=#{was_phi := true},op=copy}=I|Is], + Regs, FrameSize, VarMap, Acc) -> + %% This copy operation originates from a phi node. The source and + %% destination are not equivalent and must not be added to VarMap. + add_debug_info_is(Is, Regs, FrameSize, VarMap, [I|Acc]); +add_debug_info_is([#cg_set{anno=Anno,op=copy,dst=#b_var{name=Dst}, + args=[#b_var{name=Src}]}=I|Is], + Regs, FrameSize, VarMap0, Acc) -> + VarMap = case Anno of + #{delayed_yreg_copy := true} -> + VarMap0#{Src => Dst}; + #{} -> + VarMap0#{Dst => Src} + end, + add_debug_info_is(Is, Regs, FrameSize, VarMap, [I|Acc]); +add_debug_info_is([#cg_set{anno=Anno,op=executable_line,args=Args}=I0|Is], + Regs, FrameSize, VarMap, Acc) -> + #{def_regs := DefRegs, + alias := Alias, + literals := Literals0} = Anno, + Literals1 = [{get_original_name(#b_var{name=Var}, VarMap, Alias),Val} || + {Val,Var} <- Literals0], + Literals = [{hd(Vars),[{literal,Val}]} || + {Vars,Val} <- Literals1, Vars =/= []], + LiveRegs = [{map_get(V, Regs),get_original_name(V, VarMap, Alias)} || + V <- DefRegs, + not is_beam_register(V)], + S0 = sofs:family(LiveRegs, [{reg,[variable]}]), + S1 = sofs:family_to_relation(S0), + S2 = sofs:converse(S1), + S3 = sofs:relation_to_family(S2), + S = Literals ++ sofs:to_external(S3), + Info = {FrameSize,S}, + I = I0#cg_set{args=Args++[#b_literal{val=Info}]}, + add_debug_info_is(Is, Regs, FrameSize, VarMap, [I|Acc]); +add_debug_info_is([I|Is], Regs, FrameSize, VarMap, Acc) -> + add_debug_info_is(Is, Regs, FrameSize, VarMap, [I|Acc]); +add_debug_info_is([], _Regs, FrameSize, VarMap, Info) -> + {reverse(Info),VarMap,FrameSize}. + +get_original_name(#b_var{name=Name}, VarMap, Alias) -> + Vs = [Name|get_original_name_1(Name, VarMap, Alias)], + [V || V <- Vs, is_original_variable(V)]. + +get_original_name_1(Name, VarMap, Alias) -> + case VarMap of + #{Name := Var} -> + [Var|get_original_name_1(Var, VarMap, Alias)]; + #{} -> + [] + end ++ maps:get(Name, Alias, []). + +-spec is_original_variable(Name) -> boolean() when + Name :: non_neg_integer() | atom(). + +is_original_variable(Name) when is_atom(Name) -> + <> = atom_to_binary(Name), + if + C =:= $_ -> true; + $A =< C, C =< $Z -> true; + $À =< C, C =< $Þ, C =/= $× -> true; + true -> false + end; +is_original_variable(Name) when is_integer(Name) -> + false. + +%%% +%%% Annotate `executable_line` instructions with all variables that +%%% have been defined and are still available in a BEAM register. +%%% +%%% This pass is run when collection of BEAM debug information has +%%% been requested. +%%% + +anno_defined_regs(Linear, Def, Regs) -> + def_regs(Linear, #{0 => Def}, Regs). + +def_regs([{L,#cg_blk{is=Is0,last=Last}=Blk0}|Bs], DefMap0, Regs) -> + Def0 = map_get(L, DefMap0), + {Is,Def,MaybeDef} = def_regs_is(Is0, Regs, Def0, []), + DefMap = def_successors(Last, Def, MaybeDef, DefMap0), + Blk = Blk0#cg_blk{is=Is}, + [{L,Blk}|def_regs(Bs, DefMap, Regs)]; +def_regs([], _, _) -> []. + +def_regs_is([#cg_alloc{live=Live}=I|Is], Regs, Def0, Acc) when is_integer(Live) -> + Def = trim_xregs(Def0, Live, Regs), + def_regs_is(Is, Regs, Def, [I|Acc]); +def_regs_is([#cg_set{op=succeeded,args=[Var]}=I], _Regs, Def, Acc) -> + %% Var will only be defined on the success branch of the `br` + %% for this block. + MaybeDef = [Var], + {reverse(Acc, [I]),Def,MaybeDef}; +def_regs_is([#cg_set{op=kill_try_tag,args=[#b_var{}=Tag]}=I|Is], Regs, Def0, Acc) -> + Def = ordsets:del_element(Tag, Def0), + def_regs_is(Is, Regs, Def, [I|Acc]); +def_regs_is([#cg_set{op=catch_end,dst=Dst,args=[#b_var{}=Tag|_]}=I|Is], Regs, Def0, Acc) -> + Def1 = trim_xregs(Def0, 0, Regs), + Def2 = ordsets:del_element(Tag, Def1), + Def = ordsets:add_element(Dst, Def2), + def_regs_is(Is, Regs, Def, [I|Acc]); +def_regs_is([#cg_set{anno=Anno0,op=executable_line}=I0|Is], Regs, Def, Acc) -> + Anno = Anno0#{def_regs => Def}, + I = I0#cg_set{anno=Anno}, + def_regs_is(Is, Regs, Def, [I|Acc]); +def_regs_is([#cg_set{anno=Anno,dst=Dst,op={bif,Bif},args=Args}=I|Is], Regs, Def0, Acc) -> + Def1 = case is_gc_bif(Bif, Args) of + true -> + #{live := Live} = Anno, + trim_xregs(Def0, Live, Regs); + false -> + Def0 + end, + case Regs of + #{Dst := {Tag,_}=R} when Tag =:= x; Tag =:= y -> + Def2 = delete_xreg(Def1, R, Regs), + Def = ordsets:add_element(Dst, Def2), + def_regs_is(Is, Regs, Def, [I|Acc]); + #{} -> + def_regs_is(Is, Regs, Def1, [I|Acc]) + end; +def_regs_is([#cg_set{anno=Anno,dst=Dst}=I|Is], Regs, Def0, Acc) -> + Def1 = case Anno of + #{live := Live} -> trim_xregs(Def0, Live, Regs); + #{} -> Def0 + end, + Def2 = case Anno of + #{kill_yregs := KillYregs} -> + Def1 -- KillYregs; + #{} -> + Def1 + end, + case Anno of + #{clobbers := true} -> + Def3 = trim_xregs(Def2, 0, Regs), + Def = case Regs of + #{Dst := {Tag,_}=R} when Tag =:= x; Tag =:= y -> + Def4 = delete_xreg(Def3, R, Regs), + ordsets:add_element(Dst, Def4); + #{} -> + Def3 + end, + def_regs_is(Is, Regs, Def, [I|Acc]); + #{} -> + case Regs of + #{Dst := {Tag,_}=R} when Tag =:= x; Tag =:= y -> + Def3 = delete_xreg(Def2, R, Regs), + Def = ordsets:add_element(Dst, Def3), + def_regs_is(Is, Regs, Def, [I|Acc]); + #{} -> + def_regs_is(Is, Regs, Def1, [I|Acc]) + end + end; +def_regs_is([], _Regs, Def, Acc) -> + {reverse(Acc),Def,[]}. + +trim_xregs([V|Vs], Live, Regs) -> + case Regs of + #{V := {x,R}} when R < Live -> + [V|trim_xregs(Vs, Live, Regs)]; + #{V := {y,_}}-> + [V|trim_xregs(Vs, Live, Regs)]; + #{} -> + trim_xregs(Vs, Live, Regs) + end; +trim_xregs([], _, _) -> []. + +delete_xreg([V|Vs], R, Regs) -> + case Regs of + #{V := R} -> Vs; + #{} -> [V|delete_xreg(Vs, R, Regs)] + end; +delete_xreg([], _, _) -> []. + %%% %%% Here follows the main code generation functions. %%% @@ -1822,6 +2037,9 @@ cg_instr(bs_get_position, [Ctx], Dst, Set) -> cg_instr(executable_line, [{integer,Index}], _Dst, #cg_set{anno=Anno}) -> {line,Location} = line(Anno), [{executable_line,Location,Index}]; +cg_instr(executable_line, [{integer,Index},{literal,Info}], _Dst, #cg_set{anno=Anno}) -> + {line,Location} = line(Anno), + [{executable_line,Location,Index,Info}]; cg_instr(put_map, [{atom,assoc},SrcMap|Ss], Dst, Set) -> Live = get_live(Set), [{put_map_assoc,{f,0},SrcMap,Dst,Live,{list,Ss}}]; @@ -2132,7 +2350,8 @@ translate_phis(_, _, _) -> []. phi_copies([#b_set{dst=Dst,args=PhiArgs}|Sets], L) -> CopyArgs = [V || {V,Target} <- PhiArgs, Target =:= L], - [#cg_set{op=copy,dst=Dst,args=CopyArgs}|phi_copies(Sets, L)]; + Anno = #{was_phi => true}, + [#cg_set{anno=Anno,op=copy,dst=Dst,args=CopyArgs}|phi_copies(Sets, L)]; phi_copies([], _) -> []. %% opt_move_to_x0([Instruction]) -> [Instruction]. diff --git a/lib/compiler/src/beam_ssa_pre_codegen.erl b/lib/compiler/src/beam_ssa_pre_codegen.erl index 5adadc397f2c..f964b96164a2 100644 --- a/lib/compiler/src/beam_ssa_pre_codegen.erl +++ b/lib/compiler/src/beam_ssa_pre_codegen.erl @@ -114,6 +114,8 @@ functions([], _Ps) -> []. passes(Opts) -> AddPrecgAnnos = proplists:get_bool(dprecg, Opts), + BeamDebugInfo = proplists:get_bool(beam_debug_info, Opts), + Ps = [?PASS(assert_no_critical_edges), %% Preliminaries. @@ -121,6 +123,12 @@ passes(Opts) -> ?PASS(sanitize), ?PASS(expand_match_fail), ?PASS(expand_update_tuple), + + case BeamDebugInfo of + false -> ignore; + true -> ?PASS(break_out_executable_line) + end, + ?PASS(place_frames), ?PASS(fix_receives), @@ -820,6 +828,25 @@ sanitize_is([], Last, _InBlocks, _Blocks, Count, Values, Changed, Acc) -> no_change end. +do_sanitize_is(#b_set{anno=Anno0,op=executable_line,args=Args0}=I0, + Is, Last, InBlocks, Blocks, Count, Values, Changed0, Acc) -> + Args = sanitize_args(Args0, Values), + Anno1 = case Anno0 of + #{alias := Alias0} -> + Alias = sanitize_alias(Alias0, Values), + Anno0#{alias := Alias}; + #{} -> + Anno0 + end, + Anno = case [{Val,From} || #b_var{name=From} := #b_literal{val=Val} <- Values] of + [] -> + Anno1; + [_|_]=NewLiterals -> + Anno1#{literals => NewLiterals ++ map_get(literals, Anno1)} + end, + I = I0#b_set{anno=Anno,args=Args}, + Changed = Changed0 orelse Args =/= Args0, + sanitize_is(Is, Last, InBlocks, Blocks, Count, Values, Changed, [I|Acc]); do_sanitize_is(#b_set{op=Op,dst=Dst,args=Args0}=I0, Is, Last, InBlocks, Blocks, Count, Values, Changed0, Acc) -> Args = sanitize_args(Args0, Values), @@ -853,6 +880,19 @@ sanitize_last(#b_blk{last=Last0}=Blk, Values) -> Blk end. +sanitize_alias(Alias, Values) -> + sanitize_alias_1(maps:keys(Alias), Values, Alias). + +sanitize_alias_1([Old|Vs], Values, Alias0) -> + Alias = case Values of + #{#b_var{name=Old} := #b_var{name=New}} -> + Alias0#{New => map_get(Old, Alias0)}; + #{} -> + Alias0 + end, + sanitize_alias_1(Vs, Values, Alias); +sanitize_alias_1([], _Values, Alias) -> Alias. + sanitize_args(Args, Values) -> [sanitize_arg(Arg, Values) || Arg <- Args]. @@ -1190,6 +1230,91 @@ sort_update_tuple([#b_literal{}=Index, Value | Updates], Acc) -> sort_update_tuple([], Acc) -> append([[Index, Value] || {Index, Value} <- sort(fun erlang:'>='/2, Acc)]). + +%%% +%%% Avoiding placing stack frame allocation instructions before an +%%% `executable_line` instruction to potentially provide information +%%% for more variables. +%%% +%%% This sub pass is only run when the beam_debug_info option has been given. +%%% +%%% As an example, consider this function: +%%% +%%% foo(A, B, C) -> +%%% {ok,bar(A),B}. +%%% +%%% When compiled with the beam_debug_info option the first part of the SSA code +%%% will look this: +%%% +%%% 0: +%%% _7 = executable_line `1` +%%% _3 = call (`bar`/1), _0 +%%% +%%% The beam_ssa_pre_codegen pass will place a stack frame before the block: +%%% +%%% %% #{frame_size => 1,yregs => #{{b_var,1} => []}} +%%% 0: +%%% [1] y0/_12 = copy x1/_1 +%%% [3] z0/_7 = executable_line `1` +%%% +%%% In the resulting BEAM code there will not be any information for +%%% variable `C`, because the allocate instruction will kill it before +%%% reaching the executable_line instruction: +%%% +%%% {allocate,1,2}. +%%% {move,{x,1},{y,0}}. +%%% {executable_line,[{location,...}], +%%% 1, +%%% {1,[{'A',[{x,0}]},{'B',[{x,1},{y,0}]}]}}. +%%% +%%% If we split the block after the executable_line instruction, the +%%% allocation of the stack frame will be placed after the executable_line +%%% instruction: +%%% +%%% 0: +%%% [1] z0/_7 = executable_line `1` +%%% [3] br ^12 +%%% +%%% %% #{frame_size => 1,yregs => #{{b_var,1} => []}} +%%% 12: +%%% [5] y0/_13 = copy x1/_1 +%%% [7] x0/_3 = call (`bar`/1), x0/_0 +%%% +%%% In the resulting BEAM code, there will now be information for variable `C`: +%%% +%%% {executable_line,[{location,"t.erl",5}], +%%% 1, +%%% {none,[{'A',[{x,0}]},{'B',[{x,1}]},{'C',[{x,2}]}]}}. +%%% {allocate,1,2}. +%%% + +break_out_executable_line(#st{ssa=Blocks0,cnt=Count0}=St) -> + RPO = beam_ssa:rpo(Blocks0), + + %% Calculate the set of all indices for executable_line + %% instructions that occur as the first instruction in a + %% block. Splitting after every executable_line instruction is not + %% always beneficial, and can even result in worse information + %% about variables. + F = fun(_, #b_blk{is=[#b_set{op=executable_line, + args=[#b_literal{val=Index}]}|_]}, Acc) -> + sets:add_element(Index, Acc); + (_, _, Acc) -> + Acc + end, + ToBeSplit = beam_ssa:fold_blocks(F, RPO, sets:new(), Blocks0), + + %% Now split blocks after the found executable_line instructions that + %% are known to start blocks. + P = fun(#b_set{op=executable_line,args=[#b_literal{val=Index}]}) -> + sets:is_element(Index, ToBeSplit); + (_) -> + false + end, + {Blocks,Count} = beam_ssa:split_blocks_after(RPO, P, Blocks0, Count0), + + St#st{ssa=Blocks,cnt=Count}. + %%% %%% Find out where frames should be placed. %%% @@ -1985,7 +2110,8 @@ copy_retval_is([#b_set{op=call,dst=#b_var{}=Dst}=I0|Is], RC, Yregs, case sets:is_element(Dst, Yregs) of true -> {NewVar,Count} = new_var(Count1), - Copy = #b_set{op=copy,dst=Dst,args=[NewVar]}, + Copy = #b_set{anno=#{delayed_yreg_copy => true}, + op=copy,dst=Dst,args=[NewVar]}, I = I1#b_set{dst=NewVar}, copy_retval_is(Is, RC, Yregs, Copy, Count, [I|Acc]); false -> diff --git a/lib/compiler/src/beam_trim.erl b/lib/compiler/src/beam_trim.erl index 96f79f8f0ae1..4c1532fcc556 100644 --- a/lib/compiler/src/beam_trim.erl +++ b/lib/compiler/src/beam_trim.erl @@ -304,6 +304,12 @@ remap([return|_]=Is, _) -> remap([{line,_}=I|Is], Remap) -> [I|remap(Is, Remap)]. +remap_block([{set,[],Ss0,{executable_line,_,_,_}=Info0}|Is], Remap) -> + Ss = remap_args(Ss0, Remap), + {executable_line,Loc,Index,DebugInfo0} = Info0, + DebugInfo = remap_debug_info(DebugInfo0, Remap), + Info = {executable_line,Loc,Index,DebugInfo}, + [{set,[],Ss,Info}|remap_block(Is, Remap)]; remap_block([{set,[{x,_}]=Ds,Ss0,Info}|Is], Remap) -> Ss = remap_args(Ss0, Remap), [{set,Ds,Ss,Info}|remap_block(Is, Remap)]; @@ -313,6 +319,12 @@ remap_block([{set,Ds0,Ss0,Info}|Is], Remap) -> [{set,Ds,Ss,Info}|remap_block(Is, Remap)]; remap_block([], _) -> []. +remap_debug_info({FrameSize0,Vars0}, {Trim,Map}) -> + FrameSize = FrameSize0 - Trim, + Vars = [{Name,[remap_arg(Arg, Trim, Map) || Arg <- Args]} || + {Name,Args} <- Vars0], + {FrameSize,Vars}. + remap_args(Args, {Trim,Map}) -> [remap_arg(Arg, Trim, Map) || Arg <- Args]. diff --git a/lib/compiler/src/beam_validator.erl b/lib/compiler/src/beam_validator.erl index 791f0bbb5bc0..be17d4011675 100644 --- a/lib/compiler/src/beam_validator.erl +++ b/lib/compiler/src/beam_validator.erl @@ -373,8 +373,10 @@ vi({'%',_}, Vst) -> Vst; vi({line,_}, Vst) -> Vst; -vi({executable_line,_,_}, Vst) -> +vi({executable_line,_,Index}, Vst) when is_integer(Index) -> Vst; +vi({executable_line,_,Index,Info}, Vst) when is_integer(Index) -> + validate_executable_line(Info, Vst); vi(nif_start, Vst) -> Vst; %% @@ -2155,6 +2157,34 @@ validate_select_tuple_arity(Fail, [], _, #vst{}=Vst) -> kill_state(SuccVst) end). +%% +%% Validate executable_line instructions in the presence of BEAM debug info. +%% + +validate_executable_line({Stk,Vars}, #vst{current=St}=Vst) -> + case St of + #st{numy=Stk} -> + ok; + #st{numy=ActualStk} -> + error({beam_debug_info,frame_size,Stk,actual,ActualStk}) + end, + _ = [validate_el_vars(Regs, Name, Vst) || {Name,Regs} <- Vars], + Vst. + +validate_el_vars([R|Rs], Name, Vst) -> + Type = get_term_type(R, Vst), + validate_el_vars(Rs, Type, Name, Vst). + +validate_el_vars([R|Rs], Type, Name, Vst) -> + case get_term_type(R, Vst) of + Type -> + validate_el_vars(Rs, Type, Name, Vst); + OtherType -> + error({type_mismatch,Name,OtherType,Type}) + end; +validate_el_vars([], _Type, _Name, _Vst) -> + ok. + %% %% Infers types from comparisons, looking at the expressions that produced the %% compared values and updates their types if we've learned something new from diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl index b33b41bf7d08..3ee3c1be63dc 100644 --- a/lib/compiler/src/compile.erl +++ b/lib/compiler/src/compile.erl @@ -1041,6 +1041,9 @@ expand_opt(r25, Os) -> expand_opt(r26, Os)]; expand_opt(r26, Os) -> [no_bsm_opt | Os]; +expand_opt(beam_debug_info, Os) -> + [beam_debug_info, line_coverage, no_copt, no_bsm_opt, no_bool_opt, + no_share_opt, no_recv_opt, no_ssa_opt, no_throw_opt | Os]; expand_opt({debug_info_key,_}=O, Os) -> [encrypt_debug_info,O|Os]; expand_opt(no_type_opt=O, Os) -> diff --git a/lib/compiler/src/sys_coverage.erl b/lib/compiler/src/sys_coverage.erl index 2c0539a37c34..381c62bedeaf 100644 --- a/lib/compiler/src/sys_coverage.erl +++ b/lib/compiler/src/sys_coverage.erl @@ -193,7 +193,7 @@ bool_switch(E, T, F, AllVars, AuxVarN) -> [{tuple,Anno,[{atom,Anno,badarg},AuxVar]}]}]}]}. aux_var(Vars, N) -> - Name = list_to_atom(lists:concat(['_', N])), + Name = list_to_atom(lists:concat(['cov', N])), case sets:is_element(Name, Vars) of true -> aux_var(Vars, N + 1); false -> Name diff --git a/lib/compiler/src/v3_core.erl b/lib/compiler/src/v3_core.erl index 5a6ec26b1b8d..7f0da54f8f64 100644 --- a/lib/compiler/src/v3_core.erl +++ b/lib/compiler/src/v3_core.erl @@ -1196,7 +1196,11 @@ try_after(Line, Es0, As0, St0) -> {V, St3} = new_var(St2), % (must not exist in As1) LineAnno = lineno_anno(Line, St3), - case is_iexprs_small(As, 20) of + %% If BEAM debug info has been requested, we must not duplicate + %% `executable_line` instructions. + BeamDebugInfo = member(beam_debug_info, St0#core.opts), + + case not BeamDebugInfo andalso is_iexprs_small(As, 20) of true -> try_after_small(LineAnno, Es, As, V, St3); false -> try_after_large(LineAnno, Es, As, V, St3) end. @@ -2757,7 +2761,13 @@ uexprs([#imatch{anno=A,pat=P0,arg=Arg,fc=Fc}|Les], Ks, St0) -> case upat_is_new_var(P0, Ks) of true -> %% Assignment to a new variable. - uexprs([#iset{var=P0,arg=Arg}|Les], Ks, St0); + case P0 of + #c_var{name='_'} -> + {Var,St1} = new_var(St0), + uexprs([#iset{var=Var,arg=Arg}|Les], Ks, St1); + _ -> + uexprs([#iset{var=P0,arg=Arg}|Les], Ks, St0) + end; false when Les =:= [] -> %% Need to explicitly return match "value", make %% safe for efficiency. diff --git a/lib/compiler/test/Makefile b/lib/compiler/test/Makefile index c6e5a23a3215..773f48bd718c 100644 --- a/lib/compiler/test/Makefile +++ b/lib/compiler/test/Makefile @@ -11,6 +11,7 @@ MODULES= \ beam_block_SUITE \ beam_bounds_SUITE \ beam_validator_SUITE \ + beam_debug_info_SUITE \ beam_disasm_SUITE \ beam_doc_SUITE \ beam_except_SUITE \ @@ -159,6 +160,8 @@ ERL_FILES= $(MODULES:%=%.erl) CORE_FILES= $(CORE_MODULES:%=%.core) ERL_DUMMY_FILES= $(CORE_MODULES:%=%.erl) +BEAM_OPCODES_HRL=$(ERL_TOP)/lib/compiler/src/beam_opcodes.hrl + ##TARGET_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) ##INSTALL_PROGS= $(TARGET_FILES) @@ -291,6 +294,7 @@ release_tests_spec: make_emakefile done $(INSTALL_DATA) $(ERL_DUMMY_FILES) "$(RELSYSDIR)" rm $(ERL_DUMMY_FILES) + $(INSTALL_DATA) $(BEAM_OPCODES_HRL) "$(RELSYSDIR)" chmod -R u+w "$(RELSYSDIR)" @tar cf - *_SUITE_data property_test | (cd "$(RELSYSDIR)"; tar xf -) diff --git a/lib/compiler/test/beam_debug_info_SUITE.erl b/lib/compiler/test/beam_debug_info_SUITE.erl new file mode 100644 index 000000000000..565d3a13312c --- /dev/null +++ b/lib/compiler/test/beam_debug_info_SUITE.erl @@ -0,0 +1,524 @@ +%% +%% %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% +%% +-module(beam_debug_info_SUITE). +-include("beam_opcodes.hrl"). + +-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, + init_per_group/2,end_per_group/2, + smoke/1]). + +suite() -> [{ct_hooks,[ts_install_cth]}]. + +all() -> + [smoke, + {group,p}]. + +groups() -> + [{p,test_lib:parallel(), + []}]. + +init_per_suite(Config) -> + id(Config), + test_lib:recompile(?MODULE), + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + Config. + +smoke(_Config) -> + TestBeams0 = get_unique_beam_files(), + TestBeams = compiler_beams() ++ TestBeams0, + + S = ~""" + Below, for each module, there is a list of functions with + variables missing in the BEAM debug info. Note that there + will probably never be possible to have all variables + present in the debug info, because some variables dies before + an `executable_line` instruction is reached. + + """, + io:put_chars(S), + + test_lib:p_run(fun do_smoke/1, TestBeams). + +compiler_beams() -> + filelib:wildcard(filename:join([code:lib_dir(compiler), "ebin", "*.beam"])). + +do_smoke(Beam) -> + try + {ok,{Mod,[{abstract_code,{raw_abstract_v1,Abstr0}}]}} = + beam_lib:chunks(Beam, [abstract_code]), + + %% beam_validator will check for each executable_line instruction + %% that the frame size is correct and that all referenced BEAM + %% registers are valid. + {ok,Mod,Code} = compile:forms(Abstr0, + [beam_debug_info,binary,report_errors]), + {ok,_,Abstr} = compile:forms(Abstr0, + [beam_debug_info,dexp,binary,report_errors]), + SrcVars = source_variables(Abstr), + IndexToFunctionMap = abstr_executable_lines(Abstr), + DebugInfo = get_debug_info(Mod, Code), + {DbgVars,DbgLiterals} = debug_info_vars(DebugInfo, IndexToFunctionMap), + + %% The debug information must only contain variables that are + %% present in the source code. If the sanity check below + %% fails, it could be for one of the following reasons: + %% + %% * A compiler pass has introduced a new temporary variable + %% whose name is a legal Erlang variable name. (Such + %% temporary variables are supposed to have invalid names, + %% such as `rec1`.) + %% + %% * Something is wrong in the mapping from + %% `executable_line` instruction to function names, + %% causing a variable to be collected into the wrong + %% function. (See abstr_executable_lines/1.) + %% + %% * A heuristic in source_variables/1 is wrong, causing variables + %% that actually are present in the debug information to be + %% removed from the list of variables from the source code. + + [] = family_difference(DbgVars, SrcVars), + + %% Now figure out which variables are missing from the debug info + %% and print them. + AllDbg = family_union(DbgVars, DbgLiterals), + Diff0 = family_difference(SrcVars, AllDbg), + Diff = [begin + {Vars,B} = format_vars(Vars0), + S = io_lib:format("~p/~p: ~ts", [F,A,Vars]), + [" ",case B of + true -> " " ++ S; + false -> "** " ++ S + end,"\n"] + end || {{F,A},Vars0} <- Diff0], + io:format("~p:\n~ts", [Mod,Diff]) + catch + throw:{error,Error} -> + io:format("*** compilation failure '~p' for file ~s\n", + [Error,Beam]), + error; + Class:Error:Stk -> + io:format("~p: ~p ~p\n~p\n", [Beam,Class,Error,Stk]), + error + end. + +format_vars(Vs) -> + Str = lists:join(", ", [io_lib:format("~ts", [V]) || V <- Vs]), + B = lists:all(fun(V) -> + case atom_to_binary(V) of + <<"_",_/binary>> -> true; + _ -> false + end + end, Vs), + {Str,B}. + +debug_info_vars(DebugInfo, IndexToFunctionMap) -> + {Vars0,Literals0} = debug_info_vars_1(DebugInfo, IndexToFunctionMap, [], []), + Vars = family_union(Vars0), + Literals = family_union(Literals0), + {Vars,Literals}. + +debug_info_vars_1([{I,{_FrameSize,List}}|T], IndexToFunctionMap, VarAcc, LitAcc) -> + case debug_info_vars_2(List, [], []) of + {[],[]} -> + debug_info_vars_1(T, IndexToFunctionMap, VarAcc, LitAcc); + {Vars,Literals} -> + F = map_get(I, IndexToFunctionMap), + debug_info_vars_1(T, IndexToFunctionMap, + [{F,Vars}|VarAcc], [{F,Literals}|LitAcc]) + end; +debug_info_vars_1([], _, VarAcc, LitAcc) -> + {VarAcc,LitAcc}. + +debug_info_vars_2([Name0,Value|T], VarAcc, LitAcc) when is_binary(Name0) -> + Name = binary_to_atom(Name0), + case Value of + {x,_} -> debug_info_vars_2(T, [Name|VarAcc], LitAcc); + {y,_} -> debug_info_vars_2(T, [Name|VarAcc], LitAcc); + _ -> debug_info_vars_2(T, VarAcc, [Name|LitAcc]) + end; +debug_info_vars_2([nil,[]|T], VarAcc, LitAcc) -> + debug_info_vars_2(T, VarAcc, LitAcc); +debug_info_vars_2([], VarAcc, LitAcc) -> + {VarAcc,LitAcc}. + +family_union(S0) -> + S1 = sofs:relation(S0, [{function,[variable]}]), + S2 = sofs:relation_to_family(S1), + S3 = sofs:family_union(S2), + sofs:to_external(S3). + +family_union(F0, F1) -> + S0 = sofs:relation(F0, [{function,[variable]}]), + S1 = sofs:relation(F1, [{function,[variable]}]), + S2 = sofs:family_union(S0, S1), + sofs:to_external(S2). + +family_difference(F0, F1) -> + S0 = sofs:family(F0, [{function,[variable]}]), + S1 = sofs:family(F1, [{function,[variable]}]), + S2 = sofs:family_difference(S0, S1), + SpecFun = fun(S) -> sofs:no_elements(S) =/= 0 end, + S3 = sofs:family_specification(SpecFun, S2), + sofs:to_external(S3). + +%% +%% Extract variables mentioned in the source code. Try to remove +%% variables that will never show up in the debug information; for +%% examples, definitions of variables that are not followed by any +%% executable_line instructions can be ignored. +%% +source_variables(Abstr) -> + [{{Name,Arity},extract_src_vars(F)} || + {function,_,Name,Arity,_}=F <- Abstr]. + +extract_src_vars(F) -> + L1 = extract_src_vars(F, true, #{}), + L2 = [V || V := true <- L1], + lists:sort(L2). + +extract_src_vars({var,_,'_'}, _Lc, Acc) -> + Acc; +extract_src_vars({var,_,Name}, _Lc, Acc0) -> + case atom_to_binary(Name) of + <<"cov",_/binary>> -> + %% Ignore variable added by the sys_coverage pass. + Acc0; + <<"rec",_/binary>> -> + %% Ignore variable added by the erl_expand_pass. + Acc0; + _ -> + true = beam_ssa_codegen:is_original_variable(Name), + Acc0#{Name => true} + end; +extract_src_vars({atom,_,_}, _Lc, Acc) -> Acc; +extract_src_vars({bin,_,Es}, _Lc, Acc) -> + extract_args(Es, Acc); +extract_src_vars({bin_element,_,Val,Size,_}, _Lc, Acc0) -> + Acc1 = extract_src_vars(Val, false, Acc0), + case Size of + default -> Acc1; + _ -> extract_src_vars(Size, false, Acc1) + end; +extract_src_vars({char,_,_}, _Lc, Acc) -> Acc; +extract_src_vars({float,_,_}, _Lc, Acc) -> Acc; +extract_src_vars({integer,_,_}, _Lc, Acc) -> Acc; +extract_src_vars({nil,_}, _Lc, Acc) -> Acc; +extract_src_vars({string,_,_}, _Lc, Acc) -> Acc; +extract_src_vars({cons,_,Hd,Tl}, Lc, Acc0) -> + Acc1 = extract_src_vars(Hd, Lc, Acc0), + extract_src_vars(Tl, Lc, Acc1); +extract_src_vars({map,_,Fs}, _Lc, Acc0) -> + extract_args(Fs, Acc0); +extract_src_vars({map,_,M,Fs}, Lc, Acc0) -> + Acc1 = extract_src_vars(M, Lc, Acc0), + extract_args(Fs, Acc1); +extract_src_vars({map_field_assoc,_,K,V}, _Lc, Acc0) -> + Acc1 = extract_src_vars(K, false, Acc0), + extract_src_vars(V, false, Acc1); +extract_src_vars({map_field_exact,_,K,V}, _Lc, Acc0) -> + Acc1 = extract_src_vars(K, false, Acc0), + extract_src_vars(V, false, Acc1); +extract_src_vars({tuple,_,Es}, _Lc, Acc) -> + extract_args(Es, Acc); +extract_src_vars({call,_,F,As}, Lc, Acc0) -> + Acc1 = extract_src_vars(F, Lc, Acc0), + extract_args(As, Acc1); +extract_src_vars({remote,_,Mod,Name}, Lc, Acc0) -> + Acc1 = extract_src_vars(Mod, Lc, Acc0), + extract_src_vars(Name, Lc, Acc1); +extract_src_vars({match,_,P,E}, Lc, Acc0) -> + Acc1 = extract_src_vars(P, false, Acc0), + extract_src_vars(E, Lc, Acc1); +extract_src_vars({op,_,_Name,Arg}, Lc, Acc0) -> + extract_src_vars(Arg, Lc, Acc0); +extract_src_vars({op,_,_Name,Lhs,Rhs}, Lc, Acc0) -> + Acc1 = extract_src_vars(Lhs, false, Acc0), + extract_src_vars(Rhs, Lc, Acc1); +extract_src_vars({executable_line,_,_}, _Lc, Acc) -> + Acc; +extract_src_vars({named_fun,_,Name,Cs}, Lc, Acc0) -> + case any_executable_line_instrs(Cs) of + false -> + %% Since there are no executable lines within this fun, + %% none of the variables defined in the fun should ever + %% show up in the debug info. + Acc0; + true when Name =/= '_' -> + Acc = case Name of + '_' -> Acc0; + _ -> extract_src_vars({var,anno,Name}, Lc, Acc0) + end, + extract_cs(Cs, true, Acc) + end; +extract_src_vars({function,_Anno,_,_,Cs}, _Lc, Acc0) -> + case any_executable_line_instrs(Cs) of + true -> + extract_cs(Cs, true, Acc0); + false -> + %% There are no executable_line instructions in this + %% function. This happens if code has been placed in a + %% header filer, or if the `-file()` attribute has been + %% used to change the name of the source file. + Acc0 + end; +extract_src_vars({'fun',_Anno,{clauses,Cs}}, _Lc, Acc0) -> + case any_executable_line_instrs(Cs) of + true -> + extract_cs(Cs, true, Acc0); + false -> + Acc0 + end; +extract_src_vars({'fun',_Anno,_}, _Lc, Acc0) -> Acc0; +extract_src_vars({block,_Anno,Es}, Lc, Acc0) -> + extract_body(Es, Lc, Acc0); +extract_src_vars({'receive',_Anno,Cs}, Lc, Acc0) -> + extract_cs(Cs, Lc, Acc0); +extract_src_vars({'receive',_Anno,Cs,_To,ToE}, Lc, Acc0) -> + Acc1 = extract_cs(Cs, Lc, Acc0), + extract_body(ToE, Lc, Acc1); +extract_src_vars({'maybe',_Anno,Body}, Lc, Acc0) -> + extract_body(Body, Lc, Acc0); +extract_src_vars({'maybe',_Anno,Body,{'else',_,ElseClauses}}, Lc, Acc0) -> + Acc1 = extract_body(Body, Lc, Acc0), + extract_cs(ElseClauses, Lc, Acc1); +extract_src_vars({'maybe_match',_Anno,P,E}, Lc, Acc0) -> + Acc1 = extract_src_vars(P, false, Acc0), + extract_src_vars(E, Lc, Acc1); +extract_src_vars({'case',_Anno,E,Cs}, Lc, Acc0) -> + Acc1 = extract_src_vars(E, false, Acc0), + extract_cs(Cs, Lc, Acc1); +extract_src_vars({'if',_Anno,Cs}, Lc, Acc0) -> + extract_cs(Cs, Lc, Acc0); +extract_src_vars({'try',_Anno,Es,Scs,Ccs,As}, Lc, Acc0) -> + Acc1 = extract_body(Es, false, Acc0), + Acc2 = extract_cs(Scs, Lc, Acc1), + Acc3 = extract_cs(Ccs, Lc, Acc2), + extract_body(As, Lc, Acc3); +extract_src_vars({'catch',_Anno,E}, Lc, Acc0) -> + extract_src_vars(E, Lc, Acc0); +extract_src_vars({C,_,Build,Qs0}, Lc, Acc0) + when C =:= lc; C =:= bc; C =:= mc -> + case any_executable_line_instrs(Build) of + false -> + Qs = extract_sv_qs(Qs0), + case any_executable_line_instrs(Qs) of + false -> + Acc0; + true -> + extract_args(Qs, Acc0) + end; + true -> + Acc1 = extract_src_vars(Build, Lc, Acc0), + extract_args(Qs0, Acc1) + end; +extract_src_vars({G,_,P,E}, _Lc, Acc0) when G =:= generate; + G =:= b_generate; + G =:= m_generate -> + Acc1 = extract_src_vars(P, false, Acc0), + extract_src_vars(E, false, Acc1). + +extract_cs([{clause,_,Pats,Gs,Body}|Cs], Lc, Acc0) -> + case Lc andalso not any_executable_line_instrs(Body) of + true -> + extract_cs(Cs, Lc, Acc0); + false -> + Acc1 = extract_args(Pats, Acc0), + Acc2 = extract_guards(Gs, Acc1), + Acc3 = extract_body(Body, Lc, Acc2), + extract_cs(Cs, Lc, Acc3) + end; +extract_cs([], _, Acc) -> + Acc. + +extract_body([I], Lc, Acc) -> + case Lc andalso not any_executable_line_instrs(I) of + true -> + Acc; + false -> + extract_src_vars(I, Lc, Acc) + end; +extract_body([I|Is], Lc, Acc0) -> + Acc = extract_src_vars(I, false, Acc0), + extract_body(Is, Lc, Acc); +extract_body([], _Lc, Acc) -> Acc. + +extract_args([A|As], Acc) -> + extract_args(As, extract_src_vars(A, false, Acc)); +extract_args([], Acc) -> Acc. + +extract_guards([A|As], Acc) -> + extract_guards(As, extract_args(A, Acc)); +extract_guards([], Acc) -> Acc. + +extract_sv_qs([{block,BlkL,[{executable_line,_,_}|Bs]}|Qs1]) -> + [{block,BlkL,Bs}|extract_sv_qs_1(Qs1)]; +extract_sv_qs(Qs) -> Qs. + +extract_sv_qs_1([Q|Qs]) -> + case abstr_extract_executable_lines(Qs, []) of + [] -> + [Q]; + [_|_] -> + [Q|extract_sv_qs_1(Qs)] + end; +extract_sv_qs_1([]) -> []. + +any_executable_line_instrs(Abstr) -> + abstr_extract_executable_lines(Abstr, []) =/= []. + +%% +%% Return a mapping from executable_line instruction index to function. +%% +abstr_executable_lines(Abstr) -> + S0 = [{{Name,Arity},abstr_extract_executable_lines(Body)} || + {function,_,Name,Arity,Body} <- Abstr], + S1 = sofs:family(S0, [{function,[line]}]), + S2 = sofs:family_to_relation(S1), + S3 = sofs:converse(S2), + S4 = sofs:to_external(S3), + maps:from_list(S4). + +abstr_extract_executable_lines(Abstr) -> + abstr_extract_executable_lines(Abstr, []). + +abstr_extract_executable_lines({executable_line,_,Index}, Acc) -> + [Index|Acc]; +abstr_extract_executable_lines([H|T], Acc0) -> + Acc1 = abstr_extract_executable_lines(H, Acc0), + abstr_extract_executable_lines(T, Acc1); +abstr_extract_executable_lines(Tuple, Acc0) when is_tuple(Tuple) -> + abstr_extract_executable_lines(tuple_to_list(Tuple), Acc0); +abstr_extract_executable_lines(_, Acc) -> Acc. + +%%% +%%% Read and disassemble the BEAM debug information from the "DbgB" +%%% chunk of a BEAM file. +%%% +get_debug_info(Mod, Beam) -> + {ok,{Mod,[{"DbgB",DebugInfo0}]}} = beam_lib:chunks(Beam, ["DbgB"]), + Literals = case beam_lib:chunks(Beam, ["LitT"]) of + {ok,{Mod,[{"LitT",Literals0}]}} -> + decode_literal_table(Literals0); + {error,_,_} -> + [] + end, + CallOp = beam_opcodes:opcode(call, 2), + <> = DebugInfo0, + 0 = Version, + DebugInfo = decode_debug_info(DebugInfo1, Literals, CallOp), + lists:zip(lists:seq(1, length(DebugInfo)), DebugInfo). + +decode_literal_table(<<_:32,Compressed/binary>>) -> + <<_:32,Tab/binary>> = zlib:uncompress(Compressed), + maps:from_list(decode_literals(Tab, 0)). + +decode_literals(<>, Index) -> + [{Index,binary_to_term(Ext)}|decode_literals(T, Index+1)]; +decode_literals(<<>>, _) -> []. + +decode_debug_info(Code0, Literals, CallOp) -> + case Code0 of + <> -> + {FrameSize,Code2} = decode_arg(Code1, Literals), + {{list,List0},Code3} = decode_arg(Code2, Literals), + List = decode_list(List0), + [{FrameSize,List}|decode_debug_info(Code3, Literals, CallOp)]; + <<>> -> + [] + end. + +decode_list([{literal,Var},Where|T]) -> + [Var,Where|decode_list(T)]; +decode_list([]) -> []. + +decode_args(0, Code, _Literals) -> + {[],Code}; +decode_args(N, Code0, Literals) when is_integer(N), N > 0 -> + {Arg,Code1} = decode_arg(Code0, Literals), + {Args,Code2} = decode_args(N - 1, Code1, Literals), + {[Arg|Args],Code2}. + +decode_arg(Code0, Literals) -> + case decode_raw_arg(Code0) of + {nil,_}=Res -> Res; + {{u,N},Code1} -> + {N,Code1}; + {{atom,_},_}=Res -> Res; + {{integer,_},_}=Res -> Res; + {{x,_},_}=Res -> Res; + {{y,_},_}=Res -> Res; + {{z,1},Code1} -> + {{u,N},Code2} = decode_raw_arg(Code1), + {List,Code3} = decode_args(N, Code2, Literals), + {{list,List},Code3}; + {{z,4},Code1} -> + {{u,N},Code2} = decode_raw_arg(Code1), + {{literal,map_get(N, Literals)},Code2} + end. + +decode_raw_arg(<<0:4,0:1,?tag_a:3,Code/binary>>) -> + {nil,Code}; +decode_raw_arg(<>) -> + {{decode_tag(Tag),N},Code}; +decode_raw_arg(<<2#111:3,1:1,1:1,Tag:3,Code0/binary>>) -> + {{u,W0},Code1} = decode_raw_arg(Code0), + W = W0 + 9, + <> = Code1, + {{decode_tag(Tag),N},Code2}; +decode_raw_arg(<>) -> + W = W0 + 2, + <> = Code0, + {{decode_tag(Tag),N},Code1}; +decode_raw_arg(<>) -> + N = (High bsl 8) bor Low, + {{decode_tag(Tag),N},Code0}. + +decode_tag(?tag_u) -> u; +decode_tag(?tag_i) -> integer; +decode_tag(?tag_a) -> atom; +decode_tag(?tag_x) -> x; +decode_tag(?tag_y) -> y; +decode_tag(?tag_z) -> z. + +%%% +%%% Common utility functions. +%%% + +get_unique_beam_files() -> + F = fun IsCloned(ModString) -> + case ModString of + "_dialyzer_SUITE" -> true; + "_r25_SUITE" -> true; + [_|T] -> IsCloned(T); + _ -> false + end + end, + test_lib:get_unique_files(".beam", F). + +id(I) -> I. diff --git a/lib/compiler/test/compile_SUITE.erl b/lib/compiler/test/compile_SUITE.erl index 2d7ede128f99..031d7375f945 100644 --- a/lib/compiler/test/compile_SUITE.erl +++ b/lib/compiler/test/compile_SUITE.erl @@ -1463,7 +1463,7 @@ beam_ssa_pp_1(Mod, Abstr, Outdir) -> %% Test that warnings contain filenames and line numbers. warnings(_Config) -> - Files = get_unique_files(".erl"), + Files = test_lib:get_unique_files(".erl"), test_lib:p_run(fun do_warnings/1, Files). do_warnings(F) -> @@ -1736,7 +1736,9 @@ bc_options(Config) -> {182, small, [r26]}, {182, small, []}, - {183, small, [line_coverage]} + {183, small, [line_coverage]}, + {183, small, [beam_debug_info]}, + {183, big, [beam_debug_info]} ], Test = fun({Expected,Mod,Options}) -> @@ -2322,22 +2324,7 @@ compile_and_verify(Name, Target, Opts) -> Opts = BeamOpts. get_unique_beam_files() -> - get_unique_files(".beam"). - -get_unique_files(Ext) -> - Wc = filename:join(filename:dirname(code:which(?MODULE)), "*"++Ext), - [F || F <- filelib:wildcard(Wc), - not is_cloned(F, Ext), not is_lfe_module(F, Ext)]. - -is_cloned(File, Ext) -> - Mod = list_to_atom(filename:basename(File, Ext)), - test_lib:is_cloned_mod(Mod). - -is_lfe_module(File, Ext) -> - case filename:basename(File, Ext) of - "lfe_" ++ _ -> true; - _ -> false - end. + test_lib:get_unique_files(".beam"). %% Compiles a test module and returns the list of errors and warnings. diff --git a/lib/compiler/test/compile_SUITE_data/small.erl b/lib/compiler/test/compile_SUITE_data/small.erl index 37cd270e5047..1efb66cb2ebb 100644 --- a/lib/compiler/test/compile_SUITE_data/small.erl +++ b/lib/compiler/test/compile_SUITE_data/small.erl @@ -1,6 +1,6 @@ -module(small). --export([go/0,go/2]). +-export([go/0,go/2,latin1_var/1]). -small_attribute({value,3}). @@ -43,6 +43,8 @@ recv() -> tmo = F(), ok. +latin1_var(Överskott) -> + Överskott + 1. id(I) -> I. diff --git a/lib/compiler/test/test_lib.erl b/lib/compiler/test/test_lib.erl index 7ad2272450d2..6ae573078c15 100644 --- a/lib/compiler/test/test_lib.erl +++ b/lib/compiler/test/test_lib.erl @@ -23,8 +23,10 @@ -compile({no_auto_import,[binary_part/2]}). -export([id/1,recompile/1,recompile_core/1,parallel/0, uniq/0,opt_opts/1,get_data_dir/1, - is_cloned_mod/1,smoke_disasm/1,p_run/2,p_run/3, - highest_opcode/1]). + smoke_disasm/1, + p_run/2,p_run/3, + highest_opcode/1, + get_unique_files/1,get_unique_files/2]). %% Used by test case that override BIFs. -export([binary_part/2,binary/1]). @@ -131,24 +133,21 @@ get_data_dir(Config) -> re:replace(Acc, Suffix, "_SUITE", Opts) end, Data, Suffixes). -is_cloned_mod(Mod) -> - is_cloned_mod_1(atom_to_list(Mod)). - -%% Test whether Mod is a cloned module. We don't consider modules +%% Test whether the module is cloned. We don't consider modules %% compiled with compatibility for an older release cloned (that %% will improve coverage). -is_cloned_mod_1("_no_opt_SUITE") -> true; -is_cloned_mod_1("_no_copt_SUITE") -> true; -is_cloned_mod_1("_no_copt_ssa_SUITE") -> true; -is_cloned_mod_1("_no_ssa_opt_SUITE") -> true; -is_cloned_mod_1("_no_type_opt_SUITE") -> true; -is_cloned_mod_1("_post_opt_SUITE") -> true; -is_cloned_mod_1("_inline_SUITE") -> true; -is_cloned_mod_1("_no_module_opt_SUITE") -> true; -is_cloned_mod_1("_cover_SUITE") -> true; -is_cloned_mod_1([_|T]) -> is_cloned_mod_1(T); -is_cloned_mod_1([]) -> false. +is_cloned("_no_opt_SUITE") -> true; +is_cloned("_no_copt_SUITE") -> true; +is_cloned("_no_copt_ssa_SUITE") -> true; +is_cloned("_no_ssa_opt_SUITE") -> true; +is_cloned("_no_type_opt_SUITE") -> true; +is_cloned("_post_opt_SUITE") -> true; +is_cloned("_inline_SUITE") -> true; +is_cloned("_no_module_opt_SUITE") -> true; +is_cloned("_cover_SUITE") -> true; +is_cloned([_|T]) -> is_cloned(T); +is_cloned([]) -> false. %% Return the highest opcode use in the BEAM module. @@ -158,6 +157,25 @@ highest_opcode(Beam) -> <<16:32,FormatNumber:32,HighestOpcode:32,_/binary>> = Code, HighestOpcode. +%% Get all unique files in the test case directory. +get_unique_files(Ext) -> + get_unique_files(Ext, fun(_ModString) -> false end). + +get_unique_files(Ext, IsCloned) when is_function(IsCloned, 1) -> + Wc = filename:join(filename:dirname(code:which(?MODULE)), "*"++Ext), + [F || F <- filelib:wildcard(Wc), + not is_cloned(F, Ext, IsCloned), not is_lfe_module(F, Ext)]. + +is_cloned(File, Ext, IsCloned) -> + ModString = filename:basename(File, Ext), + is_cloned(ModString) orelse IsCloned(ModString). + +is_lfe_module(File, Ext) -> + case filename:basename(File, Ext) of + "lfe_" ++ _ -> true; + _ -> false + end. + %% p_run(fun(Data) -> ok|error, List) -> ok %% Will fail the test case if there were any errors.