以编程方式重命名函数

时间:2014-07-20 17:55:03

标签: javascript compiler-optimization

我目前正在编写一个ECMAScipt5编译器,它在解析树上执行各种给定的优化/转换,并编译回ECMAScipt5。

一项功能是在 EnvironmentRecord 中重命名 Binding

这种转换可以自动执行,例如作为旨在减少代码大小的优化的一部分,其中每个变量(不在全局范围内)将被赋予下一个最短可用名称,或者在引入新范围的语句之后通过注释手动。

但是,我必须将(自动)过程限制为变量声明。

考虑这两个例子。第一个是编译的,指定[Minify]为转换,第二个使用[Directives, PrettyPrint]

语法:Compiler.fromSource (src).compile ([/*Array of transformations*/]);

var bar,foo;
(function exampleMin () {
    var bar="foo",
        foo="bar";

    function fooBar () {
        return foo + bar;
    }
})

编译到

var bar,foo;function exampleMin(){var A="foo",B="bar";function fooBar(){return B+A}}

var bar,foo;
(function exampleMin () {
    @Rename bar:A
    @Rename foo:B
    @Rename fooBar:C
    var bar="foo",
        foo="bar";

    function fooBar () {
        return foo + bar;
    }
})

编译到

var bar,foo;
function exampleMin(){
     var A="foo",B="bar";
     function C(){
          return B+A;
     }
};

导致问题部分,功能...考虑以下

if (fooBar.name === 'fooBar') {
 //...
}

现在,如果此声明将包含在exampleMin中。用户定义的重命名会将代码转换为语义上不同的代码。这不可能通过自动执行的转换发生。

虽然我盲目地假设用户定义的函数重命名并没有以某种方式改变语义,但是如果可能的话,我想发出警告。但我不知道如何确定以编程方式重命名函数是否安全

这让我想到了问题:

  1. 在重命名函数时,除了访问函数名称外,还需要考虑什么?
  2. 必须执行哪些分析才能将函数标记为可安全优化或不安全。是否有可能。
  3. 我宁愿将函数排除在重命名之外,还是会尝试更改其他方面。与函数名称的比较。 (如果可以证明它没有副作用)
  4. 在这种特定情况下,语义的变化是否可以容忍GCC似乎这么认为),如果我作为交换,提供@do-not-optimize注释?

  5. 更新1。

    我得出的结论是,这种分析可能无法单独通过静态分析

    请考虑以下代码。

    function foo () {}
    function bar () {}
    var fns = [bar,foo];
    
    if (fns [0].name === 'bar') fns [0] ();
    
    fns.unshift (foo);
    
    if (fns [1].name === 'bar') fns [1] ();
    

    我无法想象一旦将函数添加到数组中,如何在不执行代码的情况下跟踪引用返回到它的原点。也许我需要某种形式的抽象解释 1


    UPDATE2

    在平均时间和阅读@Genes回答之后,我意识到还有其他一些事情可能不会被添加。首先,一些附注:

    • 显然我不是在编写编译器,而是编写预处理器,因为它输出的是源代码而不是机器代码。
    • 鉴于只有绑定标识符的静态访问,我对如何处理问题有一个好主意。
    • 每个环境记录中的每个绑定目前都包含一个列表,其中包含所有静态引用(我显然无法添加动态

    我目前正在进行SSA [2] 转换。所以我还没有实施任何DataFlow分析。但那是关于计划的。

    因此,为简单起见,我们假设只满足以下先决条件。

    • AST和CFG采用静态单一分配形式。
    • 已为CFG中的每个节点计算GEN和KILL集 4
    • 达到定义 4 / IN和OUT集已计算完毕。
    • 计算了DEF / USE对
    • 流依赖边已添加到CFG

      所以第一个例子的控制流图可能看起来像这样。

    enter image description here

    • 黑色非虚线表示控制流边缘。
    • 黑色虚线连接表示数据流依赖性
    • 蓝色双箭头线表示呼叫站点。
    • 蓝色虚线表示过程间依赖关系。 我不知道我是否应该在每个预定的相应节点之间建立直接连接CFG

    鉴于此,我可以简单地执行以下操作。

    对于即将重命名的每个函数:

    • 访问其声明CFG节点
    • 对于每个流依赖性边缘,请访问目标节点
    • 如果该节点是条件goto语句,并且函数引用是属性访问器的LHS,其中RHS是" name"。
      • 将该功能标记为污染

    唯一的问题是我无法看到如何为函数的非静态引用计算(甚至近似)该信息

    Soo,如果该分析没有帮助找到对函数的 ALL 引用,我也可以使用前面提到的引用列表,每个 Binding 环境记录成立。 由于函数具有声明性环境记录 以及 对象环境记录。我可以简单地看一下它的对象环境的引用计数" name" 结合

    作为参考,这是当前执行重命名的实际代码

    var boundIdentifiers = this.environment.record.bindings, //`this` refers to an AST node representing a FunctionDeclaration or a FunctionExpression
        nextName, 
        identifier, 
        binding;
    
    for (identifier in boundIdentifiers) {
        binding = boundIdentifiers [identifier];
        if (binding.uses < 2 && !binding.FunctionExpression) {
            compiler.pushWarning (binding.references [0].line, binding.references [0].column,'Declared function ' + identifier + ' is never called.') //False positive if the functions reference is obtained dynamically
        }
    
        if (boundIdentifiers [identifier].FunctionDeclaration || boundIdentifiers [identifier].FunctionExpression) {
            continue; //Skip function declarations and expressions, since their name property could be accessed
        }
    
        do {
            nextName = nextVar (); 
        } while (
            Object.hasOwnProperty.call (boundIdentifiers,nextVar) //There could exist a `hasOwnProperty` binding.
        ); //ther could a with the name that already exists in the scope. So make sure we have assign a free name.
    
        this.environment.record.setBindingName (identifier, nextName);
    }
    

    所以整体问题归结为捕捉非静态参考

    需要使用哪些分析技术和先前的优化来捕获至少一些(因为它不可能捕获所有),非静态引用。


    我修改了问题以适应更新。因此,上述问题仍然适用

    [1] A Survey of Static Program Analysis Techniques (CH:2) [2] Static Single Assignment Book [4] Representation and Analysis of Software


    作为评论中提到的 @didierc ,使用括号表示法对属性访问产生了同样的问题。因此,对象环境记录 Bindings 只能手动重命名。

3 个答案:

答案 0 :(得分:2)

我认为您需要打破.name属性以返回原始名称而不是新名称。没有别的办法。

考虑用._name()替换所有.name,并构建ref-&gt; name的查找表。

答案 1 :(得分:2)

问题不在于重命名函数是不安全的,问题在于代码依赖于&#34; name&#34;无论您正在考虑重命名,函数的属性都是不安全的。

考虑一个功能对象的名称&#39;属性未在ecma标准中定义,并且某些浏览器不会实现它。在JavaScript中,函数可以是无名的,或者可能有多个名称,因此在代码中依赖于特定于浏览器的名称属性是问题,而不是重命名函数的概念。

答案 2 :(得分:1)

假设你不能像@Phil H那样“打破”翻译,如果可能的话,这是一个有效的解决方案......

编译器处理这种情况的正常方式称为数据流分析。这相当于以某种方式描述程序结构的代码行走计算值。在所有有趣的案例中,这些值都是对流估计值。

最普遍的保守假设是,没有人知道哪个if分支将执行,循环迭代的次数也是未知的,并且没有任何关于函数调用可能做什么的知道。副作用的条款。复杂的过程间数据流分析允许丢弃最后一个。例如。 LLVM能够做到这一点,但许多其他编译系统都没有。

这个问题的特殊保守假设是,如果对某个值使用.name或类似的内省,那么它就会被“污染”。它无法重命名。通过数据流分析,您可以找到受保护的受污染名称列表。

对于这个问题,可能最适用的数据流分析是def-use分析,以构建附加到每个“定义”的“使用链”。在javascript中,定义等同于assingment和函数命名。如果您的分析足够复杂以查看内部哈希值,那么添加键值对就是def。

分析结果将附加到该定义中确定的值的每个“使用”的每个定义的列表。保守的假设意味着该列表可能包括从未实际发生的用途。

关于数据流分析的文献很多。但是开始学习它的一个很好的方法是旧的标准“龙书”,Aho Sethi和Ullman,编译器设计

<强>加成

非常动态的语言存在的问题是准确的数据流分析很难。评论中的例子:

var n='name';
function foo () {}; 
if (foo[n] === 'foo') doSth ()

是典型的。愚蠢的保守假设是,在任何索引操作a[s]中,s可能是'name'。因此无法重命名a。您必须通过使数据流分析更加详细 - 而不是估计值来克服愚蠢假设的限制。

这个框架是abstract interpretation:实际运行程序时,抽象值域替换实际数据,再次对if和循环做出保守假设。如果抽象域具有某些属性,则保证有限长度执行以计算每个变量的最终值。常数折叠是一种简单的抽象解释形式。

在您的示例中,抽象值域必须至少包含类似

的内容
{ unassigned, constant string, unknown }

在实践中,您还需要null,常数,函数和其他值。抽象解释术语会说“底部”而不是“未分配”和“顶部”而不是“未知”。

这一切都是旧事。因此,从60年代开始就有大量的正式文献,当时人们对优化lisp编译器非常感兴趣。如果您可以访问,请搜索ACM存档。