Skip to content

Commit

Permalink
Merge pull request #105 from nystrom/master
Browse files Browse the repository at this point in the history
Add `@cond` command
  • Loading branch information
pfitzseb authored Feb 12, 2024
2 parents 4fcad5f + 9b3b413 commit 86a3f2d
Show file tree
Hide file tree
Showing 27 changed files with 2,287 additions and 38 deletions.
59 changes: 44 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ julia> function f(x)
end
f (generic function with 1 method)

julia> f([1,2,3])
julia> f([1,2,3,4,5,6,7,8,9,10])
Infiltrating f(x::Vector{Int64})
at REPL[10]:5

Expand All @@ -152,6 +152,9 @@ infil> ?

@toggle: Toggle infiltrating at this @infiltrate spot (clear all with Infiltrator.clear_disabled!()).

@cond expr: Infiltrate at this @infiltrate spot only if <expr> evaluates to true (clear all with
Infiltrator.clear_conditions!()).

@continue: Continue to the next infiltration point or exit (shortcut: Ctrl-D).

@doc symbol: Get help for symbol (same as in the normal Julia REPL).
Expand All @@ -161,7 +164,7 @@ infil> ?
infil> @locals
- out::Vector{Any} = Any[2]
- i::Int64 = 1
- x::Vector{Int64} = [1, 2, 3]
- x::Vector{Int64} = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

infil> 0//0
ERROR: ArgumentError: invalid rational: zero(Int64)//zero(Int64)
Expand All @@ -183,38 +186,64 @@ Disabled infiltration at this infiltration point.
infil> @toggle
Enabled infiltration at this infiltration point.

infil> @cond i > 5
Conditionally enabled infiltration at this infiltration point.

infil> @continue

Infiltrating f(x::Vector{Int64})
at REPL[10]:5

infil> i
6

infil> intermediate = copy(out)
2-element Vector{Any}:
2
4
6-element Vector{Any}:
2
4
6
8
10
12

infil> @exfiltrate intermediate x
Exfiltrating 2 local variables into the safehouse.

infil> @exit

3-element Vector{Any}:
2
4
6
10-element Vector{Any}:
2
4
6
8
10
12
14
16
18
20


julia> safehouse.intermediate
2-element Vector{Any}:
2
4
6-element Vector{Any}:
2
4
6
8
10
12

