diff --git a/interpreter/CppInterOp/CMakeLists.txt b/interpreter/CppInterOp/CMakeLists.txt index 0a14b6a9d7107..e65c162e29d9b 100644 --- a/interpreter/CppInterOp/CMakeLists.txt +++ b/interpreter/CppInterOp/CMakeLists.txt @@ -17,6 +17,12 @@ option(CPPINTEROP_BUILD_TABLEGEN_ONLY "Only build cppinterop-tblgen (for cross-c #=Set OFF to suppress install() rules and route clangCppInterOp through # add_llvm_library(... BUILDTREE_ONLY) option(CPPINTEROP_INSTALL "Generate install rules for CppInterOp" ON) + +# OFF to ship the shared library as a single unversioned file, matching +# the Darwin layout: archives that cannot carry symlinks (Python wheels) +# would otherwise materialize the SOVERSION namelink as a second full copy. +option(CPPINTEROP_SHARED_LIBRARY_VERSIONING + "Attach the LLVM SOVERSION and namelink to shared clangCppInterOp" ON) if(EMSCRIPTEN) set(CPPINTEROP_EXTRA_WASM_FLAGS "-fwasm-exceptions" CACHE STRING "Extra flags for wasm") endif() @@ -504,6 +510,7 @@ if(CPPINTEROP_INSTALL) install(DIRECTORY include/ DESTINATION include + COMPONENT cppinterop-headers FILES_MATCHING PATTERN "*.def" PATTERN "*.h" @@ -513,12 +520,14 @@ if(CPPINTEROP_INSTALL) install(DIRECTORY tools/ DESTINATION include/CppInterOp/tools + COMPONENT cppinterop-headers FILES_MATCHING PATTERN "*.h" ) install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/include/ DESTINATION include + COMPONENT cppinterop-headers FILES_MATCHING PATTERN "CMakeFiles" EXCLUDE PATTERN "*.inc" @@ -617,6 +626,17 @@ if(NOT EMSCRIPTEN AND NOT CPPINTEROP_TABLEGEN_EXE) add_subdirectory(utils/TableGen) endif() add_subdirectory(lib) + +if(CPPINTEROP_INSTALL) + # Installing the headers needs to depend on generating the tablegen'd + # public headers. + add_custom_target(cppinterop-headers DEPENDS CppInterOpTableGen) + if(NOT LLVM_ENABLE_IDE) + add_llvm_install_targets(install-cppinterop-headers + DEPENDS cppinterop-headers + COMPONENT cppinterop-headers) + endif() +endif() if (CPPINTEROP_ENABLE_TESTING) add_subdirectory(unittests) endif(CPPINTEROP_ENABLE_TESTING) diff --git a/interpreter/CppInterOp/cmake/modules/GoogleTest.cmake b/interpreter/CppInterOp/cmake/modules/GoogleTest.cmake index 0b3d2ac740c3f..0997ff8134d25 100644 --- a/interpreter/CppInterOp/cmake/modules/GoogleTest.cmake +++ b/interpreter/CppInterOp/cmake/modules/GoogleTest.cmake @@ -84,7 +84,7 @@ ExternalProject_Add( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_SHALLOW FALSE - GIT_TAG fa8438ae6b70c57010177de47a9f13d7041a6328 + GIT_TAG v1.18.0 UPDATE_COMMAND "" ${_gtest_configure} BUILD_COMMAND ${build_cmd} diff --git a/interpreter/CppInterOp/cppinterop-version.tag b/interpreter/CppInterOp/cppinterop-version.tag index d17131ad72a20..85a7dace6dfa8 100644 --- a/interpreter/CppInterOp/cppinterop-version.tag +++ b/interpreter/CppInterOp/cppinterop-version.tag @@ -1 +1 @@ -bdf062f298a20c13c44edacfd55170dbd3ad56ca +7cb23ca494cdaf3d5e6cc57318c4f561abcf1d84 diff --git a/interpreter/CppInterOp/etc/lsan-cling.supp b/interpreter/CppInterOp/etc/lsan-cling.supp new file mode 100644 index 0000000000000..67788f3d7d1db --- /dev/null +++ b/interpreter/CppInterOp/etc/lsan-cling.supp @@ -0,0 +1,14 @@ +# Two cling interpreter-teardown leaks, fixed in root-project/root#23045; +# suppress until the fixes reach root-project/cling. Applied only to the +# cling+Address row; the clang-repl asan row runs unsuppressed. +# +# 1. deallocate() retains code via FinalizedAlloc::release(), orphaning +# each allocation's DeallocActions vector (eh-frame deregistration, +# new with LLVM 22). +# 2. ShutDown() leaks the frontend through the DisableFree path and hides +# the pointers with llvm::BuryPointer, which only holds 16 pointers; +# destroying a few interpreters per process overflows it. +# +# The allocation stacks are often 2 frames deep (the fast unwinder stops +# at JIT frames), hence the module-level match. +leak:libclangCppInterOp diff --git a/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h b/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h index 45a470e7648b3..4a2f72e61e9d8 100644 --- a/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h +++ b/interpreter/CppInterOp/include/CppInterOp/CppInterOpTypes.h @@ -412,7 +412,19 @@ enum class AllocType : unsigned char { NewArr, Malloc, Unknown, - CustomAlloc + CustomAlloc, + Null, + OperatorNew, + OperatorNewArr +}; + +enum class DeallocType : unsigned char { + None, + Delete, + DeleteArr, + Free, + Unknown, // Contradiction, delete x or delete[] x in same func + Opaque // Could not analyze }; inline QualKind operator|(QualKind a, QualKind b) { diff --git a/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt b/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt index 05ae3721e1a84..f098b417e43cf 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt +++ b/interpreter/CppInterOp/lib/CppInterOp/CMakeLists.txt @@ -225,6 +225,13 @@ add_llvm_library(clangCppInterOp ${link_libs} ) +# llvm_add_library() stamps LLVM's VERSION/SOVERSION on shared libraries on +# ELF platforms; unsetting them ships one unversioned file, as on Darwin. +if(NOT CPPINTEROP_SHARED_LIBRARY_VERSIONING) + set_property(TARGET clangCppInterOp PROPERTY VERSION) + set_property(TARGET clangCppInterOp PROPERTY SOVERSION) +endif() + if(EMSCRIPTEN) if(BUILD_SHARED_LIBS) # FIXME: When dynamically linking the Emscripten shared library to the @@ -275,6 +282,3 @@ string(REPLACE ";" "\;" _VER CPPINTEROP_VERSION) set_source_files_properties(CppInterOp.cpp PROPERTIES COMPILE_DEFINITIONS "LLVM_BINARY_DIR=\"${LLVM_BINARY_DIR}\";CPPINTEROP_VERSION=\"${_VAR}\"" ) -if(MSVC) - set_source_files_properties(CppInterOp.cpp COMPILE_FLAGS /bigobj) -endif() diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp index 6a678a197282f..7982a3f4c39e5 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.cpp @@ -59,6 +59,7 @@ #include "clang/Basic/CharInfo.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/DiagnosticSema.h" +#include "clang/Basic/LLVM.h" #include "clang/Basic/LangStandard.h" #include "clang/Basic/Linkage.h" #include "clang/Basic/OperatorKinds.h" @@ -402,6 +403,91 @@ static void ForceCodeGen(Decl* D, compat::Interpreter& I) { #endif } +// Force emission of a variable's definition by synthesizing an odr-use of +// it, instead of ForceCodeGen's UsedAttr route. The UsedAttr route records +// the emitted global in codegen's llvm.used list as a weak handle; the +// handle can go null (global deleted) before a later incremental PTU's +// emitUsed runs, and release-built clang dereferences it without checking — +// a process crash that accumulates with interpreter state rather than +// tracing to any one declaration. The odr-use anchor is a dummy global +// initialized with the variable's address, built directly through Sema (the +// AST that parsing "&var" would produce — no source-text round-trip), so +// the definition flows through the regular deferred-decl path and leaves no +// used-list residue. Returns false when the odr-use cannot be built — the +// caller falls back to ForceCodeGen. +// FIXME: Remove the synthesized-anchor mechanism once clang grows a direct +// interpreter emission API for a GlobalDecl (the emitUsed hardening it +// depends on landed via llvm/llvm-project#210959): +#if CLANG_VERSION_MAJOR >= 24 +#warning "Revisit EmitVariableViaOdrUse: a direct-emission API may exist now" +#endif +static bool EmitVariableViaOdrUse(compat::Interpreter& I, VarDecl* VD) { + // A reference cannot conjure a definition that does not exist: the dummy + // would carry an unresolvable symbol into the JIT (Emscripten's dynamic + // loader rejects the whole module over it, and the stale entry then + // breaks every later load in the process). The UsedAttr fallback defers + // harmlessly for definition-less declarations. + VarDecl* Def = VD->getDefinition(); + if (!Def) + return false; + + Sema& S = I.getCI()->getSema(); + ASTContext& C = S.getASTContext(); + + // Open the synthesizing region before ANY Sema work: odr-use marking in + // BuildDeclRefExpr can trigger an immediate static-member instantiation, + // and cling's DeclCollector aborts on instantiation callbacks that arrive + // outside an active transaction. (Inert under clang-repl — TODO destructor + // in Compatibility.h.) + compat::SynthesizingCodeRAII RAII(&I); + + // Build '&VD' the way the parser would; the address-of marks Def + // odr-used, which is what schedules the deferred definition. + ExprResult Ref = S.BuildDeclRefExpr(Def, Def->getType().getNonReferenceType(), + VK_LValue, SourceLocation()); + if (Ref.isInvalid()) + return false; + ExprResult AddrOf = + S.CreateBuiltinUnaryOp(SourceLocation(), UO_AddrOf, Ref.get()); + if (AddrOf.isInvalid()) + return false; + + // Anchor the odr-use in an external-linkage dummy the JIT must emit; the + // initializer's implicit conversion to 'const void*' is Sema-checked, so + // exotic types fail over to the fallback instead of asserting. + // FIXME: The synthesized nodes are parked in the TU for the rest of the + // session; move them under a scratch area whose AST nodes get deallocated + // after emission. + std::string DummyName = "__cppinterop_odr_use_v" + + std::to_string(getInterpInfo(&I).OdrUseCounter++); + QualType Ty = C.getPointerType(C.VoidTy.withConst()); + TranslationUnitDecl* TU = C.getTranslationUnitDecl(); + VarDecl* Dummy = VarDecl::Create(C, TU, SourceLocation(), SourceLocation(), + &C.Idents.get(DummyName), Ty, + C.getTrivialTypeSourceInfo(Ty), SC_None); + S.AddInitializerToDecl(Dummy, AddrOf.get(), /*DirectInit=*/false); + if (Dummy->isInvalidDecl()) + return false; + TU->addDecl(Dummy); + + // Under cling the RAII's transaction commit (at scope exit) triggers + // emission of everything collected above plus this dummy. + I.getCI()->getASTConsumer().HandleTopLevelDecl(DeclGroupRef(Dummy)); +#ifndef CPPINTEROP_USE_CLING + // FIXME: Parsing an empty string is the only way to flush incremental + // CodeGen for a decl handed straight to the consumer — the state-reset + // bug SynthesizingCodeRAII's clang-repl destructor should eventually + // own. Drop this when it does. + auto GeneratedPTU = I.Parse(""); + if (llvm::Error Err = + !GeneratedPTU ? GeneratedPTU.takeError() : I.Execute(*GeneratedPTU)) { + llvm::consumeError(std::move(Err)); + return false; + } +#endif + return true; +} + #define DEBUG_TYPE "jitcall" bool JitCall::AreArgumentsValid(void* result, ArgList args, void* self, size_t nary) const { @@ -659,6 +745,36 @@ bool IsComplete(ConstDeclRef DRef) { return INTEROP_RETURN(true); } +DeclRef GetOrForceDefinition(DeclRef DRef) { + INTEROP_TRACE(DRef); + if (!DRef) + return INTEROP_RETURN(nullptr); + + auto* D = unwrap(DRef); + + if (auto* TD = dyn_cast(D)) { + if (!TD->getDefinition()) { + clang::Sema& S = getSema(); + QualType QT = QualType::getFromOpaquePtr(GetTypeFromScope(DRef).data); + SourceLocation fakeLoc = GetValidSLoc(S); + compat::SynthesizingCodeRAII RAII(&getInterp()); + S.isCompleteType(fakeLoc, QT); + } + return INTEROP_RETURN(TD->getDefinition()); + } + + if (auto* FD = dyn_cast(D)) { + if (!FD->getDefinition() && FD->getTemplateInstantiationPattern()) + InstantiateFunctionDefinition(D); + return INTEROP_RETURN(FD->getDefinition()); + } + + if (auto* VD = dyn_cast(D)) + return INTEROP_RETURN(VD->getDefinition()); + + return INTEROP_RETURN(nullptr); +} + size_t SizeOf(ConstDeclRef DRef) { INTEROP_TRACE(DRef); assert(DRef); @@ -710,11 +826,14 @@ bool IsTypedefed(ConstDeclRef DRef) { return INTEROP_RETURN(llvm::isa_and_nonnull(D)); } -bool IsAbstract(ConstDeclRef DRef) { +bool IsAbstract(DeclRef DRef) { INTEROP_TRACE(DRef); const auto* D = unwrap(DRef); - if (const auto* CXXRD = llvm::dyn_cast_or_null(D)) - return INTEROP_RETURN(CXXRD->isAbstract()); + if (llvm::isa_and_nonnull(D)) { + const auto* Def = llvm::dyn_cast_or_null( + unwrap(GetOrForceDefinition(DRef))); + return INTEROP_RETURN(Def && Def->isAbstract()); + } return INTEROP_RETURN(false); } @@ -845,12 +964,16 @@ TypeRef GetEnumConstantType(ConstDeclRef DRef) { return INTEROP_RETURN(nullptr); } -size_t GetEnumConstantValue(ConstDeclRef DRef) { +int64_t GetEnumConstantValue(ConstDeclRef DRef) { INTEROP_TRACE(DRef); const auto* D = unwrap(DRef); if (const auto* ECD = llvm::dyn_cast_or_null(D)) { const llvm::APSInt& Val = ECD->getInitVal(); - return INTEROP_RETURN(Val.getExtValue()); + if (Val.isRepresentableByInt64()) + return INTEROP_RETURN(Val.getExtValue()); + // Do not round-trip through a string and std::stoul: unsigned long is + // 32 bit on LLP64/ILP32 and throws out_of_range for these values. + return INTEROP_RETURN((int64_t)Val.getZExtValue()); } return INTEROP_RETURN(0); } @@ -1029,6 +1152,27 @@ static const clang::Decl* GetUnderlyingScopeImpl(const clang::Decl* D) { return D->getCanonicalDecl(); } +// If D is a UsingShadowDecl whose target is a function-like decl, +// return that target; otherwise return D unchanged. The using-shadow +// is the only carrier of the "effective access" introduced into the +// derived class, so callers that need access info should consult D +// before unwrapping. +static clang::Decl* UnwrapUsingShadowToFunction(clang::Decl* D) { + if (auto* USD = dyn_cast_or_null(D)) + if (auto* Target = USD->getTargetDecl()) + if (isa(Target) || isa(Target)) + return Target; + return D; +} + +static const clang::Decl* UnwrapUsingShadowToFunction(const clang::Decl* D) { + if (const auto* USD = dyn_cast_or_null(D)) + if (const auto* Target = USD->getTargetDecl()) + if (isa(Target) || isa(Target)) + return Target; + return D; +} + DeclRef GetUnderlyingScope(ConstDeclRef DRef) { INTEROP_TRACE(DRef); if (!DRef) @@ -1189,11 +1333,9 @@ DeclRef GetNamed(const std::string& name, ConstDeclRef parent /*= nullptr*/) { auto* D = unwrap(GetUnderlyingScope(parent)); Within = llvm::dyn_cast(D); } -#ifdef CPPINTEROP_USE_CLING + compat::SynthesizingCodeRAII RAII(&getInterp()); if (Within) Within->getPrimaryContext()->buildLookup(); -#endif - compat::SynthesizingCodeRAII RAII(&getInterp()); // Fast path: qualified lookup. Cheap, no DRef-chain allocation, and // resolves every name not brought into `Within` via a using-directive. @@ -1203,6 +1345,34 @@ DeclRef GetNamed(const std::string& name, ConstDeclRef parent /*= nullptr*/) { if (ND && ND != (clang::NamedDecl*)-1) return INTEROP_RETURN(ND->getCanonicalDecl()); + // Redeclaration-style lookup stops at the class itself; [class.qual] + // member lookup also searches the bases. Retry accordingly. + // FIXME: GetNamed should only return decls that are named and reachable + // within the provided decl context — the same contract question as the + // LookupUnqualified FIXME above: expose distinct lookup routes so callers + // pick the semantics they want, instead of GetNamed approximating both. + if (!ND && Within) { + if (auto* RD = llvm::dyn_cast(Within)) { + auto* Def = llvm::dyn_cast_or_null( + unwrap(GetOrForceDefinition(DeclRef(RD)))); + if (!Def) + return INTEROP_RETURN(nullptr); + auto& S = getSema(); + clang::DeclarationName DName = &S.Context.Idents.get(name); + clang::LookupResult R(S, DName, clang::SourceLocation(), + clang::Sema::LookupOrdinaryName, + RedeclarationKind::NotForRedeclaration); + R.suppressDiagnostics(); + S.LookupQualifiedName(R, Def); + if (R.empty()) + return INTEROP_RETURN(nullptr); + R.resolveKind(); + if (!R.isSingleResult()) + return INTEROP_RETURN(nullptr); + return INTEROP_RETURN(R.getFoundDecl()->getCanonicalDecl()); + } + } + // Slow path: only when qualified lookup missed AND `Within` is a // namespace whose enclosing chain carries at least one using-directive // (the only reason qualified-vs-unqualified disagree at namespace @@ -1228,6 +1398,11 @@ DeclRef GetParentScope(ConstDeclRef DRef) { if (llvm::isa_and_nonnull(D)) { return INTEROP_RETURN(nullptr); } + // For a method handle that is a using-shadow, the parent must be the + // target's declaring class, not the class holding the using-declaration: + // callers (e.g. CPyCppyy) use it to adjust `this` to the declaring base's + // subobject, and the wrapper calls the target through that base type. + D = UnwrapUsingShadowToFunction(D); auto* ParentDC = D->getDeclContext(); if (!ParentDC) @@ -1349,6 +1524,8 @@ int64_t GetBaseClassOffset(ConstDeclRef derived, ConstDeclRef base) { assert(derived || base); + compat::SynthesizingCodeRAII RAII(&getInterp()); + const auto* DD = unwrap(derived); const auto* BD = unwrap(base); if (!isa(DD) || !isa(BD)) @@ -1416,7 +1593,10 @@ static void GetClassDecls(ConstDeclRef DRef, std::vector& methods) { auto* CUSD = dyn_cast(DI); if (!CUSD) { - methods.push_back(MD); + // Push the using-shadow rather than the target so that the + // effective access (USD->getAccess()) reachable from the + // introducing class is preserved for downstream consumers. + methods.push_back(USD); continue; } @@ -1536,6 +1716,7 @@ std::vector GetFunctionsUsingName(ConstDeclRef DRef, clang::LookupResult R(S, DName, SourceLocation(), Sema::LookupOrdinaryName, RedeclarationKind::ForVisibleRedeclaration); + compat::SynthesizingCodeRAII RAII(&getInterp()); CppInternal::utils::Lookup::Named(&S, R, Decl::castToDeclContext(D)); if (R.empty()) @@ -1557,7 +1738,7 @@ std::vector GetFunctionsUsingName(ConstDeclRef DRef, TypeRef GetFunctionReturnType(ConstFuncRef func) { INTEROP_TRACE(func); - const auto* D = unwrap(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); if (const auto* FD = llvm::dyn_cast_or_null(D)) { QualType Type = FD->getReturnType(); if (Type->isUndeducedAutoType()) { @@ -1637,98 +1818,261 @@ bool IsFunctionProtoType(ConstTypeRef TyRef) { return INTEROP_RETURN(llvm::isa_and_nonnull(T)); } -static AllocType handleNew(const clang::CXXNewExpr* CNE) { - if (CNE->getNumPlacementArgs() > 0) - return AllocType::None; - if (CNE->isArray()) - return AllocType::NewArr; - return AllocType::New; -} +static std::optional +AnalyzeAllocType(const clang::FunctionDecl* Fn, + std::unordered_map>& visitedFuncs); -static AllocType AnalyzeAllocType(const clang::FunctionDecl* Fn); +namespace { +struct AllocationTraverser : RecursiveASTVisitor { + std::unordered_map> varMap; + std::unordered_map>& + visitedFuncs; + // Result var keeps the combination all possible values of previous return + // statements + std::optional result; + // A map every branch writes the previous value of overwriten variables + std::unordered_map>* undoLog = + nullptr; + + AllocationTraverser(std::unordered_map>& cache) + : visitedFuncs(cache) {} + + // Do not analyze lambda functions' bodies. + bool TraverseLambdaExpr(clang::LambdaExpr*) { return true; } + + // Do not analyze inside of TagDecls(structs/unions/class) + bool TraverseDecl(clang::Decl* D) { + if (llvm::isa_and_nonnull(D)) + return true; + return RecursiveASTVisitor::TraverseDecl(D); + } -static AllocType handleCall(const clang::CallExpr* CE) { - if (const auto* FD = CE->getDirectCallee()) { - if (FD->getBuiltinID() == Builtin::ID::BImalloc) - return AllocType::Malloc; - return AnalyzeAllocType(FD); - } - // Function pointer calle - return AllocType::Unknown; -} - -static AllocType -handleExpr(const clang::Expr* expr, - std::unordered_map& varMap) { - const clang::Expr* finExpr = expr->IgnoreParenCasts(); - // Case: return new __type__ - if (const auto* CNE = dyn_cast(finExpr)) - return handleNew(CNE); - - // Case: returns a variable - if (const auto* DRE = dyn_cast(finExpr)) { - if (const auto* VD = dyn_cast(DRE->getDecl())) { - auto it = varMap.find(VD); - if (it != varMap.end()) - return it->second; + std::optional join(std::optional a, + std::optional b) { + if (a == AllocType::Null) + return b; + + if (b == AllocType::Null) + return a; + + if (a == b) + return a; + return AllocType::Unknown; + } + void undoOnVarMap() { + for (auto& [VD, oldVal] : *undoLog) + varMap[VD] = oldVal; + } + + bool TraverseIfStmt(clang::IfStmt* IS) { + TraverseStmt(IS->getConditionVariableDeclStmt()); + TraverseStmt(IS->getCond()); + auto* undoLogCopy = undoLog; + std::unordered_map> + undoLogThen; + undoLog = &undoLogThen; + TraverseStmt(IS->getThen()); + auto* elseBranch = IS->getElse(); + // There is no else + if (!elseBranch) { + for (auto& [VD, val] : undoLogThen) { + // Overwrite varMap with join of overwriten and previous value + varMap[VD] = join(val, varMap[VD]); + // If branch is nested, inform upper branch about your changes + if (undoLogCopy) + undoLogCopy->try_emplace(VD, val); + } + undoLog = undoLogCopy; + return true; } - // FIXME: BindingDecl, NonTypeTemplateParmDecl are not handled - return AllocType::None; + + // Save overwriten values in if branch to join + std::unordered_map> + valuesInIfBranch; + for (auto& [VD, val] : undoLogThen) + valuesInIfBranch[VD] = varMap[VD]; + + // Take changes on varMap back before going to else branch + undoOnVarMap(); + + std::unordered_map> + undoLogElse; + undoLog = &undoLogElse; + TraverseStmt(elseBranch); + + for (auto& [VD, val] : undoLogElse) { + auto it2 = valuesInIfBranch.find(VD); + // If var is just changed in else branch + if (it2 == valuesInIfBranch.end()) { + varMap[VD] = join(val, varMap[VD]); + continue; + } + // If var is changed in both + varMap[VD] = join(varMap[VD], it2->second); + } + + for (auto& [VD, val] : valuesInIfBranch) + // If var is changed in both, varMap carries new value from ELSE, if it is + // just changed in THEN, varMap carries old value, so both situation + // yields to same + varMap[VD] = join(val, varMap[VD]); + + // Inform upper branch about changes done for both inner if and else branch + if (undoLogCopy) { + for (auto& [VD, val] : undoLogThen) + undoLogCopy->try_emplace(VD, val); + for (auto& [VD, val] : undoLogElse) + undoLogCopy->try_emplace(VD, val); + } + + undoLog = undoLogCopy; + return true; } - // Case: malloc or another func call - if (const auto* CE = dyn_cast(finExpr)) - return handleCall(CE); - return AllocType::None; -} + bool VisitVarDecl(VarDecl* VD) { + Expr* expr = VD->getInit(); + if (expr) + varMap[VD] = handleExpr(expr); + else + varMap[VD] = AllocType::None; + return true; + } -static std::vector -getAllRetStmt(const clang::CompoundStmt* CS) { - struct RetStmtVisitor : RecursiveASTVisitor { - std::vector vec; - bool VisitReturnStmt(ReturnStmt* RS) { - vec.push_back(RS); + bool VisitBinaryOperator(clang::BinaryOperator* BO) { + Expr* LHS = BO->getLHS(); + LHS = LHS->IgnoreParenCasts(); + auto* DRE = dyn_cast(LHS); + if (!DRE) + return true; + + auto* VD = dyn_cast(DRE->getDecl()); + // FIXME: BindingDecls are not handled + if (!VD) + return true; + + if (BO->getOpcode() == BO_Assign) { + if (undoLog) + undoLog->try_emplace(VD, varMap[VD]); + Expr* RHS = BO->getRHS(); + varMap[VD] = handleExpr(RHS); return true; } - }; - RetStmtVisitor Visitor; - Visitor.TraverseStmt(const_cast(CS)); - return Visitor.vec; -} - -static std::unordered_map -getAllVarDecl(const clang::CompoundStmt* CS) { - struct VarVisitor : RecursiveASTVisitor { - std::unordered_map varMap; - bool VisitVarDecl(VarDecl* VD) { - Expr* expr = VD->getInit(); - if (expr) - varMap[VD] = handleExpr(expr, varMap); - else - varMap[VD] = AllocType::None; + if (BO->isCompoundAssignmentOp()) { + if (undoLog) + undoLog->try_emplace(VD, varMap[VD]); + varMap[VD] = AllocType::Unknown; return true; } + return true; + } - bool VisitBinaryOperator(clang::BinaryOperator* BO) { - if (BO->getOpcode() != BO_Assign) - return true; - Expr* LHS = BO->getLHS(); - LHS = LHS->IgnoreParenCasts(); - if (auto* DRE = dyn_cast(LHS)) { - if (auto* VD = dyn_cast(DRE->getDecl())) { - Expr* RHS = BO->getRHS(); - varMap[VD] = handleExpr(RHS, varMap); + bool VisitReturnStmt(ReturnStmt* RS) { + const clang::Expr* retExpr = RS->getRetValue(); + // Tmp is current return statement's AllocType value, result is combination + // of previous return statements + std::optional tmp = handleExpr(retExpr); + if (!tmp.has_value()) + return true; + if (!result.has_value()) { + result = tmp; + return true; + } + // If function's allocation behaviour differs between different cases, + // analyzer returns unknown. + result = join(result, tmp); + if (result == AllocType::Unknown) + return false; + return true; + } + + std::optional handleCall(const clang::CallExpr* CE) { + if (const auto* FD = CE->getDirectCallee()) { + if (FD->getBuiltinID() == Builtin::ID::BImalloc) + return AllocType::Malloc; + if (FD->getBuiltinID() == Builtin::ID::BI__builtin_operator_new) + return AllocType::OperatorNew; + // Detects operator new/new[]/delete/delete[] + if (FD->isReplaceableGlobalAllocationFunction()) { + switch (FD->getOverloadedOperator()) { + case OO_New: + return AllocType::OperatorNew; + case OO_Array_New: + return AllocType::OperatorNewArr; + // Unreachable code + default: + break; // OO_Delete/OO_Array_Delete } } - return true; + auto it = visitedFuncs.find(FD); + if (it == visitedFuncs.end()) { + visitedFuncs[FD] = std::nullopt; + return AnalyzeAllocType(FD, visitedFuncs); + } + return it->second; } - }; - VarVisitor Visitor; - Visitor.TraverseStmt(const_cast(CS)); - return Visitor.varMap; -} + // Function pointer calle + return AllocType::Unknown; + } -static AllocType AnalyzeAllocType(const clang::FunctionDecl* Fn) { + static AllocType handleNew(const clang::CXXNewExpr* CNE) { + if (CNE->getNumPlacementArgs() > 0) { + /* + Non-allocating placement allocation functions + void* operator new ( std::size_t count, void* ptr ); + void* operator new[]( std::size_t count, void* ptr ); + */ + const clang::FunctionDecl* OpNew = CNE->getOperatorNew(); + if (OpNew && OpNew->isReservedGlobalPlacementOperator()) + return AllocType::None; + } + if (CNE->isArray()) + return AllocType::NewArr; + return AllocType::New; + } + + std::optional handleExpr(const clang::Expr* expr) { + const clang::Expr* finExpr = expr->IgnoreParenCasts(); + // Case: return new __type__ + if (const auto* CNE = dyn_cast(finExpr)) + return handleNew(CNE); + + // Case: returns a variable + if (const auto* DRE = dyn_cast(finExpr)) { + if (const auto* VD = dyn_cast(DRE->getDecl())) { + auto it = varMap.find(VD); + if (it != varMap.end()) + return it->second; + } + // FIXME: BindingDecl, NonTypeTemplateParmDecl are not handled + if (isa(DRE->getDecl()) || + isa(DRE->getDecl())) + return AllocType::Unknown; + return AllocType::None; + } + + // Case: malloc or another func call + if (const auto* CE = dyn_cast(finExpr)) + return handleCall(CE); + + // Case: NULL, nullptr or (int*)(__integerLiteral__) + const clang::Expr* parExpr = expr->IgnoreParens(); + while (const auto* CE = dyn_cast(parExpr)) { + if (CE->getCastKind() == CK_NullToPointer) + return AllocType::Null; + parExpr = CE->getSubExpr(); + } + + return AllocType::None; + } +}; +} // namespace + +static std::optional +AnalyzeAllocType(const clang::FunctionDecl* Fn, + std::unordered_map>& visitedFuncs) { const clang::QualType QT = Fn->getReturnType(); if (!QT->isPointerType()) return AllocType::None; @@ -1739,22 +2083,13 @@ static AllocType AnalyzeAllocType(const clang::FunctionDecl* Fn) { // FIXME:: try catch blocks are not CompoundStmt, only edge case if (!CmpStmt) return AllocType::Unknown; - std::unordered_map varMap = getAllVarDecl(CmpStmt); - std::vector allRetStmt = getAllRetStmt(CmpStmt); - std::optional res; - for (const ReturnStmt* retStmt : allRetStmt) { - const clang::Expr* retExpr = retStmt->getRetValue(); - if (retExpr == nullptr) - continue; - AllocType tmp = handleExpr(retExpr, varMap); - if (!res.has_value()) - res = tmp; - // If function's allocation behaviour differs between different cases, - // analyzer returns unknown. - else if (*res != tmp) - return AllocType::Unknown; - } - return res.value_or(AllocType::None); + AllocationTraverser Traverser(visitedFuncs); + for (auto* parm : Fn->parameters()) + Traverser.VisitVarDecl(parm); + Traverser.TraverseStmt(const_cast(CmpStmt)); + auto res = Traverser.result; + visitedFuncs[Fn] = res; + return res; } AllocType GetAllocType(ConstFuncRef Fn) { @@ -1762,12 +2097,241 @@ AllocType GetAllocType(ConstFuncRef Fn) { if (Fn) { const auto* D = unwrap(Fn); if (const auto* FD = dyn_cast(D)) { - return INTEROP_RETURN(AnalyzeAllocType(FD)); + std::unordered_map> + visitedFuncs; + visitedFuncs[FD] = std::nullopt; + return INTEROP_RETURN( + AnalyzeAllocType(FD, visitedFuncs).value_or(AllocType::None)); } } return INTEROP_RETURN(AllocType::None); } +static bool AnalyzeDeallocType( + const clang::FunctionDecl* FD, std::vector& valPerParam, + std::unordered_map>>& visitedFuncs); +namespace { +struct DeallocationTraverser : RecursiveASTVisitor { + std::unordered_map aliasMap; + std::set overwritenParms; + std::vector& valPerParam; + std::unordered_map>>& visitedFuncs; + DeallocationTraverser( + std::vector& v, + std::unordered_map>>& cache) + : valPerParam(v), visitedFuncs(cache) {} + + // Do not analyze lambda functions' bodies. + bool TraverseLambdaExpr(clang::LambdaExpr*) { return true; } + + // Do not analyze inside of TagDecls(structs/unions/class) + bool TraverseDecl(clang::Decl* D) { + if (llvm::isa_and_nonnull(D)) + return true; + return RecursiveASTVisitor::TraverseDecl(D); + } + + // recursiveVec is VPP of callee + void joinVectors(std::vector& recursiveVec, + clang::CallExpr* CE) { + for (unsigned i = 0; i < recursiveVec.size() && i < CE->getNumArgs(); i++) { + if (recursiveVec[i] == DeallocType::None) + continue; + const clang::ParmVarDecl* PVD = resolveParam(CE->getArg(i)); + if (!PVD) + continue; + updateVPP(PVD->getFunctionScopeIndex(), recursiveVec[i]); + } + } + + bool VisitVarDecl(clang::VarDecl* VD) { + if (llvm::isa(VD) || + (!VD->getType()->isPointerType() && !VD->getType()->isRecordType())) + return true; + const clang::Expr* E = VD->getInit(); + updateMap(VD, E); + return true; + } + + bool VisitBinaryOperator(clang::BinaryOperator* BO) { + if (BO->getOpcode() != BO_Assign) + return true; + const auto* VD = resolveExpr(BO->getLHS()); + updateMap(VD, BO->getRHS()); + return true; + } + + bool VisitCXXDeleteExpr(clang::CXXDeleteExpr* CDE) { + const clang::Expr* E = CDE->getArgument(); + const auto* PVR = resolveParam(E); + if (!PVR) + return true; + if (CDE->isArrayForm()) { + updateVPP(PVR->getFunctionScopeIndex(), DeallocType::DeleteArr); + return true; + } + updateVPP(PVR->getFunctionScopeIndex(), DeallocType::Delete); + return true; + } + + bool VisitCallExpr(clang::CallExpr* CE) { + // FIXME: for some operators, arg and parameter match may not match because + // of hidden __this__ parameter + if (llvm::isa(CE)) + return true; + const clang::FunctionDecl* FD = CE->getDirectCallee(); + if (!FD) + return true; + if (FD->getBuiltinID() == Builtin::ID::BIfree) { + handleFree(CE); + return true; + } + + // FIXME: Recursive Case + auto it = visitedFuncs.find(FD); + if (it != visitedFuncs.end()) { + // Function calls itself, or a cycle recursion + if (it->second == std::nullopt) + return true; + joinVectors(*(it->second), CE); + return true; + } + std::vector calleeVec; + visitedFuncs[FD] = std::nullopt; + AnalyzeDeallocType(FD, calleeVec, visitedFuncs); + joinVectors(calleeVec, CE); + return true; + } + + const clang::ParmVarDecl* resolveParam(const clang::Expr* E) { + const auto* VD = resolveExpr(E); + if (!VD) + return nullptr; + if (const auto* PVD = dyn_cast(VD)) { + if (overwritenParms.count(PVD)) + return nullptr; + return PVD; + } + auto it = aliasMap.find(VD); + if (it != aliasMap.end() && it->second != nullptr) { + return it->second; + } + return nullptr; + } + + void updateMap(const clang::VarDecl* VD, const clang::Expr* E) { + if (!VD) + return; + if (!VD->getType()->isPointerType() && !VD->getType()->isRecordType()) + return; + if (const auto* PVD = dyn_cast(VD)) { + overwritenParms.insert(PVD); + return; + } + const auto* rhsVD = resolveExpr(E); + + if (!rhsVD) { + aliasMap[VD] = nullptr; + return; + } + + if (const auto* rhsPVD = dyn_cast(rhsVD)) { + if (overwritenParms.count(rhsPVD)) { + aliasMap[VD] = nullptr; + return; + } + aliasMap[VD] = rhsPVD; + // FIXME: does not handle situation where one param is assigned to + // another param + return; + } + + auto it = aliasMap.find(rhsVD); + if (it != aliasMap.end()) { + aliasMap[VD] = it->second; + return; + } + aliasMap[VD] = nullptr; + } + + void updateVPP(unsigned index, DeallocType DT) { + if (valPerParam[index] == DeallocType::Unknown) + return; + // If new value from callee is Opaque, it is best to make parameter Unknown + // since we can not know what is going inside no-body functions + if ((valPerParam[index] != DeallocType::None && valPerParam[index] != DT) || + DT == DeallocType::Opaque) { + valPerParam[index] = DeallocType::Unknown; + return; + } + valPerParam[index] = DT; + } + + void handleFree(clang::CallExpr* CE) { + const clang::Expr* paramExpr = CE->getArg(0); + const auto* PVR = resolveParam(paramExpr); + if (!PVR) + return; + updateVPP(PVR->getFunctionScopeIndex(), DeallocType::Free); + } + + const clang::VarDecl* resolveExpr(const clang::Expr* E) { + if (!E) + return nullptr; + E = E->IgnoreParenCasts(); + const auto* DRE = dyn_cast(E); + if (!DRE) + return nullptr; + const auto* VD = dyn_cast(DRE->getDecl()); + return VD; + } +}; +} // namespace + +static bool AnalyzeDeallocType( + const clang::FunctionDecl* FD, std::vector& valPerParam, + std::unordered_map>>& visitedFuncs) { + const unsigned int numParam = FD->getNumParams(); + const auto* S = FD->getBody(); + if (!S) { + valPerParam.assign(numParam, DeallocType::Opaque); + visitedFuncs[FD] = valPerParam; + return false; + } + const auto* CmpStmt = dyn_cast(S); + if (!CmpStmt) { + // FIXME: try catch block are not handled + valPerParam.assign(numParam, DeallocType::Opaque); + visitedFuncs[FD] = valPerParam; + return false; + } + valPerParam.assign(numParam, DeallocType::None); + DeallocationTraverser Traverser(valPerParam, visitedFuncs); + Traverser.TraverseStmt(const_cast(CmpStmt)); + visitedFuncs[FD] = valPerParam; + return true; +} + +bool GetDeallocType(ConstFuncRef Fn, std::vector& valPerParam) { + INTEROP_TRACE(Fn, INTEROP_OUT(valPerParam)); + if (!Fn) + return INTEROP_RETURN(false); + + const auto* D = unwrap(Fn); + const auto* FD = dyn_cast(D); + if (!FD) + return INTEROP_RETURN(false); + std::unordered_map>> + visitedFuncs; + visitedFuncs[FD] = std::nullopt; + return INTEROP_RETURN(AnalyzeDeallocType(FD, valPerParam, visitedFuncs)); +} + void GetFnTypeSignature(ConstTypeRef fn_type, std::vector& sig) { INTEROP_TRACE(fn_type, INTEROP_OUT(sig)); QualType QT = QualType::getFromOpaquePtr(fn_type.data); @@ -1787,7 +2351,7 @@ void GetFnTypeSignature(ConstTypeRef fn_type, std::vector& sig) { // exclude it, which is what callers introspecting the argument list want. size_t GetFunctionNumArgs(ConstFuncRef func) { INTEROP_TRACE(func); - const auto* D = unwrap(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); if (const auto* FD = llvm::dyn_cast_or_null(D)) return INTEROP_RETURN(FD->getNumNonObjectParams()); @@ -1799,7 +2363,7 @@ size_t GetFunctionNumArgs(ConstFuncRef func) { size_t GetFunctionRequiredArgs(ConstFuncRef func) { INTEROP_TRACE(func); - const auto* D = unwrap(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); if (const auto* FD = llvm::dyn_cast_or_null(D)) return INTEROP_RETURN(FD->getMinRequiredExplicitArguments()); @@ -1812,7 +2376,7 @@ size_t GetFunctionRequiredArgs(ConstFuncRef func) { TypeRef GetFunctionArgType(ConstFuncRef func, size_t iarg) { INTEROP_TRACE(func, iarg); - const auto* D = unwrap(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); if (const auto* FTD = llvm::dyn_cast_or_null(D)) D = FTD->getTemplatedDecl(); @@ -1838,7 +2402,7 @@ std::string GetFunctionSignature(ConstFuncRef func) { if (!func) return INTEROP_RETURN(""); - const auto* D = unwrap(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); const clang::FunctionDecl* FD; if (llvm::dyn_cast(D)) @@ -1883,13 +2447,14 @@ bool IsTemplateInstantiationOrSpecialization(const Decl* D) { bool IsFunctionDeleted(ConstFuncRef function) { INTEROP_TRACE(function); - const auto* FD = cast(unwrap(function)); + const auto* FD = cast( + UnwrapUsingShadowToFunction(unwrap(function))); return INTEROP_RETURN(FD->isDeleted()); } bool IsTemplatedFunction(ConstFuncRef func) { INTEROP_TRACE(func); - const auto* D = unwrap(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); return INTEROP_RETURN(IsTemplatedFunction(D) || IsTemplateInstantiationOrSpecialization(D)); } @@ -1905,6 +2470,7 @@ bool ExistsFunctionTemplate(const std::string& name, ConstDeclRef parent) { Within = llvm::dyn_cast(D); } + compat::SynthesizingCodeRAII RAII(&getInterp()); auto* ND = CppInternal::utils::Lookup::Named(&getSema(), name, Within); if ((intptr_t)ND == (intptr_t)0) @@ -1914,8 +2480,24 @@ bool ExistsFunctionTemplate(const std::string& name, ConstDeclRef parent) { return INTEROP_RETURN(IsTemplatedFunction(ND) || IsTemplateInstantiationOrSpecialization(ND)); - // FIXME: Cycle through the Decls and check if there is a templated function - return INTEROP_RETURN(true); + // The name is ambiguous, i.e. an overload set: cycle through the found + // decls and check if any of them is a templated function. Blindly + // returning true here would present any function with two or more + // non-template overloads as a template. + auto& S = getSema(); + auto& Ctx = getASTContext(); + clang::LookupResult R(S, &Ctx.Idents.get(name), SourceLocation(), + Sema::LookupOrdinaryName, + RedeclarationKind::ForVisibleRedeclaration); + CppInternal::utils::Lookup::Named(&S, R, Within); + for (const NamedDecl* Found : R) { + const Decl* D = Found; + if (const auto* USD = llvm::dyn_cast(Found)) + D = USD->getTargetDecl(); + if (IsTemplatedFunction(D) || IsTemplateInstantiationOrSpecialization(D)) + return INTEROP_RETURN(true); + } + return INTEROP_RETURN(false); } // Looks up all constructors in the current DeclContext @@ -1926,6 +2508,9 @@ void LookupConstructors(const std::string& name, ConstDeclRef parent, auto* D = const_cast(unwrap(parent)); if (auto* CXXRD = llvm::dyn_cast_or_null(D)) { + // Both calls below declare implicit members lazily and can deserialize + // decls from an AST file, which must happen within a transaction. + compat::SynthesizingCodeRAII RAII(&getInterp()); getSema().ForceDeclarationOfImplicitMembers(CXXRD); DeclContextLookupResult Result = getSema().LookupConstructors(CXXRD); // Obtaining all constructors when we intend to lookup a method under a @@ -1954,6 +2539,8 @@ bool GetClassTemplatedMethods(const std::string& name, ConstDeclRef parent, clang::LookupResult R(S, DName, SourceLocation(), Sema::LookupOrdinaryName, RedeclarationKind::ForVisibleRedeclaration); auto* DC = clang::Decl::castToDeclContext(DU); + + compat::SynthesizingCodeRAII RAII(&getInterp()); CppInternal::utils::Lookup::Named(&S, R, DC); if (R.getResultKind() == clang_LookupResult_Not_Found && funcs.empty()) @@ -2095,6 +2682,13 @@ BestOverloadFunctionMatch(const std::vector& candidates, // the provided AccessSpecifier. bool CheckMethodAccess(ConstFuncRef method, AccessSpecifier AS) { const auto* D = unwrap(method); + // Must NOT unwrap here: the using-shadow is the only carrier of the effective + // access (e.g. `public` in the derived class), while the target only knows + // its base access (e.g. `protected`). + if (const auto* USD = llvm::dyn_cast_or_null(D)) { + if (llvm::isa_and_nonnull(USD->getTargetDecl())) + return USD->getAccess() == AS; + } if (const auto* CXXMD = llvm::dyn_cast_or_null(D)) { return CXXMD->getAccess() == AS; } @@ -2104,7 +2698,7 @@ bool CheckMethodAccess(ConstFuncRef method, AccessSpecifier AS) { bool IsMethod(ConstFuncRef method) { INTEROP_TRACE(method); - const auto* D = unwrap(method); + const auto* D = UnwrapUsingShadowToFunction(unwrap(method)); if (const auto* FTD = dyn_cast_or_null(D)) D = FTD->getTemplatedDecl(); return INTEROP_RETURN(dyn_cast_or_null(D)); @@ -2128,7 +2722,7 @@ bool IsPrivateMethod(ConstFuncRef method) { bool IsConstructor(ConstFuncRef method) { INTEROP_TRACE(method); - const auto* D = unwrap(method); + const auto* D = UnwrapUsingShadowToFunction(unwrap(method)); if (const auto* FTD = dyn_cast(D)) return INTEROP_RETURN(IsConstructor(FTD->getTemplatedDecl())); return INTEROP_RETURN(llvm::isa_and_nonnull(D)); @@ -2136,13 +2730,13 @@ bool IsConstructor(ConstFuncRef method) { bool IsDestructor(ConstFuncRef method) { INTEROP_TRACE(method); - const auto* D = unwrap(method); + const auto* D = UnwrapUsingShadowToFunction(unwrap(method)); return INTEROP_RETURN(llvm::isa_and_nonnull(D)); } bool IsStaticMethod(ConstFuncRef method) { INTEROP_TRACE(method); - const auto* D = unwrap(method); + const auto* D = UnwrapUsingShadowToFunction(unwrap(method)); if (const auto* FTD = llvm::dyn_cast_or_null(D)) D = FTD->getTemplatedDecl(); @@ -2158,7 +2752,7 @@ bool IsExplicit(ConstFuncRef method) { if (!method) return INTEROP_RETURN(false); - const auto* D = unwrap(method); + const auto* D = UnwrapUsingShadowToFunction(unwrap(method)); if (const auto* FTD = llvm::dyn_cast_or_null(D)) D = FTD->getTemplatedDecl(); @@ -2215,7 +2809,7 @@ static void* GetFunctionAddress(const FunctionDecl* FD) { void* GetFunctionAddress(FuncRef method) { INTEROP_TRACE(method); - auto* D = unwrap(method); + auto* D = UnwrapUsingShadowToFunction(unwrap(method)); if (auto* FD = llvm::dyn_cast_or_null(D)) { if ((IsTemplateInstantiationOrSpecialization(FD) || FD->getTemplatedKind() == FunctionDecl::TK_MemberSpecialization) && @@ -2231,7 +2825,7 @@ void* GetFunctionAddress(FuncRef method) { bool IsVirtualMethod(ConstFuncRef method) { INTEROP_TRACE(method); - const auto* D = unwrap(method); + const auto* D = UnwrapUsingShadowToFunction(unwrap(method)); if (const auto* CXXMD = llvm::dyn_cast_or_null(D)) { return INTEROP_RETURN(CXXMD->isVirtual()); } @@ -2636,6 +3230,7 @@ DeclRef LookupDatamember(const std::string& name, ConstDeclRef parent) { Within = llvm::dyn_cast(D); } + compat::SynthesizingCodeRAII RAII(&getInterp()); auto* ND = CppInternal::utils::Lookup::Named(&getSema(), name, Within); if (ND && ND != (clang::NamedDecl*)-1) { if (llvm::isa_and_nonnull(ND)) { @@ -2662,6 +3257,16 @@ TypeRef GetVariableType(ConstDeclRef var) { if (const auto* DD = llvm::dyn_cast_or_null(D)) { QualType QT = DD->getType(); + // For an array of unknown bound (e.g. `extern std::string arr[];`), + // prefer the defining declaration's complete type, which carries the + // actual dimension. + if (QT->isIncompleteArrayType()) { + if (const auto* VD = llvm::dyn_cast(DD)) { + if (const VarDecl* Def = VD->getDefinition()) + QT = Def->getType(); + } + } + // Check if the TyRef is a typedef TyRef if (QT->isTypedefNameType()) { return INTEROP_RETURN(QT.getAsOpaquePtr()); @@ -2686,6 +3291,7 @@ intptr_t GetVariableOffset(compat::Interpreter& I, Decl* D, auto& C = I.getSema().getASTContext(); if (auto* FD = llvm::dyn_cast(D)) { + compat::SynthesizingCodeRAII RAII(&getInterp()); clang::RecordDecl* FieldParentRecordDecl = FD->getParent(); intptr_t offset = C.toCharUnitsFromBits(C.getFieldOffset(FD)).getQuantity(); while (FieldParentRecordDecl->isAnonymousStructOrUnion()) { @@ -2757,9 +3363,21 @@ intptr_t GetVariableOffset(compat::Interpreter& I, Decl* D, address = I.getAddressOfGlobal(GD); if (!address) { if (!VD->hasInit()) { - compat::SynthesizingCodeRAII RAII(&getInterp()); - getSema().InstantiateVariableDefinition(SourceLocation(), VD); - VD = VD->getDefinition(); + // The initializer feeding the constexpr fast path below may live on + // an already-parsed out-of-line definition (a non-template class's + // static data member, e.g. std::partial_ordering::less): prefer it, + // with no Sema work at all. Only a variable instantiated from a + // template can need Sema::InstantiateVariableDefinition — and + // without an instantiation pattern that call dereferences a null + // VarDecl in release builds. + if (VarDecl* Def = VD->getDefinition()) { + VD = Def; + } else if (VD->getTemplateInstantiationPattern()) { + compat::SynthesizingCodeRAII RAII(&getInterp()); + getSema().InstantiateVariableDefinition(SourceLocation(), VD); + if (VarDecl* Inst = VD->getDefinition()) + VD = Inst; + } } if (VD->hasInit() && (VD->isConstexpr() || VD->getType().isConstQualified())) { @@ -2772,7 +3390,14 @@ intptr_t GetVariableOffset(compat::Interpreter& I, Decl* D, } if (!address) { auto Linkage = C.GetGVALinkageForVariable(VD); - if (isDiscardableGVALinkage(Linkage)) + // Odr-use emission only for discardable-ODR entities (inline/constexpr + // statics) — the class the used-list crash traced to. Internal-linkage + // variables cannot be odr-used from a later PTU (module-local symbol: + // the reference duplicates or misses the entity), and an + // available-externally definition would not be emitted by a mere + // reference; both stay on the stock UsedAttr path. + if (isDiscardableGVALinkage(Linkage) && + (Linkage != GVA_DiscardableODR || !EmitVariableViaOdrUse(I, VD))) ForceCodeGen(VD, I); } auto VDAorErr = compat::getSymbolAddress(I, StringRef(mangledName)); @@ -3367,6 +3992,30 @@ void collect_type_info(const FunctionDecl* FD, QualType& QT, get_type_as_string(QT, type_name, C, Policy); } +bool IsCopyConstructorDeleted(QualType QT) { + CXXRecordDecl* RD = QT->getAsCXXRecordDecl(); + if (!RD) { + // For types that are not C++ records (such as PODs), we assume that they + // are copyable, ie their copy constructor is not deleted. + return false; + } + + RD = unwrap(GetOrForceDefinition(DeclRef(RD))); + if (!RD) + return false; + + if (RD->hasSimpleCopyConstructor()) + return false; + + for (auto* Ctor : RD->ctors()) + if (Ctor->isCopyConstructor()) + return Ctor->isDeleted(); + + // The return value is somewhat arbitrary: we did not see a deleted copy + // ctor. The user will be told if the generated code doesn't compile. + return false; +} + void make_narg_ctor(const FunctionDecl* FD, const unsigned N, std::ostringstream& typedefbuf, std::ostringstream& callbuf, const std::string& class_name, int indent_level, @@ -3411,7 +4060,18 @@ void make_narg_ctor(const FunctionDecl* FD, const unsigned N, } else if (isPointer) { callbuf << "*(" << type_name.c_str() << "**)args[" << i << "]"; } else { + // By-value construction: Figure out if the type can be + // copy-constructed. This is tricky and cannot be done in a fully + // reliable way, also because std::vector always defines a copy + // constructor, even if the type T is only moveable. As a heuristic, we + // only check if the copy constructor is deleted, or would be if + // implicit. + bool Move = IsCopyConstructorDeleted(QT); + if (Move) + callbuf << "static_cast<" << type_name << "&&>("; callbuf << "*(" << type_name.c_str() << "*)args[" << i << "]"; + if (Move) + callbuf << ")"; } } callbuf << ")"; @@ -3591,17 +4251,11 @@ void make_narg_call(const FunctionDecl* FD, const std::string& return_type, << type_name.c_str() << "*)args[" << i << "]"; } else if (isPointer) { callbuf << "*(" << type_name.c_str() << "**)args[" << i << "]"; - } else if (rtdecl && - (rtdecl->hasTrivialCopyConstructor() && - !rtdecl->hasSimpleCopyConstructor()) && - rtdecl->hasMoveConstructor()) { + } else if (rtdecl && IsCopyConstructorDeleted(QT)) { // By-value construction; this may either copy or move, but there is no // information here in terms of intent. Thus, simply assume that the // intent is to move if there is no viable copy constructor (ie. if the - // code would otherwise fail to even compile). There does not appear to be - // a simple way of determining whether a viable copy constructor exists, - // so check for the most common case: the trivial one, but not uniquely - // available, while there is a move constructor. + // code would otherwise fail to even compile). // Move construction as needed for classes (note that this is // implicit). Emit `std::move`'s expansion directly rather than the @@ -3655,7 +4309,10 @@ void make_narg_ctor_with_return(const FunctionDecl* FD, const unsigned N, callbuf << "if (nary > 1) {\n"; indent(callbuf, indent_level); callbuf << "(*(" << class_name << "**)ret) = "; - callbuf << "(is_arena) ? new (*(" << class_name << "**)ret) "; + // Use ::new to construct in the arena: a class-scope operator new + // (e.g. a custom allocator with no matching placement form) would + // otherwise hide the global placement operator new. + callbuf << "(is_arena) ? ::new (*(" << class_name << "**)ret) "; make_narg_ctor(FD, N, typedefbuf, callbuf, class_name, indent_level, true); @@ -3679,7 +4336,8 @@ void make_narg_ctor_with_return(const FunctionDecl* FD, const unsigned N, // : new ClassName(args...); indent(callbuf, indent_level); callbuf << "(*(" << class_name << "**)ret) = "; - callbuf << "(is_arena) ? new (*(" << class_name << "**)ret) "; + // ::new for the same reason as in the array branch above. + callbuf << "(is_arena) ? ::new (*(" << class_name << "**)ret) "; make_narg_ctor(FD, N, typedefbuf, callbuf, class_name, indent_level); callbuf << ": new "; @@ -3770,7 +4428,9 @@ void make_narg_call_with_return(compat::Interpreter& I, const FunctionDecl* FD, // Write the placement part of the placement new. // indent(callbuf, indent_level); - callbuf << "new (ret) "; + // ::new so that a class-scope operator new cannot hide the global + // placement form used to construct the return value in `ret`. + callbuf << "::new (ret) "; // // Write the TyRef part of the placement new. // @@ -4258,7 +4918,8 @@ int get_wrapper_code(compat::Interpreter& I, const FunctionDecl* FD, } JitCall::GenericCall make_wrapper(compat::Interpreter& I, - const FunctionDecl* FD) { + const FunctionDecl* FD, + bool relaxAccessControl = false) { auto& WrapperStore = getInterpInfo(&I).WrapperStore; auto R = WrapperStore.find(FD); @@ -4296,6 +4957,12 @@ JitCall::GenericCall make_wrapper(compat::Interpreter& I, // We should be able to call private default constructors. if (auto Ctor = dyn_cast(FD)) withAccessControl = !Ctor->isDefaultConstructor(); + // Members introduced into a derived class with a public using-declaration + // are reachable through the derived class, but the generated wrapper still + // calls the target through its original (e.g. protected) qualified name. + // Disable access control for this specific case so the wrapper compiles. + if (relaxAccessControl) + withAccessControl = false; void* wrapper = compile_wrapper(I, wrapper_name, wrapper_code, withAccessControl); if (wrapper) { @@ -4507,10 +5174,16 @@ static JitCall::DestructorCall make_dtor_wrapper(compat::Interpreter& interp, CPPINTEROP_API JitCall MakeFunctionCallable(InterpRef I, ConstFuncRef func) { INTEROP_TRACE(I, func); - const auto* D = unwrap(func); - if (!D) + const auto* InputD = unwrap(func); + if (!InputD) return INTEROP_RETURN(JitCall{}); + // If the caller passed a using-shadow, unwrap to the target function but + // remember the fact: the generated wrapper still references the target's + // original (e.g. protected) name, so it needs access control relaxed. + const bool isUsingShadow = isa(InputD); + const auto* D = UnwrapUsingShadowToFunction(InputD); + auto* interp = unwrap(I); // FIXME: Unify with make_wrapper. @@ -4523,14 +5196,16 @@ CPPINTEROP_API JitCall MakeFunctionCallable(InterpRef I, ConstFuncRef func) { } if (const auto* Ctor = dyn_cast(D)) { - if (auto Wrapper = make_wrapper(*interp, cast(D))) + if (auto Wrapper = + make_wrapper(*interp, cast(D), isUsingShadow)) return INTEROP_RETURN(JitCall(JitCall::kConstructorCall, Wrapper, wrap(Ctor))); // FIXME: else error we failed to compile the wrapper. return INTEROP_RETURN(JitCall{}); } - if (auto Wrapper = make_wrapper(*interp, cast(D))) { + if (auto Wrapper = + make_wrapper(*interp, cast(D), isUsingShadow)) { return INTEROP_RETURN(JitCall(JitCall::kGenericCall, Wrapper, wrap(cast(D)))); } @@ -4955,7 +5630,7 @@ class clangSilent { }; } // namespace -int Declare(compat::Interpreter& I, const char* code, bool silent) { +static int Declare(compat::Interpreter& I, const char* code, bool silent) { // Trap diagnostics on both paths: I.declare's rc is 0 even when // Parse recovered from emitted errors, so callers need the trap to // distinguish "parsed cleanly" from "parsed with errors". @@ -5316,7 +5991,10 @@ void GetClassTemplateArgs(ConstDeclRef templ_instance, void GetClassTemplateInstantiationArgs(ConstDeclRef templ_instance, std::vector& args) { INTEROP_TRACE(templ_instance, INTEROP_OUT(args)); - const auto* CTSD = unwrap(templ_instance); + const auto* CTSD = llvm::dyn_cast_or_null( + unwrap(templ_instance)); + if (!CTSD) + return INTEROP_VOID_RETURN(); for (const auto& TA : CTSD->getTemplateInstantiationArgs().asArray()) { switch (TA.getKind()) { default: @@ -5452,7 +6130,7 @@ bool IsTypeDerivedFrom(ConstTypeRef derived, ConstTypeRef base) { std::string GetFunctionArgDefault(ConstFuncRef func, size_t param_index) { INTEROP_TRACE(func, param_index); - const auto* D = unwrap(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); const clang::ParmVarDecl* PI = nullptr; if (const auto* FD = llvm::dyn_cast_or_null(D)) @@ -5495,7 +6173,7 @@ bool IsConstMethod(ConstFuncRef method) { if (!method) return INTEROP_RETURN(false); - const auto* D = unwrap(method); + const auto* D = UnwrapUsingShadowToFunction(unwrap(method)); if (const auto* func = dyn_cast(D)) return INTEROP_RETURN(func->getMethodQualifiers().hasConst()); @@ -5504,7 +6182,7 @@ bool IsConstMethod(ConstFuncRef method) { std::string GetFunctionArgName(ConstFuncRef func, size_t param_index) { INTEROP_TRACE(func, param_index); - const auto* D = unwrap(func); + const auto* D = UnwrapUsingShadowToFunction(unwrap(func)); const clang::ParmVarDecl* PI = nullptr; if (const auto* FD = llvm::dyn_cast_or_null(D)) @@ -5534,7 +6212,7 @@ Operator GetOperatorFromSpelling(const std::string& op) { OperatorArity GetOperatorArity(ConstFuncRef op) { INTEROP_TRACE(op); - const auto* D = unwrap(op); + const auto* D = UnwrapUsingShadowToFunction(unwrap(op)); if (const auto* FD = llvm::dyn_cast(D)) { if (FD->isOverloadedOperator()) { switch (FD->getOverloadedOperator()) { diff --git a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td index c87fb24680e1b..4f4c45e86ea4c 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td +++ b/interpreter/CppInterOp/lib/CppInterOp/CppInterOp.td @@ -556,6 +556,22 @@ template instantiation if necessary.}]; ]; } +def GetOrForceDefinition : CppInterOpAPI { + let Doc = [{Returns the definition of the entity referenced by \p DRef, +forcing it into the AST if only a declaration is currently loaded. Dispatches +on the entity kind using its getDefinition() accessor: a tag (class/struct/ +union/enum) is completed through Sema, which drives on-demand header parsing +and template instantiation; a function has its body instantiated; a variable +resolves to its defining declaration. Returns null when the entity has no +definition available (e.g. a forward declaration whose header is absent) or +has no definition concept.}]; + + let ReturnType = "DeclRef"; + let Args = [ + Arg<"DeclRef", "DRef"> + ]; +} + def Allocate : CppInterOpAPI { let Doc = [{Allocates memory required by an object of a given class \param[in] scope Given class for which to allocate memory for @@ -582,9 +598,13 @@ def Deallocate : CppInterOpAPI { } def IsAbstract : CppInterOpAPI { + let Doc = [{Checks if a class is abstract. Abstractness is a property of the +class definition: if only a declaration is loaded, the definition is first +forced into the AST (see GetOrForceDefinition).}]; + let ReturnType = "bool"; let Args = [ - Arg<"ConstDeclRef", "DRef"> + Arg<"DeclRef", "DRef"> ]; } @@ -772,6 +792,16 @@ def GetAllocType: CppInterOpAPI { ]; } +def GetDeallocType : CppInterOpAPI { + let Doc = "Analyzes function's body to detect memory deallocation behaviour"; + + let ReturnType = "bool"; + let Args = [ + Arg<"ConstFuncRef", "Fn">, + OutArg<"std::vector&", "valPerParam"> + ]; +} + def GetFnTypeSignature : CppInterOpAPI { let Doc = [{Pushed the signature type of the given function type, where the first item is the return type.}]; @@ -1029,10 +1059,11 @@ def GetEnumConstantType : CppInterOpAPI { } def GetEnumConstantValue : CppInterOpAPI { - let Doc = [{Gets the index value (0,1,2, etcetera) of the enum constant -that was passed into this function.}]; + let Doc = [{Gets the value of the enum constant that was passed into this +function, as its 64-bit bit pattern (values above INT64_MAX come back as +their two's complement representation).}]; - let ReturnType = "size_t"; + let ReturnType = "int64_t"; let Args = [ Arg<"ConstDeclRef", "DRef"> ]; diff --git a/interpreter/CppInterOp/lib/CppInterOp/InterpreterInfo.h b/interpreter/CppInterOp/lib/CppInterOp/InterpreterInfo.h index a61e5c7445888..f8869b7671e8a 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/InterpreterInfo.h +++ b/interpreter/CppInterOp/lib/CppInterOp/InterpreterInfo.h @@ -46,6 +46,10 @@ struct InterpreterInfo { // Owns the string arguments passed to clang during creation, since the // interpreter keeps the raw argv pointers for its whole lifetime std::vector ArgvStorage; + // Names the synthesized odr-use anchors (EmitVariableViaOdrUse); + // per-interpreter so a fresh interpreter's AST does not depend on + // sibling interpreters' query history. + unsigned OdrUseCounter = 0; InterpreterInfo(compat::Interpreter* I, bool Owned, std::vector ArgvStrs = {}) @@ -53,7 +57,8 @@ struct InterpreterInfo { InterpreterInfo(InterpreterInfo&& Other) noexcept : Interpreter(Other.Interpreter), isOwned(Other.isOwned), - ArgvStorage(std::move(Other.ArgvStorage)) { + ArgvStorage(std::move(Other.ArgvStorage)), + OdrUseCounter(Other.OdrUseCounter) { Other.Interpreter = nullptr; Other.isOwned = false; } @@ -64,6 +69,7 @@ struct InterpreterInfo { Interpreter = Other.Interpreter; isOwned = Other.isOwned; ArgvStorage = std::move(Other.ArgvStorage); + OdrUseCounter = Other.OdrUseCounter; Other.Interpreter = nullptr; Other.isOwned = false; } diff --git a/interpreter/CppInterOp/lib/CppInterOp/exports.ld b/interpreter/CppInterOp/lib/CppInterOp/exports.ld index 8ed72ec64af1b..a799ee14705aa 100644 --- a/interpreter/CppInterOp/lib/CppInterOp/exports.ld +++ b/interpreter/CppInterOp/lib/CppInterOp/exports.ld @@ -57,3 +57,4 @@ -Wl,--export=_ZNK5clang8QualType11getAsStringERKNS_14PrintingPolicyE -Wl,--export=_ZN5clang11Interpreter6createENSt3__210unique_ptrINS_16CompilerInstanceENS1_14default_deleteIS3_EEEENS2_INS_26IncrementalExecutorBuilderENS4_IS7_EEEE -Wl,--export=_ZNK5clang7TagDecl13getDefinitionEv +-Wl,--export=_ZNK5clang12FunctionDecl9isDefinedERPKS0_b diff --git a/interpreter/CppInterOp/unittests/CppInterOp/EnumReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/EnumReflectionTest.cpp index 0e5d56025ba8d..0696cd71dade4 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/EnumReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/EnumReflectionTest.cpp @@ -237,6 +237,10 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, EnumReflection_GetEnumConstantValue) { MinusTen = -10, MinusNine }; + enum Huge : unsigned long long { + Big = ((unsigned long long)1) << 63, + Max = 0xFFFFFFFFFFFFFFFFULL + }; int a = 10; )"; @@ -250,7 +254,12 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, EnumReflection_GetEnumConstantValue) { EXPECT_EQ(Cpp::GetEnumConstantValue(EnumConstants[4]), 54); EXPECT_EQ(Cpp::GetEnumConstantValue(EnumConstants[5]), -10); EXPECT_EQ(Cpp::GetEnumConstantValue(EnumConstants[6]), -9); - EXPECT_EQ(Cpp::GetEnumConstantValue(Decls[1]), 0); // Checking value of non enum constant + EXPECT_EQ(Cpp::GetEnumConstantValue(Decls[2]), 0); // Checking value of non enum constant + + auto HugeConstants = Cpp::GetEnumConstants(Decls[1]); + EXPECT_EQ(Cpp::GetEnumConstantValue(HugeConstants[0]), + (int64_t)((uint64_t)1 << 63)); + EXPECT_EQ(Cpp::GetEnumConstantValue(HugeConstants[1]), (int64_t)-1); } TYPED_TEST(CPPINTEROP_TEST_MODE, EnumReflection_GetEnums) { diff --git a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp index 9ecb52acdf36c..2de358977073f 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -8,6 +8,7 @@ #include "clang/Sema/Sema.h" #include +#include #include #include "gtest/gtest.h" @@ -163,6 +164,270 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetClassMethods) { EXPECT_EQ(get_method_name(templ_methods2[6]), "inline TT::~TT()"); } +// A method introduced into a derived class with `using Base::name;` in a +// public section must report public access (taken from the using-declaration), +// not the access of the underlying target in the base class. +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_GetClassMethods_UsingShadowAccess) { + std::vector Decls; + std::string code = R"( + class MyBase { + protected: + void foo(int, int) {} + }; + class MyDerived : public MyBase { + public: + using MyBase::foo; // promoted to public + void foo(int) {} + }; + class HiddenDerived : public MyBase { + protected: + using MyBase::foo; // stays protected + }; + )"; + + GetAllTopLevelDecls(code, Decls); + + std::vector derived_methods; + Cpp::GetClassMethods(Decls[1], derived_methods); + + // Find the using-promoted foo (the two-argument overload). + bool found_using_promoted = false; + Cpp::FuncRef using_promoted; + for (auto m : derived_methods) { + if (Cpp::GetName(Cpp::DeclRef{m.data}) != "foo") + continue; + if (Cpp::GetFunctionNumArgs(m) == 2) { + found_using_promoted = true; + using_promoted = m; + EXPECT_TRUE(Cpp::IsPublicMethod(m)); + EXPECT_FALSE(Cpp::IsProtectedMethod(m)); + EXPECT_FALSE(Cpp::IsConstructor(m)); + } + } + EXPECT_TRUE(found_using_promoted) + << "using-promoted base method missing from GetClassMethods"; + + // Resolving the address of the using-promoted overload must transparently + // unwrap the using-shadow to its target before emitting code. This is the + // only caller exercising the non-const UnwrapUsingShadowToFunction overload + // (the reflection-only APIs above all go through the const overload), and the + // resolved address must match the one obtained directly from the base method. +#ifndef _WIN32 // GetFunctionAddress is disabled on Windows; see its own test. + if (!TypeParam::isOutOfProcess && found_using_promoted) { + std::vector base_methods; + Cpp::GetClassMethods(Decls[0], base_methods); + Cpp::FuncRef base_foo; + for (auto m : base_methods) { + if (Cpp::GetName(Cpp::DeclRef{m.data}) == "foo" && + Cpp::GetFunctionNumArgs(m) == 2) + base_foo = m; + } + ASSERT_TRUE(base_foo); + + void* shadow_addr = Cpp::GetFunctionAddress(using_promoted); + EXPECT_TRUE(shadow_addr); + EXPECT_EQ(shadow_addr, Cpp::GetFunctionAddress(base_foo)); + } +#endif + + std::vector hidden_methods; + Cpp::GetClassMethods(Decls[2], hidden_methods); + + bool found_hidden = false; + for (auto m : hidden_methods) { + if (Cpp::GetName(Cpp::DeclRef{m.data}) == "foo" && + Cpp::GetFunctionNumArgs(m) == 2) { + found_hidden = true; + EXPECT_FALSE(Cpp::IsPublicMethod(m)); + EXPECT_TRUE(Cpp::IsProtectedMethod(m)); + } + } + EXPECT_TRUE(found_hidden); +} + +// Companion to the access test above, covering the *call* path: a method +// promoted into a derived class with a public `using Base::name;` must be +// invocable through the derived class even though the target still carries the +// base class's protected access. The generated wrapper references the target by +// its original (protected) qualified name, so MakeFunctionCallable threads a +// relaxAccessControl flag into wrapper compilation for this case. Exercise it +// end to end: compile the wrapper and actually invoke it. +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_MakeFunctionCallable_UsingShadow) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscripten builds"; +#endif +#if defined(CPPINTEROP_USE_CLING) && defined(_WIN32) + GTEST_SKIP() << "Disabled on Cling/Windows."; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + std::vector Decls; + std::string code = R"( + class MyBase { + protected: + int foo(int a, int b) { return a * 100 + b; } + }; + class MyDerived : public MyBase { + public: + using MyBase::foo; // promoted to public + int foo(int a) { return a; } + }; + )"; + + // `-include new` is needed for the constructor wrapper's placement new. + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-include", "new"}); + + std::vector derived_methods; + Cpp::GetClassMethods(Decls[1], derived_methods); + + // Locate the using-promoted foo (the two-argument overload from the base). + Cpp::FuncRef using_promoted; + for (auto m : derived_methods) { + if (Cpp::GetName(Cpp::DeclRef{m.data}) == "foo" && + Cpp::GetFunctionNumArgs(m) == 2) + using_promoted = m; + } + ASSERT_TRUE(using_promoted); + + // Compiling this wrapper exercises the relaxAccessControl path: the generated + // body calls MyBase::foo through its qualified (protected) name, so access + // control has to be disabled for the wrapper to compile. + Cpp::JitCall Call = Cpp::MakeFunctionCallable(using_promoted); + ASSERT_EQ(Call.getKind(), Cpp::JitCall::kGenericCall); + + // Construct a MyDerived and call the promoted overload through it. + auto Ctor = Cpp::MakeFunctionCallable(Cpp::GetDefaultConstructor(Decls[1])); + void* object = nullptr; + Ctor.Invoke((void*)&object, {}, /*self=*/nullptr); + ASSERT_TRUE(object); + + int a = 3; + int b = 7; + int result = 0; + std::array args = {(void*)&a, (void*)&b}; + Call.Invoke(&result, {args.data(), /*args_size=*/2}, object); + EXPECT_EQ(result, (a * 100) + b); + + Cpp::Destruct(object, Decls[1]); +} + +// A using-promoted method still belongs to its declaring base class. When that +// base sits at a non-zero offset inside the derived object (multiple +// inheritance), callers adjust `this` with +// GetBaseClassOffset(derived, GetParentScope(method)) — exactly what CPyCppyy +// does before invoking the wrapper, which casts `self` to the declaring base +// type. GetParentScope on the using-shadow handle must therefore return the +// target's declaring base, not the class holding the using-declaration — +// otherwise the offset comes out zero and the call writes through an +// unadjusted pointer into the wrong subobject. Mirrors cppyy's +// test_regression.py::test50_using_decl_base_this_offset. +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_UsingShadow_BaseThisOffset) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscripten builds"; +#endif +#if defined(CPPINTEROP_USE_CLING) && defined(_WIN32) + GTEST_SKIP() << "Disabled on Cling/Windows."; +#endif + + std::vector Decls; + std::string code = R"( + // Fat first base so that SecondBase lands at a non-zero offset in Derived. + struct FirstBase { + long long a, b, c, d, e, f, g, h; + FirstBase() : a(11), b(22), c(33), d(44), e(55), f(66), g(77), h(88) {} + long long get_a() const { return a; } + }; + struct SecondBase { + int value; + SecondBase() : value(-1) {} + void set_value(int v) { value = v; } + int get_value() const { return value; } + }; + struct Derived : public FirstBase, public SecondBase { + int extra; + Derived() : extra(0) {} + using SecondBase::set_value; // import the 1-arg overload + void set_value(int v, int w) { value = v + w; extra = w; } + }; + )"; + + // `-include new` is needed for the constructor wrapper's placement new. + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-include", "new"}); + + std::vector derived_methods; + Cpp::GetClassMethods(Decls[2], derived_methods); + + // Locate the using-imported set_value (the one-argument overload). + Cpp::FuncRef imported; + for (auto m : derived_methods) { + if (Cpp::GetName(Cpp::DeclRef{m.data}) == "set_value" && + Cpp::GetFunctionNumArgs(m) == 1) + imported = m; + } + ASSERT_TRUE(imported); + + // The declaring scope of the imported method is SecondBase, not Derived. + Cpp::DeclRef declaring = Cpp::GetParentScope(Cpp::DeclRef{imported.data}); + EXPECT_EQ(Cpp::GetQualifiedName(declaring), "SecondBase"); + + // ... and SecondBase sits at a non-zero offset inside Derived. + int64_t offset = Cpp::GetBaseClassOffset(Decls[2], declaring); + EXPECT_GT(offset, 0); + + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "JitCall part fails for OOP JIT builds"; + + // End-to-end: construct a Derived, call the imported overload with `this` + // adjusted to the SecondBase subobject, and check the value landed there. + auto Ctor = Cpp::MakeFunctionCallable(Cpp::GetDefaultConstructor(Decls[2])); + void* object = nullptr; + Ctor.Invoke((void*)&object, {}, /*self=*/nullptr); + ASSERT_TRUE(object); + void* second_base = static_cast(object) + offset; + + Cpp::JitCall SetValue = Cpp::MakeFunctionCallable(imported); + ASSERT_EQ(SetValue.getKind(), Cpp::JitCall::kGenericCall); + int v = 42; + std::array args = {(void*)&v}; + SetValue.Invoke(nullptr, {args.data(), /*args_size=*/1}, second_base); + + std::vector second_base_methods; + Cpp::GetClassMethods(Decls[1], second_base_methods); + Cpp::FuncRef get_value; + for (auto m : second_base_methods) { + if (Cpp::GetName(Cpp::DeclRef{m.data}) == "get_value") + get_value = m; + } + ASSERT_TRUE(get_value); + + int result = 0; + Cpp::MakeFunctionCallable(get_value).Invoke(&result, {}, second_base); + EXPECT_EQ(result, 42); + + // The FirstBase subobject must be untouched (no write through an + // unadjusted pointer). + std::vector first_base_methods; + Cpp::GetClassMethods(Decls[0], first_base_methods); + Cpp::FuncRef get_a; + for (auto m : first_base_methods) { + if (Cpp::GetName(Cpp::DeclRef{m.data}) == "get_a") + get_a = m; + } + ASSERT_TRUE(get_a); + + long long a = 0; + Cpp::MakeFunctionCallable(get_a).Invoke(&a, {}, object); + EXPECT_EQ(a, 11); + + Cpp::Destruct(object, Decls[2]); +} + TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ConstructorInGetClassMethods) { std::vector Decls; @@ -829,6 +1094,265 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_FunctionTypes) { EXPECT_TRUE(Cpp::IsSameType(typ1, typ2)); } +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetDeallocType) { + std::string code = R"( + #include + #include + + void func0(int* p){ delete p; } + + void func1(int* p){ delete[] p; } + + void func2(int* p){ free(p); } + + void func3(int* p, int* q){ delete q; } + + void func4(int* p){ int* x = p; delete x; } + + void func5(int* p){ int* x = p; int* y = x; delete y; } + + void func6(int* p){ p = nullptr; delete p; } + + void func7(int* p){ int* x = p; p = nullptr; delete x; } + + void func8(int* p, bool b){ + if(b) + delete p; + else + delete[] p; + } + + void func9(int* p, bool b){ + if(b) + delete p; + else + delete p; + } + + void func10(int* p, int x){ + if(x<0) + delete[] p; + if(x==0) + free(p); + else + delete p; + } + + void func11(int* p); + + void func12(int* p) try { delete p; } catch(...) {} + + void func13(int n){} + + int func14; + + void func15(int* p){ int x = 5; (void)x; delete p; } + + void func16(int* p){ int* x = new int; delete x; } + + void func17(int* p){ delete new int; } + + void func18(int* p, void(*fp)(int*)){ fp(p); } + + void helper19(int*); + + void func19(int* p){ helper19(p); } + + void func20(int* p){ int* x; x = p; delete x; } + + void func21(int* p){ *p = 5; delete p; } + + void func22(int* p){ if(p == nullptr) return; delete p; } + + void func23(int* p, int* q){ q = p; delete q; } + + void func24(int* ptr){ func0(ptr); } + + void func25(int* ptr1, int* ptr2){ func2(ptr1); func1(ptr2); } + + void func26(int* ptr1, int* ptr2){ func0(ptr1); func6(ptr2); } + + void func27(int* ptr1, int* ptr2){ + int* tmp1 = ptr1; + func0(tmp1); + int* tmp2 = ptr2; + func22(tmp2); + } + + void func28(int* ptr, int n){ + if(n > 0) + func28(ptr, n-1); + delete ptr; + } + + void func30(int* ptr, int n); + void func31(int* ptr, int n); + void func29(int* ptr, int n){ + func30(ptr, n); + } + + void func30(int* ptr, int n){ + func31(ptr, n-1); + } + + void func31(int* ptr, int n){ + if(n > 0) + func29(ptr, n-1); + delete ptr; + } + + // FIXME: Can not resolve parameter location in recursive call + // Probably impossible to solve statically + void func32(int* ptr1, int* ptr2, int n){ + if(n > 0) + func32(ptr2, ptr1, n-1); + delete ptr1; + } + + void func33(int* ptr1, int* ptr2){ + delete ptr1; + } + + void func34(int* ptr1, int* ptr2){ + func33(ptr2, ptr1); + } + + void func35(int* ptr1, int* ptr2){ + func24(ptr1); + func0(ptr2); + } + + void func36(int* ptr){ + helper19(ptr); + delete ptr; + } + + void func37(int* ptr){ + delete ptr; + helper19(ptr); + } + + void func38(int* ptr){ + ptr = nullptr; + func0(ptr); + } + + void func39(int* ptr, int n){ + if(n > 0) + func0(ptr); + else + func2(ptr); + } + + struct Klass { + void operator=(int* q){ + delete q; + } + }; + void func40(int* p, Klass& K){ + K = p; + } + + void func41(int* ptr){ auto l = [&]{ delete ptr; }; } + + void func42(int* ptr){ + ptr = nullptr; + int* x = ptr; + delete x; + } + + void func43(int* ptr){ func8(ptr, true); } + + void func44(int* ptr){ + struct S { void g(int* q){ delete q; } }; + delete ptr; + } + + // This test's purpose is testing some lines, no specific purpose + int* globPtr = nullptr; + void func45(int* ptr){ + int a = 5; + a = 6; + int* tmp = globPtr; + tmp = (int*)malloc(sizeof(int)); + free(tmp); + delete ptr; + } + )"; + TestFixture::CreateInterpreter(); + Interp->declare(code); + + using DT = Cpp::DeallocType; +#define TESTGDT(N, BOOL, ...) \ + { \ + std::vector result; \ + bool valid = Cpp::GetDeallocType( \ + Cpp::ConstFuncRef { Cpp::GetNamed("func" #N).data }, result); \ + EXPECT_EQ(valid, BOOL); \ + EXPECT_EQ(result, (std::vector{__VA_ARGS__})); \ + } + + TESTGDT(0, true, DT::Delete); + TESTGDT(1, true, DT::DeleteArr); + TESTGDT(2, true, DT::Free); + TESTGDT(3, true, DT::None, DT::Delete); + TESTGDT(4, true, DT::Delete); + TESTGDT(5, true, DT::Delete); + TESTGDT(6, true, DT::None); + TESTGDT(7, true, DT::Delete); + TESTGDT(8, true, DT::Unknown, DT::None); + TESTGDT(9, true, DT::Delete, DT::None); + TESTGDT(10, true, DT::Unknown, DT::None); + TESTGDT(11, false, DT::Opaque); + TESTGDT(12, false, DT::Opaque); + TESTGDT(13, true, DT::None); + TESTGDT(15, true, DT::Delete); + TESTGDT(16, true, DT::None); + TESTGDT(17, true, DT::None); + TESTGDT(18, true, DT::None, DT::None); + TESTGDT(19, true, DT::Unknown); + TESTGDT(20, true, DT::Delete); + TESTGDT(21, true, DT::Delete); + TESTGDT(22, true, DT::Delete); + TESTGDT(23, true, DT::None, DT::None); + TESTGDT(24, true, DT::Delete); + TESTGDT(25, true, DT::Free, DT::DeleteArr); + TESTGDT(26, true, DT::Delete, DT::None); + TESTGDT(27, true, DT::Delete, DT::Delete); + TESTGDT(28, true, DT::Delete, DT::None); + TESTGDT(29, true, DT::Delete, DT::None); + TESTGDT(30, true, DT::Delete, DT::None); + TESTGDT(31, true, DT::Delete, DT::None); + TESTGDT(32, true, DT::Delete, DT::None, DT::None); + TESTGDT(33, true, DT::Delete, DT::None); + TESTGDT(34, true, DT::None, DT::Delete); + TESTGDT(35, true, DT::Delete, DT::Delete); + TESTGDT(36, true, DT::Unknown); + TESTGDT(37, true, DT::Unknown); + TESTGDT(38, true, DT::None); + TESTGDT(39, true, DT::Unknown, DT::None); + // FIXME: check top of VisitCallExpr function + TESTGDT(40, true, DT::None, DT::None); + TESTGDT(41, true, DT::None); + TESTGDT(42, true, DT::None); + TESTGDT(43, true, DT::Unknown); + TESTGDT(44, true, DT::Delete); + TESTGDT(45, true, DT::Delete); + +#undef TESTGDT + + { + std::vector result; + bool valid = Cpp::GetDeallocType( + Cpp::ConstFuncRef{Cpp::GetNamed("func14").data}, result); + EXPECT_FALSE(valid); + EXPECT_TRUE(result.empty()); + } + + std::vector nullResult; + EXPECT_FALSE(Cpp::GetDeallocType(Cpp::ConstFuncRef{nullptr}, nullResult)); +} + TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { std::string code = R"( #include @@ -906,8 +1430,327 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { p += 1; return p; } + + int* func28(int n){ + if(n>0) + return func28(n-1); + return new int; + } + + int* func29(int n){ + if(n>0){ + int* ptr = func29(n-1); + return ptr; + } + return new int; + } + int* func31(int n); + int* func32(int n); + + int* func30(int n){ + return func31(n); + } + + int* func31(int n){ + return func32(n-1); + } + + int* func32(int n){ + if(n>0) + return func30(n-1); + return new int; + } + + int* func33(int n){ + class Klass { + public: + int* getArr(int m) { return new int[m]; } + }; //Inside of struct/class/lambda's are not analyzed + auto lam = []() { return (int*)malloc(sizeof(int)); }; + return new int(n); + } + + int* func34(int n){ + int* ptr = nullptr; + if(n>0) + ptr = new int(n); + return ptr; + } + + int* func35(int n){ + int* ptr = nullptr; + if(n>0) + ptr = new int(n); + else + ptr = new int(n); + return ptr; + } + + int* func36(int n){ + int* ptr = nullptr; + if(n>0) + ptr = new int(n); + else + ptr = (int*)malloc(sizeof(int)); + return ptr; + } + + int* func37(int n){ + int* ptr = nullptr; + int* ptr2 = nullptr; + if(n>0){ + ptr = new int(n); + } else { + ptr2 = new int(n); + } + return ptr; + } + + int* func38(int n){ + int* ptr = nullptr; + if(n>9) + ptr = new int(n); + else if(n>5) + ptr = new int(n); + else if(n>2) + ptr = new int(n); + else + ptr = new int(n); + return ptr; + } + + int* func39(int n){ + int* ptr = nullptr; + if(n>9) + ptr = new int(n); + else if(n>5) + ptr = (int*)malloc(sizeof(int)); + else if(n>2) + ptr = new int(n); + else + ptr = new int(n); + return ptr; + } + + int* func40(int n){ + int* ptr = nullptr; + if(n>0){ + int* tmp = nullptr; + if(n>10) + tmp = new int(n); + else + tmp = new int(n); + ptr = tmp; + } + return ptr; + } + + int* func41(int n){ + int* ptr = nullptr; + if(n>0){ + ptr = new int(n); + ptr = new int(n); + } + return ptr; + } + + int* func42(int n){ + int* ptr = nullptr; + if(int* tmp = new int(n)){ + ptr = tmp; + } + return ptr; + } + + int* func43(int n){ + int* ptr = new int(n); + if(n>0){ + int* tmp = (int*)malloc(sizeof(int)); + ptr = tmp; + } + return ptr; + } + + int* func44(int n){ + int* q = NULL; + if(n>9){ + + } + else if(n>5){ + q = new int(n); + } + else{ + q = new int(n); + } + return q; + } + + int* func45(int n){ + int* x = (int*)0; + if(n>5){ + if(n>8) + x = new int(n); + else + x = new int(n); + } + return x; + } + + int* func46(int n){ + int* p = nullptr; + if(n>20){ + if(n>15){ + if(n>10) + p = new int(n); + else + p = new int(n); + } + else { + p = new int(n); + } + } + else { + p = new int(n); + } + return p; + } + + int* func47(int n){ + int* p = nullptr; + if(n>20){ + if(n>15){ + if(n>10) + p = new int(n); + else + p = new int(n); + } + } + return p; + } + + int* func48(int n){ + int* p = nullptr; + int* q = nullptr; + if(n>30){ + q = new int(n); + if(n>25){ + if(n>22) + p = new int(n); + else + p = new int(n); + } + else { + p = new int(n); + } + } + else if(n>20){ + p = new int(n); + if(n>15) + q = new int(n); + else + q = (int*)malloc(sizeof(int)); + } + else { + p = new int(n); + q = new int(n); + } + return p; + } + + int* func49(int n){ + int* p = (int*)malloc(sizeof(int)); + int* q = new int(n); + if(n>30){ + if(n>25){ + p = (int*)malloc(sizeof(int)); + if(n>20) + q += 1; + } + else { + p = (int*)malloc(sizeof(int)); + } + } + return p; + } + + int* func50(int n){ + int* p = nullptr; + if(n>40){ + p = new int(n); + } + else if(n>30){ + if(n>25){ + if(n>20) + p = new int(n); + else + p = (int*)malloc(sizeof(int)); + } + } + else if(n>10){ + p = new int(n); + } + else { + p = new int(n); + } + return p; + } + + int* func51(int n){ + int* ptr = nullptr; + if(ptr = new int(n)){ + + } + return ptr; + } + + int* func52(int n){ + if(n>0) + return new int(n); + return NULL; + } + + int* func53(int n){ + int* p = static_cast(0); + if(n>0) + p = new int(n); + return p; + } + + int* func54(int n){ + int* p; + if(n>0) + p = new int(n); + else + p = (int*)malloc(sizeof(int)); + return p; + } + void* func55(){ return ::operator new(64); } + void* func56(){ return ::operator new[](64); } + void* func57(void* buf){ return ::operator new(sizeof(int), buf); } + void* func58(){ return __builtin_operator_new(64); } + void* func59(){int* m = (int*)0; return malloc(sizeof(int));} + + // This test is for testing some lines, does not neccesarily mean something; + // But, it also shows how BindingDecls are not handled + struct Tuple { + int* ptr1; + int* ptr2; + }; + int* func70(){ + int arr[5]; + arr[0] = 5; + arr[1] = 6; + int* ptr = (int*)::operator new(sizeof(int)); + ::operator delete(ptr); + Tuple T; + T.ptr1 = &arr[0]; + T.ptr2 = &arr[1]; + auto [a, b] = T; + a = new int; + return a; + } )"; - TestFixture::CreateInterpreter(); + TestFixture::CreateInterpreter({"-std=c++17"}); Interp->declare(code); #define TESTAC(N, EXP) \ @@ -934,7 +1777,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { TESTAC(16, Unknown); TESTAC(17, None); TESTAC(18, None); - TESTAC(19, None); + TESTAC(19, Null); TESTAC(20, Unknown); TESTAC(21, None); TESTAC(22, New); @@ -942,9 +1785,40 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { TESTAC(24, New); TESTAC(25, NewArr); TESTAC(26, Unknown); - // FIXME: Pointer overwriten by a non-assignment operator - TESTAC(27, NewArr); - + TESTAC(27, Unknown); + TESTAC(28, New); + TESTAC(29, New); + TESTAC(30, New); + TESTAC(31, New); + TESTAC(32, New); + TESTAC(33, New); + TESTAC(34, New); + TESTAC(35, New); + TESTAC(36, Unknown); + TESTAC(37, New); + TESTAC(38, New); + TESTAC(39, Unknown); + TESTAC(40, New); + TESTAC(41, New); + TESTAC(42, New); + TESTAC(43, Unknown); + TESTAC(44, New); + TESTAC(45, New); + TESTAC(46, New); + TESTAC(47, New); + TESTAC(48, New); + TESTAC(49, Malloc); + TESTAC(50, Unknown); + TESTAC(51, New); + TESTAC(52, New); + TESTAC(53, New); + TESTAC(54, Unknown); + TESTAC(55, OperatorNew); + TESTAC(56, OperatorNewArr); + TESTAC(57, None); + TESTAC(58, OperatorNew); + TESTAC(59, Malloc); + TESTAC(70, Unknown); #undef TESTAC Cpp::DeleteInterpreter(); @@ -1034,12 +1908,34 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ExistsFunctionTemplate) { }; void f(char ch) {} + + void g(int) {} + void g(double) {} + + template + void h(T a) {} + void h(int) {} + + namespace NS { + template + void k(T a) {} + } + using NS::k; + void k(int) {} )"; GetAllTopLevelDecls(code, Decls); EXPECT_TRUE(Cpp::ExistsFunctionTemplate("f", nullptr)); EXPECT_TRUE(Cpp::ExistsFunctionTemplate("f", Decls[1])); EXPECT_FALSE(Cpp::ExistsFunctionTemplate("f", Decls[2])); + // An ambiguous name (overload set) is not a template just because the + // lookup found more than one decl: only report true if a templated + // function is among the results. + EXPECT_FALSE(Cpp::ExistsFunctionTemplate("g", nullptr)); + EXPECT_TRUE(Cpp::ExistsFunctionTemplate("h", nullptr)); + // The template may enter the overload set through a using-declaration: + // the using-shadow must be unwrapped to its target. + EXPECT_TRUE(Cpp::ExistsFunctionTemplate("k", nullptr)); } TYPED_TEST(CPPINTEROP_TEST_MODE, @@ -2932,6 +3828,162 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_Construct) { Cpp::Deallocate(scope, where); } +// The wrappers behind Construct and by-value returns placement-new into a +// caller-provided buffer. A class-scope operator new with no placement form +// hides the global `operator new(size_t, void*)`, so those wrappers only +// compile if they spell it `::new (buf) C(...)`. +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ConstructClassScopeNew) { +#ifdef _WIN32 + GTEST_SKIP() << "Disabled on Windows. Needs fixing."; +#endif +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscipten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + std::vector interpreter_args = {"-include", "new"}; + std::vector Decls; + + std::string code = R"( + class WithClassNew { + public: + int x; + WithClassNew() : x(42) {} + static void* operator new(__SIZE_TYPE__ sz) { return ::operator new(sz); } + static void* operator new[](__SIZE_TYPE__ sz) { + return ::operator new[](sz); + } + static void operator delete(void* p) { ::operator delete(p); } + static void operator delete[](void* p) { ::operator delete[](p); } + }; + WithClassNew MakeWithClassNew() { return WithClassNew(); } + )"; + + GetAllTopLevelDecls(code, Decls, false, interpreter_args); + Cpp::DeclRef scope = Cpp::GetNamed("WithClassNew"); + ASSERT_TRUE(scope); + + // Heap construction; the non-arena branch of the wrapper must keep using + // the unqualified `new` so it picks up the class-scope operator new. + Cpp::ObjectRef object = Cpp::Construct(scope); + ASSERT_TRUE(object); + EXPECT_EQ(*static_cast(object.data), 42); + Cpp::Destruct(object, scope, /*withFree=*/true, /*count=*/0); + + // Placement construction into an arena. + void* where = Cpp::Allocate(scope).data; + ASSERT_TRUE(where); + EXPECT_TRUE(where == Cpp::Construct(scope, where).data); + EXPECT_EQ(*static_cast(where), 42); + Cpp::Destruct(where, scope, /*withFree=*/false, /*count=*/0); + Cpp::Deallocate(scope, where); + + // Placement construction of an array (the wrapper's `nary > 1` branch). + constexpr size_t count = 3; + // The class holds a single int, so the array stride is one int and the + // constructed elements read back as int[count]. + ASSERT_EQ(Cpp::SizeOf(scope), sizeof(int)); + where = Cpp::Allocate(scope, count).data; + ASSERT_TRUE(where); + EXPECT_TRUE(where == Cpp::Construct(scope, where, count).data); + for (size_t i = 0; i < count; ++i) + EXPECT_EQ(static_cast(where)[i], 42); + Cpp::Destruct(where, scope, /*withFree=*/false, count); + Cpp::Deallocate(scope, where, count); + + // A by-value return placement-news the result into `ret` + // (make_narg_call_with_return); this must also bypass the class-scope + // operator new. + Cpp::JitCall JC = Cpp::MakeFunctionCallable(Decls[1]); + ASSERT_TRUE(JC.getKind() == Cpp::JitCall::kGenericCall); + int result = 0; // WithClassNew's layout is a single int + JC.Invoke(&result); + EXPECT_EQ(result, 42); +} + +// Pins down which operator new the wrappers pick when the class-scope forms +// are all accessible, including a class-scope *placement* operator new. The +// contract is the standard library's construct-at contract +// ([specialized.construct]): construction into a caller-provided buffer is +// spelled `::new (buf) C(...)`, so it constructs at `buf` directly and never +// routes through a class-scope placement operator new (which unqualified +// `new (buf)` would pick). Plain heap construction still goes through the +// user's class-scope allocator. The counters make the choice observable. +TYPED_TEST(CPPINTEROP_TEST_MODE, + FunctionReflection_ConstructClassScopePlacementNew) { +#ifdef _WIN32 + GTEST_SKIP() << "Disabled on Windows. Needs fixing."; +#endif +#ifdef EMSCRIPTEN +#if CLANG_VERSION_MAJOR > 21 + GTEST_SKIP() << "Test fails for Emscipten builds using LLVM 22"; +#endif +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + std::vector interpreter_args = {"-include", "new"}; + std::vector Decls; + + std::string code = R"( + int heap_news = 0; // calls to the class-scope operator new(size_t) + int placement_news = 0; // calls to the class-scope placement form + class WithPlacementNew { + public: + int x; + WithPlacementNew() : x(7) {} + WithPlacementNew Clone() { return WithPlacementNew(); } + static void* operator new(__SIZE_TYPE__ sz) { + ++heap_news; + return ::operator new(sz); + } + static void* operator new(__SIZE_TYPE__, void* where) { + ++placement_news; + return where; + } + static void operator delete(void* p) { ::operator delete(p); } + static void operator delete(void*, void*) {} + }; + )"; + + GetAllTopLevelDecls(code, Decls, false, interpreter_args); + Cpp::DeclRef scope = Cpp::GetNamed("WithPlacementNew"); + ASSERT_TRUE(scope); + + // Heap construction (the wrapper's non-arena `new C(...)`) must keep + // honoring the user's class-scope allocator. + Cpp::ObjectRef object = Cpp::Construct(scope); + ASSERT_TRUE(object); + EXPECT_EQ(*static_cast(object.data), 7); + EXPECT_EQ(Cpp::Evaluate("heap_news").unbox(), 1); + EXPECT_EQ(Cpp::Evaluate("placement_news").unbox(), 0); + + // Construction into a caller-provided buffer bypasses the class-scope + // placement operator new, like std::construct_at does. + void* where = Cpp::Allocate(scope).data; + ASSERT_TRUE(where); + EXPECT_TRUE(where == Cpp::Construct(scope, where).data); + EXPECT_EQ(*static_cast(where), 7); + EXPECT_EQ(Cpp::Evaluate("placement_news").unbox(), 0); + Cpp::Destruct(where, scope, /*withFree=*/false, /*count=*/0); + Cpp::Deallocate(scope, where); + + // A JitCall to a method of the class with a by-value result: the wrapper + // stores the result into the caller's buffer with `::new (ret)`, so the + // class-scope placement operator new stays out of the call path here too. + Cpp::JitCall JC = Cpp::MakeFunctionCallable( + Cpp::FuncRef{Cpp::GetNamed("Clone", scope).data}); + ASSERT_TRUE(JC.getKind() == Cpp::JitCall::kGenericCall); + int result = 0; // WithPlacementNew's layout is a single int + JC.Invoke(&result, {}, object.data); + EXPECT_EQ(result, 7); + EXPECT_EQ(Cpp::Evaluate("heap_news").unbox(), 1); + EXPECT_EQ(Cpp::Evaluate("placement_news").unbox(), 0); + + Cpp::Destruct(object, scope, /*withFree=*/true, /*count=*/0); +} + // Test zero initialization of PODs and default initialization cases TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ConstructPOD) { #ifdef _WIN32 @@ -3240,7 +4292,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_UndoTest) { defined(__APPLE__) GTEST_SKIP() << "Disabled on osx for cling based on llvm 20. Needs fixing."; #endif -#if defined(CPPINTEROP_USE_CLING) && defined(CPPINTEROP_ASAN_BUILD) +#if defined(CPPINTEROP_USE_CLING) GTEST_SKIP() << "cling unload walks a module already freed by ORC " "clone-on-emit; skip until the cling-side fix lands."; #endif @@ -3947,3 +4999,57 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, /*interpreter_args=*/{"-std=c++23", "-include", "new"}); EXPECT_EQ(JitCallIntNullary("BlogTransform", "drive"), 42); } + +// A by-value parameter of a move-only type must be moved into the call: the +// wrapper otherwise fails to compile against the deleted copy constructor. +// MoveOnly's deleted copy constructor is also non-trivial (Payload's is +// user-provided), so triviality bits cannot classify it. +TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_MoveOnlyByValueArgs) { +#ifdef EMSCRIPTEN + GTEST_SKIP() << "Test fails for Emscripten builds"; +#endif + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "Test fails for OOP JIT builds"; + + std::vector Decls; + std::vector SubDecls; + std::string code = R"( + struct Payload { + Payload() = default; + Payload(const Payload&) {} + }; + struct MoveOnly { + Payload p; + MoveOnly(const MoveOnly&) = delete; + MoveOnly(MoveOnly&&) = default; + }; + int take(MoveOnly m) { return 1; } + struct Taker { + Taker(MoveOnly m) {} + }; + struct Fwd; + int take_fwd(Fwd f); + )"; + + GetAllTopLevelDecls(code, Decls, /*filter_implicitGenerated=*/false, + /*interpreter_args=*/{"-include", "new"}); + ASSERT_EQ(Decls.size(), 6); + + // Function argument path (make_narg_call). + EXPECT_EQ(Cpp::MakeFunctionCallable(Decls[2]).getKind(), + Cpp::JitCall::kGenericCall); + + // Constructor argument path (make_narg_ctor). + GetAllSubDecls(Decls[3], SubDecls); + ASSERT_TRUE(Cpp::IsConstructor(SubDecls[1])); + EXPECT_EQ(Cpp::MakeFunctionCallable(SubDecls[1]).getKind(), + Cpp::JitCall::kConstructorCall); + + // A parameter type with no reachable definition is assumed copyable; the + // wrapper compile reports the incomplete type (captured: on Windows the + // MSVC-format diagnostic would fail MSBuild's output scan). + testing::internal::CaptureStderr(); + EXPECT_EQ(Cpp::MakeFunctionCallable(Decls[5]).getKind(), + Cpp::JitCall::kUnknown); + EXPECT_FALSE(testing::internal::GetCapturedStderr().empty()); +} diff --git a/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp index e5edb32055a4c..c5b35430696f8 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/InterpreterTest.cpp @@ -579,9 +579,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_IncludePaths) { } TYPED_TEST(CPPINTEROP_TEST_MODE, Interpreter_CodeCompletion) { -#if CLANG_VERSION_MAJOR == 20 && defined(CPPINTEROP_USE_CLING) && \ - defined(_WIN32) - GTEST_SKIP() << "Test fails with Cling on Windows"; +#ifdef _WIN32 + GTEST_SKIP() << "Disabled on Windows. Needs fixing."; #endif TestFixture::CreateInterpreter(); std::vector cc; diff --git a/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp index 9ad8c1d20f6ac..9afa1f66a8825 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/ScopeReflectionTest.cpp @@ -289,6 +289,53 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_IsComplete) { EXPECT_FALSE(Cpp::IsComplete(nullptr)); } +TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetOrForceDefinition) { + std::vector Decls; + std::string code = R"( + class Complete { int x; }; + struct Fwd; + enum EnumC : int { A, B }; + int gVar = 5; + void func() {} + namespace NS {} + template struct TS { T y; }; + TS makeTS() { return {}; } + extern int extVar; + void fwdFunc(); + template T fnTmpl(T x) { return x; } + )"; + GetAllTopLevelDecls(code, Decls); + + EXPECT_TRUE(Cpp::GetOrForceDefinition(nullptr) == nullptr); + EXPECT_TRUE(Cpp::GetOrForceDefinition(Decls[0]) != nullptr); + EXPECT_TRUE(Cpp::IsComplete(Cpp::GetOrForceDefinition(Decls[0]))); + EXPECT_TRUE(Cpp::GetOrForceDefinition(Decls[1]) == nullptr); + EXPECT_FALSE(Cpp::IsComplete(Decls[1])); + EXPECT_TRUE(Cpp::GetOrForceDefinition(Decls[2]) != nullptr); + EXPECT_TRUE(Cpp::GetOrForceDefinition(Decls[3]) != nullptr); + EXPECT_TRUE(Cpp::GetOrForceDefinition(Decls[4]) != nullptr); + EXPECT_TRUE(Cpp::GetOrForceDefinition(Decls[5]) == nullptr); + EXPECT_TRUE(Cpp::GetOrForceDefinition(Decls[8]) == nullptr); + EXPECT_TRUE(Cpp::GetOrForceDefinition(Decls[9]) == nullptr); + + // TS reached via makeTS's return type, completed on demand. + Cpp::TypeRef retTy = Cpp::GetFunctionReturnType(Decls[7]); + auto tsDef = Cpp::GetOrForceDefinition(Cpp::GetScopeFromType(retTy)); + EXPECT_TRUE(tsDef != nullptr); + EXPECT_TRUE(Cpp::IsComplete(tsDef)); + + // InstantiateTemplate instantiates the prototype, not the body; the + // FunctionDecl branch forces the definition. + ASTContext& C = Interp->getCI()->getASTContext(); + std::vector fnArgs = {C.IntTy.getAsOpaquePtr()}; + Cpp::DeclRef fnInst = Cpp::InstantiateTemplate(Decls[10], fnArgs); + auto* fnFD = Cpp::unwrap(fnInst); + EXPECT_EQ(fnFD->getDefinition(), nullptr); + EXPECT_NE(fnFD->getTemplateInstantiationPattern(), nullptr); + EXPECT_TRUE(Cpp::GetOrForceDefinition(fnInst) != nullptr); + EXPECT_NE(fnFD->getDefinition(), nullptr); +} + TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_SizeOf) { std::vector Decls; std::string code = R"(namespace N {} class C{}; int I; struct S; @@ -416,12 +463,31 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_IsAbstract) { int sum(int a, int b) { return a+b; } + + class FwdDeclared; + + template + struct AbsT { + virtual void f() = 0; + }; + AbsT* makeAbsT(); )"; GetAllTopLevelDecls(code, Decls); EXPECT_FALSE(Cpp::IsAbstract(Decls[0])); EXPECT_TRUE(Cpp::IsAbstract(Decls[1])); EXPECT_FALSE(Cpp::IsAbstract(Decls[2])); + // A class with no definition anywhere answers false and must not crash. + EXPECT_FALSE(Cpp::IsAbstract(Decls[3])); + + // An uninstantiated specialization is completed on demand before answering. + // (hasDefinition() is the passive check: IsComplete itself would force.) + Cpp::TypeRef PtrTy = Cpp::GetFunctionReturnType(Decls[5]); + Cpp::DeclRef Spec = Cpp::GetScopeFromType(Cpp::GetPointeeType(PtrTy)); + auto* SpecRD = Cpp::unwrap(Spec); + EXPECT_FALSE(SpecRD->hasDefinition()); + EXPECT_TRUE(Cpp::IsAbstract(Spec)); + EXPECT_TRUE(SpecRD->hasDefinition()); } TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_IsVariable) { @@ -704,6 +770,62 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetNamed) { EXPECT_EQ(Cpp::GetQualifiedName(std_string_npos_var), "std::basic_string::npos"); } +// GetNamed on a class scope must find inherited members per [class.qual]. +TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetNamedSearchesBaseClasses) { + std::string code = R"(struct GNBase { + typedef int value_type; + int ivalue; + }; + struct GNDerived : GNBase {}; + struct GNGrandChild : GNDerived {}; + + struct GNLeft { typedef int T; }; + struct GNRight { typedef float T; }; + struct GNBoth : GNLeft, GNRight {}; + struct GNFwd; + + template struct GNTBase { typedef T type; }; + template struct GNT : GNTBase {}; + GNT gnt_provider(); + )"; + + std::vector interpreter_args = {"-include", "new"}; + TestFixture::CreateInterpreter(interpreter_args); + Interp->declare(code); + + Cpp::DeclRef base = Cpp::GetNamed("GNBase", nullptr); + Cpp::DeclRef derived = Cpp::GetNamed("GNDerived", nullptr); + ASSERT_TRUE(base); + ASSERT_TRUE(derived); + + Cpp::DeclRef vt = Cpp::GetNamed("value_type", derived); + ASSERT_TRUE(vt); + EXPECT_EQ(Cpp::GetQualifiedName(vt), "GNBase::value_type"); + EXPECT_EQ(vt, Cpp::GetNamed("value_type", base)); + EXPECT_EQ(Cpp::GetQualifiedName(Cpp::GetNamed("ivalue", derived)), + "GNBase::ivalue"); + + EXPECT_EQ(Cpp::GetNamed("value_type", Cpp::GetNamed("GNGrandChild", nullptr)), + vt); + + // An ambiguous inherited name stays unresolved. + EXPECT_FALSE(Cpp::GetNamed("T", Cpp::GetNamed("GNBoth", nullptr))); + + // A name absent from the class and all of its bases stays unresolved. + EXPECT_FALSE(Cpp::GetNamed("no_such_member", derived)); + + // A class with no definition reachable anywhere cannot be searched. + EXPECT_FALSE(Cpp::GetNamed("value_type", Cpp::GetNamed("GNFwd", nullptr))); + + // A member of an uninstantiated specialization: complete it, then search. + Cpp::DeclRef gnt = Cpp::GetScopeFromType(Cpp::GetFunctionReturnType( + Cpp::FuncRef{Cpp::GetNamed("gnt_provider").data})); + ASSERT_TRUE(gnt); + Cpp::DeclRef gnt_type = Cpp::GetNamed("type", gnt); + ASSERT_TRUE(gnt_type); + EXPECT_EQ(Cpp::GetName(gnt_type), "type"); +} + TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_GetNamedWithUsing) { // Each subcase covers one form of [namespace.udecl] / [namespace.udir]. // GetNamed must look through the alias and return the original decl, @@ -1312,6 +1434,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, Cpp::GetClassTemplateInstantiationArgs(v3_class, instance_types); EXPECT_TRUE(instance_types.size() == 0); + + // Null or non-specialization decls must degrade to "no args", not crash + // (a silently-failed trampoline instantiation reaches here with null). + instance_types.clear(); + Cpp::GetClassTemplateInstantiationArgs(nullptr, instance_types); + EXPECT_TRUE(instance_types.empty()); + Cpp::GetClassTemplateInstantiationArgs(v1, instance_types); // a VarDecl + EXPECT_TRUE(instance_types.empty()); } TYPED_TEST(CPPINTEROP_TEST_MODE, ScopeReflection_IncludeVector) { diff --git a/interpreter/CppInterOp/unittests/CppInterOp/VariableReflectionTest.cpp b/interpreter/CppInterOp/unittests/CppInterOp/VariableReflectionTest.cpp index 7648cae52cedf..0adf9dcbc6c70 100644 --- a/interpreter/CppInterOp/unittests/CppInterOp/VariableReflectionTest.cpp +++ b/interpreter/CppInterOp/unittests/CppInterOp/VariableReflectionTest.cpp @@ -1,16 +1,24 @@ #include "Utils.h" +#include "../../lib/CppInterOp/Unwrap.h" + #include "CppInterOp/CppInterOp.h" +#include "CppInterOp/CppInterOpTypes.h" #include "clang/AST/ASTContext.h" +#include "clang/AST/Attr.h" #include "clang/Basic/Version.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Sema/Sema.h" +#include "llvm/Support/Error.h" + #include "gtest/gtest.h" -#include +#include #include +#include +#include using namespace TestUtils; using namespace llvm; @@ -235,6 +243,8 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableType) { E *f; int g[4]; auto fn = []() { return 1; }; + extern int uarr[]; + int uarr[7]; )"; GetAllTopLevelDecls(code, Decls); @@ -249,6 +259,14 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableType) { EXPECT_FALSE(Cpp::IsLambdaClass(Cpp::GetVariableType(Decls[8]))); EXPECT_TRUE(Cpp::IsLambdaClass(Cpp::GetVariableType(Decls[9]))); + + std::vector UArrDecls; + for (Decl* D : Decls) + if (Cpp::GetName(D) == "uarr") + UArrDecls.push_back(D); + ASSERT_EQ(UArrDecls.size(), 2); + EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetVariableType(UArrDecls[0])), "int[7]"); + EXPECT_EQ(Cpp::GetTypeAsString(Cpp::GetVariableType(UArrDecls[1])), "int[7]"); } #define CODE \ @@ -340,6 +358,251 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableOffset) { EXPECT_TRUE(Cpp::GetVariableOffset(var)); } +TYPED_TEST(CPPINTEROP_TEST_MODE, + VariableReflection_GetVariableOffset_NonTemplateStaticNoInit) { + // A non-template class's static data member declared in-class and defined + // out-of-line reaches the no-initializer branch with no template + // instantiation pattern. Sema::InstantiateVariableDefinition requires a + // pattern and dereferences null without one (std::partial_ordering::less + // is the in-the-wild shape). Must not crash; must resolve the definition. + TestFixture::CreateInterpreter(); + Cpp::Declare(R"( + struct NonTemplateStatic { + static const NonTemplateStatic less; + int value; + }; + inline constexpr NonTemplateStatic NonTemplateStatic::less{-1}; + )"); + Cpp::DeclRef klass = Cpp::GetNamed("NonTemplateStatic"); + EXPECT_TRUE(klass); + Cpp::DeclRef var = Cpp::GetNamed("less", klass); + EXPECT_TRUE(var); + EXPECT_TRUE(Cpp::GetVariableOffset(var)); + + // Declared and never defined: still no pattern and now no definition + // either — the query must fail cleanly, not crash. + Cpp::Declare(R"( + struct NeverDefined { + static const NeverDefined missing; + int value; + }; + )"); + Cpp::DeclRef klass2 = Cpp::GetNamed("NeverDefined"); + EXPECT_TRUE(klass2); + Cpp::DeclRef var2 = Cpp::GetNamed("missing", klass2); + EXPECT_TRUE(var2); + EXPECT_FALSE(Cpp::GetVariableOffset(var2)); +} + +TYPED_TEST(CPPINTEROP_TEST_MODE, + VariableReflection_GetVariableOffset_NoStaleUsedHandle) { +#ifdef __EMSCRIPTEN__ + // The stale-handle crash this test pins is native-JIT mechanics (ORC + // freeing materialized IR), and a single failed module load poisons + // Emscripten's process-global dynamic-linking state for every test that + // follows (issue #1071) — not worth the fragility for a native-only + // scenario. + GTEST_SKIP() << "Stale-used-handle scenario is native-JIT specific"; +#endif + // Emitting a discardable-ODR variable through the UsedAttr route records + // the global as a weak handle in codegen's llvm.used list. If the global + // is later replaced/erased (weak-def discard when a later PTU re-emits + // the same entity), the next PTU's emitUsed dereferences the nulled + // handle. The offset query must not leave used-list residue. + TestFixture::CreateInterpreter(); + Cpp::Declare(R"( + struct UsedHandle { + inline static int probe = 3; + }; + )"); + Cpp::DeclRef klass = Cpp::GetNamed("UsedHandle"); + EXPECT_TRUE(klass); + Cpp::DeclRef var = Cpp::GetNamed("probe", klass); + EXPECT_TRUE(var); + EXPECT_TRUE(Cpp::GetVariableOffset(var)); + // ForceCodeGen's UsedAttr is planted permanently on the AST decl; the + // odr-use route must not. + EXPECT_FALSE(Cpp::unwrap(var)->hasAttr()); +#ifndef CPPINTEROP_USE_CLING + // The UsedAttr lives on the AST decl, so every later PTU that re-emits + // the entity re-adds it to that module's llvm.used — the residue the + // stale-handle crash grows from. Neither a module that re-emits the + // variable nor an unrelated one may carry llvm.used. (PTU/TheModule is + // clang::Interpreter surface; cling covers this path via the UsedAttr + // assert above.) + { + auto PTUOrErr = Interp->Parse("int consume_probe = UsedHandle::probe;"); + ASSERT_TRUE(bool(PTUOrErr)); + EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr); + if (auto Err = Interp->Execute(*PTUOrErr)) + llvm::consumeError(std::move(Err)); + } + { + auto PTUOrErr = Interp->Parse("int flush_ptu = 0;"); + ASSERT_TRUE(bool(PTUOrErr)); + EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr); + if (auto Err = Interp->Execute(*PTUOrErr)) + llvm::consumeError(std::move(Err)); + } + EXPECT_TRUE(Cpp::GetNamed("flush_ptu")); +#endif // !CPPINTEROP_USE_CLING + // The crash this guards against was cumulative — many force-emitted + // statics plus JIT materialization cycles, interleaved with absorbed + // parse failures. Mimic that sweep shape as a regression net; the + // per-index sources are assembled at compile time by stringification. + struct SweepCase { + const char* decl; + const char* cls; + const char* var; + const char* probe; + const char* use; + }; +#define SWEEP_CASE(n) \ + { \ + "struct Sweep" #n " { inline static int v" #n " = " #n "; };", "Sweep" #n, \ + "v" #n, "template <> struct Sweep" #n ";", \ + "int use" #n " = Sweep" #n "::v" #n ";" \ + } + constexpr std::array Sweeps = { + {SWEEP_CASE(0), SWEEP_CASE(1), SWEEP_CASE(2), SWEEP_CASE(3), + SWEEP_CASE(4), SWEEP_CASE(5), SWEEP_CASE(6), SWEEP_CASE(7)}}; +#undef SWEEP_CASE + for (const SweepCase& S : Sweeps) { + Cpp::Declare(S.decl); + Cpp::DeclRef k = Cpp::GetNamed(S.cls); + ASSERT_TRUE(k); + Cpp::DeclRef v = Cpp::GetNamed(S.var, k); + ASSERT_TRUE(v); + EXPECT_TRUE(Cpp::GetVariableOffset(v)); + Cpp::Declare(S.probe, /*silent=*/true); // expected parse failure + Cpp::Declare(S.use); + } + Cpp::Declare("int sweep_done = 1;"); + EXPECT_TRUE(Cpp::GetNamed("sweep_done")); + + // Template-specialization members take the odr-use route too — the + // Sema-built address-of needs no source spelling of the specialization, + // and the caller has already instantiated the definition. + Cpp::Declare(R"( + template struct TmplStatic { + inline static int member = 7; + }; + )"); + Cpp::DeclRef tmpl = Cpp::GetNamed("TmplStatic"); + EXPECT_TRUE(tmpl); + ASTContext& C = Interp->getCI()->getASTContext(); + std::vector template_args = { + {C.IntTy.getAsOpaquePtr()}}; + Cpp::DeclRef inst = Cpp::InstantiateTemplate(tmpl, template_args); + EXPECT_TRUE(inst); + Cpp::DeclRef member = Cpp::GetNamed("member", inst); + EXPECT_TRUE(member); + EXPECT_TRUE(Cpp::GetVariableOffset(member)); + EXPECT_FALSE(Cpp::unwrap(member)->hasAttr()); + + // Anonymous-namespace members cannot be named from a fresh chunk of + // source; they bail out of the odr-use route the same way. + Cpp::Declare(R"( + namespace { + struct AnonNsStatic { + inline static int member = 9; + }; + } + )"); + Cpp::DeclRef anon_klass = Cpp::GetNamed("AnonNsStatic"); + ASSERT_TRUE(anon_klass); + Cpp::DeclRef anon_member = Cpp::GetNamed("member", anon_klass); + ASSERT_TRUE(anon_member); + EXPECT_TRUE(Cpp::GetVariableOffset(anon_member)); +#ifndef CPPINTEROP_USE_CLING + EXPECT_TRUE(Cpp::unwrap(anon_member)->hasAttr()); +#endif + + // A linkage-spec block is transparent for naming purposes and stays on + // the odr-use route. + Cpp::Declare(R"( + extern "C++" { + struct CxxLinkStatic { + inline static int member = 11; + }; + } + )"); + Cpp::DeclRef link_klass = Cpp::GetNamed("CxxLinkStatic"); + ASSERT_TRUE(link_klass); + Cpp::DeclRef link_member = Cpp::GetNamed("member", link_klass); + ASSERT_TRUE(link_member); + EXPECT_TRUE(Cpp::GetVariableOffset(link_member)); + EXPECT_FALSE(Cpp::unwrap(link_member)->hasAttr()); +} + +#ifndef CPPINTEROP_USE_CLING +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.) +#ifdef __EMSCRIPTEN__ + // The intentional materialization failure leaves the process-global wasm + // dynamic-linking state broken for every later load (see issue #1071). + GTEST_SKIP() << "A failed module load poisons wasm dynamic linking"; +#endif +#ifdef _WIN32 + // The intentional failure makes ORC print "JIT session error :" on + // stderr, which MSBuild's canonical-error scraping promotes to a build + // error (MSB8066) even though every test passes. + GTEST_SKIP() << "MSBuild's canonical-error scraping treats ORC's " + "JIT-session-failure stderr line as a build failure"; +#endif + // The intentional failure makes the remote dlupdate fail, and the + // out-of-process session then blocks instead of reporting the error. + if (TypeParam::isOutOfProcess) + GTEST_SKIP() << "A failed dlupdate wedges the out-of-process session"; + TestFixture::CreateInterpreter(); + Cpp::Declare(R"( + int undefined_fn(); + struct Unmaterializable { + inline static int probe = undefined_fn(); + }; + )"); + Cpp::DeclRef klass = Cpp::GetNamed("Unmaterializable"); + ASSERT_TRUE(klass); + Cpp::DeclRef var = Cpp::GetNamed("probe", klass); + ASSERT_TRUE(var); + EXPECT_FALSE(Cpp::GetVariableOffset(var)); +} +#endif // !CPPINTEROP_USE_CLING + +TYPED_TEST(CPPINTEROP_TEST_MODE, + VariableReflection_GetVariableOffset_OdrUseAnchorPerInterpreter) { + // The synthesized odr-use anchors must be named per-interpreter: a fresh + // interpreter's AST (and anything derived from it, e.g. a crash + // reproducer) may not depend on how many queries a sibling interpreter + // has run. + Cpp::InterpRef I1 = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I1); + Cpp::Declare(R"(struct PerInterpA { inline static int probe = 1; };)"); + Cpp::DeclRef ka = Cpp::GetNamed("PerInterpA"); + ASSERT_TRUE(ka); + Cpp::DeclRef va = Cpp::GetNamed("probe", ka); + ASSERT_TRUE(va); + EXPECT_TRUE(Cpp::GetVariableOffset(va)); + EXPECT_TRUE(Cpp::GetNamed("__cppinterop_odr_use_v0")); + + Cpp::InterpRef I2 = TestFixture::CreateInterpreter(); + ASSERT_TRUE(I2); + Cpp::Declare(R"(struct PerInterpB { inline static int probe = 2; };)"); + Cpp::DeclRef kb = Cpp::GetNamed("PerInterpB"); + ASSERT_TRUE(kb); + Cpp::DeclRef vb = Cpp::GetNamed("probe", kb); + ASSERT_TRUE(vb); + EXPECT_TRUE(Cpp::GetVariableOffset(vb)); + // The second interpreter's first anchor is also its v0. + EXPECT_TRUE(Cpp::GetNamed("__cppinterop_odr_use_v0")); + EXPECT_FALSE(Cpp::GetNamed("__cppinterop_odr_use_v1")); + EXPECT_TRUE(Cpp::DeleteInterpreter(I2)); +} + #define CODE \ class BaseA { \ public: \