background image

Khoa công nghệ thông tin - Đại học Thái Nguyên

Bộ môn công nghệ phần mềm 

GIÁO TRÌNH MÔN CHƯƠNG TRÌNH DỊCH 

(Compiler Construction)

Thái nguyên, 2007 

background image
background image

LỜI NÓI ĐẦU

Môn học chương trình dịch là môn học của ngành khoa học máy tính. Trong 

suốt thập niên 50, trình biên dịch được xem là cực kỳ khó viết. Ngày nay, việc viết 
một chương trình dịch trở nên đơn giản hơn cùng với sự hỗ trợ của các công cụ 
khác. Cùng với sự phát triển của các chuyên ngành lý thuyết ngôn ngữ hình thức và 
automat, lý thuyết thiết kế một trình biên dịch ngày một hoàn thiện hơn. 

Có rất nhiều các trình biên dịch hiện đại, có hỗ trợ nhiều tính năng tiện ích 

khác nữa. Ví dụ: bộ visual Basic, bộ studio của Microsoft, bộ Jbuilder, netbean, 
Delphi …

 Tại sao ta không đứng trên vai những người khổng lồ đó mà lại đi nghiên 

cứu cách xây dựng một chương trình dịch nguyên thuỷ. Với vai trò là sinh viên 
công nghệ thông tin ta phải tìm hiểu nghiên cứu xem một chương trình dịch thực sự 
thực hiện như thế nào?  

Mục đích của môn học này là sinh viên sẽ học các thuật toán phân tích ngữ 

pháp và các kỹ thuật dịch, hiểu được các thuật toán xử lý ngữ nghĩa và  tối ưu hóa 
quá trình dịch.

Yêu cầu người học nắm được các thuật toán trong kỹ thuật dịch.

Nội dung môn học :  Môn học Chương trình dịch nghiên cứu 2 vấn đề:

- Lý thuyết thiết kế ngôn ngữ lập trình ( cách tạo ra một ngôn ngữ giúp người 

lập trình có thể đối thoại với máy và có thể tự động dịch được).

- Cách viết chương trình chuyển đổi từ ngôn ngữ lập trình này sang ngôn ngữ 

lập trình khác.

Học môn chương trình dịch giúp ta: 
- Nắm vững nguyên lý lập trình: Hiểu từng ngôn ngữ, điểm mạnh điểm yếu 

của nó => chọn ngôn ngữ thích hợp cho dự án của mình. Biết chọn chương trình 
dịch thích hợp (VD với pascal dưới Dos: chương trình dịch là turbo pascal. Đối với 
ngôn ngữ C: chọn turbo C hay bolean C? Bolean C tiện lợi, dễ dùng,  turbo C sinh 
mã gọn, không phải lo vè vấn đề tương thích với hệ điều hành nhưng khoá dùng 
hơn). Phân biệt được công việc nào do chương trình dịch thực hiện và do chương 
trình ứng dụng thực hiện.

- Vận dụng: thực hiện các dự án xây dựng chương trình dịch. Áp dụng vào 

các ngành khác như xử lý ngôn ngữ tự nhiên…   

background image

Để viết được trình biên dịch ta cần có kiến thức về ngôn ngữ lập trình, cấu 

trúc máy tính, lý thuyết ngôn ngữ, cấu trúc dữ liệu, phân tích thiết kế giải thuật và 
công nghệ phần mềm.

Những kiến thức của môn học cũng có thể được sử dụng trong các lĩnh vực 

khác như xử lý ngôn ngữ tự nhiên.

Tài liệu tham khảo: 

1. Giáo trình sử dụng: Dick Grune, Ceriel Jacobs, Parsing Techniques: A 

Practical Guide, 1998

2. Một số tài nguyên trực tuyến có thể được tìm thấy bằng việc sử dụng máy 

tìm   kiếm,   chẳng   hạn   như

 http://www.cppreference.com/ 

và 

http://www.sgi.com/tech/stl/.

3. Bài giảng Lý thuyết và Thực hành Chương Trình Dịch của Lê Anh Cường, 

khoa Công Nghệ, ĐHQG Hà nội, 2004.

4. Giáo trình lý thuyết, thực hành môn học Chương trình dịch của Phạm 

Hồng Nguyên, Khoa Công Nghệ, ĐHQG Hà nội, 1998.

5. Ngôn ngữ hình thức của Nguyễn Văn Ba, ĐHBK Hà nội, 1994
6. Thực hành kỹ thuật biên dịch của Nguyễn Văn Ba, ĐHBK Hà nội, 1993
7. Compiler: principles techniques and tools của A.V. Aho, Ravi Sethi, D. 

Ulman, 1986 

8. Bản dịch của tài liệu: Trình biên dịch: Nguyên lý, kỹ thuật và công cụ của 

Trần Đức Quang, 2000.

background image

Chương 1: Tổng quan về ngôn ngữ lập trình và chương trình dịch

1. Ngôn ngữ lập trình và chương trình dịch.

Con người muốn máy tính thực hiện công việc thì con người phải viết yêu cầu 
đưa cho máy tính bằng ngôn ngữ máy hiểu được. Việc viết yêu cầu gọi là lập 
trình. Ngôn ngữ dùng để lập trình gọi là ngôn ngữ lập trình. Có nhiều ngôn 
ngữ lập trình khác nhau. 

Dựa trên cơ sở của tính không phụ thuộc vào máy 

tính ngày càng cao người ta phân cấp các ngôn ngữ lập trình như sau:
- Ngôn ngữ máy (machine languge)
- Hợp ngữ (acsembly langguge) 
- Ngôn ngữ cấp cao (high level langguage)
Ngôn ngữ máy chỉ gồm các số 0 và 1, khó hiểu đối với người sử dụng. Mà 

ngôn ngữ tự nhiên của con người lại dài dòng nhiều chi tiết mập mờ, không rõ ràng 
đối với máy. 

Để con người giao tiếp được với máy dễ dàng cần một ngôn ngữ trung 

gian gần với ngôn ngữ tự nhiên. Vì vậy ta cần có một chương trình để dịch các 
chương trình trên ngôn ngữ này sang mã máy để có thể chạy được. Những chương 
trình làm nhiệm vụ như vậy gọi là các chương trình dịch. Ngoài ra, một chương 
trình dịch còn chuyển một chương trình từ ngôn ngữ nay sang ngôn ngữ khác tương 
đương. Thông thường ngôn ngưc nguồn là ngôn ngữ bậc cao và ngôn ngữ đích là 
ngôn ngữ bậc thấp, ví dụ như ngôn ngữ Pascal hay ngôn ngữ C sang ngôn ngữ 
Acsembly.

* Định nghĩa chương trình dịch:
Chương trình dịch 

là   một   chương   trình 
thực   hiện   việc   chuyển 
đổi   một   chương   trình 
hay  đoạn chương trình 
từ ngôn ngữ này (gọi là 
ngôn ngữ  nguồn)  sang 
ngôn ngữ khác (gọi là 
ngôn   ngữ   đích)   tương 
đương.

Để xây dựng được chương trình dịch cho một ngôn ngữ nào đó, ta cần biết về 

đặc tả của ngôn ngữ lập trình, cú pháp và ngữ nghĩa của ngôn ngữ lập trình đó… 
Để đặc tả ngôn ngữ lập trình, ta cần định nghĩa:

- Tập các kí hiệu cần dùng trong các chương trình hợp lệ.
- Tập các chương trình hợp lệ.

chương trình 

nguồn (ngôn 

ngữ bậc cao)

chương trình 

dịch

chương trình 

đích (ngôn 

ngữ máy)

Lỗi

Hình 1.1: Sơ đồ một chương trình dịch

background image

- Nghĩa của từng chương trình hợp lệ.
Việc định nghĩa tập các kí hiệu cần dùng của ngôn ngữ là dế dàng, ta chỉ cần 

liệt kê là đủ. Việc xác định các chương trình hợp lệ thì khó khăn hơn. Thông 
thường ta dùng các luật của văn phạm để đặc tả. Việc thứ 3, định nghĩa ý nghĩa của 
chương trình hợp lệ là khó khăn nhất. Có 3 phương pháp để xác định nghĩa của 
chương trình hợp lệ.

+ Phương pháp 1: định nghã bằng phép ánh xạ. ánh xạ mỗi chương trình vào 

một câu trong ngôn ngữ mà ta có thể hiểu được. 

+ Phương pháp 2: Xác định ý nghĩa của chương trình bằng một máy lý tưởng. 

Ý nghĩa của chương rình được đăc tả trong ngôn từ của máy lý tưởng. Máy lý 
tưởng là bộ thông dịch của ngôn ngữ.

+ Phương pháp 3: ý nghĩa cảu chương trình nguồn là sản phẩm xuất ra của 

trình biên dịch, khi nó dịch chương trình nguồn.

2. Phân loại chương trình dịch.

Có thể phân thành nhiều loại tuỳ theo các tiêu chí

 khác nhau.

- Theo số lần duyệt: Duyệt đơn, duyệt nhiều lần.

- Theo mục đích: Tải và chạy, gỡ rối, tối ưu, chuyển đổi ngôn ngữ, chuyển đôỉ 

định dạng…

- Theo độ phức tạp của chương trình nguồn và đích: 
+ Asembler (chương trình hợp dịch):

 Dịch từ ngôn ngữ asembly ra ngôn ngữ 

máy.

 

+  Preproccessor: (tiền xử lý) :

 Dịch từ ngôn ngữ cấp cao sang ngôn ngữ cấp 

cao khác (thực chất là dịch một số cấu trúc mới sang cấu trúc cũ).

+ Compiler: (biên dịch)

 dịch từ ngôn ngữ cấp cao sang ngôn ngữ cấp thấp.

- Theo phương pháp dịch chạy: 

+ Thông dịch: (diễn giải - interpreter)  chương trình thông dịch đọc chương 

trình nguồn theo từng lệnh và phân tích rồi thực hiện nó

. (Ví dụ hệ điều hành thực 

hiện các câu lệnh DOS, hay hệ quản trị cơ sở dữ liệu Foxpro)

.

  Hoặc ngôn ngữ 

nguồn không được chuyển sang ngôn ngữ máy mà chuyển sang một ngôn ngữ 
trung gian. Một chương trình sẽ có nhiệm vụ đọc chương trình ở ngôn ngữ trung 
gian này và thực hiện từng câu lệnh. Ngôn ngữ trung gian được gọi là ngôn ngữ của 
một máy ảo, chương trình thông dịch thực hiện ngôn ngữ này gọi là máy ảo. 

Chương 

trình 

nguồn

Compiler

CT ở NN 

trung gian

Interpreter

Kết 
quả

Hình 1.2 Hệ thống thông dịch

background image

Ví dụ hệ thông dịch Java. Mã nguồn Java được dịch ra dạng Bytecode. File 

đích này được một trình thông dịch gọi là máy ảo Java thực hiện. Chính vì vậy mà 
người ta nói Java có thể chạy trên mọi hệ điều hành có cài máy ảo Java.

+ Biên dịch:  toàn bộ chương trình nguồn được trình biên dịch chuyển sang 

chương trình đích ở dạng mã máy

. Chương trình đích này có thể chạy độc lập trên 

máy mà không cần hệ thống biên dịch nữa.

- Theo lớp văn phạm: LL (1) (LL – Left to right, leftmost) LR(1) (LR – letf to 

right, right most)  

1.3. Cấu trúc của chương trình dịch.

1.3.1. cấu trúc tĩnh (cấu trúc logic)

background image

1) Phân tích từ vựng: đọc luồng kí tự tạo thành chương trình nguồn từ trái 

sang phải, tách ra thành các từ tố (token).

- Từ vựng:

 Cũng như ngôn ngữ tự nhiên, ngôn ngữ lập trình cũng được xây 

dựng dựa trên 

bộ từ vựng

. Từ vựng trong ngôn ngữ lập 

trình thường được xây dựng 

dựa trên bộ chữ gồm có:

+ chữ cái: A .. Z, a . . z
+ chữ số: 0..9
+ các ký hiệu toán học: +, - , *, /, (, ), =, <,  >, !, %, /
+ các ký hiệu khác: [, ], . . . 

Các từ vựng được ngôn ngữ hiểu bao gồm các từ khóa, các tên hàm, tên hằng, tên 
biến, các phép toán, . . .

Các từ vựng có những qui định nhất định ví dụ: tên viết bởi chữ cái đầu tiên sau đó 
là không hoặc nhiều chữ cái hoặc chữ số, phép gán trong C là =,  trong Pascal là 
:=,v. . . 

Để xây dựng một chương trình dịch, hệ thống phải tìm hiểu tập từ vựng của 

ngôn ngữ nguồn và phân tích để biết được từng loại từ vựng và các thuộc tính của 

 

Ví dụ: 

Câu lệnh trong chương trình nguồn 

viết bằng ngôn ngữ pascal: 

“a := b + c * 60”

Chương trình phân tích từ vựng sẽ trả về:

 a là tên (tên (định danh ))
:= là toán tử gán
b là  tên (định danh) 
+  là toán tử cộng
c là định danh 
*  là toán tử nhân 
60 là một số
Kết quả phân tích từ vựng sẽ là:  (tên, a), phép gán, (tên, b)  phép cộng (tên, c) 

phép nhân, (số, 60) 

background image

2). Phân tích cú pháp: Phân tích cấu 

trúc ngữ pháp của chương trình. Các từ tố 
được nhóm lại theo cấu trúc phân cấp.

- Cú pháp: 

Cú pháp là thành phần 

quan trọng nhất trong một ngôn ngữ. Như 
chúng ta đã biết trong ngôn ngữ hình thức 
thì ngôn ngữ là tập các câu thỏa mãn văn 
phạm của ngôn ngữ đó. Ví dụ như 

câu = chủ ngữ + vị ngữ
vị ngữ = động từ + bổ ngữ
v.v. . . 

Trong ngôn ngữ lập trình, cú pháp của nó 
được thể hiện bởi một bộ luật cú pháp. Bộ 
luật   này   dùng   để   mô   tả   cấu   trúc   của 
chương trình, các câu lệnh.

 Chúng ta quan 

tâm đến các cấu trúc này bao gồm:

1) các khai báo
2) biểu thức số học, biểu thức logic
3) các   lệnh:   lệnh   gán,   lệnh   gọi   hàm, 

lệnh vào ra, . . .

4) câu lệnh điều kiện if  
5) câu lệnh lặp: for, while
6) chương trình con (hàm và thủ tục)

Nhiệm vụ trước tiên là phải biết được bộ luật cú pháp của ngôn ngữ mà mình định 
xây dựng chương trình cho nó.

Với một chuỗi từ tố và tập luật cú pháp của ngôn ngữ, bộ phân tích cú pháp tự 

động đưa ra cây cú pháp cho chuỗi nhập.

 Khi cây cú pháp xây dựng xong thì quá 

trình phân tích cú pháp của chuỗi nhập kết thúc thành công. Ngược lại nếu bộ phân 
tích cú pháp áp dụng tất cả các luật hiện có nhưng không thể xây dựng được cây cú 
pháp của chuỗi nhập thì thông báo rằng chuỗi nhập không viết đúng cú pháp.

Chương trình phải phân tích chương trình nguồn thành các cấu trúc cú pháp 

của ngôn ngữ, từ đó để kiểm tra tính đúng đắn về mặt ngữ pháp của chương trình 
nguồn. 

3

). Phân tích ngữ nghĩa

: Phân tích các đặc tính khác của chương trình mà 

không phải đặc tính cú pháp. Kiểm tra chương trình nguồn để tìm lỗi cú pháp và sự 
hợp kiểu.

Dựa trên cây cú pháp bộ phân tích ngữ nghĩa xử lý từng phép toán. Mỗi phép 

toán nó kiểm tra các toán hạng và loại dữ liệu của chúng có phù hợp với phép toán 
không.

background image

 

VD: tên (biến) được khai báo kiểu real, 60 là số kiểu interge vì vậy trình biên 

dịch đổi thành số thực 60.0.

Ngữ nghĩa: của một ngôn ngữ lập trình liên quan đến:

+ Kiểu, phạm vi của hằng và biến
+ Phân biệt và sử dụng đúng tên hằng, tên biến, tên hàm

Chương trình dịch phải kiểm tra được tính đúng đắn trong sử dụng các đại lượng 
này.

 Ví dụ kiểm tra không cho gán giá trị cho hằng, kiểm tra tính đúng đắn trong 

gán kiểu, kiểm tra phạm vi, kiểm tra sử dụng tên như tên không được khai báo 
trùng, dùng cho gọi hàm phải là tên có thuộc tính hàm, . . . 

4) 

Sinh mã trung gian: Sinh chương trình rong ngôn ngữ trung gian nhằm: dễ 

sinh và tối ưu mã hơn dễ chuyển đổi về mã máy hơn.

sau giai đoạn phân tích thì mã trung gian sinh ra như sau:

temp1 := 60
temp2 := id3 * temp1
temp3 := id2 + temp 2
id1 := temp3

(1.2)

(trong đó id1 là position; id2 là initial và id3 là rate)

5).

 Tối ưu mã: Sửa đổi chương trình trong ngôn ngữ trung gian hằm cải tién 

chương trình đích về hiệu năng.

Ví dụ như với mã trung gian ở (1.2), chúng ta có thể làm tốt hơn đoạn mã để 

tạo ra được các mã máy chạy nhanh hơn như sau:

temp1 := id3 * 60
id1 := id2 + temp1 (1.3)

6). 

Sinh mã: tạo ra chương trình đích từ chương trình trong ngôn ngữ trung 

gian đẫ tối ưu.

Thông thường là sinh ra mã máy hay mã hợp ngữ. Vấn đề quyết định là việc 

gán các biến cho các thanh ghi. 

Chẳng hạn sử dụng các thanh ghi R1 và R2, các chỉ thị lệnh MOVF, MULF, 

ADDF, chúng ta sinh mã cho (1.3) như sau:

MOVF id3, R2

MULF #60, R2
MOVF id2, R1
ADDF R2, R1
MOVF R1, id1

(1.4)

Ngoài ra, chương trình dịch còn phải thực hiện nhiệm vụ:

*   

Quản lý bảng ký hiệu:  Để ghi lại các kí hiệu, tên … đã sử dụng trong 

chương trình nguồn cùng các thuộc tính kèm theo như kiểu, phạm vi, giá trị ... để 
dùng cho các bước cần đến.

background image

Tõ tè(token) + Thuéc tÝnh (kiÓu, ®Þa chØ lu tr÷) = B¶ng ký 

hiÖu (Symbol table). 

T

rong quá trình phân tích từ vựng, các tên sẽ được lưu vào bảng ký hiệu, sau 

đó từ giai đoạn phân tích ngữ nghĩa các thông tin khác như thuộc tính về tên (tên 
hằng, tên biến, tên hàm) sẽ được bổ sung trong các giai đoạn sau.

- Giai đoạn phân tích từ vựng: lưu trữ trị từ vựng vào bảng kí hiệu nếu nó 

chưa có.

- Giai đoạn còn lại: lưu trữ thuộc tính của từ vựng hoặc truy xuất các thông 

tin thuộc tính cho từng giai đoạn.

Bảng kí hiệu được tổ chức như cấu trúc dữ liệu với mỗi phần tử là một mẩu 

tin dùng để lưu trữ trị từ vựng và các thuộc tính của nó.

- Trị từ vựng: tên từ tố.
- Các thuộc tính: kiểu, tầm hoạt động, số đối số, kiểu của đối số ...

VÝ dô: var position, initial, rate : real th× thuéc tÝnh kiÓu 

real cha thÓ x¸c ®Þnh. C¸c giai ®o¹n sau ®ã nh ph©n tÝch 

ng÷ nghÜa vµ sinh m· trung gian míi ®a thªm c¸c th«ng tin 
nµy vµo vµ sö dông chóng. Nãi chung giai ®o¹n sinh m· sÏ sö 

dông b¶ng ký hiÖu ®Ó gi÷ c¸c th«ng tin chi tiÕt vÒ danh 
biÓu.

* Xử lý lỗi: Khi phát hiện ra lỗi trong quá trình dịch thì nó ghi lại vị trí gặp 

lỗi, loại lỗi, những lỗi khác có liên quan đến lỗi này để thông báo cho người lập 
trình.

 

Mçi giai ®o¹n cã thÓ cã nhiÒu lçi, tïy thuéc vµo tr×nh biªn 
dÞch

 

 

 

thÓ

 

lµ:

-   Dõng   vµ   th«ng   b¸o   lçi   khi   gÆp   lçi   dÇu   tiªn   (Pascal).
- Ghi nhËn lçi vµ tiÕp tôc qu¸ tr×nh dÞch (C).
+ Giai ®o¹n ph©n tÝch tõ vùng: cã lçi khi c¸c ký tù kh«ng thÓ 

ghÐp thµnh mét token (vÝ dô: 15a, a@b,...)

+ Giai ®o¹n ph©n tÝch có ph¸p: Cã lçi khi c¸c token kh«ng 

thÓ kÕt hîp víi nhau theo cÊu tróc ng«n ng÷ (vÝ dô: if stmt then 
expr).

+ Giai ®o¹n ph©n tÝch ng÷ nghÜa b¸o lçi khi c¸c to¸n h¹ng cã 

kiÓu kh«ng ®óng yªu cÇu cña phÐp to¸n.

* Giai đoạn phân tích có đầu vào là ngôn ngữ nguồn, đầu ra là ngôn ngữ trung 

gian gọi là kỳ trước (fron end). Giai đoạn tổng hợp có đầu vào là ngôn ngữ trung 
gian và đầu ra là ngô ngữ đích gọi là kỳ sau (back end). 

background image

Đối với các ngôn ngữ nguồn, ta chỉ cần quan tâm đến việc sinh ra mã trung 

gian mà không cần biết mã máy đích của nó. Điều này làm cho công việc đơn giản, 
không phụ thuộc vào máy đích. Còn giai đoạn sau trở nên đơn giản hơn vì ngôn 
ngữ trung gian thường thì gần với mã máy. Và nó còn thể hiện ưu điểm khi chúng 
ta xây dựng nhiều cặp ngôn ngữ. Ví dụ có n ngôn ngữ nguồn, muốn xây dựng 
chương trình dịch cho n ngôn ngữ này sang m ngôn ngữ đích thì chúng ta cần n*m 
chương trình dịch; còn nếu chúng ta xây dựng theo kiến trúc front end và back end 
thì chúng ta chỉ cần n+m chương trình dịch.

1.3.2. Cấu trúc động.   

Cấu trúc động (cấu trúc theo thời gian) cho biết quan hệ giữa các phần khi 

hoạt động.

Các thành phần độc lập của chương trình có thể hoạt động theo 2 cách: lần 

lượt hay đồng thời. mỗi khi một phần nào đó của chương trình dịch xong toàn bộ 
chương trình nguồn hoặc chương trình trung gian thì ta gọi đó là một lần duyệt.

Duyệt đơn (duyệt một lần): một số thành phần của chương trình được thực 

hiện đồng thời

. Bộ phân tích cú pháp đóng vai trò trung tâm, điều khiển cả chương 

trình. Nó gọi bộ phân tích từ vựng khi cần một từ tố tiếp theo và gọi bộ phân tích 
ngữ nghĩa khi muốn chuyển cho một cấu trúc cú pháp đã được phân tích. Bộ phân 
tích ngữ nghĩa lại đưa cấu trúc sang phần sinh mã trung gian để sinh ra các mã 

trong một 

ngôn 

ngữ trung 

gian 

rồi   đưa 

vào 

bộ   tối   ưu 

và 

sinh mã.

Phân tích 

từ vựng

Chương trình nguồn

Phân tích 

cú pháp

Phân tích

ngữ nghĩa

Sinh mã trung gian

Tối ưu mã

Sinh mã

Chương trình đích

Phân tích từ vựng

Phân tích cú pháp

Phân tích ngữ nghĩa

Sinh mã trung gian

Tối ưu mã

Sinh mã đích

mã đích

Mã nguồn

background image

Chương trình dịch duyệt đơn 

Chương trình dịch duyệt nhiều lần

* Duyệt nhiều lần: các thành phần trong chương trình được thực hiện lần lượt 

và độc lập với nhau. Qua mỗi một phần, kết quả sẽ được lưu vào thiết bị lưu trữ 
ngaòi để lại được đọc vào cho bước tiếp theo.

Người ta chỉ muốn có một số ít lượt bởi vì mỗi lượt đều mất thời gian đọc và 

ghi ra tập tin trung gian. Ngược lại nếu gom quá nhiều giai đoạn vào trong một lượt 
thì  phải duy trì toàn bộ chương trình trong bộ nhớ, vì 1 giai đoạn  cần thông tin 
theo  thứ tự khác với thứ tự nó được tạo ra. Dạng biểu diễn trung gian của chương 
trình lớn hơn nhiều so với ct nguồn hoặc ct  đích, nên sẽ gặp vấn đề về bộ nhớ.

Ưu và nhược điểm của các loại:

Trong giáo trình này 

chúng ta nghiên cứu các 
giai   đoạn   của   một 
chương   trình   dịch   một 
cách riêng rẽ nhưng theo 
thiết kế duyệt một lượt.

1.4. Môi trường biên dịch

Chương trình dịch là 1 chương trình trong hệ thống liên hoàn giúp cho người 

lập trình có được một môi trường hoàn chỉnh để phát triển các ứng dụng của họ. 

Chương trình dịch trong hệ thống đó thể hiện trong sơ đồ sau:

So sánh

duyệt đơn

duyệt nhiều lần

tốc độ

tốt

Kém

bộ nhớ

kém

tốt

độ phức tạp

kém

tốt

Các ứng dụng lớn

Kém 

tốt

background image

Hình 1.3: Hệ thống xử lý ngôn ngữ

* Bộ tiền xử lý:

Chuỗi kí tự nhập vào chương trình dịch là các kí tự của chương trình nguồn 

nhưng trong thực tế, trước khi là đầu vào của một chương trình dịch, toàn bộ file 
nguồn sẽ được qua một thậm chí một vài bọo tiền xử lý

.  Sản phẩm của các bộ tiền 

xử lý này mới là chương trình nguồn thực sự của chương trình dịch

. Bộ tiền xử lý 

sẽ thực hiện các công việc sau:

- Xử lý Macro: Cho phep người dùng định nghĩa các macro là cách viết tắt của 

các cấu trúc dài hơn.

- Chèn tệp tin: Bổ sung nội dung của các tệp tin cần dùng trong chương trình. 

Ví dụ : Trong ngôn ngữ Pascal có khai báo thư viện 

Tiền xử lý

Chương trình 

dịch

Chương trình nguồn

Chương trình nguồn nguyên thủy

Assembler

Chương trình đích hợp ngữ

Mã máy định vị lại được

Tải / Liên kết

Thư viện và 

các file đối 

tượng định vị 

lại được

Mã máy thật sự

background image

“Uses crt;”

bộ tiền xử lý sẽ chền tệp tin crt vào thay cho lời khai báo.

- Bộ xử lý hoà hợp: hỗ trợ những ngôn ngữ xưa hơn bằng các cấu trúc dữ liệu 

hoặc dòng điều khiển hiện đại hơn.

- Mở rộng ngôn ngữ: gia tăng khả năng của ngôn ngữ bằng các macro có sẵn.

* Trình biên dịch hợp ngữ: Dịch các mã lệnh hợp ngữ thành mã máy. 

* Trình tải/ liên kết: 

Trình tải nhận các max máy khả tải định vị, thay đổi các địa chỉ khả tải định 

vị, đặt các chỉ thị và dữ liệu trong bộ nhớ đã được sửa đổi vào các vik trí  phù hợp.

Trình liên kết cho phép tạo ra một hcương rình từ các tệp tin thư viện hoặc 

nhiều tệp tin mã máy khả tải định vị mà chúng là kết quả của những biên dịch khác 
nhau.

background image

CHƯƠNG 2

          PHÂN TÍCH TỪ VỰNG

1. Vai trò của bộ phân tích từ vựng.

1.1. Nhiệm vụ.
Bộ phân tích từ vựng có nhiệm vụ là đọc các kí tự vào từ văn bản chương 

trình nguồn và phân tích đưa ra danh sách các từ tố (từ vựng và phân loại cú pháp 
của nó) cùng một số thông tin thuộc tính. 

Đầu ra của bộ phân tích từ vựng là danh sách các từ tố và là đầu vào cho phân 

tích cú pháp. Thực tế thì phân tích cú pháp sẽ gọi lần lượt mỗi từ tố từ bộ phân tích 
để xử lý, chứ không gọi một lúc toàn bộ danh sách từ tố của cả chương trình nguồn

Khi nhận được yêu cầu lấy một từ tố tiếp theo từ bộ phân tích cú pháp, bộ 

phân tích từ vựng sẽ đọc kí tự vào cho dến khi đưa ra được một từ tố.  

1.2. Quá trình phân tích từ vựng
1). Xóa bỏ kí tự không có nghĩa 

(các chú thích, dòng trống, kí hiệu xuống dòng, 

kí tự trống không cần thiết)

Quá trình dịch sẽ xem xét tất cả các ký tự trong dòng nhập nên những ký tự 

không có nghĩa (khoảng trắng (blanks, tabs, newlines) hoặc lời chú thích phải bị bỏ 
qua. Khi bộ phân tích từ vựng bỏ qua các khoảng trắng này thì bộ phân tích cú 
pháp không bao giờ quan tâm đến nó nữa.

2). Nhận dạng các kí hiệu: nhận dạng các từ tố.

Phân tích

từ vựng

Phân tích

cú pháp

yêu cầu lấy từ tố 

tiếp theo

 từ tố 

 chương trình 

nguồn

Bảng ký hiệu

Hinh 2.4: Sơ đồ phân tích từ tố

background image

Ví dụ ghép các chữ số để được một số và sử dụng nó như một đơn vị trong 

suốt quá trình dịch. Đặt num là một token biểu diễn cho một số nguyên. Khi một 
chuỗi các chữ số xuất hiện trong dòng nhập thì bộ phân tích sẽ gửi cho bộ phân tích 
cú pháp num. Giá trị của số nguyên đã được chuyển cho bộ phân tích cú pháp như 
là một thuộc tính của token num. 

3). Số hoá các kí hiệu: Do con số xử lý dễ dàng hơn các xâu, từ khoá, tên, nên 

xâu thay bằng số, các chữ số được đổi thành số thực sự biểu diễn trong máy. Các 
tên được cất trong danh sách tên, các xâu cất trong danh sách xâu, các chuỗi số trong 
danh sách hằng số.

1.2. Từ vị (lexeme), từ tố (token), mẫu (patter).
* Từ vị: là một nhóm các kí tự kề nhau có thể tuân theo một quy ước (mẫu hay 

luật) nào đó.

* Từ tố: là một thuật ngữ chỉ các từ vựng có cùng ý nghĩa cú pháp (cùng một 

luật mô tả).

- Đối với ngôn ngữ lập trình thì từ tố có thể được phân vào các loại sau:

+ từ khoá 
+ tên của hằng, hàm, biến
+ số
+ xâu ký tự
+ các toán tử
+ các ký hiệu.

Ví dụ:  position := initial + 10 * rate ;

 

ta có các từ vựng

position, :=, initial, +, 10, *, rate, ;

trong đó position, initial, rate là các từ vựng có cùng ý nghĩa cú pháp là các tên.

:= 

là phép gán 

+

là phép cộng

là phép nhân

10

là một con số

;

là dấu chấm phẩy

Như vậy trong câu lệnh trên có 8 từ vựng thuộc 6 từ tố.

Phân tích cú pháp sẽ làm việc trên các từ tố chứ không phải từ vựng, ví dụ như 

là làm việc trên khái niệm một số chứ không phải trên 5 hay 2; làm việc trên khái 
niệm tên chứ không phải là a, b hay c. 

* Thuộc tính của từ tố: 

Một từ tố có thể ứng với một tập các từ vị khác nhau, ta buộc phải thêm một số thông tin  

nữa để khi cần có thể biết cụ thể đó là từ vị nào. Ví dụ: 15 và 267 đều là một chuỗi số có từ tố là  
num nhưng đến bộ sinh mã phải biết cụ thể đó là số 15 và số 267. 

background image

Thuộc tính của từ tố là những thông tin kết hợp với từ tố đó. Trong thực tế, 

một từ tố sẽ chứa một con trỏ trỏ đến một vị trí trên bảng kí hiệu có chứấcc thông 
tin về nó.

Ví dụ:  position := initial + 10 * rate ;                  ta nhận được dãy từ tố:

<

tên, con trỏ trỏ đến position trên bảng kí hiệu>

<phép gán, >
<tên, con trỏ trỏ đến initial trên bảng kí hiệu>
<phép cộng, >
<tên, con trỏ trỏ đến rate trên bảng kí hiệu>
<phép nhân>
<số nguyên, giá trị số nguyên 60>

* Mẫu (luật mô tả - patter): Để cho bộ phân tích từ vựng nhận dạng được các 

từ tố, thì đối với mỗi từ tố chúng ta phải mô tả đặc điểm để xác định một từ vựng 
có thuộc từ tố đó không, mô tả đó được gọi là mẫu từ tố hay luật mô tả. 

Token

Trị từ vựng

MÉu (luËt m« t¶)

const 
if  

quan hÖ 
(relation)

tªn (id) 
Sè (num)

X©u (literal)

const 
if  

<,<=,=,<>,>,>
=

pi, count, d2
3.1416, 0, 5

"hello"

const 

if  
< hoÆc <= hoÆc =hoÆc <> hoÆc 

<> hoÆc > hoÆc >= 
më ®Çu lµ ch÷ c¸i theo sau  lµ ch÷ 

c¸i, ch÷ sè 
bÊt kú h»ng sè nµo

bÊt kú c¸c character n»m gi÷a " vµ " 
ngo¹i trõ "

Ta có thể coi: từ vị giống các từ cụ thể trong từ điển như nhà, cửa… từ tố gần giống khái  

niệm từ loại như danh từ động từ… Các mẫu (luật mô tả) dùng để nhận dạng loại từ tố, giống 
như những quy định để nhận dạng một từ là danh từ hay động từ…

Trị từ vựng được so cùng với mẫu của từ tố là chuỗi kí tự và là đơn vị của từ 

vựng.  Khi đọc chuỗi kí tự của chương trình nguồn bộ phân tích từ vựng sẽ so sánh 
chuỗi kí tự đó với mẫu của từ tố nếu phù hợp nó sẽ đoán nhận được từ tố đó và đưa 
từ tố vào bảng kí hiệu cùng với trị từ vưng của nó.

1.4. Cách lưu trữ tạm thời chương trình nguồn.

Việc đọc từng kí tự trong chương trình nguồn tốn một thời gian đáng kể nên nó ảnh hưởng 

tới tốc độ chương trình dịch. Để giải quyết vấn đề này, 

thiết kế đọc vào một lúc một chuỗi 

kí tự lưu trữ vào vùng nhớ tạm buffer

. Nhưng việc đọc như vậy gặp khó khăn do không thể  

xác định được một chuỗi như thế nào thì chứa chọn vẹn 1 từ tố. 

Và phải phân biệt được một 

chuỗi như thế nào thì chứa chọn vẹn một từ tố.

Có 2 phương pháp giải quyết như sau:

1. Cặp bộ đệm (buffer pairs)

background image

* Cấu tạo:
- Chia buffer thành 2 nửa, một nửa chứa n kí tự ( n = 1024, 4096, …).
- Sử dụng 2 con trỏ dò tìm trong buffer:
p1: (lexeme_ beginning) Đặt tại vị trí đầu của một từ vị.
p2: (forwar):di chuyển trên từng kí tự trong buffer để xác định từ tố.

  

E   =

 M   *

  C

  *   *      2

 EOF

 

 

* Hoạt động:
- Đọc n kí tự vào nửa đầu của buffer, 2 con trỏ trùng nhau tại vị trí bắt đầu.
- Con trỏ p2 tiến sang phải cho tới khi xác định được một từ tố có từ vị là 

chuỗi kí tự nằm giữa 2 con trỏ. Dời p1 lên trùng với p2, tiếp tục dò tìm từ tố mới.

- khi p2 ở cuối nửa đầu của buffer thì đọc tiếp n kí tự vào nửa đầu thứ 2. Khi 

p2 nằm ở nửa cuối của buffer thì đọc tiếp n kí tự vào nửa đầu của buffer và p2 được 
dời về đầu của bộ đệm.

- Nếu số kí tự trong chương trình nguồn còn lại ít hơn n thì một kí tự đặc biệt 

được đưa vào buffer sau các kí tự vừa đọc để báo hiệu chương trình nguồn đã được 
đọc hết.

* Giải thuật hình thức

     if    p2 ở cuối nửa đầu     then

      begin

Đọc vào nửa cuối. p2 := p2 + 1;

         end
     else    if   p2 ở cuối của nửa thứ hai     then
        begin

Đọc vào nửa đầu. p2 := p2 + 1;

         end
     else     p2 := p2 + 2

2. Phương pháp cầm canh.
Phương pháp trên mỗi lần di chuyển p2 phải kiểm tra xem có phải đã hết một 

nửa buffer chưa nên kém hiệu quả vì phải 2 lần test. Khắc phục:

- Mỗi lần chí đọc n-1 kí tự vào mỗi nửa buffer còn kí tự thứ n là kí tự đặc 

biệt (thường là EOF). Như vậy ta chỉ cần một lần test.

E = M * EOF

C * * 2 EOF

EOF

Giải thuật:

