clang 24.0.0git
CIRABIRewriteContext.cpp
Go to the documentation of this file.
1//===- CIRABIRewriteContext.cpp - CIR ABI rewrite context ----------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
10#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
11#include "mlir/IR/Builders.h"
12#include "mlir/IR/Dominance.h"
15
16using namespace cir;
17using namespace mlir;
18using namespace mlir::abi;
19
20// This rewrite context supports the Direct (with or without coercion),
21// Extend, Ignore, Indirect-return (sret), Indirect-argument (byval and
22// byref), and Expand (struct flattening) classifications.
23//
24// For byval (ArgClassification::byVal == true) the callee gets
25// llvm.byval + llvm.noalias + llvm.noundef; for byref (byVal == false)
26// the callee gets llvm.byref without the ownership attrs. Both pass
27// through an alloca+store at the call site. At the callee, byval loads
28// the incoming pointer (a local copy), while byref rewires the CIRGen
29// param-slot alloca to the incoming pointer so the body mutates the
30// caller's storage in place.
31//
32// For Expand, the single struct argument is replaced by N scalar arguments
33// (one per field). At the callee, the N field block arguments are stored
34// directly into the parameter's own alloca (the CIRGen spill slot). At the
35// call site, the struct operand is decomposed into its fields by reading
36// each member from the source alloca (get_member + load) when the operand is
37// a load of an alloca, or via cir.extract_member otherwise.
38//
39// For Direct + canFlatten (where the coerced type is a multi-field struct),
40// the coerced struct is similarly flattened into N individual wire arguments.
41// The callee reassembles the N scalar block args into the coerced struct,
42// then coerces to the original argument type if the two types differ. The
43// call site coerces the original type to the coerced struct, then extracts
44// each field as a separate call argument.
45
46namespace {
47
48/// Return the coerced RecordType for a Direct classification that should be
49/// flattened into individual scalar arguments, or a null type if the
50/// classification does not call for flattening.
51///
52/// Flattening applies when all four conditions hold:
53/// 1. The classification is Direct with a non-null coercedType.
54/// 2. canFlatten is set.
55/// 3. The coercedType is a struct (not a union).
56/// 4. The struct has more than one field (single-field structs are already
57/// scalar; flattening them produces no benefit and classic CodeGen skips
58/// them for the same reason).
59cir::RecordType getFlattenedCoercedType(const ArgClassification &ac) {
60 if (ac.kind != ArgKind::Direct || !ac.coercedType || !ac.canFlatten)
61 return {};
62 auto recTy = dyn_cast<cir::RecordType>(ac.coercedType);
63 if (!recTy || !recTy.isStruct() || recTy.getNumElements() <= 1)
64 return {};
65 return recTy;
66}
67
68/// Build the new argument-type list for a function whose ABI classification
69/// is \p fc. Handles Direct (with or without coercion), Extend, Ignore,
70/// Indirect (byval and byref), and Expand (struct flattening) arguments.
71/// The sret return pointer, when present, is prepended by
72/// rewriteFunctionDefinition rather than here.
73mlir::LogicalResult
74buildNewArgTypes(ArrayRef<mlir::Type> oldArgTypes,
75 const FunctionClassification &fc,
76 SmallVectorImpl<mlir::Type> &newArgTypes,
77 function_ref<mlir::InFlightDiagnostic()> emitError) {
78 assert(newArgTypes.empty() && "expected an empty output vector");
79 newArgTypes.reserve(oldArgTypes.size());
80 for (auto [idx, ac] : llvm::enumerate(fc.argInfos)) {
81 mlir::Type origTy = oldArgTypes[idx];
82 switch (ac.kind) {
83 case ArgKind::Direct:
84 // Direct with canFlatten and a struct coerced type: push one wire type
85 // per field of the coerced struct rather than the struct itself.
86 // Single-field coerced structs fall through to the non-flatten path —
87 // the struct is already scalar-sized and flattening adds no value.
88 if (cir::RecordType flatTy = getFlattenedCoercedType(ac)) {
89 llvm::append_range(newArgTypes, flatTy.getMembers());
90 } else {
91 // Direct with a coerced type: the wire signature uses the coerced
92 // type; the body still expects origTy and insertArgCoercion recovers
93 // it via a memory round-trip. Direct without coercion is a
94 // pass-through.
95 newArgTypes.push_back(ac.coercedType ? ac.coercedType : origTy);
96 }
97 break;
98 case ArgKind::Ignore:
99 break;
100 case ArgKind::Expand: {
101 // Flatten the struct into one wire argument per field. The
102 // reassembly in the callee body and the decomposition at the call
103 // site are handled by insertArgCoercion and rewriteCallSite.
104 auto recTy = cast<cir::RecordType>(origTy);
105 assert(recTy.isStruct() &&
106 "Expand classification requires a struct type, not a union");
107 assert(!recTy.getMembers().empty() &&
108 "Expand classification requires at least one struct field");
109 llvm::append_range(newArgTypes, recTy.getMembers());
110 break;
111 }
112 case ArgKind::Extend:
113 // Extend keeps the original (narrow) type in the signature; the
114 // sign/zero extension is communicated to LLVM via the llvm.signext /
115 // llvm.zeroext arg attribute, attached separately below. Any
116 // coercedType the classifier set on the Extend ArgClassification is
117 // informational (typically the register-width type the value gets
118 // extended to in registers) but does not change the CIR signature.
119 newArgTypes.push_back(origTy);
120 break;
121 case ArgKind::Indirect:
122 // byval and byref both use a pointer wire type. The attribute
123 // distinction (llvm.byval vs llvm.byref) is applied in updateArgAttrs;
124 // the call-site rewrite guards against byref separately because passing
125 // a byref pointer from a CIR value requires the original alloca address,
126 // which the rewriter does not yet track.
127 newArgTypes.push_back(cir::PointerType::get(origTy));
128 break;
129 }
130 }
131 return mlir::success();
132}
133
134/// Compute the new return type for a function whose return classification
135/// is \p retInfo. Direct returns keep (or coerce to) their type, Ignore and
136/// Indirect (sret) returns become void, Extend keeps its type; Expand emits
137/// an error.
138mlir::Type
139computeNewReturnType(mlir::Type origRetTy, const ArgClassification &retInfo,
140 mlir::MLIRContext *ctx,
141 function_ref<mlir::InFlightDiagnostic()> emitError) {
142 switch (retInfo.kind) {
143 case ArgKind::Direct:
144 // Direct return with a coerced type uses the coerced type on the wire;
145 // the rewriter inserts a coercion before each cir.return.
146 return retInfo.coercedType ? retInfo.coercedType : origRetTy;
147 case ArgKind::Ignore:
148 return cir::VoidType::get(ctx);
149 case ArgKind::Expand:
150 emitError() << "Expand return is not allowed (classic codegen rejects "
151 << "it in EmitFunctionEpilog)";
152 return nullptr;
153 case ArgKind::Extend:
154 // Same convention as Extend args: keep the original return type in the
155 // signature; the sign/zero extension is communicated via the
156 // llvm.signext / llvm.zeroext res attribute attached separately below.
157 return origRetTy;
158 case ArgKind::Indirect:
159 // sret: the value is returned through a pointer argument that the ABI
160 // synthesizes (rewriteFunctionDefinition prepends it to the argument
161 // list); it is not part of the source-level signature, so the wire
162 // return type becomes void.
163 return cir::VoidType::get(ctx);
164 }
165 llvm_unreachable("all ArgKind cases handled");
166}
167
168/// Create a typed poison constant to stand in for a value the body of a
169/// function (or the result of a call) still references but whose ABI
170/// classification is Ignore. Using poison is honest -- the value is
171/// genuinely unused at the ABI boundary -- and avoids a fake alloca+load
172/// pattern that would suggest we have a value when we don't.
173mlir::Value createIgnoredValue(mlir::OpBuilder &builder, mlir::Location loc,
174 mlir::Type ty) {
175 return cir::ConstantOp::create(builder, loc, ty, cir::PoisonAttr::get(ty));
176}
177
178/// Build an updated arg_attrs ArrayAttr that drops Ignore'd args, adds
179/// llvm.signext / llvm.zeroext on Extend args, and adds llvm.byval /
180/// llvm.align on Indirect args. Preserves any existing arg attributes on
181/// retained arg slots. \p origArgTypes provides the pre-rewrite type for
182/// each arg slot (needed to compute the llvm.byval pointee type).
183mlir::ArrayAttr updateArgAttrs(mlir::MLIRContext *ctx,
184 ArrayRef<mlir::Type> origArgTypes,
185 mlir::ArrayAttr existingArgAttrs,
186 const FunctionClassification &fc) {
187 mlir::Builder builder(ctx);
189 newArgAttrs.reserve(fc.argInfos.size());
190 for (auto [oldIdx, ac] : llvm::enumerate(fc.argInfos)) {
191 if (ac.kind == ArgKind::Ignore)
192 continue;
193 mlir::DictionaryAttr existing = builder.getDictionaryAttr({});
194 if (existingArgAttrs && oldIdx < existingArgAttrs.size())
195 existing = mlir::cast<mlir::DictionaryAttr>(existingArgAttrs[oldIdx]);
196 if (cir::RecordType flatTy = getFlattenedCoercedType(ac)) {
197 // Direct + canFlatten: one empty attribute dict per flattened field; the
198 // flattened scalar arguments carry no special ABI attributes.
199 newArgAttrs.append(flatTy.getNumElements(),
200 builder.getDictionaryAttr({}));
201 } else if (ac.kind == ArgKind::Expand) {
202 // Push one empty attribute dict per expanded field; the flattened
203 // scalar arguments carry no special ABI attributes.
204 auto recTy = cast<cir::RecordType>(origArgTypes[oldIdx]);
205 newArgAttrs.append(recTy.getNumElements(), builder.getDictionaryAttr({}));
206 } else if (ac.kind == ArgKind::Extend) {
207 StringRef attrName = ac.signExtend
208 ? mlir::LLVM::LLVMDialect::getSExtAttrName()
209 : mlir::LLVM::LLVMDialect::getZExtAttrName();
210 mlir::NamedAttrList attrs(existing);
211 attrs.set(attrName, builder.getUnitAttr());
212 newArgAttrs.push_back(attrs.getDictionary(ctx));
213 } else if (ac.kind == ArgKind::Indirect) {
214 // byval: caller-allocated copy; callee receives pointer to copy.
215 // byref: callee receives pointer to the caller's original storage.
216 // Both use llvm.align(A). The ownership flag differs: llvm.byval(T)
217 // vs llvm.byref(T). Both are typed attributes carrying the pointee
218 // type T (the pre-rewrite arg type); T is recorded explicitly because
219 // it cannot be recovered from the opaque LLVM pointer after lowering.
220 //
221 // For byval, two additional attributes match classic CodeGen:
222 // llvm.noundef -- the copy is always fully defined (the caller's
223 // original must be defined or UB has already occurred, and the
224 // copy inherits that property).
225 // llvm.noalias -- the copy is a fresh caller-allocated alloca that
226 // no other pointer in the function can alias. Classic CodeGen
227 // emits this when -fpass-by-value-is-noalias is set; here we
228 // emit it unconditionally because our call-site rewrite always
229 // produces a fresh alloca+store.
230 mlir::Type pointeeTy = origArgTypes[oldIdx];
231 StringRef ownershipAttr =
232 ac.byVal ? mlir::LLVM::LLVMDialect::getByValAttrName()
233 : mlir::LLVM::LLVMDialect::getByRefAttrName();
234 mlir::NamedAttrList attrs(existing);
235 attrs.set(mlir::LLVM::LLVMDialect::getAlignAttrName(),
236 builder.getI64IntegerAttr(ac.indirectAlign.value()));
237 attrs.set(ownershipAttr, mlir::TypeAttr::get(pointeeTy));
238 if (ac.byVal) {
239 attrs.set(mlir::LLVM::LLVMDialect::getNoAliasAttrName(),
240 builder.getUnitAttr());
241 attrs.set(mlir::LLVM::LLVMDialect::getNoUndefAttrName(),
242 builder.getUnitAttr());
243 }
244 newArgAttrs.push_back(attrs.getDictionary(ctx));
245 } else {
246 newArgAttrs.push_back(existing);
247 }
248 }
249 return builder.getArrayAttr(newArgAttrs);
250}
251
252/// Build an updated res_attrs ArrayAttr (single entry, since CIR funcs have
253/// at most one result) that adds llvm.signext / llvm.zeroext on an Extend
254/// return. Preserves any existing res attributes.
255mlir::ArrayAttr updateResAttrs(mlir::MLIRContext *ctx,
256 mlir::ArrayAttr existingResAttrs,
257 const ArgClassification &retInfo) {
258 if (retInfo.kind != ArgKind::Extend)
259 return existingResAttrs;
260
262 if (existingResAttrs && !existingResAttrs.empty())
263 for (mlir::NamedAttribute na :
264 mlir::cast<mlir::DictionaryAttr>(existingResAttrs[0]))
265 attrs.push_back(na);
266 StringRef attrName = retInfo.signExtend ? "llvm.signext" : "llvm.zeroext";
267 attrs.push_back(mlir::NamedAttribute(mlir::StringAttr::get(ctx, attrName),
268 mlir::UnitAttr::get(ctx)));
269 return mlir::ArrayAttr::get(ctx, {mlir::DictionaryAttr::get(ctx, attrs)});
270}
271
272/// Coerce \p src into a temporary memory slot typed for \p dstTy at the
273/// current builder insertion point, and return the destination-typed pointer
274/// to that slot without loading the value back out. This is the shared
275/// memory half of emitCoercion: callers that want the whole coerced value use
276/// emitCoercion (below); callers that want to read individual members of a
277/// coerced struct (the call-site struct flattening) take the returned pointer
278/// and emit their own cir.get_member + cir.load per field. Lowers uniformly
279/// for scalar, vector, and record types.
280///
281/// The slot is sized to the larger of the two types so that neither the store
282/// nor a later load ever runs past it: the coerced ABI type can be larger
283/// than the original (e.g. a 12-byte aggregate passed as `{i64, i64}`), so
284/// accessing the destination through a source-sized slot would over-read.
285/// Alignment is max(srcAlign, dstAlign) to satisfy both accesses. The slot
286/// is written through a source-typed view and returned as a destination-typed
287/// view.
288///
289/// The temporary alloca is placed at the start of \p slotBlock, which must
290/// dominate every use of the coerced value and must be a block that ends up
291/// inside the enclosing function's entry block after any later outlining.
292///
293/// Any operations the helper creates are appended to \p createdOps so the
294/// caller can pass them to replaceAllUsesExcept and avoid clobbering the
295/// store's value operand when later rewiring the source value.
296mlir::Value
297emitCoercionToMemory(mlir::OpBuilder &builder, mlir::Location loc,
298 mlir::Type dstTy, mlir::Value src, mlir::Block *slotBlock,
299 const mlir::DataLayout &dl,
300 SmallPtrSetImpl<mlir::Operation *> &createdOps) {
301 mlir::Type srcTy = src.getType();
302 assert(srcTy != dstTy &&
303 "emitCoercion callers must pre-check that the types differ");
304
305 uint64_t srcAlign = dl.getTypeABIAlignment(srcTy);
306 uint64_t dstAlign = dl.getTypeABIAlignment(dstTy);
307 uint64_t allocaAlign = std::max(srcAlign, dstAlign);
308 mlir::Type slotTy =
309 dl.getTypeSize(srcTy) >= dl.getTypeSize(dstTy) ? srcTy : dstTy;
310
311 auto slotPtrTy = cir::PointerType::get(slotTy);
312 auto srcPtrTy = cir::PointerType::get(srcTy);
313 auto dstPtrTy = cir::PointerType::get(dstTy);
314
315 cir::AllocaOp alloca;
316 {
317 mlir::OpBuilder::InsertionGuard guard(builder);
318 builder.setInsertionPointToStart(slotBlock);
319 alloca = cir::AllocaOp::create(builder, loc, slotPtrTy,
320 builder.getStringAttr("coerce"),
321 builder.getI64IntegerAttr(allocaAlign));
322 }
323 createdOps.insert(alloca);
324
325 // Store through a source-typed view of the slot.
326 mlir::Value srcSlot = alloca;
327 if (slotTy != srcTy) {
328 auto srcCast = cir::CastOp::create(builder, loc, srcPtrTy,
329 cir::CastKind::bitcast, alloca);
330 createdOps.insert(srcCast);
331 srcSlot = srcCast;
332 }
333 auto store = cir::StoreOp::create(builder, loc, src, srcSlot);
334 createdOps.insert(store);
335
336 // Return a destination-typed view of the slot.
337 if (slotTy != dstTy) {
338 auto dstCast = cir::CastOp::create(builder, loc, dstPtrTy,
339 cir::CastKind::bitcast, alloca);
340 createdOps.insert(dstCast);
341 return dstCast;
342 }
343 return alloca;
344}
345
346/// Coerce \p src to type \p dstTy by going through memory and load the whole
347/// coerced value back out. Builds on emitCoercionToMemory, adding the final
348/// load of the destination-typed view.
349mlir::Value emitCoercion(mlir::OpBuilder &builder, mlir::Location loc,
350 mlir::Type dstTy, mlir::Value src,
351 mlir::Block *slotBlock, const mlir::DataLayout &dl,
352 SmallPtrSetImpl<mlir::Operation *> &createdOps) {
353 mlir::Value dstSlot =
354 emitCoercionToMemory(builder, loc, dstTy, src, slotBlock, dl, createdOps);
355 auto load = cir::LoadOp::create(builder, loc, dstSlot);
356 createdOps.insert(load);
357 return load;
358}
359
360/// Convenience overload for callers that don't need the createdOps set
361/// (e.g. call-site coercion where we don't replaceAllUsesExcept).
362mlir::Value emitCoercion(mlir::OpBuilder &builder, mlir::Location loc,
363 mlir::Type dstTy, mlir::Value src,
364 mlir::Block *slotBlock, const mlir::DataLayout &dl) {
365 SmallPtrSet<mlir::Operation *, 4> ignored;
366 return emitCoercion(builder, loc, dstTy, src, slotBlock, dl, ignored);
367}
368
369/// The block a coercion slot's alloca belongs at the start of.
370///
371/// Normally the enclosing function's entry block, where HoistAllocas expects
372/// allocas to be. A body carrying a call is not always inside a function
373/// when this pass runs, though, because LoweringPrepare runs after it: a
374/// namespace-scope `T g = makeT();` is still in its cir.global ctor region,
375/// and an OpenACC recipe's init and destroy bodies are in regions the module
376/// owns. Those take the outermost region below the module, which dominates
377/// the whole body and travels with it when the body is outlined.
378mlir::Block *coercionSlotBlock(mlir::Operation *op) {
379 if (auto funcOp = op->getParentOfType<mlir::FunctionOpInterface>())
380 return &funcOp->getRegion(0).front();
381 mlir::Region *region = op->getParentRegion();
382 while (mlir::Region *outer = region->getParentRegion()) {
383 if (mlir::isa<mlir::ModuleOp>(outer->getParentOp()))
384 break;
385 region = outer;
386 }
387 assert(!region->empty() && "coercion slot needs a block to hold the alloca");
388 return &region->front();
389}
390
391/// Insert coercion before each cir.return so the returned value matches the
392/// new (coerced) return type.
393void insertReturnCoercion(mlir::FunctionOpInterface funcOp,
394 mlir::Type origRetTy, mlir::Type coercedRetTy,
395 mlir::OpBuilder &builder,
396 const mlir::DataLayout &dl) {
398 funcOp.walk([&](cir::ReturnOp r) { returns.push_back(r); });
399 for (cir::ReturnOp r : returns) {
400 if (r.getInput().empty())
401 continue;
402 mlir::Value origVal = r.getInput()[0];
403 if (origVal.getType() == coercedRetTy)
404 continue;
405 builder.setInsertionPoint(r);
406 mlir::Value coerced =
407 emitCoercion(builder, r.getLoc(), coercedRetTy, origVal,
408 &funcOp->getRegion(0).front(), dl);
409 r->setOperand(0, coerced);
410 }
411}
412
413/// Decompose a struct value into one scalar call argument per field of \p
414/// recTy, appending the field values to \p newArgs. When \p structVal is a
415/// plain (non-volatile, non-atomic) load straight from an alloca, read each
416/// field with cir.get_member + cir.load from that alloca, emitted at the
417/// original load's position so they observe the same memory state, and record
418/// the now-dead whole-struct load in \p replacedWholeLoads for later erasure.
419/// Otherwise (a call result, compound literal, or qualified load) extract each
420/// field from the value with cir.extract_member. Loading the members from the
421/// alloca rather than extracting from a whole-struct value keeps the result in
422/// a form SROA can promote (it does not reason about extractvalue). Shared by
423/// the Expand and Direct+canFlatten argument paths.
424static void
425emitStructFieldArgs(mlir::OpBuilder &builder, mlir::Location loc,
426 mlir::Value structVal, cir::RecordType recTy,
427 SmallVectorImpl<mlir::Value> &newArgs,
428 SmallVectorImpl<cir::LoadOp> &replacedWholeLoads) {
429 cir::LoadOp wholeLoad = structVal.getDefiningOp<cir::LoadOp>();
430 cir::AllocaOp srcAlloca;
431 if (wholeLoad && !wholeLoad.getIsVolatile() && !wholeLoad.getMemOrder())
432 srcAlloca = wholeLoad.getAddr().getDefiningOp<cir::AllocaOp>();
433
434 if (srcAlloca) {
435 mlir::OpBuilder::InsertionGuard guard(builder);
436 builder.setInsertionPoint(wholeLoad);
437 for (auto [f, fieldTy] : llvm::enumerate(recTy.getMembers())) {
438 mlir::Type fieldPtrTy = cir::PointerType::get(fieldTy);
439 mlir::Value fieldPtr = cir::GetMemberOp::create(
440 builder, loc, fieldPtrTy, srcAlloca, /*name=*/"", /*index=*/f);
441 newArgs.push_back(cir::LoadOp::create(builder, loc, fieldPtr));
442 }
443 replacedWholeLoads.push_back(wholeLoad);
444 } else {
445 for (unsigned f = 0; f < recTy.getNumElements(); ++f)
446 newArgs.push_back(
447 cir::ExtractMemberOp::create(builder, loc, structVal, f));
448 }
449}
450
451/// For each Direct arg with a coerced type, change the block argument's type
452/// to the coerced type and insert a coercion at function entry that maps it
453/// back to the original type for body uses. For each Indirect byval arg,
454/// change the block argument's type to a pointer and insert a load at entry
455/// so the body sees a local copy of the original value type. For each
456/// Indirect byref arg, change the block argument to a pointer and rewire the
457/// CIRGen param-slot alloca to that pointer (no entry load / byte-copy) so
458/// the body operates on the caller's storage in place. For each Expand arg,
459/// replace the single struct block argument with N scalar block arguments (one
460/// per field) and store each field directly into the parameter's own alloca
461/// (the CIRGen spill slot), erasing the original whole-struct store.
462///
463/// \p hasSRetArg is true when the function has an sret return (a hidden return
464/// pointer is prepended as block argument 0). Expand arguments expand the
465/// block argument count, so a running index tracks the current block argument
466/// position rather than computing the classification index + \p hasSRetArg
467/// directly.
468void insertArgCoercion(mlir::FunctionOpInterface funcOp,
469 const FunctionClassification &fc,
470 mlir::OpBuilder &builder, const mlir::DataLayout &dl,
471 bool hasSRetArg) {
472 mlir::Region &body = funcOp->getRegion(0);
473 if (body.empty())
474 return;
475 mlir::Block &entry = body.front();
476
477 // Running block argument index. Each non-Expand classification occupies
478 // one block argument slot; each Expand classification occupies N slots
479 // (one per struct field), so the running index must be incremented by N
480 // rather than 1 after processing an Expand arg.
481 unsigned blockArgIdx = hasSRetArg ? 1 : 0;
482
483 for (const ArgClassification &ac : fc.argInfos) {
484 assert(blockArgIdx < entry.getNumArguments() &&
485 "classification count must not exceed entry block arguments");
486
487 if (ac.kind == ArgKind::Expand) {
488 // The block arg at blockArgIdx currently has the original struct type.
489 // Replace it with N scalar args (one per field) and store each field
490 // directly into the parameter's own alloca.
491 mlir::BlockArgument origArg = entry.getArgument(blockArgIdx);
492 auto recTy = cast<cir::RecordType>(origArg.getType());
493 assert(recTy.isStruct() &&
494 "Expand classification requires a struct type, not a union");
495 unsigned numFields = recTy.getNumElements();
496 assert(numFields > 0 &&
497 "Expand classification requires at least one struct field");
498 mlir::Location loc = funcOp.getLoc();
499
500 // CIRGen spills every by-value struct parameter into its local alloca
501 // with a single store before any other use, so the struct block arg's
502 // only use is that spill. Capture it and the destination alloca so the
503 // expanded fields can be stored straight into that alloca, preserving
504 // the alloca's variable name and `init` flag and avoiding a
505 // reassemble-then-reload roundtrip. DCE may have run earlier and
506 // removed the spill (leaving the block arg unused); tolerate that by
507 // only flattening the signature and emitting no field stores.
508 cir::StoreOp paramStore;
509 cir::AllocaOp destAlloca;
510 if (!origArg.use_empty()) {
511 assert(origArg.hasOneUse() &&
512 "Expand arg must have exactly one use (the CIRGen param spill)");
513 paramStore = cast<cir::StoreOp>(*origArg.user_begin());
514 assert(paramStore.getValue() == origArg &&
515 "Expand arg's use must be the value operand of its store");
516 destAlloca = cast<cir::AllocaOp>(paramStore.getAddr().getDefiningOp());
517 }
518
519 // Erase the original whole-struct spill before retyping the block
520 // argument, so the store is never left feeding a type-mismatched value.
521 // The field stores take its place, just before the following operation
522 // (the spill always precedes the entry block's terminator).
523 mlir::Operation *fieldStoreInsertPt = nullptr;
524 if (paramStore) {
525 fieldStoreInsertPt = paramStore->getNextNode();
526 assert(fieldStoreInsertPt &&
527 "param spill must be followed by a block terminator");
528 paramStore->erase();
529 }
530
531 // Split the single struct block arg into N scalar field block args (slot
532 // 0 reuses the original; slots 1..N-1 are inserted after it). The
533 // reshape needs no insertion point. The field stores are gated on the
534 // same destAlloca condition: when the spill survived we set the insert
535 // point to its old slot (which sits after the CIRGen allocas) and store
536 // each field there; when DCE removed the spill the parameter is dead, so
537 // we only reshape the signature and emit no stores.
538 if (destAlloca)
539 builder.setInsertionPoint(fieldStoreInsertPt);
540 for (auto [f, fieldTy] : llvm::enumerate(recTy.getMembers())) {
541 if (f == 0)
542 origArg.setType(fieldTy);
543 else
544 entry.insertArgument(blockArgIdx + f, fieldTy, loc);
545 if (!destAlloca)
546 continue;
547 mlir::Type fieldPtrTy = cir::PointerType::get(fieldTy);
548 auto fieldPtr = cir::GetMemberOp::create(builder, loc, fieldPtrTy,
549 destAlloca, /*name=*/"",
550 /*index=*/f);
551 cir::StoreOp::create(builder, loc, entry.getArgument(blockArgIdx + f),
552 fieldPtr);
553 }
554
555 blockArgIdx += numFields;
556 continue;
557 }
558
559 mlir::BlockArgument blockArg = entry.getArgument(blockArgIdx);
560
561 if (cir::RecordType flatTy = getFlattenedCoercedType(ac)) {
562 // Direct + canFlatten: the coerced type is a struct whose fields become
563 // individual wire arguments. The reconstruction mirrors the Expand path
564 // — replace the single block arg with N scalar block args, store them
565 // into an alloca of the coerced struct type, reload — but then applies
566 // an additional coercion from the coerced struct type to the original
567 // argument type if the two differ in layout.
568 unsigned numFields = flatTy.getNumElements();
569 assert(numFields >= 2 && "getFlattenedCoercedType guarantees >1 fields");
570 Type origTy = blockArg.getType();
571 Location loc = funcOp.getLoc();
572
573 // Change slot 0 to field 0's type; insert slots 1..N-1 after it.
574 blockArg.setType(flatTy.getElementType(0));
575 for (unsigned f = 1; f < numFields; ++f)
576 entry.insertArgument(blockArgIdx + f, flatTy.getElementType(f), loc);
577
578 // setInsertionPointToStart: see comment in the Expand arm above.
579 builder.setInsertionPointToStart(&entry);
580 auto flatPtrTy = cir::PointerType::get(flatTy);
581 uint64_t flatAlign = dl.getTypeABIAlignment(flatTy);
582 auto flatSlot = cir::AllocaOp::create(
583 builder, loc, flatPtrTy, builder.getStringAttr("coerce"),
584 builder.getI64IntegerAttr(flatAlign));
585 SmallPtrSet<Operation *, 8> flattenOps = {flatSlot};
586 for (auto [f, fieldTy] : llvm::enumerate(flatTy.getMembers())) {
587 Type fieldPtrTy = cir::PointerType::get(fieldTy);
588 auto fieldPtr = cir::GetMemberOp::create(builder, loc, fieldPtrTy,
589 flatSlot, /*name=*/"",
590 /*index=*/f);
591 flattenOps.insert(fieldPtr);
592 auto storeOp = cir::StoreOp::create(
593 builder, loc, entry.getArgument(blockArgIdx + f), fieldPtr);
594 flattenOps.insert(storeOp);
595 }
596 auto flatLoaded =
597 cir::LoadOp::create(builder, loc, flatTy, flatSlot.getResult());
598 flattenOps.insert(flatLoaded);
599
600 // If the coerced struct type differs from the original argument type,
601 // insert a memory round-trip to recover the original type for body uses.
602 Value finalVal = flatLoaded;
603 if (origTy != flatTy) {
604 SmallPtrSet<Operation *, 4> coercionOps;
605 finalVal = emitCoercion(builder, loc, origTy, flatLoaded, &entry, dl,
606 coercionOps);
607 flattenOps.insert(coercionOps.begin(), coercionOps.end());
608 }
609
610 // Replace all original body uses of the struct block arg (now field 0)
611 // with the recovered original-type value.
612 blockArg.replaceAllUsesExcept(finalVal, flattenOps);
613
614 blockArgIdx += numFields;
615 continue;
616 }
617
618 if (ac.kind == ArgKind::Direct && ac.coercedType) {
619 mlir::Type oldArgTy = blockArg.getType();
620 mlir::Type newArgTy = ac.coercedType;
621 if (oldArgTy == newArgTy) {
622 ++blockArgIdx;
623 continue;
624 }
625 blockArg.setType(newArgTy);
626
627 builder.setInsertionPointToStart(&entry);
628 SmallPtrSet<mlir::Operation *, 4> coercionOps;
629 mlir::Value adapted = emitCoercion(builder, funcOp.getLoc(), oldArgTy,
630 blockArg, &entry, dl, coercionOps);
631
632 // Replace blockArg uses with the adapted value, except inside the
633 // helper ops we just created. This is critical: the StoreOp's value
634 // operand is blockArg, and if we naively replaceAllUses it gets swapped
635 // to adapted (now of the original type != the alloca's pointee type).
636 blockArg.replaceAllUsesExcept(adapted, coercionOps);
637 } else if (ac.kind == ArgKind::Indirect) {
638 // byval and byref share a !cir.ptr<T> wire type; the llvm.byval vs
639 // llvm.byref distinction is in the attrs applied by updateArgAttrs.
640 // Body lowering differs: byval copies into the callee (load at entry),
641 // while byref must operate on the caller's storage in place.
642 auto ptrTy = cir::PointerType::get(blockArg.getType());
643
644 if (!ac.byVal) {
645 // byref: CIRGen spills every by-value parameter into a local alloca
646 // with a single store before any other use, and CallConvLowering runs
647 // on that CIRGen output before any alloca-promoting/splitting pass, so
648 // the block argument still has exactly that one use here. Rewire the
649 // alloca to the incoming pointer and drop the store so the body
650 // operates on the caller's storage in place. A byte-copy would be
651 // wrong for non-trivially-copyable aggregates (e.g. libstdc++ SSO
652 // std::string, where it would leave `_M_p` aliasing the source's
653 // `_M_local_buf`). DCE may have removed a dead spill; tolerate that by
654 // only retyping the block argument.
655 cir::StoreOp paramStore;
656 cir::AllocaOp destAlloca;
657 if (!blockArg.use_empty()) {
658 assert(blockArg.hasOneUse() &&
659 "byref arg must have exactly one use (the CIRGen param "
660 "spill)");
661 paramStore = cast<cir::StoreOp>(*blockArg.user_begin());
662 assert(paramStore.getValue() == blockArg &&
663 "byref arg's use must be the value operand of its store");
664 destAlloca =
665 cast<cir::AllocaOp>(paramStore.getAddr().getDefiningOp());
666 }
667
668 if (paramStore)
669 paramStore->erase();
670
671 // Update the block argument to point to its original type.
672 blockArg.setType(ptrTy);
673
674 if (destAlloca) {
675 destAlloca.getResult().replaceAllUsesWith(blockArg);
676 destAlloca->erase();
677 }
678 } else {
679 // byval: load the incoming pointer so the body sees a T value (and
680 // any CIRGen param-slot store becomes a local copy of that value).
681 blockArg.setType(ptrTy);
682
683 builder.setInsertionPointToStart(&entry);
684 auto loadOp = cir::LoadOp::create(builder, funcOp.getLoc(), blockArg);
685 SmallPtrSet<mlir::Operation *, 1> loadOps = {loadOp};
686 blockArg.replaceAllUsesExcept(loadOp.getResult(), loadOps);
687 }
688 }
689 // Ignore, Extend, and Direct-without-coerce need no block-level changes.
690
691 ++blockArgIdx;
692 }
693}
694
695/// Rewrite each cir.return so the return value flows through the sret
696/// pointer (the prepended first block argument) and the function returns
697/// void.
698///
699/// CIRGen emits a local `__retval` alloca and emits `cir.return %loaded`
700/// where `%loaded = cir.load __retval`. The naive lowering -- store the
701/// loaded SSA value through the sret pointer -- byte-copies the record,
702/// which is wrong for non-trivially-copyable types: e.g. libstdc++'s SSO
703/// `std::string` has a `_M_p` pointer that aliases the source's internal
704/// `_M_local_buf`, so a byte-copy leaves the destination pointing at the
705/// source's (now-dying) stack storage and the destination's destructor
706/// later `free()`s a stack pointer.
707///
708/// Instead, route construction directly into the sret slot: find the
709/// `__retval` alloca, replace its uses with the sret pointer, and drop the
710/// trailing `cir.load __retval` so the rewritten return has no operand.
711/// The CIRGen-emitted constructor / store-into-`__retval` then targets the
712/// sret slot uniformly, matching classic CodeGen's "construct directly into
713/// `%agg.result`" pattern.
714///
715/// CIRGen emits one `%v = cir.load %__retval` / `cir.return %v` pair per
716/// return statement, and every such load reads the single `__retval`
717/// alloca (CIR does not merge returns into a shared epilogue block). The
718/// alloca is therefore rewired to the sret pointer once; each cir.return is
719/// then collapsed to a bare return and its now-dead load erased. This
720/// `cir.return (cir.load <alloca>)` shape is an invariant guaranteed by
721/// CIRGen, so it is asserted via `cast<>` rather than guarded with a
722/// fallback.
723void insertSRetStores(mlir::FunctionOpInterface funcOp, mlir::Type origRetTy,
724 mlir::OpBuilder &builder) {
725 mlir::Value sretPtr = funcOp.getArguments()[0];
726
728 funcOp->walk([&](cir::ReturnOp retOp) { returnOps.push_back(retOp); });
729
730 cir::AllocaOp retAlloca = nullptr;
731 for (cir::ReturnOp retOp : returnOps) {
732 // Every cir.return in an sret function must carry the loaded return
733 // value -- a bare return would mean the sret slot was never written.
734 assert(!retOp.getInput().empty() &&
735 "cir.return in sret function must have an operand");
736
737 cir::LoadOp retLoad =
738 mlir::cast<cir::LoadOp>(retOp.getInput()[0].getDefiningOp());
739
740 // Rewire the shared `__retval` alloca to the sret pointer once.
741 // replaceAllUsesWith updates every load of the alloca (including those
742 // feeding the other cir.return ops) to read from sretPtr instead, so
743 // all returns are covered by this single rewiring. Only then is the
744 // now-unused alloca safe to erase.
745 if (!retAlloca) {
746 retAlloca = mlir::cast<cir::AllocaOp>(retLoad.getAddr().getDefiningOp());
747 retAlloca.getResult().replaceAllUsesWith(sretPtr);
748 retAlloca->erase();
749 }
750
751 // The sret slot now holds the return value directly; replace the
752 // value-carrying return with a void return (no operand).
753 builder.setInsertionPoint(retOp);
754 cir::ReturnOp::create(builder, retOp.getLoc());
755 retOp->erase();
756 if (retLoad.use_empty())
757 retLoad->erase();
758 }
759}
760
761/// Build the attribute dictionary for the sret slot (slot 0 of an
762/// sret-returning function or call). Matches classic CodeGen's
763/// `sret(T) align A [noalias] writable dead_on_unwind`. noalias is only
764/// valid on the callee's parameter, not at the call site, so it is gated by
765/// \p withNoalias. Key order is irrelevant: DictionaryAttr sorts by name.
766SmallVector<mlir::NamedAttribute> buildSretSlotAttrs(mlir::OpBuilder &builder,
767 mlir::Type retTy,
768 uint64_t align,
769 bool withNoalias) {
771 // The sret type must be carried explicitly: LLVM's sret attribute requires
772 // it, and once the CIR `!cir.ptr<retTy>` lowers to an opaque LLVM `ptr` the
773 // pointee type can no longer be recovered from the pointer.
774 attrs.push_back(
775 builder.getNamedAttr("llvm.sret", mlir::TypeAttr::get(retTy)));
776 attrs.push_back(
777 builder.getNamedAttr("llvm.align", builder.getI64IntegerAttr(align)));
778 if (withNoalias)
779 attrs.push_back(
780 builder.getNamedAttr("llvm.noalias", builder.getUnitAttr()));
781 attrs.push_back(builder.getNamedAttr("llvm.writable", builder.getUnitAttr()));
782 attrs.push_back(
783 builder.getNamedAttr("llvm.dead_on_unwind", builder.getUnitAttr()));
784 return attrs;
785}
786
787/// Prepend the sret slot's attrs at position 0 of newCall's arg_attrs.
788/// Called after the call has been rewritten with the sret pointer at
789/// operand 0, so the operand count now includes the sret slot. \p argAttrs
790/// must already be shaped for the rewritten argument list (Extend slots
791/// carry signext/zeroext, Ignore slots dropped); it is shifted to slots
792/// 1..N behind the sret slot.
793void applySretSlotAttrs(cir::CallOp newCall, mlir::ArrayAttr argAttrs,
794 mlir::Type retTy, uint64_t align,
795 mlir::OpBuilder &builder) {
796 mlir::MLIRContext *ctx = newCall->getContext();
798 buildSretSlotAttrs(builder, retTy, align, /*withNoalias=*/false);
799
801 newArgAttrs.reserve(newCall.getArgOperands().size());
802 newArgAttrs.push_back(mlir::DictionaryAttr::get(ctx, sretAttrs));
803 if (argAttrs)
804 llvm::append_range(newArgAttrs, argAttrs);
805 assert(newArgAttrs.size() <= newCall.getArgOperands().size() &&
806 "arg_attrs wider than the rewritten call's operand list");
807 newArgAttrs.resize(newCall.getArgOperands().size(),
808 mlir::DictionaryAttr::get(ctx));
809 newCall->setAttr("arg_attrs", mlir::ArrayAttr::get(ctx, newArgAttrs));
810}
811
812/// For an indirect call, prepend the callee function pointer as operand 0 so
813/// CallOp::create rebuilds it as an indirect call, bitcasting it to a function
814/// pointer whose signature matches the rewritten operands and return type.
815/// No-op for direct calls.
816static void prependIndirectCallee(cir::CallOp call,
817 SmallVectorImpl<mlir::Value> &args,
818 mlir::Type retTy, mlir::OpBuilder &builder) {
819 if (!call.isIndirect())
820 return;
821 mlir::Value calleePtr = call.getIndirectCall();
822 SmallVector<mlir::Type> paramTypes;
823 paramTypes.reserve(args.size());
824 llvm::transform(args, std::back_inserter(paramTypes),
825 [](mlir::Value v) { return v.getType(); });
826 // Lowering builds an indirect call's LLVM function type from the callee
827 // pointer's pointee and takes the call's result from that type, so the
828 // pointee's return type has to track the rewrite: an sret return would
829 // leave a result the call no longer produces, and a coerced return one of
830 // the wrong type. The ellipsis has to survive for the same reason: the
831 // rebuilt pointee is what makes the lowered call variadic, and only a
832 // variadic call gets the vector-register count that the x86_64 SysV ABI
833 // passes in AL and that the callee's va_arg reads back.
834 auto calleeFnTy = cast<cir::FuncType>(
835 cast<cir::PointerType>(calleePtr.getType()).getPointee());
836 auto newPtrTy = cir::PointerType::get(
837 cir::FuncType::get(paramTypes, retTy, calleeFnTy.isVarArg()));
838 if (calleePtr.getType() != newPtrTy)
839 calleePtr = cir::CastOp::create(builder, call.getLoc(), newPtrTy,
840 cir::CastKind::bitcast, calleePtr);
841 args.insert(args.begin(), calleePtr);
842}
843
844/// Rewrite an indirect-return (sret) call site: prepend a return-slot
845/// pointer as operand 0, make the call return void, and either reuse a
846/// dominating single-use store destination as the slot (so construction
847/// flows directly into it) or allocate a fresh slot and load the result
848/// back out. \p newArgs is the already-shaped (Ignore-dropped,
849/// coercion-applied) non-sret argument list. The caller guarantees the
850/// call has a result and an indirect-return classification.
851void rewriteIndirectReturnCall(cir::CallOp call,
852 const FunctionClassification &fc,
853 ArrayRef<mlir::Value> newArgs,
854 mlir::Type origRetTy,
855 ArrayRef<mlir::Type> origCallArgTypes,
856 mlir::OpBuilder &builder) {
857 mlir::MLIRContext *ctx = call->getContext();
858 auto ptrTy = cir::PointerType::get(origRetTy);
859 builder.setInsertionPoint(call);
860 uint64_t sretAlign = fc.returnInfo.indirectAlign.value();
861
862 // CIRGen emits `cir.store %callResult, %dest` when the call's result is
863 // bound to a local (e.g. `T s = make();`). Allocating a fresh sret slot
864 // and copying into %dest would byte-copy the record, which is wrong for
865 // non-trivially-copyable types (the libstdc++ SSO `_M_p` pointer
866 // survives a byte-copy but ends up pointing at the dying temp's local
867 // buffer, so the destination's destructor later `free()`s a stack
868 // pointer). When the result has a single store-into-%dest use, use
869 // %dest as the sret slot directly so construction flows into it,
870 // matching classic CodeGen's "pass %s as sret" pattern. %dest must
871 // dominate the call so the rewritten call (which takes it as operand 0)
872 // does not use a value before its definition.
873 mlir::Value sretSlot = nullptr;
874 cir::StoreOp reuseStore = nullptr;
875 if (call.getResult().hasOneUse()) {
876 mlir::Operation *user = *call.getResult().getUsers().begin();
877 if (auto store = mlir::dyn_cast<cir::StoreOp>(user))
878 if (store.getValue() == call.getResult() &&
879 store.getAddr().getType() == ptrTy &&
880 mlir::DominanceInfo().properlyDominates(store.getAddr(), call)) {
881 sretSlot = store.getAddr();
882 reuseStore = store;
883 }
884 }
885 if (!sretSlot) {
886 auto alloca = cir::AllocaOp::create(
887 builder, call.getLoc(), ptrTy,
888 /*name=*/builder.getStringAttr("sret"),
889 /*alignment=*/builder.getI64IntegerAttr(sretAlign));
890 sretSlot = alloca;
891 }
892
894 sretArgs.push_back(sretSlot);
895 sretArgs.append(newArgs.begin(), newArgs.end());
896
897 mlir::Type sretVoidTy = cir::VoidType::get(ctx);
898 prependIndirectCallee(call, sretArgs, sretVoidTy, builder);
899 auto newCall = cir::CallOp::create(
900 builder, call.getLoc(), call.getCalleeAttr(), sretVoidTy, sretArgs);
901 for (mlir::NamedAttribute attr : call->getAttrs())
902 if (!newCall->hasAttr(attr.getName()))
903 newCall->setAttr(attr.getName(), attr.getValue());
904
905 // Shape the per-argument attrs exactly as the non-sret path does
906 // (signext / zeroext for Extend, drop Ignore slots, byval / align for
907 // Indirect, flatten for Expand and Direct+canFlatten) before prepending the
908 // sret slot, so sret composes correctly with Extend / Ignore / Indirect /
909 // Expand / Direct+canFlatten args.
910 mlir::ArrayAttr argAttrs = call->getAttrOfType<mlir::ArrayAttr>("arg_attrs");
911 bool needsArgAttrUpdate =
912 llvm::any_of(fc.argInfos, [](const ArgClassification &ac) {
913 return ac.kind == ArgKind::Ignore || ac.kind == ArgKind::Extend ||
914 ac.kind == ArgKind::Indirect || ac.kind == ArgKind::Expand ||
915 getFlattenedCoercedType(ac);
916 });
917 if (needsArgAttrUpdate)
918 argAttrs = updateArgAttrs(ctx, origCallArgTypes, argAttrs, fc);
919 applySretSlotAttrs(newCall, argAttrs, origRetTy, sretAlign, builder);
920
921 if (reuseStore) {
922 // The callee now constructs directly into the destination slot, so the
923 // original store-from-result is redundant; dropping it avoids a
924 // byte-copy of the record.
925 reuseStore->erase();
926 } else {
927 builder.setInsertionPointAfter(newCall);
928 auto load = cir::LoadOp::create(builder, call.getLoc(), origRetTy, sretSlot,
929 /*isDeref=*/mlir::UnitAttr(),
930 /*isVolatile=*/mlir::UnitAttr(),
931 /*is_nontemporal=*/mlir::UnitAttr(),
932 /*alignment=*/mlir::IntegerAttr(),
933 /*sync_scope=*/cir::SyncScopeKindAttr(),
934 /*mem_order=*/cir::MemOrderAttr(),
935 /*invariant=*/mlir::UnitAttr());
936 call.getResult().replaceAllUsesWith(load);
937 }
938 call->erase();
939}
940
941} // namespace
942
944 mlir::FunctionOpInterface funcOpInterface, const FunctionClassification &fc,
945 mlir::OpBuilder &builder) {
946 // The pass driver (CallConvLoweringPass) only ever hands us cir.func ops.
947 // Cast once at the top so the rest of the function reads in CIR's own
948 // vocabulary, and so we can dispatch to the CIRGlobalValueInterface for
949 // isDefinition() (FunctionOpInterface alone does not inherit from
950 // CIRGlobalValueInterface).
951 cir::FuncOp funcOp = mlir::cast<cir::FuncOp>(funcOpInterface);
952
953 if (!fc.needsRewrite())
954 return mlir::success();
955
956 ArrayRef<mlir::Type> oldArgTypes = funcOp.getArgumentTypes();
957 ArrayRef<mlir::Type> oldResultTypes = funcOp.getResultTypes();
958 mlir::MLIRContext *ctx = funcOp->getContext();
959
960 // CIR follows LLVM IR's single-result rule: a function returns either
961 // zero or one value. Document the invariant so a future multi-result
962 // change forces us to revisit the return-handling below.
963 assert(oldResultTypes.size() <= 1 &&
964 "CIR functions return zero or one value");
965
966 SmallVector<mlir::Type> newArgTypes;
967 if (mlir::failed(buildNewArgTypes(oldArgTypes, fc, newArgTypes,
968 [&]() { return funcOp.emitOpError(); })))
969 return mlir::failure();
970
971 mlir::Type voidTy = cir::VoidType::get(ctx);
972 mlir::Type origRetTy = oldResultTypes.empty() ? voidTy : oldResultTypes[0];
973 mlir::Type newRetTy = computeNewReturnType(
974 origRetTy, fc.returnInfo, ctx, [&]() { return funcOp.emitOpError(); });
975 if (!newRetTy)
976 return mlir::failure();
977 SmallVector<mlir::Type> newResultTypes = {newRetTy};
978
979 // sret return: the value is returned through a pointer the ABI inserts as
980 // argument 0. This pointer is not part of the function's source-level
981 // signature -- it is synthesized here -- and the wire return type was
982 // already set to void by computeNewReturnType. Every classification index
983 // therefore maps to a block argument shifted by one in the body handling
984 // below.
985 bool hasSRet =
986 fc.returnInfo.kind == ArgKind::Indirect && !oldResultTypes.empty();
987 if (hasSRet)
988 newArgTypes.insert(newArgTypes.begin(), cir::PointerType::get(origRetTy));
989
990 if (funcOp.isDefinition()) {
991 mlir::Region &body = funcOp->getRegion(0);
992 if (!body.empty()) {
993 // Prepend the sret pointer block argument and route every cir.return
994 // through it before any index-based argument handling below (which
995 // then accounts for the +1 offset).
996 if (hasSRet) {
997 body.front().insertArgument(0u, cir::PointerType::get(origRetTy),
998 funcOp.getLoc());
999 insertSRetStores(funcOp, origRetTy, builder);
1000 }
1001
1002 // In-body coercion for Direct-with-coerce / Extend args: change
1003 // block-arg types to the coerced types and insert a memory roundtrip
1004 // at the top of the entry block that converts each coerced value back
1005 // to its original type, then route existing body uses (including
1006 // in-body cir.call operands) through the recovered value. Done before
1007 // the Ignore-drop below so the entry block argument indices used here
1008 // still refer to the original positions.
1009 insertArgCoercion(funcOp, fc, builder, dl, hasSRet);
1010
1011 // Direct return with coerced type: insert a coercion at every
1012 // cir.return so the returned value matches the (coerced) return
1013 // type in the new function signature set below.
1014 if (fc.returnInfo.kind == ArgKind::Direct && fc.returnInfo.coercedType &&
1015 !oldResultTypes.empty() && fc.returnInfo.coercedType != origRetTy)
1016 insertReturnCoercion(funcOp, origRetTy, fc.returnInfo.coercedType,
1017 builder, dl);
1018
1019 mlir::Block &entry = body.front();
1020
1021 // Drop each Ignored argument's block argument, replacing any remaining
1022 // body uses with a poison constant (an Ignore arg is not passed at the
1023 // ABI level, so any use is vacuous; poison says exactly that). Walk
1024 // forward with a running block-argument index that mirrors
1025 // insertArgCoercion: an Expand arg or a Direct+canFlatten arg occupies N
1026 // slots, every other kept kind one. On erase, do not advance the index
1027 // -- the next block argument shifts into the vacated slot.
1028 unsigned blockArgIdx = hasSRet ? 1 : 0;
1029 for (auto [i, ac] : llvm::enumerate(fc.argInfos)) {
1030 if (blockArgIdx >= entry.getNumArguments())
1031 break;
1032 if (ac.kind == ArgKind::Ignore) {
1033 mlir::BlockArgument arg = entry.getArgument(blockArgIdx);
1034 if (!arg.use_empty()) {
1035 builder.setInsertionPointToStart(&entry);
1036 mlir::Value poison =
1037 createIgnoredValue(builder, funcOp.getLoc(), arg.getType());
1038 arg.replaceAllUsesWith(poison);
1039 }
1040 entry.eraseArgument(blockArgIdx);
1041 continue;
1042 }
1043 if (cir::RecordType flatTy = getFlattenedCoercedType(ac))
1044 blockArgIdx += flatTy.getNumElements();
1045 else if (ac.kind == ArgKind::Expand)
1046 blockArgIdx += cast<cir::RecordType>(oldArgTypes[i]).getNumElements();
1047 else
1048 ++blockArgIdx;
1049 }
1050 }
1051
1052 // When the return is classified Ignore but the original function had
1053 // a non-void return type, every cir.return becomes a naked return.
1054 // This relies on the invariant that computeNewReturnType has set
1055 // newRetTy = void for Ignore above, and that the function type is
1056 // updated below to match. Asserting this keeps the dependency
1057 // explicit.
1058 if (fc.returnInfo.kind == ArgKind::Ignore && !oldResultTypes.empty()) {
1059 assert(mlir::isa<cir::VoidType>(newRetTy) &&
1060 "Ignore-return path requires the new return type to be void");
1062 funcOp.walk([&](cir::ReturnOp r) { returns.push_back(r); });
1063 for (cir::ReturnOp r : returns) {
1064 if (r.getNumOperands() == 0)
1065 continue;
1066 builder.setInsertionPoint(r);
1067 cir::ReturnOp::create(builder, r.getLoc());
1068 r.erase();
1069 }
1070 }
1071 }
1072
1073 mlir::Type newFnTy = funcOp.cloneTypeWith(newArgTypes, newResultTypes);
1074 funcOp.setFunctionTypeAttr(mlir::TypeAttr::get(newFnTy));
1075
1076 // Rebuild arg_attrs when the function has an sret slot (slot 0 needs the
1077 // sret attribute set) or any arg is Ignore (dropped from the output array),
1078 // Extend (needs llvm.signext / llvm.zeroext), Indirect (needs
1079 // llvm.byval / llvm.align), Expand or Direct+canFlatten (both change the
1080 // argument count).
1081 bool needsArgAttrUpdate =
1082 hasSRet || llvm::any_of(fc.argInfos, [](const ArgClassification &ac) {
1083 return ac.kind == ArgKind::Ignore || ac.kind == ArgKind::Extend ||
1084 ac.kind == ArgKind::Indirect || ac.kind == ArgKind::Expand ||
1085 getFlattenedCoercedType(ac);
1086 });
1087 if (needsArgAttrUpdate) {
1088 auto existing = funcOp->getAttrOfType<mlir::ArrayAttr>("arg_attrs");
1089 mlir::ArrayAttr updated = updateArgAttrs(ctx, oldArgTypes, existing, fc);
1090 if (hasSRet) {
1091 // Prepend the sret slot's attribute dict (slot 0); the per-argument
1092 // dicts shift to slots 1..N. noalias is valid only on the callee's
1093 // parameter, so it is added only for definitions.
1094 SmallVector<mlir::NamedAttribute> sretAttrs = buildSretSlotAttrs(
1095 builder, origRetTy, fc.returnInfo.indirectAlign.value(),
1096 /*withNoalias=*/funcOp.isDefinition());
1098 withSret.push_back(mlir::DictionaryAttr::get(ctx, sretAttrs));
1099 llvm::append_range(withSret, updated);
1100 funcOp->setAttr("arg_attrs", mlir::ArrayAttr::get(ctx, withSret));
1101 } else {
1102 funcOp->setAttr("arg_attrs", updated);
1103 }
1104 }
1105
1106 // Rebuild res_attrs: layer llvm.signext / llvm.zeroext onto an Extend
1107 // return.
1108 if (fc.returnInfo.kind == ArgKind::Extend) {
1109 auto existing = funcOp->getAttrOfType<mlir::ArrayAttr>("res_attrs");
1110 funcOp->setAttr("res_attrs", updateResAttrs(ctx, existing, fc.returnInfo));
1111 }
1112
1113 return mlir::success();
1114}
1115
1116mlir::LogicalResult
1118 const FunctionClassification &fc,
1119 mlir::OpBuilder &builder) {
1120 // The classification covers exactly the callee's declared parameters, and
1121 // the rewrite below pairs it with the call's operands one for one. Both
1122 // directions of a mismatch have to be reported before the pass-through early
1123 // return, or a call whose declared parameters happen to be pass-through is
1124 // left as written with its surplus operands never classified.
1125 //
1126 // A surplus operand went through an ellipsis. A shortfall means the callee
1127 // was declared no_proto, which turns off the verifier's argument-count check
1128 // altogether.
1129 unsigned numOperands =
1130 mlir::cast<cir::CIRCallOpInterface>(callOp).getNumArgOperands();
1131 if (numOperands > fc.argInfos.size())
1132 return callOp->emitOpError()
1133 << "variadic arguments not yet implemented in CallConvLowering";
1134 if (numOperands < fc.argInfos.size())
1135 return callOp->emitOpError()
1136 << "call passes fewer arguments than the callee declares, which is "
1137 "not yet implemented in CallConvLowering";
1138
1139 if (!fc.needsRewrite())
1140 return mlir::success();
1141
1142 if (mlir::isa<cir::TryCallOp>(callOp))
1143 return callOp->emitOpError()
1144 << "TryCallOp not yet implemented in CallConvLowering";
1145
1146 auto call = mlir::cast<cir::CallOp>(callOp);
1147 mlir::MLIRContext *ctx = callOp->getContext();
1148 mlir::Block *slotBlock = coercionSlotBlock(call);
1149
1150 builder.setInsertionPoint(call);
1151
1153 mlir::ValueRange argOperands = call.getArgOperands();
1154 newArgs.reserve(argOperands.size());
1155
1156 // Whole-struct loads replaced by direct member loads for Expand operands.
1157 // They can only be erased once the original call (their remaining user) is
1158 // gone, so collect them and erase the dead ones at the end.
1159 SmallVector<cir::LoadOp> replacedWholeLoads;
1160
1161 // Capture original arg types before building newArgs (byval slots change
1162 // the wire argument from T to !cir.ptr<T>, so we save the pre-rewrite
1163 // types here for use in updateArgAttrs).
1164 SmallVector<mlir::Type> origCallArgTypes;
1165 llvm::append_range(origCallArgTypes, argOperands.getTypes());
1166 for (auto [idx, ac] : llvm::enumerate(fc.argInfos)) {
1167 if (ac.kind == ArgKind::Ignore)
1168 continue;
1169 mlir::Value arg = argOperands[idx];
1170 if (cir::RecordType flatTy = getFlattenedCoercedType(ac)) {
1171 // Direct + canFlatten: pass one scalar call argument per field of the
1172 // ABI-coerced struct. When the original and coerced types differ in
1173 // layout, coerce through a memory slot and read each field with
1174 // cir.get_member + cir.load from that slot. When the types already
1175 // match, decompose the struct value directly (reading from its source
1176 // alloca when possible).
1177 if (arg.getType() != flatTy) {
1178 SmallPtrSet<mlir::Operation *, 4> coercionOps;
1179 mlir::Value coercedPtr = emitCoercionToMemory(
1180 builder, call.getLoc(), flatTy, arg, slotBlock, dl, coercionOps);
1181 for (auto [f, fieldTy] : llvm::enumerate(flatTy.getMembers())) {
1182 mlir::Type fieldPtrTy = cir::PointerType::get(fieldTy);
1183 auto fieldPtr =
1184 cir::GetMemberOp::create(builder, call.getLoc(), fieldPtrTy,
1185 coercedPtr, /*name=*/"", /*index=*/f);
1186 newArgs.push_back(cir::LoadOp::create(builder, call.getLoc(), fieldTy,
1187 fieldPtr.getResult()));
1188 }
1189 } else {
1190 emitStructFieldArgs(builder, call.getLoc(), arg, flatTy, newArgs,
1191 replacedWholeLoads);
1192 }
1193 } else if (ac.kind == ArgKind::Expand) {
1194 // Decompose the struct value into its constituent scalar fields and
1195 // pass each as a separate argument.
1196 auto recTy = cast<cir::RecordType>(arg.getType());
1197 assert(recTy.isStruct() &&
1198 "Expand classification requires a struct type, not a union");
1199 emitStructFieldArgs(builder, call.getLoc(), arg, recTy, newArgs,
1200 replacedWholeLoads);
1201 } else if (ac.kind == ArgKind::Direct && ac.coercedType &&
1202 arg.getType() != ac.coercedType) {
1203 arg = emitCoercion(builder, call.getLoc(), ac.coercedType, arg, slotBlock,
1204 dl);
1205 newArgs.push_back(arg);
1206 } else if (ac.kind == ArgKind::Indirect) {
1207 // byval and byref: allocate a stack slot, copy the value in, and pass
1208 // the pointer. The alloca+store pattern is identical for both; the
1209 // attribute distinction (llvm.byval vs llvm.byref) is applied by
1210 // updateArgAttrs. byref does not receive llvm.noalias or llvm.noundef
1211 // because it does not assert exclusive ownership of the storage.
1212 mlir::Type argTy = arg.getType();
1213 auto ptrTy = cir::PointerType::get(argTy);
1214 uint64_t align = ac.indirectAlign.value();
1215 StringRef slotName = ac.byVal ? "byval" : "byref";
1216 auto slot = cir::AllocaOp::create(builder, call.getLoc(), ptrTy,
1217 builder.getStringAttr(slotName),
1218 builder.getI64IntegerAttr(align));
1219 cir::StoreOp::create(builder, call.getLoc(), arg, slot);
1220 arg = slot;
1221 newArgs.push_back(arg);
1222 } else {
1223 newArgs.push_back(arg);
1224 }
1225 }
1226
1227 bool hasResult = call.getNumResults() > 0;
1228 mlir::Type origRetTy =
1229 hasResult ? call.getResult().getType() : cir::VoidType::get(ctx);
1230
1231 // An indirect (sret) return has a different call shape than the coerce /
1232 // extend / ignore return handling further down (the value is returned
1233 // through a prepended pointer slot, not as a result), so dispatch to a
1234 // dedicated helper for it; everything below handles the by-value returns.
1235 if (fc.returnInfo.kind == ArgKind::Indirect && hasResult) {
1236 rewriteIndirectReturnCall(call, fc, newArgs, origRetTy, origCallArgTypes,
1237 builder);
1238 return mlir::success();
1239 }
1240
1241 mlir::Type callRetTy = origRetTy;
1242 if (fc.returnInfo.kind == ArgKind::Ignore && hasResult)
1243 callRetTy = cir::VoidType::get(ctx);
1244 bool returnNeedsCoercion =
1245 hasResult && fc.returnInfo.kind == ArgKind::Direct &&
1246 fc.returnInfo.coercedType && fc.returnInfo.coercedType != origRetTy;
1247 if (returnNeedsCoercion)
1248 callRetTy = fc.returnInfo.coercedType;
1249
1250 builder.setInsertionPoint(call);
1251 prependIndirectCallee(call, newArgs, callRetTy, builder);
1252 auto newCall = cir::CallOp::create(builder, call.getLoc(),
1253 call.getCalleeAttr(), callRetTy, newArgs);
1254 for (mlir::NamedAttribute attr : call->getAttrs())
1255 if (!newCall->hasAttr(attr.getName()))
1256 newCall->setAttr(attr.getName(), attr.getValue());
1257
1258 // Direct return with coercion: the new call returns the coerced type;
1259 // emit a coercion back to the original type for the call's existing uses.
1260 if (returnNeedsCoercion) {
1261 builder.setInsertionPointAfter(newCall);
1262 mlir::Value coercedBack = emitCoercion(builder, call.getLoc(), origRetTy,
1263 newCall.getResult(), slotBlock, dl);
1264 call.getResult().replaceAllUsesWith(coercedBack);
1265 }
1266
1267 // Layer llvm.signext / llvm.zeroext onto the new call's arg_attrs and
1268 // res_attrs for Extend args/return. Ignore args require a rebuild because
1269 // their slots are dropped; Indirect args need llvm.byval / llvm.align;
1270 // Expand and Direct+canFlatten args change the argument count.
1271 bool needsArgAttrUpdate =
1272 llvm::any_of(fc.argInfos, [](const ArgClassification &ac) {
1273 return ac.kind == ArgKind::Ignore || ac.kind == ArgKind::Extend ||
1274 ac.kind == ArgKind::Indirect || ac.kind == ArgKind::Expand ||
1275 getFlattenedCoercedType(ac);
1276 });
1277 if (needsArgAttrUpdate) {
1278 auto existing = call->getAttrOfType<mlir::ArrayAttr>("arg_attrs");
1279 newCall->setAttr("arg_attrs",
1280 updateArgAttrs(ctx, origCallArgTypes, existing, fc));
1281 }
1282 if (fc.returnInfo.kind == ArgKind::Extend) {
1283 auto existing = call->getAttrOfType<mlir::ArrayAttr>("res_attrs");
1284 newCall->setAttr("res_attrs", updateResAttrs(ctx, existing, fc.returnInfo));
1285 }
1286
1287 if (hasResult && fc.returnInfo.kind == ArgKind::Ignore) {
1288 // The new call returns void, but the original call's result may still
1289 // have uses. Substitute a poison constant of the original type so
1290 // those uses remain well-formed without pretending we have a real
1291 // value at the ABI boundary.
1292 if (!call.getResult().use_empty()) {
1293 builder.setInsertionPointAfter(newCall);
1294 mlir::Value poison =
1295 createIgnoredValue(builder, call.getLoc(), origRetTy);
1296 call.getResult().replaceAllUsesWith(poison);
1297 }
1298 } else if (hasResult && !returnNeedsCoercion) {
1299 // returnNeedsCoercion already wired up the coerced result above.
1300 call.getResult().replaceAllUsesWith(newCall.getResult());
1301 }
1302
1303 call->erase();
1304
1305 // Now that the original call is gone, drop any whole-struct loads whose
1306 // members we read directly from the source alloca, if nothing else uses
1307 // them. A single load can feed several Expand operands (e.g. after CSE
1308 // merges identical loads), so dedupe before erasing to avoid touching a
1309 // freed op twice.
1310 SmallPtrSet<mlir::Operation *, 4> erased;
1311 for (cir::LoadOp wholeLoad : replacedWholeLoads)
1312 if (erased.insert(wholeLoad).second && wholeLoad.use_empty())
1313 wholeLoad->erase();
1314
1315 return mlir::success();
1316}
1317
1319 cir::FuncOp funcOp,
1320 mlir::OpBuilder &builder) {
1321 auto oldPtrTy = mlir::cast<cir::PointerType>(addrOp.getAddr().getType());
1322 cir::FuncType newFuncTy = funcOp.getFunctionType();
1323 // An extension rides on an argument attribute and leaves the signature
1324 // alone, so such a callee still matches the written type.
1325 if (newFuncTy == oldPtrTy.getPointee())
1326 return;
1327
1328 // The verifier requires the retype even when nothing reads the address.
1329 addrOp.getAddr().setType(cir::PointerType::get(newFuncTy));
1330 if (addrOp.getAddr().use_empty())
1331 return;
1332
1333 // A later indirect call through the written type stays correct, since it
1334 // reclassifies from that type and coerces to the signature funcOp was
1335 // rewritten to. Ellipsis arguments are the exception the indirect-call
1336 // path reports rather than lowers.
1337 mlir::OpBuilder::InsertionGuard guard(builder);
1338 builder.setInsertionPointAfter(addrOp);
1339 auto bitcast = cir::CastOp::create(builder, addrOp.getLoc(), oldPtrTy,
1340 cir::CastKind::bitcast, addrOp.getAddr());
1341 addrOp.getAddr().replaceAllUsesExcept(bitcast.getResult(), bitcast);
1342}
void rewriteFunctionAddress(cir::GetGlobalOp addrOp, cir::FuncOp funcOp, mlir::OpBuilder &builder)
Retype addrOp, which holds the address of funcOp, to the signature funcOp was rewritten to,...
mlir::LogicalResult rewriteFunctionDefinition(mlir::FunctionOpInterface funcOp, const mlir::abi::FunctionClassification &fc, mlir::OpBuilder &builder) override
mlir::LogicalResult rewriteCallSite(mlir::Operation *callOp, const mlir::abi::FunctionClassification &fc, mlir::OpBuilder &builder) override
C++ view class that accepts both !cir.struct and !cir.union types.
Definition CIRTypes.h:103
llvm::ArrayRef< mlir::Type > getMembers() const
Definition CIRTypes.cpp:519
bool isStruct() const
Definition CIRTypes.cpp:549
size_t getNumElements() const
Definition CIRTypes.h:130
const internal::VariadicAllOfMatcher< Attr > attr