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

#include "frontend/BytecodeEmitter.h"
#include "frontend/EmitterScope.h"
#include "frontend/IfEmitter.h"
#include "frontend/TryEmitter.h"
#include "frontend/WhileEmitter.h"

using namespace js;
using namespace js::frontend;

UsingEmitter::UsingEmitter(BytecodeEmitter* bce) : bce_(bce) {}

bool UsingEmitter::emitTakeDisposeCapability() {
  if (!bce_->emit1(JSOp::TakeDisposeCapability)) {
    // [stack] RESOURCES-OR-UNDEF
    return false;
  }

  if (!bce_->emit1(JSOp::IsNullOrUndefined)) {
    // [stack] RESOURCES-OR-UNDEF IS-UNDEF
    return false;
  }

  InternalIfEmitter ifUndefined(bce_);

  if (!ifUndefined.emitThenElse()) {
    // [stack] UNDEFINED
    return false;
  }

  if (!bce_->emit1(JSOp::Zero)) {
    // [stack] UNDEFINED 0
    return false;
  }

  if (!ifUndefined.emitElse()) {
    // [stack] RESOURCES
    return false;
  }

  if (!bce_->emit1(JSOp::Dup)) {
    // [stack] RESOURCES RESOURCES
    return false;
  }

  if (!bce_->emitAtomOp(JSOp::GetProp,
                        TaggedParserAtomIndex::WellKnown::length())) {
    // [stack] RESOURCES COUNT
    return false;
  }

  if (!ifUndefined.emitEnd()) {
    // [stack] RESOURCES COUNT
    return false;
  }

  return true;
}

bool UsingEmitter::emitThrowIfException() {
  // [stack] EXC THROWING

  InternalIfEmitter ifThrow(bce_);

  if (!ifThrow.emitThenElse()) {
    // [stack] EXC
    return false;
  }

  if (!bce_->emit1(JSOp::Throw)) {
    // [stack]
    return false;
  }

  if (!ifThrow.emitElse()) {
    // [stack] EXC
    return false;
  }

  if (!bce_->emit1(JSOp::Pop)) {
    // [stack]
    return false;
  }

  if (!ifThrow.emitEnd()) {
    // [stack]
    return false;
  }

  return true;
}

bool DisposalEmitter::emitResourcePropertyAccess(TaggedParserAtomIndex prop,
                                                 unsigned resourcesFromTop) {
  // [stack] # if resourcesFromTop == 1
  // [stack] RESOURCES INDEX
  // [stack] # if resourcesFromTop > 1
  // [stack] RESOURCES INDEX ... (resourcesFromTop - 1 values)
  MOZ_ASSERT(resourcesFromTop >= 1);

  if (!bce_->emitDupAt(resourcesFromTop, 2)) {
    // [stack] RESOURCES INDEX ... RESOURCES INDEX
    return false;
  }

  if (!bce_->emit1(JSOp::GetElem)) {
    // [stack] RESOURCES INDEX ... RESOURCE
    return false;
  }

  if (!bce_->emitAtomOp(JSOp::GetProp, prop)) {
    // [stack] RESOURCES INDEX ... VALUE
    return false;
  }

  return true;
}

// Explicit Resource Management Proposal
// DisposeResources ( disposeCapability, completion )
// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposeresources
// Steps 1-2.
bool DisposalEmitter::prepareForDisposeCapability() {
  MOZ_ASSERT(state_ == State::Start);

  // [stack] THROWING EXC

  if (hasAsyncDisposables_) {
    // Step 1. Let needsAwait be false.
    if (!bce_->emit1(JSOp::False)) {
      // [stack] THROWING EXC NEEDS-AWAIT
      return false;
    }

    // Step 2. Let hasAwaited be false.
    if (!bce_->emit1(JSOp::False)) {
      // [stack] THROWING EXC NEEDS-AWAIT HAS-AWAITED
      return false;
    }

    if (!bce_->emitPickN(3)) {
      // [stack] EXC NEEDS-AWAIT HAS-AWAITED THROWING
      return false;
    }

    if (!bce_->emitPickN(3)) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC
      return false;
    }
  }

  // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC

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

