Làm thế nào chính xác là một cây cú pháp trừu tượng được tạo ra?


47

Tôi nghĩ rằng tôi hiểu mục tiêu của AST và tôi đã xây dựng một vài cấu trúc cây trước đây, nhưng không bao giờ là AST. Tôi chủ yếu bối rối vì các nút là văn bản chứ không phải số, vì vậy tôi không thể nghĩ ra một cách hay để nhập mã thông báo / chuỗi khi tôi phân tích một số mã.

Ví dụ, khi tôi nhìn vào sơ đồ của AST, biến và giá trị của nó là các nút lá thành một dấu bằng. Điều này có ý nghĩa hoàn hảo với tôi, nhưng tôi sẽ thực hiện việc này như thế nào? Tôi đoán tôi có thể làm điều đó theo từng trường hợp, để khi tôi vấp phải một "=" Tôi sử dụng nó như một nút và thêm giá trị được phân tích cú pháp trước "=" làm lá. Điều đó có vẻ sai, bởi vì tôi có thể phải tạo ra hàng tấn hàng tấn thứ, tùy thuộc vào cú pháp.

Và rồi tôi gặp một vấn đề khác, cái cây đi qua như thế nào? Tôi có đi hết chiều cao và quay trở lại một nút khi tôi chạm đáy không, và làm tương tự cho hàng xóm của nó?

Tôi đã thấy hàng tấn sơ đồ trên AST, nhưng tôi không thể tìm thấy một ví dụ khá đơn giản về một trong mã, có thể sẽ giúp ích.


Khái niệm chính bạn đang thiếu là đệ quy . Đệ quy là loại phản trực giác và nó khác với mọi người học khi cuối cùng sẽ 'nhấp chuột' với họ, nhưng không có đệ quy, đơn giản là không có cách nào để hiểu phân tích cú pháp (và cả rất nhiều chủ đề tính toán khác).
Kilian Foth

Tôi nhận được đệ quy, tôi chỉ nghĩ rằng thật khó để thực hiện nó trong trường hợp này. Tôi thực sự muốn sử dụng đệ quy và tôi đã kết thúc với rất nhiều trường hợp không hoạt động cho một giải pháp chung. Câu trả lời của Gdhoward đang giúp tôi rất nhiều ngay bây giờ.
Howcan

Nó có thể là bài tập để xây dựng một máy tính RPN như một bài tập. Nó sẽ không trả lời câu hỏi của bạn nhưng có thể dạy một số kỹ năng cần thiết.

Tôi thực sự đã xây dựng một Máy tính RPN trước đây. Các câu trả lời đã giúp tôi rất nhiều và tôi nghĩ bây giờ tôi có thể tạo ra một AST cơ bản. Cảm ơn!
Howcan

Câu trả lời:


47

Câu trả lời ngắn gọn là bạn sử dụng ngăn xếp. Đây là một ví dụ tốt, nhưng tôi sẽ áp dụng nó cho AST.

FYI, đây là thuật toán Shunting-Yard của Edsger Dijkstra .

Trong trường hợp này, tôi sẽ sử dụng ngăn xếp toán tử và ngăn xếp biểu thức. Vì các số được coi là biểu thức trong hầu hết các ngôn ngữ, tôi sẽ sử dụng ngăn xếp biểu thức để lưu trữ chúng.

class ExprNode:
    char c
    ExprNode operand1
    ExprNode operand2

    ExprNode(char num):
        c = num
        operand1 = operand2 = nil

    Expr(char op, ExprNode e1, ExprNode e2):
        c = op
        operand1 = e1
        operand2 = e2

# Parser
ExprNode parse(string input):
    char c
    while (c = input.getNextChar()):
        if (c == '('):
            operatorStack.push(c)

        else if (c.isDigit()):
            exprStack.push(ExprNode(c))

        else if (c.isOperator()):
            while(operatorStack.top().precedence >= c.precedence):
                operator = operatorStack.pop()
                # Careful! The second operand was pushed last.
                e2 = exprStack.pop()
                e1 = exprStack.pop()
                exprStack.push(ExprNode(operator, e1, e2))

            operatorStack.push(c)

        else if (c == ')'):
            while (operatorStack.top() != '('):
                operator = operatorStack.pop()
                # Careful! The second operand was pushed last.
                e2 = exprStack.pop()
                e1 = exprStack.pop()
                exprStack.push(ExprNode(operator, e1, e2))

            # Pop the '(' off the operator stack.
            operatorStack.pop()

        else:
            error()
            return nil

    # There should only be one item on exprStack.
    # It's the root node, so we return it.
    return exprStack.pop()

(Xin vui lòng về mã của tôi. Tôi biết nó không mạnh mẽ; nó chỉ được coi là mã giả.)

