王垠,请别再欺负我们读书少

7,408 阅读8分钟
原文链接: zhuanlan.zhihu.com

昨天知乎里又一次因为王垠的新文章《我为什么不再做PL人》热闹了一把,之前每次评论他我都没有参与过。因为说实话王垠发展到今天的地步我觉得真的很遗憾,而他的言论曾经对我是有意义的。几年前我通过他的文章也学了一些东西,知道了一些高深的名词,随后我通过一系列的学习、训练让这些名词在我这里不再高深。然而就在我逐渐完成『从工具的奴隶到工具的主人』这个过程时,当初那个高调宣称要打破一切权威迷信的人如今却要把自己塑造成一个更不可置疑的权威。

从多篇文章中(包括这篇)能看出来,王垠最引以为傲的事情就是他做出了『世界上最先进的分析器Pysonar2』。然后为了维护这个像三个代表一样不可置疑的『先进性』,他曾经多次在不同的地方把控制流分析(control flow analysis)贬得一文不值:

所以当你看透了所有这些,就会发现PL的学术界,其实反反复复在解决一些早已经解决了的问题。有时候好几个子领域,其实解决的是同一个问题。然而每一个子领域的人,却都说自己的问题在本质上是不一样的,然后号称自己是那个子领域的鼻祖。甚至有人在20多年的时间里,制造出一代又一代的PhD和教授职位。他们的理论一代一代的更新,最后却无法解决实际的问题。所谓的“控制流分析”(control-flow analysis,CFA),就是这样的一个子领域。

不懂的人可能只是把这个对CFA 的污蔑一眼带过而已,并没有想清楚为什么静态分析领域那么多热点他不喷而专挑控制流这块喷得乐此不疲。PL 这个领域确实有个大问题,就是人太少!外行人更是没人敢置疑这个『先进性』。直到现在很多人还认为其实他很有能力只是性格问题(在知乎的关于他新文章的讨论里随处可见)。那么我今天就来带大家看看Pysonar2 到底是个什么样的水平;以及为什么王垠要为了维护Pysonar2 的地位而疯狂污蔑控制流分析技术。

先给没兴趣看干货的人直接放结论:动态语言的类型推导(concrete type inference)问题可以很容易的被控制流分析实现。然而曾经的控制流分析有个缺陷,所以王垠发明了一种『错误』的方法在Pysonar2 中部分解决了这个缺陷。但是以今天控制流分析技术的角度来看Pysonar2 实在太low了。

Pysonar2 官方的说法是Python 的类型推导器(type inferencer)和索引器(indexer)。关于『动态类型语言的类型推导』的原理我在如何系统的学习动态语言的类型推导,类型系统等知识? - 彭飞的回答 中说了一半,在最关键的地方停下来了(因为当时静态分析还没学好……)。其实给动态语言做Pysonar2那种类型推导很简单,就是用控制流分析(control flow analysis)。Control flow analysis 的核心思想是用模拟程序运行的方式跟踪函数(闭包)在程序中的流动,比如说 ((lambda (x) x) (lambda (y) y)) 这个程序的分析过程是:我们知道这是一个函数调用(application),所以实参(含有y 的id function)要流入形参(x),然后含有x 的id function 的函数体在知道x -> (lambda (y) y) 的情况下运行,因为函数体只是一个简单的变量提取,所以 (lambda (y) y) 流出这个函数调用表达式继续流向外围程序直至停机,最后得到的结果是x 指向 (lambda (y) y)。在这里,我们所追踪的『函数(闭包)』被称为抽象值(Absract Value),这个abstract value 也可以是其他的东西,比如简单地换成『类型(type)』基本就得到了一个类型推导器。所以说动态语言的类型推导就是要追踪类型在程序中的流动。换句话说类型流分析是控制流分析的一个子问题。这方面的研究很早就有了,感兴趣的人可以随便Google 一下,论文很多(比如我导师的导师Jens Palsberg 写的《A Type System Equivalent to Flow Analysis》以及后续的一系列文章,再比如CFA2 的发明人曾在Mozilla 实习的时候用控制流分析方法做了一个比Pysonar2 好得多的Javascript类型推导器JavaScript Type Inference)。


