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