Tập 21: Tạo folder, đặt repo, lập luật

Từ chương này, giấc mơ bắt đầu phải trả lời những câu hỏi không biết nể nang của thực tế: file nằm ở đâu, lệnh chạy thế nào, lỗi hiện ra sao, và máy tính có thật sự hiểu điều ta vừa viết hay không.

Hai chương đầu là quãng thời gian của câu hỏi, lịch sử và những nguyên tắc thiết kế. Chúng vô cùng cần thiết, nhưng một ngôn ngữ lập trình không thể sống mãi trong vùng an toàn của triết lý tư tưởng. Đến một lúc nào đó, ta bắt buộc phải tạo thư mục, mở terminal và chấp nhận sự thật lạnh lùng rằng máy tính không bao giờ biên dịch lòng nhiệt thành của con người.

Chương này kể lại mười bước đi thực tế đầu tiên của DUY trên con đường từ mã nguồn thô tới tệp thực thi nhị phân gốc. Mỗi bước đi nếu nhìn riêng lẻ đều vô cùng nhỏ bé: một CLI thô sơ, một thông báo lỗi có caret chỉ dòng, một kiểu dữ liệu mới được nhận diện, một cây AST phân nhánh, một bộ checker. Nhưng ngôn ngữ lập trình cũng giống như một thành phố. Không có một viên gạch nào tự thân nó là thành phố; nhưng nếu thiếu đi đủ nhiều viên gạch, chắc chắn nơi đó chỉ mãi là một bãi đất trống hoang tàn.



Việc đầu tiên tôi làm để tạo một ngôn ngữ lập trình mới không phải là ngồi thiết kế type system. Tôi tạo một thư mục trống trên máy tính.

And ngay cả cái việc cực kỳ đơn giản ấy cũng không hoàn toàn suôn sẻ.

Dự án DUY ban đầu bị đặt nhầm vào một đường dẫn thư mục sâu hoắm bên trong khu vực các dự án nghiên cứu học thuật của tôi. Tôi nhìn vào đường dẫn ấy và biết ngay có điều gì đó không ổn. Một dự án mà bạn định sống chung và làm việc hàng ngày trong nhiều năm trời cần phải có một địa chỉ trực quan, ngắn gọn, có thể gõ nhanh trên terminal mà không cần phải lấy đà. Tôi kiên quyết kéo nó về một nơi đơn giản và sạch sẽ hơn:

/Users/tranducduy/MyWork/DUY

Chi tiết nhỏ này chẳng có gì anh hùng hay lãng mạn. Nhưng nó lại hoàn toàn khớp với cách một phần mềm thực tế được sinh ra. Trước khi tranh luận về static ownership hay backend LLVM, lập trình viên thường phải đi sửa lại một cái path cấu hình, đặt lại tên thư mục và xác định xem terminal hiện tại đang đứng ở đâu. Các huyền thoại công nghệ ít khi kể về những phân đoạn này, có lẽ vì lệnh pwd lên phim trông không được đẹp lắm.

Khi thư mục đã đúng vị trí, câu hỏi tiếp theo xuất hiện: bên trong nó bắt buộc phải chứa những gì?

Một ý tưởng có thể sống bay bổng trong đầu lập trình viên rất lâu mà không cần đến cấu trúc. Nhưng một repository Git thì không được hưởng đặc quyền lãng mạn ấy. Nó buộc ta phải chia nhỏ thế giới thành các file và thư mục cụ thể, xác định điểm chạy đầu vào (entry point), ghi nhận dependency, phân định rõ thứ gì thuộc mã nguồn, thứ gì là sản phẩm biên dịch đầu ra, và thứ gì cấm tuyệt đối không được đưa lên git. Chỉ riêng việc viết file cấu hình .gitignore đã là một lời thành thật thừa nhận rằng dự án của chúng ta chắc chắn sẽ sinh ra rác nếu không thiết lập luật lệ từ đầu.

Snapshot Git đầu tiên của DUY mang mã commit 4011f60. Nó chứa ba file tối giản: .gitignore, README.mdpackage.json. Lịch sử dự án lúc ấy được ghi nhận theo những khối công việc lớn, nên commit đầu tiên không phải là cuốn nhật ký chi tiết ghi nhận từng phút giây gõ phím khởi tạo. Nhưng ba artifact ấy công bố rất rõ ba nhu cầu cơ bản ban đầu:

  • Repository bắt buộc phải biết rõ thứ gì cần được giữ lại vĩnh viễn;
  • Con người đọc vào phải hiểu ngay dự án đang cố gắng giải quyết bài toán gì;
  • Máy tính phải biết cách gọi thực thi công cụ dòng lệnh của DUY.

