एक व्याकरण पर आधारित एक लेक्सर लिखते समय क्या प्रक्रिया होती है?


13

व्याकरण, लेक्सर्स और पार्सर्स के बारे में स्पष्टीकरण के उत्तर के माध्यम से पढ़ते समय , उत्तर में कहा गया है कि:

[...] एक बीएनएफ व्याकरण में शाब्दिक विश्लेषण और पार्सिंग के लिए आवश्यक सभी नियम शामिल हैं।

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

यदि लेक्सर्स, साथ ही पर्सर्स, एक ईबीएनएफ / बीएनएफ व्याकरण पर आधारित हैं, तो कोई उस पद्धति का उपयोग करके एक लेक्सर बनाने के बारे में कैसे जाएगा? यही है, मैं किसी दिए गए EBNF / BNF व्याकरण का उपयोग करके कैसे एक लेसर का निर्माण करूंगा?

मैंने कई, कई पोस्ट देखे हैं जो एक गाइड या ब्लूप्रिंट के रूप में EBNF / BNF का उपयोग करते हुए एक पार्सर लिखने के साथ सौदा करते हैं, लेकिन मैं अब तक कोई भी नहीं आया हूं जो कि लेक्सर डिजाइन के साथ बराबर दिखा।

उदाहरण के लिए, निम्नलिखित व्याकरण लें:

input = digit| string ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"', { all characters - '"' }, '"' ;
all characters = ? all visible characters ? ;

कोई ऐसा व्याकरण कैसे बनाएगा जो व्याकरण पर आधारित हो? मैं सोच सकता हूं कि इस तरह के एक व्याकरण से एक पार्सर कैसे लिखा जा सकता है, लेकिन मैं एक लेक्सर के साथ ऐसा करने की अवधारणा को समझने में विफल रहता हूं।

क्या कुछ नियम या तर्क किसी कार्य को पूरा करने के लिए उपयोग किए जाते हैं, जैसे कि एक पार्सर लिखने के साथ? सच कहूँ, मुझे आश्चर्य है कि क्या lexer डिजाइन एक EBNF / BNF व्याकरण का उपयोग करते हैं, अकेले चलो एक पर आधारित हैं।


1 विस्तारित बैकस-नौर रूप और बैकस-नौर रूप

जवाबों:


18

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

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

चूँकि एक चरित्र-दर-वर्ण आधार पर पाठ काफी अस्पष्ट है, इसलिए इससे बहुत अधिक अस्पष्टता होगी जो कि संभालने के लिए कष्टप्रद है। एक नियम की कल्पना करो R → identifier | "for " identifier। जहाँ पहचानकर्ता ASCII अक्षरों से बना है। यदि मैं अस्पष्टता से बचना चाहता हूं, तो मुझे यह निर्धारित करने के लिए अब 4-वर्ण लुकहेड की आवश्यकता है जो कि विकल्प चुना जाना चाहिए। एक लेसर के साथ, पार्सर को केवल यह जांचना होगा कि उसके पास एक IDENTIFIER है या टोकन - 1-टोकन लुकहेड है।

दो स्तरीय व्याकरण।

लेक्सर्स इनपुट वर्णमाला को अधिक सुविधाजनक वर्णमाला में अनुवाद करके काम करते हैं।

एक स्कैनर रहित पार्सर एक व्याकरण (N, P, P, S) का वर्णन करता है, जहाँ गैर-टर्मिनल N व्याकरण में नियमों के बाएँ हाथ के भाग होते हैं, वर्णमाला eg उदाहरण के लिए ASCII वर्ण हैं, प्रस्तुतियों P व्याकरण में नियम हैं। , और प्रारंभ प्रतीक S, पार्सर के शीर्ष स्तर का नियम है।

लेक्सर अब टोकन a, b, c,… की वर्णमाला को परिभाषित करता है। यह मुख्य पार्सर को इन टोकन को वर्णमाला के रूप में उपयोग करने की अनुमति देता है: a = {a, b, c,…}। लेसर के लिए, ये टोकन गैर-टर्मिनल हैं, और प्रारंभ नियम S L S S L → ε है | ए एस | बी एस | सी एस | …, यह है: टोकन का कोई भी क्रम। लेक्सर व्याकरण के नियम इन टोकन को बनाने के लिए आवश्यक सभी नियम हैं।

