Tải bản đầy đủ (.docx) (12 trang)

tiểu luận môn Nguyên lý các ngôn ngữ lập trình. Đề tài tìm hiểu bộ công cụ Flex, Bison, ứng dụng trong phân tích từ Vựng và phân tích cú pháp của một ngôn ngữ nào đó

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (270.91 KB, 12 trang )

TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI
VIỆN ĐÀO TẠO SAU ĐẠI HỌC
=====o0o=====

BÀI TẬP LỚN
MÔN: NGUYÊN LÝ NGÔN NGỮ LẬP TRÌNH

Giáo viên giảng dạy: TS. Nguyễn Hữu Đức

Đề tài: Tìm hiểu bộ công cụ Flex, Bison, ứng dụng trong phân tích từ
Vựng và phân tích cú pháp của một ngôn ngữ nào đó

NHÓM: 10
Lớp: CH2012B

Sinh viên thực hiện:
Nguyễn Thành Đô
Nguyễn Xuân Trường
Trần Văn Trung
Nguyễn Thị Thùy Dương

Hà Nội, tháng 12/2012


I.

GIỚI THIỆU
Ta sẽ phải tốn rất nhiều thời gian để tìm hiểu và xây dựng một trình trình biên dịch hoàn chỉnh một cách

thủ công. Trên thực tế, đã có rất nhiều công cụ có khả năng phát sinh ra bộ phân tích từ vựng và bộ phân tích cú
pháp. Một trong những bộ cổ điển nhất là lex và yacc – được phát minh tại Bell Lab trong thập niên 1970.



• Yacc (Yet Another Compiler Compiler) – công trình của Stephen

C. Johnson được ra đời sớm hơn, có nhiệm vụ phát sinh ra trình phân tích
cú pháp.
• Trong khi đó Mike Lesk và Eric Schmidt đã thiết kế và phát triển lex – bộ phát
sinh trình phân tích từ vựng – để hỗ trợ yacc trong việc xác định các token từ
chuỗi nhập.
FLEX[5] và BISON[5, 7] chính là phiên bản cải tiến của lex và yacc, có khả năng phân tích trên bộ văn
phạm rộng hơn, quản lý bộ nhớ tốt hơn và được sử dụng trên nhiều nền tảng.

II.

PHÂN TÍCH TỪ VỰNG
Phân tích từ vựng là giai đoạn đầu tiên của mọi trình biên dịch. Nhiệm vụ chủ yếu

của nó là đọc các ký hiệu nhập rồi tạo ra một chuỗi các token được sử dụng bởi bộ phân
tích cú pháp. Do đó, bộ phân tích từ vựng được thiết kế như một thủ tục được gọi bởi bộ
phân tích cú pháp, trả về một token khi được gọi. Trên thực tế, ta thường dùng biểu thức
chính quy để mô tả các token.
Ví dụ một văn bản nguồn trong thực tế : “tôi ăn cơm”, khi qua trình phân tích từ
vựng sẽ thành : danh từ - động từ - danh từ. Như vậy, trình biên dịch không quan tâm ngữ
nghĩa của 3 chữ “tôi”, “ăn”, “cơm” theo cách con người (sử dụng tiếng Việt) nghĩ mà nó
sẽ hiểu là xuất hiện có thứ tự 3 đơn vị từ vựng (gọi là token). Một số loại token chính trong
trình biên dịch C :


Token

Bảng



Mô tả
Các hằng số với các kiểu dữ liệu (số nguyên, số thực,

CONSTANT

…)

một

1.
tả
số

Chuỗi hằng (ví dụ “hello world”)

STRING_VALUE

Các danh biểu trong chương trình, bao gồm tên biến,

IDENTIFIER

tên hàm (bắt đầu là chữ cái, theo sau là chữ hoặc số)

IF

Lệnh if

ELSE


Lệnh else

WHILE

Lệnh while

RIGHT_OP

Toán tử shift right

LEFT_OP

Toán tử shift left

INC_OP

Toán tử ++