p2 := p2 + 1;

   if     p2( = eof     then

begin
     if     p2 ở cuối của nửa đầu     then

begin    Đọc vào nửa cuối;  p2 := p2 + 1    end

     else    if    p2 ở cuối của nửa cuối    then

begin    Đọc vào nửa đầu;  Dời p2 vào đầu của nửa đầu    end

     else  /* eof ở giữa chỉ hết chơng trình nguồn */

kết thúc phân tích từ vựng

end

background image

2. XÁC ĐỊNH TỪ TỐ.

2.1. Biểu diễn từ tố

Cách biểu diễn các luật đơn giản nhất là biểu diễn bằng lời. Tuy nhiên cách này thường 

gặp hiện tượng nhập nhằng ( cùng một lời nói có thể hiểu theo nhiều nghĩa khác nhau), phát biểu  
theo nhièu cách khác nhau khó đưa vào máy tính. Các từ tố khác nhau có các mẫu hay luật mô tả  
khác nhau. Các mẫu này là cơ sở để nhận dạng các từ tố. Ta cần thiết phải hình thức hoá các 
mẫu này để làm sao có thể lập trình được. Việc này có thể thực hiện được nhờ biểu thức chính  
qui và ôtômát hữu hạn. Ngoài ra ta có thể dùng cách biểu diễn trực quan của văn phạm phi ngữ 
cảnh là đồ thị chuyển để mô tả các loại từ tố.

2.1.1. Một số khái niệm về ngôn ngữ hình thức.
2.1.1.1. Kí hiệu, Xâu, ngôn ngữ. 

* Bảng chữ cái:  là một tập 

Σ

 

 

 hữu hạn hoặc vô hạn các đối tượng. Mỗi 

phần tử a 

∈Σ

 gọi là kí hiệu hoặc chữ cái (thuộc bảng chữ cái 

Σ

).

* Xâu: Là một dãy liên tiếp các kí hiệu thuộc cùng một bảng chữ cái.
- Độ dài xâu: là tổng vị trí của tất cả các kí hiệu có mặt trong xâu, kí hiệu là |

w|.

- Xâu rỗng: là từ có độ dài = 0 kí hiệu là 

ε

 hoặc 

. Độ dài của từ rỗng = 0.

- Xâu v là Xâu con của w nếu v được tạo bởi các ký hiệu liền kề nhau trong  w.

* Tập tất cả các từ trên bảng chữ cái 

Σ

 kí hiệu là 

Σ

*

. Tập tất cả các từ khác 

rỗng trên bảng chữ cái 

Σ

 kí hiệu là 

Σ

+

.         

Σ

*

 

 = 

Σ

+

 

 {

ε

}

* Tiền tốcủa một xâu là một xâu con bất kỳ nằm ở đầu xâu. Hậu tố của một 

xâu là xâu con nằm ở cuối xâu. 

(Tiền tố và hậu tố của một xâu khác hơn chính xâu đó 

ta gọi là tiền tố và hậu tố thực sự

)

Ngôn ngữ: Một ngôn ngữ L là một tập các chuỗi của các ký hiệu từ một bộ 

chữ cái 

Σ

 nào đó. 

(Một tập con A 

 

Σ

*

 được gọi là một ngôn ngữ trên bảng chữ cái 

Σ

).

- Tập rỗng được gọi là ngôn ngữ trống (hay ngôn ngữ rỗng). Ngôn ngữ rỗng là 

ngôn ngữ trên bất kỳ bảng chữ cái nào.

 (Ngôn ngữ rỗng khác ngôn ngữ chỉ gồm từ rỗng:  

ngôn ngữ 

 không có phần tử nào trong khi ngôn ngữ {

ε

} có một phần tử là chuỗi rỗng 

ε

)

* Các phép toán trên ngôn ngữ. 
+ Phép giao:  L = L

1

 

 L

2

 = {x 

∈Σ

*

 | x

L

1

 hoặc x 

L

2

}

+ Phép hợp:  L = L

1

 

 L

2

 = {x 

∈Σ

*

 | x

L

1

 và x 

L

2

}

+ Phép lấy phần bù của ngôn ngữ L là tập CL = { x 

∈Σ

*

 | x 

L}

 

Phép nối kết (concatenation) của hai ngôn ngữ L

1

Σ

1

 và L

2

/

Σ

2

  là :

L

1

L

2

 = {w

1

w

2

 

|

 w

1

 L

1

 và w

2

 

 L

2

 }/ 

Σ

1

 

 

Σ

2

background image

Ký hiệu L

n

 = L.L.L…L (n lần).  L

i

 = LL

i - 1

.

- Trường hợp đặc biệt : L

0

 = {

ε

}, với mọi ngôn ngữ L.

+ Phép bao đóng (closure) :  

+ Bao đóng (Kleene) của ngôn ngữ L, ký hiệu L

*

 là hợp của mọi tập tích trên L:

        

L* = 

 

Ii= 0

 

Li

+ Bao đóng dương (positive) của ngôn ngữ L, ký hiệu L

+

 được định nghĩa là 

hợp của mọi tích dương trên L :

   

L: L

+

 = 

∞∪

i = 1

 L

I

2.1.1.2. Văn phạm.
* Định nghĩa văn phạm.  (văn phạm sinh hay văn phạm ngữ cấu)
- Là một hệ thống gồm bốn thành phần xác định G = (

Σ

, P, S), trong đó:

Σ

 : tập hợp các ký hiệu kết thúc (terminal).

 : tập hợp các biến hay ký hiệu chưa kết thúc (non terminal) (với 

Σ

 

 

 = 

)

: tập hữu hạn các quy tắc ngữ pháp được gọi là các sản xuất (production), 

mỗi sản xuất biểu diễn dưới dạng 

α

 

 

β

, với 

α

β

 là các chuỗi 

 (

Σ

 

 

)

*

S 

 

: ký hiệu chưa kết thúc dùng làm ký hiệu bắt đầu (start)

Quy ước:
- Dùng các chữ cái Latinh viết hoa (A, B, C, ...) để chỉ các ký hiệu trong tập biến 

.

- Các chữ cái Latinh đầu bảng viết thường (a, b, c, ...) chỉ ký hiệu kết thúc thuộc tập 

Σ

- Xâu  thường được biểu diễn bằng các chữ cái Latinh cuối bảng viết thường (x, y, z, ...). 

* Phân loại Chosmky.
- Lớp 0: là văn phạm ngữ cấu (Phrase Structure) với các luật sản xuất có dạng:

α -> β với α 

 V

+

, β 

 V

*

- Lớp 1: là văn phạm cảm ngữ cảnh (Context Sensitive) với các luật sản xuất 

có dạng:

α -> β với α 

 V

+

, β 

 V

*

 , |α| < |β|

- Lớp 2: là văn phạm phi ngữ cảnh (Context Free Grammar - CFG ) với các 

luật sản xuất có dạng: A -> α với A 

 N, α 

 V

*

 

- Lớp 3: là văn phạm chính qui (Regular Grammar) với luật sản xuất có dạng:

A -> a, A -> Ba hoặc A-> a, A-> aB với A, B 

 N và a 

 T

Các lớp văn phạm được phân loại theo thứ tự phạm vi biểu diễn ngôn ngữ giảm dần, lớp  

văn phạm sau nằm trong phạm vi của lớp văn phạm trước:

Lớp 0 

 Lớp 1 

 Lớp 2 

 Lớp 3

2.1.1.3. Văn phạm chính quy và biểu thức chính quy.
* Văn phạm chính quy:

Ví dụ 1: Tên trong ngôn ngữ Pascal là một từ đứng đầu là chữ cái, sau đó có thể là không 

hoặc nhiều chữ cái hoặc chữ số.

background image

Biểu diễn bằng BTCQ:  

tên -> chữ_cái (chữ_cái | chữ_số)

*

Biểu diễn bằng văn phạm chính qui:

Tên -> chữ_cái A;            

A -> chữ_cái A | chữ_số A | ε

* Biểu thức chính qui được định nghĩa trên bộ chữ cái 

 như sau:

ε

 là biểu thức chính quy, biểu thị cho tập {

ε

}

- a 

 

, a là biểu thức chính quy, biểu thị cho tập {a}

- Giả sử r là biểu thức chính quy biểu thị cho ngôn ngữ L(r), s là biểu thức 

chính quy, biểu thị cho ngôn ngữ L(s) thì:

+ (r)|(s) là biểu thứcchính quy biểu thị cho tập ngôn ngữ L(r) 

 L(s)

+ (r)(s) là biểu thức chính quy biểu thị cho tập ngôn ngữ L(r)L((s)
+ (r)* là biểu thức chính quy biểu thị cho tập ngôn ngữ L(r)*
Biểu thức chính quy sử dụng các ký hiệu sau:

|           là ký hiệu hoặc (hợp)
( )

là ký hiệu dùng để nhóm các ký hiệu

là lặp lại không hoặc nhiều lần

là lặp lại một hoặc nhiều lần

là lặp lại không hoặc một lần

Ví dụ 2:  Viết biểu thức chính qui và đồ thị chuyển để biểu diễn các xâu gồm các chữ số 0 

và 1, trong đó tồn tại ít nhất một xâu con “11”

Biểu thức chính qui: 

(0|1)*11(0|1)*

Biểu diễn biểu thức chính quy dưới dạng đồ  thị chuyển:

2.1.1.3. Ôtômát hữu hạn.

* Định nghĩa: Một Otomat hữu hạn đơn định là một hệ thống M = (∑, Q, 

δ

q

0

, F), trong đó:

∑ là một bộ chữ hữu hạn, gọi là bộ chữ vào

Q là một tập hữu hạn các trạng thái

q

0

 

 Q là trạng thái đầu

 Đồ thị chuyển đơn định

0

0|1

1
2

1

1

2

start

0

0

0

0|1

1
2

1

1

2

0|1

start

 Đồ thị chuyển không đơn định

background image

 Q là tập các trạng thái cuối

δ

là hàm chuyển trạng thái 

δ

 có dạng:

δ

:  Q x ∑ -> Q thì  M gọi là ôtômát mát đơn định (kí hiệu ÔHĐ).

δ

:  Q x ∑ -> 2

  thì M gọi là ôtômát không đơn định (kí hiệu ÔHK).

* Hình trạng: của một OHĐ là một xâu có dạng qx với q 

 Q là trạng thái 

hiện thời và x 

 ∑

*

 là phần xâu vào chưa được đoán nhận.

Ví dụ: ∑ = {0, 1}; Q = {q

0

, q

1

, q

2

}; q

0

 là trạng thái ban đầu; F={q

2

}.

Hàm chuyển trạng thái được mô tả như bảng sau:(ÔHK)

Hàm chuyển trạng thái ÔHĐ

2.1.1. Biểu diễn từ tố bằng biểu thức chính quy.
* Một số từ tố được mô tả bằng lời như sau:

- Tên là một xâu bắt đầu bởi một chữ cái và theo sau là không hoặc nhiều 

chữ cái hoặc chữ số

- Số nguyên bao gồm các chữ số
- Số thực có hai phần: phần nguyên và phần thực là xâu các chữ số và hai 

phần này cách nhau bởi dấu chấm

- Các toán tử quan hệ <, <=, >, >=, <>, =

* Mô tả các mẫu từ tố trên bằng biểu thức chính qui:

Tên từ tố 

 biểu thức chính quy biểu diễn từ tố đó.

- chữ_cái 

 A|B|C|…|Z|a|b|c|…|z

chữ_số   

 0|1||2|3|4|5|6|7|8|9

δ

0

1

Q

0

q

0

q

0

, q

1

Q

1

q

2

Q

2

q

2

q

2

δ

0

1

Q

0

q

0

 q

1

Q

1

q

0

q

2

Q

2

q

2

q

2

q

0

0|1

q

1

1

1

q

2

0|1

start

 Đồ thị chuyển không đơn định

 Đồ thị chuyển đơn định

q

0

0|1

q

1

1

1

q

2

start

0

0

background image

- Tên 

  

chữ_cái (chữ_cái | chữ_số)

*

  

- Số nguyên 

 (chữ_số)

+

- Số thực 

(chữ_số)

+

.(chữ_số)

- Toán tử quan hệ:

+ Toán tử bé hơn (LT):

<

+ Toán tử bé hơn hoặc bằng (LE):

<=

+ Toán tử lớn hơn (GT):

>

+ Toán tử lớn hơn hoặc bằng (GE):

>=

+ Toán tử bằng (EQ):

=

+ Toán tử khác (NE):

<>

2.1.2. Biểu diẽn từ tố bằng đồ thị chuyển.

Toán tử quan hệ:

0

1

2

<

=

3

4
*

>

5

=

LE

NE

LT

EQ

>

GE

GT

6

7

=

8
*

0

chữ_số

1

3
*

chữ_số

.

2

chữ_số

0

chữ_sô

1

2
*

chữ số

0

chữ_cái

1

2
*

chữ_cái

chữ_số

background image

Để xây dựng một chương trình nhận dạng tất cả các loại từ tố này, chúng ta 

phải kết hợp các đồ thị này thành một đồ thị duy nhất:

2.1.3. Biểu diễn bởi OHĐ

Với ví dụ trên chúng ta xây dựng ôtômát với các thông số như sau:
Q = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14}
F = {2,4,6,10,14}
q

0

 = 0

0

chữ_cái

1

2
*

chữ_số

chữ_cái

tên

chữ_số

3

4
*

chữ_số

khác

số nguyên

6
*

5

chữ_số

.

số thực

7

8

<

=

9

>

LE

NE

LT

10

*

1
1

=

EQ

1
2

1
3

14

*

=

>

GE

GT

background image

hàm chuyển trạng thái được mô tả bởi bảng sau:

chữ_cái

chữ_số

.

<

>

khác

0

1

3

lỗi

7

11

12

lỗi

1

1

1

2

2

2

2

2

3

4

3

5

4

4

4

4

5

6

5

6

6

6

6

6

7

10

10

10

10

8

9

10

12

14

14

14

14

13

14

14

Các trạng thái 

 F là trạng thái kết thúc

Các trạng thái có dấu * là kết thúc trả về ký hiệu cuối cho từ tố tiếp theo

2.2. Viết chương trình cho đồ thị chuyển.

2.2.1. Lập bộ phân tích từ vựng bằng phương pháp diễn giải đồ thị chuyển.

Đoạn chương trình mô tả việc nhận dạng từ tố bằng cách diễn giải đồ thị 

chuyển.

Chúng sẽ sử dụng các hàm sau:.

int IsDigit ( int c); // hàm kiểm tra một ký hiệu là chữ số
int IsLetter ( int c); // hàm kiểm tra một ký hiệu là chữ cái
int GetNextChar(); // hàm lấy ký tự tiếp theo

enum Token {IDENT, INTEGER, REAL, LT, LE, GT, GE, NE, EQ, ERROR};
// hàm này trả về loại từ tố
// từ vị nằm trong s
Token  GetNextToken(char *s)
{ int state=0;

int i=0; 
while(1)
{

int c=GetNextChar();
switch(state)
{

case 0:

if(IsLetter(c)) state=1;

background image

else if(IsDigit(c)) state=3;

                              else if(c==‘<’) state=7;

  else if(c==‘=’) state=11;

else if(c==‘>’) state=12;

else return ERROR;

s[i++]=c;

      break;
case 1:

if(IsLetter(c)||IsDigit(c)) state=1;
   else return ERROR;
break;

case 2:

s[i]=0; GetBackChar();

                                               return IDENT;

case 3:

if(IsLetter(c)) state=4;

else if(IsDigit(c)) state=3;
       else if(c==‘.’) state=5;

else return 4;

s[i++]=c;

break;

case 4:

s[i]=0; GetBackChar();
return INTEGER;

case 5:

if(IsDigit(c)) state=5;
else state=6;
s[i++]=0;
break;

case 6: 

s[i]=0; GetBackChar();
return REAL;

case 7:

if(c==‘=’) state=8;
else if(c==‘>’) state=9;
       else state=10;
s[i++]=c;
break;

case 8:

s[i]=0; 
return LE;

case 9:

s[i]=0; 
return NE;

background image

case 10:

s[i]=0; GetBackChar();
return LE;

case 11:

s[i]=0;
return EQ;

case 12:

if(c==‘=’) state=13;
else state=14;
s[i++]=c;
break;

case 13:

s[i]=0;
return GE;

case 14:

s[i]=0;
return GT;

}
if(c==0) break;

}// end while

}// end function

Nhận xét: 
Ưu điểm: chương trình dễ viết và trực quan đối với số lượng các loại từ tố là 

bé.

Nhược điểm: gặp nhiều khó khăn nếu số lượng loại từ tố là lớn, và khi cần bổ 

sung loại từ tố hoặc sửa đổi mẫu từ tố thì chúng ta lại phải viết lại chương trình.

Chú ý: Trong thực tế khi xây dựng bộ phân tích từ vựng, chúng ta phải nhận dạng các  tên  

trong chương trình trình nguồn, sau đó dựa vào bảng lưu trữ để phân biệt cụ thể các từ khoá đối  
với các tên.

2.2.2. Lập bộ phân tích từ vựng bằng bảng.

Để xây dựng chương trình bằng phương pháp này, điều cơ bản nhất là chúng ta phải xây  

dựng bảng chuyển trạng thái. Để tổng quát,

  thông tin của bảng chuyển trạng thái nên 

được lưu ở một file dữ liệu bên ngoài, như vậy sẽ thuận tiện cho việc chúng ta thay 
đổi dữ liệu chuyển trạng thái của ôtômát mà không cần quan tâm đến chương trình. 

Đối với các trạng thái không phải là trạng thái kết thúc thì chúng ta chỉ cần tra 

bảng một cách tổng quát sẽ biết được trạng thái tiếp theo, và do đó chúng ta chỉ cần 
thực hiện các trường hợp cụ thể đối với các trạng thái kết thúc để biết từ tố cần trả 
về là gì.

Giả sử ta có hàm khởi tạo bảng trạng thái là: int InitStateTable();
Hàm phân loại ký hiệu đầu vào (ký hiệu kết thúc): int GetCharType();
Khi đó đoạn chương trình sẽ được mô tả như dưới đây:

#define STATE_NUM 100

background image

#define TERMINAL _NUM 100
#define STATE_ERROR –1 // trạng thái lỗi
int table[STATE_NUM][TERMINAL_NUM] 
// ban đầu gọi hàm khởi tạo bảng chuyển trạng thái.
InitStateTable(); 
int GetNextChar(); // hàm lấy ký tự tiếp theo
enum Token {IDENT, INTEGER, REAL, LT, LE, GT, GE, NE, EQ, ERROR};
// hàm này trả về loại từ tố
// từ vị nằm trong s
Token  GetNextToken(char *s)
{

int state=0;
int i=0; 
while(1)
{

int c=GetNextChar();
int type=GetCharType(c);
switch(state)
{

case 2:

s[i]=0; GetBackChar();

return IDENT;

case 4:

s[i]=0; GetBackChar();

return INTEGER;

case 6: 

s[i]=0; GetBackChar();

return REAL;

case 8:

s[i]=0; 

return LE;

case 9:

s[i]=0; 

return NE;

case 10:

s[i]=0; GetBackChar();

return LE;

case 11:

s[i]=0;
return EQ;

case 13:

s[i]=0;
return GE;

case 14:

s[i]=0;
return GT;

case STATE_ERROR: return ERROR;
defaulf:

state=table[state][type];
s[i++]=c;

}
if(c==0) break;

}// end while

}// end function

background image

Nhận xét:
Ưu điểm:    
+ Thích hợp với bộ phân tích từ vựng có nhiều trạng thái, khi đó chương trình 

sẽ gọn hơn. 

 + Khi cần cập nhật từ tố mới hoặc sửa đổi mẫu từ tố thì chúng ta chỉ cần thay 

đổi trên dữ liệu bên ngoài cho bảng chuyển trạng thái mà không cần phải sửa 
chương trình nguồn hoặc có sửa thì sẽ rất ít đối với các trạng thái kết thúc. 

Nhược điểm: khó khăn cho việc lập bảng, kích thước bảng nhiều khi là quá 

lớn, và không trực quan.

3. XÁC ĐỊNH LỖI TRONG PHÂN TÍCH TỪ VỰNG.
Chỉ có rất ít lỗi được phát hiện trong lúc phân tích từ vựng, vì bộ phân tích từ 

vựng chỉ quan sát chương trình nguồn một cách cục bộ, không xét quan hệ cấu trúc 
của các từ với nhau.

Ví dụ: khi bộ phân tích từ vựng gặp xâu fi trong biểu thức

fi a= b then . . .

thì bộ phân tích từ vựng không thể cho biết rằng fi là từ viết sai của từ khoá if hoặc là một  

tên không khai báo. Nó sẽ nghiễm nhiên coi rằng fi là một tên đúng và trả về một từ tố tên. Chú ý 
lỗi này chỉ được phát hiện bởi bộ phân tích cú pháp.

Các lỗi mà bộ phân tích từ vựng phát hiện được là các lỗi về một từ vị không 

thuộc một loại từ tố nào, 

ví dụ như gặp từ vị 12xyz.

Bé xö lý lçi ph¶i ®¹t môc ®Ých sau:

- Th«ng b¸o lçi mét c¸ch râ rµng vµ chÝnh x¸c.
- Phôc håi lçi mét c¸ch nhanh chãng ®Ó x¸c ®Þnh lçi 

tiÕp theo.

- Kh«ng lµm chËm tiÕn tr×nh cña mét ch¬ng tr×nh 

®óng.

Khi gặp những lỗi có 2 cách xử lý:

+ Hệ thống sẽ ngừng hoạt động và báo lỗi cho người sử dụng. 
+ Bộ phân tích từ vựng ghi lại các lỗi và cố gắng bỏ qua chúng để hệ 

thống tiếp tục làm việc, nhằm phát hiện đồng thời thêm nhiều lỗi khác. Mặt khác, 
nó còn có thể tự sửa (hoặc cho những gợi ý cho những từ đúng đối với từ bị lỗi).

Cách khắc phục là:
- Xoá hoặc nhảy qua kí tự mà bộ phân tích từ vựng không tìm thấy từ tố (panic 

mode).

- Thêm kí tự bị thiếu.
- Thay một kí tự sai thành kí tự đúng.

background image

- Tráo 2 kí tự đứng cạnh nhau.

4. CÁC BƯỚC ĐỂ XÂY DỰNG BỘ PHÂN TÍCH TỪ VỰNG.
Các bước tuần tự nên tiến hành để xây dựng được một bộ phân tích từ vựng 

tốt, hoạt động chính xác và dễ cải tiến, bảo hành, bảo trì.

1) Xác định các luật từ tố, các luật này được mô tả bằng lời.
2) Vẽ đồ thị chuyển cho từng mẫu một, trước đó có thể mô tả bằng biểu 

thức chính qui để tiện theo dõi và chỉnh sửa, và dễ dàng cho việc dựng đồ thị 
chuyển.

3) Kết hợp các luật này thành một đồ thị chuyển duy nhất.
4) Chuyển đồ thị chuyển thành bảng.
5) Xây dựng chương trình.
6) Bổ sung thêm phần báo lỗi để thành bộ phân tích từ vựng hoàn chỉnh.

Bài tập

1. Phân tích các chương trình pascal và C sau  thành các từ tố và thuộc 

tính tương ứng.

a) pascal:

Function max(i,j:integer): Integer

; {Trả lại số lon nhất trong 2 số nguyên i, j }

Begin

If i>j then max:=i;
Else max:=j;

End;

B) C:

Int max(int i, int j)

/* Trả lại số lon nhất trong 2 số nguyên i, j*/

{return i>j?i:j;}

Hãy cho biết có bao nhiêu từ tố được đưa ra và chia thành bao nhiêu loại?

2. Phân tích các chương trình pascal và c sau  thành các từ tố và thuộc 

tính tương ứng.

a) pascal

var i,j;
begin

for i= 0 to 100 do j=i;

write(‘i=’, ‘j:=’,j);

end;

B) C:          

  Int i,j:

Main(void
{

background image

for (i=0; i=100;i++)

printf(“i=%d;”,i,”j=%d”,j= =i);

}

3. Mô tả các ngôn ngữ chỉ định bởi các biểu thức chính quy sau:

a. 0(0|1)*0              b.((

ε

|0)1*)*

4. Viết biểu thức chính quy cho: tên, số nguyên, số thực, char, string… trong 

pascal. Xây dựng đồ thị chuyển cho chúng. Sau đó, kết hợp chúng thành đồ thị 
chuyển duy nhất.

5. Dựng đồ thị chuyển cho các mô tả dưới đây.
a. Tất cả các xâu chữ cái có 6 nguyên âm a, e, i, o, u, y theo thứ tự. Ví dụ: 

“abeiptowwrungfhy” 

b. tất cả các  xâu số không có một số nào bị lặp.
c. tất cả các  xâu số có ít nhất một số nào bị lặp.
d. tất cả các xâu gồm 0,1, không chứa xâu con 011.

Bài tập thực hành

Bài 1: Xây dựng bộ phân tích từ vựng cho ngôn ngữ pascal chuẩn.
Bài 2: Xây dựng bộ phân tích từ vựng cho ngôn ngữ C chuẩn.

background image

CHƯƠNG 3

 PHÂN TÍCH CÚ PHÁP VÀ CÁC PHƯƠNG PHÁP 

PHÂN TÍCH CƠ BẢN.

1. MỤC ĐÍCH.
Phân tích cú pháp nhận đầu vào là danh sách các từ tố của chương trình nguồn 

thành các thành phần theo văn phạm và biểu diễn cấu trúc này bằng cây phân tích 
hoặc theo một cấu trúc nào đó tương đương với cây.

Bộ phân tích cú pháp nhận chuỗi các token từ bộ phân tích từ vựng và tạo 

ra cây phân tích cú pháp. Trong thực tế còn một số nhiệm vụ thu nhập thông tin 
về token vào bảng ký hiệu, thực hiện kiểm tra kiểu về phân tích ngữ nghĩa cũng 
như sinh mã trung gian. Các phần này sẽ được trình bày trong các chương kế.

2. HOẠT ĐỘNG CỦA BỘ PHÂN TÍCH.

2.1.Văn phạm phi ngữ cảnh.

2.1.1. Định nghĩa.

* Định nghĩaVăn phạm PNC (như trên). 
* Dạng BNF (Backus – Naur Form) của văn phạm phi ngữ cảnh
+ Các ký tự viết hoa: biểu diễn ký hiệu không kết thúc, (có thể thay bằng một 

xâu đặt trong dấu ngoặc < > hoặc một từ in nghiêng).

+ Các ký tự viết chữ nhỏ và dấu toán học: biểu diễn các ký hiệu kết thúc (có 

thể thay bằng một xâu đặt trong cặp dấu nháy kép “ ” hoặc một từ in đậm).

+ ký hiệu -> hoặc = là: ký hiệu chỉ phạm trù cú pháp ở vế trái được giải thích 

bởi vế phải.

+ ký hiệu | chỉ sự lựa chọn.

Ví dụ: 

<Toán hạng> = <Tên> | <Số> | “(” <Biểu thức> “)”

hoặc 

ToánHạng -> Tên | Số | ( BiểuThức 

Phân tích 

từ vựng

Phân tích 

cú pháp

Phân tích 

ngữ nghĩa

Chương 

trình nguồn

Bảng ký 

hiệu

từ tố

yêu cầu 

từ tố

background image

2.1.2. Đồ thị chuyển biểu diễn văn phạm phi ngữ cảnh:

- Các vòng tròn với ký hiệu 

bên trong biểu thị cho trạng thái. 
Các   chữ  trên  các   cung  biểu  thị 
cho ký hiệu vào tiếp theo. Trạng 
thái vẽ bằng một vòng tròn kép là 
trạng thái kết thúc. 
Nếu trạng thái kết thúc có dấu * 
nghĩa là ký hiệu cuối không 
thuộc xâu đoán nhận.

2.1.3. Cây suy dẫn.

2.1.3.1. Suy dẫn.
Cho văn phạm G=(T,N,P,S).
- Suy dẫn trực tiếp là một quan hệ hai ngôi ký hiệu => trên tập V* nếu αβγ là 

một xâu thuộc V* và β->δ là một sản xuất trong P, thì αβγ => αδγ.

- Suy dẫn k bước, ký hiệu là 

k

=>

 hay 

k

β

α

=>

nếu tồn tại dãy α

0

, α

1

, . . . , α

k

 sao 

cho: α = α

=> α

=> . . . => α

= β

- Xâu α suy dẫn xâu  nếu k>=0 và ký hiệu là 

*

β

α

=>

- Xâu α suy dẫn không tầm thường xâu β nếu k>0 và ký hiệu là 

+

=>

β

α

2.1.3.2. C

   â  y     ph

   â  n     t  í  ch

    (

  c  â  y     suy

      dẫn

   )  

* Định nghĩa: Cây phân tích trong một văn phạm phi ngữ cảnh G = (T,N,P,S) 

là một cây thỏa mãn các điều kiện sau:

1.

Mọi nút có một nhãn, là một ký hiệu trong (T 

 N 

 {ε})

2.

Nhãn của gốc là S

3.

Nếu một nút có nhãn X là một nút trong thì X 

 N

4.

Nếu nút n có nhãn X và các nút con của nó theo thứ tự trái qua phải có 

nhãn Y

1

, Y

2

, . . ., Y

k

 thì X->Y

1

Y

2

 . . . Y

k

 sẽ là một sản xuất 

 P

5.

Nút lá có nhãn thuộc T hoặc là ε

Suy dẫn trái nhất (nói gọn là suy dẫn trái), nếu ở mỗi bước suy dẫn, biến 

được thay thế là biến nằm bên trái nhất trong dạng câu. 

* Suy dẫn phải nhất: (nói gọn là suy dẫn phải), nếu ở mỗi bước suy dẫn, biến 

được thay thế là biến nằm bên phải nhất trong dạng câu.

2.1.3.3. Đệ qui

0

1

2
*

Start

chu cai

khac

chu
cai

chu
cai

Hình 2.1: Đồ thị chuyển cho từ tố Tên

background image

* Định nghĩa: Ký hiệu không kết thúc A của văn phạm gọi là đệ qui nếu tồn 

tại:                  

+

=>

β

α

A

A

 với α, β 

 V

+

Nếu  α  =  ε  thì A gọi là đệ qui 

trái.

Nếu  β  =  ε  thì A gọi là đệ qui 

phải.

Nếu α,β 

 ε thì A gọi là đệ qui 

trong.

* Có 2 loại dệ quy trái :

Loại ttrực tiếp:  có dạng   A 

 A

α

  ( A 

+

 A

α

 )

Loại gián tiếp: Gây ra do nhiều bước suy dẫn. 

(

VÝ dô: S 

Aa | b;   A

Ac | Sd;  S lµ ®Ö qui tr¸i v× S 

 Aa 

 Sda)

* Loại bỏ đệ qui trái: (loại bỏ suy dẫn A =>

+

 A

α

 )

- Giả sử có luật đệ qui trái A->A

α

 | 

β

 chúng ta thay các luật này bằng các 

luật:           A -> 

β

A’

A’ -> 

α

A’ | 

ε

- Tổng quát hoá lên ta có:
Nếu có các luật đệ qui trái:          A -> A

α

| A

α

| . . .| A

α

m

 | 

β

1

 | 

β

2

 | . . .| 

β

n

trong đó không 

β

i

 nào bắt đầu bằng một A . Thay các sản xuất này bởi các 

sản xuất:

A -> 

β

1

A’ | 

β

2

A’ | . . . | 

β

n

A’

A’ -> 

α

1

A’ | 

α

2

A’ | . . . | 

α

m

A’ | 

ε

Ví dụ2: Xét văn phạm biểu thức số học sau:

E -> E + T | T ;       
T -> T * F | F;           
 F -> ( E ) | id

Loại bỏ đệ qui trái trực tiếp cho các sản xuất của E rồi của T, ta được văn phạm  

mới không còn sản xuất có đệ qui trái như sau:

E -> TE’;       
 E’-> +TE’ | 

ε

;  

T -> FT’;        
T’ -> *FT’ | 

ε

;           

background image

 F -> (E) | id 

Qui tắc này loại bỏ được đệ qui trái trực tiếp nằm trong các sản xuất nhưng không loại bỏ 

được đệ qui trái nằm trong các dẫn xuất có hai hoặc nhiều bước. Qui tắc này cũng không loại bỏ  

được đệ qui trái ra khỏi sản xuất A->A.

Víi ®Ö qui tr¸i gi¸n tiÕp vµ nãi chung lµ ®Ö qui tr¸i, ta sö 
dông gi¶i thuËt sau: 

VÝ dô : Víi S 

→ 

Aa | b;  A

Ac | Sd.

Sắp xếp các ký hiệu cha kết thúc theo thứ tự S,A..
Với i=1, không có đệ qui trái trực tiếp nên không có điều gì xảy ra.
với i=2 , thay luật sinh AđSd được AđAc | Aad | bd.

Loại bỏ đệ qui trái trực tiếp cho A, ta được:   SđAa |b;  AđbdA'; A'đ cA' | adA' | e 

* Phép thừa số hoá trái
Thừa số hoá trái (left factoring) là một phép biến đổi văn phạm nhằm sinh ra 

một văn phạm thích hợp cho việc phân tích cú pháp không quay lui. Ý tưởng cơ 
bản là khi không rõ sản xuất nào trong trong hai sản xuất có cùng vế trái là A được 
dùng để khai triển A thì ta có thể viết lại các sản xuất này nhằm “hoãn lại quyết 
định”, cho đến khi có đủ thông tin để đưa ra được quyết định lựa chọn sản xuất 
nào.

- Nếu có hai sản xuất A -> 

α β

1

 | 

α β

2

 thì ta không biết phải khai triển A 

theo 

α β

1

 hay 

α β

2

. Khi đó, thay hai sản xuất này bằng: 

Input:    Văn phạm không tuần hoàn hoặc e_sx (không có dạng Aị

+

A hoặc Ađe)

Output:  Văn phạm tương đương không đệ qui trái 
Phương pháp:
1. Sắp xếp các ký hiệu không kết thúc theo thứ tự A

1

, A

2

.. .. A

n

2. For i:=1 to n do
      Begin
         for j:=1 to i-1 do 
            Begin
                Thay luật sinh dạng Aiđ Aj bởi luật sinh Ajđ d 

1g 

| d 

2g 

|.. .. |d 

kg 

                 Trong đó Aj đd

1g

 | d

2g

 |.. .. |d

ky

 là các luật sinh hiện tại

            End
         Loại bỏ đệ qui trái trực tiếp trong số các Ai loại 
    End;

background image

A -> 

α

A’;

A’ -> 

β

1

 | 

β

2

Ví dụ:  S -> iEtS | iEtSeS | a;  

E -> b

Khi được thừa số hoá trái, văn phạm này trở thành:

S -> iEtSS’ | a; S’ -> eS | 

ε

; E -> b

vì thế khi cần khai triển S với ký hiệu xâu vào hiện tại là i, chúng ta có thể lựa chọn iEtSS’  

mà không phải băn khoăn giữa iEtS và iEtSeS của văn phạm cũ.

Gi¶i thuËt t¹o thõa sè ho¸ tr¸i (yÕu tè tr¸i) cho mét v¨n ph¹m:

Input:             Văn phạm G
Output:          Văn phạm tương đương với nhân tố trái.
Ph

   ơng pháp:

 

  

Với mỗi ký hiệu chưa kết thúc A, có các ký hiệu dẫn đầu các vế phải giống nhau, ta 
tìm một chuỗi a là chuỗi có độ dài lớn nhất chung cho tất cả các vế phải (a là nhân 
tố trái)
Giả sử A  

 ab

1

| ab

2

|.. .. | ab

| g

Trong đó g không có chuỗi dẫn đầu chung với các vế phải khác. Biến đổi luật sinh 
thành
A  

 a A

'

 | g 

A' 

 b 

1

| b 

2

 | .. .. | b 

n

 

2.1.3.4. Nhập nhằng

Một văn phạm G được gọi là văn phạm nhập nhằng nếu có một xâu α là kết 

quả của hai cây suy dẫn khác nhau trong G. Ngôn ngữ do văn phạm này sinh ra gọi 
là ngôn ngữ nhập nhằng.

Ví dụ:

Xét văn phạm G cho bởi các sản xuất sau: S -> S + S | S * S | ( S ) | a

Với xâu vào là w = “a+a*a” ta có:

Văn phạm này là nhập nhằng vì có hai cây đối với câu vào w như sau:

S

S

*

S

S

+

S

a

a

a

S

S

+

S

S

*

a

a

a

S

background image

Chúng ta có ví dụ đối suy dẫn trái (đối với cây đầu tiên) là:

S => S * S => S + S => S + S * S => a + S * S => a + a * S => a + a * a

suy dẫn phải (đối với cây đầu tiên ) là:

S => S * S => S * a => S + S * a => S + a * a => a + a * a.

2.2. các phương pháp phân tích.

- Mọi ngôn ngữ lập trình đều có các luật mô tả các cấu trúc cú pháp. Một chương trình viết  

đúng phải tuân theo các luật mô tả này. Phân tích cú pháp  là để tìm ra cấu trúc dựa trên văn  

phạm của một chương trình nguồn.

- Thông thường 

có hai chiến lược phân tích

:

+ Phân tích trên xuống (topdown): Cho một văn phạm PNC G = (

Σ

, P, S) 

và một câu cần phân tích w. Xuất phát từ S áp dụng các suy dẫn trái, tiến từ trái qua 
phải thử tạo ra câu w.

+ Phân tích dưới lên (bottom-up): Cho một văn phạm PNC G = (

Σ

, P, S) 

và một câu cần phân tích w. Xuất phát từ câu w áp dụng thu gọn các suy dẫn phải, 
tiến hành từ trái qua phải để đi tới kí hiệu đầu S.

Theo cách này thì phân tích Topdown và LL(k) là phân tích trên xuống, phân tích Bottom-

up và phân tích LR(k) là phân tích dưới lên.

* Điều kiện để thuật toán dừng:
+ Phân tích trên xuống dừng khi và chỉ khi G kông có đệ quy trái.
+ Phân tích dưới lên dừng khi G không chứa suy dẫn A  

+

  A và sản xuất 

A

→ε

.

* Có các phương pháp phân tích.
1) Phương pháp phân tích topdown.
2) Phương pháp phân tích bottom up. 
3) Phương pháp phân tích bảng CYK.
4) Phương pháp phân tích LL.
5) Phương pháp phân tích LR.

Phương pháp 1 và 2: là các phương pháp cơ bản, kém hiệu quả. Phương pháp 5,6 là  

phương pháp phân tích hiệu quả

.

2.3.1. phân tích topdown.

background image

Phương pháp phân tích Top-down xây dựng cây phân tích cho một xâu vào bằng cách xuất  

