Aura - Một Ngôn Ngữ Nhỏ Nhúng Được
Tuần trước, khi đọc về Aocla - đứa con tinh thần mới của tác giả Redis, tôi cảm thấy vô cùng hứng khởi. Đây là sự kết hợp độc đáo giữa FORTH và Lisp, đồng thời bổ sung thêm hỗ trợ biến cục bộ. Điều này khiến tôi nhớ lại dự án nhỏ từng thực hiện cho thư viện toán học của nhóm cách đây vài năm. Ban đầu, ý tưởng là xây dựng một ngôn ngữ chuyên dụng (DSL) nhúng vào Lua, nhằm giữ các phép tính phức tạp ở phía C, giảm thiểu chi phí chuyển đổi giữa Lua và C. Phiên bản đầu tiên được thiết kế theo kiểu FORTH dựa trên ngăn xếp, hy sinh một phần tính biểu đạt để đổi lấy hiệu năng. Tuy nhiên, trải nghiệm thực tế khá gượng gạo, dù đã thêm vài tính năng mới nhưng không cải thiện đáng kể. Cuối cùng, DSL này đã bị tách hoàn toàn khỏi thư viện toán học.
Sau khi nghiên cứu Aocla, tôi nhen nhóm ý tưởng mới. Với chỉ hơn 1.000 dòng mã, tôi đánh giá việc triển khai một ngôn ngữ nhỏ như vậy có thể hoàn thành trong 2 ngày, chi phí thử nghiệm thấp. Vậy tại sao không thử tự xây dựng một phiên bản riêng?
Tôi không xem xét chi tiết cách hiện thực của Aocla. Mô hình DSL tôi hình dung sẽ cung cấp đầy đủ cấu trúc điều khiển, kiểu dữ liệu dựa trên danh sách (list). Về mặt này, tôi đã có kinh nghiệm nên việc viết code sẽ thuận tay hơn. Kiểu số liệu tham gia tính toán sẽ được tiêm nạp từ bên ngoài, tương tự userdata của Lua nhưng hiệu quả và hỗ trợ kiểu tốt hơn. Thực tế, trong Lua, các kiểu dữ liệu tự định nghĩa luôn ở vị thế “công dân hạng hai”, chi phí giao tiếp giữa Lua và C rất cao.
Nếu nhúng vào Lua, kiểu string bản địa trở nên thừa thãi. Mọi số liệu đều có thể là cặp kiểu + ID, đảm bảo kích thước cố định. Quy mô mã nguồn DSL không lớn, bản thân code cũng là list, do đó giới hạn 64KB không gian mã là hợp lý.
Đây là ngôn ngữ hàm, mọi giá trị đều bất biến (immutable), kể cả list. Điều này đảm bảo quá trình thực thi không sửa đổi trạng thái hiện có. Mỗi thao tác được xem như một chuỗi đầu vào trên ngăn xếp, xử lý và tạo ra đầu ra. Không gian cần thiết cho quá trình chạy gồm ngăn xếp xử lý và một heap chứa list tạm thời. Chúng tôi áp dụng nguyên tắc chỉ cấp phát mà không giải phóng, không cần GC phức tạp. Sau mỗi chu trình, cả heap và stack đều có thể reset tức thì. Việc loại bỏ GC giúp lược bỏ các metadata như bộ đếm tham chiếu hay dấu hiệu rác, dữ liệu trở nên gọn nhẹ hơn, thậm chí 64KB cũng đủ cho đa số tác vụ.
Giới hạn 64KB còn giúp các chỉ số nội bộ chỉ cần 2 byte, tối ưu hóa thêm không gian.
Trong phiên bản của mình, tôi kế thừa nhiều ý tưởng từ Aocla nhưng cũng có vài điểm khác biệt:
- Phân loại list: Chia thành hai dạng con. Thứ nhất là list bất biến được tiêm nạp từ parser, chuyển đổi từ string thành mảng ID 16bit. Ví dụ, số 42 hay các hằng boolean như true/false đều là các “word” tương ứng với hàm C xử lý ngăn xếp. Parser chỉ cần cắt các word và tổ chức thành list lồng nhau, các hằng số được lưu trong bảng hằng của khối mã.
- Dlist động: Có thể tạo runtime, chứa giá trị trực tiếp bên trong. Dlist có thể tham chiếu đến slist (list từ parser), nhưng ngược lại thì không. Hàm eval xử lý cả hai loại này.
Về việc không dùng GC, mỗi chương trình chạy xong sẽ reset heap/stack trong O(1). Tuy nhiên, tôi vẫn cho phép tạo word mới runtime như một tính năng tùy chọn. Với def, nếu là slist chỉ cần lưu index trong bảng từ điển. Nếu là dlist, để tránh bị xóa sau khi chạy xong, heap được thiết kế hai đầu hướng vào nhau: dlist runtime xếp từ đáy lên, sau khi xong sẽ reset; còn dlist được “đóng băng” qua def sẽ được sao chép sâu vào đỉnh heap để lưu trữ vĩnh viễn.
Biến cục bộ là điểm sáng của Aocla mà tôi kế thừa. Tôi mở rộng giới hạn lên 255 biến và dùng tên dài hơn thay vì chữ cái đơn. Trong VM, tên biến và tên word đều được lưu chuỗi gốc (16 byte đầu + hash 32bit) để debug. Hai chuỗi được xem là giống nhau nếu 15 byte đầu (kèm ký tự null) và hash khớp, một giới hạn hợp lý cho thực tiễn.
Tuple trong Aocla dùng để gán biến cục bộ, ví dụ 1 2 3 4 (x y z w)
tương đương x=1, y=2...
. Tôi giới hạn tuple tối đa 4 phần tử, mỗi tuple được mã hóa thành ID 32bit chứa các ID biến, phần thiếu sẽ dùng 255 lấp đầy.
Trong hai ngày rưỡi (thứ Năm và thứ Sáu tuần trước), tôi hoàn thành phần cốt lõi. Đến thứ Hai tuần này, tôi dành cả ngày sửa lỗi và hoàn thiện, tạo ra phiên bản thô sơ. Dù chưa hỗ trợ đầy đủ (chủ yếu là tạo list runtime), việc bổ sung không phức tạp vì không ảnh hưởng đến VM.
Đặc điểm nổi bật là cấu trúc dữ liệu đơn giản, không con trỏ hay mảng động. Toàn bộ chạy không cần cấp phát bộ nhớ runtime, khi hết bộ nhớ sẽ kết thúc tính toán một cách lịch sự.
Báo cáo lỗi của parser còn sơ sài, thiếu cả xử lý chú thích. Điều này không phải