Khi mới bắt đầu tìm hiểu về compiler, tôi từng nghĩ nó đơn thuần là một chương trình đọc mã nguồn văn bản rồi dịch thẳng ra mã máy assembly. Cách mô tả ấy chỉ đúng theo kiểu nói một thành phố là nơi người ta đi vào từ cổng này rồi đi ra từ cổng kia. Phần thú vị và phức tạp thực sự nằm ở tất cả những trạm kiểm soát ngữ nghĩa nằm giữa hai cánh cổng ấy.
Bộ phân tích từ vựng (Lexer) biến dòng ký tự thô thành dòng các token có nghĩa. Bộ phân tích cú pháp (Parser) tổ chức các token thành cây cú pháp trừu tượng (AST). Bộ phân giải tên (Resolver) trả lời chính xác định danh nào đang trỏ đến khai báo nào trong symbol table. Bộ kiểm tra kiểu (Type Checker) kiểm tra xem các phép toán trên các kiểu dữ liệu có hợp lệ hay không. Bộ kiểm tra bộ nhớ và hiệu ứng (Memory & Effect Checker) chịu trách nhiệm thực thi các luật lệ an toàn mà DUY cam kết với người dùng. Bộ hạ cấp (Lowering) chuyển đổi cây AST giàu cú pháp thành biểu diễn trung gian (DUY IR) nhỏ gọn và rõ ràng hơn. Bộ xác minh (Verifier) kiểm tra tính toàn vẹn của IR trước khi gửi sang bộ tối ưu hóa (Optimizer). Backend compiler hạ biểu diễn ấy xuống LLVM IR hoặc mã nguồn C trung gian. Cuối cùng, trình liên kết (Linker) kết nối mã máy của chương trình với runtime và các thư viện hệ thống để đóng gói thành tệp thực thi nhị phân gốc (native binary).
graph TD
Source[Mã nguồn .duy] -->|tokenize| Tokens[Token Stream]
Tokens -->|parseProgram| AST[Abstract Syntax Tree]
AST -->|resolveImport & merge| ASTGroup[Merged AST Graph]
ASTGroup -->|TypeChecker check| TypedAST[Typed AST]
TypedAST -->|Memory & Effect Checks| CheckedAST[Checked AST - owned/view/effects]
CheckedAST -->|lowerProgram| DUY_IR[DUY IR V0]
DUY_IR -->|irVerify| VerifiedIR[Verified IR]
VerifiedIR -->|optimizeIR| OptimizedIR[Optimized IR]
OptimizedIR -->|compileLlvmSource| LLVM_IR[LLVM IR / emit-llvm]
OptimizedIR -->|compileSource| C_Code[C Code / emit-c]
LLVM_IR -->|clang compile| ObjectFile[Object File .o]
C_Code -->|clang compile| ObjectFile
ObjectFile -->|LLVM LLD linker| Binary[Native Binary]
Mỗi mũi tên trong sơ đồ trên là một nơi ngữ nghĩa an toàn của ngôn ngữ có thể bị rơi rụng hoặc dịch sai lệch.
Parser có thể phân tích sai độ ưu tiên của toán tử. Resolver có thể phân giải nhầm định danh khi gặp hiện tượng shadowing (che bóng tên). Type checker có thể vô tình chấp nhận một lời gọi hàm không tương thích kiểu. Bộ lowering có thể làm đảo lộn thứ tự thực thi của các hiệu ứng phụ. Emitter có thể sinh ra mã C vi phạm giao thức ABI của hệ điều hành.
Vì vậy, một compiler thực sự không thể chỉ dựa vào việc kiểm tra mã nguồn đầu vào và chạy thử tệp nhị phân đầu ra để cầu may. Nó bắt buộc phải cung cấp khả năng quan sát và kiểm chứng chi tiết trên từng tầng trung gian: xuất dòng token để kiểm tra, dump cấu trúc AST, in mã nguồn Typed IR, chạy độc lập bộ verifier để xác minh tính toàn vẹn, so sánh snapshot tự động và duy trì một bộ reject fixtures (các mã nguồn sai bắt buộc phải bị compiler từ chối). Khi một ca kiểm thử bị lỗi, ta phải biết chính xác lỗi xảy ra ở ga nào trong hành trình biên dịch. Nếu toàn bộ compiler chỉ là một hàm khổng lồ, việc gỡ lỗi giống như nghe tiếng động lạ trong tường nhà rồi đứng đoán xem con ốc nào đang bị lỏng bên trong.
Việc thiết kế đường ống compiler phân rã rõ ràng còn giúp ích rất lớn cho sự cộng tác của AI. Một AI agent nếu bị giao một nhiệm vụ mơ hồ như "sửa lỗi compiler" sẽ có một không gian quá rộng để tự do tạo ra các sửa đổi sai lệch với năng suất cực cao. Nhưng nếu agent được giao một phạm vi hẹp và rõ ràng như "thêm reject fixture cho luật view escape trong bộ phận checker, không được thay đổi cấu trúc backend", nó sẽ làm việc với độ chính xác tuyệt đối. Kiến trúc module không chỉ phục vụ tư duy của con người; nó thiết lập ranh giới an toàn cho sự cộng tác của máy móc.
DUY chọn xây dựng typed IR và verifier từ rất sớm vì lý do kiến trúc đó. IR là điểm giao thoa giữa ý định trừu tượng của mã nguồn và yêu cầu vật lý của backend sinh mã máy. Nếu IR quá giống AST, bộ tối ưu hóa sẽ phải hiểu quá nhiều cú pháp bề mặt rườm rà. Nếu IR quá gần mã máy, các khái niệm quyền sở hữu (ownership) và hiệu ứng phụ (effects) sẽ biến mất hoàn toàn trước khi kịp kiểm tra an toàn. DUY IR cần phải nhỏ gọn vừa đủ để dễ dàng xác minh thuật toán, nhưng phải đủ giàu ngữ nghĩa để bảo toàn các ràng buộc bất biến (invariants) quan trọng cho đến đúng thời điểm hạ xuống mã máy.
Một trình biên dịch trưởng thành còn cần phải coi hệ thống thông báo lỗi (diagnostics) như một sản phẩm phục vụ con người. Người dùng không quan tâm và không cần biết các pass xử lý nội bộ của compiler tên là gì; họ chỉ cần biết lỗi xảy ra ở chính xác dòng nào, vì sao sai và có thể sửa chữa bằng cách nào. Một compiler biên dịch đúng 100% nhưng chỉ phát ra duy nhất một thông báo "type mismatch" vô hồn ở một tệp mã nguồn dài nghìn dòng vẫn chỉ là một công cụ tồi tệ bắt con người phải trả giá đắt cho sự chính xác của nó.
Khi toàn bộ đường ống đã được vẽ rõ trên bảng, tôi nhận ra DUY không thiếu chỗ để bắt tay vào làm việc. Điều khó khăn là phải chọn ra một trục kiến trúc tạo ra sự khác biệt thực tế cho ngôn ngữ. Hệ thống effects trở thành một trong các trục ấy, bởi nó cung cấp cho trình biên dịch một ngôn ngữ nhất quán để nói về những hành vi phụ lâu nay vốn bị che giấu kín đáo đằng sau các chữ ký hàm.
Bình luận (0)
Đang tải bình luận...