Implementazione del modello di visitatore per un albero di sintassi astratto


23

Sono in procinto di creare il mio linguaggio di programmazione, che faccio a fini di apprendimento. Ho già scritto il lexer e un parser di discendenza ricorsiva per un sottoinsieme della mia lingua (attualmente supporto espressioni matematiche, come + - * /parentesi). Il parser mi restituisce un albero di sintassi astratto, sul quale chiamo il Evaluatemetodo per ottenere il risultato dell'espressione. Tutto funziona bene. Ecco approssimativamente la mia situazione attuale (esempi di codice in C #, sebbene questo sia praticamente indipendente dal linguaggio):

public abstract class Node
{
    public abstract Double Evaluate();
}

public class OperationNode : Node
{
    public Node Left { get; set; }
    private String Operator { get; set; }
    private Node Right { get; set; }

    public Double Evaluate()
    {
        if (Operator == "+")
            return Left.Evaluate() + Right.Evaluate();

        //Same logic for the other operators
    }
}

public class NumberNode : Node
{
    public Double Value { get; set; }

    public Double Evaluate()
    {
        return Value;
    }
}

Tuttavia, desidero disaccoppiare l'algoritmo dai nodi dell'albero perché voglio applicare il principio aperto / chiuso, quindi non devo riaprire tutte le classi di nodi quando voglio implementare la generazione di codice, ad esempio. Ho letto che il modello visitatore è buono per questo. Ho una buona comprensione di come funziona il modello e che l'uso della doppia spedizione è la strada da percorrere. Ma a causa della natura ricorsiva dell'albero, non sono sicuro di come dovrei affrontarlo. Ecco come sarebbe il mio visitatore:

public class AstEvaluationVisitor
{
    public void VisitOperation(OperationNode node)
    {
        // Here is where I operate on the operation node.
        // How do I implement this method?
        // OperationNode has two child nodes, which may have other children
        // How do I work the Visitor Pattern around a recursive structure?

        // Should I access children nodes here and call their Accept method so they get visited? 
        // Or should their Accept method be called from their parent's Accept?
    }

    // Other Visit implementation by Node type
}

Quindi questo è il mio problema. Voglio affrontarlo immediatamente mentre la mia lingua non supporta molte funzionalità per evitare di avere un problema più grande in seguito.

Non l'ho pubblicato su StackOverflow perché non voglio che tu fornisca un'implementazione. Voglio solo che tu condivida idee e concetti che potrei aver perso, e come dovrei affrontarlo.


1
Probabilmente implementerei invece una piega ad albero
jk.

@jk .: ti dispiacerebbe elaborare un po '?
marco-fiset,

Risposte:


10

Spetta all'implementazione del visitatore decidere se visitare i nodi figlio e in quale ordine. Questo è il punto centrale del modello di visitatore.

Per adattare il visitatore a più situazioni è utile (e abbastanza comune) usare generici come questo (è Java):

public interface ExpressionNodeVisitor<R, P> {
    R visitNumber(NumberNode number, P p);
    R visitBinary(BinaryNode expression, P p);
    // ...
}

E un acceptmetodo sarebbe simile al seguente:

public interface ExpressionNode extends Node {
    <R, P> R accept(ExpressionNodeVisitor<R, P> visitor, P p);
    // ...
}

Ciò consente di passare parametri aggiuntivi al visitatore e di recuperarne un risultato. Quindi, la valutazione dell'espressione può essere implementata in questo modo:

public class EvaluatingVisitor
    implements ExpressionNodeVisitor<Double, Void> {
    public Double visitNumber(NumberNode number, Void p) {
        // Parse the number and return it.
        return Double.valueOf(number.getText());
    }
    public Double visitBinary(BinaryNode binary, Void p) {
        switch (binary.getOperator()) {
        case '+':
            return binary.getLeftOperand().accept(this, p)
                + binary.getRightOperand().accept(this, p);
        // More cases for other operators here.
        }
    }
}

Il acceptparametro method non è usato nell'esempio sopra, ma credimi: è abbastanza utile averne uno. Ad esempio, può essere un'istanza del Logger a cui segnalare errori.


Ho finito per implementare qualcosa di simile e sono molto soddisfatto del risultato finora. Grazie!
marco-fiset,

