/*
 * Copyright (C) 2009-2024 Apple Inc. All rights reserved.
 * Copyright (C) 2020 Alexey Shvayka <shvaikalesh@gmail.com>.
 *
 * 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 "JSONObject.h"

#include "ArrayConstructor.h"
#include "BigIntObject.h"
#include "BooleanObject.h"
#include "GetterSetter.h"
#include "JSArrayInlines.h"
#include "JSCInlines.h"
#include "LiteralParser.h"
#include "NumberObject.h"
#include "ObjectConstructorInlines.h"
#include "PropertyNameArray.h"
#include "VMInlines.h"
#include <charconv>
#include <wtf/dragonbox/dragonbox_to_chars.h>
#include <wtf/text/EscapedFormsForJSON.h>
#include <wtf/text/MakeString.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/StringBuilderJSON.h>
#include <wtf/text/StringCommon.h>

// Turn this on to log information about fastStringify usage, with a focus on why it failed.
#define FAST_STRINGIFY_LOG_USAGE 0

WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN

namespace JSC {

STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(JSONObject);

static JSC_DECLARE_HOST_FUNCTION(jsonProtoFuncParse);
static JSC_DECLARE_HOST_FUNCTION(jsonProtoFuncStringify);

}

#include "JSONObject.lut.h"

namespace JSC {

JSONObject::JSONObject(VM& vm, Structure* structure)
    : JSNonFinalObject(vm, structure)
{
}

void JSONObject::finishCreation(VM& vm)
{
    Base::finishCreation(vm);
    ASSERT(inherits(info()));
    JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}

// PropertyNameForFunctionCall objects must be on the stack, since the JSValue that they create is not marked.
class PropertyNameForFunctionCall {
public:
    PropertyNameForFunctionCall(PropertyName);
    PropertyNameForFunctionCall(unsigned);

    JSValue value(VM&) const;

private:
    PropertyName m_propertyName;
    unsigned m_number;
    mutable JSValue m_value;
};

class Stringifier {
    WTF_MAKE_NONCOPYABLE(Stringifier);
    WTF_FORBID_HEAP_ALLOCATION;
public:
    static String stringify(JSGlobalObject&, JSValue, JSValue replacer, JSValue space);

private:
    class Holder {
    public:
        enum RootHolderTag { RootHolder };
        Holder(JSGlobalObject*, JSObject*, Structure*);
        Holder(RootHolderTag, JSObject*);

        JSObject* object() const { return m_object; }
        bool isArray() const { return m_isArray; }
        bool hasFastObjectProperties() const { return m_hasFastObjectProperties; }

        bool appendNextProperty(Stringifier&, StringBuilder&);

    private:
        JSObject* m_object { nullptr };
        Structure* m_structure { nullptr };
        const bool m_isJSArray { false };
        const bool m_isArray { false };
        bool m_hasFastObjectProperties { false };
        unsigned m_index { 0 };
        unsigned m_size { 0 };
        RefPtr<PropertyNameArrayData> m_propertyNames;
        Vector<std::tuple<PropertyName, unsigned>, 8> m_propertiesAndOffsets;
    };

    friend class Holder;

    Stringifier(JSGlobalObject*, JSValue replacer, JSValue space);

    JSValue toJSON(JSValue, const PropertyNameForFunctionCall&);

    enum StringifyResult { StringifyFailed, StringifySucceeded, StringifyFailedDueToUndefinedOrSymbolValue };
    StringifyResult appendStringifiedValue(StringBuilder&, JSValue, const Holder&, const PropertyNameForFunctionCall&);

    bool willIndent() const;
    void indent();
    void unindent();
    void startNewLine(StringBuilder&) const;
    bool isCallableReplacer() const { return m_replacerCallData.type != CallData::Type::None; }

    JSGlobalObject* const m_globalObject;
    JSValue m_replacer;
    bool m_usingArrayReplacer { false };
    PropertyNameArray m_arrayReplacerPropertyNames;
    CallData m_replacerCallData;
    String m_gap;

    MarkedArgumentBufferWithSize<16> m_objectStack;
    Vector<Holder, 16, UnsafeVectorOverflow> m_holderStack;
    String m_repeatedGap;
    StringView m_indent;
};

// ------------------------------ helper functions --------------------------------

static inline JSValue unwrapBoxedPrimitive(JSGlobalObject* globalObject, JSObject* object)
{
    if (object->inherits<NumberObject>())
        return jsNumber(object->toNumber(globalObject));
    if (object->inherits<StringObject>())
        return object->toString(globalObject);
    if (object->inherits<BooleanObject>() || object->inherits<BigIntObject>())
        return jsCast<JSWrapperObject*>(object)->internalValue();

    // Do not unwrap SymbolObject to Symbol. It is not performed in the spec.
    // http://www.ecma-international.org/ecma-262/6.0/#sec-serializejsonproperty

    return object;
}

static inline JSValue unwrapBoxedPrimitive(JSGlobalObject* globalObject, JSValue value)
{
    return value.isObject() ? unwrapBoxedPrimitive(globalObject, asObject(value)) : value;
}

static inline String gap(JSGlobalObject* globalObject, JSValue space)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    constexpr unsigned maxGapLength = 10;
    space = unwrapBoxedPrimitive(globalObject, space);
    RETURN_IF_EXCEPTION(scope, { });

    // If the space value is a number, create a gap string with that number of spaces.
    if (space.isNumber()) {
        double spaceCount = space.asNumber();
        size_t count;
        if (spaceCount > maxGapLength)
            count = maxGapLength;
        else if (!(spaceCount > 0))
            count = 0;
        else
            count = static_cast<size_t>(spaceCount);
        char spaces[maxGapLength];
        for (size_t i = 0; i < count; ++i)
            spaces[i] = ' ';
        return String(std::span { spaces }.first(count));
    }

    // If the space value is a string, use it as the gap string, otherwise use no gap string.
    String spaces = space.getString(globalObject);
    RETURN_IF_EXCEPTION(scope, { });
    if (spaces.length() <= maxGapLength)
        return spaces;
    return spaces.substringSharingImpl(0, maxGapLength);
}

// ------------------------------ PropertyNameForFunctionCall --------------------------------

inline PropertyNameForFunctionCall::PropertyNameForFunctionCall(PropertyName propertyName)
    : m_propertyName(propertyName)
{
}

inline PropertyNameForFunctionCall::PropertyNameForFunctionCall(unsigned number)
    : m_propertyName(nullptr)
    , m_number(number)
{
}

JSValue PropertyNameForFunctionCall::value(VM& vm) const
{
    if (!m_value) {
        if (!m_propertyName.isNull())
            m_value = jsString(vm, String { m_propertyName.uid() });
        else {
            if (m_number <= 9)
                return vm.smallStrings.singleCharacterString(m_number + '0');
            m_value = jsNontrivialString(vm, vm.numericStrings.add(m_number));
        }
    }
    return m_value;
}

// ------------------------------ Stringifier --------------------------------

Stringifier::Stringifier(JSGlobalObject* globalObject, JSValue replacer, JSValue space)
    : m_globalObject(globalObject)
    , m_replacer(replacer)
    , m_arrayReplacerPropertyNames(globalObject->vm(), PropertyNameMode::Strings, PrivateSymbolMode::Exclude)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (m_replacer.isObject()) {
        JSObject* replacerObject = asObject(m_replacer);

        m_replacerCallData = JSC::getCallData(replacerObject);
        if (m_replacerCallData.type == CallData::Type::None) {
            bool isArrayReplacer = JSC::isArray(globalObject, replacerObject);
            RETURN_IF_EXCEPTION(scope, );
            if (isArrayReplacer) {
                m_usingArrayReplacer = true;
                forEachInArrayLike(globalObject, replacerObject, [&] (JSValue name) -> bool {
                    if (name.isObject()) {
                        auto* nameObject = jsCast<JSObject*>(name);
                        if (!nameObject->inherits<NumberObject>() && !nameObject->inherits<StringObject>())
                            return true;
                    } else if (!name.isNumber() && !name.isString())
                        return true;

                    JSString* propertyNameString = name.toString(globalObject);
                    RETURN_IF_EXCEPTION(scope, false);
                    auto propertyName = propertyNameString->toIdentifier(globalObject);
                    RETURN_IF_EXCEPTION(scope, false);
                    m_arrayReplacerPropertyNames.add(WTFMove(propertyName));
                    return true;
                });
                RETURN_IF_EXCEPTION(scope, );
            }
        }
    }

    scope.release();
    m_gap = gap(globalObject, space);
}

String Stringifier::stringify(JSGlobalObject& globalObject, JSValue value, JSValue replacer, JSValue space)
{
    VM& vm = globalObject.vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    Stringifier stringifier(&globalObject, replacer, space);
    RETURN_IF_EXCEPTION(scope, { });

    PropertyNameForFunctionCall emptyPropertyName(vm.propertyNames->emptyIdentifier.impl());

    // If the replacer is not callable, root object wrapper is non-user-observable.
    // We can skip creating this wrapper object.
    JSObject* object = nullptr;
    if (stringifier.isCallableReplacer()) {
        object = constructEmptyObject(&globalObject);
        object->putDirect(vm, vm.propertyNames->emptyIdentifier, value);
    }

    StringBuilder result(OverflowPolicy::RecordOverflow);
    Holder root(Holder::RootHolder, object);
    auto stringifyResult = stringifier.appendStringifiedValue(result, value, root, emptyPropertyName);
    RETURN_IF_EXCEPTION(scope, { });
    if (UNLIKELY(result.hasOverflowed())) {
        throwOutOfMemoryError(&globalObject, scope);
        return { };
    }
    if (UNLIKELY(stringifyResult != StringifySucceeded))
        RELEASE_AND_RETURN(scope, { });
    RELEASE_AND_RETURN(scope, result.toString());
}

ALWAYS_INLINE JSValue Stringifier::toJSON(JSValue baseValue, const PropertyNameForFunctionCall& propertyName)
{
    VM& vm = m_globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    scope.assertNoException();

    JSValue toJSONFunction;
    if (baseValue.isObject())
        toJSONFunction = asObject(baseValue)->structure()->cachedSpecialProperty(CachedSpecialPropertyKey::ToJSON);

    if (!toJSONFunction) {
        PropertySlot slot(baseValue, PropertySlot::InternalMethodType::Get);
        bool hasProperty = baseValue.getPropertySlot(m_globalObject, vm.propertyNames->toJSON, slot);
        RETURN_IF_EXCEPTION(scope, { });
        toJSONFunction = hasProperty ? slot.getValue(m_globalObject, vm.propertyNames->toJSON) : jsUndefined();
        RETURN_IF_EXCEPTION(scope, { });

        if (baseValue.isObject())
            asObject(baseValue)->structure()->cacheSpecialProperty(m_globalObject, vm, toJSONFunction, CachedSpecialPropertyKey::ToJSON, slot);
    }

    auto callData = JSC::getCallData(toJSONFunction);
    if (callData.type == CallData::Type::None)
        return baseValue;

    MarkedArgumentBuffer args;
    args.append(propertyName.value(vm));
    ASSERT(!args.hasOverflowed());
    RELEASE_AND_RETURN(scope, call(m_globalObject, asObject(toJSONFunction), callData, baseValue, args));
}

// We clamp recursion well beyond anything reasonable.
constexpr unsigned maximumSideStackRecursion = 40000;
Stringifier::StringifyResult Stringifier::appendStringifiedValue(StringBuilder& builder, JSValue value, const Holder& holder, const PropertyNameForFunctionCall& propertyName)
{
    VM& vm = m_globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    // Recursion is avoided by !holderStackWasEmpty check and do/while loop at the end of this method.
    // We're having this recursion check here as a fail safe in case the code
    // below get modified such that recursion is no longer avoided.
    if (UNLIKELY(!vm.isSafeToRecurseSoft())) {
        throwStackOverflowError(m_globalObject, scope);
        return StringifyFailed;
    }

    // Call the toJSON function.
    if (value.isObject() || value.isBigInt()) {
        value = toJSON(value, propertyName);
        RETURN_IF_EXCEPTION(scope, StringifyFailed);
    }

    // Call the replacer function.
    if (isCallableReplacer()) {
        MarkedArgumentBuffer args;
        args.append(propertyName.value(vm));
        args.append(value);
        ASSERT(!args.hasOverflowed());
        ASSERT(holder.object());
        value = call(m_globalObject, m_replacer, m_replacerCallData, holder.object(), args);
        RETURN_IF_EXCEPTION(scope, StringifyFailed);
    }

    if ((value.isUndefined() || value.isSymbol()) && !holder.isArray())
        return StringifyFailedDueToUndefinedOrSymbolValue;

    if (value.isNull()) {
        builder.append("null"_s);
        return StringifySucceeded;
    }

    if (value.isObject()) {
        value = unwrapBoxedPrimitive(m_globalObject, asObject(value));
        RETURN_IF_EXCEPTION(scope, StringifyFailed);
    }

    if (value.isBoolean()) {
        if (value.isTrue())
            builder.append("true"_s);
        else
            builder.append("false"_s);
        return StringifySucceeded;
    }

    if (value.isString()) {
        auto string = asString(value)->value(m_globalObject);
        RETURN_IF_EXCEPTION(scope, StringifyFailed);
        builder.appendQuotedJSONString(string);
        return StringifySucceeded;
    }

    if (value.isNumber()) {
        if (value.isInt32())
            builder.append(value.asInt32());
        else {
            double number = value.asNumber();
            if (!std::isfinite(number))
                builder.append("null"_s);
            else
                builder.append(number);
        }
        return StringifySucceeded;
    }

    if (value.isBigInt()) {
        throwTypeError(m_globalObject, scope, "JSON.stringify cannot serialize BigInt."_s);
        return StringifyFailed;
    }

    if (!value.isObject())
        return StringifyFailed;

    JSObject* object = asObject(value);
    if (object->isCallable()) {
        if (holder.isArray()) {
            builder.append("null"_s);
            return StringifySucceeded;
        }
        return StringifyFailedDueToUndefinedOrSymbolValue;
    }

    if (UNLIKELY(builder.hasOverflowed()))
        return StringifyFailed;

    // Handle cycle detection, and put the holder on the stack.
    for (unsigned i = 0; i < m_holderStack.size(); i++) {
        if (m_holderStack[i].object() == object) {
            throwTypeError(m_globalObject, scope, "JSON.stringify cannot serialize cyclic structures."_s);
            return StringifyFailed;
        }
    }

    if (UNLIKELY(m_holderStack.size() >= maximumSideStackRecursion)) {
        throwStackOverflowError(m_globalObject, scope);
        return StringifyFailed;
    }

    bool holderStackWasEmpty = m_holderStack.isEmpty();
    Structure* structure = object->structure();
    m_holderStack.append(Holder(m_globalObject, object, structure));
    m_objectStack.appendWithCrashOnOverflow(object);
    m_objectStack.appendWithCrashOnOverflow(structure);
    RETURN_IF_EXCEPTION(scope, StringifyFailed);
    if (!holderStackWasEmpty)
        return StringifySucceeded;

    do {
        while (m_holderStack.last().appendNextProperty(*this, builder))
            RETURN_IF_EXCEPTION(scope, StringifyFailed);
        RETURN_IF_EXCEPTION(scope, StringifyFailed);
        if (UNLIKELY(builder.hasOverflowed()))
            return StringifyFailed;
        m_holderStack.removeLast();
        m_objectStack.removeLast();
        m_objectStack.removeLast();
    } while (!m_holderStack.isEmpty());
    return StringifySucceeded;
}

inline bool Stringifier::willIndent() const
{
    return !m_gap.isEmpty();
}

inline void Stringifier::indent()
{
    // Use a single shared string, m_repeatedGap, so we don't keep allocating new ones as we indent and unindent.
    unsigned newSize = m_indent.length() + m_gap.length();
    if (newSize > m_repeatedGap.length())
        m_repeatedGap = makeString(m_repeatedGap, m_gap);
    ASSERT(newSize <= m_repeatedGap.length());
    m_indent = StringView { m_repeatedGap }.left(newSize);
}

inline void Stringifier::unindent()
{
    ASSERT(m_indent.length() >= m_gap.length());
    m_indent = StringView { m_repeatedGap }.left(m_indent.length() - m_gap.length());
}

inline void Stringifier::startNewLine(StringBuilder& builder) const
{
    if (willIndent())
        builder.append('\n', m_indent);
}

inline Stringifier::Holder::Holder(JSGlobalObject* globalObject, JSObject* object, Structure* structure)
    : m_object(object)
    , m_structure(structure)
    , m_isJSArray(isJSArray(object))
    , m_isArray(JSC::isArray(globalObject, object))
{
}

inline Stringifier::Holder::Holder(RootHolderTag, JSObject* object)
    : m_object(object)
{
}

bool Stringifier::Holder::appendNextProperty(Stringifier& stringifier, StringBuilder& builder)
{
    ASSERT(m_index <= m_size);

    JSGlobalObject* globalObject = stringifier.m_globalObject;
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    // First time through, initialize.
    if (!m_index) {
        if (m_isArray) {
            uint64_t length = toLength(globalObject, m_object);
            RETURN_IF_EXCEPTION(scope, false);
            if (UNLIKELY(length > std::numeric_limits<uint32_t>::max())) {
                throwOutOfMemoryError(globalObject, scope);
                return false;
            }
            m_size = static_cast<uint32_t>(length);
            RETURN_IF_EXCEPTION(scope, false);
            builder.append('[');
        } else {
            if (stringifier.m_usingArrayReplacer) {
                m_propertyNames = stringifier.m_arrayReplacerPropertyNames.data();
                m_size = m_propertyNames->propertyNameVector().size();
            } else if (m_object->structure() == m_structure && canPerformFastPropertyNameEnumerationForJSONStringifyWithSideEffect(m_structure)) {
                m_hasFastObjectProperties = m_structure->canPerformFastPropertyEnumeration();
                m_structure->forEachProperty(vm, [&](const auto& entry) -> bool {
                    if (entry.attributes() & PropertyAttribute::DontEnum)
                        return true;

                    PropertyName propertyName(entry.key());
                    if (propertyName.isSymbol())
                        return true;
                    m_propertiesAndOffsets.constructAndAppend(propertyName, entry.offset());
                    return true;
                });
                m_size = m_propertiesAndOffsets.size();
            } else {
                PropertyNameArray objectPropertyNames(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude);
                m_object->methodTable()->getOwnPropertyNames(m_object, globalObject, objectPropertyNames, DontEnumPropertiesMode::Exclude);
                RETURN_IF_EXCEPTION(scope, false);
                m_propertyNames = objectPropertyNames.releaseData();
                m_size = m_propertyNames->propertyNameVector().size();
            }

            builder.append('{');
        }
        stringifier.indent();
    }
    if (UNLIKELY(builder.hasOverflowed()))
        return false;

    // Last time through, finish up and return false.
    if (m_index == m_size) {
        stringifier.unindent();
        if (m_size && builder[builder.length() - 1] != '{')
            stringifier.startNewLine(builder);
        builder.append(m_isArray ? ']' : '}');
        return false;
    }

    // Handle a single element of the array or object.
    unsigned index = m_index++;
    unsigned rollBackPoint = 0;
    StringifyResult stringifyResult;
    if (m_isArray) {
        // Get the value.
        JSValue value;
        if (m_isJSArray && m_object->canGetIndexQuickly(index))
            value = m_object->getIndexQuickly(index);
        else {
            value = m_object->get(globalObject, index);
            RETURN_IF_EXCEPTION(scope, false);
        }

        // Append the separator string.
        if (index)
            builder.append(',');
        stringifier.startNewLine(builder);

        // Append the stringified value.
        stringifyResult = stringifier.appendStringifiedValue(builder, value, *this, index);
        ASSERT(stringifyResult != StringifyFailedDueToUndefinedOrSymbolValue);
    } else {
        PropertyName propertyName { nullptr };
        JSValue value;
        if (m_hasFastObjectProperties) {
            propertyName = std::get<0>(m_propertiesAndOffsets[index]);
            if (m_object->structureID() == m_structure->id()) {
                unsigned offset = std::get<1>(m_propertiesAndOffsets[index]);
                value = m_object->getDirect(offset);
            } else {
                value = m_object->get(globalObject, propertyName);
                RETURN_IF_EXCEPTION(scope, false);
            }
        } else {
            if (m_propertyNames) {
                propertyName = m_propertyNames->propertyNameVector()[index];
                value = m_object->get(globalObject, propertyName);
                RETURN_IF_EXCEPTION(scope, false);
            } else {
                propertyName = std::get<0>(m_propertiesAndOffsets[index]);
                if (m_object->structureID() == m_structure->id()) {
                    unsigned offset = std::get<1>(m_propertiesAndOffsets[index]);
                    value = m_object->getDirect(offset);
                    if (value.isGetterSetter()) {
                        value = jsCast<GetterSetter*>(value)->callGetter(globalObject, m_object);
                        RETURN_IF_EXCEPTION(scope, false);
                    } else if (value.isCustomGetterSetter()) {
                        value = m_object->get(globalObject, propertyName);
                        RETURN_IF_EXCEPTION(scope, false);
                    }
                } else {
                    value = m_object->get(globalObject, propertyName);
                    RETURN_IF_EXCEPTION(scope, false);
                }
            }
        }

        rollBackPoint = builder.length();

        // Append the separator string.
        if (builder[rollBackPoint - 1] != '{')
            builder.append(',');
        stringifier.startNewLine(builder);

        // Append the property name, colon, and space.
        builder.appendQuotedJSONString(*propertyName.uid());
        builder.append(':');
        if (stringifier.willIndent())
            builder.append(' ');

        // Append the stringified value.
        stringifyResult = stringifier.appendStringifiedValue(builder, value, *this, propertyName);
    }
    RETURN_IF_EXCEPTION(scope, false);

    // From this point on, no access to the this pointer or to any members, because the
    // Holder object may have moved if the call to stringify pushed a new Holder onto
    // m_holderStack.

    switch (stringifyResult) {
        case StringifyFailed:
            builder.append("null"_s);
            break;
        case StringifySucceeded:
            break;
        case StringifyFailedDueToUndefinedOrSymbolValue:
            // This only occurs when we get an undefined value or a symbol value for
            // an object property. In this case we don't want the separator and
            // property name that we already appended, so roll back.
            builder.shrink(rollBackPoint);
            break;
    }

    return true;
}

// ------------------------------ FastStringifier --------------------------------

// FastStringifier does a no-side-effects stringify of the most common types of
// objects and arrays. It bails out if the serialization is any longer than a
// fixed buffer and handles only the simplest cases, including only 8-bit character
// strings. Instead of explicit checks to prevent excessive recursion and cycles,
// it counts on hitting the buffer size limit to catch those things. If it fails,
// since there is no side effect, the full general purpose Stringifier can be used
// and the only cost of the fast stringifying attempt is the time wasted.

enum class BufferMode : uint8_t {
    StaticBuffer,
    DynamicBuffer,
};

enum class FailureReason : uint8_t {
    BufferFull,
    Found16BitEarly,
    Found16BitLate,
    StackOverflow,
    Unknown,
};

template<typename CharType, BufferMode bufferMode>
class FastStringifier {
public:
    // Returns null string if the fast case fails.
    static String stringify(JSGlobalObject&, JSValue, JSValue replacer, JSValue space, std::optional<FailureReason>&);

    static constexpr unsigned staticBufferSize = bufferMode == BufferMode::StaticBuffer ? 8192 : 8;
    static constexpr unsigned dynamicBufferInlineCapacity = bufferMode == BufferMode::StaticBuffer ? 0 : 1024;

private:
    explicit FastStringifier(JSGlobalObject&);
    void append(JSValue);
    String result() const;

    void append(char, char, char, char);
    void append(char, char, char, char, char);
    template<typename T> void recordFailure(FailureReason, T&& reason);
    template<typename T> void recordFailure(T&& reason)
    {
        recordFailure(FailureReason::Unknown, std::forward<T>(reason));
    }
    void recordBufferFull();
    String firstGetterSetterPropertyName(JSObject&) const;
    void recordFastPropertyEnumerationFailure(JSObject&);
    bool haveFailure() const;
    bool hasRemainingCapacity(unsigned size = 1);
    bool hasRemainingCapacitySlow(unsigned size);
    bool mayHaveToJSON(JSObject&) const;

    static void logOutcome(ASCIILiteral);
    static void logOutcome(String&&);

    static unsigned usableBufferSize(unsigned availableBufferSize);

    CharType* buffer();
    const CharType* buffer() const;
    std::span<CharType> bufferSpan();

    JSGlobalObject& m_globalObject;
    VM& m_vm;
    unsigned m_length { 0 }; // length of content already filled into m_buffer.
    unsigned m_capacity { 0 };
    bool m_checkedObjectPrototype { false };
    bool m_checkedArrayPrototype { false };
    std::optional<FailureReason> m_failureReason;
    Vector<CharType, dynamicBufferInlineCapacity> m_dynamicBuffer;
    uint8_t* m_stackLimit { nullptr };

    CharType m_buffer[staticBufferSize];
};

#if !FAST_STRINGIFY_LOG_USAGE

template<typename CharType, BufferMode bufferMode>
inline void FastStringifier<CharType, bufferMode>::logOutcome(ASCIILiteral)
{
}

#else

static void logOutcomeImpl(String&& outcome)
{
    static NeverDestroyed<HashCountedSet<String>> set;
    static std::atomic<unsigned> count;
    set->add(outcome);
    if (!(++count % 100)) {
        Vector<KeyValuePair<String, unsigned>> vector;
        for (auto& pair : set.get())
            vector.append(pair);
        std::sort(vector.begin(), vector.end(), [](auto& a, auto &b) {
            return a.value != b.value ? a.value > b.value : codePointCompareLessThan(a.key, b.key);
        });
        dataLogLn("fastStringify outcomes");
        for (auto& pair : vector) {
            dataLogF("%5u", pair.value);
            dataLogLn(": ", pair.key);
        }
    }
}

template<typename CharType, BufferMode bufferMode>
void FastStringifier<CharType, bufferMode>::logOutcome(ASCIILiteral outcome)
{
    logOutcomeImpl(String { outcome });
}

template<typename CharType, BufferMode bufferMode>
void FastStringifier<CharType, bufferMode>::logOutcome(String&& outcome)
{
    logOutcomeImpl(WTFMove(outcome));
}

#endif

template<typename CharType, BufferMode bufferMode>
ALWAYS_INLINE CharType* FastStringifier<CharType, bufferMode>::buffer()
{
    if constexpr (bufferMode == BufferMode::StaticBuffer)
        return m_buffer;
    else
        return m_dynamicBuffer.data();
}

template<typename CharType, BufferMode bufferMode>
ALWAYS_INLINE const CharType* FastStringifier<CharType, bufferMode>::buffer() const
{
    if constexpr (bufferMode == BufferMode::StaticBuffer)
        return m_buffer;
    else
        return m_dynamicBuffer.data();
}

template<typename CharType, BufferMode bufferMode>
ALWAYS_INLINE std::span<CharType> FastStringifier<CharType, bufferMode>::bufferSpan()
{
    if constexpr (bufferMode == BufferMode::StaticBuffer)
        return std::span<CharType> { m_buffer };
    else
        return m_dynamicBuffer.mutableSpan();
}

template<typename CharType, BufferMode bufferMode>
inline unsigned FastStringifier<CharType, bufferMode>::usableBufferSize(unsigned availableBufferSize)
{
    // FastStringifier relies on m_capacity (i.e. the remaining usable capacity) in m_buffer
    // to limit recursion. Hence, we need to compute an appropriate m_capacity value.
    //
    // To do this, we empirically measured the worst case stack usage incurred by 1 recursion
    // of any of the append methods. Assuming each call to append() only consumes 1 LChar in
    // m_buffer, the amount of buffer size that FastStringifier is allowed to run with can be
    // estimated as:
    //
    //      stackCapacityForRecursion = remainingStackCapacity - maxLeafFunctionStackUsage
    //      maxAllowedBufferSize = stackCapacityForRecursion / maxRecursionFrameSize
    //      usableBufferSize = min(maxAllowedBufferSize, sizeof(m_buffer))
    //
    // 1. A leaf function is any function that append() calls which does not recurse.
    //    At peak recursion, there needs to be enough room left on the stack to execute any
    //    of these leaf functions i.e. maxLeafFunctionStackUsage.
    //
    //    We estimate maxLeafFunctionStackUsage to be StackBounds::DefaultReservedZone.
    //    stack.recursionLimit() already adds DefaultReservedZone to the bottom of the stack.
    //    Hence, using stack.recursionLimit() to compute stackCapacityForRecursion will leave
    //    us with the needed stack space for leaf functions to execute.
    //
    // 2. We can compute m_capacity as:
    //
    //      m_capacity = m_length + usableBufferSize
    //
    //    where m_length is the position of the next usable character for emission in m_buffer.
    //
    // 3. This calculation of m_capacity is a best effort estimate. If we're not
    //    conservative enough and get it wrong, the worst that can happen is that we'll
    //    crash when recursion causes us to step on the stack guard page at the bottom of
    //    the stack. The goal of trying to estimate a good m_capacity value is to avoid
    //    this stack overflow crash.
    //
    //    Note that for a Release build, maxRecursionFrameSize is measured to be less than
    //    384 bytes. This is well below stack guard page sizes which are between 4 and 16K
    //    depending on the OS. Hence, recursing too deeply with FastStringifier::append()
    //    is guaranteed to crash in the stack guard page.
    //
    // 4. If we're too conservative, we might fail out of FastStringifier too eagerly.
    //    In this case, we'll just fall back to the slow path Stringifier. The only down
    //    side here is potential loss of some performance opportunity when we encounter
    //    a workload that recurses deeply. We expect such workloads to be rare.

    auto& stack = Thread::current().stack();
    uint8_t* stackPointer = std::bit_cast<uint8_t*>(currentStackPointer());
    uint8_t* stackLimit = std::bit_cast<uint8_t*>(stack.recursionLimit());
    size_t stackCapacityForRecursion = stackPointer - stackLimit;

#if ASAN_ENABLED
    // Measured to be ~4608 for a Debug ASAN build on arm64E, rounding up to 5K for margin.
    constexpr size_t maxRecursionFrameSize = 5 * KB;
#elif !defined(NDEBUG)
    // Measured to be ~912 for a Debug build on arm64E, rounding up to 1280 for margin.
    constexpr size_t maxRecursionFrameSize = 1280;
#else
    // Measured to be ~224 for a Release build on arm64E, rounding up to 384 for margin.
    constexpr size_t maxRecursionFrameSize = 384;
#endif
    ASSERT(static_cast<unsigned>(stackCapacityForRecursion) == stackCapacityForRecursion);
    unsigned allowedBufferSize = stackCapacityForRecursion / maxRecursionFrameSize;
    unsigned usableBufferSize = std::min(allowedBufferSize, availableBufferSize);
    return usableBufferSize;
}

template<typename CharType, BufferMode bufferMode>
inline FastStringifier<CharType, bufferMode>::FastStringifier(JSGlobalObject& globalObject)
    : m_globalObject(globalObject)
    , m_vm(globalObject.vm())
{
    if constexpr (bufferMode == BufferMode::StaticBuffer)
        m_capacity = m_length + usableBufferSize(staticBufferSize);
    else {
        m_dynamicBuffer.grow(dynamicBufferInlineCapacity);
        m_capacity = dynamicBufferInlineCapacity;
        m_stackLimit = std::bit_cast<uint8_t*>(m_vm.softStackLimit());
    }
}

template<typename CharType, BufferMode bufferMode>
inline bool FastStringifier<CharType, bufferMode>::haveFailure() const
{
    return !!m_failureReason;
}

template<typename CharType, BufferMode bufferMode>
inline String FastStringifier<CharType, bufferMode>::result() const
{
    if (haveFailure())
        return { };
#if FAST_STRINGIFY_LOG_USAGE
    static std::atomic<unsigned> maxSizeSeen;
    if (m_length > maxSizeSeen) {
        maxSizeSeen = m_length;
        dataLogLn("max fastStringify buffer size used: ", m_length);
    }
    logOutcome("success"_s);
#endif
    return std::span { buffer(), m_length };
}

template<typename CharType, BufferMode bufferMode>
template<typename T> inline void FastStringifier<CharType, bufferMode>::recordFailure(FailureReason failureReason, T&& reason)
{
    if (!haveFailure())
        logOutcome(std::forward<T>(reason));
    m_failureReason = failureReason;
}

template<typename CharType, BufferMode bufferMode>
inline void FastStringifier<CharType, bufferMode>::recordBufferFull()
{
    recordFailure(FailureReason::BufferFull, "buffer full"_s);
}

template<typename CharType, BufferMode bufferMode>
ALWAYS_INLINE bool FastStringifier<CharType, bufferMode>::hasRemainingCapacity(unsigned size)
{
    ASSERT(!haveFailure());
    ASSERT(size > 0);
    unsigned remainingCapacity = m_capacity - m_length;
    if (size <= remainingCapacity)
        return true;
    return hasRemainingCapacitySlow(size);
}

template<typename CharType, BufferMode bufferMode>
bool FastStringifier<CharType, bufferMode>::hasRemainingCapacitySlow(unsigned size)
{
    ASSERT(!haveFailure());
    if constexpr (bufferMode == BufferMode::StaticBuffer) {
        unsigned unusedBufferSize = staticBufferSize - m_length;
        unsigned usableSize = usableBufferSize(unusedBufferSize);
        if (usableSize < size)
            return false;

        m_capacity = m_length + usableSize;
        ASSERT(m_capacity - m_length >= size);
        return true;
    } else {
        size_t newSize = std::max<size_t>(m_dynamicBuffer.size() * 2, m_dynamicBuffer.size() + size);
        if (UNLIKELY(newSize > StringImpl::MaxLength))
            return false;

        if (UNLIKELY(!m_dynamicBuffer.tryGrow(newSize)))
            return false;

        m_capacity = m_dynamicBuffer.size();
        ASSERT(m_capacity - m_length >= size);
        return true;
    }
}

#if !FAST_STRINGIFY_LOG_USAGE

template<typename CharType, BufferMode bufferMode>
inline void FastStringifier<CharType, bufferMode>::recordFastPropertyEnumerationFailure(JSObject&)
{
    recordFailure("!canPerformFastPropertyEnumerationForJSONStringify"_s);
}

#else

template<typename CharType, BufferMode bufferMode>
String FastStringifier<CharType, bufferMode>::firstGetterSetterPropertyName(JSObject& object) const
{
    auto scope = DECLARE_THROW_SCOPE(m_vm);
    PropertyNameArray names(m_vm, PropertyNameMode::Strings, PrivateSymbolMode::Include);
    JSObject::getOwnPropertyNames(&object, &m_globalObject, names, DontEnumPropertiesMode::Include);
    CLEAR_AND_RETURN_IF_EXCEPTION(scope, "getOwnPropertyNames exception occurred"_s);
    for (auto& name : names) {
        PropertySlot slot(&object, PropertySlot::InternalMethodType::Get);
        JSObject::getOwnPropertySlot(&object, &m_globalObject, name, slot);
        CLEAR_AND_RETURN_IF_EXCEPTION(scope, "getOwnPropertySlot exception occurred"_s);
        if (slot.isAccessor())
            RELEASE_AND_RETURN(scope, name.string());
    }
    RELEASE_AND_RETURN(scope, "not found"_s);
}

template<typename CharType, BufferMode bufferMode>
void FastStringifier<CharType, bufferMode>::recordFastPropertyEnumerationFailure(JSObject& object)
{
    auto& structure = *object.structure();
    if (structure.typeInfo().overridesGetOwnPropertySlot())
        recordFailure("overridesGetOwnPropertySlot"_s);
    else if (structure.typeInfo().overridesAnyFormOfGetOwnPropertyNames())
        recordFailure("overridesAnyFormOfGetOwnPropertyNames"_s);
    else if (hasIndexedProperties(structure.indexingType()))
        recordFailure("hasIndexedProperties"_s);
    else if (structure.hasAnyKindOfGetterSetterProperties())
        recordFailure("getter/setter: "_s + firstGetterSetterPropertyName(object));
    else if (structure.hasReadOnlyOrGetterSetterPropertiesExcludingProto())
        recordFailure("hasReadOnlyOrGetterSetterPropertiesExcludingProto"_s);
    else if (structure.isUncacheableDictionary())
        recordFailure("isUncacheableDictionary"_s);
    else if (structure.hasUnderscoreProtoPropertyExcludingOriginalProto())
        recordFailure("hasUnderscoreProtoPropertyExcludingOriginalProto"_s);
    else
        recordFailure("!canPerformFastPropertyEnumerationForJSONStringify mystery"_s);
}

#endif

template<typename CharType, BufferMode bufferMode>
inline bool FastStringifier<CharType, bufferMode>::mayHaveToJSON(JSObject& object) const
{
    if (auto function = object.structure()->cachedSpecialProperty(CachedSpecialPropertyKey::ToJSON))
        return !function.isUndefined();
    if (UNLIKELY(object.noSideEffectMayHaveNonIndexProperty(m_vm, m_vm.propertyNames->toJSON))) {
        // Getting the property value so we can cache it could cause side effects; instead return true without caching anything.
        return true;
    }
    // Cache this so we can answer false next time without redoing the noSideEffectMayHaveNonIndexProperty work.
    PropertySlot slot { &object, PropertySlot::InternalMethodType::Get };
    object.structure()->cacheSpecialProperty(&m_globalObject, m_vm, jsUndefined(), CachedSpecialPropertyKey::ToJSON, slot);
    return false;
}

template<typename CharType, BufferMode bufferMode>
inline void FastStringifier<CharType, bufferMode>::append(char a, char b, char c, char d)
{
    if (UNLIKELY(!hasRemainingCapacity(4))) {
        recordBufferFull();
        return;
    }
    buffer()[m_length] = a;
    buffer()[m_length + 1] = b;
    buffer()[m_length + 2] = c;
    buffer()[m_length + 3] = d;
    m_length += 4;
}

template<typename CharType, BufferMode bufferMode>
inline void FastStringifier<CharType, bufferMode>::append(char a, char b, char c, char d, char e)
{
    if (UNLIKELY(!hasRemainingCapacity(5))) {
        recordBufferFull();
        return;
    }
    buffer()[m_length] = a;
    buffer()[m_length + 1] = b;
    buffer()[m_length + 2] = c;
    buffer()[m_length + 3] = d;
    buffer()[m_length + 4] = e;
    m_length += 5;
}

template<typename CharType>
static ALWAYS_INLINE bool stringCopySameType(std::span<const CharType> span, CharType* cursor)
{
#if (CPU(ARM64) || CPU(X86_64)) && COMPILER(CLANG)
    constexpr size_t stride = SIMD::stride<CharType>;
    if (span.size() >= stride) {
        using UnsignedType = std::make_unsigned_t<CharType>;
        using BulkType = decltype(SIMD::load(static_cast<const UnsignedType*>(nullptr)));
        constexpr auto quoteMask = SIMD::splat<UnsignedType>('"');
        constexpr auto escapeMask = SIMD::splat<UnsignedType>('\\');
        constexpr auto controlMask = SIMD::splat<UnsignedType>(' ');
        const auto* ptr = span.data();
        const auto* end = ptr + span.size();
        auto* cursorEnd = cursor + span.size();
        BulkType accumulated { };
        for (; ptr + (stride - 1) < end; ptr += stride, cursor += stride) {
            auto input = SIMD::load(std::bit_cast<const UnsignedType*>(ptr));
            SIMD::store(input, std::bit_cast<UnsignedType*>(cursor));
            auto quotes = SIMD::equal(input, quoteMask);
            auto escapes = SIMD::equal(input, escapeMask);
            auto controls = SIMD::lessThan(input, controlMask);
            accumulated = SIMD::bitOr(accumulated, quotes, escapes, controls);
            if constexpr (sizeof(CharType) != 1) {
                constexpr auto surrogateMask = SIMD::splat<UnsignedType>(0xf800);
                constexpr auto surrogateCheckMask = SIMD::splat<UnsignedType>(0xd800);
                accumulated = SIMD::bitOr(accumulated, SIMD::equal(SIMD::bitAnd(input, surrogateMask), surrogateCheckMask));
            }
        }
        if (ptr < end) {
            auto input = SIMD::load(std::bit_cast<const UnsignedType*>(end - stride));
            SIMD::store(input, std::bit_cast<UnsignedType*>(cursorEnd - stride));
            auto quotes = SIMD::equal(input, quoteMask);
            auto escapes = SIMD::equal(input, escapeMask);
            auto controls = SIMD::lessThan(input, controlMask);
            accumulated = SIMD::bitOr(accumulated, quotes, escapes, controls);
            if constexpr (sizeof(CharType) != 1) {
                constexpr auto surrogateMask = SIMD::splat<UnsignedType>(0xf800);
                constexpr auto surrogateCheckMask = SIMD::splat<UnsignedType>(0xd800);
                accumulated = SIMD::bitOr(accumulated, SIMD::equal(SIMD::bitAnd(input, surrogateMask), surrogateCheckMask));
            }
        }
        return SIMD::isNonZero(accumulated);
    }
#endif
    for (auto character : span) {
        if constexpr (sizeof(CharType) != 1) {
            if (UNLIKELY(U16_IS_SURROGATE(character)))
                return true;
        }
        if (UNLIKELY(character <= 0xff && WTF::escapedFormsForJSON[character]))
            return true;
        *cursor++ = character;
    }
    return false;
}

static ALWAYS_INLINE bool stringCopyUpconvert(std::span<const LChar> span, UChar* cursor)
{
#if (CPU(ARM64) || CPU(X86_64)) && COMPILER(CLANG)
    constexpr size_t stride = SIMD::stride<LChar>;
    if (span.size() >= stride) {
        using UnsignedType = std::make_unsigned_t<LChar>;
        using BulkType = decltype(SIMD::load(static_cast<const UnsignedType*>(nullptr)));
        constexpr auto quoteMask = SIMD::splat<UnsignedType>('"');
        constexpr auto escapeMask = SIMD::splat<UnsignedType>('\\');
        constexpr auto controlMask = SIMD::splat<UnsignedType>(' ');
        constexpr auto zeros = SIMD::splat<UnsignedType>(0);
        const auto* ptr = span.data();
        const auto* end = ptr + span.size();
        auto* cursorEnd = cursor + span.size();
        BulkType accumulated { };
        for (; ptr + (stride - 1) < end; ptr += stride, cursor += stride) {
            auto input = SIMD::load(std::bit_cast<const UnsignedType*>(ptr));
            simde_vst2q_u8(std::bit_cast<UnsignedType*>(cursor), (simde_uint8x16x2_t { input, zeros }));
            auto quotes = SIMD::equal(input, quoteMask);
            auto escapes = SIMD::equal(input, escapeMask);
            auto controls = SIMD::lessThan(input, controlMask);
            accumulated = SIMD::bitOr(accumulated, quotes, escapes, controls);
        }
        if (ptr < end) {
            auto input = SIMD::load(std::bit_cast<const UnsignedType*>(end - stride));
            simde_vst2q_u8(std::bit_cast<UnsignedType*>(cursorEnd - stride), (simde_uint8x16x2_t { input, zeros }));
            auto quotes = SIMD::equal(input, quoteMask);
            auto escapes = SIMD::equal(input, escapeMask);
            auto controls = SIMD::lessThan(input, controlMask);
            accumulated = SIMD::bitOr(accumulated, quotes, escapes, controls);
        }
        return SIMD::isNonZero(accumulated);
    }
#endif
    for (auto character : span) {
        if (UNLIKELY(WTF::escapedFormsForJSON[character]))
            return true;
        *cursor++ = character;
    }
    return false;
}

template<typename CharType, BufferMode bufferMode>
void FastStringifier<CharType, bufferMode>::append(JSValue value)
{
    if constexpr (bufferMode == BufferMode::DynamicBuffer) {
        if (UNLIKELY(std::bit_cast<uint8_t*>(currentStackPointer()) < m_stackLimit)) {
            recordFailure(FailureReason::StackOverflow, "stack overflow"_s);
            return;
        }
    }

    if (value.isNull()) {
        append('n', 'u', 'l', 'l');
        return;
    }

    if (value.isTrue()) {
        append('t', 'r', 'u', 'e');
        return;
    }

    if (value.isFalse()) {
        append('f', 'a', 'l', 's', 'e');
        return;
    }

    if (value.isInt32()) {
        auto number = value.asInt32();
        constexpr unsigned maxInt32StringLength = 11; // -INT32_MIN, "-2147483648".
        if (UNLIKELY(!hasRemainingCapacity(maxInt32StringLength))) {
            recordBufferFull();
            return;
        }
        if constexpr (sizeof(CharType) == 1) {
            char* cursor = std::bit_cast<char*>(buffer()) + m_length;
            auto result = std::to_chars(cursor, cursor + maxInt32StringLength, number);
            ASSERT(result.ec != std::errc::value_too_large);
            m_length += result.ptr - cursor;
        } else {
            std::array<char, maxInt32StringLength> temporary;
            auto result = std::to_chars(temporary.data(), temporary.data() + maxInt32StringLength, number);
            ASSERT(result.ec != std::errc::value_too_large);
            unsigned lengthToCopy = result.ptr - temporary.data();
            WTF::copyElements(std::bit_cast<uint16_t*>(buffer() + m_length), std::bit_cast<const uint8_t*>(temporary.data()), lengthToCopy);
            m_length += lengthToCopy;
        }
        return;
    }

    if (value.isDouble()) {
        auto number = value.asDouble();
        if (!std::isfinite(number)) {
            append('n', 'u', 'l', 'l');
            return;
        }
        if (UNLIKELY(!hasRemainingCapacity(WTF::dragonbox::max_string_length<WTF::dragonbox::ieee754_binary64>()))) {
            recordBufferFull();
            return;
        }
        if constexpr (sizeof(CharType) == 1) {
            const char* cursor = WTF::dragonbox::detail::to_chars_n<WTF::dragonbox::Mode::ToShortest>(number, reinterpret_cast<char*>(buffer() + m_length));
            m_length = cursor - reinterpret_cast<char*>(buffer());
        } else {
            std::array<char, WTF::dragonbox::max_string_length<WTF::dragonbox::ieee754_binary64>()> temporary;
            const char* cursor = WTF::dragonbox::detail::to_chars_n<WTF::dragonbox::Mode::ToShortest>(number, temporary.data());
            size_t length = cursor - temporary.data();
            WTF::copyElements(std::bit_cast<uint16_t*>(buffer() + m_length), std::bit_cast<const uint8_t*>(temporary.data()), length);
            m_length += length;
        }
        return;
    }

    if (UNLIKELY(!value.isCell())) {
        recordFailure("value type"_s);
        return;
    }
    auto& cell = *value.asCell();

    switch (cell.type()) {
    case StringType: {
        auto string = asString(&cell)->tryGetValue();
        if (UNLIKELY(string.data.isNull())) {
            recordFailure("String::tryGetValue"_s);
            return;
        }

        auto stringLength = string.data.length();
        if constexpr (sizeof(CharType) == 1) {
            if (UNLIKELY(!string.data.is8Bit())) {
                if constexpr (bufferMode == BufferMode::DynamicBuffer)
                    recordFailure(FailureReason::Unknown, "16-bit string"_s);
                else
                    recordFailure(m_length < (m_capacity / 2) ? FailureReason::Found16BitEarly : FailureReason::Found16BitLate, "16-bit string"_s);
                return;
            }
            if (UNLIKELY(!hasRemainingCapacity(1 + stringLength + 1))) {
                recordBufferFull();
                return;
            }
            buffer()[m_length] = '"';
            if (LIKELY(!stringCopySameType(string.data.span8(), buffer() + m_length + 1))) {
                buffer()[m_length + 1 + stringLength] = '"';
                m_length += 1 + stringLength + 1;
                return;
            }
        } else {
            if (UNLIKELY(!hasRemainingCapacity(1 + stringLength + 1))) {
                recordBufferFull();
                return;
            }
            buffer()[m_length] = '"';
            if (string.data.is8Bit()) {
                if (LIKELY(!stringCopyUpconvert(string.data.span8(), buffer() + m_length + 1))) {
                    buffer()[m_length + 1 + stringLength] = '"';
                    m_length += 1 + stringLength + 1;
                    return;
                }
            } else {
                if (LIKELY(!stringCopySameType(string.data.span16(), buffer() + m_length + 1))) {
                    buffer()[m_length + 1 + stringLength] = '"';
                    m_length += 1 + stringLength + 1;
                    return;
                }
            }
        }

        if (UNLIKELY(!hasRemainingCapacity(1 + static_cast<size_t>(stringLength) * 6 + 1))) {
            recordBufferFull();
            return;
        }
        auto output = bufferSpan().subspan(m_length + 1);
        if constexpr (sizeof(CharType) == 2) {
            if (string.data.is8Bit())
                WTF::appendEscapedJSONStringContent(output, string.data.span8());
            else
                WTF::appendEscapedJSONStringContent(output, string.data.span16());
        } else
            WTF::appendEscapedJSONStringContent(output, string.data.span8());
        output[0] = '"';
        output = output.subspan(1);
        m_length = output.data() - buffer();
        return;
    }

    case ObjectType:
    case FinalObjectType: {
        auto& object = *asObject(&cell);
        if (UNLIKELY(object.isCallable())) {
            recordFailure("callable object"_s);
            return;
        }
        auto& structure = *object.structure();
        if (UNLIKELY(structure.hasPolyProto())) {
            recordFailure("hasPolyProto"_s);
            return;
        }
        if (UNLIKELY(structure.storedPrototype() != m_globalObject.objectPrototype())) {
            recordFailure("non-standard object prototype"_s);
            return;
        }
        if (!m_checkedObjectPrototype) {
            if (UNLIKELY(mayHaveToJSON(*m_globalObject.objectPrototype()))) {
                recordFailure("object prototype may have toJSON"_s);
                return;
            }
            m_checkedObjectPrototype = true;
        }
        if (UNLIKELY(!hasRemainingCapacity())) {
            recordBufferFull();
            return;
        }
        buffer()[m_length++] = '{';
        if (UNLIKELY(!structure.canPerformFastPropertyEnumeration())) {
            recordFastPropertyEnumerationFailure(object);
            return;
        }
        structure.forEachProperty(m_vm, [&](const auto& entry) -> bool {
            if (entry.attributes() & PropertyAttribute::DontEnum)
                return true;
            auto& name = *entry.key();
            if (UNLIKELY(name.isSymbol())) {
                recordFailure("symbol"_s);
                return false;
            }

            // Right now, we do not support 16-bit name here since name in 16-bit is significantly more rare than 16-bit string.
            if (UNLIKELY(!name.is8Bit())) {
                recordFailure("16-bit property name"_s);
                return false;
            }
            auto span = name.span8();

            if (UNLIKELY(object.structure() != &structure)) {
                ASSERT_NOT_REACHED();
                recordFailure("unexpected structure transition"_s);
                return false;
            }
            JSValue value = object.getDirect(entry.offset());
            if (value.isUndefined())
                return true;

            bool needComma = buffer()[m_length - 1] != '{';
            if (UNLIKELY(!hasRemainingCapacity(needComma + 1 + span.size() + 2))) {
                recordBufferFull();
                return false;
            }
            if (needComma)
                buffer()[m_length++] = ',';
            buffer()[m_length] = '"';

            if constexpr (sizeof(CharType) == 2) {
                if (UNLIKELY(stringCopyUpconvert(span, buffer() + m_length + 1))) {
                    recordFailure("property name character needs escaping"_s);
                    return false;
                }
            } else {
                if (UNLIKELY(stringCopySameType(span, buffer() + m_length + 1))) {
                    recordFailure("property name character needs escaping"_s);
                    return false;
                }
            }

            buffer()[m_length + 1 + span.size()] = '"';
            buffer()[m_length + 1 + span.size() + 1] = ':';
            m_length += 1 + span.size() + 2;
            append(value);
            return !haveFailure();
        });
        if (UNLIKELY(haveFailure()))
            return;
        if (UNLIKELY(!hasRemainingCapacity())) {
            recordBufferFull();
            return;
        }
        buffer()[m_length++] = '}';
        return;
    }

    case ArrayType: {
        auto& array = *asArray(&cell);
        if (!m_checkedArrayPrototype) {
            if (UNLIKELY(mayHaveToJSON(*m_globalObject.arrayPrototype()))) {
                recordFailure("array prototype may have toJSON"_s);
                return;
            }
            m_checkedArrayPrototype = true;
        }
        auto& structure = *array.structure();
        if (UNLIKELY(!m_globalObject.isOriginalArrayStructure(&structure))) {
            structure.forEachProperty(m_vm, [&](const PropertyTableEntry& entry) -> bool {
                if (UNLIKELY(entry.key() == m_vm.propertyNames->toJSON)) {
                    recordFailure("array has toJSON"_s);
                    return false;
                }
                return true;
            });
            if (haveFailure())
                return;
        }
        if (UNLIKELY(!hasRemainingCapacity())) {
            recordBufferFull();
            return;
        }
        buffer()[m_length++] = '[';
        for (unsigned i = 0, length = array.length(); i < length; ++i) {
            if (i) {
                if (UNLIKELY(!hasRemainingCapacity())) {
                    recordBufferFull();
                    return;
                }
                buffer()[m_length++] = ',';
            }
            if (UNLIKELY(!array.canGetIndexQuickly(i))) {
                recordFailure("!canGetIndexQuickly"_s);
                return;
            }
            append(array.getIndexQuickly(i));
            if (UNLIKELY(haveFailure()))
                return;
        }
        if (UNLIKELY(!hasRemainingCapacity())) {
            recordBufferFull();
            return;
        }
        buffer()[m_length++] = ']';
        return;
    }

    case JSFunctionType:
        recordFailure("function"_s);
        return;

    default:
        recordFailure("object type"_s);
    }
}

template<typename CharType, BufferMode bufferMode>
inline String FastStringifier<CharType, bufferMode>::stringify(JSGlobalObject& globalObject, JSValue value, JSValue replacer, JSValue space, std::optional<FailureReason>& failureReason)
{
    if (replacer.isObject()) {
        logOutcome("replacer"_s);
        return { };
    }
    if (!space.isUndefined()) {
        logOutcome("space"_s);
        return { };
    }
    FastStringifier stringifier(globalObject);
    stringifier.append(value);
    failureReason = stringifier.m_failureReason;
    return stringifier.result();
}

static NEVER_INLINE String stringify(JSGlobalObject& globalObject, JSValue value, JSValue replacer, JSValue space)
{
    VM& vm = globalObject.vm();
    uint8_t* stackLimit = std::bit_cast<uint8_t*>(vm.softStackLimit());
    if (LIKELY(std::bit_cast<uint8_t*>(currentStackPointer()) >= stackLimit)) {
        std::optional<FailureReason> failureReason;
        failureReason = std::nullopt;
        if (String result = FastStringifier<LChar, BufferMode::StaticBuffer>::stringify(globalObject, value, replacer, space, failureReason); !result.isNull())
            return result;
        if (failureReason == FailureReason::Found16BitEarly) {
            failureReason = std::nullopt;
            if (String result = FastStringifier<UChar, BufferMode::StaticBuffer>::stringify(globalObject, value, replacer, space, failureReason); !result.isNull())
                return result;

            if (failureReason == FailureReason::BufferFull) {
                failureReason = std::nullopt;
                if (String result = FastStringifier<UChar, BufferMode::DynamicBuffer>::stringify(globalObject, value, replacer, space, failureReason); !result.isNull())
                    return result;
            }
        } else if (failureReason == FailureReason::BufferFull) {
            failureReason = std::nullopt;
            if (String result = FastStringifier<LChar, BufferMode::DynamicBuffer>::stringify(globalObject, value, replacer, space, failureReason); !result.isNull())
                return result;
        }
    }
    String result = Stringifier::stringify(globalObject, value, replacer, space);
#if FAST_STRINGIFY_LOG_USAGE
    if (!result.isNull())
        dataLogLn("Not fastStringify: ", result);
#endif
    return result;
}

// ------------------------------ JSONObject --------------------------------

const ClassInfo JSONObject::s_info = { "JSON"_s, &JSNonFinalObject::s_info, &jsonTable, nullptr, CREATE_METHOD_TABLE(JSONObject) };

/* Source for JSONObject.lut.h
@begin jsonTable
  parse         jsonProtoFuncParse             DontEnum|Function 2
  stringify     jsonProtoFuncStringify         DontEnum|Function 3
@end
*/

