Tại sao C ++ không thể được phân tích cú pháp bằng trình phân tích cú pháp LR (1)?


153

Tôi đã đọc về trình phân tích cú pháp và trình tạo trình phân tích cú pháp và tìm thấy tuyên bố này trong trang phân tích cú pháp LR của wikipedia:

Nhiều ngôn ngữ lập trình có thể được phân tích cú pháp bằng cách sử dụng một số biến thể của trình phân tích cú pháp LR. Một ngoại lệ đáng chú ý là C ++.

Tại sao nó như vậy? Tính chất đặc biệt nào của C ++ khiến nó không thể phân tích cú pháp với trình phân tích cú pháp LR?

Sử dụng google, tôi chỉ thấy rằng C có thể được phân tích cú pháp hoàn hảo với LR (1) nhưng C ++ yêu cầu LR ().


7
Cũng giống như: bạn cần hiểu đệ quy để học đệ quy ;-).
Toon Krijthe

5
"Bạn sẽ hiểu trình phân tích cú pháp một khi bạn sẽ phân tích cụm từ này."
ilya n.

Câu trả lời:


92

Có một chủ đề thú vị trên Lambda the Ultimate thảo luận về ngữ pháp LALR cho C ++ .

Nó bao gồm một liên kết đến một luận án tiến sĩ bao gồm một cuộc thảo luận về phân tích cú pháp C ++, trong đó nêu rõ:

"Ngữ pháp C ++ không rõ ràng, phụ thuộc vào ngữ cảnh và có khả năng đòi hỏi phải có cái nhìn vô hạn để giải quyết một số sự mơ hồ".

Nó tiếp tục đưa ra một số ví dụ (xem trang 147 của pdf).

Ví dụ là:

int(x), y, *const z;

Ý nghĩa

int x;
int y;
int *const z;

So với:

int(x), y, new int;

Ý nghĩa

(int(x)), (y), (new int));

(một biểu thức được phân tách bằng dấu phẩy).

Hai chuỗi mã thông báo có cùng một chuỗi con ban đầu nhưng các cây phân tích khác nhau, phụ thuộc vào phần tử cuối cùng. Có thể có nhiều mã thông báo tùy ý trước khi định dạng.


29
Thật tuyệt khi có một số tóm tắt về trang 147 trên trang này. Tôi sẽ đọc trang đó mặc dù. (+1)
Vui vẻ

11
Ví dụ là: int (x), y, * const z; // nghĩa: int x; int y; int * const z; (một chuỗi các khai báo) int (x), y, new int; // nghĩa: (int (x)), (y), (int mới)); (một biểu thức được phân tách bằng dấu phẩy) Hai chuỗi mã thông báo có cùng một chuỗi con ban đầu nhưng các cây phân tích khác nhau, phụ thuộc vào phần tử cuối cùng. Có thể có nhiều mã thông báo tùy ý trước khi định dạng.
Blaisorblade

6
Chà, trong bối cảnh đó, ∞ có nghĩa là "nhiều tùy ý" bởi vì giao diện sẽ luôn bị giới hạn bởi độ dài đầu vào.
MauganRa

1
Tôi khá bối rối trước những trích dẫn được trích từ Luận án Tiến sĩ. Nếu có một sự mơ hồ, thì theo định nghĩa, NO lookahead có thể "giải quyết" sự mơ hồ (nghĩa là quyết định phân tích cú pháp nào là đúng, vì ít nhất 2 phân tích được coi là đúng theo ngữ pháp). Hơn nữa, trích dẫn đề cập đến sự mơ hồ của C nhưng lời giải thích, không thể hiện sự mơ hồ, mà chỉ là một ví dụ không mơ hồ trong đó quyết định phân tích chỉ có thể được đưa ra sau một cái nhìn dài tùy tiện.
dodecaplex

231

Trình phân tích cú pháp LR không thể xử lý các quy tắc ngữ pháp mơ hồ, theo thiết kế. (Làm cho lý thuyết trở nên dễ dàng hơn vào những năm 1970 khi các ý tưởng đang được thực hiện).

Cả C và C ++ đều cho phép tuyên bố sau:

x * y ;

