Skip to content

Commit

Permalink
add replblock
Browse files Browse the repository at this point in the history
  • Loading branch information
mortenpi committed Jul 5, 2024
1 parent d273c53 commit 1bd5123
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 5 deletions.
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ authors = ["Morten Piibeleht <[email protected]> and contributors"]
version = "0.0.1"

[deps]
#Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
IOCapture = "b5f81e59-6552-4d32-b1f0-c071b021bf89"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

Expand Down
1 change: 1 addition & 0 deletions src/CodeEvaluation.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module CodeEvaluation
using IOCapture: IOCapture
using REPL: REPL
#using Base64: stringmime

include("parseblock.jl")
include("sandbox.jl")
Expand Down
2 changes: 1 addition & 1 deletion src/parseblock.jl
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function parseblock(
else
_update_linenumbernodes!(expr, linenumbernode.file, linenumbernode.line)
end
results[i] = ParsedExpression(expr, results[i][2])
results[i] = ParsedExpression(expr, results[i].code)
end
end
results
Expand Down
116 changes: 113 additions & 3 deletions src/replblock.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,125 @@
struct CodeBlock
input::Bool
code::String
end

"""
struct REPLResult
"""
struct REPLResult1 end
struct REPLResult
sandbox::Sandbox
blocks::Vector{CodeBlock}
_code::AbstractString
_source_exprs::Vector{Any}
end

function join_to_string(result::REPLResult)
out = IOBuffer()
for block in result.blocks
println(out, block.code)
end
return String(take!(out))
end

"""
CodeEvaluation.repl!(sandbox::Sandbox, code::AbstractString) -> AbstractValue
CodeEvaluation.replblock!(sandbox::Sandbox, code::AbstractString) -> REPLResult
Evaluates the code in a special REPL-mode, where `code` gets split up into expressions,
each of which gets evaluated one by one. The output is a string representing what this
would look like if each expression had been evaluated in the REPL as separate commands.
"""
function repl!(sandbox::Sandbox, code::AbstractString)
function replblock!(
sandbox::Sandbox, code::AbstractString;
color::Bool=true,
post_process_inputs = identity,
)
exprs = parseblock(
code;
keywords = false,
# line unused, set to 0
linenumbernode = LineNumberNode(0, "REPL"),
)
codeblocks = CodeBlock[]
source_exprs = map(exprs) do pex
input = post_process_inputs(pex.code)
result = evaluate!(sandbox, pex.expr; color, softscope=true, setans = true)
# Add the input and output to the codeblocks, if appropriate.
if !isempty(input)
push!(codeblocks, CodeBlock(true, _prepend_prompt(input)))
end
# Determine the output string and add to codeblocks
object_repl_repr = let buffer = IOContext(IOBuffer(), :color=>color)
if !result.error
hide = REPL.ends_with_semicolon(input)
_result_to_string(buffer, hide ? nothing : result.value)
else
_error_to_string(buffer, result.value, result.backtrace)
end
end
# Construct the full output. We have to prepend the stdout/-err to the
# output first, and then finally render the returned object.
out = IOBuffer()
print(out, result.output) # stdout and stderr from the evaluation
if !isempty(input) && !isempty(object_repl_repr)
print(out, object_repl_repr, "\n")
end
outstr = _remove_sandbox_from_output(sandbox, String(take!(out)))
push!(codeblocks, CodeBlock(false, outstr))
return (;
expr = pex,
result,
input,
outstr,
)
end
return REPLResult(sandbox, codeblocks, code, source_exprs)
end

# Replace references to gensym'd module with Main
function _remove_sandbox_from_output(sandbox::Sandbox, str::AbstractString)
replace(str, Regex(("(Main\\.)?$(nameof(sandbox))")) => "Main")
end

function _prepend_prompt(input::AbstractString)
prompt = "julia> "
padding = " "^length(prompt)
out = IOBuffer()
for (n, line) in enumerate(split(input, '\n'))
line = rstrip(line)
println(out, n == 1 ? prompt : padding, line)
end
rstrip(String(take!(out)))
end

function _result_to_string(buffer::IO, value::Any)
if value !== nothing
Base.invokelatest(
show,
IOContext(buffer, :limit => true),
MIME"text/plain"(),
value
)
end
return _sanitise(buffer)
end

function _error_to_string(buffer::IO, e::Any, bt)
# Remove unimportant backtrace info.
bt = _remove_common_backtrace(bt, backtrace())
# Remove everything below the last eval call (which should be the one in IOCapture.capture)
index = findlast(ptr -> Base.ip_matches_func(ptr, :eval), bt)
bt = (index === nothing) ? bt : bt[1:(index - 1)]
# Print a REPL-like error message.
print(buffer, "ERROR: ")
Base.invokelatest(showerror, buffer, e, bt)
return _sanitise(buffer)
end

# Strip trailing whitespace from each line and return resulting string
function _sanitise(buffer::IO)
out = IOBuffer()
for line in eachline(seekstart(Base.unwrapcontext(buffer)[1]))
println(out, rstrip(line))
end
return rstrip(String(take!(out)), '\n')
end
34 changes: 34 additions & 0 deletions src/sandbox.jl
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,37 @@ function evaluate!(
expr,
)
end

#=
"""
"""
function show_plain(result::Result; io_context::AbstractVector = [])
_check_io_context_value(io_context)
context = isempty(io_context) ? nothing : only(io_context)
mime = MIME"text/plain"()
s = stringmime(mime, result.value, context = context)
return replace(s, Regex(("(Main\\.)?$(nameof(mod))")) => "Main")
end
function _check_io_context_value(io_context::AbstractVector)
if any(x -> !isa(x, Pair{Symbol,<:Any}), io_context)
throw(ArgumentError("`io_context` must be a `Vector` of `Pair{Symbol,<:Any}`."))
end
end
=#

function _remove_common_backtrace(bt, reference_bt = backtrace())
cutoff = nothing
# We'll start from the top of the backtrace (end of the array) and go down, checking
# if the backtraces agree
for ridx in 1:length(bt)
# Cancel search if we run out the reference BT or find a non-matching one frames:
if ridx > length(reference_bt) || bt[length(bt) - ridx + 1] != reference_bt[length(reference_bt) - ridx + 1]
cutoff = length(bt) - ridx + 1
break
end
end
# It's possible that the loop does not find anything, i.e. that all BT elements are in
# the reference_BT too. In that case we'll just return an empty BT.
bt[1:(cutoff === nothing ? 0 : cutoff)]
end
25 changes: 24 additions & 1 deletion test/parseblock.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
@testset "basic" begin
let exprs = CodeEvaluation.parseblock("")
@test isa(exprs, Vector{CodeEvaluation.ParsedExpression})
@test isempty(exprs)
end
let exprs = CodeEvaluation.parseblock("0")
@test isa(exprs, Vector{CodeEvaluation.ParsedExpression})
@test length(exprs) == 1
let expr = exprs[1]
@test expr.expr == 0
@test expr.code == "0\n" # TODO: trailing newline?
end
end
let exprs = CodeEvaluation.parseblock("40 + 2")
@test isa(exprs, Vector{CodeEvaluation.ParsedExpression})
@test length(exprs) == 1
let expr = exprs[1]
@test expr.expr == :(40 + 2)
@test expr.code == "40 + 2\n" # TODO: trailing newline?
end
end
end

@testset "complex" begin
exprs = CodeEvaluation.parseblock(
"""
x += 3
Expand All @@ -7,7 +30,7 @@
"""
)
@test isa(exprs, Vector{CodeEvaluation.ParsedExpression})
@test length(exprs) === 3
@test length(exprs) == 3

let expr = exprs[1]
@test expr.expr isa Expr
Expand Down
121 changes: 121 additions & 0 deletions test/replblock.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
@testset "replblock! - basic" begin
sb = CodeEvaluation.Sandbox()
let r = CodeEvaluation.replblock!(sb, "nothing")
@test r.sandbox === sb
@test length(r.blocks) == 2
let b = r.blocks[1]
@test b.input
@test b.code == "julia> nothing"
end
let b = r.blocks[2]
@test !b.input
@test b.code == ""
end

@test CodeEvaluation.join_to_string(r) == """
julia> nothing
"""
end
let r = CodeEvaluation.replblock!(sb, "40 + 2")
@test r.sandbox === sb
@test length(r.blocks) == 2
let b = r.blocks[1]
@test b.input
@test b.code == "julia> 40 + 2"
end
let b = r.blocks[2]
@test !b.input
@test b.code == "42\n"
end
@test CodeEvaluation.join_to_string(r) == """
julia> 40 + 2
42
"""
end
let r = CodeEvaluation.replblock!(sb, "println(\"...\")")
@test r.sandbox === sb
@test length(r.blocks) == 2
let b = r.blocks[1]
@test b.input
@test b.code == "julia> println(\"...\")"
end
let b = r.blocks[2]
@test !b.input
@test b.code == "...\n"
end
@test CodeEvaluation.join_to_string(r) == """
julia> println("...")
...
"""
end
end

@testset "replblock! - multiple expressions" begin
sb = CodeEvaluation.Sandbox()
r = CodeEvaluation.replblock!(sb, """
x = 2
x += 2
x ^ 2
""")
@test length(r.blocks) == 6
let b = r.blocks[1]
@test b.input
@test b.code == "julia> x = 2"
end
let b = r.blocks[2]
@test !b.input
@test b.code == "2\n"
end
let b = r.blocks[3]
@test b.input
@test b.code == "julia> x += 2"
end
let b = r.blocks[4]
@test !b.input
@test b.code == "4\n"
end
let b = r.blocks[5]
@test b.input
@test b.code == "julia> x ^ 2"
end
let b = r.blocks[6]
@test !b.input
@test b.code == "16\n"
end

@test CodeEvaluation.join_to_string(r) == """
julia> x = 2
2
julia> x += 2
4
julia> x ^ 2
16
"""
end

@testset "replblock! - output & results" begin
sb = CodeEvaluation.Sandbox()
r = CodeEvaluation.replblock!(sb, """
print(stdout, "out"); print(stderr, "err"); 42
""")
@test length(r.blocks) == 2
let b = r.blocks[1]
@test b.input
@test b.code == "julia> print(stdout, \"out\"); print(stderr, \"err\"); 42"
end
let b = r.blocks[2]
@test !b.input
@test b.code == "outerr42\n"
end
@test CodeEvaluation.join_to_string(r) == """
julia> print(stdout, "out"); print(stderr, "err"); 42
outerr42
"""
end
3 changes: 3 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ using Test
@testset "codeblock!" begin
include("codeblock.jl")
end
@testset "replblock!" begin
include("replblock.jl")
end
end
17 changes: 17 additions & 0 deletions test/sandbox.jl
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,20 @@ end
@test r.output == ""
end
end

#=
@testset "show_plain" begin
sb = CodeEvaluation.Sandbox()
let r = CodeEvaluation.evaluate!(sb, :(40 + 2))
@test CodeEvaluation.show_plain(r) == "42"
end
let r = CodeEvaluation.evaluate!(sb, quote
module X
function foo end
end
X.foo
end)
@test CodeEvaluation.show_plain(r) == "42"
end
end
=#

0 comments on commit 1bd5123

Please sign in to comment.