[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