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

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

Rubyでのプログラミング言語の構築:パーサー

ソースからASTへ、最初の例

投稿のこのセクションでは、パーサーが非常にをどのように処理するかを段階的に説明します。 変数バインディングが表現される単一の行で構成される単純なStoffleプログラム(つまり、変数に値を割り当てます)。ソース、レクサーによって生成されたトークンの簡略化された表現(これらのトークンはパーサーに供給される入力です)、そして最後に、プログラムを表すために生成するASTの視覚的表現です:

ソース

my_var = 1

トークン(レクサーの出力、パーサーの入力)

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

パーサーの出力の視覚的表現(抽象構文木)

Rubyでのプログラミング言語の構築:パーサー

ご想像のとおり、パーサーのコアはレクサーのコアと非常によく似ています。レクサーの場合、処理する文字がたくさんありました。ここでも、コレクションを反復処理する必要がありますが、パーサーの場合は、レクサーの友人によって生成されたトークンのリストを確認します。単一のポインター( @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]

パーサー出力の視覚的表現(抽象構文木)

Rubyでのプログラミング言語の構築:パーサー

先に進む前に、一歩下がって、演算子の優先順位規則について説明しましょう。これらは数学的慣習に基づいています。数学では、これらは単なる慣例であり、実際、私たちが慣れている演算子の優先順位をもたらす基本的なものは何もありません。これらのルールにより、式を評価し、この場合は最初に式を解析するための正しい順序を決定することもできます。各演算子の優先順位を定義するために、マップ(つまり、 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:

Rubyでのプログラミング言語の構築:パーサー

まとめ

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!


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

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

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

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