24/05/2018, 23:50

Phân tích từ vựng (Lexical Analysis)

Bây giờ chúng ta thêm vào phần trước trình biên dịch một bộ phân tích từ vựng để đọc và biến đổi dòng nhập thành một chuỗi các từ tố (token) mà bộ phân tích cú pháp có thể sử dụng được. Nhắc lại rằng một chuỗi các ký tự hợp thành một token ...

Bây giờ chúng ta thêm vào phần trước trình biên dịch một bộ phân tích từ vựng để đọc và biến đổi dòng nhập thành một chuỗi các từ tố (token) mà bộ phân tích cú pháp có thể sử dụng được. Nhắc lại rằng một chuỗi các ký tự hợp thành một token gọi là trị từ vựng (lexeme) của token đó.

Trước hết ta trình bày một số chức năng cần thiết của bộ phân tích từ vựng.

Loại bỏ các khoảng trắng và các dòng chú thích

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 (như khoảng trắng bao gồm ký tự trống (blanks), ký tự tabs, ký tự newlines) hoặc các dòng chú thích (comment) 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ờ xem xét đến chúng nữa. Chọn lựa cách sửa đổi văn phạm để đưa cả khoảng trắng vào trong cú pháp thì hầu như rất khó cài đặt.

Xử lý các hằng

Bất cứ khi nào một ký tự số xuất hiện trong biểu thức thì nó được xem như là một hằng số. Bởi vì một hằng số nguyên là một dãy các chữ số nên nó có thể được cho bởi luật sinh văn phạm hoặc tạo ra một token cho hằng số đó. Bộ phân tích từ vựng có nhiệm vụ 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 từ vựng sẽ gửi num cho bộ phân tích cú pháp. 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. Về mặt logic, bộ phân tích từ vựng sẽ chuyển cả token và các thuộc tính cho bộ phân tích cú pháp. Nếu ta viết một token và thuộc tính thành một bộ nằm giữa < > thì dòng nhập 31 + 28 + 59 sẽ được chuyển thành một dãy các bộ :

<num, 31>, < +, >, <num, 28>, < +, >, <num, 59>.

Bộ <+, > cho thấy thuộc tính của + không có vai trò gì trong khi phân tích cú pháp nhưng nó cần thiết dùng đến trong quá trình dịch.

Nhận dạng các danh biểu và từ khóa

Ngôn ngữ dùng các danh biểu (identifier) như là tên biến, mảng, hàm và văn phạm xử lý các danh biểu này như là một token. Người ta dùng token id cho các danh biểu khác nhau do đó nếu ta có dòng nhập count = count + increment; thì bộ phân tích từ vựng sẽ chuyển cho bộ phân tích cú pháp chuỗi token: id = id + id (cần phân biệt token và trị từ vựng lexeme của nó: token id nhưng trị từ vựng (lexeme) có thể là count hoặc increment). Khi một lexeme thể hiện cho một danh biểu được tìm thấy trong dòng nhập cần phải có một cơ chế để xác định xem lexeme này đã được thấy trước đó chưa? Công việc này được thực hiện nhờ sự lưu trữ trợ giúp của bảng ký hiệu (symbol table) đã nêu ở chương trước. Trị từ vựng được lưu trong bảng ký hiệu và một con trỏ chỉ đến mục ghi trong bảng trở thành một thuộc tính của token id.

Nhiều ngôn ngữ cũng sử dụng các chuỗi ký tự cố định như begin, end, if, ... để xác định một số kết cấu. Các chuỗi ký tự này được gọi là từ khóa (keyword). Các từ khóa cũng thỏa mãn qui luật hình thành danh biểu, do vậy cần qui ước rằng một chuỗi ký tự được xác định là một danh biểu khi nó không phải là từ khóa.

Một vấn đề nữa cần quan tâm là vấn đề tách ra một token trong trường hợp một ký tự có thể xuất hiện trong trị từ vựng của nhiều token. Ví dụ một số các token là các toán tử quan hệ trong Pascal như : <, < =, < >.

Giao diện của bộ phân tích từ vựng

Bộ phân tích từ vựng được đặt xen giữa dòng nhập và bộ phân tích cú pháp nên giao diện với hai bộ này như sau:

