Quy trình được tuân theo khi viết một từ vựng dựa trên ngữ pháp là gì?


12

Trong khi đọc qua một câu trả lời cho câu hỏi Làm rõ về Ngữ pháp , Trình phân tích cú pháp và Trình phân tích cú pháp , câu trả lời đã nêu rằng:

[...] Một ngữ pháp BNF chứa tất cả các quy tắc bạn cần để phân tích và phân tích từ vựng.

Điều này xuất hiện hơi kỳ lạ đối với tôi bởi vì cho đến tận bây giờ, tôi luôn nghĩ rằng một từ vựng hoàn toàn không dựa trên ngữ pháp, trong khi một trình phân tích cú pháp dựa nhiều vào một. Tôi đã đi đến kết luận này sau khi đọc rất nhiều bài đăng trên blog về việc viết lexers, và không ai từng sử dụng 1 EBNF / BNF làm cơ sở cho thiết kế.

Nếu các từ vựng, cũng như các trình phân tích cú pháp, dựa trên ngữ pháp EBNF / BNF, thì người ta sẽ tạo ra một từ vựng bằng cách sử dụng phương pháp đó như thế nào? Đó là, làm thế nào tôi có thể xây dựng một từ vựng bằng cách sử dụng một ngữ pháp EBNF / BNF nhất định?

Tôi đã thấy nhiều, rất nhiều bài viết liên quan đến việc viết một trình phân tích cú pháp bằng cách sử dụng EBNF / BNF làm hướng dẫn hoặc bản kế hoạch chi tiết, nhưng tôi đã không tìm thấy cho đến nay cho thấy tương đương với thiết kế lexer.

Ví dụ: lấy ngữ pháp sau:

input = digit| string ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"', { all characters - '"' }, '"' ;
all characters = ? all visible characters ? ;

Làm thế nào một người sẽ tạo ra một từ vựng dựa trên ngữ pháp? Tôi có thể tưởng tượng làm thế nào một trình phân tích cú pháp có thể được viết từ một ngữ pháp như vậy, nhưng tôi không nắm bắt được khái niệm làm tương tự với một từ vựng.

Có các quy tắc hoặc logic nhất định được sử dụng để thực hiện một nhiệm vụ như thế này, như với việc viết một trình phân tích cú pháp? Thành thật mà nói, tôi bắt đầu tự hỏi liệu các thiết kế lexer có sử dụng ngữ pháp EBNF / BNF hay không, chứ đừng nói là dựa trên một.


1 dạng Nus Backus mở rộngdạng Nus Backus

Câu trả lời:


17

Trình phân tích cú pháp chỉ là các trình phân tích cú pháp đơn giản được sử dụng làm tối ưu hóa hiệu suất cho trình phân tích cú pháp chính. Nếu chúng ta có một từ vựng, lexer và trình phân tích cú pháp phối hợp với nhau để mô tả ngôn ngữ hoàn chỉnh. Các trình phân tích cú pháp không có giai đoạn từ vựng riêng biệt đôi khi được gọi là Máy quét không quét.

Nếu không có từ vựng, trình phân tích cú pháp sẽ phải hoạt động trên cơ sở từng ký tự. Vì trình phân tích cú pháp phải lưu trữ siêu dữ liệu về mọi mục đầu vào và có thể phải tính toán trước các bảng cho mọi trạng thái mục đầu vào, điều này sẽ dẫn đến mức tiêu thụ bộ nhớ không thể chấp nhận cho các kích thước đầu vào lớn. Cụ thể, chúng ta không cần một nút riêng cho mỗi ký tự trong cây cú pháp trừu tượng.

Vì văn bản trên cơ sở từng ký tự là khá mơ hồ, điều này cũng sẽ dẫn đến sự mơ hồ hơn nhiều gây khó chịu khi xử lý. Hãy tưởng tượng một quy tắc R → identifier | "for " identifier. trong đó định danh được tạo thành từ các chữ cái ASCII. Nếu tôi muốn tránh sự mơ hồ, bây giờ tôi cần một cái nhìn gồm 4 ký tự để xác định nên chọn phương án nào. Với một lexer, trình phân tích cú pháp chỉ cần kiểm tra xem nó có mã thông báo IDENTIFIER hoặc FOR - một giao diện 1 mã thông báo hay không.

Ngữ pháp hai cấp độ.

Lexers hoạt động bằng cách dịch bảng chữ cái đầu vào sang một bảng chữ cái thuận tiện hơn.

Trình phân tích cú pháp không quét mô tả một ngữ pháp (N,, P, S) trong đó các đầu cuối N là phía bên trái của các quy tắc trong ngữ pháp, bảng chữ cái là các ký tự ASCII, các sản phẩm P là các quy tắc trong ngữ pháp và ký hiệu bắt đầu S là quy tắc cấp cao nhất của trình phân tích cú pháp.

