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.. Nhiệm vụ chủ
Trang 1TRƯỜ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
Trang 2I 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
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 :
Trang 3Bảng 1 Mô tả một số 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.
CONSTANT Các hằng số với các kiểu dữ liệu (số nguyên, số thực,
…) STRING_VALUE Chuỗi hằng (ví dụ “hello world”)
IDENTIFIER Các danh biểu trong chương trình, bao gồm tên biến,
tên hàm (bắt đầu là chữ cái, theo sau là chữ hoặc số)
Trang 4Qua đó, 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
Trang 5Trình phân tích từ vựng Trình phân tích cú pháp Phần còn lại của chuyến trước
Bảng danh biểu
cây phân tích cú pháp token
yêu cầu token
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
IV Bộ công cụ Flex và Bison
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
Trang 61 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
Trang 7FLEX Compiler
C Compiler
lex.out
File mô tả nguồn
lex.l
lex.yy.c
Chương trình nguồn
lex.yy.c
lex.out
chuỗi token
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)
Hình 1 Quá trình phân tích từ vựng
Trang 8unput đư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 :
Trang 9- 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 value *val;
struct operand *op;
struct sym_link *lnk ;
}
%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.
Trang 10- 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> ‘)’
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
Trang 11Mô tả tập luật sinh, luật ngữ nghĩa (*.y)
Bison
Bản mô tả các trạng thái (y.out)
Bộ phân tích cú pháp bằng mã C (yyout.c) Bản định nghĩa các token (yyout.h)
defines
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
và được sử dụng như một thủ tục (yyparse()).
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
Hình 2 Quá trình phân tích cú pháp của BISON
Trang 12luậ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)
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])