แผนผังต้นไม้บทคัดย่อสร้างขึ้นได้อย่างไร?


47

ฉันคิดว่าฉันเข้าใจเป้าหมายของ AST และฉันได้สร้างโครงสร้างต้นไม้สองสามครั้งก่อนหน้านี้ แต่ไม่เคยเป็น AST ฉันสับสนเป็นส่วนใหญ่เพราะโหนดเป็นข้อความและไม่ใช่ตัวเลขดังนั้นฉันจึงไม่สามารถคิดวิธีที่ดีในการป้อนโทเค็น / สตริงได้เนื่องจากฉันกำลังวิเคราะห์รหัส

ตัวอย่างเช่นเมื่อฉันดูไดอะแกรมของ AST ตัวแปรและค่าของมันคือโหนดปมถึงเครื่องหมายเท่ากับ มันสมเหตุสมผลดีสำหรับฉัน แต่ฉันจะใช้สิ่งนี้อย่างไร ฉันเดาว่าฉันสามารถทำได้เป็นกรณี ๆ ไปดังนั้นเมื่อฉันสะดุด "=" ฉันใช้มันเป็นโหนดและเพิ่มค่าการแจงก่อน "=" เป็นใบไม้ ดูเหมือนว่าผิดเพราะฉันอาจต้องทำเรื่องเป็นตัน ๆ ขึ้นอยู่กับวากยสัมพันธ์

จากนั้นฉันก็พบปัญหาอีกอย่างต้นไม้จะผ่านไปได้อย่างไร ฉันจะลงไปตามความสูงและกลับไปที่โหนดเมื่อฉันไปถึงจุดต่ำสุดแล้วทำแบบเดียวกันกับเพื่อนบ้านหรือไม่

ฉันเห็นไดอะแกรมมากมายบน AST แต่ฉันไม่สามารถหาตัวอย่างง่ายๆของรหัสได้ซึ่งอาจช่วยได้


แนวคิดสำคัญที่คุณกำลังขาดหายไปคือการเรียกซ้ำ การเรียกซ้ำเป็นวิธีที่ใช้ง่ายและมันก็แตกต่างกันไปสำหรับผู้เรียนทุกคนเมื่อในที่สุดจะ 'คลิก' กับพวกเขา แต่ไม่มีการเรียกซ้ำไม่มีวิธีที่จะเข้าใจการแยกวิเคราะห์ (และหัวข้อการคำนวณอื่น ๆ ทั้งหมด)
Kilian Foth

ฉันได้รับการสอบถามซ้ำฉันแค่คิดว่ามันยากที่จะนำไปใช้ในกรณีนี้ ฉันต้องการใช้การสอบถามซ้ำและลงเอยด้วยกรณีจำนวนมากที่ไม่สามารถใช้กับโซลูชันทั่วไปได้ คำตอบของ Gdhoward กำลังช่วยฉันมากมายตอนนี้
Howcan

อาจเป็นการฝึกเพื่อสร้างเครื่องคิดเลข RPNเป็นแบบฝึกหัด มันจะไม่ตอบคำถามของคุณ แต่อาจสอนทักษะที่จำเป็นบางอย่าง

ฉันเคยสร้าง RPN Calculator มาก่อน คำตอบช่วยฉันได้มากและฉันคิดว่าฉันสามารถสร้าง AST ขั้นพื้นฐานได้แล้วตอนนี้ ขอบคุณ!
Howcan

คำตอบ:


47

คำตอบสั้น ๆ คือคุณใช้สแต็ค นี่เป็นตัวอย่างที่ดี แต่ฉันจะใช้กับ AST

FYI นี้เป็น Edsger ของ Dijkstra แบ่งลานอัลกอริทึม

ในกรณีนี้ฉันจะใช้ตัวดำเนินการสแต็กและนิพจน์สแต็ก เนื่องจากตัวเลขถือว่าเป็นนิพจน์ในภาษาส่วนใหญ่ฉันจะใช้นิพจน์สแต็กเพื่อเก็บไว้

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()

(โปรดเป็นคนดีเกี่ยวกับรหัสของฉันฉันรู้ว่ามันไม่แข็งแกร่งมันควรจะเป็นรหัสเทียม)

อย่างไรก็ตามอย่างที่คุณเห็นจากโค้ดนิพจน์โดยพลการสามารถถูกดำเนินการกับนิพจน์อื่นได้ หากคุณมีอินพุตต่อไปนี้:

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

รหัสที่ฉันเขียนจะผลิต AST นี้:

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

และจากนั้นเมื่อคุณต้องการที่จะผลิตรหัสสำหรับ AST ที่คุณทำโพสต์สั่งซื้อต้นไม้ข้าม เมื่อคุณเยี่ยมชมโหนดใบ (มีตัวเลข) คุณจะสร้างค่าคงที่เนื่องจากคอมไพเลอร์จำเป็นต้องรู้ค่าตัวถูกดำเนินการ เมื่อคุณเยี่ยมชมโหนดด้วยโอเปอเรเตอร์คุณจะสร้างคำสั่งที่เหมาะสมจากโอเปอเรเตอร์ ตัวอย่างเช่นตัวดำเนินการ '+' ให้คำสั่ง "เพิ่ม" ให้คุณ


วิธีนี้ใช้ได้กับผู้ให้บริการที่มีการเชื่อมโยงจากซ้ายไปขวาไม่ใช่จากขวาไปซ้าย
Simon

@Simon มันจะง่ายมากในการเพิ่มความสามารถสำหรับผู้ประกอบการจากขวาไปซ้าย ที่ง่ายที่สุดคือการเพิ่มตารางค้นหาและถ้าผู้ประกอบการจากขวาไปซ้ายเพียงแค่ย้อนกลับลำดับของตัวถูกดำเนินการ
Gavin Howard

4
@Simon ถ้าคุณต้องการที่จะสนับสนุนทั้งสองอย่างคุณจะดีกว่าที่จะมองหาอัลกอริทึมการหลบหลีกในความรุ่งเรืองอย่างเต็มรูปแบบ เมื่ออัลกอริธึมนั่นคือแคร็กเกอร์เด็ดขาด
biziclop

19

มีความแตกต่างอย่างมีนัยสำคัญระหว่างวิธีที่ AST แสดงโดยทั่วไปในการทดสอบ (ต้นไม้ที่มีตัวเลข / ตัวแปรที่โหนดใบไม้และสัญลักษณ์ที่โหนดภายใน) และวิธีการนำไปใช้จริง

การนำไปปฏิบัติโดยทั่วไปของ AST (ในภาษา OO) ทำให้การใช้งานที่หลากหลายของ polymorphism โดยทั่วไปแล้วโหนดใน AST จะถูกนำไปใช้กับคลาสที่หลากหลายซึ่งล้วนมาจากASTNodeคลาสทั่วไป สำหรับโครงสร้างการสร้างประโยคในภาษาที่คุณกำลังประมวลผลจะมีคลาสสำหรับการแสดงโครงสร้างนั้นใน AST เช่นConstantNode(สำหรับค่าคงที่เช่น0x10หรือ42), VariableNode(สำหรับชื่อตัวแปร), AssignmentNode(สำหรับการมอบหมายงานทั่วไป), ExpressionNode(สำหรับทั่วไป นิพจน์) ฯลฯ
แต่ละชนิดของโหนดที่ระบุจะระบุว่าโหนดนั้นมีลูกหรือไม่และมีประเภทใด ConstantNodeโดยทั่วไปจะไม่มีลูกเป็นAssignmentNodeจะมีสองและExpressionBlockNodeสามารถมีจำนวนของเด็ก ๆ

AST ได้รับการสร้างโดยตัวแยกวิเคราะห์ที่รู้ว่าโครงสร้างใดที่มีการแยกวิเคราะห์ดังนั้นมันจึงสามารถสร้าง AST Node ที่เหมาะสมได้

เมื่อสำรวจ AST ความแตกต่างของโหนดจะเข้าสู่การเล่นจริงๆ ฐานจะASTNodeกำหนดการดำเนินการที่สามารถดำเนินการได้บนโหนดและแต่ละประเภทของโหนดที่ระบุจะใช้การดำเนินการเหล่านั้นในวิธีเฉพาะสำหรับการสร้างภาษานั้น ๆ


9

การสร้าง AST จากข้อความต้นฉบับคือการ " แยก " อย่างง่าย วิธีการทำนั้นขึ้นอยู่กับภาษาทางการและการดำเนินการแยกวิเคราะห์ คุณสามารถใช้เครื่องมือสร้าง parser เช่นmenhir (สำหรับ Ocaml) , GNU bisonกับflex, หรือ ANTLRฯลฯ ฯลฯ มันมักจะทำ "ด้วยตนเอง" โดยการเข้ารหัสparser ที่สืบเชื้อสายซ้ำ ๆ (ดูคำตอบนี้เพื่ออธิบายว่าทำไม) ด้านบริบทของการแยกวิเคราะห์มักจะทำที่อื่น (ตารางสัญลักษณ์คุณลักษณะ ... )

