आज, हम रूबी टेम्पलेटिंग में अपनी यात्रा जारी रखते हैं। लेक्सर के स्थान पर, आइए अगले चरण पर चलते हैं:पार्सर।
पिछली बार, हमने स्ट्रिंग इंटरपोलेशन को देखा और बाद में, अपनी खुद की टेम्प्लेटिंग भाषा बनाने में गोता लगाया। हमने एक लेक्सर को लागू करके शुरू किया जो एक टेम्पलेट को पढ़ता है और इसे टोकन की एक धारा में परिवर्तित करता है। आज, हम साथ वाले पार्सर को लागू करेंगे। हम अपने पैर की उंगलियों को थोड़ा भाषा सिद्धांत में भी डुबोएंगे।
ये रहे!
सार सिंटेक्स ट्री
आइए Welcome to {{name}}
में आपका स्वागत है के लिए हमारे सरल उदाहरण टेम्पलेट पर एक नज़र डालते हैं . स्ट्रिंग को टोकन करने के लिए लेक्सर का उपयोग करने के बाद, हमें इस तरह के टोकन की एक सूची मिलती है।
Magicbars::Lexer.tokenize("Welcome to {{name}}")
# => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]]
अंततः, हम टेम्पलेट का मूल्यांकन करना चाहते हैं और व्यंजक को वास्तविक मानों से बदलना चाहते हैं। चीजों को थोड़ा और चुनौतीपूर्ण बनाने के लिए, हम जटिल ब्लॉक अभिव्यक्तियों का मूल्यांकन भी करना चाहते हैं, जिससे दोहराव और सशर्त की अनुमति मिलती है।
ऐसा करने के लिए, हमें एक अमूर्त सिंटैक्स ट्री (एएसटी) उत्पन्न करना होगा जो टेम्पलेट की तार्किक संरचना का वर्णन करता है। ट्री में नोड्स होते हैं जो अन्य नोड्स को संदर्भित कर सकते हैं या टोकन से अतिरिक्त डेटा संग्रहीत कर सकते हैं।
हमारे सरल उदाहरण के लिए, वांछित सार सिंटैक्स ट्री इस तरह दिखता है:
व्याकरण को परिभाषित करना
व्याकरण को परिभाषित करने के लिए, आइए किसी भाषा के सैद्धांतिक आधार से शुरू करें। अन्य प्रोग्रामिंग भाषाओं की तरह, हमारी टेम्प्लेटिंग भाषा एक संदर्भ-मुक्त भाषा है और इसलिए इसे संदर्भ-मुक्त व्याकरण द्वारा वर्णित किया जा सकता है। (विकिपीडिया के विस्तृत विवरणों में गणितीय संकेतन आपको डराने न दें। अवधारणा बहुत सीधी है, और व्याकरण को नोट करने के लिए और अधिक डेवलपर-अनुकूल तरीके हैं।)
एक संदर्भ-मुक्त व्याकरण नियमों का एक समूह है जो वर्णन करता है कि किसी भाषा के सभी संभावित तारों का निर्माण कैसे किया जाता है। आइए 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
टोकन। और एक ब्लॉक एक्सप्रेशन इसका सटीक उदाहरण है कि प्राकृतिक भाषा के साथ इसका वर्णन करने की कोशिश करने के बजाय ऊपर वाले जैसे नोटेशन का उपयोग करना बेहतर क्यों है।
ऐसे उपकरण हैं जो ऊपर की तरह व्याकरण की परिभाषाओं से स्वचालित रूप से पार्सर्स उत्पन्न करते हैं। लेकिन असली रूबी जादू परंपरा में, आइए कुछ मज़ा लें और खुद पार्सर बनाएं, उम्मीद है कि इस प्रक्रिया में एक या दो चीजें सीख रहे हैं।
पार्सर बनाना
एक तरफ भाषा सिद्धांत के साथ, आइए वास्तव में पार्सर के निर्माण में कूदें। आइए एक और अधिक न्यूनतम, लेकिन फिर भी मान्य टेम्पलेट के साथ शुरू करें: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
. कहा जाता है जो टोकन को एएसटी में बदल देता है।
हमारे व्याकरण को देखते हुए, सबसे ऊपर का नियम है 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)
# => #<Magicbars::Nodes::Template:0x00007fe90e939410 @statements=#<Magicbars::Nodes::Content:0x00007fe90e939578 @content="Welcome to Ruby Magic">>
बेशक, यह केवल हमारे सरल उदाहरण के लिए काम करता है जिसमें केवल एक सामग्री नोड है। आइए एक अधिक जटिल उदाहरण पर स्विच करें जिसमें वास्तव में एक अभिव्यक्ति शामिल है: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_identifier
. को कॉल करता है parse_statement
. के बजाय . हम एक और हेल्पर मेथड को शुरू करके डुप्लीकेट लॉजिक से छुटकारा पा सकते हैं।
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)
# => #<Magicbars::Nodes::Template:0x00007f91a602f910
# @statements=
# [#<Magicbars::Nodes::Content:0x00007f91a58802c8 @content="Welcome to ">,
# #<Magicbars::Nodes::Expression:0x00007f91a602fcd0
# @arguments=[],
# @identifier=
# #<Magicbars::Nodes::Identifier:0x00007f91a5880138 @value=:name> >
इसे पढ़ना थोड़ा कठिन है, लेकिन वास्तव में, यह सही अमूर्त सिंटैक्स ट्री है। 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
introduce का परिचय दें नोड. हालांकि यह नोड थोड़ा अधिक डेटा संग्रहीत करता है, यह कुछ और नहीं करता है और इसलिए बहुत रोमांचक नहीं है।
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_statements
में संशोधन करना होगा। parse_block_expression
. पर कॉल के साथ विधि . यह अब व्याकरण के नियम जैसा ही दिखता है।
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
create बनाते हैं नोड और इसे वापस कर दें।
पार्सर को उन्नत ब्लॉक एक्सप्रेशन टेम्पलेट के टोकन के साथ कॉल करने से टेम्पलेट के लिए एएसटी वापस आ जाएगा। मैं यहां उदाहरण आउटपुट शामिल नहीं करूंगा, क्योंकि यह मुश्किल से पठनीय है। इसके बजाय, यहां जेनरेट किए गए एएसटी का एक दृश्य प्रतिनिधित्व है।
क्योंकि हम parse_statements
को कॉल कर रहे हैं parse_block_expression
. के अंदर , ब्लॉक और इनवर्स ब्लॉक दोनों में अधिक एक्सप्रेशन, ब्लॉक एक्सप्रेशन और साथ ही नियमित सामग्री शामिल हो सकती है।
यात्रा जारी है...
हमने अपनी स्वयं की सांकेतिक भाषा को लागू करने की दिशा में अपनी यात्रा के साथ अच्छी प्रगति की है। भाषा सिद्धांत में एक छोटी सी डुबकी के बाद, हमने अपनी टेम्प्लेटिंग भाषा के लिए एक व्याकरण परिभाषित किया और इसका इस्तेमाल शुरू से इसके लिए एक पार्सर को लागू करने के लिए किया।
लेक्सर और पार्सर दोनों के साथ, हम अपने टेम्पलेट से इंटरपोलेटेड स्ट्रिंग उत्पन्न करने के लिए केवल एक दुभाषिया खो रहे हैं। हम इस भाग को RubyMagic के आगामी संस्करण में शामिल करेंगे। रूबी मैजिक मेलिंगलिस्ट के बाहर आने पर सतर्क होने के लिए सदस्यता लें।