編譯實踐-PL\0編譯系統實現
姓名: |
|
專業: |
計算機科學與技術 |
學院: |
軟件學院 |
提交時間: |
2013年12月25日 |
北京航空航天大學·軟件學院
編譯實踐-PL\0編譯系統實現
- 實驗要求
-
以個人為單位進行開發,不得多人合作完成。
-
共32個學時。個人無計算機者可以申請上機機時。
-
細節要求:
-
輸入:符合PL/0文法的源程序(自己要有5個測試用例,包含出錯的情況,還要用老師提供的測試用例進行測試)
-
輸出:P-Code
-
錯誤信息:參見教材第316頁表14.4。
-
P-Code指令集:參見教材第316頁表14.5。
-
語法分析部分要求統一使用遞歸下降子程序法實現。
-
編程語言使用C、C++、C#或Java等。
-
上交材料中不但要包括源代碼(含注釋)和可執行程序,還應有完整文檔。
-
- PL/0語言描述
PL/0語言是一種類PASCAL語言,是教學用程序設計語言,它比PASCAL語言簡單,作了一些限制。PL/0的程序結構比較完全,賦值語句作為基本結構,構造概念有
-
順序執行、條件執行和重復執行,分別由begin/end,if then else和while do語句表示。
-
PL0還具有子程序概念,包括過程說明和過程調用語句。
-
在數據類型方面,PL0只包含唯一的整型,可以說明這種類型的常量和變量。
-
運算符有+,-,*,/,=,<>,<,>,<=,>=,(,)。
-
說明部分包括常量說明、變量說明和過程說明。
- PL/0語言文法的EBNF表示
<程序> ::= <分程序>.
<分程序> ::= [<常量說明部分>][變量說明部分>]{<過程說明部分>}<語句>
<常量說明部分> ::= const<常量定義>{,<常量定義>};
<常量定義> ::= <標識符>=<無符號整數>
<無符號整數> ::= <數字>{<數字>}
<標識符> ::= <字母>{<字母>|<數字>}
<變量說明部分>::= var<標識符>{,<標識符>};
<過程說明部分> ::= <過程首部><分程序>;
<過程首部> ::= procedure<標識符>;
<語句> ::= <賦值語句>|<條件語句>|<當型循環語句>|<過程調用語句>|<讀語句>|<寫語句>|<復合語句>|<重復語句>|<空>
<賦值語句> ::= <標識符>:=<表達式>
<表達式> ::= [+|-]<項>{<加法運算符><項>}
<項> ::= <因子>{<乘法運算符><因子>}
<因子> ::= <標識符>|<無符號整數>|'('<表達式>')'
<加法運算符> ::= +|-
<乘法運算符> ::= *|/
<條件> ::= <表達式><關系運算符><表達式>|odd<表達式>
<關系運算符> ::= =|<>|<|<=|>|>=
<條件語句> ::= if<條件>then<語句>[else<語句>]
<當型循環語句> ::= while<條件>do<語句>
<過程調用語句> ::= call<標識符>
<復合語句> ::= begin<語句>{;<語句>}end
<重復語句> ::= repeat<語句>{;<語句>}until<條件>
<讀語句> ::= read'('<標識符>{,<標識符>}')'
<寫語句> ::= write'('<標識符>{,<標識符>}')'
<字母> ::= a|b|...|X|Y|Z
<數字> ::= 0|1|2|...|8|9
注意:
數據類型:無符號整數
標識符類型:簡單變量(var)和常數(const)
數字位數:小於14位
標識符的有效長度:小於10位
過程嵌套:小於3層
- PL/0語言的語法圖描述
圖1-1 程序語法描述圖
圖1-2 分程序語法描述圖
圖1-6 項語法描述圖
圖1-7 因子語法描述圖
-
PL/0編譯系統結構
圖 1-8 PL/0編譯程序和解釋執行過程
PL/0編譯程序函數定義層次結構:
pl0
error
getsym
getch
gen
test
block
enter
position
constdeclaration
vardeclaration
listcode
st:atement
expression
term
factor
condition
interpret
base
下面介紹這些過程(函數)的作用。
pl0 |
主程序 |
error |
出錯處理,打印出錯位置和錯誤代碼 |
getsym |
詞法分析,讀取一個單詞 |
getch |
取字符 |
gen |
生成P-code指令,送入目標程序區 |
test |
測試當前單詞符號是否合法 |
block |
分程序分析處理 |
enter |
登記符號表 |
position |
查找標識符在符號表中的位置 |
constdeclaration |
常量定義處理 |
vardeclaration |
變量定義處理 |
listcode |
列出p-code指令清單 |
statement |
語句部分分析處理 |
expression |
表達式分析處理 |
term |
項分析處理 |
factor |
因子分析處理 |
condition |
條件分析處理 |
interpret |
P-code解釋執行程序 |
base |
通過靜態鏈求出數據區的基地址 |
-
PL/0編譯程序的詞法分析
PL/0編譯系統中所有的字符,字符串的類型為,如下表格:
保留字
begin, end, if,then, else, const,procedure,
var,do,while, call,read, write, repeat, until
算數運算符
+ ,—,*,/
比較運算符
<> , < ,<= , >, >= ,=
賦值符
:= , =
標識符
變量名,過程名,常數名
常數
10,25等整數
界符
',','.',';','(',')'
PL/0的詞法分析程序Scanner.getsym()由語法分析程序調用,主要功能為:
- 跳過空格字符。
- 識別單詞符號,返回單詞類型(按照在Symbol.java中定義的編譯系統的字符編號,返回類型碼)
- 特別的,對於編譯系統的保留字符(例如:const, if, then等)需要查找系統的保留字符表word[],為了加快查找速度,調用系統的二分搜索法Arrays.binarySearch().
-
另外,如果讀取的字符為數字,需要將該字符轉換成整數值(調用公式num = 10 * num + (ch - '0');),再存入符號表的Value區域.
Scanner.getsym()是調用掃描輸入的源程序。主要功能如下:
- 優化讀取字符效率,每次讀取一行源程序,存入緩沖區line,因此設置lineLength為源程序當前行的長度,chCount標志當前正在讀取的字符位置
- 采用"單符號先行"技術,在識別完每個符號的類型后,必須再度入下一個字符,以保證下一次再調用getsym()時,curCh保存的是該符號的首字符
圖1- 詞法分析程序的狀態轉換圖
- PL/0編譯程序的符號表管理
-
符號表結構
|
符號表類SymbolTable中用數組存儲符號表,再分配一個指針tablePtr指向當前符號表的末尾。 public class SymbolTable { //有效的符號表大小 public int tablePtr = 0; //名字表 public Item[] table = new Item[tableMax]; ... ... } |
舉例:
PL/0代碼樣例: CONST A=35,B=49; |
此時的符號表內容:
NAME:A |
KIND:CONSTANT |
VAL:35 |
||
NAME:B |
KIND:CONSTANT |
VAL:49 |
||
NAME:C |
KIND:VARIABLE |
LEVEL:LEV |
ADDR:DX |
|
NAME:D |
KIND:VARIABLE |
LEVEL:LEV |
ADDR:DX+1 |
|
NAME:E |
KIND:VARIABLE |
LEVEL:LEV |
ADDR:DX+2 |
|
NAME:P |
KIND:PROCEDURE |
LEVEL:LEV |
ADDR: |
SIZE:7 |
NAME:G |
KIND:VARIABLE |
LEVEL:LEV+1 |
ADDR:DX |
-
符號表管理
-
登記(在符號表中插入一項)
-
/** * 把某個符號登錄到名字表中,從1開始填,0表示不存在該項符號 * @param sym 要登記到名字表的符號 * @param k 該符號的類型:const, var ,procedure * @param lev 名字所在的層次 * @param dx 當前應分配的變量的相對地址,注意dx要加一 */ public void enter(Symbol sym,int type,int lev, int dx) |
-
查詢
/** * 在名字表中查找某個名字的位置 *從后往前查,這樣符合嵌套分程序名字定義和作用域的規定 * @param idt 要查找的名字 * @return 如果找到則返回名字項的下標,否則返回0 */ public int position(String idt) |
- PL/0編譯程序的語法分析
圖1- 語法調用關系圖
采用不帶回溯的遞歸子程序法,對於語言的文法要求:
- 該文法必須是非左遞歸。
-
文法的非終結符,其規則右部所生成的first集合兩兩不相交
- 若文法具有形如
,則
- 若文法具有形如
遞歸子程序設計實例
void expression(BitSet fsys, int lev) { if (symtype == plus || symtype == minus) { int adop = symtype; nextsym(); term(nxtlev, lev); if (adop == minus) gen(OPR, 0, 1); } else term(nxtlev, lev); //分析{<加法運算符><項>} while (symtype == plus || symtype == minus) { int adop = symtype; nextsym(); term(nxtlev, lev); gen(OPR, 0, adop); } } |
void term(BitSet fsys, int lev) { factor(nxtlev, lev); //分析{<乘法運算符><因子>} while (symtype == mul || symtype == div) { int mop = sym.symtype; nextsym(); factor(nxtlev, lev); gen(OPR, 0, mop); } } |
void factor(BitSet fsys, int lev) { if (symtype == ident) { int index = table.position(sym.id); if (index > 0) { Item item = table.get(index); switch (item.type) { case constant: gen(LIT, 0, item.value); break; case variable: gen(LOD, lev - item.lev, item.addr); break; } } nextsym(); } else if (symtype == number) { gen(LIT, 0, num); nextsym(); } else if (symtype == lparen) { nextsym(); expression(nxtlev, lev); if (symtype == rparen) nextsym(); } |
- PL/0編譯程序的目標代碼結構和代碼生成
-
代碼結構
P-code 語言:一種棧式機的語言。此類棧式機沒有累加器和通用寄存器,有一個棧式存儲器,有四個控制寄存器(指令寄存器 I,指令地址寄存器 P,棧頂寄存器 T和基址寄存器 B),算術邏輯運算都在棧頂進行。
F |
L |
A |
指令格式
F :操作碼
L :層次差(標識符引用層減去定義層)
A :不同的指令含義不同
表5 P-code 指令的含義
指令 |
具體含義 |
LIT 0,a |
取常量a放到數據棧棧頂 |
OPR 0,a |
執行運算,a表示執行何種運算(+ - * /) |
LOD l,a |
取變量放到數據棧棧頂(相對地址為a,層次差為l) |
STO l,a |
將數據棧棧頂內容存入變量(相對地址為a,層次差為l) |
CAL l,a |
調用過程(入口指令地址為a,層次差為l) |
INT 0,a |
數據棧棧頂指針增加a |
JMP 0,a |
無條件轉移到指令地址a |
JPC 0,a |
條件轉移到指令地址a |
//pcode類的結構 public class Pcode{ //虛擬機代碼指令 public int f; //引用層與聲明層的層次差 public int l; //指令參數 public int a; } //存放虛擬機代碼的數組 public Pcode[] pcodeArray;
//生成虛擬機代碼 public void gen(int f, int l, int a) { pcodeArray[arrayPtr++] = new Pcode(f, l, a); } |
- 代碼生成與地址返填
對於if then [else],while do和repeat until語句,要生成跳轉指令,故采用地址返填技術。
- if-then-else語句的目標代碼生成模式:
if <condition> then <statement>[else] |
|
<condition> |
|
JPC addr1 |
|
<statement> |
|
addr1: |
[else] |
- while-do語句的目標代碼生成模式:
while <condition> do <statement> |
|
addr2: |
<condition> |
JPC addr3 |
|
<statement> |
|
JPC addr2 |
|
addr3: |
- repeat-until語句的目標代碼生成模式:
repeat <statement> until <condition> |
|
addr4: |
<statement> |
<condition> |
|
JPC addr4 |
注意:由於OPR指令設計復雜,故進一步解釋:
(1).OPR 0 0 |
RETUEN (stack[sp + 1] ß base(L); sp ß bp - 1; bp ß stack[sp + 2]; pc ß stack[sp + 3];) |
(2).OPR 0 1 |
NEG (- stack[sp] ) |
(3).OPR 0 2 |
ADD (sp ß sp – 1 ; stack[sp] ß stack[sp] + stack[sp + 1]) |
(4).OPR 0 3 |
SUB (sp ß sp – 1 ; stack[sp] ßstack[sp] - stack[sp + 1]) |
(5).OPR 0 4 |
MUL (sp ß sp – 1 ; stack[sp] ß stack[sp] * stack[sp + 1]) |
(6).OPR 0 5 |
DIV (sp ß sp – 1 ; stack[sp] ß stack[sp] / stack[sp + 1]) |
(7).OPR 0 6 |
ODD (stack[sp] ß stack % 2) |
(8).OPR 0 7 |
MOD (sp ß sp – 1 ; stack[sp] ß stack[sp] % stack[sp + 1]) |
(9).OPR 0 8 |
EQL (sp ß sp – 1 ; stack[sp] ß stack[sp] == stack[sp + 1]) |
(10).OPR 0 9 |
NEQ (sp ß sp – 1 ; stack[sp] ß stack[sp] != stack[sp + 1]) |
(11).OPR 0 10 |
LSS (sp ß sp – 1 ; stack[sp] ß stack[sp] < stack[sp + 1]) |
(12).OPR 0 11 |
GEQ (sp ß sp – 1 ; stack[sp] ß stack[sp] >= stack[sp + 1]) |
(13).OPR 0 12 |
GTR (sp ß sp – 1 ; stack[sp] ß stack[sp] > stack[sp + 1]) |
(14).OPR 0 13 |
LEQ (sp ß sp – 1 ; stack[sp] ß stack[sp] <= stack[sp + 1]) |
(15).OPR 0 14 |
print (stack[sp]); sp ß sp – 1; |
(16).OPR 0 15 |
print ('\n'); |
(17).OPR 0 16 |
scan(stack[sp]); sp ß sp + 1; |
- PL/0編譯程序的語法錯誤處理
8.1錯誤處理的原則
盡可能准確指出錯誤位置和錯誤屬性
盡可能進行校正
短語層恢復技術
在進入某個語法單位時,調用TEST函數, 檢查當前符號是否屬於該語法單位的開始符號集合.
在語法單位分析結束時,調用TEST函數, 檢查當前符號是否屬於調用該語法單位時應有的后跟符號集合.
Test()函數的定義: /** * @param s1 需要的符號 * @param s2 不需要的符號,添加一個補救集合 * @param errcode 錯誤號 */ void test(BitSet s1, BitSet s2, int errcode) { if (!s1.get(sym.symtype)) { Err.report(errcode); //當檢測不通過時,不停地獲取符號,直到它屬於需要的集合 s1.or(s2); //把s2集合補充進s1集合 while (!s1.get(sym.symtype)) { nextsym(); } } } |
注意:FOLLOW集合隨着調用的深度增加,逐層增加,且與調用的位置相關。
舉例: 在write語句的下一層: <statement>::=write '('<identity>{,identity}')' fsys={[rparen, comma]+fsys}; 在factor語句的下一層 <factor>::=… …|'('<expression>')' fsys={[rparen]+fsys}; |
表1- PL/0文法非終結符的開始符號集與后繼符號集
非終結符 |
FIRST(S) |
FOLLOW(S) |
分程序 |
const var procedure ident if call begin while read write repeat |
. ; |
語句 |
ident call begin if while read write until |
. ; end |
條件 |
odd + - ( ident number |
then do |
表達式 |
= + - ( ident number |
. ; R end then do |
項 |
ident number ( |
. ; R + - end then do |
因子 |
ident number ( |
. ; R + - * / end then do |
PL/0編譯系統中,所定義的36種錯誤類型,如下列舉:
PL/0語言的出錯信息表 |
|
出錯編號 |
出錯原因 |
1 |
常數說明中的"="寫成"∶="。 |
2 |
常數說明中的"="后應是數字。 |
3 |
常數說明中的標識符后應是"="。 |
4 |
const ,var, procedure后應為標識符。 |
5 |
漏掉了','或';'。 |
6 |
過程說明后的符號不正確(應是語句開始符,或過程定義符)。 |
7 |
應是語句開始符。 |
8 |
程序體內語句部分的后跟符不正確。 |
9 |
程序結尾丟了句號'.'。 |
10 |
語句之間漏了';'。 |
11 |
標識符未說明。 |
12 |
賦值語句中,賦值號左部標識符屬性應是變量。 |
13 |
賦值語句左部標識符后應是賦值號'∶='。 |
14 |
call后應為標識符。 |
15 |
call后標識符屬性應為過程。 |
16 |
條件語句中丟了'then'。 |
17 |
丟了'end"或';'。 |
18 |
while型循環語句中丟了'do'。 |
19 |
語句后的符號不正確。 |
20 |
應為關系運算符。 |
21 |
表達式內標識符屬性不能是過程。 |
22 |
表達式中漏掉右括號')'。 |
23 |
因子后的非法符號。 |
24 |
表達式的開始符不能是此符號。 |
31 |
數越界。 |
32 |
read語句括號中的標識符不是變量。 |
33 |
格式錯誤,應為右括號 |
34 |
格式錯誤,應為左括號 |
35 |
read()中的變量未聲明 |
36 |
變量字符過長 |
- PL/0編譯程序的目標代碼解釋執行和存儲分配
- 類pcode解釋器的結構
- . 目標代碼存放在數組pcodeArray中
- . 定義一維整型數組runtimeStack作為運行棧
- .棧頂寄存器(指針)sp;
- .基址寄存器(指針)bp;
- .程序地址寄存器 pc;
- .指令寄存器 index.
- 運行棧的存儲分配
- .SL:靜態鏈,指向定義該過程的直接外過程(或主程序)運行時最新數據段的基地址。
- .DL:動態鏈,指向調用該過程前正在運行過程的數據段基地址。
- .RA:返回地址,記錄調用該過程時目標程序的斷點,即調用過程指令的下一條指令的地址
例如,假定有過程 A,B,C,其中過程 C 的說明局部於過程 B,而過程 B 的說明局部於過程 A,程序運行時,過程 A 調用過程 B,過程 B 則調用過程 C,過程 C 又調用過程 B,如下圖所示:
圖9-1過程說明嵌套圖 過程調用圖 表示 A 調用 B
從靜態鏈的角度我們可以說A是在第一層說明,B是在第二層說明,C則是在第三層說明。
若在B中存取A中說明的變量a,由於編譯程序只知道A,B間的靜態層差為1,如果這時沿着動態鏈下降一步,將導致對C的局部變量的操作。
為防止這種情況發生,設置第二條鏈,將各個數據區連接起來。我們稱之為動態鏈(dynamic link)DL。這樣,編譯程序所生成的代碼地址,指示着靜態層差和數據區的相對修正量。下面是過程 A、B 和 C 運行時刻的數據區圖示:
P-code解釋執行過程:
(1).LIT 0 A |
sp ß sp +1; stack[sp] ß A; |
(2).LOD L A |
sp ß sp +1; stack[sp] ß stack[ base(L) + A]; |
(3).STO L A |
stack[ base(L) + A] ß stack[sp]; sp ß sp -1; |
(4).CAL L A |
stack[sp + 1] ß base(L); stack[sp + 2] ß bp; stack[sp + 3] ß pc; bp ß sp + 1; pc ß A; |
(5).INT 0 A |
sp ß sp + A; (6).JMP 0 A |
pc = A; |
(7).JPC 0 A |
if stack[sp] == 0 { pc ß A; sp ß sp - 1; } |
(8).OPR 0 0 |
RETUEN (stack[sp + 1] ß base(L); sp ß bp - 1; bp ß stack[sp + 2]; pc ß stack[sp + 3];) |
(9).OPR 0 1 |
NEG (- stack[sp] ) |
(10).OPR 0 2 |
ADD (sp ß sp – 1 ; stack[sp] ß stack[sp] + stack[sp + 1]) |
(11).OPR 0 3 |
SUB (sp ß sp – 1 ; stack[sp] ßstack[sp] - stack[sp + 1]) |
(12).OPR 0 4 |
MUL (sp ß sp – 1 ; stack[sp] ß stack[sp] * stack[sp + 1]) |
(13).OPR 0 5 |
DIV (sp ß sp – 1 ; stack[sp] ß stack[sp] / stack[sp + 1]) |
(14).OPR 0 6 |
ODD (stack[sp] ß stack % 2) |
(15).OPR 0 7 |
MOD (sp ß sp – 1 ; stack[sp] ß stack[sp] % stack[sp + 1]) |
(16).OPR 0 8 |
EQL (sp ß sp – 1 ; stack[sp] ß stack[sp] == stack[sp + 1]) |
(17).OPR 0 9 |
NEQ (sp ß sp – 1 ; stack[sp] ß stack[sp] != stack[sp + 1]) |
(18).OPR 0 10 |
LSS (sp ß sp – 1 ; stack[sp] ß stack[sp] < stack[sp + 1]) |
(19).OPR 0 11 |
GEQ (sp ß sp – 1 ; stack[sp] ß stack[sp] >= stack[sp + 1]) |
(20).OPR 0 12 |
GTR (sp ß sp – 1 ; stack[sp] ß stack[sp] > stack[sp + 1]) |
(21).OPR 0 13 |
LEQ (sp ß sp – 1 ; stack[sp] ß stack[sp] <= stack[sp + 1]) |
(22).OPR 0 14 |
print (stack[sp]); sp ß sp – 1; |
(23).OPR 0 15 |
print ('\n'); |
(24).OPR 0 16 |
scan(stack[sp]); sp ß sp + 1; |
系統運行環境
硬件配置:lenovo-g470
軟件配置:netbeans-7.4
軟件運行環境:java JDK-1.7
附錄:
樣例測試
//test.pl0 |
//generated p-code |
const z=0; var head,foot,cock,rabbit,n; begin n := z; cock := 1; while cock <= head do begin rabbit :=head-cock; if cock*2+rabbit*4=foot then begin write(cock,rabbit); n:=n+1 end; cock:=cock+1 end; if n=0 then write(0,0) end. |
0 JMP 0 21 1 JMP 0 2 2 INT 0 4 3 LOD 1 3 4 STO 0 3 5 LOD 1 4 6 STO 1 3 7 LOD 0 3 8 STO 1 4 9 OPR 0 0 10 JMP 0 11 11 INT 0 3 12 LOD 1 3 13 LOD 1 3 14 LOD 1 4 15 OPR 0 5 16 LOD 1 4 17 OPR 0 4 18 OPR 0 3 19 STO 1 3 20 OPR 0 0 21 INT 0 7 22 LIT 0 45 23 STO 0 3 24 LIT 0 27 25 STO 0 4 26 CAL 0 11 27 LOD 0 3 28 LIT 0 0 29 OPR 0 9 30 JPC 0 34 31 CAL 0 2 32 CAL 0 11 33 JMP 0 27 34 LOD 0 4 35 STO 0 5 36 LIT 0 45 37 LIT 0 27 38 OPR 0 4 39 LOD 0 5 40 OPR 0 5 41 STO 0 6 42 LOD 0 5 43 OPR 0 14 44 LOD 0 6 45 OPR 0 14 46 OPR 0 15 47 OPR 0 0 |
實踐報告+源代碼鏈接: