/*
 * Copyright (C) 2015-2019 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include "config.h"
#include "ObjectPropertyConditionSet.h"

#include "JSCInlines.h"
#include <wtf/ListDump.h>

namespace JSC {

ObjectPropertyCondition ObjectPropertyConditionSet::forObject(JSObject* object) const
{
    for (const ObjectPropertyCondition& condition : *this) {
        if (condition.object() == object)
            return condition;
    }
    return ObjectPropertyCondition();
}

ObjectPropertyCondition ObjectPropertyConditionSet::forConditionKind(
    PropertyCondition::Kind kind) const
{
    for (const ObjectPropertyCondition& condition : *this) {
        if (condition.kind() == kind)
            return condition;
    }
    return ObjectPropertyCondition();
}

unsigned ObjectPropertyConditionSet::numberOfConditionsWithKind(PropertyCondition::Kind kind) const
{
    unsigned result = 0;
    for (const ObjectPropertyCondition& condition : *this) {
        if (condition.kind() == kind)
            result++;
    }
    return result;
}

bool ObjectPropertyConditionSet::hasOneSlotBaseCondition() const
{
    bool sawBase = false;
    for (const ObjectPropertyCondition& condition : *this) {
        switch (condition.kind()) {
        case PropertyCondition::Presence:
        case PropertyCondition::Equivalence:
        case PropertyCondition::HasStaticProperty:
            if (sawBase)
                return false;
            sawBase = true;
            break;
        default:
            break;
        }
    }

    return sawBase;
}

ObjectPropertyCondition ObjectPropertyConditionSet::slotBaseCondition() const
{
    ObjectPropertyCondition result;
    unsigned numFound = 0;
    for (const ObjectPropertyCondition& condition : *this) {
        if (condition.kind() == PropertyCondition::Presence
            || condition.kind() == PropertyCondition::Equivalence
            || condition.kind() == PropertyCondition::HasStaticProperty) {
            result = condition;
            numFound++;
        }
    }
    RELEASE_ASSERT(numFound == 1);
    return result;
}

ObjectPropertyConditionSet ObjectPropertyConditionSet::mergedWith(
    const ObjectPropertyConditionSet& other) const
{
    if (!isValid() || !other.isValid())
        return invalid();

    Vector<ObjectPropertyCondition> result;
    
    if (!isEmpty())
        result.appendVector(m_data->vector);
    
    for (const ObjectPropertyCondition& newCondition : other) {
        bool foundMatch = false;
        for (const ObjectPropertyCondition& existingCondition : *this) {
            if (newCondition == existingCondition) {
                foundMatch = true;
                continue;
            }
            if (!newCondition.isCompatibleWith(existingCondition))
                return invalid();
        }
        if (!foundMatch)
            result.append(newCondition);
    }

    return create(result);
}

bool ObjectPropertyConditionSet::structuresEnsureValidity() const
{
    if (!isValid())
        return false;
    
    for (const ObjectPropertyCondition& condition : *this) {
        if (!condition.structureEnsuresValidity())
            return false;
    }
    return true;
}

bool ObjectPropertyConditionSet::structuresEnsureValidityAssumingImpurePropertyWatchpoint() const
{
    if (!isValid())
        return false;
    
    for (const ObjectPropertyCondition& condition : *this) {
        if (!condition.structureEnsuresValidityAssumingImpurePropertyWatchpoint())
            return false;
    }
    return true;
}

bool ObjectPropertyConditionSet::needImpurePropertyWatchpoint() const
{
    for (const ObjectPropertyCondition& condition : *this) {
        if (condition.validityRequiresImpurePropertyWatchpoint())
            return true;
    }
    return false;
}

bool ObjectPropertyConditionSet::areStillLive(VM& vm) const
{
    bool stillLive = true;
    forEachDependentCell([&](JSCell* cell) {
        stillLive &= vm.heap.isMarked(cell);
    });
    return stillLive;
}

void ObjectPropertyConditionSet::dumpInContext(PrintStream& out, DumpContext* context) const
{
    if (!isValid()) {
        out.print("<invalid>");
        return;
    }
    
    out.print("[");
    if (m_data)
        out.print(listDumpInContext(m_data->vector, context));
    out.print("]");
}

void ObjectPropertyConditionSet::dump(PrintStream& out) const
{
    dumpInContext(out, nullptr);
}

bool ObjectPropertyConditionSet::isValidAndWatchable() const
{
    if (!isValid())
        return false;

    for (ObjectPropertyCondition condition : m_data->vector) {
        if (!condition.isWatchable())
            return false;
    }
    return true;
}

namespace {

namespace ObjectPropertyConditionSetInternal {
static constexpr bool verbose = false;
}

ObjectPropertyCondition generateCondition(
    VM& vm, JSCell* owner, JSObject* object, UniquedStringImpl* uid, PropertyCondition::Kind conditionKind)
{
    Structure* structure = object->structure(vm);
    if (ObjectPropertyConditionSetInternal::verbose)
        dataLog("Creating condition ", conditionKind, " for ", pointerDump(structure), "\n");

    ObjectPropertyCondition result;
    switch (conditionKind) {
    case PropertyCondition::Presence: {
        unsigned attributes;
        PropertyOffset offset = structure->getConcurrently(uid, attributes);
        if (offset == invalidOffset)
            return ObjectPropertyCondition();
        result = ObjectPropertyCondition::presence(vm, owner, object, uid, offset, attributes);
        break;
    }
    case PropertyCondition::Absence: {
        if (structure->hasPolyProto())
            return ObjectPropertyCondition();
        result = ObjectPropertyCondition::absence(
            vm, owner, object, uid, object->structure(vm)->storedPrototypeObject());
        break;
    }
    case PropertyCondition::AbsenceOfSetEffect: {
        if (structure->hasPolyProto())
            return ObjectPropertyCondition();
        result = ObjectPropertyCondition::absenceOfSetEffect(
            vm, owner, object, uid, object->structure(vm)->storedPrototypeObject());
        break;
    }
    case PropertyCondition::Equivalence: {
        unsigned attributes;
        PropertyOffset offset = structure->getConcurrently(uid, attributes);
        if (offset == invalidOffset)
            return ObjectPropertyCondition();
        JSValue value = object->getDirectConcurrently(structure, offset);
        if (!value)
            return ObjectPropertyCondition();
        result = ObjectPropertyCondition::equivalence(vm, owner, object, uid, value);
        break;
    }
    case PropertyCondition::HasStaticProperty: {
        auto entry = object->findPropertyHashEntry(vm, uid);
        if (!entry)
            return ObjectPropertyCondition();
        result = ObjectPropertyCondition::hasStaticProperty(vm, owner, object, uid);
        break;
    }
    default:
        RELEASE_ASSERT_NOT_REACHED();
        return ObjectPropertyCondition();
    }

    if (!result.isStillValidAssumingImpurePropertyWatchpoint()) {
        if (ObjectPropertyConditionSetInternal::verbose)
            dataLog("Failed to create condition: ", result, "\n");
        return ObjectPropertyCondition();
    }

    if (ObjectPropertyConditionSetInternal::verbose)
        dataLog("New condition: ", result, "\n");
    return result;
}

template<typename Functor>
ObjectPropertyConditionSet generateConditions(
    VM& vm, JSGlobalObject* globalObject, Structure* structure, JSObject* prototype, const Functor& functor)
{
    Vector<ObjectPropertyCondition> conditions;
    
    for (;;) {
        if (ObjectPropertyConditionSetInternal::verbose)
            dataLog("Considering structure: ", pointerDump(structure), "\n");
        
        if (structure->isProxy()) {
            if (ObjectPropertyConditionSetInternal::verbose)
                dataLog("It's a proxy, so invalid.\n");
            return ObjectPropertyConditionSet::invalid();
        }

        if (structure->hasPolyProto()) {
            // FIXME: Integrate this with PolyProtoAccessChain:
            // https://bugs.webkit.org/show_bug.cgi?id=177339
            // Or at least allow OPC set generation when the
            // base is not poly proto:
            // https://bugs.webkit.org/show_bug.cgi?id=177721
            return ObjectPropertyConditionSet::invalid();
        }
        
        JSValue value = structure->prototypeForLookup(globalObject);
        
        if (value.isNull()) {
            if (!prototype) {
                if (ObjectPropertyConditionSetInternal::verbose)
                    dataLog("Reached end of prototype chain as expected, done.\n");
                break;
            }
            if (ObjectPropertyConditionSetInternal::verbose)
                dataLog("Unexpectedly reached end of prototype chain, so invalid.\n");
            return ObjectPropertyConditionSet::invalid();
        }
        
        JSObject* object = jsCast<JSObject*>(value);
        structure = object->structure(vm);
        
        if (structure->isDictionary()) {
            if (ObjectPropertyConditionSetInternal::verbose)
                dataLog("Cannot cache dictionary.\n");
            return ObjectPropertyConditionSet::invalid();
        }

        if (!functor(conditions, object)) {
            if (ObjectPropertyConditionSetInternal::verbose)
                dataLog("Functor failed, invalid.\n");
            return ObjectPropertyConditionSet::invalid();
        }
        
        if (object == prototype) {
            if (ObjectPropertyConditionSetInternal::verbose)
                dataLog("Reached desired prototype, done.\n");
            break;
        }
    }

    if (ObjectPropertyConditionSetInternal::verbose)
        dataLog("Returning conditions: ", listDump(conditions), "\n");
    return ObjectPropertyConditionSet::create(conditions);
}

} // anonymous namespace

ObjectPropertyConditionSet generateConditionsForPropertyMiss(
    VM& vm, JSCell* owner, JSGlobalObject* globalObject, Structure* headStructure, UniquedStringImpl* uid)
{
    return generateConditions(
        vm, globalObject, headStructure, nullptr,
        [&] (Vector<ObjectPropertyCondition>& conditions, JSObject* object) -> bool {
            ObjectPropertyCondition result =
                generateCondition(vm, owner, object, uid, PropertyCondition::Absence);
            if (!result)
                return false;
            conditions.append(result);
            return true;
        });
}

ObjectPropertyConditionSet generateConditionsForPropertySetterMiss(
    VM& vm, JSCell* owner, JSGlobalObject* globalObject, Structure* headStructure, UniquedStringImpl* uid)
{
    return generateConditions(
        vm, globalObject, headStructure, nullptr,
        [&] (Vector<ObjectPropertyCondition>& conditions, JSObject* object) -> bool {
            ObjectPropertyCondition result =
                generateCondition(vm, owner, object, uid, PropertyCondition::AbsenceOfSetEffect);
            if (!result)
                return false;
            conditions.append(result);
            return true;
        });
}

ObjectPropertyConditionSet generateConditionsForPrototypePropertyHit(
    VM& vm, JSCell* owner, JSGlobalObject* globalObject, Structure* headStructure, JSObject* prototype,
    UniquedStringImpl* uid)
{
    return generateConditions(
        vm, globalObject, headStructure, prototype,
        [&] (Vector<ObjectPropertyCondition>& conditions, JSObject* object) -> bool {
            PropertyCondition::Kind kind =
                object == prototype ? PropertyCondition::Presence : PropertyCondition::Absence;
            ObjectPropertyCondition result =
                generateCondition(vm, owner, object, uid, kind);
            if (!result)
                return false;
            conditions.append(result);
            return true;
        });
}

ObjectPropertyConditionSet generateConditionsForPrototypePropertyHitCustom(
    VM& vm, JSCell* owner, JSGlobalObject* globalObject, Structure* headStructure, JSObject* prototype,
    UniquedStringImpl* uid, unsigned attributes)
{
    return generateConditions(
        vm, globalObject, headStructure, prototype,
        [&] (Vector<ObjectPropertyCondition>& conditions, JSObject* object) -> bool {
            auto kind = PropertyCondition::Absence;
            if (object == prototype) {
                Structure* structure = object->structure(vm);
                PropertyOffset offset = structure->get(vm, uid);
                if (isValidOffset(offset)) {
                    // When we reify custom accessors, we wrap them in a JSFunction that we shove
                    // inside a GetterSetter. So, once we've reified a custom accessor, we will
                    // no longer see it as a "custom" accessor/value. Hence, if our property access actually
                    // notices a custom, it must be a CustomGetterSetterType cell or something
                    // in the static property table. Custom values get reified into CustomGetterSetters.
                    JSValue value = object->getDirect(offset);

                    if (!value.isCell() || value.asCell()->type() != CustomGetterSetterType) {
                        // The value could have just got changed to some other type, so check if it's still
                        // a custom getter setter.
                        return false;
                    }

                    kind = PropertyCondition::Equivalence;
                } else if (structure->findPropertyHashEntry(uid))
                    kind = PropertyCondition::HasStaticProperty;
                else if (attributes & PropertyAttribute::DontDelete) {
                    // This can't change, so we can blindly cache it.
                    return true;
                } else {
                    // This means we materialized a custom out of thin air and it's not DontDelete (i.e, it can be
                    // redefined). This is curious. We don't actually need to crash here. We could blindly cache
                    // the function. Or we could blindly not cache it. However, we don't actually do this in WebKit
                    // right now, so it's reasonable to decide what to do later (or to warn people of forgetting DoneDelete.)
                    ASSERT_NOT_REACHED();
                    return false;
                }
            }
            ObjectPropertyCondition result = generateCondition(vm, owner, object, uid, kind);
            if (!result)
                return false;
            conditions.append(result);
            return true;
        });
}

ObjectPropertyConditionSet generateConditionsForInstanceOf(
    VM& vm, JSCell* owner, JSGlobalObject* globalObject, Structure* headStructure, JSObject* prototype,
    bool shouldHit)
{
    bool didHit = false;
    if (ObjectPropertyConditionSetInternal::verbose)
        dataLog("Searching for prototype ", JSValue(prototype), " starting with structure ", RawPointer(headStructure), " with shouldHit = ", shouldHit, "\n");
    ObjectPropertyConditionSet result = generateConditions(
        vm, globalObject, headStructure, shouldHit ? prototype : nullptr,
        [&] (Vector<ObjectPropertyCondition>& conditions, JSObject* object) -> bool {
            if (ObjectPropertyConditionSetInternal::verbose)
                dataLog("Encountered object: ", RawPointer(object), "\n");
            if (object == prototype) {
                RELEASE_ASSERT(shouldHit);
                didHit = true;
                return true;
            }

            Structure* structure = object->structure(vm);
            if (structure->hasPolyProto())
                return false;
            conditions.append(
                ObjectPropertyCondition::hasPrototype(
                    vm, owner, object, structure->storedPrototypeObject()));
            return true;
        });
    if (result.isValid()) {
        if (ObjectPropertyConditionSetInternal::verbose)
            dataLog("didHit = ", didHit, ", shouldHit = ", shouldHit, "\n");
        RELEASE_ASSERT(didHit == shouldHit);
    }
    return result;
}

ObjectPropertyConditionSet generateConditionsForPrototypeEquivalenceConcurrently(
    VM& vm, JSGlobalObject* globalObject, Structure* headStructure, JSObject* prototype, UniquedStringImpl* uid)
{
    return generateConditions(vm, globalObject, headStructure, prototype,
        [&] (Vector<ObjectPropertyCondition>& conditions, JSObject* object) -> bool {
            PropertyCondition::Kind kind =
                object == prototype ? PropertyCondition::Equivalence : PropertyCondition::Absence;
            ObjectPropertyCondition result = generateCondition(vm, nullptr, object, uid, kind);
            if (!result)
                return false;
            conditions.append(result);
            return true;
        });
}

ObjectPropertyConditionSet generateConditionsForPropertyMissConcurrently(
    VM& vm, JSGlobalObject* globalObject, Structure* headStructure, UniquedStringImpl* uid)
{
    return generateConditions(
        vm, globalObject, headStructure, nullptr,
        [&] (Vector<ObjectPropertyCondition>& conditions, JSObject* object) -> bool {
            ObjectPropertyCondition result = generateCondition(vm, nullptr, object, uid, PropertyCondition::Absence);
            if (!result)
                return false;
            conditions.append(result);
            return true;
        });
}

ObjectPropertyConditionSet generateConditionsForPropertySetterMissConcurrently(
    VM& vm, JSGlobalObject* globalObject, Structure* headStructure, UniquedStringImpl* uid)
{
    return generateConditions(
        vm, globalObject, headStructure, nullptr,
        [&] (Vector<ObjectPropertyCondition>& conditions, JSObject* object) -> bool {
            ObjectPropertyCondition result =
                generateCondition(vm, nullptr, object, uid, PropertyCondition::AbsenceOfSetEffect);
            if (!result)
                return false;
            conditions.append(result);
            return true;
        });
}

ObjectPropertyCondition generateConditionForSelfEquivalence(
    VM& vm, JSCell* owner, JSObject* object, UniquedStringImpl* uid)
{
    return generateCondition(vm, owner, object, uid, PropertyCondition::Equivalence);
}

// Current might be null. Structure can't be null.
static Optional<PrototypeChainCachingStatus> prepareChainForCaching(JSGlobalObject* globalObject, JSCell* current, Structure* structure, JSObject* target)
{
    ASSERT(structure);
    VM& vm = globalObject->vm();

    bool found = false;
    bool usesPolyProto = false;
    bool flattenedDictionary = false;

    while (true) {
        if (structure->isDictionary()) {
            if (!current)
                return WTF::nullopt;

            ASSERT(structure->isObject());
            if (structure->hasBeenFlattenedBefore())
                return WTF::nullopt;

            structure->flattenDictionaryStructure(vm, asObject(current));
            flattenedDictionary = true;
        }

        if (!structure->propertyAccessesAreCacheable())
            return WTF::nullopt;

        if (structure->isProxy())
            return WTF::nullopt;

        if (current && current == target) {
            found = true;
            break;
        }

        // We only have poly proto if we need to access our prototype via
        // the poly proto protocol. If the slot base is the only poly proto
        // thing in the chain, and we have a cache hit on it, then we're not
        // poly proto.
        JSValue prototype;
        if (structure->hasPolyProto()) {
            if (!current)
                return WTF::nullopt;
            usesPolyProto = true;
            prototype = structure->prototypeForLookup(globalObject, current);
        } else
            prototype = structure->prototypeForLookup(globalObject);

        if (prototype.isNull())
            break;
        current = asObject(prototype);
        structure = current->structure(vm);
    }

    if (!found && !!target)
        return WTF::nullopt;

    PrototypeChainCachingStatus result;
    result.usesPolyProto = usesPolyProto;
    result.flattenedDictionary = flattenedDictionary;

    return result;
}

Optional<PrototypeChainCachingStatus> prepareChainForCaching(JSGlobalObject* globalObject, JSCell* base, JSObject* target)
{
    return prepareChainForCaching(globalObject, base, base->structure(globalObject->vm()), target);
}

Optional<PrototypeChainCachingStatus> prepareChainForCaching(JSGlobalObject* globalObject, JSCell* base, const PropertySlot& slot)
{
    JSObject* target = slot.isUnset() ? nullptr : slot.slotBase();
    return prepareChainForCaching(globalObject, base, target);
}

Optional<PrototypeChainCachingStatus> prepareChainForCaching(JSGlobalObject* globalObject, Structure* baseStructure, JSObject* target)
{
    return prepareChainForCaching(globalObject, nullptr, baseStructure, target);
}

} // namespace JSC

