字句アナライザフレックスと構文アナライザレモンの予備使用

レクサーと文法アナライザーを自分で手書きするのは非常に面倒で、内部のロジックは非常に複雑でエラーが発生しやすくなります。Flexとlemonは、字句アナライザーと構文アナライザーの生成を支援するために使用されます。解析されたcコードを生成するには、少量のルールコードを記述するだけで済みます。今のところ実装の原則に注意を払わないでください。主にこれがどのように使用されるかを見てから、それに精通していて詳細なカスタマイズを実装したい場合は実装のソースコードを見てください。

1.字句アナライザー

まず、フレックスをインストールする必要があります。インストール方法については説明しません。字句解析プログラムの機能は、指定されたルールに従って文字列の文字列をトークンに分割することです。flexの場合、ルールを指定する.iファイルを記述してから、.cファイルを生成する必要があります。

例を挙げましょう。たとえば、次の.iファイルを使用して、〜A-> B-> Cの形式の文字列を解析し、文字列を〜、A、->、B、->、Cに分割します。

%{
    
    
    #include "token.h"
%}


%option reentrant
%option noyywrap


%%

[a-zA-Z]+    {
    
     return TK_SYM; }
~    {
    
     return TK_NEG; }
->    {
    
     return TK_IMPL; }

%%

%{および%}の内容は、そのままcコード(通常はヘッダーファイル)に移動されます。%optionは、レキシカルコンパイルを指定するためのいくつかのルールです。%optionリエントラントの定義は非常に重要です。この行なしで生成されるyy_flex関数は、パラメーターなしのint yylex(void)です。この行を使用すると、パラメーター付きのyy_flex関数が生成されます。 yylex(yyscan_t yyscanner)では、パラメーターのないものはトークン値のみを返すことができ、パラメーターのあるものはyyscannerから実際の値と行数およびその他の情報を取得できることがわかります。たとえば、パラメーターなしでAに解析する場合TK_SYMのみが返され、パラメーターを持つものは、トークンに対応する値Aを返すこともできます。

%%に含まれる範囲で、左側は解析されるルールであり、正規表現の形式で表されます。たとえば、[a-zA-Z] +は1つ以上の文字を意味し、右側の{}はルールです。解析されるc言語アクションが実行されます。ここでは、対応するトークンが返されます。返されない場合、yy_flex()関数は実行を継続します。

int main(int argc, char** argv)
{
    
    

  int token;
  yyscan_t scanner;
  FILE *fd;

  setbuf(stdout, NULL);
  yylex_init(&scanner);

  if (argc > 1)
  {
    
    
    if (!(fd = fopen(argv[1], "r")))
    {
    
    
      perror(argv[1]);
      return 1;
    }
    else
    {
    
    
      yyset_in(fd, scanner);
    }
  }

  token = yylex(scanner);
  while (token)
  {
    
    
    printf("tocken %d %s\n", token, yyget_text(scanner));
    token = yylex(scanner);
  }
  printf("end %d %s\n", token, yyget_text(scanner));
  yylex_destroy(scanner);

  return 0;
}

入力パラメータスキャナーは、使用前に初期化し、使用後に破棄する必要があります。プログラムにパラメーターがある場合、最初のパラメーターに対応するファイルが入力として使用され、yyset_in(fd、scanner);によって設定されます。yylex(scanner);を呼び出すたびに、トークン値が返されます。トークンには独自のマクロ定義が必要です。token.hヘッダーファイルは、ヘッダーファイルで定義されている前の.iファイルに含まれています。

#define TK_SYM 1
#define TK_NEG 2
#define TK_IMPL 3

トークン値は1から始まり、0に設定することはできません。これは、0がデフォルトの戻り値であり、パーサーも終了フラグとして0を使用するためです。解析されたトークン情報はスキャナー構造変数に格納され、yyget_text(scanner)は対応する文字列値です。

