Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyでの新しいプログラミング言語の構築:インタープリター

Githubのフルソース

Stoffleプログラミング言語の完全な実装は、GitHubで入手できます。バグを見つけたり質問がある場合は、遠慮なく問題を開いてください。

このブログ投稿では、完全にRubyで構築されたおもちゃのプログラミング言語であるStoffleのインタープリターの実装を開始します。このプロジェクトの詳細については、このシリーズの最初の部分をご覧ください。

これから作成するインタプリタは、一般にツリーウォークインタプリタと呼ばれます。このシリーズの前回の投稿では、トークンのフラットシーケンスをツリーデータ構造(抽象構文木、または略してAST)に変換するパーサーを構築しました。ご想像のとおり、私たちの通訳者は、パーサーによって作成されたASTを通過し、Stoffleプログラムに命を吹き込む仕事をしています。この最後のステップは、この言語実装の旅の中で最もエキサイティングなものだと思います。インタープリターを作成すると、最終的にすべてがクリックされ、Stoffleプログラムが実際に実行されているのを確認できます。

インタプリタの実装を2つのパートで示して説明します。この最初の部分では、変数、条件、単項および二項演算子、データ型、コンソールへの出力などの基本を機能させます。インタプリタの実装に関する2番目で最後の投稿のために、より重要なもの(関数定義、関数呼び出し、ループなど)を予約しています。

レクサーとパーサーの簡単な要約

インタプリタの実装を開始する前に、このシリーズの以前の投稿で行ったことをすぐに思い出してみましょう。まず、生のソースコードをトークンに変換するレクサーを作成しました。次に、トークンをツリー構造(AST)にモーフィングするコンポーネントであるパー​​サーを実装しました。要約すると、これまでに観察された変換は次のとおりです。

状態0:ソース

my_var = 1

状態1:レクサーは生のソースコードをトークンに変換します

[:identifier, :'=', :number]

状態2:パーサーはトークンを抽象構文ツリーに変換します

Rubyでの新しいプログラミング言語の構築:インタープリター

ウォーキングがすべてです

ASTができたので、私たちの仕事はこの構造をウォークするコードを書くことです。 ASTの各ノードが記述するものに命を吹き込むことができるRubyコードを作成する必要があります。たとえば、変数バインディングを記述するノードがある場合、タスクは、変数バインディング式の右辺の結果を何らかの方法で格納できるRubyコードを記述し、このストレージスペースを(および変数に付けられた名前からアクセスできます。

このシリーズの前のパートで行ったように、サンプルプログラムの処理に関係するすべての重要なコード行を調べて、実装について説明します。解釈する必要のあるStoffleコードは次のとおりです。

num = -2
if num > 0
  println("The number is greater than zero.")
else
  println("The number is less than or equal to zero.")
end

これは同じプログラム用に作成されたASTです:

Rubyでの新しいプログラミング言語の構築:インタープリター

私たちの散歩の最初のステップ

このシリーズの最後の投稿で覚えていると思いますが、StoffleASTのルートには常にAST::Programがあります。 ノード。このルートには通常、複数の子があります。それらのいくつかは浅くなります(単純な変数割り当てのために生成されたASTを考えてみてください)。他の子は、非常に深いサブツリーのルートになる可能性があります(本体内に多くの線があるループを考えてみてください)。これは、インタプリタに渡されたASTのウォークスルーを開始するために必要なRubyコードです:

module Stoffle
  class Interpreter
    attr_reader :program, :output, :env

    def initialize
      @output = []
      @env = {}
    end

    def interpret(ast)
      @program = ast

      interpret_nodes(program.expressions)
    end

    private

    def interpret_nodes(nodes)
      last_value = nil

      nodes.each do |node|
        last_value = interpret_node(node)
      end

      last_value
    end

    def interpret_node(node)
      interpreter_method = "interpret_#{node.type}"
      send(interpreter_method, node)
    end

    #...

  end
end

新しいInterpreterの場合 はインスタンス化され、最初から2つのインスタンス変数を作成します:@output および@env 。前者の責任は、私たちのプログラムが印刷したすべてのものを時系列で保存することです。この情報を手元に用意しておくと、自動テストやデバッグを作成するときに非常に役立ちます。 @envの責任 少し違います。 「環境」への言及などと名付けました。名前が示すように、その機能は実行中のプログラムの状態を保持することです。その機能の1つは、識別子(変数名など)とその現在の値の間のバインディングを実装することです。

#interpret_nodes メソッドはルートノードのすべての子をループします(AST::Program )。次に、#interpret_nodeを呼び出します 個々のノードごとに。

#interpret_node シンプルですが、それでも面白いです。ここでは、Rubyメタプログラミングを少し使用して、現在手元にあるノードタイプを処理するための適切なメソッドを呼び出します。たとえば、AST::VarBindingの場合 ノード、#interpret_var_binding メソッドが呼び出されます。

常に、変数について話し合う必要があります

実行しているサンプルプログラムのASTで最初に解釈する必要があるノードは、AST::VarBindingです。 。その@left AST::Identifierです 、およびその@right AST::UnaryOperatorです 。変数バインディングの解釈を担当するメソッドを見てみましょう:

def interpret_var_binding(var_binding)
  env[var_binding.var_name_as_str] = interpret_node(var_binding.right)
end

ご覧のとおり、これは非常に簡単です。キーと値のペアを@envに追加(または上書き)します ハッシュ。

キーは変数の名前です(#var_name_as_str var_binding.left.nameと同等のヘルパーメソッドです。 )。現時点では、すべての変数はグローバルです。次の投稿でスコープを処理します。

値は、割り当ての右側にある式を解釈した結果です。そのために、#interpret_nodeを使用します また。 AST::UnaryOperatorがあるので 右側では、次に呼び出されるメソッドは#interpret_unary_operatorです。 :

def interpret_unary_operator(unary_op)
  case unary_op.operator
  when :'-'
    -(interpret_node(unary_op.operand))
  else # :'!'
    !(interpret_node(unary_op.operand))
  end
end

Stoffleでサポートされている単項演算子のセマンティクス(- および! )はRubyと同じです。結果として、実装をこれ以上簡単にすることはできません。Rubyの-を適用します。 オペランドの解釈結果に対する演算子。いつもの容疑者、#interpret_node 、ここに再び表示されます。プログラムのASTから覚えているかもしれませんが、-のオペランド AST::Numberです (番号2 )。これは、次の停車地が#interpret_numberであることを意味します :

def interpret_number(number)
  number.value
end

#interpret_numberの実装 ケーキです。数値リテラルの表現としてRubyfloatを採用するという私たちの決定(これはレクサーで起こります!)はここで報われます。 @value AST::Numberの ノードはすでに必要な数値の内部表現を保持しているので、それを取得するだけです。

これで、AST::Programの最初の直接の子の解釈が終了します。 。ここで、プログラムの解釈を終了するには、他のより毛深い子、つまりタイプAST::Conditionalのノードを処理する必要があります。 。

利用規約が適用される場合があります

#interpret_nodesに戻る 、私たちの親友#interpret_node AST::Programの次の直接の子を解釈するために再度呼び出されます 。

def interpret_nodes(nodes)
  last_value = nil

  nodes.each do |node|
    last_value = interpret_node(node)
  end

  last_value
end

AST::Conditionalの解釈を担当するメソッド #interpret_conditionalです 。ただし、それを見る前に、AST::Conditionalの実装を確認して、思い出をリフレッシュしましょう。 それ自体:

class Stoffle::AST::Conditional < Stoffle::AST::Expression
  attr_accessor :condition, :when_true, :when_false

  def initialize(cond_expr = nil, true_block = nil, false_block = nil)
    @condition = cond_expr
    @when_true = true_block
    @when_false = false_block
  end

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

  def children
    [condition, when_true, when_false]
  end
end

では、@condition 真実または虚偽のいずれかになる表現を保持します。 @when_true @conditionの場合に実行される1つ以上の式を含むブロックを保持します 真実であり、@when_falseELSE 句)は、@conditionの場合に実行されるブロックを保持します たまたま間違っています。

それでは、#interpret_conditionを見てみましょう。 :

def interpret_conditional(conditional)
  evaluated_cond = interpret_node(conditional.condition)

  # We could implement the line below in a shorter way, but better to be explicit about truthiness in Stoffle.
  if evaluated_cond == nil || evaluated_cond == false
    return nil if conditional.when_false.nil?

    interpret_nodes(conditional.when_false.expressions)
  else
    interpret_nodes(conditional.when_true.expressions)
  end
end

Stoffleの真実性はRubyの場合と同じです。つまり、Stoffleでは、nilのみです。 およびfalse 偽りです。条件への他の入力はすべて真実です。

まず、conditional.conditionが保持している式を解釈して、条件を評価します。 。プログラムのASTをもう一度見て、処理しているノードを特定しましょう。

Rubyでの新しいプログラミング言語の構築:インタープリター

AST::BinaryOperatorがあることがわかりました (> num > 0で使用 )。さて、これも同じパスです。最初の#interpret_node#interpret_binary_operatorを呼び出します 今回:

def interpret_binary_operator(binary_op)
  case binary_op.operator
  when :and
    interpret_node(binary_op.left) && interpret_node(binary_op.right)
  when :or
    interpret_node(binary_op.left) || interpret_node(binary_op.right)
  else
    interpret_node(binary_op.left).send(binary_op.operator, interpret_node(binary_op.right))
  end
end

論理演算子(and およびor )は二項演算子と見なすことができるため、ここでもそれらを処理します。それらのセマンティクスはRubyの&&と同等であるため および|| 、上記のように、実装は単純な航海です。

次は、私たちが最も関心を持っている方法のセクションです。このセクションでは、他のすべての二項演算子(>を含む)を扱います )。ここでは、Rubyのダイナミズムを活用して、非常に簡潔なソリューションを考え出すことができます。 Rubyでは、操作に参加するオブジェクトのメソッドとして2項演算子を使用できます。

