जब संकलक सांख्यिकीय रूप से "जटिल" अभिव्यक्तियों की जांच करते हैं तो सामान्य प्रक्रिया का क्या उपयोग किया जाता है?


23

नोट: जब मैंने शीर्षक में "जटिल" का उपयोग किया था, तो मेरा मतलब है कि अभिव्यक्ति में कई ऑपरेटर और ऑपरेंड हैं। ऐसा नहीं है कि अभिव्यक्ति ही जटिल है।


मैं हाल ही में x86-64 विधानसभा के लिए एक साधारण संकलक पर काम कर रहा हूँ। मैंने संकलक के मुख्य सामने के छोर - लेसर और पार्सर को समाप्त कर दिया है - और अब मैं अपने प्रोग्राम का एक सार सिंटैक्स ट्री प्रतिनिधित्व उत्पन्न करने में सक्षम हूं। और जब से मेरी भाषा सांख्यिकीय रूप से टाइप की जाएगी, मैं अब अगला चरण कर रहा हूं: स्रोत कोड की जांच कर रहा हूं। हालांकि, मैं एक समस्या पर आया हूं और इसे स्वयं हल करने में सक्षम नहीं हो पाया हूं।

निम्नलिखित उदाहरण पर विचार करें:

मेरे संकलक के पार्सर ने कोड की यह पंक्ति पढ़ी है:

int a = 1 + 2 - 3 * 4 - 5

और इसे निम्नलिखित एएसटी में बदल दिया:

       =
     /   \
  a(int)  \
           -
         /   \
        -     5
      /   \
     +     *
    / \   / \
   1   2 3   4

अब इसे एएसटी की जांच करना होगा। यह पहले प्रकार से =ऑपरेटर की जाँच करके शुरू होता है । यह पहले ऑपरेटर के बाएँ हाथ की जाँच करता है। यह देखता है कि चर aको पूर्णांक के रूप में घोषित किया गया है। इसलिए अब यह सत्यापित करना चाहिए कि दाहिने हाथ की अभिव्यक्ति पूर्णांक का मूल्यांकन करती है।

मुझे समझ में आया कि अगर अभिव्यक्ति सिर्फ एक मूल्य थी, जैसे कि 1या यह कैसे किया जा सकता है 'a'? लेकिन यह कई मूल्यों और ऑपरेंड्स के साथ अभिव्यक्ति के लिए कैसे किया जाएगा - एक जटिल अभिव्यक्ति - जैसे कि ऊपर वाला? अभिव्यक्ति के मूल्य को सही ढंग से निर्धारित करने के लिए, ऐसा लगता है कि प्रकार चेकर को वास्तविक रूप से अभिव्यक्ति को निष्पादित करना होगा और परिणाम रिकॉर्ड करना होगा। लेकिन यह स्पष्ट रूप से संकलन और निष्पादन चरणों को अलग करने के उद्देश्य को हराने के लिए लगता है।

एकमात्र ऐसा तरीका है जिसकी मैं कल्पना करता हूं कि यह किया जा सकता है कि एएसटी में प्रत्येक सबप्रेप्रेशन की पत्ती की पुनरावृत्ति की जाँच करें और पत्ती के सभी प्रकारों को सत्यापित ऑपरेटर प्रकार से सत्यापित करें। तो =ऑपरेटर के साथ शुरू , टाइप चेकर फिर बाएं हाथ की एएसटी के सभी स्कैन करेगा और सत्यापित करेगा कि लीफ़ सभी पूर्णांक हैं। यह तब उपप्रकार में प्रत्येक ऑपरेटर के लिए इसे दोहराएगा।

मैंने "द ड्रैगन बुक" की अपनी प्रति में विषय पर शोध करने की कोशिश की है , लेकिन यह बहुत विस्तार से नहीं लगता है, और बस वही दोहराता है जो मैं पहले से जानता हूं।

जब एक कंपाइलर कई ऑपरेटर और ऑपरेंड के साथ अभिव्यक्ति की जाँच कर रहा होता है, तो सामान्य विधि का क्या उपयोग किया जाता है? क्या मैं ऊपर बताए गए किसी भी तरीके का इस्तेमाल कर रहा हूं? यदि नहीं, तो क्या तरीके हैं और वे वास्तव में कैसे काम करेंगे?


8
अभिव्यक्ति के प्रकार की जांच करने के लिए स्पष्ट और सरल तरीके हैं। आप हमें बेहतर बताते हैं कि क्या आप इसे "अरुचिकर" कहते हैं।
gnasher729

12
सामान्य विधि "दूसरी विधि" है: कंपाइलर इसके उप-प्रकारों के प्रकार से जटिल अभिव्यक्ति के प्रकार को संक्रमित करता है। वह मुख्य शब्द था, जो कि शब्दार्थ शब्दार्थ और इस दिन बनाए गए अधिकांश प्रकार के सिस्टम थे।
जोकर_vD

5
दो दृष्टिकोण अलग-अलग व्यवहार उत्पन्न कर सकते हैं: ऊपर-नीचे का दृष्टिकोण double a = 7/2 दाईं ओर के हिस्से को दोहरे के रूप में व्याख्या करने की कोशिश करेगा, इसलिए अंश और हर को दो के रूप में व्याख्या करने की कोशिश करेगा और यदि आवश्यक हो तो उन्हें परिवर्तित कर सकता है; परिणामस्वरूप a = 3.5। बॉटम-अप पूर्णांक विभाजन का प्रदर्शन करेगा और केवल अंतिम चरण (असाइनमेंट) पर परिवर्तित होगा a = 3.0
हेगन वॉन एटिजन

3
ध्यान दें कि आपके एएसटी की तस्वीर आपकी अभिव्यक्ति के अनुरूप नहीं है int a = 1 + 2 - 3 * 4 - 5लेकिनint a = 5 - ((4*3) - (1+2))
बेसिल स्टायरनेविच

22
आप मूल्यों के बजाय प्रकारों पर अभिव्यक्ति को "निष्पादित" कर सकते हैं; जैसे int + intबन जाता है int

जवाबों:


14

पुनरावृत्ति उत्तर है, लेकिन आप ऑपरेशन को संभालने से पहले प्रत्येक उपशीर्षक में उतरते हैं:

int a = 1 + 2 - 3 * 4 - 5

टू ट्री फॉर्म:

