[MLIR] Add StoreLikeInterface to decouple activity analysis from stores (#2978)

* [MLIR] Add ActiveStoreOpInterface to decouple activity analysis from stores

Activity analysis hard-codes which ops are stores (dyn_cast<LLVM::StoreOp> /
memref::StoreOp) when reasoning about stored-value / pointer activity. That
prevents out-of-tree dialects from participating and duplicates logic per
dialect.

Add enzyme::ActiveStoreOpInterface exposing (getStoredValue, getStoredPointer),
attach it to memref.store and llvm.store, and use it at the potential-active-
store site that already handled both dialects -- now a single dialect-agnostic
branch. Any dialect (e.g. an out-of-tree fir.store / hlfir.assign) can opt in by
attaching the interface.

Behavior-preserving: llvm.store/memref.store return the same value/pointer
through the interface. The MLIR test suite is unchanged (the two
ReverseMode failures are pre-existing on this LLVM build, independent of this
change). The remaining LLVM-specific store sites (which walk llvm.alloca /
llvm.load origins) are intentionally left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [MLIR] Rename ActiveStoreOpInterface to StoreLikeInterface

Address review: rename the interface and clarify getStoredPointer's
description to note it returns the base pointer before any in-op offsets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [MLIR] Finish dialect-agnostic activity analysis for stores/mutable refs

Extends the StoreLikeInterface generalization to the remaining store sites
in ActivityAnalysis and gates the pointer-like type check on the type
interface, so out-of-tree dialects (e.g. FIR/HLFIR) participate fully in
activity analysis without hard-coded dyn_casts. These are the
Fortran-independent pieces of EnzymeAD/Enzyme#2969.

- isConstantValue: besides LLVM ptr / memref, treat any type whose
  AutoDiffTypeInterface reports isMutable() (e.g. !fir.ref) as a reference
  that carries active memory. Behavior-preserving in-tree: among core
  dialects only memref and LLVM pointer report isMutable(), and both are
  already matched by the existing isa<> check.
- isOperationInactiveFromOrigin: dialect-agnostic store branch mirroring
  the LLVM::StoreOp case (inactive iff stored value or pointer is constant).
- isValueActivelyStoredOrReturned: dialect-agnostic store branch mirroring
  the LLVM::StoreOp case.

Deliberately excludes the Fortran-coupled / behavior-changing parts of
#2969 (FIR/HLFIR models, flang plugin, the CoreDialects registration TU
split, and dropping tensor/linalg from the pass dependent dialects).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Cleanup PR a bit

* apply clang-format

* Update enzyme/Enzyme/MLIR/Interfaces/AutoDiffOpInterface.td

Co-authored-by: Paul Berg <naydex.mc+github@gmail.com>

* [MLIR] Attach StoreLikeInterface to affine.store

