/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * vim: set ts=8 sts=4 et sw=4 tw=99:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "vm/GeneratorObject.h"

#include "jsobj.h"

#include "jsatominlines.h"
#include "jsscriptinlines.h"

#include "vm/NativeObject-inl.h"
#include "vm/Stack-inl.h"

using namespace js;

JSObject*
GeneratorObject::create(JSContext* cx, AbstractFramePtr frame)
{
    MOZ_ASSERT(frame.script()->isGenerator());
    MOZ_ASSERT(frame.script()->nfixed() == 0);

    Rooted<GlobalObject*> global(cx, cx->global());
    RootedNativeObject obj(cx);
    if (frame.script()->isStarGenerator()) {
        RootedValue pval(cx);
        RootedObject fun(cx, frame.callee());
        // FIXME: This would be faster if we could avoid doing a lookup to get
        // the prototype for the instance.  Bug 906600.
        if (!GetProperty(cx, fun, fun, cx->names().prototype, &pval))
            return nullptr;
        RootedObject proto(cx, pval.isObject() ? &pval.toObject() : nullptr);
        if (!proto) {
            proto = GlobalObject::getOrCreateStarGeneratorObjectPrototype(cx, global);
            if (!proto)
                return nullptr;
        }
        obj = NewNativeObjectWithGivenProto(cx, &StarGeneratorObject::class_, proto);
    } else {
        MOZ_ASSERT(frame.script()->isLegacyGenerator());
        RootedObject proto(cx, GlobalObject::getOrCreateLegacyGeneratorObjectPrototype(cx, global));
        if (!proto)
            return nullptr;
        obj = NewNativeObjectWithGivenProto(cx, &LegacyGeneratorObject::class_, proto);
    }
    if (!obj)
        return nullptr;

    GeneratorObject* genObj = &obj->as<GeneratorObject>();
    genObj->setCallee(*frame.callee());
    genObj->setNewTarget(frame.newTarget());
    genObj->setEnvironmentChain(*frame.environmentChain());
    if (frame.script()->needsArgsObj())
        genObj->setArgsObj(frame.argsObj());
    genObj->clearExpressionStack();

    return obj;
}

bool
GeneratorObject::suspend(JSContext* cx, HandleObject obj, AbstractFramePtr frame, jsbytecode* pc,
                         Value* vp, unsigned nvalues)
{
    MOZ_ASSERT(*pc == JSOP_INITIALYIELD || *pc == JSOP_YIELD);

    Rooted<GeneratorObject*> genObj(cx, &obj->as<GeneratorObject>());
    MOZ_ASSERT(!genObj->hasExpressionStack());

    if (*pc == JSOP_YIELD && genObj->isClosing() && genObj->is<LegacyGeneratorObject>()) {
        RootedValue val(cx, ObjectValue(*frame.callee()));
        ReportValueError(cx, JSMSG_BAD_GENERATOR_YIELD, JSDVG_IGNORE_STACK, val, nullptr);
        return false;
    }

    uint32_t yieldIndex = GET_UINT24(pc);
    genObj->setYieldIndex(yieldIndex);
    genObj->setEnvironmentChain(*frame.environmentChain());

    if (nvalues) {
        ArrayObject* stack = NewDenseCopiedArray(cx, nvalues, vp);
        if (!stack)
            return false;
        genObj->setExpressionStack(*stack);
    }

    return true;
}

bool
GeneratorObject::finalSuspend(JSContext* cx, HandleObject obj)
{
    Rooted<GeneratorObject*> genObj(cx, &obj->as<GeneratorObject>());
    MOZ_ASSERT(genObj->isRunning() || genObj->isClosing());

    bool closing = genObj->isClosing();
    genObj->setClosed();

    if (genObj->is<LegacyGeneratorObject>() && !closing)
        return ThrowStopIteration(cx);

    return true;
}

void
js::SetReturnValueForClosingGenerator(JSContext* cx, AbstractFramePtr frame)
{
    CallObject& callObj = frame.callObj();

    // Get the generator object stored on the scope chain and close it.
    Shape* shape = callObj.lookup(cx, cx->names().dotGenerator);
    GeneratorObject& genObj = callObj.getSlot(shape->slot()).toObject().as<GeneratorObject>();
    genObj.setClosed();

    // Return value is already set in GeneratorThrowOrClose.
    if (genObj.is<StarGeneratorObject>())
        return;

    // Legacy generator .close() always returns |undefined|.
    MOZ_ASSERT(genObj.is<LegacyGeneratorObject>());
    frame.setReturnValue(UndefinedValue());
}

