Source: Parser.js

'use strict';

const LoxError = require('./LoxError');
const TokenType = require('./TokenType');

const BlockStatement = require('./parsing/Statement/Block');
const ExpressionStatement = require('./parsing/Statement/Expression');
const PrintStatement = require('./parsing/Statement/Print');
const VariableStatement = require('./parsing/Statement/Variable');

const AssignmentExpression = require('./parsing/Expression/Assignment');
const BinaryExpression = require('./parsing/Expression/Binary');
const GroupingExpression = require('./parsing/Expression/Grouping');
const LiteralExpression = require('./parsing/Expression/Literal');
const UnaryExpression = require('./parsing/Expression/Unary');
const VariableExpression = require('./parsing/Expression/Variable');

class ParseError extends Error {
  constructor() {
    super();
  }
}

/**
 * Converts scanned {@link Token}s into an executable AST.
 */
class Parser {
  _previous() {
    return this._tokens[this._current - 1];
  }

  _peek() {
    return this._tokens[this._current];
  }

  _isAtEnd() {
    return this._peek().type === TokenType.EOF;
  }

  _advance() {
    if(!this._isAtEnd()) {
      this._current++;
    }
    return this._previous();
  }

  _check(tokenType) {
    if(this._isAtEnd()) {
      return false;
    }
    return this._peek().type == tokenType;
  }

  _match(...tokenTypes) {
    for(let tokenType of tokenTypes) {
      if(this._check(tokenType)) {
        this._advance();
        return true;
      }
    }

    return false;
  }

  _error(token, message) {
    LoxError.parseError(token, message);
    return new ParseError(message);
  }

  _synchronize() {
    this._advance();

    while(!this._isAtEnd()) {
      if(this._previous().type === TokenType.SEMICOLON) {
        return;
      }

      switch(this._peek().type) {
        case TokenType.CLASS:
          return;
        case TokenType.FUN:
          return;
        case TokenType.VAR:
          return;
        case TokenType.FOR:
          return;
        case TokenType.IF:
          return;
        case TokenType.WHILE:
          return;
        case TokenType.PRINT:
          return;
        case TokenType.RETURN:
          return;
      }

      this._advance();
    }
  }

  _consume(tokenType, message) {
    if(this._check(tokenType)) {
      return this._advance();
    }

    throw this._error(this._peek(), message);
  }

  _declaration() {
    try {
      if(this._match(TokenType.VAR)) {
        return this._variableDeclaration();
      }
      else {
        return this._statement();
      }
    }
    catch(error) {
      if(error instanceof ParseError) {
        this._synchronize();
      }
      else {
        throw error;
      }
    }
  }

  _variableDeclaration() {
    const name = this._consume(TokenType.IDENTIFIER, 'Expect variable name.');

    let initializer;

    if(this._match(TokenType.EQUAL)) {
      initializer = this._expression();
    }

    this._consume(TokenType.SEMICOLON, `Expect ';' after variable declaration.`);

    return new VariableStatement(name, initializer);
  }

  _statement() {
    if(this._match(TokenType.PRINT)) {
      return this._printStatement();
    }
    else if(this._match(TokenType.LEFT_BRACE)) {
      return new BlockStatement(this._blockStatement());
    }
    else {
      return this._expressionStatement();
    }
  }

  _printStatement() {
    const value = this._expression();

    this._consume(TokenType.SEMICOLON, `Expect ';' after value.`);

    return new PrintStatement(value);
  }

  _blockStatement() {
    const statements = [];

    while(!this._check(TokenType.RIGHT_BRACE) && !this._isAtEnd()) {
      statements.push(this._declaration());
    }

    this._consume(TokenType.RIGHT_BRACE, `Expect '}' at end of block.`);

    return statements;
  }

  _expressionStatement() {
    const expression = this._expression();

    this._consume(TokenType.SEMICOLON, `Expect ';' after expression.`);

    return new ExpressionStatement(expression);
  }

  _expression() {
    return this._assignment();
  }

  _assignment() {
    const expression = this._equality();

    if(this._match(TokenType.EQUAL)) {
      const equals = this._previous();
      const value = this._assignment();

      if(expression instanceof VariableExpression) {
        const name = expression.name;

        return new AssignmentExpression(name, value);
      }

      this._error(equals, 'Invalid assignment target.');
    }

    return expression;
  }

  _equality() {
    let expression = this._comparison();

    while(this._match(TokenType.BANG_EQUAL, TokenType.EQUAL_EQUAL)) {
      const operator = this._previous();
      const right = this._comparison();
      expression = new BinaryExpression(expression, operator, right);
    }

    return expression;
  }

  _comparison() {
    let expression = this._term();

    while(this._match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) {
      const operator = this._previous();
      const right = this._term();
      expression = new BinaryExpression(expression, operator, right);
    }

    return expression;
  }

  _term() {
    let expression = this._factor();

    while(this._match(TokenType.MINUS, TokenType.PLUS)) {
      const operator = this._previous();
      const right = this._factor();
      expression = new BinaryExpression(expression, operator, right);
    }

    return expression;
  }

  _factor() {
    let expression = this._unary();

    while(this._match(TokenType.SLASH, TokenType.STAR)) {
      const operator = this._previous();
      const right = this._unary();
      expression = new BinaryExpression(expression, operator, right);
    }

    return expression;
  }

  _unary() {
    if(this._match(TokenType.BANG, TokenType.MINUS)) {
      const operator = this._previous();
      const right = this._unary();
      return new UnaryExpression(operator, right);
    }

    return this._primary();
  }

  _primary() {
    if(this._match(TokenType.FALSE)) {
      return new LiteralExpression(false);
    }
    if(this._match(TokenType.TRUE)) {
      return new LiteralExpression(true);
    }
    if(this._match(TokenType.NIL)) {
      return new LiteralExpression(null);
    }

    if(this._match(TokenType.NUMBER, TokenType.STRING)) {
      return new LiteralExpression(this._previous().literal);
    }

    if(this._match(TokenType.IDENTIFIER)) {
      return new VariableExpression(this._previous());
    }

    if(this._match(TokenType.LEFT_PAREN)) {
      const expression = this._expression();
      this._consume(TokenType.RIGHT_PAREN, `Expect ')' after expression.`);
      return new GroupingExpression(expression);
    }

    throw this._error(this._peek(), 'Expect expression.');
  }

  /**
   * Parses {@link Token}s into an collection of ASTs.
   * Will report errors using {@link LoxError}.
   *
   * @param  {Token[]} tokens Scanned tokens.
   * @return {Statement[]} Collection of ASTs representing parsed source.
   */
  parse(tokens) {
    this._tokens = tokens;
    this._current = 0;

    try {
      const statements = [];

      while(!this._isAtEnd()) {
        const statement = this._declaration();

        if(statement) {
          statements.push(statement);
        }
      }

      return statements;
    }
    catch(error) {
      if(error instanceof ParseError) {
        return undefined;
      }
      else {
        throw error;
      }
    }
  }
}

module.exports = Parser;