Điều gì sẽ là kiểu dữ liệu của các mã thông báo mà một từ vựng trả về cho trình phân tích cú pháp của nó?


21

Như đã nói trong tiêu đề, loại dữ liệu nào nên trả về / cung cấp cho trình phân tích cú pháp? Khi đọc bài viết phân tích từ vựng mà Wikipedia có, nó đã tuyên bố rằng:

Trong khoa học máy tính, phân tích từ vựng là quá trình chuyển đổi một chuỗi các ký tự (chẳng hạn như trong chương trình máy tính hoặc trang web) thành một chuỗi các mã thông báo ( chuỗi có "nghĩa" được xác định).

Tuy nhiên, hoàn toàn mâu thuẫn với tuyên bố trên, Khi một câu hỏi khác tôi hỏi trên một trang web khác ( Đánh giá mã nếu bạn tò mò) đã được trả lời, Người trả lời đã nói rằng:

Các lexer thường đọc chuỗi và chuyển đổi nó thành một luồng ... của các từ vựng. Các từ vựng chỉ cần là một dòng số .

và ông đã đưa ra hình ảnh này:

nl_output => 256
output    => 257
<string>  => 258

Sau đó, trong bài viết, ông đã đề cập Flex, một từ vựng đã tồn tại và nói rằng viết 'quy tắc' với nó sẽ đơn giản hơn so với viết một từ vựng bằng tay. Anh ta tiến hành đưa cho tôi ví dụ này:

Space              [ \r\n\t]
QuotedString       "[^"]*"
%%
nl_output          {return 256;}
output             {return 257;}
{QuotedString}     {return 258;}
{Space}            {/* Ignore */}
.                  {error("Unmatched character");}
%%

Để hiểu rõ hơn và có thêm thông tin, tôi đã đọc bài viết trên Wikipedia về Flex . bài viết Flex cho thấy rằng bạn có thể xác định một tập hợp các quy tắc cú pháp, với các mã thông báo, theo cách sau:

digit         [0-9]
letter        [a-zA-Z]

%%
"+"                  { return PLUS;       }
"-"                  { return MINUS;      }
"*"                  { return TIMES;      }
"/"                  { return SLASH;      }
"("                  { return LPAREN;     }
")"                  { return RPAREN;     }
";"                  { return SEMICOLON;  }
","                  { return COMMA;      }
"."                  { return PERIOD;     }
":="                 { return BECOMES;    }
"="                  { return EQL;        }
"<>"                 { return NEQ;        }
"<"                  { return LSS;        }
">"                  { return GTR;        }
"<="                 { return LEQ;        }
">="                 { return GEQ;        }
"begin"              { return BEGINSYM;   }
"call"               { return CALLSYM;    }
"const"              { return CONSTSYM;   }
"do"                 { return DOSYM;      }
"end"                { return ENDSYM;     }
"if"                 { return IFSYM;      }
"odd"                { return ODDSYM;     }
"procedure"          { return PROCSYM;    }
"then"               { return THENSYM;    }
"var"                { return VARSYM;     }
"while"              { return WHILESYM;   }

Dường như với tôi rằng lexer Flex đang trả về các chuỗi từ khóa \ token. Nhưng nó có thể là các hằng số trả về bằng số nhất định.

Nếu lexer sẽ trả về số, làm thế nào nó đọc được chuỗi ký tự? trả về một số là tốt cho các từ khóa duy nhất, nhưng làm thế nào bạn sẽ đối phó với một chuỗi? Lexer sẽ không phải chuyển đổi chuỗi thành số nhị phân và sau đó trình phân tích cú pháp sẽ chuyển đổi số trở lại thành chuỗi. Có vẻ hợp lý hơn (và dễ dàng hơn) đối với lexer để trả về các chuỗi, và sau đó cho phép trình phân tích cú pháp chuyển đổi bất kỳ chuỗi ký tự chuỗi số nào thành số thực.

