From 646f2125ec5277164993d391f075bba7b601270e Mon Sep 17 00:00:00 2001 From: Emery Conrad Date: Thu, 5 Nov 2026 12:05:51 -0600 Subject: [PATCH] Roll back failed and undone inputs, and add a checkpoint for Undo A failed Declare, Process or Evaluate leaves the interpreter as it was before the input: - An input that parses but fails to link or to run is withdrawn. - Before Execute, a module with static initializers is checked for missing symbols. LLJIT keeps the init function of a module that fails to link, and every later input then fails. - The implicit instantiations that a failed input requested are reset. - Macros and include state are restored. GetInputCount gives a checkpoint for Undo. Undo also cleans the TU scope, reopened namespaces, replaced redeclarations and the wrapper caches. docs/UndoAndFailedInputs.rst lists what is not rolled back, and the upstream clang-repl work that can replace these workarounds. Co-developed-with-the-help-of: Claude Code (Opus 5.5, human in the loop) --- BUILD.bazel | 1 + docs/ReleaseNotes.md | 8 +- docs/UndoAndFailedInputs.rst | 158 +++++++++ docs/index.rst | 1 + lib/CppInterOp/CMakeLists.txt | 1 + lib/CppInterOp/CppInterOp.cpp | 277 ++++++++++++++- lib/CppInterOp/CppInterOp.td | 6 + lib/CppInterOp/CppInterOpInterpreter.h | 87 ++++- lib/CppInterOp/InputRollback.cpp | 217 ++++++++++++ lib/CppInterOp/InputRollback.h | 64 ++++ lib/CppInterOp/InterpreterInfo.h | 3 + unittests/CppInterOp/InterpreterTest.cpp | 317 ++++++++++++++++++ .../CppInterOp/VariableReflectionTest.cpp | 16 +- 13 files changed, 1131 insertions(+), 25 deletions(-) create mode 100644 docs/UndoAndFailedInputs.rst create mode 100644 lib/CppInterOp/InputRollback.cpp create mode 100644 lib/CppInterOp/InputRollback.h diff --git a/BUILD.bazel b/BUILD.bazel index 3f0615460..e6eb9c944 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -191,6 +191,7 @@ cc_library( "lib/CppInterOp/DynamicLibraryManager.cpp", "lib/CppInterOp/DynamicLibraryManagerSymbol.cpp", "lib/CppInterOp/ErrorInternal.cpp", + "lib/CppInterOp/InputRollback.cpp", "lib/CppInterOp/Paths.cpp", "lib/CppInterOp/Tracing.cpp", ] + glob(["lib/CppInterOp/*.h"]), diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 83d31884d..1186bc857 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -36,7 +36,13 @@ infrastructure are described first. ## Incremental C++ -- +- A failed `Declare`, `Process` or `Evaluate` withdraws its inputs, also when + it fails to link or to run. A module that cannot link no longer breaks the + inputs after it. +- New `GetInputCount` gives a checkpoint for `Undo`. +- A failed input or an undo also rolls back implicit instantiations, macros, + include state, namespace members and replaced redeclarations. See + [Undo and failed inputs](UndoAndFailedInputs.rst). ## Misc diff --git a/docs/UndoAndFailedInputs.rst b/docs/UndoAndFailedInputs.rst new file mode 100644 index 000000000..5e34b2163 --- /dev/null +++ b/docs/UndoAndFailedInputs.rst @@ -0,0 +1,158 @@ +Undo and failed inputs +====================== + +An interactive session sends one input at a time. When an input fails, or when +the user withdraws an input with ``Cpp::Undo``, the interpreter must return to +the state it had before that input. Otherwise a single mistake can make every +later input fail, and the user must restart the session. + +clang-repl removes the declarations of a withdrawn input. It does not yet roll +back all the state that the input changed outside its own declarations. The +upstream work on this is in progress (see `Upstream work`_). Until it lands, +CppInterOp does the part that it can do through the clang API. The code is in +``lib/CppInterOp/InputRollback.cpp`` and ``WithdrawInputs`` in +``lib/CppInterOp/CppInterOp.cpp``. Remove each workaround when the matching +upstream change is in all supported LLVM versions. + +API +--- + +``Cpp::GetInputCount()`` + Returns the number of inputs that the interpreter holds. Record it as a + checkpoint. + +``Cpp::Undo(N)`` + Withdraws the last ``N`` inputs. To return to a checkpoint, pass the + difference between the current count and the checkpoint. + +A failed ``Cpp::Declare``, ``Cpp::Process`` or ``Cpp::Evaluate`` withdraws its +own inputs. The caller does not call ``Cpp::Undo`` for it. + +A language binding usually adds inputs of its own, for example JIT call +wrappers. Count from a checkpoint, not from the binding's own calls. Do not +call ``Cpp::Undo`` through a JIT call wrapper: the wrapper can be one of the +inputs that it withdraws. + +What CppInterOp rolls back +-------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 35 35 + + * - State + - When + - How + * - Inputs that fail after parse (link, run time) + - Failed ``Declare``, ``Process``, ``Evaluate`` + - ``Cpp::Undo`` of the inputs that the call added + * - Module that cannot link but has static initializers + - Before ``Execute`` + - Look up its external symbols first and fail cleanly. LLJIT keeps the + init function of a module that fails to link queued, and every later + input then fails. + * - Implicit instantiations that a failed input requested + - Failed parse + - Record them through ``ASTConsumer`` callbacks; reset the body, the point + of instantiation and the pending queue + * - Macros (``#define``, ``#undef``) + - Failed parse, ``Undo`` + - Record them through ``PPCallbacks``; restore the previous definition + * - ``#pragma once`` and ``#import`` state, include guards + - Failed parse, ``Undo`` + - Clear the included-file state of files that the input entered first; + an include guard is a macro + * - Names in the TU scope + - ``Undo`` + - Remove them from ``TUScope`` and ``IdResolver`` + * - Members added to a namespace that an earlier input opened + - ``Undo`` + - Remove them from the namespace lookup + * - Enumerators of an unscoped enum + - ``Undo`` + - Remove them from the enclosing lookup. Since llvm#218149, clang + removes the enum but not its enumerators. + * - A declaration that a withdrawn redeclaration replaced + - ``Undo`` + - Put it back in the scope and the lookup; make it the latest of its + redeclaration chain when it is the first declaration + * - Cached JIT call wrappers + - ``Undo`` + - Drop the wrappers that live in a withdrawn input + +The instantiation, macro and include rollback needs clang-repl from LLVM 22 or +later. The link check is for in-process JIT only. + +What is not rolled back, and why +-------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 35 35 30 + + * - Case + - Why CppInterOp cannot do it + - Upstream work + * - CodeGen error in an input (for example a duplicate mangled name); + the session can crash + - The state is in ``CodeGenModule``, which is private to clang CodeGen + - llvm#226285, #234493, #204152, #223305 + * - Function that fails to link on its first call, then the missing + symbol is defined + - ORC keeps the error state of the failed lookup, also after the module + is removed. Undo the input and define the function again. + - None yet (ORC) + * - Class definition after a forward declaration in an earlier input, then + undo of the definition + - The redeclarations of a class share private definition data + - Fixed by llvm#218149 in LLVM 23.1.3 + * - A redeclaration chain with more than one kept declaration + - The public API can only cut a chain back to its first declaration + - Fixed by llvm#218149 in LLVM 23.1.3 + * - A header that changed on disk after undo + - The file manager caches the contents for the session + - llvm#182750 + * - Run-time side effects (memory, files, output) + - Not interpreter state + - -- + * - Objects whose class was withdrawn + - Their code is withdrawn; calls fail + - -- + * - cling + - cling has its own transactions; ``GetInputCount`` returns 0 + - -- + +Tests +----- + +``unittests/CppInterOp/InterpreterTest.cpp`` (``Interpreter_FailedLink*``, +``Interpreter_FailedParse*``, ``Interpreter_Undo*``). Bindings have more +end-to-end cases; for example cppjit's ``test/test_failed_input.py`` marks +each upstream gap as an expected failure. + +Upstream work +------------- + +Merged in clang-repl: + +- llvm#85378, #127467: error recovery in ``CleanUpPTU``. +- llvm#149396: error message on a failed undo. +- llvm#178648: ``CleanUpPTU`` and implicit C function declarations. +- llvm#213235 (issue #213230): unlink the withdrawn translation unit on undo. +- llvm#218129: do not remove ``extern "C"`` declarations twice. +- llvm#218149 (issue #201844): keep earlier declarations alive when an input + fails. + +Open: + +- llvm#223305: error recovery for clang-repl, general rollback of + declaration state across inputs. +- llvm#231363: mark and rewind for ``BumpPtrAllocator``, for #223305. +- llvm#234493: ``CodeGenModule::forgetDecl``. +- llvm#204152: module contamination after a failed template instantiation. +- llvm#226285 (issue #226252): drop the IR of a failed input. +- llvm#192588, #226253, #228330: roll back or redo the implicit instantiations + of a failed input (issue #146770). +- llvm#182750: undo of file inclusion and macro definitions. +- llvm#228370: undo the enumerators of an unscoped enumeration. +- llvm#75629: segfault in ``CleanUpPTU``. diff --git a/docs/index.rst b/docs/index.rst index b1c198e9c..2570e9924 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,5 +25,6 @@ C++ to simpler, more interactive languages like Python. reference tutorials DebuggingCppInterOp + UndoAndFailedInputs FAQ DevelopersDocumentation diff --git a/lib/CppInterOp/CMakeLists.txt b/lib/CppInterOp/CMakeLists.txt index f5e6d1879..6ca9f72a0 100644 --- a/lib/CppInterOp/CMakeLists.txt +++ b/lib/CppInterOp/CMakeLists.txt @@ -256,6 +256,7 @@ add_llvm_library(clangCppInterOp CXCppInterOp.cpp CXCppInterOpGenerated.cpp ErrorInternal.cpp + InputRollback.cpp Tracing.cpp ${DLM} LINK_LIBS diff --git a/lib/CppInterOp/CppInterOp.cpp b/lib/CppInterOp/CppInterOp.cpp index b9ece7f05..897bee349 100644 --- a/lib/CppInterOp/CppInterOp.cpp +++ b/lib/CppInterOp/CppInterOp.cpp @@ -16,6 +16,7 @@ #include "BakedArgs.h" #include "Compatibility.h" #include "ErrorInternal.h" +#include "InputRollback.h" #include "InterpreterInfo.h" #include "Sins.h" // for access to private members #include "Tracing.h" @@ -43,6 +44,7 @@ #include "clang/AST/DeclAccessPair.h" #include "clang/AST/DeclBase.h" #include "clang/AST/DeclCXX.h" +#include "clang/AST/DeclContextInternals.h" #include "clang/AST/DeclTemplate.h" #include "clang/AST/DeclarationName.h" #include "clang/AST/Expr.h" @@ -72,6 +74,7 @@ #include "clang/Basic/Version.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Interpreter/Interpreter.h" +#include "clang/Interpreter/PartialTranslationUnit.h" #include "clang/Sema/Lookup.h" #include "clang/Sema/Overload.h" #include "clang/Sema/Ownership.h" @@ -82,6 +85,7 @@ #include "llvm/ADT/APInt.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringExtras.h" @@ -120,6 +124,7 @@ #include #include #include +#include #include #include #include @@ -128,6 +133,7 @@ #include #include #include +#include #ifndef _WIN32 #include #endif @@ -3368,8 +3374,7 @@ struct VTableOverlay { VTableOverlayDtorHook cleanup = nullptr; void* cleanup_data = nullptr; - VTableOverlay(void** block, void** orig_vptr, void* inst, - std::size_t n_extra) + VTableOverlay(void** block, void** orig_vptr, void* inst, std::size_t n_extra) : block(block), original_vptr(orig_vptr), inst(inst), n_extra_prefix_slots(n_extra) { // Stash self-pointer in the hidden slot before publishing the vptr; @@ -3531,8 +3536,7 @@ MakeVTableOverlay(void* inst, ConstDeclRef base, const ConstFuncRef* methods, slots.push_back(slot); } auto* ov = applyVTableOverlay(inst, total_method_slots, slots.data(), - overlay_fns, n_overlays, - n_extra_prefix_slots); + overlay_fns, n_overlays, n_extra_prefix_slots); if (!ov) return INTEROP_RETURN(nullptr); @@ -4321,6 +4325,212 @@ inline void indent(std::ostringstream& buf, int indent_level) { buf << kIndentString; } +#ifndef CPPINTEROP_USE_CLING +ALLOW_ACCESS(clang::Interpreter, PTUs, + std::list); + +size_t InputCountOfImpl(clang::Interpreter& I) { + auto& PTUs = ACCESS(I, PTUs); + return PTUs.size(); +} + +ALLOW_ACCESS(clang::Interpreter, LastValue, clang::Value); + +void TakeLastValueImpl(clang::Interpreter& I, clang::Value& V) { + auto& Last = ACCESS(I, LastValue); + if (Last.isValid()) + V = std::move(Last); +} + +size_t InputCount(compat::Interpreter& I) { + clang::Interpreter& Inner = I; + return InputCountOfImpl(Inner); +} + +// clang's Undo leaves the names in the TU scope. +void CollectScopeNames(clang::Sema& S, clang::DeclContext* DC, + llvm::SmallVectorImpl& Out) { + for (clang::Decl* D : DC->decls()) { + if (auto* LS = llvm::dyn_cast(D)) + CollectScopeNames(S, LS, Out); + else if (auto* ND = llvm::dyn_cast(D)) + if (!ND->getDeclName().isEmpty() && S.TUScope->isDeclScope(ND)) + Out.push_back(ND); + } +} + +void CollectTopLevelNames(clang::DeclContext* DC, + llvm::SmallVectorImpl& Out) { + for (clang::Decl* D : DC->decls()) { + if (auto* LS = llvm::dyn_cast(D)) + CollectTopLevelNames(LS, Out); + else if (auto* ND = llvm::dyn_cast(D)) + if (!ND->getDeclName().isEmpty()) + Out.push_back(ND); + } +} + +// clang's Undo leaves what an input added to an earlier namespace. +void CollectReopenedMembers( + clang::DeclContext* DC, bool Reopened, + llvm::SmallVectorImpl>& Out) { + for (clang::Decl* D : DC->decls()) { + if (Reopened) + Out.push_back({DC, D}); + if (auto* NS = llvm::dyn_cast(D)) + CollectReopenedMembers(NS, Reopened || !NS->isFirstDecl(), Out); + else if (auto* LS = llvm::dyn_cast(D)) + CollectReopenedMembers(LS, Reopened, Out); + } +} + +// Cut the redeclaration chain back to \p P, or a withdrawn definition still +// counts. The public API allows this only for a first declaration. +void CutChainAfter(clang::Decl* P) { + if (P->getPreviousDecl()) + return; + if (auto* FD = llvm::dyn_cast(P)) { + if (!FD->getDescribedFunctionTemplate()) + FD->setPreviousDecl(nullptr); + } else if (auto* VD = llvm::dyn_cast(P)) { + if (!VD->getDescribedVarTemplate()) + VD->setPreviousDecl(nullptr); + } else if (auto* TND = llvm::dyn_cast(P)) { + TND->setPreviousDecl(nullptr); + } else if (auto* NS = llvm::dyn_cast(P)) { + NS->setPreviousDecl(nullptr); + } +} + +// Since llvm/llvm-project#218149 clang's Undo keeps the enumerators of an +// unscoped enum visible. Record the lookup they are in before the undo, which +// can give a withdrawn namespace a redeclaration chain of its own. +void CollectEnumerators( + clang::Decl* D, + llvm::SmallVectorImpl>& + Out) { + auto* ED = llvm::dyn_cast(D); + if (!ED || ED->isScoped()) + return; + clang::DeclContext* Primary = ED->getDeclContext()->getPrimaryContext(); + for (clang::EnumConstantDecl* E : ED->enumerators()) + Out.push_back({Primary, E}); +} + +void HideEnumerator(clang::Sema& S, clang::DeclContext* Primary, + clang::NamedDecl* E) { + if (S.TUScope && S.TUScope->isDeclScope(E)) { + S.TUScope->RemoveDecl(E); + S.IdResolver.RemoveDecl(E); + } + clang::StoredDeclsMap* Map = Primary->getLookupPtr(); + if (!Map) + return; + auto Pos = Map->find(E->getDeclName()); + if (Pos == Map->end()) + return; + Pos->second.remove(E); + if (Pos->second.isNull()) + Map->erase(Pos); +} + +// Undo the inputs after the first \p Keep. +bool WithdrawInputs(compat::Interpreter& I, size_t Keep) { + clang::Interpreter& Inner = I; + auto& PTUs = ACCESS(Inner, PTUs); + if (PTUs.size() <= Keep) + return true; + clang::Sema& S = I.getSema(); + llvm::SmallVector Names; + llvm::SmallVector TopLevel; + llvm::SmallVector, 8> Members; + for (auto It = std::next(PTUs.begin(), static_cast(Keep)); + It != PTUs.end(); ++It) { + if (S.getLangOpts().CPlusPlus && S.TUScope) + CollectScopeNames(S, It->TUPart, Names); + CollectTopLevelNames(It->TUPart, TopLevel); + CollectReopenedMembers(It->TUPart, /*Reopened=*/false, Members); + } + llvm::SmallVector, 8> + Enumerators; + for (clang::NamedDecl* ND : TopLevel) + CollectEnumerators(ND, Enumerators); + for (auto& [DC, D] : Members) + CollectEnumerators(D, Enumerators); + if (I.undo(static_cast(PTUs.size() - Keep)) != + compat::Interpreter::kSuccess) + return false; + for (clang::NamedDecl* ND : Names) { + S.TUScope->RemoveDecl(ND); + S.IdResolver.RemoveDecl(ND); + } + for (auto& [Primary, E] : Enumerators) + HideEnumerator(S, Primary, E); + // Innermost first. Since llvm/llvm-project#218149 clang's Undo does this. + for (auto& [DC, D] : llvm::reverse(Members)) + if (DC->containsDecl(D)) + DC->removeDecl(D); + // A redeclaration replaced the earlier declaration in the scope and the + // lookup; put it back. + llvm::SmallPtrSet Withdrawn(TopLevel.begin(), + TopLevel.end()); + for (auto& [DC, D] : Members) + Withdrawn.insert(D); + auto Previous = [&](clang::Decl* D) { + clang::Decl* Prev = D->getPreviousDecl(); + while (Prev && Withdrawn.contains(Prev)) + Prev = Prev->getPreviousDecl(); + return llvm::dyn_cast_or_null(Prev); + }; + for (clang::NamedDecl* ND : TopLevel) + if (clang::NamedDecl* Prev = Previous(ND)) { + CutChainAfter(Prev); + if (S.TUScope && !S.TUScope->isDeclScope(Prev)) { + S.TUScope->AddDecl(Prev); + S.IdResolver.AddDecl(Prev); + } + Prev->getDeclContext()->makeDeclVisibleInContext(Prev); + } + for (auto& [DC, D] : Members) + if (clang::NamedDecl* Prev = Previous(D)) { + CutChainAfter(Prev); + Prev->getDeclContext()->makeDeclVisibleInContext(Prev); + } + if (CppInternal::InputRollback* R = I.getRollback()) + R->withdrawFrom(Keep); + // Drop the cached wrappers that live in a withdrawn input. + InterpreterInfo& Info = getInterpInfo(&I); + auto DropWithdrawn = [&](auto& Store) { + llvm::SmallVector::key_type, 8> Dead; + for (const auto& [Key, Wrapper] : Store) { + auto In = Info.WrapperInput.find(Wrapper); + if (In != Info.WrapperInput.end() && In->second >= Keep) + Dead.push_back(Key); + } + for (const auto& Key : Dead) + Store.erase(Key); + }; + DropWithdrawn(Info.WrapperStore); + DropWithdrawn(Info.DtorWrapperStore); + llvm::SmallVector Dead; + for (const auto& [Wrapper, Input] : Info.WrapperInput) + if (Input >= Keep) + Dead.push_back(Wrapper); + for (const void* Wrapper : Dead) + Info.WrapperInput.erase(Wrapper); + return true; +} +#endif + +void NoteWrapperInput(compat::Interpreter& I, const void* Wrapper) { +#ifndef CPPINTEROP_USE_CLING + getInterpInfo(&I).WrapperInput[Wrapper] = InputCount(I) - 1; +#else + (void)I; + (void)Wrapper; +#endif +} + void* compile_wrapper(compat::Interpreter& I, const std::string& wrapper_name, const std::string& wrapper, bool withAccessControl = true) { @@ -5497,6 +5707,7 @@ JitCall::GenericCall make_wrapper(compat::Interpreter& I, compile_wrapper(I, wrapper_name, wrapper_code, withAccessControl); if (wrapper) { WrapperStore.insert(std::make_pair(FD, wrapper)); + NoteWrapperInput(I, wrapper); } else { llvm::errs() << "TClingCallFunc::make_wrapper" << ":" @@ -5688,6 +5899,7 @@ static JitCall::DestructorCall make_dtor_wrapper(compat::Interpreter& interp, /*withAccessControl=*/false); if (F) { DtorWrapperStore.insert(std::make_pair(D, F)); + NoteWrapperInput(interp, F); } else { llvm::errs() << "make_dtor_wrapper" << "Failed to compile\n" @@ -6173,7 +6385,14 @@ static int Declare(compat::Interpreter& I, const char* code, bool silent) { std::optional diagSuppr; if (silent) diagSuppr.emplace(Diag); +#ifdef CPPINTEROP_USE_CLING + auto result = I.declare(code); +#else + const size_t Before = InputCount(I); auto result = I.declare(code); + if (result != compat::Interpreter::kSuccess || Trap.hasErrorOccurred()) + WithdrawInputs(I, Before); +#endif return Trap.hasErrorOccurred() ? 1 : result; } @@ -6189,9 +6408,17 @@ int Process(const char* code) { // run time, so callers cannot rely on it alone. clang::DiagnosticsEngine& Diag = getSema().getDiagnostics(); clang::DiagnosticErrorTrap Trap(Diag); - if (getInterp().process(code) != compat::Interpreter::kSuccess) - return INTEROP_RETURN(1); - return INTEROP_RETURN(Trap.hasErrorOccurred() ? 1 : 0); + compat::Interpreter& I = getInterp(); +#ifndef CPPINTEROP_USE_CLING + const size_t Before = InputCount(I); +#endif + const bool Failed = I.process(code) != compat::Interpreter::kSuccess || + Trap.hasErrorOccurred(); +#ifndef CPPINTEROP_USE_CLING + if (Failed) + WithdrawInputs(I, Before); +#endif + return INTEROP_RETURN(Failed ? 1 : 0); } // Classify the QualType of a successfully-evaluated value into a @@ -6249,7 +6476,15 @@ static Cpp::Box::Kind classifyByQualType(clang::QualType QT) { Box Evaluate(const char* code) { INTEROP_TRACE(code); compat::Value V; - auto res = getInterp().evaluate(code, V); + compat::Interpreter& I = getInterp(); +#ifndef CPPINTEROP_USE_CLING + const size_t Before = InputCount(I); +#endif + auto res = I.evaluate(code, V); +#ifndef CPPINTEROP_USE_CLING + if (res != 0) + WithdrawInputs(I, Before); +#endif CPPINTEROP_MSAN_UNPOISON_VALUE(V); if (res != 0 || !V.hasValue()) return INTEROP_RETURN(Box{}); @@ -7053,8 +7288,32 @@ int Undo(unsigned N) { getInterp().unload(N); return INTEROP_RETURN(compat::Interpreter::kSuccess); #else - return INTEROP_RETURN(getInterp().undo(N)); + compat::Interpreter& I = getInterp(); + const size_t Count = InputCount(I); + if (N > Count || !WithdrawInputs(I, Count - N)) + return INTEROP_RETURN(compat::Interpreter::kFailure); + return INTEROP_RETURN(compat::Interpreter::kSuccess); +#endif +} + +size_t GetInputCount() { + INTEROP_TRACE(); +#ifdef CPPINTEROP_USE_CLING + return INTEROP_RETURN(0); +#else + return INTEROP_RETURN(InputCount(getInterp())); #endif } } // namespace Cpp + +#ifndef CPPINTEROP_USE_CLING +CPPINTEROP_API size_t CppInternal::InputCountOf(clang::Interpreter& I) { + return Cpp::InputCountOfImpl(I); +} + +CPPINTEROP_API void CppInternal::TakeLastValue(clang::Interpreter& I, + clang::Value& V) { + Cpp::TakeLastValueImpl(I, V); +} +#endif diff --git a/lib/CppInterOp/CppInterOp.td b/lib/CppInterOp/CppInterOp.td index a17f33462..8cee1d0b4 100644 --- a/lib/CppInterOp/CppInterOp.td +++ b/lib/CppInterOp/CppInterOp.td @@ -1660,6 +1660,12 @@ potentially mangled symbol name.}]; ]; } +def GetInputCount : CppInterOpAPI { + let Doc = [{Returns the number of inputs the interpreter holds. Pass the +difference to a later count to \\c Undo to return to that point.}]; + let ReturnType = "size_t"; +} + def Undo : CppInterOpAPI { let Doc = [{Reverts the last N operations performed by the interpreter. \\param[in] N The number of operations to undo. Defaults to 1. diff --git a/lib/CppInterOp/CppInterOpInterpreter.h b/lib/CppInterOp/CppInterOpInterpreter.h index 85c34445e..fc3a59ee0 100644 --- a/lib/CppInterOp/CppInterOpInterpreter.h +++ b/lib/CppInterOp/CppInterOpInterpreter.h @@ -9,6 +9,7 @@ #include "Compatibility.h" #include "CompatibilityGLIBC.h" #include "DynamicLibraryManager.h" +#include "InputRollback.h" #include "Paths.h" #include "clang/Interpreter/Interpreter.h" @@ -40,9 +41,13 @@ #include "llvm/ExecutionEngine/Orc/AbsoluteSymbols.h" #include "llvm/ExecutionEngine/Orc/Core.h" #include "llvm/ExecutionEngine/Orc/CoreContainers.h" +#include "llvm/ExecutionEngine/Orc/ExecutionUtils.h" #include "llvm/ExecutionEngine/Orc/LLJIT.h" #include "llvm/ExecutionEngine/Orc/Shared/ExecutorAddress.h" #include "llvm/ExecutionEngine/Orc/Shared/ExecutorSymbolDef.h" +#if __has_include("llvm/ExecutionEngine/Orc/SymbolLookupSet.h") +#include "llvm/ExecutionEngine/Orc/SymbolLookupSet.h" +#endif #include "llvm/Support/Compiler.h" #include "llvm/Support/Error.h" #include "llvm/Support/TargetSelect.h" @@ -329,6 +334,7 @@ class Interpreter { bool outOfProcess; // The device-side CompilerInstance under --cuda, owned by inner. clang::CompilerInstance* deviceCI = nullptr; + std::unique_ptr rollback; #if CLANG_VERSION_MAJOR < 24 // Weak thread_local definitions already handed to the JIT, so later modules @@ -366,7 +372,10 @@ class Interpreter { std::unique_ptr ctx = nullptr, bool oop = false, clang::CompilerInstance* DevCI = nullptr) : inner(std::move(CI)), io_context(std::move(ctx)), outOfProcess(oop), - deviceCI(DevCI) {} + deviceCI(DevCI) { + rollback = + CppInternal::InputRollback::install(*inner->getCompilerInstance()); + } public: static std::unique_ptr @@ -478,20 +487,80 @@ class Interpreter { } llvm::Expected Parse(llvm::StringRef Code) { - return inner->Parse(Code); + if (rollback) + rollback->beginInput(CppInternal::InputCountOf(*inner)); + auto PTU = inner->Parse(Code); + if (!PTU && rollback) + rollback->failedParse(); + return PTU; } + CppInternal::InputRollback* getRollback() { return rollback.get(); } + llvm::Error Execute(clang::PartialTranslationUnit& T) { +#ifndef __EMSCRIPTEN__ + if (T.TheModule && !outOfProcess) + if (llvm::Error Err = checkUnresolved(*T.TheModule)) + return Err; +#endif return inner->Execute(T); } +#ifndef __EMSCRIPTEN__ + /// LLJIT keeps the init function of a module that fails to link, and every + /// later input fails. Only a module with static initializers links here. + llvm::Error checkUnresolved(llvm::Module& M) { + if (llvm::orc::getStaticInitGVs(M).empty()) + return llvm::Error::success(); + llvm::orc::LLJIT& Jit = *compat::getExecutionEngine(*inner); + llvm::orc::SymbolLookupSet Names; + for (const llvm::GlobalValue& GV : M.global_values()) { + if (!GV.isDeclaration() || GV.hasExternalWeakLinkage() || + GV.use_empty() || GV.getName().starts_with("llvm.")) + continue; + // The JIT linker defines these itself. + if (GV.getName() == "__ImageBase" || + GV.getName() == "_GLOBAL_OFFSET_TABLE_") + continue; + Names.add(Jit.mangleAndIntern(GV.getName()), + llvm::orc::SymbolLookupFlags::WeaklyReferencedSymbol); + } + if (Names.empty()) + return llvm::Error::success(); + llvm::orc::JITDylibSearchOrder Order; + Jit.getMainJITDylib().withLinkOrderDo( + [&](const llvm::orc::JITDylibSearchOrder& O) { Order = O; }); + auto& ES = Jit.getExecutionSession(); + auto Found = ES.lookup(Order, Names); + // A symbol that fails to materialize fails the link as well. + if (!Found) { + return Found.takeError(); + } + llvm::orc::SymbolNameVector Missing; + for (const auto& [Name, Flags] : Names) + if (!Found->contains(Name)) + Missing.push_back(Name); + if (Missing.empty()) + return llvm::Error::success(); + return llvm::make_error( + Jit.getExecutionSession().getSymbolStringPool(), std::move(Missing)); + } +#endif + llvm::Error ParseAndExecute(llvm::StringRef Code, clang::Value* V = nullptr) { - // Value-returning execution keeps clang's LastValue handling (private to - // clang::Interpreter), so delegate. The no-value path -- used by wrapper - // compilation -- is split so the module can be sanitized before Execute. - if (V) - return inner->ParseAndExecute(Code, V); - auto PTU = inner->Parse(Code); + // The no-value path -- used by wrapper compilation -- sanitizes the module + // before Execute. + if (V) { + auto PTU = Parse(Code); + if (!PTU) + return PTU.takeError(); + if (PTU->TheModule) + if (llvm::Error Err = Execute(*PTU)) + return Err; + CppInternal::TakeLastValue(*inner, *V); + return llvm::Error::success(); + } + auto PTU = Parse(Code); if (!PTU) return PTU.takeError(); if (PTU->TheModule) { @@ -504,7 +573,7 @@ class Interpreter { if (!outOfProcess) compat::bindProcessWeakGlobals(*PTU->TheModule); #endif - if (llvm::Error Err = inner->Execute(*PTU)) + if (llvm::Error Err = Execute(*PTU)) return Err; } return llvm::Error::success(); diff --git a/lib/CppInterOp/InputRollback.cpp b/lib/CppInterOp/InputRollback.cpp new file mode 100644 index 000000000..eccc5faad --- /dev/null +++ b/lib/CppInterOp/InputRollback.cpp @@ -0,0 +1,217 @@ +//===--- InputRollback.cpp - Undo state an input leaves outside its decls -===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "InputRollback.h" + +#include "clang/AST/ASTConsumer.h" +#include "clang/AST/Decl.h" +#include "clang/AST/DeclTemplate.h" +#include "clang/Basic/FileEntry.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Basic/Specifiers.h" +#include "clang/Basic/Version.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Lex/HeaderSearch.h" +#include "clang/Lex/MacroInfo.h" +#include "clang/Lex/PPCallbacks.h" +#include "clang/Lex/Preprocessor.h" +#include "clang/Sema/Sema.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" + +#if CLANG_VERSION_MAJOR >= 22 +#include "clang/Frontend/MultiplexConsumer.h" +#endif + +#include +#include +#include +#include + +namespace CppInternal { + +struct InputRollback::State { + clang::CompilerInstance* CI; + size_t Input = 0; + // Implicit instantiations the current input requested. + llvm::SmallVector Funcs; + llvm::SmallVector Vars; + struct MacroEvent { + size_t Input; + clang::IdentifierInfo* II; + const clang::MacroDirective* MD; + }; + std::vector Macros; + // Files first entered by an input. + struct FileEvent { + size_t Input; + clang::FileEntryRef File; + }; + std::vector Files; + llvm::SmallPtrSet Seen; + + explicit State(clang::CompilerInstance& CI) : CI(&CI) {} + + void resetInstantiations(); + void revertPreprocessor(size_t From); +}; + +namespace { + +class InstantiationRecorder : public clang::ASTConsumer { + std::shared_ptr S; + +public: + explicit InstantiationRecorder(std::shared_ptr S) + : S(std::move(S)) {} + void + HandleCXXImplicitFunctionInstantiation(clang::FunctionDecl* FD) override { + S->Funcs.push_back(FD); + } + void HandleCXXStaticMemberVarInstantiation(clang::VarDecl* VD) override { + S->Vars.push_back(VD); + } +}; + +class PreprocessorRecorder : public clang::PPCallbacks { + std::shared_ptr S; + +public: + explicit PreprocessorRecorder(std::shared_ptr S) + : S(std::move(S)) {} + void MacroDefined(const clang::Token& Name, + const clang::MacroDirective* MD) override { + S->Macros.push_back({S->Input, Name.getIdentifierInfo(), MD}); + } + void MacroUndefined(const clang::Token& Name, const clang::MacroDefinition&, + const clang::MacroDirective* Undef) override { + if (Undef) + S->Macros.push_back({S->Input, Name.getIdentifierInfo(), Undef}); + } + void FileChanged(clang::SourceLocation Loc, FileChangeReason Reason, + clang::SrcMgr::CharacteristicKind, clang::FileID) override { + if (Reason != EnterFile) + return; + clang::SourceManager& SM = S->CI->getSourceManager(); + if (clang::OptionalFileEntryRef FE = + SM.getFileEntryRefForID(SM.getFileID(Loc))) + if (S->Seen.insert(&FE->getFileEntry()).second) + S->Files.push_back({S->Input, *FE}); + } +}; + +} // namespace + +void InputRollback::State::resetInstantiations() { + // Sema treats these as done, but their bodies never reached CodeGen. + llvm::SmallPtrSet Reset; + for (clang::FunctionDecl* FD : Funcs) { + if (FD->getTemplateSpecializationKind() != + clang::TSK_ImplicitInstantiation || + FD->isLateTemplateParsed()) + continue; + FD->setBody(nullptr); + FD->setWillHaveBody(false); + FD->setInstantiationIsPending(false); + if (auto* FTSI = FD->getTemplateSpecializationInfo()) + FTSI->setPointOfInstantiation(clang::SourceLocation()); + else if (auto* MSI = FD->getMemberSpecializationInfo()) + MSI->setPointOfInstantiation(clang::SourceLocation()); + Reset.insert(FD); + } + for (clang::VarDecl* VD : Vars) { + if (VD->getTemplateSpecializationKind() != clang::TSK_ImplicitInstantiation) + continue; + if (auto* MSI = VD->getMemberSpecializationInfo()) + MSI->setPointOfInstantiation(clang::SourceLocation()); + Reset.insert(VD); + } + clang::Sema& Sema = CI->getSema(); + auto Queued = [&](const auto& P) { return Reset.count(P.first); }; + llvm::erase_if(Sema.PendingInstantiations, Queued); + llvm::erase_if(Sema.PendingLocalImplicitInstantiations, Queued); + Funcs.clear(); + Vars.clear(); +} + +void InputRollback::State::revertPreprocessor(size_t From) { + clang::Preprocessor& PP = CI->getPreprocessor(); + // Newest first: each step restores the state before its directive. + while (!Macros.empty() && Macros.back().Input >= From) { + MacroEvent E = Macros.back(); + Macros.pop_back(); + const clang::MacroDirective* Prev = E.MD->getPrevious(); + const clang::MacroInfo* MI = Prev ? Prev->getMacroInfo() : nullptr; + if (MI) + PP.appendDefMacroDirective( + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + E.II, const_cast(MI), E.MD->getLocation()); + else if (PP.isMacroDefined(E.II)) + PP.appendMacroDirective( + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + E.II, new (PP.getPreprocessorAllocator()) + clang::UndefMacroDirective(E.MD->getLocation())); + } + // Include guards are macros; #pragma once and #import need this. + clang::HeaderSearch& HS = PP.getHeaderSearchInfo(); + while (!Files.empty() && Files.back().Input >= From) { + clang::FileEntryRef File = Files.back().File; + Files.pop_back(); + Seen.erase(&File.getFileEntry()); + PP.getIncludedFiles().erase(&File.getFileEntry()); + clang::HeaderFileInfo& HFI = HS.getFileInfo(File); + HFI.isPragmaOnce = false; + HFI.isImport = false; + } +} + +InputRollback::InputRollback(std::shared_ptr S) : S(std::move(S)) {} +InputRollback::~InputRollback() = default; + +std::unique_ptr +InputRollback::install(clang::CompilerInstance& CI) { +#if CLANG_VERSION_MAJOR >= 22 + // The consumer forwards these callbacks also in a failed input. + struct Access : clang::MultiplexConsumer { + static void add(clang::MultiplexConsumer& M, + std::unique_ptr C) { + (M.*(&Access::Consumers)).push_back(std::move(C)); + } + }; + auto St = std::make_shared(CI); + // clang builds without RTTI. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) + Access::add(static_cast(CI.getASTConsumer()), + std::make_unique(St)); + CI.getPreprocessor().addPPCallbacks( + std::make_unique(St)); + return std::make_unique(std::move(St)); +#else + (void)CI; + return nullptr; +#endif +} + +void InputRollback::beginInput(size_t Index) { + S->Input = Index; + S->Funcs.clear(); + S->Vars.clear(); +} + +void InputRollback::failedParse() { + S->resetInstantiations(); + S->revertPreprocessor(S->Input); +} + +void InputRollback::withdrawFrom(size_t Keep) { S->revertPreprocessor(Keep); } + +} // namespace CppInternal diff --git a/lib/CppInterOp/InputRollback.h b/lib/CppInterOp/InputRollback.h new file mode 100644 index 000000000..9a7c024bb --- /dev/null +++ b/lib/CppInterOp/InputRollback.h @@ -0,0 +1,64 @@ +//===--- InputRollback.h - Undo state an input leaves outside its decls ---===// +// +// Part of the compiler-research project, under the Apache License v2.0 with +// LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LIB_CPPINTEROP_INPUTROLLBACK_H +#define LIB_CPPINTEROP_INPUTROLLBACK_H + +#include "CppInterOp/CppInterOpTypes.h" + +#include +#include + +namespace clang { +class CompilerInstance; +class Interpreter; +class Value; +} // namespace clang + +namespace CppInternal { + +/// The number of partial translation units \p I holds. +CPPINTEROP_API size_t InputCountOf(clang::Interpreter& I); + +/// Moves the value of the last input into \p V, as +/// clang::Interpreter::ParseAndExecute does. +CPPINTEROP_API void TakeLastValue(clang::Interpreter& I, clang::Value& V); + +/// Undoes what clang-repl keeps of a withdrawn input: the instantiations that +/// a failed input requested, macros, and include state. +class InputRollback { +public: + struct State; + + /// Hooks into the consumer and preprocessor of \p CI. Returns null when + /// this clang version lays out the consumer differently. + static std::unique_ptr install(clang::CompilerInstance& CI); + + /// Call before parsing an input; \p Index is the input count then. + CPPINTEROP_API void beginInput(size_t Index); + /// Call when the input started by beginInput fails to parse. + CPPINTEROP_API void failedParse(); + /// Call after the inputs from \p Keep on are undone. + void withdrawFrom(size_t Keep); + + explicit InputRollback(std::shared_ptr S); + InputRollback(const InputRollback&) = delete; + InputRollback& operator=(const InputRollback&) = delete; + InputRollback(InputRollback&&) = delete; + InputRollback& operator=(InputRollback&&) = delete; + ~InputRollback(); + +private: + // Shared with the hooks, which clang owns and may outlive this object. + std::shared_ptr S; +}; + +} // namespace CppInternal + +#endif // LIB_CPPINTEROP_INPUTROLLBACK_H diff --git a/lib/CppInterOp/InterpreterInfo.h b/lib/CppInterOp/InterpreterInfo.h index b24c0b8bb..acf312d12 100644 --- a/lib/CppInterOp/InterpreterInfo.h +++ b/lib/CppInterOp/InterpreterInfo.h @@ -25,6 +25,7 @@ #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/StringMap.h" +#include #include #include #include @@ -42,6 +43,8 @@ struct InterpreterInfo { // interpreter, so the caches must be destroyed together with it. llvm::DenseMap WrapperStore; llvm::DenseMap DtorWrapperStore; + // The input that holds each cached wrapper, keyed on the wrapper. + llvm::DenseMap WrapperInput; // Constant-array initializers materialized by GetVariableOffset, keyed on // the AST node so each variable is laid out once (stable address per // query, no bump-allocator growth). A 0 entry records a deterministic diff --git a/unittests/CppInterOp/InterpreterTest.cpp b/unittests/CppInterOp/InterpreterTest.cpp index 70c645e6e..119e8a8df 100644 --- a/unittests/CppInterOp/InterpreterTest.cpp +++ b/unittests/CppInterOp/InterpreterTest.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #ifndef _WIN32 #include @@ -1057,6 +1059,321 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, SignalHandler_MultipleInterpreters) { #endif // GTEST_HAS_DEATH_TEST #ifndef __EMSCRIPTEN__ +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_FailedLinkKeepsLaterInputs) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "cling has its own declare path"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the link check is in-process only"; + TestFixture::CreateInterpreter(); + + const size_t Before = Cpp::GetInputCount(); + EXPECT_NE(0, Cpp::Declare("int fl_undefined(); int fl_use = fl_undefined();", + /*silent=*/true)); + EXPECT_EQ(Before, Cpp::GetInputCount()); + + EXPECT_EQ(0, Cpp::Declare("int fl_later = 1;")); + EXPECT_EQ(0, Cpp::Process("fl_later += 1;")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_FailedLinkRetrySameNames) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "cling has its own declare path"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the link check is in-process only"; + TestFixture::CreateInterpreter(); + + EXPECT_NE(0, Cpp::Declare("int fr_x = 1; int fr_h(); int fr_y = fr_h();", + /*silent=*/true)); + EXPECT_EQ(0, + Cpp::Declare( + "int fr_x = 2; int fr_h() { return 3; } int fr_y = fr_h();")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_FailedLinkOfLazyDefinition) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "cling has its own declare path"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the link check is in-process only"; + TestFixture::CreateInterpreter(); + + // fll_f links only on lookup, so the check's own lookup fails. + EXPECT_EQ(0, Cpp::Declare("int fll_missing(); int fll_f() { return " + "fll_missing(); }")); + EXPECT_NE(0, Cpp::Declare("int fll_x = fll_f();", /*silent=*/true)); + EXPECT_EQ(0, Cpp::Declare("int fll_later = 1;")); +} + +#ifdef __ELF__ +TYPED_TEST(CPPINTEROP_TEST_MODE, + Interpreter_FailedLinkCheckSkipsLinkerSymbols) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "cling has its own declare path"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the link check is in-process only"; + TestFixture::CreateInterpreter(); + + // The initializer makes the check run; fls_got never runs. + EXPECT_EQ(0, Cpp::Declare(R"(extern "C" char _GLOBAL_OFFSET_TABLE_[]; + void* fls_got() { return _GLOBAL_OFFSET_TABLE_; } + int fls_one() { return 1; } + int fls_init = fls_one();)")); +} +#endif + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoToInputCount) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "GetInputCount is clang-repl only"; +#endif + TestFixture::CreateInterpreter(); + + const size_t Before = Cpp::GetInputCount(); + EXPECT_EQ(0, Cpp::Declare("int uc_a = 1;")); + EXPECT_EQ(0, Cpp::Declare("int uc_b = uc_a;")); + EXPECT_EQ(0, Cpp::Undo(Cpp::GetInputCount() - Before)); + EXPECT_EQ(Before, Cpp::GetInputCount()); + + EXPECT_EQ(0, Cpp::Declare("double uc_a = 0.5; double uc_b = 1.5;")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoKeepsStartupInputs) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "GetInputCount is clang-repl only"; +#endif + TestFixture::CreateInterpreter(); + + EXPECT_EQ(0, Cpp::Declare("int uks_a = 1;")); + const size_t Count = Cpp::GetInputCount(); + EXPECT_NE(0, Cpp::Undo(Count)); + EXPECT_EQ(Count, Cpp::GetInputCount()); + EXPECT_EQ(0, Cpp::Process("uks_a += 1;")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoDropsWithdrawnWrapper) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "GetInputCount is clang-repl only"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + TestFixture::CreateInterpreter(); + Cpp::Declare("#include "); // Needed by JitCall + Cpp::Declare("int uw_five() { return 5; }"); + auto FD = Cpp::GetNamed("uw_five"); + ASSERT_TRUE(FD); + + // The wrapper is compiled after the checkpoint, so Undo withdraws it. + const size_t Before = Cpp::GetInputCount(); + auto JC1 = Cpp::MakeFunctionCallable(Cpp::FuncRef{FD.data}); + ASSERT_TRUE(JC1.isValid()); + ASSERT_EQ(0, Cpp::Undo(Cpp::GetInputCount() - Before)); + + auto JC2 = Cpp::MakeFunctionCallable(Cpp::FuncRef{FD.data}); + ASSERT_TRUE(JC2.isValid()); + int r = 0; + JC2.Invoke(&r); + EXPECT_EQ(5, r); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoKeepsOlderWrapper) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "GetInputCount is clang-repl only"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + TestFixture::CreateInterpreter(); + Cpp::Declare("#include "); // Needed by JitCall + Cpp::Declare("int uk_six() { return 6; }"); + auto FD = Cpp::GetNamed("uk_six"); + ASSERT_TRUE(FD); + ASSERT_TRUE(Cpp::MakeFunctionCallable(Cpp::FuncRef{FD.data}).isValid()); + + const size_t Before = Cpp::GetInputCount(); + EXPECT_EQ(0, Cpp::Declare("int uk_other = 1;")); + ASSERT_EQ(0, Cpp::Undo(Cpp::GetInputCount() - Before)); + + // The wrapper stays cached: no new input compiles it again. + auto JC = Cpp::MakeFunctionCallable(Cpp::FuncRef{FD.data}); + ASSERT_TRUE(JC.isValid()); + EXPECT_EQ(Before, Cpp::GetInputCount()); + int r = 0; + JC.Invoke(&r); + EXPECT_EQ(6, r); +} + +#if !defined(CPPINTEROP_USE_CLING) && CLANG_VERSION_MAJOR >= 22 +#define SKIP_WITHOUT_INPUT_ROLLBACK() +#else +#define SKIP_WITHOUT_INPUT_ROLLBACK() \ + GTEST_SKIP() << "input rollback needs clang-repl from LLVM 22" +#endif + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_FailedParseResetsInstantiations) { + SKIP_WITHOUT_INPUT_ROLLBACK(); + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the link check is in-process only"; + TestFixture::CreateInterpreter(); + + EXPECT_EQ(0, + Cpp::Declare("template T fpi_tw(T t) { return 2*t; }")); + EXPECT_NE(0, Cpp::Declare("int fpi_a = fpi_tw(3); int fpi_b = fpi_nope;", + /*silent=*/true)); + // The instantiation must be emitted again, or this fails to link. + EXPECT_EQ(0, Cpp::Process("int fpi_again = fpi_tw(3);")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + Interpreter_FailedParseResetsMemberInstantiations) { + SKIP_WITHOUT_INPUT_ROLLBACK(); + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the link check is in-process only"; + TestFixture::CreateInterpreter(); + + EXPECT_EQ(0, Cpp::Declare(R"( + template struct fpm_s { T f() { return T(2); } static T s; }; + template T fpm_s::s = T(3);)")); + EXPECT_NE(0, Cpp::Declare("int fpm_a = fpm_s().f() + fpm_s::s;" + "int fpm_b = fpm_nope;", + /*silent=*/true)); + EXPECT_EQ(0, + Cpp::Process("int fpm_again = fpm_s().f() + fpm_s::s;")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + Interpreter_FailedParseKeepsExplicitInstantiations) { + SKIP_WITHOUT_INPUT_ROLLBACK(); + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "the link check is in-process only"; + TestFixture::CreateInterpreter(); + + EXPECT_EQ(0, Cpp::Declare(R"( + template inline T fpe_f(T t) { return 2 * t; } + extern template int fpe_f(int); + template struct fpe_s { static T s; }; + template T fpe_s::s = T(3);)")); + EXPECT_NE(0, Cpp::Declare("int fpe_a = fpe_f(3); template int fpe_s::s;" + "int fpe_b = fpe_nope;", + /*silent=*/true)); + // The extern template needs its explicit instantiation from the user. + EXPECT_EQ(0, Cpp::Process("template int fpe_f(int);" + "int fpe_again = fpe_f(3) + fpe_s::s;")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_FailedParseDropsMacros) { + SKIP_WITHOUT_INPUT_ROLLBACK(); + TestFixture::CreateInterpreter(); + + EXPECT_NE(0, Cpp::Declare("#define FPM_X 1\nint fpm_bad = fpm_nope;", + /*silent=*/true)); + EXPECT_EQ(0, Cpp::Declare("#ifdef FPM_X\n#error kept\n#endif")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoRestoresMacro) { + SKIP_WITHOUT_INPUT_ROLLBACK(); + TestFixture::CreateInterpreter(); + + EXPECT_EQ(0, Cpp::Declare("#define URM_X 1")); + const size_t Before = Cpp::GetInputCount(); + EXPECT_EQ(0, Cpp::Declare("#undef URM_X\n#define URM_X 2\nint urm_d = 0;")); + EXPECT_EQ(0, Cpp::Undo(Cpp::GetInputCount() - Before)); + EXPECT_EQ(0, Cpp::Declare("static_assert(URM_X == 1, \"restored\");")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoReentersHeaders) { + SKIP_WITHOUT_INPUT_ROLLBACK(); + TestFixture::CreateInterpreter(); + + llvm::SmallString<128> Dir; + ASSERT_FALSE(llvm::sys::fs::createUniqueDirectory("undo-headers", Dir)); + auto Write = [&](const char* Name, const char* Text) { + llvm::SmallString<128> P(Dir); + llvm::sys::path::append(P, Name); + std::error_code EC; + llvm::raw_fd_ostream(P, EC) << Text; + return std::string(P); + }; + std::string Guard = Write("guard.h", "#ifndef URH_G\n#define URH_G\n" + "struct URHGuard { int v = 1; };\n" + "#endif\n"); + std::string Once = Write("once.h", "#pragma once\n" + "struct URHOnce { int v = 2; };\n"); + const std::string Include = + "#include \"" + Guard + "\"\n#include \"" + Once + "\"\n"; + + const size_t Before = Cpp::GetInputCount(); + EXPECT_EQ(0, Cpp::Declare(Include.c_str())); + EXPECT_EQ(0, Cpp::Undo(Cpp::GetInputCount() - Before)); + EXPECT_EQ(0, Cpp::Declare(Include.c_str())); + EXPECT_EQ(0, Cpp::Declare("int urh = URHGuard{}.v + URHOnce{}.v;")); + EXPECT_FALSE(llvm::sys::fs::remove_directories(Dir)); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoInReopenedNamespace) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "GetInputCount is clang-repl only"; +#endif + TestFixture::CreateInterpreter(); + + EXPECT_EQ(0, Cpp::Declare("namespace URN { namespace In { int keep; } }")); + const size_t Before = Cpp::GetInputCount(); + EXPECT_EQ(0, Cpp::Declare("namespace URN { namespace In { int v; } }")); + EXPECT_EQ(0, Cpp::Undo(Cpp::GetInputCount() - Before)); + EXPECT_FALSE(Cpp::GetNamed("v", Cpp::GetNamed("In", Cpp::GetNamed("URN")))); + + EXPECT_EQ(0, Cpp::Declare("namespace URN { namespace In { double v; } }")); + EXPECT_EQ(0, Cpp::Declare("static_assert(sizeof(URN::In::v) == " + "sizeof(double), \"new v\");" + "int* urn_keep = &URN::In::keep;")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoHidesEnumerators) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "GetInputCount is clang-repl only"; +#endif + TestFixture::CreateInterpreter(); + + EXPECT_EQ(0, Cpp::Declare("namespace UHE {}")); + const size_t Before = Cpp::GetInputCount(); + EXPECT_EQ(0, Cpp::Declare("enum UHE1 { UHE_A = 1 };" + "namespace UHE { enum E1 { UHE_B = 1 }; }")); + EXPECT_EQ(0, Cpp::Undo(Cpp::GetInputCount() - Before)); + + EXPECT_EQ(0, Cpp::Declare("enum UHE2 { UHE_A = 2 };" + "namespace UHE { enum E2 { UHE_B = 2 }; }")); + EXPECT_EQ(0, Cpp::Declare("static_assert(UHE_A == 2 && UHE::UHE_B == 2, " + "\"new enumerators\");")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoDefinitionKeepsDeclaration) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "GetInputCount is clang-repl only"; +#endif + TestFixture::CreateInterpreter(); + + EXPECT_EQ(0, Cpp::Declare("int udk_f(); extern int udk_x;")); + const size_t Before = Cpp::GetInputCount(); + EXPECT_EQ(0, Cpp::Declare("int udk_f() { return 1; } int udk_x = 1;")); + EXPECT_EQ(0, Cpp::Undo(Cpp::GetInputCount() - Before)); + EXPECT_EQ(0, Cpp::Declare("int udk_f() { return 2; } int udk_x = 2;")); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_UndoRedeclarations) { +#ifdef CPPINTEROP_USE_CLING + GTEST_SKIP() << "GetInputCount is clang-repl only"; +#endif + TestFixture::CreateInterpreter(); + + EXPECT_EQ(0, Cpp::Declare("int urd_f(); int urd_f(); typedef int urd_t;")); + const size_t Before = Cpp::GetInputCount(); + EXPECT_EQ(0, Cpp::Declare("int urd_f(); typedef int urd_t;")); + EXPECT_EQ(0, Cpp::Declare("int urd_f();")); + EXPECT_EQ(0, Cpp::Undo(Cpp::GetInputCount() - Before)); + EXPECT_EQ(0, + Cpp::Process("int urd_f() { return 3; } urd_t urd_x = urd_f();")); +} + TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_WrapperCacheIsPerInterpreter) { if (TypeParam::isOutOfProcess) GTEST_SKIP() << "Test fails for OOP JIT builds"; diff --git a/unittests/CppInterOp/VariableReflectionTest.cpp b/unittests/CppInterOp/VariableReflectionTest.cpp index b686e9817..2cd2671cb 100644 --- a/unittests/CppInterOp/VariableReflectionTest.cpp +++ b/unittests/CppInterOp/VariableReflectionTest.cpp @@ -622,8 +622,9 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableOffset_UnmaterializableDefinition) { // A definition whose dynamic initializer needs an undefined symbol parses // fine but cannot be materialized: the odr-use anchor's execution fails - // and the query must fail cleanly. (The anchor's flush block is - // clang-repl-only, hence the gate.) + // and the query must fail cleanly. A template keeps the initializer out of + // the declaring input. (The anchor's flush block is clang-repl-only, hence + // the gate.) #ifdef __EMSCRIPTEN__ // The intentional materialization failure leaves the process-global wasm // dynamic-linking state broken for every later load (see issue #1071). @@ -641,13 +642,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, if (TypeParam::isOutOfProcess) GTEST_SKIP() << "A failed dlupdate wedges the out-of-process session"; TestFixture::CreateInterpreter(); - Cpp::Declare(R"( + ASSERT_EQ(0, Cpp::Declare(R"( int undefined_fn(); - struct Unmaterializable { + template struct Unmaterializable { inline static int probe = undefined_fn(); }; - )"); - Cpp::DeclRef klass = Cpp::GetNamed("Unmaterializable"); + using UnmaterializableInt = Unmaterializable; + static_assert(sizeof(UnmaterializableInt) > 0, "instantiate the class"); + )")); + Cpp::DeclRef klass = + Cpp::GetUnderlyingScope(Cpp::GetNamed("UnmaterializableInt")); ASSERT_TRUE(klass); Cpp::DeclRef var = Cpp::GetNamed("probe", klass); ASSERT_TRUE(var);