lib/parser.js

Summary

Maintainability
A
3 hrs
Test Coverage
A
91%
"use strict";

const Modifiers = require("./modifiers");
const Types = require("./types");
const { bn, toHex } = require("./bn");
const modifiers = Object.keys(Modifiers);
const rules = [
  { type: "increment", next: "prev", tokens: ["++", "--"] },
  { type: "sign", next: "prev", tokens: ["+", "-"] },
  { type: "reference", next: "prev", tokens: ["$", "&", "*", "@"] },
  { type: "multiplication", next: "prev", tokens: ["*", "/", "%"], integer: true, left: true, right: true },
  { type: "addiction", next: "prev", tokens: ["+", "-"], integer: true, left: true, right: true }
];
const nrule = rules.length - 1;

rules.map((r, i) => {
  if(! r.next) return;
  if(r.next === "prev") return (r.next = i - 1);
});

// https://en.cppreference.com/w/cpp/language/operator_precedence

function hexSize(type) {
  return type.size < 32 ? "0x20" : toHex(type.size);
}

class RDPError extends Error {}

class Parser {
  constructor(compiler) {
    this.compiler = compiler;
  }

  advance() {
    if(this.pos !== this.tokens.length) this.pos++;
  }

  checkNumber(expression, current, token, left, unary) {
    if(! expression.type.integer) this.error(current, `invalid ${left ? "left" : "right"} operand of type '${expression.type.type}' to ${unary ? "unary" : "binary"} operator '${token}'`);

    return true;
  }

  consume(expected) {
    const current = this.current();

    if(current.eoe) {
      if(this.res.eof) this.error(this.res, `unexpected end of file when '${expected}' expected`);
      else this.error(this.res, `unexpected token when '${expected}' expected: '${this.res.token}'`);
    }
    if(current.token !== expected) this.error(current, `unexpected token when '${expected}' expected: '${current.token}'`);

    this.advance();
  }

  current() {
    return this.pos >= this.tokens.length ? { ...this.res, eoe: true } : this.tokens[this.pos];
  }

  error(context, token, msg) {
    this.compiler.addErr(context, token, msg);
    throw new RDPError(msg);
  }

  getType(type) {
    return this.compiler.getType(type);
  }

  is(rule, token) {
    const prev = this.prev();

    if(rule.tokens.indexOf(token.token) === -1) return false;

    switch(rule.type) {
    case "addiction":
      return prev.operator ? [")", "]"].indexOf(prev.token) !== -1 : true;

    case "sign":
      if(prev.boe) return true;
      return prev.operator ? [")", "]"].indexOf(prev.token) === -1 : false;
    }

    return true;
  }

  modifier(context) {
    let modifier;

    while(modifiers.indexOf((modifier = this.current()).token) !== -1) {
      this.advance();

      for(let i in Modifiers[modifier.token]) if(context[i]) this.error(modifier, `modifier '${modifier.token}' conflicts with modifier '${i}'`);
      if(context[modifier.token]) this.warning(modifier, `repeated modifier: '${modifier.token}'`);

      context.setDeclaration()[modifier.token] = modifier;
    }
  }

  next() {
    return this.pos >= this.tokens.length - 1 ? { ...this.res, eoe: true } : this.tokens[this.pos + 1];
  }

  parse(context, res) {
    try {
      this.pos = 0;
      this.res = res;
      this.tokens = context.tokens;

      const expression = this[context.attr || context.typedef ? "declaration" : "preModifier"](context);

      if(this.pos !== this.tokens.length) this.compiler.addErr(context, this.current(), `unexpected token in expression: '${this.current().token}'`);
      if(expression.scope && res.token === "{") res.scope = expression.scope;
    } catch(e) {
      if(! (e instanceof RDPError)) this.compiler.addErr(context, res, `DevelopmentError: ${e.stack}`);

      return res;
    }

    return res;
  }

  prev() {
    return ! this.pos ? { boe: true } : this.tokens[this.pos - 1];
  }

  warning(token, msg) {
    this.compiler.addWarn(token, msg);
  }

  // Descendent priorities

  preModifier(context) {
    this.modifier(context);

    return this[context.param ? "postModifier" : "declaration"](context);
  }

