編譯原理-詞法分析

詞法分析

對源程序進行掃描產生單詞符號,將源程序改造爲單詞符號串的中間程序,即輸入源程序、輸出單詞符號。詞法分析器(Lexical Analyzer)包括掃描器(Scanner)與執行詞法分析的程序

單詞符號是一個程序語言的基本語法符號。稱作 token(記號) ,是具有獨立意義的最小語法單位。將字符組合成記號與在一個英語句子中將字母構成單詞並確定單詞的含義很相像,此時的任務很像拼寫。

程序語言的單詞符號一般分爲:

  • 關鍵字:保留字
  • 標識符:變量名、過程名等
  • 常數:數字、字符串、布爾型等
  • 運算符:±/*等
  • 界符:逗號,分號,// 等

單詞符號常常表示成二元式:(單詞種別,單詞符號的屬性值)。單詞種別是語法分析需要的信息,通常用整數編碼。一個語言的單詞符號如何分種,分成幾種,怎樣編碼,是一個技術性的問題。它主要取決於處理上方便。

  • 標識符一般統歸爲一種。
  • 常數則按類型分種。
  • 關鍵字可將其全體視爲一種,也可以一字一種。採用一字一種的分法實際處理起來較爲方便。
  • 運算符可採用一符一種的分法,但也可以把具有一定共性的運算符視爲一種。
  • 界符一般用一符一種的分法。

種別通常定義爲枚舉類型的邏輯項。

typedef enum {
   IF,ELSE,PLUS,NUM,ID,……
} TokenType;

單詞符號的屬性值是指單詞符號的特性或特徵,可以是標識符在符號表的入口地址、數值的二進制值等。

如果是一符一種的分法(如關鍵字,運算符等),詞法分析器只給出其種別編碼,不給出其屬性值。
如果一個種別含有多個單詞符號,那麼對於它的每個單詞符號,除了給出種別編碼,還應給出屬性值,以便把同一種類的單詞區別開來。標識符屬性值是自身的符號串;也可是在符號表的入口地址。常數自身值是常數的二進制數值。

掃描器必須計算每一個記號的若干屬性,所以將所有的屬性收集到一個單獨構造的數據類型中是很有用的,這種數據類型稱作記號記錄(token record)。

typedef struct {
    TokenType tokenval; 
    char* stringval;
    int numval;
} TokenRecord;

或作爲一個聯合

typedef struct {
    TokenType tokenval; 
    unon { char* stringval;
          int numval; 
         } attribute;
} TokenRecord;

簡而言之,掃描源程序,按照(單詞種別,單詞符號的屬性值)這樣的二元形式輸出單詞符號串

【例】試給出程序段 if (a>1) b = 100;輸出的單詞符號串。
假定基本字、運算符和界符都是一符一種,標識符自身的值是字符串,常數是二進制值。
(2,)	基本字 if
(29,)	左括號 (
(10,‘a’)	標識符 a
(23,)	大於號 >
(11,‘1’的二進制)	常數 1
(30,)	右括號 )
(10,‘b’)	標識符 b
(17,)	賦值號 =
(11,‘100’的二進制)	常數 100
(26,)	分號 ;

另一種表示可以爲:

【例】考慮下述 C++ 代碼段:while ( i >= j ) i--;	
假定基本字、運算符和界符都是一符一種,標識符自身的值是符號表的入口地址,常數是二進制值。
經詞法分析器處理後,轉換爲如下的單詞符號序列:
	( while ,- )
    ( (	   ,- )
    ( id    ,指向i的符號表表項的指針 )
    ( >=    ,- )
    ( id    ,指向j的符號表表項的指針 )
    ( )     ,- )
    ( id    ,指向i的符號表表項的指針 )
    ( --    ,- )
    ( ;    ,- )

詞法分析作爲一個獨立的階段(一遍),把源程序的字符序列翻譯成單詞符號序列存放於文件中,待語法分析程序工作時再從文件輸入這些單詞符號進行分析。結構更簡單、清晰和條理化。有利於集中考慮詞法分析一些細節問題。
詞法分析作爲一個子程序,每當語法分析器需要一個單詞符號時就調用這個詞法分析子程序。每一次調用,詞法分析器就從輸入字符串中識別出一個單詞符號。

image.png

通常,構造詞法分析程序有兩種方法:

  • 手工方式:根據識別語言單詞的狀態轉換圖,使用某種高級語言。例如:用 C 語言直接編寫詞法分析程序
  • 自動方式:利用詞法分析程序的自動生成工具 LEX 自動生成詞法分析程序。

詞法分析手動設計

image.png

1.輸入緩衝區、預處理

詞法分析器工作的第一步是輸入源程序文本到輸入緩衝區。
預處理工作:是將輸入的源程序中的多餘的空白符、跳格符、回車符、換行符等編輯性字符以及註釋部分剔除掉,並將結果存入掃描緩衝區,方便單詞符號的識別。

2.掃描緩衝區

  • 爲了保證單詞符號不被掃描緩衝區邊界打斷,掃描緩衝區一般設計爲如下一分爲二的區域;
  • 每次輸入更新其一半空間的內容,使得詞法分析器在最壞情況下識別單詞符號的長度是掃描緩衝區長度的一半。因此也稱配對緩衝區。
  • 兩個指示器
    • 起點指示器:新單詞的首字符;
    • 搜索指示器:用於向前搜索以尋找單詞的終點;
  • 如果搜索指示器從單詞起點出發搜索到半區的邊緣,但未到單詞的終點,則調用預處理程序,把後續的字符串裝進另半區
image.png

單詞符號的識別:超前搜索:源程序中的單詞符號構成沒有特殊的結尾,單詞符號與單詞符號之間在不引起可讀性理解錯誤時也可以不必有空格作間隔,因此有時當單詞符號的所有字符都已處理後,特別是當有單詞符號是另一個單詞符號的前綴子串時,詞法分析器不能確定當前單詞識別是否已結束,需要再超前搜索若干個字符後才能確定,返回識別出的單詞,如果這時有多讀進的字符,則需要回退處理。
例如 C 語言中的單詞符號“>”、“>=”的識別就需要超前搜索。

不使用超前搜索的幾種情況

  • 規定所有基本字都是保留字;用戶不能用它們作自己的標識符;基本字作爲特殊的標識符來處理,使用保留字表;
  • 如果基本字、標識符和常數(或標號)之間沒有確定的運算符或界符作間隔,則必須使用一個空白符作間隔

狀態轉換圖:狀態轉換圖可用於識別(或接受)一定的字符串。大多數程序語言的單詞符號都可以用轉換圖予以識別。

狀態轉換圖是一張有限方向圖,結點代表狀態,用圓圈表示,狀態之間用箭弧連結,箭弧上的標記(字符)代表射出結狀態下可能出現的輸入字符或字符類,一張轉換圖只包含有限個狀態,其中有一個爲初態,至少要有一個終態

狀態轉換圖可用於識別(或接受)一定的字符串,若存在一條從初態到某一終態的道路,且這條路上所有弧上的標記符連接成的字等於 α,則稱α被該狀態轉換圖所識別(接受)

image.png image.png

【示例】

image.png

設計時假定:

  • 基本字:所有基本字都是保留字,用戶不得使用它們作爲自己定義的標識符;
  • 基本字作爲一類特殊的標識符來處理,不再專設對應的轉換圖。但需要把關鍵字預先安排在一個表格,此表叫關鍵字表。當識別出一個標識符時,就去查關鍵字表,以確定它是否爲一關鍵字。
  • 若關鍵字、標識符和常數之間沒有確定的運算符或界限府作間隔,則必須至少用一個空白符作間隔。

狀態轉換圖實現時可以讓每個狀態結點對應一小段程序。

分支節點:

image.png

循環狀態節點

image.png

終態節點

image.png

下面使用僞碼來簡單實現詞法分析器

全局變量與過程
ch 字符變量,存放最新讀入的源程序字符
strToken 字符數組,存放構成單詞符號的字符串
GetChar 子程序過程,把下一個字符讀入到 ch 中
GetBC 子程序過程,跳過空白符,直至 ch 中讀入一非空白符
Concat 子程序,把ch中的字符連接到 strToken 
IsLetter和 IsDisgital 布爾函數,判斷ch中字符是否爲字母和數字
Reserve 整型函數,對於 strToken 中的字符串查找保留字表,若它是保留字則給出它的編碼,否則回送0
Retract 子程序,把搜索指針回調一個字符位置
InsertId  整型函數,將strToken中的標識符插入符號表,返回符號表指針
InsertConst  整型函數,將strToken中的常數插入常數表,返回常數表指針
int code, value;
strToken := “ ”;	/*置strToken爲空串*/
GetChar();GetBC();
if (IsLetter())
begin
	while (IsLetter() or IsDigit())
	begin
		Concat(); GetChar(); 
	end
	Retract();
	code := Reserve();
	if (code = 0)
	begin
		value := InsertId(strToken);
		return ($ID, value);
	end
	else
		return (code, -);	