// ECMA 15.8

class Walker {
    WTF_MAKE_NONCOPYABLE(Walker);
    WTF_FORBID_HEAP_ALLOCATION;
public:
    Walker(JSGlobalObject* globalObject, JSObject* function, CallData callData)
        : m_globalObject(globalObject)
        , m_function(function)
        , m_callData(callData)
    {
    }
    JSValue walk(JSValue unfiltered);
private:
    JSValue callReviver(JSObject* thisObj, JSValue property, JSValue unfiltered)
    {
        MarkedArgumentBuffer args;
        args.append(property);
        args.append(unfiltered);
        ASSERT(!args.hasOverflowed());
        return call(m_globalObject, m_function, m_callData, thisObj, args);
    }

    friend class Holder;

    JSGlobalObject* m_globalObject;
    JSObject* m_function;
    CallData m_callData;
};

enum WalkerState { StateUnknown, ArrayStartState, ArrayStartVisitMember, ArrayEndVisitMember, 
                                 ObjectStartState, ObjectStartVisitMember, ObjectEndVisitMember };
NEVER_INLINE JSValue Walker::walk(JSValue unfiltered)
{
    VM& vm = m_globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    Vector<PropertyNameArray, 16, UnsafeVectorOverflow> propertyStack;
    Vector<uint32_t, 16, UnsafeVectorOverflow> indexStack;
    MarkedArgumentBuffer markedStack;
    Vector<unsigned, 16, UnsafeVectorOverflow> arrayLengthStack;
    
    Vector<WalkerState, 16, UnsafeVectorOverflow> stateStack;
    WalkerState state = StateUnknown;
    JSValue inValue = unfiltered;
    JSValue outValue = jsNull();
    
    while (1) {
        switch (state) {
            arrayStartState:
            case ArrayStartState: {
                ASSERT(inValue.isObject());
                ASSERT(isArray(m_globalObject, inValue));
                EXCEPTION_ASSERT(!scope.exception());

                if (UNLIKELY(markedStack.size() >= maximumSideStackRecursion))
                    return throwStackOverflowError(m_globalObject, scope);

                JSObject* array = asObject(inValue);
                markedStack.appendWithCrashOnOverflow(array);
                uint64_t length = toLength(m_globalObject, array);
                RETURN_IF_EXCEPTION(scope, { });
                if (UNLIKELY(length > std::numeric_limits<uint32_t>::max())) {
                    throwOutOfMemoryError(m_globalObject, scope);
                    return { };
                }
                RETURN_IF_EXCEPTION(scope, { });
                arrayLengthStack.append(static_cast<uint32_t>(length));
                indexStack.append(0);
            }
            arrayStartVisitMember:
            FALLTHROUGH;
            case ArrayStartVisitMember: {
                JSObject* array = asObject(markedStack.last());
                uint32_t index = indexStack.last();
                unsigned arrayLength = arrayLengthStack.last();
                if (index == arrayLength) {
                    outValue = array;
                    markedStack.removeLast();
                    arrayLengthStack.removeLast();
                    indexStack.removeLast();
                    break;
                }
                if (isJSArray(array) && array->canGetIndexQuickly(index))
                    inValue = array->getIndexQuickly(index);
                else {
                    inValue = array->get(m_globalObject, index);
                    RETURN_IF_EXCEPTION(scope, { });
                }

                if (inValue.isObject()) {
                    stateStack.append(ArrayEndVisitMember);
                    goto stateUnknown;
                } else
                    outValue = inValue;
                FALLTHROUGH;
            }
            case ArrayEndVisitMember: {
                JSObject* array = asObject(markedStack.last());
                JSValue filteredValue = callReviver(array, jsString(vm, String::number(indexStack.last())), outValue);
                RETURN_IF_EXCEPTION(scope, { });
                if (filteredValue.isUndefined())
                    array->methodTable()->deletePropertyByIndex(array, m_globalObject, indexStack.last());
                else
                    array->putDirectIndex(m_globalObject, indexStack.last(), filteredValue, 0, PutDirectIndexShouldNotThrow);
                RETURN_IF_EXCEPTION(scope, { });
                indexStack.last()++;
                goto arrayStartVisitMember;
            }
            objectStartState:
            case ObjectStartState: {
                ASSERT(inValue.isObject());
                ASSERT(!isJSArray(inValue));
                if (UNLIKELY(markedStack.size() >= maximumSideStackRecursion))
                    return throwStackOverflowError(m_globalObject, scope);

                JSObject* object = asObject(inValue);
                markedStack.appendWithCrashOnOverflow(object);
                indexStack.append(0);
                propertyStack.append(PropertyNameArray(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude));
                object->methodTable()->getOwnPropertyNames(object, m_globalObject, propertyStack.last(), DontEnumPropertiesMode::Exclude);
                RETURN_IF_EXCEPTION(scope, { });
            }
            objectStartVisitMember:
            FALLTHROUGH;
            case ObjectStartVisitMember: {
                JSObject* object = jsCast<JSObject*>(markedStack.last());
                uint32_t index = indexStack.last();
                PropertyNameArray& properties = propertyStack.last();
                if (index == properties.size()) {
                    outValue = object;
                    markedStack.removeLast();
                    indexStack.removeLast();
                    propertyStack.removeLast();
                    break;
                }
                inValue = object->get(m_globalObject, properties[index]);
                // The holder may be modified by the reviver function so any lookup may throw
                RETURN_IF_EXCEPTION(scope, { });

                if (inValue.isObject()) {
                    stateStack.append(ObjectEndVisitMember);
                    goto stateUnknown;
                } else
                    outValue = inValue;
                FALLTHROUGH;
            }
            case ObjectEndVisitMember: {
                JSObject* object = jsCast<JSObject*>(markedStack.last());
                Identifier prop = propertyStack.last()[indexStack.last()];
                JSValue filteredValue = callReviver(object, jsString(vm, prop.string()), outValue);
                RETURN_IF_EXCEPTION(scope, { });
                if (filteredValue.isUndefined())
                    JSCell::deleteProperty(object, m_globalObject, prop);
                else {
                    unsigned attributes;
                    PropertyOffset offset = object->getDirectOffset(vm, prop, attributes);
                    if (LIKELY(offset != invalidOffset && attributes == static_cast<unsigned>(PropertyAttribute::None))) {
                        object->putDirectOffset(vm, offset, filteredValue);
                        object->structure()->didReplaceProperty(offset);
                    } else {
                        bool shouldThrow = false;
                        object->createDataProperty(m_globalObject, prop, filteredValue, shouldThrow);
                    }
                }
                RETURN_IF_EXCEPTION(scope, { });
                indexStack.last()++;
                goto objectStartVisitMember;
            }
            stateUnknown:
            case StateUnknown:
                if (!inValue.isObject()) {
                    outValue = inValue;
                    break;
                }
                bool valueIsArray = isArray(m_globalObject, inValue);
                RETURN_IF_EXCEPTION(scope, { });
                if (valueIsArray)
                    goto arrayStartState;
                goto objectStartState;
        }
        if (stateStack.isEmpty())
            break;

        state = stateStack.last();
        stateStack.removeLast();
    }
    JSObject* finalHolder = constructEmptyObject(m_globalObject);
    finalHolder->putDirect(vm, vm.propertyNames->emptyIdentifier, outValue);
    RELEASE_AND_RETURN(scope, callReviver(finalHolder, jsEmptyString(vm), outValue));
}

