代码流程分析

来源:互联网 发布:excel03显示重复数据 编辑:程序博客网 时间:2024/06/08 18:46
在larva的语义和流程分析上,我偷了很大的懒,基本等于没做,因为觉得做起来太麻烦了


流程分析可以在一段语法上完全没有问题的代码中,找出可能有问题的代码,这个不同编译器支持程度也不同,其实在绝大多数情况下,不做这个问题也不大,不过larva面对了一个相关的不可逃避的问题,因为它和python一样有一个特性:若一个函数或方法最后没有return,则自动return一个空对象,larva中是nil,python中则是None,比如:
func f():    print "hello world"
f最后应该隐式return nil


根据实现的不同,这个问题也有各种程度的解决办法
最简单的就是编译器不做任何处理,采用解释器执行,无论是解释ast还是字节码,若一个函数执行到最后还没有return,虚拟机就自动return nil
另一个简单方法是,无论何种情况,给函数后面加一个return nil即可,虽然会造成下面这种情况:
func f():    return 0    return nil
但这在语法上是正确的,执行也没有问题,只是最后的return永远执行不到而已,就是占用代码空间
不过larva并不能用上述这两种方式,原因有两点:
1 larva的实现可能是(也是目前唯一的方式)转换为其它代码,而其它语言的编译器可能很严格,如果larc不报错,而是转换后的代码报错,会增加排错难度
2 为提高效率,larva引入了类型推导,在第二种做法下,会对类型推导产生干扰,例如,一个函数中所有return都是int型表达式,原本可以将其返回类型推导为int,但增加一个return nil的额外代码会改变这个结果。而另一种方式,如果先推导函数返回类型,再根据返回类型为object或int在代码最后补return nil或return 0,这就和语言规范冲突了,因为larva规定没有显式return的情况下return nil
P.S.更激进一点的意见是,直接改语言规范,在没有显式return的情况下,返回值未定义,不过目前没有往这个方向走,其实将来可以在这方面做努力的,下面会提到


larva在这个问题上采用了一种简单的折中处理,直接检测函数的最后一个语句,若没有return则补一个return nil,不过不能简单检测最后一个语句就是return,比如如下代码:
func f(a):    if a == 0:        return 123    else:        return 456
其实这种嵌套return也只有if语句的情况,编译器需要判断最后一条语句“必定”return,则分三种情况:
1 没有嵌套语句块的语句,只需要判断是不是return即可
2 for、while循环,虽然嵌套了语句块,但是不能保证一定能执行语句块,即根本不进入循环,所以也不会“必定”返回,不需判断
3 if语句,需判断
if语句的判断规则是,一个if语句必定return,当且仅当它带else部分,且所有语句块都必定return
这是一个递归定义,所以判断的算法也是递归的,很简单
P.S.目前还没有实现for和while的带else语法,如果像python一样有for...else和while...else语法,上述第2条就不成立了,对于for和while也需要判断,而且算法更复杂些


很明显larva目前这种做法是不完善的,比如对这种代码:
func f():    return    a = 0
因为我比较懒,就只检测了最后一条语句,但另一方面我认为这种代码比较少,所以就没详细考虑,不过要做似乎也不是很难,只需要循环检测stmt_list即可,后面有空再改进


上面这个例子是和return相关的死代码检测,不过还有其它一些死代码检测,比如:
if false:    print "never execute"
或者:
while true:    ... //代码块没有breakreturn //永远执行不到
或者:
for i in l:    break //或continue    print "never execute"
这类检测很多编译器也都实现了,其实就是对各种情况分别做判断


larva在流程分析上做的还有一点不够好的是变量初始化和使用的分析,比如:
func f():    print a    a = 123
根据语法规定,a是一个局部变量,若是python,则会在运行时查出错误,但是larva为了运行效率,省掉了这一步,因此规定,所有变量(larva不像python可以动态增减变量,所以编译期可以完全确定)的初始值是未定义的(但实际实现中,一般是赋一个合适的初值,比如nil),这点和其它语言有类似之处,比如C代码:
int a;printf("%d", a);a = 123;
只不过,C编译器会对这个流程做分析,提示错误或告警。但这种分析往往是基于代码前后位置的,有时候程序员可能写出语法和语义上都没啥问题的,但编译器比较难分析的代码,比如:
for i in list:    if i == 1:        print a    else:        a = 123
若list是[0, 1],则这段代码是没啥问题,由于循环和判断,a是先赋值再打印的,但是编译器比较难检测这种情况,因为根本不知道list是什么内容,但有的时候又的确会存在这种代码,所以最好的处理方式也只能是给一个警告了,程序员可以在for之前给a赋值一个nil来消除这个警告,虽然这个赋值没什么用


有人可能会说,只要所有变量都初始化就行了,这话其实只说对了一半,正确的说法是,只有所有变量都用字面量或按执行顺序有依赖地初始化,才能规避初值未定义的问题,比如这个代码:
a = bb = a
a和b的确“初始化”了,但它们互相依赖了,甚至对于这种代码:
a = a
其实写了等于没写,虽然现代C++编译器已经尽可能检查这类问题,但也有漏的,比方说在vs中的这个代码(a是局部变量):
A a = a + 1;
A是一个class,实现一下拷贝构造和operator+(int),就可以看到,这行在a初始化之前就调用了operator+(int),是可能出问题的,而且编译器没有警告
如果是上面两个变量的例子,改成这样:
int a = 1;int b = a;
如果执行顺序如此,是没啥问题,但如果这两句执行顺序不固定,就可能有问题,比如这两句在两个不同模块,和运行时模块加载和初始化顺序有关,就可能有问题了
0 0
原创粉丝点击