深入解析DoctorWkt/acwj项目:更多运算符的实现
本文是DoctorWkt/acwj项目系列教程的第21部分,我们将深入探讨如何在该项目中实现更多C语言运算符。通过本文,你将了解编译器如何处理各种运算符,包括自增/自减、位运算和逻辑运算等。
运算符概述
在本部分实现中,我们主要关注以下几类运算符:
- 自增/自减运算符:
++
和--
,包括前缀和后缀形式 - 一元运算符:
-
(负号)、~
(按位取反)和!
(逻辑非) - 二元运算符:
^
(异或)、&
(按位与)、|
(按位或)、<<
(左移)和>>
(右移) - 隐式非零运算符:在条件语句中自动将表达式结果转换为布尔值
词法分析扩展
首先需要为这些新运算符定义对应的token类型:
enum {
// 二元运算符
T_LOGOR, T_LOGAND, T_OR, T_XOR, T_AMPER,
T_LSHIFT, T_RSHIFT,
// 其他运算符
T_INC, T_DEC, T_INVERT, T_LOGNOT,
...
};
词法分析器需要能够识别这些运算符,特别是要注意区分相似的符号组合,如<
、<<
和<=
等。
语法分析与AST节点
在语法分析阶段,我们需要将这些运算符映射到AST节点类型:
enum {
// 二元运算符节点
A_LOGOR, A_LOGAND, A_OR, A_XOR, A_AND,
A_LSHIFT, A_RSHIFT,
// 一元运算符节点
A_PREINC, A_PREDEC, A_POSTINC, A_POSTDEC,
A_NEGATE, A_INVERT, A_LOGNOT,
...
};
同时需要定义运算符的优先级,确保表达式按照正确的顺序求值:
static int OpPrec[] = {
0, 10, 20, 30, // T_EOF, T_ASSIGN, T_LOGOR, T_LOGAND
40, 50, 60, // T_OR, T_XOR, T_AMPER
...
};
前缀与后缀表达式处理
根据C语言的BNF文法,我们需要分别处理前缀和后缀表达式:
- 前缀表达式:包括一元运算符和前置自增/自减
- 后缀表达式:主要包括后置自增/自减
在实现中,我们通过prefix()
和postfix()
函数来处理这两种情况。特别需要注意的是:
- 前缀
&
运算符需要将表达式视为左值 - 前缀
-
、~
和!
运算符需要将表达式视为右值 - 自增/自减运算符要求操作数必须是左值
布尔值转换
C语言允许在条件语句中使用非布尔表达式,编译器需要自动将其转换为布尔值。我们通过引入A_TOBOOL
AST节点类型来实现这一功能:
// 在解析条件语句时
condAST = binexpr(0);
if (condAST->op < A_EQ || condAST->op > A_GE)
condAST = mkastunary(A_TOBOOL, condAST->type, condAST, 0);
代码生成
在代码生成阶段,我们需要为每种新运算符生成对应的x86-64汇编指令:
-
位运算:直接使用对应的汇编指令
int cgand(int r1, int r2) { fprintf(Outfile, "\tandq\t%s, %s\n", reglist[r1], reglist[r2]); free_register(r1); return (r2); }
-
移位运算:需要先将移位量存入
%cl
寄存器int cgshl(int r1, int r2) { fprintf(Outfile, "\tmovb\t%s, %%cl\n", breglist[r2]); fprintf(Outfile, "\tshlq\t%%cl, %s\n", reglist[r1]); free_register(r2); return (r1); }
-
布尔运算:使用
test
指令结合条件设置指令int cglognot(int r) { fprintf(Outfile, "\ttest\t%s, %s\n", reglist[r], reglist[r]); fprintf(Outfile, "\tsete\t%s\n", breglist[r]); fprintf(Outfile, "\tmovzbq\t%s, %s\n", breglist[r], reglist[r]); return (r); }
-
自增/自减运算:需要同时处理内存加载和值修改
// 以char类型为例 if (op == A_PREINC) fprintf(Outfile, "\tincb\t%s(\%%rip)\n", Gsym[id].name); fprintf(Outfile, "\tmovzbq\t%s(%%rip), %s\n", Gsym[id].name, reglist[r]); if (op == A_POSTINC) fprintf(Outfile, "\tincb\t%s(\%%rip)\n", Gsym[id].name);
实现思考
在实现这些运算符时,有几个关键设计决策值得注意:
- AST节点设计:选择创建新的AST节点类型而不是修改现有节点结构,保持了代码的清晰性
- 运算符优先级:参考C语言标准确保正确的求值顺序
- 类型处理:特别是对有符号和无符号类型的正确处理
- 左值/右值区分:确保运算符应用于正确的表达式类型
总结
通过本部分的实现,我们的编译器现在支持了C语言中大多数常用的运算符。这些实现不仅增加了语言的功能性,也为后续更复杂的语言特性打下了基础。在实现过程中,我们特别注重:
- 保持代码结构的清晰和可扩展性
- 正确处理各种边界情况
- 生成高效的机器代码
- 为未来的扩展预留空间
这些运算符的实现是编译器开发中的重要里程碑,它们使得我们的编译器能够处理更加复杂和实用的C语言代码。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考