end
else if (IsDigit())
begin
	while (IsDigit())
	begin
		Concat( ); GetChar( );
	end
	Retract();
	value := InsertConst(strToken);
	return($INT, value);
end
else if (ch =‘=’) return ($ASSIGN, -);
else if (ch =‘+’) return ($PLUS, -);
else if (ch =‘*’)
begin
	GetChar();
	if (ch =‘*’) return ($POWER, -);
	Retract(); return ($STAR, -);
end
else if (ch =‘,’) return ($COMMA, -);
else if (ch =‘(’) return ($LPAR, -);
else if (ch =‘)’) return ($RPAR, -);
else ProcError( );		/* 錯誤處理*/
curState = 初態
GetChar();
while( stateTrans[curState][ch]有定義){
   //存在後繼狀態,讀入、拼接
   Concat();
   //轉換入下一狀態,讀入下一字符
   curState= stateTrans[curState][ch];
   if curState是終態 then 返回strToken中的單詞
   GetChar( ); 
}

正則式

正則式:定義語言單詞符號的一種方式(或者叫正規式)

【例】定義標識符的正則式
字母(字母 | 數字)*

正則式和正則集的遞歸定義:

  • ε 和 Φ 都是 ∑ 上的正則式,它們所表示的正則集分別爲 {ε} 和 Φ;
  • 任何 a∈∑, a 是 ∑ 上的一個正則式,它所表示的正則集爲 {a};
  • 假定 e1 和 e2 都是 ∑ 上的正則式,它們所表示的正則集分別記爲 L(e1)和 L(e2),則:
    e1|e2 是正則式,表示的正則集爲 L(e1)∪L(e2)(並集)
    e1e2 是正則式,表示的正則集爲 L(e1)L(e2) (連接集)
    (e1)* 是正則式,表示的正規集爲 (L(e1))*(閉包)
    優先級爲閉包、連接積、或。

正則集可以用正則式表示,正則式是表示正則集一種方法,一個字符串集合是正則集當且僅當它能用正則式表示

正則式表示字符串的格式,正則式 r 完全由它所匹配的串集來定義。這個集合爲由正則式生成的語言(language generated by the regular expression),寫作 L®,每個正則式可以看作是一個匹配模式。

【例】設∑={a,b,c},則aa*bb*cc* 是∑上的一個正則式
aa*bb*cc* 是∑上的一個正則式,它所表示的正則集是
L = {abc,aabc,abbc,abcc,aaabc,…}
  = {a^m b^n c^l | m,n,l ≥1}
【例】設程序語言字母表是鍵盤字符集合,則程序語言單詞符號可用如下正則式定義
關鍵字	if | else | while | do
標識符	l (l | d)*
所整常數	dd*
關係運算符	< | <= | > | >= | <>
	其中 l 代表 a~z 中任一英文字母
    其中 d 代表 0~9 中任一數字

正則式等價

若兩個正則式所表示的正則集相同,則認爲二者等價。兩個等價的正則式 R1 和 R2 記爲 R1=R2。

【例】
	(a|b)* = (a*|b*)*
	b(ab)* = (ba)* b

正則式擴展

正則式 r 的一個或多個重複,寫作 r+
. 表示與任意字符匹配。
用方括號和一個連字符表示字符的範圍,如[0-9],[a-z],[a-zA-Z]。這種表示法還可以用作表示單個的解,如a|b|c 可寫成[abc]

不在給定集合中的任意字符用 ”~”,如正規式 ~a 表示字母表中非 a 字符
可選的子表達式r?表示由 r 匹配的串是可選的(0個或1個)。如natural = [0-9]+,signedNatural = (+|-)? Natural

正則文法與正則式

正則文法與正則式都是描述正規集的工具。

對任意一個正則文法,存在定義同一語言的正則式;反之,對每個正則式存在一個生成同一語言的正則文法。

正則文法到正則式的轉換

將正則文法中的每個非終結符表示成關於它的正則式方程,獲得一個聯立方程組。依照求解規則:

若 x = αx |β(或x = αx+β),則解爲x = α*β

若 x = xα | β(或x = xα+β),則解爲x = βα*

