Adv item search: rework entire parser to fix parsing issues
This commit is contained in:
parent
9c5877c061
commit
1693432462
5 changed files with 1389 additions and 459 deletions
818
expr_parser.js
Normal file
818
expr_parser.js
Normal file
|
@ -0,0 +1,818 @@
|
||||||
|
/**
|
||||||
|
* See `expr_parser.md` for notes on the implementation of this parser!
|
||||||
|
*/
|
||||||
|
const ExprParser = (function() {
|
||||||
|
// buffer containing a list of tokens and a movable pointer into the list
|
||||||
|
class TokenBuf {
|
||||||
|
constructor(tokens) {
|
||||||
|
this.tokens = tokens;
|
||||||
|
this.index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get peek() {
|
||||||
|
return this.tokens[this.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
consume(termName) {
|
||||||
|
if (this.peek.type !== termName) {
|
||||||
|
throw new Error(`Expected a ${termName}, but got a ${this.peek.type}!`)
|
||||||
|
}
|
||||||
|
const node = { type: 'term', token: this.peek };
|
||||||
|
++this.index;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenize an expression string
|
||||||
|
function tokenize(exprStr) {
|
||||||
|
exprStr = exprStr.trim();
|
||||||
|
const tokens = [];
|
||||||
|
let col = 0;
|
||||||
|
|
||||||
|
function pushSymbol(sym) {
|
||||||
|
tokens.push({ type: sym });
|
||||||
|
col += sym.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (col < exprStr.length) {
|
||||||
|
// parse fixed symbols, like operators and stuff
|
||||||
|
switch (exprStr[col]) {
|
||||||
|
case '(':
|
||||||
|
case ')':
|
||||||
|
case ',':
|
||||||
|
case '&':
|
||||||
|
case '|':
|
||||||
|
case '+':
|
||||||
|
case '-':
|
||||||
|
case '*':
|
||||||
|
case '/':
|
||||||
|
case '^':
|
||||||
|
case '=':
|
||||||
|
pushSymbol(exprStr[col]);
|
||||||
|
continue;
|
||||||
|
case '>':
|
||||||
|
pushSymbol(exprStr[col + 1] === '=' ? '>=' : '>');
|
||||||
|
continue;
|
||||||
|
case '<':
|
||||||
|
pushSymbol(exprStr[col + 1] === '=' ? '<=' : '<');
|
||||||
|
continue;
|
||||||
|
case '!':
|
||||||
|
pushSymbol(exprStr[col + 1] === '=' ? '!=' : '!');
|
||||||
|
continue;
|
||||||
|
case ' ': // ignore extra whitespace
|
||||||
|
++col;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (exprStr.slice(col, col + 2) === "?=") {
|
||||||
|
pushSymbol("?=");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// parse a numeric literal
|
||||||
|
let m;
|
||||||
|
if ((m = /^\d+(?:\.\d*)?/.exec(exprStr.substring(col))) !== null) {
|
||||||
|
tokens.push({ type: 'nLit', value: parseFloat(m[0]) });
|
||||||
|
col += m[0].length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// parse a string literal
|
||||||
|
if ((m = /^"([^"]+)"/.exec(exprStr.substring(col))) !== null) { // with double-quotes
|
||||||
|
tokens.push({ type: 'sLit', value: m[1] });
|
||||||
|
col += m[0].length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((m = /^'([^']+)'/.exec(exprStr.substring(col))) !== null) { // with single-quotes
|
||||||
|
tokens.push({ type: 'sLit', value: m[1] });
|
||||||
|
col += m[0].length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// parse an identifier or boolean literal
|
||||||
|
if ((m = /^\w[\w\d%]*/.exec(exprStr.substring(col))) !== null) {
|
||||||
|
switch (m[0]) {
|
||||||
|
case 'true':
|
||||||
|
tokens.push({ type: 'bLit', value: true });
|
||||||
|
col += 4;
|
||||||
|
continue;
|
||||||
|
case 'false':
|
||||||
|
tokens.push({ type: 'bLit', value: false });
|
||||||
|
col += 5;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tokens.push({ type: 'ident', id: m[0] });
|
||||||
|
col += m[0].length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// if we reach here without successfully parsing a token, it's an error
|
||||||
|
throw new Error(`Could not parse character "${exprStr[col]}" at position ${col}`);
|
||||||
|
}
|
||||||
|
tokens.push({ type: 'eof' });
|
||||||
|
return new TokenBuf(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse an AST from a sequence of tokens
|
||||||
|
function takeExpr(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeConj(tokens));
|
||||||
|
return { type: 'nonterm', name: 'expr', prod: 0, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse expr!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeExprList(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeExpr(tokens));
|
||||||
|
children.push(takeExprList0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'exprList', prod: 0, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse exprList!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeExprList0(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case ')':
|
||||||
|
return { type: 'nonterm', name: 'exprList\'', prod: 1, children };
|
||||||
|
case ',':
|
||||||
|
children.push(tokens.consume(','));
|
||||||
|
children.push(takeExpr(tokens));
|
||||||
|
children.push(takeExprList0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'exprList\'', prod: 0, children };
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'exprList\'', prod: 1, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse exprList\'!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeConj(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeDisj(tokens));
|
||||||
|
children.push(takeConj0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'conj', prod: 0, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse conj!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeConj0(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '&':
|
||||||
|
children.push(tokens.consume('&'));
|
||||||
|
children.push(takeDisj(tokens));
|
||||||
|
children.push(takeConj0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'conj\'', prod: 0, children };
|
||||||
|
case ')':
|
||||||
|
case ',':
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'conj\'', prod: 1, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse conj\'!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeDisj(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeCmpEq(tokens));
|
||||||
|
children.push(takeDisj0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'disj', prod: 0, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse disj!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeDisj0(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '&':
|
||||||
|
case ')':
|
||||||
|
case ',':
|
||||||
|
return { type: 'nonterm', name: 'disj\'', prod: 1, children };
|
||||||
|
case '|':
|
||||||
|
children.push(tokens.consume('|'));
|
||||||
|
children.push(takeCmpEq(tokens));
|
||||||
|
children.push(takeDisj0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'disj\'', prod: 0, children };
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'disj\'', prod: 1, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse disj\'!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeCmpEq(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeCmpRel(tokens));
|
||||||
|
children.push(takeCmpEq0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'cmpEq', prod: 0, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse cmpEq!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeCmpEq0(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!=':
|
||||||
|
children.push(tokens.consume('!='));
|
||||||
|
children.push(takeCmpRel(tokens));
|
||||||
|
children.push(takeCmpEq0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'cmpEq\'', prod: 1, children };
|
||||||
|
case '&':
|
||||||
|
case ')':
|
||||||
|
case ',':
|
||||||
|
return { type: 'nonterm', name: 'cmpEq\'', prod: 3, children };
|
||||||
|
case '=':
|
||||||
|
children.push(tokens.consume('='));
|
||||||
|
children.push(takeCmpRel(tokens));
|
||||||
|
children.push(takeCmpEq0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'cmpEq\'', prod: 0, children };
|
||||||
|
case '?=':
|
||||||
|
children.push(tokens.consume('?='));
|
||||||
|
children.push(takeCmpRel(tokens));
|
||||||
|
children.push(takeCmpEq0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'cmpEq\'', prod: 2, children };
|
||||||
|
case '|':
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'cmpEq\'', prod: 3, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse cmpEq\'!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeCmpRel(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeSum(tokens));
|
||||||
|
children.push(takeCmpRel0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'cmpRel', prod: 0, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse cmpRel!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeCmpRel0(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!=':
|
||||||
|
case '&':
|
||||||
|
case ')':
|
||||||
|
case ',':
|
||||||
|
return { type: 'nonterm', name: 'cmpRel\'', prod: 4, children };
|
||||||
|
case '<':
|
||||||
|
children.push(tokens.consume('<'));
|
||||||
|
children.push(takeSum(tokens));
|
||||||
|
children.push(takeCmpRel0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'cmpRel\'', prod: 1, children };
|
||||||
|
case '<=':
|
||||||
|
children.push(tokens.consume('<='));
|
||||||
|
children.push(takeSum(tokens));
|
||||||
|
children.push(takeCmpRel0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'cmpRel\'', prod: 0, children };
|
||||||
|
case '=':
|
||||||
|
return { type: 'nonterm', name: 'cmpRel\'', prod: 4, children };
|
||||||
|
case '>':
|
||||||
|
children.push(tokens.consume('>'));
|
||||||
|
children.push(takeSum(tokens));
|
||||||
|
children.push(takeCmpRel0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'cmpRel\'', prod: 2, children };
|
||||||
|
case '>=':
|
||||||
|
children.push(tokens.consume('>='));
|
||||||
|
children.push(takeSum(tokens));
|
||||||
|
children.push(takeCmpRel0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'cmpRel\'', prod: 3, children };
|
||||||
|
case '?=':
|
||||||
|
case '|':
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'cmpRel\'', prod: 4, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse cmpRel\'!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeSum(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeProd(tokens));
|
||||||
|
children.push(takeSum0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'sum', prod: 0, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse sum!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeSum0(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!=':
|
||||||
|
case '&':
|
||||||
|
case ')':
|
||||||
|
return { type: 'nonterm', name: 'sum\'', prod: 2, children };
|
||||||
|
case '+':
|
||||||
|
children.push(tokens.consume('+'));
|
||||||
|
children.push(takeProd(tokens));
|
||||||
|
children.push(takeSum0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'sum\'', prod: 0, children };
|
||||||
|
case ',':
|
||||||
|
return { type: 'nonterm', name: 'sum\'', prod: 2, children };
|
||||||
|
case '-':
|
||||||
|
children.push(tokens.consume('-'));
|
||||||
|
children.push(takeProd(tokens));
|
||||||
|
children.push(takeSum0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'sum\'', prod: 1, children };
|
||||||
|
case '<':
|
||||||
|
case '<=':
|
||||||
|
case '=':
|
||||||
|
case '>':
|
||||||
|
case '>=':
|
||||||
|
case '?=':
|
||||||
|
case '|':
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'sum\'', prod: 2, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse sum\'!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeProd(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeExp(tokens));
|
||||||
|
children.push(takeProd0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'prod', prod: 0, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse prod!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeProd0(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!=':
|
||||||
|
case '&':
|
||||||
|
case ')':
|
||||||
|
return { type: 'nonterm', name: 'prod\'', prod: 2, children };
|
||||||
|
case '*':
|
||||||
|
children.push(tokens.consume('*'));
|
||||||
|
children.push(takeExp(tokens));
|
||||||
|
children.push(takeProd0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'prod\'', prod: 0, children };
|
||||||
|
case '+':
|
||||||
|
case ',':
|
||||||
|
case '-':
|
||||||
|
return { type: 'nonterm', name: 'prod\'', prod: 2, children };
|
||||||
|
case '/':
|
||||||
|
children.push(tokens.consume('/'));
|
||||||
|
children.push(takeExp(tokens));
|
||||||
|
children.push(takeProd0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'prod\'', prod: 1, children };
|
||||||
|
case '<':
|
||||||
|
case '<=':
|
||||||
|
case '=':
|
||||||
|
case '>':
|
||||||
|
case '>=':
|
||||||
|
case '?=':
|
||||||
|
case '|':
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'prod\'', prod: 2, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse prod\'!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeExp(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeUnary(tokens));
|
||||||
|
children.push(takeExp0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'exp', prod: 0, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse exp!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeExp0(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!=':
|
||||||
|
case '&':
|
||||||
|
case ')':
|
||||||
|
case '*':
|
||||||
|
case '+':
|
||||||
|
case ',':
|
||||||
|
case '-':
|
||||||
|
case '/':
|
||||||
|
case '<':
|
||||||
|
case '<=':
|
||||||
|
case '=':
|
||||||
|
case '>':
|
||||||
|
case '>=':
|
||||||
|
case '?=':
|
||||||
|
return { type: 'nonterm', name: 'exp\'', prod: 1, children };
|
||||||
|
case '^':
|
||||||
|
children.push(tokens.consume('^'));
|
||||||
|
children.push(takeUnary(tokens));
|
||||||
|
children.push(takeExp0(tokens));
|
||||||
|
return { type: 'nonterm', name: 'exp\'', prod: 0, children };
|
||||||
|
case '|':
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'exp\'', prod: 1, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse exp\'!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeUnary(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
children.push(tokens.consume('!'));
|
||||||
|
children.push(takeUnary(tokens));
|
||||||
|
return { type: 'nonterm', name: 'unary', prod: 1, children };
|
||||||
|
case '(':
|
||||||
|
children.push(takePrim(tokens));
|
||||||
|
return { type: 'nonterm', name: 'unary', prod: 2, children };
|
||||||
|
case '-':
|
||||||
|
children.push(tokens.consume('-'));
|
||||||
|
children.push(takeUnary(tokens));
|
||||||
|
return { type: 'nonterm', name: 'unary', prod: 0, children };
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takePrim(tokens));
|
||||||
|
return { type: 'nonterm', name: 'unary', prod: 2, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse unary!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takePrim(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '(':
|
||||||
|
children.push(tokens.consume('('));
|
||||||
|
children.push(takeExpr(tokens));
|
||||||
|
children.push(tokens.consume(')'));
|
||||||
|
return { type: 'nonterm', name: 'prim', prod: 4, children };
|
||||||
|
case 'bLit':
|
||||||
|
children.push(tokens.consume('bLit'));
|
||||||
|
return { type: 'nonterm', name: 'prim', prod: 1, children };
|
||||||
|
case 'ident':
|
||||||
|
children.push(tokens.consume('ident'));
|
||||||
|
children.push(takeIdentTail(tokens));
|
||||||
|
return { type: 'nonterm', name: 'prim', prod: 3, children };
|
||||||
|
case 'nLit':
|
||||||
|
children.push(tokens.consume('nLit'));
|
||||||
|
return { type: 'nonterm', name: 'prim', prod: 0, children };
|
||||||
|
case 'sLit':
|
||||||
|
children.push(tokens.consume('sLit'));
|
||||||
|
return { type: 'nonterm', name: 'prim', prod: 2, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse prim!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeIdentTail(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!=':
|
||||||
|
case '&':
|
||||||
|
return { type: 'nonterm', name: 'identTail', prod: 1, children };
|
||||||
|
case '(':
|
||||||
|
children.push(tokens.consume('('));
|
||||||
|
children.push(takeArgs(tokens));
|
||||||
|
children.push(tokens.consume(')'));
|
||||||
|
return { type: 'nonterm', name: 'identTail', prod: 0, children };
|
||||||
|
case ')':
|
||||||
|
case '*':
|
||||||
|
case '+':
|
||||||
|
case ',':
|
||||||
|
case '-':
|
||||||
|
case '/':
|
||||||
|
case '<':
|
||||||
|
case '<=':
|
||||||
|
case '=':
|
||||||
|
case '>':
|
||||||
|
case '>=':
|
||||||
|
case '?=':
|
||||||
|
case '^':
|
||||||
|
case '|':
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'identTail', prod: 1, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse identTail!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeArgs(tokens) {
|
||||||
|
const children = [];
|
||||||
|
switch (tokens.peek.type) {
|
||||||
|
case '!':
|
||||||
|
case '(':
|
||||||
|
children.push(takeExprList(tokens));
|
||||||
|
return { type: 'nonterm', name: 'args', prod: 0, children };
|
||||||
|
case ')':
|
||||||
|
return { type: 'nonterm', name: 'args', prod: 1, children };
|
||||||
|
case '-':
|
||||||
|
case 'bLit':
|
||||||
|
case 'ident':
|
||||||
|
case 'nLit':
|
||||||
|
case 'sLit':
|
||||||
|
children.push(takeExprList(tokens));
|
||||||
|
return { type: 'nonterm', name: 'args', prod: 0, children };
|
||||||
|
case 'eof':
|
||||||
|
return { type: 'nonterm', name: 'args', prod: 1, children };
|
||||||
|
default:
|
||||||
|
throw new Error('Could not parse args!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply tree transformations to recover the nominal-grammar AST
|
||||||
|
function fixUp(ast) {
|
||||||
|
if (ast.type === 'term') {
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
switch (ast.name) {
|
||||||
|
case 'exprList': // recover left-recursive structures
|
||||||
|
case 'conj':
|
||||||
|
case 'disj':
|
||||||
|
case 'exp':
|
||||||
|
return fixUpRightRecurse(ast, 1);
|
||||||
|
case 'sum':
|
||||||
|
case 'prod':
|
||||||
|
return fixUpRightRecurse(ast, 2);
|
||||||
|
case 'cmpEq':
|
||||||
|
return fixUpRightRecurse(ast, 3);
|
||||||
|
case 'cmpRel':
|
||||||
|
return fixUpRightRecurse(ast, 4);
|
||||||
|
case 'prim': // recover left-factored identifier things
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 3:
|
||||||
|
switch (ast.children[1].prod) {
|
||||||
|
case 0: // function call
|
||||||
|
return {
|
||||||
|
type: 'nonterm', name: 'prim', prod: 3,
|
||||||
|
children: [fixUp(ast.children[0]), ...mapFixUp(ast.children[1].children)]
|
||||||
|
};
|
||||||
|
case 1: // just an identifier
|
||||||
|
return { type: 'nonterm', name: 'prim', prod: 4, children: [fixUp(ast.children[0])] };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
return { ...ast, prod: 5, children: mapFixUp(ast.children) };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return { ...ast, children: mapFixUp(ast.children) };
|
||||||
|
}
|
||||||
|
function mapFixUp(nodes) {
|
||||||
|
return nodes.map(chAst => fixUp(chAst));
|
||||||
|
}
|
||||||
|
function fixUpRightRecurse(ast, maxProd) {
|
||||||
|
let nomAst = { type: 'nonterm', name: ast.name, prod: maxProd, children: [fixUp(ast.children[0])] };
|
||||||
|
let tree = ast.children[1];
|
||||||
|
while (tree.prod < maxProd) {
|
||||||
|
nomAst = {
|
||||||
|
type: 'nonterm', name: ast.name, prod: tree.prod,
|
||||||
|
children: [nomAst, ...mapFixUp(tree.children.slice(0, tree.children.length - 1))]
|
||||||
|
};
|
||||||
|
tree = tree.children[tree.children.length - 1];
|
||||||
|
}
|
||||||
|
return nomAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile nominal AST into an item builder expression function
|
||||||
|
function translate(ast, builtInProps, builtInFuncs) {
|
||||||
|
function trans(ast) {
|
||||||
|
return translate(ast, builtInProps, builtInFuncs);
|
||||||
|
}
|
||||||
|
if (ast.type === 'term') {
|
||||||
|
switch (ast.token.type) {
|
||||||
|
case 'nLit':
|
||||||
|
return ast.token.value;
|
||||||
|
case 'bLit':
|
||||||
|
return ast.token.value;
|
||||||
|
case 'sLit':
|
||||||
|
return ast.token.value;
|
||||||
|
case 'ident':
|
||||||
|
return ast.token.value;
|
||||||
|
}
|
||||||
|
} else if (ast.type === 'nonterm') {
|
||||||
|
switch (ast.name) {
|
||||||
|
case 'expr':
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
case 'exprList':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return [...ast.children[0], trans(ast.children[2])];
|
||||||
|
case 1:
|
||||||
|
return [trans(ast.children[0])];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'conj':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return new ConjTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 1:
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'disj':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return new DisjTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 1:
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'cmpEq':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return new EqTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 1:
|
||||||
|
return new NeqTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 2:
|
||||||
|
return new ContainsTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 3:
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'cmpRel':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return new LeqTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 1:
|
||||||
|
return new LtTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 2:
|
||||||
|
return new GtTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 3:
|
||||||
|
return new GeqTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 4:
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'sum':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return new AddTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 1:
|
||||||
|
return new SubTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 2:
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'prod':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return new MulTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 1:
|
||||||
|
return new DivTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 2:
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'exp':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return new ExpTerm(trans(ast.children[0]), trans(ast.children[2]));
|
||||||
|
case 1:
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'unary':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return new NegTerm(trans(ast.children[1]));
|
||||||
|
case 1:
|
||||||
|
return new InvTerm(trans(ast.children[1]));
|
||||||
|
case 2:
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'prim':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return new NumLitTerm(ast.children[0].token.value);
|
||||||
|
case 1:
|
||||||
|
return new BoolLitTerm(ast.children[0].token.value);
|
||||||
|
case 2:
|
||||||
|
return new StrLitTerm(ast.children[0].token.value);
|
||||||
|
case 3:
|
||||||
|
const fn = builtInFuncs[ast.children[0].token.id];
|
||||||
|
if (!fn) {
|
||||||
|
throw new Error(`Unknown function: ${ast.children[0].token.id}`);
|
||||||
|
}
|
||||||
|
return new FnCallTerm(fn, trans(ast.children[2]));
|
||||||
|
case 4: {
|
||||||
|
const prop = builtInProps[ast.children[0].token.id];
|
||||||
|
if (!prop) {
|
||||||
|
throw new Error(`Unknown property: ${ast.children[0].token.id}`);
|
||||||
|
}
|
||||||
|
return new PropTerm(prop);
|
||||||
|
}
|
||||||
|
case 5:
|
||||||
|
return trans(ast.children[1]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'args':
|
||||||
|
switch (ast.prod) {
|
||||||
|
case 0:
|
||||||
|
return trans(ast.children[0]);
|
||||||
|
case 1:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return class ExprParser {
|
||||||
|
constructor(builtInProps, builtInFuncs) {
|
||||||
|
this.builtInProps = builtInProps;
|
||||||
|
this.builtInFuncs = builtInFuncs;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(exprStr) {
|
||||||
|
const tokens = tokenize(exprStr);
|
||||||
|
if (tokens.tokens.length <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const transAst = takeExpr(tokens, 0);
|
||||||
|
if (tokens.peek.type !== 'eof') {
|
||||||
|
throw new Error('Could not parse entire expression!');
|
||||||
|
}
|
||||||
|
const nomAst = fixUp(transAst);
|
||||||
|
return translate(nomAst, this.builtInProps, this.builtInFuncs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
197
expr_parser.md
Normal file
197
expr_parser.md
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
This file contains notes on how the parser implemented in `expr_parser.js` is designed.
|
||||||
|
The nominal grammar is as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
expr := conj
|
||||||
|
|
||||||
|
exprList := exprList "," expr
|
||||||
|
| expr
|
||||||
|
|
||||||
|
conj := conj "&" disj
|
||||||
|
| disj
|
||||||
|
|
||||||
|
disj := disj "|" cmpEq
|
||||||
|
| cmpEq
|
||||||
|
|
||||||
|
cmpEq := cmpEq "=" cmpRel
|
||||||
|
| cmpEq "!=" cmpRel
|
||||||
|
| cmpEq "?=" cmpRel
|
||||||
|
| cmpRel
|
||||||
|
|
||||||
|
cmpRel := cmpRel "<=" sum
|
||||||
|
| cmpRel "<" sum
|
||||||
|
| cmpRel ">" sum
|
||||||
|
| cmpRel ">=" sum
|
||||||
|
| sum
|
||||||
|
|
||||||
|
sum := sum "+" prod
|
||||||
|
| sum "-" prod
|
||||||
|
| prod
|
||||||
|
|
||||||
|
prod := prod "*" exp
|
||||||
|
| prod "/" exp
|
||||||
|
| exp
|
||||||
|
|
||||||
|
exp := exp "^" unary
|
||||||
|
| unary
|
||||||
|
|
||||||
|
unary := "-" unary
|
||||||
|
| "!" unary
|
||||||
|
| prim
|
||||||
|
|
||||||
|
prim := nLit
|
||||||
|
| bLit
|
||||||
|
| sLit
|
||||||
|
| ident "(" args ")"
|
||||||
|
| ident
|
||||||
|
| "(" expr ")"
|
||||||
|
|
||||||
|
args := exprList
|
||||||
|
| ε
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to eliminate left-recursion, the above grammar is transformed into the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
expr := conj
|
||||||
|
|
||||||
|
exprList := expr exprListTail
|
||||||
|
|
||||||
|
exprList' := "," expr exprList'
|
||||||
|
| ε
|
||||||
|
|
||||||
|
conj := disj conj'
|
||||||
|
|
||||||
|
conj' := "&" disj conj'
|
||||||
|
| ε
|
||||||
|
|
||||||
|
disj := cmpEq disj'
|
||||||
|
|
||||||
|
disj' := "|" cmpEq disj'
|
||||||
|
| ε
|
||||||
|
|
||||||
|
cmpEq := cmpRel cmpEq'
|
||||||
|
|
||||||
|
cmpEq' := "=" cmpRel cmpEq'
|
||||||
|
| "!=" cmpRel cmpEq'
|
||||||
|
| "?=" cmpRel cmpEq'
|
||||||
|
| ε
|
||||||
|
|
||||||
|
cmpRel := sum cmpRel'
|
||||||
|
|
||||||
|
cmpRel' := "<=" sum cmpRel'
|
||||||
|
| "<" sum cmpRel'
|
||||||
|
| ">" sum cmpRel'
|
||||||
|
| ">=" sum cmpRel'
|
||||||
|
| ε
|
||||||
|
|
||||||
|
sum := prod sum'
|
||||||
|
|
||||||
|
sum' := "+" prod sum'
|
||||||
|
| "-" prod sum'
|
||||||
|
| ε
|
||||||
|
|
||||||
|
prod := exp prod'
|
||||||
|
|
||||||
|
prod' := "*" exp prod'
|
||||||
|
| "/" exp prod'
|
||||||
|
| ε
|
||||||
|
|
||||||
|
exp := unary exp'
|
||||||
|
|
||||||
|
exp' := "^" unary exp'
|
||||||
|
| ε
|
||||||
|
|
||||||
|
unary := "-" unary
|
||||||
|
| "!" unary
|
||||||
|
| prim
|
||||||
|
|
||||||
|
prim := nLit
|
||||||
|
| bLit
|
||||||
|
| sLit
|
||||||
|
| ident identTail
|
||||||
|
| "(" expr ")"
|
||||||
|
|
||||||
|
identTail := "(" args ")"
|
||||||
|
| ε
|
||||||
|
|
||||||
|
args := exprList
|
||||||
|
| ε
|
||||||
|
```
|
||||||
|
|
||||||
|
The parser itself is a recursive-descent LL(1) parser.
|
||||||
|
To build the parsing rules, the following `FIRST` and `FOLLOW` sets are computed for the above grammar:
|
||||||
|
|
||||||
|
Nonterminal | FIRST
|
||||||
|
----------- | -----
|
||||||
|
`expr` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`exprList` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`exprList'` | `","`, `ε`
|
||||||
|
`conj` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`conj'` | `"&"`, `ε`
|
||||||
|
`disj` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`disj'` | `"\|"`, `ε`
|
||||||
|
`cmpEq` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`cmpEq'` | `"!="`, `"="`, `"?="`, `ε`
|
||||||
|
`cmpRel` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`cmpRel'` | `"<"`, `"<="`, `">"`, `">="`, `ε`
|
||||||
|
`sum` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`sum'` | `"+"`, `"-"`, `ε`
|
||||||
|
`prod` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`prod'` | `"*"`, `"/"`, `ε`
|
||||||
|
`exp` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`exp'` | `"^"`, `ε`
|
||||||
|
`unary` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`prim` | `"("`, `bLit`, `ident`, `nLit`, `sLit`
|
||||||
|
`identTail` | `"("`, `ε`
|
||||||
|
`args` | `"!"`, `"("`, `"-"`, `bLit`, `ident`, `nLit`, `sLit`, `ε`
|
||||||
|
|
||||||
|
Nonterminal | FOLLOW
|
||||||
|
----------- | ------
|
||||||
|
`expr` | `")"`, `","`, `$`
|
||||||
|
`exprList` | `")"`
|
||||||
|
`exprList'` | `")"`
|
||||||
|
`conj` | `")"`, `","`
|
||||||
|
`conj'` | `")"`, `","`
|
||||||
|
`disj` | `"&"`, `")"`, `","`
|
||||||
|
`disj'` | `"&"`, `")"`, `","`
|
||||||
|
`cmpEq` | `"&"`, `")"`, `","`, `"\|"`
|
||||||
|
`cmpEq'` | `"&"`, `")"`, `","`, `"\|"`
|
||||||
|
`cmpRel` | `"!="`, `"&"`, `")"`, `","`, `"="`, `"?="`, `"\|"`
|
||||||
|
`cmpRel'` | `"!="`, `"&"`, `")"`, `","`, `"="`, `"?="`, `"\|"`
|
||||||
|
`sum` | `"!="`, `"&"`, `")"`, `","`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
|
||||||
|
`sum'` | `"!="`, `"&"`, `")"`, `","`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
|
||||||
|
`prod` | `"!="`, `"&"`, `")"`, `"+"`, `","`, `"-"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
|
||||||
|
`prod'` | `"!="`, `"&"`, `")"`, `"+"`, `","`, `"-"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
|
||||||
|
`exp` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
|
||||||
|
`exp'` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"\|"`
|
||||||
|
`unary` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"^"`, `"\|"`
|
||||||
|
`prim` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"^"`, `"\|"`
|
||||||
|
`identTail` | `"!="`, `"&"`, `")"`, `"*"`, `"+"`, `","`, `"-"`, `"/"`, `"<"`, `"<="`, `"="`, `">"`, `">="`, `"?="`, `"^"`, `"\|"`
|
||||||
|
`args` | `")"`
|
||||||
|
|
||||||
|
Then, the parsing rule table is as follows, where each table entry refers to the production number for the given nonterminal:
|
||||||
|
|
||||||
|
Nonterminal | "!" | "!=" | "&" | "(" | ")" | "*" | "+" | "," | "-" | "/" | "<" | "<=" | "=" | ">" | ">=" | "?=" | "^" | "|" | bLit | ident | nLit | sLit | $
|
||||||
|
----------- | --- | ---- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---- | --- | --- | ---- | ---- | --- | --- | ---- | ----- | ---- | ---- | ---
|
||||||
|
expr | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
|
||||||
|
exprList | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
|
||||||
|
exprList' | - | - | - | - | 1 | - | - | 0 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | 1
|
||||||
|
conj | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
|
||||||
|
conj' | - | - | 0 | - | 1 | - | - | 1 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | 1
|
||||||
|
disj | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
|
||||||
|
disj' | - | - | 1 | - | 1 | - | - | 1 | - | - | - | - | - | - | - | - | - | 0 | - | - | - | - | 1
|
||||||
|
cmpEq | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
|
||||||
|
cmpEq' | - | 1 | 3 | - | 3 | - | - | 3 | - | - | - | - | 0 | - | - | 2 | - | 3 | - | - | - | - | 3
|
||||||
|
cmpRel | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
|
||||||
|
cmpRel' | - | 4 | 4 | - | 4 | - | - | 4 | - | - | 1 | 0 | 4 | 2 | 3 | 4 | - | 4 | - | - | - | - | 4
|
||||||
|
sum | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
|
||||||
|
sum' | - | 2 | 2 | - | 2 | - | 0 | 2 | 1 | - | 2 | 2 | 2 | 2 | 2 | 2 | - | 2 | - | - | - | - | 2
|
||||||
|
prod | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
|
||||||
|
prod' | - | 2 | 2 | - | 2 | 0 | 2 | 2 | 2 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | - | 2 | - | - | - | - | 2
|
||||||
|
exp | 0 | - | - | 0 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | -
|
||||||
|
exp' | - | 1 | 1 | - | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | - | - | - | - | 1
|
||||||
|
unary | 1 | - | - | 2 | - | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 2 | 2 | 2 | 2 | -
|
||||||
|
prim | - | - | - | 4 | - | - | - | - | - | - | - | - | - | - | - | - | - | - | 1 | 3 | 0 | 2 | -
|
||||||
|
identTail | - | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | - | - | - | - | 1
|
||||||
|
args | 0 | - | - | 0 | 1 | - | - | - | 0 | - | - | - | - | - | - | - | - | - | 0 | 0 | 0 | 0 | 1
|
|
@ -78,6 +78,7 @@
|
||||||
<script type="text/javascript" src="damage_calc.js"></script>
|
<script type="text/javascript" src="damage_calc.js"></script>
|
||||||
<script type="text/javascript" src="display.js"></script>
|
<script type="text/javascript" src="display.js"></script>
|
||||||
<script type="text/javascript" src="query_2.js"></script>
|
<script type="text/javascript" src="query_2.js"></script>
|
||||||
|
<script type="text/javascript" src="expr_parser.js"></script>
|
||||||
<script type="text/javascript" src="load.js"></script>
|
<script type="text/javascript" src="load.js"></script>
|
||||||
<script type="text/javascript" src="items_2.js"></script>
|
<script type="text/javascript" src="items_2.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
23
items_2.js
23
items_2.js
|
@ -21,6 +21,7 @@ class ExprField {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorText.innerText = e.message;
|
this.errorText.innerText = e.message;
|
||||||
this.output = null;
|
this.output = null;
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -38,6 +39,8 @@ function compareLexico(ia, keysA, ib, keysB) {
|
||||||
if (aKey > bKey) return 1;
|
if (aKey > bKey) return 1;
|
||||||
break;
|
break;
|
||||||
case 'number': // sort numeric stuff in reverse order
|
case 'number': // sort numeric stuff in reverse order
|
||||||
|
aKey = isNaN(aKey) ? 0 : aKey;
|
||||||
|
bKey = isNaN(bKey) ? 0 : bKey;
|
||||||
if (aKey < bKey) return 1;
|
if (aKey < bKey) return 1;
|
||||||
if (aKey > bKey) return -1;
|
if (aKey > bKey) return -1;
|
||||||
break;
|
break;
|
||||||
|
@ -74,17 +77,23 @@ function init() {
|
||||||
itemEntries.push(itemElem);
|
itemEntries.push(itemElem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create the expression parser
|
||||||
|
const exprParser = new ExprParser(itemQueryProps, itemQueryFuncs);
|
||||||
|
|
||||||
// the two search query input boxes
|
// the two search query input boxes
|
||||||
const searchFilterField = new ExprField('search-filter-field', 'search-filter-error', function(exprStr) {
|
const searchFilterField = new ExprField('search-filter-field', 'search-filter-error', function(exprStr) {
|
||||||
const expr = compileQueryExpr(exprStr);
|
const expr = exprParser.parse(exprStr);
|
||||||
return expr !== null ? expr : (i, ie) => true;
|
return expr !== null ? expr : new BoolLitTerm(true);
|
||||||
});
|
});
|
||||||
const searchSortField = new ExprField('search-sort-field', 'search-sort-error', function(exprStr) {
|
const searchSortField = new ExprField('search-sort-field', 'search-sort-error', function(exprStr) {
|
||||||
const subExprs = exprStr.split(';').map(compileQueryExpr).filter(f => f != null);
|
const subExprs = exprStr.split(';').map(e => exprParser.parse(e)).filter(f => f != null);
|
||||||
return function(i, ie) {
|
return {
|
||||||
|
type: 'array',
|
||||||
|
resolve(i, ie) {
|
||||||
const sortKeys = [];
|
const sortKeys = [];
|
||||||
for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k](i, ie));
|
for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k].resolve(i, ie));
|
||||||
return sortKeys;
|
return sortKeys;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,8 +117,8 @@ function init() {
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < searchDb.length; i++) {
|
for (let i = 0; i < searchDb.length; i++) {
|
||||||
const item = searchDb[i][0], itemExp = searchDb[i][1];
|
const item = searchDb[i][0], itemExp = searchDb[i][1];
|
||||||
if (checkBool(searchFilterField.output(item, itemExp))) {
|
if (checkBool(searchFilterField.output.resolve(item, itemExp))) {
|
||||||
searchResults.push({ item, itemExp, sortKeys: searchSortField.output(item, itemExp) });
|
searchResults.push({ item, itemExp, sortKeys: searchSortField.output.resolve(item, itemExp) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
711
query_2.js
711
query_2.js
|
@ -1,61 +1,4 @@
|
||||||
/*
|
// dynamic type casts
|
||||||
* disj := conj "|" disj
|
|
||||||
* | conj
|
|
||||||
*
|
|
||||||
* conj := cmp "&" conj
|
|
||||||
* | cmpEq
|
|
||||||
*
|
|
||||||
* cmpEq := cmpRel "=" cmpEq
|
|
||||||
* | cmpRel "?=" prim
|
|
||||||
* | cmpRel "!=" cmpEq
|
|
||||||
*
|
|
||||||
* cmpRel := sum "<=" cmpRel
|
|
||||||
* | sum "<" cmpRel
|
|
||||||
* | sum ">" cmpRel
|
|
||||||
* | sum ">=" cmpRel
|
|
||||||
* | sum
|
|
||||||
*
|
|
||||||
* sum := prod "+" sum
|
|
||||||
* | prod "-" sum
|
|
||||||
* | prod
|
|
||||||
*
|
|
||||||
* prod := exp "*" prod
|
|
||||||
* | exp "/" prod
|
|
||||||
* | exp
|
|
||||||
*
|
|
||||||
* exp := unary "^" exp
|
|
||||||
* | unary
|
|
||||||
*
|
|
||||||
* unary := "-" unary
|
|
||||||
* | "!" unary
|
|
||||||
* | prim
|
|
||||||
*
|
|
||||||
* prim := nLit
|
|
||||||
* | bLit
|
|
||||||
* | sLit
|
|
||||||
* | ident "(" [disj ["," disj...]] ")"
|
|
||||||
* | ident
|
|
||||||
* | "(" disj ")"
|
|
||||||
*/
|
|
||||||
|
|
||||||
// a list of tokens indexed by a single pointer
|
|
||||||
class TokenList {
|
|
||||||
constructor(tokens) {
|
|
||||||
this.tokens = tokens;
|
|
||||||
this.ptr = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get here() {
|
|
||||||
if (this.ptr >= this.tokens.length) throw new Error('Reached end of expression');
|
|
||||||
return this.tokens[this.ptr];
|
|
||||||
}
|
|
||||||
|
|
||||||
advance(steps = 1) {
|
|
||||||
this.ptr = Math.min(this.ptr + steps, this.tokens.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// type casts
|
|
||||||
function checkBool(v) {
|
function checkBool(v) {
|
||||||
if (typeof v !== 'boolean') throw new Error(`Expected boolean, but got ${typeof v}`);
|
if (typeof v !== 'boolean') throw new Error(`Expected boolean, but got ${typeof v}`);
|
||||||
return v;
|
return v;
|
||||||
|
@ -71,67 +14,79 @@ function checkStr(v) {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkComparable(v) {
|
||||||
|
if (typeof v === 'boolean') throw new Error('Boolean is not comparable');
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
// properties of items that can be looked up
|
// properties of items that can be looked up
|
||||||
|
// each entry is a function `(item, extended item) -> value`
|
||||||
const itemQueryProps = (function() {
|
const itemQueryProps = (function() {
|
||||||
const props = {};
|
const props = {};
|
||||||
function prop(names, getProp) {
|
|
||||||
|
function prop(names, type, resolve) {
|
||||||
if (Array.isArray(names)) {
|
if (Array.isArray(names)) {
|
||||||
for (name of names) {
|
for (name of names) {
|
||||||
props[name] = getProp;
|
props[name] = { type, resolve };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
props[names] = getProp;
|
props[names] = { type, resolve };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function maxId(names, idKey) {
|
function maxId(names, idKey) {
|
||||||
prop(names, (i, ie) => ie.get('maxRolls').get(idKey) || 0);
|
prop(names, 'number', (i, ie) => ie.get('maxRolls').get(idKey) || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function minId(names, idKey) {
|
function minId(names, idKey) {
|
||||||
prop(names, (i, ie) => ie.get('minRolls').get(idKey) || 0);
|
prop(names, 'number', (i, ie) => ie.get('minRolls').get(idKey) || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rangeAvg(names, getProp) {
|
function rangeAvg(names, getProp) {
|
||||||
prop(names, (i, ie) => {
|
prop(names, 'number', (i, ie) => {
|
||||||
const range = getProp(i, ie);
|
const range = getProp(i, ie);
|
||||||
if (!range) return 0;
|
if (!range) return 0;
|
||||||
const ndx = range.indexOf('-');
|
const ndx = range.indexOf('-');
|
||||||
return (parseInt(range.substring(0, ndx), 10) + parseInt(range.substring(ndx + 1), 10)) / 2;
|
return (parseInt(range.substring(0, ndx), 10) + parseInt(range.substring(ndx + 1), 10)) / 2;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function map(names, comps, f) {
|
|
||||||
return prop(names, (i, ie) => {
|
function map(names, comps, outType, f) {
|
||||||
|
return prop(names, outType, (i, ie) => {
|
||||||
const args = [];
|
const args = [];
|
||||||
for (let k = 0; k < comps.length; k++) args.push(comps[k](i, ie));
|
for (let k = 0; k < comps.length; k++) args.push(comps[k](i, ie));
|
||||||
return f.apply(null, args);
|
return f.apply(null, args);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sum(names, ...comps) {
|
function sum(names, ...comps) {
|
||||||
return map(names, comps, (...summands) => {
|
return map(names, comps, 'number', (...summands) => {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (let i = 0; i < summands.length; i++) total += summands[i];
|
for (let i = 0; i < summands.length; i++) total += summands[i];
|
||||||
return total;
|
return total;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
prop('name', (i, ie) => i.displayName || i.name);
|
prop('name', 'string', (i, ie) => i.displayName || i.name);
|
||||||
prop('type', (i, ie) => i.type);
|
prop('type', 'string', (i, ie) => i.type);
|
||||||
prop(['cat', 'category'], (i, ie) => i.category);
|
prop(['cat', 'category'], 'string', (i, ie) => i.category);
|
||||||
const tierIndices = { Normal: 0, Unique: 1, Set: 2, Rare: 3, Legendary: 4, Fabled: 5, Mythic: 6 };
|
const tierIndices = { Normal: 0, Unique: 1, Set: 2, Rare: 3, Legendary: 4, Fabled: 5, Mythic: 6 };
|
||||||
prop(['rarityname', 'raritystr', 'tiername', 'tierstr'], (i, ie) => i.tier);
|
prop(['rarityname', 'raritystr', 'tiername', 'tierstr'], 'string', (i, ie) => i.tier);
|
||||||
prop(['rarity', 'tier'], (i, ie) => tierIndices[i.tier]);
|
prop(['rarity', 'tier'], 'string', (i, ie) => tierIndices[i.tier]);
|
||||||
|
|
||||||
prop(['level', 'lvl', 'combatlevel', 'combatlvl'], (i, ie) => i.lvl);
|
prop(['level', 'lvl', 'combatlevel', 'combatlvl'], 'number', (i, ie) => i.lvl);
|
||||||
prop(['strmin', 'strreq'], (i, ie) => i.strReq);
|
prop(['strmin', 'strreq'], 'number', (i, ie) => i.strReq);
|
||||||
prop(['dexmin', 'dexreq'], (i, ie) => i.dexReq);
|
prop(['dexmin', 'dexreq'], 'number', (i, ie) => i.dexReq);
|
||||||
prop(['intmin', 'intreq'], (i, ie) => i.intReq);
|
prop(['intmin', 'intreq'], 'number', (i, ie) => i.intReq);
|
||||||
prop(['defmin', 'defreq'], (i, ie) => i.defReq);
|
prop(['defmin', 'defreq'], 'number', (i, ie) => i.defReq);
|
||||||
prop(['agimin', 'agireq'], (i, ie) => i.agiReq);
|
prop(['agimin', 'agireq'], 'number', (i, ie) => i.agiReq);
|
||||||
sum(['summin', 'sumreq', 'totalmin', 'totalreq'], props.strmin, props.dexmin, props.intmin, props.defmin, props.agimin);
|
sum(['summin', 'sumreq', 'totalmin', 'totalreq'], props.strmin, props.dexmin, props.intmin, props.defmin, props.agimin);
|
||||||
|
|
||||||
prop('str', (i, ie) => i.str);
|
prop('str', 'number', (i, ie) => i.str);
|
||||||
prop('dex', (i, ie) => i.dex);
|
prop('dex', 'number', (i, ie) => i.dex);
|
||||||
prop('int', (i, ie) => i.int);
|
prop('int', 'number', (i, ie) => i.int);
|
||||||
prop('def', (i, ie) => i.def);
|
prop('def', 'number', (i, ie) => i.def);
|
||||||
prop('agi', (i, ie) => i.agi);
|
prop('agi', 'number', (i, ie) => i.agi);
|
||||||
sum(['skillpoints', 'skillpts', 'attributes', 'attrs'], props.str, props.dex, props.int, props.def, props.agi);
|
sum(['skillpoints', 'skillpts', 'attributes', 'attrs'], props.str, props.dex, props.int, props.def, props.agi);
|
||||||
|
|
||||||
rangeAvg(['neutraldmg', 'neutraldam', 'ndmg', 'ndam'], (i, ie) => i.nDam);
|
rangeAvg(['neutraldmg', 'neutraldam', 'ndmg', 'ndam'], (i, ie) => i.nDam);
|
||||||
|
@ -155,15 +110,15 @@ const itemQueryProps = (function() {
|
||||||
maxId(['spellrawdmg', 'spellrawdam', 'spellneutraldmg', 'spellneutraldam', 'sdraw'], 'sdRaw');
|
maxId(['spellrawdmg', 'spellrawdam', 'spellneutraldmg', 'spellneutraldam', 'sdraw'], 'sdRaw');
|
||||||
|
|
||||||
const atkSpdIndices = { SUPER_SLOW: -3, VERY_SLOW: -2, SLOW: -1, NORMAL: 0, FAST: 1, VERY_FAST: 2, SUPER_FAST: 3 };
|
const atkSpdIndices = { SUPER_SLOW: -3, VERY_SLOW: -2, SLOW: -1, NORMAL: 0, FAST: 1, VERY_FAST: 2, SUPER_FAST: 3 };
|
||||||
prop(['attackspeed', 'atkspd'], (i, ie) => i.atkSpd ? atkSpdIndices[i.atkSpd] : 0);
|
prop(['attackspeed', 'atkspd'], 'string', (i, ie) => i.atkSpd ? atkSpdIndices[i.atkSpd] : 0);
|
||||||
maxId(['bonusattackspeed', 'bonusatkspd', 'attackspeedid', 'atkspdid', 'atktier'], 'atkTier');
|
maxId(['bonusattackspeed', 'bonusatkspd', 'attackspeedid', 'atkspdid', 'atktier'], 'atkTier');
|
||||||
sum(['sumattackspeed', 'totalattackspeed', 'sumatkspd', 'totalatkspd', 'sumatktier', 'totalatktier'], props.atkspd, props.atktier);
|
sum(['sumattackspeed', 'totalattackspeed', 'sumatkspd', 'totalatkspd', 'sumatktier', 'totalatktier'], props.atkspd, props.atktier);
|
||||||
|
|
||||||
prop(['earthdef', 'edef'], (i, ie) => i.eDef || 0);
|
prop(['earthdef', 'edef'], 'number', (i, ie) => i.eDef || 0);
|
||||||
prop(['thunderdef', 'tdef'], (i, ie) => i.tDef || 0);
|
prop(['thunderdef', 'tdef'], 'number', (i, ie) => i.tDef || 0);
|
||||||
prop(['waterdef', 'wdef'], (i, ie) => i.wDef || 0);
|
prop(['waterdef', 'wdef'], 'number', (i, ie) => i.wDef || 0);
|
||||||
prop(['firedef', 'fdef'], (i, ie) => i.fDef || 0);
|
prop(['firedef', 'fdef'], 'number', (i, ie) => i.fDef || 0);
|
||||||
prop(['airdef', 'adef'], (i, ie) => i.aDef || 0);
|
prop(['airdef', 'adef'], 'number', (i, ie) => i.aDef || 0);
|
||||||
sum(['sumdef', 'totaldef'], props.edef, props.tdef, props.wdef, props.fdef, props.adef);
|
sum(['sumdef', 'totaldef'], props.edef, props.tdef, props.wdef, props.fdef, props.adef);
|
||||||
|
|
||||||
maxId(['earthdef%', 'edef%', 'edefpct'], 'eDefPct');
|
maxId(['earthdef%', 'edef%', 'edefpct'], 'eDefPct');
|
||||||
|
@ -173,7 +128,7 @@ const itemQueryProps = (function() {
|
||||||
maxId(['airdef%', 'adef%', 'adefpct'], 'aDefPct');
|
maxId(['airdef%', 'adef%', 'adefpct'], 'aDefPct');
|
||||||
sum(['sumdef%', 'totaldef%', 'sumdefpct', 'totaldefpct'], props.edefpct, props.tdefpct, props.wdefpct, props.fdefpct, props.adefpct);
|
sum(['sumdef%', 'totaldef%', 'sumdefpct', 'totaldefpct'], props.edefpct, props.tdefpct, props.wdefpct, props.fdefpct, props.adefpct);
|
||||||
|
|
||||||
prop(['health', 'hp'], (i, ie) => i.hp || 0);
|
prop(['health', 'hp'], 'number', (i, ie) => i.hp || 0);
|
||||||
maxId(['bonushealth', 'healthid', 'bonushp', 'hpid', 'hpbonus'], 'hpBonus');
|
maxId(['bonushealth', 'healthid', 'bonushp', 'hpid', 'hpbonus'], 'hpBonus');
|
||||||
sum(['sumhealth', 'sumhp', 'totalhealth', 'totalhp'], props.hp, props.hpid);
|
sum(['sumhealth', 'sumhp', 'totalhealth', 'totalhp'], props.hp, props.hpid);
|
||||||
|
|
||||||
|
@ -207,397 +162,347 @@ const itemQueryProps = (function() {
|
||||||
maxId(['lootbonus', 'lb'], 'lb');
|
maxId(['lootbonus', 'lb'], 'lb');
|
||||||
maxId(['xpbonus', 'xpb', 'xb'], 'xpb');
|
maxId(['xpbonus', 'xpb', 'xb'], 'xpb');
|
||||||
maxId(['stealing', 'esteal'], 'eSteal');
|
maxId(['stealing', 'esteal'], 'eSteal');
|
||||||
prop(['powderslots', 'powders', 'slots', 'sockets'], (i, ie) => i.slots || 0);
|
prop(['powderslots', 'powders', 'slots', 'sockets'], 'number', (i, ie) => i.slots || 0);
|
||||||
|
|
||||||
return props;
|
return props;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// functions that can be called in query expressions
|
// functions that can be called in query expressions
|
||||||
const itemQueryFuncs = {
|
const itemQueryFuncs = {
|
||||||
max(args) {
|
max: {
|
||||||
|
type: 'number',
|
||||||
|
fn: function(args) {
|
||||||
if (args.length < 1) throw new Error('Not enough args to max()');
|
if (args.length < 1) throw new Error('Not enough args to max()');
|
||||||
let runningMax = -Infinity;
|
let runningMax = -Infinity;
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
if (checkNum(args[i]) > runningMax) runningMax = args[i];
|
if (checkNum(args[i]) > runningMax) runningMax = args[i];
|
||||||
}
|
}
|
||||||
return runningMax;
|
return runningMax;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
min(args) {
|
min: {
|
||||||
|
type: 'number',
|
||||||
|
fn: function(args) {
|
||||||
if (args.length < 1) throw new Error('Not enough args to min()');
|
if (args.length < 1) throw new Error('Not enough args to min()');
|
||||||
let runningMin = Infinity;
|
let runningMin = Infinity;
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
if (checkNum(args[i]) < runningMin) runningMin = args[i];
|
if (checkNum(args[i]) < runningMin) runningMin = args[i];
|
||||||
}
|
}
|
||||||
return runningMin;
|
return runningMin;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
floor(args) {
|
floor: {
|
||||||
|
type: 'number',
|
||||||
|
fn: function(args) {
|
||||||
if (args.length < 1) throw new Error('Not enough args to floor()');
|
if (args.length < 1) throw new Error('Not enough args to floor()');
|
||||||
return Math.floor(checkNum(args[0]));
|
return Math.floor(checkNum(args[0]));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
ceil(args) {
|
ceil: {
|
||||||
|
type: 'number',
|
||||||
|
fn: function(args) {
|
||||||
if (args.length < 1) throw new Error('Not enough args to ceil()');
|
if (args.length < 1) throw new Error('Not enough args to ceil()');
|
||||||
return Math.ceil(checkNum(args[0]));
|
return Math.ceil(checkNum(args[0]));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
round(args) {
|
round: {
|
||||||
if (args.length < 1) throw new Error('Not enough args to ceil()');
|
type: 'number',
|
||||||
|
fn: function(args) {
|
||||||
|
if (args.length < 1) throw new Error('Not enough args to round()');
|
||||||
return Math.round(checkNum(args[0]));
|
return Math.round(checkNum(args[0]));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
sqrt(args) {
|
sqrt: {
|
||||||
if (args.length < 1) throw new Error('Not enough args to ceil()');
|
type: 'number',
|
||||||
|
fn: function(args) {
|
||||||
|
if (args.length < 1) throw new Error('Not enough args to sqrt()');
|
||||||
return Math.sqrt(checkNum(args[0]));
|
return Math.sqrt(checkNum(args[0]));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
abs(args) {
|
abs: {
|
||||||
if (args.length < 1) throw new Error('Not enough args to ceil()');
|
type: 'number',
|
||||||
|
fn: function(args) {
|
||||||
|
if (args.length < 1) throw new Error('Not enough args to abs()');
|
||||||
return Math.abs(checkNum(args[0]));
|
return Math.abs(checkNum(args[0]));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
contains(args) {
|
contains: {
|
||||||
|
type: 'boolean',
|
||||||
|
fn: function(args) {
|
||||||
if (args.length < 2) throw new Error('Not enough args to contains()');
|
if (args.length < 2) throw new Error('Not enough args to contains()');
|
||||||
return checkStr(args[0]).toLowerCase().includes(checkStr(args[1]).toLowerCase());
|
return checkStr(args[0]).toLowerCase().includes(checkStr(args[1]).toLowerCase());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
atkspdmod(args) {
|
atkspdmod: {
|
||||||
|
type: 'number',
|
||||||
|
fn: function(args) {
|
||||||
if (args.length < 1) throw new Error('Not enough args to atkSpdMod()');
|
if (args.length < 1) throw new Error('Not enough args to atkSpdMod()');
|
||||||
switch (checkNum(args[0])) {
|
switch (checkNum(args[0])) {
|
||||||
case 2: return 3.1;
|
case 2:
|
||||||
case 1: return 2.5;
|
return 3.1;
|
||||||
case 0: return 2.05;
|
case 1:
|
||||||
case -1: return 1.5;
|
return 2.5;
|
||||||
case -2: return 0.83;
|
case 0:
|
||||||
|
return 2.05;
|
||||||
|
case -1:
|
||||||
|
return 1.5;
|
||||||
|
case -2:
|
||||||
|
return 0.83;
|
||||||
}
|
}
|
||||||
if (args[0] <= -3) return 0.51;
|
if (args[0] <= -3) return 0.51;
|
||||||
if (args[0] >= 3) return 4.3;
|
if (args[0] >= 3) return 4.3;
|
||||||
throw new Error('Invalid argument to atkSpdMod()');
|
throw new Error('Invalid argument to atkSpdMod()');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// the compiler itself
|
// static type check
|
||||||
const compileQueryExpr = (function() {
|
function staticCheck(expType, term) {
|
||||||
// tokenize an expression string
|
if (expType === 'any' || expType === term.type) {
|
||||||
function tokenize(exprStr) {
|
return true;
|
||||||
exprStr = exprStr.trim();
|
|
||||||
const tokens = [];
|
|
||||||
let col = 0;
|
|
||||||
function pushSymbol(sym) {
|
|
||||||
tokens.push({ type: 'sym', sym });
|
|
||||||
col += sym.length;
|
|
||||||
}
|
}
|
||||||
while (col < exprStr.length) {
|
throw new Error(`Expected ${expType}, but got ${term.type}`);
|
||||||
// parse fixed symbols, like operators and stuff
|
}
|
||||||
switch (exprStr[col]) {
|
|
||||||
case '(':
|
// expression terms
|
||||||
case ')':
|
class Term {
|
||||||
case ',':
|
constructor(type) {
|
||||||
case '&':
|
this.type = type;
|
||||||
case '|':
|
|
||||||
case '+':
|
|
||||||
case '-':
|
|
||||||
case '*':
|
|
||||||
case '/':
|
|
||||||
case '^':
|
|
||||||
case '=':
|
|
||||||
pushSymbol(exprStr[col]);
|
|
||||||
continue;
|
|
||||||
case '>':
|
|
||||||
pushSymbol(exprStr[col + 1] === '=' ? '>=' : '>');
|
|
||||||
continue;
|
|
||||||
case '<':
|
|
||||||
pushSymbol(exprStr[col + 1] === '=' ? '<=' : '<');
|
|
||||||
continue;
|
|
||||||
case '!':
|
|
||||||
pushSymbol(exprStr[col + 1] === '=' ? '!=' : '!');
|
|
||||||
continue;
|
|
||||||
case ' ': // ignore extra whitespace
|
|
||||||
++col;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (exprStr.slice(col, col+2) === "?=") {
|
|
||||||
pushSymbol("?=");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// parse a numeric literal
|
|
||||||
let m;
|
|
||||||
if ((m = /^\d+(?:\.\d*)?/.exec(exprStr.substring(col))) !== null) {
|
|
||||||
tokens.push({ type: 'num', value: parseFloat(m[0]) });
|
|
||||||
col += m[0].length;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// parse a string literal
|
|
||||||
if ((m = /^"([^"]+)"/.exec(exprStr.substring(col))) !== null) { // with double-quotes
|
|
||||||
tokens.push({ type: 'str', value: m[1] });
|
|
||||||
col += m[0].length;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ((m = /^'([^']+)'/.exec(exprStr.substring(col))) !== null) { // with single-quotes
|
|
||||||
tokens.push({ type: 'str', value: m[1] });
|
|
||||||
col += m[0].length;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// parse an identifier or boolean literal
|
|
||||||
if ((m = /^\w[\w\d%]*/.exec(exprStr.substring(col))) !== null) {
|
|
||||||
switch (m[0]) {
|
|
||||||
case 'true':
|
|
||||||
tokens.push({ type: 'bool', value: true });
|
|
||||||
col += 4;
|
|
||||||
continue;
|
|
||||||
case 'false':
|
|
||||||
tokens.push({ type: 'bool', value: false });
|
|
||||||
col += 5;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tokens.push({ type: 'id', id: m[0] });
|
|
||||||
col += m[0].length;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// if we reach here without successfully parsing a token, it's an error
|
|
||||||
throw new Error(`Could not parse character "${exprStr[col]}" at position ${col}`);
|
|
||||||
}
|
|
||||||
tokens.push({ type: 'eof' });
|
|
||||||
return new TokenList(tokens);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse tokens into an ast
|
resolve(item, itemExt) {
|
||||||
function takeDisj(tokens) {
|
throw new Error('Abstract method!');
|
||||||
const left = takeConj(tokens);
|
|
||||||
if (tokens.here.type === 'sym' && tokens.here.sym === '|') {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeDisj(tokens);
|
|
||||||
return (i, ie) => checkBool(left(i, ie)) || checkBool(right(i, ie));
|
|
||||||
}
|
}
|
||||||
return left;
|
}
|
||||||
|
|
||||||
|
class LiteralTerm extends Term {
|
||||||
|
constructor(type, value) {
|
||||||
|
super(type);
|
||||||
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function takeConj(tokens) {
|
resolve(item, itemExt) {
|
||||||
const left = takeCmpEq(tokens);
|
return this.value;
|
||||||
if (tokens.here.type === 'sym' && tokens.here.sym === '&') {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeConj(tokens);
|
|
||||||
return (i, ie) => checkBool(left(i, ie)) && checkBool(right(i, ie));
|
|
||||||
}
|
}
|
||||||
return left;
|
}
|
||||||
|
|
||||||
|
class BoolLitTerm extends LiteralTerm {
|
||||||
|
constructor(value) {
|
||||||
|
super('boolean', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumLitTerm extends LiteralTerm {
|
||||||
|
constructor(value) {
|
||||||
|
super('number', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StrLitTerm extends LiteralTerm {
|
||||||
|
constructor(value) {
|
||||||
|
super('string', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BinaryOpTerm extends Term {
|
||||||
|
constructor(type, leftType, left, rightType, right) {
|
||||||
|
super(type);
|
||||||
|
staticCheck(leftType, left);
|
||||||
|
staticCheck(rightType, right);
|
||||||
|
this.left = left;
|
||||||
|
this.right = right;
|
||||||
}
|
}
|
||||||
|
|
||||||
function takeCmpEq(tokens) {
|
resolve(item, itemExt) {
|
||||||
const left = takeCmpRel(tokens);
|
return this.apply(this.left.resolve(item, itemExt), this.right.resolve(item, itemExt));
|
||||||
if (tokens.here.type === 'sym') {
|
}
|
||||||
switch (tokens.here.sym) {
|
|
||||||
case '=': {
|
apply(a, b) {
|
||||||
tokens.advance();
|
throw new Error('Abstract method!');
|
||||||
const right = takeCmpEq(tokens);
|
}
|
||||||
return (i, ie) => {
|
}
|
||||||
const a = left(i, ie), b = right(i, ie);
|
|
||||||
if (typeof a !== typeof b) return false;
|
class LogicalTerm extends BinaryOpTerm {
|
||||||
switch (typeof a) {
|
constructor(left, right) {
|
||||||
case 'number':
|
super('boolean', 'boolean', left, 'boolean', right);
|
||||||
return Math.abs(left(i, ie) - right(i, ie)) < 1e-4;
|
}
|
||||||
case 'boolean':
|
}
|
||||||
|
|
||||||
|
class ConjTerm extends LogicalTerm {
|
||||||
|
apply(a, b) {
|
||||||
|
return a && b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DisjTerm extends LogicalTerm {
|
||||||
|
apply(a, b) {
|
||||||
|
return a || b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EqualityTerm extends BinaryOpTerm {
|
||||||
|
constructor(left, right) {
|
||||||
|
super('boolean', 'any', left, 'any', right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EqTerm extends EqualityTerm {
|
||||||
|
apply(a, b) {
|
||||||
return a === b;
|
return a === b;
|
||||||
case 'string':
|
|
||||||
return a.toLowerCase() === b.toLowerCase();
|
|
||||||
}
|
}
|
||||||
throw new Error('???'); // wut
|
}
|
||||||
};
|
|
||||||
}
|
class NeqTerm extends EqualityTerm {
|
||||||
case '!=': {
|
apply(a, b) {
|
||||||
tokens.advance();
|
|
||||||
const right = takeCmpEq(tokens);
|
|
||||||
return (i, ie) => {
|
|
||||||
const a = left(i, ie), b = right(i, ie);
|
|
||||||
if (typeof a !== typeof b) return false;
|
|
||||||
switch (typeof a) {
|
|
||||||
case 'number':
|
|
||||||
return Math.abs(left(i, ie) - right(i, ie)) >= 1e-4;
|
|
||||||
case 'boolean':
|
|
||||||
return a !== b;
|
return a !== b;
|
||||||
case 'string':
|
|
||||||
return a.toLowerCase() !== b.toLowerCase();
|
|
||||||
}
|
}
|
||||||
throw new Error('???'); // wtf
|
}
|
||||||
};
|
|
||||||
|
class ContainsTerm extends BinaryOpTerm {
|
||||||
|
constructor(left, right) {
|
||||||
|
super('boolean', 'string', left, 'string', right);
|
||||||
}
|
}
|
||||||
case '?=': {
|
|
||||||
tokens.advance();
|
apply(a, b) {
|
||||||
const right = takePrim(tokens);
|
|
||||||
return (i, ie) => {
|
|
||||||
const a = left(i, ie), b = right(i, ie);
|
|
||||||
if (typeof a !== typeof b) return false;
|
|
||||||
switch (typeof a) {
|
|
||||||
case 'number':
|
|
||||||
return Math.abs(left(i, ie) - right(i, ie)) < 1e-4;
|
|
||||||
case 'boolean':
|
|
||||||
return a === b;
|
|
||||||
case 'string':
|
|
||||||
return a.toLowerCase().includes(b.toLowerCase());
|
return a.toLowerCase().includes(b.toLowerCase());
|
||||||
}
|
}
|
||||||
throw new Error('???'); // wtf
|
}
|
||||||
};
|
|
||||||
}
|
class InequalityTerm extends BinaryOpTerm {
|
||||||
}
|
constructor(left, right) {
|
||||||
}
|
super('boolean', 'any', left, 'any', right);
|
||||||
return left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function takeCmpRel(tokens) {
|
apply(a, b) {
|
||||||
const left = takeSum(tokens);
|
checkComparable(a);
|
||||||
if (tokens.here.type === 'sym') {
|
checkComparable(b);
|
||||||
switch (tokens.here.sym) {
|
return this.compare(a, b);
|
||||||
case '<=': {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeCmpRel(tokens);
|
|
||||||
return (i, ie) => checkNum(left(i, ie)) <= checkNum(right(i, ie));
|
|
||||||
}
|
|
||||||
case '<': {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeCmpRel(tokens);
|
|
||||||
return (i, ie) => checkNum(left(i, ie)) < checkNum(right(i, ie));
|
|
||||||
}
|
|
||||||
case '>': {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeCmpRel(tokens);
|
|
||||||
return (i, ie) => checkNum(left(i, ie)) > checkNum(right(i, ie));
|
|
||||||
}
|
|
||||||
case '>=': {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeCmpRel(tokens);
|
|
||||||
return (i, ie) => checkNum(left(i, ie)) >= checkNum(right(i, ie));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function takeSum(tokens) {
|
compare(a, b) {
|
||||||
const left = takeProd(tokens);
|
throw new Error('Abstract method!');
|
||||||
if (tokens.here.type === 'sym') {
|
|
||||||
switch (tokens.here.sym) {
|
|
||||||
case '+': {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeSum(tokens);
|
|
||||||
return (i, ie) => checkNum(left(i, ie)) + checkNum(right(i, ie));
|
|
||||||
}
|
}
|
||||||
case '-': {
|
}
|
||||||
tokens.advance();
|
|
||||||
const right = takeSum(tokens);
|
class LeqTerm extends InequalityTerm {
|
||||||
return (i, ie) => checkNum(left(i, ie)) - checkNum(right(i, ie));
|
compare(a, b) {
|
||||||
|
return a <= b;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LtTerm extends InequalityTerm {
|
||||||
|
compare(a, b) {
|
||||||
|
return a < b;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GtTerm extends InequalityTerm {
|
||||||
|
compare(a, b) {
|
||||||
|
return a > b;
|
||||||
}
|
}
|
||||||
return left;
|
}
|
||||||
|
|
||||||
|
class GeqTerm extends InequalityTerm {
|
||||||
|
compare(a, b) {
|
||||||
|
return a >= b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArithmeticTerm extends BinaryOpTerm {
|
||||||
|
constructor(left, right) {
|
||||||
|
super('number', 'number', left, 'number', right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddTerm extends ArithmeticTerm {
|
||||||
|
apply(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubTerm extends ArithmeticTerm {
|
||||||
|
apply(a, b) {
|
||||||
|
return a - b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MulTerm extends ArithmeticTerm {
|
||||||
|
apply(a, b) {
|
||||||
|
return a * b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DivTerm extends ArithmeticTerm {
|
||||||
|
apply(a, b) {
|
||||||
|
return a / b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExpTerm extends ArithmeticTerm {
|
||||||
|
apply(a, b) {
|
||||||
|
return a ** b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnaryOpTerm extends Term {
|
||||||
|
constructor(type, inType, inVal) {
|
||||||
|
super(type);
|
||||||
|
staticCheck(inType, inVal);
|
||||||
|
this.inVal = inVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
function takeProd(tokens) {
|
resolve(item, itemExt) {
|
||||||
const left = takeExp(tokens);
|
return this.apply(this.inVal.resolve(item, itemExt));
|
||||||
if (tokens.here.type === 'sym') {
|
|
||||||
switch (tokens.here.sym) {
|
|
||||||
case '*': {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeProd(tokens);
|
|
||||||
return (i, ie) => checkNum(left(i, ie)) * checkNum(right(i, ie));
|
|
||||||
}
|
|
||||||
case '/': {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeProd(tokens);
|
|
||||||
return (i, ie) => checkNum(left(i, ie)) / checkNum(right(i, ie));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function takeExp(tokens) {
|
apply(x) {
|
||||||
const left = takeUnary(tokens);
|
throw new Error('Abstract method!');
|
||||||
if (tokens.here.type === 'sym' && tokens.here.sym === '^') {
|
|
||||||
tokens.advance();
|
|
||||||
const right = takeExp(tokens);
|
|
||||||
return (i, ie) => checkNum(left(i, ie)) ** checkNum(right(i, ie));
|
|
||||||
}
|
}
|
||||||
return left;
|
}
|
||||||
|
|
||||||
|
class NegTerm extends UnaryOpTerm {
|
||||||
|
constructor(inVal) {
|
||||||
|
super('number', 'number', inVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
function takeUnary(tokens) {
|
apply(x) {
|
||||||
if (tokens.here.type === 'sym') {
|
return -x;
|
||||||
switch (tokens.here.sym) {
|
|
||||||
case '-': {
|
|
||||||
tokens.advance();
|
|
||||||
const operand = takeUnary(tokens);
|
|
||||||
return (i, ie) => -checkNum(operand(i, ie));
|
|
||||||
}
|
}
|
||||||
case '!': {
|
}
|
||||||
tokens.advance();
|
|
||||||
const operand = takeUnary(tokens);
|
class InvTerm extends UnaryOpTerm {
|
||||||
return (i, ie) => !checkBool(operand(i, ie));
|
constructor(inVal) {
|
||||||
}
|
super('boolean', 'boolean', inVal);
|
||||||
}
|
|
||||||
}
|
|
||||||
return takePrim(tokens);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function takePrim(tokens) {
|
apply(x) {
|
||||||
switch (tokens.here.type) {
|
return !x;
|
||||||
case 'num': {
|
|
||||||
const lit = tokens.here.value;
|
|
||||||
tokens.advance();
|
|
||||||
return (i, ie) => lit;
|
|
||||||
}
|
}
|
||||||
case 'bool': {
|
}
|
||||||
const lit = tokens.here.value;
|
|
||||||
tokens.advance();
|
class FnCallTerm extends Term {
|
||||||
return (i, ie) => lit;
|
constructor(fn, argExprs) {
|
||||||
}
|
super(fn.type);
|
||||||
case 'str': {
|
this.fn = fn;
|
||||||
const lit = tokens.here.value;
|
this.argExprs = argExprs;
|
||||||
tokens.advance();
|
|
||||||
return (i, ie) => lit;
|
|
||||||
}
|
|
||||||
case 'id':
|
|
||||||
const id = tokens.here.id;
|
|
||||||
tokens.advance();
|
|
||||||
if (tokens.here.type === 'sym' && tokens.here.sym === '(') { // it's a function call
|
|
||||||
tokens.advance();
|
|
||||||
const argExprs = [];
|
|
||||||
if (tokens.here.type !== 'sym' || tokens.here.sym !== ')') {
|
|
||||||
arg_iter: // collect arg expressions, if there are any
|
|
||||||
while (true) {
|
|
||||||
argExprs.push(takeDisj(tokens));
|
|
||||||
if (tokens.here.type === 'sym') {
|
|
||||||
switch (tokens.here.sym) {
|
|
||||||
case ')':
|
|
||||||
tokens.advance();
|
|
||||||
break arg_iter;
|
|
||||||
case ',':
|
|
||||||
tokens.advance();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`Expected "," or ")", but got ${JSON.stringify(tokens.here)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const func = itemQueryFuncs[id.toLowerCase()];
|
|
||||||
if (!func) throw new Error(`Unknown function: ${id}`);
|
|
||||||
return (i, ie) => {
|
|
||||||
const args = [];
|
|
||||||
for (let k = 0; k < argExprs.length; k++) args.push(argExprs[k](i, ie));
|
|
||||||
return func(args);
|
|
||||||
};
|
|
||||||
} else { // not a function call
|
|
||||||
const prop = itemQueryProps[id.toLowerCase()];
|
|
||||||
if (!prop) throw new Error(`Unknown property: ${id}`);
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
case 'sym':
|
|
||||||
if (tokens.here.sym === '(') {
|
|
||||||
tokens.advance();
|
|
||||||
const expr = takeDisj(tokens);
|
|
||||||
if (tokens.here.type !== 'sym' || tokens.here.sym !== ')') throw new Error('Bracket mismatch');
|
|
||||||
tokens.advance();
|
|
||||||
return expr;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
throw new Error(tokens.here.type === 'eof' ? 'Reached end of expression' : `Unexpected token: ${JSON.stringify(tokens.here)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// full compilation function, with extra safety for empty input strings
|
resolve(item, itemExt) {
|
||||||
return function(exprStr) {
|
const argVals = [];
|
||||||
const tokens = tokenize(exprStr);
|
for (const argExpr of this.argExprs) {
|
||||||
return tokens.tokens.length <= 1 ? null : takeDisj(tokens);
|
argVals.push(argExpr.resolve(item, itemExt));
|
||||||
};
|
}
|
||||||
})();
|
return this.fn.fn(argVals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PropTerm extends Term {
|
||||||
|
constructor(prop) {
|
||||||
|
super(prop.type);
|
||||||
|
this.prop = prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(item, itemExt) {
|
||||||
|
return this.prop.resolve(item, itemExt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue