File 2361-compiler-Add-support-for-coverage.patch of Package erlang
From d8ad1b0390a7678387d63f0476058420290336fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Gustavsson?= <bjorn@erlang.org>
Date: Fri, 3 Nov 2023 09:42:39 +0100
Subject: [PATCH 1/4] compiler: Add support for coverage
Add the new compiler pass `sys_coverage`. It can be invoked by giving
the compiler the `line_coverage`. It will emit `executable_line`
BEAM instructions on each executable line.
There is also an `sys_coverage:cover_transform/2` function, which is
useful for `cover`.
---
.gitignore | 1 +
erts/emulator/beam/emu/ops.tab | 2 +
.../emulator/beam/jit/arm/beam_asm_module.cpp | 3 +
erts/emulator/beam/jit/arm/ops.tab | 2 +
.../emulator/beam/jit/x86/beam_asm_module.cpp | 3 +
erts/emulator/beam/jit/x86/ops.tab | 2 +
lib/compiler/doc/src/compile.xml | 33 +-
lib/compiler/src/Makefile | 1 +
lib/compiler/src/beam_asm.erl | 29 +-
lib/compiler/src/beam_block.erl | 1 +
lib/compiler/src/beam_dict.erl | 33 +-
lib/compiler/src/beam_disasm.erl | 7 +
lib/compiler/src/beam_flatten.erl | 3 +-
lib/compiler/src/beam_ssa.erl | 2 +-
lib/compiler/src/beam_ssa_alias.erl | 2 +
lib/compiler/src/beam_ssa_codegen.erl | 4 +
lib/compiler/src/beam_ssa_opt.erl | 8 +
lib/compiler/src/beam_ssa_pre_codegen.erl | 1 +
lib/compiler/src/beam_ssa_share.erl | 31 +-
lib/compiler/src/beam_validator.erl | 2 +
lib/compiler/src/compile.erl | 2 +
lib/compiler/src/compiler.app.src | 1 +
lib/compiler/src/genop.tab | 6 +
lib/compiler/src/sys_coverage.erl | 579 ++++++++++++++++++
lib/compiler/src/v3_core.erl | 4 +
lib/compiler/test/Makefile | 12 +-
lib/compiler/test/bs_bincomp_SUITE.erl | 2 +
lib/compiler/test/test_lib.erl | 5 +-
lib/stdlib/src/erl_expand_records.erl | 4 +-
lib/stdlib/src/erl_lint.erl | 2 +
lib/stdlib/src/erl_pp.erl | 2 +
31 files changed, 752 insertions(+), 37 deletions(-)
create mode 100644 lib/compiler/src/sys_coverage.erl
diff --git a/erts/emulator/beam/emu/ops.tab b/erts/emulator/beam/emu/ops.tab
index 4d227cc89b..2b08dd60a9 100644
--- a/erts/emulator/beam/emu/ops.tab
+++ b/erts/emulator/beam/emu/ops.tab
@@ -95,6 +95,8 @@ move S X0=x==0 | line Loc => line Loc | move S X0
line n => _
line I
+executable_line Line => _
+
# For the JIT, the init_yregs/1 instruction allows generation of better code.
# For the BEAM interpreter, though, it will probably be more efficient to
# translate all uses of init_yregs/1 back to the instructions that the compiler
diff --git a/erts/emulator/beam/jit/arm/beam_asm_module.cpp b/erts/emulator/beam/jit/arm/beam_asm_module.cpp
index c66c10e6bf..c9eaff19d2 100644
--- a/erts/emulator/beam/jit/arm/beam_asm_module.cpp
+++ b/erts/emulator/beam/jit/arm/beam_asm_module.cpp
@@ -451,6 +451,9 @@ void BeamModuleAssembler::emit_func_line(const ArgWord &Loc) {
void BeamModuleAssembler::emit_empty_func_line() {
}
+void BeamModuleAssembler::emit_executable_line(const ArgWord &Loc) {
+}
+
/*
* Here follows stubs for instructions that should never be called.
*/
diff --git a/erts/emulator/beam/jit/arm/ops.tab b/erts/emulator/beam/jit/arm/ops.tab
index c5a7da3142..f3c99bac9f 100644
--- a/erts/emulator/beam/jit/arm/ops.tab
+++ b/erts/emulator/beam/jit/arm/ops.tab
@@ -86,6 +86,8 @@ func_line I
line n => _
line I
+executable_line I
+
allocate t t
allocate_heap t I t
diff --git a/erts/emulator/beam/jit/x86/beam_asm_module.cpp b/erts/emulator/beam/jit/x86/beam_asm_module.cpp
index 7eb4e2d6be..cc4a4b7e74 100644
--- a/erts/emulator/beam/jit/x86/beam_asm_module.cpp
+++ b/erts/emulator/beam/jit/x86/beam_asm_module.cpp
@@ -383,6 +383,9 @@ void BeamModuleAssembler::emit_func_line(const ArgWord &Loc) {
void BeamModuleAssembler::emit_empty_func_line() {
}
+void BeamModuleAssembler::emit_executable_line(const ArgWord &Loc) {
+}
+
/*
* Here follows stubs for instructions that should never be called.
*/
diff --git a/erts/emulator/beam/jit/x86/ops.tab b/erts/emulator/beam/jit/x86/ops.tab
index 39b9f32ef3..b835114b2a 100644
--- a/erts/emulator/beam/jit/x86/ops.tab
+++ b/erts/emulator/beam/jit/x86/ops.tab
@@ -86,6 +86,8 @@ func_line I
line n => _
line I
+executable_line I
+
allocate t t
allocate_heap t I t
diff --git a/lib/compiler/doc/src/compile.xml b/lib/compiler/doc/src/compile.xml
index a623281eb7..d7c5e34dd2 100644
--- a/lib/compiler/doc/src/compile.xml
+++ b/lib/compiler/doc/src/compile.xml
@@ -620,7 +620,38 @@ module.beam: module.erl \
option is internal to the compiler and can be changed or
removed at any time without prior warning.</p>
</item>
- </taglist>
+
+ <tag><c>line_coverage</c></tag>
+ <item>
+ <p><marker id="line_coverage"/>Instrument the compiled
+ code for line coverage by inserting an
+ <c>executable_line</c> instruction for each executable
+ line in the source code. By default, this instruction will
+ be ignored when loading the code.</p> <p>To activate the
+ <c>executable_line</c> instructions, the runtime system
+ must be started with the option <seecom
+ marker="erts:erl#+JPcover">+JPcover</seecom> to enable a
+ coverage mode. Alternatively, <seemfa
+ marker="kernel:code#set_coverage_mode/1">code:set_coverage_mode/1</seemfa>
+ can be used to set a coverage mode before
+ loading the code.</p>
+ <p>The coverage information gathered by the instrumented
+ code can be retrieved by calling <seemfa
+ marker="kernel:code#get_coverage/2">code:get_coverage(line, Module)</seemfa>.</p>
+ </item>
+
+ <tag><c>force_line_counters</c></tag>
+ <item>
+ <p><marker id="force_line_counters"/> When combined with
+ option <c>line_coverage</c>, this module will be loaded in
+ the <c>line_counter</c> coverage mode, regardless of the
+ current <seemfa
+ marker="kernel:code#get_coverage_mode/0">coverage
+ mode</seemfa> in the runtime system. This option is used
+ by <seeerl marker="tools:cover">cover</seeerl> to load
+ cover-compiled code.</p>
+ </item>
+ </taglist>
<p>If warnings are turned on (option <c>report_warnings</c>
described earlier), the following options control what type of
diff --git a/lib/compiler/src/Makefile b/lib/compiler/src/Makefile
index 7e59241429..a33d14f2d5 100644
--- a/lib/compiler/src/Makefile
+++ b/lib/compiler/src/Makefile
@@ -100,6 +100,7 @@ MODULES = \
sys_core_fold_lists \
sys_core_inline \
sys_core_prepare \
+ sys_coverage \
sys_messages \
sys_pre_attributes \
v3_core \
diff --git a/lib/compiler/src/beam_asm.erl b/lib/compiler/src/beam_asm.erl
index 277c433013..0186ab3e04 100644
--- a/lib/compiler/src/beam_asm.erl
+++ b/lib/compiler/src/beam_asm.erl
@@ -55,6 +55,10 @@
-type module_code() ::
{module(),[_],[_],[asm_function()],pos_integer()}.
+%% Flags for the line table.
+-define(BEAMFILE_EXECUTABLE_LINE, 1).
+-define(BEAMFILE_FORCE_LINE_COUNTERS, 2).
+
-spec module(module_code(), [{binary(), binary()}], [{atom(),term()}], [compile:option()]) ->
{'ok',binary()}.
@@ -180,7 +184,7 @@ build_file(Code, Attr, Dict, NumLabels, NumFuncs, ExtraChunks0, CompileInfo, Com
end,
%% Create the line chunk.
- LineChunk = chunk(<<"Line">>, build_line_table(Dict)),
+ LineChunk = chunk(<<"Line">>, build_line_table(Dict, CompilerOpts)),
%% Create the type table chunk.
{NumTypes, TypeTab} = beam_dict:type_table(Dict),
@@ -282,8 +286,8 @@ build_attributes(Attr, Compile, MD5) ->
CompileBinary = term_to_binary([{version,?COMPILER_VSN}|Compile]),
{AttrBinary,CompileBinary}.
-build_line_table(Dict) ->
- {NumLineInstrs,NumFnames0,Fnames0,NumLines,Lines0} =
+build_line_table(Dict, Options) ->
+ {NumLineInstrs,NumFnames0,Fnames0,NumLines,Lines0,ExecLine} =
beam_dict:line_table(Dict),
NumFnames = NumFnames0 - 1,
[_|Fnames1] = Fnames0,
@@ -292,10 +296,20 @@ build_line_table(Dict) ->
Lines1 = encode_line_items(Lines0, 0),
Lines = iolist_to_binary(Lines1),
Ver = 0,
- Bits = 0,
+ Bits = line_bits(ExecLine, Options),
<<Ver:32,Bits:32,NumLineInstrs:32,NumLines:32,NumFnames:32,
Lines/binary,Fnames/binary>>.
+line_bits(ExecLine, Options) ->
+ case member(force_line_counters, Options) of
+ true ->
+ ?BEAMFILE_FORCE_LINE_COUNTERS bor ?BEAMFILE_EXECUTABLE_LINE;
+ false when ExecLine =:= true ->
+ ?BEAMFILE_EXECUTABLE_LINE;
+ false ->
+ 0
+ end.
+
%% encode_line_items([{FnameIndex,Line}], PrevFnameIndex)
%% Encode the line items compactly. Tag the FnameIndex with
%% an 'a' tag (atom) and only include it when it has changed.
@@ -347,9 +361,12 @@ bif_type(_, 2) -> bif2.
make_op({'%',_}, Dict) ->
{[],Dict};
-make_op({line,Location}, Dict0) ->
- {Index,Dict} = beam_dict:line(Location, Dict0),
+make_op({line=Op,Location}, Dict0) ->
+ {Index,Dict} = beam_dict:line(Location, Dict0, Op),
encode_op(line, [Index], Dict);
+make_op({executable_line=Op,Location}, Dict0) ->
+ {Index,Dict} = beam_dict:line(Location, Dict0, Op),
+ encode_op(executable_line, [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 b6d424d09c..9cd6d93783 100644
--- a/lib/compiler/src/beam_block.erl
+++ b/lib/compiler/src/beam_block.erl
@@ -169,6 +169,7 @@ collect({put_map,{f,0},Op,S,D,R,{list,Puts}}) ->
{set,[D],[S|Puts],{alloc,R,{put_map,Op,{f,0}}}};
collect({fmove,S,D}) -> {set,[D],[S],fmove};
collect({fconv,S,D}) -> {set,[D],[S],fconv};
+collect({executable_line,Line}) -> {set,[],[],{executable_line,Line}};
collect(_) -> error.
%% embed_lines([Instruction]) -> [Instruction]
diff --git a/lib/compiler/src/beam_dict.erl b/lib/compiler/src/beam_dict.erl
index 36bdb01df5..dbc74c1518 100644
--- a/lib/compiler/src/beam_dict.erl
+++ b/lib/compiler/src/beam_dict.erl
@@ -23,7 +23,7 @@
-export([new/0,opcode/2,highest_opcode/1,
atom/2,local/4,export/4,import/4,
- string/2,lambda/3,literal/2,line/2,fname/2,type/2,
+ 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]).
@@ -58,6 +58,7 @@
fnames = #{} :: fname_tab(),
lines = #{} :: line_tab(),
num_lines = 0 :: non_neg_integer(), %Number of line instructions
+ exec_line = false :: boolean(),
next_import = 0 :: non_neg_integer(),
string_offset = 0 :: non_neg_integer(),
next_literal = 0 :: non_neg_integer(),
@@ -201,23 +202,29 @@ literal1(Key, #asm{literals=Tab0,next_literal=NextIndex}=Dict) ->
%% Returns the index for a line instruction (adding information
%% to the location information table).
--spec line(list(), bdict()) -> {non_neg_integer(), bdict()}.
+-spec line(list(), bdict(), 'line' | 'executable_line') ->
+ {non_neg_integer(), bdict()}.
-line([], #asm{num_lines=N}=Dict) ->
+line([], #asm{num_lines=N}=Dict, Instr) when is_atom(Instr) ->
%% No location available. Return the special pre-defined
%% index 0.
{0,Dict#asm{num_lines=N+1}};
-line([{location,Name,Line}|_], #asm{lines=Lines,num_lines=N}=Dict0) ->
+line([{location,Name,Line}|_], #asm{lines=Lines,num_lines=N,
+ exec_line=ExecLine0}=Dict0, Instr)
+ when is_atom(Instr) ->
{FnameIndex,Dict1} = fname(Name, Dict0),
Key = {FnameIndex,Line},
+ ExecLine = ExecLine0 or (Instr =:= executable_line),
case Lines of
- #{Key := Index} -> {Index,Dict1#asm{num_lines=N+1}};
+ #{Key := Index} ->
+ {Index,Dict1#asm{num_lines=N+1,exec_line=ExecLine}};
_ ->
- Index = maps:size(Lines) + 1,
- {Index, Dict1#asm{lines=Lines#{Key=>Index},num_lines=N+1}}
+ Index = map_size(Lines) + 1,
+ {Index, Dict1#asm{lines=Lines#{Key=>Index},num_lines=N+1,
+ exec_line=ExecLine}}
end;
-line([_|T], #asm{}=Dict) ->
- line(T, Dict).
+line([_|T], #asm{}=Dict, Instr) ->
+ line(T, Dict, Instr).
-spec fname(nonempty_string(), bdict()) ->
{non_neg_integer(), bdict()}.
@@ -337,16 +344,18 @@ build_type_table([], Acc) ->
-spec line_table(bdict()) ->
{non_neg_integer(), %Number of line instructions.
non_neg_integer(),[string()],
- non_neg_integer(),[{non_neg_integer(),non_neg_integer()}]}.
+ non_neg_integer(),[{non_neg_integer(),non_neg_integer()}],
+ boolean()}.
-line_table(#asm{fnames=Fnames0,lines=Lines0,num_lines=NumLineInstrs}) ->
+line_table(#asm{fnames=Fnames0,lines=Lines0,
+ num_lines=NumLineInstrs,exec_line=ExecLine}) ->
NumFnames = maps:size(Fnames0),
Fnames1 = lists:keysort(2, maps:to_list(Fnames0)),
Fnames = [Name || {Name,_} <- Fnames1],
NumLines = maps:size(Lines0),
Lines1 = lists:keysort(2, maps:to_list(Lines0)),
Lines = [L || {L,_} <- Lines1],
- {NumLineInstrs,NumFnames,Fnames,NumLines,Lines}.
+ {NumLineInstrs,NumFnames,Fnames,NumLines,Lines,ExecLine}.
%% Search for binary string Str in the binary string pool Pool.
%% old_string(Str, Pool) -> none | Index
diff --git a/lib/compiler/src/beam_disasm.erl b/lib/compiler/src/beam_disasm.erl
index 35ba0ba82d..06064b1469 100644
--- a/lib/compiler/src/beam_disasm.erl
+++ b/lib/compiler/src/beam_disasm.erl
@@ -1303,6 +1303,13 @@ resolve_inst({bs_match,[{Fail,Ctx,{z,1},{u,_},Args}]},_,_,_) ->
Commands = resolve_bs_match_commands(List),
{bs_match,Fail,Ctx,{commands,Commands}};
+%%
+%% OTP 27.
+%%
+
+resolve_inst({executable_line,[Index]},_,_,_) ->
+ {line,resolve_arg(Index)};
+
%%
%% Catches instructions that are not yet handled.
%%
diff --git a/lib/compiler/src/beam_flatten.erl b/lib/compiler/src/beam_flatten.erl
index c8298d5e6f..9a29b1ad50 100644
--- a/lib/compiler/src/beam_flatten.erl
+++ b/lib/compiler/src/beam_flatten.erl
@@ -62,7 +62,8 @@ norm({set,[D],[S],get_tl}) -> {get_tl,S,D};
norm({set,[D],[S|Puts],{alloc,R,{put_map,Op,F}}}) ->
{put_map,F,Op,S,D,R,{list,Puts}};
norm({set,[],[],remove_message}) -> remove_message;
-norm({set,[],[],{line,_}=Line}) -> Line.
+norm({set,[],[],{line,_}=Line}) -> Line;
+norm({set,[],[],{executable_line,_}=Line}) -> Line.
norm_allocate({_Zero,nostack,Nh,[]}, Regs) ->
[{test_heap,Nh,Regs}];
diff --git a/lib/compiler/src/beam_ssa.erl b/lib/compiler/src/beam_ssa.erl
index db35a2fa41..4650def062 100644
--- a/lib/compiler/src/beam_ssa.erl
+++ b/lib/compiler/src/beam_ssa.erl
@@ -109,7 +109,7 @@
'bs_match' | 'bs_start_match' | 'bs_test_tail' |
'build_stacktrace' |
'call' | 'catch_end' |
- 'extract' |
+ 'executable_line' | 'extract' |
'get_hd' | 'get_map_element' | 'get_tl' | 'get_tuple_element' |
'has_map_field' |
'is_nonempty_list' | 'is_tagged_tuple' |
diff --git a/lib/compiler/src/beam_ssa_alias.erl b/lib/compiler/src/beam_ssa_alias.erl
index 6dd4b92e5d..554fbdcd68 100644
--- a/lib/compiler/src/beam_ssa_alias.erl
+++ b/lib/compiler/src/beam_ssa_alias.erl
@@ -512,6 +512,8 @@ aa_is([I=#b_set{dst=Dst,op=Op,args=Args,anno=Anno0}|Is], SS0, AAS0) ->
{SS1, AAS0};
bs_test_tail ->
{SS1, AAS0};
+ executable_line ->
+ {SS1, AAS0};
has_map_field ->
{SS1, AAS0};
is_nonempty_list ->
diff --git a/lib/compiler/src/beam_ssa_codegen.erl b/lib/compiler/src/beam_ssa_codegen.erl
index 0399d358de..739180d99c 100644
--- a/lib/compiler/src/beam_ssa_codegen.erl
+++ b/lib/compiler/src/beam_ssa_codegen.erl
@@ -389,6 +389,7 @@ classify_heap_need(build_stacktrace) -> gc;
classify_heap_need(call) -> gc;
classify_heap_need(catch_end) -> gc;
classify_heap_need(copy) -> neutral;
+classify_heap_need(executable_line) -> neutral;
classify_heap_need(extract) -> gc;
classify_heap_need(get_hd) -> neutral;
classify_heap_need(get_map_element) -> neutral;
@@ -1809,6 +1810,9 @@ cg_instr(bs_get_tail, [Src], Dst, Set) ->
cg_instr(bs_get_position, [Ctx], Dst, Set) ->
Live = get_live(Set),
[{bs_get_position,Ctx,Dst,Live}];
+cg_instr(executable_line, [], _Dst, #cg_set{anno=Anno}) ->
+ {line,Location} = line(Anno),
+ [{executable_line,Location}];
cg_instr(put_map, [{atom,assoc},SrcMap|Ss], Dst, Set) ->
Live = get_live(Set),
[{put_map_assoc,{f,0},SrcMap,Dst,Live,{list,Ss}}];
diff --git a/lib/compiler/src/beam_ssa_opt.erl b/lib/compiler/src/beam_ssa_opt.erl
index 06c1e79f20..620dcb941a 100644
--- a/lib/compiler/src/beam_ssa_opt.erl
+++ b/lib/compiler/src/beam_ssa_opt.erl
@@ -488,6 +488,14 @@ merge_updates_bs([{LblA,
%% Note that we retain the first update_record in case it's used elsewhere,
%% it's too rare to warrant special handling here.
[{LblA, BlkA}, {LblB, BlkB#b_blk{is=[Update]}}| merge_updates_bs(Bs)];
+merge_updates_bs([{LblA,
+ #b_blk{is=[#b_set{op=update_record}]=IsA,
+ last=#b_br{bool=#b_literal{val=true},
+ succ=LblB}}=BlkA},
+ {LblB,
+ #b_blk{is=[#b_set{op=executable_line}]=IsB}=BlkB} | Bs0]) ->
+ Bs = [{LblB,BlkB#b_blk{is=IsA}} | Bs0],
+ [{LblA,BlkA#b_blk{is=IsB}} | merge_updates_bs(Bs)];
merge_updates_bs([{Lbl, Blk} | Bs]) ->
[{Lbl, Blk} | merge_updates_bs(Bs)];
merge_updates_bs([]) ->
diff --git a/lib/compiler/src/beam_ssa_pre_codegen.erl b/lib/compiler/src/beam_ssa_pre_codegen.erl
index 8ca16374d0..572d771e98 100644
--- a/lib/compiler/src/beam_ssa_pre_codegen.erl
+++ b/lib/compiler/src/beam_ssa_pre_codegen.erl
@@ -2604,6 +2604,7 @@ use_zreg(bs_checked_skip) -> yes;
use_zreg(bs_ensure) -> yes;
use_zreg(bs_match_string) -> yes;
use_zreg(bs_set_position) -> yes;
+use_zreg(executable_line) -> yes;
use_zreg(kill_try_tag) -> yes;
use_zreg(landingpad) -> yes;
use_zreg(recv_marker_bind) -> yes;
diff --git a/lib/compiler/src/beam_ssa_share.erl b/lib/compiler/src/beam_ssa_share.erl
index 13b6038868..8ef50eede2 100644
--- a/lib/compiler/src/beam_ssa_share.erl
+++ b/lib/compiler/src/beam_ssa_share.erl
@@ -284,8 +284,9 @@ canonical_block({L,VarMap0}, Blocks) ->
%% * Variables defined in the instruction sequence are replaced with
%% {var,0}, {var,1}, and so on. Free variables are not changed.
%%
-%% * `location` annotations that would produce a `line` instruction are
-%% kept. All other annotations are cleared.
+%% * `location` annotations that would produce `line` or
+%% `executable_line` instructions are kept. All other annotations
+%% are cleared.
%%
%% * Instructions are repackaged into tuples instead of into the
%% usual records. The main reason is to avoid violating the types for
@@ -300,17 +301,21 @@ canonical_is([#b_set{op=Op,dst=Dst,args=Args0}=I|Is], VarMap0, Acc) ->
Args = [canonical_arg(Arg, VarMap0) || Arg <- Args0],
Var = {var,map_size(VarMap0)},
VarMap = VarMap0#{Dst=>Var},
- LineAnno = case Op of
- bs_match ->
- %% The location annotation for a bs_match instruction
- %% is only used in warnings, never to emit a `line`
- %% instruction. Therefore, it should not be included.
- [];
- _ ->
- %% The location annotation will be used in a `line`
- %% instruction. It must be included.
- beam_ssa:get_anno(location, I, none)
- end,
+ LineAnno =
+ case {Op,Is} of
+ {executable_line, _} ->
+ %% The location annotation will be used in a
+ %% `executable_line` instruction.
+ beam_ssa:get_anno(location, I, none);
+ {_, [#b_set{op={succeeded,body},args=[Dst]}|_]} ->
+ %% The location annotation will be used in a `line`
+ %% instruction.
+ beam_ssa:get_anno(location, I, none);
+ {_, _} ->
+ %% The location annotation will not be included in
+ %% any BEAM instruction.
+ []
+ end,
canonical_is(Is, VarMap, {Op,LineAnno,Var,Args,Acc});
canonical_is([#b_ret{arg=Arg}], VarMap, Acc) ->
{{ret,canonical_arg(Arg, VarMap),Acc},VarMap};
diff --git a/lib/compiler/src/beam_validator.erl b/lib/compiler/src/beam_validator.erl
index ead9c12052..98bc729b18 100644
--- a/lib/compiler/src/beam_validator.erl
+++ b/lib/compiler/src/beam_validator.erl
@@ -372,6 +372,8 @@ vi({'%',_}, Vst) ->
Vst;
vi({line,_}, Vst) ->
Vst;
+vi({executable_line,_}, Vst) ->
+ Vst;
vi(nif_start, Vst) ->
Vst;
%%
diff --git a/lib/compiler/src/compile.erl b/lib/compiler/src/compile.erl
index 33c47d68a7..d4e693aee0 100644
--- a/lib/compiler/src/compile.erl
+++ b/lib/compiler/src/compile.erl
@@ -830,6 +830,8 @@ abstr_passes(AbstrStatus) ->
{delay,[{iff,debug_info,?pass(save_abstract_code)}]},
+ {iff,line_coverage,{pass,sys_coverage}},
+
?pass(expand_records),
{iff,'dexp',{listing,"expand"}},
{iff,'E',?pass(legalize_vars)},
diff --git a/lib/compiler/src/compiler.app.src b/lib/compiler/src/compiler.app.src
index 90448a67a0..71588c0826 100644
--- a/lib/compiler/src/compiler.app.src
+++ b/lib/compiler/src/compiler.app.src
@@ -75,6 +75,7 @@
sys_core_fold_lists,
sys_core_inline,
sys_core_prepare,
+ sys_coverage,
sys_messages,
sys_pre_attributes,
v3_core,
diff --git a/lib/compiler/src/genop.tab b/lib/compiler/src/genop.tab
index f2d50f3bc2..aec38bb54c 100755
--- a/lib/compiler/src/genop.tab
+++ b/lib/compiler/src/genop.tab
@@ -689,3 +689,9 @@ BEAM_FORMAT_NUMBER=0
## * {get_tail,Live,Unit,Dst}
## * {'=:=',Live,Size,Value}.
182: bs_match/3
+
+# OTP 27
+
+## @spec executable_line Location
+## @doc Provide location for an executable line.
+183: executable_line/1
diff --git a/lib/compiler/src/sys_coverage.erl b/lib/compiler/src/sys_coverage.erl
new file mode 100644
index 0000000000..994fa7b016
--- /dev/null
+++ b/lib/compiler/src/sys_coverage.erl
@@ -0,0 +1,579 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 1996-2023. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+%% Purpose: Instrument abstract code for coverage.
+
+-module(sys_coverage).
+-export([module/2,cover_transform/2]).
+-import(lists, [member/2,reverse/1,reverse/2]).
+
+-type attribute() :: atom().
+-type form() :: {function, integer(), atom(), arity(), _}
+ | {attribute, integer(), attribute(), _}.
+-type index_fun() :: fun((module(), atom(), arity(),
+ non_neg_integer(), non_neg_integer()) ->
+ non_neg_integer()).
+
+-spec module([form()], [compile:option()]) ->
+ {'ok',[form()]}.
+
+module(Forms0, _Opts) when is_list(Forms0) ->
+ IndexFun = fun(_, _, _, _, _) -> 0 end,
+ transform(Forms0, IndexFun).
+
+%% Undocumented helper function for the `cover` module.
+-spec cover_transform([form()], index_fun()) ->
+ {'ok',[form()]}.
+
+cover_transform(Forms, IndexFun) when is_list(Forms),
+ is_function(IndexFun, 5) ->
+ transform(Forms, IndexFun).
+
+%%%
+%%% Local functions.
+%%%
+
+%% Line doesn't matter.
+-define(BLOCK(Expr), {block,erl_anno:new(0),[Expr]}).
+-define(BLOCK1(Expr),
+ if
+ element(1, Expr) =:= block ->
+ Expr;
+ true ->
+ ?BLOCK(Expr)
+ end).
+-define(EXECUTABLE_LINE, executable_line).
+
+-record(vars,
+ {module=[] :: module() | [],
+ function=none :: atom(),
+ arity=0 :: arity(),
+ clause=0 :: non_neg_integer(),
+ lines=[] :: [non_neg_integer()],
+ bump_lines=[] :: [non_neg_integer()],
+ in_guard=false :: boolean(),
+ index_fun :: index_fun()
+ }).
+
+transform(Code, IndexFun) ->
+ Vars = #vars{index_fun=IndexFun},
+ transform(Code, [], Vars, none, on).
+
+transform([Form0|Forms], MungedForms, Vars0, MainFile0, Switch0) ->
+ Form = expand(Form0),
+ case munge(Form, Vars0, MainFile0, Switch0) of
+ ignore ->
+ transform(Forms, MungedForms, Vars0, MainFile0, Switch0);
+ {MungedForm, Vars, Switch} ->
+ transform(Forms, [MungedForm|MungedForms], Vars, MainFile0, Switch);
+ {MungedForm,Vars,MainFile,Switch} ->
+ transform(Forms, [MungedForm|MungedForms], Vars, MainFile, Switch)
+ end;
+transform([], MungedForms, _Vars, _, _) ->
+ {ok, reverse(MungedForms)}.
+
+%% Expand short-circuit Boolean expressions.
+expand(Expr0) ->
+ AllVars = sets:from_list(ordsets:to_list(vars([], Expr0)), [{version,2}]),
+ {Expr,_} = expand(Expr0, AllVars, 1),
+ Expr.
+
+expand({clause,Anno,Pattern,Guards,Body}, Vs, N) ->
+ %% We must not expand andalso/orelse in guards.
+ {ExpandedBody,N2} = expand(Body, Vs, N),
+ {{clause,Anno,Pattern,Guards,ExpandedBody},N2};
+expand({lc,Anno,Expr,Qs}, Vs, N) ->
+ {ExpandedExpr,N2} = expand(Expr, Vs, N),
+ {ExpandedQs,N3} = expand_qualifiers(Qs, Vs, N2),
+ {{lc,Anno,ExpandedExpr,ExpandedQs},N3};
+expand({bc,Anno,Expr,Qs}, Vs, N) ->
+ {ExpandedExpr,N2} = expand(Expr, Vs, N),
+ {ExpandedQs,N3} = expand_qualifiers(Qs, Vs, N2),
+ {{bc,Anno,ExpandedExpr,ExpandedQs},N3};
+expand({mc,Anno,Expr,Qs}, Vs, N) ->
+ {ExpandedExpr,N2} = expand(Expr, Vs, N),
+ {ExpandedQs,N3} = expand_qualifiers(Qs, Vs, N2),
+ {{mc,Anno,ExpandedExpr,ExpandedQs},N3};
+expand({op,_Anno,'andalso',ExprL,ExprR}, Vs, N) ->
+ {ExpandedExprL,N2} = expand(ExprL, Vs, N),
+ {ExpandedExprR,N3} = expand(ExprR, Vs, N2),
+ Anno = element(2, ExpandedExprL),
+ {bool_switch(ExpandedExprL,
+ ExpandedExprR,
+ {atom,Anno,false},
+ Vs, N3),
+ N3 + 1};
+expand({op,_Anno,'orelse',ExprL,ExprR}, Vs, N) ->
+ {ExpandedExprL,N2} = expand(ExprL, Vs, N),
+ {ExpandedExprR,N3} = expand(ExprR, Vs, N2),
+ Anno = element(2, ExpandedExprL),
+ {bool_switch(ExpandedExprL,
+ {atom,Anno,true},
+ ExpandedExprR,
+ Vs, N3),
+ N3 + 1};
+expand(T, Vs, N) when is_tuple(T) ->
+ {TL,N2} = expand(tuple_to_list(T), Vs, N),
+ {list_to_tuple(TL),N2};
+expand([E|Es], Vs, N) ->
+ {E2,N2} = expand(E, Vs, N),
+ {Es2,N3} = expand(Es, Vs, N2),
+ {[E2|Es2],N3};
+expand(T, _Vs, N) ->
+ {T,N}.
+
+expand_qualifiers([Q|Qs], Vs, N) ->
+ {Q2,N2} = case erl_lint:is_guard_test(Q) of
+ true ->
+ %% This qualifier is a guard test and will be
+ %% compiled as such. Don't expand andalso/orelse
+ %% because that would turn it into a body
+ %% expression that may raise an exception. Here
+ %% is an example of a filter where the error
+ %% behaviour would change:
+ %%
+ %% V == a orelse element(1, V) == a
+ %%
+ {Q,N};
+ false ->
+ %% A generator or a filter that is not a guard
+ %% test.
+ expand(Q, Vs, N)
+ end,
+ {Qs2,N3} = expand_qualifiers(Qs, Vs, N2),
+ {[Q2|Qs2],N3};
+expand_qualifiers([], _Vs, N) ->
+ {[],N}.
+
+vars(A, {var,_,V}) when V =/= '_' ->
+ [V|A];
+vars(A, T) when is_tuple(T) ->
+ vars(A, tuple_to_list(T));
+vars(A, [E|Es]) ->
+ vars(vars(A, E), Es);
+vars(A, _T) ->
+ A.
+
+bool_switch(E, T, F, AllVars, AuxVarN) ->
+ Anno = element(2, E),
+ AuxVar = {var,Anno,aux_var(AllVars, AuxVarN)},
+ {'case',Anno,E,
+ [{clause,Anno,[{atom,Anno,true}],[],[T]},
+ {clause,Anno,[{atom,Anno,false}],[],[F]},
+ %% Mark the next clause as compiler-generated to suppress
+ %% a warning if the case expression is an obvious boolean
+ %% value.
+ {clause,erl_anno:set_generated(true, Anno),[AuxVar],[],
+ [{call,Anno,
+ {remote,Anno,{atom,Anno,erlang},{atom,Anno,error}},
+ [{tuple,Anno,[{atom,Anno,badarg},AuxVar]}]}]}]}.
+
+aux_var(Vars, N) ->
+ Name = list_to_atom(lists:concat(['_', N])),
+ case sets:is_element(Name, Vars) of
+ true -> aux_var(Vars, N + 1);
+ false -> Name
+ end.
+
+%% This code traverses the abstract code, stored as the abstract_code
+%% chunk in the BEAM file, as described in absform(3). The switch is
+%% turned off when we encounter other files than the main file. This
+%% way we will be able to exclude functions defined in include files.
+
+munge({attribute,_,file,{File,_}}=Form, Vars, none, _) ->
+ {Form,Vars,File,on};
+munge({attribute,_,module,Mod}=Form, Vars, MainFile, Switch) when is_atom(Mod) ->
+ {Form,Vars#vars{module=Mod},MainFile,Switch};
+munge({function,Anno,Function,Arity,Clauses}, Vars0, _MainFile, on) ->
+ Vars = Vars0#vars{function=Function,
+ arity=Arity,
+ clause=1,
+ lines=[],
+ bump_lines=[]},
+ MungedClauses = munge_fun_clauses(Clauses, Vars),
+ {{function,Anno,Function,Arity,MungedClauses},Vars,on};
+munge({attribute,_,file,{MainFile,_}}=Form, Vars, MainFile, _Switch) ->
+ {Form,Vars,on}; % Switch on transformation!
+munge({attribute,_,file,{_InclFile,_}}=Form, Vars, _MainFile, _Switch) ->
+ {Form,Vars,off}; % Switch off transformation!
+munge({attribute,_,compile,{parse_transform,_}}, _, _, _) ->
+ %% Don't want to run parse transforms more than once.
+ ignore;
+munge(Form, Vars, _MainFile, Switch) -> % Other attributes and skipped includes.
+ {Form,Vars,Switch}.
+
+munge_fun_clauses([Clause0|Clauses], #vars{clause=ClauseIndex}=Vars0) ->
+ {clause,Anno,Pattern,Guards,Body} = Clause0,
+ {MungedGuards,_} = munge_exprs(Guards, Vars0#vars{in_guard=true}),
+ {MungedBody,_} = munge_body(Body, Vars0),
+ Vars = Vars0#vars{clause=ClauseIndex + 1},
+ Clause = {clause,Anno,Pattern,MungedGuards,MungedBody},
+ [Clause|munge_fun_clauses(Clauses, Vars)];
+munge_fun_clauses([], _Vars) -> [].
+
+%% Munge clauses in case, if, maybe, receive, and try.
+munge_clauses(Clauses, Vars) ->
+ munge_clauses(Clauses, Vars, Vars#vars.lines, []).
+
+munge_clauses([Clause|Clauses], Vars0, Lines0, MClauses) ->
+ {clause,Anno,Pattern,Guards,Body} = Clause,
+ {MungedGuards, _Vars} = munge_exprs(Guards, Vars0#vars{in_guard=true}),
+ {MungedBody, Vars} = munge_body(Body, Vars0),
+ NewBumps = new_bumps(Vars, Vars0),
+ Lines = NewBumps ++ Lines0,
+ munge_clauses(Clauses, Vars#vars{lines=Vars0#vars.lines},
+ Lines,
+ [{clause,Anno,Pattern,MungedGuards,MungedBody}|
+ MClauses]);
+munge_clauses([], Vars, Lines, MungedClauses) ->
+ {reverse(MungedClauses), Vars#vars{lines=Lines}}.
+
+munge_body(Expr, Vars) ->
+ munge_body(Expr, Vars, [], []).
+
+munge_body([E0|Es], Vars0, Acc0, LastExprBumpLines) ->
+ %% Here is the place to add an executable_line instruction.
+ Line = erl_anno:line(element(2, E0)),
+ Lines0 = Vars0#vars.lines,
+ case member(Line, Lines0) of
+ true ->
+ %% There is already a bump at this line.
+ {E1,Vars1} = munge_expr(E0, Vars0),
+ NewBumps = new_bumps(Vars1, Vars0),
+ BumpLines = [Line|Vars0#vars.bump_lines],
+ Vars2 = Vars1#vars{bump_lines=BumpLines},
+ Acc1 = maybe_fix_last_expr(Acc0, Vars2, LastExprBumpLines),
+ Acc = [E1|Acc1],
+ munge_body(Es, Vars2, Acc, NewBumps);
+ false ->
+ %% Put a bump at this line.
+ Bump = bump_call(Vars0, Line),
+ Lines1 = [Line|Lines0],
+ {E1,Vars1} = munge_expr(E0, Vars0#vars{lines=Lines1}),
+ NewBumps = new_bumps(Vars1, Vars0),
+ BumpLines = subtract(Vars1#vars.bump_lines, NewBumps),
+ Vars2 = Vars1#vars{bump_lines=BumpLines},
+ Acc1 = maybe_fix_last_expr(Acc0, Vars2, LastExprBumpLines),
+ Acc = [E1,Bump|Acc1],
+ munge_body(Es, Vars2, Acc, NewBumps)
+ end;
+munge_body([], Vars, Acc, _LastExprBumpLines) ->
+ {reverse(Acc), Vars}.
+
+%%% Fix last expression (OTP-8188). A typical example:
+%%%
+%%% 3: case X of
+%%% 4: 1 -> a; % Bump line 5 after "a" has been evaluated!
+%%% 5: 2 -> b; 3 -> c end, F()
+%%%
+%%% Line 5 wasn't bumped just before "F()" since it was already bumped
+%%% before "b" (and before "c") (one mustn't bump a line more than
+%%% once in a single "evaluation"). The expression "case X ... end" is
+%%% now traversed again ("fixed"), this time adding bumps of line 5
+%%% where appropriate, in this case when X matches 1.
+%%%
+%%% This doesn't solve all problems with expressions on the same line,
+%%% though. 'case' and 'try' are tricky. An example:
+%%%
+%%% 7: case case X of 1 -> foo(); % ?
+%%% 8: 2 -> bar() end of a -> 1;
+%%% 9: b -> 2 end.
+%%%
+%%% If X matches 1 and foo() evaluates to a then line 8 should be
+%%% bumped, but not if foo() evaluates to b. In other words, line 8
+%%% cannot be bumped after "foo()" on line 7, so one has to bump line
+%%% 8 before "begin 1 end". But if X matches 2 and bar evaluates to a
+%%% then line 8 would be bumped twice (there has to be a bump before
+%%% "bar()". It is like one would have to have two copies of the inner
+%%% clauses, one for each outer clause. Maybe the munging should be
+%%% done on some of the compiler's "lower level" format.
+%%%
+%%% 'fun' is also problematic since a bump inside the body "shadows"
+%%% the rest of the line.
+
+maybe_fix_last_expr(MungedExprs, Vars, LastExprBumpLines) ->
+ case last_expr_needs_fixing(Vars, LastExprBumpLines) of
+ {yes, Line} ->
+ fix_last_expr(MungedExprs, Line, Vars);
+ no ->
+ MungedExprs
+ end.
+
+last_expr_needs_fixing(Vars, LastExprBumpLines) ->
+ case common_elems(Vars#vars.bump_lines, LastExprBumpLines) of
+ [Line] ->
+ {yes, Line};
+ _ ->
+ no
+ end.
+
+fix_last_expr([MungedExpr|MungedExprs], Line, Vars) ->
+ %% No need to update ?COVER_TABLE.
+ Bump = bump_call(Vars, Line),
+ [fix_expr(MungedExpr, Line, Bump)|MungedExprs].
+
+fix_expr({'if',A,Clauses}, Line, Bump) ->
+ FixedClauses = fix_clauses(Clauses, Line, Bump),
+ {'if',A,FixedClauses};
+fix_expr({'case',A,Expr,Clauses}, Line, Bump) ->
+ FixedExpr = fix_expr(Expr, Line, Bump),
+ FixedClauses = fix_clauses(Clauses, Line, Bump),
+ {'case',A,FixedExpr,FixedClauses};
+fix_expr({'receive',A,Clauses}, Line, Bump) ->
+ FixedClauses = fix_clauses(Clauses, Line, Bump),
+ {'receive',A,FixedClauses};
+fix_expr({'receive',A,Clauses,Expr,Body}, Line, Bump) ->
+ FixedClauses = fix_clauses(Clauses, Line, Bump),
+ FixedExpr = fix_expr(Expr, Line, Bump),
+ FixedBody = fix_expr(Body, Line, Bump),
+ {'receive',A,FixedClauses,FixedExpr,FixedBody};
+fix_expr({'try',A,Exprs,Clauses,CatchClauses,After}, Line, Bump) ->
+ FixedExprs = fix_expr(Exprs, Line, Bump),
+ FixedClauses = fix_clauses(Clauses, Line, Bump),
+ FixedCatchClauses = fix_clauses(CatchClauses, Line, Bump),
+ FixedAfter = fix_expr(After, Line, Bump),
+ {'try',A,FixedExprs,FixedClauses,FixedCatchClauses,FixedAfter};
+fix_expr([E | Es], Line, Bump) ->
+ [fix_expr(E, Line, Bump) | fix_expr(Es, Line, Bump)];
+fix_expr(T, Line, Bump) when is_tuple(T) ->
+ list_to_tuple(fix_expr(tuple_to_list(T), Line, Bump));
+fix_expr(E, _Line, _Bump) ->
+ E.
+
+fix_clauses([], _Line, _Bump) ->
+ [];
+fix_clauses(Cs, Line, Bump) ->
+ case bumps_line(lists:last(Cs), Line) of
+ true ->
+ fix_cls(Cs, Line, Bump);
+ false ->
+ Cs
+ end.
+
+fix_cls([], _Line, _Bump) ->
+ [];
+fix_cls([Cl | Cls], Line, Bump) ->
+ case bumps_line(Cl, Line) of
+ true ->
+ [fix_expr(C, Line, Bump) || C <- [Cl | Cls]];
+ false ->
+ {clause,CA,P,G,Body} = Cl,
+ UniqueVarName = list_to_atom(lists:concat(["$cover$ ",Line])),
+ A = erl_anno:new(0),
+ V = {var,A,UniqueVarName},
+ [Last|Rest] = reverse(Body),
+ Body1 = reverse(Rest, [{match,A,V,Last},Bump,V]),
+ [{clause,CA,P,G,Body1} | fix_cls(Cls, Line, Bump)]
+ end.
+
+bumps_line(E, L) ->
+ try bumps_line1(E, L) catch true -> true end.
+
+bumps_line1({?EXECUTABLE_LINE,Line,_}, Line) ->
+ throw(true);
+bumps_line1([E | Es], Line) ->
+ bumps_line1(E, Line),
+ bumps_line1(Es, Line);
+bumps_line1(T, Line) when is_tuple(T) ->
+ bumps_line1(tuple_to_list(T), Line);
+bumps_line1(_, _) ->
+ false.
+
+%% Insert an executable_line instruction in the abstract code.
+bump_call(Vars, Line) ->
+ #vars{module=M,function=F,arity=A,clause=C,index_fun=GetIndex} = Vars,
+ Index = GetIndex(M, F, A, C, Line),
+ {?EXECUTABLE_LINE,Line,Index}.
+
+%%% End of fix of last expression.
+
+munge_expr({match,Anno,ExprL,ExprR}, Vars0) ->
+ {MungedExprL, Vars1} = munge_expr(ExprL, Vars0),
+ {MungedExprR, Vars2} = munge_expr(ExprR, Vars1),
+ {{match,Anno,MungedExprL,MungedExprR}, Vars2};
+munge_expr({maybe_match,Anno,ExprL,ExprR}, Vars0) ->
+ {MungedExprL, Vars1} = munge_expr(ExprL, Vars0),
+ {MungedExprR, Vars2} = munge_expr(ExprR, Vars1),
+ {{maybe_match,Anno,MungedExprL,MungedExprR}, Vars2};
+munge_expr({tuple,Anno,Exprs}, Vars0) ->
+ {MungedExprs, Vars1} = munge_exprs(Exprs, Vars0),
+ {{tuple,Anno,MungedExprs}, Vars1};
+munge_expr({record,Anno,Name,Exprs}, Vars0) ->
+ {MungedExprFields, Vars1} = munge_exprs(Exprs, Vars0),
+ {{record,Anno,Name,MungedExprFields}, Vars1};
+munge_expr({record,Anno,Arg,Name,Exprs}, Vars0) ->
+ {MungedArg, Vars1} = munge_expr(Arg, Vars0),
+ {MungedExprFields, Vars2} = munge_exprs(Exprs, Vars1),
+ {{record,Anno,MungedArg,Name,MungedExprFields}, Vars2};
+munge_expr({record_field,Anno,ExprL,ExprR}, Vars0) ->
+ {MungedExprR, Vars1} = munge_expr(ExprR, Vars0),
+ {{record_field,Anno,ExprL,MungedExprR}, Vars1};
+munge_expr({map,Anno,Fields}, Vars0) ->
+ {MungedFields, Vars1} = munge_exprs(Fields, Vars0),
+ {{map,Anno,MungedFields}, Vars1};
+munge_expr({map,Anno,Arg,Fields}, Vars0) ->
+ {MungedArg, Vars1} = munge_expr(Arg, Vars0),
+ {MungedFields, Vars2} = munge_exprs(Fields, Vars1),
+ {{map,Anno,MungedArg,MungedFields}, Vars2};
+munge_expr({map_field_assoc,Anno,Name,Value}, Vars0) ->
+ {MungedName, Vars1} = munge_expr(Name, Vars0),
+ {MungedValue, Vars2} = munge_expr(Value, Vars1),
+ {{map_field_assoc,Anno,MungedName,MungedValue}, Vars2};
+munge_expr({map_field_exact,Anno,Name,Value}, Vars0) ->
+ {MungedName, Vars1} = munge_expr(Name, Vars0),
+ {MungedValue, Vars2} = munge_expr(Value, Vars1),
+ {{map_field_exact,Anno,MungedName,MungedValue}, Vars2};
+munge_expr({cons,Anno,ExprH,ExprT}, Vars0) ->
+ {MungedExprH, Vars1} = munge_expr(ExprH, Vars0),
+ {MungedExprT, Vars2} = munge_expr(ExprT, Vars1),
+ {{cons,Anno,MungedExprH,MungedExprT}, Vars2};
+munge_expr({op,Anno,Op,ExprL,ExprR}, Vars0) ->
+ {MungedExprL, Vars1} = munge_expr(ExprL, Vars0),
+ {MungedExprR, Vars2} = munge_expr(ExprR, Vars1),
+ {{op,Anno,Op,MungedExprL,MungedExprR}, Vars2};
+munge_expr({op,Anno,Op,Expr}, Vars0) ->
+ {MungedExpr, Vars1} = munge_expr(Expr, Vars0),
+ {{op,Anno,Op,MungedExpr}, Vars1};
+munge_expr({'catch',Anno,Expr}, Vars0) ->
+ {MungedExpr, Vars1} = munge_expr(Expr, Vars0),
+ {{'catch',Anno,MungedExpr}, Vars1};
+munge_expr({call,Anno1,{remote,Anno2,ExprM,ExprF},Exprs}, Vars0) ->
+ {MungedExprM, Vars1} = munge_expr(ExprM, Vars0),
+ {MungedExprF, Vars2} = munge_expr(ExprF, Vars1),
+ {MungedExprs, Vars3} = munge_exprs(Exprs, Vars2),
+ {{call,Anno1,{remote,Anno2,MungedExprM,MungedExprF},MungedExprs}, Vars3};
+munge_expr({call,Anno,Expr,Exprs}, Vars0) ->
+ {MungedExpr, Vars1} = munge_expr(Expr, Vars0),
+ {MungedExprs, Vars2} = munge_exprs(Exprs, Vars1),
+ {{call,Anno,MungedExpr,MungedExprs}, Vars2};
+munge_expr({lc,Anno,Expr,Qs}, Vars0) ->
+ {MungedExpr, Vars1} = munge_expr(?BLOCK1(Expr), Vars0),
+ {MungedQs, Vars2} = munge_qualifiers(Qs, Vars1),
+ {{lc,Anno,MungedExpr,MungedQs}, Vars2};
+munge_expr({bc,Anno,Expr,Qs}, Vars0) ->
+ {MungedExpr,Vars1} = munge_expr(?BLOCK1(Expr), Vars0),
+ {MungedQs, Vars2} = munge_qualifiers(Qs, Vars1),
+ {{bc,Anno,MungedExpr,MungedQs}, Vars2};
+munge_expr({mc,Anno,{map_field_assoc,FAnno,K,V},Qs}, Vars0) ->
+ Expr = {map_field_assoc,FAnno,?BLOCK1(K),?BLOCK1(V)},
+ {MungedExpr, Vars1} = munge_expr(Expr, Vars0),
+ {MungedQs, Vars2} = munge_qualifiers(Qs, Vars1),
+ {{mc,Anno,MungedExpr,MungedQs}, Vars2};
+munge_expr({block,Anno,Body}, Vars0) ->
+ {MungedBody, Vars1} = munge_body(Body, Vars0),
+ {{block,Anno,MungedBody}, Vars1};
+munge_expr({'if',Anno,Clauses}, Vars0) ->
+ {MungedClauses,Vars1} = munge_clauses(Clauses, Vars0),
+ {{'if',Anno,MungedClauses}, Vars1};
+munge_expr({'case',Anno,Expr,Clauses}, Vars0) ->
+ {MungedExpr,Vars1} = munge_expr(Expr, Vars0),
+ {MungedClauses,Vars2} = munge_clauses(Clauses, Vars1),
+ {{'case',Anno,MungedExpr,MungedClauses}, Vars2};
+munge_expr({'receive',Anno,Clauses}, Vars0) ->
+ {MungedClauses,Vars1} = munge_clauses(Clauses, Vars0),
+ {{'receive',Anno,MungedClauses}, Vars1};
+munge_expr({'receive',Anno,Clauses,Expr,Body}, Vars0) ->
+ {MungedExpr,Vars1} = munge_expr(Expr, Vars0),
+ {MungedClauses,Vars2} = munge_clauses(Clauses, Vars1),
+ {MungedBody,Vars3} = munge_body(Body, Vars2#vars{lines=Vars1#vars.lines}),
+ Vars4 = Vars3#vars{lines=Vars2#vars.lines ++ new_bumps(Vars3, Vars2)},
+ {{'receive',Anno,MungedClauses,MungedExpr,MungedBody}, Vars4};
+munge_expr({'try',Anno,Body,Clauses,CatchClauses,After}, Vars0) ->
+ {MungedBody, Vars01} = munge_body(Body, Vars0),
+ {MungedClauses, Vars1} = munge_clauses(Clauses, Vars01),
+ {MungedCatchClauses, Vars2} = munge_clauses(CatchClauses, Vars1),
+ {MungedAfter, Vars3} = munge_body(After, Vars2),
+ {{'try',Anno,MungedBody,MungedClauses,MungedCatchClauses,MungedAfter},
+ Vars3};
+munge_expr({'maybe',Anno,Exprs}, Vars0) ->
+ {MungedExprs, Vars1} = munge_body(Exprs, Vars0),
+ {{'maybe',Anno,MungedExprs}, Vars1};
+munge_expr({'maybe',MaybeAnno,Exprs,{'else',ElseAnno,Clauses}}, Vars0) ->
+ {MungedExprs, Vars1} = munge_body(Exprs, Vars0),
+ {MungedClauses, Vars2} = munge_clauses(Clauses, Vars1),
+ {{'maybe',MaybeAnno,MungedExprs,{'else',ElseAnno,MungedClauses}}, Vars2};
+munge_expr({'fun',Anno,{clauses,Clauses}}, Vars0) ->
+ {MungedClauses,Vars1} = munge_clauses(Clauses, Vars0),
+ {{'fun',Anno,{clauses,MungedClauses}}, Vars1};
+munge_expr({named_fun,Anno,Name,Clauses}, Vars0) ->
+ {MungedClauses,Vars1} = munge_clauses(Clauses, Vars0),
+ {{named_fun,Anno,Name,MungedClauses}, Vars1};
+munge_expr({bin,Anno,BinElements}, Vars0) ->
+ {MungedBinElements,Vars1} = munge_exprs(BinElements, Vars0),
+ {{bin,Anno,MungedBinElements}, Vars1};
+munge_expr({bin_element,Anno,Value,Size,TypeSpecifierList}, Vars0) ->
+ {MungedValue,Vars1} = munge_expr(Value, Vars0),
+ {MungedSize,Vars2} = munge_expr(Size, Vars1),
+ {{bin_element,Anno,MungedValue,MungedSize,TypeSpecifierList},Vars2};
+munge_expr(Form, Vars0) ->
+ {Form, Vars0}.
+
+munge_exprs(Exprs, Vars) ->
+ munge_exprs(Exprs, Vars, []).
+
+munge_exprs([Expr|Exprs], #vars{in_guard=true}=Vars0, MungedExprs)
+ when is_list(Expr) ->
+ {MungedExpr, _Vars0} = munge_exprs(Expr, Vars0),
+ munge_exprs(Exprs, Vars0, [MungedExpr|MungedExprs]);
+munge_exprs([Expr|Exprs], Vars0, MungedExprs) ->
+ {MungedExpr, Vars1} = munge_expr(Expr, Vars0),
+ munge_exprs(Exprs, Vars1, [MungedExpr|MungedExprs]);
+munge_exprs([], Vars0, MungedExprs) ->
+ {reverse(MungedExprs), Vars0}.
+
+%% Every qualifier is decorated with a counter.
+munge_qualifiers(Qualifiers, Vars) ->
+ munge_qs(Qualifiers, Vars, []).
+
+munge_qs([{generate,Anno,Pattern,Expr}|Qs], Vars0, MQs) ->
+ A = element(2, Expr),
+ {MungedExpr, Vars1} = munge_expr(Expr, Vars0),
+ munge_qs1(Qs, A, {generate,Anno,Pattern,MungedExpr}, Vars0, Vars1, MQs);
+munge_qs([{b_generate,Anno,Pattern,Expr}|Qs], Vars0, MQs) ->
+ A = element(2, Expr),
+ {MExpr, Vars1} = munge_expr(Expr, Vars0),
+ munge_qs1(Qs, A, {b_generate,Anno,Pattern,MExpr}, Vars0, Vars1, MQs);
+munge_qs([{m_generate,Anno,Pattern,Expr}|Qs], Vars0, MQs) ->
+ A = element(2, Expr),
+ {MExpr, Vars1} = munge_expr(Expr, Vars0),
+ munge_qs1(Qs, A, {m_generate,Anno,Pattern,MExpr}, Vars0, Vars1, MQs);
+munge_qs([Expr|Qs], Vars0, MQs) ->
+ A = element(2, Expr),
+ {MungedExpr, Vars1} = munge_expr(Expr, Vars0),
+ munge_qs1(Qs, A, MungedExpr, Vars0, Vars1, MQs);
+munge_qs([], Vars0, MQs) ->
+ {reverse(MQs), Vars0}.
+
+munge_qs1(Qs, Anno, NQ, Vars0, Vars1, MQs) ->
+ case new_bumps(Vars1, Vars0) of
+ [_] ->
+ munge_qs(Qs, Vars1, [NQ | MQs]);
+ _ ->
+ {MungedTrue, Vars2} = munge_expr(?BLOCK({atom,Anno,true}), Vars1),
+ munge_qs(Qs, Vars2, [NQ, MungedTrue | MQs])
+ end.
+
+new_bumps(#vars{lines=New}, #vars{lines=Old}) ->
+ subtract(New, Old).
+
+subtract(L1, L2) ->
+ [E || E <- L1, not member(E, L2)].
+
+common_elems(L1, L2) ->
+ [E || E <- L1, member(E, L2)].
diff --git a/lib/compiler/src/v3_core.erl b/lib/compiler/src/v3_core.erl
index 7a3c8738b2..2f5db48076 100644
--- a/lib/compiler/src/v3_core.erl
+++ b/lib/compiler/src/v3_core.erl
@@ -976,6 +976,10 @@ expr({op,L,Op,L0,R0}, St0) ->
{#icall{anno=#a{anno=LineAnno}, %Must have an #a{}
module=#c_literal{anno=LineAnno,val=erlang},
name=#c_literal{anno=LineAnno,val=Op},args=As},Aps,St1};
+expr({executable_line,L,_}, St0) ->
+ {#iprimop{anno=#a{anno=lineno_anno(L, St0)},
+ name=#c_literal{val=executable_line},
+ args=[]},[],St0};
expr({ssa_check_when,L,WantedResult,Args,Tag,Clauses}, St) ->
{#c_opaque{anno=full_anno(L, St),val={ssa_check_when,WantedResult,Tag,Args,Clauses}}, [], St}.
diff --git a/lib/compiler/test/Makefile b/lib/compiler/test/Makefile
index 7efafc5e35..43c7ccec7a 100644
--- a/lib/compiler/test/Makefile
+++ b/lib/compiler/test/Makefile
@@ -122,6 +122,8 @@ R25= \
bs_utf \
bs_bincomp
+COVER=$(NO_OPT)
+
DIALYZER = bs_match
CORE_MODULES = \
@@ -156,6 +158,8 @@ NO_TYPE_OPT_MODULES= $(NO_TYPE_OPT:%=%_no_type_opt_SUITE)
NO_TYPE_OPT_ERL_FILES= $(NO_TYPE_OPT_MODULES:%=%.erl)
DIALYZER_MODULES= $(DIALYZER:%=%_dialyzer_SUITE)
DIALYZER_ERL_FILES= $(DIALYZER_MODULES:%=%.erl)
+COVER_MODULES= $(COVER:%=%_cover_SUITE)
+COVER_ERL_FILES= $(COVER_MODULES:%=%.erl)
ERL_FILES= $(MODULES:%=%.erl)
CORE_FILES= $(CORE_MODULES:%=%.core)
@@ -190,7 +194,7 @@ DISABLE_SSA_OPT = +no_bool_opt +no_share_opt +no_bsm_opt +no_fun_opt +no_ssa_opt
$(NO_CORE_OPT_ERL_FILES) $(NO_CORE_SSA_OPT_ERL_FILES) \
$(INLINE_ERL_FILES) $(R23_ERL_FILES) \
$(NO_MOD_OPT_ERL_FILES) $(NO_TYPE_OPT_ERL_FILES) \
- $(DIALYZER_ERL_FILES) $(R24_ERL_FILES) $(R25_ERL_FILES)
+ $(DIALYZER_ERL_FILES) $(COVER_ERL_FILES) $(R24_ERL_FILES) $(R25_ERL_FILES)
$(ERL_TOP)/make/make_emakefile $(ERL_COMPILE_FLAGS) -o$(EBIN) $(MODULES) \
> $(EMAKEFILE)
$(ERL_TOP)/make/make_emakefile +no_copt $(DISABLE_SSA_OPT) +no_postopt \
@@ -215,6 +219,8 @@ make_emakefile: $(NO_OPT_ERL_FILES) $(POST_OPT_ERL_FILES) $(NO_SSA_OPT_ERL_FILES
-o$(EBIN) $(CORE_MODULES) >> $(EMAKEFILE)
$(ERL_TOP)/make/make_emakefile +no_type_opt $(ERL_COMPILE_FLAGS) \
-o$(EBIN) $(NO_TYPE_OPT_MODULES) >> $(EMAKEFILE)
+ $(ERL_TOP)/make/make_emakefile +line_coverage $(ERL_COMPILE_FLAGS) \
+ -o$(EBIN) $(COVER_MODULES) >> $(EMAKEFILE)
$(ERL_TOP)/make/make_emakefile +dialyzer $(ERL_COMPILE_FLAGS) \
-o$(EBIN) $(DIALYZER_MODULES) >> $(EMAKEFILE)
@@ -265,6 +271,9 @@ docs:
%_no_type_opt_SUITE.erl: %_SUITE.erl
sed -e 's;-module($(basename $<));-module($(basename $@));' $< > $@
+%_cover_SUITE.erl: %_SUITE.erl
+ sed -e 's;-module($(basename $<));-module($(basename $@));' $< > $@
+
%_dialyzer_SUITE.erl: %_SUITE.erl
sed -e 's;-module($(basename $<));-module($(basename $@));' $< > $@
@@ -288,6 +297,7 @@ release_tests_spec: make_emakefile
$(NO_MOD_OPT_ERL_FILES) \
$(NO_SSA_OPT_ERL_FILES) \
$(NO_TYPE_OPT_ERL_FILES) \
+ $(COVER_ERL_FILES) \
$(DIALYZER_ERL_FILES) "$(RELSYSDIR)"
$(INSTALL_DATA) $(CORE_FILES) "$(RELSYSDIR)"
for file in $(ERL_DUMMY_FILES); do \
diff --git a/lib/compiler/test/bs_bincomp_SUITE.erl b/lib/compiler/test/bs_bincomp_SUITE.erl
index 41877a9e84..897501338c 100644
--- a/lib/compiler/test/bs_bincomp_SUITE.erl
+++ b/lib/compiler/test/bs_bincomp_SUITE.erl
@@ -688,6 +688,8 @@ cs_end() ->
%% Verify that the allocated size is exact (rounded up to the nearest byte).
cs(Bin) ->
case ?MODULE of
+ bs_bincomp_cover_SUITE ->
+ ok;
bs_bincomp_no_opt_SUITE ->
ok;
bs_bincomp_no_ssa_opt_SUITE ->
diff --git a/lib/compiler/test/test_lib.erl b/lib/compiler/test/test_lib.erl
index de610062dc..205ec00a96 100644
--- a/lib/compiler/test/test_lib.erl
+++ b/lib/compiler/test/test_lib.erl
@@ -90,6 +90,7 @@ opt_opts(Mod) ->
({feature,_,enable}) -> true;
({feature,_,disable}) -> true;
(inline) -> true;
+ (line_coverage) -> true;
(no_bs_create_bin) -> true;
(no_bsm_opt) -> true;
(no_bs_match) -> true;
@@ -123,7 +124,8 @@ get_data_dir(Config) ->
"_inline_SUITE",
"_no_module_opt_SUITE",
"_no_type_opt_SUITE",
- "_no_ssa_opt_SUITE"],
+ "_no_ssa_opt_SUITE",
+ "_cover_SUITE"],
lists:foldl(fun(Suffix, Acc) ->
Opts = [{return,list}],
re:replace(Acc, Suffix, "_SUITE", Opts)
@@ -144,6 +146,7 @@ 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.
diff --git a/lib/stdlib/src/erl_expand_records.erl b/lib/stdlib/src/erl_expand_records.erl
index 706b5fec6e..c0d2d1bcd3 100644
--- a/lib/stdlib/src/erl_expand_records.erl
+++ b/lib/stdlib/src/erl_expand_records.erl
@@ -452,7 +452,9 @@ expr({op,Anno,Op,L0,R0}, St0) ->
{L,St1} = expr(L0, St0),
{R,St2} = expr(R0, St1),
{{op,Anno,Op,L,R},St2};
-expr(E={ssa_check_when,_,_,_,_,_}, St) ->
+expr({executable_line,_,_}=E, St) ->
+ {E, St};
+expr({ssa_check_when,_,_,_,_,_}=E, St) ->
{E, St}.
expr_list([E0 | Es0], St0) ->
diff --git a/lib/stdlib/src/erl_lint.erl b/lib/stdlib/src/erl_lint.erl
index 107b0970cf..766aade3a0 100644
--- a/lib/stdlib/src/erl_lint.erl
+++ b/lib/stdlib/src/erl_lint.erl
@@ -2587,6 +2587,8 @@ expr({op,_Anno,_Op,L,R}, Vt, St) ->
%% The following are not allowed to occur anywhere!
expr({remote,_Anno,M,_F}, _Vt, St) ->
{[],add_error(erl_parse:first_anno(M), illegal_expr, St)};
+expr({executable_line,_,_}, _Vt, St) ->
+ {[], St};
expr({ssa_check_when,_Anno,_WantedResult,_Args,_Tag,_Exprs}, _Vt, St) ->
{[], St}.
diff --git a/lib/stdlib/src/erl_pp.erl b/lib/stdlib/src/erl_pp.erl
index 50ff87643c..a2399886ed 100644
--- a/lib/stdlib/src/erl_pp.erl
+++ b/lib/stdlib/src/erl_pp.erl
@@ -755,6 +755,8 @@ lexpr({remote,_,M,F}, Prec, Opts) ->
%% BIT SYNTAX:
lexpr({bin,_,Fs}, _, Opts) ->
bit_grp(Fs, Opts);
+lexpr({executable_line,Line,Index}, _Prec, _Opts) ->
+ leaf(format("beam_instruction:executable_line(~p, ~p)", [Line,Index]));
%% Special case for straight values.
lexpr({value,_,Val}, _,_) ->
{value,Val};
--
2.35.3