以及正規式的分配率、交換率和結合率求關於文法開始符號的正規式方程組的解。
這個解是關於文法開始符號 S 的一個正則式。上述兩個規則較爲重要,將遞歸的x消爲α的閉包

【例1】
Z0AA0A0BB1Aϵ 【例】設有正規文法G:\\\\ Z→0A\\\\ A→0A|0B\\\\ B→1A|\epsilon \\\\試給出該文法生成語言的正規式

首先給出相應的正規式方程組(+代替|)
Z = 0A ………(1)
A = 0A+0B ………(2)
B = 1A+$\epsilon $ ………(3)

將(3)代入(2)式中的 B 得
A = 0A+01A+0 ………(4)
對(4)利用分配率 A = (0+01)A+0 ………(5)
對(5)使用規則得 A = (0+01)*0 ………(6)
將(6)代入(1)得 Z = 0(0+01)*0
即正規文法G[Z]所生成語言的正規式是 0(0|01)*0

【例2】

設有正規文法G:
A→aB|bB
B→aC|a|b
C→aB
試給出該文法生成語言的正規式
同上述步驟
首先給出相應的正規式方程組(+代替|)
A = aB+bB ………(1)
B = aC+a+b ………(2)
C = aB ………(3)
將(3)代入(2)式中得
B = aaB+a+b ………(4)
對(4)使用規則得 B = (aa)*(a+b) ………(5)
將(5)代入(1)得 A = (a+b)(aa)*(a+b)
即正規文法G[Z]所生成語言的正規式是 (a|b)(aa)*(a|b)

【例3】

設有正規文法G:
	Z→U0|V1
	U→Z1|1
	V→Z0|0	試給出該文法生成語言的正規式
首先給出相應的正規式方程組(+代替|)
			Z = U0+V1			………(1)
			U = Z1+1 			………(2)
			V = Z0+0			………(3)
將(2)(3)代入(1)式得
			Z = Z10+10+Z01+01	………(4)
			Z = Z(10+01)+10+01	………(4)
對(4)使用規則得Z = (10+01)(10+01)*
即正規文法G[Z]所生成語言的正規式是 (10|01)(10|01)*

【例4】

已知描述“標識符”單詞符號的正規文法
	<標識符> →l | <標識符>l | <標識符>d
首先給出相應的正規式方程組(+代替|)
	S = l+Sl+Sd
	S = l+S(l+d)
使用規則得
	S = l(l+d)*
該文法的正規式是 
	l(l|d)*

正規式到正規文法的轉換

字母表∑上的正規式到正規文法 G=(VNV_N,VTV_T,P,S)的轉換方法如下:

  1. 令 VT = ∑
  2. 對任意正規式 R 選擇一個非終結符 Z,生成規則 Z→R,並令 S=Z;
  3. 若 a 和 b 都是正規式,對形如 A→ab 的規則轉換成 A→aB 和 B→b兩規則,其中 B 是新增的非終結符;
  4. 在已轉換的文法中,將形如 A→a*b 的規則進一步轉換成 A →aA | b;
  5. 不斷利用規則(3)和(4)進行轉換,直到每條規則最多含有一個終結符爲止。

【例1】

將R = (a|b)(aa)*(a|b) 轉換成相應的正規文法
令 A 是文法開始符號,根據規則(2)變換爲
A → (a|b)(aa)*(a|b)
根據規則(3)變換爲
A → (a|b)B
B → (aa)*(a|b)
根據規則(4)變換爲(逆推,將*轉變爲|)
A → aB|bB
B → aaB|a|b(aaB中有兩個a,要簡化到只有一個終結符爲止)
根據規則(3)變換爲
A → aB|bB
B → aC|a|b
C → aB

【例2】

將描述標識符的正規式R=l(l|d)*轉換成相應的正規文法
令 S 是文法開始符號,根據規則(2)變換爲
S→l(l|d)*
根據規則(3)變換爲
S→lA 
A→(l|d)*
根據規則(4)變換爲
S→lA
A→(l|d)A |ε
進一步變換爲
S→lA
A→lA|dA|ε(消除ε)
進一步變換爲
S→l|lA
A→l|d|lA|dA

有窮自動機

有窮自動機是具有離散輸入與輸出系統的一種抽象數學模型。有窮自動機有“確定的”和“非確定的”兩類。確定的有窮自動機 和 非確定的有窮自動機都能準確地識別正規集。

確定有窮自動機(DFA)

一個確定有窮自動機 DFA M 是一個五元式:M=( Q, ∑, f, S, Z) 其中:
Q:有限狀態集,它的每個元素稱爲一個狀態。
∑:有窮字母表,它的每個元素稱爲一個輸入字符。
F:狀態轉換函數,是從 Q×∑ 至 Q 的單值映射。f(qi, a) = qj (qi,qj ∈ Q,a ∈ ∑)表示:當現行狀態爲 qi、輸入字符爲 a 時,自動機將轉換到下一狀態 qj 。稱 qj 爲 qi 的一個後繼。
S∈Q:是唯一的初態。
Z $\subset $ Q:終態集(可空)。

【例】設DFA  M=({q0,q1,q2},{a,b},f,q0,{q2})
其中:
f(q0,a)= q1
f(q1,b)= q1
f(q0,b)= q2
f(q2,a)= q2
f(q1,a)= q1
f(q2,b)= q1

狀態轉換矩陣、狀態轉換圖

一個 DFA 可用一個矩陣表示,該矩陣的行表示狀態,列表示輸入字符,矩陣元素表示 f(s, a) 的值。這個矩陣稱爲狀態轉換距陣,或稱轉換表。
一個 DFA 也可以用一張(確定的)狀態轉換圖表示,假定 DFA M 含有 m 個狀態 n 個輸入字符,這個狀態轉換圖則有 m 個結點,每個結點最多有 n 條箭弧射出和別的狀態相連,同一結點射出的每條箭弧用 ∑ 中的一個不同的輸入字符作標記,整張圖含有唯一一個初態結點和若干個(可以是0個)終態結點。

image.png image.png

DFA M 識別的符號串:對於 ∑* 中的任何字 β,若存在一條從初態到某一終態結點的通路,且這條通路上的所有弧的標記符連接成的字等於 β ,則稱β 可爲 DFA M 所識別。若 M 的初態結同時又是終態結,則ε可爲 M 所識別。

DFA M 所能識別的符號串的全體爲其接受的語言,記爲L(M)。

結論:V $ \subset $ ∑* 是正規的當且僅當存在 ∑ 上的自動機 M,使得 V=L(M)

模擬 DFA 的算法

