From bbbf33302cec07569b08a157154c3fca9f762179 Mon Sep 17 00:00:00 2001 From: John Bowen Date: Wed, 30 Sep 2026 15:31:56 -0700 Subject: [PATCH 01/11] init clang frontend support --- cmake/ProteusFunctions.cmake | 4 + docs/user/integration.md | 7 +- src/pass/CMakeLists.txt | 16 +- src/pass/LambdaASTChecker.cpp | 338 ++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 5 +- tests/ast/CMakeLists.txt | 24 +++ tests/ast/lambda_invalid.cpp | 57 ++++++ tests/ast/lambda_valid.cpp | 29 +++ 8 files changed, 475 insertions(+), 5 deletions(-) create mode 100644 src/pass/LambdaASTChecker.cpp create mode 100644 tests/ast/CMakeLists.txt create mode 100644 tests/ast/lambda_invalid.cpp create mode 100644 tests/ast/lambda_valid.cpp diff --git a/cmake/ProteusFunctions.cmake b/cmake/ProteusFunctions.cmake index fd35e2928..84374367a 100644 --- a/cmake/ProteusFunctions.cmake +++ b/cmake/ProteusFunctions.cmake @@ -9,13 +9,17 @@ function(add_proteus target) "-fplugin=$" "-fpass-plugin=$" "SHELL:-Xclang -mllvm -Xclang -force-proteus-jit-annotate-all" + "-fplugin=\$" ) else() target_compile_options(${target} PRIVATE "-fpass-plugin=\$" + "-fplugin=\$" ) endif() + add_dependencies(${target} ProteusPass ProteusASTChecker) + target_link_options(${target} PRIVATE "SHELL:\$<\$:-Xoffload-linker --load-pass-plugin=\$>") diff --git a/docs/user/integration.md b/docs/user/integration.md index beac6fcf2..61162d927 100644 --- a/docs/user/integration.md +++ b/docs/user/integration.md @@ -36,6 +36,7 @@ linking flags, for example: ```bash CXXFLAGS += -I/include \ + -fplugin=//libProteusASTChecker.so \ -fpass-plugin=//libProteusPass.so LDFLAGS += -L/ \ @@ -43,8 +44,10 @@ LDFLAGS += -L/ \ -lproteus $(llvm-config --libs) -lclang-cpp ``` -If you don't use code annotations, you can omit the `-fpass-plugin` option, -since the LLVM pass is only needed for processing annotations. +The frontend checker loaded by `-fplugin` diagnoses unsupported uses of +`proteus::register_lambda` and `proteus::jit_variable` before LLVM IR is +emitted. If you don't use code annotations or these lambda APIs, you can omit +both plugin options. !!! note The example above is the simplest case and is closest to a host-only build. diff --git a/src/pass/CMakeLists.txt b/src/pass/CMakeLists.txt index d37e7a3f2..2b270d7cb 100644 --- a/src/pass/CMakeLists.txt +++ b/src/pass/CMakeLists.txt @@ -48,6 +48,18 @@ add_library(ProteusPassOffload SHARED $ ) +# Keep the Clang frontend plugin separate from the LLVM pass plugin: the pass +# is also loaded by opt/offload linkers, which do not provide Clang symbols. +add_library(ProteusASTChecker SHARED LambdaASTChecker.cpp) +target_include_directories(ProteusASTChecker SYSTEM PRIVATE + ${LLVM_INCLUDE_DIRS} + ${CLANG_INCLUDE_DIRS}) +if(NOT LLVM_ENABLE_RTTI) + target_compile_options(ProteusASTChecker PRIVATE -fno-rtti) +endif() +target_link_options(ProteusASTChecker + PRIVATE "$<$:LINKER:SHELL:-undefined dynamic_lookup>") + # Allow undefined symbols in shared objects on Darwin (this is the default # behaviour on Linux) target_link_options(ProteusPass @@ -72,7 +84,7 @@ target_link_options( "SHELL:$<$:-Xoffload-linker --load-pass-plugin=$>" ) -set_target_properties(ProteusPass ProteusPassOffload PROPERTIES +set_target_properties(ProteusPass ProteusPassOffload ProteusASTChecker PROPERTIES # on macOS enable passing RPATH flags to the linker; no-ops on Linux MACOSX_RPATH ON # let CMake use INSTALL_RPATH even in the build tree @@ -85,7 +97,7 @@ set_target_properties(ProteusPass ProteusPassOffload PROPERTIES # is ready to support Clang/LLVM pass consumers too. if(NOT PROTEUS_PYTHON_WHEEL) install( - TARGETS ProteusPass ProteusPassOffload + TARGETS ProteusPass ProteusPassOffload ProteusASTChecker EXPORT proteusTargets RUNTIME DESTINATION "${CMAKE_INSTALL_LIBDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" diff --git a/src/pass/LambdaASTChecker.cpp b/src/pass/LambdaASTChecker.cpp new file mode 100644 index 000000000..56fd98a38 --- /dev/null +++ b/src/pass/LambdaASTChecker.cpp @@ -0,0 +1,338 @@ +//===-- LambdaASTChecker.cpp - Validate Proteus lambda API use -----------===// +// +// Part of the Proteus Project, under the Apache License v2.0 with LLVM +// Exceptions. See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/AST/ASTConsumer.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/DeclCXX.h" +#include "clang/AST/ExprCXX.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Frontend/FrontendPluginRegistry.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include +#include +#include + +namespace { + +bool isProteusFunction(const clang::CallExpr *Call, llvm::StringRef Name) { + const clang::FunctionDecl *Callee = Call->getDirectCallee(); + return Callee && Callee->getQualifiedNameAsString() == Name; +} + +const clang::Expr *ignoreTransparentExprs(const clang::Expr *Expr) { + while (Expr) { + const clang::Expr *Next = Expr->IgnoreParens(); + if (Next != Expr) { + Expr = Next; + continue; + } + if (const auto *E = llvm::dyn_cast(Expr)) + Next = E->getSubExpr(); + else if (const auto *E = llvm::dyn_cast(Expr)) + Next = E->getSubExpr(); + else if (const auto *E = + llvm::dyn_cast(Expr)) + Next = E->getSubExpr(); + else if (const auto *E = llvm::dyn_cast(Expr)) + Next = E->getSubExpr(); + if (Next == Expr) + break; + Expr = Next; + } + return Expr; +} + +const clang::CXXRecordDecl *getCanonicalRecord(clang::QualType Type) { + Type = Type.getNonReferenceType().getUnqualifiedType(); + if (const auto *Record = Type->getAsCXXRecordDecl()) + return Record->getCanonicalDecl(); + return nullptr; +} + +bool isLambdaFunctorWrapper(const clang::CXXRecordDecl *Record) { + const auto *Specialization = + llvm::dyn_cast_or_null(Record); + return Specialization && + Specialization->getSpecializedTemplate()->getQualifiedNameAsString() == + "proteus::detail::LambdaFunctorWrapper"; +} + +bool directlyReferencesTarget( + const clang::Expr *Expr, + const llvm::DenseSet &Targets) { + Expr = ignoreTransparentExprs(Expr); + const auto *Reference = llvm::dyn_cast_or_null(Expr); + const auto *Variable = + Reference ? llvm::dyn_cast(Reference->getDecl()) : nullptr; + return Variable && Targets.contains(Variable->getCanonicalDecl()); +} + +bool takesAddressOfTarget( + const clang::Expr *Expr, + const llvm::DenseSet &Targets) { + Expr = ignoreTransparentExprs(Expr); + const auto *Operator = llvm::dyn_cast_or_null(Expr); + return Operator && Operator->getOpcode() == clang::UO_AddrOf && + directlyReferencesTarget(Operator->getSubExpr(), Targets); +} + +bool isMutableReference(clang::QualType Type) { + if (!Type->isReferenceType()) + return false; + return !Type.getNonReferenceType().isConstQualified(); +} + +bool isMutablePointer(clang::QualType Type) { + return Type->isPointerType() && !Type->getPointeeType().isConstQualified(); +} + +class CaptureMutationVisitor + : public clang::RecursiveASTVisitor { +public: + CaptureMutationVisitor( + const llvm::DenseSet &Targets, + llvm::SmallVectorImpl &Mutations) + : Targets(Targets), Mutations(Mutations) {} + + bool VisitBinaryOperator(clang::BinaryOperator *Operator) { + if (Operator->isAssignmentOp() && + directlyReferencesTarget(Operator->getLHS(), Targets)) + Mutations.push_back(Operator->getSourceRange()); + return true; + } + + bool VisitUnaryOperator(clang::UnaryOperator *Operator) { + if (Operator->isIncrementDecrementOp() && + directlyReferencesTarget(Operator->getSubExpr(), Targets)) + Mutations.push_back(Operator->getSourceRange()); + return true; + } + + bool VisitVarDecl(clang::VarDecl *Variable) { + if (!Variable->hasInit()) + return true; + clang::QualType Type = Variable->getType(); + if (isMutableReference(Type) && + directlyReferencesTarget(Variable->getInit(), Targets)) + Mutations.push_back(Variable->getSourceRange()); + else if (isMutablePointer(Type) && + takesAddressOfTarget(Variable->getInit(), Targets)) + Mutations.push_back(Variable->getSourceRange()); + return true; + } + + bool VisitCallExpr(clang::CallExpr *Call) { + const clang::FunctionDecl *Callee = Call->getDirectCallee(); + if (!Callee) + return true; + const unsigned Count = std::min(Call->getNumArgs(), Callee->getNumParams()); + for (unsigned I = 0; I < Count; ++I) { + const clang::Expr *Argument = Call->getArg(I); + clang::QualType ParameterType = Callee->getParamDecl(I)->getType(); + if ((isMutableReference(ParameterType) && + directlyReferencesTarget(Argument, Targets)) || + (isMutablePointer(ParameterType) && + takesAddressOfTarget(Argument, Targets))) + Mutations.push_back(Argument->getSourceRange()); + } + return true; + } + +private: + const llvm::DenseSet &Targets; + llvm::SmallVectorImpl &Mutations; +}; + +class LambdaVisitor : public clang::RecursiveASTVisitor { +public: + bool shouldVisitTemplateInstantiations() const { return true; } + + bool VisitLambdaExpr(clang::LambdaExpr *Lambda) { + Lambdas.push_back(Lambda); + return true; + } + + bool VisitCallExpr(clang::CallExpr *Call) { + if (isProteusFunction(Call, "proteus::jit_variable")) + JitVariableCalls.push_back(Call); + else if (isProteusFunction(Call, "proteus::register_lambda") && + Call->getNumArgs() != 0 && !Call->getArg(0)->isTypeDependent()) { + RegisterLambdaCalls.push_back(Call); + const clang::Expr *Argument = ignoreTransparentExprs(Call->getArg(0)); + if (const auto *Lambda = llvm::dyn_cast(Argument)) { + DirectlyRegisteredLambdas.insert(Lambda); + DirectlyRegisteredLambdaLocations.insert( + Lambda->getBeginLoc().getRawEncoding()); + } + if (const auto *Record = getCanonicalRecord(Argument->getType()); + Record && Record->isLambda()) + RegisteredLambdaTypes.insert(Record); + } + return true; + } + + llvm::SmallVector Lambdas; + llvm::SmallVector JitVariableCalls; + llvm::SmallVector RegisterLambdaCalls; + llvm::DenseSet DirectlyRegisteredLambdas; + llvm::DenseSet DirectlyRegisteredLambdaLocations; + llvm::DenseSet RegisteredLambdaTypes; +}; + +class LambdaASTConsumer : public clang::ASTConsumer { +public: + explicit LambdaASTConsumer(clang::CompilerInstance &CI) : CI(CI) {} + + void HandleTranslationUnit(clang::ASTContext &Context) override { + LambdaVisitor Visitor; + Visitor.TraverseDecl(Context.getTranslationUnitDecl()); + + llvm::DenseMap + CaptureInitializers; + llvm::DenseMap CaptureTypes; + llvm::DenseMap> + JitCaptureVariables; + for (const clang::LambdaExpr *Lambda : Visitor.Lambdas) { + auto Init = Lambda->capture_init_begin(); + for (const clang::LambdaCapture &Capture : Lambda->captures()) { + const clang::Expr *CaptureInit = *Init++; + if (!Capture.capturesVariable() || + !Capture.getCapturedVar()->isInitCapture()) + continue; + clang::QualType CaptureType = CaptureInit->getType(); + CaptureInit = ignoreTransparentExprs(CaptureInit); + const auto *Call = llvm::dyn_cast_or_null(CaptureInit); + if (Call && isProteusFunction(Call, "proteus::jit_variable")) { + CaptureInitializers.try_emplace(Call, Lambda); + CaptureTypes.try_emplace(Call, CaptureType); + JitCaptureVariables[Lambda].push_back( + llvm::cast( + Capture.getCapturedVar()->getCanonicalDecl())); + } + } + } + + clang::DiagnosticsEngine &Diagnostics = CI.getDiagnostics(); + const unsigned InvalidUse = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "proteus::jit_variable must be used directly as a lambda " + "init-capture initializer"); + const unsigned Unregistered = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "lambda containing proteus::jit_variable must be registered with " + "proteus::register_lambda"); + const unsigned UnsupportedType = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "proteus::jit_variable does not support capture type %0"); + const unsigned InvalidRegisterArgument = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "proteus::register_lambda requires a lambda closure object"); + const unsigned AlreadyRegistered = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "a lambda returned by proteus::register_lambda cannot be registered " + "again"); + const unsigned MutatedCapture = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "proteus::jit_variable capture must remain read-only"); + + for (const clang::CallExpr *Call : Visitor.RegisterLambdaCalls) { + const clang::Expr *Argument = Call->getArg(0); + const auto *Record = getCanonicalRecord(Argument->getType()); + if (isLambdaFunctorWrapper(Record)) + Diagnostics.Report(Argument->getExprLoc(), AlreadyRegistered) + << Argument->getSourceRange(); + else if (!Record || !Record->isLambda()) + Diagnostics.Report(Argument->getExprLoc(), InvalidRegisterArgument) + << Argument->getSourceRange(); + } + + for (const auto &Entry : JitCaptureVariables) { + llvm::DenseSet Targets; + Targets.insert(Entry.second.begin(), Entry.second.end()); + llvm::SmallVector Mutations; + CaptureMutationVisitor MutationVisitor(Targets, Mutations); + MutationVisitor.TraverseStmt(Entry.first->getBody()); + for (clang::SourceRange Range : Mutations) + Diagnostics.Report(Range.getBegin(), MutatedCapture) << Range; + } + + for (const clang::CallExpr *Call : Visitor.JitVariableCalls) { + // Dependent template patterns are checked when their specializations are + // instantiated, once both the closure and captured value types are known. + if (Call->isTypeDependent()) + continue; + + auto CaptureIt = CaptureInitializers.find(Call); + if (CaptureIt == CaptureInitializers.end()) { + Diagnostics.Report(Call->getExprLoc(), InvalidUse) + << Call->getSourceRange(); + continue; + } + + const clang::LambdaExpr *Lambda = CaptureIt->second; + const auto *LambdaType = Lambda->getLambdaClass()->getCanonicalDecl(); + if (!Visitor.DirectlyRegisteredLambdas.contains(Lambda) && + !Visitor.DirectlyRegisteredLambdaLocations.contains( + Lambda->getBeginLoc().getRawEncoding()) && + !Visitor.RegisteredLambdaTypes.contains(LambdaType)) { + Diagnostics.Report(Call->getExprLoc(), Unregistered) + << Lambda->getSourceRange(); + } + + clang::QualType Type = CaptureTypes.lookup(Call).getUnqualifiedType(); + if (!isSupportedRuntimeConstantType(Type, Context)) + Diagnostics.Report(Call->getExprLoc(), UnsupportedType) + << Type << Call->getSourceRange(); + } + } + +private: + static bool isSupportedRuntimeConstantType(clang::QualType Type, + clang::ASTContext &Context) { + if (Type->isPointerType() || Type->isBooleanType() || + (Type->isRealFloatingType() && + (Type->isSpecificBuiltinType(clang::BuiltinType::Float) || + Type->isSpecificBuiltinType(clang::BuiltinType::Double)))) + return true; + + if (!Type->isIntegralOrEnumerationType()) + return false; + const uint64_t Width = Context.getTypeSize(Type); + return Width == 8 || Width == 32 || Width == 64; + } + + clang::CompilerInstance &CI; +}; + +class LambdaASTCheckerAction : public clang::PluginASTAction { +protected: + std::unique_ptr + CreateASTConsumer(clang::CompilerInstance &CI, llvm::StringRef) override { + return std::make_unique(CI); + } + + bool ParseArgs(const clang::CompilerInstance &, + const std::vector &) override { + return true; + } + + ActionType getActionType() override { return AddBeforeMainAction; } +}; + +} // namespace + +static clang::FrontendPluginRegistry::Add + X("proteus-lambda-checker", "validate Proteus lambda API usage"); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 967c7adc2..3c37a4e33 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -59,7 +59,7 @@ function(proteus_attach_pass_plugin_rebuild_dep target) add_custom_command( OUTPUT "${proteus_pass_stamp}" COMMAND ${CMAKE_COMMAND} -E touch "${proteus_pass_stamp}" - DEPENDS ProteusPass + DEPENDS ProteusPass ProteusASTChecker VERBATIM ) @@ -70,10 +70,13 @@ function(proteus_attach_pass_plugin_rebuild_dep target) ${target} PUBLIC -fpass-plugin=$ + -fplugin=$ ) + add_dependencies(${target} ProteusASTChecker) endfunction() add_subdirectory(cpu) +add_subdirectory(ast) if(PROTEUS_ENABLE_HIP OR PROTEUS_ENABLE_CUDA) add_subdirectory(gpu) diff --git a/tests/ast/CMakeLists.txt b/tests/ast/CMakeLists.txt new file mode 100644 index 000000000..00cc91a72 --- /dev/null +++ b/tests/ast/CMakeLists.txt @@ -0,0 +1,24 @@ +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/lit.cfg.py CONTENT " +import lit.formats +import os + +config.name = 'Proteus AST checker tests' +config.test_format = lit.formats.ShTest(True) +config.environment = os.environ.copy() +config.suffixes = ['.cpp'] +config.test_source_root = '${CMAKE_CURRENT_SOURCE_DIR}' +config.test_exec_root = '${CMAKE_CURRENT_BINARY_DIR}' +config.substitutions.append(('%clang', '${LLVM_TOOLS_BINARY_DIR}/clang++')) +config.substitutions.append(('%plugin', '$')) +config.substitutions.append(('%proteus_include', '${PROJECT_SOURCE_DIR}/include')) +config.substitutions.append(('%FILECHECK', '${FILECHECK}')) +") + +# Exercise the same add_proteus() path used by installed CMake consumers, in +# addition to the direct driver invocations in the lit tests below. +add_library(proteus_ast_checker_cmake_test OBJECT lambda_valid.cpp) +add_proteus(proteus_ast_checker_cmake_test) + +add_test(NAME lambda_ast_checker + COMMAND ${LIT} -vv -D FILECHECK=${FILECHECK} .) +set_tests_properties(lambda_ast_checker PROPERTIES LABELS "ast") diff --git a/tests/ast/lambda_invalid.cpp b/tests/ast/lambda_invalid.cpp new file mode 100644 index 000000000..08a6a4717 --- /dev/null +++ b/tests/ast/lambda_invalid.cpp @@ -0,0 +1,57 @@ +// clang-format off +// RUN: not %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s 2>&1 | %FILECHECK %s + +#include + +struct Functor { + void operator()() const {} +}; + +void invalid_register() { + (void)proteus::register_lambda(Functor{}); + // CHECK-DAG: error: proteus::register_lambda requires a lambda closure object +} + +void duplicate_register() { + auto Registered = proteus::register_lambda([] {}); + (void)proteus::register_lambda(Registered); + // CHECK-DAG: error: a lambda returned by proteus::register_lambda cannot be registered again +} + +void invalid_use(int Value) { + (void)proteus::jit_variable(Value); + // CHECK-DAG: error: proteus::jit_variable must be used directly as a lambda init-capture initializer +} + +void unregistered(int Value) { + auto Lambda = [X = proteus::jit_variable(Value)] { return X; }; + // CHECK-DAG: error: lambda containing proteus::jit_variable must be registered with proteus::register_lambda + (void)Lambda; +} +// clang-format on + +void unsupported_type(short Value) { + auto Lambda = proteus::register_lambda( + [X = proteus::jit_variable(Value)] { return X; }); + // CHECK-DAG: error: proteus::jit_variable does not support capture type 'short' + (void)Lambda; +} + +void mutate_capture(int Value) { + auto Lambda = proteus::register_lambda( + [X = proteus::jit_variable(Value)]() mutable { return ++X; }); + // CHECK-DAG: error: proteus::jit_variable capture must remain read-only + (void)Lambda; +} + +void mutate_by_reference(int &Value) { ++Value; } + +void escape_capture(int Value) { + auto Lambda = proteus::register_lambda( + [X = proteus::jit_variable(Value)]() mutable { + mutate_by_reference(X); + int *Alias = &X; + return *Alias; + }); + (void)Lambda; +} diff --git a/tests/ast/lambda_valid.cpp b/tests/ast/lambda_valid.cpp new file mode 100644 index 000000000..4f56f70a8 --- /dev/null +++ b/tests/ast/lambda_valid.cpp @@ -0,0 +1,29 @@ +// RUN: %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s + +#include + +template void register_indirectly(F &&Fn) { + proteus::register_lambda(Fn)(); +} + +auto make_lambda(int Value) { + return [X = proteus::jit_variable(Value)] { return X; }; +} + +template void direct_template(int Value) { + auto Lambda = proteus::register_lambda( + [=, X = proteus::jit_variable(Value)] { return X + Offset; }); + (void)Lambda; +} + +void valid(int Value, int *Pointer) { + register_indirectly( + [X = proteus::jit_variable(Value), + P = proteus::jit_variable(Pointer)] { + *P = X; + return *P; + }); + auto Lambda = proteus::register_lambda(make_lambda(Value)); + (void)Lambda; + direct_template<1>(Value); +} From 05061765491b5a7df8e816b7d11e53850137ec26 Mon Sep 17 00:00:00 2001 From: John Bowen Date: Thu, 1 Oct 2026 11:06:45 -0700 Subject: [PATCH 02/11] Clang format and fix test harness failure --- src/pass/LambdaASTChecker.cpp | 13 ++++++------- tests/ast/CMakeLists.txt | 1 + tests/ast/lambda_invalid.cpp | 9 +++++---- tests/ast/lambda_valid.cpp | 3 +-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pass/LambdaASTChecker.cpp b/src/pass/LambdaASTChecker.cpp index 56fd98a38..b1ef1b044 100644 --- a/src/pass/LambdaASTChecker.cpp +++ b/src/pass/LambdaASTChecker.cpp @@ -75,7 +75,8 @@ bool directlyReferencesTarget( Expr = ignoreTransparentExprs(Expr); const auto *Reference = llvm::dyn_cast_or_null(Expr); const auto *Variable = - Reference ? llvm::dyn_cast(Reference->getDecl()) : nullptr; + Reference ? llvm::dyn_cast(Reference->getDecl()) + : nullptr; return Variable && Targets.contains(Variable->getCanonicalDecl()); } @@ -101,9 +102,8 @@ bool isMutablePointer(clang::QualType Type) { class CaptureMutationVisitor : public clang::RecursiveASTVisitor { public: - CaptureMutationVisitor( - const llvm::DenseSet &Targets, - llvm::SmallVectorImpl &Mutations) + CaptureMutationVisitor(const llvm::DenseSet &Targets, + llvm::SmallVectorImpl &Mutations) : Targets(Targets), Mutations(Mutations) {} bool VisitBinaryOperator(clang::BinaryOperator *Operator) { @@ -218,9 +218,8 @@ class LambdaASTConsumer : public clang::ASTConsumer { if (Call && isProteusFunction(Call, "proteus::jit_variable")) { CaptureInitializers.try_emplace(Call, Lambda); CaptureTypes.try_emplace(Call, CaptureType); - JitCaptureVariables[Lambda].push_back( - llvm::cast( - Capture.getCapturedVar()->getCanonicalDecl())); + JitCaptureVariables[Lambda].push_back(llvm::cast( + Capture.getCapturedVar()->getCanonicalDecl())); } } } diff --git a/tests/ast/CMakeLists.txt b/tests/ast/CMakeLists.txt index 00cc91a72..3ecd04deb 100644 --- a/tests/ast/CMakeLists.txt +++ b/tests/ast/CMakeLists.txt @@ -8,6 +8,7 @@ config.environment = os.environ.copy() config.suffixes = ['.cpp'] config.test_source_root = '${CMAKE_CURRENT_SOURCE_DIR}' config.test_exec_root = '${CMAKE_CURRENT_BINARY_DIR}' +config.substitutions.append(('%not', '${LLVM_TOOLS_BINARY_DIR}/not')) config.substitutions.append(('%clang', '${LLVM_TOOLS_BINARY_DIR}/clang++')) config.substitutions.append(('%plugin', '$')) config.substitutions.append(('%proteus_include', '${PROJECT_SOURCE_DIR}/include')) diff --git a/tests/ast/lambda_invalid.cpp b/tests/ast/lambda_invalid.cpp index 08a6a4717..e23f1da88 100644 --- a/tests/ast/lambda_invalid.cpp +++ b/tests/ast/lambda_invalid.cpp @@ -1,5 +1,5 @@ // clang-format off -// RUN: not %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s 2>&1 | %FILECHECK %s +// RUN: %not %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s 2>&1 | %FILECHECK %s #include @@ -33,7 +33,8 @@ void unregistered(int Value) { void unsupported_type(short Value) { auto Lambda = proteus::register_lambda( [X = proteus::jit_variable(Value)] { return X; }); - // CHECK-DAG: error: proteus::jit_variable does not support capture type 'short' + // CHECK-DAG: error: proteus::jit_variable does not support capture type + // 'short' (void)Lambda; } @@ -47,8 +48,8 @@ void mutate_capture(int Value) { void mutate_by_reference(int &Value) { ++Value; } void escape_capture(int Value) { - auto Lambda = proteus::register_lambda( - [X = proteus::jit_variable(Value)]() mutable { + auto Lambda = + proteus::register_lambda([X = proteus::jit_variable(Value)]() mutable { mutate_by_reference(X); int *Alias = &X; return *Alias; diff --git a/tests/ast/lambda_valid.cpp b/tests/ast/lambda_valid.cpp index 4f56f70a8..68a469f12 100644 --- a/tests/ast/lambda_valid.cpp +++ b/tests/ast/lambda_valid.cpp @@ -18,8 +18,7 @@ template void direct_template(int Value) { void valid(int Value, int *Pointer) { register_indirectly( - [X = proteus::jit_variable(Value), - P = proteus::jit_variable(Pointer)] { + [X = proteus::jit_variable(Value), P = proteus::jit_variable(Pointer)] { *P = X; return *P; }); From 421c6e00ecf4d3f85bcb489a95e56d762bd2b87d Mon Sep 17 00:00:00 2001 From: John Bowen Date: Thu, 1 Oct 2026 11:33:10 -0700 Subject: [PATCH 03/11] fix test harness --- tests/ast/CMakeLists.txt | 4 +--- tests/ast/lambda_invalid.cpp | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/ast/CMakeLists.txt b/tests/ast/CMakeLists.txt index 3ecd04deb..359717d96 100644 --- a/tests/ast/CMakeLists.txt +++ b/tests/ast/CMakeLists.txt @@ -8,11 +8,9 @@ config.environment = os.environ.copy() config.suffixes = ['.cpp'] config.test_source_root = '${CMAKE_CURRENT_SOURCE_DIR}' config.test_exec_root = '${CMAKE_CURRENT_BINARY_DIR}' -config.substitutions.append(('%not', '${LLVM_TOOLS_BINARY_DIR}/not')) config.substitutions.append(('%clang', '${LLVM_TOOLS_BINARY_DIR}/clang++')) config.substitutions.append(('%plugin', '$')) config.substitutions.append(('%proteus_include', '${PROJECT_SOURCE_DIR}/include')) -config.substitutions.append(('%FILECHECK', '${FILECHECK}')) ") # Exercise the same add_proteus() path used by installed CMake consumers, in @@ -21,5 +19,5 @@ add_library(proteus_ast_checker_cmake_test OBJECT lambda_valid.cpp) add_proteus(proteus_ast_checker_cmake_test) add_test(NAME lambda_ast_checker - COMMAND ${LIT} -vv -D FILECHECK=${FILECHECK} .) + COMMAND ${LIT} -vv .) set_tests_properties(lambda_ast_checker PROPERTIES LABELS "ast") diff --git a/tests/ast/lambda_invalid.cpp b/tests/ast/lambda_invalid.cpp index e23f1da88..527acc2a0 100644 --- a/tests/ast/lambda_invalid.cpp +++ b/tests/ast/lambda_invalid.cpp @@ -1,47 +1,50 @@ // clang-format off -// RUN: %not %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s 2>&1 | %FILECHECK %s +// RUN: %clang -std=c++17 -fsyntax-only -Xclang -verify -I%proteus_include -fplugin=%plugin %s +// clang-format on #include +// Double registration also reaches the library's existing invariant guard. +// expected-error@* {{static assertion failed}} +// expected-note@* 2 {{in instantiation of function template specialization}} + struct Functor { void operator()() const {} }; void invalid_register() { + // expected-error@+1 {{requires a lambda closure object}} (void)proteus::register_lambda(Functor{}); - // CHECK-DAG: error: proteus::register_lambda requires a lambda closure object } void duplicate_register() { auto Registered = proteus::register_lambda([] {}); + // expected-error@+1 {{cannot be registered again}} (void)proteus::register_lambda(Registered); - // CHECK-DAG: error: a lambda returned by proteus::register_lambda cannot be registered again } void invalid_use(int Value) { + // expected-error@+1 {{must be used directly as a lambda init-capture}} (void)proteus::jit_variable(Value); - // CHECK-DAG: error: proteus::jit_variable must be used directly as a lambda init-capture initializer } void unregistered(int Value) { + // expected-error@+1 {{must be registered with proteus::register_lambda}} auto Lambda = [X = proteus::jit_variable(Value)] { return X; }; - // CHECK-DAG: error: lambda containing proteus::jit_variable must be registered with proteus::register_lambda (void)Lambda; } -// clang-format on void unsupported_type(short Value) { + // expected-error@+2 {{does not support capture type 'short'}} auto Lambda = proteus::register_lambda( [X = proteus::jit_variable(Value)] { return X; }); - // CHECK-DAG: error: proteus::jit_variable does not support capture type - // 'short' (void)Lambda; } void mutate_capture(int Value) { + // expected-error@+2 {{capture must remain read-only}} auto Lambda = proteus::register_lambda( [X = proteus::jit_variable(Value)]() mutable { return ++X; }); - // CHECK-DAG: error: proteus::jit_variable capture must remain read-only (void)Lambda; } @@ -50,7 +53,9 @@ void mutate_by_reference(int &Value) { ++Value; } void escape_capture(int Value) { auto Lambda = proteus::register_lambda([X = proteus::jit_variable(Value)]() mutable { + // expected-error@+1 {{capture must remain read-only}} mutate_by_reference(X); + // expected-error@+1 {{capture must remain read-only}} int *Alias = &X; return *Alias; }); From 12ff1fe6f59be47a1f123c9ddc06cd0e617627e1 Mon Sep 17 00:00:00 2001 From: John Bowen Date: Wed, 30 Sep 2026 15:31:56 -0700 Subject: [PATCH 04/11] init clang frontend support --- cmake/ProteusFunctions.cmake | 4 + docs/user/integration.md | 7 +- src/pass/CMakeLists.txt | 16 +- src/pass/LambdaASTChecker.cpp | 338 ++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 5 +- tests/ast/CMakeLists.txt | 24 +++ tests/ast/lambda_invalid.cpp | 57 ++++++ tests/ast/lambda_valid.cpp | 29 +++ 8 files changed, 475 insertions(+), 5 deletions(-) create mode 100644 src/pass/LambdaASTChecker.cpp create mode 100644 tests/ast/CMakeLists.txt create mode 100644 tests/ast/lambda_invalid.cpp create mode 100644 tests/ast/lambda_valid.cpp diff --git a/cmake/ProteusFunctions.cmake b/cmake/ProteusFunctions.cmake index fd35e2928..84374367a 100644 --- a/cmake/ProteusFunctions.cmake +++ b/cmake/ProteusFunctions.cmake @@ -9,13 +9,17 @@ function(add_proteus target) "-fplugin=$" "-fpass-plugin=$" "SHELL:-Xclang -mllvm -Xclang -force-proteus-jit-annotate-all" + "-fplugin=\$" ) else() target_compile_options(${target} PRIVATE "-fpass-plugin=\$" + "-fplugin=\$" ) endif() + add_dependencies(${target} ProteusPass ProteusASTChecker) + target_link_options(${target} PRIVATE "SHELL:\$<\$:-Xoffload-linker --load-pass-plugin=\$>") diff --git a/docs/user/integration.md b/docs/user/integration.md index beac6fcf2..61162d927 100644 --- a/docs/user/integration.md +++ b/docs/user/integration.md @@ -36,6 +36,7 @@ linking flags, for example: ```bash CXXFLAGS += -I/include \ + -fplugin=//libProteusASTChecker.so \ -fpass-plugin=//libProteusPass.so LDFLAGS += -L/ \ @@ -43,8 +44,10 @@ LDFLAGS += -L/ \ -lproteus $(llvm-config --libs) -lclang-cpp ``` -If you don't use code annotations, you can omit the `-fpass-plugin` option, -since the LLVM pass is only needed for processing annotations. +The frontend checker loaded by `-fplugin` diagnoses unsupported uses of +`proteus::register_lambda` and `proteus::jit_variable` before LLVM IR is +emitted. If you don't use code annotations or these lambda APIs, you can omit +both plugin options. !!! note The example above is the simplest case and is closest to a host-only build. diff --git a/src/pass/CMakeLists.txt b/src/pass/CMakeLists.txt index d37e7a3f2..2b270d7cb 100644 --- a/src/pass/CMakeLists.txt +++ b/src/pass/CMakeLists.txt @@ -48,6 +48,18 @@ add_library(ProteusPassOffload SHARED $ ) +# Keep the Clang frontend plugin separate from the LLVM pass plugin: the pass +# is also loaded by opt/offload linkers, which do not provide Clang symbols. +add_library(ProteusASTChecker SHARED LambdaASTChecker.cpp) +target_include_directories(ProteusASTChecker SYSTEM PRIVATE + ${LLVM_INCLUDE_DIRS} + ${CLANG_INCLUDE_DIRS}) +if(NOT LLVM_ENABLE_RTTI) + target_compile_options(ProteusASTChecker PRIVATE -fno-rtti) +endif() +target_link_options(ProteusASTChecker + PRIVATE "$<$:LINKER:SHELL:-undefined dynamic_lookup>") + # Allow undefined symbols in shared objects on Darwin (this is the default # behaviour on Linux) target_link_options(ProteusPass @@ -72,7 +84,7 @@ target_link_options( "SHELL:$<$:-Xoffload-linker --load-pass-plugin=$>" ) -set_target_properties(ProteusPass ProteusPassOffload PROPERTIES +set_target_properties(ProteusPass ProteusPassOffload ProteusASTChecker PROPERTIES # on macOS enable passing RPATH flags to the linker; no-ops on Linux MACOSX_RPATH ON # let CMake use INSTALL_RPATH even in the build tree @@ -85,7 +97,7 @@ set_target_properties(ProteusPass ProteusPassOffload PROPERTIES # is ready to support Clang/LLVM pass consumers too. if(NOT PROTEUS_PYTHON_WHEEL) install( - TARGETS ProteusPass ProteusPassOffload + TARGETS ProteusPass ProteusPassOffload ProteusASTChecker EXPORT proteusTargets RUNTIME DESTINATION "${CMAKE_INSTALL_LIBDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" diff --git a/src/pass/LambdaASTChecker.cpp b/src/pass/LambdaASTChecker.cpp new file mode 100644 index 000000000..56fd98a38 --- /dev/null +++ b/src/pass/LambdaASTChecker.cpp @@ -0,0 +1,338 @@ +//===-- LambdaASTChecker.cpp - Validate Proteus lambda API use -----------===// +// +// Part of the Proteus Project, under the Apache License v2.0 with LLVM +// Exceptions. See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/AST/ASTConsumer.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/DeclCXX.h" +#include "clang/AST/ExprCXX.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Frontend/FrontendPluginRegistry.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include +#include +#include + +namespace { + +bool isProteusFunction(const clang::CallExpr *Call, llvm::StringRef Name) { + const clang::FunctionDecl *Callee = Call->getDirectCallee(); + return Callee && Callee->getQualifiedNameAsString() == Name; +} + +const clang::Expr *ignoreTransparentExprs(const clang::Expr *Expr) { + while (Expr) { + const clang::Expr *Next = Expr->IgnoreParens(); + if (Next != Expr) { + Expr = Next; + continue; + } + if (const auto *E = llvm::dyn_cast(Expr)) + Next = E->getSubExpr(); + else if (const auto *E = llvm::dyn_cast(Expr)) + Next = E->getSubExpr(); + else if (const auto *E = + llvm::dyn_cast(Expr)) + Next = E->getSubExpr(); + else if (const auto *E = llvm::dyn_cast(Expr)) + Next = E->getSubExpr(); + if (Next == Expr) + break; + Expr = Next; + } + return Expr; +} + +const clang::CXXRecordDecl *getCanonicalRecord(clang::QualType Type) { + Type = Type.getNonReferenceType().getUnqualifiedType(); + if (const auto *Record = Type->getAsCXXRecordDecl()) + return Record->getCanonicalDecl(); + return nullptr; +} + +bool isLambdaFunctorWrapper(const clang::CXXRecordDecl *Record) { + const auto *Specialization = + llvm::dyn_cast_or_null(Record); + return Specialization && + Specialization->getSpecializedTemplate()->getQualifiedNameAsString() == + "proteus::detail::LambdaFunctorWrapper"; +} + +bool directlyReferencesTarget( + const clang::Expr *Expr, + const llvm::DenseSet &Targets) { + Expr = ignoreTransparentExprs(Expr); + const auto *Reference = llvm::dyn_cast_or_null(Expr); + const auto *Variable = + Reference ? llvm::dyn_cast(Reference->getDecl()) : nullptr; + return Variable && Targets.contains(Variable->getCanonicalDecl()); +} + +bool takesAddressOfTarget( + const clang::Expr *Expr, + const llvm::DenseSet &Targets) { + Expr = ignoreTransparentExprs(Expr); + const auto *Operator = llvm::dyn_cast_or_null(Expr); + return Operator && Operator->getOpcode() == clang::UO_AddrOf && + directlyReferencesTarget(Operator->getSubExpr(), Targets); +} + +bool isMutableReference(clang::QualType Type) { + if (!Type->isReferenceType()) + return false; + return !Type.getNonReferenceType().isConstQualified(); +} + +bool isMutablePointer(clang::QualType Type) { + return Type->isPointerType() && !Type->getPointeeType().isConstQualified(); +} + +class CaptureMutationVisitor + : public clang::RecursiveASTVisitor { +public: + CaptureMutationVisitor( + const llvm::DenseSet &Targets, + llvm::SmallVectorImpl &Mutations) + : Targets(Targets), Mutations(Mutations) {} + + bool VisitBinaryOperator(clang::BinaryOperator *Operator) { + if (Operator->isAssignmentOp() && + directlyReferencesTarget(Operator->getLHS(), Targets)) + Mutations.push_back(Operator->getSourceRange()); + return true; + } + + bool VisitUnaryOperator(clang::UnaryOperator *Operator) { + if (Operator->isIncrementDecrementOp() && + directlyReferencesTarget(Operator->getSubExpr(), Targets)) + Mutations.push_back(Operator->getSourceRange()); + return true; + } + + bool VisitVarDecl(clang::VarDecl *Variable) { + if (!Variable->hasInit()) + return true; + clang::QualType Type = Variable->getType(); + if (isMutableReference(Type) && + directlyReferencesTarget(Variable->getInit(), Targets)) + Mutations.push_back(Variable->getSourceRange()); + else if (isMutablePointer(Type) && + takesAddressOfTarget(Variable->getInit(), Targets)) + Mutations.push_back(Variable->getSourceRange()); + return true; + } + + bool VisitCallExpr(clang::CallExpr *Call) { + const clang::FunctionDecl *Callee = Call->getDirectCallee(); + if (!Callee) + return true; + const unsigned Count = std::min(Call->getNumArgs(), Callee->getNumParams()); + for (unsigned I = 0; I < Count; ++I) { + const clang::Expr *Argument = Call->getArg(I); + clang::QualType ParameterType = Callee->getParamDecl(I)->getType(); + if ((isMutableReference(ParameterType) && + directlyReferencesTarget(Argument, Targets)) || + (isMutablePointer(ParameterType) && + takesAddressOfTarget(Argument, Targets))) + Mutations.push_back(Argument->getSourceRange()); + } + return true; + } + +private: + const llvm::DenseSet &Targets; + llvm::SmallVectorImpl &Mutations; +}; + +class LambdaVisitor : public clang::RecursiveASTVisitor { +public: + bool shouldVisitTemplateInstantiations() const { return true; } + + bool VisitLambdaExpr(clang::LambdaExpr *Lambda) { + Lambdas.push_back(Lambda); + return true; + } + + bool VisitCallExpr(clang::CallExpr *Call) { + if (isProteusFunction(Call, "proteus::jit_variable")) + JitVariableCalls.push_back(Call); + else if (isProteusFunction(Call, "proteus::register_lambda") && + Call->getNumArgs() != 0 && !Call->getArg(0)->isTypeDependent()) { + RegisterLambdaCalls.push_back(Call); + const clang::Expr *Argument = ignoreTransparentExprs(Call->getArg(0)); + if (const auto *Lambda = llvm::dyn_cast(Argument)) { + DirectlyRegisteredLambdas.insert(Lambda); + DirectlyRegisteredLambdaLocations.insert( + Lambda->getBeginLoc().getRawEncoding()); + } + if (const auto *Record = getCanonicalRecord(Argument->getType()); + Record && Record->isLambda()) + RegisteredLambdaTypes.insert(Record); + } + return true; + } + + llvm::SmallVector Lambdas; + llvm::SmallVector JitVariableCalls; + llvm::SmallVector RegisterLambdaCalls; + llvm::DenseSet DirectlyRegisteredLambdas; + llvm::DenseSet DirectlyRegisteredLambdaLocations; + llvm::DenseSet RegisteredLambdaTypes; +}; + +class LambdaASTConsumer : public clang::ASTConsumer { +public: + explicit LambdaASTConsumer(clang::CompilerInstance &CI) : CI(CI) {} + + void HandleTranslationUnit(clang::ASTContext &Context) override { + LambdaVisitor Visitor; + Visitor.TraverseDecl(Context.getTranslationUnitDecl()); + + llvm::DenseMap + CaptureInitializers; + llvm::DenseMap CaptureTypes; + llvm::DenseMap> + JitCaptureVariables; + for (const clang::LambdaExpr *Lambda : Visitor.Lambdas) { + auto Init = Lambda->capture_init_begin(); + for (const clang::LambdaCapture &Capture : Lambda->captures()) { + const clang::Expr *CaptureInit = *Init++; + if (!Capture.capturesVariable() || + !Capture.getCapturedVar()->isInitCapture()) + continue; + clang::QualType CaptureType = CaptureInit->getType(); + CaptureInit = ignoreTransparentExprs(CaptureInit); + const auto *Call = llvm::dyn_cast_or_null(CaptureInit); + if (Call && isProteusFunction(Call, "proteus::jit_variable")) { + CaptureInitializers.try_emplace(Call, Lambda); + CaptureTypes.try_emplace(Call, CaptureType); + JitCaptureVariables[Lambda].push_back( + llvm::cast( + Capture.getCapturedVar()->getCanonicalDecl())); + } + } + } + + clang::DiagnosticsEngine &Diagnostics = CI.getDiagnostics(); + const unsigned InvalidUse = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "proteus::jit_variable must be used directly as a lambda " + "init-capture initializer"); + const unsigned Unregistered = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "lambda containing proteus::jit_variable must be registered with " + "proteus::register_lambda"); + const unsigned UnsupportedType = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "proteus::jit_variable does not support capture type %0"); + const unsigned InvalidRegisterArgument = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "proteus::register_lambda requires a lambda closure object"); + const unsigned AlreadyRegistered = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "a lambda returned by proteus::register_lambda cannot be registered " + "again"); + const unsigned MutatedCapture = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "proteus::jit_variable capture must remain read-only"); + + for (const clang::CallExpr *Call : Visitor.RegisterLambdaCalls) { + const clang::Expr *Argument = Call->getArg(0); + const auto *Record = getCanonicalRecord(Argument->getType()); + if (isLambdaFunctorWrapper(Record)) + Diagnostics.Report(Argument->getExprLoc(), AlreadyRegistered) + << Argument->getSourceRange(); + else if (!Record || !Record->isLambda()) + Diagnostics.Report(Argument->getExprLoc(), InvalidRegisterArgument) + << Argument->getSourceRange(); + } + + for (const auto &Entry : JitCaptureVariables) { + llvm::DenseSet Targets; + Targets.insert(Entry.second.begin(), Entry.second.end()); + llvm::SmallVector Mutations; + CaptureMutationVisitor MutationVisitor(Targets, Mutations); + MutationVisitor.TraverseStmt(Entry.first->getBody()); + for (clang::SourceRange Range : Mutations) + Diagnostics.Report(Range.getBegin(), MutatedCapture) << Range; + } + + for (const clang::CallExpr *Call : Visitor.JitVariableCalls) { + // Dependent template patterns are checked when their specializations are + // instantiated, once both the closure and captured value types are known. + if (Call->isTypeDependent()) + continue; + + auto CaptureIt = CaptureInitializers.find(Call); + if (CaptureIt == CaptureInitializers.end()) { + Diagnostics.Report(Call->getExprLoc(), InvalidUse) + << Call->getSourceRange(); + continue; + } + + const clang::LambdaExpr *Lambda = CaptureIt->second; + const auto *LambdaType = Lambda->getLambdaClass()->getCanonicalDecl(); + if (!Visitor.DirectlyRegisteredLambdas.contains(Lambda) && + !Visitor.DirectlyRegisteredLambdaLocations.contains( + Lambda->getBeginLoc().getRawEncoding()) && + !Visitor.RegisteredLambdaTypes.contains(LambdaType)) { + Diagnostics.Report(Call->getExprLoc(), Unregistered) + << Lambda->getSourceRange(); + } + + clang::QualType Type = CaptureTypes.lookup(Call).getUnqualifiedType(); + if (!isSupportedRuntimeConstantType(Type, Context)) + Diagnostics.Report(Call->getExprLoc(), UnsupportedType) + << Type << Call->getSourceRange(); + } + } + +private: + static bool isSupportedRuntimeConstantType(clang::QualType Type, + clang::ASTContext &Context) { + if (Type->isPointerType() || Type->isBooleanType() || + (Type->isRealFloatingType() && + (Type->isSpecificBuiltinType(clang::BuiltinType::Float) || + Type->isSpecificBuiltinType(clang::BuiltinType::Double)))) + return true; + + if (!Type->isIntegralOrEnumerationType()) + return false; + const uint64_t Width = Context.getTypeSize(Type); + return Width == 8 || Width == 32 || Width == 64; + } + + clang::CompilerInstance &CI; +}; + +class LambdaASTCheckerAction : public clang::PluginASTAction { +protected: + std::unique_ptr + CreateASTConsumer(clang::CompilerInstance &CI, llvm::StringRef) override { + return std::make_unique(CI); + } + + bool ParseArgs(const clang::CompilerInstance &, + const std::vector &) override { + return true; + } + + ActionType getActionType() override { return AddBeforeMainAction; } +}; + +} // namespace + +static clang::FrontendPluginRegistry::Add + X("proteus-lambda-checker", "validate Proteus lambda API usage"); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 967c7adc2..3c37a4e33 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -59,7 +59,7 @@ function(proteus_attach_pass_plugin_rebuild_dep target) add_custom_command( OUTPUT "${proteus_pass_stamp}" COMMAND ${CMAKE_COMMAND} -E touch "${proteus_pass_stamp}" - DEPENDS ProteusPass + DEPENDS ProteusPass ProteusASTChecker VERBATIM ) @@ -70,10 +70,13 @@ function(proteus_attach_pass_plugin_rebuild_dep target) ${target} PUBLIC -fpass-plugin=$ + -fplugin=$ ) + add_dependencies(${target} ProteusASTChecker) endfunction() add_subdirectory(cpu) +add_subdirectory(ast) if(PROTEUS_ENABLE_HIP OR PROTEUS_ENABLE_CUDA) add_subdirectory(gpu) diff --git a/tests/ast/CMakeLists.txt b/tests/ast/CMakeLists.txt new file mode 100644 index 000000000..00cc91a72 --- /dev/null +++ b/tests/ast/CMakeLists.txt @@ -0,0 +1,24 @@ +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/lit.cfg.py CONTENT " +import lit.formats +import os + +config.name = 'Proteus AST checker tests' +config.test_format = lit.formats.ShTest(True) +config.environment = os.environ.copy() +config.suffixes = ['.cpp'] +config.test_source_root = '${CMAKE_CURRENT_SOURCE_DIR}' +config.test_exec_root = '${CMAKE_CURRENT_BINARY_DIR}' +config.substitutions.append(('%clang', '${LLVM_TOOLS_BINARY_DIR}/clang++')) +config.substitutions.append(('%plugin', '$')) +config.substitutions.append(('%proteus_include', '${PROJECT_SOURCE_DIR}/include')) +config.substitutions.append(('%FILECHECK', '${FILECHECK}')) +") + +# Exercise the same add_proteus() path used by installed CMake consumers, in +# addition to the direct driver invocations in the lit tests below. +add_library(proteus_ast_checker_cmake_test OBJECT lambda_valid.cpp) +add_proteus(proteus_ast_checker_cmake_test) + +add_test(NAME lambda_ast_checker + COMMAND ${LIT} -vv -D FILECHECK=${FILECHECK} .) +set_tests_properties(lambda_ast_checker PROPERTIES LABELS "ast") diff --git a/tests/ast/lambda_invalid.cpp b/tests/ast/lambda_invalid.cpp new file mode 100644 index 000000000..08a6a4717 --- /dev/null +++ b/tests/ast/lambda_invalid.cpp @@ -0,0 +1,57 @@ +// clang-format off +// RUN: not %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s 2>&1 | %FILECHECK %s + +#include + +struct Functor { + void operator()() const {} +}; + +void invalid_register() { + (void)proteus::register_lambda(Functor{}); + // CHECK-DAG: error: proteus::register_lambda requires a lambda closure object +} + +void duplicate_register() { + auto Registered = proteus::register_lambda([] {}); + (void)proteus::register_lambda(Registered); + // CHECK-DAG: error: a lambda returned by proteus::register_lambda cannot be registered again +} + +void invalid_use(int Value) { + (void)proteus::jit_variable(Value); + // CHECK-DAG: error: proteus::jit_variable must be used directly as a lambda init-capture initializer +} + +void unregistered(int Value) { + auto Lambda = [X = proteus::jit_variable(Value)] { return X; }; + // CHECK-DAG: error: lambda containing proteus::jit_variable must be registered with proteus::register_lambda + (void)Lambda; +} +// clang-format on + +void unsupported_type(short Value) { + auto Lambda = proteus::register_lambda( + [X = proteus::jit_variable(Value)] { return X; }); + // CHECK-DAG: error: proteus::jit_variable does not support capture type 'short' + (void)Lambda; +} + +void mutate_capture(int Value) { + auto Lambda = proteus::register_lambda( + [X = proteus::jit_variable(Value)]() mutable { return ++X; }); + // CHECK-DAG: error: proteus::jit_variable capture must remain read-only + (void)Lambda; +} + +void mutate_by_reference(int &Value) { ++Value; } + +void escape_capture(int Value) { + auto Lambda = proteus::register_lambda( + [X = proteus::jit_variable(Value)]() mutable { + mutate_by_reference(X); + int *Alias = &X; + return *Alias; + }); + (void)Lambda; +} diff --git a/tests/ast/lambda_valid.cpp b/tests/ast/lambda_valid.cpp new file mode 100644 index 000000000..4f56f70a8 --- /dev/null +++ b/tests/ast/lambda_valid.cpp @@ -0,0 +1,29 @@ +// RUN: %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s + +#include + +template void register_indirectly(F &&Fn) { + proteus::register_lambda(Fn)(); +} + +auto make_lambda(int Value) { + return [X = proteus::jit_variable(Value)] { return X; }; +} + +template void direct_template(int Value) { + auto Lambda = proteus::register_lambda( + [=, X = proteus::jit_variable(Value)] { return X + Offset; }); + (void)Lambda; +} + +void valid(int Value, int *Pointer) { + register_indirectly( + [X = proteus::jit_variable(Value), + P = proteus::jit_variable(Pointer)] { + *P = X; + return *P; + }); + auto Lambda = proteus::register_lambda(make_lambda(Value)); + (void)Lambda; + direct_template<1>(Value); +} From 963d595f348f0b051e60f759bb0c755947ba1913 Mon Sep 17 00:00:00 2001 From: John Bowen Date: Thu, 1 Oct 2026 11:06:45 -0700 Subject: [PATCH 05/11] Clang format and fix test harness failure --- src/pass/LambdaASTChecker.cpp | 13 ++++++------- tests/ast/CMakeLists.txt | 1 + tests/ast/lambda_invalid.cpp | 9 +++++---- tests/ast/lambda_valid.cpp | 3 +-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pass/LambdaASTChecker.cpp b/src/pass/LambdaASTChecker.cpp index 56fd98a38..b1ef1b044 100644 --- a/src/pass/LambdaASTChecker.cpp +++ b/src/pass/LambdaASTChecker.cpp @@ -75,7 +75,8 @@ bool directlyReferencesTarget( Expr = ignoreTransparentExprs(Expr); const auto *Reference = llvm::dyn_cast_or_null(Expr); const auto *Variable = - Reference ? llvm::dyn_cast(Reference->getDecl()) : nullptr; + Reference ? llvm::dyn_cast(Reference->getDecl()) + : nullptr; return Variable && Targets.contains(Variable->getCanonicalDecl()); } @@ -101,9 +102,8 @@ bool isMutablePointer(clang::QualType Type) { class CaptureMutationVisitor : public clang::RecursiveASTVisitor { public: - CaptureMutationVisitor( - const llvm::DenseSet &Targets, - llvm::SmallVectorImpl &Mutations) + CaptureMutationVisitor(const llvm::DenseSet &Targets, + llvm::SmallVectorImpl &Mutations) : Targets(Targets), Mutations(Mutations) {} bool VisitBinaryOperator(clang::BinaryOperator *Operator) { @@ -218,9 +218,8 @@ class LambdaASTConsumer : public clang::ASTConsumer { if (Call && isProteusFunction(Call, "proteus::jit_variable")) { CaptureInitializers.try_emplace(Call, Lambda); CaptureTypes.try_emplace(Call, CaptureType); - JitCaptureVariables[Lambda].push_back( - llvm::cast( - Capture.getCapturedVar()->getCanonicalDecl())); + JitCaptureVariables[Lambda].push_back(llvm::cast( + Capture.getCapturedVar()->getCanonicalDecl())); } } } diff --git a/tests/ast/CMakeLists.txt b/tests/ast/CMakeLists.txt index 00cc91a72..3ecd04deb 100644 --- a/tests/ast/CMakeLists.txt +++ b/tests/ast/CMakeLists.txt @@ -8,6 +8,7 @@ config.environment = os.environ.copy() config.suffixes = ['.cpp'] config.test_source_root = '${CMAKE_CURRENT_SOURCE_DIR}' config.test_exec_root = '${CMAKE_CURRENT_BINARY_DIR}' +config.substitutions.append(('%not', '${LLVM_TOOLS_BINARY_DIR}/not')) config.substitutions.append(('%clang', '${LLVM_TOOLS_BINARY_DIR}/clang++')) config.substitutions.append(('%plugin', '$')) config.substitutions.append(('%proteus_include', '${PROJECT_SOURCE_DIR}/include')) diff --git a/tests/ast/lambda_invalid.cpp b/tests/ast/lambda_invalid.cpp index 08a6a4717..e23f1da88 100644 --- a/tests/ast/lambda_invalid.cpp +++ b/tests/ast/lambda_invalid.cpp @@ -1,5 +1,5 @@ // clang-format off -// RUN: not %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s 2>&1 | %FILECHECK %s +// RUN: %not %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s 2>&1 | %FILECHECK %s #include @@ -33,7 +33,8 @@ void unregistered(int Value) { void unsupported_type(short Value) { auto Lambda = proteus::register_lambda( [X = proteus::jit_variable(Value)] { return X; }); - // CHECK-DAG: error: proteus::jit_variable does not support capture type 'short' + // CHECK-DAG: error: proteus::jit_variable does not support capture type + // 'short' (void)Lambda; } @@ -47,8 +48,8 @@ void mutate_capture(int Value) { void mutate_by_reference(int &Value) { ++Value; } void escape_capture(int Value) { - auto Lambda = proteus::register_lambda( - [X = proteus::jit_variable(Value)]() mutable { + auto Lambda = + proteus::register_lambda([X = proteus::jit_variable(Value)]() mutable { mutate_by_reference(X); int *Alias = &X; return *Alias; diff --git a/tests/ast/lambda_valid.cpp b/tests/ast/lambda_valid.cpp index 4f56f70a8..68a469f12 100644 --- a/tests/ast/lambda_valid.cpp +++ b/tests/ast/lambda_valid.cpp @@ -18,8 +18,7 @@ template void direct_template(int Value) { void valid(int Value, int *Pointer) { register_indirectly( - [X = proteus::jit_variable(Value), - P = proteus::jit_variable(Pointer)] { + [X = proteus::jit_variable(Value), P = proteus::jit_variable(Pointer)] { *P = X; return *P; }); From fc8f1018655dfef5827ca5761154ed6968576b77 Mon Sep 17 00:00:00 2001 From: John Bowen Date: Thu, 1 Oct 2026 11:33:10 -0700 Subject: [PATCH 06/11] fix test harness --- tests/ast/CMakeLists.txt | 4 +--- tests/ast/lambda_invalid.cpp | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/ast/CMakeLists.txt b/tests/ast/CMakeLists.txt index 3ecd04deb..359717d96 100644 --- a/tests/ast/CMakeLists.txt +++ b/tests/ast/CMakeLists.txt @@ -8,11 +8,9 @@ config.environment = os.environ.copy() config.suffixes = ['.cpp'] config.test_source_root = '${CMAKE_CURRENT_SOURCE_DIR}' config.test_exec_root = '${CMAKE_CURRENT_BINARY_DIR}' -config.substitutions.append(('%not', '${LLVM_TOOLS_BINARY_DIR}/not')) config.substitutions.append(('%clang', '${LLVM_TOOLS_BINARY_DIR}/clang++')) config.substitutions.append(('%plugin', '$')) config.substitutions.append(('%proteus_include', '${PROJECT_SOURCE_DIR}/include')) -config.substitutions.append(('%FILECHECK', '${FILECHECK}')) ") # Exercise the same add_proteus() path used by installed CMake consumers, in @@ -21,5 +19,5 @@ add_library(proteus_ast_checker_cmake_test OBJECT lambda_valid.cpp) add_proteus(proteus_ast_checker_cmake_test) add_test(NAME lambda_ast_checker - COMMAND ${LIT} -vv -D FILECHECK=${FILECHECK} .) + COMMAND ${LIT} -vv .) set_tests_properties(lambda_ast_checker PROPERTIES LABELS "ast") diff --git a/tests/ast/lambda_invalid.cpp b/tests/ast/lambda_invalid.cpp index e23f1da88..527acc2a0 100644 --- a/tests/ast/lambda_invalid.cpp +++ b/tests/ast/lambda_invalid.cpp @@ -1,47 +1,50 @@ // clang-format off -// RUN: %not %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s 2>&1 | %FILECHECK %s +// RUN: %clang -std=c++17 -fsyntax-only -Xclang -verify -I%proteus_include -fplugin=%plugin %s +// clang-format on #include +// Double registration also reaches the library's existing invariant guard. +// expected-error@* {{static assertion failed}} +// expected-note@* 2 {{in instantiation of function template specialization}} + struct Functor { void operator()() const {} }; void invalid_register() { + // expected-error@+1 {{requires a lambda closure object}} (void)proteus::register_lambda(Functor{}); - // CHECK-DAG: error: proteus::register_lambda requires a lambda closure object } void duplicate_register() { auto Registered = proteus::register_lambda([] {}); + // expected-error@+1 {{cannot be registered again}} (void)proteus::register_lambda(Registered); - // CHECK-DAG: error: a lambda returned by proteus::register_lambda cannot be registered again } void invalid_use(int Value) { + // expected-error@+1 {{must be used directly as a lambda init-capture}} (void)proteus::jit_variable(Value); - // CHECK-DAG: error: proteus::jit_variable must be used directly as a lambda init-capture initializer } void unregistered(int Value) { + // expected-error@+1 {{must be registered with proteus::register_lambda}} auto Lambda = [X = proteus::jit_variable(Value)] { return X; }; - // CHECK-DAG: error: lambda containing proteus::jit_variable must be registered with proteus::register_lambda (void)Lambda; } -// clang-format on void unsupported_type(short Value) { + // expected-error@+2 {{does not support capture type 'short'}} auto Lambda = proteus::register_lambda( [X = proteus::jit_variable(Value)] { return X; }); - // CHECK-DAG: error: proteus::jit_variable does not support capture type - // 'short' (void)Lambda; } void mutate_capture(int Value) { + // expected-error@+2 {{capture must remain read-only}} auto Lambda = proteus::register_lambda( [X = proteus::jit_variable(Value)]() mutable { return ++X; }); - // CHECK-DAG: error: proteus::jit_variable capture must remain read-only (void)Lambda; } @@ -50,7 +53,9 @@ void mutate_by_reference(int &Value) { ++Value; } void escape_capture(int Value) { auto Lambda = proteus::register_lambda([X = proteus::jit_variable(Value)]() mutable { + // expected-error@+1 {{capture must remain read-only}} mutate_by_reference(X); + // expected-error@+1 {{capture must remain read-only}} int *Alias = &X; return *Alias; }); From 0af4a1252502ac2ed9e27b605279f55a56703782 Mon Sep 17 00:00:00 2001 From: Zane Fink Date: Thu, 1 Oct 2026 14:01:20 -0700 Subject: [PATCH 07/11] Add AST checker tests for review-found lambda bugs --- tests/ast/lambda_invalid_captures.cpp | 66 +++++++++++++++++++++++++++ tests/ast/lambda_valid_captures.cpp | 25 ++++++++++ tests/ast/lambda_valid_templates.cpp | 34 ++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 tests/ast/lambda_invalid_captures.cpp create mode 100644 tests/ast/lambda_valid_captures.cpp create mode 100644 tests/ast/lambda_valid_templates.cpp diff --git a/tests/ast/lambda_invalid_captures.cpp b/tests/ast/lambda_invalid_captures.cpp new file mode 100644 index 000000000..733e58bbb --- /dev/null +++ b/tests/ast/lambda_invalid_captures.cpp @@ -0,0 +1,66 @@ +// clang-format off +// RUN: %clang -std=c++17 -fsyntax-only -Xclang -verify -I%proteus_include -fplugin=%plugin %s +// clang-format on + +#include + +struct Accumulator { + void operator+=(int &Value) { ++Value; } +}; + +struct IntRef { + explicit IntRef(int &Ref) : Ref(Ref) {} + int &Ref; +}; + +void mutate_through_member_operator(int Value) { + auto Lambda = + proteus::register_lambda([X = proteus::jit_variable(Value)]() mutable { + Accumulator Acc; + // expected-error@+1 {{capture must remain read-only}} + Acc += X; + return X; + }); + (void)Lambda; +} + +void mutate_through_dereferenced_address(int Value) { + auto Lambda = + proteus::register_lambda([X = proteus::jit_variable(Value)]() mutable { + // expected-error@+1 {{capture must remain read-only}} + *&X = 1; + return X; + }); + (void)Lambda; +} + +void mutate_through_assigned_pointer(int Value) { + auto Lambda = + proteus::register_lambda([X = proteus::jit_variable(Value)]() mutable { + int *Alias; + // expected-error@+1 {{capture must remain read-only}} + Alias = &X; + *Alias = 2; + return X; + }); + (void)Lambda; +} + +void mutate_through_constructor_reference(int Value) { + auto Lambda = + proteus::register_lambda([X = proteus::jit_variable(Value)]() mutable { + // expected-error@+1 {{capture must remain read-only}} + IntRef Ref(X); + Ref.Ref = 3; + return X; + }); + (void)Lambda; +} + +// The pass records the slot using jit_variable's own type, not the casted one. +void explicit_cast_of_jit_variable(int Value) { + auto Lambda = proteus::register_lambda( + // expected-error@+1 {{must be used directly as a lambda init-capture}} + [X = static_cast(proteus::jit_variable(Value))] { return X; }); + (void)Lambda; +} diff --git a/tests/ast/lambda_valid_captures.cpp b/tests/ast/lambda_valid_captures.cpp new file mode 100644 index 000000000..de8ecb606 --- /dev/null +++ b/tests/ast/lambda_valid_captures.cpp @@ -0,0 +1,25 @@ +// RUN: %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s + +#include + +void brace_init_capture(int Value) { + auto Lambda = + proteus::register_lambda([X{proteus::jit_variable(Value)}] { return X; }); + (void)Lambda; +} + +void long_double_capture(long double Value) { + auto Lambda = proteus::register_lambda( + [X = proteus::jit_variable(Value)] { return X; }); + (void)Lambda; +} + +// Calling a functor passes the functor itself as argument 0, so arguments and +// parameters must not be paired by index. +void const_ref_argument_to_functor(int Value) { + int Out = 0; + auto Copy = [](const int &From, int &To) { To = From; }; + auto Lambda = proteus::register_lambda( + [X = proteus::jit_variable(Value), &Out, Copy] { Copy(X, Out); }); + (void)Lambda; +} diff --git a/tests/ast/lambda_valid_templates.cpp b/tests/ast/lambda_valid_templates.cpp new file mode 100644 index 000000000..262e5b6c2 --- /dev/null +++ b/tests/ast/lambda_valid_templates.cpp @@ -0,0 +1,34 @@ +// RUN: %clang -std=c++17 -fsyntax-only -I%proteus_include -fplugin=%plugin %s + +#include + +// RAJA-style wrapper that registers the lambda it is given. +template void forall(int N, Body &&B) { + auto Registered = proteus::register_lambda(B); + for (int I = 0; I < N; ++I) + Registered(I); +} + +// The capture type does not depend on T, so the uninstantiated pattern is +// checked even though only the instantiation reaches register_lambda. +template void gather(T *Out, const T *In, int N, int Stride) { + forall(N, + [=, S = proteus::jit_variable(Stride)](int I) { Out[I] = In[I * S]; }); +} + +template struct Scaler { + void scale(T *Data, int N, int Factor) { + forall(N, [=, F = proteus::jit_variable(Factor)](int I) { Data[I] *= F; }); + } +}; + +template void never_instantiated(T *Out, int Value) { + auto Lambda = proteus::register_lambda( + [=, X = proteus::jit_variable(Value)] { Out[0] = X; }); + Lambda(); +} + +void valid(double *Out, const double *In, int N) { + gather(Out, In, N, 4); + Scaler().scale(Out, N, 2); +} From 9ebc1e4404163798c71760f45136bd743689d430 Mon Sep 17 00:00:00 2001 From: Zane Fink Date: Thu, 1 Oct 2026 15:05:51 -0700 Subject: [PATCH 08/11] Drop explicit-cast AST test since casting jit_variable is supported --- tests/ast/lambda_invalid_captures.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/ast/lambda_invalid_captures.cpp b/tests/ast/lambda_invalid_captures.cpp index 733e58bbb..4edccdc4d 100644 --- a/tests/ast/lambda_invalid_captures.cpp +++ b/tests/ast/lambda_invalid_captures.cpp @@ -56,11 +56,3 @@ void mutate_through_constructor_reference(int Value) { }); (void)Lambda; } - -// The pass records the slot using jit_variable's own type, not the casted one. -void explicit_cast_of_jit_variable(int Value) { - auto Lambda = proteus::register_lambda( - // expected-error@+1 {{must be used directly as a lambda init-capture}} - [X = static_cast(proteus::jit_variable(Value))] { return X; }); - (void)Lambda; -} From 4d1eaf78ebb946c240efafe8d08709fd49b92357 Mon Sep 17 00:00:00 2001 From: Zane Fink Date: Thu, 1 Oct 2026 15:05:53 -0700 Subject: [PATCH 09/11] Fix AST checker template, functor, brace-init, and long double handling --- src/pass/LambdaASTChecker.cpp | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/pass/LambdaASTChecker.cpp b/src/pass/LambdaASTChecker.cpp index b1ef1b044..9077d8c71 100644 --- a/src/pass/LambdaASTChecker.cpp +++ b/src/pass/LambdaASTChecker.cpp @@ -54,6 +54,14 @@ const clang::Expr *ignoreTransparentExprs(const clang::Expr *Expr) { return Expr; } +const clang::Expr *ignoreCaptureInitWrappers(const clang::Expr *Expr) { + Expr = ignoreTransparentExprs(Expr); + if (const auto *List = llvm::dyn_cast_or_null(Expr); + List && List->getNumInits() == 1) + return ignoreTransparentExprs(List->getInit(0)); + return Expr; +} + const clang::CXXRecordDecl *getCanonicalRecord(clang::QualType Type) { Type = Type.getNonReferenceType().getUnqualifiedType(); if (const auto *Record = Type->getAsCXXRecordDecl()) @@ -137,9 +145,18 @@ class CaptureMutationVisitor const clang::FunctionDecl *Callee = Call->getDirectCallee(); if (!Callee) return true; - const unsigned Count = std::min(Call->getNumArgs(), Callee->getNumParams()); + // Member operator calls pass the object as argument 0, ahead of the + // parameters. + const auto *Method = llvm::dyn_cast(Callee); + const unsigned FirstArgument = + llvm::isa(Call) && Method && + Method->isImplicitObjectMemberFunction() + ? 1 + : 0; + const unsigned Count = + std::min(Call->getNumArgs() - FirstArgument, Callee->getNumParams()); for (unsigned I = 0; I < Count; ++I) { - const clang::Expr *Argument = Call->getArg(I); + const clang::Expr *Argument = Call->getArg(I + FirstArgument); clang::QualType ParameterType = Callee->getParamDecl(I)->getType(); if ((isMutableReference(ParameterType) && directlyReferencesTarget(Argument, Targets)) || @@ -159,6 +176,15 @@ class LambdaVisitor : public clang::RecursiveASTVisitor { public: bool shouldVisitTemplateInstantiations() const { return true; } + // Only instantiations show which closure types reach register_lambda, so + // template patterns are skipped. + bool TraverseDecl(clang::Decl *Decl) { + if (const auto *Context = llvm::dyn_cast_or_null(Decl); + Context && Context->isDependentContext()) + return true; + return RecursiveASTVisitor::TraverseDecl(Decl); + } + bool VisitLambdaExpr(clang::LambdaExpr *Lambda) { Lambdas.push_back(Lambda); return true; @@ -213,7 +239,7 @@ class LambdaASTConsumer : public clang::ASTConsumer { !Capture.getCapturedVar()->isInitCapture()) continue; clang::QualType CaptureType = CaptureInit->getType(); - CaptureInit = ignoreTransparentExprs(CaptureInit); + CaptureInit = ignoreCaptureInitWrappers(CaptureInit); const auto *Call = llvm::dyn_cast_or_null(CaptureInit); if (Call && isProteusFunction(Call, "proteus::jit_variable")) { CaptureInitializers.try_emplace(Call, Lambda); @@ -304,7 +330,8 @@ class LambdaASTConsumer : public clang::ASTConsumer { if (Type->isPointerType() || Type->isBooleanType() || (Type->isRealFloatingType() && (Type->isSpecificBuiltinType(clang::BuiltinType::Float) || - Type->isSpecificBuiltinType(clang::BuiltinType::Double)))) + Type->isSpecificBuiltinType(clang::BuiltinType::Double) || + Type->isSpecificBuiltinType(clang::BuiltinType::LongDouble)))) return true; if (!Type->isIntegralOrEnumerationType()) From 30f698177b47b2fcc5a8b0c81b0c1bf0ef08603b Mon Sep 17 00:00:00 2001 From: John Bowen Date: Wed, 7 Oct 2026 12:18:19 -0700 Subject: [PATCH 10/11] Reject unsafe memory shapes for registered lambdas --- src/pass/LambdaASTChecker.cpp | 291 ++++++++++++++++++++++++++++ tests/ast/lambda_memory_invalid.cpp | 94 +++++++++ 2 files changed, 385 insertions(+) create mode 100644 tests/ast/lambda_memory_invalid.cpp diff --git a/src/pass/LambdaASTChecker.cpp b/src/pass/LambdaASTChecker.cpp index b1ef1b044..2c8275196 100644 --- a/src/pass/LambdaASTChecker.cpp +++ b/src/pass/LambdaASTChecker.cpp @@ -99,6 +99,214 @@ bool isMutablePointer(clang::QualType Type) { return Type->isPointerType() && !Type->getPointeeType().isConstQualified(); } +const clang::VarDecl *getDirectReferencedVariable(const clang::Expr *Expr) { + Expr = ignoreTransparentExprs(Expr); + const auto *Reference = llvm::dyn_cast_or_null(Expr); + return Reference ? llvm::dyn_cast(Reference->getDecl()) + : nullptr; +} + +const clang::VarDecl *getAddressedVariable(const clang::Expr *Expr) { + Expr = ignoreTransparentExprs(Expr); + const auto *Operator = llvm::dyn_cast_or_null(Expr); + if (!Operator || Operator->getOpcode() != clang::UO_AddrOf) + return nullptr; + return getDirectReferencedVariable(Operator->getSubExpr()); +} + +bool isPointerToLambdaFunctorWrapper(clang::QualType Type, + unsigned MinimumDepth = 1) { + unsigned Depth = 0; + Type = Type.getNonReferenceType(); + while (Type->isPointerType()) { + ++Depth; + Type = Type->getPointeeType(); + } + return Depth >= MinimumDepth && + isLambdaFunctorWrapper(getCanonicalRecord(Type)); +} + +class VariableReferenceVisitor + : public clang::RecursiveASTVisitor { +public: + explicit VariableReferenceVisitor(const clang::VarDecl *Target) + : Target(Target->getCanonicalDecl()) {} + + bool VisitDeclRefExpr(clang::DeclRefExpr *Reference) { + const auto *Variable = llvm::dyn_cast(Reference->getDecl()); + Found |= Variable && Variable->getCanonicalDecl() == Target; + return !Found; + } + + bool found() const { return Found; } + +private: + const clang::VarDecl *Target; + bool Found = false; +}; + +bool referencesVariable(const clang::Stmt *Statement, + const clang::VarDecl *Variable) { + VariableReferenceVisitor Visitor(Variable); + Visitor.TraverseStmt(const_cast(Statement)); + return Visitor.found(); +} + +class GlobalReferenceVisitor + : public clang::RecursiveASTVisitor { +public: + bool VisitDeclRefExpr(clang::DeclRefExpr *Reference) { + const auto *Variable = llvm::dyn_cast(Reference->getDecl()); + Found |= Variable && Variable->hasGlobalStorage() && + !llvm::isa(Variable); + return !Found; + } + + bool found() const { return Found; } + +private: + bool Found = false; +}; + +bool referencesGlobalStorage(const clang::Stmt *Statement) { + GlobalReferenceVisitor Visitor; + Visitor.TraverseStmt(const_cast(Statement)); + return Visitor.found(); +} + +struct ParameterEffects { + bool Written = false; + bool Escaped = false; +}; + +class ParameterEffectVisitor + : public clang::RecursiveASTVisitor { +public: + ParameterEffectVisitor(const clang::ParmVarDecl *Parameter, + ParameterEffects &Effects) + : Parameter(Parameter), Effects(Effects) {} + + bool VisitBinaryOperator(clang::BinaryOperator *Operator) { + if (!Operator->isAssignmentOp()) + return true; + if (referencesVariable(Operator->getLHS(), Parameter)) + Effects.Written = true; + if (referencesVariable(Operator->getRHS(), Parameter) && + referencesGlobalStorage(Operator->getLHS())) + Effects.Escaped = true; + return true; + } + + bool VisitUnaryOperator(clang::UnaryOperator *Operator) { + if (Operator->isIncrementDecrementOp() && + referencesVariable(Operator->getSubExpr(), Parameter)) + Effects.Written = true; + return true; + } + +private: + const clang::ParmVarDecl *Parameter; + ParameterEffects &Effects; +}; + +ParameterEffects getParameterEffects(const clang::FunctionDecl *Function, + unsigned ParameterIndex) { + ParameterEffects Effects; + if (!Function || ParameterIndex >= Function->getNumParams()) + return Effects; + if (const clang::FunctionDecl *Definition = Function->getDefinition()) { + ParameterEffectVisitor Visitor(Definition->getParamDecl(ParameterIndex), + Effects); + Visitor.TraverseStmt(Definition->getBody()); + } + return Effects; +} + +enum class StorageHazard : unsigned { + MultipleCallWrites, + TypeErasedSlot, + EscapedSlot, + MergedSlot, + AmbiguousField, +}; + +class FunctionStorageHazardVisitor + : public clang::RecursiveASTVisitor { +public: + FunctionStorageHazardVisitor(clang::DiagnosticsEngine &Diagnostics, + unsigned MultipleCallWrites, + unsigned TypeErasedSlot, unsigned EscapedSlot, + llvm::DenseSet &Reported) + : Diagnostics(Diagnostics), MultipleCallWrites(MultipleCallWrites), + TypeErasedSlot(TypeErasedSlot), EscapedSlot(EscapedSlot), + Reported(Reported) {} + + bool VisitCXXReinterpretCastExpr(clang::CXXReinterpretCastExpr *Cast) { + if (isPointerToLambdaFunctorWrapper(Cast->getType(), 2)) + report(Cast->getExprLoc(), Cast->getSourceRange(), + StorageHazard::TypeErasedSlot, TypeErasedSlot); + return true; + } + + bool VisitCallExpr(clang::CallExpr *Call) { + const clang::FunctionDecl *Callee = Call->getDirectCallee(); + if (!Callee) + return true; + const unsigned Count = std::min(Call->getNumArgs(), Callee->getNumParams()); + for (unsigned I = 0; I < Count; ++I) { + const clang::VarDecl *Variable = getAddressedVariable(Call->getArg(I)); + if (!Variable || !isPointerToLambdaFunctorWrapper(Variable->getType())) + continue; + + ParameterEffects Effects = getParameterEffects(Callee, I); + if (Effects.Written && ++CallWrites[Variable->getCanonicalDecl()] == 2) + report(Call->getExprLoc(), Call->getSourceRange(), + StorageHazard::MultipleCallWrites, MultipleCallWrites); + if (Effects.Escaped) + report(Call->getExprLoc(), Call->getSourceRange(), + StorageHazard::EscapedSlot, EscapedSlot); + } + return true; + } + +private: + void report(clang::SourceLocation Location, clang::SourceRange Range, + StorageHazard Hazard, unsigned Diagnostic) { + const uint64_t Key = + (static_cast(Hazard) << 32) | Location.getRawEncoding(); + if (Reported.insert(Key).second) + Diagnostics.Report(Location, Diagnostic) << Range; + } + + clang::DiagnosticsEngine &Diagnostics; + unsigned MultipleCallWrites; + unsigned TypeErasedSlot; + unsigned EscapedSlot; + llvm::DenseSet &Reported; + llvm::DenseMap CallWrites; +}; + +class ReturnStorageHazardVisitor + : public clang::RecursiveASTVisitor { +public: + bool VisitBinaryOperator(clang::BinaryOperator *Operator) { + if (!Operator->isAssignmentOp()) + return true; + if (const clang::VarDecl *Variable = + getDirectReferencedVariable(Operator->getLHS())) + ++Assignments[Variable->getCanonicalDecl()]; + return true; + } + + bool VisitReturnStmt(clang::ReturnStmt *Return) { + Returns.push_back(Return); + return true; + } + + llvm::DenseMap Assignments; + llvm::SmallVector Returns; +}; + class CaptureMutationVisitor : public clang::RecursiveASTVisitor { public: @@ -159,6 +367,12 @@ class LambdaVisitor : public clang::RecursiveASTVisitor { public: bool shouldVisitTemplateInstantiations() const { return true; } + bool VisitFunctionDecl(clang::FunctionDecl *Function) { + if (Function->hasBody()) + Functions.push_back(Function); + return true; + } + bool VisitLambdaExpr(clang::LambdaExpr *Lambda) { Lambdas.push_back(Lambda); return true; @@ -184,6 +398,7 @@ class LambdaVisitor : public clang::RecursiveASTVisitor { } llvm::SmallVector Lambdas; + llvm::SmallVector Functions; llvm::SmallVector JitVariableCalls; llvm::SmallVector RegisterLambdaCalls; llvm::DenseSet DirectlyRegisteredLambdas; @@ -246,6 +461,71 @@ class LambdaASTConsumer : public clang::ASTConsumer { const unsigned MutatedCapture = Diagnostics.getCustomDiagID( clang::DiagnosticsEngine::Error, "proteus::jit_variable capture must remain read-only"); + const unsigned MultipleCallWrites = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "registered lambda pointer slot cannot be overwritten by multiple " + "function calls"); + const unsigned TypeErasedSlot = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "registered lambda pointer slots cannot be accessed through " + "type-erased byte offsets"); + const unsigned EscapedSlot = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "registered lambda pointer slot cannot escape to global storage"); + const unsigned MergedSlot = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "registered lambda pointer slot cannot be selected from multiple " + "control-flow assignments"); + const unsigned AmbiguousField = Diagnostics.getCustomDiagID( + clang::DiagnosticsEngine::Error, + "registered lambda pointer slot cannot be selected from different " + "aggregate fields at runtime"); + + llvm::DenseSet ReportedStorageHazards; + for (clang::FunctionDecl *Function : Visitor.Functions) { + FunctionStorageHazardVisitor StorageVisitor( + Diagnostics, MultipleCallWrites, TypeErasedSlot, EscapedSlot, + ReportedStorageHazards); + StorageVisitor.TraverseStmt(Function->getBody()); + + if (!isPointerToLambdaFunctorWrapper(Function->getReturnType(), 2)) + continue; + + ReturnStorageHazardVisitor ReturnVisitor; + ReturnVisitor.TraverseStmt(Function->getBody()); + llvm::DenseSet ReturnedMembers; + for (clang::ReturnStmt *Return : ReturnVisitor.Returns) { + const clang::Expr *Returned = Return->getRetValue(); + if (!Returned) + continue; + if (const clang::VarDecl *Variable = + getDirectReferencedVariable(Returned)) { + if (ReturnVisitor.Assignments.lookup(Variable->getCanonicalDecl()) >= + 2) + reportStorageHazard(Diagnostics, ReportedStorageHazards, + Return->getReturnLoc(), + Return->getSourceRange(), + StorageHazard::MergedSlot, MergedSlot); + } + + Returned = ignoreTransparentExprs(Returned); + if (const auto *Address = + llvm::dyn_cast_or_null(Returned); + Address && Address->getOpcode() == clang::UO_AddrOf) + Returned = ignoreTransparentExprs(Address->getSubExpr()); + const auto *Member = + llvm::dyn_cast_or_null(Returned); + if (!Member) + continue; + const clang::ValueDecl *MemberDeclaration = Member->getMemberDecl(); + if (!ReturnedMembers.empty() && + !ReturnedMembers.contains(MemberDeclaration)) + reportStorageHazard(Diagnostics, ReportedStorageHazards, + Return->getReturnLoc(), Return->getSourceRange(), + StorageHazard::AmbiguousField, AmbiguousField); + ReturnedMembers.insert(MemberDeclaration); + } + } for (const clang::CallExpr *Call : Visitor.RegisterLambdaCalls) { const clang::Expr *Argument = Call->getArg(0); @@ -299,6 +579,17 @@ class LambdaASTConsumer : public clang::ASTConsumer { } private: + static void reportStorageHazard(clang::DiagnosticsEngine &Diagnostics, + llvm::DenseSet &Reported, + clang::SourceLocation Location, + clang::SourceRange Range, + StorageHazard Hazard, unsigned Diagnostic) { + const uint64_t Key = + (static_cast(Hazard) << 32) | Location.getRawEncoding(); + if (Reported.insert(Key).second) + Diagnostics.Report(Location, Diagnostic) << Range; + } + static bool isSupportedRuntimeConstantType(clang::QualType Type, clang::ASTContext &Context) { if (Type->isPointerType() || Type->isBooleanType() || diff --git a/tests/ast/lambda_memory_invalid.cpp b/tests/ast/lambda_memory_invalid.cpp new file mode 100644 index 000000000..9494be1e5 --- /dev/null +++ b/tests/ast/lambda_memory_invalid.cpp @@ -0,0 +1,94 @@ +// clang-format off +// RUN: %clang -std=c++17 -fsyntax-only -Xclang -verify -I%proteus_include -fplugin=%plugin %s +// clang-format on + +#include + +#include + +template struct PointerSlots { + F *Noise; + F *Invoked; +}; + +template void overwriteSlot(F *volatile *Slot, F *Replacement) { + *Slot = Replacement; +} + +template void invokeAfterTwoCalls(F *Initial, F *First, F *Final) { + F *volatile Slot = Initial; + overwriteSlot(&Slot, First); + // expected-error@+1 {{cannot be overwritten by multiple function calls}} + overwriteSlot(&Slot, Final); + (*Slot)(); +} + +template +void overwriteByteOffsetField(void *Storage, F *Replacement) { + auto *Bytes = reinterpret_cast(Storage); + // expected-error@+1 {{cannot be accessed through type-erased byte offsets}} + auto *Slot = reinterpret_cast(Bytes + sizeof(F *)); + *Slot = Replacement; +} + +void *EscapedSlot; + +template void escapePointerSlot(F **Slot) { EscapedSlot = Slot; } + +template void invokeWithEscapedSlot(F *Initial, F *Replacement) { + F *Slot = Initial; + Slot = Replacement; + (*Slot)(); + // expected-error@+1 {{cannot escape to global storage}} + escapePointerSlot(&Slot); +} + +template F *volatile *returnSlotOnce(F *volatile *Slot) { + return Slot; +} + +template +F *volatile *returnSlotThroughPhi(F *volatile *First, F *volatile *Second, + bool UseSecond) { + F *volatile *Result; + if (UseSecond) + Result = returnSlotOnce(Second); + else + Result = returnSlotOnce(First); + // expected-error@+1 {{cannot be selected from multiple control-flow}} + return Result; +} + +template +F *volatile *returnRuntimeSelectedField(PointerSlots *Slots, + bool ReturnNoise) { + if (ReturnNoise) + return &Slots->Noise; + // expected-error@+1 {{cannot be selected from different aggregate fields}} + return &Slots->Invoked; +} + +auto makeRegisteredLambda() { + return proteus::register_lambda([] {}); +} + +void instantiateUnsupportedShapes(bool SelectSecond) { + auto Initial = makeRegisteredLambda(); + auto First = makeRegisteredLambda(); + auto Final = makeRegisteredLambda(); + + invokeAfterTwoCalls(&Initial, &First, &Final); + + PointerSlots Slots{&First, &Initial}; + overwriteByteOffsetField(&Slots, &Final); + + invokeWithEscapedSlot(&Initial, &Final); + + auto *InitialSlot = &Initial; + auto *SecondSlot = &Initial; + auto *PhiSlot = returnSlotThroughPhi(&InitialSlot, &SecondSlot, SelectSecond); + overwriteSlot(PhiSlot, &Final); + + auto *FieldSlot = returnRuntimeSelectedField(&Slots, SelectSecond); + overwriteSlot(FieldSlot, &Final); +} From 65272e2e9a3e0d708605d0e45ef13f99efd8596d Mon Sep 17 00:00:00 2001 From: Zane Fink Date: Thu, 8 Oct 2026 09:42:12 -0700 Subject: [PATCH 11/11] ci: use proteus-dev group workspace for CI clones --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bd5774aa9..e011b3e1b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,7 +30,7 @@ variables: ##### LC GITLAB CONFIGURATION # Keep Proteus clones out of the default ~/.jacamar-ci/builds, which other # projects' CI jobs share and clean up. - CUSTOM_CI_BUILDS_DIR: "/usr/workspace/fink12/gitlab-runner-proteus" + CUSTOM_CI_BUILDS_DIR: "/usr/workspace/proteus-dev/gitlab-runner" # Tells Gitlab to recursively update the submodules when cloning the project. # GIT_SUBMODULE_STRATEGY: recursive