Bộ phân tích từ vựng đọc từng ký tự từ dòng nhập, nhóm chúng lại thành các trị từ vựng và chuyển các token xác định bởi trị từ vựng này cùng với các thuộc tính của nó đến những giai đoạn sau của trình biên dịch. Trong một vài tình huống, bộ phân tích từ vựng phải đọc vượt trước một số ký tự mới xác định được một token để chuyển cho bộ phân tích cú pháp. Ví dụ, trong Pascal khi gặp ký tự >, phải đọc thêm một ký tự sau đó nữa; nếu ký tự sau là = thì token được xác định là “lớn hơn hoặc bằng”, ngược lại thì token là “lớn hơn” và do đó một ký tự đã bị đọc quá. Trong trường hợp đó thì ký tự đọc quá này phải được đẩy trả về (push back) cho dòng nhập vì nó có thể là ký tự bắt đầu cho một trị từ vựng mới.

Bộ phân tích từ vựng và bộ phân tích cú pháp tạo thành một cặp "nhà sản xuất - người tiêu dùng" (producer - consumer). Bộ phân tích từ vựng sản sinh ra các token và bộ phân tích cú pháp tiêu thụ chúng. Các token được sản xuất bởi bộ phân tích từ vựng sẽ được lưu trong một bộ đệm (buffer) cho đến khi chúng được tiêu thụ bởi bộ phân tích cú pháp. Bộ phân tích từ vựng không thể hoạt động tiếp nếu buffer bị đầy và bộ phân tích cú pháp không thể hoạt động nữa nếu buffer rỗng. Thông thường, buffer chỉ lưu giữ một token. Ðể cài đặt tương tác dễ dàng, người ta tạo ra một thủ tục phân tích từ vựng được gọi từ trong thủ tục phân tích cú pháp, mỗi lần gọi trả về một token.

Việc đọc và quay lui ký tự cũng được cài đặt bằng cách dùng một bộ đệm nhập. Một khối các ký tự được đọc vào trong buffer nhập tại một thời điểm nào đó, một con trỏ sẽ giữ vị trí đã được phân tích. Quay lui ký tự được thực hiện bằng cách lùi con trỏ trở về. Các ký tự trong dòng nhập cũng có thể cần được lưu lại cho công việc ghi nhận lỗi bởi vì cần phải chỉ ra vị trí lỗi trong đoạn chương trình.

Ðể tránh việc phải quay lui, một số trình biên dịch sử dụng cơ chế đọc trước một ký tự rồi mới gọi đến bộ phân tích từ vựng. Bộ phân tích từ vựng sẽ đọc tiếp các ký tự cho đến khi đọc tới ký tự mở đầu cho một token khác. Trị từ vựng của token trước đó sẽ bao gồm các ký tự từ ký tự đọc trước đến ký tự vừa ngay ký tự vừa đọc được. Ký tự vừa đọc được sẽ là ký tự mở đầu cho trị từ vựng của token sau. Với cơ chế này thì mỗi ký tự chỉ được đọc duy nhất một lần.

Một bộ phân tích từ vựng

Bây giờ chúng ta xây dựng một bộ phân tích từ vựng cho chương trình dịch các biểu thức số học. Hình sau đây gợi ý một cách cài đặt giao diện của bộ phân tích từ vựng được viết bằng C dưới dạng hàm lexan. Lexan đọc và đẩy các ký tự trong input trở về bằng cách gọi thủ tục getchar va ungetc.

Nếu ngôn ngữ cài đặt không cho phép trả về các cấu trúc dữ liệu từ các hàm thì token và các thuộc tính của nó phải được truyền riêng rẽ. Hàm lexan trả về một số nguyên mã hóa cho một token. Token cho một ký tự có thể là một số nguyên quy ước được dùng để mã hóa cho ký tự đó. Một token như num có thể được mã hóa bằng một số nguyên lớn hơn mọi số nguyên được dùng để mã hóa cho các ký tự, chẳng hạn là 256. Ðể dễ dàng thay đổi cách mã hóa, chúng ta dùng một hằng tượng trưng NUM thay cho số nguyên mã hóa của num. Hàm lexan trả về NUM khi một dãy chữ số được tìm thấy trong input. Biến toàn cục tokenval được đặt là giá trị của chuỗi số này.

Cài đặt của hàm lexan như sau :

# include<stdio.h>

# include<ctype.h>

int lineno = 1;

int tokenval = NONE;

int lexan ( )

{

int t;

while(1) {

t = getchar( );

if ( t = = ‘ ‘ || t = = ‘ ‘) ; /* loại bỏ blank và tab */

else if (t = = ‘ ’)

lineno = lineno + 1;

else if ( isdigit (t) ) {

tokenval = t - ‘0’;

t = getchar( );

while ( isdigit (t) ) {

tokenval = tokenval * 10 + t - ‘0’;

t = getchar( );

}

ungetc (t, stdin);

return NUM;

}

else {

tokenval = NONE;

return t;

}

} /* while */

} /* lexan */

0