Computer >> कंप्यूटर ट्यूटोरियल >  >> प्रोग्रामिंग >> Ruby

रूबी इंटरप्रेटर बनाना: भाग 2 – एक कस्टम प्रोग्रामिंग भाषा का निर्माण

गिटहब पर पूर्ण स्रोत

स्टॉफल प्रोग्रामिंग भाषा का पूरा कार्यान्वयन गिटहब पर उपलब्ध है। यदि आपको बग मिलें या कोई प्रश्न हों तो बेझिझक एक इश्यू खोलें।

इस ब्लॉग पोस्ट में, हम स्टॉफल के लिए इंटरप्रेटर को लागू करना जारी रखेंगे, जो पूरी तरह से रूबी में बनी एक खिलौना प्रोग्रामिंग भाषा है। हमने पिछले पोस्ट में इंटरप्रेटर शुरू किया था। आप इस श्रृंखला के पहले भाग में इस परियोजना के बारे में अधिक पढ़ सकते हैं।

पिछले पोस्ट में, हमने स्टॉफल की सरल विशेषताओं को लागू करने का तरीका कवर किया था: वेरिएबल्स, कंडीशनल्स, यूनरी और बाइनरी ऑपरेटर्स, डेटा टाइप्स, और कंसोल पर प्रिंट करना। अब, अपनी आस्तीनें चढ़ाकर शेष चुनौतीपूर्ण हिस्सों से निपटने का समय है: फंक्शन डेफिनिशन, फंक्शन कॉलिंग, वेरिएबल स्कोपिंग, और लूप्स।

पहले की तरह, हम उसी उदाहरण प्रोग्राम का उपयोग करेंगे जो शुरुआत से अंत तक चलता है। हम इसे लाइन दर लाइन देखेंगे, प्रत्येक संरचना को जीवंत करने के लिए इंटरप्रेटर में आवश्यक कार्यान्वयन का पता लगाएंगे। अंत में, हम पिछले लेख में बनाए गए CLI का उपयोग करके प्रोग्राम को चलाकर इंटरप्रेटर को एक्शन में देखेंगे।

गॉस वापस आया

यदि आपकी याददाश्त अच्छी है, तो आपको शायद याद होगा कि श्रृंखला के दूसरे भाग में, हमने लेक्सर बनाने के तरीके पर चर्चा की थी। उस पोस्ट में, हमने स्टॉफल के सिंटैक्स को समझाने के लिए एक श्रृंखला में संख्याओं को जोड़ने वाले प्रोग्राम को देखा था। इस लेख के अंत में, हम अंततः उस प्रोग्राम को चला सकेंगे! तो, यहां वह प्रोग्राम फिर से है:

fn sum_integers: first_integer, last_integer i = first_integer sum = 0 while i <= last_integer sum = sum + i i = i + 1 end println(sum) end sum_integers(1, 100) 

हमारे पूर्णांक योग प्रोग्राम का एब्स्ट्रैक्ट सिंटैक्स ट्री (AST) निम्नलिखित है:

रूबी इंटरप्रेटर बनाना: भाग 2 – एक कस्टम प्रोग्रामिंग भाषा का निर्माण

गणितज्ञ जिसने हमारे स्टॉफल सैंपल प्रोग्राम को प्रेरित किया

कार्ल फ्रेडरिक गॉस ने कथित तौर पर केवल 7 साल की उम्र में खुद ही एक श्रृंखला में संख्याओं को जोड़ने का फॉर्मूला खोज लिया था।

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

फंक्शन डेफिनिशन

