编译器实现之旅——第五章 实现语法分析器前的准备

在前面的旅程中,我们已经实现了词法分析器。词法分析器可将源代码转变为记号流,以供语法分析器使用。所以现在就让我们启程,朝着下一站——语法分析器出发吧。

1. 什么是语法

什么是语法呢?提到词法分析器,我们能够立即联想到一个个看得见摸得着的词;而提到语法分析器,又能联想到什么呢?

词法和语法的关系,就像扑克牌和打扑克牌的游戏规则之间的关系一样:词法决定了扑克牌的印刷、张数、花色、点数等等,而语法则决定了我们应该如何打出这些扑克牌,以赢得游戏胜利;这在编程语言中就是:词法决定了代码中能够出现哪些词,而语法决定了这些词的正确组织方式。由此可见:词法是具象的,而语法是抽象的。

2. 怎么表示语法

正如我们通过一段文字描述某种游戏规则一样,我们也可以用一段文字来描述一个语法。请看:

句子的语法:
1. 一个句子,由主语 + 谓语 + 宾语构成
2. 主语是:"程序员"
3. 谓语是:"没有"
4. 宾语是:"头发"

显然,这就是一套语法了。但是我们发现,这种白话文式的语法表示显然不是我们编译器一贯的作风嘛。针对这个问题,巴科斯及后人提出了用于表示语法的"巴科斯范式"和"拓展的巴科斯范式"。拓展的巴科斯范式的主要规则如下:

1. 用"::="符号表示"是",或"由...构成"的意思。说的专业点,叫"推导出"
2. 用"|"符号表示"或"的意思
3. 用"[ ... ]"表示中括号中的内容是可选的
4. 用"{ ... }"表示大括号中的内容可以出现0至无穷多次的重复

现在,就让我们用"拓展的巴科斯范式"来描述我们的语法吧:

句子的语法:
1. 句子 ::= 主语 谓语 宾语
2. 主语 ::= "程序员"
3. 谓语 ::= "没有"
4. 宾语 ::= "头发"

3. 语法怎么使用

我们已经有了语法,现在的问题是,语法有什么用处?又该怎么发挥这些用处呢?

首先,我们显然可以做这样一件事:判定一个记号流是否符合语法。请看:

假如我们有记号流:("程序员","有很多","头发"),我们从语法的第一条开始,我们知道:"句子 ::= 主语 谓语 宾语",这句话代表什么呢?其代表着:我们需要先看到一个主语,再看到一个谓语,最后看到一个宾语,如果都看到了,我们就认为这段记号流是符合语法的,否则,只要有任何一个地方不符合要求,我们就认为这段记号流是不符合语法的。那么问题来了,主语、谓语、宾语又分别是什么?显然,接下来的三条语法告诉了我们答案。我们首先检查主语,根据语法规则,我们知道:主语必须是"程序员",记号流呢?嗯,第一个记号确实是"程序员",我们接着往下看;语法规则又告诉我们:谓语必须是"没有",而此时的记号是"有很多",这就不对了,后面也不用看了,我们可以立即判定:("程序员","有很多","头发")这个记号流是不符合语法的。

接下来的故事就不用我多说了吧。经过一番小小的努力,我们终于发现:只有:("程序员","没有","头发")这个记号流是符合语法的。

确实符合语法了,然后呢?正如前面的章节所说,此时,我们就可以将这段记号流立体化,变为一棵抽象语法树了。这棵抽象语法树长什么样呢。请看:

句子
|
|---- 程序员(主语)
|
|---- 没有(谓语)
|
|---- 头发(宾语)

一句话解释:语法长什么样,语法树就长什么样。

将上面的模型变为代码,我们就得到了抽象语法树节点的定义。请看:

class __AST
{
    // Friend
    friend class __SyntaxAnalyzer;
    friend class __SemanticAnalyzer;
    friend class __CodeGenerator;


public:

    // Constructor
    explicit __AST(__TokenType tokenType, const string &tokenStr = "", const vector<__AST *> &subList = {}, int lineNo = 0);


    // Constructor (With tokenPtr)
    explicit __AST(const __Token *tokenPtr);


    // Destructor
    ~__AST();


private:

    // Attribute
    __TokenType __tokenType;
    string __tokenStr;
    vector<__AST *> __subList;
    int __lineNo;
};