bool
js::GeneratorThrowOrClose(JSContext* cx, AbstractFramePtr frame, Handle<GeneratorObject*> genObj,
                          HandleValue arg, uint32_t resumeKind)
{
    if (resumeKind == GeneratorObject::THROW) {
        cx->setPendingException(arg);
        genObj->setRunning();
    } else {
        MOZ_ASSERT(resumeKind == GeneratorObject::CLOSE);

        if (genObj->is<StarGeneratorObject>()) {
            MOZ_ASSERT(arg.isObject());
            frame.setReturnValue(arg);
        } else {
            MOZ_ASSERT(arg.isUndefined());
        }

        cx->setPendingException(MagicValue(JS_GENERATOR_CLOSING));
        genObj->setClosing();
    }
    return false;
}

bool
GeneratorObject::resume(JSContext* cx, InterpreterActivation& activation,
                        HandleObject obj, HandleValue arg, GeneratorObject::ResumeKind resumeKind)
{
    Rooted<GeneratorObject*> genObj(cx, &obj->as<GeneratorObject>());
    MOZ_ASSERT(genObj->isSuspended());

    RootedFunction callee(cx, &genObj->callee());
    RootedValue newTarget(cx, genObj->newTarget());
    RootedObject envChain(cx, &genObj->environmentChain());
    if (!activation.resumeGeneratorFrame(callee, newTarget, envChain))
        return false;
    activation.regs().fp()->setResumedGenerator();

    if (genObj->hasArgsObj())
        activation.regs().fp()->initArgsObj(genObj->argsObj());

    if (genObj->hasExpressionStack()) {
        uint32_t len = genObj->expressionStack().length();
        MOZ_ASSERT(activation.regs().spForStackDepth(len));
        const Value* src = genObj->expressionStack().getDenseElements();
        mozilla::PodCopy(activation.regs().sp, src, len);
        activation.regs().sp += len;
        genObj->clearExpressionStack();
    }

    JSScript* script = callee->nonLazyScript();
    uint32_t offset = script->yieldOffsets()[genObj->yieldIndex()];
    activation.regs().pc = script->offsetToPC(offset);

    // Always push on a value, even if we are raising an exception. In the
    // exception case, the stack needs to have something on it so that exception
    // handling doesn't skip the catch blocks. See TryNoteIter::settle.
    activation.regs().sp++;
    MOZ_ASSERT(activation.regs().spForStackDepth(activation.regs().stackDepth()));
    activation.regs().sp[-1] = arg;

    switch (resumeKind) {
      case NEXT:
        genObj->setRunning();
        return true;

      case THROW:
      case CLOSE:
        return GeneratorThrowOrClose(cx, activation.regs().fp(), genObj, arg, resumeKind);

      default:
        MOZ_CRASH("bad resumeKind");
    }
}

bool
LegacyGeneratorObject::close(JSContext* cx, HandleObject obj)
{
     Rooted<LegacyGeneratorObject*> genObj(cx, &obj->as<LegacyGeneratorObject>());

    // Avoid calling back into JS unless it is necessary.
     if (genObj->isClosed())
        return true;

    RootedValue rval(cx);

    RootedValue closeValue(cx);
    if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().LegacyGeneratorCloseInternal,
                                         &closeValue))
    {
        return false;
    }
    MOZ_ASSERT(closeValue.isObject());
    MOZ_ASSERT(closeValue.toObject().is<JSFunction>());

    FixedInvokeArgs<0> args(cx);

    RootedValue v(cx, ObjectValue(*genObj));
    return Call(cx, closeValue, v, args, &v);
}

const Class LegacyGeneratorObject::class_ = {
    "Generator",
    JSCLASS_HAS_RESERVED_SLOTS(GeneratorObject::RESERVED_SLOTS)
};

const Class StarGeneratorObject::class_ = {
    "Generator",
    JSCLASS_HAS_RESERVED_SLOTS(GeneratorObject::RESERVED_SLOTS)
};

static const JSFunctionSpec star_generator_methods[] = {
    JS_SELF_HOSTED_FN("next", "StarGeneratorNext", 1, 0),
    JS_SELF_HOSTED_FN("throw", "StarGeneratorThrow", 1, 0),
    JS_SELF_HOSTED_FN("return", "StarGeneratorReturn", 1, 0),
    JS_FS_END
};

#define JSPROP_ROPERM   (JSPROP_READONLY | JSPROP_PERMANENT)

static const JSFunctionSpec legacy_generator_methods[] = {
    JS_SELF_HOSTED_SYM_FN(iterator, "LegacyGeneratorIteratorShim", 0, 0),
    // "send" is an alias for "next".
    JS_SELF_HOSTED_FN("next", "LegacyGeneratorNext", 1, JSPROP_ROPERM),
    JS_SELF_HOSTED_FN("send", "LegacyGeneratorNext", 1, JSPROP_ROPERM),
    JS_SELF_HOSTED_FN("throw", "LegacyGeneratorThrow", 1, JSPROP_ROPERM),
    JS_SELF_HOSTED_FN("close", "LegacyGeneratorClose", 0, JSPROP_ROPERM),
    JS_FS_END
};

