Đến với mã thông báo cho một lexer


14

Tôi đang viết một trình phân tích cú pháp cho một ngôn ngữ đánh dấu mà tôi đã tạo (viết bằng python, nhưng điều đó không thực sự liên quan đến câu hỏi này - thực tế nếu đây có vẻ là một ý tưởng tồi, tôi thích một gợi ý cho một con đường tốt hơn) .

Tôi đang đọc về các trình phân tích cú pháp ở đây: http://www.ferg.org/parsing/index.html và tôi đang viết lexer, nếu tôi hiểu chính xác, hãy chia nội dung thành các thẻ. Điều tôi gặp khó khăn trong việc hiểu là loại mã thông báo nào tôi nên sử dụng hoặc cách tạo chúng. Ví dụ: các loại mã thông báo trong ví dụ tôi liên kết đến là:

  • CHUỖI
  • XÁC NHẬN
  • CON SỐ
  • VÒI
  • BÌNH LUẬN
  • EOF
  • Nhiều biểu tượng như {và (được tính là loại mã thông báo của riêng họ

Vấn đề tôi gặp phải là các loại mã thông báo tổng quát hơn có vẻ hơi độc đoán đối với tôi. Ví dụ: tại sao CHUINGI loại mã thông báo riêng của nó so với IDENTIFIER. Một chuỗi có thể được biểu diễn dưới dạng STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START.

Điều này cũng có thể phải làm với những khó khăn trong ngôn ngữ của tôi. Ví dụ, khai báo biến được viết {var-name var value}và triển khai với {var-name}. Có vẻ như '{''}'nên là mã thông báo của riêng họ, nhưng các loại mã thông báo đủ điều kiện VAR_NAME và VAR_VALUE, hoặc cả hai loại này đều thuộc IDENTIFIER? Thêm nữa là VAR_VALUE thực sự có thể chứa khoảng trắng. Khoảng trắng sau var-nameđược sử dụng để biểu thị sự bắt đầu của giá trị trong khai báo .. bất kỳ khoảng trắng nào khác là một phần của giá trị. Liệu khoảng trắng này trở thành mã thông báo của riêng nó? Khoảng trắng chỉ có ý nghĩa đó trong bối cảnh này. Hơn nữa, {có thể không phải là khởi đầu của một khai báo biến .. nó phụ thuộc vào ngữ cảnh (lại có từ đó!). {:bắt đầu khai báo tên và{ thậm chí có thể được sử dụng như một phần của một số giá trị.

Ngôn ngữ của tôi tương tự như Python trong các khối được tạo ra với thụt lề. Tôi đã đọc về cách Python sử dụng từ vựng để tạo mã thông báo INDENT và DEDENT (phục vụ ít nhiều như những gì {}sẽ làm trong nhiều ngôn ngữ khác). Python tuyên bố là không có ngữ cảnh, điều đó có nghĩa với tôi rằng ít nhất là người từ chối không quan tâm đến vị trí của nó trong luồng trong khi tạo mã thông báo. Làm thế nào để lexer của Python biết rằng nó đang xây dựng mã thông báo INDENT có độ dài cụ thể mà không biết về các ký tự trước đó (ví dụ: dòng trước đó là một dòng mới, vì vậy hãy bắt đầu tạo khoảng trắng cho INDENT)? Tôi hỏi vì tôi cũng cần biết điều này.

Câu hỏi cuối cùng của tôi là câu hỏi ngu ngốc nhất: tại sao một từ vựng thậm chí còn cần thiết? Dường như với tôi rằng trình phân tích cú pháp có thể đi từng nhân vật và tìm ra vị trí của nó và những gì nó mong đợi. Liệu lexer thêm lợi ích của sự đơn giản?


2
Đi aheead và thử viết một trình phân tích cú pháp quét. Nếu nó hoạt động hoàn toàn (tôi tưởng tượng kết quả có thể quá mơ hồ đối với một số thuật toán phân tích cú pháp), rất có thể bạn sẽ không thấy bất kỳ ngữ pháp thực tế nào bên dưới tất cả "khoảng trắng được cho phép ở đây" và "chờ đã, tôi đã phân tích cú pháp định danh hay số? ". Tôi nói từ kinh nghiệm.

Tại sao phải phát minh lại một bánh xe tùy chỉnh? Thay vì thiết kế một ngôn ngữ yêu cầu một từ vựng được xây dựng tùy chỉnh, bạn đã cân nhắc sử dụng một ngôn ngữ hiện có đi kèm với một từ vựng tích hợp sẵn, như LISP, hoặc thậm chí FORTH chưa?
John R. Strohm

2
@ JohnR.Strohm cho mục đích học tập. Bản thân ngôn ngữ có lẽ sẽ không thực sự hữu ích.
Thuốc nổ

Câu trả lời:


11

Câu hỏi của bạn (như gợi ý đoạn cuối cùng của bạn) không thực sự về từ vựng, nó là về thiết kế chính xác của giao diện giữa lexer và trình phân tích cú pháp. Như bạn có thể tưởng tượng có rất nhiều cuốn sách về thiết kế của lexers và trình phân tích cú pháp. Tôi tình cờ thích cuốn sách phân tích cú pháp của Dick Grune , nhưng nó có thể không phải là một cuốn sách giới thiệu hay. Tôi tình cờ không thích cuốn sách dựa trên C của Appel , bởi vì mã không thể mở rộng một cách hữu ích vào trình biên dịch của riêng bạn (vì các vấn đề quản lý bộ nhớ vốn có trong quyết định giả vờ C giống như ML). Giới thiệu của riêng tôi là cuốn sách của PJ Brown , nhưng nó không phải là một giới thiệu chung tốt (mặc dù khá tốt cho phiên dịch cụ thể). Nhưng trở lại câu hỏi của bạn.

Câu trả lời là, hãy làm nhiều nhất có thể trong từ vựng mà không cần phải sử dụng các ràng buộc về phía trước hoặc phía sau.

Điều này có nghĩa là (tất nhiên phụ thuộc vào chi tiết của ngôn ngữ), bạn nên nhận ra một chuỗi là một "ký tự theo sau là một chuỗi không-" và sau đó là một ký tự khác. Trả lại cho trình phân tích cú pháp dưới dạng một đơn vị. lý do cho điều này, nhưng những lý do quan trọng là

  1. Điều này làm giảm lượng trạng thái mà trình phân tích cú pháp cần duy trì, hạn chế mức tiêu thụ bộ nhớ của nó.
  2. Điều này cho phép triển khai từ vựng tập trung vào việc nhận ra các khối xây dựng cơ bản và giải phóng trình phân tích cú pháp để mô tả cách các phần tử cú pháp riêng lẻ được sử dụng để xây dựng chương trình.

Rất thường các trình phân tích cú pháp có thể thực hiện các hành động ngay lập tức khi nhận mã thông báo từ nhà từ vựng. Ví dụ, ngay khi nhận được IDENTIFIER, trình phân tích cú pháp có thể thực hiện tra cứu bảng biểu tượng để tìm hiểu xem biểu tượng đã được biết chưa. Nếu trình phân tích cú pháp của bạn cũng phân tích các hằng chuỗi là QUOTE (IDENTIFIER SPACES) * HỎI, bạn sẽ thực hiện rất nhiều tra cứu bảng biểu tượng không liên quan, hoặc bạn sẽ kết thúc việc tra cứu bảng biểu tượng lên cao hơn các phần tử cú pháp của trình phân tích cú pháp, bởi vì bạn chỉ có thể làm bây giờ bạn chắc chắn rằng bạn không nhìn vào một chuỗi.

Để nói lại những gì tôi đang cố nói, nhưng khác đi, người viết lách nên quan tâm đến cách đánh vần của sự vật và trình phân tích cú pháp với cấu trúc của sự vật.

Bạn có thể nhận thấy rằng mô tả của tôi về một chuỗi trông giống như một biểu thức thông thường. Đây không phải là sự trùng hợp. Các máy phân tích từ điển thường được triển khai bằng các ngôn ngữ nhỏ (theo nghĩa của cuốn sách Lập trình viên ngọc trai xuất sắc của Jon Bentley ) sử dụng các biểu thức thông thường. Tôi chỉ quen suy nghĩ theo cách diễn đạt thông thường khi nhận dạng văn bản.

Về câu hỏi của bạn về khoảng trắng, hãy nhận ra nó trong từ vựng. Nếu ngôn ngữ của bạn được định dạng ở định dạng khá miễn phí, đừng trả lại mã thông báo WHITESPACE cho trình phân tích cú pháp, bởi vì ngôn ngữ đó sẽ chỉ phải vứt chúng đi, do đó, các quy tắc sản xuất của trình phân tích cú pháp của bạn sẽ bị spam về cơ bản - những điều cần nhận biết chỉ để ném họ đi

Đối với những gì có nghĩa là về cách bạn nên xử lý khoảng trắng khi nó có ý nghĩa về mặt cú pháp, tôi không chắc tôi có thể đưa ra đánh giá cho bạn rằng nó sẽ thực sự hoạt động tốt mà không cần biết thêm về ngôn ngữ của bạn. Đánh giá nhanh của tôi là để tránh các trường hợp khoảng trắng đôi khi quan trọng và đôi khi không, và sử dụng một số loại dấu phân cách (như dấu ngoặc kép). Nhưng, nếu bạn không thể thiết kế ngôn ngữ theo bất kỳ cách nào bạn thích, tùy chọn này có thể không có sẵn cho bạn.

Có nhiều cách khác để thiết kế hệ thống phân tích ngôn ngữ. Chắc chắn có các hệ thống xây dựng trình biên dịch cho phép bạn chỉ định một hệ thống lexer và trình phân tích cú pháp kết hợp (tôi nghĩ rằng phiên bản Java của ANTLR thực hiện điều này) nhưng tôi chưa bao giờ sử dụng một hệ thống.

Cuối một ghi chú lịch sử. Nhiều thập kỷ trước, điều quan trọng là người làm việc phải làm càng nhiều càng tốt trước khi bàn giao cho trình phân tích cú pháp, bởi vì hai chương trình sẽ không vừa với bộ nhớ cùng một lúc. Làm nhiều hơn trong lexer để lại nhiều bộ nhớ hơn để làm cho trình phân tích cú pháp thông minh. Tôi đã từng sử dụng Trình biên dịch C Whitesmiths trong một số năm và nếu tôi hiểu chính xác, nó sẽ chỉ hoạt động trong 64KB RAM (đó là chương trình MS-DOS mô hình nhỏ) và thậm chí nó đã dịch một biến thể của C đã rất rất gần với ANSI C.


Ghi chú lịch sử tốt về kích thước bộ nhớ là một lý do để phân chia công việc thành các từ vựng và trình phân tích cú pháp ngay từ đầu.
stevegt

3

Tôi sẽ đưa ra câu hỏi cuối cùng của bạn, thực tế không phải là ngu ngốc. Các trình phân tích cú pháp có thể và thực hiện việc xây dựng các cấu trúc phức tạp trên cơ sở từng ký tự. Nếu tôi nhớ lại, ngữ pháp trong Harbison và Steele ("C - Cẩm nang tham khảo") có các sản phẩm sử dụng các ký tự đơn làm đầu cuối và xây dựng các mã định danh, chuỗi, số, v.v. làm các đầu cuối từ các ký tự đơn.

Từ quan điểm ngôn ngữ chính thức, bất cứ điều gì mà một nhà từ vựng dựa trên biểu thức chính quy có thể nhận ra và phân loại là "chuỗi ký tự", "định danh", "số", "từ khóa", v.v., ngay cả trình phân tích cú pháp LL (1) cũng có thể nhận ra. Vì vậy, không có vấn đề lý thuyết nào với việc sử dụng trình tạo trình phân tích cú pháp để nhận ra mọi thứ.

Từ quan điểm thuật toán, một trình nhận dạng biểu thức chính quy có thể chạy nhanh hơn nhiều so với bất kỳ trình phân tích cú pháp nào. Từ quan điểm nhận thức, lập trình viên có thể dễ dàng hơn trong việc phân chia công việc giữa trình phân tích biểu thức chính quy và trình phân tích cú pháp trình phân tích cú pháp.

Tôi muốn nói rằng những cân nhắc thực tế khiến mọi người đưa ra quyết định có các từ vựng và trình phân tích cú pháp riêng biệt.


Có - và bản thân tiêu chuẩn C cũng làm điều tương tự, như thể tôi nhớ lại một cách chính xác, cả hai phiên bản của Kernighan và Ritchie đã làm.
James Youngman

3

Có vẻ như bạn đang cố gắng viết một từ vựng / trình phân tích cú pháp mà không thực sự hiểu ngữ pháp. Thông thường, khi mọi người đang viết một từ vựng và trình phân tích cú pháp, họ đang viết chúng để tuân thủ một số ngữ pháp. Nhà ngữ pháp sẽ trả lại các mã thông báo trong ngữ pháp trong khi trình phân tích cú pháp sử dụng các mã thông báo đó để khớp với quy tắc / không phải thiết bị đầu cuối . Nếu bạn có thể dễ dàng phân tích cú pháp đầu vào của mình theo từng byte, thì một từ vựng và trình phân tích cú pháp có thể là quá mức cần thiết.

Lexers làm cho mọi thứ đơn giản hơn.

Tổng quan về ngữ pháp : Một ngữ pháp là một tập hợp các quy tắc về cách một số cú pháp hoặc đầu vào sẽ trông như thế nào. Ví dụ: đây là một ngữ pháp đồ chơi (Simple_command là ký hiệu bắt đầu):

simple_command:
 WORD DIGIT AND_SYMBOL
simple_command:
     addition_expression

addition_expression:
    NUM '+' NUM

Ngữ pháp này có nghĩa là -
Một Simple_command bao gồm
A) WORD theo sau bởi DIGIT theo sau là AND_SYMBOL (đây là "mã thông báo" mà tôi xác định)
B) Một " add_expression " (đây là quy tắc hoặc "không phải thiết bị đầu cuối")

Một add_expression bao gồm:
NUM theo sau là '+' theo sau là NUM (NUM là "mã thông báo" mà tôi xác định, '+' là dấu cộng theo nghĩa đen).

Do đó, vì Simple_command là "biểu tượng bắt đầu" (nơi tôi bắt đầu), khi tôi nhận được mã thông báo, tôi kiểm tra xem liệu nó có phù hợp với Simple_command không. Nếu mã thông báo đầu tiên trong đầu vào là WORD và mã thông báo tiếp theo là DIGIT và mã thông báo tiếp theo là AND_SYMBOL, thì tôi đã khớp một số đơn giản và có thể thực hiện một số hành động. Mặt khác, tôi sẽ cố gắng khớp nó với quy tắc khác của Simple_command là thêm_expression. Do đó, nếu mã thông báo đầu tiên là một số được theo sau bởi '+' được theo sau bởi một số, thì tôi đã khớp với một đơn giản và tôi thực hiện một số hành động. Nếu đó không phải là những điều đó, thì tôi có lỗi cú pháp.

Đó là một giới thiệu rất, rất cơ bản cho ngữ pháp. Để hiểu rõ hơn, hãy xem bài viết wiki này và tìm kiếm trên web để biết các hướng dẫn ngữ pháp không ngữ cảnh.

Sử dụng sắp xếp lexer / trình phân tích cú pháp, đây là một ví dụ về cách trình phân tích cú pháp của bạn có thể trông như thế nào:

bool simple_command(){
   if (peek_next_token() == WORD){
       get_next_token();
       if (get_next_token() == DIGIT){
           if (get_next_token() == AND_SYMBOL){
               return true;
           } 
       }
   }
   else if (addition_expression()){
       return true;
   }

   return false;
}

bool addition_expression(){
    if (get_next_token() == NUM){
        if (get_next_token() == '+'){
             if (get_next_token() == NUM){
                  return true;
             }
        }
    }
    return false;
}

Ok, vì vậy mã đó là loại xấu xí và tôi sẽ không bao giờ đề xuất ba lần lồng nhau nếu các câu lệnh. Nhưng vấn đề là, hãy tưởng tượng bạn đang cố gắng thực hiện điều đó trên từng ký tự thay vì sử dụng các hàm "get_next_token" và "peek_next_token" mô-đun đẹp của bạn . Nghiêm túc, cho nó một shot. Bạn sẽ không thích kết quả. Bây giờ hãy nhớ rằng ngữ pháp ở trên ít phức tạp hơn khoảng 30 lần so với hầu hết các ngữ pháp hữu ích. Bạn có thấy lợi ích của việc sử dụng từ vựng không?

Thành thật mà nói, lexers và Parsers không phải là chủ đề cơ bản nhất trên thế giới. Trước tiên tôi khuyên bạn nên đọc và hiểu về ngữ pháp, sau đó đọc một chút về từ vựng / trình phân tích cú pháp, sau đó đi sâu vào.


Bạn có bất kỳ khuyến nghị cho việc học về ngữ pháp?
Thuốc nổ

Tôi chỉ chỉnh sửa câu trả lời của mình để bao gồm phần giới thiệu rất cơ bản về ngữ pháp và một số gợi ý để học thêm. Ngữ pháp là một chủ đề rất quan trọng trong khoa học máy tính vì vậy chúng đáng để học hỏi.
Casey Patton

1

Câu hỏi cuối cùng của tôi là câu hỏi ngu ngốc nhất: tại sao một từ vựng thậm chí còn cần thiết? Dường như với tôi rằng trình phân tích cú pháp có thể đi từng nhân vật và tìm ra vị trí của nó và những gì nó mong đợi.

Đây không phải là ngu ngốc, đó chỉ là sự thật.

Nhưng tính khả thi bằng cách nào đó phụ thuộc một chút vào các công cụ và mục tiêu của bạn. Ví dụ: nếu bạn sử dụng yacc mà không có từ vựng và bạn muốn cho phép các chữ cái unicode trong mã định danh, bạn sẽ phải viết một quy tắc lớn và xấu để giải thích liệt kê tất cả các ký tự hợp lệ. Trong khi, trong một từ vựng, bạn có thể hỏi một thói quen thư viện nếu một nhân vật là thành viên của thể loại thư.

Sử dụng hay không sử dụng từ vựng là vấn đề có mức độ trừu tượng giữa ngôn ngữ của bạn và cấp độ ký tự. Lưu ý rằng mức ký tự, ngày nay, là một sự trừu tượng hóa khác trên mức byte, đó là một sự trừu tượng hóa trên mức bit.

Vì vậy, cuối cùng, bạn thậm chí có thể phân tích cú pháp ở cấp độ bit.


0
STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START.

Không, nó không thể. Thế còn "("? Theo bạn, đó không phải là một chuỗi hợp lệ. Và trốn thoát?

Nói chung, cách tốt nhất để xử lý khoảng trắng là bỏ qua nó, ngoài việc phân định mã thông báo. Rất nhiều người thích khoảng trắng rất khác nhau và việc thực thi các quy tắc khoảng trắng là điều gây tranh cãi nhất.

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.