phát từ ký hiệu bắt đầu làm gốc và sử dụng các luật sản xuất để đi từ gốc đến lá.

- Đánh dấu thứ tự các lựa chọn của các sản xuất có cùng vế trái.

Ví dụ nếu các sản xuất có dạng S -> aSbS | aS | c thì aSbS là lựa chọn thứ nhất, aS là lựa 

chọn thứ hai và c là lựa chọn thứ ba trong việc khai triển S.

-  Tại mỗi bước suy diễn, ta cần triển khai một ký hiệu không kết thúc A và văn phạm có  

các sản xuất có vế trái là A là A->

α

1

 | 

α

2

 | . . .| 

α

k

 Khi đó ta có k thứ tự lựa chọn, đánh dấu thứ  

tự lựa chọn các sản xuất sau đó khai triển A theo một lựa chọn, nếu quá trình phân tích là không  

thành công thì quay lui tại vị trí này và khai triển A theo lựa chọn tiếp theo. 

Phân tích Top-down là phương pháp phân tích có quay lui và tạo ra suy dẫn trái nhất.

Ví dụ: Cho văn phạm S -> aSbS | aS | c
Hãy phân tích xâu vào “aacbc” bằng thuật toán Top-down, vẽ cây phân tích 

trong quá trình phân tích quay  lui.

S

a

S

b

S

a

b

S

S

a

*

S

b

S

(1)

S

a

S

b

S

a

S

b

S

a

*

S

(2)

S

a

S

b

S

a

S

b

S

c

a

*

S

b

S

(3)

S

a

S

b

S

a

S

b

S

c

a

*

S

(4)

background image

S

a

S

b

S

a

S

b

S

c

c

*

(5)

S

a

S

b

S

a

S

a

*

S

b

S

(6)

S

a

S

b

S

a

S

a

*

S

(7)

background image

2.3.1.1. Mô tả thuật toán phân tích Top-down

- Input: Văn phạm PNC G = (

Σ

, P, S) không đệ quy trái, xâu w = a

1

, a

2

, … 

a

n

   

- Output: Cây phân tích từ trên xuống của xâu w (w 

 L(G)), báo lỗi (w 

∉ 

L(G)).

- Method:  
Dùng một con trỏ chỉ đến xâu vào w. Ký hiệu trên xâu vào do con trỏ chỉ đến 

gọi là ký hiệu vào hiện tại. 

1)

Khởi tạo cây với gốc là S, con trỏ trỏ đến kí hiệu đầu tiên của xâu w là a

1

.

2)

Nếu nút đang xét 

 

 (là ký hiệu không kết thúc) A thì chọn sản xuất 

có vế trái là A trong P, giả sử sản xuất A 

 X

1

...X

k

 . 

+ Nếu k > 0: lấy nút X

1

 làm nút đang xét. 

+ Nếu k=0 (sản xuất rỗng) thì lấy nút ngay bên phải A làm nút đang xét.

3)

Nếu nút đang xét 

 

Σ

 (là ký hiệu kết thúc) a thì đối sánh a với ký hiệu 

vào hiện tại.

+  Nếu trùng nhau: thì lấy nút ngay bên phải a làm nút đang xét, con trỏ 

dịch sang bên phải một ký hiệu trên xâu w.

+ Nếu không: quay lại nút trước đó và lặp lại b2 với thử lựa chọn tiếp 

theo. 

Thủ tục trên lặp lại sau hữu hạn bước và có 2 khả năng xảy ra:
- Nếu gặp trường hợp đối sánh hết xâu vào và cây không còn nút nào 

chưa xét nữa thì ta được một cây phân tích. 

- Nếu đã quay lui hết tất cả các trường hợp mà không sinh được cây phân tích thì 
kết luận xâu vào không phân tích được bởi văn phạm đã cho.

S

a

S

b

S

a

S

c

a

*

S

b

S

(8)

S

a

S

b

S

a

S

c

a

*

S

(9)

S

a

S

b

S

a

S

c

c

10

background image

* Điều kiện để một văn phạm phi ngữ cảnh phân tích được bởi thuật toán Top-

down là văn phạm không có đệ qui trái. 

(Vì vậy ta phải thực hiện loại bỏ đệ quy trái trước  

khi phân tích văn phạm theo  phương pháp topdown)

* Độ phức tạp thuật toán là hàm số mũ n với n là độ dài xâu vào.

2.3.2. phân ttích bottom - up.

Phương pháp phân tích Bottom-up về tư tưởng là ngược lại với phương pháp Top-down. 

- Xây dựng cú pháp cho xâu nhập bắt đầu từ lá lên tới gốc. Đây là quá trình rút 

gọn một xâu thành một kí hiệu mở đầu của văn phạm. Tại mỗi bước rút gọn, một 
xâu con bằng một xâu phải của một sản xuất nào đó thì xâu con này được thay thế 
bởi vế trái của sản xuất đó. (còn gọi là phương pháp gạt thu gọn -  shift reduce 
parsing).

Cã 2 vÊn ®Ò: x¸c ®Þnh handle vµ chän luËt sinh.

CÊu t¹o:
- 1 STACK ®Ó lu c¸c ký hiÖu v¨n ph¹m.
- 1 BUFFER INPUT ®Ó gi÷ chuçi cÇn ph©n tÝch w.
-  Dïng $ ®Ó ®¸nh dÊu ®¸y stack vµ cuèi chuçi nhËp.
* Ho¹t ®éng:
- Khëi ®Çu th× stack rçng vµ w n»m trong input buffer. Bé 

ph©n tÝch 

gạt lần lượt các ký hiệu đầu vào từ trái sang phải vào ngăn xếp đến khi nào 

đạt được một thu gọn thì thu gọn (thay thế vế phải xuất hiện trên đỉnh ngăn xếp bởi vế trái 
của sản xuất đó).

Nếu có nhiều cách thu gọn tại một trạng thái thì lưu lại cho quá 

trình quay lui. Quá trình cứ tiếp tụcnếu dừng lại mà chưa đạt đến trạng thái kết 
thúc thì quay lại tại bước quay lui gần nhất.

-  Nếu quá trình đạt đến trạng thái ngăn xếp là $S và xâu vào là $ thì quá trình 

kết thúc và phân tích thành công. 

- Nếu đã xét hết tất cả các trường hợp, tức là không quay lui được nữa mà 

chưa đạt đến trạng thái kết thúc thì dừng lại và thông báo xâu vào không phân tích 
được bởi văn phạm đã cho. 

Ví dụ:       S -> aABe; A -> Abc | b;   B -> d;        Phân tích câu vào “abbcde”

quá trình phân tích Bottom-up như sau:

Ngăn xếp

Đầu vào

Hành động

$

abbcde$

gạt

$a

bbcde$

gạt

$ab

bcde$

thu gọn A -> b

$aA

bcde$

gạt

$aAb

cde$

thu gọn A -> b (2)

background image

$aAA

cde$

gạt 

$aAAc

de$

gạt

$aAAcd

e$

thu gọn B -> d (1)

$aAAcB

e$

gạt

$aAAcBe

$

dừng, quay lui 1 (gạt)

$aAAcde

$

dừng, quay lui 2 (gạt)

$aAbc

de$

thu gọn A -> Abc

$aA

de$

gạt

$aAd

e$

thu gọn B -> d 

$aAB

e$

gạt

$aABe

$

thu gọn S -> aABe

$S

$

chấp nhận

Vẽ cây cho quá trình phân tích và quay lui trên, chúng ta có kết quả như sau:

Quá trình 1                                                                  Quá trình 2

Quá trình suy dẫn cũng có thể được viết lại như sau:

Abbcde  => aAbcde (A -> b)  => aAde (A -> Abc)  => aABe (B -> d) => S (S -> aABe)

Nếu viết ngược lại chúng ta sẽ được dẫn xuất phải nhất:
S =>rm aABe =>rm aAde =>rm aAbcde =>rm abbcde

- Quá trình phân tích Bottom-up là quá trình sinh dẫn suất phải nhất

a

b

b

c

d

e

A

A

B

*

a

b

b

c

d

e

A

A*

a

b

b

c

d

e

A

B

A

S

  (2c) Quá trình 3

background image

- Phân tích Bottom-up không phân tích được văn phạm có các sản xuất B->

ε  

hoặc có suy dẫnA =>+ A

   Handle của một chuỗi

 

 

Handle của một chuỗi là một chuỗi con của nó và là vế phải của một sản xuất 

trong phép thu gọn nó thành ký hiệu vế trái của 1 sản xuất.

Ví dụ: Trong ví dụ trên.

Ngăn 
xếp

Đầu vào

Hành động

Handle

Suy dẫn 

phải

Tiền   tố   khả 
tồn

$

abbcde$

gạt

$a

bbcde$

gạt

abbcde

a

$ab

bcde$

thu gọn A -> b

b

abbcde

ab 

$aA

bcde$

gạt

aAbcde

aA

$aAb

cde$

thu gọn A -> b (2)

b

aAbcde

aAb

$aAA

cde$

gạt 

$aAAc

de$

gạt

$aAAcd

e$

thu gọn B -> d (1)

d không phải là handle do áp dụng thu 
gọn này là không thành công 

$aAAcB

e$

gạt

$aAAcBe $

dừng, quay lui 1 (gạt)

$aAAcde $

dừng, quay lui 2 (gạt)

$aAbc

de$

thu gọn A -> Abc

Abc

AAbcde

$aA

de$

gạt

$aAd

e$

thu gọn B -> d 

d

AAde

$aAB

e$

gạt

$aABe

$

thu gọn S -> aABe

$S

$

chấp nhận

Chú ý Handle là chuỗi mà chuỗi đó phải là một kết quả của suy dẫn phải từ S 

và phép thu gọn xảy ra trong suy dẫn đó.

W = a

1

a

2...

a

n

Stack

β

α

a

i        

a

i+1           ... 

      a

n

      $   

Sản xuất A 

-> 

β

Trên ngăn xếp chứa xâu y =

 

α β

,  

β

 là vế phải của một sản xuất 

được  bộ  phân  tích  áp  dụng  để  thu  gọn  và  bước  thu  gọn  này  phải 
dẫn đến quá trình phân tích thành công thì  

β

 là handle của chuỗi 

α β

v (v là phần chuỗi còn lại trên input buffer).  

Vậy nếu S =>

*rm

 

α

Aw =>

rm

 

α β

w thì 

β

 là handle của 

suy dẫn phải 

α β

w

background image

 

Trong việc sử dụng ngăn xếp để phân tích cú pháp gạt thu gọn, handle luôn 

luôn xuất hiện trên đỉnh của ngăn xếp. 

* Tiền tố khả tồn (viable prefixes)
Xâu ký hiệu trong ngăn xếp tại mỗi thời điểm của một quá trình phân tích gạt - 

thu gọn là một tiền tố khả tồn. 

Ví dụ:  tại một thời điểm trong ngăn xếp có dữ liệu là 

α β

 và xâu vào còn lại là w thì  

α β

w là một dạng câu dẫn phải và 

α β

 là một tiền tố khả tồn.

2.3.2.Phân tích LL.

Tử tưởng của phương pháp phân tích LL là khi ta triển khai một ký hiệu 

không kết thúc, lựa chọn cẩn thận các sản xuất như thế nào đó để tránh việc quay 
lui mất thời gian

.Tức là phải có một cách nào đó xác định dực ngay lựa chọn đúng mà không 

phải thử các lựa chọn khác. Thông tin để xác định lựa chọn dựa vào những gì đã biết trạng thái và  

kí hiệu kết thúc hiện tại.

LL: là một trong các phương pháp phân tích hiệu quả, nó cũng thuộc chiến lược 

phân tích topdown nhưng nó hiệu quả ở chỗ nó là phương pháp phân tích không 
quay lui. 

- Bộ phân tích tất định: Các thuật toán phân tích có đặc điểm chung là xâu vào 

được quét từ trái sang phải và quá trình phân tích là hoàn toàn xác định, do đó ta 
gọi là bộ phân tích tất định. (Phân tích topdown và bottom – up có phải là phân tích 
tất định không? – không do quá trình phân tích là không xác định).

L

: left – to – right ( quét từ phải qua trái ) L : leftmosst – derivation (suy dẫn trái nhất)

k là số ký hiệu nhìn trước để đưa ra quyết định phân tích. 

Giả sử ký hiệu không kết thúc A có các sản xuất:  A -> 

α

1

 | 

α

2

 | . . . | 

α

n

 thoả mãn tính 

chấ:t các xâu 

α

1

α

2

, . . ., 

α

n

 suy dẫn ra các xâu với ký hiệu tại vị trí đầu tiên là các ký hiệu kết  

thúc khác nhau, khi đó chúng ta chỉ cần nhìn vào ký hiệu đầu vào tiếp theo  sẽ xác định được cần  
khai triển A theo 

α

i

 nào

Nếu cần tới k ký hiệu đầu tiên thì mới phân biệt được các xâu  

α

1

α

2

. . ., 

α

n

 thì khi đó để chọn luật sản xuất nào cho khai triển A chúng ta cần nhìn k ký hiệu đầu vào 

tiếp theo

Văn phạm LL(k) là văn phạm cho phép xây dựng bộ phân tích làm việc tất định 

nếu bộ phân tích này được phép nhìn k kí hiệu vào nằm ngay bên phải của vị trí vào 
hiện tại.

background image

Ngôn ngữ sinh ra bởi văn phạm LL(k) là ngôn ngữ LL(k). 

Thông thường chúng 

ta xét với k=1.

2.3.2.1. First và follow.

* First của một xâu:

First(

α

) cho chúng ta biết xâu 

α

 có thể suy dẫn đến tận cùng thành một xâu bắt đầu bằng 

ký hiệu kết thúc nào. 

Định nghĩa First(

 

 

α

    )  

First(

α

) là tập chứa tất cả các ký hiệu kết thúc a mà a có thể là bắt đầu của 

một xâu được suy dẫn từ 

α

+ First(

α

) = {a 

 T | 

α

 =>* a

β

 }

ε

 

 First(

α

) nếu 

α

 =>* 

ε

Thuật toán tính First(X) với X là một ký hiệu văn phạm:

1. nếu X là ký hiệu kết thúc thì First(X) = {X}

2.

nếu X -> 

ε

  là một sản xuất thì thêm 

ε

 vào First(X)

3.

nếu X -> Y

1

...Y

k

 là một sản xuất thì thêm First(Y

1

) vào First(X) trừ 

ε

nếu First(Y

t

) chứa 

ε

 với mọi t=1,...,i với i<k thì thêm First(Y

i+1

) vào 

First(X) trừ 

ε

. Nếu trường hợp i=k thì thêm 

ε

 vào First(X)

Cách tính First(

 

 

α

    )   với 

α

 là một xâu.

Giả sử 

α

= X

1

X

2

 . . . X

k

. Ta tính như bước 3 của thuật toán trên: 

1.

thêm First(X

1

) vào First(

α

) trừ 

ε

2.

nếu First(X

t

) chứa  

ε

  với mọi t=1,...,i với i<k thì thêm First(X

i+1

) vào 

First(

α

) trừ 

ε

. Nếu trường hợp i=k thì thêm 

ε

 vào First(

α

)

-   Tính First của các ký hiệu không kết thúc: lần lượt xét tất cả các sản 

xuất.Tại mỗi sản xuất, áp dụng các qui tắc trong thuật toán tính First để thêm các 
ký hiệu vào các tập First. Lặp lại và dừng khi nào gặp một lượt duyệt mà không bổ 
sung thêm được bất kỳ ký hiệu nào vào tập First và ta đã tính xong các tập First cho 
các ký hiệu

.

Ví dụ 1:
Cho văn phạm sau:           S -> AB;   A -> aA | 

ε

;   B -> bB | 

ε

Hãy tính First của các ký hiệu S, A, B
Kết quả:           Fisrt(A) = {a, 

ε

};   First(B) = {b,

ε

};    First(S) = {a,b,

ε

}

background image

* Follow của một ký hiệu không kết thúc:

Định nghĩa follow(A) A là kí hiệu không kết thúc.

Follow(A) với A là ký hiệu không kết thúc là tập các ký hiệu kết thúc a mà 

chúng có thể xuất hiện ngay bên phải của A trong một số dạng câu. Nếu A là ký 
hiệu bên phải nhất trong một số dạng câu thì thêm $ vào Follow(A).

+ Follow(A) = {a

T | 

 S =>* 

α

Aa

β

 }

+ $ 

 Follow(A) khi và chỉ khi tồn tại suy dẫn S =>* 

α

A

Thuật toán tính Follow(A)  với A là một ký hiệu không kết thúc

1.

thêm $ vào Follow(S) với S là ký hiệu bắt đầu (

chú ý là nếu ta xét một tập  

con với một ký hiệu E nào đó làm ký hiệu bắt đầu thì cũng thêm $ vào Follow(E)).

2.

nếu có một sản xuất dạng B->

α

A

β

 và 

β ≠ ε

 thì thêm các phần tử 

trong First(

β

) trừ 

ε

 vào Follow(A).

thật vậy: nếu a 

 First(

β

) thì tồn tại 

β

=>*a

γ

, khi đó, do có luật B->

α

A

β

 nên 

tồn tại S =>* 

α

1

B

β

1

 => 

α

1

α

A

β β

1

=>

α

1

α

Aa

γ β

1

   Theo định nghĩa của Follow  

thì ta có a 

 Follow(A)

3.

nếu có một sản xuất dạng B->

α

A hoặc B->

α

A

β

 với 

ε ∈

First(B) thì 

mọi phần tử thuộc Follow(B) cũng thuộc Follow(A)

thật vậy:  nếu a  

  Follow(B)  thì theo định nghĩa Follow ta có S =>*  

α

1

Ba

β

1

  =>* 

α

1

α

Aa

β

1

 , suy ra a 

 Follow(A)

- Để tính Follow của các ký hiệu không kết thúc: lần lượt xét tất cả các sản 

xuất. Tại mỗi sản xuất, áp dụng các qui tắc trong thuật toán tính Follow để thêm 
các ký hiệu vào các tập Follow . Lặp lại và dừng  khi nào gặp một lượt duyệt mà 
không bổ sung được ký hiệu nào vào các tập Follow.

Ví dụ  ở trên, ta tính được tập Follow cho các ký hiệu S, A, B như sau:

Follow(S) = {$}         Follow(A) = {b,$}    Follow(B) = {}

VÝ dô2: Víi v¨n ph¹m 

T E';    E' 

+ T E' | 

∈;    

F T';    T' 

* F T' | 

∈;        

 (E) | id

Theo ®Þnh nghÜa FIRST
V× F 

E) FIRST(F) = {(, id} F 

 (id) 

Tõ T 

 F T' v× ( ( FIRST(F) ( FIRST(T)= FIRST(F)

Tõ E 

T E' v× ( ( FIRST(T) ( FIRST(E)= FIRST(T)

V× E' 

→ε

 

⇒ε

 

 FIRST(E') 

background image

MÆt kh¸c do E' ( +T E' mµ FIRST(+)={ +} ( FIRST(E')= {+, (}
T¬ng tù FIRST(T')= { *, (}
VËy ta cã                         FIRST(E)= FIRST(T)= FIRST(F)= { (, id} 

FIRST(E')= {+, 

ε

 }

FIRST(T')= { *, 

ε

}

Tính follow :    Đ

Æt $ vµo trong FOLLOW(E).

Áp dông luËt 2 cho luËt sinh F

 (E) 

⇒ε

 

FOLLOW(E) 

FOLLOW(E)={$,

ε

}

Áp dông luËt 3 cho E 

 TE

'

 

⇒ε

,$ 

 FOLLOW(E

'

 FOLLOW(E

'

)={$,

ε

}.

Áp dông luËt 2 cho E

TE' 

 mäi phÇn tö # 

ε

 cña FIRST(E') tøc + 

(FOLLOW(T).
Áp dông luËt 3 cho E' E

'

 

 +TE

'

 , E

'

 

 

ε

 

 FOLLOW(E

'

 FOLLOW(T) 

 

⇒ 

FOLLOW(T) = { +, 

ε

, $ }.

Ap dụng 

luËt 3 cho T

FT' th× FOLLOW(T') =FOLLOW(T)={+, $, 

ε

}.

Ap dông luËt 2 cho T

 FT' 

FOLLOW(F)

Ap dông luËt 3 cho T

'

 

 * F T

'

 ;T

 

ε

'

 th× FOLLOW(T') ( FOLLOW(F)th× 

FOLLOW(F)= { *, +, $, )}
VËy ta cã     FOLLOW(E)= FOLLOW(E') = { $, )}

FOLLOW(T)= FOLLOW(T') = { +,$, )}

FOLLOW(F)= {*,+, $, )}
2.3.2.2. lập bảng phân tích LL(1).

Bảng phân tích LL(1) là một mảng hai chiều: Một chiều chứa các ký hiệu 

không kết thúc, chiều còn lại chứa các ký hiệu kết thúc và $. 

Vị trí M(A,a) chứa sản xuất A->

α

 trong bảng chỉ dẫn cho ta biết rằng khi cần 

khai triển ký hiệu không kết thúc A với ký hiệu đầu vào hiện tại là a thì áp dụng sản 
xuất A->

α

Thuật toán xây dựng bảng LL(1):

Input: Văn phạm G.
Output: Bảng phân tích M.
Phương pháp: 

1.

với mỗi sản xuất A->

α

, thực hiện bước 2 và bước 3

2.

với mỗi ký hiệu kết thúc a 

 First(

α

), định nghĩa mục M(A,a) là A-

>

α

background image

3.

nếu  

ε

 

  First(

α

) và với mỗi b  

  Follow(A) thì định nghĩa mục 

M(A,b) là A->

α

 (nếu 

ε

 

 First(

α

) và $ 

 Follow(A) thì thêm A->

α

  vào 

M[A,$])
Đặt tất cả các vị trí chưa được định nghĩa trong bảng là “lỗi”.

VÝ dô:   E 

 T E';   E' 

 + T E' | 

ε ;            

F T';      T' 

 * F T' | 

ε ;        

 (E) | 

id 

TÝnh FIRST(TE') = FIRST(T) = {(,id

}

 ( M[E,id] vµ M[E,( ] 

Kí tự chưa kết 

thúc 

Kí tự kết thúc

Id

+

*

(

)

$

E

E

 TE

'

 

 

E

 TE

'

 

 

E

'

 

E

 +TE

'

 

 

E

 

ε

 

E'

 

ε

 

T

T

 FT

'

 

 

T

 FT

'

 

 

T

'

 

T'

 

ε

 

T'

 +FT

'

 

T

'

 →

 

ε

 

T'

 

ε

 

F

F

 id

 

 

F

 (E)

 

 

XÐt luËt sinh E 

 TE' 

chøa luËt sinh E 

TE'

XÐt luËt sinh E'

+ TE'

TÝnh FIRST(+TE') = FIRST(+) = {+} ( M[E',+] chøa E'

+TE'

LuËt sinh E' 

→  ε    

v× 

ε   ∈

 FIRST(() = FIRST(() FOLLOW(E') = { ), $} 

( E

→  ε  

n»m trong M[E',)] vµ M[E',$]

LuËt sinh T

FT' : FIRST(FT') = {*}

LuËt sinh T' 

→  ε:

 

ε ∈

FIRST(

α

) vµ FOLLOW(T')= {+, ), $}

LuËt sinh F

 (E) ; FIRST(((E)) = {(}

LuËt sinh F 

id ; FIRST(id)={id}

2.3.2.3. văn phạm LL (k) và LL (1)

Giải thuật trên có thể áp dụng bất kỳ văn phạm G nào để sinh ra bảng phân 

tích M. Tuy nhiên có những văn phạm ( đệ quy trái và nhập nhằng) thì trong bảng 
phân tích M có những ô chứa nhiềuhơn một luật sinh.

Ví dụ: Văn phạm S  

→  

iEtSS’ | aS’ 

 → 

eS | 

ε Ε   →  

 b 

Ký tù cha kÕt 
thóc

Ký tù kÕt thóc

A

B

e

i

t

$

background image

S

S

 a

 

 

S

 iEtSS

'

 

 

S

'

 

 

 

ε

 

 

 

S

'

 

ε

 

E

 

E

 b

 

 

 

 

* Định nghĩa: Văn phạm LL(1) là văn phạm xây dựng được bảng phân tích M 

có các ô chỉ được định nghĩa nhiều nhất là một lần

.

* Điều kiện để một văn phạm là LL(1)
- Để kiểm tra văn phạm có phải là văn phạm LL(1) hay không ta lập bảng 

phân tích LL(1) cho văn phạm đó. Nếu có mục nào đó trong bảng được định nghĩa 
nhiều hơn một lần thì văn phạm đó không phải là LL(1), nếu trái lại thì văn phạm là 
LL(1).

- Cách khác là dựa vào định nghĩa, một văn phạm là LL(1) phải thoả mãn điều 

kiện sau:

nếu A -> 

α

 | 

β

 là hai sản xuất của văn phạm đó thì phải thoả mãn:

a)

không tồn tại một ký hiệu kết thúc a mà a 

 First(

α

) và a 

 First(

β

)

b)

không thể đồng thời 

ε

 thuộc First(

α

) và First(

β

).

c)

Nếu 

ε

 

 First(

α

) thì Follow(A) và First(

β

) không có phần tử nào 

trùng nhau.

2.3.2.4.  Thuật toán phân tích LL(1)

* Mô tả: Cơ sở của phân tích LL là dựa trên phương pháp phân tích topdown 

và máy ôtômát đẩy xuống.

- Vùng đệm chứa xâu vào với cuối xâu là ký hiệu kết thúc xâu $.

background image

- Ngăn xếp chứa các ký hiệu văn phạm thể hiện quá trình phân tích. Đáy ngăn 

xếp kí hiệu $.

- Bảng phân tích M 

lµ mét m¶ng hai chiÒu M[A,a], trong ®ã A lµ 

ký hiÖu chøa kÕt thóc, a lµ ký hiÖu kÕt thóc hoÆc $.

- Thành phần chính điều khiển phân tích.
Mô hình của phân tích cú pháp LL
Tại thời điểm hiện tại, giả sử X là ký hiệu trên đỉnh ngăn xếp và a là ký hiệu 

đầu vào. Các hành động điều khiển được thực hiện như sau:

1.

nếu X = a = $, quá trình phân tích thành công

2.

nếu X = a 

 $, lấy X ra khỏi ngăn xếp và dịch con trỏ đầu vào đến ký 

hiệu tiếp theo

3.

nếu X là một ký hiệu không kết thúc, xét mục M(X,a) trong bảng phân 

tích. Có hai trường hợp xảy ra:

a)

nếu M(A,a) = X -> Y

1

. . .Y

k

  thì lấy X ra khỏi ngăn xếp và đẩy vào 

ngăn xếp Y

1

, . . ., Y

k

 theo thứ tự ngược lại (để ký hiệu được phân tích tiếp theo trên 

đỉnh ngăn xếp phải là Y

1

, tạo ra dẫn xuất trái). 

b)

nếu M(A,a) là lỗi thì quá trình phân tích gặp lỗi và gọi bộ khôi phục 

lỗi.

* Thuật toán :

- Input:  Một xâu w và một bảng phân tích M của văn phạm G.
- Output: Đưa ra suy dẫn trái nhất của w nếu w 

 L(G), báo lỗi nếu w 

 L(G).

- Method:
Ở trạng thái khỉ đầu ngăn xép được đặt các kí hiệu $S  (S là đỉnh của cây phân tích 
còn xâu vào là w$ )

Đặt con trỏ ip trỏ đến kí tự đầu tiên của xâu w$
Repeat
{Giả sử X là kí hiệu đỉnh của ngăn xếp, a là kí hiệu vào tiếp theo}
       If (X 

∈Σ

 ) or (X = $) then

              If x=a then

Pop X từ đỉnh ngăn xếp và loại bỏ a khỏi xâu vào

 Else error ();

Else 

{X không phải là kí tự kết thúc}

background image

If M[X,a] = X 

 Y

1

, Y

2

, … Y

k

 then

Begin

Pop X từ ngăn xếp;
Push Y

k

, Y

k-1

, … Y

1

 vào ngăn xếp, với Y

1

 ở đỉnh;

Đưa ra sản xuất X 

 Y

1

, Y

2

, … Y

k

 ;

End;

Else   Error();

Until X = $ 

{ngăn xếp rỗng}

Ví dụ 2:  Cho văn phạm:   E->TE’;  E’->+TE’ | 

ε

;  T->FT’;  T’->*FT’ | 

ε

;   F-

>(E) | id

a)

tính First và Follow cho các ký hiệu không kết thúc.

b)

tính First cho vế phải của các sản xuất.

c)

xây dựng bảng phân tích LL(1) cho văn phạm trên

d)

phân tích LL đối với xâu vào “id+id*id”  

Ký hiệu văn phạm

First

Follow

E

(, id

), $

E’

+, 

ε

), $

T

(, id

+, ), $

T’

*, 

ε

+, ), $

F

(, id

+, *, ), $

Sản xuất

First của vế phải

E->TE’

(, id

E’->+TE’

+

T->FT’

(, id

T’->*FT’

*

F->(E)

(

F->id

Id

Bảng phân tích LL(1)

background image

Ký hiệu

Vế trái

Ký hiệu đầu vào

Id

+

*

(

)

$

E

E->TE’

E->TE’

E’

E’->+TE’

E’->

ε

E’->

ε

T

T->FT’

T->FT’

T’

T’->

ε

T’->*FT’

T’->

ε

T’->

ε

F

F->id

F->(E)

Phân tích LL(1) cho xâu vào “id+id*id”:

Ngăn xếp

Xâu vào

Đầu ra

$E

id+id*id$

E->TE’

$E’T

id+id*id$

T->FT’

$E’T’F

id+id*id$

F->id

$E’T’id

id+id*id$

rút gọn id

$E’T’

+id*id$

T’->

ε

$E’

+id*id$

E’->+TE’

$E’T+

+id*id$

rút gọn +

$E’T

id*id$

T->FT’

$E’T’F

id*id$

F->id

$E’T’id

id*id$

rút gọn id

$E’T’

*id$

T’->*FT’

$E’T’F*

*id$

rút gọn *

$E’T’F

id$

F->id

$E’T’id

id$

rút gọn id

$E’T’

$

T’->

ε

$E’

$

E’->

ε

$

$

Từ bảng phân tích, chúng ta có suy dẫn trái như sau:

E=>TE’=>FT’E’=>idT’E’=>idE’=>id+TE’=>id+FT’E’=>id+idT’E’=>id+id

*FT’E’=> id+id*idT’E’=>id+id*idE’=>id=id*id .

background image

2.3.4. Phân tích LR.

LR là kỹ thuật phân tích cú pháp từ dưới lên khá hiệu quả, có thể được sử dụng để phân  

tích một lớp khá lớn các văn phạm phi ngữ cảnh. Kỹ thuật này gọi là phân tích cú pháp LR(k),  

trong đó:

- L là Left to right chỉ việc quét xâu vào từ trái quá phải.

- R là Right most parsing chỉ việc suy dẫn sinh ra là suy dẫn phải.

- k là số ký hiệu nhìn trước để đưa ra quyết định phân tích. 

* Phân tích LR có nhiều ưu điểm:

Nhận biết được tất cả các cấu trúc của ngôn ngữ lập trình được tạo ra dựa theo các văn 

phạm phi ngữ cảnh.

LR là phương pháp phân tích cú pháp gạt - thu gọn không quay lui tổng quát nhất đã 

được biết đến nhưng lại có thể được cài đặt hiệu quả như những phương pháp gạt - thu gọn 

khác.

lớp văn phạm phân tích được nhờ phương pháp LR là một tập bao hàm thực sự của lớp  

văn phạm phân tích được bằng cách phân tích cú pháp dự đoán.

Phát hiện được lỗi cú pháp ngay khi có thể trong quá trình quét đầu vào từ trái sang.

* Nhược điểm chủ yếu: ta phải thực hiện quá nhiều công việc để xây dựng được bộ phân  

tích LR cho một ngôn ngữ lập trình.

2.3.4.1. Thuật toán phân tích LR.

Phân tích LR là một thể phân tích cú pháp gạt - thu gọn, nhưng điểm khác biệt so với phân  

tích Bottom-up là nó không quay lui. Tại mỗi thời điểm nó xác định được duy nhất hành động gạt 

hay thu gọn. 

* Mô hình: gồm các thành phần sau:
- Stack lưu một chuỗi s

0

X

1

s

1

X

2

s

... X

m

s

m

 trong đó s

m

 nằm trên đỉnh Stack. X

i

 là 

một ký hiệu văn phạm, si là một trạng thái tóm tắt thông tin chứa trong Stack bên 
dưới nó.

- Bảng phân tích bao gồm 2 phần : hàm action và hàm goto.

 

      

action[s

m

, a

i

] có thể có một trong 4 giá trị :

1. shift s : đẩy s, trong đó s là một trạng thái.
2. reduce (A

 

β

) :thu gọn bằng luật sinh A

 

β

.

3. accept : Chấp nhận
4. error : Báo lỗi

      

Goto lấy 2 tham số là một trạng thái và một ký hiệu văn phạm, nó sinh ra 

một trạng thái.

background image

 

Cấu hình (configuration) của một bộ phân tích cú pháp LR là một cặp thành 

phần, trong đó, thành phần đầu là nội dung của Stack, phần sau là chuỗi nhập chưa 
phân tích: (s

0

X

1

s

1

X

2

s

... X

m

s

m

, a

i

 a

i+1

 ...

 

a

n

 $)

* Hoạt động: 
Với s

m

 là ký hiệu trên đỉnh Stack, ai là ký hiệu nhập hiện tại, cấu hình có được 

sau mỗi dạng bước đẩy sẽ như sau :

1. Nếu action [s

m

, a

i

] = Shift s : Thực hiện phép đẩy để được cấu hình mới :

(s

0

X

1

s

1

X

2

s

... X

m

s

m

 a

i

s, a

i +1

 ...

 

a

n

 $)

Phép đẩy làm cho s nằm trên đỉnh Stack, a

i+1

 trở thành ký hiệu hiện hành.

2. Nếu action [s

m

, a

i

] = Reduce(A 

 

β

) thì thực hiện phép thu gọn để được cấu 

hình :                                   (s

0

X

1

s

1

X

2

s

... X

m - i

s

m - i 

 As, a

i

 a

i +1

 ....

 

a

n

 $)

Trong đó, s = goto[s

m - i

, A] và r là chiều dài số lượng các ký hiệu của 

β

. Ở đây, 

trước hết 2r phần tử của Stack sẽ bị lấy ra, sau đó đẩy vào A và s.

3. Nếu action[s

m

, a

i

] = accept : quá trình phân tích kết thúc.

4. Nếu action[s

m

, a

i

] = error : gọi thủ tục phục hồi lỗi.

 Giải thuật phân tích cú pháp LR

 

Input: Một chuỗi nhập w, một bảng phân tích LR với hàm action và goto cho văn 

phạm G.

 Output:  Nếu w 

 L(G), đưa ra một sự phân tích dưới lên cho w . Ngược lại, thông  

báo lỗi.

 Phương pháp:
 Khởi tạo s

0

 là trạng thái khởi tạo nằm trong Stack và w$ nằm trong bộ đệm nhập.

Ðặt ip vào ký hiệu đầu tiên của w$;

Repeat  forever   begin

Gọi s là trạng thái trên đỉnh Stack và a là ký hiệu được trỏ bởi ip;
If  action[s, a] = Shift s'  then  begin

Ðẩy a và sau đó là s' vào Stack;
Chuyển ip tới ký hiệu kế tiếp;

end
else  if
  action[s, a] = Reduce (A 

 

β

)  then  begin

Lấy 2 * | 

β

|  ký hiệu ra khỏi Stack;

Gọi s' là trạng thái trên đỉnh Stack;
Ðẩy A, sau đó đẩy goto[s', A] vào Stack;
Xuất ra luật sinh A

 

β

;

 end
else if 
 action[s, a] = accept  then

return

else error ( )

end

Ví dụ:

Cho văn phạm:

background image

(1) E -> E + T           (2) E -> T
(3) T -> T * F            (4) T -> F
(5) F -> ( E )              (6) F -> a

Giả sử chúng ta đã xây dựng được bảng phân tích action và goto như sau:

chú ý:
các giá trị trong action được ký hiệu như sau:

a)

si có nghĩa là shift i

b)

rj có nghĩa là reduce theo luật (j)

c)

acc có nghĩa là accept

d)

khoảng trống biểu thị lỗi

trạng 
thái

Action

goto

a

+

*

(

)

$

E

T

F

0

s5

S4

1

2

3

1

s6

acc

2

r2

s7

r2

r2

3

r4

r4

r4

r4

4

s5

S4

8

2

3

5

r6

r6

r6

r6

6

s5

S4

9

3

7

s5

S4

10

8

s6

s11

9

r1

s7

r1

r1

10

r3

r3

r3

r3

11

r5

r5

r5

r5

Bảng phân tích cú pháp

Chúng ta sử dụng thuật toán  LR để phân tích xâu vào “a*a+a” đối với dữ liệu trên 
như sau:

Ngăn xếp

Đầu vào

Hành động

0

id * id + id $

gạt

0 id 5

* id + id $

thu gọn F->id

0 F 3

* id + id $

thu gọn T->F

0 T 2

* id + id $

gạt

0 T 2 * 7

id + id $

gạt

0 T 2 * 7 id 5

+ id $

thu gọn F->id

0 T 2 * 7 F 10

+ id $

thu gọn T->T*F

0 T 2

+ id $

thu gọn E->T

0 E 1

+ id $

gạt

0 E 1 + 6

id $

gạt

0 E 1 + 6 id 5

$

thu gọn F->id

0 E 1 + 6 F 3

$

thu gọn T->F

background image

0 E 1 + 6 T 9

$

thu gọn E->E+T

0 E 1

$

chấp nhận (accepted)

Quá trình phân tích LR

Một số đặc điểm của phân tích LR:

- Một tính chất cơ bản đối với bộ phân tích cú phát LR là xác định được khi nào handle xuất hiện  
trên đỉnh ngăn xếp. 
- Ký hiệu trạng thái trên đỉnh ngăn xếp đã xác định mọi thông tin của quá trình phân tích vì nó  
chỉ đến tập mục có nghĩa của tiền tố khả tồn trong ngăn xếp. Dựa vào các mục này, chúng ta có  
thể xác định khi nào thì gặp một handle trên đỉnh ngăn xếp và thực hiện hành động thu gọn.
- Một nguồn thông tin khác để xác định hành động gạt-thu gọn là k ký hiệu đầu vào tiếp theo.  
Thông thườn chúng ta xét k=0 hoặc 1. 
- Điểm khác biệt giữa phương pháp phân tích LR với phương pháp phân tích LL là: Để cho một 
văn phạm là LR(k), chúng ta phải có khả năng xác định được sự xuất hiện của vế phải của một  
sản xuất khi đã thấy tất cả quá trình dẫn xuất từ vế phải đó với thông tin thêm là k ký hiệu đầu  
vào tiếp theo. Điều kiện này rõ ràng là chính xác hơn so với điều kiện của văn phạm LL(k) là  
việc sử dụng một sản xuất chỉ dựa vào k ký hiệu đầu vào tiếp theo. Chính vì vậy mà quá trình  
phân tích LR ít có xung đột hơn, hay nói cách khác là văn phạm của nó rộng hơn LL rất nhiều
.

2.3.4.2. Một số khái niệm.

1) Tiền tố khả tồn (viable prefixes)
Xâu ký hiệu trong ngăn xếp tại mỗi thời điểm của một quá trình phân tích gạt - 

thu gọn là một tiền tố khả tồn. 

Ví dụ:  tại một thời điểm trong ngăn xếp có dữ liệu là 

α β

 và xâu vào còn lại là w thì  

α β

w là một dạng câu dẫn phải và 

α β

 là một tiền tố khả tồn.

2) Mục (Item) : Cho một văn phạm G.
Với mỗi sản xuất A->xy, ta chèn dấu chấm vào tạo thành A->x .y và gọi kết 

quả này là một mục. 

Mục A->x.y cho biết qúa trình suy dẫn sử dụng sản xuất A->xy và đã suy dẫn 

đến hết phần x trong sản xuất, quá trình suy dẫn tiếp theo đối với phần xâu vào còn 
lại sẽ bắt đầu từ y.

Ví dụ:   Luật sinh A ( XYZ có 4 mục như sau :

A

  

XYZ           A

 X

YZ

A

 XY

Z             A

 XYZ

    Luật sinh A 

 

ε

 chỉ tạo ra một mục A 

 

3)  Mục có nghĩa (valid item)

Một mục A->

β

1

.

β

2

 gọi là mục có nghĩa(valid item) đối với tiền tố khả tồn 

α β

1

 nếu tồn tại một dẫn xuất: S =>*rm 

α

Aw =>rm 

α β

1

β

2

w

background image

Tập tất cả các mục có nghĩa đối với tiền tố khả tồn gọi là tập I. 

Một tập mục có nghĩa đối với một tiền tố khả tồn nói lên rất nhiều điều trong quá trình suy  

dẫn gạt - thu gọn: Giả sử quá trình gạt thu gọn đang ở trạng thái với ngăn xếp là x và phần ký  
hiệu đầu vào là v 

(*)

ngăn xếp

đầu vào

$x

      v$

thế thì, quá trình phân tích tiếp theo sẽ phụ thuộc vào tập mục có nghĩa I của tiền tố khả  

tồn thuộc x. Với một mục [A->

β

1

.

β

2

 I, cho chúng ta biết x có dạng 

α β

1

, và quá trình phân 

tích phần còn lại w của xâu đầu vào nếu theo sản xuất A->

β

1

β

2

 sẽ được tiếp tục từ 

β

2

 của mục 

đó. Hành động gạt hay thu gọn sẽ phụ thuộc vào 

β

2

 là rỗng hay không. Nếu 

β

2

 = 

ε

  thì phải thu gọn 

β

1

 thành A, còn nếu 

β

2

 

 

ε

 thì việc phân tích theo sản xuất 

A->

β

1

β

2

 đòi hỏi phải sử dụng hành động gạt.

Nhận xét:

- Mọi quá trình phân tích tiếp theo của trạng thái (*) đều phụ thuộc vào 

các mục có nghĩa trong tập các mục có nghĩa I của tiền tố khả tồn x.

- Có thể có nhiều mục có nghĩa đối với một tiền tố x. Các mục này có 

thể có các hành động xung đột (bao gồm cả gạt và thu gọn), trong trường hợp này 
bộ phân tích sẽ phải dùng các thông tin dự đoán, dựa vào việc nhìn ký hiệu đầu vào 
tiếp theo để quyết định nên sử dụng mục có nghĩa nào với tiền tố x 

(tức là sẽ cho 

tương ứng gạt hay thu gọn)

. Nếu quá trình này cho những quyết định không xung đột 

(duy nhất) tại mọi trạng thái thì ta nói văn phạm đó phân tích được bởi thuật toán 
LR.

- Tư tưởng của phương pháp phân tích LR là phải xây dựng được tập tất cả 

các mục có nghĩa đối với tất cả các tiền tố khả tồn.  

4) Tập chuẩn tắc LR(0)

 

 

LR(0) là tập các mục có nghĩa cho tất cả các tiền tố khả tồn.

LR(0) theo nghĩa: LR nói lên đây là phương pháp phân tích LR, còn số 0 có 

nghĩa là số ký tự nhìn trước là 0.

5) Văn phạm gia tố(

 

 

Augmented Grammar)

 

 

(mở rộng)

 

 

Văn phạm G - ký hiệu bắt đầu S, thêm một ký hiệu bắt đầu mới S' và luật sinh 

S'  S để được văn phạm mới G' gọi là văn phạm gia tố.

Ví dụ: cho văn phạm G gồm các sản xuất S -> aSb | a thì văn phạm gia tố của G, ký hiệu là  

G’ gồm các sản xuất S’-S, S->aSb | a với S’ là ký hiệu bắt đầu.

background image

Ta xây dựng văn phạm gia tố của một văn phạm theo nghĩa, đối với văn phạm ban  

đầu, quá trình phân tích sẽ bắt đầu bởi các sản xuất có vế trái là S. Khi đó, chúng ta xây  
dựng văn phạm gia tố G’ thì mọi quá trình phân tích sẽ bắt đầu từ sản xuất S’->S

Sử dụng hai phép toán: phép tính bao đóng closure(I) của một tập mục I và phép sinh ra  

tập mục cho các tiền tố khả tồn mới goto(I,X) như sau:

6)  Phép toán closure

Nếu I là một tập các mục của một văn phạm G thì closure(I) là tập các mục 

được xây dựng từ I bằng hai qui tắc sau:

1. khởi đầu mỗi mục trong I đều được đưa vào closure(I)

2. nếu [A ->  

α

.B

β

]  

  closure(I) và B->

γ

  là một sản xuất thì thêm [B 

-> .

γ

 ] vào closure(I) nếu nó chưa có ở đó. Áp dụng qui tắc này đến khi không 

thêm được một mục nào vào closure(I) nữa.

Trực quan:
 nếu [A -> 

α

.B

β

] là một mục có nghĩa đối với một tiền tố khả tồn x

α

 thì với 

B->

γ

 là một sản xuất ta cũng có [B->.

γ

] là một mục có nghĩa đối với tiền tố khả 

tồn x

α

. 

Phép toán tính bao đóng của một mục là để tìm tất cả các mục có nghĩa tương 

đương của các mục trong tập đó.

Theo định nghĩa của một mục là có nghĩa đối với một tiền tố khả tồn, chúng ta có suy dẫn S  

=>*rm xAy =>rm x

α

B

β

y

sử dụng sản xuất B -> 

γ

 ta có suy dẫn S =>*rm x

α γ β

y. Vậy thì [B->.

γ

] là một mục 

có nghĩa của tiền tố khả tồn x

α

Ví dụ: 

Xét văn phạm mở rộng của biểu thức:

E' 

  E                  E  

  E + T | T    T  

  T * F | F       F  

  (E) | id

Nếu I là tập hợp chỉ gồm văn phạm {E'

 

 E} thì closure(I) bao gồm:

 E' 

 

 E                 (Luật 1)        E 

 

 E + T           (Luật 2)

 

 T                  (Luật 2)         T 

 

 T * F            (Luật 2)

 

 F                  (Luật 2)         F 

 

 (E)               (Luật 2)

 

 id                 (Luật 2)

 E' 

 

 E  được đặt vào closure(I) theo luật 1. Khi có một E đi ngay sau một 

• , 

bằng 

luật 2 ta thêm các sản xuất E với các chấm nằm ở đầu trái  ( E 

 

 E + T và E 

 

 T). 

Bây giờ lại có T đi theo sau một 

•, 

ta lại thêm 

 

 

 T * F    và T 

 

 F vào. Cuối cùng 

ta có Closure(I) như trên.

Chú ý rằng : Nếu một B - luật sinh được đưa vào closure(I) với dấu chấm mục nằm ở 

đầu vế phải thì tất cả các B - luật sinh đều được đưa vào.

 

* Phép toán goto
goto(I,X) với I là một tập các mục và X là một ký hiệu văn phạm. 

background image

goto(I,X) được định nghĩa là bao đóng của tập tất cả các mục  [A->

α

X.

β

sao cho [A->

α

.X

β

 I. 

Trực quan:
Nếu I là tập các mục có nghĩa đối với một tiền tố khả tồn  

γ

  nào đó thì 

goto(I,X) là tập các mục có nghĩa đối với tiền tố khả tồn 

γ

X.

gọi tập J=goto(I,X) thì cách tính tập J như sau:

1.

nếu [A->

α

.X

β

 I thì thêm [A->

α

X.

β

] vào J 

2.

J=closure(J)

Phép toán goto là phép phát triển để xây dựng tất cả các tập mục cho tất cả các 

tiền tố khả tồn có thể.

Ví dụ :  Giả sử I = {E' 

 E

, E 

 E 

 + T}.        Tính goto (I, +) ?

 Ta có J = { E

 E + 

 T}

 goto (I, +)  = closure(I') bao gồm các mục :

E  E + 

 T                       (Luật 1)

T  

 T * F                        (Luật 2)

T  

 F                              (Luật 2)

F  

 (E)                           (Luật 2)

                 F  

 id                             (Luật 2)

Tính Goto(I,+) bằng cách kiểm tra I cho các mục với dấu + ở ngay bên phải chấm. 

E’ 

 E

• 

không phải mục như vậy nhưng E 

 E 

 + T thì đúng. Ta chuyển dấu chấm qua 

bên kia dấu + để nhận được E 

 E + 

 T và sau đó tính closure cho tập này.

Như vậy, cho trước một văn phạm, ta có thể sử dụng hai phép toán trên để sinh ra tất cả  

các tiền tố khả tồn có thể và tập mục có nghĩa của từng tiền tố khả tồn. Với việc sử dụng phép  
tính closure và goto như trên, chúng ta xây dựng được tập các mục gọi là tập mục LR(0). 

Thuật toán xây dựng LR(0) như sau:

Cho văn phạm G, văn phạm gia tố của nó là G’
Tập C là tập các tập mục LR(0) được tính theo thuật toán như sau:

1). C = {closure({[S’->.S]})}
2). Đối với mỗi mục I trong C và mỗi ký hiệu văn phạm X, tính 

goto(I,X). Thêm goto(I,X) vào C nếu không rỗng và không trùng với bất kỳ tập nào 
có trong C

Thực hiện bước 2 đến khi nào không thêm được tập nào nữa vào C

C là tập xác định tất cả các mục có nghĩa đối với tất cả các tiền tố khả tồn vì chúng ta xuất  

phát từ mục [S’ -> .S] và xây dựng các mục có nghĩa cho tất cả các tiền tố khả tồn.

background image

Xây dựng tập C dựa trên hàm goto có thể được xem như một sơ đồ chuyển trạng thái của  

một DFA. Trong đó, I

0

  là trạng thái xuất phát, bằng cách xây dựng các trạng thái tiếp bằng 

chuyển trạng thái theo đầu vào là các ký hiệu văn phạm. Đường đi của các ký hiệu đầu vào 

chính là các tiền tố khả tồn. Các trạng thái chính là tập các mục có nghĩa của các tiền tố khả tồn  

đó. 

Ví dụ:   Cho văn phạm G

:   

E -> E + T | T;       T -> T * F | F;      F -> ( E ) | id

Hãy tính LR(0)

- Xét văn phạm G’ là văn phạm gia tố của G. Văn phạm G’ gồm các sản xuất sau:

E’ -> E;   E -> E + T | T;     T -> T * F | F;       F -> ( E ) | a

Tính theo thuật toán trên ta có kết quả như sau:

Closure({E'

 E}):

I

0

E’ -> .E

E -> .T

T -> .F

F -> .(E)

F -> .a 

Goto (I

0

, id)   

I

5

:

F -> a.

Goto (I

0

, E)    

I

1

:

E’ -> E.

E -> E.+T

Goto (I

1

+)    

I

6

:      E -> .E+T

 

E -> E+.T

                   T -> .T*F
   

T -> .F
F -> .(E)
F -> .a

Goto (I

0

, T)    

I

2

:

E -> T.

                      T -> T.*F

Goto (I

2

*)    

I

7

:

T -> T*.F

 

F -> .(E)
F -> .a

 

                           

                        

                      

background image

Sơ đồ chuyển trạng thái của DFA cho các tiền tố khả tồn:

I

0

I

1

I

6

I

9

I

7

I

4

I

3

I

5

I

2

I

7

I

10

I

4

I

5

I

3

I

4

I

8

I

11

I

6

I

2

I

3

I

5

E

+

T

*

F

(

a

T

*

F

(

a

F

(

E

)

+

(

T

F

a

a

background image

2.3.4.2. Văn phạm LR.

Làm thế nào để xây dựng được một bảng phân tích cú pháp LR cho một văn phạm đã cho ? 

Một văn phạm có thể xây dựng được một bảng phân tích cú pháp cho nó được gọi 
là văn phạm LR. 

Có những văn phạm phi ngữ cảnh không thuộc lọai LR, nhưng nói chung là 

ta có thể tránh được những văn phạm này trong hầu hết các kết cấu ngôn ngữ lập trình điển  
hình.

 Sự khác biệt rất lớn giữa các văn phạm LL và LR:

Ðối với văn phạm  LR(k), ta phải có khả năng nhận diện được sự xuất hiện của 

vế phải của một sản xuất nào đó bằng cách xem tất cả những gì suy dẫn được từ vế 
phải qua k kí hiệu vào được nhìn vượt quá. Ðòi hỏi này ít khắt khe hơn so với các 
văn phạm LL(k). 

Đối với văn phạm LL(k): ta phải nhận biết được sản xuất nào được dùng chỉ với 

k kí hiệu đầu tiên mà vế phải của nó suy dẫn ra.

 Vì vậy, các văn phạm LR có thể mô tả được nhiều ngôn ngữ hơn so với các văn 

phạm LL

.

2.3.4.3. Xây dựng bảng phân tích SLR.

Phần này trình bày cách xây dựng một bảng phân tích cú pháp LR từ văn phạm. Chúng ta sẽ  

đưa ra 3 phương pháp khác nhau về tính hiệu quả cũng như tính dễ cài đặt. Phương pháp thứ  
nhất được gọi là "LR đơn giản" (Simple LR - SLR), là phương pháp "yếu" nhất nếu tính theo số 
lượng văn phạm có thể xây dựng thành công bằng phương pháp này, nhưng đây lại là phương  
pháp dễ cài đặt nhất. Ta gọi bảng phân tích cú pháp tạo ra bởi phương pháp này là bảng SLR và  
bộ phân tích cú pháp tương ứng là bộ phân tích cú pháp SLR, với văn phạm tương đương là văn  
phạm SLR.

Phương pháp thứ 2 là phương pháp LR chuẩn ( canonical LR): phương pháp mạnh nhất  

nhưng tốn kém nhất.

Phương pháp thứ 3: LR nhìn vượt (LALR – LookaheadLR) là phương pháp trung gian về sức  

mạnh và chi phí giữ 2 phương pháp trên. Phương pháp này làm việc với hầu hết các văn phạm.  

Trong phần này ta sẽ xem xét cách xây dựng các hàm action và goto của phân tích SLR từ 

ôtômát hữu hạn đơn định dùng để nhận dạng các tiền tố có thể có. 

background image

Cho văn phạm G, ta tìm văn phạm gia tố của G là G’, từ G’ xây dựng C là tập 

chuẩn các tập mục cho G’, xây dựng hàm phân tích Action và hàm nhẩy goto từ C 
bằng thuật toán sau.    

Input: Một văn phạm tăng cường G'
Output: Bảng phân tích SLR với hàm action và goto
Phương pháp:
 1.

  

Xây dựng C = { I

0

, I

1

, ..., I

n

 }, họ tập hợp các mục LR(0) của G'.

2. Trạng thái i được xây dựng từ  Ii .Các action tương ứng trạng thái i được xác 

định như sau:

 2.1

   

. Nếu A 

 

α

 

 a

β

 

 I

i

 và goto (I

i

, a) = I

j

 thì action[i, a] = "shift j". Ở 

đây a là ký hiệu kết thúc.

2.2.   Nếu   A  

 

α

 

 

  I

i

  thì   action[i,   a]   =   "reduce   (A  

 

α

)",  

a  

∈ 

FOLLOW(A). Ở đây A không phải là S'

2.3. Nếu S' 

 S 

 

 I

i

 thì action[i, $] = "accept".

Nếu một action đụng độ được sinh ra bởi các luật trên, ta nói văn phạm 

không phải là SLR(1). Giải thuật sinh ra bộ phân tích cú pháp sẽ thất bại trong 
trường hợp này.

 3. Với mọi ký hiệu chưa kết thúc A, nếu goto (I

i

,A) = I

j

 thì goto [i, A] = j

4. Tất cả các ô không xác định được bởi 2 và 3 đều là “error
5. Trạng thái khởi đầu của bộ phân tích cú pháp được xây dựng từ tập các mục 

chứa S’

 

 S

 

Ví dụ  Ta xây dựng bảng phân tích cú pháp SLR cho văn phạm tăng cường G' trong ví dụ 
trên.
E' 

 E                   E  

 E + T | T                    T  

 T * F | F                  F  

 (E) | id

(0)

         

E'

 E

(1)

         

 E + T

(2)

         

 T

(3)

         

 T * F

(4)

         

 F

(5)

         

 (E)

(6)    F 

 id

1. C = { I

0

, I

1

, ... I

11  

}

2. FOLLOW(E) = {+, ), $}
    FOLLOW(T) = {*, +, ), $}                             
    FOLLOW(F) = {*, +, ), $}

 Dựa vào họ tập hợp mục C đã được xây dựng trong ví dụ 4.22, ta thấy:

 Trước tiên xét tập mục I

0

 : Mục F 

 

 (E) cho ra action[0, (] = "shift 4", và mục F 

 

• 

id cho action[0, id] = "shift 5". Các mục khác trong I

0

 không sinh được hành động nào.

background image

Bây giờ xét  I

1

  : Mục E'

  E  

  cho action[1, $] = "accept", mục E  

  E  

  + T cho 

action[1, +] = "shift 6".

 Kế đến xét I

2

:       E 

 T 

 

                               T 

 T 

 * F

Vì FOLLOW(E) = {+, ), $}, mục đầu tiên làm cho action[2, $] = action[2,+] = "reduce 

(E 

 T)". Mục thứ hai làm cho action[2,*] = "shift 7".

 Tiếp tục theo cách này, ta thu được bảng phân tích cú pháp SLR:

 

trạng 

thái

Action

goto

a

+

*

(

)

$

E

T

F

0

s5

S4

1

2

3

1

s6

acc

2

r2

s7

r2

r2

3

r4

r4

r4

r4

4

s5

S4

8

2

3

5

r6

r6

r6

r6

6

s5

S4

9

3

7

s5

S4

10

8

s6

s11

9

r1

s7

r1

r1

10

r3

r3

r3

r3

11

r5

r5

r5

r5

Bảng phân tích xác định bởi giải thuật 4.7 gọi là bảng SLR(1) của văn phạm G, bộ  

phân tích LR sử dụng bảng SLR(1) gọi là bộ phân tích SLR(1) và văn phạm có một bảng 
SLR(1) gọi là văn phạm SLR(1).

 

 Mọi văn phạm SLR(1) đều không mơ hồ, Tuy nhiên  

có 

những văn phạm không mơ hồ nhưng không phải là 
SLR(1).

 

 Ví dụ: Xét văn phạm G với tập luật sinh như sau:

    

 L = R       S 

 R           L 

 * R 

 id             

 L

 

 Ðây là một văn phạm không mơ hồ nhưng không 
phải là văn phạm SLR(1).

 

 Họ tập hợp các mục C bao gồm:

 

 

 
 
 

    

I

0

 :    S' 

 

 S

S  

 

 L = R

S  

 

 R

L  

 

 * R

L  

 

  id

         R 

 

 L

I

1

 :    S' 

 S 

I

2 

:    S  

 L 

 = R

          R 

  L 

 

          I

3

 :    S 

  R 

          I

4

 :    L 

 * 

 R

                    R 

 

 L

                    L 

 

 * R

                    L 

 

 id

          I

5

 :    L 

 id 

          I

6

 :    S 

 L = 

 R

                    R 

 

 L

                    L 

 

 * R

                    L 

 

 id

I

7

 :     L 

 * R

I

8

 :     R 

 L

I

9

 :     S 

 L = R

background image

 Khi xây dựng bảng phân tích SLR cho văn phạm, khi xét tập mục I2 ta thấy mục 
đầu tiên trong tập này làm cho action[2, =] = "shift 6". Bởi vì = 

 FOLLOW(R), 

nên mục thứ hai sẽ đặt action[2, =] = "reduce (R 

 L)" 

 Có sự  đụng độ  tại 

action[2, =]. Vậy văn phạm trên không là văn phạm SLR(1).

2.3.4.4. Xây dựng bảng phân tích LR chuẩn.

* Mục LR(1) của văn phạm G là một cặp dạng [A 

 

α• β

, a], trong đó A 

→ 

α β

 là luật sinh,  a là một ký hiệu kết thúc hoặc $. 

* Thuật toán xây dựng họ tập hợp mục LR(1)

 

 

Giải thuật:  Xây dựng họ tập hợp các mục LR(1)

 Input : Văn phạm tăng cường G’
Output: Họ tập hợp các mục LR(1).
 Phương pháp: Các thủ tục closure, goto và thủ tục chính Items như sau:

 Function Closure (I);

begin

Repeat

For Mỗi mục [A 

 

α

 

 B

β

,a] trong I, mỗi luật sinh B 

 

γ

 trong G' và mỗi 

ký hiệu kết thúc b 

 FIRST (

β

a) sao cho [B 

 

 

γ

, b] 

 I  do

Thêm [ B 

 

 

γ

, b] vào I;

Until Không còn mục nào có thể thêm  cho I được nữa;
return I;

end;

 

Function goto (I, X);
begin

Gọi J là tập hợp các mục [A 

 

α

X

•β

, a] sao cho [A 

 

α•

X

β

, a]

 I;

return Closure(J);

end;
 Procedure Items (G');
begin

background image

C := Closure ({[S' 

 

S, $]})

Repeat
For
  
Mỗi tập các mục I trong C và mỗi ký hiệu văn phạm X
sao cho goto(I, X) 

 

 và goto(I, X) 

 C do

Thêm goto(I, X) vào C;

Until  Không còn tập các mục nào có thể thêm cho C;

          end;
Ví dụ
: Xây dựng bảng LR chính tắc cho văn phạm gia tố G' có các luật sinh sau :
                     S'  S
                    (1)

    

S  L = R3

                    (2)

    

S  R

                    (3)

    

L  * R

                    (4)

    

L  id

                    (5)  R  L
 
     Trong đó: tập ký hiệu chưa kết thúc ={S, L, R} và tập ký hiệu kết thúc {=, *, id, $}

      I

0

 :                          S' 

 

 S, $

Closure (S'  

S, $)  S  

 

 L = R, $

                  S  

 

 R, $

                  L  

 

 * R, = | $

                  L  

 

  id, = | $

                  R 

 

 L, $

 
Goto (I

0

,S)    I

1

 :        S' 

 S 

, $

 
Goto (I

0

, L)   I

2 

:        S  

 L 

 = R, $

                            R 

  L 

, $

 
Goto (I 

0

,R)   I

3

:         S 

  R 

, $

 
Goto (I

0

,*)     I

4

:         L 

  * 

 R, = | $

                   R   

 L, = | $  

                   L  

 

 * R, = | $

                   R 

 

  id, = | $

 
Goto (I

0

,id)    I

5

 :        L 

 id 

, = | $

 
Goto (I

2

,=)  

  

 I

6

 :        S 

 L = 

 R, $

                                      R 

 

 L, $

                                      L 

 

 * R, $

                                      L 

 

 id, $

Goto (I

4

,R)   I

7

 : L 

 * R

, = | $

 
Goto (I

4

, L)   I

8

 : R

 L

, = | $

 
Goto (I

6

,R)    I

9

 : S 

 L = R

, $

 
Goto (I

6

,L)    I

10

 :R 

 L

, $

 
Goto (I

6

,*)     I

11

 :L 

 * 

 R, $

             R 

 

 L, $ 

             L  

 

 * R, $

                                R 

 

  id, $

 
Goto (I

6

, id)   I

12

 :L 

 id 

, $

 
Goto (I

11

,R)   I

13

 :R 

 * R

, $

 
Goto (I

11

,L)   

  I

10

 
Goto (I

11

,*)    

 I

11

 
Goto (I

11

,id)  

   I

12

 
* Thuật toán xây dựng bảng phân tích cú pháp LR chính tắc 

background image

 

 

Giải thuật:  Xây dựng bảng phân tích LR chính tắc

 Input

Văn phạm tăng cường G'

Output

: Bảng LR với các hàm action và goto

Phương pháp:

1

. Xây dựng C = { I0, I1, .... In } là họ tập hợp mục LR(1)

2. Trạng thái thứ i được xây dựng từ Ii. Các action tương ứng trạng thái i 

được xác định như sau:

2.1. Nếu [A 

→ α  •

 a

β

,b] 

 Ii và goto(Ii,a) = Ij thì action[i, a]= "shift 

j". Ở đây a phải là ký hiệu kết thúc. 

2.2. Nếu [A 

→ α  •,

 a

] ∈

 Ii , A 

 S' thì action[i, a] = "reduce (A 

→ α)

2.3. Nếu [S' 

→ 

S

,$] 

 Ii thì action[i, $] = "accept".

Nếu có một sự đụng độ giữa các luật nói trên thì ta nói

 

văn phạm không 

phải là LR(1) và giải thuật sẽ thất bại.

 3. Nếu goto(Ii, A) = Ij thì goto[i,A] = j
4. Tất cả các ô không xác định được bởi 2 và 3 đều là "error"
5. Trạng thái khởi đầu của bộ phân tích cú pháp được xây dựng từ tập các 

mục chứa [S' 

→ •

S,$]

 Bảng phân tích xác định bởi giải thuật 4.9 gọi là bảng phân tích LR(1) chính tắc của văn  

phạm G, bộ phân tích LR sử dụng bảng LR(1) gọi là bộ phân tích LR(1) chính tắc và văn phạm  
có một bảng LR(1) không có các action đa trị thì được gọi là văn phạm LR(1).

 Ví dụ :  Xây dựng bảng phân tích LR chính tắc cho văn phạm  ở ví dụ trên
 

Trạng 

thái

Action

Goto

=

*

id

$

S

L

R

0

 

s

4

s

5

 

1

2

3

1

 

 

 

acc

 

 

 

2

s

6

 

 

r

5

 

 

 

3

 

 

 

r

2

 

 

 

4

 

s

4

s

5

 

 

8

7

5

r

4

 

 

 

 

 

 

6

 

s

11

s

12

 

 

10

9

7

r

3

 

 

 

 

 

 

8

r

5

 

 

 

 

 

 

9

 

 

 

r

1

 

 

 

10

 

 

 

r

5

 

 

 

11

 

s

11

s

12

 

 

10

13

background image

12

 

 

 

r

4

 

 

 

13

 

 

 

r

3

 

 

 

 
Hình 4.14 - Bảng phân tích cú pháp LR chính tắc

 

Mỗi văn phạm SLR(1) là một văn phạm LR(1), nhưng với một văn phạm SLR(1), bộ  

phân tích cú pháp LR chính tắc có thể có nhiều trạng thái hơn so với bộ phân tích cú  
pháp SLR cho văn phạm đó.
 

2.3.4.5. Xây dựng bảng phân tích LALR.

Phần này giới thiệu phương pháp cuối cùng để xây dựng bộ phân tích cú pháp LR - kỹ 

thuật LALR (Lookahead-LR), phương pháp này thường được sử dụng trong thực tế bởi vì những 
bảng LALR thu được nói chung là nhỏ hơn nhiều so với các bảng LR chính tắc và phần lớn các 
kết cấu cú pháp của ngôn ngữ lập trình đều có thể được diễn tả thuận lợi bằng văn phạm LALR.

a. Hạt nhân (core) của một tập hợp mục LR(1)

 
1. Một tập hợp mục LR(1) có dạng {[A 

 

α• β

, a]}, trong đó A 

 

α β

 là một 

luật sinh và a là ký hiệu kết thúc có hạt nhân (core) là tập hợp {A 

 

α

 

•β

}.

2. Trong họ tập hợp các mục LR(1) C = {I

0

, I

1

, ..., I

n

} có thể có các tập hợp các 

mục có chung một hạt nhân.

 
Ví dụ Trong ví dụ 4.25, ta thấy trong họ tập hợp mục có một số các mục có chung hạt  
nhân là : 

I4 và I11
I5 và I12
I7 và I13
I8 và I10

 

b. Thuật toán xây dựng bảng phân tích cú pháp LALR

 

 

Giải thuậ:  Xây dựng bảng phân tích LALR

 Input

Văn phạm tăng cường G'

Output

Bảng phân tích LALR

Phương pháp:

 

1

.   Xây dựng họ tập hợp các mục LR(1)  C = {I0, I1, ..., In } 

2.   Với mỗi hạt nhân tồn tại trong tập các mục LR(1) tìm trên tất cả các tập hợp 
có cùng hạt nhân này và  thay thế các tập hợp này bởi hợp của chúng.
3.   Ðặt C' = {I0, I1, ..., Im } là kết quả thu được từ  C bằng cách hợp các tập 
hợp có cùng hạt nhân. Action tương ứng với trạng thái i được xây dựng từ Ji 
theo cách thức như giải thuật 4.9.

background image

Nếu có một sự đụng độ giữa các action thì giải thuật xem như thất bại và ta 

nói văn phạm không phải là văn phạm LALR(1).
4.   Bảng goto được xây dựng như sau

Giả sử J = I1 

 I2 

 ... 

 Ik . Vì I1, I2, ... Ik có chung một hạt nhân nên goto 

(I1,X), goto (I2,X), ..., goto (Ik,X) cũng có chung hạt nhân. Ðặt K bằng hợp tất 
cả các tập hợp có chung hạt nhân với goto (I1,X) ( goto(J, X) = K.
 

Ví dụ : Với ví dụ trên, ta có họ tập hợp mục C' như sau
            
            C' = {I

0

, I

1

, I

2

, I

3

, I

411

, I

512

, I

6

, I

713

, I

810

, I

}

 

     I

0

 :                            S' 

 

 S, $

closure (S'  

S, $)  : S  

 

 L = R, $

                    S  

 

 R, $

                    L  

 

 * R, =

                    L  

 

 id, =

                    R 

 

 L, $

 

I

1

 = Goto (I

0

,S)  :         S' 

 S 

, $        

 
I

2 

= Goto (I

0

, L) :         S  

 L 

 = R, 

$   
                                        R 

  L 

, $

 

I

3

 = Goto (I 

0

,R) :         S 

  R 

   

 

I

411

 = Goto (I

0

,*), Goto (I

6

,*) :   

          L  

 * 

 R, = | $  

          R  

 

 L, =  | $     

          L  

 

 * R, = | $    

          R  

 

  id, = | $

I

512

 = Goto (I

0

,id), Goto (I

6

,id) : 

      L 

 id 

, = | $    

    
I

6

 = Goto(I

2

,=) :

      S 

 L = 

 R,$      

      R 

 

 L, $

      L 

 

 * R, $

      L 

 

 id, $

 
I

713

 = Goto(I

411

, R) : 

      L 

 * R

, = | $    

 
I

810

 = Goto(I

411

, L), Goto(I

6

, L): 

      R 

 L

, = | $    

 
I

9

 = Goto(I

6

, R) : 

      S 

 L = R

, $    

 
 

     

Ta có thể xây dựng bảng phân tích cú pháp LALR cho văn phạm như sau :

 

State

Action

Goto

=

*

id

$

S

L

R

0

 

s

411

s

512

 

1

2

3

1

 

 

acc

 

 

 

 

2

s

6

 

 

 

 

 

 

background image

3

 

 

 

r

2

 

 

 

411

 

 

 

 

 

810

713

512

r

4

 

 

r

4

 

 

 

6

 

s

411

s

512

 

 

810

9

713

r

3

 

 

r

3

 

 

 

810

r

5

 

 

r

5

 

 

 

9

 

 

 

r

1

 

 

 

 

Hình  - Bảng phân tích cú pháp LALR

     Bảng phân tích được tạo ra bởi giải thuật 4.10 gọi là bảng phân tích LALR cho văn phạm G. 
Nếu trong bảng không có các action đụng độ thì văn phạm đã cho gọi là văn phạm LALR(1). Họ 
tập hợp mục C' được gọi là họ tập hợp mục LALR(1).

2.4. Bắt lỗi.

* Giai đoạn phân tích cú pháp phát hiện và khắc phục được khá nhiều 

lỗi. Ví dụ lỗi do các từ tố từ bộ phân tích từ vựng không theo thứ tự của luật 
văn phạm của ngôn ngữ.

* Bộ bắt lỗi trong phần phân tích cú pháp có mục đích:
+ Phát hiện, chỉ ra vị trí và mô tả chính xác rõ rang các lỗi.
+ Phục hồi quá trìh phân tích sau khi gặp lỗi đủ nhanh để có thể phát 

hiện ra các lỗi tiếp theo.

+ Không làm giảm đáng kể thời gian xử lý các chương trình viết đúng.
* Các chiến lược phục hồi lỗi.
- Có nhiều chiến lược mà bộ phân tích có thể dùng để phục hồi quá 

trình phân tích sau khi gặp một lỗi cú pháp. Không có chiến lược nào tổng 
quát và hoàn hảo, có một số phương pháp dùng rộng rãi.

+ Phục hồi kiểu trừng phạt: Phương pháp đơn giản nhất và được áp 

dụng trong đa số các bộ phân tích. Mỗi khi phát hiện lỗi bộ phân tích sẽ bỏ 
qua một hoặc một số kí hiệu vào mà không kiểm tra cho đến khi nó gặp một kí 
hiệu trong tập từ tố đồng bộ. Các từ tố đồng bộ thường được xác định trước 
( VD: end, ; )

Người thiết kế chương trình dịch phải tự chọn các từ tố đồng bộ. 
Ưu điểm: Đơn giản, không sợ bj vòng lặp vô hạn, hiệu quả khi gặp câu 

lệnh có nhiều lỗi.

background image

+ Khôi phục cụm từ: Mỗi khi phát hienj lỗi, bộ phân tích cố gắng phân 

tích phần còn lại của câu lệnh. Nó có thể thay thế phần đầu của phần còn lại 
xâu này bằng một xâu nào đó cho phép bộ phân tích làm việc tiếp. Những việc 
này do người thiết kế chương trình dịch nghĩ ra.

+ Sản xuất lỗi: Người thiết kế phải có hiểu biết về các lỗi hường gặp và 

gia cố văn phạm của  ngôn ngữ này tại các luật sinh ra cấu trúc lỗi. Dùng văn 
phạm này để khôi phục bộ phân tích. Nếu bọ phân tích dùng một luật lỗi có 
thể chỉ ra các cấu trúc lỗi phát hiện ở đầu vào.

Ngoài ra ngừơi ta có cách bắt lỗi cụ thể hơn trong từng phương pháp phân 

tích khác nhau.

2.4.1. Khôi phục lỗi trong phân tích tất định LL.

* Một lỗi được phát hiện trong phân tích LL khi:
-  Ký hiệu kết thúc nằm trên đỉnh ngăn xếp không đối sánh được với ký hiệu 

đầu vào hiện tại.

- Mục M(A,a) trong bảng phân tích là lỗi (rỗng).
* Khắc phục lỗi theo kiểu trừng phạt là bỏ qua các ký hiệu trên xâu vào cho 

đến khi xuất hiện một ký hiệu thuộc tập ký hiệu đã xác định trước gọi là tập ký hiệu 
đồng bộ. Xét một số cách chọn tập đồng bộ như sau:

a)

Đưa tất cả các ký hiệu trong Follow(A) vào tập đồng bộ hoá của ký 

hiệu không kết thúc A. Nếu gặp lỗi, bỏ qua các từ tố của xâu vào cho đến khi gặp 
một phần tử của Follow(A) thì lấy A ra khỏi ngăn xếp và tiếp tục quá trình phân tích.

b)

Đưa tất cả các ký hiệu trong First(A) vào tập đồng bộ hoá của ký hiệu 

không kết thúc A. Nếu gặp lỗi, bỏ qua các từ tố của xâu vào cho đến khi gặp một 
phần tử thuộc First(A) thì quá trình phân tích được tiếp tục.
Ví dụ: với ví dụ trên, ta thử phân tích xâu vào có lỗi là “)id*+id” với tập đồng bộ 
hoá của các ký hiệu không kết thúc được xây dựng từ tập First và tập Follow của 
ký hiệu đó.

Ngăn xếp

Xâu vào

Hành động

$E

)id*+id$

M(E,)) = lỗi, bỏ qua ‘)’ để găp id 

 First(E)

$E

id*+id$

E->TE’

$E’T

id*+id$

T->FT’

$E’T’F

id*+id$

F->id

$E’T’id

id*+id$

