/* -----------------------------------------------------------------------------
 *
 * (c) The GHC Team, 1998-2019
 *
 * Non-moving garbage collector and allocator:
 * Indirection short-cutting and the selector optimisation
 *
 * ---------------------------------------------------------------------------*/

#include "Rts.h"
#include "GC.h"
#include "SMPClosureOps.h"
#include "NonMovingMark.h"
#include "NonMovingShortcut.h"
#include "Printer.h"

#define MAX_THUNK_SELECTOR_DEPTH 16

//#define SELECTOR_OPT_DEBUG

#if defined(SELECTOR_OPT_DEBUG)
static void
print_selector_chain(StgClosure *p)
{
    debugBelch("Selector chain: %p", (void*)p);
    StgClosure *next = p->payload[0];
    while (next != NULL) {
        debugBelch(", %p", next);
        next = next->payload[0];
    }
    debugBelch("\n");
}
#endif

static void
update_selector_chain(
        StgClosure *chain,
        StgClosure **origin,
        StgSelector * const p0,
        StgClosure * const val
) {
    ASSERT(val != NULL);

    // Make sure we don't introduce nonmoving-to-moving pointers here.
    ASSERT(isNonmovingClosure(val));

    // This case we can't handle because we don't know info ptr of the closure
    // before we locked it.
    ASSERT(chain != val);

#if defined(SELECTOR_OPT_DEBUG)
    if (chain != NULL) {
        print_selector_chain(chain);
        debugBelch("Value: ");
        printClosure(val);
    }
#endif

    while (chain) {
        // debugBelch("chain: %p\n", (void*)chain);

        StgClosure *next = chain->payload[0];

        // We only update closures in the non-moving heap
        ASSERT(isNonmovingClosure(chain));

        ((StgInd*)chain)->indirectee = val;
        unlockClosure((StgClosure*)chain, &stg_IND_info);

        chain = next;
    }

    if (origin != NULL && (StgClosure*)p0 != val) {
        cas((StgVolatilePtr)origin, (StgWord)p0, (StgWord)val);
    }
}

