lib/parser.js
"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;