/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "vm/Watchtower.h"

#include "js/CallAndConstruct.h"
#include "js/experimental/TypedData.h"
#include "vm/Compartment.h"
#include "vm/JSContext.h"
#include "vm/JSObject.h"
#include "vm/NativeObject.h"
#include "vm/PlainObject.h"
#include "vm/Realm.h"
#include "vm/TypedArrayObject.h"

#include "vm/Compartment-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"
#include "vm/Realm-inl.h"
#include "vm/Shape-inl.h"

using namespace js;

static bool AddToWatchtowerLog(JSContext* cx, const char* kind,
                               HandleObject obj, HandleValue extra) {
  // Add an object storing {kind, object, extra} to the log for testing
  // purposes.

  MOZ_ASSERT(obj->useWatchtowerTestingLog());

  RootedString kindString(cx, NewStringCopyZ<CanGC>(cx, kind));
  if (!kindString) {
    return false;
  }

  Rooted<PlainObject*> logObj(cx, NewPlainObjectWithProto(cx, nullptr));
  if (!logObj) {
    return false;
  }
  if (!JS_DefineProperty(cx, logObj, "kind", kindString, JSPROP_ENUMERATE)) {
    return false;
  }
  if (!JS_DefineProperty(cx, logObj, "object", obj, JSPROP_ENUMERATE)) {
    return false;
  }
  if (!JS_DefineProperty(cx, logObj, "extra", extra, JSPROP_ENUMERATE)) {
    return false;
  }

  if (!cx->runtime()->watchtowerTestingLog->append(logObj)) {
    ReportOutOfMemory(cx);
    return false;
  }

  return true;
}

static bool ReshapeForShadowedProp(JSContext* cx, Handle<NativeObject*> obj,
                                   HandleId id) {
  // |obj| has been used as the prototype of another object. Check if we're
  // shadowing a property on its proto chain. In this case we need to reshape
  // that object for shape teleporting to work correctly.
  //
  // See also the 'Shape Teleporting Optimization' comment in jit/CacheIR.cpp.

  MOZ_ASSERT(obj->isUsedAsPrototype());

  // Lookups on integer ids cannot be cached through prototypes.
  if (id.isInt()) {
    return true;
  }

  bool useDictionaryTeleporting =
      cx->zone()->shapeZone().useDictionaryModeTeleportation();

  RootedObject proto(cx, obj->staticPrototype());
  while (proto) {
    // Lookups will not be cached through non-native protos.
    if (!proto->is<NativeObject>()) {
      break;
    }

    Handle<NativeObject*> nproto = proto.as<NativeObject>();

    if (mozilla::Maybe<PropertyInfo> propInfo = nproto->lookup(cx, id)) {
      if (proto->hasObjectFuse()) {
        MOZ_ASSERT(ObjectFuse::tracksPropertyKey(id));
        if (auto* objFuse = cx->zone()->objectFuses.get(nproto)) {
          objFuse->handleTeleportingShadowedProperty(cx, *propInfo);
        }
      }
      if (useDictionaryTeleporting) {
        JS_LOG(teleporting, Debug,
               "Shadowed Prop: Dictionary Reshape for Teleporting");

        return JSObject::reshapeForTeleporting(cx, proto);
      }

      JS_LOG(teleporting, Info,
             "Shadowed Prop: Invalidating Reshape for Teleporting");
      return JSObject::setInvalidatedTeleporting(cx, proto);
    }

    proto = proto->staticPrototype();
  }

  return true;
}

static void InvalidateMegamorphicCache(JSContext* cx, Handle<NativeObject*> obj,
                                       bool invalidateGetPropCache = true) {
  // The megamorphic cache only checks the receiver object's shape. We need to
  // invalidate the cache when a prototype object changes its set of properties,
  // to account for cached properties that are deleted, turned into an accessor
  // property, or shadowed by another object on the proto chain.

  MOZ_ASSERT(obj->isUsedAsPrototype());

  if (invalidateGetPropCache) {
    cx->caches().megamorphicCache.bumpGeneration();
  }
  cx->caches().megamorphicSetPropCache->bumpGeneration();
}

