diff --git a/docs/src/dev_reference.md b/docs/src/dev_reference.md index e57b91fa..5f1e8171 100644 --- a/docs/src/dev_reference.md +++ b/docs/src/dev_reference.md @@ -63,8 +63,10 @@ breakpoint enable disable remove +toggle break_on break_off +breakpoints JuliaInterpreter.dummy_breakpoint ``` @@ -77,6 +79,9 @@ JuliaInterpreter.FrameData JuliaInterpreter.FrameInstance JuliaInterpreter.BreakpointState JuliaInterpreter.BreakpointRef +JuliaInterpreter.AbstractBreakpoint +JuliaInterpreter.BreakpointSignature +JuliaInterpreter.BreakpointFileLocation ``` ## Internal storage diff --git a/docs/src/index.md b/docs/src/index.md index 7671e77e..203ce6a0 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -29,7 +29,7 @@ julia> @interpret sum(list) You can interrupt execution by setting breakpoints. You can set breakpoints via packages that explicitly target debugging, -like [Juno](http://junolab.org/), [Debugger](https://github.com/JuliaDebug/Debugger.jl), and +like [Juno](https://junolab.org/), [Debugger](https://github.com/JuliaDebug/Debugger.jl), and [Rebugger](https://github.com/timholy/Rebugger.jl). But all of these just leverage the core functionality defined in JuliaInterpreter, so here we'll illustrate it without using any of these other packages. @@ -38,8 +38,7 @@ Let's set a conditional breakpoint, to be triggered any time one of the elements argument to `sum` is bigger than 4: ```jldoctest demo1; filter = r"in Base at .*$" -julia> @breakpoint sum([1, 2]) any(x->x>4, a) -breakpoint(sum(a::AbstractArray) in Base at reducedim.jl:648, line 648) +julia> bp = @breakpoint sum([1, 2]) any(x->x>4, a); ``` Note that in writing the condition, we used `a`, the name of the argument to the relevant @@ -53,7 +52,7 @@ Now let's see what happens: julia> @interpret sum([1,2,3]) # no element bigger than 4, breakpoint should not trigger 6 -julia> frame, bp = @interpret sum([1,2,5]) # should trigger breakpoint +julia> frame, bpref = @interpret sum([1,2,5]) # should trigger breakpoint (Frame for sum(a::AbstractArray) in Base at reducedim.jl:648 c 1* 648 1 ─ nothing 2 648 │ %2 = (Base.#sum#550)(Colon(), #self#, a) @@ -63,18 +62,16 @@ a = [1, 2, 5], breakpoint(sum(a::AbstractArray) in Base at reducedim.jl:648, lin `frame` is described in more detail on the next page; for now, suffice it to say that the `c` in the leftmost column indicates the presence of a conditional breakpoint -upon entry to `sum`. `bp` is a reference to the breakpoint. You can manipulate these -at the command line: +upon entry to `sum`. `bpref` is a reference to the breakpoint of type [`BreakpointRef`](@ref). +The breakpoint `bp` we created can be manipulated at the command line ```jldoctest demo1; filter = r"in Base at .*$" julia> disable(bp) -false julia> @interpret sum([1,2,5]) 8 julia> enable(bp) -true julia> @interpret sum([1,2,5]) (Frame for sum(a::AbstractArray) in Base at reducedim.jl:648 diff --git a/src/JuliaInterpreter.jl b/src/JuliaInterpreter.jl index 02c83280..d0de6f51 100644 --- a/src/JuliaInterpreter.jl +++ b/src/JuliaInterpreter.jl @@ -14,7 +14,7 @@ using InteractiveUtils using CodeTracking export @interpret, Compiled, Frame, root, leaf, - BreakpointRef, breakpoint, @breakpoint, breakpoints, enable, disable, remove, + BreakpointRef, breakpoint, @breakpoint, breakpoints, enable, disable, remove, toggle, debug_command, @bp, break_on, break_off module CompiledCalls @@ -39,6 +39,9 @@ include("commands.jl") include("breakpoints.jl") function set_compiled_methods() + ########### + # Methods # + ########### # Work around #28 by preventing interpretation of all Base methods that have a ccall to memcpy push!(compiled_methods, which(vcat, (Vector,))) push!(compiled_methods, first(methods(Base._getindex_ra))) @@ -69,6 +72,7 @@ function set_compiled_methods() # These are currently extremely slow to interpret (https://github.com/JuliaDebug/JuliaInterpreter.jl/issues/193) push!(compiled_methods, which(subtypes, Tuple{Module, Type})) push!(compiled_methods, which(subtypes, Tuple{Type})) + push!(compiled_methods, which(match, Tuple{Regex, String, Int, UInt32})) # Anything that ccalls jl_typeinf_begin cannot currently be handled for finf in (Core.Compiler.typeinf_code, Core.Compiler.typeinf_ext, Core.Compiler.typeinf_type) @@ -77,6 +81,9 @@ function set_compiled_methods() end end + ########### + # Modules # + ########### push!(compiled_modules, Base.Threads) end diff --git a/src/breakpoints.jl b/src/breakpoints.jl index 33268ff0..91cc8e2c 100644 --- a/src/breakpoints.jl +++ b/src/breakpoints.jl @@ -1,26 +1,113 @@ -const _breakpoints = BreakpointRef[] -breakpoints() = copy(_breakpoints) +const _breakpoints = AbstractBreakpoint[] -Base.getindex(bp::BreakpointRef) = bp.framecode.breakpoints[bp.stmtidx] -function Base.setindex!(bp::BreakpointRef, isactive::Bool) - bp.framecode.breakpoints[bp.stmtidx] = BreakpointState(isactive, bp[].condition) +""" + breakpoints()::Vector{AbstractBreakpoint} + +Return an array with all breakpoints. +""" +breakpoints() = _breakpoints + +function add_to_existing_framecodes(bp::AbstractBreakpoint) + for framecode in values(framedict) + add_breakpoint_if_match!(framecode, bp) + end end -function toggle!(bp::BreakpointRef) - state = bp[] - bp.framecode.breakpoints[bp.stmtidx] = BreakpointState(!state.isactive, state.condition) + +function add_breakpoint_if_match!(framecode::FrameCode, bp::AbstractBreakpoint) + if framecode_matches_breakpoint(framecode, bp) + stmtidx = bp.line === 0 ? 1 : statementnumber(framecode, bp.line) + breakpoint!(framecode, stmtidx, bp.condition, bp.enabled[]) + push!(bp.instances, BreakpointRef(framecode, stmtidx)) + end end -function add_breakpoint(framecode, stmtidx) - bp = BreakpointRef(framecode, stmtidx) - # Since there can be only one BreakpointState for a given framecode/stmtidx, - # check whether _breakpoints is already storing a reference to that location - idx = findfirst(isequal(bp), _breakpoints) - if idx === nothing - push!(_breakpoints, bp) +function framecode_matches_breakpoint(framecode::FrameCode, bp::BreakpointSignature) + function extract_function_from_method(m::Method) + sig = Base.unwrap_unionall(m.sig) + ft0 = sig.parameters[1] + ft = Base.unwrap_unionall(ft0) + if ft <: Function && isa(ft, DataType) && isdefined(ft, :instance) + return ft.instance + elseif isa(ft, DataType) && ft.name === Type.body.name + return ft.parameters[1] + else + return ft + end end + + framecode.scope isa Method || return false + meth = framecode.scope + bp.f isa Method && return meth === bp.f + bp.f === extract_function_from_method(meth) || return false + bp.sig === nothing && return true + return bp.sig <: meth.sig +end + +""" + breakpoint(f, [sig], [line], [condition]) + +Add a breakpoint to `f` with the specified argument types `sig`.¨ +If `sig` is not given, the breakpoint will apply to all methods of `f`. +If `f` is a method, the breakpoint will only apply to that method. +Optionally specify an absolute line number `line` in the source file; the default +is to break upon entry at the first line of the body. +Without `condition`, the breakpoint will be triggered every time it is encountered; +the second only if `condition` evaluates to `true`. +`condition` should be written in terms of the arguments and local variables of `f`. + +# Example +```julia +function radius2(x, y) + return x^2 + y^2 +end + +breakpoint(radius2, Tuple{Int,Int}, :(y > x)) +``` +""" +function breakpoint(f::Union{Method, Function}, sig=nothing, line::Integer=0, condition::Condition=nothing) + sig !== nothing && (sig = Base.to_tuple_type(sig)) + bp = BreakpointSignature(f, sig, line, condition, Ref(true), BreakpointRef[]) + add_to_existing_framecodes(bp) + idx = findfirst(bp2 -> same_location(bp, bp2), _breakpoints) + idx === nothing ? push!(_breakpoints, bp) : (_breakpoints[idx] = bp) + return bp +end +breakpoint(f::Union{Method, Function}, sig, condition::Condition) = breakpoint(f, sig, 0, condition) +breakpoint(f::Union{Method, Function}, line::Integer, condition::Condition=nothing) = breakpoint(f, nothing, line, condition) +breakpoint(f::Union{Method, Function}, condition::Condition) = breakpoint(f, nothing, 0, condition) + + +""" + breakpoint(file, line, [condition]) + +Set a breakpoint in `file` at `line`. The argument `file` can be a filename, a partial path or absolute path. +For example, `file = foo.jl` will match against all files with the name `foo.jl`, +`file = src/foo.jl` will match against all paths containing `src/foo.jl`, e.g. both `Foo/src/foo.jl` and `Bar/src/foo.jl`. +Absolute paths only matches against the file with that exact absolute path. +""" +function breakpoint(file::AbstractString, line::Integer, condition::Condition=nothing) + file = normpath(file) + apath = CodeTracking.maybe_fix_path(abspath(file)) + ispath(apath) && (apath = realpath(apath)) + bp = BreakpointFileLocation(file, apath, line, condition, Ref(true), BreakpointRef[]) + add_to_existing_framecodes(bp) + idx = findfirst(bp2 -> same_location(bp, bp2), _breakpoints) + idx === nothing ? push!(_breakpoints, bp) : (_breakpoints[idx] = bp) return bp end +function framecode_matches_breakpoint(framecode::FrameCode, bp::BreakpointFileLocation) + framecode.scope isa Method || return false + meth = framecode.scope + methpath = CodeTracking.maybe_fix_path(String(meth.file)) + ispath(methpath) && (methpath = realpath(methpath)) + if bp.abspath == methpath || endswith(methpath, bp.path) + return method_contains_line(meth, bp.line) + else + return false + end +end + function shouldbreak(frame::Frame, pc::Int) bps = frame.framecode.breakpoints isassigned(bps, pc) || return false @@ -58,63 +145,81 @@ function prepare_slotfunction(framecode::FrameCode, body::Union{Symbol,Expr}) return Expr(:function, Expr(:call, funcname, framename), Expr(:block, assignments..., body)) end -const Condition = Union{Nothing,Expr,Tuple{Module,Expr}} _unpack(condition) = isa(condition, Expr) ? (Main, condition) : condition ## The fundamental implementations of breakpoint-setting -function breakpoint!(framecode::FrameCode, pc, condition::Condition=nothing) +function breakpoint!(framecode::FrameCode, pc, condition::Condition=nothing, enabled=true) stmtidx = pc if condition === nothing - framecode.breakpoints[stmtidx] = BreakpointState() + framecode.breakpoints[stmtidx] = BreakpointState(enabled) else mod, cond = _unpack(condition) fex = prepare_slotfunction(framecode, cond) - framecode.breakpoints[stmtidx] = BreakpointState(true, Core.eval(mod, fex)) + framecode.breakpoints[stmtidx] = BreakpointState(enabled, Core.eval(mod, fex)) end - return add_breakpoint(framecode, stmtidx) end breakpoint!(frame::Frame, pc=frame.pc, condition::Condition=nothing) = breakpoint!(frame.framecode, pc, condition) +update_states!(bp::AbstractBreakpoint) = foreach(bpref -> update_state!(bpref, bp.enabled[]), bp.instances) +update_state!(bp::BreakpointRef, v::Bool) = bp[] = v + """ - enable(bp::BreakpointRef) + enable(bp::AbstractBreakpoint) Enable breakpoint `bp`. """ -enable(bp::BreakpointRef) = bp[] = true +enable(bp::AbstractBreakpoint) = (bp.enabled[] = true; update_states!(bp)) +enable(bp::BreakpointRef) = bp[] = true + """ - disable(bp::BreakpointRef) + disable(bp::AbstractBreakpoint) Disable breakpoint `bp`. Disabled breakpoints can be re-enabled with [`enable`](@ref). """ +disable(bp::AbstractBreakpoint) = (bp.enabled[] = false; update_states!(bp)) disable(bp::BreakpointRef) = bp[] = false """ - remove(bp::BreakpointRef) + remove(bp::AbstractBreakpoint) Remove (delete) breakpoint `bp`. Removed breakpoints cannot be re-enabled. """ -function remove(bp::BreakpointRef) +function remove(bp::AbstractBreakpoint) idx = findfirst(isequal(bp), _breakpoints) - deleteat!(_breakpoints, idx) + idx === nothing || deleteat!(_breakpoints, idx) + foreach(remove, bp.instances) +end +function remove(bp::BreakpointRef) bp.framecode.breakpoints[bp.stmtidx] = BreakpointState(false, falsecondition) return nothing end +""" + toggle(bp::AbstractBreakpoint) + +Toggle breakpoint `bp`. +""" +toggle(bp::AbstractBreakpoint) = (bp.enabled[] = !bp.enabled[]; update_states!(bp)) +function toggle(bp::BreakpointRef) + state = bp[] + bp.framecode.breakpoints[bp.stmtidx] = BreakpointState(!state.isactive, state.condition) +end + """ enable() Enable all breakpoints. """ -enable() = for bp in _breakpoints enable(bp) end +enable() = foreach(enable, _breakpoints) """ disable() Disable all breakpoints. """ -disable() = for bp in _breakpoints disable(bp) end +disable() = foreach(disable, _breakpoints) """ remove() @@ -123,10 +228,9 @@ Remove all breakpoints. """ function remove() for bp in _breakpoints - bp.framecode.breakpoints[bp.stmtidx] = BreakpointState(false, falsecondition) + foreach(remove, bp.instances) end empty!(_breakpoints) - return nothing end """ @@ -168,106 +272,6 @@ function break_off(states::Vararg{Symbol}) end end -""" - breakpoint(f, sig) - breakpoint(f, sig, line) - breakpoint(f, sig, condition) - breakpoint(f, sig, line, condition) - breakpoint(...; enter_generated=false) - -Add a breakpoint to `f` with the specified argument types `sig`. -Optionally specify an absolute line number `line` in the source file; the default -is to break upon entry at the first line of the body. -Without `condition`, the breakpoint will be triggered every time it is encountered; -the second only if `condition` evaluates to `true`. -`condition` should be written in terms of the arguments and local variables of `f`. - -# Example -```julia -function radius2(x, y) - return x^2 + y^2 -end - -breakpoint(radius2, Tuple{Int,Int}, :(y > x)) -``` -""" -function breakpoint(f, sig::Type, line::Integer, condition::Condition=nothing; enter_generated=false) - method = which(f, sig) - framecode, _ = prepare_framecode(method, sig; enter_generated=enter_generated) - # Don't use statementnumber(method, line) in case it's enter_generated - linec = line - whereis(method)[2] + method.line - stmtidx = statementnumber(framecode, linec) - breakpoint!(framecode, stmtidx, condition) -end -function breakpoint(f, sig::Type, condition::Condition=nothing; enter_generated=false) - method = which(f, sig) - framecode, _ = prepare_framecode(method, sig; enter_generated=enter_generated) - breakpoint!(framecode, 1, condition) -end - -""" - breakpoint(method::Method) - breakpoint(method::Method, line) - breakpoint(method::Method, condition::Expr) - breakpoint(method::Method, line, condition::Expr) - -Add a breakpoint to `method`. -""" -function breakpoint(method::Method, line::Integer, condition::Condition=nothing) - framecode, stmtidx = statementnumber(method, line) - breakpoint!(framecode, stmtidx, condition) -end -function breakpoint(method::Method, condition::Condition=nothing) - framecode = get_framecode(method) - breakpoint!(framecode, 1, condition) -end - -""" - breakpoint(f) - breakpoint(f, condition) - -Break-on-entry to all methods of `f`. -""" -function breakpoint(f, condition::Condition=nothing) - bps = BreakpointRef[] - for method in methods(f) - push!(bps, breakpoint(method, condition)) - end - return bps -end - -""" - breakpoint(filename, line) - breakpoint(filename, line, condition) - -Set a breakpoint at the specified file and line number. -""" -function breakpoint(filename::AbstractString, line::Integer, args...) - local sigs - try - sigs = signatures_at(filename, line) - catch - sigs = nothing - end - if sigs === nothing - # TODO: build a Revise-free fallback. Note this won't work well for methods with keywords. - error("no signatures found at $filename, $line.\nRestarting and `using Revise` and the relevant package may fix this problem.") - end - for sig in sigs - method = JuliaInterpreter.whichtt(sig) - method === nothing && continue - # Check to see if this method really contains that line. Methods that fill in a default positional argument, - # keyword arguments, and @generated sections may not contain the line. - _, line1 = whereis(method) - offset = line1 - method.line - src = JuliaInterpreter.get_source(method) - lastline = src.linetable[end] - if getline(lastline) + offset >= line - return breakpoint(method, line, args...) - end - end - error("no signatures found at $filename, $line among the signatures $sigs") -end """ @breakpoint f(args...) condition=nothing diff --git a/src/construct.jl b/src/construct.jl index c524d2bd..e2fb84ed 100644 --- a/src/construct.jl +++ b/src/construct.jl @@ -34,6 +34,15 @@ const debug_recycle = Base.RefValue(false) push!(junk, frame.framedata) end +function clear_caches() + empty!(junk) + empty!(framedict) + empty!(genframedict) + for bp in breakpoints() + empty!(bp.instances) + end +end + const empty_svec = Core.svec() function namedtuple(kwargs) @@ -527,7 +536,7 @@ T = Float64 See [`enter_call`](@ref) for a similar approach not based on expressions. """ function enter_call_expr(expr; enter_generated = false) - empty!(junk) + clear_caches() r = determine_method_for_expr(expr; enter_generated = enter_generated) if isa(r, Tuple) return prepare_frame(r[1:end-1]...) @@ -569,7 +578,7 @@ would be created by the generator. See [`enter_call_expr`](@ref) for a similar approach based on expressions. """ function enter_call(@nospecialize(finfo), @nospecialize(args...); kwargs...) - empty!(junk) + clear_caches() if isa(finfo, Tuple) f = finfo[1] enter_generated = finfo[2]::Bool diff --git a/src/types.jl b/src/types.jl index 5c8f4dba..c073f384 100644 --- a/src/types.jl +++ b/src/types.jl @@ -58,11 +58,11 @@ end One `FrameCode` can be shared by many calling `Frame`s. Important fields: -- `scope`: the `Method` or `Module` in which this frame is to be evaluated -- `src`: the `CodeInfo` object storing (optimized) lowered source code +- `scope`: the `Method` or `Module` in which this frame is to be evaluated. +- `src`: the `CodeInfo` object storing (optimized) lowered source code. - `methodtables`: a vector, each entry potentially stores a "local method table" for the corresponding `:call` expression in `src` (undefined entries correspond to statements that do not - contain `:call` expressions) + contain `:call` expressions). - `used`: a `BitSet` storing the list of SSAValues that get referenced by later statements. """ struct FrameCode @@ -90,7 +90,21 @@ function FrameCode(scope, src::CodeInfo; generator=false, optimize=true) end end used = find_used(src) - return FrameCode(scope, src, methodtables, breakpoints, used, generator) + framecode = FrameCode(scope, src, methodtables, breakpoints, used, generator) + if scope isa Method + for bp in _breakpoints + # Manual union splitting + if bp isa BreakpointSignature + add_breakpoint_if_match!(framecode, bp) + elseif bp isa BreakpointFileLocation + add_breakpoint_if_match!(framecode, bp) + else + error("unhandled breakpoint type") + end + end + end + + return framecode end nstatements(framecode::FrameCode) = length(framecode.src.code) @@ -101,8 +115,8 @@ Base.show(io::IO, framecode::FrameCode) = print_framecode(io, framecode) `FrameInstance` represents a method specialized for particular argument types. Fields: -- `framecode`: the [`FrameCode`](@ref) for the method -- `sparam_vals`: the static parameter values for the method +- `framecode`: the [`FrameCode`](@ref) for the method. +- `sparam_vals`: the static parameter values for the method. """ struct FrameInstance framecode::FrameCode @@ -123,7 +137,7 @@ Important fields: to extract the current value of local variables. - `ssavalues`: a vector containing the [Static Single Assignment](https://en.wikipedia.org/wiki/Static_single_assignment_form) - values produced at the current state of execution + values produced at the current state of execution. - `sparams`: the static type parameters, e.g., for `f(x::Vector{T}) where T` this would store the value of `T` given the particular input `x`. - `exception_frames`: a list of indexes to `catch` blocks for handling exceptions within @@ -147,11 +161,11 @@ end """ `Frame` represents the current execution state in a particular call frame. Fields: -- `framecode`: the [`FrameCode`] for this frame -- `framedata`: the [`FrameData`] for this frame -- `pc`: the program counter (integer index of the next statment to be evaluated) for this frame -- `caller`: the parent caller of this frame, or `nothing` -- `callee`: the frame called by this one, or `nothing` +- `framecode`: the [`FrameCode`] for this frame. +- `framedata`: the [`FrameData`] for this frame. +- `pc`: the program counter (integer index of the next statment to be evaluated) for this frame. +- `caller`: the parent caller of this frame, or `nothing`. +- `callee`: the frame called by this one, or `nothing`. The `Base` functions `show_backtrace` and `display_error` are overloaded such that `show_backtrace(io::IO, frame::Frame)` and `display_error(io::IO, er, frame::Frame)` @@ -215,9 +229,9 @@ By calling the function `locals`[@ref] on a `Frame`[@ref] a `Vector` of `Variable`'s is returned. Important fields: -- `value::Any`: the value of the local variable -- `name::Symbol`: the name of the variable as given in the source code -- `isparam::Bool`: if the variable is a type parameter, for example `T` in `f(x::T) where {T} = x` . +- `value::Any`: the value of the local variable. +- `name::Symbol`: the name of the variable as given in the source code. +- `isparam::Bool`: if the variable is a type parameter, for example `T` in `f(x::T) where {T} = x`. """ struct Variable value::Any @@ -248,6 +262,9 @@ struct BreakpointRef err end BreakpointRef(framecode, stmtidx) = BreakpointRef(framecode, stmtidx, nothing) +Base.getindex(bp::BreakpointRef) = bp.framecode.breakpoints[bp.stmtidx] +Base.setindex!(bp::BreakpointRef, isactive::Bool) = + bp.framecode.breakpoints[bp.stmtidx] = BreakpointState(isactive, bp[].condition) function Base.show(io::IO, bp::BreakpointRef) if checkbounds(Bool, bp.framecode.breakpoints, bp.stmtidx) @@ -261,3 +278,104 @@ function Base.show(io::IO, bp::BreakpointRef) end print(io, ')') end + +# Possible types for breakpoint condition +const Condition = Union{Nothing,Expr,Tuple{Module,Expr}} + +""" +`AbstractBreakpoint` is the abstract type that is the supertype for breakpoints. Currently, +the concrete breakpoint types [`BreakpointSignature`](@ref) and [`BreakpointFileLocation`](@ref) +exist. + +Common fields shared by the concrete breakpoints: + +- `condition::Union{Nothing,Expr,Tuple{Module,Expr}}`: the condition when the breakpoint applies . + `nothing` means unconditionally, otherwise when the `Expr` (optionally in `Module`). +- `enabled::Ref{Bool}`: If the breakpoint is enabled (should not be directly modified, use [`enable()`](@ref) or [`disable()`](@ref)). +- `instances::Vector{BreakpointRef}`: All the [`BreakpointRef`](@ref) that the breakpoint has applied to. +- `line::Int` The line of the breakpoint (equal to 0 if unset). + +See [`BreakpointSignature`](@ref) and [`BreakpointFileLocation`](@ref) for additional fields in the concrete types. +""" +abstract type AbstractBreakpoint end + +same_location(::AbstractBreakpoint, ::AbstractBreakpoint) = false + +function print_bp_condition(io::IO, cond::Condition) + if cond !== nothing + if isa(cond, Tuple{Module, Expr}) && (expr = expr[2]) + cond = (cond[1], Base.remove_linenums!(copy(cond[2]))) + elseif isa(cond, Expr) + cond = Base.remove_linenums!(copy(cond)) + end + print(io, " ", cond) + end +end + +""" +A `BreakpointSignature` is a breakpoint that is set on methods or functions. + +Fields: + +- `f::Union{Method, Function}`: A method or function that the breakpoint should apply to. +- `sig::Union{Nothing, Type}`: if `f` is a `Method`, always equal to `nothing`. Otherwise, contains the method signature + as a tuple type for what methods the breakpoint should apply to. + +For common fields shared by all breakpoints, see [`AbstractBreakpoint`](@ref). +""" +struct BreakpointSignature <: AbstractBreakpoint + f::Union{Method, Function} + sig::Union{Nothing, Type} + line::Int # 0 is a sentinel for first statement + condition::Condition + enabled::Ref{Bool} + instances::Vector{BreakpointRef} +end +same_location(bp2::BreakpointSignature, bp::BreakpointSignature) = + bp2.f == bp.f && bp2.sig == bp.sig && bp2.line == bp.line +function Base.show(io::IO, bp::BreakpointSignature) + print(io, bp.f) + if bp.sig !== nothing + print(io, '(', join("::" .* string.(bp.sig.types), ", "), ')') + end + if bp.line !== 0 + print(io, ":", bp.line) + end + print_bp_condition(io, bp.condition) + if !bp.enabled[] + print(io, " [disabled]") + end +end + +""" +A `BreakpointFileLocation` is a breakpoint that is set on a line in a file. + +Fields: +- `path::String`: The literal string that was used to create the breakpoint, e.g. `"path/file.jl"`. +- `abspath`::String: The absolute path to the file when the breakpoint was created, e.g. `"/Users/Someone/path/file.jl"`. + +For common fields shared by all breakpoints, see [`AbstractBreakpoint`](@ref). +""" +struct BreakpointFileLocation <: AbstractBreakpoint + # Both the input path and the absolute path is stored to handle the case + # where a user sets a breakpoint on a relative path e.g. `../foo.jl`. The absolute path is needed + # to handle the case where the current working directory change, and + # the input path is needed to do "partial path matches", e.g match "src/foo.jl" against + # "Package/src/foo.jl". + path::String + abspath::String + line::Int + condition::Condition + enabled::Ref{Bool} + instances::Vector{BreakpointRef} +end +same_location(bp2::BreakpointFileLocation, bp::BreakpointFileLocation) = + bp2.path == bp.path && bp2.abspath == bp.abspath && bp2.line == bp.line +function Base.show(io::IO, bp::BreakpointFileLocation) + print(io, bp.path, ':', bp.line) + print_bp_condition(io, bp.condition) + if !bp.enabled[] + print(io, " [disabled]") + end +end + diff --git a/src/utils.jl b/src/utils.jl index e312b526..9bf37641 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -233,6 +233,20 @@ function codelocation(code::CodeInfo, idx) return codeloc end +function compute_corrected_linerange(method::Method) + _, line1 = whereis(method) + offset = line1 - method.line + src = JuliaInterpreter.get_source(method) + lastline = src.linetable[end] + return line1:getline(lastline) + offset +end + +function method_contains_line(method::Method, line::Integer) + # Check to see if this method really contains that line. Methods that fill in a default positional argument, + # keyword arguments, and @generated sections may not contain the line. + return line in compute_corrected_linerange(method) +end + """ stmtidx = statementnumber(frame, line) diff --git a/test/breakpoints.jl b/test/breakpoints.jl index d871f371..fcd7c65a 100644 --- a/test/breakpoints.jl +++ b/test/breakpoints.jl @@ -8,19 +8,17 @@ function loop_radius2(n) end tmppath = "" -if isdefined(Main, :Revise) - global tmppath - tmppath, io = mktemp() - print(io, """ - function jikwfunc(x, y=0; z="hello") - a = x + y - b = z^a - return length(b) - end - """) - close(io) - includet(tmppath) +global tmppath +tmppath, io = mktemp() +print(io, """ +function jikwfunc(x, y=0; z="hello") + a = x + y + b = z^a + return length(b) end +""") +close(io) +include(tmppath) using JuliaInterpreter, Test @@ -34,6 +32,8 @@ function stacklength(frame) return n end +struct Squarer end + @testset "Breakpoints" begin breakpoint(radius2) frame = JuliaInterpreter.enter_call(loop_radius2, 2) @@ -70,9 +70,9 @@ end # Conditional breakpoints on local variables remove() halfthresh = loop_radius2(5) - @breakpoint loop_radius2(10) 5 s>$halfthresh - frame, bp = @interpret loop_radius2(10) - @test isa(bp, JuliaInterpreter.BreakpointRef) + bp = @breakpoint loop_radius2(10) 5 s>$halfthresh + frame, bpref = @interpret loop_radius2(10) + @test isa(bpref, JuliaInterpreter.BreakpointRef) lframe = leaf(frame) s_extractor = eval(JuliaInterpreter.prepare_slotfunction(lframe.framecode, :s)) @test s_extractor(lframe) == loop_radius2(6) @@ -102,29 +102,20 @@ end @test JuliaInterpreter.finish_stack!(frame) == 2 # Breakpoints by file/line - if isdefined(Main, :Revise) - remove() - method = which(JuliaInterpreter.locals, Tuple{Frame}) - breakpoint(String(method.file), method.line+1) - frame = JuliaInterpreter.enter_call(loop_radius2, 2) - ret = @interpret JuliaInterpreter.locals(frame) - @test isa(ret, Tuple{Frame,JuliaInterpreter.BreakpointRef}) - # Test kwarg method - remove() - bp = breakpoint(tmppath, 3) - frame, bp2 = @interpret jikwfunc(2) - @test bp2 == bp - var = JuliaInterpreter.locals(leaf(frame)) - @test !any(v->v.name == :b, var) - @test filter(v->v.name == :a, var)[1].value == 2 - else - try - breakpoint(pathof(JuliaInterpreter.CodeTracking), 5) - catch err - @test isa(err, ErrorException) - @test occursin("Revise", err.msg) - end - end + remove() + method = which(JuliaInterpreter.locals, Tuple{Frame}) + breakpoint(String(method.file), method.line+1) + frame = JuliaInterpreter.enter_call(loop_radius2, 2) + ret = @interpret JuliaInterpreter.locals(frame) + @test isa(ret, Tuple{Frame,JuliaInterpreter.BreakpointRef}) + # Test kwarg method + remove() + bp = breakpoint(tmppath, 3) + frame, bp2 = @interpret jikwfunc(2) + var = JuliaInterpreter.locals(leaf(frame)) + @test !any(v->v.name == :b, var) + @test filter(v->v.name == :a, var)[1].value == 2 + # Direct return @breakpoint gcd(1,1) a==5 @@ -185,14 +176,11 @@ end io = IOBuffer() frame = JuliaInterpreter.enter_call(loop_radius2, 2) bp = JuliaInterpreter.BreakpointRef(frame.framecode, 1) - show(io, bp) - @test String(take!(io)) == "breakpoint(loop_radius2(n) in $(@__MODULE__) at $(@__FILE__):3, line 3)" + @test repr(bp) == "breakpoint(loop_radius2(n) in $(@__MODULE__) at $(@__FILE__):3, line 3)" bp = JuliaInterpreter.BreakpointRef(frame.framecode, 0) # fictive breakpoint - show(io, bp) - @test String(take!(io)) == "breakpoint(loop_radius2(n) in $(@__MODULE__) at $(@__FILE__):3, %0)" + @test repr(bp) == "breakpoint(loop_radius2(n) in $(@__MODULE__) at $(@__FILE__):3, %0)" bp = JuliaInterpreter.BreakpointRef(frame.framecode, 1, ArgumentError("whoops")) - show(io, bp) - @test String(take!(io)) == "breakpoint(loop_radius2(n) in $(@__MODULE__) at $(@__FILE__):3, line 3, ArgumentError(\"whoops\"))" + @test repr(bp) == "breakpoint(loop_radius2(n) in $(@__MODULE__) at $(@__FILE__):3, line 3, ArgumentError(\"whoops\"))" # In source breakpointing f_outer_bp(x) = g_inner_bp(x) @@ -207,8 +195,105 @@ end fr, bp = @interpret f_outer_bp(3) @test leaf(fr).framecode.scope.name == :g_inner_bp @test bp.stmtidx == 3 + + # Breakpoints on types + remove() + g() = Int(5.0) + @breakpoint Int(5.0) + frame, bp = @interpret g() + @test bp isa BreakpointRef + @test leaf(frame).framecode.scope === @which Int(5.0) + + # Breakpoint on call overloads + (::Squarer)(x) = x^2 + squarer = Squarer() + @breakpoint squarer(2) + frame, bp = @interpret squarer(3.0) + @test bp isa BreakpointRef + @test leaf(frame).framecode.scope === @which squarer(3.0) +end + +mktemp() do path, io + print(io, """ + function somefunc(x, y=0) + a = x + y + b = z^a + return a + b + end + """) + close(io) + breakpoint(path, 3) + include(path) + frame, bp = @interpret somefunc(2, 3) + @test bp isa BreakpointRef + @test JuliaInterpreter.whereis(frame) == (path, 3) + breakpoint(path, 2) + frame, bp = @interpret somefunc(2, 3) + @test bp isa BreakpointRef + @test JuliaInterpreter.whereis(frame) == (path, 2) + remove() + # Test relative paths + mktempdir(dirname(path)) do tmp + cd(tmp) do + breakpoint(joinpath("..", basename(path)), 3) + frame, bp = @interpret somefunc(2, 3) + @test bp isa BreakpointRef + @test JuliaInterpreter.whereis(frame) == (path, 3) + remove() + breakpoint(joinpath("..", basename(path)), 3) + cd(homedir()) do + frame, bp = @interpret somefunc(2, 3) + @test bp isa BreakpointRef + @test JuliaInterpreter.whereis(frame) == (path, 3) + end + end + end end if tmppath != "" rm(tmppath) end + +@testset "toggling" begin + remove() + f_break(x::Int) = x + bp = breakpoint(f_break) + frame, bpref = @interpret f_break(5) + @test bpref isa BreakpointRef + toggle(bp) + @test (@interpret f_break(5)) == 5 + f_break(x::Float64) = 2x + @test (@interpret f_break(2.0)) == 4.0 + toggle(bp) + frame, bpref = @interpret f_break(5) + @test bpref isa BreakpointRef + frame, bpref = @interpret f_break(2.0) + @test bpref isa BreakpointRef +end + +using Dates +@testset "breakpoint in stdlibs by path" begin + m = @which now() - Month(2) + f = String(m.file) + l = m.line + 1 + for f in (f, basename(f)) + remove() + breakpoint(f, l) + frame, bp = @interpret now() - Month(2) + @test bp isa BreakpointRef + @test JuliaInterpreter.whereis(frame)[2] == l + end +end + +@testset "breakpoint in Base by path" begin + m = @which sin(2.0) + f = String(m.file) + l = m.line + 1 + for f in (f, basename(f)) + remove() + breakpoint(f, l) + frame, bp = @interpret sin(2.0) + @test bp isa BreakpointRef + @test JuliaInterpreter.whereis(frame)[2] == l + end +end diff --git a/test/interpret.jl b/test/interpret.jl index ce43d302..061a22dc 100644 --- a/test/interpret.jl +++ b/test/interpret.jl @@ -522,4 +522,11 @@ function f() z = [Core.SSAValue(5),] repr(z[1]) end -@test @interpret f() == f() \ No newline at end of file +@test @interpret f() == f() + +# Test JuliaInterpreter version of #265 +f(x) = x +g(x) = f(x) +@test (@interpret g(5)) == g(5) +f(x) = x*x +@test (@interpret g(5)) == g(5) diff --git a/test/runtests.jl b/test/runtests.jl index d81c8d82..b6f0d99d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -14,5 +14,6 @@ JuliaInterpreter.debug_recycle[] = true include("toplevel.jl") include("limits.jl") include("breakpoints.jl") + remove() include("debug.jl") end