语法和语义分析
顾名思义,一个命令式语言是由一个个“命令”为单元组成,不过一般很少用命令这个词,而是细分一下,比较低级的语言用指令(instruction),高低的语言一般认为是一个语句(statement,以后简称stmt),词法分析只将一段高级语言代码分解成一个个词,接下来还要做语句层面的分析,最终目标是生成抽象语法树(ast)
代码:
a = 1;
s = 0;
while (a <= 100)
{
if (a % 2 == 0)
{
continue;
}
s = s + a;
}
print s;
这段代码含有四条语句,两个赋值,一个while和一个print,其中while里面的内容并不会被展开,可以认为编译过后,这段代码会生成一个长度为4的stmt_list,包含四个stmt对象
stmt对象有各种类型,比如赋值、for循环、while循环等,每种类型包含的内容也不同,但一般来说是包含stmt和表达式(expression,以后简称expr)。又因为所有stmt要放在一个list,所以它们应该有同一个基类或实现同一个接口(java代码表示):
class Stmt
{
RetType execute();
}
整个语法分析就是分析出一个stmt的list:
StmtList build_stmt_list(TokenList token_list);
语法分析过程,编译原理一般都会说用上下文无关语言,BNF范式等,实际上这个分析和词法分析有类似的地方,都可以用自动机来做,只不过一个是正则语言的自动机,一个需要用下推自动机,因为可能有任意深度的语句包含,这里我们可以用递归来实现
另外,如果认真设计语法,也能很大简化这个过程,实际上语法分析和我们阅读代码在“可读性”上有很大的一致性,比如分支语句:
if (x) {...} else
{...} if (x) else
下面这行是我举的例子,并不是某个我知道的语言的语法。这两个例子都可以是一条分支语句,在日常说话中,我们可能会说出后面这种类似语法,但对于代码而言,选择前者可读性较好,因为如果{}里面很长,我们得到很后面才知道这句语句是什么意思,对编译器也是一样,if写在前面能及早确定stmt类型,因此不妨考察下各种语言,绝大多数语句都是关键字开头的,所以stmt_list的分析过程大致可以写成这样(伪代码):
while (token_list.size() > 0)
{
if (token_list[0] == "if") .....;//parse if
else if (token_list[0] == "while") .....; //parse while
......
else ......; //parse expr
}
不以关键字开头的一般都是表达式,比如函数调用或赋值(如果赋值被设计为表达式的话)
一个print语句对象和解析代码可能是这样:
class StmtPrint extends Stmt
{
Expr e;
RetType execute();
}
assert token_list.pop_front() == "print"
Stmt stmt = new StmtPrint()
stmt.e = build_expr(token_list)
return stmt
而一个while语句可能是这样:
class StmtWhile extends Stmt
{
Expr condition;
StmtList stmt_list;
RetType execute();
}
assert token_list.pop_front() == "while"
assert token_list.pop_front() == "("
Stmt stmt = new StmtWhile()
stmt.condition = build_expr(token_list)
assert token_list.pop_front() == ")"
assert token_list.pop_front() == "{"
stmt.stmt_list = build_stmt_list(token_list) //递归解析while的执行体
assert token_list.pop_front() == "}"
return stmt
比较复杂的可能是连续if...else这种,有两种风格,C和java的else if是把后面的if放在else部分,即:
if (a) x; else if (b) y; else z;
相当于:
if (a) x; else {if (b) y; else z;}
这样
class StmtIf extends Stmt
{
Expr condition;
StmtList when_true;
StmtList else_part;
RetType execute();
}
这样的一个问题是多个else if可能会导致树非常深
另一种像python的elif就是所有条件和执行体是并列的,差不多是这样:
class StmtIf extends Stmt
{
class IfElem
{
Expr condition;
StmtList stmt_list;
}
IfElemList if_list;
StmtList else_part;
RetType execute();
}
执行时顺序遍历if_list,如果有condition成立则执行对应的stmt_list,如果所有都不成立,则执行else_part,若一个语句没有最后的else,则else_part为null
当然像continue,break这种,就没有内容了,其余像return、for甚至try这种语句的解析都是一样的,略过
不以关键字开头的都是表达式,这里分两种情况,赋值表达式和非赋值表达式,C和java里面都是讲赋值实现为表达式,然后有时候在判断全等时候将==写为=就出现了比较隐晦的错误,所以我倾向于将赋值看做语句而非表达式,赋值语句由左右两个表达式组成,其中左表达式必须是左值
expr的实现编译原理有讲,用两个栈实现表达式树,很经典的算法。实际实现的时候可以用一些小技巧,比如碰到正括号的时候不入符号栈,而是直接递归调用build_expr,这样能简单些。另外需要注意某些符号可能在不同的状态下有不同的含义,例如+-可以是加减,也可以是正负,python中的“[”可能是取前面表达式的下标运算,也可能是一个列表的开始,python的tuple两头可以不加(),用逗号分隔,C语言也有逗号运算符,而同时逗号在这两个语言也是函数的参数分隔等等。还有一些更模糊的地方,比如python的for语法,是for ... in ....,而同时in在表达式也作为一个运算符,如果太难解析,不妨对某些语法规定必须加括号
所有expr都是右值表达式,但不是所有都是左值表达式,左值是指能放在赋值语句左边的。判断一个表达式是否左值可以先将其作为右值表达式来解析,看它的表达式树的根的操作符,如果是访问变量、访问属性、访问数组元素或通过地址取内存内容(C中的*运算)等,总之就是直接从存储区某个地方拿数据的操作,即是左值
不过有时候左值判断也要结合一些特殊语法,比如python中的unpack语法:
a, b, c = f()
赋值语句左边是个tuple形式(list形式也行),这里从语法上可以判断出,一个build_tuple表达式是左值当且仅当它所有元素是左值,递归判断即可
语法分析可以完成expr树,在静态类型语言中,每个变量或表达式的结果都是有类型的,还需要对树进行类型检查,比如:
long a;
void f(double, long);
f(a+1, 123);
最后这条语句的语法分析expr树画出来应该是: () / | \ f + 123 / \ a 1 然后对每个节点做类型分析,a是个long,1是个int,需要提升为long,于是加一个类型转换的节点,节点“+”的类型为long,而f接受的参数为double,也需要类型转换,第二个参数123也是,于是就成为: () / | \ / | \ f D L | |
+ 123
/ \
a L
|
1
其中D和L表示double和long类型转换运算 P.S.这个树中是将f作为()操作的一个参数,这是把f当成一个对象来看待,当然也可以将f()本身当成一个操作
在语义分析中,这个树还可以进一步简化,对于L-123这个子树而言,在任何情况下运行时都会得出结果123L,因此可以在编译期就计算好,同理L-1这个子树也可以计算,但是+不行,因为a的值只有运行时才知道,如果一个表达式树(整棵或子树)都由常量和确定的运算组成,且返回一个不可变的值,则可以在编译期算好,比如:
seconds_per_day = 24 * 60 * 60
代码中可以写个常量算式,更直观,对运行效率没有影响 python代码赋值一个tuple:
a = 1,2,(3,"a")
右边的build_tuple树的值是在编译期确定的,会整合成一个常量,但是这样不行:
a = [1,2,3]
虽然子树是由常量和一个build_list操作组成,但是由于list是可变对象,可能在后面改变a的内容,所以不能作为一个常量出现(即便能,运行时也是拷贝一个副本给a)