2012-01-07 13 views
17

पुस्तक 'मॉडर्न कंपाइलर डिज़ाइन' कंपाइलर्स के बारे में अच्छी किताब है। अपने स्रोत कोड में जो मुझे परेशान करता है वह एएसटी या सार सिंटेक्स ट्री है। मान लीजिए कि हम एक कोष्ठक अभिव्यक्ति पार्सर लिखना चाहते हैं जो कुछ ऐसा करता है: ((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 -> +|* 

उत्तर

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 

के रूप में मिलता है हालांकि compilers लेखन के इस दृष्टिकोण एक अनुकूलन संकलक करने के लिए अच्छी तरह से बड़े पैमाने नहीं है।

आउटपुट चरण में "पेफोल" ऑप्टिमाइज़र जोड़कर कुछ अनुकूलन प्राप्त करना संभव है, लेकिन कई उपयोगी अनुकूलन केवल उच्च बिंदु दृश्य से कोड को देख सकते हैं।

इसके अलावा भी नंगे मशीन कोड पीढ़ी को और कोड देखकर लाभ हो सकता है, उदाहरण के लिए यह तय करने के लिए कि कौन सा रजिस्टर निर्दिष्ट करता है या यह तय करने के लिए कि कौन से संभावित असेंबलर कार्यान्वयन एक विशिष्ट कोड पैटर्न के लिए सुविधाजनक होंगे।

उदाहरण के लिए एक ही अभिव्यक्ति

mov eax, dword ptr x 
sub eax, 3 
imul dword ptr y 
inc eax 
2

आप डिज्कस्ट्रा के Shunting-yard algorithm के साथ एक एएसटी बना सकते हैं।

किसी बिंदु पर आपके पास स्मृति में पूरी अभिव्यक्ति या एएसटी होगी, जब तक कि आप पार्सिंग के दौरान तत्काल परिणामों की गणना न करें। यह (उप-) अभिव्यक्तियों के साथ काम करता है जिसमें केवल अक्षर या संकलन समय स्थिरांक होते हैं, लेकिन रनटाइम पर गणना की गई किसी भी चर के साथ नहीं।

4

दस में से नौ बार आप लेक्सिंग और पार्सिंग के बाद जो भी कर रहे हैं उसके लिए स्मृति में एएसटी को बचाएंगे।

बार जब आप एक एएसटी है आप चीजों की एक संख्या कर सकते हैं: जैसे (शायद प्रत्यावर्तन का उपयोग कर, शायद अपने स्वयं के कस्टम ढेर का प्रयोग करके)

  • कुछ अन्य उत्पादन में बदलने,

    • यह सीधे मूल्यांकन किसी अन्य भाषा या किसी अन्य प्रकार के अनुवाद में कोड।
    • पसंदीदा अनुदेश करने के लिए इसे संकलित सेट
    • आदि
  • 0

    के लिए एक अनुकूलन संकलक द्वारा संकलित किया जा सकता है तो मैं स्मृति में एक पेड़ को बचाने चाहिए या सिर्फ पुनरावर्ती कॉल का उपयोग करें;

    आप मेमोरी में पेड़ बनाने के लिए अपने पार्सर में रिकर्सिव कॉल का उपयोग करेंगे।

    और निश्चित रूप से, आप पेड़ को स्मृति में संसाधित करना चाहते हैं।

    एक अनुकूलन कंपाइलर कई स्मृति में कोड के प्रतिनिधित्व (और उन्हें रूपांतरित) रखता है।

    0

    प्रश्न का उत्तर इस बात पर निर्भर करता है कि क्या आप एक कंपाइलर, एक दुभाषिया, या बीच में कुछ (मध्यवर्ती भाषा के चारों ओर एक दुभाषिया लिपटे) चाहते हैं। यदि आप एक दुभाषिया चाहते हैं, तो एक पुनरावर्ती मूल पार्सर एक ही समय में अभिव्यक्ति का मूल्यांकन करेगा, इसलिए स्मृति में इसे पकड़ने की कोई आवश्यकता नहीं है। यदि आप एक कंपाइलर चाहते हैं, तो उदाहरण की तरह निरंतर अभिव्यक्ति अनुकूलित और अनुकूलित की जानी चाहिए, लेकिन अधिकांश अभिव्यक्ति चर पर संचालित होंगी, और आपको एक रैखिक रूप में परिवर्तित होने से पहले एक मध्यवर्ती चरण के रूप में पेड़ रूप में परिवर्तित करने की आवश्यकता है।

    एक संकर संकलक/दुभाषिया आम तौर पर अभिव्यक्ति संकलित करेगा, लेकिन इसे नहीं करना है। यह अक्सर एक प्रोग्राम लिखने का एक सस्ता तरीका है जो निष्पादन योग्य को स्रोत कोड के साथ दुभाषिया को लपेटने के लिए आउटपुट करता है। Matlab इस तकनीक का उपयोग करता है - कोड वास्तव में संकलित किया जाता था लेकिन इंटरैक्टिव संस्करण के साथ स्थिरता के साथ समस्याएं थीं। हालांकि मैं अभिव्यक्ति के लिए एक पार्स पेड़ उत्पन्न करने में कठिनाई की अनुमति नहीं दूंगा।

    संबंधित मुद्दे