Tại sao thực hiện một lexer như một mảng 2d và một chuyển đổi khổng lồ?


24

Tôi đang dần dần làm việc để hoàn thành văn bằng của mình và học kỳ này là Trình biên dịch 101. Chúng tôi đang sử dụng Sách Rồng . Một thời gian ngắn tham gia khóa học và chúng ta đang nói về phân tích từ vựng và cách nó có thể được thực hiện thông qua automata hữu hạn xác định (sau đây, DFA). Thiết lập các trạng thái từ vựng khác nhau của bạn, xác định chuyển tiếp giữa chúng, v.v.

Nhưng cả giáo sư và cuốn sách đều đề xuất thực hiện chúng thông qua các bảng chuyển tiếp với một mảng 2d khổng lồ (các trạng thái không đầu cuối khác nhau như một chiều và các ký hiệu đầu vào có thể khác) và một câu lệnh chuyển đổi để xử lý tất cả các đầu cuối cũng như gửi đến các bảng chuyển tiếp nếu ở trạng thái không đầu cuối.

Lý thuyết là tốt và tốt, nhưng như một người thực sự đã viết mã trong nhiều thập kỷ, việc thực hiện là vô ích. Nó không thể kiểm tra được, nó không thể bảo trì, không thể đọc được và đó là một nỗi đau và một nửa để gỡ lỗi. Tệ hơn nữa, tôi không thể thấy nó sẽ thực tế đến mức nào nếu ngôn ngữ có khả năng UTF. Có một triệu mục nhập bảng chuyển đổi cho mỗi trạng thái không đầu cuối trở nên khó khăn trong sự vội vàng.

Vậy thỏa thuận là gì? Tại sao cuốn sách dứt khoát về chủ đề nói làm theo cách này?

Là chi phí chung của các cuộc gọi chức năng thực sự nhiều? Đây có phải là một cái gì đó hoạt động tốt hoặc là cần thiết khi ngữ pháp không được biết trước (biểu thức thông thường?)? Hoặc có lẽ một cái gì đó xử lý tất cả các trường hợp, ngay cả khi các giải pháp cụ thể hơn sẽ hoạt động tốt hơn cho các ngữ pháp cụ thể hơn?

( Lưu ý: có thể trùng lặp " Tại sao sử dụng một cách tiếp cận OO thay vì một câu lệnh switch khổng lồ? " Gần, nhưng tôi không quan tâm đến OO Một cách tiếp cận chức năng hoặc thậm chí tiếp cận saner bắt buộc với các chức năng độc lập sẽ là tốt..)

Và vì lợi ích của ví dụ, hãy xem xét một ngôn ngữ chỉ có định danh và những định danh đó là [a-zA-Z]+. Trong triển khai DFA, bạn sẽ nhận được một cái gì đó như:

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(mặc dù một cái gì đó sẽ xử lý chính xác cuối tập tin)

So với những gì tôi mong đợi:

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

Với mã được NextTokentái cấu trúc thành chức năng của chính nó một khi bạn có nhiều điểm đến từ khi bắt đầu DFA.


5
một di sản của cổ xưa (1977) Nguyên tắc thiết kế trình biên dịch ? 40 năm trước, phong cách mã hóa đã khác đi nhiều
gnat

7
Làm thế nào bạn sẽ thực hiện chuyển đổi của các trạng thái DFA? Và những gì về thiết bị đầu cuối và không thiết bị đầu cuối, "không phải thiết bị đầu cuối" thường đề cập đến các quy tắc sản xuất trong ngữ pháp, sẽ xuất hiện sau khi phân tích từ vựng.

10
Những bảng này không có nghĩa là có thể đọc được cho con người, chúng có thể được trình biên dịch sử dụng và thực hiện rất nhanh. Thật dễ dàng để nhảy xung quanh một bảng khi nhìn về phía trước trong đầu vào (ví dụ để bắt đệ quy trái, mặc dù trong thực tế hầu hết các ngôn ngữ được xây dựng để tránh điều đó).

5
Nếu một phần nào đó của sự khó chịu của bạn xuất phát từ việc biết cách làm một công việc tốt hơn và không có khả năng nhận được bất kỳ phản hồi hoặc đánh giá cao nào cho cách tiếp cận mà bạn thích - vì hàng thập kỷ trong ngành đã đào tạo chúng tôi mong đợi phản hồi và đôi khi đánh giá cao - có lẽ bạn nên viết bản triển khai tốt hơn của mình và đăng nó lên CodeReview.SE để có được sự an tâm đó.
Jimmy Hoffa

