jazz/parser.js

"use strict";

import * as ast from "./ast.js";
import { SyntaxError } from "./error.js";
import * as tokens from "./tokens.js";

/**
 * Check whether a value exists in an array.
 *
 * @param {*} value: value to locate
 * @param {Array<*>} arr: source array
 * @returns {boolean} true when value exists in array
 */
function inArray(value, arr) {
  return arr.indexOf(value) >= 0;
}

/**
 * Parser converting scanner tokens into Jazz AST nodes.
 */
class Parser {
  /**
   * Create a parser.
   *
   * @param {Object} scanner: scanner instance providing next() tokens
   */
  constructor(scanner) {
    this.scanner = scanner;
    this.globals = [];
    this.debug = false;
    this.locals = [];
    this.localsStack = [];
  }

  peek() {
    if (this.nextToken) {
      return this.nextToken;
    }
    this.nextToken = this.scanner.next();
    return this.nextToken;
  }

  next() {
    if (!this.nextToken) {
      this.nextToken = this.scanner.next();
    }
    this.currentToken = this.nextToken;
    delete this.nextToken;
    return this.currentToken;
  }

  current() {
    return this.currentToken;
  }

  /**
   * Parse the full token stream into an AST root.
   *
   * @returns {ast.AST} parsed AST root and globals
   */
  parse() {
    this.next();
    const suite = this._parseSuite();
    this._check(tokens.EOF);
    return new ast.AST(suite, this.globals);
  }

  _pushLocals() {
    this.localsStack.push(this.locals);
  }

  _popLocals() {
    this.locals = this.localsStack.pop();
  }

  _declareLocal(name) {
    if (!inArray(name, this.locals)) {
      this.locals.push(name);
    }
  }

  _declareGlobal(name) {
    if (!inArray(name, this.globals)) {
      this.globals.push(name);
    }
  }

  _isLocal(name) {
    return inArray(name, this.locals);
  }

  _syntaxError(tok, message) {
    throw new SyntaxError(this.scanner.filename, tok.row, tok.col, message);
  }

  _check(type) {
    const curr = this.current();
    if (curr.type !== type) {
      this._syntaxError(curr, `expected ${type}, but got ${curr.type}`);
    }
    return curr;
  }

  _expect(type) {
    this._check(type);
    return this.next();
  }

  _parseExpr() {
    let result = this._parseComparisonExpr();
    if (this.current().type === tokens.AND) {
      this.next();
      result = new ast.And(result, this._parseExpr());
    } else if (this.current().type === tokens.OR) {
      this.next();
      result = new ast.Or(result, this._parseExpr());
    }
    return result;
  }

  _parseComparisonExpr() {
    let result = this._parseSimpleExpr();

    if (this.current().type === tokens.EQ) {
      this.next();
      result = new ast.BinOp(result, this._parseComparisonExpr(), "==");
    } else if (this.current().type === tokens.NEQ) {
      this.next();
      result = new ast.BinOp(result, this._parseComparisonExpr(), "!=");
    } else if (this.current().type === tokens.GT) {
      this.next();
      result = new ast.BinOp(result, this._parseComparisonExpr(), ">");
    }

    return result;
  }

  _parseSimpleExpr() {
    let result;

    if (this.current().type === tokens.NOT) {
      this.next();
      result = new ast.Not(this._parseSimpleExpr());
    } else if (this.current().type === "(") {
      this.next();
      result = this._parseExpr();
      this._expect(")");
    } else if (this.current().type === "{") {
      this.next();
      result = this._parseHash();
      this._expect("}");
    } else if (this.current().type === "@") {
      this.next();
      const expr = this._parseExpr();
      this.next();
      const args = this._parseArgumentList();
      this._expect(")");
      result = new ast.SyncCall(expr, args);
    } else if (this.current().type === tokens.IDENT) {
      result = this._parseIdent();
      if (!this._isLocal(result.name)) {
        this._declareGlobal(result.name);
      }
      result = this._parseGetAttrList(result);
    } else if (this.current().type === tokens.STR) {
      const stringToken = this._check(tokens.STR);
      this.next();
      result = new ast.Str(stringToken.value);
    } else if (this.current().type === tokens.NUM) {
      const numberToken = this._check(tokens.NUM);
      this.next();
      result = new ast.Num(numberToken.value);
    } else if (this.current().type === tokens.BOOL) {
      const boolToken = this._check(tokens.BOOL);
      this.next();
      result = new ast.Bool(boolToken.value);
    } else if (this.current().type === tokens.EMPTY) {
      this.next();
      result = new ast.Empty(this._parseSimpleExpr());
    } else {
      this._syntaxError(
        this.current(),
        `expected expression, but got ${this.current().type}`,
      );
    }

    return result;
  }

