Tôi đã thực hiện rất nhiều nghiên cứu trong vài ngày qua, để hiểu rõ hơn tại sao những công nghệ riêng biệt này tồn tại, và điểm mạnh và điểm yếu của chúng là gì.
Một số câu trả lời đã tồn tại gợi ý về một số khác biệt của chúng, nhưng chúng không đưa ra bức tranh hoàn chỉnh, và dường như có phần ý kiến, đó là lý do tại sao câu trả lời này được viết.
Giải trình này dài, nhưng quan trọng. chịu đựng tôi (Hoặc nếu bạn không kiên nhẫn, hãy cuộn đến cuối để xem sơ đồ).
Để hiểu được sự khác biệt giữa Trình kết hợp phân tích cú pháp và Trình tạo phân tích cú pháp, trước tiên người ta cần hiểu sự khác biệt giữa các loại phân tích cú pháp tồn tại.
Phân tích cú pháp
Phân tích cú pháp là quá trình phân tích một chuỗi các ký hiệu theo một ngữ pháp chính thức. (Trong Khoa học máy tính), phân tích cú pháp được sử dụng để có thể cho phép máy tính hiểu văn bản được viết bằng ngôn ngữ, thường tạo ra một cây phân tích đại diện cho văn bản viết, lưu trữ ý nghĩa của các phần viết khác nhau trong mỗi nút của cây. Cây phân tích cú pháp này sau đó có thể được sử dụng cho nhiều mục đích khác nhau, chẳng hạn như dịch nó sang ngôn ngữ khác (được sử dụng trong nhiều trình biên dịch), diễn giải các hướng dẫn bằng văn bản theo cách nào đó (SQL, HTML), cho phép các công cụ như Linters
thực hiện công việc của họ , v.v ... Đôi khi, một cây phân tích không rõ ràngđược tạo, nhưng thay vào đó, hành động nên được thực hiện tại mỗi loại nút trong cây được thực thi trực tiếp. Điều này làm tăng hiệu quả, nhưng dưới nước vẫn tồn tại một cây phân tích ngầm.
Phân tích cú pháp là một vấn đề khó tính toán. Đã có hơn năm mươi năm nghiên cứu về chủ đề này, nhưng vẫn còn nhiều điều để tìm hiểu.
Nói một cách đơn giản, có bốn thuật toán chung để cho phép máy tính phân tích cú pháp đầu vào:
- LL phân tích cú pháp. (Không phân tích ngữ cảnh, phân tích từ trên xuống.)
- Phân tích cú pháp LR. (Không phân tích ngữ cảnh, phân tích từ dưới lên.)
- PEG + Phân tích cú pháp Packrat.
- Phân tích Earley.
Lưu ý rằng các loại phân tích cú pháp là rất chung chung, mô tả lý thuyết. Có nhiều cách để thực hiện từng thuật toán này trên các máy vật lý, với sự đánh đổi khác nhau.
LL và LR chỉ có thể xem các ngữ pháp không ngữ cảnh (nghĩa là bối cảnh xung quanh các mã thông báo được viết không quan trọng để hiểu cách chúng được sử dụng).
Phân tích cú pháp PEG / Packrat và phân tích Earley được sử dụng ít hơn nhiều: Phân tích Earley rất hay ở chỗ nó có thể xử lý nhiều ngữ pháp hơn (bao gồm cả những ngữ pháp không nhất thiết phải là Ngữ cảnh) nhưng nó kém hiệu quả hơn (như được tuyên bố bởi rồng cuốn sách (phần 4.1.1); Tôi không chắc những tuyên bố này có còn chính xác không).
Phân tích cú pháp ngữ pháp biểu thức + phân tích cú pháp là một phương pháp tương đối hiệu quả và cũng có thể xử lý nhiều ngữ pháp hơn cả LL và LR, nhưng ẩn đi sự mơ hồ, như sẽ nhanh chóng được chạm vào bên dưới.
LL (Đạo hàm từ trái sang phải, ngoài cùng bên trái)
Đây có thể là cách tự nhiên nhất để suy nghĩ về phân tích cú pháp. Ý tưởng là xem xét mã thông báo tiếp theo trong chuỗi đầu vào và sau đó quyết định một trong số nhiều cuộc gọi đệ quy có thể được thực hiện để tạo cấu trúc cây.
Cây này được xây dựng 'từ trên xuống', nghĩa là chúng ta bắt đầu từ gốc của cây và đi theo các quy tắc ngữ pháp giống như cách chúng ta đi qua chuỗi đầu vào. Nó cũng có thể được xem như là xây dựng một 'postfix' tương đương với luồng mã thông báo 'infix' đang được đọc.
Các trình phân tích cú pháp thực hiện phân tích cú pháp kiểu LL có thể được viết để trông rất giống với ngữ pháp gốc đã được chỉ định. Điều này làm cho nó tương đối dễ hiểu, gỡ lỗi và nâng cao chúng. Bộ kết hợp bộ phân tích cú pháp cổ điển không gì khác hơn là 'các mảnh lego' có thể được ghép lại để xây dựng bộ phân tích cú pháp kiểu LL.
LR (Đạo hàm từ trái sang phải, từ phải sang phải)
Phân tích cú pháp LR đi theo cách khác, từ dưới lên: Ở mỗi bước, (các) phần tử trên cùng của ngăn xếp được so sánh với danh sách ngữ pháp, để xem liệu chúng có thể được giảm xuống
theo quy tắc cấp cao hơn trong ngữ pháp hay không. Nếu không, mã thông báo tiếp theo từ luồng đầu vào là shift ed và được đặt trên đỉnh của ngăn xếp.
Một chương trình là chính xác nếu cuối cùng chúng ta kết thúc với một nút duy nhất trên ngăn xếp đại diện cho quy tắc bắt đầu từ ngữ pháp của chúng ta.
Nhìn thẳng
Trong một trong hai hệ thống này, đôi khi cần phải xem lén nhiều mã thông báo hơn từ đầu vào trước khi có thể quyết định lựa chọn nào để thực hiện. Đây là (0)
, (1)
, (k)
hoặc (*)
-syntax bạn thấy sau khi tên của hai thuật toán này nói chung, chẳng hạn như LR(1)
hay LL(k)
. k
thường là viết tắt của 'nhiều như nhu cầu ngữ pháp của bạn', trong khi *
thường là viết tắt của 'trình phân tích cú pháp này thực hiện quay lui', mạnh hơn / dễ thực hiện, nhưng có bộ nhớ và sử dụng thời gian cao hơn nhiều so với trình phân tích cú pháp có thể tiếp tục phân tích cú pháp tuyến tính.
Lưu ý rằng các trình phân tích cú pháp kiểu LR đã có nhiều mã thông báo trên ngăn xếp khi chúng có thể quyết định 'nhìn về phía trước', vì vậy chúng đã có thêm thông tin để gửi đi. Điều này có nghĩa là họ thường cần ít 'cái nhìn' hơn so với trình phân tích cú pháp kiểu LL cho cùng một ngữ pháp.
LL so với LR: Sự mơ hồ
Khi đọc hai mô tả ở trên, người ta có thể tự hỏi tại sao phân tích cú pháp kiểu LR tồn tại, vì phân tích cú pháp kiểu LL có vẻ tự nhiên hơn rất nhiều.
Tuy nhiên, phân tích cú pháp kiểu LL có một vấn đề: đệ quy trái .
Rất tự nhiên để viết một ngữ pháp như:
expr ::= expr '+' expr | term
term ::= integer | float
Nhưng, trình phân tích cú pháp kiểu LL sẽ bị kẹt trong một vòng lặp đệ quy vô hạn khi phân tích ngữ pháp này: Khi thử khả năng bên trái của expr
quy tắc, nó sẽ lặp lại quy tắc này mà không cần sử dụng bất kỳ đầu vào nào.
Có nhiều cách để giải quyết vấn đề này. Đơn giản nhất là viết lại ngữ pháp của bạn để loại đệ quy này không xảy ra nữa:
expr ::= term expr_rest
expr_rest ::= '+' expr | ϵ
term ::= integer | float
(Ở đây, ϵ là viết tắt của 'chuỗi rỗng')
Ngữ pháp này bây giờ là đệ quy đúng. Lưu ý rằng nó ngay lập tức khó đọc hơn rất nhiều.
Trong thực tế, đệ quy trái có thể xảy ra gián tiếp với nhiều bước khác ở giữa. Điều này làm cho nó một vấn đề khó để tìm ra. Nhưng cố gắng giải quyết nó làm cho ngữ pháp của bạn khó đọc hơn.
Như Mục 2.5 của Sách Rồng nêu rõ:
Chúng ta dường như có một mâu thuẫn: một mặt chúng ta cần một ngữ pháp tạo điều kiện cho việc dịch thuật, mặt khác chúng ta cần một ngữ pháp khác biệt đáng kể tạo điều kiện cho việc phân tích cú pháp. Giải pháp là bắt đầu với ngữ pháp để dịch dễ dàng và cẩn thận chuyển đổi nó để tạo điều kiện phân tích cú pháp. Bằng cách loại bỏ đệ quy trái, chúng ta có thể có được một ngữ pháp phù hợp để sử dụng trong một dịch giả đệ quy gốc.
Các trình phân tích cú pháp kiểu LR không gặp vấn đề về đệ quy trái này, vì chúng xây dựng cây từ dưới lên.
Tuy nhiên , bản dịch tinh thần của một ngữ pháp như trên sang trình phân tích cú pháp kiểu LR (thường được triển khai dưới dạng Tự động hóa trạng thái hữu hạn )
rất khó (và dễ bị lỗi), vì thường có hàng trăm hoặc hàng nghìn trạng thái + chuyển trạng thái để xem xét. Đây là lý do tại sao các trình phân tích cú pháp kiểu LR thường được tạo bởi Trình tạo phân tích cú pháp, còn được gọi là 'trình biên dịch trình biên dịch'.
Cách giải quyết sự mơ hồ
Chúng tôi đã thấy hai phương pháp để giải quyết các mơ hồ đệ quy bên trái ở trên: 1) viết lại cú pháp 2) sử dụng trình phân tích cú pháp LR.
Nhưng có nhiều loại mơ hồ khác khó giải quyết hơn: Điều gì xảy ra nếu hai quy tắc khác nhau được áp dụng như nhau cùng một lúc?
Một số ví dụ phổ biến là:
Cả hai trình phân tích cú pháp kiểu LL và kiểu LR đều có vấn đề với những điều này. Các vấn đề với phân tích biểu thức số học có thể được giải quyết bằng cách đưa ra ưu tiên toán tử. Theo cách tương tự, các vấn đề khác như Dangling Else có thể được giải quyết, bằng cách chọn một hành vi ưu tiên và gắn bó với nó. (Ví dụ, trong C / C ++, sự lơ lửng khác luôn thuộc về 'nếu' gần nhất).
Một "giải pháp" khác cho vấn đề này là sử dụng Ngữ pháp phân tích cú pháp (PEG): Điều này tương tự với ngữ pháp BNF được sử dụng ở trên, nhưng trong trường hợp mơ hồ, luôn luôn 'chọn đầu tiên'. Tất nhiên, điều này không thực sự 'giải quyết' vấn đề, mà chỉ che giấu rằng sự mơ hồ thực sự tồn tại: Người dùng cuối có thể không biết lựa chọn nào mà trình phân tích cú pháp đưa ra và điều này có thể dẫn đến kết quả không mong muốn.
Nhiều thông tin sâu hơn rất nhiều so với bài đăng này, bao gồm cả lý do tại sao nói chung không thể biết liệu ngữ pháp của bạn không có bất kỳ sự mơ hồ nào và ý nghĩa của bài viết này là bài viết tuyệt vời của LL và LR trong bối cảnh: Tại sao phân tích cú pháp công cụ rất khó . Tôi rất có thể giới thiệu nó; nó giúp tôi rất nhiều để hiểu tất cả những điều tôi đang nói bây giờ.
50 năm nghiên cứu
Nhưng cuộc sống vẫn tiếp diễn. Nó chỉ ra rằng các trình phân tích cú pháp kiểu 'bình thường' được triển khai như các máy tự động trạng thái hữu hạn thường cần hàng ngàn trạng thái + chuyển tiếp, đó là một vấn đề trong quy mô chương trình. Vì vậy, các biến thể như Simple LR (SLR) và LALR (Nhìn về phía trước) đã được viết kết hợp các kỹ thuật khác để làm cho máy tự động nhỏ hơn, giảm dung lượng đĩa và bộ nhớ của các chương trình phân tích cú pháp.
Ngoài ra, một cách khác để giải quyết các vấn đề mơ hồ được liệt kê ở trên là sử dụng các kỹ thuật tổng quát , trong trường hợp mơ hồ, cả hai khả năng đều được giữ và phân tích cú pháp: Một trong hai khả năng đó có thể không phân tích được dòng (trong trường hợp đó khả năng khác là 'đúng' một), cũng như trả lại cả hai (và theo cách này cho thấy sự mơ hồ tồn tại) trong trường hợp cả hai đều đúng.
Thật thú vị, sau khi thuật toán Generalized LR được mô tả, hóa ra một cách tiếp cận tương tự có thể được sử dụng để triển khai các trình phân tích LL tổng quát hóa , có độ phức tạp tương tự nhanh ($ O (n ^ 3) $ đối với các ngữ pháp mơ hồ, $ O (n) $ cho các ngữ pháp hoàn toàn không rõ ràng, mặc dù có nhiều sổ sách hơn so với trình phân tích cú pháp (LA) đơn giản, có nghĩa là hệ số không đổi cao hơn) nhưng một lần nữa cho phép trình phân tích cú pháp được viết theo kiểu đệ quy (từ trên xuống) tự nhiên hơn rất nhiều để viết và gỡ lỗi.
Trình kết hợp phân tích cú pháp, Trình tạo phân tích cú pháp
Vì vậy, với giải trình dài này, chúng tôi hiện đang đi đến cốt lõi của câu hỏi:
Sự khác biệt của Trình kết hợp phân tích cú pháp và Trình tạo phân tích cú pháp và khi nào nên sử dụng cái này so với cái khác?
Chúng thực sự là những loại quái vật khác nhau:
Bộ kết hợp phân tích cú pháp được tạo bởi vì mọi người đang viết trình phân tích cú pháp từ trên xuống và nhận ra rằng nhiều trong số này có nhiều điểm chung .
Trình tạo trình phân tích cú pháp được tạo vì mọi người đang tìm cách xây dựng trình phân tích cú pháp không có vấn đề mà trình phân tích cú pháp kiểu LL gặp phải (tức là trình phân tích cú pháp kiểu LR), điều này rất khó thực hiện bằng tay. Những cái phổ biến bao gồm Yacc / Bison, thực hiện (LA) LR).
Thật thú vị, ngày nay cảnh quan bị vấy bẩn phần nào:
Có thể viết Bộ kết hợp phân tích cú pháp hoạt động với thuật toán GLL , giải quyết sự mơ hồ - các vấn đề mà trình phân tích cú pháp kiểu LL cổ điển gặp phải, trong khi có thể đọc / hiểu được như tất cả các loại phân tích từ trên xuống.
Trình tạo phân tích cú pháp cũng có thể được viết cho trình phân tích cú pháp kiểu LL. ANTLR thực hiện chính xác điều đó và sử dụng các phương pháp phỏng đoán khác (Thích nghi LL (*)) để giải quyết sự mơ hồ mà các trình phân tích cú pháp kiểu LL cổ điển có.
Nói chung, việc tạo một trình tạo trình phân tích cú pháp LR và gỡ lỗi đầu ra của trình tạo trình phân tích cú pháp kiểu LR (LA) chạy trên ngữ pháp của bạn là khó khăn, do việc dịch ngữ pháp gốc của bạn sang dạng LR 'từ trong ra ngoài'. Mặt khác, các công cụ như Yacc / Bison đã có nhiều năm kinh optimisations, và nhìn thấy rất nhiều công dụng trong tự nhiên, có nghĩa là nhiều người bây giờ xem xét nó như là các cách để làm phân tích và hoài nghi đối với cách tiếp cận mới.
Bạn nên sử dụng cái nào, tùy thuộc vào mức độ khó của ngữ pháp của bạn và tốc độ của trình phân tích cú pháp. Tùy thuộc vào ngữ pháp, một trong những kỹ thuật này (/ triển khai các kỹ thuật khác nhau) có thể nhanh hơn, có dung lượng bộ nhớ nhỏ hơn, dấu chân đĩa nhỏ hơn hoặc dễ mở rộng hơn hoặc dễ gỡ lỗi hơn các kỹ thuật khác. Mileage của bạn có thể khác nhau .
Lưu ý bên lề: Về chủ đề Phân tích từ điển.
Phân tích từ điển có thể được sử dụng cho cả Trình kết hợp phân tích cú pháp và Trình tạo phân tích cú pháp. Ý tưởng là để có một trình phân tích cú pháp 'câm' rất dễ thực hiện (và do đó nhanh) thực hiện lần đầu tiên vượt qua mã nguồn của bạn, loại bỏ ví dụ lặp lại khoảng trắng, nhận xét, v.v. và có thể 'tokenizing' trong một cách thô các yếu tố khác nhau tạo nên ngôn ngữ của bạn.
Ưu điểm chính là bước đầu tiên này làm cho trình phân tích cú pháp thực sự đơn giản hơn rất nhiều (và vì điều đó có thể nhanh hơn). Nhược điểm chính là bạn có một bước dịch riêng biệt, và ví dụ báo cáo lỗi với số dòng và số cột trở nên khó khăn hơn do loại bỏ khoảng trắng.
Một lexer cuối cùng là 'chỉ' một trình phân tích cú pháp khác và có thể được thực hiện bằng bất kỳ kỹ thuật nào ở trên. Do tính đơn giản của nó, thường các kỹ thuật khác được sử dụng hơn so với trình phân tích cú pháp chính và ví dụ như các trình tạo 'lexer' tồn tại thêm.
Tl; Tiến sĩ:
Đây là một sơ đồ áp dụng cho hầu hết các trường hợp:
javac
, Scala). Nó cung cấp cho bạn quyền kiểm soát nhiều nhất đối với trạng thái trình phân tích cú pháp nội bộ, giúp tạo ra các thông báo lỗi tốt (trong những năm gần đây