কম্পিউটার

রুবিতে একটি প্রোগ্রামিং ভাষা তৈরি করা:দ্য ইন্টারপ্রেটার, পার্ট 2

গিথুবের সম্পূর্ণ উৎস

Stoffle প্রোগ্রামিং ভাষার একটি সম্পূর্ণ বাস্তবায়ন GitHub এ উপলব্ধ। আপনি বাগ খুঁজে পেলে বা প্রশ্ন থাকলে নির্দ্বিধায় একটি সমস্যা খুলুন৷

এই ব্লগ পোস্টে, আমরা Stoffle-এর জন্য দোভাষী বাস্তবায়ন চালিয়ে যাব, একটি খেলনা প্রোগ্রামিং ভাষা যা সম্পূর্ণরূপে রুবিতে নির্মিত। আমরা আগের পোস্টে দোভাষী শুরু করেছি। আপনি এই সিরিজের প্রথম অংশে এই প্রকল্প সম্পর্কে আরও পড়তে পারেন।

শেষ পোস্টে, আমরা স্টফলের সহজ বৈশিষ্ট্যগুলি কীভাবে বাস্তবায়ন করতে হয় তা কভার করেছি:ভেরিয়েবল, কন্ডিশনাল, ইউনারী এবং বাইনারি অপারেটর, ডেটা টাইপ এবং কনসোলে মুদ্রণ। এখন, আমাদের হাতা গুটিয়ে নেওয়ার এবং আরও চ্যালেঞ্জিং অবশিষ্ট বিটগুলিকে মোকাবেলা করার সময় এসেছে:ফাংশন সংজ্ঞা, ফাংশন কলিং, পরিবর্তনশীল স্কোপিং এবং লুপস৷

আমরা আগে যেমন করেছিলাম, আমরা এই পোস্টের শুরু থেকে শেষ পর্যন্ত একই উদাহরণ প্রোগ্রাম ব্যবহার করব। আমরা আমাদের স্টফল উদাহরণ প্রোগ্রামে প্রতিটি ভিন্ন কাঠামোকে জীবন্ত করার জন্য দোভাষীর কাছে প্রয়োজনীয় বাস্তবায়নের অন্বেষণ করে লাইন দ্বারা লাইন দিয়ে যাব। অবশেষে, আমরা দোভাষীকে কাজ করতে দেখব এবং সিরিজের পূর্ববর্তী নিবন্ধে আমরা তৈরি করা 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) হল নিম্নলিখিত:

রুবিতে একটি প্রোগ্রামিং ভাষা তৈরি করা:দ্য ইন্টারপ্রেটার, পার্ট 2

দি গণিতবিদ যিনি আমাদের স্টফল নমুনা প্রোগ্রামকে অনুপ্রাণিত করেছিলেন

কার্ল ফ্রেডরিখ গাউস অনুমিতভাবে মাত্র 7 বছর বয়সে, একটি সিরিজে সংখ্যার যোগফলের একটি সূত্র নিজেরাই বের করেছিলেন।

আপনি হয়তো লক্ষ্য করেছেন, আমাদের প্রোগ্রাম গাউসের তৈরি সূত্র ব্যবহার করে না। যেহেতু আমাদের কাছে আজকাল কম্পিউটার রয়েছে, তাই আমাদের কাছে "ব্রুট-ফোর্স" উপায়ে এই সমস্যাটি সমাধান করার বিলাসিতা রয়েছে। আমাদের সিলিকন বন্ধুদের আমাদের জন্য কঠোর পরিশ্রম করতে দিন।

ফাংশনের সংজ্ঞা

আমাদের প্রোগ্রামে আমরা প্রথমে যা করি তা হল 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
    # The instance variable @name is an AST::Identifier.
    name.name
  end

  def ==(other)
    children == other&.children
  end

  def children
    [name, params, body]
  end
end

একটি Stoffle::AST::FunctionDefinition এর সাথে যুক্ত ফাংশনের নাম থাকা মানে আমরা ফাংশনটি চালানোর জন্য প্রয়োজনীয় সমস্ত তথ্য অ্যাক্সেস করতে পারি। উদাহরণস্বরূপ, আমাদের কাছে প্রত্যাশিত আর্গুমেন্টের সংখ্যা রয়েছে এবং একটি ফাংশন কল এটি প্রদান না করলে সহজেই একটি ত্রুটি নির্গত করতে পারে। এটি এবং অন্যান্য বিশদ বিবরণ আমরা দেখতে পাব যখন আমরা একটি ফাংশন কল ব্যাখ্যা করার জন্য দায়ী কোডটি অন্বেষণ করব।

একটি ফাংশন কল করা

আমাদের উদাহরণের মাধ্যমে চলতে চলতে, আমাদের এখন ফাংশন কলে ফোকাস করা যাক। sum_integers সংজ্ঞায়িত করার পরে ফাংশন, আমরা আর্গুমেন্ট হিসাবে 1 এবং 100 নম্বর পাস করাকে বলি:

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)

  # Executing the function body.
  call_stack << stack_frame
  value = interpret_nodes(fn_def.body.expressions)
  call_stack.pop
  value
end

এটি একটি জটিল ফাংশন, তাই আমাদের এখানে আমাদের সময় নিতে হবে। যেমন শেষ নিবন্ধে ব্যাখ্যা করা হয়েছে, প্রথম লাইনটি কল করা ফাংশনটি println কিনা তা পরীক্ষা করার জন্য দায়ী . যদি আমরা একটি ব্যবহারকারী-সংজ্ঞায়িত ফাংশন নিয়ে কাজ করি, যা এখানে হয়, তাহলে আমরা এগিয়ে যাই এবং #fetch_function_definition ব্যবহার করে এর সংজ্ঞা নিয়ে আসি। . নীচে দেখানো হিসাবে, এই ফাংশনটি একটি সাধারণ পাল, এবং আমরা মূলত Stoffle::AST::FunctionDefinition পুনরুদ্ধার করি এএসটি নোড আমরা পূর্বে পরিবেশে সংরক্ষণ করেছি বা ফাংশনটি বিদ্যমান না থাকলে একটি ত্রুটি নির্গত করে।

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-এ ফিরে যাওয়া , জিনিস আরো আকর্ষণীয় পেতে শুরু. আমাদের সহজ খেলনা ভাষায় ফাংশন সম্পর্কে চিন্তা করার সময়, আমাদের দুটি বিশেষ উদ্বেগ রয়েছে। প্রথমত, ফাংশনের স্থানীয় ভেরিয়েবলের ট্র্যাক রাখার জন্য আমাদের একটি কৌশল প্রয়োজন। আমাদেরও 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

  # Applying the values passed in this particular function call to the respective defined parameters.
  if fn_def.params != nil
    fn_def.params.each_with_index do |param, i|
      if env.has_key?(param.name)
        # A global variable is already defined. We assign the passed in value to it.
        env[param.name] = interpret_node(fn_call.args[i])
      else
        # A global variable with the same name doesn't exist. We create a new local variable.
        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-এ ফিরে যাই . পদ্ধতির প্রথম সেগমেন্টে, আমরা প্রথমে স্ট্যাক ফ্রেম অবজেক্ট থেকে ফাংশন ডেফিনিশন এবং ফাংশন কল নোডগুলি পুনরুদ্ধার করি যা পাস করা হয়েছিল৷ এগুলি হাতে থাকলে, প্রদত্ত আর্গুমেন্টের সংখ্যা পরামিতিগুলির সংখ্যার সাথে মেলে কিনা তা পরীক্ষা করা সহজ। ফাংশন বলা হচ্ছে প্রত্যাশা. প্রদত্ত আর্গুমেন্ট এবং প্রত্যাশিত প্যারামিটারের মধ্যে অমিল থাকলে আমরা একটি ত্রুটি উত্থাপন করি। #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)

  # Executing the function body.
  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
    # We are inside a function. If the name points to a global var, we assign the value to it.
    # Otherwise, we create and / or assign to a local var.
    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
    # We are not inside a function. Therefore, we create and / or assign to a global var.
    env[var_binding.var_name_as_str] = interpret_node(var_binding.right)
  end
end

আপনার কি মনে আছে যখন আমরা ফাংশন কলের জন্য তৈরি স্ট্যাক ফ্রেমটিকে call_stack এ পুশ করেছিলাম ? এটি এখন সুবিধাজনক কারণ আমরা call_stack যাচাই করে আমরা একটি ফাংশনের ভিতরে আছি কিনা তা পরীক্ষা করতে পারি শূন্যের চেয়ে বেশি দৈর্ঘ্য আছে (অর্থাৎ, অন্তত আছে একটি স্ট্যাক ফ্রেম)। যদি আমরা একটি ফাংশনের ভিতরে থাকি, যা আমরা বর্তমানে যে কোডটি ব্যাখ্যা করছি তার ক্ষেত্রে, আমরা পরীক্ষা করি যে আমাদের কাছে ইতিমধ্যে একটি বৈশ্বিক ভেরিয়েবল আছে কিনা সেই ভেরিয়েবলের একই নামের সাথে আমরা এখন একটি মান বাঁধার চেষ্টা করছি। আপনি ইতিমধ্যেই জানেন, সংঘর্ষ হলে, আমরা কেবল বিদ্যমান গ্লোবাল ভেরিয়েবলের মান নির্ধারণ করব এবং স্থানীয় একটি তৈরি করা হবে না। যখন নামটি ব্যবহার করা হচ্ছে না, তখন আমরা একটি নতুন স্থানীয় ভেরিয়েবল তৈরি করি এবং এটিতে অভিপ্রেত মান নির্ধারণ করি। যেহেতু call_stack একটি স্ট্যাক (অর্থাৎ, প্রথম আউট ডেটা স্ট্রাকচারে শেষ), আমরা জানি যে এই স্থানীয় ভেরিয়েবলটিকে env এ সংরক্ষণ করা উচিত শেষের স্ট্যাক করা ফ্রেম (অর্থাৎ, বর্তমানে প্রক্রিয়া করা ফাংশনের জন্য তৈরি ফ্রেম)। অবশেষে, #interpret_var_binding এর শেষ অংশ কার্যের বাইরে ঘটছে অ্যাসাইনমেন্ট নিয়ে কাজ করে। যেহেতু শুধুমাত্র ফাংশন স্টফলে নতুন স্কোপ তৈরি করে, তাই এখানে কিছুই বদলায় না, কারণ বাইরের ফাংশন তৈরি করা ভেরিয়েবল সবসময় গ্লোবাল থাকে এবং ইনস্ট্যান্স ভেরিয়েবল env এ সংরক্ষণ করা হয়। .

আমাদের প্রোগ্রামে ফিরে আসা, পরবর্তী ধাপ হল পূর্ণসংখ্যার যোগফলের জন্য দায়ী লুপকে ব্যাখ্যা করা। আসুন আমাদের মেমরি রিফ্রেশ করি এবং আমাদের Stoffle প্রোগ্রামের AST আবার একবার দেখে নেওয়া যাক:

রুবিতে একটি প্রোগ্রামিং ভাষা তৈরি করা:দ্য ইন্টারপ্রেটার, পার্ট 2

লুপের প্রতিনিধিত্বকারী নোড হল 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 (যেমন, '>', 'বা' ইত্যাদি)। লুপের বডির জন্য, আমাদের একটি Stoffle::AST::Block থাকবে . এই অর্থে তোলে, ডান? লুপের সবচেয়ে মৌলিক রূপ হল এক বা একাধিক অভিব্যক্তি (একটি ব্লক ) পুনরাবৃত্তি করা যখন একটি অভিব্যক্তি সত্য হয় (অর্থাৎ, যখন শর্তাধীন একটি সত্য মান মূল্যায়ন করে)।

আমাদের দোভাষীর সংশ্লিষ্ট পদ্ধতি হল #interpret_repetition :

def interpret_repetition(repetition)
  while interpret_node(repetition.condition)
    interpret_nodes(repetition.block.expressions)
  end
