2021-01-27 04:18:16 +00:00
|
|
|
/*
|
|
|
|
* disj := conj "|" disj
|
|
|
|
* | conj
|
|
|
|
*
|
|
|
|
* conj := cmp "&" conj
|
|
|
|
* | cmpEq
|
|
|
|
*
|
|
|
|
* cmpEq := cmpRel "=" cmpEq
|
|
|
|
* | 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);
|
|
|
|
}
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
// type casts
|
|
|
|
function checkBool(v) {
|
|
|
|
if (typeof v !== 'boolean') throw new Error(`Expected boolean, but got ${typeof v}`);
|
|
|
|
return v;
|
|
|
|
}
|
2021-01-25 22:06:41 +00:00
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
function checkNum(v) {
|
|
|
|
if (typeof v !== 'number') throw new Error(`Expected number, but got ${typeof v}`);
|
|
|
|
return v;
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
function checkStr(v) {
|
|
|
|
if (typeof v !== 'string') throw new Error(`Expected string, but got ${typeof v}`);
|
|
|
|
return v;
|
|
|
|
}
|
2021-01-25 22:06:41 +00:00
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
// properties of items that can be looked up
|
|
|
|
const itemQueryProps = (function() {
|
|
|
|
const props = {};
|
|
|
|
function prop(names, prop) {
|
|
|
|
if (Array.isArray(names)) {
|
|
|
|
for (name of names) {
|
|
|
|
props[name] = prop;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
props[names] = prop;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function maxId(names, idKey) {
|
|
|
|
prop(names, (i, ie) => ie.get('maxRolls').get(idKey));
|
|
|
|
}
|
|
|
|
|
|
|
|
prop('name', (i, ie) => i.displayName || i.name);
|
|
|
|
|
|
|
|
prop('str', (i, ie) => i.str);
|
|
|
|
prop('dex', (i, ie) => i.dex);
|
|
|
|
prop('int', (i, ie) => i.int);
|
|
|
|
prop('def', (i, ie) => i.def);
|
|
|
|
prop('agi', (i, ie) => i.agi);
|
|
|
|
// TODO more properties
|
|
|
|
|
|
|
|
return props;
|
|
|
|
})();
|
|
|
|
|
|
|
|
// functions that can be called in query expressions
|
|
|
|
const itemQueryFuncs = {
|
|
|
|
max(args) {
|
|
|
|
if (args.length < 1) throw new Error('Not enough args to max()')
|
|
|
|
let runningMax = -Infinity;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
|
|
if (checkNum(args[i]) > runningMax) runningMax = args[i];
|
|
|
|
}
|
|
|
|
return runningMax;
|
|
|
|
},
|
|
|
|
min(args) {
|
|
|
|
if (args.length < 1) throw new Error('Not enough args to min()')
|
|
|
|
let runningMin = Infinity;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
|
|
if (checkNum(args[i]) < runningMin) runningMin = args[i];
|
|
|
|
}
|
|
|
|
return runningMin;
|
|
|
|
},
|
|
|
|
floor(args) {
|
|
|
|
if (args.length < 1) throw new Error('Not enough args to floor()')
|
|
|
|
return Math.floor(checkNum(args[0]));
|
|
|
|
},
|
|
|
|
ceil(args) {
|
|
|
|
if (args.length < 1) throw new Error('Not enough args to ceil()')
|
|
|
|
return Math.ceil(checkNum(args[0]));
|
|
|
|
}
|
|
|
|
// TODO more functions
|
2021-01-25 22:06:41 +00:00
|
|
|
};
|
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
// the compiler itself
|
|
|
|
const compileQueryExpr = (function() {
|
|
|
|
// tokenize an expression string
|
|
|
|
function tokenize(exprStr) {
|
|
|
|
exprStr = exprStr.trim();
|
|
|
|
const tokens = [];
|
|
|
|
let col = 0;
|
|
|
|
function pushSymbol(sym) {
|
|
|
|
tokens.push({ type: 'sym', 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;
|
|
|
|
}
|
|
|
|
// 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) {
|
|
|
|
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;
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
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
|
|
|
|
function takeDisj(tokens) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
function takeConj(tokens) {
|
|
|
|
const left = takeCmpEq(tokens);
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
function takeCmpEq(tokens) {
|
|
|
|
const left = takeCmpRel(tokens);
|
|
|
|
if (tokens.here.type === 'sym') {
|
|
|
|
switch (tokens.here.sym) {
|
|
|
|
case '=': {
|
|
|
|
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;
|
|
|
|
case 'string':
|
|
|
|
return a.toLowerCase() === b.toLowerCase();
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
throw new Error('???'); // wut
|
|
|
|
};
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
case '!=': {
|
|
|
|
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;
|
|
|
|
case 'string':
|
|
|
|
return a.toLowerCase() !== b.toLowerCase();
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
throw new Error('???'); // wtf
|
|
|
|
};
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return left;
|
|
|
|
}
|
|
|
|
|
|
|
|
function takeCmpRel(tokens) {
|
|
|
|
const left = takeSum(tokens);
|
|
|
|
if (tokens.here.type === 'sym') {
|
|
|
|
switch (tokens.here.sym) {
|
|
|
|
case '<=': {
|
|
|
|
tokens.advance();
|
|
|
|
const right = takeCmpRel(tokens);
|
|
|
|
return (i, ie) => checkNum(left(i, ie)) <= checkNum(right(i, ie));
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
case '<': {
|
|
|
|
tokens.advance();
|
|
|
|
const right = takeCmpRel(tokens);
|
|
|
|
return (i, ie) => checkNum(left(i, ie)) < checkNum(right(i, ie));
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
case '>': {
|
|
|
|
tokens.advance();
|
|
|
|
const right = takeCmpRel(tokens);
|
|
|
|
return (i, ie) => checkNum(left(i, ie)) > checkNum(right(i, ie));
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
case '>=': {
|
|
|
|
tokens.advance();
|
|
|
|
const right = takeCmpRel(tokens);
|
|
|
|
return (i, ie) => checkNum(left(i, ie)) >= checkNum(right(i, ie));
|
2021-01-23 16:38:13 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return left;
|
|
|
|
}
|
|
|
|
|
|
|
|
function takeSum(tokens) {
|
|
|
|
const left = takeProd(tokens);
|
|
|
|
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));
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
case '-': {
|
|
|
|
tokens.advance();
|
|
|
|
const right = takeSum(tokens);
|
|
|
|
return (i, ie) => checkNum(left(i, ie)) - checkNum(right(i, ie));
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return left;
|
|
|
|
}
|
|
|
|
|
|
|
|
function takeProd(tokens) {
|
|
|
|
const left = takeExp(tokens);
|
|
|
|
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));
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
case '/': {
|
|
|
|
tokens.advance();
|
|
|
|
const right = takeProd(tokens);
|
|
|
|
return (i, ie) => checkNum(left(i, ie)) / checkNum(right(i, ie));
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return left;
|
|
|
|
}
|
|
|
|
|
|
|
|
function takeExp(tokens) {
|
|
|
|
const left = takeUnary(tokens);
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
function takeUnary(tokens) {
|
|
|
|
if (tokens.here.type === 'sym') {
|
|
|
|
switch (tokens.here.sym) {
|
|
|
|
case '-': {
|
|
|
|
tokens.advance();
|
|
|
|
const operand = takeUnary(tokens);
|
|
|
|
return (i, ie) => -checkNum(operand(i, ie));
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
case '!': {
|
|
|
|
tokens.advance();
|
|
|
|
const operand = takeUnary(tokens);
|
|
|
|
return (i, ie) => !checkBool(operand(i, ie));
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return takePrim(tokens);
|
|
|
|
}
|
|
|
|
|
|
|
|
function takePrim(tokens) {
|
|
|
|
switch (tokens.here.type) {
|
|
|
|
case 'num': {
|
|
|
|
const lit = tokens.here.value;
|
|
|
|
tokens.advance();
|
|
|
|
return (i, ie) => lit;
|
|
|
|
}
|
|
|
|
case 'bool': {
|
|
|
|
const lit = tokens.here.value;
|
|
|
|
tokens.advance();
|
|
|
|
return (i, ie) => lit;
|
|
|
|
}
|
|
|
|
case 'str': {
|
|
|
|
const lit = tokens.here.value;
|
|
|
|
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];
|
|
|
|
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];
|
|
|
|
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;
|
2021-01-25 22:06:41 +00:00
|
|
|
}
|
2021-01-27 04:18:16 +00:00
|
|
|
throw new Error(tokens.here.type === 'eof' ? 'Reached end of expression' : `Unexpected token: ${JSON.stringify(tokens.here)}`);
|
|
|
|
}
|
2021-01-25 22:06:41 +00:00
|
|
|
|
2021-01-27 04:18:16 +00:00
|
|
|
// full compilation function, with extra safety for empty input strings
|
|
|
|
return function(exprStr) {
|
|
|
|
const tokens = tokenize(exprStr);
|
|
|
|
return tokens.tokens.length <= 1 ? null : takeDisj(tokens);
|
|
|
|
};
|
|
|
|
})();
|