Source: Interpreter.js

'use strict';

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

/**
 * Executes a collection of ASTs in order to run Lox code.
 */
class Interpreter {
  constructor() {
    this._environment = new Environment();
  }

  _evaluate(expression) {
    return expression.accept(this);
  }

  _execute(statement) {
    statement.accept(this);
  }

  _executeBlock(statements, environment) {
    const enclosingEnvironment = this._environment;

    try {
      this._environment = environment;

      for(let statement of statements) {
        this._execute(statement);
      }
    }
    finally {
      this._environment = enclosingEnvironment;
    }
  }

  static _isTruthy(expression) {
    if(expression === null) {
      return false;
    }
    if(typeof expression === 'boolean') {
      return expression;
    }
    return true;
  }

  static _isEqual(a, b) {
    if(a === null) {
      return b === null;
    }

    return a === b;
  }

  static _CheckNumberOperands(operator, ...args) {
    const message = `${args.length > 1 ? 'Operands' : 'Operand'} must be a number.`;

    for(let arg of args) {
      if(typeof arg !== 'number') {
        throw new RuntimeError(operator, message);
      }
    }
  }

  static _stringify(value) {
    if(value === null) {
      return 'nil';
    }

    return `${value}`;
  }

  visitBlockStatement(statement) {
    this._executeBlock(statement.statements, new Environment(this._environment));
  }

  visitExpressionStatement(statement) {
    const value = this._evaluate(statement.expression);
    if(this._repl) {
      console.log(Interpreter._stringify(value));
    }
  }

  visitPrintStatement(statement) {
    const value = this._evaluate(statement.expression);
    console.log(Interpreter._stringify(value));
  }

  visitVariableStatement(statement) {
    let value = null;

    if(statement.initializer) {
      value = this._evaluate(statement.initializer);
    }

    this._environment.define(statement.name, value);
  }

  visitAssignmentExpression(expression) {
    const value = this._evaluate(expression.value);

    this._environment.assign(expression.name, value);

    return value;
  }

  visitBinaryExpression(expression) {
    const left = this._evaluate(expression.left);
    const right = this._evaluate(expression.right);

    switch(expression.operator.type) {
      // Arithmetic
      case TokenType.MINUS:
        Interpreter._CheckNumberOperands(expression.operator, left, right);
        return left - right;
      case TokenType.PLUS:
        if(typeof left === 'string' || typeof right === 'string') {
          return left + right;
        }
        if(typeof left === 'number' && typeof right === 'number') {
          return left + right;
        }
        throw new RuntimeError(expression.operator, 'Operands must numbers or strings.');
      case TokenType.SLASH:
        Interpreter._CheckNumberOperands(expression.operator, left, right);
        return left / right;
      case TokenType.STAR:
        Interpreter._CheckNumberOperands(expression.operator, left, right);
        return left * right;
        // Comparison
      case TokenType.GREATER:
        Interpreter._CheckNumberOperands(expression.operator, left, right);
        return left > right;
      case TokenType.GREATER_EQUAL:
        Interpreter._CheckNumberOperands(expression.operator, left, right);
        return left >= right;
      case TokenType.LESS:
        Interpreter._CheckNumberOperands(expression.operator, left, right);
        return left < right;
      case TokenType.LESS_EQUAL:
        Interpreter._CheckNumberOperands(expression.operator, left, right);
        return left <= right;
        // Equality
      case TokenType.BANG_EQUAL:
        return !Interpreter._isEqual(left, right);
      case TokenType.EQUAL_EQUAL:
        return Interpreter._isEqual(left, right);
    }
  }

  visitGroupingExpression(expression) {
    return this._evaluate(expression.expression);
  }

  visitLiteralExpression(expression) {
    return expression.value;
  }

  visitUnaryExpression(expression) {
    const right = this._evaluate(expression.right);

    switch(expression.operator.type) {
      case TokenType.MINUS:
        Interpreter._CheckNumberOperands(expression.operator, right);
        return -1 * right;
      case TokenType.BANG:
        return !Interpreter._isTruthy(right);
    }
  }

  visitVariableExpression(expression) {
    return this._environment.get(expression.name);
  }

  /**
   * Interprets collection of ASTs in order to execute user input.
   *
   * @param  {Statement[]} statements Collection of statements representing parsed user input.
   * @param  {boolean} repl = false If true then {@link ExpressionStatement}s will print their value; otherwise nothing.
   */
  interpret(statements, repl = false) {
    this._repl = repl;
    try {
      for(let statement of statements) {
        this._execute(statement);
      }
    }
    catch(error) {
      if(error instanceof RuntimeError) {
        LoxError.runtimeError(error);
      }
      else {
        throw error;
      }
    }
    finally {
      this._repl = false;
    }
  }
}

module.exports = Interpreter;