Skip to content

Commit b8bb53f

Browse files
committed
[Compiler] new approach to verify --trim output
Move all this code into the Compiler julia code, where we have much better utilities for printing and observability for analysis.
1 parent 8a31ad6 commit b8bb53f

File tree

14 files changed

+450
-343
lines changed

14 files changed

+450
-343
lines changed

Compiler/src/Compiler.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,19 @@ macro __SOURCE_FILE__()
189189
return QuoteNode(__source__.file::Symbol)
190190
end
191191

192-
module IRShow end
192+
module IRShow end # relies on string and IO operations defined in Base
193+
baremodule TrimVerifier end # relies on IRShow, so define this afterwards
194+
193195
function load_irshow!()
194196
if isdefined(Base, :end_base_include)
195197
# This code path is exclusively for Revise, which may want to re-run this
196198
# after bootstrap.
197-
include(IRShow, Base.joinpath(Base.dirname(Base.String(@__SOURCE_FILE__)), "ssair/show.jl"))
199+
Compilerdir = Base.dirname(Base.String(@__SOURCE_FILE__))
200+
include(IRShow, Base.joinpath(Compilerdir, "ssair/show.jl"))
201+
include(TrimVerifier, Base.joinpath(Compilerdir, "verifytrim.jl"))
198202
else
199203
include(IRShow, "ssair/show.jl")
204+
include(TrimVerifier, "verifytrim.jl")
200205
end
201206
end
202207
if !isdefined(Base, :end_base_include)

Compiler/src/typeinfer.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,15 +1353,17 @@ function typeinf_ext_toplevel(methods::Vector{Any}, worlds::Vector{UInt}, trim::
13531353
end
13541354
push!(codeinfos, callee)
13551355
push!(codeinfos, src)
1356-
elseif trim
1357-
println("warning: failed to get code for ", mi)
13581356
end
13591357
end
13601358
latest = false
13611359
end
1360+
trim && verify_typeinf_trim(codeinfos)
13621361
return codeinfos
13631362
end
13641363

1364+
verify_typeinf_trim(io::IO, codeinfos::Vector{Any}) = error("--trim option not defined")
1365+
verify_typeinf_trim(codeinfos::Vector{Any}) = invokelatest(verify_typeinf_trim, stdout, codeinfos)
1366+
13651367
function return_type(@nospecialize(f), t::DataType) # this method has a special tfunc
13661368
world = tls_world_age()
13671369
args = Any[_return_type, NativeInterpreter(world), Tuple{Core.Typeof(f), t.parameters...}]

Compiler/src/verifytrim.jl

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
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

Compiler/test/testgroups

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ tarjan
1616
validation
1717
special_loading
1818
abioverride
19+
verifytrim

0 commit comments

Comments
 (0)