- PostHog đã viết lại trình phân tích cú pháp SQL C++ dựa trên ANTLR qua nhiều phiên Claude Code, tạo ra trình phân tích cú pháp Rust 16K dòng, 5K dòng công cụ và hàng nghìn dòng kiểm thử, đạt mức tăng tốc khoảng 70 lần trên laptop
- Bản triển khai mới xoay quanh trình phân tích cú pháp đệ quy đi xuống có dự đoán và lõi biểu thức Pratt, chỉ áp dụng lookahead có giới hạn và backtracking suy đoán ở những nơi cần thiết, loại bỏ chi phí duyệt đồ thị đa dụng của ANTLR
- Giữ trình phân tích cú pháp C++ hiện có làm bản triển khai chuẩn, kết hợp kiểm thử dựa trên thuộc tính, truy vấn thực đã ẩn danh, kiểm thử hồi quy, sinh ca kiểm thử theo độ phủ mã và thu gọn bằng ShrinkRay để lặp lại việc loại bỏ các điểm không khớp giữa hai trình phân tích cú pháp
- Ở chế độ shadow trong production, kết quả của hàng triệu lần phân tích cú pháp không khác trình phân tích cú pháp cũ lần nào, nên lưu lượng đã được chuyển đổi chỉ sau vài giờ; ghi nhận mức tăng tốc trung bình 454 lần trong production
- Cách để trình sinh parser cung cấp ngữ pháp và bản triển khai chuẩn, còn LLM xác minh tính tương đương bằng fuzzing, cho thấy khả năng rút ngắn việc phát triển parser hiệu năng cao vốn cần chuyên môn và kéo dài nhiều tháng xuống chỉ còn vài ngày
Vì sao PostHog cần trình phân tích cú pháp SQL
- PostHog chuyển đổi SQL đầu vào thành ClickHouse SQL thô để người dùng có thể truy cập trực tiếp dữ liệu bằng SQL
- Cung cấp khung nhìn dữ liệu logic độc lập với cách bố trí vật lý của cơ sở dữ liệu
- Bảo vệ để các truy vấn hiện có không bị hỏng khi thay đổi tầng cơ sở dữ liệu
- Bổ sung tối ưu hóa hiệu năng và kiểm soát truy cập vào quá trình chuyển đổi
- Hầu hết công cụ của PostHog như phân tích sản phẩm, phát lại phiên, theo dõi lỗi cũng đưa các truy vấn viết bằng SQL qua cùng quy trình chuyển đổi
- Để chuyển đổi SQL, trước hết cần biến nó thành cây cú pháp trừu tượng (AST), rồi cây này lại được chuyển thành ClickHouse SQL
- Parser là thành phần đầu tiên xử lý đầu vào truy vấn không đáng tin cậy
- Toàn bộ kiểm soát truy cập và tối ưu hóa phía sau đều phụ thuộc vào cây do parser tạo ra
Cấu trúc và chi phí của parser ANTLR hiện có
- Trước thời AI coding, việc tự viết và duy trì parser rất khó, nên PostHog dùng trình sinh parser mã nguồn mở ANTLR
- Khi cung cấp ngữ pháp dưới dạng file .g4 khai báo, ANTLR sẽ sinh phần lớn mã parser
- Parser hiện có cũng đã được sinh bằng C++, vì vậy cải thiện hiệu năng lần này không đơn thuần là kết quả của việc đổi ngôn ngữ triển khai sang Rust
- ANTLR mạnh và linh hoạt, nhưng cần làm nhiều việc hơn khi xử lý từng token
- Nó biên dịch ngữ pháp thành mạng chuyển tiếp tăng cường (ATN), tức một dạng gần với ô-tô-mát hữu hạn không đơn định (NFA) có kèm stack
- Khi chạy, một interpreter đa dụng duyệt đồ thị này
- Thay vì các hàm viết trực tiếp như
parseExpression(), nó đi qua các tầng trừu tượng và lời gọi gián tiếp bổ sung
- Vì cũng hỗ trợ lookahead động tùy ý, khi có nhiều lựa chọn, nó mô phỏng đồng thời tất cả cách diễn giải cho đến khi chỉ còn một cách hợp lệ
- Ngay cả ANTLR được tối ưu đủ tốt cũng khó nhanh hơn parser đệ quy đi xuống viết trực tiếp do cấu trúc interpreter duyệt đồ thị
Thử nghiệm song song hai cách triển khai parser
- Dùng AI giúp việc viết và duy trì parser tự triển khai trở nên dễ hơn, nhưng chỉ yêu cầu Claude tạo một parser Rust không lỗi là chưa đủ
- Claude tạo ra nhiều lỗi
- Liên tục nghi ngờ chính khả năng viết lại
- Có xu hướng muốn kết thúc công việc sau mỗi vòng coding
- Trong nhiều phiên Claude Code chạy dài, hai cách tiếp cận được thử nghiệm song song
- Cách tiếp cận tập trung vào hiệu năng dùng parser đệ quy đi xuống và vòng lặp biểu thức Pratt, chỉ thêm lookahead và backtracking ở nơi cần thiết
- Cách tiếp cận tập trung vào khả năng thành công bám sát hành vi của ANTLR tối đa có thể, nhưng triển khai các chuyển tiếp bằng mã tường minh thay vì duyệt đồ thị đa dụng
- Cuối cùng hai cách tiếp cận hoạt động ở mức tương tự nhau, nhưng phải mất vài ngày mới xác nhận được điều đó
- Mục tiêu là khớp hoàn toàn với parser C++ hiện có trên mọi truy vấn thực tế, và cho kết quả gần nhất có thể trên cả các truy vấn nhân tạo
- Những SQL trông bất thường nhưng hợp lệ như
SELECT SELECT FROM FROM WHERE WHERE AND ANDcũng được đưa vào phạm vi kiểm thử
- Những SQL trông bất thường nhưng hợp lệ như
- Dùng parser C++ hiện có làm bản triển khai chuẩn (oracle), phát triển theo hướng kiểm thử: tìm SQL được xử lý khác nhau, sửa parser mới rồi so sánh lại
Sinh điểm không khớp bằng kiểm thử dựa trên thuộc tính
- Sau khi trước tiên vượt qua các kiểm thử hồi quy tích lũy trong quá trình phát triển parser cũ, nhóm bắt đầu sinh các điểm không khớp mới một cách có hệ thống
- Trong kiểm thử dựa trên thuộc tính (PBT) dùng Hypothesis, thuộc tính kiểm chứng được định nghĩa là “parser mới khớp với bản triển khai chuẩn”
- Giá trị đầu vào là truy vấn SQL
- Hypothesis tìm kiếm các truy vấn khiến kết quả của hai parser không khớp
- Để sinh SQL thú vị, họ cùng Claude tạo một công cụ sinh code cho bộ sinh SQL từ file ngữ pháp ANTLR
- Cũng viết một parser riêng để đọc chính file
.g4 - Sau đó đưa các biến đổi như hoán đổi token hoặc thêm ngoặc vào bước sinh
- Cũng viết một parser riêng để đọc chính file
Cấu trúc prompt để ngăn các bản sửa mong manh
- PBT tạo ra kiểm thử mới một cách ổn định, nhưng Claude lặp lại các bản sửa mong manh, chẳng hạn sửa một trường hợp cụ thể bằng lookahead một token rồi sau đó mới phát hiện cần lookahead hai token
- Cửa sổ ngữ cảnh thường đầy và bị nén, nên có khả năng Claude sửa trong trạng thái đã quên ngữ pháp thực tế hoặc hành vi của parser chuẩn
- Vấn đề này được giảm nhẹ bằng cách yêu cầu, ngay trước khi viết mã sửa từng điểm không khớp, phải nạp cả file ngữ pháp liên quan và mã nguồn C++ vào ngữ cảnh
Giữ CPU và Claude cùng chạy liên tục
- PBT tiếp tục chạy nền, ghi các ca thất bại vào file; các công cụ được cấu hình để Claude lấy các ca đó khi không có việc khác
- Các ca thất bại được thu thập từ nhiều nguồn
- Kiểm thử hồi quy hiện có
- SQL do PBT sinh ra
- Truy vấn đã ẩn danh lấy từ log truy vấn production
- Các ca được tạo bằng cách yêu cầu agent chạy nền “suy nghĩ thật sâu về các trường hợp biên”
- Hai parser phát triển song song dùng chung bộ kiểm thử hồi quy, nên lỗi được phát hiện trong một phiên sẽ lập tức được phản ánh sang phiên kia
- Hypothesis có thể thu gọn các ca do nó tự sinh thành ca tái hiện tối thiểu, nhưng không áp dụng được cho SQL bên ngoài, nên các ca đó dùng ShrinkRay
- Sau đó, nhóm cũng bổ sung sinh kiểm thử theo độ phủ mã
- Phát hiện các thành phần SQL chưa được chạy và thiên lệch quá trình sinh để các cấu trúc đó xuất hiện thường xuyên hơn
- Điều này không bắt buộc để đạt độ chính xác 100% trên tập truy vấn production, nhưng giúp tìm ra các trường hợp rất tinh vi
Vòng lặp tự động tìm và sửa lỗi
- Vòng lặp phát triển cuối cùng tự chủ lặp lại các bước sau
- Sinh lỗi mới bằng PBT, tập truy vấn thực, kiểm thử hồi quy và tìm kiếm trường hợp biên
- Thu gọn lỗi và thêm vào danh sách kiểm thử hồi quy ngày càng mở rộng
- Ưu tiên giải pháp tổng quát, đồng thời kiểm tra cách xử lý của ngữ pháp và bản triển khai chuẩn C++
- Sau khi sửa, xuất một đoạn tóm tắt một đoạn văn mà con người đọc được
- Chạy toàn bộ kiểm thử hồi quy để xác nhận mọi ca đều pass
- Tự động chạy lại cùng quy trình
- Vì parser mới nhanh hơn nhiều, nó có thể chạy trong production cùng parser C++ cũ ở chế độ shadow và báo cáo khác biệt kết quả
Xác minh và chuyển đổi trong production
- So sánh trước bằng log truy vấn production đã kiểm thử khoảng 50.000 truy vấn
- Ở chế độ shadow, hàng triệu lần phân tích cú pháp đã chạy nhanh chóng, và không xảy ra bất kỳ điểm không khớp nào với parser cũ
- Ban đầu dự định duy trì chế độ shadow trong vài ngày, nhưng kết quả đủ mạnh nên lưu lượng production đã được chuyển sang parser mới chỉ sau vài giờ
- Sau khi chuyển đổi, họ duy trì shadow ngược 0,1% để so sánh lại với parser cũ
- Parser mới không chỉ xuất AST mà còn xuất thông tin vị trí nguồn giống hệt parser C++ ANTLR
Nhanh hơn 70 lần trên laptop, 454 lần trong production
- Trong benchmark trên laptop, hiệu năng nhanh hơn parser cũ khoảng 70 lần
- Với truy vấn production, trung bình nhanh hơn 454 lần
- Trong production, hệ thống chủ yếu xử lý SQL dài hơn không trúng cache parser, nên chênh lệch hiệu năng lớn hơn so với trên laptop
- Kết quả cuối cùng gồm khoảng 16K dòng mã parser, khoảng 5K dòng công cụ và hàng nghìn dòng kiểm thử
- Trên các truy vấn thực tế, parser mới tương đương parser cũ; khác biệt chỉ giới hạn ở một số rất ít truy vấn được dựng nhân tạo
Cấu trúc parser cuối cùng và cách phát triển đã thay đổi
- Parser cuối cùng có cấu trúc như sau
- Phần lớn được triển khai bằng parser đệ quy đi xuống có dự đoán
- Xử lý biểu thức dùng lõi parser Pratt
- Con trỏ mặc định là LL(2), và chỉ tại các điểm cụ thể mới mở rộng phạm vi bằng tìm kiếm lookahead có giới hạn không tiêu thụ đầu vào
- Chỉ áp dụng backtracking suy đoán cục bộ dựa trên lựa chọn có thứ tự ở một số ít quyết định bắt buộc
- Toàn bộ parser được Claude Opus 4.7 viết bằng Rust vào tháng 5 năm 2026, và code không được viết trực tiếp bằng tay
- Cấu hình PBT sinh đầu vào từ ngữ pháp và dùng độ phủ mã để dẫn hướng việc sinh gần với phương pháp hiện đại của fuzzing parser, khác với “vibe coding” đơn thuần
- Công việc có thể mất nhiều tháng ngay cả với nhà phát triển có kiến thức chuyên biệt về parser đã được thực hiện trong vài ngày
- Trong tương lai, có thể sẽ phổ biến cách dùng trình sinh parser như ANTLR để cung cấp bản triển khai chuẩn, còn LLM viết parser triển khai trực tiếp nhanh hơn bằng cách khớp tính tương đương thông qua PBT và fuzzing
Chưa có bình luận nào.