Github पर पूर्ण स्रोत
स्टॉफ़ल प्रोग्रामिंग भाषा का पूर्ण कार्यान्वयन GitHub पर उपलब्ध है। अगर आपको बग मिलते हैं या आपके कोई प्रश्न हैं, तो बेझिझक कोई समस्या खोलें।
इस ब्लॉग पोस्ट में, हम स्टॉफ़ल के लिए दुभाषिया को लागू करना शुरू करने जा रहे हैं, जो पूरी तरह से रूबी में निर्मित एक खिलौना प्रोग्रामिंग भाषा है। आप इस श्रृंखला के पहले भाग में इस परियोजना के बारे में अधिक पढ़ सकते हैं।
हम जिस दुभाषिया का निर्माण करने जा रहे हैं उसे आमतौर पर ट्री-वॉक दुभाषिया कहा जाता है। इस श्रृंखला की पिछली पोस्ट में, हमने टोकन के एक फ्लैट अनुक्रम को एक ट्री डेटा संरचना (एक सार सिंटैक्स ट्री, या संक्षेप में एएसटी) में बदलने के लिए एक पार्सर का निर्माण किया। जैसा कि आप कल्पना कर रहे होंगे, हमारे दुभाषिया के पास हमारे पार्सर द्वारा बनाए गए एएसटी के माध्यम से जाने और एक स्टॉफ़ल कार्यक्रम में जीवन को सांस लेने का काम है। मुझे यह अंतिम चरण इस भाषा कार्यान्वयन यात्रा में सबसे रोमांचक लगता है। दुभाषिया का निर्माण करते समय, सब कुछ अंत में क्लिक करता है, और हम स्टॉफ़ल कार्यक्रमों को वास्तविक रूप से चलने में सक्षम होते हैं!
मैं दुभाषिया के कार्यान्वयन को दो भागों में दिखाने और समझाने जा रहा हूँ। इस पहले भाग में, हम मूल बातें काम करने जा रहे हैं:चर, सशर्त, यूनरी और बाइनरी ऑपरेटर, डेटा प्रकार, और कंसोल पर प्रिंटिंग। हम अपने दुभाषिया को लागू करने पर दूसरी और आखिरी पोस्ट के लिए अधिक भावपूर्ण सामग्री (फ़ंक्शन परिभाषा, फ़ंक्शन कॉलिंग, लूप, आदि) आरक्षित कर रहे हैं।
लेक्सर और पार्सर का एक त्वरित पुनर्कथन
इससे पहले कि हम इसमें गोता लगाएँ और दुभाषिया को लागू करना शुरू करें, आइए जल्दी से खुद को याद दिलाएँ कि हमने इस श्रृंखला की पिछली पोस्टों में क्या किया था। सबसे पहले, हमने लेक्सर बनाया, जो कच्चे स्रोत कोड को टोकन में बदल देता है। फिर, हमने पार्सर को लागू किया, जो एक ट्री स्ट्रक्चर (एएसटी) में टोकन को मॉर्फ करने के लिए जिम्मेदार घटक है। संक्षेप में, अब तक हमने जो परिवर्तन देखे हैं, वे ये हैं:
राज्य 0:स्रोत
my_var = 1
राज्य 1:Lexer कच्चे स्रोत कोड को टोकन में बदल देता है
[:identifier, :'=', :number]
राज्य 2:पार्सर टोकन को एक सार सिंटेक्स ट्री में बदल देता है
इट्स ऑल अबाउट वॉकिंग
अब जब हमारे पास एएसटी है, तो हमारा काम इस संरचना पर चलने के लिए कोड लिखना है। हमें रूबी कोड लिखना है जो हमारे एएसटी में वर्णित प्रत्येक नोड को जीवन दे सकता है। यदि हमारे पास एक नोड है जो एक चर बंधन का वर्णन करता है, उदाहरण के लिए, हमारा कार्य रूबी कोड लिखना है जो किसी भी तरह से हमारे चर बाध्यकारी अभिव्यक्ति के दाहिने हाथ के परिणाम को संग्रहीत करने में सक्षम है और इस भंडारण स्थान से जुड़ा हुआ है (और के माध्यम से सुलभ) चर को दिया गया नाम।
जैसा कि हमने इस श्रृंखला के पिछले भागों में किया था, हम एक उदाहरण कार्यक्रम को संभालने में शामिल कोड की सभी महत्वपूर्ण पंक्तियों के माध्यम से कार्यान्वयन का पता लगाने जा रहे हैं। स्टॉफ़ल कोड के जिस अंश की हमें व्याख्या करनी है, वह निम्नलिखित है:
num = -2
if num > 0
println("The number is greater than zero.")
else
println("The number is less than or equal to zero.")
end
यह उसी कार्यक्रम के लिए निर्मित एएसटी है:
हमारे चलने का पहला चरण
जैसा कि आपको शायद इस श्रृंखला की पिछली पोस्ट से याद होगा, स्टॉफ़ल एएसटी का मूल हमेशा एक AST::Program
होता है। नोड. इस जड़ में आम तौर पर कई बच्चे होते हैं। उनमें से कुछ उथले होंगे (एक साधारण चर असाइनमेंट के लिए उत्पादित एएसटी के बारे में सोचें)। अन्य बच्चे काफी गहरे उप-वृक्षों की जड़ हो सकते हैं (इसके शरीर के अंदर कई रेखाओं के साथ एक लूप के बारे में सोचें)। यह रूबी कोड है जिसे हमें एएसटी के माध्यम से चलना शुरू करने की आवश्यकता है जो हमारे दुभाषिया को पारित किया गया था:
module Stoffle
class Interpreter
attr_reader :program, :output, :env
def initialize
@output = []
@env = {}
end
def interpret(ast)
@program = ast
interpret_nodes(program.expressions)
end
private
def interpret_nodes(nodes)
last_value = nil
nodes.each do |node|
last_value = interpret_node(node)
end
last_value
end
def interpret_node(node)
interpreter_method = "interpret_#{node.type}"
send(interpreter_method, node)
end
#...
end
end
जब कोई नया Interpreter
तत्काल है, शुरू से ही हम दो आवृत्ति चर बनाते हैं:@output
और @env
. पूर्व की जिम्मेदारी कालानुक्रमिक क्रम में, हमारे कार्यक्रम द्वारा मुद्रित की गई हर चीज को संग्रहीत करना है। स्वचालित परीक्षण या डिबगिंग लिखते समय इस जानकारी को हाथ में रखना बहुत उपयोगी होता है। @env
. की जिम्मेदारी थोड़ा अलग है। हमने इसे "पर्यावरण" के संदर्भ के रूप में नाम दिया है। जैसा कि नाम से पता चलता है, इसका कार्य हमारे चल रहे कार्यक्रम की स्थिति को बनाए रखना है। इसका एक कार्य एक पहचानकर्ता (जैसे, एक चर नाम) और उसके वर्तमान मूल्य के बीच बंधन को लागू करना होगा।
#interpret_nodes
रूट नोड के सभी बच्चों के माध्यम से विधि लूप (AST::Program
) फिर, यह #interpret_node
. को कॉल करता है प्रत्येक व्यक्तिगत नोड के लिए।
#interpret_node
सरल है लेकिन फिर भी दिलचस्प है। यहां, हम वर्तमान में हाथ में नोड प्रकार को संभालने के लिए उपयुक्त विधि को कॉल करने के लिए रूबी मेटाप्रोग्रामिंग का थोड़ा सा उपयोग करते हैं। उदाहरण के लिए, AST::VarBinding
. के लिए नोड, #interpret_var_binding
विधि वह है जिसे कॉल किया जाता है।
हमेशा, हमें वैरिएबल के बारे में बात करनी है
हम जिस उदाहरण प्रोग्राम से गुजर रहे हैं उसके एएसटी में पहला नोड एक AST::VarBinding
है। . इसका @left
एक AST::Identifier
है , और इसका @right
एक AST::UnaryOperator
है . आइए एक चर बाइंडिंग की व्याख्या करने के लिए जिम्मेदार विधि पर एक नज़र डालें:
def interpret_var_binding(var_binding)
env[var_binding.var_name_as_str] = interpret_node(var_binding.right)
end
जैसा कि आप देख सकते हैं, यह काफी सीधा है। हम @env
. में एक की-वैल्यू पेयर जोड़ते हैं (या ओवरराइट करते हैं) हैश।
कुंजी चर का नाम है (#var_name_as_str
var_binding.left.name
. के बराबर एक सहायक विधि है ) फिलहाल, सभी चर वैश्विक हैं। हम अगले पोस्ट में स्कोपिंग को संभालेंगे।
मान असाइनमेंट के दाईं ओर के व्यंजक की व्याख्या का परिणाम है। ऐसा करने के लिए, हम #interpret_node
. का उपयोग करते हैं फिर से। चूँकि हमारे पास एक AST::UnaryOperator
. है दाईं ओर, अगली विधि जिसे कॉल किया जाता है वह है #interpret_unary_operator
:
def interpret_unary_operator(unary_op)
case unary_op.operator
when :'-'
-(interpret_node(unary_op.operand))
else # :'!'
!(interpret_node(unary_op.operand))
end
end
स्टॉफ़ल के समर्थित यूनरी ऑपरेटरों के शब्दार्थ (-
और !
) रूबी में समान हैं। परिणामस्वरूप, कार्यान्वयन आसान नहीं हो सकता:हम रूबी के -
. को लागू करते हैं संकार्य की व्याख्या के परिणाम के लिए संचालिका। सामान्य संदिग्ध, #interpret_node
, यहाँ फिर से प्रकट होता है। जैसा कि आप हमारे कार्यक्रम के एएसटी से याद कर सकते हैं, -
. के लिए संकार्य एक AST::Number
है (संख्या 2
) इसका मतलब है कि हमारा अगला पड़ाव #interpret_number
. पर है :
def interpret_number(number)
number.value
end
#interpret_number
. का क्रियान्वयन केक का एक टुकड़ा है। रूबी फ्लोट को संख्या अक्षर के प्रतिनिधित्व के रूप में अपनाने का हमारा निर्णय (यह लेक्सर में होता है!) यहां भुगतान करता है। @value
AST::Number
. का नोड में पहले से ही संख्याओं का हमारा वांछित आंतरिक प्रतिनिधित्व है, इसलिए हम इसे अभी पुनः प्राप्त करते हैं।
इसके साथ, हम AST::Program
. के पहले प्रत्यक्ष बच्चे की व्याख्या करना समाप्त कर देते हैं . अब, हमारे कार्यक्रम की व्याख्या करने के लिए, हमें इसके अन्य, अधिक बालों वाले, बच्चे को संभालना होगा:एक प्रकार का नोड AST::Conditional
।
नियम और शर्तें लागू हो सकती हैं
वापस #interpret_nodes
. में , हमारा सबसे अच्छा दोस्त #interpret_node
AST::Program
. के अगले प्रत्यक्ष बच्चे की व्याख्या करने के लिए फिर से कॉल किया जाता है ।
def interpret_nodes(nodes)
last_value = nil
nodes.each do |node|
last_value = interpret_node(node)
end
last_value
end
AST::Conditional
. की व्याख्या करने के लिए उत्तरदायी विधि #interpret_conditional
है . हालांकि, इस पर एक नज़र डालने से पहले, आइए AST::Conditional
के कार्यान्वयन की समीक्षा करके अपनी यादों को ताज़ा करें स्वयं:
class Stoffle::AST::Conditional < Stoffle::AST::Expression
attr_accessor :condition, :when_true, :when_false
def initialize(cond_expr = nil, true_block = nil, false_block = nil)
@condition = cond_expr
@when_true = true_block
@when_false = false_block
end
def ==(other)
children == other&.children
end
def children
[condition, when_true, when_false]
end
end
ठीक है, तो @condition
एक अभिव्यक्ति रखता है जो या तो सत्य या असत्य होगा; @when_true
@condition
. के मामले में निष्पादित होने के लिए एक या अधिक अभिव्यक्तियों के साथ एक ब्लॉक रखता है सत्य है, और @when_false
(ELSE
क्लॉज) @condition
. के मामले में चलाने के लिए ब्लॉक रखता है झूठा होता है।
अब, आइए एक नजर डालते हैं #interpret_condition
. पर :
def interpret_conditional(conditional)
evaluated_cond = interpret_node(conditional.condition)
# We could implement the line below in a shorter way, but better to be explicit about truthiness in Stoffle.
if evaluated_cond == nil || evaluated_cond == false
return nil if conditional.when_false.nil?
interpret_nodes(conditional.when_false.expressions)
else
interpret_nodes(conditional.when_true.expressions)
end
end
स्टॉफ़ल में सत्यता रूबी के समान ही है। दूसरे शब्दों में, स्टॉफ़ल में, केवल nil
और false
झूठे हैं। किसी शर्त के लिए कोई अन्य इनपुट सत्य है।
हम पहले conditional.condition
. द्वारा धारित व्यंजक की व्याख्या करके स्थिति का मूल्यांकन करते हैं . आइए अपने कार्यक्रम के एएसटी पर फिर से एक नज़र डालते हैं यह पता लगाने के लिए कि हम किस नोड के साथ काम कर रहे हैं:
यह पता चला है कि हमारे पास एक AST::BinaryOperator
. है (>
num > 0
. में उपयोग किया जाता है ) ठीक है, फिर से वही रास्ता है:पहला #interpret_node
, जो #interpret_binary_operator
. को कॉल करता है इस बार:
def interpret_binary_operator(binary_op)
case binary_op.operator
when :and
interpret_node(binary_op.left) && interpret_node(binary_op.right)
when :or
interpret_node(binary_op.left) || interpret_node(binary_op.right)
else
interpret_node(binary_op.left).send(binary_op.operator, interpret_node(binary_op.right))
end
end
हमारे लॉजिकल ऑपरेटर्स (and
और or
) को बाइनरी ऑपरेटर माना जा सकता है, इसलिए हम उन्हें यहां भी हैंडल करते हैं। चूँकि उनका सिमेंटिक रूबी के &&
. के बराबर है और ||
, कार्यान्वयन सादा नौकायन है, जैसा कि आप ऊपर देख सकते हैं।
आगे उस पद्धति का भाग है जिसमें हम सबसे अधिक रुचि रखते हैं; यह खंड अन्य सभी बाइनरी ऑपरेटरों को संभालता है (>
. सहित) ) यहां, हम अपने पक्ष में रूबी की गतिशीलता का लाभ उठा सकते हैं और एक बहुत ही संक्षिप्त समाधान के साथ आ सकते हैं। रूबी में, बाइनरी ऑपरेटर एक ऑपरेशन में भाग लेने वाली वस्तुओं में विधियों के रूप में उपलब्ध हैं:
-2 > 0 # is equivalent to
-2.send(:'>', 0) # this
# and the following line would be a general solution,
# very similar to what we have in the interpreter
operand_1.send(binary_operator, operand_2)
<ब्लॉकक्वॉट> बाइनरी ऑपरेटर्स का वर्बोज़ इम्प्लीमेंटेशन
जैसा कि आपने देखा, बाइनरी ऑपरेटरों का हमारा कार्यान्वयन बहुत संक्षिप्त है। यदि रूबी इतनी गतिशील भाषा नहीं होती, या रूबी और स्टॉफ़ल के बीच ऑपरेटरों के शब्दार्थ भिन्न होते, तो हम इस तरह से समाधान को कोडित नहीं कर सकते थे।
यदि आप कभी भी खुद को भाषा डिजाइनर/कार्यान्वयनकर्ता के रूप में ऐसी स्थिति में पाते हैं, तो आप हमेशा एक सरल (लेकिन उस सुरुचिपूर्ण नहीं) समाधान पर वापस आ सकते हैं:स्विच निर्माण का उपयोग करना। हमारे मामले में, कार्यान्वयन कुछ इस तरह दिखेगा:
# ... inside #interpret_binary_operator ...
case binary_op.operator
when :'+'
interpret_node(binary_op.left) + interpret_node(binary_op.right)
# ... other operators
end
#interpret_conditional
पर वापस जाने से पहले , आइए यह सुनिश्चित करने के लिए एक त्वरित चक्कर लगाएं कि कुछ भी अनदेखा नहीं किया गया है। यदि आपको वह प्रोग्राम याद है जिसकी हम व्याख्या कर रहे हैं, तो num
चर का उपयोग तुलना में किया जाता है (बाइनरी ऑपरेटर का उपयोग करके >
) हमने अभी एक साथ एक्सप्लोर किया। हमने बाएं ऑपरेंड को कैसे पुनः प्राप्त किया (यानी, num
. में संग्रहीत मान चर) उस तुलना का? इसके लिए जिम्मेदार तरीका है #interpret_identifier
, और इसका क्रियान्वयन आसान-पेसी है:
def interpret_identifier(identifier)
if env.has_key?(identifier.name)
env[identifier.name]
else
# Undefined variable.
raise Stoffle::Error::Runtime::UndefinedVariable.new(identifier.name)
end
end
अब, #interpret_conditional
पर वापस जाएं . हमारे छोटे से कार्यक्रम के मामले में, स्थिति का मूल्यांकन रूबी false
. के रूप में किया जाता है मूल्य। हम इस मान का उपयोग यह निर्धारित करने के लिए करते हैं कि हमें सशर्त संरचना की IF या ELSE शाखा को निष्पादित करना है या नहीं। हम ईएलएसई शाखा की व्याख्या करने के लिए आगे बढ़ते हैं, जिसका संबद्ध कोड ब्लॉक conditional.when_false
में संग्रहीत है। . यहाँ, हमारे पास एक AST::Block
है , जो हमारे AST (AST::Program
. के रूट नोड से बहुत मिलता-जुलता है ) इसी तरह, ब्लॉक में संभावित रूप से अभिव्यक्तियों का एक समूह होता है जिसे व्याख्या करने की आवश्यकता होती है। इस उद्देश्य के लिए, हम #interpret_nodes
. का भी उपयोग करते हैं ।
def interpret_conditional(conditional)
evaluated_cond = interpret_node(conditional.condition)
# We could implement the line below in a shorter way, but better to be explicit about truthiness in Stoffle.
if evaluated_cond == nil || evaluated_cond == false
return nil if conditional.when_false.nil?
interpret_nodes(conditional.when_false.expressions)
else
interpret_nodes(conditional.when_true.expressions)
end
end
अगला एएसटी नोड जिसे हमें संभालना है वह है AST::FunctionCall
. फ़ंक्शन कॉल की व्याख्या करने के लिए जिम्मेदार विधि है #interpret_function_call
:
def interpret_function_call(fn_call)
return if println(fn_call)
end
जैसा कि हमने लेख की शुरुआत में चर्चा की थी, इस श्रृंखला की अगली पोस्ट में फ़ंक्शन परिभाषा और फ़ंक्शन कॉलिंग को कवर किया जाएगा। इसलिए, हम केवल फ़ंक्शन कॉलिंग का एक विशेष मामला लागू कर रहे हैं। अपनी छोटी खिलौना भाषा में, हम println
. प्रदान करते हैं रनटाइम के हिस्से के रूप में और इसे सीधे यहां दुभाषिया में लागू करें। हमारी परियोजना के उद्देश्यों और दायरे को देखते हुए यह एक अच्छा पर्याप्त समाधान है।
def println(fn_call)
return false if fn_call.function_name_as_str != 'println'
result = interpret_node(fn_call.args.first).to_s
output << result
puts result
true
end
हमारे AST::FunctionCall
. का पहला और एकमात्र तर्क एक AST::String
है , जिसे #interpret_string
. द्वारा नियंत्रित किया जाता है :
def interpret_string(string)
string.value
end
#interpret_string
. में , हमारे पास #interpret_number
. का ठीक वैसा ही मामला है . एक AST::String
पहले से ही उपयोग के लिए तैयार रूबी स्ट्रिंग मान रखता है, इसलिए हमें इसे पुनः प्राप्त करना होगा।
अब, #println
पर वापस जाएं :
def println(fn_call)
return false if fn_call.function_name_as_str != 'println'
result = interpret_node(fn_call.args.first).to_s
output << result
puts result
true
end
result
. में फ़ंक्शन तर्क (रूबी स्ट्रिंग में कनवर्ट) को संग्रहीत करने के बाद , हमारे पास पूरा करने के लिए दो और चरण हैं। सबसे पहले, हम जो प्रिंट करने वाले हैं उसे हम कंसोल में @output
. में स्टोर करते हैं . जैसा कि पहले बताया गया है, यहां विचार आसानी से निरीक्षण करने में सक्षम होना है कि क्या मुद्रित किया गया था (और किस क्रम में)। इसे हाथ में रखने से दुभाषिया डिबगिंग या परीक्षण करते समय हमारा जीवन आसान हो जाता है। अंत में, कंसोल पर कुछ प्रिंट करने को लागू करने के लिए, हम रूबी के puts
. का उपयोग करते हैं ।
निष्पादन के मामले
अब जब हमने स्टॉफ़ल की नंगे हड्डियों को लागू करने के लिए आवश्यक सभी चीजों का पता लगा लिया है, तो आइए अपने दुभाषिया को काम करते हुए देखने के लिए एक बहुत ही बुनियादी निष्पादन योग्य बनाएं।
#!/usr/bin/env ruby
require_relative '../lib/stoffle'
path = ARGV[0]
source = File.read(path)
lexer = Stoffle::Lexer.new(source)
parser = Stoffle::Parser.new(lexer.start_tokenization)
interpreter = Stoffle::Interpreter.new
interpreter.interpret(parser.parse)
exit(0)
<ब्लॉकक्वॉट> टिप: स्टॉफ़ल के दुभाषिया का कहीं से भी उपयोग करने के लिए, अपने PATH में एक्ज़ीक्यूटेबल जोड़ना याद रखें।
अंत में हमारे कार्यक्रम को चलाने का समय आ गया है। यदि सब कुछ ठीक से काम करता है, तो हमें कंसोल पर मुद्रित स्ट्रिंग "संख्या शून्य से कम या बराबर है" देखना चाहिए। जब हम दुभाषिया चलाते हैं तो ठीक यही होता है:
<ब्लॉकक्वॉट>
टिप: यदि आपके पास दुभाषिया स्थापित है, तो num
को बदलने का प्रयास करें हमारे नमूना कार्यक्रम में चर ताकि इसमें शून्य से अधिक संख्या हो। जैसा कि अपेक्षित था, अब IF शाखा निष्पादित हो जाएगी, और स्ट्रिंग "संख्या शून्य से अधिक है" मुद्रित की जाएगी।
रैपिंग अप
इस पोस्ट में, हमने स्टॉफ़ल के दुभाषिया की शुरुआत देखी। हमने भाषा की कुछ बुनियादी बातों को संभालने के लिए इसके लिए पर्याप्त दुभाषिया लागू किया:चर, सशर्त, यूनरी और बाइनरी ऑपरेटर, डेटा प्रकार, और कंसोल पर प्रिंटिंग। दुभाषिया पर अगले और अंतिम भाग में, हम अपनी छोटी खिलौना भाषा को डिज़ाइन के अनुसार काम करने के लिए आवश्यक शेष बिट्स से निपटेंगे:चर स्कोपिंग, फ़ंक्शन परिभाषा, फ़ंक्शन कॉलिंग और लूप। मुझे आशा है कि आपको लेख पढ़ने में मज़ा आया (मुझे निश्चित रूप से इसे लिखने में मज़ा आया!), और हम जल्द ही श्रृंखला की अगली पोस्ट में आपसे मिलेंगे!