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


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(예 : 상수, 대한 0x1042), 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.