Ví dụ trong quá trình phân tích từ vựng, các chuỗi ký tự tạo ra một token trị từ vựng của token sẽ được lưu vào một mục ghi trong bảng danh biểu.. Các giai đoạn sau đó có thể bổ sung thê[r]
(1)CHƯƠNG I GIỚI THIỆU VỀ SỰ BIÊN DỊCH Nội dung chính: Để máy tính có thể hiểu và thực thi chương trình viết ngôn ngữ cấp cao, ta cần phải có trình biên dịch thực việc chuyển đổi chương trình đó sang chương trình dạng ngôn ngữ đích Chương này trình bày cách tổng quan cấu trúc trình biên dịch và mối liên hệ nó với các thành phần khác - “họ hàng” nó - tiền xử lý, tải và soạn thảo liên kết,v.v Cấu trúc trình biên dịch mô tả chương là cấu trúc mức quan niệm bao gồm các giai đoạn: 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ã và Sinh mã đích Mục tiêu cần đạt: Sau học xong chương này, sinh viên phải nắm cách tổng quan nhiệm vụ các thành phần trình biên dịch, mối liên hệ các thành phần đó và môi trường nơi trình biên dịch thực công việc nó Tài liệu tham khảo: [1] Trình Biên Dịch - Phan Thị Tươi (Trường Ðại học kỹ thuật Tp.HCM) - NXB Giáo dục, 1998 [2] Compilers : Principles, Technique and Tools - Alfred V.Aho, Jeffrey D.Ullman - Addison - Wesley Publishing Company, 1986 [3] Compiler Design – Reinhard Wilhelm, Dieter Maurer - Addison - Wesley Publishing Company, 1996 I TRÌNH BIÊN DỊCH Nói cách đơn giản, trình biên dịch là chương trình làm nhiệm vụ đọc chương trình viết ngôn ngữ - ngôn ngữ nguồn (source language) - dịch nó thành chương trình tương đương ngôn ngữ khác - ngôn ngữ đích (target languague) Một phần quan trọng quá trình dịch là ghi nhận lại các lỗi có chương trình nguồn để thông báo lại cho người viết chương trình Chương trình nguồn Trình biên dịch Chương trình đích Hình 1.1 - Một trình biên dịch Mô hình phân tích - tổng hợp trình biên dịch Chương trình dịch thường bao gồm hai quá trình : phân tích và tổng hợp - Phân tích → đặc tả trung gian - Tổng hợp → chương trình đích (2) Chương trình nguồn Phán Phân tích Phán têch têch Tổng hợp Đặc tả trung gian Chương trình đích Hình 1.2 - Mô hình phân tích - tổng hợp Trong quá trình phân tích chương trình nguồn phân rã thành cấu trúc phân cấp, thường là dạng cây - cây cú pháp (syntax tree) mà đó có nút là toán tử và các nhánh là các toán hạng Ví dụ 1.1: Cây cú pháp cho câu lệnh gán position := initial + rate * 60 := position + initial * rate 60 Môi trường trình biên dịch Ngoài trình biên dịch, chúng ta có thể cần dùng nhiều chương trình khác để tạo chương trình đích có thể thực thi (executable) Các chương trình đó gồm: Bộ tiền xử lý, Trình dịch hợp ngữ, Bộ tải và soạn thảo liên kết Một chương trình nguồn có thể phân thành các module và lưu các tập tin riêng rẻ Công việc tập hợp lại các tập tin này thường giao cho chương trình riêng biệt gọi là tiền xử lý (preprocessor) Bộ tiền xử lý có thể "bung" các ký hiệu tắt gọi là các macro thành các câu lệnh ngôn ngữ nguồn Ngoài ra, chương trình đích tạo trình biên dịch có thể cần phải xử lý thêm trước chúng có thể chạy Thông thường, trình biên dịch tạo mã lệnh hợp ngữ (assembly code) để trình dịch hợp ngữ (assembler) dịch thành dạng mã máy liên kết với số thủ tục thư viện hệ thống thành các mã thực thi trên máy Hình sau trình bày quá trình biên dịch điển hình : (3) Chương trình nguồn khung Bộ tiền xử lý Chương trình nguồn Trình biên dịch Chương trình đích hợp ngữ Trình dịch hợp ngữ Mã máy khả tái định vị Trình tải / Liên kết Thư viện, Tập tin đối tượng khả tái định vị Mã máy tuyệt đối Hình 1.3 - Một trình xử lý ngôn ngữ điển hình II SỰ PHÂN TÍCH CHƯƠNG TRÌNH NGUỒN Phần này giới thiệu các quá trình phân tích và cách dùng nó thông qua số ngôn ngữ định dạng văn Phân tích từ vựng (Lexical Analysis) Trong trình biên dịch, giai đọan phân tích từ vựng đọc chương trình nguồn từ trái sang phải (quét nguyên liệu - scanning) để tách thành các thẻ từ (token) Ví dụ 1.2: Quá trình phân tích từ vựng cho câu lệnh gán position := initial + rate * 60 tách thành các token sau: Danh biểu position Ký hiệu phép gán := Danh biểu initial (4) Ký hiệu phép cộng (+) Danh biểu rate Ký hiệu phép nhân (*) Số 60 Trong quá trình phân tích từ vựng các khoảng trắng (blank) bị bỏ qua Phân tích cú pháp (Syntax Analysis) Giai đoạn phân tích cú pháp thực công việc nhóm các thẻ từ chương trình nguồn thành các ngữ đoạn văn phạm (grammatical phrase), mà sau đó trình biên dịch tổng hợp thành phẩm Thông thường, các ngữ đoạn văn phạm này biểu diễn dạng cây phân tích cú pháp (parse tree) với : - Ngôn ngữ đặc tả các luật sinh - Phân tích cú pháp dựa vào luật sinh để xây dựng cây phân tích cú pháp Ví dụ 1.3: Giả sử ngôn ngữ đặc tả các luật sinh sau : Stmt → id := expr expr → expr + expr | expr * expr | id | number Với câu nhập: position := initial + rate * 60, cây phân tích cú pháp xây dựng sau : Stmt id expr := position expr + expr id expr expr initial id number rate Hình 1.4 - Một cây phân tích cú pháp 60 Cấu trúc phân cấp chương trình thường diễn tả quy luật đệ qui Ví dụ 1.4: 1) Danh biểu (identifier) là biểu thức (expr) 2) Số (number) là biểu thức 3) Nếu expr1 và expr2 là các biểu thức thì: expr1 + expr2 expr1 * expr2 (expr) (5) là biểu thức Câu lệnh (statement) có thể định nghĩa đệ qui : 1) Nếu id1 là danh biểu và expr2 là biểu thức thì id1 := expr2 là lệnh (stmt) 2) Nếu expr1 là biểu thức và stmt2 là lệnh thì while (expr1) stmt2 if (expr1) then stmt2 là các lệnh Người ta dùng các qui tắc đệ qui trên để đặc tả luật sinh (production) cho ngôn ngữ Sự phân chia quá trình phân tích từ vựng và phân tích cú pháp tuỳ theo công việc thực Phân tích ngữ nghĩa (Semantic Analysis) Giai đoạn phân tích ngữ nghĩa thực việc kiểm tra xem chương trình nguồn có chứa lỗi ngữ nghĩa hay không và tập hợp thông tin kiểu cho giai đoạn sinh mã sau Một phần quan trọng giai đoạn phân tích ngữ nghĩa là kiểm tra kiểu (type checking) và ép chuyển đổi kiểu Ví dụ 1.5: Trong biểu thức position := initial + rate * 60 Các danh biểu (tên biến) khai báo là real, 60 là số integer vì trình biên dịch đổi số nguyên 60 thành số thực 60.0 := + position * initial 60 rate thành := + position * initial rate inttoreal 60.0 Hình 1.5 - Chuyển đổi kiểu trên cây phân tích cú pháp (6) III CÁC GIAI ÐOẠN BIÊN DỊCH Ðể dễ hình dung, trình biên dịch chia thành các giai đoạn, giai đoạn chuyển chương trình nguồn từ dạng biểu diễn này sang dạng biểu diễn khác Một cách phân rã điển hình trình biên dịch trình bày hình sau Chương trình nguồn Phân tích từ vựng Phân tích cú pháp Phân tích ngữ nghĩa Quản lý bảng ký hiệu Xử lý lỗi Sinh mã trung gian Tối ưu mã Sinh mã đích Chương trình đích Hình 1.6 - Các giai đoạn trình biên dịch Việc quản lý bảng ký hiệu và xử lý lỗi thực xuyên suốt qua tất các giai đoạn Quản lý bảng ký hiệu Một nhiệm vụ quan trọng trình biên dịch là ghi lại các định danh sử dụng chương trình nguồn và thu thập các thông tin các thuộc tính khác định danh Những thuộc tính này có thể cung cấp thông tin vị trí lưu trữ cấp phát cho định danh, kiểu và tầm vực định danh, và định danh là tên thủ tục thì thuộc tính là các thông tin số lượng và kiểu các đối số, phương pháp truyền đối số và kiểu trả thủ tục có Bảng ký hiệu (symbol table) là cấu trúc liệu mà phần tử là mẩu tin dùng để lưu trữ định danh, bao gồm các trường lưu giữ ký hiệu và các thuộc tính nó Cấu trúc này cho phép tìm kiếm, truy xuất danh biểu cách nhanh chóng Trong quá trình phân tích từ vựng, danh biểu tìm thấy và nó đưa vào bảng ký hiệu nói chung các thuộc tính nó có thể chưa xác định giai đoạn này (7) Ví dụ 1.6: Chẳng hạn, khai báo Pascal có dạng var position, initial, rate : real thì thuộc tính kiểu real chưa thể xác định các danh biểu xác định và đưa vào bảng ký hiệu Các giai đoạn sau đó phân tích ngữ nghĩa và sinh mã trung gian đư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ã thường đưa các thông tin chi tiết vị trí lưu trữ dành cho định danh và sử dụng chúng cần thiết Bảng ký hiệu position initial rate Xử lý lỗi Mỗi giai đoạn có thể gặp nhiều lỗi, nhiên sau phát lỗi, tùy thuộc vào trình biên dịch mà có các cách xử lý lỗi khác nhau, chẳng hạn : - Dừng và thông báo lỗi gặp lỗi đầ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 thường gặp lỗi các ký tự không thể ghép thành token Giai đoạn phân tích cú pháp gặp lỗi các token không thể kết hợp với theo đúng cấu trúc ngôn ngữ Giai đoạn phân tích ngữ nghĩa báo lỗi các toán hạng có kiểu không đúng yêu cầu phép toán hay các kết cấu không có nghĩa thao tác thực mặc dù chúng hoàn toàn đúng mặt cú pháp Các giai đoạn phân tích Giai đoạn phân tích từ vựng: Ðọc ký tự gộp lại thành token, token có thể là danh biểu, từ khóa, ký hiệu, Chuỗi ký tự tạo thành token gọi là lexeme - trị từ vựng token đó Ví dụ 1.7: Danh biểu rate có token id, trị từ vựng là rate và danh biểu này đưa vào bảng ký hiệu nó chưa có đó Giai đoạn phân tích cú pháp và phân tích ngữ nghĩa: Xây dựng cấu trúc phân cấp cho chuỗi các token, biểu diễn cây cú pháp và kiểm tra ngôn ngữ theo cú pháp Ví dụ 1.8: Cây cú pháp và cấu trúc lưu trữ cho biểu thức position := initial + rate * 60 (8) := := + id1 id * id2 + id id3 60 id * num 60 Hình 1.7 - Cây cú pháp và cấu trúc lưu trữ Sinh mã trung gian Sau phân tích ngữ nghĩa, số trình biên dịch tạo dạng biểu diễn trung gian chương trình nguồn Chúng ta có thể xem dạng biểu diễn này chương trình dành cho máy trừu tượng Chúng có đặc tính quan trọng : dễ sinh và dễ dịch thành chương trình đích Dạng biểu diễn trung gian có nhiều loại Thông thường, người ta sử dụng dạng "mã máy địa chỉ" (three-address code), tương tự dạng hợp ngữ cho máy mà đó vị trí nhớ có thể đóng vai trò ghi Mã máy địa là dãy các lệnh liên tiếp, lệnh có thể có tối đa đối số Ví dụ 1.9: t1 := inttoreal (60) t2 := id3 * t1 t3 := id2 + t2 id1 := t3 Dạng trung gian này có số tính chất: - Mỗi lệnh chứa nhiều toán tử Do đó tạo lệnh này, trình biên dịch phải xác định thứ tự các phép toán, ví dụ * thực trước + - Trình biên dịch phải tạo biến tạm để lưu trữ giá trị tính toán cho lệnh - Một số lệnh có ít toán hạng Tối ưu mã Giai đoạn tối ưu mã cố gắng cải thiện mã trung gian để có thể có mã máy thực nhanh Một số phương pháp tối ưu hóa hoàn toàn bình thường Ví dụ 1.10: Mã trung gian nêu trên có thể tối ưu thành: t1 := id3 * 60.0 id1 := id2 + t1 Ðể tối ưu mã, ta thấy việc đổi số nguyên 60 thành số thực 60.0 có thể thực lần vào lúc biên dịch, vì có thể loại bỏ phép toán inttoreal Ngoài ra, t3 dùng lần để chuyển giá trị cho id1 nên có thể giảm bớt (9) Có khác biệt lớn khối lượng tối ưu hoá mã các trình biên dịch khác thực Trong trình biên dịch gọi là "trình biên dịch chuyên tối ưu", phần thời gian đáng kể dành cho giai đoạn này Tuy nhiên, có phương pháp tối ưu giúp giảm đáng kể thời gian chạy chương trình nguồn mà không làm chậm thời gian dịch quá nhiều Sinh mã Giai đoạn cuối cùng biên dịch là sinh mã đích, thường là mã máy mã hợp ngữ Các vị trí vùng nhớ chọn lựa cho biến chương trình sử dụng Sau đó, các thị trung gian dịch thành chuỗi các thị mã máy Vấn đề định là việc gán các biến cho các ghi Ví dụ 1.11: Sử dụng các ghi (chẳng hạn R1, R2) cho việc sinh mã đích sau: MOVF id3, R2 MULF #60.0, R2 MOVF id2, R1 ADDF R2, R1 MOVF R1, id1 Toán hạng thứ và thứ hai thị tương ứng mô tả đối tượng nguồn và đích Chữ F thị cho biết thị xử lý các số chấm động (floating_point) Dấu # để xác định số 60.0 xem số Ví dụ Xem hình vẽ 1.8 (trang 10) mô tả các giai đoạn biên dịch cho biểu thức: position := initial + rate * 60 IV NHÓM CÁC GIAI ÐOẠN Các giai đoạn mà chúng ta đề cập trên là thực theo trình tự logic trình biên dịch Nhưng thực tế, cài đặt các hoạt động nhiều giai đoạn có thể nhóm lại với Thông thường chúng nhóm thành hai nhóm bản, gọi là: kỳ đầu (Front end) và kỳ sau (Back end) Kỳ đầu (Front End) Kỳ đầu bao gồm các giai đoạn các phần giai đoạn phụ thuộc nhiều vào ngôn ngữ nguồn và độc lập với máy đích Thông thường, nó chứa các giai đoạn sau: Phân tích từ vựng, Phân tích cú pháp, Phân tích ngữ nghĩa và Sinh mã trung gian Một phần công việc tối ưu hóa mã thực kỳ đầu Front end bao gồm việc xử lý lỗi xuất giai đoạn Kỳ sau (Back End) Kỳ sau bao gồm số phần nào đó trình biên dịch phụ thuộc vào máy đích và nói chung các phần này không phụ thuộc vào ngôn ngữ nguồn mà là ngôn ngữ trung gian Trong kỳ sau, chúng ta gặp số vấn đề tối ưu hoá mã, phát sinh mã đích cùng với việc xử lý lỗi và các thao tác trên bảng ký hiệu (10) position := initial + rate * 60 Phân tích từ vựng id1 := id2 + id3 * 60 := Phân tích cú pháp + id1 id2 * 60 id3 := Phân tích ngữ nghĩa + id1 * id2 id3 Sinh mã trung gian Tối ưu hóa mã Phát sinh mã đích inttoreal t1 := inttoreal (60) t2:= id3 * t1 t3 := id2 + t2 id1 := t3 60.0 t1 := id3 * 60.0 id1 := id2 + t1 MOVF MULF MOVF ADDF MOVF id3, R2 #60.0, R2 id2, R1 R2, R1 R1, id1 Hình 1.8 - Minh họa các giai đoạn biên dịch biểu thức 10 (11) CHƯƠNG II MỘT TRÌNH BIÊN DỊCH ÐƠN GIẢN Nội dung chính: Chương này giới thiệu trình biên dịch cho các biểu thức số học đơn giản (trình biên dịch đơn giản) gồm hai kỳ: Kỳ đầu (Front end) và kỳ sau (Back end) Nội dung chính chương tập trung vào kỳ đầu gồm các giai đoạn: Phân tích từ vựng, phân tích cú pháp và sinh mã trung gian với mục đích chuyển biểu thức số học đơn giản từ dạng trung tố sang hậu tố Kỳ sau chuyển đổi biểu thức dạng hậu tố sang mã máy ảo kiểu stack, sau đó thực thi đoạn mã đó trên máy ảo kiểu stack kết tính toán cuối cùng Mục tiêu cần đạt: Sau học xong chương này, sinh viên phải nắm được: • Các thành phần cấu tạo nên trình biên dịch đơn giản • Hoạt động và cách cài đặt các giai đoạn kỳ trước trình biên dịch đơn giản • Cách sử dụng máy trừu tượng kiểu stack để chuyển đổi các biểu thức hậu tố sang mã máy ảo và cách thực thi các đoạn mã ảo này để có kết cuối cùng Kiến thức Để tiếp nhận các nội dung trình bày chương 2, sinh viên phải: • Biết ngôn ngữ lập trình nào đó: C, Pascal, v.v để hiểu cách cài đặt trình biên dịch • Có kiến thức cấu trúc liệu để hiểu cách tổ chức liệu thực cài đặt Tài liệu tham khảo: [1] Trình Biên Dịch - Phan Thị Tươi (Trường Ðại học kỹ thuật Tp.HCM) - NXB Giáo dục, 1998 [2] Compilers : Principles, Technique and Tools - Alfred V.Aho, Jeffrey D.Ullman - Addison - Wesley Publishing Company, 1986 I ÐỊNH NGHĨA CÚ PHÁP Văn phạm phi ngữ cảnh Ðể xác định cú pháp ngôn ngữ, người ta dùng văn phạm phi ngữ cảnh CFG (Context Free Grammar) hay còn gọi là văn phạm BNF (Backers Naur Form) Văn phạm phi ngữ cảnh bao gồm bốn thành phần: Một tập hợp các token - các ký hiệu kết thúc (terminal symbols) Ví dụ: Các từ khóa, các chuỗi, dấu ngoặc đơn, 11 (12) Một tập hợp các ký hiệu chưa kết thúc (nonterminal symbols), còn gọi là các biến (variables) Ví dụ: Câu lệnh, biểu thức, Một tập hợp các luật sinh (productions) đó luật sinh bao gồm ký hiệu chưa kết thúc - gọi là vế trái, mũi tên và chuỗi các token và / các ký hiệu chưa kết thúc gọi là vế phải Một các ký hiệu chưa kết thúc dùng làm ký hiệu bắt đầu văn phạm Chúng ta qui ước: - Mô tả văn phạm cách liệt kê các luật sinh - Luật sinh chứa ký hiệu bắt đầu liệt kê đầu tiên - Nếu có nhiều luật sinh có cùng vế trái thì nhóm lại thành luật sinh nhất, đó các vế phải cách ký hiệu “|”đọc là “hoặc” Ví dụ 2.1: Xem biểu thức là danh sách các số phân biệt dấu + và dấu - Ta có, văn phạm với các luật sinh sau xác định cú pháp biểu thức list → list + digit list → list - digit ⇔ list → digit list → list + digit | list - digit | digit digit → | | | digit → | | | | Như văn phạm phi ngữ cảnh đây là: - Tập hợp các ký hiệu kết thúc: 0, 1, 2, , 9, +, - Tập hợp các ký hiệu chưa kết thúc: list, digit - Các luật sinh đã nêu trên - Ký hiệu chưa kết thúc bắt đầu: list Ví dụ 2.2: Từ ví dụ 2.1 ta thấy: - + là list vì: là list vì nó là digit - là list vì là list và là digit - + là list vì - là list và là digit Ví dụ 2.3: Một list là chuỗi các lệnh, phân cách dấu ; khối begin - end Pascal Một danh sách rỗng các lệnh có thể có begin và end Chúng ta xây dựng văn phạm các luật sinh sau: block → begin opt_stmts end opt_stmts → stmt_list | ε stmt_list → stmt_list ; stmt | stmt 12 (13) Trong đó opt_stmts (optional statements) là danh sách các lệnh không có lệnh nào (ε) Luật sinh cho stmt_list giống luật sinh cho list ví dụ 2.1, cách thay +, - ; và stmt thay cho digit Cây phân tích cú pháp (Parse Tree) Cây phân tích cú pháp minh họa ký hiệu ban đầu văn phạm dẫn đến chuỗi ngôn ngữ Nếu ký hiệu chưa kết thúc A có luật sinh A → XYZ thì cây phân tích cú pháp có thể có nút có nhãn A và có nút có nhãn tương ứng từ trái qua phải là X, Y, Z A X Y Z Một cách hình thức, cho văn phạm phi ngữ cảnh thì cây phân tích cú pháp là cây có các tính chất sau đây: Nút gốc có nhãn là ký hiệu bắt đầu Mỗi lá có nhãn là ký hiệu kết thúc ε Mỗi nút có nhãn là ký hiệu chưa kết thúc Nếu A là ký hiệu chưa kết thúc dùng làm nhãn cho nút nào đó và X1 Xn là nhãn các nó theo thứ tự từ trái sang phải thì A → X1X2 Xn là luật sinh Ở đây X1, , Xn có thể là ký hiệu kết thúc chưa kết thúc Ðặc biệt, A → ε thì nút có nhãn A có thể có có nhãn ε Sự mơ hồ văn phạm Một văn phạm có thể sinh nhiều cây phân tích cú pháp cho cùng chuỗi nhập thì gọi là văn phạm mơ hồ Ví du 2.4: Giả sử chúng ta không phân biệt list với digit, xem chúng là string ta có văn phạm: string → string + string | string - string | | | | Với văn phạm này thì chuỗi biểu thức - + có đến hai cây phân tích cú pháp sau : string string string string + string string string string - Hình 2.1 - Minh họa văn phạm mơ hồ string + string 13 (14) Tương tự với cách đặt dấu ngoặc vào biểu thức sau : (9 - 5) + - ( + 2) Bởi vì chuỗi với nhiều cây phân tích cú pháp thường có nhiều nghĩa, đó biên dịch các chương trình ứng dụng, chúng ta cần thiết kế các văn phạm không có mơ hồ cần bổ sung thêm các qui tắc cần thiết để giải mơ hồ cho văn phạm Sự kết hợp các toán tử Thông thường, theo quy ước ta có biểu thức + + tương đương (9 + 5) + và - - tương đương với (9 - 5) - Khi toán hạng có hai toán tử trái và phải thì nó phải chọn hai để xử lý trước Nếu toán tử bên trái thực trước ta gọi là kết hợp trái Ngược lại là kết hợp phải Thường thì bốn phép toán số học: +, -, *, / có tính kết hợp trái Các phép toán số mũ, phép gán (=) có tính kết hợp phải Ví dụ 2.5: Trong ngôn ngữ C, biểu thức a = b = c tương đương a = ( b = c) vì chuỗi a = b = c với toán tử kết hợp phải sinh văn phạm: right → letter = right | letter letter → a | b | | z Ta có cây phân tích cú pháp có dạng sau (chú ý hướng cây nghiêng bên phải cây cho các phép toán có kết hợp trái thường nghiêng trái) right letter right = a letter b = letter c Hình 2.2 - Minh họa cây phân tích cú pháp cho toán tử kết hợp phải Thứ tự ưu tiên các toán tử Xét biểu thức + * Có cách để diễn giải biểu thức này, đó là + (5 * 2) ( + 5) * Tính kết hợp phép + và * không giải mơ hồ này, vì cần phải quy định thứ tự ưu tiên các loại toán tử khác Thông thường toán học, các toán tử * và / có độ ưu tiên cao + và - Cú pháp cho biểu thức : Văn phạm cho các biểu thức số học có thể xây dựng từ bảng kết hợp và ưu tiên các toán tử Chúng ta có thể bắt đầu với bốn phép tính số học theo thứ bậc sau : Kết hợp trái +, - Thứ tự ưu tiên Kết hợp trái *, / từ thấp đến cao 14 (15) Chúng ta tạo hai ký hiệu chưa kết thúc expr và term cho hai mức ưu tiên và ký hiệu chưa kết thúc factor làm đơn vị phát sinh sở biểu thức Ta có đơn vị biểu thức là số biểu thức dấu ngoặc factor → digit | (expr) Phép nhân và chia có thứ tự ưu tiên cao đồng thời chúng kết hợp trái nên luật sinh cho term tương tự cho list : term → term * factor | term / factor | factor Tương tự, ta có luật sinh cho expr : expr → expr + term | expr - term | term Vậy, cuối cùng ta thu văn phạm cho biểu thức sau : expr → expr + term | expr - term | term term → term * factor | term / factor | factor factor → digit | (expr) Như vậy: Văn phạm này xem biểu thức là danh sách các term phân cách dấu + - Term là list các factor phân cách * / Chú ý biểu thức nào ngoặc là factor, vì với các dấu ngoặc chúng ta có thể xây dựng các biểu thức lồng sâu nhiều cấp tuỳ ý Cú pháp các câu lệnh: Từ khóa (keyword) cho phép chúng ta nhận câu lệnh hầu hết các ngôn ngữ Ví dụ Pascal, hầu hết các lệnh bắt đầu từ khóa ngoại trừ lệnh gán Một số lệnh Pascal định nghĩa văn phạm (mơ hồ) sau, đó id danh biểu (tên biến) stmt → id := expr | if expr then stmt | if expr then stmt else stmt | while expr stmt | begin opt_stmts end Ký hiệu chưa kết thúc opt_stmts sinh danh sách có thể rỗng các lệnh, phân cách dấu chấm phẩy (;) II DỊCH TRỰC TIẾP CÚ PHÁP (Syntax - Directed Translation) Ðể dịch kết cấu ngôn ngữ lập trình, quá trình dịch, biên dịch cần lưu lại nhiều đại lượng khác cho việc sinh mã ngoài mã lệnh cần tạo cho kết cấu Chẳng hạn nó cần biết kiểu (type) kết cấu, địa lệnh đầu tiên mã đích, số lệnh phát sinh,v.v Vì ta nói cách ảo thuộc tính (attribute) kèm theo kết cấu Một thuộc tính có thể biểu diễn cho đại lượng kiểu, chuỗi, địa vùng nhớ, v.v Chúng ta sử dụng định nghĩa trực tiếp cú pháp (syntax - directed definition) nhằm đặc tả việc phiên dịch các kết cấu ngôn ngữ lập trình theo các thuộc tính kèm 15 (16) với thành phần cú pháp nó Chúng ta sử dụng thuật ngữ có tính thủ tục là lược đồ dịch (translation scheme) để đặc tả quá trình dịch Trong chương này, ta sử dụng lược đồ dịch để dịch biểu thức trung tố thành dạng hậu tố Ký pháp hậu tố (Postfix Notation) Ký pháp hậu tố biểu thức E có thể định nghĩa quy nạp sau: Nếu E là biến hay thì ký pháp hậu tố E chính là E Nếu E là biểu thức có dạng E1 op E2 đó op là toán tử hai ngôi thì ký pháp hậu tố E là E1’ E2’ op Trong đó E1’, E2’ tương ứng là ký pháp hậu tố E1, E2 Nếu E là biểu thức dạng (E1) thì ký pháp hậu tố E là ký pháp hậu tố E1 Trong dạng ký pháp hậu tố, dấu ngoặc là không cần thiết vì vị trí và số lượng các đối số cho phép xác định giải mã cho biểu thức hậu tố Ví dụ 2.6: Dạng hậu tố biểu thức (9 - 5) + là - + Dạng hậu tố biểu thức - (5 + 2) là + - Ðịnh nghĩa trực tiếp cú pháp (Syntax - Directed Definition) Ðịnh nghĩa trực tiếp cú pháp sử dụng văn phạm phi ngữ cảnh để đặc tả cấu trúc cú pháp dòng input nhập Nó liên kết ký hiệu văn phạm với tập các thuộc tính và luật sinh kết hợp với tập các quy tắc ngữ nghĩa (semantic rule) để tính giá trị thuộc tính kèm với ký hiệu có luật sinh văn phạm Văn phạm và tập các quy tắc ngữ nghĩa tạo nên định nghĩa trực tiếp cú pháp Phiên dịch (translation) là ánh xạ input - output (input - output mapping) Output cho input x xác định theo cách sau Trước hết xây dựng cây phân tích cú pháp cho x Giả sử nút n cây phân tích cú pháp có nhãn là ký hiệu văn phạm X Ta viết X.a để giá trị thuộc tính a X nút đó Giá trị X.a n tính cách sử dụng quy tắc ngữ nghĩa cho thuộc tính a kết hợp với luật sinh cho X nút n Cây phân tích cú pháp có thể rõ giá trị thuộc tính nút gọi là cây phân tích cú pháp chú thích (annotated parse tree) Thuộc tính tổng hợp (Synthesized Attributes) Một thuộc tính gọi là tổng hợp giá trị nó nút trên cây cú pháp xác định từ các giá trị các thuộc tính các nút nút đó Ví dụ 2.7: Ðịnh nghĩa trực tiếp cú pháp cho việc dịch các biểu thức các số cách dấu + - thành ký pháp hậu tố sau: Luật sinh Quy tắc ngữ nghĩa E → E1 + T E.t := E1.t || T.t || ‘+’ E → E1 - T E.t := E1.t || T.t || ‘-’ E→ T E.t := T.t T→ T.t := ‘0’ 16 (17) T→ T.t := ‘9’ Hình 2.3 - Ví dụ định nghĩa trực tiếp cú pháp Chẳng hạn, quy tắc ngữ nghĩa E.t := E1.t || T.t || ‘+’ kết hợp với luật sinh xác định giá trị thuộc tính E.t cách ghép các ký pháp hậu tố E1.t và T.t và dấu ‘+’ Dấu || có nghĩa ghép các chuỗi Ta có cây phân tích cú pháp chú thích cho biểu thức - + sau : E.t = - + E.t = E.t = T.t = T.t = T.t = 9 - + Hình 2.4 - Minh họa cây phân tích cú pháp chú thích Giá trị thuộc tính t nút tính cách dùng quy tắc ngữ nghĩa kết hợp với luật sinh nút đó Giá trị thuộc tính nút gốc là ký pháp hậu tố chuỗi sinh cây phân tích cú pháp Duyệt theo chiều sâu (Depth - First Traversal) Quá trình dịch cài đặt cách đánh giá các luật ngữ nghĩa cho các thuộc tính cây phân tích cú pháp theo thứ tự xác định trước Ta dùng phép duyệt cây theo chiều sâu để đánh giá quy tắc ngữ nghĩa Bắt đầu từ nút gốc, thăm (đệ qui) các nút theo thứ tự từ trái sang phải Procedure visit (n : node); begin for với nút m n, từ trái sang phải visit (m); Ðánh giá quy tắc ngữ nghĩa nút n; end Lược đồ dịch (Translation Scheme) Một lược đồ dịch là văn phạm phi ngữ cảnh, đó các đoạn chương trình gọi là hành vi ngữ nghĩa (semantic actions) gán vào vế phải luật sinh Lược đồ dịch định nghĩa trực tiếp cú pháp thứ tự đánh giá các quy tắc ngữ nghĩa trình bày cách rõ ràng Vị trí mà đó hành vi thực trình bày cặp dấu ngoặc nhọn { } và viết vào vế phải luật sinh Ví dụ 2.8: rest → + term {print (‘+’)} rest1 17 (18) rest + {print(‘+’) } term rest1 Hình 2.5 - Một nút lá xây dựng cho hành vi ngữ nghĩa Lược đồ dịch tạo output cho câu nhập x sinh từ văn phạm đã cho cách thực các hành vi theo thứ tự mà chúng xuất quá trình duyệt theo chiều sâu cây phân tích cú pháp x Chẳng hạn, xét cây phân tích cú pháp với nút có nhãn rest biểu diễn luật sinh nói trên Hành vi ngữ nghĩa { print(‘+’) } thực sau cây term duyệt trước cây rest1 thăm Phát sinh dịch (Emitting a Translation) Trong chương này, hành vi ngữ nghĩa lược đồ dịch ghi kết quá trình phiên dịch vào tập tin, lần chuỗi ký tự Chẳng hạn, dịch - + thành - + cách ghi ký tự - + đúng lần mà không phải ghi lại quá trình dịch các biểu thức Khi tạo output theo cách này, thứ tự in các ký tự quan trọng Chú ý các định nghĩa trực tiếp cú pháp có đặc điểm sau: chuỗi biểu diễn cho dịch ký hiệu chưa kết thúc vế trái luật sinh là ghép nối các dịch vế phải theo đúng thứ tự chúng luật sinh và có thể thêm số chuỗi khác xen vào Một định nghĩa trực tiếp cú pháp theo dạng này xem là đơn giản Ví dụ 2.9: Với định nghĩa trực tiếp cú pháp hình 2.3, ta xây dựng lược đồ dịch sau : E → E1 + T { print (‘+’) } E → E1 - T { print (‘-’) } E→T T→0 { print (‘0’) } T→9 { print (‘9’) } Hình 2.6 - Lược đồ dịch biểu thức trung tố thành hậu tố Ta có các hành động dịch biểu thức - + thành - + sau : E E E T + T { print(‘-’) } T { print(‘+’) { print(‘2’) } { print(‘5’) } { print(‘9’) } Hình 2.7 - Các hành động dịch biểu thức 9-5+2 thành 5- + 18 (19) Xem quy tắc tổng quát, phần lớn các phương pháp phân tích cú pháp xử lý input chúng từ trái sang phải, lược đồ dịch đơn giản (lược đồ dịch dẫn xuất từ định nghĩa trực tiếp cú pháp đơn giản), các hành vi ngữ nghĩa thực từ trái sang phải Vì thế, để cài đặt lược đồ dịch đơn giản, chúng ta có thể thực các hành vi ngữ nghĩa lúc phân tích cú pháp mà không thiết phải xây dựng cây phân tích cú pháp III PHÂN TÍCH CÚ PHÁP (PARSING) Phân tích cú pháp là quá trình xác định xem liệu chuỗi ký hiệu kết thúc (token) có thể sinh từ văn phạm hay không ? Khi nói vấn đề này, chúng ta xem xây dựng cây phân tích cú pháp, mặc dù trình biên dịch có thể không xây dựng cây Tuy nhiên, quá trình phân tích cú pháp (parse) phải có khả xây dựng nó, không thì việc phiên dịch không bảo đảm tính đúng đắn Phần lớn các phương pháp phân tích cú pháp rơi vào lớp: phương pháp phân tích từ trên xuống và phương pháp phân tích từ lên Những thuật ngữ này muốn đề cập đến thứ tự xây dựng các nút cây phân tích cú pháp Trong phương pháp đầu, quá trình xây dựng gốc tiến hành hướng xuống các nút lá, còn phương pháp sau thì thực từ các nút lá hướng gốc Phương pháp phân tích từ trên xuống thông dụng nhờ vào tính hiệu nó xây dựng theo lối thủ công Ngược lại, phương pháp phân tích từ lên lại có thể xử lý lớp văn phạm và lược đồ dịch phong phú Vì vậy, đa số các công cụ phần mềm giúp xây dựng thể phân tích cú pháp cách trực tiếp từ văn phạm có xu hướng sử dụng phương pháp từ lên Phân tích cú pháp từ trên xuống (Top - Down Parsing) Xét văn phạm sinh tập các kiểu liệu Pascal type → simple | ↑ id | array [simple] of type simple → integer | char | num num Phân tích trên xuống bắt đầu nút gốc, nhãn là ký hiệu chưa kết thúc bắt đầu và lặp lại việc thực hai bước sau đây: Tại nút n, nhãn là ký hiệu chưa kết thúc A, chọn luật sinh A và xây dựng các n cho các ký hiệu vế phải luật sinh Tìm nút mà đó cây xây dựng Ðối với số văn phạm, các bước trên cài đặt phép quét (scan) dòng nhập từ trái qua phải Ví dụ 2.10: Với các luật sinh văn phạm trên, ta xây dựng cây cú pháp cho dòng nhập: array [num num] of integer Mở đầu ta xây dựng nút gốc với nhãn type Ðể xây dựng các nút type ta chọn luật sinh type → array [simple] of type Các ký hiệu nằm bên phải luật sinh này là array, [, simple, ], of, type đó nút gốc type có có nhãn tương ứng (áp dụng bước 1) Trong các nút type, từ trái qua thì nút có nhãn simple (một ký hiệu chưa kết thúc) đó có thể xây dựng cây nút simple (bước 2) 19 (20) Trong các luật sinh có vế trái là simple, ta chọn luật sinh simple → num num để xây dựng Nói chung, việc chọn luật sinh có thể xem quá trình thử và sai (trial - and - error) Nghĩa là luật sinh chọn để thử và sau đó quay lại để thử luật sinh khác luật sinh ban đầu không phù hợp Một luật sinh là không phù hợp sau sử dụng luật sinh này chúng ta không thể xây dựng cây hợp với dòng nhập Ðể tránh việc lần ngược, người ta đưa phương pháp gọi là phương pháp phân tích cú pháp dự đoán type (a) (b) array (c) [ simple num ] of num type simple (d) integer (e) Hình 2.8 - Minh họa quá trình phân tích cú pháp từ trên xuống Phân tích cú pháp dự đoán (Predictive Parsing) Phương pháp phân tích cú pháp đệ qui xuống (recursive-descent parsing) là phương pháp phân tích trên xuống, đó chúng ta thực loạt thủ tục đệ qui để xử lý chuỗi nhập Mỗi thủ tục kết hợp với ký hiệu chưa kết thúc văn phạm Ở đây chúng ta xét trường hợp đặc biệt phương pháp đệ qui xuống là phương pháp phân tích dự đoán đó ký hiệu dò tìm xác định thủ tục chọn ký hiệu chưa kết thúc Chuỗi các thủ tục gọi quá trình xử lý chuỗi nhập tạo cây phân tích cú pháp Ví dụ 2.11: Xét văn phạm trên, ta viết các thủ tục type và simple tương ứng với các ký hiệu chưa kết thúc type và simple văn phạm Ta còn đưa thêm thủ tục match để đơn giản hóa đoạn mã cho hai thủ tục trên, nó dịch tới ký hiệu tham số t nó so khớp với ký hiệu dò tìm đầu đọc (lookahead) procedure match (t: token); begin if lookahead = t then lookahead := nexttoken else error end; procedure type; begin if lookahead in [integer, char, num] then simple else if lookahead = ‘↑‘ then begin 20 (21) match (‘↑‘); match(id); end else if lookahead = array then begin match(array); match(‘[‘); simple; match(‘]’); match(of); type end else error; end; procedure simple; begin if lookahead = integer then match(integer) else if lookahead = char then match(char) else if lookahead = num then begin match(num); match(dotdot); match(num); end else error end; Hình 2.9 - Ðoạn mã giả minh họa phương pháp phân tích dự đoán Phân tích cú pháp bắt đầu lời gọi tới thủ tục cho ký hiệu bắt đầu type Với dòng nhập array [num num] of integer thì đầu đọc lookahead bắt đầu đọc token array Thủ tục type sau đó thực chuỗi lệnh: match(array); match(‘[‘); simple; match(‘]’); match(of); type Sau đã đọc array và [ thì ký hiệu là num Tại điểm này thì thủ tục simple và các lệnh match(num); match(dotdot); match(num) thực Xét luật sinh type → simple Luật sinh này có thể dùng ký hiệu dò tìm sinh simple, chẳng hạn ký hiệu dò tìm là integer mặc dù văn phạm không có luật sinh type → integer, có luật sinh simple → integer, đó luật sinh type → simple dùng cách type gọi simple Phân tích dự đoán dựa vào thông tin các ký hiệu đầu sinh vế phải luật sinh Nói chính xác hơn, giả sử ta có luật sinh A → γ , ta định nghĩa tập hợp : FIRST(γ) = { token | xuất các ký hiệu đầu nhiều chuỗi sinh γ } Nếu γ là ε có thể sinh ε thì ε ∈ FIRST(γ) Ví dụ 2.12: Xét văn phạm trên, ta dễ dàng xác định: FIRST( simple) = { integer, char, num } 21 (22) FIRST(↑id) = { ↑ } FIRST( array [simple] of type ) = { array } Nếu ta có A → α và A → β, phân tích đệ qui xuống không phải quay lui FIRST(α) ∩ FIRST(β) = ∅ Nếu ký hiệu dò tìm thuộc FIRST(α) thì A → α dùng Ngược lại, ký hiệu dò tìm thuộc FIRST(β) thì A → β dùng Trường hợp α = ε (Luật sinh ε) Ví dụ 2.13: Xét văn phạm chứa các luật sinh sau : stmt → begin opt_stmts end opt_stmts → stmt_list | ε Khi phân tích cú pháp cho opt_stmts, ký hiệu dò tìm ∉ FIRST(stmt_list) thì sử dụng luật sinh: opt_stmts → ε Chọn lựa này hòan tòan chính xác ký hiệu dò tìm là end, ký hiệu dò tìm khác end gây lỗi và phát phân tích stmt Thiết kế phân tích cú pháp dự đoán Bộ phân tích dự đoán là chương trình bao gồm các thủ tục tương ứng với các ký hiệu chưa kết thúc Mỗi thủ tục thực hai công việc sau: Luật sinh mà vế phải α nó dùng ký hiệu dò tìm thuộc FIRST(α) Nếu có đụng độ hai vế phải ký hiệu dò tìm nào thì không thể dùng phương pháp này Một luật sinh với ε nằm bên vế phải dùng ký hiệu dò tìm không thuộc tập hợp FIRST vế phải nào khác Một ký hiệu chưa kết thúc tương ứng lời gọi thủ tục, token phải phù hợp với ký hiệu dò tìm Nếu token không phù hợp với ký hiệu dò tìm thì có lỗi Loại bỏ đệ qui trái Một phân tích cú pháp đệ quy xuống có thể dẫn đến vòng lặp vô tận gặp luật sinh đệ qui trái dạng E → E + T vì ký hiệu trái bên vế phải giống ký hiệu chưa kết thúc bên vế trái luật sinh Ðể giải vấn đề này chúng ta phải loại bỏ đệ qui trái cách thêm vào ký hiệu chưa kết thúc Chẳng hạn với luật sinh dạng A → Aα | β.Ta thêm vào ký hiệu chưa kết thúc R để viết lại thành tập luật sinh sau : A→ βR R→ αR| ε Ví dụ 2.14: Xét luật sinh đệ quy trái : E → E + T | T Sử dụng quy tắc khử đệ quy trái nói trên với : A ≅ E, α ≅ + T, β≅ T Luật sinh trên có thể biến đổi tương đương thành tập luật sinh : E→ TR R→ +TR| ε 22 (23) IV MỘT CHƯƠNG TRÌNH DỊCH BIỂU THỨC ÐƠN GIẢN Sử dụng các kỹ thuật nêu trên, chúng ta xây dựng dịch trực tiếp cú pháp mà nó dịch biểu thức số học đơn giản từ trung tố sang hậu tố Ta bắt đầu với các biểu thức là các chữ số viết cách + - Xét lược đồ dịch cho dạng biểu thức này : expr → expr + term { print (‘+’) } expr → expr - term { print (‘-’) } expr → term term → { print (‘0’) } term → { print (‘9’) } Hình 2.10 - Ðặc tả lược đồ dịch khởi đầu Văn phạm tảng cho lược đồ dịch trên có chứa luật sinh đệ qui trái, phân tích cú pháp dự đoán không xử lý văn phạm dạng này, cho nên ta cần loại bỏ đệ quy trái cách đưa vào ký hiệu chưa kết thúc rest để văn phạm thích hợp sau: expr → term rest rest → + term { print(‘+’) } rest | - term {print(‘-’) rest | ε term → { print(‘0’) } term → { print(‘1’) } term → { print(‘9’) } Hình sau đây mô tả quá trình dịch biểu thức - + dựa vào lược đồ dịch trên: expr term rest { print(‘9’) } - term { print(‘-’) } { print(‘5’) } Hình 2.11 - Dịch - 5+2 thành 5- 2+ rest + term { print(+’) } rest { print(‘2’) } ε Bây ta cài đặt chương trình dịch C theo đặc tả trên Phần chính chương trình này là các đoạn mã C cho các hàm expr, term và rest // Hàm expr( ) tương ứng với ký hiệu chưa kết thúc expr expr( ) 23 (24) { term( ) ; rest( ); } // Hàm expr( ) tương ứng với ký hiệu chưa kết thúc expr rest( ) { if (lookahead = = ‘+’ ) { match(‘+’) ; term( ) ; putchar (‘+ ‘) ; rest( ); } else if (lookahead = = ‘-’) { match(‘-’) ; term( ) ; putchar (‘-’) ; rest( ); } else ; } // Hàm expr( ) tương ứng với ký hiệu chưa kết thúc expr term( ) { if (isdigit (lookahead) { putchar (lookahead); match (lookahead); } else error( ); } Tối ưu hóa chương trình dịch Một số lời gọi đệ quy có thể thay các vòng lặp để làm cho chương trình thực nhanh Ðoạn mã cho rest có thể viết lại sau : rest( ) { L : if (lookahead = = ‘+’ ) { match(‘+’) ; term( ) ; putchar (‘+ ‘) ; goto L; } else if (lookahead = = ‘-’) { match(‘-’) ; term( ) ; putchar (‘-’) ; goto L; 24 (25) } else ; } Nhờ thay này, hai hàm rest và expr có thể tích hợp lại thành Mặt khác, C, câu lệnh stmt có thể thực lặp lặp lại cách viết : while (1) stmt với là điều kiện đúng Chúng ta có thể thóat khỏi vòng lặp dễ dàng lệnh break Ðoạn chương trình có thể viết lại sau : expr ( ) { term ( ) while (1) if (lookahead = = ‘+’ ) { match(‘+’) ; term( ) ; putchar (‘+ ‘) ; } else if (lookahead = = ‘-’) { match(‘-’) ; term( ) ; putchar (‘-’) ; } else break; } Chương trình C dịch biểu thức trung tố sang hậu tố Chương trình nguồn C hoàn chỉnh cho chương trình dịch có mã sau : # include< ctype.h> /* nạp tập tin chứa isdigit vào*/ int lookahead; main ( ) { lookahead = getchar( ); expr( ) ; putchar(‘ \n‘); /* thêm vào ký tự xuống hàng */ } expr( ) { term( ); while(1) 25 (26) if (lookahead = = ‘+’) { match(‘+’); term( ); putchar(‘+ ‘); } else if (lookahead = = ‘-’ ) { match(‘-’); term( ); putchar(‘-’); } else break; } term( ) { if (isdigit(lookahead)) { putchar(lookahead); match(lookahead); } else error( ); } match ( int t) { if (lookahead = = t) lookahead = getchar(); else error( ); } error( ) { printf (“syntax error \n”); /* in thông báo lỗi */ exit(1); /* kết thúc */ } V PHÂN TÍCH TỪ VỰNG (Lexical Analysis) Bây chúng ta thêm vào phần trước trình biên dịch phân tích từ vựng để đọc và biến đổi dòng nhập thành chuỗi các từ tố (token) mà phân tích cú pháp có thể sử dụng Nhắc lại chuỗi các ký tự hợp thành token gọi là trị từ vựng (lexeme) token đó Trước hết ta trình bày số chức cần thiết 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 xem xét tất các ký tự dòng nhập nên 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) 26 (27) các dòng chú thích (comment) phải bị bỏ qua Khi phân tích từ vựng đã bỏ qua các khoảng trắng này thì phân tích cú pháp không xem xét đến chúng Chọn lựa cách sửa đổi văn phạm để đưa khoảng trắng vào cú pháp thì khó cài đặt Xử lý các Bất nào ký tự số xuất biểu thức thì nó xem là số Bởi vì số nguyên là dãy các chữ số nên nó có thể cho luật sinh văn phạm tạo token cho số đó Bộ phân tích từ vựng có nhiệm vụ ghép các chữ số để số và sử dụng nó đơn vị suốt quá trình dịch Ðặt num là token biểu diễn cho số nguyên Khi chuỗi các chữ số xuất dòng nhập thì phân tích từ vựng gửi num cho phân tích cú pháp Giá trị số nguyên chuyển cho phân tích cú pháp là thuộc tính token num Về mặt logic, phân tích từ vựng chuyển token và các thuộc tính cho phân tích cú pháp Nếu ta viết token và thuộc tính thành nằm < > thì dòng nhập 31 + 28 + 59 chuyển thành dãy các : <num, 31>, < +, >, <num, 28>, < +, >, <num, 59> Bộ <+, > cho thấy thuộc tính + không có vai trò gì phân tích cú pháp nó cần thiết dùng đến 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) là tên biến, mảng, hàm và văn phạm xử lý các danh biểu này là token Người ta dùng token id cho các danh biểu khác đó ta có dòng nhập count = count + increment; thì phân tích từ vựng chuyển cho 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 nó: token id trị từ vựng (lexeme) có thể là count increment) Khi lexeme thể cho danh biểu tìm thấy dòng nhập cần phải có chế để xác định xem lexeme này đã thấy trước đó chưa? Công việc này thực nhờ lưu trữ trợ giúp bảng ký hiệu (symbol table) đã nêu chương trước Trị từ vựng lưu bảng ký hiệu và trỏ đến mục ghi bảng trở thành thuộc tính token id Nhiều ngôn ngữ sử dụng các chuỗi ký tự cố định begin, end, if, để xác định số kết cấu Các chuỗi ký tự này gọi là từ khóa (keyword) Các từ khóa thỏa mãn qui luật hình thành danh biểu, cần qui ước chuỗi ký tự xác định là danh biểu nó không phải là từ khóa Một vấn đề cần quan tâm là vấn đề tách token trường hợp ký tự có thể xuất trị từ vựng nhiều token Ví dụ số các token là các toán tử quan hệ Pascal : <, < =, < > Giao diện phân tích từ vựng Bộ phân tích từ vựng đặt xen dòng nhập và phân tích cú pháp nên giao diện với hai này sau: 27 (28) Đọc ký tự Bộ phân tích từ vựng Input Chuyển token và thuộc tính Bộ phân tích cú pháp Đẩy ký tự trở Hình 2.12 - Giao diện phân tích từ vựng Bộ phân tích từ vựng đọc 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 trị từ vựng này cùng với các thuộc tính nó đến giai đoạn sau trình biên dịch Trong vài tình huống, phân tích từ vựng phải đọc vượt trước số ký tự xác định token để chuyển cho phân tích cú pháp Ví dụ, Pascal gặp ký tự >, phải đọc thêm ký tự sau đó nữa; ký tự sau là = thì token xác định là “lớn bằng”, ngược lại thì token là “lớn hơn” và đó ký tự đã bị đọc quá Trong trường hợp đó thì ký tự đọc quá này phải đẩy trả (push back) cho dòng nhập vì nó có thể là ký tự bắt đầu cho trị từ vựng Bộ phân tích từ vựng và phân tích cú pháp tạo thành 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 các token và phân tích cú pháp tiêu thụ chúng Các token sản xuất phân tích từ vựng lưu đệm (buffer) chúng tiêu thụ phân tích cú pháp Bộ phân tích từ vựng không thể hoạt động tiếp buffer bị đầy và phân tích cú pháp không thể hoạt động buffer rỗng Thông thường, buffer lưu giữ token Ðể cài đặt tương tác dễ dàng, người ta tạo thủ tục phân tích từ vựng gọi từ thủ tục phân tích cú pháp, lần gọi trả token Việc đọc và quay lui ký tự cài đặt cách dùng đệm nhập Một khối các ký tự đọc vào buffer nhập thời điểm nào đó, trỏ giữ vị trí đã phân tích Quay lui ký tự thực cách lùi trỏ trở Các ký tự dòng nhập có thể cần lưu lại cho công việc ghi nhận lỗi vì cần phải vị trí lỗi đoạn chương trình Ðể tránh việc phải quay lui, số trình biên dịch sử dụng chế đọc trước ký tự gọi đến phân tích từ vựng Bộ phân tích từ vựng đọc tiếp các ký tự đọc tới ký tự mở đầu cho token khác Trị từ vựng token trước đó bao gồm các ký tự từ ký tự đọc trước đến ký tự vừa ký tự vừa đọc Ký tự vừa đọc là ký tự mở đầu cho trị từ vựng token sau Với chế này thì ký tự đọc lần Một phân tích từ vựng Bây chúng ta xây dựng 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 ý cách cài đặt giao diện phân tích từ vựng viết C dạng hàm lexan Lexan đọc và đẩy các ký tự input trở cách gọi thủ tục getchar va ungetc 28 (29) Dùng getchar() đọc input Trả token cho bên gọi Bộ phân tích từ vựng Đẩy ký tự trở ungetc (c, stdin) lexan ( ) Đặt giá trị thuộc tính vào biến toàn cục tokenval Hình 2.13 - Cài đặt giao diện phân tích từ vựng Nếu ngôn ngữ cài đặt không cho phép trả các cấu trúc liệu từ các hàm thì token và các thuộc tính nó phải truyền riêng rẽ Hàm lexan trả số nguyên mã hóa cho token Token cho ký tự có thể là số nguyên quy ước dùng để mã hóa cho ký tự đó Một token num có thể mã hóa số nguyên lớn số nguyên 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 tượng trưng NUM thay cho số nguyên mã hóa num Hàm lexan trả NUM dãy chữ số tìm thấy input Biến toàn cục tokenval đặt là giá trị chuỗi số này Cài đặt hàm lexan sau : # include<stdio.h> # include<ctype.h> int lineno = 1; int tokenval = NONE; int lexan ( ) { int t; while(1) { t = getchar( ); if ( t = = ‘ ‘ || t = = ‘\t‘) ; /* loại bỏ blank và tab */ else if (t = = ‘\n’) lineno = lineno + 1; else if ( isdigit (t) ) { tokenval = t - ‘0’; t = getchar( ); while ( isdigit (t) ) { tokenval = tokenval * 10 + t - ‘0’; t = getchar( ); 29 (30) } ungetc (t, stdin); return NUM; } else { tokenval = NONE; return t; } } } /* while */ /* lexan */ VI SỰ HÌNH THÀNH BẢNG KÝ HIỆU Một cấu trúc liệu gọi là bảng ký hiệu (symbol table) thường dùng để lưu giữ thông tin các cấu trúc ngôn ngữ nguồn Các thông tin này tập hợp từ các giai đoạn phân tích trình biên dịch và sử dụng giai đoạn tổng hợp để sinh mã đích Ví dụ quá trình phân tích từ vựng, các chuỗi ký tự tạo token (trị từ vựng token) lưu vào mục ghi bảng danh biểu Các giai đoạn sau đó có thể bổ sung thêm các thông tin kiểu danh biểu, cách sử dụng nó và vị trí lưu trữ Giai đoạn sinh mã dùng thông tin này để tạo mã phù hợp, cho phép lưu trữ và truy xuất biến đó Giao diện bảng ký hiệu Các thủ tục trên bảng ký hiệu chủ yếu liên quan đến việc lưu trữ và truy xuất các trị từ vựng Khi trị từ vựng lưu trữ thì token kết hợp với nó lưu Hai thao tác sau thực trên bảng ký hiệu Insert (s, t): Trả mục ô cho chuỗi s, token t Lookup (s): Trả mục ô cho chuỗi s chuỗi s không tồn Bộ phân tích từ vựng sử dụng thao tác tìm kiếm lookup để xác định xem ô cho trị từ vựng token nào đó đã tồn bảng ký hiệu hay chưa? Nếu chưa thì dùng thao tác xen vào insert để tạo ô cho nó Xử lý từ khóa dành riêng Ta có thể sử dụng bảng ký hiệu nói trên để xử lý các từ khóa dành riêng (reserved keyword) Ví dụ với hai token div và mod với hai trị từ vựng tương ứng là div và mod Chúng ta có thể khởi tạo bảng ký hiệu lời gọi: insert (“div”, div); insert (“mod”, mod); Sau đó lời gọi lookup (“div”) trả token div, đó “div” không thể dùng làm danh biểu Với phương pháp vừa trình bày thì tập các từ khóa lưu trữ bảng ký hiệu trước việc phân tích từ vựng diễn Ta có thể lưu trữ các từ khóa bên ngoài 30 (31) bảng ký hiệu là danh sách có thứ tự các từ khóa Trong quá trình phân tích từ vựng, trị từ vựng xác định thì ta phải tìm (nhị phân) danh sách các từ khóa xem có trị từ vựng này không Nếu có, thì trị từ vựng đó là từ khóa, ngược lại, đó là danh biểu và đưa vào bảng ký hiệu Cài đặt bảng ký hiệu Cấu trúc liệu cụ thể dùng cài đặt cho bảng ký hiệu trình bày hình đây Chúng ta không muốn dùng lượng không gian nhớ định để lưu các trị từ vựng tạo danh biểu vì lượng không gian cố định có thể không đủ lớn để lưu các danh biểu dài và lãng phí gặp danh biểu ngắn Thông thường, bảng ký hiệu gồm hai mảng : Mảng lexemes (trị từ vựng) dùng để lưu trữ các chuỗi ký tự tạo danh biểu, các chuỗi này ngăn cách các ký tự EOS (end - of - string) Mảng symtable với phần tử là mẩu tin (record) bao gồm hai trường, trường trỏ lexptr trỏ tới đầu trị từ vựng và trường token Cũng có thể dùng thêm các trường khác để lưu trữ giá trị các thuộc tính Mục ghi thứ zero mảng symtable phải để trống vì giá trị trả hàm lookup trường hợp không tìm thấy ô tương ứng cho chuỗi ký hiệu Symtable Lexptr Token Attributes div mod id id d i v EOS m o d EOS c o u n t EOS i EOS Lexeme Hình 2.14 - Bảng ký hiệu và mảng để lưu các chuỗi Trong hình trên, ô thứ và thứ hai bảng ký hiệu dành cho các từ khóa div và mod Ô thứ ba và thứ tư dành cho các danh biểu count và i Ðoạn mã (ngôn ngữ giả) cho phân tích từ vựng dùng để xử lý các danh biểu sau Nó xử lý khoảng trắng và số nguyên giống thủ tục đã nói phần trước Khi phân tích từ vựng đọc vào chữ cái, nó bắt đầu lưu các chữ cái và chữ số vào vùng đệm lexbuf Chuỗi tập hợp lexbuf sau đó tìm mảng symtable bảng ký hiệu cách dùng hàm lookup Bởi vì bảng ký hiệu đã khởi tạo với ô cho div và mod (hình 2.14) nên nó tìm thấy 31 (32) trị từ vựng này lexbuf có chứa div hay mod, ngược lại không có ô cho chuỗi chứa lexbuf thì hàm lookup trả và đó hàm insert gọi để tạo ô symtable và p là số ô bảng ký hiệu chuỗi lexbuf Chỉ số này truyền tới phân tích cú pháp cách đặt tokenval := p và token nằm trường token trả Kết mặc nhiên là trả số nguyên mã hóa cho ký tự dùng làm token Function lexan: integer; var lexbuf: array[0 100] of char; c: char begin loop begin đọc ký tự vào c; if c là ký tự trống blank ký tự tab then không thực điều gì ; else if c là ký tự newline then lineno = lineno + else if c là ký tự số then begin đặt tokenval là giá trị ký số này và các ký số theo sau; return NUM; end else if c là chữ cái then begin đặt c và các ký tự, ký số theo sau vào lexbuf; p := lookup (lexbuf); if p = then p := insert (lexbuf, id); tokenval := p; return trường token ô có mục p; end else begin /* token là ký tự đơn */ đặt tokenval là NONE; /* không có thuộc tính */ return số nguyên mã hóa ký tự c; end; end; 32 (33) end; VII MÁY ẢO KIỂU STACK Ta đã biết kết giai đoạn phân tích là biểu diễn trung gian chương trình nguồn mà giai đoạn tổng hợp sử dụng nó để phát sinh mã đích Một dạng phổ biến biểu diễn trung gian là mã máy ảo kiểu Stack (abstact stack machine - ASM) Trong phần này, chúng ta trình bày khái quát máy ảo kiểu Stack và cách sinh mã chương trình cho nó Máy ảo này bao gồm thành phần: Vùng nhớ thị (instructions): là nơi chứa các thị Các thị này hạn chế và chia thành nhóm chính: nhóm thị số học trên số nguyên, nhóm thị thao tác trên Stack và nhóm thị điều khiển trình tự Vùng Stack: là nơi thực các thị trên các phép toán số học Vùng nhớ liệu (data): là nơi lưu trữ riêng các liệu Hình sau đây minh họa cho nguyên tắc thực dạng máy này, trỏ pc (program counter) thị chờ để thực Các giá trị dùng quá trình tính toán nạp vào đỉnh Stack Sau tính toán xong, kết lưu đỉnh Stack INSTRUCTIONS STACK DATA push 16 11 31 rvalue + rvalue * … top pc Hình 2.15 - Minh họa hình ảnh máy ảo kiểu Stack Ví dụ 2.15: Biểu thức (5 + b) * c với b = 11, c = thực trên Stack dạng biểu thức hậu tố b + c * Các thị số học Máy ảo phải cài đặt toán tử ngôn ngữ trung gian Khi gặp các thị số học đơn giản, máy thực phép toán tương ứng với hai giá trị trên đỉnh Stack, kết lưu vào đỉnh STACK Một phép toán phức tạp có thể cần phải cài đặt loạt thị máy Mã chương trình máy ảo cho biểu thức số học mô hành động ước lượng dạng hậu tố cho biểu thức đó cách sử dụng Stack Việc ước lượng tiến hành cách xử lý chuỗi hậu tố từ trái sang phải, đẩy toán hạng vào Stack gặp nó Với toán tử k - ngôi, đối số cận trái nó nằm (k -1) vị trí bên đỉnh Stack và đối số cận phải nằm đỉnh Hành động ước lượng áp dụng toán tử cho k giá trị trên đỉnh Stack, lấy toán hạng và đặt kết trở lại vào Stack 33 (34) Trong ngôn ngữ trung gian, giá trị là số nguyên; số tương ứng với false và các số khác tương ứng với true Toán tử logic and và or cần phải có đối số Chỉ thị L- value và R-value Ta cần phân biệt ý nghĩa các danh biểu vế trái và vế phải phép gán Trong phép gán sau : i := 5; i := i +1; vế phải xác định giá trị nguyên, còn vế trái xác định nơi giá trị lưu Tương tự, p và q là trỏ đến các ký tự dạng : p ↑ := q ↑; thì vế phải q↑ xác định ký tự, còn p↑ xác định vị trí ký tự lưu Các thuật ngữ L-value (giá trị trái) và R-value (giá trị phải) muốn nói đến các giá trị thích hợp tương ứng vế trái và vế phải phép gán Nghĩa là, R-value có thể xem là ‘giá trị’ còn L-value chính là các địa L-value l : Ðẩy nội dung vị trí liệu l vào Stack R-value l : Đẩy địa vị trí liệu l vào Stack Các thị thao tác trên STACK Bên cạnh thị cho thao tác đẩy số nguyên vào Stack và lấy giá trị khỏi đỉnh Stack, còn có số thị truy xuất vùng nhớ liệu sau: push v : Ðẩy giá trị v vào đỉnh Stack (top := top +1) pop : Lấy giá trị khỏi đỉnh Stack (top := top +1) := : R-value trên đỉnh Stack lưu vào L-value bên nó và lấy hai khỏi Stack (top := top -2) copy : Sao chép giá trị đỉnh Stack (top := top +1) Dịch các biểu thức Ðoạn mã chương trình dùng để ước lượng biểu thức trên máy ảo kiểu Stack có liên quan mật thiết với ký pháp hậu tố cho biểu thức đó Ví dụ 2.16: Dịch phép gán sau thành mã máy ảo kiểu Stack: day := (1461 * y) div + (153 * m + 2) div + d Ký pháp hậu tố biểu thức sau : day 1461 y * div 153 m * + div + d + := Ðoạn mã máy có dạng : L-value day push push 1461 + R-value y push * push div + 34 (35) div R-value push 153 + R- value m := d * Các thị điều khiển trình tự Máy ảo kiểu Stack thực các thị theo đúng thứ tự liệt kê trừ yêu cầu thực khác các câu lệnh nhảy có điều kiện không điều kiện Có số các tùy chọn dùng để mô tả các đích nhảy : Toán hạng làm thị cho biết vị trí đích Toán hạng làm thị mô tả khoảng cách tương đối cần nhảy theo chiều tới lui Ðích nhảy đến mô tả các ký hiệu tượng trưng gọi là các nhãn Một số thị điều khiển trình tự cho máy là : lable l : Gán đích các lệnh nhảy đến là l, không có tác dụng khác goto l : Chỉ thị lấy từ câu lệnh có lable l gofalse l : Lấy giá trị trên đỉnh Stack ra, giá trị là thì nhảy đến l, ngược lại, thực lệnh gotrue l : Lấy giá trị trên đỉnh Stack ra, giá trị khác thì nhảy đến l, ngược lại, thực lệnh halt : Ngưng thực chương trình Dịch các câu lệnh Sơ đồ phác thảo đoạn mã máy ảo cho số lệnh cấu trúc hình sau: IF expr THEN stmt WHILE expr DO stmt Code for expr Label test Gofalse out Code for expr Code for stmt Gofalse out Lable out Code for stmt Goto test Lable out Hình 2.16 - Sơ đồ đoạn mã cho số lệnh cấu trúc Xét sơ đồ đoạn mã cho câu lệnh If Giả sử newlable là thủ tục trả 35 (36) nhãn cho lần gọi Trong hành vi ngữ nghĩa sau đây, nhãn trả lời gọi đến newlabel ghi lại cách dùng biến cục out : stmt → if expr then stmt1 { out := newlable; stmt.t := expr.t || ‘ gofalse ’ out || stmt1.t || ‘ lable ’ out } Thay vì in các câu lệnh, ta có thể sử dụng thủ tục emit để che dấu các chi tiết in Chẳng hạn emit phải xem xét xem thị máy ảo có cần nằm trên hàng riêng biệt hay không Sử dụng thủ tục emit, ta có thể viết lại sau : stmt → if expr { out := newlable; emit (‘ gofalse ’, out); } then stmt1 { emit (‘ lable ’, out); } Khi hành vi ngữ nghĩa xuất bên luật sinh, ta xét các phần tử vế phải luật sinh theo thứ tự từ trái sang phải Ðoạn mã (ngôn ngữ giả) cho phép dịch phép gán và câu lệnh điều kiện If tương ứng sau : procedure stmt; var test, out: integer; /* dùng cho các nhãn */ begin if lookahead = id then begin emit (‘lvalue’, tokenval); match (id); match (‘:=‘); expr; end else if lookahead = ‘if’ then begin match (‘if’); expr; out := newlable; emit (‘gofalse’, out); match(‘then’); stmt; emit (‘lable’, out); end /* đoạn mã cho các lệnh còn lại */ else error; end; 36 (37) VIII KẾT NỐI CÁC KỸ THUẬT Trong các phần trên, chúng ta đã trình bày số kỹ thuật phiên dịch trực tiếp cú pháp để xây dựng kỳ đầu trình biên dịch Phần này thực việc kết nối chúng lại cách giới thiệu chương trình C có chức dịch trung tố - hậu tố cho ngôn ngữ gồm dãy các biểu thức kết thúc các dấu chấm phẩy Các biểu thức gồm có các số, danh biểu, các toán tử +, -, *, /, div và mod Output cho chương trình là dạng biểu diễn hậu tố cho biểu thức Mô tả chương trình dịch Chương trình dịch thiết kế cách dùng lược đồ dịch trực tiếp cú pháp có dạng sau : start → list eof list → expr ; list |ε expr → expr + term { print (‘+ ’) } | expr - term { print (‘- ’) } | term term → term * factor { print (‘* ’) } | term / factor { print (‘/ ’) } | term div factor { print (‘DIV’) } | term mod factor { print (‘MOD’) } | factor factor → ( expr ) | id { print (id.lexeme) } | num { print (num.value) } Trong đó, token id biểu diễn dãy không rỗng gồm các chữ cái và ký số bắt đầu chữ cái, num là dãy ký số, eof là ký tự cuối tập tin (end - of - file) Các token phân cách dãy ký tự blank, tab và newline - gọi chung là các khoảng trắng (white space) Thuộc tính lexeme token id là chuỗi ký tự tạo token dó, thuộc tính value token num chứa số nguyên biểu diễn num Ðoạn mã cho chương trình dịch bao gồm thủ tục, thủ tục lưu tập tin riêng Ðiểm bắt đầu thực thi chương trình nằm thủ tục chính main.c gồm có lời gọi đến init( ) để khởi gán, theo sau là lời gọi đến parse( ) để dịch Các thủ tục còn lại mô tả tổng quan hình sau: 37 (38) Biểu thức trung tố init.c lexer.c symbol.c parser.c error.c emitter.c Biểu thức hậu tố Hình 2.17 - Sơ đồ các thủ tục cho chương trình dịch biểu thừc Trước trình bày đoạn mã lệnh cho chương trình dịch, chúng ta mô tả sơ lược thủ tục và cách xây dựng chúng Thủ tục phân tích từ vựng lexer.c Bộ phân tích từ vựng là thủ tục có tên lexan( ) gọi từ phân tích cú pháp cần tìm các token Thủ tục này đọc ký tự dòng nhập, trả token vừa xác định cho phân tích cú pháp Giá trị các thuộc tính kèm với token gán cho biến toàn cục tokenval Bộ phân tích cú pháp có thể nhận các token sau : + - * / DIV MOD ( ) ID NUM DONE Trị từ vựng Token Giá trị thuộc tính Khoảng trắng Chuỗi các chữ số NUM Div DIV Mod MOD Chuỗi mở đầu là chữ cái, theo sau là chữ cái chữ số ID Ký tự cuối tập tin - eof Các ký tự khác Giá trị số Chỉ số symtable DONE Ký tự tương ứng NONE Trong đó ID biểu diễn cho danh biểu, NUM biểu diễn cho số và DONE là ký tự cuối tập tin eof Các khoảng trắng đã loại bỏ Bảng sau trình bày các token và giá trị thuộc tính sinh phân tích từ vựng cho token chương trình nguồn Thủ tục phân tích cú pháp parser.c Bộ phân tích cú pháp xây dựng theo phương pháp phân tích đệ quy xuống Trước tiên, ta loại bỏ đệ quy trái khỏi lược đồ dịch cách thêm vào biến R1 cho expr và R2 cho factor, thu lược đồ dịch sau: start → list eof 38 (39) list → expr ; list |ε expr → term R1 R1 → + term { print (‘ + ’) } R1 | - term { print (‘ - ’) } R1 | ε term → factor R2 R2 → * factor { print (‘ * ’) } R2 | / factor { print (‘ / ’) } R2 | DIV factor { print (‘DIV’) } R2 | MOD factor { print (‘MOD’) }R2 | ε factor → ( expr ) | id { print (id.lexeme) } | num { print (num.value) } Sau đó, chúng ta xây dựng các hàm cho các ký hiệu chưa kết thúc expr, term và factor Hàm parse( ) cài đặt ký hiệu bắt đầu start văn phạm, nó gọi lexan cần token Bộ phân tích cú pháp giai đoạn này sử dụng hàm emit để sinh kết và hàm error để ghi nhận lỗi cú pháp Thủ tục kết xuất emitter.c Thủ tục này có hàm emit (t, tval) sinh kết cho token t với giá trị thuộc tính tval Thủ tục quản lý bảng ký hiệu symbol.c và khởi tạo init.c Thủ tục symbol.c cài đặt cấu trúc liệu cho bảng danh biểu Các ô mảng symtable là các cặp gồm trỏ đến mảng lexemes và số nguyên biểu thị cho token lưu vị trí đó Thủ tục init.c dùng để khởi gán các từ khóa vào bảng danh biểu Biểu diễn trị từ vựng và token cho tất các từ khóa lưu mảng keywords cùng kiểu với mảng symtable Hàm init( ) duyệt qua mảng keyword, sử dụng hàm insert để đặt các từ khóa vào bảng danh biểu Thủ tục lỗi error.c Thủ tục này quản lý các ghi nhận lỗi và cần thiết Khi gặp lỗi cú pháp, trình biên dịch in thông báo cho biết lỗi đã xảy trên dòng nhập hành và dừng lại Một kỹ thuật khắc phục lỗi tốt có thể nhảy qua dấu chấm phẩy và tiếp tục phân tích câu lệnh sau đó Cài đặt chương trình nguồn Chương trình nguồn C cài đặt chương trình dịch trên 39 (40) / **** global.h # include <stdio.h> # include <ctype.h> ***************************************** / /* tải các thủ tục xuất nhập */ /* tải các thủ tục kiểm tra ký tự */ # define BSIZE # define NONE # define EOS 128 /* buffer size kích thước vùng đệm */ -1 '\0' # define # define # define # define # define 256 257 258 259 260 int int NUM DIV MOD ID DONE tokenval; lineno; /* giá trị cuả thuôcü tính token */ struct entry char int } struct entry { * lexptr; token; /* khuôn dạng cho ô bảng ký hiệu*/ symtable[ ] /* bảng ký hiệu*/ / **** lexer.c ***************************************** / # include "global.h" char lexbuf [BSIZE] int lineno = 1; int tokenval = NONE; int lexan ( ) /* phân tích từ vựng */ { int t; while(1) { t = getchar ( ); if ( t = = ‘ ‘ || t = = ‘ \t’) ; /* xóa các khoảng trắng */ else if (t = = ‘ \n ’) lineno = lineno + 1; else if ( isdigit (t) ) { /* t là ký số */ ungetc (t, stdin); scanf (" %d", & tokenval); return NUM; } else if ( isalpha (t) ) { /* t là chữ cái */ 40 (41) int p, b = 0; while ( isalnum (t) ) { /* t thuộc loại chữ - số */ lexbuf[b] = t; t = getchar ( ); b = b + 1; if (b > = BSIZE) error("compiler error"); } lexbuf[b] = EOS; if (t ! = EOF) ungetc (t, stdin); p = lookup (lexbuf); if (p = = 0) p = insert (lexbuf, ID) tokenval = p; return symtable[p].token; } else if (t = = EOF) { return DONE; else { tokenval = NONE; return t; } } } / **** parser.c ***************************************** / # include "global.h" int lookahead; parse ( ) /* phân tích cú pháp và dịch danh sách biểu thức */ { lookahead = lexan ( ); while (lookahead ! = DONE) { expr( ) ; match (‘ ; ’); } } expr ( ) { int t; term ( ); while(1) switch (lookahead) { case ' + ' : case ' - ' : t = lookahead; 41 (42) match (lookahead); term ( ); emit (t, NONE); continue; default : return; } } term ( ) { int t; factor ( ); while(1) switch (lookahead) { case ' * ' : case ' / ' : case ' DIV ' : case 'MOD ' : t = lookahead; match (lookahead); factor ( ); emit (t, NONE); continue; default : return; } } factor ( ) { switch (lookahead) { case ' ( ' : match (' ( '); expr ( ); match (' ) '); break; case NUM : emit (NUM, tokenval) ; match (' NUM '); break; case ID : emit (ID, tokenval) ; match (' ID '); break; default : error ( "syntax error"); } } match ( t ) int t; { if (lookahead = = t) lookahead = lexan( ); else error ("syntax error"); } / **** emitter.c ***************************************** / # include "global.h" 42 (43) emit (t, tval) /* tạo kết */ int t, tval; { switch ( t ) { case ' + ' : case ' - ' : case ' * ' : case ' / ' : printf (" %c \n", t); break; case DIV : printf (" DIV \n", t); break; case MOD : printf (" MOD \n", t); break; case NUM : printf (" %d \n", tval ); break; case ID : printf (" %s \n", symtable [tval] lexptr); break; default : printf (" token %d , tokenval %d \n ", t, tval ); } } / **** symbol.c ***************************************** / # include "global.h" # define # define STRMAX SYMMAX 999 100 /* kích thước mảng lexemes */ /* kích thước mảng symtable */ char int struct int lexemes [STRMAX]; lastchar = -1 /* vị trí dùng cuối cùng lexemes */ entry symtable [SYMMAX]; lastentry = /* vị trí dùng cuối cùng symtable */ int lookup (s) char s [ ]; /* trả vị trí ô cho s */ { int p; for (p = lastentry; p > 0; p = p - 1) if (strcmp (symtable[p].lexptr, s ) = = 0) return p; return 0; } int insert (s, tok) char s [ ]; int tok; /* trả vị trí ô cho s */ { int len; len = strlen (s) /* strlen tính chiều dài s */ 43 (44) if ( lastentry + > = SYMMAX) error ("symbol table full"); if ( lastchar + len + > = SYMMAX) error ("lexemes array full"); lastentry = lastentry + 1; symable [lastentry].token = tok; symable [lastentry].lexptr = &lexemes [lastchar + 1]; lastchar = lastchar + len + 1; strcpy (symable [lastentry].lexptr, s); return lastentry; } / **** init.c ***************************************** / # include "global.h" struct entry keyword [ ] = { "div", DIV "mod", MOD 0, } init ( ) /* đưa các từ khóa vào symtable */ { struct entry * p ; for (p = keywords; p → token; p ++ ) if (strcmp (symtable[p].lexptr, s ) = = 0) insert (p → lexptr, p → token) ; } / **** error.c ***************************************** / # include "global.h" eeror (m) /* sinh tất các thông báo lỗi * / char * m; { fprintf (stderr, " line % d : % s \n, lineno, m) exit ( ) /* kết thúc không thàình công * / } / **** main.c ***************************************** / # include "global.h" main ( ) { 44 (45) init ( ); parse ( ); exit (0); /* kết thúc thàình công * / } / ****************************************************************** / 45 (46) BÀI TẬP CHƯƠNG II 2.1 Cho văn phạm phi ngữ cảnh sau: S→SS+|SS*|a a) Viết các luật sinh dẫn câu nhập: aa+a* b) Xây dựng cây phân tích cú pháp cho câu nhập trên? c) Văn phạm này sinh ngôn ngữ gì? Giải thích câu trả lời 2.2 Ngôn ngữ gì sinh từ các văn phạm sau? Văn phạm nào là văn phạm mơ hồ? a) S → S | b) S → + S S | - S S | a c) S → S ( S ) S | ∈ d) S → a S b S | b S a S | ∈ e) S → a | S + S | S S | S * | ( S ) 2.3 Xây dựng văn phạm phi ngữ cảnh đơn nghĩa cho các ngôn ngữ sau đây: a) Các biểu thức số học dạng hậu tố b) Danh sách danh biểu có tính kết hợp trái phân cách dấu phẩy c) Danh sách danh biểu có tính kết hợp phải phân cách dấu phẩy d) Các biểu thức số học số nguyên và danh biểu với phép toán hai ngôi : + -, *, / 2.4 Viết thị máy ảo kiểu Stack cho quá trình dịch các biểu thức sau sang dạng hậu tố: a) t : = (a mod b) * 1998 - (2000 * c +100 ) div +1999 b) t : = a1 mod c2 + ( b3 -156 * d4 ) div / c) y := x + 100 z t 2.5 Xây dựng lược đồ dịch trực tiếp cú pháp để dịch biểu thức số học từ dạng trung tố sang dạng hậu tố ( cho các phép toán ngôi ) a) Xây dựng chương trình đổi mã hậu tố sang mã máy ảo kiểu Stack b) Viết chương trình thực thi mã máy ảo 46 (47) 2.6 Yêu cầu bài cho biểu thức số học dạng hậu tố sang dạng trung tố 2.7 Xây dựng lược đồ dịch trực tiếp cú pháp để xác định các dấu ngoặc chuỗi nhập là cân 2.8 Xây dựng lược đồ dịch trực tiếp cú pháp để dịch phát biểu FOR ngôn ngữ C có dạng sau: FOR ( exp1; exp2; exp3 ) Stmt sang dạng mà máy ảo kiểu Stack Viết chương trình thực thi mã máy ảo kiểu Stack 2.9 Xét đoạn văn phạm sau đây cho các câu lệnh if-then và if-then-else: Stmt → if expr then stmt | if expr then stmt else stmt | other a) Chứng tỏ văn phạm này là văn phạm mơ hồ b) Xây dựng văn phạm không mơ hồ tương đương với quy tắc: else chưa kết hợp kết hợp với then chưa kết hợp gần trước đó c) Xây dựng lược đồ dịch trực tiếp cú pháp để dịch các câu lệnh điều kiện thành mã máy ảo kiểu Stack 2.10 Xây dựng lược đồ dịch trực tiếp cú pháp để dịch các phát biểu ngôn ngữ PASCAL có dạng sau sang dạng mà máy ảo kiểu Stack Viết chương trình thực thi mã máy ảo kiểu Stack: a) REPEAT Stmt UNTIL expr b) IF expr THEN Stmt ELSE Stmt c) WHILE expr DO Stmt d) FOR i := expr1 downto expr2 DO Stmt 47 (48) CHƯƠNG III PHÂN TÍCH TỪ VỰNG Nội dung chính: Chương này trình bày các kỹ thuật xác định và cài đặt phân tích từ vựng Kỹ thuật đơn giản để xây dựng phân tích từ vựng là xây dựng các lược đồ - automata hữu hạn xác định (Deterministic Finite Automata - DFA) không xác định (Nondeterministic Finite Automata - NFA) – mô tả cấu trúc các thẻ từ (token) ngôn ngữ nguồn và sau đó dịch “thủ công” chúng sang chương trình nhận dạng các token Một kỹ thuật khác nhằm tạo phân tích từ vựng là sử dụng Lex – ngôn ngữ hành động theo mẫu (pattern) Trước tiên, người thiết kế trình biên dịch phải mô tả các mẫu xác định các biểu thức chính quy, sau đó sử dụng trình biên dịch Lex để tự động tạo định dạng automata hữu hạn hiệu (bộ phân tích từ vựng) Các mô tả và cách thức hoạt động chi tiết công cụ Lex trình bày rõ phần phụ lục A Mục tiêu cần đạt: Sau học xong chương này, sinh viên phải nắm các kỹ thuật tạo phân tích từ vựng Cụ thể, • Xây dựng các lược đồ cho các biểu thức chính quy mô tả ngôn ngữ cần viết trình biên dịch Sau đó chuyển đổi chúng sang chương trình phân tích từ vựng • Sử dụng công cụ có sẵn Lex để sinh phân tích từ vựng Kiến thức bản: Sinh viên phải có các kiến thức về: • DFA và NFA Các automata hữu hạn xác định và không xác định này sử dụng để nhận dạng chính xác ngôn ngữ mà các biểu thức chính quy có thể biểu diễn • Cách chuyển đổi từ NFA sang DFA nhằm làm đơn giản hóa quá trình cài đặt phân tích từ vựng Tài liệu tham khảo: [1] Automata and Formal Language An Introduction – Dean Kelley – Prentice Hall, Englewood Cliffs, New Jersey 07632 [2] Compilers : Principles, Technique and Tools - Alfred V.Aho, Jeffrey D.Ullman - Addison - Wesley Publishing Company, 1986 [3] Compiler Design – Reinhard Wilhelm, Dieter Maurer - Addison - Wesley Publishing Company, 1996 [4] Design of Compilers : Techniques of Programming Language Translation - Karen A Lemone - CRC Press, Inc, 1992 [5] Modern Compiler Implementation in C - Andrew W Appel - Cambridge University Press, 1997 48 (49) I VAI TRÒ CỦA BỘ PHÂN TÍCH TỪ VỰNG Phân tích từ vựng là giai đoạn đầu tiên trình biên dịch Nhiệm vụ chủ yếu nó là đọc các ký hiệu nhập tạo chuỗi các token sử dụng phân tích cú pháp Sự tương tác này thể hình sau, đó phân tích từ vựng thiết kế thủ tục gọi phân tích cú pháp, trả token gọi Chương trình nguồn token Bộ phân tích cú pháp Bộ phân tích từ vựng Lấy token kế Bảng ký hiệu Hình 3.1 - Giao diện phân tích từ vựng Các vấn đề giai đoạn phân tích từ vựng Có nhiều lý để tách riêng giai đoạn phân tích từ vựng với giai đoạn phân tích cú pháp: Thứ nhất, nó làm cho việc thiết kế đơn giản và dễ hiểu Chẳng hạn, phân tích cú pháp không phải xử lý các khoảng trắng hay các lời chú thích vì chúng đã phân tích từ vựng loại bỏ Hiệu trình biên dịch cải thiện, nhờ vào số chương trình xử lý chuyên dụng làm giảm đáng kể thời gian đọc liệu từ chương trình nguồn và nhóm các token Tính đa tương thích (mang dễ dàng) trình biên dịch cải thiện Ðặc tính ký tự nhập và khác biệt loại thiết bị có thể giới hạn bước phân tích từ vựng Dạng biểu diễn các ký hiệu đặc biệt là ký hiệu không chuẩn, chẳng hạn ký hiệu ( Pascal có thể cô lập phân tích từ vựng Token, mẫu từ vựng và trị từ vựng Khi nói đến phân tích từ vựng, ta sử dụng các thuật ngữ từ tố (thẻ từ, token), mẫu từ vựng (pattern) và trị từ vựng (lexeme) với nghĩa cụ thể sau: - Từ tố (token) là các ký hiệu kết thúc văn phạm ngôn ngữ nguồn, chẳng hạn như: từ khóa, danh biểu, toán tử, dấu câu, hằng, chuỗi, - Trị từ vựng (lexeme) token là chuỗi ký tự biểu diễn cho token đó - Mẫu từ vựng (pattern) là qui luật mô tả tập các trị từ vựng kết hợp với token nào đó Một số ví dụ cách dùng các thuật ngữ này trình bày bảng sau: 49 (50) Token Trị từ vựng minh họa Mô tả mẫu từ vựng const const const if if if relation <, <=, =, < >, >, >= < <= = <> > >= id pi, count, d2 Mở đầu là chữ cái theo sau là chữ cái, chữ số num 3.1416, 0, Bất kỳ số nào literal “ hello ” Mọi chữ cái nằm “ và “ ngoại trừ “ Hình 3.2 - Các ví dụ token Thuộc tính token Khi có nhiều mẫu từ vựng khớp với trị từ vựng, phân tích từ vựng trường hợp này phải cung cấp thêm số thông tin khác cho các bước biên dịch sau đó Do đó token, phân tích từ vựng đưa thông tin các token vào các thuộc tính kèm chúng Các token có ảnh hưởng đến các định phân tích cú pháp; các thuộc tính ảnh hưởng đến việc phiên dịch các thẻ từ Token kết hợp với thuộc tính nó tạo thành <token, tokenval> Ví dụ 3.1: Token và giá trị thuộc tính kèm câu lệnh Fortran : E = M * C ** đưọc viết dãy các sau: < id, trỏ bảng ký hiệu E > < assign_op, > < id, trỏ bảng ký hiệu M > < mult_op, > < id, trỏ bảng ký hiệu C> < exp_op, > < num, giá trị nguyên > Chú ý số không cần giá trị thuộc tính, thành phần đầu tiên là đủ để nhận dạng trị từ vựng Lỗi từ vựng Chỉ số ít lỗi phát bước phân tích từ vựng, vì phân tích từ vựng có nhiều cách nhìn nhận chương trình nguồn Ví dụ chuỗi fi nhìn thấy lần đầu tiên chương trình C với ngữ cảnh : fi ( a == f (x)) Bộ phân tích từ vựng không thể biết đây là lỗi không viết đúng từ khóa if hay danh biểu chưa khai báo Vì fi là danh biểu hợp lệ nên phân tích từ vựng phải trả token và để giai đoạn khác sau đó xác định lỗi Tuy nhiên, vài tình phải khắc phục lỗi để phân tích tiếp Chiến lược đơn giản là "phương thức hoảng sợ" (panic mode): Các ký tự xóa khỏi chuỗi nhập còn lại 50 (51) tìm token hoàn chỉnh Kỹ thuật này đôi gây nhầm lẫn cho giai đoạn phân tích cú pháp, nói chung là có thể sử dụng Một số chiến lược khắc phục lỗi khác là: Xóa ký tự dư Xen thêm ký tự bị Thay ký tự không đúng ký tự đúng Chuyển đổi hai ký tự II LƯU TRỮ TẠM CHƯƠNG TRÌNH NGUỒN Việc đọc ký tự chương trình nguồn có thể tiêu hao số thời gian đáng kể đó ảnh hưởng đến tốc độ dịch Ðể giải vấn đề này người ta đọc lúc chuỗi ký tự, lưu trữ vào vùng nhớ tạm - gọi là đệm input (buffer) Tuy nhiên, việc đọc gặp số trở ngại không thể xác định chuỗi nào thì chứa trọn vẹn token? Phần này giới thiệu vài phương pháp đọc đệm hiệu quả: Cặp đệm (Buffer Pairs) Ðối với nhiều ngôn ngữ nguồn, có vài trường hợp phân tích từ vựng phải đọc thêm số ký tự chương trình nguồn vượt quá trị từ vựng cho mẫu trước có thể thông báo đã so trùng token Trong phương pháp cặp đệm, vùng đệm chia thành hai nửa với kích thước nhau, nửa chứa N ký tự Thông thường, N là số ký tự trên khối đĩa, N 1024 4096 Mỗi lần đọc, N ký tự từ chương trình nguồn đọc vào nửa đệm lệnh đọc (read) hệ thống Nếu số ký tự còn lại chương trình nguồn ít N thì ký tự đặc biệt eof đưa vào buffer sau các ký tự vừa đọc để báo hiệu chương trình nguồn đã đọc hết Sử dụng hai trỏ dò tìm buffer Chuỗi ký tự nằm hai trỏ luôn luôn là trị từ vựng hành Khởi đầu, hai trỏ đặt trùng vị trí bắt đầu trị từ vựng Con trỏ p1 (lexeme_beginning) - trỏ bắt đầu trị từ vựng - giữ cố định vị trí này trỏ p2 (forwar) - trỏ tới - di chuyển qua ký tự buffer để xác định token Khi trị từ vựng cho token đã xác định, trỏ p1 dời lên trùng với p2 và bắt đầu dò tìm trị từ vựng E = M * C * * p1 EOF p2 Hình 3.3 - Cặp hai nửa vùng đệm Khi trỏ p2 tới ranh giới vùng đệm, nửa bên phải lấp đầy N ký tự chương trình nguồn Khi trỏ p2 tới vị trí cuối đệm, nửa bên trái lấp đầy N ký tự và p2 dời vị trí bắt đầu đệm 51 (52) Phương pháp cặp đệm này thường họat động tốt đó số lượng ký tự đọc trước bị giới hạn và số trường hợp nó có thể không nhận dạng token trỏ p2 phải vượt qua khoảng cách lớn chiều dài vùng đệm Giải thuật hình thức cho họat động trỏ p2 đệm : 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 nửa cuối then begin Ðọc vào nửa đầu; Dời p2 đầu đệm ; end else p2 := p2 + Khóa cầm canh (Sentinel) Phương pháp cặp đệm đòi hỏi lần di chuyển p2 phải kiểm tra xem có phải đã hết nửa buffer chưa nên kém hiệu vì phải hai lần kiểm tra Ðể khắc phục điều này, lần đọc N-1 ký tự vào nửa buffer còn ký tự thứ N là ký tự đặc biệt, thường là eof Như chúng ta đã rút ngắn lần kiểm tra E = M * eof C * * p1 p2 eof Hình 3.4 - Khóa cầm canh eof cuối vùng đệm Giải thuật hình thức cho họat động trỏ p2 đệm : p2 := p2 + 1; if p2↑ = eof then begin 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 nửa sau then 52 (53) begin Ðọc vào nửa đầu; Dời p2 vào đầu nửa đầu; end else /* EOF vùng đệm hết chương trình nguồn */ kết thúc phân tích từ vựng; end III ÐẶC TẢ TOKEN (Specification of Token ) Chuỗi và ngôn ngữ Chuỗi là tập hợp hữu hạn các ký tự Ðộ dài chuỗi là số các ký tự chuỗi Chuỗi rỗng ε là chuỗi có độ dài Ngôn ngữ là tập hợp các chuỗi Ngôn ngữ có thể bao gồm chuỗi rỗng ký hiệu là ∅ Các phép toán trên ngôn ngữ Cho ngôn ngữ L và M : - Hợp L và M : L ∪ M = { s | s ∈ L s ∈ M } - Ghép (concatenation) L và M: LM = { st | s ∈ L và t ∈ M } - Bao đóng Kleen L: L* = ∞∪i = Li - (Ghép nhiều L) Bao đóng dương (positive closure) L: L+ = ∞∪i = Li (Ghép nhiều L) Ví dụ 3.2: L = {A, B, , Z, a, b, , z } D = { 0, 1, , , } L ∪ D là tập hợp các chữ cái và số LD là tập hợp các chuỗi bao gồm chữ cái và chữ số L4 là tập hợp tất các chuỗi chữ cái L* là tâp hợp tất các chuỗi các chữ cái bao gồm chuỗi rỗng L( L ∪ D)* là tập hợp tất các chuỗi mở đầu chữ cái theo sau là chữ cái hay chữ số D+ là tập hợp tất các chuỗi gồm nhiều chữ số Biểu thức chính quy (Regular Expression) Trong Pascal, danh biểu là phần tử tập hợp L (L ∪ D)* Chúng ta có thể viết: danhbiểu = letter (letter | digit)* - Ðây là biểu thức chính quy 53 (54) Biểu thức chính quy xây dựng trên tập hợp các luật xác định Mỗi biểu thức chính quy r đặc tả ngôn ngữ L(r) Sau đây là các luật xác định biểu thức chính quy trên tập Alphabet ∑ ε là biểu thức chính quy đặc tả cho chuỗi rỗng {ε } Nếu a ∈ ∑ thì a là biểu thức chính quy r đặc tả tập hợp các chuỗi {a} Giả sử r và s là các biểu thức chính quy đặc tả các ngôn ngữ L(r) và L(s) ta có: a (r) | (s) là biểu thức chính quy đặc tả L(r) ∪ L(s) b (r) (s) là biểu thức chính quy đặc tả L(r)L(s) c (r)* là biểu thức chính quy đặc tả (L(r))* Quy ước: Toán tử bao đóng * có độ ưu tiên cao và kết hợp trái Toán tử ghép có độ ưu tiên thứ hai và kết hợp trái Toán tử hợp | có độ ưu tiên thấp và kết hợp trái Ví dụ 3.3: Cho ∑ = { a, b} Biểu thức chính quy a | b đặc tả {a, b} Biểu thức chính quy (a | b) (a | b) đặc tả tập hợp {aa, ab, ba, bb}.Tập hợp này có thể đặc tả biểu thức chính quy tương đương sau: aa | ab | ba | bb Biểu thức chính quy a* đặc tả { ε, a, aa, aaa, } Biểu thức chính quy (a | b)* đặc tả {(, a, b, aa,bb, } Tập này có thể đặc tả (a*b* )* Biểu thức chính quy a | a* b đặc tả {a, b, ab, aab, } Hai biểu thức chính quy cùng đặc tả tập hợp ta nói chúng tương đương và viết r = s Các tính chất đại số biểu thức chính quy Biểu thức chính quy tuân theo số luật đại số và có thể dùng các luật này để biến đổi biểu thức thành dạng tương đương Bảng sau trình bày số luật đại số cho các biểu thức chính quy r, s và t Tính chất Mô tả r|s=s|r | có tính chất giao hoán r | (s | t) = (r | s ) | t | có tính chất kết hợp (rs) t = r (st) Phép ghép có tính chất kết hợp r (s | t) = rs | rt (s | t) r = sr | tr εr = r Phép ghép phân phối phép | ε là phần tử đơn vị phép ghép 54 (55) rε = r r* = ( r | ε )* Quan hệ r và ε r* * = r * * có hiệu lực Hình 3.5 - Một số tính chất đại số biểu thức chính quy Ðịnh nghĩa chính quy (Regular Definitions) Ðịnh nghĩa chính quy là chuỗi các định nghĩa có dạng : d1 Æ r1 di là tên, d2 Æ r2 ri là biểu thức chính quy dn Æ rn Ví dụ 3.4: Tập hợp các danh biểu Pascal là tập hợp các chuỗi chữ cái và số, mở đầu chữ cái Ðịnh nghĩa chính quy tập đó là: letter Æ A | B | | Z | a | b | | z digit Æ | | | id Æ letter (letter | digit)* Ví dụ 3.5 : Các số không dấu Pascal là các chuỗi 5280, 39.37, 6.336E4 1.894E-4 Ðịnh nghĩa chính quy sau đặc tả tập các số này là : digit Æ | | | digits Æ digit digit* optional_fraction Æ digits | ε optional_exponent Æ ( E ( + | - | ε ) digits) | ε num Æ digits optional_fraction optional_exponent Ký hiệu viết tắt Người ta quy định các ký hiệu viết tắt cho thuận tiện việc biểu diễn sau: Một nhiều: dùng dấu + Không một: dùng dấu ? Ví dụ 3.6: r | ε viết tắt là r ? Ví dụ 3.7: Viết tắt cho định nghĩa chính quy tập hợp số num ví dụ 3.5 digit Æ | | | digits Æ digit + optional_fraction Æ ( digits ) ? optional_exponent Æ ( E ( + | - ) ? digits) ? num Æ digits optional_fraction optional_exponent 55 (56) Lớp ký tự [abc] = a | b | c [a - z] = a | b | | z Sử dụng lớp ký hiệu chúng ta có thể mô tả danh biểu là chuỗi sinh biểu thức chính quy : [A - Z a - z] [A - Z a - z - 9]* IV NHẬN DẠNG TOKEN Trong suốt phần này, chúng ta dùng ngôn ngữ tạo văn phạm đây làm thí dụ minh họa : stmt Æ if expr then stmt | if expr then stmt else stmt |ε expr Æ term relop term | term term Æ id | num Trong đó các ký hiệu kết thúc if, then, else, relop, id, num cho định nghĩa chính quy sau: if Æ if then Æ then else Æ else relop Æ < | <= | = | <> | > | >= id Æ letter (letter | digit) * num Æ digit + ( digit +) ? (E (+ | -) ? digit +) ? Ðịnh nghĩa chính quy các khoảng trắng ws (white space) delim Æ blank | tab | newline ws Æ delim+ Mục đích chúng ta là xây dựng phân tích từ vựng có thể định vị từ tố cho các token vùng đệm và tạo output là cặp token thích hợp và giá trị thuộc tính nó cách dùng mẫu biểu thức chính quy cho các token sau: Biểu thức chính quy Token Trị thuộc tính ws - - if if - then then 56 (57) - else else id id num num giá trị số < relop LT (Less Than) <= relop LE (Less Or Equal) = relop EQ (Equal) <> relop NE (Not Equal) > relop GT (Greater Than) >= relop GE (Greater Or Equal) trỏ bảng ký hiệu Hình 3.6 - Mẫu biểu thức chính quy cho số token Sơ đồ dịch Ðể dễ dàng nhận dạng token, chúng ta xây dựng cho token sơ đồ dịch (translation diagram) Sơ đồ dịch bao gồm các trạng thái (state) ký hiệu vòng tròn và các cạnh mũi tên nối các trạng thái Nói chung thường có nhiều sơ đồ dịch, sơ đồ đặc tả nhóm token Nếu xảy thất bại chúng ta theo sơ đồ dịch thì chúng ta dịch lui trỏ tới nơi nó đã trạng thái khởi đầu sơ đồ này kích họat sơ đồ dịch Do trỏ đầu trị từ vựng và trỏ tới cùng đến vị trí trạng thái khởi đầu sơ đồ, trỏ tới dịch lui lại để đến vị trí trỏ đầu trị từ vựng tới Nếu xảy thất bại tất sơ đồ dịch thì xem lỗi từ vựng đã phát và chúng ta khởi động thủ tục khắc phục lỗi Phần đây trình bày số sơ đồ dịch nhận dạng các token văn phạm ví dụ trên Sơ đồ dịch nhận dạng cho token relop: < = start return( relop, LE ) return( relop, NE ) > other = > * return( relop, LT ) return( relop, EQ ) = other return( relop, GE ) return( relop, GT ) * Hình 3.7 - Sơ đồ dịch cho các toán tử quan hệ Chúng ta dùng ký hiệu * để trạng thái mà chúng ta đã đọc quá ký tự, cần phải quay lui trỏ lại Sơ đồ dịch nhận dạng token id: 57 (58) letter or digit start letter other * 10 return( gettoken(), install_id() ) 11 Hình 3.8 - Sơ đồ dịch cho các danh biểu và từ khóa Một kỹ thuật đơn giản để tách từ khóa khỏi các danh biểu là khởi tạo bảng ký hiệu lưu trữ thông tin danh biểu cách thích hợp Ðối với các token cần nhận dạng văn phạm này, chúng ta cần nhập các chuỗi if, then và else vào bảng ký hiệu trước đọc các ký hiệu đệm nguyên liệu Ðồng thời ghi chú bảng ký hiệu để trả token đó các chuỗi này nhận Sử dụng các hàm gettoken( ) và install_id( ) tương ứng để nhận token và các thuộc tính trả Sơ đồ dịch nhận dạng token num: Một số vấn đề nảy sinh chúng ta xây dựng nhận dạng cho các số không dấu Trị từ vựng cho token num phải là trị từ vựng dài có thể Do đó, việc thử nhận dạng số trên các sơ đồ dịch phải theo thứ tự từ sơ đồ nhận dạng số dài digit start digit 12 13 • digit E + or - digit 14 digit 15 16 E other 18 * 19 digit digit start digit 17 digit 21 • 20 digit digit 22 other * 23 24 digit start digit 25 other * 26 27 Hình 3.9 - Sơ đồ dịch cho các số không dấu Pascal Có nhiều cách để tránh các đối sánh dư thừa các sơ đồ dịch trên Một cách là viết lại các sơ đồ dịch cách tổ hợp chúng thành - công việc nói chung là không đơn giản Một cách khác là thay đổi cách đáp ứng với thất bại qua trình duyệt qua sơ đồ Phương pháp sử dụng đây là cho phép ta vượt qua nhiều trạng thái kiểm nhận và quay trở lại trạng thái kiểm nhận cuối cùng đã qua thất bại xảy Sơ đồ dịch nhận dạng khoảng trắng ws (white space): Việc xử lý các khoảng trắng ws không hoàn toàn giống các mẫu nói trên vì không có gì để trả cho phân tích cú pháp tìm thấy các khoảng trắng 58 (59) chuỗi nhập Và đó, thao tác đơn giản cho việc dò tìm trên sơ đồ dịch phát khoảng trắng là trở lại trạng thái bắt đầu sơ đồ dịch đầu tiên để tìm mẫu khác delim start delim 28 29 other * 30 Hình 3.10 - Sơ đồ dịch cho các khoảng trắng Cài đặt sơ đồ dịch Dãy các sơ đồ dịch có thể chuyển thành chương trình để tìm kiếm token đặc tả các sơ đồ Mỗi trạng thái tương ứng với đoạn mã chương trình Nếu có các cạnh từ trạng thái thì đọc ký tự và tùy thuộc vào ký tự đó mà đến trạng thái khác Ta dùng hàm nextchar( ) đọc ký tự từ đệm input và trỏ p2 di chuyển sang phải ký tự Nếu không có cạnh từ trạng thái hành phù hợp với ký tự vừa đọc thì trỏ p2 phải quay lại vị trí p1 để chuyển sang sơ đồ dịch Hàm fail( ) làm nhiệm vụ này Nếu không có sơ đồ nào khác để thử, fail( ) gọi thủ tục khắc phục lỗi Ðể trả các token, chúng ta dùng biến tòan cục lexical_value Nó gán cho các trỏ các hàm install_id( ) và install_num( ) trả về, tương ứng tìm danh biểu số Lớp token trả thủ tục chính phân tích từ vựng có tên là nexttoken( ) int state = 0, start = 0; int lexical_value; /* để “trả về” thành phần thứ hai token */ int fail ( ) { forward = token_beginning; switch (start) { case : start = 9; break; case : start = 12; break; case 12 : start = 20; break; case 20 : start = 25; break; case 25 : recover ( ); break; default : / * lỗi trình biên dịch */ } return start; } token nexttoken ( ) 59 (60) { while (1) { switch (state) { case : c = nextchar ( ) ; / * c là ký hiệu đọc trước */ if ( c = = blank || c = = tab || c = = newline ) { state = 0; lexeme_beginning ++ ; / * dịch trỏ đến đầu trị từ vựng */ } else if (c = = ‘ < ’) state = 1; else if (c = = ‘ = ’) state = 5; else if (c = = ‘ > ’) state = 6; else state = fail ( ) ; break ; [ / * các trường hợp 1- đây */ case : c = nextchar ( ) ; if (isletter (c)) else state=10; state = fail ( ) ; break ; case 10 : c = nextchar ( ) ; if (isletter (c)) else state=10; if (isdigit(c)) else state = 10 ; state = 11 ; break ; case 11 : retract (1) ; install_id ( ) ; return (gettoken ( )); / * các trường hợp 12 - 24 đây */ case 25 : c = nextchar ( ) ; if (isdigit (c)) else state=26; state = fail ( ) ; break ; case 26 : c = nextchar ( ) ; if (isdigit (c)) else state=26; state = 27 ; break ; case 27 : retract (1) ; install_num ( ) ; return (NUM); 60 (61) } } } V NGÔN NGỮ ÐẶC TẢ CHO BỘ PHÂN TÍCH TỪ VỰNG Bộ sinh phân tích từ vựng Có nhiều công cụ để xây dựng phân tích từ vựng dựa vào các biểu thức chính quy Lex là công cụ sử dụng rộng rãi để tạo phân tích từ vựng Trước hết đặc tả cho phân tích từ vựng chuẩn bị cách tạo chương trình lex.l ngôn ngữ lex Trình biên dịch Lex dịch lex.l thành chương trình C là lex.yy.c Chương trình này bao gồm các đặc tả sơ đồ dịch xây dựng từ các biểu thức chính quy lex.l, kết hợp với các thủ tục chuẩn nhận dạng trị từ vựng Các hành vi kết hợp với biểu thức chính quy lex.l là các đoạn chương trình C chuyển sang lex.yy.c Cuối cùng trình biên dịch C dịch lex.yy.c thành chương trình đối tượng a.out, đó là phân tích từ vựng có thể chuyển dòng nhập thành chuỗi các token Chương trình nguồn lex: lex.l Lex Compiler Lex.yy.c Lex.yy.c C Compiler a.out Chuỗi nhập a.out Chuỗi các token Hình 3.11 - Tạo phân tích từ vựng Lex Chú ý: Những điều ta nói trên là nói lex UNIX Ngày có nhiều version lex Lex cho Pascal Javalex Ðặc tả lex Một chương trình lex bao gồm thành phần: Khai báo %% Quy tắc dịch %% Các thủ tục phụ Phần khai báo bao gồm khai báo biến, và các định nghĩa chính quy Phần quy tắc dịch cho các lệnh có dạng: p1 {action } 61 (62) p2 {action } pn {action n } Trong đó pi là các biểu thức chính quy, action i là đoạn chương trình mô tả hành động phân tích từ vựng thực pi tương ứng phù hợp với trị từ vựng Trong lex các đoạn chương trình này viết C nói chung có thể viết ngôn ngữ nào Các thủ tục phụ là cài đặt các hành động phần Ví dụ 3.8: Sau đây trình bày chương trình Lex nhận dạng các token văn phạm đã nêu phần trước và trả token tìm thấy %{ /* định nghĩa các LT, LE, EQ, NE, GT, GE, IF, THEN, ELSE, ID, NUMBER, RELOP */ }% /* định nghĩa chính quy */ delim [\t\n] ws {delim}+ letter [A - Za - z] digit [0 - 9] id {letter}({letter}| {digit})* number {digit}+(\.{digit}+)?(E[+\-]?{digit}+)? %% {ws} {/* Không có action, không có return */} if {return(IF); } then {return(THEN); } else {return(ELSE); } {id} {yylval = install_id( ); return(ID) } {number} {yylval = install_num( ); return(NUMBER) } “< ” {yylval = LT; return(RELOP) } “<= “ {yylval = LE; return(RELOP) } “= “ {yylval = EQ; return(RELOP) } “<> “ {yylval = NE; return(RELOP) } “> “ {yylval = GT; return(RELOP) } “>= “ {yylval = GE; return(RELOP) } %% 62 (63) install_id ( ) { /* Thủ tục phụ cài id vào bảng ký hiệu */ } install_num ( ) { /* Thủ tục phụ cài số vào bảng ký hiệu */ } 63 (64) BÀI TẬP CHƯƠNG III 3.1 Xác định chữ cái các ngôn ngữ sau: a) Pascal b) C c) LISP 3.2 Hãy xác định các trị từ vựng có thể hình thành các token các đoạn chương trình sau: a) PASCAL function max (i, j :integer) : integer; { Trả số nguyên lớn số i và j } begin i > j then max : = i else max : = j; end; b) C int max (i, j) int i, j; /* Trả số nguyên lớn số i và j */ { return i > j ? i : j } c) FORTRAN 77 FUNCTION MAX (i, j) C Trả số nguyên lớn số i và j IF ( I GT J) THEN MAX = I ELSE MAX = J END IF RETURN 64 (65) 3.3 Viết chương trình Lex chép tập tin, thay các chuỗi khoảng trắng thành khoảng trắng 3.4 Viết đặc tả Lex cho các token ngôn ngữ Pascal và dùng trình biên dịch Lex để xây dựng phân tích từ vựng cho Pascal 65 (66) CHƯƠNG IV PHÂN TÍCH CÚ PHÁP Nội dung chính: Mỗi ngôn ngữ lập trình có các quy tắc diễn tả cấu trúc cú pháp các chương trình có định dạng đúng Các cấu trúc cú pháp này mô tả văn phạm phi ngữ cảnh Phần đầu chương nhắc lại khái niệm văn phạm phi ngữ cảnh, cách tìm văn phạm tương đương không còn đệ quy trái và mơ hồ Phần lớn nội dung chương trình bày các phương pháp phân tích cú pháp thường sử dụng các trình biên dịch: Phân tích cú pháp từ trên xuống (Top down) và Phân tích cú pháp từ lên (Bottom up) Các chương trình nguồn có thể chứa các lỗi cú pháp Trong quá trình phân tích cú pháp chương trình nguồn, bất tiện chương trình dừng và thông báo lỗi gặp lỗi đầu tiên Vì cần phải có kỹ thuật để vượt qua các lỗi cú pháp để tiếp tục quá trình dịch - Các kỹ thuật phục hồi lỗi Từ văn phạm đặc tả ngôn ngữ lập trình và lựa chọn phương pháp phân tích cú pháp phù hợp, sinh viên có thể tự mình xây dựng phân tích cú pháp Phần còn lại chương giới thiệu công cụ Yacc Sinh viên có thể sử dụng công cụ này để tạo phân tích cú pháp thay vì phải tự cài đặt Mô tả chi tiết Yacc tìm thấy phần phụ lục B Mục tiêu cần đạt: Sau học xong chương này, sinh viên phải nắm được: • Các phương pháp phân tích cú pháp và các chiến lược phục hồi lỗi • Cách tự cài đặt phân tích cú pháp từ văn phạm phi ngữ cảnh xác định • Cách sử dụng công cụ Yacc để sinh phân tích cú pháp Kiến thức bản: Sinh viên phải có các kiến thức về: • Văn phạm phi ngữ cảnh (Context Free Grammar – CFG), Automat đẩy xuống (Pushdown Automata – PDA) • Cách biến đổi từ CFG PDA Tài liệu tham khảo: [1] Automata and Formal Language An Introduction – Dean Kelley – Prentice Hall, Englewood Cliffs, New Jersey 07632 [2] Compilers : Principles, Technique and Tools - Alfred V.Aho, Jeffrey D.Ullman - Addison - Wesley Publishing Company, 1986 [3] Compiler Design – Reinhard Wilhelm, Dieter Maurer - Addison - Wesley Publishing Company, 1996 [4] Design of Compilers : Techniques of Programming Language Translation - Karen A Lemone - CRC Press, Inc, 1992 [5] Modern Compiler Implementation in C - Andrew W Appel - Cambridge University Press, 1997 65 (67) I VAI TRÒ CỦA BỘ PHÂN TÍCH CÚ PHÁP Vai trò phân tích cú pháp Bộ phân tích cú pháp nhận chuỗi các token từ phân tích từ vựng và xác nhận chuỗi này có thể sinh từ văn phạm ngôn ngữ nguồn cách tạo cây phân tích cú pháp cho chuỗi Bộ phân tích cú pháp có chế ghi nhận các lỗi cú pháp theo phương thức linh hoạt và có khả phục hồi các lỗi thường gặp để có thể tiếp tục xử lý phần còn lại chuỗi nhập Chương trình nguồn Bộ phân tích từ vựng token Bộ phân tích cú Lấy token pháp tiếp Biểu diễn Cây Phần phân tích còn lại trung gian cú pháp front end Bảng ký hiệu Hình 4.1 - Vị trí phân tích cú pháp mô hình trình biên dịch Xử lý lỗi cú pháp Chương trình nguồn có thể chứa các lỗi nhiều mức độ khác nhau: - Lỗi từ vựng danh biểu, từ khóa, toán tử viết không đúng - Lỗi cú pháp ghi biểu thức toán học với các dấu ngoặc đóng và mở không cân - Lỗi ngữ nghĩa toán tử áp dụng vào toán hạng không tương thích - Lỗi logic thực lời gọi đệ qui không thể kết thúc Phần lớn việc phát và phục hồi lỗi trình biện dịch tập trung vào giai đọan phân tích cú pháp Vì thế, xử lý lỗi (error handler) quá trình phân tích cú pháp phải đạt mục đích sau: Ghi nhận và thông báo lỗi cách rõ ràng và chính xác Phục hồi lỗi cách nhanh chóng để có thể xác định các lỗi Không làm chậm tiến trình chương trình đúng Các chiến lược phục hồi lỗi Phục hồi lỗi là kỹ thuật vượt qua các lỗi để tiếp tục quá trình dịch Nhiều chiến lược phục hồi lỗi có thể dùng phân tích cú pháp Mặc dù không có chiến lược nào chấp nhận hoàn toàn, số chúng đã áp dụng rộng rãi Ở đây, chúng ta giới thiệu số chiến lược : a Phương thức "hoảng sợ" (panic mode recovery): Ðây là phương pháp đơn giản cho cài đặt và có thể dùng cho hầu hết các phương pháp phân tích Khi 66 (68) lỗi phát thì phân tích cú pháp bỏ qua ký hiệu tìm thấy tập hợp định các token đồng (synchronizing tokens), các token đồng thường là dấu chấm phẩy (;) end b Chiến lược mức ngữ đoạn (phrase_level recovery): Khi phát lỗi, phân tích cú pháp có thể thực hiệu chỉnh cục trên phần còn lại dòng nhập Cụ thể là thay phần đầu còn lại chuỗi ký tự có thể tiếp tục Chẳng hạn, dấu phẩy (,) dấu chấm phẩy (;), xóa dấu phẩy lạ thêm vào dấu chấm phẩy c Chiến lược dùng các luật sinh sửa lỗi (error production): Thêm vào văn phạm ngôn ngữ luật sinh lỗi và sử dụng văn phạm này để xây dựng phân tích cú pháp, chúng ta có thể sinh đoán lỗi thích hợp để cấu trúc lỗi nhận biết dòng nhập d Chiến lược hiệu chỉnh toàn cục (global correction): Một cách lý tưởng là trình biên dịch tạo số thay đổi xử lý lỗi Có giải thuật để lựa chọn số tối thiểu các thay đổi để đạt hiệu chỉnh có chi phí toàn cục nhỏ Cho chuỗi nhập có lỗi x và văn phạm G, các giải thuật này tìm cây phân tích cú pháp cho chuỗi y mà số lượng các thao tác chèn, xóa và thay đổi token cần thiết để chuyển x thành y là nhỏ Nói chung, kỹ thuật này còn dạng nghiên cứu lý thuyết II BIẾN ÐỔI VĂN PHẠM PHI NGỮ CẢNH Nhiều ngôn ngữ lập trình có cấu trúc đệ quy mà nó có thể định nghĩa các văn phạm phi ngữ cảnh (context-free grammar) G với thành phần G (V, T, P, S), đó: • V : là tập hữu hạn các ký hiệu chưa kết thúc hay các biến (variables) • T : là tập hữu hạn các ký hiệu kết thúc (terminals) • P : là tập luật sinh văn phạm (productions) • S ∈ V: là ký hiệu bắt đầu văn phạm (start symbol) Ví dụ 4.1: Văn phạm với các luật sinh sau cho phép định nghĩa các biểu thức số học đơn giản (với E là biểu thức expression) : E → E A E ⏐ (E) ⏐ - E ⏐ id A → +⏐-⏐*⏐/⏐↑ Cây phân tích cú pháp và dẫn xuất Cây phân tích cú pháp có thể xem dạng biểu diễn hình ảnh dẫn xuất Ta nói αAβ dẫn xuất αγβ (ký hiệu: αAβ ⇒ αγβ) A → γ là luật sinh, α và β là các chuỗi tùy ý các ký hiệu văn phạm Nếu α1 ⇒ α2 ⇒ ⇒ αn ta nói α1 dẫn xuất (suy ra) αn Ký hiệu ⇒ : dẫn xuất qua bước ⇒* : dẫn xuất qua nhiều bước 67 (69) ⇒ + : dẫn xuất qua nhiều bước Ta có tính chất: α ⇒* α với ∀α α ⇒* β và β ⇒* γ thì α ⇒* γ Cho văn phạm G với ký hiệu bắt đầu S Ta dùng quan hệ ⇒+ để định nghĩa L(G) ngôn ngữ sinh G Chuỗi L(G) có thể chứa ký hiệu kết thúc G Chuỗi các ký hiệu kết thúc w thuộc L(G) và S ⇒+ w, chuỗi w gọi là câu G Một ngôn ngữ sinh văn phạm gọi là ngôn ngữ phi ngữ cảnh Nếu hai văn phạm cùng sinh cùng ngôn ngữ thì chúng gọi là hai văn phạm tương đương Nếu S ⇒* α, đó α có thể chứa ký hiệu chưa kết thúc thì ta nói α là dạng câu (sentential form) G Một câu là dạng câu có chứa toàn các ký hiệu kết thúc Một cây phân tích cú pháp có thể xem biểu diễn đồ thị cho dẫn xuất Ðể hiểu phân tích cú pháp làm việc ta cần xét dẫn xuất đó có ký hiệu chưa kết thúc trái dạng câu nào thay bước, dẫn xuất gọi là trái Nếu α ⇒ β đó ký hiệu chưa kết thúc trái α thay thế, ta viết α ⇒* lm β Nếu S ⇒* lm α ta nói α là dạng câu trái văn phạm Tương tự, ta có dẫn xuất phải - còn gọi là dẫn xuất chính tắc (canonical derivations) Ví dụ 4.2: Cây phân tích cú pháp cho chuỗi nhập : - (id + id) sinh từ văn phạm ví dụ 4.1 E - E ( E ) E + E id id Hình 4.2 - Minh họa cây phân tích cú pháp Ðể thấy mối quan hệ cây phân tích cú pháp và dẫn xuất, ta xét dẫn xuất : α1 ⇒ α2⇒ ⇒ αn đó αi là ký hiệu chưa kết thúc A Với αi ta xây dựng cây phân tích cú pháp Ví dụ với dẫn xuất: E ⇒ -E ⇒ - (E) ⇒ - (E + E) ⇒ - (id + E) ⇒ - (id + id) Ta có quá trình xây dựng cây phân tích cú pháp sau : 68 (70) E E⇒ - E ⇒ E E ⇒ _ E ( E ) - ⇒ E ( E ( E ) E + E E E ⇒ E E + _ E ) ( E E id id E + ) E id Hình 4.3 - Xây dựng cây phân tích cú pháp từ dẫn xuất Loại bỏ mơ hồ Một văn phạm tạo nhiều cây phân tích cú pháp cho cùng chuỗi nhập gọi là văn phạm mơ hồ Nếu văn phạm là mơ hồ, ta không thể xác định cây phân tích cú pháp nào chọn Vì thế, ta phải viết lại văn phạm nhằm tránh mơ hồ nó Một ví dụ, chúng ta loại bỏ mơ hồ văn phạm sau : Stmt → if expr then stmt ⏐ if expr then stmt else stmt ⏐ other Ðây là văn phạm mơ hồ vì câu nhập if E1 then if E2 then S1 else S2 có hai cây phân tích cú pháp : Stmt if expr E1 then if Stmt expr E2 then Stmt S1 elsem Stmt S2 69 (71) Stmt if expr then Stmt elsem E1 if expr then E2 Stmt Stmt S2 S1 Hình 4.4 - Hai cây phân tích cú pháp cho câu nhập Ðể tránh mơ hồ này ta đưa nguyên tắc "Khớp else với then chưa khớp gần trước đó" Với qui tắc này, ta viết lại văn phạm trên sau : Stmt → matched_stmt | unmatched_stmt matched_stmt → if expr then matched_stmt else matched_stmt ⏐ other unmatched_stmt → if expr then Stmt ⏐ if expr then matched_stmt else unmatched_stmt Văn phạm tương đương này sinh tập chuỗi giống văn phạm mơ hồ trên, nó có cách dẫn xuất cây phân tích cú pháp cho chuỗi nhập Loại bỏ đệ qui trái Một văn phạm là đệ qui trái (left recursive) nó có ký hiệu chưa kết thúc A cho có dẫn xuất A ⇒+ Aα, với α là chuỗi nào đó Các phương pháp phân tích từ trên xuống không thể nào xử lý văn phạm đệ qui trái, đó cần phải dùng chế biến đổi tương đương để loại bỏ các đệ qui trái Ðệ qui trái có hai loại : Loại trực tiếp: Dạng A → Aα Loại gián tiếp: A ⇒i Aα với i ≥ Xét văn phạm sau: S → Aa | b A→ Ac | Sd | ε Biến S là biến đệ qui trái vì S ⇒ Aa ⇒ Sda, đây không phải là đệ qui trái trực tiếp Với đệ qui trái trực tiếp: Luật sinh có dạng: A → Aα1 | Aα2 | | Aαm | β1 | β2 | | βn Sẽ thay : A → β1A’ | β2A’ | | βnA’ A' → α1A'| α2A' | | αm 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) 70 (72) Giải thuật 4.1: Loại bỏ đệ qui trái Input: Văn phạm không tuần hoàn và không có các luật sinh ε (nghĩa là văn phạm không chứa các dạng A ⇒ +A và A→ ε) Output: Văn phạm tương đương không đệ qui trái Phương pháp: Sắp xếp các ký hiệu không kết thúc theo thứ tự A1, A2, , An For i:=1 to n Begin for j:=1 to i -1 begin Thay luật sinh dạng Ai → Ajγ luật sinh Ai→ δ1γ | δ2γ | | δkγ đó Aj → δ1 | δ2 | | δk là tất các Ai luật sinh tại; end; Loại bỏ đệ qui trái trực tiếp số các Ai luật sinh; End; Ví dụ 4.3: Áp dụng thuật toán trên cho văn phạm ví dụ trên Về lý thuyết, thuật toán 4.1 không bảo đảm hoạt động trường hợp văn phạm có chứa các luật sinh ε, trường hợp này luật sinh A → ε rõ ràng là "vô hại" Sắp xếp các ký hiệu chưa 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 Với i = 2, thay các S - luật sinh vào A → Sd được: A→ Ac | Aad | bd | ε Loại bỏ đệ qui trái trực tiếp cho các A luật sinh, ta : S→ Aa | b A→ bdA' A'→ cA' | adA | ε Tạo yếu tố trái Tạo yếu tố trái (left factoring) là phép biến đổi văn phạm có ích để có văn phạm thuận tiện cho việc phân tích dự đoán Ý tưởng là không rõ luật sinh nào hai luật sinh khả triển có thể dùng để khai triển ký hiệu chưa kết thúc A, chúng ta có thể viết lại các A - luật sinh nhằm "hoãn" lại việc định thấy đủ nguyên liệu cho lựa chọn đúng Xét văn phạm cho câu lệnh if: stmt → if expr then stmt else stmt | if expr then stmt 71 (73) Khi gặp token if, chúng ta không thể định cần chọn luật sinh nào để triển khai cho stmt Ðể giải vấn đề này, cách tổng quát, có luật sinh dạng A → αβ1 | αβ2, ta biến đổi luật sinh thành dạng : A → αA' A'→ β1 | β2 Giải thuật 4.2 : Tạo yếu tố trái cho văn phạm Input: Văn phạm G Output: Văn phạm tương đương với yếu tố trái Phương pháp: Vớ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 chuỗi α là chuỗi có độ dài lớn chung cho tất các vế phải (α là yếu tố trái) Giả sử A → αβ1 | αβ2 | | αβn | γ, đó γ không có chuỗi dẫn đầu chung với các vế phải khác Ta biến đổi luật sinh thành : A → αA' | γ A'→ β1 | β2 | | βn Với A' là ký hiệu chưa kết thúc Áp dụng lặp lặp lại phép biến đổi này không còn hai khả triển nào cho ký hiệu chưa kết thúc có tiền tố chung Ví dụ 4.4: Áp dụng thuật toán 4.2 cho văn phạm sau: S → i E t S | i E t S eS | α E→b Ta có văn phạm tương đương có chứa yếu tố trái sau : S → i E t S S' | α S' → eS | ε E→ b III PHÂN TÍCH CÚ PHÁP TỪ TRÊN XUỐNG Trong mục này, chúng ta giới thiệu các ý niệm phương pháp phân tích cú pháp từ trên xuống (Top Down Parsing) và trình bày dạng không quay lui hiệu phương pháp phân tích từ trên xuống, gọi là phương pháp phân tích dự đoán (predictive parser) Chúng ta định nghĩa lớp văn phạm LL(1) (viết tắt Left-to-right parse, Leftmost-derivation, 1-symbol lockahead ), đó phân tích dự đoán có thể xây dựng cách tự động Phân tích cú pháp đệ qui lùi (Recursive Descent Parsing) Phân tích cú pháp từ trên xuống có thể xem nỗ lực tìm kiếm dẫn xuất trái cho chuỗi nhập Nó có thể xem nỗ lực xây dựng cây phân tích cú pháp nút gốc và phát sinh dần xuống lá Một dạng tổng quát kỹ thuật phân tích từ trên xuống, gọi là phân tích cú pháp đệ quy lùi, có thể quay lui để 72 (74) quét lại chuỗi nhập Tuy nhiên, dạng này thường ít gặp Lý là với các kết cấu ngôn ngữ lập trình, chúng ta dùng đến nó Bộ phân tích cú pháp dự đoán (Predictive Parser) Trong nhiều trường hợp, cách viết văn phạm cách cẩn thận, loại bỏ đệ qui trái khỏi văn phạm tạo yếu tố trái, chúng ta có thể thu văn phạm mà phân tích cú pháp đệ quy lùi phân tích được, không cần quay lui, gọi là phân tích cú pháp dự đoán Xây dựng sơ đồ dịch cho phân tích dự đoán: Ðể xây dựng sơ đồ dịch cho phương pháp phân tích xuống, trước hết loại bỏ đệ qui trái, tạo yếu tố trái cho văn phạm Sau đó thực các bước sau cho ký hiệu chưa kết thúc A : Tạo trạng thái khởi đầu và trạng thái kết thúc Với luật sinh A → X1X2 Xn , tạo đường từ trạng thái khởi đầu đến trạng thái kết thúc các cạnh có nhãn X1X2 Xn Một cách cụ thể, sơ đồ dịch vẽ theo các nguyên tắc sau: Mỗi ký hiệu chưa kết thúc tương ứng với sơ đồ dịch đó nhãn cho các cạnh là token ký hiệu chưa kết thúc Mỗi token tương ứng với việc đoán nhận token đó và đọc token x Mỗi ký hiệu chưa kết thúc tương ứng với lời gọi thủ tục cho ký hiệu đó A Mỗi luật sinh có dạng A → α1 | α2 | | αn tương ứng với sơ đồ dịch α1 α2 αn Mỗi luật sinh dạng A → α1 α2 αn tương ứng với sơ đồ dịch α1 α2 αn Ví dụ 4.5: Xét văn phạm sinh biểu thức toán học E→E+T|T T→T*F|F 73 (75) F → (E) | id Loại bỏ đệ quy trái văn phạm, ta văn phạm tương đương sau : E → TE ‘ E’ → + TE’ | ε T → FT ’ T‘ → * FT ’ | ε F → (E) | id Một chương trình phân tích cú pháp dự đoán thiết kế dựa trên sơ đồ dịch cho các ký hiệu chưa kết thúc văn phạm Nó cố gắng so sánh các ký hiệu kết thúc với chuỗi nguyên liệu và đưa lời gọi đệ qui nó phải theo cạnh có nhãn là ký hiệu chưa kết thúc Các sơ đồ dịch tương ứng : T E T F E’ E’ 10 T E’ ε T’ T‘ + * F 11 12 T’ 13 ε F ( 14 15 E 16 ) 17 id Hình 4.5 - Các sơ đồ dịch cho các ký hiệu văn phạm Các sơ đồ dịch có thể đơn giản hóa cách thay sơ đồ này vào sơ đồ khác, thay này tương tự phép biến đổi trên văn phạm ε + T T E' : ε E: T + T ⇒ E' : + ε + ⇒E: T ε ε Tương tự ta có: * ⇒ T: F ε ⇒ F: 13 14 ( F 15 Hình 4.6 - Rút gọn sơ đồ dịch 16 ) 17 ε Phân tích dự đoán không đệ qui 74 (76) Chúng ta có thể xây dựng phân tích dự đoán không đệ qui cách trì tường minh Stack không phải ngầm định qua các lời gọi đệ quy Vấn đề chính quá trình phân tích dự đoán là việc xác định luật sinh áp dụng cho biến bước Một phân tích dự đoán làm việc theo mô hình sau: a + b $ INPUT STACK X Y Chương trình phân tích OUTPUT Z $ Bảng phân tích M Hình 4.7 - Mô hình phân tích cú pháp dự đoán không đệ quy - INPUT là đệm chứa chuỗi cần phân tích, kết thúc ký hiệu $ - STACK chứa chuỗi các ký hiệu văn phạm với ký hiệu $ nằm đáy Stack - Bảng phân tích M là mảng hai chiều dạng M[A,a], đó A là ký hiệu chưa kết thúc, a là ký hiệu kết thúc $ Bộ phân tích cú pháp điều khiển chương trình hoạt động sau: Chương trình xét ký hiệu X trên đỉnh Stack và ký hiệu nhập hành a Hai ký hiệu này xác định hoạt động phân tích cú pháp sau: Nếu X = a = $ thì chương trình phân tích cú pháp kết thúc thành công Nếu X = a ≠ $, Pop X khỏi Stack và đọc ký hiệu nhập Nếu X là ký hiệu chưa kết thúc thì chương trình truy xuất đến phần tử M[X,a] bảng phân tích M: - Nếu M[X,a] là luật sinh có dạng X → UVW thì Pop X khỏi đỉnh Stack và Push W, V, U vào Stack (với U trên đỉnh Stack), đồng thời xuất sinh luật sinh X → UVW - Nếu M[X,a] = error, gọi chương trình phục hồi lỗi Giải thuật 4.3 : Phân tích cú pháp dự đoán không đệ quy Input: Chuỗi nhập w và bảng phân tích cú pháp M cho văn phạm G Output: Nếu w ∈ L (G), cho dẫn xuất trái w Ngược lại, thông báo lỗi Phương pháp: Khởi đầu Stack chứa ký hiệu chưa kết thúc bắt đầu (S) trên đỉnh và đệm chứa câu nhập dạng w$ Ðặt trỏ ip trỏ tới ký hiệu đầu tiên w$ ; Repeat Gọi X là ký hiệu trên đỉnh Stack và a là ký hiệu trỏ ip ; 75 (77) If X là ký hiệu kết thúc $ then If X = a then lấy X khỏi Stack và dịch chuyển ip else error ( ) Else // X là ký hiệu chưa kết thúc If M[X,a] = X → Y1 Y2 Yk then begin Lấy X khỏi Stack; Ðẩy Yk ,Yk-1, ,Y1 vào Stack; Xuất luật sinh X → Y1 Y2 Yk; end else error ( ) Until /* Stack rỗng */ X=$ Ví dụ 4.6: Xét văn phạm đã khử đệ qui trái sinh biểu thức toán học ví dụ 4.5 : E → TE’ E’ → + TE’ | ε T → FT’ T’ → * FT’ | ε F → (E) | id Bảng phân tích M văn phạm cho sau : (ô trống tương ứng với lỗi) Ký hiệu chưa kết thúc Ký hiệu nhập id + * E → TE’ E E' T ) $ E→ ε E’→ ε T’→ ε T’→ ε E → TE’ E → +TE’ T → FT‘ T' F ( T → FT’ T’→ ε T’→ *FT’ F → id F → (E) Hình 4.8 - Bảng phân tích cú pháp M cho văn phạm Quá trình phân tích cú pháp cho chuỗi nhập: id + id * id trình bày bảng sau : STACK INPUT OUTPUT $E id + id * id $ $ E' T id + id * id $ E → T E' 76 (78) $ E' T' F id + id * id $ T → F T' $ E' T' id id + id * id $ F → id + id * id $ $ E' T' $ E' + id * id $ T' → ε + id * id $ E' → + T E' $ E' T + $ E' T id * id $ $ E' T' F id * id $ T → F T' id * id $ F → id $ E' T' id $ E' T' * id $ $ E' T' F * * id $ $ E' T' F id $ $ E' T' id id $ $ E' T' $ $ E' $ $ $ T' → * F T' F → id T' → ε E' → ε Cây phân tích cú pháp hình thành từ output : E E' T F id T' ε + T' F id E' T * ε F T' id ε Nhận xét: - Mỗi văn phạm có bảng phân tích M tương ứng - Chương trình không cần thay đổi cho các văn phạm khác Hàm FIRST và FOLLOW FIRST và FOLLOW là các tập hợp cho phép xây dựng bảng phân tích M và phục hồi lỗi theo chiến lược panic_mode Ðịnh nghĩa FIRST(α): Giả sử α là chuỗi các ký hiệu văn phạm, FIRST(α) là tập hợp các ký hiệu kết thúc mà nó bắt đầu chuỗi dẫn xuất từ α 77 (79) Nếu α ⇒* ε thì ε ∈ FIRST(α) Cách tính FIRST(X): Thực các quy luật sau không còn có ký hiệu kết thúc nào ε có thể thêm vào tập FIRST(X) : Nếu X là kí hiệu kết thúc thì FIRST(X) là {X} Nếu X → ε là luật sinh thì thêm ε vào FIRST(X) Nếu X → Y1Y2Y3 Yk là luật sinh thì thêm tất các ký hiệu kết thúc khác ε FIRST(Y1) vào FIRST(X) Nếu ε ∈ FIRST(Y1) thì tiếp tục thêm vào FIRST(X) tất các ký hiệu kết thúc khác ε FIRST(Y2) Nếu ε ∈ FIRST(Y1) ∩ FIRST(Y2) thì thêm tất các ký hiệu kết thúc khác ε ∈ FIRST(Y3) Cuối cùng thêm ε vào FIRST(X) ε ∈ ∩ki=1 FIRST(Yi) Ví dụ 4.7: Với văn phạm sau: E → T E' E' → + T E' | ε T → F T' T' → * F T' | ε F → (E) | id Theo định nghĩa tập FIRST, ta có : Vì F ⇒ (E) | id ⇒ FIRST(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') Mặt khác E' → + TE' mà FIRST(+) = {+} ⇒ FIRST(E') = {+, ε } Tương tự FIRST(T') = {*, ε } Vậy ta có : FIRST(E) = FIRST(T) = FIRST(F) = { (, id } FIRST(E') = {+, ε } FIRST(T') = {*, ε } Ðịnh nghĩa FOLLOW(A): (với A là ký hiệu chưa kết thúc) là tập hợp các ký hiệu kết thúc a mà nó xuất sau A (bên phía phải A) dạng câu nào đó Tức là tập hợp các ký hiệu kết thúc a, cho tồn dẫn xuất dạng S ⇒* αAaβ Chú ý A là ký hiệu phải dạng câu nào đó thì $ ∈ FOLLOW(A) ($ là ký hiệu kết thúc chuỗi nhập ) Cách tính FOLLOW(A): Áp dụng các quy tắc sau không thể thêm gì vào tập FOLLOW Ðặt $ vào follow(S), đó S là ký hiệu bắt đầu văn phạm và $ là ký hiệu kết thúc chuỗi nhập 78 (80) Nếu có luật sinh A→ αBβ thì thêm phần tử khác ε FIRST(β)vào FOLLOW(B) Nếu có luật sinh A→ αB A→ αBβ mà ε ∈ FIRST(β) thì thêm tất các phần tử FOLLOW(A) vào FOLLOW(B) Ví dụ 4.8: Với văn phạm ví dụ 4.6 nói trên: Áp dụng luật cho luật sinh F→ (E) ⇒ ) ∈ FOLLOW(E) ⇒ FOLLOW(E) ={$, ) } Áp dụng luật cho E → TE' ⇒ ), $ ∈ FOLLOW(E') ⇒ FOLLOW(E') ={$, ) } Áp dụng luật cho E → TE' ⇒ + ∈ FOLLOW(T) Áp dụng luật cho E' → +TE' , E' → ε ⇒ FOLLOW(E') ⊂ FOLLOW(T) ⇒ FOLLOW(T) = { +, ), $ } Áp dụng luật cho T→ FT' thì FOLLOW(T') = FOLLOW(T) ={+, ), $ } Áp dụng luật cho T→ FT' ⇒ * ∈ FOLLOW(F) Áp dụng luật cho T' → * F T' , T'→ ε ⇒ FOLLOW(T') ⊂ FOLLOW(F) ⇒ FOLLOW(F) = {*, +, ), $ } Vậy ta có: FOLLOW(E) = FOLLOW(E') = { $, ) } FOLLOW(T) = FOLLOW(T') = { +, ), $ } FOLLOW(F) = {*,+, ), $ } Xây dựng bảng phân tích M Giải thuật 4.4 : Xây dựng bảng phân tích cú pháp dự đoán Input: Văn phạm G Output: Bảng phân tích cú pháp M Phương pháp: Với luật sinh A→ α văn phạm, thực hai bước và Với ký hiệu kết thúc a ∈ FIRST(α), thêm A→ α vào M[A,a] Nếu ε ∈ FIRST(α) thì đưa luật sinh A→ α vào M[A,b] với ký hiệu kết thúc b ∈ FOLLOW(A) Nếu ε ∈ FIRST(α) và $ ∈ FOLLOW(A) thì đưa luật sinh A→ α vào M[A,$] Ô còn trống bảng tương ứng với lỗi (error) Ví dụ 4.9: Áp dụng thuật toán trên cho văn phạm ví dụ 4.6 Ta thấy: Luật sinh E → TE' : Tính FIRST(TE') = FIRST(T) = {(,id} ⇒ M[E,id] và M[E,( ] chứa luật sinh E → TE' Luật sinh E'→ + TE' : Tính FIRST(+TE') = FIRST(+) = {+} ⇒ M[E',+] chứa E' → +TE' 79 (81) Luật sinh E' → ε : Vì ε ∈ FIRST(E') và FOLLOW(E') = { ), $ } ⇒ E → ε nằm M[E',)] và M[E',$] Luật sinh T'→ * FT' : FIRST(* FT') = {* } ⇒ T' → * FT' nằm M[T',*] Luật sinh T' → ε: Vì ε ∈ FIRST(T') và FOLLOW(T') = {+, ), $} ⇒ T' → ε nằm M[T', +] , M[T', )] và M[T',$] Luật sinh F→ (E) ; FIRST((E)) = { ( } ⇒ F → ( E) nằm M[F, (] Luật sinh F → id ; FIRST(id) = {id} ⇒ F → id nằm M[F, id] Bảng phân tích cú pháp M văn phạm xây dựng hình 4.8 Văn phạm LL(1) Giải thuật 4.4 có thể áp dụng cho văn phạm G nào để sinh bảng phân tích M Tuy nhiên, có văn phạm (đệ quy trái mơ hồ) thì bảng phân tích M có thể có ô đa trị (có chưá nhiều luật sinh) Ví dụ 4.10: Xét văn phạm sau: S → iE t S S' | a S' → eS | ε E→b Bảng phân tích cú pháp M văn phạm sau : Ký hiệu chưa kết thúc S Ký hiệu kết thúc a b e T $ S→ iEtSS' S→ a S→ε S' E i ' S → eS S'→ε E→b Hình 4.9 - Bảng phân tích cú pháp M cho văn phạm ví dụ 4.10 Ðây là văn phạm mơ hồ và mơ hồ này thể qua việc chọn luật sinh gặp ký hiệu e (else) Ô vị trí M [S', e] gọi là ô đa trị Một văn phạm mà bảng phân tích M không có các` ô đa trị gọi là văn phạm LL(1) với ý nghĩa sau : L: Left-to-right parse (mô tả hành động quét chuỗi nhập từ trái sang phải) L: Leftmost-derivation (biểu thị việc sinh dẫn xuất trái cho chuỗi nhập) 80 (82) 1: 1-symbol lookahead (tại bước, đầu đọc đọc trước token để thực các định phân tích cú pháp) Văn phạm LL(1) có số tính chất đặc biệt Không có văn phạm mơ hồ hay đệ quy trái nào có thể là LL(1) Người ta đã chứng minh văn phạm G là LL(1) và A → α | β là luật sinh phân biệt G, các điều kiện sau đây đúng: Không có ký hiệu kết thúc a nào mà α và β dẫn xuất các chuỗi bắt đầu a Tối đa có α có β có thể dẫn xuất chuỗi rỗng Nếu β ⇒* ε thì α không dẫn xuất chuỗi nào bắt đầu ký hiệu kết thúc thuộc tập FOLLOW(A) Rõ ràng văn phạm ví dụ 4.5 cho các biểu thức số học là LL(1), văn phạm ví dụ 4.10 là văn phạm mô hình hóa câu lệnh if - then - else không phải là LL(1) Vấn đề đặt bây là làm nào để giải các ô đa trị? Một phương án khả thi là biến đổi văn phạm cách loại bỏ đệ quy trái, tạo yếu tố trái có thể với mong muốn sinh văn phạm với bảng phân tích cú pháp không chứa ô đa trị nào Nhưng có số văn phạm mà không có cách gì biến đổi thành văn phạm LL(1) Nói chung, không có quy tắc tổng quát nào để biến ô đa trị thành ô đơn trị mà không làm ảnh hưởng đến ngôn ngữ nhận dạng phân tích cú pháp Khó khăn chính dùng phân tích cú pháp dự đoán là việc viết văn phạm cho ngôn ngữ nguồn Việc loại bỏ đệ quy trái và tạo yếu tố trái dễ thực chúng biến đổi văn phạm trở thành khó đọc và khó dùng cho các mục đích biên dịch Phục hồi lỗi phân tích dự đoán Một lỗi tìm thấy quá trình phân tích dự đoán khi: Ký hiệu kết thúc trên đỉnh Stack không phù hợp với token dòng nhập Hoặc : Trên đỉnh Stack là ký hiệu chưa kết thúc A, token dòng nhập là a M[A,a] rỗng Phục hồi lỗi theo phương pháp panic_mode là bỏ qua các ký hiệu dòng nhập gặp phần tử tập hợp các token đồng (synchronizing token) Tính hiệu phương pháp này tùy thuộc vào cách chọn tập hợp các token đồng Một số heuristics có thể là: Ta có thể đưa tất các ký hiệu FOLLOW(A) vào tập hợp token đồng cho ký hiệu chưa kết thúc A FOLLOW(A) chưa phải là tập hợp các token đồng cho A Ví dụ, các lệnh C kết thúc ; (dấu chấm phẩy ) Nếu lệnh thiếu dấu ; thì từ khóa lệnh bị bỏ qua Thông thường ngôn ngữ có cấu trúc 81 (83) phân cấp, ví dụ biểu thức nằm lệnh, lệnh nằm khối lệnh, v.v Chúng ta có thể thêm vào tập hợp đồng cấu trúc ký hiệu mà nó bắt đầu cho cấu trúc cao Ví dụ, ta có thể thêm các từ khoá bắt đầu cho các lệnh vào tập đồng cho ký hiệu chưa kết thúc sinh biểu thức Nếu chúng ta thêm các phần tử FIRST(A) vào tập đồng cho ký hiệu chưa kết thúc A thì quá trình phân tích có thể hòa hợp với A ký hiệu FIRST(A) xuất dòng nhập Ví dụ 4.11: Sử dụng các ký hiệu kết thúc tập FOLLOW làm token đồng hóa hoạt động khá hữu hiệu phân tích cú pháp cho các biểu thức văn phạm ví dụ 4.6 FOLLOW(E) = FOLLOW(E') = { $, )} FOLLOW(T) = FOLLOW(T') = { +,$, )} FOLLOW(F) = {*,+, $, )} Bảng phân tích M cho văn phạm này thêm vào các ký hiệu đồng "synch", lấy từ tập FOLLOW các ký hiệu chưa kết thúc - xác định các token đồng : Ký hiệu chưa kết thúc Ký hiệu kết thúc E E→TE' id E' + * ( E→TE' E'→ +TE' T T→ FT' T' F F→ id synch T→ FT' T'→ ε T'→ *FT' synch synch F→ (E) ) $ synch synch E'→ ε E'→ ε synch synch T'→ ε T'→ ε synch synch Hình 4.10 - Bảng phân tích cú pháp M phục hồi lỗi Bảng này sử dụng sau: Nếu M[A,a] là rỗng thì bỏ qua token a Nếu M[A,a] là "synch" thì lấy A khỏi Stack nhằm tái hoạt dộng quá trình phân tích Nếu token trên đỉnh Stack không phù hợp với token dòng nhập thì lấy token khỏi Stack Chẳng hạn, với chuỗi nhập : + id * + id, phân tích cú pháp và chế phục hồi lỗi thực sau : STACK $E INPUT OUTPUT + id * + id $ error, nhảy qua + $E id * + id $ E → T E' $ E' T id * + id $ T → F T' 82 (84) $ E' T' F id * + id $ F → id $ E' T' id id * + id $ $ E' T' $ E' T' F * $ E' T' F $ E' T' $ E' $ E' T + * + id $ T' → * F T' * + id $ + id $ error, M[F,+] = synch pop F + id $ T → ε + id $ E' → + T E' + id $ $ E' T $ E' T' F id $ T' → F T' id $ F→ id $ E' T' id id $ $ E' T' $ $ E' $ $ $ T' → ε E' → ε IV PHÂN TÍCH CÚ PHÁP TỪ DƯỚI LÊN Phần này giới thiệu kiểu phân tích cú pháp từ lên tổng quát gọi là phân tích cú pháp Shift -Reduce Một dạng dễ cài đặt nó gọi là phân tích cú pháp thứ bậc toán tử (Operator - Precedence parsing) trình bày và cuối cùng, phương pháp tổng quát kỹ thuật Shift - Reduce là phân tích cú pháp LR (LR parsing) thảo luận Bộ phân tích cú pháp Shift - Reduce Phân tích cú pháp Shift - Reduce cố gắng xây dựng cây phân tích cú pháp cho chuỗi nhập nút lá và lên hướng nút gốc Ðây có thể xem là quá trình thu gọn (reduce) chuỗi w thành ký hiệu bắt đầu văn phạm Tại bước thu gọn, chuỗi cụ thể đối sánh với vế phải luật sinh nào đó thì chuỗi này thay ký hiệu vế trái luật sinh đó Và chuỗi chọn đúng bước, dẫn xuất phải đảo ngược xây dựng Ví dụ 4.12: Cho văn phạm: S→aABe A→ A b c | b B→d Câu abbcde có thể thu gọn thành S theo các bước sau: abbcde aAbcde aAde 83 (85) aABe S Thực chất đây là dẫn xuất phải đảo ngược sau : S ⇒ rm aABe ⇒rm aAde ⇒rm aAbcde ⇒rm abbcde (Dẫn xuất phải là chuỗi các thay ký hiệu chưa kết thúc phải nhất) Handle Handle chuỗi là chuỗi hợp với vế phải luật sinh và chúng ta thu gọn nó thành vế trái luật sinh đó thì có thể dẫn đến ký hiệu chưa kết thúc bắt đầu Ví dụ 4.13: Xét văn phạm sau: E→E+E E→E*E E → (E) E→ id Chuỗi dẫn xuất phải : E ⇒rm E + E (các handle gạch dưới) ⇒rm E + E * E ⇒rm E + E * id3 ⇒rm E + id2 * id3 ⇒rm id1 + id2 * id3 Cắt tỉa handle (Handle Pruning) Handle pruning là kỹ thuật dùng để tạo dẫn xuất phải đảo ngược từ chuỗi ký hiệu kết thúc w mà chúng ta muốn phân tích Nếu w là câu văn phạm thì w = γn Trong đó, γn là dạng câu phải thứ n dẫn xuất phải mà chúng ta chưa biết S ⇒ γ0 ⇒rm γ1 ⇒rm γ2 ⇒rmγn-1 ⇒rm γn = w Ðể xây dựng dẫn xuất này theo thứ tự ngược lại, chúng ta tìm handle βn γn và thay βn An (An là vế trái luật sinh An → βn) để dạng câu phải thứ n -1 là γn-1 Quy luật trên tiếp tục Nếu ta có dạng câu phải γ0 = S thì phân tích thành công Ví dụ 4.14: Với văn phạm: E → E + E | E * E | (E) | id Và câu nhập: id1 + id2 * id3 , ta có các bước thu gọn câu nhập thành ký hiệu bắt đầu E sau : 84 (86) Dạng câu phải Handle Luật thu gọn id1 + id2 * id3 id1 E → id E + id2 * id3 id2 E → id E + E * id3 id3 E → id E+E*E E*E E→ E*E E+E E+E E→ E+E E Thành công Cài đặt phân tích cú pháp Shift - Reduce Có hai vấn đề cần phải giải chúng ta dùng kỹ thuật phân tích cú pháp này Thứ là định vị chuỗi cần thu gọn dạng câu dẫn phải, và thứ hai là xác định luật sinh nào dùng có nhiều luật sinh chứa chuỗi đó vế phải Cấu tạo: Dùng Stack để lưu các ký hiệu văn phạm Dùng đệm nhập INPUT để giữ chuỗi nhập cần phân tích w Ta dùng ký hiệu $ để đánh dấu đáy Stack và xác định cuối chuỗi nhập Hoạt động: Khởi đầu thì Stack rỗng và w nằm đệm input Bộ phân tích đẩy các ký hiệu nhập vào Stack handle β nằm trên đỉnh Stack Thu gọn β thành vế trái luật sinh nào đó Lặp lại bước và gặp lỗi Stack chứa ký hiệu bắt đầu và đệm input rỗng (thông báo kết thúc thành công) Ví dụ 4.15: Với văn phạm E → E + E | E * E | (E) | id và câu nhập id1 + id2 * id3 Quá trình phân tích cú pháp thực sau: STACK $ INPUT ACTION id1 + id2 * id3 $ Đẩy $ id1 + id2 * id3 $ Thu gọn E → id $E + id2 * id3 $ Đẩy $E+ $ E + id2 $E+E $E+E* $ E + E * id3 id2 * id3 $ Đẩy * id3 $ Thu gọn E → id * id3 $ Đẩy id3 $ Đẩy $ 85 (87) $E+E*E $ Thu gọn E → id $E+E $ Thu gọn E → E * E $ Thu gọn E → E + E $E Chấp nhận V PHÂN TÍCH CÚ PHÁP THỨ BẬC TOÁN TỬ Lớp văn phạm có tính chất không có luật sinh nào có vế phải là ε có hai ký hiệu chưa kết thúc nào nằm kế có thể dễ dàng xây dựng phân tích cú pháp Shift- Reduce hiệu theo lối thủ công Một kỹ thuật phân tích dễ cài đặt gọi là phân tích cú pháp thứ bậc toán tử Quan hệ thứ tự ưu tiên Bảng định nghĩa quan hệ thứ bậc cho sau : Quan hệ Ý nghĩa a <• b a có độ ưu tiên thấp b a =y b a có độ ưu tiên b a •> b a có độ ưu tiên cao b Hình 4.11 - Các quan hệ thứ bậc toán tử Sử dụng quan hệ thứ tự ưu tiên toán tử Các quan hệ ưu tiên này giúp việc xác định handle Trước hết, ta dựa vào các quy tắc sau để xây dựng bảng quan hệ ưu tiên các ký hiệu kết thúc Nếu toán tử θ1 có độ ưu tiên cao θ2 thì θ1 •> θ2 và θ2 <• θ1; (E + E * E + E thì handle là E * E) Nếu toán tử θ1 có độ ưu tiên θ2 thì : θ1 •> θ2 và θ2 •> θ1 các toán tử là kết hợp trái θ1 <• θ2 và θ2 <• θ1 các toán tử là kết hợp phải Ví dụ 4.16: Toán tử + và - có độ ưu tiên và kết hợp trái nên: + •> + ; + •> - ; - •> - ; - •> + Phép toán ↑ kết hợp phải nên ↑ <• ↑ E - E + E ⇒ handle là E - E E ↑ E ↑ E ⇒ handle là E ↑ E (phần cuối) Ví dụ 4.17: Với chuỗi nhập id + id * id 86 (88) Ta có bảng quan hệ thứ bậc các toán tử sau : id id + * $ •> •> •> + <• •> <• •> * <• •> •> •> $ <• <• <• Sử dụng $ để đánh dấu cuối chuỗi và $ <• θ, ∀θ Ta có chuỗi với các quan hệ thứ bậc chèn vào là : $ <• id •> + <• id •> * <• id •> $ Chẳng hạn, <• chèn vào $ bên trái và id vì <• là mục hàng $ và cột id Handle có thể tìm thấy thông qua quá trình sau : Duyệt chuỗi từ trái sang phải gặp •> đầu tiên (trong ví dụ chúng ta là •> sau id đầu tiên) • Sau đó, duyệt ngược lại (hướng sang trái), vượt qua các = (nếu có) gặp a <• (trong ví dụ, chúng ta quét ngược đến $) Handle là chuỗi chứa ký hiệu bên trái •> đầu tiên và bên phải <• gặp bước (2), chứa luôn các ký hiệu chưa kết thúc xen bao quanh (trong ví dụ, handle là id đầu tiên) Với ví dụ trên, sau thu gọn id thành E ta $ E + E * E $ Bỏ các ký hiệu chưa kết thúc E ta $ + * $ Thêm các quan hệ ưu tiên ta $ <• + <• * •> $ Ðiều này chứng tỏ handle là E * E thu gọn thành E Vì các ký hiệu chưa kết thúc không ảnh hưởng gì đến việc phân tích cú pháp, nên chúng ta không cần phải phân biệt chúng Giải thuật 4.5: Phân tích cú pháp thứ bậc toán tử Input: Chuỗi nhập w và bảng các quan hệ thứ bậc Output: Nếu w là chuỗi chuẩn dạng đúng thì cho cây phân tích cú pháp Ngược lại, thông báo lỗi Phương pháp: Khởi đầu, Stack chứa ký hiệu $ và đệm chứa câu nhập dạng w$ Ðặt trỏ ip trỏ tới ký hiệu đầu tiên w$ ; Repeat forever If $ nằm đỉnh Stack và ip đến $ then return 87 (89) Else begin If a <• b a = b then begin Ðẩy b vào Stack; Dịch ip đến ký hiệu đệm; end Else if a •> b then /* thu gọn */ Repeat Lấy ký hiệu trên đỉnh Stack ra; Until Ký hiệu kết thúc trên đỉnh Stack có quan hệ <• với ký hiệu kết thúc vừa lấy ra; else error ( ) End VI BỘ PHÂN TÍCH CÚ PHÁP LR Phần này giới thiệu kỹ thuật phân tích cú pháp từ lên khá hiệu quả, có thể sử dụng để phân tích lớp rộng 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) L (left - to - right): Duyệt chuỗi nhập từ trái sang phải R (rightmost derivation): Xây dựng chuỗi dẫn xuất phải đảo ngược k : Số lượng ký hiệu nhập xét thời điểm dùng để đưa định phân tích Khi không đề cập đến k, chúng ta hiểu ngầm là k = Các ưu điểm phân tích cú pháp LR - Bộ phân tích cú pháp LR có thể xây dựng để nhận biết tất các ngôn ngữ lập trình tạo văn phạm phi ngữ cảnh - Phương pháp phân tích cú pháp LR là phương pháp tổng quát phương pháp chuyên thu gọn không quay lui Nó có thể cài đặt có hiệu các phương pháp chuyên thu gọn khác - Lớp văn phạm có thể dùng phương pháp LR là lớp rộng lớn lớp văn phạm có thể sử dụng phương pháp dự đoán - Bộ phân tích cú pháp LR có thể xác định lỗi cú pháp nhanh duyệt dòng nhập từ trái sang phải Nhược điểm chủ yếu phương pháp này là cần phải thực quá nhiều công việc để xây dựng phân tích cú pháp LR theo kiểu thủ công cho văn phạm ngôn ngữ lập trình điển hình 88 (90) Thuật toán phân tích cú pháp LR a1 INPUT STACK sm an Chương trình phân tích cú pháp LR Xm $ OUTPUT sm-1 Xm-1 action goto s0 Hình 4.12 - Mô hình phân tích cú pháp LR • Stack lưu chuỗi s0X1s1X2s2 Xmsm đó sm nằm trên đỉnh Stack Xi là ký hiệu văn phạm, si là trạng thái tóm tắt thông tin chứa Stack bên nó • Bảng phân tích bao gồm phần: hàm action và hàm goto action[sm, ai] có thể có giá trị : shift s: đẩy s, đó s là trạng thái reduce A→ β: thu gọn luật sinh A→ β accept: Chấp nhận error: Báo lỗi Goto lấy tham số là trạng thái và ký hiệu văn phạm, nó sinh trạng thái Cấu hình (configuration) phân tích cú pháp LR là cặp thành phần, đó, thành phần đầu là nội dung Stack, phần sau là chuỗi nhập chưa phân tích: (s0X1s1X2s2 Xmsm, ai+1 an $) Với sm là ký hiệu trên đỉnh Stack, là ký hiệu nhập tại, cấu hình có sau dạng bước đẩy sau : Nếu action[sm, ai] = Shift s : Thực phép đẩy để cấu hình mới: (s0X1s1X2s2 Xmsm ais, +1 an $) Phép đẩy làm cho s nằm trên đỉnh Stack, ai+1 trở thành ký hiệu hành Nếu action [sm, ai] = Reduce(A → β) thì thực phép thu gọn để cấu hình: (s0X1s1X2s2 Xm - ism - i As, ai +1 an $) 89 (91) Trong đó, s = goto[sm - i, A] và r là chiều dài số lượng các ký hiệu β Ở đây, trước hết 2r phần tử Stack bị lấy ra, sau đó đẩy vào A và s Nếu action[sm, ai] = accept: quá trình phân tích kết thúc Nếu action[sm, ai] = error: gọi thủ tục phục hồi lỗi Giải thuật 4.6 : Phân tích cú pháp LR Input: Một chuỗi nhập w, 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 phân tích lên cho w Ngược lại, thông báo lỗi Phương pháp: Khởi tạo s0 là trạng thái khởi tạo nằm Stack và w$ nằm đệm nhập Ðặt ip vào ký hiệu đầu tiên w$; Repeat forever begin Gọi s là trạng thái trên đỉnh Stack và a là ký hiệu trỏ 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 * | β| ký hiệu 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 luật sinh A → β; end else if action[s, a] = accept then return else error ( ) end Ví dụ 4.18: Hình sau trình bày các hàm action và goto bảng phân tích cú pháp LR cho văn phạm các biểu thức số học đây với các toán tử ngôi + và * (1) E→ E + T (2) E→ T Trạng thái (3) T → T * F (4) T→ F Action id + * ( Goto ) $ s4 s5 s6 E T F acc 90 (92) (5) F → (E) r2 s7 r2 r2 (6) F → id r4 r4 r4 r4 s5 s4 r6 r6 r6 r6 Ý nghĩa: si : chuyển trạng thái i s5 s4 ri : thu gọn luật sinh i s5 s4 acc: accept (chấp nhận) s6 error : khoảng trống r1 s7 r1 r1 10 r3 r3 r3 r3 11 r5 r5 r5 r5 s11 Hình 4.13 - Bảng phân tích cú pháp SLR cho văn phạm ví dụ Với chuỗi nhập id * id + id, các bước chuyển trạng thái trên Stack và nội dung đệm nhập trình bày sau : STACK INPUT ACTION (1) (2) id * id + id $ Reduce by F→ id (3) F (4) T * id + id $ Reduce by T → F * id + id $ Shift (5) T * (6) T * id (7) T * F 10 (8) T (9) E (10) E + (11) E + id (12) E + F (13) E + T (14) E id * id + id $ Shift id + id $ Shift + id $ Reduce by F → id + id $ Reduce by T → T * F + id $ Reduce by E→ T + id $ Shift id $ Shift $ Reduce by F → id $ Reduce by T → F $ Reduce by E→ E + T $ Thành công Văn phạm LR Làm nào để xây dựng bảng phân tích cú pháp LR cho văn phạm đã cho? Một văn phạm có thể xây dựng bảng phân tích cú pháp cho nó gọi là văn phạm LR Có văn phạm phi ngữ cảnh không thuộc lọai LR, 91 (93) nói chung là ta có thể tránh văn phạm này hầu hết các kết cấu ngôn ngữ lập trình điển hình Có khác biệt lớn các văn phạm LL và LR Ðể cho văn phạm là LR(k), chúng ta phải có khả nhận diện xuất vế phải luật sinh ứng với k ký hiệu đọc trước Ðòi hỏi này ít khắt khe so với các văn phạm LL(k) Vì vậy, các văn phạm LR có thể mô tả nhiều ngôn ngữ so với các văn phạm LL Xây dựng bảng phân tích cú pháp SLR Phần này trình bày cách xây dựng bảng phân tích cú pháp LR từ văn phạm Chúng ta đưa phương pháp khác tính hiệu tính dễ cài đặt Phương pháp thứ gọi là "LR đơn giản" (Simple LR - SLR), là phương pháp "yếu" tính theo số lượng văn phạm có thể xây dựng thành công phương pháp này, đây lại là phương pháp dễ cài đặt Ta gọi bảng phân tích cú pháp tạo phương pháp này là bảng SLR và phân tích cú pháp tương ứng là phân tích cú pháp SLR, với văn phạm tương đương là văn phạm SLR a Mục (Item): Cho văn phạm G, mục LR(0) văn phạm là luật sinh G với dấu chấm mục vị trí nào đó vế phải Ví dụ 4.19: Luật sinh A → XYZ có mục sau : A → •XYZ A → X• YZ A → XY• Z A → XYZ• Luật sinh A → ε tạo mục A → • b Văn phạm tăng cường (Augmented Grammar) Giả sử G là văn phạm với ký hiệu bắt đầu S, ta thêm ký hiệu bắt đầu S' và luật sinh S' → S để văn phạm G' gọi là văn phạm tăng cường c Phép toán bao đóng (Closure) Giả sử I là tập các mục văn phạm G thì bao đóng closure(I) là tập các mục xây dựng từ I theo qui tắc sau: Tất các mục I thêm cho closure(I) Nếu A → α • Bβ ∈ closure(I) và B → γ là luật sinh thì thêm B → • γ vào closure(I) nó chưa có đó Lặp lại bước này không thể thêm vào closure(I) Ví dụ 4.20: Xét văn phạm tăng cường biểu thức: E' → E E → E+T|T T → T*F|F 92 (94) F → (E) | id Nếu I là tập hợp 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) E→•T (Luật 2) T→•T*F (Luật 2) T→•F (Luật 2) F → • (E) (Luật 2) F → • id (Luật 2) Chú ý rằng: Nếu B - luật sinh đưa vào closure(I) với dấu chấm mục nằm đầu vế phải thì tất các B - luật sinh đưa vào d Phép toán Goto Goto(I, X), đó I là tập các mục và X là ký hiệu văn phạm là bao đóng tập hợp các mục A → αX•β cho A → α•Xβ ∈ I Cách tính goto(I, X): Tạo tập I' = ∅ Nếu A → α•Xβ ∈ I thì đưa A→ αX•β vào I', tiếp tục quá trình này xét hết tập I Goto(I, X) = closure(I') Ví dụ 4.21: Giả sử I = { E' → E•, E → E • + T } Tính goto (I, +) ? Ta có I' = { 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) e Giải thuật xây dựng họ tập hợp các mục LR(0) văn phạm G' Gọi C là họ tập hợp các mục LR(0) văn phạm tăng cường G' Ta có thủ tục xây dựng C sau : Procedure Item (G') begin C := closure({ S' → •S}); 93 (95) Repeat For Với tập các mục I C và ký hiệu văn phạm X cho goto (I, X) ≠ ∅ và goto(I, X) ∉ C Thêm goto(I, X) vào C; Until Không còn tập hợp mục nào có thể thêm vào C; end; Ví dụ 4.22: Với văn phạm tăng cường G' biểu thức ví dụ 4.20 : Ta xây dựng họ tập hợp mục C văn phạm sau: Closure({E'→ E}): I0 : E'→ • E E→•E+T Goto (I0, id) I5: F → id • Goto (I1, +) I6: E→E+•T E→•T T→•T*F T→•F T→•T*F F → • (E) T→•F F → • id F → • (E) F → • id Goto (I0, E) I1: E'→ E • E→E•+T Goto (I2, *) I7: T → T* • F F → • (E) Goto (I0, T) I2: F → • id E→T• T→T•*F Goto (I4, E) Goto (I0, F) I3: T→F• Goto (I0, ( ) I4: F → (• E) I8: T → (E •) E→E•+T Goto (I6,T) I9: E→E+T• T→T•*F E→•E+T E→•T T→•T* F Goto (I7,F) I10: T → T * F • Goto (I8,) ) I11: F → (E) • T→•F F → • (E) F → • id f Thuật toán xây dựng bảng phân tích SLR 94 (96) Giải thuật 4.7: Xây dựng bảng phân tích SLR 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: Xây dựng C = { I0, I1, , In }, họ tập hợp các mục LR(0) G' Trạng thái i xây dựng từ Ii Các action tương ứng trạng thái i xác định sau: 2.1 Nếu A → α • aβ ∈ Ii và goto (Ii, a) = Ij thì action[i, a] = "shift j" Ở đây a là ký hiệu kết thúc 2.2 Nếu A → α• ∈ Ii thì action[i, a] = "reduce (A → α)", ∀a ∈ FOLLOW(A) Ở đây A không phải là S' 2.3 Nếu S' → S• ∈ Ii thì action[i, $] = "accept" Nếu action đụng độ sinh 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 phân tích cú pháp thất bại trường hợp này Với ký hiệu chưa kết thúc A, goto (Ii,A) = Ij thì goto [i, A] = j Tất các ô không xác định và là “error” Trạng thái khởi đầu phân tích cú pháp xây dựng từ tập các mục chứa S’→ • S Ví dụ 4.23: 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' ví dụ 4.20 trên E' → E (0) E'→ E (4) T→F E →E+T|T (1) E→E+T (5) F → (E) T →T*F|F (2) E→T (6) F → id F → (E) | id (3) T→T*F C = { I0, I1, I11 } FOLLOW(E) = {+, ), $} FOLLOW(T) = {*, +, ), $} FOLLOW(F) = {*, +, ), $} Dựa vào họ tập hợp mục C đã xây dựng ví dụ 4.22, ta thấy: Trước tiên xét tập mục I0 : Mục F → • (E) cho action[0, (] = "shift 4", và mục F → • id cho action[0, id] = "shift 5" Các mục khác I0 không sinh hành động nào Bây xét I1 : Mục E'→ E • cho action[1, $] = "accept", mục E → E • + T cho action[1, +] = "shift 6" Kế đến xét I2 : E → T • 95 (97) 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 bảng phân tích cú pháp SLR đã trình bày hình 4.13 Bảng phân tích xác định giải thuật 4.7 gọi là bảng SLR(1) văn phạm G, phân tích LR sử dụng bảng SLR(1) gọi là phân tích SLR(1) và văn phạm có bảng SLR(1) gọi là văn phạm SLR(1) Mọi văn phạm SLR(1) không mơ hồ Tuy nhiên có văn phạm không mơ hồ không phải là SLR(1) Ví dụ 4.24: Xét văn phạm G với tập luật sinh sau: S→L=R S→R L→*R L → id R→L Ðây là văn phạm không mơ hồ không phải là văn phạm SLR(1) Họ tập hợp các mục C bao gồm: I0 : S' → • S S →•L=R S →•R L →•*R L → • id R→•L I1 : S' → S • I2 : S →L•=R R→ L• I3 : S → R • I4 : L → * • R R→•L L→•*R L → • id I5 : L → id • I6 : S → L = • R R→•L L→•*R L → • id I7 : L → * R• I8 : R → L• I9 : S → L = R• 96 (98) Khi xây dựng bảng phân tích SLR cho văn phạm, xét tập mục I2 ta thấy mục đầu tiên tập này làm cho action[2, =] = "shift 6" Bởi vì = ∈ FOLLOW(R), nên mục thứ hai đặt action[2, =] = "reduce (R → L)" ⇒ Có đụng độ action[2, =] Vậy văn phạm trên không là văn phạm SLR(1) Xây dựng bảng phân tích cú pháp LR chính tắc (Canonical LR Parsing Table) a Mục LR(1) văn phạm G là cặp dạng [A → α•β, a], đó A → αβ là luật sinh, a là ký hiệu kết thúc $ b Thuật toán xây dựng họ tập hợp mục LR(1) Giải thuật 4.8: 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 sau: Function Closure (I); begin Repeat For Mỗi mục [A → α•Bβ,a] I, luật sinh B → γ G' và ký hiệu kết thúc b ∈ FIRST (βa) cho [B → • γ, b] ∉ I Thêm [B → •γ, b] vào I; Until Không còn mục nào có thể thêm cho I 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] cho [A → α•Xβ, a]∈ I; return Closure(J); end; Procedure Items (G'); begin C := Closure ({[S' → •S, $]}) Repeat For Mỗi tập các mục I C và ký hiệu văn phạm X cho goto(I, X) ≠ ∅ và goto(I, X) ∉ C 97 (99) 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ụ 4.25: Xây dựng bảng LR chính tắc cho văn phạm tăng cường G' có chứa các luật sinh sau : S' Æ S (1) S Æ L = R (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, $} I0 : S' → • S, $ Closure (S' Æ •S, $) S → • L = R, $ S → • R, $ Goto (I4,R) I7 : L → * R•, = | $ L → • * R, = | $ L → • id, = | $ Goto (I4, L) I8 : R→ L•, = | $ R → • L, $ Goto (I6,R) I9 : S → L = R•, $ Goto (I0,S) I1 : S' → S •, $ Goto (I6,L) I10 : R → L•, $ Goto (I0, L) I2 : S → L • = R, $ R → L •, $ Goto (I6,*) I11 : L → * • R, $ R → • L, $ Goto (I 0,R) I3: S → R •, $ L → • * R, $ R → • id, $ Goto (I0,*) I4: L → * • R, = | $ R Æ • L, = | $ Goto (I6, id) I12 : L → id •, $ L → • * R, = | $ R → • id, = | $ Goto (I0,id) I5 : L → id •, = | $ Goto (I11,R) I13 : R → * R•, $ Goto (I11,L) ≡ I10 98 (100) Goto (I2,=) I6 : S → L = • R, $ Goto (I11,*) ≡ I11 R → • L, $ Goto (I11,id) ≡ I12 L → • * R, $ L → • id, $ c Thuật toán xây dựng bảng phân tích cú pháp LR chính tắc Giải thuật 4.9: 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: Xây dựng C = { I0, I1, In } là họ tập hợp mục LR(1) Trạng thái thứ i xây dựng từ Ii Các action tương ứng trạng thái i xác đị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ó đụng độ 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 thất bại Nếu goto(Ii, A) = Ij thì goto[i,A] = j Tất các ô không xác định và là "error" Trạng thái khởi đầu phân tích cú pháp xây dựng từ tập các mục chứa [S' → •S,$] Bảng phân tích xác định giải thuật 4.9 gọi là bảng phân tích LR(1) chính tắc văn phạm G, phân tích LR sử dụng bảng LR(1) gọi là phân tích LR(1) chính tắc và văn phạm có bảng LR(1) không có các action đa trị thì gọi là văn phạm LR(1) Ví dụ 4.26: 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 = * id s4 s5 S L R r5 s6 r2 s4 $ acc Goto s5 r4 99 (101) s11 r3 r5 s12 r1 10 r5 11 s11 s12 12 r4 13 r3 10 10 13 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à văn phạm LR(1), với văn phạm SLR(1), phân tích cú pháp LR chính tắc có thể có nhiều trạng thái so với phân tích cú pháp SLR cho văn phạm đó Xây dựng bảng phân tích cú pháp LALR Phần này giới thiệu phương pháp cuối cùng để xây dựng phân tích cú pháp LR kỹ thuật LALR (Lookahead-LR), phương pháp này thường sử dụng thực tế vì bảng LALR thu nói chung là nhỏ 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 ngôn ngữ lập trình có thể diễn tả thuận lợi văn phạm LALR a Hạt nhân (core) tập hợp mục LR(1) Một tập hợp mục LR(1) có dạng {[ A → α•β, a]}, đó A → αβ là 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 → α •β} Trong họ tập hợp các mục LR(1) C = { I0, I1, , In } có thể có các tập hợp các mục có chung hạt nhân Ví dụ 4.27: Trong ví dụ 4.25, ta thấy họ tập hợp mục có 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ật 4.10: 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: Xây dựng họ tập hợp các mục LR(1) C = { I0, I1, , In } 100 (102) Với hạt nhân tồn tập các mục LR(1) tìm trên tất các tập hợp có cùng hạt nhân này và thay các tập hợp này hợp chúng Ðặt C' = { I0, I1, , Im } là kết thu từ C 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 xây dựng từ Ji theo cách thức giải thuật 4.9 Nếu có đụng độ các action thì giải thuật xem thất bại và ta nói văn phạm không phải là văn phạm LALR(1) Bảng goto xây dựng sau Giả sử J = I1 ∪ I2 ∪ ∪ Ik Vì I1, I2, Ik có chung hạt nhân nên goto (I1,X), goto (I2,X), , goto (Ik,X) có chung hạt nhân Ðặt K hợp tất các tập hợp có chung hạt nhân với goto (I1,X) ⇒ goto(J, X) = K Ví dụ 4.28: Với ví dụ trên, ta có họ tập hợp mục C' sau C' = {I0, I1, I2, I3, I411, I512, I6, I713, I810, I9 } S' → • S, $ I0 : closure (S' Æ •S, $) : S → • L = R, $ I512 = Goto (I0,id), Goto (I6,id) : L → id •, = | $ S → • R, $ L → • * R, = I6 = Goto(I2,=) : L → • id, = S → L = • R,$ R → • L, $ R → • L, $ L → • * R, $ I1 = Goto (I0,S) : S' → S •, $ I2 = Goto (I0, L) : S → L • = R, $ R → L •, $ I3 = Goto (I 0,R) : S → R • L → • id, $ I713 = Goto(I411, R) : L → * R•, = | $ I810 = Goto(I411, L), Goto(I6, L): R → L•, = | $ I411 = Goto (I0,*), Goto (I6,*) : L → * • R, = | $ R → • L, = | $ I9 = Goto(I6, R) : S → L = R•, $ L → • * R, = | $ R → • id, = | $ Ta có thể xây dựng bảng phân tích cú pháp LALR cho văn phạm sau: 101 (103) State Action = * id s411 s512 Goto $ L R 810 713 810 acc s6 r2 411 512 S r4 r4 s411 s512 713 r3 r3 810 r5 r5 r1 Hình 4.15 - Bảng phân tích cú pháp LALR Bảng phân tích tạo giải thuật 4.10 gọi là bảng phân tích LALR cho văn phạm G Nếu 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' gọi là họ tập hợp mục LALR(1) VII SỬ DỤNG CÁC VĂN PHẠM MƠ HỒ Như chúng ta đã nói trước đây văn phạm mơ hồ không phải là LR Tuy nhiên có số văn phạm mơ hồ lại có ích cho việc đặc tả và cài đặt ngôn ngữ Chẳng hạn văn phạm mơ hồ cho kết cấu biểu thức đặc tả cách ngắn gọn và tự nhiên văn phạm không mơ hồ nào khác Văn phạm mơ hồ còn dùng việc tách biệt các kết cấu cú pháp thường gặp cho quá trình tối ưu hóa Với văn phạm mơ hồ, chúng ta có thể đưa thêm các luật sinh vào văn phạm Mặc dù các văn phạm là đa nghĩa, trường hợp, chúng ta đưa thêm các quy tắc khử mơ hồ (disambiguating rule), phép chọn cây phân tích cú pháp cho câu nhập Theo cách này, đặc tả ngôn ngữ tổng thể là đơn nghĩa Sử dụng độ ưu tiên và tính kết hợp các toán tử để giải đụng độ Xét văn phạm biểu thức số học với hai toán tử + và * : E Æ E + E | E * E | (E) |id (1) Ðây là văn phạm mơ hồ vì nó không xác định độ ưu tiên và tính kết hợp các tóan tử + và * Trong đó ta có văn phạm tương đương không mơ hồ cho biểu thức có dạng sau: 102 (104) EÆE+T|T TÆT*F|F (2) F Æ (E) | id Văn phạm này xác định + có độ ưu tiên thấp * và hai toán tử kết hợp trái Tuy nhiên có lý để chúng ta sử dụng văn phạm (1) không phải là (2): Dễ dàng thay đổi tính kết hợp và độ ưu tiên + và * mà không phá hủy các luật sinh và số các trạng thái phân tích (như ta thấy sau này) Bộ phân tích cho văn phạm (2) thời gian thu gọn các luật sinh E Æ T và T Æ F Hai luật sinh này không nói lên tính kết hợp và độ ưu tiên Nhưng với văn phạm (1) thì làm nào để tránh đụng độ? Trước hết chúng ta hãy thành lập sưu tập C các tập mục LR(0) văn phạm tăng cường nó I0: E'→ • E Goto(I2,E) I6: E'→ (E •) E→•E+E E→E•+E E→•E*E E→E•*E E → • (E) Goto(I2,() ≡ I2 E → • id Goto(I2,id) ≡ I3 Goto(I0,E) I1: E'→ E • Goto(I4,E) I7: E→E+E• E→E•+E E→E•+E E→E•*E E→E•*E Goto(I4,( ) ≡ I2 Goto(I0,() I2: E → (• E) Goto(I4,id)≡ I3 E→•E+E E → • E* E Goto(I5,E) I8: E→E*E• E → • (E) E→E•+E E → • id E→E•*E Goto(I5,() ≡ I2 Goto(I0,id) I3: E → id• Goto(I5,id)≡ I3 Goto(I1,+ ) I4: E → E + • E Goto(I6,)) I9: E → (E) • E→•E+E E→•E* E Goto(I6,+) ≡ I4 E → • ( E) Goto(I6,*) ≡ I5 103 (105) Goto(I7,+) ≡ I4 E → • id Goto(I7,*) ≡ I5 Goto(I1,*) I5: E → E * • E Goto(I8,+) ≡ I4 E→•E+E Goto(I8,*) ≡ I5 E→•E* E E → • ( E) E → • id Bảng phân tích SLR đụng độ xây dựng sau : Trạng thái Action id s3 * ( ) $ E s2 s4 + Goto s5 acc s3 r4 r4 r4 r4 s3 s2 s3 s2 s4 s5 s9 s4 / r1 s5 / r1 r1 r1 s4 / r2 s5 / r2 r2 r2 r3 r3 r3 r3 Hình 4.16 - Bảng phân tích cú pháp SLR đụng độ Nhìn vào bảng SLR hình trên, ta thấy có sụ đụng độ action [7, +] và action [7,*]; action [8, +] và action [8,*] Chúng ta giải đụng độ này quy tắc kết hợp và độ ưu tiên các toán tử Xét chuỗi nhập id + id * id Stack Input Output id + id * id $ id + id * id $ Shift s3 0E1 + id * id $ Reduce by E Æ id 0E1+4 id * id $ Shift s4 E + id * id $ Shift s3 0E1+4E7 * id $ Reduce by E Æ id 104 (106) Bây đến ô đụng độ action[7, *] nên lấy r1 hay s5? Lúc này chúng ta đã phân tích qua phần chuỗi id * id Nếu ta chọn r1 tức là thu gọn luật sinh E Æ E + E, có nghĩa là chúng ta đã thực phép cộng trước Do ta muốn tóan tử * có độ ưu tiên cao + thì phải chọn s5 Nếu chuỗi nhập là id + id + id thì quá trình phân tích văn phạm dẫn đến hình trạng là : Stack Output 0E1+4E7 + id $ Sẽ phải xét action [7, +] nên chọn r1 hay s4? Nếu ta chọn r1 tức là thu gọn luật sinh E Æ E + E tức là + thực trước hay toán tử + có kết hợp trái => action [7, +] = r1 Một cách tương tự ta quy định phép * có độ ưu tiên cao + và phép * kết hợp trái thì action [8, *] = r2 vì * kết hợp trái (xét chuỗi id * id * id) Action [8,+] = r2 vì toán tử * có độ ưu tiên cao + (trường hợp xét chuỗi id * id + id) Sau đã giải đụng độ đó ta có bảng phân tích SLR đơn giản bảng phân tích văn phạm tương đương (2) (chỉ sử dụng 10 trạng thái thay vì 12 trạng thái) Tương tự, ta có thể xây dựng bảng phân tích LR chính tắc và LALR cho văn phạm (1) Giải trường hợp văn phạm mơ hồ IF THEN ELSE Xét văn phạm cho lệnh điều kiện: Stmt Æ if expr then stmt else stmt | if expr then stmt | other Ðể đơn giản ta viết i thay cho if expr then, S thay cho stmt, e thay cho else và a thay cho other, ta có văn phạm viết lại sau : S’ Æ S S Æ iS eS (1) S Æ iS (2) SÆa (3) Họ tập hợp mục C các tập mục LR(0) là: 105 (107) I0 : S' → • S, S → iS• eS Goto (I2, S) I4: S → • iSeS S → iS• S → • iS S →•a Goto (I4,e) S → iSe• S I5 : S → • iSeS S → • iS Goto (I0,S) I1 : S' → S • S →•a Goto (I0,i) I2 : S → i • SeS S →i• S Goto (I5,S) S → • iSeS Goto(I2,i) ≡ I2 S → • iS Goto(I2,a) ≡ I3 S →•a Goto(I5,i) ≡ I2 I6 : S → iSeS• Goto(I5,a) ≡ I3 Goto (I0,a) I3: S → a • Ta xây dựng bảng phân tích SLR đụng độ sau: Trạng Action thái i e Goto a s3 s2 $ acc s3 s2 r3 r3 s5| r2 r2 S s3 s2 6 r1 Hình 4.17 - Bảng phân tích cú pháp LR cho văn phạm if - else Ðể giải đụng độ action[4, e] Trường hợp này xảy tình trạng chuỗi ký hiệu if expr then stmt nằm Stack và else là ký hiệu nhập hành Sử dụng nguyên tắc kết hợp else với then chưa kết hợp gần trước đó nên ta phải Shift else vào Stack để kết hợp với then nên action [4, e] = s5 Ví dụ 4.29: Với chuỗi nhập i i a e a (if expr1 then if expr2 then a1 else a2) 106 (108) Stack Input Output iiaea$ 0i2 i a e a $ Shift s2 a e a $ Shift s2 0i2i2 e a $ Shift s3 0i2i2a3 a $ Reduce by S Æ a 0i2i2S4 0i2i2S4e5 $ Shift s5 0i2i2S4e5a3 $ Shift s3 0i2i2S4e5S6 $ Reduce by S Æ a 0i2S4 $ Reduce by S Æ iS eS 0s1 $ Reduce by S Æ iS VIII BỘ SINH BỘ PHÂN TÍCH CÚ PHÁP Phần này trình bày cách dùng sinh phân tích cú pháp (parser generator) hỗ trợ cho việc xây dựng kỳ đầu trình biện dịch Một sinh phân tích cú pháp là YACC (Yet Another Compiler - Compiler) Phiên đầu tiên Yacc S.C.Johnson tạo và Yacc cài đặt lệnh hệ UNIX và đã dùng để cài đặt cho hàng trăm trình biên dịch Bộ sinh thể phân tích cú pháp Yacc Đặc tả Yacc Translate.y Y.tab.c Input Yacc Compiler C Compiler a.out Y.tab.c a.out Output Hình 4.18 - Tạo chương trình dịch input / output với Yacc Một chương trình dịch có thể xây dựng nhờ Yacc phương thức minh họa hình 4.18 trên Trước tiên, cần chuẩn bị tập tin, chẳng hạn là translate.y, chứa đặc tả Yacc chương trình dịch Lệnh yacc translate.y hệ UNIX biến đổi tập tin translate.y thành chương trình C có tên là y.tab.C phương pháp LALR đã trình bày trên Chương trình y.tab.C là biểu diễn phân tích cú pháp LALR viết ngôn ngữ C cùng với các thủ tục C khác có thể người sử dụng chuẩn bị Bằng cách dịch y.tab.C cùng với thư viện ly chứa chương trình phân tích cú pháp LR nhờ lệnh cc y.tab.C - ly chúng ta thu chương trình đối tượng a.out thực quá trình dịch đặc tả chương trình Yacc ban đầu Nếu cần thêm các thủ tục khác, chúng có thể biên dịch tải vào y.tab.C giống chương trình C khác 107 (109) Ðặc tả YACC Một chương trình nguồn Yacc bao gồm phần: Phần khai báo %% Các luật dịch %% Các thủ tục Ví dụ 4.30: Ðể minh họa việc chuẩn bị chương trình nguồn Yacc, chúng ta hãy xây dựng chương trình máy tính bỏ túi đơn giản, đọc biểu thức số học, ước lượng in giá trị số nó Chúng ta xây dựng văn phạm sau : E→E+T|T T→T*F|F F → (E) | digit Token digit là ký hiệu số từ đến Một chương trình Yacc dành cho văn phạm này sau : %{ # include < ctype.h> %} % token DIGIT %% line : expr '\n' { print ("%d\n", $1); } ; expr : expr | '+' term { $$ = $1 + $3; } '* ' factor { $$ = $1 * $3; } term ; term : term | factor ; factor: | '(' expr ')' { $$ = $2 ; } DIGIT ; 108 (110) %% yylex ( ) { int c c = getchar ( ); if (isdigit(c)) { yyval = c -'0'; return DIGIT; } return c; } Phần khai báo có thể bao gồm phần nhỏ: - Khai báo C đặt nằm cặp dấu %{ và }% Những khai báo này sử dụng phần và phần - Khai báo các token (DIGIT là token) Các token khai báo đây dùng phần và phần Phần luật dịch: Mỗi luật dịch là luật sinh kết hợp với hành vi ngữ nghĩa Mỗi luật sinh có dạng <vế trái> → <alt1> | <alt2> | <altn> mô tả Yacc : <vế trái> : <alt1> { hành vi ngữ nghĩa } | <alt2> { hành vi ngữ nghĩa } | <altn> { hành vi ngữ nghĩa n } ; Trong luật sinh, các ký tự nằm cặp dấu nháy đơn 'c' là ký hiệu kết thúc c, chuỗi chữ cái và chữ số không nằm cặp dấu nháy đơn và không khai báo là token là ký hiệu chưa kết thúc Hành vi ngữ nghĩa Yacc là chuỗi các lệnh C có dạng: $$ 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 $I Giá trị thuộc tính kết hợp với ký hiệu văn phạm thứ i (kết thúc chưa) vế phải 109 (111) Phần thủ tục: Là các thủ tục viết ngôn ngữ C Ở đây phân tích từ vựng yylex( ) sinh cặp gồm token và giá trị thuộc tính kết hợp với nó Các token trả phải khai báo phần khai báo Giá trị thuộc kết hợp với token giao tiếp với phân tích cú pháp thông qua biến yylval (một biến định nghĩa yacc) Chú ý: Chúng ta có thể kết hợp Lex và Yacc cách dùng "lex.yy.c" thay cho thủ tục yylex( ) phần thứ #include 110 (112) BÀI TẬP CHƯƠNG IV 4.1 Cho văn phạm G chứa các luật sinh sau: S → ( L)⏐ a L→L,S|S a) Hãy các thành phần văn phạm phi ngữ cảnh cho G b) Viết văn phạm tương đương sau loại bỏ đệ quy trái c) Xây dựng phân tích cú pháp dự đoán cho văn phạm d) Hãy dùng phân tích cú pháp đã xây dựng để vẽ cây phân tích cú pháp cho các câu nhập sau: i) (a, a) ii) (a, (a, a)) iii) (a, (a, a), (a, a))) e) Xây dựng dẫn xuất trái, dẫn xuất phải cho câu phần d f) Hãy cho biết ngôn ngữ văn phạm G sinh ? 4.2 Cho văn phạm G chứa các luật sinh sau: S → aSbS⏐ bSaS | ε a) Chứng minh văn phạm này là mơ hồ cách xây dựng chuỗi dẫn xuất trái khác cho cùng câu nhập abab b) Xây dựng các chuỗi dẫn xuất phải tương ứng cho câu nhập abab c) Vẽ các cây phân tích cú pháp tương ứng d) Văn phạm này sinh ngôn ngữ gì ? e) Xây dựng phân tích cú pháp đệ quy lùi cho văn phạm trên Có thể xây dựng phân tích cú pháp dự đoán cho văn phạm này không ? 4.3 Cho văn phạm G chứa các luật sinh sau: bexpr → bexpr or bterm | bterm bterm → bterm and bfactor | bfactor bfactor → not bfactor | (bexpr) | true | false a) Hãy xây dựng phân tích cú pháp dự đoán cho văn phạm G b) Xây dựng cây phân tích cú pháp cho câu : not ( true and false ) c) Chứng minh văn phạm này sinh toàn các biểu thức boole 111 (113) d) Văn phạm G có là văn phạm mơ hồ không ? Tại ? e) Xây dựng phân tích cú pháp SLR cho văn phạm 4.4 Cho văn phạm G chứa các luật sinh sau: R → R + R | RR | R* | (R) | a | b a) Chứng minh văn phạm này sinh biểu thức chính quy trên các ký hiệu a và b b) Chứng tỏ đây là văn phạm mơ hồ c) Xây dựng văn phạm không mơ hồ tương đương với thứ tự ưu tiên các phép tóan giảm dần sau : phép bao đóng, phép nối kết, phép hợp d) Vẽ cây phân tích cú pháp hai văn phạm trên cho câu nhập : a + b * c e) Xây dựng phân tích cú pháp dự đoán từ văn phạm không mơ hồ f) Xây dựng bảng phân tích cú pháp SLR cho văn phạm G Ðề nghị quy tắc giải đụng độ cho các biểu thức chính quy phân tích cách bình thường 4.5 Văn phạm sau đây là đề nghị điều chỉnh tính mơ hồ cho văn phạm chứa câu lệnh if - then - else: Stmt → if expr then stmt | matched_stmt Matched_Stmt → if expr then matched_stmt else stmt | other Chứng minh văn phạm này mơ hồ 4.6 Thiết kế văn phạm cho các ngôn ngữ sau Ngôn ngữ nào là chính quy? a) Tập tất các chuỗi và cho số có ít số sau nó b) Các chuỗi và với số số số số c) Các chuỗi và với số số không số số d) Các chuỗi và không chứa chuỗi 001 chuỗi 4.7 Cho văn phạm G chứa các luật sinh sau : S → aSa | aa Xây dựng phân tích cú pháp đệ quy lùi cho văn phạm với yêu cầu phải thử khả triển aSa trước aa 112 (114) 4.8 Cho văn phạm G chứa các luật sinh sau: S → aAB A → Abc | b B→ d a) Xây dựng phân tích cú pháp dự đoán cho văn phạm b) Hãy dùng phân tích cú pháp đã xây dựng để phát sinh cây phân tích cú pháp cho câu nhập: abbcd 4.9 Cho văn phạm G chứa các luật sinh sau: E → E or T | T T → T and F | F F → ( E) | not F | id a) Hãy xây dựng phân tích cú pháp dự đoán cho văn phạm b) Vẽ cây phân tích cú pháp cho câu nhập : id and not ( id or id ) 4.10 Cho văn phạm G chứa các luật sinh sau: S → AB A → Ab | a B → cB | d a) Xây dựng phân tích cú pháp thứ tự ưu tiên cho văn phạm b) Hãy dùng phân tích cú pháp đã xây dựng để phát sinh cây phân tích cú pháp cho câu nhập: abccd 4.11 Cho văn phạm G: S→D•D|D D → DB | B B→0|1 a) Xây dựng phân tích cú pháp thứ tự ưu tiên cho văn phạm b) Hãy dùng phân tích cú pháp đã xây dựng để phát sinh cây phân tích cú pháp cho câu nhập: 101•101 4.12 Cho văn phạm G Assign → id : = exp Exp → Exp + Term | Term 113 (115) Term → Term * Factor | Factor Factor → id | ( Exp ) a) Xây dựng phân tích cú pháp thứ tự ưu tiên cho văn phạm b) Hãy dùng phân tích cú pháp đã xây dựng để phát sinh cây phân tích cú pháp cho câu nhập: id : = id + id * id 4.13 Cho văn phạm mơ hồ sau: S → AS | b A → SA | a a) Xây dựng họ tập hợp mục LR(0) cho văn phạm này b) Xây dựng bảng phân tích cú pháp SLR c) Thực quá trình phân tích cú pháp SLR khả triển cho chuỗi nhập : abab d) Xây dựng bảng phân tích cú pháp chính tắc e) Xây dựng bảng phân tích cú pháp LALR 4.14 Cho văn phạm G sau: E→E+T|T T → TF | F F→F*|a|b a) Xây dựng bảng phân tích cú pháp SLR cho văn phạm này b) Thực quá trình phân tích cú pháp SLR cho chuỗi nhập : b + ab* a c) Xây dựng bảng phân tích cú pháp LALR 4.15 Chứng tỏ văn phạm sau đây: S → Aa | bAc | dc | bda A→d là LALR(1) không phải SLR(1) 4.16 Cho văn phạm G sau: E → E sub R | E sup E | { E } | c R → E sup E | E a) Xây dựng bảng phân tích cú pháp SLR cho văn phạm này b) Ðề nghị quy tắc giải đụng độ để các biểu thức text có thể phân tích cách bình thường 114 (116) 4.17 Viết chương trình Yacc nhận chuỗi input là các biểu thức số học, sinh output là chuỗi biểu thức hậu tố tương ứng 4.18 Viết chương trình Yacc nhận biểu thức chính quy làm chuỗi input và sinh output là cây phân tích cú pháp nó 115 (117) CHƯƠNG V DỊCH TRỰC TIẾP CÚ PHÁP Nội dung chính: Khi viết chương trình ngôn ngữ lập trình nào đó, ngoài việc quan tâm đến cấu trúc chương trình (cú pháp – văn phạm), ta còn phải chú ý đến ý nghĩa chương trình Như vậy, thiết kế trình biên dịch, ta không chú ý đến văn phạm mà còn chú ý đến ngữ nghĩa Chương trình bày các cách biểu diễn ngữ nghĩa chương trình Mỗi ký hiệu văn phạm kết hợp với tập các thuộc tính – các thông tin Mỗi luật sinh kết hợp với tập các luật ngữ nghĩa – các quy tắc xác định trị các thuộc tính Việc đánh giá các luật ngữ nghĩa sử dụng để thực công việc nào đó tạo mã trung gian, lưu thông tin vào bảng ký hiệu, xuất các thông báo lỗi, v.v Ta thấy rõ việc đánh giá này các chương sau: 6, 8, Hai cách để kết hợp các luật sinh với các luật ngữ nghĩa trình bày chương là: Định nghĩa trực tiếp cú pháp và Lược đồ dịch Ở mức quan niệm, cách sử dụng định nghĩa trực tiếp cú pháp lược đồ dịch, ta phân tích dòng thẻ từ, xây dựng cây phân tích cú pháp và duyệt cây cần để đánh giá các luật ngữ nghĩa các nút cây Mục tiêu cần đạt: Sau học xong chương này, sinh viên phải nắm được: • Các cách kết hợp các luật sinh với các luật ngữ nghĩa: Định nghĩa trực tiếp cú pháp và Lược đồ dịch • Biết cách thiết kế chương trình – dịch dự đoán - thực công việc nào đó từ lược đồ dịch hay từ định nghĩa trực tiếp cú pháp xác định Tài liệu tham khảo: [1] Compilers : Principles, Technique and Tools - Alfred V.Aho, Jeffrey D.Ullman - Addison - Wesley Publishing Company, 1986 [2] Modern Compiler Implementation in C - Andrew W Appel - Cambridge University Press, 1997 I ÐỊNH NGHĨA TRỰC TIẾP CÚ PHÁP Ðịnh nghĩa trực tiếp cú pháp là tổng quát hóa văn phạm phi ngữ cảnh, đó ký hiệu văn phạm kết hợp với tập các thuộc tính Cây phân tích cú pháp có trình bày giá trị các thuộc tính nút gọi là cây chú thích Khái niệm định nghĩa trực tiếp cú pháp Trong định nghĩa trực tiếp cú pháp, luật sinh A → α kết hợp tập luật ngữ nghĩa có dạng b := f (c1, c2, , ck) đó f là hàm và : 116 (118) 1- b là thuộc tính tổng hợp A và c1, c2, , ck là các thuộc tính các ký hiệu văn phạm luật sinh Hoặc 2- b là thuộc tính kế thừa các ký hiệu văn phạm vế phải luật sinh và c1, c2, , ck là các thuộc tính các ký hiệu văn phạm luật sinh Ta nói b phụ thuộc c1, c2, , ck Thuộc tính tổng hợp • Là thuộc tính mà giá trị nó nút trên cây phân tích cú pháp tính từ giá trị thuộc tính các nút nó • Ðịnh nghĩa trực tiếp cú pháp sử dụng các thuộc tính tổng hợp gọi là định nghĩa S _ thuộc tính • Cây phân tích cú pháp định nghĩa S_ thuộc tính có thể chú thích từ lên trên Ví dụ 5.1: Xét định nghĩa trực tiếp cú pháp Luật sinh Luật ngữ nghĩa L Æ En print(E.val) E Æ E1 + T E.val := E1.val + T.val EÆT E.val := T.val T Æ T1 * F T.val := T1.val * F.val TÆF T.val := F.val F Æ (E) F.val := E.val F Æ digit F.val := digit.lexval Hình 5.1 - Ðịnh nghĩa trực tiếp cú pháp cho máy tính tay đơn giản Định nghĩa này kết hợp thuộc tính tổng hợp có giá trị nguyên val với ký hiệu chưa kết thúc E, T và F Token digit có thuộc tính tổng họp lexval với giả sử giá trị thuộc tính này cung cấp phân tích từ vựng Ðây là định nghĩa S_thuộc tính Với biểu thức * + 4n (n là ký hiệu newline) có cây chú thích sau: L E.val = 19 E.val = 15 + * F.val = digit.lexval = T.val = F.val = T val = 15 T.val = n F val = digit.lexval = digit.lexval = 117 (119) Hình 5.2- Cây chú thích cho biểu thức 3* 5+4n Thuộc tính kế thừa • Là thuộc tính mà giá trị nó xác định từ giá trị các thuộc tính các nút cha anh em nó • Nói chung ta có thể viết định nghĩa trực tiếp cú pháp thành định nghĩa S_ thuộc tính Nhưng số trường hợp, việc sử dụng thuộc tính kế thừa lại thuận tiện vì tính tự nhiên nó Ví dụ 5.2: Xét định nghĩa trực tiếp cú pháp sau cho khai báo kiểu cho biến: Luật sinh Luật ngữ nghĩa D Æ TL L.in := T.type T Æ int T.type := integer T Æ real T.type := real L Æ L1, id L1.in := L.in; addtype (id.entry, L.in) L Æ id addtype (id.entry, L.in) Hình 5.3 - Ðịnh nghĩa trực tiếp cú pháp với thuộc tính kế thừa L.in type là thuộc tính tổng hợp kết hợp với ký hiệu chưa kết thúc T, giá trị nó xác định từ khóa khai báo Bằng cách sử dụng thuộc tính kế thừa in kết hợp với ký hiệu chưa kết thúc L chúng ta xác định kiểu cho các danh biểu và dùng thủ tục addtype đưa kiểu này vào bảng ký hiệu tương ứng với danh biểu Ví dụ 5.3: Xét phép khai báo: real id1, id2, id3 Ta có cây chú thích: D L.in = real T type = real real , id3 L in = real , L in real id2 id1 Hình 5.4- Cây phân tích cú pháp với thuộc tính kế thừa in nút gán nhãnL 118 (120) Ðồ thị phụ thuộc • Ðồ thị phụ thuộc là đồ thị có hướng mô tả phụ thuộc các thuộc tính mỗt nút cây phân tích cú pháp • Cho cây phân tích cú pháp thì đồ thị phụ thuộc tương ứng xây dựng theo giải thuật sau: FOR nút n cây phân tích cú pháp DO FOR với thuộc tính a ký hiệu văn phạm n DO Xây dựng nút đồ thị phụ thuộc cho a FOR với nút n trên cây phân tích cú pháp DO FOR với luật ngữ nghĩa dạng b = f(c1, c2, , ck) kết hợp với luật sinh dùng nút n DO FOR i:=1 TO k DO Xây dựng cạnh từ nút cho ci đến nút cho b Ví dụ 5.4: Với định nghĩa S_ thuộc tính E Æ E1 + E2 E.val := E1.val + E2.val Ta có đồ thị phụ thuộc: E E1 val + val E2 val Hình 5.5- E.val tổng hợp từ E1.val và E2.val Ví dụ 5.5: Dựa vào định nghĩa trực tiếp cú pháp ví dụ 5.2, ta có đồ thị phụ thuộc khai báo real id1, id2, id3 D T in L real , in L id3 entry , in L 10 id2 entry entry id1 Hình 5.6- Ðồ thị phụ thuộc cho cây phân tích cú pháp hình 5.4 119 (121) Chú ý: Với luật ngữ nghĩa dạng b = f( c1, c2, , ck) có chứa lời gọi thủ tục thì chúng ta tạo thuộc tính tổng hợp giả Trong ví dụ chúng ta là nút 6, 8, 10 II XÂY DỰNG CÂY CÚ PHÁP • Cây cú pháp (syntax - tree) là dạng rút gọn 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 Ví dụ với luật sinh S ( if B then S1 else S2 biểu diễn cây cú pháp: if - then - else B S1 S2 • Một kiểu rút gọn khác cây cú pháp là chuỗi các luật sinh đơn rút gọn lại Chẳng hạn ta có: + E * rút gọn từ E + T T T * id F F id id Xây dựng cây cú pháp cho biểu thức Tương tự việc dịch biểu thức thành dạng hậu tố Xây dựng cây cho biểu thức cách tạo nút cho toán hạng và toán tử Con nút toán tử là gốc cây biểu diễn cho biểu thức toán hạng toán tử đó Mỗi nút có thể cài đặt mẩu tin có nhiều trường Trong nút toán tử, có trường toán tử là nhãn nút, các trường còn lại chứa 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: mknode(op, left, right): Tạo nút toán tử có nhãn là op và hai trường chứa trỏ, trỏ tới trái left và phải right 120 (122) mkleaf(id, entry): Tạo nút lá với nhãn là id và trường chứa trỏ entry, trỏ tới ô bảng ký hiệu mkleaf(num,val): Tạo nút lá với nhãn là num và trường val, giá trị số Ví dụ 5.6: Ðể xây dựng cây cú pháp cho biểu thức: a - + c ta dùng 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 xây dựng từ lên entrya là trỏ, trỏ tới ô a bảng ký hiệu entryc là trỏ, trỏ tới ô c bảng ký hiệu + id id entryc num entrya Hình 5.7- Cây cú pháp cho biểu thức a - + c Xây dựng cây cú pháp từ định nghĩa trực tiếp cú pháp Căn 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 cây cú pháp Ví dụ 5.7: Ðị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 E Æ E1 + T E.nptr := mknode(‘+’, E1.nptr, T.nptr) E Æ E1 - T E.nptr := mknode(‘-’, E1.nptr, T.nptr) EÆT E.nptr := T.nptr T Æ (E) T.nptr := E.nptr T Æ id T nptr := mkleaf(id, id.entry) T Æ num T.nptr := mkleaf(num, num.val) Hình 5.8 - Ðịnh nghĩa trực tiếp cú pháp để tạo cây cú pháp cho biểu thức Các nút trên cây phân tích cú pháp có nhãn là các ký hiệu chưa kết thúc E và T sử dụng thuộc tính tổng hợp nptr để lưu trỏ trỏ tới nút trên cây cú pháp 121 (123) Với biểu thức a - + c ta có cây phân tích cú pháp (biểu diễn đường chấm) tronh hHình 5.9 122 (124) E.nptr E.nptr E.npt T.nptr + id T.nptr - T.nptr num id + - id id num entryc entrya Hình 5.9 - Xây dựng cây cú pháp cho a - + c Luật ngữ nghĩa cho phép tạo cây cú pháp Cây cú pháp có ý nghĩa mặt cài đặt còn cây phân tích cú pháp có ý nghĩa mặt logic Ðồ thị có hướng không tuần hoàn cho biểu thức (Directed Acyclic Graph - DAG) DAG giống cây cú pháp, nhiên cây cú pháp các biểu thức giống biểu diễn lặp lại còn DAG thì không Trong DAG, nút có thể có nhiều “cha” Ví dụ 5.8: Cho biểu thức a + a * (b - c) + (b - c) * d Ta có cây cú pháp và DAG: + + + a a d - * - b * + * c * a d - b c b Cây cú pháp DAG Hình 5.10 - Cây cú pháp và DAG biểu thức c 123 (125) Ðể xây dựng DAG, trước tạo nút phải kiểm tra xem nút đó đã tồn chưa, đã tồn thì hàm tạo nút (mknode, mkleaf) trả trỏ nút đã tồn tại, chưa thì tạo nút Cài đặt DAG: Người ta thường sử dụng mảng mẩu tin , mẩu tin là nút Ta có thể tham khảo tới nút số mảng Ví dụ 5.9: Lệnh gán i := i + 10 DAG := Biểu diễn + i 10 id num + := entry i 10 Hình 5.11- Minh họa cài đặt DAG Nút 1: có nhãn là id, 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à +, trái là nút 1, phải là nút Nút 4: có nhãn là :=, trái là nút 1, phải là nút Giải thuật: Phương pháp số giá trị (value – number) để xây dựng nút DAG Giả sử các nút lưu mảng và nút tham khảo số giá trị nó Mỗi nút toán tử là 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 mảng nút m có nhãn là op trái là l, phải là r Nếu tìm thấy thì trả m, ngược lại tạo nút n, có nhãn là op, trái là l, phải là r và trả n III ÐÁNH GIÁ DƯỚI LÊN ÐỐI VỚI ÐỊNH NGHĨA S_THUỘC TÍNH Sử dụng Stack Như đã biết, định nghĩa S_ thuộc tính chứa các thuộc tính tổng hợp đó phương pháp phân tích lên là phù hợp với định nghĩa trực tiếp cú pháp này Phương pháp phân tích lên sử dụng STACK để lưu trữ thông tin cây đã 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ài đặt cặp mảng state và val Giả sử luật ngữ nghĩa A.a := f ( X.x, Y.y, Z.z ) kết hợp với luật sinh A → XYZ Trước XYZ rút gọn thành A thì val[top] = Z.z, val[top - 1] = Y.y, val[top - 2] 124 (126) = X.x Sau rút gọn, top bị giảm đơn vị, A nằm state[top] và thuộc tính tổng hợp nằm val[top] State val X X.x Y Y.y Z Z.z Stack Mỗi ô stack là trỏ trỏ tới bảng phân tích LR(1) Nếu phần tử thứ I stack là ký hiệu A thì val[i] là giá trị thuộc tính kết hợp với A top Hình 5.12 - Stack phân tích cú pháp vào trường lưu giữ thuộc tính tổng hợp Ví dụ Ví dụ 5.10: Xét định nghĩa trực tiếp cú pháp: Luật sinh Luật ngữ nghĩa L Æ En print(E.val) E Æ E1 + T E.val := E1.val + T.val EÆT E.val := T.val T Æ T1 * F T.val := T1.val * F.val TÆF T.val := F.val F Æ (E) F.val := E.val F Æ digit F.val := digit.lexval Với biểu thức * + n ta có cây chú thích: L E.val = 19 E.val = 15 + T.val = 15 T.val = F.val = * n T.val = F.val = F.val = digit.lexval = digit.lexval = digit.lexval = 125 (127) Hình 5.13 – Cây chú thích cho biểu thức * + n Cây chú thích này có thể đánh giá phân tích cú pháp LR từ lên trên Chú ý phân tích đã nhận biết giá trị thuộc tính digit.lexval Khi digit đưa vào stack thì token digit đưa vào state[top] và giá trị thuộc tính nó đưa vào val[top] Chúng ta có thể sử dụng kỹ thuật mục VI chương IV để xây dựng phân tích LR Ðể đánh giá các thuộc tính chúng ta thay đổi phân tích cú pháp để thực đoạn mã sau: Luật sinh Luật ngữ nghĩa L Æ En print(val[top]) E Æ E1 + T val[ntop] := val[top - 2] + val[top] EÆT T Æ T1 * F val[ntop] := val[top - 2] * val[top] TÆF F Æ (E) val[ntop] := val[top - 1] F Æ digit Hình 5.14- Cài đặt máy tính tay sử dụng phân tích cú pháp LR Khi luật sinh với r ký hiệu bên vế phải rút gọn thì ntop = top - r + Sau đoạn mã thực thì đặt top = ntop Bảng sau trình bày quá trình thực phân tích cú pháp Input State Val Luật sinh dùng 3*5+4n _ _ *5+4n 3 *5+4n F F Æ digit *5 + n T TÆF 5+4n T* 3- +4n T*5 3-5 +4n T*F 3-5 F Æ digit +4n T 15 TÆT*F +4n E 15 EÆT E+ 15 - n E+4 15 - n E+F 15 - F Æ digit n E+T 15 - TÆF 4n 126 (128) n EÆE+T E 19 En 19 - L 19 L Æ En Hình 5.15- Các phép chuyển tạo thông dịch trên chuỗi nhập 3* 5+4n IV ÐỊNH NGHĨA L_THUỘC TÍNH Ðịnh nghĩa L_thuộc tính Mỗi định nghĩa trực tiếp cú pháp là định nghĩa L thuộc tính thuộc tính kế thừa Xi (1 <= i <= n) vế phải luật sinh A → X1X2 Xn phụ thuộc vào: Các thuộc tính X1, X2, , Xi - Các thuộc tính kế thừa A Ví dụ 5.11: Cho định nghĩa trực tiếp cú pháp: Luật sinh AÆLM Luật ngữ nghĩa L.i := l(A.i) M.i := m(L.s) A.s := f(M.s) R.i := r(A.i) AÆQR Q.i := q(R.s) A.s := f(Q.r) Hình 5.16 - Ðịnh nghĩa trực tiếp cú pháp không phải là định nghĩa L_thuộc tính Ðây không phải là định nghĩa L_thuộc tính vì thuộc tính kế thừa Q.i phụ thuộc vào thuộc tính R.s ký hiệu bên phải nó luật sinh Lược đồ dịch Lược đồ dịch là văn phạm phi ngữ cảnh đó các thuộc tính kết hợp với các ký hiệu văn phạm và các hành vi ngữ nghĩa nằm cặp dấu { } xen vào bên phải luật sinh Ví dụ 5.12: Lược đồ dịch biểu thức trung tố với phép cộng và trừ thành dạng hậu tố: EÆ TR R Æ addop T { print ( addop.lexeme) } R1 | ε T Æ num { print ( num.val) } Với biểu thức - + ta có cây phân tích cú pháp (hình 5.16) Ðể xây dựng lược đồ dịch, chúng ta xét hai trường hợp sau đây: Trường hợp 1: Chỉ chứa thuộc tính tổng hợp: 127 (129) Với luật ngữ nghĩa, ta tạo hành vi ngữ nghĩa và đặt hành vi này vào cuối vế phải luật sinh Ví dụ 5.13: Luật sinh Luật ngữ nghĩa T Æ T1 * F T.val := T1.val * F.val Ta có lược đồ dịch: T Æ T1 * F { T.val := T1.val * F.val} Trường hợp 2: Có thuộc tính tổng hợp và kế thừa phải thỏa mãn yêu cầu sau đây: Thuộc tính kế thừa ký hiệu vế phải luật sinh phải xác định hành vi nằm trước ký hiệu đó Một hành vi không tham khảo tới thuộc tính tổng hợp ký hiệu nằm bên phải hành vi đó Thuộc tính tổng hợp ký hiệu chưa kết thúc vế trái có thể xác định sau tất các thuộc tính mà nó tham khảo đã xác định Hành vi xác định các thuộc tính này luôn đặt cuối vế phải luật sinh Với định nghĩa trực tiếp cú pháp L_thuộc tính ta có thể xây dựng lược đồ dịch thỏa mãn yêu cầu nói trên E R T T { print(‘-’) } R { print(‘9’) } { print(‘5’) } + T { print(‘+’) } R { print(‘2’) } ε Hình 5.17 - Cây phân tích cú pháp các hoạt động biểu diễn 9-5+2 Ví dụ 5.14: Bộ xử lý các công thức toán học – EQN - có thể xây dựng các biểu thức toán học từ các toán tử sub (subscripts) và sup (superscripts) Chẳng hạn: input output BOX sub box BOX box a sub {i sup } 128 (130) Ðể xác định chiều rộng và chiều cao các hộp ta có định nghĩa L_thuộc tính sau: Luật sinh Luật ngữ nghĩa SÆB B.ps := 10 S.ht := B.ht B Æ B1B2 B1.ps := B.ps B B2.ps := B.ps B B.ht := max(B1.ht, B2.ht) B Æ B1 sub B2 B1.ps := B.ps B B2.ps := shrink(B.ps) B B.ht := disp(B1.ht, B2.ht) B Æ text B.ht := text.h * B.ps Hình 5.18 - Ðịnh nghĩa trực tiếp cú pháp xác định kích thước và chiều cao các hộp Trong đó: - Ký hiệu chưa kết thúc B biểu diễn công thức - Luật sinh B → BB biểu diễn kề hai hộp - Luật sinh B → B sub B biểu diễn đặt, đó hộp số thứ có kích thước nhỏ hơn, nằm thấp hộp thứ - Thuộc tính kế thừa ps (point size - kích thước điểm) phản ánh độ lớn công thức - Luật sinh B → text ứng với luật ngữ nghiã B.ht:= text.h * B.ps lấy chiều cao thực text (h) nhân với kích thước điểm B để có chiều cao hộp - Luật sinh B → B1B2 áp dụng thì B1, B2 kế thừa kích thước điểm B luật copy Ðộ cao B giá trị lớn độ cao B1, B2 - Khi luật sinh B → B1 sub B2 áp dụng thì hàm shrink giảm kích thước điểm B2 còn 30% và hàm disp đẩy hộp B2 xuống Ðây là định nghĩa L_thuộc tính vì có thuộc tính kế thừa ps và thuộc tính này phụ thuộc vào vế trái luật sinh Dựa vào yêu cầu nói trên, ta xen các hành vi ngữ nghĩa tương ứng với luật ngữ nghĩa vào vế phải luật sinh để lược đồ dịch SÆ {B.ps := 10 } B BÆ {S.ht := B.ht } { B1.ps := B.ps } B1 {B2.ps := B.ps } B2 {B.ht := max(B1.ht, B2.ht ) } B 129 (131) BÆ {B1.ps := B.ps } B1 sub {B2.ps := shrink(B.ps) } B2 {B.ht := disp(B1.ht, B2.ht ) } B B Æ text {B.ht := text.h * B.ps } Hình 5.19 - Lược đồ dịch tạo từ hình 5.18 Chú ý: Ðể dễ đọc ký hiệu văn phạm luật sinh viết trên dòng và hành vi viết vào bên phải Chẳng hạn: S Æ {B.ps := 10 } B {S.ht := B.ht } Ðược viết thành S Æ {B.ps := 10 } B {S.ht := B.ht } V DỊCH TRÊN XUỐNG Loại bỏ đệ qui trái Vấn đề loại bỏ đệ qui trái văn phạm đã trình bày mục III chương IV Ở đây chúng ta giải vấn đề chuyển lược đồ dịch văn phạm đệ quy trái thành lược đồ dịch không còn đệ quy Giả sử, ta có lược đồ dịch dạng A Æ A1 Y {A.a := g(A1.a, Y.y) } AÆX {A.a := f(X.x) } Ðây là văn phạm đệ quy trái, áp dụng giải thuật khử đệ qui trái ta văn phạm không đệ quy trái AÆXR RÆYR|ε Bổ sung hành vi ngữ nghĩa cho văn phạm ta lược đồ dịch: A Æ X { R.i := f(X.x) } R {A.a := R.s } R Æ Y {R1.i := g(R.i, Y.y) } R1 {R.s := R1.s } R Æ ε {R.s := R.i } Ví dụ 5.15: Xét lược đồ dịch văn phạm đệ quy trái cho biểu thức E Æ E1 + T {E.val := E1.val + T.val } E Æ E1 - T {E.val := E1.val - T.val } EÆT {E.val := T.val } 130 (132) T Æ (E) {T.val := E.val } T Æ num {T.val := num.val } Hình 5.20 - Lược đồ dịch văn phạm đệ quy trái Vận dụng ý kiến trên ta khử đệ quy trái để lược đồ dịch không đệ quy trái E Æ T {R.i := T.val } R {E.val := R.s } RÆ + T {R1.i := R.i + T.val } R1 {R.s := R1.s } RÆ T {R1.i := R.i - T.val } R1 {R.s := R1.s } RÆ ε {R.s := R.i } TÆ ( E ) {T.val := E.val } T Æ num { T.val := num.val } Hình 5.21 - Lược đồ dịch đã chuyển đổi có văn phạm đệ quy phải Chẳng hạn đánh giá E biểu thức - + T.val = num.val = R.i = - T.val = num.val = R.i = + T.val = num.val = R.i = ε Hình 5.22 - Xác định giá trị biểu thức 9-5+2 Ví du 5.16: Xét lược đồ dịch xây dựng cây cú pháp cho biểu thức E Æ E1 + T {E.nptr := mknode(‘+’, E1.nptr, T.nptr) } E Æ E1 - T {E.nptr := mknode(‘-’, E1.nptr, T.nptr) } EÆT {E.nptr := T.nptr } T Æ (E) {T.nptr := E.nptr } T Æ id {T.nptr := mkleaf(id, id.entry) } 131 (133) T Æ num {T.nptr := mkleaf(num, num.val) } Áp dụng quy tắc khử đệ quy trái trên với E ≈ A, +T, -T ≈ Y và T ≈ X ta có lược đồ dịch E Æ T {R.i := T.nptr } R {E.nptr := R.s } RÆ + T {R1.i := mknode(‘+’, R.nptr, T.nptr) } R1 {R.s := R1.s } RÆ T {R1.i := mknode(‘-’, R.nptr, T.nptr) } R1 {R.s := R1.s } RÆ ε {R.s := R.i } TÆ ( E ) T Æ id {T.nptr := E.nptr } {T.nptr := mkleaf(id, id.entry) } T Æ num { T.nptr := mkleaf(num, num.val) } Hình 5.23 - Lược đồ dịch chuyển đổi để xây dựng cây cú pháp Thiết kế dịch dự đoán Giải thuật: Xây dựng dịch trực tiếp cú pháp dự đoán (Predictive - Syntax Directed Translation) Input: Một lược đồ dịch cú pháp trực tiếp với văn phạm có thể phân tích dự đoán Output: Mã cho dịch trực tiếp cú pháp Phương pháp: Với ký hiệu chưa kết thúc A, xây dựng hàm có các tham số hình thức tương ứng với các thuộc tính kế thừa A và trả giá trị thuộc tính tổng hợp A Mã cho ký hiệu chưa kết thúc A định luật sinh nào dùng cho ký hiệu nhập hành Mã kết hợp với luật sinh sau: chúng ta xem xét token, ký hiệu chưa kết thúc và hành vi bên phải luật sinh từ trái sang phải i) Ðối với token X với thuộc tính tổng hợp x, lưu giá trị x vào biến khai báo cho X.x Sau đó phát sinh lời gọi để hợp thức (match) token X và dịch chuyển đầu đọc 132 (134) ii) Ðối với ký hiệu chưa kết thúc B, phát sinh lệnh gán C := B(b1, b2, , bk) với lời gọi hàm vế phải lệnh gán, đó b1, b2, , bk là các biến cho các thuộc tính kế thừa B và C là biến cho thuộc tính tổng hợp B iii) Ðối với hành vi, chép mã vào phân tích cú pháp, thay tham chiếu tới thuộc tính biến cho thuộc tính đó Ví dụ 5.17: Xét lược đồ dịch cho việc xây dựng cây cú pháp cho biểu thức Ta thấy đó là văn phạm LL(1) nên phù hợp cho phân tích trên xuống Với ký hiệu chưa kết thúc E, R, T ta xây dựng hàm tương ứng: function E: ↑ syntax - tree - node; /* E không có thuộc tính kế thừa */ function R (i : ↑ syntax - tree - node) : ↑ syntax - tree - node function T : ↑ syntax - tree - node; Dùng token addop biểu diễn cho + và - ta có thể kết hợp hai luật sinh thành luật sinh RÆ RÆ addop T {R1.i := mknode(addop.lexeme, R.i, T.nptr) } R1 {R.s := R1.s } ε {R.s := R.i } Ta có hàm R sau: function R(i : ↑ syntax_ tree_node) : ↑ syntax_tree_node; var nptr, i1, s1, s : ↑ syntax_tree_node; addoplexeme : char; begin if lookahead = addop then begin /* luật sinh R → addop TR */ addoplexeme := lexval; match(addop); nptr := T; i1 := mknode(addoplexeme, i, nptr); s1 := R(i1); s := s1; end else s := i; /* Luật sinh R → ε */ return s; end; Hình 5.24 - Xây dựng cây cú pháp đệ quy giảm 133 (135) BÀI TẬP CHƯƠNG V 5.1 Xây dựng cây phân tích cú pháp chú thích cho biểu thức số học sau: (4 * 7+ 1) * 5.2 Xây dựng cây phân tích cú pháp và cây cú pháp cho biểu thức ((a) + (b)) theo: a) Ðịnh nghĩa trực tiếp cú pháp cho biểu thức số học b) Lược đồ dịch cho biểu thức số học 5.3 Xây dựng DAG cho các biểu thức sau đây: a) a + a + ( a+ a + a + ( a+ a+ a+ a)) b) x *( *x + x * x) 5.4 Văn phạm sau đây sinh các biểu thức có áp dụng toán tử số học + cho các số nguyên và số thực Khi số nguyên công lại, kiểu kết là kiểu nguyên, ngược lại nó là kiểu số thực E→ E+T | T T → num • num | num a) Ðưa định nghĩa trực tiếp cú pháp xác định kiểu biểu thức b) Mở rộng định nghĩa trực tiếp cú pháp trên để dịch các biểu thức thành ký pháp hậu tố và xác định các kiểu Dùng toán tử ngôn inttoreal để chuyển giá trị nguyên thành giá trị thực tương đương mà nhờ đó hai toán hạng + dạng hậu tố có cùng kiểu 5.5 Giả sử các khai báo sinh văn phạm sau: D → id L L → , id L | : T T → integer | real a) Xây dựng lược đồ dịch để nhập kiểu định danh vào bảng danh biểu b) Xây dựng chương trình dịch dự đoán từ lược đồ dịch trên 134 (136) 5.6 Cho văn phạm sinh các dòng text sau: S→ L L→ LB|B B → B sub F | F F → { L } | text a) Xây dựng định nghĩa trực tiếp cú pháp cho văn phạm b) Chuyển định nghĩa trực tiếp cú pháp trên thành lược đồ dịch c) Loại bỏ đệ quy trái lược đồ dịch vừa xây dựng 135 (137) CHƯƠNG VI KIỂM TRA KIỂU Nội dung chính: Hai cách kiểm tra kiểu là kiểm tra tĩnh thực thời gian biên dịch chương trình nguồn và kiểm tra động thực thời gian thực thi chương trình đích Trong chương này ta tập trung vào phần xử lý ngữ nghĩa cách kiểm tra tĩnh mà cụ thể là kiểm tra kiểu Phần đầu chương trình bày các khái niệm hệ thống kiểu, các biểu thức kiểu Phần còn lại mô tả cách tạo kiểm tra kiểu đơn giản Mục tiêu cần đạt: Sau học xong chương này, sinh viên phải nắm được: • Hệ thống kiểu với các biểu thức kiểu (kiểu sở và kiểu có cấu trúc) thường gặp ngôn ngữ lập trình nào • Dịch trực tiếp cú pháp cài đặt kiểm tra kiểu đơn giản từ đó có thể mở rộng để cài đặt cho ngôn ngữ phức tạp Kiến thức bản: Sinh viên phải biết số ngôn ngữ lập trình cấp cao Pascal, C++, Java, v.v đã học môn ngôn ngữ lập trình (phần đề cập đến các kiểu sở và kiểu có cấu trúc) Tài liệu tham khảo: [1] Compilers : Principles, Technique and Tools - Alfred V.Aho, Jeffrey D.Ullman - Addison - Wesley Publishing Company, 1986 [2] Modern Compiler Implementation in C - Andrew W Appel - Cambridge University Press, 1997 [3] Compiler Design – Reinhard Wilhelm, Dieter Maurer - Addison - Wesley Publishing Company, 1996 I HỆ THỐNG KIỂU Trong các ngôn ngữ nói chung có kiểu sở và kiểu có cấu trúc Chẳng hạn trang Pascal, kiểu sở là: boolean, char, integer, real, kiểu miền và kiểu liệt kê Các kiểu có cấu trúc mảng, mẩu tin, tập hợp, Biểu thức kiểu Biểu thức kiểu bao gồm: Kiểu sở là biểu thức kiểu: boolean, char, integer, real Ngoài còn có các kiểu sở đặc biệt như: kiểu type_error: lỗi quá trình kiểm tra kiểu; kiểu void, “không có giá trị”, cho phép kiểm tra kiểu lệnh 135 (138) Vì biểu thức kiểu có thể đặt tên, tên kiểu là biểu thức kiểu Cấu trúc kiểu là biểu thức kiểu, các cấu trúc bao gồm: a Mảng (array): Nếu T là biểu thức kiểu thì array(I, T) là biểu thức kiểu Một mảng có tập số I và các phần tử có kiểu T b Tích (products): Nếu T1, T2 là biểu thức kiểu thì tích Decas T1* T2 là biểu thức kiểu c Mẩu tin (records): Là cấu trúc bao gồm các tên trường, kiểu trường d Con trỏ (pointers): Nếu T là biểu thức kiểu thì pointer(T) là biểu thức kiểu T e Hàm (functions): Một cách toán học, hàm ánh xạ các phần tử tập xác định (domain) lên tập giá trị (range) Một hàm là biểu thức kiểu D Æ R Hệ thống kiểu Hệ thống kiểu là sưu tập các quy tắc để gán các biểu thức kiểu vào các phần chương trình Bộ kiểm tra kiểu cài đặt hệ thống kiểu Kiểm tra kiểu tĩnh và động Kiểm tra thực chương trình dịch gọi là kiểm kiểu tĩnh Kiểm tra thực chạy chương trình đích gọi là kiểm tra kiểu động II ÐẶC TẢ MỘT BỘ KIỂM TRA KIỂU ÐƠN GIẢN Trong phần này chúng ta mô tả kiểm tra kiểu cho ngôn ngữ đơn giản đó kiểu danh biểu phải khai báo trước sử dụng Bộ kiểm tra kiểu là lược đồ dịch mà nó tổng hợp kiểu biểu thức từ kiểu các biểu thức nó Một ngôn ngữ đơn giản Văn phạm sau sinh chương trình, biểu diễn ký hiệu chưa kết thúc P chứa chuỗi các khai báo D và biểu thức đơn giản E PÆD;E D Æ D ; D | id : T T Æ char | integer | array[num] of T1 | ↑T1 E Æ literal | num | id | E1 mod E2 | E1 [E2] | E1↑ Hình 6.1 - Văn phạm ngôn ngữ đơn giản • Các kiểu sở: char, integer và type-error • Mảng Chẳng hạn array[256] of char là biểu thức kiểu (1 256, char) • Kiểu trỏ ↑T là biểu thức kiểu pointer(T) Ta có lược đồ dịch để lưu trữ kiểu danh biểu PÆD;E 136 (139) DÆD;D D Æ id : T {addtype(id.entry, T.type) } T Æ char {T.type := char } T Æ integer {T.type := integer } T Æ ↑T1 {T.type := pointer(T1.type) } T Æ array[num] of T1 {T.type := array(1 num.val, T1.type) } Hình 6.2- Lược đồ dịch lưu trữ kiểu danh biểu Kiểm tra kiểu biểu thức Lược đồ dịch cho kiểm tra kiểu biểu thức sau: E Æ literal {E.type := char } E Æ num {E.type := integer } E Æ id {E.type := lookup(id.entry) } E Æ E1 mod E2 {E.type := if E1.type = integer and E2.type = integer then integer else type_error } E Æ E1[E2] {E.type := if E2.type = integer and E1.type = array(s,t) then t else type_error } E Æ E1↑ { E.type := if E1.type = pointer(t) then t else type_error } Hình 6.3- Lược đồ dịch kiểm tra kiểu biểu thức Ở đây ta dùng hàm lookup(e) để tìm kiểu lưu trữ ô bảng ký hiệu mà ô đó trỏ e Kiểm tra kiểu các lệnh Ta có lược đồ dịch cho kiểm tra kiểu lệnh S Æ id := E { S.type := if id.type = E.type then void else type_error } S Æ if E then S1 {S.type := if E.type = boolean then S1.type else type_error } S Æ while E S1 {S.type := if E.type = boolean then S1.type else type_error } S Æ S1 ; S2 {S.type := if S1.type = void and S2.type = void then void else type_error } Hình 6.4- Lược đồ dịch kiểm tra kiểu các lệnh 137 (140) Kiểm tra kiểu các hàm Áp dụng hàm vào đối số có thể cho luật sinh E → E (E) Lược đồ dịch cho kiểm tra kiểu cho áp dụng hàm là: E Æ E1 (E2) {E.type := if E2.type = s and E1.type = s -> t then t else type_error } Hình 6.5- Lược đồ dịch kiểm tra kiểu hàm Luật sinh trên biểu diễn biểu thức hình thành áp dụng E1 lên E2, kiểu E1 phải là hàm s -> t từ kiểu s E2 tới kiểu giới hạn t nào đó; kiểu E1 (E2) là t III SỰ TƯƠNG ÐƯƠNG CỦA CÁC BIỂU THỨC KIỂU Thông thường kiểm tra kiểu có dạng: “nếu hai biểu thức kiểu thì trả kiểu ngược lại trả type_error” Ðiều quan trọng là cần xác định nào hai biểu thức kiểu tương đương Tương đương cấu trúc Hai biểu thức kiểu gọi là tương đương cấu trúc cấu trúc chúng giống hệt Ví dụ 6.1: - Biểu thức kiểu integer tương đương với integer vì chúng là kiểu sở - pointer(integer) tương đương với pointer(integer) vì hai hình thành cách áp dụng cùng cấu trúc trỏ pointer lên các kiểu tương đương Giả sử, s và t là hai biểu thức kiểu, hàm sau kiểm tra xem chúng có tương đương hay không? Function sequiv(s, t) : boolean; Begin if s và t cùng là kiểu sở then return true else if s = array(s1, s2) and t = array(t1, t2) then return sequiv(s1, t1) and sequiv(s2, t2) else if s = pointer(s1) and t = pointer(t1) then return sequiv(s1, t1) else if s = s1 -> s2 and t = t1 -> t2 then return sequiv(s1, t1) and sequiv(s2, t2) else return false; end; 138 (141) Hình 6.6- Ðoạn ngôn ngữ giả kiểm tra tương đương cấu trúc hai biểu thức kiểu s và t Tương đương tên Trong số ngôn ngữ, kiểu cho tên Ví dụ Pascal type link = ↑ cell; var next : link; last : link; p : ↑cell; q, r : ↑cell; Danh biểu link khai báo là tên kiểu ↑cell Vấn đề đặt là next, last, p, q, r có kiểu giống hay không? Câu trả lời phụ thuộc vào cài đặt Hai biểu thức kiểu là tương đương tên tên chúng giống Theo quan niệm tương đương tên thì last và next có cùng kiểu; p, q và r có cùng kiểu next và p có kiểu khác IV CHUYỂN ÐỔI KIỂU Xét biểu thức x + i đó x có kiểu real và i có kiểu integer Vì biểu diễn các số nguyên, số thực khác máy tính đó các thị máy khác dùng cho số thực và số nguyên Trình biên dịch có thể thực việc chuyển đổi kiểu để hai toán hạng có cùng kiểu phép toán cộng xảy Bộ kiểm tra kiểu trình biên dịch có thể dùng để thêm các phép toán biến đổi kiểu vào biểu diễn trung gian chương trình nguồn Chẳng hạn ký hiệu hậu tố x + i có thể là: x i inttoreal real+ Trong đó: inttoreal đổi số nguyên i thành số thực, real+ thực phép cộng các số thực Sự ép buộc chuyển đổi kiểu Sự chuyển đổi từ kiểu này sang kiểu khác gọi là ẩn (implicit) nó làm cách tự động chương trình dịch Chuyển đổi kiểu ẩn còn gọi là ép buộc chuyển đổi kiểu (coercions) Ví dụ 6.2: Ðịnh nghĩa trực tiếp cú pháp cho kiểm tra kiểu và ép buộc chuyển đổi kiểu biến đổi kiểu từ integer thành real: Luật sinh Luật ngữ nghĩa E Æ num E.type := integer E Æ num.num E.type := real E Æ id E.type := lookup(id.entry) E Æ E1 op E2 E.type := if E1.type = integer and E2.type = integer then integer else if E1.type = integer and E2.type = real 139 (142) then real else if E1.type = real and E2.type = integer then real else if E1.type = real and E2.type = real then real else type_error Hình 6.7- Ðịnh nghĩa trực tiếp cú pháp cho kiểm tra kiểu và ép buộc chuyển đổi kiểu Chý ý việc ép buộc chuyển đổi kiểu có thể dẫn đến lãng phí thời gian thực chương trình Ví dụ 6.3: Với khai báo x là mảng các số thực thì lệnh for i:=1 to n x[i]:=1 thực 48,4 micro giây còn lệnh for i:=1 to n x[i]:=1.0 thực 5,4 micro giây Sở dĩ vì mã phát sinh cho lệnh thứ chứa lời gọi thủ tục đổi số nguyên thành số thực thời gian thực 140 (143) BÀI TẬP CHƯƠNG VI 6.1 Viết các biểu thức kiểu cho các kiểu liệu sau đây: a) Một mảng các trỏ có kích thước từ đến 100, trỏ đến đối tượng các số thực b) Mảng chiều các số nguyên, hàng có kích thước từ đến 9, cột có số từ -10 đến 10 c) Các hàm mà miền định nghĩa là các hàm với các đối số nguyên, trị là trỏ trỏ đến các số nguyên và miền xác định nó là các mẫu tin chứa số nguyên và ký tự 6.2 Giả sử có khai báo C sau: typedef struct { CELL PCELL int a, b ; } CELL, * PCELL ; foo [ 100 ] ; bar (x, y) int x ; CELL y { } Viết các biểu thức kiểu cho các kiểu liệu foo và bar 6.3 Cho văn phạm sau đây định nghĩa chuỗi các chuỗi ký tự: P→ D→ T→ E→ L→ D; E D ; D | id : T list of T | char | integer ( L ) | literal | num | id E,L| E Hãy viết các quy tắc biên dịch để xác định các biểu thức kiểu (E) và list (L) 6.4 Giả sử tên kiểu là link và cell định nghĩa phần tên cho biểu thức kiểu Hãy xác định biểu thức kiểu nào đây là tương đương cấu trúc, biểu thức kiểu nào tương đương tên a) link b) pointer (cell) c) pointer (link) d) pointer (record ((info x integer) x ( next x pointer (cell))) 6.5 Giả sử kiểu định danh là miền số nguyên Cho biểu thức với các phép toán +, - , * , div và mod Pascal, hãy viết quy tắc kiểm tra kiểu để gán biểu thức vào vùng miền giá trị mă nó nằm đó 141 (144) CHƯƠNG VII MÔI TRƯỜNG THỜI GIAN THỰC HIỆN Nội dung chính: Trước xem xét vấn đề sinh mã trình bày các chương sau, chương này trình bày số vấn đề liên quan đến việc gọi thực chương trình con, các chiến lược cấp phát nhớ và quản lý bảng ký hiệu Cùng tên chương trình nguồn có thể biểu thị cho nhiều đối tượng liệu chương trình đích Sự biểu diễn các đối tượng liệu thời gian thực thi xác định kiểu nó Sự cấp phát và thu hồi các đối tượng liệu quản lý tập các chương trình dạng mã đích Việc thiết kế các chương trình này xác định ngữ nghĩa chương trình nguồn Mỗi thực thi chương trình gọi là mẩu tin kích hoạt Nếu chương trình đệ quy, số mẩu tin kích hoạt có thể tồn cùng thời điểm Mỗi ngôn ngữ lập trình có quy tắc tầm vực để xác định việc xử lý tham khảo đến các tên không cục Tùy vào ngôn ngữ, nó cho phép chương trình chứa các chương trình lồng không lồng nhau; Cho phép gọi đệ quy không đệ quy; Cho phép truyền tham số giá trị hay tham chiếu …Vì thế, thiết kế chương trình dạng mã đích ta cần chú ý đến các yếu tố này Mục tiêu cần đạt: Sau học xong chương này, sinh viên phải nắm được: • Cách gọi và thực thi chương trình • Cách tổ chức nhớ và các chiến lược cấp phát – thu hồi nhớ Kiến thức bản: Sinh viên phải biết số ngôn ngữ lập trình cấp cao Pascal, C++, Java, v.v đã học môn ngôn ngữ lập trình (phần đề cập đến các chương trình con) Tài liệu tham khảo: [1] Compilers : Principles, Technique and Tools - Alfred V.Aho, Jeffrey D.Ullman - Addison - Wesley Publishing Company, 1986 [2] Modern Compiler Implementation in C - Andrew W Appel - Cambridge University Press, 1997 I CHƯƠNG TRÌNH CON Ðịnh nghĩa chương trình Ðịnh nghĩa chương trình là khai báo nó Dạng đơn giản là kết hợp tên chương trình và thân nó Ví dụ 7.1: Chương trình Pascal đọc và xếp các số nguyên 142 (145) (1) program sort(input, output) (2) var a: array[0 10] of integer; (3) procedure readarray; (4) var i: integer; (5) begin (6) for i=1 to read(a[i]); (7) end; (8) function partition(y,z:integer): integer; (9) var i,j,x,v: integer; (10) begin (11) end; (12) (13) (14) procedure quicksort(m,n:integer); var i: integer; begin; (15) if (n>m) then begin (16) i:= partition(m,n); (17) quicksort(m,i-1); (18) quicksort(i+1,n); (19) end; (20) end; (21) begin (22) a[0]:= -9999, a[10]:= 9999; (23) readarray; (24) quicksort(1,9); (25) end Hình 7.1- Chương trình Pascal đọc và xếp các số nguyên Chương trình trên chứa các định nghĩa chương trình - Chương trình readarray từ dòng - 7, thân nó từ - - Chương trình partition từ dòng - 11, thân nó từ 10 - 11 - Chương trình quicksort từ dòng 12 - 20, thân nó từ 14 - 20 Chương trình chính xem là chương trình có thân từ dòng 21 - 25 Khi tên chương trình xuất phần thân chương trình ta nói chương trình gọi điểm đó 143 (146) Cây hoạt động Trong quá trình thực chương trình thì: Dòng điều khiển là tuần tự: tức là việc thực chương trình bao gồm chuỗi các bước Tại bước có điều khiển xác định Việc thực chương trình bắt đầu điểm bắt đầu thân chương trình và trả điều khiển cho chương trình gọi điểm nằm sau lời gọi việc thực chương trình kết thúc • Thời gian tồn chương trình p là chuỗi các bước bước đầu tiên và bước cuối cùng thực thân chương trình bao gồm thời gian thực các chương trình gọi p • Nếu a và b là hai hoạt động hai chương trình tương ứng thì thời gian tồn chúng tách biệt lồng • Một chương trình là đệ quy hoạt động có thể bắt đầu trước hoạt động trước đó chương trình đó kết thúc • Ðể đặc tả cách thức điều khiển vào hoạt động chương trình ta dùng cấu trúc cây gọi là cây hoạt động Mỗi nút biểu diễn cho hoạt động chương trình Nút gốc biểu diễn cho hoạt động chương trình chính Nút a là cha b và dòng điều khiển hoạt động đó từ a sang b Nút a bên trái nút b thời gian tồn a xuất trước thời gian tồn b Ví dụ 7.2: Xét chương trình sort nói trên - Bắt đầu thực chương trình - Vào readarray - Ra khỏi readarray - Vào quicksort(1,9) - Vào partition(1,9) - Ra khỏi partition(1,9) // giả sử trả - Vào quicksort(1,3) - - Ra khỏi quicksort(1,3) - Vào quicksort(5,9); - - Ra khỏi quicksort(5,9) - Sự thực kết thúc 144 (147) Hình 7.2 - Xuất các mẩu tin hoạt động đề nghị chương trình hình 7.1 Ta có cây hoạt động tương ứng s r q(1,9) p(1,9) q(1,3) p(1,3) q(5,9) q(1,0) q(2,3) p(5,9) q(5,5) p(2,3) q(2,1) q(3,3) q(7,9) p(7,9) q(7,7) q(9,9) Hình 7.3- Cây hoạt động tương ứng với phần xuất hình 7.2 Ngăn xếp điều khiển Dòng điều khiển chương trình tương ứng với phép duyệt theo chiều sâu cây hoạt động Bắt đầu từ nút gốc, thăm nút trước các nó và thăm các cách đệ quy nút từ trái sang phải Chúng ta có thể dùng Stack, gọi là Stack điều khiển, để lưu trữ hoạt động chương trình Khi hoạt động chương trình bắt đầu thì đẩy nút tương ứng với hoạt động đó lên đỉnh Stack Khi hoạt động kết thúc thì pop nút đó khỏi Stack Nội dung Stack thể đường dẫn đến nút gốc cây hoạt động Khi nút n nằm trên đỉnh Stack thì Stack chứa các nút nằm trên đường từ n đến gốc Ví dụ 7.3: Hình sau trình bày nội dung Stack lưu trữ đường từ nút q(2, 3) đến nút gốc Các cạnh nét đứt thể nút đã pop khỏi Stack s q(1,9) r q(1,3) p(1,9) p(1,3) q(1,0) q(2,3) Hình 7.4 - Stack điều khiển chứa các nút dẫn tới nút gốc Tại thời điểm này thì đường dẫn tới gốc là: s q(1, 9) q(1, 3) q(2, 3) ( q(2, 3) nằm trên đỉnh Stack) 145 (148) Tầm vực khai báo Ðoạn chương trình chịu ảnh hưởng khai báo gọi là tầm vực khai báo đó Trong chương trình có thể có nhiều khai báo trùng tên ví dụ biến i chương trình sort Các khai báo này độc lập với và chịu chi phối quy tắc tầm ngôn ngữ Sự xuất tên chương trình gọi là cục (local) chương trình tầm vực khai báo nằm chương trình con, ngược lại gọi là không cục (nonlocal) Liên kết tên Trong ngôn ngữ ngôn ngữ lập trình, thuật ngữ môi trường (enviroment) để ánh xạ từ tên đến vị trí ô nhớ và thuật ngữ trạng thái (state) để ánh xạ từ vị trí ô nhớ tới giá trị lưu trữ đó môi trường tên trạng thái giá trị ô nhớ Hình 7.5 - Hai ánh xạ từ tên tới giá trị Môi trường khác trạng thái: lệnh gán làm thay đổi trạng thái không thay đổi môi trường Khi môi trường kết hợp vị trí ô nhớ s với tên x ta nói x liên kết tới s Sự kết hợp đó gọi là mối liên kết x Liên kết là động (dynamic counterpart) khai báo Chúng ta có tương ứng các ký hiệu động và tĩnh: Ký hiệu tĩnh Bản động Ðịnh nghĩa chương trình Sự hoạt động cuả chương trình Sự khai báo tên Liên kết tên Tầm vực khai báo Thời gian tồn liên kết Hình 7.6 - Sự tương ứng ký hiệu động và tĩnh Các vấn đề cần quan tâm làm chương trình dịch Các vấn đề cần đặt tổ chức lưu trữ và liên kết tên: Chương trình có thể đệ quy không? Ðiều gì xảy cho giá trị các tên cục trả điều khiển từ hoạt động chương trình 146 (149) Một chương trình có thể tham khảo tới tên cục không? Các tham số truyền nào gọi chương trình Một chương trình có thể truyền tham số? Một chương trình có thể trả kết quả? Bộ nhớ có cấp phát động không? Bộ nhớ có phải giải phóng cách tường minh? II TỔ CHỨC BỘ NHỚ Tổ chức nhớ thời gian thực đây có thể sử dụng cho các ngôn ngữ Fortran, Pascal và C Phân chia nhớ thời gian thực Bộ nhớ có thể chia để lưu trữ các phần: Mã đích Ðối tượng liệu Bản Stack điều khiển để lưu trữ hoạt động chương trình Trong đó kích thước mã đích xác định thời gian dịch cho nên nó cấp phát tĩnh vùng thấp nhớ Tương tự kích thước số đối tượng liệu có thể biết thời gian dịch cho nên nó cấp phát tĩnh Cài đặt các ngôn ngữ Pascal, C dùng mở rộng Stack điều khiển để quản lý hoạt động chương trình Khi có lời gọi chương trình con, thể hoạt động bị ngắt và thông tin tình trạng máy, chẳng hạn giá trị đếm chương trình (program counter) và ghi lưu vào Stack Khi điều khiển trả từ lời gọi, hoạt động này tiếp tục sau khôi phục lại giá trị ghi và đặt đếm chương trình vào sau lời gọi Ðối tượng liệu mà thời gian tồn nó chứa hoạt động lưu Stack Một vùng khác nhớ gọi là Heap lưu trữ tất các thông tin khác Code Static Data Stack Heap 147 (150) Hình 7.7 - Phân chia nhớ thời gian thực cho mã đích và các vùng liệu khác Mẩu tin hoạt động Thông tin cần thiết để thực chương trình quản lý cách dùng mẩu tin hoạt động bao gồm số trường sau : Giá trị trả Các tham số thực tế Liên kết điều khiển Liên kết truy nhập Trạng thái máy Dữ liệu cục Giá trị tạm thời Hình 7.8 - Mẩu tin hoạt động tổng quát Ý nghĩa các trường sau: Giá trị tạm thời: lưu giữ quá trình đánh giá biểu thức Dữ liệu cục bộ: Lưu trữ liệu cục thực chương trình Trạng thái máy: lưu giữ thông tin trạng thái máy trước chương trình gọi Thông tin máy bao gồm đếm chương trình và ghi lệnh mà nó phục hồi điều khiển trả từ chương trình Liên kết truy nhập: tham khảo tới liệu không cục lưu các mẩu tin hoạt động khác Liên kết điều khiển: trỏ tới mẩu tin hoạt động chương trình gọi Các tham số thực tế: sử dụng các chương trình gọi chương trình gọi Thông thường các tham số lưu ghi không phải mẩu tin hoạt động Giá trị trả về: dùng chương trình gọi để trả cho chương trình gọi giá trị Trong thực tế giá trị này thường trả ghi III CHIẾN LƯỢC CẤP PHÁT BỘ NHỚ Ðối với các vùng nhớ khác tổ chức nhớ, ta có các chiến lược cấp phát khác : Cấp phát tĩnh cho tất các đối tượng liệu thời gian dịch 148 (151) Cấp phát sử dụng Stack cho nhớ thời gian thực Ðối với vùng liệu Heap sử dụng cấp phát và thu hồi dạng Heap Cấp phát tĩnh Trong cấp phát tĩnh, tên liên kết với vùng nhớ lúc chương trình dịch Vì mối liên kết không thay đổi thời gian chạy nên lần chương trình kích hoạt, tên nó liên kết với cùng vùng nhớ Tính chất này cho phép giá trị các tên cục giữ lại thông qua hoạt động các chương trình Từ kiểu tên, trình biên dịch xác định kích thước nhớ nó Do đó trình biên dịch xác định vị trí mẩu tin kích hoạt đoạn mã chương trình và các mẩu tin kích hoạt khác Trong thời gian biên dịch, chúng ta có thể điền vào đoạn các địa mà mã lệnh có thể tìm đến để truy xuất liệu Tương tự địa các vùng lưu trữ thông tin chương trình gọi xác định thời gian dịch Tuy nhiên cấp phát tĩnh có số hạn chế sau: Kích thước và vị trí đối tượng liệu nhớ phải xác định thời gian dịch Không cho phép gọi đệ quy vì tất các kích hoạt chương trình dùng chung liên kết tên cục Cấu trúc liệu không thể cấp phát động vì không có chế để cấp phát thời gian thực Cấp phát ô nhớ sử dụng Stack Bộ nhớ tổ chức là Stack Các mẩu tin kích hoạt push vào Stack hoạt động bắt đầu và pop khỏi Stack hoạt động kết thúc Ví dụ 7.4: Chúng ta minh họa việc cấp phát và loại bỏ mẩu tin kích hoạt tương ứng với cây hoạt động chương trình sort: Cây hoạt động Mẩu tin kích hoạt Stack s a: array s s s a: array r r i: integer s r s a: array q(1,9) q(1,9) i: integer 149 (152) s r s a: array q(1,9) q(1,9) i: integer p(1,9) q(1,3) p(1,3) q(1,3) q(1,0) q(2,3) i: integer q(2,3) i: integer Hình 7.9 - Sự cấp phát và lọai bỏ các mẩu tin kích hoạt Bộ nhớ cho liệu cục lần gọi chương trình chứa mẩu tin kích hoạt cho lần gọi đó Như các tên cục liên kết với nhớ hoạt động, vì mẩu tin kích hoạt push vào Stack có lời gọi chương trình Dữ liệu các biến cục bị xóa bỏ thực chương trình kết thúc Giả sử ghi top đánh dấu đỉnh Stack Tại thời gian thực mẩu tin kích hoạt có thể cấp phát thu hồi cách tăng giảm ghi top bằòng kích thước mẩu tin kích hoạt Gọi thực chương trình Gọi chương trình thực lệnh gọi mã đích - lệnh gọi cấp phát mẩu tin kích hoạt và đưa thông tin vào cho các trường - lệnh trở phục hồi các trạng thái máy để chương trình gọi tiếp tục thực Tham số và giá trị trả Liên kết và trạng thái máy Dữ liệu tạm và cục Tham số và trị trả Liên kết và trạng thái máy top_sp Dữ liệu tạm và cục Mẩu tin kích hoạt chương trình gọi Trách nhiệm chương trình gọi Mẩu tin kích hoạt chương trình bị gọi Trách nhiệm chương trình bị gọi Hình 7.10 - Phân chia công việc chương trình gọi và chương trình bị gọi 150 (153) Hình trên mô tả mối quan hệ mẩu tin kích hoạt chương trình gọi và chương trình bị gọi Mỗi mẩu tin có ba trường chủ yếu: các tham số thực tế và trị trả về, các mối liên kết và trạng thái máy và cuối cùng là trường liệu tạm và cục Thanh ghi top.sp đến cuối trường các mối liên kết và trạng thái máy Vị trí này biết chương trình gọi Ðoạn mã cho chương trình bị gọi có thể truy xuất liệu tạm và cục nó cách sử dụng độ dời (offsets) từ top.sp Lệnh gọi thực các công việc sau : Chương trình gọi đánh giá các tham số thực tế Chương trình gọi lưu địa trả và giá trị cũ top.sp vào mẩu tin kích hoạt chương trình bị gọi Sau đó tăng giá trị top.sp Chương trình gọi lưu giá trị ghi và các thông tin trạng thái khác Chương trình gọi khởi tạo liệu cục nó và bắt đầu thực Lệnh trả thực các công việc sau: Chương trình bị gọi gởi giá trị trả vào mẩu tin kích hoạt chương trình gọi Căn vào thông tin trường trạng thái, chương trình bị gọi khôi phục top_sp giá trị các ghi và truyền tới địa trả mã chương trình gọi Mặc dù top_sp đã bị giảm, chương trình gọi cần chép giá trị trả vào mẩu tin kích hoạt nó để sử dụng cho việc tính toán biểu thức Dữ liệu có kích thước thay đổi Một số ngôn ngữ cho phép liệu có kích thước thay đổi Chẳng hạn chương trình p có mảng có kích thước thay đổi, các mảng này lưu trữ ngoài mẩu tin kích hoạt p Trong mẩu tin kích hoạt p chứa các trỏ trỏ tới điểm bắt đầu mảng Ðịa tương đối các trỏ này biết thời gian dịch nên mã đích có thể truy nhập tới các phần tử mảng thông qua trỏ Hình sau trình bày chương trình q gọi p Mẩu tin kích hoạt q nằm sau các mảng p Truy nhập vào liệu Stack thông qua hai trỏ top, top.sp: top đỉnh Stack nơi mẩu tin kích hoạt có thể bắt đầu top_sp dùng để tìm liệu cục 151 (154) Liên kết điều khiển Mẩu tin kích hoạt p Con trỏ tới A Con trỏ tới B Con trỏ tới C Mảng A Mảng B Các mảng p Mảng C Liên kết điều khiển top_sp Mẩu tin kích hoạt cho q gọi p Các mảng q top Hình 7.11 - Truy xuất các mảng cấp phát động Cấp phát Heap Chiến thuật cấp phát sử dụng Stack không đáp ứng các yêu cầu sau: Giá trị tên cục giữ lại hoạt động chương trình kết thúc Hoạt động chương trình bị gọi tồn sau chương trình gọi Các yêu cầu trên không thể cấp phát và thu hồi theo chế LIFO (Last - In, First - Out) tức là tổ chức theo Stack Heap là khối ô nhớ liên tục chia nhỏ để có thể cấp phát cho các mẩu tin kích hoạt các đối tượng liệu khác Sự khác cấp phát Stack và Heap là chỗ mẩu tin cho hoạt động giữ lại hoạt động đó kết thúc 152 (155) Các hoạt động Các mẩu tin kích hoạt Stack s s r q(1,9) Các mẩu tin kích hoạt Heap s q(1,9) Liên kết điều khiển r Liên kết điều khiển q(1,9) Liên kết điều khiển Hình 7.12 - Mẩu tin kích hoạt giữ lại Heap Về mặt vật lý, mẩu tin kích hoạt cho q(1,9) không phụ thuộc mẩu tin kích hoạt cho r Khi mẩu tin kích hoạt cho r bị giải phóng thì quản lý Heap có thể dùng vùng nhớ tự này để cấp phát cho mẩu tin khác Một số vấn đề thuộc quản lý hiệu Heap trình bày mục VIII IV TRUY XUẤT TÊN KHÔNG CỤC BỘ Quy tắc tầm vực Quy tắc tầm vực ngôn ngữ xác định việc xử lý tham khảo đến các tên không cục Quy tắc tầm vực bao gồm hai loại: Quy tắc tầm tĩnh và quy tắc tầm động Quy tắc tầm tĩnh (static - scope rule): Xác định khai báo áp dụng cho tên cách kiểm tra văn chương trình nguồn Các ngôn ngữ Pascal, C và Ada sử dụng quy tắc tầm tĩnh với quy định bổ sung: “tầm gần nhất” Quy tắc tầm động (dynamic- scope rule): Xác định khai báo có thể áp dụng cho tên thời gian thực cách xem xét hoạt động hành Các ngôn ngữ Lisp, APL và Snobol sử dụng quy tắc tầm động Cấu trúc khối Một khối bắt đầu tập hợp các khai báo cho tên (khai báo biến, định nghĩa kiểu, định nghĩa ) sau đó là tập hợp các lệnh mà đó các tên có thể tham khảo Cấu trúc khối thường sử dụng các ngôn ngữ cấu trúc Pascal, Ada, PL/1 Trong đó chương trình hay chương trình tổ chức thành các khối lồng 153 (156) Ngôn ngữ cấu trúc khối sử dụng quy tắc tầm tĩnh Tầm khai báo cho quy tắc tầm gần (most closely nested) Một khai báo đầu khối xác định tên cục khối đó Bất kỳ tham khảo tới tên thân khối xem xét là tham khảo tới liệu cục khối nó tồn Nếu tên x tham khảo thân khối B và x không khai báo B thì x xem là tham khảo tới khai báo B’ là khối nhỏ chứa B Nếu B’ không có khai báo cho x thì lại tham khảo tới B’’ là khối nhỏ chứa B’ Nếu khối chứa định nghĩa các khối khác thì khai báo các khối hoàn toàn bị che dấu khối ngoài Cấu trúc khối có thể cài đặt cách sử dụng chế cấp phát Stack Khi điều khiển vào khối thì ô nhớ cho các tên cấp phát và chúng bị thu hồi điều khiển rời khỏi khối Tầm tĩnh với các chương trình không lồng Quy tắc tầm tĩnh ngôn ngữ C đơn giản so với Pascal và các định nghĩa chương trình C không lồng Một chương trình C là chuỗi các khai báo biến và hàm Nếu có tham khảo không cục đến tên a hàm nào đó thì a phải tham khảo bên ngoài tất các hàm Tất các tên khai báo bên ngoài hàm có thể cấp phát tĩnh Vị trí các ô nhớ này biết thời gian dịch đó tham khảo tới tên không cục thân hàm xác định địa tuyệt đối Các tên cục hàm nằm mẩu tin hoạt động trên đỉnh Stack và có thể xác định cách sử dụng địa tương đối Tầm tĩnh với các chương trình lồng Trong ngôn ngữ Pascal các chương trình có thể lồng nhiều cấp Ví dụ 7.5: Xét chương trình (1) program sort(input, output); (2) var a: array [0 10] of integer; (3) x : integer; (4) procedure readarray; (5) var y : integer; (6) begin a end; {readarray} (7) procedure exchange(i,j:integer); (8) begin (9) x:= a[i]; a[i] := a[j]; a[j] := x; (10) end; {exchange} (11) procedure quicksort(m,n:integer); (12) var k,v: integer; 154 (157) (13) function partition(y,z: integer) : integer; (14) var i,j : integer; (15) begin a (16) .v (17) .exchange(i,j) (18) end; {partition} (19) begin end; {quicksort} (20) begin end; {sort} Hình 7.13 - Một chương trình Pascal với các chương trình lồng Xét chương trình partition, đó tham khảo đến các tên không cục như: a: Khai báo chương trình chính v: khai báo quicksort; exchange:khai báo chương trình chính Ðộ sâu lồng Chúng ta sử dụng thuật ngữ độ lồng sâu để tầm tĩnh Tên chương trình chính có độ sâu cấp và chúng ta tăng thêm từ chương trình vào chương trình bao (khai báo) nó Như chương trình partition, a có độ sâu cấp 1, v có độ sâu cấp 2, i có độ sâu cấp Quicksort có độ sâu cấp 2, partition có độ sâu cấp 3, exchange có độ sâu cấp Liên kết truy xuất Ðể cài đặt tầm tĩnh cho các chương trình lồng ta dùng trỏ liên kết truy xuất mẩu tin kích hoạt Nếu chương trình p lồng trực tiếp q thì liên kết mẩu tin kích hoạt p trỏ tới liên kết truy xuất mẩu tin kích hoạt hành q Hình sau mô tả nội dung Stack thực chương trình sort ví dụ trên Ví dụ 7.6: s s a,x a,x q(1,9) q(1,9) access link q(1,9) k,v access link q(1,3) access link k,v (a) k,v access link (b) k,v 155 (158) s s a,x a,x q(1,9) q(1,9) k,v access link q(1,9) access k,v link q(1,3) q(1,3) k,v access link access link k,v k,v p(1,3) p(1,3) access link access link i,j i,j access link e(1,3) (c) (d) access link Hình 7.14 - Liên kết truy xuất cho phép tìm kiếm ô nhớ các tên không cục Liên kết truy xuất s rỗng vì s không có bao đóng Liên kết truy xuất mẩu tin kích hoạt chương trình trỏ đến mẩu tin kích hoạt bao đóng nó Giả sử chương trình p có độ lồng sâu np tham khảo tới tên không cục a có độ lồng sâu na <= np Việc tìm đến địa a tiến hành sau: - Khi chương trình p gọi thì mẩu tin kích hoạt p nằm trên đỉnh Stack Tính giá trị np - na Giá trị này tính thời gian dịch - Ði xuống np - na mức theo liên kết truy xuất ta tìm đến mẩu tin kích hoạt chương trình đó a khai báo Tại đây địa a xác định cách lấy địa mẩu tin cộng với độ dời a (địa tương đối a) Ví dụ 7.7 (ứng với hình 7.14c) : Hàm partition có độ lồng sâu là np = tham khảo tới biến a có độ lồng sâu na = và biến v có độ lồng sâu nv =2 Ðể xác định a cần tính np- na = -1 = => cần hạ hai cấp Từ p(1,3) hạ cấp đến q(1,3) theo liên kết truy xuất Từ q(1,3) hạ cấp đến s theo liên kết truy xuất đến s là nơi a khai báo 156 (159) Ðể xác định v cần tính np- nv = 3- = => cần hạ cấp xuốn q(1,3) là nơi v khai báo Giả sử chương trình p có độ lồng sâu np gọi chương trình e độ lồng sâu ne Ðoạn mã để thiết lập liên kết truy xuất phụ thuộc vào việc chương trình gọi có định nghĩa chương trình gọi hay không? Trường hợp 1: np < ne: Chương trình e có độ lồng sâu lớn chương trình p đó e lồng p p không thể tham khảo đến e (e bị che dấu khỏi p) Ví dụ sort gọi quickort, quicksort gọi partition Trường hợp 2: np >= ne: chương trình e có độ lồng sâu nhỏ độ lồng sâu chương trình p Theo quy tắc tầm tĩnh thì p có thể tham khảo e Ví dụ quicksort gọi chính nó, partition gọi exchange Từ chương trình gọi np-ne +1 bước làm theo liên kết truy nhập ta tìm mẩu tin kích hoạt bao đóng gần chứa chương trình gọi và chương trình gọi Chẳng hạn p(1,3) gọi e(1,3), np =3, ne =2 Ta phải làm - + hai bước theo liên kết truy xuất từ p đến s Display: để truy xuất nhanh các tên không cục người ta dùng mảng d các trỏ tới các mẩu tin kích hoạt mảng này gọi là display Giả sử điều khiển nằm hoạt động chương trình t có độ lồng sâu j thì j-1 phần tử display trỏ tới các mẩu tin kích hoạt các bao đóng gần p và d[j] trỏ tới kích hoạt p Một tên không cục a có độ sâu i nằm mẩu tin kích hoạt trỏ d[i] Ví dụ 7.8: s s d[1] d[1] d[2] q(1,9) d[2] q(1,9) saved d[2] saved d[2] (a) q(1,3) saved d[2] (b) 157 (160) s d[1] d[2] d[3] q(1,9) saved d[2] s d[1] d[2] d[3] q(1,9) saved d[2] q(1,3) q(1,3) saved d[2] (d) (c) saved d[2] p(1,3) p(1,3) saved d[3] saved d[3] e(1,3) saved d[2] Hình 7.15 - Sử dụng display các chương trình không truyền các tham số (a): Tình trạng trước q(1,3) bắt đầu, quicksort có độ lồng sâu cấp 2, d[2] gửi cho mẩu tin kích hoạt quicksort nó bắt đầu Giá trị d[2] lưu mẩu tin kích hoạt q(1,9) (b): Khi q(1,3) bắt đầu d[2] trỏ tới mẩu tin kích hoạt mức ứng với q(1,3), giá trị d[2] lại lưu mẩu tin này Giá trị này là cần thiết để phục hồi display cũ điều khiển trả cho q(1,9) Như mẩu tin kích họat đẩy vào Stack thì: - Lưu giá trị d[i] vào mẩu tin đó - Ðặt d[i] trỏ tới mẩu tin đó Khi mẩu tin pop khỏi Stack thì d[i] phục hồi Giả sử chương trình có độ lồng sâu cấp j gọi chương trình có độ lồng sâu cấp i Có hai trường hợp xảy phụ thuộc chương trình gọi có định nghĩa chương trình gọi hay không Trường hợp 1: j < i => i = j+1: thêm ô nhớ d[i], cấp phát mẩu tin kích hoạt cho chương trình i, ghi d[i] vào đó và đặt d[i] trỏ tới nó (ví dụ 7.8a, 7.8c) Trường hợp 2: j >= i: Ghi giá trị cũ d[i] vào mẩu tin kích hoạt và đặt d[i] trỏ vào mẩu tin cuối (ví dụ 7.8b và 7.8d) Tầm động Với khái niệm tầm động, hoạt động kế thừa liên kết đã tồn tên không cục Tên không cục a hoạt động chương trình gọi tham khảo đến cùng ô nhớ hoạt động chương trình gọi Ðối với tên cục thì liên kết thiết lập tới ô nhớ mẩu tin hoạt động 158 (161) Ví dụ 7.9: Xét chương trình: (1) program dynamic (input, output); (2) var r : real; (3) procedure show; (4) begin write(r : : 3); end; (5) procedure small; (6) var r : real; (7) begin r := 0.125; show; end; (8) begin (9) r := 0.25; (10) show, small, writeln; (11) end; Hình 7.16 - Kết chương trình tùy thuộc vào tầm động hay tầm tĩnh sử dụng Kết thực chương trình: • Dưới tầm tĩnh; 0.250 0.250 • Dưới tầm động: 0.250 0.125 Khi show gọi dòng 10 chương trình chính thì 0.250 in vì r chương trình chính sử dụng Tuy nhiên show gọi dòng small thì 0.125 in vì r chương trình small sử dụng Cơ chế tầm động sử dụng liên kết điều khiển để tham khảo tên không cục Dynamic Dynamic r = 0.25 r = 0.25 show small control link control link r = 0.125 Show gọi dòng 10 tham khảo r= 0.25 show control link Show gọi dòng tham khảo r = 0.125 159 (162) Hình 7.17 - Sử dụng liên kết điều khiển để tham khảo các tên không cục V TRUYỀN THAM SỐ Khi chương trình gọi chương trình khác thì phương pháp thông thường để giao tiếp chúng là thông qua tên không cục và thông qua các tham số chương trình gọi Ví dụ 7.10: Ðể đổi hai giá trị a[i] và a[j] cho ta dùng (1) procedure exchange(i,j : integer); (2) var x : integer; (3) begin (4) (5) x := a[i]; a[i] := a[j]; a[j] := x; end; đó mảng a là tên không cục và i,j là các tham số Có nhiều phương pháp truyền tham số như: - Truyền giá trị (Transmision by value, call- by-value) - Truyền tham khảo (Transmision by name, call- by-name) Ở đây chúng ta xét hai phương pháp phổ biến nhất: Truyền giá trị Là phương pháp đơn giản truyền tham số sử dụng C và Pascal Truyền giá trị xử lý sau: Tham số hình thức xem là tên cục đó ô nhớ các tham số hình thức nằm mẩu tin kích hoạt chương trình gọi Chương trình gọi đánh giá các tham số thực tế và đặt các giá trị chúng vào ô nhớ tham số hình thức Truyền tham chiếu (truyền địa hay truyền vị trí) Chương trình gọi truyền cho chương trình gọi trỏ tới địa tham số thực tế Ví dụ 7.11: (1) program reference (input, output) (2) var i: integer; (3) a: array[0 10] of integer; (4) procedure swap(var x, y: integer); (5) var temp : integer; (6) begin (7) temp := x; 160 (163) (8) x := y; (9) y := temp; (10) end; (11) begin (12) i := 1; a[1] := 2; (13) swap(i,a[1]); (14) end; Hình 7.18 - Chương trình Pascal với thủ tục swap Với lời gọi dòng (13) ta có các bước sau: Copy địa i và a[ i] vào mẩu tin hoạt động swap thành arg1, arg2 tương ứng với x, y Ðặt temp nội dung vị trí trả arg1 tức là temp := Bước này tương ứng lệnh temp := x dòng (7) swap Ðặt nội dung vị trí trỏ arg1 giá trị vị trí trả arg2, tức là i := a[1] Bước này tương ứng lệnh x := y dòng (8) swap Ðặt nội dung vị trí trỏ arg2 giá trị temp Tức là a[1] := i Bước này tương ứng lệnh y := temp VI BẢNG KÝ HIỆU Chương trình dịch sử dụng bảng ký hiệu để lưu trữ thông tin tầm vực và mối liên kết các tên Bảng ký hiệu truy xuất nhiều lần tên xuất chương trình nguồn Có hai chế tổ chức bảng ký hiệu là danh sách tuyến tính và bảng băm Cấu trúc ô bảng ký hiệu Mỗi ô bảng ký hiệu tương ứng với tên Ðịnh dạng các ô này thường không giống vì thông tin lưu trữ tên phụ thuộc vào việc sử dụng tên đó Thông thường ô cài đặt mẩu tin Nếu muốn có đồng các mẩu tin ta có thể lưu thông tin bên ngoài bảng ký hiệu, ô bảng chứa các trỏ trỏ tới thông tin đó, Trong bảng ký hiệu có thể có lưu các từ khóa ngôn ngữ Nếu thì chúng phải đưa vào bảng ký hiệu trước phân tích từ vựng bắt đầu Vấn đề lưu trữ lexeme danh biểu Các danh biểu các ngôn ngữ lập trình thường có hai loại: Một số ngôn ngữ quy định độ dài danh biểu không vượt quá giới hạn nào đó Một số khác không giới hạn độ dài 161 (164) Trường hợp danh biểu bị giới hạn độ dài thì chuỗi các ký tự tạo nên danh biểu lưu trữ bảng ký hiệu Attribute Name s o r t e a d a r 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 lưu mảng riêng và bảng ký hiệu giữ các trỏ trỏ tới đầu Lexeme Name Attribute SymTable Lexeme s o r t eos a eos r e a d a r r a y eos i eos 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 Tổ chức bảng ký hiệu danh sách tuyến tính Cấu trúc đơn giản, dễ cài đặt cho bảng ký hiệu là danh sách tuyến tính các mẩu tin Ta dùng mảng 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 đưa vào danh sách theo thứ tự mà chúng phát Vị trí mảng đánh dấu trỏ available ô bảng tạo Việc tìm kiếm tên bảng ký hiệu 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 thông tin độ sâu tên Bằng cách tìm kiếm từ available trở đầu mảng chúng ta đảm bảo tìm thấy tên tầng gần id1 info id2 info2 162 (165) Hình 7.21 - Danh sách tuyến tính các mẩu tin Tổ chức bảng ký hiệu bảng băm Kỹ thuật sử dụng bảng băm để cài đặt bảng ký hiệu thường sử dụng vì tính hiệu 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 CP m match 20 last action ws 32 210 Hình 7.22 - Bảng băm có kích thước 211 Bảng băm là mảng bao gồm m trỏ Bảng danh biểu chia thành m danh sách liên kết, danh sách liên kết trỏ phần tử 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 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ả giá trị nằm và m- h(s) = t => Danh biểu s đưa vào danh sách liên kết trỏ phần tử t 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 sau: Giả sử s bao gồm các ký tự c1, c2, c3, , ck Mỗi ký tự cho ứng với số nguyên dương n1, n2, n3, ,nk; lấy h = n1 + n2 + + nk Xác định h(s) = h mod m 163 (166) BÀI TẬP CHƯƠNG VII 7.1 Hãy dùng quy tắc tầm vực ngôn ngữ Pascal để xác định tầm vực ý nghĩa các khai báo cho lần xuất tên a, b chương trình sau Output chương trình là các số nguyên từ đến Program a ( input, output); Procedure b ( u, v, x, y : integer); Var a : record a, b : integer end; b : record a, b : integer end; begin With a begin a := u ; b := v end; With b begin a := x ; b := y end; Writeln ( a.a, a.b, b.a, b.b ); end; Begin B ( 1, 2, 3, 4) End 7.2 Chương trình sau in giá trị nào giả sử thông số truyền bằng: a) trị b) quy chiếu c) trị - kết d) tên Program main ( input, output); Procedure p ( x, y, z ); begin y := y + 1; z := z + x; end; Begin a := ; b := ; p (a +b ; a, a ) print a 164 (167) End 7.3 Cho đoạn chương trình Algol sau : begin Procedure A ( px); procedure px { tham số hình thức px là thủ tục } begin procedure B ( pz); procedure pz { tham số hình thức pz là thủ tục } begin pz; end; B (px); end; procedure C; begin procedure D; begin end; A(D); end; C; end Hãy giải thích quá trình thực thi chương trình trên, các bước truyền tham số (giải thích hình ảnh Stack) 7.4 Cho đoạn chương trình sau: var a, b : integer; Procedure Var AB a, c : real; k, l : integer; procedure Var AC x, y : real; b : array [ 10] of integer; begin 165 (168) end; begin end; begin end Hãy xây dựng bảng ký hiệu thao các phương pháp sau: a) Danh sách tuyến tính b) Băm (hash), giả sử ta có kết hàm biến đổi băm sau: a = 3; b = 4; c = 4; k = 2; x = 4; y = 5; AB = 2; AC = 6; l = 3; 7.5 Cho đoạn chương trình sau: Program Var baitap; a : real; procedure Var sub1 ; x, y : real; begin end; procedure Var sub2 (t :integer); k : integer; procedure Var sub3 ; m : real; begin end; procedure Var t; x, y : real; begin end; 166 (169) begin end Hãy vẽ bảng ký hiệu cho chương trình có trỏ trỏ đến bảng ký hiệu chương trình bị gọi và có trỏ trỏ ngược lại bảng ký hiệu chương trình gọi nó 167 (170) CHƯƠNG VIII SINH Mà TRUNG GIAN Nội dung chính: Thay vì chương trình nguồn dịch trực tiếp sang mã đích, nó nên dịch sang dạng mã trung gian kỳ trước trước tiếp tục dịch sang mã đích kỳ sau vì số tiện ích: Thuận tiện muốn thay đổi cách biểu diễn chương trình đích; Giảm thời gian thực thi chương trình đích vì mã trung gian có thể tối ưu Chương này giới thiệu các dạng biểu diễn trung gian đặc biệt là dạng mã ba địa Phần lớn nội dung chương tập trung vào trình bày cách tạo sinh mã trung gian đơn giản dạng mã đại Bộ sinh mã này dùng phương thức trực tiếp cú pháp để dịch các khai báo, câu lệnh gán, các lệnh điều khiển sang mã ba địa Mục tiêu cần đạt: Sau học xong chương này, sinh viên phải nắm cách tạo sinh mã trung gian cho ngôn ngữ lập trình đơn giản (chỉ chứa số loại khai báo, lệnh điều khiển và câu lệnh gán) từ đó có thể mở rộng để cài đặt sinh mã cho ngôn ngữ phức tạp Tài liệu tham khảo: [1] Compilers : Principles, Technique and Tools - Alfred V.Aho, Jeffrey D.Ullman Addison - Wesley Publishing Company, 1986 I NGÔN NGỮ TRUNG GIAN Cây cú pháp, ký pháp hậu tố và mã địa là các loại biểu diễn trung gian Biểu diễn đồ thị Cây cú pháp mô tả cấu trúc phân cấp tự nhiên chương trình nguồn DAG cho ta cùng lượng thông tin cách biểu diễn ngắn gọn đó các biểu thức không 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: := := + a * b + a * c Cây cú pháp b * - b c c DAG Hình 8.1- Biểu diễn đồ thị a :=b * - c + b * - c 168 (171) Ký pháp hậu tố là biểu diễn tuyến tính cây cú pháp Nó là danh sách các nút cây, đó nút xuất sau nó a b c - * b c - * + := là biểu diễn hậu tố cây cú pháp hình trên Cây cú pháp có thể cài đặt phương pháp: - Mỗi nút biểu diễn mẫu tin, với trường cho toán tử và các trường khác trỏ đến nó - Một mảng các mẩu tin, đó số phần tử mảng đóng vai trò là trỏ nút Tất các nút trên cây cú pháp có thể tuân theo trỏ, nút gốc 10 := id a + := * id b id - b - id b id c - * id b id c - * + id a 10 := 11 id c d c cây cú pháp hình 8.1 Hình 8.2 - Hai biểu idiễn Mã địa Mã lệnh địa là chuỗi các lệnh có dạng tổng quát là x :=y op z Trong đó x,y,z là tên, liệu tạm sinh dịch, op là toán tử số học logic Chú ý không có quá toán tử vế phải lệnh Do đó biểu thức x + y * z phải dịch thành chuỗi : t1 := y * z t2 := x + t1 Trong đó t1, t2 là tên tạm sinh dịch Mã lệnh địa là biểu diễn tuyến tính cây cú pháp DAG, đó các tên tường minh biểu diễn cho các nút trên đồ thị t1 := - c t1 := -c t2 := b * t1 t2 := b * t1 t3 := - c t3 := t2 + t2 t4 := b * t3 a := t3 169 (172) t5 := t2 + t4 a := t5 Mã lệnh địa cây cú pháp Mã lệnh địa DAG Hình 8.3 - Mã lệnh địa tương ứng với cây cú pháp và DAG hình 8.1 Các mã lệnh địa phổ biến Lệnh gán dạng x := y op z, đó op là toán tử ngôi số học logic Lệnh gán dạng x := op y, đó op là toán tử ngôi Chẳng hạn, phép lấy số đối, toán tử NOT, các toán tử SHIFT, các toán tử chuyển đổi Lệnh COPY dạng x :=y, đó giá trị y gán cho x Lệnh nhảy không điều kiện goto L Lệnh địa có nhãn L là lệnh thực Các lệnh nhảy có điều kiện if x relop y goto L Lệnh này áp dụng toán tử quan hệ relop (<, =, >=, ) vào x và y Nếu x và y thỏa quan hệ relop thì lệnh nhảy với nhãn L thực hiện, ngược lại lệnh đứng sau IF x relop y goto L thực param x và call p, n cho lời gọi chương trình và return y Trong đó, y biểu diễn giá trị trả có thể lựa chọn Cách sử dụng phổ biến là chuỗi lệnh địa param x1 param x2 param call xn p, n sinh là phần lời gọi chương trình p (x1,x2, , xn) Lệnh gán dạng x := y[i] và x[i] := y Lệnh đầu lấy giá trị vị trí nhớ y xác định giá trị ô nhớ i gán cho x Lệnh thứ lấy giá trị y gán cho ô nhớ x xác định i Lệnh gán địa và trỏ dạng x :=&y , x := *y và *x := y Trong đó, x := &y đặt giá trị x vị trí y Câu lệnh x := *y với y là trỏ mà r_value nó là vị trí, r_value x đặt nội dung vị trí này Cuối cùng *x := y đặt r_value đối tượng trỏ x r_value y Dịch trực tiếp cú pháp thành mã lệnh địa Ví dụ 8.2: Ðịnh nghĩa S _ thuộc tính sinh mã lệnh địa cho lệnh gán: Luật sinh Luật ngữ nghĩa S→ id := E S.code := E.code || gen (id.place ':=' E.place) E→ E1 + E2 E.place := newtemp; E.code := E1.code || E2.code || gen (E.place ':=' E1.place '+' E2.place) E→ E1 * E2 E.place := newtemp; E.code := E1.code || E2.code || 170 (173) gen (E.place ':=' E1.place '*' E2.place) E.place := newtemp; E.code := E1.code|| gen (E.place ':=' 'uminus' E1.place ) E→ - E1 E.place := newtemp; E.code := E1.code E→ (E1) E.place := id.place; E.code := '' E→ id Hình 8.4 - Ðịnh nghĩa trực tiếp cú pháp sinh mã lệnh ba địa cho lệnh gán Với chuỗi nhập a = b * - c + b * - c, nó sinh mã lệnh địa t1 := -c t2 := b * t1 t3 := - c thuộc tính tổng hợp S.code biểu diễn mã địa cho lệnh gán S Ký hiệu chưa kết thúc E có thuộc tính E.place là giá trị E và E.code là chuỗi lệnh địa để đánh giá E t4 := b * t3 t5 := t2 + t4 a := t5 Khi mã lệnh địa đuợc sinh, tên tạm tạo cho nút trên cây cú pháp Giá trị ký hiệu chưa kết thúc E luật sinh E → E1 + E2 tính vào tên tạm t Nói chung mã lệnh địa cho lệnh gán id := E bao gồm mã lệnh cho việc đánh giá E vào biến tạm t, sau đó là lệnh gán id.place := t Hàm newtemp trả chuỗi các tên t1, t2, , tn tương ứng các lời gọi liên tiếp Gen (x ':=' y '+' z) để biểu diễn lệnh địa x :=y+z S.begin: E.code if E.place = goto S.after S1.code S.after: goto S.begin Luật sinh S→ while E S1 Luật ngữ nghĩa S.begin := newlabel; S.after := newlabel; S.code := gen(S.begin ':') || E.code || gen('if' E.place '=' 'goto' S.after) || S1.code || gen('goto' S.begin) || gen(S.after ':') 171 (174) Hình 8.5 - Ðịnh nghĩa trực tiếp cú pháp sinh mã lệnh ba địa cho câu lệnh while Lệnh S → while E S1 sinh cách dùng các thuộc tính S.begin và S.after để đánh dấu lệnh đầu tiên đoạn mã lệnh E và lệnh sau đoạn mã lệnh S Hàm newlabel trả nhãn lần gọi Cài đặt lệnh địa Lệnh địa là dạng trừu tượng mã lệnh trung gian Trong chương trình dịch, các mã lệnh này có thể cài đặt mẩu tin với các trường cho toán tử và toán hạng Có cách biểu diễn là tứ, tam và tam gián tiếp Bộ tứ Bộ tứ (quadruples) là cấu trúc mẩu tin có trường ta gọi là op, arg1, arg2 và result Trường op chứa toán tử Lệnh địa x := y op z biểu diễn cách thay y arg1, z arg2 và x result Các lệnh với toán tử ngôi x := -y hay x := y thì không sử dụng arg2 Các toán tử param không sử dụng arg2 lẫn result Các lệnh nhảy có điều kiện và không điều kiện đặt nhãn đích result Nội dung các trường arg1, arg2 và result trỏ tới ô bảng ký hiệu các tên biểu diễn các trường này Nếu thì các tên tạm phải đưa vào bảng ký hiệu chúng tạo Ví dụ 8.3: Bộ tứ cho lệnh a := b * -c + b * -c op arg1 arg2 result (0) uminus c (1) * b (2) uminus c (3) * b t3 t4 (4) + t2 t4 t5 (5) := t5 t1 t1 t2 t3 a Hình 8.6 - Biểu diễn tứ cho các lệnh ba địa Bộ tam Ðể tránh phải lưu tên tạm bảng ký hiệu; chúng ta có thể tham khảo tới giá trị tạm vị trí lệnh tính nó Ðể làm điểu đó ta sử dụng tam (triples) là mẩu tin có trường op, arg1 và arg2 Trong đó, arg1 và arg2 trỏ tới bảng ký hiệu (đối với tên người lập trình định nghĩa) trỏ tới phần tử tam (đối với giá trị tạm) (0) (1) (2) (3) (4) (5) op uminus * uminus * + := arg1 c b c b (1) a arg2 (0) (2) (3) (4) Hình 8.7 - Biểu diễn tam cho các lệnh ba địa Các lệnh x[i]:=y và x:=y[i] sử dụng ô cấu trúc tam 172 (175) (0) (2) op [] := arg1 x (0) arg2 i y (0) (2) op [] := arg1 y x arg2 i (0) Hình 8.8 - Biểu diễn tam cho x[i]:=y và x:=y[i] Bộ tam gián tiếp Một cách biểu diễn khác tam là thay vì liệt kê các tam trực tiếp ta sử dụng danh sách các trỏ các tam (0) (1) (2) (3) (4) (5) statements (14) (15) (16) (17) (18) (19) (14) (15) (16) (17) (18) (19) op uminus * uminus * + := arg1 c b c b (15) a arg2 (14) (16) (17) (18) Hình 8.9 - Biểu diễn tam gián tiếp cho các lệnh ba địa II KHAI BÁO Khai báo chương trình Các tên cục chương trình truy xuất đến thông qua địa tương đối nó Gọi là offset Ví dụ 8.4: Xét lược đồ dịch cho việc khai báo biến P→ {offset:= } D D→D;D D → id : T {enter(id.name, T,type, offset); offset := offset + T.width } T → integer { T.type:= integer; T.width := } T → real { T.type:= real ; T.width := } T → array[ num] of T1{ T.type:= array(num.val, T1.type); T.width := num.val * T1.width } T→ ↑ T1 { T.type := pointer(T1.type) ; T.width := } Hình 8.10 - Xác định kiểu và địa tương đối các tên khai báo Trong ví dụ trên, ký hiệu chưa kết thúc P sinh chuỗi các khai báo dạng id:T Trước khai báo đầu tiên xét thì offset = Khi khai báo tìm thấy tên và giá trị offset đưa vào bảng ký hiệu, sau đó offset tăng lên khoảng kích thước đối tượng liệu cho tên đó Thủ tục enter(name, type, offset) tạo ô bảng ký hiệu với tên, kiểu và địa tương đối vùng liệu nó Ta sử dụng các thuộc tính tổng hợp type và width để kiểu và kích thước (số đơn vị nhớ) kiểu đó Chú ý lược đồ dịch P → {offset := 0} D có thể viết lại cách thay hành vi {offset := 0} ký hiệu chưa kết thúc M để 173 (176) P→MD M → ε {offset := } Tất các hành vi nằm cuối vế phải Lưu trữ thông tin tầm Trong ngôn ngữ mà chương trình phép khai báo lồng Khi chương trình tìm thấy thì quá trình khai báo chương trình bao bị tạm dừng Văn phạm cho khai báo đó là; P→D D → D ; D | id: T | proc id ; D; S Ðể đơn giản chúng ta tạo bảng ký hiệu riêng cho chương trình Khi khai báo chương trình D → proc id D1 ; S tạo và các khai báo D1 lưu trữ bảng ký hiệu Ví dụ 8.5: Chương trình Sort có bốn chương trình lồng readarray, exchange, quicksort và partition Ta có năm bảng ký hiệu tương ứng sort quicksort nil header a readarray k x header v readarray partition exchange header partition quicksort id exchange header header i j Hình 8.11 - Những bảng ký hiệu các chương trình lồng Luật ngữ nghĩa xác định các thao tác sau mktable (previous): Tạo bảng ký hiệu và trỏ tới bảng đó Tham số previous là trỏ trỏ tới bảng ký hiệu chương trình bao Con trỏ previous lưu header bảng ký hiệu Trong header còn có thể có các thông tin khác độ sâu lồng chương trình enter (table, name, type, offset): Tạo ô bảng ký hiệu trỏ table addwidth (table, width): Ghi kích thước tích lũy tất các ô bảng vào header kết hợp với bảng đó 174 (177) enterproc (table, name, newtable): Tạo ô cho tên chương trình vào bảng trỏ table newtable trỏ tới bảng ký hiệu chương trình này Ta có lược đồ dịch P→MD { addwidth(top(tblptr), top(offset)); pop(tblptr); pop(offset) } M→ε { t:=mktable(nil); push(t, tblptr) ; push(0,offset) } D→ D1 ; D2 D→ proc id ; N D1 ; S D→ id : T {t:= top(tblptr); addwidth(t, top(offset)); pop(tblptr; pop(offset); enterproc(top(tblptr), id_name,t) } {enter(top(tblptr), id_name, T.type, top(offset)); top(offset):= top(offset) + T.width } N→ ε {t:=mktable(top(tblptr)); push(t, tblptr); push(0,offset) } Hình 8.12 - Xử lý các khai báo chương trình lồng Ta dùng Stack tblptr để giữ các trỏ bảng ký hiệu Chẳng hạn, các khai báo partition khảo sát thì tblptr chứa các trỏ các bảng sort, quicksoft và partition Con trỏ bảng hành nằm trên đỉnh Stack offset là Stack khác để lưu trữ offset Chú ý với lược đồ A→ BC {action A} thì tất các hành vi các cây B và C thực trước A Do đó hành vi kết hợp với M thực trước Nó không tạo bảng ký hiệu cho tầng ngoài cùng (chương trình sort) cách dùng mktable(nil), trỏ tới bảng này đưa vào Stack tblptr đồng thời đưa vào Stack offset Ký hiệu chưa kết thúc N đóng vai trò tương tự M khai báo chương trình xuất Nó dùng mktable(top(tblptr)) để tạo bảng Tham số top(tblptr) cho giá trị trỏ tới bảng lại push vào đỉnh Stack tblptr và push vào Stack offset Với khai báo biến id:T; ô tạo bảng ký hiệu hành Giá trị top(offset) tăng lên T.width Khi hành vi vế phải P → proc id ; N D1 ; S diễn ra, kích thước tất các đối tượng liệu khai báo D1 nằm trên đỉnh Stack offsert Nó lưu trữ cách dùng addwidth, các Stack tblptr và offset bị pop và chúng ta trở để thao tác trên các khai báo chương trình Xử lý mẩu tin Khai báo mẩu tin cho luật sinh T→ record D end Luật dịch tương ứng T→ record L D end { T.type := record(top(tblptr)); T.width := top(offset); pop(tblptr) ; pop(offset) } L→ ε { t:= mktable(nil); push(t,tblptr) ; push(0,offset) } Hình 8.13 - Cài đặt bảng ký hiệu cho các tên trường mẩu tin 175 (178) Sau từ khóa record tìm thấy thì hành vi kết hợp với L tạo bảng ký hiệu cho các tên trường Các hành vi D → id : T đưa thông tin tên trường id vào bảng ký hiệu cho mẩu tin III LỆNH GÁN Tên bảng ký hiệu Xét lược đồ dịch để sinh mã lệnh địa cho lệnh gán: S→ id := E {p:=lookup( id.name); if p <> nil then emit( p ':=' E.place) else error } E→ E1 + E2 { E.place := newtemp; emit(E.place ':=' E1.place '+’ E2.place) } E→ E1 * E2 { E.place := newtemp; emit(E.place ':=' E1.place '*’ E2.place) } E→ - E1 { E.place := newtemp; emit(E.place ':=' 'unimus' E1.place) } E→ ( E1 ) E→ id { E.place:=E1.place) } { p:=lookup( id.name); if p <> nil then E.place := p else error } Hình 8.14 - Lược đồ dịch sinh mã lệnh ba địa cho lệnh gán Hàm lookup tìm bảng ký hiệu xem có hay không tên cho id.name Nếu có thì trả trỏ ô, không trả nil Xét luật sinh D → proc id ; ND1 ; S Như trên đã nói, hành vi kết hợp với ký hiệu chưa kết thúc N cho phép trỏ bảng ký hiệu cho chương trình nằm trên đỉnh Stack tblptr Các tên lệnh gán sinh ký hiệu chưa kết thúc S khai báo chương trình này bao nó Khi tham khảo tới tên thì trước hết hàm lookkup tìm xem có tên đó bảng ký hiệu hành hay không (Bảng danh biểu hành trỏ top(tblptr)) Nếu không thì dùng trỏ header bảng để tìm bảng ký hiệu bao nó và tìm tên đó Nếu tên không tìm thấy tất các mức thì lookup trả nil Ðịa hóa các phần tử mảng Các phần tử mảng có thể truy xuất nhanh chúng liền khối các ô nhớ kết tiếp Trong mảng chiều kích thước phần tử là w thì địa tương đối phần tử thứ i mảng A tính theo công thức Ðịa tương đối A[i] = base + (i-low) * w Trong đó low: là cận tập số base: là địa tương đối ô nhớ cấp phát cho mảng tức là địa tương đối A[low] 176 (179) Biến đổi chút ta Ðịa tương đối A[i]= i * w + (base -low * w) Trong đó: c=base - low * w có thể tính thời gian dịch và lưu bảng ký hiệu Do đó địa tương đối A[i] = i * w +c Mảng hai chiều co ïthể xem là mảng theo hai dạng: theo dòng (row_major) theo cột (colum_major) a[1,1] → a[1,2] → a[1,3] a[1,1] a[1,2] a[1,3] a[2,1] → a[2,2] → a[2,3] a[2,1] a[2,2] a[2,3] a[1,1] Dòng a[1,2] a[1,3] Cột Cột a[2,1] Dòng a[2,2] a[1,1] a[2,1] a[1,2] a[2,2] Cột a[2,3] Theo dòng a[1,3] a[2,3] Theo cột Hình 8.15 - Những cách xếp mảng hai chiều Trong trưòng hợp lưu trữ theo dòng, địa tương đối phần tử a[i1, j2] có thể tính theo công thức Ðịa tương đối A[i1, j2] = base + ((i1- low1) * n2 +j2 -low2) * w Trong đó low1 và low2 là cận hai tập số n2 : là số các phần tử dòng Nếu gọi high2 là cận trên tập số thứ thì n2 = high2 -low2 +1 Trong đó công thức trên có i1, i2 là chưa biết thời gian dịch Do đó, biến đổi công thức để : Ðịa tương đối A[i1, j2]= ((i1 * n2)+j2) * w +(base-((low1* n2)+low2) * w) Trong đó C= (base- ((low1 * n2) + low2) * w) tính thời gian dịch và ghi vào bảng ký hiệu Tổng quát hóa cho trường hợp k chiều, ta có Ðịa tương đối A[i1, i2, ik] là (( ((i1n2 + i2) n3 +i3) ) nk+ik) w+base-(( ((low1n2 + low2) n3+low3) )nk+ lowk) w Biến đổi kiểu lệnh gán Giả sử chúng ta có kiểu là integer và real; integer phải đổi thành real cần thiết Ta có, các hành vi ngữ nghĩa kết hợp với luật sinh E → E1 + E2 sau: E.place := newtemp 177 (180) if E1.type= integer and E2.type = integer then begin emit(E.place ':=' E1.place 'int + ' E2.place); E.type:= integer; end else if E1.type=real and E2.type =real then begin emit(E.place ':=' E1.place 'real + ' E2.place); E.type:= real; end else if E1.type=integer and E2.type =real then begin u:=newtemp; emit(u ':=' ‘intoreal' E1.place); emit(E.place ':=' u 'real +' E2.place); E.type:= real; end else if E1.type=real and E2.type =integer then begin u:=newtemp; emit(u ':=' 'intoreal' E2.place); emit(E.place ':= ' E1.place 'real +' u); E.type:= real; end else E.type := type_error; end Hình 8.16 - Hành vi ngữ nghĩa E → E1 +E2 Ví dụ 8.5: Với lệnh gán x := y + i * j đó x,y khai báo là real; i , j khai báo là integer Mã lệnh địa xuất là: t1 := i int * j t3 := intoreal t1 t2 := y real + t3 x := t2 IV BIỂU THỨC LOGIC Biểu thức logic sinh văn phạm sau: E→ E or E | E and E | not E | (E) | id relop id | true | false Trong đó or và and kết hợp trái; or có độ ưu tiên thấp nhất, là and và sau cùng là not Thông thường có phương pháp chính để biểu diễn giá trị logic Phương pháp 1: Mã hóa true và false các số và việc đánh giá biểu thức thực tương tự biểu thức số học, có thể biểu diễn true , false 0; các số khác không biểu diễn true, số không biểu diễn false 178 (181) Phương pháp 2: Biểu diễn giá trị biểu thức logic vị trí đến chương trình Phương pháp này thuận lợi để cài đặt biểu thức logic các điều khiển Biểu diễn số Sử dụng để biểu diễn true và để biểu diễn false Biểu thức đánh giá từ trái sang phải theo cách tương tự biểu thức số học Ví dụ 8.6: Với biểu thức a or b and not c, ta có dãy lệnh địa chỉ: t1 := not c t2 := b and t1 t3 := a or t2 Biểu thức quan hệ a<b tương đương lệnh điều kiện if a<b then else dãy lênh địa tương ứng là 100 : if a<b goto 103 101 : t := 102 : goto 104 103 : t :=1 104 : Ta có, lược đồ dịch để sinh mã lệnh địa biểu thức logic: E → E1 or E2 {E.place:= newtemp; emit(E.place ':=' E1.place 'or' E2.place) } E → E1 and E2 { E.place:= newtemp; emit(E.place ':=' E1.place 'and' E2.place)} E → not E1 {E.place:= newtemp; emit(E.place ':=' 'not' E1.place ) } E → id1 relop id2 { E.place:= newtemp; emit('if' id1.place relop.op id2.place 'goto' nextstat +3); emit(E.place ':=' '0'); emit('goto' nextstat +2); emit(E.place ':=' '1') } E → true { E.place:= newtemp; emit(E.place ':=' '1') } E → false { E.place:= newtemp; emit(E.place ':=' '0') } Hình 8.17 - Lược đồ dịch sử dụng biểu diễn số để sinh mã lệnh ba địa cho các biểu thức logic Ví dụ 8.7: Với biểu thức a < b or c< d and e < f, nó sinh lệnh địa sau: 100 : if a<b goto 103 101 : t1 := 102 : goto 104 103 : t1 := 104 : if c<d goto 107 105 : t2 := 106 : goto 108 107 : t2 := 179 (182) 108 : if e<f goto 111 109 : t3 := 110 : goto 112 111 : t3 := 112 : t4 := t2 and t3 113 : t5 := t1 or t4 Hình 8.18 - Sự biên dịch sang mã lệnh ba địa cho a<b or c<d and e<f Mã nhảy Ðánh giá biểu thức logic mà không sinh mã lệnh cho các toán tử or, and và not Chúng ta biểu diễn giá trị môt biểu thức vị trí chuỗi mã Ví dụ, chuỗi mã lệnh trên, giá trị t1 phụ thuộc vào việc chúng ta chọn lệnh 101 hay lệnh 103 Do đó giá trị t1 là thừa Các lệnh điều khiển S→ if E then S1 | if E then S1 else S2 | while E S1 Với biểu thức logic E, chúng ta kết hợp với nhãn E.true : Nhãn dòng điều khiển E là true E.false : Nhãn dòng điều khiển E là false S.code : Mã lệnh địa sinh S S.next : Là nhãn mà lệnh địa đầu tiên thực sau mã lệnh S S.begin : Nhãn định lệnh đầu tiên sinh cho S E.code E.true: E.false: to E.true to E.false S1.code (a) if -then E.code E.true: E.false: S.next: to E.true to E.false S.begin: E.code E.true: S1.code S1.code goto S.next S2.code E.false: to E.true to E.false goto S.begin (b) if -then-else (c) while-do Hình 8.19 - Mã lệnh các lệnh if-then, if-then-else, và while-do 180 (183) Ta có định nghĩa trực tiếp cú pháp cho các lệnh điều khiển Luật sinh S→ if E then S1 Luật ngữ nghĩa E.true := newlabel; E.false := S.next; S1.next := S.next; S.code := E.code || gen(E.true ':') || S1.code S→ if E then S1 else S2 E.true := newlabel; E.false := newlabel; S1.next := S.next; S2.next := S.next; S.code := E.code || gen(E.true ':') || S1.code || gen('goto' S.next) || gen(E.false ':') || S2.code S→ while E S1 S.begin := newlabel; E.true := newlabel; E.fasle := S.next; S1.next := S.begin; S.code:= gen(S.begin':') || E.code || gen(E.true ':') || S1.code || gen('goto' S.begin) Hình 8.20 - Ðịnh nghĩa trực tiếp cú pháp dòng điều khiển Dịch biểu thức logic các lệnh điều khiển • Nếu E có dạng a<b thì mã lệnh sinh có dạng if a<b goto E.true goto E.false • Nếu E có dạng E1 or E2 Nếu E1 là true thì E là true Nếu E1 là flase thì phải đánh giá E2 Do đó E1.false là nhãn lệnh đầu tiên E2 E true hay false phụ thuộc vào E2 là true hay false • Tương tự cho E1 and E2 • Nếu E có dạng not E1 thì E1 là true thì E là false và ngược lại Ta có định nghĩa trực tiếp cú pháp cho việc dịch các biểu thức logic thành mã lệnh địa Chú ý true và false là các thuộc tính kế thừa Luật sinh E→ E1 or E2 Luật ngữ nghĩa E1.true := E.true; E1.false := newlabel; E2.true := E.true; 181 (184) E2.false := E.false; E.code := E1.code || gen(E.false ':') || E2.code E→ E1 and E2 E1.true := newlabel; E1.false := E.false; E2.true := E.true; E2.false := E.false; E.code := E1.code || gen(E.true ':') || E2.code E→ not E1 E1.true := E.false; E1.false := E.true; E.code := E1.code E → (E1) E1.true := E.true; E1.false := E.false; E.code := E1.code E → id1 relop id2 E.code := gen('if' id1.place relop.op id2.place 'goto' E.true) || gen('goto' E.false) E → true E.code:= gen('goto' E.true) E → false E.code:= gen('goto' E.false) Hình 8.21 - Ðịnh nghĩa trực tiếp cú pháp sinh mã lệnh ba địa cho biểu thức logic Biểu thức logic và biểu thức số học Trong thực tế biểu thức logic thường chứa biểu thức số học (a+b) < c Trong các ngôn ngữ mà false có giá trị số là và true có giá trị số là thì (a<b) + (b<a) có thể xem là biểu thức số học có giá trị a = b và có giá trị a <> b Phương pháp biểu diễn biểu thức logic mã lệnh nhảy có thể còn sử dụng Xét văn phạm E → E+ E | E and E | E relop E | id Trong đó, E and E đòi hỏi hai đối số phải là logic Trong + và relop có các đối số là biểu thức logic hoặc/và số học Ðể sinh mã lệnh trường hợp này, chúng ta dùng thuộc tính tổng hợp E.type có thể là arith bool E có các thuộc tính kế thừa E.true và E.false biểu thức số học Ta có luật ngữ nghĩa kết hợp với E → E1 + E2 sau E.type := arith; if E1.type = arith and E2.type = arith then begin /* phép cộng số học bình thường */ E.place := newtemp; E.code := E1.code || E2.code || gen(E.place ':=' E1.place '+' E2.place) end else if E1.type = arith and E2.type = bool then begin 182 (185) E.place := newtemp; E2.true := newlabel; E2.false := newlabel; E.code := E1.code || E2.code || gen(E2.true ':' E.place ':= ' E1.place +1) || gen('goto' nextstat +1) || gen(E2.false ':' E.place ':= ' E1.place) else if Hình 8.22 - Luật ngữ nghĩa cho luật sinh E → E1 +E2 Trong trường hợp có biểu thức logic nào có biểu thức số học, chúng ta sinh mã lệnh cho E1, E2 các lệnh E2.true : E.place := E1.place +1 goto nextstat +1 E2.false : E.place := E1.place V LỆNH CASE Lệnh CASE SWITCH thường sử dụng các ngôn ngữ lập trình Cú pháp lệnh SWITCH/ CASE SWITCH E begin case V1 : S1 case V2 : S2 case Vn-1 : Sn-1 default: Sn end Hình 8.23 - Cú pháp câu lệnh switch Dịch trực tiếp cú pháp lệnh Case Ðánh giá biểu thức Tùy giá trị danh sách các case giá trị biểu thức Nếu không tìm thấy thì giá trị default biểu thức xác định Thực các lệnh kết hợp với giá trị tìm để cài đặt Ta có phương pháp cài đặt sau mã lệnh để đánh giá biểu thức E vào t goto test L1 : mã lệnh S1 goto next L2: mã lệnh S2 183 (186) goto next Ln-1 : mã lệnh Sn-1 goto next Ln : mã lệnh Sn goto next test : if t=V1 goto L1 if t=V2 goto L2 if t=Vn-1 goto Ln-1 else goto Ln next: Hình 8.24 - Dịch câu lệnh case Một phương pháp khác để cài đặt lệnh SWITCH là mã lệnh để đánh giá biểu thức E vào t if t <> V1 goto L1 mã lệnh S1 goto next L1 : if t <> V2 goto L2 mã lệnh S2 goto next L2: Ln-2 : if t <> Vn-1 goto Ln-1 mã lệnh Sn-1 goto next Ln-1 : mã lệnh Sn next: Hình 8.24 - Một cách dịch khác câu lệnh case 184 (187) BÀI TẬP CHƯƠNG VIII 8.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 - địa 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 - địa chỉ) cho các biểu thức C đơn giản sau: a) x = b) x = y c) x = x + d) x = a + b * c e) x = a / ( b + c) - d * ( e + f ) 8.4 Sinh mã trung gian ( dạng mã máy - địa chỉ) cho các biểu thức C sau: 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 - địa chỉ: A [ i,j ] := B [ i,j ] + C [A[ k,l]] + D [ i + j ] 185 (188) CHƯƠNG IX SINH Mà ÐÍCH Nội dung chính: Giai đoạn cuối quá trình biên dịch là sinh mã đích Dữ liệu nhập sinh mã đích là biểu diễn trung gian chương trình nguồn và liệu xuất nó là chương trình đích (hình 9.1) Kỹ thuật sinh mã đích trình bày 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 Chương trình nguồn Biên dịch kỳ đầu Mã trung gian Bộ tối ưu mã Mã trung gian Bộ sinh mã Chương trình đích đích Bảng danh biểu Hình 9.1- Vị trí sinh mã đích Nhìn chung sinh mã đích phải đảm bảo chạy hiệu và phải tạo chương trình đích đúng sử dụng hiệu tài nguyên máy đích Về mặt lý thuyết, vấn đề sinh mã tối ưu là không thực Trong thực tế, ta có thể chọn kỹ thuật heuristic để tạo mã tốt không thiết là mã tối ưu Chương này đề cập đến các vấn đề cần quan tâm thiết kế sinh mã Bên cạnh đó sinh mã đích đơn giản từ chuỗi các lệnh ba địa giới thiệu Mục tiêu cần đạt: Sau học xong chương này, sinh viên phải: • Nắm các vấn đề cần chú ý thiết kế sinh mã đích • Biết cách tạo sinh mã đích đơn giản từ chuỗi các mã lệnh ba điạ Từ đó có thể mở rộng sinh mã này cho phù hợp với ngôn ngữ lập trình cụ thể Kiến thức bản: Sinh viên phải có kiến thức kiến trúc máy tính đặc biệt là phần hợp ngữ (assembly language) để thuận tiện cho việc tiếp nhận kiến thức máy đích Tài liệu tham khảo: [1] Compilers : Principles, Technique and Tools - Alfred V.Aho, Jeffrey D.Ullman Addison - Wesley Publishing Company, 1986 [2] Design of Compilers : Techniques of Programming Language Translation Karen A Lemone - CRC Press, Inc, 1992 - 187 (189) I CÁC VẤN ÐỀ THIẾT KẾ BỘ SINH Mà Trong thiết kế sinh mã, các vấn đề chi tiết quản trị nhớ, chọn thị cấp phát ghi và đánh giá thứ tự thực phụ thuộc nhiều vào ngôn ngữ đích và hệ điều hành Dữ liệu vào sinh mã Dữ liệu vào sinh mã gồm biểu diễn trung gian chương trình nguồn, cùng thông tin bảng danh biểu dùng để xác định địa các đối tượng liệu thời gian thực thi Các đối tượng liệu này tượng trưng tên biểu diễn trung gian Biểu diễn trung gian chương trình nguồn có thể các dạng: Ký pháp hậu tố, mã ba địa chỉ, cây cú pháp, DAG Dữ liệu xuất sinh mã – Chương trình đích Giống mã trung gian, liệu xuất sinh mã có thể các dạng: Mã máy tuyệt đối, mã máy khả định vị địa hợp ngữ Việc tạo chương trình đích dạng mã máy tuyệt đối cho phép chương trình này lưu vào nhớ và thực Nếu chương trình đích dạng mã máy khả định vị địa (module đối tượng) thì hệ thống cho phép các chương trình biên dịch riêng rẽ Một tập các module đối tượng có thể liên kết và tải vào nhớ để thực nhờ tải liên kết (linking loader) Mặc dù ta phải trả giá thời gian cho việc liên kết và tải vào nhớ các module đã liên kết ta tạo các module đối tượng khả định vị địa Nhưng bù lại, ta có mềm dẻo việc biên dịch các chương trình riêng rẽ và có thể gọi chương trình đã biên dịch trước đó từ module đối tượng Nếu mã đích không tự động tái định vị địa chỉ, trình biên dịch phải cung cấp thông tin tái định cho tải (loader) để liên kết các chương trình đã biên dịch lại với Việc tạo chương đích dạng hợp ngữ cho phép ta dùng biên dịch hợp ngữ để tạo mã máy Lựa chọn thị Tập các thị máy đích xác định tính phức tạp việc lựa chọn thị Tính chuẩn và hoàn chỉnh tập thị là yếu tố quan trọng Nếu máy đích không cung cấp mẫu chung cho kiểu liệu thì trường hợp ngoại lệ phải xử lý riêng Tốc độ thị và biểu diễn máy là yếu tố quan trọng Nếu ta không quan tâm đến tính hiệu chương trình đích thì việc lựa chọn thị đơn giản Với lệnh ba địa ta có thể phác họa khung cho mã đích Giả sử lệnh ba địa dạng x := y + z, với x, y, z cấp phát tĩnh, có thể dịch sang chuỗi mã đích: MOV y, R0 /* Lưu y vào ghi Ro */ ADD z, R0 /* cộng z vào nội dung Ro, kết chứa 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 dẫn đến dư thừa mã Chẳng hạn với: a:= b + c d:= a + e 188 (190) ta chuyển sang mã đích: MOV b, Ro ADD c, Ro MOV Ro, a MOV a, R0 ADD e,Ro MOV Ro, d và ta nhận thấy thị thứ tư là thừa Chất lượng mã tạo xác định tốc độ và kích thước mã Một máy đích có tập thị phong phú có thể cung cấp nhiều cách để thực tác vụ cho trước Ðiều này có thể dẫn đến tốc độ thực thị khác Chẳng hạn, máy đích có thị INC thì câu lệnh ba địa a := a + có thể cài đặt câu lệnh INC a Cách này hiệu là dùng chuỗi các thị sau: MOV a, Ro ADD # 1, Ro MOV Ro ,a Như ta đã nói, tốc độ thị là 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 định chuỗi mã máy nào là tốt cho câu lệnh ba điạ còn phụ thuộc vào ngữ cảnh nơi chưá câu lệnh đó Cấp phát ghi Các thị dùng toán hạng ghi thường ngắn và nhanh các thị dùng toán hạng nhớ Vì thế, hiệu ghi đặc biệt quan trọng việc sinh mã tốt Ta thường dùng ghi hai trường hợp: Trong cấp phát ghi, ta lựa chọn tập các biến lưu trú các ghi thời điểm chương trình Trong gán ghi, ta lấy ghi đặc biệt mà biến thường trú đó Việc tìm kiếm lệnh gán tối ưu ghi, với các giá trị ghi đơn, cho các biến là công việc khó khăn Vấn đề càng trở nên phức tạp vì phần cứng và / hệ điều hành máy đích yêu cầu qui ước sử dụng ghi Lựa chọn cho việc đánh giá thứ tự Thứ tự thực tính toán có thể ảnh hưởng đến tính hiệu mã đích Một số thứ tự tính toán có thể cần ít ghi để lưu giữ các kết trung gian các thứ tự tính toán khác Việc lựa chọn thứ tự tốt là vấn đề khó Ta nên tránh vấn đề này cách sinh mã cho các lệnh ba địa theo thứ tự mà chúng đã sinh mã trung gian Sinh mã Tiêu chuẩn quan trọng sinh mã là phải tạo mã đúng Tính đúng mã có ý nghĩa quan trọng Với quy định tính đúng mã, việc thiết kế sinh mã cho nó thực hiện, kiểm tra, bảo trì đơn giản là mục tiêu thiết kế quan trọng 189 (191) II MÁY ÐÍCH Trong chương trình này, chúng ta dùng máy đích là máy ghi (rigister 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ã trình bày chương này có thể dùng cho nhiều loại máy tính khác Máy đích chúng ta là máy tính địa byte với từ gồm bốn byte và có n ghi : R0, R1 Rn-1 Máy đích gồm các thị hai địa 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 liệu Ví dụ số mã tác vụ: MOV chuyển source đến destination ADD cộng source và destination SUB trừ source cho destination Source và destination thị xác định cách kết hợp các ghi và các vị trí nhớ với các mode địa Mô tả content (a) biểu diễn cho nội dung ghi điạ nhớ biểu diễn a mode địa cùng với dạng hợp ngữ và giá kết hợp: Mode Dạng Ðịa Giá Absolute M M Register R R Indexed c(R) c + contents ( R) Indirect register *R contents ( R) Indirect indexed *c(R) contents (c+ contents ( R)) Vị trí nhớ M ghi R biểu diễn chính nó đưọc sử dụng nguồn hay đích Ðộ dời địa c từ giá trị ghi R viết là c( R) Chẳng hạn: MOV R0, M : Lưu nội dung ghi R0 vào vị trí nhớ M MOV 4(R0), M : Xác định địa cách lấy độ dời tương đối (offset) cộng với nội dung R0, sau đó lấy nội dung địa này, contains(4 + contains(R0)), lưu vào vị trí nhớ M MOV * 4(R0) , M : Lưu giá trị contents (contents (4 + contents (R0))) vào vị trí nhớ M MOV #1, R0 : Lấy lưu vào ghi R0 Giá thị Giá thị (instrustion cost) tính cộng với giá kết hợp mode địa nguồn và đích bảng trên Giá này tượng trưng cho chiều dài thị Mode địa dùng ghi có giá không và có giá nó dùng vị trí nhớ 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 thị Ðối với phần lớn các máy và phần lớn các thị, thời gian cần để lấy thị từ nhớ bao 190 (192) xảy trước thời gian thực thị Vì vậy, việc tối thiểu hóa độ dài thị, ta còn tối thiểu hoá thời gian cần để thực thị Một số minh họa việc tính giá thị: Chỉ thị MOV R0, R1 : Sao chép nội dung ghi R0 vào ghi R1 Chỉ thị này có giá là vì nó chiếm từ nhớ MOV R5, M: Sao chép nội dung ghi R5 vào vị trí nhớ M Chỉ thị này có giá trị là hai vì địa vị trí nhớ M là từ sau thị Chỉ thị ADD #1, R3: cộng vào nội dung ghi R3 Chỉ thị có giá là hai vì phải xuất từ sau thị Chỉ thị SUB 4(R0), *12 (R1) : Lưu giá trị contents (contents (12 + contents (R1))) - contents (4 + contents (R0)) vào đích *12( R1) Giá thị này là ba vì và 12 lưu trữ hai từ theo sau thị Với câu lệnh ba địa chỉ, ta có thể có nhiều cách cài đặt khác Ví dụ câu lệnh a := b + c - đó b và c là biến đơn, lưu các vị trí nhớ phân biệt có tên b, c - có cách cài đặt sau: MOV b, Ro ADD c, R0 giá = MOV Ro, a MOV b, a giá = ADD c, a Giả sử ghi R0, R1, R2 giữ địa a, b, c Chúng ta có thể dùng hai địa sau cho việc sinh mã lệnh: a := b + c => MOV *R1, *Ro giá = ADD * R2, *Ro Giả sử ghi R1 và R2 chứa giá trị b và c và trị b không cần lưu lại sau lệnh gán Chúng ta có thể dùng hai thị sau: ADD R2, R1 giá = MOV R1, a Như vậy, với cách cài đặt khác ta có giá khác Ta thấy muốn sinh mã tốt thì phải hạ giá các thị Tuy nhiên việc làm khó mà thực Nếu có quy ước trước cho ghi, lưu giữ địa vị trí nhớ chứa giá trị tính toán hay địa để đưa trị vào, thì việc lựa chọn thị dễ dàng III QUẢN LÝ BỘ NHỚ TRONG THỜI GIAN THỰC HIỆN Trong phần này ta nói việc sinh mã để quản lý các mẩu tin hoạt động thời gian thực Hai chiến lược cấp phát nhớ chuẩn trình bày 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í mẩu tin hoạt động nhớ xác định thời gian biên dịch Với cấp phát Stack, mẩu tin hoạt động đưa vào Stack có thực thủ tục và lấy khỏi Stack hoạt động kết thúc Ở đây, ta xem xét cách thức mã đích thủ tục tham chiếu tới các đối tượng liệu 191 (193) các mẩu tin hoạt động Như ta đã nói chương VII, mẩu tin hoạt động cho thủ tục có các trường: tham số, kết quả, thông tin trạng thái máy, 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à liệu cục bộ, các trường còn lại dùng đã đề 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à phần chuỗi hành vi gọi và trả chương trình Ta quan tâm đến việc sinh mã cho các lệnh sau: call return halt action /* tượng trưng cho các lệnh khác */ Chẳng hạn, mã ba địa chỉ, chứa các loại câu lệnh trên, cho các chương trình c và p các mẩu tin hoạt động chúng: /* mã cho s */ action1 call p action2 halt /* mã cho c */ action3 return Bảng mã 0: Địa trả 0: Địa trả 8: arr 4: buf 56: i 60: i Bảng ghi hoạt động cho c 84: n Bảng ghi hoạt động cho p Hình 9.2 – Dữ liệu vào sinh mã Kích thước và việc xếp đặt các mẩu tin kết hợp với sinh mã nhờ thông tin tên bảng danh biểu Ta giả sử nhớ thời gian thực phân chia thành các vùng cho mã, liệu tĩnh và Stack Cấp phát tĩnh Chúng ta xét các thị cần thiết để thực việc cấp phát tĩnh Lệnh call mã trung gian thực dãy hai thị đích Chỉ thị MOV lưu địa trả Chỉ thị GOTO chuyển quyền điều khiển cho chương trình 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 tham chiếu tới các địa mẩu tin hoạt động và thị đầu tiên đoạn mã chương trình gọi # here + 20 thị MOV là địa trả Nó chính là địa thị đứng sau lệnh GOTO Mã chương trình kết thúc lệnh trả 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ả dịch sang mã máy là GOTO *callee_static_area thực việc chuyển quyền điều khiển địa lưu giữ ô nhớ đầu tiên mẩu tin hoạt động 192 (194) Ví dụ 9.1: Mã đích chương trình sau tạo từ các chương trình c và p hình 9.2 Giả sử rằng: các mã đó lưu địa bắt đầu là 100 và 200, thị action chiếm 20 byte, và các mẩu tin hoạt động cho c và p cấp phát tĩnh bắt đầu các địa 300 và 364 Ta dùng thị action để thực câu lệnh action Như vậy, mã đích cho các chương trình con: /* mã cho c*/ 100: ACTION1 120: MOV #140, 364 /* lưu địa trả 140 */ 132: GOTO 200 /* gọi p */ 140: ACTION2 160: HALT /* mã cho p */ 200: ACTION3 220: GOTO *364 /* trả địa lưu vị trí 364 */ /* 300-364 lưu mẩu tin hoạt động c */ 300: /* chứa địa trả */ 304: /* liệu cục c */ /* 364 - 451 chứa mẩu tin hoạt động p */ 364: /* chứa địa trả */ 368: /* liệu cục p */ Hình 9.3 - Mã đích cho liệu vào hình 9.2 Sự thực bắt đầu thị action địa 100 Chỉ thị MOV địa 120 lưu địa trả 140 vào trường trạng thái máy, là từ đầu tiên mẩu tin hoạt động p Chỉ thị GOTO 200 chuyển quyền điều khiển thị đầu tiên đoạn mã chương trình p Chỉ thị GOTO *364 địa 132 chuyển quyền điều khiển sang thị đầu tiên mã đích chương trình gọi Giá trị 140 lưu vào địa 364, *364 biểu diễn giá trị 140 lệnh GOTO địa 220 thực Vì quyền điều khiển trả địa 140 và tiếp tục thực chương trình c Cấp phát theo chế Stack Cấp phát tĩnh trở thành cấp phát Stack ta sử dụng địa tương đối để lưu giữ các mẩu tin hoạt động Vị trí mẩu tin hoạt động xác định thời gian thực thi Trong cấp phát Stack, vị trí này thường lưu vào ghi Vì các ô nhớ mẩu tin hoạt động truy xuất là độ dời (offset) so với giá trị ghi đó Thanh ghi SP chứa địa bắt đầu mẩu tin hoạt động chương trình nằm trên đỉnh Stack Khi lời gọi chương trình xuất hiện, chương trình bị gọi cấp phát, SP tăng lên giá trị kích thước mẩu tin hoạt động chương trình gọi và chuyển quyền điều khiển cho chương trình gọi Khi quyền điều khiển trả cho chương trình gọi, SP giảm khoảng kích thước mẩu tin hoạt động chương trình gọi Vì thế, mẩu tin chương trình gọi đã giải phóng Mã cho chương trình đầu tiên có dạng: 193 (195) MOV # Stackstart, SP /* khởi động Stack */ Ðoạn mã cho chương trình HALT /* kết thúc thực thi */ Trong đó thị đầu tiên MOV #Stackstart, SP khởi động Stack theo cách đặt SP với địa bắt đầu Stack vùng nhớ Chuỗi gọi tăng giá trị SP, lưu giữ địa trả và chuyển quyền điều khiển chương trình gọi ADD # caller.recordsize, SP MOV # here + 16, *SP /* lưu địa trả */ GOTO callee.code_area Thuộc tính caller.recordsize biểu diễn kích thước mẩu tin hoạt động Vì thế, thị ADD đưa SP trỏ tới phần bắt đầu mẩu tin hoạt động #here +16 thị MOV là địa thị theo sau GOTO, nó lưu địa trỏ SP Chuỗi trả gồm hai thị: Chương trình chuyển quyền điều khiển tới địa trả GOTO *0(SP) /* trả chương trình gọi */ SUB #caller.recordsize, SP Trong đó O(SP) là địa ô nhớ đầu tiên mẩu tin hoạt động *O(SP) trả địa lưu đây Chỉ thị SUB #caller.recordsize, SP: Giảm giá trị SP xuống khoảng kích thước mẩu tin hoạt động chương trình gọi Như mẩu tin hoạt động chương trình bị gọi đã xóa khỏi Stack Ví dụ 9.2: Giả sử kích thước các mẩu tin hoạt động các chương trình s, p, và q xác định thời gian biên dịch là ssize, psize, và qsize tương ứng Ô nhớ đầu tiên mẩu tin hoạt động lưu địa trả Ta giả sử rằng, đoạn mã cho các chương trình này bắt đầu các địa 100, 200, 300 tương ứng, và địa bắt đầu Stack là 600 Mã đích cho chương trình hình 9.4 mô tả hình 9.5: 194 (196) Hình 9.4 - Mã ba địa minh hoạ cấp phát sử dụng Stack /* mã cho s */ action1 call q action2 halt /* mã cho p */ action3 return /* mã cho q */ action4 call p action5 call q action6 call q return /* mã cho s*/ 100: MOV # 600, SP /* khởi động Stack */ 108: ACTION1 128: ADD #ssize, SP /* chuỗi gọi bắt đầu */ 136: MOV #152, *SP /* lưu địa trả */ 144: GOTO 300 152: SUB #ssize, SP /* gọi q */ /* Lưu giữ SP */ 160: ACTION2 180: HALT /* mã cho p */ 200: ACTION3 220: GOTO *0(SP) /* trả chương trình gọi */ /* mã cho q */ 300: ACTION4 320: ADD #qsize, SP 328: MOV #344, *SP 336: GOTO 200 344: SUB /* nhảy có điều kiện 456 */ /* lưu địa trả */ /* gọi p */ #qsize, SP 352: ACTION5 372: ADD #qsize, SP 380: MOV #396, *SP /* lưu địa trả */ 195 (197) 388: GOTO 300 /* gọi q */ 396: SUB #qsize, SP 404: ACTION6 424: ADD #qsize, SP 432: MOV #448, *SP /* lưu địa trả */ 440: GOTO 300 /* gọi q */ 448: SUB #qsize, SP 456: GOTO *0(SP) /* trả chương trình gọi */ 600: /* địa bắt đầu Stack trung tâm */ Hình 9.5 - Mã đích cho chuỗi ba địa hình 9.4 Ta giả sử action4 gồm lệnh nhảy có điều kiện tới địa 456 có lệnh trả 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 không trả chương trình gọi ngay, lần sau thì có thể SP có giá trị lúc đầu là 600, địa bắt đầu Stack SP lưu giữ giá trị 620 trước chuyển quyền điều khiển từ s sang q vì kích thước mẩu tin hoạt động s là 20 Khi q gọi p, SP tăng lên 680 thị địa 320 thực hiện, Sp chuyển sang 620 sau chuyển quyền điều khiển cho chương trình p Nếu lời gọi đệ quy q trả thì giá trị lain SP suốt quá trình thực là 680 Vị trí cấp phát theo chế Stack có thể lên đến địa 739 vì mẩu tin hoạt động q bắt đầu 680 và chiếm 60 byte Ðịa các tên thời gian thực Chiến lược cấp phát lưu trữ và xếp đặt liệu cục mẩu tin hoạt động chương trình xác định cách thức truy xuất vùng nhớ tên Nếu chúng ta dùng chế cấp phát tĩnh với vùng liệu cấp phát địa static Với lệnh gán x := 0, địa tương đối x bảng danh biểu là 12 Vậy địa x nhớ là static + 12 Lệnh gán x:=0 chuyển sang mã ba địa static[12] := Nếu vùng liệu bắt đầu địa 100, mã đích cho thị là: MOV #0,112 Nếu ngôn ngữ dùng chế display để truy xuất tên không cục bộ, giả sử x là tên cục chương trình hành và ghi R3 lưu giữ địa bắt đầu mẩu tin hoạt động đó thì chúng ta dịch lệnh x := sang chuỗi mã ba địa chỉ: t1 := 12 + R3 * t1 := Từ đó ta chuyển sang mã đích: MOV #0, 12(R3) Chú ý rằng, giá trị ghi R3 không xác định thời gian biên dịch 196 (198) IV KHỐI CƠ BẢN VÀ LƯU ÐỒ Ðồ thị biểu diễn các lệnh ba địa chỉ, gọi là lưu đồ, giúp ta hiểu các giải thuật sinh mã đồ thị không xác định cụ thể giải thuật sinh mã Các nút lưu đồ biểu diễn tính toán, các cạnh biểu diễn dòng điều khiển Khối Khối (basic block) là chuỗi các lệnh đó dòng điều khiển vào lệnh đầu tiên khối và lệnh cuối cùng khối mà không bị dừng rẽ nhánh Ví dụ chuỗi lệnh ba địa sau tạo nên khối t1 := a * a t2 := a * b t3 := * t2 t4 := t1 + t2 t5 := b * b t6 := t4 + t5 Lệnh ba địa x := y + z dùng các giá trị chứa các vị trí nhớ y, z để thực phép cộng và xác định địa x để lưu kết phép cộng vào Một tên khối gọi là ‘sống‘ điểm nào đó giá trị nó sử dụng sau điểm đó chương trình dùng khối khác Giải thuật sau đây phân chia chuỗi các lệnh ba địa sang các khối Giải thuật 9.1: Phân chia các khối Input: Các lệnh ba địa Output: Danh sách các khối với chuỗi các lệnh ba địa cho khối Phương pháp: Xác định tập các lệnh dẫn đầu (leader), các lệnh đầu tiên các khối bản, ta dùng các quy tắc sau: i) Lệnh đầu tiên là lệnh dẫn đầu ii) Bất kỳ lệnh nào là đích nhảy đến lệnh GOTO có điều kiện không điều kiện là lệnh dẫn đầu iii) Bất kỳ lệnh nào sau lệnh GOTO có điều kiện không điều kiện là lệnh dẫn đầu Với lệnh dẫn đầu, khối gồm có nó và tất các lệnh không gồm lệnh dẫn đầu nào khác hay là lệnh kết thúc chương trình Ví dụ 9.3: Ðoạn chương trình sau tính tích vectơ vô hướng hai vectơ a và b có độ dài 20 Begin prod := i := Repeat prod: = prod + a [i] * b[i]; 197 (199) i := i + Until i > 20 End Hình 9.6 - Chương trình tính tích vectơ vô hướng Ðoạn chương trình này dịch sang mã ba địa sau: (1) prod := (2) i := (3) t1 := * i (4) t2 := a[t1] (5) t3 := * i (6) t4 := b[t3] (7) t5 := t2 * t4 (8) t6 := prod + t5 (9) prod := t6 (10) t7 := i + (11) i := t7 (12) if i<=20 goto (3) /* tính a[i] */ Hình 9.7 - Mã ba địa để tính tích vectơ vô hướng Lệnh (1) là lệnh dẫn đầu theo quy tắc i, lệnh (3) là lệnh dẫn đầu theo quy tắc ii và lệnh sau lệnh (12) là lệnh dẫn đầu theo quy tắc iii Như lệnh (1) và (2) tạo nên khối thứ Lệnh (3) đến (12) tạo nên khối thứ hai Sự chuyển đổi các khối Khối tính các biểu thức Các biểu thức này là giá trị các tên “sống” khỏi khối Hai khối tương đương chúng tính các biểu thức giống Một số chuyển đổi có thể áp dụng vào khối mà không làm thay đổi các biểu thức tính toán đó Nhiều phép chuyển đổi có ích vì nó cải thiện chất lượng mã đích sinh từ khối Hai phương pháp chuyển đổi cục quan trọng áp dụng cho các khối là chuyển đổi bảo toàn cấu trúc và chuyển đổi đại số Chuyển đổi bảo toàn cấu trúc Những chuyển đổi bảo toàn cấu trúc trên các khối bao gồm: Loại bỏ các biểu thức chung Loại bỏ mã chết Ðặt tên lại các biến tạm Hoán đổi hai lệnh độc lập kề Giả sử các khối không chứa dãy, trỏ hay lời gọi chương trình Loại bỏ các biểu thức chung 198 (200) Khối sau: a := b + c b := a - d c := b + c d := a - d Câu lệnh thứ hai và thứ tư tính cùng biểu thức b + c - d Vì vậy, khối này chuyển thành khối tương đương sau: a := b + c b := a - d c := b + c d := b Loại bỏ mã lệnh chết Giả sử x không còn sử dụng Nếu câu lệnh x := y + z xuất khối thì lệnh này bị loại mà không làm thay đổi giá trị khối Ðặt lại tên cho biến tạm Giả sử ta có lệnh t := b + c với t là biến tạm Nếu ta viết lại lệnh này thành u := b + c mà u là biến tạm và thay t u chỗ nào xuất t thì giá trị khối không bị thay đổi Thực tế, ta có thể chuyển khối sang khối tương đương Và ta gọi khối tạo là dạng chuẩn Giả sử chúng ta khối với hai câu lệnh kế tiếp: t1 := b + c t2 := x + y Ta có thể hoán đổi hai lệnh này mà không làm thay đổi giá trị khối và x và y không phải t1 b và c không phải là t2 Khối có dạng chuẩn cho phép tất các lệnh có quyền hoán đổi có thể Chuyển đổi đại số Các biểu thức khối có thể chuyển đổi sang các biểu thức tương đương Phép chuyển đổi đại số này giúp ta đơn giản hoá các biểu thức thay các biểu thức có giá cao các biểu thức có giá rẻ Chẳng hạn, câu lệnh x := x + x := x * có thể loại bỏ khỏi khối mà không làm thay đối giá trị biểu thức Toán tử lũy thừa câu lệnh x := y ** cần lời gọi hàm để thực Tuy nhiên, lệnh này có thể thay lệnh tương đương có giá rẻ mà không cần lời gọi hàm Lưu đồ Ta có thể thêm thông tin dòng điều khiển vào tập các khối việc xây dựng các đồ thị trực tiếp gọi là lưu đồ (flew graph) Các nút lưu đồ là khối Một nút gọi là khởi đầu nó chứa lệnh đầu tiên chương trình Cạnh nối trực tiếp từ khối B1 đến khối B2 B2 là khối đứng sau B1 chuỗi thực nào đó Nghĩa là, nếu: Lệnh nhảy không có điều kiện từ lệnh cuối B1 đến lệnh đầu tiên B2 B 199 (201) B2 đứng sau B1 thứ tự chương trình và B1 không kết thúc lệnh nhảy không điều kiện Chúng ta nói B1 là tiền bối (predecessor) B2 hay B2 là hậu duệ (sucecssor) B1 prod := i := Ví dụ 9.4: B1 t1 := * i B2 t2 := a[t1] t3 := * i t4 := b[t3] t5 := t2 * t4 t6 := prod + t5 prod := t6 t7 := i +1 i := t7 if i<=20 goto b2 Hình 9.8 - Lưu đồ chương trình Biểu diễn các khối Các khối biểu diễn nhiều loại cấu trúc liệu Sau phân chia các lệnh ba địa giải thuật 9.1 Mỗi khối biểu diễn mẩu tin gồm số tứ , theo sau là trỏ trỏ tới lệnh dẫn đầu (bộ tứ đầu tiên) khối, và danh sách các tiền bối và hậu duệ khối DAG là cấu trúc liệu thích hợp để thực việc chuyển đổi các khối Xây dựng DAG từ các lệnh ba địa là cách tốt để xác định được: Các biểu thức chung (được tính nhiều lần), tên dùng khối không dùng ngoài khối, và các biểu thức mà giá trị nó dùng khỏi khối Giải thuật 9.2: Xây dựng DAG Input: Khối Output: DAG cho khối bản, chứa các thông tin sau: Tên cho nút Tên nút lá là danh biểu (hằng số) Tên nút trung gian là toán tử Với nút, danh sách (có thể rỗng) gồm các danh biểu (hằng số không phép có danh sách này) Phương pháp: Giả sử ta đã có cấu trúc liệu để tạo nút có hai Ta phải phân biệt bên trái và bên phải nút có hai Ta có vị trí để ghi tên cho nút và có chế tạo danh sách liên kết các danh biểu gắn với nút Ta giả sử tồn hàm node (identifier), ta xây dựng DAG, trả nút có liên quan với identifier Thực tế node (identifier) là nút biểu diễn giá trị danh biểu (identifier) thời điểm quá trình xây dựng DAG Quá trình xây dựng DAG thực qua các bước từ (1) đến (3) cho lệnh khối Lúc đầu, ta giả sử chưa có các nút và hàm node không định nghĩa cho tất các đối số Các dạng lệnh ba địa các dạng sau: (i) x := y op z, (ii) x := op y, (iii) x := y 200 (202) Trường hợp lệnh điều kiện, chẳng hạn if i <= 20 goto, ta coi là trường hợp (i) với x không định nghĩa Nếu node (y) không định nghĩa, tạo lá có tên y và node (y) chính là nút này Trong trường hợp (i), node(z) không định nghĩa, ta tạo lá tên z và lá chính là node (z) Trong trường hợp (i) , xác định xem trên DAG có nút nào có tên op mà trái là node (y) và phải là node (z) Nếu không thì tạo nút có tên op, ngược lại n là nút đã tìm thấy đã tạo Trong trường hợp (ii) , ta xác định xem có nút nào có tên op, mà nó có là node (y) Nếu chưa có nút trên ta tạo nút op và coi n là nút tìm thấy vừa tạo Trong trường hợp thứ (iii) thì đặt n là node(y) Xoá x khỏi danh sách các danh biểu gán với nút node(x) Nối x vào danh sách các danh biểu gắn vào nút n tìm bước (2) và đặt node(x) cho n Vòng lặp Vòng lặp (loop) là tập hợp các nút lưu đồ cho: Tất các nút tập hợp phải kết nối chặt chẽ với nhau, nghĩa là phải có đường với kích thước lớn từ nút vòng lặp đến nút khác và nó phải nằm hoàn toàn vòng lặp Các nút lựa chọn này có lối vào, nghĩa là đường từ nút ngoài vòng lặp vào vòng lặp phải qua lối vào đó Nếu vòng lặp không chứa vòng lặp nào khác thì gọi là vòng lặp cùng V THÔNG TIN SỬ DỤNG TIẾP Tính toán sử dụng tiếp Việc sử dụng tên lệnh ba địa định nghĩa sau: Giả sử lệnh ba địa i gán giá trị cho x Nếu x là toán hạng lệnh j, dòng điều khiển từ lệnh i đến j đồng thời dọc đường này ta không xen vào các lệnh gán cho x, thì ta nói lệnh j dùng giá trị x tính toán i Trong chương này, ta đề cập đến việc tiếp tục sử dụng tên lệnh ba địa khối chứa lệnh đó Giải thuật xác định sử dụng tạo nên duyệt lùi qua các khối Ta có thể dễ dàng xác định lệnh ba địa cuối cùng khối Vì các chương trình có hiệu ứng lề tùy ý nên ta giả sử lời gọi chương trình bắt đầu khối Ðể tìm lệnh cuối cùng khối bản, ta duyệt lùi tới phần đầu, lưu giữ các thông tin như: x có tiếp tục sử dụng khối hay không? x có còn “sống” khỏi khối? Giả sử ta tới lệnh ba địa i: x := y op z duyệt lùi Ta có thể làm theo các sau: Gán cho lệnh i các thông tin tìm thấy bảng danh biểu Ðó là thông tin xác định x, y, z có tiếp tục sử dụng? và còn “sống”? Trong bảng danh biểu, đặt x không “sống” và không dùng tiếp 201 (203) Trong bảng danh biểu, đặt y, z “sống” và sử dụng tiếp Chú ý các bước và không thể xen kẽ vì x có thể là y z Ðối với lệnh ba địa dạng x := y x := op y, các bước làm tương tự trên bỏ qua z Bộ nhớ các tên tạm Nhìn chung, ta có thể gói các biến trung gian vào cùng vị trí nhớ chúng không cùng “sống” Hầu tất các tên tạm (biến trung gian) định nghĩa và sử dụng các khối bản, thông tin sử dụng tiếp có thể gói các biến trung gian Trong chương này, ta không nói tới các biến trung gian sử dụng nhiếu khối Ta có thể cấp phát các vị trí nhớ cho các biến trung gian cách kiểm tra kết trả biến và gán biến trung gian vào vị trí đầu tiên trường các biến trung gian, trường không chứa biến “sống” Nếu biến trung gian không gán cho vị trí nào tạo trước đó, thêm vị trí vào vùng liệu chương trình hành Trong nhiều trường hợp, các biến trung gian gói vào các ghi là vào các vị trí nhớ và t2: Chẳng hạn, sáu biến trung gian khối sau gói vào hai vị trí nhớ là t1 t1 := a * a t2 := a * b t2 := * t2 t1 := t1 + t2 t2 := b * b t1 := t1 + t2 VI BỘ SINH Mà ÐƠN GIẢN Ta giả sử rằng, sinh mã này sinh mã đích từ chuỗi các lệnh ba địa Mỗi toán tử lệnh ba địa tương ứng với toán tử máy đích Các kết tính toán có thể nằm lại ghi bao lâu có thể và lưu trữ khi: (a) Thanh ghi đó sử dụng cho tính toán khác (b) Trước có lệnh gọi chương trình con, lệnh nhảy lệnh có nhãn Ðiều kiện (b) giá trị nào phải lưu vào nhớ trước kết thúc khối Vì sau khỏi khối bản, ta có thể tới các khối khác ta có thể tới khối xác định từ khối khác Trong trường hợp (a), ta không thể làm điều này mà không giả sử số lượng dùng khối xuất cùng 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 lưu giữ tất các giá trị qua ranh giới khối gọi chương trình Ta có thể tạo mã phù họp với câu lệnh ba địa a := b + c ta tạo thị đơn ADD Rj, Ri với giá là Kết a đưa vào ghi Ri ghi Ri chứa b, ghi Rj chứa c, và b không sử dụng 202 (204) Nếu b Ri , c nhớ , ta có thể tạo thị: ADD c, Ri giá = Hoặc b ghi Ri và giá trị c đưa từ nhớ vào Rj sau đó thực phép cộng hai ghi Ri, Rj, ta có thể tạo các thị: MOV c, Rj ADD Rj , Ri giá = Qua các trường hợp trên chúng ta thấy có nhiều khả để tạo mã đích cho lệnh ba địa Tuy nhiên, việc lựa chọn khả nào lại tuỳ thuộc vào ngữ cảnh thời điểm cần tạo mã Mô tả ghi và địa Giải thuật sinh mã đích dùng mô tả (descriptor) để lưu giữ nội dung ghi và địa tên Bộ mô tả ghi lưu giữ gì tồn ghi cho ta biết nào cần ghi Ta giả sử lúc đầu, mô tả khởi động cho tất các ghi rỗng Khi sinh mã cho các khối bản, ghi giữ giá trị các tên thời điểm thực Bộ mô tả địa lưu giữ các vị trí nhớ nơi giá trị tên có thể tìm thấy thời điểm thực thi Các vị trí đó có thể là ghi, vị trí trên Stack, địa nhớ Tất các thông tin này lưu bảng danh biểu và dùng để xác định phương pháp truy xuất tên Giải thuật sinh mã đích Giải thuật sinh mã nhận vào chuỗi các lệnh ba địa khối Với lệnh ba địa dạng x := y op z ta thực các bước sau: Gọi hàm getreg để xác định vị trí L nơi lưu giữ kết phép tính y op z L thường là ghi nó có thể là vị trí nhớ Xác định địa mô tả cho y để từ đó xác định y’, vị trí hành y Chúng ta ưu tiên chọn ghi cho y’nếu ghi và vị trí nhớ giữ giá trị y Nếu giá trị y chưa có L, ta tạo thị: MOV y', L để lưu y vào L Tạo thị op z', L với z' là vị trí hành z Ta ưu tiên chọn ghi cho z' giá trị z lưu giữ ghi và nhớ Việc xác lập mô tả địa x x vị trí L Nếu L là ghi thì L là giữ trị x và loại bỏ x khỏi tất các mô tả ghi khác Nếu giá trị y và/ z không còn dùng khỏi khối, và chúng ghi thì sau khỏi khối ta phải xác lập mô tả ghi để các ghi trên không giữ trị y và/hoặc z Nếu mã ba địa có phép toán ngôi thì các bước thực sinh mã đích tương tự trên Một trường hợp cần đặc biệt lưu ý là lệnh x := y Nếu y ghi, ta phải thay đổi ghi và mô tả địa chỉ, là giá trị x tìm thấy ghi chứagiá trị y Nếu y không dùng tiếp thì ghi đó không còn lưu trị y Nếu y nhớ, ta dùng hàm getreg để tìm ghi tải giá trị y và xác lập ghi đó là 203 (205) vị trí x Nếu ta thông báo vị trí nhớ chứa giá trị x là vị trí nhớ y thì vấn đề trở nên phức tạp vì ta không thể thay đổi giá trị y không tìm chỗ khác để lưu giá trị x trước đó Hàm getreg Hàm getreg trả vị trí nhớ L lưu giữ giá trị x lệnh x := y op z Sau đây là cách đơn giản dùng để cài đặt hàm: Nếu y ghi và y không dùng sau thực x := y op z thì trả ghi chứa y cho L và xác lập thông tin cho mô tả địa y y không còn L Ngược lại, trả ghi rỗng (nếu có) Nếu không có ghi rỗng và x còn dùng tiếp khối toán tử op cần ghi, ta chọn ghi không rỗng R Lưu giá trị R vào vị trí nhớ M thị MOV R,M Nếu M chưa chứa giá trị nào, xác lập thông tin mô tả địa cho M và trả R Nếu R giữ trị số biến, ta phải dùng thị MOV để lưu giá trị cho biến Nếu x không dùng không có ghi phù hợp nào tìm thấy, ta chọn vị trí nhớ x L Ví dụ 9.5: Lệnh gán d := (a - b) + (a - c) + (a - c) Có thể chuyển sang chuỗi mã ba địa chỉ: t := a - b u := a - c v := t + u d := v + u và d “sống” đến hết chương trình Từ chuỗi lệnh ba địa này, giải thuật sinh mã vừa trình bày tạo chuỗi mã đích với giả sử rằng: a, b, c luôn nhớ và t, u, v là các biến tạm không có nhớ Câu lệnh địa t := a - b Mã đích MOV a, R0 Giá Bộ mô tả ghi Bộ mô tả địa Thanh ghi rỗng, R0 t R0 chứa t SUB b, R0 MOV a, R1 SUB c, R1 R1 chứa u u R1 v := t + u ADD R1, R0 R0 chứa v v R0 d := v + u ADD R1, R0 R1 chúa u d rong R0 MOV R0, d R0 chứa d d nhớ u := a - c R0 chứa t t R0 u rong R1 Hình 9.9 - Chuỗi mã đích 204 (206) Lần gọi đầu tiên hàm getreg trả R0 vị trí để xác định t Vì a không R0 , ta tạo thỉ MOV a, R0 và SUB b, R0 Ta cập nhật lại mô tả để R0 chứa t Việc sinh mã đích tiếp tục tiến hành theo cách này lệnh ba địa cuối cùng d := v + u xử lý Chú ý R0 là rỗng vì u không còn dùng Sau đó ta tạo thị, cuối cùng khối, MOV R0, d để lưu biến “sống” d Giá chuỗi mã đích sinh trên là 12 Tuy nhiên, ta có thể giảm giá xuống còn 11 cách thay thị MOV a, R1 MOV R0, R1 và xếp thị này sau thị thứ Sinh mã cho loại lệnh khác Các phép toán xác định số và trỏ câu lệnh ba địa thực giống 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ấp phát tĩnh Câu lệnh địa a:= b[ i ] i ghi Ri Mã i nhớ Mi Giá MOV b(Ri ), R Mã i trên Stack Giá MOV Mi, R Mã MOV Si(A), R MOV b(R), R a[i]:=b MOV b, a(Ri) Giá MOV b(R), R MOV Mi , R MOV Si(A), R MOV b, a (R) MOV b, a (R) Hình 9.10 - Chuỗi mã đích cho phép gán mục Với câu lệnh ba địa trên ta có thể có nhiều đoạn mã đích khác tuỳ thuộc vào i ghi, vị trí nhớ Mi trên Stack vị trí Si và trỏ ghi A tới mẩu tin hoạt động i Thanh ghi R là kết trả hàm getreg gọi Ðối với lệnh gán đầu tiên, ta đưa a vào R a tiếp tục dùng khối và có sẵn ghi R Trong câu lệnh thứ hai ta giả sử a cấp phát tĩnh Sau đây là chuỗi mã đích sinh cho các lệnh gán trỏ dạng a := *p và *p := a Vị trí nhớ p xác định chuỗi mã đích tương ứng Câu lệnh địa a:= *p p ghi Rp Mã MOV *Rp, a Giá p nhớ Mi Mã MOV Mp, R Giá MOV *R, R *p:= a MOV a, *Rp MOV Mp, R MOV a, *R p Stack Mã MOV Sp(A), R Giá MOV *R, R MOV a, R MOV R, *Sp(A) Hình 9.11 - Mã đích cho phép gán trỏ Ba chuỗi mã đích tuỳ thuộc vào p ghi Rp, p vị trí nhớ Mp, p Stack offset là Sp và trỏ, ghi A, trỏ tới mẩu tin hoạt động p Thanh ghi R là kết trả hàm getreg gọi Trong câu lệnh gán thứ hai ta giả sử a cấp phát tĩnh 205 (207) Sinh mã cho lệnh điều kiện Máy tính thực thi lệnh nhảy có điều kiện theo hai cách sau: Rẽ nhánh giá trị ghi xác định trùng với 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 if x < y goto z có thể thực cách lấy x ghi R trừ y Sau đó nhảy z giá trị ghi R là âm Dùng tập các mã điều kiện để xác định giá trị ghi R là âm, không hay dương Chỉ thị so sánh CMP 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 x > y, Chỉ thị nhảy có điều kiện thực điều kiện < , =, >, >=,<>, <= xác lập Ta dùng thị nhảy có điều kiện CJ <= z để nhảy đến z mã điều kiện là âm không Chẳng hạn, lệnh điều kiện if x < y goto z dịch sang mã máy sau CMP x,y CJ < z 206 (208)