diff --git a/test/Project.toml b/test/Project.toml index 87bbd781dd..e306e1369b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -6,6 +6,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" EnzymeTestUtils = "12d8515a-0907-448a-8884-5fe00fdf1c5a" Enzyme_jll = "7cc45869-7501-5eee-bdea-0790c847d4ef" +FileCheck = "4e644321-382b-4b05-b0b6-5d23c3d944fb" FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" GPUCompiler = "61eb1bfa-7361-4325-ad38-22787b887f55" InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" @@ -30,4 +31,5 @@ EnzymeTestUtils = {path = "../lib/EnzymeTestUtils"} [compat] EnzymeTestUtils = "0.2.1" +FileCheck = "1.2" ParallelTestRunner = "2.0.1" diff --git a/test/passes.jl b/test/passes.jl index 5a80a920e0..7ff022f916 100644 --- a/test/passes.jl +++ b/test/passes.jl @@ -1,172 +1,203 @@ -using Enzyme, LLVM, Test +using Enzyme, LLVM, Test, FileCheck @testset "Partial return preservation" begin - LLVM.Context() do ctx - mod = parse(LLVM.Module, """ - source_filename = "start" - target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128-ni:10:11:12:13" - target triple = "x86_64-linux-gnu" - - declare noalias nonnull {} addrspace(10)* @julia.gc_alloc_obj({}**, i64, {} addrspace(10)*) local_unnamed_addr #5 - - define internal fastcc nonnull {} addrspace(10)* @inner({} addrspace(10)* %v1, {} addrspace(10)* %v2) { - top: - %newstruct = call noalias nonnull dereferenceable(16) {} addrspace(10)* @julia.gc_alloc_obj({}** null, i64 16, {} addrspace(10)* addrspacecast ({}* inttoptr (i64 129778359735376 to {}*) to {} addrspace(10)*)) #30 - %a31 = addrspacecast {} addrspace(10)* %newstruct to {} addrspace(10)* addrspace(11)* - %a32 = getelementptr inbounds {} addrspace(10)*, {} addrspace(10)* addrspace(11)* %a31, i64 1 - store atomic {} addrspace(10)* %v1, {} addrspace(10)* addrspace(11)* %a31 release, align 8 - %a33 = addrspacecast {} addrspace(10)* %newstruct to i8 addrspace(11)* - %a34 = getelementptr inbounds i8, i8 addrspace(11)* %a33, i64 8 - %a35 = bitcast i8 addrspace(11)* %a34 to {} addrspace(10)* addrspace(11)* - store atomic {} addrspace(10)* %v2, {} addrspace(10)* addrspace(11)* %a35 release, align 8 - ret {} addrspace(10)* %newstruct - } - - define {} addrspace(10)* @caller({} addrspace(10)* %v1, {} addrspace(10)* %v2) { - top: - %ac = call fastcc nonnull {} addrspace(10)* @inner({} addrspace(10)* %v1, {} addrspace(10)* %v2) - %b = addrspacecast {} addrspace(10)* %ac to {} addrspace(10)* addrspace(11)* - %c = load atomic {} addrspace(10)*, {} addrspace(10)* addrspace(11)* %b unordered, align 8 - ret {} addrspace(10)* %c - } - - attributes #5 = { inaccessiblememonly mustprogress nofree nounwind willreturn allockind("alloc,uninitialized") allocsize(1) "enzyme_no_escaping_allocation" "enzymejl_world"="31504" } - """) - - Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), false) - - callfn = LLVM.functions(mod)["inner"] - @test length(collect(filter(Base.Fix2(isa, LLVM.StoreInst), collect(instructions(first(blocks(callfn))))))) == 2 + @test @filecheck begin + # Both stores into the freshly allocated struct must be preserved. + # Match without spelling out the pointer type so this works under both + # opaque (`ptr addrspace(10)`) and typed (`{} addrspace(10)*`) pointers. + @check_label "@inner" + @check "store atomic" + @check_same "%v1" + @check "store atomic" + @check_same "%v2" + LLVM.Context() do ctx + mod = parse( + LLVM.Module, """ + source_filename = "start" + target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128-ni:10:11:12:13" + target triple = "x86_64-linux-gnu" + + declare noalias nonnull {} addrspace(10)* @julia.gc_alloc_obj({}**, i64, {} addrspace(10)*) local_unnamed_addr #5 + + define internal fastcc nonnull {} addrspace(10)* @inner({} addrspace(10)* %v1, {} addrspace(10)* %v2) { + top: + %newstruct = call noalias nonnull dereferenceable(16) {} addrspace(10)* @julia.gc_alloc_obj({}** null, i64 16, {} addrspace(10)* addrspacecast ({}* inttoptr (i64 129778359735376 to {}*) to {} addrspace(10)*)) #30 + %a31 = addrspacecast {} addrspace(10)* %newstruct to {} addrspace(10)* addrspace(11)* + %a32 = getelementptr inbounds {} addrspace(10)*, {} addrspace(10)* addrspace(11)* %a31, i64 1 + store atomic {} addrspace(10)* %v1, {} addrspace(10)* addrspace(11)* %a31 release, align 8 + %a33 = addrspacecast {} addrspace(10)* %newstruct to i8 addrspace(11)* + %a34 = getelementptr inbounds i8, i8 addrspace(11)* %a33, i64 8 + %a35 = bitcast i8 addrspace(11)* %a34 to {} addrspace(10)* addrspace(11)* + store atomic {} addrspace(10)* %v2, {} addrspace(10)* addrspace(11)* %a35 release, align 8 + ret {} addrspace(10)* %newstruct + } + + define {} addrspace(10)* @caller({} addrspace(10)* %v1, {} addrspace(10)* %v2) { + top: + %ac = call fastcc nonnull {} addrspace(10)* @inner({} addrspace(10)* %v1, {} addrspace(10)* %v2) + %b = addrspacecast {} addrspace(10)* %ac to {} addrspace(10)* addrspace(11)* + %c = load atomic {} addrspace(10)*, {} addrspace(10)* addrspace(11)* %b unordered, align 8 + ret {} addrspace(10)* %c + } + + attributes #5 = { inaccessiblememonly mustprogress nofree nounwind willreturn allockind("alloc,uninitialized") allocsize(1) "enzyme_no_escaping_allocation" "enzymejl_world"="31504" } + """ + ) + + Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), false) + string(mod) + end end end @testset "Dead return removal" begin - LLVM.Context() do ctx - mod = parse(LLVM.Module, """ - source_filename = "start" - target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128-ni:10:11:12:13" - target triple = "x86_64-linux-gnu" - - declare noalias nonnull {} addrspace(10)* @julia.gc_alloc_obj({}**, i64, {} addrspace(10)*) local_unnamed_addr #5 - - define internal fastcc nonnull {} addrspace(10)* @julia_MyPrognosticVars_161({} addrspace(10)* %v1, {} addrspace(10)* %v2) { - top: - %newstruct = call noalias nonnull dereferenceable(16) {} addrspace(10)* @julia.gc_alloc_obj({}** null, i64 16, {} addrspace(10)* addrspacecast ({}* inttoptr (i64 129778359735376 to {}*) to {} addrspace(10)*)) #30 - %a31 = addrspacecast {} addrspace(10)* %newstruct to {} addrspace(10)* addrspace(11)* - %a32 = getelementptr inbounds {} addrspace(10)*, {} addrspace(10)* addrspace(11)* %a31, i64 1 - store atomic {} addrspace(10)* %v1, {} addrspace(10)* addrspace(11)* %a31 release, align 8 - %a33 = addrspacecast {} addrspace(10)* %newstruct to i8 addrspace(11)* - %a34 = getelementptr inbounds i8, i8 addrspace(11)* %a33, i64 8 - %a35 = bitcast i8 addrspace(11)* %a34 to {} addrspace(10)* addrspace(11)* - store atomic {} addrspace(10)* %v2, {} addrspace(10)* addrspace(11)* %a35 release, align 8 - ret {} addrspace(10)* %newstruct - } - - define void @caller({} addrspace(10)* %v1, {} addrspace(10)* %v2) { - top: - %ac = call fastcc nonnull {} addrspace(10)* @julia_MyPrognosticVars_161({} addrspace(10)* %v1, {} addrspace(10)* %v2) - ret void - } - - attributes #5 = { inaccessiblememonly mustprogress nofree nounwind willreturn allockind("alloc,uninitialized") allocsize(1) "enzyme_no_escaping_allocation" "enzymejl_world"="31504" } - """) - - Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), false) - callfn = LLVM.functions(mod)["caller"] - @test length(collect(instructions(first(blocks(callfn))))) == 1 + @test @filecheck begin + # The call to the (now dead) callee is removed, leaving only `ret void`, + # and the callee itself is deleted from the module. + @check_label "define void @caller" + @check_next "top:" + @check_next "ret void" + @check_not "@julia_MyPrognosticVars_161" + LLVM.Context() do ctx + mod = parse( + LLVM.Module, """ + source_filename = "start" + target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128-ni:10:11:12:13" + target triple = "x86_64-linux-gnu" + + declare noalias nonnull {} addrspace(10)* @julia.gc_alloc_obj({}**, i64, {} addrspace(10)*) local_unnamed_addr #5 + + define internal fastcc nonnull {} addrspace(10)* @julia_MyPrognosticVars_161({} addrspace(10)* %v1, {} addrspace(10)* %v2) { + top: + %newstruct = call noalias nonnull dereferenceable(16) {} addrspace(10)* @julia.gc_alloc_obj({}** null, i64 16, {} addrspace(10)* addrspacecast ({}* inttoptr (i64 129778359735376 to {}*) to {} addrspace(10)*)) #30 + %a31 = addrspacecast {} addrspace(10)* %newstruct to {} addrspace(10)* addrspace(11)* + %a32 = getelementptr inbounds {} addrspace(10)*, {} addrspace(10)* addrspace(11)* %a31, i64 1 + store atomic {} addrspace(10)* %v1, {} addrspace(10)* addrspace(11)* %a31 release, align 8 + %a33 = addrspacecast {} addrspace(10)* %newstruct to i8 addrspace(11)* + %a34 = getelementptr inbounds i8, i8 addrspace(11)* %a33, i64 8 + %a35 = bitcast i8 addrspace(11)* %a34 to {} addrspace(10)* addrspace(11)* + store atomic {} addrspace(10)* %v2, {} addrspace(10)* addrspace(11)* %a35 release, align 8 + ret {} addrspace(10)* %newstruct + } + + define void @caller({} addrspace(10)* %v1, {} addrspace(10)* %v2) { + top: + %ac = call fastcc nonnull {} addrspace(10)* @julia_MyPrognosticVars_161({} addrspace(10)* %v1, {} addrspace(10)* %v2) + ret void + } + + attributes #5 = { inaccessiblememonly mustprogress nofree nounwind willreturn allockind("alloc,uninitialized") allocsize(1) "enzyme_no_escaping_allocation" "enzymejl_world"="31504" } + """ + ) + + Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), false) + string(mod) + end end end @testset "Return roots preservation" begin - LLVM.Context() do ctx - mod = parse(LLVM.Module, """ - define private void @julia_dims_4189({ double, {} addrspace(10)*, {} addrspace(10)* }* sret({ double, {} addrspace(10)*, {} addrspace(10)* }) %res, [2 x {} addrspace(10)*]* "enzymejl_returnRoots"="2", double addrspace(11)* %data) #0 { - top: - %val = load double, double addrspace(11)* %data, align 8 - store { double, {} addrspace(10)*, {} addrspace(10)* } zeroinitializer, { double, {} addrspace(10)*, {} addrspace(10)* }* %res - ret void - } - - define void @caller({} addrspace(10)* %v1, {} addrspace(10)* %v2, double addrspace(11)* %data) { - top: - %sret = alloca { double, {} addrspace(10)*, {} addrspace(10)* } - %roots = alloca [2 x {} addrspace(10)*] - call void @julia_dims_4189({ double, {} addrspace(10)*, {} addrspace(10)* }* sret({ double, {} addrspace(10)*, {} addrspace(10)* }) %sret, [2 x {} addrspace(10)*]* "enzymejl_returnRoots"="2" %roots, double addrspace(11)* %data) - ret void - } - - attributes #0 = { nofree nosync nounwind willreturn noinline "enzyme_inactive" } - """) - - Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), true) - - caller = LLVM.functions(mod)["caller"] - - insts = collect(instructions(first(blocks(caller)))) - calls = collect(filter(i -> isa(i, LLVM.CallInst), insts)) - - @test length(calls) == 1 - if length(calls) == 1 - call = calls[1] - @test length(operands(call)) == 3 # 2 arg (sret + roots) + called function + @test @filecheck begin + # The dead `%data` argument is dropped from the call, but the `sret` and + # `enzymejl_returnRoots` arguments must be preserved (roots last). + @check_label "define void @caller" + @check "call void @julia_dims_4189" + @check_same "sret" + @check_same "enzymejl_returnRoots" + @check_same "%roots)" + LLVM.Context() do ctx + mod = parse( + LLVM.Module, """ + define private void @julia_dims_4189({ double, {} addrspace(10)*, {} addrspace(10)* }* sret({ double, {} addrspace(10)*, {} addrspace(10)* }) %res, [2 x {} addrspace(10)*]* "enzymejl_returnRoots"="2", double addrspace(11)* %data) #0 { + top: + %val = load double, double addrspace(11)* %data, align 8 + store { double, {} addrspace(10)*, {} addrspace(10)* } zeroinitializer, { double, {} addrspace(10)*, {} addrspace(10)* }* %res + ret void + } + + define void @caller({} addrspace(10)* %v1, {} addrspace(10)* %v2, double addrspace(11)* %data) { + top: + %sret = alloca { double, {} addrspace(10)*, {} addrspace(10)* } + %roots = alloca [2 x {} addrspace(10)*] + call void @julia_dims_4189({ double, {} addrspace(10)*, {} addrspace(10)* }* sret({ double, {} addrspace(10)*, {} addrspace(10)* }) %sret, [2 x {} addrspace(10)*]* "enzymejl_returnRoots"="2" %roots, double addrspace(11)* %data) + ret void + } + + attributes #0 = { nofree nosync nounwind willreturn noinline "enzyme_inactive" } + """ + ) + + Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), true) + string(mod) end end end @testset "Recursively dead function removal" begin - LLVM.Context() do ctx - mod = parse(LLVM.Module, """ - source_filename = "start" - target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128-ni:10:11:12:13" - target triple = "x86_64-linux-gnu" - - define internal fastcc void @dead_callee(i32* nocapture %arg) { - top: - %val = load i32, i32* %arg, align 4 - ret void - } - - define internal fastcc void @dead_recursive_fn(i32* %arg) { - top: - call fastcc void @dead_recursive_fn(i32* %arg) - call fastcc void @dead_callee(i32* %arg) - ret void - } - """) - - Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), false) - - @test !haskey(LLVM.functions(mod), "dead_recursive_fn") - @test !haskey(LLVM.functions(mod), "dead_callee") + @test @filecheck begin + # Both the recursive function and its callee are dead and must be removed. + @check_not "@dead_recursive_fn" + @check_not "@dead_callee" + LLVM.Context() do ctx + mod = parse( + LLVM.Module, """ + source_filename = "start" + target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128-ni:10:11:12:13" + target triple = "x86_64-linux-gnu" + + define internal fastcc void @dead_callee(i32* nocapture %arg) { + top: + %val = load i32, i32* %arg, align 4 + ret void + } + + define internal fastcc void @dead_recursive_fn(i32* %arg) { + top: + call fastcc void @dead_recursive_fn(i32* %arg) + call fastcc void @dead_callee(i32* %arg) + ret void + } + """ + ) + + Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), false) + string(mod) + end end end @testset "Mismatched calling convention/function type DAE safety" begin - LLVM.Context() do ctx - mod = parse(LLVM.Module, """ - source_filename = "start" - target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128-ni:10:11:12:13" - target triple = "x86_64-linux-gnu" - - define internal fastcc void @callee(i32* %arg1, i32* %arg2) { - top: - store i32 42, i32* %arg1, align 4 - ret void - } - - define void @caller(i32 addrspace(10)* %arg1, i32 addrspace(10)* %arg2) { - top: - call fastcc void (i32 addrspace(10)*, i32 addrspace(10)*) bitcast (void (i32*, i32*)* @callee to void (i32 addrspace(10)*, i32 addrspace(10)*)*)(i32 addrspace(10)* %arg1, i32 addrspace(10)* %arg2) - ret void - } - """) - - Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), false) - - @test haskey(LLVM.functions(mod), "callee") - callee = LLVM.functions(mod)["callee"] - @test length(LLVM.parameters(callee)) == 2 + @test @filecheck begin + # The call site bitcasts the callee to a mismatched function type, so DAE + # must leave the callee untouched: both of its arguments are preserved. + @check "define internal fastcc void @callee(" + @check_same "%arg1" + @check_same "%arg2" + LLVM.Context() do ctx + mod = parse( + LLVM.Module, """ + source_filename = "start" + target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128-ni:10:11:12:13" + target triple = "x86_64-linux-gnu" + + define internal fastcc void @callee(i32* %arg1, i32* %arg2) { + top: + store i32 42, i32* %arg1, align 4 + ret void + } + + define void @caller(i32 addrspace(10)* %arg1, i32 addrspace(10)* %arg2) { + top: + call fastcc void (i32 addrspace(10)*, i32 addrspace(10)*) bitcast (void (i32*, i32*)* @callee to void (i32 addrspace(10)*, i32 addrspace(10)*)*)(i32 addrspace(10)* %arg1, i32 addrspace(10)* %arg2) + ret void + } + """ + ) + + Enzyme.Compiler.removeDeadArgs!(mod, Enzyme.Compiler.JIT.get_tm(), false) + string(mod) + end end end