Skip to content

Commit 0d4f065

Browse files
mgyoo86claude
andauthored
(feat): Add @continue N command (#169)
* feat: add `@continue N` to skip N-1 hits and stop at Nth Wraps existing @cond (if any) with a countdown closure. Only counts hits where @cond is true (IDE model), restoring the original condition once the counter is exhausted. * test: update terminal regression outputs for @continue N help text * test: add @continue N tests with value verification - continue_n: @continue 3 without condition, verify i==4 - continue_n_cond: @continue 3 with @cond i%2==0, verify i==6 * test: regenerate outputs for Julia 1.7-1.11 Regenerated f.multiout and g.multiout for help text change, and added continue_n/continue_n_cond snapshots for @continue N tests. * test: add Julia 1.6 outputs (copied from 1.7) Copied from 1.7 outputs since TerminalRegressionTests fails to resolve locally on 1.6. All existing 1.6 outputs are identical to 1.7 (except toplevel.multiout), so the copy should be correct. To be verified by CI. * fix: improve @continue N error message with validation Consolidate input validation with `string(n) != rest` to catch trailing garbage (e.g. "5 1", "3.5"), and show a clearer error message with "Invalid usage." highlighted in red. * docs: add description for @continue N command in README * fix: enhance @continue N validation and handle single continuation case * style: fix Runic formatting for indexing expression * refactor: fold plain @continue into @continue N branch and rename prev Address PR review feedback: - Merge `@continue` and `@continue N` into a single branch with n=1 fast-path for the plain case - Rename captured variable `prev` to `orig_cond` for clarity Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: restore stale @continue N countdowns on session end Replace the anonymous closure with a CountdownCond callable struct that tracks the session generation at creation time. When a session ends prematurely (error, @abort), SESSION_GEN is bumped and the countdown self-heals on next evaluation by restoring the original @cond. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix format * refactor: move SESSION_GEN counter into Session struct as generation field Per reviewer feedback, the session generation counter belongs inside the Session struct for correctness with potential future multi-session support, rather than as a global Ref. * refactor: address reviewer feedback on @continue N parsing - Simplify @continue guard to startswith(sline, r"@continue\b") - Relax input validation: accept any parseable positive Int (e.g. +3, 03) - Remove skip message println for consistency with other REPL commands - Regenerate test outputs for all Julia versions (1.6-1.12) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bf378b2 commit 0d4f065

31 files changed

+1875
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ infil> ?
142142
143143
• @continue: Continue to the next infiltration point or exit (shortcut: Ctrl-D).
144144
145+
• @continue N: Continue and stop at the Nth hit of this infiltration point. If a `@cond` is active, only hits where the condition evaluates to true are counted.
146+
145147
• @doc symbol: Get help for symbol (same as in the normal Julia REPL).
146148
147149
• @exit: Stop infiltrating for the remainder of this session and exit.

src/Infiltrator.jl

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,16 +211,58 @@ Enable or disable the check for safe REPL mode switching. May result in a non-fu
211211
toggle_async_check(enabled) = CHECK_TASK[] = enabled
212212
const CHECK_TASK = Ref{Bool}(true)
213213

214+
# Callable struct for @continue N countdown conditions.
215+
# Stores session generation at creation time; if the session has changed
216+
# (error, @abort, etc.), the countdown self-heals by restoring the original
217+
# condition on next evaluation.
218+
struct CountdownCond
219+
counter::Ref{Int}
220+
orig_cond::Any
221+
spot::Tuple{String, Int}
222+
session_gen::Int
223+
end
224+
function (c::CountdownCond)(_locals)
225+
cs = getfield(store, :conditions)
226+
# Stale check: session ended since this countdown was created
227+
if c.session_gen != getfield(store, :generation)
228+
if isnothing(c.orig_cond)
229+
delete!(cs, c.spot)
230+
return true
231+
else
232+
cs[c.spot] = c.orig_cond
233+
return c.orig_cond(_locals)
234+
end
235+
end
236+
# Check existing @cond first; skip without counting if false
237+
if !isnothing(c.orig_cond) && !c.orig_cond(_locals)
238+
return false
239+
end
240+
# @cond satisfied (or absent): decrement counter
241+
if c.counter[] > 0
242+
c.counter[] -= 1
243+
return false
244+
end
245+
# Counter exhausted: restore original @cond and stop
246+
if isnothing(c.orig_cond)
247+
delete!(cs, c.spot)
248+
else
249+
cs[c.spot] = c.orig_cond
250+
end
251+
return true
252+
end
253+
214254
mutable struct Session
215255
store::Module
216256
exiting::Bool
217257
disabled::Set
218258
conditions::Dict
259+
generation::Int
219260
function Session(exiting, disabled, conditions)
220261
session = new()
221262
session.exiting = exiting
222263
session.disabled = disabled
223264
session.conditions = conditions
265+
session.generation = 0
224266
return session
225267
end
226268
end
@@ -313,6 +355,7 @@ end
313355
End this infiltration session (reverts the effect of `@exit` in the `debug>` REPL).
314356
"""
315357
function end_session!(s::Session = store)
358+
setfield!(s, :generation, getfield(s, :generation) + 1)
316359
setfield!(s, :exiting, false)
317360
return nothing
318361
end
@@ -488,6 +531,7 @@ The following commands are special cased:
488531
- `@toggle`: Toggle infiltrating at this `@infiltrate` spot (clear all with `Infiltrator.clear_disabled!()`).
489532
- `@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.
490533
- `@continue`: Continue to the next infiltration point or exit (shortcut: Ctrl-D).
534+
- `@continue N`: Skip this infiltration point N-1 times and stop at the Nth hit. Other `@infiltrate` points still stop immediately.
491535
- `@doc symbol`: Get help for `symbol` (same as in the normal Julia REPL).
492536
- `@exit`: Stop infiltrating for the remainder of this session and exit.
493537
- `@abort`: Stop program execution by throwing an `AbortException`.
@@ -781,7 +825,26 @@ function debugprompt(mod, locals, trace, terminal, repl, ex, bt; nostack = false
781825
LineEdit.transition(s, :abort)
782826
LineEdit.reset_state(s)
783827
return true
784-
elseif sline == "@continue"
828+
elseif startswith(sline, r"@continue\b")
829+
rest = strip(sline[(length("@continue") + 1):end])
830+
n = isempty(rest) ? 1 : tryparse(Int, rest)
831+
if isnothing(n) || n < 1
832+
printstyled(io, "Invalid usage."; color = Base.error_color())
833+
println(io, " Expected: @continue N (where N is a positive integer)")
834+
LineEdit.reset_state(s)
835+
return true
836+
end
837+
# n=1 can exit early
838+
if n == 1
839+
LineEdit.transition(s, :abort)
840+
LineEdit.reset_state(s)
841+
return true
842+
end
843+
spot = (file, fileline)
844+
cs = getfield(store, :conditions)
845+
skip = n - 1
846+
orig_cond = get(cs, spot, nothing)
847+
cs[spot] = CountdownCond(Ref(skip), orig_cond, spot, getfield(store, :generation))
785848
LineEdit.transition(s, :abort)
786849
LineEdit.reset_state(s)
787850
return true
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
++++++++++++++++++++++++++++++++++++++++++++++++++
2+
|1
3+
|Infiltrating <unknown>
4+
|
5+
|infil>
6+
--------------------------------------------------
7+
|A
8+
|AAAAAAAAAAAAAAAAAAAAAA
9+
|
10+
|BBBBBBB
11+
++++++++++++++++++++++++++++++++++++++++++++++++++
12+
|1
13+
|Infiltrating <unknown>
14+
|
15+
|infil> @continue 3
16+
|
17+
|2
18+
|3
19+
|4
20+
|Infiltrating <unknown>
21+
|
22+
|infil>
23+
--------------------------------------------------
24+
|A
25+
|AAAAAAAAAAAAAAAAAAAAAA
26+
|
27+
|BBBBBBBCCCCCCCCCCC
28+
|
29+
|C
30+
|C
31+
|C
32+
|CCCCCCCCCCCCCCCCCCCCCC
33+
|
34+
|BBBBBBB
35+
++++++++++++++++++++++++++++++++++++++++++++++++++
36+
|1
37+
|Infiltrating <unknown>
38+
|
39+
|infil> @continue 3
40+
|
41+
|2
42+
|3
43+
|4
44+
|Infiltrating <unknown>
45+
|
46+
|infil> @exfiltrate i
47+
|Exfiltrating 1 local variable into the safehouse.
48+
|
49+
|infil>
50+
--------------------------------------------------
51+
|A
52+
|AAAAAAAAAAAAAAAAAAAAAA
53+
|
54+
|BBBBBBBCCCCCCCCCCC
55+
|
56+
|C
57+
|C
58+
|C
59+
|CCCCCCCCCCCCCCCCCCCCCC
60+
|
61+
|BBBBBBBCCCCCCCCCCCCC
62+
|CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
63+
|
64+
|BBBBBBB
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
++++++++++++++++++++++++++++++++++++++++++++++++++
2+
|1
3+
|Infiltrating <unknown>
4+
|
5+
|infil>
6+
--------------------------------------------------
7+
|A
8+
|AAAAAAAAAAAAAAAAAAAAAA
9+
|
10+
|BBBBBBB
11+
++++++++++++++++++++++++++++++++++++++++++++++++++
12+
|1
13+
|Infiltrating <unknown>
14+
|
15+
|infil> @cond i % 2 == 0
16+
|Conditionally enabled infiltration at this infiltration point.
17+
|
18+
|infil>
19+
--------------------------------------------------
20+
|A
21+
|AAAAAAAAAAAAAAAAAAAAAA
22+
|
23+
|BBBBBBBCCCCCCCCCCCCCCCC
24+
|CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
25+
|
26+
|BBBBBBB
27+
++++++++++++++++++++++++++++++++++++++++++++++++++
28+
|1
29+
|Infiltrating <unknown>
30+
|
31+
|infil> @cond i % 2 == 0
32+
|Conditionally enabled infiltration at this infiltration point.
33+
|
34+
|infil> @continue 3
35+
|
36+
|2
37+
|3
38+
|4
39+
|5
40+
|6
41+
|Infiltrating <unknown>
42+
|
43+
|infil>
44+
--------------------------------------------------
45+
|A
46+
|AAAAAAAAAAAAAAAAAAAAAA
47+
|
48+
|BBBBBBBCCCCCCCCCCCCCCCC
49+
|CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
50+
|
51+
|BBBBBBBCCCCCCCCCCC
52+
|
53+
|C
54+
|C
55+
|C
56+
|C
57+
|C
58+
|CCCCCCCCCCCCCCCCCCCCCC
59+
|
60+
|BBBBBBB
61+
++++++++++++++++++++++++++++++++++++++++++++++++++
62+
|1
63+
|Infiltrating <unknown>
64+
|
65+
|infil> @cond i % 2 == 0
66+
|Conditionally enabled infiltration at this infiltration point.
67+
|
68+
|infil> @continue 3
69+
|
70+
|2
71+
|3
72+
|4
73+
|5
74+
|6
75+
|Infiltrating <unknown>
76+
|
77+
|infil> @exfiltrate i
78+
|Exfiltrating 1 local variable into the safehouse.
79+
|
80+
|infil>
81+
--------------------------------------------------
82+
|A
83+
|AAAAAAAAAAAAAAAAAAAAAA
84+
|
85+
|BBBBBBBCCCCCCCCCCCCCCCC
86+
|CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
87+
|
88+
|BBBBBBBCCCCCCCCCCC
89+
|
90+
|C
91+
|C
92+
|C
93+
|C
94+
|C
95+
|CCCCCCCCCCCCCCCCCCCCCC
96+
|
97+
|BBBBBBBCCCCCCCCCCCCC
98+
|CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
99+
|
100+
|BBBBBBB

0 commit comments

Comments
 (0)