/* 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 "frontend/PrivateOpEmitter.h"

#include "frontend/BytecodeEmitter.h"
#include "frontend/NameOpEmitter.h"
#include "vm/Opcodes.h"
#include "vm/ThrowMsgKind.h"  // ThrowMsgKind

using namespace js;
using namespace js::frontend;

PrivateOpEmitter::PrivateOpEmitter(BytecodeEmitter* bce, Kind kind,
                                   TaggedParserAtomIndex name)
    : bce_(bce), kind_(kind), name_(name) {
  MOZ_ASSERT(kind_ != Kind::Delete);
}

bool PrivateOpEmitter::init() {
  // Static analysis needs us to initialise this to something, so use Dynamic()
  NameLocation loc = NameLocation::Dynamic();
  bce_->lookupPrivate(name_, loc, brandLoc_);
  loc_ = mozilla::Some(loc);
  return true;
}

bool PrivateOpEmitter::emitLoad(TaggedParserAtomIndex name,
                                const NameLocation& loc) {
  NameOpEmitter noe(bce_, name, loc, NameOpEmitter::Kind::Get);
  return noe.emitGet();
}

bool PrivateOpEmitter::emitLoadPrivateBrand() {
  return emitLoad(TaggedParserAtomIndex::WellKnown::dot_privateBrand_(),
                  *brandLoc_);
}

bool PrivateOpEmitter::emitBrandCheck() {
  MOZ_ASSERT(state_ == State::Reference);

  if (isBrandCheck()) {
    // Emit a CheckPrivateField CheckRhs; note: The message is irrelvant here,
    // it will never be thrown, so DoubleInit was chosen arbitrarily.
    if (!bce_->emitCheckPrivateField(ThrowCondition::OnlyCheckRhs,
                                     ThrowMsgKind::PrivateDoubleInit)) {
      //            [stack] OBJ KEY BBOOL
      return false;
    }

    return true;
  }

  //                [stack] OBJ KEY
  if (isFieldInit()) {
    if (!bce_->emitCheckPrivateField(ThrowCondition::ThrowHas,
                                     ThrowMsgKind::PrivateDoubleInit)) {
      //            [stack] OBJ KEY false
      return false;
    }
  } else {
    bool assigning =
        isSimpleAssignment() || isCompoundAssignment() || isIncDec();
    if (!bce_->emitCheckPrivateField(ThrowCondition::ThrowHasNot,
                                     assigning
                                         ? ThrowMsgKind::MissingPrivateOnSet
                                         : ThrowMsgKind::MissingPrivateOnGet)) {
      //            [stack] OBJ KEY true
      return false;
    }
  }

  return true;
}

bool PrivateOpEmitter::emitReference() {
  MOZ_ASSERT(state_ == State::Start);

  if (!init()) {
    return false;
  }

  if (brandLoc_) {
    if (!emitLoadPrivateBrand()) {
      //            [stack] OBJ BRAND
      return false;
    }
  } else {
    if (!emitLoad(name_, loc_.ref())) {
      //            [stack] OBJ NAME
      return false;
    }
  }
#ifdef DEBUG
  state_ = State::Reference;
#endif
  return true;
}

bool PrivateOpEmitter::emitGet() {
  MOZ_ASSERT(state_ == State::Reference);

  //                [stack] OBJ NAME

  if (brandLoc_) {
    // Note that the decision of what we leave on the stack depends on kind_,
    // not loc_->bindingKind().  We can't emit code for a call just because this
    // private member is a method. `obj.#method` is allowed without a call,
    // just fetching the function object (it's useful in code like
    // `obj.#method.bind(...)`). Even if the user says `obj.#method += 7`, we
    // emit honest bytecode for the brand check, method load, and addition, and
    // throw the error later. This preserves stack nuses/ndefs balance.
    if (!emitBrandCheck()) {
      //            [stack] OBJ BRAND true
      return false;
    }

    if (isCompoundAssignment()) {
      if (!bce_->emit1(JSOp::Pop)) {
        //          [stack] OBJ BRAND
        return false;
      }
    } else if (isCall()) {
      if (!bce_->emitPopN(2)) {
        //          [stack] OBJ
        return false;
      }
    } else {
      if (!bce_->emitPopN(3)) {
        //          [stack]
        return false;
      }
    }

    if (!emitLoad(name_, loc_.ref())) {
      //            [stack] OBJ BRAND METHOD  # if isCompoundAssignment
      //            [stack] OBJ METHOD        # if call
      //            [stack] METHOD            # otherwise
      return false;
    }
  } else {
    if (isCall()) {
      if (!bce_->emitDupAt(1)) {
        //          [stack] OBJ NAME OBJ
        return false;
      }
      if (!bce_->emit1(JSOp::Swap)) {
        //          [stack] OBJ OBJ NAME
        return false;
      }
    }
    //              [stack] OBJ? OBJ NAME
    if (!emitBrandCheck()) {
      //            [stack] OBJ? OBJ NAME true
      return false;
    }
    if (!bce_->emit1(JSOp::Pop)) {
      //            [stack] OBJ? OBJ NAME
      return false;
    }

    if (isCompoundAssignment()) {
      if (!bce_->emit1(JSOp::Dup2)) {
        //          [stack] OBJ NAME OBJ NAME
        return false;
      }
    }

    if (!bce_->emitElemOpBase(JSOp::GetElem)) {
      //            [stack] OBJ NAME VALUE  # if isCompoundAssignment
      //            [stack] OBJ METHOD      # if Call
      //            [stack] VALUE           # otherwise
      return false;
    }
  }

  if (isCall()) {
    if (!bce_->emit1(JSOp::Swap)) {
      //            [stack] METHOD OBJ
      return false;
    }
  }

  //                [stack] OBJ NAME VALUE  # if isCompoundAssignment
  //                [stack] METHOD OBJ      # if call
  //                [stack] VALUE           # otherwise

#ifdef DEBUG
  state_ = State::Get;
#endif
  return true;
}

bool PrivateOpEmitter::emitGetForCallOrNew() { return emitGet(); }

bool PrivateOpEmitter::emitAssignment() {
  MOZ_ASSERT(isSimpleAssignment() || isFieldInit() || isCompoundAssignment());
  MOZ_ASSERT_IF(!isCompoundAssignment(), state_ == State::Reference);
  MOZ_ASSERT_IF(isCompoundAssignment(), state_ == State::Get);

  //                [stack] OBJ KEY RHS

  if (brandLoc_) {
    if (!bce_->emit2(JSOp::ThrowMsg,
                     uint8_t(ThrowMsgKind::AssignToPrivateMethod))) {
      return false;
    }

    // Balance the expression stack.
    if (!bce_->emitPopN(2)) {
      //            [stack] OBJ
      return false;
    }
  } else {
    // Emit a brand check. If this is compound assignment, emitGet() already
    // emitted a check for this object and key. There's no point checking
    // again--a private field can't be removed from an object.
    if (!isCompoundAssignment()) {
      if (!bce_->emitUnpickN(2)) {
        //          [stack] RHS OBJ KEY
        return false;
      }
      if (!emitBrandCheck()) {
        //          [stack] RHS OBJ KEY BOOL
        return false;
      }
      if (!bce_->emit1(JSOp::Pop)) {
        //          [stack] RHS OBJ KEY
        return false;
      }
      if (!bce_->emitPickN(2)) {
        //          [stack] OBJ KEY RHS
        return false;
      }
    }

    JSOp setOp = isFieldInit() ? JSOp::InitHiddenElem : JSOp::StrictSetElem;
    if (!bce_->emitElemOpBase(setOp)) {
      //            [stack] RHS
      return false;
    }
  }

#ifdef DEBUG
  state_ = State::Assignment;
#endif
  return true;
}

bool PrivateOpEmitter::emitIncDec(ValueUsage valueUsage) {
  MOZ_ASSERT(state_ == State::Reference);
  MOZ_ASSERT(isIncDec());
  //                [stack] OBJ NAME

  if (!bce_->emitDupAt(1, 2)) {
    //              [stack] OBJ NAME OBJ NAME
    return false;
  }

  if (!emitGet()) {
    //              [stack] OBJ NAME VALUE
    return false;
  }

  MOZ_ASSERT(state_ == State::Get);

  JSOp incOp = isInc() ? JSOp::Inc : JSOp::Dec;

  if (!bce_->emit1(JSOp::ToNumeric)) {
    //              [stack] OBJ NAME N
    return false;
  }
  if (isPostIncDec() && valueUsage == ValueUsage::WantValue) {
    //              [stack] OBJ NAME N
    if (!bce_->emit1(JSOp::Dup)) {
      //            [stack] OBJ NAME N N
      return false;
    }
    if (!bce_->emit2(JSOp::Unpick, 3)) {
      //            [stack] N OBJ NAME N
      return false;
    }
  }
  if (!bce_->emit1(incOp)) {
    //              [stack] N? OBJ NAME N+1
    return false;
  }

  if (brandLoc_) {
    if (!bce_->emit2(JSOp::ThrowMsg,
                     uint8_t(ThrowMsgKind::AssignToPrivateMethod))) {
      return false;
    }

    // Balance the expression stack.
    if (!bce_->emitPopN(2)) {
      //            [stack] N? N+1
      return false;
    }
  } else {
    if (!bce_->emitElemOpBase(JSOp::StrictSetElem)) {
      //            [stack] N? N+1
      return false;
    }
  }

  if (isPostIncDec() && valueUsage == ValueUsage::WantValue) {
    if (!bce_->emit1(JSOp::Pop)) {
      //            [stack] N
      return false;
    }
  }

  return true;
}