さらに、yylex()関数のロジックは、最初にファイル全体をスキャンする、つまりファイルの最後までスキャンしてから解析を開始することです。stdinからの入力フレックスは、次の入力ストリームに応じて自動的に調整されます。キャリッジリターンが表示された後に解析されるが、これは日食であるデバッグは役に立たないため、入力文字列とEnterキーが応答しないという現象が発生します。Enterキーをスキャンして分析する場合は、次のことができます。 yy_init_buffer関数でb-> yy_is_interactiveを手動で1に設定します

状態切り替えの構文もあり、役に立たないと感じますが、それについて話しましょう。以下のコードを見てください。

["]                     {
    
     BEGIN(DOUBLE_QUOTED); }
<DOUBLE_QUOTED>[^"]+    {
    
     }
<DOUBLE_QUOTED>["]      {
    
     BEGIN(INITIAL); return ARGUMENT; }
<DOUBLE_QUOTED><<EOF>>  {
    
     return -1; }

このコードは、最初の二重引用符を解析した後にDOUBLE_QUOTED状態に入ることを意味します。<DOUBLE_QUOTED>は、次のルールがDOUBLE_QUOTED状態でのみ解析され、<DOUBLE_QUOTED> ["]がに戻った後に2番目の二重引用符に解析されることを意味します。初期状態INITIAL、状態は最初にこの文%x DOUBLE_QUOTEDを介して状態名を定義する必要があります。INITIALはデフォルトで定義されており、ユーザー定義は必要ありません。

2.構文アナライザー

最も一般的に使用されるパーサーはバイソンで、ここで選択したのはレモンです。レモンは元々sqliteの構文解析用に開発されました。もちろん、他の構文解析にも使用できます。機能はバイソンの機能と似ています。しかし、レモンはよりコンパクトで文法的です。豊富なデバッグ情報を備えた、より親しみやすいものです。さらに重要なことに、誰かがレモンのソースコードに関する非常に詳細な本「LEMONSyntax Analysis Generator(LALR 1 Type)Source CodeScenarioAnalysis」を作成しました。 「」

レモンのソースコードは単なるlemon.cファイルであり、直接コンパイルするだけです。次に、レモンを使用した文法分析用の.cファイルを生成するために2つのファイル、つまり.yサフィックスが付いた文法ファイルとテンプレートファイルlempar.cが必要です。文法ファイルがtest.yの場合、コマンド./lemon test.yの後に、test.cの文法分析ファイルと状態遷移を説明するための.outファイルが生成されます。の使用法を見てみましょう。 api。以下は計算の分析です構文1+ 2

int main()
{
    
    
    void* pParser = ParseAlloc(malloc);
    
    ParseTrace(stdout, "");
    Parse(pParser, NUM, 1);
    Parse(pParser, PLUS, 0);
    Parse(pParser, NUM, 2);

    Parse(pParser, 0, 0);
    ParseFree(pParser, free);
}

ここで、NUMとPLUSはマクロ定義のトークンオブジェクトです。構文解析を開始する前に、構文解析オブジェクトを作成し、使用するメモリ割り当て関数を設定します。使用後は必ず破棄してください。ParseTrace(stdout、 "");この行によって出力される構文分析プロセス全体のデバッグ情報は、主にLRLA(1)分析のタイムシフトとプロトコルの詳細に関するものです。次に、トークンとそれに対応する関連情報を毎回解析のために解析機能に入力します。もちろん、トークンは字句解析器で生成できます。

.yサフィックスが付いたファイルである文法ファイル全体を見てみましょう。

%include {
    
    
#include <iostream>
#include "example1.h"
using namespace std;

}


%token_type {
    
     int }
%left PLUS MINUS.
%left DIVIDE TIMES.

program ::= expr(A). {
    
    
                            cout << "Result = " << A << "\n" << endl;
                            }

expr(A) ::= expr(B) MINUS expr(C). {
    
     A = B - C; }
expr(A) ::= expr(B) PLUS expr(C). {
    
     A = B + C; }
expr(A) ::= expr(B) TIMES expr(C). {
    
     A = B * C; }
expr(A) ::= expr(B) DIVIDE expr(C). {
    
     A = B / C; }

expr(A) ::= NUM(B). {
    
     A = B; }

%の先頭のパラメーターはレモンの特別な宣言を参照し、%includeはヘッダーファイルを参照します。コンテンツのこの部分はそのまま生成された.cファイルの先頭に移動されます。%token_typeは3番目のタイプを指定します。 Parseのパラメーター。通常はトークンです。情報パラメーターのタイプに対応します。各式の後の括弧の後には、解析中にオブジェクトが続きます。このオブジェクトは、渡される3番目のパラメーターです。もちろん、%extra_argumentを使用して4番目のパラメータータイプを指定することもできます。これにより、解析プロセス中に、より多くの情報を渡して解析できます。%leftは、操作が左結合であり、次の行で宣言された%left演算子の優先度が前の行よりも高いことを指定します。

次は文法宣言部分です。入力式が:: =の右側を満たしている場合、左側に縮小されます。{}の内容は、文法規則が縮小されたときに実行されるコードであり、その後の括弧はexprはトークンによってバインドされます指定されたオブジェクトは、Parse()関数の3番目のパラメーターによって入力されます。式は、終端記号と非終端記号に分けられます。終端記号は大文字で表され、使用前に定義する必要があるParse()の2番目のパラメーターによって入力されます。

#define PLUS                            1
#define MINUS                           2
#define DIVIDE                          3
#define TIMES                           4
#define NUM                             5

0は定義できません。これは、入力終了のデフォルトの記号です。非終端記号は、文法生成仕様から取得された小文字で表され、パーサー内でのみ使用されます。

解析プロセス全体について簡単に説明します。まず、NUM(1)と入力します。この時点では、文法スタックに記号はありません。プロトコルを作成する必要はありません。NUMをスタックに直接プッシュしてPLUSと入力するだけです。繰り返しますが、この時点で、スタックにはNUM個のシンボルがあります。現在の入力はターミネータではないため、文法expr(A):: = NUM​​(B)は最初にexpr(A)に縮小されます。プログラム:: = expr(A)を減らすことはできません。このルールは最初の行に配置されます。現在の文法モジュールが完全に解析され、exprをこれ以上指定できず、PLUSがスタックにプッシュされ、スタックにexprとPLUSがあることを示します。 。次に、NUM(2)と入力します。削減するものがない場合は、NUMをスタックに入れます。このとき、スタックにはexpr、PLUS、NUMがあります。最後に、ターミネータを入力し、最初にNUMをexprに減らし、次にスタックにexpr、PLUS、およびexprがあり、次にexpr(A):: = expr(B)PLUS expr(C)があります。文法は引き続き削減されます。入力がターミネーターであるため、exprに変換し、最後にexprをプログラムに縮小できます。この時点で、解析は完全に終了し、yy_accept関数が呼び出されます。

もちろん、プロセス全体をより完全に理解したい場合は、.outファイルの状態遷移図とParseTrace(stdout、 "");出力シフトおよびプロトコル情報を組み合わせる必要があります。以下はParseTraceのデバッグです。情報

Input NUM
Shift 8
Stack: NUM
Input PLUS
Reduce [expr ::= NUM].
Shift 5
Stack: expr
Shift 3
Stack: expr PLUS
Input NUM
Shift 8
Stack: expr PLUS NUM
Input $
Reduce [expr ::= NUM].
Shift 6
Stack: expr PLUS expr
Reduce [expr ::= expr PLUS expr].
Shift 5
Stack: expr
Reduce [program ::= expr].
Result = 3

Accept!
Popping $

追加のニーズを満たすために解析プロセスにいくつかの変更を加えたい、主に次の機能に焦点を当てる

yy_find_shift_action()//确认移进和规约的转移状态
yy_shift()//执行移进操作
yy_reduce()//执行规约操作
yy_find_reduce_action()//确认是否可以进一步规约

これらの機能については、「LEMON構文解析ジェネレータ(LALR 1タイプ)ソースコードシナリオ分析」の359ページで説明しています。もちろん、直接理解するのは簡単ではありません。レモンソースコードとの分析プロセスを組み合わせる必要があります。 LALR(1)。。

おすすめ

転載: blog.csdn.net/pfysw/article/details/99279076