递归下降解析器

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了递归下降解析器相关的知识,希望对你有一定的参考价值。

“现代编译器设计”一书是关于编译器的好书。在它的源代码中,令我烦恼的是AST或抽象语法树。假设我们想编写一个带括号的表达式解析器,它解析类似于:((2+3)*4) * 2的东西!这本书说我们有一个像:

        ((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  -> +|*
答案

您可以将树存储在内存中,也可以直接生成所需的输出代码。存储中间形式通常是为了能够在生成输出之前对更高级别的代码进行一些处理。

例如,在您的情况下,很容易发现您的表达式不包含变量,因此结果是固定数字。但是,一次仅查看一个节点,这是不可能的。更明确的是,如果在查看“2 *”后,您生成用于计算某事物的两倍的机器代码,当另一部分例如为“3”时,此代码会被浪费,因为您的程序将计算“3”,然后计算加载“6”时每次加倍,但更短更快。

如果要生成机器代码,那么首先需要知道代码将生成哪种机器...最简单的模型使用基于堆栈的方法。在这种情况下,您不需要寄存器分配逻辑,并且很容易直接编译到机器代码而无需中间表示。考虑这个只处理整数,四个操作,一元否定和变量的小例子......您会注意到根本没有使用任何数据结构:读取源代码字符并将机器指令写入输出...

#include <stdio.h>
#include <stdlib.h>

void error(const char *what) {
    fprintf(stderr, "ERROR: %s
", 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
", 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("
");
}

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
");
    } 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
");
            compileTerm(s);
            printf("    mov  ebx, eax
");
            printf("    pop  eax
");
            printf("    imul ebx
");
        } else if (*s == '/') {
            s++;
            printf("    push eax
");
            compileTerm(s);
            printf("    mov  ebx, eax
");
            printf("    pop  eax
");
            printf("    idiv ebx
");
        } else break;
    }
}

void compileAddSub(const char *& s) {
    compileMulDiv(s);
    for (;;) {
        if (*s == '+') {
            s++;
            printf("    push eax
");
            compileMulDiv(s);
            printf("    mov  ebx, eax
");
            printf("    pop  eax
");
            printf("    add  eax, ebx
");
        } else if (*s == '-') {
            s++;
            printf("    push eax
");
            compileMulDiv(s);
            printf("    mov  ebx, eax
");
            printf("    pop  eax
");
            printf("    sub  eax, ebx
");
        } else break;
    }
}

void compileExpression(const char *& s) {
    compileAddSub(s);
}

int main(int argc, const char *argv[]) {
    if (argc != 2) error("Syntax: simple-compiler <expr>
");
    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
另一答案

在完成lexing和解析之后,你将把内存中的AST保存到内存中九次。

一旦你有AST,你可以做很多事情:

  • 直接评估它(可能使用递归,也许使用您自己的自定义堆栈)
  • 将其转换为其他输出,例如另一种语言的代码或其他类型的翻译。
  • 将其编译为首选指令集
  • 等等
另一答案

你可以用Dijkstra的Shunting-yard algorithm创建一个AST。

在某些时候,你将在内存中拥有整个表达式或AST,除非你在解析时计算立即结果。这适用于仅包含文字或编译时常量的(子)表达式,但不包含在运行时计算的任何变量。

另一答案

所以我应该在内存中保存一棵树还是只使用递归调用;

您将在解析器中使用递归调用在内存中构建树。

当然,您希望将树保留在内存中以进行处理。

优化编译器将代码的多个表示保存在内存中(并对其进行转换)。

另一答案

问题的答案取决于您是否需要编译器,解释器或介于两者之间的某种东西(一种围绕中间语言的解释器)。如果需要解释器,递归下降解析器将同时计算表达式,因此不需要将其保存在内存中。如果你想要一个编译器,那么像这个例子的常量表达式可以并且应该被优化,但是大多数表达式将对变量进行操作,并且在转换为线性形式之前需要转换为树形作为中间步骤。

混合编译器/解释器通常会编译表达式,但它不必。它通常是编写程序的廉价方式,该程序输出可执行文件以简单地用源代码包装解释器。 Matlab使用这种技术 - 代码过去是真正编译的,但是与交互式版本的一致性存在问题。但是我不允许为表达式生成解析树的难度决定问题。

以上是关于递归下降解析器的主要内容,如果未能解决你的问题,请参考以下文章

了解Haskell中已实现的递归下降解析器

Python实现JSON生成器和递归下降解释器

Python 之父的解析器系列之五:左递归 PEG 语法

ANTLR快餐教程 - ANTLR其实很简单

Android 逆向使用 Python 解析 ELF 文件 ( Capstone 反汇编 ELF 文件中的机器码数据 | 创建反汇编解析器实例对象 | 设置汇编解析器显示细节 )(代码片段

如何用 C# 编写解析器? [关闭]