token của ngôn ngữ C
Ví dụ 1: Với câu nhập
COUNT = COUNT + 1;
Khi qua trình phân tích từ vựng, kết quả nhận được sẽ là chuỗi các token sau :
IDENTIFIER

ASSIGN_OP IDENTIFIER ADD_OP CONSTANT PUNC

Trình phân tích từ vựng giao tiếp trực tiếp với trình phân tích cú pháp qua một giao
thức đơn giản nhưng được định nghĩa khá đầy đủ. Giao thức đó được thể hiện qua hình
vẽ :


Hình 1 .Giao thức liên hệ của bộ phân tích từ vựng.


Qua đó, trình phân tích từ vựng sẽ nhận những định nghĩa về token từ trình phân
tích cú pháp và sẽ trả về token phù hợp.
Chính sự giao tiếp đơn giản này đã khiến cho bộ phân tích từ vựng tỏ ra khá độc
lập so với các phần còn lại của trình biên dịch. Theo giao thức đã trình bày ở Hình , trình
phân tích từ vựng chỉ liên hệ trực tiếp với trình phân tích cú pháp trong vai trò như một
thủ tục. Do đó, một sự thay đổi dù lớn hay nhỏ ở trình phân tích từ vựng cũng không gây
ảnh hưởng đến hoạt động chung của trình biên dịch.
Tuy nhiên, có một số trường hợp mà trình phân tích từ vựng, trình phân tích cú
pháp và bảng danh biểu cần có sự liên hệ mật thiết với nhau để xử lý.
Ví dụ 2: Khai báo kiễu dữ liệu do người dùng tự định nghĩa
typedef int Dummy;
Hoặc
struct Dummy
{
int first, second;
};
Từ cấu trúc khai báo trở đi, Dummy khi được sử dụng sẽ không được hiểu là một danh
biểu nữa mà phải là một từ dành riêng, một kiểu dữ liệu do người dùng định nghĩa. Vì vậy,
bộ phân tích từ vựng sẽ trả về một token đặc biệt khác để bộ phân tích cú pháp có thể
nhận dạng Dummy như một kiểu dữ liệu.
Một công việc nữa của bộ phân tích từ vựng là xác định loại hằng số và chuyển
sang dạng lưu trữ thích hợp (từ dạng dữ liệu nhập là chuỗi).
Ví dụ 3: Với các biễu diễn:
0b1010

: biểu diễn nhị phân của số 10


012

: biểu diễn bát phân của số 10

10

: biểu diễn thập phân của số 10

0x10

: biểu diễn thập lục phân của số 10

thì ta đều phải chuyển sang dạng lưu trữ với giá trị hằng số thích hợp.


III. PHÂN TÍCH CÚ PHÁP
Mỗi ngôn ngữ lập trình đều có các quy tắc diễn tả cấu trúc cú pháp của các chương
trình có định dạng đúng. Các cấu trúc cú pháp này được mô tả bởi văn phạm phi ngữ
cảnh (Context Free Gramma – CFG).
Trình phân tích cú pháp (parser) nhận chuỗi các token từ trình phân tích từ vựng
(Hình 2 ), và xác định rằng chuỗi này có hợp lệ hay không bằng cách tạo ra cây phân tích
cú pháp từ văn phạm của ngôn ngữ nguồn.
Có 2 phương pháp chính được dùng để phân tích cú pháp theo tập văn phạm đã
được định nghĩa:

• Phân tích cú pháp từ trên xuống (Top-Down Parsing) hay

phân tích cú pháp dự đoán (Predictive Parser): ta sẽ cố gắng
tìm kiếm một chuỗi dẫn xuất trái nhất cho chuỗi nhập bằng

cách xây dựng cây phân tích cú pháp bắt đầu từ nút gốc.
• Phân tích cú pháp từ dưới lên (Bottom-Up Parsing): bộ phân
tích cú pháp sẽ bắt đầu từ chuỗi token và cố gắng tìm kiếm
luật sinh thích hợp để có thể dẫn về ký tự bắt đầu của văn
phạm.

