pdf.js/src/core/xfa/formcalc_parser.js
calixteman b5be515375
XFA - Add a lexer/parser for FormCalc language (#12936)
- 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
2021-02-17 20:28:06 +01:00

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 };