Skip to content

Commit

Permalink
Add UnresolvedRuntimeCall for unresolved function calls
Browse files Browse the repository at this point in the history
These can occasionally happen due to known incomplete spots in our call
resolution (#69) or due
to changes in the optimizer, etc.

Rather than conservatively calling these "dynamic dispatches", this adds
a new error type that encourages the user to file a bug upstream, since a
feature-complete AllocCheck should never encounter them.
  • Loading branch information
topolarity committed Oct 8, 2024
1 parent bcd38ab commit f7ab19c
Show file tree
Hide file tree
Showing 4 changed files with 30 additions and 8 deletions.
3 changes: 3 additions & 0 deletions src/AllocCheck.jl
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ function find_allocs!(mod::LLVM.Module, meta, entry_name::String; ignore_throw=t
bt = backtrace_(inst; compiled)
fname = replace(name(decl), r"^ijl_"=>"jl_")
push!(errors, AllocatingRuntimeCall(fname, bt))
elseif class === :unresolved
bt = backtrace_(inst; compiled)
push!(errors, UnresolvedRuntimeCall(bt))
end

if decl isa LLVM.Function && length(blocks(decl)) > 0 && !in(decl, seen)
Expand Down
10 changes: 6 additions & 4 deletions src/classify.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ perform allocation, but which might allocate to get its job done (e.g. jl_subtyp
function classify_runtime_fn(name::AbstractString; ignore_throw::Bool)
match_ = match(r"^(ijl_|jl_)(.*)$", name)

isnothing(match_) && return (:unknown, false)
isnothing(match_) && return (:none, false)
name = match_[2]

may_alloc = fn_may_allocate(name; ignore_throw)
Expand All @@ -28,8 +28,10 @@ function classify_runtime_fn(name::AbstractString; ignore_throw::Bool)
"f__call_in_world", "f__call_in_world_total", "f_intrinsic_call", "f_invoke",
"f_opaque_closure_call", "apply", "apply_generic", "gf_invoke",
"gf_invoke_by_method", "gf_invoke_lookup_worlds", "invoke", "invoke_api",
"call", "call0", "call1", "call2", "call3", "unknown_fptr")
"call", "call0", "call1", "call2", "call3")
return (:dispatch, may_alloc)
elseif name == "unknown_fptr"
return (:unresolved, false)
else
return (:runtime, may_alloc)
end
Expand Down Expand Up @@ -222,7 +224,7 @@ and replace it with a new locally-declared function that has the
resolved name as its identifier.
"""
function rename_call!(call::LLVM.CallInst, mod::LLVM.Module)
callee = called_operand(call)
callee = LLVM.called_operand(call)
if isa(callee, LLVM.LoadInst)

fn_got = unwrap_ptr_casts(operands(callee)[1])
Expand Down Expand Up @@ -252,7 +254,7 @@ function rename_call!(call::LLVM.CallInst, mod::LLVM.Module)
# Call to a runtime-determined function pointer, usually an OpaqueClosure
# or a ccall that we were not able to fully resolve.
#
# We label this as a DynamicDispatch to an unknown function target.
# We label this as an UnresolvedRuntimeCall, and request that the user report our bug
fname = "jl_unknown_fptr"
end

Expand Down
17 changes: 17 additions & 0 deletions src/types.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
struct UnresolvedRuntimeCall
backtrace::Vector{Base.StackTraces.StackFrame}
end

Base.hash(self::UnresolvedRuntimeCall, h::UInt) = nice_hash(self.backtrace, h)
Base.:(==)(self::UnresolvedRuntimeCall, other::UnresolvedRuntimeCall) = nice_isequal(self.backtrace,other.backtrace)

function Base.show(io::IO, call::UnresolvedRuntimeCall)
if length(call.backtrace) == 0
Base.printstyled(io, "Unresolved runtime call", color=:red, bold=true)
else
Base.printstyled(io, "Unresolved runtime call", color=:red, bold=true)
Base.println(io, " in ", call.backtrace[1].file, ":", call.backtrace[1].line)
show_backtrace_and_excerpt(io, call.backtrace)
end
Base.println(io, " (This is an AllocCheck.jl bug, please report it at https://github.com/JuliaLang/AllocCheck.jl/issues)")
end

struct AllocatingRuntimeCall
name::String
Expand Down
8 changes: 4 additions & 4 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using AllocCheck
using AllocCheck: AllocatingRuntimeCall, DynamicDispatch, AllocationSite
using AllocCheck: AllocatingRuntimeCall, DynamicDispatch, AllocationSite, UnresolvedRuntimeCall
using Test

mutable struct Foo{T}
Expand Down Expand Up @@ -75,7 +75,7 @@ end
for alloc in allocs)

allocs = check_allocs(call_opaque_closure, (Int, Int); ignore_throw = false)
@test length(allocs) > 0 && any(alloc isa DynamicDispatch for alloc in allocs)
@test length(allocs) > 0 && any(alloc isa UnresolvedRuntimeCall for alloc in allocs)
end

@testset "@check_allocs macro (syntax)" begin
Expand Down Expand Up @@ -205,11 +205,11 @@ end
@test any(x isa AllocationSite && x.type == Memory # uses jl_genericmemory_copy_slice
for x in check_allocs(copy, (Vector{Int},)))

@test all(x isa DynamicDispatch || (x isa AllocationSite && x.type == Memory{UInt8}) # uses jl_string_to_genericmemory
@test all(x isa UnresolvedRuntimeCall || (x isa AllocationSite && x.type == Memory{UInt8}) # uses jl_string_to_genericmemory
for x in check_allocs(Base.array_new_memory, (Memory{UInt8}, Int)))

# Marked broken because the `Expr(:foreigncall, QuoteNode(:jl_alloc_string), ...)` should be resolved
# by AllocCheck.jl, but is instead (conservatively) marked as a DynamicDisaptch.
# by AllocCheck.jl, but is instead (conservatively) marked as a UnresolvedRuntimeCall
#
# We get thrown off by the `jl_load_and_lookup` machinery here.
@test_broken all(x isa AllocationSite && x.type == Memory{UInt8} # uses jl_string_to_genericmemory
Expand Down

0 comments on commit f7ab19c

Please sign in to comment.