Search code examples
javaantlr4visitor-pattern

How to implement a shared visitor for two similar ANTLR4 grammars?


Let's have two grammars:

grammar Grammar1;

NUMBER : [0-9]+ ;

WS : [ \r\n\t]+ -> skip ;

root : expr EOF
     ;

expr : '*' expr expr    # Multiplication
     | expr '+' expr    # Addition
     | NUMBER           # Number
     ;


grammar Grammar2;

NUMBER : [0-9]+ ;

WS : [ \r\n\t]+ -> skip ;

root : expr EOF
     ;

expr : NUMBER '!'           # Factorial
     | NUMBER '^' NUMBER    # Exponentiation
     | expr '+' expr        # Addition
     | NUMBER               # Number
     ;

We can see that they both share a number and addition while everything else is different.

The two convertors into an Expression AST will look like this:

public class Grammar1Converter extends Grammar1BaseVisitor<Expression> {
    public Expression visitRoot(Grammar1Parser.RootContext ctx);
    
    public Expression visitMultiplication(Grammar1Parser.MultiplicationContext ctx);
    
    public Expression visitNumber(Grammar2Parser.NumberContext ctx);
    
    public Expression visitAddition(Grammar1Parser.AdditionContext ctx);
}

public class Grammar2Converter extends Grammar2BaseVisitor<Expression> {
    public Expression visitRoot(Grammar2Parser.RootContext ctx);

    public Expression visitFactorial(Grammar2Parser.FactorialContext ctx);

    public Expression visitExponentiation(Grammar2Parser.ExponentiationContext ctx);

    public Expression visitNumber(Grammar2Parser.NumberContext ctx);

    public Expression visitAddition(Grammar2Parser.AdditionContext ctx);
}

Both grammars share the structure they're converted into. How can we implement a shared converter for the visitNumber and visitAddition? Both converters already extend an abstract class so the basic inheritance is out of question. Both convertors have a different context type in the argument so we can't just have a general visitor for the shared method as we would have to convert between the contexts. Is there a way of avoiding repeating the code?


Solution

  • As it's been pointed out in the comments, we can make use of the hierarchies and interfaces we're given by antlr.

    We'll make a new utility class that'll take care of the shared functionality (you can use generic types if you want).

    public final class SharedGrammerConverter() { ... }
    

    For the leaf nodes we won't need to pass the visitor:

    // SharedGrammarConverter
    public static Expression visitNumber(ParserRuleContext ctx) {
        return new Number(Integer.parseInt(ctx.getChild(0).getText()));
    }
    
    // Grammar1Converter
    public Expression visitNumber(Grammar1Parser.NumberContext ctx) {
        return SharedGrammarConverter.visitNumber(ctx);
    }
    
    // Grammar2Converter
    public Expression visitNumber(Grammar2Parser.NumberContext ctx) {
        return SharedGrammarConverter.visitNumber(ctx);
    }
    

    If we need to visit the subtrees, we'll pass the visitor as well:

    // SharedGrammarConverter
    public static Expression visitAddition(ParserRuleContext ctx,
                                           AbstractParseTreeVisitor<Expression> visitor) {
        Expression left = visitor.visit(ctx.getChild(0));
        Expression right = visitor.visit(ctx.getChild(2));
        return new Addition(left, right);
    }
    
    // Grammar1Converter
    public Expression visitAddition(Grammar1Parser.AdditionContext ctx) {
        return SharedGrammarConverter.visitAddition(ctx, this);
    }
    
    // Grammar2Converter
    public Expression visitAddition(Grammar2Parser.AdditionContext ctx) {
        return SharedGrammarConverter.visit(ctx, this);
    }