輸入:輸入以文件結束符 eof 結尾的串 x;一個 DFA D,其開始狀態爲 s0,接受狀態集合爲 F。
輸出:如果 D 接受 x,則回答 “yes”,否則回答“No”。
方法:把下列算法應用於輸入字符串x。函數 move(s,c) 給出在狀態 s 上遇到輸入字符 c 時應該轉換到的下一個狀態。函數 getch() 返回輸入串 x 的下一個字符。

s=s0;
while ((c=getch())!=eof) {
	s=move(s,c);
	if (s is in F) return “yes”;
}

非確定有限自動機(NFA)

一個非確定有限自動機 M 是一個五元式:M=(Q,∑,S,Z,F),其中:

Q:有限狀態集
∑:有窮字母表
F:狀態轉換函數,是一個從 Q×∑* 至 S 的子集 S’ 的映射(多值映射)。即
f: Q×∑* →2Q 冪集
S $\subset $ Q:非空初態集
Z $\subset $ Q:終態集(可空)

一個 NFA 也可用一個矩陣表示,該矩陣的行表示狀態,列表示輸入字符,矩陣元素表示 f(s, a) 的值(狀態集)。一個 NFA 也可以用一張狀態轉換圖表示。

NFA 和DFA的區別

NFA可以有多個初態;
弧上的標記可以是∑*中的一個字(甚至可以是一個正規式),而不一定是單個字符;
同一個字可能出現在同狀態射出的多條弧上;
DFA是NFA的特例。

image.png

NFA M 識別的符號串,對於 ∑* 中的任何字 β,若存在一條從初態到某一終態結點的通路,且這條通路上的所有弧的標記符連接成的字(忽略ε弧)等於 β,則稱β 可爲 NFA M 所識別。若 M 的某些狀態既是初態又是終態則空字 ε 被 M 所接受。

NFA M 所能識別的符號串的全體爲其接受的語言,記爲L(M),如上例中NFA M’ 所識別的語言爲L(M’) = b*(b|ab)(bb)*

由 NFA 的定義可知,同一個符號串 β 可由多條路來識別,DFA 是 NFA的特例,利用有窮自動機構造詞法分析程序的方法是:

  1. 從語言單詞的描述中構造出 NFA;
  2. 將 NFA 轉化爲 DFA;
  3. 化簡爲狀態最少化的 DFA;
  4. 對 DFA 的每一個狀態構造一個程序段將其轉化爲識別單詞的詞法分析程序。

NFA 確定化爲 DFA 的方法

NFA 的確定化是指對任給的 NFA,都能相應地構造一DFA,使它們接受相同的語言。

對於一個 NFA,由於狀態轉換函數 f 是一個多值函數,因此總存在一些狀態 q,對於它們有

f(q,a)={q1, q2,…,qn}

它是 NFA 狀態集合的一個子集,爲了將 NFA 轉換爲 DFA,把狀態集合{q1, q2,…,qn}看做一個狀態 A,也就是說,從 NFA 構造 DFA 的基本思想是 DFA 的每一個狀態代表 NFA 狀態集合的某個子集,這個 DFA 使用它的狀態去記錄在 NFA 讀入輸入符號之後可能到達的所有狀態的集合,稱此構造方法爲子集法。

狀態集合 I 的 ε-閉包

設 I 是 NFA N 的一個狀態子集,ε-CLOSURE(I)定義如下:

若 s ∈ I,則 s ∈ ε-CLOSURE(I)
若 s ∈ I,那麼從 s 出發經過任意條 ε 弧而能到達的任何狀態 s’,都屬於 ε-CLOSURE(I)

ε-CLOSURE(I)是一個從給定結點集合出發在轉換圖上搜索可達結點集合的過程。

將 I 中所有的狀態壓入棧 stack 中;
將 ε-CLOSURE(I)初始化爲 I;
while 棧stack不空 do
begin
	將棧頂元素 t 彈出棧;
	for 每個這樣的狀態u:從t到u有一條標記爲ε的邊 do
		if u 不在 ε-CLOSURE(I) 中 do
		begin
		將 u 添加到 ε-CLOSURE(I);
		將 u 壓入棧 stack 中
		end 
end
image.png

從 NFA N=(Q,∑,F,S,Z)構造等價的DFA M=(Q’,∑,F’,S’,Z’)的方法

首先將從初態 S 出發經過任意條 ε 弧所能到達的狀態所組成的集合作爲 M 的初態 S’,然後從 S’ 出發,經過對輸入符號 a∈∑ 的狀態轉移所能到達的狀態(包括讀輸入符號之前或之後所有可能的 ε 轉移所能到達的狀態)所組成的集合作爲 M 的新狀態,如此重複,直到不再有新的狀態出現爲止。

置 DFA M 中的狀態集合 Q’和 Z’爲 ∅ 集。
給出 M 的初態 S’= ε-CLOSURE(S),並把 S’ 置爲未標記狀態後加入到 Q’中。
初始時,ε-CLOSURE(S)是Q’中唯一的狀態且未被標記;
while Q’中存在一個未標記的狀態 T do
begin
標記 T;
  for 每個輸入符號 a do
  begin 
  	U = ε-CLOSURE( f(T,a) );
    if  U 沒有在 Q’中 then
    將 U 作爲一個未標記的狀態添加到 Q’中;
    f’(T,a) = U;
    end
end

【例】NFA確定化

image.png image.png image.png image.png image.png image.png image.png image.png

有窮自動機與文法的相互轉化

右線性正規文法到有窮自動機的轉換方法

image.png image.png

左線性正規文法到有窮自動機的轉換方法

image.png image.png

有窮自動機到正規文法的轉換方法

image.png

有窮自動機與正則表達式的相互轉化

由正則表達式構造NFA

輸入:字母表上的正規式 R

輸出:識別語言 L® 的 NFA N

image.png
image.png
image.png

整個分裂過程中,所有新結點均採用不同的名字,保留 X,Y 爲全圖唯一初態結點和終態結點。

image.png

有窮自動機到正規式的轉換

逆推過程,增加新初態 X,與所有原初態用ε相連,增加新終態 Y,與所有原終態用ε相連,從而構成一個新的NFA M’,它只有一個初態 X 和一個終態 Y。在X 與 Y 之間進行弧合併。

圖片.png 圖片.png 圖片.png 圖片.png

實際設計

詞法分析的任務就是掃描源文件,按照(單詞符號(token),單詞符號屬性值)這樣的二元形式輸出單詞符號串

詞法分析中要使用正則表達式來掃描整個文件以判別單詞符號的種類,單詞符號按照種類進行劃分,例如

