2012-01-07 97 views
17

「現代編譯器設計」一書是關於編譯器的好書。在它的源代碼中,令我煩惱的是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 -> +|* 

回答

18

可以存儲在內存中的樹,或者你可以直接產生所需要的輸出代碼。存儲中間表格通常可以在生成輸出之前在較高級別上對代碼進行一些處理。

以你的情況爲例,很容易發現你的表達式不包含變量,因此結果是一個固定的數字。但是一次只能看到一個節點,但這是不可能的。如果在查看「2 *」後生成機器代碼來計算某些東西的二倍,那麼當其他部分例如爲「3」時,這些代碼會被浪費,因爲您的程序將計算「3」,然後計算雙倍的每次而只是加載「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 
2

您可以創建Dijkstra的Shunting-yard algorithm的AST。

在某些時候,您將在內存中擁有整個表達式或AST,除非您在解析時計算立即結果。這適用於僅包含文字或編譯時間常量的(子)表達式,但不適用於在運行時計算的任何變量。

4

在完成lexing和解析後,無論您在做什麼,您都可以將AST存儲在內存中的九次。

一旦你有一個AST,你可以做很多事情:(可能使用遞歸,可能使用自己的自定義堆棧)

  • 將其轉化爲其他一些輸出,如

    • 直接對其進行評估用另一種語言編碼或某種其他類型的翻譯。
    • 編譯到最佳指令集
  • 0

    所以我要救一棵樹在內存或只使用遞歸調用;

    您將在分析器中使用遞歸調用來構建內存樹。

    ,當然還有,要保持樹的內存來處理它。

    一個優化編譯器將幾個表示代碼存儲在內存中(並對它們進行轉換)。

    0

    的問題的答案取決於你是否想要一個編譯器,一個解釋,或東西(圍繞中間語言包的解釋器)之間。如果你想要一個解釋器,遞歸下降解析器將同時評估表達式,所以不需要將它保存在內存中。如果你想要一個編譯器,那麼像例子這樣的常量表達式可以並且應該被優化,但是大多數表達式都會對變量進行操作,並且在轉換爲線性表單之前需要將其轉換爲樹形式作爲中間步驟。

    的混合編譯器/解釋器通常會編譯表達式,但它並沒有。這通常是一種編寫程序的廉價方式,它輸出可執行文件以簡單地將解釋器與源代碼一起打包。 Matlab使用這種技術 - 代碼用於真正的編譯,但是與交互式版本的一致性存在問題。然而,我不會允許爲表達式生成解析樹的難度決定了問題。