Skip to content

Commit a64772e

Browse files
committed
Add support for breakpoints
1 parent 6eaf051 commit a64772e

File tree

5 files changed

+382
-52
lines changed

5 files changed

+382
-52
lines changed

src/JuliaInterpreter.jl

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ using UUIDs
1111
using Random.DSFMT
1212
using InteractiveUtils
1313

14-
export @enter, @make_stack, @interpret, Compiled, JuliaStackFrame
14+
export @enter, @make_stack, @interpret, Compiled, JuliaStackFrame, Breakpoints, breakpoint
1515

1616
module CompiledCalls
1717
# This module is for handling intrinsics that must be compiled (llvmcall)
@@ -47,7 +47,7 @@ end
4747

4848
"""
4949
`JuliaFrameCode` holds static information about a method or toplevel code.
50-
One `JuliaFrameCode` can be shared by many `JuliaFrameState` calling frames.
50+
One `JuliaFrameCode` can be shared by many `JuliaStackFrame` calling frames.
5151
5252
Important fields:
5353
- `scope`: the `Method` or `Module` in which this frame is to be evaluated
@@ -61,6 +61,7 @@ struct JuliaFrameCode
6161
scope::Union{Method,Module}
6262
code::CodeInfo
6363
methodtables::Vector{Union{Compiled,TypeMapEntry}} # line-by-line method tables for generic-function :call Exprs
64+
breakpoints::Vector{Tuple{Bool,Function}} # (isactive, condition)
6465
used::BitSet
6566
wrapper::Bool
6667
generator::Bool
@@ -69,7 +70,7 @@ struct JuliaFrameCode
6970
end
7071

7172
function JuliaFrameCode(frame::JuliaFrameCode; wrapper = frame.wrapper, generator=frame.generator, fullpath=frame.fullpath)
72-
JuliaFrameCode(frame.scope, frame.code, frame.methodtables, frame.used,
73+
JuliaFrameCode(frame.scope, frame.code, frame.methodtables, frame.breakpoints, frame.used,
7374
wrapper, generator, fullpath)
7475
end
7576

@@ -80,8 +81,9 @@ function JuliaFrameCode(scope, code::CodeInfo; wrapper=false, generator=false, f
8081
code = copy_codeinfo(code)
8182
methodtables = Vector{Union{Compiled,TypeMapEntry}}(undef, length(code.code))
8283
end
84+
breakpoints = Vector{Tuple{Bool,Function}}(undef, length(code.code))
8385
used = find_used(code)
84-
return JuliaFrameCode(scope, code, methodtables, used, wrapper, generator, fullpath)
86+
return JuliaFrameCode(scope, code, methodtables, breakpoints, used, wrapper, generator, fullpath)
8587
end
8688

8789
"""
@@ -211,6 +213,16 @@ function is_generated(meth)
211213
isdefined(meth, :generator)
212214
end
213215

216+
function sparam_syms(meth::Method)
217+
s = Symbol[]
218+
sig = meth.sig
219+
while sig isa UnionAll
220+
push!(s, Symbol(sig.var.name))
221+
sig = sig.body
222+
end
223+
return s
224+
end
225+
214226
function namedtuple(kwargs)
215227
names, types, vals = Symbol[], [], []
216228
for pr in kwargs
@@ -269,6 +281,48 @@ function prepare_args(@nospecialize(f), allargs, kwargs)
269281
return f, allargs
270282
end
271283