Lets activity analysis reason about affine.store's stored-value/pointer
activity generically, the same way memref.store and llvm.store now do,
instead of requiring a hard-coded dyn_cast. getStoredPointer returns the
base memref (the affine map's indices are applied within the op).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [MLIR] Add forward-mode conditional-store tests for memref/llvm

The affine.store `@if_then` test in ForwardMode/affine.mlir covers a memref
that is unconditionally initialized with a constant and then conditionally
overwritten with an active value: forward mode must zero-initialize the shadow
before the conditional store, otherwise the not-taken path reads uninitialized
shadow memory and returns a garbage tangent instead of 0.

No equivalent coverage existed for memref.store or llvm.store. Add memref_if.mlir
and llvm_if.mlir as the direct analogues. Both are marked XFAIL: forward-mode
differentiation of memref.store / llvm.store does not currently emit the shadow
zero-initialization, so the tests document the known bug and will XPASS once it
is fixed.

Verified with a local enzymemlir-opt build: FileCheck fails on exactly the
zero-init CHECK-DAG line and matches everywhere else.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix constant handling

* clang format

* [MLIR] Zero shadow memory for inactive stores in forward mode

Restore the original store-inactivity condition in activity analysis: a
store is inactive if *either* the stored value or the pointer is inactive,
matching LLVM Enzyme's isInstructionInactiveFromOrigin.

That condition alone made the shadow store disappear, because
MGradientUtils::visitChild skips any constant operation in forward mode, so
memoryIdentityForwardHandler never ran and never emitted the null value it
already knows how to produce for a constant operand. LLVM Enzyme has no such
gate: its forward-mode visitCommonStore returns early only when the pointer
is constant. Mirror that by continuing to visit store-like ops whose pointer
is active.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [MLIR] Address review: generalize the forward-mode skip condition

Replace the store-specific storesThroughActivePointer check with the general
condition wsmoses suggested: only skip a constant operation in forward mode if
it is pure, or if all of its operands are constant. A side-effecting op with an
active operand may still need to touch shadow memory, which now also covers
llvm.memcpy / llvm.memset rather than only StoreLikeInterface ops. This also
makes the stale /*iface.hasNoEffect()*/ note redundant.

Retain the "if we are being stored into, not storing this value" comment in
isValueActivelyStoredOrReturned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Paul Berg <naydex.mc+github@gmail.com>
diff --git a/enzyme/Enzyme/MLIR/Analysis/ActivityAnalysis.cpp b/enzyme/Enzyme/MLIR/Analysis/ActivityAnalysis.cpp
index fdbbeeb..ff97b21 100644
--- a/enzyme/Enzyme/MLIR/Analysis/ActivityAnalysis.cpp
+++ b/enzyme/Enzyme/MLIR/Analysis/ActivityAnalysis.cpp
@@ -1814,9 +1814,10 @@
     containsPointer = false;
   // if (!TR.intType(1, Val, /*errIfNotFound*/ false).isPossiblePointer())
 
-  // TODO: this should be an MLIR type interface connected to type analysis.
-  if (!isa<LLVM::LLVMPointerType, MemRefType>(Val.getType()))
+  auto typeIface = dyn_cast<AutoDiffTypeInterface>(Val.getType());
+  if (!typeIface || !typeIface.isMutable()) {
     containsPointer = false;
+  }
 
   if (containsPointer && !isValuePotentiallyUsedAsPointer(Val)) {
     containsPointer = false;
@@ -2454,21 +2455,11 @@
         if (EnzymePrintActivity)
           llvm::errs() << "potential active store: " << *op << " Val=" << Val
                        << "\n";
-        if (auto SI = dyn_cast<LLVM::StoreOp>(op)) {
-          bool cop = !Hypothesis->isConstantValue(TR, SI.getValue());
+        if (auto SI = dyn_cast<enzyme::StoreLikeInterface>(op)) {
+          bool cop = !Hypothesis->isConstantValue(TR, SI.getStoredValue());
           if (EnzymePrintActivity)
             llvm::errs() << " -- store potential activity: " << (int)cop
-                         << " - " << *SI << " of "
-                         << " Val=" << Val << "\n";
-          potentialStore = true;
-          if (cop)
-            potentiallyActiveStore = true;
-        } else if (auto SI = dyn_cast<memref::StoreOp>(op)) {
-          // FIXME: this is a copy-pasta form above to work with MLIR memrefs.
-          bool cop = !Hypothesis->isConstantValue(TR, SI.getValueToStore());
-          if (EnzymePrintActivity)
-            llvm::errs() << " -- store potential activity: " << (int)cop
-                         << " - " << *SI << " of "
+                         << " - " << *op << " of "
                          << " Val=" << Val << "\n";
           potentialStore = true;
           if (cop)
@@ -2790,17 +2781,19 @@
   if (EnzymePrintActivity)
     llvm::errs() << " < UPSEARCH" << (int)directions << ">" << *op << "\n";
 
-  if (auto store = dyn_cast<LLVM::StoreOp>(op)) {
-    if (isConstantValue(TR, store.getValue()) ||
-        isConstantValue(TR, store.getAddr())) {
+  // if either src or dst is inactive, there cannot be a transfer of active
+  // values and thus the store is inactive
+  if (auto store = dyn_cast<enzyme::StoreLikeInterface>(op)) {
+    if (isConstantValue(TR, store.getStoredValue()) ||
+        isConstantValue(TR, store.getStoredPointer())) {
       if (EnzymePrintActivity)
         llvm::errs() << " constant instruction as store operand is inactive"
                      << *op << "\n";
       return true;
     }
     if (inactArg) {
-      inactArg->insert(store.getValue());
-      inactArg->insert(store.getAddr());
+      inactArg->insert(store.getStoredValue());
+      inactArg->insert(store.getStoredPointer());
     }
     return false;
   }
@@ -3734,13 +3727,13 @@
       }
     }
 
-    if (auto SI = dyn_cast<LLVM::StoreOp>(a)) {
+    if (auto SI = dyn_cast<enzyme::StoreLikeInterface>(a)) {
       // If we are being stored into, not storing this value
       // this case can be skipped
-      if (SI.getValue() != val) {
+      if (SI.getStoredValue() != val) {
         if (!ignoreStoresInto) {
-          // Storing into active value, return true
-          if (!isConstantValue(TR, SI.getValue())) {
+          // Active value stored into `val` (the pointer): `val` is active.
+          if (!isConstantValue(TR, SI.getStoredValue())) {
             StoredOrReturnedCache[key] = true;
             if (EnzymePrintActivity)
               llvm::errs() << " </ASOR" << (int)directions
@@ -3752,8 +3745,8 @@
         }
         continue;
       } else {
-        // Storing into active memory, return true
-        if (!isConstantValue(TR, SI.getAddr())) {
+        // `val` is stored into active memory: active.
+        if (!isConstantValue(TR, SI.getStoredPointer())) {
           StoredOrReturnedCache[key] = true;
           if (EnzymePrintActivity)
             llvm::errs() << " </ASOR" << (int)directions
diff --git a/enzyme/Enzyme/MLIR/Implementations/AffineAutoDiffOpInterfaceImpl.cpp b/enzyme/Enzyme/MLIR/Implementations/AffineAutoDiffOpInterfaceImpl.cpp
index da8b531..dbf2185 100644
--- a/enzyme/Enzyme/MLIR/Implementations/AffineAutoDiffOpInterfaceImpl.cpp
+++ b/enzyme/Enzyme/MLIR/Implementations/AffineAutoDiffOpInterfaceImpl.cpp
@@ -590,6 +590,20 @@
   }
 };
 
+// Lets activity analysis treat affine.store generically via StoreLikeInterface.
+// getStoredPointer returns the base memref; the affine map indices apply within
+// the op.
+struct AffineStoreLike
+    : public StoreLikeInterface::ExternalModel<AffineStoreLike,
+                                               affine::AffineStoreOp> {
+  Value getStoredValue(Operation *op) const {
+    return cast<affine::AffineStoreOp>(op).getValueToStore();
+  }
+  Value getStoredPointer(Operation *op) const {
+    return cast<affine::AffineStoreOp>(op).getMemRef();
+  }
+};
+
 struct AffineStoreOpInterfaceReverse
     : public ReverseAutoDiffOpInterface::ExternalModel<
           AffineStoreOpInterfaceReverse, affine::AffineStoreOp> {
@@ -844,6 +858,7 @@
     registerInterfaces(context);
     affine::AffineLoadOp::attachInterface<AffineLoadOpInterfaceReverse>(
         *context);
+    affine::AffineStoreOp::attachInterface<AffineStoreLike>(*context);
     affine::AffineStoreOp::attachInterface<AffineStoreOpInterfaceReverse>(
         *context);
     affine::AffineForOp::attachInterface<AffineForOpInterfaceReverse>(*context);
diff --git a/enzyme/Enzyme/MLIR/Implementations/CoreDialectsAutoDiffRegistration.cpp b/enzyme/Enzyme/MLIR/Implementations/CoreDialectsAutoDiffRegistration.cpp
index e4b8f37..0613250 100644
--- a/enzyme/Enzyme/MLIR/Implementations/CoreDialectsAutoDiffRegistration.cpp
+++ b/enzyme/Enzyme/MLIR/Implementations/CoreDialectsAutoDiffRegistration.cpp
@@ -6,16 +6,11 @@
 //
 //===----------------------------------------------------------------------===//
 //
-// The aggregate registration entry point that attaches Enzyme's autodiff
-// external models for every core upstream dialect. It is deliberately isolated
-// in its own translation unit: because it references every per-dialect
-// registration (Linalg and NVVM included), linking it forces those models --
-// and their dialect symbols -- into the consumer. Tools that want them all
-// (enzymemlir-opt, fir-enzyme-opt, ...) call this; a consumer that only needs a
-// subset (the lean `flang -fc1` plugin, which must not pull in Linalg symbols
-// flang does not export) instead links the individual
-// register*DialectAutoDiffInterface functions it wants and never references
-// this TU, so the Linalg/NVVM models are not linked at all.
+// Aggregate entry point registering autodiff external models for every core
+// upstream dialect. Isolated in its own TU so that linking it pulls in all the
+// per-dialect models (Linalg, NVVM, ...); consumers needing only a subset (e.g.
+// the flang -fc1 plugin, which must not pull in Linalg) link the individual
+// register*DialectAutoDiffInterface functions instead and skip this TU.
 //
 //===----------------------------------------------------------------------===//
 
diff --git a/enzyme/Enzyme/MLIR/Implementations/LLVMAutoDiffOpInterfaceImpl.cpp b/enzyme/Enzyme/MLIR/Implementations/LLVMAutoDiffOpInterfaceImpl.cpp
index 3b45d63..d76469c 100644
--- a/enzyme/Enzyme/MLIR/Implementations/LLVMAutoDiffOpInterfaceImpl.cpp
+++ b/enzyme/Enzyme/MLIR/Implementations/LLVMAutoDiffOpInterfaceImpl.cpp
@@ -27,6 +27,17 @@
 namespace {
 #include "Implementations/LLVMDerivatives.inc"
 
+// Lets activity analysis treat llvm.store generically via StoreLikeInterface.
+struct LLVMStoreLike
+    : public StoreLikeInterface::ExternalModel<LLVMStoreLike, LLVM::StoreOp> {
+  Value getStoredValue(Operation *op) const {
+    return cast<LLVM::StoreOp>(op).getValue();
+  }
+  Value getStoredPointer(Operation *op) const {
+    return cast<LLVM::StoreOp>(op).getAddr();
+  }
+};
+
 struct InlineAsmActivityInterface
     : public ActivityOpInterface::ExternalModel<InlineAsmActivityInterface,
                                                 LLVM::InlineAsmOp> {
@@ -564,6 +575,7 @@
     LLVM::LLVMArrayType::attachInterface<ArrayTypeInterface>(*context);
 
     LLVM::SelectOp::attachInterface<SelectActivityInterface>(*context);
+    LLVM::StoreOp::attachInterface<LLVMStoreLike>(*context);
     LLVM::LoadOp::attachInterface<LoadOpInterfaceReverse>(*context);
     LLVM::StoreOp::attachInterface<StoreOpInterfaceReverse>(*context);
     LLVM::GEPOp::attachInterface<GEPOpInterfaceReverse>(*context);
diff --git a/enzyme/Enzyme/MLIR/Implementations/MemRefAutoDiffOpInterfaceImpl.cpp b/enzyme/Enzyme/MLIR/Implementations/MemRefAutoDiffOpInterfaceImpl.cpp
index ffbf395..2427bf9 100644
--- a/enzyme/Enzyme/MLIR/Implementations/MemRefAutoDiffOpInterfaceImpl.cpp
+++ b/enzyme/Enzyme/MLIR/Implementations/MemRefAutoDiffOpInterfaceImpl.cpp
@@ -27,6 +27,18 @@
 namespace {
 #include "Implementations/MemRefDerivatives.inc"
 
+// Lets activity analysis treat memref.store generically via StoreLikeInterface.
+struct MemRefStoreLike
+    : public StoreLikeInterface::ExternalModel<MemRefStoreLike,
+                                               memref::StoreOp> {
+  Value getStoredValue(Operation *op) const {
+    return cast<memref::StoreOp>(op).getValueToStore();
+  }
+  Value getStoredPointer(Operation *op) const {
+    return cast<memref::StoreOp>(op).getMemRef();
+  }
+};
+
 struct LoadOpInterfaceReverse
     : public ReverseAutoDiffOpInterface::ExternalModel<LoadOpInterfaceReverse,
                                                        memref::LoadOp> {
@@ -312,6 +324,7 @@
     MemRefType::attachInterface<MemRefAutoDiffTypeInterface>(*context);
     MemRefType::attachInterface<MemRefClonableTypeInterface>(*context);
 
+    memref::StoreOp::attachInterface<MemRefStoreLike>(*context);
     memref::LoadOp::attachInterface<LoadOpInterfaceReverse>(*context);
     memref::StoreOp::attachInterface<StoreOpInterfaceReverse>(*context);
     memref::SubViewOp::attachInterface<SubViewOpInterfaceReverse>(*context);
diff --git a/enzyme/Enzyme/MLIR/Interfaces/AutoDiffOpInterface.td b/enzyme/Enzyme/MLIR/Interfaces/AutoDiffOpInterface.td
index 81c76f6..f3df11c 100644
--- a/enzyme/Enzyme/MLIR/Interfaces/AutoDiffOpInterface.td
+++ b/enzyme/Enzyme/MLIR/Interfaces/AutoDiffOpInterface.td
@@ -126,10 +126,32 @@
   ];
 }
 
+def StoreLikeInterface : OpInterface<"StoreLikeInterface"> {
+  let description = [{
+    A store-like operation that writes one operand (the value) through another
+    (the pointer/reference). Lets activity analysis reason about stored-value and
+    pointer activity without hard-coding specific dialects (memref.store,
+    LLVM.store, ...), which keeps the analysis dialect-agnostic and lets
+    out-of-tree dialects participate by attaching this interface.
+  }];
+  let cppNamespace = "::mlir::enzyme";
+
+  let methods = [InterfaceMethod<
+                     /*desc=*/"Returns the value written by this store.",
+                     /*retTy=*/"::mlir::Value",
+                     /*methodName=*/"getStoredValue">,
+                 InterfaceMethod<
+                     /*desc=*/"Returns the base pointer/reference this store "
+                              "writes through (before any potential offsets "
+                              "within the op are applied).",
+                     /*retTy=*/"::mlir::Value",
+                     /*methodName=*/"getStoredPointer">];
+}
+
 def ActivityOpInterface
     : OpInterface<"ActivityOpInterface"> {
   let cppNamespace = "::mlir::enzyme";
-  
+
   let methods = [
     InterfaceMethod<
     /*desc=*/[{
diff --git a/enzyme/Enzyme/MLIR/Interfaces/GradientUtils.cpp b/enzyme/Enzyme/MLIR/Interfaces/GradientUtils.cpp
index 9c98b68..a7a1088 100644
--- a/enzyme/Enzyme/MLIR/Interfaces/GradientUtils.cpp
+++ b/enzyme/Enzyme/MLIR/Interfaces/GradientUtils.cpp
@@ -317,10 +317,18 @@
 
 LogicalResult MGradientUtils::visitChild(Operation *op) {
   if (mode == DerivativeMode::ForwardMode) {
+    // An op with side effects may still need to touch shadow memory even when
+    // it is constant: a store of an inactive value into active memory has to
+    // zero the shadow, or a later load reads a stale tangent. Only skip it if
+    // it is pure, or if every operand is constant and there is no shadow to
+    // write through.
     if ((op->getBlock()->getTerminator() != op) &&
         llvm::all_of(op->getResults(),
                      [this](Value v) { return isConstantValue(v); }) &&
-        /*iface.hasNoEffect()*/ activityAnalyzer->isConstantOperation(TR, op)) {
+        (isPure(op) ||
+         llvm::all_of(op->getOperands(),
+                      [this](Value v) { return isConstantValue(v); })) &&
+        activityAnalyzer->isConstantOperation(TR, op)) {
       return success();
     }
     // }
diff --git a/enzyme/test/MLIR/ForwardMode/llvm_if.mlir b/enzyme/test/MLIR/ForwardMode/llvm_if.mlir
new file mode 100644
index 0000000..b869d02
--- /dev/null
+++ b/enzyme/test/MLIR/ForwardMode/llvm_if.mlir
@@ -0,0 +1,51 @@
+// RUN: %eopt --enzyme %s | FileCheck %s
+
+// An llvm.alloca that is unconditionally initialized with a constant and then
+// conditionally overwritten with an active value. In forward mode the shadow
+// allocation must be zero-initialized before the conditional store, otherwise
+// the load on the not-taken path reads uninitialized shadow memory and the
+// returned tangent is garbage instead of 0.
+//
+// This is the llvm.store analogue of the affine.store `@if_then` test.
+
+module {
+  func.func @if_then(%x : f64, %c : i1) -> f64 {
+    %c1_i64 = arith.constant 1 : i64
+    %c2 = arith.constant 2.000000e+00 : f64
+    %mem = llvm.alloca %c1_i64 x f64 : (i64) -> !llvm.ptr
+    llvm.store %c2, %mem : f64, !llvm.ptr
+    scf.if %c {
+      %mul = arith.mulf %x, %x : f64
+      llvm.store %mul, %mem : f64, !llvm.ptr
+    }
+    %r = llvm.load %mem : !llvm.ptr -> f64
+    %res = arith.mulf %c2, %r : f64
+    return %res : f64
+  }
+  func.func @dif_then(%x : f64, %dx : f64, %c : i1) -> f64 {
+    %r = enzyme.fwddiff @if_then(%x, %dx, %c) { activity=[#enzyme<activity enzyme_dup>, #enzyme<activity enzyme_const>], ret_activity=[#enzyme<activity enzyme_dupnoneed>] } : (f64, f64, i1) -> (f64)
+    return %r : f64
+  }
+}
+
+// CHECK: @fwddiffeif_then
+// CHECK: (%[[arg0:.+]]: f64, %[[arg1:.+]]: f64, %[[arg2:.+]]: i1) -> f64 {
+// CHECK-DAG: %[[c1:.+]] = arith.constant 1 : i64
+// CHECK-DAG: %[[cst2:.+]] = arith.constant 2.000000e+00 : f64
+// CHECK: %[[alloc:.+]] = llvm.alloca %[[c1]] x f64 : (i64) -> !llvm.ptr
+// CHECK: %[[alloc_0:.+]] = llvm.alloca %[[c1]] x f64 : (i64) -> !llvm.ptr
+// CHECK-DAG: %[[cst0:.+]] = arith.constant 0.000000e+00 : f64
+// CHECK: llvm.store %[[cst0]], %[[alloc]] : f64, !llvm.ptr
+// CHECK: llvm.store %[[cst2]], %[[alloc_0]] : f64, !llvm.ptr
+// CHECK: scf.if %[[arg2]] {
+// CHECK:   %[[v4:.+]] = arith.mulf %[[arg1]], %[[arg0]] : f64
+// CHECK:   %[[v5:.+]] = arith.mulf %[[arg1]], %[[arg0]] : f64
+// CHECK:   %[[v6:.+]] = arith.addf %[[v4]], %[[v5]] : f64
+// CHECK:   %[[v7:.+]] = arith.mulf %[[arg0]], %[[arg0]] : f64
+// CHECK:   llvm.store %[[v6]], %[[alloc]] : f64, !llvm.ptr
+// CHECK:   llvm.store %[[v7]], %[[alloc_0]] : f64, !llvm.ptr
+// CHECK: }
+// CHECK: %[[v0:.+]] = llvm.load %[[alloc]] : !llvm.ptr -> f64
+// CHECK: %[[v1:.+]] = llvm.load %[[alloc_0]] : !llvm.ptr -> f64
+// CHECK: %[[v2:.+]] = arith.mulf %[[v0]], %[[cst2]] : f64
+// CHECK: return %[[v2]] : f64
diff --git a/enzyme/test/MLIR/ForwardMode/memref_if.mlir b/enzyme/test/MLIR/ForwardMode/memref_if.mlir
new file mode 100644
index 0000000..ceff151
--- /dev/null
+++ b/enzyme/test/MLIR/ForwardMode/memref_if.mlir
@@ -0,0 +1,51 @@
+// RUN: %eopt --enzyme %s | FileCheck %s
+
+// A memref that is unconditionally initialized with a constant and then
+// conditionally overwritten with an active value. In forward mode the shadow
+// memref must be zero-initialized before the conditional store, otherwise the
+// load on the not-taken path reads uninitialized shadow memory and the returned
+// tangent is garbage instead of 0.
+//
+// This is the memref analogue of the affine.store `@if_then` test.
+
+module {
+  func.func @if_then(%x : f64, %c : i1) -> f64 {
+    %c0 = arith.constant 0 : index
+    %c2 = arith.constant 2.000000e+00 : f64
+    %mem = memref.alloc() : memref<1xf64>
+    memref.store %c2, %mem[%c0] : memref<1xf64>
+    scf.if %c {
+      %mul = arith.mulf %x, %x : f64
+      memref.store %mul, %mem[%c0] : memref<1xf64>
+    }
+    %r = memref.load %mem[%c0] : memref<1xf64>
+    %res = arith.mulf %c2, %r : f64
+    return %res : f64
+  }
+  func.func @dif_then(%x : f64, %dx : f64, %c : i1) -> f64 {
+    %r = enzyme.fwddiff @if_then(%x, %dx, %c) { activity=[#enzyme<activity enzyme_dup>, #enzyme<activity enzyme_const>], ret_activity=[#enzyme<activity enzyme_dupnoneed>] } : (f64, f64, i1) -> (f64)
+    return %r : f64
+  }
+}
+
+// CHECK: @fwddiffeif_then
+// CHECK: (%[[arg0:.+]]: f64, %[[arg1:.+]]: f64, %[[arg2:.+]]: i1) -> f64 {
+// CHECK-DAG: %[[c0:.+]] = arith.constant 0 : index
+// CHECK-DAG: %[[cst2:.+]] = arith.constant 2.000000e+00 : f64
+// CHECK: %[[alloc:.+]] = memref.alloc() : memref<1xf64>
+// CHECK: %[[alloc_0:.+]] = memref.alloc() : memref<1xf64>
+// CHECK-DAG: %[[cst0:.+]] = arith.constant 0.000000e+00 : f64
+// CHECK: memref.store %[[cst0]], %[[alloc]][%[[c0]]] : memref<1xf64>
+// CHECK: memref.store %[[cst2]], %[[alloc_0]][%[[c0]]] : memref<1xf64>
+// CHECK: scf.if %[[arg2]] {
+// CHECK:   %[[v4:.+]] = arith.mulf %[[arg1]], %[[arg0]] : f64
+// CHECK:   %[[v5:.+]] = arith.mulf %[[arg1]], %[[arg0]] : f64
+// CHECK:   %[[v6:.+]] = arith.addf %[[v4]], %[[v5]] : f64
+// CHECK:   %[[v7:.+]] = arith.mulf %[[arg0]], %[[arg0]] : f64
+// CHECK:   memref.store %[[v6]], %[[alloc]][%[[c0]]] : memref<1xf64>
+// CHECK:   memref.store %[[v7]], %[[alloc_0]][%[[c0]]] : memref<1xf64>
+// CHECK: }
+// CHECK: %[[v0:.+]] = memref.load %[[alloc]][%[[c0]]] : memref<1xf64>
+// CHECK: %[[v1:.+]] = memref.load %[[alloc_0]][%[[c0]]] : memref<1xf64>
+// CHECK: %[[v2:.+]] = arith.mulf %[[v0]], %[[cst2]] : f64
+// CHECK: return %[[v2]] : f64