Nó có hai phân tích khác nhau:

  1. Nó có thể là khai báo của y, như con trỏ để gõ x
  2. Nó có thể là bội số của x và y, ném đi câu trả lời.

Bây giờ, bạn có thể nghĩ rằng cái sau là ngu ngốc và nên được bỏ qua. Hầu hết sẽ đồng ý với bạn; tuy nhiên, có những trường hợp nó có thể có tác dụng phụ (ví dụ: nếu bội số bị quá tải). Nhưng đó không phải là vấn đề. Vấn đề là ở đó hai phân tích khác nhau, và do đó một chương trình có thể có nghĩa là những thứ khác nhau tùy thuộc vào cách này nên đã được phân tích cú pháp.

Trình biên dịch phải chấp nhận thông tin phù hợp trong các trường hợp thích hợp và trong trường hợp không có bất kỳ thông tin nào khác (ví dụ: kiến ​​thức về loại x) phải thu thập cả hai để quyết định sau này phải làm gì. Do đó, một ngữ pháp phải cho phép điều này. Và điều đó làm cho ngữ pháp mơ hồ.

Do đó, phân tích cú pháp thuần túy không thể xử lý việc này. Cũng không thể có nhiều trình tạo trình phân tích cú pháp có sẵn rộng rãi khác, như Antlr, JavaCC, YACC hoặc Bison truyền thống, hoặc thậm chí các trình phân tích cú pháp kiểu PEG, được sử dụng theo cách "thuần túy".

Có rất nhiều trường hợp phức tạp hơn (cú pháp phân tích cú pháp yêu cầu tìm kiếm tùy ý, trong khi LALR (k) có thể nhìn về phía trước hầu hết các mã thông báo k), nhưng chỉ mất một ví dụ để bắn hạ phân tích thuần túy (hoặc các mẫu khác).

Hầu hết các trình phân tích cú pháp C / C ++ thực sự xử lý ví dụ này bằng cách sử dụng một số loại trình phân tích cú pháp xác định có thêm một hack: chúng đan xen phân tích cú pháp với bộ sưu tập bảng ký hiệu ... để đến lúc gặp "x", trình phân tích cú pháp biết nếu x là một loại hoặc không, và do đó có thể chọn giữa hai phân tích tiềm năng. Nhưng một trình phân tích cú pháp không thực hiện ngữ cảnh này và các trình phân tích cú pháp LR (các trình phân tích thuần túy, v.v.) không có ngữ cảnh (tốt nhất).

Người ta có thể gian lận và thêm các kiểm tra ngữ nghĩa theo thời gian giảm theo quy tắc trong các trình phân tích cú pháp LR để thực hiện định hướng này. (Mã này thường không đơn giản). Hầu hết các loại trình phân tích cú pháp khác có một số phương tiện để thêm kiểm tra ngữ nghĩa tại các điểm khác nhau trong phân tích cú pháp, có thể được sử dụng để làm điều này.

Và nếu bạn gian lận đủ, bạn có thể làm cho trình phân tích cú pháp LR hoạt động cho C và C ++. Các anh chàng GCC đã làm một lúc, nhưng đã từ bỏ việc phân tích cú pháp bằng tay, tôi nghĩ bởi vì họ muốn chẩn đoán lỗi tốt hơn.

Mặc dù vậy, có một cách tiếp cận khác, rất hay và sạch và phân tích cú pháp C và C ++ tốt mà không cần bất kỳ trình hack bảng biểu tượng nào: trình phân tích cú pháp GLR . Đây là các trình phân tích cú pháp miễn phí ngữ cảnh đầy đủ (có cái nhìn vô hạn hiệu quả). Các trình phân tích cú pháp GLR đơn giản chấp nhận cả hai phân tích cú pháp, tạo ra một "cây" (thực ra là một biểu đồ chu kỳ có hướng chủ yếu giống như cây) đại diện cho phân tích mơ hồ. Một vượt qua phân tích cú pháp có thể giải quyết sự mơ hồ.

