Rubyでのプログラミング言語の構築:パーサー
Githubのフルソース
Stoffleプログラミング言語の完全な実装は、GitHubで入手できます。このリファレンス実装には、コードを読みやすくするためのコメントがたくさんあります。バグを見つけたり質問がある場合は、遠慮なく問題を開いてください。
このブログ投稿では、完全にRubyで構築されたおもちゃのプログラミング言語であるStoffleのパーサーを実装します。このプロジェクトの詳細については、このシリーズの最初の部分をご覧ください。
パーサーを最初から作成することで、予期しない洞察と知識が得られました。この経験は個人によって異なると思いますが、この記事を読んだ後は、次の少なくとも1つについての知識を習得または深めたと考えて差し支えありません。
- ソースコードの見方が異なり、その否定できないツリーのような構造に気付くようになります。 はるかに簡単です;
- シンタックスシュガーをより深く理解できるようになります Stoffle、Ruby、その他のプログラミング言語など、どこでも見られるようになります。
パーサーはパレートの法則の完璧な例だと思います。すばらしいエラーメッセージなど、いくつかの優れた機能を作成するために必要な作業量は明らかに多く、基本を稼働させるために必要な作業に不釣り合いです。私たちは本質に焦点を合わせ、私の愛する読者であるあなたへの挑戦として改善を残します。このシリーズの後半の記事で、より興味深いエキストラのいくつかを処理する場合と処理しない場合があります。
シンタックスシュガー シンタックスシュガーは、言語を使いやすくする構造を示すために使用される表現ですが、その削除によってプログラミング言語の機能が失われることはありません。良い例はRubyの
elsif
です 構築します。 Stoffleにはそのような構造がないため、同じ構造を表現するには、より冗長にする必要があります。if some_condition # ... else # oops, no elsif available if another_condition # ... end end
パーサーとは何ですか?
まず、生の文字シーケンスであるソースコードから始めます。次に、このシリーズの前回の記事で示したように、レクサーはこれらの文字をトークンと呼ばれる適切なデータ構造にグループ化します。ただし、完全にフラットなデータが残っています。これは、ソースコードのネストされた性質を適切に表していないものです。したがって、パーサーには、この一連の文字からツリーを作成する役割があります。タスクが完了すると、プログラムの各部分がどのようにネストされ、相互に関連しているかを最終的に表現できるデータが得られます。
パーサーによって作成されるツリーは、一般に、抽象構文木(AST)と呼ばれます。名前が示すように、ツリーデータ構造を扱っています。そのルートはプログラム自体を表し、このプログラムノードの子はプログラムを構成する多くの式です。単語abstract ASTでは、抽象化する能力を指します 前のステップで明示的に存在していたパーツ。良い例は、式ターミネータ(Stoffleの場合は新しい行)です。これらはASTを構築するときに考慮されますが、ターミネータを表すために特定のノードタイプは必要ありません。ただし、明示的なトークンがあったことを忘れないでください ターミネーターを表すため。
ASTの例を視覚化すると便利ではないでしょうか。あなたの願いは注文です!以下は、この投稿の後半で段階的に解析する単純なStoffleプログラムと、それに対応する抽象構文ツリーです。
fn double: num
num * 2
end
ソースからASTへ、最初の例
投稿のこのセクションでは、パーサーが非常にをどのように処理するかを段階的に説明します。 変数バインディングが表現される単一の行で構成される単純なStoffleプログラム(つまり、変数に値を割り当てます)。ソース、レクサーによって生成されたトークンの簡略化された表現(これらのトークンはパーサーに供給される入力です)、そして最後に、プログラムを表すために生成するASTの視覚的表現です:
ソース
my_var = 1
トークン(レクサーの出力、パーサーの入力)
[:identifier, :'=', :number]
パーサーの出力の視覚的表現(抽象構文木)
ご想像のとおり、パーサーのコアはレクサーのコアと非常によく似ています。レクサーの場合、処理する文字がたくさんありました。ここでも、コレクションを反復処理する必要がありますが、パーサーの場合は、レクサーの友人によって生成されたトークンのリストを確認します。単一のポインター( @next_p
)トークンのコレクションにおける私たちの位置を追跡するため。このポインタは次へをマークします 処理されるトークン。この単一のポインターしかありませんが、必要に応じて使用できる他の多くの「仮想」ポインターがあります。実装に沿って進むと、それらが表示されます。そのような「仮想」ポインタの1つは、 current
です。 (基本的に、 @next_p-1
のトークン 。
#parse
トークンをASTに変換するために呼び出すメソッドです。ASTは@ast
で利用できます。 インスタンス変数。 #parse
の実装 簡単です。 #consume
を呼び出して、コレクションを進めていきます @next_p
を移動します 処理するトークンがなくなるまで(つまり、 next_p
#parse_expr_recursively
ASTノードを返すか、まったく返さない可能性があります。たとえば、ASTでターミネータを表す必要はありません。ループの現在の反復でノードが構築された場合は、それを @ast
に追加します 続行する前に。 #parse_expr_recursively
@next_p
も移動しています なぜなら、特定の構成の始まりを示すトークンを見つけた場合、現在解析しているものを完全に表すノードを最終的に構築できるようになるまで、複数回進む必要があるからです。 if
を表すノードを構築するために消費する必要のあるトークンの数を想像してみてください 。
module Stoffle
class Parser
attr_accessor :tokens, :ast, :errors
# ...
def initialize(tokens)
@tokens = tokens
@ast = AST::Program.new
@next_p = 0
@errors = []
end
def parse
while pending_tokens?
consume
node = parse_expr_recursively
ast << node if node != nil
end
end
# ...
end
end
上記のスニペットでは、初めて、パーサー実装の一部である多くのタイプのASTノードの1つが提示されました。以下に、 AST ::Program
の完全なソースコードを示します。 ノードタイプ。ご想像のとおり、これはプログラム全体を表すツリーのルートです。その中で最も興味深い部分を詳しく見てみましょう:
- Stoffleプログラムは
@expressions
で構成されています;これらは#children
ですAST ::Program
の ノード; - もう一度見るように、すべてのノードタイプは
#==
を実装します 方法。結果として、2つの単純なノードだけでなく、プログラム全体を簡単に比較できます。 2つのプログラム(または2つの複雑なノード)を比較する場合、それらの同等性は、すべての子の同等性、すべての子のすべての子の同等性などによって決定されます。#==
で使用されるこのシンプルで強力な戦略 実装のテストに非常に役立ちます。
class Stoffle::AST::Program
attr_accessor :expressions
def initialize
@expressions = []
end
def <<(expr)
expressions << expr
end
def ==(other)
expressions == other&.expressions
end
def children
expressions
end
end
式とステートメント 一部の言語では、多くの構成要素が値を生成しません。条件付きは典型的な例です。これらはステートメントと呼ばれます 。ただし、他の構造は値に評価されます(関数呼び出しなど)。これらは式と呼ばれます 。ただし、他の言語では、すべてが式であり、値を生成します。 Rubyはこのアプローチの例です。たとえば、次のスニペットをIRBに入力して、メソッド定義が評価する値を確認してみてください。
irb(main):001:0> def two; 2; end
IRBを起動したくない場合は、ニュースをお伝えします。 Rubyでは、メソッド定義 expression シンボル(メソッド名)に評価されます。ご存知のように、StoffleはRubyに大きく影響を受けているため、小さなおもちゃの言語では、すべてが表現でもあります。
これらの定義は実用的な目的には十分ですが、実際にはコンセンサスがなく、ステートメントと表現という用語が他の場所で異なって定義されている場合があります。
深く掘り下げる:#parse_expr_recursivelyの実装を開始する
上記のスニペットで見たように、 #parse_expr_recursively
新しいASTノードを構築するために呼び出されるメソッドです。 #parse_expr_recursively
解析プロセスでは他の多くの小さなメソッドを使用しますが、それがパーサーの本当のエンジンであると静かに言うことができます。それほど長くはありませんが、この方法は消化がより困難です。したがって、2つの部分に分割します。投稿のこのセクションでは、最初のセグメントから始めましょう。これは、Stoffleプログラミング言語のいくつかの単純な部分を解析するのに十分強力です。復習として、単一の変数バインディング式で構成される単純なプログラムを解析するために必要な手順を実行していることを忘れないでください。
ソース
my_var = 1
トークン(レクサーの出力、パーサーの入力)
[:identifier, :'=', :number]
処理する必要のあるトークンを確認し、他の同様の単純な式のレクサー出力がどうなるかを想像した後、トークンタイプを特定の解析メソッドに関連付けてみるのは良い考えのようです。
def parse_expr_recursively
parsing_function = determine_parsing_function
if parsing_function.nil?
unrecognized_token_error
return
end
send(parsing_function)
end
#parse_expr_recursively
のこの最初の実装では 、それはまさに私たちがしていることです。 かなりたくさんあるので 処理する必要のあるさまざまなトークンタイプの場合、この意思決定プロセスを別のメソッド- #determine_parsing_function
に抽出することをお勧めします。 、これはすぐにわかります。
終了したら、認識できないトークンはないはずですが、安全策として、解析関数が現在のトークンに関連付けられているかどうかを確認します。そうでない場合は、インスタンス変数 @errors
にエラーを追加します 、解析中に発生したすべての問題を保持します。ここでは詳しく説明しませんが、興味があればGitHubでパーサーの完全な実装を確認できます。
#determine_parsing_function
呼び出される解析メソッドの名前を表すシンボルを返します。 Rubyのsend
を使用します その場で適切なメソッドを呼び出す。
次に、 #determine_parsing_function
を見てみましょう。 Stoffleのさまざまな構成を解析するために特定のメソッドを呼び出すためのこの初期メカニズムを理解する。 #determine_parsing_function
二項演算子を除くすべて(キーワードや単項演算子など)に使用されます。二項演算子の場合に使用される手法については、後で説明します。とりあえず、 #determine_parsing_function
をチェックしてみましょう。 :
def determine_parsing_function
if [:return, :identifier, :number, :string, :true, :false, :nil, :fn,
:if, :while].include?(current.type)
"parse_#{current.type}".to_sym
elsif current.type == :'('
:parse_grouped_expr
elsif [:"\n", :eof].include?(current.type)
:parse_terminator
elsif UNARY_OPERATORS.include?(current.type)
:parse_unary_operator
end
end
前に説明したように、 #current
仮想です 現在処理中のトークンへのポインタ。 #determine_parsing_function
の実装 非常に簡単です。現在のトークン(具体的には、そのタイプ)を確認します )そして、呼び出される適切なメソッドを表すシンボルを返します。
変数バインディングを解析するために必要な手順を実行していることを忘れないでください( my_var =1
)、したがって、処理するトークンタイプは [:identifier、:'='、:number]
です。 。現在のトークンタイプは:identifier
です。 、したがって、 #determine_parsing_function
:parse_identifier
を返します 、ご想像のとおり。次のステップである#parse_identifier
を見てみましょう。 方法:
def parse_identifier
lookahead.type == :'=' ? parse_var_binding : AST::Identifier.new(current.lexeme)
end
ここでは、基本的に、他のすべての解析メソッドが行うことの非常に単純な表現があります。 #parse_identifier
内 -および他の解析メソッドでは、トークンをチェックして、期待している構造が実際に存在するかどうかを判断します。識別子があることはわかっていますが、次のトークンを調べて、変数バインディングを処理しているかどうかを判断する必要があります。これは、次のトークンタイプが:'='
の場合に当てはまります。 、またはそれ自体で識別子を持っている場合、またはより複雑な式に参加している場合。たとえば、変数に格納されている値を操作している算術式を想像してみてください。
:'='
があるので 次に来る、 #parse_var_binding
呼ばれる予定です:
def parse_var_binding
identifier = AST::Identifier.new(current.lexeme)
consume(2)
AST::VarBinding.new(identifier, parse_expr_recursively)
end
ここでは、現在処理している識別子を表す新しいASTノードを作成することから始めます。 AST ::Identifier
のコンストラクター 識別子トークンの語彙素(つまり、文字のシーケンス、文字列 "my_var"
)が必要です。 私たちの場合)、それが私たちがそれに供給するものです。次に、処理中のトークンのストリーム内の2つのスポットを進め、タイプが:number
のトークンを作成します。 次に分析するもの。 [:identifier、:'='、:number]
を扱っていることを思い出してください。 。
最後に、変数バインディングを表すASTノードを構築して返します。 AST ::VarBinding
のコンストラクター 識別子ASTノード(バインディング式の左側)と有効な式(右側)の2つのパラメーターが必要です。ここで注意すべき重要なことが1つあります。変数バインディング式の右辺を生成するには、 #parse_expr_recursively
を呼び出します。 もう一度 。これは最初は少し奇妙に感じるかもしれませんが、この例のように、変数は単なる数値だけでなく、非常に複雑な式にバインドできることに注意してください。解析戦略を一言で定義すると、再帰的になります。 。さて、あなたはなぜ #parse_expr_recursively
なのかを理解し始めていると思います 名前があります。
このセクションを終了する前に、両方の AST ::Identifier
をすばやく調べる必要があります。 およびAST::VarBinding
。まず、 AST ::Identifier
:
class Stoffle::AST::Identifier < Stoffle::AST::Expression
attr_accessor :name
def initialize(name)
@name = name
end
def ==(other)
name == other&.name
end
def children
[]
end
end
ここには特別なものは何もありません。ノードには変数の名前が格納されており、子はありません。
さて、 AST ::VarBinding
:
class Stoffle::AST::VarBinding < Stoffle::AST::Expression
attr_accessor :left, :right
def initialize(left, right)
@left = left
@right = right
end
def ==(other)
children == other&.children
end
def children
[left, right]
end
end
左側はAST::Identity
です。 ノード。右側は、識別子がバインドされている式を表す、ほぼすべての可能なタイプのノード(この例のような単純な数値から、より複雑なものまで)です。 #children
変数バインディングの1つは、 @left
に保持されているASTノードです。 および@right
。
ソースからASTへ、2番目の例
#parse_expr_recursively
の現在の化身 前のセクションで見たように、はすでにいくつかの単純な式を解析できます。このセクションでは、バイナリ演算子や論理演算子などのより複雑なエンティティも解析できるように、実装を終了します。ここでは、パーサーが関数を定義するプログラムをどのように処理するかを段階的に説明します。ソース、レクサーによって生成されたトークンの簡略化された表現、および最初のセクションで示したように、プログラムを表すために生成するASTの視覚的表現は次のとおりです。
ソース
fn double: num
num * 2
end
トークン(レクサーの出力、パーサーの入力)
[:fn, :identifier, :":", :identifier, :"\n", :identifier, :*, :number, :"\n", :end, :"\n", :eof]
パーサー出力の視覚的表現(抽象構文木)
先に進む前に、一歩下がって、演算子の優先順位規則について説明しましょう。これらは数学的慣習に基づいています。数学では、これらは単なる慣例であり、実際、私たちが慣れている演算子の優先順位をもたらす基本的なものは何もありません。これらのルールにより、式を評価し、この場合は最初に式を解析するための正しい順序を決定することもできます。各演算子の優先順位を定義するために、マップ(つまり、 Hash
)を用意するだけです。 )トークンタイプと整数の。数値が大きいほど、オペレーターを最初に処理する必要があります :
# We define these precedence rules at the top of Stoffle::Parser.
module Stoffle
class Parser
# ...
UNARY_OPERATORS = [:'!', :'-'].freeze
BINARY_OPERATORS = [:'+', :'-', :'*', :'/', :'==', :'!=', :'>', :'<', :'>=', :'<='].freeze
LOGICAL_OPERATORS = [:or, :and].freeze
LOWEST_PRECEDENCE = 0
PREFIX_PRECEDENCE = 7
OPERATOR_PRECEDENCE = {
or: 1,
and: 2,
'==': 3,
'!=': 3,
'>': 4,
'<': 4,
'>=': 4,
'<=': 4,
'+': 5,
'-': 5,
'*': 6,
'/': 6,
'(': 8
}.freeze
# ...
end
end
#parse_expr_recursively
で使用される手法 これは、コンピューター科学者のVaughanPrattが1973年の論文「TopDownOperatorPrecedence」で発表した有名なパーサーアルゴリズムに基づいています。ご覧のとおり、アルゴリズムは非常に単純ですが、完全に把握するのは少し難しいです。少し不思議な感じがします。この投稿で、この手法の動作を直感的に理解するために行うことは、上記のStoffleスニペットを解析するときに何が起こるかを段階的に説明することです。したがって、これ以上面倒なことはせずに、 #parse_expr_recursively
の完成版を次に示します。 :
def parse_expr_recursively(precedence = LOWEST_PRECEDENCE)
parsing_function = determine_parsing_function
if parsing_function.nil?
unrecognized_token_error
return
end
expr = send(parsing_function)
return if expr.nil? # When expr is nil, it means we have reached a \n or a eof.
# Note that, here, we are checking the NEXT token.
while nxt_not_terminator? && precedence < nxt_precedence
infix_parsing_function = determine_infix_function(nxt)
return expr if infix_parsing_function.nil?
consume
expr = send(infix_parsing_function, expr)
end
expr
end
#parse_expr_recursively
パラメータprecedence
を受け入れるようになりました 。これは、メソッドの特定の呼び出しで考慮される優先順位の「レベル」を表します。さらに、メソッドの最初の部分はほとんど同じです。次に、この最初の部分で式を作成できた場合は、メソッドの新しい部分が登場します。次のトークンはターミネーター(つまり、改行またはファイルの終わり)および優先順位( precedence
)ではありませんが param)は次のトークンの優先順位よりも低いため、トークンストリームを引き続き消費する可能性があります。
while
の内部を見る前に 、2番目の条件( precedence
expr
ローカル変数)は、おそらくまだ構築されていないノードの子です(抽象構文ツリーであるASTを構築していることを思い出してください。 )。 Stoffleスニペットの解析を実行する前に、単純な算術式の解析について考えてみましょう: 2 + 2
。この式を解析するとき、メソッドの最初の部分は AST ::Number
を作成します 最初の2
を表すノード expr
に保存します 。次に、 while
にステップインします。 次のトークンの優先順位(:'+'
)デフォルトの優先順位よりも高くなります。次に、呼び出された合計を処理するための解析メソッドがあり、それに AST ::Number
を渡します。 ノードと二項演算子を表すノードの受信( AST ::BinaryOperator
)。最後に、上書きします expr
に保存されている値 、最初の 2
を表すノード 2 + 2
で 、この新しいノードはプラス演算子を表します。最後に、このアルゴリズムにより、ノードを再配置できることに注意してください。; AST ::Number
の作成から始めました ノードであり、 AST ::BinaryOperator
の子の1つとして、ツリーのより深いノードになりました。 ノード。
これで、 #parse_expr_recursively
の全体的な説明を終えました。 、単純な関数定義に戻りましょう:
fn double: num
num * 2
end
このスニペットを解析するときにパーサーの実行の簡単な説明を見るというアイデアでさえ、面倒に感じるかもしれませんが(実際、そうかもしれません!)、#parse_expr_recursively<の両方をよりよく理解するために非常に価値があると思います。 / code> 特定のビットの解析(関数定義と積演算子)。まず最初に、処理するトークンタイプを示します(以下は
@ tokens.map(&:type)
の出力です 、レクサーが今見たスニペットのトークン化を完了した後のパーサー内):
[:fn, :identifier, :":", :identifier, :"\n", :identifier, :*, :number, :"\n", :end, :"\n", :eof]
次の表は、上記のトークンを解析するときに最も重要なメソッドが呼び出される順序を示しています。これは単純化であることに注意してください。パーサー実行のすべての正確なステップを本当に理解したい場合は、ByebugなどのRubyデバッガーを使用し、プログラムの実行時に行ごとに進めることをお勧めします。
顕微鏡下のパーサー 私たちが調査しているこの正確なスニペットを使用するテストがあります。これはStoffleのソースで入手できます。これはspec/lib / stoffle/parser_spec.rb内にあります。これは、
complex_program_ok_2.sfe
というスニペットを使用するテストです。 。パーサーの実行を段階的に調べるために、ソースを編集して
byebug
への呼び出しを追加できます。Parser#parse
の先頭 、RSpecで前述のテストのみを実行してから、Byebugのstep
を使用します プログラムを一度に1行進めるコマンド。GitHubにあるプロジェクトのREADMEファイルにアクセスして、Byebugの動作と使用可能なすべてのコマンドの詳細を確認してください。
呼び出されたメソッド | 現在のトークン | 次のトークン | 注目すべき変数/呼び出し結果 | Obs |
---|---|---|---|---|
解析 | nil | :fn | ||
parse_expr_recursively | :fn | :識別子 | precedence =0、parsing_function =:parse_fn | |
parse_function_definition | :fn | :識別子 | parse_fnはparse_function_definitionのエイリアスです | |
parse_function_params | :識別子 | : ":" | ||
parse_block | : "\ n" | :識別子 | ||
parse_expr_recursively | :識別子 | :* | precedence =0、parsing_function =:parse_identifier、nxt_precedence()は6を返し、infix_parsing_function =:parse_binary_operator | |
parse_identifier | :識別子 | :* | ||
parse_binary_operator | :* | :number | op_precedence =6 | |
parse_expr_recursively | :number | : "\ n" | precedence =6、parsing_function =:parse_number、nxt_precedence()は0を返します | |
parse_number | :number | : "\ n" |
呼び出されるメソッドの一般的な概念とシーケンスがわかったので、まだ詳しく見ていない構文解析メソッドのいくつかを調べてみましょう。
#parse_function_definitionメソッド
メソッド#parse_function_definition
現在のトークンが:fn
のときに呼び出されました 次は:identifier
でした :
def parse_function_definition
return unless consume_if_nxt_is(build_token(:identifier))
fn = AST::FunctionDefinition.new(AST::Identifier.new(current.lexeme))
if nxt.type != :"\n" && nxt.type != :':'
unexpected_token_error
return
end
fn.params = parse_function_params if nxt.type == :':'
return unless consume_if_nxt_is(build_token(:"\n", "\n"))
fn.body = parse_block
fn
end
#consume_if_nxt_is
-ご想像のとおり-次のトークンが特定のタイプの場合、ポインタを進めます。それ以外の場合は、 @errors
にエラーを追加します 。 #consume_if_nxt_is
トークンの構造をチェックして有効な構文があるかどうかを判断する場合に、非常に便利であり、多くのパーサーメソッドで使用されます。その後、現在のトークンのタイプは:identifier
になります。 ('double'
を処理しています 、関数の名前)、 AST ::Identifier
を作成します そしてそれをコンストラクターに渡して、関数定義を表すノードを作成します( AST ::FunctionDefinition
)。ここでは詳しく説明しませんが、基本的には AST ::FunctionDefinition
ノードは、関数名、場合によってはパラメーターの配列と関数本体を予期します。
#parse_function_definition
の次のステップ 識別子の次のトークンが式ターミネーター(つまり、パラメーターのない関数)であるか、コロン(つまり、1つまたは複数のパラメーターを持つ関数)であるかを確認することです。 double
の場合のように、パラメータがある場合 定義している関数、 #parse_function_params
を呼び出します それらを解析します。このメソッドについてはすぐに確認しますが、最初に続行して、 #parse_function_definition
の探索を終了しましょう。 。
最後のステップは、別の構文チェックを実行して、関数名+ paramsの後にターミネーターがあることを確認してから、 #parse_block
を呼び出して関数本体の解析に進みます。 。最後に、 fn
を返します。 、完全に構築された AST ::FunctionDefinition
を保持します インスタンス、関数名、パラメータ、および本文を完備します。
前のセクションで、 #parse_function_params
を見ました #parse_function_definition
内で呼び出されます 。パーサーの実行フローと状態を要約した表に戻ると、 #parse_function_params
の場合にそれを確認できます。 実行を開始します。現在のトークンのタイプは:identifier
です。 、次は: ":"
です (つまり、関数名の解析が終了しました)。これらすべてを念頭に置いて、さらにいくつかのコードを見てみましょう。
def parse_function_params
consume
return unless consume_if_nxt_is(build_token(:identifier))
identifiers = []
identifiers << AST::Identifier.new(current.lexeme)
while nxt.type == :','
consume
return unless consume_if_nxt_is(build_token(:identifier))
identifiers << AST::Identifier.new(current.lexeme)
end
identifiers
end
このメソッドの各部分を説明する前に、処理する必要のあるトークンと、このジョブの現在の場所を要約してみましょう。
[:fn, :identifier, :":", :identifier, :"\n", :identifier, :*, :number, :"\n", :end, :"\n", :eof]
# here ▲
#parse_function_params
の最初の部分 簡単です。有効な構文がある場合(の後に少なくとも1つの識別子:
)、ポインタを2つの位置に移動することになります:
[:fn, :identifier, :":", :identifier, :"\n", :identifier, :*, :number, :"\n", :end, :"\n", :eof]
# ▲
これで、解析される最初のパラメーター(:identifier
タイプのトークン)に座っています。 )。予想どおり、 AST ::Identifier
を作成します そして、それを、潜在的に、まだ解析されていない他の複数のパラメーターの配列にプッシュします。 #parse_function_params
の次のビット 、パラメータセパレータ(つまり、タイプ:'、'
のトークン)がある限り、パラメータの解析を続行します )。ローカル変数identifiers
を返すことでメソッドを終了します 、複数の AST ::Identity
を含む可能性のある配列 ノード。それぞれが1つのパラメータを表します。ただし、この場合、この配列には1つの要素しかありません。
関数本体はどうですか?
それでは、関数定義の解析の最後の部分である、その本体の処理について詳しく見ていきましょう。 #parse_block
の場合 と呼ばれる、関数パラメータのリストの最後をマークしたターミネータに座っています:
[:fn, :identifier, :":", :identifier, :"\n", :identifier, :*, :number, :"\n", :end, :"\n", :eof]
# ▲
And, here's the implementation of #parse_block
:
def parse_block
consume
block = AST::Block.new
while current.type != :end && current.type != :eof && nxt.type != :else
expr = parse_expr_recursively
block << expr unless expr.nil?
consume
end
unexpected_token_error(build_token(:eof)) if current.type == :eof
block
end
AST::Block
is the AST node for representing, well, a block of code. In other words, AST::Block
just holds a list of expressions, in a very similar fashion as the root node of our program, an AST::Program
node (as we saw at the beginning of this post). To parse the block (i.e., the function body), we continue advancing through unprocessed tokens until we encounter a token that marks the end of the block.
To parse the expressions that compose the block, we use our already-known #parse_expr_recursively
。 We will step into this method call in just a moment; this is the point in which we will start parsing the product operation happening inside our double
関数。 Following this closely will allow us to understand the use of precedence values inside #parse_expr_recursively
and how an infix operator (the *
in our case) gets dealt with. Before we do that, however, let's finish our exploration of #parse_block
。
Before returning an AST node representing our block, we check whether the current token is of type :eof
。 If this is the case, we have a syntax error since Stoffle requires a block to end with the end
キーワード。 To wrap up the explanation of #parse_block
, I should mention something you have probably noticed; one of the conditions of our loop verifies whether the next token is of type :else
。 This happens because #parse_block
is shared by other parsing methods, including the methods responsible for parsing conditionals and loops. Pretty neat, huh?!
Parsing Infix Operators
The name may sound a bit fancy, but infix operators are, basically, those we are very used to seeing in arithmetic, plus some others that we may be more familiar with by being software developers:
module Stoffle
class Parser
# ...
BINARY_OPERATORS = [:'+', :'-', :'*', :'/', :'==', :'!=', :'>', :'<', :'>=', :'<='].freeze
LOGICAL_OPERATORS = [:or, :and].freeze
# ...
end
end
They are expected to be used infixed when the infix notation is used, as is the case with Stoffle, which means they should appear in the middle of their two operands (e.g., as *
appears in num * 2
in our double
関数)。 Something worth mentioning is that although the infix notation is pretty popular, there are other ways of positioning operators in relation to their operands. If you are curious, research a little bit about "Polish notation" and "reverse Polish notation" methods.
To finish parsing our double
function, we have to deal with the *
operator:
fn double: num
num * 2
end
In the previous section, we mentioned that parsing the expression that composes the body of double
starts when #parse_expr_recursively
is called within #parse_block
。 When that happens, here's our position in @tokens
:
[:fn, :identifier, :":", :identifier, :"\n", :identifier, :*, :number, :"\n", :end, :"\n", :eof]
# ▲
And, to refresh our memory, here's the code for #parse_expr_recursively
again:
def parse_expr_recursively(precedence = LOWEST_PRECEDENCE)
parsing_function = determine_parsing_function
if parsing_function.nil?
unrecognized_token_error
return
end
expr = send(parsing_function)
return if expr.nil? # When expr is nil, it means we have reached a \n or a eof.
# Note that here, we are checking the NEXT token.
while nxt_not_terminator? && precedence < nxt_precedence
infix_parsing_function = determine_infix_function(nxt)
return expr if infix_parsing_function.nil?
consume
expr = send(infix_parsing_function, expr)
end
expr
end
In the first part of the method, we will use the same #parse_identifier
method we used to parse the num
variable. Then, for the first time, the conditions of the while
loop will evaluate to true; the next token is not a terminator, and the precedence of the next token is greater than the precedence of this current execution of parse_expr_recursively
(precedence
is the default, 0, while nxt_precedence
returns 6 since the next token is of type :'*'
)。 This indicates that the node we already built (an AST::Identifier
representing num
) will probably be deeper in our AST (i.e., it will be the child of a node yet to be built). We enter the loop and call #determine_infix_function
, passing to it the next token (the *
):
def determine_infix_function(token = current)
if (BINARY_OPERATORS + LOGICAL_OPERATORS).include?(token.type)
:parse_binary_operator
elsif token.type == :'('
:parse_function_call
end
end
Since *
is a binary operator, running #determine_infix_function
will result in :parse_binary_operator
。 Back in #parse_expr_recursively
, we will advance our tokens pointer by one position and then call #parse_binary_operator
, passing along the value of expr
(the AST::Identifier
representing num
):
[:fn, :identifier, :":", :identifier, :"\n", :identifier, :*, :number, :"\n", :end, :"\n", :eof]
#▲
def parse_binary_operator(left)
op = AST::BinaryOperator.new(current.type, left)
op_precedence = current_precedence
consume
op.right = parse_expr_recursively(op_precedence)
op
end
class Stoffle::AST::BinaryOperator < Stoffle::AST::Expression
attr_accessor :operator, :left, :right
def initialize(operator, left = nil, right = nil)
@operator = operator
@left = left
@right = right
end
def ==(other)
operator == other&.operator && children == other&.children
end
def children
[left, right]
end
end
At #parse_binary_operator
, we create an AST::BinaryOperator
(its implementation is shown above if you are curious) to represent *
, setting its left operand to the identifier (num
) we received from #parse_expr_recursively
。 Then, we save the precedence value of *
at the local var op_precedence
and advance our token pointer. To finish building our node representing *
, we call #parse_expr_recursively
again! We need to proceed in this fashion because the right-hand side of our operator will not always be a single number or identifier; it can be a more complex expression, such as something like num * (2 + 2)
。
One thing of utmost importance that happens here at #parse_binary_operator
is the way in which we call #parse_expr_recursively
back again. We call it passing 6
as a precedence value (the precedence of *
, stored at op_precedence
)。 Here we observe an important aspect of our parsing algorithm, which was mentioned previously. By passing a relatively high precedence value, it seems like *
is pulling the next token as its operand. Imagine we were parsing an expression like num * 2 + 1
; in this case, the precedence value of *
passed in to this next call to #parse_expr_recursively
would guarantee that the 2
would end up being the right-hand side of *
and not an operand of +
, which has a lower precedence value of 5
。
After #parse_expr_recursively
returns an AST::Number
node, we set it as the right-hand size of *
and, finally, return our complete AST::BinaryOperator
ノード。 At this point, we have, basically, finished parsing our Stoffle program. We still have to parse some terminator tokens, but this is straightforward and not very interesting. At the end, we will have an AST::Program
instance at @ast
with expressions that could be visually represented as the tree we saw at the beginning of this post and in the introduction to the second section of the post:
In this post, we covered the principal aspects of Stoffle's parser. If you understand the bits we explored here, you shouldn't have much trouble understanding other parser methods we were not able to cover, such as parsing conditionals and loops. I encourage you to explore the source code of the parser by yourself and tweak the implementation if you are feeling more adventurous! The implementation is accompanied by a comprehensive test suite, so don't be afraid to try things out and mess up with the parser.
In subsequent posts in this series, we will finally breathe life into our little monster by implementing the last bit we are missing:the tree-walk interpreter. I can't wait to be there with you as we run our first Stoffle program!
-
Rubyネットワークプログラミング
Rubyでカスタムネットワーククライアントとサーバーを作成しますか?または、それがどのように機能するかを理解しますか? 次に、ソケットを処理する必要があります。 このルビーネットワークプログラミングのツアーに参加してください 基本を学び、Rubyを使用して他のサーバーやクライアントと会話を始めましょう! では、ソケットとは何ですか ? ソケットは通信チャネルのエンドポイントであり、クライアントとサーバーの両方がソケットを使用して通信します。 動作方法は非常にシンプルです : 接続が確立されると、データをソケットに入れることができます。データはもう一方の端に送られ、そこで受信者はソケ
-
プログラミング言語の影響グラフを視覚化する
Gephi と Sigma.js を使用したネットワーク可視化チュートリアル これが今日作成するもののプレビューです:プログラミング言語はグラフに影響を与えます。リンクをチェックして、過去と現在の 250 を超えるプログラミング言語間の「デザインの影響」の関係を調べてください! あなたの番です! 今日の高度に接続された世界では、ネットワークは現代生活のいたるところにある側面です。 これまでの一日の始まり — ロンドンの交通網を利用しました 町に旅行する。それから支店に入りました お気に入りのコーヒー ショップの Wi-Fi ネットワークに接続するために Chromebook を使用しまし