这是整个编译器的最后一部分,解析表达式。什么是表达式?表达式是将各种语言要素的一个组合,用来求值。例如:函数调用、变量赋值、运算符运算等等。

表达式的解析难点有二:一是运算符的优先级问题,二是如何将表达式编译成目标代码。我们就来逐一说明。

手把手教你构建 C 语言编译器系列共有10个部分:

  1. 手把手教你构建 C 语言编译器(0)——前言
  2. 手把手教你构建 C 语言编译器(1)——设计
  3. 手把手教你构建 C 语言编译器(2)——虚拟机
  4. 手把手教你构建 C 语言编译器(3)——词法分析器
  5. 手把手教你构建 C 语言编译器(4)——递归下降
  6. 手把手教你构建 C 语言编译器(5)——变量定义
  7. 手把手教你构建 C 语言编译器(6)——函数定义
  8. 手把手教你构建 C 语言编译器(7)——语句
  9. 手把手教你构建 C 语言编译器(8)——表达式
  10. 手把手教你构建 C 语言编译器(9)——总结

运算符的优先级

运算符的优先级决定了表达式的运算顺序,如在普通的四则运算中,乘法 * 优先级高于加法 +,这就意味着表达式 2 + 3 * 4 的实际运行顺序是 2 + (3 * 4) 而不是 (2 + 3) * 4

C 语言定义了各种表达式的优先级,可以参考 C 语言运算符优先级。

传统的编程书籍会用“逆波兰式”实现四则运算来讲解优先级问题。实际上,优先级关心的就是哪个运算符先计算,哪个运算符后计算(毕竟叫做“优先级”嘛)。而这就意味着我们需要决定先为哪个运算符生成目标代码(汇编),因为汇编代码是顺序排列的,我们必须先计算优先级高的运算符。

那么如何确定运算符的优先级呢?答曰:栈(递归调用的实质也是栈的处理)。

举一个例子:2 + 3 - 4 * 5,它的运算顺序是这样的:

  1. 将 2 入栈
  2. 遇到运算符 +,入栈,此时我们期待的是+的另一个参数
  3. 遇到数字 3,原则上我们需要立即计算 2+3的值,但我们不确定数字 3 是否属于优先级更高的运算符,所以先将它入栈。
  4. 遇到运算符 -,它的优先级和 + 相同,此时判断参数 3 属于这前的 +。将运算符 + 出栈,并将之前的 2 和 3 出栈,计算 2+3 的结果,得到 5 入栈。同时将运算符 - 入栈。
  5. 遇到数字4,同样不能确定是否能立即计算,入栈
  6. 遇到运算符 * 优先级大于 -,入栈
  7. 遇到数字5,依旧不能确定是否立即计算,入栈
  8. 表达式结束,运算符出栈,为 *,将参数出栈,计算 4*5 得到结果 20 入栈。
  9. 运算符出栈,为 -,将参数出栈,计算 5-20,得到 -15 入栈。
  10. 此时运算符栈为空,因此得到结果 -15
// after step 1, 2
|      |
+------+
| 3    |   |      |
+------+   +------+
| 2    |   | +    |
+------+   +------+// after step 4
|      |   |      |
+------+   +------+
| 5    |   | -    |
+------+   +------+// after step 7
|      |
+------+
| 5    |
+------+   +------+
| 4    |   | *    |
+------+   +------+
| 5    |   | -    |
+------+   +------+

综上,在计算一个运算符‘x’之前,必须先查看它的右方,找出并计算所有优先级大于‘x’的运算符,之后再计算运算符‘x’。

最后注意的是优先通常只与多元运算符相关,单元运算符往往没有这个问题(因为只有一个参数)。也可以认为“优先级”的实质就是两个运算符在抢参数。

一元运算符

上节中说到了运算符的优先级,也提到了优先级一般只与多元运算符有关,这也意味着一元运算符的优先级总是高于多元运算符。因为我们需要先对它们进行解析。

当然,这部分也将同时解析参数本身(如变量、数字、字符串等等)。

关于表达式的解析,与语法分析相关的部分就是上文所说的优先级问题了,而剩下的较难较烦的部分是与目标代码的生成有关的。因此对于需要讲解的运算符,我们主要从它的目标代码入手。