Hoặc lexer có thể trả lại cả hai? Tôi đã cố gắng viết một từ vựng đơn giản trong c ++, cho phép bạn chỉ có một kiểu trả về cho các hàm của mình. Do đó dẫn tôi đến câu hỏi của tôi.

Để cô đọng câu hỏi của tôi thành một đoạn văn: Khi viết một từ vựng và giả sử rằng nó chỉ có thể trả về một loại dữ liệu (chuỗi hoặc số), đó sẽ là lựa chọn hợp lý hơn?


The lexer trả về những gì bạn bảo nó trả lại. Nếu thiết kế của bạn gọi cho số, thì nó sẽ trả về số. Rõ ràng, đại diện cho chuỗi ký tự sẽ đòi hỏi nhiều hơn thế. Xem thêm Đâyphải là công việc của Lucy để phân tích số và chuỗi không? Lưu ý rằng chuỗi ký tự thường không được coi là "Thành phần ngôn ngữ".
Robert Harvey

@RobertHarvey Vì vậy, bạn sẽ chuyển đổi chuỗi ký tự thành số nhị phân?.
Christian Dean

Theo tôi hiểu, mục đích của lexer là lấy các yếu tố ngôn ngữ (như từ khóa, toán tử, v.v.) và biến chúng thành mã thông báo. Như vậy, các chuỗi trích dẫn không được quan tâm đến từ vựng, bởi vì chúng không phải là các yếu tố ngôn ngữ. Mặc dù bản thân tôi chưa bao giờ viết một từ vựng, tôi sẽ tưởng tượng rằng chuỗi trích dẫn chỉ đơn giản được chuyển qua không thay đổi (bao gồm cả các trích dẫn).
Robert Harvey

Vì vậy, những gì bạn nói là lexer không đọc hoặc quan tâm đến chuỗi ký tự. Và vì vậy, trình phân tích cú pháp phải tìm những chuỗi ký tự này? Điều này rất khó hiểu.
Christian Dean

Bạn có thể muốn dành vài phút để đọc nó: en.wikipedia.org/wiki/Lexical_analysis
Robert Harvey

Câu trả lời:


10

Nói chung, nếu bạn đang xử lý một ngôn ngữ mặc dù từ vựng và phân tích cú pháp, bạn đã có định nghĩa về mã thông báo từ vựng của mình, ví dụ:

NUMBER ::= [0-9]+
ID     ::= [a-Z]+, except for keywords
IF     ::= 'if'
LPAREN ::= '('
RPAREN ::= ')'
COMMA  ::= ','
LBRACE ::= '{'
RBRACE ::= '}'
SEMICOLON ::= ';'
...

và bạn có một ngữ pháp cho trình phân tích cú pháp:

STATEMENT ::= IF LPAREN EXPR RPAREN STATEMENT
            | LBRACE STATEMENT BRACE
            | EXPR SEMICOLON
EXPR      ::= ID
            | NUMBER
            | ID LPAREN EXPRS RPAREN
...

Từ vựng của bạn lấy luồng đầu vào và tạo ra một luồng mã thông báo. Luồng mã thông báo được sử dụng bởi trình phân tích cú pháp để tạo ra một cây phân tích cú pháp. Trong một số trường hợp, chỉ cần biết loại mã thông báo là đủ (ví dụ: LPAREN, RBRACE, FOR), nhưng trong một số trường hợp, bạn sẽ cần giá trị thực được liên kết với mã thông báo. Chẳng hạn, khi bạn gặp mã thông báo ID, bạn sẽ muốn các ký tự thực sự tạo nên ID sau này khi bạn đang cố gắng tìm ra định danh nào bạn đang cố gắng tham chiếu.

Vì vậy, bạn thường có một cái gì đó ít nhiều như thế này:

enum TokenType {
  NUMBER, ID, IF, LPAREN, RPAREN, ...;
}

class Token {
  TokenType type;
  String value;
}

