Soyut bir sözdizimi ağacı tam olarak nasıl oluşturulur?


47

Bir AST'nin amacını anladığımı düşünüyorum ve daha önce hiç AST olmayan birkaç ağaç yapısı inşa ettim. Çoğunlukla kafam karıştı, çünkü düğümler metin değil sayı değil, bu yüzden bazı kodları ayrıştırırken bir belirteç / string girmek için iyi bir yol düşünemiyorum.

Örneğin, AST'nin diyagramlarına baktığımda, değişken ve değeri yaprak düğümleri eşit bir işarete sahipti. Bu bana çok mantıklı geliyor, ama bunu nasıl uygulayacağım? Sanırım duruma göre yapabilirim, böylece bir "=" üzerine rastladığımda bunu bir düğüm olarak kullanıyorum ve "=" den önce ayrıştırılan değeri yaprak olarak eklerim. Sadece yanlış görünüyor, çünkü muhtemelen sentaksına bağlı olarak tonlarca ve tonlarca şey için dava açmak zorunda kalacağım.

Sonra başka bir problemle karşılaştım, ağaç nasıl geçiyor? Ben aşağıdan aşağıya doğru gidip, dibe vurduğumda bir düğüme gidip komşusu için de aynısını mı yapıyorum?

AST'lerde tonlarca diyagram gördüm, ancak kodda oldukça basit bir örnek bulamadım, bu muhtemelen yardımcı olacak.


Kaçırdığınız anahtar kavram, özyinelemedir . Özyineleme bir tür karşı-sezgiseldir ve nihayet onlarla 'tık' olacağı her öğrenci için farklıdır, ancak özyineleme olmadan ayrıştırmayı (ve bir sürü başka hesaplama konusunu) anlamanın hiçbir yolu yoktur.
Kilian Foth,

Özyineleme alıyorum, sadece bu durumda uygulamanın zor olacağını düşündüm. Aslında özyinelemeyi kullanmak istedim ve genel bir çözüm için işe yaramayacak pek çok vaka ile sonuçlandım. Gdhoward'ın cevabı şu an bana çok yardımcı oluyor.
Ağustos'ta

Egzersiz olarak bir RPN hesap makinesi oluşturmak alıştırması olabilir. Sorunuza cevap vermeyecek ancak bazı gerekli becerileri öğretebilir.

Aslında daha önce bir RPN Hesap Makinesi kurdum. Cevaplar bana çok yardımcı oldu ve şimdi temel bir AST yapabilirim. Teşekkürler!
Saat

Yanıtlar:


47

Kısa cevap, yığınları kullanmanızdır. Bu iyi bir örnek, ancak bunu bir AST'ye uygulayacağım.

Bilginize, bu Edsger Dijkstra'nın Shunting-Yard Algoritması .

Bu durumda, bir işleç yığını ve bir ifade yığını kullanacağım. Sayılar çoğu dilde ifadeler olarak kabul edildiğinden, bunları depolamak için ifade yığınını kullanacağım.

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

(Lütfen kodum konusunda iyi olun. Bunun sağlam olmadığını biliyorum; yalnızca sahte kod olması gerekiyordu.)

Her neyse, koddan da görebileceğiniz gibi, isteğe bağlı ifadeler diğer ifadelere işlenen olabilir. Aşağıdaki girişe sahipseniz:

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

Yazdığım kod bu AST'yi üretecekti:

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

Ve sonra bu AST'nin kodunu üretmek istediğinizde, bir Post Order Tree Geçişi yaparsınız . Bir yaprak düğümünü ziyaret ettiğinizde (bir sayı ile), bir sabit üretersiniz, çünkü derleyicinin işlenen değerlerini bilmesi gerekir. Bir operatöre sahip bir düğümü ziyaret ettiğinizde, operatörden uygun talimatı verirsiniz. Örneğin, '+' operatörü size bir "ekle" talimatı verir.


Bu, sağdan sola değil, soldan sağa ilişkilendirme yapan operatörler için işe yarar.
Simon

@Simon, sağdan sola operatörler için yetenek eklemek için çok basit olurdu. En basit olanı bir arama tablosu eklemek ve sağdan sola bir işleç ise, işlenenlerin sırasını tersine çevirmek olacaktır.
Gavin Howard,

4
@Simon Her ikisini de desteklemek istiyorsanız, şantiye bahçesinde algoritmasını tam olarak görerek tamamlamanız iyi olur . Algoritmalar giderken, bu mutlak bir kraker.
biziclop

19

Bir AST'nin testte tipik olarak nasıl gösterildiği (iç kısımlardaki yaprak düğümlerinde sayı / değişken olan bir ağaç) ve iç düğümlerdeki semboller arasında anlamlı bir fark vardır.