ngôn ngữ nguồn
token
Trình phân tích từ vựng

cây phân tích cú pháp
Trình phân tích cú pháp

biểu diễn trung gian
Phần còn lại của chuyến trước

yêu cầu token

Bảng danh biểu

Hình 2. Vị trí của trình phân tích cú pháp trong chuyến trước của trình biên dịch
IV.

Bộ công cụ Flex và Bison


1. Bộ phát sinh trình phân tích từ vựng FLEX
a.

Cấu trúc.

Cấu trúc của một chương trình FLEX :
Phần khai báo (định nghĩa, khai báo biến, prototype)
%%
Biểu thức chính quy do người dùng định nghĩa nhằm xác định các token.
%%
Các hàm hỗ trợ
Phần khai báo chủ yếu là các phần định nghĩa cái biến, hàm và khởi tạo mà ta sẽ đưa trực tiếp vào

chương trình (mã C). Phần mã C được giới hạn trong giữa ký hiệu “%{“ và “%}”. FLEX sẽ copy toàn bộ những gì
giữa ký hiệu “%{“ và “%}” trực tiếp vào file Lexer.c sau khi chạy FLEX.
Phần thứ hai là các biểu thức chính quy do người dùng định nghĩa hay các luật nhằm xác định loại token
từ những ký tự nhập vào. Những luật trong phần này luôn bao gồm 2 phần: biểu thức chính quy và phương
thức thực hiện khi chuỗi nhập phù hợp với mẫu được định nghĩa trong biểu thức chính quy. Phương thức thực
hiện được xác định nhờ cặp ngoặc “{“ và “}”.

Ví dụ 1: Biểu thức chính quy và phương thức thực hiện khi xác định số thập lục.
0[xX][a-fA-F0-9]+

{ return CONSTANT; }

Phần cuối cùng là các hàm hỗ trợ cho quá trình phân tích từ vựng được viết bằng mã C và sẽ được đưa
trực tiếp vào chương trình.

b.

Quy trình vận hành.
FLEX sẽ nhận định nghĩa các token của người dùng bằng biểu thức chính quy, từ đó

sẽ biên dịch (bằng trình biên dịch của FLEX) sang ngôn ngữ C để có thể chạy cùng chương
trình.



File mô tả nguồn
lex.l

FLEX Compiler

lex.yy.c

C Compiler

lex.out

lex.out

chuỗi token

Chương trình nguồn

lex.yy.c

Hình 1. Quá trình phân tích từ vựng

Sau khi được phát sinh từ tập biểu thức chính quy, bộ phân tích từ vựng sẽ được định nghĩa như một
hàm trả ra token tiếp theo (TOKEN yylex();). Khi yylex() được gọi, nó sẽ phân tích chương trình nguồn
thành từng đơn vị từ vựng và tìm biểu thức chính quy phù hợp với những đơn vị từ vựng đó. Khi có sự đối sánh
xuất hiện, yylex() sẽ thực hiện phần mã C tương ứng với biểu thức chính quy được chọn.

c. Một số hàm hỗ trợ.
int main() :

Thường được ngầm hiểu là không có và người dùng nên tự định nghĩa hàm main riêng cho mình.
char *yytext
Chuỗi giữ lexeme hiện tại, kết thúc bằng ký tự ‘\0’.
int yyleng
Chiều dài của chuỗi yytext.
int yylineno
Dòng chứa lexeme mà ta đang xét. Nếu lexeme có nhiều dòng thì yylineno chính là dòng cuối cùng của
lexeme.
int input()
Đọc, trả về ký tự nhập tiếp theo ký tự cuối cùng trong lexeme. Ký tự này được thêm vào cuối của
lexeme (yytext). yyleng sẽ tự tăng lên 1 đơn vị. Hàm trả về 0 khi kết thúc file.
void unput(int c)


