Skip to content

Commit caff042

Browse files
committed
Integrate with CodeTracking and support line numbers when setting breakpoints
If the user is running an appropriate version of Revise, reported location will update as code moves around due to edits. Because methods can move files as well as from line-to-line, `linenumber` should not be used except internally. (Use `whereis` instead.) If the user is not running Revise, CodeTracking will be harmless.
1 parent 9c5fd8f commit caff042

File tree

6 files changed

+132
-36
lines changed

6 files changed

+132
-36
lines changed

Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
33
version = "0.1.1"
44

55
[deps]
6+
CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"
67
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
78
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
89
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

src/JuliaInterpreter.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ using UUIDs
1010
# in Base and stdlib
1111
using Random.DSFMT
1212
using InteractiveUtils
13+
using CodeTracking
1314

1415
export @interpret, Compiled, JuliaStackFrame,
1516
Breakpoints, breakpoint, @breakpoint, breakpoints, enable, disable, remove
@@ -1084,7 +1085,8 @@ macro interpret(arg)
10841085
push!(stack, frame)
10851086
return stack, BreakpointRef(frame.code, 1)
10861087
end
1087-
finish_and_return!(stack, frame)
1088+
ret = finish_and_return!(stack, frame)
1089+
isa(ret, BreakpointRef) ? (stack, ret) : ret
10881090
end
10891091
end
10901092

src/breakpoints.jl

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Breakpoints
33
using ..JuliaInterpreter
44
using JuliaInterpreter: JuliaFrameCode, JuliaStackFrame, BreakpointState,
55
truecondition, falsecondition, prepare_framecode, get_framecode,
6-
sparam_syms, linenumber
6+
sparam_syms, linenumber, statementnumber
77
using Base.Meta: isexpr
88
using InteractiveUtils
99

