Tập 23: `Hello, world` và sự thật khó khăn

Một ngôn ngữ lập trình mới thường được tác giả giới thiệu với thế giới bằng các chương trình viết đúng ngữ pháp chạy ra kết quả hoàn hảo. Người dùng thực tế khi tiếp cận ngôn ngữ lại thường gặp gỡ nó lần đầu tiên qua một chương trình viết sai.

Chúng ta gõ thiếu một dấu ngoặc nhọn. Chúng ta viết nhầm tên một biến cục bộ. Chúng ta vô tình truyền một biến kiểu bool vào nơi chữ ký hàm yêu cầu kiểu số nguyên i32. Nếu trình biên dịch chỉ thông minh và hoạt động trơn tru với các chương trình viết đúng của tác giả ngôn ngữ, cuộc gặp gỡ đầu tiên giữa ngôn ngữ mới với thế giới kỹ sư bên ngoài sẽ kết thúc vô cùng chóng vánh.

Ngay sau khi chương trình hello in ra thành công con số 42, Sprint 02 của DUY lập tức chuyển hướng sang một mục tiêu ít mang lại sự phấn khích hơn nhưng lại là điều kiện sống còn của trải nghiệm sử dụng: thông báo lỗi của trình biên dịch bắt buộc phải đọc hiểu được.

Trước khi tích hợp dữ liệu vị trí mã nguồn (source span), trình biên dịch chỉ có thể báo lỗi theo kiểu biết có lỗi xảy ra nhưng không chỉ ra được lỗi nằm ở chính xác vị trí nào trong tệp nguồn. Nó giống như một bác sĩ bước vào phòng bệnh báo cáo ngắn gọn “cơ thể anh đang gặp sự cố sức khỏe” rồi quay lưng đi về. Thông tin ấy không hoàn toàn vô ích, nhưng người nghe hoàn toàn bất lực không biết mình cần phải làm gì tiếp theo để sửa chữa cơ thể.

DUY bắt đầu gắn chặt dữ liệu vị trí vật lý vào từng token và từng nút trên cây AST: dòng bắt đầu, cột bắt đầu, dòng kết thúc, cột kết thúc. Bộ parser có nhiệm vụ tạo ra thông tin span này cho mọi biểu thức (expression) và câu lệnh (statement). Bộ type checker khi phát hiện ra một lỗi không khớp kiểu dữ liệu sẽ đóng gói nút AST lỗi cùng thông tin span tương ứng gửi vào cấu trúc lỗi DuyError. Trình kết xuất diagnostics (diagnostic renderer) sẽ đọc tệp nguồn gốc dưới đĩa, trích xuất dòng mã nguồn lỗi và dựng lên một code frame hiển thị trực quan lỗi kèm theo caret (dấu chỉ vị trí ^^) chỉ thẳng vào đúng từ khóa viết sai.

Một ca kiểm thử viết sai kiểu dữ liệu được thiết kế tối giản:

fn main() -> i32 {
  let ok: bool = true
  let bad: i32 = ok
  return bad
}

Con người nhìn qua dòng thứ ba là hiểu ngay lỗi ở đâu. Nhưng trình biên dịch của DUY phải tự mình phân giải định danh ok, nhận diện biến này mang kiểu bool, nhận diện biến bad được khai báo mang kiểu i32, so sánh đối chiếu hai kiểu dữ liệu này, phát hiện sự không tương thích, lấy ra thông tin span của biểu thức gán, rồi render thông tin lỗi tại chính xác tọa độ cột của định danh ok.

Kết quả hiển thị trên terminal không chỉ là một dòng chữ vô hồn “Type error”. Hệ thống kiểm thử tự động yêu cầu luồng lỗi stderr bắt buộc phải chứa tên file nguồn, số dòng 3, số cột và dấu caret định vị chính xác:

  --> tests/fixtures/type_error.duy:3:18
3 |   let bad: i32 = ok
  |                  ^^

Trình biên dịch đã thoát khỏi trạng thái của một quan tòa chỉ biết lạnh lùng phán quyết. Nó bắt đầu biết cách trình bày bằng chứng phạm tội một cách thuyết phục cho lập trình viên.