end

এখানে, আপনি এই পদ্ধতির সরলতা (এবং, আমি বলতে সাহস, সৌন্দর্য) দ্বারা বিস্মিত হতে পারে। আমরা অতীতের নিবন্ধগুলিতে ইতিমধ্যে অন্বেষণ করা পদ্ধতিগুলিকে একত্রিত করে লুপগুলির ব্যাখ্যা বাস্তবায়ন করতে পারি। রুবির while ব্যবহার করে লুপ, আমরা নিশ্চিত করতে পারি যে আমরা আমাদের স্টফল লুপ রচনা করে এমন নোডগুলিকে ব্যাখ্যা করা চালিয়ে যাচ্ছি (বারবার কল করে #interpret_nodes ) যখন শর্তসাপেক্ষের মূল্যায়ন সত্য। শর্তসাপেক্ষ মূল্যায়নের কাজটি সাধারণ সন্দেহভাজনকে কল করার মতোই সহজ, #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 # We store the current stack level to know when to stop returning.
      return last_value
    end

    if unwind_call_stack == call_stack.length
      # We are still inside a function that returned, so we keep on bubbling up from its structures (e.g., conditionals, loops etc).
      return last_value
    elsif unwind_call_stack > call_stack.length
      # We returned from the function, so we reset the "unwind indicator".
      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 ব্যবহারের অনুমতি দিই না এক্সপ্রেশন ফাংশনের বাইরে, তাই এই চেক ব্যর্থ হলে আমরা একটি ত্রুটি উত্থাপন করি। প্রোগ্রামার দ্বারা অভিপ্রেত মানটি ফেরত দেওয়ার আগে, আমরা প্রথমে কল স্ট্যাকের বর্তমান দৈর্ঘ্যের রেকর্ড রাখি, এটি ইনস্ট্যান্স ভেরিয়েবল 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)

  # Executing the function body.
  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
      # We are still inside a function that returned, so we keep on bubbling up from its structures (e.g., conditionals, loops etc).
      return last_value
    elsif unwind_call_stack > call_stack.length
      # We returned from the function, so we reset the "unwind indicator".
      self.unwind_call_stack = -1
    end

    # ...
  end

  # ...
end

এটি প্রথমে উপলব্ধি করা কিছুটা কঠিন হতে পারে। আমি আপনাকে GitHub-এ সম্পূর্ণ বাস্তবায়ন পরীক্ষা করার জন্য উত্সাহিত করছি এবং যদি আপনি মনে করেন যে এটি আপনাকে দোভাষীর এই শেষ বিটটি বুঝতে সাহায্য করতে পারে তবে এটির সাথে খেলুন। এখানে মনে রাখা গুরুত্বপূর্ণ বিষয় হল যে একটি সাধারণ প্রোগ্রামে অনেক গভীরভাবে নেস্টেড কাঠামো থাকে। তাই, #interpret_nodes চালানো হচ্ছে সাধারণত #interpret_nodes-এ একটি নতুন কলের ফলে , যার ফলে #interpret_nodes-এ আরও কল হতে পারে এবং তাই! যখন আমরা একটি return হিট করি একটি ফাংশনের ভিতরে, আমরা একটি গভীরভাবে নেস্টেড কাঠামোর ভিতরে থাকতে পারি। উদাহরণস্বরূপ, কল্পনা করুন যে return একটি শর্তাধীন যে একটি লুপের অংশ ভিতরে আছে. ফাংশন থেকে ফিরে আসতে, আমাদের সমস্ত #interpret_nodes থেকে ফিরতে হবে যতক্ষণ না আমরা #interpret_function_call দ্বারা শুরু করা থেকে ফিরে না আসি (অর্থাৎ, #interpret_nodes-এ কল যেটি ফাংশন বডির ব্যাখ্যা শুরু করেছে)।