(assign (a) (sub (sub (add (1) (2)) (mul (3) (4))) (5))

प्रकार का जिक्र पहले बाएं हाथ की ओर से चलने से होता है, फिर दाहिने हाथ की तरफ, और फिर ऑपरेटर के हैंडल से जैसे ही पता चलता है:

(assign*(a) (sub (sub (add (1) (2)) (mul (3) (4))) (5))

-> उतर में उतरता है

(assign (a*) (sub (sub (add (1) (2)) (mul (3) (4))) (5))

-> अनुमान है aaजाना जाता है int। अब हम वापस assignनोड में हैं:

(assign (int:a)*(sub (sub (add (1) (2)) (mul (3) (4))) (5))

-> आरएच में उतरते हैं, तब तक आंतरिक ऑपरेटरों की झोली में जब तक हम कुछ दिलचस्प नहीं मारते

(assign (int:a) (sub*(sub (add (1) (2)) (mul (3) (4))) (5))
(assign (int:a) (sub (sub*(add (1) (2)) (mul (3) (4))) (5))
(assign (int:a) (sub (sub (add*(1) (2)) (mul (3) (4))) (5))
(assign (int:a) (sub (sub (add (1*) (2)) (mul (3) (4))) (5))

-> के प्रकार का अनुमान लगाते हैं 1, जो कि है int, और माता-पिता के पास वापस आ जाते हैं

(assign (int:a) (sub (sub (add (int:1)*(2)) (mul (3) (4))) (5))

-> rhs में जाते हैं

(assign (int:a) (sub (sub (add (int:1) (2*)) (mul (3) (4))) (5))

-> के प्रकार का अनुमान लगाते हैं 2, जो कि है int, और माता-पिता के पास वापस आ जाते हैं

(assign (int:a) (sub (sub (add (int:1) (int:2)*) (mul (3) (4))) (5))

-> के प्रकार का अनुमान लगाते हैं add(int, int), जो कि है int, और माता-पिता के पास वापस आ जाते हैं

(assign (int:a) (sub (sub (int:add (int:1) (int:2))*(mul (3) (4))) (5))

-> रस्ते में उतरते हैं

(assign (int:a) (sub (sub (int:add (int:1) (int:2)) (mul*(3) (4))) (5))

आदि, जब तक आप साथ समाप्त नहीं हो जाते

(assign (int:a) (int:sub (int:sub (int:add (int:1) (int:2)) (int:mul (int:3) (int:4))) (int:5))*

क्या असाइनमेंट अपने आप में एक प्रकार की अभिव्यक्ति भी है, जो आपकी भाषा पर निर्भर करता है।

महत्वपूर्ण टेकअवे: पेड़ में किसी भी ऑपरेटर नोड के प्रकार को निर्धारित करने के लिए, आपको केवल उसके तत्काल बच्चों को देखना होगा, जिन्हें पहले से ही एक प्रकार सौंपा जाना चाहिए।


43

कंपाइलर कई ऑपरेटरों और ऑपरेंड के साथ अभिव्यक्ति की जाँच करते समय टाइप करने वाली विधि आमतौर पर किस विधि का उपयोग करती है।

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

टाइप जाँच आसान हो सकता है अगर:

  • आपके सभी चर जैसे aस्पष्ट रूप से एक प्रकार के साथ घोषित किए गए हैं। यह C या पास्कल या C ++ 98 की तरह है, लेकिन C ++ 11 की तरह नहीं, जिसके साथ कुछ प्रकार का अनुमान है auto
  • सभी शाब्दिक मूल्यों की तरह 1, 2या 'c'एक अंतर्निहित प्रकार है: एक पूर्णांक शाब्दिक हमेशा प्रकार है int, एक चरित्र शाब्दिक हमेशा प्रकार है char...।
  • फ़ंक्शंस और ऑपरेटर ओवरलोडेड नहीं हैं, उदाहरण के लिए +ऑपरेटर के पास हमेशा टाइप होता है (int, int) -> int। सी के पास ऑपरेटरों के लिए ओवरलोडिंग है ( +हस्ताक्षरित और अहस्ताक्षरित पूर्णांक प्रकारों के लिए और डबल्स के लिए काम करता है) लेकिन कार्यों का कोई अतिभार नहीं है।

इन बाधाओं के तहत, एक नीचे पुनरावर्ती एएसटी प्रकार सजावट एल्गोरिथ्म पर्याप्त हो सकता है (यह केवल प्रकारों के बारे में परवाह करता है , न कि ठोस मूल्यों के बारे में, इसलिए एक संकलन-समय दृष्टिकोण है):

  • प्रत्येक दायरे के लिए, आप सभी दृश्यमान चर (पर्यावरण कहा जाता है) के प्रकारों के लिए एक तालिका रखते हैं। एक घोषणा के बाद int a, आप a: intतालिका में प्रविष्टि जोड़ देंगे ।

  • पत्तियों की टाइपिंग तुच्छ पुनरावृत्ति आधार का मामला है: जैसे शाब्दिक प्रकार 1पहले से ही ज्ञात है, और चर के प्रकार aको पर्यावरण में देखा जा सकता है।

  • पहले से गणना किए गए (नेस्टेड सब-एक्सप्रेशन) ऑपरेंड्स के अनुसार कुछ ऑपरेटर और ऑपरेंड्स के साथ एक एक्सप्रेशन टाइप करने के लिए, हम ऑपरेंड्स पर रिकर्सन का उपयोग करते हैं (इसलिए हम पहले इन सब-एक्सप्रेशंस टाइप करते हैं) और ऑपरेटर से संबंधित टाइपिंग नियमों का पालन करते हैं ।

तो अपने उदाहरण में, 4 * 3और 1 + 2लिखे जाते हैं intक्योंकि 4और 3और 1और 2लिखी जा चुकी हैं intऔर अपने टाइपिंग नियमों का कहना है कि योग या दोनों के उत्पाद int-s एक है int, और इतने पर के लिए (4 * 3) - (1 + 2)

उसके बाद पियर्स के प्रकार और प्रोग्रामिंग लैंग्वेज बुक पढ़ें । मैं Ocaml और λ- पथरी का एक छोटा सा सीखने की सलाह देता हूं

अधिक गतिशील रूप से टाइप की गई भाषाओं (लिस्प जैसी) के लिए क्विनक के लिस्प इन स्माल पीसेज भी पढ़ें

स्कॉट की प्रोग्रामिंग लैंग्वेज प्रैग्मेटिक्स बुक भी पढ़ें

BTW, आपके पास भाषा अज्ञेय टाइपिंग कोड नहीं हो सकता है, क्योंकि टाइप सिस्टम भाषा के शब्दार्थ का एक अनिवार्य हिस्सा है ।


2
C ++ 11 autoसरल कैसे नहीं है? इसके बिना आपको दाईं ओर के प्रकार का पता लगाना होगा, फिर देखें कि बाईं ओर के प्रकार के साथ कोई मेल या रूपांतरण है या नहीं। autoआप के साथ सिर्फ सही पक्ष के प्रकार का पता लगाने और आप कर रहे हैं।
nwp

3
@nwp C ++ auto, C # varऔर गो :=चर परिभाषाओं का सामान्य विचार बहुत सरल है: परिभाषा के दाईं ओर की जाँच करें। परिणामी प्रकार बाईं ओर के चर का प्रकार है। लेकिन शैतान विवरण में है। उदाहरण के लिए, C ++ की परिभाषाएँ स्वयं-संदर्भित हो सकती हैं, इसलिए आप rhs पर घोषित होने वाले चर का उल्लेख कर सकते हैं, जैसे int i = f(&i)। यदि प्रकार का iअनुमान लगाया गया है, तो उपरोक्त एल्गोरिथ्म विफल हो जाएगा: आपको निम्न प्रकार का पता करने की आवश्यकता iहै i। इसके बजाय, आपको पूर्ण एचएम-शैली प्रकार के प्रकार के साथ इंजेक्शन की आवश्यकता होगी।
आमोन

13

सी में (और स्पष्ट रूप से सी के आधार पर सबसे सांख्यिकीय टाइप की गई भाषाएं) प्रत्येक ऑपरेटर को फ़ंक्शन कॉल के लिए सिंटैक्टिक चीनी के रूप में देखा जा सकता है।

तो आपकी अभिव्यक्ति को फिर से लिखा जा सकता है:

int a{operator-(operator-(operator+(1,2),operator*(3,4)),5)};

फिर ओवरलोड रिज़ॉल्यूशन किक करेगा और तय करेगा कि प्रत्येक फ़ंक्शन (int, int)या (const int&, const int&)प्रकार का है।

यह तरीका टाइप रिज़ॉल्यूशन को समझने और अनुसरण करने के लिए आसान बनाता है और (अधिक महत्वपूर्ण बात) इसे लागू करना आसान है। प्रकारों के बारे में जानकारी केवल 1 तरह से बहती है (आंतरिक भावों की ओर से)।

यही कारण है कि double x = 1/2;परिणाम होगा x == 0क्योंकि 1/2एक अंतर अभिव्यक्ति के रूप में मूल्यांकन किया जाता है।


6
सी के लिए लगभग सच है, जहां +फ़ंक्शन कॉल की तरह संभाला नहीं जाता है (क्योंकि इसमें ऑपरेंड के लिए doubleऔर इसके लिए अलग-अलग टाइपिंग intहै)
बेसिल स्टारीनेविच

2
@BasileStarynkevitch: यह अतिभारित कार्यों की एक श्रृंखला की तरह लागू हो जाता है: operator+(int,int), operator+(double,double), operator+(char*,size_t), आदि पार्सर सिर्फ ट्रैक के रखने के लिए है जो एक का चयन किया गया।
राँभना बतख

3
@aschepler कोई भी सुझाव नहीं दे रहा था कि स्रोत और कल्पना-स्तर पर, C ने वास्तव में ओवरलोड किए गए कार्य या ऑपरेटर कार्य किए हैं
बिल्ली

1
बिलकूल नही। केवल यह इंगित करते हुए कि सी पार्सर के मामले में, "फ़ंक्शन कॉल" कुछ और है जिससे आपको निपटना होगा, जो वास्तव में "ऑपरेटरों के रूप में फ़ंक्शन कॉल" के साथ आम नहीं है जैसा कि यहां वर्णित है। वास्तव में, सी में पता लगाने के प्रकार बाहर की f(a,b)तुलना में लगाना थोड़ा आसान है a+b
एशप्लर

2
किसी भी उचित C कंपाइलर के कई चरण होते हैं। पास में (प्रीप्रोसेसर के बाद) आप पार्सर ढूंढते हैं, जो एएसटी बनाता है। यहां यह स्पष्ट रूप से स्पष्ट है कि ऑपरेटर फ़ंक्शन कॉल नहीं कर रहे हैं। लेकिन कोड पीढ़ी में, आपको अब कोई परवाह नहीं है कि किस भाषा के निर्माण ने एएसटी नोड बनाया है। नोड के गुण स्वयं निर्धारित करते हैं कि नोड का इलाज कैसे किया जाता है। विशेष रूप से, + बहुत अच्छी तरह से एक फ़ंक्शन कॉल हो सकता है - यह आमतौर पर प्लेटफ़ॉर्म पर अनुकरण किए गए फ़्लोटिंग पॉइंट गणित के साथ होता है। नकली एफपी गणित का उपयोग करने का निर्णय कोड पीढ़ी में होता है; कोई पूर्व एएसटी अंतर की आवश्यकता है।
एमएसलटर्स

6

अपने एल्गोरिथ्म पर ध्यान केंद्रित करते हुए, इसे नीचे-ऊपर तक बदलने का प्रयास करें। आप प्रकार पीएफ चर और स्थिरांक जानते हैं; परिचालक को परिणाम प्रकार के साथ नोड को टैग करें। पत्ती ऑपरेटर के प्रकार को निर्धारित करें, आपके विचार के विपरीत भी।


6

यह वास्तव में काफी आसान है, जब तक आप +एकल अवधारणा के बजाय विभिन्न प्रकार के कार्यों के बारे में सोचते हैं ।

    int operator=(int)
     /   \
  a(int)  \
        int operator-(int,int)
         /                  \
    int operator-(int,int)    5
         /              \
int operator+(int,int) int operator*(int,int)
    / \                      / \
   1   2                    3   4

दाहिने हाथ की तरफ के पार्सिंग चरण के दौरान, पार्सर पुनः प्राप्त करता है 1, जानता है कि intए, फिर पर्स +, और स्टोर करता है कि "अनसुलझे फ़ंक्शन नाम" के रूप में, फिर यह पार्स करता है 2, जानता है कि यह एक है int, और फिर स्टैक को वापस करता है। +समारोह नोड अब दोनों पैरामीटर प्रकार जानता है, तो हल कर सकते हैं +में int operator+(int, int)है, इसलिए अब यह इस उप-अभिव्यक्ति के प्रकार जानता है, और पार्सर यह मेरी रास्ते पर जारी है।

जैसा कि आप देख सकते हैं, एक बार जब पेड़ पूरी तरह से निर्मित हो जाता है, तो फ़ंक्शन कॉल सहित प्रत्येक नोड, इसके प्रकारों को जानता है। यह महत्वपूर्ण है क्योंकि यह उन कार्यों के लिए अनुमति देता है जो उनके मापदंडों की तुलना में विभिन्न प्रकारों को वापस करते हैं।

char* ptr = itoa(3);

यहाँ, पेड़ है:

    char* itoa(int)
     /           \
  ptr(char*)      3

4

टाइप चेकिंग का आधार यह नहीं है कि कंपाइलर क्या करता है, यह वह है जो भाषा को परिभाषित करता है।

सी भाषा में, प्रत्येक ऑपरेंड का एक प्रकार होता है। "एबीसी" में "कांस्ट चर की सरणी" टाइप है। 1 में "इंट" है। 1L में "लंबी" टाइप है। यदि x और y अभिव्यक्ति हैं, तो x + y के प्रकार और इसी तरह के नियम हैं। इसलिए संकलक को स्पष्ट रूप से भाषा के नियमों का पालन करना पड़ता है।

स्विफ्ट जैसी आधुनिक भाषाओं पर, नियम बहुत अधिक जटिल हैं। सी। की तरह कुछ मामले सरल होते हैं। अन्य मामलों में, कंपाइलर एक अभिव्यक्ति देखता है, पहले ही बताया जा चुका है कि अभिव्यक्ति किस प्रकार की होनी चाहिए, और उसके आधार पर सबएक्सप्रेस के प्रकारों को निर्धारित करता है। यदि x और y विभिन्न प्रकारों के चर हैं, और एक समान अभिव्यक्ति दी गई है, तो उस अभिव्यक्ति का मूल्यांकन अलग तरीके से किया जा सकता है। उदाहरण के लिए 12 * (2/3) असाइन करने पर 8.0 से एक डबल और 0 से इंट को असाइन किया जाएगा। और आपके पास ऐसे मामले हैं जहां कंपाइलर जानता है कि दो प्रकार संबंधित हैं और यह पता लगाते हैं कि वे किस प्रकार पर आधारित हैं।

स्विफ्ट उदाहरण:

var x: Double
var y: Int

x = 12 * (2 / 3)
y = 12 * (2 / 3)

print (x, y)

प्रिंट "8.0, 0"।

असाइनमेंट में x = 12 * (2/3): बाएं हाथ की तरफ एक ज्ञात प्रकार डबल है, इसलिए दाहिने हाथ की तरफ डबल टाइप करना चाहिए। "*" ऑपरेटर डबल लौटाने के लिए केवल एक अधिभार है, और वह डबल * डबल -> डबल है। इसलिए 12 में डबल टाइप होना चाहिए, साथ ही 2 / 3. 12 "IntegerLiteralConvertible" प्रोटोकॉल का समर्थन करता है। डबल के पास "IntegerLiteralConvertible" प्रकार का तर्क लेने वाला एक आरंभिक है, इसलिए 12 को डबल में बदल दिया गया है। 2/3 में डबल टाइप होना चाहिए। डबल लौटाने वाले "/" ऑपरेटर के लिए केवल एक अधिभार है, और वह है डबल / डबल -> डबल। 2 और 3 को डबल में बदला जाता है। 2/3 का परिणाम 0.6666666 है। 12 * (2/3) का परिणाम 8.0 है। 8.0 को सौंपा गया है।

असाइनमेंट y = 12 * (2/3) में, बाएं हाथ की तरफ y में Int है, इसलिए दाहिने हाथ की तरफ में Int टाइप होना चाहिए, इसलिए 12, 2, 3 को परिणाम 2/3 के साथ Int में बदल दिया जाता है। 0, 12 * (2/3) = 0।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.