-2 > 0           # is equivalent to
-2.send(:'>', 0) # this
# and the following line would be a general solution,
# very similar to what we have in the interpreter
operand_1.send(binary_operator, operand_2)
二項演算子の詳細な実装

ご覧のとおり、二項演算子の実装は非常に簡潔です。 Rubyがそのような動的言語ではなかった場合、または演算子のセマンティクスがRubyとStoffleで異なる場合、この方法でソリューションをコーディングすることはできませんでした。

言語設計者/実装者のような立場にいることに気付いた場合は、スイッチ構造を使用するという単純な(ただしそれほどエレガントではない)ソリューションにいつでも頼ることができます。この場合、実装は次のようになります。

# ... inside #interpret_binary_operator ...

case binary_op.operator
when :'+'
  interpret_node(binary_op.left) + interpret_node(binary_op.right)
# ... other operators
end

#interpret_conditionalに戻る前に 、見落とされていないことを確認するために、簡単に迂回しましょう。私たちが解釈しているプログラムを覚えているなら、num 変数は比較に使用されます(二項演算子>を使用) )一緒に探索しました。左側のオペランド(つまり、numに格納されている値)をどのように取得しましたか その比較の変数)?その原因となるメソッドは#interpret_identifierです。 、およびその実装は簡単です-簡単です:

def interpret_identifier(identifier)
  if env.has_key?(identifier.name)
    env[identifier.name]
  else
    # Undefined variable.
    raise Stoffle::Error::Runtime::UndefinedVariable.new(identifier.name)
  end
end

ここで、#interpret_conditionalに戻ります。 。私たちの小さなプログラムの場合、条件はRuby falseに評価されました 価値。この値を使用して、条件構造のIFまたはELSEブランチを実行する必要があるかどうかを判断します。関連するコードブロックがconditional.when_falseに格納されているELSEブランチの解釈に進みます。 。ここに、AST::Blockがあります。 、これはASTのルートノード(AST::Program)と非常によく似ています。 )。同様に、ブロックには、解釈する必要のある一連の式が含まれている可能性があります。この目的のために、#interpret_nodesも使用します 。

def interpret_conditional(conditional)
  evaluated_cond = interpret_node(conditional.condition)

  # We could implement the line below in a shorter way, but better to be explicit about truthiness in Stoffle.
  if evaluated_cond == nil || evaluated_cond == false
    return nil if conditional.when_false.nil?

    interpret_nodes(conditional.when_false.expressions)
  else
    interpret_nodes(conditional.when_true.expressions)
  end
end

次に処理する必要のあるASTノードはAST::FunctionCallです。 。関数呼び出しの解釈を担当するメソッドは、#interpret_function_callです。 :

def interpret_function_call(fn_call)
  return if println(fn_call)
end

記事の冒頭で説明したように、関数の定義と関数の呼び出しについては、このシリーズの次の投稿で取り上げます。したがって、関数呼び出しの特殊なケースのみを実装しています。私たちの小さなおもちゃの言語では、printlnを提供しています ランタイムの一部として、ここのインタープリターに直接実装します。私たちのプロジェクトの目的と範囲を考えると、これは十分に良い解決策です。

