javascript - 以编程方式重命名功能

标签 javascript compiler-optimization

我目前正在编写ECMAScipt5编译器,该编译器在解析树上执行各种给定的优化/转换,然后编译回ECMAScipt5。
一种功能是重命名EnvironmentRecord中的绑定(bind)。
该变换可以例如自动执行。作为旨在减少代码大小的优化的一部分,将为每个变量(不在全局范围内)提供下一个最短的可用名称,或者在引入新范围的语句后通过注释手动为其命名。
但是,我必须将(自动)过程限制为变量声明。
考虑这两个例子。第一个经过编译,将[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中。用户定义的重命名会将代码转换为语义上不同的代码。自动执行的转换绝对不能发生这种情况。
尽管我盲目地假设用户定义的函数重命名不会以某种方式更改语义,但是如果可能的话,我想发出一个警告。但是我不知道如何确定以编程方式重命名是否安全。
这使我想到了以下问题:
  • 重命名功能时,除了访问功能名称外还需要考虑什么?
  • 必须执行什么分析才能将功能标记为可以安全优化或不能安全优化。有没有可能。
  • 我宁愿将函数排除在重命名之外,还是尝试更改例如与功能名称的比较。 (如果也可以证明没有副作用)
  • 在这样的特定情况下,如果我提供一个@do-not-optimize注释作为交换,在语义上的变化是否可以容忍(GCC似乎是这样认为的)?

  • 更新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

    更新2
    同时,在阅读@Genes答案后,我意识到还有其他一些事情可能不会增加。首先,一些注意事项:
  • 显然我不是在编写编译器,而是在编写预处理器,因为它输出源代码而不是机器代码。
  • 鉴于绑定(bind)标识符将只有静态访问,因此我对如何解决该问题有一个好主意。
  • 每个环境记录中的每个Binding当前都拥有一个列表,该列表包含了其所有静态引用(我显然不能添加动态引用)。

    我目前正在从事SSA [2]转换。所以我还没有实现任何DataFlow分析。但这已在计划中。
    因此,为简单起见,我们假设满足以下先决条件。
  • AST和CFG处于“静态单一分配”形式。
  • 已为CFG 4中的每个节点计算了GEN和KILL集
  • 到达定义4 / IN和OUT集已计算出来。
  • 已计算
  • DEF / USE对
  • 流依赖性边已添加到CFG
    因此,第一个示例的控制流图可能看起来像这样。
  • 黑色非虚线表示控制流边缘。
  • 黑色虚线连接表示数据流依赖性
  • 蓝色双箭头线表示 call 站点。
  • 蓝色虚线表示过程间相关性。但是我不确定是否应该在每个步骤CFG
  • 的相应节点之间建立直接连接

    鉴于此,我可以简单地执行以下操作。
    对于将要重命名的每个功能:
  • 访问其声明CFG节点
  • 对于每个流依赖性边缘,请访问目标节点
  • 如果该节点是条件goto语句,并且函数引用是RHS为“名称”的属性访问器的LHS。
  • 将该函数标记为已污染


  • 唯一的问题是我看不到如何为函数的非静态引用计算(甚至近似)该信息。
    太好了,如果这种分析不能帮助您找到函数的所有引用,那么我也可以使用前面提到的引用列表,即环境记录中的每个Binding都具有。
    由于函数具有声明性环境记录,而具有对象环境记录。我可以简单地看一下其对象环境“名称”绑定(bind)的引用计数。
    作为引用,这是当前执行重命名的实际代码
    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一样,使用括号表示法的属性访问也会出现相同的问题。因此,对象环境记录的绑定(bind)只能手动重命名。

    最佳答案

    假设您无法像@Phil H所说的那样“破坏”解释器,如果可能的话,这是一个有效的解决方案...

    编译器处理此类情况的通常方式称为数据流分析。这相当于通过代码遍历计算以某种方式描述程序结构的值。在所有有趣的情况下,这些值都是收敛估计。

    最普遍的保守假设是,对于执行哪个if分支一无所知,对循环进行迭代的次数也一无所知,而函数调用在副作用方面的作用也一无所知。复杂的过程间数据流分析允许删除最后一个。例如。 LLVM可以做到这一点,但许多其他编译系统却不行。

    对于此问题的特殊保守假设是,如果对值使用.name或类似的自省(introspection),则它“被污染”。无法重命名。数据流分析将使您找到保守的受污染名称列表。

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

    分析结果将是一个列表,该列表附加到在该定义期间建立的值的每个“用途”的每个定义。保守的假设意味着该列表可能包含从未真正发生过的用途。

    关于数据流分析,有大量文献。但是,开始学习它的绝妙方法是旧标准的“龙皮书”,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文件。

    关于javascript - 以编程方式重命名功能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24853292/

    相关文章:

    循环中的 C++ 浮点错误与单个表达式中的错误

    c++ - 在 if-else if 链中使用 Likely()/Unlikely() 预处理器宏

    c++ - -如果读取大文件,O2 优化不会跳出循环

    javascript - 如何不制作div堆栈?

    Javascript 将欧元符号转义为 %u2AC 而不是 %u20AC

    javascript - 如何检查ApolloError是否是由客户端网络连接引起的?

    javascript - Nodejs http请求console.log函数打印两次

    c++ - 别名 - clang 优化器害怕什么?

    Javascript:通过innerHTML动态编写HTML

    compiler-errors - gfortran如何处理无法访问的代码?