unput đưa ký tự c ngược trở lại chuỗi nhập. Khi đó lexeme hiện tại sẽ giảm đi 1 ký tự, yyleng cũng
sẽ giảm 1 đơn vị. Khi gọi input() tiếp theo thì ký tự c sẽ được trả về.
void yyless (int n)
Tương tự như unput nhưng ở đây sẽ đưa n ký tự trở lại chuỗi nhập. n không được vượt quá yyleng.
void yymore()
Biểu thức chính quy sẽ bỏ qua trường hợp đối sánh hiện tại mà tiếp tục xét tiếp. Ứng dụng yymore
trong trường hợp xác định kiểu dữ liệu chuỗi constant mà có chứa dấu nháy kép, như chuỗi : “string with
a \” in it”. Khi đó, biểu thức chính quy và phương thức thực hiện sẽ là :
\”[^\”]\”
{
if (yytext[yyleng – 2] == ‘\\’)
yymore();
else
return STRING;
}
Trong đó, biểu thức chính quy \”[^\”]\” nhận các chuỗi bắt đầu bằng ký tự “, kết

thúc bằng ký tự “ nhưng chưa xét tới trường hợp chuỗi có thể chứa ký tự “ ở giữa như
“string with a \” in it”. Rõ ràng đây là chuỗi hợp lệ và ta nhận dạng ký tự ” ở giữa
bằng ký tự \. Do đó, khi yytext[yyleng – 2] == ‘\\’, tức là ký tự trước ký tự nháy kép
là ký tự \ thì ta không dừng lại mà tiếp tục xét chuỗi nhập cho đến khi gặp ký báo hiệu kết
thúc chuỗi thực sự (yymore())
ECHO
Xuất lexeme hiện tại ra màn hình console (stdout).

2.

Bộ phát sinh trình phân tích cú pháp BISON
a. Cấu trúc.

Tương tự như FLEX, Bison nhận input là một file bao gồm các đặc
tả của một ngôn ngữ. Từ đó, Bison biên dịch ra bộ phân tích cú pháp
bằng mã C để chạy cùng với chương trình. Cấu trúc của file đặc tả
ngôn ngữ cũng gồm 3 phần :


- Phần khai báo:
a. Khai báo C thông thường (biến, prototype hàm, cấu trúc, đĩnh

nghĩa...), được giới hạn trong %{ và %} như FLEX.
b. Khai báo kiểu dữ liệu của các thuộc tính tổng hợp.
c. Khai báo các token và các thuộc tính kết hợp với token (nếu
có).
d. Khai báo các thuộc tính kết hợp với các ký hiệu không kết thúc.
Ví dụ 1: Văn phạm thuộc tính của một số ký hiệu không kết thúc trong file input
của Bison.
%union {

struct symbol
*sym;
struct value *val;
struct operand *op;
struct sym_link
*lnk ;
char
yychar[255];
int
yyint;
}
%token <yychar> ID TYPE_NAME
%type <op> stmt_list stmt
%type <yyint> unary_op assignment_op
%type <sym> id
Khi đó, các ký tự không kết thúc như stmt, stmt_list… sẽ có những thuộc
tính như một biến struct operand*, các ký tự không kết thúc unary_op,
assignment_op sẽ có thuộc tính là một biến kiểu liệu cơ sở int . Ngoài ra, các
token (ký tự kết thúc) như ID, TYPE_NAME cũng có thể được khai báo và kèm theo
thuộc tính char[] để biểu diễn kiểu dữ liệu dạng chuỗi.
-

Phần mô tả tập luật sinh hình thành nên ngôn ngữ:

Trong luật sinh, các ký tự nằm trong cặp dấu nháy đơn. 'c' là
một ký hiệu kết thúc c, một chuỗi chữ cái và chữ số không nằm trong
cặp dấu nháy đơn và không được khai báo là token sẽ là ký hiệu chưa
kết thúc.



Hành vi ngữ nghĩa của Bison là một chuỗi các lệnh mã C với:



$$ Giá trị thuộc tính kết hợp với ký hiệu chưa kết thúc bên vế trái

của luật sinh.
• $n Giá trị thuộc tính kết hợp với ký hiệu văn phạm thứ n (ký hiệu kết
thúc hoặc chưa kết thúc) của vế phải.
-