void MaybePopReturnFuses(JSContext* cx, Handle<NativeObject*> nobj) {
  GlobalObject* global = &nobj->global();
  JSObject* objectProto = &global->getObjectPrototype();
  if (nobj == objectProto) {
    nobj->realm()->realmFuses.objectPrototypeHasNoReturnProperty.popFuse(
        cx, nobj->realm()->realmFuses);
    return;
  }

  JSObject* iteratorProto = global->maybeGetIteratorPrototype();
  if (nobj == iteratorProto) {
    nobj->realm()->realmFuses.iteratorPrototypeHasNoReturnProperty.popFuse(
        cx, nobj->realm()->realmFuses);
    return;
  }

  JSObject* arrayIterProto = global->maybeGetArrayIteratorPrototype();
  if (nobj == arrayIterProto) {
    nobj->realm()->realmFuses.arrayIteratorPrototypeHasNoReturnProperty.popFuse(
        cx, nobj->realm()->realmFuses);
    return;
  }
}

void MaybePopTypedArrayConstructorSpeciesFuses(JSContext* cx,
                                               NativeObject* nobj) {
  MOZ_ASSERT(nobj->hasRealmFuseProperty());

  if (IsTypedArrayConstructor(nobj)) {
    nobj->realm()->realmFuses.optimizeTypedArraySpeciesFuse.popFuse(
        cx, nobj->realm()->realmFuses);
  }
}

// static
bool Watchtower::watchPropertyAddSlow(JSContext* cx, Handle<NativeObject*> obj,
                                      HandleId id) {
  MOZ_ASSERT(watchesPropertyAdd(obj));

  if (obj->isUsedAsPrototype()) {
    if (!ReshapeForShadowedProp(cx, obj, id)) {
      return false;
    }
    if (!id.isInt()) {
      InvalidateMegamorphicCache(cx, obj);
    }

    if (id == NameToId(cx->names().return_)) {
      MaybePopReturnFuses(cx, obj);
    }
  }
  if (MOZ_UNLIKELY(obj->hasRealmFuseProperty())) {
    if (id.isWellKnownSymbol(JS::SymbolCode::species)) {
      MaybePopTypedArrayConstructorSpeciesFuses(cx, obj);
    }
  }

  if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
    RootedValue val(cx, IdToValue(id));
    if (!AddToWatchtowerLog(cx, "add-prop", obj, val)) {
      return false;
    }
  }

  return true;
}

static bool ReshapeForProtoMutation(JSContext* cx, Handle<NativeObject*> obj) {
  // To avoid the JIT guarding on each prototype in the proto chain to detect
  // prototype mutation, we can instead reshape the rest of the proto chain such
  // that a guard on any of them is sufficient. To avoid excessive reshaping and
  // invalidation, we apply heuristics to decide when to apply this and when
  // to require a guard.
  //
  // There are two cases:
  //
  // (1) The object is not marked IsUsedAsPrototype. This is the common case.
  //     Because shape implies proto, we rely on the caller changing the
  //     object's shape. The JIT guards on this object's shape or prototype so
  //     there's nothing we have to do here for objects on the proto chain.
  //
  // (2) The object is marked IsUsedAsPrototype. This implies the object may be
  //     participating in shape teleporting. To invalidate JIT ICs depending on
  //     the proto chain being unchanged, set the InvalidatedTeleporting shape
  //     flag for this object and objects on its proto chain.
  //
  //     This flag disables future shape teleporting attempts, so next time this
  //     happens the loop below will be a no-op.
  //
  // NOTE: We only handle NativeObjects and don't propagate reshapes through
  //       any non-native objects on the chain.
  //
  // See Also:
  //  - GeneratePrototypeGuards
  //  - GeneratePrototypeHoleGuards

  MOZ_ASSERT(obj->isUsedAsPrototype());

  RootedObject pobj(cx, obj);

  bool useDictionaryTeleporting =
      cx->zone()->shapeZone().useDictionaryModeTeleportation();

  while (pobj && pobj->is<NativeObject>()) {
    if (pobj->hasObjectFuse()) {
      if (auto* objFuse =
              cx->zone()->objectFuses.get(pobj.as<NativeObject>())) {
        objFuse->handleTeleportingProtoMutation(cx);
      }
    }
    if (useDictionaryTeleporting) {
      MOZ_ASSERT(!pobj->hasInvalidatedTeleporting(),
                 "Once we start using invalidation shouldn't do any more "
                 "dictionary mode teleportation");
      JS_LOG(teleporting, Debug,
             "Proto Mutation: Dictionary Reshape for Teleporting");

      if (!JSObject::reshapeForTeleporting(cx, pobj)) {
        return false;
      }
    } else if (!pobj->hasInvalidatedTeleporting()) {
      JS_LOG(teleporting, Info,
             "Proto Mutation: Invalidating Reshape for Teleporting");

      if (!JSObject::setInvalidatedTeleporting(cx, pobj)) {
        return false;
      }
    }
    pobj = pobj->staticPrototype();
  }

  return true;
}

