jazz/ast.js

"use strict";

/**
 * Render an array of AST values using each value's toString output.
 *
 * @param {Array<*>} arr: values to stringify
 * @returns {string} array representation for debug output
 */
function arrayToString(arr) {
  return `[${arr
    .map((item) => {
      return item ? item.toString() : "undefined";
    })
    .join(", ")}]`;
}

/**
 * AST root wrapping parsed template body and discovered globals.
 */
class AST {
  /**
   * Create an AST root.
   *
   * @param {Suite} root: root suite node
   * @param {Array<string>} globals: discovered global variable names
   */
  constructor(root, globals) {
    this.root = root;
    this.globals = globals;
  }

  toString() {
    return `AST(globals=${arrayToString(this.globals)}, ${this.root.toString()})`;
  }
}

/**
 * Statement suite containing zero or more child statements.
 */
class Suite {
  /**
   * Create a suite node.
   *
   * @param {Array<*>} body: statement nodes
   */
  constructor(body) {
    this.type = "jazz.ast.Suite";
    this.body = body;
  }

  toString() {
    return `Suite(${this.body.map((stmt) => stmt.toString()).join(", ")})`;
  }
}

/**
 * Identifier expression.
 */
class Ident {
  /**
   * Create an identifier node.
   *
   * @param {string} name: identifier name
   */
  constructor(name) {
    this.type = "jazz.ast.Ident";
    this.name = name;
  }

  toString() {
    return `Ident(${this.name})`;
  }
}

/**
 * If/elif/else statement node.
 */
class IfStmt {
  /**
   * Create an if statement node.
   *
   * @param {*} expr: condition expression
   * @param {Suite} suite: true branch suite
   * @param {*} [orelse]: else branch suite or chained if node
   */
  constructor(expr, suite, orelse) {
    this.type = "jazz.ast.IfStmt";
    this.expr = expr;
    this.suite = suite;
    this.orelse = orelse;
  }

  toString() {
    return `IfStmt(${this.expr.toString()}, ${this.suite.toString()}, ${this.orelse ? this.orelse.toString() : "null"})`;
  }
}

/**
 * Foreach statement node.
 */
class ForEach {
  /**
   * Create a foreach statement node.
   *
   * @param {Ident} ident: loop variable identifier
   * @param {*} expr: iterable expression
   * @param {Suite} suite: loop body suite
   */
  constructor(ident, expr, suite) {
    this.type = "jazz.ast.ForEach";
    this.ident = ident;
    this.expr = expr;
    this.suite = suite;
  }

  toString() {
    return `ForEach(${this.ident.toString()}, ${this.expr.toString()}, ${this.suite.toString()})`;
  }
}

/**
 * Echo statement node.
 */
class Echo {
  /**
   * Create an echo statement node.
   *
   * @param {*} value: expression to output
   */
  constructor(value) {
    this.type = "jazz.ast.EchoStmt";
    this.value = value;
  }

  toString() {
    return `Echo(${this.value.toString()})`;
  }
}

/**
 * Attribute or index access expression node.
 */
class GetAttr {
  /**
   * Create an attribute access node.
   *
   * @param {*} expr: target expression
   * @param {*} id: attribute identifier or index expression
   * @param {boolean} [array]: true when array-style indexing is used
   */
  constructor(expr, id, array) {
    this.expr = expr;
    this.id = id;
    this.array = array;
  }

  toString() {
    const id = this.array ? `[${this.id.toString()}]` : this.id.toString();
    return `GetAttr(${this.expr.toString()}, ${id})`;
  }
}

/**
 * Async function call expression node.
 */
class Call {
  /**
   * Create a call expression node.
   *
   * @param {*} expr: callable expression
   * @param {Array<*>} args: call arguments
   */
  constructor(expr, args) {
    this.expr = expr;
    this.args = args;
  }

  toString() {
    return `Call(${this.id ? this.id.toString() : "undefined"}, ${arrayToString(this.args)})`;
  }
}

/**
 * String literal expression node.
 */
class Str {
  /**
   * Create a string literal node.
   *
   * @param {string} value: literal string value
   */
  constructor(value) {
    this.value = value;
  }

