আজ, আমরা রুবি টেমপ্লেটিং-এ আমাদের যাত্রা চালিয়ে যাচ্ছি। লেক্সার inplace থাকায়, চলুন পরবর্তী ধাপে যাই: পার্সার।
গতবারেরা, আমরা স্ট্রিং ইন্টারপোলেশন দেখেছিলাম এবং তারপর, আমাদের নিজস্ব টেমপ্লেটিং ল্যাঙ্গুয়েজ তৈরি করার中略ে ডুব দিয়েছিলাম। আমরা শুরু করেছিলাম একটি লেক্সার ইমপ্লিমেন্ট করে যা একটি টেমপ্লেট পড়ে এবং সেটিকে টোকেনের একটি স্ট্রিমে কনভার্ট করে। আজ, আমরা সাথে পার্সার ইমপ্লিমেন্ট করব। আমরা ভাষা তত্ত্বের দিকে একটু ডুব দিব।
চলুন শুরু করি!
অ্যাবস্ট্র্যাক্ট সিনট্যাক্স ট্রি
আমাদের সিম্পল উদাহরণ টেমপ্লেট Welcome to {{name}}-এ ফিরে যাই। লেক্সার ব্যবহার করে স্ট্রিং টোকেনাইজ করার পর, আমরা একটি টোকেন লিস্ট পাই এরকম:
Magicbars::Lexer.tokenize("Welcome to {{name}}") # => [[:CONTENT, "Welcome to "], [:OPEN_EXPRESSION], [:IDENTIFIER, "name"], [:CLOSE]] চূড়ান্তভাবে, আমরা টেমপ্লেট ইভ্যালুয়েট করতে চাই এবং এক্সপ্রেশনকে রিয়েল ভ্যালু দিয়ে রিপ্লেস করতে চাই। জিনিসপত্র কিছুটা চ্যালেঞ্জিং করতে, আমরা কমপ্লেক্স ব্লক এক্সপ্রেশনও ইভ্যালুয়েট করতে চাই, যাতে রিপিটিশন এবং কন্ডিশনাল সম্ভব হয়। এটি করার জন্য, আমাদের একটি অ্যাবস্ট্র্যাক্ট সিনট্যাক্স ট্রি (AST) জেনারেট করতে হবে যা টেমপ্লেটের লজিক্যাল কাঠামো বর্ণনা করে। ট্রিটি নোডসমূহ থেকে গঠিত যেগুলো অন্য নোড রেফারেন্স করতে পারে বা টোকেন থেকে অতিরিক্ত ডেটা স্টোর করতে পারে। আমাদের সিম্পল উদাহরণের জন্য, ডিজায়ার্ড অ্যাবস্ট্র্যাক্ট সিনট্যাক্স ট্রি এরকম দেখে:
একটি গ্রামার ডিফাইন করা
গ্রামার ডিফাইন করতে, চলুন একটি ল্যাঙ্গুয়েজের থিওরেটিক্যাল বেসিস থেকে শুরু করি। অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজের মত, আমাদের টেমপ্লেটিং ল্যাঙ্গুয়েজ একটি কনটেক্সট-ফ্রি ল্যাঙ্গুয়েজ এবং তাই একটি কনটেক্সট-ফ্রি গ্রামার দিয়ে বর্ণনা করা যায়। (Wikipedia-এ গণিতের নোটেশন देखकर ভয় পাবেন না। কনসেপ্টটি প্রিটি স্ট্রেট ফরওয়ার্ড, এবং একটি ল্যাঙ্গুয়েজের গ্রামার নোটেট করার ডেভেলপার-ফ্রেন্ডলি পদ্ধতিও আছে।) একটি কনটেক্সট-ফ্রি গ্রামার হলো নিয়মের একটি সেট যা বর্ণনা করে একটি ল্যাঙ্গুয়েজের সব সম্ভাব্য স্ট্রিং কীভাবে কনস্ট্রাক্ট হয়। আমাদের টেমপ্লেটিং ল্যাঙ্গুয়েজের গ্রামার 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 }; প্রতিটি অ্যাসাইনমেন্ট একটি রুল ডিফাইন করে। রুলের নাম বাম পাশে এবং একটি গুচ্ছ অন্য রুল (lower case) বা টোকেন (upper case) লেক্সার থেকে ডান পাশে। রুল এবং টোকেন কমা , দিয়ে কনক্যাটেনেট করা যায় বা পাইপ | সিম্বল দিয়ে অল্টারনেট করা যায়। কার্লি ব্রেস { ... } এর ভেতরে থাকা রুল এবং টোকেন বহুবার রিপিট হতে পারে। যখন সেগুলো ব্রাকেট [ ... ] এর ভেতরে থাকে, সেগুলো অপশনাল বলে ধরা হয়। উপরের গ্রামারটি একটি সংক্ষিপ্ত পদ্ধতিতে বর্ণনা করে যে একটি টেমপ্লেট statements থেকে গঠিত। একটি statement হয় একটি CONTENT টোকেন, একটি এক্সপ্রেশন, অথবা একটি ব্লক এক্সপ্রেশন। একটি এক্সপ্রেশন হলো একটি OPEN_EXPRESSION টোকেন, যינוতর একটি IDENTIFIER টোকেন, যينوতর আর্গুমেন্টস, যينوতর একটি CLOSE টোকেন। এবং একটি ব্লক এক্সপ্রেশন হলো কেন naturl language-এ বর্ণনা করার চেয়ে উপরের নোটেশন ব্যবহার করা ভালো তার একটি পারফেক্ট উদাহরণ। এমন টুল আছে যা অটোমেটিক্যালি পার্সার জেনারেট করে গ্রামার ডেফিনিশন থেকে যেন উপরেরটি। কিন্তু সত্যিকারের রুবি ম্যজিক ট্রেডিশনে, আসুন কিছু ফান করি এবং পার্সারটি নিজেরা বিল্ড করি, আশা করি একটু দুটো কিছু শিখব প্রক্রিয়াতে।
পার্সার বিল্ড করা
ভাষা তত্ত্ব এক পাশে রেখে, আসুন আসলে পার্সার বিল্ড করার মধ্যে ঢুকি। একটি আরও মিনিম্যাল, কিন্তু এখনও বৈধ, টেমপ্লেট দিয়ে শুরু করি: 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 নোড রিটার্ন করবে। নোডগুলো সিম্পল ক্লাস যder কোনো বিহেবিয়ার নেই নিজস্ব। সেগুলো শুধু অন্য নোড কানেক্ট করে বা টোকেন থেকে কিছু ভ্যালু স্টোর করে। এখানে 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 এখন, parse মেথড ইমপ্লিমেন্ট করি 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 নতুন নোডগুলো inplace থাকলে, 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 মেথড inplace থাকলে, নতুন 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 ব্যবহার করে দেওয়া মেথড নাম কল করতে থাকবে যতক্ষণ না সেটি আর কোনো নোড রিটার্ন করে না। একবার সেটি হলে, কালেক্টেড রেজাল্টস (অথবা শুধু একটি খালি অ্যারে) রিটার্ন করা হয়। এই হেল্পার inplace থাকলে, parse_statements এবং parse_arguments উভয়ই वन-লাইন মেথড হয়ে যায়। def parse_statements repeat(:parse_statement) end def parse_arguments repeat(:parse_identifier) end সব চেঞ্জ inplace থাকলে, টোকেন স্ট্রিম পার্স করার চেষ্টা করি: Magicbars::Parser.parse(tokens) # => # , # # > এটি একটু হার্ড টু রিড কিন্তু এটি, ফ্যাক্টে, সঠিক অ্যাবস্ট্র্যাক্ট সিনট্যাক্স ট্রি। 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_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 নোড তৈরি করি এবং রিটার্ন করি। অ্যাডভান্সড ব্লক এক্সপ্রেশন টেমপ্লেটের টোকেনস দিয়ে পার্সার কল করলে টেমপ্লেটের জন্য AST রিটার্ন করবে। আমি উদাহরণ আউটপুট এখানে ইনক্লুড করছি না, কারণ এটি বেঅরলি রিডেবল। বদলে, এখানে জেনারেটেড AST-এর একটি ভিজুয়াল রিপ্রেজেন্টেশন। যেহেতু আমরা parse_statements কল করছি parse_block_expression-এর ভেতরে, ব্লক এবং ইনভার্স ব্লক দুটোই আরো এক্সপ্রেশন, ব্লক এক্সপ্রেশন, সেইসাথে রেগুলার কন্টেন্ট ইনক্লুড করতে পারে।
যাত্রা চালিয়ে যাচ্ছে…
আমাদের নিজস্ব টেমপ্লেটিং ল্যাঙ্গুয়েজ ইমপ্লিমেন্ট করার দিকে যাত্রায় আমরা ভালো প্রগতি করেছি। ভাষা তত্ত্বে একটি ছোট ডিপের পর, আমরা আমাদের টেমপ্লেটিং ল্যাঙ্গুয়েজের জন্য একটি গ্রামার ডিফাইন করেছি এবং সেটি ব্যবহার করে স্ক্র্যাচ থেকে একটি পার্সার ইমপ্লিমেন্ট করেছি। লেক্সার এবং পার্সার দুটোই inplace থাকায়, আমাদের শুধু একটি ইন্টারপ্রেটার বাকি আছে ইন্টারপোলেটেড স্ট্রিং জেনারেট করার জন্য আমাদের টেমপ্লেট থেকে। আমরা এই পার্টটি RubyMagic-এর একটি আগামী एডিশনে কভার করব। Ruby Magic মেইলিংলিস্ট সাবস্ক্রাইব করুন, সেটি আউট হলে অ্যালার্ট পেতে।