हमारे प्रोग्राम में सबसे पहले हम sum_integers फंक्शन को डिफाइन करते हैं। फंक्शन घोषित करने का क्या मतलब है? जैसा कि आप अनुमान लगा रहे होंगे, यह एक वेरिएबल को मान असाइन करने के समान एक क्रिया है। जब हम एक फंक्शन को परिभाषित करते हैं, तो हम एक नाम (यानी फंक्शन का नाम, एक आइडेंटिफायर) को एक या अधिक एक्सप्रेशन्स (यानी फंक्शन की बॉडी) के साथ जोड़ते हैं। हम उन नामों को भी रजिस्टर करते हैं जिनसे फंक्शन कॉल के दौरान पास किए गए मान बंधे होने चाहिए। ये आइडेंटिफायर फंक्शन के निष्पादन के दौरान लोकल वेरिएबल्स बन जाते हैं और पैरामीटर्स कहलाते हैं। फंक्शन कॉल होने पर पास किए गए मान (जो पैरामीटर्स से बंधे होते हैं) आर्गुमेंट्स कहलाते हैं।

आइए #interpret_function_definition पर एक नज़र डालें:

def interpret_function_definition(fn_def) env[fn_def.function_name_as_str] = fn_def end 

काफी सीधा, है ना? जैसा कि आपको शायद इस श्रृंखला के पिछले पोस्ट से याद होगा, जब हमारा इंटरप्रेटर इंस्टेंटिएट होता है, तो हम एक एनवायरनमेंट बनाते हैं। यह प्रोग्राम की स्थिति को रखने की जगह है, और हमारे मामले में यह केवल एक रूबी हैश है। पिछले पोस्ट में, हमने देखा था कि वेरिएबल्स और उनसे बंधे मान env में कैसे स्टोर होते हैं। फंक्शन डेफिनिशन्स भी वहीं स्टोर होंगे। कुंजी फंक्शन का नाम है, और मान फंक्शन को परिभाषित करने वाला AST नोड है (Stoffle::AST::FunctionDefinition)। इस AST नोड पर एक नज़र:

class Stoffle::AST::FunctionDefinition < Stoffle::AST::Expression attr_accessor :name, :params, :body def initialize(fn_name = nil, fn_params = [], fn_body = nil) @name = fn_name @params = fn_params @body = fn_body end def function_name_as_str # इंस्टेंस वेरिएबल @name एक AST::Identifier है। name.name end def ==(other) children == other&.children end def children [name, params, body] end end 

फंक्शन के नाम को Stoffle::AST::FunctionDefinition के साथ जोड़ने का मतलब है कि हम फंक्शन को निष्पादित करने के लिए आवश्यक सभी जानकारी तक पहुंच सकते हैं। उदाहरण के लिए, हमारे पास अपेक्षित आर्गुमेंट्स की संख्या होती है और यदि फंक्शन कॉल उसे प्रदान नहीं करता तो आसानी से एरर दे सकते हैं। यह और अन्य विवरण हम तब देखेंगे जब हम फंक्शन कॉल की व्याख्या करने वाले कोड का पता लगाएंगे।

फंक्शन कॉल करना

हमारे उदाहरण को जारी रखते हुए, अब फंक्शन कॉल पर ध्यान केंद्रित करते हैं। sum_integers फंक्शन को परिभाषित करने के बाद, हम इसे 1 और 100 आर्गुमेंट्स के साथ कॉल करते हैं:

fn sum_integers: first_integer, last_integer i = first_integer sum = 0 while i <= last_integer sum = sum + i i = i + 1 end println(sum) end sum_integers(1, 100) 

फंक्शन कॉल की व्याख्या #interpret_function_call पर होती है:

def interpret_function_call(fn_call) return if println(fn_call) fn_def = fetch_function_definition(fn_call.function_name_as_str) stack_frame = Stoffle::Runtime::StackFrame.new(fn_def, fn_call) assign_function_args_to_params(stack_frame) # फंक्शन बॉडी निष्पादित करना। call_stack << stack_frame value = interpret_nodes(fn_def.body.expressions) call_stack.pop value end 