julia> @withstore begin
x = 23
x .* intermediate
end
2-element Vector{Int64}:
46
92
6-element Vector{Int64}:
46
92
138
184
230
276
```

## Related projects
Expand Down
70 changes: 66 additions & 4 deletions src/Infiltrator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ mutable struct Session
store::Module
exiting::Bool
disabled::Set
conditions::Dict
end
function Base.show(io::IO, s::Session)
n = length(get_store_names(s))
Expand Down Expand Up @@ -166,7 +167,7 @@ Also see [`clear_store!`](@ref), [`set_store!`](@ref), and [`@withstore`](@ref)
safehouse-related functionality.
"""
@doc store_docstring
const store = Session(Module(), false, Set())
const store = Session(Module(), false, Set(), Dict())
@doc store_docstring
const safehouse = store
@doc store_docstring
Expand Down Expand Up @@ -202,6 +203,16 @@ function clear_disabled!(s = store)
return nothing
end

"""
clear_conditions!(s = safehouse)
Clear all conditional infiltration points.
"""
function clear_conditions!(s = store)
empty!(getfield(s, :conditions))
return nothing
end

"""
end_session!(s = safehouse)
Expand Down Expand Up @@ -240,7 +251,12 @@ function start_prompt(mod, locals, file, fileline;
lock(INFILTRATION_LOCK[])
try
getfield(store, :exiting) && return
(file, fileline) in getfield(store, :disabled) && return
spot = (file, fileline)
spot in getfield(store, :disabled) && return
cs = getfield(store, :conditions)
f = get(cs, spot, nothing)
# Use invokelatest because we may have added f before returning to the REPL.
isnothing(f) || Base.invokelatest(f, locals) || return

if terminal === nothing || repl === nothing
active_repl_backend = nothing
Expand Down Expand Up @@ -339,6 +355,7 @@ The following commands are special cased:
- `@exfiltrate`: Save all local variables into the store. `@exfiltrate x y` saves `x` and `y`;
this variant can also exfiltrate variables defined in the `infil>` REPL.
- `@toggle`: Toggle infiltrating at this `@infiltrate` spot (clear all with `Infiltrator.clear_disabled!()`).
- `@cond expr`: Infiltrate at this `@infiltrate` spot only if `expr` evaluates to true (clear all with `Infiltrator.clear_conditions!()`). Only local variables can be accessed here.
- `@continue`: Continue to the next infiltration point or exit (shortcut: Ctrl-D).
- `@doc symbol`: Get help for `symbol` (same as in the normal Julia REPL).
- `@exit`: Stop infiltrating for the remainder of this session and exit (on Julia versions prior to
Expand Down Expand Up @@ -505,15 +522,61 @@ function debugprompt(mod, locals, trace, terminal, repl, nostack = false; file,
elseif sline == "@toggle"
spot = (file, fileline)
ds = getfield(store, :disabled)
cs = getfield(store, :conditions)
if spot in ds
delete!(ds, spot)
println(io, "Enabled infiltration at this infiltration point.\n")
if haskey(cs, spot)
println(io, "Conditionally enabled infiltration at this infiltration point.\n")
else
println(io, "Enabled infiltration at this infiltration point.\n")
end
else
push!(ds, spot)
println(io, "Disabled infiltration at this infiltration point.\n")
end
LineEdit.reset_state(s)
return true
elseif startswith(sline, "@cond")
spot = (file, fileline)
ds = getfield(store, :disabled)
cs = getfield(store, :conditions)
rest = lstrip(sline[6:end])

try
expr = Base.parse_input_line(rest)
@assert expr.head == :toplevel
# Unwrap the :toplevel node and replace with a :block
expr = Expr(:block, expr.args...)
# Wrap the expr in a closure.
# We need to gensym a new name to avoid this error:
# cannot declare #1#2 constant; it already has a value
# Pass a locals dict into the function so we access the locals at the
# next infiltration point rather than capturing the locals here.
fname = gensym(:cond)
locals_dict = gensym(:locals)
expr = quote
function $(fname)($locals_dict)
$(
( Expr(:(=), x, Expr(:ref, locals_dict, Expr(:call, Symbol, string(x))))
for x in keys(locals)
)...
)
$(expr)
end
$fname
end
# Eval and save in the dict.
result = Core.eval(evalmod, expr)
cs[spot] = result
# Remove the spot from the disabled set.
delete!(ds, spot)
println(io, "Conditionally enabled infiltration at this infiltration point.\n")
LineEdit.reset_state(s)
return true
catch err
# If we get an error, just punt to the eval code to handle the error.
line = rest
end
elseif sline == "@exit"
setfield!(store, :exiting, true)
if !REPL_HOOKED[]
Expand Down Expand Up @@ -726,7 +789,6 @@ end

function interpret(expr, evalmod)
out = Core.eval(evalmod, :(ans = $(expr)))

return out
end

Expand Down
4 changes: 3 additions & 1 deletion test/fixtures/toplevel-fixture.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Infiltrator
println(2+2)
if VERSION > v"1.7"
println(2+2)
end
@infiltrate

"success"
4 changes: 4 additions & 0 deletions test/generate.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
for version in ["1.1", "1.6", "1.7", "1.8", "1.9", "1.10"]
println("Generating outputs with Julia v$version")
run(addenv(`julia +$version --project=$(dirname(@__DIR__)) -e 'using Pkg; Pkg.test()'`, "INFILTRATOR_CREATE_TEST" => 1))
end
Loading

0 comments on commit 86a3f2d

Please sign in to comment.