rút gọn id

background image

$E’T’

*+id$

T’->*FT’

$E’T’F*

*+id$

rút gọn *

$E’T’F

+id$

M(F,+) = lỗi, bỏ qua. Tại đây xảy ra hai trường hợp(ta 
chọn a):    a).bỏ qua + vì id 

 First(F) 

       b).bỏ qua F vì + 

 Follow(F)

$E’T’F

id$

F->id

$E’T’id

id$

rút gọn id

$E’T’

$

T’->

ε

$E’

$

E’->

ε

$

$

2.4.2. Khôi phục lỗi trong phân tích LR.

Một bộ phân tích LR sẽ phát hiện ra lỗi khi nó gặp một mục báo lỗi trong 

bảng action (chú ý sẽ không bao giờ bộ phân tích gặp thông báo lỗi trong bảng 
goto). Chúng ta có thể thực hiện chiến lược khắc phục lỗi cố gắng cô lập đoạn câu 
chứa lỗi cú pháp: quét dọc xuống ngăn xếp cho đến khi tìm được một trạng thái s 
có một hành động goto trên một ký hiệu không kết thúc A ngay sau nó. Sau đó bỏ 
đi không hoặc nhiều ký hiệu đầu vào cho đến khi gặp một ký hiệu kết thúc a thuộc 
Follow(A), lúc này bộ phân tích sẽ đưa trạng thái goto(s,A) vào ngăn xếp và tiếp 
tục quá trình phân tích.

background image

Phương pháp phân tích bảng  CYK (Cocke – Younger – Kasami)

- Giải thuật làm việc với tất cả các VP PNC. Thời gian phân tích là: n

3

 (n là 

độ dài xâu vào cần phân tích), nếu văn phạm không nhập nhằng thì thờI gian phân 
tích là: n

2

.

- Điều kiện của thuật toán là văn phạm PNC ở dạng chuẩn chômsky (CNF) 

và không có 

ε

 sản xuất (sản xuất A 

 

ε

 ) và các kí hiệu vô ích.

Giải thuật CYK:
- Tạo một hình tam giác (mỗi chiều có độ dài là n , n là độ dài của xâu). 

Thực hiện giải thuật:

Begin

1) For i:=1 to n do

ij

 = { A | A 

 a là một sản xuất và a là kí hiệu thứ i trong w};

2) For j:=2 to n do

For i:=1 to (n – j +1) do

Begin

ij

 = 

;

For k:=1 to (j -1) do

ij

 = 

ij

 

  { A | A 

 BC là một sản xuất; B 

 

ik

 C

 

i+k, j -k 

};

end;

end;

Ví dụ: Xét văn phạm chuẩn chômsky 

 AB|BC; A 

 BA|a;      B 

 CC|b;            C 

 AB|a;

        (1)    (2)           (3) (4)              (5) (6)                    (7) (8) 
Xâu vào w= baaba;
        i        
j

b             a          a           b         a

B

A,C

A,C

B

A,C

S,A

B

S,C

S,A

B

B

S,A,C

background image

S,A,C

b             a          a           b         a

B

A,C

A,C

B

A,C

S,A

B

S,C

S,A

B

B

S,A,C

S,A,C

- Quá trình tính  

ij 

 . VD: tính 

 24

 , Tính:

 

21 = 

{A,C}, 

 

 

33

 = {B}, 

21

33

 = {AB,CB} Do (1), (7) nên đưa S,C vào 

 24

.

22 = 

{B}, 

 

 

42

 = {S,A}, 

22

42

 = {BS,BA} Do (3) nên đưa A vào 

 24

.

23 = 

{B}, 

 

 

51

 = {A,C}, 

23

51

 = {BA,BC}  (2),(3) nên đưa S,C vào 

∆  

24

.

Kết quả: 

 24

 = {S,A,C}.

- Nếu S ở ô cuối cùng thì ta kết luận: Xâu vào phân tích thành công và có 

thể dựng được cây phân tích cho nó. Số lượng cây phân tích = số lượng S có 
trong ô này.

b             a          a           b         a

B

A,C

A,C

B

A,C

S,A

B

S,C

S,A

B

B

S,A,C

S,A,C

A

B

S

B

B

A C

C

b

a

C

A B a

a

b

background image

Bài tập

Luyện tập: 
cho văn phạm 

E -> T + E | T
T -> a

Hãy xây dựng bảng SLR(1) cho văn phạm trên

Thực hành: Thử nghiệm trên văn phạm biểu thức nêu trên

1) xây dựng tập LR(0) tự động
2) xây dựng bảng phân tích SLR(1) tự động
3) phân tích xâu vào

background image

CHƯƠNG 5                  BIÊN DỊCH DỰA CÚ PHÁP.

1. MỤC ĐÍCH, NHIỆM VỤ.

- Các hành động dịch phụ thuộc rất nhiều vào cú pháp của chương trình nguồn 

cần dịch.Quá trình dịch được điều khiển theo cấu trúc cú pháp của chương 
trình nguồn, cú pháp này được xác định thông qua bộ phân tích cú pháp.

- Nhằm điều khiển các phần hoạt động theo cú pháp, cách thường dùng là gia 

cố các luật sản xuất ( mà ta biết cụ thể những luật nào và thứ tự thực hiện ra 
sao thông qua cây phân tích) bằng cách thêm các thuộc tính cho văn phạm 
đấy, và các qui tắc sinh thuộc tính gắn với từng luật cú pháp. Các qui tắc đó, 
ta gọi là qui tắc ngữ nghĩa (semantic rules).

- thực hiện các qui tắc ngữ nghĩa đó sẽ cho thông tin về ngữ nghĩa, dùng để 

kiểm tra kiểu, lưu thông tin vào bảng ký hiệu và sinh mã trung gian.

- Có hai tiếp cận để liên kết (đặc tả) các qui tắc ngữ nghĩa vào các luật cú pháp 

(sản xuất) là cú pháp điều khiển (syntax-directed definition) và lược đồ dịch 
(translation scheme). 

- Các luật ngữ nghĩa còn có các hành động phụ (ngoài việc sinh thuộc tính cho 

các ký hiệu văn phạm trong sản xuất) như in ra một giá trị hoặc cập nhật một 
biến toàn cục.

Các kiến thức trong phần này không nằm trong khối chức năng riêng rẽ nào của chương 

trình dịch mà được dùng làm cơ sở cho toàn bộ các khối nằm sau khối phân tích cú pháp. 

Một xâu vào 

 Cây phân tích  

 Đồ thị phụ thuộc  

 thứ tựđánh giá cho các luật ngữ 

nghĩa.

2. ĐỊNH NGHĨA CÚ PHÁP ĐIỀU KHIỂN.
Cú pháp điều khiển (syntax-directed definition) là một dạng tổng quát hoá của 

văn phạm phi ngữ cảnh, trong đó mỗi ký hiệu văn phạm có một tập thuộc tính đi 
kèm, được chia thành 2 tập con là thuộc tính tổng hợp (synthesized attribute) và 
thuộc tính kế thừa (inherited attribute) của ký hiệu văn phạm đó.

Một cây phân tích cú pháp có trình bày các giá trị của các thuộc tính tại mỗi 

nút được gọi là cây phân tích cú pháp có chú giải (hay gọi là cây phân tích đánh 
dấu) (annotated parse tree).

2.1. Cú pháp điều khiển.

2.1.1. Dạng của định nghĩa cú pháp điều khiển.

Trong mỗi cú pháp điều khiển, mỗi sản xuất A->

α

 có thể được liên kết với 

một tập các qui tắc ngữ nghĩa có dạng b = f(c

1

, . . .,c

k

) với f là một hàm và

a)

b là một thuộc tính tổng hợp của A, còn c

1

, . . .,c

k

 là các thuộc tính của các ký 

hiệu trong sản xuất đó. Hoặc

b)

b là một thuộc tính kế thừa của một trong những ký hiệu ở vế phải của sản 
xuất, còn c

1

, . . . ,c

k

 là thuộc tính của các ký hiệu văn phạm.

background image

Ta nói là thuộc tính b phụ thuộc vào các thuộc tính c

1

, . . .,c

k.

- Một văn phạm thuộc tính (Attribute Grammar) là một cú pháp điều khiển 

mà các luật ngữ nghĩa không có hành động phụ.
Ví dụ: Sau đây là văn phạm cho một chương trình máy tính bỏ túi với val là một 
thuộc tính biểu diễn giá trị của ký hiệu văn phạm.

Sản xuất

Luật ngữ nghĩa

L -> E n

Print(E.val)

E -> E

1

 + T

E.val = E

1

.val + T.val

E -> T

E.val = T.val

T -> T

1

 * F

T.val = T

1

.val * F.val

T -> F

T.val = F.val

F -> ( E )

F.val = E.val

F -> digit

F.val = digit.lexval

Từ tố digit có thuộc tính Lexval: là giá trị của digit đó được tính nhờ bộ phân tích từ vựng. Kí  
hiệu n : xuống dòng, Print : in kết quả ra màn hình.

 2.1.2. Thuộc tính tổng hợp.

Trên một cây phân tích, thuộc tính tổng hợp được tính dựa vào các thuộc ở 

các nút con của nút đó, hay nói cách khác thuộc tính tổng hợp được tính cho các ký 
hiệu ở vế trái của sản xuất và tính dựa vào thuộc tính của các ký hiệu ở vế phải.

Một cú pháp điều khiển chỉ sử dụng các thuộc tính tổng hợp được gọi là cú 

pháp điều khiển thuần tính S (S-attribute definition). 

Một cây phân tích cho văn phạm cú pháp điều khiển thuần tính S có thể thực 

hiện các luật ngữ nghĩa theo hướng từ lá đến gốc và có thể sử dụng trong phương 
pháp phân tích LR.
Ví dụ:  vẽ cây cho đầu vào: 3*4+4n

ví dụ 1

Chúng ta duyệt và thực hiện các hành 
động ngữ nghĩa của ví dụ trên theo đệ 

qui 

trên xuống: khi gặp một nút ta sẽ thực 
hiện tính thuộc tính tổng hợp của các 
con của nó rồi thực hiện hành động ngữ 
nghĩa trên nút đó. Nói cách khác, khi phân tích cú pháp theo kiểu bottom-up, thì khi nào 

L

E

1

E

2

T

3

T

1

T

2

*

F

2

F

1

3

+

F

3

n

4

4

background image

gặp hành động thu gọn, chúng ta sẽ thực hiện hành động ngữ nghĩa để đánh giá thuộc 
tính tổng hợp.

F

1

.val=3 (syntax: F

1

->3  semantic: F

1

.val=3.lexical)

F

2

.val=4 (syntax: F

2

->3  semantic: F

2

.val=4.lexical)

T

2

.val=3 (syntax: T

2

->F

1

 semantic: T

2

.val=F

1

.val )

T

1

.val=3*4=12 (syntax: T

1

->T

2

*F

2

  semantic: T

1

.val=T

2

.val*F

2

.val)

F

3

.val=4 (syntax: F

3

->4  semantic: F

3

.val=4.lexical)

T

3

.val=4 (syntax: T

3

->F

3

 semantic: T

3

.val=F

3

.val )

E

1

.val=12+4=16 (syntax: E

1

->E

2

+T

 semantic: E

1

.val=E

2

.val+T

3

.val)

“16” (syntax: L->E

1

 n  semantic: print(E

1

.val))

2.1.3. Thuộc tính kế thừa.

Thuộc tính kế thừa (inherited attribute) là thuộc tính tại một nút có giá trị 

được xác định theo giá trị thuộc tính của cha hoặc anh em của nó.

Thuộc tính kế thừa rất có ích trong diễn tả sự phụ thuộc ngữ cảnh. Ví dụ 

chúng ta có thể xem một định danh xuất hiện bên trái hay bên phải của toán tử gán 
để quyết định dùng địa chỉ hay giá trị của định danh.
Ví dụ về khai báo:

sản xuất

luật ngữ nghĩa

D -> T L

L.in := T.type

T -> int

T.type := interger

T -> real

T.type := real

L -> L

1

, id

L

1

.in := L.in ; addtype(id.entry, L.in)

L -> id

addtype(id.entry,L.in)

Ví dụ: int a,b,c   Ta có cây cú pháp:

Chúng ta duyệt và thực hiện các hành 
động ngữ nghĩa sẽ được kết quả như 
sau:

T.type = interger (syntax:T->int   semantic: T.type=interger)
L

1

.in = interger (syntax: D -> T L

1

  semantic: L

1

.in=T.type)

D

T

L

1

int

L

2

a

,

L

3

b

,

c

background image

L

2

.in = interger (syntax: L

1

 -> L

2

 , a   semantic: L

2

.in = L

1

.in )

a.entry = interger (syntax: L

1

 -> L

2

 , a   semantic: addtype(a.entry,L

1

.in) )

L

3

.in = interger (syntax: L

2

 -> L

3

 , b   semantic: L

3

.in = L

2

.in )

b.entry = interger (syntax: L

2

 -> L

3

 , b   semantic: addtype(b.entry,L

2

.in) )

c.entry = interger (syntax: L

3

 -> c   semantic: addtype(c.entry,L

3

.in) )

Bài luyện tập:

1) Cho văn phạm sau định nghĩa một số ở hệ cơ số 2

B -> 0 | 1 | B 0 | B 1

Hãy định nghĩa một cú pháp điều khiển để dịch một số ở hệ cơ số 2 thành một số ở hệ cơ số 
10 (hay nói cách khác là tính giá trị của một số ở hệ cơ số 2). Xây dựng cây đánh dấu(xây 
dựng cây cú pháp cùng với giá trị thuộc tính trên mỗi nút) với đầu vào là “1001”.
Mở rộng: sinh viên tự làm bài toán này với các sản xuất định nghĩa một số thực ở hệ cơ số 2:

S->L.L | L
L->LB | B
B->0 | 1

Lời giải: Định nghĩa thuộc tính tổng hợp val của ký hiệu B để chứa giá trị tính được của số 
biểu diễn bởi B.
xuất phát từ cách tính:
(a

n

a

n-1

 . . . a

1

a

0

)

2

  := a

n

*2

n

+a

n-1

*2

n-1

+. . . +a

1

*2+a

0

:= 2*(a

n

*2

n-1

+. . .+a

1

)+a

0

:= 2*(a

n

. . .a

1

)+a

0

Do đó nếu có
B -> B

1

 1 thì B.val := 2*B

1

.val+1

B -> B

1

 0 thì B.val := 2*B

1

.val

Vì vậy, chúng ta xây dựng các luật dịch như sau:

Luật phi ngữ cảnh

Luật dịch

B->0

B.val=0;

B->1

B.val:=1;

B->B

1

 0

B.val:=2*B

1

.val +0

B->B 1

B.val:=2*B

1

.val+1

Cây đánh dấu:

1

0

0

B

:  val:=2*1+0=2

B

: val:=2*2+0=4

B

:  val:=2*4+1=9

background image

Xét một cây đánh dấu khác cho xâu vào “1011”

2.2. Đồ thị phụ thuộc.

Nếu một thuộc tính b tại một nút trong cây phân tích cú pháp phụ thuộc vào một 

thuộc tính c, thế thì hành động ngữ nghĩa cho b tại nút đó phải được thực hiện sau khi  
thực hiện hành động ngữ nghĩa cho c. Sự phụ thuộc qua lại của các thuộc tính tổng hợp  
và kế thừa tại các nút trong một cây phân tích cú pháp có thể được mô tả bằng một đồ thị  
có hướng gọi là đồ thị phụ thuộc (dependency graph).

- Đồ thị phụ thuộc là một đồ thị có hướng mô tả sự phụ thuộc giữa các thuộc 

tính tại mỗi nút của cây phân tích cú pháp.

 Trước khi xây dựng một đồ thị phụ thuộc cho một cây phân tích cú pháp, 

chúng ta chuyển mỗi hành động ngữ nghĩa thành dạng b := f(c

1

,c

2

,. . .,c

k

) bằng cách 

dùng một thuộc tính tổng hợp giả b cho mỗi hành động ngữ nghĩa có chứa một lời 
gọi thủ tục. Đồ thị này có một nút cho mỗi thuộc tính, một cạnh đi vào một nút cho 
b từ một nút cho c nếu thuộc tính b phụ thuộc vào thuộc tính c. Chúng ta có thuật 
toán xây dựng đồ thị phụ thuộc cho một văn phạm cú pháp điều khiển như sau:

for mỗi nút n trong cây phân tích cú pháp do

for mỗi thuộc tính a của ký hiệu văn phạm tại n do

B

:  val:=1

1

1

1

0

B

:  val:=1

1

B

:  val:=2*1+0=2

B

: val:=2*2+1=5

B

val:=5*2+1=11

background image

xây dựng một nút trong đồ thị phụ thuộc cho a;

for mỗi nút n trong cây phân tích cú pháp do

for mỗi hành động ngữ nghĩa b:=f(c

1

,c

2

, . . .,c

k

)

đi kèm với sản xuất được dùng tại n do
for
 i:=1 to k do

xây dựng một cạnh từ nút c

i

 đến nút b

VD 1: Dựa vào cây phân tích ( nét đứt đoạn) và luật ngữ nghĩa ứng với sản xuất ở bảng, ta thêm  
các nút và cạnh thành đồ thị phụ thuộc:

 

Ví dụ 2: 
Với ví dụ 2, ta có một đồ thị phụ thuộc như sau:
chú ý:
+ chuyển hành động ngữ nghĩa addentry(id.entry,L.in) của sản xuất L->L , id thành thuộc tính giả 
f phụ thuộc vào entry và in

sản xuất

luật ngữ nghĩa

D -> T L

L.in := T.type

T -> int

T.type := interger

T -> real

T.type := real

L -> L

1

, id

L

1

.in := L.in ; addtype(id.entry, L.in)

L -> id

addtype(id.entry,L.in)

D

T

L

rea

l

c

L

,

b

L

,

a

type

in

in

in

entry

entry

entry

f

f

f

Sản xuất

Luật ngữ nghĩa

 E

1

 | E

2

E.val = E

1

.val + E

2

.val

E

E

1

+

E

2

Val

Val

background image

2.3. Thứ tự đánh giá thuộc tính.

Trên đồ thị DAG được xây dựng như ví dụ trên, chúng ta phải xác định thứ 

tự của các nút để làm sao cho khi duyệt các nút theo thứ tự này thì một nút sẽ có 
thứ tự sau nút mà nó phụ thuộc ta gọi là một sắp xếp topo. Tức là nếu các nút được 
đánh thứ tự m

1

, m

2

, . . .,m

k

 thì nếu có m

->m

j

 là một cạnh từ m

i

 đến m

j

 thì m

i

 xuất 

hiện trước m

j

 trong thứ tự đó hay i<j. Nếu chúng ta duyệt theo thứ tự đã được sắp 

xếp này thì sẽ được một cách duyệt hợp lý cho các hành động ngữ nghĩa. Nghĩa là 
trong một sắp xếp topo, giá trị các thuộc tính phụ thuộc c

1

,c

2

 . . .

 ,c

k

 trong một hành 

động ngữ nghĩa b:=f(c

1

,c

2

 . . .

 ,c

k

) đã được tính trước khi ta ước lượng f.

Đối với một đồ thị tổng quát, chúng ta phải để ý đến các đặc điểm sau:

+ xây dựng đồ thị phụ thuộc cho các thuộc tính của ký hiệu văn phạm phải 

được xây dựng trên cây cú pháp. Tức là xây dựng cây cú pháp với mỗi nút (đỉnh) 
đại diện cho một ký hiệu văn phạm sau đó mới xây dựng đồ thị phụ thuộc theo 
thuật toán 5.1

+ trong đồ thị phụ thuộc, mỗi nút đại diện cho một thuộc tính của một ký 

hiệu văn phạm.

+ có thể một loại thuộc tính này lại phụ thuộc vào một loại thuộc tính khác, 

chứ không nhất thiết là chỉ các thuộc tính cùng loại mới phụ thuộc vào nhau. Trong 
ví dụ trên, thuộc tính entry phụ thuộc vào thuộc tính in.

+ có thể có “vòng” trong đồ thị phụ thuộc, khi đó chúng ta sẽ không tính 

được giá trị ngữ nghĩa cho các nút vì gặp một hiện tượng khi tính a cần tính b, mà 
khi tính b lại cần tính a.

Chính vì vậy, trong thực tế chúng ta chỉ xét đến văn phạm cú pháp ngữ nghĩa 

mà đồ thị phụ thuộc của nó là một DAG không có vòng.

Đối với ví dụ trên, chúng ta xây dựng được một thứ tự phụ thuộc trên các 

thuộc tính đối với cây cú pháp cho câu vào “real a,b,c” như sau:

D

T

L

rea

l

c

L

,

b

,

type: 4

in: 5

in: 7

in: 8

entry: 3

entry: 2

f: 8

f: 6

f: 9

background image

Sau khi chúng ta đã có đồ thị phụ thuộc này, chúng ta thực hiện các hành động ngữ 
nghĩa theo thứ tự như sau (ký hiệu a

i

 là giá trị thuộc tính ở nút thứ i):

- đối với nút 1,2 ,3 chúng ta duyệt qua nhưng chưa thực hiện hành động ngữ 

nghĩa nào cả

-

nút 4: ta có a

4

 := real 

-

nút 5: a

5

 := a

4

 

:= real

-

nút 6:  addtype(c.entry,a

5

) = addtype(c.entry,real) 

-

nút 7: a

7

 := a

5

 

:= real

-

nút 8:  addtype(b.entry,a

7

) = addtype(b.entry,real) 

-

nút 9:  addtype(a.entry,a

8

) = addtype(a.entry,real) 

Các phương pháp duyệt hành động ngữ nghĩa

1.

Phương pháp dùng cây phân tích cú pháp.  Kết quả trả về của phân tích cú 
pháp phải là cây phân tích cú pháp, sau đó xây dựng một thứ tự duyệt hay 
một sắp xếp topo của đồ thị từ cây phân tích cú pháp đó. Phương pháp này 
không thực hiện được nếu đồ thị phụ thuộc có “vòng”.

2.

Phương pháp dựa trên luật. Vào lúc xây dựng trình biên dịch, các luật ngữ 
nghĩa được phân tích (thủ công hay bằng công cụ) để thứ tự thực hiện các 
hành động ngữ nghĩa đi kèm với các sản xuất được xác định trước vào lúc 
xây dựng.

3.

Phương pháp quên lãng (oblivious method). Một thứ tự duyệt được lựa chọn 
mà không cần xét đến các luật ngữ nghĩa. Thí dụ nếu quá trình dịch xảy ra 
trong khi phân tích cú pháp thì thứ tự duyệt phải phù hợp với phương pháp 
phân tích cú pháp, độc lập với luật ngữ nghĩa. Tuy nhiên phương pháp này 
chỉ thực hiện trên một lớp các cú pháp điều khiển nhất định.

Phương pháp dựa trên qui tắc và phương pháp quên lãng không nhất thiết phải xây dựng một đồ  
thị phụ thuộc, vì vậy nó rất là hiệu quả về mặt thời gian cũng như không gian tính toán.
Trong thực tế, các ngôn ngữ lập trình thông thường có yêu cầu quá trình phân tích là tuyến tính, 
quá trình phân tích ngữ nghĩa phải kết hợp được với các phương pháp phân tích cú pháp tuyến  
tính như LL, LR. Để thực hiện được điều này, các thuộc tính ngữ nghĩa cũng cần thoả mãn điều  
kiện: một thuộc tính ngữ nghĩa sẽ được sinh ra chỉ phụ thuộc vào các thông tin trước nó. Chính  
vì vậy chúng ta sẽ xét một lớp cú pháp điều khiển rất thông dụng và được sử dụng hiệu quả gọi  
là cú pháp điều khiển thuần tính L.

Cú pháp điều khiển thuần tính L 

Một thứ tự duyệt tự nhiên đặc trưng cho nhiều phương pháp dịch Top-down và Bottom-up  

là thủ tục duyệt theo chiều sâu (depth-first order). Thủ tục duyệt theo chiều sâu được trình bày  
như dưới đây:

procedure dfvisit(n:node);

L

a

entry: 1

background image

begin

for mỗi con m của n tính từ trái sang phải do 
begin

tính các thuộc tính kế thừa của m
dfvisit(m)

end
tính các thuộc tính tổng hợp của n

end
 

Một lớp các cú pháp điều khiển được gọi là cú pháp điều khiển thuần tính L hay gọi là 

điều khiển thuần tính L (L-attributed definition) có các thuộc tính luôn có thể tính toán theo chiều 
sâu.

Cú pháp điều khiển thuần tính L:

Một cú pháp điều khiển gọi là thuần tính L nếu mỗi thuộc tính kế thừa của X

ở vế phải của luật sinh A -> X

1

 X

2

 . . . X

n

 với 1<=j<=n chỉ phụ thuộc vào:

1.

các thuộc tính của các ký hiệu X

1

, X

2

, . . .,X

j-1

 ở bên trái của X

j

 trong 

sản xuất và

2.

các thuộc tính kế thừa của A

Chú ý rằng mỗi cú pháp điều khiển thuần tính S đều thuần tính L vì các điều  

kiện trên chỉ áp dụng cho các thuộc tính kế thừa.

Ta thấy nếu ngôn ngữ mà ngữ nghĩa của một từ tố được xác định chỉ phụ 

thuộc vào ngữ cảnh bên trái (các từ tố bên trái) thì một phương pháp duyệt cú pháp 
từ trái sang phải cho đầu vào có thể kết hợp với điều khiển ngữ nghĩa để duyệt cả 
cú pháp và ngữ nghĩa đồng thời. Từ đó, ta thấy cú pháp điều khiển thuần tính L 
thoả mãn điều kiện này. Hay với cú pháp điều khiển thuần tính L, ta có thể duyệt 
đầu vào từ trái sang phải để sinh các thông tin cú pháp và ngữ nghĩa một cách đồng 
thời. Với phương pháp phân tích cú pháp tuyến tính LL và LR, ta có thể kết hợp để 
thực hiện cả các hành động ngữ nghĩa thuần tính L.

Nhưng nếu mô tả các hành động ngữ nghĩa theo cú pháp điều khiển thì 

không xác định thứ tự của các hành động trong một sản xuất. Vì vậy ở đây ta xét 
một tiếp cận khác là dùng lược đồ dịch để mô tả luật ngữ nghĩa đồng thời với

 thứ 

tự 

thực hiện chúng trong một sản xuất

.

Thực hiện hành động ngữ nghĩa trong phân tích LL

Thiết kế dịch là dịch một lượt: khi ta đọc đầu vào đến đâu thì chúng ta sẽ 

phân tích cú pháp đến đó và thực hiện các hành động ngữ nghĩa luôn.

Một phương pháp xây dựng chương trình phân tích cú pháp kết hợp với thực 

hiện các hành động ngữ nghĩa như sau:

- với mỗi một ký hiệu không kết thúc được gắn với một hàm thực hiện. Giả sử 

với ký hiệu không kết thúc A, ta có hàm thực hiện 

void ParseA(Symbol A);

background image

- mỗi ký hiệu kết thúc được gắn  với một hàm đối sánh xâu vào

-

giả sử ký hiệu không kết thúc A là vế trái của  luật   A-> 

α

1

 | 

α

2

 | . . . | 

α

n

Như vậy hàm phân tích ký hiệu A sẽ được định nghĩa như sau:
void ParseA(Symbol A, Rule r, ...)
{

if(r==A->

α

1

)

gọi hàm xử lý ngữ nghĩa tương ứng luật A->

α

1

else if(r==A->

α

2

)

gọi hàm xử lý ngữ nghĩa tương ứng luật A->

α

2

. . . 
else if(r==A->

α

n

)

gọi hàm xử lý ngữ nghĩa tương ứng luật A->

α

n

}
Đối chiếu ký hiệu đầu vào và A, tìm trong bảng phân tích LL xem sẽ khai 
triển A theo luật nào. Chẳng hạn ký hiệu xâu vào hiện thời a  

  first(

α

i

), 

chúng ta sẽ khai triển A theo luật A -> X

1

. . . X

k

 với 

α

i

 = X

1

. . . X

k

Ở đây, ta sẽ sử dụng lược đồ dịch để kết hợp phân tích cú pháp và ngữ nghĩa. 
Do đó đó khi khai triển A theo vế phải, ta sẽ gặp 3 trường hợp sau:
1. nếu phần tử đang xét là một ký hiệu kết thúc, ta gọi hàm đối sánh với xâu 

vào, nếu thoả mãn thì nhẩy con trỏ đầu vào lên một bước, nếu trái lại là 
lỗi.

2. nếu phần tử đang xét là một ký hiệu không kết thúc, chúng ta gọi hàm 

duyệt ký hiệu không kết thúc này với tham số bao gồm các thuộc tính của 
các ký hiệu anh em bên trái, và thuộc tính kế thừa của A.

3. nếu phần tử đang xét là một hành động ngữ nghĩa, chúng ta thực hiện 

hành động ngữ nghĩa này.

Ví dụ:

E ->  T {R.i:=T.val} 

{E.val:=R.s}

R ->  +

{R

1

.i:=R.i+T.val}

R

1

 {R.s:=R

1

.s}

R -> 

ε

 {R.s:=R.i}

T ->  ( E ) {T.val:=E.val}
T ->  num {T.val:=num.val}

void ParseE(...)
{

// chỉ có một lược đồ dịch:
// E -> T {R.i:=T.val}
 

// R {E.val:=R.s}

ParseT(...);

R.i := T.val

background image

ParseR(...);

E.val := R.s

}

void ParseR(...)
{

// trường hợp 1
//R ->  +

//T {R1.i:=R.i+T.val}
//R1 {R.s:=T.val+R1.i}

if(luật=R->TR

1

)

match(‘+’);// đối sánh
ParseT(...); R

1

.i:=R.i+T.val;

ParseR(...); R.s:=R

1

.s

}
else if(luật=R->

ε

)

{ // R ->

ε

 {R.s:=R.i}

R.s:=R.i

}

 }

Tương tự đối với hàm ParseT()
Bây giờ ta xét xâu vào: “6+4”

First(E)=First(T) = {(,num}
First(R) = {

ε

,+}

Follow(R) = {$,)}
Xây dựng bảng LL(1)

num

+

(

)

$

E

E->TR

E->TR

T

T->num

T->(E)

R

R->+TR

R->

ε

R->

ε

Đầu vào “6+4”, sau khi phân tích từ vựng ta được “num1 + num2”

Ngăn xếp

Đầu vào

Luật sản xuất

Luật ngữ nghĩa

$E
$RT
$Rnum1
$R
$R

1

T+

$R

1

T

$R

1

num2

$R

1

$

num1 + num2 $
num1 + num2 $
num1 + num2 $

+ num2 $
+ num2 $

num2 $
num2 $

$
$

E->TR
T->num1

R->+TR

1

T->num2

R

1

->

ε

T.val=6

R.i=T.val=6

T.val=4

R

1

.i=T.val=4

R

1

.s=T.val+R

1

.i=10

R.s=R

1

.s=10

E.val=R.s=10

background image

Nhận xét:

Mọi cú pháp điều khiển thuần tính L dựa trên văn phạm LL(1) đều có thể kết hợp quá  

trình phân tích cú pháp tuyến tính với việc thực hiện các hành động ngữ nghĩa.

Thực hiện hành động ngữ nghĩa trong phân tích LR

Đối với cú pháp điều khiển thuần tính S (chỉ có các thuộc tính tổng hợp), tại mỗi 

bước thu gọn bởi một luật, chúng ta thực hiện các hành động ngữ nghĩa tính thuộc tính 
tổng hợp của vế trái dựa vào các thuộc tính tổng hợp của các ký hiệu vế phải đã được 
tính.
Ví dụ, đối với cú pháp điều khiển tính giá trị biểu thức cho máy tính bỏ túi:

Luật cú pháp

Luật ngữ nghĩa (luật dịch)

L->E n

print(E.val)

E->E

1

+T

E.val:=E

1

.val+T.val

E->T

E.val:=T.val

T->T

1

*F

T.val:=T

1

.val*F.val

T->F

T.val:=F.val

F->(E)

F.val:=E.val

F->digit

F.val:=digit.lexval

Chẳng hạn chúng ta sẽ thực hiện các luật ngữ nghĩa này bằng cách sinh ra thêm một 
ngăn xếp để lưu giá trị thuộc tính val cho các ký hiệu (gọi là ngăn xếp giá trị). Mỗi khi 
trong ngăn xếp trạng thái có ký hiệu mới, chúng ta lại đặt vào trong ngăn xếp giá trị 
giá trị thuộc tính val cho ký hiệu mới này. Còn nếu khi ký hiệu bị loại bỏ ra khỏi ngăn 
xếp trạng thái thì chúng ta cũng loại bỏ giá trị tương ứng với nó ra khỏi ngăn xếp giá 
trị. Chúng ta có thể xem qua quá trình phân tích gạt, thu gọn với ví dụ cho xâu vào 
“3*5+4”:
chú ý:
+ phân tích từ tố cho ta kết quả xâu vào là (ký hiệu d là digit):

d1(3)*d2(5)+d3(4)n

+ với ký hiệu không có giá trị val, chúng ta ký hiệu ‘-‘ cho val của nó

xâu vào

ngăn xếp trạng 
thái

ngăn   xếp   giá 
trị 

luật cú pháp, ngữ nghĩa

d1 * d2 + d3 n

gạt

* d2 + d3 n

d1

3

F->digit

* d2 + d3 n

F

3

F.val:=digit.lexval

(loại bỏ digit)

T->F

 * d2 + d3 n

T

3

T.val:=F.val

(loại bỏ F)

gạt

d2 + d3 n

* T

- 3

gạt

background image

+ d3 n

d2 * T

5 - 3

F->digit

+ d3 n

F * T

5 – 3

F.val:=digit.lexval

(loại bỏ digit)
T->T

1

*F

+ d3 n

T

15

T.val:=T

1

.val*F.val

(loại bỏ T

1

,*,F)

E->T

+ d3 n

 E

 15

E.val:=T.val
(loại bỏ T)
gạt

d3 n

 + E

- 15

gạt

n

d3 + E

4 - 15

F->digit

n

F + E

4 – 15

F.val:=digit.lexval

(loại bỏ digit)
T->F

n

T + E

4 - 15

T.val:=F.val
(loại bỏ F)
E->E

1

+T

n

E

19

E.val:=E

1

.val+T.val

(loại bỏ E

1

,+,T )

gạt

E n

- 19

L->E n

L

19

L.val:=E.val
(loại bỏ E,n)

Chú ý là không phải mọi cú pháp điều khiển thuần tính L đều có thể kết hợp thực hiện các 
hành động ngữ nghĩa khi phân tích cú pháp mà không cần xây dựng cây cú pháp. Chỉ có một 
lớp hạn chế các cú pháp điều khiển có thể thực hiện như vậy, trong đó rõ nhất là cú pháp điều 
khiển thuần tuý S. 
Sau đây, chúng ta giới thiệu một số cú pháp điều khiển khác mà cũng có thể thực hiện khi 
phân tích LR bằng một số kỹ thuật:
 
1) loại bỏ việc gắn kết các hành động ngữ nghĩa ra khỏi lược đồ dịch
2) kế thừa các thuộc tính trên ngăn xếp
3) Mô phỏng thao tác đánh giá các thuộc tính kế thừa
4) Thay thuộc tính kế thừa bằng thuộc tính tổng hợp

Sinh viên tự tham khảo trong tài liệu các phần này.

3. LƯỢC ĐỒ CHUYỂN ĐỔI(Lược đồ dịch) - Translation Scheme
Lược đồ chuyển đổi là một văn phạm phi ngữ cảnh trong đó các thuộc tính 

được liên kết với các ký hiệu văn phạm và các hành động ngữ nghĩa nằm giữa hai 
dấu ngoặc móc {} được chèn vào một vị trí nào đó bên vế phải của sản xuất. 

+ Lược đồ dịch vẫn có cả thuộc tính tổng hợp và thuộc tính kế thừa

background image

+ Lược đồ dịch xác định thứ tự thực hiện hành động ngữ nghĩa trong mỗi sản 

xuất
Ví dụ: một lược đồ dịch để sinh biểu thức hậu vị cho một biểu thức như sau:

E -> T R
R -> + T {print(‘+’)} R
R -> 

ε

T -> num {print(num.val)}

Xét biểu thức “3+1+5”
Chúng ta duyệt theo thủ tục duyệt theo chiều sâu. Các hành động ngữ nghĩa được 
đánh thứ tự lần lượt 1,2,3, . . .

Kết quả dịch là “3 1 + 5 +”

Chú ý là nếu trong lược đồ dịch ta đặt hành động ngữ nghĩa ở vị trí khác đi, chúng ta sẽ  
có kết quả dịch khác ngay. Ví dụ, đối với lược đồ dịch, ta thay đổi một chút thành lược đồ 
dịch như sau:

E -> T R
R -> + T R {print(‘+’)}
R -> 

ε

T -> num {print(num.val)}

Xét biểu thức “3+1+5”
Chúng ta duyệt theo thủ tục duyệt theo chiều sâu. Các hành động ngữ nghĩa được 
đánh thứ tự lần lượt 1,2,3, . . .

E

T

R

3

+

T

R

+

T

R

1

5

1: print(‘3’)

2: print(‘1’)

3: print(‘+’)

4: print(‘5’)

ε

5: print(‘+’)

E

background image

Kết quả dịch là “3 1 5 + +”

Khi thiết kế lược đồ dịch, chúng ta cần một số điều kiện để đảm bảo rằng 

một giá trị thuộc tính phải có sẵn khi chúng ta tham chiếu đến nó:

1. Một thuộc tính kế thừa cho một ký hiệu ở vế phải của một sản xuất phải 

được tính ở một hành động nằm trước ký hiệu đó.

2. Một hành động không được tham chiếu đến thuộc tính của một ký hiệu ở 

bên phải của hành động đó.

3. Một thuộc tính tổng hợp cho một ký hiệu không kết thúc ở vế trái chỉ có 

thể được tính sau khi tất cả thuộc tính nó cần tham chiếu đến đã được tính 
xong. Hành động như thế thường được đặt ở cuối vế phải của luật sinh.

