추상 구문 트리는 정확히 어떻게 생성됩니까?


47

나는 AST의 목표를 이해하고 있다고 생각하며, 전에 몇 가지 트리 구조를 구축했지만 결코 AST는 아닙니다. 노드가 텍스트가 아니고 숫자가 아니기 때문에 혼란 스럽습니다. 그래서 코드를 파싱 할 때 토큰 / 문자열을 입력하는 좋은 방법을 생각할 수 없습니다.

예를 들어, AST의 다이어그램을 볼 때 변수와 값은 등호의 리프 노드였습니다. 이것은 나에게 완벽한 의미가 있지만 이것을 어떻게 구현할 것인가? 사례별로 수행 할 수 있으므로 "="를 우연히 발견 할 때 노드로 사용하고 "="앞에 구문 분석 된 값을 리프로 추가합니다. 구문에 따라 아마도 톤과 톤을 처리해야하기 때문에 잘못 된 것 같습니다.

그리고 또 다른 문제가 생겼습니다. 나무는 어떻게 지나가나요? 높이를 줄이려고하고 바닥에 닿았을 때 노드를 다시 올라가서 이웃 노드에 대해서도 동일하게합니까?

AST에서 수많은 다이어그램을 보았지만 코드에서 상당히 간단한 예제를 찾을 수 없었으므로 도움이 될 것입니다.


빠진 핵심 개념은 재귀 입니다. 재귀는 일종의 반 직관적이며, 학습자와 함께 '클릭'할 때 모든 학습자마다 다릅니다. 그러나 재귀가 없으면 구문 분석을 이해할 수있는 방법이 없습니다 (그리고 다른 많은 계산 주제도).
— Kilian Foth

재귀를 얻었습니다.이 경우에는 구현하기가 어렵다고 생각했습니다. 실제로 재귀를 사용하고 싶었고 일반적인 솔루션으로는 작동하지 않는 많은 경우가 생겼습니다. Gdhoward의 답변은 현재 많은 도움이되고 있습니다.
— Howcan

RPN 계산기 를 연습으로 만드는 것이 운동 일 수 있습니다 . 귀하의 질문에 대답하지는 않지만 필요한 기술을 가르 칠 수 있습니다.

실제로 전에 RPN 계산기를 만들었습니다. 대답은 많은 도움이되었고 지금은 기본적인 AST를 만들 수 있다고 생각합니다. 감사!
— Howcan

답변:


47

짧은 대답은 스택을 사용한다는 것입니다. 이것은 좋은 예이지만 AST에 적용하겠습니다.

참고로, 이것은 Edsger Dijkstra의 Shunting-Yard 알고리즘 입니다.

이 경우 연산자 스택과 식 스택을 사용합니다. 숫자는 대부분의 언어에서 표현식으로 간주되므로 표현식 스택을 사용하여 저장합니다.

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에 대한 코드를 생성하려면 Post Order Tree Traversal을 수행하십시오 . 리프 노드 (숫자 포함)를 방문하면 컴파일러에서 피연산자 값을 알아야하므로 상수가 생성됩니다. 연산자가있는 노드를 방문하면 연산자에서 적절한 명령어를 생성합니다. 예를 들어, '+'연산자는 "add"명령어를 제공합니다.


이것은 오른쪽에서 왼쪽이 아니라 왼쪽에서 오른쪽 연관성을 가진 연산자에 효과적입니다.
— Simon

@Simon, 오른쪽에서 왼쪽으로 사용할 수있는 기능을 추가하는 것은 매우 간단합니다. 가장 간단한 방법은 찾아보기 테이블을 추가하는 것이며 연산자가 오른쪽에서 왼쪽 인 경우 피연산자의 순서를 반대로 바꿉니다.
— Gavin Howard

4
@Simon 두 가지를 모두 지원하려면 분로 장 알고리즘 을 최대한 활용 하는 것이 좋습니다 . 알고리즘이 진행됨에 따라 절대 크래커입니다.
— biziclop

