https://github.com/anutosh491 created 
https://github.com/llvm/llvm-project/pull/197133

Fixes #178139

This work is built on top of discussions done in 
https://github.com/llvm/llvm-project/pull/132670
Making a fresh PR cause the branch is a a year old and a bit outdated.

### Problem Statement : This is a 3 part problem statement. Imagine our 
compiler builder args look like this 
```
  CB.SetCompilerArgs({
      "-v",
      "-xc++",
      "-std=c++23",
      "-Xclang", "-iwithsysroot/include/compat",
      "-fwasm-exceptions",
      "-mllvm", "-wasm-enable-sjlj"
  });
```

**Problem 1** : clang-repl won't respect mllvm args just yet But the 
interpreter should be responsible for this. Hence

i) We move relevant code into a function called `parseLLVMArgs` in 
`CompilerInstance.cpp`
ii) And we need to add `CI->parseLLVMArgs();` while interpreter creation in 
`Interpreter.cpp`

**Result** : We are now at a stage where clang-repl would repsect the mllvm 
args passed from the top

**Problem 2** : clang-repl would respect the mllvm but only for the first 
`ParseAndExecute` call. So the block parsing the `Runtimes` is formed 
correctly. Any cell after that would fail with 
```
LLVM ERROR: -exception-model=wasm only allowed with at least one of 
-wasm-enable-eh or -wasm-enable-sjlj
Aborted()
```

And this is because of this line here.
https://github.com/llvm/llvm-project/blob/3bab75245a2bbbec36206f7e0c569c17a1a487e6/lld/wasm/Driver.cpp#L1315

We call `lldMain` in `Wasm.cpp` which leads us to `linkerMain` in 
`lld/wasm/Driver.cpp`. 

Now I thought this is a case where wasm-ld is not respecting "incremental" 
compilation and resetting options set by the clang globally. But as Sam's 
comment conveys 
[here](https://github.com/llvm/llvm-project/issues/178139#issuecomment-3806525095)
 I think wasm-ld/lld's design **is not wrong**.

My understanding goes like this

1. lld's reset is correct within lld's model. lld was never designed to coexist 
with a live incremental compiler in the same process. Its cleanup code exists 
for test isolation and is doing exactly what it was designed to do. Asking lld 
to skip the reset would break test isolation

2. lld cannot know which options it "owns" vs. which were set by the caller. 
LLVM's `cl::` system is a global flat namespace. There is no ownership or 
scoping concept. When lld calls `ResetAllOptionOccurrences()`, it resets 
*everything* — it literally cannot distinguish.

3. The options being reset are conceptually the CALLER's responsibility. 
`-wasm-enable-eh` and `-wasm-enable-sjlj` are codegen flags set by the frontend 
(via `-fwasm-exceptions` / `-mllvm`). The fact that they happen to live in the 
same global cl registry that lld resets is an artifact of LLVM's global option 
design, not a meaningful coupling between lld and clang.

The Right Fix: **Restore in WasmIncrementalExecutor::addModule()**

**Result** : clang-repl would respect mllvm across all cells.

<img width="703" height="454" alt="image" 
src="https://github.com/user-attachments/assets/862d5459-6e78-41e3-bc03-1bbd8c081edd";
 />

**Problem 3** : Once we've solved the above 2 problems. I added a test in 
InterpreterTest.cpp but that woudl fail with 
```
[----------] 1 test from InterpreterTest
[ RUN      ] InterpreterTest.EmscriptenExceptionHandling
[DBG CreateCpp] wasm-enable-eh registered: NO
[DBG CreateCpp] wasm-enable-sjlj registered: NO
clang version 23.0.0git ([email protected]:anutosh491/llvm-project.git 
d1b0c854354cb9bc0033b126fa1f71b2a6c721b2)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir:
 "" -cc1 -triple wasm32-unknown-emscripten -emit-obj -disable-free 
-clear-ast-before-backend -disable-llvm-verifier -discard-value-names 
-main-file-name "<<< inputs >>>" -mrelocation-model static -mframe-pointer=none 
-ffp-contract=on -fno-rounding-math -mconstructor-aliases -target-feature 
+exception-handling -target-feature +multivalue -target-feature 
+reference-types -exception-model=wasm -mllvm -wasm-enable-eh -target-cpu 
generic -debugger-tuning=gdb -fdebug-compilation-dir=/ -v 
-fcoverage-compilation-dir=/ -resource-dir lib/clang/23 -internal-isystem 
lib/clang/23/include -internal-isystem /include/wasm32-emscripten 
-internal-isystem /include -std=c++23 -fdeprecated-macro -ferror-limit 19 
-fmessage-length=80 -fvisibility=default -fgnuc-version=4.2.1 
-fno-implicit-modules -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions 
-exception-model=wasm -emit-llvm-only -fincremental-extensions -mllvm 
-wasm-enable-sjlj -o "<<< inputs >>>.o" -x c++ "<<< inputs >>>"
[DBG parseLLVMArgs] wasm-enable-eh registered: NO
[DBG parseLLVMArgs] wasm-enable-sjlj registered: NO
[DBG parseLLVMArgs] args to parse: -wasm-enable-eh -wasm-enable-sjlj
clang (LLVM option parsing): Unknown command line argument '-wasm-enable-eh'.  
Try: 'clang (LLVM option parsing) --help'
clang (LLVM option parsing): Did you mean '--rng-seed'?
clang (LLVM option parsing): Unknown command line argument '-wasm-enable-sjlj'. 
 Try: 'clang (LLVM option parsing) --help'
clang (LLVM option parsing): Did you mean '--sort-timers'?
```

**Root cause** : llvm_shutdown() Destroys the Option Registry

`InterpreterTestBase::TearDownTestSuite()` (in `InterpreterTestFixture.h`) 
calls `llvm::llvm_shutdown()` after each test suite completes. This function 
destroys all `ManagedStatic<>` objects, including 
`ManagedStatic<CommandLineParser>` — the singleton that holds the entire 
`cl::opt` registry.

When the next test suite starts and accesses the `CommandLineParser` again, a 
fresh empty instance is created. The `cl::opt` global objects 
(like`WasmEnableEH`) still exist in memory — their constructors ran at program
startup — but they have `FullyInitialized = true` and will **not** call 
`addArgument()` again. So they remain permanently unregistered in the new 
parser.

Fix : Guard llvm_shutdown() on Emscripten. Have added a comment in 
`TearDownTestSuite()` for the same.

Now we have 
```
[ RUN      ] InterpreterTest.EmscriptenExceptionHandling
clang version 23.0.0git ([email protected]:anutosh491/llvm-project.git 
d1b0c854354cb9bc0033b126fa1f71b2a6c721b2)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: 
 "" -cc1 -triple wasm32-unknown-emscripten -emit-obj -disable-free 
-clear-ast-before-backend -disable-llvm-verifier -discard-value-names 
-main-file-name "<<< inputs >>>" -mrelocation-model static -mframe-pointer=none 
-ffp-contract=on -fno-rounding-math -mconstructor-aliases -target-feature 
+exception-handling -target-feature +multivalue -target-feature 
+reference-types -exception-model=wasm -mllvm -wasm-enable-eh -target-cpu 
generic -debugger-tuning=gdb -fdebug-compilation-dir=/ -v 
-fcoverage-compilation-dir=/ -resource-dir lib/clang/23 -internal-isystem 
lib/clang/23/include -internal-isystem /include/wasm32-emscripten 
-internal-isystem /include -std=c++23 -fdeprecated-macro -ferror-limit 19 
-fmessage-length=80 -fvisibility=default -fgnuc-version=4.2.1 
-fno-implicit-modules -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions 
-exception-model=wasm -emit-llvm-only -fincremental-extensions -mllvm 
-wasm-enable-sjlj -o "<<< inputs >>>.o" -x c++ "<<< inputs >>>"
clang -cc1 version 23.0.0git based upon LLVM 23.0.0git default target 
wasm32-unknown-emscripten
ignoring nonexistent directory "lib/clang/23/include"
ignoring nonexistent directory "/include/wasm32-emscripten"
ignoring nonexistent directory "/include"
#include "..." search starts here:
End of search list.
[       OK ] InterpreterTest.EmscriptenExceptionHandling (37 ms)
[----------] 10 tests from InterpreterTest (674 ms total)
```









>From 707120c326feb3ae99be02863e21cf7efeef58b9 Mon Sep 17 00:00:00 2001
From: anutosh491 <[email protected]>
Date: Tue, 12 May 2026 14:22:33 +0530
Subject: [PATCH] Handle -mllvm args for clang-repl before calling
 executeAction

---
 clang/include/clang/Frontend/CompilerInstance.h |  5 +++++
 .../clang/Interpreter/IncrementalExecutor.h     |  3 +++
 clang/lib/Frontend/CompilerInstance.cpp         | 14 ++++++++++++++
 .../FrontendTool/ExecuteCompilerInvocation.cpp  | 16 +---------------
 clang/lib/Interpreter/IncrementalExecutor.cpp   |  2 +-
 clang/lib/Interpreter/Interpreter.cpp           |  7 +++++++
 clang/lib/Interpreter/Wasm.cpp                  | 17 ++++++++++++++++-
 clang/lib/Interpreter/Wasm.h                    |  5 ++++-
 clang/unittests/Interpreter/CMakeLists.txt      |  1 +
 clang/unittests/Interpreter/InterpreterTest.cpp | 16 ++++++++++++++++
 .../Interpreter/InterpreterTestFixture.h        | 12 +++++++++++-
 11 files changed, 79 insertions(+), 19 deletions(-)

diff --git a/clang/include/clang/Frontend/CompilerInstance.h 
b/clang/include/clang/Frontend/CompilerInstance.h
index bce9ff421ec87..6502c22e9dd83 100644
--- a/clang/include/clang/Frontend/CompilerInstance.h
+++ b/clang/include/clang/Frontend/CompilerInstance.h
@@ -259,6 +259,11 @@ class CompilerInstance : public ModuleLoader {
   /// Load the list of plugins requested in the \c FrontendOptions.
   void LoadRequestedPlugins();
 
+  /// Parse and apply LLVM command line arguments from FrontendOptions.
+  /// This processes the LLVMArgs option that comes from -mllvm flags.
+  /// This should be called after plugins are loaded and before ExecuteAction.
+  void parseLLVMArgs();
+
   /// @}
   /// @name Compiler Invocation and Options
   /// @{
diff --git a/clang/include/clang/Interpreter/IncrementalExecutor.h 
b/clang/include/clang/Interpreter/IncrementalExecutor.h
index 0e879efa0b55f..a293978b2917b 100644
--- a/clang/include/clang/Interpreter/IncrementalExecutor.h
+++ b/clang/include/clang/Interpreter/IncrementalExecutor.h
@@ -52,6 +52,9 @@ class IncrementalExecutorBuilder {
   std::optional<llvm::CodeModel::Model> CM = std::nullopt;
   /// An optional external IncrementalExecutor
   std::unique_ptr<IncrementalExecutor> IE;
+  /// mllvm args from the frontend; on wasm these are re-applied after each
+  /// lldMain call because lld resets all cl options for test-isolation 
purposes.
+  std::vector<std::string> LLVMArgs;
   /// An optional external orc jit builder
   std::unique_ptr<llvm::orc::LLJITBuilder> JITBuilder;
   /// A default callback that can be used in the IncrementalCompilerBuilder to
diff --git a/clang/lib/Frontend/CompilerInstance.cpp 
b/clang/lib/Frontend/CompilerInstance.cpp
index 91eda7392784f..52357fd86a3b4 100644
--- a/clang/lib/Frontend/CompilerInstance.cpp
+++ b/clang/lib/Frontend/CompilerInstance.cpp
@@ -1108,6 +1108,20 @@ void CompilerInstance::LoadRequestedPlugins() {
   }
 }
 
+void CompilerInstance::parseLLVMArgs() {
+  if (!getFrontendOpts().LLVMArgs.empty()) {
+    unsigned NumArgs = getFrontendOpts().LLVMArgs.size();
+    auto Args = std::make_unique<const char *[]>(NumArgs + 2);
+    Args[0] = "clang (LLVM option parsing)";
+    for (unsigned i = 0; i != NumArgs; ++i)
+      Args[i + 1] = getFrontendOpts().LLVMArgs[i].c_str();
+    Args[NumArgs + 1] = nullptr;
+    llvm::cl::ParseCommandLineOptions(NumArgs + 1, Args.get(), /*Overview=*/"",
+                                      /*Errs=*/nullptr,
+                                      /*VFS=*/&getVirtualFileSystem());
+  }
+}
+
 /// Determine the appropriate source input kind based on language
 /// options.
 static Language getLanguageFromOptions(const LangOptions &LangOpts) {
diff --git a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp 
b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
index e4622496758ac..7ecf356f15641 100644
--- a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
+++ b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
@@ -239,21 +239,7 @@ bool ExecuteCompilerInvocation(CompilerInstance *Clang) {
 
   Clang->LoadRequestedPlugins();
 
-  // Honor -mllvm.
-  //
-  // FIXME: Remove this, one day.
-  // This should happen AFTER plugins have been loaded!
-  if (!Clang->getFrontendOpts().LLVMArgs.empty()) {
-    unsigned NumArgs = Clang->getFrontendOpts().LLVMArgs.size();
-    auto Args = std::make_unique<const char*[]>(NumArgs + 2);
-    Args[0] = "clang (LLVM option parsing)";
-    for (unsigned i = 0; i != NumArgs; ++i)
-      Args[i + 1] = Clang->getFrontendOpts().LLVMArgs[i].c_str();
-    Args[NumArgs + 1] = nullptr;
-    llvm::cl::ParseCommandLineOptions(NumArgs + 1, Args.get(), /*Overview=*/"",
-                                      /*Errs=*/nullptr,
-                                      /*VFS=*/&Clang->getVirtualFileSystem());
-  }
+  Clang->parseLLVMArgs();
 
 #if CLANG_ENABLE_STATIC_ANALYZER
   // These should happen AFTER plugins have been loaded!
diff --git a/clang/lib/Interpreter/IncrementalExecutor.cpp 
b/clang/lib/Interpreter/IncrementalExecutor.cpp
index 65cb29a2f441a..51a601433e552 100644
--- a/clang/lib/Interpreter/IncrementalExecutor.cpp
+++ b/clang/lib/Interpreter/IncrementalExecutor.cpp
@@ -393,7 +393,7 @@ 
IncrementalExecutorBuilder::create(llvm::orc::ThreadSafeContext &TSC,
   llvm::Error Err = llvm::Error::success();
   std::unique_ptr<IncrementalExecutor> Executor;
 #ifdef __EMSCRIPTEN__
-  Executor = std::make_unique<WasmIncrementalExecutor>(Err);
+  Executor = std::make_unique<WasmIncrementalExecutor>(Err, LLVMArgs);
 #else
   Executor = std::make_unique<OrcIncrementalExecutor>(TSC, *JITBuilder, Err);
 #endif
diff --git a/clang/lib/Interpreter/Interpreter.cpp 
b/clang/lib/Interpreter/Interpreter.cpp
index 2684a00ce5f07..5d02dceb472a2 100644
--- a/clang/lib/Interpreter/Interpreter.cpp
+++ b/clang/lib/Interpreter/Interpreter.cpp
@@ -257,6 +257,9 @@ Interpreter::Interpreter(std::unique_ptr<CompilerInstance> 
Instance,
   auto LLVMCtx = std::make_unique<llvm::LLVMContext>();
   TSCtx = std::make_unique<llvm::orc::ThreadSafeContext>(std::move(LLVMCtx));
 
+  // Honor -mllvm options
+  CI->parseLLVMArgs();
+
   Act = TSCtx->withContextDo([&](llvm::LLVMContext *Ctx) {
     return std::make_unique<IncrementalAction>(*CI, *Ctx, ErrOut, *this,
                                                std::move(Consumer));
@@ -499,6 +502,10 @@ llvm::Error Interpreter::CreateExecutor() {
   if (!IncrExecutorBuilder)
     IncrExecutorBuilder = std::make_unique<IncrementalExecutorBuilder>();
 
+  // Propagate mllvm args so the wasm executor can restore them after each
+  // lldMain invocation (which resets all cl options for test isolation).
+  IncrExecutorBuilder->LLVMArgs = CI->getFrontendOpts().LLVMArgs;
+
   auto ExecutorOrErr = IncrExecutorBuilder->create(*TSCtx, CI->getTarget());
   if (ExecutorOrErr)
     IncrExecutor = std::move(*ExecutorOrErr);
diff --git a/clang/lib/Interpreter/Wasm.cpp b/clang/lib/Interpreter/Wasm.cpp
index 96600cf9fa6d0..9ebd23da5f564 100644
--- a/clang/lib/Interpreter/Wasm.cpp
+++ b/clang/lib/Interpreter/Wasm.cpp
@@ -12,6 +12,7 @@
 
 #include "Wasm.h"
 
+#include "llvm/Support/CommandLine.h"
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/Path.h"
 #include <llvm/IR/LegacyPassManager.h>
@@ -59,7 +60,9 @@ bool link(llvm::ArrayRef<const char *> args, 
llvm::raw_ostream &stdoutOS,
 
 namespace clang {
 
-WasmIncrementalExecutor::WasmIncrementalExecutor(llvm::Error &Err) {
+WasmIncrementalExecutor::WasmIncrementalExecutor(llvm::Error &Err,
+                                                 std::vector<std::string> 
LLVMArgs)
+    : StoredLLVMArgs(std::move(LLVMArgs)) {
   llvm::ErrorAsOutParameter EAO(&Err);
 
   if (Err)
@@ -132,6 +135,18 @@ llvm::Error 
WasmIncrementalExecutor::addModule(PartialTranslationUnit &PTU) {
   lld::Result Result =
       lld::lldMain(LinkerArgs, llvm::outs(), llvm::errs(), WasmDriverArgs);
 
+  // lld::wasm::linkerMain calls cl::ResetAllOptionOccurrences() which wipes
+  // all global LLVM cl options, including mllvm flags set by the frontend
+  // (e.g. -wasm-enable-eh, -wasm-enable-sjlj). Re-apply them so the next
+  // Parse() call's WebAssemblyTargetMachine creation finds the correct state.
+  if (!StoredLLVMArgs.empty()) {
+    std::vector<const char *> ArgPtrs;
+    ArgPtrs.push_back("clang-repl (restoring LLVM options)");
+    for (const std::string &Arg : StoredLLVMArgs)
+      ArgPtrs.push_back(Arg.c_str());
+    llvm::cl::ParseCommandLineOptions(ArgPtrs.size(), ArgPtrs.data());
+  }
+
   if (Result.retCode)
     return llvm::make_error<llvm::StringError>(
         "Failed to link incremental module", llvm::inconvertibleErrorCode());
diff --git a/clang/lib/Interpreter/Wasm.h b/clang/lib/Interpreter/Wasm.h
index bf8777a41eac2..6340393dc6b9b 100644
--- a/clang/lib/Interpreter/Wasm.h
+++ b/clang/lib/Interpreter/Wasm.h
@@ -19,12 +19,14 @@
 
 #include "clang/Interpreter/IncrementalExecutor.h"
 #include "llvm/ADT/SmallString.h"
+#include <string>
+#include <vector>
 
 namespace clang {
 
 class WasmIncrementalExecutor : public IncrementalExecutor {
 public:
-  WasmIncrementalExecutor(llvm::Error &Err);
+  WasmIncrementalExecutor(llvm::Error &Err, std::vector<std::string> LLVMArgs);
   ~WasmIncrementalExecutor() override;
 
   llvm::Error addModule(PartialTranslationUnit &PTU) override;
@@ -38,6 +40,7 @@ class WasmIncrementalExecutor : public IncrementalExecutor {
 
 private:
   llvm::SmallString<256> TempDir;
+  std::vector<std::string> StoredLLVMArgs;
 };
 
 } // namespace clang
diff --git a/clang/unittests/Interpreter/CMakeLists.txt 
b/clang/unittests/Interpreter/CMakeLists.txt
index 66b396b53cb55..9fdd69ea49069 100644
--- a/clang/unittests/Interpreter/CMakeLists.txt
+++ b/clang/unittests/Interpreter/CMakeLists.txt
@@ -76,6 +76,7 @@ get_target_property(LINKED_LIBS ClangReplInterpreterTests 
LINK_LIBRARIES)
 list(REMOVE_ITEM LINKED_LIBS LLVMSupport)
 set_target_properties(ClangReplInterpreterTests PROPERTIES LINK_LIBRARIES 
"${LINKED_LIBS}")
 target_link_options(ClangReplInterpreterTests
+  PUBLIC "SHELL: -fwasm-exceptions"
   PUBLIC "SHELL: -s MAIN_MODULE=1"
   PUBLIC "SHELL: -s ALLOW_MEMORY_GROWTH=1"
   PUBLIC "SHELL: -s STACK_SIZE=32mb"
diff --git a/clang/unittests/Interpreter/InterpreterTest.cpp 
b/clang/unittests/Interpreter/InterpreterTest.cpp
index 9ff9092524d21..32b6e00e05a67 100644
--- a/clang/unittests/Interpreter/InterpreterTest.cpp
+++ b/clang/unittests/Interpreter/InterpreterTest.cpp
@@ -443,4 +443,20 @@ TEST_F(InterpreterTest, TranslationUnit_CanonicalDecl) {
             sema.getASTContext().getTranslationUnitDecl()->getCanonicalDecl());
 }
 
+TEST_F(InterpreterTest, EmscriptenExceptionHandling) {
+#ifndef __EMSCRIPTEN__
+  GTEST_SKIP() << "This test only applies to Emscripten builds.";
+#endif
+
+  using Args = std::vector<const char *>;
+  Args ExtraArgs = {"-std=c++23", "-v",
+                    "-fwasm-exceptions",
+                    "-mllvm", "-wasm-enable-sjlj"};
+
+  std::unique_ptr<Interpreter> Interp = createInterpreter(ExtraArgs);
+
+  llvm::cantFail(
+      Interp->ParseAndExecute("try { throw 1; } catch (...) { 0; }"));
+}
+
 } // end anonymous namespace
diff --git a/clang/unittests/Interpreter/InterpreterTestFixture.h 
b/clang/unittests/Interpreter/InterpreterTestFixture.h
index b088fa4a5f896..c4a30dd06220c 100644
--- a/clang/unittests/Interpreter/InterpreterTestFixture.h
+++ b/clang/unittests/Interpreter/InterpreterTestFixture.h
@@ -55,7 +55,17 @@ class InterpreterTestBase : public ::testing::Test {
     llvm::InitializeNativeTargetAsmPrinter();
   }
 
-  static void TearDownTestSuite() { llvm::llvm_shutdown(); }
+  static void TearDownTestSuite() {
+    // llvm_shutdown() cleans up JIT (LLJIT/ORC) state between test suites.
+    // On Emscripten, WasmIncrementalExecutor is used instead of LLJIT, so
+    // there is no JIT state to tear down. Calling llvm_shutdown() here would
+    // destroy the ManagedStatic<CommandLineParser> global, permanently
+    // deregistering all cl::opt options (wasm-enable-eh, etc.) since their
+    // constructors already ran and will not fire again after the reset.
+#ifndef __EMSCRIPTEN__
+    llvm::llvm_shutdown();
+#endif
+  }
 };
 
 } // namespace clang

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to