From 9d1d9c43ec66fcae79d780f277933a5ff6983912 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Wed, 24 Jun 2026 09:04:51 +0200 Subject: [PATCH 1/3] Migrate test/passes.jl to FileCheck.jl Replace LLVM.jl IR introspection in the dead-arg removal pass tests with FileCheck directives over the printed post-pass module. The hand-written input IR is unchanged; only the assertion mechanism changes. Adds FileCheck to the test project. Co-Authored-By: Claude Opus 4.8 --- test/Project.toml | 2 + test/passes.jl | 323 +++++++++++++++++++++++++--------------------- 2 files changed, 176 insertions(+), 149 deletions(-) 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..b1e46a9e05 100644 --- a/test/passes.jl +++ b/test/passes.jl @@ -1,172 +1,197 @@ -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. + @check_label "@inner" + @check "store atomic ptr addrspace(10) %v1" + @check "store atomic ptr addrspace(10) %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(i32* %arg1, i32* %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 From 808bba432221d082d5b7978c321f903f0fbbba7c Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Tue, 30 Jun 2026 11:14:40 +0200 Subject: [PATCH 2/3] Fix DAE safety FileCheck for opaque pointers The mismatched-calling-convention testset hard-coded typed-pointer syntax (i32* %arg1, i32* %arg2) in its @check, but under opaque pointers the callee prints as define internal fastcc void @callee(ptr nocapture writeonly %arg1, ...) Match the intent instead: the callee is preserved with both arguments. Co-Authored-By: Claude Opus 4.8 --- test/passes.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/passes.jl b/test/passes.jl index b1e46a9e05..1bf4e71bb5 100644 --- a/test/passes.jl +++ b/test/passes.jl @@ -168,7 +168,9 @@ end @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(i32* %arg1, i32* %arg2)" + @check "define internal fastcc void @callee(" + @check_same "%arg1" + @check_same "%arg2" LLVM.Context() do ctx mod = parse( LLVM.Module, """ From f235e4c7e1c3b23b64aaa49c3f2657516257ee21 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Mon, 6 Jul 2026 13:27:48 +0200 Subject: [PATCH 3/3] Make store-preservation FileCheck pointer-type agnostic The Partial return preservation testset hard-coded opaque-pointer syntax (store atomic ptr addrspace(10) %v1), which fails on Julia 1.10/1.11 default configs that print typed pointers (store atomic {} addrspace(10)* %v1). Match on the stored value instead so it works under both. Co-Authored-By: Claude Opus 4.8 --- test/passes.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/passes.jl b/test/passes.jl index 1bf4e71bb5..7ff022f916 100644 --- a/test/passes.jl +++ b/test/passes.jl @@ -4,9 +4,13 @@ using Enzyme, LLVM, Test, FileCheck @testset "Partial return preservation" begin @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 ptr addrspace(10) %v1" - @check "store atomic ptr addrspace(10) %v2" + @check "store atomic" + @check_same "%v1" + @check "store atomic" + @check_same "%v2" LLVM.Context() do ctx mod = parse( LLVM.Module, """