常量

首先是数字,用 IMM 指令将它加载到 AX 中即可:

if (token == Num) {match(Num);// emit code*++text = IMM;*++text = token_val;expr_type = INT;
}

接着是字符串常量。它比较特殊的一点是 C 语言的字符串常量支持如下风格:

char *p;
p = "first line""second line";

即跨行的字符串拼接,它相当于:

char *p;
p = "first linesecond line";

所以解析的时候要注意这一点:

else if (token == '"') {// emit code*++text = IMM;*++text = token_val;match('"');// store the rest stringswhile (token == '"') {match('"');}// append the end of string character '\0', all the data are default// to 0, so just move data one position forward.data = (char *)(((int)data + sizeof(int)) & (-sizeof(int)));expr_type = PTR;
}

sizeof

sizeof 是一个一元运算符,我们需要知道后面参数的类型,类型的解析在前面的文章中我们已经很熟悉了。

else if (token == Sizeof) {// sizeof is actually an unary operator// now only `sizeof(int)`, `sizeof(char)` and `sizeof(*...)` are// supported.match(Sizeof);match('(');expr_type = INT;if (token == Int) {match(Int);} else if (token == Char) {match(Char);expr_type = CHAR;}while (token == Mul) {match(Mul);expr_type = expr_type + PTR;}match(')');// emit code*++text = IMM;*++text = (expr_type == CHAR) ? sizeof(char) : sizeof(int);expr_type = INT;
}

注意的是只支持 sizeof(int)sizeof(char) 及 sizeof(pointer type...)。并且它的结果是 int 型。

变量与函数调用

由于取变量的值与函数的调用都是以 Id 标记开头的,因此将它们放在一起处理。

else if (token == Id) {// there are several type when occurs to Id// but this is unit, so it can only be// 1. function call// 2. Enum variable// 3. global/local variablematch(Id);id = current_id;if (token == '(') {// function callmatch('(');// ①// pass in argumentstmp = 0; // number of argumentswhile (token != ')') {expression(Assign);*++text = PUSH;tmp ++;if (token == ',') {match(',');}}match(')');// ②// emit codeif (id[Class] == Sys) {// system functions*++text = id[Value];}else if (id[Class] == Fun) {// function call*++text = CALL;*++text = id[Value];}else {printf("%d: bad function call\n", line);exit(-1);}// ③// clean the stack for argumentsif (tmp > 0) {*++text = ADJ;*++text = tmp;}expr_type = id[Type];}else if (id[Class] == Num) {// ④// enum variable*++text = IMM;*++text = id[Value];expr_type = INT;}else {// ⑤// variableif (id[Class] == Loc) {*++text = LEA;*++text = index_of_bp - id[Value];}else if (id[Class] == Glo) {*++text = IMM;*++text = id[Value];}else {printf("%d: undefined variable\n", line);exit(-1);}//⑥// emit code, default behaviour is to load the value of the// address which is stored in `ax`expr_type = id[Type];*++text = (expr_type == Char) ? LC : LI;}
}

①中注意我们是顺序将参数入栈,这和第三章:虚拟机中讲解的指令是对应的。与之不同,标准 C 是逆序将参数入栈的。

②中判断函数的类型,同样在第三章:“虚拟机”中我们介绍过内置函数的支持,如 printfreadmalloc 等等。内置函数有对应的汇编指令,而普通的函数则编译成 CALL <addr> 的形式。

③用于清除入栈的参数。因为我们不在乎出栈的值,所以直接修改栈指针的大小即可。

④:当该标识符是全局定义的枚举类型时,直接将对应的值用 IMM 指令存入 AX 即可。

⑤则是用于加载变量的值,如果是局部变量则采用与 bp 指针相对位置的形式(参见第 7章函数定义)。而如果是全局变量则用 IMM 加载变量的地址。

⑥:无论是全局还是局部变量,最终都根据它们的类型用 LC 或 LI 指令加载对应的值。

关于变量,你可能有疑问,如果遇到标识符就用 LC/LI 载入相应的值,那诸如 a[10] 之类的表达式要如何实现呢?后面我们会看到,根据标识符后的运算符,我们可能会修改或删除现有的 LC/LI 指令。