7
Câu trả lời đơn giản là bởi vì từ vựng thường được triển khai như một máy trạng thái hữu hạn và được tạo tự động từ ngữ pháp - và một bảng trạng thái, không đáng ngạc nhiên, được biểu diễn dễ dàng và gọn nhẹ nhất dưới dạng bảng. Như với mã đối tượng, thực tế là con người không dễ làm việc với nó là không liên quan vì con người không làm việc với nó; họ thay đổi nguồn và tạo một thể hiện mới.
keshlam

Câu trả lời:


16

Trong thực tế, các bảng này được tạo từ các biểu thức chính quy xác định mã thông báo của ngôn ngữ:

number := [digit][digit|underscore]+
reserved_word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

Chúng tôi đã có các tiện ích để tạo ra các máy phân tích từ vựng từ năm 1975 khi lex được viết.

Về cơ bản, bạn đang đề xuất thay thế các biểu thức thông thường bằng mã thủ tục. Điều này mở rộng một vài ký tự trong một biểu thức chính quy thành một vài dòng mã. Mã thủ tục viết tay để phân tích từ vựng của bất kỳ ngôn ngữ thú vị vừa phải có xu hướng vừa không hiệu quả vừa khó duy trì.


4
Tôi không chắc chắn tôi đang đề nghị bán buôn. Biểu thức chính quy sẽ xử lý các ngôn ngữ tùy ý (thông thường). Không có cách tiếp cận tốt hơn khi làm việc với các ngôn ngữ cụ thể? Cuốn sách chạm đến các phương pháp dự đoán nhưng sau đó bỏ qua chúng trong các ví dụ. Ngoài ra, đã thực hiện một bộ phân tích ngây thơ cho C # năm trước, tôi không thấy nó quá khó để duy trì. Không hiệu quả? chắc chắn, nhưng không quá khủng khiếp để đưa ra kỹ năng của tôi tại thời điểm đó.
Telastyn

1
@Telastyn: gần như không thể đi nhanh hơn DFA theo bảng: nhận ký tự tiếp theo, tra cứu trạng thái tiếp theo trong bảng chuyển đổi, thay đổi trạng thái. Nếu trạng thái mới là thiết bị đầu cuối, phát ra mã thông báo. Trong C # hoặc Java, bất kỳ cách tiếp cận nào liên quan đến việc tạo bất kỳ chuỗi tạm thời nào cũng sẽ chậm hơn.
kevin cline

@kevincline - chắc chắn, nhưng trong ví dụ của tôi không có chuỗi tạm thời. Ngay cả trong C, nó sẽ chỉ là một chỉ mục hoặc một con trỏ bước qua chuỗi.
Telastyn

6
@JimmyHoffa: có, hiệu suất chắc chắn có liên quan trong trình biên dịch. Trình biên dịch nhanh vì chúng đã được tối ưu hóa thành địa ngục và ngược lại. Không tối ưu hóa vi mô, họ chỉ không làm những việc không cần thiết như tạo và loại bỏ các đối tượng tạm thời không cần thiết. Theo kinh nghiệm của tôi, hầu hết các mã xử lý văn bản thương mại thực hiện một phần mười công việc của một trình biên dịch hiện đại và mất mười lần thời gian để thực hiện nó. Hiệu suất là rất lớn khi bạn đang xử lý một gigabyte văn bản.
kevin cline

1
@Telastyn, bạn đã nghĩ đến "cách tiếp cận tốt hơn" nào và bạn nghĩ nó sẽ "tốt hơn" theo cách nào? Vì chúng tôi đã có các công cụ từ vựng được kiểm tra tốt và chúng tạo ra các trình phân tích cú pháp rất nhanh (như những người khác đã nói, các DFA điều khiển bảng rất nhanh), nên sử dụng chúng rất hợp lý. Tại sao chúng ta muốn phát minh ra một cách tiếp cận đặc biệt mới cho một ngôn ngữ cụ thể, khi chúng ta chỉ có thể viết một ngữ pháp lex? Ngữ pháp lex dễ bảo trì hơn và trình phân tích cú pháp kết quả có nhiều khả năng đúng (được đưa ra mức độ kiểm tra tốt của lex và các công cụ tương tự).
DW

7

Động lực cho thuật toán cụ thể phần lớn là do nó là một bài tập học tập, vì vậy nó cố gắng theo sát ý tưởng về DFA, và giữ các trạng thái và chuyển tiếp rất rõ ràng trong mã. Theo quy định, dù sao đi nữa, không ai thực sự viết bất kỳ mã nào trong số này - bạn sẽ sử dụng một công cụ để tạo mã từ ngữ pháp. Và công cụ đó sẽ không quan tâm đến tính dễ đọc của mã bởi vì nó không phải là mã nguồn, nó là đầu ra dựa trên định nghĩa của ngữ pháp.

