如何使用解析树?

时间:2016-07-22 00:43:07

标签: parsing compilation context-free-grammar

我目前正在学习解析器。我一直在看视频并尝试编写代码,但我开始很难理解。我想也许理解解析器的动机可以帮助理解它们的工作方式以及它们应该如何构建。

因此解析器的目标是获取一串令牌并创建一个解析树。我可以理解解析树是什么,但我只是看不到它是如何使用的。最终,编译器使用解析树来创建机器代码,但具体如何呢?有人能告诉我一个例子吗?

还有什么解析(和解析树)用于?

1 个答案:

答案 0 :(得分:2)

想象一下,你想为数学表达式制作一种语言。用户可能输入

(3 + 4) * 36

编译器会为该输入创建一个类似于

的解析树
      * 
     / \
    +   36
   / \
  3   4

一个简单的编译器可以生成机器代码,通过递归遍历树,发出子项指令,然后是它自己的指令来评估这个数学表达式。 Aka是一个后期遍历。在这种情况下发出的指令顺序为:

  1. 将数字3放在堆栈上的说明
  2. 将数字4放在堆栈上的说明
  3. 关于从堆栈中弹出前2个元素,添加它们并将结果放在堆栈上的说明
  4. 将数字36放在堆栈上的说明
  5. 弹出堆栈顶部2个元素,将它们相乘,并将结果放在堆栈上的说明。
  6. 此代码以完全相同的方式编译树。运行程序时,它会打印评估表达式所需的MIPS汇编指令。

    #include <stdio.h>
    
    enum TYPE {
        ADD,
        MULTIPLY,
        NUMBER
    };
    
    struct tree {
        enum TYPE type;
        int number_val;
        struct tree* left;
        struct tree* right;
    };
    
    void emit(struct tree* node);
    
    void emitNumber(struct tree* node) {
        // load the 32-bit number into a register
        printf("lui $t0, %d\n", (node->number_val) & 0xFFFF0000);
        printf("ori $t0, $t0, %d\n", (node->number_val) & 0x0000FFFF);
    
        // put the number on the stack
        puts("addi $sp, $sp, -4");        
        puts("sw $t0, 0($sp)");
    }
    
    void emitAdd(struct tree* node) {
        emit(node->left);
        emit(node->right);
    
        // pop the left and right args off the stack and put them in registers
        puts("lw $t0, 0($sp)");
        puts("addi $sp, $sp, +4");
        puts("lw $t1, 0($sp)");
        puts("addi $sp, $sp, +4");
    
        // add them and put the result on the stack    
        puts("add $t2, $t0, $t1");
        puts("addi $sp, $sp, -4");
        puts("sw $t2, 0($sp)");
    }
    
    void emitMult(struct tree* node) {
        emit(node->left);
        emit(node->right);
    
        // pop the left and right args off the stack and put them in registers
        puts("lw $t0, 0($sp)");
        puts("addi $sp, $sp, +4");
        puts("lw $t1, 0($sp)");
        puts("addi $sp, $sp, +4");
    
        // multiply them and put the result on the stack    
        puts("mul $t2, $t0, $t1");
        puts("addi $sp, $sp, -4");
        puts("sw $t2, 0($sp)");
    }
    
    void emit(struct tree* node) {
        if (node == NULL) {
            return;
        }
    
        switch (node->type) {
            case NUMBER:
                emitNumber(node);
                break;
            case ADD:
                emitAdd(node);
                break;
            case MULTIPLY:
                emitMult(node);
                break;
        }
    }
    
    int main() {
        // create an example tree
        struct tree three = { NUMBER, 3, NULL, NULL };
        struct tree four = { NUMBER, 4, NULL, NULL };
        struct tree thirtysix = { NUMBER, 36, NULL, NULL };
        struct tree add = { ADD, 0, &three, &four };
        struct tree mult = { MULTIPLY, 0, &add, &thirtysix };
    
        emit(&mult);
    
        // put the calculated result in register $t0
        puts("lw $t0, 0($sp)");
    }
    

    您可以在MIPS模拟器(如MARS或SPIM)上测试输出。最后,注册$t0保存结果252,这就是答案!

    要为完全成熟的语言编写一个编译器,它需要树中更多类型的节点,并且需要更多的发射函数。您还需要考虑如何在函数调用期间在堆栈上保存/恢复变量。您还希望编译器跨架构工作。有一些解决方案...你可以发出在虚拟机上运行的字节码,就像Python或Java或C#一样。或者你可以编译成像Clang那样使用LLVM的中间程序集,它通过另一个编译阶段来定位确切的体系结构。

    希望这能让您了解遍历树以生成实际指令是多么容易,以及为什么您更喜欢这种树表示而不是文本。