简单的C语言解释运行器实现(四)—— 语法分析

上一篇:定义语法
下一篇:语义分析

语法树

是啥

在知道了文法的定义后,我们就要根据文法分析生成语法树了。

语法树可以表达是文法展开的过程,比如i+i*i
对应文法是

E ::= T | E + T | E - T
T ::= F | T * F | T / F
F ::= i

文法展开过程是:

E -> E + T -> T + T -> F + T -> i + T
  -> i + T * F -> i + F * F -> i + i * i

根据文法构造的语法树就是:

   E
  /|\
 F + T
 |  /|\
 i F * F
   |   |
   i   i

就很清楚了,我们得到语法树后可以计算”中序”遍历的序列,并对每个节点加括号,那么就可以还原原算式和文法的结构。

(i+(i*i))
(-E-----)
 F+(-T-)
 i  F*F
    i i

如果是连等式,那么有

a=b=c=d=e=1
  =
 / \
a   =
   / \
  b   =
     / \
    c   =
       / \
      d   =
         / \
        e   1

因为=是右结合的,所以语法树是右偏的。

可以说语法树展现了语句的结构,也展现文法的展开方式。

前置离散数学和数据结构。

语法树实现及节点类型表示参见
https://github.com/huanghongxun/Compiler/blob/master/compiler/syntax_tree.h
https://github.com/huanghongxun/Compiler/blob/master/compiler/syntax_descriptors.h

求值

我们有必要实现一个在语法树上直接运算的程序,这样我们可以在编译期完成常量的计算,计算数组定义时各维大小的表达式(如int a[2 * 3][(int) 3.0 / 2]),以及实现constexpr。

说简单其实可以说简单,我们可以这么写(伪代码)