强制转换

虽然我们前面没有提到,但我们一直用 expr_type 来保存一个表达式的类型,强制转换的作用是获取转换的类型,并直接修改 expr_type 的值。

else if (token == '(') {// cast or parenthesismatch('(');if (token == Int || token == Char) {tmp = (token == Char) ? CHAR : INT; // cast typematch(token);while (token == Mul) {match(Mul);tmp = tmp + PTR;}match(')');expression(Inc); // cast has precedence as Inc(++)expr_type  = tmp;} else {// normal parenthesisexpression(Assign);match(')');}
}

指针取值

诸如 *a 的指针取值,关键是判断 a 的类型,而就像上节中提到的,当一个表达式解析结束时,它的类型保存在变量 expr_type 中。

else if (token == Mul) {// dereference *<addr>match(Mul);expression(Inc); // dereference has the same precedence as Inc(++)if (expr_type >= PTR) {expr_type = expr_type - PTR;} else {printf("%d: bad dereference\n", line);exit(-1);}*++text = (expr_type == CHAR) ? LC : LI;
}

取址操作

这里我们就能看到“变量与函数调用”一节中所说的修改或删除 LC/LI 指令了。前文中我们说到,对于变量,我们会先加载它的地址,并根据它们类型使用 LC/LI 指令加载实际内容,例如对变量 a

IMM <addr>
LI

那么对变量 a 取址,其实只要不执行 LC/LI 即可。因此我们删除相应的指令。

else if (token == And) {// get the address ofmatch(And);expression(Inc); // get the address ofif (*text == LC || *text == LI) {text --;} else {printf("%d: bad address of\n", line);exit(-1);}expr_type = expr_type + PTR;
}

逻辑取反

我们没有直接的逻辑取反指令,因此我们判断它是否与数字 0 相等。而数字 0 代表了逻辑 “False”。

else if (token == '!') {// notmatch('!');expression(Inc);// emit code, use <expr> == 0*++text = PUSH;*++text = IMM;*++text = 0;*++text = EQ;expr_type = INT;
}

按位取反

同样我们没有相应的指令,所以我们用异或来实现,即 ~a = a ^ 0xFFFF

else if (token == '~') {// bitwise notmatch('~');expression(Inc);// emit code, use <expr> XOR -1*++text = PUSH;*++text = IMM;*++text = -1;*++text = XOR;expr_type = INT;
}

正负号

注意这里并不是四则运算中的加减法,而是单个数字的取正取负操作。同样,我们没有取负的操作,用 0 - x 来实现 -x

else if (token == Add) {// +var, do nothingmatch(Add);expression(Inc);expr_type = INT;
}
else if (token == Sub) {// -varmatch(Sub);if (token == Num) {*++text = IMM;*++text = -token_val;match(Num);} else {*++text = IMM;*++text = -1;*++text = PUSH;expression(Inc);*++text = MUL;}expr_type = INT;
}

自增自减

注意的是自增自减操作的优先级是和它的位置有关的。如 ++p 的优先级高于 p++,这里我们解析的就是类似 ++p 的操作。

else if (token == Inc || token == Dec) {tmp = token;match(token);expression(Inc);// ①if (*text == LC) {*text = PUSH;  // to duplicate the address*++text = LC;} else if (*text == LI) {*text = PUSH;*++text = LI;} else {printf("%d: bad lvalue of pre-increment\n", line);exit(-1);}*++text = PUSH;*++text = IMM;// ②*++text = (expr_type > PTR) ? sizeof(int) : sizeof(char);*++text = (tmp == Inc) ? ADD : SUB;*++text = (expr_type == CHAR) ? SC : SI;
}

对应的汇编代码也比较直观,只是在实现 ++p时,我们要使用变量 p 的地址两次,所以我们需要先 PUSH (①)。

②则是因为自增自减操作还需要处理是指针的情形。

二元运算符

这里,我们需要处理多运算符的优先级问题,就如前文的“优先级”一节提到的,我们需要不断地向右扫描,直到遇到优先级 小于 当前优先级的运算符。

回想起我们之前定义过的各个标记,它们是以优先级从低到高排列的,即 Assign 的优先级最低,而 Brak[) 的优先级最高。