// ECMA-262 v5 15.12.2
JSC_DEFINE_HOST_FUNCTION(jsonProtoFuncParse, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    auto* string = callFrame->argument(0).toString(globalObject);
    RETURN_IF_EXCEPTION(scope, { });
    auto view = string->view(globalObject);
    RETURN_IF_EXCEPTION(scope, { });

    JSValue unfiltered;
    if (view->is8Bit()) {
        LiteralParser jsonParser(globalObject, view->span8(), StrictJSON);
        unfiltered = jsonParser.tryLiteralParse();
        EXCEPTION_ASSERT(!scope.exception() || !unfiltered);
        if (!unfiltered) {
            RETURN_IF_EXCEPTION(scope, { });
            return throwVMError(globalObject, scope, createSyntaxError(globalObject, jsonParser.getErrorMessage()));
        }
    } else {
        LiteralParser jsonParser(globalObject, view->span16(), StrictJSON);
        unfiltered = jsonParser.tryLiteralParse();
        EXCEPTION_ASSERT(!scope.exception() || !unfiltered);
        if (!unfiltered) {
            RETURN_IF_EXCEPTION(scope, { });
            return throwVMError(globalObject, scope, createSyntaxError(globalObject, jsonParser.getErrorMessage()));
        }
    }
    
    if (callFrame->argumentCount() < 2)
        return JSValue::encode(unfiltered);
    
    JSValue function = callFrame->uncheckedArgument(1);
    auto callData = JSC::getCallData(function);
    if (callData.type == CallData::Type::None)
        return JSValue::encode(unfiltered);
    scope.release();
    Walker walker(globalObject, asObject(function), callData);
    return JSValue::encode(walker.walk(unfiltered));
}