Hiện tại lexer định nghĩa một bảng chữ cái mã thông báo a, b, c, Nhận. Điều này cho phép trình phân tích cú pháp chính sử dụng các mã thông báo này làm bảng chữ cái: = {a, b, c, nhà}. Đối với lexer, các mã thông báo này không phải là thiết bị đầu cuối và quy tắc bắt đầu S L là S L → | một S | b S | c S | Sầu, đó là: bất kỳ chuỗi mã thông báo nào. Các quy tắc trong ngữ pháp lexer là tất cả các quy tắc cần thiết để tạo ra các mã thông báo này.

Lợi thế về hiệu suất đến từ việc thể hiện các quy tắc của nhà từ vựng như một ngôn ngữ thông thường . Chúng có thể được phân tích cú pháp hiệu quả hơn nhiều so với các ngôn ngữ không ngữ cảnh. Cụ thể, các ngôn ngữ thông thường có thể được nhận ra trong không gian O (n) và thời gian O (n). Trong thực tế, một trình tạo mã có thể biến một lexer như vậy thành các bảng nhảy hiệu quả cao.

Trích xuất mã thông báo từ ngữ pháp của bạn.

Để chạm vào ví dụ của bạn: các quy tắc digitstringđược thể hiện ở cấp độ theo từng ký tự. Chúng tôi có thể sử dụng chúng làm mã thông báo. Phần còn lại của ngữ pháp vẫn còn nguyên. Đây là ngữ pháp từ vựng, được viết dưới dạng ngữ pháp tuyến tính phải để làm rõ rằng nó là chính quy:

digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;

Nhưng vì nó là thông thường, chúng tôi thường sẽ sử dụng các biểu thức thông thường để thể hiện cú pháp mã thông báo. Dưới đây là các định nghĩa mã thông báo ở trên dưới dạng biểu thức chính quy, được viết bằng cú pháp loại trừ lớp ký tự .NET và các lớp ký tự POSIX:

digit ~ [0-9]
string ~ "[[:print:]-["]]*"

Ngữ pháp cho trình phân tích cú pháp chính sau đó chứa các quy tắc còn lại không được xử lý bởi nhà từ vựng. Trong trường hợp của bạn, đó chỉ là:

input = digit | string ;

Khi lexers không thể được sử dụng dễ dàng.

Khi thiết kế một ngôn ngữ, chúng ta thường quan tâm rằng ngữ pháp có thể được phân tách rõ ràng thành cấp độ từ vựng và cấp độ phân tích cú pháp và cấp độ từ vựng mô tả một ngôn ngữ thông thường. Không phải lúc nào cũng khả thi.

  • Khi nhúng ngôn ngữ. Một số ngôn ngữ cho phép bạn nội suy mã thành chuỗi : "name={expression}". Cú pháp biểu thức là một phần của ngữ pháp không ngữ cảnh và do đó không thể được mã hóa bằng một biểu thức thông thường. Để giải quyết vấn đề này, chúng tôi hoặc kết hợp lại trình phân tích cú pháp với lexer hoặc chúng tôi giới thiệu các mã thông báo bổ sung như thế nào STRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END. Quy tắc ngữ pháp cho một chuỗi có thể trông giống như : String → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END. Tất nhiên, Biểu thức có thể chứa các chuỗi khác, dẫn chúng ta đến vấn đề tiếp theo.

  • Khi mã thông báo có thể chứa nhau. Trong các ngôn ngữ giống như C, từ khóa không thể phân biệt với các định danh. Điều này được giải quyết trong lexer bằng cách ưu tiên từ khóa hơn số nhận dạng. Một chiến lược như vậy không phải lúc nào cũng có thể. Tưởng tượng một tập tin cấu hình trong đó Line → IDENTIFIER " = " REST, phần còn lại là bất kỳ ký tự nào cho đến cuối dòng, ngay cả khi phần còn lại trông giống như một định danh. Một dòng ví dụ sẽ là a = b c. Các lexer thực sự ngu ngốc và không biết thứ tự các mã thông báo có thể xảy ra theo thứ tự nào. Vì vậy, nếu chúng tôi ưu tiên IDENTIFIER hơn REST, nhà từ vựng sẽ cung cấp cho chúng tôi IDENT(a), " = ", IDENT(b), REST( c). Nếu chúng tôi ưu tiên REST hơn IDENTIFIER, lexer sẽ chỉ cung cấp cho chúng tôi REST(a = b c).

    Để giải quyết điều này, chúng ta phải kết hợp lại từ vựng với trình phân tích cú pháp. Việc phân tách có thể được duy trì phần nào bằng cách làm cho lexer lười biếng: mỗi lần trình phân tích cú pháp cần mã thông báo tiếp theo, nó sẽ yêu cầu nó từ lexer và nói với lexer tập hợp các mã thông báo có thể chấp nhận. Thực tế, chúng tôi đang tạo một quy tắc cấp cao nhất mới cho ngữ pháp từ vựng cho từng vị trí. Ở đây, điều này sẽ dẫn đến các cuộc gọi nextToken(IDENT), nextToken(" = "), nextToken(REST), và mọi thứ hoạt động tốt. Điều này đòi hỏi một trình phân tích cú pháp biết toàn bộ mã thông báo có thể chấp nhận tại mỗi vị trí, ngụ ý một trình phân tích cú pháp từ dưới lên như LR.

  • Khi lexer phải duy trì trạng thái. Ví dụ, ngôn ngữ Python phân định các khối mã không phải bằng dấu ngoặc nhọn, mà bằng cách thụt lề. Có nhiều cách để xử lý cú pháp nhạy cảm bố cục trong một ngữ pháp, nhưng những kỹ thuật đó quá mức cần thiết cho Python. Thay vào đó, lexer kiểm tra độ thụt của từng dòng và phát ra các mã thông báo INDENT nếu tìm thấy một khối thụt lề mới và mã thông báo DEDENT nếu khối kết thúc. Điều này đơn giản hóa ngữ pháp chính vì giờ đây nó có thể giả vờ những mã thông báo đó giống như dấu ngoặc nhọn. Các lexer tuy nhiên bây giờ cần phải duy trì trạng thái: thụt hiện tại. Điều này có nghĩa là từ vựng về mặt kỹ thuật không còn mô tả một ngôn ngữ thông thường, mà thực sự là một ngôn ngữ nhạy cảm theo ngữ cảnh. May mắn thay, sự khác biệt này không liên quan trong thực tế và lexer của Python vẫn có thể hoạt động trong thời gian O (n).


