গিটহাবে পুরো সোর্স
স্টফল প্রোগ্রামিং ভাষার সম্পূর্ণ বাস্তবায়ন 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) আমাদের পূর্ণসংখ্যা যোগ প্রোগ্রামের অ্যাবস্ট্রাক্ট সিনট্যাক্স ট্রি (AST) নিচে দেওয়া হল:

আমাদের স্টফল স্যাম্পল প্রোগ্রামকে অনুপ্রাণিত করলেন গণিতবিদ
কার্ল ফ্রিডরিখ গাস সärkরবো ৭ বছর বয়সে নিজে থেকে একটি সিরিজের সংখ্যা যোগ করার ফর্মুলা বের করেছিলেন বলে মনে করা হয়।
আপনি লক্ষ্য করতে পারেন, আমাদের প্রোগ্রাম গাসের ফর্মুলা ব্যবহার করে না। যেহেতু আজ কম্পিউটার আছে, আমরা এই সমস্যাটি "ব্রুট-ফোর্স" উপায়ে সমাধান করতে পারি। আমাদের সিলিকন বন্ধুদের কঠিন কাজটা করতে দিন।
ফাংশন ডেফিনিশন
আমাদের প্রোগ্রামে প্রথমে sum_integers ফাংশন ডিফাইন করা হয়। ফাংশন ডিক্লেয়ার করা কি মানে? আপনি অনুমান করতে পারছেন, এটি একটি ভেরিয়েবলে মান অ্যাসাইন করার মতো একটি অ্যাকশন। যখন আমরা একটি ফাংশন ডিফাইন করি, আমরা একটি নাম (ফাংশন নাম, একটি আইডেন্টিফায়ার) এক বা একাধিক এক্সপ্রেশনের সাথে (ফাংশনের বডি) যুক্ত করি। আমরা রেজিস্টার করি ফাংশন কল সময় পাস করা মানগুলো কোন নামগুলোর সাথে বাউন্ড হবে। এই আইডেন্টিফায়ারগুলো ফাংশন এক্সিকিউশন সময় লোকাল ভেরিয়েবল হয়ে যায় এবং এদের বলা হয় প্যারামিটার। ফাংশন কল সময় পাস করা মানগুলো (যেগুলো প্যারামিটারদের সাথে বাউন্ড হয়) আর্গুমেন্ট कहलाते हैं।
#interpret_function_definition দেখে নেওয়া যাক:
def interpret_function_definition(fn_def) env[fn_def.function_name_as_str] = fn_def end খুব সিম্পল, ঠিক? পূর্বের পোস্টে যেভাবে আমাদের ইন্টারপ্রেটার ইনস্ট্যানশिएট হওয়ার সময় একটি এনভায়রনমেন্ট তৈরি করেছিলাম, সেটি মনে আছে। এটি প্রোগ্রাম স্টেট রাখার জায়গা, আমাদের কেসে এটি সিম্পল একটি রুবি হ্যাশ। গত পোস্টে আমরা দেখেছিলাম ভেরিয়েবল এবং তাদের সাথে বাউন্ড মানগুলো কীভাবে env-এ স্টোর হয়। ফাংশন ডেফিনিশনও সেখানে স্টোর হবে। কী হবে ফাংশন নাম, এবং ভ্যালু হবে ফাংশন ডিফাইন করতে ব্যবহৃত AST নোড (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 # @name ইন্সট্যান্স ভেরিয়েবল একটি AST::Identifier। name.name end def ==(other) children == other&.children end def children [name, params, body] end end ফাংশন নামকে Stoffle::AST::FunctionDefinition এর সাথে যুক্ত করলে আমরা ফাংশন এক্সিকিউট করতে প্রয়োজন সকল তথ্য অ্যাক্সেস করতে পারি। উদাহরণস্বরূপ, আমাদের হাতে থাকে কতগুলো আর্গুমেন্ট এক্সপেক্ট করা হয়েছে এবং সহজে এরর.emit করতে পারি যদি ফাংশন কল সেটি প্রদান না করে। এই এবং অন্যান্য ডিটেইল আমরা পরবর্তীতে এক্সপ্লোর করব ফাংশন কল ইন্টারপ্রেট করার জন্য দায়বদ্ধ কোডে।
ফাংশন কল
আমাদের উদাহরণের ওকথাওর এগিয়ে যাই, এখন ফাংশন কল ফোকাস করি। 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) ফাংশন কলের ইন্টারপ্রেটেশন হয় #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) # ফাংশন বডি এক্সিকিউট করা। call_stack << stack_frame value = interpret_nodes(fn_def.body.expressions) call_stack.pop value end এটি একটি জটিল ফাংশন, তাই আমাদের সময় নিতে হবে। পূর্বের আর্টিকেলে ব্যাখ্যা করা হয়েছে, প্রথম লাইন চেক করে যে কল হওয়া ফাংশন println কিনা। যদি ইউজার-ডিফাইনড ফাংশন হয়, যা এখানে সেটি, তাহলে আমরা এগিয়ে যাই এবং #fetch_function_definition ব্যবহার করে এর ডেফিনিশন ফেচ করি। নিচে দেখানো হয়েছে, এই ফাংশন খুব সহজ, আমরা মূলত আগে সংরক্ষিত Stoffle::AST::FunctionDefinition AST নোডটি এনভায়রনমেন্ট থেকে রিট্রিভ করি বা ফাংশন না থাকলে এরর.emit করি।
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-এ ফিরে যাই, জিনিসগুলো আরো আকর্ষণীয় হয়ে যাচ্ছে। আমাদের সিম্পল টয় ল্যাঙ্গুয়েজে ফাংশন নিয়ে আমাদের দুটি বিশেষ উদ্বেগ। প্রথম, ফাংশনhez লোকাল ভেরিয়েবল ট্র্যাক রাখার একটি কৌশল দরকার। দ্বিতীয়, 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 # এই নির্দিষ্ট ফাংশন কল থেকে পাস করা মানগুলো সংশ্লিষ্ট ডিফাইন করা প্যারামিটারের সাথে অ্যাপ্লাই করা। if fn_def.params != nil fn_def.params.each_with_index do |param, i| if env.has_key?(param.name) # একটি গ্লোবাল ভেরিয়েবল ইতিমধ্যে ডিফাইন করা আছে। আমরা পাস করা মান সেটি অ্যাসাইন করি। env[param.name] = interpret_node(fn_call.args[i]) else # একই নামের কোন গ্লোবাল ভেরিয়েবল নেই। আমরা একটি নতুন লোকাল ভেরিয়েবল তৈরি করি। stack_frame.env[param.name] = interpret_node(fn_call.args[i]) end end end end #assign_function_args_to_params বাস্তবায়ন এক্সপ্লোর করার আগে, ভেরিয়েবল স্কোপিং নিয়ে সংক্ষিপ্ত আলোচনা করা জরুরি। এটি জটিল এবং সূক্ষ্ম বিষয়। স্টফলের জন্য, আমরা খুব প্র্যাগম্যাটিক হয়ে একটি সহজ সমাধান গ্রহণ করব। আমাদের ছোট ভাষায়, শুধু ফাংশনই নতুন স্কোপ তৈরি করে। علاৱে, গ্লোবাল ভেরিয়েবল সর্বোচ্চ অগ্রাধিকার পায়। ফলে, ফাংশনের বাইরে ডিক্লেয়ার করা (প্রথম ব্যবহার) সকল ভেরিয়েবল গ্লোবাল হয় এবং env-এ সংরক্ষিত হয়। ফাংশনের ভেতরের ভেরিয়েবল সেগুলোর লোকাল হয় এবং ফাংশন কল ইন্টারপ্রেট করার সময় তৈরি হওয়া স্ট্যাক ফ্রেমের env-এ সংরক্ষিত হয়। কিন্তু একটি এক্সেপশন আছে: একটি ভেরিয়েবল নাম যা কোনো বিদ্যমান গ্লোবাল ভেরিয়েবলের সাথে কলাইড করে। যদি কলিশন ঘটে, তবে একটি লোকাল ভেরিয়েবল তৈরি নहीं হবে, বরং আমরা বিদ্যমান গ্লোবাল ভেরিয়েবল থেকে পড়ব এবং অ্যাসাইন করব।
ভেরিয়েবল স্কোপিং কৌশল ক্লিয়ার হয়ে গেলে, #assign_function_args_to_params-এ ফিরে যাই। মেথডের প্রথম সেগমেন্টে, আমরা প্রথমে ফাংশন ডেফিনিশন এবং ফাংশন কল নোডগুলো স্ট্যাক ফ্রেম অবজেক্ট থেকে রিট্রিভ করি যা পাস করা হয়েছিল। এগুলো হাতে থাকলে, প্রদত্ত আর্গুমেন্টের সংখ্যা কল করা ফাংশনের এক্সপেক্টেড প্যারামিটার সংখ্যার সাথে মিলে কিনা চেক করা সহজ। দেওয়া আর্গুমেন্ট এবং এক্সপেক্টেড প্যারামিটারের মধ্যে মিসম্যাচ থাকলে এরর raise করি। #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) # ফাংশন বডি এক্সিকিউট করা। 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 # আমরা একটি ফাংশনের ভেতরে আছি। যদি নাম একটি গ্লোবাল ভেরিয়েবল নির্দেশ করে, আমরা সেটিতে মান অ্যাসাইন করি। # অন্যথায়, আমরা একটি লোকাল ভেরিয়েবল তৈরি এবং/অথবা অ্যাসাইন করি। 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 # আমরা কোনো ফাংশনের ভেতরে নই। তাই, আমরা একটি গ্লোবাল ভেরিয়েবল তৈরি এবং/অথবা অ্যাসাইন করি। env[var_binding.var_name_as_str] = interpret_node(var_binding.right) end end মনে আছে যখন আমরা ফাংশন কলের জন্য তৈরি স্ট্যাক ফ্রেমকে call_stack-এ পুশ করেছিলাম? এখন সেটি কাজে লাগছে কারণ আমরা চেক করতে পারি আমরা একটি ফাংশনের ভেতরে আছি কিনা যাচাই করে call_stack-এর লেংথ শূন্যের চেয়ে বড় কিনা (অর্থাৎ কমপক্ষে একটি স্ট্যাক ফ্রেম আছে)। যদি আমরা একটি ফাংশনের ভেতরে আছি, যা বর্তমানে ইন্টারপ্রেট করা কোডের কেস, আমরা চেক করি আমাদের কাছে ইতিমধ্যে একটি গ্লোবাল ভেরিয়েবল আছে কিনা সেই নামের যা আমরা এখন একটি মান বাউন্ড করার চেষ্টা করছি। আপনি ইতিমধ্যে জানেন, কলিশন হলে, আমরা সহজেই মান বিদ্যমান গ্লোবাল ভেরিয়েবলে অ্যাসাইন করব, এবং একটি লোকাল তৈরি হবে না। যখন নাম ব্যবহৃত নয়, আমরা একটি নতুন লোকাল ভেরিয়েবল তৈরি করি এবং ইচ্ছিত মান সেটি অ্যাসাইন করি। যেহেতু call_stack একটি স্ট্যাক (লাস্ট ইন ফার্স্ট আউট ডেটা স্ট্রাকচার), আমরা জানি এই লোকাল ভেরিয়েবলটি স্ট্যাক করা শেষ ফ্রেমের env-এ সংরক্ষিত হবে (অর্থাৎ বর্তমানে প্রসেস করা ফাংশনের জন্য তৈরি ফ্রেম)। অবশেষে, #interpret_var_binding-এর শেষ অংশ ফাংশনের বাইরে হয়endo অ্যাসাইনমেন্ট ডील করে। যেহেতু শুধু ফাংশনই স্টফলে নতুন স্কোপ তৈরি করে, এখানে কিছুই বদলেনা, কারণ ফাংশনের বাইরে তৈরি ভেরিয়েবলগুলো সবসময় গ্লোবাল এবং ইনস্ট্যান্স ভেরিয়েবল env-এ সংরক্ষিত হয়।
আমাদের প্রোগ্রামে ফিরে যাই, পরবর্তী ধাপ হলো পূর্ণসংখ্যা যোগ করার জন্য দায়বদ্ধ লুপ ইন্টারপ্রেট করা। আমাদের মেমরি রিফ্রেশ করি এবং আমাদের স্টফল প্রোগ্রামের AST আবার দেখি:

