[llvm] [opt] Do not exit on first emitError (PR #194858)

Arseniy Obolenskiy via llvm-commits llvm-commits at lists.llvm.org
Wed Apr 29 06:51:56 PDT 2026


https://github.com/aobolensk updated https://github.com/llvm/llvm-project/pull/194858

>From d4276202fae2921f382ca131894e852b04bccad0 Mon Sep 17 00:00:00 2001
From: Arseniy Obolenskiy <arseniy.obolenskiy at amd.com>
Date: Wed, 29 Apr 2026 15:22:20 +0200
Subject: [PATCH] [opt] Do not exit on first emitError

- opt default diagnostic handler calls exit(1) on the first emitError, so later passes don't run and behavior diverges from llc
- set OptDiagnosticHandler that collects all the errors and then exits with non-zero exit code after reporting them
- Fix 2 issues related to the behavior change:
  * IR2VecPrinterPass asserted on an invalid vocabulary after the analysis already emitted an error
  * InlinerPass::getAdvisor overwrote OwnedAdvisor with the null result of getReplayInlineAdvisor on a missing replay file

related to #142014
---
 llvm/lib/Analysis/IR2Vec.cpp                  |  3 +-
 llvm/lib/Transforms/IPO/Inliner.cpp           | 20 +++++++------
 .../AtomicExpand/AMDGPU/unaligned-atomic.ll   |  2 +-
 .../NVPTX/safestack-libcall-error.ll          |  4 +--
 llvm/test/tools/opt/diagnostic-handler.ll     | 28 +++++++++++++++++++
 llvm/tools/opt/NewPMDriver.cpp                | 24 ++++++++++++++++
 llvm/tools/opt/NewPMDriver.h                  | 10 +++++++
 llvm/tools/opt/optdriver.cpp                  |  9 ++++++
 8 files changed, 87 insertions(+), 13 deletions(-)
 create mode 100644 llvm/test/tools/opt/diagnostic-handler.ll

diff --git a/llvm/lib/Analysis/IR2Vec.cpp b/llvm/lib/Analysis/IR2Vec.cpp
index 55b10c3cf2bc8..e24a08b3b8a65 100644
--- a/llvm/lib/Analysis/IR2Vec.cpp
+++ b/llvm/lib/Analysis/IR2Vec.cpp
@@ -671,7 +671,8 @@ IR2VecVocabAnalysis::run(Module &M, ModuleAnalysisManager &AM) {
 PreservedAnalyses IR2VecPrinterPass::run(Module &M,
                                          ModuleAnalysisManager &MAM) {
   auto &Vocabulary = MAM.getResult<IR2VecVocabAnalysis>(M);
-  assert(Vocabulary.isValid() && "IR2Vec Vocabulary is invalid");
+  if (!Vocabulary.isValid())
+    return PreservedAnalyses::all();
 
   for (Function &F : M) {
     auto Emb = Embedder::create(IR2VecEmbeddingKind, F, Vocabulary);
diff --git a/llvm/lib/Transforms/IPO/Inliner.cpp b/llvm/lib/Transforms/IPO/Inliner.cpp
index 7985e74cc5389..ed3fb61a1dfa9 100644
--- a/llvm/lib/Transforms/IPO/Inliner.cpp
+++ b/llvm/lib/Transforms/IPO/Inliner.cpp
@@ -163,14 +163,18 @@ InlinerPass::getAdvisor(const ModuleAnalysisManagerCGSCCProxy::Result &MAM,
         InlineContext{LTOPhase, InlinePass::CGSCCInliner});
 
     if (!CGSCCInlineReplayFile.empty())
-      OwnedAdvisor = getReplayInlineAdvisor(
-          M, FAM, M.getContext(), std::move(OwnedAdvisor),
-          ReplayInlinerSettings{CGSCCInlineReplayFile,
-                                CGSCCInlineReplayScope,
-                                CGSCCInlineReplayFallback,
-                                {CGSCCInlineReplayFormat}},
-          /*EmitRemarks=*/true,
-          InlineContext{LTOPhase, InlinePass::ReplayCGSCCInliner});
+      if (auto ReplayAdvisor = getReplayInlineAdvisor(
+              M, FAM, M.getContext(),
+              std::make_unique<DefaultInlineAdvisor>(
+                  M, FAM, getInlineParams(),
+                  InlineContext{LTOPhase, InlinePass::CGSCCInliner}),
+              ReplayInlinerSettings{CGSCCInlineReplayFile,
+                                    CGSCCInlineReplayScope,
+                                    CGSCCInlineReplayFallback,
+                                    {CGSCCInlineReplayFormat}},
+              /*EmitRemarks=*/true,
+              InlineContext{LTOPhase, InlinePass::ReplayCGSCCInliner}))
+        OwnedAdvisor = std::move(ReplayAdvisor);
 
     return *OwnedAdvisor;
   }
diff --git a/llvm/test/Transforms/AtomicExpand/AMDGPU/unaligned-atomic.ll b/llvm/test/Transforms/AtomicExpand/AMDGPU/unaligned-atomic.ll
index f8f81d0103f83..4ad087181968b 100644
--- a/llvm/test/Transforms/AtomicExpand/AMDGPU/unaligned-atomic.ll
+++ b/llvm/test/Transforms/AtomicExpand/AMDGPU/unaligned-atomic.ll
@@ -6,7 +6,7 @@ define i32 @atomic_load_global_align1(ptr addrspace(1) %ptr) {
   ret i32 %val
 }
 
-; FIXME: 2nd error not emitted in next function
+; CHECK: error: unsupported atomic store: instruction alignment 1 is smaller than the required 4-byte alignment for this atomic operation
 define void @atomic_store_global_align1(ptr addrspace(1) %ptr, i32 %val) {
   store atomic i32 %val, ptr addrspace(1) %ptr monotonic, align 1
   ret void
diff --git a/llvm/test/Transforms/SafeStack/NVPTX/safestack-libcall-error.ll b/llvm/test/Transforms/SafeStack/NVPTX/safestack-libcall-error.ll
index e8287639b969a..b8960f5c07898 100644
--- a/llvm/test/Transforms/SafeStack/NVPTX/safestack-libcall-error.ll
+++ b/llvm/test/Transforms/SafeStack/NVPTX/safestack-libcall-error.ll
@@ -1,9 +1,7 @@
 ; RUN: not opt -disable-output -mtriple=nvptx64-- -mcpu=sm_90 -passes='require<libcall-lowering-info>,safe-stack' %s 2>&1 | FileCheck --implicit-check-not=error %s
 
 ; CHECK: error: no location available for safestack pointer address
-
-; FIXME: If opt had more reasonable error handling behavior, this should emit 2 errors
-; xCHECK: error: no libcall available for stackprotector check fail
+; CHECK: error: no libcall available for stackprotector check fail
 define void @foo(i32 %t) #0 {
   %vla = alloca i32, i32 %t, align 4
   call void @baz(ptr %vla)
diff --git a/llvm/test/tools/opt/diagnostic-handler.ll b/llvm/test/tools/opt/diagnostic-handler.ll
new file mode 100644
index 0000000000000..127a6ab8710fc
--- /dev/null
+++ b/llvm/test/tools/opt/diagnostic-handler.ll
@@ -0,0 +1,28 @@
+; Verify that opt's diagnostic handler does not exit on the first error
+; reported via LLVMContext::emitError. opt should continue to run remaining
+; passes (so the print-module pass at the end of the pipeline still emits
+; the module to stdout), then return a non-zero exit status.
+;
+; Without the OptDiagnosticHandler, the first emitError would invoke
+; exit(1) from LLVMContext::diagnose() and the print-module pass would
+; never run, so stdout would be empty.
+
+; RUN: not opt -S -passes='module(sancov-module),print<module-debuginfo>' \
+; RUN:     -sanitizer-coverage-level=1 -sanitizer-coverage-stack-depth %s \
+; RUN:     >%t.out 2>%t.err
+; RUN: FileCheck --check-prefix=ERR --input-file=%t.err %s
+; RUN: FileCheck --check-prefix=OUT --input-file=%t.out %s
+
+; sancov-module emits an error when '__sancov_lowest_stack' has the wrong
+; type. The pass returns safely and the pipeline continues.
+ at __sancov_lowest_stack = thread_local global i32 0
+
+define i32 @f() {
+  ret i32 0
+}
+
+; ERR: error: '__sancov_lowest_stack' should not be declared by the user
+
+; The print-module pass after sancov-module still runs, proving opt did
+; not exit on the first emitError.
+; OUT: define i32 @f()
diff --git a/llvm/tools/opt/NewPMDriver.cpp b/llvm/tools/opt/NewPMDriver.cpp
index 2f57ae6d045e3..569426a7fd504 100644
--- a/llvm/tools/opt/NewPMDriver.cpp
+++ b/llvm/tools/opt/NewPMDriver.cpp
@@ -23,6 +23,8 @@
 #include "llvm/Bitcode/BitcodeWriterPass.h"
 #include "llvm/CodeGen/LibcallLoweringInfo.h"
 #include "llvm/Config/llvm-config.h"
+#include "llvm/IR/DiagnosticInfo.h"
+#include "llvm/IR/DiagnosticPrinter.h"
 #include "llvm/IR/Dominators.h"
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/IR/Module.h"
@@ -238,6 +240,22 @@ static cl::opt<bool> DisableLoopUnrolling(
     "disable-loop-unrolling",
     cl::desc("Disable loop unrolling in all relevant passes"), cl::init(false));
 
+bool OptDiagnosticHandler::handleDiagnostics(const DiagnosticInfo &DI) {
+  DiagnosticHandler::handleDiagnostics(DI);
+  // Mirror the filtering performed by LLVMContext::diagnose's default path:
+  // optimization remarks are gated by the -pass-remarks* flags (isEnabled),
+  // and verbose remarks additionally require hotness data.
+  if (auto *Remark = dyn_cast<DiagnosticInfoOptimizationBase>(&DI))
+    if (!Remark->isEnabled() || (Remark->isVerbose() && !Remark->getHotness()))
+      return true;
+
+  DiagnosticPrinterRawOStream DP(errs());
+  errs() << LLVMContext::getDiagnosticMessagePrefix(DI.getSeverity()) << ": ";
+  DI.print(DP);
+  errs() << "\n";
+  return true;
+}
+
 template <typename PassManagerT>
 bool tryParsePipelineText(PassBuilder &PB,
                           const cl::opt<std::string> &PipelineOpt) {
@@ -573,6 +591,12 @@ bool llvm::runPassPipeline(
   // Now that we have all of the passes ready, run them.
   MPM.run(M, MAM);
 
+  // If a pass reported an error via LLVMContext::emitError, fail without
+  // writing the output module.
+  if (auto *DH = M.getContext().getDiagHandlerPtr())
+    if (DH->HasErrors)
+      return false;
+
   // Declare success.
   if (OK != OK_NoOutput) {
     Out->keep();
diff --git a/llvm/tools/opt/NewPMDriver.h b/llvm/tools/opt/NewPMDriver.h
index 042d5d4bbfe47..0c60ace68c561 100644
--- a/llvm/tools/opt/NewPMDriver.h
+++ b/llvm/tools/opt/NewPMDriver.h
@@ -20,6 +20,7 @@
 #ifndef LLVM_TOOLS_OPT_NEWPMDRIVER_H
 #define LLVM_TOOLS_OPT_NEWPMDRIVER_H
 
+#include "llvm/IR/DiagnosticHandler.h"
 #include "llvm/Support/CommandLine.h"
 
 namespace llvm {
@@ -31,6 +32,15 @@ class TargetMachine;
 class ToolOutputFile;
 class TargetLibraryInfoImpl;
 
+/// A custom diagnostic handler for opt that consumes diagnostics so that the
+/// default LLVMContext::diagnose() does not call exit() on the first error.
+/// opt can then drain remaining diagnostics from a single pass run and report
+/// them all, mirroring llc's behavior so that opt remains a faithful test
+/// driver for passes that report errors via LLVMContext::emitError.
+struct OptDiagnosticHandler : public DiagnosticHandler {
+  bool handleDiagnostics(const DiagnosticInfo &DI) override;
+};
+
 extern cl::opt<bool> DebugifyEach;
 extern cl::opt<std::string> DebugifyExport;
 
diff --git a/llvm/tools/opt/optdriver.cpp b/llvm/tools/opt/optdriver.cpp
index 3e5b1f95b1de9..2e24c2e353ef1 100644
--- a/llvm/tools/opt/optdriver.cpp
+++ b/llvm/tools/opt/optdriver.cpp
@@ -538,6 +538,10 @@ optMain(int argc, char **argv,
   if (!DisableDITypeMap)
     Context.enableDebugTypeODRUniquing();
 
+  // Set a diagnostic handler that doesn't exit on the first error so that opt
+  // can drain remaining diagnostics from a single pass run, mirroring llc.
+  Context.setDiagnosticHandler(std::make_unique<OptDiagnosticHandler>());
+
   Expected<LLVMRemarkFileHandle> RemarksFileOrErr =
       setupLLVMOptimizationRemarks(Context, RemarksFilename, RemarksPasses,
                                    RemarksFormat, RemarksWithHotness,
@@ -974,6 +978,11 @@ optMain(int argc, char **argv,
   if (DebugifyEach && !DebugifyExport.empty())
     exportDebugifyStats(DebugifyExport, Passes.getDebugifyStatsMap());
 
+  // If a pass reported an error via LLVMContext::emitError, fail without
+  // writing the output module.
+  if (Context.getDiagHandlerPtr()->HasErrors)
+    return 1;
+
   // Declare success.
   if (!NoOutput)
     Out->keep();



More information about the llvm-commits mailing list