“现代编译器设计”一书是关于编译器的好书。在它的源代码中,令我烦恼的是AST或抽象语法树。假设我们想编写一个带括号的表达式解析器,它解析类似于:((2+3)*4) * 2
的东西!这本书说我们有一个AST:
((2+3)*4) * 2
/ | \
(2+3) *4 * 2
/ | \
(2+3) * 4
/ | \
2 + 3
我应该在内存中保存一棵树还是只使用递归调用;注意:如果我不将其存储在内存中,我该如何将其转换为机器代码?
解析器代码:
int parse(Expression &expr)
{
if(token.class=='D')
{
expr.type='D';
expr.value=token.val-'0';
get_next_token();
return 1;
}
if(token.class=='(')
{
expr.type='P';
get_next_token();
parse(&expr->left);
parse_operator(&expr->op);
parse(&expr->right);
if(token.class!=')')
Error("missing )");
get_next_token();
return 1;
}
return 0;
}
语法是:
expr -> expr | (expr op expr)
digit -> 0|1|2....|9
op -> +|*
答案 0 :(得分:21)
您可以将树存储在内存中,也可以直接生成所需的输出代码。存储中间形式通常是为了能够在生成输出之前对更高级别的代码进行一些处理。
在您的情况下,例如,发现您的表达式不包含变量并因此结果是固定数字会很简单。但是,一次仅查看一个节点,这是不可能的。更明确的是,如果在查看“2 *”后,您生成用于计算某事物的两倍的机器代码,当另一部分例如为“3”时,此代码会被浪费,因为您的程序将计算“3”,然后计算每次加载“6”时的两倍只会加载“6”,但更短更快。
如果要生成机器代码,那么首先需要知道代码将生成哪种机器...最简单的模型使用基于堆栈的方法。在这种情况下,您不需要寄存器分配逻辑,并且很容易直接编译到机器代码而无需中间表示。考虑这个只处理整数,四个操作,一元否定和变量的小例子......您会注意到根本没有使用任何数据结构:读取源代码字符并将机器指令写入输出...
#include <stdio.h>
#include <stdlib.h>
void error(const char *what) {
fprintf(stderr, "ERROR: %s\n", what);
exit(1);
}
void compileLiteral(const char *& s) {
int v = 0;
while (*s >= '0' && *s <= '9') {
v = v*10 + *s++ - '0';
}
printf(" mov eax, %i\n", v);
}
void compileSymbol(const char *& s) {
printf(" mov eax, dword ptr ");
while ((*s >= 'a' && *s <= 'z') ||
(*s >= 'A' && *s <= 'Z') ||
(*s >= '0' && *s <= '9') ||
(*s == '_')) {
putchar(*s++);
}
printf("\n");
}
void compileExpression(const char *&);
void compileTerm(const char *& s) {
if (*s >= '0' && *s <= '9') {
// Number
compileLiteral(s);
} else if ((*s >= 'a' && *s <= 'z') ||
(*s >= 'A' && *s <= 'Z') ||
(*s == '_')) {
// Variable
compileSymbol(s);
} else if (*s == '-') {
// Unary negation
s++;
compileTerm(s);
printf(" neg eax\n");
} else if (*s == '(') {
// Parenthesized sub-expression
s++;
compileExpression(s);
if (*s != ')')
error("')' expected");
s++;
} else {
error("Syntax error");
}
}
void compileMulDiv(const char *& s) {
compileTerm(s);
for (;;) {
if (*s == '*') {
s++;
printf(" push eax\n");
compileTerm(s);
printf(" mov ebx, eax\n");
printf(" pop eax\n");
printf(" imul ebx\n");
} else if (*s == '/') {
s++;
printf(" push eax\n");
compileTerm(s);
printf(" mov ebx, eax\n");
printf(" pop eax\n");
printf(" idiv ebx\n");
} else break;
}
}
void compileAddSub(const char *& s) {
compileMulDiv(s);
for (;;) {
if (*s == '+') {
s++;
printf(" push eax\n");
compileMulDiv(s);
printf(" mov ebx, eax\n");
printf(" pop eax\n");
printf(" add eax, ebx\n");
} else if (*s == '-') {
s++;
printf(" push eax\n");
compileMulDiv(s);
printf(" mov ebx, eax\n");
printf(" pop eax\n");
printf(" sub eax, ebx\n");
} else break;
}
}
void compileExpression(const char *& s) {
compileAddSub(s);
}
int main(int argc, const char *argv[]) {
if (argc != 2) error("Syntax: simple-compiler <expr>\n");
compileExpression(argv[1]);
return 0;
}
例如,使用1+y*(-3+x)
作为输入运行编译器,您将获得输出
mov eax, 1
push eax
mov eax, dword ptr y
push eax
mov eax, 3
neg eax
push eax
mov eax, dword ptr x
mov ebx, eax
pop eax
add eax, ebx
mov ebx, eax
pop eax
imul ebx
mov ebx, eax
pop eax
add eax, ebx
然而,这种编写编译器的方法无法很好地扩展到优化编译器。
虽然可以通过在输出阶段添加“窥视孔”优化器来进行一些优化,但只有从更高的角度来看代码才能进行许多有用的优化。
即使裸机代码生成也可以通过查看更多代码而受益,例如决定哪个寄存器分配给什么或决定哪个可能的汇编器实现对特定代码模式更方便。
例如,可以通过优化编译器将相同的表达式编译为
mov eax, dword ptr x
sub eax, 3
imul dword ptr y
inc eax
答案 1 :(得分:4)
十分之九的时间你会将内存中的AST保存在lexing和解析完成后你正在做的事情。
一旦你有了AST,你可以做很多事情:
答案 2 :(得分:2)
你可以用Dijkstra的Shunting-yard algorithm创建一个AST。
在某些时候,你将在内存中拥有整个表达式或AST,除非你在解析时计算立即结果。这适用于仅包含文字或编译时常量的(子)表达式,但不包含在运行时计算的任何变量。
答案 3 :(得分:0)
所以我应该在内存中保存一棵树还是只使用递归调用;
您将在解析器中使用递归调用在内存中构建树。
当然,您希望将树保留在内存中以进行处理。
优化编译器将代码的几个表示保存在内存中(并对其进行转换)。
答案 4 :(得分:0)
问题的答案取决于您是否需要编译器,解释器或介于两者之间的某种东西(一种围绕中间语言的解释器)。如果需要解释器,递归下降解析器将同时计算表达式,因此不需要将其保存在内存中。如果你想要一个编译器,那么像示例一样的常量表达式可以并且应该被优化,但是大多数表达式将对变量进行操作,并且在转换为线性形式之前需要转换为树形作为中间步骤。
混合编译器/解释器通常会编译表达式,但它并不是必须的。它通常是编写程序的廉价方式,该程序输出可执行文件以简单地用源代码包装解释器。 Matlab使用这种技术 - 代码过去是真正编译的,但是与交互式版本的一致性存在问题。但是,我不会允许为表达式生成解析树的难度来确定问题。