Recursive Descent Parser

后端 未结 5 1638
南笙
南笙 2020-12-23 15:23

The book \'Modern Compiler Design\' is the nice book about compilers. In its source code something that is annoying me is AST or Abstract Syntax Tree. Suppose we want to wri

相关标签:
5条回答
  • 2020-12-23 15:31

    The answer to the question depends on whether you want a compiler, an interpreter, or something in between (an interpreter wrapped around an intermediate language). If you want an interpreter, a recursive descent parser will at the same time evaluate the expression, so there is no need to hold it in memory. If you want a compiler, then a constant expression like the example can and should be optimised, but most expressions will operate on variables, and you need to convert to tree form as an intermediate step before converting to a linear form.

    A hybrid compiler / interpreter will usually compile expressions, but it doesn't have to. It's often a cheap way of writing a program which outputs an executable to simply wrap the interpreter up with the source code. Matlab uses this technique - code used to be genuinely compiled but there were problems with consistency with the interactive version. However I wouldn't allow the difficulty of generating a parse tree for expressions determine the issue.

    0 讨论(0)
  • 2020-12-23 15:34

    Nine times out of ten you'll save the AST in memory for whatever you are doing after lexing and parsing are done.

    Once you have an AST you can do a number of things:

    • Evaluate it directly (perhaps using recursion, perhaps using your own custom stack)
    • Transform it into some other output, such as code in another language or some other type of translation.
    • Compile it to preferred instruction set
    • etc.
    0 讨论(0)
  • 2020-12-23 15:41

    You can store the tree in memory or you can directly produce the required output code. Storing the intermediate form is normally done to be able to do some processing on the code at an higher level before generating output.

    In your case for example it would be simple to discover that your expression contains no variables and therefore the result is a fixed number. Looking only at one node at a time this however is not possible. To be more explicit if after looking at "2*" you generate machine code for computing the double of something this code is sort of wasted when the other part is for example "3" because your program will compute "3" and then compute the double of that every time while just loading "6" would be equivalent but shorter and faster.

    If you want to generate the machine code then you need first to know for what kind of machine the code is going to be generated... the simplest model uses a stack-based approach. In this case you need no register allocation logic and it's easy to compile directly to machine code without the intermediate representation. Consider this small example that handles just integers, four operations, unary negation and variables... you will notice that no data structure is used at all: source code characters are read and machine instructions are written to output...

    #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;
    }
    

    For example running the compiler with 1+y*(-3+x) as input you get as output

    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
    

    However this approach of writing compilers doesn't scale well to an optimizing compiler.

    While it's possible to get some optimization by adding a "peephole" optimizer in the output stage, many useful optimizations are possible only looking at code from an higher point of view.

    Also even the bare machine code generation could benefit by seeing more code, for example to decide which register assign to what or to decide which of the possible assembler implementations would be convenient for a specific code pattern.

    For example the same expression could be compiled by an optimizing compiler to

    mov  eax, dword ptr x
    sub  eax, 3
    imul dword ptr y
    inc  eax
    
    0 讨论(0)
  • 2020-12-23 15:42

    So should I save a tree in memory or just use recursive calls;

    You'll use recursive calls in your parser to build the tree in memory.

    And of course, you want to keep the tree in memory to process it.

    An optimizing compiler keeps several representations of the code in memory (and transform them).

    0 讨论(0)
  • 2020-12-23 15:51

    You can create an AST with Dijkstra's Shunting-yard algorithm.

    At some point you will have the whole expression or AST in memory though, unless you calculate immediate results while parsing. This works with (sub-)expressions containing only literals or compile time constants, but not with any variables calculated at runtime.

    0 讨论(0)
提交回复
热议问题