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
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ julia> using TypeProfiler
julia> profile_and_watch_file("demo.jl")

profiling demo.jl ... (finished in 2.745 sec)
═════ 6 possible errors found in demo.jl ═════
═════ 6 possible errors found ═════
┌ @ demo.jl:11 Main.fib(Main.m)
│ variable Main.m is not defined: Main.fib(Main.m)
Expand Down Expand Up @@ -96,7 +96,7 @@ This technique is often called "abstract interpretation" and TP internally uses

Lastly let's apply the following diff to demo.jl so that it works nicely:

> git diff --no-index demo.jl demo-fixed.jl
> git apply fix-demo.diff

```diff
diff --git a/demo.jl b/demo-fixed.jl
Expand Down Expand Up @@ -136,7 +136,7 @@ index d2b188a..1d1b3da 100644
-@inline bar(v::Ty{T}) where {T<:Number} = bar(v.fdl) # typo "fdl"
+@inline bar(v::Ty{T}) where {T<:Number} = bar(v.fld) # typo fixed
@inline bar(v::Ty) = bar(convert(Number, v.fld))

foo(1.2)
-foo("1") # `String` can't be converted to `Number`
+foo('1') # `Char` will be converted to `UInt32`
Expand All @@ -145,16 +145,14 @@ index d2b188a..1d1b3da 100644
If you save the file, TP will automatically trigger profiling, and this time, won't complain anything:

```julia
profiling demo.jl ... (finished in 1.913 sec)
profiling demo.jl ... (finished in 5.146 sec)
No errors !
```


### TODOs

- profiling on a package (without actual loading)
* support `module`
* special case `include` calls
- support `module` expressions, profiling on a package (without actual loading)
- more reports
* more correct error reports in general
* report some cases of `throw`, e.g. `rand('1')::ArgumentError("Sampler for this object is not defined")`
Expand Down
44 changes: 0 additions & 44 deletions demo-fixed.jl

This file was deleted.

41 changes: 41 additions & 0 deletions fix-demo.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
diff --git a/demo.jl b/demo-fixed.jl
index d2b188a..1d1b3da 100644
--- a/demo.jl
+++ b/demo.jl
@@ -5,11 +5,21 @@
# fibonacci
# ---------

-fib(n) = n ≤ 2 ? n : fib(n-1) + fib(n-2)
+# cache, cache, cache
+function fib(n::T) where {T<:Number}
+ cache = Dict(zero(T)=>zero(T), one(T)=>one(T))
+ return _fib(n, cache)
+end
+_fib(n, cache) = if haskey(cache, n)
+ cache[n]
+else
+ cache[n] = _fib(n-1, cache) + _fib(n-2, cache)
+end

-fib(1000) # never terminates in ordinal execution
-fib(m) # undef var
-fib("1000") # obvious type error
+fib(BigInt(1000)) # will terminate in ordinal execution as well
+m = 1000 # define m
+fib(m)
+fib(parse(Int, "1000"))


# language features
@@ -27,8 +37,8 @@ end

# macros will be expanded
@inline bar(n::T) where {T<:Number} = n < 0 ? zero(T) : one(T)
-@inline bar(v::Ty{T}) where {T<:Number} = bar(v.fdl) # typo "fdl"
+@inline bar(v::Ty{T}) where {T<:Number} = bar(v.fld) # typo fixed
@inline bar(v::Ty) = bar(convert(Number, v.fld))

foo(1.2)
-foo("1") # `String` can't be converted to `Number`
+foo('1') # `Char` will be converted to `UInt32`
3 changes: 3 additions & 0 deletions src/TypeProfiler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import Base:
import Base.Meta:
isexpr, _parse_string

import Base.Iterators:
flatten

using FileWatching, Requires

# includes
Expand Down
49 changes: 37 additions & 12 deletions src/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ function abstract_eval_special_value(interp::TPInterpreter, @nospecialize(e), vt
# add_remark!(interp, sv, UndefVarErrorReport(sv, sv.mod, s))
# end
elseif isa(e, GlobalRef)
check_global_ref!(interp, sv, e.mod, e.name) && (ret = Bottom) # ret here should annotated as `Any` by `NativeInterpreter`, but here I would like to be more conservative and change it to `Bottom`
vgv = getvirtualglobalvar(interp, e.mod, e.name)
if isnothing(vgv)
check_global_ref!(interp, sv, e.mod, e.name) && (ret = Bottom) # ret here should annotated as `Any` by `NativeInterpreter`, but here I would like to be more conservative and change it to `Bottom`
else
ret = vgv
end
end

return ret
Expand Down Expand Up @@ -97,17 +102,33 @@ end
# return ret
# end

# FIXME:
# this is such an super fragile and problematic copy-and-paste;
# the whole point is to not let abstract interpretation to halt even after there is an
# obvious error point found (, which is usually represented by `Bottom`-annotated type and
# triggers early-loop-escape in the native implementation), and allow TP to collect as much
# errors as possible; so the only diffs are:
# - the first argument changed to `TPInterpreter`
# - comment out 3 early-loop-escapes
#
# ... and actually this introduces one obvious bug that `typeinf_local(::TPInterpreter, ::InferenceState)`
# sometimes runs the same inference routine twice, which leads to duplicated error reports
"""
typeinf_local(::TPInterpreter, ::InferenceState)

works as `typeinf_local(::AbstractInterpreter, ::InferenceState)` (i.e. "make as much
progress on `frame` as possible (without handling cycles)") but with few of tweaks for the
TypeProfiler.jl's abstract interpretation.

this is such an super fragile and problematic copy-and-paste; the points are:
1. to not let abstract interpretation to halt even after there is an obvious error point
found (, which is usually represented by `Bottom`-annotated type and triggers
early-loop-escape in the native implementation), and allow TP to collect as much errors
as possible
2. to keep traced types of (virtual) global variables in toplevel frames (, which are
technically wrapped into a single virtual function, but originally globals) in the
`TPInterpreter.virtualglobalvartable` so that they can be referred across profiling on
different virtual functions

so the actual diffs are:
- the first argument changed to `TPInterpreter`
- comment out 3 early-loop-escapes
- do "virtual global variable assignment" for toplevel frames

!!! danger "FIXME:"
... and actually this introduces one obvious bug that `typeinf_local(::TPInterpreter, ::InferenceState)`
sometimes runs the same inference routine twice, which leads to duplicated error reports
"""
function typeinf_local(::TPInterpreter, ::InferenceState) end

push_inithook!() do
Core.eval(Core.Compiler, quote
Expand Down Expand Up @@ -231,6 +252,10 @@ function typeinf_local(interp::$(TPInterpreter), frame::InferenceState)
lhs = stmt.args[1]
if isa(lhs, Slot)
changes = StateUpdate(lhs, VarState(t, false), changes)
# virtual global variable assignment
if $(istoplevelframe)(frame)
$(setvirtualglobalvar!)(interp, frame, lhs, t)
end
end
elseif hd === :method
fname = stmt.args[1]
Expand Down
29 changes: 26 additions & 3 deletions src/abstractinterpreterinterface.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# define `AbstractInterpreter` API
# `AbstractInterpreter` API
# -------------------------

struct TPInterpreter <: AbstractInterpreter
native::NativeInterpreter
Expand All @@ -7,15 +8,21 @@ struct TPInterpreter <: AbstractInterpreter
compress::Bool
discard_trees::Bool

# TypeProfiler.jl specific
istoplevel::Bool
virtualglobalvartable::Dict{Module,Dict{Symbol,Any}} # maybe we don't need this nested dicts

function TPInterpreter(world::UInt = get_world_counter();
inf_params::InferenceParams = InferenceParams(),
opt_params::OptimizationParams = OptimizationParams(),
optimize::Bool = false,
compress::Bool = false,
discard_trees::Bool = false
discard_trees::Bool = false,
istoplevel::Bool = false,
virtualglobalvartable::AbstractDict = Dict()
)
native = NativeInterpreter(world; inf_params, opt_params)
return new(native, [], optimize, compress, discard_trees)
return new(native, [], optimize, compress, discard_trees, istoplevel, virtualglobalvartable)
end
end

Expand All @@ -41,3 +48,19 @@ end
may_optimize(interp::TPInterpreter) = interp.optimize
may_compress(interp::TPInterpreter) = interp.compress
may_discard_trees(interp::TPInterpreter) = interp.discard_trees

# TypeProfiler.jl specific
# ------------------------

istoplevel(interp::TPInterpreter) = interp.istoplevel
function getvirtualglobalvar(interp, mod, sym)
haskey(interp.virtualglobalvartable, mod) || return nothing
return get(interp.virtualglobalvartable[mod], sym, nothing)
end
function setvirtualglobalvar!(interp, frame, lhs::Slot, @nospecialize(rhs))
mod = frame.mod
haskey(interp.virtualglobalvartable, mod) || (interp.virtualglobalvartable[mod] = Dict())

sym = frame.src.slotnames[lhs.id]
interp.virtualglobalvartable[mod][sym] = rhs
end
18 changes: 11 additions & 7 deletions src/print.jl
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ end
# --------

function print_reports(io::IO,
filename::AbstractString,
reports::Vector{<:ToplevelErrorReport},
@nospecialize(postprocess = identity);
print_toplevel_sucess::Bool = false,
Expand All @@ -68,8 +67,7 @@ function print_reports(io::IO,
color = ERROR_COLOR

s = with_bufferring(arg) do io
s = string(pluralize(length(reports), "toplevel error"), " found in ",
(fullpath ? tofullpath : identity)(filename))
s = string(pluralize(length(reports), "toplevel error"), " found")
printlnstyled(io, LEFT_ROOF, s, RIGHT_ROOF; color = HEADER_COLOR)

rail = with_bufferring(arg) do io
Expand Down Expand Up @@ -104,7 +102,6 @@ print_report(io, report::ActualErrorWrapped) = showerror(io, report.err, report.
# ---------

function print_reports(io::IO,
filename::AbstractString,
reports::Vector{<:InferenceErrorReport},
@nospecialize(postprocess = identity);
filter_native_remarks::Bool = true,
Expand All @@ -126,8 +123,7 @@ function print_reports(io::IO,
end

s = with_bufferring(:color => color) do io
s = string(pluralize(length(reports), "possible error"), " found in ",
(fullpath ? tofullpath : identity)(filename))
s = string(pluralize(length(reports), "possible error"), " found")
printlnstyled(io, LEFT_ROOF, s, RIGHT_ROOF; color = HEADER_COLOR)

wrote_linfos = Set{UInt64}()
Expand All @@ -146,7 +142,15 @@ end
# `typeinf_local` in src/abstractinterpretation.jl
function uniquify_reports!(reports::Vector{<:InferenceErrorReport})
return unique!(reports) do report
return last(report.st), report.msg, report.sig # uniquify keys
# uniquify keys
return (
# caller
first(report.st),
# error
last(report.st),
report.msg,
report.sig
)
end
end

Expand Down
Loading