Câu trả lời rất hay @amon, Cảm ơn bạn. Tôi sẽ phải mất một thời gian để tiêu hóa nó hoàn toàn. Tôi đã tuy nhiên, tự hỏi một vài điều về câu trả lời của bạn. Khoảng đoạn thứ tám, bạn chỉ ra cách tôi có thể sửa đổi ngữ pháp EBNF mẫu của mình thành các quy tắc cho trình phân tích cú pháp. Ngữ pháp bạn hiển thị cũng được sử dụng bởi trình phân tích cú pháp? Hoặc vẫn còn một ngữ pháp riêng cho trình phân tích cú pháp?
Christian Dean

@Engineer Tôi đã thực hiện một vài chỉnh sửa. EBNF của bạn có thể được sử dụng trực tiếp bởi trình phân tích cú pháp. Tuy nhiên, ví dụ của tôi cho thấy phần nào của ngữ pháp có thể được xử lý bởi một từ vựng riêng biệt. Bất kỳ quy tắc nào khác vẫn sẽ được xử lý bởi trình phân tích cú pháp chính, nhưng trong ví dụ của bạn chỉ là như vậy input = digit | string.
amon

4
Ưu điểm lớn của trình phân tích cú pháp không quét là chúng dễ soạn hơn nhiều; ví dụ cực đoan đó là các thư viện kết hợp trình phân tích cú pháp, trong đó bạn không làm gì ngoài việc soạn thảo trình phân tích cú pháp. Soạn các trình phân tích cú pháp rất thú vị đối với các trường hợp như ECMAScript-embed-in-HTML-embed-in-PHP-rắc-with-SQL-with-a-template-ngôn ngữ-top-top hoặc Ruby-example-embed-in-Markdown- nhúng trong Ruby-tài liệu-bình luận hoặc một cái gì đó tương tự.
Jörg W Mittag

Điểm đạn cuối cùng là rất quan trọng nhưng tôi cảm thấy cách bạn viết nó là sai lệch. Đúng là các từ vựng không thể được sử dụng dễ dàng với cú pháp dựa trên thụt lề, nhưng phân tích cú pháp không quét thậm chí còn khó hơn trong trường hợp đó. Vì vậy, bạn thực sự muốn sử dụng một từ vựng nếu bạn có loại ngôn ngữ đó, làm tăng thêm nó với trạng thái có liên quan.
dùng541686

@Mehrdad Mã thông báo thụt lề / mã thông báo theo phong cách Python chỉ có thể áp dụng cho các ngôn ngữ nhạy cảm thụt lề rất đơn giản và thường không được áp dụng. Một thay thế chung hơn là các ngữ pháp thuộc tính, nhưng sự hỗ trợ của chúng còn thiếu các công cụ tiêu chuẩn. Ý tưởng là chúng tôi chú thích mọi đoạn AST bằng cách thụt lề của nó và thêm các ràng buộc cho tất cả các quy tắc. Các thuộc tính rất đơn giản để thêm với phân tích cú pháp kết hợp, điều này cũng giúp bạn dễ dàng thực hiện phân tích cú pháp không quét.
amon
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.