#undef JSPROP_ROPERM

static JSObject*
NewSingletonObjectWithObjectPrototype(JSContext* cx, Handle<GlobalObject*> global)
{
    RootedObject proto(cx, global->getOrCreateObjectPrototype(cx));
    if (!proto)
        return nullptr;
    return NewObjectWithGivenProto<PlainObject>(cx, proto, SingletonObject);
}

JSObject*
js::NewSingletonObjectWithFunctionPrototype(JSContext* cx, Handle<GlobalObject*> global)
{
    RootedObject proto(cx, global->getOrCreateFunctionPrototype(cx));
    if (!proto)
        return nullptr;
    return NewObjectWithGivenProto<PlainObject>(cx, proto, SingletonObject);
}

/* static */ bool
GlobalObject::initLegacyGeneratorProto(JSContext* cx, Handle<GlobalObject*> global)
{
    if (global->getReservedSlot(LEGACY_GENERATOR_OBJECT_PROTO).isObject())
        return true;

    RootedObject proto(cx, NewSingletonObjectWithObjectPrototype(cx, global));
    if (!proto || !proto->setDelegate(cx))
        return false;
    if (!DefinePropertiesAndFunctions(cx, proto, nullptr, legacy_generator_methods))
        return false;

    global->setReservedSlot(LEGACY_GENERATOR_OBJECT_PROTO, ObjectValue(*proto));
    return true;
}

/* static */ bool
GlobalObject::initStarGenerators(JSContext* cx, Handle<GlobalObject*> global)
{
    if (global->getReservedSlot(STAR_GENERATOR_OBJECT_PROTO).isObject())
        return true;

    RootedObject iteratorProto(cx, GlobalObject::getOrCreateIteratorPrototype(cx, global));
    if (!iteratorProto)
        return false;

    RootedObject genObjectProto(cx, global->createBlankPrototypeInheriting(cx,
                                                                           &PlainObject::class_,
                                                                           iteratorProto));
    if (!genObjectProto)
        return false;
    if (!DefinePropertiesAndFunctions(cx, genObjectProto, nullptr, star_generator_methods) ||
        !DefineToStringTag(cx, genObjectProto, cx->names().Generator))
    {
        return false;
    }

    RootedObject genFunctionProto(cx, NewSingletonObjectWithFunctionPrototype(cx, global));
    if (!genFunctionProto || !genFunctionProto->setDelegate(cx))
        return false;
    if (!LinkConstructorAndPrototype(cx, genFunctionProto, genObjectProto) ||
        !DefineToStringTag(cx, genFunctionProto, cx->names().GeneratorFunction))
    {
        return false;
    }

    RootedValue function(cx, global->getConstructor(JSProto_Function));
    if (!function.toObjectOrNull())
        return false;
    RootedObject proto(cx, &function.toObject());
    RootedAtom name(cx, cx->names().GeneratorFunction);
    RootedObject genFunction(cx, NewFunctionWithProto(cx, Generator, 1,
                                                      JSFunction::NATIVE_CTOR, nullptr, name,
                                                      proto, gc::AllocKind::FUNCTION,
                                                      SingletonObject));
    if (!genFunction)
        return false;
    if (!LinkConstructorAndPrototype(cx, genFunction, genFunctionProto))
        return false;

    global->setReservedSlot(STAR_GENERATOR_OBJECT_PROTO, ObjectValue(*genObjectProto));
    global->setReservedSlot(STAR_GENERATOR_FUNCTION, ObjectValue(*genFunction));
    global->setReservedSlot(STAR_GENERATOR_FUNCTION_PROTO, ObjectValue(*genFunctionProto));
    return true;
}

MOZ_MUST_USE bool
js::CheckStarGeneratorResumptionValue(JSContext* cx, HandleValue v)
{
    // yield/return value should be an Object.
    if (!v.isObject())
        return false;

    JSObject* obj = &v.toObject();

    // It should have `done` data property with boolean value.
    Value doneVal;
    if (!GetPropertyPure(cx, obj, NameToId(cx->names().done), &doneVal))
        return false;
    if (!doneVal.isBoolean())
        return false;

    // It should have `value` data property, but the type doesn't matter
    JSObject* ignored;
    Shape* shape;
    if (!LookupPropertyPure(cx, obj, NameToId(cx->names().value), &ignored, &shape))
        return false;
    if (!shape)
        return false;
    if (!shape->hasDefaultGetter())
        return false;

    return true;
}