सभी को नमस्कार! एक प्रोग्रामर के रूप में, मैं हमेशा अपने कौशल में सुधार करने के तरीकों की तलाश कर रहा हूं। एक शुक्रवार की रात, मेरे दिमाग में विचार आया - "क्या आप मुझे एक कंपाइलर लिखेंगे?"
कौन इसका पता लगाने के लिए परवाह करता है, बिल्ली का स्वागत है।
"संकलक के शास्त्रीय सिद्धांत" वी। ई। कार्पोव के आधार पर, हम संकलन के 5 मुख्य चरणों को अलग कर सकते हैं:
- लेक्सिकल विश्लेषण
- पदच्छेद
- मध्य कोड पीढ़ी
- अनुकूलन
- अंतिम, वस्तु, कोड का सृजन
हर चीज के बारे में, पांच भागों में, आप बहुत सारे वाक्य और लेख लिख सकते हैं। लेकिन, आज, हम पहले दो के बारे में बात करेंगे और मैंने एक अलग पुस्तकालय में उनकी संरचना का चयन कैसे किया।
जब मैंने लिखने का फैसला किया, यहां तक कि संकलक का एक छोटा सा हिस्सा, मैंने यह नहीं सोचा कि मैं किस भाषा के लिए लिख रहा था, इस कारण से, किसी भी भाषा के शाब्दिक और वाक्य विश्लेषण के लिए एक पुस्तकालय था।
tokenization
इससे पहले कि आप एक वाक्यात्मक और शाब्दिक पेड़ का निर्माण करें, परिणामस्वरूप कोड उत्पन्न करें और अन्य स्वादिष्ट चीजें करें, आपको स्रोत कोड को लाइनों, वर्णों, संख्याओं में तोड़ने की आवश्यकता है।
जहां प्रत्येक तत्व में एक सटीक परिभाषित प्रकार होगा। अपरिभाषित प्रकार के टोकन को पार्सिंग के दौरान वाक्यविन्यास त्रुटियों के रूप में माना जाएगा।
संकलन के संदर्भ में, स्रोत कोड को स्रोत-मानचित्र के रूप में माना जाता है, प्रोग्रामर से प्रतिक्रिया के लिए शाब्दिक और पार्सिंग की प्रक्रिया में इसे संग्रहीत करने के लिए अच्छा अभ्यास होगा और स्रोत कोड में सिंटैक्स त्रुटियों का संकेत देगा।
आप एक साधारण नियमित अभिव्यक्ति का उपयोग करके टोकन के एक सरणी में स्रोत कोड को तोड़ सकते हैं:
/\S+/gm
यह अतिरिक्त पार्सिंग स्थितियों के आधार पर बदल सकता है, जैसे: लाइन ब्रेक पार्सिंग, टैब पार्सिंग, स्पेस पार्सिंग।
अलगाव का परिणाम स्रोत कोड के शब्दों की एक सरणी होगी, और शब्द अंतरिक्ष से अंतरिक्ष में पार्स किए जाते हैं, अर्थात्। यह डिजाइन:
let hello = 'world';
इसे टोकन के निम्नलिखित सेट में बदल दिया जाएगा:
["let", "hello", "=", "'world';"]
टोकन के अंतिम सेट को प्राप्त करने के लिए, आपको उनमें से प्रत्येक को अतिरिक्त नियमित अभिव्यक्ति के साथ गुजरना होगा:
/(\W)|(\w+)/gm
परिणाम टोकन की आवश्यकता होगी जो हमें चाहिए:
["let", "hello", "=", "'", "world", "'", ";"]
हमारे द्वारा प्राप्त सभी टोकन, हम स्रोत-मानचित्र में उनके सूचकांकों के साथ, सरणी को लिखते हैं
लेक्सिकल विश्लेषण
अब हमारे पास टोकन की एक सरणी है, हमें उन्हें पार्स करने के लिए पास करने के लिए उनके प्रकार को निर्धारित करने की आवश्यकता है।
टोकन और उनके प्रकारों को परिभाषित करने वाले एल्गोरिथ्म को कहा जाता है - लेक्सर
टोकन और उसका प्रकार, जिसे लेक्सर परिभाषित करता है, टोकन कहलाता है
प्रत्येक टोकन में विशिष्ट रूप से परिभाषित प्रकार हो सकता है, उदाहरण के लिए:
['let', 'const', 'var']
हालांकि, मैंने तथाकथित सॉल्वर'व का उपयोग करके टोकन के प्रकारों के निर्धारण के लिए एक योजना लागू की।
यह निम्नानुसार काम करता है:
1. आप टोकन प्रकारों के लिए स्थिरांक परिभाषित करते हैं:
const DefaultTokenTypes = { KEYWORD: "Keyword", IDENTIFIER: "Identifier", OPERATOR: "Operator", DELIMITER: "Delimiter", LINEBREAK: "Linebreak", STRING: "String", NUMERIC: "Numeric", UNKNOWN: 'Unknown' };
2. इसके बाद, तथाकथित सॉल्वर'ई को निर्धारित करना आवश्यक है:
const solvers = {}; solvers[MyTokenTypes.KEYWORD] = { include: [ 'const', 'let' ] }; solvers[MyTokenTypes.NUMERIC] = { regexp: /^[0-9.]*$/gm }; solvers[DefaultTokenTypes.STRING] = { type: StringSolver, delimiters: ['"', "'", '`'] }; solvers[MyTokenTypes.IDENTIFIER] = { regexp: /^[a-zA-Z_][a-zA-Z_0-9]*$/gm }; solvers[MyTokenTypes.DELIMITER] = { default: true };
जैसा कि आप देख सकते हैं, टोकन में अलग-अलग सेटिंग्स हो सकती हैं:
शामिल करें - शब्दों की एक सरणी, संयोग से, जिसके साथ, टोकन का प्रकार निर्धारित किया जा सकता है।
regexp - एक नियमित अभिव्यक्ति, जिसके साथ संयोग से, टोकन का प्रकार निर्धारित किया जा सकता है।
डिफ़ॉल्ट - अपरिभाषित टोकन के लिए मानक प्रकार।
आप
टाइप पैरामीटर को भी नोटिस कर सकते हैं, जो इंगित करता है कि इस सॉल्वर को
टाइप में निर्दिष्ट एक से विरासत में मिला होना चाहिए
इस मामले में, सॉल्वर स्ट्रिंग को परिभाषित करता है जो कि
सीमांकक में निर्दिष्ट वर्णों में से एक में संलग्न है
3. हम टोकन सरणी के लिए सॉल्वर का उपयोग करते हैं और टाइप किए गए टोकन की एक सरणी प्राप्त करते हैं। किसी दिए गए स्रोत कोड के लिए:
let a = 50;
हमें निम्नलिखित पेड़ मिलते हैं:
[ { "type": "Keyword", "value": "let", "range": [0, 3] }, { "type": "Identifier", "value": "a", "range": [4, 5] }, { "type": "Delimiter", "value": "=", "range": [6, 7] }, { "type": "Numeric", "value": "50", "range": [8, 10] }, { "type": "Delimiter", "value": ";", "range": [10, 11] } ]
स्रोत कोड में खंड की शुरुआत और अंत कहां है।
पदच्छेद
टोकन की एक सरणी प्राप्त करने के बाद, आपको उन्हें पार्स करना चाहिए और स्रोत कोड के वाक्यविन्यास संरचना (पेड़) का निर्धारण करना चाहिए।
पार्सिंग के लिए विभिन्न विकल्प हैं, लेकिन मैंने एक टॉप-डाउन, डायरेक्ट, एल्गोरिथम चुना।
टेम्प्लेट एक सरणी का उपयोग करके एक-एक करके पार्स किए जाते हैं। यदि टेम्पलेट टोकन के वर्तमान अनुक्रम से मेल खाता है - वाक्यविन्यास ट्री में, एक नई शाखा बनाई जाती है।
एक सरणी से एक टेम्पलेट का एक उदाहरण:
let declaration = new SequenceNode({ tokenType: MyTokenTypes.KEYWORD, type: MyNodeTypes.DECLARATION, sequence: [ {type: MyTokenTypes.KEYWORD}, {type: MyTokenTypes.IDENTIFIER}, {type: MyTokenTypes.DELIMITER}, {type: [MyTokenTypes.NUMERIC, MyTokenTypes.STRING]}, ';' ], onError: (e) => {
tokenType - एक मैच के लिए जाँच शुरू करने के लिए, जिसमें से टोकन का वर्णन करता है।
प्रकार - नोड के प्रकार का वर्णन करता है, सभी प्रकारों को भी परिभाषित किया जाना चाहिए, टोकन प्रकारों के समान।
अनुक्रम -
अनुक्रम का एक सरणी, इसमें प्रकार के टोकन, विशिष्ट मूल्य या वाक्यविन्यास के अन्य नोड्स होते हैं।
onError - कॉलबैक, जो कि एक सिंटैक्स त्रुटि
होने पर काम करेगा, इस नोड को पार्स करते समय, यह स्रोत कोड में त्रुटि के प्रकार + को वापस कर देता है।
आइए इस नोड के
अनुक्रम का विश्लेषण करें:
[ {type: MyTokenTypes.KEYWORD},
मैंने विभिन्न प्रयोजनों के लिए, नोड्स के कई रूपों को लागू किया: तत्वों के एक समूह को परिभाषित करना, टोकन के अनुक्रम को परिभाषित करना (तर्क, ब्लॉक)। यह बिना किसी समस्या के पार्सिंग एरो फ़ंक्शन को अनुमति देता है।
आप सॉल्वर और नोड्स की सभी विविधताओं के बारे में पढ़ सकते हैं जिन्हें मैंने इस पुस्तकालय के प्रलेखन में लागू किया था।
सामग्री
→ स्रोत लिंक:
Tyk→
क्लासिकल कंपाइलर थ्योरी