อย่างไรก็ตามในทางปฏิบัติ AST นั้นซับซ้อนกว่าสิ่งที่คุณเชื่อ ตัวอย่างเช่นในคอมไพเลอร์เช่นGCC AST จะเก็บข้อมูลตำแหน่งต้นทางและข้อมูลการพิมพ์บางอย่าง อ่านข้อมูลเกี่ยวกับทั่วไปต้นไม้ใน GCC และดูภายในของgcc / tree.def BTW ดูในGCC MELT (ซึ่งฉันได้ออกแบบและนำไปใช้) ซึ่งเกี่ยวข้องกับคำถามของคุณ


ฉันกำลังทำล่าม Lua เพื่อแยกข้อความต้นฉบับและแปลงในอาร์เรย์ใน JS ฉันสามารถพิจารณาเป็น AST ได้หรือไม่ ฉันควรจะทำสิ่งนี้: --My comment #1 print("Hello, ".."world.") เปลี่ยนเป็น `[{" type ":" - "," content ":" ความคิดเห็นของฉัน # 1 "}, {" ประเภท ":" โทร "," ชื่อ ":" พิมพ์ "," อาร์กิวเมนต์ ": [[" พิมพ์ ":" str "," action ":" .. "," เนื้อหา ":" Hello, "}, {" พิมพ์ ":" str "," เนื้อหา ": "โลก." }]]}] `ฉันคิดว่ามันง่ายกว่าใน JS มากกว่าภาษาอื่น ๆ !
Hydroper

@TheProHands นี่จะถือว่าเป็นโทเค็นไม่ใช่ AST
YoYoYonnY

2

ฉันรู้ว่าคำถามนี้มีอายุ 4 ปีขึ้นไป แต่ฉันรู้สึกว่าฉันควรเพิ่มคำตอบที่ละเอียดยิ่งขึ้น

บทคัดย่อไวยากรณ์ต้นไม้ถูกสร้างขึ้นไม่แตกต่างจากต้นไม้อื่น ข้อความที่เป็นจริงมากขึ้นในกรณีนี้คือโหนดต้นไม้ไวยากรณ์มีจำนวนโหนดที่หลากหลายตามที่ต้องการ

ตัวอย่างคือนิพจน์ไบนารีเช่นนิพจน์อย่าง1 + 2 ง่ายที่จะสร้างโหนดรูทเดียวที่ถือโหนดขวาและซ้ายที่เก็บข้อมูลเกี่ยวกับตัวเลข ในภาษา C ดูเหมือนว่า

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 ที่ฉันเพียงพิมพ์เนื้อหาของแต่ละโหนด:

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;
    }
}

ขอให้สังเกตว่าฟังก์ชั่นการเข้าชมซ้ำแต่ละโหนดตามประเภทของโหนดที่เรากำลังเผชิญอยู่

ลองเพิ่มตัวอย่างที่ซับซ้อนมากขึ้นifสร้างข้อความสั่ง! จำได้ว่าถ้างบสามารถมีส่วนคำสั่งอื่นก็ได้ มาเพิ่มคำสั่ง if-else เข้ากับโครงสร้างโหนดเดิมของเรา โปรดจำไว้ว่าหากข้อความเหล่านั้นสามารถมีได้หากข้อความดังนั้นการเรียกซ้ำภายในระบบโหนดของเราจึงอาจเกิดขึ้นได้ คำสั่งอื่นเป็นทางเลือกดังนั้นelsestmtฟิลด์สามารถเป็นค่า NULL ซึ่งฟังก์ชันผู้เยี่ยมชมซ้ำสามารถละเว้นได้

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;
};

ย้อนกลับไปในฟังก์ชั่นการพิมพ์ผู้เยี่ยมชมโหนดของเราที่เรียกว่าAST_PrintNodeเราสามารถรองรับifคำสั่ง AST สร้างโดยการเพิ่มรหัส C นี้:

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;

ง่ายเหมือนที่! โดยสรุปต้นไม้ Syntax ไม่ได้เป็นอะไรมากไปกว่าแผนผังของสหภาพที่ติดแท็กของต้นไม้และข้อมูลของมันเอง!

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.