Chúng tôi sử dụng kỹ thuật này trong giao diện C và C ++ cho Tookit Tái cấu trúc phần mềm DMS của chúng tôi (tính đến tháng 6 năm 2017, chúng xử lý đầy đủ C ++ 17 theo phương ngữ MS và GNU). Chúng đã được sử dụng để xử lý hàng triệu dòng hệ thống C và C ++ lớn, với các phân tích cú pháp chính xác, đầy đủ tạo ra AST với các chi tiết đầy đủ về mã nguồn. (Xem phân tích vexing nhất của AST cho C ++. )


11
Mặc dù ví dụ 'x * y' rất thú vị, điều tương tự có thể xảy ra trong C ('y' có thể là một typedef hoặc một biến). Nhưng C có thể được phân tích cú pháp bởi trình phân tích cú pháp LR (1), vậy sự khác biệt với C ++ là gì?
Martin Côte

12
Người trả lời của tôi đã quan sát thấy C có vấn đề tương tự, tôi nghĩ bạn đã bỏ lỡ điều đó. Không, nó không thể được phân tích cú pháp bởi LR (1), vì lý do tương tự. Er, ý bạn là 'y' có thể là một typedef? Có lẽ bạn có nghĩa là 'x'? Điều đó không thay đổi bất cứ điều gì.
Ira Baxter

6
Parse 2 không nhất thiết là ngu ngốc trong C ++, vì * có thể bị ghi đè để có tác dụng phụ.
Dour High Arch

8
Tôi nhìn x * yvà cười khúc khích - thật đáng kinh ngạc khi ít ai nghĩ về những điều mơ hồ nhỏ nhặt như thế này.
new123456

51
@altie Chắc chắn không ai sẽ quá tải toán tử bit-shift để làm cho nó ghi hầu hết các loại biến vào luồng, phải không?
Troy Daniels

16

Vấn đề không bao giờ được định nghĩa như thế này, trong khi nó sẽ rất thú vị:

bộ sửa đổi nhỏ nhất đối với ngữ pháp C ++ cần thiết là gì để ngữ pháp mới này có thể được phân tích cú pháp hoàn hảo bởi trình phân tích cú pháp yacc "không ngữ cảnh"? (chỉ sử dụng một 'hack': định dạng tên / định danh, trình phân tích cú pháp thông báo từ vựng của mỗi typedef / class / struct)

Tôi thấy một vài cái:

  1. Type Type;bị cấm. Một mã định danh được khai báo là một tên kiểu chữ không thể trở thành một mã định danh không phải là tên chữ (lưu ý struct Type Typekhông mơ hồ và vẫn có thể được cho phép).

    Có 3 loại names tokens:

    • types : kiểu dựng sẵn hoặc do typedef / class / struct
    • chức năng mẫu
    • định danh: hàm / phương thức và biến / đối tượng

    Việc xem xét các hàm mẫu như các mã thông báo khác nhau sẽ giải quyết sự func<mơ hồ. Nếu funclà tên hàm mẫu, thì <phải là đầu của danh sách tham số mẫu, nếu không funclà con trỏ hàm và <là toán tử so sánh.

  2. Type a(2);là một đối tượng khởi tạo. Type a();Type a(int)là các nguyên mẫu chức năng.

  3. int (k); là hoàn toàn bị cấm, nên được viết int k;

  4. typedef int func_type();typedef int (func_type)();bị cấm.

    Một hàm typedef phải là một con trỏ hàm typedef: typedef int (*func_ptr_type)();

  5. đệ quy mẫu được giới hạn ở 1024, nếu không, mức tối đa tăng có thể được chuyển qua dưới dạng tùy chọn cho trình biên dịch.

  6. int a,b,c[9],*d,(*f)(), (*g)()[9], h(char); cũng có thể bị cấm, thay thế bằng int a,b,c[9],*d; int (*f)();

    int (*g)()[9];

    int h(char);

    một dòng trên mỗi nguyên mẫu hàm hoặc khai báo con trỏ hàm.

    Một thay thế rất được ưa thích sẽ là thay đổi cú pháp con trỏ hàm khủng khiếp,

    int (MyClass::*MethodPtr)(char*);

    được nối lại như:

    int (MyClass::*)(char*) MethodPtr;

    điều này được kết hợp với các nhà điều hành diễn viên (int (MyClass::*)(char*))

  7. typedef int type, *type_ptr; cũng có thể bị cấm: một dòng trên mỗi typedef. Do đó, nó sẽ trở thành

    typedef int type;

    typedef int *type_ptr;

  8. sizeof int, sizeof char, sizeof long longVà đồng. có thể được khai báo trong mỗi tệp nguồn. Vì vậy, mỗi tệp nguồn sử dụng loại intnên bắt đầu bằng

    #type int : signed_integer(4)

    unsigned_integer(4)sẽ bị cấm bên ngoài #type chỉ thị đó, đây sẽ là một bước tiến lớn đến sizeof intsự mơ hồ ngu ngốc hiện diện trong rất nhiều tiêu đề C ++

Trình biên dịch triển khai C ++ được resyntaxed, nếu gặp một nguồn C ++ sử dụng cú pháp mơ hồ, di chuyển source.cppquá một ambiguous_syntaxthư mục và sẽ tự động tạo một bản dịch rõ ràng source.cpptrước khi biên dịch nó.

Vui lòng thêm cú pháp C ++ mơ hồ của bạn nếu bạn biết một số!


3
C ++ quá cố thủ. Không ai sẽ làm điều này trong thực tế. Những người (như chúng tôi) xây dựng mặt trước chỉ đơn giản là cắn viên đạn và thực hiện kỹ thuật để làm cho trình phân tích cú pháp hoạt động. Và, miễn là các mẫu tồn tại trong ngôn ngữ, bạn sẽ không nhận được trình phân tích cú pháp không ngữ cảnh thuần túy.
Ira Baxter

9

Như bạn có thể thấy trong câu trả lời của tôi ở đây , C ++ chứa cú pháp không thể được phân tích cú pháp bởi trình phân tích LL hoặc LR do giai đoạn phân giải kiểu (thường là phân tích cú pháp) thay đổi thứ tự các hoạt động và do đó hình dạng cơ bản của AST ( thường được dự kiến ​​sẽ được cung cấp bởi một phân tích giai đoạn đầu tiên).


3
Công nghệ phân tích cú pháp xử lý sự mơ hồ chỉ đơn giản là tạo ra cả hai biến thể AST khi chúng phân tích cú pháp và chỉ cần loại bỏ biến thể không chính xác tùy thuộc vào thông tin loại.
Ira Baxter

@Ira: Vâng, đúng vậy. Ưu điểm đặc biệt của nó là cho phép bạn duy trì sự phân tách của phân tích giai đoạn đầu tiên. Mặc dù nó được biết đến nhiều nhất trong trình phân tích cú pháp GLR, nhưng không có lý do cụ thể nào tôi thấy rằng bạn không thể nhấn C ++ bằng "GLL?" trình phân tích cú pháp là tốt.
Sam Harwell

"GLL"? Chà, chắc chắn, nhưng bạn sẽ phải tìm ra lý thuyết và viết ra một bài báo cho phần còn lại sử dụng. Nhiều khả năng, bạn có thể sử dụng trình phân tích cú pháp được mã hóa từ trên xuống hoặc trình phân tích cú pháp LALR () quay lại (nhưng vẫn giữ các phân tích cú pháp "bị từ chối") hoặc chạy trình phân tích cú pháp Earley. GLR có lợi thế là một giải pháp tốt chết tiệt, được ghi chép lại và đến bây giờ đã được chứng minh. Một công nghệ GLL sẽ phải có một số lợi thế khá quan trọng để hiển thị GLR.
Ira Baxter

Dự án Rascal (Hà Lan) tuyên bố họ đang xây dựng trình phân tích cú pháp GLL không quét. Làm việc trong tiến trình, có thể khó tìm thấy bất kỳ thông tin trực tuyến. vi.wikipedia.org/wiki/RascalMPL
Ira Baxter

@IraBaxter Dường như có những phát triển mới trên GLL: xem bài viết năm 2010 này về GLL dotat.at/tmp/gll.pdf
Sjoerd

6

Tôi nghĩ rằng bạn khá gần với câu trả lời.

