|
| 1 | +# This file is a part of Julia. License is MIT: https://julialang.org/license |
| 2 | + |
| 3 | +import ..Compiler: verify_typeinf_trim |
| 4 | + |
| 5 | +using ..Compiler: CodeInstance, IdSet, IdDict, String, Method, MethodInstance, empty!, get, getindex, setindex!, iterate, !==, :, isexpr, getproperty, setproperty!, Vector, pop!, isempty, in, !, length, push!, pushfirst!, popfirst!, reverse, reverse!, haskey, argextype, SSAValue, GlobalRef, CodeInfo, QuoteNode, PiNode, PhiNode, PhiCNode, IntrinsicFunction, Builtin, singleton_type, reinterpret, get_ci_mi, sptypes_from_meth_instance, @nospecialize, +, widenconst, max, >, >=, <, <=, ==, !=, error, VarState, ∈, ∉, =>, Pair, map!, similar, isdispatchelem, Callable, get_world_counter, Csize_t, Cint, C_NULL, unsafe_pointer_to_objref, SimpleVector, GenericMemory, Array, Tuple, NamedTuple, Exception, hasintersect |
| 6 | +using ..IRShow: LineInfoNode, print, show, println, append_scopes!, IOContext, IO, normalize_method_name |
| 7 | +using ..Base: Base, sourceinfo_slotnames |
| 8 | +using ..Base.StackTraces: StackFrame |
| 9 | + |
| 10 | +## declarations ## |
| 11 | + |
| 12 | +struct CallMissing <: Exception |
| 13 | + codeinst::CodeInstance |
| 14 | + codeinfo::CodeInfo |
| 15 | + sptypes::Vector{VarState} |
| 16 | + stmtidx::Int |
| 17 | + desc::String |
| 18 | +end |
| 19 | + |
| 20 | +struct CCallableMissing <: Exception |
| 21 | + rt |
| 22 | + sig |
| 23 | + desc |
| 24 | +end |
| 25 | + |
| 26 | +const ParentMap = IdDict{CodeInstance,Tuple{CodeInstance,Int}} |
| 27 | +const ErrorList = Vector{Pair{Bool,Any}} # severity => exception |
| 28 | + |
| 29 | +const runtime_functions = Symbol[ |
| 30 | + # a denylist of any runtime functions which someone might ccall which can call jl_apply or access reflection state |
| 31 | + # which might not be captured by the trim output |
| 32 | + :jl_apply, |
| 33 | +] |
| 34 | + |
| 35 | +## code for pretty printing ## |
| 36 | + |
| 37 | +# wrap a statement in a typeassert for printing clarity, unless that info seems already obvious |
| 38 | +function mapssavaluetypes(codeinfo::CodeInfo, sptypes::Vector{VarState}, stmt) |
| 39 | + @nospecialize stmt |
| 40 | + newstmt = mapssavalues(codeinfo, sptypes, stmt) |
| 41 | + typ = widenconst(argextype(stmt, codeinfo, sptypes)) |
| 42 | + if newstmt isa Expr |
| 43 | + if newstmt.head ∈ (:quote, :inert) |
| 44 | + return newstmt |
| 45 | + end |
| 46 | + elseif newstmt isa GlobalRef && isdispatchelem(typ) |
| 47 | + return newstmt |
| 48 | + elseif newstmt isa Union{Int, UInt8, UInt16, UInt32, UInt64, Float16, Float32, Float64, String, QuoteNode} |
| 49 | + return newstmt |
| 50 | + elseif newstmt isa Callable |
| 51 | + return newstmt |
| 52 | + end |
| 53 | + return Expr(:(::), newstmt, typ) |
| 54 | +end |
| 55 | + |
| 56 | +# map the ssavalues in a (value-producing) statement to the expression they came from, summarizing some things to avoid excess printing |
| 57 | +function mapssavalues(codeinfo::CodeInfo, sptypes::Vector{VarState}, stmt) |
| 58 | + @nospecialize stmt |
| 59 | + if stmt isa SSAValue |
| 60 | + return mapssavalues(codeinfo, sptypes, codeinfo.code[stmt.id]) |
| 61 | + elseif stmt isa PiNode |
| 62 | + return mapssavalues(codeinfo, sptypes, stmt.val) |
| 63 | + elseif stmt isa Expr |
| 64 | + stmt.head ∈ (:quote, :inert) && return stmt |
| 65 | + newstmt = Expr(stmt.head) |
| 66 | + if stmt.head === :foreigncall |
| 67 | + return Expr(:call, :ccall, mapssavalues(codeinfo, sptypes, stmt.args[1])) |
| 68 | + elseif stmt.head ∉ (:new, :method, :toplevel, :thunk) |
| 69 | + newstmt.args = map!(similar(stmt.args), stmt.args) do arg |
| 70 | + @nospecialize arg |
| 71 | + return mapssavaluetypes(codeinfo, sptypes, arg) |
| 72 | + end |
| 73 | + if newstmt.head === :invoke |
| 74 | + # why is the fancy printing for this not in show_unquoted? |
| 75 | + popfirst!(newstmt.args) |
| 76 | + newstmt.head = :call |
| 77 | + end |
| 78 | + end |
| 79 | + return newstmt |
| 80 | + elseif stmt isa PhiNode |
| 81 | + return PhiNode() |
| 82 | + elseif stmt isa PhiCNode |
| 83 | + return PhiNode() |
| 84 | + end |
| 85 | + return stmt |
| 86 | +end |
| 87 | + |
| 88 | +function verify_print_stmt(io::IOContext{IO}, codeinfo::CodeInfo, sptypes::Vector{VarState}, stmtidx::Int) |
| 89 | + if codeinfo.slotnames !== nothing |
| 90 | + io = IOContext(io, :SOURCE_SLOTNAMES => sourceinfo_slotnames(codeinfo)) |
| 91 | + end |
| 92 | + print(io, mapssavaluetypes(codeinfo, sptypes, SSAValue(stmtidx))) |
| 93 | +end |
| 94 | + |
| 95 | +function verify_print_error(io::IOContext{IO}, desc::CallMissing, parents::ParentMap) |
| 96 | + (; codeinst, codeinfo, sptypes, stmtidx, desc) = desc |
| 97 | + frames = verify_create_stackframes(codeinst, stmtidx, parents) |
| 98 | + print(io, desc, " from ") |
| 99 | + verify_print_stmt(io, codeinfo, sptypes, stmtidx) |
| 100 | + Base.show_backtrace(io, frames) |
| 101 | + print(io, "\n\n") |
| 102 | + nothing |
| 103 | +end |
| 104 | + |
| 105 | +function verify_print_error(io::IOContext{IO}, desc::CCallableMissing, parents::ParentMap) |
| 106 | + print(io, desc.desc, " for ", desc.sig, " => ", desc.rt, "\n\n") |
| 107 | + nothing |
| 108 | +end |
| 109 | + |
| 110 | +function verify_create_stackframes(codeinst::CodeInstance, stmtidx::Int, parents::ParentMap) |
| 111 | + scopes = LineInfoNode[] |
| 112 | + frames = StackFrame[] |
| 113 | + parent = (codeinst, stmtidx) |
| 114 | + while parent !== nothing |
| 115 | + codeinst, stmtidx = parent |
| 116 | + di = codeinst.debuginfo |
| 117 | + append_scopes!(scopes, stmtidx, di, :var"unknown scope") |
| 118 | + for i in reverse(1:length(scopes)) |
| 119 | + lno = scopes[i] |
| 120 | + inlined = i != 1 |
| 121 | + def = lno.method |
| 122 | + def isa Union{Method,Core.CodeInstance,MethodInstance} || (def = nothing) |
| 123 | + sf = StackFrame(normalize_method_name(lno.method), lno.file, lno.line, def, false, inlined, 0) |
| 124 | + push!(frames, sf) |
| 125 | + end |
| 126 | + empty!(scopes) |
| 127 | + parent = get(parents, codeinst, nothing) |
| 128 | + end |
| 129 | + return frames |
| 130 | +end |
| 131 | + |
| 132 | +## code for analysis ## |
| 133 | + |
| 134 | +function may_dispatch(@nospecialize ftyp) |
| 135 | + if ftyp <: IntrinsicFunction |
| 136 | + return true |
| 137 | + elseif ftyp <: Builtin |
| 138 | + # other builtins (including the IntrinsicFunctions) are good |
| 139 | + return Core._apply isa ftyp || |
| 140 | + Core._apply_iterate isa ftyp || |
| 141 | + Core._apply_pure isa ftyp || |
| 142 | + Core._call_in_world isa ftyp || |
| 143 | + Core._call_in_world_total isa ftyp || |
| 144 | + Core._call_latest isa ftyp || |
| 145 | + Core.invoke isa ftyp || |
| 146 | + Core.finalizer isa ftyp || |
| 147 | + Core.modifyfield! isa ftyp || |
| 148 | + Core.modifyglobal! isa ftyp || |
| 149 | + Core.memoryrefmodify! isa ftyp |
| 150 | + else |
| 151 | + return true |
| 152 | + end |
| 153 | +end |
| 154 | + |
| 155 | +function verify_codeinstance!(codeinst::CodeInstance, codeinfo::CodeInfo, inspected::IdSet{CodeInstance}, caches::IdDict{MethodInstance,CodeInstance}, parents::ParentMap, errors::ErrorList) |
| 156 | + mi = get_ci_mi(codeinst) |
| 157 | + sptypes = sptypes_from_meth_instance(mi) |
| 158 | + src = codeinfo.code |
| 159 | + for i = 1:length(src) |
| 160 | + stmt = src[i] |
| 161 | + isexpr(stmt, :(=)) && (stmt = stmt.args[2]) |
| 162 | + error = "" |
| 163 | + warn = false |
| 164 | + if isexpr(stmt, :invoke) || isexpr(stmt, :invoke_modify) |
| 165 | + error = "unresolved invoke" |
| 166 | + edge = stmt.args[1] |
| 167 | + if edge isa CodeInstance |
| 168 | + haskey(parents, edge) || (parents[edge] = (codeinst, i)) |
| 169 | + edge in inspected && continue |
| 170 | + end |
| 171 | + # TODO: check for calls to Base.atexit? |
| 172 | + elseif isexpr(stmt, :call) |
| 173 | + error = "unresolved call" |
| 174 | + farg = stmt.args[1] |
| 175 | + ftyp = widenconst(argextype(farg, codeinfo, sptypes)) |
| 176 | + if ftyp <: IntrinsicFunction |
| 177 | + #TODO: detect if f !== Core.Intrinsics.atomic_pointermodify (see statement_cost), otherwise error |
| 178 | + continue |
| 179 | + elseif ftyp <: Builtin |
| 180 | + if !may_dispatch(ftyp) |
| 181 | + continue |
| 182 | + end |
| 183 | + if Core._apply_iterate isa ftyp |
| 184 | + if length(stmt.args) >= 3 |
| 185 | + # args[1] is _apply_iterate object |
| 186 | + # args[2] is invoke object |
| 187 | + farg = stmt.args[3] |
| 188 | + ftyp = widenconst(argextype(farg, codeinfo, sptypes)) |
| 189 | + if may_dispatch(ftyp) |
| 190 | + error = "unresolved call to function" |
| 191 | + else |
| 192 | + for i in 4:length(stmt.args) |
| 193 | + atyp = widenconst(argextype(stmt.args[i], codeinfo, sptypes)) |
| 194 | + if !(atyp <: Union{SimpleVector, GenericMemory, Array, Tuple, NamedTuple}) |
| 195 | + error = "unresolved argument to call" |
| 196 | + break |
| 197 | + end |
| 198 | + end |
| 199 | + end |
| 200 | + end |
| 201 | + elseif Core.finalizer isa ftyp |
| 202 | + if length(stmt.args) == 3 |
| 203 | + # TODO: check that calling `args[1](args[2])` is defined before warning |
| 204 | + error = "unresolved finalizer registered" |
| 205 | + warn = true |
| 206 | + end |
| 207 | + else |
| 208 | + error = "unresolved call to builtin" |
| 209 | + end |
| 210 | + end |
| 211 | + extyp = argextype(SSAValue(i), codeinfo, sptypes) |
| 212 | + if extyp === Union{} |
| 213 | + warn = true # downgrade must-throw calls to be only a warning |
| 214 | + end |
| 215 | + elseif isexpr(stmt, :cfunction) |
| 216 | + error = "unresolved cfunction" |
| 217 | + #TODO: parse the cfunction expression to check the target is defined |
| 218 | + warn = true |
| 219 | + elseif isexpr(stmt, :foreigncall) |
| 220 | + foreigncall = stmt.args[1] |
| 221 | + if foreigncall isa QuoteNode |
| 222 | + if foreigncall.value in runtime_functions |
| 223 | + error = "disallowed ccall into a runtime function" |
| 224 | + end |
| 225 | + end |
| 226 | + elseif isexpr(stmt, :new_opaque_closure) |
| 227 | + error = "unresolved opaque closure" |
| 228 | + # TODO: check that this opaque closure has a valid signature for possible codegen and code defined for it |
| 229 | + warn = true |
| 230 | + end |
| 231 | + if !isempty(error) |
| 232 | + push!(errors, warn => CallMissing(codeinst, codeinfo, sptypes, i, error)) |
| 233 | + end |
| 234 | + end |
| 235 | +end |
| 236 | + |
| 237 | +## entry-point ## |
| 238 | + |
| 239 | +function get_verify_typeinf_trim(codeinfos::Vector{Any}) |
| 240 | + this_world = get_world_counter() |
| 241 | + inspected = IdSet{CodeInstance}() |
| 242 | + caches = IdDict{MethodInstance,CodeInstance}() |
| 243 | + errors = ErrorList() |
| 244 | + parents = ParentMap() |
| 245 | + for i = 1:2:length(codeinfos) |
| 246 | + item = codeinfos[i] |
| 247 | + if item isa CodeInstance |
| 248 | + push!(inspected, item) |
| 249 | + if item.owner === nothing && item.min_world <= this_world <= item.max_world |
| 250 | + mi = get_ci_mi(item) |
| 251 | + if mi === item.def |
| 252 | + caches[mi] = item |
| 253 | + end |
| 254 | + end |
| 255 | + end |
| 256 | + end |
| 257 | + for i = 1:2:length(codeinfos) |
| 258 | + item = codeinfos[i] |
| 259 | + if item isa CodeInstance |
| 260 | + src = codeinfos[i + 1]::CodeInfo |
| 261 | + verify_codeinstance!(item, src, inspected, caches, parents, errors) |
| 262 | + else |
| 263 | + rt = item::Type |
| 264 | + sig = codeinfos[i + 1]::Type |
| 265 | + ptr = ccall(:jl_get_specialization1, |
| 266 | + #= MethodInstance =# Ptr{Cvoid}, (Any, Csize_t, Cint), |
| 267 | + sig, this_world, #= mt_cache =# 0) |
| 268 | + asrt = Any |
| 269 | + valid = if ptr !== C_NULL |
| 270 | + mi = unsafe_pointer_to_objref(ptr)::MethodInstance |
| 271 | + ci = get(caches, mi, nothing) |
| 272 | + if ci isa CodeInstance |
| 273 | + # TODO: should we find a way to indicate to the user that this gets called via ccallable? |
| 274 | + # parent[ci] = something |
| 275 | + asrt = ci.rettype |
| 276 | + ci in inspected |
| 277 | + else |
| 278 | + false |
| 279 | + end |
| 280 | + else |
| 281 | + false |
| 282 | + end |
| 283 | + if !valid |
| 284 | + warn = false |
| 285 | + push!(errors, warn => CCallableMissing(rt, sig, "unresolved ccallable")) |
| 286 | + elseif !(asrt <: rt) |
| 287 | + warn = hasintersect(asrt, rt) |
| 288 | + push!(errors, warn => CCallableMissing(asrt, sig, "ccallable declared return type does not match inference")) |
| 289 | + end |
| 290 | + end |
| 291 | + end |
| 292 | + return (errors, parents) |
| 293 | +end |
| 294 | + |
| 295 | +# It is unclear if this file belongs in Compiler itself, or should instead be a codegen |
| 296 | +# driver / verifier implemented by juliac-buildscript.jl for the purpose of extensibility. |
| 297 | +# For now, it is part of Base.Compiler, but executed with invokelatest so that packages |
| 298 | +# could provide hooks to change, customize, or tweak its behavior and heuristics. |
| 299 | +Base.delete_method(Base.which(verify_typeinf_trim, (IO, Vector{Any}))) |
| 300 | +function verify_typeinf_trim(io::IO, codeinfos::Vector{Any}) |
| 301 | + errors, parents = get_verify_typeinf_trim(codeinfos) |
| 302 | + |
| 303 | + # count up how many messages we printed, of each severity |
| 304 | + counts = [0, 0] # errors, warnings |
| 305 | + io = IOContext{IO}(io) |
| 306 | + # print all errors afterwards, when the parents map is fully constructed |
| 307 | + for desc in errors |
| 308 | + warn, desc = desc |
| 309 | + severity = warn ? 2 : 1 |
| 310 | + no = (counts[severity] += 1) |
| 311 | + print(io, warn ? "Verifier warning #" : "Verifier error #", no, ": ") |
| 312 | + # TODO: should we coalesce any of these stacktraces to minimize spew? |
| 313 | + verify_print_error(io, desc, parents) |
| 314 | + end |
| 315 | + |
| 316 | + let severity = 0 |
| 317 | + if counts[2] > 0 |
| 318 | + print("Trim verify finished with ", counts[2], counts[2] == 1 ? " warning.\n\n" : " warnings.\n\n") |
| 319 | + severity = 2 |
| 320 | + end |
| 321 | + if counts[1] > 0 |
| 322 | + print("Trim verify finished with ", counts[1], counts[1] == 1 ? " error.\n\n" : " errors.\n\n") |
| 323 | + severity = 1 |
| 324 | + end |
| 325 | + # messages classified as errors are fatal, warnings are not |
| 326 | + 0 < severity <= 1 && error("verify_typeinf_trim failed") |
| 327 | + end |
| 328 | + nothing |
| 329 | +end |
0 commit comments