1

I've been learning how to make an AST with Antlr4's visitor and after reading Terrance Parr's book, and multiple forums on the topic of AST generation specifically with Antlr visitors, it seems that the standard approach for doing this involves overriding the Antlr generated visit methods like this (From The Definitive Antlr 4 Reference).

public static class EvalVisitor extends LExprBaseVisitor<Integer> {
    public Integer visitMult(LExprParser.MultContext ctx) {
        return visit(ctx.e(0)) * visit(ctx.e(1));
    }
    public Integer visitAdd(LExprParser.AddContext ctx) {
        return visit(ctx.e(0)) + visit(ctx.e(1));
    }
    public Integer visitInt(LExprParser.IntContext ctx) {
        return Integer.valueOf(ctx.INT().getText());
    }
}

In this book, and many threads, typically the way to approach overriding the base visitor is to call visit() within the visitor itself. Seemingly, the visitor itself governs the traversal of the parse tree.

However, when I look anywhere else about how the visitor pattern is typically implemented, the visit method almost never calls visit() within itself. Usually the accept method is used within a node or element to govern the traversal of a tree structure and calls accept on the node's children to do so, while the visitor mainly handles what operations occur for that particular visit, on that particular tree's node.

Wikipedia Example, but reduced for most important parts

public class ExpressionPrintingVisitor
{
    public void PrintAddition(Addition addition)
    {
        double leftValue = addition.Left.GetValue();
        double rightValue = addition.Right.GetValue();
        var sum = addition.GetValue();
        Console.WriteLine("{0} + {1} = {2}", leftValue, rightValue, sum);
    }
}

public class Addition : Expression
{
    public Expression Left { get; set; }
    public Expression Right { get; set; }

    public Addition(Expression left, Expression right)
    {
        Left = left;
        Right = right;
    }
    
    public override void Accept(ExpressionPrintingVisitor v)
    {
        Left.Accept(v);
        Right.Accept(v);
        v.PrintAddition(this);
    }
    
    public override double GetValue()
    {
        return Left.GetValue() + Right.GetValue();    
    }
}

In this particular example, PrintAddition() is the "visit()" method being called by accept to perform the operation.

Am I misunderstanding the visitor pattern? Is the antlr4 visitor not a standard visitor? Or is more happening under the hood of the Antlr visitor that I'm not understanding. To me, the simplified description of the visitor pattern implementation is using an "accept" method on a node's children to traverse a tree, while calling the visitor to perform operations on the children of that node.

I appreciate any help on the topic, and apologize if anything is unclear.

  • Would it help you to know that the `visit` method is defined simply as `return tree.accept(this);`? – sepp2k Feb 27 '22 at 10:34
  • 1
    On an unrelated note, that Wikipedia example is horrible. There's no `Visitor` interface and instead it hard-codes knowledge of printing into `Expression`'s `Accept` method. *That* is something that's definitely not a proper implementation of the visitor pattern. – sepp2k Feb 27 '22 at 10:34
  • Interesting. So the visit method itself is more or less just a call to accept, while the override visitMult is the true visit method? That wiki example is definitely not great, but it was just the only one that I could find off hand that I was able to copy and paste. But the main idea I guess I wanted to show is that I typically see accept call visit, while visit just performs an operation of some kind. Either way, it seems strange to me that the Antlr visitor would return a call to accept, rather than just returning some value to the accept function where visit was initially called. – chickenslips Feb 27 '22 at 19:53
  • Since accept is typically used to govern traversal, while visit performs some action. It seems strange to govern traversal within the visit itself. Or am I thinking about this wrong? – chickenslips Feb 27 '22 at 19:57
  • "Either way, it seems strange to me that the Antlr visitor would return a call to accept, rather than just returning some value to the accept function where visit was initially called" I'm not sure what you mean by that. The return statement is there because some times (as in this example), it's useful to have return values. In a "pure" implementation, the visit methods (both `visit` and `visitFoo`) would be `void` and the implementation of `visit` would just be `tree.accept(this)` without the `return` keyword, but that doesn't really change much. – sepp2k Feb 27 '22 at 20:05
  • "Since accept is typically used to govern traversal, while visit performs some action" I wouldn't say "typically". The GoF book mentions both ways, but when it comes to compilers and interpreters, you usually want control over when, whether and how often your children are traversed. When you don't want that, you can use listeners in ANTLR (which at least still give you control over whether your actions run before or after visiting the children). – sepp2k Feb 27 '22 at 20:05

1 Answers1

2

Along with the comments that mention that the visit method is really a convenience wrapper around tree.accept(this), you may want to take a look at AbstractParseTreeVisitor. The default implementation of visitChildren does, indeed take care of recursively navigating your parse tree calling the visit*(ctx) methods as appropriate. So you can have a Visitor<T> class that does not necessarily override all of the visit*(cox) methods and let's the Visitor take care of navigating the sub-tree.

That said, if you want to have the ANTLR runtime completely handle the navigation for you, then the *Listener gives you that capability (It doesn't return a value like a *Visitor does, but that is more appropriate for traversing an entire parse tree).

We tend to prefer *Visitors when we want to obtain a value from traversing a sub-tree, and/or when we want to do something like an interpreter. If you're implementing an interpreter, then you certainly want to take control of which children are visited (to handle conditional branching) as well as how many times children are visited (for iteration).

Mike Cargal
  • 6,610
  • 3
  • 21
  • 27
  • Thank you for the response! I am currently building a java-like compiler for my undergrad capstone. I've only had one programming patterns class, so I'm really trying to make sure that I understand what I'm doing by using the visitor. After reading the replies on this page, it seems that there's definitely multiple ways to implement a visitor, and that in Antlr's case, managing traversal within the visitor itself is okay. In terms of managing traversal, I'll take a closer look at using visitChildren. Thanks for the help :) – chickenslips Feb 28 '22 at 19:13
  • yeah, also good to remember that, while it "Gang of Four" book is quite good, it's not "divinely inspired and infallible". They're just recognized, common, patterns found in many good OO programs. – Mike Cargal Feb 28 '22 at 19:15