Bir AST'nin (OO dilinde) tipik bir uygulaması, polimorfizmin yoğun şekilde kullanılmasını sağlar. AST'deki düğümler tipik olarak hepsi ortak bir ASTNodesınıftan türeyen çeşitli sınıflarla uygulanır . İşlediğiniz dildeki her sözdizimsel yapı için, AST'de bu yapıyı temsil etmek için bir sınıf olacaktır ConstantNode(örneğin, 0x10veya gibi sabitler için 42), VariableNode(değişken isimleri için), AssignmentNode(atama işlemleri için), ExpressionNode(genel için ifadeler), vb.
Her belirli düğüm türü, bu düğümün çocuk sahibi olup olmadığını, ne kadar ve muhtemelen ne tür olduğunu belirtir. Bir ConstantNodevasiyetin çocuğu olmaz, bir AssignmentNodevasiyetin iki çocuğu olur ve ExpressionBlockNodeherhangi bir sayıda çocuğa sahip olabilir.

AST ayrıştırıcı tarafından inşa edilir, hangi yapının henüz ayrıştırıldığını bilir, böylece doğru AST Düğümü türünü oluşturabilir.

AST'yi geçerken, düğümlerin polimorfizmi gerçekten devreye giriyor. Temel ASTNode, düğümlerde gerçekleştirilebilecek işlemleri tanımlar ve her bir düğüm türü bu işlemleri o dil için özel bir şekilde uygular.


9

AST'yi kaynak metinden inşa etmek "basit" bir çözümlemedir . Tam olarak nasıl yapıldığı, ayrıştırılmış biçimsel dile ve uygulamaya bağlıdır. Menhir (Ocaml için) , GNU bisonile flexveya ANTLR vb. Gibi ayrıştırıcı jeneratörleri kullanabilirsiniz . Genellikle yinelemeli iniş ayrıştırıcıları kodlayarak "elle" yapılır ( nedenini açıklayan bu cevaba bakınız ). Ayrıştırmanın bağlamsal yönü genellikle başka yerlerde yapılır (sembol tabloları, özellikler, ....).

Bununla birlikte, pratikte AST inandığınızdan çok daha karmaşıktır. Örneğin, GCC gibi bir derleyicide AST kaynak konum bilgisini ve bazı yazım bilgilerini tutar. GCC'deki Genel Ağaçlar hakkında bilgi edinin ve gcc / tree.def dosyasının içine bakın . BTW, ayrıca tasarladığım ve uyguladığım GCC MELT içine de bakın , bu sorunuzla alakalı.


Kaynak metni ayrıştırmak ve JS'deki bir dizide dönüştürmek için bir Lua tercümanı yapıyorum. Bunu bir AST olarak görebilir miyim? Böyle bir şey yapmam gerekiyor: --My comment #1 print("Hello, ".."world.") dönüştürür `[{" type ":" - "," content ":" Yorumum # 1 "}, {" type ":" call "," name ":" print "," arguments ": [[{" tür ":" str "," eylem ":" .. "," içerik ":" Merhaba "}, {" tür ":" str "," içerik ": "dünya." }]]}] `Bence JS’de diğer dillerden çok daha kolay!
Hydroper

@ TheProHands Bu AST değil, belirteç olarak kabul edilir.
YoYoYonnY

2

Bu sorunun 4 yaşından büyük olduğunu biliyorum ama daha ayrıntılı bir cevap eklemem gerektiğini düşünüyorum.

Soyut Sözdizimi Ağaçları diğer ağaçlardan farklı olarak yaratılmaz; bu durumda daha doğru olan ifade, Sözdizimi Ağacı düğümlerinin, AS İHTİYACI OLARAK değişken miktarda düğümlere sahip olmasıdır.

Örnek, şöyle bir ikili ifadelerdir. Bunun gibi 1 + 2 basit bir ifade, sayılarla ilgili verileri tutan sağ ve sol düğümü tutan tek bir kök düğüm oluşturur. C dilinde, bunun gibi bir şey olurdu

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

Sorunuz da nasıl geçileceği idi? Bu durumda Traversing Ziyaret Düğümleri denir . Her Düğümü ziyaret etmek, her bir Sözdizimi düğümü verilerinin nasıl değerlendirileceğini belirlemek için her düğüm türünü kullanmanızı gerektirir.

İşte C'deki her bir düğümün içeriğini basmaya başladığım başka bir örnek:

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

Hangi düğümle uğraştığımıza göre, işlevin her düğümü tekrar tekrar ziyaret ettiğine dikkat edin.

Daha karmaşık bir örnek, bir ififade yapısı ekleyelim ! İfadelerin isteğe bağlı başka bir cümlecik olabileceğini de hatırlayın. İf-else ifadesini orijinal düğüm yapımıza ekleyelim. Eğer ifadelerin kendileri de eğer ifadelerde olabilirse, düğüm sistemimizde bir tür özyinelemenin oluşabileceğini unutmayın. Başka ifadeler isteğe bağlıdır, bu nedenle elsestmtözyinelemeli ziyaretçi işlevinin göz ardı edebileceği NULL olabilir.

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

geri çağrılan düğüm ziyaretçi baskısı işlevimize geri AST_PrintNodedönersek, ifbu C kodunu ekleyerek AST yapı ifadesini düzenleyebiliriz :

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;

Kadar basit! Sonuç olarak, Sözdizimi Ağacı, etiketli bir birliğin ağacından ve verilerinin kendisinden çok daha fazlası değildir!

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.