Attach indirect call effects directly to expression
diff --git a/src/ir/effects.h b/src/ir/effects.h
index 56cea0a..20ef0a3 100644
--- a/src/ir/effects.h
+++ b/src/ir/effects.h
@@ -758,7 +758,7 @@
       parent.implicitTrap = true;
 
       const EffectAnalyzer* callTargetEffects = nullptr;
-      if (auto it = parent.module.indirectCallEffects.find(curr->heapType);
+      if (auto it = parent.module.indirectCallEffects.find(curr);
           it != parent.module.indirectCallEffects.end()) {
         callTargetEffects = it->second.get();
       }
@@ -770,8 +770,7 @@
       }
 
       const EffectAnalyzer* callTargetEffects = nullptr;
-      if (auto it = parent.module.indirectCallEffects.find(
-            curr->target->type.getHeapType());
+      if (auto it = parent.module.indirectCallEffects.find(curr);
           it != parent.module.indirectCallEffects.end()) {
         callTargetEffects = it->second.get();
       }
diff --git a/src/ir/linear-execution.h b/src/ir/linear-execution.h
index 9e69405..ea5bbd8 100644
--- a/src/ir/linear-execution.h
+++ b/src/ir/linear-execution.h
@@ -185,8 +185,8 @@
             return true;
           }
 
-          auto* effects = find_or_null(self->getModule()->indirectCallEffects,
-                                       callRef->target->type.getHeapType());
+          auto* effects =
+            find_or_null(self->getModule()->indirectCallEffects, callRef);
           if (!effects) {
             return false;
           }
@@ -202,7 +202,7 @@
         bool refutesThrowEffect = false;
         if (self->getModule()) {
           if (auto* effects = find_or_null(
-                self->getModule()->indirectCallEffects, callIndirect->heapType);
+                self->getModule()->indirectCallEffects, callIndirect);
               effects) {
             refutesThrowEffect = !(*effects)->throws_;
           }
diff --git a/src/ir/type-updating.cpp b/src/ir/type-updating.cpp
index ee7cf7d..0ca698a 100644
--- a/src/ir/type-updating.cpp
+++ b/src/ir/type-updating.cpp
@@ -325,35 +325,6 @@
   for (auto& tag : wasm.tags) {
     tag->type = updater.getNew(tag->type);
   }
-
-  // Update indirect call effects per type.
-  // When A is rewritten to B, B inherits the effects of A and A loses its
-  // effects.
-  std::unordered_map<HeapType, std::shared_ptr<const EffectAnalyzer>>
-    newTypeEffects;
-
-  for (const auto& [oldType, newType] : oldToNewTypes) {
-    std::shared_ptr<const EffectAnalyzer>* oldEffects =
-      find_or_null(wasm.indirectCallEffects, oldType);
-    std::shared_ptr<const EffectAnalyzer>* targetEffects =
-      find_or_null(wasm.indirectCallEffects, newType);
-
-    if (!targetEffects) {
-      // Nothing to update, we already know nothing and assume all effects.
-      continue;
-    }
-
-    if (!oldEffects) {
-      targetEffects->reset();
-      continue;
-    }
-
-    auto merged = std::make_shared<EffectAnalyzer>(**targetEffects);
-    merged->mergeIn(**oldEffects);
-    *targetEffects = std::move(merged);
-  }
-
-  wasm.indirectCallEffects = std::move(newTypeEffects);
 }
 
 void GlobalTypeRewriter::mapTypeNamesAndIndices(const TypeMap& oldToNewTypes) {
diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp
index efcc45e..798a381 100644
--- a/src/passes/GlobalEffects.cpp
+++ b/src/passes/GlobalEffects.cpp
@@ -41,8 +41,8 @@
   // Directly-called functions from this function.
   std::unordered_set<Name> calledFunctions;
 
-  // Types that are targets of indirect calls.
-  std::unordered_set<HeapType> indirectCalledTypes;
+  // Expressions that are indirect calls.
+  std::vector<const Expression*> indirectCalls;
 };
 
 // Only funcs that are referenced may be the target of an indirect call. A