6

In precedenza ho implementato il modello di visitatore su un albero ricorsivo.

La mia particolare struttura di dati ricorsivi era estremamente semplice: solo tre tipi di nodo: il nodo generico, un nodo interno che ha figli e un nodo foglia che ha dati. Questo è molto più semplice di quanto mi aspetto dal tuo AST, ma forse le idee possono ridimensionarsi.

Nel mio caso, non ho deliberatamente permesso all'Accettazione di un nodo con i bambini di chiamare Accetta sui suoi figli o di chiamare visitatore. Visita (figlio) dall'interno dell'Accettazione. È responsabilità dell'implementazione corretta del membro "Visita" del visitatore delegare Accetta i figli del nodo visitato. Ho scelto in questo modo perché volevo consentire a diverse implementazioni di Visitatori di poter decidere l'ordine di visita indipendentemente dalla rappresentazione dell'albero.

Un vantaggio secondario è che non ci sono quasi artefatti del modello Visitatore all'interno dei miei nodi dell'albero - ogni "Accetta" chiama semplicemente "Visita" sul visitatore con il tipo di calcestruzzo corretto. Ciò semplifica l'individuazione e la comprensione della logica di visita, è tutto all'interno dell'implementazione del visitatore.

Per chiarezza ho aggiunto un pseudocodice C ++. Innanzitutto i nodi:

class INode {
  public:
    virtual void Accept(IVisitor& i_visitor) = 0;
};

class NodeWithChildren : public INode {
  public:
     virtual void Accept(IVisitor& i_visitor) override {
        i_visitor.Visit(*this);
     }
     // Plus interface for getting the children, exercise for the reader ;-)
 };

 class LeafNode : public INode {
   public:
     virtual void Accept(IVisitor& i_visitor) override {
       i_visitor.Visit(*this);
     }
 };

E il visitatore:

class IVisitor {
  public:
     virtual void Visit(NodeWithChildren& i_node) = 0;
     virtual void Visit(LeafNode& i_node) = 0;
};

class ConcreteVisitor : public IVisitor
  public:
     virtual void Visit(NodeWithChildren& i_node) override {
       // Do something useful, then...
       for(Node * p_child : i_node) {
         child->Accept(*this);
       }
     }

     virtual void Visit(LeafNode& i_node) override {
        // Just do something useful, there are no children.
     }

};

1
+1 per allow different Visitor implementations to be able to decide the order of visitation. Ottima idea.
marco-fiset,

@ marco-fiset L'algoritmo (visitatore) dovrà quindi sapere come sono strutturati i dati (nodi). Ciò interromperà la separazione algoritmo-dati fornita dal modello visitatore.
B Visschers,

2
@BVisschers I visitatori implementano una funzione per ciascun tipo di nodo, in modo da sapere su quale nodo opera in un dato momento. Non rompe nulla.
marco-fiset,

3

Elabora il modello di visitatore attorno a una struttura ricorsiva nello stesso modo in cui faresti qualsiasi altra cosa con la tua struttura ricorsiva: visitando i nodi della tua struttura in modo ricorsivo.

public class OperationNode
{
    public int SomeProperty { get; set; }
    public List<OperationNode> Children { get; set; }
}

public static void VisitNode(OperationNode node)
{
    ... Visit this node

    foreach(var node in Children)
    {
         VisitNode(node);
    }
}

public static void VisitAllNodes()
{
    VisitNode(rootNode);
}

Questo può fallire per i parser se la lingua ha costrutti profondamente nidificati - può essere necessario mantenere uno stack indipendentemente dallo stack di chiamate della lingua.
Pete Kirkham,

1
@PeteKirkham: Quello dovrebbe essere un albero abbastanza profondo.
Robert Harvey,

@PeteKirkham Cosa vuoi dire che può fallire? Intendi una sorta di StackOverflowException o che il concetto non si ridimensionerà bene? Per il momento non mi interessa la performance, lo faccio solo per divertimento e apprendimento.
marco-fiset,

@ marco-fiset Sì, ottieni un'eccezione di overflow dello stack se dici, prova ad analizzare un file XML grande e profondo con un visitatore. Lo farai per la maggior parte dei linguaggi di programmazione.
Pete Kirkham,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.