Trải nghiệm lập trình R được nâng cấp đáng kể nhờ Tree-sitter
Bài viết khám phá cách Tree-sitter, một trình tạo bộ phân tích cú pháp, đang cải thiện hệ sinh thái công cụ cho ngôn ngữ lập trình R. Từ tính năng hỗ trợ mã nguồn trong IDE Positron cho đến các công cụ dòng lệnh nhanh chóng như Air và Jarl, việc tích hợp ngữ pháp R vào Tree-sitter mang lại lợi ích to lớn cho các nhà phát triển.

Cách đây gần hai năm, dựa trên nền tảng công trình của Jim Hester và Kevin Ushey, Davis Vaughan đã hoàn thành một tệp JavaScript có tác động rất lớn đến cộng đồng R: một ngữ pháp (grammar) R cho trình tạo bộ phân tích cú pháp Tree-sitter. Động thái này thậm chí đã nhận được sự hoan nghênh nhiệt liệt trong một bài thuyết trình tại hội nghị useR! 2024.
Vậy, khán giả đã vỗ tay cho một tệp JavaScript chứa các quy tắc ngữ pháp sao? Không, sự hào hứng đến từ trải nghiệm phát triển (developer experience) được cải thiện cho ngôn ngữ R mà tệp này mang lại. Trong bài viết này, chúng ta sẽ tìm hiểu Tree-sitter là gì và cách các công cụ được xây dựng dựa trên nó có thể lợi ích cho quy trình phát triển R của bạn.
Tree-sitter là gì?
Tree-sitter là một trình tạo bộ phân tích cú pháp (parser generator) được viết bằng ngôn ngữ C, với các ràng buộc (bindings) tồn tại trong một số ngôn ngữ bao gồm cả Rust và R.
Cơ chế hoạt động của Tree-sitter
Để hiểu rõ hơn, hãy quay lại một chút. Phân tích cú pháp (parsing) có ý nghĩa như thế nào? Khi bạn đọc mã R, làm thế nào bạn biết rằng mean là tên một hàm, na.rm là tên một đối số, hay TRUE là một giá trị logic? Bạn phải phân tích đoạn mã đó thành cái được gọi là cây phân tích cú pháp (parse tree) trong đầu mình.
R bản thân nó có thể phân tích mã R nhờ vào ngữ pháp của nó. Tuy nhiên, Tree-sitter thực hiện việc phân tích mã này nhanh hơn, đặc biệt là nhờ hỗ trợ phân tích gia tăng (incremental parsing). Đây là yếu tố then chốt để cập nhật cây cú pháp ngay khi bạn đang gõ mã trong trình soạn thảo. Tree-sitter có khả năng phân tích bất kỳ mã nào miễn là có ngữ pháp cho nó, và nó đã được sử dụng cho nhiều ngôn ngữ lập trình khác nhau.
Để Tree-sitter "học" một ngôn ngữ mới, bạn cần cung cấp cho nó một tệp chứa định nghĩa cú pháp của ngôn ngữ đó. Đây chính là nơi tệp JavaScript của Davis Vaughan và cộng sự phát huy tác dụng. Kho lưu trữ treesitter-r, cung cấp bản dịch ngữ pháp R sang định dạng mà Tree-sitter mong đợi, là nền tảng của tất cả các công cụ được trình bày trong bài viết này.
Ứng dụng thực tế trong hệ sinh thái R
Lý do thực sự khiến khán giả vỗ tay dành tặng Davis Vaughan là ông đã giải thích cách ngữ pháp R cho Tree-sitter đã được triển khai lên GitHub. Nhờ đó, trải nghiệm duyệt mã R trên GitHub trở nên tốt đẹp tương tự như duyệt mã JavaScript hay các ngôn ngữ khác. Ví dụ, nếu bạn tìm kiếm tên một hàm trong kho lưu trữ, định nghĩa của hàm đó sẽ được chỉ ra trong kết quả tìm kiếm.
Một ứng dụng rất hữu ích khác là việc sử dụng Tree-sitter bởi Ark, nhân R (R kernel) được sử dụng trong IDE Positron. Ark cung cấp tính năng tự động hoàn thiện và trợ giúp khi di chuột (hover help) trong Positron, giúp các nhà phát triển làm việc hiệu quả hơn. Các môi trường phát triển khác như Emacs cũng có hỗ trợ cho Tree-sitter.
Các công cụ phân tích và tìm kiếm mã
Bạn có thể phân tích và tìm kiếm mã R bằng gói R {treesitter} và cú pháp truy vấn của Tree-sitter. Gói này là một phần phụ thuộc của gói {gander} do Simon Couch phát triển, nhằm mang lại trải nghiệm tốt hơn khi làm việc với các Mô hình ngôn ngữ lớn (LLMs) khi viết mã R.
Một trường hợp sử dụng khác là phần mở rộng {igraph.r2cdocs} cho gói {igraph}, nó phân tích toàn bộ mã R của igraph để xác định xem các hàm đã xuất có gọi gián tiếp hàm nào có tên kết thúc bằng _impl hay không, từ đó liên kết tài liệu từ phần hướng dẫn sử dụng của R.
Gói {pkgdepends} gọi Tree-sitter (thông qua C) để phát hiện các phụ thuộc trong các tệp. Ngoài ra, công cụ ast-grep được xây dựng trên Tree-sitter để tìm kiếm và viết lại mã với cú pháp truy vấn rõ ràng hơn, giúp chúng ta không cần phải viết các biểu thức chính quy (regex) mong manh và dễ sai sót.
Meme về sự phức tạp của Regex
Các công cụ dòng lệnh (CLI) hiệu năng cao
Một số công cụ dòng lệnh hiện đại được xây dựng trên nền tảng Rust và Tree-sitter đang mang lại tốc độ xử lý ấn tượng:
- Air: Được phát triển bởi Davis Vaughan và Lionel Henry, Air là một công cụ CLI viết bằng Rust dựa trên Tree-sitter. Nó có khả năng định dạng mã (formatting) nhanh chóng như chớp mắt.
- Jarl: Phát triển bởi Etienne Bacher, Jarl là một CLI xây dựng trên Air (do đó cũng dựa trên Tree-sitter) bằng Rust. Nó giúp rà soát mã (linting) và sửa lỗi, cũng cực kỳ nhanh chóng. Công cụ này thậm chí có thể phát hiện mã không thể tiếp cận được, các hàm không được sử dụng và định nghĩa hàm trùng lặp.
Việc tạo ra các công cụ CLI bao gồm các ràng buộc Rust hiệu quả hơn nhiều so với việc tạo ra các gói R bao gồm gói {treesitter}, vì Rust là ngôn ngữ cấp thấp hơn với ít chi phí hơn và mã này có thể dễ dàng song song hóa.
Các công cụ thú vị khác
Công cụ difftastic của Wilfred Hughes là "một công cụ diff cấu trúc hiểu cú pháp". Điều này có nghĩa là difftastic không chỉ so sánh các dòng hay "từ" mà so sánh cú pháp thực tế bằng cách nhìn vào các dòng xung quanh dòng đã thay đổi. Đặc biệt, nó hiểu R ngay lập tức mà không cần cấu hình phức tạp.
Ngoài ra, gói {muttest} của Jakub Sobolewski là một công cụ dùng cho kiểm thử đột biến (mutation testing), phụ thuộc vào gói {treesitter}. Trong loại kiểm thử này, mã của bạn sẽ bị biến đổi ngẫu nhiên (ví dụ: đổi + thành -) và chạy thử nghiệm để xem liệu các bài kiểm tra có bắt được lỗi đột biến hay không.
Kết luận
Trong bài viết này, chúng tôi đã trình bày tổng quan về các công cụ dựa trên Tree-sitter cho ngôn ngữ R. Hệ sinh thái các công cụ này đang được phát triển rất tích cực, vì vậy một số công cụ có thể sẽ xuất hiện và biến mất. Tuy nhiên, ý tưởng cắm ngữ pháp R vào một trình tạo bộ phân tích cú pháp tổng quát để mang lại các tính năng hữu ích cho các nhà phát triển R sẽ luôn đúng. Có lẽ trong tương lai, bạn sẽ đóng góp vào hệ sinh thái này, thông qua một công cụ hiện có hoặc bằng cách tạo ra một công cụ mới?