284+
function prepare_framecode(method::Method, argtypes; enter_generated=false)
285+
sig = method.sig
286+
isa(method, TypeMapEntry) && (method = method.func)
287+
if method.module == Core.Compiler || method.module == Base.Threads || method compiled_methods
288+
return Compiled()
289+
end
290+
# Get static parameters
291+
(ti, lenv::SimpleVector) = ccall(:jl_type_intersection_with_env, Any, (Any, Any),
292+
argtypes, sig)::SimpleVector
293+
enter_generated &= is_generated(method)
294+
if is_generated(method) && !enter_generated
295+
framecode = get(genframedict, (method, argtypes), nothing)
296+
else
297+
framecode = get(framedict, method, nothing)
298+
end
299+
if framecode === nothing
300+
if is_generated(method) && !enter_generated
301+
# If we're stepping into a staged function, we need to use
302+
# the specialization, rather than stepping through the
303+
# unspecialized method.
304+
code = Core.Compiler.get_staged(Core.Compiler.code_for_method(method, argtypes, lenv, typemax(UInt), false))
305+
code === nothing && return nothing
306+
generator = false
307+
else
308+
if is_generated(method)
309+
code = get_source(method.generator)
310+
generator = true
311+
else
312+
code = get_source(method)
313+
generator = false
314+
end
315+
end
316+
framecode = JuliaFrameCode(method, code; generator=generator)
317+
if is_generated(method) && !enter_generated
318+
genframedict[(method, argtypes)] = framecode
319+
else
320+
framedict[method] = framecode
321+
end
322+
end
323+
return framecode, lenv
324+
end
325+
272326
function whichtt(tt)
273327
m = ccall(:jl_gf_invoke_lookup, Any, (Any, UInt), tt, typemax(UInt))
274328
m === nothing && return nothing
@@ -328,47 +382,17 @@ function prepare_call(@nospecialize(f), allargs; enter_generated = false)
328382
f(allargs[2:end]...)
329383
end
330384
args = allargs
331-
sig = method.sig
332-
isa(method, TypeMapEntry) && (method = method.func)
333-
if method.module == Core.Compiler || method.module == Base.Threads || method compiled_methods
334-
return Compiled()
335-
end
336-
# Get static parameters
337-
(ti, lenv::SimpleVector) = ccall(:jl_type_intersection_with_env, Any, (Any, Any),
338-
argtypes, sig)::SimpleVector
339-
enter_generated &= is_generated(method)
340-
if is_generated(method) && !enter_generated
341-
framecode = get(genframedict, (method, argtypes), nothing)
342-
else
343-
framecode = get(framedict, method, nothing)
385+
ret = prepare_framecode(method, argtypes; enter_generated=enter_generated)
386+
# Exceptional returns
387+
if ret === nothing
388+
# The generator threw an error. Let's generate the same error by calling it.
389+
f(allargs[2:end]...)
344390
end
345-
if framecode === nothing
346-
if is_generated(method) && !enter_generated
347-
# If we're stepping into a staged function, we need to use
348-
# the specialization, rather than stepping through the
349-
# unspecialized method.
350-
code = Core.Compiler.get_staged(Core.Compiler.code_for_method(method, argtypes, lenv, typemax(UInt), false))
351-
generator = false
352-
if code === nothing
353-
# The generator threw an error. Let's generate the same error by calling it.
354-
f(allargs[2:end]...)
355-
end
356-
else
357-
if is_generated(method)
358-
args = Any[_Typeof(a) for a in args]
359-
code = get_source(method.generator)
360-
generator = true
361-
else
362-
code = get_source(method)
363-
generator = false
364-
end
365-
end
366-
framecode = JuliaFrameCode(method, code; generator=generator)
367-
if is_generated(method) && !enter_generated
368-
genframedict[(method, argtypes)] = framecode
369-
else
370-
framedict[method] = framecode
371-
end
391+
isa(ret, Compiled) && return ret
392+
# Typical return
393+
framecode, lenv = ret
394+
if is_generated(method) && enter_generated
395+
args = Any[_Typeof(a) for a in args]
372396
end
373397
return framecode, args, lenv, argtypes
374398
end
@@ -793,6 +817,7 @@ function prepare_locals(framecode, argvals::Vector{Any})
793817
pc = Ref(JuliaProgramCounter(1))
794818
end
795819
for i = 1:meth.nargs
820+
last_reference[framecode.code.slotnames[i]] = i
796821
if meth.isva && i == meth.nargs
797822
locals[i] = nargs < i ? Some{Any}(()) : (let i=i; Some{Any}(ntuple(k->argvals[i+k-1], nargs-i+1)); end)
798823
break
@@ -1083,6 +1108,10 @@ function __init__()
10831108
set_compiled_methods()
10841109
end
10851110