प्रदर्शन का लाभ नियमित भाषा के रूप में लेक्सर के नियमों को व्यक्त करने से आता है । इन्हें संदर्भ-मुक्त भाषाओं की तुलना में बहुत अधिक कुशलता से प्राप्त किया जा सकता है। विशेष रूप से, नियमित भाषाओं को O (n) स्थान और O (n) समय में पहचाना जा सकता है। व्यवहार में, एक कोड जनरेटर इस तरह के एक लेसर को अत्यधिक कुशल कूद तालिकाओं में बदल सकता है।

अपने व्याकरण से टोकन निकालना।

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

digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;

लेकिन चूंकि यह नियमित है, इसलिए हम आमतौर पर टोकन सिंटैक्स को व्यक्त करने के लिए नियमित अभिव्यक्ति का उपयोग करेंगे। यहाँ रेगेक्स के रूप में उपरोक्त टोकन परिभाषाएँ हैं, जो .NET वर्ण वर्ग बहिष्करण सिंटैक्स और पोसिक्स वर्णक्रमीयता का उपयोग करके लिखी गई हैं:

digit ~ [0-9]
string ~ "[[:print:]-["]]*"

मुख्य पार्सर के लिए व्याकरण में तब शेष नियम होते हैं जो लेक्सर द्वारा नियंत्रित नहीं किए जाते हैं। आपके मामले में, यह सिर्फ:

input = digit | string ;

जब लेक्सर्स का उपयोग आसानी से नहीं किया जा सकता है।

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

  • जब भाषाओं को एम्बेड कर रहे हैं। कुछ भाषाएं आपको कोड को स्ट्रिंग्स में इंटरपोल करने की अनुमति देती हैं "name={expression}":। अभिव्यक्ति सिंटैक्स संदर्भ-मुक्त व्याकरण का हिस्सा है और इसलिए इसे नियमित अभिव्यक्ति द्वारा टोकन नहीं किया जा सकता है। इसे हल करने के लिए, हम या तो लेसर के साथ पार्सर को फिर से जोड़ते हैं, या हम अतिरिक्त टोकन पेश करते हैं जैसे STRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END। एक स्ट्रिंग के लिए व्याकरण नियम तब लग सकता है String → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END:। बेशक अभिव्यक्ति में अन्य तार हो सकते हैं, जो हमें अगली समस्या की ओर ले जाता है।

  • जब टोकन एक दूसरे को शामिल कर सकते हैं। सी जैसी भाषाओं में, कीवर्ड पहचानकर्ताओं से अप्रभेद्य होते हैं। यह पहचानकर्ता से अधिक कीवर्ड को प्राथमिकता देकर लेक्सर में हल किया जाता है। ऐसी रणनीति हमेशा संभव नहीं होती है। एक कॉन्‍फ़‍िगर फ़ाइल की कल्पना करें कि कहां Line → IDENTIFIER " = " REST, जहां कोई भी रेखा के अंत तक कोई भी चरित्र है, भले ही बाकी एक पहचानकर्ता की तरह दिखता हो। एक उदाहरण रेखा होगी a = b c। लेक्सर वास्तव में गूंगा है और यह नहीं जानता है कि टोकन किस क्रम में हो सकता है। इसलिए यदि हम REST के ऊपर IDENTIFIER को प्राथमिकता देते हैं, तो लेक्सर हमें देगा IDENT(a), " = ", IDENT(b), REST( c)। यदि हम IDENTIFIER पर बाकी को प्राथमिकता देते हैं, तो लेक्सर हमें दे देगा REST(a = b c)

    इसे हल करने के लिए, हमें लेसर को पार्सर के साथ फिर से जोड़ना होगा। लेसर को आलसी बनाकर अलगाव को कुछ हद तक बनाए रखा जा सकता है: हर बार जब पार्सर को अगले टोकन की आवश्यकता होती है, तो वह इसे लेसर से अनुरोध करता है और लेक्सर को स्वीकार्य टोकन का सेट बताता है। प्रभावी रूप से, हम प्रत्येक स्थिति के लिए लेक्सर व्याकरण के लिए एक नया शीर्ष-स्तरीय नियम बना रहे हैं। यहाँ, यह कॉल में परिणाम देगा nextToken(IDENT), nextToken(" = "), nextToken(REST), और सब कुछ ठीक काम करता है। इसके लिए एक ऐसे पार्सर की आवश्यकता होती है, जो प्रत्येक स्थान पर स्वीकार्य टोकन का पूरा सेट जानता हो, जो एलआर की तरह नीचे-ऊपर के पार्सर का अर्थ रखता है।

  • जब लेसर को राज्य बनाए रखना है। उदाहरण के लिए पायथन भाषा कोड को कर्ली ब्रेसिज़ से नहीं, बल्कि इंडेंटेशन द्वारा ब्लॉक करती है। एक व्याकरण के भीतर लेआउट-संवेदनशील सिंटैक्स को संभालने के तरीके हैं, लेकिन उन तकनीकों को पायथन के लिए ओवरकिल किया जाता है। इसके बजाय, lexer प्रत्येक पंक्ति के इंडेंटेशन की जांच करता है, और एक नया इंडेंट ब्लॉक पाए जाने पर INDENT टोकन का उत्सर्जन करता है, और ब्लॉक समाप्त होने पर DEDENT टोकन। यह मुख्य व्याकरण को सरल करता है क्योंकि यह अब दिखावा कर सकता है कि टोकन घुंघराले ब्रेसिज़ की तरह हैं। लेक्सर को अब राज्य बनाए रखने की आवश्यकता है: वर्तमान इंडेंटेशन। इसका मतलब है कि लेक्सर तकनीकी रूप से अब एक नियमित भाषा का वर्णन नहीं करता है, लेकिन वास्तव में एक संदर्भ-संवेदनशील भाषा है। सौभाग्य से यह अंतर व्यवहार में प्रासंगिक नहीं है, और पायथन का लेक्सर अभी भी O (n) समय में काम कर सकता है।


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