File README.md đặt một sơ đồ đường ống biên dịch lên đầu bàn làm việc:

Mã nguồn DUY (.duy) 
  -> Phân tích từ vựng & cú pháp (Tokenize + Parse)
  -> Kiểm tra kiểu tĩnh (Type-check)
  -> Hạ cấp sang biểu diễn trung gian (DUY IR)
  -> Tối ưu hóa IR (Optimize)
  -> Sinh mã máy C hoặc LLVM (Emit C/LLVM)
  -> Biên dịch bằng Clang (Clang compilation)
  -> Tệp nhị phân thực thi gốc (Native binary)

Nhìn lại, sơ đồ pipeline ấy mang tính tham vọng lớn hơn rất nhiều so với tình trạng kỹ thuật thực tế của khoảnh khắc khởi đầu. Nó giống như bản vẽ một tuyến tàu điện hiện đại được treo trang trọng trước nhà ga khi mà hầu hết các ga tàu vẫn còn chưa được đổ móng. Nhưng ít nhất nó định vị rõ ràng hướng đặt đường ray cho đội ngũ. Nếu không có sơ đồ định hướng ấy, mỗi AI agent có thể sẽ tự ý xây dựng một đoạn đường cực kỳ đẹp mắt nhưng không cách nào nối được vào nhau.

File manifest package.json thực hiện một nhiệm vụ ít thơ mộng hơn: đăng ký lệnh duy vào hệ thống. Ở giai đoạn khởi động (bootstrap), Node.js là một môi trường chạy cực kỳ thực tế và nhanh chóng giúp CLI và compiler chạy sớm để kiểm chứng. Đây chắc chắn chưa phải là hình ảnh cuối cùng của một systems language độc lập. Nó chỉ là hệ thống giàn giáo. Giàn giáo tốt phải đủ vững chãi để người thợ đứng lên làm việc an toàn, đồng thời phải đủ thô ráp để không ai nhầm lẫn nó với chính tòa nhà đang xây.

Tôi từng có những lúc vô cùng sốt ruột với chi tiết này. Đã tự gọi là ngôn ngữ hệ thống mới thì tại sao compile time lại còn phụ thuộc vào Node.js và JavaScript? Tại sao backend biên dịch lại phải đi đường vòng qua C? Câu trả lời tuy không dễ chịu nhưng lại rất thực tế: muốn tự giải phóng khỏi một công cụ có sẵn, trước tiên bạn bắt buộc phải có một hệ thống mới đủ hoạt động để có thể tự thay thế công cụ đó từng phần. Viết một compiler bằng chính ngôn ngữ chưa tồn tại là bài toán con gà và quả trứng kinh điển; chỉ khác biệt là con gà có bộ parser còn quả trứng thì cần đến linker.

Vì vậy, Sprint 01 của DUY không đặt ra các mục tiêu xa vời như “xóa bỏ Node.js”, “thay thế hoàn toàn C” hay “tự biên dịch chính mình (self-hosting)”. Nó đặt ra một mục tiêu vô cùng nhỏ bé và thực dụng:

Làm cho lệnh duy có thể được sử dụng trực tiếp trên terminal của hệ điều hành.

Kế hoạch ghi rõ các lệnh CLI bắt buộc phải tồn tại:

duy build   -> Biên dịch tạo tệp nhị phân
duy run     -> Biên dịch rồi thực thi ngay kết quả
duy check   -> Kiểm tra ngữ pháp và kiểu mà không sinh file
duy emit-c  -> Xuất mã nguồn C trung gian để debug

Đây là một quyết định kiến trúc quan trọng hơn vẻ bề ngoài của nó. Một ngôn ngữ lập trình chỉ thực sự bắt đầu trở thành một công cụ khi người dùng có các động từ cụ thể để yêu cầu nó làm việc. build hứa hẹn tạo ra sản phẩm. run hứa hẹn đi hết đường ống rồi chạy kết quả. check hứa hẹn phản hồi nhanh mà không bắt người dùng phải chờ đợi clang sinh mã máy. emit-c thừa nhận cây cầu backend đang dùng và mở toang nó ra để phục vụ gỡ lỗi.

