如何解析纯函数

时间:2013-01-16 08:21:48

标签: javascript functional-programming code-analysis

假设您有以下功能

var action = (function () {

  var a = 42;
  var b = 2;

  function action(c) {
    return a + 4 * b + c;
  }

  return action;
}());

// how would you parse action into it's serialized LISP / AST format?
var parsed = parse(action);

是否可以使用一个引用函数action的函数,并输出LISP格式(lambda (c) (plus (plus 42 (multiply 4 2)) c))

我们可以对action可能的内容加以限制。

  • 身体应该只是一个表达
  • 它应该是一个纯粹的功能
  • 任何自由变量都是常量

主要问题是给出一个可以使用一系列输入调用的函数,它的源代码是否可以找到用自由变量代替的正确值?

对于上面的例子,你知道a和b是常数,你可以智能地绘制输出几个值并查看模式然后只知道常量是什么。

问题:

如何编写一个函数,该函数接受函数引用及其源代码,并为函数生成某种形式的AST,并用任何自由变量替换它们的运行时值。

AST格式的一个例子是代码的LISP等价物。

我基本上想要对函数进行序列化和反序列化,并使其行为相同

应该注意的是,如果将{ a: a, b: b }传递给分析函数,问题就变得微不足道了。那将是作弊。

用例:

我想生成一个纯JavaScript函数的语言无关形式,所以我可以有效地将它传递给C ++,而不需要我的库的用户使用DSL来创建这个函数

让我们假设你有一个数据库驱动程序

var cursor = db.table("my-table").map(function (row) {
  return ["foo", row.foo]
})

您希望在运行时确定函数是什么,并将其转换为AST格式,以便您可以使用高效的查询构建器将其转换为SQL或数据库具有的任何查询引擎。

这意味着你不必写:

var cursor = db.table("my-table").map(function (rowQueryObject) {
    return db.createArray(db.StringConstant("foo"), rowQueryObject.getProperty("foo"))
})

DB库可以使用查询对象执行哪个函数,并且可以构建不带详细方法的查询对象转换。

2 个答案:

答案 0 :(得分:1)

这是一个完整的解决方案(使用可由解析函数访问的变量目录):

var CONSTANTS = { 
   a: 42,
   b: 2,
   c: 4
};

function test() {
    return a + 4 * b + c;
}

function getReturnStatement(func) {
    var funcStr = func.toString();
    return (/return\s+(.*?);/g).exec(funcStr)[1];
}

function replaceVariables(expr) {
    var current = '';
    for (var i = 0; i < expr.length; i += 1) {
        while (/[a-zA-Z_$]/.test(expr[i]) && i < expr.length) {
            current += expr[i];
            i += 1;
        }
        if (isNumber(CONSTANTS[current])) {
            expr = expr.replace(current, CONSTANTS[current]);
        }
        current = '';
    }
    return expr;
}

function isNumber(arg) {
    return !isNaN(parseInt(arg, 10));
}      

function tokenize(expr) {
    var tokens = [];
    for (var i = 0; i < expr.length; i += 1) {
        if (isWhitespace(expr[i])) {
            continue;
        } else if (isOperator(expr[i])) {
            tokens.push({
                type: 'operator',
                value: expr[i]
            });
        } else if (isParentheses(expr[i])) {
            tokens.push({
                type: 'parant',
                value: expr[i]
            });
        } else {
            var num = '';
            while (isNumber(expr[i]) && i < expr.length) {
                num += expr[i];
                i += 1;
            }
            i -= 1;
            tokens.push({
                type: 'number',
                value: parseInt(num, 10)
            });
        }
    }
    return tokens;
}