// ECMA-262 v5 15.12.3
JSC_DEFINE_HOST_FUNCTION(jsonProtoFuncStringify, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
    String result = stringify(*globalObject, callFrame->argument(0), callFrame->argument(1), callFrame->argument(2));
    return result.isNull() ? encodedJSUndefined() : JSValue::encode(jsString(globalObject->vm(), WTFMove(result)));
}

JSValue JSONParse(JSGlobalObject* globalObject, StringView json)
{
    if (json.isNull())
        return JSValue();

    if (json.is8Bit()) {
        LiteralParser jsonParser(globalObject, json.span8(), StrictJSON);
        return jsonParser.tryLiteralParse();
    }

    LiteralParser jsonParser(globalObject, json.span16(), StrictJSON);
    return jsonParser.tryLiteralParse();
}

JSValue JSONParseWithException(JSGlobalObject* globalObject, StringView json)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (json.isNull())
        return JSValue();

    if (json.is8Bit()) {
        LiteralParser jsonParser(globalObject, json.span8(), StrictJSON);
        JSValue result = jsonParser.tryLiteralParse();
        RETURN_IF_EXCEPTION(scope, { });
        if (!result)
            throwSyntaxError(globalObject, scope, jsonParser.getErrorMessage());
        return result;
    }

    LiteralParser jsonParser(globalObject, json.span16(), StrictJSON);
    JSValue result = jsonParser.tryLiteralParse();
    RETURN_IF_EXCEPTION(scope, { });
    if (!result)
        throwSyntaxError(globalObject, scope, jsonParser.getErrorMessage());
    return result;
}

String JSONStringify(JSGlobalObject* globalObject, JSValue value, JSValue space)
{
    return stringify(*globalObject, value, jsNull(), space);
}

String JSONStringify(JSGlobalObject* globalObject, JSValue value, unsigned indent)
{
    return stringify(*globalObject, value, jsNull(), jsNumber(indent));
}

} // namespace JSC

WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
