diff --git a/src/codeedges.jl b/src/codeedges.jl index 0e17bda..395d248 100644 --- a/src/codeedges.jl +++ b/src/codeedges.jl @@ -297,24 +297,25 @@ function direct_links!(cl::CodeLinks, src::CodeInfo) icl = CodeLinks(cl.thismod, arg1) add_inner!(cl, icl, i) continue - elseif isexpr(stmt, :method) - if length(stmt.args) === 1 + elseif isexpr(stmt, :method) || is_define_method_call_2arg(stmt) || is_define_method_call_4arg(stmt) + if ismethod1(stmt) # A function with no methods was defined. Associate its new binding to it. - name = stmt.args[1] - if isa(name, Symbol) - name = GlobalRef(cl.thismod, name) + name = method_name(stmt) + if isa(name, Symbol) || isa(name, QuoteNode) + name = isa(name, QuoteNode) ? name.value : name + mmod = method_module(stmt) + name = GlobalRef(mmod !== nothing ? mmod : cl.thismod, name::Symbol) end - if !isa(name, GlobalRef) - error("name ", typeof(name), " not recognized") + if isa(name, GlobalRef) + assign = get!(Vector{Int}, cl.nameassigns, name) + push!(assign, i) + targetstore = get!(Links, cl.namepreds, name) + target = P(name, targetstore) + add_links!(target, stmt, cl) end - assign = get!(Vector{Int}, cl.nameassigns, name) - push!(assign, i) - targetstore = get!(Links, cl.namepreds, name) - target = P(name, targetstore) - add_links!(target, stmt, cl) - elseif length(stmt.args) === 3 && (arg3 = stmt.args[3]; arg3 isa CodeInfo) # method definition + elseif ismethod3(stmt) && (mbody = method_body(stmt); mbody isa CodeInfo) # method definition # A method was defined for an existing function. - icl = CodeLinks(cl.thismod, arg3) + icl = CodeLinks(cl.thismod, mbody) add_inner!(cl, icl, i) end rhs = stmt @@ -1042,7 +1043,7 @@ function add_typedefs!(isrequired, src::CodeInfo, edges::CodeEdges, (typedef_blo for s in var.succs s ∈ norequire && continue stmt2 = stmts[s] - if isexpr(stmt2, :method) && (fname = (stmt2::Expr).args[1]; fname === false || fname === nothing) + if ismethod(stmt2) && (fname = method_name(stmt2::Expr); fname === false || fname === nothing) isrequired[s] = true end end @@ -1058,7 +1059,7 @@ function add_typedefs!(isrequired, src::CodeInfo, edges::CodeEdges, (typedef_blo while i <= length(stmts) && !ismethod3(stmts[i]) i += 1 end - if i <= length(stmts) && (stmts[i]::Expr).args[1] == false + if i <= length(stmts) && method_name(stmts[i]::Expr) == false tpreds = terminal_preds(i, edges) if minimum(tpreds) == idx && i ∉ norequire changed |= !isrequired[i] diff --git a/src/signatures.jl b/src/signatures.jl index 65bf3dc..b70e14c 100644 --- a/src/signatures.jl +++ b/src/signatures.jl @@ -23,6 +23,20 @@ function signature(sigsv::SimpleVector) return sig::Union{DataType,UnionAll} end +function resolve_method_table(interp::Interpreter, frame::Frame, @nospecialize(arg)) + value = try + lookup(interp, frame, arg) + catch + arg isa Expr || return nothing + try + Core.eval(moduleof(frame), arg) + catch + nothing + end + end + return value isa Core.MethodTable ? value : nothing +end + """ (mt, sigt), lastpc = signature([interp::Interpreter=RecursiveInterpreter()], frame::Frame, pc::Int) @@ -39,7 +53,7 @@ If no 3-argument `:method` expression is found, `nothing` will be returned in pl function signature(interp::Interpreter, frame::Frame, @nospecialize(stmt), pc::Int) mod = moduleof(frame) lastpc = frame.pc = pc - while !isexpr(stmt, :method, 3) # wait for the 3-arg version + while !ismethod3(stmt) # wait for the 3-arg :method or 4-arg define_method if isanonymous_typedef(stmt) lastpc = pc = step_through_methoddef(interp, frame, stmt) # define an anonymous function elseif is_Typeof_for_anonymous_methoddef(stmt, frame.framecode.src.code, mod) @@ -52,8 +66,18 @@ function signature(interp::Interpreter, frame::Frame, @nospecialize(stmt), pc::I stmt = pc_expr(frame, pc) end isa(stmt, Expr) || return nothing, pc - mt = extract_method_table(frame, stmt) - sigsv = lookup(interp, frame, stmt.args[2])::SimpleVector + if is_define_method_call_4arg(stmt) + mt = resolve_method_table(interp, frame, stmt.args[3]) + else + mt = extract_method_table(frame, stmt) + end + if is_define_method_call_4arg(stmt) + # define_method(mod, name, sigdata, codeinfo): sigdata is a svec(types, sparams, lnn) + sigdata = lookup(interp, frame, stmt.args[4])::SimpleVector + sigsv = Core.svec(sigdata[1], sigdata[2]) + else + sigsv = lookup(interp, frame, stmt.args[2])::SimpleVector + end sigt = signature(sigsv) return MethodInfoKey(mt, sigt), lastpc end @@ -88,11 +112,14 @@ end function signature_top(frame, stmt::Expr, pc) @assert ismethod3(stmt) + if is_define_method_call_4arg(stmt) + return minid(stmt.args[4], frame.framecode.src.code, pc) + end return minid(stmt.args[2], frame.framecode.src.code, pc) end function step_through_methoddef(interp::Interpreter, frame::Frame, @nospecialize(stmt)) - while !isexpr(stmt, :method) + while !ismethod(stmt) pc = step_expr!(interp, frame, stmt, true) stmt = pc_expr(frame, pc) end @@ -166,8 +193,9 @@ function identify_framemethod_calls(frame::Frame) end end elseif ismethod1(stmt) - key = stmt.args[1] - key = normalize_defsig(key, frame) + key = method_name(stmt) + mmod = method_module(stmt) + key = normalize_defsig(key, mmod !== nothing ? mmod : moduleof(frame)) key = key::GlobalRef mi = get(methodinfos, key, nothing) if mi === nothing @@ -176,8 +204,9 @@ function identify_framemethod_calls(frame::Frame) mi.stop == -1 && (mi.start = i) # advance the statement # unless we've seen the method3 end elseif ismethod3(stmt) - key = stmt.args[1] - key = normalize_defsig(key, frame) + key = method_name(stmt) + mmod = method_module(stmt) + key = normalize_defsig(key, mmod !== nothing ? mmod : moduleof(frame)) if key isa GlobalRef # XXX A temporary hack to fix https://github.com/JuliaDebug/LoweredCodeUtils.jl/issues/80 # We should revisit it. @@ -186,7 +215,7 @@ function identify_framemethod_calls(frame::Frame) elseif key isa Expr # this is a module-scoped call. We don't have to worry about these because they are named continue end - msrc = stmt.args[3] + msrc = method_body(stmt) if msrc isa CodeInfo # XXX: Properly support interpolated `Core.MethodTable`. This will require using # `stmt.args[2]` instead of `stmt.args[1]` to identify the parent function. @@ -339,7 +368,7 @@ function _rename_framemethods!(interp::Interpreter, frame::Frame, linetop, linebody, callee, caller = sc.linetop, sc.linebody, sc.callee, sc.caller cname = get(replacements, callee, nothing) if cname !== nothing && cname !== callee - replacename!((src.code[linetop].args[3])::CodeInfo, callee=>cname) + replacename!(method_body(src.code[linetop])::CodeInfo, callee=>cname) end end return methodinfos @@ -374,8 +403,8 @@ function find_name_caller_sig(interp::Interpreter, frame::Frame, pc::Int, name:: pc === nothing && return nothing stmt = pc_expr(frame, pc) end - body = stmt.args[3] - if normalize_defsig(stmt.args[1], frame) !== name && isa(body, CodeInfo) + body = method_body(stmt) + if normalize_defsig(method_name(stmt), frame) !== name && isa(body, CodeInfo) # This might be the GeneratedFunctionStub for a @generated method for (i, bodystmt) in enumerate(body.code) if isexpr(bodystmt, :meta) && (bodystmt::Expr).args[1] === :generated @@ -435,7 +464,7 @@ end function get_running_name(interp::Interpreter, frame::Frame, pc::Int, name::GlobalRef) nameinfo = find_name_caller_sig(interp, frame, pc, name) if nameinfo === nothing - pc = skip_until(@nospecialize(stmt)->isexpr(stmt, :method, 3), frame, pc) + pc = skip_until(@nospecialize(stmt)->ismethod3(stmt), frame, pc) pc = next_or_nothing(interp, frame, pc) return name, pc, nothing end @@ -546,7 +575,7 @@ function methoddef!(interp::Interpreter, signatures::Vector{MethodInfoKey}, fram framecode, pcin = frame.framecode, pc if ismethod3(stmt) pc3 = pc - arg1 = stmt.args[1] + arg1 = method_name(stmt) (mt, sigt), pc = signature(interp, frame, stmt, pc) # Resolve the signature against the live method tables at the latest committed world. # `whichtt`'s default world is the caller's task world, which is too old here: the method @@ -586,7 +615,8 @@ function methoddef!(interp::Interpreter, signatures::Vector{MethodInfoKey}, fram return pc, pc3 end ismethod1(stmt) || Base.invokelatest(error, "expected method opening, got ", stmt) - name = normalize_defsig(stmt.args[1], frame) + mmod = method_module(stmt) + name = normalize_defsig(method_name(stmt), mmod !== nothing ? mmod : moduleof(frame)) if isa(name, Bool) error("not valid for anonymous methods") elseif name === missing @@ -611,7 +641,9 @@ function methoddef!(interp::Interpreter, signatures::Vector{MethodInfoKey}, fram found = true break end - othername = normalize_defsig(newstmt.args[1], frame) + othermod = method_module(newstmt) + othername = normalize_defsig(method_name(newstmt), + othermod !== nothing ? othermod : moduleof(frame)) if isa(othername, GlobalRef) && !startswith(String(othername.name), '#') break end @@ -621,14 +653,15 @@ function methoddef!(interp::Interpreter, signatures::Vector{MethodInfoKey}, fram while true # methods containing inner methods may need multiple trips through this loop methinfo, pc = signature(interp, frame, stmt, pc) stmt = pc_expr(frame, pc) - while !isexpr(stmt, :method, 3) + while !ismethod3(stmt) pc = next_or_nothing(interp, frame, pc) # this should not check define, we've probably already done this once pc === nothing && return nothing # this was just `function foo end`, signal "no def" stmt = pc_expr(frame, pc) end pc3 = pc stmt = stmt::Expr - name3 = normalize_defsig(stmt.args[1], frame) + mmod3 = method_module(stmt) + name3 = normalize_defsig(method_name(stmt), mmod3 !== nothing ? mmod3 : moduleof(frame)) methinfo === nothing && (error("expected a signature"); return next_or_nothing(interp, frame, pc)), pc3 mt, sigt = methinfo # Methods like f(x::Ref{<:Real}) that use gensymmed typevars will not have the *exact* diff --git a/src/utils.jl b/src/utils.jl index e912d80..0e28493 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -66,16 +66,68 @@ end ismethod(frame::Frame) = ismethod(pc_expr(frame)) ismethod3(frame::Frame) = ismethod3(pc_expr(frame)) -ismethod(stmt) = isexpr(stmt, :method) -ismethod1(stmt) = isexpr(stmt, :method, 1) -ismethod3(stmt) = isexpr(stmt, :method, 3) +# Check if a call argument refers to Core.define_method +function is_define_method_ref(@nospecialize(f)) + is_global_ref(f, Core, :define_method) && return true + @static if isdefined(Core, :define_method) + is_quotenode_egal(f, Core.define_method) && return true + end + return false +end + +# define_method(mod, name) — 2-arg form creates generic function binding +function is_define_method_call_2arg(@nospecialize(stmt)) + isexpr(stmt, :call) || return false + length(stmt.args) == 3 || return false + return is_define_method_ref(stmt.args[1]) +end + +# define_method(mod, name_or_mt, sigdata, codeinfo) — 4-arg form defines a method +function is_define_method_call_4arg(@nospecialize(stmt)) + isexpr(stmt, :call) || return false + length(stmt.args) == 5 || return false + return is_define_method_ref(stmt.args[1]) +end + +ismethod(stmt) = isexpr(stmt, :method) || is_define_method_call_2arg(stmt) || is_define_method_call_4arg(stmt) +ismethod1(stmt) = isexpr(stmt, :method, 1) || is_define_method_call_2arg(stmt) +ismethod3(stmt) = isexpr(stmt, :method, 3) || is_define_method_call_4arg(stmt) + +# Extract the "name" argument from a method-definition statement. +# For Expr(:method, name, ...) it's args[1]; for define_method(mod, name, ...) it's args[3]. +function method_name(@nospecialize(stmt)) + if is_define_method_call_2arg(stmt) || is_define_method_call_4arg(stmt) + return stmt.args[3] + else + return stmt.args[1] + end +end + +# Extract the module from a define_method call, or nothing for :method expressions. +function method_module(@nospecialize(stmt)) + if is_define_method_call_2arg(stmt) || is_define_method_call_4arg(stmt) + return stmt.args[2] # define_method(mod, name, ...) + end + return nothing +end + +# Extract the CodeInfo body from a method3 statement. +# For Expr(:method, name, sig, body) it's args[3]; for define_method(mod, name, sigdata, body) it's args[5]. +function method_body(@nospecialize(stmt)) + if is_define_method_call_4arg(stmt) + return stmt.args[5] + else + return stmt.args[3] + end +end + function ismethod_with_name(src, stmt, target::AbstractString; reentrant::Bool=false) if reentrant name = stmt else ismethod3(stmt) || return false - name = stmt.args[1] - if name === nothing + name = method_name(stmt) + if name === nothing && isexpr(stmt, :method) name = stmt.args[2] end end diff --git a/test/codeedges.jl b/test/codeedges.jl index a725ea6..1de2874 100644 --- a/test/codeedges.jl +++ b/test/codeedges.jl @@ -15,6 +15,7 @@ function hastrackedexpr(@nospecialize(stmt)) haseval = f === :eval || (callee_matches(f, Base, :getproperty) && is_quotenode(stmt.args[2], :eval)) callee_matches(f, Core, :_typebody!) && return true, haseval callee_matches(f, Core, :_setsuper!) && return true, haseval + LoweredCodeUtils.is_define_method_ref(f) && return true, haseval f === :include && return true, haseval elseif stmt.head === :thunk any(s->any(hastrackedexpr(s)), stmt.args[1].code) && return true, haseval @@ -352,10 +353,10 @@ module ModSelective end edges = CodeEdges(ModSelective, src) isrequired = fill(false, length(src.code)) let j = length(src.code) - 1 - while !Meta.isexpr(src.code[j], :method, 3) + while !LoweredCodeUtils.ismethod3(src.code[j]) j -= 1 end - @assert Meta.isexpr(src.code[j], :method, 3) + @assert LoweredCodeUtils.ismethod3(src.code[j]) isrequired[j] = true end lines_required!(isrequired, src, edges) @@ -410,7 +411,7 @@ module ModSelective end end) src = thk.args[1] edges = CodeEdges(Main, src) - idx = findfirst(@nospecialize(stmt)->Meta.isexpr(stmt, :method), src.code) + idx = findfirst(LoweredCodeUtils.ismethod, src.code) lr = lines_required(idx, src, edges; norequire=exclude_named_typedefs(src, edges)) idx = findfirst(@nospecialize(stmt)->Meta.isexpr(stmt, :(=)) && Meta.isexpr(stmt.args[2], :call) && is_global_ref(stmt.args[2].args[1], Core, :Box), src.code) @test lr[idx] diff --git a/test/signatures.jl b/test/signatures.jl index 88a9dec..34c51ea 100644 --- a/test/signatures.jl +++ b/test/signatures.jl @@ -559,6 +559,8 @@ end module ExternalMT Base.Experimental.@MethodTable method_table macro overlay(ex) esc(:(Base.Experimental.@overlay $method_table $ex)) end + get_method_table() = method_table + macro overlay_call(ex) esc(:(Base.Experimental.@overlay get_method_table() $ex)) end end @testset "Support for external method tables" begin @@ -588,6 +590,15 @@ end (mt, sig) = pop!(signatures) @test (mt, sig) === (ExternalMT.method_table, Tuple{typeof(ExternalMT.foo), Int64}) LoweredCodeUtils.identify_framemethod_calls(frame) # make sure this does not throw + + # Exercise a method-table value computed by an inline call. + ex = :(@overlay_call foo(x::Float64) = "computed overlay") + Core.eval(ExternalMT, ex) + frame = Frame(ExternalMT, ex) + methoddefs!(signatures, frame; define = false) + @test length(signatures) == 1 + (mt, sig) = pop!(signatures) + @test (mt, sig) === (ExternalMT.method_table, Tuple{typeof(ExternalMT.foo), Float64}) end @testset "normalize_defsig with a quoted Symbol callee (issue Revise#914)" begin