function toPrefix(tokens) {
    var operandStack = [],
        operatorStack = [],
        current,
        top = function (stack) {
            if (stack) {
                return stack[stack.length - 1];
            }
            return undefined;
        };

    while (tokens.length) {
        current = tokens.pop();
        if (current.type === 'number') {
            operandStack.push(current);
        } else if (current.value === '(' || 
                !operatorStack.length || 
                (getPrecendence(current.value) >
                 getPrecendence(top(operatorStack).value))) {

            operatorStack.push(current);
        } else if (current.value === ')') {
            while (top(operatorStack).value !== '(') {
                var tempOperator = operatorStack.pop(),
                    right = operandStack.pop(),
                    left = operandStack.pop();
                operandStack.push(tempOperator, left, right);
            }
            operatorStack.pop();
        } else if (getPrecendence(current.value) <= 
                getPrecendence(top(operatorStack).value)) {
            while (operatorStack.length &&
                    getPrecendence(current.value) <=
                    getPrecendence(top(operatorStack).value)) {

                tempOperator = operatorStack.pop();
                right = operandStack.pop();
                left = operandStack.pop();
                operandStack.push(tempOperator, left, right);
            }
        }

    }

    while (operatorStack.length) {
        tempOperator = operatorStack.pop();
        right = operandStack.pop();
        left = operandStack.pop();
        operandStack.push(tempOperator, left, right);
    }

    return operandStack;
}

function isWhitespace(arg) {
    return (/^\s$/).test(arg);
}

function isOperator(arg) {
    return (/^[*+\/-]$/).test(arg);
}

function isParentheses(arg) {
    return (/^[)(]$/).test(arg);
}

function getPrecendence(operator) {
    console.log(operator);
    switch (operator) {
        case '*':
            return 4;
        case '/':
            return 4;
        case '+':
            return 2;
        case '-':
            return 2;
        default:
            return undefined;
    }
}

function getLispString(tokens) {
    var result = '';
    tokens.forEach(function (e) {
        if (e)
            switch (e.type) {
            case 'number':
            result += e.value;
            break;
            case 'parant':
            result += e.value;
            break;
            case 'operator':
            result += getOperator(e.value);
            break;
            default:
            break;
        }
        result += ' ';
    });
    return result;
}

function getOperator(operator) {
    switch (operator) {
        case '+':
            return 'plus';
        case '*':
            return 'multiplicate';
        case '-':
            return 'minus';
        case '\\':
            return 'divide';
        default:
            return undefined;
    }
}

var res = getReturnStatement(test);
console.log(res);
res = replaceVariables(res);
console.log(res);
var tokens = tokenize(res);
console.log(tokens);
var prefix = toPrefix(tokens);
console.log(prefix);
console.log(getLispString(prefix));

我刚写了这样的风格可能会有一些问题,但我认为这个想法很清楚。

您可以使用.toString方法获取函数体。之后,您可以使用正则表达式来匹配return语句

(/return\s+(.*?);/g).exec(funcStr)[1];

请注意,您必须使用分号才能成功匹配!在下一步中,使用CONSTANTS对象将所有变量转换为数值(我看到您有一些参数,因此您可能需要进行少量修改)。之后,字符串被标记化,以便于解析。在下一步中,中缀表达式将转换为前缀1。在最后一步,我构建了一个字符串,使输出看起来像你需要的那样(+ - plus- - minus等等。

答案 1 :(得分:0)

由于我不确定在调用方法之后你能够获得该方法的主体,所以这是另一种解决方案:

var a = 42;
var b = 2;

function action(c) {
  return a + 4 * b + c;
}

/**
 * get the given func body
 * after having replaced any available var from the given scope
 * by its *real* value
 */
function getFunctionBody(func, scope) {
  // get the method body
  var body = func.toString().replace(/^.*?{\s*((.|[\r\n])*?)\s*}.*?$/igm, "$1");  
  var matches = body.match(/[a-z][a-z0-9]*/igm);
  // for each potential var
  for(var i=0; i<matches.length; i++) {
    var potentialVar = matches[i];
    var scopedValue = scope[potentialVar];
    // if the given scope has the var defined
    if(typeof scopedValue !== "undefined") {
      // add "..." for strings
      if(typeof scopedValue === "string") {
        scopedValue = '"' + scopedValue + '"';
      }
      // replace the var by its scoped value
      var regex = new RegExp("([^a-z0-9]+|^)" + potentialVar + "([^a-z0-9]+|$)", "igm");
      var replacement = "$1" + scopedValue + "$2";
      body = body.replace(regex, replacement);
    }
  }
  return body;
}

// calling
var actionBody = getFunctionBody(action, this);

// log
alert(actionBody);

打印:

return 42 + 4 * 2 + c;

<强> DEMO

然后,您必须实施自己的function toLISP(body)或您可能需要的任何其他功能。

请注意,它不适用于复杂的范围变量,例如var a = {foo: "bar"}