def println(fn_call)
  return false if fn_call.function_name_as_str != 'println'

  result = interpret_node(fn_call.args.first).to_s
  output << result
  puts result
  true
end

AST::FunctionCallの最初で唯一の引数 AST::Stringです 、#interpret_stringによって処理されます :

def interpret_string(string)
  string.value
end

#interpret_string内 、#interpret_numberとまったく同じケースがあります 。 AST::String すぐに使用できるRuby文字列値をすでに保持しているため、取得する必要があります。

ここで、#printlnに戻ります。 :

def println(fn_call)
  return false if fn_call.function_name_as_str != 'println'

  result = interpret_node(fn_call.args.first).to_s
  output << result
  puts result
  true
end

関数の引数(Ruby文字列に変換)をresultに格納した後 、完了するにはさらに2つのステップがあります。まず、コンソールに出力しようとしているものを@outputに保存します 。前に説明したように、ここでの考え方は、何が印刷されたか(そしてどのような順序で)を簡単に検査できるようにすることです。これを手元に用意しておくと、インタプリタをデバッグまたはテストするときに私たちの生活が楽になります。最後に、コンソールへの印刷を実装するために、Rubyのputsを使用します 。

実行事項

Stoffleの基本を実装するために必要なすべてを調べたので、インタプリタの動作を確認するための非常に基本的な実行可能ファイルを作成しましょう。

#!/usr/bin/env ruby

require_relative '../lib/stoffle'

path = ARGV[0]
source = File.read(path)
lexer = Stoffle::Lexer.new(source)
parser = Stoffle::Parser.new(lexer.start_tokenization)
interpreter = Stoffle::Interpreter.new

interpreter.interpret(parser.parse)

exit(0)

ヒント: Stoffleのインタプリタをどこからでも使用するには、実行可能ファイルをPATHに追加することを忘れないでください。

いよいよプログラムを実行する時が来ました。すべてが正常に機能する場合は、コンソールに「数値がゼロ以下です」という文字列が出力されているはずです。これは、インタープリターを実行したときに起こることです。

Rubyでの新しいプログラミング言語の構築:インタープリター

ヒント: インタプリタがインストールされている場合は、numを変更してみてください サンプルプログラムの変数で、ゼロより大きい数値を保持します。予想どおり、IFブランチが実行され、「数値がゼロより大きい」という文字列が出力されます。

まとめ

この投稿では、Stoffleの通訳の始まりを見ました。変数、条件付き、単項および二項演算子、データ型、コンソールへの出力など、言語の基本の一部を処理するのに十分なインタープリターを実装しました。インタープリターの次の最後の部分では、小さなおもちゃの言語を設計どおりに機能させるために必要な残りのビット(変数のスコープ、関数の定義、関数の呼び出し、ループ)に取り組みます。記事を読んで楽しんでいただければ幸いです(私は確かにそれを書くのが楽しかったです!)。シリーズの次の投稿ですぐにお会いしましょう!


  1. Rubyネットワークプログラミング

    Rubyでカスタムネットワーククライアントとサーバーを作成しますか?または、それがどのように機能するかを理解しますか? 次に、ソケットを処理する必要があります。 このルビーネットワークプログラミングのツアーに参加してください 基本を学び、Rubyを使用して他のサーバーやクライアントと会話を始めましょう! では、ソケットとは何ですか ? ソケットは通信チャネルのエンドポイントであり、クライアントとサーバーの両方がソケットを使用して通信します。 動作方法は非常にシンプルです : 接続が確立されると、データをソケットに入れることができます。データはもう一方の端に送られ、そこで受信者はソケ

  2. プログラミング言語の影響グラフを視覚化する

    Gephi と Sigma.js を使用したネットワーク可視化チュートリアル これが今日作成するもののプレビューです:プログラミング言語はグラフに影響を与えます。リンクをチェックして、過去と現在の 250 を超えるプログラミング言語間の「デザインの影響」の関係を調べてください! あなたの番です! 今日の高度に接続された世界では、ネットワークは現代生活のいたるところにある側面です。 これまでの一日の始まり — ロンドンの交通網を利用しました 町に旅行する。それから支店に入りました お気に入りのコーヒー ショップの Wi-Fi ネットワークに接続するために Chromebook を使用しまし