TOKEN: {
<VOID : "void">
| <CHAR : "char">
| <SHORT : "short">
| <INT : "int">
| <LONG : "long">
| <STRUCT : "struct">
| <UNION : "union">
| <ENUM : "enum">
| <STATIC : "static">
| <EXTERN : "extern">
| <CONST : "const">
| <SIGNED : "signed">
| <UNSIGNED : "unsigned">
| <IF : "if">
| <ELSE : "else">
| <SWITCH : "switch">
| <CASE : "case">
| <DEFAULT_ : "default">
| <WHILE : "while">
| <DO : "do">
| <FOR : "for">
| <RETURN : "return">
| <BREAK : "break">
| <CONTINUE : "continue">
| <GOTO : "goto">
| <TYPEDEF : "typedef">
| <IMPORT : "import">
| <SIZEOF : "sizeof">
}

上述Token描述了關鍵字規則

TOKEN: {
<IDENTIFIER: ["a"-"z", "A"-"Z", "_"](["a"-"z", "A"-"Z", "_", "0"-"9"])*>
}

上述Token描述了標識符規則

正則表達式會使用最長前綴匹配規則,如遇到了voidFunction 會匹配voidFunction(標識符)而不是void (保留字)Function。

同理,描述數值規則可使用(匹配10,16,8進制數值)

TOKEN: {
<INTEGER: ["1"-"9"] (["0"-"9"])* ("U")? ("L")?
| "0" ["x", "X"] (["0"-"9", "a"-"f", "A"-"F"])+ ("U")? ("L")?
| "0" (["0"-"7"])* ("U")? ("L")?
>
}

對於空白符或者註釋來說要跳過,因此不使用TOKEN來描述,使用SPECIAL_TOKEN來描述空白符

SPECIAL_TOKEN: { <SPACES: ([" ", "\t", "\n", "\r", "\f"])+> }

