/**
 * Copyright 2014 Shape Security, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const { ErrorMessages } = require('./errors');

const acceptRegex = require('shift-regexp-acceptor');

const { Tokenizer, TokenClass, TokenType } = require('./tokenizer');

const AST = require('shift-ast');

// Empty parameter list for ArrowExpression
const ARROW_EXPRESSION_PARAMS = 'CoverParenthesizedExpressionAndArrowParameterList';
const EXPORT_UNKNOWN_SPECIFIER = 'ExportNameOfUnknownType';

const Precedence = {
  Sequence: 0,
  Yield: 1,
  Assignment: 1,
  Conditional: 2,
  ArrowFunction: 2,
  LogicalOR: 3,
  LogicalAND: 4,
  BitwiseOR: 5,
  BitwiseXOR: 6,
  BitwiseAND: 7,
  Equality: 8,
  Relational: 9,
  BitwiseSHIFT: 10,
  Additive: 11,
  Multiplicative: 12,
  Unary: 13,
  Postfix: 14,
  Call: 15,
  New: 16,
  TaggedTemplate: 17,
  Member: 18,
  Primary: 19,
};

const BinaryPrecedence = {
  '||': Precedence.LogicalOR,
  '&&': Precedence.LogicalAND,
  '|': Precedence.BitwiseOR,
  '^': Precedence.BitwiseXOR,
  '&': Precedence.BitwiseAND,
  '==': Precedence.Equality,
  '!=': Precedence.Equality,
  '===': Precedence.Equality,
  '!==': Precedence.Equality,
  '<': Precedence.Relational,
  '>': Precedence.Relational,
  '<=': Precedence.Relational,
  '>=': Precedence.Relational,
  'in': Precedence.Relational,
  'instanceof': Precedence.Relational,
  '<<': Precedence.BitwiseSHIFT,
  '>>': Precedence.BitwiseSHIFT,
  '>>>': Precedence.BitwiseSHIFT,
  '+': Precedence.Additive,
  '-': Precedence.Additive,
  '*': Precedence.Multiplicative,
  '%': Precedence.Multiplicative,
  '/': Precedence.Multiplicative,
};

function isValidSimpleAssignmentTarget(node) {
  if (node == null) return false;
  switch (node.type) {
    case 'IdentifierExpression':
    case 'ComputedMemberExpression':
    case 'StaticMemberExpression':
      return true;
  }
  return false;
}

function isPrefixOperator(token) {
  switch (token.type) {
    case TokenType.INC:
    case TokenType.DEC:
    case TokenType.ADD:
    case TokenType.SUB:
    case TokenType.BIT_NOT:
    case TokenType.NOT:
    case TokenType.DELETE:
    case TokenType.VOID:
    case TokenType.TYPEOF:
      return true;
  }
  return false;
}

function isUpdateOperator(token) {
  return token.type === TokenType.INC || token.type === TokenType.DEC;
}

class GenericParser extends Tokenizer {
  constructor(source) {
    super(source);
    this.allowIn = true;
    this.inFunctionBody = false;
    this.inParameter = false;
    this.allowYieldExpression = false;
    this.allowAwaitExpression = false;
    this.firstAwaitLocation = null; // for forbidding `await` in async arrow params.
    this.module = false;
    this.moduleIsTheGoalSymbol = false;
    this.strict = false;

    // Cover grammar
    this.isBindingElement = true;
    this.isAssignmentTarget = true;
    this.firstExprError = null;
  }

  match(subType) {
    return this.lookahead.type === subType;
  }

  matchIdentifier() {
    switch (this.lookahead.type) {
      case TokenType.IDENTIFIER:
      case TokenType.LET:
      case TokenType.YIELD:
      case TokenType.ASYNC:
        return true;
      case TokenType.AWAIT:
        if (!this.moduleIsTheGoalSymbol) {
          if (this.firstAwaitLocation === null) {
            this.firstAwaitLocation = this.getLocation();
          }
          return true;
        }
        return false;
      case TokenType.ESCAPED_KEYWORD:
        if (this.lookahead.value === 'await' && !this.moduleIsTheGoalSymbol) {
          if (this.firstAwaitLocation === null) {
            this.firstAwaitLocation = this.getLocation();
          }
          return true;
        }
        return this.lookahead.value === 'let'
          || this.lookahead.value === 'yield'
          || this.lookahead.value === 'async';
    }
    return false;
  }

  eat(tokenType) {
    if (this.lookahead.type === tokenType) {
      return this.lex();
    }
    return null;
  }

  expect(tokenType) {
    if (this.lookahead.type === tokenType) {
      return this.lex();
    }
    throw this.createUnexpected(this.lookahead);
  }

  matchContextualKeyword(keyword) {
    return this.lookahead.type === TokenType.IDENTIFIER && !this.lookahead.escaped && this.lookahead.value === keyword;
  }

  expectContextualKeyword(keyword) {
    if (this.lookahead.type === TokenType.IDENTIFIER && !this.lookahead.escaped && this.lookahead.value === keyword) {
      return this.lex();
    }
    throw this.createUnexpected(this.lookahead);
  }

  eatContextualKeyword(keyword) {
    if (this.lookahead.type === TokenType.IDENTIFIER && !this.lookahead.escaped && this.lookahead.value === keyword) {
      return this.lex();
    }
    return null;
  }

  consumeSemicolon() {
    if (this.eat(TokenType.SEMICOLON)) return;
    if (this.hasLineTerminatorBeforeNext) return;
    if (!this.eof() && !this.match(TokenType.RBRACE)) {
      throw this.createUnexpected(this.lookahead);
    }
  }

  // this is a no-op, reserved for future use
  startNode(node) {
    return node;
  }

  copyNode(src, dest) {
    return dest;
  }

  finishNode(node /* , startState */) {
    return node;
  }

  parseModule() {
    this.moduleIsTheGoalSymbol = this.module = this.strict = true;
    this.lookahead = this.advance();

    let startState = this.startNode();
    let { directives, statements } = this.parseBody();
    if (!this.match(TokenType.EOS)) {
      throw this.createUnexpected(this.lookahead);
    }
    return this.finishNode(new AST.Module({ directives, items: statements }), startState);
  }

  parseScript() {
    this.lookahead = this.advance();

    let startState = this.startNode();
    let { directives, statements } = this.parseBody();
    if (!this.match(TokenType.EOS)) {
      throw this.createUnexpected(this.lookahead);
    }
    return this.finishNode(new AST.Script({ directives, statements }), startState);
  }

  parseFunctionBody() {
    let oldInFunctionBody = this.inFunctionBody;
    let oldModule = this.module;
    let oldStrict = this.strict;
    this.inFunctionBody = true;
    this.module = false;

    let startState = this.startNode();
    this.expect(TokenType.LBRACE);
    let body = new AST.FunctionBody(this.parseBody());
    this.expect(TokenType.RBRACE);
    body = this.finishNode(body, startState);

    this.inFunctionBody = oldInFunctionBody;
    this.module = oldModule;
    this.strict = oldStrict;

    return body;
  }

  parseBody() {
    let directives = [], statements = [], parsingDirectives = true, directiveOctal = null;

    while (true) {
      if (this.eof() || this.match(TokenType.RBRACE)) break;
      let token = this.lookahead;
      let text = token.slice.text;
      let isStringLiteral = token.type === TokenType.STRING;
      let isModule = this.module;
      let directiveLocation = this.getLocation();
      let directiveStartState = this.startNode();
      let stmt = isModule ? this.parseModuleItem() : this.parseStatementListItem();
      if (parsingDirectives) {
        if (isStringLiteral && stmt.type === 'ExpressionStatement' && stmt.expression.type === 'LiteralStringExpression') {
          if (!directiveOctal && token.octal) {
            directiveOctal = this.createErrorWithLocation(directiveLocation, 'Unexpected legacy octal escape sequence: \\' + token.octal);
          }
          let rawValue = text.slice(1, -1);
          if (rawValue === 'use strict') {
            this.strict = true;
          }
          directives.push(this.finishNode(new AST.Directive({ rawValue }), directiveStartState));
        } else {
          parsingDirectives = false;
          if (directiveOctal && this.strict) {
            throw directiveOctal;
          }
          statements.push(stmt);
        }
      } else {
        statements.push(stmt);
      }
    }
    if (directiveOctal && this.strict) {
      throw directiveOctal;
    }

    return { directives, statements };
  }

  parseImportSpecifier() {
    let startState = this.startNode(), name;
    if (this.matchIdentifier()) {
      name = this.parseIdentifier();
      if (!this.eatContextualKeyword('as')) {
        return this.finishNode(new AST.ImportSpecifier({
          name: null,
          binding: this.finishNode(new AST.BindingIdentifier({ name }), startState),
        }), startState);
      }
    } else if (this.lookahead.type.klass.isIdentifierName) {
      name = this.parseIdentifierName();
      this.expectContextualKeyword('as');
    }

    return this.finishNode(new AST.ImportSpecifier({ name, binding: this.parseBindingIdentifier() }), startState);
  }

  parseNameSpaceBinding() {
    this.expect(TokenType.MUL);
    this.expectContextualKeyword('as');
    return this.parseBindingIdentifier();
  }

  parseNamedImports() {
    let result = [];
    this.expect(TokenType.LBRACE);
    while (!this.eat(TokenType.RBRACE)) {
      result.push(this.parseImportSpecifier());
      if (!this.eat(TokenType.COMMA)) {
        this.expect(TokenType.RBRACE);
        break;
      }
    }
    return result;
  }

  parseFromClause() {
    this.expectContextualKeyword('from');
    let value = this.expect(TokenType.STRING).str;
    return value;
  }

  parseImportDeclaration() {
    let startState = this.startNode(), defaultBinding = null, moduleSpecifier;
    this.expect(TokenType.IMPORT);
    if (this.match(TokenType.STRING)) {
      moduleSpecifier = this.lex().str;
      this.consumeSemicolon();
      return this.finishNode(new AST.Import({ defaultBinding: null, namedImports: [], moduleSpecifier }), startState);
    }
    if (this.matchIdentifier()) {
      defaultBinding = this.parseBindingIdentifier();
      if (!this.eat(TokenType.COMMA)) {
        let decl = new AST.Import({ defaultBinding, namedImports: [], moduleSpecifier: this.parseFromClause() });
        this.consumeSemicolon();
        return this.finishNode(decl, startState);
      }
    }
    if (this.match(TokenType.MUL)) {
      let decl = new AST.ImportNamespace({
        defaultBinding,
        namespaceBinding: this.parseNameSpaceBinding(),
        moduleSpecifier: this.parseFromClause(),
      });
      this.consumeSemicolon();
      return this.finishNode(decl, startState);
    } else if (this.match(TokenType.LBRACE)) {
      let decl = new AST.Import({
        defaultBinding,
        namedImports: this.parseNamedImports(),
        moduleSpecifier: this.parseFromClause(),
      });
      this.consumeSemicolon();
      return this.finishNode(decl, startState);
    }
    throw this.createUnexpected(this.lookahead);
  }

  parseExportSpecifier() {
    let startState = this.startNode();
    let name = this.finishNode({ type: EXPORT_UNKNOWN_SPECIFIER, isIdentifier: this.matchIdentifier(), value: this.parseIdentifierName() }, startState);
    if (this.eatContextualKeyword('as')) {
      let exportedName = this.parseIdentifierName();
      return this.finishNode({ name, exportedName }, startState);
    }
    return this.finishNode({ name, exportedName: null }, startState);
  }

  parseExportClause() {
    this.expect(TokenType.LBRACE);
    let result = [];
    while (!this.eat(TokenType.RBRACE)) {
      result.push(this.parseExportSpecifier());
      if (!this.eat(TokenType.COMMA)) {
        this.expect(TokenType.RBRACE);
        break;
      }
    }
    return result;
  }

  parseExportDeclaration() {
    let startState = this.startNode(), decl;
    this.expect(TokenType.EXPORT);
    switch (this.lookahead.type) {
      case TokenType.MUL:
        this.lex();
        // export * FromClause ;
        decl = new AST.ExportAllFrom({ moduleSpecifier: this.parseFromClause() });
        this.consumeSemicolon();
        break;
      case TokenType.LBRACE: {
        // export ExportClause FromClause ;
        // export ExportClause ;
        let namedExports = this.parseExportClause();
        let moduleSpecifier = null;
        if (this.matchContextualKeyword('from')) {
          moduleSpecifier = this.parseFromClause();
          decl = new AST.ExportFrom({ namedExports: namedExports.map(e => this.copyNode(e, new AST.ExportFromSpecifier({ name: e.name.value, exportedName: e.exportedName }))), moduleSpecifier });
        } else {
          namedExports.forEach(({ name }) => {
            if (!name.isIdentifier) {
              throw this.createError(ErrorMessages.ILLEGAL_EXPORTED_NAME);
            }
          });
          decl = new AST.ExportLocals({ namedExports: namedExports.map(e => this.copyNode(e, new AST.ExportLocalSpecifier({ name: this.copyNode(e.name, new AST.IdentifierExpression({ name: e.name.value })), exportedName: e.exportedName }))) });
        }
        this.consumeSemicolon();
        break;
      }
      case TokenType.CLASS:
        // export ClassDeclaration
        decl = new AST.Export({ declaration: this.parseClass({ isExpr: false, inDefault: false }) });
        break;
      case TokenType.FUNCTION:
        // export HoistableDeclaration
        decl = new AST.Export({ declaration: this.parseFunction({ isExpr: false, inDefault: false, allowGenerator: true, isAsync: false }) });
        break;
      case TokenType.ASYNC: {
        let preAsyncStartState = this.startNode();
        this.lex();
        decl = new AST.Export({ declaration: this.parseFunction({ isExpr: false, inDefault: false, allowGenerator: true, isAsync: true, startState: preAsyncStartState }) });
        break;
      }
      case TokenType.DEFAULT:
        this.lex();
        switch (this.lookahead.type) {
          case TokenType.FUNCTION:
            // export default HoistableDeclaration[Default]
            decl = new AST.ExportDefault({
              body: this.parseFunction({ isExpr: false, inDefault: true, allowGenerator: true, isAsync: false }),
            });
            break;
          case TokenType.CLASS:
            // export default ClassDeclaration[Default]
            decl = new AST.ExportDefault({ body: this.parseClass({ isExpr: false, inDefault: true }) });
            break;
          case TokenType.ASYNC: {
            let preAsyncStartState = this.startNode();
            let lexerState = this.saveLexerState();
            this.lex();
            if (!this.hasLineTerminatorBeforeNext && this.match(TokenType.FUNCTION)) {
              decl = new AST.ExportDefault({
                body: this.parseFunction({ isExpr: false, inDefault: true, allowGenerator: false, isAsync: true, startState: preAsyncStartState }),
              });
              break;
            }
            this.restoreLexerState(lexerState);
          }
          // else fall through
          default:
            // export default [lookahead ∉ {function, async [no LineTerminatorHere] function, class}] AssignmentExpression[In] ;
            decl = new AST.ExportDefault({ body: this.parseAssignmentExpression() });
            this.consumeSemicolon();
            break;
        }
        break;
      case TokenType.VAR:
      case TokenType.LET:
      case TokenType.CONST:
        // export LexicalDeclaration
        decl = new AST.Export({ declaration: this.parseVariableDeclaration(true) });
        this.consumeSemicolon();
        break;
      default:
        throw this.createUnexpected(this.lookahead);
    }
    return this.finishNode(decl, startState);
  }

  parseModuleItem() {
    switch (this.lookahead.type) {
      case TokenType.IMPORT:
        return this.parseImportDeclaration();
      case TokenType.EXPORT:
        return this.parseExportDeclaration();
      default:
        return this.parseStatementListItem();
    }
  }

  lookaheadLexicalDeclaration() {
    if (this.match(TokenType.LET) || this.match(TokenType.CONST)) {
      let lexerState = this.saveLexerState();
      this.lex();
      if (
        this.matchIdentifier() ||
        this.match(TokenType.LBRACE) ||
        this.match(TokenType.LBRACK)
      ) {
        this.restoreLexerState(lexerState);
        return true;
      }
      this.restoreLexerState(lexerState);
    }
    return false;
  }

  parseStatementListItem() {
    if (this.eof()) throw this.createUnexpected(this.lookahead);

    switch (this.lookahead.type) {
      case TokenType.FUNCTION:
        return this.parseFunction({ isExpr: false, inDefault: false, allowGenerator: true, isAsync: false });
      case TokenType.CLASS:
        return this.parseClass({ isExpr: false, inDefault: false });
      case TokenType.ASYNC: {
        let preAsyncStartState = this.getLocation();
        let lexerState = this.saveLexerState();
        this.lex();
        if (!this.hasLineTerminatorBeforeNext && this.match(TokenType.FUNCTION)) {
          return this.parseFunction({ isExpr: false, inDefault: false, allowGenerator: true, isAsync: true, startState: preAsyncStartState });
        }
        this.restoreLexerState(lexerState);
        return this.parseStatement();
      }
      default:
        if (this.lookaheadLexicalDeclaration()) {
          let startState = this.startNode();
          return this.finishNode(this.parseVariableDeclarationStatement(), startState);
        }
        return this.parseStatement();
    }
  }

  parseStatement() {
    let startState = this.startNode();
    let stmt = this.isolateCoverGrammar(this.parseStatementHelper);
    return this.finishNode(stmt, startState);
  }

  parseStatementHelper() {
    if (this.eof()) {
      throw this.createUnexpected(this.lookahead);
    }

    switch (this.lookahead.type) {
      case TokenType.SEMICOLON:
        return this.parseEmptyStatement();
      case TokenType.LBRACE:
        return this.parseBlockStatement();
      case TokenType.LPAREN:
        return this.parseExpressionStatement();
      case TokenType.BREAK:
        return this.parseBreakStatement();
      case TokenType.CONTINUE:
        return this.parseContinueStatement();
      case TokenType.DEBUGGER:
        return this.parseDebuggerStatement();
      case TokenType.DO:
        return this.parseDoWhileStatement();
      case TokenType.FOR:
        return this.parseForStatement();
      case TokenType.IF:
        return this.parseIfStatement();
      case TokenType.RETURN:
        return this.parseReturnStatement();
      case TokenType.SWITCH:
        return this.parseSwitchStatement();
      case TokenType.THROW:
        return this.parseThrowStatement();
      case TokenType.TRY:
        return this.parseTryStatement();
      case TokenType.VAR:
        return this.parseVariableDeclarationStatement();
      case TokenType.WHILE:
        return this.parseWhileStatement();
      case TokenType.WITH:
        return this.parseWithStatement();
      case TokenType.FUNCTION:
      case TokenType.CLASS:
        throw this.createUnexpected(this.lookahead);

      default: {
        let lexerState = this.saveLexerState();
        if (this.eat(TokenType.LET)) {
          if (this.match(TokenType.LBRACK)) {
            this.restoreLexerState(lexerState);
            throw this.createUnexpected(this.lookahead);
          }
          this.restoreLexerState(lexerState);
        } else if (this.eat(TokenType.ASYNC)) {
          if (!this.hasLineTerminatorBeforeNext && this.match(TokenType.FUNCTION)) {
            throw this.createUnexpected(this.lookahead);
          }
          this.restoreLexerState(lexerState);
        }
        let expr = this.parseExpression();
        // 12.12 Labelled Statements;
        if (expr.type === 'IdentifierExpression' && this.eat(TokenType.COLON)) {
          let labeledBody = this.match(TokenType.FUNCTION)
            ? this.parseFunction({ isExpr: false, inDefault: false, allowGenerator: false, isAsync: false })
            : this.parseStatement();
          return new AST.LabeledStatement({ label: expr.name, body: labeledBody });
        }
        this.consumeSemicolon();
        return new AST.ExpressionStatement({ expression: expr });
      }
    }
  }

  parseEmptyStatement() {
    this.lex();
    return new AST.EmptyStatement;
  }

  parseBlockStatement() {
    return new AST.BlockStatement({ block: this.parseBlock() });
  }

  parseExpressionStatement() {
    let expr = this.parseExpression();
    this.consumeSemicolon();
    return new AST.ExpressionStatement({ expression: expr });
  }

  parseBreakStatement() {
    this.lex();

    // Catch the very common case first: immediately a semicolon (U+003B).
    if (this.eat(TokenType.SEMICOLON) || this.hasLineTerminatorBeforeNext) {
      return new AST.BreakStatement({ label: null });
    }

    let label = null;
    if (this.matchIdentifier()) {
      label = this.parseIdentifier();
    }

    this.consumeSemicolon();

    return new AST.BreakStatement({ label });
  }

  parseContinueStatement() {
    this.lex();

    // Catch the very common case first: immediately a semicolon (U+003B).
    if (this.eat(TokenType.SEMICOLON) || this.hasLineTerminatorBeforeNext) {
      return new AST.ContinueStatement({ label: null });
    }

    let label = null;
    if (this.matchIdentifier()) {
      label = this.parseIdentifier();
    }

    this.consumeSemicolon();

    return new AST.ContinueStatement({ label });
  }


  parseDebuggerStatement() {
    this.lex();
    this.consumeSemicolon();
    return new AST.DebuggerStatement;
  }

  parseDoWhileStatement() {
    this.lex();
    let body = this.parseStatement();
    this.expect(TokenType.WHILE);
    this.expect(TokenType.LPAREN);
    let test = this.parseExpression();
    this.expect(TokenType.RPAREN);
    this.eat(TokenType.SEMICOLON);
    return new AST.DoWhileStatement({ body, test });
  }

  parseForStatement() {
    this.lex();
    let isAwait = this.allowAwaitExpression && this.eat(TokenType.AWAIT);
    this.expect(TokenType.LPAREN);
    let test = null;
    let right = null;
    if (isAwait && this.match(TokenType.SEMICOLON)) {
      throw this.createUnexpected(this.lookahead);
    }
    if (this.eat(TokenType.SEMICOLON)) {
      if (!this.match(TokenType.SEMICOLON)) {
        test = this.parseExpression();
      }
      this.expect(TokenType.SEMICOLON);
      if (!this.match(TokenType.RPAREN)) {
        right = this.parseExpression();
      }
      return new AST.ForStatement({ init: null, test, update: right, body: this.getIteratorStatementEpilogue() });
    }
    let startsWithLet = this.match(TokenType.LET);
    let isForDecl = this.lookaheadLexicalDeclaration();
    let leftStartState = this.startNode();
    if (this.match(TokenType.VAR) || isForDecl) {
      let previousAllowIn = this.allowIn;
      this.allowIn = false;
      let init = this.parseVariableDeclaration(false);
      this.allowIn = previousAllowIn;

      if (init.declarators.length === 1 && (this.match(TokenType.IN) || this.matchContextualKeyword('of'))) {
        let ctor;
        let decl = init.declarators[0];

        if (this.match(TokenType.IN)) {
          if (isAwait) {
            throw this.createUnexpected(this.lookahead);
          }
          if (decl.init !== null && (this.strict || init.kind !== 'var' || decl.binding.type !== 'BindingIdentifier')) {
            throw this.createError(ErrorMessages.INVALID_VAR_INIT_FOR_IN);
          }
          ctor = AST.ForInStatement;
          this.lex();
          right = this.parseExpression();
        } else {
          if (decl.init !== null) {
            throw this.createError(isAwait ? ErrorMessages.INVALID_VAR_INIT_FOR_AWAIT : ErrorMessages.INVALID_VAR_INIT_FOR_OF);
          }
          if (isAwait) {
            ctor = AST.ForAwaitStatement;
          } else {
            ctor = AST.ForOfStatement;
          }
          this.lex();
          right = this.parseAssignmentExpression();
        }

        let body = this.getIteratorStatementEpilogue();

        return new ctor({ left: init, right, body });
      } else if (isAwait) {
        throw this.createUnexpected(this.lookahead);
      }
      this.expect(TokenType.SEMICOLON);
      if (init.declarators.some(decl => decl.binding.type !== 'BindingIdentifier' && decl.init === null)) {
        throw this.createError(ErrorMessages.UNINITIALIZED_BINDINGPATTERN_IN_FOR_INIT);
      }
      if (!this.match(TokenType.SEMICOLON)) {
        test = this.parseExpression();
      }
      this.expect(TokenType.SEMICOLON);
      if (!this.match(TokenType.RPAREN)) {
        right = this.parseExpression();
      }
      return new AST.ForStatement({ init, test, update: right, body: this.getIteratorStatementEpilogue() });

    }
    let previousAllowIn = this.allowIn;
    this.allowIn = false;
    let expr = this.inheritCoverGrammar(this.parseAssignmentExpressionOrTarget);
    this.allowIn = previousAllowIn;

    if (this.isAssignmentTarget && expr.type !== 'AssignmentExpression' && (this.match(TokenType.IN) || this.matchContextualKeyword('of'))) {
      if (expr.type === 'ObjectAssignmentTarget' || expr.type === 'ArrayAssignmentTarget') {
        this.firstExprError = null;
      }
      if (startsWithLet && this.matchContextualKeyword('of')) {
        throw this.createError(isAwait ? ErrorMessages.INVALID_LHS_IN_FOR_AWAIT : ErrorMessages.INVALID_LHS_IN_FOR_OF);
      }
      let ctor;
      if (this.match(TokenType.IN)) {
        if (isAwait) {
          throw this.createUnexpected(this.lookahead);
        }
        ctor = AST.ForInStatement;
        this.lex();
        right = this.parseExpression();
      } else {
        if (isAwait) {
          ctor = AST.ForAwaitStatement;
        } else {
          ctor = AST.ForOfStatement;
        }
        this.lex();
        right = this.parseAssignmentExpression();
      }

      return new ctor({ left: this.transformDestructuring(expr), right, body: this.getIteratorStatementEpilogue() });
    } else if (isAwait) {
      throw this.createError(ErrorMessages.INVALID_LHS_IN_FOR_AWAIT);
    }
    if (this.firstExprError) {
      throw this.firstExprError;
    }
    while (this.eat(TokenType.COMMA)) {
      let rhs = this.parseAssignmentExpression();
      expr = this.finishNode(new AST.BinaryExpression({ left: expr, operator: ',', right: rhs }), leftStartState);
    }
    if (this.match(TokenType.IN)) {
      throw this.createError(ErrorMessages.INVALID_LHS_IN_FOR_IN);
    }
    if (this.matchContextualKeyword('of')) {
      throw this.createError(ErrorMessages.INVALID_LHS_IN_FOR_OF);
    }
    this.expect(TokenType.SEMICOLON);
    if (!this.match(TokenType.SEMICOLON)) {
      test = this.parseExpression();
    }
    this.expect(TokenType.SEMICOLON);
    if (!this.match(TokenType.RPAREN)) {
      right = this.parseExpression();
    }
    return new AST.ForStatement({ init: expr, test, update: right, body: this.getIteratorStatementEpilogue() });
  }

  getIteratorStatementEpilogue() {
    this.expect(TokenType.RPAREN);
    let body = this.parseStatement();
    return body;
  }

  parseIfStatementChild() {
    return this.match(TokenType.FUNCTION)
      ? this.parseFunction({ isExpr: false, inDefault: false, allowGenerator: false, isAsync: false })
      : this.parseStatement();
  }

  parseIfStatement() {
    this.lex();
    this.expect(TokenType.LPAREN);
    let test = this.parseExpression();
    this.expect(TokenType.RPAREN);
    let consequent = this.parseIfStatementChild();
    let alternate = null;
    if (this.eat(TokenType.ELSE)) {
      alternate = this.parseIfStatementChild();
    }
    return new AST.IfStatement({ test, consequent, alternate });
  }

  parseReturnStatement() {
    if (!this.inFunctionBody) {
      throw this.createError(ErrorMessages.ILLEGAL_RETURN);
    }

    this.lex();

    // Catch the very common case first: immediately a semicolon (U+003B).
    if (this.eat(TokenType.SEMICOLON) || this.hasLineTerminatorBeforeNext) {
      return new AST.ReturnStatement({ expression: null });
    }

    let expression = null;
    if (!this.match(TokenType.RBRACE) && !this.eof()) {
      expression = this.parseExpression();
    }

    this.consumeSemicolon();
    return new AST.ReturnStatement({ expression });
  }

  parseSwitchStatement() {
    this.lex();
    this.expect(TokenType.LPAREN);
    let discriminant = this.parseExpression();
    this.expect(TokenType.RPAREN);
    this.expect(TokenType.LBRACE);

    if (this.eat(TokenType.RBRACE)) {
      return new AST.SwitchStatement({ discriminant, cases: [] });
    }

    let cases = this.parseSwitchCases();
    if (this.match(TokenType.DEFAULT)) {
      let defaultCase = this.parseSwitchDefault();
      let postDefaultCases = this.parseSwitchCases();
      if (this.match(TokenType.DEFAULT)) {
        throw this.createError(ErrorMessages.MULTIPLE_DEFAULTS_IN_SWITCH);
      }
      this.expect(TokenType.RBRACE);
      return new AST.SwitchStatementWithDefault({
        discriminant,
        preDefaultCases: cases,
        defaultCase,
        postDefaultCases,
      });
    }
    this.expect(TokenType.RBRACE);
    return new AST.SwitchStatement({ discriminant, cases });
  }

  parseSwitchCases() {
    let result = [];
    while (!(this.eof() || this.match(TokenType.RBRACE) || this.match(TokenType.DEFAULT))) {
      result.push(this.parseSwitchCase());
    }
    return result;
  }

  parseSwitchCase() {
    let startState = this.startNode();
    this.expect(TokenType.CASE);
    return this.finishNode(new AST.SwitchCase({
      test: this.parseExpression(),
      consequent: this.parseSwitchCaseBody(),
    }), startState);
  }

  parseSwitchDefault() {
    let startState = this.startNode();
    this.expect(TokenType.DEFAULT);
    return this.finishNode(new AST.SwitchDefault({ consequent: this.parseSwitchCaseBody() }), startState);
  }

  parseSwitchCaseBody() {
    this.expect(TokenType.COLON);
    return this.parseStatementListInSwitchCaseBody();
  }

  parseStatementListInSwitchCaseBody() {
    let result = [];
    while (!(this.eof() || this.match(TokenType.RBRACE) || this.match(TokenType.DEFAULT) || this.match(TokenType.CASE))) {
      result.push(this.parseStatementListItem());
    }
    return result;
  }

  parseThrowStatement() {
    let token = this.lex();
    if (this.hasLineTerminatorBeforeNext) {
      throw this.createErrorWithLocation(token, ErrorMessages.NEWLINE_AFTER_THROW);
    }
    let expression = this.parseExpression();
    this.consumeSemicolon();
    return new AST.ThrowStatement({ expression });
  }

  parseTryStatement() {
    this.lex();
    let body = this.parseBlock();

    if (this.match(TokenType.CATCH)) {
      let catchClause = this.parseCatchClause();
      if (this.eat(TokenType.FINALLY)) {
        let finalizer = this.parseBlock();
        return new AST.TryFinallyStatement({ body, catchClause, finalizer });
      }
      return new AST.TryCatchStatement({ body, catchClause });
    }

    if (this.eat(TokenType.FINALLY)) {
      let finalizer = this.parseBlock();
      return new AST.TryFinallyStatement({ body, catchClause: null, finalizer });
    }
    throw this.createError(ErrorMessages.NO_CATCH_OR_FINALLY);
  }

  parseVariableDeclarationStatement() {
    let declaration = this.parseVariableDeclaration(true);
    this.consumeSemicolon();
    return new AST.VariableDeclarationStatement({ declaration });
  }

  parseWhileStatement() {
    this.lex();
    this.expect(TokenType.LPAREN);
    let test = this.parseExpression();
    let body = this.getIteratorStatementEpilogue();
    return new AST.WhileStatement({ test, body });
  }

  parseWithStatement() {
    this.lex();
    this.expect(TokenType.LPAREN);
    let object = this.parseExpression();
    this.expect(TokenType.RPAREN);
    let body = this.parseStatement();
    return new AST.WithStatement({ object, body });
  }

  parseCatchClause() {
    let startState = this.startNode();

    this.lex();

    let binding = null;

    // Catch binding is optional
    if (this.match(TokenType.LPAREN)) {
      this.lex();
      if (this.match(TokenType.RPAREN) || this.match(TokenType.LPAREN)) {
        throw this.createUnexpected(this.lookahead);
      }
      binding = this.parseBindingTarget();
      this.expect(TokenType.RPAREN);
    }

    let body = this.parseBlock();

    return this.finishNode(new AST.CatchClause({ binding, body }), startState);
  }

  parseBlock() {
    let startState = this.startNode();
    this.expect(TokenType.LBRACE);
    let body = [];
    while (!this.match(TokenType.RBRACE)) {
      body.push(this.parseStatementListItem());
    }
    this.expect(TokenType.RBRACE);
    return this.finishNode(new AST.Block({ statements: body }), startState);
  }

  parseVariableDeclaration(bindingPatternsMustHaveInit) {
    let startState = this.startNode();
    let token = this.lex();

    // preceded by this.match(TokenSubType.VAR) || this.match(TokenSubType.LET);
    let kind = token.type === TokenType.VAR ? 'var' : token.type === TokenType.CONST ? 'const' : 'let';
    let declarators = this.parseVariableDeclaratorList(bindingPatternsMustHaveInit);
    return this.finishNode(new AST.VariableDeclaration({ kind, declarators }), startState);
  }

  parseVariableDeclaratorList(bindingPatternsMustHaveInit) {
    let result = [];
    do {
      result.push(this.parseVariableDeclarator(bindingPatternsMustHaveInit));
    } while (this.eat(TokenType.COMMA));
    return result;
  }

  parseVariableDeclarator(bindingPatternsMustHaveInit) {
    let startState = this.startNode();

    if (this.match(TokenType.LPAREN)) {
      throw this.createUnexpected(this.lookahead);
    }

    let previousAllowIn = this.allowIn;
    this.allowIn = true;
    let binding = this.parseBindingTarget();
    this.allowIn = previousAllowIn;

    if (bindingPatternsMustHaveInit && binding.type !== 'BindingIdentifier' && !this.match(TokenType.ASSIGN)) {
      this.expect(TokenType.ASSIGN);
    }

    let init = null;
    if (this.eat(TokenType.ASSIGN)) {
      init = this.parseAssignmentExpression();
    }

    return this.finishNode(new AST.VariableDeclarator({ binding, init }), startState);
  }

  isolateCoverGrammar(parser) {
    let oldIsBindingElement = this.isBindingElement,
        oldIsAssignmentTarget = this.isAssignmentTarget,
        oldFirstExprError = this.firstExprError,
        result;
    this.isBindingElement = this.isAssignmentTarget = true;
    this.firstExprError = null;
    result = parser.call(this);
    if (this.firstExprError !== null) {
      throw this.firstExprError;
    }
    this.isBindingElement = oldIsBindingElement;
    this.isAssignmentTarget = oldIsAssignmentTarget;
    this.firstExprError = oldFirstExprError;
    return result;
  }

  inheritCoverGrammar(parser) {
    let oldIsBindingElement = this.isBindingElement,
        oldIsAssignmentTarget = this.isAssignmentTarget,
        oldFirstExprError = this.firstExprError,
        result;
    this.isBindingElement = this.isAssignmentTarget = true;
    this.firstExprError = null;
    result = parser.call(this);
    this.isBindingElement = this.isBindingElement && oldIsBindingElement;
    this.isAssignmentTarget = this.isAssignmentTarget && oldIsAssignmentTarget;
    this.firstExprError = oldFirstExprError || this.firstExprError;
    return result;
  }

  parseExpression() {
    let startState = this.startNode();

    let left = this.parseAssignmentExpression();
    if (this.match(TokenType.COMMA)) {
      while (!this.eof()) {
        if (!this.match(TokenType.COMMA)) break;
        this.lex();
        let right = this.parseAssignmentExpression();
        left = this.finishNode(new AST.BinaryExpression({ left, operator: ',', right }), startState);
      }
    }
    return left;
  }

  finishArrowParams(head) {
    let { params = null, rest = null } = head;
    if (head.type !== ARROW_EXPRESSION_PARAMS) {
      if (head.type === 'IdentifierExpression') {
        params = [this.targetToBinding(this.transformDestructuring(head))];
      } else {
        throw this.createUnexpected(this.lookahead);
      }
    }
    return this.copyNode(head, new AST.FormalParameters({ items: params, rest }));
  }

  parseArrowExpressionTail(params, isAsync, startState) {
    this.expect(TokenType.ARROW);
    let previousYield = this.allowYieldExpression;
    let previousAwait = this.allowAwaitExpression;
    let previousAwaitLocation = this.firstAwaitLocation;
    this.allowYieldExpression = false;
    this.allowAwaitExpression = isAsync;
    this.firstAwaitLocation = null;
    let body;
    if (this.match(TokenType.LBRACE)) {
      let previousAllowIn = this.allowIn;
      this.allowIn = true;
      body = this.parseFunctionBody();
      this.allowIn = previousAllowIn;
    } else {
      body = this.parseAssignmentExpression();
    }
    this.allowYieldExpression = previousYield;
    this.allowAwaitExpression = previousAwait;
    this.firstAwaitLocation = previousAwaitLocation;
    return this.finishNode(new AST.ArrowExpression({ isAsync, params, body }), startState);
  }

  parseAssignmentExpression() {
    return this.isolateCoverGrammar(this.parseAssignmentExpressionOrTarget);
  }

  parseAssignmentExpressionOrTarget() {
    let startState = this.startNode();
    if (this.allowYieldExpression && this.match(TokenType.YIELD)) {
      this.isBindingElement = this.isAssignmentTarget = false;
      return this.parseYieldExpression();
    }
    let expr = this.parseConditionalExpression();
    if (!this.hasLineTerminatorBeforeNext && this.match(TokenType.ARROW)) {
      this.isBindingElement = this.isAssignmentTarget = false;
      this.firstExprError = null;
      let isAsync = expr.type === ARROW_EXPRESSION_PARAMS && expr.isAsync;
      return this.parseArrowExpressionTail(this.finishArrowParams(expr), isAsync, startState);
    }
    let isAssignmentOperator = false;
    let operator = this.lookahead;
    switch (operator.type) {
      case TokenType.ASSIGN_BIT_OR:
      case TokenType.ASSIGN_BIT_XOR:
      case TokenType.ASSIGN_BIT_AND:
      case TokenType.ASSIGN_SHL:
      case TokenType.ASSIGN_SHR:
      case TokenType.ASSIGN_SHR_UNSIGNED:
      case TokenType.ASSIGN_ADD:
      case TokenType.ASSIGN_SUB:
      case TokenType.ASSIGN_MUL:
      case TokenType.ASSIGN_DIV:
      case TokenType.ASSIGN_MOD:
      case TokenType.ASSIGN_EXP:
        isAssignmentOperator = true;
        break;
    }
    if (isAssignmentOperator) {
      if (!this.isAssignmentTarget || !isValidSimpleAssignmentTarget(expr)) {
        throw this.createError(ErrorMessages.INVALID_LHS_IN_ASSIGNMENT);
      }
      expr = this.transformDestructuring(expr);
    } else if (operator.type === TokenType.ASSIGN) {
      if (!this.isAssignmentTarget) {
        throw this.createError(ErrorMessages.INVALID_LHS_IN_ASSIGNMENT);
      }
      expr = this.transformDestructuring(expr);
    } else {
      return expr;
    }
    this.lex();
    let rhs = this.parseAssignmentExpression();

    this.firstExprError = null;
    let node;
    if (operator.type === TokenType.ASSIGN) {
      node = new AST.AssignmentExpression({ binding: expr, expression: rhs });
    } else {
      node = new AST.CompoundAssignmentExpression({ binding: expr, operator: operator.type.name, expression: rhs });
      this.isBindingElement = this.isAssignmentTarget = false;
    }
    return this.finishNode(node, startState);
  }

  targetToBinding(node) {
    if (node === null) {
      return null;
    }

    switch (node.type) {
      case 'AssignmentTargetIdentifier':
        return this.copyNode(node, new AST.BindingIdentifier({ name: node.name }));
      case 'ArrayAssignmentTarget':
        return this.copyNode(node, new AST.ArrayBinding({ elements: node.elements.map(e => this.targetToBinding(e)), rest: this.targetToBinding(node.rest) }));
      case 'ObjectAssignmentTarget':
        return this.copyNode(node, new AST.ObjectBinding({ properties: node.properties.map(p => this.targetToBinding(p)), rest: this.targetToBinding(node.rest) }));
      case 'AssignmentTargetPropertyIdentifier':
        return this.copyNode(node, new AST.BindingPropertyIdentifier({ binding: this.targetToBinding(node.binding), init: node.init }));
      case 'AssignmentTargetPropertyProperty':
        return this.copyNode(node, new AST.BindingPropertyProperty({ name: node.name, binding: this.targetToBinding(node.binding) }));
      case 'AssignmentTargetWithDefault':
        return this.copyNode(node, new AST.BindingWithDefault({ binding: this.targetToBinding(node.binding), init: node.init }));
    }

    // istanbul ignore next
    throw new Error('Not reached');
  }

  transformDestructuring(node) {
    switch (node.type) {

      case 'DataProperty':
        return this.copyNode(node, new AST.AssignmentTargetPropertyProperty({
          name: node.name,
          binding: this.transformDestructuringWithDefault(node.expression),
        }));
      case 'ShorthandProperty':
        return this.copyNode(node, new AST.AssignmentTargetPropertyIdentifier({
          binding: this.copyNode(node, new AST.AssignmentTargetIdentifier({ name: node.name.name })),
          init: null,
        }));

      case 'ObjectExpression': {
        let last = node.properties.length > 0 ? node.properties[node.properties.length - 1] : void 0;
        if (last != null && last.type === 'SpreadProperty') {
          return this.copyNode(node, new AST.ObjectAssignmentTarget({
            properties: node.properties.slice(0, -1).map(e => e && this.transformDestructuringWithDefault(e)),
            rest: this.transformDestructuring(last.expression),
          }));
        }

        return this.copyNode(node, new AST.ObjectAssignmentTarget({
          properties: node.properties.map(e => e && this.transformDestructuringWithDefault(e)),
          rest: null,
        }));
      }
      case 'ArrayExpression': {
        let last = node.elements[node.elements.length - 1];
        if (last != null && last.type === 'SpreadElement') {
          return this.copyNode(node, new AST.ArrayAssignmentTarget({
            elements: node.elements.slice(0, -1).map(e => e && this.transformDestructuringWithDefault(e)),
            rest: this.copyNode(last.expression, this.transformDestructuring(last.expression)),
          }));
        }
        return this.copyNode(node, new AST.ArrayAssignmentTarget({
          elements: node.elements.map(e => e && this.transformDestructuringWithDefault(e)),
          rest: null,
        }));
      }
      case 'IdentifierExpression':
        return this.copyNode(node, new AST.AssignmentTargetIdentifier({ name: node.name }));

      case 'StaticPropertyName':
        return this.copyNode(node, new AST.AssignmentTargetIdentifier({ name: node.value }));

      case 'ComputedMemberExpression':
        return this.copyNode(node, new AST.ComputedMemberAssignmentTarget({ object: node.object, expression: node.expression }));
      case 'StaticMemberExpression':
        return this.copyNode(node, new AST.StaticMemberAssignmentTarget({ object: node.object, property: node.property }));

      case 'ArrayAssignmentTarget':
      case 'ObjectAssignmentTarget':
      case 'ComputedMemberAssignmentTarget':
      case 'StaticMemberAssignmentTarget':
      case 'AssignmentTargetIdentifier':
      case 'AssignmentTargetPropertyIdentifier':
      case 'AssignmentTargetPropertyProperty':
      case 'AssignmentTargetWithDefault':
        return node;
    }
    // istanbul ignore next
    throw new Error('Not reached');
  }

  transformDestructuringWithDefault(node) {
    switch (node.type) {
      case 'AssignmentExpression':
        return this.copyNode(node, new AST.AssignmentTargetWithDefault({
          binding: this.transformDestructuring(node.binding),
          init: node.expression,
        }));
    }
    return this.transformDestructuring(node);
  }

  lookaheadAssignmentExpression() {
    if (this.matchIdentifier()) {
      return true;
    }
    switch (this.lookahead.type) {
      case TokenType.ADD:
      case TokenType.ASSIGN_DIV:
      case TokenType.BIT_NOT:
      case TokenType.CLASS:
      case TokenType.DEC:
      case TokenType.DELETE:
      case TokenType.DIV:
      case TokenType.FALSE:
      case TokenType.FUNCTION:
      case TokenType.INC:
      case TokenType.LBRACE:
      case TokenType.LBRACK:
      case TokenType.LPAREN:
      case TokenType.NEW:
      case TokenType.NOT:
      case TokenType.NULL:
      case TokenType.NUMBER:
      case TokenType.STRING:
      case TokenType.SUB:
      case TokenType.SUPER:
      case TokenType.THIS:
      case TokenType.TRUE:
      case TokenType.TYPEOF:
      case TokenType.VOID:
      case TokenType.TEMPLATE:
        return true;
    }
    return false;
  }

  parseYieldExpression() {
    let startState = this.startNode();

    this.lex();
    if (this.hasLineTerminatorBeforeNext) {
      return this.finishNode(new AST.YieldExpression({ expression: null }), startState);
    }
    let isGenerator = !!this.eat(TokenType.MUL);
    let expr = null;
    if (isGenerator || this.lookaheadAssignmentExpression()) {
      expr = this.parseAssignmentExpression();
    }
    let ctor = isGenerator ? AST.YieldGeneratorExpression : AST.YieldExpression;
    return this.finishNode(new ctor({ expression: expr }), startState);
  }

  parseConditionalExpression() {
    let startState = this.startNode();
    let test = this.parseBinaryExpression();
    if (this.firstExprError) return test;
    if (this.eat(TokenType.CONDITIONAL)) {
      this.isBindingElement = this.isAssignmentTarget = false;
      let previousAllowIn = this.allowIn;
      this.allowIn = true;
      let consequent = this.isolateCoverGrammar(this.parseAssignmentExpression);
      this.allowIn = previousAllowIn;
      this.expect(TokenType.COLON);
      let alternate = this.isolateCoverGrammar(this.parseAssignmentExpression);
      return this.finishNode(new AST.ConditionalExpression({ test, consequent, alternate }), startState);
    }
    return test;
  }

  isBinaryOperator(type) {
    switch (type) {
      case TokenType.OR:
      case TokenType.AND:
      case TokenType.BIT_OR:
      case TokenType.BIT_XOR:
      case TokenType.BIT_AND:
      case TokenType.EQ:
      case TokenType.NE:
      case TokenType.EQ_STRICT:
      case TokenType.NE_STRICT:
      case TokenType.LT:
      case TokenType.GT:
      case TokenType.LTE:
      case TokenType.GTE:
      case TokenType.INSTANCEOF:
      case TokenType.SHL:
      case TokenType.SHR:
      case TokenType.SHR_UNSIGNED:
      case TokenType.ADD:
      case TokenType.SUB:
      case TokenType.MUL:
      case TokenType.DIV:
      case TokenType.MOD:
        return true;
      case TokenType.IN:
        return this.allowIn;
      default:
        return false;
    }
  }

  parseBinaryExpression() {
    let startState = this.startNode();
    let left = this.parseExponentiationExpression();
    if (this.firstExprError) {
      return left;
    }

    let operator = this.lookahead.type;
    if (!this.isBinaryOperator(operator)) return left;

    this.isBindingElement = this.isAssignmentTarget = false;

    this.lex();
    let stack = [];
    stack.push({ startState, left, operator, precedence: BinaryPrecedence[operator.name] });
    startState = this.startNode();
    let right = this.isolateCoverGrammar(this.parseExponentiationExpression);
    operator = this.lookahead.type;
    while (this.isBinaryOperator(operator)) {
      let precedence = BinaryPrecedence[operator.name];
      // Reduce: make a binary expression from the three topmost entries.
      while (stack.length && precedence <= stack[stack.length - 1].precedence) {
        let stackItem = stack[stack.length - 1];
        let stackOperator = stackItem.operator;
        left = stackItem.left;
        stack.pop();
        startState = stackItem.startState;
        right = this.finishNode(new AST.BinaryExpression({ left, operator: stackOperator.name, right }), startState);
      }

      this.lex();
      stack.push({ startState, left: right, operator, precedence });

      startState = this.startNode();
      right = this.isolateCoverGrammar(this.parseExponentiationExpression);
      operator = this.lookahead.type;
    }

    // Final reduce to clean-up the stack.
    return stack.reduceRight((expr, stackItem) =>
      this.finishNode(new AST.BinaryExpression({
        left: stackItem.left,
        operator: stackItem.operator.name,
        right: expr,
      }), stackItem.startState),
    right);
  }

  parseExponentiationExpression() {
    let startState = this.startNode();

    let leftIsParenthesized = this.lookahead.type === TokenType.LPAREN;
    let left = this.parseUnaryExpression();
    if (this.lookahead.type !== TokenType.EXP) {
      return left;
    }
    if (left.type === 'UnaryExpression' && !leftIsParenthesized) {
      throw this.createError(ErrorMessages.INVALID_EXPONENTIATION_LHS);
    }
    this.lex();

    this.isBindingElement = this.isAssignmentTarget = false;

    let right = this.isolateCoverGrammar(this.parseExponentiationExpression);
    return this.finishNode(new AST.BinaryExpression({ left, operator: '**', right }), startState);
  }

  parseUnaryExpression() {
    if (this.lookahead.type.klass !== TokenClass.Punctuator && this.lookahead.type.klass !== TokenClass.Keyword) {
      return this.parseUpdateExpression();
    }

    let startState = this.startNode();
    if (this.allowAwaitExpression && this.eat(TokenType.AWAIT)) {
      this.isBindingElement = this.isAssignmentTarget = false;
      let expression = this.isolateCoverGrammar(this.parseUnaryExpression);
      return this.finishNode(new AST.AwaitExpression({ expression }), startState);
    }

    let operator = this.lookahead;
    if (!isPrefixOperator(operator)) {
      return this.parseUpdateExpression();
    }

    this.lex();
    this.isBindingElement = this.isAssignmentTarget = false;

    let node;
    if (isUpdateOperator(operator)) {
      let operandStartLocation = this.getLocation();
      let operand = this.isolateCoverGrammar(this.parseUnaryExpression);
      if (!isValidSimpleAssignmentTarget(operand)) {
        throw this.createErrorWithLocation(operandStartLocation, ErrorMessages.INVALID_UPDATE_OPERAND);
      }
      operand = this.transformDestructuring(operand);
      node = new AST.UpdateExpression({ isPrefix: true, operator: operator.value, operand });
    } else {
      let operand = this.isolateCoverGrammar(this.parseUnaryExpression);
      node = new AST.UnaryExpression({ operator: operator.value, operand });
    }

    return this.finishNode(node, startState);
  }

  parseUpdateExpression() {
    let startLocation = this.getLocation();
    let startState = this.startNode();

    let operand = this.parseLeftHandSideExpression({ allowCall: true });
    if (this.firstExprError || this.hasLineTerminatorBeforeNext) return operand;

    let operator = this.lookahead;
    if (!isUpdateOperator(operator)) return operand;
    this.lex();
    this.isBindingElement = this.isAssignmentTarget = false;
    if (!isValidSimpleAssignmentTarget(operand)) {
      throw this.createErrorWithLocation(startLocation, ErrorMessages.INVALID_UPDATE_OPERAND);
    }
    operand = this.transformDestructuring(operand);

    return this.finishNode(new AST.UpdateExpression({ isPrefix: false, operator: operator.value, operand }), startState);
  }

  parseLeftHandSideExpression({ allowCall }) {
    let startState = this.startNode();
    let previousAllowIn = this.allowIn;
    this.allowIn = true;

    let expr, token = this.lookahead;

    if (this.eat(TokenType.SUPER)) {
      this.isBindingElement = false;
      this.isAssignmentTarget = false;
      expr = this.finishNode(new AST.Super, startState);
      if (this.match(TokenType.LPAREN)) {
        if (allowCall) {
          expr = this.finishNode(new AST.CallExpression({
            callee: expr,
            arguments: this.parseArgumentList().args,
          }), startState);
        } else {
          throw this.createUnexpected(token);
        }
      } else if (this.match(TokenType.LBRACK)) {
        expr = this.finishNode(new AST.ComputedMemberExpression({
          object: expr,
          expression: this.parseComputedMember(),
        }), startState);
        this.isAssignmentTarget = true;
      } else if (this.match(TokenType.PERIOD)) {
        expr = this.finishNode(new AST.StaticMemberExpression({
          object: expr,
          property: this.parseStaticMember(),
        }), startState);
        this.isAssignmentTarget = true;
      } else {
        throw this.createUnexpected(token);
      }
    } else if (this.match(TokenType.NEW)) {
      this.isBindingElement = this.isAssignmentTarget = false;
      expr = this.parseNewExpression();
    } else if (this.match(TokenType.ASYNC)) {
      expr = this.parsePrimaryExpression();
      // there's only three things this could be: an identifier, an async arrow, or an async function expression.
      if (expr.type === 'IdentifierExpression' && allowCall && !this.hasLineTerminatorBeforeNext) {
        if (this.matchIdentifier()) {
          // `async [no lineterminator here] identifier` must be an async arrow
          let afterAsyncStartState = this.startNode();
          let previousAwait = this.allowAwaitExpression;
          this.allowAwaitExpression = true;
          let param = this.parseBindingIdentifier();
          this.allowAwaitExpression = previousAwait;
          this.ensureArrow();
          return this.finishNode({
            type: ARROW_EXPRESSION_PARAMS,
            params: [param],
            rest: null,
            isAsync: true,
          }, afterAsyncStartState);
        }
        if (this.match(TokenType.LPAREN)) {
          // the maximally obnoxious case: `async (`
          let afterAsyncStartState = this.startNode();
          let previousAwaitLocation = this.firstAwaitLocation;
          this.firstAwaitLocation = null;
          let { args, locationFollowingFirstSpread } = this.parseArgumentList();
          if (this.isBindingElement && !this.hasLineTerminatorBeforeNext && this.match(TokenType.ARROW)) {
            if (locationFollowingFirstSpread !== null) {
              throw this.createErrorWithLocation(locationFollowingFirstSpread, ErrorMessages.UNEXPECTED_TOKEN(','));
            }
            if (this.firstAwaitLocation !== null) {
              throw this.createErrorWithLocation(this.firstAwaitLocation, ErrorMessages.NO_AWAIT_IN_ASYNC_PARAMS);
            }
            let rest = null;
            if (args.length > 0 && args[args.length - 1].type === 'SpreadElement') {
              rest = this.targetToBinding(this.transformDestructuringWithDefault(args[args.length - 1].expression));
              if (rest.init != null) {
                throw this.createError(ErrorMessages.UNEXPECTED_REST_PARAMETERS_INITIALIZATION);
              }
              args = args.slice(0, -1);
            }
            let params = args.map(arg => this.targetToBinding(this.transformDestructuringWithDefault(arg)));
            return this.finishNode({
              type: ARROW_EXPRESSION_PARAMS,
              params,
              rest,
              isAsync: true,
            }, afterAsyncStartState);
          }
          this.firstAwaitLocation = previousAwaitLocation || this.firstAwaitLocation;
          // otherwise we've just taken the first iteration of the loop below
          this.isBindingElement = this.isAssignmentTarget = false;
          expr = this.finishNode(new AST.CallExpression({
            callee: expr,
            arguments: args,
          }), startState);
        }
      }
    } else {
      expr = this.parsePrimaryExpression();
      if (this.firstExprError) {
        return expr;
      }
    }

    while (true) {
      if (allowCall && this.match(TokenType.LPAREN)) {
        this.isBindingElement = this.isAssignmentTarget = false;
        expr = this.finishNode(new AST.CallExpression({
          callee: expr,
          arguments: this.parseArgumentList().args,
        }), startState);
      } else if (this.match(TokenType.LBRACK)) {
        this.isBindingElement = false;
        this.isAssignmentTarget = true;
        expr = this.finishNode(new AST.ComputedMemberExpression({
          object: expr,
          expression: this.parseComputedMember(),
        }), startState);
      } else if (this.match(TokenType.PERIOD)) {
        this.isBindingElement = false;
        this.isAssignmentTarget = true;
        expr = this.finishNode(new AST.StaticMemberExpression({
          object: expr,
          property: this.parseStaticMember(),
        }), startState);
      } else if (this.match(TokenType.TEMPLATE)) {
        this.isBindingElement = this.isAssignmentTarget = false;
        expr = this.finishNode(new AST.TemplateExpression({
          tag: expr,
          elements: this.parseTemplateElements(),
        }), startState);
      } else {
        break;
      }
    }

    this.allowIn = previousAllowIn;

    return expr;
  }

  parseTemplateElements() {
    let startState = this.startNode();
    let token = this.lookahead;
    if (token.tail) {
      this.lex();
      return [this.finishNode(new AST.TemplateElement({ rawValue: token.slice.text.slice(1, -1) }), startState)];
    }
    let result = [
      this.finishNode(new AST.TemplateElement({ rawValue: this.lex().slice.text.slice(1, -2) }), startState),
    ];
    while (true) {
      result.push(this.parseExpression());
      if (!this.match(TokenType.RBRACE)) {
        throw this.createILLEGAL();
      }
      this.index = this.startIndex;
      this.line = this.startLine;
      this.lineStart = this.startLineStart;
      this.lookahead = this.scanTemplateElement();
      startState = this.startNode();
      token = this.lex();
      if (token.tail) {
        result.push(this.finishNode(new AST.TemplateElement({ rawValue: token.slice.text.slice(1, -1) }), startState));
        return result;
      }
      result.push(this.finishNode(new AST.TemplateElement({ rawValue: token.slice.text.slice(1, -2) }), startState));
    }
  }

  parseStaticMember() {
    this.lex();
    if (this.lookahead.type.klass.isIdentifierName) {
      return this.lex().value;
    }
    throw this.createUnexpected(this.lookahead);
  }

  parseComputedMember() {
    this.lex();
    let expr = this.parseExpression();
    this.expect(TokenType.RBRACK);
    return expr;
  }

  parseNewExpression() {
    let startState = this.startNode();
    this.lex();
    if (this.eat(TokenType.PERIOD)) {
      this.expectContextualKeyword('target');
      return this.finishNode(new AST.NewTargetExpression, startState);
    }
    let callee = this.isolateCoverGrammar(() => this.parseLeftHandSideExpression({ allowCall: false }));
    return this.finishNode(new AST.NewExpression({
      callee,
      arguments: this.match(TokenType.LPAREN) ? this.parseArgumentList().args : [],
    }), startState);
  }

  parseRegexFlags(flags) {
    let isGlobal = false,
        ignoreCase = false,
        multiLine = false,
        unicode = false,
        sticky = false,
        dotAll = false;
    for (let i = 0; i < flags.length; ++i) {
      let f = flags[i];
      switch (f) {
        case 'g':
          if (isGlobal) {
            throw this.createError('Duplicate regular expression flag \'g\'');
          }
          isGlobal = true;
          break;
        case 'i':
          if (ignoreCase) {
            throw this.createError('Duplicate regular expression flag \'i\'');
          }
          ignoreCase = true;
          break;
        case 'm':
          if (multiLine) {
            throw this.createError('Duplicate regular expression flag \'m\'');
          }
          multiLine = true;
          break;
        case 'u':
          if (unicode) {
            throw this.createError('Duplicate regular expression flag \'u\'');
          }
          unicode = true;
          break;
        case 'y':
          if (sticky) {
            throw this.createError('Duplicate regular expression flag \'y\'');
          }
          sticky = true;
          break;
        case 's':
          if (dotAll) {
            throw this.createError('Duplicate regular expression flag \'s\'');
          }
          dotAll = true;
          break;
        default:
          throw this.createError(`Invalid regular expression flag '${f}'`);
      }
    }
    return { global: isGlobal, ignoreCase, multiLine, unicode, sticky, dotAll };
  }

  parsePrimaryExpression() {
    if (this.match(TokenType.LPAREN)) {
      return this.parseGroupExpression();
    }

    let startState = this.startNode();

    if (this.eat(TokenType.ASYNC)) {
      if (!this.hasLineTerminatorBeforeNext && this.match(TokenType.FUNCTION)) {
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.finishNode(this.parseFunction({ isExpr: true, inDefault: false, allowGenerator: true, isAsync: true }), startState);
      }
      return this.finishNode(new AST.IdentifierExpression({ name: 'async' }), startState);
    }

    if (this.matchIdentifier()) {
      return this.finishNode(new AST.IdentifierExpression({ name: this.parseIdentifier() }), startState);
    }
    switch (this.lookahead.type) {
      case TokenType.STRING:
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.parseStringLiteral();
      case TokenType.NUMBER:
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.parseNumericLiteral();
      case TokenType.THIS:
        this.lex();
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.finishNode(new AST.ThisExpression, startState);
      case TokenType.FUNCTION:
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.finishNode(this.parseFunction({ isExpr: true, inDefault: false, allowGenerator: true, isAsync: false }), startState);
      case TokenType.TRUE:
        this.lex();
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.finishNode(new AST.LiteralBooleanExpression({ value: true }), startState);
      case TokenType.FALSE:
        this.lex();
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.finishNode(new AST.LiteralBooleanExpression({ value: false }), startState);
      case TokenType.NULL:
        this.lex();
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.finishNode(new AST.LiteralNullExpression, startState);
      case TokenType.LBRACK:
        return this.parseArrayExpression();
      case TokenType.LBRACE:
        return this.parseObjectExpression();
      case TokenType.TEMPLATE:
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.finishNode(new AST.TemplateExpression({ tag: null, elements: this.parseTemplateElements() }), startState);
      case TokenType.DIV:
      case TokenType.ASSIGN_DIV: {
        this.isBindingElement = this.isAssignmentTarget = false;
        this.lookahead = this.scanRegExp(this.match(TokenType.DIV) ? '/' : '/=');
        let token = this.lex();
        let lastSlash = token.value.lastIndexOf('/');
        let pattern = token.value.slice(1, lastSlash);
        let flags = token.value.slice(lastSlash + 1);
        let ctorArgs = this.parseRegexFlags(flags);
        if (!acceptRegex(pattern, ctorArgs)) {
          throw this.createError(ErrorMessages.INVALID_REGEX);
        }
        ctorArgs.pattern = pattern;
        return this.finishNode(new AST.LiteralRegExpExpression(ctorArgs), startState);
      }
      case TokenType.CLASS:
        this.isBindingElement = this.isAssignmentTarget = false;
        return this.parseClass({ isExpr: true, inDefault: false });
      default:
        throw this.createUnexpected(this.lookahead);
    }
  }

  parseNumericLiteral() {
    let startLocation = this.getLocation();
    let startState = this.startNode();
    let token = this.lex();
    if (token.octal && this.strict) {
      if (token.noctal) {
        throw this.createErrorWithLocation(startLocation, 'Unexpected noctal integer literal');
      } else {
        throw this.createErrorWithLocation(startLocation, 'Unexpected legacy octal integer literal');
      }
    }
    let node = token.value === 1 / 0
      ? new AST.LiteralInfinityExpression
      : new AST.LiteralNumericExpression({ value: token.value });
    return this.finishNode(node, startState);
  }

  parseStringLiteral() {
    let startLocation = this.getLocation();
    let startState = this.startNode();
    let token = this.lex();
    if (token.octal != null && this.strict) {
      throw this.createErrorWithLocation(startLocation, 'Unexpected legacy octal escape sequence: \\' + token.octal);
    }
    return this.finishNode(new AST.LiteralStringExpression({ value: token.str }), startState);
  }

  parseIdentifierName() {
    if (this.lookahead.type.klass.isIdentifierName) {
      return this.lex().value;
    }
    throw this.createUnexpected(this.lookahead);
  }

  parseBindingIdentifier() {
    let startState = this.startNode();
    return this.finishNode(new AST.BindingIdentifier({ name: this.parseIdentifier() }), startState);
  }

  parseIdentifier() {
    if (this.lookahead.value === 'yield' && this.allowYieldExpression) {
      throw this.createError(ErrorMessages.ILLEGAL_YIELD_IDENTIFIER);
    }
    if (this.lookahead.value === 'await' && this.allowAwaitExpression) {
      throw this.createError(ErrorMessages.ILLEGAL_AWAIT_IDENTIFIER);
    }
    if (this.matchIdentifier()) {
      return this.lex().value;
    }
    throw this.createUnexpected(this.lookahead);
  }

  parseArgumentList() {
    this.lex();
    let args = this.parseArguments();
    this.expect(TokenType.RPAREN);
    return args;
  }

  parseArguments() {
    let args = [];
    let locationFollowingFirstSpread = null;
    while (!this.match(TokenType.RPAREN)) {
      let arg;
      let startState = this.startNode();
      if (this.eat(TokenType.ELLIPSIS)) {
        arg = this.finishNode(new AST.SpreadElement({ expression: this.inheritCoverGrammar(this.parseAssignmentExpressionOrTarget) }), startState);
        if (locationFollowingFirstSpread === null) {
          args.push(arg);
          if (this.match(TokenType.RPAREN)) {
            break;
          }
          locationFollowingFirstSpread = this.getLocation();
          this.expect(TokenType.COMMA);
          continue;
        }
      } else {
        arg = this.inheritCoverGrammar(this.parseAssignmentExpressionOrTarget);
      }
      args.push(arg);
      if (this.match(TokenType.RPAREN)) {
        break;
      }
      this.expect(TokenType.COMMA);
    }
    return { args, locationFollowingFirstSpread };
  }

  // 11.2 Left-Hand-Side Expressions;

  ensureArrow() {
    if (this.hasLineTerminatorBeforeNext) {
      throw this.createError(ErrorMessages.UNEXPECTED_LINE_TERMINATOR);
    }
    if (!this.match(TokenType.ARROW)) {
      this.expect(TokenType.ARROW);
    }
  }

  parseGroupExpression() {
    // At this point, we need to parse 3 things:
    //  1. Group expression
    //  2. Assignment target of assignment expression
    //  3. Parameter list of arrow function
    let rest = null;
    let preParenStartState = this.startNode();
    let start = this.expect(TokenType.LPAREN);
    let postParenStartState = this.startNode();
    if (this.match(TokenType.RPAREN)) {
      this.lex();
      let paramsNode = this.finishNode({
        type: ARROW_EXPRESSION_PARAMS,
        params: [],
        rest: null,
        isAsync: false,
      }, preParenStartState);
      this.ensureArrow();
      this.isBindingElement = this.isAssignmentTarget = false;
      return paramsNode;
    } else if (this.eat(TokenType.ELLIPSIS)) {
      rest = this.parseBindingTarget();
      if (this.match(TokenType.ASSIGN)) {
        throw this.createError(ErrorMessages.INVALID_REST_PARAMETERS_INITIALIZATION);
      }
      if (this.match(TokenType.COMMA)) {
        throw this.createError(ErrorMessages.INVALID_LAST_REST_PARAMETER);
      }
      this.expect(TokenType.RPAREN);
      let paramsNode = this.finishNode({
        type: ARROW_EXPRESSION_PARAMS,
        params: [],
        rest,
        isAsync: false,
      }, preParenStartState);
      this.ensureArrow();
      this.isBindingElement = this.isAssignmentTarget = false;
      return paramsNode;
    }
    let group = this.inheritCoverGrammar(this.parseAssignmentExpressionOrTarget);

    let params = this.isBindingElement ? [this.targetToBinding(this.transformDestructuringWithDefault(group))] : null;

    while (this.eat(TokenType.COMMA)) {
      if (this.match(TokenType.RPAREN)) {
        if (!this.isBindingElement) {
          throw this.createUnexpected(this.lookahead);
        }
        this.firstExprError = this.firstExprError || this.createUnexpected(this.lookahead);
        group = null;
        break;
      }
      this.isAssignmentTarget = false;
      if (this.match(TokenType.ELLIPSIS)) {
        if (!this.isBindingElement) {
          throw this.createUnexpected(this.lookahead);
        }
        this.lex();
        rest = this.parseBindingTarget();
        if (this.match(TokenType.ASSIGN)) {
          throw this.createError(ErrorMessages.INVALID_REST_PARAMETERS_INITIALIZATION);
        }
        if (this.match(TokenType.COMMA)) {
          throw this.createError(ErrorMessages.INVALID_LAST_REST_PARAMETER);
        }
        break;
      }

      if (group) {
        // Can be either binding element or assignment target.
        let expr = this.inheritCoverGrammar(this.parseAssignmentExpressionOrTarget);
        if (this.isBindingElement) {
          params.push(this.targetToBinding(this.transformDestructuringWithDefault(expr)));
        } else {
          params = null;
        }

        if (this.firstExprError) {
          group = null;
        } else {
          group = this.finishNode(new AST.BinaryExpression({
            left: group,
            operator: ',',
            right: expr,
          }), postParenStartState);
        }
      } else {
        // Can be only binding elements.
        let binding = this.parseBindingElement();
        params.push(binding);
      }
    }
    this.expect(TokenType.RPAREN);

    if (!this.hasLineTerminatorBeforeNext && this.match(TokenType.ARROW)) {
      if (!this.isBindingElement) {
        throw this.createErrorWithLocation(start, ErrorMessages.ILLEGAL_ARROW_FUNCTION_PARAMS);
      }

      this.isBindingElement = false;
      return this.finishNode({
        type: ARROW_EXPRESSION_PARAMS,
        params,
        rest,
        isAsync: false,
      }, preParenStartState);
    }
    // Ensure assignment pattern:
    if (rest) {
      this.ensureArrow();
    }
    this.isBindingElement = false;
    if (!isValidSimpleAssignmentTarget(group)) {
      this.isAssignmentTarget = false;
    }
    return group;
  }

  parseArrayExpression() {
    let startLocation = this.getLocation();
    let startState = this.startNode();

    this.lex();

    let exprs = [];
    let rest = null;

    while (true) {
      if (this.match(TokenType.RBRACK)) {
        break;
      }
      if (this.eat(TokenType.COMMA)) {
        exprs.push(null);
      } else {
        let elementStartState = this.startNode();
        let expr;
        if (this.eat(TokenType.ELLIPSIS)) {
          // Spread/Rest element
          expr = this.inheritCoverGrammar(this.parseAssignmentExpressionOrTarget);
          if (!this.isAssignmentTarget && this.firstExprError) {
            throw this.firstExprError;
          }
          if (expr.type === 'ArrayAssignmentTarget' || expr.type === 'ObjectAssignmentTarget') {
            rest = expr;
            break;
          }
          if (expr.type !== 'ArrayExpression' && expr.type !== 'ObjectExpression' && !isValidSimpleAssignmentTarget(expr)) {
            this.isBindingElement = this.isAssignmentTarget = false;
          }
          expr = this.finishNode(new AST.SpreadElement({ expression: expr }), elementStartState);
          if (!this.match(TokenType.RBRACK)) {
            this.isBindingElement = this.isAssignmentTarget = false;
          }
        } else {
          expr = this.inheritCoverGrammar(this.parseAssignmentExpressionOrTarget);
          if (!this.isAssignmentTarget && this.firstExprError) {
            throw this.firstExprError;
          }
        }
        exprs.push(expr);

        if (!this.match(TokenType.RBRACK)) {
          this.expect(TokenType.COMMA);
        }
      }
    }

    if (rest && this.match(TokenType.COMMA)) {
      throw this.createErrorWithLocation(startLocation, ErrorMessages.UNEXPECTED_COMMA_AFTER_REST);
    }

    this.expect(TokenType.RBRACK);

    if (rest) {
      // No need to check isAssignmentTarget: the only way to have something we know is a rest element is if we have ...Object/ArrayAssignmentTarget, which implies we have a firstExprError; as such, if isAssignmentTarget were false, we'd've thrown above before setting rest.
      return this.finishNode(new AST.ArrayAssignmentTarget({
        elements: exprs.map(e => e && this.transformDestructuringWithDefault(e)),
        rest,
      }), startState);
    } else if (this.firstExprError) {
      let last = exprs[exprs.length - 1];
      if (last != null && last.type === 'SpreadElement') {
        return this.finishNode(new AST.ArrayAssignmentTarget({
          elements: exprs.slice(0, -1).map(e => e && this.transformDestructuringWithDefault(e)),
          rest: this.transformDestructuring(last.expression),
        }), startState);
      }
      return this.finishNode(new AST.ArrayAssignmentTarget({
        elements: exprs.map(e => e && this.transformDestructuringWithDefault(e)),
        rest: null,
      }), startState);

    }
    return this.finishNode(new AST.ArrayExpression({ elements: exprs }), startState);
  }

  parseObjectExpression() {
    let startState = this.startNode();
    this.lex();
    let properties = [];
    while (!this.match(TokenType.RBRACE)) {
      let isSpreadProperty = false;
      if (this.match(TokenType.ELLIPSIS)) {
        isSpreadProperty = true;
        let spreadPropertyOrAssignmentTarget = this.parseSpreadPropertyDefinition();
        properties.push(spreadPropertyOrAssignmentTarget);
      } else {
        let property = this.inheritCoverGrammar(this.parsePropertyDefinition);
        properties.push(property);
      }
      if (!this.match(TokenType.RBRACE)) {
        this.expect(TokenType.COMMA);
        if (isSpreadProperty) {
          this.isBindingElement = this.isAssignmentTarget = false;
        }
      }
    }
    this.expect(TokenType.RBRACE);
    if (this.firstExprError) {
      if (!this.isAssignmentTarget) {
        throw this.createError(ErrorMessages.INVALID_LHS_IN_BINDING);
      }
      let last = properties[properties.length - 1];
      if (last != null && last.type === 'SpreadProperty') {
        return this.finishNode(new AST.ObjectAssignmentTarget({
          properties: properties.slice(0, -1).map(p => this.transformDestructuringWithDefault(p)),
          rest: this.transformDestructuring(last.expression),
        }), startState);
      }
      return this.finishNode(new AST.ObjectAssignmentTarget({ properties: properties.map(p => this.transformDestructuringWithDefault(p)), rest: null }), startState);
    }
    return this.finishNode(new AST.ObjectExpression({ properties }), startState);
  }

  parseSpreadPropertyDefinition() {
    let startState = this.startNode();
    this.expect(TokenType.ELLIPSIS);
    let expression = this.parseAssignmentExpression();
    if (!isValidSimpleAssignmentTarget(expression)) {
      this.isBindingElement = this.isAssignmentTarget = false;
    } else if (expression.type !== 'IdentifierExpression') {
      this.isBindingElement = false;
    }
    return this.finishNode(new AST.SpreadProperty({ expression }), startState);
  }

  parsePropertyDefinition() {
    let startLocation = this.getLocation();
    let startState = this.startNode();
    let token = this.lookahead;

    let { methodOrKey, kind } = this.parseMethodDefinition();
    switch (kind) {
      case 'method':
        this.isBindingElement = this.isAssignmentTarget = false;
        return methodOrKey;
      case 'identifier':
        if (token.value === 'await' && this.firstAwaitLocation == null) {
          this.firstAwaitLocation = this.getLocation();
        }
        if (this.eat(TokenType.ASSIGN)) {
          if (this.allowYieldExpression && token.value === 'yield') {
            throw this.createError(ErrorMessages.ILLEGAL_YIELD_IDENTIFIER);
          }
          if (this.allowAwaitExpression && token.value === 'await') {
            throw this.createError(ErrorMessages.ILLEGAL_AWAIT_IDENTIFIER);
          }
          // CoverInitializedName
          let init = this.isolateCoverGrammar(this.parseAssignmentExpression);
          this.firstExprError = this.createErrorWithLocation(startLocation, ErrorMessages.ILLEGAL_PROPERTY);
          return this.finishNode(new AST.AssignmentTargetPropertyIdentifier({
            binding: this.transformDestructuring(methodOrKey),
            init,
          }), startState);
        } else if (!this.match(TokenType.COLON)) {
          if (this.allowYieldExpression && token.value === 'yield') {
            throw this.createError(ErrorMessages.ILLEGAL_YIELD_IDENTIFIER);
          }
          if (this.allowAwaitExpression && token.value === 'await') {
            throw this.createError(ErrorMessages.ILLEGAL_AWAIT_IDENTIFIER);
          }
          if (token.type === TokenType.IDENTIFIER || token.value === 'let' || token.value === 'yield' || token.value === 'async' || token.value === 'await') {
            return this.finishNode(new AST.ShorthandProperty({ name: this.finishNode(new AST.IdentifierExpression({ name: methodOrKey.value }), startState) }), startState);
          }
          throw this.createUnexpected(token);
        }
    }

    // property
    this.expect(TokenType.COLON);

    let expr = this.inheritCoverGrammar(this.parseAssignmentExpressionOrTarget);
    if (this.firstExprError) {
      return this.finishNode(new AST.AssignmentTargetPropertyProperty({ name: methodOrKey, binding: expr }), startState);
    }
    return this.finishNode(new AST.DataProperty({ name: methodOrKey, expression: expr }), startState);
  }

  parsePropertyName() {
    // PropertyName[Yield,GeneratorParameter]:
    let token = this.lookahead;
    let startState = this.startNode();

    if (this.eof()) {
      throw this.createUnexpected(token);
    }

    switch (token.type) {
      case TokenType.STRING:
        return {
          name: this.finishNode(new AST.StaticPropertyName({
            value: this.parseStringLiteral().value,
          }), startState),
          binding: null,
        };
      case TokenType.NUMBER: {
        let numLiteral = this.parseNumericLiteral();
        return {
          name: this.finishNode(new AST.StaticPropertyName({
            value: `${numLiteral.type === 'LiteralInfinityExpression' ? 1 / 0 : numLiteral.value}`,
          }), startState),
          binding: null,
        };
      }
      case TokenType.LBRACK: {
        this.lex();
        let expr = this.parseAssignmentExpression();
        this.expect(TokenType.RBRACK);
        return { name: this.finishNode(new AST.ComputedPropertyName({ expression: expr }), startState), binding: null };
      }
    }

    let name = this.parseIdentifierName();
    return {
      name: this.finishNode(new AST.StaticPropertyName({ value: name }), startState),
      binding: this.finishNode(new AST.BindingIdentifier({ name }), startState),
    };
  }

  /**
   * Test if lookahead can be the beginning of a `PropertyName`.
   * @returns {boolean}
   */
  lookaheadPropertyName() {
    switch (this.lookahead.type) {
      case TokenType.NUMBER:
      case TokenType.STRING:
      case TokenType.LBRACK:
        return true;
      default:
        return this.lookahead.type.klass.isIdentifierName;
    }
  }

  // eslint-disable-next-line valid-jsdoc
  /**
   * Try to parse a method definition.
   *
   * If it turns out to be one of:
   *  * `IdentifierReference`
   *  * `CoverInitializedName` (`IdentifierReference "=" AssignmentExpression`)
   *  * `PropertyName : AssignmentExpression`
   * The parser will stop at the end of the leading `Identifier` or `PropertyName` and return it.
   *
   * @returns {{methodOrKey: (Method|PropertyName), kind: string}}
   */
  parseMethodDefinition() {
    let token = this.lookahead;
    let startState = this.startNode();

    let preAsyncTokenState = this.saveLexerState();

    let isAsync = !!this.eat(TokenType.ASYNC);
    if (isAsync && this.hasLineTerminatorBeforeNext) {
      isAsync = false;
      this.restoreLexerState(preAsyncTokenState);
    }

    let isGenerator = !!this.eat(TokenType.MUL);
    if (isAsync && !this.lookaheadPropertyName()) {
      isAsync = false;
      isGenerator = false;
      this.restoreLexerState(preAsyncTokenState);
    }

    let { name } = this.parsePropertyName();

    if (!isGenerator && !isAsync) {
      if (token.type === TokenType.IDENTIFIER && token.value.length === 3) {
        // Property Assignment: Getter and Setter.
        if (token.value === 'get' && this.lookaheadPropertyName() && !token.escaped) {
          ({ name } = this.parsePropertyName());
          this.expect(TokenType.LPAREN);
          this.expect(TokenType.RPAREN);
          let previousYield = this.allowYieldExpression;
          let previousAwait = this.allowAwaitExpression;
          let previousAwaitLocation = this.firstAwaitLocation;
          this.allowYieldExpression = false;
          this.allowAwaitExpression = false;
          this.firstAwaitLocation = null;
          let body = this.parseFunctionBody();
          this.allowYieldExpression = previousYield;
          this.allowAwaitExpression = previousAwait;
          this.firstAwaitLocation = previousAwaitLocation;
          return {
            methodOrKey: this.finishNode(new AST.Getter({ name, body }), startState),
            kind: 'method',
          };
        } else if (token.value === 'set' && this.lookaheadPropertyName() && !token.escaped) {
          ({ name } = this.parsePropertyName());
          this.expect(TokenType.LPAREN);
          let previousYield = this.allowYieldExpression;
          let previousAwait = this.allowAwaitExpression;
          let previousAwaitLocation = this.firstAwaitLocation;
          this.allowYieldExpression = false;
          this.allowAwaitExpression = false;
          this.firstAwaitLocation = null;
          let param = this.parseBindingElement();
          this.expect(TokenType.RPAREN);
          let body = this.parseFunctionBody();
          this.allowYieldExpression = previousYield;
          this.allowAwaitExpression = previousAwait;
          this.firstAwaitLocation = previousAwaitLocation;
          return {
            methodOrKey: this.finishNode(new AST.Setter({ name, param, body }), startState),
            kind: 'method',
          };
        }
      }
    }
    if (isAsync) {
      let previousYield = this.allowYieldExpression;
      let previousAwait = this.allowAwaitExpression;
      this.allowYieldExpression = isGenerator;
      this.allowAwaitExpression = true;
      let params = this.parseParams();
      this.allowYieldExpression = isGenerator;
      this.allowAwaitExpression = true;
      let body = this.parseFunctionBody();
      this.allowYieldExpression = previousYield;
      this.allowAwaitExpression = previousAwait;
      return {
        methodOrKey: this.finishNode(new AST.Method({ isAsync, isGenerator, name, params, body }), startState),
        kind: 'method',
      };
    }

    if (this.match(TokenType.LPAREN)) {
      let previousYield = this.allowYieldExpression;
      let previousAwait = this.allowAwaitExpression;
      let previousAwaitLocation = this.firstAwaitLocation;
      this.allowYieldExpression = isGenerator;
      this.allowAwaitExpression = false;
      this.firstAwaitLocation = null;
      let params = this.parseParams();
      let body = this.parseFunctionBody();
      this.allowYieldExpression = previousYield;
      this.allowAwaitExpression = previousAwait;
      this.firstAwaitLocation = previousAwaitLocation;

      return {
        methodOrKey: this.finishNode(new AST.Method({ isAsync, isGenerator, name, params, body }), startState),
        kind: 'method',
      };
    }

    if (isGenerator && this.match(TokenType.COLON)) {
      throw this.createUnexpected(this.lookahead);
    }

    return {
      methodOrKey: name,
      kind: token.type.klass.isIdentifierName ? 'identifier' : 'property',
      escaped: token.escaped,
    };
  }

  parseClass({ isExpr, inDefault }) {
    let startState = this.startNode();

    this.lex();
    let name = null;
    let heritage = null;

    if (this.matchIdentifier()) {
      name = this.parseBindingIdentifier();
    } else if (!isExpr) {
      if (inDefault) {
        name = new AST.BindingIdentifier({ name: '*default*' });
      } else {
        throw this.createUnexpected(this.lookahead);
      }
    }

    if (this.eat(TokenType.EXTENDS)) {
      heritage = this.isolateCoverGrammar(() => this.parseLeftHandSideExpression({ allowCall: true }));
    }

    this.expect(TokenType.LBRACE);
    let elements = [];
    while (!this.eat(TokenType.RBRACE)) {
      if (this.eat(TokenType.SEMICOLON)) {
        continue;
      }
      let isStatic = false;
      let classElementStart = this.startNode();
      let { methodOrKey, kind, escaped } = this.parseMethodDefinition();
      if (kind === 'identifier' && methodOrKey.value === 'static' && !escaped) {
        isStatic = true;
        ({ methodOrKey, kind } = this.parseMethodDefinition());
      }
      if (kind === 'method') {
        elements.push(this.finishNode(new AST.ClassElement({ isStatic, method: methodOrKey }), classElementStart));
      } else {
        throw this.createError('Only methods are allowed in classes');
      }
    }
    return this.finishNode(new (isExpr ? AST.ClassExpression : AST.ClassDeclaration)({ name, super: heritage, elements }), startState);
  }

  parseFunction({ isExpr, inDefault, allowGenerator, isAsync, startState = this.startNode() }) {
    this.lex();
    let name = null;
    let isGenerator = allowGenerator && !!this.eat(TokenType.MUL);

    let previousYield = this.allowYieldExpression;
    let previousAwait = this.allowAwaitExpression;
    let previousAwaitLocation = this.firstAwaitLocation;

    if (isExpr) {
      this.allowYieldExpression = isGenerator;
      this.allowAwaitExpression = isAsync;
    }

    if (!this.match(TokenType.LPAREN)) {
      name = this.parseBindingIdentifier();
    } else if (!isExpr) {
      if (inDefault) {
        name = new AST.BindingIdentifier({ name: '*default*' });
      } else {
        throw this.createUnexpected(this.lookahead);
      }
    }
    this.allowYieldExpression = isGenerator;
    this.allowAwaitExpression = isAsync;
    this.firstAwaitLocation = null;
    let params = this.parseParams();
    let body = this.parseFunctionBody();
    this.allowYieldExpression = previousYield;
    this.allowAwaitExpression = previousAwait;
    this.firstAwaitLocation = previousAwaitLocation;

    return this.finishNode(new (isExpr ? AST.FunctionExpression : AST.FunctionDeclaration)({ isAsync, isGenerator, name, params, body }), startState);
  }

  parseArrayBinding() {
    let startState = this.startNode();

    this.expect(TokenType.LBRACK);

    let elements = [], rest = null;

    while (true) {
      if (this.match(TokenType.RBRACK)) {
        break;
      }
      let el;

      if (this.eat(TokenType.COMMA)) {
        el = null;
      } else {
        if (this.eat(TokenType.ELLIPSIS)) {
          rest = this.parseBindingTarget();
          break;
        } else {
          el = this.parseBindingElement();
        }
        if (!this.match(TokenType.RBRACK)) {
          this.expect(TokenType.COMMA);
        }
      }
      elements.push(el);
    }

    this.expect(TokenType.RBRACK);

    return this.finishNode(new AST.ArrayBinding({ elements, rest }), startState);
  }

  parseBindingProperty() {
    let startState = this.startNode();
    let isIdentifier = this.matchIdentifier();
    let token = this.lookahead;
    let { name, binding } = this.parsePropertyName();
    if (isIdentifier && name.type === 'StaticPropertyName') {
      if (!this.match(TokenType.COLON)) {
        if (this.allowYieldExpression && token.value === 'yield') {
          throw this.createError(ErrorMessages.ILLEGAL_YIELD_IDENTIFIER);
        }
        if (this.allowAwaitExpression && token.value === 'await') {
          throw this.createError(ErrorMessages.ILLEGAL_AWAIT_IDENTIFIER);
        }
        let defaultValue = null;
        if (this.eat(TokenType.ASSIGN)) {
          defaultValue = this.parseAssignmentExpression();
        }
        return this.finishNode(new AST.BindingPropertyIdentifier({
          binding,
          init: defaultValue,
        }), startState);
      }
    }
    this.expect(TokenType.COLON);
    binding = this.parseBindingElement();
    return this.finishNode(new AST.BindingPropertyProperty({ name, binding }), startState);
  }

  parseObjectBinding() {
    let startState = this.startNode();
    this.expect(TokenType.LBRACE);

    let properties = [];
    let rest = null;
    while (!this.match(TokenType.RBRACE)) {
      if (this.eat(TokenType.ELLIPSIS)) {
        rest = this.parseBindingIdentifier();
        break;
      }
      properties.push(this.parseBindingProperty());
      if (!this.match(TokenType.RBRACE)) {
        this.expect(TokenType.COMMA);
      }
    }

    this.expect(TokenType.RBRACE);

    return this.finishNode(new AST.ObjectBinding({ properties, rest }), startState);
  }

  parseBindingTarget() {
    if (this.matchIdentifier()) {
      return this.parseBindingIdentifier();
    }
    switch (this.lookahead.type) {
      case TokenType.LBRACK:
        return this.parseArrayBinding();
      case TokenType.LBRACE:
        return this.parseObjectBinding();
    }
    throw this.createUnexpected(this.lookahead);
  }

  parseBindingElement() {
    let startState = this.startNode();
    let binding = this.parseBindingTarget();
    if (this.eat(TokenType.ASSIGN)) {
      let init = this.parseAssignmentExpression();
      binding = this.finishNode(new AST.BindingWithDefault({ binding, init }), startState);
    }
    return binding;
  }

  parseParam() {
    let previousInParameter = this.inParameter;
    this.inParameter = true;
    let param = this.parseBindingElement();
    this.inParameter = previousInParameter;
    return param;
  }

  parseParams() {
    let startState = this.startNode();
    this.expect(TokenType.LPAREN);

    let items = [], rest = null;
    while (!this.match(TokenType.RPAREN)) {
      if (this.eat(TokenType.ELLIPSIS)) {
        rest = this.parseBindingTarget();
        if (this.lookahead.type === TokenType.ASSIGN) {
          throw this.createError(ErrorMessages.UNEXPECTED_REST_PARAMETERS_INITIALIZATION);
        }
        if (this.match(TokenType.COMMA)) {
          throw this.createError(ErrorMessages.UNEXPECTED_COMMA_AFTER_REST);
        }
        break;
      }
      items.push(this.parseParam());
      if (this.match(TokenType.RPAREN)) break;
      this.expect(TokenType.COMMA);
    }

    this.expect(TokenType.RPAREN);

    return this.finishNode(new AST.FormalParameters({ items, rest }), startState);
  }
}

module.exports = { GenericParser };
