/*
 * Copyright (C) 2011-2024 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. AND ITS CONTRIBUTORS ``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 ITS 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 "ConservativeRoots.h"

#include "CalleeBits.h"
#include "CodeBlock.h"
#include "CodeBlockSetInlines.h"
#include "HeapUtil.h"
#include "JITStubRoutineSet.h"
#include "JSCast.h"
#include "JSCellInlines.h"
#include "MarkedBlockInlines.h"
#include "WasmCallee.h"
#include <wtf/OSAllocator.h>

WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN

namespace JSC {

static constexpr bool verboseWasmCalleeScan = false;

ConservativeRoots::ConservativeRoots(JSC::Heap& heap)
    : m_roots(m_inlineRoots)
    , m_size(0)
    , m_capacity(inlineCapacity)
    , m_heap(heap)
{
#if ENABLE(WEBASSEMBLY)
    Locker locker(heap.m_wasmCalleesPendingDestructionLock);
    for (auto& iter : heap.m_wasmCalleesPendingDestruction) {
        void* boxedWasmCallee = CalleeBits::boxNativeCallee(iter.ptr());
        ASSERT_UNUSED(boxedWasmCallee, boxedWasmCallee == removeArrayPtrTag(boxedWasmCallee));
        dataLogLnIf(verboseWasmCalleeScan, "Looking for ", RawPointer(iter.ptr()), " boxed: ", RawPointer(boxedWasmCallee));
        // FIXME: This seems like it could have some kind of bulk add.
        m_wasmCalleesPendingDestructionCopy.add(iter.ptr());
        m_boxedWasmCalleeFilter.add(std::bit_cast<uintptr_t>(CalleeBits::boxNativeCallee(iter.ptr())));
    }
#endif
}

ConservativeRoots::~ConservativeRoots()
{
#if ENABLE(WEBASSEMBLY)
    if (m_wasmCalleesPendingDestructionCopy.size()) {
        // We need to deref these from outside the lock since other Callees could be registered by the destructors.
        Vector<RefPtr<Wasm::Callee>, 8> wasmCalleesToRelease;
        {
            Locker locker(m_heap.m_wasmCalleesPendingDestructionLock);
            wasmCalleesToRelease = m_heap.m_wasmCalleesPendingDestruction.takeIf<8>([&] (const auto& callee) {
                dataLogLnIf(verboseWasmCalleeScan && m_wasmCalleesPendingDestructionCopy.contains(callee.ptr()), RawPointer(callee.ptr()), " was ", m_wasmCalleesDiscovered.contains(callee.ptr()) ? "discovered" : "not discovered");
                return m_wasmCalleesPendingDestructionCopy.contains(callee.ptr()) && !m_wasmCalleesDiscovered.contains(callee.ptr());
            });
            dataLogLnIf(verboseWasmCalleeScan, m_heap.m_wasmCalleesPendingDestruction.size(), " callees remaining");
        }
    }
#endif

    if (m_roots != m_inlineRoots)
        OSAllocator::decommitAndRelease(m_roots, m_capacity * sizeof(HeapCell*));
}

void ConservativeRoots::grow()
{
    size_t newCapacity = m_capacity * 2;
    HeapCell** newRoots = static_cast<HeapCell**>(OSAllocator::reserveAndCommit(newCapacity * sizeof(HeapCell*)));
    memcpy(newRoots, m_roots, m_size * sizeof(HeapCell*));
    if (m_roots != m_inlineRoots)
        OSAllocator::decommitAndRelease(m_roots, m_capacity * sizeof(HeapCell*));
    m_capacity = newCapacity;
    m_roots = newRoots;
}

// This function must be run after stopThePeriphery() is called and
// before liveness data is cleared to be accurate.
template<bool lookForWasmCallees, typename MarkHook>
inline void ConservativeRoots::genericAddPointer(char* pointer, HeapVersion markingVersion, HeapVersion newlyAllocatedVersion, TinyBloomFilter<uintptr_t> jsGCFilter, TinyBloomFilter<uintptr_t> boxedWasmCalleeFilter, MarkHook& markHook)
{
    ASSERT(m_heap.worldIsStopped());
    pointer = removeArrayPtrTag(pointer);
    markHook.mark(pointer);

    auto markFoundGCPointer = [&] (void* p, HeapCell::Kind cellKind) {
        if (isJSCellKind(cellKind))
            markHook.markKnownJSCell(static_cast<JSCell*>(p));

        if (m_size == m_capacity)
            grow();

        m_roots[m_size++] = std::bit_cast<HeapCell*>(p);
    };

    const UncheckedKeyHashSet<MarkedBlock*>& set = m_heap.objectSpace().blocks().set();

    ASSERT(m_heap.objectSpace().isMarking());
    static constexpr bool isMarking = true;

#if ENABLE(WEBASSEMBLY) && USE(JSVALUE64)
    if constexpr (lookForWasmCallees) {
        CalleeBits calleeBits = std::bit_cast<CalleeBits>(pointer);
        // No point in even checking the hash set if the pointer doesn't even look like a native callee.
        if (calleeBits.isNativeCallee()) {
            if (!boxedWasmCalleeFilter.ruleOut(std::bit_cast<uintptr_t>(pointer))) {
                Wasm::Callee* wasmCallee = static_cast<Wasm::Callee*>(calleeBits.asNativeCallee());
                if (m_wasmCalleesPendingDestructionCopy.contains(wasmCallee)) {
                    m_wasmCalleesDiscovered.add(wasmCallee);
                    return;
                }
            }
            // FIXME: We could probably just return here.
        }
    }
#else
    UNUSED_PARAM(boxedWasmCalleeFilter);
#endif

    // It could point to a precise allocation.
    if (m_heap.objectSpace().preciseAllocationsForThisCollectionSize()) {
        if (m_heap.objectSpace().preciseAllocationsForThisCollectionBegin()[0]->aboveLowerBound(pointer)
            && m_heap.objectSpace().preciseAllocationsForThisCollectionEnd()[-1]->belowUpperBound(pointer)) {
            PreciseAllocation** result = approximateBinarySearch<PreciseAllocation*>(
                m_heap.objectSpace().preciseAllocationsForThisCollectionBegin(),
                m_heap.objectSpace().preciseAllocationsForThisCollectionSize(),
                PreciseAllocation::fromCell(pointer),
                [] (PreciseAllocation** ptr) -> PreciseAllocation* { return *ptr; });
            if (result) {
                auto attemptLarge = [&] (PreciseAllocation* allocation) {
                    if (allocation->contains(pointer) && allocation->hasValidCell())
                        markFoundGCPointer(allocation->cell(), allocation->attributes().cellKind);
                };

                if (result > m_heap.objectSpace().preciseAllocationsForThisCollectionBegin())
                    attemptLarge(result[-1]);
                attemptLarge(result[0]);
                if (result + 1 < m_heap.objectSpace().preciseAllocationsForThisCollectionEnd())
                    attemptLarge(result[1]);
            }
        }
    }

    MarkedBlock* candidate = MarkedBlock::blockFor(pointer);
    // It's possible for a butterfly pointer to point past the end of a butterfly. Check this now.
    if (pointer <= std::bit_cast<char*>(candidate) + sizeof(IndexingHeader)) {
        // We may be interested in the last cell of the previous MarkedBlock.
        char* previousPointer = std::bit_cast<char*>(std::bit_cast<uintptr_t>(pointer) - sizeof(IndexingHeader) - 1);
        MarkedBlock* previousCandidate = MarkedBlock::blockFor(previousPointer);
        if (!jsGCFilter.ruleOut(std::bit_cast<uintptr_t>(previousCandidate))
            && set.contains(previousCandidate)
            && mayHaveIndexingHeader(previousCandidate->handle().cellKind())) {
            previousPointer = static_cast<char*>(previousCandidate->handle().cellAlign(previousPointer));
            if (previousCandidate->handle().isLiveCell(markingVersion, newlyAllocatedVersion, isMarking, previousPointer))
                markFoundGCPointer(previousPointer, previousCandidate->handle().cellKind());
        }
    }

    if (jsGCFilter.ruleOut(std::bit_cast<uintptr_t>(candidate))) {
        ASSERT(!candidate || !set.contains(candidate));
        return;
    }

    if (!set.contains(candidate))
        return;

    HeapCell::Kind cellKind = candidate->handle().cellKind();

    auto tryPointer = [&] (void* pointer) {
        bool isLive = candidate->handle().isLiveCell(markingVersion, newlyAllocatedVersion, isMarking, pointer);
        if (isLive)
            markFoundGCPointer(pointer, cellKind);
        // Only return early if we are marking a non-butterfly, since butterflies without indexed properties could point past the end of their allocation.
        // If we do, and there is another live butterfly immediately following the first, we will mark the latter one here but we still need to
        // mark the former.
        return isLive && !mayHaveIndexingHeader(cellKind);
    };

    if (isJSCellKind(cellKind)) {
        if (MarkedBlock::isAtomAligned(pointer)) [[likely]] {
            if (tryPointer(pointer))
                return;
        }
    }

    // We could point into the middle of an object.
    char* alignedPointer = static_cast<char*>(candidate->handle().cellAlign(pointer));
    if (tryPointer(alignedPointer))
        return;

    // Also, a butterfly could point at the end of an object plus sizeof(IndexingHeader). In that
    // case, this is pointing to the object to the right of the one we should be marking.
    if (candidate->candidateAtomNumber(alignedPointer) > 0 && pointer <= alignedPointer + sizeof(IndexingHeader))
        tryPointer(alignedPointer - candidate->cellSize());
}

template<typename MarkHook>
SUPPRESS_ASAN
void ConservativeRoots::genericAddSpan(void* begin, void* end, MarkHook& markHook)
{
    if (begin > end)
        std::swap(begin, end);

    RELEASE_ASSERT(isPointerAligned(begin));
    RELEASE_ASSERT(isPointerAligned(end));
    // Make a local copy of filters to show the compiler it won't alias, and can be register-allocated.
    TinyBloomFilter<uintptr_t> jsGCFilter = m_heap.objectSpace().blocks().filter();
    TinyBloomFilter<uintptr_t> boxedWasmCalleeFilter = m_boxedWasmCalleeFilter;

    HeapVersion markingVersion = m_heap.objectSpace().markingVersion();
    HeapVersion newlyAllocatedVersion = m_heap.objectSpace().newlyAllocatedVersion();
#if ENABLE(WEBASSEMBLY)
    if (boxedWasmCalleeFilter.bits()) {
        constexpr bool lookForWasmCallees = true;
        for (char** it = static_cast<char**>(begin); it != static_cast<char**>(end); ++it)
            genericAddPointer<lookForWasmCallees>(*it, markingVersion, newlyAllocatedVersion, jsGCFilter, boxedWasmCalleeFilter, markHook);
    } else {
#else
    {
#endif
        constexpr bool lookForWasmCallees = false;
        for (char** it = static_cast<char**>(begin); it != static_cast<char**>(end); ++it)
            genericAddPointer<lookForWasmCallees>(*it, markingVersion, newlyAllocatedVersion, jsGCFilter, boxedWasmCalleeFilter, markHook);
    }
}

class DummyMarkHook {
public:
    void mark(void*) { }
    void markKnownJSCell(JSCell*) { }
};

void ConservativeRoots::add(void* begin, void* end)
{
    DummyMarkHook dummy;
    genericAddSpan(begin, end, dummy);
}

class CompositeMarkHook {
public:
    CompositeMarkHook(JITStubRoutineSet& stubRoutines, CodeBlockSet& codeBlocks, const AbstractLocker& locker)
        : m_stubRoutines(stubRoutines)
        , m_codeBlocks(codeBlocks)
        , m_codeBlocksLocker(locker)
    {
    }
    
    void mark(void* address)
    {
        m_stubRoutines.mark(address);
    }
    
    void markKnownJSCell(JSCell* cell)
    {
        if (cell->type() == CodeBlockType)
            m_codeBlocks.mark(m_codeBlocksLocker, jsCast<CodeBlock*>(cell));
    }

private:
    JITStubRoutineSet& m_stubRoutines;
    CodeBlockSet& m_codeBlocks;
    const AbstractLocker& m_codeBlocksLocker;
};

void ConservativeRoots::add(
    void* begin, void* end, JITStubRoutineSet& jitStubRoutines, CodeBlockSet& codeBlocks)
{
    Locker locker { codeBlocks.getLock() };
    CompositeMarkHook markHook(jitStubRoutines, codeBlocks, locker);
    genericAddSpan(begin, end, markHook);
}

} // namespace JSC

WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
