编译器实现之旅——第八章 实现语义分析器

作为编译器后端的第一站,我们首先来实现语义分析器。

1. 语义分析器概观

正如上一章所说,语义分析器主要用于对抽象语法树进行语义层面的进一步检查,并生成符号表。我们也为符号表给出了一个"记录任何你想额外记录下的东西的表"这样的说了等于没说的定义。那么,CMM编译器的语义分析器到底需要做什么?其符号表又需要保存什么呢?

事实上,出于简单考虑,CMM编译器的语义分析器并不做任何的语义检查,只负责生成符号表。为了明确符号表到底需要记录什么,我们需要回顾CMM语言的这几点功能:

  1. 支持赋值(这也包括了支持变量)
  2. 支持函数
  3. 支持数组
  4. 区分全局作用域与局部作用域

也就是说,我们需要通过某种数据结构,将变量名、函数名、数组大小、作用域信息等内容全都放进去。似乎很复杂?请接着往下看。

2. 符号表的数据结构

不难发现,变量名和函数名,其实都是抽象语法树中的某个记号字符串;而数组大小其实也是抽象语法树中的某个记号字符串,只不过我们这里需要将其转为整型;那么,作用域信息呢?很简单,我们只需要回答这样一个问题:谁产生了作用域?答案当然是函数。也就是说,对于每个函数,其都有一个作用域,此外,还有一个全局作用域。我们还能发现:所有的变量名都位于某个作用域中;而数组大小是数组变量的一个"附加属性"。

将上述分析进行整理,我们就得到了以下要点:

  1. 我们需要记录一个全局作用域,和若干函数作用域。且函数作用域由各个函数产生
  2. 在每个作用域中,我们需要分别记录若干变量名
  3. 每个变量可能具有数组大小这一"附加属性"

有了这几点思考后,让我们继续思考:

  1. 作用域该怎么记录?用函数名即可。而全局作用域名只需要使用一个"非法函数名"即可,我们不妨使用"__GLOBAL__"
  2. 变量名该怎么记录?同理,用变量名即可;此外,我们还需要为每个变量名从0开始按顺序编号。至于编号的作用,我们将在代码生成器的相关章节中找到答案
  3. 数组大小该怎么记录?显然,用一个整型即可
  4. 作用域是最大的符号表层次,每个作用域中都包含着若干变量名,而每个变量名都具有变量的编号和数组大小这两个属性;如果这个变量不是数组,那么数组大小设为0即可

一个数据结构在我们的脑海中渐渐浮现:哈希表。

3. 语义分析器的实现

没错,我们可以用哈希表来同时存放变量名、函数名、数组大小、作用域信息等内容。这样的哈希表是双层的:第一层是作用域,我们可以通过函数名或"__GLOBAL__"作为键进行访问;第二层是变量名,显然,我们可以通过某个变量名作为键进行访问;而哈希值是一对整型,第一个整型代表了变量的编号,而第二个整型代表了数组大小。

语义分析器的实现如下所示:

class __SemanticAnalyzer
{
    // Friend
    friend class Core;


public:

    // Constructor
    __SemanticAnalyzer(__AST *root = nullptr);


private:

    // Attribute
    __AST *__root;


    // Semantic Analysis
    unordered_map<string, unordered_map<string, pair<int, int>>> __semanticAnalysis() const;
};


__SemanticAnalyzer::__SemanticAnalyzer(__AST *root):
    __root(root) {}


