[CIR] Implement most of 'musttail' statement attr (#213154)

This implements the basics, and leaves NYI in a few places (particularly
those that require cleanup calculations). There IS some additional work
that needs to be done when the ABI work is put in place as well, and the
thunk 'tail' hint isn't implemented either, and is left as a
missing-feature.

This is necessary for compiling the bytecode interpreter in Clang in a
few places during self-build.
This commit is contained in:
Erich Keane 2026-07-30 16:18:06 -07:00 committed by GitHub
parent a1d93f19be
commit 06361fb2ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 312 additions and 24 deletions

View file

@ -91,7 +91,7 @@ struct MissingFeatures {
static bool opCallReturn() { return false; }
static bool opCallArgEvaluationOrder() { return false; }
static bool opCallCallConv() { return false; }
static bool opCallMustTail() { return false; }
static bool opCallThunkTailHint() { return false; }
static bool opCallInAlloca() { return false; }
static bool opCallAttrs() { return false; }
static bool opCallSurroundingTry() { return false; }

View file

@ -1170,7 +1170,8 @@ static RValue emitAtomicLibCall(CIRGenFunction &cgf, llvm::StringRef funcName,
cir::FuncOp fn = cgf.cgm.createRuntimeFunction(fnTy, funcName, fnAttrs);
auto callee = CIRGenCallee::forDirect(fn);
return cgf.emitCall(fnInfo, callee, ReturnValueSlot(), args);
return cgf.emitCall(fnInfo, callee, ReturnValueSlot(), args,
/*isMustTail=*/false);
}
static RValue emitLibCallForAtomicExpr(CIRGenFunction &cgf, AtomicExpr *e,

View file

@ -312,7 +312,7 @@ void CIRGenNVCUDARuntime::emitDeviceStubBodyNew(CIRGenFunction &cgf,
const CIRGenFunctionInfo &callInfo =
cgm.getTypes().arrangeFunctionDeclaration(cudaLaunchKernelFD);
cgf.emitCall(callInfo, CIRGenCallee::forDirect(cudaKernelLauncherFn),
ReturnValueSlot(), launchArgs);
ReturnValueSlot(), launchArgs, /*isMustTail=*/false);
if (cgm.getASTContext().getTargetInfo().getCXXABI().isMicrosoft() &&
!cgf.getLangOpts().HIP)

View file

@ -1173,7 +1173,7 @@ RValue CIRGenFunction::emitCall(const CIRGenFunctionInfo &funcInfo,
ReturnValueSlot returnValue,
const CallArgList &args,
cir::CIRCallOpInterface *callOp,
mlir::Location loc) {
bool isMustTail, mlir::Location loc) {
QualType retTy = funcInfo.getReturnType();
cir::FuncType cirFuncTy = getTypes().getFunctionType(funcInfo);
@ -1350,7 +1350,41 @@ RValue CIRGenFunction::emitCall(const CIRGenFunctionInfo &funcInfo,
if (callOp)
*callOp = theCall;
assert(!cir::MissingFeatures::opCallMustTail());
if (isMustTail) {
// PPC/MIPS have some diagnostics for classic-codegen, but we don't support
// them yet.
const llvm::Triple &triple = getTarget().getTriple();
if (triple.isPPC() || triple.isMIPS()) {
cgm.errorNYI(mustTailCall->getBeginLoc(),
"musttail call target legality checks");
return getUndefRValue(retTy);
}
// Musttail is required to return immediately. Classic codegen does some
// work here to go through the exception handling scopes to put them before
// the call (it seems?) since musttail must be the last op before the
// return. For now, skip this so we an do it later.
if (ehStack.stable_begin() != prologueCleanupDepth) {
cgm.errorNYI(mustTailCall->getBeginLoc(),
"musttail call that skips cleanups");
return getUndefRValue(retTy);
}
theCall->setAttr(cir::CIRDialect::getMustTailAttrName(),
builder.getUnitAttr());
if (isa<cir::VoidType>(convertType(retTy)))
cir::ReturnOp::create(builder, loc);
else
cir::ReturnOp::create(builder, loc, theCall->getResult(0));
// Musttail must return immediately, so we just do that. All of the below
// stuff is effectively UB if this is a musttail, so just do a return
// immediately.
builder.createBlock(builder.getBlock()->getParent());
return getUndefRValue(retTy);
}
assert(!cir::MissingFeatures::opCallReturn());
mlir::Type retCIRTy = convertType(retTy);

View file

@ -958,7 +958,8 @@ void CIRGenFunction::emitForwardingCallToLambda(
// Now emit our call.
CIRGenCallee callee =
CIRGenCallee::forDirect(calleePtr, GlobalDecl(callOperator));
RValue rv = emitCall(calleeFnInfo, callee, returnSlot, callArgs);
RValue rv = emitCall(calleeFnInfo, callee, returnSlot, callArgs,
/*isMustTail=*/false);
// Forward the returned value through the function's return slot.
if (!resultType->isVoidType()) {
@ -1555,7 +1556,8 @@ void CIRGenFunction::emitCXXConstructorCall(
args, d, type, extraArgs.prefix, extraArgs.suffix, passPrototypeArgs);
CIRGenCallee callee = CIRGenCallee::forDirect(calleePtr, GlobalDecl(d, type));
cir::CIRCallOpInterface c;
emitCall(info, callee, ReturnValueSlot(), args, &c, getLoc(loc));
emitCall(info, callee, ReturnValueSlot(), args, &c, /*isMustTail=*/false,
getLoc(loc));
if (cgm.getCodeGenOpts().OptimizationLevel != 0 && !crd->isDynamicClass() &&
type != Ctor_Base && cgm.getCodeGenOpts().StrictVTablePointers)

View file

@ -2426,11 +2426,10 @@ RValue CIRGenFunction::emitCall(clang::QualType calleeTy,
assert(!cir::MissingFeatures::opCallFnInfoOpts());
assert(!cir::MissingFeatures::hip());
assert(!cir::MissingFeatures::opCallMustTail());
cir::CIRCallOpInterface callOp;
RValue callResult = emitCall(funcInfo, callee, returnValue, args, &callOp,
getLoc(e->getExprLoc()));
e == mustTailCall, getLoc(e->getExprLoc()));
assert(!cir::MissingFeatures::generateDebugInfo());

View file

@ -118,10 +118,10 @@ CIRGenFunction::emitCXXMemberPointerCallExpr(const CXXMemberCallExpr *ce,
// Build the call.
CIRGenCallee callee(fpt, calleePtr.getDefiningOp());
assert(!cir::MissingFeatures::opCallMustTail());
return emitCall(cgm.getTypes().arrangeCXXMethodCall(argsList, fpt, required,
/*PrefixSize=*/0),
callee, returnValue, argsList, nullptr, loc);
callee, returnValue, argsList, nullptr, ce == mustTailCall,
loc);
}
RValue CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
@ -322,8 +322,8 @@ RValue CIRGenFunction::emitCXXMemberOrOperatorCall(
args, fpt, callInfo.reqArgs, callInfo.prefixSize);
assert((ce || currSrcLoc) && "expected source location");
mlir::Location loc = ce ? getLoc(ce->getExprLoc()) : *currSrcLoc;
assert(!cir::MissingFeatures::opCallMustTail());
return emitCall(fnInfo, callee, returnValue, args, nullptr, loc);
return emitCall(fnInfo, callee, returnValue, args, nullptr,
ce && ce == mustTailCall, loc);
}
static void emitNullBaseClassInitialization(CIRGenFunction &cgf,
@ -756,9 +756,9 @@ static RValue emitNewDeleteCall(CIRGenFunction &cgf,
cir::FuncOp calleePtr = cgf.cgm.getAddrOfFunction(calleeDecl);
CIRGenCallee callee =
CIRGenCallee::forDirect(calleePtr, GlobalDecl(calleeDecl));
RValue rv =
cgf.emitCall(cgf.cgm.getTypes().arrangeFreeFunctionCall(args, calleeType),
callee, ReturnValueSlot(), args, &callOrTryCall);
RValue rv = cgf.emitCall(
cgf.cgm.getTypes().arrangeFreeFunctionCall(args, calleeType), callee,
ReturnValueSlot(), args, /*isMustTail=*/false, &callOrTryCall);
/// C++1y [expr.new]p10:
/// [In a new-expression,] an implementation is allowed to omit a call
@ -1367,9 +1367,8 @@ RValue CIRGenFunction::emitCXXDestructorCall(
commonBuildCXXMemberOrOperatorCall(*this, dtorDecl, thisVal, implicitParam,
implicitParamTy, ce, args, nullptr);
assert((ce || dtor.getDecl()) && "expected source location provider");
assert(!cir::MissingFeatures::opCallMustTail());
return emitCall(cgm.getTypes().arrangeCXXStructorDeclaration(dtor), callee,
ReturnValueSlot(), args, nullptr,
ReturnValueSlot(), args, nullptr, ce && ce == mustTailCall,
ce ? getLoc(ce->getExprLoc())
: getLoc(dtor.getDecl()->getSourceRange()));
}

View file

@ -586,6 +586,10 @@ public:
}
};
// The CallExpr within the current statement that the musttail attribute
// applies to. nullptr if there is no 'musttail' on the current statement.
const CallExpr *mustTailCall = nullptr;
struct VlaSizePair {
mlir::Value numElts;
QualType type;
@ -1786,14 +1790,14 @@ public:
RValue emitCall(const CIRGenFunctionInfo &funcInfo,
const CIRGenCallee &callee, ReturnValueSlot returnValue,
const CallArgList &args, cir::CIRCallOpInterface *callOp,
mlir::Location loc);
bool isMustTail, mlir::Location loc);
RValue emitCall(const CIRGenFunctionInfo &funcInfo,
const CIRGenCallee &callee, ReturnValueSlot returnValue,
const CallArgList &args,
const CallArgList &args, bool isMustTail,
cir::CIRCallOpInterface *callOrTryCall = nullptr) {
assert(currSrcLoc && "source location must have been set");
return emitCall(funcInfo, callee, returnValue, args, callOrTryCall,
*currSrcLoc);
isMustTail, *currSrcLoc);
}
RValue emitCall(clang::QualType calleeTy, const CIRGenCallee &callee,

View file

@ -21,6 +21,7 @@
#include "clang/AST/StmtOpenACC.h"
#include "clang/AST/StmtOpenMP.h"
#include "clang/CIR/MissingFeatures.h"
#include "llvm/Support/SaveAndRestore.h"
using namespace clang;
using namespace clang::CIRGen;
@ -87,6 +88,9 @@ mlir::LogicalResult CIRGenFunction::emitCompoundStmtWithoutScope(
mlir::LogicalResult
CIRGenFunction::emitAttributedStmt(const AttributedStmt &s) {
const CallExpr *musttail = nullptr;
for (const Attr *attr : s.getAttrs()) {
switch (attr->getKind()) {
default:
@ -95,12 +99,17 @@ CIRGenFunction::emitAttributedStmt(const AttributedStmt &s) {
case attr::NoInline:
case attr::AlwaysInline:
case attr::NoConvergent:
case attr::MustTail:
case attr::Atomic:
case attr::HLSLControlFlowHint:
cgm.errorNYI(s.getSourceRange(),
"Unimplemented statement attribute: ", attr->getKind());
break;
case attr::MustTail: {
const Stmt *sub = s.getSubStmt();
const ReturnStmt *ret = cast<ReturnStmt>(sub);
musttail = cast<CallExpr>(ret->getRetValue()->IgnoreParens());
break;
}
case attr::CXXAssume: {
const Expr *assumptionExpr = cast<CXXAssumeAttr>(attr)->getAssumption();
if (getLangOpts().CXXAssumptions && builder.getInsertionBlock() &&
@ -114,6 +123,8 @@ CIRGenFunction::emitAttributedStmt(const AttributedStmt &s) {
}
}
SaveAndRestore save_musttail(mustTailCall, musttail);
return emitStmt(s.getSubStmt(), /*useCurrentScope=*/true, s.getAttrs());
}

View file

@ -744,13 +744,13 @@ void CIRGenFunction::emitCallAndReturnForThunk(cir::FuncOp callee,
CIRGenCallee cirCallee = CIRGenCallee::forDirect(callee, curGD);
mlir::Location loc = builder.getUnknownLoc();
RValue rv = emitCall(*curFnInfo, cirCallee, slot, callArgs,
/*callOrTryCall=*/nullptr, loc);
/*callOrTryCall=*/nullptr, /*isMustTail=*/false, loc);
// Consider return adjustment if we have ThunkInfo.
if (thunk && !thunk->Return.isEmpty())
rv = performReturnAdjustment(*this, resultType, rv, *thunk);
else
assert(!cir::MissingFeatures::opCallMustTail());
assert(!cir::MissingFeatures::opCallThunkTailHint());
// Emit return. For aggregate returns the call has already written the
// result through the slot bound to returnValue above; emit the

View file

@ -0,0 +1,238 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
// RUN: FileCheck --check-prefix=LLVM --input-file=%t.ll %s
int Bar(int);
int Baz(int);
int Func1(int x) {
if (x)
[[clang::musttail]] return Bar(x);
else
[[clang::musttail]] return Baz(x);
}
// CIR-LABEL: cir.func{{.*}} @_Z5Func1i
// CIR: %[[R1:.+]] = cir.call @_Z3Bari(%{{.+}}) musttail : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R1]] : !s32i
// CIR: %[[R2:.+]] = cir.call @_Z3Bazi(%{{.+}}) musttail : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R2]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z5Func1i
// LLVM: %[[C1:.+]] = musttail call{{.*}} i32 @_Z3Bari(i32{{.*}})
// LLVM-NEXT: ret i32 %[[C1]]
// LLVM: %[[C2:.+]] = musttail call{{.*}} i32 @_Z3Bazi(i32{{.*}})
// LLVM-NEXT: ret i32 %[[C2]]
int Nested(int x) {
[[clang::musttail]] return Bar(Bar(x));
}
// CIR-LABEL: cir.func{{.*}} @_Z6Nestedi
// CIR: %[[INNER:.+]] = cir.call @_Z3Bari(%{{.+}}) : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: %[[OUTER:.+]] = cir.call @_Z3Bari(%[[INNER]]) musttail : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[OUTER]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z6Nestedi
// LLVM: %[[INNER:.+]] = call{{.*}} i32 @_Z3Bari
// LLVM: %[[OUTER:.+]] = musttail call{{.*}} i32 @_Z3Bari(i32{{.*}} %[[INNER]])
// LLVM-NEXT: ret i32 %[[OUTER]]
int Scoped(int x) {
{ [[clang::musttail]] return Bar(x); }
}
// CIR-LABEL: cir.func{{.*}} @_Z6Scopedi
// CIR: %[[R:.+]] = cir.call @_Z3Bari(%{{.+}}) musttail : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z6Scopedi
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 @_Z3Bari
// LLVM-NEXT: ret i32 %[[R]]
void ReturnsVoid();
void VoidTail() {
[[clang::musttail]] return ReturnsVoid();
}
// CIR-LABEL: cir.func{{.*}} @_Z8VoidTailv
// CIR: cir.call @_Z11ReturnsVoidv() musttail : () -> ()
// CIR-NEXT: cir.return
// LLVM-LABEL: define{{.*}} void @_Z8VoidTailv
// LLVM: musttail call void @_Z11ReturnsVoidv()
// LLVM-NEXT: ret void
void IndirectLocal(int x) {
void (*p)(int) = nullptr;
[[clang::musttail]] return p(x);
}
// CIR-LABEL: cir.func{{.*}} @_Z13IndirectLocali
// CIR: cir.call %{{.+}}(%{{.+}}) musttail : (!cir.ptr<!cir.func<(!s32i)>>, !s32i{{.*}}) -> ()
// CIR-NEXT: cir.return
// LLVM-LABEL: define{{.*}} void @_Z13IndirectLocali
// LLVM: musttail call void %{{.+}}(i32{{.*}})
// LLVM-NEXT: ret void
struct Data {
int (*fptr)(Data *);
};
int IndirectField(Data *data) {
[[clang::musttail]] return data->fptr(data);
}
// CIR-LABEL: cir.func{{.*}} @_Z13IndirectFieldP4Data
// CIR: %[[R:.+]] = cir.call %{{.+}}(%{{.+}}) musttail : (!cir.ptr<{{.*}}>, !cir.ptr<!rec_Data>{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z13IndirectFieldP4Data
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 %{{.+}}(ptr{{.*}})
// LLVM-NEXT: ret i32 %[[R]]
struct Foo {
int MemberFunction(int x);
static int StaticMethod(int x);
int TailFrom(int x);
int TailFrom2(int x);
};
int Foo::TailFrom(int x) {
[[clang::musttail]] return MemberFunction(x);
}
// CIR-LABEL: cir.func{{.*}} @_ZN3Foo8TailFromEi
// CIR: %[[R:.+]] = cir.call @_ZN3Foo14MemberFunctionEi(%{{.+}}, %{{.+}}) musttail : (!cir.ptr<!rec_Foo>{{.*}}, !s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_ZN3Foo8TailFromEi
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 @_ZN3Foo14MemberFunctionEi(ptr{{.*}}, i32{{.*}})
// LLVM-NEXT: ret i32 %[[R]]
int StaticViaClass(int x) {
[[clang::musttail]] return Foo::StaticMethod(x);
}
// CIR-LABEL: cir.func{{.*}} @_Z14StaticViaClassi
// CIR: %[[R:.+]] = cir.call @_ZN3Foo12StaticMethodEi(%{{.+}}) musttail : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z14StaticViaClassi
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 @_ZN3Foo12StaticMethodEi(i32{{.*}})
// LLVM-NEXT: ret i32 %[[R]]
int StaticViaObject(int x) {
Foo foo;
[[clang::musttail]] return foo.StaticMethod(x);
}
// CIR-LABEL: cir.func{{.*}} @_Z15StaticViaObjecti
// CIR: %[[R:.+]] = cir.call @_ZN3Foo12StaticMethodEi(%{{.+}}) musttail : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z15StaticViaObjecti
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 @_ZN3Foo12StaticMethodEi(i32{{.*}})
// LLVM-NEXT: ret i32 %[[R]]
template <class T> int TplBody(int x) {
T t;
[[clang::musttail]] return Bar(x);
}
int InstBody(int x) { return TplBody<int>(x); }
// CIR-LABEL: cir.func{{.*}} @_Z7TplBodyIiEii
// CIR: %[[R:.+]] = cir.call @_Z3Bari(%{{.+}}) musttail : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z7TplBodyIiEii
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 @_Z3Bari
// LLVM-NEXT: ret i32 %[[R]]
template <class T> T TplQualified(T x) {
[[clang::musttail]] return ::Bar(x);
}
int InstQualified(int x) { return TplQualified<int>(x); }
// CIR-LABEL: cir.func{{.*}} @_Z12TplQualifiedIiET_S0_
// CIR: %[[R:.+]] = cir.call @_Z3Bari(%{{.+}}) musttail : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z12TplQualifiedIiET_S0_
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 @_Z3Bari
// LLVM-NEXT: ret i32 %[[R]]
struct TrivialDtor {};
int ReturnsInt(int x);
int TrivialLocal(int x) {
TrivialDtor foo;
[[clang::musttail]] return ReturnsInt(x);
}
// CIR-LABEL: cir.func{{.*}} @_Z12TrivialLocali
// CIR: %[[R:.+]] = cir.call @_Z10ReturnsInti(%{{.+}}) musttail : (!s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z12TrivialLocali
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 @_Z10ReturnsInti
// LLVM-NEXT: ret i32 %[[R]]
// FIXME(cir): This requires a cleanup scope of some sort, but is NYI.
// int VlaScope(int x) {
// int vla[x];
// [[clang::musttail]] return Bar(x);
// }
int (Foo::*pmf)(int);
int Foo::TailFrom2(int x) {
[[clang::musttail]] return (this->*pmf)(x);
}
// CIR-LABEL: cir.func{{.*}} @_ZN3Foo9TailFrom2Ei
// CIR: %[[R:.+]] = cir.call %{{.+}}(%{{.+}}, %{{.+}}) musttail : (!cir.ptr<{{.*}}>, !cir.ptr<!void>{{.*}}, !s32i{{.*}}) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_ZN3Foo9TailFrom2Ei
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 %{{.+}}(ptr{{.*}}, i32{{.*}})
// LLVM-NEXT: ret i32 %[[R]]
struct V {
virtual void t();
virtual void f();
};
void V::f() {
[[clang::musttail]] return t();
}
// CIR-LABEL: cir.func{{.*}} @_ZN1V1fEv
// CIR: cir.call %{{.+}}(%{{.+}}) musttail : (!cir.ptr<{{.*}}>, !cir.ptr<!rec_V>{{.*}}) -> ()
// CIR-NEXT: cir.return
// LLVM-LABEL: define{{.*}} void @_ZN1V1fEv
// LLVM: musttail call void %{{.+}}(ptr{{.*}})
// LLVM-NEXT: ret void
// FIXME(cir): Another one that requires cleanups.
// struct NT { NT(const NT &); char d[32]; };
// NT RCBV();
// NT SretTail() {
// [[clang::musttail]] return RCBV();
// }
int Lam() {
auto l = []() { return 12; };
[[clang::musttail]] return (+l)();
}
// CIR-LABEL: cir.func{{.*}} @_Z3Lamv
// CIR: %{{.+}} = cir.call @_ZZ3LamvENK3$_0cvPFivEEv(%{{.+}}){{.*}} : (!cir.ptr<{{.*}}>{{.*}}) -> (!cir.ptr<!cir.func<() -> !s32i>>{{.*}})
// CIR: %[[R:.+]] = cir.call %{{.+}}() musttail : (!cir.ptr<!cir.func<() -> !s32i>>) -> (!s32i{{.*}})
// CIR-NEXT: cir.return %[[R]] : !s32i
// LLVM-LABEL: define{{.*}} i32 @_Z3Lamv
// LLVM: %[[R:.+]] = musttail call{{.*}} i32 %{{.+}}()
// LLVM-NEXT: ret i32 %[[R]]