Skip to content

Commit d363d89

Browse files
authored
Recursive %include from client side (#39)
This makes simple recursive uses of include(path) work for string literal values of `path` by statically resolving all such includes on the client side and passing the big composite expression which results to the server.
1 parent 150e272 commit d363d89

File tree

7 files changed

+123
-61
lines changed

7 files changed

+123
-61
lines changed

src/client.jl

Lines changed: 97 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,51 @@ function simple_macro_expand!(func, ex, macro_name)
3232
return ex
3333
end
3434

35+
# Parse all top level code from `path`, using a file:// URI as the file name.
36+
function parseall_with_file_urls(path)
37+
path = abspath(path)
38+
text = read(path, String)
39+
# Some rough heuristics to construct a file URI. This gives us
40+
# a place to put the host name.
41+
if !startswith(path, '/')
42+
path = '/'*path
43+
end
44+
if Sys.iswindows()
45+
path = replace(path, '\\'=>'/')
46+
end
47+
path_uri = "file://$(gethostname())$path"
48+
return VERSION >= v"1.6" ?
49+
Meta.parseall(text, filename=path_uri) :
50+
Base.parse_input_line(text, filename=path_uri)
51+
end
52+
53+
# Replace simple occurances of `include(path)` at top level and module scope
54+
# when `path` is a string literal.
55+
function replace_includes!(ex, parentdir)
56+
if Meta.isexpr(ex, :call) && ex.args[1] == :include
57+
if length(ex.args) == 2 && ex.args[2] isa AbstractString
58+
p = joinpath(parentdir, ex.args[2])
59+
inc_ex = parseall_with_file_urls(p)
60+
replace_includes!(inc_ex, dirname(p))
61+
return inc_ex
62+
else
63+
error("Path in expression `$ex` must be a literal string to work with `%include`")
64+
end
65+
elseif Meta.isexpr(ex, :toplevel)
66+
map!(e->replace_includes!(e, parentdir), ex.args, ex.args)
67+
elseif Meta.isexpr(ex, :module)
68+
map!(e->replace_includes!(e, parentdir), ex.args[3].args, ex.args[3].args)
69+
end
70+
return ex
71+
end
72+
73+
# Parse the code in `path` and recursively replace occurances of
74+
# `include(path)` with the parsed code from that path.
75+
function parse_and_replace_includes(path)
76+
path = abspath(path)
77+
ex = replace_includes!(parseall_with_file_urls(path), dirname(path))
78+
end
79+
3580
# Read and verify header bytes on initializing the connection
3681
function verify_header(io, ser_version=Serialization.ser_version)
3782
magic = String(read(io, length(PROTOCOL_MAGIC)))
@@ -257,7 +302,46 @@ function REPL.complete_line(provider::RemoteCompletionProvider,
257302
end
258303

259304
function run_remote_repl_command(conn, out_stream, cmdstr)
260-
ensure_connected!(conn) do
305+
# Compute command
306+
magic = match_magic_syntax(cmdstr)
307+
if isnothing(magic)
308+
# Normal remote evaluation
309+
ex = parse_input(cmdstr)
310+
311+
ex = simple_macro_expand!(ex, Symbol("@remote")) do clientside_ex
312+
try
313+
x = Main.eval(clientside_ex)
314+
if x === Base.stdout
315+
# The local stdout cannot be serialized in any sensible way,
316+
# but we store a placeholder for it which will be transformed
317+
# into a serverside approximation of the client stream.
318+
return STDOUT_PLACEHOLDER
319+
else
320+
# Any expressions wrapped in `@remote` need to be executed
321+
# on the client and wrapped in a QuoteNode to prevent them
322+
# being eval'd again in the expression on the server side.
323+
QuoteNode(x)
324+
end
325+
catch _
326+
error("Error while evaluating `@remote($clientside_ex)` before passing to the server")
327+
end
328+
end
329+
330+
cmd = (:eval, ex)
331+
else
332+
# Magic prefixes
333+
if magic[1] == "?"
334+
# Help mode
335+
cmd = (:help, magic[2])
336+
elseif magic[1] == "%module"
337+
mod_ex = Meta.parse(magic[2])
338+
cmd = (:in_module, mod_ex)
339+
elseif magic[1] == "%include"
340+
cmd = (:eval, parse_and_replace_includes(magic[2]))
341+
end
342+
end
343+
344+
messageid, value = ensure_connected!(conn) do
261345
# Set terminal properties for formatting result
262346
display_props = Dict(
263347
:displaysize=>displaysize(out_stream),
@@ -267,71 +351,23 @@ function run_remote_repl_command(conn, out_stream, cmdstr)
267351
# TODO breaking change - send these as part of :eval, perhaps ?
268352
send_and_receive(conn, (:display_properties, display_props), read_response=false)
269353

270-
# Send actual command
271-
magic = match_magic_syntax(cmdstr)
272-
if isnothing(magic)
273-
# Normal remote evaluation
274-
ex = parse_input(cmdstr)
275-
276-
ex = simple_macro_expand!(ex, Symbol("@remote")) do clientside_ex
277-
try
278-
x = Main.eval(clientside_ex)
279-
if x === Base.stdout
280-
# The local stdout cannot be serialized in any sensible way,
281-
# but we store a placeholder for it which will be transformed
282-
# into a serverside approximation of the client stream.
283-
return STDOUT_PLACEHOLDER
284-
else
285-
# Any expressions wrapped in `@remote` need to be executed
286-
# on the client and wrapped in a QuoteNode to prevent them
287-
# being eval'd again in the expression on the server side.
288-
QuoteNode(x)
289-
end
290-
catch _
291-
error("Error while evaluating `@remote($clientside_ex)` before passing to the server")
292-
end
293-
end
354+
send_and_receive(conn, cmd)
355+
end
294356

295-
cmd = (:eval, ex)
296-
else
297-
# Magic prefixes
298-
if magic[1] == "?"
299-
# Help mode
300-
cmd = (:help, magic[2])
301-
elseif magic[1] == "%module"
302-
mod_ex = Meta.parse(magic[2])
303-
cmd = (:in_module, mod_ex)
304-
elseif magic[1] == "%include"
305-
path = abspath(magic[2])
306-
text = read(path, String)
307-
# Some rough heuristics to construct a file URI. This gives us
308-
# a place to put the host name.
309-
if !startswith(path, '/')
310-
path = '/'*path
311-
end
312-
if Sys.iswindows()
313-
path = replace(path, '\\'=>'/')
314-
end
315-
path_uri = "file://$(gethostname())$path"
316-
cmd = (:eval, :(Base.include_string(@__MODULE__, $text, $path_uri)))
357+
result_for_display = nothing
358+
if messageid in (:in_module, :eval_result, :help_result, :error)
359+
if !isnothing(value)
360+
if messageid != :eval_result || !REPL.ends_with_semicolon(cmdstr)
361+
result_for_display = Text(value)
317362
end
318363
end
319-
messageid, value = send_and_receive(conn, cmd)
320-
result_for_display = nothing
321-
if messageid in (:in_module, :eval_result, :help_result, :error)
322-
if !isnothing(value)
323-
if messageid != :eval_result || !REPL.ends_with_semicolon(cmdstr)
324-
result_for_display = Text(value)
325-
end
326-
end
327-
if messageid == :in_module
328-
conn.in_module = mod_ex
329-
end
330-
else
331-
@error "Unexpected response from server" messageid
364+
if messageid == :in_module
365+
conn.in_module = mod_ex
332366
end
333-
return result_for_display
367+
else
368+
@error "Unexpected response from server" messageid
334369
end
370+
return result_for_display
335371
end
336372

337373
remote_eval_and_fetch(::Nothing, ex) = error("No remote connection is active")

test/runtests.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,17 @@ try
181181
path = joinpath(@__DIR__, "to_include.jl")
182182
@test runcommand("%include $path") == "12345"
183183
@test runcommand("var_in_included_file") == "12345"
184+
subinclude1_url = runcommand("var_in_subinclude1")
185+
@test startswith(subinclude1_url, "\"file://$(gethostname())")
186+
@test endswith(subinclude1_url, "subincludes/subinclude1.jl\"")
187+
subinclude2_url = runcommand("var_in_subinclude2")
188+
@test startswith(subinclude2_url, "\"file://$(gethostname())")
189+
@test endswith(subinclude2_url, "subincludes/subinclude2.jl\"")
190+
subinclude3_url = runcommand("IncludedModule.var_in_subinclude3")
191+
@test startswith(subinclude3_url, "\"file://$(gethostname())")
192+
@test endswith(subinclude3_url, "subincludes/subinclude3.jl\"")
193+
194+
@test_throws ErrorException runcommand("%include $(joinpath(@__DIR__, "to_include_bad_path.jl"))")
184195

185196
# Test the @remote macro
186197
Main.eval(:(clientside_var = 0:41))

test/subincludes/subinclude1.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
var_in_subinclude1 = @__FILE__
2+
3+
include("subinclude2.jl")

test/subincludes/subinclude2.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
var_in_subinclude2 = @__FILE__

test/subincludes/subinclude3.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
var_in_subinclude3 = @__FILE__

test/to_include.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1+
include("subincludes/subinclude1.jl")
2+
3+
module IncludedModule
4+
include("subincludes/subinclude3.jl")
5+
end
6+
17
var_in_included_file = 12345

test/to_include_bad_path.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
using Random
2+
3+
some_path = "$(randstring()).jl"
4+
include(some_path)

0 commit comments

Comments
 (0)