// Returns value of the selector thunk. The value is a non-moving closure. If
// it's not possible to evaluate the selector thunk the return value will be the
// selector itself.
static StgClosure*
nonmoving_eval_thunk_selector_(
        MarkQueue *queue,
        StgSelector * const p0,
        StgClosure ** const origin,
        int depth
) {
    // This function should only be called on non-moving objects.
    ASSERT(HEAP_ALLOCED_GC((P_)p0) && isNonmovingClosure((StgClosure*)p0));

    markQueuePushClosure(queue, (StgClosure*)p0, NULL);

    // INVARIANT: A non-moving object. Locked (below).
    StgClosure *p = (StgClosure*)p0;

    // Chain of non-moving selectors to update. These will be INDs to `p` when
    // we reach to a value. INVARIANT: All objects in the chain are locked, and
    // in the non-moving heap.
    StgClosure *chain = NULL;

    // Variables to update: p.
selector_changed:
    ;

    // debugBelch("Selector changed: %p\n", (void*)p);

    // Lock the selector to avoid concurrent modification in mutators
    const StgInfoTable *selector_info_ptr = lockClosure((StgClosure*)p);
    StgInfoTable *selector_info_tbl = INFO_PTR_TO_STRUCT(selector_info_ptr);

    if (selector_info_tbl->type != THUNK_SELECTOR) {
        // Selector updated in the meantime, or we reached to a value. Update
        // the chain.
        unlockClosure(p, selector_info_ptr);
        update_selector_chain(chain, origin, p0, p);
        return p;
    }

    // The closure is locked and it's a selector thunk. If the selectee is a
    // CONSTR we do the selection here and the In the selected value will be the
    // value of this selector thunk.
    //
    // Two cases:
    //
    // - If the selected value is also a selector thunk, then we loop and
    //   evaluate it. The final value will be the value of both the current
    //   selector and the selected value (which is also a selector thunk).
    //
    // - If the selectee is a selector thunk, we recursively evaluate it (up to
    //   a certain depth, specified with MAX_THUNK_SELECTOR_DEPTH), then do the
    //   selection on the value of it.

    //
    // Do the selection
    //

    uint32_t field = selector_info_tbl->layout.selector_offset;
    StgClosure *selectee = UNTAG_CLOSURE(((StgSelector*)p)->selectee);

    // Variables to update: selectee
selectee_changed:
    // debugBelch("Selectee changed: %p\n", (void*)selectee);

    if (!isNonmovingClosure(selectee)) {
        // The selectee is a moving object, and it may be moved by a concurrent
        // minor GC while we read the info table and fields, so don't try to
        // read the fields, just update the chain.
        unlockClosure(p, selector_info_ptr);
        update_selector_chain(chain, origin, p0, p);
        return p;
    }

    // Selectee is a non-moving object, mark it.
    markQueuePushClosure(queue, selectee, NULL);

    // This may synchronize with the release in updateWithIndirection.
    const StgInfoTable *selectee_info_tbl = get_itbl_acquire(selectee);
    switch (selectee_info_tbl->type) {
        case WHITEHOLE: {
            // Probably a loop. Abort.
            unlockClosure(p, selector_info_ptr);
            update_selector_chain(chain, origin, p0, p);
            return p;
        }

        case CONSTR:
        case CONSTR_1_0:
        case CONSTR_0_1:
        case CONSTR_2_0:
        case CONSTR_1_1:
        case CONSTR_0_2:
        case CONSTR_NOCAF: {
            // Selectee is a constructor in the non-moving heap.
            // Select the field.

            // Check that the size is in range.
            ASSERT(field < (StgWord32)(selectee_info_tbl->layout.payload.ptrs +
                                       selectee_info_tbl->layout.payload.nptrs));

            StgClosure *val = UNTAG_CLOSURE(selectee->payload[field]);

            // `val` is the value of this selector thunk. We need to check a
            // few cases:
            //
            // - If `val` is in the moving heap, we stop here and update the
            //   chain. All updated objects should be added to the mut_list.
            //   (TODO (osa): What happens if the value is evacuated as we do
            //   this?)
            //
            // - If `val` is in the non-moving heap, we check if it's also a
            //   selector. If it is we add it to the chain and loop.

            // Follow indirections. Variables to update: `val`.
        val_changed:
            if (!isNonmovingClosure(val)) {
                // The selected value is a moving object, so we won't be
                // updating the chain to this object.
                unlockClosure(p, selector_info_ptr);
                update_selector_chain(chain, origin, p0, p);
                return p;
            }

            switch (get_itbl(val)->type) {
            case IND:
            case IND_STATIC:
                ;
                // Follow the indirection
                StgClosure *indirectee = UNTAG_CLOSURE(((StgInd*)val)->indirectee);
                if (isNonmovingClosure(indirectee)) {
                    val = UNTAG_CLOSURE(((StgInd*)val)->indirectee);
                    goto val_changed;
                } else {
                    unlockClosure(p, selector_info_ptr);
                    update_selector_chain(chain, origin, p0, p);
                    return p;
                }
            case THUNK_SELECTOR:
                // Value of the selector thunk is again a selector thunk in the
                // non-moving heap. Add the current selector to the chain and
                // loop.
                p->payload[0] = chain;
                chain = p;
                p = val;
                goto selector_changed;
            default:
                // Found a value, add the current selector to the chain and
                // update it.
                p->payload[0] = chain;
                chain = p;
                update_selector_chain(chain, origin, p0, val);
                return val;
            }
        }

        case IND:
        case IND_STATIC: {
            StgClosure *indirectee = UNTAG_CLOSURE(((StgInd *)selectee)->indirectee);
            if (isNonmovingClosure(indirectee)) {
                selectee = indirectee;
                goto selectee_changed;
            } else {
                unlockClosure(p, selector_info_ptr);
                update_selector_chain(chain, origin, p0, p);
                return p;
            }
        }

        case BLACKHOLE: {
            StgClosure *indirectee = ((StgInd*)selectee)->indirectee;

            if (!isNonmovingClosure(UNTAG_CLOSURE(indirectee))) {
                unlockClosure(p, selector_info_ptr);
                update_selector_chain(chain, origin, p0, p);
                return p;
            }

            // Establish whether this BH has been updated, and is now an
            // indirection, as in evacuate().
            if (GET_CLOSURE_TAG(indirectee) == 0) {
                const StgInfoTable *i = indirectee->header.info;
                if (i == &stg_TSO_info
                    || i == &stg_WHITEHOLE_info
                    || i == &stg_BLOCKING_QUEUE_CLEAN_info
                    || i == &stg_BLOCKING_QUEUE_DIRTY_info) {
                    unlockClosure(p, selector_info_ptr);
                    update_selector_chain(chain, origin, p0, p);
                    return (StgClosure*)p;
                }
                ASSERT(i != &stg_IND_info); // TODO not sure about this part
            }

            // It's an indirection, follow it.
            selectee = UNTAG_CLOSURE(indirectee);
            goto selectee_changed;
        }

        case AP:
        case AP_STACK:
        case THUNK:
        case THUNK_1_0:
        case THUNK_0_1:
        case THUNK_2_0:
        case THUNK_1_1:
        case THUNK_0_2:
        case THUNK_STATIC: {
            // Not evaluated yet
            unlockClosure(p, selector_info_ptr);
            update_selector_chain(chain, origin, p0, p);
            return (StgClosure*)p;
        }

        case THUNK_SELECTOR: {
            // Selectee is a selector thunk. Evaluate it if we haven't reached
            // the recursion limit yet.
            if (depth < MAX_THUNK_SELECTOR_DEPTH) {
                StgClosure *new_selectee =
                    UNTAG_CLOSURE(nonmoving_eval_thunk_selector_(
                                queue, (StgSelector*)selectee, NULL, depth+1));
                ASSERT(isNonmovingClosure(new_selectee));
                if (selectee == new_selectee) {
                    unlockClosure(p, selector_info_ptr);
                    update_selector_chain(chain, origin, p0, p);
                    return (StgClosure*)p;
                } else {
                    selectee = new_selectee;
                    goto selectee_changed;
                }
            } else {
                // Recursion limit reached
                unlockClosure(p, selector_info_ptr);
                update_selector_chain(chain, origin, p0, p);
                return (StgClosure*)p;
            }
        }

        default: {
            barf("nonmoving_eval_thunk_selector: strange selectee %d",
                 (int)(selectee_info_tbl->type));
        }
    }
}

void
nonmoving_eval_thunk_selector(MarkQueue *queue, StgSelector *p, StgClosure **origin)
{
    nonmoving_eval_thunk_selector_(queue, p, origin, 0);
}
