[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:48 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();
+}
----------------
jeanPerier wrote:

I do not see the point of all the `evaluateStaticInteger` complexity to try to lookup for 1.
As far as Fortran/FIR is concerned, `1` will fit on all integer types and it is safe to walk all the integer converts looking for a constant without all the complex overflow analysis.

As one/zero are an important value, I would actually favor that we update fir.convert folder to just always propagate 1 and zero through converts regardless of the integer types. Then you should be able to just use `fir::getIntIfConstant` and save a couple hundred lines.

Note that values that would become 1 perf overflow are not specified, so we should not start optimizing code where the `step 1` is the outcome of some integer overflow in the step computation.

https://github.com/llvm/llvm-project/pull/222723


More information about the flang-commits mailing list