यह एक जटिल फंक्शन है, इसलिए हमें यहां समय लेना होगा। जैसा कि पिछले लेख में बताया गया था, पहली लाइन यह जांचती है कि कॉल किया जा रहा फंक्शन println तो नहीं है। यदि हम यूज़र-डिफाइन्ड फंक्शन से निपट रहे हैं, जो यहां मामला है, तो हम #fetch_function_definition का उपयोग करके उसकी डेफिनिशन प्राप्त करते हैं। जैसा कि नीचे दिखाया गया है, यह फंक्शन सीधा है, और हम मूल रूप से पहले से एनवायरनमेंट में स्टोर किए गए Stoffle::AST::FunctionDefinition AST नोड को पुनः प्राप्त करते हैं या यदि फंक्शन मौजूद नहीं है तो एरर देते हैं।

def fetch_function_definition(fn_name) fn_def = env[fn_name] raise Stoffle::Error::Runtime::UndefinedFunction.new(fn_name) if fn_def.nil? fn_def end 

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

कॉल स्टैक और स्टैक फ्रेम

ध्यान रखें कि हम कॉल स्टैक और स्टैक फ्रेम शब्दों का उपयोग ढीले अर्थों में कर रहे हैं। प्रोसेसर्स और निम्न-स्तरीय प्रोग्रामिंग भाषाओं में आमतौर पर कॉल स्टैक और स्टैक फ्रेम भी होते हैं, लेकिन वे ठीक वही नहीं हैं जो हमारे खिलौना भाषा में हैं।

यदि इन अवधारणाओं ने आपकी जिज्ञासा बढ़ाई है, तो मैं कॉल स्टैक और स्टैक फ्रेम पर शोध करने की अत्यधिक अनुशंसा करता हूं। यदि आप वास्तव में धातु के करीब जाना चाहते हैं, तो मैं विशेष रूप से प्रोसेसर कॉल स्टैक पर गौर करने का सुझाव दूंगा।

यहां Stoffle::Runtime::StackFrame को लागू करने का कोड है:

module Stoffle module Runtime class StackFrame attr_reader :fn_def, :fn_call, :env def initialize(fn_def_ast, fn_call_ast) @fn_def = fn_def_ast @fn_call = fn_call_ast @env = {} end end end end 

अब, #interpret_function_call पर वापस, अगला कदम फंक्शन कॉल में पास किए गए मानों को संबंधित अपेक्षित पैरामीटर्स को असाइन करना है, जो फंक्शन बॉडी के अंदर स्थानीय वेरिएबल्स के रूप में सुलभ होंगे। #assign_function_args_to_params इस चरण के लिए जिम्मेदार है:

def assign_function_args_to_params(stack_frame) fn_def = stack_frame.fn_def fn_call = stack_frame.fn_call given = fn_call.args.length expected = fn_def.params.length if given != expected raise Stoffle::Error::Runtime::WrongNumArg.new(fn_def.function_name_as_str, given, expected) end # इस विशेष फंक्शन कॉल में पास किए गए मानों को संबंधित परिभाषित पैरामीटर्स पर लागू करना। if fn_def.params != nil fn_def.params.each_with_index do |param, i| if env.has_key?(param.name) # एक ग्लोबल वेरिएबल पहले से परिभाषित है। हम पास किए गए मान को इसे असाइन करते हैं। env[param.name] = interpret_node(fn_call.args[i]) else # उसी नाम का कोई ग्लोबल वेरिएबल मौजूद नहीं है। हम एक नया लोकल वेरिएबल बनाते हैं। stack_frame.env[param.name] = interpret_node(fn_call.args[i]) end end end end 

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

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

#interpret_function_call पर वापस, हम अंत में अपने नवनिर्मित स्टैक फ्रेम को कॉल स्टैक पर पुश करते हैं। फिर, हम अपने पुराने मित्र #interpret_nodes को फंक्शन बॉडी की व्याख्या शुरू करने के लिए कॉल करते हैं:

def interpret_function_call(fn_call) return if println(fn_call) fn_def = fetch_function_definition(fn_call.function_name_as_str) stack_frame = Stoffle::Runtime::StackFrame.new(fn_def, fn_call) assign_function_args_to_params(stack_frame) # फंक्शन बॉडी निष्पादित करना। call_stack << stack_frame value = interpret_nodes(fn_def.body.expressions) call_stack.pop value end 

फंक्शन बॉडी की व्याख्या