@Engineer मैंने कुछ संपादन किए हैं। आपके EBNF का उपयोग सीधे पार्सर द्वारा किया जा सकता है। हालांकि, मेरा उदाहरण दिखाता है कि व्याकरण के किन हिस्सों को एक अलग लेक्सर द्वारा संभाला जा सकता है। कोई अन्य नियम अभी भी मुख्य पार्सर द्वारा संभाला जाएगा, लेकिन आपके उदाहरण में बस input = digit | string
अमन

4
स्केनरलेस पार्सर्स का बड़ा फायदा यह है कि वे रचना के लिए बहुत आसान हैं; इसका चरम उदाहरण पार्सर कॉम्बिनेटर लाइब्रेरी हैं, जहां आप पार्सर की रचना के अलावा कुछ नहीं करते हैं। पार्सर्स की रचना करना ऐसे मामलों के लिए दिलचस्प है जैसे ECMAScript-एम्बेडेड-इन-HTML-एम्बेडेड-इन-PHP-स्प्रिंकल-विथ SQL-with-a-टेम्पलेट-भाषा-शीर्ष या रूबी-उदाहरण-एम्बेडेड-इन-मार्कडाउन- एम्बेडेड-रूबी-प्रलेखन-टिप्पणियां या ऐसा कुछ।
जोर्ग डब्ल्यू मित्तग

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

@ मेहरदाद पायथन-स्टाइल लेसर-चालित इंडेंट / डेडेंट टोकन केवल बहुत ही सरल इंडेंटेशन-संवेदी भाषाओं के लिए संभव हैं, और आम तौर पर लागू नहीं होते हैं। एक अधिक सामान्य विकल्प विशेषता व्याकरण हैं, लेकिन मानक उपकरणों में उनके समर्थन की कमी है। विचार यह है कि हम प्रत्येक एएसटी टुकड़े को इसके इंडेंटेशन के साथ एनोटेट करते हैं, और सभी नियमों में बाधा डालते हैं। एट्रिब्यूट्स सीधे कॉम्बिनेटर पार्सिंग के साथ जोड़ने के लिए सरल हैं, जो स्केनरलेस पार्सिंग करना भी आसान बनाता है।
अमन
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.