有人可能会问既然用控制流分析就可以解决类型分析,那为什么王垠要再做一个Pysonar2呢?原因是当时的控制流分析方法太弱(这句话有点不太公平,因为当时CFA2 已经出现,虽然以今天的眼光来看CFA2 就是个辣鸡,但对比Pysonar2 它起码是正确(sound)的方法)。

下图是王垠做的Pysonar 原理的幻灯片,其中这个Recursion Detection 就是他所说的Pysonar 最与众不同的核心技术:
明白人一看就知道,其实他想要解决的问题就是call/return mismatch(王垠在喷别人重新发明概念的同时自己也在做相同的事)。但是他的解决方法是一种针对类型推导所特定的方法,这种方法看上去很高效,但实际上代码冗长而丑陋并且不能保证给出正确的结果(unsound,这点王垠自己也一笔带过地承认了)。

那么在控制流分析中call/return mismatch 这个问题为什么很难解决呢?因为传统的k-CFA 控制流分析方法是把程序的无限状态空间抽象成一个有限的『抽象状态空间』然后用一个有限状态自动机来处理。然而由于函数、异常、call/cc 等跨过程的控制流结构的存在,实际程序的状态存在递归(无限但有规律),所以正确方法应该是用一个下推自动机来处理。这个问题在数据流分析领域很多年前就被人注意到了,Reps 大神的IFDS/IDE 就是用下推自动机的思路来解决一小部分跨过程数据流分析中函数会乱返回(call/return mismatch)的问题。随后控制流分析界也逐步开始下推控制流分析(Pushdown Control Flow Analysis,或者叫Context-Free Language Reachability)方法的研究,从CFA2 到PDCFA 再到AAC 直至牛逼到爆的P4F。对于控制流分析来说,类型推导问题早就是小菜一碟了,不仅理论简单优美、程序实现容易,运行效率也一直在突破着一个个渐进复杂度(大O)的屏障。

以上是理论部分,下面我们再通过几个例子来扒些Pysonar2 的缺陷(其中前两点缺陷才是动态语言类型推导目前的命门所在,而Pysonar2 根本没有考虑):

1. Pysonar2 宣称推导出的是『最强大的』Intersection Type,实际上它只用了Union Type 而已。而事实上Union Type 是一种很直观也很无奈的选择,因为控制流分析方法做的类型推导是从类型的使用推断类型的定义,所以Pysonar2 做的是concrete type inference 而不是Haskell 那中polymorphic type inference。也就是说它可以给出{int -> int} | {bool ->bool} 这样的类型,却不能准确地猜到它是forall a. a -> a ; 如图

2. 这种『从类型的使用推断类型的定义』方法还有一个缺点,就是不能发现所有的类型错误,因为错误的类型使用最后会传播到错误的类型定义。比如下面这个例子:

函数f 的第一次调用 f(A()) 是一个明显的类型错误,但Pysonar2 根本无力发现。

3. 上一个例子中,还可以看出一个错误,就是『a.x』中的x 只指向了Class A 而没有指向Class B。这个原因是,作者根本没有理解静态分析的本质,上面提到说抽象值(Abstract Value)可以是函数、闭包或类型等等,但实际上更准确的说法是『抽象值是函数、闭包或类型的集合』。因为静态分析要对程序的运行行为做approximate,即它不是找出程序的运行结果(因为做不到),而是要找出程序运行结果的所有可能性。

4. 我还发现了Pysonar2 中的一个看似很严重的Bug,但实际上这只是一个设计上的小失误而已。最后我来告诉大家这个Bug 应该怎么改。如下程序示例:

可以看出fib 函数被推导出的类型是 int -> int,但奇怪的是函数体内却又不知道参数n 的类型了(因为这个函数没有被调用),而且每一次n 出现的位置得到的结果还不一样(汗……)。这是因为Pysonar2 把每一个局部变量独立看待了,正确的做法应该是在分析之前在整个AST 上先做一次语义分析(semantic analysis),把每个变量的访问和它的定义联系起来,这样同一个变量在不同位置得到的分析结果就可以自然地互相传播。