  toString() {
    return `Str('${this.value.replace(/'/g, "\\'")}')`;
  }
}

/**
 * Number literal expression node.
 */
class Num {
  /**
   * Create a number literal node.
   *
   * @param {string|number} value: literal numeric value
   */
  constructor(value) {
    this.value = value;
  }

  toString() {
    return `Num(${this.value})`;
  }
}

/**
 * Boolean literal expression node.
 */
class Bool {
  /**
   * Create a boolean literal node.
   *
   * @param {string|boolean} value: literal boolean value
   */
  constructor(value) {
    this.value = value;
  }

  toString() {
    return `Bool(${this.value})`;
  }
}

/**
 * Logical AND expression node.
 */
class And {
  /**
   * Create a logical AND node.
   *
   * @param {*} left: left expression
   * @param {*} right: right expression
   */
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }

  toString() {
    return `And(${this.left.toString()}, ${this.right.toString()})`;
  }
}

/**
 * Logical OR expression node.
 */
class Or {
  /**
   * Create a logical OR node.
   *
   * @param {*} left: left expression
   * @param {*} right: right expression
   */
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }

  toString() {
    return `Or(${this.left.toString()}, ${this.right.toString()})`;
  }
}

/**
 * Logical NOT expression node.
 */
class Not {
  /**
   * Create a logical NOT node.
   *
   * @param {*} expr: expression to negate
   */
  constructor(expr) {
    this.expr = expr;
  }

  toString() {
    return `Not(${this.expr.toString()})`;
  }
}

/**
 * Binary operation expression node.
 */
class BinOp {
  /**
   * Create a binary operation node.
   *
   * @param {*} left: left expression
   * @param {*} right: right expression
   * @param {string} op: operator string
   */
  constructor(left, right, op) {
    this.left = left;
    this.right = right;
    this.op = op;
  }

  toString() {
    return `BinOp(${this.left.toString()}, ${this.op}, ${this.right.toString()})`;
  }
}

/**
 * Empty-check expression node.
 */
class Empty {
  /**
   * Create an empty-check node.
   *
   * @param {*} expr: expression to evaluate for emptiness
   */
  constructor(expr) {
    this.expr = expr;
  }

  toString() {
    return `Empty(${this.expr.toString()})`;
  }
}

/**
 * Hash/object literal expression node.
 */
class Hash {
  /**
   * Create a hash literal node.
   *
   * @param {Array<Array<*>>} body: key-value pair expressions
   */
  constructor(body) {
    this.body = body;
  }

  toString() {
    return `Hash(${this.body
      .map((kv) => `${kv[0].toString()}:${kv[1].toString()}`)
      .join(", ")})`;
  }
}

/**
 * Synchronous function call expression node.
 */
class SyncCall {
  /**
   * Create a synchronous call node.
   *
   * @param {*} expr: callable expression
   * @param {Array<*>} args: call arguments
   */
  constructor(expr, args) {
    this.expr = expr;
    this.args = args;
  }

  toString() {
    return `SyncCall(${this.id ? this.id.toString() : "undefined"}, ${arrayToString(this.args)})`;
  }
}

/**
 * Array indexing expression node.
 */
class GetArr {
  /**
   * Create an array index access node.
   *
   * @param {*} expr: array expression
   * @param {*} index: index expression
   */
  constructor(expr, index) {
    this.expr = expr;
    this.index = index;
    this.value = this;
  }

  toString() {
    return `GetArr(${this.expr.toString()}, ${this.index.toString()})`;
  }
}

export {
  AST,
  Suite,
  Ident,
  IfStmt,
  Echo,
  ForEach,
  GetAttr,
  Call,
  SyncCall,
  Str,
  Num,
  Bool,
  Hash,
  Or,
  And,
  Not,
  Empty,
  BinOp,
  GetArr,
};

export default {
  AST,
  Suite,
  Ident,
  IfStmt,
  Echo,
  ForEach,
  GetAttr,
  Call,
  SyncCall,
  Str,
  Num,
  Bool,
  Hash,
  Or,
  And,
  Not,
  Empty,
  BinOp,
  GetArr,
};