MemEffects memory_effects_impl()

in hphp/runtime/vm/jit/memory-effects.cpp [305:1848]


MemEffects memory_effects_impl(const IRInstruction& inst) {
  switch (inst.op()) {

  //////////////////////////////////////////////////////////////////////
  // Region exits

  // These exits don't leave the current php function, and could head to code
  // that could read or write anything as far as we know (including frame
  // locals).
  case ReqBindJmp:
    return ExitEffects {
      *AUnknown.exclude_vm_reg(),
      stack_below(inst.extra<ReqBindJmp>()->irSPOff)
    };
  case ReqInterpBBNoTranslate:
    return ExitEffects {
      *AUnknown.exclude_vm_reg(),
      stack_below(inst.extra<ReqInterpBBNoTranslate>()->irSPOff)
    };
  case ReqRetranslate:
    return ExitEffects {
      *AUnknown.exclude_vm_reg(),
      stack_below(inst.extra<ReqRetranslate>()->offset)
    };
  case ReqRetranslateOpt:
    return ExitEffects {
      *AUnknown.exclude_vm_reg(),
      stack_below(inst.extra<ReqRetranslateOpt>()->offset)
    };
  case JmpSwitchDest:
    return ExitEffects {
      *AUnknown.exclude_vm_reg(),
      *stack_below(inst.extra<JmpSwitchDest>()->spOffBCFromIRSP).
        precise_union(AMIStateAny)
    };
  case JmpSSwitchDest:
    return ExitEffects {
      *AUnknown.exclude_vm_reg(),
      *stack_below(inst.extra<JmpSSwitchDest>()->offset).
        precise_union(AMIStateAny)
    };

  //////////////////////////////////////////////////////////////////////
  // Unusual instructions

  /*
   * The ReturnHook sets up the ActRec so the unwinder knows everything is
   * already released (i.e. it calls ar->setLocalsDecRefd()).
   *
   * The eval stack is also dead at this point (the return value is passed to
   * ReturnHook as src(1), and the ReturnHook may not access the stack).
   */
  case ReturnHook:
    return may_load_store_kill(
      AHeapAny | AActRec {inst.src(0)}, AHeapAny,
      [&] {
        auto const u =
          AStackAny.precise_union(ALocalAny)->precise_union(AMIStateAny);
        // The return hooks trashes the frame context in debug builds
        if (debug) return *u->precise_union(AFContext { inst.src(0) });
        return *u;
      }()
    );

  // The suspend hooks can load anything (re-entering the VM), but can't write
  // to frame locals.
  case SuspendHookAwaitEF:
  case SuspendHookAwaitEG:
  case SuspendHookAwaitR:
  case SuspendHookCreateCont:
  case SuspendHookYield:
    // We rely on the may_reenter effects to add the appropriate local asets.
    return may_load_store_kill(
      AHeapAny | AActRec {inst.src(0)}, AHeapAny, AMIStateAny);

  /*
   * If we're returning from a function, it's ReturnEffects.  The RetCtrl
   * opcode also suspends resumables, which we model as having any possible
   * effects.
   */
  case RetCtrl:
    if (inst.extra<RetCtrl>()->suspendingResumed) {
      // Suspending can go anywhere, and doesn't even kill locals.
      return UnknownEffects {};
    }
    return ReturnEffects {
      AStackAny | ALocalAny | AMIStateAny | AFBasePtr
    };

  case AsyncFuncRetPrefetch:
    return IrrelevantEffects {};

  case AsyncFuncRet:
  case AsyncFuncRetSlow:
  case AsyncGenRetR:
    return ReturnEffects { AStackAny | AMIStateAny | livefp(inst.src(1)) };

  case AsyncGenYieldR:
  case AsyncSwitchFast:
    // Suspending can go anywhere, and doesn't even kill locals.
    return UnknownEffects {};

  case GenericRetDecRefs:
    /*
     * The may-store information here is ALocalAny: even though we
     * know it doesn't really "store" to the frame locals, the values
     * that used to be there are no longer available because they are
     * DecRef'd, which we are required to report as may-store
     * information to make it visible to reference count
     * optimizations.  It's conceptually the same as if it was storing
     * an Uninit over each of the locals, but the stores of uninits
     * would be dead so we're not actually doing that.
     */
    return may_load_store_kill(
      ALocalAny | AHeapAny,
      ALocalAny | AHeapAny,
      AMIStateAny
    );

  case EndCatch: {
    auto const stack_kills = stack_below(inst.extra<EndCatch>()->offset);
    return ExitEffects {
      AUnknown,
      stack_kills | AMIStateAny
    };
  }

  case EnterTCUnwind: {
    auto const stack_kills = stack_below(inst.extra<EnterTCUnwind>()->offset);
    return ExitEffects {
      AUnknown,
      stack_kills | AMIStateAny
    };
  }

  /*
   * BeginInlining must always be the first instruction in the inlined call. It
   * defines a new FP for the callee but does not perform any stores or
   * otherwise initialize the FP.
   */
  case BeginInlining: {
    /*
     * SP relative offset of the firstin the inlined call.
     */
    auto inlineStackOff =
      inst.extra<BeginInlining>()->spOffset + kNumActRecCells;
    return may_load_store_kill(
      AEmpty,
      AEmpty,
      /*
       * This prevents stack slots from the caller from being sunk into the
       * callee. Note that some of these stack slots overlap with the frame
       * locals of the callee-- those slots are inacessible in the inlined
       * call as frame and stack locations may not alias.
       */
      stack_below(inlineStackOff)
    );
  }

  case EndInlining: {
    assertx(inst.src(0)->inst()->is(BeginInlining));
    auto const fp = inst.src(0);
    auto const callee = inst.src(0)->inst()->extra<BeginInlining>()->func;
    const AliasClass ar = AActRec { inst.src(0) };
    auto const locals = [&] () -> AliasClass {
      if (!callee->numLocals()) return AEmpty;
      return ALocal {fp, AliasIdSet::IdRange(0, callee->numLocals())};
    }();

    // NB: It's okay if the AliasIdSet for locals cannot be precise. We want to
    //     kill *every* local in the frame so there's nothing else that can
    //     accidentally be included in the set.
    return may_load_store_kill(AEmpty, AEmpty, ar | locals | AMIStateAny);
  }

  case InlineCall:
    return PureInlineCall {
      AFBasePtr,
      inst.src(0),

      // Right now when we "publish" a frame by storing it in rvmfp() we
      // implicitly depend on the AFFunc and AFMeta bits being stored. In the
      // future we may want to track this explicitly.
      //
      // We also need to ensure that all of our parent frames have this stored
      // this information. To achieve this we also register a load on AFBasePtr,
      // forcing them to also be published. Notice that we don't actually
      // depend on this load to properly initialize m_sfp or rvmfp().
      AliasClass(AFFunc { inst.src(0) }) | AFMeta { inst.src(0) } | AFBasePtr
    };

  case InterpOne:
    return interp_one_effects(inst);
  case InterpOneCF: {
    auto const extra = inst.extra<InterpOneData>();
    return ExitEffects {
      *AUnknown.exclude_vm_reg(),
      stack_below(extra->spOffset) | AMIStateAny
    };
  }

  case NativeImpl:
    return UnknownEffects {};


  // These C++ helpers can invoke the user error handler and go do whatever
  // they want to non-frame locations.
  case VerifyParamCallable:
  case VerifyParamCls:
  case VerifyParamFail:
  case VerifyParamFailHard:
  case VerifyPropCls:
  case VerifyPropFail:
  case VerifyPropFailHard:
  case VerifyProp:
  case VerifyPropAll:
  case VerifyPropCoerce:
  case VerifyPropCoerceAll:
  case VerifyReifiedLocalType:
  case VerifyReifiedReturnType:
  case VerifyRetCallable:
  case VerifyRetCls:
  case VerifyRetFail:
  case VerifyRetFailHard:
    return may_load_store(AHeapAny, AHeapAny);

  case ContEnter:
    {
      auto const extra = inst.extra<ContEnter>();
      return CallEffects {
        // Kills. Everything on the stack.
        stack_below(extra->spOffset) | AMIStateAny | AVMRegAny,
        // No inputs. The value being sent is passed explicitly.
        AEmpty,
        // ActRec. It is on the heap and we already implicitly assume that
        // CallEffects can perform arbitrary heap operations.
        AEmpty,
        // No outputs.
        AEmpty,
        // Locals.
        backtrace_locals(inst)
      };
    }

  case Call:
    {
      auto const extra = inst.extra<Call>();
      return CallEffects {
        // Kills. Everything on the stack below the incoming parameters.
        stack_below(extra->spOffset) | AMIStateAny | AVMRegAny,
        // Input arguments.
        extra->numInputs() == 0 ? AEmpty : AStack::range(
          extra->spOffset,
          extra->spOffset + extra->numInputs()
        ),
        // ActRec.
        actrec(inst.src(0), extra->spOffset + extra->numInputs()),
        // Inout outputs.
        extra->numOut == 0 ? AEmpty : AStack::range(
          extra->spOffset + extra->numInputs() + kNumActRecCells,
          extra->spOffset + extra->numInputs() + kNumActRecCells +
            extra->numOut
        ),
        // Locals.
        backtrace_locals(inst)
      };
    }

  case CallBuiltin:
    {
      auto const extra = inst.extra<CallBuiltin>();
      auto const callee = extra->callee;
      auto const [inout, read]  = [&] {
        auto read = AEmpty;
        auto inout = AEmpty;
        auto const paramOff = callee->isMethod() ? 3 : 2;
        for (auto i = paramOff; i < inst.numSrcs(); ++i) {
          if (inst.src(i)->type() <= TPtr) {
            auto const cls = pointee(inst.src(i));
            if (callee->isInOut(i - paramOff)) {
              inout = inout | cls;
            } else {
              read = read | cls;
            }
          }
        }
        return std::make_pair(inout, read);
      }();
      auto const foldable = callee->isFoldable() ? AEmpty : ARdsAny;
      return GeneralEffects {
        read | AHeapAny | foldable | AVMRegAny | AVMRegState,
        AHeapAny | foldable | AVMRegAny,
        AEmpty,
        stack_below(extra->spOffset) | AMIStateAny,
        inout,
        AEmpty
      };
    }

  // Resumable suspension takes everything from the frame and moves it into the
  // heap.
  case CreateGen:
  case CreateAGen:
  case CreateAFWH: {
    auto const fp = canonical(inst.src(0));
    auto fpInst = fp->inst();
    auto const frame = [&] () -> AliasClass {
      if (fpInst->is(DefFP, DefFuncEntryFP)) return ALocalAny;
      assertx(fpInst->is(BeginInlining));
      auto const nlocals = fpInst->extra<BeginInlining>()->func->numLocals();
      return nlocals
        ? ALocal { fp, AliasIdSet::IdRange(0, nlocals)}
        : AEmpty;
    }();
    return may_load_store_move(
      frame | AActRec { fp },
      AHeapAny,
      frame
    );
  }

  // AGWH construction updates the AsyncGenerator object.
  case CreateAGWH:
    return may_load_store(AHeapAny | AActRec { inst.src(0) }, AHeapAny);

  case CreateAAWH:
    {
      auto const extra = inst.extra<CreateAAWH>();
      auto const frame = ALocal {
        inst.src(0),
        AliasIdSet {
          AliasIdSet::IdRange{ extra->first, extra->first + extra->count }
        }
      };
      return may_load_store(frame, AHeapAny);
    }

  case CountWHNotDone:
    {
      auto const extra = inst.extra<CountWHNotDone>();
      auto const frame = ALocal {
        inst.src(0),
        AliasIdSet {
          AliasIdSet::IdRange{ extra->first, extra->first + extra->count }
        }
      };
      return may_load_store(frame, AEmpty);
    }

  // This re-enters to call extension-defined instance constructors.
  case ConstructInstance:
    return may_load_store(AHeapAny, AHeapAny);

  // Closures don't ever throw or reenter on construction
  case ConstructClosure:
    return IrrelevantEffects{};

  case CheckStackOverflow:
  case CheckSurpriseFlagsEnter:
  case CheckSurpriseAndStack:
    return may_load_store(AEmpty, AEmpty);

  //////////////////////////////////////////////////////////////////////
  // Iterator instructions

  case IterInit:
  case LIterInit:
  case IterNext:
  case LIterNext: {
    auto const& args = inst.extra<IterData>()->args;
    assertx(!args.hasKey());
    auto const fp = inst.src(inst.op() == IterNext ? 0 : 1);
    AliasClass val = ALocal { fp, safe_cast<uint32_t>(args.valId) };
    return iter_effects(inst, fp, val);
  }

  case IterInitK:
  case LIterInitK:
  case IterNextK:
  case LIterNextK: {
    auto const& args = inst.extra<IterData>()->args;
    assertx(args.hasKey());
    auto const fp = inst.src(inst.op() == IterNextK ? 0 : 1);
    AliasClass key = ALocal { fp, safe_cast<uint32_t>(args.keyId) };
    AliasClass val = ALocal { fp, safe_cast<uint32_t>(args.valId) };
    return iter_effects(inst, fp, key | val);
  }

  case IterFree: {
    auto const base = aiter_base(inst.src(0), iterId(inst));
    return may_load_store(AHeapAny | base, AHeapAny);
  }

  case CheckIter: {
    auto const iter = inst.extra<CheckIter>()->iterId;
    return may_load_store(aiter_type(inst.src(0), iter), AEmpty);
  }

  case LdIterBase:
    return PureLoad { aiter_base(inst.src(0), iterId(inst)) };

  case LdIterPos:
    return PureLoad { aiter_pos(inst.src(0), iterId(inst)) };

  case LdIterEnd:
    return PureLoad { aiter_end(inst.src(0), iterId(inst)) };

  case StIterBase:
    return PureStore { aiter_base(inst.src(0), iterId(inst)), inst.src(1) };

  case StIterType: {
    auto const iter = inst.extra<StIterType>()->iterId;
    return PureStore { aiter_type(inst.src(0), iter), nullptr };
  }

  case StIterPos:
    return PureStore { aiter_pos(inst.src(0), iterId(inst)), inst.src(1) };

  case StIterEnd:
    return PureStore { aiter_end(inst.src(0), iterId(inst)), inst.src(1) };

  case KillActRec:
    return may_load_store_kill(AEmpty, AEmpty, AActRec { inst.src(0) });

  case KillLoc: {
    auto const local = inst.extra<LocalId>()->locId;
    return may_load_store_kill(AEmpty, AEmpty, ALocal { inst.src(0), local });
  }

  case KillIter: {
    auto const iters = aiter_all(inst.src(0), iterId(inst));
    return may_load_store_kill(AEmpty, AEmpty, iters);
  }

  //////////////////////////////////////////////////////////////////////
  // Instructions that explicitly manipulate locals

  case StLoc:
  case StLocMeta:
    return PureStore {
      ALocal { inst.src(0), inst.extra<LocalId>()->locId },
      inst.src(1),
      nullptr
    };

  case StLocRange:
    {
      auto const extra = inst.extra<StLocRange>();
      auto acls = AEmpty;

      for (auto locId = extra->start; locId < extra->end; ++locId) {
        acls = acls | ALocal { inst.src(0), locId };
      }
      return PureStore { acls, inst.src(1), nullptr };
    }

  case LdLoc:
    return PureLoad { ALocal { inst.src(0), inst.extra<LocalId>()->locId } };

  case LdLocForeign:
    return may_load_store(ALocalAny, AEmpty);

  case CheckLoc:
    return may_load_store(
      ALocal { inst.src(0), inst.extra<LocalId>()->locId },
      AEmpty
    );

  //////////////////////////////////////////////////////////////////////
  // Pointer-based loads and stores

  case LdMem:
    return PureLoad { pointee(inst.src(0)) };
  case StMem:
  case StMemMeta:
    return PureStore { pointee(inst.src(0)), inst.src(1), inst.src(0) };

  case LdClsInitElem:
    return PureLoad { AHeapAny };

  case StClsInitElem:
    return PureStore { AHeapAny };

  case LdPairElem:
    return PureLoad { AHeapAny };

  case LdMBase:
    return PureLoad { AMIStateBase };

  case StMBase:
    return PureStore { AMIStateBase, inst.src(0), nullptr };

  case StMROProp:
    return PureStore { AMIStateROProp, inst.src(0), nullptr };

  case CheckMROProp:
    return may_load_store(AMIStateROProp, AEmpty);

  case FinishMemberOp:
    return may_load_store_kill(AEmpty, AEmpty, AMIStateAny);

  case IsNTypeMem:
  case IsTypeMem:
  case CheckTypeMem:
  case CheckInitMem:
    return may_load_store(pointee(inst.src(0)), AEmpty);

  case CheckRDSInitialized:
    return may_load_store(
      ARds { inst.extra<CheckRDSInitialized>()->handle },
      AEmpty
    );
  case MarkRDSInitialized:
    return may_load_store(
      AEmpty,
      ARds { inst.extra<MarkRDSInitialized>()->handle }
    );
  case MarkRDSAccess:
    return IrrelevantEffects{};
  // LdTVFromRDS and StTVInRDS load/store aux bit, so they cannot be
  // PureLoad/PureStore -- load/store elim do not track aux bit accesses.
  case LdTVFromRDS:
    return may_load_store(
      ARds { inst.extra<LdTVFromRDS>()->handle },
      AEmpty
    );
  case StTVInRDS:
    return may_load_store(
      AEmpty,
      ARds { inst.extra<StTVInRDS>()->handle }
    );

  case InitProps:
    return may_load_store(
      AHeapAny,
      AHeapAny | ARds { inst.extra<InitProps>()->cls->propHandle() }
    );

  case InitSProps:
    return may_load_store(
      AHeapAny,
      AHeapAny | ARds { inst.extra<InitSProps>()->cls->sPropInitHandle() }
    );

  case LdARFunc:
  case LdClsFromClsMeth:
  case LdFuncFromClsMeth:
  case LdFuncFromRFunc:
  case LdGenericsFromRFunc:
  case LdClsFromRClsMeth:
  case LdFuncFromRClsMeth:
  case LdGenericsFromRClsMeth:
    return may_load_store(AEmpty, AEmpty);

  //////////////////////////////////////////////////////////////////////
  // Object/Ref loads/stores

  case InitObjProps:
    return may_load_store(AEmpty, APropAny);

  case InitObjMemoSlots:
    // Writes to memo slots, but these are not modeled.
    return IrrelevantEffects {};

  case LockObj:
    // Writes object attributes, but these are not modeled.
    return IrrelevantEffects {};

  // Loads $obj->trace, stores $obj->file and $obj->line.
  case InitThrowableFileAndLine:
    return may_load_store(AHeapAny, APropAny);

  //////////////////////////////////////////////////////////////////////
  // Array loads and stores

  case InitVecElem: {
    auto const arr = inst.src(0);
    auto const val = inst.src(1);
    auto const idx = inst.extra<InitVecElem>()->index;
    return PureStore { AElemI { arr, idx }, val, arr };
  }

  case InitDictElem: {
    auto const arr = inst.src(0);
    auto const val = inst.src(1);
    auto const key = inst.extra<InitDictElem>()->key;
    return PureStore { AElemS { arr, key }, val, arr };
  }

  case InitStructElem: {
    auto const arr = inst.src(0);
    auto const val = inst.src(1);
    auto const key = inst.extra<InitStructElem>()->key;
    return PureStore { AElemS { arr, key }, val, arr };
  }

  case LdMonotypeDictVal: {
    // TODO(mcolavita): When we have a type-array-elem method to get the key
    // of an arbitrary array-like type, use that to narrow this load.
    return PureLoad { AElemAny };
  }

  case LdMonotypeVecElem:
  case LdVecElem: {
    auto const base = inst.src(0);
    auto const key  = inst.src(1);
    return PureLoad {
      key->hasConstVal() ? AElemI { base, key->intVal() } : AElemIAny
    };
  }

  case DictGetK:
  case KeysetGetK:
  case BespokeGet:
  case KeysetGetQuiet:
  case DictGetQuiet: {
    auto const base = inst.src(0);
    auto const key  = inst.src(1);
    assertx(key->type().subtypeOfAny(TInt, TStr));
    if (key->isA(TInt)) {
      return PureLoad {
        key->hasConstVal() ? AElemI { base, key->intVal() } : AElemIAny,
      };
    } else {
      return PureLoad {
        key->hasConstVal() ? AElemS { base, key->strVal() } : AElemSAny,
      };
    }
  }

  case DictIsset:
  case DictIdx:
  case KeysetIsset:
  case KeysetIdx:
  case AKExistsDict:
  case AKExistsKeyset:
  case BespokeGetThrow: {
    auto const base = inst.src(0);
    auto const key  = inst.src(1);
    assertx(key->type().subtypeOfAny(TInt, TStr));
    auto const elem = [&] {
      if (key->isA(TInt)) {
        return key->hasConstVal() ? AElemI { base, key->intVal() } : AElemIAny;
      } else {
        return key->hasConstVal() ? AElemS { base, key->strVal() } : AElemSAny;
      }
    }();
    return may_load_store(elem, AEmpty);
  }

  case InitVecElemLoop:
    {
      auto const extra = inst.extra<InitVecElemLoop>();
      auto const stack_in = AStack::range(
        extra->offset,
        extra->offset + static_cast<int32_t>(extra->size)
      );
      return may_load_store_move(stack_in, AElemIAny, stack_in);
    }

  // These ops may read anything referenced by the input array or object,
  // but not any of the locals or stack frame slots.
  case NewLoggingArray:
  case ProfileArrLikeProps:
    return may_load_store(AHeapAny, AEmpty);

  case NewKeysetArray:
    {
      // NewKeysetArray is reading elements from the stack, but writes to a
      // completely new array, so we can treat the store set as empty.
      auto const extra = inst.extra<NewKeysetArray>();
      auto const stack_in = AStack::range(
        extra->offset,
        extra->offset + static_cast<int32_t>(extra->size)
      );
      return may_load_store_move(stack_in, AEmpty, stack_in);
    }

  case NewStructDict:
    {
      // NewStructDict reads elements from the stack, but writes to a
      // completely new array, so we can treat the store set as empty.
      auto const extra = inst.extra<NewStructData>();
      auto const stack_in = AStack::range(
        extra->offset,
        extra->offset + static_cast<int32_t>(extra->numKeys)
      );
      return may_load_store_move(stack_in, AEmpty, stack_in);
    }

  case NewBespokeStructDict:
    {
      // NewBespokeStructDict reads elements from the stack, but writes to
      // a completely new array, so we can treat the stores as empty.
      auto const extra = inst.extra<NewBespokeStructDict>();
      auto const stack_in = AStack::range(
        extra->offset,
        extra->offset + static_cast<int32_t>(extra->numSlots)
      );
      return may_load_store_move(stack_in, AEmpty, stack_in);
    }

  case MemoGetStaticValue:
  case MemoGetLSBValue:
  case MemoGetInstanceValue:
    // Only reads the memo value (which isn't modeled here).
    return may_load_store(AEmpty, AEmpty);

  case MemoSetStaticValue:
  case MemoSetLSBValue:
  case MemoSetInstanceValue:
    // Writes to the memo value (which isn't modeled)
    return may_load_store(AEmpty, AEmpty);

  case MemoGetStaticCache:
  case MemoGetLSBCache:
  case MemoSetStaticCache:
  case MemoSetLSBCache: {
    // Reads some (non-zero) set of locals for keys, and reads/writes from the
    // memo cache (which isn't modeled).
    auto const extra = inst.extra<MemoCacheStaticData>();
    auto const frame = ALocal {
      inst.src(0),
      AliasIdSet{
        AliasIdSet::IdRange{
          extra->keys.first,
          extra->keys.first + extra->keys.count
        }
      }
    };

    return may_load_store(frame, AEmpty);
  }

  case MemoGetInstanceCache:
  case MemoSetInstanceCache: {
    // Reads some set of locals for keys, and reads/writes from the memo cache
    // (which isn't modeled).
    auto const extra = inst.extra<MemoCacheInstanceData>();
    auto const frame = [&]() -> AliasClass {
      // Unlike MemoGet/SetStaticCache, we can have an empty key range here.
      if (extra->keys.count == 0) return AEmpty;

      return ALocal {
        inst.src(0),
        AliasIdSet{
          AliasIdSet::IdRange{
            extra->keys.first,
            extra->keys.first + extra->keys.count
          }
        }
      };
    }();
    return may_load_store(frame, AEmpty);
  }

  case BespokeIterGetKey:
  case LdPtrIterKey:
    // Array element keys are not tracked by memory effects right
    // now. Be conservative and use AElemAny.
    return may_load_store(AElemAny, AEmpty);

  case LdPtrIterVal:
    return PureLoad { AElemAny };

  case BespokeIterGetVal:
    return may_load_store(AElemAny, AEmpty);

  case ElemDictK:
    return IrrelevantEffects {};

  case VecFirst: {
    auto const base = inst.src(0);
    return may_load_store(AElemI { base, 0 }, AEmpty);
  }
  case VecLast: {
    auto const base = inst.src(0);
    if (base->hasConstVal(TArrLike)) {
      auto const index = static_cast<int64_t>(base->arrLikeVal()->size() - 1);
      return may_load_store(AElemI { base, index }, AEmpty);
    }
    return may_load_store(AElemIAny, AEmpty);
  }
  case DictFirst:
  case DictLast:
  case KeysetFirst:
  case KeysetLast:
    return may_load_store(AElemAny, AEmpty);

  case DictFirstKey:
  case DictLastKey:
  case LdMonotypeDictTombstones:
  case LdMonotypeDictKey:
    return may_load_store(AEmpty, AEmpty);

  case CheckDictKeys:
  case CheckDictOffset:
  case CheckKeysetOffset:
  case CheckMissingKeyInArrLike:
  case ProfileDictAccess:
  case ProfileKeysetAccess:
  case CheckArrayCOW:
  case ProfileArrayCOW:
    return may_load_store(AHeapAny, AEmpty);

  case SameArrLike:
  case NSameArrLike:
    return may_load_store(AElemAny, AEmpty);

  case EqArrLike:
  case NeqArrLike: {
    if (inst.src(0)->type() <= TKeyset && inst.src(1)->type() <= TKeyset) {
      return may_load_store(AElemAny, AEmpty);
    } else {
      return may_load_store(AHeapAny, AHeapAny);
    }
  }

  case AKExistsObj:
    return may_load_store(AHeapAny, AHeapAny);

  //////////////////////////////////////////////////////////////////////
  // Member instructions

  case CheckMBase:
    return may_load_store(pointee(inst.src(0)), AEmpty);

  /*
   * Various minstr opcodes that take a Lval in src 0, which may or may not
   * point to a frame local or the evaluation stack. Some may read or write to
   * that pointer while some only read. They can all re-enter the VM and access
   * arbitrary heap locations.
   */
  case IncDecElem:
  case SetElem:
  case SetNewElem:
  case SetOpElem:
  case SetNewElemDict:
  case SetNewElemVec:
  case SetNewElemKeyset:
  case UnsetElem:
  case ElemDictD:
  case ElemDictU:
  case BespokeElem:
  case ElemDX:
  case ElemUX:
  case SetRange:
  case SetRangeRev:
    // These member ops will load and store from the base lval which they
    // take as their first argument, which may point anywhere in the heap.
    return may_load_store(
      AHeapAny | all_pointees(inst),
      AHeapAny | all_pointees(inst)
    );

  case CGetElem:
  case IssetElem:
  case ElemX:
  case CGetProp:
  case CGetPropQ:
  case SetProp:
  case UnsetProp:
  case IssetProp:
  case IncDecProp:
  case SetOpProp:
  case ReserveVecNewElem:
    return may_load_store(AHeapAny, AHeapAny);

  /*
   * Intermediate minstr operations. In addition to a base pointer like the
   * operations above, these may take a pointer to MInstrState::tvRef
   * which they may store to (but not read from).
   */
  case PropX:
  case PropDX:
  case PropQ:
    return minstr_with_tvref(inst);

  /*
   * Collection accessors can read from their inner array buffer, but stores
   * COW and behave as if they only affect collection memory locations.  We
   * don't track those, so it's returning AEmpty for now.
   */
  case MapIsset:
  case PairIsset:
  case VectorIsset:
    return may_load_store(AHeapAny, AEmpty /* Note */);
  case MapGet:
  case MapSet:
  case VectorSet:
    return may_load_store(AHeapAny, AEmpty /* Note */);

  case LdInitPropAddr:
    return may_load_store(
      AProp {
        inst.src(0),
        safe_cast<uint16_t>(inst.extra<LdInitPropAddr>()->index)
      },
      AEmpty
    );
  case LdInitRDSAddr:
    return may_load_store(
      ARds { inst.extra<LdInitRDSAddr>()->handle },
      AEmpty
    );

  //////////////////////////////////////////////////////////////////////
  // Instructions that allocate new objects, without reading any other memory
  // at all, so any effects they have on some types of memory locations we
  // track are isolated from anything else we care about.

  case NewClsMeth:
  case NewRClsMeth:
  case NewCol:
  case NewColFromArray:
  case NewPair:
  case NewInstanceRaw:
  case NewDictArray:
  case NewRFunc:
  case FuncCred:
  case AllocVec:
  case AllocStructDict:
  case AllocBespokeStructDict:
  case ConvDblToStr:
  case ConvIntToStr:
  case InitStructPositions:
  case AllocInitROM:
  case StPtrAt:
  case StTypeAt:
  case VoidPtrAsDataType:
    return IrrelevantEffects {};

  case AllocObj:
    // AllocObj re-enters to call constructors, but if it weren't for that we
    // could ignore its loads and stores since it's a new object.
    return may_load_store(AEmpty, AEmpty);
  case AllocObjReified:
    // Similar to AllocObj but also stores the reification
    return may_load_store(AEmpty, AHeapAny);

  //////////////////////////////////////////////////////////////////////
  // Instructions that explicitly manipulate the stack.

  case LdStk:
    return PureLoad { AStack::at(inst.extra<LdStk>()->offset) };

  case StStk:
  case StStkMeta:
    return PureStore {
      AStack::at(inst.extra<IRSPRelOffsetData>()->offset),
      inst.src(1),
      nullptr
    };

  case StStkRange: {
    auto const extra = inst.extra<StStkRange>();
    auto const startOff = extra->start;
    auto const count = extra->count;
    return PureStore {
      AStack::range(startOff, startOff + static_cast<int32_t>(count)),
      inst.src(1),
      nullptr
    };
  }

  case StOutValue:
    // Technically these writes affect the caller's stack, but there is no way
    // to actually observe them from within the callee. They can also only
    // occur once on any exit path from a function.
    return may_load_store(AEmpty, AEmpty);

  case LdOutAddr:
    return IrrelevantEffects{};

  case CheckStk:
    return may_load_store(
      AStack::at(inst.extra<CheckStk>()->offset),
      AEmpty
    );

  case DbgTraceCall:
    return may_load_store(AStackAny | ALocalAny, AEmpty);

  case Unreachable:
    // Unreachable code kills every memory location.
    return may_load_store_kill(AEmpty, AEmpty, AUnknown);

  case ResolveTypeStruct: {
    auto const extra = inst.extra<ResolveTypeStructData>();
    auto const stack_in = AStack::range(
      extra->offset,
      extra->offset + static_cast<int32_t>(extra->size)
    );
    return may_load_store(AliasClass(stack_in)|AHeapAny, AHeapAny);
  }

  case DefFP:
    return may_load_store(AFBasePtr, AFBasePtr);

  case DefFuncEntryFP:
    return may_load_store(AFBasePtr, AFBasePtr | AFMeta { inst.dst() });

  case InitFrame:
    /* The last opcode of prologues. Does not modify any defined frame. */
    return may_load_store(AEmpty, AEmpty);

  case LdARFlags:
    return PureLoad { AFMeta { inst.src(0) }};

  case LdUnitPerRequestFilepath:
    return PureLoad {
      ARds { inst.extra<LdUnitPerRequestFilepath>()->handle },
    };

  case LdImplicitContext:
    return PureLoad { ARds { ImplicitContext::activeCtx.handle() } };

  case StImplicitContext:
    return PureStore {
      ARds { ImplicitContext::activeCtx.handle() }, inst.src(0), nullptr
    };

  case StImplicitContextWH:
    return may_load_store(
      ARds { ImplicitContext::activeCtx.handle() },
      AEmpty
    );

  //////////////////////////////////////////////////////////////////////
  // Instructions that never read or write memory locations tracked by this
  // module.

  case AbsDbl:
  case AddDbl:
  case AddInt:
  case AddIntO:
  case AddOffset:
  case AdvanceDictPtrIter:
  case AdvanceVecPtrIter:
  case AndInt:
  case AssertType:
  case AssertLoc:
  case AssertStk:
  case AssertMBase:
  case BespokeIterEnd:
  case BespokeIterFirstPos:
  case BespokeIterLastPos:
  case ConvFuncPrologueFlagsToARFlags:
  case DefFrameRelSP:
  case DefFuncPrologueCallee:
  case DefFuncPrologueCtx:
  case DefFuncPrologueFlags:
  case DefFuncPrologueNumArgs:
  case DefRegSP:
  case EndGuards:
  case EnterPrologue:
  case EnterTranslation:
  case EqBool:
  case EqCls:
  case EqLazyCls:
  case EqFunc:
  case EqStrPtr:
  case EqArrayDataPtr:
  case EqDbl:
  case EqInt:
  case EqPtrIter:
  case ExitPrologue:
  case GetDictPtrIter:
  case GetVecPtrIter:
  case GteBool:
  case GteInt:
  case GtBool:
  case GtInt:
  case Jmp:
  case JmpNZero:
  case JmpZero:
  case LdPropAddr:
  case LdStkAddr:
  case LdVecElemAddr:
  case LteBool:
  case LteDbl:
  case LteInt:
  case LtBool:
  case LtInt:
  case GtDbl:
  case GteDbl:
  case LtDbl:
  case DivDbl:
  case DivInt:
  case MulDbl:
  case MulInt:
  case MulIntO:
  case NeqBool:
  case NeqDbl:
  case NeqInt:
  case SameObj:
  case NSameObj:
  case EqRes:
  case NeqRes:
  case CmpBool:
  case CmpInt:
  case CmpDbl:
  case SubDbl:
  case SubInt:
  case SubIntO:
  case XorBool:
  case XorInt:
  case OrInt:
  case AssertNonNull:
  case CheckNonNull:
  case CheckNullptr:
  case CheckSmashableClass:
  case Ceil:
  case Floor:
  case DefLabel:
  case CheckInit:
  case Nop:
  case Mod:
  case Conjure:
  case ConjureUse:
  case EndBlock:
  case ConvBoolToInt:
  case ConvBoolToDbl:
  case DefConst:
  case LdLocAddr:
  case Sqrt:
  case Shl:
  case Shr:
  case Lshr:
  case IsNType:
  case IsType:
  case Mov:
  case ConvDblToBool:
  case ConvDblToInt:
  case DblAsBits:
  case LdMIStateTempBaseAddr:
  case LdClsCns:
  case LdSubClsCns:
  case LdResolvedTypeCns:
  case LdResolvedTypeCnsClsName:
  case LdResolvedTypeCnsNoCheck:
  case CheckSubClsCns:
  case LdClsCnsVecLen:
  case FuncHasAttr:
  case ClassHasAttr:
  case LdFuncRequiredCoeffects:
  case IsFunReifiedGenericsMatched:
  case JmpPlaceholder:
  case LdSmashable:
  case LdSmashableFunc:
  case LdRDSAddr:
  case CheckRange:
  case ProfileType:
  case LdIfaceMethod:
  case InstanceOfIfaceVtable:
  case IsTypeStructCached:
  case LdTVAux:
  case MethodExists:
  case GetTime:
  case GetTimeNs:
  case ProfileInstanceCheck:
  case Select:
  case LookupSPropSlot:
  case ConvPtrToLval:
  case ProfileProp:
  case ProfileIsTypeStruct:
  case LdLazyClsName:
  case DirFromFilepath:
  case CheckFuncNeedsCoverage:
  case RecordFuncCall:
  case LoadBCSP:
  case StructDictSlot:
  case StructDictElemAddr:
  case StructDictAddNextSlot:
  case StructDictTypeBoundCheck:
    return IrrelevantEffects {};

  case LookupClsCns:
  case LookupClsCtxCns:
    return may_load_store(AEmpty, AEmpty);

  case StClosureArg:
    return PureStore {
      AProp {
        inst.src(0),
        safe_cast<uint16_t>(inst.extra<StClosureArg>()->index)
      },
      inst.src(1),
      inst.src(0)
    };

  case StArResumeAddr:
    return PureStore { AFMeta { inst.src(0) }, nullptr };

  case LdFrameThis:
  case LdFrameCls:
    return PureLoad { AFContext { inst.src(0) }};

  case StFrameCtx:
    return PureStore { AFContext { inst.src(0) }, inst.src(1) };

  case StFrameFunc:
    return PureStore { AFFunc { inst.src(0) }, nullptr };

  case StFrameMeta:
    return PureStore { AFMeta { inst.src(0) }, nullptr };

  case StVMFP:
    return PureStore { AVMFP, inst.src(0) };

  case StVMSP:
    return PureStore { AVMSP, inst.src(0) };

  case StVMPC:
    return PureStore { AVMPC, nullptr };

  case StVMReturnAddr:
    return PureStore { AVMRetAddr, inst.src(0) };

  case StVMRegState:
    return PureStore { AVMRegState, inst.src(0) };

  //////////////////////////////////////////////////////////////////////
  // Instructions that technically do some things w/ memory, but not in any way
  // we currently care about.  They however don't return IrrelevantEffects
  // because we assume (in refcount-opts) that IrrelevantEffects instructions
  // can't even inspect Countable reference count fields, and several of these
  // can.  All GeneralEffects instructions are assumed to possibly do so.

  case DecRefNZ:
  case ProfileDecRef:
  case AFWHBlockOn:
  case AFWHPushTailFrame:
  case IncRef:
  case LdClosureCls:
  case LdClosureThis:
  case LdRetVal:
  case ConvStrToInt:
  case ConvResToInt:
  case OrdStr:
  case ChrInt:
  case CreateSSWH:
  case CheckSurpriseFlags:
  case CheckType:
  case ZeroErrorLevel:
  case RestoreErrorLevel:
  case CheckCold:
  case ContValid:
  case IncProfCounter:
  case IncCallCounter:
  case IncStat:
  case ContCheckNext:
  case CountVec:
  case CountDict:
  case CountKeyset:
  case HasReifiedGenerics:
  case InstanceOf:
  case InstanceOfBitmask:
  case NInstanceOfBitmask:
  case InstanceOfIface:
  case InterfaceSupportsArrLike:
  case InterfaceSupportsDbl:
  case InterfaceSupportsInt:
  case InterfaceSupportsStr:
  case IsLegacyArrLike:
  case IsWaitHandle:
  case IsCol:
  case HasToString:
  case DbgAssertRefCount:
  case DbgCheckLocalsDecRefd:
  case GtStr:
  case GteStr:
  case LtStr:
  case LteStr:
  case EqStr:
  case NeqStr:
  case SameStr:
  case NSameStr:
  case CmpStr:
  case GtRes:
  case GteRes:
  case LtRes:
  case LteRes:
  case CmpRes:
  case LdBindAddr:
  case LdSSwitchDest:
  case RBTraceEntry:
  case RBTraceMsg:
  case ConvIntToBool:
  case ConvIntToDbl:
  case ConvStrToBool:
  case ConvStrToDbl:
  case ConvResToDbl:
  case ExtendsClass:
  case LdUnwinderValue:
  case LdClsName:
  case LdLazyCls:
  case LdAFWHActRec:
  case LdContActRec:
  case LdContArKey:
  case LdContArValue:
  case LdContField:
  case LdContResumeAddr:
  case StContArKey:
  case StContArValue:
  case StContArState:
  case ContArIncIdx:
  case ContArIncKey:
  case ContArUpdateIdx:
  case LdClsCachedSafe:
  case LdClsInitData:
  case UnwindCheckSideExit:
  case LdCns:
  case LdFuncVecLen:
  case LdClsMethod:
  case LdClsMethodCacheCls:
  case LdClsMethodCacheFunc:
  case LdClsMethodFCacheFunc:
  case LdTypeCns:
  case LdTypeCnsNoThrow:
  case LdTypeCnsClsName:
  case ProfileSwitchDest:
  case LdFuncCls:
  case LdFuncInOutBits:
  case LdFuncNumParams:
  case LdFuncName:
  case LdMethCallerName:
  case LdObjClass:
  case LdObjInvoke:
  case LdObjMethodD:
  case LdObjMethodS:
  case LdStrLen:
  case StringIsset:
  case LdWHResult:
  case LdWHState:
  case LdWHNotDone:
  case LookupClsMethod:
  case LookupClsRDS:
  case StrictlyIntegerConv:
  case DbgAssertFunc:
  case ProfileCall:
  case ProfileMethod:
  case DecReleaseCheck:
    return may_load_store(AEmpty, AEmpty);

  case BeginCatch:
    return may_load_store(AEmpty, AVMRegAny | AVMRegState);

  case LogArrayReach:
  case LogGuardFailure:
    return may_load_store(AHeapAny, AEmpty);

  // Some that touch memory we might care about later, but currently don't:
  case ColIsEmpty:
  case ColIsNEmpty:
  case ConvTVToBool:
  case ConvObjToBool:
  case CountCollection:
  case LdVectorSize:
  case CheckVecBounds:
  case LdColVec:
  case LdColDict:
    return may_load_store(AEmpty, AEmpty);

  //////////////////////////////////////////////////////////////////////
  // Instructions that can re-enter the VM and touch most heap things.  They
  // also may generally write to the eval stack below an offset (see
  // alias-class.h above AStack for more).

  case DecRef:
    return may_load_store(AHeapAny, AHeapAny);

  case ReleaseShallow:
    return may_load_store(AHeapAny, AHeapAny);

  case GetMemoKey:
    return may_load_store(AHeapAny, AHeapAny);

  case GetMemoKeyScalar:
    return IrrelevantEffects{};

  case ProfileGlobal:
  case LdGblAddr:
  case LdGblAddrDef:
  case BaseG:
    return may_load_store(AEmpty, AEmpty);

  case LdClsCtor:
    return may_load_store(AEmpty, AEmpty);

  case RaiseCoeffectsCallViolation:
  case RaiseCoeffectsFunParamCoeffectRulesViolation:
  case RaiseCoeffectsFunParamTypeViolation:
    return may_load_store(AEmpty, AEmpty);

  case LdClsPropAddrOrNull:   // may run 86{s,p}init, which can autoload
  case LdClsPropAddrOrRaise:  // raises errors, and 86{s,p}init
    return may_load_store(
      AHeapAny,
      AHeapAny | all_pointees(inst) | AMIStateROProp
    );
  case Clone:
  case ThrowArrayIndexException:
  case ThrowArrayKeyException:
  case ThrowUninitLoc:
  case ThrowUndefPropException:
  case RaiseTooManyArg:
  case RaiseError:
  case RaiseNotice:
  case RaiseWarning:
  case RaiseForbiddenDynCall:
  case RaiseForbiddenDynConstruct:
  case RaiseStrToClassNotice:
  case CheckClsMethFunc:
  case CheckClsReifiedGenericMismatch:
  case CheckFunReifiedGenericMismatch:
  case CheckInOutMismatch:
  case CheckReadonlyMismatch:
  case ConvTVToStr:
  case ConvObjToStr:
  case Count:      // re-enters on CountableClass
  case GtObj:
  case GteObj:
  case LtObj:
  case LteObj:
  case EqObj:
  case NeqObj:
  case CmpObj:
  case GtArrLike:
  case GteArrLike:
  case LtArrLike:
  case LteArrLike:
  case CmpArrLike:
  case OODeclExists:
  case LdCls:          // autoload
  case LdClsCached:    // autoload
  case LdFunc:         // autoload
  case LdFuncCached:   // autoload
  case InitClsCns:      // autoload
  case InitSubClsCns: // May run 86cinit
  case ProfileSubClsCns: // May run 86cinit
  case LookupClsMethodCache:  // autoload
  case LookupClsMethodFCache: // autoload
  case LookupCnsE:
  case LookupFuncCached: // autoload
  case StringGet:      // raise_notice
  case OrdStrIdx:      // raise_notice
  case AddNewElemKeyset:   // can re-enter
  case DictGet:
  case KeysetGet:
  case DictSet:
  case BespokeSet:
  case BespokeAppend:
  case BespokeUnset:
  case StructDictUnset:
  case ConcatStrStr:
  case PrintStr:
  case PrintBool:
  case PrintInt:
  case ConcatIntStr:
  case ConcatStrInt:
  case ConvObjToDbl:
  case ConvObjToInt:
  case ConvTVToInt:
  case ConcatStr3:
  case ConcatStr4:
  case ConvTVToDbl:
  case ConvObjToVec:
  case ConvObjToDict:
  case ConvObjToKeyset:
  case ThrowOutOfBounds:
  case ThrowInvalidArrayKey:
  case ThrowInvalidOperation:
  case ThrowCallReifiedFunctionWithoutGenerics:
  case ThrowDivisionByZeroException:
  case ThrowHasThisNeedStatic:
  case ThrowInOutMismatch:
  case ThrowReadonlyMismatch:
  case ThrowLateInitPropError:
  case ThrowMissingArg:
  case ThrowMissingThis:
  case ThrowParameterWrongType:
  case ArrayMarkLegacyShallow:
  case ArrayMarkLegacyRecursive:
  case ThrowCannotModifyReadonlyCollection:
  case ThrowLocalMustBeValueTypeException:
  case ThrowMustBeEnclosedInReadonly:
  case ThrowMustBeMutableException:
  case ThrowMustBeReadonlyException:
  case ThrowMustBeValueTypeException:
  case ArrayUnmarkLegacyShallow:
  case ArrayUnmarkLegacyRecursive:
  case SetOpTV:
  case OutlineSetOp:
  case ThrowAsTypeStructException:
  case PropTypeRedefineCheck: // Can raise and autoload
  case HandleRequestSurprise:
  case BespokeEscalateToVanilla:
    return may_load_store(AHeapAny, AHeapAny);

  case AddNewElemVec:
  case RaiseErrorOnInvalidIsAsExpressionType:
  case IsTypeStruct:
  case RecordReifiedGenericsAndGetTSList:
  case CopyArray:
    return may_load_store(AElemAny, AEmpty);

  case ConvArrLikeToVec:
  case ConvArrLikeToDict:
  case ConvArrLikeToKeyset: // Decrefs input values
    return may_load_store(AElemAny, AEmpty);

  // debug_backtrace() traverses stack and WaitHandles on the heap.
  case DebugBacktrace:
    return may_load_store(AHeapAny|ALocalAny|AStackAny, AHeapAny);

  // This instruction doesn't touch memory we track, except that it may
  // re-enter to construct php Exception objects.  During this re-entry anything
  // can happen (e.g. a surprise flag check could cause a php signal handler to
  // run arbitrary code).
  case AFWHPrepareChild:
    return may_load_store(AActRec { inst.src(0) }, AEmpty);

  //////////////////////////////////////////////////////////////////////
  // The following instructions are used for debugging memory optimizations.
  // We can't ignore them, because they can prevent future optimizations;
  // eg t1 = LdStk<N>; DbgTrashStk<N>; StStk<N> t1
  // If we ignore the DbgTrashStk it looks like the StStk is redundant

  case DbgTrashStk:
    return may_load_store_kill(
      AEmpty, AEmpty, AStack::at(inst.extra<DbgTrashStk>()->offset));
  case DbgTrashFrame:
    return may_load_store_kill(
      AEmpty, AEmpty, actrec(inst.src(0), inst.extra<DbgTrashFrame>()->offset));
  case DbgTrashMem:
    return may_load_store_kill(AEmpty, AEmpty, pointee(inst.src(0)));

  //////////////////////////////////////////////////////////////////////

  }

  not_reached();
}