// Copyright 2025 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef V8_MAGLEV_MAGLEV_KNOWN_NODE_ASPECTS_H_
#define V8_MAGLEV_MAGLEV_KNOWN_NODE_ASPECTS_H_

#include <iosfwd>
#include <utility>

#include "src/base/logging.h"
#include "src/common/globals.h"
#include "src/maglev/maglev-ir.h"
#include "src/maglev/maglev-node-type.h"
#include "src/objects/contexts.h"

namespace v8 {
namespace internal {
namespace maglev {

class Graph;
struct LoopEffects;
class KnownNodeAspects;
class TraceLogger;

using PossibleMaps = compiler::ZoneRefSet<Map>;

#ifdef DEBUG
bool IsStringRootIndex(RootIndex index);

inline bool IsInternalizedStringConstant(ValueNode* node) {
  if (auto constant = node->TryCast<HeapConstant>()) {
    return constant->ref().IsInternalizedString();
  }
  if (auto root_constant = node->TryCast<RootConstant>()) {
    return IsStringRootIndex(root_constant->index());
  }
  return false;
}

inline bool IsNonInternalizedStringConstant(ValueNode* node) {
  if (auto constant = node->TryCast<HeapConstant>()) {
    return constant->ref().IsString() &&
           !constant->ref().IsInternalizedString();
  }
  // We don't need to check RootConstants because they are always internalized
  // strings.
  return false;
}
#endif  // DEBUG

class NodeInfo {
 public:
  NodeInfo() = default;

  struct ClearUnstableMapsOnCopy {
    const NodeInfo& val;
  };
  explicit NodeInfo(ClearUnstableMapsOnCopy other) V8_NOEXCEPT {
    type_ = other.val.type_;
    alternative_ = other.val.alternative_;
    if (other.val.possible_maps_are_known_) {
      possible_maps_ = other.val.possible_maps_;
      possible_maps_are_known_ = true;
      any_map_or_node_type_is_unstable_ =
          other.val.any_map_or_node_type_is_unstable_;
      OnSideEffect();
    }
  }

  NodeType type() const { return type_; }
  NodeType IntersectType(NodeType other) {
    return type_ = maglev::IntersectType(type_, other);
  }
  NodeType UnionType(NodeType other) {
    return type_ = maglev::UnionType(type_, other);
  }

  // Optional alternative nodes with the equivalent value but a different
  // representation.
  class AlternativeNodes {
   public:
    AlternativeNodes() { store_.fill(nullptr); }

#define ALTERNATIVES(V)                                       \
  V(tagged, Tagged, Tagged)                                   \
  V(int32, Int32, Int32)                                      \
  V(truncated_int32_to_number, TruncatedInt32ToNumber, Int32) \
  V(float64, Float64, Float64)                                \
  V(holey_float64, HoleyFloat64, HoleyFloat64)                \
  V(checked_value, CheckedValue, Tagged)

    enum Kind {
#define KIND(name, Name, repr) k##Name,
      ALTERNATIVES(KIND)
#undef KIND
          kNumberOfAlternatives
    };

