From f09909db00aeb2f06c66b1538bb26251306789e6 Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Mon, 27 Apr 2026 11:52:23 +0200 Subject: [PATCH 1/3] Add PassInstrumentation tests for JULIA_LLVM_ARGS Test that LLVM command-line flags passed via JULIA_LLVM_ARGS are propagated to the NewPM pipeline through StandardInstrumentations: - --print-after-all --filter-print-funcs=SomeFunction: function-level pass (NoOpFunctionPass) respects isFunctionInPrintList() - --print-changed=diff-quiet --filter-passes=globaldce: GlobalDCEPass removes a dead internal global and produces a diff; filter-passes uses the pipeline-facing name (globaldce, not GlobalDCEPass) - Custom Julia function pass (NewPMFunctionPass) is visible to pass instrumentation under its registered name Co-Authored-By: Claude Sonnet 4.6 --- test/newpm.jl | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/test/newpm.jl b/test/newpm.jl index e8fecbb9..3a20403c 100644 --- a/test/newpm.jl +++ b/test/newpm.jl @@ -573,6 +573,131 @@ end end end +@testset "PassInstrumentation" begin + # Verify that JULIA_LLVM_ARGS flags are propagated to the NewPM pipeline. + # We run a script in a subprocess so that clopts() doesn't mutate + # global LLVM state for the rest of the test suite. + # Use a function-level pass so that --filter-print-funcs applies. + # Module-level passes dump the whole module regardless of the filter; + # function-level passes respect isFunctionInPrintList(). + # Dead internal function so GlobalDCEPass has an actual IR change to produce. + # --print-changed tracks per-function changes, not global-variable removals, + # so a dead *function* is required to produce a diff. + # --filter-print-funcs=dead_func avoids log spam: Julia's JIT modules don't + # contain "dead_func", so only our pipeline triggers the diff. + script = """ + using LLVM + @dispose ctx=Context() begin + mod = LLVM.Module("test") + ft = LLVM.FunctionType(LLVM.VoidType()) + fn = LLVM.Function(mod, "SomeFunction", ft) + + @dispose builder=IRBuilder() begin + entry = BasicBlock(fn, "entry") + position!(builder, entry) + ret!(builder) + end + + dead_fn = LLVM.Function(mod, "dead_func", ft) + linkage!(dead_fn, LLVM.API.LLVMInternalLinkage) + @dispose builder=IRBuilder() begin + entry = BasicBlock(dead_fn, "entry") + position!(builder, entry) + ret!(builder) + end + + @dispose pb=NewPMPassBuilder() begin + add!(pb, NewPMModulePassManager()) do mpm + add!(mpm, GlobalDCEPass()) + add!(mpm, NewPMFunctionPassManager()) do fpm + add!(fpm, NoOpFunctionPass()) + end + end + run!(pb, mod) + end + end + """ + mktemp() do path, io + write(io, script) + flush(io) + + cmd = `$(Base.julia_cmd()) --project=$(Base.active_project()) $path` + + # Without flags: no IR dump output + out = IOBuffer(); err = IOBuffer() + @test success(pipeline(cmd, stdout=out, stderr=err)) + @test !occursin("IR Dump", String(take!(err))) + + # --print-after-all with --filter-print-funcs: only dumps IR for + # SomeFunction; function-level passes respect isFunctionInPrintList() + # whereas module-level passes dump unconditionally + cmd_print = addenv(cmd, "JULIA_LLVM_ARGS" => + "--print-after-all --filter-print-funcs=SomeFunction") + out = IOBuffer(); err = IOBuffer() + @test success(pipeline(cmd_print, stdout=out, stderr=err)) + output = String(take!(err)) + @test occursin("IR Dump After NoOpFunctionPass on SomeFunction", output) + + # --print-changed=diff-quiet --filter-passes=globaldce + # --filter-print-funcs=dead_func: GlobalDCEPass removes dead_func and + # emits a diff; the function filter keeps Julia's JIT output out + cmd_changed = addenv(cmd, "JULIA_LLVM_ARGS" => + "--print-changed=diff-quiet --filter-passes=globaldce --filter-print-funcs=dead_func") + out = IOBuffer(); err = IOBuffer() + @test success(pipeline(cmd_changed, stdout=out, stderr=err)) + output = String(take!(err)) + @test occursin("IR Dump After GlobalDCEPass on [module]", output) + @test occursin("- ret void", output) + end + + # Custom Julia function passes are registered under a user-chosen name and + # are visible to pass instrumentation just like built-in passes. + custom_script = """ + using LLVM + @dispose ctx=Context() begin + mod = LLVM.Module("test") + ft = LLVM.FunctionType(LLVM.VoidType()) + fn = LLVM.Function(mod, "SomeFunction", ft) + + @dispose builder=IRBuilder() begin + entry = BasicBlock(fn, "entry") + position!(builder, entry) + ret!(builder) + end + + function my_pass!(f::LLVM.Function) + return false # no changes + end + MyPass() = NewPMFunctionPass("my-pass", my_pass!) + + @dispose pb=NewPMPassBuilder() begin + register!(pb, MyPass()) + add!(pb, NewPMModulePassManager()) do mpm + add!(mpm, NewPMFunctionPassManager()) do fpm + add!(fpm, MyPass()) + end + end + run!(pb, mod) + end + end + """ + mktemp() do path, io + write(io, custom_script) + flush(io) + + cmd = `$(Base.julia_cmd()) --project=$(Base.active_project()) $path` + + # --print-after-all --filter-print-funcs=SomeFunction shows IR after + # the custom pass runs on SomeFunction + cmd_custom = addenv(cmd, "JULIA_LLVM_ARGS" => + "--print-after-all --filter-print-funcs=SomeFunction") + out = IOBuffer(); err = IOBuffer() + @test success(pipeline(cmd_custom, stdout=out, stderr=err)) + output = String(take!(err)) + @test occursin("IR Dump After JuliaCustomFunctionPass on SomeFunction", output) + end +end + @testset "alias analyses" begin # default pipeline @dispose ctx=Context() mod=test_module() pb=NewPMPassBuilder(debug_logging=true) begin From e3bbb947107ee727a39d597c253773d9887165ed Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Mon, 27 Apr 2026 12:07:50 +0200 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Valentin Churavy --- test/newpm.jl | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/test/newpm.jl b/test/newpm.jl index 3a20403c..27ef2ec8 100644 --- a/test/newpm.jl +++ b/test/newpm.jl @@ -577,14 +577,6 @@ end # Verify that JULIA_LLVM_ARGS flags are propagated to the NewPM pipeline. # We run a script in a subprocess so that clopts() doesn't mutate # global LLVM state for the rest of the test suite. - # Use a function-level pass so that --filter-print-funcs applies. - # Module-level passes dump the whole module regardless of the filter; - # function-level passes respect isFunctionInPrintList(). - # Dead internal function so GlobalDCEPass has an actual IR change to produce. - # --print-changed tracks per-function changes, not global-variable removals, - # so a dead *function* is required to produce a diff. - # --filter-print-funcs=dead_func avoids log spam: Julia's JIT modules don't - # contain "dead_func", so only our pipeline triggers the diff. script = """ using LLVM @dispose ctx=Context() begin @@ -628,9 +620,7 @@ end @test success(pipeline(cmd, stdout=out, stderr=err)) @test !occursin("IR Dump", String(take!(err))) - # --print-after-all with --filter-print-funcs: only dumps IR for - # SomeFunction; function-level passes respect isFunctionInPrintList() - # whereas module-level passes dump unconditionally + # `JULIA_LLVM_ARGS="--print-after-all --filter-print-funcs=SomeFunction"` cmd_print = addenv(cmd, "JULIA_LLVM_ARGS" => "--print-after-all --filter-print-funcs=SomeFunction") out = IOBuffer(); err = IOBuffer() @@ -638,9 +628,7 @@ end output = String(take!(err)) @test occursin("IR Dump After NoOpFunctionPass on SomeFunction", output) - # --print-changed=diff-quiet --filter-passes=globaldce - # --filter-print-funcs=dead_func: GlobalDCEPass removes dead_func and - # emits a diff; the function filter keeps Julia's JIT output out + # JULIA_LLVM_ARGS="--print-changed=diff-quiet --filter-passes=globaldce --filter-print-funcs=dead_func"` cmd_changed = addenv(cmd, "JULIA_LLVM_ARGS" => "--print-changed=diff-quiet --filter-passes=globaldce --filter-print-funcs=dead_func") out = IOBuffer(); err = IOBuffer() @@ -687,8 +675,7 @@ end cmd = `$(Base.julia_cmd()) --project=$(Base.active_project()) $path` - # --print-after-all --filter-print-funcs=SomeFunction shows IR after - # the custom pass runs on SomeFunction + # `JULIA_LLVM_ARGS="--print-after-all --filter-print-funcs=SomeFunction` cmd_custom = addenv(cmd, "JULIA_LLVM_ARGS" => "--print-after-all --filter-print-funcs=SomeFunction") out = IOBuffer(); err = IOBuffer() From 2a791bc3cc92c95596ed751a7f28d381879d02ca Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Mon, 27 Apr 2026 17:52:06 +0200 Subject: [PATCH 3/3] fixup! Add PassInstrumentation tests for JULIA_LLVM_ARGS --- test/newpm.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/newpm.jl b/test/newpm.jl index 27ef2ec8..5e3811c8 100644 --- a/test/newpm.jl +++ b/test/newpm.jl @@ -628,14 +628,13 @@ end output = String(take!(err)) @test occursin("IR Dump After NoOpFunctionPass on SomeFunction", output) - # JULIA_LLVM_ARGS="--print-changed=diff-quiet --filter-passes=globaldce --filter-print-funcs=dead_func"` + # JULIA_LLVM_ARGS="--print-changed=quiet --filter-passes=globaldce --filter-print-funcs=dead_func" cmd_changed = addenv(cmd, "JULIA_LLVM_ARGS" => - "--print-changed=diff-quiet --filter-passes=globaldce --filter-print-funcs=dead_func") + "--print-changed=quiet --filter-passes=globaldce --filter-print-funcs=dead_func") out = IOBuffer(); err = IOBuffer() @test success(pipeline(cmd_changed, stdout=out, stderr=err)) output = String(take!(err)) - @test occursin("IR Dump After GlobalDCEPass on [module]", output) - @test occursin("- ret void", output) + @test occursin("IR Deleted After GlobalDCEPass", output) end # Custom Julia function passes are registered under a user-chosen name and