লুপকে রিপ্রেজেন্ট করা নোড হলো 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 লক্ষ্য করুন, এই AST নোড মূলত পূর্বের আর্টিকেলগুলোতে এক্সপ্লোর করা কনসেপ্টগুলো একসাথে নিয়ে আসে। এর কন্ডিশনালের জন্য, আমাদের একটি এক্সপ্রেশন থাকবে যার রুটে (এক্সপ্রেশনের AST রুট নোড ভাবুন) সাধারণত একটি Stoffle::AST::BinaryOperator থাকবে (যেমন '>', 'or' ইত্যাদি)। লুপের বডির জন্য, আমাদের একটি Stoffle::AST::Block থাকবে। এটা মেনে পড়ে, ঠিক না? লুপের সবচেয়ে বেসিক ফর্ম হলো এক বা একাধিক এক্সপ্রেশন (একটি ব্লক) যা রিপিট হবে যতক্ষণ একটি এক্সপ্রেশনTruthy (অর্থাৎ কন্ডিশনালTruthy মান এভ্যালুয়েট করে)।
আমাদের ইন্টারপ্রেটারে সংশ্লিষ্ট মেথড হলো #interpret_repetition:
def interpret_repetition(repetition) while interpret_node(repetition.condition) interpret_nodes(repetition.block.expressions) end end এখানে, আপনি এই মেথডের সادگی (এবং, দুঃসাহসিকভাবে বলতে গেলে, সৌন্দর্য) দেখে আশ্চর্য করতে পারেন। আমরা লুপ ইন্টারপ্রেটেশন বাস্তবায়ন করতে পারি পূর্বের আর্টিকেলগুলোতে এক্সপ্লোর করা মেথডগুলো কম্বাইন করে। রুবির while লুপ ব্যবহার করে, আমরা নিশ্চিত করতে পারি যে আমরা আমাদের স্টফল লупকে কনস্টিটিউট করা নোডগুলো ইন্টারপ্রেট করা চালিয়ে যাচ্ছি (বাড়ি বাড়ি #interpret_nodes কল করে) যতক্ষণ কন্ডিশনালের এভ্যালুয়েশন true। কন্ডিশনাল এভ্যালুয়েট করার কাজ তো সহজই, সাদার্ণ সাস্পেক্ট #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 # বর্তমান স্ট্যাক লেভেল স্টোর করি যেন জানতে পারি কখন রিটার্ন বন্ধ করতে হবে। return last_value end if unwind_call_stack == call_stack.length # আমরা এখনও একটি রিটার্ন করা ফাংশনের ভেতরে আছি, তাই এর স্ট্রাকচারগুলোতে (যেমন কন্ডিশনাল, লুপ ইত্যাদি) থেকে বাবল আপ করতেই থাকি। return last_value elsif unwind_call_stack > call_stack.length # আমরা ফাংশন থেকে রিটার্ন করেছি, তাই "আনউইন্ড ইন্ডিকেটর" রিসেট করি। 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 AST নোড ডিটেক্ট করে। এটি করার পর, আমরা প্রথমে একটি স্যানিটি চেক করি যাচাইতে যে আমরা একটি ফাংশনের ভেতরে আছি। এর জন্য, আমরা সিম্পলি কল স্ট্যাকের লেংথ চেক করতে পারি। শূন্যের চেয়ে বড় লেংথ মানে আমরা একটি ফাংশনের ভেতরে আছি। স্টফলে, আমরা ফাংশনের বাইরে return এক্সপ্রেশন অ্যালো করি না, তাই এই চেক ফেললে এরর raise করি। প্রোগ্রামারের ইচ্ছামত রিটার্ন মান রিটার্ন করার আগে, আমরা প্রথমে কল স্ট্যাকের বর্তমান লেংথ রেকর্ড রাখি, সেটি ইনস্ট্যান্স ভেরিয়েবল 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) # ফাংশন বডি এক্সিকিউট করা। 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 # আমরা এখনও একটি রিটার্ন করা ফাংশনের ভেতরে আছি, তাই এর স্ট্রাকচারগুলোতে (যেমন কন্ডিশনাল, লুপ ইত্যাদি) থেকে বাবল আপ করতেই থাকি। return last_value elsif unwind_call_stack > call_stack.length # আমরা ফাংশন থেকে রিটার্ন করেছি, তাই "আনউইন্ড ইন্ডিকেটর" রিসেট করি। self.unwind_call_stack = -1 end # ... end # ... end এটি প্রথমে একটু বুঝতে কঠিন লাগতে পারে। আমি আপনাকে GitHub-এ ফুল বাস্তবায়ন চেক করার এবং এর সাথে এক্সপেরিমেন্ট করার পরামর্শ দিচ্ছি যদি মনে করেন সেটি এই লাস্ট বিট বুঝতে সাহায্য করবে। এখানে মনে রাখার মূল পয়েন্ট হলো একটি টিপিক্যাল প্রোগ্রামের অনেক গভীর নেস্টেড স্ট্রাকচার থাকে। ফলে, #interpret_nodes এক্সিকিউট করা সাধারণত #interpret_nodes-এর একটি নতুন কল রেজাল্ট করে, যেটি আবার #interpret_nodes-এর আরও কল রেজাল্ট করতে পারে এবং így চলতে থাকে! যখন আমরা একটি ফাংশনের ভেতরে return হিট করি, আমরা একটি গভীর নেস্টেড স্ট্রাকচারের ভেতরে থাকতে পারি। কল্পনা করুন, return একটি কন্ডিশনালের ভেতরে আছে যা একটি লুপের অংশ। ফাংশন থেকে রিটার্ন করতে হলে, আমাদের সব #interpret_nodes কল থেকে রিটার্ন করতে হবে যতক্ষণ আমরা সেটি রিটার্ন না করি যা #interpret_function_call দ্বারা ইনিশিয়েট করা হয়েছিল (অর্থাৎ #interpret_nodes কল যা ফাংশন বডি ইন্টারপ্রেটেশন শুরু করেছিল)।
উপরোক্ত কোড সেগমেন্টে, আমরা হাইলাইট করেছি সেটি কীভাবে করা হয়। @unwind_call_stack-এ পজিটিভ ভ্যালু থাকা এবং সেটি বর্তমান কল স্ট্যাক লেংথের সমান হলে, আমরা নিশ্চিত হয়ে যাই যে আমরা একটি ফাংশনের ভেতরে আছি যা রিটার্ন করেছে এবং আমরা এখনও #interpret_function_call দ্বারা ইনিশিয়েট করা অরিজিনাল কল থেকে রিটার্ন করিনি। যখন এটি ফাইনালি হয়, @unwind_call_stack বর্তমান কল স্ট্যাক লেংথের চেয়ে বড় হবে; সুতরাং, আমরা জানি আমরা যে ফাংশন থেকে রিটার্ন করেছিলাম সেটি এক্সিট করেছি, এবং আমাদের আর বাবল আপ প্রসেস চালিয়ে যেতে হবে না। তারপর, আমরা @unwind_call_stack রিসেট করি। @unwind_call_stack-এর ব্যবহার একদম ক্লিয়ার করার জন্য, এখানে এর পসিবল ভ্যালুগুলো:
- -1, এর ইніশিয়াল এবং নিরপেক্ষ মান, ইন্ডিকেট করে যে আমরা কোনো রিটার্ন করা ফাংশনের ভেতরে নই।
- কল স্ট্যাক লেংথের সমান পজিটিভ ভ্যালু, ইন্ডিকেট করে যে আমরা এখনও একটি রিটার্ন করা ফাংশনের ভেতরে আছি।
- কল স্ট্যাক লেংথের চেয়ে বড় পজিটিভ ভ্যালু, ইন্ডিকেট করে যে আমরা আর সেই রিটার্ন করা ফাংশনের ভেতরে নই।
স্টফল CLI ব্যবহার করে আমাদের প্রোগ্রাম চালানো
সিরিজের পূর্বের আর্টিকেলে, আমরা একটি সিম্পল CLI তৈরি করেছিলাম স্টফল প্রোগ্রাম ইন্টারপ্রেট করা সহজ করতে। এখন যেহেতু আমরা ইন্টারপ্রেটারের বাস্তবায়ন এক্সপ্লোর করেছি, চলুন এটিকে একশনে দেখি, আমাদের প্রোগ্রাম চালিয়ে। 위에 অনেক সেকশনে দেখানো হয়েছে, আমাদের কোড sum_integers ফাংশন ডিফাইন করে এবং তারপর কল করে আর্গুমেন্ট 1 এবং 100 পাস করে। যদি আমাদের ইন্টারপ্রেটার সঠিকভাবে কাজ করে, আমাদের কনসোলে 5050.0 (১ থেকে ১০০ পর্যন্ত পূর্ণসংখ্যার সেটের যোগফল) প্রিন্ট হওয়া উচিত:

শেষ বچار
এই পোস্টে, আমরা ইন্টারপ্রেটার সম্পূর্ণ করার জন্য প্রয়োজনীয় ফাইনাল পার্টগুলো বাস্তবায়ন করেছি। আমরা ফাংশন ডেফিনিশন, ফাংশন কলিং, ভেরিয়েবল স্কোপিং, এবং লুপ সমাধান করেছি। এখন, আমাদের কাছে একটি সিম্পল কিন্তু কাজ করার প্রোগ্রামিং ভাষা আছে!
সিরিজের পরবর্তী এবং ফাইনাল পার্টে, আমি কিছু রিসোর্স শেয়ার করব যেগুলো আমি মনে করি সেসব লোকের জন্য অপশন যারা প্রোগ্রামিং ল্যাঙ্গুয়েজ বাস্তবায়ন অধ্যয়ন চালিয়ে যেতে চান। আমি কিছু চ্যালেঞ্জ ও প্রপোজ করব যেগুলো আপনি নিতে পারেন আপনার লার্নিং চালিয়ে যেতে এবং আপনার স্টফল ভার্সন উন্নত করতে। তারপর দেখা হবে; সিয়াও!