[" “, “\t”, “\n”, “\r”, “\f”] 表示 " “(空格)、”\t”(製表符)、"\n"(換行符)、"\r"(回車)、"\f"(換頁符)之中的任意一個,後面加上“+”表示上述 5 種字符之一1 個或多個排列而成的字符串。

描述行註釋

SPECIAL_TOKEN: {
<LINE_COMMENT: "//" (~["\n", "\r"])* ("\n" | "\r\n" | "\r")?>
}

上述代碼所描述的模式是以“//”開始,接着是換行符以外的字符,並以換行符結尾的字符串。簡單來說,這裏描述的是從“//”開始到換行符爲止的字符串。文件的最後可能沒有換行符,因此換行符是可以省略的。

描述塊註釋
首先要注意的是下列模式是無法正確地掃描塊註釋的。

SKIP { <"/*" (~[])* "*/"> }

按照最長匹配原則,可能會把代碼也當做註釋匹配進去,如

/* 本應只有這一行是註釋…… */
int
main(int argc, char **argv)
{
printf("Hello, World!\n");
return 0; 
}/* 以狀態 0 結束 */

如果這樣寫,那麼直到註釋的終結符爲止都和模式“(~[])*”匹配,
爲了解決這個問題,需要進行如下修改,也就是進行狀態轉移

MORE: { <"/*"> : IN_BLOCK_COMMENT }
<IN_BLOCK_COMMENT> MORE: { <~[]> }
<IN_BLOCK_COMMENT> SKIP: { <"*/"> : DEFAULT }

上述例子中的 IN_BLOCK_COMMENT 是掃描的狀態(state)。通過使用狀態,可以實現只掃描代碼的一部分。
讓我們來講解一下狀態的使用方法。首先再看一下上述例子中的第 1 行。

SKIP: { <"/*"> : IN_BLOCK_COMMENT }

這樣在規則定義中寫下 { 模式:狀態名 } 的話,就表示匹配模式後會遷移(transit)到對應的狀態。上述例子中會遷移到名爲 IN_BLOCK_COMMENT 的狀態。
掃描器在遷移到某個狀態後只會運行該狀態專用的詞法分析規則。也就是說,在上述例子中,除了IN_BLOCK_COMMENT 狀態專用的規則之外,其他的規則將變得無效。要定義某狀態下專用的規則,可以如下這樣在 TOKEN 等命令前加上 < 狀態名 >。

< 狀態名 > TOKEN: {~}
< 狀態名 > SKIP: {~}
< 狀態名 > SPECIAL_TOKEN: {~}

DEFAULT 狀態(DEFAULT state)表示掃描器在開始詞法分析時的狀態。沒有特別指定狀態的詞法分析規則都會被視作 DEFAULT 狀態。也就是說,至今爲止所定義的保留字的掃描規則、標識符的規則以及行註釋的規則實際上都屬於 DEFAULT 狀態。<"*/">:DEFAULT 的意思是匹配模式 “*/” 的話就回到最初的狀態。

MORE命令將表示爲“僅匹配該規則的話掃描還沒有結束”,也就是說,進入該狀態的匹配必須表示爲/*…*/這樣的形式,否則就會報錯

掃描字符串字面量

MORE: { <"\""> : IN_STRING } // 規則 1
<IN_STRING> MORE: {
<(~["\"", "\\", "\n", "\r"])+> // 規則 2
| <"\\" (["0"-"7"]){3}> // 規則 3
| <"\\" ~[]> // 規則 4
}
<IN_STRING> TOKEN: { <STRING: "\""> : DEFAULT } // 規則 5

首先,藉助狀態遷移可以用多個規則來描述 token。掃描到規則 1 的起始符“"”後遷移到IN_STRING 狀態,只有規則 2、3、4 在該狀態下是有效的。其次,除了最後的規則 5 之外,規則 1 ~ 4 都使用 MORE 命令將用多個規則掃描一個 token。也就是以“ ”包裹的任意字符

試驗

基於自動機的詞法分析器

使用正則表達式進行詞法分析,針對的語言的語法如下所示。

輸入:所給源程序字符串。輸出:二元組(syn,token或sum) 構成的序列,syn 爲單詞種別碼,token爲存放的單詞自身字符串,sum爲整形常量。
語言的詞法爲:
1、關鍵字
  main
  if then else
  while do
  repeat until
  for from to step
  switch of case default
  return
  integer real char bool
  and or not mod 
  read write
  所有關鍵字都是小寫。
2、專用符號
運算符包括:=、+、-、*、/、<、<=、>、>=、!=
分隔符包括:,、;、:,{、}、[、]、(、)
 3、其它標記ID和NUM
通過以下正規式定義其它標記:
ID→letter(letter | digit)*
NUM→digit digit*
letter→a | … | z | A | … | Z
digit→0|…|9
 4、空格由空白、製表符和換行符組成
  空格一般用來分隔ID、NUM、專用符號和關鍵字,詞法分析階段通常被忽略。
單詞符號種別碼在教科書中未設定,因此此處指定爲按上述單詞符號的順序從1開始按照順序遞增。

詞法分析記號描述Token

/**
 * 詞法分析-單詞符號表示
 * 抽象類,單詞種別的父類,維護符號與種別碼的映射
 */
public abstract class Token {
    //表示文件末尾的結束符
    public static final Token EOF = new Token(-1) {
    };
    //表示每一行的結束符,也就是換行符\n
    public static final String EOL = "\\n";
    /**
     * 定義匹配不同單詞種別的正則表達式
     * 因爲末尾有一個| 所有要按順序寫
     */
    public static final String KEYWORD_REGEX = "main|if|then|else|while|do|repeat|until|for|" +
            "from|to|step|switch|of|case|default|return|integer|real|char|bool|and|or|not|mod|read|write|";//匹配關鍵字
    public static final String OPERATOR_REGEX = "=|\\+|\\-|\\*|\\/|<|<=|>|>=|!=|";//匹配運算符
    public static final String SEPARATOR_REGEX = "[,;:{}\\[\\]\\(\\)]|";//匹配分隔符
    public static final String ID_REGEX = "[a-zA-Z][a-zA-Z0-9]*|";//匹配標識符
    public static final String NUM_REGEX = "[0-9]+";//常量值標識符


    /**
     * 定義單詞符號與種別碼的映射
     * 所有的信息按順序存放在文件中,讀取文件並加載到該靜態類中
     */
    public static Map<String, Integer> tokenTypeMap;
    //映射的配置文件的位置
    public static String mapConfigPath = new File("").getAbsolutePath() + "/tokenTypeMap.config";

    static {
        tokenTypeMap = new LinkedHashMap<>();
        try {
            Scanner in = new Scanner(new BufferedInputStream(new FileInputStream(mapConfigPath)));
            int ite = 1;
            String res = in.hasNext() ? in.next() : null;
            while (res != null) {
                if (!(res.equals("") || res.charAt(0) == ' ' || res.charAt(0) == '#')) {
                    tokenTypeMap.put(res, ite++);
                }
                res = in.hasNext() ? in.next() : null;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private int lineNumber;//該單詞符號所在的行號

    public Token(int line) {
        this.lineNumber = line;
    }

}

上述讀取的配置文件爲關鍵字,運算符等固定的符號的配置文件,將其讀入內存並按照順序編碼

tokenTypeMap.config

   #關鍵字
   main
   if then else
   while do
   repeat until
   for from to step
   switch of case default
   return
   integer real char bool
   and or not mod
   read write
   #運算符
   = + - * / < <= > >= !=
   #分隔符
   , ; : { } [ ] ( )
   #標識符與常數值
   ID NUM

詞法分析輸出結果TokenRecord

/**
 * 詞法分析的輸出結果
 * (單詞符號碼,單詞符號屬性值)
 */
public class TokenRecord extends Token {
    public TokenRecord(int line) {
        super(line);
    }

    private int flagCode;//標識碼
    private String stringValue;//字符值
    private String numValue;//數值

    public int getFlagCode() {
        return flagCode;
    }

    public String getNumValue() {
        return numValue;
    }

    public String getStringValue() {
        return stringValue;
    }

    public void setFlagCode(int flagCode) {
        this.flagCode = flagCode;
    }

    public void setNumValue(String numValue) {
        this.numValue = numValue;
    }

    public void setStringValue(String stringValue) {
        this.stringValue = stringValue;
    }
}

詞法分析編譯異常CompileException

/**
 * 詞法分析時出現的編譯錯誤
 * 拋出異常
 */
public class CompileException extends Exception {

    public int errorLine;//出現錯誤的行號
    public String errorReason;//出現錯誤的原因

    public CompileException(int errorLine, String errorReason) {
        this.errorLine = errorLine;
        this.errorReason = errorReason;
    }

    @Override
    public String toString() {
        return "第" + errorLine + "行:" + errorReason;
    }
}

預處理器Processor

/**
 * 預處理器,刪除程序中的空行、空格與註釋
 */
public class Preprocessor {
    /**
     * 讀取指定程序文件,進行預處理
     *
     * @param file
     * @return
     */
    public static LinkedHashMap<Integer, String> preprocess(File file) throws CompileException {
        LinkedHashMap<Integer, String> res = new LinkedHashMap<>();
        boolean blockStatus = false;//是否處於塊註釋中
        int lineNumber = 1;
        try {
            Scanner scanner = new Scanner(new FileReader(file));
            String lineInfo = scanner.hasNextLine() ? scanner.nextLine() : null;
            //對每一行進行處理
            while (lineInfo != null) {
                StringBuilder lineProcessValue = new StringBuilder();
                for (int i = 0; i < lineInfo.length(); i++) {
                    //處於塊註釋中
                    if (blockStatus) {
                        if (i + 1 < lineInfo.length() && lineInfo.charAt(i) == '*' && lineInfo.charAt(i + 1) == '/') {
                            i++;
                            blockStatus = false;
                        }
                        continue;
                    }
                    if (lineInfo.charAt(i) == ' ' || lineInfo.charAt(i) == '\n') continue;//空格或者換行符省略
                    if (i + 1 < lineInfo.length() && lineInfo.charAt(i) == '/' && lineInfo.charAt(i + 1) == '/')
                        break;//遇到行註釋,本行退出
                    if (i + 1 < lineInfo.length() && lineInfo.charAt(i) == '/' && lineInfo.charAt(i + 1) == '*') {//遇到塊註釋,進行標識
                        i++;
                        blockStatus = true;
                        continue;
                    }
                    lineProcessValue.append(lineInfo.charAt(i));
                }
                lineInfo = scanner.hasNextLine() ? scanner.nextLine() : null;
                if (!lineProcessValue.toString().equals("")) res.put(lineNumber, lineProcessValue.toString());
                lineNumber++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        //塊註釋出錯,拋出異常
        //TODO 因爲沒有定義字符串字面量"..."與字符字面量'.',雖然可以檢驗是否存在連續的//,但是如果//放在字符串字面量裏檢驗規則就變了,這裏就不檢驗
        if (blockStatus) throw new CompileException(lineNumber, "檢查註釋");

        return res;
    }
}

詞法分析器Lexer

/**
 * 詞法分析器,讀取指定文件,返回單詞符號二元組
 */
public class Lexer {
    //正則式
    public static final String regex = Token.KEYWORD_REGEX + Token.OPERATOR_REGEX +
            Token.SEPARATOR_REGEX + Token.ID_REGEX + Token.NUM_REGEX;

    /**
     * 進行詞法分析
     *
     * @param file 源文件
     * @return (種別碼,單詞符號或值)二元組
     */
    public List<TokenRecord> lex(File file) {
        List<TokenRecord> tuple = new ArrayList<>();
        try {
            //先進行預處理
            LinkedHashMap<Integer, String> map = Preprocessor.preprocess(file);
            //正則匹配每行的數據
            Pattern pattern = Pattern.compile(regex);
            for (int lineNumber : map.keySet()) {
                String string = map.get(lineNumber);
                Matcher matcher = pattern.matcher(string);
                while (matcher.find()) {
                    TokenRecord tokenRecord = new TokenRecord(lineNumber);
                    String match = matcher.group();
                    if (Token.tokenTypeMap.containsKey(match)) {
                        System.out.println("(" + match + ",-)");
                        tokenRecord.setFlagCode(Token.tokenTypeMap.get(match));
                        tokenRecord.setStringValue("-");
                    } else if ('0' <= match.charAt(0) && match.charAt(0) <= '9') {
                        System.out.println("(NUM," + match + ")");
                        tokenRecord.setFlagCode(Token.tokenTypeMap.get("NUM"));
                        tokenRecord.setStringValue(match);
                    } else {
                        System.out.println("(ID," + match + ")");
                        tokenRecord.setFlagCode(Token.tokenTypeMap.get("ID"));
                        tokenRecord.setStringValue(match);
                    }
                    tuple.add(tokenRecord);
                }
            }
        } catch (CompileException e) {//編譯異常
            e.printStackTrace();
            return null;
        }
        return tuple;
    }
}

測試

/**
 * 詞法分析器測試
 */
public class LexerTest {
    public static void main(String[] args) {
        try {
            File file = new File(new File("").getAbsoluteFile() + "/test.txt");
            Lexer lexer = new Lexer();
            for (TokenRecord tokenRecord : lexer.lex(file)) {
                System.out.println("(" + tokenRecord.getFlagCode() + "," + tokenRecord.getStringValue() + ")");
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

測試文件爲test.txt

integer main(){
    integer i=0;//行註釋
    while(i<100)i++;/*塊註釋
    */
    return i;
}

NFA確定化爲DFA

設計確定有窮自動機DFA和非確定有窮自動機NFA描述的對象模型,實現DFA和NFA的基本操作(輸入和輸出)。設計一個將NFA確定化成DFA的方法。

測試文件結構爲:
1)狀態個數stateNum,約定狀態編號0..(stateNum-1);
2)字符個數 symbolNum,約定符號編號 1..symbolNum,編號爲0的符號爲 ;
3)以下若干行是狀態轉換,一行一個轉換,以-1結束;
轉換的格式:狀態,符號(可以是0),若干個狀態,以-1結束;
4)開始狀態集合,-1結束;
5)終止狀態集合,-1結束;
【例】
11
2

0 0 1 7 -1
1 0 2 4 -1
2 1 3 -1
3 0 6 -1
4 2 5 -1
5 0 6 -1
6 0 1 7 -1
7 1 8 -1
8 2 9 -1
9 2 10 -1
-1

0 -1
10 -1

輸出:確定的DFA,描述爲:
狀態個數:5
字符表個數:2
狀態轉換:
(0,1)->1
(0,2)->2
(1,1)->1
(1,2)->3
(2,1)->1
(2,2)->2
(3,1)->1
(3,2)->4
(4,1)->1
(4,2)->2
開始狀態:0
結束狀態集[4]

DFA描述

/**
* @author LSL
*/
public class DFA {
    private List<Integer> statusList;//狀態集
    private List<Integer> symbolList;//字符集
    private List<Function> functionList;//狀態轉換集
    private int begin;//初態
    private List<Integer> endList;//終態集

    public DFA(){
        statusList=new ArrayList<>();
        symbolList=new ArrayList<>();
        functionList=new ArrayList<>();
        begin=0;
        endList=new ArrayList<>();
    }

    /**
     * 狀態保存,此處不使用int[][]
     */
    static class Function{
        private int state;//狀態
        private int symbol;//符號
        private int convertState;//轉換後狀態

        public Function(int state,int symbol,int convertState){
            this.state=state;
            this.symbol=symbol;
            this.convertState=convertState;
        }

        @Override
        public boolean equals(Object object){
            if(!(object instanceof Function))return false;
            return state==((Function) object).state && symbol==((Function) object).symbol;
        }

        public int getConvertState() {
            return convertState;
        }
    }

    @Override
    public String toString(){
        StringBuilder res=new StringBuilder("狀態個數:"+statusList.size()+"\n"+
                "字符表個數:"+(symbolList.size()-1)+"\n"+//字符表包含0,此處應減一
                "狀態轉換:\n");
        //排序
        functionList.sort((Function f1,Function f2)->{
            if(f1.state==f2.state)return f1.symbol-f2.symbol;
            else return f1.state-f2.state;
        });
        //輸出函數
        for (Function function:functionList){
            res.append("(").append(function.state).append(",").append(function.symbol).
                    append(")->").append(function.getConvertState()).append("\n");
        }
        res.append("開始狀態:").append(begin).append("\n");
        res.append("結束狀態集").append(endList.toString()).append("\n");
        return res.toString();
    }

    //getter與setter...此處省略
    
    public void addConvertState(int state, int symbol, int convertState){
        Function function=new Function(state,symbol,convertState);
        functionList.add(function);
    }
}

NFA描述

/**
* @author LSL
*/
public class NFA {
    private List<Integer> statusList=new ArrayList<>();//狀態集
    private List<Integer> symbolList=new ArrayList<>();//字符集
    private List<FunctionExtension> functionList=new ArrayList<>();//狀態轉換集
    private List<Integer> beginList=new ArrayList<>();//初態集
    private List<Integer> endList=new ArrayList<>();//終態集

    /**
     * 讀取文件構造NFA
     * @param file
     */
    public NFA(File file){
        try {
            Scanner scanner=new Scanner(new FileReader(file));
            int stateNum=scanner.nextInt();
            int symbolNum=scanner.nextInt();

            Set<Integer> statusSet=new HashSet<>();//狀態集合
            Set<Integer> symbolSet=new HashSet<>();//符號集合
            String line=scanner.nextLine();
            while (line.equals(""))line=scanner.nextLine();//避免多餘的空行
            while (!line.equals("-1")){
                String[] num=line.split(" ");
                statusSet.add(Integer.parseInt(num[0]));//狀態集
                symbolSet.add(Integer.parseInt(num[1]));//符號集
                FunctionExtension extension=new FunctionExtension(Integer.parseInt(num[0]),Integer.parseInt(num[1]));//轉換
                for(int j=2;j<num.length-1;j++){
                    statusSet.add(Integer.parseInt(num[j]));
                    extension.convertStateList.add(Integer.parseInt(num[j]));
                }
                functionList.add(extension);
                line=scanner.nextLine();
            }

            statusList.addAll(statusSet);
            symbolList.addAll(symbolSet);

            //讀取開始狀態集
            line=scanner.nextLine();
            while (line.equals(""))line=scanner.nextLine();//避免多餘的空行
            String[] num=line.split(" ");
            for(int i=0;i<num.length-1;i++)beginList.add(Integer.parseInt(num[0]));//開始狀態集
            //讀取結束狀態集
            line=scanner.nextLine();
            while (line.equals(""))line=scanner.nextLine();//避免多餘的空行
            num=line.split(" ");
            for(int i=0;i<num.length-1;i++)endList.add(Integer.parseInt(num[0]));//開始狀態集
            //TODO 由狀態數和符號數可校驗讀取是否正確
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 狀態保存,此處不使用int[][]
     */
    static class FunctionExtension{
        private int state;//狀態
        private int symbol;//符號
        private List<Integer> convertStateList=new ArrayList<>();//轉換後狀態

        public FunctionExtension(int state,int symbol){
            this.state=state;
            this.symbol=symbol;
        }
    }

    /**
     * 將該NFA轉換爲DFA
     * @return 轉換後的DFA
     */
    public DFA convertToDFA(){
        DFA res=new DFA();

        Map<List<Integer>,Integer> convertMap=new LinkedHashMap<>();//未標記的轉換映射列表
        Map<List<Integer>,Boolean> convertMapFlag=new LinkedHashMap<>();//映射是否被標記
        int stateId=0;//狀態標識(索引)
        List<Integer> init=Closure(beginList);
        convertMap.put(init,stateId++);//DFA索引映射NFA狀態集
        convertMapFlag.put(init,false);

        while (true){
            List<Integer> T=null;
            //尋找是否存在未標記狀態
            for(List<Integer> flag:convertMapFlag.keySet()){
                if(!convertMapFlag.get(flag)){
                    T=flag;//找到未標記狀態
                    break;
                }
            }
            if(T==null)break;
            convertMapFlag.put(T,true);//重置爲標記狀態

            //對每個符號進行一次操作
            for(int symbol:symbolList){
                /**
                 * 此處,因爲0代表epsilon,不應該對epsilon進行Closure操作
                 */
                if(symbol==0)continue;
                //獲取f(T,symbol)=f(status1,symbol) U f(status2,symbol) U ......
                Set<Integer> tmp=new HashSet<>();//Set去重
                for(int status:T){
                    tmp.addAll(getFunctionConvert(status,symbol));
                }
                List<Integer> U=Closure(new ArrayList<>(tmp));//獲取Closure(f(T,symbol))
                //判斷U集合是否已經在已標記狀態集合映射中,此處比對List<Integer>
                boolean find=false;
                List<Integer> UCopy = null;
                for(List<Integer> flagList:new ArrayList<>(convertMap.keySet())){
                    if(sortListEquals(U,flagList)){
                        find=true;
                        UCopy=flagList;//等價於U
                        break;
                    }
                }
                //如果U不在映射集合中則加入
                if(!find){
                    convertMap.put(U,stateId++);
                    convertMapFlag.put(U,false);//未標記
                }
                //設置DFA的轉換f'(T,symbol)=U
                if(!find)res.addConvertState(convertMap.get(T),symbol,convertMap.get(U));
                else res.addConvertState(convertMap.get(T),symbol,convertMap.get(UCopy));
            }
        }

        //初始化dfa的狀態集,符號表,初始狀態,終態集
        for(List<Integer> list:convertMap.keySet()){//狀態集
            res.getStatusList().add(convertMap.get(list));
        }
        res.getSymbolList().addAll(symbolList);//符號表
        res.setBegin(0);//初始態一定爲0
        res.getEndList().add(stateId-1);//終態集

        return res;
    }

    /**
     * 從給定狀態集合出發,尋找可達節點
     * @param stateList 初始狀態集合
     * @return 從該集合中的節點出發的所有可達節點集合
     */
    private List<Integer> Closure(List<Integer> stateList){
        //將closure(state)初始化爲狀態集合
        List<Integer> res=stateList;
        //將所有狀態壓入棧中
        Stack<Integer> stack=new Stack<>();
        stack.addAll(stateList);

        while (!stack.empty()){
            int node=stack.pop();//出棧
            //搜尋函數轉換列表,發現結果集
            for (FunctionExtension extension:functionList){
                if(extension.state==node&&extension.symbol==0){//0代表epsilon
                    for(int endState:extension.convertStateList){//可達終態集
                        if(!res.contains(endState)){
                            res.add(endState);
                            stack.push(endState);
                        }
                    }
                }
            }
        }

        res.sort(Comparator.comparingInt(num->num));//返回的結果排序
        return res;
    }

    /**
     * 給定狀態與符號,返回轉換後的狀態集
     * @param state
     * @param symbol
     * @return
     */
    public List<Integer> getFunctionConvert(int state,int symbol){
        for(FunctionExtension extension:functionList){
            if(extension.state==state && extension.symbol==symbol)return extension.convertStateList;
        }
        return new ArrayList<>();
    }

    /**
     * 判斷兩個列表是否元素完全相同,列表已排序
     * @param a
     * @param b
     * @return
     */
    public boolean sortListEquals(List<Integer> a,List<Integer> b){
        if(a.size()!=b.size())return false;
        for(int i=0;i<a.size();i++){
            if(!a.get(i).equals(b.get(i)))return false;
        }
        return true;
    }

    @Override
    public String toString(){
        StringBuilder res=new StringBuilder(statusList.toString()+symbolList.toString()+beginList.toString()+endList.toString());
        for(FunctionExtension extension:functionList){
            res.append("\n").append(extension.state).append(" ").append(extension.symbol).append(" ").append(extension.convertStateList);
        }
        return res.toString();
    }
}

測試

public class NfaToDfaTest {
    public static void main(String[] args){
        File file = new File(new File("").getAbsoluteFile() + "/test.txt");
        NFA nfa=new NFA(file);
        System.out.println(nfa.convertToDFA().toString());
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章