Skip to content

[CIR] Add proper handling for no prototype function calls #150553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions clang/include/clang/CIR/Dialect/IR/CIROps.td
Original file line number Diff line number Diff line change
Expand Up @@ -1946,6 +1946,10 @@ def CIR_FuncOp : CIR_Op<"func", [
The function linkage information is specified by `linkage`, as defined by
`GlobalLinkageKind` attribute.

The `no_proto` keyword is used to identify functions that were declared
without a prototype and, consequently, may contain calls with invalid
arguments and undefined behavior.

Example:

```mlir
Expand All @@ -1964,6 +1968,7 @@ def CIR_FuncOp : CIR_Op<"func", [
let arguments = (ins SymbolNameAttr:$sym_name,
CIR_VisibilityAttr:$global_visibility,
TypeAttrOf<CIR_FuncType>:$function_type,
UnitAttr:$no_proto,
UnitAttr:$dso_local,
DefaultValuedAttr<CIR_GlobalLinkageKind,
"cir::GlobalLinkageKind::ExternalLinkage">:$linkage,
Expand Down Expand Up @@ -2005,13 +2010,6 @@ def CIR_FuncOp : CIR_Op<"func", [
return getFunctionType().getReturnTypes();
}

// TODO(cir): this should be an operand attribute, but for now we just hard-
// wire this as a function. Will later add a $no_proto argument to this op.
bool getNoProto() {
assert(!cir::MissingFeatures::opFuncNoProto());
return false;
}

//===------------------------------------------------------------------===//
// SymbolOpInterface Methods
//===------------------------------------------------------------------===//
Expand Down
14 changes: 8 additions & 6 deletions clang/include/clang/CIR/MissingFeatures.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,16 @@ struct MissingFeatures {
// FuncOp handling
static bool opFuncOpenCLKernelMetadata() { return false; }
static bool opFuncAstDeclAttr() { return false; }
static bool opFuncAttributesForDefinition() { return false; }
static bool opFuncCallingConv() { return false; }
static bool opFuncExtraAttrs() { return false; }
static bool opFuncNoProto() { return false; }
static bool opFuncCPUAndFeaturesAttributes() { return false; }
static bool opFuncSection() { return false; }
static bool opFuncMultipleReturnVals() { return false; }
static bool opFuncAttributesForDefinition() { return false; }
static bool opFuncExceptions() { return false; }
static bool opFuncExtraAttrs() { return false; }
static bool opFuncMaybeHandleStaticInExternC() { return false; }
static bool opFuncMultipleReturnVals() { return false; }
static bool opFuncOperandBundles() { return false; }
static bool opFuncParameterAttributes() { return false; }
static bool opFuncSection() { return false; }
static bool setLLVMFunctionFEnvAttributes() { return false; }
static bool setFunctionAttributes() { return false; }

Expand All @@ -96,7 +98,6 @@ struct MissingFeatures {
static bool opCallReturn() { return false; }
static bool opCallArgEvaluationOrder() { return false; }
static bool opCallCallConv() { return false; }
static bool opCallNoPrototypeFunc() { return false; }
static bool opCallMustTail() { return false; }
static bool opCallVirtual() { return false; }
static bool opCallInAlloca() { return false; }
Expand All @@ -109,6 +110,7 @@ struct MissingFeatures {
static bool opCallCIRGenFuncInfoExtParamInfo() { return false; }
static bool opCallLandingPad() { return false; }
static bool opCallContinueBlock() { return false; }
static bool opCallChain() { return false; }

// CXXNewExpr
static bool exprNewNullCheck() { return false; }
Expand Down
8 changes: 8 additions & 0 deletions clang/lib/CIR/CodeGen/CIRGenCall.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,14 @@ RValue CIRGenFunction::emitCall(const CIRGenFunctionInfo &funcInfo,
cir::FuncOp directFuncOp;
if (auto fnOp = dyn_cast<cir::FuncOp>(calleePtr)) {
directFuncOp = fnOp;
} else if (auto getGlobalOp = mlir::dyn_cast<cir::GetGlobalOp>(calleePtr)) {
// FIXME(cir): This peephole optimization avoids indirect calls for
// builtins. This should be fixed in the builtin declaration instead by
// not emitting an unecessary get_global in the first place.
// However, this is also used for no-prototype functions.
mlir::Operation *globalOp = cgm.getGlobalValue(getGlobalOp.getName());
assert(globalOp && "undefined global function");
directFuncOp = mlir::cast<cir::FuncOp>(globalOp);
} else {
[[maybe_unused]] mlir::ValueTypeRange<mlir::ResultRange> resultTypes =
calleePtr->getResultTypes();
Expand Down
5 changes: 5 additions & 0 deletions clang/lib/CIR/CodeGen/CIRGenCall.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ class CIRGenCallee {
assert(isOrdinary());
return reinterpret_cast<mlir::Operation *>(kindOrFunctionPtr);
}

void setFunctionPointer(mlir::Operation *functionPtr) {
assert(isOrdinary());
kindOrFunctionPtr = SpecialKind(reinterpret_cast<uintptr_t>(functionPtr));
}
};

/// Type for representing both the decl and type of parameters to a function.
Expand Down
43 changes: 41 additions & 2 deletions clang/lib/CIR/CodeGen/CIRGenExpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1269,7 +1269,7 @@ RValue CIRGenFunction::getUndefRValue(QualType ty) {
}

RValue CIRGenFunction::emitCall(clang::QualType calleeTy,
const CIRGenCallee &callee,
const CIRGenCallee &origCallee,
const clang::CallExpr *e,
ReturnValueSlot returnValue) {
// Get the actual function type. The callee type will always be a pointer to
Expand All @@ -1280,6 +1280,8 @@ RValue CIRGenFunction::emitCall(clang::QualType calleeTy,
calleeTy = getContext().getCanonicalType(calleeTy);
auto pointeeTy = cast<PointerType>(calleeTy)->getPointeeType();

CIRGenCallee callee = origCallee;

if (getLangOpts().CPlusPlus)
assert(!cir::MissingFeatures::sanitizers());

Expand All @@ -1296,7 +1298,44 @@ RValue CIRGenFunction::emitCall(clang::QualType calleeTy,
const CIRGenFunctionInfo &funcInfo =
cgm.getTypes().arrangeFreeFunctionCall(args, fnType);

assert(!cir::MissingFeatures::opCallNoPrototypeFunc());
// C99 6.5.2.2p6:
// If the expression that denotes the called function has a type that does
// not include a prototype, [the default argument promotions are performed].
// If the number of arguments does not equal the number of parameters, the
// behavior is undefined. If the function is defined with a type that
// includes a prototype, and either the prototype ends with an ellipsis (,
// ...) or the types of the arguments after promotion are not compatible
// with the types of the parameters, the behavior is undefined. If the
// function is defined with a type that does not include a prototype, and
// the types of the arguments after promotion are not compatible with those
// of the parameters after promotion, the behavior is undefined [except in
// some trivial cases].
// That is, in the general case, we should assume that a call through an
// unprototyped function type works like a *non-variadic* call. The way we
// make this work is to cast to the exxact type fo the promoted arguments.
if (isa<FunctionNoProtoType>(fnType)) {
assert(!cir::MissingFeatures::opCallChain());
assert(!cir::MissingFeatures::addressSpace());
cir::FuncType calleeTy = getTypes().getFunctionType(funcInfo);
// get non-variadic function type
calleeTy = cir::FuncType::get(calleeTy.getInputs(),
calleeTy.getReturnType(), false);
auto calleePtrTy = cir::PointerType::get(calleeTy);

mlir::Operation *fn = callee.getFunctionPointer();
mlir::Value addr;
if (auto funcOp = mlir::dyn_cast<cir::FuncOp>(fn)) {
addr = builder.create<cir::GetGlobalOp>(
getLoc(e->getSourceRange()),
cir::PointerType::get(funcOp.getFunctionType()), funcOp.getSymName());
} else {
addr = fn->getResult(0);
}

fn = builder.createBitcast(addr, calleePtrTy).getDefiningOp();
callee.setFunctionPointer(fn);
}

assert(!cir::MissingFeatures::opCallFnInfoOpts());
assert(!cir::MissingFeatures::hip());
assert(!cir::MissingFeatures::opCallMustTail());
Expand Down
85 changes: 82 additions & 3 deletions clang/lib/CIR/CodeGen/CIRGenModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,60 @@ cir::GlobalLinkageKind CIRGenModule::getCIRLinkageForDeclarator(
return cir::GlobalLinkageKind::ExternalLinkage;
}

/// This function is called when we implement a function with no prototype, e.g.
/// "int foo() {}". If there are existing call uses of the old function in the
/// module, this adjusts them to call the new function directly.
///
/// This is not just a cleanup: the always_inline pass requires direct calls to
/// functions to be able to inline them. If there is a bitcast in the way, it
/// won't inline them. Instcombine normally deletes these calls, but it isn't
/// run at -O0.
void CIRGenModule::replaceUsesOfNonProtoTypeWithRealFunction(
mlir::Operation *old, cir::FuncOp newFn) {
// If we're redefining a global as a function, don't transform it.
auto oldFn = mlir::dyn_cast<cir::FuncOp>(old);
if (!oldFn)
return;

// TODO(cir): this RAUW ignores the features below.
assert(!cir::MissingFeatures::opFuncExceptions());
assert(!cir::MissingFeatures::opFuncParameterAttributes());
assert(!cir::MissingFeatures::opFuncOperandBundles());
if (oldFn->getAttrs().size() <= 1)
errorNYI(old->getLoc(),
"replaceUsesOfNonProtoTypeWithRealFunction: Attribute forwarding");

// Mark new function as originated from a no-proto declaration.
newFn.setNoProto(oldFn.getNoProto());

// Iterate through all calls of the no-proto function.
std::optional<mlir::SymbolTable::UseRange> symUses =
oldFn.getSymbolUses(oldFn->getParentOp());
for (const mlir::SymbolTable::SymbolUse &use : symUses.value()) {
mlir::OpBuilder::InsertionGuard guard(builder);

if (auto noProtoCallOp = mlir::dyn_cast<cir::CallOp>(use.getUser())) {
builder.setInsertionPoint(noProtoCallOp);

// Patch call type with the real function type.
cir::CallOp realCallOp = builder.createCallOp(
noProtoCallOp.getLoc(), newFn, noProtoCallOp.getOperands());

// Replace old no proto call with fixed call.
noProtoCallOp.replaceAllUsesWith(realCallOp);
noProtoCallOp.erase();
} else if (auto getGlobalOp =
mlir::dyn_cast<cir::GetGlobalOp>(use.getUser())) {
// Replace type
getGlobalOp.getAddr().setType(
cir::PointerType::get(newFn.getFunctionType()));
} else {
errorNYI(use.getUser()->getLoc(),
"replaceUsesOfNonProtoTypeWithRealFunction: unexpected use");
}
}
}

cir::GlobalLinkageKind
CIRGenModule::getCIRLinkageVarDefinition(const VarDecl *vd, bool isConstant) {
assert(!isConstant && "constant variables NYI");
Expand Down Expand Up @@ -1692,8 +1746,7 @@ cir::FuncOp CIRGenModule::getOrCreateCIRFunction(
// Lookup the entry, lazily creating it if necessary.
mlir::Operation *entry = getGlobalValue(mangledName);
if (entry) {
if (!isa<cir::FuncOp>(entry))
errorNYI(d->getSourceRange(), "getOrCreateCIRFunction: non-FuncOp");
assert(mlir::isa<cir::FuncOp>(entry));

assert(!cir::MissingFeatures::weakRefReference());

Expand Down Expand Up @@ -1729,6 +1782,30 @@ cir::FuncOp CIRGenModule::getOrCreateCIRFunction(
invalidLoc ? theModule->getLoc() : getLoc(funcDecl->getSourceRange()),
mangledName, mlir::cast<cir::FuncType>(funcType), funcDecl);

// If we already created a function with the same mangled name (but different
// type) before, take its name and add it to the list of functions to be
// replaced with F at the end of CodeGen.
//
// This happens if there is a prototype for a function (e.g. "int f()") and
// then a definition of a different type (e.g. "int f(int x)").
if (entry) {

// Fetch a generic symbol-defining operation and its uses.
auto symbolOp = mlir::cast<mlir::SymbolOpInterface>(entry);

// This might be an implementation of a function without a prototype, in
// which case, try to do special replacement of calls which match the new
// prototype. The really key thing here is that we also potentially drop
// arguments from the call site so as to make a direct call, which makes the
// inliner happier and suppresses a number of optimizer warnings (!) about
// dropping arguments.
if (symbolOp.getSymbolUses(symbolOp->getParentOp()))
replaceUsesOfNonProtoTypeWithRealFunction(entry, funcOp);

// Obliterate no-proto declaration.
entry->erase();
}

if (d)
setFunctionAttributes(gd, funcOp, /*isIncompleteFunction=*/false, isThunk);

Expand Down Expand Up @@ -1805,7 +1882,9 @@ CIRGenModule::createCIRFunction(mlir::Location loc, StringRef name,
func = builder.create<cir::FuncOp>(loc, name, funcType);

assert(!cir::MissingFeatures::opFuncAstDeclAttr());
assert(!cir::MissingFeatures::opFuncNoProto());

if (funcDecl && !funcDecl->hasPrototype())
func.setNoProto(true);

assert(func.isDeclaration() && "expected empty body");

Expand Down
3 changes: 3 additions & 0 deletions clang/lib/CIR/CodeGen/CIRGenModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ class CIRGenModule : public CIRGenTypeCache {

static void setInitializer(cir::GlobalOp &op, mlir::Attribute value);

void replaceUsesOfNonProtoTypeWithRealFunction(mlir::Operation *old,
cir::FuncOp newFn);

cir::FuncOp
getOrCreateCIRFunction(llvm::StringRef mangledName, mlir::Type funcType,
clang::GlobalDecl gd, bool forVTable,
Expand Down
7 changes: 7 additions & 0 deletions clang/lib/CIR/Dialect/IR/CIRDialect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1464,10 +1464,14 @@ ParseResult cir::FuncOp::parse(OpAsmParser &parser, OperationState &state) {
llvm::SMLoc loc = parser.getCurrentLocation();
mlir::Builder &builder = parser.getBuilder();

mlir::StringAttr noProtoNameAttr = getNoProtoAttrName(state.name);
mlir::StringAttr visNameAttr = getSymVisibilityAttrName(state.name);
mlir::StringAttr visibilityNameAttr = getGlobalVisibilityAttrName(state.name);
mlir::StringAttr dsoLocalNameAttr = getDsoLocalAttrName(state.name);

if (parser.parseOptionalKeyword(noProtoNameAttr).succeeded())
state.addAttribute(noProtoNameAttr, parser.getBuilder().getUnitAttr());

// Default to external linkage if no keyword is provided.
state.addAttribute(getLinkageAttrNameString(),
GlobalLinkageKindAttr::get(
Expand Down Expand Up @@ -1572,6 +1576,9 @@ mlir::Region *cir::FuncOp::getCallableRegion() {
}

void cir::FuncOp::print(OpAsmPrinter &p) {
if (getNoProto())
p << " no_proto";

if (getComdat())
p << " comdat";

Expand Down
26 changes: 13 additions & 13 deletions clang/test/CIR/CodeGen/call.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ struct S {
};

void f1(struct S);
void f2() {
void f2(void) {
struct S s;
f1(s);
}
Expand All @@ -28,8 +28,8 @@ void f2() {
// OGCG: %[[S:.+]] = load i64, ptr %{{.+}}, align 4
// OGCG-NEXT: call void @f1(i64 %[[S]])

struct S f3();
void f4() {
struct S f3(void);
void f4(void) {
struct S s = f3();
}

Expand All @@ -38,21 +38,21 @@ void f4() {
// CIR-NEXT: cir.store align(4) %[[S]], %{{.+}} : !rec_S, !cir.ptr<!rec_S>

// LLVM-LABEL: define{{.*}} void @f4() {
// LLVM: %[[S:.+]] = call %struct.S (...) @f3()
// LLVM: %[[S:.+]] = call %struct.S @f3()
// LLVM-NEXT: store %struct.S %[[S]], ptr %{{.+}}, align 4

// OGCG-LABEL: define{{.*}} void @f4() #0 {
// OGCG: %[[S:.+]] = call i64 (...) @f3()
// OGCG: %[[S:.+]] = call i64 @f3()
// OGCG-NEXT: store i64 %[[S]], ptr %{{.+}}, align 4

struct Big {
int data[10];
};

void f5(struct Big);
struct Big f6();
struct Big f6(void);

void f7() {
void f7(void) {
struct Big b;
f5(b);
}
Expand All @@ -69,7 +69,7 @@ void f7() {
// OGCG: %[[B:.+]] = alloca %struct.Big, align 8
// OGCG-NEXT: call void @f5(ptr noundef byval(%struct.Big) align 8 %[[B]])

void f8() {
void f8(void) {
struct Big b = f6();
}

Expand All @@ -78,14 +78,14 @@ void f8() {
// CIR: cir.store align(4) %[[B]], %{{.+}} : !rec_Big, !cir.ptr<!rec_Big>

// LLVM-LABEL: define{{.*}} void @f8() {
// LLVM: %[[B:.+]] = call %struct.Big (...) @f6()
// LLVM: %[[B:.+]] = call %struct.Big @f6()
// LLVM-NEXT: store %struct.Big %[[B]], ptr %{{.+}}, align 4

// OGCG-LABEL: define{{.*}} void @f8() #0 {
// OGCG: %[[B:.+]] = alloca %struct.Big, align 4
// OGCG-NEXT: call void (ptr, ...) @f6(ptr dead_on_unwind writable sret(%struct.Big) align 4 %[[B]])
// OGCG-NEXT: call void @f6(ptr dead_on_unwind writable sret(%struct.Big) align 4 %[[B]])

void f9() {
void f9(void) {
f1(f3());
}

Expand All @@ -98,14 +98,14 @@ void f9() {

// LLVM-LABEL: define{{.*}} void @f9() {
// LLVM: %[[SLOT:.+]] = alloca %struct.S, i64 1, align 4
// LLVM-NEXT: %[[RET:.+]] = call %struct.S (...) @f3()
// LLVM-NEXT: %[[RET:.+]] = call %struct.S @f3()
// LLVM-NEXT: store %struct.S %[[RET]], ptr %[[SLOT]], align 4
// LLVM-NEXT: %[[ARG:.+]] = load %struct.S, ptr %[[SLOT]], align 4
// LLVM-NEXT: call void @f1(%struct.S %[[ARG]])

// OGCG-LABEL: define{{.*}} void @f9() #0 {
// OGCG: %[[SLOT:.+]] = alloca %struct.S, align 4
// OGCG-NEXT: %[[RET:.+]] = call i64 (...) @f3()
// OGCG-NEXT: %[[RET:.+]] = call i64 @f3()
// OGCG-NEXT: store i64 %[[RET]], ptr %[[SLOT]], align 4
// OGCG-NEXT: %[[ARG:.+]] = load i64, ptr %[[SLOT]], align 4
// OGCG-NEXT: call void @f1(i64 %[[ARG]])
Expand Down
Loading