以下は個人的な実験コードです、いくつかの省略があるはずです、交換と議論を歓迎しますwww~
参考資料:コンパイル原理の実験 2: LL(1) Syntax Analyzer - Chris-Zhang - 博客园
プロジェクトファイル: main.cpp、LLdefine.h、LLfun.cpp
プロジェクトのソース コードのリソースを参照してください: LLone.zip (シンプル LL(1) パーサー)-C/C++ ドキュメント リソース-CSDN ダウンロード
プログラム機能:ユーザーが文法を入力し、直接左再帰を除去(間接左再帰を除く)、FIRST集合とFOLLOW集合を計算し、解析テーブルを生成し、ユーザーが入力した文が文法に適合しているかどうかを判定する。
操作結果:
Please enter VN:E F T
Please enter the start VN:E
Please enter VT:+ * ( ) i
Please enter Grammar(empty:@,end with #):
E->E+T|T
T->T*F|F
F->(E)|i
#
*****check Left Recursion*****
direct Recursion is found in 'E->E+T'.Grammar has changed.
direct Recursion is found in 'T->T*F'.Grammar has changed.
*****First Set*****
E : ( i
F : ( i
T : ( i
E' : + @
T' : * @
+ : +
* : *
( : (
) : )
i : i
*****Follow Set*****
E : # )
F : # ) * +
T : # ) +
E' : # )
T' : # ) +
*****Analytical Table*****
+ * ( ) i #
E E->TE' E->TE'
F F->(E) F->i
T T->FT' T->FT'
E' E'->+TE' E'->@ E'->@
T' T'->@ T'->*FT' T'->@ T'->@
Please enter a sentence:98+99+90
*****Analyse the sentence*****
Analytical Stack Rest Sentence Grammar Used
#E i+i+i# E->TE'
#'ET i+i+i# T->FT'
#'E'TF i+i+i# F->i
#'E'Ti i+i+i#
#'E'T +i+i# T'->@
#'E +i+i# E'->+TE'
#'ET+ +i+i#
#'ET i+i# T->FT'
#'E'TF i+i# F->i
#'E'Ti i+i#
#'E'T +i# T'->@
#'E +i# E'->+TE'
#'ET+ +i#
#'ET i# T->FT'
#'E'TF i# F->i
#'E'Ti i#
#'E'T # T'->@
#'E # E'->@
# #
Success:The sentence belongs to the grammar.
1.データ構造の説明
LL(1) パーサーの構築は 5 つの部分に分割できます。最初の部分は、ユーザーが文法規則を入力して文法を生成するためのもので、2 番目の部分は左再帰を検出することであり (ここでは直接左再帰のみが検出されます)、直接左再帰が認識された場合は文法が変換されます。 3 番目の部分は FIRST セットと FOLLOW セットを解くこと、4 番目の部分は LL(1) 分析テーブルを構築すること、5 番目の部分はユーザーが文を入力し、分析スタックと残りの文字列スタックを構築し、分析することです判決の合法性。
そこで、終端記号、非終端記号、文法規則などのデータをカプセル化して格納するとともに、これらの機能を実現するメンバ関数を確立するクラスLLoneが確立される。同時に、文法ルールの保存を容易にするために、ルールの左部分と右部分を別々に保存する構造文法が確立され、これは後続のプログラム機能の実現に便利です。構造体とクラス定義は次のとおりです。
#pragma once
#include<iostream>
#include<set>
#include<string>
#include<map>
#include<vector>
#include<stack>
#include<iomanip>
using namespace std;
//文法结构体
struct grammar {
string left; //左部
vector<string> right; //右部
};
//LL(1)类
class LLone {
public:
vector<char>vt;//终结符
vector<string>vn;//非终结符
set<char>vt_s;
set<string>vn_s;
string start;
string usr_in;
vector<grammar>production;//产生式集合
map<string, set<char>>first;//first集
map<string, set<char>>follow;//follow集
set<string>toEmpty;//vn能否推出空
set<string>isFirst;//是否已求过first集
map<string, int>vnTnum;//vn在分析表中下标
map<char, int>vtTnum;//vt在分析表中下标
vector < vector<string>> table;//分析表
stack<string>ana; //分析栈
stack<char>sen; //余留输入串
void getInfo();//获取vt,vn,grammar
void checkLRE();//检测并消除左递归
void allFirst();//获取全部First集(外部接口)
int getFirst(string vn_f);//递归获取First集
void allFollow();//获取全部Follow集(外部接口)
void printFirst();//打印First集
void printFollow();//打印Follow集
void makeTable();//构建LL(1)分析表
void printTable();//打印分析表
void usrInput();//获取用户输入的句子
void analyse();//分析句子
void printStack();//打印栈的内容
};
2.アルゴリズム解析プロセス
(1) ユーザーが入力したいくつかの文法規則を識別し、文法を生成する
利用者が入力する文法規則は文字列であるため、プログラムでは終端や非終端などの文字を区別することができないため、利用者は規則を入力する前に文法中に出現する終端や非終端をすべて入力する必要がある。初期化では、FOLLOW セットを構築し、後でスタックを分析するときに開始記号を使用する必要があるため、ユーザーは文法で開始非終端記号を通知する必要もあります。上記の情報が収集された後、ユーザーは文法規則を再入力できます。空の記号列εは入力しにくいため、プログラム内で '@' が使用されます。プログラムがルールを認識すると、ルールが左右に分割され、左側が文字列に格納され、文字列中の「->」を削除した後、右側が認識され、「|」が認識されます。分割として、複数の右側の部分が Vector<string> に追加されます。また、ルールの数が不明なため、ルールの入力は「#」で終わります。
(2) 直接左再帰を検出して変換する
直接左再帰を伴う文法規則は A::=Aa|b の形式であり、左再帰は新しい非終端記号 A' を導入することで削除できます。A' を導入すると、A::=Aa|b は次のように等価に書くことができます。
A::=bA'
A'::=aA'| ε
これにより、直接の左再帰が排除されます。したがって、vector<grammar>production に保存されているすべての文法規則を調べて、右側の部分の最初の文字が左側の部分と等しい場合、それは直接左再帰が見つかったことを意味します。次に、左側の部分をベースに「\」を追加して新しい非終端を形成し(他の非終端との重複を防ぐため、この方法が使用されます)、上記の規則に従って文法を書き直します。Aa の形をした A の右側を aA' に書き換えます | A' の右側にεを追加し、A の右側から削除します。次に、b のような形をしている A の右側の部分をすべて bA' に書き換えて、左再帰の削除を完了します。
(3) FIRST 集合と FOLLOW 集合を解く
<i>最初の集合を解く
直接左再帰を排除することに基づいて、文法の最初のセットを見つけます。文法内の各文法記号X∈( V N ∪ V T)について、FIRST(X) を構築するときは、FIRST セットが展開されなくなるまで次の規則を継続的に使用します。
• X∈ VTの場合、FIRST(X)={X}。
· X ∈ VNの場合、 FIRST(X) に a または ε を追加するX::=a α (a ∈ V T ) または X::= εの形式の規則があります。
·文法 G に X::Y1Y2…Yk という形式のルールがあると仮定します。Y1 ∈ V Nの場合、 FIRST(Y1) 内のすべての非ε記号を FIRST(X) に追加します (すべて 2<=i)。 <=k の場合、 Y1 ⇒ *εの場合、 Y2 に設定された最初のシンボル ( εを除く) を FIRST(X) に追加し、以下同様に Yk-1 ⇒ *εになるまで続き、その後 Yk に設定された最初のシンボル ( εを除く) を追加します。外側) も FIRST(X) に追加されます。
· Y1Y2...Yk の各非終端記号が空の記号文字列を推定できる場合、つまり Y1Y2...Yk ⇒ *ε、FIRST(X) にεを追加します。
したがって、FIRST セットを解くプロセスは 2 つの部分に分割されます。最初の部分は非終端記号の FIRST セットを見つけることであり、2 番目の部分は非終端記号の最初のセットを見つけることです。
最初の端子記号のセットを見つけます。ソリューション内のルール走査の順序は不確実であるため、FIRST セットの整合性は保証できません。そのため、各非終端に対して完全な FIRST セットが確実に取得されるように、FIRST セットを見つける回数は 2 に設定されます。シンボル。また、空の記号文字列εの場合、FIRST( ε ) = εとなります。
<ii> FOLLOW 集合を解く
文法内の非終端記号 A ごとに、FOLLOW(A) を構築するために、各 FOLLOW セットが展開されなくなるまで、次のルールを繰り返し適用できます。
•文法の開始記号 S を # ∈ FOLLOW(S) とする。
·文法に A::= α B βの形式のルールがあり、β≠εの場合、 FIRST( β )内のすべての非ε記号をFOLLOW(B) に追加します。
·文法に A::= α B または A:: A::= α B βの形式のルールがあり、β ⇒ *εの場合、FOLLOW(A) 内のすべての端子は FOLLOW(B )、ここでα はεの場合があります。
したがって、FIRST 集合を解く過程と同様に、FOLLOW 集合を解く過程も上記の 3 つの部分に分かれます。最終的に FOLLOW セットの完全性を取得するために、FOLLOW セットを取得するための全体のサイクル数は 2 に設定されます。
(4) LL(1)解析テーブルの構築
FIRST セットと FOLLOW セットを計算した後、文法 G のルール A::= αごとに、テーブル内の各要素は次のアルゴリズムに従って決定できます (分析テーブルが M であると仮定します)。
・FIRST( α )の各端子 aについて、M[A,a]=“A-> α ”と設定します。
· εϵ FIRST( α ) の場合、 FOLLOW(A ) に属する各シンボル b (b は終端記号または #) に対して M[A,b]=“A-> α ” を設定します。
•上記 2 つのルールで定義できないMの要素をすべてエラーとして設定します。
したがって、上記3つの規則に従って、行数が非終端記号の数、列数が終端記号の数+1(+#)となる解析表Mを構築することができる。
(5) ユーザー入力文を解析
まずユーザーが入力した文字列を読み取り、算術文法の場合、ユーザーが入力した文字列に数字が含まれている場合は、整数を終端記号「i」に置き換えます。たとえば、ユーザーが 98+99+80 を入力すると、プログラムはそれを i+i+i として処理します。
次に、分析スタックと残りの文字列スタックの 2 つのスタックを構築します。2 つのスタックを初期化します。分析スタックの場合は、# と開始記号 E をそれぞれスタックにプッシュします。残りの文字列スタックの場合は、最初に # をプッシュし、次に処理されたユーザー入力文字列を逆の順序でスタックにプッシュします。次に、2 つのスタックのスタック トップを読み取り、LL(1) 分析テーブルを確認し、文法ルールが見つかった場合は、分析スタックのスタック トップを取り出し、文法ルールの右側の部分を分析スタックにプッシュします。逆の順序。ルールが見つからない場合は、入力文が文法に属さないことを示すエラーが報告されて終了します。上記のプロセスを繰り返します。2 つのスタックの先頭の要素が同じである場合、それらは削除され、次のサイクルが実行されます。スタックの 2 つの先頭の要素が両方とも終端シンボルであり、等しくない場合は、エラーが発生します。通報されて退出する。ループ終了後に 2 つのスタックの要素数が 0 であれば、文が文法に受け入れられ、マッチングが成功したことを意味します。