__AST::__AST(__TokenType tokenType, const string &tokenStr, const vector<__AST *> &subList, int lineNo):
    __tokenType(tokenType),
    __tokenStr (tokenStr),
    __subList  (subList),
    __lineNo   (lineNo) {}


__AST::__AST(const __Token *tokenPtr):
    __tokenType(tokenPtr->__tokenType),
    __tokenStr (tokenPtr->__tokenStr),
    __lineNo   (tokenPtr->__lineNo) {}


__AST::~__AST()
{
    for (auto subPtr: __subList)
    {
        delete subPtr;
    }
}

可见,抽象语法树节点的定义和前面Token类的定义是高度相似的。抽象语法树节点也需要标记的类别、标记字符串,以及标记所在的行数。此外,抽象语法树节点还需要一个vector,以保存这个节点的所有子节点。

4. 消除左递归

这巴科斯范式可不是省油的灯啊。这一节和下一节,我们将分别来看看巴科斯范式中的两个常见问题。首先来看消除左递归。

假设有以下语法:

Six ::= Six '6'
      | '6'

这个语法说了什么?其表示,一个"Six"可以推导出一个"Six '6'";而"Six"又是什么?是"Six '6'";而"Six"又是什么?是"Six '6'"...

怎么回事?我们连一个记号字符串都还没看到呢,就光顾着在这无限循环了,这就是"左递归"问题,是我们需要消除的。

怎么消除呢?首先需要明确的是,语法表示并不是神圣不可侵犯的,而是可以进行等价变换的。那么,上面这个语法该怎么变换呢?让我们来找找规律:

1. Six ::= '6'
2. Six ::= Six '6' ::= '6' '6'
3. Six ::= Six '6' ::= Six '6' '6' ::= '6' '6' '6'
...

找到规律了!我们发现:

  1. 每一次使用"Six '6'"替换"Six",就会在"Six"的右边多出一个"6"
  2. "Six"终将被"6"而不是"Six '6'"替换

也就是说,"Six"推导出的应该是:1至有限多个"6"。写成语法就是:

Six ::= '6' { '6' }

可见,我们成功的消除了左递归。现在,语法的推导就可以正常开始并进行下去,而不会出现无限循环的情况了。

5. First集合

解决了左递归问题后,让我们再来看看接下来的这个问题。

假设有以下语法:

1. A ::= B | C
2. B ::= 'b'
3. C ::= D
4. D ::= 'd'

"A ::= B | C"?选B还是C?我们无从获知。因为"B"和"C"并不代表某个具体的字符,而是另外的两条语法。这时候怎么办呢?这时候呀,我们可就要刨根问底了。

我们先来看看"B"又是什么,我们发现:"B ::= 'b'",也就是说,如果我们看到了"b",我们就选"B"。

那么,"C"又是什么?我们发现:"C ::= D",不行,我们需要继续看。"D"又是什么?我们发现:"D ::= 'd'"。也就是说:

  1. 如果我们看到了"d",我们就选"D"
  2. 由于"C ::= D",所以如果我们看到了"d",我们也可以选"C"

经过一番刨根问底,现在我们知道了:对于"A ::= B | C"这条语法而言,如果我们看到了"b",我们就选"B";如果我们看到了"d",我们就选"C"。当然了,如果我们看到的既不是"b"也不是"d",这就是语法错误了。

上面我们得到的结果,其实就是First集合了。First集合可以帮助我们在"多选一"类语法中立即做出正确的选择。

我们的旅程到这里就快要暂告一段落了,最后,我们可以考虑两个问题。请看:

  1. 我就不用First集合,行不行?

行,当我们看到"A ::= B | C"时,管他呢,我们就选"B"。如果选"B"是错的,那么我们就想办法把这个"选错了"的信息传回来,然后我们再换成"C"试试看。

这样做的缺点显而易见:计算量可能会非常大,且毫无意义。如果"A ::= B | C"是语法的顶层,那么我们甚至有可能直到看到最后一个记号,才发现选错了,然后一切推翻重来。

  1. 刨根问底,会不会还是问不出个所以然?

不会。因为...

...接连便是难懂的话,什么"乔姆斯基体系",什么"上下文无关文法"之类...

至此,我们就完成了所有准备工作,可以开始实现语法分析器了。请看下一章:《实现语法分析器》。

posted @ 2021-02-19 16:28  樱雨楼  阅读(534)  评论(0编辑  收藏  举报