উপরের কোডের সেগমেন্টে, আমরা ঠিক কীভাবে এটি করি তা হাইলাইট করি। @unwind_call_stack-এ একটি ইতিবাচক মান থাকার মাধ্যমে এবং যেটি কল স্ট্যাকের বর্তমান দৈর্ঘ্যের সমান, আমরা নিশ্চিতভাবে জানি যে আমরা একটি ফাংশনের ভিতরে আছি এবং আমরা এখনও return করিনি। #interpret_function_call দ্বারা শুরু করা আসল কল থেকে . অবশেষে যখন এটি ঘটে, @unwind_call_stack কল স্ট্যাকের বর্তমান দৈর্ঘ্যের চেয়ে বেশি হবে; এইভাবে, আমরা জানি যে আমরা যে ফাংশনটি ফিরে এসেছিল তা থেকে বেরিয়ে এসেছি, এবং আমাদের আর বুদবুদ করার প্রক্রিয়া চালিয়ে যেতে হবে না। তারপর, আমরা @unwind_call_stack রিসেট করি . শুধু @unwind_call_stack ব্যবহার করতে স্ফটিক পরিষ্কার, এখানে এর সম্ভাব্য মান রয়েছে:

  • -1 , এর প্রারম্ভিক এবং নিরপেক্ষ মান, ইঙ্গিত করে যে আমরা কোনো ফাংশনের ভিতরে নেই যা ফিরে এসেছে।
  • কল স্ট্যাকের দৈর্ঘ্যের সমান ইতিবাচক মান , নির্দেশ করে যে আমরা এখনও একটি ফাংশনের ভিতরে আছি যা ফিরে এসেছে।
  • কল স্ট্যাকের দৈর্ঘ্যের চেয়ে বেশি ইতিবাচক মান , ইঙ্গিত করে যে আমরা আর ফাংশনের ভিতরে নেই যেটি ফিরে এসেছে।

Stoffle CLI ব্যবহার করে আমাদের প্রোগ্রাম চালানো

সিরিজের পূর্ববর্তী নিবন্ধে, আমরা Stoffle প্রোগ্রামগুলিকে সহজে ব্যাখ্যা করার জন্য একটি সাধারণ CLI তৈরি করেছি। এখন যেহেতু আমরা দোভাষীর বাস্তবায়ন অন্বেষণ করেছি, আসুন এটিকে কার্যত দেখি, আমাদের প্রোগ্রামটি চলমান। উপরে যেমন বিভিন্ন বিভাগে দেখানো হয়েছে, আমাদের কোড সংজ্ঞায়িত করে এবং তারপর sum_integers কে কল করে আর্গুমেন্ট পাস করার ফাংশন 1 এবং 100 . যদি আমাদের দোভাষী সঠিকভাবে কাজ করে, তাহলে আমাদের 5050.0 দেখতে হবে (1 থেকে শুরু হওয়া এবং 100 এ শেষ হওয়া পূর্ণসংখ্যার সেটের যোগফল) কনসোলে মুদ্রিত:

রুবিতে একটি প্রোগ্রামিং ভাষা তৈরি করা:দ্য ইন্টারপ্রেটার, পার্ট 2

ক্লোজিং থট

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

এই সিরিজের পরবর্তী এবং শেষ অংশে, আমি এমন কিছু সংস্থান শেয়ার করব যা আমি তাদের জন্য দুর্দান্ত বিকল্প হিসাবে বিবেচনা করি যারা তাদের প্রোগ্রামিং ভাষা বাস্তবায়নের অধ্যয়ন চালিয়ে যেতে চায়। আপনার Stoffle-এর সংস্করণ উন্নত করার সময় আপনার শেখা চালিয়ে যাওয়ার জন্য আমি কিছু চ্যালেঞ্জের প্রস্তাব করব। পরে দেখা হবে; ciao!


  1. রুবিতে একটি নতুন প্রোগ্রামিং ভাষা তৈরি করা:দোভাষী

  2. রুবিতে কার্যকরী প্রোগ্রামিং (সম্পূর্ণ নির্দেশিকা)

  3. রুবি নেটওয়ার্ক প্রোগ্রামিং

  4. প্রোগ্রামিং ভাষার প্রভাব গ্রাফটি কল্পনা করুন