北航 編譯實踐 PL/0文法


 

編譯實踐-PL\0編譯系統實現

 

 

 

姓名:

 

專業:

計算機科學與技術

學院:

軟件學院

提交時間:

20131225

 

 

 

 

北京航空航天大學·軟件學院

編譯實踐-PL\0編譯系統實現

  1. 實驗要求
  • 以個人為單位進行開發,不得多人合作完成。
  • 32個學時。個人無計算機者可以申請上機機時。
  • 細節要求:
    • 輸入:符合PL/0文法的源程序(自己要有5個測試用例,包含出錯的情況,還要用老師提供的測試用例進行測試)
    • 輸出:P-Code
    • 錯誤信息:參見教材第316頁表14.4
    • P-Code指令集:參見教材第316頁表14.5
    • 語法分析部分要求統一使用遞歸下降子程序法實現。
    • 編程語言使用CC++C#Java等。
    • 上交材料中不但要包括源代碼(含注釋)和可執行程序,還應有完整文檔。
  1. PL/0語言描述

PL/0語言是一種類PASCAL語言,是教學用程序設計語言,它比PASCAL語言簡單,作了一些限制。PL/0的程序結構比較完全,賦值語句作為基本結構,構造概念有

  • 順序執行、條件執行和重復執行,分別由begin/end,if then elsewhile do語句表示。
  • PL0還具有子程序概念,包括過程說明和過程調用語句。
  • 在數據類型方面,PL0只包含唯一的整型,可以說明這種類型的常量和變量。
  • 運算符有+-*/=<><><=>=()
  • 說明部分包括常量說明、變量說明和過程說明。
  1. 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

  1. PL/0語言的語法圖描述

1-1 程序語法描述圖

1-2 分程序語法描述圖

 

1-6 項語法描述圖

1-7 因子語法描述圖

 

 

  1. PL/0編譯系統結構

    1-8 PL/0編譯程序和解釋執行過程

     

     

    PL/0編譯程序函數定義層次結構:

    pl0

        error

        getsym

            getch

        gen

        test

        block

            enter

            position

            constdeclaration

            vardeclaration

            listcode

            statement

                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

通過靜態鏈求出數據區的基地址

 

  1. 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- 詞法分析程序的狀態轉換圖

  1. PL/0編譯程序的符號表管理
    • 符號表結構
  • 符號表中每一條記錄所對應的結構:

    public class Item {

    public static final int constant = 0;

    public static final int variable = 1;

    public static final int procedure = 2;

    String name; //名字

    int type; //類型,const var or procedure

    int value; //數值,const使用

    int level; //所處層,varprocedure使用

    int addr; //地址,varprocedure使用

    int size; //需要分配的數據區空間,僅procedure使用

    }

 

符號表類SymbolTable中用數組存儲符號表,再分配一個指針tablePtr指向當前符號表的末尾。

public class SymbolTable {

//有效的符號表大小

public int tablePtr = 0;

    //名字表

    public Item[] table = new Item[tableMax];

    ... ...

}

舉例:

PL/0代碼樣例:

CONST A=35B=49
VAR C
DE
PROCEDURE P

VAR G
XYZ

 

此時的符號表內容:

NAMEA

KIND:CONSTANT

VAL:35

   

NAMEB

KIND:CONSTANT

VAL:49

   

NAMEC

KIND:VARIABLE

LEVEL:LEV

ADDR:DX

 

NAMED

KIND:VARIABLE

LEVEL:LEV

ADDR:DX+1

 

NAMEE

KIND:VARIABLE

LEVEL:LEV

ADDR:DX+2

 

NAMEP

KIND:PROCEDURE

LEVEL:LEV

ADDR:

SIZE:7

NAMEG

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)

 

  1. PL/0編譯程序的語法分析

 

1- 語法調用關系圖

 

采用不帶回溯的遞歸子程序法,對於語言的文法要求:

  1. 該文法必須是非左遞歸。
  2. 文法的非終結符,其規則右部所生成的first集合兩兩不相交
    1. 若文法具有形如,則

遞歸子程序設計實例

  • <expression>::=[+|-]<term>{(+|-)<>}

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);

  }

}

 

  • <term>::=<factor>{(*|/)<term>} 

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);

  }

}

 

  • <factor>::=<ident>|<number>|'('<experssion>')'

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();

}

 

  1. 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 dorepeat 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;

 

 

  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

變量字符過長

 

  1. PL/0編譯程序的目標代碼解釋執行和存儲分配
  • pcode解釋器的結構
  1. . 目標代碼存放在數組pcodeArray
  2. . 定義一維整型數組runtimeStack作為運行棧
  3. .棧頂寄存器(指針)sp;
  4. .基址寄存器(指針)bp;
  5. .程序地址寄存器 pc;
  6. .指令寄存器 index.
  • 運行棧的存儲分配
  1. .SL:靜態鏈,指向定義該過程的直接外過程(或主程序)運行時最新數據段的基地址。
  2. .DL:動態鏈,指向調用該過程前正在運行過程的數據段基地址。
  3. .RA:返回地址,記錄調用該過程時目標程序的斷點,即調用過程指令的下一條指令的地址

                  例如,假定有過程 ABC,其中過程 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 linkDL這樣,編譯程序所生成的代碼地址,指示着靜態層差和數據區的相對修正量。下面是過程 AB 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

 

實踐報告+源代碼鏈接:

 http://files.cnblogs.com/ZJUT-jiangnan/compiler.rar


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM