diff --git a/src/compiler/codegen/expressions.jl b/src/compiler/codegen/expressions.jl index 31b75122..f9e4c4e0 100644 --- a/src/compiler/codegen/expressions.jl +++ b/src/compiler/codegen/expressions.jl @@ -221,6 +221,30 @@ function record_coverage!(ctx::CGCtx) line > 0 || return nothing file = string(loc.file) isempty(file) && return nothing + ccall(:jl_coverage_visit_line, Cvoid, (Cstring, Csize_t, Cint), + file, ncodeunits(file), line) + ctx.recorded_coverage = true + return nothing +end + +# The per-statement `:code_coverage_effect`s only cover body lines. Julia's codegen +# visits the function-definition (signature) line separately at the prologue +# (`toplineno`); mirror that here, or the signature is reported as missed while the +# body is hit. Only for tracked kernels (body carried coverage effects), to avoid +# reporting signatures of untracked code. +function record_definition_coverage!(ctx::CGCtx) + Base.JLOptions().code_coverage == 0 && return nothing + ctx.recorded_coverage || return nothing + inst = first_located_instruction(ctx.sci, ctx.sci.entry) + inst === nothing && return nothing + stack = source_location(ctx.sci, inst) + isempty(stack) && return nothing + outer = stack[1] # outermost frame = the kernel itself + m = outer.method + m isa MethodInstance && (m = m.def) + file = string(m isa Method ? m.file : outer.file) + line = Int(m isa Method ? m.line : outer.line) + (isempty(file) || line <= 0) && return nothing ccall(:jl_coverage_visit_line, Cvoid, (Cstring, Csize_t, Cint), file, ncodeunits(file), line) return nothing diff --git a/src/compiler/codegen/kernel.jl b/src/compiler/codegen/kernel.jl index 4ba7f831..feea62b0 100644 --- a/src/compiler/codegen/kernel.jl +++ b/src/compiler/codegen/kernel.jl @@ -188,6 +188,10 @@ function emit_kernel!(writer::BytecodeWriter, func_buf::Vector{UInt8}, # Emit the structured IR (uses original Julia SSA indices everywhere) emit_block!(ctx, ctx.sci.entry) + # Cover the function-definition line (Julia's codegen does this at the + # prologue; the per-statement coverage effects only cover body lines). + record_definition_coverage!(ctx) + finalize_function!(func_buf, cb, writer.debug_info) end diff --git a/src/compiler/utils.jl b/src/compiler/utils.jl index b3f6c40a..2c4ebcc3 100644 --- a/src/compiler/utils.jl +++ b/src/compiler/utils.jl @@ -322,6 +322,11 @@ mutable struct CGCtx # `tuple_element_source` and other parent-walking queries can start # from the right scope. `nothing` when no block has been entered yet. current_block::Any + + # Set by `record_coverage!` when at least one `:code_coverage_effect` was + # marked for this kernel. Gates the function-definition-line coverage visit + # in `emit_kernel!` so untracked kernels aren't reported. + recorded_coverage::Bool end function CGCtx(; cb::CodeBuilder, tt::TypeTable, sci::StructuredIRCode, @@ -354,6 +359,7 @@ function CGCtx(; cb::CodeBuilder, tt::TypeTable, sci::StructuredIRCode, nothing, # bounds_info — set by run_passes! Dict{Value, Value}(), # assume_wrapped nothing, # current_block — set by emit_block! + false, # recorded_coverage — set by record_coverage! ) end diff --git a/test/codegen/coverage.jl b/test/codegen/coverage.jl index 5f4033cd..37ab6346 100644 --- a/test/codegen/coverage.jl +++ b/test/codegen/coverage.jl @@ -33,6 +33,22 @@ return false end + # Execution count recorded for an exact line, or `nothing` if not instrumented. + function lcov_line_count(tracefile, file, line) + in_block = false + for l in eachline(tracefile) + if startswith(l, "SF:") + in_block = (l == "SF:" * file) + elseif l == "end_of_record" + in_block = false + elseif in_block && startswith(l, "DA:") + ln, cnt = parse.(Int, split(l[4:end], ",")) + ln == line && return cnt + end + end + return nothing + end + if Base.JLOptions().code_coverage == 0 @test_skip "requires --code-coverage" else @@ -50,6 +66,15 @@ m = only(methods(f)) @test lcov_any_covered(tracefile, string(m.file), m.line, m.line + 4) end + + # The kernel entry's definition (signature) line specifically must be + # covered — the per-statement coverage effects only mark body lines, so + # without the prologue visit the header shows as missed (red). Use the + # compiled entry `cov_kernel`, not the `@inline`d `cov_only_scale` whose + # signature line is intentionally not marked (Julia doesn't mark it either). + m = only(methods(cov_kernel)) + @test lcov_line_count(tracefile, string(m.file), m.line) !== nothing + @test something(lcov_line_count(tracefile, string(m.file), m.line), 0) >= 1 end end end