enum {Num = 128, Fun, Sys, Glo, Loc, Id,Char, Else, Enum, If, Int, Return, Sizeof, While,Assign, Cond, Lor, Lan, Or, Xor, And, Eq, Ne, Lt, Gt, Le, Ge, Shl, Shr, Add, Sub, Mul, Div, Mod, Inc, Dec, Brak
};

所以,当我们调用 expression(level) 进行解析的时候,我们其实通过了参数 level 指定了当前的优先级。在前文的一元运算符处理中也用到了这一点。

所以,此时的二元运算符的解析的框架为:

while (token >= level) {// parse token for binary operator and postfix operator
}

解决了优先级的问题,让我们继续讲解如何把运算符编译成汇编代码吧。

赋值操作

赋值操作是优先级最低的运算符。考虑诸如 a = (expession) 的表达式,在解析 = 之前,我们已经为变量 a 生成了如下的汇编代码:

IMM <addr>
LC/LI

当解析完=右边的表达式后,相应的值会存放在 ax 中,此时,为了实际将这个值保存起来,我们需要类似下面的汇编代码:

IMM <addr>
PUSH
SC/SI

明白了这点,也就能理解下面的源代码了:

tmp = expr_type;
if (token == Assign) {// var = expr;match(Assign);if (*text == LC || *text == LI) {*text = PUSH; // save the lvalue's pointer} else {printf("%d: bad lvalue in assignment\n", line);exit(-1);}expression(Assign);expr_type = tmp;*++text = (expr_type == CHAR) ? SC : SI;
}

三目运算符

这是 C 语言中唯一的一个三元运算符: ? :,它相当于一个小型的 If 语句,所以生成的代码也类似于 If 语句,这里就不多作解释。

else if (token == Cond) {// expr ? a : b;match(Cond);*++text = JZ;addr = ++text;expression(Assign);if (token == ':') {match(':');} else {printf("%d: missing colon in conditional\n", line);exit(-1);}*addr = (int)(text + 3);*++text = JMP;addr = ++text;expression(Cond);*addr = (int)(text + 1);
}

逻辑运算符

这包括 || 和 &&。它们对应的汇编代码如下:

<expr1> || <expr2>     <expr1> && <expr2>...<expr1>...          ...<expr1>...JNZ b                  JZ b...<expr2>...          ...<expr2>...
b:                     b:

所以源码如下:

else if (token == Lor) {// logic ormatch(Lor);*++text = JNZ;addr = ++text;expression(Lan);*addr = (int)(text + 1);expr_type = INT;
}
else if (token == Lan) {// logic andmatch(Lan);*++text = JZ;addr = ++text;expression(Or);*addr = (int)(text + 1);expr_type = INT;
}

数学运算符

它们包括 |^&==!= <=>=<><<>>+-*/%。它们的实现都很类似,我们以异或 ^ 为例:

<expr1> ^ <expr2>...<expr1>...          <- now the result is on ax
PUSH
...<expr2>...          <- now the value of <expr2> is on ax
XOR

所以它对应的代码为:

else if (token == Xor) {// bitwise xormatch(Xor);*++text = PUSH;expression(And);*++text = XOR;expr_type = INT;
}

其它的我们便不再详述。但这当中还有一个问题,就是指针的加减。在 C 语言中,指针加上数值等于将指针移位,且根据不同的类型移动的位移不同。如 a + 1,如果 a 是 char * 型,则移动一字节,而如果 a 是 int * 型,则移动 4 个字节(32位系统)。

另外,在作指针减法时,如果是两个指针相减(相同类型),则结果是两个指针间隔的元素个数。因此要有特殊的处理。

下面以加法为例,对应的汇编代码为:

<expr1> + <expr2>normal         pointer<expr1>        <expr1>
PUSH           PUSH
<expr2>        <expr2>     |
ADD            PUSH        | <expr2> * <unit>IMM <unit>  |MUL         |ADD

即当 <expr1> 是指针时,要根据它的类型放大 <expr2> 的值,因此对应的源码如下:

else if (token == Add) {// addmatch(Add);*++text = PUSH;expression(Mul);expr_type = tmp;if (expr_type > PTR) {// pointer type, and not `char *`*++text = PUSH;*++text = IMM;*++text = sizeof(int);*++text = MUL;}*++text = ADD;
}

