Skip to content

Commit 92b8b37

Browse files
committed
Add device compilation static assertions
1 parent 6b41de3 commit 92b8b37

4 files changed

Lines changed: 146 additions & 2 deletions

File tree

src/GPUCompiler.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ include("driver.jl")
7777

7878
# other reusable functionality
7979
include("execution.jl")
80+
include("static_assert.jl")
8081
include("reflection.jl")
8182

8283
include("precompile.jl")

src/static_assert.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
export @static_assert
2+
3+
"""
4+
@static_assert condition message
5+
6+
Require `condition` to be proven true while compiling device code. Unlike
7+
`Base.@static`, the condition is evaluated after target-specific values have
8+
been propagated through LLVM. A condition that remains dynamic is rejected;
9+
this is not a runtime assertion mechanism.
10+
11+
`message` must be a string literal. It is reported with the device-code
12+
backtrace if the assertion cannot be proven.
13+
"""
14+
macro static_assert(condition, message)
15+
message isa String || throw(ArgumentError("@static_assert message must be a string literal"))
16+
marker = static_assert_marker(message)
17+
return quote
18+
if !$(esc(condition))
19+
$marker
20+
end
21+
nothing
22+
end
23+
end
24+
25+
const STATIC_ASSERT_MARKER = "gpu_static_assert"
26+
const STATIC_ASSERTION = "static assertion failed"
27+
28+
function static_assert_marker(message::String)
29+
LLVM.Context() do _
30+
entry, entry_type = LLVM.Interop.create_function()
31+
mod = LLVM.parent(entry)
32+
@dispose builder=IRBuilder() begin
33+
block = BasicBlock(entry, "entry")
34+
position!(builder, block)
35+
36+
# LLVM.jl owns the pointer representation and creates an anonymous private
37+
# string global, just like LLVM's annotation helpers.
38+
string = globalstring_ptr!(builder, message)
39+
marker_type = LLVM.FunctionType(LLVM.VoidType(), [value_type(string)])
40+
marker = LLVM.Function(mod, STATIC_ASSERT_MARKER, marker_type)
41+
call!(builder, marker_type, marker, [string])
42+
ret!(builder)
43+
end
44+
return LLVM.Interop.call_function(entry, Nothing, Tuple{})
45+
end
46+
end
47+
48+
function static_assert_message(inst::LLVM.CallInst)
49+
try
50+
value = only(arguments(inst))
51+
while value isa ConstantExpr
52+
value = first(operands(value))
53+
end
54+
value isa GlobalVariable || return nothing
55+
initializer = LLVM.initializer(value)
56+
initializer isa ConstantDataSequential || initializer isa ConstantArray || return nothing
57+
values = initializer isa ConstantDataSequential ? collect(initializer) : operands(initializer)
58+
bytes = UInt8[convert(UInt8, byte) for byte in values]
59+
!isempty(bytes) && bytes[end] == 0x00 && pop!(bytes)
60+
return String(bytes)
61+
catch err
62+
err isa ArgumentError || err isa BoundsError || rethrow()
63+
return nothing
64+
end
65+
end

src/validation.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,15 @@ const DYNAMIC_CALL = "dynamic function invocation"
135135
function Base.showerror(io::IO, err::InvalidIRError)
136136
print(io, "InvalidIRError: compiling ", err.job.source, " resulted in invalid LLVM IR")
137137
for (kind, bt, meta) in err.errors
138-
printstyled(io, "\nReason: unsupported $kind"; color=:red)
138+
prefix = kind == STATIC_ASSERTION ? "Reason: $kind" : "Reason: unsupported $kind"
139+
printstyled(io, "\n$prefix"; color=:red)
139140
if meta !== nothing
140141
if kind == RUNTIME_FUNCTION || kind == UNKNOWN_FUNCTION || kind == POINTER_FUNCTION || kind == DYNAMIC_CALL || kind == CCALL_FUNCTION || kind == LAZY_FUNCTION
141142
printstyled(io, " (call to ", meta, ")"; color=:red)
142143
elseif kind == DELAYED_BINDING
143144
printstyled(io, " (use of '", meta, "')"; color=:red)
145+
elseif kind == STATIC_ASSERTION
146+
printstyled(io, " (", meta, ")"; color=:red)
144147
end
145148
end
146149
Base.show_backtrace(io, bt)
@@ -225,7 +228,9 @@ function check_ir!(job, errors::Vector{IRError}, inst::LLVM.CallInst)
225228
fn = LLVM.name(dest)
226229