  declaration(context) {
    let current = this.current();
    const parse = () => this[context.param || context.typeonly ? "reference" : "comma"](context);
    const pos = this.pos;

    if(current.token === "(" && this.next().isType) {
      const round = this.round(context);

      if(round.isType) {
        context.setDeclaration();

        return parse();
      }

      this.pos = pos;
      throw Error("TODO");
    }

    if(current.isType) {
      if(current.token === "auto") {
        if(context.attr) this.error(context, current, "type auto not allowed for attributes");
        if(context.param) this.error(context, current, "type auto not allowed for parameters");
        if(context.typedef) this.error(context, current, "type auto not allowed for typedef");
      }

      context.setDeclaration().setType(current.type);
      this.advance();

      return { ...parse(), first: current };
    } else if(context.declaration && ! context.isEnd(current)) {
      if(! current.symbol) this.error(context, current, `unexpected token when type expected: '${current.token}'`);
      if(this.compiler.reserved[current.token]) this.error(context, current, `unexpected reserved token when type expected: '${current.token}'`);
      this.error(context, current, `unknown type name: '${current.token}'`);
    }

    return parse();
  }

  comma(context) {
    const parse = () => {
      const subContext = context.clone();

      if(context.typeonly) {
        const expression = this.declaration(subContext);
        const current = this.current();

        if(! context.isEnd(current)) this.error(context, current, `unexpected token when ',' or ')' expected: '${current.token}'`);

        return { ...subContext.type, isType: true };
      }

      if(context.declaration) {
        const expression =
          this[
            context.typedef || (context.attr && context.parent && context.parent.scope && context.parent.scope.struct)
              ? "reference"
              : context.attr
                ? "literal"
                : context.param
                  ? "preModifier"
                  : "postModifier"
          ](subContext);
        const current = this.current();
        const what = context.param ? "',' or ')'" : context.attr ? "';'" : "',' or ';'";

        if(! context.isEnd(current)) {
          if(! subContext.name) this.error(context, current, `unexpected token when ${what} expected: '${current.token}'`);
          this.error(context, current, `unexpected token when ${what} expected: '${current.token}'`);
        }

        this.compiler[context.typedef ? "addType" : subContext.scope ? "addFunction" : "addVariable"](subContext, this.current(), this.prev());

        if(subContext.scope) expression.scope = subContext.scope;

        return expression;
      }

      return this.assignment(context);
    };

    let left = parse();

    while(this.current().token === ",") {
      if(context.attr) this.error(context, this.current(), "unexpected token when ';' expected: ','");

      this.advance();
      context.delEnd("{");

      const current = parse();

      if(! left.list) {
        if(left.isType) {
          if(context.attr) this.error(context, this.current(), "type list not allawed for attributes");
          if(context.param) this.error(context, this.current(), "type list not allawed for parameters");
        }

        left = { ...left, list: [left] };
      }

      if(left.list[0].isType && ! current.isType) this.error(context, this.current(), "unexpected expression when type expected");
      if(current.isType && ! left.list[0].isType) this.error(context, this.current(), "unexpected type when expression expected");
      left.list.push(current);
    }

    return left;
  }

  postModifier(context) {
    this.expression = context;

    const left = this.assignment(context);

    this.modifier(context);
    if(this.current().token === "{") context.defined = true;

    return left;
  }

  assignment(context) {
    const left = this[context.declaration ? (context.param ? "declaration" : "function") : "arithmetic"](context, nrule);
    let current, subContext;

    if(["=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", "&=", "^=", "|="].indexOf((current = this.current()).token) !== -1) {
      if(context.declaration && current.token === "=" && context.dim.length) this.error(context, current, "arrays can't be initialized");

      this.advance();
      subContext = context.clone().unsetDeclaration();

      const right = this.assignment(subContext, nrule);

      if(context.declaration) {
        if(current.token !== "=") this.error(context, current, `unexpected operator when '=' expected: '${current.token}'`);
        if(right.constant) context.val = right.hex;
      }

      const { first, lvalue, type } = left;
      const rtype = right.type;

      if(current.token !== "=") this.error(context, current, `operator not yet implemented: '${current.token}'`);

      if(! lvalue) this.error(context, current, `left operand of operator '${current.token}' is not an lvalue`);

      if(type.integer && rtype.integer) {
        if(! type.signed && rtype.signed) this.warning(current, `type sign mismatch in assignment`);
      } else if(type.bytes && rtype.bytes) {
      } else if(type.pointer && rtype.pointer) {
        if(type.short !== rtype.short) this.warning(current, `pointer type mismatch in assignment`);
      } else this.error(context, current, `types mismatch in assignment`);

      if(rtype.size > type.size) this.warning(current, `type size mismatch in assignment`);

      return { first, left, op: current.token, right, type };
    }

    return left;
  }

  function(context) {
    const left = this.reference(context);

    if(this.current().token === "(") {
      context.scope = this.compiler.pushScope();
      this.advance();
      if(this.current().token !== ")") this.comma(context.clone().setParameter());
      this.consume(")");
      this.compiler.popScope();
    } else if(context.type.list) this.error(context, this.current(), "typelist allowed only for functions");

    return left;
  }

