आज, हम Ruby टेम्पलेटिंग में अपनी यात्रा जारी रखते हैं। लेक्सर के साथ, आइए अगले चरण पर चलते हैं: पार्सर।
पिछली बार, हमने स्ट्रिंग इंटरपोलेशन को देखा और बाद में, अपनी खुद की टेम्पलेटिंग भाषा बनाने में गोता लगाया। हमने एक लेक्सर को लागू करके शुरू किया जो एक टेम्पलेट को पढ़ता है और इसे टोकन की एक स्ट्रीम में परिवर्तित करता है। आज, हम साथ में पार्सर को लागू करेंगे। हम भाषा सिद्धांत में भी अपने पैर की उंगलियों को डुबोएंगे।
चलते हैं!
Abstract Syntax Trees
आइए हमारे सरल उदाहरण टेम्पलेट Welcome to {{name}} पर वापस देखें। लेक्सर का उपयोग करके स्ट्रिंग को टोकनाइज़ करने के बाद, हमें टोकन की एक सूची मिलती है जैसे यह।
Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]
अंततः, हम टेम्पलेट का मूल्यांकन करना चाहते हैं और एक्सप्रेशन को वास्तविक मानों से बदलना चाहते हैं। चीजों को थोड़ा अधिक चुनौतीपूर्ण बनाने के लिए, हम जटिल ब्लॉक एक्सप्रेशन का मूल्यांकन भी करना चाहते हैं, जिससे पुनरावृत्ति और सशर्त की अनुमति मिलती है।
यह करने के लिए, हमें एक abstract syntax tree (AST) उत्पन्न करना होगा जो टेम्पलेट की तार्किक संरचना का वर्णन करता है। ट्री में नोड्स होते हैं जो अन्य नोड्स को संदर्भित कर सकते हैं या टोकन से अतिरिक्त डेटा स्टोर कर सकते हैं।
हमारे सरल उदाहरण के लिए, वांछित abstract syntax tree इस तरह दिखता है:
एक व्याकरण को परिभाषित करना
व्याकरण को परिभाषित करने के लिए, आइए भाषा के सैद्धांतिक आधार के साथ शुरू करें। अन्य प्रोग्रामिंग भाषाओं की तरह, हमारी टेम्पलेटिंग भाषा एक context-free भाषा है और इसलिए इसे context-free grammar द्वारा वर्णित किया जा सकता है। (Wikipedia के विस्तृत विवरण में गणितीय संकेतन आपको डराने न दें। अवधारणा बहुत सीधी है, और व्याकरण को संकेत करने के अधिक डेवलपर-अनुकूल तरीके हैं।)
एक context-free grammar नियमों का एक समूह है जो वर्णन करता है कि एक भाषा के सभी संभावित तार कैसे निर्मित होते हैं। आइए EBNF संकेतन में हमारी टेम्पलेटिंग भाषा के व्याकरण को देखें:
template = statements;
statements = { statement };
statement = CONTENT | expression | block_expression;
expression = OPEN_EXPRESSION, IDENTIFIER, arguments, CLOSE;
block_expression = OPEN_BLOCK, IDENTIFIER, arguments, CLOSE, statements, [ OPEN_INVERSE, CLOSE, statements ], OPEN_END_BLOCK, IDENTIFIER, CLOSE;
arguments = { IDENTIFIER };
प्रत्येक असाइनमेंट एक नियम को परिभाषित करता है। नियम का नाम बाईं ओर है और दाईं ओर हमारे लेक्सर से अन्य नियम (निचले मामले) या टोकन (ऊपरी मामले) का एक समूह है। नियमों और टोकन को कॉमा , का उपयोग करके जोड़ा जा सकता है या पाइप | प्रतीक का उपयोग करके वैकल्पिक किया जा सकता है। कर्ली ब्रेसेस { ... } के अंदर के नियम और टोकन कई बार दोहराए जा सकते हैं। जब वे ब्रैकेट [ ... ] के अंदर होते हैं, तो उन्हें वैकल्पिक माना जाता है।
उपरोक्त व्याकरण यह वर्णन करने का एक संक्षिप्त तरीका है कि एक टेम्पलेट स्टेटमेंट्स से बना होता है। एक स्टेटमेंट या तो एक CONTENT टोकन, एक एक्सप्रेशन, या एक ब्लॉक एक्सप्रेशन होता है। एक एक्सप्रेशन एक OPEN_EXPRESSION टोकन है, जिसके बाद एक IDENTIFIER टोकन, आर्ग्युमेंट्स, और एक CLOSE टोकन होता है। और एक ब्लॉक एक्सप्रेशन इसका सही उदाहरण है कि प्राकृतिक भाषा का उपयोग करके इसका वर्णन करने की कोशिश करने के बजाय ऊपर दिए गए संकेतन का उपयोग करना बेहतर क्यों है।
ऐसे उपकरण हैं जो व्याकरण परिभाषाओं जैसे ऊपर दिए गए से स्वचालित रूप से पार्सर उत्पन्न करते हैं। लेकिन सच्ची Ruby Magic परंपरा में, आइए कुछ मज़ा लें और पार्सर को स्वयं बनाएं, उम्मीद है कि इस प्रक्रिया में एक या दो चीजें सीखेंगे।
पार्सर बनाना
भाषा सिद्धांत को एक तरफ रखकर, आइए वास्तव में पार्सर बनाने में कूदें। आइए एक और भी न्यूनतम, लेकिन फिर भी वैध, टेम्पलेट के साथ शुरू करें: Welcome to Ruby Magic। इस टेम्पलेट में कोई एक्सप्रेशन नहीं है और टोकन की सूची में सिर्फ एक तत्व होता है। यहां यह कैसा दिखता है:
[[:CONTENT, "Welcome to Ruby Magic"]]
सबसे पहले, हम अपनी पार्सर क्लास सेट करते हैं। यह इस तरह दिखती है:
module Magicbars
class Parser
def self.parse(tokens)
new(tokens).parse
end
attr_reader :tokens
def initialize(tokens)
@tokens = tokens
end
def parse
# Parsing starts here
end
end
end
क्लास टोकन की एक सरणी लेती है और इसे स्टोर करती है। इसमें केवल एक सार्वजनिक विधि parse होती है जो टोकन को AST में परिवर्तित करती है।
हमारे व्याकरण पर वापस देखते हुए, शीर्ष नियम template है। इसका तात्पर्य है कि पार्सिंग प्रक्रिया की शुरुआत में parse एक Template नोड लौटाएगा।
नोड्स सरल क्लासेस हैं जिनका अपना कोई व्यवहार नहीं होता। वे केवल अन्य नोड्स को जोड़ते हैं या टोकन से कुछ मान स्टोर करते हैं। यहां Template नोड कैसा दिखता है:
module Magicbars
module Nodes
class Template
attr_reader :statements
def initialize(statements)
@statements = statements
end
end
end
end
हमारे उदाहरण को काम करने के लिए, हमें एक Content नोड की भी आवश्यकता है। यह केवल टोकन से टेक्स्ट कंटेंट ("Welcome to Ruby Magic") को स्टोर करता है।
module Magicbars
module Nodes
class Content
attr_reader :content
def initialize(content)
@content = content
end
end
end
end
अब, आइए पार्स विधि को लागू करें ताकि Template का एक उदाहरण और Content का एक उदाहरण बनाया जा सके और उन्हें सही ढंग से जोड़ा जा सके।
def parse
Magicbars::Nodes::Template.new(parse_content)
end
def parse_content
return unless tokens[0][0] == :CONTENT
Magicbars::Nodes::Content.new(tokens[0][1])
end
जब हम पार्सर चलाते हैं, तो हमें सही परिणाम मिलता है:
Magicbars::Parser.parse(tokens)
# => #>
स्वीकार करते हैं, यह केवल हमारे सरल उदाहरण के लिए काम करता है जिसमें केवल एक कंटेंट नोड है। आइए एक अधिक जटिल उदाहरण पर स्विच करें जिसमें वास्तव में एक एक्सप्रेशन शामिल है: Welcome to {{name}}।
Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]
इसके लिए, हमें एक Expression नोड और एक Identifier नोड की आवश्यकता है। Expression नोड पहचानकर्ता के साथ-साथ किसी भी आर्ग्युमेंट को स्टोर करता है (जो, व्याकरण के अनुसार, शून्य या अधिक Identifier नोड्स की एक सरणी है)। अन्य नोड्स की तरह, यहां देखने के लिए ज्यादा कुछ नहीं है।
module Magicbars
module Nodes
class Expression
attr_reader :identifier, :arguments
def initialize(identifier, arguments)
@identifier = identifier
@arguments = arguments
end
end
end
end
module Magicbars
module Nodes
class Identifier
attr_reader :value
def initialize(value)
@value = value.to_sym
end
end
end
end
नए नोड्स के साथ, आइए parse विधि को संशोधित करें ताकि यह नियमित कंटेंट के साथ-साथ एक्सप्रेशन को भी संभाल सके। हम इसे एक parse_statements विधि पेश करके करते हैं जो तब तक parse_statement को कॉल करती रहती है जब तक यह एक मान नहीं लौटाती।
def parse
Magicbars::Nodes::Template.new(parse_statements)
end
def parse_statements
results = []
while result = parse_statement
results << result
end
results
end
parse_statement स्वयं पहले parse_content को कॉल करता है और यदि वह कोई मान नहीं लौटाता है, तो यह parse_expression को कॉल करता है।
def parse_statement
parse_content || parse_expression
end
क्या आपने देखा कि parse_statement विधि व्याकरण में statement नियम के समान दिखने लगी है? यह वह जगह है जहां व्याक्रम को पहले से स्पष्ट रूप से लिखने का समय लेना बहुत मदद करता है ताकि यह सुनिश्चित किया जा सके कि हम सही रास्ते पर हैं।
अब, आइए parse_content विधि को संशोधित करें ताकि यह केवल पहले टोकन को न देखे। हम इसे इनिशियलाइज़र में एक अतिरिक्त @position इंस्टेंस वेरिएबल पेश करके और वर्तमान टोकन को लाने के लिए इसका उपयोग करके करते हैं।
attr_reader :tokens, :position
def initialize(tokens)
@tokens = tokens
@position = 0
end
# ...
def parse_content
return unless token = tokens[position]
return unless token[0] == :CONTENT
@position += 1
Magicbars::Nodes::Content.new(token[1])
end
parse_content विधि अब वर्तमान टोकन को देखती है और उसके प्रकार की जांच करती है। यदि यह एक CONTENT टोकन है, तो यह स्थिति को बढ़ाता है (क्योंकि वर्तमान टोकन सफलतापूर्वक पार्स हो गया था) और Content नोड बनाने के लिए टोकन के कंटेंट का उपयोग करता है। यदि कोई वर्तमान टोकन नहीं है (क्योंकि हम टोकन के अंत में हैं) या प्रकार मेल नहीं खाता, तो विधि जल्दी बाहर निकलती है और nil लौटाती है।
सुधरे हुए parse_content विधि के साथ, आइए नई parse_expression विधि से निपटें।
def parse_expression
return unless token = tokens[position]
return unless token[0] == :OPEN_EXPRESSION
@position += 1
identifier = parse_identifier
arguments = parse_arguments
if !tokens[position] || tokens[position][0] != :CLOSE
raise "Unexpected token #{tokens[position][0]}. Expected :CLOSE."
end
@position += 1
Magicbars::Nodes::Expression.new(identifier, arguments)
end
सबसे पहले, हम जांचते हैं कि एक वर्तमान टोकन है और उसका प्रकार OPEN_EXPRESSION है। यदि ऐसा है, तो हम अगले टोकन पर आगे बढ़ते हैं और parse_identifier और parse_arguments को कॉल करके क्रमशः पहचानकर्ता और आर्ग्युमेंट्स को पार्स करते हैं। दोनों विधियां संबंधित नोड्स लौटाएंगी और वर्तमान टोकन को आगे बढ़ाएंगी। जब यह हो जाता है, तो हम सुनिश्चित करते हैं कि वर्तमान टोकन मौजूद है और एक :CLOSE टोकन है। यदि यह नहीं है, तो हम एक त्रुटि उठाते हैं। अन्यथा, हम एक अंतिम बार स्थिति को आगे बढ़ाते हैं, नवनिर्मित Expression नोड को लौटाने से पहले।
इस बिंदु पर, हम कुछ पैटर्न उभरते देखते हैं। हम कई बार अगले टोकन पर आगे बढ़ रहे हैं और हम यह भी जांच रहे हैं कि एक वर्तमान टोकन है और उसका प्रकार। चूंकि उसके लिए कोड थोड़ा बोझिल है, आइए दो हेल्पर विधियों को पेश करें।
def expect(*expected_tokens)
upcoming = tokens[position, expected_tokens.size]
if upcoming.map(&:first) == expected_tokens
advance(expected_tokens.size)
upcoming
end
end
def advance(offset = 1)
@position += offset
end
expect विधि टोकन प्रकारों की एक चर संख्या लेती है और उन्हें टोकन स्ट्रीम में अगले टोकन के विरुद्ध जांचती है। यदि वे सभी मेल खाते हैं, तो यह मिलान करने वाले टोकन के पार आगे बढ़ती है और उन्हें लौटाती है। advance विधि केवल दिए गए ऑफसेट द्वारा @position इंस्टेंस वेरिएबल को बढ़ाती है।
ऐसे मामलों के लिए जहां अगले अपेक्षित टोकन के संबंध में कोई लचीलापन नहीं है, हम एक विधि भी पेश करते हैं जो टोकन मेल न खाने पर एक अच्छा त्रुटि संदेश उठाती है।
def need(*required_tokens)
upcoming = tokens[position, required_tokens.size]
expect(*required_tokens) or raise "Unexpected tokens. Expected #{required_tokens.inspect} but got #{upcoming.inspect}"
end
इन हेल्पर विधियों का उपयोग करके, parse_content और parse_expression अब क्लीनर और अधिक पठनीय हैं।
def parse_content
if content = expect(:CONTENT)
Magicbars::Nodes::Content.new(content[0][1])
end
end
def parse_expression
return unless expect(:OPEN_EXPRESSION)
identifier = parse_identifier
arguments = parse_arguments
need(:CLOSE)
Magicbars::Nodes::Expression.new(identifier, arguments)
end
अंत में, आइए parse_identifier और parse_arguments को भी देखें। हेल्पर विधियों के लिए धन्यवाद, parse_identifier विधि parse_content विधि जितनी सरल है। एकमात्र अंतर यह है कि यह एक अन्य नोड प्रकार लौटाता है।
def parse_identifier
if identifier = expect(:IDENTIFIER)
Magicbars::Nodes::Identifier.new(identifier[0][1])
end
end
parse_arguments विधि को लागू करते समय, हमने देखा कि यह parse_statements विधि के लगभग समान है। एकमात्र अंतर यह है कि यह parse_statement के बजाय parse_identifier को कॉल करता है। हम एक और हेल्पर विधि पेश करके डुप्लिकेट तर्क से छुटकारा पा सकते हैं।
def repeat(method)
results = []
while result = send(method)
results << result
end
results
end
repeat विधि दिए गए विधि नाम को कॉल करने के लिए send का उपयोग करती है जब तक कि यह एक नोड नहीं लौटाता। एक बार ऐसा होने पर, एकत्रित परिणाम (या सिर्फ एक खाली सरणी) लौटाए जाते हैं। इस हेल्पर के साथ, दोनों parse_statements और parse_arguments एक-लाइन विधियां बन जाती हैं।
def parse_statements
repeat(:parse_statement)
end
def parse_arguments
repeat(:parse_identifier)
end
इन सभी परिवर्तनों के साथ, आइए टोकन स्ट्रीम को पार्स करने का प्रयास करें:
Magicbars::Parser.parse(tokens)
# => #,
# # >
इसे पढ़ना थोड़ा कठिन है लेकिन यह वास्तव में सही abstract syntax tree है। Template नोड में एक Content और एक Expression स्टेटमेंट है। Content नोड का मान "Welcome to " है और Expression नोड का पहचानकर्ता Identifier नोड है जिसका मान :name है।
ब्लॉक एक्सप्रेशन पार्स करना
हमारे पार्सर कार्यान्वयन को पूरा करने के लिए, हमें अभी भी ब्लॉक एक्सप्रेशन पार्स करने को लागू करना होगा। एक अनुस्मारक के रूप में, यहां वह टेम्पलेट है जिसे हम पार्स करना चाहते हैं:
Welcome to {{name}}! {{#if subscribed}} Thank you for subscribing to our mailing list. {{else}} Please sign up for our mailing list to be notified about new articles! {{/if}} Your friends at {{company_name}}
ऐसा करने के लिए, आइए पहले एक BlockExpression नोड पेश करें। हालांकि यह नोड थोड़ा अधिक डेटा स्टोर करता है, यह कुछ और नहीं करता और इसलिए बहुत रोमांचक नहीं है।
module Magicbars
module Nodes
class BlockExpression
attr_reader :identifier, :arguments, :statements, :inverse_statements
def initialize(identifier, arguments, statements, inverse_statements)
@identifier = identifier
@arguments = arguments
@statements = statements
@inverse_statements = inverse_statements
end
end
end
end
Expression नोड की तरह, यह पहचानकर्ता के साथ-साथ किसी भी आर्ग्युमेंट को स्टोर करता है। इसके अतिरिक्त, यह ब्लॉक के स्टेटमेंट्स और इनवर्स ब्लॉक के स्टेटमेंट्स को भी स्टोर करता है।
व्याकरण पर वापस देखते हुए, हम देखते हैं कि ब्लॉक एक्सप्रेशन को पार्स करने के लिए, हमें parse_block_expression के कॉल के साथ parse_statements विधि में संशोधन करना होगा। यह अब व्याकरण में नियम की तरह दिखता है।
def parse_statement
parse_content || parse_expression || parse_block_expression
end
parse_block_expression विधि स्वयं थोड़ी अधिक जटिल है। लेकिन हमारी हेल्पर विधियों के लिए धन्यवाद, यह अभी भी काफी पठनीय है।
def parse_block_expression
return unless expect(:OPEN_BLOCK)
identifier = parse_identifier
arguments = parse_arguments
need(:CLOSE)
statements = parse_statements
if expect(:OPEN_INVERSE, :CLOSE)
inverse_statements = parse_statements
end
need(:OPEN_END_BLOCK)
if identifier.value != parse_identifier.value
raise("Error. Identifier in closing expression does not match identifier in opening expression")
end
need(:CLOSE)
Magicbars::Nodes::BlockExpression.new(identifier, arguments, statements, inverse_statements)
end
पहला भाग parse_expression विधि के समान है। यह पहचानकर्ता और आर्ग्युमेंट्स के साथ शुरुआती ब्लॉक एक्सप्रेशन को पार्स करता है। बाद में, यह अंदर के ब्लॉक को पार्स करने के लिए parse_statements को कॉल करता है।
एक बार यह हो जाने के बाद, हम {{else}} एक्सप्रेशन की जांच करते हैं, जिसे OPEN_INVERSE टोकन के बाद CLOSE टोकन द्वारा पहचाना जाता है। यदि दोनों टोकन मिलते हैं, तो हम इनवर्स ब्लॉक को पार्स करने के लिए parse_statements को फिर से कॉल करते हैं। अन्यथा, हम उस भाग को पूरी तरह से छोड़ देते हैं।
अंतिम चीज़ के रूप में, हम सुनिश्चित करते हैं कि ओपन ब्लॉक एक्सप्रेशन में उसी पहचानकर्ता का उपयोग करके एक एंड ब्लॉक एक्सप्रेशन है। यदि पहचानकर्ता मेल नहीं खाते, तो हम एक त्रुटि उठाते हैं। अन्यथा, हम एक नया BlockExpression नोड बनाते हैं और इसे लौटाते हैं।
उन्नत ब्लॉक एक्सप्रेशन टेम्पलेट के टोकन के साथ पार्सर को कॉल करने से टेम्पलेट के लिए AST लौटाया जाएगा। मैं यहां उदाहरण आउटपुट शामिल नहीं करूंगा, क्योंकि यह मुश्किल से पठनीय है। इसके बजाय, यहां उत्पन्न AST का एक दृश्य प्रतिनिधित्व है।
क्योंकि हम parse_block_expression के अंदर parse_statements को कॉल कर रहे हैं, ब्लॉक और इनवर्स ब्लॉक दोनों में अधिक एक्सप्रेशन, ब्लॉक एक्सप्रेशन, साथ ही नियमित कंटेंट शामिल हो सकते हैं।
यात्रा जारी है…
हमारी अपनी टेम्पलेटिंग भाषा को लागू करने की दिशा में हमारी यात्रा में हमने अच्छी प्रगति की है। भाषा सिद्धांत में एक संक्षिप्त डुबकी के बाद, हमने अपनी टेम्पलेटिंग भाषा के लिए एक व्याकरण को परिभाषित किया और इसका उपयोग करके स्क्रैच से एक पार्सर को लागू किया।
लेक्सर और पार्सर दोनों के साथ, हम केवल एक इंटरप्रेटर को याद कर रहे हैं जो हमारे टेम्पलेट से इंटरपोलेटेड स्ट्रिंग उत्पन्न करेगा। हम इस भाग को RubyMagic के आगामी संस्करण में कवर करेंगे। जब यह निकले तो अलर्ट पाने के लिए Ruby Magic मेलिंगलिस्ट की सदस्यता लें।