Đó là một chủ đề lớn nhưng thay vì gạt bạn ra một cách hào hoa "đi đọc sách đi, nhóc" thay vào đó tôi sẽ vui lòng đưa ra gợi ý cho bạn để giúp bạn quấn đầu xung quanh nó.
Hầu hết các trình biên dịch và / hoặc trình thông dịch đều hoạt động như thế này:
Tokenize : Quét văn bản mã và chia nó thành một danh sách các mã thông báo.
Bước này có thể khó khăn vì bạn không thể phân tách chuỗi trên các khoảng trắng, bạn phải nhận ra đó if (bar) foo += "a string";
là danh sách 8 mã thông báo: WORD, OPEN_PAREN, WORD, CLOSE_PAREN, WORD, ASIGNMENT_ADD, STRING_LITITH, TERMINATOR. Như bạn có thể thấy, chỉ cần tách mã nguồn trên các khoảng trắng sẽ không hoạt động, bạn phải đọc từng ký tự thành một chuỗi, vì vậy nếu bạn gặp một ký tự chữ và số, bạn tiếp tục đọc các ký tự cho đến khi bạn nhấn một ký tự không phải là chữ và số chỉ cần đọc là một WORD để được phân loại thêm sau này. Bạn có thể tự quyết định mức độ tokenizer của mình như thế nào: liệu nó có nuốt như OPEN_QUOTE, UNPARSED_TEXT, CLOSE_QUOTE hay bất cứ điều gì, đây chỉ là một trong nhiều lựa chọn mà bạn phải tự quyết định khi mã hóa nó."a string"
thông báo như một mã thông báo có tên là STRING_LITITH để được phân tích cú pháp sau hay không"a string"
Lex : Vì vậy, bây giờ bạn có một danh sách các mã thông báo. Bạn có thể đã gắn thẻ một số mã thông báo với phân loại mơ hồ như WORD vì trong lần đầu tiên bạn không mất quá nhiều nỗ lực để tìm ra bối cảnh của từng chuỗi ký tự. Vì vậy, bây giờ hãy đọc lại danh sách mã thông báo của bạn và phân loại lại từng mã thông báo mơ hồ với loại mã thông báo cụ thể hơn dựa trên các từ khóa trong ngôn ngữ của bạn. Vì vậy, bạn có một WORD như "if" và "if" nằm trong danh sách các từ khóa đặc biệt được gọi là biểu tượng IF để bạn thay đổi loại biểu tượng của mã thông báo đó từ WORD thành IF và bất kỳ WORD nào không có trong danh sách từ khóa đặc biệt của bạn , chẳng hạn như WORD foo, là một IDENTIFIER.
Phân tích : Vì vậy, bây giờ bạn quay if (bar) foo += "a string";
một danh sách các thẻ lexed trông như thế này: NẾU OPEN_PAREN số nhận dạng CLOSE_PAREN IDENTIFIER ASIGN_ADD STRING_LITERAL TERMINATOR. Bước này là nhận ra các chuỗi mã thông báo dưới dạng câu lệnh. Đây là phân tích cú pháp. Bạn làm điều này bằng cách sử dụng một ngữ pháp như:
TUYÊN BỐ: = ASIGN_EXPRESSION | NỀN TẢNG
IF_STATMENT: = IF, PAREN_EXPRESSION, STATMENT
ASIGN_EXPRESSION: = IDENTIFIER, ASIGN_OP, VALUE
PAREN_EXPRESSSION: = OPEN_PAREN, GIÁ TRỊ, CLOSE_PAREN
GIÁ TRỊ: = IDENTIFIER | STRING_LITITH | PAREN_EXPRESSION
ASIGN_OP: = THIẾT BỊ | ASIGN_ADD | ASIGN_SUBTRACT | ASIGN_MULT
Các sản phẩm sử dụng "|" giữa các thuật ngữ có nghĩa là "khớp với bất kỳ từ nào trong số này", nếu nó có dấu phẩy giữa các thuật ngữ thì có nghĩa là "khớp với chuỗi thuật ngữ này"
bạn sử dụng cái này như thế nào? Bắt đầu với mã thông báo đầu tiên, hãy thử kết hợp chuỗi mã thông báo của bạn với các sản phẩm này. Vì vậy, trước tiên, bạn cố gắng khớp danh sách mã thông báo của mình với TUYÊN BỐ, vì vậy bạn đã đọc quy tắc TUYÊN BỐ và thông báo "TUYÊN BỐ là một ASIGN_EXPRESSION hoặc IF_STATMENT" để bạn tìm cách khớp với ASIGN_EXPRESSION trước, vì vậy bạn hãy tìm quy tắc ngữ pháp cho ASIGN_EXPRESSION và thông báo "ASIGN_EXPRESSION là IDENTIFIER, theo sau là ASIGN_OP, theo sau là VALUE, vì vậy bạn tra cứu quy tắc ngữ pháp cho IDENTIFIER và bạn thấy không có lỗi ngữ pháp nào cho IDENTIFIER vì vậy điều đó có nghĩa là IDENTifyER là" thiết bị đầu cuối " phân tích cú pháp để khớp với nó để bạn có thể thử khớp trực tiếp với mã thông báo của mình. Nhưng mã thông báo nguồn đầu tiên của bạn là IF và IF không giống với IDENTIFIER nên không khớp. Gì bây giờ? Bạn quay lại quy tắc TUYÊN BỐ và cố gắng khớp với thuật ngữ tiếp theo: IF_STATMENT. Bạn tra cứu IF_STATMENT, nó bắt đầu bằng IF, tra cứu IF, IF là thiết bị đầu cuối, so sánh thiết bị đầu cuối với mã thông báo đầu tiên của bạn, mã thông báo IF phù hợp, tiếp tục tuyệt vời, thuật ngữ tiếp theo là PAREN_EXPRESSION, tra cứu PAREN_EXPRESSION, đây không phải là thiết bị đầu cuối, đó là thuật ngữ đầu tiên, PAREN_EXPRESSION bắt đầu với OPEN_PAREN, tra cứu OPEN_PAREN, đó là một thiết bị đầu cuối, khớp OPEN_PAREN với mã thông báo tiếp theo của bạn, nó khớp, .... v.v.
Cách dễ nhất để tiếp cận bước này là bạn có một hàm gọi là parse () mà bạn chuyển mã thông báo mã nguồn mà bạn đang cố khớp và thuật ngữ ngữ pháp bạn đang cố gắng khớp với nó. Nếu thuật ngữ ngữ pháp không phải là một thiết bị đầu cuối thì bạn lặp lại: bạn gọi parse () một lần nữa chuyển nó cùng mã thông báo nguồn và thuật ngữ đầu tiên của quy tắc ngữ pháp này. Đây là lý do tại sao nó được gọi là "trình phân tích cú pháp gốc đệ quy" Hàm parse () trả về (hoặc sửa đổi) vị trí hiện tại của bạn khi đọc mã thông báo nguồn, về cơ bản nó sẽ trả lại mã thông báo cuối cùng trong chuỗi phù hợp và bạn tiếp tục cuộc gọi tiếp theo phân tích () từ đó.
Mỗi lần phân tích cú pháp () khớp với một sản phẩm như ASIGN_EXPRESSION, bạn tạo một cấu trúc đại diện cho đoạn mã đó. Cấu trúc này chứa các tham chiếu đến các mã thông báo nguồn gốc. Bạn bắt đầu xây dựng một danh sách các cấu trúc này. Chúng ta sẽ gọi toàn bộ cấu trúc này là Cây Cú pháp Trừu tượng (AST)
Biên dịch và / hoặc Thực thi : Đối với một số sản phẩm nhất định trong ngữ pháp của bạn, bạn đã tạo các hàm xử lý mà nếu được cung cấp cấu trúc AST, nó sẽ biên dịch hoặc thực thi đoạn AST đó.
Vì vậy, hãy nhìn vào mảnh AST của bạn có loại ASIGN_ADD. Vì vậy, với tư cách là một trình thông dịch, bạn có hàm ASIGN_ADD_execute (). Hàm này được truyền dưới dạng một phần của AST tương ứng với cây phân tích cú pháp foo += "a string"
, vì vậy hàm này nhìn vào cấu trúc đó và nó biết rằng thuật ngữ đầu tiên trong cấu trúc phải là IDENTIFIER và thuật ngữ thứ hai là VALUE, vì vậy ASIGN_ADD_execute () chuyển thuật ngữ VALUE cho hàm VALUE_eval () trả về một đối tượng biểu thị giá trị được đánh giá trong bộ nhớ, sau đó ASIGN_ADD_execute () thực hiện tra cứu "foo" trong bảng biến của bạn và lưu trữ một tham chiếu đến bất cứ thứ gì được trả về bởi eval_value () chức năng.
Đó là một thông dịch viên. Thay vào đó, một trình biên dịch sẽ có các hàm xử lý dịch AST thành mã byte hoặc mã máy thay vì thực thi nó.
Bước 1 đến 3 và một số 4, có thể được thực hiện dễ dàng hơn bằng cách sử dụng các công cụ như Flex và Bison. (còn gọi là Lex và Yacc) nhưng tự mình viết một thông dịch viên từ đầu có lẽ là bài tập có sức mạnh nhất mà bất kỳ lập trình viên nào cũng có thể đạt được. Tất cả các thách thức lập trình khác có vẻ tầm thường sau khi lên đỉnh này.
Lời khuyên của tôi là bắt đầu nhỏ: một ngôn ngữ nhỏ, với một ngữ pháp nhỏ, và thử phân tích cú pháp và thực hiện một vài câu đơn giản, sau đó phát triển từ đó.
Đọc những điều này, và chúc may mắn!
http://www.iro.umontreal.ca/~felipe/IFT2030-Automne2002/Complements/tinyc.c
http://en.wikipedia.org/wiki/Recursive_descent_parser
lex
,yacc
vàbison
.