b5be515375
- the language specifications are: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.364.2157&rep=rep1&type=pdf#page=1049 - it can be used to: * as a scripting language for calculation, validations, ... * in SOM expressions to select nodes: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.364.2157&rep=rep1&type=pdf#page=101
1341 lines
29 KiB
JavaScript
1341 lines
29 KiB
JavaScript
/* Copyright 2021 Mozilla Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { Lexer, TOKEN } from "./formcalc_lexer.js";
|
|
|
|
const Errors = {
|
|
assignment: "Invalid token in assignment.",
|
|
block: "Invalid token in do ... end declaration.",
|
|
elseif: "Invalid elseif declaration.",
|
|
for: "Invalid token in for ... endfor declaration.",
|
|
foreach: "Invalid token in foreach ... endfor declaration.",
|
|
func: "Invalid token in func declaration.",
|
|
if: "Invalid token if ... endif declaration.",
|
|
index: "Invalid token in index.",
|
|
params: "Invalid token in parameter list.",
|
|
var: "Invalid token in var declaration.",
|
|
while: "Invalid token while ... endwhile declaration.",
|
|
};
|
|
|
|
const BUILTINS = new Set([
|
|
// Arithmetic.
|
|
"abs",
|
|
"avg",
|
|
"ceil",
|
|
"count",
|
|
"floor",
|
|
"max",
|
|
"min",
|
|
"mod",
|
|
"round",
|
|
"sum",
|
|
// Date and time.
|
|
"date",
|
|
"date2num",
|
|
"datefmt",
|
|
"isodate2num",
|
|
"isotime2num",
|
|
"localdatefmt",
|
|
"localtimefmt",
|
|
"num2date",
|
|
"num2gmtime",
|
|
"num2time",
|
|
"time",
|
|
"time2num",
|
|
"timefmt",
|
|
// Financial.
|
|
"apr",
|
|
"cterm",
|
|
"fv",
|
|
"ipmt",
|
|
"npv",
|
|
"pmt",
|
|
"ppmt",
|
|
"pv",
|
|
"rate",
|
|
"term",
|
|
// Logical.
|
|
"choose",
|
|
"exists",
|
|
"hasvalue",
|
|
"oneof",
|
|
"within",
|
|
// String.
|
|
"at",
|
|
"concat",
|
|
"decode",
|
|
"encode",
|
|
"format",
|
|
"left",
|
|
"len",
|
|
"lower",
|
|
"ltrim",
|
|
"parse",
|
|
"replace",
|
|
"right",
|
|
"rtrim",
|
|
"space",
|
|
"str",
|
|
"stuff",
|
|
"substr",
|
|
"uuid",
|
|
"upper",
|
|
"wordnum",
|
|
// Url.
|
|
"get",
|
|
"post",
|
|
"put",
|
|
// Miscellaneous.
|
|
"eval",
|
|
"ref",
|
|
"unitvalue",
|
|
"unittype",
|
|
// Undocumented.
|
|
"acos",
|
|
"asin",
|
|
"atan",
|
|
"cos",
|
|
"deg2rad",
|
|
"exp",
|
|
"log",
|
|
"pi",
|
|
"pow",
|
|
"rad2deg",
|
|
"sin",
|
|
"sqrt",
|
|
"tan",
|
|
]);
|
|
|
|
const LTR = true;
|
|
const RTL = false;
|
|
|
|
const Operators = {
|
|
dot: { id: 0, prec: 0, assoc: RTL, nargs: 0, repr: "." },
|
|
dotDot: { id: 1, prec: 0, assoc: RTL, nargs: 0, repr: ".." },
|
|
dotHash: { id: 2, prec: 0, assoc: RTL, nargs: 0, repr: ".#" },
|
|
|
|
call: { id: 1, prec: 1, assoc: LTR, nargs: 0 },
|
|
|
|
// Unary operators.
|
|
minus: { id: 4, nargs: 1, prec: 2, assoc: RTL, repr: "-", op: x => -x },
|
|
plus: { id: 5, nargs: 1, prec: 2, assoc: RTL, repr: "+", op: x => +x },
|
|
not: {
|
|
id: 6,
|
|
nargs: 1,
|
|
prec: 2,
|
|
assoc: RTL,
|
|
repr: "!",
|
|
op: x => (!x ? 1 : 0),
|
|
},
|
|
|
|
mul: { id: 7, nargs: 2, prec: 3, assoc: LTR, repr: "*", op: (x, y) => x * y },
|
|
div: { id: 8, nargs: 2, prec: 3, assoc: LTR, repr: "/", op: (x, y) => x / y },
|
|
|
|
add: { id: 9, nargs: 2, prec: 4, assoc: LTR, repr: "+", op: (x, y) => x + y },
|
|
sub: {
|
|
id: 10,
|
|
nargs: 2,
|
|
prec: 4,
|
|
assoc: LTR,
|
|
repr: "-",
|
|
op: (x, y) => x - y,
|
|
},
|
|
|
|
lt: {
|
|
id: 11,
|
|
nargs: 2,
|
|
prec: 5,
|
|
assoc: LTR,
|
|
repr: "<",
|
|
op: (x, y) => (x < y ? 1 : 0),
|
|
},
|
|
le: {
|
|
id: 12,
|
|
nargs: 2,
|
|
prec: 5,
|
|
assoc: LTR,
|
|
repr: "<=",
|
|
op: (x, y) => (x <= y ? 1 : 0),
|
|
},
|
|
gt: {
|
|
id: 13,
|
|
nargs: 2,
|
|
prec: 5,
|
|
assoc: LTR,
|
|
repr: ">",
|
|
op: (x, y) => (x > y ? 1 : 0),
|
|
},
|
|
ge: {
|
|
id: 14,
|
|
nargs: 2,
|
|
prec: 5,
|
|
assoc: LTR,
|
|
repr: ">=",
|
|
op: (x, y) => (x >= y ? 1 : 0),
|
|
},
|
|
|
|
eq: {
|
|
id: 15,
|
|
nargs: 2,
|
|
prec: 6,
|
|
assoc: LTR,
|
|
repr: "===",
|
|
op: (x, y) => (x === y ? 1 : 0),
|
|
},
|
|
ne: {
|
|
id: 16,
|
|
nargs: 2,
|
|
prec: 6,
|
|
assoc: LTR,
|
|
repr: "!==",
|
|
op: (x, y) => (x !== y ? 1 : 0),
|
|
},
|
|
|
|
and: {
|
|
id: 17,
|
|
nargs: 2,
|
|
prec: 7,
|
|
assoc: LTR,
|
|
repr: "&&",
|
|
op: (x, y) => (x && y ? 1 : 0),
|
|
},
|
|
|
|
or: {
|
|
id: 18,
|
|
nargs: 2,
|
|
prec: 8,
|
|
assoc: LTR,
|
|
repr: "||",
|
|
op: (x, y) => (x || y ? 1 : 0),
|
|
},
|
|
|
|
// Not real operators.
|
|
paren: { id: 19, prec: 9, assoc: RTL, nargs: 0 },
|
|
subscript: { id: 20, prec: 9, assoc: RTL, nargs: 0 },
|
|
};
|
|
|
|
const OPERATOR = true;
|
|
const OPERAND = false;
|
|
|
|
// How it works...
|
|
//
|
|
// There is two stacks: one for operands and one for operators.
|
|
// Each time an operand is met (number, identifier, ...),
|
|
// it's pushed on operands stack.
|
|
// Unary operators such as + or - are guessed according to the last pushed
|
|
// thing:
|
|
// for example, if an operand has been push then a '-' is a subtraction
|
|
// but if an operator has been push (e.g. '*') then a '-' is the negate
|
|
// operation ('... * - ...' can't be a subtraction).
|
|
// Each time an operator is met its precedence is compared with the one of the
|
|
// operator on top of operators stack:
|
|
// - if top has precendence on operator then top is applied to the operands
|
|
// on their stack;
|
|
// - else just push the operator.
|
|
// For example: 1 + 2 * 3
|
|
// round 1: operands: [1], operators: []
|
|
// round 2: operands: [1], operators: [+]
|
|
// round 3: operands: [1, 2], operators: [+]
|
|
//
|
|
// + has not the precedence on *
|
|
// round 4: operands: [1, 2], operators: [+, *]
|
|
// round 5: operands: [1, 2, 3], operators: [+, *]
|
|
// no more token: apply operators on operands:
|
|
// round 6: operands: [1, 6], operators: [+]
|
|
// round 7: operands: [7], operators: []
|
|
// Parenthesis are treated like an operator with no precedence on the real ones.
|
|
// As a consequence, any operation is done before this fake one and when
|
|
// a right parenthesis is met then we can apply operators to operands
|
|
// until the opening parenthesis is met.
|
|
//
|
|
class SimpleExprParser {
|
|
constructor(lexer) {
|
|
this.lexer = lexer;
|
|
this.operands = [];
|
|
this.operators = [];
|
|
this.last = OPERATOR;
|
|
}
|
|
|
|
reset() {
|
|
this.operands.length = 0;
|
|
this.operators.length = 0;
|
|
this.last = OPERATOR;
|
|
}
|
|
|
|
parse(tok) {
|
|
tok = tok || this.lexer.next();
|
|
|
|
while (true) {
|
|
// Token ids (see form_lexer.js) are consecutive in order
|
|
// to have switch table with no holes.
|
|
switch (tok.id) {
|
|
case TOKEN.and:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.and);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.divide:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.div);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.dot:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.dot);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.dotDot:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.dotDot);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.dotHash:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.dotHash);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.dotStar:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.dot);
|
|
this.pushOperand(new AstEveryOccurence());
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.eq:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.eq);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.ge:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.ge);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.gt:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.gt);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.le:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.le);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.leftBracket:
|
|
if (this.last === OPERAND) {
|
|
this.flushWithOperator(Operators.subscript);
|
|
const operand = this.operands.pop();
|
|
const index = SimpleExprParser.parseIndex(this.lexer);
|
|
this.operands.push(new AstSubscript(operand, index));
|
|
this.last = OPERAND;
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.leftParen:
|
|
if (this.last === OPERAND) {
|
|
const lastOperand = this.operands[this.operands.length - 1];
|
|
if (!(lastOperand instanceof AstIdentifier)) {
|
|
return [tok, this.getNode()];
|
|
}
|
|
lastOperand.toLowerCase();
|
|
const name = lastOperand.id;
|
|
|
|
this.flushWithOperator(Operators.call);
|
|
const callee = this.operands.pop();
|
|
const params = SimpleExprParser.parseParams(this.lexer);
|
|
|
|
if (callee instanceof AstIdentifier && BUILTINS.has(name)) {
|
|
this.operands.push(new AstBuiltinCall(name, params));
|
|
} else {
|
|
this.operands.push(new AstCall(callee, params));
|
|
}
|
|
|
|
this.last = OPERAND;
|
|
} else {
|
|
this.operators.push(Operators.paren);
|
|
this.last = OPERATOR;
|
|
}
|
|
break;
|
|
case TOKEN.lt:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.lt);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.minus:
|
|
if (this.last === OPERATOR) {
|
|
this.pushOperator(Operators.minus);
|
|
} else {
|
|
this.pushOperator(Operators.sub);
|
|
}
|
|
break;
|
|
case TOKEN.ne:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.ne);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.not:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.not);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.null:
|
|
if (this.last === OPERATOR) {
|
|
this.pushOperand(new AstNull());
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.number:
|
|
if (this.last === OPERATOR) {
|
|
this.pushOperand(new AstNumber(tok.value));
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.or:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.or);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.plus:
|
|
if (this.last === OPERATOR) {
|
|
this.pushOperator(Operators.plus);
|
|
} else {
|
|
this.pushOperator(Operators.add);
|
|
}
|
|
break;
|
|
case TOKEN.rightBracket:
|
|
if (!this.flushUntil(Operators.subscript.id)) {
|
|
return [tok, this.getNode()];
|
|
}
|
|
break;
|
|
case TOKEN.rightParen:
|
|
if (!this.flushUntil(Operators.paren.id)) {
|
|
return [tok, this.getNode()];
|
|
}
|
|
break;
|
|
case TOKEN.string:
|
|
if (this.last === OPERATOR) {
|
|
this.pushOperand(new AstString(tok.value));
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.this:
|
|
if (this.last === OPERATOR) {
|
|
this.pushOperand(new AstThis());
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.times:
|
|
if (this.last === OPERAND) {
|
|
this.pushOperator(Operators.mul);
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
case TOKEN.identifier:
|
|
if (this.last === OPERATOR) {
|
|
this.pushOperand(new AstIdentifier(tok.value));
|
|
break;
|
|
}
|
|
return [tok, this.getNode()];
|
|
default:
|
|
return [tok, this.getNode()];
|
|
}
|
|
tok = this.lexer.next();
|
|
}
|
|
}
|
|
|
|
static parseParams(lexer) {
|
|
const parser = new SimpleExprParser(lexer);
|
|
const params = [];
|
|
while (true) {
|
|
const [tok, param] = parser.parse();
|
|
if (param) {
|
|
params.push(param);
|
|
}
|
|
if (tok.id === TOKEN.rightParen) {
|
|
return params;
|
|
} else if (tok.id !== TOKEN.comma) {
|
|
throw new Error(Errors.params);
|
|
}
|
|
parser.reset();
|
|
}
|
|
}
|
|
|
|
static parseIndex(lexer) {
|
|
let tok = lexer.next();
|
|
if (tok.id === TOKEN.times) {
|
|
tok = lexer.next();
|
|
if (tok.id !== TOKEN.rightBracket) {
|
|
throw new Error(Errors.index);
|
|
}
|
|
return new AstEveryOccurence();
|
|
}
|
|
const [token, expr] = new SimpleExprParser(lexer).parse(tok);
|
|
if (token.id !== TOKEN.rightBracket) {
|
|
throw new Error(Errors.index);
|
|
}
|
|
return expr;
|
|
}
|
|
|
|
pushOperator(op) {
|
|
this.flushWithOperator(op);
|
|
this.operators.push(op);
|
|
this.last = OPERATOR;
|
|
}
|
|
|
|
pushOperand(op) {
|
|
this.operands.push(op);
|
|
this.last = OPERAND;
|
|
}
|
|
|
|
operate(op) {
|
|
if (op.nargs === 1) {
|
|
const arg = this.operands.pop();
|
|
this.operands.push(AstUnaryOperator.getOperatorOrValue(op, arg));
|
|
} else {
|
|
const arg2 = this.operands.pop();
|
|
const arg1 = this.operands.pop();
|
|
this.operands.push(AstBinaryOperator.getOperatorOrValue(op, arg1, arg2));
|
|
}
|
|
}
|
|
|
|
flushWithOperator(op) {
|
|
while (true) {
|
|
const top = this.operators[this.operators.length - 1];
|
|
if (top) {
|
|
if (top.id >= 0 && SimpleExprParser.checkPrecedence(top, op)) {
|
|
this.operators.pop();
|
|
this.operate(top);
|
|
continue;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
flush() {
|
|
while (true) {
|
|
const op = this.operators.pop();
|
|
if (!op) {
|
|
return;
|
|
}
|
|
this.operate(op);
|
|
}
|
|
}
|
|
|
|
flushUntil(id) {
|
|
while (true) {
|
|
const op = this.operators.pop();
|
|
if (!op) {
|
|
return false;
|
|
}
|
|
if (op.id === id) {
|
|
return true;
|
|
}
|
|
this.operate(op);
|
|
}
|
|
}
|
|
|
|
getNode() {
|
|
this.flush();
|
|
return this.operands.pop();
|
|
}
|
|
|
|
static checkPrecedence(left, right) {
|
|
return (
|
|
left.prec < right.prec || (left.prec === right.prec && left.assoc === LTR)
|
|
);
|
|
}
|
|
}
|
|
|
|
class Leaf {
|
|
dump() {
|
|
throw new Error("Not implemented method");
|
|
}
|
|
|
|
isSomPredicate() {
|
|
return false;
|
|
}
|
|
|
|
isDotExpression() {
|
|
return false;
|
|
}
|
|
|
|
isConstant() {
|
|
return false;
|
|
}
|
|
|
|
toNumber() {
|
|
return 0;
|
|
}
|
|
|
|
toComparable() {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class AstCall extends Leaf {
|
|
constructor(callee, params) {
|
|
super();
|
|
this.callee = callee;
|
|
this.params = params;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
callee: this.callee.dump(),
|
|
params: this.params.map(x => x.dump()),
|
|
};
|
|
}
|
|
}
|
|
|
|
class AstBuiltinCall extends Leaf {
|
|
constructor(id, params) {
|
|
super();
|
|
this.id = id;
|
|
this.params = params;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
builtin: this.id,
|
|
params: this.params.map(x => x.dump()),
|
|
};
|
|
}
|
|
}
|
|
|
|
class AstSubscript extends Leaf {
|
|
constructor(operand, index) {
|
|
super();
|
|
this.operand = operand;
|
|
this.index = index;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
operand: this.operand.dump(),
|
|
index: this.index.dump(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class AstBinaryOperator extends Leaf {
|
|
constructor(id, left, right, repr) {
|
|
super();
|
|
this.id = id;
|
|
this.left = left;
|
|
this.right = right;
|
|
this.repr = repr;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
operator: this.repr,
|
|
left: this.left.dump(),
|
|
right: this.right.dump(),
|
|
};
|
|
}
|
|
|
|
isDotExpression() {
|
|
return Operators.id.dot <= this.id && this.id <= Operators.id.dotHash;
|
|
}
|
|
|
|
isSomPredicate() {
|
|
return (
|
|
this.isDotExpression() ||
|
|
(Operators.id.lt <= this.id &&
|
|
this.id <= Operators.id.or &&
|
|
((this.left.isDotExpression() && this.right.isConstant()) ||
|
|
(this.left.isConstant() && this.right.isDotExpression()) ||
|
|
(this.left.isDotExpression() && this.right.isDotExpression())))
|
|
);
|
|
}
|
|
|
|
static getOperatorOrValue(operator, left, right) {
|
|
if (!left.isConstant() || !right.isConstant()) {
|
|
return new AstBinaryOperator(operator.id, left, right, operator.repr);
|
|
}
|
|
|
|
if (
|
|
Operators.lt.id <= operator.id &&
|
|
operator.id <= Operators.ne.id &&
|
|
!(left instanceof AstNumber) &&
|
|
!(right instanceof AstNumber)
|
|
) {
|
|
return new AstNumber(
|
|
operator.op(left.toComparable(), right.toComparable())
|
|
);
|
|
}
|
|
|
|
return new AstNumber(operator.op(left.toNumber(), right.toNumber()));
|
|
}
|
|
}
|
|
|
|
class AstUnaryOperator extends Leaf {
|
|
constructor(id, arg, repr) {
|
|
super();
|
|
this.id = id;
|
|
this.arg = arg;
|
|
this.repr = repr;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
operator: this.repr,
|
|
arg: this.arg.dump(),
|
|
};
|
|
}
|
|
|
|
static getOperatorOrValue(operator, arg) {
|
|
if (!arg.isConstant()) {
|
|
return new AstUnaryOperator(operator.id, arg, operator.repr);
|
|
}
|
|
|
|
return new AstNumber(operator.op(arg.toNumber()));
|
|
}
|
|
}
|
|
|
|
class AstNumber extends Leaf {
|
|
constructor(number) {
|
|
super();
|
|
this.number = number;
|
|
}
|
|
|
|
dump() {
|
|
return this.number;
|
|
}
|
|
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
|
|
toNumber() {
|
|
return this.number;
|
|
}
|
|
}
|
|
|
|
class AstString extends Leaf {
|
|
constructor(str) {
|
|
super();
|
|
this.str = str;
|
|
}
|
|
|
|
dump() {
|
|
return this.str;
|
|
}
|
|
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
|
|
toNumber() {
|
|
return !isNaN(this.str) ? parseFloat(this.str) : 0;
|
|
}
|
|
|
|
toComparable() {
|
|
return this.str;
|
|
}
|
|
}
|
|
|
|
class AstThis extends Leaf {
|
|
dump() {
|
|
return { special: "this" };
|
|
}
|
|
}
|
|
|
|
class AstIdentifier extends Leaf {
|
|
constructor(id) {
|
|
super();
|
|
this.id = id;
|
|
}
|
|
|
|
dump() {
|
|
return { id: this.id };
|
|
}
|
|
|
|
toLowerCase() {
|
|
this.id = this.id.toLowerCase();
|
|
}
|
|
}
|
|
|
|
class AstNull extends Leaf {
|
|
dump() {
|
|
return { special: null };
|
|
}
|
|
|
|
isConstant() {
|
|
return true;
|
|
}
|
|
|
|
toComparable() {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class AstEveryOccurence {
|
|
dump() {
|
|
return { special: "*" };
|
|
}
|
|
}
|
|
|
|
class VarDecl extends Leaf {
|
|
constructor(id, expr) {
|
|
super();
|
|
this.id = id;
|
|
this.expr = expr;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
var: this.id,
|
|
expr: this.expr.dump(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class Assignment extends Leaf {
|
|
constructor(id, expr) {
|
|
super();
|
|
this.id = id;
|
|
this.expr = expr;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
assignment: this.id,
|
|
expr: this.expr.dump(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class FuncDecl extends Leaf {
|
|
constructor(id, params, body) {
|
|
super();
|
|
this.id = id;
|
|
this.params = params;
|
|
this.body = body;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
func: this.id,
|
|
params: this.params,
|
|
body: this.body.dump(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class IfDecl extends Leaf {
|
|
constructor(condition, thenClause, elseIfClause, elseClause) {
|
|
super();
|
|
this.condition = condition;
|
|
this.then = thenClause;
|
|
this.elseif = elseIfClause;
|
|
this.else = elseClause;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
decl: "if",
|
|
condition: this.condition.dump(),
|
|
then: this.then.dump(),
|
|
elseif: this.elseif ? this.elseif.map(x => x.dump()) : null,
|
|
else: this.else ? this.else.dump() : null,
|
|
};
|
|
}
|
|
}
|
|
|
|
class ElseIfDecl extends Leaf {
|
|
constructor(condition, thenClause) {
|
|
super();
|
|
this.condition = condition;
|
|
this.then = thenClause;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
decl: "elseif",
|
|
condition: this.condition.dump(),
|
|
then: this.then.dump(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class WhileDecl extends Leaf {
|
|
constructor(condition, whileClause) {
|
|
super();
|
|
this.condition = condition;
|
|
this.body = whileClause;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
decl: "while",
|
|
condition: this.condition.dump(),
|
|
body: this.body.dump(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class ForDecl extends Leaf {
|
|
constructor(assignment, upto, end, step, body) {
|
|
super();
|
|
this.assignment = assignment;
|
|
this.upto = upto;
|
|
this.end = end;
|
|
this.step = step;
|
|
this.body = body;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
decl: "for",
|
|
assignment: this.assignment.dump(),
|
|
type: this.upto ? "upto" : "downto",
|
|
end: this.end.dump(),
|
|
step: this.step ? this.step.dump() : null,
|
|
body: this.body.dump(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class ForeachDecl extends Leaf {
|
|
constructor(id, params, body) {
|
|
super();
|
|
this.id = id;
|
|
this.params = params;
|
|
this.body = body;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
decl: "foreach",
|
|
id: this.id,
|
|
params: this.params.map(x => x.dump()),
|
|
body: this.body.dump(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class BlockDecl extends Leaf {
|
|
constructor(body) {
|
|
super();
|
|
this.body = body;
|
|
}
|
|
|
|
dump() {
|
|
return {
|
|
decl: "block",
|
|
body: this.body.dump(),
|
|
};
|
|
}
|
|
}
|
|
|
|
class ExprList extends Leaf {
|
|
constructor(expressions) {
|
|
super();
|
|
this.expressions = expressions;
|
|
}
|
|
|
|
dump() {
|
|
return this.expressions.map(x => x.dump());
|
|
}
|
|
}
|
|
|
|
class BreakDecl extends Leaf {
|
|
dump() {
|
|
return { special: "break" };
|
|
}
|
|
}
|
|
|
|
class ContinueDecl extends Leaf {
|
|
dump() {
|
|
return { special: "continue" };
|
|
}
|
|
}
|
|
|
|
class Parser {
|
|
constructor(code) {
|
|
this.lexer = new Lexer(code);
|
|
}
|
|
|
|
parse() {
|
|
const [tok, decls] = this.parseExprList();
|
|
if (tok.id !== TOKEN.eof) {
|
|
throw new Error("Invalid token in Form code");
|
|
}
|
|
return decls;
|
|
}
|
|
|
|
parseExprList() {
|
|
const expressions = [];
|
|
let tok = null,
|
|
expr;
|
|
while (true) {
|
|
[tok, expr] = this.parseExpr(tok);
|
|
if (!expr) {
|
|
return [tok, new ExprList(expressions)];
|
|
}
|
|
expressions.push(expr);
|
|
}
|
|
}
|
|
|
|
parseExpr(tok) {
|
|
tok = tok || this.lexer.next();
|
|
switch (tok.id) {
|
|
case TOKEN.identifier:
|
|
return this.parseAssigmentOrExpr(tok);
|
|
case TOKEN.break:
|
|
return [null, new BreakDecl()];
|
|
case TOKEN.continue:
|
|
return [null, new ContinueDecl()];
|
|
case TOKEN.do:
|
|
return this.parseBlock();
|
|
case TOKEN.for:
|
|
return this.parseFor();
|
|
case TOKEN.foreach:
|
|
return this.parseForeach();
|
|
case TOKEN.func:
|
|
return this.parseFuncDecl();
|
|
case TOKEN.if:
|
|
return this.parseIf();
|
|
case TOKEN.var:
|
|
return this.parseVarDecl();
|
|
case TOKEN.while:
|
|
return this.parseWhile();
|
|
default:
|
|
return this.parseSimpleExpr(tok);
|
|
}
|
|
}
|
|
|
|
parseAssigmentOrExpr(tok) {
|
|
const savedTok = tok;
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id === TOKEN.assign) {
|
|
const [tok1, expr] = this.parseSimpleExpr(null);
|
|
return [tok1, new Assignment(savedTok.value, expr)];
|
|
}
|
|
|
|
const parser = new SimpleExprParser(this.lexer);
|
|
parser.pushOperand(new AstIdentifier(savedTok.value));
|
|
|
|
return parser.parse(tok);
|
|
}
|
|
|
|
parseBlock() {
|
|
const [tok1, body] = this.parseExprList();
|
|
|
|
const tok = tok1 || this.lexer.next();
|
|
if (tok.id !== TOKEN.end) {
|
|
throw new Error(Errors.block);
|
|
}
|
|
|
|
return [null, new BlockDecl(body)];
|
|
}
|
|
|
|
parseVarDecl() {
|
|
// 'var' Identifier ('=' SimpleExpression)?
|
|
let tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.identifier) {
|
|
throw new Error(Errors.var);
|
|
}
|
|
|
|
const identifier = tok.value;
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.assign) {
|
|
return [tok, new VarDecl(identifier, null)];
|
|
}
|
|
|
|
const [tok1, expr] = this.parseSimpleExpr();
|
|
return [tok1, new VarDecl(identifier, expr)];
|
|
}
|
|
|
|
parseFuncDecl() {
|
|
// 'func' Identifier ParameterList 'do' ExpressionList 'endfunc'.
|
|
let tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.identifier) {
|
|
throw new Error(Errors.func);
|
|
}
|
|
|
|
const identifier = tok.value;
|
|
const params = this.parseParamList();
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.do) {
|
|
throw new Error(Errors.func);
|
|
}
|
|
|
|
const [tok1, body] = this.parseExprList();
|
|
|
|
tok = tok1 || this.lexer.next();
|
|
if (tok.id !== TOKEN.endfunc) {
|
|
throw new Error(Errors.func);
|
|
}
|
|
|
|
return [null, new FuncDecl(identifier, params, body)];
|
|
}
|
|
|
|
parseParamList() {
|
|
// '(' Identifier * ')'.
|
|
const params = [];
|
|
|
|
let tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.leftParen) {
|
|
throw new Error(Errors.func);
|
|
}
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id === TOKEN.rightParen) {
|
|
return params;
|
|
}
|
|
|
|
while (true) {
|
|
if (tok.id !== TOKEN.identifier) {
|
|
throw new Error(Errors.func);
|
|
}
|
|
params.push(tok.value);
|
|
tok = this.lexer.next();
|
|
if (tok.id === TOKEN.rightParen) {
|
|
return params;
|
|
}
|
|
if (tok.id !== TOKEN.comma) {
|
|
throw new Error(Errors.func);
|
|
}
|
|
tok = this.lexer.next();
|
|
}
|
|
}
|
|
|
|
parseSimpleExpr(tok = null) {
|
|
return new SimpleExprParser(this.lexer).parse(tok);
|
|
}
|
|
|
|
parseIf() {
|
|
// 'if' '(' SimpleExpression ')' then ExpressionList
|
|
// ('elseif' '(' SimpleExpression ')' then ExpressionList )*
|
|
// ('else' ExpressionList)?
|
|
// 'endif'.
|
|
let elseIfClause = [];
|
|
let tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.leftParen) {
|
|
throw new Error(Errors.if);
|
|
}
|
|
|
|
const [tok1, condition] = this.parseSimpleExpr();
|
|
|
|
tok = tok1 || this.lexer.next();
|
|
if (tok.id !== TOKEN.rightParen) {
|
|
throw new Error(Errors.if);
|
|
}
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.then) {
|
|
throw new Error(Errors.if);
|
|
}
|
|
|
|
const [tok2, thenClause] = this.parseExprList();
|
|
tok = tok2 || this.lexer.next();
|
|
|
|
while (tok.id === TOKEN.elseif) {
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.leftParen) {
|
|
throw new Error(Errors.elseif);
|
|
}
|
|
|
|
const [tok3, elseIfCondition] = this.parseSimpleExpr();
|
|
|
|
tok = tok3 || this.lexer.next();
|
|
if (tok.id !== TOKEN.rightParen) {
|
|
throw new Error(Errors.elseif);
|
|
}
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.then) {
|
|
throw new Error(Errors.elseif);
|
|
}
|
|
|
|
const [tok4, elseIfThenClause] = this.parseExprList();
|
|
elseIfClause.push(new ElseIfDecl(elseIfCondition, elseIfThenClause));
|
|
|
|
tok = tok4 || this.lexer.next();
|
|
}
|
|
|
|
if (elseIfClause.length === 0) {
|
|
elseIfClause = null;
|
|
}
|
|
|
|
if (tok.id === TOKEN.endif) {
|
|
return [null, new IfDecl(condition, thenClause, elseIfClause, null)];
|
|
}
|
|
|
|
if (tok.id !== TOKEN.else) {
|
|
throw new Error(Errors.if);
|
|
}
|
|
|
|
const [tok5, elseClause] = this.parseExprList();
|
|
|
|
tok = tok5 || this.lexer.next();
|
|
if (tok.id !== TOKEN.endif) {
|
|
throw new Error(Errors.if);
|
|
}
|
|
|
|
return [null, new IfDecl(condition, thenClause, elseIfClause, elseClause)];
|
|
}
|
|
|
|
parseWhile() {
|
|
// 'while' '(' SimpleExpression ')' 'do' ExprList 'endwhile'
|
|
let tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.leftParen) {
|
|
throw new Error(Errors.while);
|
|
}
|
|
|
|
const [tok1, condition] = this.parseSimpleExpr();
|
|
|
|
tok = tok1 || this.lexer.next();
|
|
if (tok.id !== TOKEN.rightParen) {
|
|
throw new Error(Errors.while);
|
|
}
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.do) {
|
|
throw new Error(Errors.while);
|
|
}
|
|
|
|
const [tok2, whileClause] = this.parseExprList();
|
|
|
|
tok = tok2 || this.lexer.next();
|
|
if (tok.id !== TOKEN.endwhile) {
|
|
throw new Error(Errors.while);
|
|
}
|
|
|
|
return [null, new WhileDecl(condition, whileClause)];
|
|
}
|
|
|
|
parseAssignment() {
|
|
let tok = this.lexer.next();
|
|
let hasVar = false;
|
|
if (tok.id === TOKEN.var) {
|
|
hasVar = true;
|
|
tok = this.lexer.next();
|
|
}
|
|
|
|
if (tok.id !== TOKEN.identifier) {
|
|
throw new Error(Errors.assignment);
|
|
}
|
|
|
|
const identifier = tok.value;
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.assign) {
|
|
throw new Error(Errors.assignment);
|
|
}
|
|
|
|
const [tok1, expr] = this.parseSimpleExpr();
|
|
if (hasVar) {
|
|
return [tok1, new VarDecl(identifier, expr)];
|
|
}
|
|
return [tok1, new Assignment(identifier, expr)];
|
|
}
|
|
|
|
parseFor() {
|
|
// 'for' Assignment ('upto'|'downto') Expr ('step' Expr)? 'do'
|
|
// ExprList 'endfor'
|
|
let tok,
|
|
step = null;
|
|
let upto = false;
|
|
const [tok1, assignment] = this.parseAssignment();
|
|
|
|
tok = tok1 || this.lexer.next();
|
|
if (tok.id === TOKEN.upto) {
|
|
upto = true;
|
|
} else if (tok.id !== TOKEN.downto) {
|
|
throw new Error(Errors.for);
|
|
}
|
|
|
|
const [tok2, end] = this.parseSimpleExpr();
|
|
|
|
tok = tok2 || this.lexer.next();
|
|
if (tok.id === TOKEN.step) {
|
|
[tok, step] = this.parseSimpleExpr();
|
|
tok = tok || this.lexer.next();
|
|
}
|
|
|
|
if (tok.id !== TOKEN.do) {
|
|
throw new Error(Errors.for);
|
|
}
|
|
|
|
const [tok3, body] = this.parseExprList();
|
|
|
|
tok = tok3 || this.lexer.next();
|
|
if (tok.id !== TOKEN.endfor) {
|
|
throw new Error(Errors.for);
|
|
}
|
|
|
|
return [null, new ForDecl(assignment, upto, end, step, body)];
|
|
}
|
|
|
|
parseForeach() {
|
|
// 'for' Identifier 'in' '(' ArgumentList ')' 'do'
|
|
// ExprList 'endfor'
|
|
let tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.identifier) {
|
|
throw new Error(Errors.foreach);
|
|
}
|
|
|
|
const identifier = tok.value;
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.in) {
|
|
throw new Error(Errors.foreach);
|
|
}
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.leftParen) {
|
|
throw new Error(Errors.foreach);
|
|
}
|
|
|
|
const params = SimpleExprParser.parseParams(this.lexer);
|
|
|
|
tok = this.lexer.next();
|
|
if (tok.id !== TOKEN.do) {
|
|
throw new Error(Errors.foreach);
|
|
}
|
|
|
|
const [tok1, body] = this.parseExprList();
|
|
|
|
tok = tok1 || this.lexer.next();
|
|
if (tok.id !== TOKEN.endfor) {
|
|
throw new Error(Errors.foreach);
|
|
}
|
|
|
|
return [null, new ForeachDecl(identifier, params, body)];
|
|
}
|
|
}
|
|
|
|
export { Errors, Parser };
|