Hành Trình Hiện Thực Hóa Trình Biên Dịch - nói dối e blog

Hành Trình Hiện Thực Hóa Trình Biên Dịch

Một thời gian trước, tôi đã hoàn thành máy ảo cho ngôn ngữ kịch bản. Tuy nhiên, chỉ có máy ảo thôi thì chưa đủ ý nghĩa nếu chưa có ngôn ngữ chạy trên đó. Vì vậy, tôi đã bắt tay vào phát triển trình biên dịch từ khá sớm. Dù vậy, giữa chừng tôi phải tạm gác lại do lịch trình dày đặc: tham dự hội nghị C++ tại Thượng Hải, sau đó là chuyến công tác tuyển dụng tại Thành Đô. Những chuyến đi này khiến tôi kiệt sức, mãi đến vài ngày gần đây mới có thời gian quay lại thực hiện tiếp.

Tôi nhớ rằng môn Nguyên lý biên dịch thường được giảng dạy ở bậc đại học. Dù không phải dân chuyên ngành, tôi vẫn có chút ấn tượng về kiến thức này. Hồi trung học, tôi từng theo học một lớp đại học mở rộng, nơi giáo viên đã dạy về biên dịch. Lúc đó, tôi mới chỉ làm quen với C, thành thạo nhất là Basic và hợp ngữ 6502. Những khái niệm về biên dịch lúc ấy thật khó hiểu, tôi chỉ kịp ghi nhớ lơ mơ vài điểm, đủ để sau này khoe mẽ với bạn bè.

Trong diễn đàn C++ của chúng ta từng có cuộc tranh luận thú vị: Có người cho rằng viết được trình biên dịch mới là “cao thủ”, còn làm giao diện người dùng thì không đáng kể. Tôi hoàn toàn không đồng tình. Mỗi lĩnh vực đều có độ phức tạp riêng, nghiên cứu sâu vào đâu cũng thấy mênh mông cả. Tôi vẫn nghĩ rằng việc viết trình biên dịch chắc chắn không hề đơn giản.

Lần này, tôi quyết định không dùng các công cụ như yacc/lex, thậm chí không chia tách quá rõ ràng giữa phân tích từ vựng và cú pháp. Tôi hơi tự phụ cho rằng các thế hệ đi trước chia thành nhiều giai đoạn vì hai lý do: một là tuổi tác khiến trí nhớ suy giảm, hai là cần phân công công việc cho nhiều người cùng làm. Còn tôi, tuổi trẻ đầu óc còn minh mẫn, hoàn toàn có thể xử lý nhiều thứ song song. Bộ não con người khi đọc code cũng đâu cần quét đi quét lại nhiều lần? Về mặt lý thuyết, chỉ cần một lần quét là đủ.

Tôi rất ngưỡng mộ tác giả trình biên dịch Turbo Pascal ngày xưa. Ông ấy dùng hợp ngữ viết trình biên dịch một lần quét, tốc độ cực nhanh, tiêu tốn rất ít bộ nhớ. Việc quét một lần giúp giảm thiểu việc lưu trữ ngữ cảnh, tăng hiệu quả sử dụng cache CPU. Kết hợp nhiều giai đoạn phân tích còn giúp loại bỏ các bước tính toán trùng lặp. Tất nhiên, thách thức lớn nhất là đòi hỏi trình độ cao từ lập trình viên. Mà tôi thì xưa nay luôn thích chinh phục thử thách.

Ban đầu tôi khá liều lĩnh khi không thiết kế BNF rõ ràng, chỉ biết rằng sẽ dùng phương pháp phân tích đệ quy xuống. Dù ngôn ngữ của tôi mang phong cách C, đã lược bỏ các toán tử ba ngôi phức tạp và các quy tắc giải nghĩa con trỏ rườm rà, nhưng vấn đề về lvalue/rvalue vẫn rất nan giải. Đặc biệt là khi xử lý các phép toán kết hợp trái-phải đan xen, lại còn phải xử lý việc “quay lui” khi một ký hiệu có thể mang nhiều ý nghĩa khác nhau trong ngữ cảnh khác nhau. Việc tích hợp nhiều giai đoạn phân tích khiến thiết kế trở nên vô cùng phức tạp.