static constexpr bool IsTypedArrayProtoKey(JSProtoKey protoKey) {
  switch (protoKey) {
#define PROTO_KEY(_, T, N) \
  case JSProto_##N##Array: \
    return true;
    JS_FOR_EACH_TYPED_ARRAY(PROTO_KEY)
#undef PROTO_KEY
    default:
      return false;
  }
}

static_assert(
    !IsTypedArrayProtoKey(JSProto_TypedArray),
    "IsTypedArrayProtoKey(JSProto_TypedArray) is expected to return false");

static bool WatchProtoChangeImpl(JSContext* cx, HandleObject obj) {
  if (!obj->is<NativeObject>()) {
    return true;
  }
  auto nobj = obj.as<NativeObject>();

  if (nobj->isUsedAsPrototype()) {
    if (!ReshapeForProtoMutation(cx, nobj)) {
      return false;
    }

    InvalidateMegamorphicCache(cx, nobj);

    if (nobj == nobj->global().maybeGetArrayIteratorPrototype()) {
      nobj->realm()->realmFuses.arrayIteratorPrototypeHasIteratorProto.popFuse(
          cx, nobj->realm()->realmFuses);
    }

    if (nobj == nobj->global().maybeGetIteratorPrototype()) {
      nobj->realm()->realmFuses.iteratorPrototypeHasObjectProto.popFuse(
          cx, nobj->realm()->realmFuses);
    }

    auto protoKey = StandardProtoKeyOrNull(nobj);
    if (IsTypedArrayProtoKey(protoKey) &&
        nobj == nobj->global().maybeGetPrototype(protoKey)) {
      nobj->realm()->realmFuses.optimizeTypedArraySpeciesFuse.popFuse(
          cx, nobj->realm()->realmFuses);
    }
  }

  if (MOZ_UNLIKELY(nobj->hasRealmFuseProperty())) {
    MaybePopTypedArrayConstructorSpeciesFuses(cx, nobj);
  }

  return true;
}

// static
bool Watchtower::watchProtoChangeSlow(JSContext* cx, HandleObject obj) {
  MOZ_ASSERT(watchesProtoChange(obj));

  if (!WatchProtoChangeImpl(cx, obj)) {
    return false;
  }

  if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
    if (!AddToWatchtowerLog(cx, "proto-change", obj,
                            JS::UndefinedHandleValue)) {
      return false;
    }
  }

  return true;
}