相应的减法的代码就不贴了,可以自己实现看看,也可以看文末给出的链接。

自增自减

这次是后缀形式的,即 p++ 或 p--。与前缀形式不同的是,在执行自增自减后, ax上需要保留原来的值。所以我们首先执行类似前缀自增自减的操作,再将 ax 中的值执行减/增的操作。

// 前缀形式 生成汇编代码
*++text = PUSH;
*++text = IMM;
*++text = (expr_type > PTR) ? sizeof(int) : sizeof(char);
*++text = (tmp == Inc) ? ADD : SUB;
*++text = (expr_type == CHAR) ? SC : SI;// 后缀形式 生成汇编代码
*++text = PUSH;
*++text = IMM;
*++text = (expr_type > PTR) ? sizeof(int) : sizeof(char);
*++text = (token == Inc) ? ADD : SUB;
*++text = (expr_type == CHAR) ? SC : SI;
*++text = PUSH;                                             //
*++text = IMM;                                              // 执行相反的增/减操作
*++text = (expr_type > PTR) ? sizeof(int) : sizeof(char);   //
*++text = (token == Inc) ? SUB : ADD;                       //

数组取值操作

在学习 C 语言的时候你可能已经知道了,诸如 a[10] 的操作等价于 *(a + 10)。因此我们要做的就是生成类似的汇编代码:

else if (token == Brak) {// array access var[xx]match(Brak);*++text = PUSH;expression(Assign);match(']');if (tmp > PTR) {// pointer, `not char *`*++text = PUSH;*++text = IMM;*++text = sizeof(int);*++text = MUL;}else if (tmp < PTR) {printf("%d: pointer type expected\n", line);exit(-1);}expr_type = tmp - PTR;*++text = ADD;*++text = (expr_type == CHAR) ? LC : LI;
}

代码

除了上述对表达式的解析外,我们还需要初始化虚拟机的栈,我们可以正确调用 main 函数,且当 main 函数结束时退出进程。

int *tmp;
// setup stack
sp = (int *)((int)stack + poolsize);
*--sp = EXIT; // call exit if main returns
*--sp = PUSH; tmp = sp;
*--sp = argc;
*--sp = (int)argv;
*--sp = (int)tmp;

当然,最后要注意的一点是:所有的变量定义必须放在语句之前。

本章的代码可以在 Github 上下载,也可以直接 clone

git clone -b step-6 https://github.com/lotabout/write-a-C-interpreter

通过 gcc -o xc-tutor xc-tutor.c 进行编译。并执行 ./xc-tutor hello.c 查看结果。

正如我们保证的那样,我们的代码是自举的,能自己编译自己,所以你可以执行 ./xc-tutor xc-tutor.c hello.c。可以看到和之前有同样的输出。

小结

本章我们进行了最后的解析,解析表达式。本章有两个难点:

  1. 如何通过递归调用 expression 来实现运算符的优先级。
  2. 如何为每个运算符生成对应的汇编代码。

尽管代码看起来比较简单(虽然多),但其中用到的原理还是需要仔细推敲的。

最后,恭喜你!通过一步步的学习,自己实现了一个C语言的编译器(好吧,是解释器)。

https://lotabout.me/2016/write-a-C-interpreter-8/