  arithmetic(context, n) {
    const rule = rules[n];
    let current, first, hex, lhex, lvalue, op, rhex, right, size, step, token, type;
    let left = rule.left ? this.arithmetic(context, rule.next) : {};

    const mergeTypes = () => {
      const ltype = left.type;
      const rtype = right.type;
      const size = ltype.size > rtype.size ? ltype.size : rtype.size;

      return Types[(ltype.signed || rtype.signed ? "int" : "uint") + size * 8];
    };

    const ops = { add: () => lhex.add(rhex), div: () => lhex.div(rhex), mod: () => lhex.mod(rhex), mul: () => lhex.mul(rhex), sub: () => lhex.sub(rhex) };

    const operate = op => (left = left.constant && right.constant ? { constant: true, first: left.first, hex: toHex(ops[op]()), type } : { first: left.first, left, op, right, type });

    while(this.is(rule, (current = this.current()))) {
      this.advance();
      right = rule.right ? this.arithmetic(context, rule.next) : {};
      lhex = left.constant ? bn(left.hex) : null;
      rhex = right.constant ? bn(right.hex) : null;
      ({ first, token } = current);

      switch(rule.type) {
      case "addiction":
        if(left.type.pointer) {
          if(right.type.pointer) this.error(current, `invalid operands of type '${left.type.type}' and '${right.type.type}' to binary operator '${token}'`);
          this.checkNumber(right, current, token);
          size = hexSize(left.type);
          if(right.constant) right.hex = toHex(rhex.mul(bn(size)));
          else right = { ...right, left: { hex: size }, op: "mul", right };
          type = left.type;
        } else {
          this.checkNumber(left, current, token, true);
          if(right.type.pointer) {
            if(token === "-") this.checkNumber(right, current, token);
            size = hexSize(right.type);
            if(left.constant) left.hex = toHex(lhex.mul(bn(size)));
            else left = { ...left, left, op: "mul", right: { hex: size } };
            type = right.type;
          } else {
            this.checkNumber(right, current, token);
            type = mergeTypes();
          }
        }
        operate(token === "+" ? "add" : "sub");
        break;

      case "increment":
        right = this.arithmetic(context, n);
        ({ hex, lvalue, type } = right);
        if(! lvalue) this.error(current, `right operand of operator '${token}' must be an lvalue`);
        if(type.pointer) {
          size = hexSize(type);
          step = size;
        } else {
          this.checkNumber(right, current, token, false, true);
          step = "0x1";
        }
        return { first, op: token, right, step, type };

      case "multiplication":
        this.checkNumber(left, current, token, true);
        this.checkNumber(right, current, token);
        type = mergeTypes();
        operate(token === "*" ? "mul" : token === "/" ? "div" : "mod");
        break;

      case "reference":
        right = this.arithmetic(context, n);
        ({ hex, lvalue, type } = right);
        switch(token) {
        case "&":
          if(! lvalue) this.error(current, "right operand of operator '&' must be an lvalue");
          const ntype = type.type + "*";
          this.compiler.scope.addType(ntype, ntype);
          type = this.getType(ntype);

          return "mmap" in right ? { constant: true, first, hex: toHex(right.mmap), type } : { first, op: "reference", right, type };
        }

        this.error(current, JSON.stringify(right));
        break;

      case "sign":
        right = this.arithmetic(context, n);
        ({ hex, type } = right);
        op = "nop";
        if(token !== "+" || ! right.type.pointer) {
          this.checkNumber(right, current, token, false, true);
          if(token === "-") {
            op = "neg";
            type = Types["int" + type.size * 8];
            if(right.constant) hex = toHex(bn(hex).neg());
          }
        }

        return right.constant ? { constant: true, first, hex, type } : { first, op, right, type };
      }
    }

    if(rule.type === "increment") return this.square(context);

    return rule.left ? left : this.arithmetic(context, rule.next);
  }

  reference(context) {
    const prev = this.prev();
    const reference = this.current();

    if(["$", "&", "*", "@"].indexOf(reference.token) !== -1 && (prev.isType || prev.boe || (prev.operator && [")", "]"].indexOf(prev.token) === -1))) {
      this.advance();

      if(context.declaration) {
        let type = this.getType(context.type.type);
        let last = type.type.substring(type.type.length - 1);

        if(last === "&" || last === "@" || (last === "*" && reference.token === "$")) this.error(context, reference, `invalid type: '${type.type}${reference.token}'`);

        type = type.type + reference.token;

        this.compiler.scope.addType(type, type);
        context.type = this.getType(type);

        return this.reference(context);
      } else throw Error("TODO");
    }

    return context.typeonly ? { isType: true } : this.square(context);
  }