static void MaybePopArrayConstructorFuses(JSContext* cx, NativeObject* obj,
                                          jsid id) {
  if (obj != obj->global().maybeGetConstructor(JSProto_Array)) {
    return;
  }
  if (id.isWellKnownSymbol(JS::SymbolCode::species)) {
    obj->realm()->realmFuses.optimizeArraySpeciesFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopArrayPrototypeFuses(JSContext* cx, NativeObject* obj,
                                        jsid id) {
  if (obj != obj->global().maybeGetArrayPrototype()) {
    return;
  }
  if (id.isWellKnownSymbol(JS::SymbolCode::iterator)) {
    obj->realm()->realmFuses.arrayPrototypeIteratorFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
  if (id.isAtom(cx->names().constructor)) {
    obj->realm()->realmFuses.optimizeArraySpeciesFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopArrayIteratorPrototypeFuses(JSContext* cx,
                                                NativeObject* obj, jsid id) {
  if (obj != obj->global().maybeGetArrayIteratorPrototype()) {
    return;
  }
  if (id.isAtom(cx->names().next)) {
    obj->realm()->realmFuses.arrayPrototypeIteratorNextFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopMapPrototypeFuses(JSContext* cx, NativeObject* obj,
                                      jsid id) {
  if (obj != obj->global().maybeGetPrototype(JSProto_Map)) {
    return;
  }
  if (id.isWellKnownSymbol(JS::SymbolCode::iterator)) {
    obj->realm()->realmFuses.optimizeMapObjectIteratorFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
  if (id.isAtom(cx->names().set)) {
    obj->realm()->realmFuses.optimizeMapPrototypeSetFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopMapIteratorPrototypeFuses(JSContext* cx, NativeObject* obj,
                                              jsid id) {
  if (obj != obj->global().maybeBuiltinProto(
                 GlobalObject::ProtoKind::MapIteratorProto)) {
    return;
  }
  if (id.isAtom(cx->names().next)) {
    obj->realm()->realmFuses.optimizeMapObjectIteratorFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopSetPrototypeFuses(JSContext* cx, NativeObject* obj,
                                      jsid id) {
  if (obj != obj->global().maybeGetPrototype(JSProto_Set)) {
    return;
  }
  if (id.isWellKnownSymbol(JS::SymbolCode::iterator)) {
    obj->realm()->realmFuses.optimizeSetObjectIteratorFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
  if (id.isAtom(cx->names().add)) {
    obj->realm()->realmFuses.optimizeSetPrototypeAddFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopSetIteratorPrototypeFuses(JSContext* cx, NativeObject* obj,
                                              jsid id) {
  if (obj != obj->global().maybeBuiltinProto(
                 GlobalObject::ProtoKind::SetIteratorProto)) {
    return;
  }
  if (id.isAtom(cx->names().next)) {
    obj->realm()->realmFuses.optimizeSetObjectIteratorFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopWeakMapPrototypeFuses(JSContext* cx, NativeObject* obj,
                                          jsid id) {
  if (obj != obj->global().maybeGetPrototype(JSProto_WeakMap)) {
    return;
  }
  if (id.isAtom(cx->names().set)) {
    obj->realm()->realmFuses.optimizeWeakMapPrototypeSetFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopWeakSetPrototypeFuses(JSContext* cx, NativeObject* obj,
                                          jsid id) {
  if (obj != obj->global().maybeGetPrototype(JSProto_WeakSet)) {
    return;
  }
  if (id.isAtom(cx->names().add)) {
    obj->realm()->realmFuses.optimizeWeakSetPrototypeAddFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopPromiseConstructorFuses(JSContext* cx, NativeObject* obj,
                                            jsid id) {
  if (obj != obj->global().maybeGetConstructor(JSProto_Promise)) {
    return;
  }
  if (id.isWellKnownSymbol(JS::SymbolCode::species) ||
      id.isAtom(cx->names().resolve)) {
    obj->realm()->realmFuses.optimizePromiseLookupFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopPromisePrototypeFuses(JSContext* cx, NativeObject* obj,
                                          jsid id) {
  if (obj != obj->global().maybeGetPrototype(JSProto_Promise)) {
    return;
  }
  if (id.isAtom(cx->names().constructor) || id.isAtom(cx->names().then)) {
    obj->realm()->realmFuses.optimizePromiseLookupFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopRegExpPrototypeFuses(JSContext* cx, NativeObject* obj,
                                         jsid id) {
  if (obj != obj->global().maybeGetPrototype(JSProto_RegExp)) {
    return;
  }
  if (id.isAtom(cx->names().flags) || id.isAtom(cx->names().global) ||
      id.isAtom(cx->names().hasIndices) || id.isAtom(cx->names().ignoreCase) ||
      id.isAtom(cx->names().multiline) || id.isAtom(cx->names().sticky) ||
      id.isAtom(cx->names().unicode) || id.isAtom(cx->names().unicodeSets) ||
      id.isAtom(cx->names().dotAll) || id.isAtom(cx->names().exec) ||
      id.isWellKnownSymbol(JS::SymbolCode::match) ||
      id.isWellKnownSymbol(JS::SymbolCode::matchAll) ||
      id.isWellKnownSymbol(JS::SymbolCode::replace) ||
      id.isWellKnownSymbol(JS::SymbolCode::search) ||
      id.isWellKnownSymbol(JS::SymbolCode::split)) {
    obj->realm()->realmFuses.optimizeRegExpPrototypeFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopArrayBufferConstructorFuses(JSContext* cx,
                                                NativeObject* obj, jsid id) {
  if (obj != obj->global().maybeGetConstructor(JSProto_ArrayBuffer)) {
    return;
  }
  if (id.isWellKnownSymbol(JS::SymbolCode::species)) {
    obj->realm()->realmFuses.optimizeArrayBufferSpeciesFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopArrayBufferPrototypeFuses(JSContext* cx, NativeObject* obj,
                                              jsid id) {
  if (obj != obj->global().maybeGetPrototype(JSProto_ArrayBuffer)) {
    return;
  }
  if (id.isAtom(cx->names().constructor)) {
    obj->realm()->realmFuses.optimizeArrayBufferSpeciesFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopSharedArrayBufferConstructorFuses(JSContext* cx,
                                                      NativeObject* obj,
                                                      jsid id) {
  if (obj != obj->global().maybeGetConstructor(JSProto_SharedArrayBuffer)) {
    return;
  }
  if (id.isWellKnownSymbol(JS::SymbolCode::species)) {
    obj->realm()->realmFuses.optimizeSharedArrayBufferSpeciesFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopSharedArrayBufferPrototypeFuses(JSContext* cx,
                                                    NativeObject* obj,
                                                    jsid id) {
  if (obj != obj->global().maybeGetPrototype(JSProto_SharedArrayBuffer)) {
    return;
  }
  if (id.isAtom(cx->names().constructor)) {
    obj->realm()->realmFuses.optimizeSharedArrayBufferSpeciesFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopTypedArrayConstructorFuses(JSContext* cx, NativeObject* obj,
                                               jsid id) {
  if (obj != obj->global().maybeGetConstructor(JSProto_TypedArray)) {
    return;
  }
  if (id.isWellKnownSymbol(JS::SymbolCode::species)) {
    obj->realm()->realmFuses.optimizeTypedArraySpeciesFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopTypedArrayPrototypeFuses(JSContext* cx, NativeObject* obj,
                                             jsid id) {
  auto protoKey = StandardProtoKeyOrNull(obj);
  if (protoKey != JSProto_TypedArray && !IsTypedArrayProtoKey(protoKey)) {
    return;
  }
  if (obj != obj->global().maybeGetPrototype(protoKey)) {
    return;
  }
  if (id.isAtom(cx->names().constructor)) {
    obj->realm()->realmFuses.optimizeTypedArraySpeciesFuse.popFuse(
        cx, obj->realm()->realmFuses);
  }
}

static void MaybePopRealmFuses(JSContext* cx, NativeObject* obj, jsid id) {
  // Handle writes to Array constructor fuse properties.
  MaybePopArrayConstructorFuses(cx, obj, id);

  // Handle writes to Array.prototype fuse properties.
  MaybePopArrayPrototypeFuses(cx, obj, id);

  // Handle writes to %ArrayIteratorPrototype% fuse properties.
  MaybePopArrayIteratorPrototypeFuses(cx, obj, id);

  // Handle writes to Map.prototype fuse properties.
  MaybePopMapPrototypeFuses(cx, obj, id);

  // Handle writes to %MapIteratorPrototype% fuse properties.
  MaybePopMapIteratorPrototypeFuses(cx, obj, id);

  // Handle writes to Set.prototype fuse properties.
  MaybePopSetPrototypeFuses(cx, obj, id);

  // Handle writes to %SetIteratorPrototype% fuse properties.
  MaybePopSetIteratorPrototypeFuses(cx, obj, id);

  // Handle writes to WeakMap.prototype fuse properties.
  MaybePopWeakMapPrototypeFuses(cx, obj, id);

  // Handle writes to WeakSet.prototype fuse properties.
  MaybePopWeakSetPrototypeFuses(cx, obj, id);

  // Handle writes to Promise constructor fuse properties.
  MaybePopPromiseConstructorFuses(cx, obj, id);

  // Handle writes to Promise.prototype fuse properties.
  MaybePopPromisePrototypeFuses(cx, obj, id);

  // Handle writes to RegExp.prototype fuse properties.
  MaybePopRegExpPrototypeFuses(cx, obj, id);

  // Handle writes to ArrayBuffer constructor fuse properties.
  MaybePopArrayBufferConstructorFuses(cx, obj, id);

  // Handle writes to ArrayBuffer.prototype fuse properties.
  MaybePopArrayBufferPrototypeFuses(cx, obj, id);

  // Handle writes to SharedArrayBuffer constructor fuse properties.
  MaybePopSharedArrayBufferConstructorFuses(cx, obj, id);

  // Handle writes to SharedArrayBuffer.prototype fuse properties.
  MaybePopSharedArrayBufferPrototypeFuses(cx, obj, id);

  // Handle writes to %TypedArray% constructor fuse properties.
  MaybePopTypedArrayConstructorFuses(cx, obj, id);

  // Handle writes to %TypedArray%.prototype and concrete TypedArray.prototype
  // fuse properties.
  MaybePopTypedArrayPrototypeFuses(cx, obj, id);
}

// static
bool Watchtower::watchPropertyRemoveSlow(JSContext* cx,
                                         Handle<NativeObject*> obj, HandleId id,
                                         PropertyInfo propInfo,
                                         bool* wasTrackedObjectFuseProp) {
  MOZ_ASSERT(watchesPropertyRemove(obj));

  if (obj->isUsedAsPrototype() && !id.isInt()) {
    InvalidateMegamorphicCache(cx, obj);
  }

  if (obj->isGenerationCountedGlobal()) {
    obj->as<GlobalObject>().bumpGenerationCount();
  }

  if (MOZ_UNLIKELY(obj->hasRealmFuseProperty())) {
    MaybePopRealmFuses(cx, obj, id);
  }
  if (obj->hasObjectFuse() && ObjectFuse::tracksPropertyKey(id)) {
    if (auto* objFuse = cx->zone()->objectFuses.get(obj)) {
      objFuse->handlePropertyRemove(cx, propInfo, wasTrackedObjectFuseProp);
    }
  }

  if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
    RootedValue val(cx, IdToValue(id));
    if (!AddToWatchtowerLog(cx, "remove-prop", obj, val)) {
      return false;
    }
  }

  return true;
}

// static
bool Watchtower::watchPropertyFlagsChangeSlow(JSContext* cx,
                                              Handle<NativeObject*> obj,
                                              HandleId id,
                                              PropertyInfo propInfo,
                                              PropertyFlags newFlags) {
  MOZ_ASSERT(watchesPropertyFlagsChange(obj));
  MOZ_ASSERT(obj->lookupPure(id).ref() == propInfo);
  MOZ_ASSERT(propInfo.flags() != newFlags);

  if (obj->isUsedAsPrototype() && !id.isInt()) {
    InvalidateMegamorphicCache(cx, obj);
  }

  if (obj->isGenerationCountedGlobal()) {
    // The global generation counter only cares whether a property
    // changes from data property to accessor or vice-versa. Changing
    // the flags on a property doesn't matter.
    bool wasAccessor = propInfo.isAccessorProperty();
    bool isAccessor = newFlags.isAccessorProperty();
    if (wasAccessor != isAccessor) {
      obj->as<GlobalObject>().bumpGenerationCount();
    }
  }

  if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
    RootedValue val(cx, IdToValue(id));
    if (!AddToWatchtowerLog(cx, "change-prop-flags", obj, val)) {
      return false;
    }
  }

  return true;
}

// static
template <AllowGC allowGC>
void Watchtower::watchPropertyValueChangeSlow(
    JSContext* cx, typename MaybeRooted<NativeObject*, allowGC>::HandleType obj,
    typename MaybeRooted<PropertyKey, allowGC>::HandleType id,
    typename MaybeRooted<Value, allowGC>::HandleType value,
    PropertyInfo propInfo) {
  MOZ_ASSERT(watchesPropertyValueChange(obj));

  // Note: this is also called when changing the GetterSetter value of an
  // accessor property or when redefining a data property as an accessor
  // property and vice versa.

  // This is a no-op for indexed properties (sparse elements).
  if (id.isInt()) {
    return;
  }

  // Handle object fuses before the check for no-op changes below. We don't
  // attach SetProp stubs for constant properties, so if a constant property is
  // overwritten with the same value, we want to mark it non-constant.
  // See Watchtower::canOptimizeSetSlotSlow.
  if (obj->hasObjectFuse()) {
    MOZ_ASSERT(ObjectFuse::tracksPropertyKey(id));
    if (auto* objFuse = cx->zone()->objectFuses.get(obj)) {
      objFuse->handlePropertyValueChange(cx, propInfo);
    }
  }

  if (propInfo.hasSlot() && obj->getSlot(propInfo.slot()) == value) {
    // We're not actually changing the property's value.
    return;
  }

  if (MOZ_UNLIKELY(obj->hasRealmFuseProperty())) {
    MaybePopRealmFuses(cx, obj, id);
  }

  // If we cannot GC, we can't manipulate the log, but we need to be able to
  // call this in places we cannot GC.
  if constexpr (allowGC == AllowGC::CanGC) {
    if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
      RootedValue val(cx, IdToValue(id));
      if (!AddToWatchtowerLog(cx, "change-prop-value", obj, val)) {
        // Ignore OOM because this is just a testing feature and infallible
        // watchPropertyValueChange simplifies the callers.
        cx->clearPendingException();
      }
    }
  }
}

template void Watchtower::watchPropertyValueChangeSlow<AllowGC::CanGC>(
    JSContext* cx,
    typename MaybeRooted<NativeObject*, AllowGC::CanGC>::HandleType obj,
    typename MaybeRooted<PropertyKey, AllowGC::CanGC>::HandleType id,
    typename MaybeRooted<Value, AllowGC::CanGC>::HandleType value,
    PropertyInfo propInfo);
template void Watchtower::watchPropertyValueChangeSlow<AllowGC::NoGC>(
    JSContext* cx,
    typename MaybeRooted<NativeObject*, AllowGC::NoGC>::HandleType obj,
    typename MaybeRooted<PropertyKey, AllowGC::NoGC>::HandleType id,
    typename MaybeRooted<Value, AllowGC::NoGC>::HandleType value,
    PropertyInfo propInfo);

// static
SetSlotOptimizable Watchtower::canOptimizeSetSlotSlow(JSContext* cx,
                                                      NativeObject* obj,
                                                      PropertyKey key,
                                                      PropertyInfo prop) {
  MOZ_ASSERT(obj->hasObjectFuse());
  MOZ_ASSERT(ObjectFuse::tracksPropertyKey(key));

  ObjectFuse* objFuse = cx->zone()->objectFuses.getOrCreate(cx, obj);
  if (!objFuse) {
    cx->recoverFromOutOfMemory();
    return SetSlotOptimizable::No;
  }

  if (objFuse->canOptimizeSetSlot(prop)) {
    return SetSlotOptimizable::Yes;
  }

  // If a property is constant, there's no point in attaching a SetProp IC stub.
  // The next time we set this property, we have to call into the VM to mark
  // it NotConstant and potentially pop fuses. After that, we can attach a
  // regular SetProp IC stub. If we never set this property again, there's no
  // need to optimize this SetProp.
  return SetSlotOptimizable::NotYet;
}

// static
bool Watchtower::watchFreezeOrSealSlow(JSContext* cx, Handle<NativeObject*> obj,
                                       IntegrityLevel level) {
  MOZ_ASSERT(watchesFreezeOrSeal(obj));

  // Invalidate the megamorphic set-property cache when freezing a prototype
  // object. Non-writable prototype properties can't be shadowed (through
  // SetProp) so this affects the behavior of add-property cache entries.
  if (level == IntegrityLevel::Frozen && obj->isUsedAsPrototype()) {
    InvalidateMegamorphicCache(cx, obj, /* invalidateGetPropCache = */ false);
  }

  if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
    if (!AddToWatchtowerLog(cx, "freeze-or-seal", obj,
                            JS::UndefinedHandleValue)) {
      return false;
    }
  }

  return true;
}
