Tập 24: Ngữ pháp là hợp đồng

Khi ngôn ngữ lập trình còn ở quy mô nhỏ, việc thêm một tính năng mới vào cú pháp tạo cảm giác tiến độ cực kỳ nhanh chóng. Hôm nay ta thêm kiểu chuỗi ký tự, ngày mai thêm kiểu số thực, ngày kia thêm kiểu cấu trúc struct. Trang tài liệu README.md dài ra nhanh chóng và ta dễ dàng ảo tưởng rằng tốc độ tăng từ khóa cú pháp tỉ lệ thuận với tốc độ trưởng thành ngữ nghĩa của ngôn ngữ.

Sprint 03 đến Sprint 06 đưa bốn viên gạch dữ liệu cơ bản vào ngữ pháp của DUY: strings, numeric types, structs và fixed arrays. Chúng làm cho mã nguồn DUY bắt đầu trông giống như một nơi có thể viết ra các chương trình thực tế, thay vì một chiếc máy tính bỏ túi chỉ biết xử lý các lệnh if nguyên thủy.

Kiểu chuỗi ký tự strings đầu tiên trông vô cùng giản đơn:

fn main() -> i32 {
  print("DUY")
  return 0
}

Nhưng để đưa được ba ký tự chữ ấy hiển thị đúng đắn ra màn hình terminal, lexer bắt buộc phải nhận diện dấu nháy kép, parser phải dựng nút AST StringLiteral, type checker phải hiểu nó mang kiểu dữ liệu str, emitter phải xử lý escape ký tự chuẩn xác khi chuyển dịch sang C, và runtime print phải tự động ánh xạ cấu trúc tương ứng ở backend. Chỉ riêng việc viết chuỗi có xuống dòng "dòng 1\ndòng 2" đã kéo theo hàng loạt bài toán xử lý escape sequence, newline, dấu gạch chéo ngược backslash và lỗi chuỗi ký tự không có dấu đóng nháy kép ở cuối file.

Các kiểu số (numeric types) làm cho hợp đồng dữ liệu trở nên chi tiết và sát máy hơn. Các từ khóa i32, u64, f64 không đơn thuần là ba cách viết số khác nhau cho đẹp mắt. Chúng tuyên bố rõ ràng với phần cứng về độ rộng bit (width), tính chất có dấu/không dấu (signedness), các phép toán được phép thực thi và cách biểu diễn vật lý ở backend. Hệ thống tests không chỉ kiểm tra xem chương trình chạy ra kết quả 42 hay 3.14; nó kiểm tra nghiêm ngặt xem mã nguồn C sinh ra ở backend có sử dụng chính xác các kiểu số cố định bit tương ứng như int32_t, uint64_tdouble hay không. Nếu bộ checker hiểu một kiểu dữ liệu ở frontend nhưng backend lại tự ý dịch hạ cấp thành một kiểu dữ liệu có độ rộng khác, lời hứa an toàn của ngôn ngữ đã bị phản bội ở ngay giữa hành trình biên dịch.

Structs đưa các khái niệm định danh và cấu trúc layout vào trong dữ liệu:

struct Point {
  x: i32
  y: i32
}

let p: Point = Point { x: 10, y: 20 }
print(p.x)

Compiler bắt buộc phải lưu giữ thông tin khai báo struct, phát hiện ngay lập tức nếu người dùng truy cập một trường dữ liệu không tồn tại, kiểm tra kiểu của từng giá trị gán, xử lý biểu thức field access và sinh ra layout bộ nhớ chuẩn xác ở backend. Kiểu dữ liệu không còn là một nhãn nguyên thủy đơn giản nữa; nó mang theo cấu trúc layout phức tạp mà nhiều phase trong compiler bắt buộc phải cùng đồng ý thống nhất.

Fixed arrays (mảng có kích thước cố định) đưa yếu tố kích thước vào trong chữ ký kiểu: [i32; 3]. Literal mảng bắt buộc phải có số lượng phần tử trùng khớp với khai báo kiểu, từng phần tử phải tương thích kiểu dữ liệu, phép truy cập chỉ số array indexing bắt buộc phải dùng số nguyên, và backend cần phải sắp xếp layout mảng liên tục trong bộ nhớ. Ngay tại khoảnh khắc đó, một câu hỏi thiết kế hệ thống xuất hiện mà không cách nào giải quyết được bằng cú pháp bề ngoài: phép kiểm tra biên mảng (bounds check) sẽ được thực thi ở đâu và tiêu tốn bao nhiêu chi phí hiệu năng chạy?