1111+
include("breakpoints.jl")
1112+
using .Breakpoints
1113+
using .Breakpoints: shouldbreak, Breakpoint
1114+
10861115
include("precompile.jl")
10871116
_precompile_()
10881117

src/breakpoints.jl

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
module Breakpoints
2+
3+
using ..JuliaInterpreter
4+
using JuliaInterpreter: JuliaFrameCode, JuliaStackFrame,
5+
prepare_framecode, framedict, get_source, sparam_syms,
6+
linenumber
7+
8+
export breakpoint
9+
10+
# A type that is unique to this package for which there are no valid operations
11+
struct Unassigned end
12+
13+
"""
14+
Breakpoint(framecode, stmtidx)
15+
16+
A reference to a breakpoint at a particular statement index `stmtidx` in `framecode`.
17+
"""
18+
struct Breakpoint
19+
framecode::JuliaFrameCode
20+
stmtidx::Int
21+
end
22+
23+
function Base.show(io::IO, bp::Breakpoint)
24+
lineno = linenumber(bp.framecode, bp.stmtidx)
25+
print(io, "breakpoint(", bp.framecode.scope, ", ", lineno, ')')
26+
end
27+
28+
const _breakpoints = Set{Breakpoint}()
29+
30+
function shouldbreak(frame, pc=frame.pc[])
31+
idx = convert(Int, pc)
32+
isassigned(frame.code.breakpoints, idx) || return false
33+
bp = frame.code.breakpoints[idx]
34+
bp[1] || return false
35+
slotfunction = bp[2]
36+
return slotfunction(frame)::Bool
37+
end
38+
39+
function prepare_slotfunction(framecode::JuliaFrameCode, body::Union{Symbol,Expr})
40+
ismeth = framecode.scope isa Method
41+
uslotnames = Set{Symbol}()
42+
slotnames = Symbol[]
43+
for name in framecode.code.slotnames
44+
if name uslotnames
45+
push!(slotnames, name)
46+
push!(uslotnames, name)
47+
end
48+
end
49+
assignments = Expr[]
50+
framename = gensym("frame")
51+
default = Unassigned()
52+
for i = 1:length(slotnames)
53+
slotname = framecode.code.slotnames[i]
54+
qslotname = QuoteNode(slotname)
55+
getexpr = :(something($framename.locals[$framename.last_reference[$qslotname]]))
56+
push!(assignments, Expr(:(=), slotname, :(haskey($framename.last_reference, $qslotname) ? $getexpr : $default)))
57+
end
58+
if ismeth
59+
syms = sparam_syms(framecode.scope)
60+
for i = 1:length(syms)
61+
push!(assignments, Expr(:(=), syms[i], :($framename.sparams[$i])))
62+
end
63+
end
64+
funcname = ismeth ? gensym("slotfunction") : gensym(Symbol(framecode.scope.name, "_slotfunction"))
65+
return Expr(:function, Expr(:call, funcname, framename), Expr(:block, assignments..., body))
66+
end
67+
68+
truecondition(frame) = true
69+
falsecondition(frame) = false
70+
71+
## The fundamental implementations of breakpoint-setting
72+
function breakpoint!(framecode::JuliaFrameCode, pc, condition::Union{Bool,Expr}=true)
73+
stmtidx = convert(Int, pc)
74+
if isa(condition, Bool)
75+
framecode.breakpoints[stmtidx] = condition ? (true, truecondition) : (false, falsecondition)
76+
else
77+
fex = prepare_slotfunction(framecode, condition)
78+
framecode.breakpoints[stmtidx] = (true, eval(fex))
79+
end
80+
bp = Breakpoint(framecode, stmtidx)
81+
push!(_breakpoints, bp)
82+
return bp
83+
end
84+
breakpoint!(frame::JuliaStackFrame, pc=frame.pc[], condition::Union{Bool,Expr}=true) =
85+
breakpoint!(frame.code, pc, condition)
86+
87+
function set_breakpoint_active!(framecode::JuliaFrameCode, active, pc)
88+
stmtidx = convert(Int, pc)
89+
_, condition = framecode.breakpoints[stmtidx]
90+
framecode.breakpoints[stmtidx] = (active, condition)
91+
active
92+
end
93+
set_breakpoint_active!(frame::JuliaStackFrame, active, pc) =
94+
set_breakpoint_active!(frame.code, active, pc)
95+
96+
function get_breakpoint_active(framecode::JuliaFrameCode, pc)
97+
stmtidx = convert(Int, pc)
98+
return framecode.breakpoints[stmtidx][1]
99+
end
100+
get_breakpoint_active(frame::JuliaStackFrame, pc) =
101+
get_breakpoint_active(frame.code, pc)
102+
103+
function toggle_breakpoint_active!(framecode::JuliaFrameCode, pc)
104+
stmtidx = convert(Int, pc)
105+
active, condition = framecode.breakpoints[stmtidx]
106+
framecode.breakpoints[stmtidx] = (!active, condition)
107+
!active
108+
end
109+
toggle_breakpoint_active!(frame::JuliaStackFrame, active, pc) =
110+
toggle_breakpoint_active!(frame.code, active, pc)
111+
112+
enable(bp::Breakpoint) = set_breakpoint_active!(bp.framecode, true, bp.stmtidx)
113+
disable(bp::Breakpoint) = set_breakpoint_active!(bp.framecode, false, bp.stmtidx)
114+
function remove(bp::Breakpoint)
115+
bp.framecode.breakpoints[bp.stmtidx] = (false, falsecondition)
116+
delete!(_breakpoints, bp)
117+
return nothing
118+
end
119+
120+
enable() = for bp in _breakpoints enable(bp) end
121+
disable() = for bp in _breakpoints disable(bp) end
122+
function remove()
123+
for bp in _breakpoints
124+
bp.framecode.breakpoints[bp.stmtidx] = (false, falsecondition)
125+
end
126+
empty!(_breakpoints)
127+
return nothing
128+
end
129+
130+
"""
131+
breakpoint(f, sig)
132+
breakpoint(f, sig, condition)
133+
breakpoint(...; enter_generated=false)
134+
135+
Add a breakpoint upon entry to `f` with the specified argument types `sig`.
136+
The first will break unconditionally, the second only if `condition` evaluates to `true`.
137+
`condition` should be written in terms of the arguments and local variables of `f`.
138+
139+
# Example
140+
```julia
141+
function radius2(x, y)
142+
return x^2 + y^2
143+
end
144+
145+
breakpoint(radius2, Tuple{Int,Int}, :(y > x))
146+
```
147+
"""
148+
function breakpoint(f, sig::Type, condition::Union{Bool,Expr}=true; enter_generated=false)
149+
method = which(f, sig)
150+
framecode, _ = prepare_framecode(method, sig; enter_generated=enter_generated)
151+
breakpoint!(framecode, 1, condition)
152+
end
153+
154+
"""
155+
breakpoint(method::Method)
156+
breakpoint(method::Method, condition::Expr)
157+
158+
Add a breakpoint upon entry to `method`. The first will break unconditionally, the second
159+
only if `condition` evaluates to `true`. `condition` should be written in terms of the
160+
arguments and local variables of `method`.
161+
"""
162+
function breakpoint(method::Method, condition::Union{Bool,Expr}=true)
163+
# FIXME: this duplicates stuff in prepare_call
164+
framecode = get(framedict, method, nothing)
165+
if framecode === nothing
166+
code = get_source(method)
167+
framecode = JuliaFrameCode(method, code; generator=false)
168+
framedict[method] = framecode
169+
end
170+
breakpoint!(framecode, 1, condition)
171+
end
172+
173+
"""
174+
breakpoint(f)
175+
breakpoint(f, condition)
176+
177+
Break-on-entry to all methods of `f`.
178+
"""
179+
function breakpoint(f, condition::Union{Bool,Expr}=true)
180+
for method in methods(f)
181+
breakpoint(method, condition)
182+
end
183+
nothing
184+
end
185+
186+
end

0 commit comments

Comments
 (0)