[flang-commits] [flang] [llvm] [flang] Support static-unit array slices in FIR LoopVersioning (PR #222723)
via flang-commits
flang-commits at lists.llvm.org
Fri Sep 18 12:51:50 PDT 2026
================
@@ -229,6 +418,452 @@ static mlir::Value normaliseVal(mlir::Value val) {
return unwrapPassThroughOps(unwrapReboxOp(val));
}
+/// Collect slice-specific state for one indexing operation during the existing
+/// loop walk. A true result means that an enabled sliced access was handled
+/// completely and must not enter the slice-free collection path. When slice
+/// support is disabled, the function preserves the existing rejection and
+/// lets that path perform its normal cleanup. A slice-free access may reject
+/// an earlier sliced use of the same descriptor but otherwise continues there.
+static bool collectSliceUse(fir::DoLoopOp loop, mlir::Operation *op,
+ ArgInfo &info, bool isOriginalArgument,
+ ArgsUsageInLoop &argsInLoop,
+ std::unique_ptr<LoopSliceUses> &loopUses,
+ std::optional<SliceDiscovery> &slices,
+ mlir::DominanceInfo &domInfo,
+ const fir::KindMapping &kindMap,
+ const mlir::DataLayout &dataLayout) {
+ auto arrayCoor = mlir::dyn_cast<fir::ArrayCoorOp>(op);
+ if (!arrayCoor || !arrayCoor.getSlice()) {
+ if (slices) {
+ // TODO: Support descriptors used by both sliced and slice-free accesses.
+ // Until then, this combination is intentionally unsupported.
+ // One slice-free direct owner makes the descriptor ineligible without
+ // retaining that owner's operations or ArgInfo.
+ slices->rejected.insert(info.arg);
+ if (loopUses)
+ if (auto found = loopUses->indices.find(info.arg);
+ found != loopUses->indices.end())
+ loopUses->uses[found->second].rejected = true;
+ }
+ return false;
+ }
+
+ if (!slices) {
+ argsInLoop.cannotTransform.insert(info.arg);
+ return false;
+ }
+
+ // A descriptor-wide decision cannot recover after any direct owner is
+ // rejected. Keep propagating the rejection without allocating access plans
+ // that preflight can never publish.
+ if (slices->rejected.contains(info.arg)) {
+ argsInLoop.cannotTransform.insert(info.arg);
+ argsInLoop.usageInfo.erase(info.arg);
+ return true;
+ }
+ if (!loopUses)
+ loopUses = std::make_unique<LoopSliceUses>();
+ auto recorded = recordSliceUse(loop, info.arg, arrayCoor, info, *loopUses,
+ slices->nextUseOrder);
+ if (!recorded) {
+ argsInLoop.cannotTransform.insert(info.arg);
+ argsInLoop.usageInfo.erase(info.arg);
+ return true;
+ }
+ auto [useIndex, node, firstUse] = *recorded;
+
+ // Dominance is owner-local, while rank and element size are invariant for
+ // the concrete descriptor. Reuse the initial argument facts or a retained
+ // owner's facts instead of repeating the layout query in every owner.
+ if (firstUse) {
+ if (!domInfo.dominates(info.arg, loop)) {
+ loopUses->uses[useIndex].rejected = true;
+ } else if (auto found = slices->descriptors.find(info.arg);
+ found != slices->descriptors.end() &&
+ !found->second.sliced.empty()) {
+ const ArgInfo &previous = found->second.sliced.front()->info;
+ info.rank = previous.rank;
+ info.size = previous.size;
+ } else if (!isOriginalArgument) {
+ std::tie(info.rank, info.size) =
+ getRankAndElementSize(kindMap, dataLayout, info.arg);
+ }
+ node->info = info;
+ if (info.rank == 0 || info.size == 0)
+ loopUses->uses[useIndex].rejected = true;
+ }
+
+ // Preserve the existing collection rejection until descriptor-wide
+ // preflight publishes the complete frozen plan.
+ argsInLoop.cannotTransform.insert(info.arg);
+ argsInLoop.usageInfo.erase(info.arg);
+ return true;
+}
+
+/// Return whether direct byte addressing would bypass descriptor semantics.
+static bool hasUnsupportedSliceSemantics(mlir::Value value,
+ mlir::func::FuncOp func) {
+ if (fir::isa_volatile_type(value.getType()))
+ return true;
+ mlir::Value root = value;
+ while (fir::ReboxOp rebox = root.getDefiningOp<fir::ReboxOp>()) {
+ if (!fir::reboxPreservesContinuity(rebox,
+ /*mayHaveNonDefaultLowerBounds=*/true,
+ /*checkWhole=*/false))
+ break;
+ if (rebox.getOptional() || fir::isa_volatile_type(rebox.getType()) ||
+ fir::isa_volatile_type(rebox.getBox().getType()))
+ return true;
+ root = rebox.getBox();
+ }
+ while (true) {
+ if (fir::DeclareOp declare = root.getDefiningOp<fir::DeclareOp>()) {
+ auto variable =
+ mlir::cast<fir::FortranVariableOpInterface>(declare.getOperation());
+ auto attrs = declare.getFortranAttrs();
+ if (variable.isOptional() || fir::isa_volatile_type(declare.getType()) ||
+ fir::isa_volatile_type(declare.getMemref().getType()) ||
+ (attrs &&
+ fir::bitEnumContainsAny(
+ *attrs, fir::FortranVariableFlagsEnum::fortran_volatile)))
+ return true;
+ root = declare.getMemref();
+ continue;
+ }
+ if (auto pack = root.getDefiningOp<fir::PackArrayOp>()) {
+ if (fir::isa_volatile_type(pack.getType()) ||
+ fir::isa_volatile_type(pack.getArray().getType()))
+ return true;
+ root = pack.getArray();
+ continue;
+ }
+ break;
+ }
+ if (auto blockArg = mlir::dyn_cast<mlir::BlockArgument>(root);
+ blockArg && blockArg.getOwner() == &func.getBody().front()) {
+ unsigned number = blockArg.getArgNumber();
+ return func.getArgAttr(number, fir::getOptionalAttrName()) ||
+ func.getArgAttr(number, fir::getVolatileAttrName());
+ }
+ return false;
+}
+
+/// Return whether a value is produced by fir.undefined.
+static bool isUndefined(mlir::Value value) {
+ return value && mlir::isa_and_nonnull<fir::UndefOp>(value.getDefiningOp());
+}
+
+/// Classify a slice triple exactly as generic XArrayCoor lowering does.
+static SliceTripleKind classifySliceTriple(mlir::Value lower, mlir::Value upper,
+ mlir::Value step) {
+ if (isUndefined(upper))
+ return SliceTripleKind::Scalar;
+ if (!isUndefined(lower) && !isUndefined(step))
+ return SliceTripleKind::Section;
+ return SliceTripleKind::Unsupported;
+}
+
+/// Derive the address contract of one top-level FIR module.
+/// A nested builtin module can be lowered either under its own contract or by
+/// an ancestor module pass, so this initial slice path rejects it fail closed.
+static std::optional<SliceTargetInfo>
+getSliceTargetInfo(mlir::ModuleOp module,
+ const fir::KindMapping &moduleKindMap) {
+ if (module->getParentOfType<mlir::ModuleOp>())
+ return std::nullopt;
+
+ auto getIndexWidth = [](mlir::ModuleOp owner) -> std::optional<unsigned> {
+ llvm::StringRef layoutString;
+ if (auto layout = owner->getAttrOfType<mlir::StringAttr>(
+ mlir::LLVM::LLVMDialect::getDataLayoutAttrName()))
+ layoutString = layout.getValue();
+ auto parsedLayout = llvm::DataLayout::parse(layoutString);
+ if (!parsedLayout) {
+ llvm::consumeError(parsedLayout.takeError());
+ return std::nullopt;
+ }
+ // FIR-to-LLVM lowers abstract MLIR index values to i32 only for 32-bit
+ // pointers and to i64 otherwise. Generic XArrayCoor computes boxed byte
+ // offsets in i64, but a 32-bit GEP observes the same low address bits.
+ return parsedLayout->getPointerSizeInBits(0) == 32 ? 32u : 64u;
+ };
+
+ std::optional<unsigned> indexWidth = getIndexWidth(module);
+ if (!indexWidth)
+ return std::nullopt;
+ return SliceTargetInfo{*indexWidth, &moduleKindMap};
+}
+
+/// Return the effective width of an integer-like slice operand. Admission
+/// checks and constant-chain simulation share this cached kind-mapped result.
+static unsigned getSliceOperandWidth(mlir::Type type,
+ const SliceTargetInfo &target,
+ SliceWidthCache &cache) {
+ if (auto found = cache.find(type); found != cache.end())
+ return found->second;
+
+ unsigned width = 0;
+ if (mlir::isa<mlir::IndexType>(type)) {
+ width = target.indexWidth;
+ } else if (auto integer = mlir::dyn_cast<mlir::IntegerType>(type)) {
+ width = integer.getWidth();
+ } else if (auto integer = mlir::dyn_cast<fir::IntegerType>(type)) {
+ assert(target.kindMapping && "slice target must retain its kind mapping");
+ width = target.kindMapping->getIntegerBitsize(integer.getFKind());
+ }
+ cache.try_emplace(type, width);
+ return width;
+}
+
+/// Return whether fir.convert can preserve this operand in the index domain.
+/// The caller supplies the already computed operand width so later
+/// classification can reuse it without another kind-mapping lookup.
+static bool canConvertSliceOperand(mlir::Value value, unsigned width,
+ unsigned indexWidth) {
+ mlir::Type type = value.getType();
+ // Generic XArrayCoor lowering sign-extends narrow integer adaptors, while
+ // fir.convert preserves builtin unsigned extension. An exact target-width
+ // unsigned value requires no extension, so both paths consume the same bits.
+ // Wider values remain excluded by the lossless width ceiling below.
+ if (auto integer = mlir::dyn_cast<mlir::IntegerType>(type);
+ integer && integer.isUnsigned() && width != indexWidth)
+ return false;
+ return width > 1 && width <= indexWidth;
+}
+
+/// Return whether integer widening from this source uses zero extension.
+static bool isZeroExtendedSliceInteger(mlir::Type type) {
+ auto integer = mlir::dyn_cast<mlir::IntegerType>(type);
+ return integer && (integer.isUnsigned() ||
+ (integer.isSignless() && integer.getWidth() == 1));
+}
+
+/// Evaluate one constant integer conversion chain for the module contract.
+/// Truncation, signed extension, builtin i1 extension, and FIR kind widths
+/// mirror ConvertOpConversion. Every intermediate result is retained so a
+/// later query can resume at the nearest previously evaluated predecessor.
+static std::optional<StaticIntegerState>
+evaluateStaticInteger(mlir::Value value, const SliceTargetInfo &target,
+ StaticIntegerCache &cache, SliceWidthCache &widthCache) {
+ if (auto found = cache.find(value); found != cache.end())
+ return found->second;
+
+ llvm::SmallVector<fir::ConvertOp, 4> conversions;
+ mlir::Value source = value;
+ while (!cache.contains(source)) {
+ auto convert = source.getDefiningOp<fir::ConvertOp>();
+ if (!convert)
+ break;
+ if (!fir::isa_integer(source.getType()) ||
+ !fir::isa_integer(convert.getValue().getType())) {
+ cache.try_emplace(source, std::nullopt);
+ break;
+ }
+ conversions.push_back(convert);
+ source = convert.getValue();
+ }
+
+ constexpr unsigned addressIndexWidth = 64;
+ std::optional<StaticIntegerState> state;
+ if (auto found = cache.find(source); found != cache.end()) {
+ state = found->second;
+ } else {
+ std::optional<llvm::APInt> constant = fir::getIntIfConstant(source);
+ unsigned sourceWidth =
+ getSliceOperandWidth(source.getType(), target, widthCache);
+ // Do not seed evaluation from an i1 constant. A direct i1-to-index
+ // conversion is sign-extended by generic lowering. An explicit
+ // i1-to-wider-integer conversion is zero-extended, but that chain remains
+ // conservatively rejected unless canonicalization has already materialized
+ // the wider constant.
+ if (constant && sourceWidth > 1) {
+ unsigned retainedWidth = std::min(sourceWidth, addressIndexWidth);
+ llvm::APInt retained = isZeroExtendedSliceInteger(source.getType())
+ ? constant->zextOrTrunc(retainedWidth)
+ : constant->sextOrTrunc(retainedWidth);
+ state = StaticIntegerState{sourceWidth, std::move(retained)};
+ }
+ cache.try_emplace(source, state);
+ }
+
+ for (fir::ConvertOp convert : llvm::reverse(conversions)) {
+ if (state) {
+ mlir::Type fromType = convert.getValue().getType();
+ mlir::Type toType = convert.getType();
+ unsigned fromWidth = getSliceOperandWidth(fromType, target, widthCache);
+ unsigned toWidth = getSliceOperandWidth(toType, target, widthCache);
+ unsigned retainedFromWidth = std::min(fromWidth, addressIndexWidth);
+ if (!fromWidth || !toWidth || state->width != fromWidth ||
+ state->bits.getBitWidth() != retainedFromWidth) {
+ state.reset();
+ } else {
+ unsigned retainedToWidth = std::min(toWidth, addressIndexWidth);
+ llvm::APInt retained = state->bits;
+ if (retainedToWidth < retainedFromWidth)
+ retained = retained.trunc(retainedToWidth);
+ else if (retainedToWidth > retainedFromWidth)
+ retained = isZeroExtendedSliceInteger(fromType)
+ ? retained.zext(retainedToWidth)
+ : retained.sext(retainedToWidth);
+ state = StaticIntegerState{toWidth, std::move(retained)};
+ }
+ }
+ cache.try_emplace(convert.getResult(), state);
+ }
+ return state;
+}
+
+/// Return whether an integer value becomes positive one through fir.convert
+/// under the top-level module contract. Analysis runs before modification, so
+/// the result for the exact SSA value remains reusable throughout preflight.
+static bool isStaticOneInteger(mlir::Value value, const SliceTargetInfo &target,
+ StaticIntegerCache &cache,
+ SliceWidthCache &widthCache) {
+ std::optional<StaticIntegerState> state =
+ evaluateStaticInteger(value, target, cache, widthCache);
+ if (!state)
+ return false;
+ // Generic XArrayCoor uses a 64-bit address index and integerCast applies
+ // signed extension or truncation to the final step. The direct path does not
+ // materialize a proven unit step, so classify the value in that same domain.
+ constexpr unsigned addressIndexWidth = 64;
+ return state->bits.sextOrTrunc(addressIndexWidth).isOne();
+}
+
+/// Return whether a slice step uses a supported static-one form.
+/// A chain whose unevaluated constant root is i1 is conservatively rejected.
+/// Canonicalization may first fold an explicit i1-to-wider-integer zero
+/// extension, after which the resulting wider constant can be recognized as
+/// one. An intermediate i1 produced from a wider integer is modeled with
+/// fir.convert's zero extension, while a final i1 remains unsupported because
+/// generic XArrayCoor lowering sign-extends it. Wider step types need no
+/// target-index admission check because a proven unit step is not materialized
+/// by the direct path.
+static bool isStaticOneSliceStep(mlir::Value value,
+ const SliceTargetInfo &target,
+ StaticIntegerCache &cache,
+ SliceWidthCache &widthCache) {
+ return getSliceOperandWidth(value.getType(), target, widthCache) > 1 &&
+ isStaticOneInteger(value, target, cache, widthCache);
+}
+
+/// Preflight descriptor properties shared by every one of its sliced accesses.
+/// The returned sequence type is reused by access-level preflight so rank,
+/// element size, and descriptor type are checked once per descriptor.
+static mlir::FailureOr<fir::SequenceType>
+analyzeSliceDescriptor(const ArgInfo &arg, unsigned indexWidth) {
+ auto reject =
+ [&](llvm::StringRef reason) -> mlir::FailureOr<fir::SequenceType> {
+ LLVM_DEBUG(llvm::dbgs()
+ << "Sliced array_coor rejected: " << reason << '\n');
+ return mlir::failure();
+ };
+
+ assert((indexWidth == 32 || indexWidth == 64) &&
+ "slice target must provide a supported index width");
+ uint64_t maxElementSize =
+ indexWidth == 32
+ ? static_cast<uint64_t>(std::numeric_limits<std::int32_t>::max())
+ : static_cast<uint64_t>(std::numeric_limits<std::int64_t>::max());
+ if (arg.rank > CFI_MAX_RANK || arg.size > maxElementSize)
----------------
jeanPerier wrote:
That is not really a problem that should matter here I think. The lowering/optimizer pretty much universally assume that the size of arrays we compute must fit on index type in all the IR we generate.
Flang does not support 32bits target so far, and I think the right solution for this support will be either to change all our index type to i64, or to limit the maximum size of arrays supported on 32bit architecture to what can fit on an i32.
In any case, I do not think every pass should start to have code handling a situation that we do not support at all and whose support should not involve adding handling in every pass.
https://github.com/llvm/llvm-project/pull/222723
More information about the flang-commits
mailing list