Mã của bạn sạch hơn đối với người duy trì DFA viết tay, nhưng xa hơn một chút khỏi các khái niệm được dạy.


7

Vòng lặp bên trong của:

                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;

có rất nhiều lợi thế về hiệu suất. Không có nhánh nào trong đó, bởi vì bạn làm chính xác điều tương tự cho mọi ký tự đầu vào. Hiệu suất của trình biên dịch có thể được kiểm soát bởi lexer (phải hoạt động theo tỷ lệ của mỗi ký tự đầu vào). Điều này thậm chí còn đúng hơn khi Sách Rồng được viết.

Trong thực tế, bên cạnh các sinh viên CS học từ vựng, không ai phải thực hiện (hoặc gỡ lỗi) vòng lặp bên trong đó bởi vì nó là một phần của bản tóm tắt đi kèm với công cụ xây dựng transitionbảng.


5

Từ bộ nhớ, - đã lâu rồi tôi mới đọc cuốn sách này và tôi khá chắc chắn rằng mình đã không đọc phiên bản mới nhất, tôi chắc chắn không nhớ thứ gì đó trông giống Java - phần đó được viết bằng mã được dự định là một mẫu, bảng được điền với một trình tạo lexer giống như lexer. Vẫn từ bộ nhớ, có một phần về nén bảng (một lần nữa từ bộ nhớ, nó được viết theo cách nó cũng có thể áp dụng cho các trình phân tích cú pháp điều khiển bảng, do đó có lẽ trong cuốn sách nhiều hơn những gì bạn đã thấy). Tương tự, cuốn sách mà tôi nhớ đã giả định một bộ ký tự 8 bit, tôi mong đợi một phần xử lý bộ ký tự lớn hơn trong các phiên bản sau này, có thể là một phần của nén bảng. Tôi đã đưa ra một cách khác để xử lý câu trả lời cho câu hỏi SO.

Có một lợi thế về hiệu suất chắc chắn khi có dữ liệu vòng lặp chặt chẽ được điều khiển trong kiến ​​trúc hiện đại: nó khá thân thiện với bộ nhớ cache (nếu bạn đã nén các bảng) và dự đoán nhảy là hoàn hảo nhất có thể (một lỗi ở cuối lexem, có lẽ là một bỏ lỡ việc chuyển công tắc tới mã phụ thuộc vào ký hiệu; giả sử rằng việc giải nén bảng của bạn có thể được thực hiện với các bước nhảy dự đoán). Chuyển máy trạng thái đó sang mã thuần sẽ làm giảm hiệu suất dự đoán bước nhảy và có thể làm tăng áp lực bộ đệm.


2

Đã từng làm việc với Dragon Book trước đây, lý do chính để có các đòn bẩy và trình phân tích cú pháp điều khiển bảng là để bạn có thể sử dụng các biểu thức thông thường để tạo lexer và BNF để tạo trình phân tích cú pháp. Cuốn sách cũng đề cập đến cách các công cụ như lex và yacc hoạt động, và để bạn biết các công cụ này hoạt động như thế nào. Hơn nữa, điều quan trọng là bạn phải làm việc thông qua một số ví dụ thực tế.

Mặc dù có nhiều ý kiến, nó không liên quan gì đến phong cách mã được viết trong những năm 40, 50, 60 ..., nó phải làm để có được sự hiểu biết thực tế về những gì các công cụ đang làm cho bạn và những gì bạn có để làm cho họ làm việc. Nó có mọi thứ để làm với sự hiểu biết cơ bản về cách trình biên dịch làm việc cả từ quan điểm lý thuyết và thực tiễn.

Hy vọng, người hướng dẫn của bạn cũng sẽ cho phép bạn sử dụng lex và yacc (trừ khi đó là lớp cấp độ sau đại học và bạn có thể viết lex và yacc).


0

Đến bữa tiệc muộn :-) Các mã thông báo được khớp với các biểu thức thông thường. Vì có rất nhiều trong số chúng, bạn có công cụ đa regex, đến lượt nó là DFA khổng lồ.

"Tệ hơn nữa, tôi không thể thấy nó sẽ thực tế đến mức nào nếu ngôn ngữ có khả năng UTF."

Nó không liên quan (hoặc minh bạch). Bên cạnh đó UTF có tài sản đẹp, các thực thể của nó không chồng chéo thậm chí một phần. Ví dụ, ký tự đại diện byte "A" (từ bảng ASCII-7) không được sử dụng lại cho bất kỳ ký tự UTF nào khác.

Vì vậy, bạn có DFA duy nhất (đa regex) cho toàn bộ lexer. Làm thế nào tốt hơn để viết nó xuống hơn mảng 2d?

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.