/// /// require('source-map-support').install(); var falafel = require("falafel"); var XRegExp = require('xregexp').XRegExp; import Json = require('source-processor'); import error = require('source-processor'); import optimizer = require('../src/optimizer'); import globals = require('../src/globals'); //todo //implement explicit types rather than ad hoc string export class Function { identifier: string; //the function name signature: string; //the function name, and the number of params, e.g. f(x) signature is f(0) parameter_map: {[pos:number]:string;};//maps the position of the param to a string value expression: Expression; static DECLARATION_FORMAT = XRegExp( '(?\\w+) \\s* # name is alphanumeric\n'+ '\\( \\s* #open brace for function args \n'+ '(? (\\w+\\s*(,\\s*\\w+\\s*)*)?) #comma seperated list of params\n'+ '\\) \\s* #close brace for function args \n', 'gx'); constructor(declaration: string, expression: Json.JString) { //break the function declaration into its parts var match = XRegExp.exec(declaration, Function.DECLARATION_FORMAT); var params = XRegExp.split(match.paramlist, /\s*,\s*/); //bug fix for weird split behaviour (preserved in XRegExp) //if the declaration has no parameters, the "paramlist" gets split as [''] for some reason if(params.length==1 && params[0] == '') params = []; //convert it to empty list as it should be this.identifier = match.name; this.signature = match.name + "(" + params.length + ")"; //now build up positional information of params this.parameter_map = params; this.expression = Expression.parseUser(expression); } static parse(json: Json.JValue): Function { //console.log("Function.parse:", json); var fun: Function; //there should only be one entry json.asObject().forEach(function(key: Json.JString, val: Json.JValue) { fun = new Function(key.asString().value, val.coerceString()); }); return fun } } /** * instances support indexing like arrays, which maps cannocal function declarations to Function definitions */ export class Functions { [index: string]: Function; static parse(json: Json.JValue): Functions{ //console.log("Functions.parse:", json); var functions = new Functions(); if (json == null) return functions; json.asArray().forEach(function(val: Json.JValue) { var fun: Function = Function.parse(val); functions[fun.identifier] = fun; }); return functions } } export class Symbols{ functions: {[index: string]: Function} = {}; variables: {[index: string]: any} = {}; //string->AST node clone():Symbols{ var clone: Symbols = new Symbols(); for(var p in this.functions) clone.functions[p] = this.functions[p]; for(var v in this.variables) clone.variables[v] = this.variables[v]; return clone; } loadFunction(functions: Functions) { for(var identifier in functions){ this.functions[identifier] = functions[identifier]; } } } export class Expression{ raw: string; source: Json.JString; //null source means the compiler generated it static FALSE: Expression = new Expression("false", null); static TRUE: Expression = new Expression("true", null); constructor (raw: string, source: Json.JString) { this.raw = raw; this.source = source; } static parse(raw: string): Expression{ return new Expression(raw, null); } static parseUser(json: Json.JString): Expression{ return new Expression(optimizer.sanitizeQuotes(json.value), json); } /** * changes next and prev references for next.parent() and prev.parent() */ rewriteForChild(): string { var falafel_visitor = function(node) { if(node.type == "Identifier"){ if(node.name == "next"){ node.update("next.parent()"); }else if(node.name == "prev"){ node.update("prev.parent()"); } } }; return falafel(this.raw, {}, falafel_visitor).toString(); } /** * changes next and prev references for next['child_name'] and prev['child_name'] * wildchild's can't be represented in their parents context, so wildchilds are conservatively * represented as "false" */ rewriteForParent(child_name): string{ if(child_name.indexOf("$") == 0) return "false"; //wildchilds can't be pushed up if(child_name.indexOf("~$") == 0) return "true"; //wilderchilds can be pushed up var falafel_visitor = function(node){ if(node.type == "Identifier"){ if(node.name == "next"){ node.update("next['" + child_name +"']"); }else if(node.name == "prev"){ node.update("prev['" + child_name +"']"); } } }; return falafel(this.raw, {}, falafel_visitor).toString(); } expandFunctions(symbols: Symbols): string { var self: Expression = this; var falafel_visitor = function(node){ //console.log("type:", node.type); if(node.type == "Identifier"){ if(symbols.functions[node.name]){ node.expr_type = "pred" }else if(symbols.variables[node.name]){ node.update(symbols.variables[node.name].source()); node.expr_type = symbols.variables[node.name].expr_type } } else if(node.type == "CallExpression"){ if(node.callee.expr_type === "pred"){ //console.log(node) //we are calling a user defined function var fun: Function = symbols.functions[node.callee.name]; //clone the global symbol table and populate with binding to parameters var function_symbols = symbols.clone(); var params = node.arguments; for(var p_index in params){ var p_node = params[p_index]; var local_name = fun.parameter_map[p_index]; function_symbols.variables[local_name] = p_node; } var expansion = fun.expression.expandFunctions(function_symbols); node.update("(" + expansion + ")"); node.expr_type = "value"; } } }; var code: string = falafel(this.raw, {}, falafel_visitor).toString(); return optimizer.simplify(code); } generate(symbols: Symbols): string { var self: Expression = this; //the falafel visitor function replaces source with a different construction var falafel_visitor = function(node){ //console.log("type:", node.type); if(node.type == "Identifier"){ //console.log("identifier: ", node.name); if(node.name == "data"){ node.expr_type = "rule" }else if(node.name == "newData"){ node.expr_type = "rule" }else if(node.name == "next"){ node.update("newData"); node.expr_type = "rule" }else if(node.name == "prev"){ node.update("data"); node.expr_type = "rule" }else if(node.name == "root"){ node.expr_type = "rule" }else if(node.name.indexOf("$")==0){ node.expr_type = "value" }else if(node.name == "auth"){ node.expr_type = "map" }else if(symbols.functions[node.name]){ node.expr_type = "pred" }else if(symbols.variables[node.name]){ var label = node.name; node.update(symbols.variables[node.name].source()); node.expr_type = symbols.variables[node.name].expr_type node.label = label; } }else if(node.type == "Literal"){ //console.log("literal: ", node.value); if ((typeof node.value == 'string' || node.value instanceof String) && node.raw.indexOf('/') == 0){ node.expr_type = "regex"; } else { node.expr_type = "value"; } }else if(node.type == "ArrayExpression"){ //console.log("ArrayExpression", node); node.expr_type = "value"; }else if(node.type == "MemberExpression"){ //console.log("MemberExpression:", node); //if the object is a type (rules, map or value) it unlocks different valid properties if(node.object.expr_type == "rule"){ node.expr_type = null; if(node.property.type == 'Identifier'){ //reserved methods if(node.property.name == 'val'){ node.expr_type = "fun():value" }else if(node.property.name == 'parent'){ node.expr_type = "fun():rule" }else if(node.property.name == 'hasChildren'){ node.expr_type = "fun(array):value" }else if(node.property.name == 'hasChild'){ node.expr_type = "fun():value" }else if(node.property.name == 'isString'){ node.expr_type = "fun():value" }else if(node.property.name == 'isBoolean'){ node.expr_type = "fun():value" }else if(node.property.name == 'isNumber'){ node.expr_type = "fun():value" }else if(node.property.name == 'exists'){ node.expr_type = "fun():value" }else if(node.property.name == 'now'){ node.expr_type = "value" }else if(node.property.name == 'getPriority'){ node.expr_type = "fun():value" }else if(node.property.expr_type == 'rule'){ //not a recognised //cooertion from rule to value node.update(node.object.source() + ".child(" + node.property.source() + ".val())"); node.expr_type = "rule" }else if(node.property.expr_type == 'value'){ //not recognised member, so it must be an implicit child relation (without quotes in child) if (node.property.label && !isArraySyntaxMemberExpression(node)) { //data.a should stay as property.a, we don't want to use variable substitution in the property //so we use the orginal label node.update(node.object.source() + ".child('" + node.property.label + "')"); } else { //data[a] should stay as property.a, we don;t want to use variable substitution in the property //so we use the orginal label node.update(node.object.source() + ".child(" + node.property.source() + ")"); } node.expr_type = "rule" }else{ //not recognised member, so it must be an implicit child relation (with quotes in child) node.update(node.object.source() + ".child('" + node.property.source() + "')"); node.expr_type = "rule" } }else if(node.property.expr_type == 'rule'){ //not a recognised //cooertion from rule to value node.update(node.object.source() + ".child(" + node.property.source() + ".val())"); node.expr_type = "rule" }else if(node.property.expr_type == 'value'){ //not recognised member, so it must be an implicit child relation (without quotes in child) node.update(node.object.source() + ".child(" + node.property.source() + ")"); node.expr_type = "rule" } }else if(node.object.expr_type == "map"){//auth is a map, auth.* always goes to a value node.expr_type = "value" }else if(node.object.expr_type == "value"){ //inbuild methods for values node.expr_type = "value"; //inbuilt methods for value objects if(node.property.type == 'Identifier'){ if(node.property.name == 'contains'){ node.expr_type = "fun(value):value" }else if(node.property.name == 'replace'){ node.expr_type = "fun(value,value):value" }else if(node.property.name == 'toLowerCase'){ node.expr_type = "fun():value" }else if(node.property.name == 'toUpperCase'){ node.expr_type = "fun():value" }else if(node.property.name == 'beginsWith'){ node.expr_type = "fun():value" }else if(node.property.name == 'endsWith'){ node.expr_type = "fun():value" }else if(node.property.name == 'matches'){ node.expr_type = "fun(regex):value" } } } if(node.expr_type == null){ throw error.message("Bug: 3 Unexpected situation in type system " + node.object.expr_type + " " + node.property.expr_type).source(self.source).on(new Error()); } }else if(node.type == "CallExpression"){ if(node.callee.expr_type === "fun():rule"){ node.expr_type = "rule"; }else if(node.callee.expr_type === "fun():value"){ node.expr_type = "value"; }else if(node.callee.expr_type === "fun(array):rule"){ node.expr_type = "value"; }else if(node.callee.expr_type === "fun(value):value"){ node.expr_type = "value"; }else if(node.callee.expr_type === "fun(array):value"){ node.expr_type = "value"; }else if(node.callee.expr_type === "fun(regex):value"){ node.expr_type = "value"; }else if(node.callee.expr_type === "fun(value,value):value"){ node.expr_type = "value"; }else if(node.callee.expr_type === "pred"){ //console.log(node) //we are calling a user defined function var fun: Function = symbols.functions[node.callee.name]; //clone the global symbol table and populate with binding to parameters var function_symbols = symbols.clone(); var params = node.arguments; for(var p_index in params){ var p_node = params[p_index]; var local_name = fun.parameter_map[p_index]; function_symbols.variables[local_name] = p_node; } var expansion = fun.expression.generate(function_symbols); node.update("(" + expansion + ")"); node.expr_type = "value"; }else{ if (node.callee.type == 'MemberExpression' && node.callee.property.type == 'Identifier' && node.callee.property.name == 'child') { throw error.message("parent.child() syntax is not supported in Blaze, use parent. or parent[] instead").source(self.source).on(new Error()); } else { var uncoerced = [ 'contains', 'toLowerCase', 'replace', 'toUpperCase', 'beginsWith', 'endsWith', 'matches' ]; if (node.callee.property != null && uncoerced.indexOf(node.callee.property.name) >=0 ) { throw error.message("Unexpected situation in type system on '" + node.source() + "', (you probably forgot to use .val() before using an inbuilt method)").source(self.source).on(new Error()); } else { throw error.message("Bug: 4 Unexpected situation in type system on '" + node.callee.expr_type + "'").source(self.source).on(new Error()); } } } }else if(node.type == "BinaryExpression" || node.type == "BooleanExpression" || node.type == "LogicalExpression"){ //coersion to value, if a rule (i.e. appending .val() when in a binary operator as ruleSnapshots don't support any binary operators) if(node.left.expr_type === "rule"){ node.left.update(node.left.source() + ".val()"); } if(node.right.expr_type === "rule"){ node.right.update(node.right.source() + ".val()"); } node.expr_type = "value" }else if(node.type == "UnaryExpression"){ node.expr_type = node.argument.expr_type; }else if(node.type == "ExpressionStatement"){ }else if(node.type == "Program"){ }else{ throw error.message("Bug: 5 Unrecognised Type In Expression Parser: " + node.type).source(self.source).on(new Error()); } }; var code: string = falafel(this.raw, {}, falafel_visitor).toString(); return globals.optimize ? optimizer.optimizeAndTrim(code): optimizer.simplify(code); } } /** * figures out whether this is data.a or data[a] syntax * @param node */ function isArraySyntaxMemberExpression (node): boolean { return node.source().slice(node.object.source().length).trim().charAt(0) == '['; }