Rubyでの新しいプログラミング言語の構築:インタープリター
Githubのフルソース
Stoffleプログラミング言語の完全な実装は、GitHubで入手できます。バグを見つけたり質問がある場合は、遠慮なく問題を開いてください。
このブログ投稿では、完全にRubyで構築されたおもちゃのプログラミング言語であるStoffleのインタープリターの実装を開始します。このプロジェクトの詳細については、このシリーズの最初の部分をご覧ください。
これから作成するインタプリタは、一般にツリーウォークインタプリタと呼ばれます。このシリーズの前回の投稿では、トークンのフラットシーケンスをツリーデータ構造(抽象構文木、または略してAST)に変換するパーサーを構築しました。ご想像のとおり、私たちの通訳者は、パーサーによって作成されたASTを通過し、Stoffleプログラムに命を吹き込む仕事をしています。この最後のステップは、この言語実装の旅の中で最もエキサイティングなものだと思います。インタープリターを作成すると、最終的にすべてがクリックされ、Stoffleプログラムが実際に実行されているのを確認できます。
インタプリタの実装を2つのパートで示して説明します。この最初の部分では、変数、条件、単項および二項演算子、データ型、コンソールへの出力などの基本を機能させます。インタプリタの実装に関する2番目で最後の投稿のために、より重要なもの(関数定義、関数呼び出し、ループなど)を予約しています。
インタプリタの実装を開始する前に、このシリーズの以前の投稿で行ったことをすぐに思い出してみましょう。まず、生のソースコードをトークンに変換するレクサーを作成しました。次に、トークンをツリー構造(AST)にモーフィングするコンポーネントであるパーサーを実装しました。要約すると、これまでに観察された変換は次のとおりです。
状態0:ソース
my_var = 1
状態1:レクサーは生のソースコードをトークンに変換します
[:identifier, :'=', :number]
状態2:パーサーはトークンを抽象構文ツリーに変換します
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です:
このシリーズの最後の投稿で覚えていると思いますが、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_false
(ELSE
句)は、@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をもう一度見て、処理しているノードを特定しましょう。
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に追加することを忘れないでください。
いよいよプログラムを実行する時が来ました。すべてが正常に機能する場合は、コンソールに「数値がゼロ以下です」という文字列が出力されているはずです。これは、インタープリターを実行したときに起こることです。
ヒント: インタプリタがインストールされている場合は、
num
を変更してみてください サンプルプログラムの変数で、ゼロより大きい数値を保持します。予想どおり、IFブランチが実行され、「数値がゼロより大きい」という文字列が出力されます。
この投稿では、Stoffleの通訳の始まりを見ました。変数、条件付き、単項および二項演算子、データ型、コンソールへの出力など、言語の基本の一部を処理するのに十分なインタープリターを実装しました。インタープリターの次の最後の部分では、小さなおもちゃの言語を設計どおりに機能させるために必要な残りのビット(変数のスコープ、関数の定義、関数の呼び出し、ループ)に取り組みます。記事を読んで楽しんでいただければ幸いです(私は確かにそれを書くのが楽しかったです!)。シリーズの次の投稿ですぐにお会いしましょう!
-
Rubyネットワークプログラミング
Rubyでカスタムネットワーククライアントとサーバーを作成しますか?または、それがどのように機能するかを理解しますか? 次に、ソケットを処理する必要があります。 このルビーネットワークプログラミングのツアーに参加してください 基本を学び、Rubyを使用して他のサーバーやクライアントと会話を始めましょう! では、ソケットとは何ですか ? ソケットは通信チャネルのエンドポイントであり、クライアントとサーバーの両方がソケットを使用して通信します。 動作方法は非常にシンプルです : 接続が確立されると、データをソケットに入れることができます。データはもう一方の端に送られ、そこで受信者はソケ
-
プログラミング言語の影響グラフを視覚化する
Gephi と Sigma.js を使用したネットワーク可視化チュートリアル これが今日作成するもののプレビューです:プログラミング言語はグラフに影響を与えます。リンクをチェックして、過去と現在の 250 を超えるプログラミング言語間の「デザインの影響」の関係を調べてください! あなたの番です! 今日の高度に接続された世界では、ネットワークは現代生活のいたるところにある側面です。 これまでの一日の始まり — ロンドンの交通網を利用しました 町に旅行する。それから支店に入りました お気に入りのコーヒー ショップの Wi-Fi ネットワークに接続するために Chromebook を使用しまし