    static void DCheckAlternativeOverwrite(Kind kind, ValueNode* old_alt,
                                           ValueNode* new_alt) {
#ifdef DEBUG
      if (old_alt == nullptr) return;

      if (kind == Kind::kCheckedValue) {
        // For checked_value, we only allow:
        // 1. Overwriting a non-internalized constant by
        // CheckedInternalizedString.
        // 2. Overwriting CheckedInternalizedString by an internalized string
        // constant.
        if (IsNonInternalizedStringConstant(old_alt)) {
          DCHECK_EQ(new_alt->opcode(), Opcode::kCheckedInternalizedString);
        } else if (old_alt->opcode() == Opcode::kCheckedInternalizedString) {
          DCHECK(IsInternalizedStringConstant(new_alt));
        } else {
          // Any other overwrite of checked_value is forbidden.
          DCHECK(false);
        }
      } else {
        // For other alternatives, the old one shouldn't be a constant and the
        // new one should be.
        DCHECK(!IsConstantNode(old_alt->opcode()));
        DCHECK(IsConstantNode(new_alt->opcode()));
      }
#endif
    }

#define API(name, Name, repr)                                             \
  ValueNode* name() const {                                               \
    if (!store_[Kind::k##Name]) return nullptr;                           \
    return store_[Kind::k##Name]->UnwrapIdentities();                     \
  }                                                                       \
  ValueNode* set_##name(ValueNode* val) {                                 \
    DCHECK_EQ(val->value_representation(), ValueRepresentation::k##repr); \
    DCheckAlternativeOverwrite(Kind::k##Name, name(), val);               \
    return store_[Kind::k##Name] = val;                                   \
  }                                                                       \
  template <typename Function>                                            \
  ValueNode* get_or_set_##name(Function create) {                         \
    ValueNode* existing_alt = name();                                     \
    if (existing_alt != nullptr) return existing_alt;                     \
    ValueNode* new_alt = create();                                        \
    if (new_alt) {                                                        \
      return set_##name(new_alt);                                         \
    }                                                                     \
    return nullptr;                                                       \
  }
    ALTERNATIVES(API)
#undef API
#undef ALTERNATIVES

    ValueNode* get(UseRepresentation repr) {
      switch (repr) {
        case UseRepresentation::kTagged:
        case UseRepresentation::kTaggedForNumberToString:
          return tagged();
        case UseRepresentation::kInt32:
          return int32();
        case UseRepresentation::kTruncatedInt32:
          return truncated_int32_to_number();
        case UseRepresentation::kFloat64:
          return float64();
        case UseRepresentation::kHoleyFloat64:
          return holey_float64();
        case UseRepresentation::kUint32:
          UNREACHABLE();
      }
      UNREACHABLE();
    }

    bool has_none() const { return store_ == AlternativeNodes().store_; }

    void MergeWith(const AlternativeNodes& other) {
      for (size_t i = 0; i < Kind::kNumberOfAlternatives; ++i) {
        if (store_[i] && store_[i] != other.store_[i]) {
          store_[i] = nullptr;
        }
      }
    }

   private:
    // TODO(leszeks): At least one of these is redundant for every node,
    // consider a more compressed form or even linked list.
    std::array<ValueNode*, Kind::kNumberOfAlternatives> store_;

    // Prevent callers from copying these when they try to update the
    // alternatives by making these private.
    AlternativeNodes(const AlternativeNodes&) V8_NOEXCEPT = default;
    AlternativeNodes& operator=(const AlternativeNodes&) V8_NOEXCEPT = default;
    friend class NodeInfo;
  };

  const AlternativeNodes& alternative() const { return alternative_; }
  AlternativeNodes& alternative() { return alternative_; }

  bool no_info_available() const {
    return type_ == NodeType::kUnknown && alternative_.has_none() &&
           !possible_maps_are_known_;
  }

  bool is_smi() const { return NodeTypeIsSmi(type_); }
  bool is_any_heap_object() const { return NodeTypeIsAnyHeapObject(type_); }
  bool is_string() const { return NodeTypeIsString(type_); }
  bool is_internalized_string() const {
    return NodeTypeIsInternalizedString(type_);
  }
  bool is_symbol() const { return NodeTypeIsSymbol(type_); }

  // Mutate this node info by merging in another node info, with the result
  // being a node info that is the subset of information valid in both inputs.
  void MergeWith(const NodeInfo& other, Zone* zone,
                 bool& side_effects_require_invalidation) {
    UnionType(other.type_);
    alternative_.MergeWith(other.alternative_);
    if (possible_maps_are_known_) {
      if (other.possible_maps_are_known_) {
        // Map sets are the set of _possible_ maps, so on a merge we need to
        // _union_ them together (i.e. intersect the set of impossible maps).
        possible_maps_.Union(other.possible_maps_, zone);
        // If either path is stale, the result is stale.
        maps_are_stale_ = maps_are_stale_ || other.maps_are_stale_;
      } else {
        possible_maps_.clear();
        possible_maps_are_known_ = false;
        maps_are_stale_ = false;
      }
    }

    any_map_or_node_type_is_unstable_ =
        possible_maps_are_known_ && (any_map_or_node_type_is_unstable_ ||
                                     other.any_map_or_node_type_is_unstable_);

    if (NodeTypeIsUnstable(type_) ||
        (any_map_or_node_type_is_unstable_ && !maps_are_stale_)) {
      side_effects_require_invalidation = true;
    }
  }

  bool possible_maps_are_unstable() const {
    return any_map_or_node_type_is_unstable_;
  }

  void OnSideEffect() {
    if (any_map_or_node_type_is_unstable_) maps_are_stale_ = true;
    type_ = MakeTypeStable(type_);
  }

  template <typename Function>
  bool MarkMapsStaleIfAny(const Function& condition) {
    if (!any_map_or_node_type_is_unstable_) return false;
    for (auto map : possible_maps_) {
      if (condition(map)) {
        maps_are_stale_ = true;
        return true;
      }
    }
    return false;
  }

  bool possible_maps_are_known() const { return possible_maps_are_known_; }

  const PossibleMaps& possible_maps() const {
    // If !possible_maps_are_known_ then every map is possible and using the
    // (probably empty) possible_maps_ set is definitely wrong.
    CHECK(possible_maps_are_known_);
    return possible_maps_;
  }

  static void TraceSetPossibleMaps(const KnownNodeAspects& known_node_aspects,
                                   const NodeInfo* node_info,
                                   const PossibleMaps& possible_maps);

  void SetPossibleMaps(const PossibleMaps& possible_maps,
                       bool any_map_or_node_type_is_unstable,
                       NodeType possible_type, compiler::JSHeapBroker* broker,
                       const KnownNodeAspects& known_node_aspects) {
    if (V8_UNLIKELY(v8_flags.trace_maglev_kna)) {
      TraceSetPossibleMaps(known_node_aspects, this, possible_maps);
    }

    possible_maps_ = possible_maps;
    possible_maps_are_known_ = true;
    any_map_or_node_type_is_unstable_ = any_map_or_node_type_is_unstable;
    maps_are_stale_ = false;
#ifdef DEBUG
    if (possible_maps.size()) {
      NodeType expected = StaticTypeForMap(*possible_maps.begin(), broker);
      for (auto map : possible_maps) {
        expected = maglev::UnionType(StaticTypeForMap(map, broker), expected);
      }
      // Ensure the claimed type is not narrower than what can be learned from
      // the map checks.
      DCHECK(NodeTypeIs(expected, possible_type));
    } else {
      DCHECK_EQ(possible_type, NodeType::kUnknown);
    }
#endif
    IntersectType(possible_type);
  }

  // "Stale" means that 1. we've seen unstable maps, and 2. a side effect may
  // have occurred in the meantime.
  void MarkFresh() { maps_are_stale_ = false; }
  bool maps_are_stale() const { return maps_are_stale_; }

  // Objects with "unstable" maps may transition and thus need protection with
  // map checks. (Objects with "stable" maps invalidate compilation
  // dependencies when transitioning.)
  // Note this may also be true if a NodeType is unstable, see below.
  bool any_map_or_node_type_is_unstable() const {
    return any_map_or_node_type_is_unstable_;
  }

  // Unstable NodeTypes: this is a different concept than unstable maps. These
  // are simply NodeTypes that may change. Currently only String types can do
  // so.
  void set_node_type_is_unstable() {
    // Reuse any_map_or_node_type_is_unstable to signal that the node type is
    // unstable.
    any_map_or_node_type_is_unstable_ = true;
  }

 private:
  NodeType type_ = NodeType::kUnknown;

  bool any_map_or_node_type_is_unstable_ = false;
  bool maps_are_stale_ = false;

  // Maps for a node. Sets of maps that only contain stable maps are valid
  // across side-effecting calls, as long as we install a dependency, otherwise
  // they are cleared on side-effects.
  // TODO(v8:7700): Investigate a better data structure to use than ZoneMap.
  bool possible_maps_are_known_ = false;
  PossibleMaps possible_maps_;

  AlternativeNodes alternative_;
};

class KnownNodeAspects {
 public:
  // Permanently valid if checked in a dominator.
  using NodeInfos = ZoneMap<ValueNode*, NodeInfo>;

  // Copy constructor is defaulted but private so that we explicitly call the
  // Clone method.
  KnownNodeAspects& operator=(const KnownNodeAspects& other) = delete;
  KnownNodeAspects(KnownNodeAspects&& other) = delete;
  KnownNodeAspects& operator=(KnownNodeAspects&& other) = delete;

  KnownNodeAspects* Clone(Zone* zone) const {
    return zone->New<KnownNodeAspects>(*this);
  }

  // Loop headers can safely clone the node types, since those won't be
  // invalidated in the loop body, and similarly stable maps will have
  // dependencies installed. Unstable maps however might be invalidated by
  // calls, and we don't know about these until it's too late.
  KnownNodeAspects* CloneForLoopHeader(bool optimistic_initial_state,
                                       LoopEffects* loop_effects,
                                       Zone* zone) const;

  void ClearUnstableNodeAspectsForStoreMap(StoreMap* node,
                                           bool is_tracing_enabled);
  void ClearUnstableNodeAspects(bool is_tracing_enabled);

  void OnSideEffect() {
    // A side effect could change existing objects' maps. For stable maps we
    // know this hasn't happened (because we added a dependency on the maps
    // staying stable and therefore not possible to transition away from), but
    // we can no longer assume that objects with unstable maps still have the
    // same map. Unstable maps can also transition to stable ones, so we have to
    // clear _all_ maps for a node if it had _any_ unstable map.
    if (!side_effects_require_invalidation_) return;
    for (auto& it : node_infos_) {
      it.second.OnSideEffect();
    }
    side_effects_require_invalidation_ = false;
  }

  template <typename Function>
  bool MarkMapsStaleIfAny(const Function& condition) {
    if (!side_effects_require_invalidation_) return false;
    bool any_stale = false;
    for (auto& it : node_infos_) {
      any_stale |= it.second.MarkMapsStaleIfAny(condition);
    }
    return any_stale;
  }

  void ClearAvailableExpressions() { available_expressions_.clear(); }

  void ClearAll() {
    loaded_constant_properties_.clear();
    loaded_properties_.clear();
    loaded_context_constants_.clear();
    loaded_context_slots_.clear();
    available_expressions_.clear();
    side_effects_require_invalidation_ = false;
    may_have_aliasing_contexts_ = ContextSlotLoadsAlias::kNone;
    node_infos_.clear();
    virtual_objects_ = {};
  }

  const NodeInfo* TryGetInfoFor(ValueNode* node) const {
    node = node->Unwrap();
    return const_cast<KnownNodeAspects*>(this)->TryGetInfoFor(node);
  }

  NodeInfo* TryGetInfoFor(ValueNode* node) {
    node = node->Unwrap();
    auto info_it = FindInfo(node);
    if (!IsValid(info_it)) return nullptr;
    return &info_it->second;
  }

  // Slow linear lookup, for debug code only.
  ValueNode* FindNode(const NodeInfo* info) const {
    for (const auto& pair : node_infos_) {
      if (&pair.second == info) return pair.first;
    }
    return nullptr;
  }

  NodeInfo* GetOrCreateInfoFor(compiler::JSHeapBroker* broker,
                               ValueNode* node) {
    node = node->Unwrap();
    auto info_it = FindInfo(node);
    if (IsValid(info_it)) return &info_it->second;
    auto res = &node_infos_.emplace(node, NodeInfo()).first->second;
    res->IntersectType(node->GetStaticType(broker));
    return res;
  }

  ValueNode* TryGetAlternativeFor(ValueNode* node, UseRepresentation repr) {
    node = node->Unwrap();
    auto info_it = FindInfo(node);
    if (!IsValid(info_it)) return nullptr;
    return info_it->second.alternative().get(repr);
  }

  std::optional<PossibleMaps> TryGetPossibleMaps(ValueNode* node) {
    DCHECK_NOT_NULL(node);
    if (NodeInfo* info = TryGetInfoFor(node)) {
      if (info->possible_maps_are_known()) {
        return info->possible_maps();
      }
      return {};
    }
    return {};
  }

  bool CheckType(compiler::JSHeapBroker* broker, ValueNode* node, NodeType type,
                 NodeType* current_type = nullptr) {
    NodeType static_type = node->GetStaticType(broker);
    if (current_type) *current_type = static_type;
    if (NodeTypeIs(static_type, type)) return true;
    if (IsEmptyNodeType(IntersectType(static_type, type))) return false;
    const NodeInfo* node_info = TryGetInfoFor(node);
    if (!node_info) return false;
    if (current_type) *current_type = node_info->type();
    return NodeTypeIs(node_info->type(), type);
  }

  NodeType CheckTypes(compiler::JSHeapBroker* broker, ValueNode* node,
                      std::initializer_list<NodeType> types) {
    const NodeInfo* node_info = TryGetInfoFor(node);
    for (NodeType type : types) {
      if (node->StaticTypeIs(broker, type)) return type;
      if (node_info) {
        if (NodeTypeIs(node_info->type(), type)) return type;
      }
    }
    return NodeType::kUnknown;
  }

  bool MayBeNullOrUndefined(compiler::JSHeapBroker* broker, ValueNode* node) {
    NodeType static_type = node->GetStaticType(broker);
    if (!NodeTypeMayBeNullOrUndefined(static_type)) return false;
    const NodeInfo* node_info = TryGetInfoFor(node);
    if (!node_info) return true;
    return NodeTypeMayBeNullOrUndefined(node_info->type());
  }

  bool IsEmptyNodeType(NodeType type) const {
    return v8::internal::maglev::IsEmptyNodeType(type);
  }

  bool IsEmptyNodeType(compiler::JSHeapBroker* broker, ValueNode* node) const {
    // We use GetTypeUnchecked to avoid introducing Traps on None under
    // maglev-assert-types (as the caller of this function is already currently
    // handling the empty node type case).
    return v8::internal::maglev::IsEmptyNodeType(
        GetTypeUnchecked(broker, node));
  }

  bool EnsureType(compiler::JSHeapBroker* broker, ValueNode* node,
                  NodeType type, NodeType* old_type = nullptr) {
    NodeType static_type = node->GetStaticType(broker);
    if (old_type) *old_type = static_type;
    // TODO(428667907): Ideally we should bail out early for the kNone type.
    if (NodeTypeIs(static_type, type, NodeTypeIsVariant::kAllowNone)) {
      return true;
    }
    NodeInfo* known_info = GetOrCreateInfoFor(broker, node);
    if (old_type) *old_type = known_info->type();
    // TODO(428667907): Ideally we should bail out early for the kNone type.
    if (NodeTypeIs(known_info->type(), type, NodeTypeIsVariant::kAllowNone)) {
      return true;
    }
    known_info->IntersectType(type);
    if (auto phi = node->TryCast<Phi>()) {
      known_info->IntersectType(phi->type());
    }
    if (NodeTypeIsUnstable(type)) {
      known_info->set_node_type_is_unstable();
      side_effects_require_invalidation_ = true;
    }
    return false;
  }

  template <typename Function>
  bool EnsureType(compiler::JSHeapBroker* broker, ValueNode* node,
                  NodeType type, Function ensure_new_type) {
    if (node->StaticTypeIs(broker, type)) return true;
    NodeInfo* known_info = GetOrCreateInfoFor(broker, node);
    if (NodeTypeIs(known_info->type(), type)) return true;
    ensure_new_type(known_info->type());
    known_info->IntersectType(type);
    if (NodeTypeIsUnstable(type)) {
      known_info->set_node_type_is_unstable();
      side_effects_require_invalidation_ = true;
    }
    return false;
  }

  void Merge(const KnownNodeAspects& other, Zone* zone);

  // If IsCompatibleWithLoopHeader(other) returns true, it means that
  // Merge(other) would not remove any information from `this`.
  bool IsCompatibleWithLoopHeader(const KnownNodeAspects& other) const;

  // Side effects can require invalidation e.g. if unstable maps or node types
  // (which are two different conceps) have been recorded.
  void MarkSideEffectsRequireInvalidation() {
    side_effects_require_invalidation_ = true;
  }

  VirtualObjectList& virtual_objects() { return virtual_objects_; }
  const VirtualObjectList& virtual_objects() const { return virtual_objects_; }

  // Cached property loads.

  // Maps key->object->value, so that stores to a key can invalidate all loads
  // of that key (in case the objects are aliasing).
  using LoadedPropertyMap =
      ZoneMap<PropertyKey, ZoneMap<ValueNode*, ValueNode*>>;

  using LoadedContextSlotsKey = std::tuple<ValueNode*, int>;
  using LoadedContextSlots = ZoneMap<LoadedContextSlotsKey, ValueNode*>;

  ValueNode* TryFindLoadedProperty(ValueNode* lookup_start_object,
                                   PropertyKey name) {
    return TryFindLoadedProperty(loaded_properties_, lookup_start_object, name);
  }
  ValueNode* TryFindLoadedConstantProperty(ValueNode* lookup_start_object,
                                           PropertyKey name) {
    return TryFindLoadedProperty(loaded_constant_properties_,
                                 lookup_start_object, name);
  }
  ValueNode* TryFindLoadedProperty(ValueNode* lookup_start_object,
                                   PropertyKey name, bool is_const) {
    if (is_const) {
      return TryFindLoadedConstantProperty(lookup_start_object, name);
    }
    return TryFindLoadedProperty(lookup_start_object, name);
  }

  ZoneMap<ValueNode*, ValueNode*>& GetLoadedPropertiesForKey(Zone* zone,
                                                             bool is_const,
                                                             PropertyKey key) {
    LoadedPropertyMap& properties =
        is_const ? loaded_constant_properties_ : loaded_properties_;
    // Try to get loaded_properties[key] if it already exists, otherwise
    // construct loaded_properties[key] = ZoneMap{zone()}.
    return properties.try_emplace(key, zone).first->second;
  }

  bool ClearLoadedPropertiesForKey(PropertyKey key) {
    auto it = loaded_properties_.find(key);
    if (it != loaded_properties_.end()) {
      it->second.clear();
      return true;
    }
    return false;
  }

  void increment_effect_epoch() {
    if (effect_epoch_ < kEffectEpochOverflow) effect_epoch_++;
  }

  template <typename NodeT, typename... Args>
  NodeT* FindExpression(uint32_t hash,
                        std::array<ValueNode*, NodeT::kInputCount>& inputs,
                        Args&&... args) {
    auto it = available_expressions_.find(hash);
    if (it == available_expressions_.end()) return nullptr;

    static constexpr Opcode op = Node::opcode_of<NodeT>;
    auto candidate = it->second.node;

    if (candidate->Is<Identity>()) {
      // This expression was removed from the graph. Do not reuse it.
      available_expressions_.erase(it);
      return nullptr;
    }

    const bool sanity_check =
        candidate->template Is<NodeT>() &&
        static_cast<size_t>(candidate->input_count()) == inputs.size();
    DCHECK_IMPLIES(sanity_check,
                   (StaticPropertiesForOpcode(op) & candidate->properties()) ==
                       candidate->properties());
    const bool epoch_check = !Node::needs_epoch_check(op) ||
                             effect_epoch_ <= it->second.effect_epoch;
    if (sanity_check && epoch_check) {
      if (static_cast<NodeT*>(candidate)->options() ==
          std::forward_as_tuple(std::forward<Args>(args)...)) {
        int i = 0;
        for (const auto& inp : inputs) {
          if (inp != candidate->input(i).node()) {
            break;
          }
          i++;
        }
        if (static_cast<size_t>(i) == inputs.size()) {
          return static_cast<NodeT*>(candidate);
        }
      }
    }
    if (!epoch_check) {
      available_expressions_.erase(it);
    }
    return nullptr;
  }

  template <typename NodeT>
  void AddExpression(uint32_t hash, NodeT* node) {
    static constexpr Opcode op = Node::opcode_of<NodeT>;
    uint32_t epoch = Node::needs_epoch_check(op)
                         ? effect_epoch_
                         : KnownNodeAspects::kEffectEpochForPureInstructions;
    if (epoch == kEffectEpochOverflow) return;
    available_expressions_.emplace(hash, AvailableExpression{node, epoch});
  }

  enum class ContextSlotLoadsAlias : uint8_t {
    kNone,
    kOnlyLoadsRelativeToCurrentContext,
    kOnlyLoadsRelativeToConstant,
    kYes,
  };
  ContextSlotLoadsAlias may_have_aliasing_contexts() const {
    return may_have_aliasing_contexts_;
  }
  static ContextSlotLoadsAlias ContextSlotLoadsAliasMerge(
      ContextSlotLoadsAlias m1, ContextSlotLoadsAlias m2) {
    if (m1 == m2) return m1;
    if (m1 == ContextSlotLoadsAlias::kNone) return m2;
    if (m2 == ContextSlotLoadsAlias::kNone) return m1;
    return ContextSlotLoadsAlias::kYes;
  }
  struct ContextStoreResult {
    enum Type {
      kNone,
      kUpdatedValue,
      kSetNewValue,
    } type;
    SmallZoneVector<LoadedContextSlotsKey, 8> aliased_slots;
  };

  void UpdateMayHaveAliasingContexts(compiler::JSHeapBroker* broker,
                                     LocalIsolate* local_isolate,
                                     ValueNode* context);

  ContextStoreResult RecordContextSlotStore(Graph* graph, ValueNode* context,
                                            int offset, ValueNode* value,
                                            MaybeAssignedFlag assigned);

  // Returns the value in the cache if exists without adding a new cache entry.
  ValueNode* TryGetContextCachedValue(ValueNode* context, int offset,
                                      MaybeAssignedFlag assigned) {
    auto map = (assigned == kMaybeAssigned) ? loaded_context_slots_
                                            : loaded_context_constants_;
    auto it = map.find({context, offset});
    if (it == map.end()) return nullptr;
    if (it->second) {
      it->second = it->second->UnwrapIdentities();
    }
    return it->second;
  }
  // Returns the value in the cache and add a new entry.
  ValueNode*& GetContextCachedValue(ValueNode* context, int offset,
                                    MaybeAssignedFlag assigned) {
    ValueNode*& cached_value =
        (assigned == kMaybeAssigned)
            ? loaded_context_slots_[{context, offset}]
            : loaded_context_constants_[{context, offset}];
    if (cached_value) {
      cached_value = cached_value->UnwrapIdentities();
    }
    return cached_value;
  }
  bool HasContextCacheValue(ValueNode* context, int offset,
                            MaybeAssignedFlag assigned) {
    return (assigned == kMaybeAssigned)
               ? loaded_context_slots_.contains({context, offset})
               : loaded_context_constants_.contains({context, offset});
  }
  bool IsContextCacheEmpty(MaybeAssignedFlag assigned) {
    return (assigned == kMaybeAssigned) ? loaded_context_slots_.empty()
                                        : loaded_context_constants_.empty();
  }

  template <typename NodeT>
  void MarkPossibleSideEffect(NodeT* node, compiler::JSHeapBroker* broker,
                              bool is_tracing_enabled) {
    // Don't do anything for nodes without side effects.
    if constexpr (!NodeT::kProperties.can_write()) return;

    increment_effect_epoch();

    if constexpr (Node::opcode_of<NodeT> == Opcode::kMaybeGrowFastElements) {
      if (ClearLoadedPropertiesForKey(broker->length_string())) {
        if (V8_UNLIKELY(v8_flags.trace_maglev_kna && is_tracing_enabled)) {
          std::cout << "  * Removing non-constant cached \"length\" property";
        }
      }
    }

    if constexpr (IsElementsArrayWrite(Node::opcode_of<NodeT>)) {
      if (ClearLoadedPropertiesForKey(PropertyKey::Elements())) {
        if (V8_UNLIKELY(v8_flags.trace_maglev_kna && is_tracing_enabled)) {
          std::cout << "  * Removing non-constant cached [Elements]";
        }
      }
    } else if constexpr (std::is_same_v<NodeT, CheckMapsWithMigration> ||
                         std::is_same_v<NodeT, MigrateMapIfNeeded>) {
      // These instructions only migrate representations of values, not the
      // values themselves, so cached values are still valid.
    } else if constexpr (std::is_same_v<NodeT, StoreMap>) {
      ClearUnstableNodeAspectsForStoreMap(node, is_tracing_enabled);
    } else if constexpr (!IsSimpleFieldStore(Node::opcode_of<NodeT>) &&
                         !IsTypedArrayStore(Node::opcode_of<NodeT>)) {
      // Don't change known node aspects for simple field stores. The only
      // relevant side effect on these is writes to objects which invalidate
      // loaded properties and context slots, and we invalidate these already as
      // part of emitting the store.
      ClearUnstableNodeAspects(is_tracing_enabled);
    }
  }

  void Print(std::ostream& os) const;
  void Print() const { return Print(std::cout); }

  explicit KnownNodeAspects(Zone* zone)
      : loaded_constant_properties_(zone),
        loaded_properties_(zone),
        loaded_context_constants_(zone),
        loaded_context_slots_(zone),
        available_expressions_(zone),
        side_effects_require_invalidation_(false),
        may_have_aliasing_contexts_(ContextSlotLoadsAlias::kNone),
        effect_epoch_(0),
        node_infos_(zone),
        virtual_objects_() {}

 private:
  bool SetContextCachedValue(ValueNode* context, int offset, ValueNode* value,
                             MaybeAssignedFlag assigned);

  SmallZoneVector<LoadedContextSlotsKey, 8> ClearAliasedContextSlotsFor(
      Graph* graph, ValueNode* context, int offset, ValueNode* value);

  static constexpr uint32_t kEffectEpochForPureInstructions =
      std::numeric_limits<uint32_t>::max();
  static constexpr uint32_t kEffectEpochOverflow =
      kEffectEpochForPureInstructions - 1;

  struct AvailableExpression {
    NodeBase* node;
    uint32_t effect_epoch;
  };

  // These can call GetTypeUnchecked directly. Other uses should ideally go
  // through MaglevReducer::GetType or similar and add type assertions if
  // v8_flags.maglev_assert_types is on.
  // TODO(477184397): Consider unfriending.
  template <typename U>
  friend class MaglevReducer;
  friend class RecomputeKnownNodeAspectsProcessor;
  friend class MergePointInterpreterFrameState;

  NodeType GetTypeUnchecked(compiler::JSHeapBroker* broker,
                            ValueNode* node) const {
    // We first check the KnownNodeAspects in order to return the most precise
    // type possible.
    auto info = TryGetInfoFor(node);
    if (info == nullptr) {
      // If this node has no NodeInfo (or not known type in its NodeInfo), we
      // fall back to its static type.
      return node->GetStaticType(broker);
    }
    NodeType actual_type =
        IntersectType(info->type(), node->GetStaticType(broker));
    if (node->Is<ReturnedValue>()) {
      // The returned value might be more precise than the one stored in the
      // node info.
      actual_type = IntersectType(
          actual_type, GetTypeUnchecked(broker, node->input_node(0)));
    }
#ifdef DEBUG
    NodeType static_type = node->GetStaticType(broker);
    // TODO(428667907): Ideally we should bail out early for the kNone type.
    if (!NodeTypeIs(actual_type, static_type, NodeTypeIsVariant::kAllowNone)) {
      // In case we needed a numerical alternative of a smi value, the type
      // must generalize. In all other cases the node info type should reflect
      // the actual type.
      DCHECK(static_type == NodeType::kSmi &&
             actual_type == NodeType::kNumber &&
             !TryGetInfoFor(node)->alternative().has_none());
    }
#endif  // DEBUG
    return actual_type;
  }

  // Valid across side-effecting calls, as long as we install a dependency.
  LoadedPropertyMap loaded_constant_properties_;
  // Flushed after side-effecting calls.
  LoadedPropertyMap loaded_properties_;
  // Unconditionally valid across side-effecting calls.
  ZoneMap<std::tuple<ValueNode*, int>, ValueNode*> loaded_context_constants_;
  // Flushed after side-effecting calls.
  LoadedContextSlots loaded_context_slots_;
  // For CSE.
  ZoneMap<uint32_t, AvailableExpression> available_expressions_;
  // This is an optimization. We track whether any NodeInfos require work on
  // side effects. If not, we don't even iterate over NodeInfos.
  bool side_effects_require_invalidation_;
  // This field indicates if the current state of loaded_context_slots might
  // contain contexts aliases. If that is the case, then we need to be more
  // conservative about updating the state on stores.
  ContextSlotLoadsAlias may_have_aliasing_contexts_;
  uint32_t effect_epoch_;
  // TODO(leszeks): Store these more efficiently than with std::map -- in
  // particular, clear out entries that are no longer reachable, perhaps also
  // allow lookup by interpreter register rather than by node pointer.
  NodeInfos node_infos_;
  VirtualObjectList virtual_objects_;

  friend KnownNodeAspects* Zone::New<KnownNodeAspects, const KnownNodeAspects&>(
      const KnownNodeAspects&);
  KnownNodeAspects(const KnownNodeAspects& other) V8_NOEXCEPT = default;
  // Copy constructor for CloneForLoopHeader
  friend KnownNodeAspects* Zone::New<KnownNodeAspects, const KnownNodeAspects&,
                                     bool&, LoopEffects*&, Zone*&>(
      const KnownNodeAspects&, bool&, maglev::LoopEffects*&, Zone*&);
  KnownNodeAspects(const KnownNodeAspects& other, bool optimistic_initial_state,
                   LoopEffects* loop_effects, Zone* zone);

  ValueNode* TryFindLoadedProperty(const LoadedPropertyMap& properties,
                                   ValueNode* lookup_start_object,
                                   PropertyKey name) {
    auto props_for_name = properties.find(name);
    if (props_for_name == properties.end()) return nullptr;

    auto it = props_for_name->second.find(lookup_start_object);
    if (it == props_for_name->second.end()) return nullptr;

    return it->second->UnwrapIdentities();
  }

  NodeInfos::iterator FindInfo(ValueNode* node) {
    node = node->Unwrap();
    return node_infos_.find(node);
  }
  NodeInfos::const_iterator FindInfo(ValueNode* node) const {
    node = node->Unwrap();
    return node_infos_.find(node);
  }
  bool IsValid(NodeInfos::iterator& it) { return it != node_infos_.end(); }
  bool IsValid(NodeInfos::const_iterator& it) const {
    return it != node_infos_.end();
  }
};

std::ostream& operator<<(std::ostream& os, const KnownNodeAspects& aspects);

template <typename MapContainer>
class KnownMapsMerger {
 public:
  explicit KnownMapsMerger(compiler::JSHeapBroker* broker, Zone* zone,
                           const MapContainer& requested_maps)
      : broker_(broker), zone_(zone), requested_maps_(requested_maps) {}

  void IntersectWithKnownNodeAspects(
      ValueNode* object, const KnownNodeAspects& known_node_aspects) {
    const NodeInfo* node_info = known_node_aspects.TryGetInfoFor(object);
    NodeType type = node_info ? node_info->type() : NodeType::kUnknown;
    // KnownMapsMerger is for avoiding map checks that we've already done, so
    // only consider non-stale maps here.
    if (node_info && node_info->possible_maps_are_known() &&
        !node_info->maps_are_stale()) {
      // TODO(v8:7700): Make intersection non-quadratic.
      for (compiler::MapRef possible_map : node_info->possible_maps()) {
        if (std::find(requested_maps_.begin(), requested_maps_.end(),
                      possible_map) != requested_maps_.end()) {
          // No need to add dependencies, we already have them for all known
          // possible maps.
          // Filter maps which are impossible given this objects type. Since we
          // want to prove that an object with map `map` is not an instance of
          // `type`, we cannot use `StaticTypeForMap`, as it only provides an
          // approximation. This filtering is done to avoid creating
          // non-sensical types later (e.g. if we think only a non-string map
          // is possible, after a string check).
          if (IsInstanceOfNodeType(possible_map, type, broker_)) {
            InsertMap(possible_map);
          }
        } else {
          known_maps_are_subset_of_requested_maps_ = false;
        }
      }
      if (intersect_set_.is_empty()) {
        node_type_ = EmptyNodeType();
      }
    } else {
      // A missing entry here means the universal set, i.e., we don't know
      // anything about the possible maps of the object. Intersect with the
      // universal set, which means just insert all requested maps.
      known_maps_are_subset_of_requested_maps_ = false;
      existing_known_maps_found_ = false;
      for (const compiler::MapRef map : requested_maps_) {
        InsertMap(map);
      }
    }
  }

  void UpdateKnownNodeAspects(ValueNode* object,
                              KnownNodeAspects& known_node_aspects) {
    // Update known maps.
    auto node_info = known_node_aspects.GetOrCreateInfoFor(broker_, object);
    node_info->SetPossibleMaps(intersect_set_,
                               any_map_or_node_type_is_unstable_, node_type_,
                               broker_, known_node_aspects);
    // Make sure known_node_aspects.side_effects_require_invalidation is updated
    // in case any_map_or_node_type_is_unstable changed to true for this object
    // -- this can happen if this was an intersection with the universal set
    // which added new possible unstable maps.
    if (any_map_or_node_type_is_unstable_) {
      known_node_aspects.MarkSideEffectsRequireInvalidation();
    }
    // At this point, known_node_aspects.side_effects_require_invalidation may
    // be true despite there no longer being any unstable maps for any nodes (if
    // this was the only node with unstable maps and this intersection removed
    // those). This is ok, because that's at worst just an overestimate -- we
    // could track whether this node's any_map_or_node_type_is_unstable flipped
    // from true to false, but this is likely overkill. Insert stable map
    // dependencies which weren't inserted yet. This is only needed if our set
    // of known maps was empty and we created it anew based on maps we checked.
    if (!existing_known_maps_found_) {
      for (compiler::MapRef map : intersect_set_) {
        if (map.is_stable()) {
          broker_->dependencies()->DependOnStableMap(map);
        }
      }
    } else {
      // TODO(victorgomes): Add a DCHECK_SLOW that checks if the maps already
      // exist in the CompilationDependencySet.
    }
  }

  bool known_maps_are_subset_of_requested_maps() const {
    return known_maps_are_subset_of_requested_maps_;
  }
  bool emit_check_with_migration() const { return emit_check_with_migration_; }

  const compiler::ZoneRefSet<Map>& intersect_set() const {
    return intersect_set_;
  }

  NodeType node_type() const { return node_type_; }

 private:
  compiler::JSHeapBroker* broker_;
  Zone* zone_;
  const MapContainer& requested_maps_;
  compiler::ZoneRefSet<Map> intersect_set_;
  bool known_maps_are_subset_of_requested_maps_ = true;
  bool existing_known_maps_found_ = true;
  bool emit_check_with_migration_ = false;
  bool any_map_or_node_type_is_unstable_ = false;
  NodeType node_type_ = EmptyNodeType();

  Zone* zone() const { return zone_; }

  void InsertMap(compiler::MapRef map) {
    if (map.is_migration_target()) {
      emit_check_with_migration_ = true;
    }
    NodeType new_type = StaticTypeForMap(map, broker_);
    if (new_type == NodeType::kHeapNumber) {
      new_type = UnionType(new_type, NodeType::kSmi);
    }
    node_type_ = UnionType(node_type_, new_type);
    if (!map.is_stable()) {
      any_map_or_node_type_is_unstable_ = true;
    }
    intersect_set_.insert(map, zone());
  }
};

}  // namespace maglev
}  // namespace internal
}  // namespace v8

#endif  // V8_MAGLEV_MAGLEV_KNOWN_NODE_ASPECTS_H_