अब जब हमने फंक्शन कॉल की व्याख्या कर ली है, तो फंक्शन बॉडी की व्याख्या करने का समय है:

fn sum_integers: first_integer, last_integer i = first_integer sum = 0 while i <= last_integer sum = sum + i i = i + 1 end println(sum) end sum_integers(1, 100) 

हमारे sum_integers फंक्शन की पहली दो लाइनें वेरिएबल असाइनमेंट हैं। हमने इस विषय को इस श्रृंखला के पिछले ब्लॉग पोस्ट में कवर किया था। हालांकि, अब हमारे पास वेरिएबल स्कोपिंग है, और परिणामस्वरूप, असाइनमेंट से निपटने वाला कोड थोड़ा बदल गया है। आइए इसका पता लगाएं:

def interpret_var_binding(var_binding) if call_stack.length > 0 # हम एक फंक्शन के अंदर हैं। यदि नाम ग्लोबल वेरिएबल की ओर इशारा करता है, तो हम मान को इसे असाइन करते हैं। # अन्यथा, हम एक लोकल वेरिएबल बनाते हैं और/या असाइन करते हैं। if env.has_key?(var_binding.var_name_as_str) env[var_binding.var_name_as_str] = interpret_node(var_binding.right) else call_stack.last.env[var_binding.var_name_as_str] = interpret_node(var_binding.right) end else # हम फंक्शन के अंदर नहीं हैं। इसलिए, हम एक ग्लोबल वेरिएबल बनाते हैं और/या असाइन करते हैं। env[var_binding.var_name_as_str] = interpret_node(var_binding.right) end end 

क्या आपको याद है जब हमने फंक्शन कॉल के लिए बनाए गए स्टैक फ्रेम को call_stack में पुश किया था? यह अब काम आता है क्योंकि हम जांच सकते हैं कि call_stack की लंबाई शून्य से अधिक है या नहीं (यानी, इसमें कम से कम एक स्टैक फ्रेम है) यह सत्यापित करके कि हम फंक्शन के अंदर हैं। यदि हम फंक्शन के अंदर हैं, जो वर्तमान में हम जिस कोड की व्याख्या कर रहे हैं उसमें मामला है, तो हम जांचते हैं कि क्या हमारे पास पहले से ही उस वेरिएबल के समान नाम का एक ग्लोबल वेरिएबल है जिसको हम अब मान बांधने का प्रयास कर रहे हैं। जैसा कि आप पहले से जानते हैं, यदि टकराव होता है, तो हम केवल मौजूदा ग्लोबल वेरिएबल को मान असाइन करेंगे, और एक लोकल वेरिएबल नहीं बनेगा। जब नाम उपयोग में नहीं होता है, तो हम एक नया लोकल वेरिएबल बनाते हैं और इच्छित मान को इसे असाइन करते हैं। चूंकि call_stack एक स्टैक है (यानी लास्ट इन फर्स्ट आउट डेटा स्ट्रक्चर), हम जानते हैं कि इस लोकल वेरिएबल को अंतिम स्टैक्ड फ्रेम के env में स्टोर किया जाना चाहिए (यानी वर्तमान में संसाधित किए जा रहे फंक्शन के लिए बनाया गया फ्रेम)। अंत में, #interpret_var_binding का अंतिम भाग फंक्शन्स के बाहर होने वाले असाइनमेंट्स से संबंधित है। चूंकि स्टॉफल में केवल फंक्शन्स ही नए स्कोप बनाते हैं, यहां कुछ भी नहीं बदलता है, क्योंकि फंक्शन्स के बाहर बनाए गए वेरिएबल्स हमेशा ग्लोबल होते हैं और इंस्टेंस वेरिएबल env में स्टोर होते हैं।

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

रूबी इंटरप्रेटर बनाना: भाग 2 – एक कस्टम प्रोग्रामिंग भाषा का निर्माण

लूप को दर्शाने वाला नोड Stoffle::AST::Repetition है:

