在前几部分中,我们重点分析了前处理和词法分析过程,已经将源代码拆解成一个个的token了。接下来,我们将分析这些token的逻辑结构是否严格按照C语言定义的语法规则来排布,这就是将要介绍的内容:语法分析。
还是从代码入手分析,这样更加生动形象。给定下面这段C代码:
int a;
int main()
{
int b;
b = 1;
return (a + 2) - b;
}
数学家对物理学家说:“只要你能将物理问题转化为数学问题,我就都能解决。”这段话告诉我们要学会抽象。具体而言,
- 显然,
int a
和int b
是完全类似的,我们可以将其归为一类。更宽泛的,除了声明变量,也可以将函数变量的声明int main
也归类为同一类别,并统之为declaration
,因为它们都是一类型符int
开始的。 -
b = 1
和(a + 2) * 2 - b
都是操作声明的变量,统称为expression
。 -
expression
以分号结尾,表明一个具体的操作,称为statement
。
可能有点绕,归纳起来,采用缩进表示层级关系,上面代码可以这样进行描述:
program
declaration
declaration --> function_definition
statement --> compound_statement
declaration
statement --> expression_statement
expression
statement --> return_statement
expression
箭头右边所指对应着该名字的详细分类,我们会在后面深入研究。
通过这种方式,我们就将C语言中看似复杂多变、毫无头绪的结构变成了一种抽象的、方便处理的形式。值得注意的是,这些划分和教科书上面的介绍肯定有所出入,只是在参考资料的基础上,我想按照自己的理解进行介绍,不至于按照条条框框地介绍让人觉得枯燥无味。
其实,无论多复杂的C语言代码,都可以将其分成三大类:表达式、语句和声明。如果大家接受这样一套定义规则,那么我们将依次介绍这些内容,开始语法分析的处理过程。
3.1 表达式(expression)
先来研究在上面代码中出现的这段表达式:
(a + 2) - b --> a, 2, b
箭头右边的变量和具体的值,是我们所能拆分的最小单元了。并且恰好是词法分析过程中得到的那些token,我们将其记为primary_expression
。于是,primary_expression
的组成形式为:
primary_expression : ID
| INT
其中|
表示“或者”,表示可以取任何一个类型的值。ID
代表标识符,如a
和b
,INT
是具体的数值所对应的类型,后面还会介绍的CHAR
也是这样的。这些primary_expression
通过加减运算符号联系起来,形成稍微复杂一点的additive_expression
。于是,括号内的部分可以表示成:
additive_expression : primary_expression + primary_expression
进一步的,如何表示(a + 2) - b
呢?如果能够将之表示为:
additive_expression : additive_expression - primary_expression
那么,相信你已经对这样一套流程有了初步的认识了。值得注意的是,上面的表述方法中,我们还没有将括号纳入其中,但由于括号优先级最高,又必须考虑。怎么办呢?那就将括号内的部分打包当成最小单元,问题也就引刃而解了。从而可以更新primary_expression
的组成形式,得到:
primary_expression : ( additive_expression )
| ID
| INT
再将两条additive_expression
合并起来:
additive_expression : primary_expression + primary_expression
| additive_expression - primary_expression
这样就构成了表达式(a + 2) - b
的完整描述。这里面其实暗含着一种递归,直到我们能够返回ID
或者INT
为止。那么对照着,下面的表达式:
((a + 1) + 2) - b
也能用上面的表示方法描述。于是乎,就达到了我们抽象问题的目的。但是,如果包含乘法,或者更加复杂一些的表达式呢?我先直接写出完整的四则运算的描述:
primary_expression : ( additive_expression )
| CHAR
| INT
| ID
unary_expression : primary_expression
| + unary_expression
| - unary_expression
multiple_expression : unary_expression
| multiple_expression * unary_expression
| multiple_expression / unary_expression
additive_expression : multiple_expression
| additive_expression + multiple_expression
| additive_expression - multiple_expression
怎么来理解这些表达式的描述呢?举个例子,假设有这样一个表达式:
(3 - 2 / 2)
我们用下图来理解这些表达式之间的关系:
此刻,相信大家一定也看明白了整个的分析过程。接下来,我们开始实现这样一个过程。首先,定义一个类来描述这种结构。
class Parser(object):
def __init__(self, lexer):
self.lexer = lexer
self.current_token = self.lexer.get_next_token()
def eat(self, token_type):
if self.current_token.type == token_type:
self.current_token = self.lexer.get_next_token()
else:
raise Exception()
这里的eat
函数其实就是调用词法分析中的get_next_token()
,但是增加了一个判断,使得我们能够按照期望的token来进行匹配,否则就会抛出异常。这样,就形成了按照给定语法结构进行语法分析的基本框架。
对照着上文四则运算的描述,具体的实现代码如下:
def additive_expression(self):
node = self.multiple_expression()
while self.current_token.type in (PLUS, MINUS):
token = self.current_token
self.eat(token.type)
node = BinOp(left=node, op=token.value, \
right=self.multiple_expression())
return node
def multiple_expression(self):
node = self.unary_expression()
while self.current_token.type in (MUL, DIVIDE):
token = self.current_token
self.eat(token.type)
node = BinOp(left=node, op=token.value, \
right=self.unary_expression()))
return node
def unary_expression(self):
token = self.current_token
if token.type == PLUS:
self.eat(token.type)
node = self.unary_expression()
elif token.type == MINUS:
self.eat(token.type)
node = Negative(self.unary_expression())
else:
node = self.primary_expression()
return node
def primary_expression(self):
token = self.current_token
if token.type in (INT, CHAR, ID):
self.eat(token.type)
return token.value
elif token.type == LPAREN:
self.eat(LPAREN)
node = self.additive_expression()
self.eat(RPAREN)
return node
return None
这段代码中,类似于PLUS
这种全部由大写字母组成的名词就是token。而BinOp
和Negative
则是对处理得到的数据的一种组合,方便后续处理,我们会在后面详细介绍。
有了这些内容,写个简单的计算器应用应该会非常简单了。但对于C语言而言,光有这些这些基本类型还远远不够,我们进一步来研究对数组和结构体等这些稍微复杂的数据类型,所组成的表达式应该如何描述。例如:
str[3]
point->x
person.id
&x
这里,处理括号的思想将不再适用,数组下标或者结构体成员都是具体的primary_expression
,我们只能增加表达式的描述内容。因此,扩展后的表达式描述如下:
unary_expression : postfix_expression
| & unary_expression
| * unary_expression
| + unary_expression
| - unary_expression
postfix_expression : primary_expression
| postfix_expression [ expression ]
| postfix_expression . ID
| postfix_expression -> ID
那么,对于具体的实现,就只需要依葫芦画瓢,这里就不再赘述了。
不要忘了,还有函数的使用。C语言中,调用函数的方式如下:
printf("%s", "Hello world!");
add(1, 2);
callBackCb();
于是,根据结构,继续补充描述:
postfix_expression : primary_expression
| postfix_expression [ expression ]
| postfix_expression . ID
| postfix_expression -> ID
| ( )
| ( argument_expression_list )
argument_expression_list : primary_expression
| argument_expression_list, primary_expression
这里留下一个问题,对于C语言定义的其它运算,如逻辑运算符(&&
,||
)、关系运算符(>=
)等等又该如何处理呢?可以按照上面给定的这些描述触类旁通。具体的细节,我们留在下一部分进行详细介绍。
实现简易的C语言编译器(part 0)
实现简易的C语言编译器(part 1)
实现简易的C语言编译器(part 2)
实现简易的C语言编译器(part 4)
实现简易的C语言编译器(part 5)
实现简易的C语言编译器(part 6)
实现简易的C语言编译器(part 7)
实现简易的C语言编译器(part 8)
实现简易的C语言编译器(part 9)
实现简易的C语言编译器(part 10)
实现简易的C语言编译器(part 11)