227230
# some special handling for runtime functions that we don't implement
228-
if fn == "jl_get_binding_or_error" || fn == "ijl_get_binding_or_error"
231+
if fn == STATIC_ASSERT_MARKER
232+
push!(errors, (STATIC_ASSERTION, bt, static_assert_message(inst)))
233+
elseif fn == "jl_get_binding_or_error" || fn == "ijl_get_binding_or_error"
229234
try
230235
m, sym = arguments(inst)
231236
sym = first(operands(sym::ConstantExpr))::ConstantInt

test/native.jl

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,79 @@ end
848848
end
849849
end
850850

851+
@testset "static assertions" begin
852+
mod = @eval module $(gensym())
853+
using ..GPUCompiler
854+
kernel() = (@static_assert true "this should disappear"; return)
855+
end
856+
857+
llvm = sprint(io -> Native.code_llvm(io, mod.kernel, Tuple{}; dump_module=true))
858+
@test !occursin(GPUCompiler.STATIC_ASSERT_MARKER, llvm)
859+
@test Native.code_execution(mod.kernel, Tuple{}) !== nothing
860+
861+
mod = @eval module $(gensym())
862+
using ..GPUCompiler
863+
kernel() = (@static_assert false "the target is too old"; return)
864+
end
865+
@test_throws_message(InvalidIRError,
866+
Native.code_execution(mod.kernel, Tuple{})) do msg
867+
occursin(GPUCompiler.STATIC_ASSERTION, msg) &&
868+
occursin("the target is too old", msg) &&
869+
occursin("kernel", msg)
870+
end
871+
872+
mod = @eval module $(gensym())
873+
using ..GPUCompiler
874+
function kernel(condition)
875+
@static_assert condition "condition was not proven"
876+
return
877+
end
878+
end
879+
@test_throws_message(InvalidIRError,
880+
Native.code_execution(mod.kernel, Tuple{Bool}; opt_level=0)) do msg
881+
occursin(GPUCompiler.STATIC_ASSERTION, msg) &&
882+
occursin("condition was not proven", msg)
883+
end
884+
885+
mod = @eval module $(gensym())
886+
using ..GPUCompiler
887+
function kernel()
888+
if false
889+
@static_assert false "dead assertion"
890+
end
891+
return
892+
end
893+
end
894+
@test Native.code_execution(mod.kernel, Tuple{}; opt_level=0) !== nothing
895+
896+
mod = @eval module $(gensym())
897+
using ..GPUCompiler
898+
function kernel(condition)
899+
@static_assert condition "first failure"
900+
@static_assert condition "second failure"
901+
return
902+
end
903+
end
904+
@test_throws_message(InvalidIRError,
905+
Native.code_execution(mod.kernel, Tuple{Bool})) do msg
906+
occursin("first failure", msg) && occursin("second failure", msg) &&
907+
!occursin("unknown function", msg)
908+
end
909+
910+
mod = @eval module $(gensym())
911+
using ..GPUCompiler
912+
@inline assertion() = @static_assert false "inlined failure"
913+
kernel() = (assertion(); return)
914+
end
915+
@test_throws_message(InvalidIRError,
916+
Native.code_execution(mod.kernel, Tuple{})) do msg
917+
occursin("inlined failure", msg) &&
918+
occursin("assertion", msg) && occursin("kernel", msg)
919+
end
920+
921+
@test_throws ArgumentError macroexpand(mod, :(@static_assert true string("message")))
922+
end
923+
851924
@testset "invalid LLVM IR (ccall)" begin
852925
mod = @eval module $(gensym())
853926
function foobar(p)

0 commit comments

Comments
 (0)