Vì vậy, khi lexer trả về mã thông báo, bạn sẽ biết nó thuộc loại nào (mà bạn cần để phân tích cú pháp) và chuỗi ký tự được tạo từ đó (mà sau này bạn sẽ cần để giải thích chuỗi và số bằng chữ, số nhận dạng, v.v.) Bạn có thể cảm thấy như bạn đang trả về hai giá trị, vì bạn đang trả về một loại tổng hợp rất đơn giản, nhưng bạn thực sự cần cả hai phần. Rốt cuộc, bạn muốn đối xử với các chương trình sau đây khác nhau:

if (2 > 0) {
  print("2 > 0");
}
if (0 > 2) {
  print("0 > 2");
}

Các mã này tạo ra cùng một chuỗi các loại mã thông báo : IF, LPAREN, SỐ, GREATER_THAN, SỐ, RPAREN, LBRACE, ID, LPAREN, STRING, RPAREN, SEMICOLON, RBRACE. Điều đó có nghĩa là họ cũng phân tích giống nhau. Nhưng khi bạn thực sự làm gì đó với cây phân tích, bạn sẽ quan tâm rằng giá trị của số thứ nhất là '2' (hoặc '0') và giá trị của số thứ hai là '0' (hoặc '2 ') và giá trị của chuỗi là' 2> 0 '(hoặc' 0> 2 ').


Tôi nhận được hầu hết những gì nói của bạn, nhưng như thế nào được mà String valuesẽ được điền? nó sẽ được lấp đầy bằng một chuỗi hoặc một số? Và ngoài ra, làm thế nào tôi sẽ xác định Stringloại?
Christian Dean

1
@ Mr.Python Trong trường hợp đơn giản nhất, đó chỉ là chuỗi các ký tự khớp với sản xuất từ ​​vựng. Vì vậy, nếu bạn thấy foo (23, "bar") , bạn sẽ nhận được mã thông báo [ID, "foo"], [LPAREN, "("], [SỐ, "23"], [COMMA, "," ], [CHUINGI, "" 23 ""], [RPAREN, ")"] . Bảo tồn thông tin đó có thể quan trọng. Hoặc bạn có thể thực hiện một cách tiếp cận khác và có giá trị có loại kết hợp có thể là chuỗi hoặc số, v.v. và chọn loại giá trị phù hợp dựa trên loại mã thông báo bạn có (ví dụ: khi loại mã thông báo là SỐ , sử dụng value.num và khi nó CHUINGI, hãy sử dụng value.str).
Joshua Taylor

@MrPython "Và còn nữa, tôi sẽ định nghĩa kiểu Chuỗi như thế nào?" Tôi đã viết từ một tư duy Java-ish. Nếu bạn đang làm việc trong C ++, bạn có thể sử dụng loại chuỗi của C ++ hoặc nếu bạn đang làm việc trong C, bạn có thể sử dụng char *. Vấn đề là liên kết với mã thông báo, bạn có giá trị tương ứng hoặc văn bản mà bạn có thể diễn giải để tạo ra giá trị.
Joshua Taylor

1
@ ollydbg23 đó là một lựa chọn và không phải là không hợp lý, nhưng nó làm cho hệ thống không nhất quán trong nội bộ. Ví dụ: nếu bạn muốn giá trị chuỗi của thị trấn cuối cùng mà bạn đã phân tích, giờ đây bạn phải kiểm tra rõ ràng giá trị null và sau đó sử dụng tra cứu mã thông báo ngược để tìm hiểu chuỗi đó sẽ là gì. Thêm vào đó, nó khớp nối chặt chẽ hơn giữa lexer và trình phân tích cú pháp; sẽ có thêm mã để cập nhật nếu LPAREN có thể khớp với nhiều chuỗi khác nhau.
Joshua Taylor

2
@ ollydbg23 Một trường hợp sẽ là một công cụ khai thác giả đơn giản. Nó đủ dễ để làm parse(inputStream).forEach(token -> print(token.string); print(' '))(nghĩa là chỉ cần in các giá trị chuỗi của các mã thông báo, được phân tách bằng dấu cách). Điều đó khá nhanh. Và ngay cả khi LPAREN chỉ có thể xuất phát từ "(", đó có thể là một chuỗi không đổi trong bộ nhớ, do đó, bao gồm một tham chiếu đến nó trong mã thông báo có thể không đắt hơn bao gồm cả tham chiếu null. Nói chung, tôi muốn viết mã không làm cho tôi trường hợp đặc biệt bất kỳ mã nào.
Joshua Taylor

