Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 110 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,134 @@

TypeProfiler.jl employs Julia's type inference for bug reports.

!!! note
TypeProfiler.jl needs Julia v1.6, especially [this commit](https://github.com/JuliaLang/julia/commit/d5cf73ffffbab40ae06cc1ec99cac9d8e3d2b6a2);
as such I recommend you give a try on this package with Julia built from source after the commit.

```julia
julia> using TypeProfiler

julia> fib(n) = n ≤ 2 ? n : fib(n - 1) + fib(n - 2)
fib (generic function with 1 method)

julia> @profile_call fib(100000) # computation explodes otherwise
No errors !
Int64
Int64

julia> @profile_call fib(100000.)
No errors !
Float64
Float64

julia> @profile_call fib("100000") # report errors
1 errors found
┌ @ none:1 within `fib`
│┌ @ operators.jl:317 <=(x, y) = (x < y) | (x == y)
││┌ @ operators.jl:268 <(x, y) = isless(x, y)
│││ no method matching signature: isless(::String, ::Int64)
1 error found
┌ @ none:1 within `fib(n) in Main at none:1`
│┌ @ operators.jl:326 <=(x, y) = (x < y) | (x == y)
││┌ @ operators.jl:277 <(x, y) = isless(x, y)
│││ no matching method found for signature: isless(::String, ::Int64)
││└
TypeProfiler.Unknown
Union{}

julia> @profile_call sum("julia")
2 errors found
┌ @ reduce.jl:528 sum(a; kw...) = sum(identity, a; kw...)
│┌ @ reduce.jl:528 sum(a; kw...) = sum(identity, a; kw...)
││┌ @ reduce.jl:501 sum(f, a; kw...) = mapreduce(f, add_sum, a; kw...)
│││┌ @ reduce.jl:501 sum(f, a; kw...) = mapreduce(f, add_sum, a; kw...)
││││┌ @ reduce.jl:287 mapreduce(f, op, itr; kw...) = mapfoldl(f, op, itr; kw...)
│││││┌ @ reduce.jl:287 mapreduce(f, op, itr; kw...) = mapfoldl(f, op, itr; kw...)
││││││┌ @ reduce.jl:160 mapfoldl(f, op, itr; init=_InitialValue()) = mapfoldl_impl(f, op, init, itr)
│││││││┌ @ reduce.jl:160 mapfoldl(f, op, itr; init=_InitialValue()) = mapfoldl_impl(f, op, init, itr)
││││││││┌ @ reduce.jl:42 function mapfoldl_impl(f::F, op::OP, nt, itr) where {F,OP}
│││││││││┌ @ reduce.jl:47 function foldl_impl(op::OP, nt, itr) where {OP}
││││││││││┌ @ reduce.jl:53 function _foldl_impl(op::OP, init, itr) where {OP}
│││││││││││┌ @ reduce.jl:81 @inline (op::BottomRF)(acc, x) = op.rf(acc, x)
││││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││││ no matching method found for signature: +(::Char, ::Char)
││││││││││││└
││││││││││┌ @ reduce.jl:354 @inline reduce_empty_iter(op, itr) = reduce_empty_iter(op, itr, IteratorEltype(itr))
│││││││││││┌ @ reduce.jl:355 @inline reduce_empty_iter(op, itr, ::HasEltype) = reduce_empty(op, eltype(itr))
││││││││││││┌ @ reduce.jl:328 reduce_empty(op::BottomRF, ::Type{T}) where {T} = reduce_empty(op.rf, T)
│││││││││││││┌ @ reduce.jl:320 reduce_empty(::typeof(add_sum), ::Type{T}) where {T} = reduce_empty(+, T)
││││││││││││││┌ @ reduce.jl:311 reduce_empty(::typeof(+), ::Type{T}) where {T} = zero(T)
│││││││││││││││ no matching method found for signature: zero(::Type{Char})
││││││││││││││└
Char

julia> ary = Union{Symbol,Int}[:one, 1]

julia> @profile_call sum("julia") # can find errors that would happen in "deeper" calls
3 errors found
┌ @ reduce.jl:500 sum(a) = sum(identity, a)
│┌ @ reduce.jl:483 sum(f, a) = mapreduce(f, add_sum, a)
││┌ @ reduce.jl:280 mapreduce(f, op, itr; kw...) = mapfoldl(f, op, itr; kw...)
│││┌ @ reduce.jl:280 mapreduce(f, op, itr; kw...) = mapfoldl(f, op, itr; kw...)
││││┌ @ reduce.jl:157 mapfoldl(f, op, itr; kw...) = mapfoldl_impl(f, op, kw.data, itr)
│││││┌ @ reduce.jl:157 mapfoldl(f, op, itr; kw...) = mapfoldl_impl(f, op, kw.data, itr)
││││││┌ @ reduce.jl:41 return foldl_impl(op′, nt, itr′)
│││││││┌ @ reduce.jl:45 v = _foldl_impl(op, get(nt, :init, _InitialValue()), itr)
││││││││┌ @ namedtuple.jl:271 get(nt::NamedTuple, key::Union{Integer, Symbol}, default) = haskey(nt, key) ? getfield(nt, key) : default
│││││││││ invalid builtin function call: getfield(::NamedTuple{(),Tuple{}}, ::Symbol)
││││││││└
││││││││┌ @ reduce.jl:59 v = op(v, y[1])
│││││││││┌ @ reduce.jl:78 @inline (op::BottomRF)(acc, x) = op.rf(acc, x)
││││││││││┌ @ reduce.jl:21 add_sum(x, y) = x + y
│││││││││││ no method matching signature: +(::Char, ::Char)
julia> @profile_call sum(ary) # can profile on abstract-typed inputs
14 errors found
┌ @ reducedim.jl:867 @inline ($fname)(a::AbstractArray; dims=:, kw...) = ($_fname)(a, dims; kw...)
│┌ @ reducedim.jl:867 @inline ($fname)(a::AbstractArray; dims=:, kw...) = ($_fname)(a, dims; kw...)
││┌ @ reducedim.jl:871 ($_fname)(a, ::Colon; kw...) = ($_fname)(identity, a, :; kw...)
│││┌ @ reducedim.jl:871 ($_fname)(a, ::Colon; kw...) = ($_fname)(identity, a, :; kw...)
││││┌ @ reducedim.jl:872 ($_fname)(f, a, ::Colon; kw...) = mapreduce(f, $op, a; kw...)
│││││┌ @ reducedim.jl:872 ($_fname)(f, a, ::Colon; kw...) = mapreduce(f, $op, a; kw...)
││││││┌ @ reducedim.jl:310 mapreduce(f, op, A::AbstractArrayOrBroadcasted; dims=:, init=_InitialValue()) =
│││││││┌ @ reducedim.jl:310 mapreduce(f, op, A::AbstractArrayOrBroadcasted; dims=:, init=_InitialValue()) =
││││││││┌ @ reducedim.jl:318 _mapreduce_dim(f, op, ::_InitialValue, A::AbstractArrayOrBroadcasted, ::Colon) =
│││││││││┌ @ reduce.jl:396 function _mapreduce(f, op, ::IndexLinear, A::AbstractArrayOrBroadcasted)
││││││││││┌ @ reduce.jl:351 mapreduce_empty_iter(f, op, itr, ItrEltype) =
│││││││││││┌ @ reduce.jl:355 @inline reduce_empty_iter(op, itr, ::HasEltype) = reduce_empty(op, eltype(itr))
││││││││││││┌ @ reduce.jl:329 reduce_empty(op::MappingRF, ::Type{T}) where {T} = mapreduce_empty(op.f, op.rf, T)
│││││││││││││┌ @ reduce.jl:343 mapreduce_empty(::typeof(identity), op, T) = reduce_empty(op, T)
││││││││││││││┌ @ reduce.jl:320 reduce_empty(::typeof(add_sum), ::Type{T}) where {T} = reduce_empty(+, T)
│││││││││││││││┌ @ reduce.jl:311 reduce_empty(::typeof(+), ::Type{T}) where {T} = zero(T)
││││││││││││││││ no matching method found for signature: zero(::Type{Union{Int64, Symbol}})
│││││││││││││││└
││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││ no matching method found for signature: +(::Symbol, ::Int64)
││││││││││└
│││││││┌ @ reduce.jl:46 v isa _InitialValue && return reduce_empty_iter(op, itr)
││││││││┌ @ reduce.jl:343 @inline reduce_empty_iter(op, itr) = reduce_empty_iter(op, itr, IteratorEltype(itr))
│││││││││┌ @ reduce.jl:344 @inline reduce_empty_iter(op, itr, ::HasEltype) = reduce_empty(op, eltype(itr))
││││││││││┌ @ reduce.jl:317 reduce_empty(op::BottomRF, T) = reduce_empty(op.rf, T)
│││││││││││┌ @ reduce.jl:310 reduce_empty(::typeof(add_sum), T) = reduce_empty(+, T)
││││││││││││┌ @ reduce.jl:303 reduce_empty(::typeof(+), T) = zero(T)
│││││││││││││ no method matching signature: zero(::Type{Char})
││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││ no matching method found for signature: +(::Int64, ::Symbol)
││││││││││└
││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││ no matching method found for signature: +(::Symbol, ::Symbol)
││││││││││└
││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││ no matching method found for signature: +(::Int64, ::Symbol)
││││││││││└
││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││ no matching method found for signature: +(::Int64, ::Symbol)
││││││││││└
││││││││││┌ @ reduce.jl:257 mapreduce_impl(f, op, A::AbstractArrayOrBroadcasted, ifirst::Integer, ilast::Integer) =
│││││││││││┌ @ reduce.jl:233 @noinline function mapreduce_impl(f, op, A::AbstractArrayOrBroadcasted,
││││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││││ no matching method found for signature: +(::Symbol, ::Int64)
││││││││││││└
││││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││││ no matching method found for signature: +(::Int64, ::Symbol)
││││││││││││└
Char
││││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││││ no matching method found for signature: +(::Symbol, ::Symbol)
││││││││││││└
││││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││││ no matching method found for signature: +(::Int64, ::Symbol)
││││││││││││└
││││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││││ no matching method found for signature: +(::Int64, ::Symbol)
││││││││││││└
││││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││││ no matching method found for signature: +(::Symbol, ::Int64)
││││││││││││└
││││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││││ no matching method found for signature: +(::Int64, ::Symbol)
││││││││││││└
││││││││││││┌ @ reduce.jl:24 add_sum(x, y) = x + y
│││││││││││││ no matching method found for signature: +(::Symbol, ::Symbol)
││││││││││││└
Union{Int64, Symbol}
```

### TODOs

in order of priority:

- [x] show profiled results
- [x] escape recursive calls
- [ ] bug fixes: `TypeVar` etc. can serious errors within the current implementation
- [ ] toplevel executions
- handle untyped IRs while splitting expressions as JuliaIntepreter.jl does
- then, TP will be able to profile a file directly like an usual static linter
- [ ] more reports
- [ ] `UndefVarError`
- [x] report `GlobalRef` for really undefined variable
- report `:throw_undef_if_not` ? (includes lots of false positives as is)
- [ ] method ambiguity error
- more and more ...
- [ ] don't trace into "primitive" functions in `Core` and `Base`: with the similar approach to https://github.com/FluxML/Mjolnir.jl/tree/9435d98673752cec4e222e31a6b9f38edcd7d5e0/src/lib
- [ ] balance between Julia's inference approach and error profiling
- setup a type-level virtual machine and enable profiling on toplevel code
- more reports
* invalid built-in function calls
* report some cases of `throw`, e.g. `rand('1')::ArgumentError("Sampler for this object is not defined")`
- balance between Julia's inference approach and error profiling ?
- Julia's type inference allows abstract type (like `Any`) to slip into the inference process by various heuristics, in order to ensure its termination and obtain the performance
- but this is obviously unideal for TP, since our basic stance is _"better safe than sorry"_, meaning ideally we want to find all the possible errors while revealing some uncertainty Julia's inference accepts
- nevertheless, as far as TP relies on the Julia's inference, we need to achieve the conservative error profiling in the existence of abstract types, _somehow_
- as a consequence, TP will be able to profile, e.g.:
- [ ] `print`
- [ ] `sort`
- [ ] support generated functions
- [ ] replace `Core` types: enables profiling things in `Core.Compiler` module

### Ideas

- report performance pitfalls
- somehow profiles possible exceptions ?
- but this is somewhat unideal in the context of bug reports, since the stance would be _"better safe than sorry"_, meaning we ideally want to find all the possible errors while revealing some uncertainty Julia's inference accepts
- maybe we need some additional setup for supporting generated functions
- report performance pitfalls ?
72 changes: 16 additions & 56 deletions src/TypeProfiler.jl
Original file line number Diff line number Diff line change
@@ -1,64 +1,24 @@
@doc read(normpath(dirname(@__DIR__), "README.md"), String)
module TypeProfiler

export
# profile_file, profile_text,
@profile_call

# TODO: using once gets stable
import Core:
SimpleVector, svec,
MethodInstance, CodeInfo, LineInfoNode,
GotoNode, PiNode, PhiNode, PhiCNode, UpsilonNode, SlotNumber
import Core.Compiler:
specialize_method, typeinf_ext,
tmerge,
SSAValue
import Base:
unwrap_unionall, rewrap_unionall
import Base.Meta: isexpr

const to_tt = Base.to_tuple_type

include("types.jl")
include("utils.jl")
include("construct.jl")
include("interpret.jl")
include("builtin.jl")
include("profile.jl")
include("print.jl")
include("profiler/profiler.jl")

# for debugging
# -------------

# this method is basically only for testing
# TODO: keyword args
function prepare_frame(f, args...)
slottyps = Type[typeof′(f), typeof′.(args)...]
tt = to_tt(slottyps)
mms = matching_methods(tt)
@assert (n = length(mms)) === 1 "$(n === 0 ? "no" : "multiple") methods found: $tt"
tt, sparams::SimpleVector, m::Method = mms[1]
mi = specialize_method(m, tt, sparams)
return prepare_frame(mi, slottyps)
end
using Base:
Meta.isexpr

macro profile_call(ex, kwargs...)
@assert isexpr(ex, :call) "function call expression should be given"
f = ex.args[1]
args = ex.args[2:end]
return quote let
maybe_newframe = prepare_frame($(esc(f)), $(map(esc, args)...))
!isa(maybe_newframe, Frame) && return maybe_newframe
frame = maybe_newframe::Frame
try
evaluate_or_profile!(frame)
print_report(frame; $(map(esc, kwargs)...))
catch err
rethrow(err)
finally
global $(esc(:frame)) = frame # HACK: assign the erroneous frame into global `frame` variable for debugging
end
end end
@assert isexpr(ex, :call) "function call expression should be given"
f = ex.args[1]
args = ex.args[2:end]

quote let
interp, frame = Profiler.profile_call($(esc(f)), $(map(esc, args)...))
Profiler.print_reports(interp; $(map(esc, kwargs)...))
frame.result.result
end end
end

export
@profile_call

end
64 changes: 64 additions & 0 deletions src/abandoned/TypeProfiler.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
module TypeProfiler

export
# profile_file, profile_text,
@profile_call

# TODO: using once gets stable
import Core:
SimpleVector, svec,
MethodInstance, CodeInfo, LineInfoNode,
GotoNode, PiNode, PhiNode, PhiCNode, UpsilonNode, SlotNumber
import Core.Compiler:
specialize_method, typeinf_ext,
tmerge,
SSAValue
import Base:
unwrap_unionall, rewrap_unionall
import Base.Meta: isexpr

const to_tt = Base.to_tuple_type

include("types.jl")
include("utils.jl")
include("construct.jl")
include("interpret.jl")
include("builtin.jl")
include("profile.jl")
include("print.jl")

# for debugging
# -------------

# this method is basically only for testing
# TODO: keyword args
function prepare_frame(f, args...)
slottyps = Type[typeof′(f), typeof′.(args)...]
tt = to_tt(slottyps)
mms = matching_methods(tt)
@assert (n = length(mms)) === 1 "$(n === 0 ? "no" : "multiple") methods found: $tt"
tt, sparams::SimpleVector, m::Method = mms[1]
mi = specialize_method(m, tt, sparams)
return prepare_frame(mi, slottyps)
end

macro profile_call(ex, kwargs...)
@assert isexpr(ex, :call) "function call expression should be given"
f = ex.args[1]
args = ex.args[2:end]
return quote let
maybe_newframe = prepare_frame($(esc(f)), $(map(esc, args)...))
!isa(maybe_newframe, Frame) && return maybe_newframe
frame = maybe_newframe::Frame
try
evaluate_or_profile!(frame)
print_report(frame; $(map(esc, kwargs)...))
catch err
rethrow(err)
finally
global $(esc(:frame)) = frame # HACK: assign the erroneous frame into global `frame` variable for debugging
end
end end
end

end
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading