Author: Anutosh Bhat Date: 2026-10-08T04:24:58Z New Revision: 415b70a8a11d369176ea2db5151424d9354ba41a
URL: https://github.com/llvm/llvm-project/commit/415b70a8a11d369176ea2db5151424d9354ba41a DIFF: https://github.com/llvm/llvm-project/commit/415b70a8a11d369176ea2db5151424d9354ba41a.diff LOG: [clang-repl] Handle mllvm args for clang-repl before calling executeAction (#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) ``` Added: Modified: clang/include/clang/Frontend/CompilerInstance.h clang/include/clang/Interpreter/IncrementalExecutor.h clang/lib/Frontend/CompilerInstance.cpp clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp clang/lib/Interpreter/IncrementalExecutor.cpp clang/lib/Interpreter/Interpreter.cpp clang/lib/Interpreter/Wasm.cpp clang/lib/Interpreter/Wasm.h clang/unittests/Interpreter/CMakeLists.txt clang/unittests/Interpreter/InterpreterTest.cpp clang/unittests/Interpreter/InterpreterTestFixture.h Removed: ################################################################################ diff --git a/clang/include/clang/Frontend/CompilerInstance.h b/clang/include/clang/Frontend/CompilerInstance.h index 977180a8dc3aa..b87dd037480d6 100644 --- a/clang/include/clang/Frontend/CompilerInstance.h +++ b/clang/include/clang/Frontend/CompilerInstance.h @@ -264,6 +264,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 913da9230a947..f1a68c0288d00 100644 --- a/clang/include/clang/Interpreter/IncrementalExecutor.h +++ b/clang/include/clang/Interpreter/IncrementalExecutor.h @@ -52,6 +52,10 @@ 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 7048bec2cdceb..b09c9a9f5a5be 100644 --- a/clang/lib/Frontend/CompilerInstance.cpp +++ b/clang/lib/Frontend/CompilerInstance.cpp @@ -1135,6 +1135,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 e872f0823f23e..24e699b42ca7f 100644 --- a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp +++ b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp @@ -253,21 +253,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 5d01ba4774269..101120626f766 100644 --- a/clang/lib/Interpreter/IncrementalExecutor.cpp +++ b/clang/lib/Interpreter/IncrementalExecutor.cpp @@ -429,7 +429,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 40fbc83db9107..85cefa1270049 100644 --- a/clang/lib/Interpreter/Interpreter.cpp +++ b/clang/lib/Interpreter/Interpreter.cpp @@ -352,6 +352,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)); @@ -608,6 +611,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 573c8910d3a41..017b049fb470c 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> @@ -61,7 +62,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) @@ -142,6 +145,22 @@ llvm::Error WasmIncrementalExecutor::addModule(PartialTranslationUnit &PTU) { if (!Result.canRunAgain) lld::exitLld(Result.retCode); + // 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. + // + // FIXME: Remove this once library command-line options no longer rely on + // process-global cl::opt state. See: + // https://discourse.llvm.org/t/rfc-declare-library-command-line-options-in-tablegen-one-struct-per-library/91877 + 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 6d999104054c3..d18d7819001b1 100644 --- a/clang/unittests/Interpreter/CMakeLists.txt +++ b/clang/unittests/Interpreter/CMakeLists.txt @@ -77,6 +77,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 a0ed7e671ddbe..17cc6fc9b6ca8 100644 --- a/clang/unittests/Interpreter/InterpreterTest.cpp +++ b/clang/unittests/Interpreter/InterpreterTest.cpp @@ -174,6 +174,9 @@ TEST_F(InterpreterTest, TranslationUnitRedeclChainAcrossManyPTUs) { } TEST_F(InterpreterTest, UndoLeavesDeclsInTranslationUnitChain) { +#ifdef __EMSCRIPTEN__ + GTEST_SKIP() << "Undo is not supported for Emscripten builds"; +#endif std::unique_ptr<Interpreter> Interp = createInterpreter(); cantFail(Interp->Parse("struct Kept {};")); @@ -520,6 +523,11 @@ TEST_F(InterpreterTest, ValueSetRawBitsCopiesByteCount) { // Earlier the move ctor called Release() on the just-moved-into storage, // double-releasing on the next read. TEST_F(InterpreterTest, ValueMoveSemantics) { + // FIXME: Emscripten cannot resolve MoveT's destructor symbol + // `_ZN5MoveTD2Ev` from the incrementally loaded Wasm side module. +#ifdef __EMSCRIPTEN__ + GTEST_SKIP() << "Unresolved destructor symbol: _ZN5MoveTD2Ev"; +#endif std::vector<const char *> Args = {"-fno-sized-deallocation"}; std::unique_ptr<Interpreter> Interp = createInterpreter(Args); @@ -588,4 +596,19 @@ 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