Bốn tính năng dữ liệu ấy dạy cho chúng tôi một bài học lớn: ngữ pháp ngôn ngữ là một bản hợp đồng cam kết đa phương trong compiler. Lexer đồng ý token nào hợp lệ. Parser đồng ý các token kết hợp với nhau ra sao. Checker đồng ý biểu thức nào có nghĩa logic. IR đồng ý lưu giữ những thông tin gì. Backend đồng ý cách biểu diễn vật lý cuối cùng dưới máy. Tài liệu hướng dẫn đồng ý lập trình viên được quyền kỳ vọng những gì từ ngôn ngữ.

Nếu một bên tự ý thay đổi mà các bên khác không được cập nhật tương ứng, tính năng mới sẽ rơi vào trạng thái nửa sống nửa chết. Parser chấp nhận cú pháp nhưng checker im lặng báo lỗi không hiểu. Checker thông qua nhưng emitter crash sập tiến trình. C backend chạy đúng nhưng LLVM backend sinh lỗi mã máy. Bộ định dạng code formatter xóa mất code của người dùng. Giao thức LSP tô màu nhầm sang một ngôn ngữ khác. Một tính năng không bao giờ được phép đánh dấu hoàn thành chỉ vì parser đã nuốt trôi mã nguồn đầu vào.

Mỗi sprint phát triển của DUY đều có demo chạy được và gate kiểm thử nghiêm ngặt tương ứng: strings phải in ra đúng; ánh xạ số sang C phải dùng đúng kiểu fixed-width; struct field đọc được dữ liệu; array in đúng phần tử. Các ca kiểm thử này còn vô cùng đơn giản, nhưng chúng thực thi một nguyên tắc kiến trúc đáng sắt đá: cú pháp chỉ hoàn thành khi hành vi logic của nó đã đi trọn vẹn và an toàn qua toàn bộ đường ống biên dịch.

Sự hỗ trợ của AI giúp bề mặt cú pháp tăng trưởng nhanh hơn gấp trăm lần năng lực củng cố ngữ nghĩa của compiler. Trợ lý AI có thể sinh ra các nhánh parser mới, các nhánh emitter và các ca kiểm thử happy-path chỉ trong vòng vài phút. Phần việc khó khăn và tẻ nhạt hơn nhiều là đi tìm kiếm và xây dựng các ca kiểm thử phủ nhận (reject cases): struct thiếu trường, truyền trường lạ, sai kiểu dữ liệu trường, độ dài mảng không trùng khớp, escape ký tự lỗi và sự lệch pha ngữ nghĩa giữa các backend. Chính những ca kiểm thử reject cases này mới là thứ biến một cú pháp bề ngoài trở thành luật lệ biên dịch thực sự của ngôn ngữ.

Vì thế, DUY bắt buộc phải học cách nói câu “chưa hỗ trợ” một cách trung thực. Một tính năng có cú pháp nhỏ gọn nhưng kín kẽ về mặt ngữ nghĩa và hợp đồng luôn tốt hơn một tính năng rộng lớn nhưng mỗi backend lại hiểu và dịch theo một nghĩa khác nhau. Ngôn ngữ lập trình là nơi người dùng đặt kỳ vọng kiến trúc lâu dài; một sự mơ hồ ngữ nghĩa được dung túng hôm nay sẽ biến thành một món nợ tương thích khổng lồ cản bước dự án trong tương lai.

Sau khi strings, numbers, structs và arrays được thêm vào, bộ parser đã có khá nhiều hình dạng ngữ pháp để nhận biết. Nhưng cây AST mới chỉ là hình học bề ngoài của câu chữ. Tập tiếp theo bắt đầu khi cây AST bắt buộc phải nhìn thấy các ý định ngữ nghĩa sâu sắc hơn của lập trình viên: mượn tạm dữ liệu, chương trình liên kết nhiều file và xử lý thất bại như một giá trị dữ liệu thông thường.


Bình luận (0)

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