6

Như đã nói trong tiêu đề, loại dữ liệu nào nên trả về / cung cấp cho trình phân tích cú pháp?

"Mã thông báo", rõ ràng. Một lexer tạo ra một luồng mã thông báo, vì vậy nó sẽ trả về một luồng mã thông báo .

Ông đã đề cập đến Flex, một từ vựng đã có sẵn và nói rằng việc viết 'quy tắc' với nó sẽ đơn giản hơn so với việc viết một từ vựng bằng tay.

Các từ vựng do máy tạo ra có lợi thế là bạn có thể tạo chúng nhanh chóng, điều này đặc biệt tiện dụng nếu bạn nghĩ rằng ngữ pháp từ vựng của bạn sẽ thay đổi rất nhiều. Chúng có nhược điểm là bạn thường không linh hoạt trong các lựa chọn thực hiện.

Điều đó nói rằng, ai quan tâm nếu nó "đơn giản"? Viết lexer thường không phải là phần khó!

Khi viết một từ vựng và giả sử rằng nó chỉ có thể trả về một loại dữ liệu (chuỗi hoặc số), đó sẽ là lựa chọn hợp lý hơn?

Cũng không. Một lexer thường có hoạt động "tiếp theo" trả về mã thông báo, vì vậy nó sẽ trả về mã thông báo . Mã thông báo không phải là một chuỗi hoặc một số. Đó là một mã thông báo.

Từ vựng cuối cùng tôi đã viết là một từ vựng "đầy đủ độ trung thực", nghĩa là nó đã trả lại một mã thông báo theo dõi vị trí của tất cả các khoảng trắng và các nhận xét - mà chúng tôi gọi là "trivia" - trong chương trình, cũng như mã thông báo. Trong lexer của tôi, một mã thông báo được xác định là:

  • Một loạt các câu đố hàng đầu
  • Một loại mã thông báo
  • Độ rộng mã thông báo trong các ký tự
  • Một loạt các câu đố nhỏ

Trivia được định nghĩa là:

  • Một loại câu đố - khoảng trắng, dòng mới, nhận xét, v.v.
  • Một chiều rộng đố trong các nhân vật

Vì vậy, nếu chúng ta có một cái gì đó như

    foo + /* comment */
/* another comment */ bar;

rằng sẽ Lex bốn thẻ với các loại thẻ Identifier, Plus, Identifier, Semicolon, và độ rộng 3, 1, 3, 1. Từ định danh đầu tiên có đố bao gồm lãnh đạo Whitespacecó chiều rộng 4 và trailing đố Whitespacevới chiều rộng của 1. Pluskhông có người đố hàng đầu và câu đố nhỏ bao gồm một khoảng trắng, một bình luận và một dòng mới. Mã định danh cuối cùng có một câu đố hàng đầu về một nhận xét và một khoảng trắng, v.v.

Với lược đồ này, mỗi ký tự trong tệp được tính vào đầu ra của lexer, đây là một thuộc tính tiện dụng cần có cho những thứ như tô màu cú pháp.

Tất nhiên, nếu bạn không cần những thứ lặt vặt thì bạn chỉ cần tạo mã thông báo hai thứ: loại và chiều rộng.