class Stoffle::AST::Repetition < Stoffle::AST::Expression attr_accessor :condition, :block def initialize(cond_expr = nil, repetition_block = nil) @condition = cond_expr @block = repetition_block end def ==(other) children == other&.children end def children [condition, block] end end 

ध्यान दें कि यह AST नोड मूल रूप से उन अवधारणाओं को एक साथ लाता है जिनका हमने पिछले लेखों में पता लगाया है। इसकी कंडीशनल के लिए, हमारे पास एक एक्सप्रेशन होगा जिसके मूल में आमतौर पर एक Stoffle::AST::BinaryOperator होगा (जैसे '>', 'or', आदि)। लूप की बॉडी के लिए, हमारे पास एक Stoffle::AST::Block होगा। यह समझ में आता है, है ना? लूप का सबसे बुनियादी रूप एक या अधिक एक्सप्रेशन्स (एक ब्लॉक) है जिसे तब तक दोहराया जाता है जब तक एक एक्सप्रेशन सत्य है (यानी जब तक कंडीशनल एक सत्य मान का मूल्यांकन करता है)।

हमारे इंटरप्रेटर में संबंधित मेथड #interpret_repetition है:

def interpret_repetition(repetition) while interpret_node(repetition.condition) interpret_nodes(repetition.block.expressions) end end 