Dù sao, như bạn có thể thấy từ mã, các biểu thức tùy ý có thể là toán hạng cho các biểu thức khác. Nếu bạn có đầu vào sau:

5 * 3 + (4 + 2 % 2 * 8)

mã tôi đã viết sẽ tạo ra AST này:

     +
    / \
   /   \
  *     +
 / \   / \
5   3 4   *
         / \
        %   8
       / \
      2   2

Và sau đó khi bạn muốn tạo mã cho AST đó, bạn thực hiện một Traversal Tree Order Post . Khi bạn truy cập một nút lá (có một số), bạn tạo một hằng số vì trình biên dịch cần biết các giá trị toán hạng. Khi bạn truy cập một nút với một toán tử, bạn tạo ra lệnh thích hợp từ toán tử. Ví dụ: toán tử '+' cung cấp cho bạn lệnh "thêm".


Điều này làm việc cho các nhà khai thác đã kết hợp từ trái sang phải, không phải từ trái sang phải.
Simon

@Simon, sẽ cực kỳ đơn giản để thêm khả năng cho các nhà khai thác từ phải sang trái. Đơn giản nhất sẽ là thêm một bảng tra cứu và nếu một toán tử từ phải sang trái, chỉ cần đảo ngược thứ tự của các toán hạng.
Gavin Howard

4
@Simon Nếu bạn muốn hỗ trợ cả hai, tốt hơn hết bạn nên tìm kiếm thuật toán sân shunting trong vinh quang đầy đủ của nó. Khi các thuật toán đi, đó là một cracker tuyệt đối.
biziclop

19

Có một sự khác biệt đáng kể giữa cách AST thường được mô tả trong thử nghiệm (một cây có số / biến ở các nút lá và ký hiệu ở các nút bên trong) và cách nó được thực hiện.

Việc triển khai điển hình của AST (bằng ngôn ngữ OO) sử dụng đa hình. Các nút trong AST thường được triển khai với nhiều lớp khác nhau, tất cả đều xuất phát từ một ASTNodelớp chung . Đối với mỗi cấu trúc cú pháp trong ngôn ngữ bạn đang xử lý, sẽ có một lớp để biểu thị cấu trúc đó trong AST, chẳng hạn như ConstantNode(đối với các hằng số, chẳng hạn như 0x10hoặc 42), VariableNode(đối với tên biến), AssignmentNode(đối với các thao tác gán), ExpressionNode(cho chung biểu thức), v.v ...
Mỗi loại nút cụ thể chỉ định nếu nút đó có con, bao nhiêu và có thể thuộc loại nào. A ConstantNodethường sẽ không có con, một AssignmentNodesẽ có hai và ExpressionBlockNodecó thể có bất kỳ số lượng con.

AST được xây dựng bởi trình phân tích cú pháp, người biết cấu trúc nào nó vừa phân tích cú pháp, vì vậy nó có thể xây dựng đúng loại AST Node.

Khi đi qua AST, tính đa hình của các nút thực sự phát huy tác dụng. Cơ sở ASTNodexác định các hoạt động có thể được thực hiện trên các nút và mỗi loại nút cụ thể thực hiện các hoạt động đó theo cách cụ thể cho cấu trúc ngôn ngữ cụ thể đó.


9

Xây dựng AST từ văn bản nguồn là " phân tích " đơn giản . Làm thế nào chính xác nó được thực hiện phụ thuộc vào ngôn ngữ chính thức được phân tích cú pháp và việc thực hiện. Bạn có thể sử dụng các trình tạo trình phân tích cú pháp như menhir (cho Ocaml) , GNU bisonvới flexhoặc ANTLR, v.v. Nó thường được thực hiện "thủ công" bằng cách mã hóa một số trình phân tích cú pháp gốc đệ quy (xem câu trả lời này giải thích tại sao). Khía cạnh ngữ cảnh của phân tích cú pháp thường được thực hiện ở nơi khác (bảng biểu tượng, thuộc tính, ....).

Tuy nhiên, trong thực tế AST phức tạp hơn nhiều so với những gì bạn tin tưởng. Chẳng hạn, trong một trình biên dịch như GCC , AST giữ thông tin vị trí nguồn và một số thông tin gõ. Đọc về Cây chung trong GCC và xem bên trong gcc / tree.def của nó . BTW, hãy nhìn vào bên trong GCC MELT (mà tôi đã thiết kế và triển khai), nó có liên quan đến câu hỏi của bạn.


Tôi đang tạo một trình thông dịch Lua để phân tích văn bản nguồn và chuyển đổi trong một mảng trong JS. Tôi có thể coi nó là AST không? Tôi phải làm một cái gì đó như thế này: --My comment #1 print("Hello, ".."world.") chuyển đổi thành `[{" type ":" - "," content ":" Nhận xét của tôi # 1 "}, {" type ":" call "," name ":" in "," đối số ": [[{" loại ":" str "," hành động ":" .. "," nội dung ":" Xin chào, "}, {" loại ":" str "," nội dung ": "thế giới." }]]}] `Tôi nghĩ rằng nó đơn giản hơn nhiều so với bất kỳ ngôn ngữ nào khác!
Hydroper