// Explicit Resource Management Proposal
// DisposeResources ( disposeCapability, completion )
// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposeresources
// Steps 3-5, 7.
//
// This implementation of DisposeResources is designed for using and await using
// syntax as well as for disposals in DisposableStack and AsyncDisposableStack,
// it covers the complete algorithm as defined in the spec for both sync and
// async disposals as necessary in bytecode.
bool DisposalEmitter::emitEnd(EmitterScope& es) {
  MOZ_ASSERT(state_ == State::DisposeCapability);

  // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC RESOURCES COUNT

  // For the purpose of readbility some values are omitted from
  // the stack comments and are assumed to be present,
  // we mention the values in the comments as and when they are being
  // operated upon.

  // [stack] ... RESOURCES COUNT

  // We do the iteration in reverse order as per spec,
  // there can be the case when count is 0 and hence index
  // below becomes -1 but the loop condition will ensure
  // no code is executed in that case.
  if (!bce_->emit1(JSOp::Dec)) {
    // [stack] ... RESOURCES INDEX
    return false;
  }

  InternalWhileEmitter wh(bce_);

  // Step 3. For each element resource of
  // disposeCapability.[[DisposableResourceStack]], in reverse list order, do
  if (!wh.emitCond()) {
    // [stack] ... RESOURCES INDEX
    return false;
  }

  if (!bce_->emit1(JSOp::Dup)) {
    // [stack] ... RESOURCES INDEX INDEX
    return false;
  }

  if (!bce_->emit1(JSOp::Zero)) {
    // [stack] ... RESOURCES INDEX INDEX 0
    return false;
  }

  if (!bce_->emit1(JSOp::Ge)) {
    // [stack] ... RESOURCES INDEX BOOL
    return false;
  }

  if (!wh.emitBody()) {
    // [stack] ... RESOURCES INDEX
    return false;
  }

  // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC RESOURCES INDEX

  if (hasAsyncDisposables_) {
    // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX

    // Step 3.b. Let hint be resource.[[Hint]].
    // (reordered)
    // Step 3.d. If hint is sync-dispose and needsAwait is true and hasAwaited
    // is false, then
    if (!emitResourcePropertyAccess(TaggedParserAtomIndex::WellKnown::hint())) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX HINT
      return false;
    }

    // [stack] NEEDS-AWAIT HAS-AWAITED ... HINT

    static_assert(uint8_t(UsingHint::Sync) == 0, "Sync hint must be 0");
    static_assert(uint8_t(UsingHint::Async) == 1, "Async hint must be 1");
    if (!bce_->emit1(JSOp::Not)) {
      // [stack] NEEDS-AWAIT HAS-AWAITED ... IS-SYNC
      return false;
    }

    if (!bce_->emitDupAt(6, 2)) {
      // [stack] NEEDS-AWAIT HAS-AWAITED ... IS-SYNC NEEDS-AWAIT HAS-AWAITED
      return false;
    }

    // [stack] ... IS-SYNC NEEDS-AWAIT HAS-AWAITED

    if (!bce_->emit1(JSOp::Not)) {
      // [stack] ... IS-SYNC NEEDS-AWAIT (!HAS-AWAITED)
      return false;
    }

    // The use of BitAnd is a simple optimisation to avoid having
    // jumps if we were to implement this using && operator. The value
    // IS-SYNC is integer 0 or 1 (see static_assert above) and
    // NEEDS-AWAIT and HAS-AWAITED are boolean values. thus
    // the result of the operation is either 0 or 1 which is
    // truthy value that can be consumed by the IfEmitter.
    if (!bce_->emit1(JSOp::BitAnd)) {
      // [stack] ... IS-SYNC (NEEDS-AWAIT & !HAS-AWAITED)
      return false;
    }

    if (!bce_->emit1(JSOp::BitAnd)) {
      // [stack] ... (IS-SYNC & NEEDS-AWAIT & !HAS-AWAITED)
      return false;
    }

    // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX AWAIT-NEEDED

    InternalIfEmitter ifNeedsSyncDisposeUndefinedAwaited(bce_);

    if (!ifNeedsSyncDisposeUndefinedAwaited.emitThen()) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX
      return false;
    }

    // Step 3.d.i. Perform ! Await(undefined).
    if (!bce_->emit1(JSOp::Undefined)) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX UNDEF
      return false;
    }

    if (!bce_->emitAwaitInScope(es)) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX RESOLVED
      return false;
    }

    // Step 3.d.ii. Set needsAwait to false.
    if (!bce_->emitPickN(6)) {
      // [stack] HAS-AWAITED THROWING EXC RESOURCES INDEX RESOLVED NEEDS-AWAIT
      return false;
    }

    if (!bce_->emitPopN(2)) {
      // [stack] HAS-AWAITED THROWING EXC RESOURCES INDEX
      return false;
    }

    if (!bce_->emit1(JSOp::False)) {
      // [stack] HAS-AWAITED THROWING EXC RESOURCES INDEX NEEDS-AWAIT
      return false;
    }

    if (!bce_->emitUnpickN(5)) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX
      return false;
    }

    if (!ifNeedsSyncDisposeUndefinedAwaited.emitEnd()) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX
      return false;
    }
  }

  // [stack] ... RESOURCES INDEX

  // Step 3.c. Let method be resource.[[DisposeMethod]].
  // (reordered)
  // Step 3.e. If method is not undefined, then
  if (!emitResourcePropertyAccess(TaggedParserAtomIndex::WellKnown::method())) {
    // [stack] ... RESOURCES INDEX METHOD
    return false;
  }

  if (!bce_->emit1(JSOp::IsNullOrUndefined)) {
    // [stack] ... RESOURCES INDEX METHOD IS-UNDEF
    return false;
  }

  InternalIfEmitter ifMethodNotUndefined(bce_);

  if (!ifMethodNotUndefined.emitThenElse(IfEmitter::ConditionKind::Negative)) {
    // [stack] ... RESOURCES INDEX METHOD
    return false;
  }

  if (!bce_->emit1(JSOp::Pop)) {
    // [stack] ... RESOURCES INDEX
    return false;
  }

  TryEmitter tryCall(bce_, TryEmitter::Kind::TryCatch,
                     TryEmitter::ControlKind::NonSyntactic);

  if (!tryCall.emitTry()) {
    // [stack] ... RESOURCES INDEX
    return false;
  }

  // Step 3.c. Let method be resource.[[DisposeMethod]].
  // (reordered)
  if (!emitResourcePropertyAccess(TaggedParserAtomIndex::WellKnown::method())) {
    // [stack] ... RESOURCES INDEX METHOD
    return false;
  }

  // Step 3.a. Let value be resource.[[ResourceValue]].
  // (reordered)
  if (!emitResourcePropertyAccess(TaggedParserAtomIndex::WellKnown::value(),
                                  2)) {
    // [stack] ... RESOURCES INDEX METHOD VALUE
    return false;
  }

  // Step 3.e.i. Let result be Completion(Call(method, value)).
  if (!bce_->emitCall(JSOp::Call, 0)) {
    // [stack] ... RESOURCES INDEX RESULT
    return false;
  }

  if (hasAsyncDisposables_) {
    // Step 3.e.ii. If result is a normal completion and hint is async-dispose,
    // then
    if (!emitResourcePropertyAccess(TaggedParserAtomIndex::WellKnown::hint(),
                                    2)) {
      // [stack] ... RESOURCES INDEX RESULT HINT
      return false;
    }

    // Hint value is either 0 or 1, which can be consumed by the IfEmitter,
    // see static_assert above.
    // [stack] ... RESOURCES INDEX RESULT IS-ASYNC

    InternalIfEmitter ifAsyncDispose(bce_);

    if (!ifAsyncDispose.emitThen()) {
      // [stack] ... RESOURCES INDEX RESULT
      return false;
    }

    // [stack] NEEDS-AWAIT THROWING EXC RESOURCES INDEX RESULT HAS-AWAITED

    // Step 3.e.ii.2. Set hasAwaited to true. (reordered)
    if (!bce_->emitPickN(5)) {
      // [stack] NEEDS-AWAIT THROWING EXC RESOURCES INDEX RESULT HAS-AWAITED
      return false;
    }

    if (!bce_->emit1(JSOp::Pop)) {
      // [stack] NEEDS-AWAIT THROWING EXC RESOURCES INDEX RESULT
      return false;
    }

    if (!bce_->emit1(JSOp::True)) {
      // [stack] NEEDS-AWAIT THROWING EXC RESOURCES INDEX RESULT HAS-AWAITED
      return false;
    }

    if (!bce_->emitUnpickN(5)) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX RESULT
      return false;
    }

    // Step 3.e.ii.1. Set result to Completion(Await(result.[[Value]])).
    if (!bce_->emitAwaitInScope(es)) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX RESOLVED
      return false;
    }

    if (!ifAsyncDispose.emitEnd()) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX RESULT
      return false;
    }
  }

  // [stack] ... THROWING EXC RESOURCES INDEX RESULT

  if (!bce_->emit1(JSOp::Pop)) {
    // [stack] ... THROWING EXC RESOURCES INDEX
    return false;
  }

  // Step 3.e.iii. If result is a throw completion, then
  if (!tryCall.emitCatch()) {
    // [stack] ... THROWING EXC RESOURCES INDEX EXC2
    return false;
  }

  if (!bce_->emitPickN(3)) {
    // [stack] ... THROWING RESOURCES INDEX EXC2 EXC
    return false;
  }

  if (bce_->sc->isSuspendableContext() &&
      bce_->sc->asSuspendableContext()->isGenerator()) {
    // [stack] ... THROWING RESOURCES INDEX EXC2 EXC

    // Generator closure is implemented by throwing a magic value
    // thus when we have a throw completion we must check whether
    // the pending exception is a generator closing exception and overwrite
    // it with the normal exception here or else we will end up exposing
    // the magic value to user program.
    if (!bce_->emit1(JSOp::IsGenClosing)) {
      // [stack] ... THROWING RESOURCES INDEX EXC2 EXC GEN-CLOSING
      return false;
    }

    if (!bce_->emit1(JSOp::Not)) {
      // [stack] ... THROWING RESOURCES INDEX EXC2 EXC !GEN-CLOSING
      return false;
    }

    if (!bce_->emitPickN(5)) {
      // [stack] ... RESOURCES INDEX EXC2 EXC (!GEN-CLOSING) THROWING
      return false;
    }

    if (!bce_->emit1(JSOp::BitAnd)) {
      // [stack] ... RESOURCES INDEX EXC2 EXC (!GEN-CLOSING & THROWING)
      return false;
    }
  } else {
    if (!bce_->emitPickN(4)) {
      // [stack] ... RESOURCES INDEX EXC2 EXC THROWING
      return false;
    }
  }

  // [stack] NEEDS-AWAIT? HAS-AWAITED? RESOURCES INDEX EXC2 EXC THROWING

  InternalIfEmitter ifException(bce_);

  // Step 3.e.iii.1. If completion is a throw completion, then
  if (!ifException.emitThenElse()) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? RESOURCES INDEX EXC2 EXC
    return false;
  }

  // Step 3.e.iii.1.a-f
  if (!bce_->emit1(JSOp::CreateSuppressedError)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? RESOURCES INDEX SUPPRESSED
    return false;
  }

  if (!bce_->emitUnpickN(2)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? SUPPRESSED RESOURCED INDEX
    return false;
  }

  if (!bce_->emit1(JSOp::True)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? SUPPRESSED RESOURCED INDEX THROWING
    return false;
  }

  if (!bce_->emitUnpickN(3)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING SUPPRESSED RESOURCED INDEX
    return false;
  }

  // Step 3.e.iii.2. Else,
  // Step 3.e.iii.2.a. Set completion to result.
  if (!ifException.emitElse()) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? RESOURCES INDEX EXC2 EXC
    return false;
  }

  if (!bce_->emit1(JSOp::Pop)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? RESOURCES INDEX EXC2
    return false;
  }

  if (!bce_->emitUnpickN(2)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? EXC2 RESOURCES INDEX
    return false;
  }

  if (!bce_->emit1(JSOp::True)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? EXC2 RESOURCES INDEX THROWING
    return false;
  }

  if (!bce_->emitUnpickN(3)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC2 RESOURCES INDEX
    return false;
  }

  if (!ifException.emitEnd()) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC RESOURCES INDEX
    return false;
  }

  if (!tryCall.emitEnd()) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC RESOURCES INDEX
    return false;
  }

  // [stack] ... THROWING EXC RESOURCES INDEX

  // Step 3.f. Else,
  // Step 3.f.i. Assert: hint is async-dispose.
  // (implicit)
  if (!ifMethodNotUndefined.emitElse()) {
    // [stack] ... THROWING EXC RESOURCES INDEX METHOD
    return false;
  }

  if (!bce_->emit1(JSOp::Pop)) {
    // [stack] ... THROWING EXC RESOURCES INDEX
    return false;
  }

  if (hasAsyncDisposables_) {
    // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX

    // Step 3.f.ii. Set needsAwait to true.
    if (!bce_->emitPickN(5)) {
      // [stack] HAS-AWAITED THROWING EXC RESOURCES INDEX NEEDS-AWAIT
      return false;
    }

    if (!bce_->emit1(JSOp::Pop)) {
      // [stack] HAS-AWAITED THROWING EXC RESOURCES INDEX
      return false;
    }

    if (!bce_->emit1(JSOp::True)) {
      // [stack] HAS-AWAITED THROWING EXC RESOURCES INDEX NEEDS-AWAIT
      return false;
    }

    if (!bce_->emitUnpickN(5)) {
      // [stack] NEEDS-AWAIT HAS-AWAITED THROWING EXC RESOURCES INDEX
      return false;
    }
  }

  if (!ifMethodNotUndefined.emitEnd()) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC RESOURCES INDEX
    return false;
  }

  if (!bce_->emit1(JSOp::Dec)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC RESOURCES INDEX
    return false;
  }

  if (!wh.emitEnd()) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC RESOURCES INDEX
    return false;
  }

  if (!bce_->emitPopN(2)) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC
    return false;
  }

  if (hasAsyncDisposables_) {
    // Step 4. If needsAwait is true and hasAwaited is false, then
    if (!bce_->emitPickN(3)) {
      // [stack] HAS-AWAITED THROWING EXC NEEDS-AWAIT
      return false;
    }

    if (!bce_->emitPickN(3)) {
      // [stack] THROWING EXC NEEDS-AWAIT HAS-AWAITED
      return false;
    }

    if (!bce_->emit1(JSOp::Not)) {
      // [stack] THROWING EXC NEEDS-AWAIT (!HAS-AWAITED)
      return false;
    }

    if (!bce_->emit1(JSOp::BitAnd)) {
      // [stack] THROWING EXC (NEEDS-AWAIT & !HAS-AWAITED)
      return false;
    }

    InternalIfEmitter ifNeedsUndefinedAwait(bce_);

    if (!ifNeedsUndefinedAwait.emitThen()) {
      // [stack] THROWING EXC
      return false;
    }

    if (!bce_->emit1(JSOp::Undefined)) {
      // [stack] THROWING EXC UNDEF
      return false;
    }

    if (!bce_->emitAwaitInScope(es)) {
      // [stack] THROWING EXC RESOLVED
      return false;
    }

    if (!bce_->emit1(JSOp::Pop)) {
      // [stack] THROWING EXC
      return false;
    }

    if (!ifNeedsUndefinedAwait.emitEnd()) {
      // [stack] THROWING EXC
      return false;
    }
  }

  // Step 7. Return ? completion.
  if (!bce_->emit1(JSOp::Swap)) {
    // [stack] EXC THROWING
    return false;
  }

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

