[flang-commits] [flang] [llvm] [flang] Support static-unit array slices in FIR LoopVersioning (PR #222723)
Sergey Shcherbinin via flang-commits
flang-commits at lists.llvm.org
Thu Sep 24 03:17:00 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)
+ return reject("UnsupportedRankOrElementSize");
+
+ auto boxType = mlir::dyn_cast<fir::BaseBoxType>(arg.arg.getType());
+ if (!boxType)
+ return reject("UnsupportedDescriptor");
+ // This initial slice path accepts only descriptors whose direct element is
+ // a sequence. Descriptors with heap or pointer storage wrappers remain on
+ // the generic path.
+ auto sequenceType = mlir::dyn_cast<fir::SequenceType>(boxType.getEleTy());
+ if (!sequenceType || sequenceType.getDimension() != arg.rank)
+ return reject("UnsupportedDescriptorElement");
+ return sequenceType;
+}
+
+/// Preflight properties that belong to one physical fir.array_coor access.
+/// The returned slice handle lets descriptor-wide preflight reuse slice-level
+/// classification without resolving the carrier a second time.
+static mlir::FailureOr<fir::SliceOp> validateSliceAccess(
+ fir::ArrayCoorOp op, const ArgInfo &arg, fir::SequenceType sequenceType,
+ const SliceTargetInfo &target, SliceWidthCache &widthCache) {
+ auto reject = [&](llvm::StringRef reason) -> mlir::FailureOr<fir::SliceOp> {
+ LLVM_DEBUG(llvm::dbgs()
+ << "Sliced array_coor rejected: " << reason << '\n');
+ return mlir::failure();
+ };
+
+ fir::SliceOp slice = op.getSlice().getDefiningOp<fir::SliceOp>();
+ if (!slice)
+ return reject("UnsupportedSliceCarrier");
+ if (fir::unwrapRefType(op.getType()) != sequenceType.getEleTy())
+ return reject("ResultElementTypeMismatch");
+ if (op.getIndices().size() != arg.rank)
+ return reject("UnsupportedIndexConvention");
+ if (mlir::Value shape = op.getShape()) {
+ auto shapeOp = shape.getDefiningOp<fir::ShapeOp>();
+ if (!shapeOp)
+ return reject("UnsupportedShapeCarrier");
----------------
SergeyShch01 wrote:
Removed. The rewritten patch relies on the existing lower-bound handling and only checks the slice properties required for folding it into the flat index.
https://github.com/llvm/llvm-project/pull/222723
More information about the flang-commits
mailing list