UNPKG

prisme-flow

Version:

prisme platform flow engine

1,275 lines (1,223 loc) 129 kB
/** * © Copyright IBM Corp. 2016, 2017 All Rights Reserved * Project name: JSONata * This project is licensed under the MIT License, see LICENSE */ /** * @module JSONata * @description JSON query and transformation language */ /** * jsonata * @function * @param {Object} expr - JSONata expression * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression */ var jsonata = (function() { 'use strict'; var operators = { '.': 75, '[': 80, ']': 0, '{': 70, '}': 0, '(': 80, ')': 0, ',': 0, '@': 75, '#': 70, ';': 80, ':': 80, '?': 20, '+': 50, '-': 50, '*': 60, '/': 60, '%': 60, '|': 20, '=': 40, '<': 40, '>': 40, '`': 80, '**': 60, '..': 20, ':=': 10, '!=': 40, '<=': 40, '>=': 40, '~>': 40, 'and': 30, 'or': 25, 'in': 40, '&': 50, '!': 0, // not an operator, but needed as a stop character for name tokens '~': 0 // not an operator, but needed as a stop character for name tokens }; var escapes = { // JSON string escape sequences - see json.org '"': '"', '\\': '\\', '/': '/', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t' }; // Tokenizer (lexer) - invoked by the parser to return one token at a time var tokenizer = function (path) { var position = 0; var length = path.length; var create = function (type, value) { var obj = {type: type, value: value, position: position}; return obj; }; var scanRegex = function() { // the prefix '/' will have been previously scanned. Find the end of the regex. // search for closing '/' ignoring any that are escaped, or within brackets var start = position; var depth = 0; var pattern; var flags; while(position < length) { var currentChar = path.charAt(position); if(currentChar === '/' && path.charAt(position - 1) !== '\\' && depth === 0) { // end of regex found pattern = path.substring(start, position); if(pattern === '') { throw { code: "S0301", stack: (new Error()).stack, position: position }; } position++; currentChar = path.charAt(position); // flags start = position; while(currentChar === 'i' || currentChar === 'm') { position++; currentChar = path.charAt(position); } flags = path.substring(start, position) + 'g'; return new RegExp(pattern, flags); } if((currentChar === '(' || currentChar === '[' || currentChar === '{') && path.charAt(position - 1) !== '\\' ) { depth++; } if((currentChar === ')' || currentChar === ']' || currentChar === '}') && path.charAt(position - 1) !== '\\' ) { depth--; } position++; } throw { code: "S0302", stack: (new Error()).stack, position: position }; }; var next = function (prefix) { if (position >= length) return null; var currentChar = path.charAt(position); // skip whitespace while (position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { position++; currentChar = path.charAt(position); } // test for regex if (prefix !== true && currentChar === '/') { position++; return create('regex', scanRegex()); } // handle double-char operators if (currentChar === '.' && path.charAt(position + 1) === '.') { // double-dot .. range operator position += 2; return create('operator', '..'); } if (currentChar === ':' && path.charAt(position + 1) === '=') { // := assignment position += 2; return create('operator', ':='); } if (currentChar === '!' && path.charAt(position + 1) === '=') { // != position += 2; return create('operator', '!='); } if (currentChar === '>' && path.charAt(position + 1) === '=') { // >= position += 2; return create('operator', '>='); } if (currentChar === '<' && path.charAt(position + 1) === '=') { // <= position += 2; return create('operator', '<='); } if (currentChar === '*' && path.charAt(position + 1) === '*') { // ** descendant wildcard position += 2; return create('operator', '**'); } if (currentChar === '~' && path.charAt(position + 1) === '>') { // ~> chain function position += 2; return create('operator', '~>'); } // test for single char operators if (operators.hasOwnProperty(currentChar)) { position++; return create('operator', currentChar); } // test for string literals if (currentChar === '"' || currentChar === "'") { var quoteType = currentChar; // double quoted string literal - find end of string position++; var qstr = ""; while (position < length) { currentChar = path.charAt(position); if (currentChar === '\\') { // escape sequence position++; currentChar = path.charAt(position); if (escapes.hasOwnProperty(currentChar)) { qstr += escapes[currentChar]; } else if (currentChar === 'u') { // \u should be followed by 4 hex digits var octets = path.substr(position + 1, 4); if (/^[0-9a-fA-F]+$/.test(octets)) { var codepoint = parseInt(octets, 16); qstr += String.fromCharCode(codepoint); position += 4; } else { throw { code: "S0104", stack: (new Error()).stack, position: position }; } } else { // illegal escape sequence throw { code: "S0103", stack: (new Error()).stack, position: position, token: currentChar }; } } else if (currentChar === quoteType) { position++; return create('string', qstr); } else { qstr += currentChar; } position++; } throw { code: "S0101", stack: (new Error()).stack, position: position }; } // test for numbers var numregex = /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?/; var match = numregex.exec(path.substring(position)); if (match !== null) { var num = parseFloat(match[0]); if (!isNaN(num) && isFinite(num)) { position += match[0].length; return create('number', num); } else { throw { code: "S0102", stack: (new Error()).stack, position: position, token: match[0] }; } } // test for names var i = position; var ch; var name; for (;;) { ch = path.charAt(i); if (i === length || ' \t\n\r\v'.indexOf(ch) > -1 || operators.hasOwnProperty(ch)) { if (path.charAt(position) === '$') { // variable reference name = path.substring(position + 1, i); position = i; return create('variable', name); } else { name = path.substring(position, i); position = i; switch (name) { case 'or': case 'in': case 'and': return create('operator', name); case 'true': return create('value', true); case 'false': return create('value', false); case 'null': return create('value', null); default: if (position === length && name === '') { // whitespace at end of input return null; } return create('name', name); } } } else { i++; } } }; return next; }; /** * Parses a function signature definition and returns a validation function * @param {string} signature - the signature between the <angle brackets> * @returns {Function} validation function */ function parseSignature(signature) { // create a Regex that represents this signature and return a function that when invoked, // returns the validated (possibly fixed-up) arguments, or throws a validation error // step through the signature, one symbol at a time var position = 1; var params = []; var param = {}; var prevParam = param; while (position < signature.length) { var symbol = signature.charAt(position); if(symbol === ':') { // TODO figure out what to do with the return type // ignore it for now break; } var next = function() { params.push(param); prevParam = param; param = {}; }; var findClosingBracket = function(str, start, openSymbol, closeSymbol) { // returns the position of the closing symbol (e.g. bracket) in a string // that balances the opening symbol at position start var depth = 1; var position = start; while(position < str.length) { position++; symbol = str.charAt(position); if(symbol === closeSymbol) { depth--; if(depth === 0) { // we're done break; // out of while loop } } else if(symbol === openSymbol) { depth++; } } return position; }; switch (symbol) { case 's': // string case 'n': // number case 'b': // boolean case 'l': // not so sure about expecting null? case 'o': // object param.regex = '[' + symbol + 'm]'; param.type = symbol; next(); break; case 'a': // array // normally treat any value as singleton array param.regex = '[asnblfom]'; param.type = symbol; param.array = true; next(); break; case 'f': // function param.regex = 'f'; param.type = symbol; next(); break; case 'j': // any JSON type param.regex = '[asnblom]'; param.type = symbol; next(); break; case 'x': // any type param.regex = '[asnblfom]'; param.type = symbol; next(); break; case '-': // use context if param not supplied prevParam.context = true; prevParam.contextRegex = new RegExp(prevParam.regex); // pre-compiled to test the context type at runtime prevParam.regex += '?'; break; case '?': // optional param case '+': // one or more prevParam.regex += symbol; break; case '(': // choice of types // search forward for matching ')' var endParen = findClosingBracket(signature, position, '(', ')'); var choice = signature.substring(position + 1, endParen); if(choice.indexOf('<') === -1) { // no parameterized types, simple regex param.regex = '[' + choice + 'm]'; } else { // TODO harder throw { code: "S0402", stack: (new Error()).stack, value: choice, offset: position }; } param.type = '(' + choice + ')'; position = endParen; next(); break; case '<': // type parameter - can only be applied to 'a' and 'f' if(prevParam.type === 'a' || prevParam.type === 'f') { // search forward for matching '>' var endPos = findClosingBracket(signature, position, '<', '>'); prevParam.subtype = signature.substring(position + 1, endPos); position = endPos; } else { throw { code: "S0401", stack: (new Error()).stack, value: prevParam.type, offset: position }; } break; } position++; } var regexStr = '^' + params.map(function(param) { return '(' + param.regex + ')'; }).join('') + '$'; var regex = new RegExp(regexStr); var getSymbol = function(value) { var symbol; if(isFunction(value)) { symbol = 'f'; } else { var type = typeof value; switch (type) { case 'string': symbol = 's'; break; case 'number': symbol = 'n'; break; case 'boolean': symbol = 'b'; break; case 'object': if (value === null) { symbol = 'l'; } else if (Array.isArray(value)) { symbol = 'a'; } else { symbol = 'o'; } break; case 'undefined': // any value can be undefined, but should be allowed to match symbol = 'm'; // m for missing } } return symbol; }; var throwValidationError = function(badArgs, badSig) { // to figure out where this went wrong we need apply each component of the // regex to each argument until we get to the one that fails to match var partialPattern = '^'; var goodTo = 0; for(var index = 0; index < params.length; index++) { partialPattern += params[index].regex; var match = badSig.match(partialPattern); if(match === null) { // failed here throw { code: "T0410", stack: (new Error()).stack, value: badArgs[goodTo], index: goodTo + 1 }; } goodTo = match[0].length; } // if it got this far, it's probably because of extraneous arguments (we // haven't added the trailing '$' in the regex yet. throw { code: "T0410", stack: (new Error()).stack, value: badArgs[goodTo], index: goodTo + 1 }; }; return { definition: signature, validate: function(args, context) { var suppliedSig = ''; args.forEach(function(arg) { suppliedSig += getSymbol(arg); }); var isValid = regex.exec(suppliedSig); if(isValid) { var validatedArgs = []; var argIndex = 0; params.forEach(function(param, index) { var arg = args[argIndex]; var match = isValid[index + 1]; if(match === '') { if (param.context) { // substitute context value for missing arg // first check that the context value is the right type var contextType = getSymbol(context); // test contextType against the regex for this arg (without the trailing ?) if(param.contextRegex.test(contextType)) { validatedArgs.push(context); } else { // context value not compatible with this argument throw { code: "T0411", stack: (new Error()).stack, value: context, index: argIndex + 1 }; } } else { validatedArgs.push(arg); argIndex++; } } else { // may have matched multiple args (if the regex ends with a '+' // split into single tokens match.split('').forEach(function(single) { if (param.type === 'a') { if (single === 'm') { // missing (undefined) arg = undefined; } else { arg = args[argIndex]; var arrayOK = true; // is there type information on the contents of the array? if (typeof param.subtype !== 'undefined') { if (single !== 'a' && match !== param.subtype) { arrayOK = false; } else if (single === 'a') { if (arg.length > 0) { var itemType = getSymbol(arg[0]); if (itemType !== param.subtype.charAt(0)) { // TODO recurse further arrayOK = false; } else { // make sure every item in the array is this type var differentItems = arg.filter(function (val) { return (getSymbol(val) !== itemType); }); arrayOK = (differentItems.length === 0); } } } } if (!arrayOK) { throw { code: "T0412", stack: (new Error()).stack, value: arg, index: argIndex + 1, type: param.subtype // TODO translate symbol to type name }; } // the function expects an array. If it's not one, make it so if (single !== 'a') { arg = [arg]; } } validatedArgs.push(arg); argIndex++; } else { validatedArgs.push(arg); argIndex++; } }); } }); return validatedArgs; } throwValidationError(args, suppliedSig); } }; } // This parser implements the 'Top down operator precedence' algorithm developed by Vaughan R Pratt; http://dl.acm.org/citation.cfm?id=512931. // and builds on the Javascript framework described by Douglas Crockford at http://javascript.crockford.com/tdop/tdop.html // and in 'Beautiful Code', edited by Andy Oram and Greg Wilson, Copyright 2007 O'Reilly Media, Inc. 798-0-596-51004-6 var parser = function (source) { var node; var lexer; var symbol_table = {}; var base_symbol = { nud: function () { return this; } }; var symbol = function (id, bp) { var s = symbol_table[id]; bp = bp || 0; if (s) { if (bp >= s.lbp) { s.lbp = bp; } } else { s = Object.create(base_symbol); s.id = s.value = id; s.lbp = bp; symbol_table[id] = s; } return s; }; var advance = function (id, infix) { if (id && node.id !== id) { var code; if(node.id === '(end)') { // unexpected end of buffer code = "S0203"; } else { code = "S0202"; } throw { code: code, stack: (new Error()).stack, position: node.position, token: node.id, value: id }; } var next_token = lexer(infix); if (next_token === null) { node = symbol_table["(end)"]; node.position = source.length; return node; } var value = next_token.value; var type = next_token.type; var symbol; switch (type) { case 'name': case 'variable': symbol = symbol_table["(name)"]; break; case 'operator': symbol = symbol_table[value]; if (!symbol) { throw { code: "S0204", stack: (new Error()).stack, position: next_token.position, token: value }; } break; case 'string': case 'number': case 'value': type = "literal"; symbol = symbol_table["(literal)"]; break; case 'regex': type = "regex"; symbol = symbol_table["(regex)"]; break; /* istanbul ignore next */ default: throw { code: "S0205", stack: (new Error()).stack, position: next_token.position, token: value }; } node = Object.create(symbol); node.value = value; node.type = type; node.position = next_token.position; return node; }; // Pratt's algorithm var expression = function (rbp) { var left; var t = node; advance(null, true); left = t.nud(); while (rbp < node.lbp) { t = node; advance(); left = t.led(left); } return left; }; // match infix operators // <expression> <operator> <expression> // left associative var infix = function (id, bp, led) { var bindingPower = bp || operators[id]; var s = symbol(id, bindingPower); s.led = led || function (left) { this.lhs = left; this.rhs = expression(bindingPower); this.type = "binary"; return this; }; return s; }; // match infix operators // <expression> <operator> <expression> // right associative var infixr = function (id, bp, led) { var bindingPower = bp || operators[id]; var s = symbol(id, bindingPower); s.led = led || function (left) { this.lhs = left; this.rhs = expression(bindingPower - 1); // subtract 1 from bindingPower for right associative operators this.type = "binary"; return this; }; return s; }; // match prefix operators // <operator> <expression> var prefix = function (id, nud) { var s = symbol(id); s.nud = nud || function () { this.expression = expression(70); this.type = "unary"; return this; }; return s; }; symbol("(end)"); symbol("(name)"); symbol("(literal)"); symbol("(regex)"); symbol(":"); symbol(";"); symbol(","); symbol(")"); symbol("]"); symbol("}"); symbol(".."); // range operator infix("."); // field reference infix("+"); // numeric addition infix("-"); // numeric subtraction infix("*"); // numeric multiplication infix("/"); // numeric division infix("%"); // numeric modulus infix("="); // equality infix("<"); // less than infix(">"); // greater than infix("!="); // not equal to infix("<="); // less than or equal infix(">="); // greater than or equal infix("&"); // string concatenation infix("and"); // Boolean AND infix("or"); // Boolean OR infix("in"); // is member of array infixr(":="); // bind variable prefix("-"); // unary numeric negation infix("~>"); // function application // field wildcard (single level) prefix('*', function () { this.type = "wildcard"; return this; }); // descendant wildcard (multi-level) prefix('**', function () { this.type = "descendant"; return this; }); // function invocation infix("(", operators['('], function (left) { // left is is what we are trying to invoke this.procedure = left; this.type = 'function'; this.arguments = []; if (node.id !== ')') { for (;;) { if (node.type === 'operator' && node.id === '?') { // partial function application this.type = 'partial'; this.arguments.push(node); advance('?'); } else { this.arguments.push(expression(0)); } if (node.id !== ',') break; advance(','); } } advance(")", true); // if the name of the function is 'function' or λ, then this is function definition (lambda function) if (left.type === 'name' && (left.value === 'function' || left.value === '\u03BB')) { // all of the args must be VARIABLE tokens this.arguments.forEach(function (arg, index) { if (arg.type !== 'variable') { throw { code: "S0208", stack: (new Error()).stack, position: arg.position, token: arg.value, value: index + 1 }; } }); this.type = 'lambda'; // is the next token a '<' - if so, parse the function signature if(node.id === '<') { var sigPos = node.position; var depth = 1; var sig = '<'; while(depth > 0 && node.id !== '{' && node.id !== '(end)') { var tok = advance(); if(tok.id === '>') { depth--; } else if(tok.id === '<') { depth++; } sig += tok.value; } advance('>'); try { this.signature = parseSignature(sig); } catch(err) { // insert the position into this error err.position = sigPos + err.offset; throw err; } } // parse the function body advance('{'); this.body = expression(0); advance('}'); } return this; }); // parenthesis - block expression prefix("(", function () { var expressions = []; while (node.id !== ")") { expressions.push(expression(0)); if (node.id !== ";") { break; } advance(";"); } advance(")", true); this.type = 'block'; this.expressions = expressions; return this; }); // array constructor prefix("[", function () { var a = []; if (node.id !== "]") { for (;;) { var item = expression(0); if (node.id === "..") { // range operator var range = {type: "binary", value: "..", position: node.position, lhs: item}; advance(".."); range.rhs = expression(0); item = range; } a.push(item); if (node.id !== ",") { break; } advance(","); } } advance("]", true); this.lhs = a; this.type = "unary"; return this; }); // filter - predicate or array index infix("[", operators['['], function (left) { if(node.id === "]") { // empty predicate means maintain singleton arrays in the output var step = left; while(step && step.type === 'binary' && step.value === '[') { step = step.lhs; } step.keepArray = true; advance("]"); return left; } else { this.lhs = left; this.rhs = expression(operators[']']); this.type = 'binary'; advance("]", true); return this; } }); var objectParser = function (left) { var a = []; if (node.id !== "}") { for (;;) { var n = expression(0); advance(":"); var v = expression(0); a.push([n, v]); // holds an array of name/value expression pairs if (node.id !== ",") { break; } advance(","); } } advance("}", true); if(typeof left === 'undefined') { // NUD - unary prefix form this.lhs = a; this.type = "unary"; } else { // LED - binary infix form this.lhs = left; this.rhs = a; this.type = 'binary'; } return this; }; // object constructor prefix("{", objectParser); // object grouping infix("{", operators['{'], objectParser); // if/then/else ternary operator ?: infix("?", operators['?'], function (left) { this.type = 'condition'; this.condition = left; this.then = expression(0); if (node.id === ':') { // else condition advance(":"); this.else = expression(0); } return this; }); // tail call optimization // this is invoked by the post parser to analyse lambda functions to see // if they make a tail call. If so, it is replaced by a thunk which will // be invoked by the trampoline loop during function application. // This enables tail-recursive functions to be written without growing the stack var tail_call_optimize = function(expr) { var result; if(expr.type === 'function') { var thunk = {type: 'lambda', thunk: true, arguments: [], position: expr.position}; thunk.body = expr; result = thunk; } else if(expr.type === 'condition') { // analyse both branches expr.then = tail_call_optimize(expr.then); expr.else = tail_call_optimize(expr.else); result = expr; } else if(expr.type === 'block') { // only the last expression in the block var length = expr.expressions.length; if(length > 0) { expr.expressions[length - 1] = tail_call_optimize(expr.expressions[length - 1]); } result = expr; } else { result = expr; } return result; }; // post-parse stage // the purpose of this is flatten the parts of the AST representing location paths, // converting them to arrays of steps which in turn may contain arrays of predicates. // following this, nodes containing '.' and '[' should be eliminated from the AST. var post_parse = function (expr) { var result = []; switch (expr.type) { case 'binary': switch (expr.value) { case '.': var lstep = post_parse(expr.lhs); if (lstep.type === 'path') { Array.prototype.push.apply(result, lstep); } else { result.push(lstep); } var rest = post_parse(expr.rhs); if(rest.type !== 'path') { rest = [rest]; } Array.prototype.push.apply(result, rest); result.type = 'path'; break; case '[': // predicated step // LHS is a step or a predicated step // RHS is the predicate expr result = post_parse(expr.lhs); var step = result; if(result.type === 'path') { step = result[result.length - 1]; } if (typeof step.group !== 'undefined') { throw { code: "S0209", stack: (new Error()).stack, position: expr.position }; } if (typeof step.predicate === 'undefined') { step.predicate = []; } step.predicate.push(post_parse(expr.rhs)); break; case '{': // group-by // LHS is a step or a predicated step // RHS is the object constructor expr result = post_parse(expr.lhs); if (typeof result.group !== 'undefined') { throw { code: "S0210", stack: (new Error()).stack, position: expr.position }; } // object constructor - process each pair result.group = { lhs: expr.rhs.map(function (pair) { return [post_parse(pair[0]), post_parse(pair[1])]; }), position: expr.position }; break; default: result = {type: expr.type, value: expr.value, position: expr.position}; result.lhs = post_parse(expr.lhs); result.rhs = post_parse(expr.rhs); } break; case 'unary': result = {type: expr.type, value: expr.value, position: expr.position}; if (expr.value === '[') { // array constructor - process each item result.lhs = expr.lhs.map(function (item) { return post_parse(item); }); } else if (expr.value === '{') { // object constructor - process each pair result.lhs = expr.lhs.map(function (pair) { return [post_parse(pair[0]), post_parse(pair[1])]; }); } else { // all other unary expressions - just process the expression result.expression = post_parse(expr.expression); // if unary minus on a number, then pre-process if (expr.value === '-' && result.expression.type === 'literal' && isNumeric(result.expression.value)) { result = result.expression; result.value = -result.value; } } break; case 'function': case 'partial': result = {type: expr.type, name: expr.name, value: expr.value, position: expr.position}; result.arguments = expr.arguments.map(function (arg) { return post_parse(arg); }); result.procedure = post_parse(expr.procedure); break; case 'lambda': result = {type: expr.type, arguments: expr.arguments, signature: expr.signature, position: expr.position}; var body = post_parse(expr.body); result.body = tail_call_optimize(body); break; case 'condition': result = {type: expr.type, position: expr.position}; result.condition = post_parse(expr.condition); result.then = post_parse(expr.then); if (typeof expr.else !== 'undefined') { result.else = post_parse(expr.else); } break; case 'block': result = {type: expr.type, position: expr.position}; // array of expressions - process each one result.expressions = expr.expressions.map(function (item) { return post_parse(item); }); // TODO scan the array of expressions to see if any of them assign variables // if so, need to mark the block as one that needs to create a new frame break; case 'name': result = [expr]; result.type = 'path'; break; case 'literal': case 'wildcard': case 'descendant': case 'variable': case 'regex': result = expr; break; case 'operator': // the tokens 'and' and 'or' might have been used as a name rather than an operator if (expr.value === 'and' || expr.value === 'or' || expr.value === 'in') { expr.type = 'name'; result = post_parse(expr); } else if (expr.value === '?') { // partial application result = expr; } else { throw { code: "S0201", stack: (new Error()).stack, position: expr.position, token: expr.value }; } break; default: var code = "S0206"; /* istanbul ignore else */ if (expr.id === '(end)') { code = "S0207"; } throw { code: code, stack: (new Error()).stack, position: expr.position, token: expr.value }; } return result; }; // now invoke the tokenizer and the parser and return the syntax tree lexer = tokenizer(source); advance(); // parse the tokens var expr = expression(0); if (node.id !== '(end)') { throw { code: "S0201", stack: (new Error()).stack, position: node.position, token: node.value }; } expr = post_parse(expr); return expr; }; // Start of Evaluator code var staticFrame = createFrame(null); /** * Check if value is a finite number * @param {float} n - number to evaluate * @returns {boolean} True if n is a finite number */ function isNumeric(n) { var isNum = false; if(typeof n === 'number') { var num = parseFloat(n); isNum = !isNaN(num); if (isNum && !isFinite(num)) { throw { code: "D1001", value: n, stack: (new Error()).stack }; } } return isNum; } /** * Returns true if the arg is an array of numbers * @param {*} arg - the item to test * @returns {boolean} True if arg is an array of numbers */ function isArrayOfNumbers(arg) { var result = false; if(Array.isArray(arg)) { result = (arg.filter(function(item){return !isNumeric(item);}).length === 0); } return result; } // Polyfill /* istanbul ignore next */ Number.isInteger = Number.isInteger || function(value) { return typeof value === "number" && isFinite(value) && Math.floor(value) === value; }; /** * Evaluate expression against input data * @param {Object} expr - JSONata expression * @param {Object} input - Input data to evaluate against * @param {Object} environment - Environment * @returns {*} Evaluated input data */ function evaluate(expr, input, environment) { var result; var entryCallback = environment.lookup('__evaluate_entry'); if(entryCallback) { entryCallback(expr, input, environment); } switch (expr.type) { case 'path': result = evaluatePath(expr, input, environment); break; case 'binary': result = evaluateBinary(expr, input, environment); break; case 'unary': result = evaluateUnary(expr, input, environment); break; case 'name': result = evaluateName(expr, input, environment); break; case 'literal': result = evaluateLiteral(expr, input, environment); break; case 'wildcard': result = evaluateWildcard(expr, input, environment); break; case 'descendant': result = evaluateDescendants(expr, input, environment); break; case 'condition': result = evaluateCondition(expr, input, environment); break; case 'block': result = evaluateBlock(expr, input, environment); break; case 'regex': result = evaluateRegex(expr, input, environment); break; case 'function': result = evaluateFunction(expr, input, environment); break; case 'variable': result = evaluateVariable(expr, input, environment); break; case 'lambda': result = evaluateLambda(expr, input, environment); break; case 'partial': result = evaluatePartialApplication(expr, input, environment); b