  _parseGetAttrList(expr) {
    let result = expr;

    while (this.current().type === "." || this.current().type === "[") {
      switch (this.current().type) {
        case ".":
          this.next();
          result = new ast.GetAttr(result, this._parseIdent());
          break;
        case "[":
          this.next();
          result = new ast.GetAttr(result, this._parseSimpleExpr(), true);
          this._expect("]");
          break;
        default:
      }
    }

    return result;
  }

  _parseHash() {
    const body = [];

    while (this.current().type !== "}") {
      const key = this._parseSimpleExpr();
      this._expect(":");
      const value = this._parseExpr();
      body.push([key, value]);
      if (this.current().type === ",") {
        this.next();
      }
    }

    return new ast.Hash(body);
  }

  _parseIdent() {
    const identToken = this._check(tokens.IDENT);
    this.next();
    return new ast.Ident(identToken.value);
  }

  _parseSuite() {
    const body = [];
    let more = true;

    while (more) {
      const curr = this.current();
      switch (curr.type) {
        case tokens.IF:
          body.push(this._parseIfStmt());
          break;
        case tokens.ECHO:
          body.push(this._parseEcho());
          break;
        case tokens.IDENT:
        case tokens.NUM:
        case tokens.STR:
        case tokens.BOOL:
        case tokens.NOT:
        case "(":
        case "@":
          body.push(this._parseEchoExpr());
          break;
        case tokens.FOREACH:
          body.push(this._parseForEach());
          break;
        default:
          more = false;
      }
    }

    return new ast.Suite(body);
  }

  _parseIfStmt() {
    this._expect(tokens.IF);
    const expr = this._parseExpr();
    const suite = this._parseSuite();
    const root = new ast.IfStmt(expr, suite);
    let ref = root;

    while (this.current().type === tokens.ELIF) {
      this._expect(tokens.ELIF);
      ref.orelse = new ast.IfStmt(this._parseExpr(), this._parseSuite());
      ref = ref.orelse;
    }

    if (this.current().type === tokens.ELSE) {
      this._expect(tokens.ELSE);
      ref.orelse = this._parseSuite();
    }

    this._expect(tokens.END);
    return root;
  }

  _parseEcho() {
    const value = this._check(tokens.ECHO).value;
    this.next();
    return new ast.Echo(new ast.Str(value));
  }

  _parseEchoExpr() {
    const result = this._parseExpr();
    if (this.current().type === "(") {
      this.next();
      const args = this._parseArgumentList();
      this._expect(")");
      return new ast.Call(result, args);
    }

    return new ast.Echo(this._parseGetAttrList(result));
  }

  _parseArgumentList() {
    if (this.current().type === ")") {
      return [];
    }

    const args = [];
    for (;;) {
      const expr = this._parseExpr();
      args.push(expr);
      if (this.current().type !== ",") {
        break;
      }
      this.next();
    }

    return args;
  }

  _parseForEach() {
    this._pushLocals();
    this._expect(tokens.FOREACH);
    const ident = this._parseIdent();
    this._declareLocal(ident.name);
    this._expect(tokens.IN);
    const expr = this._parseExpr();
    const suite = this._parseSuite();
    this._expect(tokens.END);
    this._popLocals();
    return new ast.ForEach(ident, expr, suite);
  }
}

/**
 * Create a parser instance from scanner.
 *
 * @param {Object} scanner: scanner instance
 * @returns {Parser} parser instance
 */
function createParser(scanner) {
  return new Parser(scanner);
}

export { Parser, createParser };

export default {
  Parser,
  createParser,
};