@@ -162,19 +162,12 @@
               funcInfo.calledFunctions.insert(call->target);
             } else if (effects.calls &&
                        options.worldMode == WorldMode::Closed) {
-              HeapType type;
-              if (auto* callRef = curr->dynCast<CallRef>()) {
-                // call_ref on unreachable does not have a call effect,
-                // so this must be a HeapType.
-                type = callRef->target->type.getHeapType();
-              } else if (auto* callIndirect = curr->dynCast<CallIndirect>()) {
-                type = callIndirect->heapType;
+              if (curr->is<CallRef>() || curr->is<CallIndirect>()) {
+                funcInfo.indirectCalls.push_back(curr);
               } else {
                 funcInfo.effects = std::nullopt;
                 return;
               }
-
-              funcInfo.indirectCalledTypes.insert(type);
             } else if (effects.calls) {
               assert(options.worldMode == WorldMode::Open);
               funcInfo.effects = std::nullopt;
@@ -245,7 +238,13 @@
 
     // Function -> Type
     allFunctionTypes.insert(caller->type.getHeapType());
-    for (HeapType calleeType : callerInfo.indirectCalledTypes) {
+    for (const Expression* expr : callerInfo.indirectCalls) {
+      HeapType calleeType;
+      if (auto* callRef = expr->dynCast<CallRef>()) {
+        calleeType = callRef->target->type.getHeapType();
+      } else {
+        calleeType = expr->cast<CallIndirect>()->heapType;
+      }
       callees.insert(calleeType);
 
       // Add the key to ensure the lookup doesn't fail for indirect calls to
@@ -447,6 +446,8 @@
 
 struct GenerateGlobalEffects : public Pass {
   void run(Module* module) override {
+    module->indirectCallEffects.clear();
+
     std::map<Function*, FuncInfo> funcInfos =
       analyzeFuncs(*module, getPassOptions());
 
@@ -455,11 +456,25 @@
     auto callGraph = buildCallGraph(
       *module, funcInfos, referencedFuncs, getPassOptions().worldMode);
 
-    propagateEffects(*module,
-                     getPassOptions(),
-                     funcInfos,
-                     module->indirectCallEffects,
-                     callGraph);
+    std::unordered_map<HeapType, std::shared_ptr<const EffectAnalyzer>>
+      typeEffects;
+
+    propagateEffects(
+      *module, getPassOptions(), funcInfos, typeEffects, callGraph);
+
+    for (const auto& [func, info] : funcInfos) {
+      for (const Expression* expr : info.indirectCalls) {
+        HeapType type;
+        if (auto* callRef = expr->dynCast<CallRef>()) {
+          type = callRef->target->type.getHeapType();
+        } else {
+          type = expr->cast<CallIndirect>()->heapType;
+        }
+        if (auto it = typeEffects.find(type); it != typeEffects.end()) {
+          module->indirectCallEffects[expr] = it->second;
+        }
+      }
+    }
   }
 };
 
diff --git a/src/wasm.h b/src/wasm.h
index f5cade5..b3bceb3 100644
--- a/src/wasm.h
+++ b/src/wasm.h
@@ -2744,7 +2744,7 @@
   // exists to a function, the data can be out of date (no effort is made to
   // clean up the data if e.g. all indirect calls to a function are removed).
   // TODO: Account for exactness here.
-  std::unordered_map<HeapType, std::shared_ptr<const EffectAnalyzer>>
+  std::unordered_map<const Expression*, std::shared_ptr<const EffectAnalyzer>>
     indirectCallEffects;
 
   MixedArena allocator;
diff --git a/test/lit/passes/global-effects-indirect-merge.wast b/test/lit/passes/global-effects-indirect-merge.wast
index b4823e8..f8af349 100644
--- a/test/lit/passes/global-effects-indirect-merge.wast
+++ b/test/lit/passes/global-effects-indirect-merge.wast
@@ -54,11 +54,6 @@
   ;; MERGE_FIRST:      (func $test (type $0) (param $effectful-ref (ref $effectful)) (param $not-effectful-ref (ref $effectful))
   ;; MERGE_FIRST-NEXT:  (drop
   ;; MERGE_FIRST-NEXT:   (call_ref $effectful
-  ;; MERGE_FIRST-NEXT:    (local.get $not-effectful-ref)
-  ;; MERGE_FIRST-NEXT:   )
-  ;; MERGE_FIRST-NEXT:  )
-  ;; MERGE_FIRST-NEXT:  (drop
-  ;; MERGE_FIRST-NEXT:   (call_ref $effectful
   ;; MERGE_FIRST-NEXT:    (local.get $effectful-ref)
   ;; MERGE_FIRST-NEXT:   )
   ;; MERGE_FIRST-NEXT:  )