Bạn có thể nhận thấy rằng mã thông báo và câu đố chỉ chứa chiều rộng của chúng, không phải vị trí tuyệt đối của chúng trong mã nguồn. Đó là cố ý. Đề án như vậy có lợi thế:

  • Nó là nhỏ gọn trong bộ nhớ và định dạng dây
  • Nó cho phép tái lập lại các chỉnh sửa; Điều này rất hữu ích nếu lexer đang chạy bên trong IDE. Đó là, nếu bạn phát hiện một chỉnh sửa trong mã thông báo, bạn chỉ cần sao lưu từ khóa của mình vào một vài mã thông báo trước khi chỉnh sửa và bắt đầu lại từ khóa cho đến khi bạn đồng bộ hóa với luồng mã thông báo trước đó. Khi bạn nhập một ký tự, vị trí của mỗi mã thông báo sau khi ký tự đó thay đổi, nhưng thường chỉ có một hoặc hai mã thông báo thay đổi về chiều rộng, do đó bạn có thể sử dụng lại tất cả trạng thái đó.
  • Các ký tự chính xác của mỗi mã thông báo có thể dễ dàng được lấy bằng cách lặp qua luồng mã thông báo và theo dõi phần bù hiện tại. Một khi bạn có các ký tự chính xác thì bạn có thể dễ dàng trích xuất văn bản khi cần thiết.

Nếu bạn không quan tâm đến bất kỳ kịch bản nào trong số đó, thì mã thông báo có thể được biểu diễn dưới dạng một loại và một phần bù, thay vì một loại và chiều rộng.

Nhưng điều quan trọng nhất ở đây là: lập trình là nghệ thuật tạo ra sự trừu tượng hữu ích . Bạn đang thao túng mã thông báo, vì vậy hãy tạo ra sự trừu tượng hóa hữu ích đối với mã thông báo và sau đó bạn có thể chọn cho mình những chi tiết triển khai làm cơ sở cho nó.


3

Nói chung, bạn trả về một cấu trúc nhỏ có số biểu thị mã thông báo (hoặc giá trị enum để dễ sử dụng) và giá trị tùy chọn (chuỗi hoặc có thể là giá trị chung / templated). Một cách tiếp cận khác là trả về một loại dẫn xuất cho các phần tử cần mang thêm dữ liệu. Cả hai đều hơi khó chịu, nhưng giải pháp đủ tốt cho một vấn đề thực tế.


Bạn có ý nghĩa gì bởi sự khó chịu nhẹ ? Có phải chúng là cách không hiệu quả để có được giá trị chuỗi?
Christian Dean

@ Mr.Python - họ sẽ dẫn đến rất nhiều kiểm tra trước khi sử dụng mã, điều này không hiệu quả, nhưng moreso làm cho mã phức tạp hơn / dễ vỡ hơn một chút.
Telastyn

Tôi có một câu hỏi tương tự khi thiết kế một từ vựng trong C ++, tôi có thể trả về một Token *hoặc chỉ đơn giản là một Tokenhoặc một TokenPtrcon trỏ chung của Tokenlớp. Nhưng tôi cũng thấy một số lexer chỉ trả về một TokenType và lưu trữ giá trị chuỗi hoặc số trong các biến toàn cục hoặc tĩnh khác. Một câu hỏi khác là làm thế nào chúng ta có thể lưu trữ thông tin Vị trí, tôi có cần phải có cấu trúc Token có các trường TokenType, String và Location không? Cảm ơn.
ollydbg23

@ ollydbg23 - bất kỳ thứ gì trong số này có thể hoạt động. Tôi sẽ sử dụng một cấu trúc. Và đối với các ngôn ngữ không học, bạn sẽ sử dụng trình tạo trình phân tích cú pháp.
Telastyn

@Telastyn cảm ơn bạn đã trả lời. Bạn có nghĩa là một cấu trúc mã thông báo có thể là một cái gì đó như struct Token {TokenType id; std::string lexeme; int line; int column;}, phải không? Đối với chức năng công khai của Lexer, chẳng hạn như PeekToken(), chức năng có thể trả về một Token *hoặc TokenPtr. Tôi nghĩ rằng trong một thời gian, nếu hàm chỉ trả về TokenType, thì Trình phân tích cú pháp cố gắng lấy thông tin khác về Mã thông báo như thế nào? Vì vậy, một con trỏ như datatype được ưa thích để trả về từ hàm đó. Bất kỳ ý kiến ​​về ý tưởng của tôi? Cảm ơn
ollydbg23
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.