@TheProHands Đây sẽ được coi là mã thông báo, không phải AST.
YoYoYonnY

2

Tôi biết câu hỏi này đã hơn 4 tuổi nhưng tôi cảm thấy nên thêm một câu trả lời chi tiết hơn.

Cây Syntax trừu tượng được tạo ra không khác với các cây khác; tuyên bố đúng hơn trong trường hợp này là các nút Syntax Tree có số lượng nút rất NHƯ NHU CẦU.

Một ví dụ là các biểu thức nhị phân như 1 + 2 Một biểu thức đơn giản như thế sẽ tạo ra một nút gốc duy nhất giữ một nút phải và trái chứa dữ liệu về các số. Trong ngôn ngữ C, nó trông giống như

struct ASTNode;
union SyntaxNode {
    int64_t         llVal;
    uint64_t        ullVal;
    struct {
        struct ASTNode *left, *right;
    } BinaryExpr;
};

enum SyntaxNodeType {
    AST_IntVal, AST_Add, AST_Sub, AST_Mul, AST_Div, AST_Mod,
};

struct ASTNode {
    union SyntaxNode *Data;
    enum SyntaxNodeType Type;
};

Câu hỏi của bạn cũng là làm thế nào để đi qua? Di chuyển trong trường hợp này được gọi là Nút thăm . Truy cập từng nút yêu cầu bạn sử dụng từng loại nút để xác định cách đánh giá dữ liệu của từng nút Cú pháp.

Đây là một ví dụ khác về C trong đó tôi chỉ cần in nội dung của từng nút:

void AST_PrintNode(const ASTNode *node)
{
    if( !node )
        return;

    char *opername = NULL;
    switch( node->Type ) {
        case AST_IntVal:
            printf("AST Integer Literal - %lli\n", node->Data->llVal);
            break;
        case AST_Add:
            if( !opername )
                opername = "+";
        case AST_Sub:
            if( !opername )
                opername = "-";
        case AST_Mul:
            if( !opername )
                opername = "*";
        case AST_Div:
            if( !opername )
                opername = "/";
        case AST_Mod:
            if( !opername )
                opername = "%";
            printf("AST Binary Expr - Oper: \'%s\' Left:\'%p\' | Right:\'%p\'\n", opername, node->Data->BinaryExpr.left, node->Data->BinaryExpr.right);
            AST_PrintNode(node->Data->BinaryExpr.left); // NOTE: Recursively Visit each node.
            AST_PrintNode(node->Data->BinaryExpr.right);
            break;
    }
}

Lưu ý cách hàm truy cập đệ quy từng nút theo loại nút chúng ta đang xử lý.

Hãy thêm một ví dụ phức tạp hơn, một ifcấu trúc câu lệnh! Hãy nhớ lại rằng nếu các câu lệnh cũng có thể có một mệnh đề khác tùy chọn. Hãy thêm câu lệnh if-other vào cấu trúc nút ban đầu của chúng tôi. Hãy nhớ rằng nếu bản thân các câu lệnh cũng có thể có các câu lệnh if, do đó, một loại đệ quy trong hệ thống nút của chúng ta có thể xảy ra. Các câu lệnh khác là tùy chọn để elsestmttrường có thể là NULL mà hàm khách truy cập đệ quy có thể bỏ qua.

struct ASTNode;
union SyntaxNode {
    int64_t         llVal;
    uint64_t        ullVal;
    struct {
        struct ASTNode *left, *right;
    } BinaryExpr;
    struct {
        struct ASTNode *expr, *stmt, *elsestmt;
    } IfStmt;
};

enum SyntaxNodeType {
    AST_IntVal, AST_Add, AST_Sub, AST_Mul, AST_Div, AST_Mod, AST_IfStmt, AST_ElseStmt, AST_Stmt
};

struct ASTNode {
    union SyntaxNode *Data;
    enum SyntaxNodeType Type;
};

trở lại trong chức năng in của khách truy cập nút được gọi AST_PrintNode, chúng ta có thể điều chỉnh ifcâu lệnh AST xây dựng bằng cách thêm mã C này:

case AST_IfStmt:
    puts("AST If Statement\n");
    AST_PrintNode(node->Data->IfStmt.expr);
    AST_PrintNode(node->Data->IfStmt.stmt);
    AST_PrintNode(node->Data->IfStmt.elsestmt);
    break;

Đơn giản vậy thôi! Tóm lại, Cây Cú pháp không khác gì một cây liên kết được gắn thẻ của cây và chính dữ liệu của nó!

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.