एक 2d सरणी और एक विशाल स्विच के रूप में एक लेक्सर क्यों लागू करें?


24

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

लेकिन प्रोफेसर और पुस्तक दोनों ने उन्हें संक्रमण तालिकाओं के माध्यम से लागू करने का प्रस्ताव दिया, जो एक विशाल 2d सरणी (एक आयाम के रूप में विभिन्न गैर-टर्मिनल राज्यों, और दूसरे के रूप में संभव इनपुट प्रतीकों) और टर्मिनलों के सभी को संभालने के लिए एक स्विच स्टेटमेंट की राशि है। यदि गैर-टर्मिनल स्थिति में संक्रमण तालिकाओं के लिए प्रेषण।

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

तो सौदा क्या है? विषय पर निश्चित पुस्तक इस तरह से करने के लिए क्यों कह रही है?

क्या फ़ंक्शन कॉल का ओवरहेड वास्तव में इतना है? क्या यह कुछ ऐसा है जो अच्छी तरह से काम करता है या आवश्यक है जब व्याकरण समय से पहले (नियमित अभिव्यक्ति?) नहीं जाना जाता है? या शायद कुछ ऐसा जो सभी मामलों को संभालता है, भले ही अधिक विशिष्ट समाधान अधिक विशिष्ट व्याकरण के लिए बेहतर काम करेंगे?

( ध्यान दें: संभव डुप्लिकेट " क्यों एक विशाल स्विच स्टेटमेंट के बजाय एक ओओ दृष्टिकोण का उपयोग करें? " करीब है, लेकिन मुझे ओओ के बारे में परवाह नहीं है। स्टैंडअलोन कार्यों के साथ एक कार्यात्मक दृष्टिकोण या यहां तक ​​कि सैनर अनिवार्य दृष्टिकोण ठीक होगा।)

और उदाहरण के लिए, एक ऐसी भाषा पर विचार करें जिसमें केवल पहचानकर्ता हों, और वे पहचानकर्ता हों [a-zA-Z]+। DFA कार्यान्वयन में, आपको कुछ ऐसा मिलेगा:

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(हालांकि कुछ ऐसा है जो फ़ाइल के अंत को सही ढंग से संभाल लेगा)

मेरी अपेक्षा के अनुसार:

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

NextTokenDFA की शुरुआत से कई गंतव्यों के होने के बाद, अपने स्वयं के फंक्शन में कोड के साथ ।


5
एक प्राचीन (1977) की विरासत संकलक डिजाइन के सिद्धांतों ? 40 साल पहले, कोडिंग शैली बहुत अलग थी
gnat

7
आप डीएफए राज्यों के बदलावों को कैसे लागू करेंगे? और टर्मिनलों और गैर-टर्मिनलों के बारे में यह क्या है, "गैर-टर्मिनल" आमतौर पर व्याकरण में उत्पादन नियमों को संदर्भित करता है, जो कि शाब्दिक विश्लेषण के बाद आएगा ।

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

5
अगर आपकी जलन का कुछ हिस्सा यह जानना है कि एक बेहतर काम कैसे किया जाए और आपके पास किसी दृष्टिकोण के लिए कोई प्रतिक्रिया या प्रशंसा प्राप्त करने की क्षमता का अभाव है - जैसा कि उद्योग में दशकों से हमें प्रतिक्रिया की उम्मीद करने के लिए प्रशिक्षित किया जाता है और कई बार सराहना की जाती है - शायद आपको अपने बेहतर कार्यान्वयन को लिखना चाहिए और इसे अपने स्वयं के मन की शांति के लिए कुछ पाने के लिए CodeReview.SE को पोस्ट करना चाहिए।
जिमी हॉफ

