diff --git a/expr_parser.js b/expr_parser.js new file mode 100644 index 0000000..8304af7 --- /dev/null +++ b/expr_parser.js @@ -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); + } + }; +})(); diff --git a/expr_parser.md b/expr_parser.md new file mode 100644 index 0000000..f7e3ffc --- /dev/null +++ b/expr_parser.md @@ -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 diff --git a/items_2.html b/items_2.html index 1bb3ff0..256f3bf 100644 --- a/items_2.html +++ b/items_2.html @@ -78,6 +78,7 @@ + diff --git a/items_2.js b/items_2.js index bbe96f2..d724cbd 100644 --- a/items_2.js +++ b/items_2.js @@ -21,6 +21,7 @@ class ExprField { } catch (e) { this.errorText.innerText = e.message; this.output = null; + console.error(e); } return true; } @@ -38,6 +39,8 @@ function compareLexico(ia, keysA, ib, keysB) { if (aKey > bKey) return 1; break; 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; break; @@ -74,17 +77,23 @@ function init() { itemEntries.push(itemElem); } + // create the expression parser + const exprParser = new ExprParser(itemQueryProps, itemQueryFuncs); + // the two search query input boxes const searchFilterField = new ExprField('search-filter-field', 'search-filter-error', function(exprStr) { - const expr = compileQueryExpr(exprStr); - return expr !== null ? expr : (i, ie) => true; + const expr = exprParser.parse(exprStr); + return expr !== null ? expr : new BoolLitTerm(true); }); const searchSortField = new ExprField('search-sort-field', 'search-sort-error', function(exprStr) { - const subExprs = exprStr.split(';').map(compileQueryExpr).filter(f => f != null); - return function(i, ie) { - const sortKeys = []; - for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k](i, ie)); - return sortKeys; + const subExprs = exprStr.split(';').map(e => exprParser.parse(e)).filter(f => f != null); + return { + type: 'array', + resolve(i, ie) { + const sortKeys = []; + for (let k = 0; k < subExprs.length; k++) sortKeys.push(subExprs[k].resolve(i, ie)); + return sortKeys; + } }; }); @@ -108,8 +117,8 @@ function init() { try { for (let i = 0; i < searchDb.length; i++) { const item = searchDb[i][0], itemExp = searchDb[i][1]; - if (checkBool(searchFilterField.output(item, itemExp))) { - searchResults.push({ item, itemExp, sortKeys: searchSortField.output(item, itemExp) }); + if (checkBool(searchFilterField.output.resolve(item, itemExp))) { + searchResults.push({ item, itemExp, sortKeys: searchSortField.output.resolve(item, itemExp) }); } } } catch (e) { diff --git a/query_2.js b/query_2.js index 6f9f149..d0e935f 100644 --- a/query_2.js +++ b/query_2.js @@ -1,61 +1,4 @@ -/* - * 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 +// dynamic type casts function checkBool(v) { if (typeof v !== 'boolean') throw new Error(`Expected boolean, but got ${typeof v}`); return v; @@ -71,67 +14,79 @@ function checkStr(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 +// each entry is a function `(item, extended item) -> value` const itemQueryProps = (function() { const props = {}; - function prop(names, getProp) { + + function prop(names, type, resolve) { if (Array.isArray(names)) { for (name of names) { - props[name] = getProp; + props[name] = { type, resolve }; } } else { - props[names] = getProp; + props[names] = { type, resolve }; } } + 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) { - 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) { - prop(names, (i, ie) => { + prop(names, 'number', (i, ie) => { const range = getProp(i, ie); if (!range) return 0; const ndx = range.indexOf('-'); 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 = []; for (let k = 0; k < comps.length; k++) args.push(comps[k](i, ie)); return f.apply(null, args); }); } + function sum(names, ...comps) { - return map(names, comps, (...summands) => { + return map(names, comps, 'number', (...summands) => { let total = 0; for (let i = 0; i < summands.length; i++) total += summands[i]; return total; }); } - prop('name', (i, ie) => i.displayName || i.name); - prop('type', (i, ie) => i.type); - prop(['cat', 'category'], (i, ie) => i.category); + prop('name', 'string', (i, ie) => i.displayName || i.name); + prop('type', 'string', (i, ie) => i.type); + prop(['cat', 'category'], 'string', (i, ie) => i.category); 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(['rarity', 'tier'], (i, ie) => tierIndices[i.tier]); + prop(['rarityname', 'raritystr', 'tiername', 'tierstr'], 'string', (i, ie) => i.tier); + prop(['rarity', 'tier'], 'string', (i, ie) => tierIndices[i.tier]); - prop(['level', 'lvl', 'combatlevel', 'combatlvl'], (i, ie) => i.lvl); - prop(['strmin', 'strreq'], (i, ie) => i.strReq); - prop(['dexmin', 'dexreq'], (i, ie) => i.dexReq); - prop(['intmin', 'intreq'], (i, ie) => i.intReq); - prop(['defmin', 'defreq'], (i, ie) => i.defReq); - prop(['agimin', 'agireq'], (i, ie) => i.agiReq); + prop(['level', 'lvl', 'combatlevel', 'combatlvl'], 'number', (i, ie) => i.lvl); + prop(['strmin', 'strreq'], 'number', (i, ie) => i.strReq); + prop(['dexmin', 'dexreq'], 'number', (i, ie) => i.dexReq); + prop(['intmin', 'intreq'], 'number', (i, ie) => i.intReq); + prop(['defmin', 'defreq'], 'number', (i, ie) => i.defReq); + prop(['agimin', 'agireq'], 'number', (i, ie) => i.agiReq); sum(['summin', 'sumreq', 'totalmin', 'totalreq'], props.strmin, props.dexmin, props.intmin, props.defmin, props.agimin); - 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); + prop('str', 'number', (i, ie) => i.str); + prop('dex', 'number', (i, ie) => i.dex); + prop('int', 'number', (i, ie) => i.int); + prop('def', 'number', (i, ie) => i.def); + prop('agi', 'number', (i, ie) => i.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); @@ -155,15 +110,15 @@ const itemQueryProps = (function() { 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 }; - 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'); sum(['sumattackspeed', 'totalattackspeed', 'sumatkspd', 'totalatkspd', 'sumatktier', 'totalatktier'], props.atkspd, props.atktier); - prop(['earthdef', 'edef'], (i, ie) => i.eDef || 0); - prop(['thunderdef', 'tdef'], (i, ie) => i.tDef || 0); - prop(['waterdef', 'wdef'], (i, ie) => i.wDef || 0); - prop(['firedef', 'fdef'], (i, ie) => i.fDef || 0); - prop(['airdef', 'adef'], (i, ie) => i.aDef || 0); + prop(['earthdef', 'edef'], 'number', (i, ie) => i.eDef || 0); + prop(['thunderdef', 'tdef'], 'number', (i, ie) => i.tDef || 0); + prop(['waterdef', 'wdef'], 'number', (i, ie) => i.wDef || 0); + prop(['firedef', 'fdef'], 'number', (i, ie) => i.fDef || 0); + prop(['airdef', 'adef'], 'number', (i, ie) => i.aDef || 0); sum(['sumdef', 'totaldef'], props.edef, props.tdef, props.wdef, props.fdef, props.adef); maxId(['earthdef%', 'edef%', 'edefpct'], 'eDefPct'); @@ -173,7 +128,7 @@ const itemQueryProps = (function() { maxId(['airdef%', 'adef%', 'adefpct'], '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'); sum(['sumhealth', 'sumhp', 'totalhealth', 'totalhp'], props.hp, props.hpid); @@ -207,397 +162,347 @@ const itemQueryProps = (function() { maxId(['lootbonus', 'lb'], 'lb'); maxId(['xpbonus', 'xpb', 'xb'], 'xpb'); 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; })(); // 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]; + max: { + type: 'number', + fn: function(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; } - 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]; + min: { + type: 'number', + fn: function(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; } - 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])); - }, - round(args) { - if (args.length < 1) throw new Error('Not enough args to ceil()'); - return Math.round(checkNum(args[0])); - }, - sqrt(args) { - if (args.length < 1) throw new Error('Not enough args to ceil()'); - return Math.sqrt(checkNum(args[0])); - }, - abs(args) { - if (args.length < 1) throw new Error('Not enough args to ceil()'); - return Math.abs(checkNum(args[0])); - }, - contains(args) { - if (args.length < 2) throw new Error('Not enough args to contains()'); - return checkStr(args[0]).toLowerCase().includes(checkStr(args[1]).toLowerCase()); - }, - atkspdmod(args) { - if (args.length < 1) throw new Error('Not enough args to atkSpdMod()'); - switch (checkNum(args[0])) { - case 2: return 3.1; - case 1: return 2.5; - case 0: return 2.05; - case -1: return 1.5; - case -2: return 0.83; + floor: { + type: 'number', + fn: function(args) { + if (args.length < 1) throw new Error('Not enough args to floor()'); + return Math.floor(checkNum(args[0])); + } + }, + ceil: { + type: 'number', + fn: function(args) { + if (args.length < 1) throw new Error('Not enough args to ceil()'); + return Math.ceil(checkNum(args[0])); + } + }, + round: { + type: 'number', + fn: function(args) { + if (args.length < 1) throw new Error('Not enough args to round()'); + return Math.round(checkNum(args[0])); + } + }, + sqrt: { + type: 'number', + fn: function(args) { + if (args.length < 1) throw new Error('Not enough args to sqrt()'); + return Math.sqrt(checkNum(args[0])); + } + }, + abs: { + type: 'number', + fn: function(args) { + if (args.length < 1) throw new Error('Not enough args to abs()'); + return Math.abs(checkNum(args[0])); + } + }, + contains: { + type: 'boolean', + fn: function(args) { + if (args.length < 2) throw new Error('Not enough args to contains()'); + return checkStr(args[0]).toLowerCase().includes(checkStr(args[1]).toLowerCase()); + } + }, + atkspdmod: { + type: 'number', + fn: function(args) { + if (args.length < 1) throw new Error('Not enough args to atkSpdMod()'); + switch (checkNum(args[0])) { + case 2: + return 3.1; + case 1: + return 2.5; + 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 4.3; + throw new Error('Invalid argument to atkSpdMod()'); } - if (args[0] <= -3) return 0.51; - if (args[0] >= 3) return 4.3; - throw new Error('Invalid argument to atkSpdMod()'); } }; -// 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; - } - 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); +// static type check +function staticCheck(expType, term) { + if (expType === 'any' || expType === term.type) { + return true; + } + throw new Error(`Expected ${expType}, but got ${term.type}`); +} + +// expression terms +class Term { + constructor(type) { + this.type = type; } - // 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; + resolve(item, itemExt) { + throw new Error('Abstract method!'); + } +} + +class LiteralTerm extends Term { + constructor(type, value) { + super(type); + this.value = value; } - 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; + resolve(item, itemExt) { + return this.value; + } +} + +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) { - 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(); - } - throw new Error('???'); // wut - }; - } - 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(); - } - throw new Error('???'); // wtf - }; - } - case '?=': { - tokens.advance(); - 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()); - } - throw new Error('???'); // wtf - }; - } - } - } - return left; + resolve(item, itemExt) { + return this.apply(this.left.resolve(item, itemExt), this.right.resolve(item, itemExt)); } - 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)); - } - 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; + apply(a, b) { + throw new Error('Abstract method!'); + } +} + +class LogicalTerm extends BinaryOpTerm { + constructor(left, right) { + super('boolean', 'boolean', left, 'boolean', right); + } +} + +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; + } +} + +class NeqTerm extends EqualityTerm { + apply(a, b) { + return a !== b; + } +} + +class ContainsTerm extends BinaryOpTerm { + constructor(left, right) { + super('boolean', 'string', left, 'string', right); } - 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)); - } - case '-': { - tokens.advance(); - const right = takeSum(tokens); - return (i, ie) => checkNum(left(i, ie)) - checkNum(right(i, ie)); - } - } - } - return left; + apply(a, b) { + return a.toLowerCase().includes(b.toLowerCase()); + } +} + +class InequalityTerm extends BinaryOpTerm { + constructor(left, right) { + super('boolean', 'any', left, 'any', right); } - 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)); - } - case '/': { - tokens.advance(); - const right = takeProd(tokens); - return (i, ie) => checkNum(left(i, ie)) / checkNum(right(i, ie)); - } - } - } - return left; + apply(a, b) { + checkComparable(a); + checkComparable(b); + return this.compare(a, b); } - 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; + compare(a, b) { + throw new Error('Abstract method!'); + } +} + +class LeqTerm extends InequalityTerm { + 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; + } +} + +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 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)); - } - case '!': { - tokens.advance(); - const operand = takeUnary(tokens); - return (i, ie) => !checkBool(operand(i, ie)); - } - } - } - return takePrim(tokens); + resolve(item, itemExt) { + return this.apply(this.inVal.resolve(item, itemExt)); } - 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.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)}`); + apply(x) { + throw new Error('Abstract method!'); + } +} + +class NegTerm extends UnaryOpTerm { + constructor(inVal) { + super('number', 'number', inVal); } - // 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); - }; -})(); + apply(x) { + return -x; + } +} + +class InvTerm extends UnaryOpTerm { + constructor(inVal) { + super('boolean', 'boolean', inVal); + } + + apply(x) { + return !x; + } +} + +class FnCallTerm extends Term { + constructor(fn, argExprs) { + super(fn.type); + this.fn = fn; + this.argExprs = argExprs; + } + + resolve(item, itemExt) { + const argVals = []; + for (const argExpr of this.argExprs) { + 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); + } +}