Ví dụ lược đồ dịch sau đây không thoả mãn các yêu cầu này:
S -> A

1

 A

2

 {A

1

.in:=1; A

2

.in:=2}

A -> a {print(A.in)}

Ta thấy thuộc tính kế thừa A.in trong luật thứ 2 chưa được định nghĩa vào lúc muốn in ra  
giá trị của nó khi duyệt theo hướng sâu trên cây phân tích cho đầu vào aa. Để thoả mãn  
thuộc tính L, chúng ta có thể sửa lại lược đồ trên thành như sau:

S ->  {A

1

.in:=1 } A

1

 {A

2

.in:=2} A

2

A -> a {print(A.in)}

Như vậy, thuộc tính A.in được tính trước khi chúng ta duyệt A.

Những điều kiện này được thoả nếu văn phạm có điều khiển thuần tính L. 

Khi đó chúng ta sẽ đặt các hành động theo nguyên tắc như sau:

1. Hành động tính thuộc tính kế thừa của một ký hiệu văn phạm A bên vế 

phải được đặt trước A.

T

R

3

+

T

R

+

T

R

1

5

1: print(‘3’)

2: print(‘1’)

5: print(‘+’)

3: print(‘5’)

ε

4: print(‘+’)

background image

2. Hành động tính thuộc tính tổng hợp của ký hiệu vế trái được đặt ở cuối 

luật sản xuất.

Ví dụ:
Cho văn phạm biểu diễn biểu thức gồm các toán tử + và - với toán hạng là các số:

E -> T R
R -> + T R
R -> - T R
R -> 

ε

T -> ( E )
T -> num

Xây dựng lược đồ dịch trên văn phạm này để tính giá trị của biểu thức.
Giải đáp: 

Trước hết, chúng ta thử xem cây phân tích cú pháp cho đầu vào “6+4-1”

Gọi  val  là thuộc tính chứa giá trị tính được của các ký hiệu văn phạm E và T. 
Thuộc tính s là thuộc tính tổng hợp và i là thuộc tính kế thừa để chứa giá trị tính 
được của ký hiệu R. Chúng ta đặt R.i chứa giá trị của phần biểu thức đứng trước R 
và R.s chứa kết quả. Chúng ta xây dựng lược đồ dịch như sau:
E ->  T {R.i:=T.val} 

{E.val:=R.s}

R ->  +

{R

1

.i:=R.i+T.val}

R

1

 {R.s:=R

1

.s }

R ->  -

{ R

1

.i:=R.i-T.val }

E

T

R

num(6

)

+

T

R

-

T

R

ε

num(4

)

num(1

)

background image

R

1

 {R.s:=R

1

.s}

R -> 

ε

 {R.s:=R.i}

T ->  ( E ) {T.val:=E.val}
T ->  num {T.val:=num.val}

Lưu ý:

nếu chúng ta xác định một cách duyệt khác cách duyệt theo hướng sâu thì cách đặt  
hành động dịch vào vị trí nào sẽ được làm khác đi. Tuy nhiên cách duyệt theo hướng  
sâu là cách duyệt phổ biến nhất và tự nhiên nhất (vì ngữ nghĩa sẽ được xác định dần  
theo chiều duyệt đầu vào từ trái sang phải) nên chúng ta coi khi duyệt một cây phân  
tích, chúng ta sẽ duyệt theo hướng sâu

.

4. DỰNG CÂY CÚ PHÁP.  

4.1. Cây cú pháp.

Cây cú pháp (syntax - tree) là dạng rút gọn của cây phân tích cú pháp dùng 

để biểu diễn cấu trúc ngôn ngữ. Trong cây cú pháp các toán tử và từ khóa không 
phải là nút lá mà là các nút trong.
Ví dụ với luật sinh S 

→ 

if B then S1 else S2 được biểu diễn bởi cây cú pháp: 

E val=9

T val=6

R i=6 s=9

num(6

)

+

val=4

R i=10 s=9

-

T val=1

R i=9 s=9

ε

num(4

)

num(1

)

background image

Xây dựng cây cú pháp cho biểu thức.

Tương tự như việc dịch một biểu thức thành dạng hậu tố. Xây dựng cây con cho 
biểu thức con bằng cách tạo ra một nút cho toán hạng và toán tử. Con của nút toán 
tử là gốc của cây con biểu diễn cho biểu thức con toán hạng của toán tử đó. Mỗi 
một nút có thể cài đặt bằng một mẩu tin có nhiều trường. 
Trong nút toán tử, có một trường chỉ toán tử như là nhãn của nút, các trường còn lại 
chứa con trỏ, trỏ tới các nút toán hạng. 
Để xây dựng cây cú pháp cho biểu thức chúng ta sử dụng các hàm sau đây:
1/ mknode(op, left, right) : Tạo một nút toán tử ó nhãn là op và hai trờng chứa con 
trỏ, trỏ tới left và right.
2/ mkleaf(id, entry): Tạo một nút lá với nhãn là id và một trờng chứa con trỏ entry, 
trỏ tới ô trong bảng ký hiệu danh biểu.
3/ mkleaf(num,val): Tạo một nút lá với nhãn là num và trờng val, giá trị của số.
Ví dụ: Để xây dựng cây cú pháp cho biểu thức: a - 4 + c ta dùng một dãy các lời 
gọi các hàm nói trên.
(1): p1 := mkleaf(id, entrya)           (4): p4 := mkleaf(id, entryc)
(2): p2 := mkleaf(num,4)                (5): p5 := mknode(" +", p3, p4)
(3): p3 := mknode(" -", p1, p2)
Cây được xây dựng từ dưới lên
entrya là con trỏ, trỏ tới ô của a trong bảng ký hiệu
entryc là con trỏ, trỏ tới ô của c trong bảng ký hiệu

background image

* xây dựng cây cú pháp từ định nghĩa trực tiếp cú pháp.
Căn cứ vào các luật sinh văn phạm và luật ngữ nghĩa kết hợp mà ta phân bổ việc 
gọi các hàm mknode và mkleaf để tạo ra cây cú pháp. 
Ví dụ:   Định nghĩa trực tiếp cú pháp giúp việc xây dựng cây cú pháp cho biểu thức 
là:
Luật sinh Luật ngữ nghĩa

 E1 + T E.nptr := mknode('+', E1.nptr, T.nptr)

 E1 - T E.nptr := mknode('-', E1.nptr, T.nptr)

T E.nptr := T.nptr

 (E) T.nptr := E.nptr

 id T. nptr := mkleaf(id, id.entry)

 num T.nptr := mkleaf(num, num.val)

Với biểu thức a - 4 + c ta có cây phân tích cú pháp (biểu diễn bởi đường chấm) 

background image

Luật ngữ nghĩa cho phép tạo ra cây cú pháp. Cây cú pháp có ý nghĩa về mặt cài đặt 
còn cây phân tích cú pháp chỉ có ý nghĩa về mặt logic. 

4.3. Đồ thị DRAG.

DAG ( Directed Acyclic Graph): Đồ thị bao gồm các đỉnh chứa các thuộc tính và 
cỏc cạnh cú hướng để biểu thị sự phụ thuộc giữa các đỉnh. Cũng giống như cây cú 
pháp, tuy nhiên trong cây cú pháp các biểu thức con giống nhau biểu diễn lặp lại 
còn trong DAG thì không. Trong DAG, một nút con có thể có nhiều "cha"

.

background image

Ví dụ: cho biểu thức a + a * (b - c) + (b - c) * d

Để xây dựng một DAG, trước khi tạo một nút phải kiểm tra xem nút đó đã tồn 

tại chưa, nếu đã tồn tại thì hàm tạo nút (mknode, mkleaf) trả về con trỏ của nút đã 
tồn tại, nếu chưa thì tạo nút mới.

Cài đặt DAG
Người ta thường sử dụng một mảng mẩu tin , mỗi mẩu tin là một nút. Ta có 

thể tham khảo tới nút bằng chỉ số của mảng.

Ví dụ: 
Lệnh gán DAG Biểu diễn i := i + 10

Nút 1: có nhãn là id, con trỏ trỏ tới entry i.
Nút 2: có nhãn là num, giá trị là 10.
Nút 3: có nhãn là +, con trái là nút 1, con phải là nút 2.
Nút 4: có nhãn là :=, con trái là nút 1, con phải là nút 3.

background image

Giải  thuật  5.1:  Phương  pháp  value_number  để   xây  dựng  một  nút  trong 

DAG.
Giả sử rằng các nút được lưu trong một mảng và mỗi nút đợc tham khảo bởi số giá 
trị của nó. Mỗi một nút toán tử là một bộ ba <op, l, r >
Input: Nhãn op, nút l và nút r.
Output: Một nút với <op, l, r>
Phương pháp: Tìm trong mảng một nút m có nhãn là op con trái là l, con phải là r.
Nếu tìm thấy thì trả về m, ngợc lại tạo ra một nút mới n, có nhãn là op, con trái là l, 
con phải là r và trả về m.

background image

CHƯƠNG 6                  

PHÂN TÍCH NGỮ NGHĨA

.

1. MỤC ĐÍCH NHIỆM VỤ.

Nhiệm vụ: kiểm tra tính đúng đắn về mặt ngữ nghĩa của chương trình nguồn. 

Việc kiểm tra được chia làm hai loại là kiểm tra tĩnh và kiểm tra động

 (Việc kiểm tra 

của chương trình dịch được gọi là tĩnh, việc kiểm tra thực hiện trong khi chương trình đích chạy 

gọi là động. Một kiểu hệ thống đúng đắn sẽ xoá bỏ sự cần thiết kiểm tra động.).

 

Xét một số dạng của kiểm tra tĩnh:

- Kiểm tra kiểu: kiểm tra về tính đúng đắn của các kiểu toán hạng trong biểu 

thức.

- Kiểm tra dòng điều khiển: một số điều khiển phải có cấu trúc hợp lý, 

ví dụ 

như lệnh break trong ngôn ngữ pascal  phải nằm trong một vòng lặp.

- Kiểm tra tính nhất quán: có những ngữ cảnh mà trong đó một đối tượng 

được định nghĩa chỉ đúng một lần. 

Ví dụ, trong Pascal, một tên phải được khai báo duy 

nhất, các nhãn trong lệnh case phải khác nhau, và các phần tử trong kiểu vô hướng không được 

lặp lại.

- Kiểm tra quan hệ tên: Đôi khi một tên phải xuất hiện từ hai lần trở lên. 

Ví 

dụ, trong Assembly, một chương trình con có một tên mà chúng phải xuất hiện ở đầu và cuối của  

chương trình con này.

Trong phạm vi tài liệu này, ta chỉ xét một số dạng trong kiểm tra kiểu của chương trình 

nguồn.

2. BIỂU THỨC KIỂU

 (type expressions)

Kiểu của một cấu trúc ngôn ngữ được biểu thị bởi “biểu thức kiểu”. Một biểu 

thức kiểu có thể là một kiểu cơ bản hoặc được xây dựng từ các kiểu cơ bản theo 
một số toán tử nào đó.

Ta xét một lớp các biểu thức kiểu như sau:
1). Kiểu cơ bản: 
Gồm boolean, char, interger, real. Có các kiểu cơ bản đặc biệt là type_error 

(để trả về một cấu trúc bị lỗi kiểu),  void (biểu thị các cấu trúc không cần xác định 
kiểu như câu lệnh).

2). Kiểu hợp thành:

background image

+ Mảng: Nếu T là một biểu thức kiểu thì array(I,T) là một biểu thức kiểu đối 

với một mảng các phần tử kiểu T và I là tập các chỉ số. 

Ví dụ, trong ngôn ngữ Pascal khai báo:  var A: array[1..10] of interger;

sẽ xác định kiểu của A là array(1..10,interger)

Tích của biểu thức kiểu: là một biểu thức kiểu. Nếu T

1

 và T

2

 là các kiểu biểu 

thức kiểu thì tích Đề các của T

1

xT

2

 là một biểu thức kiểu.

+ Bản ghi: Kiểu của một bản ghi chính là biểu thức kiểu được xây dựng từ các 

kiểu của các trường của nó. 

Ví dụ trong ngôn ngữ Pascal:

type row=record

address: interger;

lexeme: array[1..15] of char;

end;

var table: array[1..101] of row;

như vậy một biến của row thì tương ứng với một biểu thức kiểu là:

record((address x interger) x (lexeme x array(1..15,char)))

+ Con trỏ: Giả sử T là một biểu thức kiểu thì pointer(T) là một biểu thị một 

biểu thức kiểu xác định kiểu cho con trỏ của một đối tượng kiểu T

Ví dụ, trong ngôn ngữ Pascal:  var p: ^row    thì p có kiểu là pointer(row)

+ Hàm: Một hàm là một ánh xạ từ các phần tử của một tập vào một tập khác. 

Kiểu một hàm là ánh xạ từ một kiểu miền D vào một kiểu phạm vi R. Biểu thức 
kiểu cho một hàm như vậy sẽ được ký hiệu là D->R. 

Ví dụ trong ngôn ngữ Pascal, một hàm khai báo như sau:     function f(a,b:interger): 

^interger;
có kiểu miền là interger x interger và kiểu phạm vi là pointer(interger). Và như vậy biểu thức 
kiểu xác định kiểu cho hàm đó là:    0       interger x interger -> pointer(interger)

3. CÁC HỆ THỐNG KIỂU.

Một hệ thống kiểu là một tập các luật để xác định kiểu cho các phần trong 

chương trình nguồn. Một bộ kiểm tra kiểu làm nhiệm vụ thực thi các luật trong hệ 
thống kiểu này. Ở đây, hệ thống kiểu được xác định bởi các luật ngữ nghĩa dựa trên 
luật cú pháp. Các vấn đề được nghiên cứu trong phần cú pháp điều khiển và lược 
đồ dịch. 

background image

Một hệ thống kiểu đúng đắn sẽ xoá bỏ sự cần thiết phải kiểm tra động (vì nó cho phép xác  

định tĩnh, các lỗi không xảy ra trong lúc chương trình đích chạy). Một ngôn ngữ gọi là định kiểu 

mạnh nếu chương trình dịch của nó có thể bảo đảm rằng các chương trình mà nó dịch tốt sẽ hoạt  

động không có lỗi về kiểu. Điều quan trọng là khi bộ kiểm tra phát hiện lỗi, nó phải khắc phục  

lỗi dể tiếp tục kiểm tra. trước hết nó thông báo về lỗi mô tả và vị trí lỗi. Lỗi xấut hiện gây ảnh  

hưởng đếncác luật kiểm tra lỗi, do vậy phải thiết kế kiểu hệ thống như thế nào để các luật có thể  

đương đầu với các lỗi này.

3.1. Một số luật ngữ nghĩa kiểm tra kiểu

Đối với câu lệnh không có giá trị, ta có thể gán cho nó kiểu cơ sở đặc biệt void. Nếu có lỗi 

về kiểu được phát hiện trong câu lệnh thì ta gán cho nó giá trị kiểu là type_error

Xét cách xây dựng luật ngữ nghĩa kiểm tra kiểu qua một số ví dụ sau:

VD1: Văn phạm cho khai báo:

D -> D ; D
D -> id : T
T -> interger
T -> char
T -> ^ T
T -> array [num] of T

Luật cú pháp

Luật ngữ nghĩa

D -> id : T

AddType(id.entry,T.type)

T -> char

T.type := char

T -> interger

T.type := interger

T -> ^T

1

T.type := pointer(T

1

.type)

T -> array [num] of T

1

T.type := array(num.val,T

1

.type)

VD2: Văn phạm sau cho biểu thức

S -> id := E
E -> E + E
E -> E mod E

background image

E -> E

1

 [ E

2

 ]

E -> num
E -> id

Luật cú pháp

Luật ngữ nghĩa

S -> id := E

S.type := if id.type=E.type then void
else type_error

E -> E

1

 + E

2

E.type:= 

  if E

1

.type=interger and E

2

.type=interger then interger

  else  if E

1

.type=interger and E

2

.type=real then real

  else  if E

1

.type=real and E

2

.type=interger then real

  else  if E

1

.type=real and E

2

.type=real then real

   else type_error

E -> num

E.type := interger

E -> id

E.type := GetType(id. entry)

E -> E

1

 mod E

2

E.type := if E

1

.type=interger and E

2

.type=interger then interger 

else type_error

E -> E

1

 [ E

2

 ]

E.type := if E

2

.type=interger and E

1

.type=array(s,t) then t else 

type_error

VD3: Kiểm tra kiểu cho các câu lệnh:

S -> if E then S
S -> while E do S
S -> S

1

 ; S

2

 

Luật cú pháp

Luật ngữ nghĩa

S -> if E then S

1

S.type := if E.type=boolean then S

1

.type

                 else type_error

background image

S -> while E do S

1

S.type := if E.type=boolean then S

1.

type

                else type_error

S -> S

1

 ; S

2

S.type := if S

1

.type=void and S

2

.type=void then 

void 
               else type_error

VD4:  Kiểu hàm:   luật cú pháp sau đây thể hiện lời gọi hàm:       E -> E

1

 ( E

2

 )

Ví dụ:

function f(a,b:char):^interger;
begin

. . . 

end;
var 

p:^interger; q:^char;

      

x,y:interger;

begin

. . .
p:=f(x,y);// đúng
q:=f(x,y);// sai
end;

Luật cú pháp

Luật ngữ nghĩa

E -> E

1

 ( E

2

 )

E.type := if E

2

.type=s and E

1

.type=s->t then t   

                 else type_error

3.2. Ví dụ về một bộ kiểm tra kiểu đơn giản.

Ví dụ về một ngôn ngữ đơn giản mà kiểu của các biến phải được khai báo trước khi dùng. 

Bộ kiểm tra kiểu này là một cú pháp dạng lược đồ chuyển đổi nhằm tập hợp kiểu của từng biểu 

thức từ các kiểu của các biểu thức con. Bộ kiểm tra kiểu  có thể làm việc với các mảng, các con  

trỏ, lệnh, hàm.

*  

Một văn phạm dưới đây sinh ra các chương trình, biểu diẽn bởi biến P, bao 

gồm một chuỗi các khai báo D theo sau một biểu thức đơn E, các kiểu T.

 D;E

background image

 D;D|tên:T

 charintegerarraysốof T| ^T

 chữ cái Số | Tên|  E mod E | E; E |E^

- Một chương trình có thể sinh ra từ văn phạm trên như sau:
Key: Integer;
Key mod 1999
*  Lược đồ chuyển đổi như sau:

 D; E

 D;D 

 Tên:T              

{addtype (tên.entry, T.type)}

 Char               

{T.type:= char}

 integer            

{T.type:= integer}

 ^T

1                              

{T.type:= pointer(T

1

.type)}

 array | số | of T

1

  

{T.type:= aray(số.val,T

1

.type)}

Hành động ứng với sản xuất 

 Tên:T lưu vào bảng kí hiệu một kiểu cho một tên.  

Hàm    

{addtype (tên.entry, T.type)} nghĩa là cất một thuộc tính T.type vào bản kí hiệu ở vị trí 

entry.

Kiểm tra kiểu của các biểu thức.
 Trong các luật sau:

 chữ cái {E.type : = char}

 Số { E.type := integer}

Kiểu của thuộc tính tổng hợp của E cho biểu thưc được gán bằng kiểu hệ 

thống để sinh biểu thức bởi E. Các luật ngữ nghĩa cho thấy các hằng số biểu diễn 
bằng từ tố chữ cái và số có kiểu char và integer.

Ta dùng hàm lookup(e) để lấy kiểu caats trong bảng ký hiệu trỏ bởi e. Khi một 

tên xuất hiện trong biểu thức kiểu khao báo của nó được lấy và gán cho thuộc tính 
kiểu  

 tên {E.type:= lookup (tên.entry)}

- Một biểu thức được tạo bởi lệnh mod cho 2 biểu thức con có kiểu integer thì 

nó cũng có kiểu là integer nếu không là kiểu type_error.

E  

  E

1

  mod E

2

  {E.type : = if E

1

.type = integer and E

2

.type = integer then 

integer else type_error}

background image

- Đối với mảng E

1

[E

2

 ]bieeur thức chỉ số E

2

 phải có kiểu là integer các phần tử 

của mảng có kiểu t chính là kiểu array(s,t) của E

t

E  

 E

1

[E

2

] {E.type :=if E

2

.type = integer and E

t

.type = array(s,t) then t else 

type_error}

- Đối với thuật toán lấy giá trị con trỏ.

 E

t

^ {E.type := if E

1

.type = pointer (t) then else type_error}

 * Kiểm tra kiểu của câu lệnh:
Đối với câu lệnh không có giá trị: gán kiểu đặc biệt void . nếu có  lỗi được 

phát hiện trong câu lệnh : kiểu câu lệnh là : type_error.

Các câu lệnh gồm: lệnh gán, điều kiện, vòng lặp while. Chuooix các câu lệnh 

phân cách nhau bằng dấu chấm phẩy. một chương trình hoàn chỉnh có luật dạng P 

 D ; S cho biết một chương trình bao gồm các khai báo và theo sau là các câu 

lệnh .

 S 

 tên: = E { S.type:= if tên.type= E.type then void else type _error }

 if E else S

1

 {S.type := if E.type = boolean then S

1

.type else type_error }

 While E do S

1

 {S.type:= if E.type = boolean then S

1

.type = void then void 

else type_error }

* kiểm tra biểu thức của hàm.
Các hàm với tham số có sản xuất dạng:      E 

 E (E) 

Các luật ứng với kiểu biểu thức của kí hiệu không kết thúc T có thể làm thừa 

số theo các sản xuất sau:

 T

1

’ T

2

 {T.type := T

1

.type 

 T

2

.type}

Luật kiểm tra kiểu của một hàm là: E 

 E

1

(E

2

) {E.type : =if E

2

.type =s 

 t 

then t else type_error}

luật này cho biết trong một biểu thức được tạo bằng cách áp dụng E

1

 vào E

kiểu của s 

 t phải là một hàm từ kiểu của s vào kiểu nào đó t. kiểu E

1

(E

2

) là t.

3. MỘT SỐ VẤN ĐỀ KHÁC CỦA KIỂM TRA KIỂU.

3.1. Sự tương đương của kiểu biểu thức.

Nhiều luật có dạng “if kiểu của 2 biểu thức giống nhau thì trả về kiểu đó else  

trả về type _error” Vậy làn sao để xác định chính xác khi nào thì 2 kiểu biểu thức  
là tương đương? 

Hàm dùng để kiểm tra sự tương đương về cấu trúc của kiểu biểu thức.

background image

Function sequiv(s,t): boolean;
begin
if s và t cùng kiểu cơ sở then  return true;
else if s = array (s

1,

s

2

) and t = array (t

1

,t

2

) then  

                 return sequiv(s

1,

t

1

) and sequiv(s

2

,t

2

)

    else if s=pointer(s

1

) and t=pointer(t

1

) then    return sequiv(s

1,

t

1

)

           else if s=s

 s

2

 and t = t

 t

2

 then    return sequiv(s

1,

t

1

) and sequiv(s

2

,t

2

)

                  else return false;
end;

3.2. Đổi kiểu.

Xét biểu thức dạng : x+i, (x: kiểu real, i kiểu integer)

Biểu diễn real và integer trong máy tính là khác nhau đồng thời cách thực hiện phép cộng 

đối với số real và số integer khác nhau

. Để thực hiện phép cộng, trớc tiên chương trình 

dịch đổi cả 2 toán tử về một kiểu (kiểu real) sau đó thực hiện cộng.

Bộ kiểm tra kiểu trong chương trình dịch được dùng để chèn thêm phép toán 

vào các biểu diễn trung gian của chương trình nguồn. 

Ví dụ: chèn thêm phép toán inttoreal (dùng chuyển một số integer thành số 

real) rồi mới thực hiện phép cộng số thực real + như sau:          xi inttoreal real +

 * Ép kiểu:
Một phép đổi kiểu được gọi là không rõ (ẩn) nếu nó thực hiện một cách tự 

động bởi chương trình dịch, phép đổi kiểu này còn gọi là ép kiểu. 

(ép kiểu thường gây 

mất thông tin)  

Một phép đổi kiểu được gọi là rõ nếu người lập trình phải viết số thứ để thực 

hiện phép đổi này. Ví dụ:

Sản xuất

Luật ngữ nghĩa

 Số 

E.type:= integer

 Số.số

E.type:= real

 tên

E.type:= lookup (tên.entry)

background image

 E

1

 op E

2

E,type:= if E

1

.type = integer and E

2

.type = integer   Then integer

               Else if E

1

.type = integer and E

2

.type = real  Then real

                   Else if E

1

.type = real and E

2

.type = integer Then real

                           Else if E

1

.type = real and E

2

.type = real  Then real

                                   Else type_error

3.3. Định nghĩa chồng của hàm và các phép toán.

Kí hiệu chồng là kí hiệu có nhiều nghĩa khác nhau phụ thộc vào ngữ cảnh của 

nó.

VD: + là toán tử chồng, A+B   ý nghĩa khác nhau đối với từng trường hợp A,B là số  

nguyên, số thực, số phức, ma trận…

Định nghĩa chồng cho phép tạo ra nhiều hàm khác nhau nhưng có cùng một 

tên. Để xác định thực sự dùng định nghĩa chồng nào ta phải căn cứ vào ngữ cảnh 
lúc áp dụng.

Điều kiện để thực hiện toán tử chồng là phải có sự khác nhau về kiểu hoặc số 

tham số. Do đó ta có thể dựa vào luật ngữ nghĩa để kiểm tra kiểu và gọi các hàm xử 
lý.

background image

CHƯƠNG 7                BẢNG KÍ HIỆU.

1. MỤC ĐÍCH, NHIỆM VỤ.

Một chương trình dịch cần phải thu thập và sử dụng các thông tin về các tên 

trong chương trình nguồn. Các thông tin này được lưu trong một cấu trúc dữ liệu 
gọi là một bảng kí hiệu. Các thông tin bao gồm tên, kiểu, dạng của nó ( một biến 
hay là một cấu trúc), vị trí cảu nó trong bộ nhớ, các thuộc tính khác phụ thuộc vào 
ngôn gnữ lập trình.

Mỗi lần tên cần xem xét, chương trình dịch sẽ tìm trong bảng kí hiệu xem đã 

có tên đó chưa. Nếu tên đó là mớithì thêm vào bảng. Các thông tin về tên được tìm 
và đưa vào bảng trong giai đoạn phân tích từ vựng và cú pháp.

Các thông tin trong bảng kí hiệu được dùng trong phân tích ngữ nghĩa, 

( kiểm traviệc dùng các tên có khớp với khai báo không) trong giai đoạn sinh mã 
( kích thước của tên, loại bộ nhớ phải cấp phát cho một tên).

Dùng bảng kí hiệu trong quá trình phát hiện và khôi phục lỗi.   

 

2. CÁC YÊU CẦU ĐỐI VỚI BẢNG KÍ HIỆU.

Ta cần có một số khả năng làm viếc  với bảng như sau:

1) phát hiện một tên cho trước có trong bảng hay không?
2) thêm tên mới.
3) lấy thông tin tương ứng với tên cho trước.
4) thêm thông tin mới vào tên cho trước.
5) xoá một tên hoặc nhóm tên.

Các thông tin trong bảng kí hiệu có thể gồm:

1) Xâu kí tự tạo nên tên.
2) Thuộc tính của tên.
3) các tham số như số chiều của mảng.
4) Có thể có con trỏ đên tên cấp phát.

Các thông tin đưa vào bảgn trong những thời điểm khác nhau.

3. CẤU TRÚC DỮ LIỆU CỦA BẢNG KÍ KIỆU

Có nhiều cách tổ chức bảng kí hiệu khác nhau như có thể tách bảng riêng rẽ ứng với tên biến, 
nhãn, hằng số, tên hàm và các kiểu tên khác… tuỳ thuộc vào từng ngôn ngữ. 
Về cách tổ chức dữ liệu có thể tỏ chức bởi danh sách tuyến tính, cây tìm kiếm, bảng băm…

Mỗi ô trong bảng ký hiệu tương ứng với một tên. Ðịnh dạng của các ô này 

thường không giống nhau vì thông tin lưu trữ về một tên phụ thuộc vào việc sử 
dụng tên đó. Thông thường một ô được cài đặt bởi một mẩu tin có dạng ( tên, thuộc 
tính). 

background image

Nếu muốn có được sự đồng nhất của các mẩu tin ta có thể lưu thông tin bên 

ngoài bảng ký hiệu, trong mỗi ô của bảng chỉ chứa các con trỏ trỏ tới thông tin đó,

 Trong bảng ký hiệu cũng có thể có  lưu các từ khóa của ngôn ngữ. Nếu vậy thì 

chúng phải được đưa vào bảng ký hiệu trước khi bộ phân tích từ vựng bắt đầu.

Nếu ghi trực tiếp tên trong trường tên của bảng thì: ưu điểm: đơn giản, nhanh. 

Nhược điểm: Độ dài tên bị giới hạn bởi kích thước của trường , hiệu quả sử dụng 
bộ nhớ không cao. 

Trường hợp danh biểu bị giới hạn về độ dài thì chuỗi các ký tự tạo nên 

danh biểu được lưu trữ trong bảng ký hiệu.

 

Name

Attribute

s o r

t

 

 

 

 

 

 

a

 

 

 

 

 

 

 

 

 

r e a d a r

r a y

 

i

 

 

 

 

 

 

 

 

 

  

 

Hình 7.19 - Bảng ký hiệu  lưu giữ các tên bị giới hạn độ dài

 

Trường hợp độ dài tên không bị giới hạn thì các Lexeme được lưu trong một 

mảng riêng và bảng ký hiệu chỉ giữ  các con trỏ trỏ tới đầu mỗi Lexeme

 

 

background image

Hình 7.20 - Bảng ký hiệu  lưu giữ các tên không bị giới hạn độ dài

3.1 Danh sách.

Cấu trúc đơn giản, dễ cài đặt nhất cho bảng ký hiệu là danh sách tuyến tính của các  

mẩu tin. 

Ta dùng một mảng hoặc nhiều mảng tương đương để lưu trữ tên và các thông tin 

kết hợp với chúng. Các tên mới được đưa vào trong danh sách theo thứ tự mà 
chúng được phát hiện. Vị trí của mảng được đánh dấu bởi con trỏ available chỉ ra 
một ô mới của bảng sẽ được tạo ra.

 

Việc tìm kiếm một tên trong bảng ký hiệu được bắt đầu từ available đến đầu 

bảng. Trong các ngôn ngữ cấu trúc khối sử dụng quy tắc tầm tĩnh. Thông tin kết 
hợp với tên có thể bao gồm cả thông tin về độ sâu của tên. Bằng cách tìm kiếm từ 
available trở về đầu mảng chúng ta đảm bảo rằng sẽ tìm thấy tên trong tầng gần 
nhất.
 

  

Hình 7.21 - Danh sách tuyến tính các mẩu tin

 

3.2. Cây tìm kiếm.

Một trong các dạng cây tìm kiếm hiệu quả là: cây tìm kiếm nhị phân tìm 

kiếm. Các nút của cây có khoá là tên của bản ghi, hai con tro Left, right. 
Đối với mọi nút trên cây phải thoả mãn:

- Mọi khoá thuộc cây con trái nhỏ hơn khoá của gốc.
- Mọi nút của cây con phải lớn hơn khoá của gốc. 

Giải thuật tìm kiếm trên cây nhị phân:
- So sánh giá trị tìm kiếm x với khoá của gốc: 

background image

+ Nếu trùng: tìm kiếm thoả mãn.
+ Nếu < hơn: Thực hiện lại cách tìm kiểm với cây con bên trái.
+ Nếu > gốc: thực hiện lại cách tìm kiếm với cây con bên phải.

Để đảm bảo thời gian tìm kiếm người ta thay thé cây nhị phân tìm kiếm bằng cây nhị 
phân cân bằng. 

3.3. Bảng Băm.

Kỹ thuật sử dụng bảng băm để cài đặt bảng ký hiệu thường được sử dụng vì tính hiệu quả của  

nó.

 

Cấu tạo bao gồm hai phần; bảng băm và các danh sách liên kết.

 

 

Hình 7.22 - Bảng băm có kích thước 211

 

1. Bảng băm là một mảng bao gồm m con trỏ. 
2. Bảng danh biểu được chia thành m danh sách liên kết, mỗi danh sách liên kết 

được trỏ bởi một phần tử trong bảng băm.

Việc phân bổ các danh biểu vào   danh sách liên kết nào do hàm băm (hash 

function) quy định. Giả sử s là chuỗi ký tự xác định danh biểu, hàm băm h tác động 
lên s trả về một giá trị nằm giữa 0 và m- 1 h(s) = t => Danh biểu s được đưa vào 
trong danh sách liên kết được trỏ bởi phần tử t của bảng băm.

Có nhiều phương pháp để xác định hàm băm.

 

Phương pháp đơn giản nhất như sau:

background image

 1. Giả sử s bao gồm các ký tự c1, c2, c3, ..., ck. Mỗi ký tự cho ứng với một số 

nguyên dương n1, n2, n3,...,nk; lấy h = n1 + n2 +...+ nk.

2. Xác định h(s) = h mod m

background image

CHƯƠNG 8              

SINH MÃ TRUNG GIAN

.

1. MỤC ĐÍCH NHIỆM VỤ.

* Sinh mã trung gian có những ưu điểm như sau:
- Dễ thiết kế từng phần
- Sinh được mã độc lập với từng máy tính cụ thể. Từ đó làm giảm độ phức tạp 

của sinh mã thực sự.

- Dễ tối ưu mã.
* Các vấn đề của bộ sinh mã trung gian là:

- Dùng mã trung gian nào.
- Thuật toán sinh mã trung gian.

Hành động sinh mã trung gian thực hiện qua cú pháp điều khiển.

 

Ngôn ngữ trung gian là ngôn ngữ nằm giữa ngôn ngữ nguồn và ngôn ngữ đích. Chương 

trình viết bằng ngôn ngữ trung gian vẫn tương đương với chương trình viét bàng ngôn ngữ 

nguồn về chức năng nhiệm vụ. Sau đây ta xét loại mã trung gian thông dụng nhất.

2. CÁC NGÔN NGỮ TRUNG GIAN

Cây cú pháp, ký pháp hậu tố và mã 3 địa chỉ là các loại biểu diễn trung gian.

 2.1. Đồ thị.

Cây cú pháp mô tả cấu trúc phân cấp tự nhiên của chương trình nguồn. DAG cho ta 
cùng lượng thông tin nhưng bằng cách biểu diễn ngắn gọn hơn trong đó các biểu 
thức con không được biểu diễn lặp lại.

Ví dụ 8.1: Với lệnh gán a := b * - c + b * - c, ta có cây cú pháp và DAG:

 

Hình 8.1 - Biểu diễn đồ thị của a :=b * - c + b * - c

background image

 

Ký pháp hậu tố là một biểu diễn tuyến tính của cây cú pháp. Nó là một danh 

sách các nút của cây, trong đó một nút xuất hiện ngay sau con của nó .

a b c - * b  c - * +  := là biểu diễn hậu tố của cây cú  pháp hình trên.

     

Cây  cú pháp có thể được cài đặt bằng một trong 2 phương pháp:

 

- Mỗi nút được biểu diễn bởi một mẫu tin, với một trường cho toán tử và các 

trường khác trỏ đến con của nó.

- Một mảng các mẩu tin, trong đó chỉ số của phần tử mảng đóng vai trò như là 

con trỏ của một nút.

Tất cả các nút trên cây cú pháp có thể tuân theo con trỏ, bắt đầu từ nút gốc tại 10

 

Hình 8.2  - Hai biểu diễn của cây cú pháp trong hình 8.1

2.2. Kí pháp hậu tố.
Định nghĩa kí pháp hậu tố của một biểu thức:
1) E là một biến hoặc hằng số, kí pháp hậu tố của E là E.
2) E là biểu thức dạng: E

1

 op E

2

với op là toán tử 2 ngôi thì kí pháp hậu tố của E là: 

E’

1

E’

2

op với E’

1

, E’

2

 là kí pháp hậu tố của E

1

, E

2

 tương ứng. 

background image

3) Nếu E là biểu thức dạng (E

1

), thì kí pháp hậu tố của E

1

 cũng là kí pháp hậu tố 

của E. 

Ví dụ: 

Ví dụ: Kí pháp hậu tố của (9-5)+2 là 95-2+;
Kí pháp hậu tố của 9-(5+2) là 952+-;
Kí pháp hậu tố của câu lệnh 
if a then if c-d then a+c else a*c else a+b
là a?(c-d?a+c:a*c):a+b tức là: acd-ac+ac*?ac+?
* Định nghĩa cú pháp điều khiển tạo mã hậu tố.

Mà3 ĐỊA CHỈ.

Mã ba địa là một chuỗi các câu lệnh, thông thường có dạng:           x:= y op z

X,y,z là tên, hằng do người lập trình tự đặt, op là một phép toán nào đó phép toán 

toán học, logic

Dưới đây là một số câu lệnh ba địa chỉ thông dụng:

1.

Các câu lệnh gán có dạng x := y op z, trong đó op là một phép toán số 

học hai ngôi hoặc phép toán logic.

2.

Các phép gán có dạng x := op y, trong đó op là phép toán một ngôi. 

Các phép toán một ngôi chủ yếu là phép trừ, phép phủ định logic, phép chuyển 
đổi kiểu, phép dịch bít.

3.

Các câu lệnh sao chép dạng x := y, gán y vào x.

4.

Lệnh nhảy không điều kiện goto L. Câu lệnh ba địa chỉ có nhãn L là 

câu lệnh được thực hiện tiếp theo.

5.

Các lệnh nhảy có điều kiện như if x relop y goto L. Câu lệnh này thực 

hiện một phép toán quan hệ cho x và y, thực hiện câu lệnh có nhãn L nếu quan 
hệ này là đúng, nếu trái lại sẽ thực hiện câu lệnh tiếp theo.

6.

Câu lệnh param x và call p,n dùng để gọi thủ tục. Còn lệnh return y để 

