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