diff --git a/src/codeedges.jl b/src/codeedges.jl index 677ef9b..04363cb 100644 --- a/src/codeedges.jl +++ b/src/codeedges.jl @@ -163,8 +163,10 @@ function postprint_linelinks(io::IO, idx::Int, src::CodeInfo, cl::CodeLinks, bbc printstyled(io, bbchanged ? " " : "│", color=:light_black) printstyled(io, " # ", color=:yellow) stmt = src.code[idx] - if is_assignment_like(stmt) - lhs = normalize_defsig(stmt.args[1], cl.thismod) + lhs_rhs = get_lhs_rhs(stmt) + if lhs_rhs !== nothing + lhs, _ = lhs_rhs + lhs = normalize_defsig(lhs, cl.thismod) if @issslotnum(lhs) # id = lhs.id # preds, succs = cl.slotpreds[id], cl.slotsuccs[id] @@ -190,24 +192,37 @@ function namedkeys(cl::CodeLinks) return ukeys end -is_assignment_like(stmt::Expr) = isexpr(stmt, :(=)) || (isexpr(stmt, :const) && length(stmt.args) == 2) -is_assignment_like(@nospecialize stmt) = false +function get_lhs_rhs(@nospecialize stmt) + if isexpr(stmt, :(=)) + return Pair{Any,Any}(stmt.args[1], stmt.args[2]) + elseif isexpr(stmt, :const) && length(stmt.args) == 2 + return Pair{Any,Any}(stmt.args[1], stmt.args[2]) + elseif isexpr(stmt, :call) && length(stmt.args) == 4 + f = stmt.args[1] + if isa(f, GlobalRef) && f.name === :setglobal! + mod = stmt.args[2] + mod isa Module || return nothing + name = stmt.args[3] + if name isa QuoteNode + name = name.value + end + name isa Symbol || return nothing + lhs = GlobalRef(mod, name) + return Pair{Any,Any}(lhs, stmt.args[4]) + end + end + return nothing +end function direct_links!(cl::CodeLinks, src::CodeInfo) # Utility for when a stmt itself contains a CodeInfo function add_inner!(cl::CodeLinks, icl::CodeLinks, idx) for (name, _) in icl.nameassigns - assigns = get(cl.nameassigns, name, nothing) - if assigns === nothing - cl.nameassigns[name] = assigns = Int[] - end + assigns = get!(Vector{Int}, cl.nameassigns, name) push!(assigns, idx) end for (name, _) in icl.namesuccs - succs = get(cl.namesuccs, name, nothing) - if succs === nothing - cl.namesuccs[name] = succs = Links() - end + succs = get!(Links, cl.namesuccs, name) push!(succs.ssas, idx) end end @@ -215,13 +230,13 @@ function direct_links!(cl::CodeLinks, src::CodeInfo) P = Pair{Union{SSAValue,SlotNumber,GlobalRef},Links} for (i, stmt) in enumerate(src.code) - if isexpr(stmt, :thunk) && isa(stmt.args[1], CodeInfo) - icl = CodeLinks(cl.thismod, stmt.args[1]) + if isexpr(stmt, :thunk) && (arg1 = stmt.args[1]; arg1 isa CodeInfo) + icl = CodeLinks(cl.thismod, arg1) add_inner!(cl, icl, i) continue - elseif isa(stmt, Expr) && stmt.head ∈ trackedheads - if stmt.head === :method && length(stmt.args) === 3 && isa(stmt.args[3], CodeInfo) - icl = CodeLinks(cl.thismod, stmt.args[3]) + elseif isexpr(stmt, :method) + if length(stmt.args) === 3 && (arg3 = stmt.args[3]; arg3 isa CodeInfo) + icl = CodeLinks(cl.thismod, arg3) add_inner!(cl, icl, i) end name = stmt.args[1] @@ -234,10 +249,7 @@ function direct_links!(cl::CodeLinks, src::CodeInfo) cl.nameassigns[name] = assign = Int[] end push!(assign, i) - targetstore = get(cl.namepreds, name, nothing) - if targetstore === nothing - cl.namepreds[name] = targetstore = Links() - end + targetstore = get!(Links, cl.namepreds, name) target = P(name, targetstore) add_links!(target, stmt, cl) elseif name in (nothing, false) @@ -247,10 +259,10 @@ function direct_links!(cl::CodeLinks, src::CodeInfo) end rhs = stmt target = P(SSAValue(i), cl.ssapreds[i]) - elseif is_assignment_like(stmt) + elseif (lhs_rhs = get_lhs_rhs(stmt); lhs_rhs !== nothing) # An assignment stmt = stmt::Expr - lhs, rhs = stmt.args[1], stmt.args[2] + lhs, rhs = lhs_rhs if @issslotnum(lhs) lhs = lhs::AnySlotNumber id = lhs.id @@ -260,10 +272,7 @@ function direct_links!(cl::CodeLinks, src::CodeInfo) if isa(lhs, Symbol) lhs = GlobalRef(cl.thismod, lhs) end - targetstore = get(cl.namepreds, lhs, nothing) - if targetstore === nothing - cl.namepreds[lhs] = targetstore = Links() - end + targetstore = get!(Links, cl.namepreds, lhs) target = P(lhs, targetstore) assign = get(cl.nameassigns, lhs, nothing) if assign === nothing @@ -299,23 +308,35 @@ function add_links!(target::Pair{Union{SSAValue,SlotNumber,GlobalRef},Links}, @n stmt = GlobalRef(cl.thismod, stmt) end push!(targetstore, stmt) - namestore = get(cl.namesuccs, stmt, nothing) - if namestore === nothing - cl.namesuccs[stmt] = namestore = Links() - end + namestore = get!(Links, cl.namesuccs, stmt) push!(namestore, targetid) - elseif isa(stmt, Expr) && stmt.head !== :copyast - stmt = stmt::Expr - arng = 1:length(stmt.args) - if stmt.head === :call - f = stmt.args[1] - if !@isssa(f) && !@issslotnum(f) - # Avoid putting named callees on the namestore - arng = 2:length(stmt.args) + elseif isa(stmt, Expr) + if stmt.head === :globaldecl + for i = 1:length(stmt.args) + a = stmt.args[i] + if a isa GlobalRef + namestore = get!(Links, cl.namepreds, a) # TODO should this information be tracked in the separate `cl.namedecls` store? + push!(namestore, targetid) + if targetid isa SSAValue + push!(namestore, SSAValue(targetid.id+1)) # +1 for :latestworld + end + else + add_links!(target, a, cl) + end + end + elseif stmt.head !== :copyast + stmt = stmt::Expr + arng = 1:length(stmt.args) + if stmt.head === :call + f = stmt.args[1] + if !@isssa(f) && !@issslotnum(f) + # Avoid putting named callees on the namestore + arng = 2:length(stmt.args) + end + end + for i in arng + add_links!(target, stmt.args[i], cl) end - end - for i in arng - add_links!(target, stmt.args[i], cl) end elseif stmt isa Core.GotoIfNot add_links!(target, stmt.cond, cl) @@ -362,7 +383,6 @@ struct Variable preds::Vector{Int} succs::Vector{Int} end -Variable() = Variable(Int[], Int[], Int[]) function Base.show(io::IO, v::Variable) print(io, "assigned on ", showempty(v.assigned)) @@ -423,9 +443,9 @@ function CodeEdges(src::CodeInfo, cl::CodeLinks) emptylist = Int[] for (i, stmt) in enumerate(src.code) # Identify line predecents for slots and named variables - if is_assignment_like(stmt) + if (lhs_rhs = get_lhs_rhs(stmt); lhs_rhs !== nothing) stmt = stmt::Expr - lhs = stmt.args[1] + lhs, _ = lhs_rhs # Mark predecessors and successors of this line by following ssas & named assignments if @issslotnum(lhs) lhs = lhs::AnySlotNumber @@ -611,7 +631,7 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges) i = 1 nstmts = length(src.code) while i <= nstmts - stmt = rhs(src.code[i]) + stmt = getrhs(src.code[i]) if istypedef(stmt) && !isanonymous_typedef(stmt::Expr) r = typedef_range(src, i) pushall!(norequire, r) @@ -715,10 +735,17 @@ function add_succs!(isrequired, idx, edges::CodeEdges, succs, norequire) end return chngd end -function add_obj!(isrequired, objs, obj, edges::CodeEdges, norequire) +function add_obj!(isrequired, objs, obj::GlobalRef, edges::CodeEdges, norequire) chngd = false + for p in edges.byname[obj].preds + p ∈ norequire && continue + isrequired[p] && continue + isrequired[p] = true + chngd = true + end for d in edges.byname[obj].assigned d ∈ norequire && continue + isrequired[d] && continue isrequired[d] || add_preds!(isrequired, d, edges, norequire) isrequired[d] = true chngd = true @@ -871,7 +898,7 @@ function find_typedefs(src::CodeInfo) i = 1 nstmts = length(src.code) while i <= nstmts - stmt = rhs(src.code[i]) + stmt = getrhs(src.code[i]) if istypedef(stmt) && !isanonymous_typedef(stmt::Expr) stmt = stmt::Expr r = typedef_range(src, i) @@ -972,8 +999,8 @@ function add_inplace!(isrequired, src, edges, norequire) for k in edges.preds[j] isrequired[k] || continue predstmt = src.code[k] - if is_assignment_like(predstmt) - lhs = predstmt.args[1] + if (lhs_rhs = get_lhs_rhs(predstmt); lhs_rhs !== nothing) + lhs, _ = lhs_rhs if @issslotnum(lhs) && lhs.id == id changed |= mark_if_inplace(stmt, j) break @@ -1056,7 +1083,7 @@ function print_with_code(io::IO, src::CodeInfo, isrequired::AbstractVector{Bool} preprint(::IO) = nothing preprint(io::IO, idx::Int) = (c = isrequired[idx]; printstyled(io, lpad(idx, nd), ' ', c ? "t " : "f "; color = c ? :cyan : :plain)) postprint(::IO) = nothing - postprint(io::IO, idx::Int, bbchanged::Bool) = nothing + postprint(::IO, idx::Int, bbchanged::Bool) = nothing print_with_code(preprint, postprint, io, src) end diff --git a/src/packagedef.jl b/src/packagedef.jl index 7930601..f39f758 100644 --- a/src/packagedef.jl +++ b/src/packagedef.jl @@ -8,7 +8,6 @@ using Base.Meta: isexpr const SSAValues = Union{Core.Compiler.SSAValue, JuliaInterpreter.SSAValue} -const trackedheads = (:method,) const structdecls = (:_structtype, :_abstracttype, :_primitivetype) export signature, rename_framemethods!, methoddef!, methoddefs!, bodymethod diff --git a/src/utils.jl b/src/utils.jl index 21ca245..fa5c658 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -58,9 +58,9 @@ function callee_matches(f, mod, sym) return false end -function rhs(stmt) - is_assignment_like(stmt) && return (stmt::Expr).args[2] - return stmt +function getrhs(@nospecialize(stmt)) + lhs_rhs = get_lhs_rhs(stmt) + return lhs_rhs === nothing ? stmt : lhs_rhs[2] end ismethod(frame::Frame) = ismethod(pc_expr(frame)) @@ -107,7 +107,7 @@ function ismethod_with_name(src, stmt, target::AbstractString; reentrant::Bool=f end # anonymous function types are defined in a :thunk expr with a characteristic CodeInfo -function isanonymous_typedef(stmt) +function isanonymous_typedef(@nospecialize stmt) if isa(stmt, Expr) stmt.head === :thunk || return false stmt = stmt.args[1] @@ -119,21 +119,22 @@ function isanonymous_typedef(stmt) isexpr(stmt, :call) || return false is_global_ref(stmt.args[1], Core, :_typebody!) || return false stmt = isa(stmt.args[3], Core.SSAValue) ? src.code[end-3] : src.code[end-2] - is_assignment_like(stmt) || return false - name = stmt.args[1] - if isa(name, GlobalRef) - name = name.name + lhs_rhs = get_lhs_rhs(stmt) + lhs_rhs === nothing && return false + lhs, _ = lhs_rhs + if isa(lhs, GlobalRef) + lhs = lhs.name else - isa(name, Symbol) || return false + isa(lhs, Symbol) || return false end - return startswith(String(name), "#") + return startswith(String(lhs), "#") end return false end function istypedef(stmt) isa(stmt, Expr) || return false - stmt = rhs(stmt) + stmt = getrhs(stmt) isa(stmt, Expr) || return false @static if all(s->isdefined(Core,s), structdecls) if stmt.head === :call @@ -162,6 +163,7 @@ function typedef_range(src::CodeInfo, idx) istart = idx while istart >= 1 isexpr(src.code[istart], :global) && break + isexpr(src.code[istart], :latestworld) && break istart -= 1 end istart >= 1 || error("no initial :global found") @@ -171,6 +173,7 @@ function typedef_range(src::CodeInfo, idx) stmt = src.code[iend] if isa(stmt, Expr) stmt.head === :global && break + stmt.head === :latestworld && break if stmt.head === :call if (is_global_ref(stmt.args[1], Core, :_typebody!) || is_quotenode_egal(stmt.args[1], Core._typebody!)) have_typebody = true @@ -179,7 +182,7 @@ function typedef_range(src::CodeInfo, idx) # Advance to the type-assignment while iend <= n stmt = src.code[iend] - is_assignment_like(stmt) && break + get_lhs_rhs(stmt) !== nothing && break iend += 1 end end diff --git a/test/codeedges.jl b/test/codeedges.jl index 9317342..412620a 100644 --- a/test/codeedges.jl +++ b/test/codeedges.jl @@ -6,7 +6,7 @@ using LoweredCodeUtils: callee_matches, istypedef, exclude_named_typedefs using JuliaInterpreter: is_global_ref, is_quotenode using Test -function hastrackedexpr(@nospecialize(stmt); heads=LoweredCodeUtils.trackedheads) +function hastrackedexpr(@nospecialize(stmt)) haseval = false if isa(stmt, Expr) if stmt.head === :call @@ -16,8 +16,8 @@ function hastrackedexpr(@nospecialize(stmt); heads=LoweredCodeUtils.trackedheads callee_matches(f, Core, :_setsuper!) && return true, haseval f === :include && return true, haseval elseif stmt.head === :thunk - any(s->any(hastrackedexpr(s; heads=heads)), stmt.args[1].code) && return true, haseval - elseif stmt.head ∈ heads + any(s->any(hastrackedexpr(s)), stmt.args[1].code) && return true, haseval + elseif stmt.head === :method return true, haseval end end @@ -66,24 +66,24 @@ module ModSelective end Core.eval(ModEval, ex) isrequired = lines_required(GlobalRef(ModSelective, :x), src, edges) # theere is too much diversity in lowering across Julia versions to make it useful to test `sum(isrequired)` - selective_eval_fromstart!(frame, isrequired) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test ModSelective.x === ModEval.x @test allmissing(ModSelective, (:y, :z, :a, :b, :k)) @test !allmissing(ModSelective, (:x, :y)) # add :y here to test the `all` part of the test itself # To evaluate z we need to do all the computations for y isrequired = lines_required(GlobalRef(ModSelective, :z), src, edges) - selective_eval_fromstart!(frame, isrequired) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test ModSelective.y === ModEval.y @test ModSelective.z === ModEval.z @test allmissing(ModSelective, (:a, :b, :k)) # ... but not a and b isrequired = lines_required(length(src.code)-1, src, edges) # this should be the assignment of b - selective_eval_fromstart!(frame, isrequired) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test ModSelective.a === ModEval.a @test ModSelective.b === ModEval.b # Test that we get two separate evaluations of k @test allmissing(ModSelective, (:k,)) isrequired = lines_required(GlobalRef(ModSelective, :k), src, edges) - selective_eval_fromstart!(frame, isrequired) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test ModSelective.k != ModEval.k # Control-flow @@ -124,7 +124,7 @@ module ModSelective end src = frame.framecode.src edges = CodeEdges(ModSelective, src) isrequired = lines_required(GlobalRef(ModSelective, :a3), src, edges) - selective_eval_fromstart!(frame, isrequired) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) Core.eval(ModEval, ex) @test ModSelective.a3 === ModEval.a3 == 2 @test allmissing(ModSelective, (:z3, :x3, :y3)) @@ -143,7 +143,7 @@ module ModSelective end src = frame.framecode.src edges = CodeEdges(ModSelective, src) isrequired = lines_required(GlobalRef(ModSelective, :valcf), src, edges) - selective_eval_fromstart!(frame, isrequired) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test ModSelective.valcf == 4 ex = quote @@ -161,7 +161,7 @@ module ModSelective end edges = CodeEdges(ModSelective, src) isrequired = lines_required(GlobalRef(ModSelective, :c_os), src, edges) @test sum(isrequired) >= length(isrequired) - 3 - selective_eval_fromstart!(frame, isrequired) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) Core.eval(ModEval, ex) @test ModSelective.c_os === ModEval.c_os == Sys.iswindows() @@ -187,7 +187,7 @@ module ModSelective end @test sum(isrequired) == 1 isrequired[edges.succs[findfirst(isrequired)]] .= true # add lines that use Core.eval lines_required!(isrequired, src, edges) - selective_eval_fromstart!(frame, isrequired) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test ModSelective.feval1(1.0f0) == 1 @test ModSelective.feval1(1.0) == 1 @test_throws MethodError ModSelective.feval1(1) @@ -206,13 +206,13 @@ module ModSelective end end end frame = Frame(ModSelective, ex) - JuliaInterpreter.finish_and_return!(frame, true) + JuliaInterpreter.finish_and_return!(frame, #=istoplevel=#true) @test ModSelective.k11 == 11 @test 3 <= ModSelective.s11 <= 15 Core.eval(ModSelective, :(k11 = 0; s11 = -1)) edges = CodeEdges(ModSelective, frame.framecode.src) isrequired = lines_required(GlobalRef(ModSelective, :s11), frame.framecode.src, edges) - selective_eval_fromstart!(frame, isrequired, true) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test ModSelective.k11 == 0 @test 3 <= ModSelective.s11 <= 15 @@ -224,7 +224,7 @@ module ModSelective end # Check that the StructParent name is discovered everywhere it is used var = edges.byname[GlobalRef(ModSelective, :StructParent)] isrequired = minimal_evaluation(hastrackedexpr, src, edges) - selective_eval_fromstart!(frame, isrequired, true) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test supertype(ModSelective.StructParent) === AbstractArray # Also check redefinition (it's OK when the definition doesn't change) Core.eval(ModEval, ex) @@ -232,7 +232,7 @@ module ModSelective end src = frame.framecode.src edges = CodeEdges(ModEval, src) isrequired = minimal_evaluation(hastrackedexpr, src, edges) - selective_eval_fromstart!(frame, isrequired, true) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test supertype(ModEval.StructParent) === AbstractArray # Finding all dependencies in a struct definition @@ -241,9 +241,19 @@ module ModSelective end frame = Frame(ModSelective, ex) src = frame.framecode.src edges = CodeEdges(ModSelective, src) - isrequired = minimal_evaluation(@nospecialize(stmt)->(LoweredCodeUtils.ismethod_with_name(src, stmt, "NoParam"),false), src, edges) # initially mark only the constructor - selective_eval_fromstart!(frame, isrequired, true) - @test isa(ModSelective.NoParam(), ModSelective.NoParam) + isrequired = minimal_evaluation(src, edges) do @nospecialize stmt + # initially mark only the constructor + @static if VERSION ≥ v"1.12-" + return (Meta.isexpr(stmt, :call) && stmt.args[1] == GlobalRef(Core, :_defaultctors), false) + else + return (LoweredCodeUtils.ismethod_with_name(src, stmt, "NoParam"), false) + end + end + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) + let NoParam = @invokelatest ModSelective.NoParam + @test isa(NoParam(), NoParam) + end + # Parametric ex = quote struct Struct{T} <: StructParent{T,1} @@ -253,9 +263,19 @@ module ModSelective end frame = Frame(ModSelective, ex) src = frame.framecode.src edges = CodeEdges(ModSelective, src) - isrequired = minimal_evaluation(@nospecialize(stmt)->(LoweredCodeUtils.ismethod_with_name(src, stmt, "Struct"),false), src, edges) # initially mark only the constructor - selective_eval_fromstart!(frame, isrequired, true) - @test isa(ModSelective.Struct([1,2,3]), ModSelective.Struct{Int}) + isrequired = minimal_evaluation(src, edges) do @nospecialize stmt + # initially mark only the constructor + @static if VERSION ≥ v"1.12-" + return (Meta.isexpr(stmt, :call) && stmt.args[1] == GlobalRef(Core, :_defaultctors), false) + else + return (LoweredCodeUtils.ismethod_with_name(src, stmt, "Struct"), false) + end + end + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) + let Struct = @invokelatest ModSelective.Struct + @test isa(Struct([1,2,3]), Struct{Int}) + end + # Keyword constructor (this generates :copyast expressions) ex = quote struct KWStruct @@ -271,7 +291,7 @@ module ModSelective end src = frame.framecode.src edges = CodeEdges(ModSelective, src) isrequired = minimal_evaluation(@nospecialize(stmt)->(LoweredCodeUtils.ismethod3(stmt),false), src, edges) # initially mark only the constructor - selective_eval_fromstart!(frame, isrequired, true) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) kws = ModSelective.KWStruct(y=5.0f0) @test kws.y === 5.0f0 @@ -281,14 +301,15 @@ module ModSelective end src = frame.framecode.src edges = CodeEdges(ModSelective, src) isrequired = fill(false, length(src.code)) - j = length(src.code) - 1 - while !Meta.isexpr(src.code[j], :method, 3) - j -= 1 + let j = length(src.code) - 1 + while !Meta.isexpr(src.code[j], :method, 3) + j -= 1 + end + @assert Meta.isexpr(src.code[j], :method, 3) + isrequired[j] = true end - @assert Meta.isexpr(src.code[j], :method, 3) - isrequired[j] = true lines_required!(isrequired, src, edges) - selective_eval_fromstart!(frame, isrequired, true) + selective_eval_fromstart!(frame, isrequired, #=istoplevel=#true) @test ModSelective.max_values(Int16) === 65536 # Avoid redefining types @@ -414,9 +435,8 @@ module ModSelective end end @testset "selective interpretation of toplevel definitions" begin - gen_mock_module() = Core.eval(@__MODULE__, :(module $(gensym(:LoweredCodeUtilsTestMock)) end)) function check_toplevel_definition_interprete(ex, defs, undefs) - m = gen_mock_module() + m = Module(:LoweredCodeUtilsTestMock) lwr = Meta.lower(m, ex) src = first(lwr.args) stmts = src.code @@ -426,8 +446,8 @@ end frame = Frame(m, src) selective_eval_fromstart!(frame, isrq, #=toplevel=#true) - for def in defs; @test isdefined(m, def); end - for undef in undefs; @test !isdefined(m, undef); end + for def in defs; @test @invokelatest(isdefined(m, def)); end + for undef in undefs; @test !@invokelatest(isdefined(m, undef)); end end @testset "case: $(i), interpret: $(defs), ignore $(undefs)" for (i, ex, defs, undefs) in ( diff --git a/test/signatures.jl b/test/signatures.jl index 8adb2b8..ad8d7d9 100644 --- a/test/signatures.jl +++ b/test/signatures.jl @@ -477,8 +477,7 @@ macro deffoogr_sandbox() $gr(args...) = length(args) end end -let - ex = quote +let ex = quote @deffoogr_sandbox @show sandboxgr.foogr_sandbox(1,2,3) end