trả về một giá trị lưu trong y. Ví dụ để gọi thủ tục p(x

1

,x

2

,...,x

n

) thì sẽ sinh các 

câu lệnh ba địa chỉ tương ứng như sau:

param x

1

param x

2

 

. . .

background image

param x

n

call p, n
7. Các phép gán chỉ số có dạng   x := y[i] có ý nghĩa là gán cho x giá trị 

tại vị trí i sau y

tương tự đối với x[i] := y
8. Phép gán địa chỉ và con trỏ có dạng x := &y, x := *y, *x := y

2.1. Cài đặt các câu lệnh ba địa chỉ

Trong chương trình dịch, những câu lệnh mã 3 địa chỉ có thể được cài đặt như các 

cấu trúc với các trường chứa các toán tử và toán hạng. Những biểu diễn đó là bộ tứ 
(quadruple) và bộ ba (triple

).

2.1.1. Bộ tứ

Bộ tứ là một cấu trúc bản ghi với bốn trường, được gọi là op, arg1, arg2 và 

result

Ví dụ:  câu lệnh x := y + z 

 op là +, arg1 là y, arg2 là z và result chứa x. Đối với toán tử một ngôi thì không dùng 

arg2.

Ví dụ: Câu lệnh          a := -b * (c+d)
sẽ được chuyển thành đoạn mã ba địa chỉ như sau:

t1 := - b
t2 := c+d
t3 := t1 * t2
a := t3

và được biểu diễn bằng bộ tứ như sau:

Op

arg1

arg2

result

0

Uminus

b

t1

1

+

t2

2

*

t1

t2

t3

3

Assign

t3

a

2.1.2. Bộ ba

Để tránh phải đưa các tên tạm thời vào bảng ký hiệu, chúng ta có thể tham 

chiếu đến một giá trị tạm bằng vị trí của câu lệnh dùng để tính nó (tham chiếu đến 

background image

câu lệnh đó chính là tham chiếu đến con trỏ chứa bộ ba của câu lệnh đó). Nếu 
chúng ta làm như vậy, câu lệnh mã ba địa chỉ sẽ được biểu diễn bằng một cấu trúc 
chỉ gồm có ba trường op, arg1 và arg2.

Ví dụ trên sẽ được chuyển thành bộ ba như sau:

op

arg1

arg2

0

uminus

b

1

d

2

*

(0)

(1)

3

assign

a

(2)

Chú ý, câu lệnh sao chép đặt kết quả trong arg1 và tham số trong arg2 và toán tử là 

assign.

Các số trong ngoặc tròn biểu diễn các con trỏ chỉ đến một cấu trúc bộ ba, còn 

con trỏ chỉ đến bảng ký hiệu được biểu diễn bằng chính các tên. Trong thực hành, 
thông tin cần để diễn giải các loại mục ghi khác nhau trong arg1 và arg2 có thể 
được mã hoá vào trường op hoặc đưa thêm một số trường khác.

Chú ý, phép toán ba ngôi như x[i] := y cần đến hai mục trong cấu trúc bộ ba 

như được chỉ ra như sau:

op

arg1

arg2

(0)
(1)

[]=

assign

x

(0)

i

y

tương tự đối với phép toán ba ngôi x := y[i]

op

arg1

arg2

(0)
(1)

[]=

assign

y
x

i

(0)

2.2. Cú pháp điều khiển sinh mã ba địa chỉ

Đối với mỗi ký hiệu X, ký hiệu:

background image

-

X.place là nơi để chứa mã ba địa chỉ sinh ra bởi X (dùng để chứa các kết quả 

trung gian). 

Vì thế sẽ có một hàm định nghĩa là newtemp dùng để sinh ra một biến trung gian 

(biến tạm) để gán cho X.place.

-

X.code chứa đoạn mã ba địa chỉ của X

-

Thủ tục gen để sinh ra câu lệnh ba địa chỉ.

Sau đây, chúng ta xét ví dụ sinh mã ba địa chỉ cho một số dạng câu lệnh.

2.2.1. Sinh mã ba địa chỉ cho biểu thức số học:

Sản xuất

Luật ngữ nghĩa

S -> id := E

S.code := E.code || gen(id.place ‘:=’ E.place)

E -> E

1

 + E

2

E.place := newtemp;
E.code := E

1

.code || E

2

.code || gen(E.place ‘:=’ E

1

.place 

‘+’ E

2

.place)

E -> E

1

 * E

2

E.place := newtemp;
E.code := E

1

.code || E

2

.code || gen(E.place ‘:=’ E

1

.place 

‘+’ E

2

.place)

E -> - E

1

E.place := newtemp;
E.code := E

1

.code || gen(E.place ‘:=’ ‘uminus’ E

1

.place)

E -> ( E

1

 )

E.place := E

1

.place

E.code := E

1

.code

E -> id

E.place := id.place
E.code := ‘’

Ví dụ: 
Hãy sinh mã ba địa chỉ cho câu lệnh sau “x := a + ( b * c )”

=> x := E 
=> x := E

1

 + E

2

 

=> x := a + E

2

 

=> x := a + ( E

3

 )

background image

=> x := a + ( E

4

 * E

)

=> x := a+ ( b * E

5

 )

=> x := a + ( b * c )
E

5

.place := c E

5

.code := ‘’

E

4

.place := b  E

4

.code := ‘’

E

3

.place := t

1

E

3

.code := t

1

 := b * c

E

2

.place := t

1

E

2

.code := t

1

 := b * c

E

1

.place := a E

1

.code := ‘’

E

1

.place := a E

1

.code := ‘’

E.place := t

2

E.code := t

1

 := b * c ||  t

2

 := a + t

1

 

S.code := t

1

 := b * c ||  t

2

 := a + t

1

 || x := t

2

2.2.2. Sinh mã ba địa chỉ cho biểu thức Boole:

Đối với một biểu thức Boole E, ta dịch E thành một dãy các câu lệnh ba địa 

chỉ, trong đó đối với các phép toán logic sẽ sinh ra các lệnh nhảy có điều kiện và 
không có điều kiện đến một trong hai vị trí: E.true, nơi quyền điều khiển sẽ chuyển 
tới nếu E đúng, và E.false, nơi quyền điều khiển sẽ chuyển tới nếu E sai.

Ví dụ: E có dạng a<b. Thế thì mã sinh ra sẽ có dạng 

if a<b goto E.true
goto E.false

Ví dụ    đoạn lệnh sau:

if a>b then 

a:=a-b;

else

b:=b-a;

được chuyển thành mã ba địa chỉ như sau 

E.true = L1 và E.false = L2

if a>b goto L1
goto L2

L1: 

t1 := a –b

a := t1
goto Lnext

L2: 

t2 := b-a

b := t2 

Lnext:

Một số cú pháp điều khiển sinh mã ba địa chỉ cho các biểu thức Boole.

Để sinh ra các nhãn, chúng ta sử dụng thủ tục newlable để sinh ra một nhãn mới.

background image

Sản xuất

Luật ngữ nghĩa

E -> E

1

 or E

2

 

E

1

.true := E.true;

E

1

.false := newlable;

E

2

.true := E.true;

E

2

.false := E.false;

E.code := E

1

.code || gen(E

1

.false ‘:’) || E

2

.code

E -> E

1

 and E

2

 

E

1

.true := newlable;

E

1

.false := E.false;

E

2

.true := E.true;

E

2

.false := E.false;

E.code := E

1

.code || gen(E

1

.true ‘:’) || E

2

.code

E -> not E

1

E

1

.true := E.false;

E

1

.false := E.true;

E.code := E

1

.code;

E -> ( E

1

 )

E

1

.true := E.true;

E

1

.false := E.false;

E.code := E

1

.code;

E -> id

1

 relop id

2

E.code := gen(‘if’ id

1

.place relop.op id

2

.place ‘goto’ E.true) || 

gen(‘goto’ E.false)

E -> true

E.code := gen(‘goto’ E.true)

E -> false

E.code := gen(‘goto’ E.false)

Ví dụ: Sinh mã ba địa chỉ cho đoạn chương trình sau:

if a>b and c>d then

x:=y+z

else

x:=y-z

Lời giải:
Nếu coi E là biểu thức logic a>b and c>d thì đoạn chương trình trên trở thành

background image

if E then x:=y+z , khi đó mã ba địa chỉ cho đoạn chương trình có dạng:

E.code {
if E=true  goto E.true
goto E.false }
E.true: t1:= y+z

x := t1;

E.false : 

t2 := y-z
x :=t2

Như vậy chúng ta phải phân tích bên trong của biểu thức E, và dễ thấy các lệnh nhảy bên 

trong E chính là E.true và E.false, điều đó giải thích tại sao chúng ta lại có các luật ngữ nghĩa 

như bảng trên.

Áp dụng các luật sinh mã ba địa chỉ trong bảng trên chúng ta có đoạn mã ba 

địa chỉ cho đoạn chương trình nguồn ở trên là:

if a>b  goto L1
goto L3
L1:

if c>d goto L2

goto L3

L2:

t1 := y+z

x := t1
goto L4

L3: 

t2 := y-z

x := t2

L4:

2.2.3. Sinh mã ba địa chỉ cho một số lệnh điều khiển:

Trong các câu lệnh điều khiển có điều kiện, ta dựa vào biểu thức logic E để 

chuyển việc thực hiện các câu lệnh tới vị trí thích hợp. Do đó ta cần hai nhãn: 
E.true (

để xác định vị trí câu lệnh chuyển tới khi biểu thức logic E là đúng

), nhãn 

E.false (

để xác định vị trí câu lệnh chuyển tới khi biểu thức logic E là sai

). 

Để sinh ra một nhãn mới, ta dùng thủ tục newlable.
Nhãn S.next đối với khối lệnh sinh ra bởi ký hiệu S là nhãn xác định vị trí tiếp 

theo của các lệnh sau S.

background image

Đối với câu lệnh   S -> while E do S

1

ta cần có một nhãn bắt đầu của khối lệnh này để nhảy đến mỗi khi E đúng, vì 

vậy cần nhãn S.begin để xác định vị trí bắt đầu khối lệnh này.  

Sản xuất

Luật ngữ nghĩa

S -> if E then S

1

E.true := newlable;
E.false := S.next;
S

1

.next := S.next;

S.code := E.code || gen(E.true ‘:’) || S

1

.code

S -> if E then S

1

 else 

S

2

E.true := newlable;
E.false := newlable;
S

1

.next := S.next;

S

2

.next := S.next;

S.code := E.code || gen(E.true ‘:’) || S

1

.code || 

gen(‘goto’ S.next) || gen(E.false ‘:’) || S

2

.code

S -> while E do S

1

S.begin := newlable;
E.true := newlable;
E.false := S.next
S

1

.next := S.begin;

S.code := gen(S.begin ‘:’) || E.code || gen(E.true ‘:’) || 
S

1

.code || gen(‘goto’ S.begin) 

Ví dụ 1: sinh đoạn mã ba địa chỉ cho đoạn mã nguồn sau:

          while a<>b do

if a>b then 
a:=a-b
else

b:=b-a

Lời giải

 

 :  

L1:

if a<>b goto L2
goto Lnext

L2:

if a>b goto L3
goto L4

L3:

t1 := a-b

background image

a := t1
goto L1

L4:

t2 := b-a
b := t2
goto L1

Lnext:

2.2.3.Các khai báo. 

Đối với các khai báo định danh, ta không sinh ra mã lệnh tương ứng trong mã 

ba địa chỉ mà dùng bảng ký hiệu để lưu trữ. 

Như vậy có thể hiểu là kết quả của sinh mã 

ba địa chỉ từ chương trình nguồn là tập lệnh ba địa chỉ và bảng ký hiệu quản lý các định danh.

Với mỗi định danh, ta lưu các thông tin về kiểu và địa chỉ tương đối để lưu giá 

trị cho định danh đó. 

Ví dụ:
Giả sử ký hiệu offset để chứa địa chỉ tương đối của các định danh; mỗi số 

interger chiếm 4 byte, số real chứa 8 byte và mỗi con trỏ chứa 4 byte; giả sử hàm 
enter dùng để nhập thông tin về kiểu và địa chỉ tương đối cho một định danh, chúng 
ta có ví dụ dưới đây mô ta việc sinh thông tin vào bảng ký hiệu cho các khai báo.

Sản xuất

Luật ngữ nghĩa

P -> D

offset := 0

D -> D ; D

D -> id : T

enter(id.name,T.type, offset) ;

offset := offset + T. width

T -> interger

T.type := interger;

T. width := 4

T -> real

T.type := real; T. width := 8

T -> array [ num ] of T

1

T.type := array(num.val,T

1

.type);

T.width := num.val * T

1

. width

T -> ^T

1

T.type := pointer(T

1

.type)

T. width := 4

background image

Trong các đoạn mã ba địa chỉ, khi đề cập đến một tên, ta sẽ tham chiếu đến bảng ký 
hiệu để lấy thông tin về kiểu, địa chỉ tương ứng để sử dụng trong các câu lệnh. 

Hay 

nói cách khác chúng ta có thể thay một định danh bởi chỉ mục của định danh đó trong bảng ký 

hiệu.

Chú ý: Địa chỉ tương đối của một phần tử trong mảng, ví dụ x[i], được tính bằng 
địa chỉ của x cộng với i lần độ dài của mỗi phần tử. 

Bài tập

Bài tập 1: Hãy chuyển các câu lệnh hoặc đoạn chương trình sau thành đoạn mã ba 
địa chỉ:

1)

a * - (b+c)

2)

đoạn chương trình C

main ()
{ int i; int a[100];

i=1;
while(i<=10)

{ a[i]=0;

i=i+1;

}

}

.1. Dịch biểu thức : a * - ( b + c)  thành các dạng :
 
    a) Cây cú pháp.
    b) Ký pháp hậu tố.
    c) Mã lệnh máy 3 - địa chỉ.

 

8.2. Trình bày cấu trúc lưu trữ biểu thức  - ( a + b) * ( c + d ) + ( a + b + c)  ở  các dạng :
 
    a) Bộ tứ .
    b) Bộ tam.
    c) Bộ tam gián tiếp.

 

8.3. Sinh mã trung gian ( dạng mã máy 3 - địa chỉ) cho các biểu thức C đơn giản sau :
 
    a) x = 1
    b) x = y
    c) x = x + 1
    d) x = a + b * c
    e) x = a / ( b + c) - d * ( e + f )

 

8.4. Sinh mã trung gian ( dạng mã máy 3 - địa chỉ) cho các biểu thức C sau :

background image

 
    a) x = a [i] + 11
    b) a [i] = b [ c[j] ]
    c) a [i][j] = b [i][k] * c [k][j]
    d) a[i] = a[i] + b[j]
    e) a[i] + = b[j]

 

8.5. Dịch lệnh gán sau thành mã máy 3 - địa chỉ :

 
                A [ i,j ] := B [ i,j ] + C [A[ k,l]] + D [ i + j ]

  

background image

CHƯƠNG 9

              SINH MÃ

1. MỤC ĐÍCH NHIỆM VỤ

Giai đoạn cuối của quá trình biên dịch là sinh mã đích.

 

Kỹ thuật sinh mã đích được 

trình bày trong chương này không phụ thuộc vào việc dùng hay không dùng giai đoạn tối ưu mã 
trung gian 

.

Sinh mã tốt rất khó, mã sinh ra thường gắn với một loại máy tính cụ thể nào 

đó.

Đầu vào của bộ sinh mã là mã trung gian, đầu ra là một chương trình viết 

dạng mã đối tượng nào đó và gọi là chương trình đích.

Ðầu vào của bộ sinh mã gồm biểu diễn  trung gian của chương trình nguồn, cùng 

thông tin trong bảng danh biểu được dùng để xác định địa chỉ  của các đối tượng dữ liệu 
trong thời gian thực thi. Các đối tượng dữ liệu này được tượng trưng bằng tên trong biểu 
diễn trung gian. Biểu diễn trung gian của chương trình nguồn có thể ở một trong các 
dạng: ký pháp hậu tố, mã ba địa chỉ, cây cú pháp, DAG

Tiêu chuẩn quan trọng nhất đối với bộ sinh mã là sinh mã đúng.

Tính đúng của mã 

có một ý nghĩa rất quan trọng. Với những quy định về tính đúng của mã, việc thiết kế bộ 
sinh mã sao cho nó được thực hiện, kiểm tra, bảo trì đơn giản là mục tiêu thiết kế quan 
trọng .

2. CÁC DẠNG MàĐỐI TƯỢNG.

2.1. Mã máy định vị tuyệt đối.

Một chương trình mã máy tuyệt đối có các lệnh mã máy được định vị tuyệt 

đối. Chương trình dịch xác định hoàn toàn chương trình đối tượng này.

Mã được một chương trình dịch thực sự tạo ra và đặt vào các vị trí này nên 

chương trình có thể hoạt động ngay.

background image

Ưu điểm: giảm số 

2.2. Mã đối tượng có thể định vị lại được.

2.3. Mã đối tượng thông dịch.

Việc tạo ra chương đích ở dạng hợp ngữ  cho phép ta dùng bộ biên dịch hợp ngữ để 

tạo ra mã máy. 

3. CÁC VẤN ĐỀ THIẾT KẾ CỦA BỘ SINH MÃ.

Sự lựa chọn chỉ thị

 
Tập các chỉ thị của máy đích sẽ xác định tính phức tạp của việc lựa chọn chỉ thị. Tính 

chuẩn và hoàn chỉnh của tập chỉ thị là những yếu tố quan trọng. Nếu máy đích không 
cung cấp một mẫu chung cho mỗi  kiểu dữ liệu thì mỗi trường hợp ngoại lệ phải xử lý 
riêng. Tốc độ chỉ thị và sự biểu diễn  của máy cũng là những yếu tố quan trọng. Nếu ta 
không quan tâm đến tính hiệu quả của chương trình đích thì việc lựa chọn chỉ thị sẽ đơn 
giản hơn. Với mỗi lệnh ba địa chỉ ta có thể phác họa một bộ khung cho mã đích. Giả sử  
lệnh ba địa chỉ  dạng x := y + z, với x, y, z được cấp phát tĩnh, có thể được dịch sang 
chuỗi mã đích:

 
MOV   y, R0          /* Lưu y vào thanh ghi Ro */
ADD   z,  R0          /* cộng z vào nội dung Ro, kết quả chứa trong Ro */
MOV   R0, x          /* lưu nội dung Ro vào x */
 
Tuy nhiên việc sinh mã cho chuỗi các lệnh ba địa chỉ sẽ dẫn đến sự dư thừa mã. Chẳng 

hạn với:

a:= b + c
d:= a + e

ta chuyển sang mã đích:

MOV  b, R

o

ADD   c, R

o

MOV  R

o

, a

MOV  a, R

0

ADD   e,R

o

MOV  R

o

, d

và ta nhận thấy rằng chỉ thị thứ tư là thừa. 

Chất lượng mã được tạo ra, được xác định bằng tốc độ và kích thước của mã. Một máy 

đích có tập chỉ thị phong phú có thể sẽ cung cấp nhiều cách để hiện thực một tác vụ cho 
trước. Ðiều này có thể dẫn đến tốc độ thực hiện chỉ thị rất khác nhau. Chẳng hạn, nếu máy 

background image

đích có chỉ thị INC thì câu lệnh ba địa chỉ a := a + 1 có thể được cài đặt chỉ bằng câu  
lệnh INC a. Cách nầy  hiệu quả
 hơn là dùng chuỗi các chỉ thị sau:

 
MOV   a, R

o

ADD   # 1, R

o

MOV   R

o ,

a

 
Như ta đã nói, tốc độ của chỉ thị là một trong những yếu tố quan trọng để thiết kế chuỗi 

mã tốt. Nhưng, thông tin thời gian thường khó xác định.

 
Việc quyết định chuỗi mã máy nào là tốt nhất cho câu lệnh ba điạ chỉ còn phụ thuộc 

vào ngữ cảnh của nơi chưá câu lệnh đó.
Cấp phát thanh ghi 

 
Các chỉ thị dùng toán hạng thanh ghi thường ngắn hơn và nhanh hơn các chỉ thị dùng 

toán hạng trong bộ nhớ. Vì thế,  hiệu quả của thanh ghi đặc biệt quan trọng trong việc 
sinh mã tốt. Ta thường dùng thanh ghi trong hai trường hợp:

 
1. Trong khi cấp phát thanh ghi, ta lựa chọn tập các biến lưu trú trong các thanh ghi tại 

một thời điểm trong chương trình.

2. Trong khi gán thanh ghi, ta lấy ra thanh ghi đặc biệt mà biến sẽ thường trú trong đó.

 
Việc tìm kiếm một lệnh gán tối ưu của thanh ghi, ngay với cả các giá trị thanh ghi đơn, 

cho các biến là một công việc khó khăn. Vấn đề càng trở nên phức tạp hơn vì phần cứng 
và / hoặc hệ điều hành của máy đích yêu cầu qui ước sử dụng thanh ghi. 

3.3. Quản lý bộ nhớ.

Trong phần này  ta sẽ nói về việc sinh mã để quản lý các mẩu tin hoạt động trong thời 

gian thực hiện. Hai chiến lược cấp phát bộ nhớ chuẩn được trình bầy trong chương VII  là 
cấp phát tĩnh và cấp phát Stack. Với cấp phát tĩnh, vị trí của mẩu tin hoạt động trong bộ 
nhớ được xác định trong thời gian biên dịch. Với cấp phát Stack, một mẩu tin hoạt động 
được đưa vào Stack khi có sự thực hiện một thủ tục và được lấy ra khỏi Stack khi hoạt 
động kết thúc. Ở đây, ta sẽ xem xét cách thức mã đích của một thủ tục tham chiếu tới các 
đối tượng dữ liệu trong các mẩu tin hoạt động. Như ta đã nói ở chương VII, một mẩu tin 
hoạt động cho một thủ tục có các trường: tham số, kết quả, thông tin về trạng thái máy, dữ 
liệu cục bộ, lưu trữ tạm thời và cục bộ, và các liên kết. Trong phần nầy, ta minh họa các 
chiến lược cấp phát sử dụng trường trạng thái để giữ giá trị trả về và dữ liệu cục bộ, các 
trường còn lại được dùng như  đã đề cập ở chương VII.

 
Việc cấp phát và giải phóng các mẩu tin hoạt động là một phần trong chuỗi hành vi gọi 

và trả về của chương trình con. Ta quan tâm đến việc sinh mã cho các lệnh sau:

 
1. call
2. return
3. halt

background image

4. action   /*  tượng trưng cho các lệnh khác */
 
Chẳng hạn
, mã ba địa chỉ, chỉ chứa các loại câu lệnh trên, cho các chương trình c và p 

cũng như các mẩu tin hoạt động của chúng:

 

 

Hình 9.2 - Ðầu vào của bộ sinh mã

Kích thước và việc xếp đặt các mẩu tin được kết hợp với bộ sinh mã nhờ thông tin về 

tên trong bảng danh biểu.

Ta giả sử bộ nhớ thời gian thực hiện được phân chia thành các vùng cho mã, dữ liệu 

tĩnh và Stack. 

 
1. Cấp phát tĩnh

 
Chúng ta sẽ xét các chỉ thị cần thiết để thực hiện việc cấp phát tĩnh. Lệnh call trong mã 

trung gian được thực hiện bằng dãy hai chỉ thị đích. Chỉ thị MOV lưu địa chỉ trả về. Chỉ 
thị GOTO chuyển quyền điều khiển cho chương trình được gọi.

 
MOV   # here + 20, callee.static_area
GOTO   callee.code
_area

Các thuộc tính callee.static_area và callee.code_area là các hằng tham chiếu tới các địa 

chỉ của mẩu tin hoạt động và chỉ thị đầu tiên trong đoạn mã của chương trình con được 
gọi.  # here + 20 trong chỉ thị MOV là địa chỉ trả về. Nó cũng chính là địa chỉ của chỉ thị 
đứng sau lệnh GOTO. Mã của chương trình con kết thúc bằng lệnh trả về chương trình 
gọi, trừ chương trình chính, đó là lệnh halt. Lệnh này trả quyền điều khiển cho hệ điều 
hành. Lệnh trả về được dịch sang mã máy là GOTO *callee_static_area thực hiện việc  
chuyển quyền điều khiển về địa chỉ được  lưu giữ  ở ô nhớ đầu tiên của mẩu tin hoạt động 
.

 

background image

Ví dụ 9.1: Mã đích trong chương trình sau được tạo ra từ  các chương trình con c và p ở 
hình 9.2. Giả sử rằng: các mã đó được lưu tại địa chỉ bắt đầu là 100 và 200, mỗi chỉ thị 
action chiếm 20 byte, và các mẩu tin hoạt động cho c và p được cấp phát tĩnh bắt đầu tại 
các địa chỉ 300 và 364 . Ta dùng chỉ thị action để thực hiện câu lệnh action. Như vậy, mã 
đích cho các chương trình con:
 

                                                            /* mã cho c*/
100: ACTION

1

120: MOV   #140, 364          /* lưu địa chỉ trả về 140 */
132: GOTO   200                  /* gọi p */
140: ACTION

2

160: HALT
 
                                                            /* mã cho p */
200: ACTION

3

220: GOTO   *364                /* trả về địa chỉ được lưu tại vị trí 364 */
                                                            /* 300-364 lưu mẩu tin hoạt động của c */
300:                                        /* chứa địa chỉ trả về */
304:                                        /* dữ liệu cục bộ của c */
                                                            /* 364 - 451 chứa mẩu tin hoạt động của p */
364:                                        /* chứa địa chỉ trả về */
368:                                        /* dữ liệu cục bộ của p */
 
 
                                    Hình 9.3 - Mã đích cho đầu vào của hình 9.2

 
Sự thực hiện bắt đầu bằng chỉ thị action tại địa chỉ 100. Chỉ thị MOV ở địa chỉ 120 sẽ 

lưu địa chỉ trả về 140 vào trường trạng thái máy, là từ đầu tiên trong mẩu tin hoạt động 
của p. Chỉ thị GOTO 200 sẽ chuyển quyền điều khiển về chỉ thị đầu tiên trong đoạn mã 
của chương trình con p. Chỉ thị GOTO *364 tại địa chỉ 132 chuyển quyền điều khiển sang 
chỉ thị đầu tiên trong mã đích của chương trình con được gọi. 

 
Giá trị 140 được lưu vào địa chỉ 364, *364 biểu diễn giá trị 140 khi lệnh GOTO tại địa 

chỉ 220 được thực hiện. Vì thế  quyền điều khiển trả về địa chỉ 140 và tiếp tục thực hiện  
chương trình con c.

 
2. Cấp phát theo cơ chế Stack

 
Cấp phát tĩnh sẽ trở thành cấp phát Stack nếu ta sử dụng địa chỉ tương đối để lưu giữ 

các mẩu tin hoạt động. Vị trí mẩu tin hoạt động chỉ được xác định trong thời gian thực thi. 
Trong cấp phát Stack, vị trí nầy thường được lưu vào thanh ghi. Vì thế các ô nhớ của mẩu 
tin hoạt động được truy xuất như là độ dời (offset) so với giá trị trong thanh ghi đó.

 
Thanh ghi SP chứa địa chỉ bắt đầu của mẩu tin hoạt động của chương trình con nằm 

trên đỉnh Stack. Khi lời gọi của chương trình con xuất hiện, chương trình bị gọi được cấp 

background image

phát, SP  được tăng lên một giá trị bằng kích thước mẩu tin hoạt động của chương trình 
gọi và chuyển quyền điều khiển cho chương trình con được gọi. Khi quyền điều khiển trả 
về cho chương trình gọi, SP giảm đi một khoảng bằng kích thước mẩu tin hoạt động của 
chương trình gọi. Vì thế, mẩu tin của chương trình con được gọi đã được giải phóng.

 
Mã cho chương trình con đầu tiên  có dạng:
 
MOV   # Stackstart, SP               /* khởi động Stack */
Ðoạn mã cho chương trình con
HALT                                            /* kết thúc sự thực thi */

Trong đó chỉ thị đầu tiên MOV  #Stackstart, SP khởi động Stack theo cách đặt SP bằng 

với địa chỉ bắt đầu của Stack trong vùng nhớ.

Chuỗi gọi sẽ tăng giá trị của SP, lưu giữ địa chỉ trả về và chuyển quyền điều khiển về 

chương trình được gọi.

 
ADD   # caller.recordsize, SP
MOV   # here + 16, *SP              /* lưu địa chỉ trả về */
GOTO   callee.code
_area
 
Thuộc tính caller.recordsize biểu diễn kích thước của mẩu tin hoạt động. Vì thế, chỉ thị 

ADD đưa SP trỏ tới phần bắt đầu của mẩu tin hoạt động kế tiếp. #here +16 trong chỉ thị 
MOV là địa chỉ của chỉ thị theo sau GOTO, nó được lưu tại địa chỉ được trỏ bởi SP.

 
Chuỗi trả về gồm hai chỉ thị:

 
1. Chương trình con chuyển quyền điều khiển tới địa chỉ trả về
 
GOTO   *0(SP)                         /* trả về chương trình gọi */
SUB   #caller.recordsize,  SP

 
Trong đó O(SP) là địa chỉ của ô nhớ đầu tiên trong mẩu tin hoạt động. *O(SP) trả về 

địa chỉ  được lưu tại đây.

 
2. Chỉ thị SUB  #caller.recordsize, SP: Giảm giá trị của SP xuống một khoảng bằng 

kích thước mẩu tin hoạt động của chương trình gọi. Như vậy mẩu tin hoạt động chương 
trình bị gọi đã xóa khỏi Stack .

Ví dụ 9.2: Giả sử rằng kích thước của các mẩu tin hoạt động của các chương trình con s, 
p,  và q được xác định tại thời gian biên dịch là ssize, psize, và qsize tương ứng. Ô nhớ 
đầu tiên trong mỗi mẩu tin hoạt động lưu địa chỉ trả về. Ta cũng giả sử rằng, đoạn mã cho 
các chương trình con nầy bắt đầu tại các địa chỉ 100, 200, 300 tương ứng, và địa chỉ bắt 
đầu của Stack là 600. Mã đích cho chương trình trong hình 9.4  được mô tả trong hình 
9.5:

background image

/*  mã cho s */

action1

call q

action2

halt

/*  mã cho p */

action3

return

/*  mã cho q */

action

4

call p

action

5

call q

action

6

call q

return

 

Hình 9.4 - Mã ba địa chỉ minh hoạ cấp phát sử dụng Stack

 

                                                            /* mã cho s*/
100: MOV  # 600, SP                        /* khởi động Stack */
108: ACTION

1

128: ADD  #ssize, SP                       /* chuỗi gọi bắt đầu */
136: MOV  #152, *SP                      /* lưu địa chỉ trả về */
144: GOTO  300                               /* gọi q */
152: SUB   #ssize, SP                       /* Lưu giữ SP */
160: ACTION

2

180: HALT
 
                                                            /* mã cho p */
200: ACTION

3

220: GOTO   *0(SP)                         /* trả về chương trình gọi */

 

/* mã cho q */

 

300: ACTION4                                  /* nhảy có điều kiện về 456 */
320: ADD    #qsize, SP
328: MOV    #344, *SP                    /* lưu địa chỉ trả về */
336: GOTO  200                               /* gọi p */
344: SUB    #qsize, SP
352: ACTION

5

372: ADD   #qsize, SP
380: MOV   #396, *SP                     /* lưu địa chỉ trả về */
388: GOTO  300                               /* gọi q */

background image

396: SUB   #qsize, SP
404: ACTION

6

424: ADD   #qsize, SP
432: MOV   #448, *SP                     /* lưu địa chỉ trả về */
440: GOTO   300                              /* gọi q */
448: SUB   #qsize, SP
456: GOTO   *0(SP)                        /* trả về chương trình gọi */
 
600:                                                   /* địa chỉ bắt đầu của Stack trung tâm */
 
                        Hình 9.5 - Mã đích cho chuỗi ba địa chỉ trong hình 9.4

 
Ta giả sử rằng action4 gồm  lệnh nhảy có điều kiện tới địa chỉ 456 có lệnh trả về từ q. 

Ngược lại chương trình đệ quy q có thể gọi chính nó mãi. Trong ví dụ này chúng ta giả sử 
lần gọi đầu tiên trên q sẽ không trả về chương tình gọi ngay, nhưng những lần sau thì có 
thể. SP có giá trị lúc đầu là 600, địa chỉ bắt đầu của Stack. SP lưu giữ giá trị 620 chỉ trước 
khi chuyển quyền điều khiển từ s sang q vì kích thước của mẩu tin hoạt động s là 20. Khi 
q gọi p, SP sẽ tăng lên 680 khi chỉ thị tại địa chỉ 320 được thực hiện, Sp chuyển sang 620 
sau khi chuyển quyền điều khiển  cho chương trình con p. Nếu lời gọi đệ quy của q trả về 
ngay thì giá trị lain nhất của SP trong suốt quá trình thực hiện là 680. Vị trí được cấp phát 
theo cơ chế Stack có thể lên đến địa chỉ 739 vì mẩu tin hoạt động của q bắt đầu tại 680 và 
chiếm 60 byte.
 
3. Ðịa chỉ của các tên trong thời gian thực hiện

 
Chiến lược cấp phát lưu trữ và xếp đặt dữ liệu cục bộ trong mẩu tin hoạt động của 

chương trình con xác định cách thức truy xuất vùng nhớ của tên.

 
Nếu chúng ta dùng cơ chế cấp phát tĩnh với vùng dữ liệu được cấp phát tại địa chỉ 

static. Với lệnh gán x := 0, địa chỉ tương đối của x trong bảng danh biểu là 12. Vậy địa chỉ 
của x trong bộ nhớ là static + 12.   Lệnh gán x:=0 được chuyển sang mã ba địa chỉ 
static[12] := 0. Nếu  vùng dữ liệu bắt đầu tại địa chỉ 100, mã đích cho chỉ thị là:

 
MOV  #0,112

 
Nếu ngôn ngữ dùng cơ chế display để truy xuất tên không cục bộ, giả sử x là tên cục 

bộ của chương trình con hiện hành và thanh ghi R3 lưu giữ địa chỉ bắt đầu của mẩu tin 
hoạt động đó thì chúng ta sẽ dịch lệnh x := 0  sang chuỗi mã ba địa chỉ:

 
  t

1

 := 12 + R

3

 * t

:= 0

Từ đó ta chuyển sang mã đích:

      MOV   #0, 12(R

3

)

background image

 

Chú ý rằng, giá trị thanh ghi R3 không được xác định trong thời gian biên dịch

3.4. Chọn chỉ thị lệnh.

Tập các chỉ thị của máy đích sẽ xác định tính phức tạp của việc lựa chọn chỉ thị. Tính 

chuẩn và hoàn chỉnh của tập chỉ thị là những yếu tố quan trọng. Nếu máy đích không 
cung cấp một mẫu chung cho mỗi  kiểu dữ liệu thì mỗi trường hợp ngoại lệ phải xử lý 
riêng. Tốc độ chỉ thị và sự biểu diễn  của máy cũng là những yếu tố quan trọng. Nếu ta 
không quan tâm đến tính hiệu quả của chương trình đích thì việc lựa chọn chỉ thị sẽ đơn 
giản hơn. Với mỗi lệnh ba địa chỉ ta có thể phác họa một bộ khung cho mã đích. Giả sử  
lệnh ba địa chỉ  dạng x := y + z, với x, y, z được cấp phát tĩnh, có thể được dịch sang 
chuỗi mã đích:

 
MOV   y, R0          /* Lưu y vào thanh ghi Ro */
ADD   z,  R0          /* cộng z vào nội dung Ro, kết quả chứa trong Ro */
MOV   R0, x          /* lưu nội dung Ro vào x */
 
Tuy nhiên việc sinh mã cho chuỗi các lệnh ba địa chỉ sẽ dẫn đến sự dư thừa mã. Chẳng 

hạn với:

a:= b + c
d:= a + e

ta chuyển sang mã đích:

MOV  b, R

o

ADD   c, R

o

MOV  R

o

, a

MOV  a, R

0

ADD   e,R

o

MOV  R

o

, d

và ta nhận thấy rằng chỉ thị thứ tư là thừa. 

Chất lượng mã được tạo ra, được xác định bằng tốc độ và kích thước của mã. Một máy 

đích có tập chỉ thị phong phú có thể sẽ cung cấp nhiều cách để hiện thực một tác vụ cho 
trước. Ðiều này có thể dẫn đến tốc độ thực hiện chỉ thị rất khác nhau. Chẳng hạn, nếu máy 
đích có chỉ thị INC thì câu lệnh ba địa chỉ a := a + 1 có thể được cài đặt chỉ bằng câu  
lệnh INC a. Cách nầy  hiệu quả
 hơn là dùng chuỗi các chỉ thị sau:

 
MOV   a, R

o

ADD   # 1, R

o

MOV   R

o ,

a

 
Như ta đã nói, tốc độ của chỉ thị là một trong những yếu tố quan trọng để thiết kế chuỗi 

mã tốt. Nhưng, thông tin thời gian thường khó xác định.

 

background image

Việc quyết định chuỗi mã máy nào là tốt nhất cho câu lệnh ba điạ chỉ còn phụ thuộc 

vào ngữ cảnh của nơi chưá câu lệnh đó.

3.5. Sử dụng thanh ghi.

Các chỉ thị dùng toán hạng thanh ghi thường ngắn hơn và nhanh hơn các chỉ thị dùng 

toán hạng trong bộ nhớ. Vì thế,  hiệu quả của thanh ghi đặc biệt quan trọng trong việc 
sinh mã tốt. Ta thường dùng thanh ghi trong hai trường hợp:

 
1. Trong khi cấp phát thanh ghi, ta lựa chọn tập các biến lưu trú trong các thanh ghi tại 

một thời điểm trong chương trình.

2. Trong khi gán thanh ghi, ta lấy ra thanh ghi đặc biệt mà biến sẽ thường trú trong đó.

 
Việc tìm kiếm một lệnh gán tối ưu của thanh ghi, ngay với cả các giá trị thanh ghi đơn, 

cho các biến là một công việc khó khăn. Vấn đề càng trở nên phức tạp hơn vì phần cứng 
và / hoặc hệ điều hành của máy đích yêu cầu qui ước sử dụng thanh ghi. 

 

