From bbdaa9cb7e8159dd4c8e56d73cc74c6ae8c99acb Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Wed, 5 Aug 2026 16:31:35 +0200 Subject: [PATCH] TypeAnalysis: accept `nusw` as well as `inbounds` on GEP indices visitGEPOperator only typed a GEP's indices as Integer when the GEP was `inbounds`. flang never emits `inbounds` for array element addresses -- it emits `nusw nuw`, from XArrayCoorOp lowering in flang's CodeGen.cpp -- so a Fortran array index was never typed. That is self-reinforcing: the index is not Integer because the GEP is not inbounds and the base is not yet a known Pointer, and the base never becomes a Pointer because pointer propagation requires either inbounds or indices that are already integral. Both the index chain and the array come out completely untyped. Activity analysis then cannot prove the arithmetic inactive, and integer address computations reach visitBinaryOperator's unhandled case as "cannot handle unknown binary operator". `nusw` is the premise the rule actually needs: the index is added as a signed byte offset which does not wrap, so it is an offset rather than something that might itself be a pointer. `inbounds` additionally guarantees the result stays within the object, which this rule does not rely on. The pointer propagation below is deliberately left keyed on isInBounds(); once the indices are typed Integer, its existing allIntegral path enables propagation on the next fixpoint iteration without weakening that stronger premise. Two tests: TypeAnalysis/gepnusw.ll pins the rule on hand-written IR, with an inbounds twin that must analyse identically, and Fortran/TypeAnalysis/gep_nusw.f90 shows the same shape is what flang actually produces. Both fail without the change. On a whole-program Fortran module differentiating DVODE this takes "cannot handle unknown binary operator" from 27 to 17, eliminating every failure in dvindy and dvjust; the remainder are a separate, unrelated cause. check-enzyme and check-typeanalysis show no newly failing tests. Co-Authored-By: Claude Opus 5 --- enzyme/Enzyme/TypeAnalysis/TypeAnalysis.cpp | 11 +++- enzyme/test/Fortran/CMakeLists.txt | 1 + .../test/Fortran/TypeAnalysis/CMakeLists.txt | 8 +++ enzyme/test/Fortran/TypeAnalysis/gep_nusw.f90 | 23 +++++++++ enzyme/test/TypeAnalysis/gepnusw.ll | 51 +++++++++++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 enzyme/test/Fortran/TypeAnalysis/CMakeLists.txt create mode 100644 enzyme/test/Fortran/TypeAnalysis/gep_nusw.f90 create mode 100644 enzyme/test/TypeAnalysis/gepnusw.ll diff --git a/enzyme/Enzyme/TypeAnalysis/TypeAnalysis.cpp b/enzyme/Enzyme/TypeAnalysis/TypeAnalysis.cpp index 9b8e6fce620b..92b45e16de67 100644 --- a/enzyme/Enzyme/TypeAnalysis/TypeAnalysis.cpp +++ b/enzyme/Enzyme/TypeAnalysis/TypeAnalysis.cpp @@ -2115,8 +2115,17 @@ void TypeAnalyzer::visitGEPOperator(GEPOperator &gep) { } } + // An index is an offset rather than a pointer if the GEP promises not to + // wrap. `nusw` suffices; `inbounds` also guarantees the result stays within + // the object, which this rule does not need. flang emits `nusw nuw`, never + // `inbounds`, for array element addresses. + bool indexCannotWrap = gep.isInBounds(); +#if LLVM_VERSION_MAJOR >= 19 + indexCannotWrap |= gep.hasNoUnsignedSignedWrap(); +#endif + if (has_non_const_idx && - (gep.isInBounds() || + (indexCannotWrap || (!EnzymeStrictAliasing && pointerAnalysis.Inner0() == BaseType::Pointer && getAnalysis(&gep).Inner0() == BaseType::Pointer))) { diff --git a/enzyme/test/Fortran/CMakeLists.txt b/enzyme/test/Fortran/CMakeLists.txt index 975cca7940b0..b5361514bd8c 100644 --- a/enzyme/test/Fortran/CMakeLists.txt +++ b/enzyme/test/Fortran/CMakeLists.txt @@ -2,6 +2,7 @@ message("Building Fortran tests") add_subdirectory(ForwardMode) add_subdirectory(ReverseMode) +add_subdirectory(TypeAnalysis) # Run regression and unit tests add_lit_testsuite(check-enzyme-fortran "Running enzyme fortran integration tests" diff --git a/enzyme/test/Fortran/TypeAnalysis/CMakeLists.txt b/enzyme/test/Fortran/TypeAnalysis/CMakeLists.txt new file mode 100644 index 000000000000..641bf9f77ab4 --- /dev/null +++ b/enzyme/test/Fortran/TypeAnalysis/CMakeLists.txt @@ -0,0 +1,8 @@ +# Run regression and unit tests +add_lit_testsuite(check-enzyme-fortran-typeanalysis "Running enzyme fortran type analysis tests" + ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${ENZYME_TEST_DEPS} LLVMEnzyme-${LLVM_VERSION_MAJOR} + ARGS -v +) + +set_target_properties(check-enzyme-fortran-typeanalysis PROPERTIES FOLDER "Tests") diff --git a/enzyme/test/Fortran/TypeAnalysis/gep_nusw.f90 b/enzyme/test/Fortran/TypeAnalysis/gep_nusw.f90 new file mode 100644 index 000000000000..97459fd1f8f0 --- /dev/null +++ b/enzyme/test/Fortran/TypeAnalysis/gep_nusw.f90 @@ -0,0 +1,23 @@ +! REQUIRES: flangenzyme +! RUN: %fc -S -emit-llvm -O1 %s -o %t.ll +! RUN: FileCheck %s --check-prefix=IR < %t.ll +! RUN: %opt < %t.ll %newLoadEnzyme -passes="print-type-analysis" -type-analysis-func=stride_index_ -S -o /dev/null | FileCheck %s --check-prefix=TA + +! Companion to test/TypeAnalysis/gepnusw.ll: shows that the `nusw nuw` GEP the +! rule is about is what flang actually emits for an array element address. + +subroutine stride_index(a, n, i, res) + implicit none + integer, intent(in) :: n, i + double precision, intent(in) :: a(n, *) + double precision, intent(out) :: res + res = a(i, 2) +end subroutine stride_index + +! IR: getelementptr nusw nuw + +! The leading dimension `n`, and the stride computation indexing with it: +! TA: ptr %{{[0-9]+}}: {[-1]:Pointer, [-1,0]:Integer, [-1,1]:Integer, [-1,2]:Integer, [-1,3]:Integer} +! TA: %{{[0-9]+}} = load i32, ptr %{{[0-9]+}}, align 4{{.*}}: {[-1]:Integer} +! TA-NEXT: %{{[0-9]+}} = tail call i32 @llvm.smax.i32({{.*}}): {[-1]:Integer} +! TA-NEXT: %{{[0-9]+}} = zext nneg i32 %{{[0-9]+}} to i64: {[-1]:Integer} diff --git a/enzyme/test/TypeAnalysis/gepnusw.ll b/enzyme/test/TypeAnalysis/gepnusw.ll new file mode 100644 index 000000000000..f46cc412a108 --- /dev/null +++ b/enzyme/test/TypeAnalysis/gepnusw.ll @@ -0,0 +1,51 @@ +; RUN: if [ %llvmver -ge 19 ]; then %opt < %s %newLoadEnzyme -passes="print-type-analysis" -type-analysis-func=f_nusw -S -o /dev/null | FileCheck %s --check-prefix=NUSW; fi +; RUN: if [ %llvmver -ge 19 ]; then %opt < %s %newLoadEnzyme -passes="print-type-analysis" -type-analysis-func=f_inbounds -S -o /dev/null | FileCheck %s --check-prefix=INBOUNDS; fi + +; A `nusw` GEP must type its indices exactly as an `inbounds` one does. The two +; functions below are identical apart from the no-wrap flag. + +declare i32 @llvm.smax.i32(i32, i32) + +define double @f_nusw(ptr %yh, ptr %ldyh, i64 %i) { +entry: + %n = load i32, ptr %ldyh, align 4 + %m = call i32 @llvm.smax.i32(i32 %n, i32 0) + %z = zext i32 %m to i64 + %idx = mul i64 %i, %z + %p = getelementptr nusw nuw double, ptr %yh, i64 %idx + %v = load double, ptr %p, align 8 + ret double %v +} + +define double @f_inbounds(ptr %yh, ptr %ldyh, i64 %i) { +entry: + %n = load i32, ptr %ldyh, align 4 + %m = call i32 @llvm.smax.i32(i32 %n, i32 0) + %z = zext i32 %m to i64 + %idx = mul i64 %i, %z + %p = getelementptr inbounds double, ptr %yh, i64 %idx + %v = load double, ptr %p, align 8 + ret double %v +} + +; NUSW: ptr %yh: {[-1]:Pointer} +; NUSW-NEXT: ptr %ldyh: {[-1]:Pointer, [-1,0]:Integer, [-1,1]:Integer, [-1,2]:Integer, [-1,3]:Integer} +; NUSW-NEXT: i64 %i: {[-1]:Integer} +; NUSW-NEXT: entry +; NUSW-NEXT: %n = load i32, ptr %ldyh, align 4: {[-1]:Integer} +; NUSW-NEXT: %m = call i32 @llvm.smax.i32(i32 %n, i32 0): {[-1]:Integer} +; NUSW-NEXT: %z = zext i32 %m to i64: {[-1]:Integer} +; NUSW-NEXT: %idx = mul i64 %i, %z: {[-1]:Integer} +; NUSW-NEXT: %p = getelementptr nusw nuw double, ptr %yh, i64 %idx: {[-1]:Pointer, [-1,0]:Float@double} +; NUSW-NEXT: %v = load double, ptr %p, align 8: {[-1]:Float@double} + +; INBOUNDS: ptr %yh: {[-1]:Pointer} +; INBOUNDS-NEXT: ptr %ldyh: {[-1]:Pointer, [-1,0]:Integer, [-1,1]:Integer, [-1,2]:Integer, [-1,3]:Integer} +; INBOUNDS-NEXT: i64 %i: {[-1]:Integer} +; INBOUNDS-NEXT: entry +; INBOUNDS-NEXT: %n = load i32, ptr %ldyh, align 4: {[-1]:Integer} +; INBOUNDS-NEXT: %m = call i32 @llvm.smax.i32(i32 %n, i32 0): {[-1]:Integer} +; INBOUNDS-NEXT: %z = zext i32 %m to i64: {[-1]:Integer} +; INBOUNDS-NEXT: %idx = mul i64 %i, %z: {[-1]:Integer} +; INBOUNDS-NEXT: %p = getelementptr inbounds double, ptr %yh, i64 %idx: {[-1]:Pointer, [-1,0]:Float@double} +; INBOUNDS-NEXT: %v = load double, ptr %p, align 8: {[-1]:Float@double}