Lời từ chối biên dịch đầu tiên của compiler thường gây ra sự khó chịu cho lập trình viên. Lời từ chối biên dịch đúng đầu tiên lại là một cột mốc trưởng thành lớn của ngôn ngữ.
Bộ parser chỉ có nhiệm vụ xác nhận một câu viết đúng ngữ pháp. Bộ type checker bắt buộc phải trả lời câu hỏi câu viết ấy có nghĩa logic hay không. Phép tính 1 + 2 có nghĩa logic. Phép tính true + Point thì hoàn toàn vô nghĩa, dù parser hoàn toàn có thể dựng lên một cây AST nhị phân cực kỳ đẹp đẽ cho phép cộng ấy.
Với Result, Option và khớp mẫu match, câu hỏi ngữ nghĩa trở nên sâu sắc và phức tạp hơn nhiều. Một biến thể variant mang theo payload dữ liệu gì? Cấu trúc pattern khớp có trùng khớp với kiểu dữ liệu của biến được match hay không? Các biến khai báo bên trong nhánh match mang kiểu dữ liệu nào? Các nhánh rẽ trả về kết quả có tương thích kiểu với nhau không? Và câu hỏi quan trọng nhất: lập trình viên đã xử lý hết mọi trường hợp có thể xảy ra của dữ liệu chưa?
match result {
Ok(v) => print(v)
Err(e) => print(0)
}
Sprint 10 đưa biểu thức khớp mẫu match V0 đi qua parser, xây dựng cấu trúc biểu diễn đơn giản, viết bản dự thảo thuật toán kiểm tra tính đầy đủ của các nhánh (exhaustiveness check) và chiến lược sinh mã C tương ứng. Chữ “dự thảo” (draft) bắt buộc phải được giữ nguyên trong câu chuyện kể. Việc kiểm tra đủ nhánh trên hai biến thể đơn giản khác xa việc xây dựng thuật toán exhaustiveness check cho các mẫu khớp lồng nhau phức tạp (nested patterns), các nhánh có điều kiện lọc (guards), các enum đa tầng hoặc khả năng mở rộng thư viện trong tương lai.
Nhưng ngay cả phiên bản sơ khai nhỏ bé ấy đã thay đổi hoàn toàn triết lý xử lý thất bại của DUY. Lập trình viên không được phép nhận về một kiểu Result rồi vô trách nhiệm giả vờ như chương trình chỉ có duy nhất một con đường thành công. Trình biên dịch bắt buộc họ phải đối mặt và xử lý cả trường hợp Ok lẫn trường hợp Err. Sự phiền toái nhỏ ấy là một hình thức tôn trọng thực tế vận hành hệ thống.
Bất kỳ ai từng vận hành phần mềm ở production đều hiểu rõ lỗi hệ thống bị lập trình viên bỏ quên không bao giờ tự biến mất. Nó chỉ chọn thời điểm xuất hiện tồi tệ nhất để sập chương trình, thường là vào lúc nửa đêm khi người viết code đã đi ngủ hoặc đã chuyển sang công ty khác. Một bộ type checker tốt kiên quyết kéo cuộc gặp gỡ lỗi ấy về compile time, khi context logic của chương trình vẫn đang hiển thị rõ ràng trên màn hình và tách cà phê của bạn vẫn còn nóng.
Tôi bắt đầu cảm thấy một niềm vui kỳ lạ khi viết các ca kiểm thử báo lỗi (reject tests). Happy path (mã chạy đúng) mang lại cảm giác dự án đang tiến lên. Reject path (mã viết sai bị từ chối) mang lại bằng chứng rằng nền móng của ngôn ngữ đang cứng cáp lại. Khi một fixture kiểm thử cố tình viết thiếu một nhánh xử lý variant và trình biên dịch kiên quyết báo đỏ từ chối đúng nguyên nhân, nó chứng minh sòng phẳng rằng DUY không chỉ có cú pháp an toàn ở vỏ ngoài mà đã bắt đầu có hành vi an toàn thực sự từ bên trong cốt lõi.
Tất nhiên, checker hoàn toàn có thể nói không sai lầm. Nó có thể phân giải nhầm symbol trong bảng ký hiệu, suy luận kiểu dữ liệu quá hẹp hòi, hoặc từ chối oan một chương trình hoàn toàn hợp lệ chỉ vì thuật toán kiểm tra của compiler chưa đủ thông minh. Đây là lý do vì sao mọi thông báo lỗi bắt buộc phải gắn span vị trí vật lý và mỗi luật lệ biên dịch cần phải duy trì song song cả hai bộ ca kiểm thử: accept corpus và reject corpus. Nếu chỉ có reject test, compiler dễ biến thành một người gác cổng khó tính và độc đoán. Nếu chỉ có accept test, nó lại thành một người gác cổng ngủ quên để mặc cho lỗi lọt qua.
Mỗi khi thêm một luật lệ ngữ nghĩa mới cho compiler, DUY bắt buộc phải tự trả lời ba câu hỏi:
1. Chương trình hợp lệ nào bắt buộc phải được chấp nhận? (Accept)
2. Chương trình sai trái nào bắt buộc phải bị từ chối? (Reject)
3. Lập trình viên sẽ hiểu nguyên nhân bị từ chối bằng cách nào? (Explain)
Ba câu hỏi này về sau trở thành cấu trúc cốt lõi cho việc thiết kế static ownership, effect system, unsafe boundary và IR verification. Type checker V0 chưa giải quyết trọn vẹn toàn bộ đồ thị, nhưng đã thiết lập thành công một văn hóa kỹ thuật đúng đắn: Correctness (tính đúng đắn) của chương trình không đơn thuần là việc chạy ra kết quả mong muốn ở happy path.
Một vấn đề kiến trúc khác bắt đầu lộ ra. AST ngày càng phình to và giàu cú pháp bề mặt, khiến cho backend sinh mã máy phải hiểu quá nhiều chi tiết vụn vặt của frontend. Nếu emitter trực tiếp đi qua từng nút AST để sinh mã C hay LLVM, mỗi tính năng cú pháp mới được thêm vào sẽ làm mã nguồn codegen phình to và bộ tối ưu hóa cực kỳ khó hoạt động. Compiler cần một ngôn ngữ biểu diễn nội bộ nhỏ gọn và explicit hơn.
Đó là lúc DUY IR xuất hiện. Và như một quy luật tự nhiên, sự ra đời của một tầng kiến trúc mới mang theo cả những ưu điểm thiết kế tốt lẫn những lỗi logic mới tinh vi mà trước đó dự án chưa đủ quy mô để mắc phải.
Bình luận (0)
Đang tải bình luận...