Як саме створюється абстрактне синтаксичне дерево?


47

Я думаю, що я розумію ціль AST, і раніше я побудував пару деревних структур, але ніколи AST. Я в основному плутаюся, оскільки вузли - це текст, а не число, тому я не можу придумати приємний спосіб ввести маркер / рядок, коли я аналізую якийсь код.

Наприклад, коли я дивився на діаграми AST, змінна та її значення були вузлами листків до знаку рівності. Це має для мене ідеальний сенс, але як би я пішов на реалізацію цього? Я думаю, що я можу це робити окремо, так що коли я натрапляю на "=", я використовую це як вузол, і додаю значення, розібране перед "=" як аркуш. Це просто здається неправильним, тому що, мабуть, мені доведеться складати справи для тонн і тонн речей, залежно від синтаксису.

І тоді я зіткнувся з іншою проблемою, як перетинається дерево? Чи я пройдусь вниз по висоті і повернуся вгору по вузлу, коли натиснув на дно, і зроби те саме для свого сусіда?

Я бачив багато діаграм на AST, але не зміг знайти досить простий приклад одного в коді, який, мабуть, допоможе.


Ключова концепція, яку вам не вистачає, - це рекурсія . Рекурсія є якоюсь контрінтуїтивною, і для кожного учня вона різна, коли вона нарешті буде "клацати" з ними, але без рекурсії просто немає способу зрозуміти аналіз (і ще безліч інших обчислювальних тем).
Кіліан Фот

Я отримую рекурсію, я просто думав, що буде важко реалізувати це в цій справі. Я насправді хотів використати рекурсію, і я закінчив багато випадків, які не працювали б для загального рішення. Відповідь Гдворда зараз мені дуже допомагає.
Howcan

Це може бути вправою побудувати калькулятор RPN як вправу. Він не відповість на ваше запитання, але може навчити деяким необхідним навичкам.

Я фактично раніше побудував калькулятор RPN. Відповіді мені дуже допомогли, і я думаю, що зараз можу зробити базовий AST. Дякую!
Howcan

Відповіді:


47

Коротка відповідь - ви використовуєте стеки. Це хороший приклад, але я застосую його до AST.

FYI, це алгоритм Енджера Дайкстри « Мансардно-ярдовий» .

У цьому випадку я буду використовувати стек операторів і стек виразів. Оскільки числа вважаються виразами на більшості мов, я буду використовувати стек виразів для їх зберігання.

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, було б надзвичайно просто додати можливості для операторів справа-наліво. Найпростіше було б додати таблицю пошуку, а якщо оператор справа наліво, просто змінити порядок операндів.
Гевін Говард

4
@Simon Якщо ви хочете підтримати і те, і інше, вам краще шукати алгоритм маневрових дворів у повній красі. Що стосується алгоритмів, то це абсолютний злом.
biziclop

19

Існує значна різниця між тим, як типово зображується AST у тесті (дерево з цифрами / змінними на вузлах листків та символами на внутрішніх вузлах) та тим, як він реально реалізований.

Типова реалізація AST (мовою ОО) дуже сильно використовує поліморфізм. Вузли в AST, як правило, реалізуються з різними класами, всі похідні від загального ASTNodeкласу. Для кожної синтаксичної конструкції на мові, яку ви обробляєте, буде клас для представлення цієї конструкції в AST, такий як ConstantNode(для констант, таких як 0x10або 42), VariableNode(для імен змінних), AssignmentNode(для операцій з присвоєнням), ExpressionNode(для загальних вирази) тощо.
Кожен конкретний тип вузла визначає, чи є у цього вузла діти, скільки та можливо того типу. Як ConstantNodeправило, дітей не AssignmentNodeбуде, у двох буде, а у дітей ExpressionBlockNodeможе бути будь-яка кількість дітей.

AST будується аналізатором, який знає, яку конструкцію він щойно проаналізував, тому він може сконструювати правильний вид AST Node.

Під час обходу АСТ поліморфізм вузлів дійсно грає. База ASTNodeвизначає операції, які можна виконати над вузлами, і кожен конкретний тип вузла реалізує ці операції певним чином для цієї конкретної мовної конструкції.


9

Побудова AST з вихідного тексту - це "просто" розбір . Як саме це буде зроблено, залежить від розібраної формальної мови та реалізації. Ви можете використовувати генератори парсера, такі як menhir (для Ocaml) , GNU bisonз flex, або ANTLR тощо. Це часто робиться "вручну", кодуючи деякий рекурсивний аналізатор спуску (див. Цю відповідь, пояснюючи чому). Контекстуальний аспект розбору часто робиться в іншому місці (таблиці символів, атрибути, ....).

Однак на практиці AST набагато складніше, ніж те, в що ви вірите. Наприклад, у компіляторі на зразок GCC AST зберігає інформацію про вихідне місце розташування та деяку інформацію для друку. Прочитайте про родові дерева в GCC та загляньте всередину його gcc / tree.def . До речі, також загляньте всередину GCC MELT (яку я розробив та впровадив), це відповідає вашому питанню.


Я роблю інтерпретатора Lua для розбору вихідного тексту та перетворення масиву в JS. Чи можу я вважати це AST? Я повинен зробити щось подібне: --My comment #1 print("Hello, ".."world.") перетворюється на `[{" type ":" - "," content ":" Мій коментар №1 "}, {" type ":" call "," name ":" print "," аргументи ": [[{" type ":" str "," action ":" .. "," content ":" Привіт ",}, {" type ":" str "," content ": "світ". }]]}] `Я думаю, що в JS це набагато простіше, ніж будь-яка інша мова!
Hydroper

@TheProHands Це вважатиметься лексемами, а не AST.
YoYoYonnY

2

Я знаю, що це питання віком 4+ років, але я думаю, що мені слід додати більш детальну відповідь.

Абстрактні синтаксичні дерева створені нічим не відрізняються від інших дерев; Більш істинне твердження в цьому випадку полягає в тому, що вузли синтаксичного дерева мають різну кількість вузлів, як це потрібно.

Прикладом є бінарні вирази на зразок 1 + 2 Простий вираз, подібний до того, який би створив єдиний кореневий вузол, утримуючи правий і лівий вузол, який містить дані про числа. На мові С це виглядатиме щось на кшталт

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;

Так просто! На закінчення, Синтаксичне дерево - це не набагато більше, ніж дерево позначеного об'єднання дерева та саме його даних!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.