Một cải tiến quan trọng so với C là thiết kế trả về nhiều giá trị kiểu Lua. Trong môi trường không có con trỏ, tính năng này giúp tăng hiệu suất máy ảo. Nếu không, việc trả về nhiều giá trị buộc phải dùng bảng (table), dẫn đến chi phí cấp phát bộ nhớ, kích hoạt garbage collection và yêu cầu kiểm tra lại mã sau khi xử lý - tất cả đều làm giảm hiệu suất. Mục tiêu của tôi là vượt qua Lua về mặt hiệu năng, nên đây là tính năng không thể thiếu.

Tuy nhiên, việc xử lý hành vi trả về theo ngữ cảnh của hàm gọi lại gây không ít khó khăn. Tôi sao chép cơ chế “tail call” của Lua: chỉ khi lời gọi hàm nằm ở vị trí cuối cùng thì mới giữ nguyên số giá trị trả về, còn lại sẽ ép về một giá trị. Việc hiện thực hóa cơ chế này đã khiến tôi vật lộn suốt vài đêm liền.

Khi bắt tay vào viết code tuần này, đã hai tuần trôi qua kể từ lần code trước. Tôi phát hiện phần lớn code cũ không còn dùng được nữa. Với loại thuật toán phức tạp như thế này, tôi nhận ra cần duy trì liên tục mạch tư duy. Một khi luồng suy nghĩ bị gián đoạn, việc tìm lại “cache tư duy” trong đầu vô cùng khó khăn. Việc ghi chú trong code cũng không giúp được nhiều, chỉ đủ để lại các ghi chú “todo” nhắc nhở bản thân không bỏ sót chức năng. Sau vài ngày thức trắng, cuối cùng tôi cũng đạt được kết quả khả quan.

Nhìn lại quá trình, phần lõi trình biên dịch đã được viết đi viết lại tới ba lần. Quy trình lặp lại như sau: Đầu tiên viết phiên bản “xấu xí” chỉ đủ hoàn thành chức năng cơ bản - một đống code đầy lỗi, chỉ xử lý được các trường hợp đặc biệt và bỏ sót nhiều tính năng. Sau đó, khi đã có định hướng rõ ràng, tôi bắt đầu重构 (cải tổ lại cấu trúc). Mỗi lần chỉ chọn một tình huống đơn giản để viết lại theo phương pháp mới, khi đã tốt hơn phiên bản cũ thì loại bỏ dần code cũ. Các hàm không được sửa đổi từng cái một vì thiết kế liên tục thay đổi: Các công việc trước đây do nhiều hàm phối hợp thực hiện, nay có thể được tích hợp vào các hàm khác. Điều đáng mừng là tổng lượng code đã giảm xuống còn 70% so với lúc cao nhất, trong khi số tính năng lại tăng lên rõ rệt. Chương trình ngày càng trở nên rõ ràng mạch lạc.

Cảm giác lúc đó thật khó tả! Tiếng “ong ong” trong đầu biến mất, tâm trạng vô cùng sảng khoái. Đã lâu lắm rồi tôi chưa cảm nhận được niềm vui thuần khiết này. Có lẽ kỹ năng kiểm soát code của tôi đã tiến bộ vượt bậc, và đây cũng là chương trình phức tạp nhất tôi gặp trong thời gian gần đây.

Công việc tiếp theo dường như dễ dàng hơn nhiều. Khi hoàn thành phân tích biểu thức, tôi có thể viết hàng trăm dòng code một mạch, gần như không mắc lỗi, xử lý được các biểu thức phức tạp. Trước đây, trình biên dịch chỉ phân tích được các biểu thức thao tác với table. Trong hai ngày tới

0%