tomashauser
tomashauser

Reputation: 571

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?

Upvotes: 2

Views: 331

Answers (1)

tomashauser
tomashauser

Reputation: 571

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

Upvotes: 2

Related Questions