3.6. Thứ tự làm việc.

Thứ tự thực hiện tính toán có thể ảnh hưởng đến tính hiệu quả của mã đích . Một số 

thứ tự tính toán  có thể cần ít thanh ghi để lưu giữ các kết quả trung gian hơn các thứ tự 
tính toán khác. Việc lựa chọn được thứ tự tốt nhất là một vấn đề khó. Ta nên tránh vấn đề 
này bằng cách sinh mã cho các lệnh ba địa chỉ theo thứ tự mà chúng đã được sinh ra bởi 
bộ mã trung gian.
Sinh mã
 

Tiêu chuẩn quan trọng nhất của bộ sinh mã là phải tạo ra mã đúng. Tính đúng của mã 

có một ý nghĩa rất quan trọng. Với những quy định về tính đúng của mã, việc thiết kế bộ 
sinh mã sao cho nó được thực hiện, kiểm tra, bảo trì đơn giản là mục tiêu thiết kế quan 
trọng .

4. MÁY ĐÍCH.

Trong chương trình này, chúng ta sẽ dùng máy đích như là máy thanh ghi (register 

machine). Máy này tượng trưng cho máy tính loại trung bình. Tuy nhiên, các kỹ thuật 
sinh mã được trình bầy trong chương này có thể dùng cho nhiều loại máy tính khác nhau.

 
Máy đích của chúng ta là máy tính địa chỉ byte với mỗi từ gồm bốn byte và có n thanh 

ghi : R

0

, R

1

 ... R

n-1

 . Máy đích gồm các chỉ thị hai địa chỉ có dạng chung:

 
       op source, destination

 
Trong đó op là mã tác vụ. Source (nguồn) và destination (đích) là các trường dữ liệu. 

Ví dụ một số mã tác vụ:

background image

 
MOV          chuyển  source đến destination
ADD          cộng source và destination
SUB           trừ  source cho destination
 
Source và destination của một chỉ thị được xác định bằng cách kết hợp các  thanh ghi 

và các vị trí nhớ với các mode địa chỉ. Mô tả content (a) biểu diễn cho nội dung của thanh 
ghi hoặc điạ chỉ của bộ nhớ được biểu diễn bởi a.

mode địa chỉ cùng với dạng hợp ngữ  và giá kết hợp:

 

Mode

Dạng

Ðịa chỉ

Giá

Absolute

Register

Indexed

Indirect  register
Indirect  indexed

M

R

c(R)

*R

*c(R)

M

R

c + contents ( R)

contents ( R)

contents (c+ contents ( R))

1
0
1
0
1

 
Vị trí nhớ M hoặc thanh ghi  R biểu diễn chính nó khi đưọc sử dụng như một nguồn 

hay đích. Ðộ dời địa chỉ c từ giá trị trong thanh ghi R được viết là c( R).

 
Chẳng hạn:
 

1.  MOV   R

0

, M : Lưu nội dung của thanh ghi R

0

 vào vị trí nhớ  M .

2.   MOV    4(R

0

), M : Xác định một địa chỉ mới bằng cách lấy độ dời tương đối  

(offset) 4 cộng với nội dung của R

0

, sau đó lấy nội dung tại địa chỉ này, contains(4 + 

contains(R

0

)), lưu vào vị trí nhớ M.

3.  MOV   * 4(R

0

) , M : Lưu giá trị contents (contents (4 + contents (R

0

))) vào  vị trí 

nhớ M. 

4.  MOV  #1, R

0

 : Lấy hằng 1 lưu vào thanh ghi R

0

 
Giá của chỉ thị

 
Giá của chỉ thị (instrustion cost) được tính bằng một cộng với giá kết hợp mode địa chỉ 

nguồn và đích trong bảng trên. Giá này tượng trưng cho chiều dài của chỉ thị. Mode địa 
chỉ dùng thanh ghi sẽ  có giá bằng không  và có giá bằng một khi nó dùng vị trí nhớ hoặc 
hằng. Nếu vấn đề vị trí nhớ là quan trọng thì chúng ta nên tối thiểu hóa chiều dài chỉ thị. 
Ðối với phần lớn các máy và phần lớn các chỉ thị, thời gian cần để lấy một chỉ thị từ bộ 
nhớ bao giờ cũng xảy ra trước thời gian thực hiện chỉ thị. Vì vậy, bằng việc tối thiểu hóa 
độ dài chỉ thị, ta còn tối thiểu hoá được thời gian cần để thực hiện chỉ thị.

 
Một số minh họa việc tính giá của chỉ thị:
 

background image

1. Chỉ thị MOV  R

0

, R

1

 : Sao chép nội dung thanh ghi R

0

 vào thanh ghi R

1

. Chỉ thị này 

có giá là một vì nó chỉ chiếm một từ trong bộ nhớ .

2. MOV  R

5

, M:  Sao chép nội dung thanh ghi R

5

 vào vị trí nhớ M. Chỉ thị này có giá 

trị là hai vì địa chỉ của vị trí nhớ M là một từ  sau chỉ thị.

3. Chỉ thị ADD  #1, R

3

: cộng hằng 1 vào nội dung thanh ghi R

3

. Chỉ thị có giá là hai vì 

hằng 1 phải xuất hiện trong từ  kế tiếp sau chỉ thị.

4. Chỉ thị SUB 4(R

0

), *12 (R

1

) : Lưu   giá trị của contents (contents (12 + contents 

(R1))) - contents (4 + contents (R

0

)) vào đích *12( R

1

). Giá của chỉ thị nầy là ba vì hằng 4 

và 12 được lưu trữ trong hai từ kế tiếp theo sau chỉ thị.

 
Với mỗi câu lệnh ba địa chỉ, ta có thể có nhiều cách cài đặt khác nhau. Ví dụ câu lệnh 

a := b + c - trong đó b và c là biến đơn, được lưu chứa trong các vị trí nhớ phân biệt có tên 
b, c - có những cách cài đặt sau:

1. MOV   b, R

o

    ADD    c, R0                           giá =  6
    MOV   R

o

, a

2. MOV   b, a                              giá =  6
    ADD    c, a
3. Giả sử thanh ghi R0, R1, R2 giữ địa chỉ của a, b, c. Chúng ta có thể dùng hai địa 

chỉ sau cho việc sinh mã lệnh:

    a := b + c  => 
    MOV   *R1, *Ro                    giá = 2
    ADD   
* R

2

*R

o

4. Giả sử thanh ghi R1 và R2 chứa giá trị của b và c và trị của b không cần lưu lại 

sau lệnh gán. Chúng ta có thể dùng hai chỉ thị sau:

    ADD   R2, R1                         giá = 3
    MOV   R

1

, a

Như vậy, với mỗi cách cài đặt khác nhau ta có những giá khác nhau. Ta cũng thấy rằng 

muốn sinh mã tốt thì phải hạ giá của các chỉ thị . Tuy nhiên việc làm khó mà thực hiện 
được. Nếu có những quy ước trước cho thanh ghi, lưu giữ địa chỉ của vị trí nhớ chứa giá 
trị tính toán hay địa chỉ để đưa trị vào, thì việc lựa chọn chỉ thị sẽ dễ dàng hơn.

 

5. MỘT BỘ SINH MàĐƠN GIẢN.

Ta giả sử rằng, bộ sinh mã này sinh mã đích từ chuỗi các lệnh ba địa chỉ. Mỗi toán tử 

trong lệnh ba địa chỉ  tương ứng với một toán tử của máy đích. Các kết quả tính toán có 
thể nằm lại trong thanh ghi cho tới bao lâu có thể được và chỉ được  lưu trữ khi:

 
(a)     Thanh ghi đó được sử dụng cho sự tính toán khác
(b)    Trước khi có lệnh gọi chương trình con, lệnh nhảy hoặc lệnh có nhãn.

 
Ðiều kiện (b) chỉ ra rằng bất cứ giá trị nào cũng phải được lưu  vào bộ nhớ trước khi 

kết thúc một khối cơ bản. Vì  sau khi ra khỏi khối cơ bản, ta có thể đi tới các khối khác 
hoặc ta có thể đi tới một khối xác định từ một khối khác. Trong trường hợp (a), ta không 

background image

thể làm được điều nầy mà không giả sử   rằng số lượng được dùng bởi khối xuất hiện 
trong cùng thanh ghi không có cách nào để đạt tới khối đó. Ðể tránh lỗi có thể xảy ra, giải 
thuật sinh mã đơn giản sẽ lưu giữ tất cả các giá trị khi đi qua ranh giới của khối cơ bản 
cũng như khi gọi chương trình con.

 
Ta có thể tạo ra mã phù họp với câu lệnh ba địa chỉ a := b + c nếu ta tạo ra chỉ thị đơn 

ADD Rj, Ri với giá là 1. Kết quả a được đưa vào thanh ghi Ri chỉ nếu thanh ghi Ri chứa 
b, thanh ghi Rj chứa c, và b không được sử dụng nữa.

 
Nếu b ở trong Ri , c ở trong bộ nhớ , ta có thể tạo chỉ thị:
 
       ADD   c, Ri                 giá = 2
 
Hoặc nếu  b ở trong thanh ghi Ri  và giá trị của c được đưa từ bộ nhớ vào Rj sau đó 

thực hiện phép cộng hai thanh ghi Ri, Rj, ta có thể tạo các chỉ thị:

 
       MOV   c, R

j

       ADD   Rj , Ri              giá = 3
 
Qua các trường hợp trên chúng ta thấy rằng có nhiều khả năng để tạo ra mã đích cho 

một lệnh ba địa chỉ. Tuy nhiên, việc lựa chọn khả năng nào lại tuỳ thuộc vào ngữ cảnh 
của mỗi thời điểm cần  tạo mã.

1. Mô tả thanh ghi và địa chỉ

 
Giải thuật sinh mã đích  dùng bộ mô tả (descriptor) để lưu giữ nội dung thanh ghi và 

địa chỉ của tên.

 
1. Bộ mô tả thanh ghi sẽ lưu giữ những gì tồn tại trong từng thanh ghi cũng như cho ta 

biết khi nào cần một thanh ghi mới. Ta giả sử rằng lúc đầu, bộ mô tả sẽ khởi động sao cho 
tất cả các thanh ghi đều rỗng. Khi sinh mã cho các khối cơ bản, mỗi thanh ghi sẽ giữ giá 
trị 0 hoặc các tên tại thời điểm thực hiện.

2. Bộ mô tả địa chỉ sẽ lưu giữ các vị trí nhớ  nơi giá trị của tên có thể được tìm thấy tại 

thời điểm thực thi. Các vị trí đó có thể là thanh ghi, vị trí trên Stack, địa chỉ bộ nhớ. Tất cả 
các thông tin này được lưu chứa trong bảng danh biểu và sẽ được dùng để xác định 
phương pháp truy xuất tên.

 
2. Giải thuật sinh mã đích

 

Giải thuật sinh mã sẽ nhận vào chuỗi các lệnh ba địa chỉ của một khối cơ bản. Với mỗi 

lệnh ba địa chỉ  dạng x := y op z  ta thực hiện các bước sau:

 
1. Gọi hàm  getreg  để xác định vị trí L nơi lưu giữ kết quả của phép tính  y op z. L 

thường là thanh ghi nhưng nó cũng có thể là một vị trí nhớ. 

background image

2. Xác định địa chỉ mô tả cho y để từ  đó xác định y’ một trong những vị trí hiện hành 

của y. Chúng ta ưu tiên chọn thanh ghi cho y’ nếu cả thanh ghi và vị trí nhớ đang giữ  giá 
trị của y. Nếu giá trị của y chưa có trong L, ta tạo ra chỉ thị:

MOV   y', L    để lưu bản sao của y vào L.
3. Tạo chỉ thị op z', L  với z' là vị trí hiện hành của z. Ta ưu tiên chọn thanh ghi cho z' 

nếu giá trị của z được lưu giữ  ở cả thanh ghi và bộ nhớ. Việc xác lập mô tả địa chỉ của x 
chỉ ra rằng x đang ở trong vị trí L. Nếu L là thanh ghi thì L là đang giữ trị của x và loại bỏ 
x ra khỏi tất cả các bộ mô tả thanh ghi khác.

4. Nếu giá trị hiện tại của y và/ hoặc z  không còn được dùng nữa khi ra khỏi khối, và 

chúng đang ở trong thanh ghi thì sau khi ra khỏi khối ta phải xác lập mô tả thanh ghi để 
chỉ ra rằng các thanh ghi trên sẽ không giữ trị y và/hoặc z.

 
Nếu mã ba địa chỉ có  phép toán một ngôi thì các bước thực hiện sinh mã đích cũng 

tương tự như  trên.

 
Một trường hợp cần đặc biệt lưu ý là lệnh x := y. Nếu y ở trong thanh ghi, ta phải thay 

đổi thanh ghi và bộ mô tả địa chỉ, là giá trị của x được tìm thấy ở thanh ghi chứagiá trị của 
y. Nếu y không được dùng tiếp thì thanh ghi đó sẽ không còn lưu trị của y nữa. Nếu y ở 
trong bộ nhớ, ta dùng hàm getreg để tìm một thanh ghi tải giá trị của y và xác lập rằng 
thanh ghi đó là vị trí của x. Nếu ta thông báo rằng vị trí nhớ chứa giá trị của x là vị trí nhớ 
của y thì vấn đề trở nên phức tạp hơn vì ta không thể thay đổi giá trị của y nếu không tìm 
một chỗ khác để  lưu giá trị của x trước đó. 
 
3. Hàm getreg

 
Hàm getreg sẽ trả về vị trí nhớ L lưu giữ giá trị của x trong lệnh x := y op z. Sau đây là 

cách đơn giản dùng để cài đặt hàm:

1. Nếu y đang ở trong thanh ghi và y sẽ không được dùng nữa sau khi thực hiện x := y 

op z thì trả thanh ghi chứa y cho L và xác lập thông tin cho bộ mô tả địa chỉ của y rằng y 
không còn trong L.

2. Ngược lại, trả về một thanh ghi rỗng (nếu có). 
3. Nếu không có thanh ghi rỗng  và nếu x còn được dùng tiếp trong khối hoặc toán tử 

op cần thanh ghi, ta chọn một thanh ghi không rỗng R. Lưu giá trị của R vào vị trí nhớ M 
bằng chỉ thị MOV R,M. Nếu M chưa chứa giá trị nào, xác lập thông tin  bộ mô tả địa chỉ 
cho M và trả về R. Nếu R giữ trị của một số biến, ta phải dùng chỉ thị MOV để lần lượt 
lưu giá trị cho từng biến.

4.  Nếu x không được dùng nữa hoặc không có một thanh ghi phù hợp nào được tìm 

thấy,  ta chọn vị trí nhớ của x như L.

Ví dụ 9.5 : Lệnh gán  d := (a - b) + (a - c) + (a - c)

Có thể được chuyển sang chuỗi mã ba địa chỉ:
 
       t := a - b
       u := a - c

background image

       v := t + u
       d := v + u
 
và d sẽ “sống” đến hết chương trình. Từ chuỗi lệnh ba địa chỉ nầy, giải thuật sinh mã 

vừa được trình bày sẽ tạo chuỗi mã đích với giả sử rằng: a, b, c luôn ở trong bộ nhớ và t, 
u, v là các biến tạm không có trong bộ nhớ .

 

Câu lệnh 3 

địa chỉ

Mã đích

Giá

Bộ mô tả thanh ghi

Bộ mô tả địa chỉ

t := a - b

 
 

u := a - c

 

v := t + u

d := v + u

 

MOV     a, R

0

 

SUB       b, R

0

MOV     a, R

1

SUB       c, R

1

ADD      R

1

, R

0

ADD      R

1

, R

0

MOV     R

0

, d

2

 

2
2
2
1
1
2

Thanh ghi rỗng, R

0

chứa t

R

0

 chứa t

          

R

1

 chứa u

R

0

 chứa v

R

1

 chúa u

R

0

 chứa d

t ở trong R

0

 

t ở trong R

0

u ở  rong R

1

u ở trong R

1

v ở trong R

0

d ở  rong R

0

d ở trong bộ nhớ

 

Hình 9.9 - Chuỗi mã đích

 
Lần gọi đầu tiên của hàm getreg trả về R

0

 như  một vị trí để xác định t. Vì a không ở 

trong R

0

 , ta tạo ra chỉ thỉ MOV a, R

0

 và SUB  b, R

0

. Ta cập nhật lại bộ mô tả để chỉ ra 

rằng R

0

 chứa t.

 
Việc sinh mã đích tiếp tục tiến hành theo cách nầy cho đến khi lệnh ba địa chỉ cuối 

cùng d := v + u được xử lý. Chú ý rằng R

0

 là rỗng vì u không còn được dùng nữa. Sau đó 

ta tạo ra chỉ thị, cuối cùng của khối, MOV R

0

, d để lưu  biến “sống” d. Giá của chuỗi mã 

đích được sinh ra như ở trên là 12. Tuy nhiên, ta có thể giảm giá xuống còn 11 bằng cách  
thay chỉ thị MOV  a, R

1

 bằng MOV R

0

, R

1

 và xếp chỉ thị nầy sau chỉ thị thứ nhất. 

 
4. Sinh mã cho loại lệnh khác

 

Các phép toán xác định chỉ số và con trỏ trong câu lệnh ba địa chỉ được thực hiện 

giống như các phép toán hai ngôi. Hình sau minh họa việc sinh mã đích cho các câu lệnh 
gán:  a := b[i],  a[i] := b và giả sử b được cấp phát tĩnh .

 

Câu lệnh 

3 địa chỉ

(1)

i trong thanh ghi R

i

(2)

i trong bộ nhớ Mi

(3)

i  trên Stack

Giá

Giá

Giá

a:= b[ i ]

 

a[i]:=b

MOV  b(R

), R

 
MOV  b, a(R

i

)

2

 

3

 

MOV  M

i

, R

MOV  b(R), R
MOV  M

, R

MOV  b, a (R)

4

 

5

MOV  S

i

(A), R

MOV  b(R), R
MOV  S

i

(A), R

MOV  b, a (R)

4

 

5

 

 

background image

Hình 9.10 -  Chuỗi mã đích cho phép gán chỉ mục

 

Với mỗi câu lệnh ba địa chỉ trên ta có thể có nhiều đoạn mã đích khác nhau tuỳ thuộc 

vào i đang ở trong thanh ghi, hoặc trong vị trí nhớ M

i

 hoặc trên Stack tại vị trí S

i

 và con 

trỏ trong thanh ghi A chỉ tới mẩu tin hoạt động của i. Thanh ghi R là kết quả trả về khi 
hàm getreg được gọi. Ðối với lệnh gán đầu tiên, ta đưa a vào trong R nếu a tiếp tục được 
dùng trong khối và có sẵn thanh ghi R. Trong câu lệnh thứ hai ta giả sử rằng a được cấp 
phát tĩnh.

 
Sau đây là chuỗi mã đích được sinh ra cho các lệnh gán con trỏ  dạng a := *p  *p :=  

a. Vị trí nhớ p sẽ xác định chuỗi mã đích tương ứng.

 

Câu lệnh 

3 địa chỉ

p trong thanh ghi 

R

p

p trong bộ nhớ Mi

p trong Stack

 

Giá

Giá

Giá

a:= *p
 
*p:= a

MOV *R

p

, a

 
MOV a, *R

p

2

 

2

 

MOV  M

p

, R

MOV  *R, R
MOV  M

p

, R

MOV   a, *R

3

 

4

MOV  S

p

(A), R

MOV  *R, R
MOV  a, R
MOV  R, *S

p

(A)

3

 

4

 

 

Hình 9.11 - Mã đích cho phép gán con trỏ

 
Ba chuỗi mã đích tuỳ thuộc vào p ở trong thanh ghi R

p

, hoặc p trong vị trí nhớ M

p

hoặc p ở trong Stack tại offset là S

p

 và con trỏ, trong thanh ghi A, trỏ tới mẩu tin hoạt 

động của p. Thanh ghi R là kết quả trả về khi hàm getreg được gọi. Trong câu lệnh gán 
thứ hai ta giả sử rằng a được cấp phát tĩnh.
                       
5. Sinh mã cho lệnh điều kiện

 
Máy tính sẽ thực thi lệnh nhảy có điều kiện theo một trong hai cách sau:
 
1. Rẽ nhánh khi giá trị của thanh ghi được xác định trùng với một trong sáu điều kiện 

sau: âm, không, dương, không âm, khác không, không dương. Chẳng hạn, câu lệnh ba địa 
chỉ  if x < y goto z có thể được thực hiện bằng cách lấy x trong thanh ghi R trừ y. Sau đó 
sẽ nhảy về z nếu giá trị trong thanh ghi R là âm. 

2. Dùng tập các mã điều kiện để xác định giá trị trong thanh ghi R là âm, bằng không 

hay dương. Chỉ thị so sánh CMP sẽ kiểm tra mã điều kiện mà không cần biết  trị tính toán 
cụ thể. Chẳng hạn, CMP x, y xác lập điều kiện dương nếu x > y,... Chỉ thị nhảy có điều 
kiện được thực hiện nếu điều kiện < , =, >, >=,<>, <=  được xác lập. Ta dùng chỉ thị nhảy 
có điều kiện CJ <= z để nhảy đến z nếu mã điều kiện là âm hoặc bằng không.

 
Chẳng hạn, lệnh điều kiện  if x < y goto z được dịch sang mã máy như sau.
                   CMP x,y
                   CJ < z

background image

§Ó x©y dùng s¬ ®å dÞch cho ph¬ng ph¸p ph©n tÝch duyÖt 

lïi, tríc hÕt lo¹i bá ®Ö qui tr¸i, t¹o nh©n tè tr¸i cña v¨n ph¹m s¬ 
®å dÞch theo nguyªn t¾c sau:
A1). Mçi ký hiÖu cha kÕt thóc t¬ng øng víi mét s¬ ®åtrong ®ã 
nh·n cho c¸c c¹nh lµ token hoÆc ký hiÖu cha kÕt thóc.

VÝ dô :XÐt v¨n ph¹m sinh biÓu thøc to¸n häc

 E + T |T

 T * E | F

 E0 | id

Khö ®Ö qui tr¸i ta ®îc

TE' 

E' 

 + TE' | 

ε

 

FT' 

T' 

 * FT' | 

ε

 

 (E) | id S¬ ®å dÞch t¬ng øng 

background image

Rót gän s¬ ®å b»ng c¸c thay thÕ t¬ng øng 

 

background image

CS 3240 Homework I

Scanning and Parsing

Let us consider the language of arithmetic expressions
 
The alphabet of this language is the set 
{+, -, *, /, (, ), x, y, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 
 
Note commas are not a part of the alphabet in the above set – they are only shown to separate 
elements of the set. That is, strings in this language can be composed only by using one or more 
of  the following
 + - * / ( ) x y 0 1 2 3 4 5 6 7 8 9
 
The tokens in this language are of the following classes
 
MOPER

* /

AOPER

: + -

CONS

Strings made of 0 through 9

VAR

x y

OPARAN

(

CPARAN

: )

 
Consider a compiler that scans and parses the language of arithmetic expressions
 
Question 1: As you scan the following expression from left to right, list the tokens and the token 
class identified by the scanner for each of the arithmetic expressions below. Identify, explain 
and clearly mark the errors if any                                     (30 points)
 
a.

( x * ( y + 100 ) + y – ( x + y – 320 ) ) 

b.

( y + 100 * x + ( 2 + x^3 ) / y ) 

c.

x * ) 4 + / 100 - y

d.

y * ( ( x + 100

e.

(20 + x * 4 / 30y3 ) 

 
The grammar for the language of arithmetic expressions is as follows
 
<EXPR>

<TERM> AOPER <TERM>

<EXPR>

<TERM>

<TERM>

<FAC> MOPER <FAC>

<TERM>

<FAC>

<FAC>

OPARAN <EXPR> CPARAN

<FAC>

VAR

<FAC>

CONS

 
 
Question 2: What are the terminals and non-terminals in this grammar?    (10 points)
 
Question 3: For each of the expressions below, scan it from left to right; list the tokens returned 
by the scanner and the  rules  used by the parser (showing appropriate expansions of the non-

background image

terminals)   for   matching.  Identify,   explain  and   clearly  mark  the   errors   if   any 
(40 points)
 
 

a.

a.

              

( x + y )

b.

b.

              

( y * - x ) + 10

c.

c.

              

( x * ( y + 10 ) )

d.

d.

              

( x + y ) * ( y + z )

e.

e.

              

( x + ( y – ( 2 ) )

 

 
Question 4: You are asked the count the number of constants (CONS), variables (VAR) and 
MOPER  in an expression. Insert action symbols in the grammar described before Question 2, 
explain   what   semantic   actions   they   trigger   and   what   each   semantic   action   does. 
(20 points)

Regular Expressions

 
Question 1: Consider the concept of “closure”. A set S is said to be closed under a (binary) 
operation  

  if and only if applying the operation to two elements in the set results in another 

element in the set. For example, consider the set of natural numbers  N  and the “+” (addition) 
operation. If we add any two natural  numbers, we get a natural  number. Formally x, y are 
elements of N implies x y is an element of N.  State true or false and explain why

 
a. Only infinite sets (sets with infinite number of elements, like the set of natural numbers) 

can be closed 

 
b. Infinite sets are closed under all operations 

 

c. The set [a-z]* is closed under concatenation operation 

 
Question 2: 
 
For each of the regular expressions below, state if they describe the same set of strings (state if 
they are equivalent). If they are equivalent, what is the string they describe?
 
1. 

[a-z][a-z]*

and

[a-z]+ 

 
2. 

[a-z0-9]+

and

[a-z]+[0-9]+

 
3. 

[ab]?[12]?

and

a1|b1|a2|b2

 
4. 

[ab12]+

and

a|b|1|2|[ab12]* 

 
5. 

[-az]*

and

[a-z]* 

 
6. 

[abc]+

and

[cba]+ 

background image

 
7. 

[a-j][k-z]

and

[a-z] 

 
 
Question 3: 
 
For each of the strings described below, write a regular expression that describes them and draw a 
finite automaton that accepts them. 
 

1.

1.

                  

The string of zero or more a followed by three b followed zero or more c

 
2.

2.

                  

The string of zero or more  a,  b  and  c  but every  a  is followed by two or 

more b

 

3.

3.

                  

All strings of digits that represent even numbers

 

4.

4.

                  

All strings of a’s and b’s that contain no three consecutive b’s.

 

5.

5.

                  

All strings that can be made from {0, 1} except the strings 11 and 111

background image

Question 1: Pumping Lemma and Regular Languages
You can use the pumping lemma and the closure of the class of regular 
languages under
union, intersection and complement to answer the following question. Proofs 
should be
rigorous. Note that for each of the questions below, you may or may not have 
to use the
pumping lemma.
Note that the notation 0

means “0 repeated times”. So the language of 

strings of the
form 0

such that ¡Ý 0 would contain strings like the null string 0, 00, 000, 

… (this is
[0]*. Whereas the language of strings of the form 0

such that ¡Ý 1 would 

be [0]+)
a. Is the language of strings of the form 0

m

1

n

0

such that m, n 

¡Ý regular? If 

it is regular,
prove that it is regular. If it is not regular, prove that is not regular. Note 
that, a rigorous
proof is needed. General reasoning or explanations that are not rigorous will 
not get full
credit. (15 points)
b. Consider a language whose alphabet is from the set {a, b}. Is the 
language of
palindromes over this alphabet regular? If it is regular, prove that it is 
regular. If it is not
regular, prove that is not regular. Note that, a rigorous proof is needed. 
General reasoning
or explanations that are not rigorous will not get full credit. (15 points)
Hint: A palindrome is a word such that when read backwards, is the same 
word. For
example the word “mom” when read left to right is the same as it is when it 
is read right
to left. In general, the first half, when reversed, yields the second half. If the 
length of the
string is odd, the middle character is left as it is. For example, consider the 
word
“redivider”. Reversing “redi” yields “ider” and “v” is left as it is. For strings 
with
alphabet {a, b}, “aaabaaa” is a palindrome but “abaaa” is not.
c. A language, whose alphabet is {a, b}, such that the strings of the 
language contain
equal number of “ab” and “ba”. Note that “aba” is part of the language, 
because the first
letter and the second letter form “ab” and the second and third form “ba”. Is 
this language
regular? If it is regular, prove that it is regular. If it is not regular, prove that 
is not

background image

regular. Note that, a rigorous proof is needed. General reasoning or 
explanations that are
not rigorous will not get full credit. (15 points)
d. The class of regular languages is closed under union. That is of A is a 
regular language
and B is a regular language, then C is a regular language, where C = A . B. 
Note that B
. C. (B is a subset of C). Let D be some subset of C (that is, D . C). In general, 
is D
regular? If it is regular, prove that it is regular. If it is not regular, prove that 
is not
regular. Note that, a rigorous proof is needed. General reasoning or 
explanations that are
not rigorous will not get full credit. (15 points)
Question 2:
Consider the language described by the regular expression a+b*a, the set of 
all strings
that has one or more a’s followed by zero or more b’s and ending in a single 
a.
a. Construct a NFA which recognizes this language. Note that you need to 
construct a
primitive NFA using the constructions describe in class. (10 points)
b. Convert the above NFA to a DFA using . closure. Clearly indicate the steps 
of .
closure. (20 points)
c. Convert the above DFA to an optimized DFA (10 points)

background image

HomeWork

1. Work on the homework individually. Do not collaborate or copy from others
2. The homework is due on Tuesday, April 24 In Class. No late submissions 
will be entertained
3. Do not email your answers to either the Professor or the TA. Emailed 
answers will not be
considered for evaluation
Question 1. (50 Points)
Consider the following grammar. Construct LR(0) items, DFA for this grammar 
showing LR(0) shiftreduce
table. Is this grammar LR(0)? Indicate all possible shift-reduce as well as 
reduce-reduce
conflicts. Using the concept of look-ahead, generate SLR(1) table – which 
LR(0) conflicts get
eliminated? Using the input (ID + ID) * ID show the SLR(1) parse - show the 
stack states and shifts
and reductions as shown in the examples in the Louden book.
Grammar:
E' -> E
E -> E + T
E -> T
T -> T * ID
T -> ID
T -> (E)
Question 2. (50 Points)
Construct a pushdown automaton for the following language:
L = { a

i

b

j

c

| i, j, k >= 0, either i = j or j = k}

background image

Practice

Q #1. Design a Turing machine for recognizing the language (please give a 
formal
description including tape alphabet, full state transition diagram identifying 
the
acceptance and rejection states if any)
L = {a

b

c

| n >= 0}

L = { w | w contains twice as many 0's as 1's, w is made from {0,1}* }
Q #2. Design a Turing machine to perform multiplication of two natural 
numbers
represented as the number of zeroes. For example, number five is 
represented as 00000
Hint: Use repeated addition
Q #3 Design LR(0) items, their DFA and SLR(1) parse table for the following 
grammar
showing the parse for the following input : ((a), a, (a, a)) Also show the parse 
tree
obtained. Is this a LR(0) grammar? If not show the conflicts and show how 
you can
resolve them through SLR(1) construction
Grammar :
E -> (L)| a
L -> L, E| E
Q #4 Design Context free grammars for the following languages (alphabet is 
{0,1})
a. {w | w starts and ends with the same symbol (either 0 or 1, which is the 
alphabet)}
b. {w | w = w

ie, w is a palindrome}

c. {a

b

c

| i = j or j = k, i, j, k >= 0}

Q #5 Design pushdown automata (PDA) for the following language:
{w | w has odd length and the middle character is 0}
Q #6 Show first, follow and predict sets for the following grammar after 
removing left
recursion and left factoring:
E -> E + T
E -> T
T -> T * P
T -> P
P -> (E)
P -> ID
Q # 7 Using the pumping lemma show that the following languages are not 
regular:
{0

1

| m not equal to n}

{0

2n

| n >= 0}
Q #8 Design NFA, DFA and minimize the DFA for the regular expression:
0

*

1

*

0

*

0

background image

Test 1

Question 1: DFAs (Choose any three questions out of five: 30 points)
Devise DFAs for:
1. All strings that start with 1 must end with a 0 and those which start with
0 must end with 1 (alphabet of this language is {0,1}), no null string
2. All strings from the alphabet {a, b} which contain an odd number of a’s
and even (but non-zero) number of b’s
3. All strings that must have 0110 as the substring (alphabet {0,1})
4. All strings which have a length greater than or equal to 3 and ending on
b or two consecutive a’s
5. Strings that do not contain 3 consecutive a’s
Question 2: Regular expressions (Choose any three questions out of 
five: 30 points)
Write regular expressions for:
1. Expressions that enumerate all positive integers (including 0) upto 100000
but without any leading zeroes
2. Strings made from {a, b} that start and end on the same letter (ie, strings
starting with a end on a and those starting with b end on b)
3. Floats using decimal point representation with integer and fractional parts
– no leading or trailing zeros and precision upto 4 places after decimal
4. Identifiers that start with a digit or lowercase letter following which one
can optionally have one or more of digits or letters or underscores.
Identifiers can not end on an underscore (consecutive underscores ok
though)
5. Positive integers no leading zeros in which all 2’s should occur only after
3’s and all 1’s should occur only after 2’s (ie, no 2 should occur before a 3
or no 1 should occur before a 2).
Question 3Regular Expression NFA DFA (30 points)
Convert the following regular expression into a NFA and convert the NFA to 
DFA
showing the key steps (such as computing å-closures of sets of states etc.) : 
b[ab]

Show

all possible NFA transitions (using parallel tree) for the string babba and 
verify the state
transitions in corresponding DFA
Question 4: State True or False (10 points)
a. Consider a language S=(a|b)*. Consider a Regular Language L, whose 
alphabet is
from the set .= {a, b}. Let M be a DFA that Recognizes L. Let M' be a DFA
obtained from M by changing all accepting states of the M into non-accepting
states, and by changing all non-accepting states of M to accepting states. M'
recognizes the complement of language L given by S – L
b. For every NFA and its equivalent DFA, the number of states in equivalent 
DFA
must be at least equal to the number of states in the NFA.
c. Consider languages L and L’ such that L . L’. Let M be a DFA that 
recognizes L

background image

and M’ be DFA that recognizes L’ then the number of states in M’ must be 
equal
to or greater than those in M.
d. Consider languages L and L’ such that L . L’. Let M be a DFA that 
recognizes L
and M’ be DFA that recognizes L’ then the number of states in M’ must be 
lesser
than or equal to those in M.
e. For every regular expression there can exist more than one DFA that 
recognizes
the language described by the regular expression.
.

background image

Tesst 2

background image

Project

Notes:
1. This project has two phases. Phase 1 is due by April 14

th 

by 5pm. Phase 2 

is due by April 28

th

by 5pm.
2. There will be no extensions for either phases
3. You will work in groups of three
4. Each group should submit a report and source code for each phase. If 
multiple source files, they
must be tarred along with the makefile
5. You can program in C, C++ or Java. Do not use tools (like lex and yacc) or 
the standard
template library
6. Code should be properly documented with meaningful variable and 
function names. Short
elegant code will get bonus points.
7. You will find the course slides on DFA/NFA/scanner/recursive descent 
parser useful.
8. Each phase of the project is worth 100 points. The bonus section is worth 
50 points.

Phase 1:

Objective: To write a scanner and parser which can construct and execute an 
NFA for any regular
expression.
Consider the language of regular expressions. The alphabet of this language 
is the set 

{a, b, *,

+, (, ), ., |} 

(commas and spaces are not part of the language). Using 

this alphabet one can
write any regular expression. Our goal in this project is to be able to read any 
regular expression
described by the following grammar and construct primitive NFAs and join 
them together to form a
NFA that will recognize strings described by the regular expression. We will 
do this step by step by
developing answers to the following questions. The production rules for this 
language are given by
R . R*
R . R+
R . (R)
R . (R | R)
R . R

.

R

R . a
R . b
Question 1: Rewrite the grammar to remove left recursion.
Question 2: Identify the tokens of this language and write a scanner program 
which can scan this
language and return tokens .

background image

Question 3: Write a recursive descent parser which can parse this language 
(based on the modified
grammar which removed left recursion) and yield a parse tree. Note that this 
grammar has implicit
precedence. That is for a regular expression, a.b* the “*” operates on “b” and 
not a.b as a whole. This
is true unless it is bracketed. In, (a.b)* on the other hand, the “*” operates on 
(a.b) When you build a
parse tree you must take care of such precedences
Question 4: Now you need to write a program which can construct a NFAs 
based on the parse tree
based on primitive NFAs. As discussed in class, primitive NFAs should be 
joined together to form
NFA for the complete regular expression. This final NFA will be represented 
as an adjacency matrix
described below. Thus the output of this program should be an adjacency 
matrix.
Adjacency matrix: Any NFA is a directed graph. A directed graph G consists of 
a set of nodes (in our
case states) and directed edges (in our case, transitions). For example, in the 
graph below, A,B,C are
nodes and 1,2,3 are edges
A
B
C
1 2
3
Any directed graph can be represented by an adjacency matrix. For example, 
the matrix below
represents the graph. Since edge “1” connects A to B, there is a “1” in the 
row corresponding to “A”
and the column corresponding to “B”.
A B C
A 1 3
B 2
C
Similarly an NFA can be represented by an adjacency matrix. Note that more 
than one element can be
present in a cell. For example, in the NFA if the edge from A to B is labeled 
a,b then you would have
both “a” and “b” in the corresponding cell.
Question 5: Given such an adjacency matrix of an NFA and given an input 
string consisting of a’s and
b’s write a program to simulate the NFA and output if the string is accepted 
or rejected. Note : NFAs
can progress on multiple paths and you should simulate this effect – if one of 
the paths results in accept
state then the input string is accepted by NFA.

background image

Phase 2: 

To write a program which will construct a DFA from any NFA. You 

will use adjacency
matrix as the representation and use epsilon closures to generate DFA. 
Finally write a program to
simulate the DFA.

Bonus: 

Given an adjacency matrix for a DFA, write a program to produce 

minimal DFA by state
merging.

background image

Document Outline