7
इसका सरल उत्तर यह है कि लेक्सर को आमतौर पर एक परिमित राज्य मशीन के रूप में लागू किया जाता है और व्याकरण से स्वचालित रूप से उत्पन्न होता है - और एक राज्य तालिका आश्चर्यजनक रूप से नहीं, सबसे आसानी से और कॉम्पैक्ट रूप से एक तालिका के रूप में प्रस्तुत की जाती है। ऑब्जेक्ट कोड के साथ, यह तथ्य कि मनुष्यों के साथ काम करना आसान नहीं है अप्रासंगिक है क्योंकि मनुष्य इसके साथ काम नहीं करते हैं; वे स्रोत बदलते हैं और एक नया उदाहरण उत्पन्न करते हैं।
केश्लाम

जवाबों:


16

व्यवहार में ये टेबल नियमित भाव से उत्पन्न होते हैं जो भाषा के टोकन को परिभाषित करते हैं:

number := [digit][digit|underscore]+
reserved_word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

लेक्स लिखे जाने के बाद से हमारे पास 1975 से लेक्सिकल एनालिसिस करने के लिए उपयोगिताएँ हैं ।

आप मूल रूप से प्रक्रियात्मक कोड के साथ नियमित अभिव्यक्ति को बदलने का सुझाव दे रहे हैं। यह कुछ वर्णों को एक नियमित अभिव्यक्ति में कोड की कई पंक्तियों में विस्तारित करता है। किसी भी दिलचस्प भाषा के शाब्दिक विश्लेषण के लिए हस्तलिखित प्रक्रियात्मक कोड दोनों को अक्षम और बनाए रखने में मुश्किल होता है।


4
मुझे यकीन नहीं है कि मैं सुझाव दे रहा हूं कि थोक। नियमित अभिव्यक्तियाँ मनमानी (नियमित) भाषाओं से निपटेंगी। विशिष्ट भाषाओं के साथ काम करते समय बेहतर दृष्टिकोण नहीं हैं? पुस्तक भविष्य कहनेवाला दृष्टिकोण को छूती है लेकिन फिर उन्हें उदाहरणों में अनदेखा कर देती है। इसके अलावा, C # सालों पहले एक भोले-भाले विश्लेषक का काम करने के बाद मुझे इसे बनाए रखना बहुत मुश्किल नहीं लगा। अकुशल? यकीन है, लेकिन बहुत नहीं तो उस समय मेरे कौशल दिया।
तेलस्टीन

1
@ टेलस्टाइन: टेबल-चालित डीएफए की तुलना में तेजी से जाना लगभग असंभव है: अगला चरित्र प्राप्त करें, संक्रमण तालिका में अगली स्थिति देखें, स्थिति बदलें। यदि नया राज्य टर्मिनल है, तो एक टोकन का उत्सर्जन करें। C # या Java में किसी भी अस्थायी स्ट्रिंग्स को बनाने वाला कोई भी तरीका धीमा होगा।
केविन क्लाइन

@kevincline - यकीन है, लेकिन मेरे उदाहरण में अस्थायी तार नहीं हैं। यहां तक ​​कि सी में भी यह केवल एक इंडेक्स या स्ट्रिंग के माध्यम से एक संकेतक होगा।
तेलेस्टिन

6
@ जिमीहॉफ़: हाँ, प्रदर्शन निश्चित रूप से संकलनकर्ताओं में प्रासंगिक है। कंपाइलर तेज़ हैं क्योंकि उन्हें नरक और पीठ के लिए अनुकूलित किया गया है। सूक्ष्म-अनुकूलन नहीं, वे अनावश्यक काम नहीं करते हैं और अनावश्यक वस्तुओं को अस्थायी रूप से त्यागते हैं। मेरे अनुभव में, अधिकांश व्यावसायिक टेक्स्ट प्रोसेसिंग कोड एक आधुनिक कंपाइलर का दसवां काम करता है और इसे करने में दस गुना समय लगता है। प्रदर्शन बहुत बड़ा है जब आप पाठ के एक गीगाबाइट को संसाधित कर रहे हैं।
केविन क्लाइन