19

테스트에서 AST가 일반적으로 묘사되는 방식 (리프 노드에 숫자 / 변수가있는 트리와 내부 노드에 기호가 있음)과 실제로 구현되는 방법 사이에는 큰 차이가 있습니다.

OO (OO 언어로)의 일반적인 구현은 다형성을 많이 사용합니다. AST의 노드는 일반적으로 공통 ASTNode클래스 에서 파생 된 다양한 클래스로 구현됩니다 . 당신이 처리하는 언어의 각 구문 구조의 경우, 같은 AST에서 그 구조, 표현하기위한 클래스가있을 것입니다 ConstantNode(예 : 상수, 대한 0x10나 42), VariableNode(변수 이름), AssignmentNode(할당 작업을), ExpressionNode일반에 대한 ( 표현) 등
해당 노드가 아이를 가지고있는 경우 각각의 특정 노드 유형이 지정 얼마나 많은 가능성이 유형의. A ConstantNode는 일반적으로 자녀가 없으며, AssignmentNode유언장에는 2 명의 자녀 가 있으며 , 자녀 ExpressionBlockNode수는 얼마든지있을 수 있습니다.

AST는 구문 분석기에 의해 작성되며, 파서가 구문 분석 한 구문을 알고 있으므로 올바른 종류의 AST 노드를 구성 할 수 있습니다.

AST를 통과 할 때 노드의 다형성이 실제로 작동합니다. 기본 ASTNode은 노드에서 수행 할 수있는 작업을 정의하며 각 특정 노드 유형은 해당 언어 구성에 대해 특정 방식으로 해당 작업을 구현합니다.


9

소스 텍스트에서 AST를 빌드하는 것은 "간단하게" 구문 분석 됩니다. 정확히 어떻게 수행되는지는 구문 분석 된 공식 언어와 구현에 따라 다릅니다. menhir (Ocaml의 경우) , GNU bisonwith flex또는 ANTLR 등과 같은 파서 생성기를 사용할 수 있습니다 . 종종 재귀 적 강하 파서 를 코딩하여 "수동으로"수행됩니다 ( 이 이유를 설명하는 이 답변 참조 ). 구문 분석의 문맥 적 측면은 종종 다른 곳 (기호 테이블, 속성 등)에서 수행됩니다.

그러나 실제로 AST는 귀하가 생각하는 것보다 훨씬 복잡합니다. 예를 들어 GCC 와 같은 컴파일러 에서 AST는 소스 위치 정보와 일부 입력 정보를 유지합니다. GCC의 일반 트리 에 대해 읽고 gcc / tree.def를 살펴 보십시오 . BTW, GCC MELT (내가 디자인하고 구현 한) 내부를 살펴보십시오 . 귀하의 질문과 관련이 있습니다.


소스 텍스트를 구문 분석하고 JS의 배열로 변환하기 위해 Lua 인터프리터를 만들고 있습니다. AST라고 생각할 수 있습니까? 나는 다음과 같이해야한다 : --My comment #1 print("Hello, ".."world.") `[{ "type": "-", "content": "My comment # 1"}, { "type": "call", "name": " print ","arguments ": [[{"type ":"str ","action ":".. ","content ":"Hello, "}, {"type ":"str ","content ": "세계." }]]}]`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 절을 ​​가질 수 있습니다. if-else 문을 원래 노드 구조에 추가합시다. if 문 자체에도 if 문이있을 수 있으므로 노드 시스템 내에서 일종의 재귀가 발생할 수 있습니다. 다른 명령문은 선택 사항이므로 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;
};

라는 노드 방문자 인쇄 함수로 돌아가서 다음 C 코드를 추가하여 AST 구문 문을 AST_PrintNode수용 할 수 있습니다 if.

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 Tree는 그 나무와 그 데이터 자체의 결합 된 태그의 나무에 지나지 않습니다!

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.