diff --git a/src/core/xfa/formcalc_lexer.js b/src/core/xfa/formcalc_lexer.js new file mode 100644 index 000000000..edafeee8a --- /dev/null +++ b/src/core/xfa/formcalc_lexer.js @@ -0,0 +1,385 @@ +/* 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. + */ + +const KEYWORDS = new Set([ + "and", + "break", + "continue", + "do", + "downto", + "else", + "elseif", + "end", + "endfor", + "endfunc", + "endif", + "endwhile", + "eq", + "exit", + "for", + "foreach", + "func", + "ge", + "gt", + "if", + "in", + "infinity", + "le", + "lt", + "nan", + "ne", + "not", + "null", + "or", + "return", + "step", + "then", + "this", + "throw", + "upto", + "var", + "while", +]); + +const TOKEN = { + /* Appears in expression */ + and: 0, + divide: 1, + dot: 2, + dotDot: 3, + dotHash: 4, + dotStar: 5, + eq: 6, + ge: 7, + gt: 8, + le: 9, + leftBracket: 10, + leftParen: 11, + lt: 12, + minus: 13, + ne: 14, + not: 15, + null: 16, + number: 17, + or: 18, + plus: 19, + rightBracket: 20, + rightParen: 21, + string: 22, + this: 23, + times: 24, + identifier: 25, // in main statments too + + /* Main statements */ + break: 26, + continue: 27, + do: 28, + for: 29, + foreach: 30, + func: 31, + if: 32, + var: 33, + while: 34, + + /* Others */ + assign: 35, + comma: 36, + downto: 37, + else: 38, + elseif: 39, + end: 40, + endif: 41, + endfor: 42, + endfunc: 43, + endwhile: 44, + eof: 45, + exit: 46, + in: 47, + infinity: 48, + nan: 49, + return: 50, + step: 51, + then: 52, + throw: 53, + upto: 54, +}; + +const hexPattern = /^[uU]([0-9a-fA-F]{4,8})/; +const numberPattern = /^[0-9]*(?:\.[0-9]*)?(?:[Ee][+-]?[0-9]+)?/; +const dotNumberPattern = /^[0-9]*(?:[Ee][+-]?[0-9]+)?/; +const eolPattern = /[\r\n]+/; +const identifierPattern = new RegExp("^[\\p{L}_$!][\\p{L}\\p{N}_$]*", "u"); + +class Token { + constructor(id, value = null) { + this.id = id; + this.value = value; + } +} + +const Singletons = (function () { + const obj = Object.create(null); + const nonSingleton = new Set([ + "identifier", + "string", + "number", + "nan", + "infinity", + ]); + for (const [name, id] of Object.entries(TOKEN)) { + if (!nonSingleton.has(name)) { + obj[name] = new Token(id); + } + } + obj.nan = new Token(TOKEN.number, NaN); + obj.infinity = new Token(TOKEN.number, Infinity); + + return obj; +})(); + +class Lexer { + constructor(data) { + this.data = data; + this.pos = 0; + this.len = data.length; + this.strBuf = []; + } + + skipUntilEOL() { + const match = this.data.slice(this.pos).match(eolPattern); + if (match) { + this.pos += match.index + match[0].length; + } else { + // No eol so consume all the chars. + this.pos = this.len; + } + } + + getIdentifier() { + this.pos--; + const match = this.data.slice(this.pos).match(identifierPattern); + if (!match) { + throw new Error( + `Invalid token in FormCalc expression at position ${this.pos}.` + ); + } + + const identifier = this.data.slice(this.pos, this.pos + match[0].length); + this.pos += match[0].length; + + const lower = identifier.toLowerCase(); + if (!KEYWORDS.has(lower)) { + return new Token(TOKEN.identifier, identifier); + } + + return Singletons[lower]; + } + + getString() { + const str = this.strBuf; + const data = this.data; + let start = this.pos; + while (this.pos < this.len) { + const char = data.charCodeAt(this.pos++); + if (char === 0x22 /* = " */) { + if (data.charCodeAt(this.pos) === 0x22 /* = " */) { + // Escaped quote. + str.push(data.slice(start, this.pos++)); + start = this.pos; + continue; + } + // End of string + break; + } + + if (char === 0x5c /* = \ */) { + const match = data.substring(this.pos, this.pos + 10).match(hexPattern); + if (!match) { + continue; + } + + str.push(data.slice(start, this.pos - 1)); + const code = match[1]; + if (code.length === 4) { + str.push(String.fromCharCode(parseInt(code, 16))); + start = this.pos += 5; + } else if (code.length !== 8) { + str.push(String.fromCharCode(parseInt(code.slice(0, 4), 16))); + start = this.pos += 5; + } else { + str.push(String.fromCharCode(parseInt(code, 16))); + start = this.pos += 9; + } + } + } + + const lastChunk = data.slice(start, this.pos - 1); + if (str.length === 0) { + return new Token(TOKEN.string, lastChunk); + } + + str.push(lastChunk); + const string = str.join(""); + str.length = 0; + + return new Token(TOKEN.string, string); + } + + getNumber(first) { + const match = this.data.substring(this.pos).match(numberPattern); + if (!match) { + return first - 0x30 /* = 0 */; + } + const number = parseFloat( + this.data.substring(this.pos - 1, this.pos + match[0].length) + ); + + this.pos += match[0].length; + return new Token(TOKEN.number, number); + } + + getCompOperator(alt1, alt2) { + if (this.data.charCodeAt(this.pos) === 0x3d /* = = */) { + this.pos++; + return alt1; + } + return alt2; + } + + getLower() { + const char = this.data.charCodeAt(this.pos); + if (char === 0x3d /* = = */) { + this.pos++; + return Singletons.le; + } + + if (char === 0x3e /* = > */) { + this.pos++; + return Singletons.ne; + } + + return Singletons.lt; + } + + getSlash() { + if (this.data.charCodeAt(this.pos) === 0x2f /* = / */) { + this.skipUntilEOL(); + return false; + } + return true; + } + + getDot() { + const char = this.data.charCodeAt(this.pos); + if (char === 0x2e /* = . */) { + this.pos++; + return Singletons.dotDot; + } + + if (char === 0x2a /* = * */) { + this.pos++; + return Singletons.dotStar; + } + + if (char === 0x23 /* = # */) { + this.pos++; + return Singletons.dotHash; + } + + if (0x30 /* = 0 */ <= char && char <= 0x39 /* = 9 */) { + this.pos++; + const match = this.data.substring(this.pos).match(dotNumberPattern); + if (!match) { + return new Token(TOKEN.number, (char - 0x30) /* = 0 */ / 10); + } + const end = this.pos + match[0].length; + const number = parseFloat(this.data.substring(this.pos - 2, end)); + this.pos = end; + return new Token(TOKEN.number, number); + } + + return Singletons.dot; + } + + next() { + while (this.pos < this.len) { + const char = this.data.charCodeAt(this.pos++); + switch (char) { + case 0x09 /* = \t */: + case 0x0a /* = \n */: + case 0x0b /* = \v */: + case 0x0c /* = \f */: + case 0x0d /* = \r */: + case 0x20 /* = */: + break; + case 0x22 /* = " */: + return this.getString(); + case 0x26 /* = & */: + return Singletons.and; + case 0x28 /* = ( */: + return Singletons.leftParen; + case 0x29 /* = ) */: + return Singletons.rightParen; + case 0x2a /* = * */: + return Singletons.times; + case 0x2b /* = + */: + return Singletons.plus; + case 0x2c /* = , */: + return Singletons.comma; + case 0x2d /* = - */: + return Singletons.minus; + case 0x2e /* = . */: + return this.getDot(); + case 0x2f /* = / */: + if (this.getSlash()) { + return Singletons.divide; + } + // It was a comment. + break; + case 0x30 /* = 0 */: + case 0x31 /* = 1 */: + case 0x32 /* = 2 */: + case 0x33 /* = 3 */: + case 0x34 /* = 4 */: + case 0x35 /* = 5 */: + case 0x36 /* = 6 */: + case 0x37 /* = 7 */: + case 0x38 /* = 8 */: + case 0x39 /* = 9 */: + return this.getNumber(char); + case 0x3b /* = ; */: + this.skipUntilEOL(); + break; + case 0x3c /* = < */: + return this.getLower(); + case 0x3d /* = = */: + return this.getCompOperator(Singletons.eq, Singletons.assign); + case 0x3e /* = > */: + return this.getCompOperator(Singletons.ge, Singletons.gt); + case 0x5b /* = [ */: + return Singletons.leftBracket; + case 0x5d /* = ] */: + return Singletons.rightBracket; + case 0x7c /* = | */: + return Singletons.or; + default: + return this.getIdentifier(); + } + } + return Singletons.eof; + } +} + +export { Lexer, Token, TOKEN }; diff --git a/src/core/xfa/formcalc_parser.js b/src/core/xfa/formcalc_parser.js new file mode 100644 index 000000000..bf9de0947 --- /dev/null +++ b/src/core/xfa/formcalc_parser.js @@ -0,0 +1,1340 @@ +/* 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 }; diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 2ccdc87ed..07a2502a5 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -39,6 +39,7 @@ "unicode_spec.js", "util_spec.js", "writer_spec.js", + "xfa_formcalc_spec.js", "xfa_parser_spec.js", "xml_spec.js" ] diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 55e6bdfe7..d54427eb8 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -85,6 +85,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/unicode_spec.js", "pdfjs-test/unit/util_spec.js", "pdfjs-test/unit/writer_spec.js", + "pdfjs-test/unit/xfa_formcalc_spec.js", "pdfjs-test/unit/xfa_parser_spec.js", "pdfjs-test/unit/xml_spec.js", ].map(function (moduleName) { diff --git a/test/unit/xfa_formcalc_spec.js b/test/unit/xfa_formcalc_spec.js new file mode 100644 index 000000000..748353425 --- /dev/null +++ b/test/unit/xfa_formcalc_spec.js @@ -0,0 +1,729 @@ +/* Copyright 2020 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 { Errors, Parser } from "../../src/core/xfa/formcalc_parser.js"; +import { Lexer, Token, TOKEN } from "../../src/core/xfa/formcalc_lexer.js"; + +describe("FormCalc expression parser", function () { + const EOF = new Token(TOKEN.eof); + + describe("FormCalc lexer", function () { + it("should lex numbers", function () { + const lexer = new Lexer( + "12 1.2345 .7 .12345 1e-2 1.2E+3 1e2 1.2E3 nan 12. 2.e3 infinity 99999999999999999 123456789.012345678 9e99999" + ); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 12)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 1.2345)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 0.7)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 0.12345)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 1e-2)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 1.2e3)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 1e2)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 1.2e3)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, NaN)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 12)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 2e3)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, Infinity)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 100000000000000000)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 123456789.01234567)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, Infinity)); + expect(lexer.next()).toEqual(EOF); + }); + + it("should lex strings", function () { + const lexer = new Lexer( + `"hello world" "hello ""world" "hello ""world"" ""world""""hello""" "hello \\uabcdeh \\Uabcd \\u00000123abc" "a \\a \\ub \\Uc \\b"` + ); + expect(lexer.next()).toEqual(new Token(TOKEN.string, `hello world`)); + expect(lexer.next()).toEqual(new Token(TOKEN.string, `hello "world`)); + expect(lexer.next()).toEqual( + new Token(TOKEN.string, `hello "world" "world""hello"`) + ); + expect(lexer.next()).toEqual( + new Token(TOKEN.string, `hello \uabcdeh \uabcd \u0123abc`) + ); + expect(lexer.next()).toEqual( + new Token(TOKEN.string, `a \\a \\ub \\Uc \\b`) + ); + expect(lexer.next()).toEqual(EOF); + }); + + it("should lex operators", function () { + const lexer = new Lexer("( , ) <= <> = == >= < > / * . .* .# [ ] & |"); + expect(lexer.next()).toEqual(new Token(TOKEN.leftParen)); + expect(lexer.next()).toEqual(new Token(TOKEN.comma)); + expect(lexer.next()).toEqual(new Token(TOKEN.rightParen)); + expect(lexer.next()).toEqual(new Token(TOKEN.le)); + expect(lexer.next()).toEqual(new Token(TOKEN.ne)); + expect(lexer.next()).toEqual(new Token(TOKEN.assign)); + expect(lexer.next()).toEqual(new Token(TOKEN.eq)); + expect(lexer.next()).toEqual(new Token(TOKEN.ge)); + expect(lexer.next()).toEqual(new Token(TOKEN.lt)); + expect(lexer.next()).toEqual(new Token(TOKEN.gt)); + expect(lexer.next()).toEqual(new Token(TOKEN.divide)); + expect(lexer.next()).toEqual(new Token(TOKEN.times)); + expect(lexer.next()).toEqual(new Token(TOKEN.dot)); + expect(lexer.next()).toEqual(new Token(TOKEN.dotStar)); + expect(lexer.next()).toEqual(new Token(TOKEN.dotHash)); + expect(lexer.next()).toEqual(new Token(TOKEN.leftBracket)); + expect(lexer.next()).toEqual(new Token(TOKEN.rightBracket)); + expect(lexer.next()).toEqual(new Token(TOKEN.and)); + expect(lexer.next()).toEqual(new Token(TOKEN.or)); + expect(lexer.next()).toEqual(EOF); + }); + + it("should skip comments", function () { + const lexer = new Lexer(` + + \t\t 1 \r\n\r\n + + ; blah blah blah + + 2 + + // blah blah blah blah blah + + + 3 + `); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 1)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 2)); + expect(lexer.next()).toEqual(new Token(TOKEN.number, 3)); + expect(lexer.next()).toEqual(EOF); + }); + + it("should lex identifiers", function () { + const lexer = new Lexer( + "eq for fore while continue hello こんにちは世界 $!hello今日は12今日は" + ); + expect(lexer.next()).toEqual(new Token(TOKEN.eq)); + expect(lexer.next()).toEqual(new Token(TOKEN.for)); + expect(lexer.next()).toEqual(new Token(TOKEN.identifier, "fore")); + expect(lexer.next()).toEqual(new Token(TOKEN.while)); + expect(lexer.next()).toEqual(new Token(TOKEN.continue)); + expect(lexer.next()).toEqual(new Token(TOKEN.identifier, "hello")); + expect(lexer.next()).toEqual( + new Token(TOKEN.identifier, "こんにちは世界") + ); + expect(lexer.next()).toEqual(new Token(TOKEN.identifier, "$")); + expect(lexer.next()).toEqual( + new Token(TOKEN.identifier, "!hello今日は12今日は") + ); + expect(lexer.next()).toEqual(EOF); + }); + }); + + describe("FormCalc parser", function () { + it("should parse basic arithmetic expression", function () { + const parser = new Parser("1 + 2 * 3"); + expect(parser.parse().dump()[0]).toEqual(7); + }); + + it("should parse basic arithmetic expression with the same operator", function () { + const parser = new Parser("1 + a + 3"); + expect(parser.parse().dump()[0]).toEqual({ + operator: "+", + left: { + operator: "+", + left: 1, + right: { id: "a" }, + }, + right: 3, + }); + }); + + it("should parse expressions with unary operators", function () { + const parser = new Parser(` + s = +x + 1 + t = -+u * 2 + t = +-u * 2 + u = -foo() + `); + expect(parser.parse().dump()).toEqual([ + { + assignment: "s", + expr: { + operator: "+", + left: { operator: "+", arg: { id: "x" } }, + right: 1, + }, + }, + { + assignment: "t", + expr: { + operator: "*", + left: { + operator: "-", + arg: { + operator: "+", + arg: { id: "u" }, + }, + }, + right: 2, + }, + }, + { + assignment: "t", + expr: { + operator: "*", + left: { + operator: "+", + arg: { + operator: "-", + arg: { id: "u" }, + }, + }, + right: 2, + }, + }, + { + assignment: "u", + expr: { + operator: "-", + arg: { + callee: { id: "foo" }, + params: [], + }, + }, + }, + ]); + }); + + it("should parse basic expression with a string", function () { + const parser = new Parser(`(5 - "abc") * 3`); + expect(parser.parse().dump()[0]).toEqual(15); + }); + + it("should parse basic expression with a calls", function () { + const parser = new Parser(`foo(2, 3, a & b) or c * d + 1.234 / e`); + expect(parser.parse().dump()[0]).toEqual({ + operator: "||", + left: { + callee: { id: "foo" }, + params: [ + 2, + 3, + { + operator: "&&", + left: { id: "a" }, + right: { id: "b" }, + }, + ], + }, + right: { + operator: "+", + left: { + operator: "*", + left: { id: "c" }, + right: { id: "d" }, + }, + right: { + operator: "/", + left: 1.234, + right: { id: "e" }, + }, + }, + }); + }); + + it("should parse basic expression with a subscript", function () { + let parser = new Parser(`こんにちは世界[-0]`); + let dump = parser.parse().dump()[0]; + expect(dump).toEqual({ + operand: { id: "こんにちは世界" }, + index: -0, + }); + expect(Object.is(-0, dump.index)).toBe(true); + + parser = new Parser(`こんにちは世界[+0]`); + dump = parser.parse().dump()[0]; + expect(dump).toEqual({ + operand: { id: "こんにちは世界" }, + index: +0, + }); + expect(Object.is(+0, dump.index)).toBe(true); + + parser = new Parser(`こんにちは世界[*]`); + expect(parser.parse().dump()[0]).toEqual({ + operand: { id: "こんにちは世界" }, + index: { special: "*" }, + }); + }); + + it("should parse basic expression with dots", function () { + const parser = new Parser("a.b.c.#d..e.f..g.*"); + expect(parser.parse().dump()[0]).toEqual({ + operator: ".", + left: { id: "a" }, + right: { + operator: ".", + left: { id: "b" }, + right: { + operator: ".#", + left: { id: "c" }, + right: { + operator: "..", + left: { id: "d" }, + right: { + operator: ".", + left: { id: "e" }, + right: { + operator: "..", + left: { id: "f" }, + right: { + operator: ".", + left: { id: "g" }, + right: { special: "*" }, + }, + }, + }, + }, + }, + }, + }); + }); + + it("should parse var declaration with error", function () { + let parser = new Parser("var 123 = a"); + expect(() => parser.parse()).toThrow(new Error(Errors.var)); + + parser = new Parser(`var "123" = a`); + expect(() => parser.parse()).toThrow(new Error(Errors.var)); + + parser = new Parser(`var for var a`); + expect(() => parser.parse()).toThrow(new Error(Errors.var)); + }); + + it("should parse for declaration with a step", function () { + const parser = new Parser(` +var s = 0 +for var i = 1 upto 10 + x step 1 do + s = s + i * 2 +endfor`); + expect(parser.parse().dump()).toEqual([ + { + var: "s", + expr: 0, + }, + { + decl: "for", + assignment: { + var: "i", + expr: 1, + }, + type: "upto", + end: { + operator: "+", + left: 10, + right: { id: "x" }, + }, + step: 1, + body: [ + { + assignment: "s", + expr: { + operator: "+", + left: { id: "s" }, + right: { + operator: "*", + left: { id: "i" }, + right: 2, + }, + }, + }, + ], + }, + ]); + }); + + it("should parse for declaration without a step", function () { + const parser = new Parser(` +for i = 1 + 2 downto 10 do + s = foo() +endfor`); + expect(parser.parse().dump()).toEqual([ + { + decl: "for", + assignment: { + assignment: "i", + expr: 3, + }, + type: "downto", + end: 10, + step: null, + body: [ + { + assignment: "s", + expr: { + callee: { id: "foo" }, + params: [], + }, + }, + ], + }, + ]); + }); + + it("should parse for declaration with error", function () { + let parser = new Parser("for 123 = i upto 1 do a = 1 endfor"); + expect(() => parser.parse()).toThrow(new Error(Errors.assignment)); + + parser = new Parser("for var 123 = i upto 1 do a = 1 endfor"); + expect(() => parser.parse()).toThrow(new Error(Errors.assignment)); + + parser = new Parser("for var i = 123 upt 1 do a = 1 endfor"); + expect(() => parser.parse()).toThrow(new Error(Errors.for)); + + parser = new Parser("for var i = 123 var 1 do a = 1 endfor"); + expect(() => parser.parse()).toThrow(new Error(Errors.for)); + + parser = new Parser( + "for var i = 123 upto 1 step for var j = 1 do endfor do a = 1 endfor" + ); + expect(() => parser.parse()).toThrow(new Error(Errors.for)); + + parser = new Parser("for var i = 123 downto 1 do a = 1 endfunc"); + expect(() => parser.parse()).toThrow(new Error(Errors.for)); + + parser = new Parser("for var i = 123 downto 1 do a = 1"); + expect(() => parser.parse()).toThrow(new Error(Errors.for)); + }); + + it("should parse foreach declaration", function () { + const parser = new Parser(` +foreach i in (a, b, c, d) do + s = foo()[i] +endfor`); + expect(parser.parse().dump()).toEqual([ + { + decl: "foreach", + id: "i", + params: [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }], + body: [ + { + assignment: "s", + expr: { + operand: { + callee: { id: "foo" }, + params: [], + }, + index: { id: "i" }, + }, + }, + ], + }, + ]); + }); + + it("should parse foreach declaration with error", function () { + let parser = new Parser("foreach 123 in (1, 2, 3) do a = 1 endfor"); + expect(() => parser.parse()).toThrow(new Error(Errors.foreach)); + + parser = new Parser("foreach foo in 1, 2, 3) do a = 1 endfor"); + expect(() => parser.parse()).toThrow(new Error(Errors.foreach)); + + parser = new Parser("foreach foo in (1, 2, 3 do a = 1 endfor"); + expect(() => parser.parse()).toThrow(new Error(Errors.params)); + + parser = new Parser("foreach foo in (1, 2 3) do a = 1 endfor"); + expect(() => parser.parse()).toThrow(new Error(Errors.params)); + + parser = new Parser("foreach foo in (1, 2, 3) od a = 1 endfor"); + expect(() => parser.parse()).toThrow(new Error(Errors.foreach)); + + parser = new Parser("foreach foo in (1, 2, 3) do a = 1 endforeach"); + expect(() => parser.parse()).toThrow(new Error(Errors.foreach)); + + parser = new Parser("foreach foo in (1, 2, 3) do a = 1 123"); + expect(() => parser.parse()).toThrow(new Error(Errors.foreach)); + }); + + it("should parse while declaration", function () { + const parser = new Parser(` +while (1) do + if (0) then + break + else + continue + endif +endwhile + `); + expect(parser.parse().dump()).toEqual([ + { + decl: "while", + condition: 1, + body: [ + { + decl: "if", + condition: 0, + then: [{ special: "break" }], + elseif: null, + else: [{ special: "continue" }], + }, + ], + }, + ]); + }); + + it("should parse while declaration with error", function () { + let parser = new Parser("while a == 1 do a = 2 endwhile"); + expect(() => parser.parse()).toThrow(new Error(Errors.while)); + + parser = new Parser("while (a == 1 do a = 2 endwhile"); + expect(() => parser.parse()).toThrow(new Error(Errors.while)); + + parser = new Parser("while (a == 1) var a = 2 endwhile"); + expect(() => parser.parse()).toThrow(new Error(Errors.while)); + + parser = new Parser("while (a == 1) do var a = 2 end"); + expect(() => parser.parse()).toThrow(new Error(Errors.while)); + }); + + it("should parse do declaration", function () { + const parser = new Parser(` +do + x = 1 +; a comment in the middle of the block + y = 2 +end + `); + expect(parser.parse().dump()).toEqual([ + { + decl: "block", + body: [ + { + assignment: "x", + expr: 1, + }, + { + assignment: "y", + expr: 2, + }, + ], + }, + ]); + }); + + it("should parse do declaration with error", function () { + const parser = new Parser(` +do + x = 1 + y = 2 +endfunc + `); + expect(() => parser.parse()).toThrow(new Error(Errors.block)); + }); + + it("should parse func declaration", function () { + const parser = new Parser(` +func こんにちは世界123(a, b) do + a + b +endfunc + `); + expect(parser.parse().dump()).toEqual([ + { + func: "こんにちは世界123", + params: ["a", "b"], + body: [ + { + operator: "+", + left: { id: "a" }, + right: { id: "b" }, + }, + ], + }, + ]); + }); + + it("should parse func declaration with error", function () { + let parser = new Parser("func 123(a, b) do a = 1 endfunc"); + expect(() => parser.parse()).toThrow(new Error(Errors.func)); + + parser = new Parser("func foo(a, b) for a = 1 endfunc"); + expect(() => parser.parse()).toThrow(new Error(Errors.func)); + + parser = new Parser("func foo(a, b) do a = 1 endfun"); + expect(() => parser.parse()).toThrow(new Error(Errors.func)); + + parser = new Parser("func foo(a, b, c do a = 1 endfunc"); + expect(() => parser.parse()).toThrow(new Error(Errors.func)); + + parser = new Parser("func foo(a, b, 123) do a = 1 endfunc"); + expect(() => parser.parse()).toThrow(new Error(Errors.func)); + }); + + it("should parse if declaration", function () { + const parser = new Parser(` + if (a & b) then + var s = 1 + endif + + if (a or b) then + var s = 1 + else + var x = 2 + endif + + if (0) then + s = 1 + elseif (1) then + s = 2 + elseif (2) then + s = 3 + elseif (3) then + s = 4 + else + s = 5 + endif + +// a comment + + if (0) then + s = 1 + elseif (1) then + s = 2 + endif + `); + expect(parser.parse().dump()).toEqual([ + { + decl: "if", + condition: { + operator: "&&", + left: { id: "a" }, + right: { id: "b" }, + }, + then: [ + { + var: "s", + expr: 1, + }, + ], + elseif: null, + else: null, + }, + { + decl: "if", + condition: { + operator: "||", + left: { id: "a" }, + right: { id: "b" }, + }, + then: [ + { + var: "s", + expr: 1, + }, + ], + elseif: null, + else: [ + { + var: "x", + expr: 2, + }, + ], + }, + { + decl: "if", + condition: 0, + then: [ + { + assignment: "s", + expr: 1, + }, + ], + elseif: [ + { + decl: "elseif", + condition: 1, + then: [ + { + assignment: "s", + expr: 2, + }, + ], + }, + { + decl: "elseif", + condition: 2, + then: [ + { + assignment: "s", + expr: 3, + }, + ], + }, + { + decl: "elseif", + condition: 3, + then: [ + { + assignment: "s", + expr: 4, + }, + ], + }, + ], + else: [ + { + assignment: "s", + expr: 5, + }, + ], + }, + { + decl: "if", + condition: 0, + then: [ + { + assignment: "s", + expr: 1, + }, + ], + elseif: [ + { + decl: "elseif", + condition: 1, + then: [ + { + assignment: "s", + expr: 2, + }, + ], + }, + ], + else: null, + }, + ]); + }); + + it("should parse if declaration with error", function () { + let parser = new Parser("if foo == 1 then a = 1 endif"); + expect(() => parser.parse()).toThrow(new Error(Errors.if)); + + parser = new Parser("if (foo == 1 then a = 1 endif"); + expect(() => parser.parse()).toThrow(new Error(Errors.if)); + + parser = new Parser( + "if (foo == 1) then a = 1 elseiff (foo == 2) then a = 2 endif" + ); + expect(() => parser.parse()).toThrow(new Error(Errors.if)); + + parser = new Parser( + "if (foo == 1) then a = 1 elseif (foo == 2) then a = 2 end" + ); + expect(() => parser.parse()).toThrow(new Error(Errors.if)); + }); + }); +});