यहां, आप इस मेथड की सरलता (और, मैं कहूंगा, सुंदरता) से आश्चर्यचकित हो सकते हैं। हम उन मेथड्स को संयोजित करके लूप्स की व्याख्या को लागू कर सकते हैं जिनका हमने पिछले लेखों में पहले ही पता लगा लिया है। रूबी के while लूप का उपयोग करके, हम यह सुनिश्चित कर सकते हैं कि हम अपने स्टॉफल लूप को बनाने वाले नोड्स की व्याख्या करना जारी रखें (बार-बार #interpret_nodes को कॉल करके) जब तक कंडीशनल का मूल्यांकन सत्य है। कंडीशनल का मूल्यांकन करने का काम सामान्य संदिग्ध, #interpret_node मेथड को कॉल करना जितना आसान है।

फंक्शन से वापस लौटना

हम फिनिश लाइन के लगभग करीब हैं! लूप के बाद, हम योग के परिणाम को कंसोल पर प्रिंट करते हैं। हम इसे फिर से नहीं देखेंगे क्योंकि हमने इसे श्रृंखला के पिछले भाग में कवर किया था। एक त्वरित पुनर्कथन के रूप में, याद रखें कि println फंक्शन स्टॉफल द्वारा ही प्रदान किया जाता है और इंटरप्रेटर में आंतरिक रूप से, हम केवल रूबी के अपने puts मेथड का उपयोग कर रहे हैं।

इस पोस्ट को समाप्त करने के लिए, हमें #interpret_nodes पर फिर से गौर करना होगा। इसका अंतिम संस्करण कुछ समय पहले देखे गए संस्करण से थोड़ा अलग है। अब, इसमें फंक्शन से वापस लौटने और कॉल स्टैक को अनवाइंड करने को हैंडल करने का कोड शामिल है। यहां #interpret_nodes का पूरा अंतिम संस्करण है:

def interpret_nodes(nodes) last_value = nil nodes.each do |node| last_value = interpret_node(node) if return_detected?(node) raise Stoffle::Error::Runtime::UnexpectedReturn unless call_stack.length > 0 self.unwind_call_stack = call_stack.length # हम वर्तमान स्टैक स्तर को स्टोर करते हैं ताकि पता चले कि कब रुकना है। return last_value end if unwind_call_stack == call_stack.length # हम अभी भी एक फंक्शन के अंदर हैं जिसने रिटर्न किया, इसलिए हम उसकी संरचनाओं (जैसे कंडीशनल्स, लूप्स आदि) से बबल अप करते रहते हैं। return last_value elsif unwind_call_stack > call_stack.length # हम उस फंक्शन से वापस आ गए जिसने रिटर्न किया, इसलिए हम "अनवाइंड इंडिकेटर" को रीसेट करते हैं। self.unwind_call_stack = -1 end end last_value end 

जैसा कि आप पहले से जानते हैं, #interpret_nodes का उपयोग हर बार किया जाता है जब हमें एक्सप्रेशन्स के एक समूह की व्याख्या करने की आवश्यकता होती है। इसका उपयोग हमारे प्रोग्राम की व्याख्या शुरू करने के लिए और हर अवसर पर किया जाता है जब हम उन नोड्स का सामना करते हैं जिनसे एक ब्लॉक जुड़ा होता है (जैसे Stoffle::AST::FunctionDefinition)। विशेष रूप से, फंक्शन्स के साथ व्यवहार करते समय, दो परिदृश्य होते हैं: एक फंक्शन की व्याख्या करना और return एक्सप्रेशन से टकराना या फंक्शन की व्याख्या उसके अंत तक करना और किसी return एक्सप्रेशन से न टकराना। दूसरे मामले में, इसका मतलब है कि या तो फंक्शन में कोई स्पष्ट return एक्सप्रेशन नहीं है या हम जिस कोड पथ से गुजरे उसमें return नहीं था।

आगे बढ़ने से पहले हमारी याददाश्त ताज़ा करते हैं। जैसा कि आप शायद कुछ पैराग्राफ ऊपर से याद करते हैं, #interpret_nodes को तब कॉल किया गया था जब हमने sum_integers फंक्शन की व्याख्या शुरू की थी (यानी जब इसे हमारे प्रोग्राम में कॉल किया गया था)। फिर से, यहां उस प्रोग्राम का स्रोत कोड है जिसे हम देख रहे हैं:

fn sum_integers: first_integer, last_integer i = first_integer sum = 0 while i <= last_integer sum = sum + i i = i + 1 end println(sum) end sum_integers(1, 100) 

हम फंक्शन की व्याख्या के अंत में हैं। जैसा कि आप अनुमान लगा रहे होंगे, हमारे फंक्शन में स्पष्ट return नहीं है। यह #interpret_nodes का सबसे आसान पथ है। हम मूल रूप से फंक्शन के सभी नोड्स के माध्यम से पुनरावृति करते हैं, अंत में अंतिम व्याख्या किए गए एक्सप्रेशन का मान लौटाते हैं (त्वरित अनुस्मारक: स्टॉफल में अंतर्निहित रिटर्न्स हैं)। यह हमें फिनिश लाइन तक ले जाता है, हमारे प्रोग्राम की व्याख्या को समाप्त करता है।

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

सबसे पहले, return एक्सप्रेशन का मूल्यांकन ऑपरेशन की शुरुआत में होता है। इसका मान वही होगा जो वापस किया जा रहा है उसका मूल्यांकन। यहां Stoffle::AST::Return के लिए कोड है:

class Stoffle::AST::Return < Stoffle::AST::Expression attr_accessor :expression def initialize(expr) @expression = expr end def ==(other) children == other&.children end def children [expression] end end 

फिर, हमारे पास एक सरल कंडीशनल है जो return AST नोड्स का पता लगाएगा। ऐसा करने के बाद, हम पहले एक सैनीटी चेक करते हैं यह सत्यापित करने के लिए कि हम एक फंक्शन के अंदर हैं। ऐसा करने के लिए, हम बस कॉल स्टैक की लंबाई की जांच कर सकते हैं। शून्य से अधिक लंबाई का मतलब है कि हम वास्तव में एक फंक्शन के अंदर हैं। स्टॉफल में, हम फंक्शन्स के बाहर return एक्सप्रेशन्स के उपयोग की अनुमति नहीं देते हैं, इसलिए यदि यह जांच विफल होती है तो हम एरर उठाते हैं। प्रोग्रामर द्वारा इच्छित मान को वापस करने से पहले, हम पहले कॉल स्टैक की वर्तमान लंबाई का रिकॉर्ड रखते हैं, इसे इंस्टेंस वेरिएबल unwind_call_stack पर स्टोर करते हैं। यह समझने के लिए कि यह क्यों महत्वपूर्ण है, आइए #interpret_function_call की समीक्षा करें:

def interpret_function_call(fn_call) return if println(fn_call) fn_def = fetch_function_definition(fn_call.function_name_as_str) stack_frame = Stoffle::Runtime::StackFrame.new(fn_def, fn_call) assign_function_args_to_params(stack_frame) # फंक्शन बॉडी निष्पादित करना। call_stack << stack_frame value = interpret_nodes(fn_def.body.expressions) call_stack.pop value end 

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

def interpret_nodes(nodes) # ... nodes.each do |node| # ... if unwind_call_stack == call_stack.length # हम अभी भी एक फंक्शन के अंदर हैं जिसने रिटर्न किया, इसलिए हम उसकी संरचनाओं (जैसे कंडीशनल्स, लूप्स आदि) से बबल अप करते रहते हैं। return last_value elsif unwind_call_stack > call_stack.length # हम उस फंक्शन से वापस आ गए जिसने रिटर्न किया, इसलिए हम "अनवाइंड इंडिकेटर" को रीसेट करते हैं। self.unwind_call_stack = -1 end # ... end # ... end 

यह पहली बार में समझने में थोड़ा मुश्किल हो सकता है। मैं आपको गिटहब पर पूर्ण कार्यान्वयन की जांच करने और अगर आपको लगता है कि यह इंटरप्रेटर के इस अंतिम भाग को समझने में मदद कर सकता है तो इसके साथ खेलने के लिए प्रोत्साहित करता हूं। यहां ध्यान रखने वाला महत्वपूर्ण बिंदु यह है कि एक विशिष्ट प्रोग्राम में कई गहरी नेस्टेड संरचनाएं होती हैं। इसलिए, #interpret_nodes को निष्पादित करने का आमतौर पर परिणाम #interpret_nodes के लिए एक नई कॉल होता है, जिसके परिणामस्वरूप #interpret_nodes के लिए और कॉल हो सकते हैं और इसी तरह! जब हम एक फंक्शन के अंदर return से टकराते हैं, तो हम एक गहरी नेस्टेड संरचना के अंदर हो सकते हैं। कल्पना करें, उदाहरण के लिए, कि return एक कंडीशनल के अंदर है जो एक लूप का हिस्सा है। फंक्शन से वापस लौटने के लिए, हमें सभी #interpret_nodes कॉल्स से वापस लौटना होगा जब तक कि हम #interpret_function_call द्वारा शुरू की गई कॉल से वापस नहीं लौटते (यानी #interpret_nodes की वह कॉल जिसने फंक्शन बॉडी की व्याख्या शुरू की थी)।

ऊपर के कोड सेगमेंट में, हम वास्तव में यह कैसे करते हैं इसे हाइलाइट करते हैं। @unwind_call_stack पर एक सकारात्मक मान रखकर और वह जो कॉल स्टैक की वर्तमान लंबाई के बराबर है, हम निश्चित रूप से जानते हैं कि हम एक फंक्शन के अंदर हैं और हम अभी भी उस मूल कॉल से return नहीं हुए हैं जिसे #interpret_function_call द्वारा शुरू किया गया था। जब यह अंततः होता है, तो @unwind_call_stack कॉल स्टैक की वर्तमान लंबाई से अधिक होगा; इस प्रकार, हम जानते हैं कि हम उस फंक्शन से बाहर निकल चुके हैं जिसने रिटर्न किया था, और हमें बबल अप करने की प्रक्रिया को जारी रखने की आवश्यकता नहीं है। फिर, हम @unwind_call_stack को रीसेट करते हैं। @unwind_call_stack के उपयोग को बिल्कुल स्पष्ट करने के लिए, यहां इसके संभावित मान हैं:

  • -1, इसका प्रारंभिक और तटस्थ मान, यह दर्शाता है कि हम किसी ऐसे फंक्शन के अंदर नहीं हैं जिसने रिटर्न किया हो।
  • कॉल स्टैक लंबाई के बराबर सकारात्मक मान, यह दर्शाता है कि हम अभी भी एक फंक्शन के अंदर हैं जिसने रिटर्न किया।
  • कॉल स्टैक लंबाई से अधिक सकारात्मक मान, यह दर्शाता है कि हम अब उस फंक्शन के अंदर नहीं हैं जिसने रिटर्न किया।

स्टॉफल CLI का उपयोग करके अपने प्रोग्राम को चलाना

श्रृंखला के पिछले लेख में, हमने स्टॉफल प्रोग्राम्स की व्याख्या को आसान बनाने के लिए एक सरल CLI बनाया था। अब जब हमने इंटरप्रेटर के कार्यान्वयन का पता लगा लिया है, तो आइए इसे एक्शन में देखें, हमारे प्रोग्राम को चलाकर। जैसा कि ऊपर कई अलग-अलग खंडों में दिखाया गया है, हमारा कोड sum_integers फंक्शन को परिभाषित करता है और फिर इसे आर्गुमेंट्स 1 और 100 पास करके कॉल करता है। यदि हमारा इंटरप्रेटर ठीक से काम कर रहा है, तो हमें 5050.0 (1 से शुरू होने वाले और 100 पर समाप्त होने वाले पूर्णांकों के समुच्चय का योग) कंसोल पर प्रिंट हुआ देखना चाहिए:

रूबी इंटरप्रेटर बनाना: भाग 2 – एक कस्टम प्रोग्रामिंग भाषा का निर्माण

समापन विचार

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

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


  1. रूबी में फंक्शनल प्रोग्रामिंग में महारत: एक पूर्ण गाइड रूबी में फंक्शनल प्रोग्रामिंग में महारत: एक पूर्ण गाइड

    शायद आपने अभी फंक्शनल प्रोग्रामिंग के बारे में सुना है और आपके कुछ सवाल हैं। जैसे… फंक्शनल प्रोग्रामिंग वास्तव में क्या है? यह ऑब्जेक्ट-ओरिएंटेड प्रोग्रामिंग से कैसे तुलना करता है? क्या आपको रूबी में फंक्शनल प्रोग्रामिंग का उपयोग करना चाहिए? मैं इन सवालों का जवाब दूंगा ताकि आप बेहतर ढंग से समझ सके

  1. रूबी नेटवर्क प्रोग्रामिंग में मास्टर करें: सॉकेट्स के साथ कस्टम क्लाइंट्स और सर्वर्स बनाएं रूबी नेटवर्क प्रोग्रामिंग में मास्टर करें: सॉकेट्स के साथ कस्टम क्लाइंट्स और सर्वर्स बनाएं

    क्या आप रूबी में कस्टम नेटवर्क क्लाइंट्स और सर्वर्स बनाना चाहते हैं? या बस यह समझना चाहते हैं कि यह कैसे काम करता है? तब आपको सॉकेट्स से निपटना होगा। मेरे साथ रूबी नेटवर्क प्रोग्रामिंग के इस दौरे में शामिल हों ताकि मूल बातें सीख सकें, और रूबी का उपयोग करके अन्य सर्वरों और क्लाइंट्स से बात करना शुरू

  1. Gephi और Sigma.js के साथ एक डायनामिक प्रोग्रामिंग लैंग्वेज इन्फ्लुएंस ग्राफ बनाएं Gephi और Sigma.js के साथ एक डायनामिक प्रोग्रामिंग लैंग्वेज इन्फ्लुएंस ग्राफ बनाएं

    Gephi और Sigma.js के साथ एक नेटवर्क विज़ुअलाइज़ेशन ट्यूटोरियलयहां आज हम जो बनाने जा रहे हैं उसका पूर्वावलोकन है: प्रोग्रामिंग लैंग्वेज इन्फ्लुएंस ग्राफ। 250 से अधिक प्रोग्रामिंग लैंग्वेजेज के बीच डिजाइन इन्फ्लुएंस संबंधों का पता लगाने के लिए लिंक देखें!आपकी बारी!आज की हाइपर-कनेक्टेड दुनिया में, नेटव