手把手教你构建 C 语言编译器(8)- 表达式相关推荐

  1. 手把手教你构建 C 语言编译器.参考.0 -- 前言

    手把手教你构建C语言编译器.参考.0 -- 前言 写在前面 简介 写在前面 在此,十分感谢 三点水 的学习分享手把手教你构建 C 语言编译器: https://lotabout.me/2015/wri ...

  2. 手把手教你构建 C 语言编译器(4)- 递归下降

    本章我们将讲解递归下降的方法,并用它完成一个基本的四则运算的语法分析器. 手把手教你构建 C 语言编译器系列共有10个部分: 手把手教你构建 C 语言编译器(0)--前言 手把手教你构建 C 语言编译 ...

  3. 手把手教你构建 C 语言编译器(3)- 词法分析器

    本章我们要讲解如何构建词法分析器. 手把手教你构建 C 语言编译器系列共有10个部分: 手把手教你构建 C 语言编译器(0)--前言 手把手教你构建 C 语言编译器(1)--设计 手把手教你构建 C ...

  4. alpha因子常见问题_手把手教你构建量化因子分析体系

    原标题:手把手教你构建量化因子分析体系 挖掘Alpha因子.评价Alpha因子.改进Alpha因子是量化投资者职业生涯永恒的奋斗目标,而一套严密有效的因子分析体系是这一切的基石."不以规矩, ...

  5. 独家 | 手把手教你学习R语言(附资源链接)

    作者:NSS 翻译:杨金鸿 术语校对:韩海畴 全文校对:林亦霖 本文约3000字,建议阅读7分钟. 本文为带大家了解R语言以及分段式的步骤教程! 人们学习R语言时普遍存在缺乏系统学习方法的问题.学习者 ...

  6. 超级干货 :手把手教你学习R语言(附资源链接)

    作者:NSS:翻译:杨金鸿:校对:韩海畴,林亦霖: 本文约3000字,建议阅读7分钟. 本文为带大家了解R语言以及分段式的步骤教程! 人们学习R语言时普遍存在缺乏系统学习方法的问题.学习者不知道从哪开 ...

  7. 手把手教你学习R语言

    本文为带大家了解R语言以及分段式的步骤教程! 人们学习R语言时普遍存在缺乏系统学习方法的问题.学习者不知道从哪开始,如何进行,选择什么学习资源.虽然网络上有许多不错的免费学习资源,然而它们多过了头,反 ...

  8. 6个关键步骤,手把手教你构建图模型

    导读:图模型作为当前流行的信息处理加工技术,自提出以来,迅速在学术界和工业界得到了普及,在智能推荐.决策分析等方面有着广泛的应用. 作者:华为公司数据管理部 来源:大数据DT(ID:hzdashuju ...

  9. 实战案例,手把手教你构建电商用户画像 | 附代码

    导读:本文以真实案例,手把手教你搭建电商系统的用户画像. 先来看该电商用户画像用到的标签. 数据内容包括user_id(用户身份).item_id(商品).IDbehavior_type(用户行为类型 ...

最新文章

  1. android中static方法,StaticLayout如何在Android中使用?
  2. (转载)Nutch 2.0 之 抓取流程简单分析
  3. mysql 删除用户变量_MySql安装与MySQL添加用户、删除用户与授权
  4. spring之二——使用maven创建一个java project
  5. Office 365开发者的前端必备课程
  6. Board Moves CodeForces - 1353C(数学)
  7. distutils.errors.DistutilsError: Could not find suitable distribution for Requirement.parse(‘setupto
  8. 两点(51Nod-1416)
  9. 使用matlab的appdesigner制作分析固定简单电路的APP
  10. 【免费】文字转换语音合成软件地摊叫卖录音真人配音广告制作朗读播音软件
  11. 第三阶段应用层——1.7 数码相册—电子书(2)—编写通用的Makefile
  12. 如何快速合并PDF文件?几个方法教你合并PDF
  13. RK 7.1 导航栏添加隐藏虚拟按键的开关 并向上滑动显示虚拟按键
  14. centos镜像下载教程
  15. 上传文件连接失败问题
  16. pysptk, 语音信号处理工具包( SPTK )的python 封装器
  17. 【编程题】【Scratch二级】2019.06 飞不出去的蝴蝶
  18. php开发和java的区别
  19. 在虚拟机Ubuntu下安装java环境
  20. 浅谈TCP协议与DDOS

热门文章

  1. 在计算机教学过程当中,案例教学中计算机基础教学的运用论文
  2. 在Ubuntu 上安装和配置postfix邮箱服务器
  3. 嵌入式的应用领域有哪些呢?
  4. android分辨率 尺寸 dpi换算
  5. MATLAB获取屏幕分辨率
  6. python 修改excel 路径_python更改已存在excel文件的方法
  7. RK3568平台开发系列讲解(驱动篇)驱动开发之GPIO使用
  8. Python大神80行代码实现“头脑王者”,运营窍诀躺着教你赚钱!
  9. 不再封控,各高校要如何开展教学
  10. 解决关于引入的网络图片,浏览器无法正常打开的问题