[flang-commits] [flang] [flang][HLFIR] Preserve RHS lower bounds in SeparateAllocatableAssign (PR #202373)

via flang-commits flang-commits at lists.llvm.org
Mon Jun 8 08:51:10 PDT 2026


https://github.com/khaki3 created https://github.com/llvm/llvm-project/pull/202373

Example:
```fortran
type t
  integer, allocatable :: a(:)
end type
integer, pointer :: p(:)
allocate(p(-5:-3))
x = t(p)                  ! allocatable component <- source, lbound -5
print *, lbound(x%a, 1)   ! should be -5
```

In this code, the structure constructor assigns the allocatable component from a source with a non-default lower bound, so `lbound(x%a,1)` must be `-5`. Lowering wraps the source in an `hlfir.expr` and emits `hlfir.assign ... realloc`. `SeparateAllocatableAssign` split the realloc but assumed an `hlfir.expr` RHS always has lower bound `1`, reallocating the component with `1` instead of `-5`.

Fix: read the reallocation lower bounds from the entity that owns the RHS descriptor — the variable RHS itself, or the variable wrapped by `hlfir.as_expr`. Only truly synthetic exprs (elemental, array constructor) keep lower bound `1`.

>From 5523054454932e96bd743d58dbac31b993a78802 Mon Sep 17 00:00:00 2001
From: Kazuaki Matsumura <kmatsumura at nvidia.com>
Date: Mon, 8 Jun 2026 02:10:16 -0700
Subject: [PATCH 1/2] [flang][HLFIR] Preserve RHS lower bounds in
 SeparateAllocatableAssign

When SeparateAllocatableAssign splits an `hlfir.assign ... realloc`, the
lower bounds of the (re-)allocated LHS must come from the RHS descriptor.
The pass assumed an hlfir.expr RHS always has lower bound 1, which corrupts
structure-constructor assigns such as `t = x(f())`, where the allocatable
component must take the source's bounds (F2018 7.5.10): the component was
reallocated with lower bound 1 instead of LBOUND(source).

Take the lower bounds from the entity that actually carries them: the
variable for a variable RHS, or the variable wrapped by hlfir.as_expr for an
hlfir.expr RHS (e.g. an allocatable/pointer function result used in a
structure constructor). Synthetic expressions (elemental, array constructor)
still default to lower bound 1.
---
 .../Transforms/SeparateAllocatableAssign.cpp  | 33 +++++++++++++++----
 .../HLFIR/separate-allocatable-assign.fir     | 32 ++++++++++++++++++
 2 files changed, 58 insertions(+), 7 deletions(-)

diff --git a/flang/lib/Optimizer/HLFIR/Transforms/SeparateAllocatableAssign.cpp b/flang/lib/Optimizer/HLFIR/Transforms/SeparateAllocatableAssign.cpp
index 0160ff7d75f76..afc7acc4a56ce 100644
--- a/flang/lib/Optimizer/HLFIR/Transforms/SeparateAllocatableAssign.cpp
+++ b/flang/lib/Optimizer/HLFIR/Transforms/SeparateAllocatableAssign.cpp
@@ -115,14 +115,33 @@ class SeparateAllocatableAssignConversion
     llvm::SmallVector<mlir::Value> rhsExtents =
         hlfir::getIndexExtents(loc, builder, rhsShape);
 
-    // F2018 10.2.1.3: when the LHS is (re-)allocated, its lower bounds
-    // come from LBOUND(rhs).  For variable RHS, extract the actual lower
-    // bounds from the entity; for hlfir.expr RHS, LBOUND is always 1.
+    // When the LHS is (re-)allocated, its lower bounds come from the RHS
+    // descriptor, which Fortran lowering has already populated with the
+    // semantically-correct bounds:
+    //   - F2018 10.2.1.3 for intrinsic assignment: LBOUND(rhs);
+    //   - F2018 7.5.10 for a structure constructor: an allocatable source keeps
+    //     its bounds, other sources reset the lower bound to 1.
+    // Find the entity that carries those bounds:
+    //   - a variable RHS is itself the descriptor;
+    //   - an hlfir.expr produced by hlfir.as_expr from a variable (e.g. an
+    //     allocatable function result used in a structure constructor) keeps
+    //     the bounds in the wrapped variable's descriptor;
+    //   - any other hlfir.expr (elemental, array constructor, ...) has no
+    //     underlying descriptor, so its lower bounds are 1.
+    mlir::Value lboundSource;
+    if (!mlir::isa<hlfir::ExprType>(rhs.getType()))
+      lboundSource = assign.getRhs();
+    else if (auto asExpr = assign.getRhs().getDefiningOp<hlfir::AsExprOp>())
+      lboundSource = asExpr.getVar();
+
     llvm::SmallVector<mlir::Value> rhsLbounds;
-    if (!mlir::isa<hlfir::ExprType>(rhs.getType())) {
-      auto bounds = hlfir::genBounds(loc, builder, rhs);
-      for (auto &[lb, ub] : bounds)
-        rhsLbounds.push_back(lb);
+    if (lboundSource) {
+      hlfir::Entity boundsEntity{lboundSource};
+      if (boundsEntity.isArray()) {
+        auto bounds = hlfir::genBounds(loc, builder, boundsEntity);
+        for (auto &[lb, ub] : bounds)
+          rhsLbounds.push_back(lb);
+      }
     }
 
     fir::MutableBoxValue mutableBox(lhs.getFirBase(), /*lenParameters=*/{},
diff --git a/flang/test/HLFIR/separate-allocatable-assign.fir b/flang/test/HLFIR/separate-allocatable-assign.fir
index 97c664c38a94f..fb8ae9512ac63 100644
--- a/flang/test/HLFIR/separate-allocatable-assign.fir
+++ b/flang/test/HLFIR/separate-allocatable-assign.fir
@@ -179,3 +179,35 @@ func.func @test_lower_bounds(%arg0: !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32
 // Lower bound 10 should appear in the embox/store of the new allocation.
 // CHECK: %[[BOX:.*]] = fir.load %{{.*}} : !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>
 // CHECK: hlfir.assign %{{.*}} to %[[BOX]] : !fir.box<!fir.array<3xi32>>, !fir.box<!fir.heap<!fir.array<?xi32>>>
+
+// Test: lower bounds from an hlfir.expr RHS that wraps a variable (via
+// hlfir.as_expr) must be preserved during reallocation.  This is the
+// structure-constructor case `t = x(f())` where `f` is an allocatable function
+// result with a non-default lower bound: the allocatable component must take
+// the source's bounds (F2018 7.5.10).  The pass must read the lower bound from
+// the wrapped variable's descriptor instead of assuming 1.
+func.func @test_expr_lower_bounds(%arg0: !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>, %arg1: !fir.ref<!fir.array<3xi32>>) {
+  %c10 = arith.constant 10 : index
+  %c3 = arith.constant 3 : index
+  %true = arith.constant true
+  %shapeshift = fir.shape_shift %c10, %c3 : (index, index) -> !fir.shapeshift<1>
+
+  %dest:2 = hlfir.declare %arg0 {fortran_attrs = #fir.var_attrs<allocatable>, uniq_name = "_QFEdest"} : (!fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>) -> (!fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>, !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>)
+  %source:2 = hlfir.declare %arg1(%shapeshift) {uniq_name = "_QFEsource"} : (!fir.ref<!fir.array<3xi32>>, !fir.shapeshift<1>) -> (!fir.box<!fir.array<3xi32>>, !fir.ref<!fir.array<3xi32>>)
+  %expr = hlfir.as_expr %source#0 move %true : (!fir.box<!fir.array<3xi32>>, i1) -> !hlfir.expr<3xi32>
+
+  hlfir.assign %expr to %dest#0 realloc temporary_lhs : !hlfir.expr<3xi32>, !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>
+  hlfir.destroy %expr : !hlfir.expr<3xi32>
+  return
+}
+
+// CHECK-LABEL: func.func @test_expr_lower_bounds
+// The realloc should be separated with the wrapped variable's lower bound 10.
+// CHECK-NOT: hlfir.assign{{.*}}realloc
+// CHECK: %[[C10:.*]] = arith.constant 10 : index
+// CHECK: fir.if
+// CHECK: fir.allocmem
+// Lower bound 10 (not 1) must appear in the embox/store of the new allocation.
+// CHECK: fir.shape_shift %[[C10]]
+// CHECK: %[[BOX:.*]] = fir.load %{{.*}} : !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>
+// CHECK: hlfir.assign %{{.*}} to %[[BOX]] temporary_lhs : !hlfir.expr<3xi32>, !fir.box<!fir.heap<!fir.array<?xi32>>>

>From 35501ad2d3279195950687d9706117ff812a56eb Mon Sep 17 00:00:00 2001
From: Kazuaki Matsumura <kmatsumura at nvidia.com>
Date: Mon, 8 Jun 2026 02:54:06 -0700
Subject: [PATCH 2/2] [flang][HLFIR] Shorten lower-bound comments in
 SeparateAllocatableAssign

---
 .../Transforms/SeparateAllocatableAssign.cpp    | 17 ++++-------------
 .../test/HLFIR/separate-allocatable-assign.fir  |  4 ++--
 2 files changed, 6 insertions(+), 15 deletions(-)

diff --git a/flang/lib/Optimizer/HLFIR/Transforms/SeparateAllocatableAssign.cpp b/flang/lib/Optimizer/HLFIR/Transforms/SeparateAllocatableAssign.cpp
index afc7acc4a56ce..b4391f9069d48 100644
--- a/flang/lib/Optimizer/HLFIR/Transforms/SeparateAllocatableAssign.cpp
+++ b/flang/lib/Optimizer/HLFIR/Transforms/SeparateAllocatableAssign.cpp
@@ -115,19 +115,10 @@ class SeparateAllocatableAssignConversion
     llvm::SmallVector<mlir::Value> rhsExtents =
         hlfir::getIndexExtents(loc, builder, rhsShape);
 
-    // When the LHS is (re-)allocated, its lower bounds come from the RHS
-    // descriptor, which Fortran lowering has already populated with the
-    // semantically-correct bounds:
-    //   - F2018 10.2.1.3 for intrinsic assignment: LBOUND(rhs);
-    //   - F2018 7.5.10 for a structure constructor: an allocatable source keeps
-    //     its bounds, other sources reset the lower bound to 1.
-    // Find the entity that carries those bounds:
-    //   - a variable RHS is itself the descriptor;
-    //   - an hlfir.expr produced by hlfir.as_expr from a variable (e.g. an
-    //     allocatable function result used in a structure constructor) keeps
-    //     the bounds in the wrapped variable's descriptor;
-    //   - any other hlfir.expr (elemental, array constructor, ...) has no
-    //     underlying descriptor, so its lower bounds are 1.
+    // The re-allocated LHS takes the RHS lower bounds, which lowering has
+    // already put in the RHS descriptor. Read them from the entity that owns
+    // that descriptor: a variable RHS itself, or the variable wrapped by
+    // hlfir.as_expr. Other hlfir.exprs have no descriptor, so lower bound is 1.
     mlir::Value lboundSource;
     if (!mlir::isa<hlfir::ExprType>(rhs.getType()))
       lboundSource = assign.getRhs();
diff --git a/flang/test/HLFIR/separate-allocatable-assign.fir b/flang/test/HLFIR/separate-allocatable-assign.fir
index fb8ae9512ac63..53ae8594a1d98 100644
--- a/flang/test/HLFIR/separate-allocatable-assign.fir
+++ b/flang/test/HLFIR/separate-allocatable-assign.fir
@@ -184,8 +184,8 @@ func.func @test_lower_bounds(%arg0: !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32
 // hlfir.as_expr) must be preserved during reallocation.  This is the
 // structure-constructor case `t = x(f())` where `f` is an allocatable function
 // result with a non-default lower bound: the allocatable component must take
-// the source's bounds (F2018 7.5.10).  The pass must read the lower bound from
-// the wrapped variable's descriptor instead of assuming 1.
+// the source's bounds.  The pass must read the lower bound from the wrapped
+// variable's descriptor instead of assuming 1.
 func.func @test_expr_lower_bounds(%arg0: !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>, %arg1: !fir.ref<!fir.array<3xi32>>) {
   %c10 = arith.constant 10 : index
   %c3 = arith.constant 3 : index



More information about the flang-commits mailing list