Các hàm hỗ trợ.

Ngôn ngữ chủ yếu để mô tả các luật hình thành nên parser là
CFG (văn phạm phi ngữ cảnh). Và dạng tiêu chuẩn để mô tả một CFG
mà BISON áp dụng là BNF (Backus-Naur Form) [5]– từng được dùng để
miêu tả ngôn ngữ Algol 60.
Ví dụ 3: Mô tả dạng BNF cho ngôn ngữ xây dựng biểu thức đơn giản
<s> ::= <e>
<e> ::= <e> ‘+’<t>
|
<t>
<t> ::= <t> ‘*’ <f>
|
<f>
<f> ::= ‘(‘ <e> ‘)’
|
NUM
Mỗi dòng là một luật sinh chỉ ra cách hình thành nên một
nhánh của cây phân tích cú pháp. Bison đã đơn giản hóa BNF để

dễ sử dụng hơn.
Ví dụ 2: Mô tả của Bison cho ngôn ngữ đưa ra ở Ví dụ 3
s
e
t
f
|

:
:
|
:
|
:
NUM

e
e ‘+’ t
t
t ‘*’ f
f
‘(‘ e’)’

Văn phạm thuộc tính trong Bison được thể hiện ở phần khai báo. Ta có thể gán cho
các ký hiệu kết thúc hoặc các ký hiệu không kết thúc các thuộc tính là các kiểu dữ liệu cơ
sở, hoặc ngay cả những cấu trúc tự định nghĩa


b.


Quy trình vận hành.
Bison nhận tập tin mô tả ngữ pháp (luật sinh, luật ngữ nghĩa) để
qua đó phát sinh ra bộ phân tích cú pháp (viết bằng ngôn ngữ C). Bộ
phân tích cú pháp này cũng sẽ được biên dịch chung với chương trình

--defines

và được sử dụng như một thủ tục (yyparse()).

Mô tả tập luật sinh, luật ngữ nghĩa (*.y)

--verbose Bản mô tả các trạng thái (y.out)

Bison

Bản định nghĩa các token
Bộ phân
(yyout.h)
tích cú pháp bằng mã C (yyout.c)

Hình 2. Quá trình phân tích cú pháp của BISON

Khi yyparse() được gọi, nó sẽ dựa vào trạng thái hiện tại và
bảng ACTION-GOTO để chọn ra hành vi thích hợp, đồng thời, thực hiện
luật ngữ nghĩa ứng với văn phạm sau khi rút gọn. Ngoài ra, BISON
cũng có thể tạo ra file định nghĩa các token (đã khai báo) được sử
dụng chung trong trình phân tích từ vựng cũng như trình phân tích cú
pháp.
Phương pháp phân tích dưới lên sử dụng một Stack để lưu trữ thông tin về cây con
đã được phân tích. Chúng ta có thể mở rộng Stack này để lưu trữ giá trị thuộc tính tổng

hợp. Stack được cài đặt bởi một cặp mảng trạng thái và giá trị.
Ví dụ 5: Hoạt động của stack lưu trữ trạng thái và giá trị (val)
stmt_list1 : stmt_list2

stmt

Trước khi stmt_list2 stmt được rút gọn thành stmt_list thì:


stack[top].val
= stmt.op
stack[top - 1].val
= stmt_list2.op
Sau khi rút gọn, top sẽ giảm đi 1 đơn vị ( top = top – 1), và

stack[top].val
= stmt_list1.op
Với cách xử lý này, Bison sử dụng thuộc tính của các ký hiệu thông qua stack value.
Khi muốn sử dụng thuộc tính op của stmt_list2 ta chỉ cần gọi $1, tương tự gọi $2
đối với stmt.Khi biên dịch qua ngôn ngữ C, Bison sẽ đối xử với $1 như yyvsp[1].op, $2 như yyvsp[0].op. Sau khi rút gọn, con trỏ yyvsp sẽ được cập nhật
để đẩy stmt_list1.op vào stack và nằm trên đỉnh stack (yyvsp[0]).



×