c语言语法树节点挂接,简单的C语言解释运行器实现(四)—— 语法分析

本文详细介绍了编译器中语法树的构建过程,通过文法规则展开示例阐述了如何根据文法生成语法树,并探讨了中序遍历和求值方法。同时,讲解了递归下降分析法在解析字符串时的应用,以及如何处理右结合性的运算符。文章还提及了在编译期进行常量计算和类型检查的重要性,提供了相关代码实现的链接。
摘要由CSDN通过智能技术生成

上一篇:定义语法

下一篇:语义分析

语法树

是啥

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

语法树可以表达是文法展开的过程,比如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(descriptor_assign(op.code), op);

ast->children.push_back(node);

ast->children.push_back(parse_unit9());

return ast;

}

else

{

return node;

}

}

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

注意

我们在解析语句的时候,会有几种情况,分别是定义变量、表达式、if等特殊功能语句。其中特殊功能语句开头的标识符都比较特别,区分起来很容易,我们不作讨论。我们讨论一下关于区分定义变量和表达式。考虑到两者的开头都允许字母数字下划线组成的标识符,我们必须区分标识符是变量名还是类型,我们需要在语法分析期记录类型表,我们在遇到static,const,long以及类型表中的标识符、或者其他定义的类型修饰符(比如接下来为了实现与解释器交互的内建函数,我们需要__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

上一篇:定义语法

下一篇:语义分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值