unordered_map<string, unordered_map<string, pair<int, int>>> __SemanticAnalyzer::__semanticAnalysis() const
{
    /*
        symbolTable: Function Name => Variable Name => (Variable Number, Array Size)
    */
    unordered_map<string, unordered_map<string, pair<int, int>>> symbolTable
    {
        {"__GLOBAL__", {}}
    };

    int globalIdx = 0;

    /*
        __TokenType::__Program
            |
            |---- __Decl
            |
            |---- [__Decl]
            .
            .
            .
    */
    for (auto declNodePtr: __root->__subList)
    {
        /*
            __VarDecl | __FuncDecl
        */
        if (declNodePtr->__tokenType == __TokenType::__FuncDecl)
        {
            /*
                __TokenType::__FuncDecl
                    |
                    |---- __Type
                    |
                    |---- __TokenType::__Id
                    |
                    |---- __ParamList | nullptr
                    |
                    |---- __LocalDecl
                    |
                    |---- __StmtList
            */
            int varIdx = 0;
            string funcName = declNodePtr->__subList[1]->__tokenStr;

            symbolTable[funcName];

            if (declNodePtr->__subList[2])
            {
                /*
                    __TokenType::__ParamList
                        |
                        |---- __Param
                        |
                        |---- [__Param]
                        .
                        .
                        .
                */
                for (auto paramPtr: declNodePtr->__subList[2]->__subList)
                {
                    /*
                        __TokenType::__Param
                            |
                            |---- __Type
                            |
                            |---- __TokenType::__Id
                    */
                    string varName = paramPtr->__subList[1]->__tokenStr;

                    symbolTable[funcName][varName] = {varIdx++, 0};
                }
            }

            /*
                __TokenType::__LocalDecl
                    |
                    |---- [__VarDecl]
                    .
                    .
                    .
            */
            for (auto varDeclPtr: declNodePtr->__subList[3]->__subList)
            {
                /*
                    __TokenType::__VarDecl
                        |
                        |---- __Type
                        |
                        |---- __TokenType::__Id
                        |
                        |---- [__TokenType::__Number]
                */
                string varName = varDeclPtr->__subList[1]->__tokenStr;

                int varSize = varDeclPtr->__subList.size() == 2 ? 0 : stoi(varDeclPtr->__subList[2]->__tokenStr);

                symbolTable[funcName][varName] = {varIdx, varSize};
                varIdx += varSize + 1;
            }
        }
        else
        {
            /*
                __TokenType::__VarDecl
                    |
                    |---- __Type
                    |
                    |---- __TokenType::__Id
                    |
                    |---- [__TokenType::__Number]
            */
            string varName = declNodePtr->__subList[1]->__tokenStr;

            int varSize = declNodePtr->__subList.size() == 2 ? 0 : stoi(declNodePtr->__subList[2]->__tokenStr);

            symbolTable["__GLOBAL__"][varName] = {globalIdx, varSize};
            globalIdx += varSize + 1;
        }
    }

    return symbolTable;
}

语义分析器以抽象语法树的树根作为输入,并在整个抽象语法树中获知我们所需要的信息。在进入抽象语法树之前,我们首先创建了一个空的符号表,由于全局作用域一定存在,故我们立即为符号表添加上代表着全局作用域的键;我们还定义了一个整型globalIdx,以跟踪各个全局变量的编号。

现在的问题是:这些变量名、函数名、数组大小、作用域信息等内容到底在抽象语法树的哪儿?为了回答这个问题,我们就必须回顾CMM的相关语法规则及其所对应的语法树结构了。请看:

  1. 语法树的树根具有一系列的子节点,这些子节点通过Decl语法构造而来;而Decl又可以推导出"VarDecl | FuncDecl",也就是说,整个语法树上按顺序排列着变量的声明,或函数的声明
  2. 对于变量的声明,即VarDecl语法,首先,很显然:这些变量就是全局变量;而继续查看VarDecl语法的语法树结构我们发现:变量名应该出现在VarDecl节点的第二子节点上;而数组长度,如果有的话,应该出现在VarDecl节点的第三子节点上
  3. 对于函数的声明,即FuncDecl语法,我们可以发现:函数名应该出现在FuncDecl节点的第二子节点上;而变量名应该出现在两处:第一处是FuncDecl节点的第三子节点,即ParamList节点的各个子节点上(仅当FuncDecl节点的第三子节点不是void),这些子节点代表了函数的形参;而第二处则位于FuncDecl节点的第四子节点,即CompoundStmt节点的第一子节点,又即LocalDecl节点的各个子节点上,这些子节点代表了函数的局部变量。所有这些子节点的变量名,都位于各个子节点的第二子节点上;而数组长度,对于形参子节点而言没有,对于局部变量子节点而言,则位于各个子节点的第三子节点上(这段说明真的是太绕了)

我们还需要关注的一点是:变量该如何编号?通常情况下,变量从0开始编号,每次加1就行了。但由于CMM中的变量可能是一个数组,故在语义分析器的实现中,我们不仅在每次遇到一个新的变量名后将变量的编号加1,我们还在每次遇到数组时将编号额外的加上了数组的长度。这样做的意义是什么?我们将在代码生成器的相关章节看到答案。

最后,还有一个需要留意的地方,那就是:在FuncDecl节点的解析中,两处变量名的解析顺序是不能颠倒的。同样,这样做的意义我们也将在代码生成器的相关章节看到答案。

至此,语义分析器就已经实现完成了。通过语义分析器,我们得到了对于代码生成器而言非常重要的一个"信息中心"——符号表。接下来,就让我们来看看低级指令的执行器——虚拟机是怎么实现的吧。请看下一章:《实现虚拟机》。

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