Skip to content

Commit

Permalink
Improve error message for which(fn, types) (#47369)
Browse files Browse the repository at this point in the history
Error messages for `MethodError` are much more helpful in determining
why the method was not successfully dispatched than simply "No unique
matching method found."

Fixes #47322
  • Loading branch information
apaz-cli authored Feb 3, 2024
1 parent fda9321 commit fc06291
Show file tree
Hide file tree
Showing 15 changed files with 94 additions and 49 deletions.
81 changes: 40 additions & 41 deletions base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -246,14 +246,15 @@ end
function showerror(io::IO, ex::MethodError)
# ex.args is a tuple type if it was thrown from `invoke` and is
# a tuple of the arguments otherwise.
is_arg_types = isa(ex.args, DataType)
arg_types = (is_arg_types ? ex.args : typesof(ex.args...))::DataType
is_arg_types = !isa(ex.args, Tuple)
arg_types = is_arg_types ? ex.args : typesof(ex.args...)
arg_types_param::SimpleVector = (unwrap_unionall(arg_types)::DataType).parameters
san_arg_types_param = Any[rewrap_unionall(a, arg_types) for a in arg_types_param]
f = ex.f
meth = methods_including_ambiguous(f, arg_types)
if isa(meth, MethodList) && length(meth) > 1
return showerror_ambiguous(io, meth, f, arg_types)
end
arg_types_param::SimpleVector = arg_types.parameters
print(io, "MethodError: ")
ft = typeof(f)
f_is_function = false
Expand All @@ -262,10 +263,11 @@ function showerror(io::IO, ex::MethodError)
f = (ex.args::Tuple)[2]
ft = typeof(f)
arg_types_param = arg_types_param[3:end]
san_arg_types_param = san_arg_types_param[3:end]
kwargs = pairs(ex.args[1])
ex = MethodError(f, ex.args[3:end::Int], ex.world)
arg_types = Tuple{arg_types_param...}
end
name = ft.name.mt.name
if f === Base.convert && length(arg_types_param) == 2 && !is_arg_types
f_is_function = true
show_convert_error(io, ex, arg_types_param)
Expand All @@ -277,36 +279,28 @@ function showerror(io::IO, ex::MethodError)
if ft <: Function && isempty(ft.parameters) && _isself(ft)
f_is_function = true
end
print(io, "no method matching ")
if is_arg_types
print(io, "no method matching invoke ")
else
print(io, "no method matching ")
end
buf = IOBuffer()
iob = IOContext(buf, io) # for type abbreviation as in #49795; some, like `convert(T, x)`, should not abbreviate
show_signature_function(iob, isa(f, Type) ? Type{f} : typeof(f))
print(iob, "(")
for (i, typ) in enumerate(arg_types_param)
print(iob, "::", typ)
i == length(arg_types_param) || print(iob, ", ")
end
if !isempty(kwargs)
print(iob, "; ")
for (i, (k, v)) in enumerate(kwargs)
print(iob, k, "::", typeof(v))
i == length(kwargs)::Int || print(iob, ", ")
end
end
print(iob, ")")
show_tuple_as_call(iob, :function, arg_types; hasfirst=false, kwargs = !isempty(kwargs) ? Any[(k, typeof(v)) for (k, v) in kwargs] : nothing)
str = String(take!(buf))
str = type_limited_string_from_context(io, str)
print(io, str)
end
# catch the two common cases of element-wise addition and subtraction
if (f === Base.:+ || f === Base.:-) && length(arg_types_param) == 2
if (f === Base.:+ || f === Base.:-) && length(san_arg_types_param) == 2
# we need one array of numbers and one number, in any order
if any(x -> x <: AbstractArray{<:Number}, arg_types_param) &&
any(x -> x <: Number, arg_types_param)
if any(x -> x <: AbstractArray{<:Number}, san_arg_types_param) &&
any(x -> x <: Number, san_arg_types_param)

nounf = f === Base.:+ ? "addition" : "subtraction"
varnames = ("scalar", "array")
first, second = arg_types_param[1] <: Number ? varnames : reverse(varnames)
first, second = san_arg_types_param[1] <: Number ? varnames : reverse(varnames)
fstring = f === Base.:+ ? "+" : "-" # avoid depending on show_default for functions (invalidation)
print(io, "\nFor element-wise $nounf, use broadcasting with dot syntax: $first .$fstring $second")
end
Expand All @@ -315,17 +309,25 @@ function showerror(io::IO, ex::MethodError)
print(io, "\nUse square brackets [] for indexing an Array.")
end
# Check for local functions that shadow methods in Base
if f_is_function && isdefined(Base, name)
basef = getfield(Base, name)
if basef !== ex.f && hasmethod(basef, arg_types)
print(io, "\nYou may have intended to import ")
show_unquoted(io, Expr(:., :Base, QuoteNode(name)))
let name = ft.name.mt.name
if f_is_function && isdefined(Base, name)
basef = getfield(Base, name)
if basef !== f && hasmethod(basef, arg_types)
print(io, "\nYou may have intended to import ")
show_unquoted(io, Expr(:., :Base, QuoteNode(name)))
end
end
end
if (ex.world != typemax(UInt) && hasmethod(ex.f, arg_types) &&
!hasmethod(ex.f, arg_types, world = ex.world))
if (ex.world != typemax(UInt) && hasmethod(f, arg_types) &&
!hasmethod(f, arg_types, world = ex.world))
curworld = get_world_counter()
print(io, "\nThe applicable method may be too new: running in world age $(ex.world), while current world is $(curworld).")
elseif f isa Function
print(io, "\nThe function `$f` exists, but no method is defined for this combination of argument types.")
elseif f isa Type
print(io, "\nThe type `$f` exists, but no method is defined for this combination of argument types when trying to construct it.")
else
print(io, "\nThe object of type `$(typeof(f))` exists, but no method is defined for this combination of argument types when trying to treat it as a callable object.")
end
if !is_arg_types
# Check for row vectors used where a column vector is intended.
Expand All @@ -342,7 +344,7 @@ function showerror(io::IO, ex::MethodError)
"\nYou can convert to a column vector with the vec() function.")
end
end
Experimental.show_error_hints(io, ex, arg_types_param, kwargs)
Experimental.show_error_hints(io, ex, san_arg_types_param, kwargs)
try
show_method_candidates(io, ex, kwargs)
catch ex
Expand All @@ -354,16 +356,12 @@ end
striptype(::Type{T}) where {T} = T
striptype(::Any) = nothing

