[clang] 59d202c - [CIR][SYCL] Device kernel caller (#213771)
via cfe-commits
cfe-commits at lists.llvm.org
Sat Aug 15 18:15:28 PDT 2026
Author: Konstantinos Parasyris
Date: 2026-08-15T18:15:24-07:00
New Revision: 59d202c7711dabafafed4e408a50f46399322479
URL: https://github.com/llvm/llvm-project/commit/59d202c7711dabafafed4e408a50f46399322479
DIFF: https://github.com/llvm/llvm-project/commit/59d202c7711dabafafed4e408a50f46399322479.diff
LOG: [CIR][SYCL] Device kernel caller (#213771)
During device compilation, emit a SYCL kernel caller offload entry point
in
place of each function declared with the sycl_kernel_entry_point
attribute,
mirroring classic CodeGen's `CodeGenModule::EmitSYCLKernelCaller`.
Depends on #213728
Added:
Modified:
clang/lib/CIR/CodeGen/CIRGenFunction.cpp
clang/lib/CIR/CodeGen/CIRGenFunction.h
clang/lib/CIR/CodeGen/CIRGenModule.cpp
clang/lib/CIR/CodeGen/CIRGenModule.h
clang/lib/CIR/CodeGen/CIRGenSYCL.cpp
clang/lib/CIR/CodeGen/CIRGenTypes.cpp
clang/lib/CIR/CodeGen/CIRGenTypes.h
clang/lib/CIR/CodeGen/TargetInfo.h
clang/lib/CIR/CodeGen/Targets/SPIRV.cpp
clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp
Removed:
################################################################################
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
index 66e7b6d5061df..39cf3ee20d52b 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
@@ -369,14 +369,17 @@ void CIRGenFunction::LexicalScope::emitImplicitReturn() {
CIRGenBuilderTy &builder = cgf.getBuilder();
LexicalScope *localScope = cgf.curLexScope;
- const auto *fd = cast<clang::FunctionDecl>(cgf.curGD.getDecl());
+ // Synthesized functions (e.g. SYCL kernel caller entry points) have no
+ // FunctionDecl; the non-void flow-off-the-end handling below is guarded on
+ // fd.
+ const auto *fd = dyn_cast_or_null<clang::FunctionDecl>(cgf.curGD.getDecl());
// In C++, flowing off the end of a non-void function is always undefined
// behavior. In C, flowing off the end of a non-void function is undefined
// behavior only if the non-existent return value is used by the caller.
// That influences whether the terminating op is trap, unreachable, or
// return.
- if (cgf.getLangOpts().CPlusPlus && !fd->hasImplicitReturnZero() &&
+ if (fd && cgf.getLangOpts().CPlusPlus && !fd->hasImplicitReturnZero() &&
!cgf.sawAsmBlock && !fd->getReturnType()->isVoidType() &&
builder.getInsertionBlock() &&
!previousOpIsNonYieldingCleanup(builder.getInsertionBlock())) {
@@ -651,7 +654,7 @@ mlir::LogicalResult CIRGenFunction::emitFunctionBody(const clang::Stmt *body) {
return emitStmt(body, /*useCurrentScope=*/true);
}
-static void eraseEmptyAndUnusedBlocks(cir::FuncOp func) {
+void CIRGenFunction::eraseEmptyAndUnusedBlocks(cir::FuncOp func) {
// Remove any leftover blocks that are unreachable and empty, since they do
// not represent unreachable code useful for warnings nor anything deemed
// useful in general.
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index 0117945993ade..20e2d13c9652f 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -49,6 +49,7 @@ class LoopOp;
} // namespace mlir
namespace clang {
+class OutlinedFunctionDecl;
class SYCLKernelCallStmt;
} // namespace clang
@@ -2306,6 +2307,13 @@ class CIRGenFunction : public CIRGenTypeCache {
mlir::LogicalResult emitSYCLKernelCallStmt(const SYCLKernelCallStmt &s);
+ void emitSYCLKernelCaller(const clang::OutlinedFunctionDecl *outlinedFnDecl,
+ cir::FuncOp funcOp, cir::FuncType funcType,
+ FunctionArgList &args);
+
+ /// Remove leftover empty and unreachable blocks from an emitted function.
+ static void eraseEmptyAndUnusedBlocks(cir::FuncOp func);
+
std::optional<mlir::Value>
emitTargetBuiltinExpr(unsigned builtinID, const clang::CallExpr *e,
ReturnValueSlot &returnValue);
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.cpp b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
index e4650b775bc3c..16254d1bdba91 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.cpp
@@ -451,9 +451,13 @@ void CIRGenModule::emitDeferred() {
fd->isDefined()) {
// Functions with an invalid sycl_kernel_entry_point attribute are
// ignored during device compilation.
- if (!fd->getAttr<SYCLKernelEntryPointAttr>()->isInvalidAttr())
- errorNYI(fd->getSourceRange(),
- "SYCL kernel caller offload entry point");
+ if (!fd->getAttr<SYCLKernelEntryPointAttr>()->isInvalidAttr()) {
+ // Generate and emit the SYCL kernel caller function.
+ emitSYCLKernelCaller(fd, getASTContext());
+ // Recurse to emit any symbols directly or indirectly referenced
+ // by the SYCL kernel caller function.
+ emitDeferred();
+ }
// Do not emit the sycl_kernel_entry_point attributed function.
continue;
}
diff --git a/clang/lib/CIR/CodeGen/CIRGenModule.h b/clang/lib/CIR/CodeGen/CIRGenModule.h
index 9d19a58a05b4d..78612dd2e9de8 100644
--- a/clang/lib/CIR/CodeGen/CIRGenModule.h
+++ b/clang/lib/CIR/CodeGen/CIRGenModule.h
@@ -654,6 +654,11 @@ class CIRGenModule : public CIRGenTypeCache {
void emitGlobalDefinition(clang::GlobalDecl gd,
mlir::Operation *op = nullptr);
void emitGlobalFunctionDefinition(clang::GlobalDecl gd, mlir::Operation *op);
+
+ /// Emit the SYCL kernel caller offload entry point function generated for a
+ /// function declared with the sycl_kernel_entry_point attribute.
+ void emitSYCLKernelCaller(const clang::FunctionDecl *kernelEntryPointFn,
+ clang::ASTContext &ctx);
void emitGlobalVarDefinition(const clang::VarDecl *vd,
bool isTentative = false);
diff --git a/clang/lib/CIR/CodeGen/CIRGenSYCL.cpp b/clang/lib/CIR/CodeGen/CIRGenSYCL.cpp
index 9308b1fe4189f..b873c2d6f24b7 100644
--- a/clang/lib/CIR/CodeGen/CIRGenSYCL.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenSYCL.cpp
@@ -11,9 +11,16 @@
//===----------------------------------------------------------------------===//
#include "CIRGenFunction.h"
+#include "CIRGenModule.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Attr.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/SYCLKernelInfo.h"
#include "clang/AST/StmtSYCL.h"
+#include "llvm/Support/SaveAndRestore.h"
+
using namespace clang;
using namespace clang::CIRGen;
@@ -33,3 +40,109 @@ CIRGenFunction::emitSYCLKernelCallStmt(const SYCLKernelCallStmt &s) {
// of the original function body.
return emitStmt(s.getKernelLaunchStmt(), /*useCurrentScope=*/true);
}
+
+// Emit the body of a SYCL kernel caller offload entry point. Mirrors the tail
+// of generateCode, but is driven by an OutlinedFunctionDecl and an explicit
+// argument list rather than a FunctionDecl.
+void CIRGenFunction::emitSYCLKernelCaller(
+ const OutlinedFunctionDecl *outlinedFnDecl, cir::FuncOp funcOp,
+ cir::FuncType funcType, FunctionArgList &args) {
+ const Stmt *body = outlinedFnDecl->getBody();
+ SourceLocation loc = outlinedFnDecl->getLocation();
+ SourceRange bodyRange = body->getSourceRange();
+
+ // Synthesized entry point: no FunctionDecl, emitted with an empty GlobalDecl.
+ curGD = GlobalDecl();
+
+ SourceLocRAIIObject fnLoc{*this, loc.isValid() ? getLoc(loc)
+ : builder.getUnknownLoc()};
+
+ mlir::Location fusedLoc = getLoc(bodyRange);
+ mlir::Block *entryBB = funcOp.addEntryBlock();
+
+ SymTableScopeTy varScope(symbolTable);
+ {
+ LexicalScope lexScope(*this, fusedLoc, entryBB);
+ startFunction(GlobalDecl(), getContext().VoidTy, funcOp, funcType, args,
+ loc, bodyRange.getBegin());
+ if (mlir::failed(emitFunctionBody(body)))
+ return;
+ if (mlir::failed(funcOp.verifyBody()))
+ return;
+ finishFunction(body->getEndLoc());
+ }
+
+ eraseEmptyAndUnusedBlocks(funcOp);
+}
+
+void CIRGenModule::emitSYCLKernelCaller(const FunctionDecl *kernelEntryPointFn,
+ ASTContext &ctx) {
+ assert(ctx.getLangOpts().SYCLIsDevice &&
+ "SYCL kernel caller offload entry point functions can only be emitted"
+ " during device compilation");
+
+ const auto *kernelEntryPointAttr =
+ kernelEntryPointFn->getAttr<SYCLKernelEntryPointAttr>();
+ assert(kernelEntryPointAttr && "Missing sycl_kernel_entry_point attribute");
+ assert(!kernelEntryPointAttr->isInvalidAttr() &&
+ "sycl_kernel_entry_point attribute is invalid");
+
+ // Find the SYCLKernelCallStmt.
+ SYCLKernelCallStmt *kernelCallStmt =
+ cast<SYCLKernelCallStmt>(kernelEntryPointFn->getBody());
+
+ // Retrieve the SYCL kernel caller parameters from the OutlinedFunctionDecl.
+ FunctionArgList args;
+ const OutlinedFunctionDecl *outlinedFnDecl =
+ kernelCallStmt->getOutlinedFunctionDecl();
+ args.append(outlinedFnDecl->param_begin(), outlinedFnDecl->param_end());
+
+ // Compute the function info and CIR function type.
+ const CIRGenFunctionInfo &fnInfo =
+ getTypes().arrangeDeviceKernelCallerDeclaration(ctx.VoidTy, args);
+ cir::FuncType funcType = getTypes().getFunctionType(fnInfo);
+
+ // Retrieve the generated name for the SYCL kernel caller function.
+ CanQualType kernelNameType =
+ ctx.getCanonicalType(kernelEntryPointAttr->getKernelName());
+ const SYCLKernelInfo &kernelInfo = ctx.getSYCLKernelInfo(kernelNameType);
+
+ // Synthesized from the OutlinedFunctionDecl, not a FunctionDecl, so create
+ // the function directly with a null FunctionDecl (mirrors classic CodeGen's
+ // llvm::Function::Create).
+ cir::FuncOp funcOp = createCIRFunction(
+ getLoc(kernelEntryPointFn->getSourceRange()), kernelInfo.GetKernelName(),
+ funcType, /*funcDecl=*/nullptr);
+ funcOp.setLinkage(cir::GlobalLinkageKind::ExternalLinkage);
+
+ // Emit as a device kernel (e.g. spir_kernel). Classic CodeGen derives this
+ // from CC_DeviceKernel via SetLLVMFunctionAttributes; CIR does not yet route
+ // opFuncCallingConv onto the FuncOp, so set it from the target hook.
+ funcOp.setCallingConv(getTargetCIRGenInfo().getDeviceKernelCallingConv());
+
+ // Route through the shared attribute path so generic function attributes
+ // (e.g. convergent) are applied, matching classic CodeGen's
+ // SetLLVMFunctionAttributes. There is no FunctionDecl, so pass an empty
+ // GlobalDecl.
+ setCIRFunctionAttributes(GlobalDecl(), fnInfo, funcOp, /*isThunk=*/false);
+
+ // TODO: attributes applied by classic CodeGen not yet handled in CIR:
+ // SetSYCLKernelAttributes (norecurse, mustprogress), addSYCLModuleIdAttr.
+ assert(!cir::MissingFeatures::setLLVMFunctionFEnvAttributes());
+
+ // Emit the SYCL kernel caller function.
+ CIRGenFunction cgf(*this, builder);
+ llvm::SaveAndRestore<CIRGenFunction *> savedCGF(curCGF, &cgf);
+ {
+ mlir::OpBuilder::InsertionGuard guard(builder);
+ cgf.emitSYCLKernelCaller(outlinedFnDecl, funcOp, funcType, args);
+ }
+
+ setDSOLocal(static_cast<mlir::Operation *>(funcOp));
+
+ setNonAliasAttributes(GlobalDecl(), funcOp);
+ // CIR's setter takes a FunctionDecl; nullptr skips OutlinedFunctionDecl-
+ // derived attributes (e.g. inline hints), not yet handled.
+ assert(!cir::MissingFeatures::opFuncExtraAttrs());
+ setCIRFunctionAttributesForDefinition(/*fd=*/nullptr, funcOp);
+}
diff --git a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
index b2824d6f64700..346542134470a 100644
--- a/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenTypes.cpp
@@ -1,6 +1,7 @@
#include "CIRGenTypes.h"
#include "CIRGenCXXABI.h"
+#include "CIRGenCall.h"
#include "CIRGenFunctionInfo.h"
#include "CIRGenModule.h"
#include "mlir/IR/BuiltinTypes.h"
@@ -785,6 +786,20 @@ const CIRGenFunctionInfo &CIRGenTypes::arrangeCIRFunctionInfo(
return *fi;
}
+const CIRGenFunctionInfo &
+CIRGenTypes::arrangeDeviceKernelCallerDeclaration(QualType resultType,
+ const FunctionArgList &args) {
+ SmallVector<CanQualType, 16> argTypes;
+ for (const VarDecl *arg : args)
+ argTypes.push_back(astContext.getCanonicalParamType(arg->getType()));
+
+ // Classic CodeGen passes FnInfoOpts::None here; that is the no-op case, so
+ // nothing is needed even once CIR models FnInfoOpts.
+ return arrangeCIRFunctionInfo(
+ resultType->getCanonicalTypeUnqualified(), /*isInstanceMethod=*/false,
+ argTypes, FunctionType::ExtInfo(CC_DeviceKernel), RequiredArgs::All);
+}
+
const CIRGenFunctionInfo &CIRGenTypes::arrangeGlobalDeclaration(GlobalDecl gd) {
assert(!dyn_cast<ObjCMethodDecl>(gd.getDecl()) &&
"This is reported as a FIXME in LLVM codegen");
diff --git a/clang/lib/CIR/CodeGen/CIRGenTypes.h b/clang/lib/CIR/CodeGen/CIRGenTypes.h
index a7827f76bd5f2..c5f8b521ed888 100644
--- a/clang/lib/CIR/CodeGen/CIRGenTypes.h
+++ b/clang/lib/CIR/CodeGen/CIRGenTypes.h
@@ -44,6 +44,7 @@ class CallArgList;
class CIRGenBuilderTy;
class CIRGenCXXABI;
class CIRGenModule;
+class FunctionArgList;
/// This class organizes the cross-module state that is used while lowering
/// AST types to CIR types.
@@ -186,6 +187,12 @@ class CIRGenTypes {
const CIRGenFunctionInfo &
arrangeFunctionDeclaration(const clang::FunctionDecl *fd);
+ /// Arrange the function info for a device kernel caller entry point (e.g. a
+ /// SYCL kernel caller).
+ const CIRGenFunctionInfo &
+ arrangeDeviceKernelCallerDeclaration(clang::QualType resultType,
+ const FunctionArgList &args);
+
/// A builtin function is a freestanding function using the default
/// C conventions.
const CIRGenFunctionInfo &arrangeBuiltinFunctionCall(QualType resultType,
diff --git a/clang/lib/CIR/CodeGen/TargetInfo.h b/clang/lib/CIR/CodeGen/TargetInfo.h
index 84680f82f4e4b..290027dad074f 100644
--- a/clang/lib/CIR/CodeGen/TargetInfo.h
+++ b/clang/lib/CIR/CodeGen/TargetInfo.h
@@ -153,6 +153,12 @@ class TargetCIRGenInfo {
mlir::Operation *global,
CIRGenModule &module) const {}
+ /// Get the CIR calling convention to use for a device kernel entry point
+ /// (e.g. an OpenCL/SYCL or CUDA/HIP kernel) on this target.
+ virtual cir::CallingConv getDeviceKernelCallingConv() const {
+ return cir::CallingConv::C;
+ }
+
virtual bool isScalarizableAsmOperand(CIRGenFunction &cgf,
mlir::Type ty) const {
return false;
diff --git a/clang/lib/CIR/CodeGen/Targets/SPIRV.cpp b/clang/lib/CIR/CodeGen/Targets/SPIRV.cpp
index 643c635128d09..f2d9810b36061 100644
--- a/clang/lib/CIR/CodeGen/Targets/SPIRV.cpp
+++ b/clang/lib/CIR/CodeGen/Targets/SPIRV.cpp
@@ -48,6 +48,10 @@ class SPIRVTargetCIRGenInfo : public TargetCIRGenInfo {
func.setCallingConv(cir::CallingConv::SpirKernel);
}
}
+
+ cir::CallingConv getDeviceKernelCallingConv() const override {
+ return cir::CallingConv::SpirKernel;
+ }
};
} // namespace
diff --git a/clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp b/clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp
index c6522064f2fdc..4d7ffe01fdebe 100644
--- a/clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp
+++ b/clang/test/CIR/CodeGenSYCL/kernel-caller-entry-point.cpp
@@ -1,10 +1,25 @@
-// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spir64-unknown-unknown \
-// RUN: -fclangir -emit-cir -verify %s
+// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spirv64-unknown-unknown -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s -check-prefix=CIR
+// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spirv64-unknown-unknown -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --input-file=%t-cir.ll %s -check-prefixes=LLVM,LLVM-OGCG
+// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spirv64-unknown-unknown -emit-llvm %s -o %t.ll
+// RUN: FileCheck --input-file=%t.ll %s -check-prefixes=OGCG,LLVM-OGCG
-// During device compilation, a SYCL kernel caller offload entry point is
-// emitted in place of each sycl_kernel_entry_point attributed function. That
-// lowering is not yet implemented in CIR, so it must be reported as a clean
-// "Not Yet Implemented" diagnostic rather than crashing.
+// On an ELF target such as spir64, the kernel caller entry point definition is
+// dso_local. dso_local is only attached to a definition, so this also verifies
+// that setDSOLocal() runs after body emission.
+// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spir64-unknown-unknown -fclangir -emit-cir %s -o %t-elf.cir
+// RUN: FileCheck --input-file=%t-elf.cir %s -check-prefix=CIR-ELF
+// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spir64-unknown-unknown -fclangir -emit-llvm %s -o %t-elf-cir.ll
+// RUN: FileCheck --input-file=%t-elf-cir.ll %s -check-prefix=LLVM-OGCG-ELF
+// RUN: %clang_cc1 -std=c++20 -fsycl-is-device -triple spir64-unknown-unknown -emit-llvm %s -o %t-elf.ll
+// RUN: FileCheck --input-file=%t-elf.ll %s -check-prefix=LLVM-OGCG-ELF
+
+// During device compilation, an offload kernel caller entry point is emitted
+// in place of each sycl_kernel_entry_point attributed function. The entry
+// point is named after the kernel name type and its body is the transformed
+// body held by the OutlinedFunctionDecl (which invokes the kernel functor).
+// The sycl_kernel_entry_point attributed function itself is not emitted.
// Required by sycl_kernel_entry_point semantics.
template <typename KernelName, typename... Ts>
@@ -12,12 +27,52 @@ void sycl_kernel_launch(const char *, Ts...) {}
template <typename KernelName, typename KernelType>
[[clang::sycl_kernel_entry_point(KernelName)]]
-// expected-error at +1 {{ClangIR code gen Not Yet Implemented: SYCL kernel caller offload entry point}}
void kernel_single_task(KernelType kf) { kf(); }
struct KN;
+struct MemberKN;
struct K {
void operator()() const {}
};
-void test() { kernel_single_task<KN>(K{}); }
+// A sycl_kernel_entry_point function may also be a non-static member function
+// (Sema only rejects explicit-object members, ctors and dtors). The offload
+// entry point is still a free function and must not run an instance-function
+// prologue.
+struct Invoker {
+ template <typename KernelName, typename KernelType>
+ [[clang::sycl_kernel_entry_point(KernelName)]]
+ void kernel_single_task(KernelType kf) { kf(); }
+};
+
+void test() {
+ kernel_single_task<KN>(K{});
+ Invoker{}.kernel_single_task<MemberKN>(K{});
+}
+
+// The kernel caller entry point is named after the kernel name type (KN), is
+// emitted with the spir_kernel calling convention, and its body calls the
+// kernel functor's operator(). The sycl_kernel_entry_point function and its
+// launch call are not emitted during device compilation.
+// CIR-LABEL: cir.func {{.*}}@_ZTS2KN{{.*}}cc(spir_kernel)
+// CIR: cir.call @_ZNK1KclEv
+// CIR: cir.return
+// CIR-NOT: cir.func {{.*}}@_Z18kernel_single_task
+// CIR-NOT: cir.call {{.*}}@_Z17sycl_kernel_launch
+
+// The member-function entry point is emitted the same way, as a free function
+// (no implicit `this` parameter).
+// CIR-LABEL: cir.func {{.*}}@_ZTS8MemberKN{{.*}}cc(spir_kernel)
+// CIR: cir.call @_ZNK1KclEv
+// CIR: cir.return
+
+// LLVM-OGCG-LABEL: define {{.*}}spir_kernel void @_ZTS2KN
+// LLVM: call {{.*}}void @_ZNK1KclEv
+// OGCG: call {{.*}}spir_func void @_ZNK1KclEv
+// LLVM-OGCG: ret void
+// LLVM-OGCG-NOT: define {{.*}}@_Z18kernel_single_task
+
+// On ELF, the kernel caller entry point definition is dso_local in CIR,
+// CIR-lowered LLVM IR, and classic CodeGen alike.
+// CIR-ELF: cir.func {{.*}}dso_local {{.*}}@_ZTS2KN
+// LLVM-OGCG-ELF: define {{.*}}dso_local {{.*}}void @_ZTS2KN
More information about the cfe-commits
mailing list