Skip to content

Commit 0c2430a

Browse files
REPL: use an Event to orchestrate prompt entry in precompile script
1 parent b332132 commit 0c2430a

File tree

3 files changed

+64
-66
lines changed

3 files changed

+64
-66
lines changed

stdlib/REPL/src/LineEdit.jl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,11 @@ mutable struct MIState
8585
line_modify_lock::Base.ReentrantLock
8686
hint_generation_lock::Base.ReentrantLock
8787
n_keys_pressed::Int
88+
# Optional event that gets notified each time the prompt is ready for input
89+
prompt_ready_event::Union{Nothing, Base.Event}
8890
end
8991

90-
MIState(i, mod, c, a, m) = MIState(i, mod, mod, c, a, m, String[], 0, Char[], 0, :none, :none, Channel{Function}(), Base.ReentrantLock(), Base.ReentrantLock(), 0)
92+
MIState(i, mod, c, a, m) = MIState(i, mod, mod, c, a, m, String[], 0, Char[], 0, :none, :none, Channel{Function}(), Base.ReentrantLock(), Base.ReentrantLock(), 0, nothing)
9193

9294
const BufferLike = Union{MIState,ModeState,IOBuffer}
9395
const State = Union{MIState,ModeState}
@@ -2977,6 +2979,10 @@ function prompt!(term::TextTerminal, prompt::ModalInterface, s::MIState = init_s
29772979
enable_bracketed_paste(term)
29782980
try
29792981
activate(prompt, s, term, term)
2982+
# Notify that prompt is ready for input
2983+
if s.prompt_ready_event !== nothing
2984+
notify(s.prompt_ready_event)
2985+
end
29802986
old_state = mode(s)
29812987
# spawn this because the main repl task is sticky (due to use of @async and _wait2)
29822988
# and we want to not block typing when the repl task thread is busy

stdlib/REPL/src/REPL.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,14 +779,18 @@ mutable struct LineEditREPL <: AbstractREPL
779779
interface::ModalInterface
780780
backendref::REPLBackendRef
781781
frontend_task::Task
782+
# Optional event to notify when the prompt is ready (used by precompilation)
783+
prompt_ready_event::Union{Nothing, Base.Event}
782784
function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
783785
opts = Options()
784786
opts.hascolor = hascolor
785787
if !hascolor
786788
opts.beep_colors = [""]
787789
end
788-
new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
790+
r = new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
789791
in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
792+
r.prompt_ready_event = nothing
793+
r
790794
end
791795
end
792796
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
@@ -1665,6 +1669,10 @@ function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
16651669
end
16661670
repl.backendref = backend
16671671
repl.mistate = LineEdit.init_state(terminal(repl), interface)
1672+
# Copy prompt_ready_event from repl to mistate (used by precompilation)
1673+
if isdefined(repl, :prompt_ready_event) && repl.prompt_ready_event !== nothing
1674+
repl.mistate.prompt_ready_event = repl.prompt_ready_event
1675+
end
16681676
run_interface(terminal(repl), interface, repl.mistate)
16691677
# Terminate Backend
16701678
put!(backend.repl_channel, (nothing, -1))

stdlib/REPL/src/precompile.jl

Lines changed: 48 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,42 +15,55 @@ finally
1515
end
1616

1717
function repl_workload()
18-
# these are intentionally triggered
18+
# Capture debug output to show if something goes wrong
19+
debug_output = IOBuffer()
20+
21+
# Errors that are intentionally triggered by the script
1922
allowed_errors = [
2023
"BoundsError: attempt to access 0-element Vector{Any} at index [1]",
2124
"MethodError: no method matching f(::$Int, ::$Int)",
2225
"Padding of type", # reinterpret docstring has ERROR examples
2326
]
24-
function check_errors(out)
25-
str = String(out)
26-
if occursin("ERROR:", str) && !any(occursin(e, str) for e in allowed_errors)
27-
@error "Unexpected error (Review REPL precompilation with debug_output on):\n$str" exception=(
28-
Base.PrecompilableError(), Base.backtrace())
29-
exit(1)
27+
28+
function check_output()
29+
str = String(take!(copy(debug_output)))
30+
for line in eachline(IOBuffer(str))
31+
if occursin("ERROR:", line) && !any(e -> occursin(e, line), allowed_errors)
32+
println(stderr, """
33+
========================================================================
34+
ERROR: Unexpected error during REPL precompilation
35+
========================================================================
36+
Debug output:
37+
------------------------------------------------------------------------
38+
""")
39+
println(stderr, str)
40+
println(stderr, "========================================================================")
41+
error("REPL precompilation encountered unexpected error: $line")
42+
end
3043
end
3144
end
32-
## Debugging options
33-
# View the code sent to the repl by setting this to `stdout`
34-
debug_output = devnull # or stdout
3545

3646
CTRL_C = '\x03'
3747
CTRL_D = '\x04'
3848
CTRL_R = '\x12'
3949
UP_ARROW = "\e[A"
4050
DOWN_ARROW = "\e[B"
4151

42-
# This is notified as soon as the first prompt appears
43-
repl_init_event = Base.Event()
44-
repl_init_done_event = Base.Event()
52+
# Event that REPL notifies each time it's ready for input (autoreset so each wait blocks until next notify)
53+
prompt_ready = Base.Event(true)
54+
# Event to signal that REPL.activate has been called
55+
activate_done = Base.Event()
4556

4657
atreplinit() do repl
47-
# Main is closed so we can't evaluate in it, but atreplinit runs at
48-
# a time that repl.mistate === nothing so REPL.activate fails. So do
49-
# it async and wait for the first prompt to know its ready.
58+
# Set the prompt_ready_event on the repl - run_frontend will copy it to mistate
59+
if repl isa REPL.LineEditREPL
60+
repl.prompt_ready_event = prompt_ready
61+
end
62+
# Start async task to wait for first prompt then activate the module
5063
t = @async begin
51-
wait(repl_init_event)
64+
wait(prompt_ready)
5265
REPL.activate(REPL.Precompile; interactive_utils=false)
53-
notify(repl_init_done_event)
66+
notify(activate_done)
5467
end
5568
Base.errormonitor(t)
5669
end
@@ -83,14 +96,6 @@ function repl_workload()
8396
println("done")
8497
"""
8598

86-
JULIA_PROMPT = "julia> "
87-
# The help text for `reinterpret` has example `julia>` prompts in it,
88-
# so use the longer prompt to avoid desychronization.
89-
ACTIVATED_JULIA_PROMPT = "(REPL.Precompile) julia> "
90-
PKG_PROMPT = "pkg> "
91-
SHELL_PROMPT = "shell> "
92-
HELP_PROMPT = "help?> "
93-
9499
tmphistfile = tempname()
95100
write(tmphistfile, """
96101
# time: 2020-10-31 13:16:39 AWST
@@ -120,20 +125,16 @@ function repl_workload()
120125
Base._fd(pts) == rawpts || Base.close_stdio(rawpts)
121126
end
122127
# Prepare a background process to copy output from `ptm` until `pts` is closed
123-
output_copy = Base.BufferStream()
124128
tee = @async try
125129
while !eof(ptm)
126130
l = readavailable(ptm)
127131
write(debug_output, l)
128-
write(output_copy, l)
129132
end
130-
write(debug_output, "\n#### EOF ####\n")
131133
catch ex
132134
if !(ex isa Base.IOError && ex.code == Base.UV_EIO)
133135
rethrow() # ignore EIO on ptm after pts dies
134136
end
135137
finally
136-
close(output_copy)
137138
close(ptm)
138139
end
139140
Base.errormonitor(tee)
@@ -159,46 +160,27 @@ function repl_workload()
159160
redirect_stderr(isopen(orig_stderr) ? orig_stderr : devnull)
160161
end
161162
schedule(repltask)
162-
# wait for the definitive prompt before start writing to the TTY
163-
check_errors(readuntil(output_copy, JULIA_PROMPT, keep=true))
164-
165-
# Switch to the activated prompt
166-
notify(repl_init_event)
167-
wait(repl_init_done_event)
163+
# Wait for the first prompt, then for activate to complete
164+
wait(activate_done)
165+
# Send a newline to get the activated prompt
168166
write(ptm, "\n")
169-
# The prompt prints twice - once for the restatement of the input, once
170-
# to indicate ready for the new prompt.
171-
check_errors(readuntil(output_copy, ACTIVATED_JULIA_PROMPT, keep=true))
172-
check_errors(readuntil(output_copy, ACTIVATED_JULIA_PROMPT, keep=true))
167+
# Wait for the new prompt to be ready
168+
wait(prompt_ready)
173169

174-
write(debug_output, "\n#### REPL STARTED ####\n")
175170
# Input our script
176171
precompile_lines = split(repl_script::String, '\n'; keepempty=false)
177-
curr = 0
178172
for l in precompile_lines
179-
sleep(0.01) # try to let a bit of output accumulate before reading again
180-
curr += 1
181-
# push our input
182-
write(debug_output, "\n#### inputting statement: ####\n$(repr(l))\n####\n")
183-
# If the line ends with a CTRL_C, don't write an extra newline, which would
184-
# cause a second empty prompt. Our code below expects one new prompt per
185-
# input line and can race out of sync with the unexpected second line.
186-
endswith(l, CTRL_C) ? write(ptm, l) : write(ptm, l, "\n")
187-
check_errors(readuntil(output_copy, "\n"))
188-
# wait for the next prompt-like to appear
189-
check_errors(readuntil(output_copy, "\n"))
190-
strbuf = ""
191-
while !eof(output_copy)
192-
strbuf *= String(readavailable(output_copy))
193-
occursin(ACTIVATED_JULIA_PROMPT, strbuf) && break
194-
occursin(PKG_PROMPT, strbuf) && break
195-
occursin(SHELL_PROMPT, strbuf) && break
196-
occursin(HELP_PROMPT, strbuf) && break
197-
sleep(0.01) # try to let a bit of output accumulate before reading again
173+
# If the line ends with a CTRL_C, don't write an extra newline
174+
# CTRL_C cancels input but doesn't print a new prompt, so don't wait
175+
if endswith(l, CTRL_C)
176+
write(ptm, l)
177+
sleep(0.1) # Brief pause to let CTRL_C be processed
178+
else
179+
write(ptm, l, "\n")
180+
# Wait for REPL to signal it's ready for next input
181+
wait(prompt_ready)
198182
end
199-
check_errors(strbuf)
200183
end
201-
write(debug_output, "\n#### COMPLETED - Closing REPL ####\n")
202184
write(ptm, "$CTRL_D")
203185
wait(repltask)
204186
finally
@@ -208,7 +190,9 @@ function repl_workload()
208190
end
209191
wait(tee)
210192
end
211-
write(debug_output, "\n#### FINISHED ####\n")
193+
# Check for any unexpected errors in the output
194+
check_output()
195+
rm(tmphistfile, force=true)
212196
nothing
213197
end
214198

0 commit comments

Comments
 (0)