@@ -91,18 +91,22 @@ function prepare_slotfunction(framecode::JuliaFrameCode, body::Union{Symbol,Expr
9191
return Expr(:function, Expr(:call, funcname, framename), Expr(:block, assignments..., body))
9292
end
9393

94+
const Condition = Union{Nothing,Expr,Tuple{Module,Expr}}
95+
_unpack(condition) = isa(condition, Expr) ? (Main, condition) : condition
96+
9497
## The fundamental implementations of breakpoint-setting
95-
function breakpoint!(framecode::JuliaFrameCode, pc, condition::Union{Bool,Expr}=true)
98+
function breakpoint!(framecode::JuliaFrameCode, pc, condition::Condition=nothing)
9699
stmtidx = convert(Int, pc)
97-
if isa(condition, Bool)
98-
framecode.breakpoints[stmtidx] = BreakpointState(condition)
100+
if condition === nothing
101+
framecode.breakpoints[stmtidx] = BreakpointState()
99102
else
100-
fex = prepare_slotfunction(framecode, condition)
101-
framecode.breakpoints[stmtidx] = BreakpointState(true, eval(fex))
103+
mod, cond = _unpack(condition)
104+
fex = prepare_slotfunction(framecode, cond)
105+
framecode.breakpoints[stmtidx] = BreakpointState(true, Core.eval(mod, fex))
102106
end
103107
return add_breakpoint(framecode, stmtidx)
104108
end
105-
breakpoint!(frame::JuliaStackFrame, pc=frame.pc[], condition::Union{Bool,Expr}=true) =
109+
breakpoint!(frame::JuliaStackFrame, pc=frame.pc[], condition::Condition=nothing) =
106110
breakpoint!(frame.code, pc, condition)
107111

108112
enable(bp::BreakpointRef) = bp[] = true
@@ -126,11 +130,16 @@ end
126130

127131
"""
128132
breakpoint(f, sig)
133+
breakpoint(f, sig, line)
129134
breakpoint(f, sig, condition)
135+
breakpoint(f, sig, line, condition)
130136
breakpoint(...; enter_generated=false)
131137
132-
Add a breakpoint upon entry to `f` with the specified argument types `sig`.
133-
The first will break unconditionally, the second only if `condition` evaluates to `true`.
138+
Add a breakpoint to `f` with the specified argument types `sig`.
139+
Optionally specify an absolute line number `line` in the source file; the default
140+
is to break upon entry at the first line of the body.
141+
Without `condition`, the breakpoint will be triggered every time it is encountered;
142+
the second only if `condition` evaluates to `true`.
134143
`condition` should be written in terms of the arguments and local variables of `f`.
135144
136145
# Example
@@ -142,19 +151,33 @@ end
142151
breakpoint(radius2, Tuple{Int,Int}, :(y > x))
143152
```
144153
"""
145-
function breakpoint(f, sig::Type, condition::Union{Bool,Expr}=true; enter_generated=false)
154+
function breakpoint(f, sig::Type, line::Integer, condition::Condition=nothing; enter_generated=false)
155+
method = which(f, sig)
156+
framecode, _ = prepare_framecode(method, sig; enter_generated=enter_generated)
157+
# Don't use statementnumber(method, line) in case it's enter_generated
158+
linec = line - whereis(method)[2] + method.line
159+
stmtidx = statementnumber(framecode, linec)
160+
breakpoint!(framecode, stmtidx, condition)
161+
end
162+
function breakpoint(f, sig::Type, condition::Condition=nothing; enter_generated=false)
146163
method = which(f, sig)
147164
framecode, _ = prepare_framecode(method, sig; enter_generated=enter_generated)
148165
breakpoint!(framecode, 1, condition)
149166
end
150167

151168
"""
152169
breakpoint(method::Method)
170+
breakpoint(method::Method, line)
153171
breakpoint(method::Method, condition::Expr)
172+
breakpoint(method::Method, line, condition::Expr)
154173
155-
Add a breakpoint upon entry to `method`.
174+
Add a breakpoint to `method`.
156175
"""
157-
function breakpoint(method::Method, condition::Union{Bool,Expr}=true)
176+
function breakpoint(method::Method, line::Integer, condition::Condition=nothing)
177+
framecode, stmtidx = statementnumber(method, line)
178+
breakpoint!(framecode, stmtidx, condition)
179+
end
180+
function breakpoint(method::Method, condition::Condition=nothing)
158181
framecode = get_framecode(method)
159182
breakpoint!(framecode, 1, condition)
160183
end
@@ -165,19 +188,69 @@ end
165188
166189
Break-on-entry to all methods of `f`.
167190
"""
168-
function breakpoint(f, condition::Union{Bool,Expr}=true)
191+
function breakpoint(f, condition::Condition=nothing)
169192
bps = BreakpointRef[]
170193
for method in methods(f)
171194
push!(bps, breakpoint(method, condition))
172195
end
173196
return bps
174197
end
175198

176-
macro breakpoint(call_expr, condition)
199+
"""
200+
@breakpoint f(args...) condition=nothing
201+
@breakpoint f(args...) line condition=nothing
202+
203+
Break upon entry, or at the specified line number, in the method called by `f(args...)`.
204+
Optionally supply a condition expressed in terms of the arguments and internal variables
205+
of the method.
206+
If `line` is supplied, it must be a literal integer.
207+
208+
# Example
209+
210+
Suppose a method `mysum` is defined as follows, where the numbers to the left are the line
211+
number in the file:
212+
213+
```
214+
12 function mysum(A)
215+
13 s = zero(eltype(A))
216+
14 for a in A
217+
15 s += a
218+
16 end
219+
17 return s
220+
18 end
221+
```
222+
223+
Then
224+
225+
```
226+
@breakpoint mysum(A) 15 s>10
227+
```
228+
229+
would cause execution of the loop to break whenever `s>10`.
230+
"""
231+
macro breakpoint(call_expr, args...)
177232
whichexpr = InteractiveUtils.gen_call_with_extracted_types(__module__, :which, call_expr)
178-
return quote
179-
local method = $whichexpr
180-
$breakpoint(method, $(Expr(:quote, condition)))
233+
haveline, line, condition = false, 0, nothing
234+
while !isempty(args)
235+
arg = first(args)
236+
if isa(arg, Integer)
237+
haveline, line = true, arg
238+
else
239+
condition = arg
240+
end
241+
args = Base.tail(args)
242+
end
243+
condexpr = condition === nothing ? nothing : Expr(:quote, condition)
244+
if haveline
245+
return quote
246+
local method = $whichexpr
247+
$breakpoint(method, $line, $condexpr)
248+
end
249+
else
250+
return quote
251+
local method = $whichexpr
252+
$breakpoint(method, $condexpr)
253+
end
181254
end
182255
end
183256

src/interpret.jl

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -679,30 +679,54 @@ end
679679
isgotonode(node) = isa(node, GotoNode) || isexpr(node, :gotoifnot)
680680

681681
"""
682-
linenumber(frame, pc=frame.pc[])
682+
loc = whereis(frame, pc=frame.pc[])
683683
684-
Return line number for `frame` at `pc` or `nothing` if it cannot be determined.
684+
Return the file and line number for `frame` at `pc`. If this cannot be
685+
determined, `loc == nothing`. Otherwise `loc == (filepath, line)`.
686+
687+
When `frame` represents top-level code,
685688
"""
686-
function linenumber(framecode::JuliaFrameCode, pc)
689+
function CodeTracking.whereis(framecode::JuliaFrameCode, pc)
687690
codeloc = framecode.code.codelocs[convert(Int, pc)]
688691
codeloc == 0 && return nothing
692+
lineinfo = framecode.code.linetable[codeloc]
689693
return framecode.scope isa Method ?
690-
framecode.code.linetable[codeloc].line :
691-
codeloc
694+
whereis(lineinfo, framecode.scope) : string(lineinfo.file), lineinfo.line
695+
end
696+
CodeTracking.whereis(frame::JuliaStackFrame, pc=frame.pc[]) = whereis(frame.code, pc)
697+
698+
# Note: linenumber is now an internal method for use by `next_line!`
699+
# If you want to know the actual line number in a file, call `whereis`.
700+
function linenumber(framecode::JuliaFrameCode, pc)
701+
codeloc = framecode.code.codelocs[convert(Int, pc)]
702+
codeloc == 0 && return nothing
703+
return framecode.code.linetable[codeloc].line
692704
end
693705
linenumber(frame::JuliaStackFrame, pc=frame.pc[]) = linenumber(frame.code, pc)
694706

695707
"""
696-
statementnumber(frame, line)
708+
stmtidx = statementnumber(frame, line)
697709
698710
Return the index of the first statement in `frame`'s `CodeInfo` that corresponds to `line`.
699711
"""
700712
function statementnumber(framecode::JuliaFrameCode, line)
701-
lineidx = searchsortedfirst(framecode.code.linetable, line; by=lin->lin.line)
713+
lineidx = searchsortedfirst(framecode.code.linetable, line; by=lin->isa(lin,Integer) ? lin : lin.line)
714+
1 <= lineidx <= length(framecode.code.linetable) || throw(ArgumentError("line $line not found in $(framecode.scope)"))
702715
return searchsortedfirst(framecode.code.codelocs, lineidx)
703716
end
704717
statementnumber(frame::JuliaStackFrame, line) = statementnumber(frame.code, line)
705718

719+
"""
720+
framecode, stmtidx = statementnumber(method, line)
721+
722+
Return the index of the first statement in `framecode` that corresponds to the given `line` in `method`.
723+
"""
724+
function statementnumber(method::Method, line; line1=whereis(method)[2])
725+
linec = line - line1 + method.line # line number at time of compilation
726+
framecode = get_framecode(method)
727+
return framecode, statementnumber(framecode, linec)
728+
end
729+
706730
function next_line!(stack, frame, dbstack = nothing)
707731
initial = linenumber(frame)
708732
first = true

test/breakpoints.jl

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,11 @@ end
4444

4545
# Conditional breakpoints on local variables
4646
remove()
47-
stack = JuliaStackFrame[]
48-
frame = JuliaInterpreter.enter_call(loop_radius2, 10)
4947
halfthresh = loop_radius2(5)
50-
JuliaInterpreter.next_line!(stack, frame)
51-
JuliaInterpreter.next_line!(stack, frame)
52-
pc = frame.pc[]
53-
Breakpoints.breakpoint!(frame.code, pc, :(s > $halfthresh))
54-
bp = JuliaInterpreter.finish_and_return!(stack, frame)
48+
@breakpoint loop_radius2(10) 5 s>$halfthresh
49+
stack, bp = @interpret loop_radius2(10)
5550
@test isa(bp, Breakpoints.BreakpointRef)
51+
frame = stack[end]
5652
s_extractor = eval(Breakpoints.prepare_slotfunction(frame.code, :s))
5753
@test s_extractor(frame) == loop_radius2(6)
5854
JuliaInterpreter.finish_stack!(stack)

test/interpret.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using JuliaInterpreter
22
using JuliaInterpreter: enter_call_expr
3-
using Test, InteractiveUtils
3+
using Test, InteractiveUtils, CodeTracking
44

55
pc = JuliaInterpreter.JuliaProgramCounter(2)
66
@test convert(Int, pc) == 2
@@ -216,9 +216,9 @@ function f(x)
216216
return x*x
217217
end
218218
frame = JuliaInterpreter.enter_call(f, 3)
219-
@test JuliaInterpreter.linenumber(frame, JuliaInterpreter.JuliaProgramCounter(1)) == defline + 1
220-
@test JuliaInterpreter.linenumber(frame, JuliaInterpreter.JuliaProgramCounter(3)) == defline + 4
221-
@test JuliaInterpreter.linenumber(frame, JuliaInterpreter.JuliaProgramCounter(5)) == defline + 6
219+
@test whereis(frame, JuliaInterpreter.JuliaProgramCounter(1))[2] == defline + 1
220+
@test whereis(frame, JuliaInterpreter.JuliaProgramCounter(3))[2] == defline + 4
221+
@test whereis(frame, JuliaInterpreter.JuliaProgramCounter(5))[2] == defline + 6
222222

223223
# issue #28
224224
let a = ['0'], b = ['a']

0 commit comments

Comments
 (0)