bool UsingEmitter::emitDisposeResourcesForEnvironment(EmitterScope& es) {
  // [stack] THROWING EXC

  DisposalEmitter de(bce_, hasAwaitUsing_);
  if (!de.prepareForDisposeCapability()) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC
    return false;
  }

  // Explicit Resource Management Proposal
  // DisposeResources ( disposeCapability, completion )
  // https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposeresources
  //
  // Step 6. Set disposeCapability.[[DisposableResourceStack]] to a new empty
  // List.
  if (!emitTakeDisposeCapability()) {
    // [stack] NEEDS-AWAIT? HAS-AWAITED? THROWING EXC RESOURCES COUNT
    return false;
  }

  if (!de.emitEnd(es)) {
    // [stack] EXC THROWING
    return false;
  }

  return true;
}

bool UsingEmitter::prepareForDisposableScopeBody(BlockKind blockKind) {
  MOZ_ASSERT(state_ == State::Start);

  // For-of loops are already wrapped in try-catch and have special case
  // handling for the same.
  // See ForOfLoopControl::emitEndCodeNeedingIteratorClose.
  if (blockKind != BlockKind::ForOf) {
    tryEmitter_ = bce_->fc->getAllocator()->make_unique<TryEmitter>(
        bce_, TryEmitter::Kind::TryFinally, TryEmitter::ControlKind::Disposal);
    if (!tryEmitter_) {
      return false;
    }

    if (!tryEmitter_->emitTry()) {
      return false;
    }
  }

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

// Explicit Resource Management Proposal
// GetDisposeMethod ( V, hint )
// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-getdisposemethod
// Steps 1.a-1.b.i., 2-3.
bool UsingEmitter::emitGetDisposeMethod(UsingHint hint) {
  // [stack] VAL

  // Step 1. If hint is async-dispose, then
  if (hint == UsingHint::Async) {
    // Step 1.a. Let method be ? GetMethod(V, @@asyncDispose).
    if (!bce_->emit1(JSOp::Dup)) {
      // [stack] VAL VAL
      return false;
    }

    if (!bce_->emit1(JSOp::Dup)) {
      // [stack] VAL VAL VAL
      return false;
    }

    if (!bce_->emit2(JSOp::Symbol, uint8_t(JS::SymbolCode::asyncDispose))) {
      // [stack] VAL VAL VAL SYMBOL
      return false;
    }

    if (!bce_->emit1(JSOp::GetElem)) {
      // [stack] VAL VAL ASYNC-DISPOSE
      return false;
    }

    // Step 1.b. If method is undefined, then
    // GetMethod returns undefined if the function is null but
    // since we do not do the conversion here we check for
    // null or undefined here.
    if (!bce_->emit1(JSOp::IsNullOrUndefined)) {
      // [stack] VAL VAL ASYNC-DISPOSE IS-NULL-OR-UNDEF
      return false;
    }

    InternalIfEmitter ifAsyncDisposeNullOrUndefined(bce_);

    if (!ifAsyncDisposeNullOrUndefined.emitThenElse()) {
      // [stack] VAL VAL ASYNC-DISPOSE
      return false;
    }

    if (!bce_->emit1(JSOp::Pop)) {
      // [stack] VAL VAL
      return false;
    }

    if (!bce_->emit1(JSOp::Dup)) {
      // [stack] VAL VAL VAL
      return false;
    }

    if (!bce_->emit2(JSOp::Symbol, uint8_t(JS::SymbolCode::dispose))) {
      // [stack] VAL VAL VAL SYMBOL
      return false;
    }

    // Step 1.b.i. Set method to ? GetMethod(V, @@dispose).
    if (!bce_->emit1(JSOp::GetElem)) {
      // [stack] VAL VAL DISPOSE
      return false;
    }

    if (!bce_->emit1(JSOp::True)) {
      // [stack] VAL VAL DISPOSE NEEDS-CLOSURE
      return false;
    }

    if (!ifAsyncDisposeNullOrUndefined.emitElse()) {
      // [stack] VAL VAL ASYNC-DISPOSE
      return false;
    }

    if (!bce_->emit1(JSOp::False)) {
      // [stack] VAL VAL ASYNC-DISPOSE NEEDS-CLOSURE
      return false;
    }

    if (!ifAsyncDisposeNullOrUndefined.emitEnd()) {
      // [stack] VAL VAL METHOD NEEDS-CLOSURE
      return false;
    }

  } else {
    MOZ_ASSERT(hint == UsingHint::Sync);

    // Step 2. Else,
    // Step 2.a. Let method be ? GetMethod(V, @@dispose).
    if (!bce_->emit1(JSOp::Dup)) {
      // [stack] VAL VAL
      return false;
    }

    if (!bce_->emit1(JSOp::Dup)) {
      // [stack] VAL VAL VAL
      return false;
    }

    if (!bce_->emit2(JSOp::Symbol, uint8_t(JS::SymbolCode::dispose))) {
      // [stack] VAL VAL VAL SYMBOL
      return false;
    }

    if (!bce_->emit1(JSOp::GetElem)) {
      // [stack] VAL VAL DISPOSE
      return false;
    }

    if (!bce_->emit1(JSOp::False)) {
      // [stack] VAL VAL DISPOSE NEEDS-CLOSURE
      return false;
    }
  }

  if (!bce_->emitDupAt(1)) {
    // [stack] VAL VAL METHOD NEEDS-CLOSURE METHOD
    return false;
  }

  // According to spec GetMethod throws TypeError if the method is not callable
  // and returns undefined if the value is either undefined or null.
  // but the caller of this function, emitCreateDisposableResource is
  // supposed to throw TypeError as well if returned value is undefined,
  // thus we combine the steps here.
  if (!bce_->emitCheckIsCallable()) {
    // [stack] VAL VAL METHOD NEEDS-CLOSURE METHOD IS-CALLABLE
    return false;
  }

  InternalIfEmitter ifMethodNotCallable(bce_);

  if (!ifMethodNotCallable.emitThen(IfEmitter::ConditionKind::Negative)) {
    // [stack] VAL VAL METHOD NEEDS-CLOSURE METHOD
    return false;
  }

  if (!bce_->emit2(JSOp::ThrowMsg, uint8_t(ThrowMsgKind::DisposeNotCallable))) {
    // [stack] VAL VAL METHOD NEEDS-CLOSURE METHOD
    return false;
  }

  if (!ifMethodNotCallable.emitEnd()) {
    // [stack] VAL VAL METHOD NEEDS-CLOSURE METHOD
    return false;
  }

  if (!bce_->emit1(JSOp::Pop)) {
    // [stack] VAL VAL METHOD NEEDS-CLOSURE
    return false;
  }

  return true;
}

// Explicit Resource Management Proposal
// CreateDisposableResource ( V, hint [ , method ] )
// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-createdisposableresource
bool UsingEmitter::emitCreateDisposableResource(UsingHint hint) {
  // [stack] VAL

  // Step 1. If method is not present, then (implicit)
  // Step 1.a. If V is either null or undefined, then
  if (!bce_->emit1(JSOp::IsNullOrUndefined)) {
    // [stack] VAL IS-NULL-OR-UNDEF
    return false;
  }

  InternalIfEmitter ifNullUndefined(bce_);

  if (!ifNullUndefined.emitThenElse()) {
    // [stack] VAL
    return false;
  }

  // Step 1.a.i. Set V to undefined.
  if (!bce_->emit1(JSOp::Undefined)) {
    // [stack] VAL UNDEF
    return false;
  }

  // Step 1.a.ii. Set method to undefined.
  if (!bce_->emit1(JSOp::Undefined)) {
    // [stack] VAL UNDEF UNDEF
    return false;
  }

  if (!bce_->emit1(JSOp::False)) {
    // [stack] VAL UNDEF UNDEF NEEDS-CLOSURE
    return false;
  }

  // Step 1.b. Else,
  if (!ifNullUndefined.emitElse()) {
    // [stack] VAL
    return false;
  }

  // Step 1.b.i. If V is not an Object, throw a TypeError exception.
  if (!bce_->emitCheckIsObj(CheckIsObjectKind::Disposable)) {
    // [stack] VAL
    return false;
  }

  // Step 1.b.ii. Set method to ? GetDisposeMethod(V, hint).
  // Step 1.b.iii. If method is undefined, throw a TypeError exception.
  if (!emitGetDisposeMethod(hint)) {
    // [stack] VAL VAL METHOD NEEDS-CLOSURE
    return false;
  }

  if (!ifNullUndefined.emitEnd()) {
    // [stack] VAL VAL METHOD NEEDS-CLOSURE
    return false;
  }

  return true;
}

// Explicit Resource Management Proposal
// 7.5.4 AddDisposableResource ( disposeCapability, V, hint [ , method ] )
// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-adddisposableresource
// Steps 1, 3-4.
bool UsingEmitter::prepareForAssignment(UsingHint hint) {
  MOZ_ASSERT(state_ == State::DisposableScopeBody);
  MOZ_ASSERT(bce_->innermostEmitterScope()->hasDisposables());

  if (hint == UsingHint::Async) {
    setHasAwaitUsing(true);
  }

  // [stack] VAL

  // Step 1. If method is not present, then (implicit)
  // Step 1.a. If V is either null or undefined and hint is sync-dispose, return
  // unused.
  if (hint == UsingHint::Sync) {
    if (!bce_->emit1(JSOp::IsNullOrUndefined)) {
      // [stack] VAL IS-NULL-OR-UNDEF
      return false;
    }

    if (!bce_->emit1(JSOp::Not)) {
      // [stack] VAL !IS-NULL-OR-UNDEF
      return false;
    }
  } else {
    MOZ_ASSERT(hint == UsingHint::Async);
    if (!bce_->emit1(JSOp::True)) {
      // [stack] VAL TRUE
      return false;
    }
  }

  // [stack] VAL SHOULD-CREATE-RESOURCE

  InternalIfEmitter ifCreateResource(bce_);

  if (!ifCreateResource.emitThen()) {
    // [stack] VAL
    return false;
  }

  // Step 1.c. Let resource be ? CreateDisposableResource(V, hint).
  if (!emitCreateDisposableResource(hint)) {
    // [stack] VAL VAL METHOD NEEDS-CLOSURE
    return false;
  }

  // Step 3. Append resource to disposeCapability.[[DisposableResourceStack]].
  if (!bce_->emit2(JSOp::AddDisposable, uint8_t(hint))) {
    // [stack] VAL
    return false;
  }

  if (!ifCreateResource.emitEnd()) {
    // [stack] VAL
    return false;
  }

  // Step 4. Return unused.
  return true;
}

bool ForOfDisposalEmitter::prepareForForOfLoopIteration() {
  MOZ_ASSERT(state_ == State::Start);
  EmitterScope* es = bce_->innermostEmitterScopeNoCheck();
  MOZ_ASSERT(es->hasDisposables());

  if (!bce_->emit1(JSOp::False)) {
    // [stack] THROWING
    return false;
  }

  if (!bce_->emit1(JSOp::Undefined)) {
    // [stack] THROWING UNDEF
    return false;
  }

  if (!emitDisposeResourcesForEnvironment(*es)) {
    // [stack] EXC THROWING
    return false;
  }

  if (!emitThrowIfException()) {
    // [stack]
    return false;
  }

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

bool ForOfDisposalEmitter::prepareForForOfIteratorClose() {
  MOZ_ASSERT(state_ == State::Iteration);
  EmitterScope* es = bce_->innermostEmitterScopeNoCheck();
  MOZ_ASSERT(es->hasDisposables());

  // [stack] EXC THROWING

  if (hasAwaitUsing()) {
    if (!bce_->emit1(JSOp::GetRval)) {
      // [stack] EXC THROWING RVAL
      return false;
    }
    if (!bce_->emitUnpickN(2)) {
      // [stack] RVAL EXC THROWING
      return false;
    }
  }

  if (!bce_->emit1(JSOp::Swap)) {
    // [stack] RVAL? THROWING EXC
    return false;
  }

  if (!emitDisposeResourcesForEnvironment(*es)) {
    // [stack] RVAL? EXC THROWING
    return false;
  }

  if (hasAwaitUsing()) {
    if (!bce_->emitPickN(2)) {
      // [stack] EXC THROWING RVAL
      return false;
    }
    if (!bce_->emit1(JSOp::SetRval)) {
      // [stack] EXC THROWING
      return false;
    }
  }

  // The ForOfIteratorClose logic can be emitted multiple times (due different
  // configurations of for-of loops like yields inside a for-of loop for
  // example), and thus prepareForForOfIteratorClose stays in the Iteration
  // state.
  return true;
}

bool UsingEmitter::emitEnd() {
  MOZ_ASSERT(state_ == State::DisposableScopeBody);
  EmitterScope* es = bce_->innermostEmitterScopeNoCheck();
  MOZ_ASSERT(es->hasDisposables());
  MOZ_ASSERT(tryEmitter_.get() != nullptr);

  if (!tryEmitter_->emitFinally()) {
    // [stack] EXC-OR-RESUME STACK THROWING RVAL?
    return false;
  }

  if (!bce_->emitDupAt(tryEmitter_->shouldUpdateRval() ? 1 : 0)) {
    // [stack] EXC-OR-RESUME STACK THROWING RVAL? THROWING
    return false;
  }

  InternalIfEmitter ifThrowing(bce_);

  if (!ifThrowing.emitThenElse()) {
    // [stack] EXC STACK THROWING RVAL?
    return false;
  }

  if (!bce_->emit1(JSOp::True)) {
    // [stack] EXC STACK THROWING RVAL? THROWING
    return false;
  }

  if (!bce_->emitDupAt(tryEmitter_->shouldUpdateRval() ? 4 : 3)) {
    // [stack] EXC STACK THROWING RVAL? THROWING EXC
    return false;
  }

  if (!ifThrowing.emitElse()) {
    // [stack] RESUME STACK THROWING RVAL?
    return false;
  }

  if (!bce_->emit1(JSOp::False)) {
    // [stack] RESUME STACK THROWING RVAL? THROWING
    return false;
  }

  if (!bce_->emit1(JSOp::Undefined)) {
    // [stack] RESUME STACK THROWING RVAL? THROWING UNDEF
    return false;
  }

  if (!ifThrowing.emitEnd()) {
    // [stack] EXC-OR-RESUME STACK THROWING RVAL? THROWING EXC-OR-UNDEF
    return false;
  }

  if (!emitDisposeResourcesForEnvironment(*es)) {
    // [stack] EXC-OR-RESUME STACK THROWING RVAL? DISPOSAL-EXC DISPOSAL-THROWING
    return false;
  }

  if (bce_->sc->isSuspendableContext() &&
      bce_->sc->asSuspendableContext()->isGenerator()) {
    // [stack] ... DISP-EXC DISP-THROWING

    if (!bce_->emit1(JSOp::Swap)) {
      // [stack] ... DISP-THROWING DISP-EXC
      return false;
    }

    if (!bce_->emit1(JSOp::IsGenClosing)) {
      // [stack] ... DISP-THROWING DISP-EXC GEN-CLOSING
      return false;
    }

    if (!bce_->emit1(JSOp::Not)) {
      // [stack] ... DISP-THROWING DISP-EXC !GEN-CLOSING
      return false;
    }

    if (!bce_->emitPickN(2)) {
      // [stack] ... DISP-EXC !GEN-CLOSING DISP-THROWING
      return false;
    }

    if (!bce_->emit1(JSOp::BitAnd)) {
      // [stack] ... DISP-EXC (DISP-THROWING & !GEN-CLOSING)
      return false;
    }
  }

  if (!emitThrowIfException()) {
    // [stack] EXC-OR-RESUME STACK THROWING RVAL?
    return false;
  }

  if (!tryEmitter_->emitEnd()) {
    // [stack]
    return false;
  }

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

// Explicit Resource Management Proposal
// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset
// Step 9.k.i.
bool NonLocalIteratorCloseUsingEmitter::prepareForIteratorClose(
    EmitterScope& es) {
  MOZ_ASSERT(state_ == State::Start);
  // In this function we prepare for the closure of the iterator but first
  // emitting the dispose loop and preseving exceptions on the stack and after
  // that emitting a try to wrap the iterator closure code that shall come after
  // this.
  if (!es.hasDisposables()) {
#ifdef DEBUG
    state_ = State::IteratorClose;
#endif
    return true;
  }

  setHasAwaitUsing(es.hasAsyncDisposables());

  // [stack] ITER

  if (hasAwaitUsing()) {
    // Since in case of for-of loop the disposals are wrapped in a try-catch,
    // and await-using would suspend the frame causing the rval to be lost,
    // we need to preserve the rval and restore it after the disposal.
    if (!bce_->emit1(JSOp::GetRval)) {
      // [stack] ITER RVAL
      return false;
    }
  }

  // [stack] ITER RVAL?

  if (!bce_->emit1(JSOp::False)) {
    // [stack] THROWING
    return false;
  }

  if (!bce_->emit1(JSOp::Undefined)) {
    // [stack] THROWING UNDEF
    return false;
  }

  if (!emitDisposeResourcesForEnvironment(es)) {
    // [stack] ITER RVAL? EXC-DISPOSE DISPOSE-THROWING
    return false;
  }

  if (!bce_->emitPickN(hasAwaitUsing() ? 3 : 2)) {
    // [stack] RVAL? EXC-DISPOSE DISPOSE-THROWING ITER
    return false;
  }

  if (hasAwaitUsing()) {
    if (!bce_->emitPickN(3)) {
      // [stack] EXC-DISPOSE DISPOSE-THROWING ITER RVAL
      return false;
    }

    if (!bce_->emit1(JSOp::SetRval)) {
      // [stack] EXC-DISPOSE DISPOSE-THROWING ITER
      return false;
    }
  }

  tryClosingIterator_ = bce_->fc->getAllocator()->make_unique<TryEmitter>(
      bce_, TryEmitter::Kind::TryCatch, TryEmitter::ControlKind::NonSyntactic);
  if (!tryClosingIterator_) {
    return false;
  }

  if (!tryClosingIterator_->emitTry()) {
    // [stack] EXC-DISPOSE DISPOSE-THROWING ITER
    return false;
  }

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

// Explicit Resource Management Proposal
// 7.4.9 IteratorClose ( iteratorRecord, completion )
// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-iteratorclose&secAll=true
// Steps 5-6.
//
// 7.4.11 AsyncIteratorClose ( iteratorRecord, completion )
// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-asynciteratorclose&secAll=true
// Steps 5-6.
bool NonLocalIteratorCloseUsingEmitter::emitEnd() {
  // This function handles the steps after the iterator close operation which
  // may or may not have thrown. note that prepareForIteratorClose would have
  // already wrapped the iterator closure with a try and have preserved any
  // exception by the disposal operation on the stack now this function does the
  // equivalent of the following pseudocode (consider excDispose and
  // disposeThrowing and iter equal to corresponding values left on stack by
  // prepareForIteratorClose):
  //
  //
  // let excToThrow, throwing = disposeThrowing;
  // try {
  //   IteratorClose(iter);
  // } catch (excIterClose) {
  //   throwing = true;
  //   if (disposeThrowing) {
  //     excToThrow = excDispose;
  //   } else {
  //     excToThrow = excIterClose;
  //   }
  // }
  // if (throwing) {
  //   throw excToThrow;
  // }
  //
  MOZ_ASSERT(state_ == State::IteratorClose);

  if (!tryClosingIterator_) {
#ifdef DEBUG
    state_ = State::End;
#endif
    return true;
  }

  // [stack] EXC-DISPOSE DISPOSE-THROWING ITER

  if (!tryClosingIterator_->emitCatch()) {
    // [stack] EXC-DISPOSE DISPOSE-THROWING ITER EXC-ITER-CLOSE
    return false;
  }

  if (!bce_->emitPickN(2)) {
    // [stack] EXC-DISPOSE ITER EXC-ITER-CLOSE DISPOSE-THROWING
    return false;
  }

  InternalIfEmitter ifDisposeWasThrowing(bce_);

  if (!ifDisposeWasThrowing.emitThenElse()) {
    // [stack] EXC-DISPOSE ITER EXC-ITER-CLOSE
    return false;
  }

  if (!bce_->emit1(JSOp::Pop)) {
    // [stack] EXC-DISPOSE ITER
    return false;
  }

  if (!bce_->emit1(JSOp::True)) {
    // [stack] EXC-DISPOSE ITER THROWING
    return false;
  }

  // This swap operation is to make the stack state consistent with the
  // the non-throwing case.
  if (!bce_->emit1(JSOp::Swap)) {
    // [stack] EXC-DISPOSE THROWING ITER
    return false;
  }

  if (!ifDisposeWasThrowing.emitElse()) {
    // [stack] EXC-DISPOSE ITER EXC-ITER-CLOSE
    return false;
  }

  if (!bce_->emitPickN(2)) {
    // [stack] ITER EXC-ITER-CLOSE EXC-DISPOSE
    return false;
  }

  if (!bce_->emit1(JSOp::Pop)) {
    // [stack] ITER EXC-ITER-CLOSE
    return false;
  }

  if (!bce_->emit1(JSOp::Swap)) {
    // [stack] EXC-ITER-CLOSE ITER
    return false;
  }

  if (!bce_->emit1(JSOp::True)) {
    // [stack] EXC-ITER-CLOSE ITER THROWING
    return false;
  }

  if (!bce_->emit1(JSOp::Swap)) {
    // [stack] EXC-ITER-CLOSE THROWING ITER
    return false;
  }

  if (!ifDisposeWasThrowing.emitEnd()) {
    // [stack] EXC THROWING ITER
    return false;
  }

  if (!tryClosingIterator_->emitEnd()) {
    // [stack] EXC THROWING ITER
    return false;
  }

  if (!bce_->emit1(JSOp::Swap)) {
    // [stack] EXC ITER THROWING
    return false;
  }

  InternalIfEmitter ifThrowing(bce_);

  if (!ifThrowing.emitThenElse()) {
    // [stack] EXC ITER
    return false;
  }

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

  if (!bce_->emit1(JSOp::Throw)) {
    // [stack] ITER
    return false;
  }

  if (!ifThrowing.emitElse()) {
    // [stack] EXC ITER
    return false;
  }

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

  if (!bce_->emit1(JSOp::Pop)) {
    // [stack] ITER
    return false;
  }

  if (!ifThrowing.emitEnd()) {
    // [stack] ITER
    return false;
  }

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