Tôi sẽ cố gắng đưa nó vào điều khoản của giáo dân.
Nếu bạn nghĩ về cây phân tích cú pháp (không phải AST, mà là sự truy cập và mở rộng của trình phân tích cú pháp), thì đệ quy trái sẽ dẫn đến một cây mọc ngược và xuôi. Đệ quy phải hoàn toàn ngược lại.
Ví dụ, một ngữ pháp phổ biến trong trình biên dịch là một danh sách các mục. Hãy lấy một danh sách các chuỗi ("đỏ", "xanh", "xanh") và phân tích nó. Tôi có thể viết ngữ pháp một vài cách. Các ví dụ sau đây là đệ quy trực tiếp trái hoặc phải, tương ứng:
arg_list: arg_list:
STRING STRING
| arg_list ',' STRING | STRING ',' arg_list
Những cây cho những phân tích này:
(arg_list) (arg_list)
/ \ / \
(arg_list) BLUE RED (arg_list)
/ \ / \
(arg_list) GREEN GREEN (arg_list)
/ /
RED BLUE
Lưu ý làm thế nào nó phát triển theo hướng đệ quy.
Đây thực sự không phải là một vấn đề, bạn có thể muốn viết một ngữ pháp đệ quy trái ... nếu công cụ trình phân tích cú pháp của bạn có thể xử lý nó. Trình phân tích cú pháp từ dưới lên xử lý nó tốt. Vì vậy, có thể phân tích cú pháp LL hiện đại hơn. Vấn đề với các ngữ pháp đệ quy không phải là đệ quy, đó là đệ quy mà không tiến bộ trình phân tích cú pháp, hoặc, đệ quy mà không tiêu tốn mã thông báo. Nếu chúng tôi luôn tiêu thụ ít nhất 1 mã thông báo khi chúng tôi tái diễn, cuối cùng chúng tôi sẽ đạt đến cuối phân tích. Đệ quy trái được định nghĩa là đệ quy mà không tiêu thụ, đó là một vòng lặp vô hạn.
Hạn chế này hoàn toàn là một chi tiết triển khai thực hiện một ngữ pháp với trình phân tích cú pháp LL từ trên xuống ngây thơ (trình phân tích cú pháp gốc đệ quy). Nếu bạn muốn gắn bó với các ngữ pháp đệ quy trái, bạn có thể xử lý nó bằng cách viết lại sản xuất để tiêu thụ ít nhất 1 mã thông báo trước khi đệ quy, vì vậy điều này đảm bảo chúng tôi không bao giờ bị kẹt trong vòng lặp không sản xuất. Đối với bất kỳ quy tắc ngữ pháp nào là đệ quy trái, chúng ta có thể viết lại nó bằng cách thêm một quy tắc trung gian làm phẳng ngữ pháp thành một cấp độ nhìn, tiêu thụ mã thông báo giữa các sản phẩm đệ quy. (LƯU Ý: Tôi không nói đây là cách duy nhất hoặc cách ưa thích để viết lại ngữ pháp, chỉ nêu ra quy tắc khái quát. Trong ví dụ đơn giản này, tùy chọn tốt nhất là sử dụng hình thức đệ quy đúng). Vì cách tiếp cận này được khái quát hóa, một trình tạo phân tích cú pháp có thể thực hiện nó mà không liên quan đến lập trình viên (về mặt lý thuyết). Trong thực tế, tôi tin rằng ANTLR 4 bây giờ làm điều đó.
Đối với ngữ pháp ở trên, việc thực hiện LL hiển thị đệ quy bên trái sẽ trông như thế này. Trình phân tích cú pháp sẽ bắt đầu với việc dự đoán một danh sách ...
bool match_list()
{
if(lookahead-predicts-something-besides-comma) {
match_STRING();
} else if(lookahead-is-comma) {
match_list(); // left-recursion, infinite loop/stack overflow
match(',');
match_STRING();
} else {
throw new ParseException();
}
}
Trong thực tế, những gì chúng ta đang thực sự giải quyết là "thực hiện ngây thơ", tức là. ban đầu chúng tôi đưa ra một câu đã cho, sau đó gọi đệ quy hàm cho dự đoán đó và hàm đó gọi một cách ngây thơ một lần nữa.
Các trình phân tích cú pháp từ dưới lên không có vấn đề về các quy tắc đệ quy theo một trong hai hướng, bởi vì chúng không lặp lại phần đầu của câu, chúng hoạt động bằng cách đặt câu lại với nhau.
Đệ quy trong một ngữ pháp chỉ là một vấn đề nếu chúng ta sản xuất từ trên xuống, tức là. trình phân tích cú pháp của chúng tôi hoạt động bằng cách "mở rộng" dự đoán của chúng tôi khi chúng tôi tiêu thụ mã thông báo. Nếu thay vì mở rộng, chúng tôi thu gọn (sản phẩm bị "giảm"), như trong trình phân tích cú pháp từ dưới lên LALR (Yacc / Bison), thì đệ quy của một trong hai bên không phải là vấn đề.
::=
từExpression
sangTerm
và nếu bạn làm tương tự sau lần đầu tiên||
, nó sẽ không còn được đệ quy nữa? Nhưng nếu bạn chỉ làm điều đó sau đó::=
, nhưng không||
, nó vẫn sẽ được đệ quy?