Skip to content

Commit 93f6473

Browse files
serenity4aviatesk
andauthored
Support external method tables in method evaluation (#680)
* Support evaluation of overlayed methods * Add comment * Bump version * Support more expression types defining the external method table * Attempt a few fixes * Fix 1.12/nightly failures that were present before this PR * Adjust to JuliaLang/julia#58076 * `jl_method_def` actually returned a `Method` already * Export `extract_method_table`, add option to disallow `eval` * Quickly try a nightly fix * Remove unnecessary dependency on Tensors for testing The test that used it doesn't actually need it to test what was intended * Wrap evaluation in try/catch * Use isdefinedglobal * Adjust broken test to have it throw consistently Otherwise, it would fail at the first evaluation and succeed on subsequent ones. * Update comment * Replace `Core.eval` with `getglobal` * Update src/interpret.jl Co-authored-by: Shuhei Kadowaki <[email protected]> --------- Co-authored-by: Cédric Belmant <[email protected]> Co-authored-by: Shuhei Kadowaki <[email protected]>
1 parent 77effa3 commit 93f6473

File tree

6 files changed

+80
-22
lines changed

6 files changed

+80
-22
lines changed

Project.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "JuliaInterpreter"
22
uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
3-
version = "0.9.44"
3+
version = "0.9.45"
44

55
[deps]
66
CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"
@@ -27,8 +27,7 @@ Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
2727
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
2828
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
2929
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
30-
Tensors = "48a634ad-e948-5137-8d70-aa71f2a747f4"
3130
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3231

3332
[targets]
34-
test = ["CassetteOverlay", "DataFrames", "Dates", "DeepDiffs", "Distributed", "FunctionWrappers", "HTTP", "LinearAlgebra", "Logging", "LoopVectorization", "Mmap", "PyCall", "SHA", "SparseArrays", "Tensors", "Test"]
33+
test = ["CassetteOverlay", "DataFrames", "Dates", "DeepDiffs", "Distributed", "FunctionWrappers", "HTTP", "LinearAlgebra", "Logging", "LoopVectorization", "Mmap", "PyCall", "SHA", "SparseArrays", "Test"]

src/interpret.jl

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ evaluate_call!(frame::Frame, call_expr::Expr; kwargs...) = evaluate_call!(finish
313313

314314
# The following come up only when evaluating toplevel code
315315
function evaluate_methoddef(frame::Frame, node::Expr)
316+
mt = extract_method_table(frame, node)
317+
mt !== nothing && return evaluate_overlayed_methoddef(frame, node, mt)
316318
f = node.args[1]
317319
if f isa Symbol || f isa GlobalRef
318320
mod = f isa Symbol ? moduleof(frame) : f.mod
@@ -332,9 +334,34 @@ function evaluate_methoddef(frame::Frame, node::Expr)
332334
length(node.args) == 1 && return f
333335
sig = @lookup(frame, node.args[2])::SimpleVector
334336
body = @lookup(frame, node.args[3])::Union{CodeInfo, Expr}
335-
# branching on https://github.com/JuliaLang/julia/pull/41137
336-
ccall(:jl_method_def, Cvoid, (Any, Ptr{Cvoid}, Any, Any), sig, C_NULL, body, moduleof(frame)::Module)
337-
return f
337+
method = ccall(:jl_method_def, Any, (Any, Ptr{Cvoid}, Any, Any), sig, C_NULL, body, moduleof(frame)::Module)::Method
338+
return method
339+
end
340+
341+
function evaluate_overlayed_methoddef(frame::Frame, node::Expr, mt::MethodTable)
342+
# Overlaying an empty function such as `function f end` is not legal, and `f` must
343+
# already be defined so we don't need to do as much work as in `evaluate_methoddef`.
344+
sig = @lookup(frame, node.args[2])::SimpleVector
345+
body = @lookup(frame, node.args[3])::Union{CodeInfo, Expr}
346+
method = ccall(:jl_method_def, Any, (Any, Any, Any, Any), sig, mt, body, moduleof(frame)::Module)::Method
347+
return method
348+
end
349+
350+
function extract_method_table(frame::Frame, node::Expr; eval = true)
351+
isexpr(node, :method, 3) || return nothing
352+
arg = node.args[1]
353+
isa(arg, MethodTable) && return arg
354+
if !isa(arg, Symbol) && !isa(arg, GlobalRef)
355+
eval || return nothing
356+
value = try Core.eval(moduleof(frame), arg) catch _ nothing end
357+
isa(value, MethodTable) && return value
358+
return nothing
359+
end
360+
mod, name = isa(arg, Symbol) ? (moduleof(frame), arg) : (arg.mod, arg.name)
361+
@invokelatest(isdefinedglobal(mod, name)) || return nothing
362+
value = @invokelatest getglobal(mod, name)
363+
isa(value, MethodTable) && return value
364+
return nothing
338365
end
339366

340367
function do_assignment!(frame::Frame, @nospecialize(lhs), @nospecialize(rhs))
@@ -505,7 +532,7 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
505532
# (https://github.com/JuliaDebug/JuliaInterpreter.jl/issues/591)
506533
elseif istoplevel
507534
if node.head === :method && length(node.args) > 1
508-
evaluate_methoddef(frame, node)
535+
rhs = evaluate_methoddef(frame, node)
509536
elseif node.head === :module
510537
error("this should have been handled by split_expressions")
511538
elseif node.head === :using || node.head === :import || node.head === :export

src/packagedef.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using Base.Meta
22
import Base: +, -, convert, isless, get_world_counter, mapany, ntupleany, invokelatest
33
using Core: CodeInfo, SimpleVector, LineInfoNode, GotoNode, GotoIfNot, ReturnNode,
4-
GeneratedFunctionStub, MethodInstance, NewvarNode, TypeName
4+
GeneratedFunctionStub, MethodInstance, MethodTable, NewvarNode, TypeName
55

66
using UUIDs
77
using Random

src/utils.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,17 @@ function to_function(@nospecialize(x))
2626
end
2727

2828
"""
29-
method = whichtt(tt)
29+
method = whichtt(tt, mt = nothing)
3030
3131
Like `which` except it operates on the complete tuple-type `tt`,
3232
and doesn't throw when there is no matching method.
3333
"""
34-
function whichtt(@nospecialize(tt))
34+
function whichtt(@nospecialize(tt), mt::Union{Nothing, MethodTable} = nothing)
3535
# TODO: provide explicit control over world age? In case we ever need to call "old" methods.
3636
# branch on https://github.com/JuliaLang/julia/pull/44515
37-
# for now, actual code execution doesn't ever need to consider overlayed method table
38-
match, _ = Core.Compiler._findsup(tt, nothing, get_world_counter())
37+
# for now, code execution doesn't have the capability to use an overlayed method table,
38+
# which is meant to be addressed in https://github.com/JuliaDebug/JuliaInterpreter.jl/pull/682.
39+
match, _ = Core.Compiler._findsup(tt, mt, get_world_counter())
3940
match === nothing && return nothing
4041
return match.method
4142
end

test/interpret.jl

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,16 @@ end
414414
f_gf(x) = false ? some_undef_var_zzzzzzz : x
415415
@test @interpret f_gf(2) == 2
416416

417-
function g_gf()
418-
eval(:(z = 2))
419-
return z
417+
gvar = gensym()
418+
@eval function g_gf()
419+
@eval $gvar = 2
420+
return $gvar
421+
end
422+
if VERSION v"1.12-"
423+
@test_throws "`$gvar` not defined" @interpret(g_gf() == 2)
424+
else
425+
@test @interpret(g_gf() == 2)
420426
end
421-
@test @interpret g_gf() == 2
422427

423428
global q_gf = 0
424429
function h_gf()
@@ -462,13 +467,13 @@ file, line = JuliaInterpreter.whereis(fr)
462467
@test line == (@__LINE__() - 4)
463468

464469
# Test path to files in stdlib
465-
fr = JuliaInterpreter.enter_call(Test.eval, 1)
470+
fr = JuliaInterpreter.enter_call(rand)
466471
file, line = JuliaInterpreter.whereis(fr)
467472
@test isfile(file)
468473
@static if VERSION < v"1.12.0-DEV.173"
469474
@test isfile(JuliaInterpreter.getfile(fr.framecode.src.linetable[1]))
470475
end
471-
@test occursin(contractuser(Sys.STDLIB), repr(fr))
476+
@test occursin(joinpath(contractuser(Sys.STDLIB), "Random"), repr(fr))
472477

473478
# Test undef sparam (https://github.com/JuliaDebug/JuliaInterpreter.jl/issues/165)
474479
function foo(x::T) where {T <: AbstractString, S <: AbstractString}
@@ -626,8 +631,7 @@ end
626631

627632
# parametric llvmcall (issues #112 and #288)
628633
module VecTest
629-
using Tensors
630-
Vec{N,T} = NTuple{N,VecElement{T}}
634+
const Vec{N,T} = NTuple{N,VecElement{T}}
631635
# The following test mimic SIMD.jl
632636
const _llvmtypes = Dict{DataType, String}(
633637
Float64 => "double",
@@ -647,16 +651,16 @@ module VecTest
647651
Core.getfield(Base, :llvmcall)($exp, Vec{$N, $T}, Tuple{Vec{$N, $T}, Vec{$N, $T}}, x, y)
648652
end
649653
end
650-
f() = 1.0 * one(Tensor{2,3})
654+
f(a) = vecadd(a, a)
651655
end
652656
let
653657
# NOTE we need to make sure this code block is compiled, since vecadd is generated function,
654658
# but currently `@interpret` doesn't handle a call to generated functions very well
655659
Base.Experimental.@force_compile
656660
a = (VecElement{Float64}(1.0), VecElement{Float64}(2.0))
657661
@test @interpret(VecTest.vecadd(a, a)) == VecTest.vecadd(a, a)
662+
@test @interpret(VecTest.f(a)) == VecTest.f(a)
658663
end
659-
@test @interpret(VecTest.f()) == [1 0 0; 0 1 0; 0 0 1]
660664

661665
# Test exception type for undefined variables
662666
f_undefvar() = s = s + 1

test/toplevel.jl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,3 +606,30 @@ end
606606
end
607607
@test length(modexs) == 2 # FIXME don't use index in tests
608608
end
609+
610+
@testset "External method tables" begin
611+
ex = quote
612+
external_foo() = 1
613+
Base.Experimental.@MethodTable method_table
614+
end
615+
frame = Frame(Toplevel, ex)
616+
JuliaInterpreter.finish!(frame, true)
617+
618+
nmethods_in_overlay() = length(Base.MethodList(Toplevel.method_table).ms)
619+
@test nmethods_in_overlay() == 0
620+
621+
ex = :(Base.Experimental.@overlay method_table external_foo() = 2)
622+
frame = Frame(Toplevel, ex)
623+
JuliaInterpreter.finish!(frame, true)
624+
@test nmethods_in_overlay() == 1
625+
626+
ex = :(Base.Experimental.@overlay $(Toplevel.method_table) external_foo(x; y = 3) = 3 + y)
627+
frame = Frame(Toplevel, ex)
628+
JuliaInterpreter.finish!(frame, true)
629+
@test nmethods_in_overlay() == 3 # `external_foo(x)` and `kwcall` methods were added
630+
631+
ex = :(Base.Experimental.@overlay getproperty(@__MODULE__, :method_table) external_foo(x::Int) = 4)
632+
frame = Frame(Toplevel, ex)
633+
JuliaInterpreter.finish!(frame, true)
634+
@test nmethods_in_overlay() == 4
635+
end

0 commit comments

Comments
 (0)