LR (1) có nghĩa là phân tích cú pháp từ trái sang phải chỉ cần một mã thông báo để nhìn về phía trước cho bối cảnh, trong khi đó LR () có nghĩa là nhìn về phía trước vô hạn. Đó là, trình phân tích cú pháp sẽ phải biết tất cả mọi thứ đang đến để tìm ra vị trí hiện tại của nó.


4
Tôi nhớ lại từ lớp trình biên dịch của mình rằng LR (n) cho n> 0 có thể giảm về mặt toán học thành LR (1). Điều đó không đúng với n = vô cùng?
rmeador

14
Không, có một ngọn núi không thể vượt qua về sự khác biệt giữa n và vô cùng.
ephemient

4
Không phải là câu trả lời: Có, trong một khoảng thời gian vô hạn? :)
Steve Fallows

7
Trên thực tế, theo hồi ức mơ hồ của tôi về cách thức xảy ra LR (n) -> LR (1), nó liên quan đến việc tạo các trạng thái trung gian mới, vì vậy thời gian chạy là một số hàm không cố định của 'n'. Dịch LR (inf) -> LR (1) sẽ mất thời gian vô hạn.
Aaron

5
"Không phải là câu trả lời: Có, trong một khoảng thời gian vô hạn?" - Không: cụm từ 'được cung cấp một lượng thời gian vô hạn' chỉ là một cách nói ngắn gọn, không nhạy cảm "không thể được thực hiện trong bất kỳ khoảng thời gian hữu hạn nào". Khi bạn thấy "vô hạn", hãy nghĩ: "không phải là hữu hạn".
ChrisW

4

Vấn đề "typedef" trong C ++ có thể được phân tích cú pháp bằng trình phân tích cú pháp LALR (1) để xây dựng bảng ký hiệu trong khi phân tích cú pháp (không phải là trình phân tích cú pháp LALR thuần túy). Vấn đề "mẫu" có lẽ không thể giải quyết được bằng phương pháp này. Ưu điểm của loại trình phân tích cú pháp LALR (1) này là ngữ pháp (hiển thị bên dưới) là một ngữ pháp LALR (1) (không mơ hồ).

/* C Typedef Solution. */

/* Terminal Declarations. */

   <identifier> => lookup();  /* Symbol table lookup. */

/* Rules. */

   Goal        -> [Declaration]... <eof>               +> goal_

   Declaration -> Type... VarList ';'                  +> decl_
               -> typedef Type... TypeVarList ';'      +> typedecl_

   VarList     -> Var /','...     
   TypeVarList -> TypeVar /','...

   Var         -> [Ptr]... Identifier 
   TypeVar     -> [Ptr]... TypeIdentifier                               

   Identifier     -> <identifier>       +> identifier_(1)      
   TypeIdentifier -> <identifier>      =+> typedefidentifier_(1,{typedef})

// The above line will assign {typedef} to the <identifier>,  
// because {typedef} is the second argument of the action typeidentifier_(). 
// This handles the context-sensitive feature of the C++ language.

   Ptr          -> '*'                  +> ptr_

   Type         -> char                 +> type_(1)
                -> int                  +> type_(1)
                -> short                +> type_(1)
                -> unsigned             +> type_(1)
                -> {typedef}            +> type_(1)

/* End Of Grammar. */

Các đầu vào sau đây có thể được phân tích cú pháp mà không có vấn đề:

 typedef int x;
 x * y;

 typedef unsigned int uint, *uintptr;
 uint    a, b, c;
 uintptr p, q, r;

Trình tạo trình phân tích cú pháp LRSTAR đọc ký hiệu ngữ pháp ở trên và tạo một trình phân tích cú pháp xử lý vấn đề "typedef" mà không có sự mơ hồ trong cây phân tích hoặc AST. (Tiết lộ: Tôi là người đã tạo ra LRSTAR.)


Đó là cách hack tiêu chuẩn được GCC sử dụng với trình phân tích cú pháp LR trước đây để xử lý sự mơ hồ của những thứ như "x * y;" Than ôi, vẫn còn yêu cầu nhìn lớn tùy ý để phân tích các cấu trúc khác, do đó, LR (k) không phải là một giải pháp bất kỳ k cố định. (GCC chuyển sang gốc đệ quy với nhiều quảng cáo hơn).
Ira Baxter
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.