实现简易的C语言编译器(part 3)

        在前几部分中,我们重点分析了前处理和词法分析过程,已经将源代码拆解成一个个的token了。接下来,我们将分析这些token的逻辑结构是否严格按照C语言定义的语法规则来排布,这就是将要介绍的内容:语法分析
        还是从代码入手分析,这样更加生动形象。给定下面这段C代码:

int a;
int main()
{
    int b;
    b = 1;
    return (a + 2)  - b;
}

数学家对物理学家说:“只要你能将物理问题转化为数学问题,我就都能解决。”这段话告诉我们要学会抽象。具体而言,

  • 显然,int aint 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代表标识符,如abINT是具体的数值所对应的类型,后面还会介绍的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)

我们用下图来理解这些表达式之间的关系:


图3-1. 表达式:(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。而BinOpNegative则是对处理得到的数据的一种组合,方便后续处理,我们会在后面详细介绍。
        有了这些内容,写个简单的计算器应用应该会非常简单了。但对于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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容