Tôi viết ra các luật lệ nghiêm ngặt cho sprint:

- Mỗi sprint bắt buộc phải có một demo hoạt động được.
- Toàn bộ hệ thống kiểm thử (tests) phải báo xanh.
- Tài liệu hướng dẫn (docs) phải được cập nhật tương ứng.
- Các ví dụ (examples) cũ tuyệt đối không được phép bị lỗi chạy (no regression).

Đó là khoảnh khắc DUY chính thức có một thứ còn quan trọng hơn cả cú pháp: định nghĩa thực tế về chữ “Xong” (Definition of Done).

Trong các dự án phát triển phần mềm có sự tham gia của nhiều AI agents, chữ “xong” rất dễ bị lạm phát một cách vô tội vạ. Một agent có thể viết xong năm file mã nguồn mới rồi báo cáo hoàn thành nhiệm vụ. Một agent khác tự đánh dấu tick vào checklist vì thấy đoạn code trông có vẻ hợp lý. Các báo cáo tiến độ gửi lên thường vô cùng mạch lạc và đẹp đẽ, đôi khi còn mạch lạc hơn chính mã nguồn thực tế dưới ổ đĩa. Nếu không có các lệnh xác thực tự động (verify command) và các sản phẩm có thể chạy thực tế để quan sát, tiến độ dự án sẽ phình to rất nhanh trên các trang tài liệu, trong khi sản phẩm thực tế vẫn dậm chân tại chỗ.

Luật lệ của sprint là cách tôi ép buộc các tham vọng thiết kế phải đi qua khe cửa hẹp của thực tế. Muốn tuyên bố CLI hoạt động, bạn bắt buộc phải gọi được lệnh đó trên terminal. Muốn tuyên bố compiler hiểu được cú pháp mới, bạn bắt buộc phải có fixture kiểm thử tương ứng. Muốn bảo đảm không có lỗi regression, toàn bộ ví dụ cũ phải chạy thông suốt. Mỗi câu tuyên bố tiến độ đều cần phải có một bằng chứng vật lý mà máy tính có thể bác bỏ ngay lập tức nếu nó sai.

Luật lệ này không hề kìm hãm tốc độ của dự án. Nó chỉ đơn giản là loại bỏ đi những tiến độ tưởng tượng.

Tất nhiên, các luật lệ ban đầu ở Sprint 01 chưa thể hoàn hảo ngay. “Tests passing” chưa mang nhiều ý nghĩa nếu số lượng test quá ít ỏi. “Docs updated” rất dễ biến tướng thành việc sửa đổi đối phó cùng một trạng thái ở năm file tài liệu khác nhau. “Demo hoạt động được” có thể chỉ bao phủ duy nhất một con đường thuận lợi (happy path). Về sau, DUY bắt buộc phải bổ sung các bộ reject fixtures, so sánh vi sai (differential compare), bộ verifier cho IR, fuzz testing và các quy trình tự dựng compiler. Nhưng Sprint 01 đã gieo trồng thành công một thói quen kỹ thuật lành mạnh: tuyệt đối không đánh giá tiến độ dự án bằng số dòng code đã viết hay độ tự tin của người báo cáo.

Tôi vẫn nhớ cảm giác nhìn cây thư mục sơ khai đầu tiên của DUY trên ổ đĩa. Nó còn vô cùng nhỏ bé, nhưng đã có một trung tâm rõ ràng. Không còn là câu chuyện viển vông về “sẽ có một ngôn ngữ lập trình tên là DUY” nữa. Bây giờ, đã có một nơi chốn vật lý cụ thể để các ý tưởng thiết kế vấp ngã, được sửa chữa, rồi lại tiếp tục vấp ngã theo những cách mới tinh vi và phức tạp hơn.

Một repository tốt không bảo đảm cho dự án thành công. Nó chỉ đơn giản là tước đi của dự án mọi nơi chốn để trốn tránh thực tế của chính mình. Git sẽ nhớ rõ file nào được thêm. Tests sẽ nhớ rõ hành vi nào từng chạy đúng. Plans sẽ nhớ rõ ta đã hứa hẹn những gì. Và người viết code bắt buộc phải học cách chung sống kỷ luật với cả ba loại trí nhớ máy tính ấy.


Bình luận (0)

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