Github पर पूर्ण स्रोत
स्टॉफ़ल प्रोग्रामिंग भाषा का पूर्ण कार्यान्वयन GitHub पर उपलब्ध है। अगर आपको बग मिलते हैं या आपके कोई प्रश्न हैं, तो बेझिझक कोई समस्या खोलें।
इस ब्लॉग पोस्ट में, हम स्टॉफ़ल के लिए दुभाषिया को लागू करना जारी रखेंगे, जो पूरी तरह से रूबी में निर्मित एक खिलौना प्रोग्रामिंग भाषा है। हमने पिछली पोस्ट में दुभाषिया शुरू किया था। आप इस श्रृंखला के पहले भाग में इस परियोजना के बारे में अधिक पढ़ सकते हैं।
पिछली पोस्ट में, हमने कवर किया था कि स्टॉफ़ल की सरल विशेषताओं को कैसे लागू किया जाए:चर, सशर्त, यूनरी और बाइनरी ऑपरेटर, डेटा प्रकार, और कंसोल पर प्रिंटिंग। अब, समय आ गया है कि हम अपनी आस्तीन ऊपर चढ़ाएं और अधिक चुनौतीपूर्ण शेष बिट्स से निपटें:फ़ंक्शन परिभाषा, फ़ंक्शन कॉलिंग, वैरिएबल स्कोपिंग और लूप।
जैसा कि हमने पहले किया था, हम इस पोस्ट के शुरू से अंत तक उसी उदाहरण प्रोग्राम का उपयोग करेंगे। हम अपने स्टॉफ़ल उदाहरण कार्यक्रम में प्रत्येक अलग संरचना को जीवंत करने के लिए दुभाषिया पर आवश्यक कार्यान्वयन की खोज करते हुए लाइन दर लाइन इसके माध्यम से जाएंगे। अंत में, हम दुभाषिया को कार्य करते हुए देखेंगे और श्रृंखला के पिछले लेख में बनाए गए 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)
हमारे पूर्णांक योग कार्यक्रम के लिए सार सिंटैक्स ट्री (एएसटी) निम्नलिखित है:
<ब्लॉकक्वॉट>
वह गणितज्ञ जिसने हमारे स्टॉफ़ल नमूना कार्यक्रम को प्रेरित किया
माना जाता है कि कार्ल फ्रेडरिक गॉस ने केवल 7 साल की उम्र में, एक श्रृंखला में संख्याओं का योग करने के लिए एक सूत्र का पता लगाया था।
जैसा कि आपने देखा होगा, हमारा कार्यक्रम गॉस द्वारा तैयार किए गए सूत्र का उपयोग नहीं करता है। चूंकि हमारे पास आजकल कंप्यूटर हैं, इसलिए हमारे पास इस समस्या को "क्रूर-बल" तरीके से हल करने की विलासिता है। हमारे सिलिकॉन मित्रों को हमारे लिए कड़ी मेहनत करने दें।
फ़ंक्शन परिभाषा
हमारे प्रोग्राम में सबसे पहले हम sum_integers
. को परिभाषित करते हैं समारोह। फ़ंक्शन घोषित करने का क्या अर्थ है? जैसा कि आप अनुमान लगा रहे होंगे, यह एक चर के लिए मान निर्दिष्ट करने के समान एक क्रिया है। जब हम किसी फ़ंक्शन को परिभाषित करते हैं, तो हम एक या अधिक अभिव्यक्तियों (यानी, फ़ंक्शन का शरीर) के साथ एक नाम (यानी, फ़ंक्शन का नाम, एक पहचानकर्ता) को जोड़ रहे हैं। हम यह भी रजिस्टर करते हैं कि फंक्शन कॉल के दौरान किन नामों को पास किया जाना चाहिए। ये पहचानकर्ता फ़ंक्शन के निष्पादन के दौरान स्थानीय चर बन जाते हैं और उन्हें पैरामीटर कहा जाता है। फ़ंक्शन को कॉल करते समय पास किए गए मान (और पैरामीटर से बंधे होते हैं) तर्क होते हैं।
आइए एक नज़र डालते हैं #interpret_function_definition
. पर :
def interpret_function_definition(fn_def)
env[fn_def.function_name_as_str] = fn_def
end
बहुत सीधा, हुह? जैसा कि आपको शायद इस श्रृंखला की पिछली पोस्ट से याद होगा, जब हमारा दुभाषिया तुरंत चालू हो जाता है, तो हम एक वातावरण बनाते हैं। यह एक ऐसी जगह है जिसका उपयोग प्रोग्राम स्टेट को होल्ड करने के लिए किया जाता है, और हमारे मामले में, यह केवल रूबी हैश है। पिछली पोस्ट में, हमने देखा कि कैसे वेरिएबल और उनसे जुड़े मान env
. में स्टोर किए जाते हैं . फ़ंक्शन परिभाषाएं भी वहां संग्रहीत की जाएंगी। कुंजी फ़ंक्शन का नाम है, और मान एएसटी नोड है जिसका उपयोग फ़ंक्शन को परिभाषित करने के लिए किया जाता है (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
# The instance variable @name is an 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)
# Executing the function body.
call_stack << stack_frame
value = interpret_nodes(fn_def.body.expressions)
call_stack.pop
value
end
यह एक जटिल कार्य है, इसलिए हमें यहां अपना समय निकालना होगा। जैसा कि पिछले लेख में बताया गया है, पहली पंक्ति यह जांचने के लिए ज़िम्मेदार है कि कॉल किया जा रहा फ़ंक्शन println
है या नहीं . यदि हम एक उपयोगकर्ता-परिभाषित फ़ंक्शन के साथ काम कर रहे हैं, जो कि यहां मामला है, तो हम आगे बढ़ते हैं और #fetch_function_definition
का उपयोग करके इसकी परिभाषा प्राप्त करते हैं। . जैसा कि नीचे दिखाया गया है, यह फ़ंक्शन एक सादा सेल है, और हम मूल रूप से Stoffle::AST::FunctionDefinition
पुनर्प्राप्त करते हैं। एएसटी नोड जिसे हमने पहले पर्यावरण में संग्रहीत किया था या यदि फ़ंक्शन मौजूद नहीं है तो एक त्रुटि का उत्सर्जन करता है।
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
# Applying the values passed in this particular function call to the respective defined parameters.
if fn_def.params != nil
fn_def.params.each_with_index do |param, i|
if env.has_key?(param.name)
# A global variable is already defined. We assign the passed in value to it.
env[param.name] = interpret_node(fn_call.args[i])
else
# A global variable with the same name doesn't exist. We create a new local variable.
stack_frame.env[param.name] = interpret_node(fn_call.args[i])
end
end
end
end
इससे पहले कि हम #assign_function_args_to_params
explore देखें कार्यान्वयन के लिए, सबसे पहले परिवर्तनीय दायरे पर संक्षेप में चर्चा करना आवश्यक है। यह एक जटिल और सूक्ष्म विषय है। स्टॉफ़ल के लिए, आइए हम बहुत व्यावहारिक बनें और एक सरल उपाय अपनाएं। हमारी छोटी भाषा में, केवल नए क्षेत्र बनाने वाले निर्माण कार्य हैं। इसके अलावा, वैश्विक चर हमेशा पहले आते हैं। परिणामस्वरूप, किसी फ़ंक्शन के बाहर घोषित सभी चर (यानी, पहला उपयोग) वैश्विक हैं और 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)
# Executing the function body.
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
# We are inside a function. If the name points to a global var, we assign the value to it.
# Otherwise, we create and / or assign to a local var.
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
# We are not inside a function. Therefore, we create and / or assign to a global var.
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
पर स्टोर होते हैं। ।
हमारे कार्यक्रम पर लौटते हुए, अगला कदम पूर्णांकों के योग के लिए जिम्मेदार लूप की व्याख्या करना है। आइए हम अपनी याददाश्त को ताज़ा करें और अपने स्टॉफ़ल कार्यक्रम के एएसटी को फिर से देखें:
लूप का प्रतिनिधित्व करने वाला नोड है 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
ध्यान दें कि यह एएसटी नोड मूल रूप से उन अवधारणाओं को एक साथ लाता है जिन्हें हमने पिछले लेखों में खोजा है। इसकी सशर्त के लिए, हमारे पास एक अभिव्यक्ति होगी जो आम तौर पर इसकी जड़ में होगी (अभिव्यक्ति के एएसटी रूट नोड के बारे में सोचें) एक Stoffle::AST::BinaryOperator
(जैसे, '>', 'या' इत्यादि)। लूप की बॉडी के लिए, हमारे पास एक 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 # We store the current stack level to know when to stop returning.
return last_value
end
if unwind_call_stack == call_stack.length
# We are still inside a function that returned, so we keep on bubbling up from its structures (e.g., conditionals, loops etc).
return last_value
elsif unwind_call_stack > call_stack.length
# We returned from the function, so we reset the "unwind indicator".
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
. का पता लगाएगी एएसटी नोड्स। ऐसा करने के बाद, हम पहले यह सत्यापित करने के लिए एक विवेक जांच करते हैं कि हम किसी फ़ंक्शन के अंदर हैं। ऐसा करने के लिए, हम बस कॉल स्टैक की लंबाई की जांच कर सकते हैं। शून्य से अधिक लंबाई का मतलब है कि हम वास्तव में एक फ़ंक्शन के अंदर हैं। Stoffle में, हम 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)
# Executing the function body.
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
# We are still inside a function that returned, so we keep on bubbling up from its structures (e.g., conditionals, loops etc).
return last_value
elsif unwind_call_stack > call_stack.length
# We returned from the function, so we reset the "unwind indicator".
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 , इसका प्रारंभिक और तटस्थ मूल्य, यह दर्शाता है कि हम किसी भी फ़ंक्शन के अंदर नहीं हैं जो लौटा है।
- कॉल स्टैक की लंबाई के बराबर सकारात्मक मान , यह दर्शाता है कि हम अभी भी एक फ़ंक्शन के अंदर हैं जो वापस आ गया है।
- सकारात्मक मान कॉल स्टैक की लंबाई से अधिक है , यह दर्शाता है कि अब हम उस फ़ंक्शन के अंदर नहीं हैं जो लौटा है।
स्टोफ़ल सीएलआई का उपयोग करके अपना प्रोग्राम चलाना
श्रृंखला के पिछले लेख में, हमने स्टॉफ़ल कार्यक्रमों की व्याख्या को आसान बनाने के लिए एक सरल सीएलआई बनाया था। अब जब हमने दुभाषिया के कार्यान्वयन का पता लगा लिया है, तो आइए इसे अपने कार्यक्रम को चलाते हुए क्रिया में देखें। जैसा कि ऊपर कई अलग-अलग अनुभागों में दिखाया गया है, हमारा कोड परिभाषित करता है और फिर sum_integers
. को कॉल करता है तर्कों को पारित करने वाला कार्य 1
और 100
. यदि हमारा दुभाषिया ठीक से काम कर रहा है, तो हमें 5050.0
. देखना चाहिए (पूर्णांकों के सेट का योग 1 से शुरू होकर 100 पर समाप्त होता है) कंसोल पर प्रिंट होता है:
समापन विचार
इस पोस्ट में, हमने अपने दुभाषिया को पूरा करने के लिए आवश्यक अंतिम भागों को लागू किया। हमने फंक्शन डेफिनिशन, फंक्शन कॉलिंग, वेरिएबल स्कोपिंग और लूप्स का सामना किया। अब, हमारे पास एक सरल लेकिन कार्यशील प्रोग्रामिंग भाषा है!
इस श्रृंखला के अगले और अंतिम भाग में, मैं कुछ संसाधनों को साझा करूंगा, जिन्हें मैं उन लोगों के लिए बढ़िया विकल्प मानता हूं जो अपनी प्रोग्रामिंग भाषा कार्यान्वयन अध्ययन जारी रखना चाहते हैं। मैं कुछ चुनौतियों का भी प्रस्ताव दूंगा जो आप स्टॉफ़ल के अपने संस्करण में सुधार करते हुए अपने सीखने को जारी रखने के लिए ले सकते हैं। बाद में मिलते हैं; सियाओ!