  square(context) {
    const left = this.round(context);

    while(this.current().token === "[") {
      this.advance();

      const next = this.current(context);
      const subContext = context.clone();
      let right;

      if(next.isType) {
        subContext.setTypeOnly();
        right = this.declaration(subContext, nrule);
      } else {
        subContext.unsetDeclaration();
        right = this.arithmetic(subContext, nrule);
      }

      if(context.declaration) {
        if(right.isType) context.dim.push({ ...subContext.type, first: right.first, isType: true });
        else {
          if(context.contract && ! right.constant) this.error(context, right.first, "contract arrays can't have dynamic dimentions");
          if(context.attr && ! right.constant) this.error(context, right.first, "attribute arrays can't have dynamic dimentions");
          if(right.type.type.substring(0, 4) !== "uint") this.error(context, next, "array dimention must be of type uintN");

          context.dim.push(right);
        }
      } else if(next.isType) this.error(context, next, `unexpected type when expression expected: '${right.type.type}'`);
      else this.error(context, next, `TODO`);

      this.consume("]");
    }

    return left;
  }

  round(context) {
    const current = this.current();

    if(current.token === "(") {
      const subContext = context.clone().addEnd(")");

      if(this.next().isType) subContext.setTypeOnly().addEnd(",");
      this.advance();
      const right = this.comma(subContext);
      this.consume(")");

      if(right.list) {
        if(right.list[0].isType) {
          const name = "(" + right.list.map(t => t.type).join(", ") + ")";
          this.compiler.scope.addType(name, name);
          const type = this.getType(name);
          context.setType(type);

          return { ...type, isType: true };
        }
      }

      return right;
    }

    return this.literal(context);
  }

  literal(context) {
    const compiler = this.compiler;
    const first = this.current();
    const prev = this.prev();
    const scope = compiler.scope;
    const token = first.token;

    this.advance();

    if(context.declaration) {
      const what = context.type ? "identifier" : "type";

      if(first.eof) this.error(context, first, `unexpected end of file when ${what} expected: '${token}'`);
      if(context.type && token === ";" && prev.definition) return {};
      if(! first.number && ! first.string && ! first.symbol) this.error(context, first, `unexpected token when ${what} expected: '${token}'`);
      if(compiler.reserved[token]) this.error(context, first, `unexpected reserved token when ${what} expected: '${token}'`);

      if(context.attr) {
        if(scope.getVariable(token, true)) this.error(context, first, `redefined attribute: '${token}'`);
      } else if(compiler.getType(token)) {
        if(context.type) this.error(context, first, `unexpected type when identifier expected: '${token}'`);
        context.type = compiler.getType(token);
        this.reference(context);

        return {};
      } else if(scope.getVariable(token, true) || scope.getFunction(token)) this.error(context, first, `redefined identifier: '${token}'`);

      context.name = token;
      const { name, type } = context;

      return { lvalue: true, name, type };
    }

    if(first.eoe) this.error(context, first, "unexpected end of expression");
    if(first.number || first.string) return { ...first, first };
    if(! first.symbol) this.error(context, first, `unexpected token in expression: '${token}'`);
    if(compiler.reserved[token]) this.error(context, first, `unexpected reserved token in expression: '${token}'`);

    let somthing;

    if((somthing = compiler.getType(token))) return { ...somthing, first, isType: true };
    if(scope.getFunction(token)) throw Error("TODO");
    if((somthing = scope.getVariable(token))) return this.postOperand(context, { ...somthing, first });
    this.error(context, first, `undefined symbol: '${token}'`);
  }

  postOperand(context, left) {
    const current = this.current();
    const { first, lvalue, type } = left;
    const { token } = current;
    let right, step;

    switch(token) {
    case "++":
    case "--":
      this.advance();
      if(! lvalue) this.error(current, `left operand of operator '${token}' is not an lvalue`);

      if(type.pointer) step = hexSize(type);
      else {
        this.checkNumber(left, current, token, true, true);
        step = "0x1";
      }
      return this.postOperand(context, { first, left, op: token, step, type });

    case "[":
      this.advance();
      if(! left.type.pointer) this.error(current, `cannot indext type '${left.type.type}'`);
      right = this.assignment(context);
      if(left.type.index ? left.type.index !== right.type.type : ! right.type.integer) this.error(current, `type '${left.type.type}' cannot be indexed with type '${right.type.type}'`);
      this.consume("]");

      return this.postOperand(context, { first, left, op: "index", right, type: type.deref });
    }

    return left;
  }
}

module.exports = Parser;