Tôi từng nghĩ hệ thống diagnostics chỉ là một lớp sơn trang trí bề ngoài được phủ lên sau khi lõi ngữ nghĩa đã hoàn tất. Chỉ khi trực tiếp bắt tay vào viết compiler, tôi mới nhận ra điều ngược lại: nếu thông báo lỗi không định vị được vị trí và chỉ rõ nguyên nhân lỗi, chính những người phát triển compiler cũng hoàn toàn bất lực không biết bộ checker của mình đang bị sai lệch logic ở ga nào trong pipeline. Hệ thống diagnostics chất lượng cao là công cụ sinh tồn phục vụ phát triển nội bộ trước khi nó trở thành trải nghiệm của người dùng bên ngoài.

Thông tin vị trí nguồn (source spans) còn tạo dựng nền tảng vững chắc cho việc phát triển giao thức LSP (Language Server Protocol) về sau: tự động hiển thị lỗi khi đang gõ code trong editor, tính năng go-to-definition (nhảy tới khai báo), tự động định dạng mã nguồn (formatter), và cung cấp dữ liệu lỗi có cấu trúc chặt chẽ cho các trợ lý AI. Một quyết định thiết kế nhỏ và tẻ nhạt ở Sprint 02 đã mở ra cánh cửa công nghệ cho hàng loạt công cụ thông minh ở nhiều phase sau của dự án.

Nhưng thông tin span rất dễ bị đánh rơi trong quá trình biên dịch. Parser có thể gắn span rất chuẩn xác, nhưng khi đi qua giai đoạn hạ cấp (lowering) tạo ra các nút IR mới, lập trình viên nếu lười biếng rất dễ quên không sao chép dữ liệu vị trí gốc theo nút mới. Quá trình xử lý import module có thể vô tình làm thất lạc tên file nguồn. Một biểu thức phức tạp được ghép từ nhiều nút con có thể tính toán ra một vị trí span quá rộng che khuất đi điểm sai thực tế. Nếu hệ thống kiểm thử chỉ so khớp nội dung chữ của thông báo lỗi mà bỏ qua việc kiểm tra độ chính xác của dòng và cột, các lỗi thất lạc span sẽ âm thầm vượt qua cổng kiểm soát một cách lịch sự.

Đây là sự thật khắc nghiệt phía sau chương trình hello chạy đúng: việc biên dịch thành công một vài ví dụ đúng chưa nói lên được nhiều điều về khả năng sống sót lâu dài của ngôn ngữ trong thực tế. Lập trình viên dành 80% cuộc đời của họ bên trong những đoạn code chưa biên dịch được. Trải nghiệm biên dịch thất bại không phải là một trường hợp phụ họa (edge case); nó chính là giao diện tương tác hàng ngày của con người với ngôn ngữ.

Một trình biên dịch kỳ cựu không chỉ được đánh giá cao nhờ những chương trình phức tạp mà nó chấp nhận; nó được đánh giá trước hết nhờ chất lượng của những lời từ chối biên dịch. Một lời từ chối biên dịch tốt không bao giờ làm cho người viết code cảm thấy mình ngu ngốc. Nó giúp họ xây dựng lại một mô hình tư duy chính xác hơn về luật lệ ngữ nghĩa của ngôn ngữ và chỉ ra con đường ngắn nhất để sửa chữa mã nguồn.

DUY ở Sprint 02 mới chỉ có một bộ hiển thị code frame cơ bản, chưa có hệ thống mã lỗi ổn định (error codes) hay chế độ giải thích lỗi chi tiết (explain mode). Nhưng chúng tôi đã chọn đi một con đường đúng đắn: vị trí lỗi là dữ liệu cấu trúc cốt lõi của compiler, tuyệt đối không phải là một chuỗi văn bản được vá víu thủ công vào giai đoạn cuối cùng.

Sau khi compiler đã biết cách chỉ ra chính xác vị trí sai, câu hỏi tiếp theo là nó sẽ thực thi các luật lệ an toàn ấy cho những kiểu dữ liệu nào. Các kiểu chuỗi ký tự (strings), số (numbers), struct và mảng (arrays) bắt đầu đi vào cấu trúc ngữ pháp của DUY. Mỗi tính năng mới được thêm vào cú pháp mở ra một cách diễn đạt mới cho con người, đồng thời mở ra thêm hàng chục cách viết sai khác nhau cho chương trình.


Bình luận (0)

Đang tải bình luận...