function showerror_ambiguous(io::IO, meths, f, args)
function showerror_ambiguous(io::IO, meths, f, args::Type)
@nospecialize f args
print(io, "MethodError: ")
show_signature_function(io, isa(f, Type) ? Type{f} : typeof(f))
print(io, "(")
p = args.parameters
for (i,a) in enumerate(p)
print(io, "::", a)
i < length(p) && print(io, ", ")
end
println(io, ") is ambiguous.\n\nCandidates:")
show_tuple_as_call(io, :var"", args, hasfirst=false)
println(io, " is ambiguous.\n\nCandidates:")
sigfix = Any
for m in meths
print(io, " ")
Expand All @@ -375,7 +373,7 @@ function showerror_ambiguous(io::IO, meths, f, args)
let sigfix=sigfix
if all(m->morespecific(sigfix, m.sig), meths)
print(io, "\nPossible fix, define\n ")
Base.show_tuple_as_call(io, :function, sigfix)
show_tuple_as_call(io, :function, sigfix)
else
print(io, "To resolve the ambiguity, try making one of the methods more specific, or ")
print(io, "adding a new method more specific than any of the existing applicable methods.")
Expand All @@ -401,9 +399,10 @@ stacktrace_contract_userdir()::Bool = Base.get_bool_env("JULIA_STACKTRACE_CONTRA
stacktrace_linebreaks()::Bool = Base.get_bool_env("JULIA_STACKTRACE_LINEBREAKS", false) === true

function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=())
is_arg_types = isa(ex.args, DataType)
is_arg_types = !isa(ex.args, Tuple)
arg_types = is_arg_types ? ex.args : typesof(ex.args...)
arg_types_param = Any[arg_types.parameters...]
arg_types_param = Any[(unwrap_unionall(arg_types)::DataType).parameters...]
arg_types_param = Any[rewrap_unionall(a, arg_types) for a in arg_types_param]
# Displays the closest candidates of the given function by looping over the
# functions methods and counting the number of matching arguments.
f = ex.f
Expand Down
1 change: 1 addition & 0 deletions base/experimental.jl
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ Then if you call `Hinter.only_int` on something that isn't an `Int` (thereby tri
```
julia> Hinter.only_int(1.0)
ERROR: MethodError: no method matching only_int(::Float64)
The function `only_int` exists, but no method is defined for this combination of argument types.
Did you mean to call `any_number`?
Closest candidates are:
...
Expand Down
1 change: 1 addition & 0 deletions base/namedtuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ Base.Pairs{Symbol, Int64, Tuple{Symbol}, @NamedTuple{init::Int64}}
julia> sum("julia"; init=1)
ERROR: MethodError: no method matching +(::Char, ::Char)
The function `+` exists, but no method is defined for this combination of argument types.
Closest candidates are:
+(::Any, ::Any, ::Any, ::Any...)
Expand Down
12 changes: 11 additions & 1 deletion base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2192,7 +2192,17 @@ See also: [`parentmodule`](@ref), [`@which`](@ref Main.InteractiveUtils.@which),
"""
function which(@nospecialize(f), @nospecialize(t))
tt = signature_type(f, t)
return which(tt)
world = get_world_counter()
match, _ = Core.Compiler._findsup(tt, nothing, world)
if match === nothing
me = MethodError(f, t, world)
ee = ErrorException(sprint(io -> begin
println(io, "Calling invoke(f, t, args...) would throw:");
Base.showerror(io, me);
end))
throw(ee)
end
return match.method
end

"""
Expand Down
4 changes: 4 additions & 0 deletions doc/src/manual/constructors.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ Point{Float64}(1.0, 2.5)
julia> Point(1,2.5) ## implicit T ##
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
Point(::T, ::T) where T<:Real at none:2
Expand Down Expand Up @@ -372,6 +374,7 @@ However, other similar calls still don't work:
```jldoctest parametric2
julia> Point(1.5,2)
ERROR: MethodError: no method matching Point(::Float64, ::Int64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
Point(::T, !Matched::T) where T<:Real
Expand Down Expand Up @@ -556,6 +559,7 @@ julia> struct SummedArray{T<:Number,S<:Number}
julia> SummedArray(Int32[1; 2; 3], Int32(6))
ERROR: MethodError: no method matching SummedArray(::Vector{Int32}, ::Int32)
The type `SummedArray` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
SummedArray(::Vector{T}) where T
Expand Down
1 change: 1 addition & 0 deletions doc/src/manual/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@ foo (generic function with 1 method)
julia> foo([1])
ERROR: MethodError: no method matching foo(::Vector{Int64})
The function `foo` exists, but no method is defined for this combination of argument types.
Closest candidates are:
foo(!Matched::Vector{Real})
Expand Down
1 change: 1 addition & 0 deletions doc/src/manual/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ julia> args = [1, 2, 3]
julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)
The function `baz` exists, but no method is defined for this combination of argument types.
Closest candidates are:
baz(::Any, ::Any)
Expand Down
12 changes: 12 additions & 0 deletions doc/src/manual/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Applying it to any other types of arguments will result in a [`MethodError`](@re
```jldoctest fofxy
julia> f(2.0, 3)
ERROR: MethodError: no method matching f(::Float64, ::Int64)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(::Float64, !Matched::Float64)
Expand All @@ -86,6 +87,7 @@ Stacktrace:
julia> f(Float32(2.0), 3.0)
ERROR: MethodError: no method matching f(::Float32, ::Float64)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(!Matched::Float64, ::Float64)
Expand All @@ -96,6 +98,7 @@ Stacktrace:
julia> f(2.0, "3.0")
ERROR: MethodError: no method matching f(::Float64, ::String)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(::Float64, !Matched::Float64)
Expand All @@ -106,6 +109,7 @@ Stacktrace:
julia> f("2.0", "3.0")
ERROR: MethodError: no method matching f(::String, ::String)
The function `f` exists, but no method is defined for this combination of argument types.
```

As you can see, the arguments must be precisely of type [`Float64`](@ref). Other numeric
Expand Down Expand Up @@ -164,6 +168,7 @@ and applying it will still result in a [`MethodError`](@ref):
```jldoctest fofxy
julia> f("foo", 3)
ERROR: MethodError: no method matching f(::String, ::Int64)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(!Matched::Number, ::Number)
Expand All @@ -174,6 +179,7 @@ Stacktrace:
julia> f()
ERROR: MethodError: no method matching f()
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(!Matched::Float64, !Matched::Float64)
Expand Down Expand Up @@ -432,6 +438,7 @@ julia> myappend([1,2,3],4)
julia> myappend([1,2,3],2.5)
ERROR: MethodError: no method matching myappend(::Vector{Int64}, ::Float64)
The function `myappend` exists, but no method is defined for this combination of argument types.
Closest candidates are:
myappend(::Vector{T}, !Matched::T) where T
Expand All @@ -449,6 +456,7 @@ julia> myappend([1.0,2.0,3.0],4.0)
julia> myappend([1.0,2.0,3.0],4)
ERROR: MethodError: no method matching myappend(::Vector{Float64}, ::Int64)
The function `myappend` exists, but no method is defined for this combination of argument types.
Closest candidates are:
myappend(::Vector{T}, !Matched::T) where T
Expand Down Expand Up @@ -494,6 +502,7 @@ true
julia> same_type_numeric("foo", 2.0)
ERROR: MethodError: no method matching same_type_numeric(::String, ::Float64)
The function `same_type_numeric` exists, but no method is defined for this combination of argument types.
Closest candidates are:
same_type_numeric(!Matched::T, ::T) where T<:Number
Expand All @@ -506,6 +515,7 @@ Stacktrace:
julia> same_type_numeric("foo", "bar")
ERROR: MethodError: no method matching same_type_numeric(::String, ::String)
The function `same_type_numeric` exists, but no method is defined for this combination of argument types.
julia> same_type_numeric(Int32(1), Int64(2))
false
Expand Down Expand Up @@ -888,6 +898,7 @@ bar (generic function with 1 method)
julia> bar(1,2,3)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64)
The function `bar` exists, but no method is defined for this combination of argument types.
Closest candidates are:
bar(::Any, ::Any, ::Any, !Matched::Any)
Expand All @@ -901,6 +912,7 @@ julia> bar(1,2,3,4)
julia> bar(1,2,3,4,5)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64, ::Int64, ::Int64)
The function `bar` exists, but no method is defined for this combination of argument types.
Closest candidates are:
bar(::Any, ::Any, ::Any, ::Any)
Expand Down
7 changes: 6 additions & 1 deletion doc/src/manual/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -713,10 +713,12 @@ For the default constructor, exactly one argument must be supplied for each fiel
```jldoctest pointtype
julia> Point{Float64}(1.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64)
The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.
[...]
julia> Point{Float64}(1.0,2.0,3.0)
julia> Point{Float64}(1.0, 2.0, 3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.
[...]
```

Expand Down Expand Up @@ -748,6 +750,7 @@ to `Point` have the same type. When this isn't the case, the constructor will fa
```jldoctest pointtype
julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
Point(::T, !Matched::T) where T
Expand Down Expand Up @@ -1413,6 +1416,8 @@ is raised:
```jldoctest; filter = r"Closest candidates.*"s
julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
The function `supertype` exists, but no method is defined for this combination of argument types.
Closest candidates are:
[...]
```
Expand Down
2 changes: 2 additions & 0 deletions stdlib/Test/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ Error During Test
Test threw an exception of type MethodError
Expression: foo(:cat) == 1
MethodError: no method matching length(::Symbol)
The function `length` exists, but no method is defined for this combination of argument types.
Closest candidates are:
length(::SimpleVector) at essentials.jl:256
length(::Base.MethodList) at reflection.jl:521
Expand Down
2 changes: 1 addition & 1 deletion test/ambiguous.jl
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ let io = IOBuffer()
cf = @eval @cfunction(ambig, Int, (UInt8, Int)) # test for a crash (doesn't throw an error)
@test_throws(MethodError(ambig, (UInt8(1), Int(2)), get_world_counter()),
ccall(cf, Int, (UInt8, Int), 1, 2))
@test_throws(ErrorException("no unique matching method found for the specified argument types"),
@test_throws("Calling invoke(f, t, args...) would throw:\nMethodError: no method matching ambig",
which(ambig, (UInt8, Int)))
@test length(code_typed(ambig, (UInt8, Int))) == 0
end
Expand Down
1 change: 1 addition & 0 deletions test/embedding/embedding-test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ end
close(err.in)
out_task = @async readlines(out)
@test readline(err) == "MethodError: no method matching this_function_has_no_methods()"
@test readline(err) == "The function `this_function_has_no_methods` exists, but no method is defined for this combination of argument types."
@test success(p)
lines = fetch(out_task)
@test length(lines) == 11
Expand Down
Loading

2 comments on commit fc06291

@vtjnash
Copy link
Member

@vtjnash vtjnash commented on fc06291 Feb 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanosoldier runbenchmarks(ALL, isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here.

Please sign in to comment.