1
@Telastyn, आपके मन में "बेहतर दृष्टिकोण" क्या था, और आप किस तरीके से "बेहतर" होने की उम्मीद करेंगे? यह देखते हुए कि हमारे पास पहले से ही लेक्सिंग उपकरण हैं जो अच्छी तरह से जांचे जाते हैं, और वे बहुत तेज पार्सर पैदा करते हैं (जैसा कि अन्य लोगों ने कहा है, टेबल-चालित डीएफए बहुत तेज हैं), यह उनका उपयोग करने के लिए समझ में आता है। हम एक विशिष्ट भाषा के लिए एक नया विशेष दृष्टिकोण क्यों आविष्कार करना चाहेंगे, जब हम सिर्फ एक व्याकरण लिख सकते हैं? लेक्स व्याकरण अधिक अनुरक्षणीय है, और परिणामी पार्सर के सही होने की संभावना अधिक है (यह देखते हुए कि कितनी अच्छी तरह से परीक्षण किया गया लेक्स और इसी तरह के उपकरण हैं)।
डीडब्ल्यू

7

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

आपका कोड हाथ से लिखे DFA को बनाए रखने वाले किसी व्यक्ति के लिए क्लीनर है, लेकिन सिखाई जा रही अवधारणाओं से थोड़ा दूर।


7

का आंतरिक लूप:

                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;

प्रदर्शन के बहुत सारे फायदे हैं। इसमें कोई शाखाएं नहीं हैं, क्योंकि आप हर इनपुट चरित्र के लिए एक ही काम करते हैं। कंपाइलर के प्रदर्शन को लेक्सर (जो कि इनपुट के हर चरित्र के पैमाने पर काम करना चाहिए) द्वारा गेट किया जा सकता है। यह और भी सच था जब ड्रैगन बुक लिखी गई थी।

व्यवहार में, सीएस के छात्रों को पढ़ने के अलावा, किसी को भी उस आंतरिक लूप को लागू (या डिबग) नहीं करना पड़ता क्योंकि यह बॉयलरप्लेट का हिस्सा होता है जो transitionतालिका बनाने वाले उपकरण के साथ आता है ।


5

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

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


2

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

कई टिप्पणियों के बावजूद, इसका उस कोड की शैली से कोई लेना-देना नहीं है जो 40, 50, 60, ... में लिखी गई थी, इसका एक व्यावहारिक समझ हासिल करने के साथ करना होगा कि उपकरण आपके लिए क्या कर रहे हैं और आपके पास क्या है उन्हें काम करने के लिए क्या करना है। यह मौलिक समझ के साथ करने के लिए सब कुछ है कि कैसे संकलक एक सैद्धांतिक और व्यावहारिक दोनों दृष्टिकोण से काम करते हैं।

उम्मीद है, आपका प्रशिक्षक आपको lex और yacc का उपयोग करने देगा (जब तक कि यह स्नातक स्तर की कक्षा न हो और आपको lex और yacc लिखना हो)।


0

पार्टी के लिए देर से :-) नियमित अभिव्यक्ति के खिलाफ टोकन का मिलान किया जाता है। चूंकि उनमें से कई हैं, आपके पास मल्टी रेगेक्स इंजन है, जो बदले में विशाल डीएफए है।

"इससे भी बदतर, मैं नहीं देख सकता कि अगर भाषा यूटीएफ सक्षम होती तो यह कैसे दूर से व्यावहारिक होता।"

यह अप्रासंगिक (या पारदर्शी) है। इसके अलावा UTF के पास अच्छी संपत्ति है क्योंकि इसकी इकाइयाँ आंशिक रूप से भी ओवरलैप नहीं होती हैं। उदाहरण के लिए "A" (ASCII-7 टेबल से) चरित्र का बाइट किसी अन्य UTF वर्ण के लिए फिर से उपयोग नहीं किया जाता है।

तो, आपके पास पूरे लीकर के लिए एकल डीएफए (जो मल्टी-रेगेक्स) है। 2d सरणी से इसे लिखना कितना बेहतर है?

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