object eval(AST ast)
{
    if (ast->type is binary_operator) // 二元运算符
    {
        switch (ast->op)
        {
            case "+": // 两个子树先求值再加和
                return eval(ast->children[0]) + eval(ast->children[1]);
            case "-": // 两个子树先求值再做差
                return eval(ast->children[0]) - eval(ast->children[1]);
            ... // * / << >> && & || | ^ % ... < > <= >=
        }
    }
    else if (ast->type is unary_operator) // 一元运算符
    {
        switch (ast->op)
        {
            case "-":
                return -eval(ast->children[0]);
            ... // + & * ! ~
        }
    }
    else if (ast->type is cast) // 转型
    {
        switch (ast->cast_to)
        {
            is int:
                return (int)eval(ast->children[0]);
            ...
    }
    else if (ast->type is constant)
    {
        return ast->value;
    }
    else
        throw runtime_error("Unsupported syntax tree type");
}

如果你了解树的话,上面的代码应该很好懂。也就是说,我们先递归求解子树的值,然后根据当前的语法树节点进行计算。

具体实现参见 https://github.com/huanghongxun/Compiler/blob/master/compiler/syntax_tree_evaluation.cpp

不过实际上进行计算的话是需要类型的,如果我们在语义分析阶段完成类型分析后实现会更简单。具体计算数组维度的请参见语义分析代码
https://github.com/huanghongxun/Compiler/blob/master/compiler/semantic_analyzer.cpp

递归下降分析法

例子

递归下降分析法这个名称听起来挺高端的,但是实际上就是对于每条文法都实现一个子程序而已,在介绍实际做法之前,我们先考虑如下的例子:

我们要解析这样的字符串表示A(B(,C(,)),D(,)
我们定义文法:

C ::= a .. z
R ::= e | C(R,R)

其中R表示定义一棵二叉树,叶节点表示为C(,)
我们要怎么解析呢?根据递归下降分析法的要求,我们要对每条文法实现一个子程序,文法C无非就是isalpha,我们实现R的解析:

int i = 0;
tree *parse_R()
{
    tree *node = new tree;
    auto c = str[i]; // 拿到当前的字符
    if (isalpha(c)) // 当前的字符是C,我们能匹配到R的第二条
    {
        i++; // 跳过字母
        assert(str[i++] == '('); // 字母下一位必须是左括号
        node->left = parse_R(); // 递归匹配R
        assert(str[i++] == ','); // 必须使用逗号隔开
        node->right = parse_R(); // 递归匹配R
        assert(str[i++] == ')'); // 必须以右括号结束

        return node;
    }
    else if (c == ',' || c == ')') // 当前字符不是字母,但可以匹配到R的第一条,因为空的下一位只能是逗号和右括号
    {
        return null;
    }
    else // R的两条都没有匹配到,当前字符不是字母,空的下一位却不是逗号和右括号
        assert(false); // 不符合语法
}

这样我们就可以解析出树了。这个方法是最快的,我们只需要扫描一遍字符串即可。

实现

接下来我们讨论实际的写法,比如之前定义的

UNIT_3 ::= UNIT_2 | UNIT_3 + UNIT_2 | UNIT_3 - UNIT_2

如果我们直接递归调用UNIT_3实现UNIT_3,肯定是不行的,因为我们程序实现不好做到预判运算符,对于这种递归,我们手动展开UNIT_3

UNIT_3 ::= UNIT_2 +/- UNIT_2 +/- ... +/- UNIT_2

那么很容易发现,无非就是从左往右匹配UNIT_2,然后我们就会遇到+-,然后再遇到下一个+-,也就是说因为从左往右解析,我们自然地完成了符号的左结合特性。我们可以这么实现:

syntax_tree *parse_unit3()
{
    syntax_tree *ast = parse_unit2();
    while (peek_token() == "+" || peek_token() == "-")
    {
        string t = next_token();
        syntax_tree n_ast = new syntax_tree(descriptor_binary_operator("+"));
        n_ast->children.push_back(ast);
        n_ast->children.push_back(parse_unit2());
        ast = n_ast;
    }
    return ast;
}

因为更高优先级的已经在parse_unit2处理过了,所以我们能保证更高优先级的一定在子树中,即优先执行更高优先级的运算符。

对于右结合的赋值系列符号,我们可以这么写:

syntax_tree *compiler::syntax_analyzer::parse_unit9()
{
    syntax_tree *node = parse_unit8();

    if (peek_token("=") || peek_token("+=") || peek_token("-=") || peek_token("/=") || peek_token("%=") || peek_token("*=") || peek_token("|=") || peek_token("&=") || peek_token("^=") || peek_token("<<=") || peek_token(">>="))
    {
        auto op = next_token();
        AST ast = make_shared<syntax_tree>(descriptor_assign(op.code), op);
        ast->children.push_back(node);
        ast->children.push_back(parse_unit9());
        return ast;
    }
    else
    {
        return node;
    }
}

因为右结合,所以我们就可以直接按照文法递归实现。

注意

我们在解析语句的时候,会有几种情况,分别是定义变量、表达式、if等特殊功能语句。其中特殊功能语句开头的标识符都比较特别,区分起来很容易,我们不作讨论。我们讨论一下关于区分定义变量和表达式。考虑到两者的开头都允许字母数字下划线组成的标识符,我们必须区分标识符是变量名还是类型,我们需要在语法分析期记录类型表,我们在遇到staticconstlong以及类型表中的标识符、或者其他定义的类型修饰符(比如接下来为了实现与解释器交互的内建函数,我们需要__built_in标识,VC的__cdecl__stdcall等)时,匹配到定义变量,否则如果是变量名,当作表达式处理;如果都不能匹配,那么只能是未声明的类型或者未定义的变量。为了简化问题,更加精确的错误分析我们就不讨论了。

我们甚至可以合并函数和变量定义的文法。

具体实现参考
https://github.com/huanghongxun/Compiler/blob/master/compiler/syntax_analyzer.cpp
https://github.com/huanghongxun/Compiler/blob/master/compiler/parsers.cpp

上一篇:定义语法
下一篇:语义分析

你可能感兴趣的:(编译器,简单的C语言编译器实现)